鸿洋大神新作:“手把手”的性能优化文章来了(2)

================================================================

在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,并可自维护的情况下,其实是挺好的,我非常支持这个方案,就是有一定风险。

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

退一步

=================================================================

刚才我们说了,完全托管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生成代码,能够兼容项目的正常迭代;

确定方案

==================================================================

到这里目标已经确定了。

在 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即可:

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

对标阿里P7的性能优化学习之路

=============================================================================

给大家免费分享的资料包括Android进阶知识体系+Android高级工程师学习手册+进阶Android阿里P7学习视频+各个大厂Android高频面试题

  • Android进阶知识体系(性能优化模块)

  • Android高级工程师学习手册(成体系)

  • 进阶Android阿里P7学习视频

评论区评论 领取资料 或者 点击 了解更多 即可领取上面所有Android高级工程师学习资料!

其他需要,也可以在我的 Github 上查看

上面所有学习内容,我愿意全部免费分享给大家。希望大家能多多支持。

开始实施

==================================================================

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的目录:

image

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

我们打开看一眼:

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

<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 viewSet = new HashSet<>()

// 自己根据输出几个添加

List 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,输出:

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

输出结果为:

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

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

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

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 getSupportedAnnotationTypes() {

Set 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 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 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 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循环就搞定了。

我们现在运行下。

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

类内容:

// 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);

推荐学习资料

  • Android进阶学习全套手册

  • Android对标阿里P7学习视频

  • BAT TMD大厂Android高频面试题


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
eturn null;\n");

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

}

一个for循环就搞定了。

我们现在运行下。

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

类内容:

// 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);

推荐学习资料

  • Android进阶学习全套手册

    [外链图片转存中…(img-7ht1NyBY-1715434694780)]

  • Android对标阿里P7学习视频

    [外链图片转存中…(img-QhpboOEW-1715434694780)]

  • BAT TMD大厂Android高频面试题

[外链图片转存中…(img-Jw5Ctuq7-1715434694781)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值