Flutter线上代码覆盖率FlutterCodeX

简介: 再也不用担心瘦包的问题~

作者:闲鱼技术-君爱

背景

近年来,闲鱼旧业务在Flutter架构升级下,大量页面通过Flutter开发实现。业务不断迭代,包体积也随之增大,闲鱼Android、iOS安装包大小较去年有较大增加,其中,Flutter在闲鱼包体积中占比20%,闲鱼开发逐步需要考虑进行Flutter侧工程治理。Flutter官方也在为包大小不断努力,致力于降低打包产物的大小,但仍未有成熟方案。因此现阶段,我们可以考虑如何将无效代码下线。

通过人工梳理的方式,依赖于开发人员的业务熟悉程度,难免疏漏。我们需要有准确的的线上代码覆盖率,作为数据依据,推动业务进行行之有效的代码下线。

本文为您介绍,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,由编译时代码插桩插件、运行时数据采集模块组成。

  • 代码插桩插件

编译时,通过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函数。

  • 运行时数据采集模块

运行时,工程中每个类初始化后将会自动调用addCallTime方法,将类调用信息缓存,选择用户退出后台的时机,进行数据文件进行压缩上传,目前我们采用阿里云OSS文件上传。根据应用活跃用户数,设置采样率,命中至少5万用户UV。

  • 数据汇总与产出

最后,线上运行一段时间后,我们将数据汇总,与打包构建产物中的完整类列表进行比对,即可获得线上代码覆盖率数据,推动业务进行行之有效的瘦身。

以简单Demo工程为例:

最后

目前,FlutterCodeX在闲鱼App即将上线,结合客户端Android、iOS代码覆盖率数据,有效地推动废弃业务下线,助力包体瘦身,对工程健康做长效监控与改善。

相关文章
|
Dart Linux API
Flutter 上使用 C/C++ 代码(上)
Flutter 上使用 C/C++ 代码(上)
2886 0
Flutter 上使用 C/C++ 代码(上)
|
1月前
|
Dart
Flutter之ExpansionTile实现以代码方式码折叠、展开
Flutter之ExpansionTile实现以代码方式码折叠、展开
34 4
|
1月前
|
Dart
Flutter笔记:手动配置VSCode中Dart代码自动格式化
Flutter笔记:手动配置VSCode中Dart代码自动格式化
77 5
|
4月前
|
Dart 安全
简化代码、提高效率:Dart和Flutter开发小技巧
在日常开发中,我们常常会使用一些常用的技巧或语法糖,以简化代码、提高开发效率。本文将分享一些在Dart和Flutter中常用的小贴士,帮助你更轻松地编写优雅高效的代码。
简化代码、提高效率:Dart和Flutter开发小技巧
|
4月前
|
Dart 前端开发 Android开发
【Flutter前端技术开发专栏】Flutter中的平台特定代码实现
【4月更文挑战第30天】Flutter旨在实现跨平台移动应用开发,但有时需针对iOS或Android编写特定代码。平台通道是关键机制,允许Dart代码与原生代码交互。通过`MethodChannel`等实现跨平台通信,然后在iOS和Android上响应调用。条件编译则在编译时决定特定平台代码。本文展示了如何在Flutter中处理平台特定功能,包括示例代码和总结。
99 0
【Flutter前端技术开发专栏】Flutter中的平台特定代码实现
|
4月前
|
Dart 前端开发 Android开发
【Flutter前端技术开发专栏】Flutter与原生代码的集成与交互
【4月更文挑战第30天】本文探讨了如何在Flutter中集成和交互原生代码,以利用特定平台的API和库。当需要访问如蓝牙、特定支付SDK或复杂动画时,集成原生代码能提升效率和性能。集成方法包括:使用Platform Channel进行通信,借助现有Flutter插件,以及Android和iOS的Embedding。文中通过一个电池信息获取的例子展示了如何使用`MethodChannel`在Dart和原生代码间传递调用。这些技术使开发者能充分利用原生功能,加速开发进程。
86 0
【Flutter前端技术开发专栏】Flutter与原生代码的集成与交互
|
4月前
|
存储 Dart 前端开发
为什么说 Compose 的声明式代码最简洁 ?Compose/React/Flutter/SwiftUI 语法对比
为什么说 Compose 的声明式代码最简洁 ?Compose/React/Flutter/SwiftUI 语法对比
173 1
|
11月前
|
存储 Dart 前端开发
原来Flutter代码是这样运行在原生系统的!快来了解Flutter标准模板,感受原生系统中Flutter的魅力!
原来Flutter代码是这样运行在原生系统的!快来了解Flutter标准模板,感受原生系统中Flutter的魅力!
74 0
|
开发框架 Dart 开发工具
使用Flutter开发一套可同时运行在Android和iOS平台的代码
Flutter是一种跨平台移动应用开发框架,它允许开发者使用单一代码库构建高性能、美观且可在多个平台上运行的应用程序。本文将介绍如何使用Flutter开发一套同时适用于Android和iOS平台的代码。