dd-booster源码分析(gradle插件开发、字节码、ASM、Booster)

一、背景

Booster 是一款专门为移动应用设计的易用、轻量级且可扩展的质量优化框架,其目标主要是为了解决随着 APP 复杂度的提升而带来的性能、稳定性、包体积等一系列质量问题。 Booster 提供了性能检测、多线程优化、资源索引内联、资源去冗余、资源压缩、系统 Bug 修复等一系列功能模块,可以使得稳定性能够提升 15% ~ 25%,包体积可以减小 1MB ~ 10MB。

github地址:github.com/didi/booste...

Booster是一款设计方面非常广的优化框架,研究Booster之前需要先了解Gradle插件、字节码、ASM。

二、gradle插件开发

开始之前先查看版本兼容性(重要!重要!重要!)

我使用的gradle-plugin版本是4.0.0;gradle版本是6.1.1

developer.android.google.cn/studio/rele...

研究Booster源码首先需要了解如何开发一个gradle插件,下面是快速上手版本

2.1 打开android studio创建新的android工程

2.2 创建moudle

2.3 配置moudle的build.gradle

php 复制代码
apply plugin: 'maven'
apply plugin: 'org.jetbrains.kotlin.jvm'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation gradleApi()

    implementation 'com.android.tools.build:gradle:4.0.0'

    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}

group='com.tt.plugin'
version='1.0.0'

uploadArchives {
    repositories {
        mavenDeployer {
            group(group)
            version(version)
            //本地的Maven地址设置
            repository(url: uri('../maven_repo'))
        }
    }
}

2.4 实现

在main/resources/META_INF/gradle-plugins文件下创建如下文件

创建插件类

kotlin 复制代码
class DemoPlugin : Plugin<Project> {

    companion object {
        private const val TAG = "kotlin-DemoPlugin: "
    }
    
    override fun apply(project: Project) {
        println(TAG + project.name + ";num = 0")
        printDependencies(project)
    }

    private fun printDependencies(project: Project) {
        val dependExtension = project.extensions.create("printDepend", DenpeExtension::class.java)
        project.afterEvaluate {
            // build.gradle配置执行完成
            if (dependExtension.enable?.get() == true) {
                val configuration = project.configurations.getByName("debugCompileClasspath")
                val allDependencies = configuration.allDependencies
                allDependencies.forEach { denp ->
                    println("${TAG}group = ${denp.group};name = ${denp.name};version = ${denp.version}")
                }
            }
        }
    }

}
kotlin 复制代码
internal interface DenpeExtension {
    val enable: Property<Boolean?>?
}

2.5 发布

在2.3中我们已经配置好发布信息,执行uploadArchives命令即可

执行发布命令后,在项目根目录下出现maven-repo,说明成功了

2.6 使用

在项目的build.gradle中声明仓库地址和依赖

typescript 复制代码
buildscript {
    ext.kotlin_version = '1.6.10'
    repositories {
        google()
        mavenCentral()
        maven {
            url('./maven_repo')
        }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.tt.plugin:plugin:1.0.0'
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

在app的build.gradle使用插件,到这里你的插件就已经运行起来了

bash 复制代码
apply plugin: 'com.tt.plugin'

printDepend {
    enable = true
}

2.7 debug调试

打开Edit Configurations,创建Remote JVM Debug

修改项目的gradle.properties,增加如下配置,如果出现错误,将原来的值注释

ini 复制代码
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
-Dorg.gradle.debug=true

断点调试,在代码处打断点,点击debug甲壳虫就可以了,如下

参考文章

如果对gradle不熟悉可以参考以下文章,再进行demo开发

1、入门gradle

juejin.cn/post/715510...

2、一文搞懂gradle配置

juejin.cn/post/716033...

3、gradle中的DSL、Groovy、Kotlin

juejin.cn/post/716663...

4、gradle的生命周期

juejin.cn/post/717068...

5、gradle常用命令与参数

juejin.cn/post/717149...

6、gradle依赖管理和版本决议

juejin.cn/post/721557...

7、gradle构建核心之Task指南

juejin.cn/post/724820...

8、gradle插件发布指南

juejin.cn/post/728006...

9、gradle plugin插件开发及其断点

juejin.cn/post/694862...

三、字节码

字节码文件本质上就是一张表,如下

字节码主要由以下七部分组成:

  • 魔数与Class文件版本
  • 常量池
  • 访问标志
  • 类索引、父类索引、接口索引
  • 字段表集合
  • 方法表集合
  • 属性表集合

创建Demo.java类,执行javac命令,得到Demo.class字节码文件

typescript 复制代码
public class Demo {
	public static void main(String args[]){
		System.out.println("Hello World.");
  }
}

使用文本编辑器打开Demo.class,字段含义如下(最好手动解析加深印象)

yaml 复制代码
cafe babe 魔数
0000      次版本号
0034      主版本号(jdk1.8)
001d      常量池(16进制1d转换为十进制是29,常量数量是29-1=28)
0a        (1)Methodref_info  
0006      常量池第6个常量所表示class_info信息         java/lang/Object    
000f      常量池第15个常量所表示name_and_type信息      <init>()V
09        (2)Fieldref_info
0010      常量池第16个常量所表示class_info信息        java/lang/System
0011      常量池第17个常量所表示name_and_type信息     out Ljava/io/PrintStream
08        (3)String_info                          
0012      常量池第18个常量索引                       Hello World.
0a        (4)Methodref_info 
0013      常量池第19个常量所表示class_info信息        java/io/PrintStream
0014      常量池第20个常量所表示name_and_type信息     println (Ljava/lang/String;)V
07        (5)Class_info
0015      常量池第21个常量索引                       Demo
07        (6)Class_info
0016      常量池第22个常量索引                       java/lang/Object
01        (7)Utf8_info
0006      字符串长度为6个字节
3c696e69743e    字符串的值                          <init>
01        (8)Utf8_info
0003      字符串长度为3个字节
282956                                            ()V
01        (9)Utf8_info
0004      字符串长度为4个字节
436f6465                                          Code
01        (10)Utf8_info
000f      字符串长度为15个字节
4c696e654e756d6265725461626c65                    LineNumberTable
01        (11)Utf8_info
0004      字符串长度为4个字节
6d61696e                                          main  
01        (12)Utf8_info
0016      字符串长度为22个字节
285b4c6a6176612f6c616e672f537472696e673b2956     ([Ljava/lang/String;)V
01        (13)Utf8_info
000a      字符串长度为10个字节
536f7572636546696c65                             SourceFile
01        (14)Utf8_info
0009      字符串长度为9个字节
44656d6f2e6a617661                               Demo.java
0c        (15)NameAndType_info
0007      常量池第7个常量所表示的信息                <init>
0008      常量池第8个常量所表示的信息                ()V
07        (16)Class_info
0017      常量池第23个常量所表示的信息               java/lang/System
0c        (17)NameAndType_info
0018      常量池第24个常量所表示的信息               out
0019      常量池第25个常量所表示的信息               Ljava/io/PrintStream;
01        (18)Utf8_info
000c      字符串长度为12个字节
48656c6c6f20576f726c642e                         Hello World.
07        (19)Class_info
001a      常量池第26个常量所表示的信息               java/io/PrintStream
0c        (20)NameAndType_info
001b      常量池第27个常量所表示的信息               println
001c      常量池第28个常量所表示的信息               (Ljava/lang/String;)V
01        (21)Utf8_info
0004      字符串长度为4个字节                 
44656d6f                                         Demo
01        (22)Utf8_info 
0010      字符串长度为16个字节
6a6176612f6c616e672f4f626a656374                 java/lang/Object
01        (23)Utf8_info
0010      字符串长度为16个字节
6a6176612f6c616e672f53797374656d                 java/lang/System
01        (24)Utf8_info
0003      字符串长度为3个字节
6f7574                                           out
01        (25)Utf8_info
0015      字符串长度为21个字节
4c6a6176612f696f2f5072696e7453747265616d3b       Ljava/io/PrintStream;
01        (26)Utf8_info
0013      字符串长度为19个字节
6a6176612f696f2f5072696e7453747265616d           java/io/PrintStream
01        (27)Utf8_info
0007      字符串长度为7个字节
7072696e746c6e                                   println
01        (28)Utf8_info
0015      字符串长度为21个字节
284c6a6176612f6c616e672f537472696e673b2956       (Ljava/lang/String;)V

0021      访问标志,0001或0020,public 并且允许使用invokespecial字节码指令的新语义
0005      类索引,常量池第5个常量所表示的信息          Demo
0006      父类索引,常量池第6个常量所表示的信息        java/lang/Object
0000      接口索引,没有实现接口,所以是0,所以没有后面的接口内容
0000      字段表数量,没有声明类级或实例级变量,所以没有后面的字段表
0002      方法表方法数量,每个方法都用method_info表示
0001      方法访问标识,                            ACC_PUBLIC
0007      方法名称索引项,指向常量池第7个常量所表示的信息    <init>
0008      方法描述符索引项,指向常量池第8个常量所表示的信息  ()V
0001      属性计表器
0009      指向常量池第9个常量所表示的信息                  Code
0000001d  属性长度29                                      
0001      max_stack,操作数栈深度最大值
0001      max_locals,局部变量所需的存储空间
00000005  字节码指令长度
2a        对应的指令为aload_0
b7        指令为invokespecial
0001      这是invokespecial的参数
b1        对应的指令为return
0000      异常表数据
0001      属性表长度
000a      指向常量池第10个常量所表示的信息                 LineNumberTable
00000006  属性长度6个字节
0001      line_number_info长度
0000      start_pc 表示的字节码行号为第 0 行
0001      line_number 表示 Java 源码行号为第 1 行
0009      方法访问标识,                                public static void
000b      常量池第11个常量所表示的信息                    main
000c      常量池第12个常量所表示的信息                    ([Ljava/lang/String;)V 
0001      属性表长度
0009      指向常量池第9个常量所表示的信息                  Code
00000025  属性长度为37
0002      max_stack,操作数栈深度最大值2,说明有2个局部变量
0001      max_locals,局部变量所需的存储空间
00000009  字节码指令长度
b2
0002
1203 
b600 
04b1
0000     异常表数据 
0001     属性表长度
000a     常量池第10个常量所表示的信息                      LineNumberTable
0000000a 属性长度10个字节
0002     line_number_info长度
0000     start_pc 表示的字节码行号为第 0 行
0003     line_number 表示 Java 源码行号为第 3 行
0008     start_pc 表示的字节码行号为第 8 行
0004     line_number 表示 Java 源码行号为第 4 行
0001     1个属性
000d     常量池第13个常量所表示的信息                      SourceFile
00000002 长度2个字节
000e     常量池第14个常量所表示的信息                      Demo.java

使用javap -verbose Demo.class命令自动解析字节码,和我们手动解析的进行对比,是一致的

less 复制代码
public class Demo

  minor version: 0

  major version: 52

  flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V

   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;

   #3 = String             #18            // Hello World.

   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V

   #5 = Class              #21            // Demo

   #6 = Class              #22            // java/lang/Object

   #7 = Utf8               <init>

   #8 = Utf8               ()V

   #9 = Utf8               Code

  #10 = Utf8               LineNumberTable

  #11 = Utf8               main

  #12 = Utf8               ([Ljava/lang/String;)V

  #13 = Utf8               SourceFile

  #14 = Utf8               Demo.java

  #15 = NameAndType        #7:#8          // "<init>":()V

  #16 = Class              #23            // java/lang/System

  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;

  #18 = Utf8               Hello World.

  #19 = Class              #26            // java/io/PrintStream

  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V

  #21 = Utf8               Demo

  #22 = Utf8               java/lang/Object

  #23 = Utf8               java/lang/System

  #24 = Utf8               out

  #25 = Utf8               Ljava/io/PrintStream;

  #26 = Utf8               java/io/PrintStream

  #27 = Utf8               println

  #28 = Utf8               (Ljava/lang/String;)V

{

  public Demo();

    descriptor: ()V

    flags: ACC_PUBLIC

    Code:

      stack=1, locals=1, args_size=1

         0: aload_0

         1: invokespecial #1                  // Method java/lang/Object."<init>":()V

         4: return

      LineNumberTable:

        line 1: 0

 
  public static void main(java.lang.String[]);

    descriptor: ([Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

      stack=2, locals=1, args_size=1

         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

         3: ldc           #3                  // String Hello World.

         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

         8: return

      LineNumberTable:

        line 3: 0

        line 4: 8

}

字节码指令官网docs.oracle.com/javase/spec...

加载指令load

load类指令是将局部变量表中指定位置的变量加载到操作数栈中,比如 iload_0 将局部变量表中下标(slot)为 0 的 int 型变量加载到操作数栈上。

存储指令store

store 类指令是将操作数栈栈顶的数据存储到局部变量表中,比如 istore_0 将操作数栈顶的元素存储到局部变量表中下标为 0 的位置,这个位置的元素类型为 int,store 指令和 load 指令用法类似

访问 Filed

getFiled:比如 getfield #2 ,会获取常量池中的 #2 字段压入栈顶,同时将 this 出栈

putFiled:设置字段的值

访问方法

invokestatic:用于调用静态方法

invokespecial:用于调用私有实例方法、构造器方法以及使用super关键字调用父类的实例方法等

invokevirtual:用于调用非私有实例方法

invokeinterface:用于调用接口方法

invokedynamic:用于调用动态方法,比如 lambda

栈帧

JVM 是一个基于栈的虚拟机,每个线程都有一个虚拟机栈用来存储栈帧( stack frame ),栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,栈帧随着方法调用而创建,随着方法结束而销毁。 每个栈帧可以简单的认为由三部分组成:

局部变量表的大小在编译期间就已经确定,对应 Code 属性中的 locals 字段 局部变量区一般用来缓存一些临时数据,比如计算的结果。实际上,JVM 会把局部变量区当成一个 数组,里面会依次缓存 this 指针(非静态方法)、参数、局部变量。

实战分析

arduino 复制代码
public class Demo {
    public static int add() {
    	int a = 1;
    	int b = 2;
    	return a + b;
    }
}

public class Demo
{
  public static int add();

    descriptor: ()I

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

      stack=2, locals=2, args_size=0

         0: iconst_1 // 常量1入栈

         1: istore_0 // 将栈顶元素出栈并且存到局部变量表slot0处

         2: iconst_2 // 常量2入栈

         3: istore_1 // 将栈顶元素出栈并且存到局部变量表slot1处

         4: iload_0  // 加载局部变量表slot0处的元素到栈顶

         5: iload_1  // 加载局部变量表slot1处的元素到栈顶

         6: iadd     // 将操作数栈内的两个元素出栈,相加后将结果入栈

         7: ireturn  // 返回栈顶元素,方法结束

      LineNumberTable:

        line 6: 0

        line 7: 2

        line 8: 4
}
SourceFile: "Demo.java"

参考文章:

字节码文件结构:

www.cnblogs.com/chanshuyi/p...

字节码增强技术探索:

tech.meituan.com/2019/09/05/...

ASM基础知识:

juejin.cn/post/700057...

四、ASM

访问者模式主要用于修改或操作一些数据结构比较稳定的数据,字节码文件的结构是由JVM固定的,所以很适合利用访问者模式对字节码文件进行修改,ASM对字节码操作使用的就是访问者模式,访问者模式代码如下:

typescript 复制代码
interface CarElement {
    void accept(CarElementVisitor visitor);
}

interface CarElementVisitor {
    void visit(Body body);
    void visit(Car car);
    void visit(Engine engine);
    void visit(Wheel wheel);
}

class Wheel implements CarElement {
    private final String name;
    public Wheel(final String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    @Override
    public void accept(CarElementVisitor visitor) {
        visitor.visit(this);
    }
}

class Body implements CarElement {
    @Override
    public void accept(CarElementVisitor visitor) {
        visitor.visit(this);
    }
}

class Engine implements CarElement {
    @Override
    public void accept(CarElementVisitor visitor) {
        visitor.visit(this);
    }
}

class Car implements CarElement {
    private final List<CarElement> elements;
    public Car() {
        elements = new ArrayList<>();
        elements.add(new Wheel("front left"));
        elements.add(new Wheel("front right"));
        elements.add(new Wheel("back left"));
        elements.add(new Wheel("back right"));
        elements.add(new Body());
        elements.add(new Engine());
    }
    @Override
    public void accept(CarElementVisitor visitor) {
        for (CarElement element : elements) {
            element.accept(visitor);
        }
        visitor.visit(this);
    }
}

class CarElementDoVisitor implements CarElementVisitor {
    @Override
    public void visit(Body body) {
        System.out.println("Moving my body");
    }
    @Override
    public void visit(Car car) {
        System.out.println("Starting my car");
    }
    @Override
    public void visit(Wheel wheel) {
        System.out.println("Kicking my " + wheel.getName() + " wheel");
    }
    @Override
    public void visit(Engine engine) {
        System.out.println("Starting my engine");
    }
}

class CarElementPrintVisitor implements CarElementVisitor {
    @Override
    public void visit(Body body) {
        System.out.println("Visiting body");
    }
    @Override
    public void visit(Car car) {
        System.out.println("Visiting car");
    }
    @Override
    public void visit(Engine engine) {
        System.out.println("Visiting engine");
    }
    @Override
    public void visit(Wheel wheel) {
        System.out.println("Visiting " + wheel.getName() + " wheel");
    }
}

public class VisitorDemo {
    public static void main(final String[] args) {
        Car car = new Car();
        car.accept(new CarElementPrintVisitor());
        car.accept(new CarElementDoVisitor());
    }
}

Android打包过程:

Transform官网:tools.android.com/tech-docs/n...

Transform详解: www.jianshu.com/p/37a5e0588...

自定义Transform: juejin.cn/post/715984...

根据官网介绍,Transform API允许第三方的plugin在生成dex文件前操作class文件,一次app打包可能会经历多次Transform,Transform将输入进行处理,然后写到指定的目录作为下一个Transform输入源,如下

自定义Transform

封装一套通用的模版代码

kotlin 复制代码
package com.tt.plugin.base

import com.android.SdkConstants
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.builder.utils.isValidZipEntryName
import com.android.utils.FileUtils
import com.google.common.io.Files
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream

abstract class BaseCustomTransform(private val enableLog: Boolean) : Transform() {

    //线程池,可提升 80% 的执行速度
//    private var waitableExecutor: WaitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()

    /**
     * 此方法提供给上层进行字节码插桩
     */
    abstract fun provideFunction(): ((InputStream, OutputStream) -> Unit)?

    /**
     * 上层可重写该方法进行文件过滤
     */
    open fun classFilter(className: String) = className.endsWith(SdkConstants.DOT_CLASS)


    /**
     * 默认:获取输入的字节码文件
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 默认:检索整个项目的内容
     */
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }


    /**
     * 默认开启增量编译
     */
    override fun isIncremental(): Boolean {
        return true
    }

    /**
     * 对输入的数据做检索操作:
     * 1、处理增量编译
     * 2、处理并发逻辑
     */
    override fun transform(transformInvocation: TransformInvocation) {
        super.transform(transformInvocation)

        log("Transform start...")

        //输入内容
        val inputProvider = transformInvocation.inputs
        //输出内容
        val outputProvider = transformInvocation.outputProvider

        // 1. 子类实现字节码插桩操作
        val function = provideFunction()

        // 2. 不是增量编译,删除所有旧的输出内容
        if (!transformInvocation.isIncremental) {
            outputProvider.deleteAll()
        }

        for (input in inputProvider) {
            // 3. Jar 包处理
            log("Transform jarInputs start.")
            for (jarInput in input.jarInputs) {
                val inputJar = jarInput.file
                val outputJar = outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                if (transformInvocation.isIncremental) {
                    // 3.1. 增量编译中处理 Jar 包逻辑
                    when (jarInput.status ?: Status.NOTCHANGED) {
                        Status.NOTCHANGED -> {
                            // Do nothing.
                        }
                        Status.ADDED, Status.CHANGED -> {
                            // Do transform.
//                            waitableExecutor.execute {
//                                doTransformJar(inputJar, outputJar, function)
//                            }
                            doTransformJar(inputJar, outputJar, function)
                        }
                        Status.REMOVED -> {
                            // Delete.
                            FileUtils.delete(outputJar)
                        }
                    }
                } else {
                    // 3.2 非增量编译中处理 Jar 包逻辑
//                    waitableExecutor.execute {
//                        doTransformJar(inputJar, outputJar, function)
//                    }
                    doTransformJar(inputJar, outputJar, function)
                }
            }
            // 4. 文件夹处理
            log("Transform dirInput start.")
            for (dirInput in input.directoryInputs) {
                val inputDir = dirInput.file
                val outputDir = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
                if (transformInvocation.isIncremental) {
                    // 4.1. 增量编译中处理文件夹逻辑
                    for ((inputFile, status) in dirInput.changedFiles) {
                        val outputFile = concatOutputFilePath(outputDir, inputFile)
                        when (status ?: Status.NOTCHANGED) {
                            Status.NOTCHANGED -> {
                                // Do nothing.
                            }
                            Status.ADDED, Status.CHANGED -> {
                                // Do transform.
//                                waitableExecutor.execute {
//                                    doTransformFile(inputFile, outputFile, function)
//                                }
                                doTransformFile(inputFile, outputFile, function)
                            }
                            Status.REMOVED -> {
                                // Delete
                                FileUtils.delete(outputFile)
                            }
                        }
                    }
                } else {
                    // 4.2. 非增量编译中处理文件夹逻辑
                    // Traversal fileTree (depthFirstPreOrder).
                    for (inputFile in FileUtils.getAllFiles(inputDir)) {
//                        waitableExecutor.execute {
//                            val outputFile = concatOutputFilePath(outputDir, inputFile)
//                            if (classFilter(inputFile.name)) {
//                                doTransformFile(inputFile, outputFile, function)
//                            } else {
//                                // Copy.
//                                Files.createParentDirs(outputFile)
//                                FileUtils.copyFile(inputFile, outputFile)
//                            }
//                        }
                        val outputFile = concatOutputFilePath(outputDir, inputFile)
                        if (classFilter(inputFile.name)) {
                            doTransformFile(inputFile, outputFile, function)
                        } else {
                            // Copy.
                            Files.createParentDirs(outputFile)
                            FileUtils.copyFile(inputFile, outputFile)
                        }
                    }
                }
            }
        }
//        waitableExecutor.waitForTasksWithQuickFail<Any>(true)
        log("Transform end...")
    }

    /**
     * Do transform Jar.
     */
    private fun doTransformJar(inputJar: File, outputJar: File, function: ((InputStream, OutputStream) -> Unit)?) {
        // Create parent directories to hold outputJar file.
        Files.createParentDirs(outputJar)
        // Unzip.
        FileInputStream(inputJar).use { fis ->
            ZipInputStream(fis).use { zis ->
                // Zip.
                FileOutputStream(outputJar).use { fos ->
                    ZipOutputStream(fos).use { zos ->
                        var entry = zis.nextEntry
                        while (entry != null && isValidZipEntryName(entry)) {
                            if (!entry.isDirectory) {
                                zos.putNextEntry(ZipEntry(entry.name))
                                if (classFilter(entry.name)) {
                                    // Apply transform function.
                                    applyFunction(zis, zos, function)
                                } else {
                                    // Copy.
                                    zis.copyTo(zos)
                                }
                            }
                            entry = zis.nextEntry
                        }
                    }
                }
            }
        }
    }

    /**
     * Do transform file.
     */
    private fun doTransformFile(inputFile: File, outputFile: File, function: ((InputStream, OutputStream) -> Unit)?) {
        // Create parent directories to hold outputFile file.
        Files.createParentDirs(outputFile)
        FileInputStream(inputFile).use { fis ->
            FileOutputStream(outputFile).use { fos ->
                // Apply transform function.
                applyFunction(fis, fos, function)
            }
        }
    }

    private fun applyFunction(input: InputStream, output: OutputStream, function: ((InputStream, OutputStream) -> Unit)?) {
        try {
            if (null != function) {
                function.invoke(input, output)
            } else {
                // Copy
                input.copyTo(output)
            }
        } catch (e: UncheckedIOException) {
            throw e.cause!!
        }
    }

    /**
     * 创建输出的文件
     */
    private fun concatOutputFilePath(outputDir: File, inputFile: File) = File(outputDir, inputFile.name)

    /**
     * log 打印
     */
    private fun log(logStr: String) {
        if (enableLog) {
            println("$name - $logStr")
        }
    }
}

通用模版实现类

kotlin 复制代码
package com.tt.plugin

import com.tt.plugin.base.BaseCustomTransform
import java.io.InputStream
import java.io.OutputStream

class DemoTransform(val enableLog: Boolean): BaseCustomTransform(enableLog) {
    /**
     * 此方法提供给上层进行字节码插桩
     */
    override fun provideFunction() = { inputStream: InputStream, outputStream: OutputStream ->
        outputStream.write(inputStream.readBytes())
    }

    /**
     * Returns the unique name of the transform.
     *
     *
     * This is associated with the type of work that the transform does. It does not have to be
     * unique per variant.
     */
    override fun getName(): String {
        return "DemoTransform"
    }

}

注册Transform,写在plugin中

kotlin 复制代码
private fun registerTransform(project: Project) {
    val appExtension = project.extensions.getByType(AppExtension::class.java)
    appExtension.registerTransform(DemoTransform(true))
}

构建app后,app/build/intermediates/transforms文件下出现DemoTransform文件夹,里面有很多jar包

ASM统计方法耗时

kotlin 复制代码
package com.tt.plugin

import com.tt.plugin.asm_visitor.CostTimeClassVisitor
import com.tt.plugin.base.BaseCustomTransform
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.InputStream
import java.io.OutputStream

class DemoTransform(val enableLog: Boolean): BaseCustomTransform(enableLog) {

    companion object {
        const val TAG = "DemoTransform: "
    }

    override fun classFilter(className: String): Boolean {
        return className.endsWith("Activity.class")
                || className.endsWith("Test.class")
    }

    /**
     * 此方法提供给上层进行字节码插桩
     */
    override fun provideFunction() = { inputStream: InputStream, outputStream: OutputStream ->
        // 使用input输入流构建ClassReader
        val classReader = ClassReader(inputStream)
        // 使用ClassReader和flags构建ClassWriter
        val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES)
        // 使用 ClassWriter 构建我们自定义的 ClassVisitor
        val visitor = CostTimeClassVisitor(classWriter)
        // 最后通过 ClassReader 的 accept 将每一条字节码指令传递给 ClassVisitor
        classReader.accept(visitor, ClassReader.EXPAND_FRAMES)
        //将修改后的字节码文件转换成字节数组
        val byteArray = classWriter.toByteArray()
        //最后通过输出流修改文件,这样就实现了字节码的插桩
        outputStream.write(byteArray)
    }

    /**
     * Returns the unique name of the transform.
     * This is associated with the type of work that the transform does. It does not have to be
     * unique per variant.
     */
    override fun getName(): String {
        return "DemoTransform"
    }

}
kotlin 复制代码
package com.tt.plugin.asm_visitor

import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.AdviceAdapter

class CostTimeClassVisitor(val nextVisitor: ClassVisitor) :
    ClassVisitor(Opcodes.ASM6, nextVisitor) {

    private var className: String? = null

    companion object {
        val TAG = "CostTimeClassVisitor: "
    }

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        className = name
        this.cv.visitField(Opcodes.ACC_PRIVATE, "time", "J", null, 0L)
        println("${TAG} visit: version = ${version}; access = $access; name = $name; superName = $superName; signature = $signature")
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val visitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        println("${TAG} visitMethod: access = $access; name = $access; descriptor = $descriptor; signature = $signature")
        return object : AdviceAdapter(Opcodes.ASM6, visitor, access, name, descriptor) {
            var ishook = false
            /**
             * 开始扫描
             */
            override fun visitCode() {
                super.visitCode()
                println("${TAG}name = ${name} visitCode; ishook = $ishook")
            }

            /**
             * 访问注解
             */
            override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
                if ("Lcom/tt/pluginappthird/CostTime;" == descriptor) {
                    ishook = true
                }
                println("${TAG}name = ${name} visitAnnotation: descriptor = $descriptor; ishook = $ishook")
                return super.visitAnnotation(descriptor, visible)
            }

            /**
             * 方法开始
             */
            override fun onMethodEnter() {
                println("${TAG}name = ${name} onMethodEnter; ishook = $ishook")
                if (ishook) {
                    visitVarInsn(ALOAD, 0)
                    visitVarInsn(ALOAD, 0)
                    visitFieldInsn(GETFIELD, className, "time", "J")
                    visitMethodInsn(
                        INVOKESTATIC,
                        "android/os/SystemClock",
                        "uptimeMillis",
                        "()J",
                        false
                    )
                    visitInsn(LSUB)
                    visitFieldInsn(PUTFIELD, className, "time", "J")
                }
            }

            /**
             * 方法结束
             */
            override fun onMethodExit(opcode: Int) {
                println("${TAG}name = ${name} onMethodExit; ishook = $ishook")
                if (ishook) {
                    visitVarInsn(ALOAD, 0)
                    visitVarInsn(ALOAD, 0)
                    visitFieldInsn(GETFIELD, className, "time", "J")
                    visitMethodInsn(
                        INVOKESTATIC,
                        "android/os/SystemClock",
                        "uptimeMillis",
                        "()J",
                        false
                    )
                    visitInsn(LADD)
                    visitMethodInsn(
                        PUTFIELD,
                        className,
                        "time",
                        "J",
                        false
                    )
                    visitLdcInsn("MainActivity")
                    visitLdcInsn("$name: time = ")
                    visitVarInsn(ALOAD, 0)
                    visitFieldInsn(GETFIELD, className, "time", "J")
                    visitMethodInsn(
                        INVOKESTATIC,
                        "java/lang/Long",
                        "valueOf",
                        "(J)Ljava/lang/Long;",
                        false
                    )
                    visitMethodInsn(
                        INVOKESTATIC,
                        "kotlin/jvm/internal/Intrinsics",
                        "stringPlus",
                        "(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;",
                        false
                    )
                    visitMethodInsn(INVOKESTATIC,
## "android/util/Log",
                        "d",
                        "(Ljava/lang/String;Ljava/lang/String;)I",
                        false)
                    visitInsn(POP)
                }
            }

            /**
             * 访问结束
             */
            override fun visitEnd() {
                super.visitEnd()
                println("${TAG}name = ${name} visitEnd; ishook = $ishook")
            }
        }
    }
}

studio安装ASMPlugin插件后,可以将.java文件转换为字节码格式,ASM的代码照着敲就可以了,遇到不清楚的去官网查一下就可以了,如下:

五、Booster

Booster地址:github.com/didi/booste...

从booster-transform-thread分析booster

项目的build.gradlet添加如下代码:

bash 复制代码
buildscript {
    ext.kotlin_version = '1.6.10'
    ext.booster_version = '4.14.2'
    repositories {
        google()
        mavenCentral()
        maven {
            url('./maven_repo')
        }
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.tt.plugin:plugin:1.0.0'
        classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
        classpath "com.didiglobal.booster:booster-transform-thread:$booster_version"
    }
}

app的build.gradle添加

arduino 复制代码
apply plugin: 'com.didiglobal.booster'

测试代码,在下面的代码中打印子线程的名字,我们在创建线程时并没有设置线程名字,运行后看下效果

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    @CostTime
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val t1 = Thread {
            Log.d(TAG, "t1 name = ${Thread.currentThread().name}")
        }
        t1.start()
    }
    

日志输入结果如下,可以看到线程被赋予了名字

ini 复制代码
com.tt.pluginappthird D/MainActivity: t1 name = com.tt.pluginappthird.MainActivity

booster是怎么做的呢?从源码可知插件入口是BoosterPlugin,那我们继续看看BoosterPlugin做了什么

kotlin 复制代码
class BoosterPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // android项目
        project.extensions.findByName("android") ?: throw GradleException("$project is not an Android project")
        // 插件版本小于3.6 添加监听
        if (!GTE_V3_6) {
            project.gradle.addListener(BoosterTransformTaskExecutionListener(project))
        }
        // 加载处理器
        val processors = loadVariantProcessors(project)

        // gradle生命周期初始化、配置、执行,state.executed配置完成(可以拿到依赖哪些三方库)
        if (project.state.executed) {
            project.setup(processors)
        } else {
            project.afterEvaluate {
                project.setup(processors)
            }
        }
        // 注册transform
        project.getAndroid<BaseExtension>().registerTransform(BoosterTransform.newInstance(project))
    }
}
kotlin 复制代码
// 内联函数
@Throws(ServiceConfigurationError::class)
internal fun loadVariantProcessors(project: Project): List<VariantProcessor> {
    // 创建ServiceLoader,传参classloder和project的class文件
    // 加载
    return newServiceLoader<VariantProcessor>(project.buildscript.classLoader, Project::class.java).load(project)
}

internal inline fun <reified T> newServiceLoader(classLoader: ClassLoader, vararg types: Class<*>): ServiceLoader<T> {
    // 返回ServiceLoaderFactory,T是VariantProcessor,*types是Project::class.java
    return ServiceLoaderFactory(classLoader, T::class.java).newServiceLoader(*types)
}

// 内联类
internal class ServiceLoaderFactory<T>(private val classLoader: ClassLoader, private val service: Class<T>) {
    // ServiceLoader的实现类是ServiceLoaderImpl
    fun newServiceLoader(vararg types: Class<*>): ServiceLoader<T> = ServiceLoaderImpl(classLoader, service, *types)
}

由以上代码得知newServiceLoader().load(project)最后调用的是ServiceLoaderImpl.load(),然后看看ServiceLoaderImpl.load()做了什么

kotlin 复制代码
override fun load(vararg args: Any): List<T> {
    // args是传递project
    // 调用了lookup,看下做了什么
    return lookup<T>().map { provider ->
        // provider是加载的类文件
        try {
            try {
                // *types是Project::class.java,T是VariantProcessor
                // 利用反射创建META-INF/services/${VariantProcessor::class.java.name}下的类对象
                provider.getConstructor(*types).newInstance(*args) as T
            } catch (e: NoSuchMethodException) {
                provider.getDeclaredConstructor().newInstance() as T
            }
        } catch (e: Throwable) {
            throw ServiceConfigurationError("Provider $provider not found")
        }
    }
}
kotlin 复制代码
fun <T> lookup(): Set<Class<T>> {
    // name = META-INF/services/${service.name}
    // sevice是传进来的VariantProcessor的class文件
    // 加载META-INF/services/${VariantProcessor::class.java.name}文件
    return classLoader.getResources(name)?.asSequence()?.map(::parse)?.flatten()?.toSet()?.mapNotNull { provider ->
        try {
            // classloader加载文件
            val providerClass = Class.forName(provider, false, classLoader)
            if (!service.isAssignableFrom(providerClass)) {
                throw ServiceConfigurationError("Provider $provider not a subtype")
            }
            providerClass as Class<T>
        } catch (e: Throwable) {
            null
        }
    }?.toSet() ?: emptySet()
}

META-INF/services/${VariantProcessor::class.java.name},这个又是什么?其实这是spi机制,就是将实现类注册到META-INF/services中,然后使用serviceLoader去加载;具体请参考下面的链接 juejin.cn/post/684490...

到这里也知道了loadVariantProcessors(project)其实就是利用@AutoService加载VariantProcessor实现类,booster-transform-thread中的是ThreadVariantProcessor,获取到实现类后调用了setup函数

kotlin 复制代码
private fun Project.setup(processors: List<VariantProcessor>) {
    val android = project.getAndroid<BaseExtension>()
    when (android) {
        is AppExtension -> android.applicationVariants
        is LibraryExtension -> android.libraryVariants
        else -> emptyList<BaseVariant>()
    }.takeIf<Collection<BaseVariant>>(Collection<BaseVariant>::isNotEmpty)?.let { variants ->
        variants.forEach { variant ->
            processors.forEach { processor ->
                // 执行了每个实现类的process函数
                processor.process(variant)
            }
        }
    }
}
kotlin 复制代码
@AutoService(VariantProcessor::class)
class ThreadVariantProcessor : VariantProcessor {
    override fun process(variant: BaseVariant) {
        if (variant !is LibraryVariant && !variant.isDynamicFeature) {
            // 给项目添加依赖:booster-android-instrument-thread
            variant.project.dependencies.add("implementation", "${Build.GROUP}:booster-android-instrument-thread:${Build.VERSION}")
        }
    }

}

booster-android-instrument-thread项目代码如下,主要是和线程相关,到这里我们的demo项目已经引用了booster-android-instrument-thread

接下来看下booster如何给线程添加名字,在BoosterPlugin的apply函数中注册了BoosterTransform

kotlin 复制代码
// 注册transform
project.getAndroid<BaseExtension>().registerTransform(BoosterTransform.newInstance(project))
// newInstance函数,在不同版本创建不同的transform
companion object {
    fun newInstance(project: Project, name: String = "booster"): BoosterTransform {
        // 创建TransformParameter
        val parameter = project.newTransformParameter(name)
        return when {
            GTE_V3_4 -> BoosterTransformV34(parameter)
            else -> BoosterTransform(parameter)
        }
    }
}
// 执行transform函数
final override fun transform(invocation: TransformInvocation) {
    BoosterTransformInvocation(invocation, this).apply {
        if (isIncremental) {
            // 增量编译
            doIncrementalTransform()
        } else {
            // 非增量编译
            outputProvider?.deleteAll()
            doFullTransform()
        }
    }
}

internal fun doIncrementalTransform() = doTransform(this::transformIncrementally)

private fun doTransform(block: (ExecutorService, Set<File>) -> Iterable<Future<*>>) {
    this.outputs.clear()
    this.collectors.clear()

    val executor = Executors.newFixedThreadPool(NCPU)
    // transform执行之前
    this.onPreTransform()

    // Look ahead to determine which input need to be transformed even incremental build
    val outOfDate = this.lookAhead(executor).onEach {
        project.logger.info("✨ ${it.canonicalPath} OUT-OF-DATE ")
    }

    try {
        // 执行transformIncrementally函数
        block(executor, outOfDate).forEach {
            it.get()
        }
    } finally {
        executor.shutdown()
        executor.awaitTermination(1, TimeUnit.HOURS)
    }
    // transform执行之后
    this.onPostTransform()

    if (transform.verifyEnabled) {
        this.doVerify()
    }
}

private fun transformIncrementally(executor: ExecutorService, outOfDate: Set<File>) = this.inputs.map { input ->
    input.jarInputs.filter {
        it.status != NOTCHANGED || outOfDate.contains(it.file)
    }.map { jarInput ->
        // 处理jar包
        executor.submit {
            doIncrementalTransform(jarInput)
        }
    } + input.directoryInputs.filter {
        it.changedFiles.isNotEmpty() || outOfDate.contains(it.file)
    }.map { dirInput ->
        executor.submit {
            // 处理非jar包
            doIncrementalTransform(dirInput, dirInput.file.toURI())
        }
    }
}.flatten()

// 无论是jar包还是非jar处理,最后会执行到这里
private fun ByteArray.transform(): ByteArray {
    // 遍历transformers执行transformer.transform
    return transformers.fold(this) { bytes, transformer ->
        transformer.transform(this@BoosterTransformInvocation, bytes)
    }
}

从上面代码可以分析出transform()最后执行的是transformers: List中的transformer,接下来看看transformers: List是什么

kotlin 复制代码
// 构建TransformParameter
fun Project.newTransformParameter(name: String): TransformParameter {
    return TransformParameter(name, buildscript, plugins, properties, lookupTransformers(buildscript.classLoader))
}

// parameter就是在创建BoosterTransform时传进来的TransformParameter
// parameter.transformers就是lookupTransformers(buildscript.classLoader)的返回值
private val transformers: List<Transformer> = transform.parameter.transformers.map {
    try {
        it.getConstructor(ClassLoader::class.java).newInstance(transform.parameter.buildscript.classLoader)
    } catch (e1: Throwable) {
        try {
            it.getConstructor().newInstance()
        } catch (e2: Throwable) {
            throw e2.apply {
                addSuppressed(e1)
            }
        }
    }
}

// 这套代码和上面的使用Autoservice加载VariantProcessor基本一样,不过在这里加载的是Transformer实现类
@Throws(ServiceConfigurationError::class)
internal fun lookupTransformers(classLoader: ClassLoader): Set<Class<Transformer>> {
    return ServiceLoaderImpl(classLoader, Transformer::class.java, ClassLoader::class.java).lookup()
}

到这里其实已经知道BoosterTransform.transform()利用spi技术执行到Transformer实现类,在 AsmTransformer是其对应的实现类,而AsmTransformer加载的是ClassTransformer

css 复制代码
constructor(classLoader: ClassLoader = Thread.currentThread().contextClassLoader) : this(ServiceLoader.load(ClassTransformer::class.java, classLoader).sortedBy {
    it.javaClass.getAnnotation(Priority::class.java)?.value ?: 0
}, classLoader)

在booster-transform-thread中ClassTransformer实现类是ThreadTransformer,最后会执行 transform()::ThreadTransformer;代码如下

kotlin 复制代码
@AutoService(ClassTransformer::class)
class ThreadTransformer : ClassTransformer {
    // ...省略代码
    // 执行transform
    override fun transform(context: TransformContext, klass: ClassNode): ClassNode {
        // ...省略代码
        klass.methods?.forEach { method ->
            method.instructions?.iterator()?.asIterable()?.forEach {
                when (it.opcode) {
                    Opcodes.INVOKEVIRTUAL -> (it as MethodInsnNode).transformInvokeVirtual(context, klass, method)
                    Opcodes.INVOKESTATIC -> (it as MethodInsnNode).transformInvokeStatic(context, klass, method)
                    Opcodes.INVOKESPECIAL -> (it as MethodInsnNode).transformInvokeSpecial(context, klass, method)
                    // 创建线程,传名字
                    Opcodes.NEW -> (it as TypeInsnNode).transform(context, klass, method)
                    Opcodes.ARETURN -> if (method.desc == "L$THREAD;") {
                        method.instructions.insertBefore(it, LdcInsnNode(makeThreadName(klass.className)))
                        method.instructions.insertBefore(it, MethodInsnNode(Opcodes.INVOKESTATIC, SHADOW_THREAD, "setThreadName", "(Ljava/lang/Thread;Ljava/lang/String;)Ljava/lang/Thread;", false))
                        logger.println(" + $SHADOW_THREAD.makeThreadName(Ljava/lang/String;Ljava/lang/String;): ${klass.name}.${method.name}${method.desc}")
                    }
                }
            }
        }
        return klass
    }

    private fun TypeInsnNode.transform(context: TransformContext, klass: ClassNode, method: MethodNode) {
        when (this.desc) {
            /*-----------*/ HANDLER_THREAD -> this.transformNew(context, klass, method, SHADOW_HANDLER_THREAD)
            // new Thread
            /*-------------------*/ THREAD -> this.transformNew(context, klass, method, SHADOW_THREAD)
            /*-----*/ THREAD_POOL_EXECUTOR -> this.transformNew(context, klass, method, SHADOW_THREAD_POOL_EXECUTOR, true)
            SCHEDULED_THREAD_POOL_EXECUTOR -> this.transformNew(context, klass, method, SHADOW_SCHEDULED_THREAD_POOL_EXECUTOR, true)
            /*--------------------*/ TIMER -> this.transformNew(context, klass, method, SHADOW_TIMER)
        }
    }

    private fun TypeInsnNode.transformNew(@Suppress("UNUSED_PARAMETER") context: TransformContext, klass: ClassNode, method: MethodNode, type: String, optimizable: Boolean = false) {
        this.find {
            (it.opcode == Opcodes.INVOKESPECIAL) &&
                    (it is MethodInsnNode) &&
                    (this.desc == it.owner && "<init>" == it.name)
        }?.isInstanceOf { init: MethodInsnNode ->
            // 替换desc
            // 例如 android/os/HandlerThread => com/didiglobal/booster/instrument/ShadowHandlerThread
            this.desc = type

            // 替换构造函数
            // 例如 android/os/HandlerThread(Ljava/lang/String;) => com/didiglobal/booster/instrument/ShadowHandlerThread(Ljava/lang/String;Ljava/lang/String;)
            val rp = init.desc.lastIndexOf(')')
            init.apply {
                owner = type
                desc = "${desc.substring(0, rp)}Ljava/lang/String;${if (optimizable) "Z" else ""}${desc.substring(rp)}"
            }
            // 使用ASM进行替换
            method.instructions.insertBefore(init, LdcInsnNode(makeThreadName(klass.className)))
            if (optimizable) {
                method.instructions.insertBefore(init, LdcInsnNode(optimizationEnabled))
            }
        } ?: throw TransformException("`invokespecial $desc` not found: ${klass.name}.${method.name}${method.desc}")
    }
}

ThreadTransformer就是利用将demo项目中使用线程的地方,通过asm替换为booster-android-instrument-thread中封装的线程类,并且加上线程的名字及优化等

这边文章涉及到gradle、字节码、ASM、Booster较多内容,技术细节写的比较宽松!!!

相关推荐
一一Null32 分钟前
Token安全存储的几种方式
android·java·安全·android studio
JarvanMo1 小时前
flutter工程化之动态配置
android·flutter·ios
时光少年4 小时前
Android 副屏录制方案
android·前端
时光少年4 小时前
Android 局域网NIO案例实践
android·前端
alexhilton4 小时前
Jetpack Compose的性能优化建议
android·kotlin·android jetpack
流浪汉kylin4 小时前
Android TextView SpannableString 如何插入自定义View
android
火柴就是我6 小时前
git rebase -i,执行 squash 操作 进行提交合并
android
你说你说你来说6 小时前
安卓广播接收器(Broadcast Receiver)的介绍与使用
android·笔记
你说你说你来说6 小时前
安卓Content Provider介绍及使用
android·笔记
RichardLai887 小时前
[Flutter学习之Dart基础] - 类
android·flutter