ByteX-const-inline-plugin源码解析

1,352 阅读7分钟

背景

什么是常量

常量字段是指运行期间不会改变的字段,对于其赋值之后,其值是固定不变的(反射或者直接修改内存除外)。常量可以分为以下两种:

  • 编译期常量:在编译期间就可以确定的值的常量,或者说在字节码里面含有ConstantValues属性的field

  • 比如:public static final String TAG = “MainActivity”;
  • 运行期间常量:需要在运行的时候初始化才能确定值的常量,该常量相对于编译期常量的一个特殊点是存在对应PUTSTATIC指令进行赋值。

  • 比如:public static final String TAG = MainActivity.class.getSimpleName();

常量内联

在App包体积优化方向中,这些常量在包体积大小中也占了不小的比例,虽然他们仅仅是一行简简单的常量代码,比如TAG但是当这些编译期常量在项目中越来越多的时候,那么对这个常量内联优化后的收益比例也是很可观的;

内联前

内联后

内联后常量代码被删除了,且常量引用处变成了常量值

const-inline-plugin

const-inline-plugin就是一种通过ASM操作class文件对编译常量进行内联优化后删除编译常量,然后文件从而达到减少包大小的目的方案

使用收益

在一个普通demo apk的项目收益可以有0.05MB,大约减少了51KB包大小

使用前

使用后

两种常量的区分方法

在说源码之前我们先了解一下在操作ASM里怎么去区分对于编译期常量运行期间常量

常量处理中一般编译期常量是可以内联优化的而运行期间常量则不能修改,这两种常量的区别是在与有没有PUTSTATIC(表示设置一个类的静态变量)指令进行赋值。下面通过一个smail代码来看一下这两种常量的区别

  • 源代码

  • smail代码

可以看到在LoginFragment类里设置了两个变量,TAGcalzzName然后smail代码可以看出在LoginFragment类的初始化clinit方法里对calzzName做了变量赋值的操作(前缀 ssputsget 指令用于静态字段的读写操作。即PUTSTATIC/GETSTATIC指令)

所以我们在进行ASM操作时对静态常量进行查找判断有没有PUTSTATIC指令即可判断出是不是**运行期间常量**

const-inline-plugin源码解析

暂时无法在飞书文档外展示此内容

traverse() -第一次工程遍历

第一次工程遍历主要就是为了找出项目里的编译期常量反射常量

traverse() - ClassVisitor

@Override
public void traverse(@NotNull String relativePath, @NotNull ClassVisitorChain chain) {
    //do not scan R.class
    if (!Utils.isRFile(relativePath)) {
        // 查找常量
        chain.connect(new InlineConstPreviewClassVisitor(context));
    }
    super.traverse(relativePath, chain);
}

traverse()方法里判断如果不是R文件,则进入 class文件分析类InlineConstPreviewClassVisitor,该类主要是来遍历filed判断是不是static & final 如果满足则加入集合;主要方法如下

  • visitAnnotation() - 访问Field的注解
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    // 如果配置了过滤掉被注解注释过的常量,这里会对当前class进行添加集合
    if (mContext.inSkipWithAnnotations(desc)) {
        mContext.addSkipAnnotationClass(mClassName);
    }
    return super.visitAnnotation(desc, visible);
}

如果配置了skipWithAnnotations那么在这个方法是对变量的注解进行判断如果符合则把当前的class添加到skipAnnotationClasses集合里过滤掉

  • visitField() - 访问变量
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    FieldVisitor fieldVisitor = super.visitField(access, name, desc, signature, value);
    if (TypeUtil.isStatic(access) && TypeUtil.isFinal(access)) {
        // 把static final 变量添加到集合里
        mContext.addConstField(mClassName, access, name, desc, signature, value);
        // 判断变量的注解
        return new RuntimeConstFieldScanFieldVisitor(name, desc, fieldVisitor);
    }
    return fieldVisitor;
}

该方法里判断了了是不是static & final 如果满足则加入常量集合里,然后返回RuntimeConstFieldScanFieldVisitor类,该类主要是判断静态变量上有没有注解,同visitAnnotation()方法作用相同

  • visitMethod() - 该方法返回了RuntimeConstFieldScanMethodVisitor类主要作用是查找方法里有没有PUTSTATIC 指令判断是不是运行期常量
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    //We need to find all fields marked as final but still assigned in the method
    return new RuntimeConstFieldScanMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions));
}
class RuntimeConstFieldScanMethodVisitor extends MethodVisitor {
    ...
    /** 域操作指令,用来加载或者存储对象的Field */
    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
        if (opcode == Opcodes.PUTSTATIC) {
            // 从方法里判断查找判断是不是运行期常量,比如clinit方法里
            FieldNode constField = mContext.getConstField(owner, name, desc, false);
            if (constField != null) {
                mContext.addRuntimeConstField(owner, name, desc);
            }
        }
        super.visitFieldInsn(opcode, owner, name, desc);
    }
    ...
}

RuntimeConstFieldScanMethodVisitor.visitFieldInsn()里判断静态变量filed是不是PUTSTATIC,及我们上面提到的判断运行期间常量方法,并把它加入集合中

traverse() - ClassNode

该方法主要是用来反射分析,或许所有反射的调用出,把反射调用存储到集合里,后续判断当前的常量是否被引用到了反射里,如果被反射调用了,则不进行内联。

transform() - 第二次遍历,内联常量

@Override
public boolean transform(@NotNull String relativePath, @NotNull ClassVisitorChain chain) {
    if (!Utils.isRFile(relativePath)) {
        chain.connect(new InlineConstClassVisitor(context));
    }
    return super.transform(relativePath, chain);
}

就是在该方法里对非R文件类进行内联常量,该方法做了两件事

  • 删除可以替换的常量
  • 把使用到常量的地方内联为常量的值

具体代码是在InlineConstClassVisitor完成了,在该类里主要看三个方法

needSkip()是否需要替换

该方法判断了9种情况不需要替换(详情可见注释),然后其他情况都是需要替换内联的

private int needSkip(int access, String className, String fieldName, String desc, Object value) {
    // 非static
    if (!TypeUtil.isStatic(access)) {
        return SKIP_NOT_STATIC;
    }
    // 非final
    if (!TypeUtil.isFinal(access)) {
        return SKIP_NOT_FINAL;
    }
    // 值为空
    if (value == null) {
        return SKIP_VALUE_NULL;
    }
    // 非集合里的常量
    if (mContext.getConstField(className, fieldName, desc, true) == null) {
        return SKIP_NOT_CONST;
    }
    // 在白名单里
    if (mContext.inWhiteList(className, fieldName, desc)) {
        return SKIP_IN_WHITE_LIST;
    }
    // 过滤调运行时的常量/或者开启了过滤注解的开关且有注解的常量
    if (mContext.isRuntimeConstField(className, fieldName, desc)) {
        return SKIP_RUNTIME_CONST;
    }
    // 过滤 -被注解过的常量,包含class
    if (mContext.inSkipAnnotationClass(className)) {
        return SKIP_TYPE_IN_ANNOTATION_CLASS;
    }
    // 使用插件内置的反射检查过滤掉可能的反射常量,过滤反射常量
    if (mContext.extension.isAutoFilterReflectionField() && mContext.isReflectField(access, className, fieldName)) {
        return SKIP_TYPE_AUTO_FILTER_REFLECTION;
    }
    // 使用插件内置字符串匹配可能反射常量,
    if (mContext.extension.isSupposesReflectionWithString() && mContext.inStringPool(fieldName)) {
        return SKIP_TYPE_IN_STRING_POOL;
    }
    return NO_SKIP;
}

visitField() - 访问变量

@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    int skip = needSkip(access, mClassName, name, desc, value);
    if (skip == NO_SKIP) {
        mContext.getLogger().i("delete const field", String.format("delete %s static final %s %s = %s", mClassName, desc, name, value));
        return null;
    } else if (skip == SKIP_TYPE_AUTO_FILTER_REFLECTION) {
        mContext.getLogger().i("skip reflect field(reflection)", String.format("skip delete %s static final %s %s = %s", mClassName, desc, name, value));
    } else if (skip == SKIP_TYPE_IN_STRING_POOL) {
        mContext.getLogger().i("skip reflect field(String)", String.format("skip delete %s static final %s %s = %s", mClassName, desc, name, value));
    }
    return super.visitField(access, name, desc, signature, value);
}

该方法通过调用needSkip()方法来判断当前变量是否需要删除,只有返回了NO_SKIP才删除,其他都是不变

visitMethod() - 访问方法

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    return new InlineConstMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions), name);
}

InlineConstMethodVisitor.visitFieldInsn()的方法里类主要做了两件事就是内联替换使用到的常量,把它替换为常量的值,并检查**运行期常量**是否被替换

  • 通过判断opcode是不是Opcodes.GETSTATIC(即是不是调用静态变量)然后进一步调用needSkip()判断是不是需要内联替换为常量的值
  • 二次检查opcode是不是Opcodes.PUTSTATIC然后进一步调用needSkip()判断是不是需要内联替换为常量的值,如果是则抛出异常,因为一般情况下PUTSTATIC的指令即运行期常量不能被替换
private class InlineConstMethodVisitor extends MethodVisitor {
    ....

    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
        if (opcode == Opcodes.GETSTATIC) {
            FieldNode constField = mContext.getConstField(owner, name, desc, true);
            if (constField != null && NO_SKIP == needSkip(constField.access, owner, name, desc, constField.value)) {
                //inline const
                mContext.getLogger().i("inline const field", String.format("change instruction in method %s.%s: GETSTATIC %s.%s to LDC %s", Utils.replaceSlash2Dot(mClassName), methodName, Utils.replaceSlash2Dot(owner), name, constField.value));
                // 替换为常量的值
                super.visitLdcInsn(constField.value);
                return;
            }
        } else if (opcode == Opcodes.PUTSTATIC) {
            //check again
            FieldNode constField = mContext.getConstField(owner, name, desc, true);
            if (constField != null && NO_SKIP == needSkip(constField.access, owner, name, desc, constField.value)) {
                throw new ConstInlineException("unexcepted situation:" + owner + "." + name + "=" + constField.value + ":" + mContext.isRuntimeConstField(owner, name, desc));
            }
        }
        super.visitFieldInsn(opcode, owner, name, desc);
    }
}

到此常量内联就结束了,虽然这个plugin仅仅是对常量内联,但是当项目庞大时包体积优化收益还是会很明显的

参考链接