• 业余时间作为学习


    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. 在开发群里当客服,需要一颗坚强的心(认真脸)
  • 相关阅读:
    用spring boot 来创建第一个application
    Entily实体类
    ORM
    lambda expression
    Domain logic approochs
    mysql的数据类型(Data type)
    Backup &recovery备份和还原
    spring AOP Capability and goals
    CDI Features
    Tomcat的配置与安装
  • 原文地址:https://www.cnblogs.com/liunx1109/p/14360452.html
Copyright © 2020-2023  润新知