业余时间作为学习

ButterKnife无反射优化

ButterKnife真是个让人又爱又恨的库,在Android技术混沌初开的年代,JakeWharton大神靠一己之力通过ButterKnife解放了无数人写findViewById的双手,我接触超过90%的Android项目都曾用过他。但随着项目越来越大,这个几乎作为Android开发标配的开源库又成为了略显鸡肋、性能堪忧的绊脚石。比如解析麻烦、管理混乱的R2设计,以及性能和安全设计都拙急的ViewBinder反射机制。

这里简单介绍下ButterKnife的原理,来引述ButterKnife坑爹的反射机制以及安全问题。

Principle

当我们一般在Viewer Class内使用ButterKnife来快速bind view。

public class FooViewer {
    @Bind(R.id.tv_title)
    View mTvTitle;

    public void init() {
        ButterKnife.bind(this, actualAndroidView);
    }
}

编译时,会通过APT生成ViewBinding对象,在这个对象里,你可以看到真正的findViewById方法。

public class FooViewer_ViewBinding implements Unbinder {
    public FooViewer_ViewBinding(final FooViewer target, View source) {
    target.mTvTitle = (TextView) view.findViewById(R.id.tv_title);
  }
}

然后,只需要在调用 ButterKnife.bind(target, actualAndroidView) 时,找到这个FooViewer_ViewBinding便能bind上。
所以,问题就来到了,怎么找到FooViewer_ViewBinding呢?

很简单,ButterKnife就是给你的类名加上"_ViewBinding",然后反射即可。
让我们看看ButterKnife的bind函数是怎么做的,

public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());

// 寻找target对应的ViewBinding class
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    ...
}

而在findBindingConstructorForClass里,你就会发现他的原理有多么简单,且坑!

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    ...

    try {
            // 这里直接用className 加上后缀,就是对应ViewBinding的class了
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } catch (ClassNotFoundException e) {

      // !!! 如果没有找到,就从target的父类继续找 !!!
            bindingCtor = findBindingConstructorForClass(cls.getSuperclass());

    } 

    ...
    return bindingCtor;
}

Problem

现在,你知道问题所在了吧。

  1. 安全性问题

    按照这个如此简陋的名称匹配规则,为了反射到viewer对应的viewBinding,需要keep住viewer的className。我们一般会赋予viewer有意义的名称,如果不混淆,很容易暴露出逻辑意义,让黑产找到突破口。

    笔者当年做过一款金融app,金融类app对安全的要求甚至包括包名需要完全混淆,因此不得不放弃ButterKnife。

    同时过多的keep也会带来包体积增大的问题。

  2. 性能问题

    通过上述源码,可以看到butterknife需要通过反射找到viewBinding,findclass就是一个很慢的过程。同时,当没找到target对应的viewBinding时,会递归的从其父类继续找,这样如果继承层级深了,又会造成多次反射。

    另外,找到viewBinding class后还需要通过反射来为其实例化,这也是个不小的性能开销。

    对于简单的app来说,多一两次反射不能称之为“问题”;而对快手来说,在mvps架构下,一个页面可能有上千个presenter(比如播放页面),然后presenter还有复杂的继承关系,每次打开新页面,极端情况下能带来数千次的反射。

    根据我们19年的统计,ButterKnife带来的ANR达到x%(数据脱敏,反正很高),有些团队去掉presenter里的butterknife后,页面性能提升了一倍,可见问题有多严重。

    null

    (每个Presenter,都因为ButterKnife的冗余反射,带来数百毫秒的性能损失)

那么,有没有办法既能继续使用ButterKnife,又能解决安全和性能问题呢?

Solution

通过对ButterKnife的原理分析,我们知道ButterKnife是为了找到ViewBinding,才带来了有缺陷的反射机制。所以,只要换个方式去找ViewBinding不就完了?

假如我们这样设计:

扩充一个接口,

public interface ViewBindingProvider {
  @Nullable
  Unbinder getBinder(@NonNull Object target, @NonNull View source);
}

让我们的viewer实现它来返回真正的viewBinding,bind的时候,调用这个接口的getBinder方法不就拿到了吗?

public class FooViewer implement ViewBindingProvider {
    @Bind(R.id.tv_title)
    View mTvTitle;

    Unbinder getBinder(@NonNull Object target, @NonNull View source) {
    return new FooViewer_ViewBinding();
  }
}

而刚才的ButterKnife.bind函数也可以换成无反射写法

public static Unbinder bind(@NonNull Object target, @NonNull View source) {

    if (target instanceof ViewBindingProvider) {
      Unbinder unbinder = ((ViewBindingProvider) target).getBinder(target, source);
      if (unbinder != null) {
        return unbinder;
      }
    } else {
      return Unbinder.EMPTY;
    }

}

当然肯定不能要求大家手动为每个viewer实现这个接口,这里我们就要祭出ASM大法了,编译期直接通过字节码技术,添加 ViewBindingProvider 接口,同时实现 getBinder方法。

ASM

借用 Asuka 框架,我可以很简单的实现这个功能。

在Android里首先需要创建一个Transform,在里面对class做ASM操作即可 (以下为伪代码)

class ViewBindingInjectSubTransform(project: Project) : ParallelTransform(project) {

    private val mUseButterKnifeClass = CopyOnWriteArrayList<String>()

        // 这里我们先编译所有class,找到所有后缀为 _ViewBinding 的class,去掉后缀,是不是就是原来的viewer class呢?
    override fun preProcessFile(inputFileEntity: FileEntity, input: InputStream?, status: Status) {
        if (inputFileEntity.name.endsWith("_ViewBinding.class")) {
            val target = inputFileEntity.relativePath.removeSuffix("_ViewBinding.class")
            mUseButterKnifeClass.add(target)
        }
    }

        // 然后我们遍历所有的class,匹配刚才找到的需要使用butterknife的class,使用ASM进行修改
    override fun processFile(inputFileEntity: FileEntity, input: InputStream?, output: OutputStream?, status: Status): Boolean {
        val className = inputFileEntity.name
        if (className.endsWith("class")
                && mUseButterKnifeClass.contains(inputFileEntity.relativePath.removeSuffix(".class"))
        ) {
            val classReader = ClassReader(input.readBytes())
            val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
                        // 添加ButterKnife ViewBindingProvier接口的ClassVisitor
            val classVisitor = ButterKnifeClassVisitor(classWriter)
            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
            val code = classWriter.toByteArray()
            output.write(code)
            return true
        }
        return false
    }
}
class ButterKnifeClassVisitor(classVisitor: ClassWriter): ClassVisitor(Opcodes.ASM5, classVisitor) {

    private var mNeedInsert = true

    override fun visit(
        version: Int,
        access: Int,
        name: String,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
      val provider = "butterknife/ViewBindingProvider"
      // 给原来的class添加一个  butterknife/ViewBindingProvider 接口
            super.visit(version, access, name, signature, superName, interfaces + provider)

            // 获取class的名字 方便后面创建对象  
            mName = name ?: ""
    }

    override fun visitEnd() {
        insertGetBinderMethod(cv as ClassWriter)
    }

    private fun insertGetBinderMethod(classWriter: ClassWriter) {
                // 得到 xxx_ViewBinding 的 class name
        val viewBindingName = mName + "_ViewBinding"
        val params = "(L$mName;Landroid/view/View;)V"

                // 为class创建一个getBinder方法,然后方法里实现返回 xxx_viewBinding 实例
        val mv = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "getBinder",
            "(Ljava/lang/Object;Landroid/view/View;)Lbutterknife/Unbinder;",
            null, null)

        mv.visitCode()
        mv.visitTypeInsn(Opcodes.NEW, viewBindingName)
        mv.visitInsn(Opcodes.DUP)
        mv.visitVarInsn(Opcodes.ALOAD, 1)
        mv.visitTypeInsn(Opcodes.CHECKCAST, mName)
        mv.visitVarInsn(Opcodes.ALOAD, 2)
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, viewBindingName,
            "<init>", params, false)

        mv.visitInsn(Opcodes.ARETURN)
        mv.visitMaxs(4, 3)
        mv.visitEnd()
    }
}

完整的实现详见 https://git.corp.kuaishou.com/android/kwai-butterknife

Result

记得19年底的时候,凯哥跟我说ButterKnife ANR很多,包括X3的沈老爷等一些同学深受其苦,也组织了一波去Butterknife的工作。但是喜欢ButterKnife的同学也很多,同时ButterKnife侵入量巨大, 想要完全去除也不可能。

那时候我正好为编译优化写了个Asuka框架,拿来实验性的优化了一波。后续看到由ButterKnife造成的ANR完全消失了,效果非常明显。不过为了稳定性,没有完全替换。现在一年过去了也没报出什么问题,因此决定在春节以后直接全量上线。

用同样的原理,我们还能解决EventBus等相似的反射问题。所以哪位同学要是有兴趣的话,可以把EventBus也改一波。

至于为什么19年就做了的优化,现在才写文章介绍,当然是为了“招队友!!!”

应用研发-基础架构-效率工具 求志同道合的朋友们加入,跟我们一起服务于快手这大几百号开发老爷。

包括:

  1. 编译优化编译优化编译优化
  2. 新技术拓展和布道
  3. 效率工具设计研发
  4. 插件化技术开疆辟土
  5. 在开发群里当客服,需要一颗坚强的心(认真脸)
原文地址:https://www.cnblogs.com/liunx1109/p/14360452.html