文章來源:https://news.realm.io/cn/news/droidcon-farber-improving-android-app-performance/
每個人都知道一個 App 的成功,與這個 App 的性能體驗有着很密切的關係。但是如何讓你的 App 擁有極致性能體驗呢?在 DroidCon NYC 2015 的這個分享裏,Boris Farber 帶來了他關於 Android Api 以及如何避免一些常見坑的經驗。帶你瞭解如何縮短啓動時間,優化滑動效果,創建更加順滑的用戶體驗。
Save the date for Droidcon SF in March — a conference with best-in-class presentations from leaders in all parts of the Android ecosystem.
簡介 (0:00)
大家好,我是 Boris,現在是 Google 的一枚員工,目前專注於需要高性能的 App。這個分享是我長期以來從錯誤中,以及在給合作伙伴做諮詢的時候攢下的最佳實踐。如果你有一個小型的 App,讀過之後,會在你的 App 成長階段起到幫助。
我常常會見到那些啓動時間很長,滑動不流暢,甚至出現沒有反應的 App。我們通常要花很多時間去改善這些問題,畢竟我們都希望自己的 App 能夠成功。
Activity 泄漏 (1:17)
我們第一個需要修復的問題就是 Activity 泄漏,我們先來看看內存泄漏是怎麼發生的。 Activity 泄漏通常是內存泄漏的一種。爲什麼會泄漏呢?如果你持有一個未使用的 Activity 的引用,其實也就持有了 Activity 的佈局,自然也就包含了所有的 View。最棘手的是持有靜態引用。別忘了,Activity 和 Fragment 都有自己的生命週期。一旦我們持有了靜態引用,Activity 和 Fragment 就不會被垃圾回收器清理掉了。這就是爲什麼靜態引用很危險。
m_staticActivity = staticFragment.getActivity()
我看過太多次這樣的代碼了。
另外,泄漏 Listener 也是經常會發生的事情。比如說,我有下面的代碼。LeakActivity
繼承自 Activity
,我們有一個單例:NastyManager
,當我們通過 addListener(this)
將
Activity 作爲 Listener 和 NastyManager 綁定起來的時候,不好的事情就發生了。
public class LeakActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
NastyManager.getInstance().addListener(this);
}
}
想要修復這樣的 Bug,其實相當簡單,就是在你的 Acitivity 被銷燬的時候,將他和 NastyManager
取消掉綁定就好了。
@Override
public void onDestroy() {
super.onDestroy();
NastyManager.getInstance().removeListener(this);
}
相對上面的解決方案,我們自然還有更好的。比如我們真的需要用到單例嗎?通常,並不需要。不過某些時候可能真的很需要。我們得權衡和設計。不過無論如何,記住,當 Activity 銷燬的時候,在單例中移除掉對 Activity 的引用。下面我們討論下: 如果是內部類,會發生什麼?比如說,我們有一個在 Activity 裏有一個很簡短的非靜態 Handler。
儘管它看起來很短,但是隻要它還存活着,那麼包含它的 Activity 就會存活着。如果你不信我,在 VM 裏試試看。這就是另一個內存泄漏的案例:Activity 內部的 Handler。
public class MainActivity extends Activity {
//...
Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
}
}
}
Handler 是個很常用也很有用的類,異步,線程安全等等。如果有下面這樣的代碼,會發生什麼呢?handler.postDeslayed
,假設
delay 時間是幾個小時… 這意味着什麼?意味着只要 handler 的消息還沒有被處理結束,它就一直存活着,包含它的 Activity 就跟着活着。我們來想辦法修復它,修復的方案是WeakReference
,也就是所謂的弱引用。垃圾回收器在回收的時候,是會忽視掉弱引用的,所以包含它的
Activity 會被正常清理掉。大概代碼如下:
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> mActivity;
// ...
public MyHandler(MainActivity activity) {
mActivity = new WeakReference<MainActivity>(activity);
//...
}
@Override
public void handleMessage(Message msg) {
}
//...
}
概括來說:我們有個內部類,就像 Handler,內部非靜態類是不能脫離所屬類而單獨存活的,Android 裏通常是 Activity。所以,看看你的代碼裏的內部類,確保他們沒有出現內存泄漏。
相比非靜態內部類,最好使用靜態內部類。區別就是靜態內部類不依賴所屬類,他們擁有不同的生命週期。我經常見到類似的原因引起的內存泄露。
如何避免 Activity 泄漏? (8:37)
-
移除掉所有的靜態引用。
-
考慮用 EventBus 來解耦 Listener。
-
記着在不需要的時候,解除 Listener 的綁定。
-
儘量用靜態內部類。
-
做 Code Review。個人經驗:Code Review 能很早的發現內存泄漏。
-
瞭解你程序的結構。
-
用類似 MAT,Eclipse Analyzer,LeakCanary 這樣的工具分析內存。
-
在 Callback 裏打印 Log。
滑動 (10:05)
實現流暢滑動的技巧:UI 線程只用作 UI 渲染。這一條真諦能夠解決 99% 的滑動卡頓問題。不要在 UI 線程做下面的事情:
- 載入圖片
- 網絡請求
- 解析 JSON
- 讀取數據庫
做這些操作是很慢的,像圖片,網絡,JSON考慮用現成的庫,有很多社區提供的解決方案,數據庫考慮下用 Loader,支持批量更新和載入。
圖片 (11:26)
圖片相關的庫有很多,比如 Glide, Picasso, Fresco。你可以自己去了解下他們之間的區別,以幫助自己在特定場景下做出取捨。
內存(12:13)
Bitmap 操作是很需要技巧的,圖片一般比較大,而且系統對最大內存又有限制和要求。在我面對 4.0 之前的系統的時候,我簡直要崩潰了。內存管理也很需要技巧。有的時候需要放到文件裏,有的時候需要放到內存裏,別忘了,我們還有一個很有用的工具:LRUCache。
網絡(12:54)
首先,Java 的網絡請求確實是 Android 的一個阻礙。很多 Java.net 的 API 都是阻斷執行的,切記不可在 UI 線程執行網絡請求。在線程裏執行或者直接使用第三方庫吧。
異步 HTTP 其實也挺麻煩的,4.4 起 OkHttp 就成了 Android 代碼的一部分了,然而… 如果你需要最新版本的 OkHttp ,可以考慮自己引入。另外有個不錯的庫叫: Volley,也可以試試 Square 的 Retrofit。這些都能讓你的網絡請求變得更友好。
大 JSON (14:35)
在 UI 線程,也不做解析 Json 的事情,因爲這是一個很耗時的事情。試着用 Google 的 GSON 來做反序列化的操作。
對於巨大的 JSON 解析,建議用更快的 Jackson 以及 ig-json-parser,這兩個工具在 JSON 的解析上做的非常漂亮。從公司的反饋結果來看 ig-json-parser 的效率是最高的。
Looper.myLooper()
== Looper.getMainLooper()
是可以幫助你確定你是否在主線程的代碼。
如何優化滑動速度? (16:56)
- UI 線程只做 UI 更新。
- 理解併發 API。
- 開始使用優秀的第三方庫。
- 使用 Loader 加載數據庫數據
之所以要用第三方庫,是因爲你自己去完善一個複雜功能是需要花時間的。如果你打算專注在自己的功能性的 App 上,那麼用庫吧。
併發 APIs (18:00)
如何讓 App 快速響應請求是個很重要。開發者們,甚至包括我,經常忘記 Service 的方法是在 UI 線程執行的。請考慮使用 IntentService
,AsyncTask
,Executors
,Handler
和 Loopers
。
我們來盤點下這些的區別:
IntentService (19:07)
我在之前的公司,我用 IntentService 來執行上傳功能。IntentService 是一個單線程,一次一個任務的工作流。我們沒有很複雜的任務系統。如果你有大型複雜的任務,而且這個任務不需要跟 UI 打交道,那麼考慮用 IntentService 吧。
AsyncTask (19:56)
如果你的任務需要更新 UI,那麼考慮用 AsyncTask 吧,AsyncTask 雖然相對容易,但是有些坑得留意。當你旋轉手機的時候,Activity 會被關閉,然後重啓。不然可能造成內存泄露。
Executor Framework (21:11)
這是 Java 6 自帶的併發方案。默認是存在一個由系統管理的線程池,你可以通過 callback,future 來控制和管理。這根 MapRedues 發難有點像,面對複雜的任務,你希望能夠把他們拆分交給多個線程來處理。Executor 的框架就很能勝任這種場景。
如何適應併發APIs? (22:07)
- 學會和理解 API,懂得權衡
-
確保找到了問題的正確解決方案
-
瞭解問題真實所在
-
重構代碼
Deprecation (22:42)
我們肯定都知道,最好能夠避免使用廢棄的 API。比如以下的例子:
- 不要通過反射來調用私有 API。
- 不要再 NDK 和 C 語言層調用私有 Native 方法。
- 不要輕易調用 Runtime.exec 指令完成進程通訊功能。
adb shell am
做進程通訊並不好。
廢棄的意思是這些 API 將會被移除,通常在正式版發佈 1,2天左右,你的 App 就不會工作了。更糟糕的情況是,如果你的 App 依賴了一些庫,而這些庫喲改了廢棄的 Api 或者工具。那可就慘了,如果一旦作者沒有更新…你懂得。
不要用廢棄 Api 的另一個原因是性能問題和安全問題。
如何避免廢棄 Api:
- 使用正確的 API。
- 重構依賴。
- 不要濫用系統。
- 更新依賴和工具。
- 越新的通常越好。
用 Toolbar 而非 ActionBar,在需要動畫的時候用 RecyclerView,因爲它專門爲動畫做過優化。同時 Android M 裏移除了 Apache Http Connection。請使用 HttpURLConnection,它擁有更簡單的 API,更小的體積,默認的壓縮功能,更好的 Response 緩存,等等其他很讚的功能。
架構 (27:03)
架構中的 Bug 總是最爲煩人。想要避免這種問題,學習下 App 組件的生命週期。比如什麼是 Activity 的 Flag?什麼是 Fragment?什麼事 stated fragment?什麼是 task?讀讀文檔,嘗試下用回調的 log 搞清楚這些概念。
時常有人問我:“Picasso 和 Glide 哪個更好?我改用 Volley 還是 OkHttp?”,這種問題根本沒有 100% 正確的答案。不過,當我在選擇一個庫的時候,我會用下面的 Checklist 來決策:
- 確保它能夠解決你的問題。
- 確保它和當前所有的依賴能正常工作。
- 檢查依賴
- 留意一下依賴的版本衝突
- 瞭解維護情況和成本
總的來說,提及架構和設計,最好的方法就是讓你的程序最快響應。確保用戶能夠快速理解你的 App,並且擁有良好體驗。
作者信息:
Boris Farber
Android and JVM Programming Expert. Boris enjoys the challenges of design, languages and frameworks. He aims for simple solutions to complex problems. As a Developer Advocate he helps promote Android APIs, acting as a bridge between internal teams and developers building apps and solutions on top of Android APIs.