Android Weekly Notes Issue #224
Android Weekly Issue #224
本期內容包括: Google Play的pre-launch報告; Wear的Complications API; Android Handler解析; RxAndroid; 測量效能的庫: Pury; 方法數限制; APK內容分析; Redux for Android; 一種view造成的洩露; 註解處理; 更好的Adapter; Intro屏等等.
ARTICLES & TUTORIALS
Google Play team在I/O 2016的時候宣佈了很多新features, 其中有一個pre-launch report.
這個report是幹什麼的呢, 它會報告在一些裝置上測試你的應用的時候發現的issues.
要生成這種報告, 你應該在Developer console上enable它. 然後上傳alpha/beta apk. 上傳到beta channel之後, 5-10分鐘就會生成報告.
報告主要包括三個部分:
- Crashes
- Screenshots
- Security
在鐘錶的定義裡, complications是指表上除了小時和分鐘指示之外其他的東西.
在Android Wear裡面我們已經有一些complications的例子, 比如向用戶顯示計步器, 天氣預報, 下一個會議時間等等.
但是之前有一個很大的限制就是每一個小應用都必須實現自己的邏輯來取資料, 比如有兩個應用都取了今天的天氣預報資訊, 將會有兩套機制取同樣的資料, 這明顯是一種浪費.
Android Wear 2.0推出了Complications API解決了這個問題.
通訊主要是Data providers和Watch faces之間的, 前者包含取資料的邏輯, 後者負責顯示.
Complications API定義了一些Complications Types, 見官方文件.
作者在他朋友的開源應用裡用了新的API: Memento-Namedays, 這個應用是生日或者日期提醒類的.
首先, 作者用Wearable Data Layer API同步了手機和手錶的資料. 然後在Wear module裡繼承ComplicationProviderService
建立了complication data provider, 這裡就提供了onComplicationActivated
onComplicationDeactivated
, onComplicationUpdate
等回撥.
使用者也可以點選Complications, 可以用setTapAction()
指定點選後要啟動的Activity.
可以指定ComplicationProviderService
的更新頻率, 是在manifest裡用這個key:
android.support.wearable.complications.UPDATE_PERIOD_SECONDS
.
更新得太頻繁會比較費電.
需要注意的是這並不是一個常量, 因為系統也會根據手機的狀況進行一些調節, 不必要的時候就不需要頻繁更新.
本文作者採用的方式是用ProviderUpdateRequester
. 在manifest裡面設定0.
ComponentName providerComponentName = new ComponentName(
context,
MyComplicationProviderService.class
);
ProviderUpdateRequester providerUpdateRequester = new
ProviderUpdateRequester(context, providerComponentName);
providerUpdateRequester.requestUpdateAll();
這裡是作者PR: PR
首先, 作者舉了一個簡單的例子, 用兩種方法, 用Handler來實現下載圖片並顯示到ImageView上的過程.
主要是因為網路請求需要在非UI執行緒, 而View操作需要在UI執行緒. Handler就用來在這兩種執行緒之間切換排程.
Handler的組成
- Handler
- Message
- Message Queue
- Looper
Handler
Handler是執行緒間訊息傳遞的直接介面, 生產者和消費者執行緒都是通過呼叫下面的操作和Handler互動:
- creating, inserting, removing Messages from Message Queue.
- processing Messages on the consumer thread.
每一個Handler都是和一個Looper和一個Message Queue關聯的. 有兩種方法來建立一個Handler:
- 用預設構造器, 將會使用當前執行緒的Looper.
- 顯式地指明要用的Looper.
Handler不能沒有Looper, 如果構造時沒有指明Looper, 當前執行緒也沒有Looper, 那麼將會丟擲異常.
因為Handler需要Looper中的訊息佇列.
一個執行緒上的多個Handler共享同一個訊息佇列, 因為它們共享同一個Looper.
Message
Message是一個包含任意資料的容器, 它包含的資料資訊是callback, data bundle和obj/arg1/arg2, 還有三個附加資料what, time和target.
可以呼叫Handler的obtainMessage()
方法來建立Message, 這樣message是從message pool中取出的, target會自動設定成Handler自己. 所以直接可以在後面呼叫sendToTarget()
方法.
Message pool是一個最大尺寸為50的LinkedList. 當訊息被處理完之後, 會放回pool, 並且重置所有欄位.
當我們使用Handler來post(Runnable)
的時候, 實際上是隱式地建立一個Message, 它的callback存這個Runnable.
Message Queue
Message Queue 是一個無邊界的LinkedList, 元素是Message物件. 它按照時間順序來插入Message, 所以timestamp最小的最先分發.
MessageQueue中有一個dispatch barrier
表示當前時間, 當message的timestamp小於當前時間時, 被分發和處理.
Handler提供了一些方法在發message的時候設定不同的時間戳:
sendMessageDelayed()
: 當前時間 + delay時間.
sendMessageAtFrontOfQueue()
: 把時間戳設為0, 不建議使用.
sendMessageAtTime()
.
Handler經常需要和UI互動, 可能會引用Activity, 所以也經常會引起記憶體洩漏.
作者舉了兩個例子, 略.
需要注意:
非靜態內部類會持有外部類例項引用.
Message會持有Handler引用, 主執行緒的Looper和MessageQueue在程式執行期間是一直存在的.
建議的是, 內部類用static修飾, 另用WeakReference.
Debug Tips
顯示Looper中dispatched的Messages:
final Looper looper = getMainLooper();
looper.setMessageLogging(new LogPrinter(Log.DEBUG, "Looper"));
顯示MessageQueue中和handler相關的pending messages:
handler.dump(new LogPrinter(Log.DEBUG, "Handler"), "");
Looper
Looper 從訊息佇列中讀取訊息, 然後分發給target handler. 每當一個Message穿過了dispatch barrier
, 它就可以在下一個訊息迴圈中被Looper讀.
一個執行緒只能關聯一個Looper. 因為Looper類中有一個靜態的ThreadLocal物件保證了只有一個Looper和執行緒關聯, 企圖再加一個就會丟擲異常.
呼叫Looper.quit()
會立即終止Looper, 丟棄所有訊息.
而Looper.quitSafely()
會將已經通過dispatch barrier
的訊息處理了, 只丟棄pending的訊息.
Looper是在Thread的run()
方法裡setup的, Looper.prepare()
會檢查是否之前存在一個Looper
和這個執行緒關聯, 如果有則拋異常, 沒有則建立一個新的Looper
物件, 建立一個新的MessageQueue. 見程式碼.
現在Handler
可以接收或者傳送訊息到MessageQueue
了. 執行Looper.loop()
方法將會開始從佇列讀出訊息. 每一個loop迭代都會取出下一個訊息.
作者這個是個系列文章, 本文是part 10.
Android的listener很多, 我們可以通過RxJava把listener都變成發射資訊的源, 然後我們subscribe.
本文舉例講了Observable.fromCallable()
和Observable.fromAsync()
方法的用法.
在做任何優化之前我們都應該先定位問題. 首先是收集效能資料, 如果收集到的資訊超過了可以接受的閾值, 我們再進一步深究, 找到引起問題的方法或者API.
幸運的是, 有一些工具可以幫我們profiling:
- Hugo 用
@DebugLog
註解來標記方法, 然後引數, 返回值, 執行時間都會log出來. - Android Studio toolset. 比如System Trace, 非常準確, 提供了很多資訊, 但是需要你花時間來收集和分析資料.
- 後臺解決方案, 比如JMeter, 它們提供了很多功能, 需要花時間來學習如何使用, 第二就是高併發profile也不是常見的需求.
Missing tool
關於我們關心的應用的速度問題, 大多數可以分為兩種:
- 特定方法和API的執行時間, 這個可以被Hugo cover.
- 兩個事件之間的時間, 這可能是獨立的兩段程式碼, 但是在邏輯上關聯. Android Studio toolset可以cover這種, 但是你需要花很多時間來做profile.
作者意識到下面的需求沒有被滿足:
- 開始和結束profiling應該是被兩個獨立的事件觸發的, 這樣才可以滿足我們靈活性的需求.
- 如果我們想監控performance, 僅僅開始和結束事件是不夠的. 有時候我們需要知道這之間發生了什麼, 這些階段資訊應該被放在一個報告裡, 讓我們更容易明白和分享資料.
- 有時候我們需要做重複操作, 比如loading RecyclerView的下一頁, 那麼一個回合的操作顯然是不夠的, 我們需要進行多次操作, 然後顯示統計資料, 比如平均值, 最小最大值.
基於上面的需求, 作者建立了Pury.
Introduction to Pury
Pury是一個profiling的庫, 用於測量多個獨立事件之間的時間.
事件可以通過註解或者方法呼叫來觸發, 一個scenario的所有事件被放在同一個報告裡.
然後作者舉了兩個例子, 一個用來測量啟動時間, 另一個用來測量loading pages.
Inner structure and limitations
效能測量是Profilers
做的, 每一個Profiler
包含一個list, 裡面是Runs
. 多個Profilers
可以並行執行, 但是每個Profiler
中只有一個Run
是active的.
Profiling with Pury
Pury可以測量多個獨立事件之間的時間, 事件可以用註解或者方法呼叫觸發.
基本的註解有: @StartProfiling
, @StopProfiling
, @MethodProfiling
方法:
Pury.startProfiling();
Pury.stopProfiling();
最後作者介紹了一些使用細節.
專案地址: Pury
作為Android開發, 你可能會看到過這種資訊:
Too many field references: 88974; max is 65536.
You may try using –multi-dex option.
首先, 為什麼會存在65k的方法數限制呢?
Android應用是放在APK檔案裡的, 這裡麵包含了可執行的二進位制碼檔案(DEX - Dalvik Executable), 裡面包含了讓app工作的程式碼.
DEX規範限制了單個的DEX檔案中的方法總數最大為65535, 包括了Android framework方法, library方法, 還有你自己程式碼中的方法. 如果超過了這個限制你將不得不配置你的app來生成多個DEX檔案(multidex configuration).
但是開啟了multidex配置之後有一些隨機性的相容問題, 所以我們在決定開啟multidex之前, 首先採取的第一步是減少方法數來避免這個問題.
在我們開始改動之前, 先提出了這些問題:
- 我們有多少方法?
- 這些方法都是從哪裡來?
- 主要的方法來源是誰?
- 我們真的需要所有這些方法嗎?
在搜尋這些問題的答案的過程中, 我們發現了一些有用的工具和tips:
MethodsCount.com 將會告訴你一個庫有多少方法, 還提供了每個方法的依賴.
JakeWharton/dex-method-list utility 可以顯示.apk, .aar, .dex, .jar或.class檔案中的所有方法引用. 這可以用來發現一個庫中到底有多少方法是被你的app使用了.
mihaip/dex-method-counts 這個工具可以按包來輸出方法, 計算出一個DEX檔案中的方法數然後按包來分組輸出. 這有利於我們明白哪些庫是方法數的主要來源.
Gradle build system 提供了關於專案結構很有價值的資訊. 一個有用的task是dependencies
, 讓你看到庫的依賴樹, 這樣你就可以看到重複的依賴, 進而刪除它們來減少方法數.
Classyshark 是一個Android可執行檔案的瀏覽器. 用這個工具你可以開啟Android的可執行檔案(.jar, .class, .apk, .dex, .so, .aar, 和Android XML)來分析它的內容.
apk-method-count 這是一個工具, 用來快速地查apk中的方法數, 拖拽apk之後就會得到結果.
APK: Android application package 是Android系統的一種檔案格式, 實際上是一種壓縮檔案, 如果把.apk重新命名為.zip, 就可以取出其內容.
但是此時我們直接在文字編輯器開啟AndroidManifest.xml的時候看到的全是機器碼.
當然是有工具來幫我們分析這些東西的, 這個工具從一開始就有, 那就是aapt, 它是Android Build Tool的一部分.
aapt - Android Asset Packaging Tool 這個工具可以用來檢視和增刪apk中的檔案, 打包資源, 研究PNG檔案等等.
它的位置在: <path_to_android_sdk>/build-tools/<build_tool_version_such_as_24.0.2>/aapt
.
aapt能做的事情, 從man可以看出:
- aapt list - Listing contents of a ZIP, JAR or APK file.
- aapt dump - Dumping specific information from an APK file.
- aapt package - Packaging Android resources.
- aapt remove - Removing files from a ZIP, JAR or APK file.
- aapt add - Adding files to a ZIP, JAR or APK file.
- aapt crunch - Crunching PNG files.
用這個工具來分析我們的apk:
輸出基本資訊:
aapt dump badging app-debug.apk
輸出宣告的許可權:
aapt dump permissions app-debug.apk
輸出配置:
aapt dump configurations app-debug.apk
還有其他這些:
# Print the resource table from the APK.
aapt dump resources app-debug.apk
# Print the compiled xmls in the given assets.
aapt dump xmltree app-debug.apk
# Print the strings of the given compiled xml assets.
aapt dump xmlstrings app-debug.apk
# List contents of Zip-compatible archive.
aapt list -v -a app-debug.apk
Redux是一個當前JavaScript中很火的構架模式. Reductor把它的概念借鑑到了Java和Android中.
關於狀態管理到底有什麼好方法呢, 作者想到了前端開發中的SPA(Single-page application), 和Android應用很像, 有沒有什麼可借鑑的呢? 答案是有.
Redux 是一個JavaScript應用的可預測的狀態容器, 可以用下面三個基本原則來描述:
- 單一的真相來源
- 狀態只讀
- 變化是純函式造成的
Redux的靈感來源有Flux和Elm Architecture.
強烈建議閱讀一下它的文件.
Reductor是作者用Java又實現了一次Redux.
作者用了一個Todo app的例子來說明如何使用, 以及它的好處.
作者先寫了一個naive的實現, 然後不斷地舉出它的缺點, 然後改進它.
其中作者用到了pcollection來實現persistent/immutable的集合.
最後還把程式碼改為對測試友好的.
開始作者舉了一個例子, 一個自定義View, subscribe了Authenticator單例的username變化事件, 從而更新UI.
public class HeaderView extends FrameLayout {
private final Authenticator authenticator;
public HeaderView(Context context, AttributeSet attrs) {...}
@Override protected void onFinishInflate() {
final TextView usernameView = (TextView) findViewById(R.id.username);
authenticator.username().subscribe(new Action1<String>() {
@Override public void call(String username) {
usernameView.setText(username);
}
});
}
}
但是程式碼存在一個主要的問題: 我們從來沒有unsubscribe. 這樣匿名內部類物件就持有外部類物件, 整個view hierarchy就洩露了, 不能被GC.
為了解決這個問題, 在View的onDetachedFromWindow()
回撥裡呼叫unsubscribe()
.
作者以為這樣解決了問題, 但是並沒有, 還是檢測出了洩露, 並且作者發現View的onAttachedToWindow()
和onDetachedFromWindow()
都沒有被呼叫.
作者研究了onAttachedToWindow()
的呼叫時機:
- When a view is added to a parent view with a window, onAttachedToWindow() is called immediately, from addView().
- When a view is added to a parent view with no window, onAttachedToWindow() will be called when that parent is attached to a window.
而作者的佈局是在Activity的onCreate()
裡面setContentView()
設定的.
這時候每一個View都收到了View.onFinishInflate()
回撥, 卻沒有調View.onAttachedToWindow()
.
View.onAttachedToWindow()
is called on the first view traversal, sometime after Activity.onStart()
.
onStart()
方法是不是每次都會呼叫呢? 不是的, 如果我們在onCreate()
裡面呼叫了finish()
, onDestroy()
會立即執行, 而不經過其中的其他生命週期回撥.
明白了這個原理之後, 作者的改進是把訂閱放在了View.onAttachedToWindow()
裡, 這樣就不會洩露了. 對稱總是好的.
作者用例子說明了如何自定義註解和其處理器, 讓被標記的類自動成為Parcelable的.
看了這個有助於理解各種依賴和了解相關的目錄結構.
在Android應用中, 經常需要展示List, 那就需要一個Adapter來持有資料.
RecyclerView的基本操作是: 建立一個view, 然後這個ViewHolder顯示view資料; 把這個ViewHolder和adapter持有的資料繫結, 通常是一個model classes的list.
當資料型別只有一種時, 實現很簡單, 不容易出錯. 但是當要顯示的資料有很多種時, 就變得複雜起來.
首先你需要覆寫:
override fun getItemViewType(position: Int) : Int
預設是返回0, 實現以後把不同的type轉換為不同的整型值.
然後你需要覆寫:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
為每一種type建立一個ViewHolder.
第三步是:
override fun onBindViewHolder(holder: ViewHolder, position: Int): Any
這裡沒有type引數.
The Uglyness
好像看起來沒有什麼問題?
讓我們重新看getItemViewType()
這個方法. 系統需要給每一個position都對應一個type, 所以你可能會寫出這樣的程式碼:
if (things.get(position) is Duck) {
return TYPE_DUCK
} else if (things.get(position) is Mouse) {
return TYPE_MOUSE
}
這很醜不是嗎?
如果你的ViewHolder沒有一個共同的基類, 在binding的時候也是這麼醜:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val thing = things.get(position)
if (thing is Animal) {
(holder as AnimalViewHolder).bind(thing as Animal)
} else if (thing is Car) {
(holder as CarViewHolder).bind(thing as Car)
}
...
}
很多的instance-of和強制型別轉換, 它們都是code smells. 違反了很多軟體設計的原則, 並且當我們想要新添一種型別時, 需要改動很多方法. 我們的目標是新增新型別的時候不用更改Adapter之前的程式碼.
開閉原則: Open for Extension, Closed for Modification.
Let's Fix It
用一個map來查詢? 不好.
把type放在model裡? 不好.
解決問題的一種辦法是: 加入ViewModel, 作為中間層.
但是如果你不想建立很多的ViewModel類, 還有其他的辦法: Visitor模式
interface Visitable {
fun type(typeFactory: TypeFactory) : Int
}
interface Animal : Visitable
interface Car : Visitable
class Mouse: Animal {
override fun type(typeFactory: TypeFactory)
= typeFactory.type(this)
}
工廠:
interface TypeFactory {
fun type(duck: Duck): Int
fun type(mouse: Mouse): Int
fun type(dog: Dog): Int
fun type(car: Car): Int
}
返回對應的id:
class TypeFactoryForList : TypeFactory {
override fun type(duck: Duck) = R.layout.duck
override fun type(mouse: Mouse) = R.layout.mouse
override fun type(dog: Dog) = R.layout.dog
override fun type(car: Car) = R.layout.car
現在有兩個主流的libraries為Android 應用提供了好看的intro screens, 但是感覺並不是很好用, 所以作者他們釋出了一個新的歡迎介面的庫TangoAgency/material-intro-screen, 好用易擴充套件.
本文討論God Object, Blob, 這種很大的類和方法, 做了很多事情. 如果你想要重構, 先加點測試, 也發現很難, 因為它的依賴太多了, 做了太多事情.
首先, 例項化:
加set方法, 讓資料庫依賴抽離出來, 這樣測試的時候可以傳一個Fake的進去.
第二, 更多依賴:
把UserManger和網路請求等依賴也抽為成員變數, 加上set方法或者構造引數, 這樣在測試的時候易於把mock的東西傳進去.
第三, 清理: 要牢記單一職能原則, 進行職能拆分.
最後, 現實: 清理是一個持續化的過程, 得一步一步來, 有時候小步的改動會幫助你發現另外需要改動的地方.
LIBRARIES & CODE
AES-256加密的SharedPreferences.
Pury
報告多個不同事件之間的時間, 可用於效能測量.
Floating Action Button, 展開後是一個NavigationView.
易用易擴充套件的歡迎介面.
SPECIALS
資源分享, 包括部落格論壇Video社群等等.