寫App有一個讓人頭痛的是App大小的問題, 而這大小有部分是由App裡面所用的圖所貢獻, 為了減少這部份消耗掉的資源, 不管是用較大壓縮率的格式來壓縮圖檔, 還是其他, 大家都想盡辦法想解決這問題, 現在由於平面化的UI設計, 使得又有不錯的方法來解決這問題, 平面化UI設計的特色是大部分的圖檔都是單色而非五顏六色, 這使得用向量圖, 甚至用字型來解決這問題變得可行

免費的圖標字型(icon font)

把所有的向量圖示變成字型檔可以節省不少空間, 以流行的Font Awesome 來說, 它包含了634個圖標, 卻只佔了153KB, 這在以往可能是不到十個圖標的檔案就會達到的大小, 相較之下節省了不少空間, 像這樣開放的圖示字型, 可以找到不少:

  • Font Awesome : 蠻流行的一個開放icon組, 提供了ttf, woff等字型檔格式
  • Google material icons : Google開放源碼的免費icon組, 它不只提供Android, iOS可使用的圖檔外, 也提供了字型檔的部分, 而且它的字型檔支援了Ligatures (後面會再提到它好用的地方),這也使得它比Font Awesome來的好用
  • Weather Icons : 顧名思義, 這提供了222個可以用於表示天氣的icons, 不過對於風向的表示的部分, 它是用同一個圖示只是在web上利用css旋轉來顯示不同方向的風, 這一點應用到App上的話, 我是還沒找到比較適合表達的方式
  • Octicons : 由GitHub開源出來的圖標字型, 圖標不多, 但自己新增應該蠻方便的(自己增加svg檔用grunt去build)

除了這些之外, 應該還可以找到不少免費的圖標字型(icon font)可以用, IconFontKit這邊就列了不少(它也整合了)可用的圖標字型

使用這些, 除了可以節省app的大小, 也可以省下不少設計圖標的時間, 但也不是沒缺點, 因為是字型的關係, 它每一個icon都是對應到一個unicode字元, 這字元大多數跟icon的形狀沒關係, 也就不是那麼好對應, 通常都要查一下對照表找出字元碼

利用現成的framework整合

要在iOS上使用這些圖標字型(icon font)的方式好幾種,寫程式去load字型是一種, 當然就有不少大德, 寫好包裝可以讓你用cocoapods或是cathage直接引入, 這邊有幾個不錯的:

用這些現成的framework的好處是, 一來減去自己手動包裝字型進app的複雜度, 二來是, 這些已經幫你定義好一些對應圖標的常數, 讓你用比較方便的方式而不是記憶unicode字元來對應這些圖標

但它也是有缺點的, 大部分這些的作法都是runtime才去載入跟註冊字型, 因此你必須是在程式內設定你的UILabel, UIButton的字型, 無法事先就在Interface Builder做預覽, 所以個人比較喜歡的方式就是自己動手來

手動在xcode上使用自訂字型

自己手動加的好處就是, Interface Builder上就可以套用, 直接就可以看到結果, 但就是稍微繁瑣了一點

Interface Builder直接看結果

首先, 要把字型檔拖入你的Project裡面:

ttf files in project

接著打開Info.plist, 加上一個新的項目叫做*“Fonts provided by application”*

這個是一個陣列(Array), 它的內容就是你要加入的字型檔檔名, 把你要加的每一個都列進去

Info.plist

接著, 在Interface Builder裡你所要使用icon font的地方, 比如說UILabel設定你的字型, 原本的字型是設定為*“System”, 把它改成“Custom”*, 並選定你所需要的字型名稱, 例如FontAwesome, 要注意的是, 字型名稱不一定等同於你字型檔的名字:

Interface builder

Interface builder

接下來在Text的部分輸入這個圖示的代表的Unicode字元就好, 不是Unicode碼, 而是那個字元本身, 這挺不方便的, 可能用copy paste的才有辦法, 這是這個方法最大的缺點

這問題還是有方法克服, 這也就是前面為何提到會比較推薦使用Google material icons而不是Font Awesome , 這原因就是Ligatures

Ligatures

Ligatures是一個字型上蠻方便的特色的, 關於Ligatures可以先看一下這篇, 這是在Google material icons提到的一篇文章:

The Era of Symbol Fonts

剛剛提到的一個很大的缺點是, 你要知道圖示對應的Unicode碼才可以在你的UI上顯示你想要的圖示, 這相當不方便, 尤其那些Unicode碼可能根本完全不代表任何意義

比較人性點的作法是當你想要一個圖示代表藍芽, 用bluetooth就可以找到對應圖示, 而Ligatures就是一個這樣的存在

我們先來看看, 如果使用沒有而Ligatures的FontAwesome, 你在Text打上**“Contacts”**會是怎樣一個情形?

Ligatures1

它會直接一字不漏的呈現**“Contacts”**,這還是因為FontAwesome有包含原本英數字字型在裡面, 有些其他的自行更慘, 根本就是一片白

讓我們再看看用Google material icons的字型,同樣的東西會有什麼結果

Ligatures2

因為這個字型有支援Ligatures, 所以在這邊contacts就會被直接代換成它對應的圖示了, 我們就不用寄那種完全看不懂的unicode碼了

但大部分的字型其實也都沒有, 所以自訂字型該怎做?那就留待之後研究了

去年為了參加WWDC, 開始練了Swift, 寫了兩個library, 不過好像一直都沒寫過完整的App, 連UI好像都沒真的去刻過(去年寫的東西跟UI比較無關), 因此最近利用了一些時間開始了個side project, 做side project就常常會把時間花在一些枝微末節的地方, 比如說, 為了做一個像Android那樣的Floating action button, 去找來一個現成的3rd party lib - yoavlt/LiquidFloatingActionButton , 那個像水珠一樣突出去的效果, 我還蠻愛的:

但缺點是, 後面缺一個擋住背景元件的, 以致於要去點伸上來的小按鈕容易誤按後面的元件, 因此就想自己改一個後面多一個overlay的版本, 當然也不想隨便貼一張白白的就交差, 起碼要像這樣:

這邊不是單純蓋一個深色半透明的背景而已, 還需要作一點模糊的部分

在iOS上(iOS8 之後), 作這樣的東西很簡單, 只要利用UIVisualEffectViewUIBlurEffect這兩個東西, 寫法很簡單:

    let blurEffect = UIBlurEffect(style: .Dark)
    let uiEffectView = UIVisualEffectView(effect: blurEffect)
    uiEffectView.frame = overlayView.bounds
    overlayView.addSubview(uiEffectView)

UIBlurEffect有三種樣式, Dark, Light, 和ExtraLight, 上面的範例是Dark, 蠻適合用在這地方的, 利用這個方法就可以不用自己擷取screenshot再算模糊化了

之前被Parse搞的半死, 一直很好奇它的API到Mongodb的request之間到底是怎樣的對應

要弄清楚這個其實也不難, 把Mongodb的profiler全打開去看log就好了(db.setProfilingLevel(2)), 但這也是有缺點, profiler會寫到system.profile這個collection去, 而它是固定大小, 不能無限制的放, 再加上它還要多寫入這段, 多多少少影響效能

我需要的是一個從外部來觀察的工具, 不會影響到DB本身, 並且也可以將網路本身所花費的時間也包含進去, 所以想到的是在中間插一個proxy server

在現成的工具找到一個叫MonoDB Proxy的工具, 這是用nodejs寫的, 勉強可以, 也證明了這個方法是可行的, 但這工具雖然有做到代理這部份, 但在log部分, 由於它並未解析bson, 所以詳細的內容並不好看, 所以就自己來寫一個

功能需求

  1. 支援mongodb wire protocol, 而不是只是單純的轉送資料
  2. 印出request跟response內JSON的內容
  3. 要能夠知道每個request所需要的時間(含網路)

成品

最後寫出的的成品在這: https://github.com/julianshen/mongoproxy

整個還蠻簡單的:

  1. wire.go 實作wire protocol
  2. proxy.go 實作從client收資料並轉寫到server端
  3. cmd/mp/main.go command line主程式的部分

使用方法

這個工具是用Go寫的, 所以使用之前需要先安裝go

安裝

go get julianshen/mongoproxy/mp

這個步驟做完後, 就可以把mp這個指令裝好了, 確定 $GOPATH/bin是在你路徑內, mp這個檔也是在那邊

使用

mp --port=6001 --remote=mydb:27017 --response

其中:

  1. port是你這個proxy server的服務點
  2. remote是遠端的mongodb (host:port)
  3. Reponse是需不需要log回傳的部分

實作Wire protocol

本來覺得Wire protocol會蠻複雜的, 結果, 其實是蠻簡單的

所有的wire protocol request都會有一個標準的表頭:

struct MsgHeader {
    int32   messageLength; // total message size, including this
    int32   requestID;     // identifier for this message
    int32   responseTo;    // requestID from the original request
                           //   (used in responses from db)
    int32   opCode;        // request type - see table below
}

對應golang, 我定義成這樣:

type MsgHeader struct {
	MessageLength int32 // total message size, including this
	RequestID     int32 // identifier for this message
	ResponseTo    int32 // requestID from the original request
	//   (used in responses from db)
	Opcode // request type - see table below
}

因為一開始就可以讀到整個訊息長度的, 所以就蠻好解析的, wire protocol的實作我是有參考了dvara, 本來是有想拿它的code來改, 但看了一下發現它也沒完整實作wire protocol, 秉著自己也來了解一下這部份的想法, 就重頭自己刻了

dvara不同的地方是, 我用go的binary package來讀header而非自己刻一個, binary.Read的確是一個蠻好用的工具, 用底下的code就可以讀出header這個資料結構:

h := MsgHeader{}
err := binary.Read(r, binary.LittleEndian, &h)

另外, 除header外, 各request的所帶的欄位各自不同, 這部份的作法就是定好各個所需的資料結構, 用reflection的方式來讀取各相關資料:

v := reflect.ValueOf(req)
v = v.Elem()

// 根據資料結構內定義的每個欄位用相關的方法讀取
for i := 0; i < v.NumField(); i++ {
    f := v.Field(i)
    t := f.Type()

    if bytesRead == int(h.MessageLength) {
        break
    } else if bytesRead > int(h.MessageLength) {
        return nil, ErrorWrongLen
    }

    switch {
    case t == reflect.TypeOf((bson.D)(nil)):
        d, n, e := readDoc(bufferReader)

解析bson

這部份就沒再重新造輪子了, 直接用golang著名的mongodb driver mgo裡的bson lib: https://godoc.org/gopkg.in/mgo.v2/bson , 這個bson lib已經寫的很不錯了, 直接拿來用即可

在這個package內, 泛用的bson資料結構有兩種: bson.Mbson.D

這兩個是不同用途的,仔細看一下M跟D的定義:

type M map[string]interface{}

type D []DocElem

如果你是要把解析出的資料用map來操作, M是蠻方便的, 一開始我也是依著之前我寫相關的東西的習慣用M, 不過這邊卻是不可以用M的, 這也是我碰到bug的地方

由於M解析出的是Map, 所以每個field的順序它並沒記住, 但偏偏在wire protocol裡, 尤其是 $cmd, 順序是重要的, 所以Unmarshal出的M再Marshal回去, 順序可能不是原本的順序了, 而這在這個proxy應用上, client寫什麼東西過來就要寫什麼到server才不至於出錯

測試和Debug

我是用Wireshark來驗證我的實作有沒問題, Wireshark預設會把到27017 port的資料解析成wire protocol的內容供閱讀, 當然也可以自己手動請它解析

其他應用

這樣的proxy應該可以不只應用在debug, 像是Parse open source的dvara, 它也是利用了proxy來做connection pooling, 應該也可以用在request routing和caching的應用上

一直把Blog當作紀錄一些事情用的地方, 想法, 或是學習過的一些東西, 雖然不是很頻繁的在寫, 不過也寫了好幾年了, 從以前到現在, 換過好幾次平台, 從自己架的wordpress, 到posterous, blogger, 一直到現在用的Tumblr, 這中間最喜歡的還是posterous, 不過它已經不存在了, 而Tumblr也用了好一陣子了, 雖然加減湊合著用好像還OK, 不過編輯上也一直不是那麼順手, 尤其是貼source code, 因此也一直想把它換掉

medium 呢? medium我還沒很有動力去搞懂它, 會在上面看一些文章沒錯, 但把Blog移到上面去, 好像也沒啥動力

考慮了好一陣子, 不過最後還是把整個Blog遷移了, 反正現在有一個自己的domain (blog.jln.co), 搬家不用改地址!

遷移之前, 想了幾個我的需求:

需求

  1. 有方便的編輯器可以用, 最好可以支援mark down, 打html有點麻煩, WYSIWYG編輯器通常效率也不高
  2. 貼code好貼, 也方便閱讀
  3. 模版好編輯, 至少我要能知道怎麼改模版
  4. 好預覽
  5. 能同步到各個social network, 至少Facebook, Twitter, 也能同步到我原本的Tumblr和blogger
  6. 要能夠友善支援Open graph和Twitter card, 對每次share的FB的醜醜文字版型實在不喜歡
  7. 好轉移, 好備份, 有版本控制更好!

解決方案

現成的blog service, 好的什麼都要錢(ghost, wordpress), 免費的大概也都被我用過了, 自己架, 又得管server, 後來想想, blog都是靜態的網頁, 也真的不需要一個很複雜的系統, 這時候就想到一個解決方案, 那就是Github

Github有一個Github pages的服務, 可以讓你host你的靜態網頁, 所以只要有一個方法可以把Blog轉成靜態網頁就可以了, 這個還蠻Geek的方式感覺就蠻適合我的

找到了幾個方法轉blog:

  1. Jekyll
  2. Octopress
  3. Hexo

Octopress是已經完結不再維護了, 所以就Jekyll和Hexo兩個在抉擇, 兩者都有蠻多的主題跟模版功能, 也有不少plugins

Hexo是台灣精品, 而且是nodekjs寫的, 語言上我比較熟, 要改比較好改,而Jekyll是Ruby寫的, 我跟Ruby很不熟, 但它跟Github pages結合緊密, 而且已經有tool可以從Tumblr移轉內容過來了, 最後我選擇了Jekyll, 選擇Jekyll的原因是

  1. Github page直接原生支援, 只要把md檔push上去後, 就會自動產生網頁(不過後來我還是先在自己電腦產生完再push, 後面再說原因)
  2. 找到一個適合的也不太難改的theme, 就懶得去翻Hexo的了, 而且找到的那個對Open graph和Twitter card的支援也不錯, 不用改太多
  3. 原生的Tumblr import tool

不過整個也花了不少個晚上修改, 又花了一個晚上才能寫完這篇紀錄 @@

安裝Jekyll

網路上可以找到很多安裝Jekyll的文章, 這邊就不說太多, 我試了兩個不同的作法:

作法 1

這是最基本的作法

  1. 先用 gem install jekyll 安裝jekyll
  2. jekyll new myblog 在myblog目錄產生一個基本的網站
  3. jekyll s 會在 localhost:4000 開啟一個服務, 這時就透過browser可以看看你的blog長怎樣了

這方法的缺點是, 套用主題時, 還得改一堆東西, 有點麻煩

作法 2
  1. 一樣先裝jekyll
  2. 找到一個人家做好的theme (我是用hpstr這個), clone下來改, 因為大部分的都已經把他用的plugin之類的都先寫好在config了, 就省不少事, 建議找theme要注意其對應的Jekyll版本, 像我裝的Jekyll是3, 所以找的是適合3的
  3. gem install bundler 安裝bundler
  4. bundle install 來安裝相關的plugins
  5. jekyll s 看結果

我後來採用的是作法2, 踩在人家的肩膀上比較快 :P

設定github pages

這部份沒什麼難度, 創建一個專案名稱叫做 你的名字.github.com , 把相關檔案放到這邊去就行了

Github pages支援兩種方式, 一種是純粹的靜態網頁, 就是把html跟其他相關檔案丟到這邊就好(master branch), 另一種方式就是Jekyll, Jekyll的部分只要把前一個動作的檔案放進來就好, 它會自己幫你產生對應的靜態網頁

不過第二種方法的缺點是因為Github pages只支援幾種Jekyll plugins, 而且不支援自訂的plugins(放在 _plugins 目錄下的), 就算你在你自己電腦裡面跑完把產生後的 _site 目錄一起放上去也是沒用的

從Tumblr匯入之前文章

這部份沒太複雜, 是透過jekyll-import

安裝jekyll-import一樣可以透過gem install來安裝, 但建議不要這樣做, 是因為他用的Tumblr API是JSONP但程式卻將它當一般JSON在解碼, 會出錯, 這部份在最新版的code有解決(害我還去追了source code), 所以抓source回來自己build比較安全

裝好之後執行:

#!/bin/sh
ruby -rubygems -e 'require "jekyll-import";
    JekyllImport::Importers::Tumblr.run({
      "url"            => "http://blog.jln.co",
      "format"         => "html", # or "md"
      "grab_images"    => true,  # whether to download images as well.
      "add_highlights" => false,  # whether to wrap code blocks (indented 4 spaces) in a Liquid "highlight" tag
      "rewrite_urls"   => false   # whether to write pages that redirect from the old Tumblr paths to the new Jekyll paths
    })'

它就會把文章抓回到 _post的目錄下

Open graph & Twitter card

如果把內容分享到Facebook或是Twitter上只有短短的文字, 不是很好看, 所以希望在這部份能夠至少加上一張圖

hpstr這個theme已經有在 ** _includes/head.html ** 寫好OG跟Twitter card相關的tag了, 圖片(og:image)的部分, 他的規則/順序是:

  1. Front matter 裡面設定的
  2. _config.xml 裡的logo設定

我希望是更自動一點, 而不是自己去設定, 所以找到一個plugin : jekyll-auto-image, 這個plugin聰明一點, 他的順序是:

  1. 內文內出現的第一張圖
  2. _config.xml 裡面設定的圖

但這樣還稍嫌單調一些, 我想要達成的是:

  1. 內文內出現的第一張圖
  2. 根據標籤選的對應的圖(比如說把我go語言相關的設成Gopher的圖片), 這樣不會每個都一樣, 比較有變化一點
  3. 預設的圖

再加上auto image裡面關於image的設定跟我用的theme有所衝突, 這就需要把原本的auto image改一下了

我新增一個rb檔, 放在 ** _plugins **目錄下, 內容如下:

require "jekyll"

module Jekyll

  class AutoImageGenerator < Generator

    def generate(site)
      @site = site

      site.pages.each do |page|
        img = get_image(page)
        page.data['image1'] = img if img
      end
      # Now do the same with posts
      site.posts.docs.each do |post|
        #puts "hola"
        #puts Jekyll::VERSION
        #puts post.class
        #puts post.inspect
        #puts post.data.inspect
        #puts "-----"      
        #puts post.output
        #puts "----"
        img = get_image(post)
        post.data['thumb'] = img if img
      end
    end # generate

    def get_image(page)

      if page.data['thumb']
        return page.data['thumb']
      end
      # convert the contents to html, and extract the first <img src="" apearance
      # I know, it's not efficient, but rather easy to implement :)

      if page.class == Jekyll::Document # for jekyll 3.0 posts & collections
        htmled = Jekyll::Renderer.new(@site, page, @site.site_payload).convert(page.content)
      else
        htmled = page.transform # for jekyll 2.x pages
      end

      img_url = htmled.match(/<img.*\ssrc=[\"\']([\S.]+)[\"\']/i)
      return img_url[1] if img_url != nil

      tags = page.data['tags']
      imagemap = @site.config['imagemap']
      if tags != nil && imagemap != nil
        tags.each do |t|
          if imagemap[t.downcase] != nil
            url = imagemap[t.downcase]

            if !url.match(/^http/)
              url = '/images/' + url
            end
            return url
          end
        end
      end

      return @site.config['logo'] if @site.config['logo'] != nil
      return nil
    end

  end # class
end # module

這版就有達到我想要的目的, 但這不代表問題解了, 反而是問題的開始, 前面有說過github只支援特定的plugins, 所以怎樣這段改過的plugin都不會被它執行到, 唯一能做的解法就是改用靜態網頁的方式

jekyll產生的靜態網頁都放在 _site 目錄下, 所以真的要進master branch只有這裡面的, 其他都不用, 所以要先在本地端用 jekyll b去產生相關的靜態網頁到 _site裡再上傳

那怎測試呢? 一開始為了測試方便我都以本地端的server做測試, 但如果要測試Facebook share的話則需要能夠讓Facebook 連過來抓取網頁來分析, 但這點在區網不是很方便, 所以就帶入了 ngrok 這個工具, 這工具就是提供給你一個外部的網址, 你在本地端跑了ngrok這程式後, 就有一個可以對應你本地端的server, 還蠻好用的

自動發布到社群網站

這當然是用ifttt囉, 因為有用到jekyll-feed這個plugin, 所以會產生一個feed.xml的檔案, 所以就利用ifttt的feed channel來設定觸發

結語

本來明明不想花太多功夫的, 結果不但花了很多功夫, 還花了一頁來紀錄這篇 orz

今天追完了綠箭, 該來點功課, 不過今天忘了買啤酒了:(

Parse Push, 這功能無疑應該是Parse本身比較受歡迎的部分, 既然Parse明年要關門大吉了,這部份也是必須要搬家的, 但搬家比較麻煩的不是要搬到哪去, 而是之前累積的使用者, 總不希望一搬了就找不到他們了, Parse Push支援了包含了Google的GCM和Apple的APNS,以及他們自己的PPNS,以GCM來說, 在_Installation裡面, Parse是有儲存了GCM的deviceToken, 如果你之前用的是自己的GCM Sender ID, 那這部份直接搬了就可以用了, 但如果不是, 那預設用的是Parse自己的, 這樣這些deviceToken就不能夠使用自己的Sender ID來送, 必須要用用下面的方法來補救:

  • 從Google的Developer console取的Sender ID跟API Key
  • 在AndroidManifest.xml加入 <meta-data android:name="com.parse.push.gcm_sender_id" android:value="id:12345678"> (必須要以"id:“加上你的sender id)
  • 發布新的升級到Play store
  • 使用者更新後至少發送一次Push (如果你在Application中有去儲存Installation, 這時候因為有了新的sender, 所以deviceToken會被更新)
  • 可以用你自己的API key送給新的device token了(可以不用透過Parse console去送, 也可以用其他程式)

這個Go的範例可以用來試驗送PUSH到你的手機上:

找出你手機的deviceToken後即可用這個來驗證是否能夠成功送出push

但這樣還不夠, 我們還需要一個負責幫我們送push的server, 在之前那篇Parse自救方案裡有提到一個Pushd, 這是其中一個作法, 不過需要改一些東西才可以符合Parse Android SDK接收的格式, 另一個作法是透過open source的Parse Server, 原本它剛一出來時, 並沒支援Push, 但後來有其他人幫它加了上去了, 但它的方式是提供了API給你送PUSH, 但一來把API server跟PUSH server放一起並不是件好事, 二來當送大量的PUSH這樣的設計並不保險(並沒有queue, 送到一半server死了就麻煩了)

不過要以Parse server的為基礎拆出push的部分也不是很難, 做了一個簡單的範例: github.com/julianshen/parse_push_js

在這範例裡, 使用了Parse Server的ParsePushAdapter, 並且利用了Redis當作queue來做成的簡單的push server, 這樣應該就加減堪用了, 不過這邊有一個還需要再改的是, ParsePushAdapter 並不會把產生的push id回傳到上一層, 這樣要做追蹤紀錄其實有點比較不方便, 這邊還需要再加強一下