About the Speaker: Boris Farber
每個人都知道一個 App 的成功更這個 App 的性能體驗有着很密切的關系。但是如何讓你的 App 擁有極緻性能體驗呢在 DroidCon NYC 2015 的這個分享裡Boris Farber 帶來了他關于 Android Api 以及如何避免一些常見的坑的經驗。了解如何縮短啟動時間優化滑動效果建立更加順滑的使用者體驗。
@borisfarber
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 能很早的發現記憶體洩漏。
- 了解你程式的結構。
- 用類似 MATEclipse AnalyzerLeakCanary 這樣的工具分析記憶體。
- 在 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 自帶的并發方案。預設是存在一個由系統管理的線程池你可以通過 callbackfuture 來控制和管理。這根 MapRedues 發難有點像面對複雜的任務你希望能夠把他們拆分交給多個線程來處理。Executor 的架構就很能勝任這種場景。
如何适應并發APIs? (22:07)
- 學會和了解 API懂得權衡
- 確定找到了問題的正确解決方案
- 了解問題真實所在
- 重構代碼
Deprecation (22:42)
我們肯定都知道最好能夠避免使用廢棄的 API。比如以下的例子
- 不要通過反射來調用私有 API。
- 不要再 NDK 和 C 語言層調用私有 Native 方法。
- 不要輕易調用 Runtime.exec 指令完成程序通訊功能。
-
做程序通訊并不好。adb shell am
廢棄的意思是這些 API 将會被移除通常在正式版釋出 12天左右你的 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 來決策
- 確定它能夠解決你的問題。
- 確定它和目前所有的依賴能正常工作。
- 檢查依賴
- 留意一下依賴的版本沖突
- 了解維護情況和成本