Android Weekly Issue #224
September 25th, 2016
Android Weekly Issue #224
本期內容包括: Google Play的pre-launch報告; Wear的Complications API; Android Handler解析; RxAndroid; 測量性能的庫: Pury; 方法數(shù)限制; APK內容分析; Redux for Android; 一種view造成的泄露; 注解處理; 更好的Adapter; Intro屏等等.
ARTICLES & TUTORIALS
Apk的pre-launch報告 Awesome pre-launch reports for Alpha/Beta APK's
Google Play team在I/O 2016的時候宣布了很多新features, 其中有一個pre-launch report.
這個report是干什么的呢, 它會報告在一些設備上測試你的應用的時候發(fā)現(xiàn)的issues.
要生成這種報告, 你應該在Developer console上enable它. 然后上傳alpha/beta apk. 上傳到beta channel之后, 5-10分鐘就會生成報告.
報告主要包括三個部分:
- Crashes
- Screenshots
- Security
官方文檔: pre-launch
Wear Complications API
在鐘表的定義里, complications是指表上除了小時和分鐘指示之外其他的東西.
在Android Wear里面我們已經有一些complications的例子, 比如向用戶顯示計步器, 天氣預報, 下一個會議時間等等.
但是之前有一個很大的限制就是每一個小應用都必須實現(xiàn)自己的邏輯來取數(shù)據, 比如有兩個應用都取了今天的天氣預報信息, 將會有兩套機制取同樣的數(shù)據, 這明顯是一種浪費.
Android Wear 2.0推出了Complications API解決了這個問題.
通信主要是Data providers和Watch faces之間的, 前者包含取數(shù)據的邏輯, 后者負責顯示.
Complications API定義了一些Complications Types, 見官方文檔.
作者在他朋友的開源應用里用了新的API: Memento-Namedays, 這個應用是生日或者日期提醒類的.
首先, 作者用Wearable Data Layer API同步了手機和手表的數(shù)據. 然后在Wear module里繼承ComplicationProviderService
創(chuàng)建了complication data provider, 這里就提供了onComplicationActivated
, onComplicationDeactivated
, onComplicationUpdate
等回調.
用戶也可以點擊Complications, 可以用setTapAction()
指定點擊后要啟動的Activity.
可以指定ComplicationProviderService
的更新頻率, 是在manifest里用這個key:
android.support.wearable.complications.UPDATE_PERIOD_SECONDS
.
更新得太頻繁會比較費電.
需要注意的是這并不是一個常量, 因為系統(tǒng)也會根據手機的狀況進行一些調節(jié), 不必要的時候就不需要頻繁更新.
本文作者采用的方式是用ProviderUpdateRequester
. 在manifest里面設置0.
ComponentName providerComponentName = new ComponentName(
context,
MyComplicationProviderService.class
);
ProviderUpdateRequester providerUpdateRequester = new
ProviderUpdateRequester(context, providerComponentName);
providerUpdateRequester.requestUpdateAll();
最后, 這里是官網文檔:
Complications.
這里是作者PR: PR
Android Handler Internals
首先, 作者舉了一個簡單的例子, 用兩種方法, 用Handler來實現(xiàn)下載圖片并顯示到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關聯(lián)的. 有兩種方法來創(chuàng)建一個Handler:
- 用默認構造器, 將會使用當前線程的Looper.
- 顯式地指明要用的Looper.
Handler不能沒有Looper, 如果構造時沒有指明Looper, 當前線程也沒有Looper, 那么將會拋出異常.
因為Handler需要Looper中的消息隊列.
一個線程上的多個Handler共享同一個消息隊列, 因為它們共享同一個Looper.
Message
Message是一個包含任意數(shù)據的容器, 它包含的數(shù)據信息是callback, data bundle和obj/arg1/arg2, 還有三個附加數(shù)據what, time和target.
可以調用Handler的obtainMessage()
方法來創(chuàng)建Message, 這樣message是從message pool中取出的, target會自動設置成Handler自己. 所以直接可以在后面調用sendToTarget()
方法.
Message pool是一個最大尺寸為50的LinkedList. 當消息被處理完之后, 會放回pool, 并且重置所有字段.
當我們使用Handler來post(Runnable)
的時候, 實際上是隱式地創(chuàng)建一個Message, 它的callback存這個Runnable.
Message Queue
Message Queue 是一個無邊界的LinkedList, 元素是Message對象. 它按照時間順序來插入Message, 所以timestamp最小的最先分發(fā).
MessageQueue中有一個dispatch barrier
表示當前時間, 當message的timestamp小于當前時間時, 被分發(fā)和處理.
Handler提供了一些方法在發(fā)message的時候設置不同的時間戳:
sendMessageDelayed()
: 當前時間 + delay時間.
sendMessageAtFrontOfQueue()
: 把時間戳設為0, 不建議使用.
sendMessageAtTime()
.
Handler經常需要和UI交互, 可能會引用Activity, 所以也經常會引起內存泄漏.
作者舉了兩個例子, 略.
需要注意:
非靜態(tài)內部類會持有外部類實例引用.
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 從消息隊列中讀取消息, 然后分發(fā)給target handler. 每當一個Message穿過了dispatch barrier
, 它就可以在下一個消息循環(huán)中被Looper讀.
一個線程只能關聯(lián)一個Looper. 因為Looper類中有一個靜態(tài)的ThreadLocal對象保證了只有一個Looper和線程關聯(lián), 企圖再加一個就會拋出異常.
調用Looper.quit()
會立即終止Looper, 丟棄所有消息.
而Looper.quitSafely()
會將已經通過dispatch barrier
的消息處理了, 只丟棄pending的消息.
Looper是在Thread的run()
方法里setup的, Looper.prepare()
會檢查是否之前存在一個Looper
和這個線程關聯(lián), 如果有則拋異常, 沒有則建立一個新的Looper
對象, 創(chuàng)建一個新的MessageQueue. 見代碼.
現(xiàn)在Handler
可以接收或者發(fā)送消息到MessageQueue
了. 執(zhí)行Looper.loop()
方法將會開始從隊列讀出消息. 每一個loop迭代都會取出下一個消息.
Crunching RxAndroid - Part 10 細細咀嚼RxAndroid
作者這個是個系列文章, 本文是part 10.
Android的listener很多, 我們可以通過RxJava把listener都變成發(fā)射信息的源, 然后我們subscribe.
本文舉例講了Observable.fromCallable()
和Observable.fromAsync()
方法的用法.
Pury a new way to profile your Android application
在做任何優(yōu)化之前我們都應該先定位問題. 首先是收集性能數(shù)據, 如果收集到的信息超過了可以接受的閾值, 我們再進一步深究, 找到引起問題的方法或者API.
幸運的是, 有一些工具可以幫我們profiling:
-
Hugo 用
@DebugLog
注解來標記方法, 然后參數(shù), 返回值, 執(zhí)行時間都會log出來. - Android Studio toolset. 比如System Trace, 非常準確, 提供了很多信息, 但是需要你花時間來收集和分析數(shù)據.
- 后臺解決方案, 比如JMeter, 它們提供了很多功能, 需要花時間來學習如何使用, 第二就是高并發(fā)profile也不是常見的需求.
Missing tool
關于我們關心的應用的速度問題, 大多數(shù)可以分為兩種:
- 特定方法和API的執(zhí)行時間, 這個可以被Hugo cover.
- 兩個事件之間的時間, 這可能是獨立的兩段代碼, 但是在邏輯上關聯(lián). Android Studio toolset可以cover這種, 但是你需要花很多時間來做profile.
作者意識到下面的需求沒有被滿足:
- 開始和結束profiling應該是被兩個獨立的事件觸發(fā)的, 這樣才可以滿足我們靈活性的需求.
- 如果我們想監(jiān)控performance, 僅僅開始和結束事件是不夠的. 有時候我們需要知道這之間發(fā)生了什么, 這些階段信息應該被放在一個報告里, 讓我們更容易明白和分享數(shù)據.
- 有時候我們需要做重復操作, 比如loading RecyclerView的下一頁, 那么一個回合的操作顯然是不夠的, 我們需要進行多次操作, 然后顯示統(tǒng)計數(shù)據, 比如平均值, 最小最大值.
基于上面的需求, 作者創(chuàng)建了Pury.
Introduction to Pury
Pury是一個profiling的庫, 用于測量多個獨立事件之間的時間.
事件可以通過注解或者方法調用來觸發(fā), 一個scenario的所有事件被放在同一個報告里.
然后作者舉了兩個例子, 一個用來測量啟動時間, 另一個用來測量loading pages.
Inner structure and limitations
性能測量是Profilers
做的, 每一個Profiler
包含一個list, 里面是Runs
. 多個Profilers
可以并行運行, 但是每個Profiler
中只有一個Run
是active的.
Profiling with Pury
Pury可以測量多個獨立事件之間的時間, 事件可以用注解或者方法調用觸發(fā).
基本的注解有: @StartProfiling
, @StopProfiling
, @MethodProfiling
方法:
Pury.startProfiling();
Pury.stopProfiling();
最后作者介紹了一些使用細節(jié).
項目地址: Pury
處理方法數(shù)限制問題 Dealing With the 65K Methods limit on Android
作為Android開發(fā), 你可能會看到過這種信息:
Too many field references: 88974; max is 65536.
You may try using –multi-dex option.
首先, 為什么會存在65k的方法數(shù)限制呢?
Android應用是放在APK文件里的, 這里面包含了可執(zhí)行的二進制碼文件(DEX - Dalvik Executable), 里面包含了讓app工作的代碼.
DEX規(guī)范限制了單個的DEX文件中的方法總數(shù)最大為65535, 包括了Android framework方法, library方法, 還有你自己代碼中的方法. 如果超過了這個限制你將不得不配置你的app來生成多個DEX文件(multidex configuration).
但是開啟了multidex配置之后有一些隨機性的兼容問題, 所以我們在決定開啟multidex之前, 首先采取的第一步是減少方法數(shù)來避免這個問題.
在我們開始改動之前, 先提出了這些問題:
- 我們有多少方法?
- 這些方法都是從哪里來?
- 主要的方法來源是誰?
- 我們真的需要所有這些方法嗎?
在搜尋這些問題的答案的過程中, 我們發(fā)現(xiàn)了一些有用的工具和tips:
MethodsCount.com 將會告訴你一個庫有多少方法, 還提供了每個方法的依賴.
JakeWharton/dex-method-list utility 可以顯示.apk, .aar, .dex, .jar或.class文件中的所有方法引用. 這可以用來發(fā)現(xiàn)一個庫中到底有多少方法是被你的app使用了.
mihaip/dex-method-counts 這個工具可以按包來輸出方法, 計算出一個DEX文件中的方法數(shù)然后按包來分組輸出. 這有利于我們明白哪些庫是方法數(shù)的主要來源.
Gradle build system 提供了關于項目結構很有價值的信息. 一個有用的task是dependencies
, 讓你看到庫的依賴樹, 這樣你就可以看到重復的依賴, 進而刪除它們來減少方法數(shù).
Classyshark 是一個Android可執(zhí)行文件的瀏覽器. 用這個工具你可以打開Android的可執(zhí)行文件(.jar, .class, .apk, .dex, .so, .aar, 和Android XML)來分析它的內容.
apk-method-count 這是一個工具, 用來快速地查apk中的方法數(shù), 拖拽apk之后就會得到結果.
What's in the APK APK中有什么
APK: Android application package 是Android系統(tǒng)的一種文件格式, 實際上是一種壓縮文件, 如果把.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
Reductor - Redux for Android
Redux是一個當前JavaScript中很火的構架模式. Reductor把它的概念借鑒到了Java和Android中.
關于狀態(tài)管理到底有什么好方法呢, 作者想到了前端開發(fā)中的SPA(Single-page application), 和Android應用很像, 有沒有什么可借鑒的呢? 答案是有.
Redux 是一個JavaScript應用的可預測的狀態(tài)容器, 可以用下面三個基本原則來描述:
- 單一的真相來源
- 狀態(tài)只讀
- 變化是純函數(shù)造成的
Redux的靈感來源有Flux和Elm Architecture.
強烈建議閱讀一下它的文檔.
Reductor是作者用Java又實現(xiàn)了一次Redux.
作者用了一個Todo app的例子來說明如何使用, 以及它的好處.
作者先寫了一個naive的實現(xiàn), 然后不斷地舉出它的缺點, 然后改進它.
其中作者用到了pcollection來實現(xiàn)persistent/immutable的集合.
最后還把代碼改為對測試友好的.
Android leak pattern: subscriptions in views
開始作者舉了一個例子, 一個自定義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()
.
作者以為這樣解決了問題, 但是并沒有, 還是檢測出了泄露, 并且作者發(fā)現(xiàn)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()
會立即執(zhí)行, 而不經過其中的其他生命周期回調.
明白了這個原理之后, 作者的改進是把訂閱放在了View.onAttachedToWindow()
里, 這樣就不會泄露了. 對稱總是好的.
Annotation Processing in Android Studio 注解和其處理器
作者用例子說明了如何自定義注解和其處理器, 讓被標記的類自動成為Parcelable的.
看了這個有助于理解各種依賴和了解相關的目錄結構.
建議使用: android-apt.
Parcelable.
相關庫代碼: aitorvs/auto-parcel.
Writing Better Adapters 寫出更好的Adapter
在Android應用中, 經常需要展示List, 那就需要一個Adapter來持有數(shù)據.
RecyclerView的基本操作是: 創(chuàng)建一個view, 然后這個ViewHolder顯示view數(shù)據; 把這個ViewHolder和adapter持有的數(shù)據綁定, 通常是一個model classes的list.
當數(shù)據類型只有一種時, 實現(xiàn)很簡單, 不容易出錯. 但是當要顯示的數(shù)據有很多種時, 就變得復雜起來.
首先你需要覆寫:
override fun getItemViewType(position: Int) : Int
默認是返回0, 實現(xiàn)以后把不同的type轉換為不同的整型值.
然后你需要覆寫:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
為每一種type創(chuàng)建一個ViewHolder.
第三步是:
override fun onBindViewHolder(holder: ViewHolder, position: Int): Any
這里沒有type參數(shù).
The Uglyness
好像看起來沒有什么問題?
讓我們重新看getItemViewType()
這個方法. 系統(tǒng)需要給每一個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, 作為中間層.
但是如果你不想創(chuàng)建很多的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
Material Intro Screen for Android Apps
現(xiàn)在有兩個主流的libraries為Android 應用提供了好看的intro screens, 但是感覺并不是很好用, 所以作者他們發(fā)布了一個新的歡迎界面的庫TangoAgency/material-intro-screen
, 好用易擴展.
Testing Legacy Code: Hidden Dependencies
本文討論God Object, Blob, 這種很大的類和方法, 做了很多事情. 如果你想要重構, 先加點測試, 也發(fā)現(xiàn)很難, 因為它的依賴太多了, 做了太多事情.
首先, 實例化:
加set方法, 讓數(shù)據庫依賴抽離出來, 這樣測試的時候可以傳一個Fake的進去.
第二, 更多依賴:
把UserManger和網絡請求等依賴也抽為成員變量, 加上set方法或者構造參數(shù), 這樣在測試的時候易于把mock的東西傳進去.
第三, 清理: 要牢記單一職能原則, 進行職能拆分.
最后, 現(xiàn)實: 清理是一個持續(xù)化的過程, 得一步一步來, 有時候小步的改動會幫助你發(fā)現(xiàn)另外需要改動的地方.
LIBRARIES & CODE
EncryptedPreferences
AES-256加密的SharedPreferences.
Pury
報告多個不同事件之間的時間, 可用于性能測量.
Floating-Navigation-View
Floating Action Button, 展開后是一個NavigationView.
Material Intro Screen
易用易擴展的歡迎界面.
SPECIALS
Huge list of useful resources for Android development
資源分享, 包括博客論壇Video社區(qū)等等.