技巧:“*敏*感*词*”的性能优化文章来了!
优采云 发布时间: 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->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->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->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