34

“手把手”的性能优化文章来了!

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ%3D%3D&%3Bmid=2650831279&%3Bidx=1&%3Bsn=1ef254253ba43190e7f6dffa31252172
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上周在留言区答应大家的文章,没有拖延症,如期到来。

1

概述

在Android开发过程中,我们基本每天都在写各种各样的xml布局文件,然后app会在运行时,将我们的布局文件转化成View显示在界面上。

这个转化,主要就是解析xml布局文件,然后根据xml的中每个View标签,将:

  1. 标签名-> View的名称

  2. 各种属性 -> AttributeSet对象

然后反射调用View两个参数的构造方法。

这也是为什么,我们在自定义控件的时候,如果需要在xml使用,需要复写其两参的构造函数。

这个设计确实极具扩展性,但是也引入了一定的性能问题。

可以很明显的看到xml文件到View这个过程中,涉及到一些耗时操作:

  1. io 操作,xml解析;

  2. 反射;

尤其是真实项目中,一些页面布局元素非常多,那么整个页面几十个控件可能都需要去反射生成。

所以很多时候,一些核心页面,为了提升构建速度,我们会考虑直接用代码生成,来替代xml写法,这样做带来一个最大的问题就是可维护性急剧下降。

在既想要可维护性又想要运行时效率的情况下,很多开发者想到,xml毕竟是非常有规律的文件,我们可以在编译时解析成View,运行时直接拿到View,就能避免IO操作以及反射操作了。

确实,想法非常完美,github上也有一个由掌阅发布的开源库:

https://github.com/iReaderAndroid/X2C

x2c的想法非常好,基本上彻底解决了我上面提出的两个耗时问题,但是引入了新的问题,就是兼容性和稳定性。

而且x2c生成代码使用了apt,apt一个都是针对本module去做一些事情,涉及到复杂的module间依赖,就会遇到很多问题,x2c在apt这方面应该也做了很多处理,但是这些处理在遇到很多项目在编译期做各种编译优化的时候,就会摩擦出一些火花。

本文也会涉及到apt,因为不涉及资源,也遇到一些问题,下文会说。

当然如果能够引入x2c,并可自维护的情况下,其实是挺好的,我非常支持这个方案,就是有一定风险。

注:本文不讨论到底哪个方案牛逼,博客更多的还是为了学习,重点还是吸收每个方案包含的知识点,扩充自己的可用知识库。

2

退一步

刚才我们说了,完全托管xml->View这一过程具有一定的风险, 那么我们是否可退一步来看这个问题呢?

既然xml文件到View这个过程中,涉及到两个耗时点:

  • io 操作,xml解析;

  • 反射;

xml解析我们不太好干涉,这种看起来风险就高的东西还是交给Google自己吧,而且底层还涉及到有一些xmlblock的缓存逻辑。

那只剩下一个反射操作了,这是个软柿子吗?

我们有办法去除发射逻辑吗?

当然有,大家肯定都再熟悉不过了。

如果关注本号,我们在16年就写过:

探究 LayoutInflater setFactory

通过setFactory,我们不仅能够控制View的生成,甚至可以把一个View变成另一个View,比如文中,我们把TextView变成了Button。

后续换肤、黑白化一些方案都基于此。

也就说我们现在可以:

运行时,接管某个View的生成,即针对单个View标签我们可以去除反射的逻辑了。

类似代码:

if ("LinearLayout".equals(name)){
    View view = new LinearLayout(context, attrs);
    return view;
}

但是, 一般线上的项目都非常大,可能有各种各样的自定义View,类似上面的if else,怎么写呢?

先收集起来,然后手写?

怎么收集项目中用到的所有的View的呢?

假设我们收集到了,手写的话,项目一般都是增量的,后续新增的View怎么办呢?

可以看到我们面临两个问题:

  1. 如何收集项目中在xml中使用到的View;

  2. 如何保证写出的View生成代码,能够兼容项目的正常迭代;

3

确定方案

到这里目标已经确定了。

在 xml -> View的过程中,去除反射相关逻辑

来说说我们面临的两个问题如何解决:

1. 如何收集项目中在xml中使用到的View;

收集所有在xml中用到的View,有个简单的想法,我们可以解析项目中所有的layout.xml文件,不过项目中layout.xml文件每个模块都 有,而且有些依赖的aar,还需要解压太难了。

细想一下,我们apk在生成过程中,资源应该需要merger吧,是不是解析某个Task merger后的产物即可。

确实有,后面详细实施会提到。

下面看第二个问题:

2. 如何保证写出的View生成代码,能够兼容项目的正常迭代;

我们已经能够收集到所使用的,所有的View列表了,那么针对这种:

if ("LinearLayout".equals(name)){
    View view = new LinearLayout(context, attrs);
    return view;
}

有规律又简单的逻辑, 完全可以在编译时生成一个代码类,完成相关转化代码生成,这里选择了apt。

有了xml -> View转化逻辑的代码类,最后只要在运行时,利用LayoutFactory注入即可。

3. 找一个稳妥的注入逻辑

大家都知道我们的View生成相关逻辑在LayoutInflater下面的代码中:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
       // ...
    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }

    if (view == null) {
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('.')) {
                view = onCreateView(parent, name, attrs);
            } else {
                view = createView(name, null, attrs);
            }
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }

    return view;

}

View经过mFactory2,mFactory,mPrivateFactory,如果还不能完成构建,后面等它的就是反射了。

而前两个factory,support包一般扩展功能会用,例如 TextView-> AppCompatTextView

我们考虑利用mPrivateFactory,利用mPrivateFactory的好处就是,在目前的版本中mPrivateFactory就是Activity,所以我们只要复写Activivity的onCreateView即可:

uYz2ia7.jpg!web

这样完全不需要hook,也不干涉appcompat相关生成逻辑,可谓是0风险了。

4

开始实施

1. 获取项目中使用的控件名列表

我新建了一个项目,随便写了一些自定义控件叫MyMainView1,MyMainView,MyMainView3,MyMainView4都在layout文件中声明了,就不贴布局文件了。

之前我们说了,我们要在apk的构建过程中去寻找合适的注入点完成这个事情。

那么apk构建过程中,什么时候会merge资源呢?

我们打印下构建过程中所有的task,输入命令:

./gradlew  app:assembleDebug --console=plain

输出:

>Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:checkDebugManifest UP-TO-DATE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:mainApkListPersistenceDebug UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders UP-TO-DATE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:compileDebugSources UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:transformClassesWithDexBuilderForDebug UP-TO-DATE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:mergeDexDebug UP-TO-DATE
> Task :app:signingConfigWriterDebug UP-TO-DATE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs UP-TO-DATE
> Task :app:stripDebugDebugSymbols UP-TO-DATE
> Task :app:packageDebug UP-TO-DATE
> Task :app:assembleDebug UP-TO-DATE

哪个最像呢?一眼看有个叫:mergeDebugResources的Task,就它了。

与build目录对应,也有个mergeDebugResources的目录:

ArYNj2b.jpg!web

注意里面有个merger.xml,其中就包含了整个项目所有资源合并后的内容。

我们打开看一眼:

重点关注里面的type=layout的相关标签。

FvEFVnq.jpg!web
<file name="activity_main1"
                path="/Users/zhanghongyang/work/TestViewOpt/app/src/main/res/layout/activity_main1.xml"
                qualifiers="" type="layout" />

可以看到包含了我们layout文件的路劲,那么我们只要解析这个merger.xml,然后找到里面所有type=layout的标签,再解析出layout文件的实际路劲,再解析对应的layout xml就能拿到控件名了。

对了,这个任务要注入到mergeDebugResources后面执行。

怎么注入一个任务呢?

非常简单:

project.afterEvaluate {
    def mergeDebugResourcesTask = project.tasks.findByName("mergeDebugResources")
    if (mergeDebugResourcesTask != null) {
        def resParseDebugTask = project.tasks.create("ResParseDebugTask", ResParseTask.class)
        resParseDebugTask.isDebug = true
        mergeDebugResourcesTask.finalizedBy(resParseDebugTask);
    }

}

根目录:view_opt.gradle

我们首先找到mergeDebugResources这个task,再其之后,注入一个ResParseTask的任务。

然后在ResParseTask中完成文件解析:

class ResParseTask extends DefaultTask {
    File viewNameListFile
    boolean isDebug
    HashSet<String> viewSet = new HashSet<>()
    // 自己根据输出几个添加
    List<String> ignoreViewNameList = Arrays.asList("include", "fragment", "merge", "view","DateTimeView")

    @TaskAction
    void doTask() {

        File distDir = new File(project.buildDir, "tmp_custom_views")
        if (!distDir.exists()) {
            distDir.mkdirs()
        }
        viewNameListFile = new File(distDir, "custom_view_final.txt")
        if (viewNameListFile.exists()) {
            viewNameListFile.delete()
        }
        viewNameListFile.createNewFile()
        viewSet.clear()
        viewSet.addAll(ignoreViewNameList)

        try {
            File resMergeFile = new File(project.buildDir, "/intermediates/incremental/merge" + (isDebug ? "Debug" : "Release") + "Resources/merger.xml")

            println("resMergeFile:${resMergeFile.getAbsolutePath()} === ${resMergeFile.exists()}")

            if (!resMergeFile.exists()) {
                return
            }

            XmlSlurper slurper = new XmlSlurper()
            GPathResult result = slurper.parse(resMergeFile)
            if (result.children() != null) {
                result.childNodes().forEachRemaining({ o ->
                    if (o instanceof Node) {
                        parseNode(o)
                    }
                })
            }


        } catch (Throwable e) {
            e.printStackTrace()
        }

    }

    void parseNode(Node node) {
        if (node == null) {
            return
        }
        if (node.name() == "file" && node.attributes.get("type") == "layout") {
            String layoutPath = node.attributes.get("path")
            try {
                XmlSlurper slurper = new XmlSlurper()
                GPathResult result = slurper.parse(layoutPath)

                String viewName = result.name();
                if (viewSet.add(viewName)) {
                    viewNameListFile.append("${viewName}\n")
                }
                if (result.children() != null) {
                    result.childNodes().forEachRemaining({ o ->
                        if (o instanceof Node) {
                            parseLayoutNode(o)
                        }
                    })
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }

        } else {
            node.childNodes().forEachRemaining({ o ->
                if (o instanceof Node) {
                    parseNode(o)
                }
            })
        }

    }

    void parseLayoutNode(Node node) {
        if (node == null) {
            return
        }
        String viewName = node.name()
        if (viewSet.add(viewName)) {
            viewNameListFile.append("${viewName}\n")
        }
        if (node.childNodes().size() <= 0) {
            return
        }
        node.childNodes().forEachRemaining({ o ->
            if (o instanceof Node) {
                parseLayoutNode(o)
            }
        })
    }

}

根目录:view_opt.gradle

代码很简单,主要就是解析merger.xml,找到所有的layout文件,然后解析xml,最后输出到build目录中。

代码我们都写在view_opt.gradle,位于项目的根目录,在app的build.gradle中apply即可:

apply from: rootProject.file('view_opt.gradle')

然后我们再次运行assembleDebug,输出:

JRnuAza.jpg!web

注意,上面我们还有个ignoreViewNameList对象,我们过滤了一些特殊标签,例如:"include", "fragment", "merge", "view",你可以根据输出结果自行添加。

输出结果为:

jeUbeeN.jpg!web

可以看到是去重后的View的名称。

这里提一下,有很多同学看到写gradle脚本就感觉恐惧,其实很简单,你就当写Java就行了,不熟悉的语法就用Java写就好了,没什么特殊的。

到这里我们就有了所有使用到的View的名称。

2. apt 生成代理类

有了所有用到的View的名称,接下来我们利用apt生成一个代理类,以及代理方法。

要用到apt,那么我们需要新建3个模块:

  1. ViewOptAnnotation: 存放注解;

  2. ViewOptProcessor:放注解处理器相关代码;

  3. ViewOptApi:放相关使用API的。

关于Apt的相关基础知识就不提了哈,这块知识太杂了,大家自己查阅下,后面我把demo传到github大家自己看。

我们就直接看我们最核心的Processor类了:

@AutoService(Processor.class)
public class ViewCreatorProcessor extends AbstractProcessor {

    private Messager mMessager;


    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        Set<? extends Element> classElements = roundEnvironment.getElementsAnnotatedWith(ViewOptHost.class);

        for (Element element : classElements) {
            TypeElement classElement = (TypeElement) element;
            ViewCreatorClassGenerator viewCreatorClassGenerator = new ViewCreatorClassGenerator(processingEnv, classElement, mMessager);
            viewCreatorClassGenerator.getJavaClassFile();
            break;
        }
        return true;

    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ViewOptHost.class.getCanonicalName());
        return types;
    }

}

核心方法就是process了,直接交给了ViewCreatorClassGenerator去生成我们的Java类了。

看之前我们思考下我们的逻辑,其实我们这个代理类非常简单,我们只要构建好我们的类名,方法名,方法内部,根据View名称的列表去写swicth就可以了。

看代码吧。

定义类名:

public ViewCreatorClassGenerator(ProcessingEnvironment processingEnv, TypeElement classElement, Messager messager) {
        mProcessingEnv = processingEnv;
        mMessager = messager;
        mTypeElement = classElement;
        PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(classElement);
        String packageName = packageElement.getQualifiedName().toString();
        //classname
        String className = ClassValidator.getClassName(classElement, packageName);

        mPackageName = packageName;
        mClassName = className + "__ViewCreator__Proxy";
    }

我们类名就是使用注解的类名后拼接__ViewCreator__Proxy。

生成类主体结构:

public void getJavaClassFile() {

    Writer writer = null;
    try {
        JavaFileObject jfo = mProcessingEnv.getFiler().createSourceFile(
                mClassName,
                mTypeElement);

        String classPath = jfo.toUri().getPath();

        String buildDirStr = "/app/build/";
        String buildDirFullPath = classPath.substring(0, classPath.indexOf(buildDirStr) + buildDirStr.length());
        File customViewFile = new File(buildDirFullPath + "tmp_custom_views/custom_view_final.txt");

        HashSet<String> customViewClassNameSet = new HashSet<>();
        putClassListData(customViewClassNameSet, customViewFile);

        String generateClassInfoStr = generateClassInfoStr(customViewClassNameSet);

        writer = jfo.openWriter();
        writer.write(generateClassInfoStr);
        writer.flush();

        mMessager.printMessage(Diagnostic.Kind.NOTE, "generate file path : " + classPath);

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (writer != null) {
            try {
                writer.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }
}

这里首先我们读取了,我们刚才生成的tmp_custom_views/custom_view_final.txt,存放到了一个hashSet中。

然后交给了generateClassInfoStr方法:

private String generateClassInfoStr(HashSet<String> customViewClassNameSet) {

    StringBuilder builder = new StringBuilder();
    builder.append("// Generated code. Do not modify!\n");
    builder.append("package ").append(mPackageName).append(";\n\n");
    builder.append("import com.zhy.demo.viewopt.*;\n");
    builder.append("import android.content.Context;\n");
    builder.append("import android.util.AttributeSet;\n");
    builder.append("import android.view.View;\n");


    builder.append('\n');

    builder.append("public class ").append(mClassName).append(" implements " + sProxyInterfaceName);
    builder.append(" {\n");

    generateMethodStr(builder, customViewClassNameSet);
    builder.append('\n');

    builder.append("}\n");
    return builder.toString();

}

可以看到这里其实就是拼接了类的主体结构。

详细的方法生成逻辑:

private void generateMethodStr(StringBuilder builder, HashSet<String> customViewClassNameSet) {

    builder.append("@Override\n ");
    builder.append("public View createView(String name, Context context, AttributeSet attrs ) {\n");


    builder.append("switch(name)");
    builder.append("{\n"); // switch start

    for (String className : customViewClassNameSet) {
        if (className == null || className.trim().length() == 0) {
            continue;
        }
        builder.append("case \"" + className + "\" :\n");
        builder.append("return new " + className + "(context,attrs);\n");
    }

    builder.append("}\n"); //switch end

    builder.append("return null;\n");
    builder.append("  }\n"); // method end

}

一个for循环就搞定了。

我们现在运行下。

会在项目的如下目录生成代理类:

qA7zeym.jpg!web

类内容:

// Generated code. Do not modify!
package com.zhy.demo.viewopt;

import com.zhy.demo.viewopt.*;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

public class ViewOpt__ViewCreator__Proxy implements IViewCreator {
    @Override
    public View createView(String name, Context context, AttributeSet attrs) {
        switch (name) {
            case "androidx.appcompat.widget.FitWindowsLinearLayout":
                return new androidx.appcompat.widget.FitWindowsLinearLayout(context, attrs);
            case "androidx.appcompat.widget.AlertDialogLayout":
                return new androidx.appcompat.widget.AlertDialogLayout(context, attrs);
            case "androidx.core.widget.NestedScrollView":
                return new androidx.core.widget.NestedScrollView(context, attrs);
            case "android.widget.Space":
                return new android.widget.Space(context, attrs);
            case "androidx.appcompat.widget.DialogTitle":
                return new androidx.appcompat.widget.DialogTitle(context, attrs);
            case "androidx.appcompat.widget.ButtonBarLayout":
                return new androidx.appcompat.widget.ButtonBarLayout(context, attrs);
            case "androidx.appcompat.widget.ActionMenuView":
                return new androidx.appcompat.widget.ActionMenuView(context, attrs);
            case "androidx.appcompat.view.menu.ExpandedMenuView":
                return new androidx.appcompat.view.menu.ExpandedMenuView(context, attrs);
            case "Button":
                return new Button(context, attrs);
            case "androidx.appcompat.widget.ActionBarContainer":
                return new androidx.appcompat.widget.ActionBarContainer(context, attrs);
            case "TextView":
                return new TextView(context, attrs);
            case "ImageView":
                return new ImageView(context, attrs);
            case "Space":
                return new Space(context, attrs);
            case "androidx.appcompat.widget.FitWindowsFrameLayout":
                return new androidx.appcompat.widget.FitWindowsFrameLayout(context, attrs);
            case "androidx.appcompat.widget.ContentFrameLayout":
                return new androidx.appcompat.widget.ContentFrameLayout(context, attrs);
            case "CheckedTextView":
                return new CheckedTextView(context, attrs);
            case "DateTimeView":
                return new DateTimeView(context, attrs);
            case "androidx.appcompat.widget.ActionBarOverlayLayout":
                return new androidx.appcompat.widget.ActionBarOverlayLayout(context, attrs);
            case "androidx.appcompat.view.menu.ListMenuItemView":
                return new androidx.appcompat.view.menu.ListMenuItemView(context, attrs);
            case "androidx.appcompat.widget.ViewStubCompat":
                return new androidx.appcompat.widget.ViewStubCompat(context, attrs);
            case "RadioButton":
                return new RadioButton(context, attrs);
            case "com.example.testviewopt.view.MyMainView4":
                return new com.example.testviewopt.view.MyMainView4(context, attrs);
            case "com.example.testviewopt.view.MyMainView3":
                return new com.example.testviewopt.view.MyMainView3(context, attrs);
            case "View":
                return new View(context, attrs);
            case "com.example.testviewopt.view.MyMainView2":
                return new com.example.testviewopt.view.MyMainView2(context, attrs);
            case "androidx.appcompat.widget.ActionBarContextView":
                return new androidx.appcompat.widget.ActionBarContextView(context, attrs);
            case "com.example.testviewopt.view.MyMainView1":
                return new com.example.testviewopt.view.MyMainView1(context, attrs);
            case "ViewStub":
                return new ViewStub(context, attrs);
            case "ScrollView":
                return new ScrollView(context, attrs);
            case "Chronometer":
                return new Chronometer(context, attrs);
            case "androidx.constraintlayout.widget.ConstraintLayout":
                return new androidx.constraintlayout.widget.ConstraintLayout(context, attrs);
            case "CheckBox":
                return new CheckBox(context, attrs);
            case "androidx.appcompat.view.menu.ActionMenuItemView":
                return new androidx.appcompat.view.menu.ActionMenuItemView(context, attrs);
            case "FrameLayout":
                return new FrameLayout(context, attrs);
            case "RelativeLayout":
                return new RelativeLayout(context, attrs);
            case "androidx.appcompat.widget.Toolbar":
                return new androidx.appcompat.widget.Toolbar(context, attrs);
            case "LinearLayout":
                return new LinearLayout(context, attrs);
        }
        return null;
    }

}

看起来很完美...

不过目前是报错状态,报什么错呢?

错误: 找不到符号
return new Button(context,attrs);
           ^
  符号:   类 Button
  位置: 类 ViewOpt__ViewCreator__Proxy

这些系统控件没有导包。

比如Button,应该是:android.widget.Button。

那么我们可以选择

import android.widget.*

不过有个问题,你会发现, android的View并不是都在android.widget下,例如View在android.view下,WebView在android.webkit下面。

所以我们要把这三个包都导入。

这个时候,你会不会有疑问, 系统也只能通过xml拿到TextView,他咋知道是android.widget.LinearLayout还是android.view.LinearLayout?

难不成一个个尝试反射?

是的,你没猜错,LayoutInflater运行时的对象为:PhoneLayoutInflater,你看源码就知道了:

public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };


    public PhoneLayoutInflater(Context context) {
        super(context);
    }

    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }


    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }

    public LayoutInflater cloneInContext(Context newContext) {
        return new PhoneLayoutInflater(this, newContext);
    }
}

循环拼接前缀遍历...

不过怎么没看到android.view.这个前缀,嗯,在super.onCreateView里面:

#LayoutInflater
protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

ok,这个时候,你可能还会遇到一些系统hide View找不到的情况,主要是因为你本地的android.jar里面没有那些hide View对应的class,所以编译不过,这种极少数,你可以选择在刚才过滤的List里面添加一下。

好了,到这里我们的代理类:

ViewOpt__ViewCreator__Proxy

生成了。

3. 编写生成View的代码

@ViewOptHost
public class ViewOpt {

    private static volatile IViewCreator sIViewCreator;

    static {
        try {
            String ifsName = ViewOpt.class.getName();
            String proxyClassName = String.format("%s__ViewCreator__Proxy", ifsName);
            Class proxyClass = Class.forName(proxyClassName);
            Object proxyInstance = proxyClass.newInstance();
            if (proxyInstance instanceof IViewCreator) {
                sIViewCreator = (IViewCreator) proxyInstance;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public static View createView(String name, Context context, AttributeSet attrs) {


        try {
            if (sIViewCreator != null) {
                View view = sIViewCreator.createView(name, context, attrs);
                if (view != null) {
                    Log.d("lmj", name + " 拦截生成");
                }
                return view;
            }
        } catch (Throwable ex) {
            ex.printStackTrace();
        }

        return null;
    }

}

其实就是反射我们刚才的生成的代理类对象,拿到它的实例。

然后强转为IViewCreator对象,这样我们后续直接 sIViewCreator.createView 调用就可以了。

这里大家有没有看到一个知识点:

就是为什么apt生成的代理类,总会让它去继承某个类或者实现某个接口?

这样在后续调用代码的时候就不需要反射了。

有了生成View的逻辑,然后注入到mPrivaryFactory就可以了,其实就是我们的Activity,找到你项目中的BaseActivity:

public class BaseActivity extends AppCompatActivity {

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View view = ViewOpt.createView(name, context, attrs);
        if (view != null) {
            return view;
        }
        return super.onCreateView(parent, name, context, attrs);
    }
}

流程结束。

运行下,可以看下log:

2020-05-31 18:07:26.300 31454-31454/? D/lmj: LinearLayout 拦截生成
2020-05-31 18:07:26.300 31454-31454/? D/lmj: ViewStub 拦截生成
2020-05-31 18:07:26.300 31454-31454/? D/lmj: FrameLayout 拦截生成
2020-05-31 18:07:26.305 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarOverlayLayout 拦截生成
2020-05-31 18:07:26.306 31454-31454/? D/lmj: androidx.appcompat.widget.ContentFrameLayout 拦截生成
2020-05-31 18:07:26.311 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContainer 拦截生成
2020-05-31 18:07:26.318 31454-31454/? D/lmj: androidx.appcompat.widget.Toolbar 拦截生成
2020-05-31 18:07:26.321 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContextView 拦截生成
2020-05-31 18:07:26.347 31454-31454/? D/lmj: androidx.constraintlayout.widget.ConstraintLayout 拦截生成

对应的布局:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

有没有很奇怪...

哪来的LinearLayout,ViewStub这些?

其实就是我们Activity的decorView对应的布局文件里面的。

为啥没有TextView?

因为TextView并support库拦截了,生成了AppcompatTextView,也是new的,早不需要走反射逻辑了。

ok,初步完工。

5

一个潜在的问题

经过gradle,apt,以及对于LayoutInflater流程的了解,我们把相关知识拼接在一起,完成了这次布局优化。

是不是还挺有成就感的。

不过,如果大家有对apt特别熟悉的,应该会发现一个潜在的问题。

什么问题呢?

我们现在新建两个library的module,让:

app implementation lib1
lib1 implementation lib2

在lib2里面写个自定义控件。

我们在lib2中自定义一个控件Lib2View,然后在lib2的layout中引用。

lib2的xml:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Lib2Activity">

    <com.example.lib2.Lib2View
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.lib2.Lib2View>

</androidx.constraintlayout.widget.ConstraintLayout>

然后我们再次执行app:assembleDebug:

你会发现,报错了:

ViewOpt__ViewCreator__Proxy.java:47: 错误: 找不到符号
return new com.example.lib2.Lib2View(context,attrs);
                           ^
  符号:   类 Lib2View
  位置: 程序包 com.example.lib2

错误的原因是,虽然我们收集到了Lib2View,也生成了相关方法,代码我们访问不到这个类。

为什么访问不到呢?

因为我们用了implementation,再看一眼:

app implementation lib1
lib1 implementation lib2

implementation隔离了app对lib2的类引用,虽然打包后大家都能正常访问,但是在编译期间是访问不到的。

这就是我说的apt存在的问题,apt主要是针对单个module的,对于这种多module并不是很合适。

所以,如果用Transfrom来做相关类生成,就不会有类似问题。

但是,博客我都写到这了,你让我换方案?

如果我们改成api,或者之前的compile,都能编译通过。

那么能不能在打包到时候,把implementation动态的换成api呢?

经过一顿对gradle API的摸索,发现是支持的:

project.afterEvaluate {
    android.libraryVariants.all { variant ->
        def variantName = variant.name
        def depSet = new HashSet()
        tasks.all {
            if ("assemble${variantName.capitalize()}".equalsIgnoreCase(it.name)) {
                project.configurations.each { conf ->
                    if (conf.name == "implementation") {
                        conf.dependencies.each { dep ->
                            depSet.add(dep)
                            project.dependencies.add("api", dep)
                        }
                        depSet.each {
                            conf.dependencies.remove(it)
                        }
                    }
                }
            }
        }
    }
}

我们可以将implementation,添加到api中。

将上述脚本apply到你的library的build.gradle中即可。

还留了一些问题

文中我们是以assembleDebug来演示的,那么release打包怎么办呢?

release只需要修改一个地方,就是merger.xml,不在mergeDebugResources下了,而在mergeReleaseResources下面了。

其次因为你的代理类需要反射,注意keep相关类就好了。

这个我就不特别帮大家处理了,如果release你搞不定,我建议你别实施这个方案了,先学习文章中相关知识点吧。

6

Google也在做类似的事情

看来Google也意识到layout构建的耗时了。

zeInUjJ.jpg!web

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/startop/view_compiler/README.md;bpv=0;bpt=0

可以看到Google在做View Compiler的相关事情,不过目前尚未开启,对应到运行时源码中,应该是:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
              + Integer.toHexString(resource) + ")");
    }

    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

可以看到inflate中多了一个tryInflatePrecompiled方法,看起来是可以直接给一个layout id,返回一个构建好的View。

期待后续该方案上线。

7

总结

最近推送了非常多的性能优化的文案,大家也抱怨都不是实战,感觉像理论。

不过很多实践确实需要理论的支撑,以及一些工具使用的辅助,很多时候性能优化,其实就是找问题,解决方案,想有一些通用性的优化方案太少了。

这次花了一点时间分享一个View构建这一块的优化给大家,可以看到就这么点功能,其实我们涉及到了:

  1. gradle 构建相关知识;

  2. apt 相关知识;

  3. LayooutFactory相关知识;

虽然我们遇到了一些挫折,比如文章最后说到的apt生成的类,无法访问非传递依赖模块中的类,不过我们还是解决了。

有时候遇到问题,我就安慰自己:

感觉又能学到一点东西了

没什么一直顺利的事情,正确面对问题,只有不断的遇到问题、解决问题,才能成长。

另外,建议大家日常积累知识点,不要看到文章,发现自己不熟悉就不想看,看到自己其实早就清楚的,看的津津有味...

demo:

https://github.com/hongyangAndroid/ViewOptDemo

推荐阅读

面试官: 说一下你做过哪些性能优化?

Android Studio 4.0 稳定版发布啦!

你可能从来没这样用过okhttp 拦截器

vEVjqa7.jpg!web

扫一扫  关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~

┏(^0^)┛明天见!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK