非死book是如何收集其Android應用性能數據的
非死book一直致力于不斷提高Android應用的運行速度。雖然他們內部已經有類似 CTScan 這樣的性能跟蹤系統,但Android生態系統的多樣性使他們無法在實驗室中測試每一種可能。因此,他們希望通過遙測技術從人們真實使用的Android手機中收集性能信息來補充測試數據。近日,非死book工程師Delyan Kratunov 撰文 介紹了他們收集Android應用遠程性能檢測數據的方法。
很長一段時間以來,遙測技術都僅限于費力地插入代碼,標識動作的起點和終點。這種方法有諸多弊端:
- 開發者插入的檢測點限制了遙測數據的詳細程度,并導致這種方法只能檢測可以預見的性能影響;
- Android應用的多線程特點以及用戶交互的高度異步特點導致很難徹底檢測代碼;
- 代碼的快速變化會導致已有的檢測標記出現“位衰減”。
同時,Delyan還指出,他們也不希望使用下面這兩種方法:
- 使用Android內置的性能檢測方法 :Dalvik和ART都提供了可以從“Debug”類調用的、方法級的性能分析器。這些方法可以編程觸發,輸出結果保存在開發人員指定的文件中。但是他們發現,startMethodTracing方法開銷很大。更糟糕的是,在某些Android版本中,該方法會禁用Dalvik的JIT編譯器,進一步降低應用性能。總之,該工具會扭曲檢測數據。
- 大幅增加手工插入的檢測點 :手動插入性能檢測點非常耗時且容易出錯。工程師的時間不應該花費在可以自動化的事情上。而且,在一個不斷變化的代碼庫中,確保這類檢測點的正確性需要做大量的工作。
他們所采用的方法,靈感來自于 該領域先前的研究 ,核心是一個基于規則的字節碼重寫器(基于 ASM 庫)。該重寫器可以匹配代碼位置,然后插入或操作代碼。就是說,在Java代碼經javac編譯成Java虛擬機字節碼之后,但是在傳遞給dx轉換成Dalvik VM格式之前,它會介入修改JVM字節碼。
作為構建系統的一部分,該字節碼重寫器會在Android應用的全部Java字節碼上運行,執行少數幾個簡單的轉換,產生大量發生過重寫的代碼位置。例如,下面的規則將在特定方法的入口和出口處插入代碼:
new EntryExitRule.Builder() .setMatcherConfiguration( subclassesOf( getObjectType("android/app/Activity") ).withMethods( getMethod("void onCreate(android.os.Bundle)"), getMethod("void onRestart()"), getMethod("void onStart()"), getMethod("void onResume()"), getMethod("void onPause()"), getMethod("void onStop()"), getMethod("void onDestroy()"))) .setDetourType(LOG_UTILS_TYPE) .setDetourMethodEntry(LOG_METHOD_ACTIVITY_START) .setDetourMethodExit(LOG_METHOD_ACTIVITY_END) .setCategory(Categories.LIFECYCLE) .build()
在運行時,這些方法會在日志中記錄一個或多個檢測事件,并且,這些事件可以組合到一個單獨的跟蹤文件中。他們的檢測粒度是框架調用和回調層。就是說,檢測應用如何同Android框架交互以及框架反過來如何調用應用。這非常有用,因為應用組件不同生命周期之間的交互對運行時性能有重大影響。而且,由于檢測點插入是自動完成的,所以無需擔心代碼變化會影響檢測點。
在字節碼中插入檢測點還有一個好處,就是讓他們能夠透明地處理異步跟蹤。也就是說,他們可以在線程之間自動傳遞足夠的上下文信息。這樣,他們就能將邏輯控制流串連起來。例如,下面的規則是檢測Handler API的:
RedirectionRule.builder() .setMatcherConfiguration( subclassesOf( getObjectType("android/os/Handler") ).withMethods( getMethod("boolean post(Runnable)"), getMethod("boolean postAtFrontOfQueue(Runnable)"), getMethod("boolean postAtTime(Runnable, Object, long)"), getMethod("boolean postAtTime(Runnable, long)"), getMethod("boolean postDelayed(Runnable, long)"), getMethod("void removeCallbacks(Runnable)"))) .setDetourClass("com/非死book/tools/dextr/runtime/detour/HandlerDetour") .setCategory(Categories.ASYNC) .build()
雖然有無數種在線程之間切換控制的方法,但實際上,一個很小的規則集合就可以覆蓋應用中大多數異步代碼。總的來說,這種跨線程跟蹤能力讓他們對應用執行流程有了更深入的了解,可以暴露出一些難以捉摸的性能缺陷,如調度延遲和不必要的異步跳轉。
此外,在實現該方法的過程中,他們還遇到了其它一些需要克服的問題。比如,僅使用基本數據類型。當字節碼重寫器操作應用代碼時,它會在每個代碼位置插入一個唯一標識。在應用構建時,它會生成一個標識與代碼位置的映射。在運行時,他們只記錄32位的整型標識,然后在服務器端轉換成代碼位置。這樣,事件大小就可以固定,而且非常小。同時,這也縮小了跟蹤文件,減少了運行時開銷。此處僅舉一例,更多信息請查看 原文 。