September 25th, 2016
Android Weekly Issue #224html
本期內容包括: Google Play的pre-launch報告; Wear的Complications API; Android Handler解析; RxAndroid; 測量性能的庫: Pury; 方法數限制; APK內容分析; Redux for Android; 一種view形成的泄露; 註解處理; 更好的Adapter; Intro屏等等.前端
Google Play team在I/O 2016的時候宣佈了不少新features, 其中有一個pre-launch report.java
這個report是幹什麼的呢, 它會報告在一些設備上測試你的應用的時候發現的issues.android
要生成這種報告, 你應該在Developer console上enable它. 而後上傳alpha/beta apk. 上傳到beta channel以後, 5-10分鐘就會生成報告.git
報告主要包括三個部分:github
官方文檔: pre-launch數據庫
在鐘錶的定義裏, complications是指表上除了小時和分鐘指示以外其餘的東西.apache
在Android Wear裏面咱們已經有一些complications的例子, 好比向用戶顯示計步器, 天氣預報, 下一個會議時間等等.redux
可是以前有一個很大的限制就是每個小應用都必須實現本身的邏輯來取數據, 好比有兩個應用都取了今天的天氣預報信息, 將會有兩套機制取一樣的數據, 這明顯是一種浪費.c#
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();
最後, 這裏是官網文檔:
Complications.
這裏是做者PR: PR
首先, 做者舉了一個簡單的例子, 用兩種方法, 用Handler來實現下載圖片並顯示到ImageView上的過程.
主要是由於網絡請求須要在非UI線程, 而View操做須要在UI線程. Handler就用來在這兩種線程之間切換調度.
Handler的組成
Handler
Handler是線程間消息傳遞的直接接口, 生產者和消費者線程都是經過調用下面的操做和Handler交互:
每個Handler都是和一個Looper和一個Message Queue關聯的. 有兩種方法來建立一個Handler:
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:
@DebugLog
註解來標記方法, 而後參數, 返回值, 執行時間都會log出來.Missing tool
關於咱們關心的應用的速度問題, 大多數能夠分爲兩種:
做者意識到下面的需求沒有被知足:
基於上面的需求, 做者建立了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能夠看出:
用這個工具來分析咱們的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()
的調用時機:
而做者的佈局是在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-apt.
Parcelable.
相關庫代碼: aitorvs/auto-parcel.
在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的東西傳進去.
第三, 清理: 要牢記單一職能原則, 進行職能拆分.
最後, 現實: 清理是一個持續化的過程, 得一步一步來, 有時候小步的改動會幫助你發現另外須要改動的地方.
AES-256加密的SharedPreferences.
報告多個不一樣事件之間的時間, 可用於性能測量.
Floating Action Button, 展開後是一個NavigationView.
易用易擴展的歡迎界面.
資源分享, 包括博客論壇Video社區等等.