写点什么

Flutter 线上代码覆盖率解决方案——FlutterCodeX

  • 2020-05-14
  • 本文字数:2596 字

    阅读完需:约 9 分钟

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 增加编译插件,在编译时遍历所有类文件进行代码插入即可。


publicclass 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")classCodeXExecute{@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 classvoid visitClassDeclaration(ClassDeclaration node) {SourceNode sourceNode = SourceNode(source_path, node.name?.name); node.members.forEach((ClassMember member) {// find all constructorif(member isConstructorDeclaration) {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 代码覆盖率数据,有效地推动废弃业务下线,助力包体瘦身,对工程健康做长效监控与改善。


本文转载自公众号闲鱼技术(ID:XYtech_Alibaba)。


原文链接


https://mp.weixin.qq.com/s/VkjhTSKPpEZVYYsvcLsxCA


2020-05-14 14:062819

评论

发布
暂无评论
发现更多内容

如何在预算确定的情况下发挥财务规划的最大价值?

智达方通

企业管理 全面预算管理

JavaScript Quine揭秘:如何让程序输出自身源代码?

不在线第一只蜗牛

JavaScript

系统容量评估方法

陈一之

构架师 容量设计

京东商品详情API接口(JD.item_get)

tbapi

京东商品详情接口 京东API 京东商品数据采集

假如你从7月开始准备Java面试,如何秋招拿下offer?

程序员高级码农

java 程序员‘

怎么修改网站域名的DNS服务器?

防火墙后吃泡面

混合开发赋能快节奏业务增长:跨端生态构建与敏捷迭代实践

xuyinyin

单卡推理吞吐2300Tokens/s,昇腾AI云服务正在改写算力法则

Alter

AI测试的准确率覆盖率稳定性

测试人

软件测试

芯火三十年:纵横四海(2013-2021)

脑极体

AI

轻松上手|用 TRAE 搭建 AI 健身私教

火山引擎开发者社区

AI Trae

MyEMS能源管理系统后台配置-传感器管理

开源能源管理系统

开源 能源管理

计算架构,行业AI竞争的下一个分水岭

极客天地

OmniGen2重磅升级,统一图像生成再进化

智源研究院

图像生成

这份指南教你如何打造一款 AI 陪伴虚拟角色

火山引擎开发者社区

AI 智能体

国产IT运维监控系统标杆之选:Gartner推荐的嘉为蓝鲸全栈智能可观测中心——IBM Tivoli国产化替代实践

嘉为蓝鲸

AIOPS Gartner 智能运维 可观测 国产IT运维监控系统

抖音内容技术团队开源ContentV:有限算力下高效训练视频生成模型的新路径

字节跳动开源

开源 视频生成 ContentV

分享一个 Cursor mdc 生成器,基于 Gemini 2.5,很实用!

Immerse

字节跳动 ByteBrain 开源 MySQL 虚拟索引 VIDEX:让 AI+DB 也能大规模落地

火山引擎开发者社区

字节跳动

Product Hunt 双料榜首,开发仅用 2 周:00 后创业者的 AI 智能体实践

火山引擎开发者社区

AI DeckSpeed

华为开发者空间全面升级,全新特性赋能AI智能应用开发

华为云开发者联盟

Spring Boot 插件化开发模式,忒香了!

Geek_e3e86e

Java 编程

MyEMS能源管理系统后台配置-计量表管理

开源能源管理系统

开源 能源管理

三角偶极子天线(下)---MIMO系统, 包络相关系数ECC和分集增益DG

思茂信息

cst仿真软件 CST软件 CST Studio Suite

商品中心—库存分桶的一致性改造文档

电子尖叫食人鱼

C# 数据库

1小时搞定跨浏览器测试!零基础玩转Playwright自动化

测试人

软件测试

企业级远程控制方案选型指南:四款主流方案深度对比

科技热闻

在AI时代,挖掘新需求比实现功能更具挑战性

qife122

开发者工具 需求分析

VKProxy新增CORS设置和http响应缓存

八苦-瞿昙

Proxy csharp

CAD一键锁定坐标,图块批量插入快人N步!

在路上

cad 浩辰CAD CAD看图王

MCP客户端与服务端使用教程

测试人

软件测试

Flutter线上代码覆盖率解决方案——FlutterCodeX_开源_君爱_InfoQ精选文章