顯示具有 IntentService 標籤的文章。 顯示所有文章
顯示具有 IntentService 標籤的文章。 顯示所有文章

2011年3月4日 星期五

讓癮科技 Android 應用登上榜首的秘密

How do I make my Engadget(Chinese Version) app to become the most popular app?

經過將近一個月的測試,2011/2/6 首次發行 癮科技 Android app 到 Android Market 上。六天後的今天 (2011/3/3),這個應用很快地爬到「新聞與雜誌」類別排行榜中的第一名位置,同時也是所有應用程式排行榜的第四名。在目前 Android Market 已有 16 萬個 Android 應用 一起激烈競爭的情況下,新的應用能這麼快登上排行榜,真的是一件很不容易的事。這中間,癮科技寫的「不要再叫我們蘋科技!癮科技新聞訂閱程式也有Android版了」這篇介紹文,當然有很大的幫助。

除此之外,到底有什麼樣的秘訣,才能讓我的應用程式登上排行榜呢?這是我最常被兩岸開發者問到的問題之一。

要讓應用程式登上排行榜,可以分成非技術面與技術面兩個面向來談。非技術的部份,我早在 2009/10 一場研討會的「發現 Android 熱門應用程式的秘密」的講座中,分享給大家。只不過這場演講主要的對象是對岸的開發者,對於台灣的開發者,只好等下次有人邀請我演講時,我再分享給大家。

技術面的部份,最主要的除了不能讓你的應用程式輕易當機之外,當然就是要讓使用者用起來很順暢才行。「使用的很順暢」這句話講起來很輕鬆,不過卻是得花最多時間與人力去調整各項細節,才能做到的地方。

這些細節,使用者當然不會注意到,也不會告訴你。就看你是否知道使用者的痛苦(需求),以及你如何深入了解這些痛苦的解決方法是甚麼。在這裡我就拿我最近開發的癮科技 app 來當案例。說明我們在技術的層面上,做了哪些努力。

第一:這不是一個單純的 RSS reader。一般這樣的應用程式開發者,可能都會以 RSS reader 的架構來開發這樣的應用。為了讓這個應用能跑得更順,我們決定走不一樣的路,在 server 端提供專屬的 APIs,在手機端也做了一些一般 RSS reader 做不到的功能。例如,你可以往下一直看過去的 N 篇文章。

第二:所有抓下來的文章與圖片,都存到 SD 卡上,以節省手機儲存空間的使用。多數的手機,內部儲存空間都是有限的,這造成使用者常要刪除舊的應用,以增加多餘的空間來裝新應用。這從我的 App 2 SD 與 aCCleaner 應用受歡迎的程度,就可以了解使用者在這方面的需求很大。如果你的應用佔的空間太大,在這點就吃虧很多。看到這,你可能會說這有什麼,大家都有這功能。不對,細節就在這裡。如果你有使用其他的新聞閱讀軟體,就可以觀察到他還是吃了不少 data/cache 空間。這原因就在你應用所建立的 Sqlite database 以及 WebView 所建立的 database/cache 都還是存在內部儲存空間。要將這些完全移到 SD 卡上,這可花了我好幾天追蹤原始碼,才知道要怎麼解決這個問題。目前,我只將 preferences 存在內部儲存空間中,其他都移到 SD 卡上。

第三:自動縮放文章中的文字與圖片大小。水平捲動對手機使用者來說,是一件很痛苦的事,這是千萬要避免發生的事。自動縮圖片,就是要解這問題。另外,多數的新聞閱讀應用的字體大小都太小了,行距也不對。這對要長時間閱讀文章的人,是一種折磨。你現在看到文章中的預設字體大小與行距,可是我們花了好多時間才調校出來的。你說,這不又是一個要在細節下功夫的地方?

第四:離線也能閱讀文章。離線閱讀的功能,對於這樣的新聞應用是一個很重要的功能,這樣使用者才真的能隨時隨地都可以看文章。文章與圖片都存到 SD 卡上,不是就能實現這個功能了嗎?不對,這只做了一半。另外一半就是你還要實現自動預下載文章的功能,有了這功能使用者才能隨時隨地有「新」文章可看。寫網路程式可算是手機程式中一件非常有挑戰的事,因為手機的 CPU 會進入 sleep 模式,網路可能隨時會中斷,速度可能掉到 GPRS 的速度等等各式各樣的問題都會出現。這些種種你必須要面對的問題,你都得處理,才能讓你的應用跑得很 solid。

雖然我們做了這麼多的努力,這個應用還有一些沒做好的部分。我們還會繼續在這個應用中奮戰好一段時間,在接下來的新版本中,我們會陸續解決這些問題,敬請期待!


繼續閱讀全文...

2010年11月9日 星期二

並行下載的完美解決方案

How to implement the function that supports concurrent downloading?

在看完 小心,AsyncTask 不是萬能的 以及 深入研究 IntentService 原始碼 這兩篇文章後,我想你應該已經對如何寫個正確處理費時工作的 Android 程式,建立起正確的觀念。

從這兩篇文章中,我們也了解到,在 IntentServcie 中等待被服務的工作,並不會被一起並行 (Concurrent),而是循序執行。在多數的案例中,循序執行已能滿足你的需求,而且這樣的程式也比較能夠受控制而不會出錯。

不過在處理網路下載這件事情上,循序執行可不是最好的方式,每件網路下載的工作,免不了要浪費時間在等待網路的傳輸上。為了要善用 CPU 的資源,加速資料的下載工作(例如下載多張圖片),你今天可能會想要這些等待被服務的工作,能夠一起被並行,而不是循序執行。在研讀完這個 IntentService 的原始碼後,你自己知不知道如何寫個可支援並行工作的 IntentService?

如果你曾研讀過 AsyncTask 的原始碼,你應該知道所有的 AsyncTask實例 (instances),都是由同一個 ThreadPoolExecutor 在管理。每一個 AsyncTask 的實例,都會由 ThreadPoolExecutor 中分配到一個 thread 來執行。因此所有的 AsyncTask 實例都是並行的。要實現一個可支援並行工作的 IntentService,我們剛好可以利用 Service + AsyncTask 的組合。

底下是我所實現的 ConcurrentIntentService 的完整程式代碼。

要使用這個 ConcurrentIntentService,和使用 IntentService 是相同的,你只要繼承這個 ConcurrentIntentService,並將費時的工作,移到 onHandleIntent() 中處裡即可。onHandleIntent() 是被 non-UI thread 所喚起的。因此在這個函式中,你可以放心地去執行你的下載工作,而不用擔心會產生任何的 ANR 錯誤。

我們先看 #29~#40 行onStart() 這個函式。

每當你想執行某件費時的工作,透過 Context.startService() 呼叫到 Service.onStart() 時,我們立即產生一個 MyAsyncTask (AsyncTask 的子類,見 #56~#83) 的實例(#37),並將他放到 m_mapIntent2AsyncTask 這個 HashMap 中(#38)。

設計這類的樣式,其中一項重要的工作是決定何時要殺掉 Service。在 IntentService 中,每項工作都是循序執行的,因此他利用 stopSelf(int),來讓系統判斷是否最後一項工作已執行完畢。系統會在最後一項工作已執行完畢時,自動殺掉 Service。不過在 ConcurrentIntentService 中的所有工作,都是並行的,而且每件工作的執行時間也長短不一。因此,我們不能參考 IntentService 的作法,我們必須借助一個 HashMap 來管理,還有哪些待執行的工作,自己決定要殺掉 Service 的時機。

在 MyAsyncTask 這個類別 (#56~#83) 的 doInBackground() 函式中,我們先將欲執行的工作從 m_mapIntent2AsyncTask 中移除。接下來,再呼叫你設計的 onHandleIntent(Intent),每個 AsyncTask 執行完後,最終都會執行 onPostExecute()。因此,我們在這個函式中,判斷 m_mapIntent2AsyncTask 中是否還有待執行的工作。如果沒有,那就是這個 Service 的生命該終結的時候 (#73~#74)。

其他部份的設計邏輯,和 IntentService 類似,請自行研讀。不過,我們為這個 ConcurrentIntentService,還特別加了 cancel() (#19~27) 介面,讓你可以取消所有正在進行或等待執行的工作。

如何在 Android 應用中,順利處理雲端資源的存取工作,一直是應用開發者的痛苦。不過,透過系統提供的 IntentService 與我自行設計的 ConcurrentIntentService,應當可以幫助開發者減少在處理網路下載時所遇到的問題。

要寫 Android 應用,一點都不難,不就是 Java。這是我最常聽到,剛跨過入門檻開發者告訴我的一句話。不過當你曾嘗試寫過 Android 的雲端應用後,你應該有所體會,要寫好的一個 Android 的網路應用,這當中還是有許多門檻需要跨過的。


繼續閱讀全文...

2010年10月5日 星期二

深入研究 IntentService 原始碼

IntentService Source Code Internals

在 小心,AsyncTask 不是萬能的 中,我提到 IntentService 是你在實現下載功能時,最好用的武器之一。

這次,我們來深入研究 IntentService 的原始碼,看他是如何用短短幾行代碼,來解決這個問題。

從 class 的宣告中,你可以知道 IntentService 就是一般的 Service。

接著他定義一個 ServiceHandler。一般我們在創建 Handler 時,都是用預設的建構式。預設的 Handler 建構式,是依附在當前的 thread 身上。所以一般開發者都以為 Handler 一定是依附在 main-thread 上,這是不對的觀念。

這個 ServiceHandler,是依附在一個 non-UI thread 的 Looper 上。Looper 就是訊息迴圈 (message loop),這是 Android UI 在處理各式訊息時,最重要的元件之一。而 ServiceHandler 建構式中所傳入的 Looper 物件,是在 onCreate() 中所建立的。

看到了吧,mServiceLooper 就是 thread 物件中的 Looper 物件。這個 thread 物件就是一個 HandlerThread,他是一個 non-UI thread。

所以現在這個 Service,一個帶有 message loop(mServiceLooper) 的 non-UI thread (mServiceHandler) 正一直在背後執行著,監看是否有任何傳入的 message。

接著,每當你呼叫 startService(new Intent(this, MyIntentService.class)) 啟動 IntentService 時,其實他只是很簡單地送出一個 message 到 mServiceHandler 上。

我們知道這個 message 最終會由 Handler.handleMessage() 來處理。由於整個 mServiceHandler 是在 non-UI thread 中執行。當他在 handleMessage() 中呼叫你的 onHandleIntent() 時,你的 onHandleIntent() 自然也是在 non-UI thread 中執行。

onHandleIntent() 執行結束後,接著呼叫 stopSelf(int)。由於 IntentService 是設計用來可以在一個 Service 中服務多項工作,因此在這裡,不可以呼叫 stopSelf()。

如果你呼叫 startService() 多次,每一次的呼叫都會被轉成一個 message,並放在 mServiceLooper 的 message queue 中,等待被服務。一個 message 所對應的工作被完成後,才會繼續服務下一個工作。所以,這些等待被服務的工作,並不是一起並行 (Concurrent) 的,而是循序執行。

當所有的工作都處裡完時,也是 Service 該結束的時候。

在 onDestroy() 中,唯一要做的事,就是要將 mServiceLooper 停下來。

從研究這個 IntentService 的原始碼,我們可以學到如何運用簡單的 pattern (Service + Handler + HandlerThread),幫我們更簡易與有系統地,完成我們所想要做的事。

最後我們先前說過,在 IntentServcie 中等待被服務的工作,並不會被一起並行,而是循序執行。如果你今天想要這些等待被服務的工作,能夠一起被並行,在研讀完這個 IntentService 的原始碼後,你自己知不知道如何寫個可支援並行工作的 IntentService?

提示:可以用 Service + AsyncTask 的組合。


繼續閱讀全文...

2010年9月14日 星期二

小心,AsyncTask 不是萬能的

Don't only use AsyncTask for your download task

要進入 Android 應用開發之門,真的一點都不難,不就是 Java 嘛。這是我最常聽到,剛跨過入門檻開發者告訴我的一句話。

這句話其實是沒有錯的,Android 應用開發的入門檻,比起其他平台,已經低了許多。要跨過這門檻,的確是輕鬆容易許多。不過,如果要從入門到進階,邁向下一個關卡,你第一個要了解的是 Activity 的生命週期 和 Process 的生命週期,而且是要『透 . 徹 . 了 . 解』。這一步很重要,卻被許多開發者輕忽了。

尤其這 Activity 的生命週期,實際上的行為會比你從文件上看到的說明還來的複雜。也因此,我發現有不少,即使已有多個 Android 應用開發經驗的開發者,在開發 Android 應用時,還是栽了不少跟斗。這第一個問題,就出在這些開發者,還是沒達到我說的『透徹了解』境界。因此,今年的進階應用開發課程,特地將這一部分加入進來,希望對想踏入進階之門的開發者,有所助益。

好了,如果你已經有一段 Android 應用的開發經驗,那應該知道在 Main-thread(UI-thread) 中,你不能執行一件需時 5 秒以上的工作,例如網路或資料庫的存取、音樂的播放等等。要不然你的應用就會產生 ANR 錯誤。要解決這個 ANR 錯誤,唯一的方法就是自行建立一個新的 Thread 物件,並將該費時的工作放在 Thread.run() 中執行。關於如何解決 ANR 的細節,我建議你先讀 Painless threading,這是一篇值得一讀的好文章。

在這篇文章中,介紹了從 Android 1.5 才加入的 AsyncTask。AsyncTask 很好用,同時我也建議你研究他的 原始碼。AsyncTask 就是太好用了,有些開發者就認為,單用 AsyncTask 就能解決他的問題。其實,我早在請將要執行很久的程式碼,放在 Service 中執行這篇中,就已提過。單將費時的程式碼放在 Thread.run() 中執行,還是不夠的,你只解決一半的問題。不過,多數開發者不是便宜行事,就是不相信我說的。這些便宜行事的開發者,就是在賭這系統強制殺掉你應用的機率有多少;而那些不相信我說的,就是因為他沒有透徹了解 Activity/Process 的生命週期。

完整解決費時工作的方法,不僅要將費時的工作放在 Thread.run() 中執行,還要將這個 Thread 放在 Service 中執行。

你要知道 Android 的四大元件,Activity, BroadcastReceiver, Servcice and ContentProvider,除了 ContentProvide 外,全都是在 main-thread 中執行。而這些元件中,就只有 Service 的生命週期是最持續(長久)的。Activity 只要執行到 onPause(),BroadcastReceiver 只要離開 onReceiver(),系統隨時會殺掉這些元件,而且機率還很高。Service 當然也是會被系統砍掉,只不過它的優先順序,排在較低等級。自然被系統砍掉的機率就低很多。你還可以更進一步利用 Serivce.startForeground() 降低你被系統殺掉的優先順序。關於這部分,你要熟讀 Process 的生命週期 與 What is a Service?。

其實為了減輕開發者的負擔, Android 1.5 已經加了 IntentService 這個新類別。如果你要寫個用到網路的應用,用這個 IntentService 才是你的完美解決方案。

要使用這個 IntentService 其實很簡單,你只要繼承這個 IntentService 並將該項費時的工作,移到 onHandleIntent() 中即可。onHandleIntent() 是被 non-UI thread 所喚起的。因此在這裏面你可以放心地去執行你的下載工作。

下次,我們來研究一下這個 IntentService 的原始碼,看他是如何做的。

參考資料:


繼續閱讀全文...