技巧:“*敏*感*词*”的性能优化文章来了!

优采云 发布时间: 2022-11-16 12:37

  技巧:“*敏*感*词*”的性能优化文章来了!

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

  后续的换皮和一些黑白的方案都是以此为基础。

  这意味着我们现在可以:

  在运行时,接管某个View的生成,也就是我们可以去掉单个View标签的反射逻辑。

  相似代码:

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

  <br />

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

  先采集,再手写?

  如何采集项目中使用的所有View?

  假设我们采集了,如果是手写,项目一般都是增量的,那新加的View呢?

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

  如何采集项目中xml中使用的View;

  如何保证编写的View生成代码兼容项目的正常迭代;

  3 确定方案

  目标已经在这里确定了。

  在xml-&gt;View的过程中,去掉反射相关的逻辑

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

  1、如何采集项目中xml中使用的View;

  采集所有xml中用到的View,有一个简单的思路,我们可以解析项目中所有的layout.xml文件,但是项目中的layout.xml文件有各个module,有些依赖的aars需要解压太难.

  仔细想想,在我们apk的生成过程中,资源应该是需要合并的,是否是解析某个Task合并后的产物。

  确实,具体的实现会在后面说到。

  我们来看第二个问题:

  2、如何保证编写的View生成代码兼容项目的正常迭代;

  我们已经能够采集所有使用过的视图列表,因此为此:

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

  逻辑规则,简单,编译时生成一个代码类,完成相关的转换代码生成。这里我选择apt。

  有了xml-&gt;View转换逻辑的代码类,只需要在运行时使用LayoutFactory注入即可。

  3.找到安全的注入逻辑

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

  View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,<br />        boolean ignoreThemeAttr) {<br />       // ...<br />    View view;<br />    if (mFactory2 != null) {<br />        view = mFactory2.onCreateView(parent, name, context, attrs);<br />    } else if (mFactory != null) {<br />        view = mFactory.onCreateView(name, context, attrs);<br />    } else {<br />        view = null;<br />    }<br /><br />    if (view == null && mPrivateFactory != null) {<br />        view = mPrivateFactory.onCreateView(parent, name, context, attrs);<br />    }<br /><br />    if (view == null) {<br />        final Object lastContext = mConstructorArgs[0];<br />        mConstructorArgs[0] = context;<br />        try {<br />            if (-1 == name.indexOf('.')) {<br />                view = onCreateView(parent, name, attrs);<br />            } else {<br />                view = createView(name, null, attrs);<br />            }<br />        } finally {<br />            mConstructorArgs[0] = lastContext;<br />        }<br />    }<br /><br />    return view;<br /><br />}<br />

  <br />

  View通过mFactory2、mFactory和mPrivateFactory。建不完,后面等待的就是反思。

  前两个工厂和支持包一般用于扩展功能,比如TextView-&gt;AppCompatTextView。

  我们考虑使用 mPrivateFactory。使用mPrivateFactory的好处是,在当前版本中,mPrivateFactory是Activity,所以我们只需要重写Activity的onCreateView即可:

  这样一来,完全不需要hooks,也不会干扰appcompat相关的生成逻辑,可谓是零风险。

  4 开始实施

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

  我新建了一个工程,在布局文件中写了一些自定义控件,分别叫MyMainView1、MyMainView、MyMainView3、MyMainView4,布局文件就不贴了。

  正如我们之前所说,我们需要在构建apk的过程中找到一个合适的注入点来完成这个任务。

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

  我们把构建过程中的所有任务打印出来,输入命令:

  ./gradlew  app:assembleDebug --console=plain<br />

  <br />

  输出:

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

  <br />

  哪一个最像?一看就有一个Task叫:mergeDebugResources,就是这样。

  对应build目录,还有一个mergeDebugResources目录:

  注意里面有一个merger.xml,里面收录了整个项目所有资源的合并内容。

  我们打开看看:

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

  <br />

  <br />

  可以看到收录我们布局文件的路径,那么我们只需要解析这个merger.xml,然后在里面找到所有type=layout的标签,然后解析出布局文件的实际路径,然后解析出相应的layout xml来获取控件的名称。

  对了,这个任务需要注入到mergeDebugResources中执行。

  如何注入任务?

  很简单:

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

  <br />

  根目录:view_opt.gradle

  我们首先找到mergeDebugResources任务,然后注入一个ResParseTask任务。

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

  <br />

  class ResParseTask extends DefaultTask {<br />    File viewNameListFile<br />    boolean isDebug<br />    HashSet viewSet = new HashSet()<br />    // 自己根据输出几个添加<br />    List ignoreViewNameList = Arrays.asList("include", "fragment", "merge", "view","DateTimeView")<br /><br />    @TaskAction<br />    void doTask() {<br /><br />        File distDir = new File(project.buildDir, "tmp_custom_views")<br />        if (!distDir.exists()) {<br />            distDir.mkdirs()<br />        }<br />        viewNameListFile = new File(distDir, "custom_view_final.txt")<br />        if (viewNameListFile.exists()) {<br />            viewNameListFile.delete()<br />        }<br />        viewNameListFile.createNewFile()<br />        viewSet.clear()<br />        viewSet.addAll(ignoreViewNameList)<br /><br />        try {<br />            File resMergeFile = new File(project.buildDir, "/intermediates/incremental/merge" + (isDebug ? "Debug" : "Release") + "Resources/merger.xml")<br /><br />            println("resMergeFile:${resMergeFile.getAbsolutePath()} === ${resMergeFile.exists()}")<br /><br />            if (!resMergeFile.exists()) {<br />                return<br />            }<br /><br />            XmlSlurper slurper = new XmlSlurper()<br />            GPathResult result = slurper.parse(resMergeFile)<br />            if (result.children() != null) {<br />                result.childNodes().forEachRemaining({ o -><br />                    if (o instanceof Node) {<br />                        parseNode(o)<br />                    }<br />                })<br />            }<br /><br /><br />        } catch (Throwable e) {<br />            e.printStackTrace()<br />        }<br /><br />    }<br /><br />    void parseNode(Node node) {<br />        if (node == null) {<br />            return<br />        }<br />        if (node.name() == "file" && node.attributes.get("type") == "layout") {<br />            String layoutPath = node.attributes.get("path")<br />            try {<br />                XmlSlurper slurper = new XmlSlurper()<br />                GPathResult result = slurper.parse(layoutPath)<br /><br />                String viewName = result.name();<br />                if (viewSet.add(viewName)) {<br />                    viewNameListFile.append("${viewName}\n")<br />                }<br />                if (result.children() != null) {<br />                    result.childNodes().forEachRemaining({ o -><br />                        if (o instanceof Node) {<br />                            parseLayoutNode(o)<br />                        }<br />                    })<br />                }<br />            } catch (Throwable e) {<br />                e.printStackTrace();<br />            }<br /><br />        } else {<br />            node.childNodes().forEachRemaining({ o -><br />                if (o instanceof Node) {<br />                    parseNode(o)<br />                }<br />            })<br />        }<br /><br />    }<br /><br />    void parseLayoutNode(Node node) {<br />        if (node == null) {<br />            return<br />        }<br />        String viewName = node.name()<br />        if (viewSet.add(viewName)) {<br />            viewNameListFile.append("${viewName}\n")<br />        }<br />        if (node.childNodes().size() <br />            if (o instanceof Node) {<br />                parseLayoutNode(o)<br />            }<br />        })<br />    }<br /><br />}<br />

  <br />

  根目录:view_opt.gradle

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

  我们都把代码写在位于项目根目录的view_opt.gradle中,应用在app的build.gradle中:

  <br />

  apply from: rootProject.file('view_opt.gradle')<br />

  <br />

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

  

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

  输出是:

  可以看到是去重View的名字。

  在这里提一下,很多同学看到写gradle脚本都会感到害怕。其实很简单。你可以只写Java。如果不熟悉语法,可以用Java写。没有什么特别的。

  在这一点上,我们有所有使用的视图的名称。

  2.apt生成代理类

  有了所有用到的View的名字,然后我们用apt生成一个代理类和代理方法。

  要使用 apt,我们需要创建 3 个新模块:

  ViewOptAnnotation:存储注解;

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

  ViewOptApi:放相关API。

  关于Apt的基础知识就不说了。这块知识太复杂了。你可以自己查一下。后面我会把demo上传到github,大家自己看看。

  直接看我们的核心Processor类:

<p>@AutoService(Processor.class)<br />public class ViewCreatorProcessor extends AbstractProcessor {<br /><br />    private Messager mMessager;<br /><br /><br />    @Override<br />    public synchronized void init(ProcessingEnvironment processingEnvironment) {<br />        super.init(processingEnvironment);<br />        mMessager = processingEnv.getMessager();<br />    }<br /><br />    @Override<br />    public boolean process(Set

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线