寫 Mobile App 簡單嗎?

之前面試 mobile app 工程師時,一位人選告訴我,他不想在原先公司(頗知名, 薪水也高)繼續做下去的原因,是因為 Android SDK 他已經很熟,再寫覺得也沒 有進步空間,所以想出來學一些其他的,例如底層一些的技術。

這似乎是個常見的說法,從剛入職場就常聽到,但可能年歲漸長,有些不同體悟。 例如寫 driver 就是那樣,系統整合就是那樣,所以想學別的。這其中必定有什 麼不對的地方,但是是什麼呢?

問題出在需求

Android SDK 本來創作出來的用意就是希望盡可能簡單,因為 Google 希望能讓 開發 Android app 是簡單的,吸引更多 developer 投入,才能讓生態圈蓬勃發 展。但開發界面上的簡單、API 的簡單,不表示功能上的簡單。如果覺得 SDK 學 完了,工作就變得太簡單,那應該是因為需求太簡單了。

為何需求是簡單的

Google 新出的 Inbox 功能簡單嗎?Facebook app 簡單嗎?要看由什麼角度來看。 他們致力追求的都是使用界面上的單純,但往往越單純的界面,後面的機制越複 雜。一個極度好用的功能,往往實作上相對困難。所以這裡延伸出的問題是,為 何在知名大廠,所面對的,仍然是實作上的簡單。

Mobile App 之外也經常如此

例如在 ODM 廠,處理來自各廠商的 BSP,做久了架構熟了,釐清的方式也熟了, 有問題找原廠支援,工作就開始容易。這表示 Android 系統面簡單嗎?問題恐怕 出在,為何沒辦法做到複雜的東西。BSP 已經是相對應用端,使用一個本來就是 為了方便使用者的 package,本來就不該困難。

廣 vs 深

於是可以發現,很少有真正簡單的領域,往往問題出在身處那領域的什麼位置。 台灣求深的廠商不多,相對的職位也不多,往往造成求深不如求廣,我自身就是 個例子。但這是個惡性循環,在職涯選擇上,或許可針對某特定領域,透過自我 要求,追求卓越,盡力往研發的上游端前進。

大勢不佳,但人才全球流動,以海盜心態武裝自己,拋棄故鄉,該去哪就去哪, 應該是概率最佳的選擇。若在本土發展,另闢蹊徑,總是比較困難的。

Solarized color 8 (bright black) 與背景雷同問題

使用 Solarized 這套配色對眼睛蠻 舒服的,而且有 light 或 dark 型態可以切換,跟 Base 16Zenburn 比起來,我是最喜歡 Solarized。但他有個長久以來的問題,就是在 terminal 下,background 跟 color 8 (bright black) 是一樣的顏色,都是 base 03。Node.js 很多套件在 彩色顯示時都會用到 bright black,一旦用到就會跟背景同色,看不見。

之前我的解法是把 background 設為 color 0,也就是真的 black,但在 Solarized 設計上,這兩色是鄰近的而不是對比色,所以雖不是同色,但仍然很 難看到。後來參考了內建 Solarized theme 的 Terminator 解 法,是直接把 color 8 設定成 #6a848a,這不論在 light 或 dark 模式下, 反差都很清晰,是目前為止我覺得最好的解。

相關討論可以看 issue 220,這問 題存在很久了但作者就是不爽解決。

多核心下 nginx - node.js 的組合

目前我們網站都是寫在 node.js 上,然後用 nginx 當 reverse proxy 來接。

以四核然後有 VT 為例,nproc 看到的會是 8,此時一些相對應的 nginx.conf 設定:

1
2
worker_processes 8;
worker_cpu_affinity 00010001 00010001 00100010 00100010 01000100 01000100 10001000 10001000;

worker_cpu_affinity 的值是按照 /proc/cpuinfo 內容來決定的,用來做範例的機器排列是 processor 0~3 對應到 core 0~3,然後 processor 4~7 又對應到 core 0~3,所以 processor [0, 4], [1, 5], [2, 6], [3, 7] 各自屬於同一個 core。同一個 core 可以允許 processes 互換,所以上面 worker_cpu_affinity 的意思是指定 worker 0 可以跑在 0, 4 兩個 CPU 上(第 0 跟第 4 bit 為 1),然後 worker 1 也是跑在 0, 4 上面。依此類推。

node.js 部份並沒有用 forever 來跑,而是用 upstart 來看住。相關的一些設定長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
# portal.conf

start on runlevel [2345]

task

script
NPROC=$(nproc)
for N in $(seq $NPROC); do
start portal-worker N=$N
done
end script

用 portal 去跑好幾個 portal-worker 起來。而 portal-worker 則是會看執行時給的 N 值來決定怎麼跑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# portal worker

manual

respawn

instance $N

env NODE_ENV=production
setuid www-data
setgid www-data
chdir /home/johndoe/portal
script
test $N = 1 && exec taskset 0x11 node server.js -c config-1
test $N = 2 && exec taskset 0x11 node server.js -c config-2
test $N = 3 && exec taskset 0x22 node server.js -c config-3
test $N = 4 && exec taskset 0x22 node server.js -c config-4
test $N = 5 && exec taskset 0x44 node server.js -c config-5
test $N = 6 && exec taskset 0x44 node server.js -c config-6
test $N = 7 && exec taskset 0x88 node server.js -c config-7
test $N = 8 && exec taskset 0x88 node server.js -c config-8
end script

portal-worker 的 log 都會在 /var/log/upstart 底下,例如 N=1 時,就會是 /var/log/upstart/portal-worker-1.log。另外可以看到 taskset 的邏輯基本上跟 worker_cpu_affinity 是一樣的。後面 -c 是自訂參數,用來指定不同設定檔,例如規定 node process 聽在不同的 port 上。這樣搭配 nginx 裡頭 upstream 功能就可以平均分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
upstream portal {
server 127.0.0.1:5001;
server 127.0.0.1:5002;
server 127.0.0.1:5003;
server 127.0.0.1:5004;
server 127.0.0.1:5005;
server 127.0.0.1:5006;
server 127.0.0.1:5007;
server 127.0.0.1:5008;
}

server {
# ...snipped...
location / {
proxy_pass http://portal;
proxy_redirect default;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $hostname;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

值得注意的是,upstart 在 12.04 有個 bug,Supplementary groups not set for user jobs,這會導致上面的 setuid www-data 不如預期,例如 user www-data 的 supplementary group 有一個 groupabc,然後某檔案設定為 groupabc 可以寫,結果 upstart 跑起來以後是寫不進去的。 解法之一是改用 sudo -u www-data -E -- 來跑。14.04 就沒這問題了。

MongoDB 搬空的 collections

show collections 會看到 system.indexes,只 dump 這個 collection 的 話,restore 時就會做出所有 collections 但是是空的。假定每個 collection 至少都有一個 index 的話。這在做測試用的 database 時很方便。

1
2
mongodump -d mydb -c system.indexes
mongorestore -d mydb dump/mydb

Golang import path `all`

go help packages 可以看到三個 reserved names 如下:

1
2
3
4
5
6
7
8
- "main" denotes the top-level package in a stand-alone executable.

- "all" expands to all package directories found in all the GOPATH
trees. For example, 'go list all' lists all the packages on the local
system.

- "std" is like all but expands to just the packages in the standard
Go library.

實際使用起來有如下的一些可能,不過最好用 root 裝 go,免得影響到 std libraries.

  1. 更新全部 go get -u all
  2. 清掉全部 build 出的 libraries go clean -i all
  3. install 全部 go install all

善用 exec redirection

寫 shell script 時常碰到 list 不好處理的問題,裡面如果包含空白之類的 IFS 區隔,一不小心就會發生意外。例如目錄中包含 My Music 這個目錄, 用以下的 shell script 處理 ls -1 時,就會分別印 MyMusic

1
2
3
for f in `ls -1`; do
echo $f
done

解法可以用 read 讀入變數:

1
2
3
ls -1 | while read f; do
echo $f
done

如果需要複雜一些的篩選,甚至處理有 \n 在裡頭的檔案,那可以用 find -print0

1
2
3
4
find -mindepth 1 -maxdepth 1 -print0 | \
while read -d $'\0' f; do
echo $f
done

但這又有個討厭的地方,就是在迴圈中如果需要用到 stdin 就出錯了,因為 stdin 是接到 findstdout。解法會動用到 process substitution 跟 exec redirection 用來改變環境的 file descriptor 設定。

1
2
3
4
exec 3< <(find -mindepth 1 -maxdepth 1 -type d -print0)
while read -d $'\0' f <&3-; do
echo $f
done

主要差異是第一行,<(COMMAND) 的用意是把 find 的輸出接到一個 named pipe 或者是 /dev/fd 之下當成一個檔案(詳情請參閱 info bash),而 exec 3< 的意思則是用 fd 3 打開這個檔案。下一行 read 後面接的 <&3- 是把 fd 3 move 到 read 的 stdin,這樣就不會影響到 loop 中的 stdin 了。

中文字型大不易

個人習慣的閱讀用字型為 sans-serif,為求搭配,中文字也想採用無襯線的黑體 或圓體。然而這件事情卻是超乎想像的困難,原因在於以下需求:

  1. 繁體部份不得為 GB 18030 寫法。(參考 GB 18030 就在你身邊 )若非為此,文泉驿系列字型,就是很好的選擇。

  2. 簡繁字體需同源。這是由於 UTF-8 中簡繁字有許多共用,若 fontconfig 先 指定繁體字集,再 failover 到簡體,而兩者風格不一致,那麼在顯示簡體文章 時,若在繁體字集中先找到字,就會直接使用,找不到的話就會用簡體字集。這 會導致整個版面交錯排列來自兩個字集的字型,此時若不一致,就會非常難看。 譬如這種慘況:

在 Ubuntu 13.10 中直接可找到的繁體黑體或圓體,比如 fonts-cwtex-heib 或 fonts-cwtex-yen 都有跟簡體字型不一致的問題。反倒是如果不要求要無襯線字 體,fonts-arphic-uming 就已經解決了,因為簡、繁、香港字型皆有提供。

Apple 的 黑體-繁黑体-简 , 以及微軟的正黑體,其實都符合這些需求,但 Ubuntu 套件中的中文字型則不然。

Update 2018/08/21:

Google 的 Noto CJK 符合以上所有需求, 且有襯線與無襯線皆提供。

Big Integer in JavaScript

應該很多人都知道 JavaScript 的數值型別沒有整數,一併都是 double precision 64-bit 浮點數。但實際運作起來還是很討厭啊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> b = parseInt('9007199254740992', 10); // 2^53
9007199254740992
> typeof(b);
'number'
> b + 1;
9007199254740992
> b = parseInt('9007199254740994', 10);
9007199254740994
> typeof(b);
'number'
> b + 1;
9007199254740996
> 'wtf?'
'wtf?'
> b = parseInt('18446744073709551616', 10); // 2^64
18446744073709552000
> isNaN(b)
false
> isFinite(b);
true

Reference: http://ecma262-5.com/ELS5_HTML.htm#Section_8.5

其實已經不準確了,但沒有錯誤訊息。這裡我本想做的事情是 JSON parse/stringify facebook user id,據 Facebook blog 說的長度是 64 bits. 當然會有人建議乾脆存成字串啦,但除非移民太空,不然其實不可能超過 64 了。在找支援的套件時,都不是很熱門,感覺怕怕的。後來看到 json-bignum,內部是直接用字串存,簡單,看了一下覺得還可以,但當然在比較之類的就要小心了,例如相等的話要先 toString() 之後才能比。

Ordered Spec in PyMongo

最近寫 Python 存取 mongoDB 時注意到一個問題,就是在下 find 時很自然的用了 dict 來下條件,但其實 mongoDB 的 find 會需要注意先後次序,用來跟index 搭 配在某些情況例如 hint,次序是有影響的。PyMongo 在這部份的說明其實蠻模糊 的,相關的文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pymongo.collection

find(self, *args, **kwargs)

:Parameters:
- `spec` (optional): a SON object specifying elements which
must be present for a document to be included in the
result set


bson.son

class SON(__builtin__.dict)
SON data.

A subclass of dict that maintains ordering of keys and provides a
few extra niceties for dealing with SON. SON objects can be
converted to and from BSON.

__init__(self, data=None, **kwargs)

所以若要保留順序,就要直接用 SON 物件來當 find 的參數。建立的方法跟 dict.items() 傳回來的東西一樣,是 “list of D’s (key, value) pairs, as 2-tuples”

1
2
3
4
5
6
>>> from bson import son
>>> a = son.SON([('a', 1), ('b', 1), ('c', 1), ('d', 1)])
>>> a
SON([('a', 1), ('b', 1), ('c', 1), ('d', 1)])
>>> a.to_dict()
{'a': 1, 'c': 1, 'b': 1, 'd': 1}

WXR Parser

WXR Parser 可用來分析 WordPress.com 匯出的 XML 檔案,是為了這次要搬出來然後用 Wintersmith 靜態產生 blog 頁面寫的,目前可以匯 出適合 Wintersmith 使用的目錄結構。不過因為設計上 parser 跟 backend 分 開,所以要擴充來產生其他格式也蠻簡單。

A simple WXR parser written in Python to parse the XML export from WordPress and store the information it in in Python’s basic data structures, i.e. dictionaries and lists. It also goes with a backend to export it in Markdown syntax suitable for Wintersmith. In its current form, it can simplify the migration from WordPress to Wintersmith, but it’s easy to be extended to export more formats.

It’s created because the author failed to find a simple one to use.

至於本來想讓 Wintersmith 支援 org-mode,這部份就不打算弄了。主要由於幾個原因:

  1. org-mode 輸出成 html 預設是 standalone 的,也就是會匯出一個結構完整 的 html 檔案。這在當成文件匯出時很不錯,但並不符合這類 static site generator 的預期。

  2. org-mode 新版本支援直接輸出為 markdown。