天天看點

Flutter線上代碼覆寫率FlutterCodeX

作者:閑魚技術-君愛

背景

近年來,閑魚舊業務在Flutter架構更新下,大量頁面通過Flutter開發實作。業務不斷疊代,包體積也随之增大,閑魚Android、iOS安裝包大小較去年有較大增加,其中,Flutter在閑魚包體積中占比20%,閑魚開發逐漸需要考慮進行Flutter側工程治理。Flutter官方也在為包大小不斷努力,緻力于降低打包産物的大小,但仍未有成熟方案。是以現階段,我們可以考慮如何将無效代碼下線。

通過人工梳理的方式,依賴于開發人員的業務熟悉程度,難免疏漏。我們需要有準确的的線上代碼覆寫率,作為資料依據,推動業務進行行之有效的代碼下線。

本文為您介紹,Flutter的線上代碼覆寫率解決方案——FlutterCodeX。針對類級别編譯時代碼插粧,運作時背景資料聚合,進行資料采集上報,獲得最終代碼覆寫率資料,推動廢棄業務下線,達到包體瘦身,對工程健康做長效監控與改善。

Flutter線上代碼覆寫率FlutterCodeX

插樁方案探索

線上上代碼覆寫率的統計中,問題的難點主要在于,如何準确判斷類,是否被調用過?一般人會馬上可以想到,隻需要在每個類初始化時,加入一段代碼,标記該類已經被調用,最快的就是建構函數中添加,但成本極高,有沒有自動化、無侵入的插樁方案呢?以下從iOS、Android、Flutter不同的插樁方案進行簡單的對比。

iOS

iOS中,ObjC首次調用類初始化時,+initialize被執行,系統會自動标記已被調用,在 metaClass的 data的flags字段中的 1<<29 位的這個bit RW_INITIALIZED,就記錄着類是否initialize。可以通過判斷類是否被初始化,是以在運作時,找到合适的時機,周遊所有類,進行資料的聚合上傳。

static BOOL MOCClassIsInitilatized(Class cls) {
    void *metaClass = (__bridge void *)object_getClass(cls);
    class_rw_t *rw = *(class_rw_t **)((uintptr_t)metaClass + 4 * sizeof(uintptr_t));
    if(((class_rw_t *)((uintptr_t)rw & FAST_DATA_MASK))->flags & RW_INITIALIZED) {
        return YES;
    }
    return NO;
}
           

Android

Android中,Java語言可以不需要侵入原有代碼,以添加靜态代碼塊的形式添加插樁代碼,buildscript增加編譯插件,在編譯時周遊所有類檔案進行代碼插入即可。

public class A {
    static {
        // todo report class A initialize
    }
}
           

Flutter

Flutter與Android、IOS的方案均有一定差異,Dart沒有Java的靜态代碼塊,也沒有類似ObjC的系統标記。在什麼地方插樁,可以不侵入原有代碼呢?

理論上,Dart Class初始化執行順序為:

  1. class variables initialize on declaration (no static)
  2. initializer list
  3. superclass’s constructor
  4. main class’s constructor

改寫構造函數會直接侵入原有代碼,Dart構造函數的多樣寫法也增加了自動化插件的難度。是以改寫構造器不是第一選擇。根據初始化執行順序,很快可以想到,是否可以增加新的類成員,初始化時調用插樁代碼,以達到類初始化插粧的效果。例如

class A {
    bool isCodeX = ReportUtil.addCallTime('A');
    // ...biz
}
           

但在Dart中,針對擁有常量建構器的類,要求所有的成員均為final,成員初始化必須在第1第2階段,或構造函數入參進行初始化,即使是extends、with也強制要求子類及Mixin所有的變量均為final。而Flutter中,Widget等常用元件,均使用常量建構函數,無法通過這種形式插樁。

class A {
    final num x, y;
    const A(this.x, this.y);
}
           

注入代碼的形式不可用!

還有其他辦法嗎?可不可以通過AOP的方式,hook住所有的類建構器呢?而閑魚技術團隊剛剛開源的AspectD,恰好可以解決這個問題。

AspectD是針對Dart的AOP程式設計架構,通過Transform實作dill變換以實作AOP,可以便捷地實作無侵入代碼自由注入。

在Flutter v1.12.13下驗證,針對常量建構器、無建構函數、命名為ClassName.identifier形式建構函數,均測試通過!AspectD代碼如下:

@Aspect()
@pragma("vm:entry-point")
class CodeXExecute {
    @pragma("vm:entry-point")
    CodeXExecute();

    @Call("package:flutter_codex_demo/test.dart", "A", "+A")
    @pragma("vm:entry-point")
    void _incrementA(PointCut pointcut) {
        pointcut.proceed();
        // todo report class A initialize
    }
}
           

AspectD原理不在此詳細說明,有興趣請移步

https://github.com/alibaba-flutter/aspectd

整體設計方案

FlutterCodeX線上代碼覆寫率SDK,由編譯時代碼插樁插件、運作時資料采集子產品組成。

Flutter線上代碼覆寫率FlutterCodeX
  • 代碼插樁插件

編譯時,通過build_runner,CodeXGenerator與CodeAstVisitor進行工程内所有類ast解析,周遊所有類構造函數,自動生成AspectD的PointCut Execute類檔案,hook類建構函數,在構造函數執行完畢後,插樁标記類調用資訊,同時還生成項目的完整類清單至建構産物。關鍵代碼如下:

CodeAstVisitor:

// visit all class
void visitClassDeclaration(ClassDeclaration node) {
    SourceNode sourceNode = SourceNode(source_path, node.name?.name);
    node.members.forEach((ClassMember member) {
        // find all constructor
        if (member is ConstructorDeclaration) {
            String constructorName = member.name?.name;
        if (constructorName == null || constructorName.isEmpty) {
            // ClassName Constructor
            constructorName = sourceNode.name;
        } else {
            // ClassName.identifier Constructor
            constructorName = (sourceNode.name ?? '') + "\\." + constructorName;
        }
        sourceNode.constructor.add(constructorName);
        return;
    }});

    CodeXGenerator.collector.codeList[sourceNode.key()] = sourceNode;
}
           

AspectD Execute如下圖所示,類A擁有兩個構造函數,生成兩個AspectD AOP函數。

Flutter線上代碼覆寫率FlutterCodeX
  • 運作時資料采集子產品

運作時,工程中每個類初始化後将會自動調用addCallTime方法,将類調用資訊緩存,選擇使用者退出背景的時機,進行資料檔案進行壓縮上傳,目前我們采用阿裡雲OSS檔案上傳。根據應用活躍使用者數,設定采樣率,命中至少5萬使用者UV。

  • 資料彙總與産出

最後,線上運作一段時間後,我們将資料彙總,與打包建構産物中的完整類清單進行比對,即可獲得線上代碼覆寫率資料,推動業務進行行之有效的瘦身。

以簡單Demo工程為例:

Flutter線上代碼覆寫率FlutterCodeX

最後

目前,FlutterCodeX在閑魚App即将上線,結合用戶端Android、iOS代碼覆寫率資料,有效地推動廢棄業務下線,助力包體瘦身,對工程健康做長效監控與改善。

繼續閱讀