2011年12月29日 星期四

【Startup 談心酸】 當革命情感對上體制

Standard
我總認為,當別人問起職業,回答『Startup(新創)』是一件很自傲的事。雖然公司不大,但是從兩手空空開始,完成理想的感覺真的是棒呆了!你必需經歷『養活自己』、『勒緊褲帶』、『想破腦袋』還有『革命不怕失敗』的種種過程,最重要的是,伴隨而來的『人的問題』,很有可能成為最後一根稻草,壓垮我們。但每當跨越過每一個階段,彷彿就吃了甜美多汁的果實,相當有滋味。

常聽到有人說:『先不談成功與否,Startup能做超過一年,就相當不容易了。』,其實是事實,從創業中敗陣下來的人不在少數,敗陣的原因更不勝枚舉,有人是受不了壓力,有人把持不住Cash Flow(現金流),有人經營心態不正確,或是 idea 失敗等等。但是,這些原因真的都比不上『同伴』所造成的打擊。

如果你不是一個人創業,而是三五好友有志一同,那我要恭喜你,畢竟要能找到方向相同,又有足夠能力的人一起是非常困難的。但是,我也要為你感到遺憾,因為『人的問題』會帶來更多波折和困難,造成創業失敗的比例更高。『同伴』是個雙刃劍,雖能共患難,有革命情感,但也會因決裂而一夕之間毀滅。

這裡有一個朋友的小故事,相當常見,或許大家都很熟悉其中情節:
有一群人在學校就是好朋友,而所會的技術也讓他們相當出類拔粹,於是,他們孕釀出創業的想法。為了小試身手,這些人開始共同接一些小案子,並培養革命情感。看似一切很美好,但在過程中問題出現了,有人怠惰(或是能力不足)造成其他人負擔增加,又或者是因此造成案子無法順利結案。好不容易,其他隊員們跳出來『Cover』,讓一切過了關,客戶也付了錢。

然而,問題才正要發燒,在分配利益時,有人開始提出不滿:『為什麼出包的同伴,可以拿的錢和沒出包的人一樣多?為什麼救火的人,幫出包的人完成不是自己的大部份工作,卻只拿到原本的待遇?』此外,對工作的分配也頗有微詞,有人認為自己做的工作,相對困難許多,應該分到更多的錢。更誇張的是,每一個人都生怕別人多拿或是拿的比自己多。

還好,為了讓大家不要決裂,盡可能滿足大家的要求,有人跳出來處理這件事,除了自己掏腰包,甚至動用了原本大家講好要保留下來的『公基金』。然後發現,雖然大家都同意切出一部份成為公基金,但心中仍然認定這公基金有一部份是屬於自己的,隨時都可以要回去,有人更不時想動用或拿回這個錢。一旦動用公基金,就會牽動到大夥的神經,引發更多莫明其妙的問題。

問題並沒有因為時間久而消失,之後的幾次案子,多多少少都出現類似的狀況,磨光了真正想做事的人,其心中的熱情。最終,革命情感不敵絕望,因看不到未來而拆夥。

這個劇情也許正在你的團隊中上演,更或者有更多超出這故事內容的問題發生。總而言之,問題出在於每個人都不肯讓自己的利益有任何損傷,也只能看到自己眼前的利益。就算有人出來願意奉獻,也只是減緩問題發酵,並沒有真正解決問題。一旦緩充的空間消耗殆盡,這個團隊就走向末路。

很多人會說,大家都是好朋友,也都有相同志向,過程中難免會有些問題,我們都可以不用太計較,這樣也有共患難的革命情感。但事實上,人性很難抵抗,已經有太多人實驗過,都證明了『不計較才計較』。只是有人爆發的點比較高,平時看不出來,可是一旦爆發,便天崩地裂跟你沒完。想想看,連親密的親人都可以為了遺產殺紅了眼,更何況只是『普通朋友』。

最後你會發現,革命情感是解決不了任何事,正確的建立公司體制是最好的方式,雖然很死板無情,但不容任何人有第二句話,也給參與者有人性上的約束,前人所遺留下來的方式,是有其道理的。

2011年12月25日 星期日

從 Linux Kernel 出發看!探討 Process 組成結構!

Standard
對所有電腦使用者而言,執行一支應用程式就像吃蛋糕一樣簡單。對於程式開發者而言,寫支 Hello World Program 並跑起來,也是三十秒內可以完成的事。但是,這樣容易的動作,其實底層有著複雜的機制,否則短短十行程式碼的 Hello World 能夠動起來,真的是奇跡了。想要知曉程式是怎麼被作業系統執行,要追溯到 Process 的機制,在研究過 Linux Kernel 的 Process 相關機制後,一切就將明朗。

註:在本篇文章內,都將會假設 binary program 已經被作業系統解析並放到記憶體執行,說明只著重於 Process 的部份 。雖然 Process 與 binary program 和其載體息息相關,但本文不會討論 ELF 的細節,有興趣的讀者可以自己去尋找相關文件閱讀。

若你是程式開發者,對『Process(程序)』一詞應該不陌生。嚴格來說, Process 就是處於執行狀態的程式,如果參考一些原文書或英文文獻,它們也許會這樣定義:『Process is a program in execution』。所以我們可以認定,Process 的組成,是擁有著『程式執行檔的 binary code + 記錄資料的記憶體』。

本文將 Process 分成內外兩個角度來探討:
  1. 從 Process 內部,程式執行的觀點 
  2. 從 Process 外部,也就是作業系統的觀點 

程式執行的觀點

單純以 Process 內部的角度來看,Process 就是一般的程式碼被放到記憶體執行。曾於舊文『Linux 下程序的記憶體映射』略為提及,一支程式擁有著數個 segments(區段),並仰賴著這些 segment 來持續運作。大致上來說,每個 segment 有不同的用途:
  • Code Segment - 存放主要程式
  • Data Segment - 存放已被初始化並賦予值的全域變數
  • BSS Segment - 紀錄尚未被賦予值的全域變數
  • Stack Segment(Stack/Heap) - 紀錄 Process 在執行時動態註冊的變數包括 function 中的 local variable

對程式本身來說,會動態並隨機使用的是 Stack Segment,程式向作業系統(OS) 要記憶體空間後,就可以在 Stack Segment 讀寫該記憶體空間。而實際上 OS 底層的行為,是配置記憶體, 然後映射到 Stack Segment 供程式使用。

作業系統的觀點

與程式觀點相呼應,作業系統有記憶體管理機制,其建立虛擬記憶體空間,將程式對映進去該空間後,開始執行。作業系統管理著 Process 所擁有的 Segments,為每個 Segment 都配置了『虛擬記憶體區域(VMA, Virtual Memory Area)』。

在 Linux 之下,我們可以透過 cat /proc/<Process ID>/maps 的方式去取得一個 Process 所用到的記憶體,以下是觀察系統上的 bash(PID=18165):
$ cat /proc/18165/maps
08048000-0810a000 r-xp 00000000 08:01 1851399    /bin/bash
0810a000-0810f000 rw-p 000c1000 08:01 1851399    /bin/bash
0810f000-08114000 rw-p 00000000 00:00 0
08752000-08a46000 rw-p 00000000 00:00 0          [heap]
b737b000-b7396000 r--p 00000000 08:01 28598806   /usr/share/locale/zh_TW/LC_MESSAGES/libc.mo
b7396000-b73a0000 r-xp 00000000 08:01 12755293   /lib/i386-linux-gnu/i686/cmov/libnss_files-2.13.so
b73a0000-b73a1000 r--p 00009000 08:01 12755293   /lib/i386-linux-gnu/i686/cmov/libnss_files-2.13.so
b73a1000-b73a2000 rw-p 0000a000 08:01 12755293   /lib/i386-linux-gnu/i686/cmov/libnss_files-2.13.so
b73a2000-b73b5000 r-xp 00000000 08:01 12755189   /lib/i386-linux-gnu/i686/cmov/libnsl-2.13.so
b73b5000-b73b6000 r--p 00012000 08:01 12755189   /lib/i386-linux-gnu/i686/cmov/libnsl-2.13.so
b73b6000-b73b7000 rw-p 00013000 08:01 12755189   /lib/i386-linux-gnu/i686/cmov/libnsl-2.13.so
b73b7000-b73b9000 rw-p 00000000 00:00 0
b73ca000-b73cc000 r--p 00000000 08:01 28600275   /usr/share/locale/zh_TW/LC_MESSAGES/bash.mo
b73cc000-b73d3000 r--s 00000000 08:01 28666899   /usr/lib/i386-linux-gnu/gconv/gconv-modules.cache
b73d3000-b754a000 r--p 00000000 08:01 28615144   /usr/lib/locale/locale-archive
b754a000-b754b000 rw-p 00000000 00:00 0
b754b000-b7568000 r-xp 00000000 08:01 12755040   /lib/i386-linux-gnu/libtinfo.so.5.9
b7568000-b756a000 r--p 0001c000 08:01 12755040   /lib/i386-linux-gnu/libtinfo.so.5.9
b756a000-b756b000 rw-p 0001e000 08:01 12755040   /lib/i386-linux-gnu/libtinfo.so.5.9
b756b000-b76be000 r-xp 00000000 08:01 12755289   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b76be000-b76bf000 ---p 00153000 08:01 12755289   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b76bf000-b76c1000 r--p 00153000 08:01 12755289   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b76c1000-b76c2000 rw-p 00155000 08:01 12755289   /lib/i386-linux-gnu/i686/cmov/libc-2.13.so
b76c2000-b76c6000 rw-p 00000000 00:00 0
b76c6000-b76c8000 r-xp 00000000 08:01 12755049   /lib/i386-linux-gnu/i686/cmov/libdl-2.13.so
b76c8000-b76c9000 r--p 00001000 08:01 12755049   /lib/i386-linux-gnu/i686/cmov/libdl-2.13.so
b76c9000-b76ca000 rw-p 00002000 08:01 12755049   /lib/i386-linux-gnu/i686/cmov/libdl-2.13.so
b76ca000-b76ec000 r-xp 00000000 08:01 12755034   /lib/i386-linux-gnu/libncurses.so.5.9
b76ec000-b76ed000 r--p 00021000 08:01 12755034   /lib/i386-linux-gnu/libncurses.so.5.9
b76ed000-b76ee000 rw-p 00022000 08:01 12755034   /lib/i386-linux-gnu/libncurses.so.5.9
b76f4000-b76fd000 r-xp 00000000 08:01 12754995   /lib/i386-linux-gnu/i686/cmov/libnss_nis-2.13.so
b76fd000-b76fe000 r--p 00008000 08:01 12754995   /lib/i386-linux-gnu/i686/cmov/libnss_nis-2.13.so
b76fe000-b76ff000 rw-p 00009000 08:01 12754995   /lib/i386-linux-gnu/i686/cmov/libnss_nis-2.13.so
b76ff000-b7705000 r-xp 00000000 08:01 12755103   /lib/i386-linux-gnu/i686/cmov/libnss_compat-2.13.so
b7705000-b7706000 r--p 00005000 08:01 12755103   /lib/i386-linux-gnu/i686/cmov/libnss_compat-2.13.so
b7706000-b7707000 rw-p 00006000 08:01 12755103   /lib/i386-linux-gnu/i686/cmov/libnss_compat-2.13.so
b7707000-b7708000 r--p 00176000 08:01 28615144   /usr/lib/locale/locale-archive
b7708000-b770a000 rw-p 00000000 00:00 0
b770a000-b770b000 r-xp 00000000 00:00 0          [vdso]
b770b000-b7726000 r-xp 00000000 08:01 12755299   /lib/i386-linux-gnu/ld-2.13.so
b7726000-b7727000 r--p 0001b000 08:01 12755299   /lib/i386-linux-gnu/ld-2.13.so
b7727000-b7728000 rw-p 0001c000 08:01 12755299   /lib/i386-linux-gnu/ld-2.13.so
bf916000-bf937000 rw-p 00000000 00:00 0          [stack]

大致上來說可以看到有這些 VMA 的存在(已省略會偏離主題的 VMA):
08048000-0810a000    Code
0810a000-0810f000    Data
08752000-08a46000    Heap
bf916000-bf937000    Stack

對於作業系統而言,管理這些記憶體和程式狀態,才是真正的重點。所以,就作業系統核心的角度來看 Process,可以從排程的程式來切入瞭解。翻閱 Linux Kernel 原始程式碼的檔案『include/linux/sched.h』,Process 的狀態結構被定義在 struct task_struct 之中,而其中的 mm 就是記錄著該 Process 的記憶體配置資訊,我們可以從中得到 Process 當前所擁有的全部 VMA 和 Segments 所在位址:
struct task_struct {
        ...
        struct mm_struct *mm, *active_mm;
        ...
};
struct mm_struct {
        struct vm_area_struct * mmap;           /* list of VMAs */
        ...
        unsigned long start_code, end_code, start_data, end_data;
        unsigned long start_brk, brk, start_stack;
        ...
};

另外,從『include/linux/mm.h』可以找到 VMA 的串列資料結構定義:
struct vm_area_struct {
        struct mm_struct * vm_mm;       /* The address space we belong to. */
        unsigned long vm_start;         /* Our start address within vm_mm. */
        unsigned long vm_end;           /* The first byte after our end address
                                           within vm_mm. */
        ....
        /* linked list of VM areas per task, sorted by address */
        struct vm_area_struct *vm_next;
        ....
}

後記

若要繼續追查下去,還有動態連結的部份要探討,會牽涉到更多機制,由於篇幅有限,下次有時間再來寫完。

2011年12月23日 星期五

如何在 Debian 建置 NodeJS + Express 環境

Standard
截至目前為止,只有 Debian Sid(unstable) 提供 Nodejs 和 Express 套件可讓使用者直接安裝。但是,在 Server 的環境之下,一般都使用 Debian 5.0/6.0 (Lenny/Squeeze) 穩定版的系統,因此沒有套件可以直接安裝,唯一的方法就是自己下載編譯 Nodejs。還好自己手動安裝的過程並不困難,幾個步驟就可以完成。

安裝 Nodejs

更新系統並安裝編譯 Nodejs 所需的套件:
sudo apt-get update
sudo apt-get install git-core curl build-essential openssl libssl-dev

從 Git Repository 下載 Nodejs 原始碼:
git clone https://github.com/joyent/node.git

進入 Nodejs 原始碼目錄,並切選擇我們要的版本(截至本文,0.6.6 是最新版):
cd node
git checkout v0.6.6

# Note: 可以使用 git tag 看到 Nodejs 所有的版本列表

編譯並安裝 Nodejs (預設會裝到 /usr/local/lib/node):
./configure
make
sudo make install

如果安裝過程中沒有任何問題,就可以使用 node 指令查看 Nodejs 的版本了:
node -v

手動設定公用的 Nodejs Module 路徑:
echo "NODE_PATH=/usr/local/lib/node_modules" >> .bashrc

# Note: 如果想讓所有 Server 上的 user 都套用設定,可以放在 /etc/profile


安裝 Express Web Framework

使用 npm 安裝 express(使用 -g 選項會安裝到公用的目錄 /usr/local/lib/node_modules):
sudo npm install express -g

Express 通常預設使用 jade template engine,也需要手動安裝:
sudo npm install jade -g

安裝完成後,就可以立即測試 nodejs + express:
mkdir test
cd test
express
node app.js

如果從瀏覽器可以看到畫面,就代表安裝成功:
http://ServerIP:3000/

後記

截至本文,Debian Sid 的 Nodejs 套件還是 0.4.12 版本,如果想要用新的 Nodejs,就要透過本文的方法來安裝。

2011年12月20日 星期二

Nodejs + Express 的 Route 流程規劃

Standard
如大多數人的習慣,往往我們都把 Route 的路徑都定義在 app.js 當中。Express 所提供的眾多範例程式中,雖然有將 Route 抽出來放在 routes 目錄下的實作方式,但也太簡單,只支援一層的設計。這意味著,如果 routes 之下有更多子目錄,放著更多的 route 設定,都是不會被讀取的。

這邊做了簡單的修改,利用遞迴的做法,掃描所有的子目錄,然後去載入所有的 js 檔案以擴充 Route 的設定。

以下是 route.js 的程式碼:
var vm = require('vm');
var fs = require('fs');

module.exports = function(app) {
  var dir = __dirname + '/routes';
  var context = {
    app: app,
    require: require
  };
  var newContext;

  /* Initializing Context */
  for (var key in global)
    context[key] = global[key];

  newContext = vm.createContext(context);

  /* Loading all routes */
  loadRouteDir(newContext, dir);
};

function loadRouteDir(context, path) {
  /* Scanning files */
  fs.readdirSync(path).forEach(function(file) {
    loadRouteFile(context, path, file);
  });
}

function loadRouteFile(context, path, file) {
  var fullpath = path + '/' + file;

  fs.stat(fullpath, function(err, stats) {
    if (stats.isFile()) {
      /* Loading route */
      var str = fs.readFileSync(fullpath, 'utf8');
      vm.runInContext(str, context, file);
    } else if (stats.isDirectory()) {
      /* Loading sub-directory */
      loadRouteDir(context, fullpath);
    }
  });
}

在 app.js 中只要這樣使用即可:
/* app is Express Server  */
require('./route')(app);

2011年12月8日 星期四

Openmoko 永遠活在我們的心中

Standard
還記得 2006 年 12 月業界出現了一個瘋狂的想法:『Openmoko 的開放手機』,但就在 2011 年 12 月滿五年,這家公司(Openmoko, Inc) 也將劃下了一個圓滿的句點。

回顧過去,Openmoko 不只是單純的軟體層面開放,而是連硬體設計的細節都公開。這對當時的大環境,高端手機技術和 Know-how 都被嚴密掌握在特定公司的時局下,無疑是個瘋狂的舉動。想當然,Openmoko 花了大量錢財與人力物力,還有國內外社群的力量,終於做出了自己的第一代手機原型(參考圖 Neo 1973),爾後又開發出第二代硬體加強版的機種(參考圖 Neo FreeRunner)。

Neo 1973
還記得,當時大多數人都還是使用功能手機(Feature phone),智慧型手機(Smart Phone)也還停留在被難用 Windows CE 稱霸的時代,誰也還沒有想到未來會有 iPhone 和 Android Phone,對絕大多數人而言,手機是個神祕且只有少數人可以參與開發的領域。有趣的是,Openmoko 除了做開放的手機之外,也對應用程式(Apps)安裝於手機上的想法有了初步的構想,而這些想法,都觸動了不少軟體開發者和投資人的目光。

雖然直到最後,Openmoko 的手機還不足以讓一般消費者順暢使用,但許多構想,無論是開放手機,有擴充性的應用程式平台,還是使用 Linux 開發前衛的智慧型手機,都確實已深埋在不少人心中。世界各地的 Hacker 和高手,無一不想擁有這樣一台的開放手機。

Openmoko 也在教育界扎根,除了提供開發的教育訊練外,也去拜訪了許多大專院校,讓許多原本沒有機會碰到手機開發的學生和技術人員,有機會一睹這神秘的領域。然後,也有不少學校買了 Openmoko 的手機,做為開發教學使用,直至今天,這些手機還是相當好的開發教材,甚至,也已經有不少教授在這上面開發了一系列教材。(如:淡江大學資工系已經將 Openmoko 手機當成 C 語言教學和相關基礎技術的實作平台)
Neo FreeRunner

隨著 iPhone 的問市,Google Android 的出現,Openmoko 逐漸失去了媒體焦點,也有不少不為人知的事情,讓公司逐漸縮編並轉型(之後轉做過 WikiReader),以致現在的人,多半都只是耳聞,漸漸忘了當時 Openmoko 花大錢招頂級人才,遍布全世界的開發者和分公司,有如全身鍍金般受各界祝福和朝聖的風光。

就在最旺的時候,筆者曾經有幸為 Openmoko 工作過一陣子,雖然只是在家工作,很少去公司,和裡面的人員甚少當面接觸,但也為曾在那工作感到驕傲。為此,看著 Openmoko 的殞落,聽到 Openmoko 將結束的消息,不免有些震驚和失落感,當年筆者離開時的不悅,瞬間煙消雲散,之中衝突原由已不需要再提及。



最近筆者回鍋,回去幫忙 Openmoko 做一些案子,原本只打算幫忙不打算談錢,但結果最後被 Openmoko 的創始人 Sean Moss-Pultz 硬逼著接下案子,成為了 Openmoko 名符其實的最後一個案子外包者,真不知該哭還該笑。

至於之後會如何?Sean 將回到事業原點,與我們一樣同為新創(Startup)界的一員,落實他全新的想法。在此祝福 Sean,能再接再勵,帶著過去的經驗,創出一番新事業。而 Openmoko,將永遠活在我們的心中。

2011年12月3日 星期六

Android 問題百出之 2.3.x 的 JavaScript Interface

Standard
有鑑於 Android 問題太多,只好定了個系列標題『Android 問題百出』當開頭,並將碰到的問題和解決或避開的方法記錄在內。話說回來,筆者個人其實相當討厭 Android,自 Android 出現以來,從未真的投入其中並賺過什麼錢,會接觸,多半是興趣玩弄或是幫一些朋友的公司臨時打工救火。不過,既然是救火,任何千奇百怪的問題或狀況都會遭遇到,甚至還得『限時』解決別人解決不了的問題。

這次碰到的問題就是 Android 2.3.x Gingerbread 缺少 JavaScript Interface 的實作,如果你的應用程式有自己實作 API 供 JavaScript 程式使用,那這些 API 將會完全失效。而這樣的問題,對於使用 Web 技術(HTML5 + Javascript)的應用程式來說,相當嚴重。

話說 Android 在 2.3 版本之後,採用了 V8 做為 JavaScript Engine。在快速掃過 Android 關於 WebView 和 WebKit 的程式碼後,發現不幸的是『Google 再次未將程式寫完』。但這一次,不只是功能未完成,而是將原本可以用的功能(在 Android 2.2),變成不能用。這讓筆者非常不能理解,為何 Google 換了 V8 Engine 後,明知功能沒有改完,卻仍將這部份實作隨新版 Android 釋出?然後,絕大多數廠商,在毫無感覺之下,直接繼承了這樣的 Bug。

對硬體裝置的廠商來說,重新實作這個 JavaScript Interface 支援是最佳的解決辦法,髒一點的方法是將 V8 改回舊的 JSCore。但很可惜的是,就算有人改好了,也沒有廠商願意釋出這部份的程式碼。(所以才會不停有人願意花大錢找筆者這種臨時救火工呀。)

此外,如果你是一般的應用程式開發者,因為動不了底層,則完全無解。但是,這邊有個 Workaround,可以暫時代替 JavaScript Interface,讓應用程式開發者可以避開這問題(以下假設自定的 JavaScript Interface Class 為 myAPI)。

定義將會用到的變數:
/* Define private variable */
private static WebView mWebView;
private static myAPI mJSIF;
private static boolean javascriptInterfaceBroken = false;

初始化 WebView 時加上 Workaround:
/* Initializing WebView */
mWebView = (WebView) findViewById(R.id.supermark_dialog);
mWebView.getSettings().setJavaScriptEnabled(true);

/* Check Android version */
try {
    if (Build.VERSION.RELEASE.contains("2.3.")) {
        javascriptInterfaceBroken = true;
    }
} catch (Exception e) { }

/* Create own JavaScript Interface */
mJSIF = new myAPI(mWebView);

if (!javascriptInterfaceBroken) {
    mWebView.addJavascriptInterface(mJSIF, "Fred");
} else {
    /* Workaround to add JavaScript Interface to webview for Fucking Android 2.3 */
    mWebView.setWebViewClient(new WebViewClient() {
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            if (javascriptInterfaceBroken) {
                String handleGingerbreadStupidity =
                    "javascript: function do() {window.location='http://Fred:do:null';};" +
                    "javascript: function handler() {" +
                    "this.do=do;" +
                    "}; " +
                    "javascript: var Fred = new handler();";
                view.loadUrl(handleGingerbreadStupidity);
            }
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (javascriptInterfaceBroken) {
                if (url.contains("Fred")) {
                    /* Parsing URL */
                    StringTokenizer st = new StringTokenizer(url, ":");
                    st.nextToken();
                    st.nextToken();
                    String function = st.nextToken();
                    String parameter = st.nextToken();

                    /* Call function */
                    if (function.equals("do")) {
                        mJSIF.do();
                    }
                }

                return true;
            }

            return false;
        }
    });
}

最後,我們還是可以如使用原生的 JavaScript Interface 一般,在 HTML5 + JavaScript 中呼叫自定的 API:
<script>
window.Fred.do();
</script>

簡單來說,就是創造假的 URL 和相應的 Parser,去接收從 JavaScript 傳來的要求。

後記

如果說一直專心並精通於某樣領域的人稱之為專家,那筆者肯定不是 Android 專家。但是,好在長久來與 Open Source 拼命所練就出來的功夫,無論什麼樣的火,總算都能夠快速找到火源和撲滅,應該可以算是滅火專家吧。 :-D