天天看點

Android Weekly Notes Issue #224

本期内容包括: Google Play的pre-launch報告; Wear的Complications API; Android Handler解析; RxAndroid; 測量性能的庫: Pury; 方法數限制; APK内容分析; Redux for Android; 一種view造成的洩露; 注解處理; 更好的Adapter; Intro屏等等.

Android Weekly Issue #224

September 25th, 2016

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是幹什麼的呢, 它會報告在一些裝置上測試你的應用的時候發現的issues.

要生成這種報告, 你應該在Developer console上enable它. 然後上傳alpha/beta apk. 上傳到beta channel之後, 5-10分鐘就會生成報告.

報告主要包括三個部分:

  • Crashes
  • Screenshots
  • Security

官方文檔: pre-launch

Wear Complications API

在鐘表的定義裡, 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();
           

最後, 這裡是官網文檔:

Complications.

這裡是作者PR: PR

Android Handler Internals

首先, 作者舉了一個簡單的例子, 用兩種方法, 用Handler來實作下載下傳圖檔并顯示到ImageView上的過程.

主要是因為網絡請求需要在非UI線程, 而View操作需要在UI線程. Handler就用來在這兩種線程之間切換排程.

Handler的組成

  • Handler
  • Message
  • Message Queue
  • Looper

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是一個包含任意資料的容器, 它包含的資料資訊是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 是一個無邊界的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 從消息隊列中讀取消息, 然後分發給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疊代都會取出下一個消息.

Crunching RxAndroid - Part 10 細細咀嚼RxAndroid

作者這個是個系列文章, 本文是part 10.

Android的listener很多, 我們可以通過RxJava把listener都變成發射資訊的源, 然後我們subscribe.

本文舉例講了

Observable.fromCallable()

Observable.fromAsync()

方法的用法.

Pury a new way to profile your Android application

在做任何優化之前我們都應該先定位問題. 首先是收集性能資料, 如果收集到的資訊超過了可以接受的門檻值, 我們再進一步深究, 找到引起問題的方法或者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

處理方法數限制問題 Dealing With the 65K Methods limit on Android

作為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之後就會得到結果.

What's in the 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
           

Reductor - Redux for Android

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的集合.

最後還把代碼改為對測試友好的.

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()

作者以為這樣解決了問題, 但是并沒有, 還是檢測出了洩露, 并且作者發現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()

裡, 這樣就不會洩露了. 對稱總是好的.

Annotation Processing in Android Studio 注解和其處理器

作者用例子說明了如何自定義注解和其處理器, 讓被标記的類自動成為Parcelable的.

看了這個有助于了解各種依賴和了解相關的目錄結構.

建議使用: android-apt.

Parcelable.

相關庫代碼: aitorvs/auto-parcel.

Writing Better Adapters 寫出更好的Adapter

在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
           

Material Intro Screen for Android Apps

現在有兩個主流的libraries為Android 應用提供了好看的intro screens, 但是感覺并不是很好用, 是以作者他們釋出了一個新的歡迎界面的庫TangoAgency/material-intro-screen

, 好用易擴充.

Testing Legacy Code: Hidden Dependencies

本文讨論God Object, Blob, 這種很大的類和方法, 做了很多事情. 如果你想要重構, 先加點測試, 也發現很難, 因為它的依賴太多了, 做了太多事情.

首先, 執行個體化:

加set方法, 讓資料庫依賴抽離出來, 這樣測試的時候可以傳一個Fake的進去.

第二, 更多依賴:

把UserManger和網絡請求等依賴也抽為成員變量, 加上set方法或者構造參數, 這樣在測試的時候易于把mock的東西傳進去.

第三, 清理: 要牢記單一職能原則, 進行職能拆分.

最後, 現實: 清理是一個持續化的過程, 得一步一步來, 有時候小步的改動會幫助你發現另外需要改動的地方.

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社群等等.

作者: 聖騎士Wind

出處: 部落格園: 聖騎士Wind

Github: https://github.com/mengdd

微信公衆号: 聖騎士Wind

Android Weekly Notes Issue #224