滴滴Booster移動APP質量優化框架 學習之旅

 

推薦閱讀:html

滴滴Booster移動App質量優化框架-學習之旅 一java

Android 模塊Api化演練android

不同視角的Glide剖析(一)git

 

1、Booster簡介github

Booster是滴滴最近開源一個的移動應用質量優化框架項目,專門爲移動應用而設計的簡單易用、輕量級、功能強大且可擴展的質量優化工具包,其經過動態發現和加載機制提供可擴展的能力。不過目前優化的功能點不過。web

Booster 主要由 Transformer 和 Task 組成,Transformer 主要用於對字節碼進行掃描或修改(取決於 Transformer 的功能),Task 主要用於構建過程當中的資源處理,爲了知足特異的優化需求,Booster 提供了 Transformer SPI and VariantProcessor SPI容許開發者進行定製,如下是 Booster 的總體框架:數組

 

 SPI帶來的好處顯而易見,不須要改動插件,只須要添加相關依賴,就可隨意動態卸載和裝載TransformerVariantProcessor ,作到插件與TransformerVariantProcessor多線程

徹底解耦。其實SPI化思想應用很普遍,好比美團外賣開源的WMRouter的路由節點裝載、Glide中GlideModule的裝載,不過相關配置信息保存早AndroidManifest文件中的元數據中。oracle

 

2、目前Booster功能特性app

1.動態加載模塊

支持差別化的優化需求,Booster  實現了模塊的動態加載,以便於開發者能在不使用配置的狀況下選擇使用指定的模塊,詳見:booster-task-all、booster-transform-all,也能夠定製task和transform,而後設置classpath。

2.第三方類庫注入

支持動態添加依賴或者注入某些類和庫(好比插樁、無埋點統計等),詳見:booster-transform-lint。

3.多線程優化

 業務線衆多的 APP 廣泛存在線程過載的問題,而線程管理一直是開發者最頭疼的問題之一,雖然能夠經過制定嚴格的代碼規範來歸避此類問題發生而對於第三方 SDK 來講,代碼規範則有些力不從心。爲了完全的解決這一問題,Booster 經過在編譯期間修改字節碼實現了全局線程池優化,並對線程進行重命名。詳見:booster-transform-thread。

 4.SharedPreferences 優化

SharedPreferences幾乎無處不在,而在主線程中修改 SharedPreferences 會致使卡頓甚至 ANR,爲了完全的解決這一問題,Booster 對 APP 中的指令進行了全局的替換。詳見:booster-transform-shared-preferences。

5.常量字段刪除

不管是資源索引,仍是其它常量字段,在編譯完成後,就沒有存在的價值了(反射除外),所以,Booster 將對資源索引字段訪問的指令替換爲常量指令,將其它常量字段從類中刪除,一方面能夠提高運行時性能,另外一方面,還能減少包體積,資源索引(R)表面上看起來微不足道,實際上佔用很多空間。

6.資源壓縮

APP 的包體積也是一個很是重要的指標,在 APP 安裝中,圖片資源佔了至關大的比例,一般狀況下,圖片質量下降 10%-20% 並不會影響視覺效果,所以,Booster 採用有損壓縮來下降圖片的大小,並且,圖像尺寸越小,加載速度越快,佔用內存越少。

Booster 提供了兩種壓縮方案:

1.pngquant 有損壓縮(須要自行安裝 pngquant 命令行工具)

2.cwebp 有損壓縮(已內置)

7.性能檢測

APP 的卡頓率是衡量應用運行時性能的一個重要指標,爲了能提早發現潛在的卡頓問題,Booster 經過靜態分析實現了性能檢測,並生成可視化的報告幫助開發者定位問題所在,

其實現原理是經過分析全部的 class 文件,構建一個全局的 Call Graph, 而後從 Call Graph 中找出在主線程中調用的鏈路(Application、四大組件、ViewWidget等相關的方法),而後再將這些鏈路以類爲單位分別輸出報告,詳見:booster-transform-lint。

8.WebView 預加載

爲了解決 WebView  初始化致使的卡頓問題,Booster 經過注入指令的方式,在主線程空閒時提早加載 WebView。

 

 3、Booster框架實現原理

  Booster以gradle插件形式實現,Transform,Task分別抽象爲Transformer ,VariantProcessor,,支持Transformer SPI化 和VariantProcessor SPI化,供外部定製使用

,插件實現代碼以下:

class BoosterPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        when {
            project.plugins.hasPlugin("com.android.application") -> project.getAndroid<AppExtension>().let { android ->
                //註冊Transform,spi 加載全部實現Transformer類
                android.registerTransform(BoosterAppTransform())
                project.afterEvaluate {
                    //spi 加載全部實現VariantProcessor類,並迭代執行全部VariantProcessor
                    ServiceLoader.load(VariantProcessor::class.java, javaClass.classLoader).toList().let { processors ->
                        android.applicationVariants.forEach { variant ->
                            processors.forEach { processor ->
                                processor.process(variant)
                            }
                        }
                    }
                }
            }
            project.plugins.hasPlugin("com.android.library") -> project.getAndroid<LibraryExtension>().let { android ->
                //註冊Transform,spi 加載全部實現Transformer類,並迭代執行全部Transformer
android.registerTransform(BoosterLibTransform()) project.afterEvaluate { //spi 加載全部實現VariantProcessor類,並迭代執行全部VariantProcessor ServiceLoader.load(VariantProcessor::class.java, javaClass.classLoader).toList().let { processors -> android.libraryVariants.forEach { variant -> processors.forEach { processor -> processor.process(variant) } } } } } } } }

 

從插件源碼中,顯而易見能夠找到VariantProcessor SPI化的地方,那Transformer SPI化的邏輯在哪裏了? BoosterAppTransform和BoosterLibTransform都繼承BoosterTransform,而BoosterTransform的Transform功能委託給BoosterTransformInvocation(支持增量和全量Transform),從其源碼中輕易找到Transformer SPI化的邏輯,相關代碼以下:

internal class BoosterTransformInvocation(private val delegate: TransformInvocation) : TransformInvocation, TransformContext, TransformListener, ArtifactManager {

    /*
     * Preload transformers as List to fix NoSuchElementException caused by ServiceLoader in parallel mode
     */
    private val transformers = ServiceLoader.load(Transformer::class.java, javaClass.classLoader).toList()

//迭代回調onPostTransform
override fun onPostTransform(context: TransformContext) = transformers.forEach { it.onPostTransform(this) } ... }

 

內置Transformer的子類只有AsmTransformer,這裏又對ClassTransformer進行SPI化

@AutoService(Transformer::class)
class AsmTransformer : Transformer {

    /*
     * Preload transformers as List to fix NoSuchElementException caused by ServiceLoader in parallel mode
     */
    private val transformers = ServiceLoader.load(ClassTransformer::class.java, javaClass.classLoader).toList()
    
//迭代回調onPostTransform
override fun onPostTransform(context: TransformContext) { transformers.forEach { it.onPostTransform(context) } } ... }

 ps:AutoService 自動生成MEATA_INF配置文件

全部這裏定製Transformer,能夠兩種方式:

1、自定義Transformer ,使用AutoService註解該類

2、自定義ClassTransformer,由AsmTransformer 遞歸回調ClassTransformer的onPostTransform,使用AutoService註解該類

官方內置Transformer 定製,傾向使用第二種方式,好比:booster-transform-threadbooster-transform-lint等。

 

4、定製Transformer 實踐

在官方文檔中添加內置的Transformer 和task時,是在Root Project中添加依賴classpath,classpath通常是添加buildscript自己須要運行的東西,buildScript是用來加載gradle腳本自身須要使用的資源,能夠聲明的資源包括依賴項、第三方插件、maven倉庫地址等。而app/lib中的gradle中dependencies 中添加的使應用程序所須要的依賴包,也就是項目運行所須要的東西。

在之前一直覺得用final 和static修飾的字段都是常量,其實否則,常量(或者說是常量變量)僅可能爲簡單類型或者String類型,是被一個常量表達式初始化,而且必須爲final的,在Java語言規範(§4.12.4)中,對於常量有着明確的定義。

這裏實現打印全部類的final 和static修飾的字段的Transformer,新建一個java libModule,實現Transformer關鍵代碼以下:

override fun transform(context: TransformContext, klass: ClassNode): ClassNode {
    klass.printConstantFields()
    return klass
}

private fun ClassNode.printConstantFields() {
    fields.map {
        it as FieldNode
    }.filter {
         0 != (Opcodes.ACC_STATIC and it.access) && 0 != (Opcodes.ACC_FINAL and it.access) //&& it.value != null
    }.forEach {
           //        fields.remove(it)
         logger.println("field: `$name.${it.name} : ${it.desc}` = ${it.valueAsString()}")
    }
}
 

FieldNode表明字段Node,value字段的值只能爲基本類型和String,那麼字段爲引用類型,其value值爲何了?

在AppModule中定義User定義以下:

 

發佈定製Transformer的module,再配置Root project 的classpath,進行打包後,查看Transformer報告以下:

由此能夠判斷類中字段爲常量的依據爲:

//it爲FieldNode
0
!= (ACC_STATIC and it.access) && 0 != (ACC_FINAL and it.access) && it.value != null

 

5、內置Transformers、tasks分析

分析完了Booster框架Spi原理後,接下來依次分析內置Transformers、tasks,從簡單的入手

Transformer實際上是對字節碼進行修改,新增,刪除等操做,在上車以前須要瞭解ASM,Java字節碼指令集。這裏簡單介紹下本文用到的ASM知識點。

MethodNode中大多數屬性和方法都和ClassNode相似,其中最主要的屬性就是InsnList了,InsnList是一個雙向鏈表對象,包含了存儲方法

的字節指令序,每條指令對應一個AbstractInsnNode(表明字節碼指令的抽象)。MethodInsnNode是繼承AbstractInsnNode表明一次方法調用,

關鍵屬性定義以下:

public class MethodInsnNode extends AbstractInsnNode {

    //方法所在的類
    public String owner;

    //方法名
    public String name;
   
   //方法的形參和返回值類型描述 如:(Ljava/lang/Thread;Ljava/lang/String;)Ljava/lang/Thread;
   public String desc; //方法是否認義在接口中 

public boolean itf;
}

 

booster-transform-shared-preferences

咱們都知道shared-preferences的commit的操做(有返回值),有可能阻塞ui線程。Booster對沒有用到返回值的commit操做放到異步線程中去,對應Transformer實現以下:

override fun transform(context: TransformContext, klass: ClassNode): ClassNode {
        if (klass.name == SHADOW_EDITOR) {
            return klass
        }
        
       //遍歷ClassNode中opcode爲INVOKEINTERFACE和owner爲android/content/SharedPreferences$Editor的methodNodes
        klass.methods.forEach { method ->
//遍歷methodNode篩選出MethodInsnNode            
method.instructions?.iterator()?.asIterable()?.filterIsInstance(MethodInsnNode::class.java)?.filter { it.opcode == Opcodes.INVOKEINTERFACE && it.owner == SHARED_PREFERENCES_EDITOR }?.forEach { invoke -> when ("${invoke.name}${invoke.desc}") { "commit()Z" -> if (Opcodes.POP == invoke.next?.opcode) { // if the return value of commit() does not used // use asynchronous commit() instead invoke.optimize(klass, method) method.instructions.remove(invoke.next) } "apply()V" -> invoke.optimize(klass, method) } } } return klass } private fun MethodInsnNode.optimize(klass: ClassNode, method: MethodNode) { logger.println(" * ${this.owner}.${this.name}${this.desc} => $SHADOW_EDITOR.apply(L$SHARED_PREFERENCES_EDITOR;)V: ${klass.name}.${method.name}${method.desc}") this.itf = false this.owner = SHADOW_EDITOR this.name = "apply" this.opcode = Opcodes.INVOKESTATIC this.desc = "(L$SHARED_PREFERENCES_EDITOR;)V" }

 

你們可能對這些字節碼操做的邏輯看不懂,不要緊,能夠藉助AndroidStudio查看字節碼指令對照下,就秒懂了,編寫以下圖代碼,查看Kotin 的ByteCode如圖:

 

對照上圖,調用1比調用2多了條POP指令,因此多了條Opcodes.POP == invoke.next?.opcode過濾條件,調用1比調用3多了POP指令,因此修改invoke後,須要從method的instructions中刪除invoke.next。

查看Booster把不用到返回值的commit操做放到異步線程中去,具體實現以下:

public class ShadowEditor {

    public static void apply(final SharedPreferences.Editor editor) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    editor.commit();
                }
            });
        } else {
            editor.commit();
        }
    }

}

Editor.apply原本就是異步的,不須要出處理,只把在主線程中調用不使用返回值的Editor.commit操做放到AsyncTask的SERIAL_EXECUTOR線程池中異步調用。

 

booster-transform-shrink

該transform的做用使刪除應用包中多餘的常量(基本類型和String);不管是資源索引,仍是其它常量字段,在編譯完成後,就沒有存在的價值了(反射除外),所以,Booster 將對資源索引字段訪問的指令替換爲常量指令,將其它常量字段從類中刪除,一方面能夠提高運行時性能,另外一方面,還能減少包體積,資源索引(R)表面上看起來微不足道,實際上佔用很多空間。

有人可能對常量在編譯完成後,沒有存在的價值了(反射除外),不太理解,可使用常量,查看其字節碼,見下圖:

 

在字節碼並無看到引用User類中常量的信息,而是直接使用了常量的值,因此類中的常量字段在編譯後,就沒有價值了。就能夠刪除了,固然反射該常量字段時,就不能刪除了。該transform shrink的主要邏輯實現步驟以下:

1.讀取符號列表symbolList,步驟6用到

2.讀取白名單,支持類和常量字段粒度。

3.考慮三方庫須要保留的常量

4.刪除多餘R類

5.刪除常量

6.用常量替換資源索引

 

1.讀取符號列表

在編譯和打包過中會生成一些中間輔助文件,符號列表文件就是其中之一,以下圖:

 

符號列表文件路徑爲

 

符號列表文件保存了App和Lib模塊的全部R類merged後的全部字段信息,格式以下

dataType type name value
//int id always 0x7f08001c
//int[] styleable ActionBar { 0x7f030031, 0x7f030033,....}
dateType: int和 int[] 兩種,資源名對應的值得類型,dimen,anim的值等都爲int類型,自定義控件的styleable爲int[]
type:  dimen、anim、id等資源類型
name: 資源名
value: 資源名對應的值

 

2.讀取白名單,支持類和常量字段粒度

當應用程序想要使用常量字段名(好比反射)和和保留類的全部常量字段時,須要字段名和類名添加到白名單中。

 

3.考慮三方庫須要保留的常量

這裏考慮了com.android.support.constraint包、GreenDao

咱們如今會傾向使用ConstrainLayout減小布局層級,有可能會使用到Group和Barrier,這兩個組件有個constraint_referenced_ids屬性,根據該屬性的值獲取id名數組,而後根據id名值知道id的值,這個過程會設計到反射和Resource.getIdentifier,須要把這些id名添加到保留字段集合中,若存在須要保留字段,就需保留其R.$id類,添加白名單,避免該R.$id類被刪除。

從Android Studio 3.0開始,google默認開啓了aapt2做爲資源編譯的編譯器,aapt2的出現,爲資源的增量編譯提供;aapt2將原先的資源編譯打包過程拆分紅了兩部分,即編譯和連接,這樣就能很好的提高資源的編譯性能,好比只有一個資源文件發送改變時,你只須要從新編譯改變的文件,而後將其與其餘未改變的資源進行連接便可。編譯後的產物路徑,以下圖:

編譯後的產物都是.flat文件,Booster只須要從layout編譯產物中搜索constraint_referenced_ids,有意思的是進行搜索時,使用到Fork/Join框架,這是Java7提供了的一個用於並行執行任務的框架, 是一個把大任務分割成若干個小任務,最終彙總每一個小任務結果後獲得大任務結果的框架。.flat文件的解析這裏就不介紹了。

GreenDao中每個Entity,插件會自動生成EntityDao,每個Dao都有TABLENAME的常量,GreenDao初始化的時候會經過DataConfig反射獲取各個Entity的TABLENAME常量,因此須要保留該常量字段。

4.刪出多餘R類文件

appModule、libModule,、三方庫中R類會在中間產物目錄/javac/下存有一個份R類的文件,以下圖:

 

那麼爲何能刪除多餘的R類文件了?當AppModule添加libModule,三方庫的依賴時,會把libModule、三方庫中R類merge到AppModule中的R類。咱們驗證下,在libModule中添加資源:

<string name="library_str">Library</string>

在中間產物目錄/javac/下打開appModule的R類,搜索library_str,能夠搜索獲得。在打包生成的apk文件經過Jadx-gui查看appModule的R類一樣能夠搜索到library_str。

 

搜索冗餘的R類文件的邏輯:

private fun TransformContext.findRedundantR(): List<Pair<File, String>> {
        return artifacts.get(JAVAC).map { classes ->
            val base = classes.toURI()

            classes.search { r ->
                r.name.startsWith("R") && r.name.endsWith(".class") && (r.name[1] == '$' || r.name.length == 7)
            }.map { r ->
                r to base.relativize(r.toURI()).path.substringBeforeLast(".class")
            }
        }.flatten().filter {
            it.second != appRStyleable // keep application's R$styleable.class
        }.filter { pair ->
            !ignores.any { it.matches(pair.second) }
        }
    }

 

爲何要過濾掉app的 R$styleable.文件了?

由於自定義的控件的styleable爲int[]類型,好比:

public final class R$styleable {
     public static final int[] ActionBar = new int[]{2130903089,...};

}

int[]類型很是量,前文已經知道AppModule R$styleable已經整合了libModule和三方庫的R$styleable,因此須要保留app的 R$styleable.文件。

 

5.刪除常量

在前文定製transform時,已經知道常量的條件爲

//it爲FieldNode
0 != (ACC_STATIC and it.access) && 0 != (ACC_FINAL and it.access) && it.value != null

 

再過濾掉白名單就能夠刪除剩下刪除字段了

 

6.用常量替換資源索引

咱們都清楚libModule中R文件的靜態變量並無賦予final屬性,即不是常量,用到資源的地方,就須要經過資源索引,能夠查看libModule中有資源引用的類的字節碼進行驗證,以下圖:

 

 經過Jadx-guic查看libModule包名下Test類,如圖:

 

第五步會把這些字段都刪除了,不替換的話,顯然程序會崩潰,在第一步獲取的符號列表symbolList存有全部R類資源字段的相關信息,從符號列表中提取資源索引對應的值,LDC指令替換掉GETSTATIC指令。

 

待續!

 

參考資料:

滴滴Booster doc 

滴滴開源 Booster:移動APP質量優化框架

Java字節碼指令大全

ASM(六) 利用TreeApi 動態生成以及轉換方法字節碼

RecursiveTask和RecursiveAction的使用 以及java 8 並行流和順序流

aapt2 資源 compile 過程

Android 編譯打包的那些疑問

 

 若是您對博主的更新內容持續感興趣,請關注公衆號!

相關文章
相關標籤/搜索