操作方法:“*敏*感*词*”的性能优化文章来了!
优采云 发布时间: 2022-10-26 07:31操作方法:“*敏*感*词*”的性能优化文章来了!
通过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?
假设我们已经采集到了,如果是手写的话,项目一般是增量的。那么后续的新视图呢?
如您所见,我们面临两个问题:
如何采集项目中xml中使用的Views;
如何保证编写的生成的View代码与项目的正常迭代兼容;
3 确定计划
这里的目标已经确定。
在xml -> View的过程中,去掉反射相关的逻辑
下面说说如何解决我们面临的两个问题:
1、如何采集项目中xml中使用的Views;
采集xml中用到的所有View,有一个简单的思路,我们可以解析项目中所有的layout.xml文件,但是项目中的layout.xml文件有各个模块,一些依赖的aars也需要解压难的。
想一想,在我们生成apk的过程中,资源应该是需要一个merge的,不管是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 />
视图通过 mFactory2、mFactory 和 mPrivateFactory。如果无法完成施工,稍后会反映。
在前两个工厂中,支持包一般用于扩展功能,例如TextView->AppCompatTextView。
我们考虑使用 mPrivateFactory。使用mPrivateFactory的好处是在当前版本中,mPrivateFactory是Activity,所以我们只需要重写Activity的onCreateView即可:
这根本不需要钩子,也不会干扰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目录:
注意里面有一个merge.xml,里面收录了整个项目所有资源的合并内容。
让我们打开它看看:
关注里面带有 type=layout 的相关标签。
<br />
<br />
可以看到收录我们布局文件的道路强度,那么我们只需要解析这个merge.xml,然后在里面找到type=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生成代理类
使用所有使用的视图的名称,我们使用 apt 生成代理类和代理方法。
要使用 apt,我们需要创建 3 个新模块:
ViewOptAnnotation:存储注解;
ViewOptProcessor:放注解处理器相关代码;
ViewOptApi:放相关API。
关于Apt的基础知识我就不说了。这个知识太复杂了。你可以自己检查。后面我会把demo传到github上给大家看。
让我们直接看一下我们的核心处理器类:
<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