技巧:“*敏*感*詞*”的性能優(yōu)化文章來(lái)了!
優(yōu)采云 發(fā)布時(shí)間: 2022-11-16 12:37技巧:“*敏*感*詞*”的性能優(yōu)化文章來(lái)了!
通過(guò)setFactory,我們不僅可以控制View的生成,甚至可以將一個(gè)View變成另一個(gè)View。比如文章中,我們把TextView變成了Button。
后續的換皮和一些黑白的方案都是以此為基礎。
這意味著(zhù)我們現在可以:
在運行時(shí),接管某個(gè)View的生成,也就是我們可以去掉單個(gè)View標簽的反射邏輯。
相似代碼:
if?("LinearLayout".equals(name)){<br />????View?view?=?new?LinearLayout(context,?attrs);<br />????return?view;<br />}
<br />
但是一般網(wǎng)上的項目都很大,可能會(huì )有各種自定義的View,類(lèi)似上面的if else,怎么寫(xiě)呢?
先采集,再手寫(xiě)?
如何采集項目中使用的所有View?
假設我們采集了,如果是手寫(xiě),項目一般都是增量的,那新加的View呢?
我們可以看到我們面臨兩個(gè)問(wèn)題:
如何采集項目中xml中使用的View;
如何保證編寫(xiě)的View生成代碼兼容項目的正常迭代;
3 確定方案
目標已經(jīng)在這里確定了。
在xml->View的過(guò)程中,去掉反射相關(guān)的邏輯
下面說(shuō)一下如何解決我們面臨的兩個(gè)問(wèn)題:
1、如何采集項目中xml中使用的View;
采集所有xml中用到的View,有一個(gè)簡(jiǎn)單的思路,我們可以解析項目中所有的layout.xml文件,但是項目中的layout.xml文件有各個(gè)module,有些依賴(lài)的aars需要解壓太難.
仔細想想,在我們apk的生成過(guò)程中,資源應該是需要合并的,是否是解析某個(gè)Task合并后的產(chǎn)物。
確實(shí),具體的實(shí)現會(huì )在后面說(shuō)到。
我們來(lái)看第二個(gè)問(wèn)題:
2、如何保證編寫(xiě)的View生成代碼兼容項目的正常迭代;
我們已經(jīng)能夠采集所有使用過(guò)的視圖列表,因此為此:
if?("LinearLayout".equals(name)){<br />????View?view?=?new?LinearLayout(context,?attrs);<br />????return?view;<br />}<br />
邏輯規則,簡(jiǎn)單,編譯時(shí)生成一個(gè)代碼類(lèi),完成相關(guān)的轉換代碼生成。這里我選擇apt。
有了xml->View轉換邏輯的代碼類(lèi),只需要在運行時(shí)使用LayoutFactory注入即可。
3.找到安全的注入邏輯
大家都知道我們View生成相關(guān)的邏輯在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通過(guò)mFactory2、mFactory和mPrivateFactory。建不完,后面等待的就是反思。
前兩個(gè)工廠(chǎng)和支持包一般用于擴展功能,比如TextView->AppCompatTextView。
我們考慮使用 mPrivateFactory。使用mPrivateFactory的好處是,在當前版本中,mPrivateFactory是Activity,所以我們只需要重寫(xiě)Activity的onCreateView即可:
這樣一來(lái),完全不需要hooks,也不會(huì )干擾appcompat相關(guān)的生成邏輯,可謂是零風(fēng)險。
4 開(kāi)始實(shí)施
1.獲取項目中使用的控件名稱(chēng)列表
我新建了一個(gè)工程,在布局文件中寫(xiě)了一些自定義控件,分別叫MyMainView1、MyMainView、MyMainView3、MyMainView4,布局文件就不貼了。
正如我們之前所說(shuō),我們需要在構建apk的過(guò)程中找到一個(gè)合適的注入點(diǎn)來(lái)完成這個(gè)任務(wù)。
那么在apk構建過(guò)程中,什么時(shí)候會(huì )合并資源呢?
我們把構建過(guò)程中的所有任務(wù)打印出來(lái),輸入命令:
./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 />
哪一個(gè)最像?一看就有一個(gè)Task叫:mergeDebugResources,就是這樣。
對應build目錄,還有一個(gè)mergeDebugResources目錄:
注意里面有一個(gè)merger.xml,里面收錄了整個(gè)項目所有資源的合并內容。
我們打開(kāi)看看:
關(guān)注里面type=layout的相關(guān)標簽。
<br />
<br />
可以看到收錄我們布局文件的路徑,那么我們只需要解析這個(gè)merger.xml,然后在里面找到所有type=layout的標簽,然后解析出布局文件的實(shí)際路徑,然后解析出相應的layout xml來(lái)獲取控件的名稱(chēng)。
對了,這個(gè)任務(wù)需要注入到mergeDebugResources中執行。
如何注入任務(wù)?
很簡(jiǎn)單:
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任務(wù),然后注入一個(gè)ResParseTask任務(wù)。
然后在ResParseTask中完成文件解析:
<br />
class?ResParseTask?extends?DefaultTask?{<br />????File?viewNameListFile<br />????boolean?isDebug<br />????HashSet?viewSet?=?new?HashSet()<br />????//?自己根據輸出幾個(gè)添加<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
代碼很簡(jiǎn)單,主要就是解析merge.xml,找到所有的布局文件,然后解析xml,最后輸出到build目錄。
我們都把代碼寫(xiě)在位于項目根目錄的view_opt.gradle中,應用在app的build.gradle中:
<br />
apply?from:?rootProject.file('view_opt.gradle')<br />
<br />
然后我們再次運行assembleDebug,輸出:
注意上面我們還有一個(gè)ignoreViewNameList對象,我們過(guò)濾了一些特殊的標簽,比如:“include”、“fragment”、“merge”、“view”,你可以根據輸出結果自行添加。
輸出是:
可以看到是去重View的名字。
在這里提一下,很多同學(xué)看到寫(xiě)gradle腳本都會(huì )感到害怕。其實(shí)很簡(jiǎn)單。你可以只寫(xiě)Java。如果不熟悉語(yǔ)法,可以用Java寫(xiě)。沒(méi)有什么特別的。
在這一點(diǎn)上,我們有所有使用的視圖的名稱(chēng)。
2.apt生成代理類(lèi)
有了所有用到的View的名字,然后我們用apt生成一個(gè)代理類(lèi)和代理方法。
要使用 apt,我們需要創(chuàng )建 3 個(gè)新模塊:
ViewOptAnnotation:存儲注解;
ViewOptProcessor:放注解處理器相關(guān)代碼;
ViewOptApi:放相關(guān)API。
關(guān)于A(yíng)pt的基礎知識就不說(shuō)了。這塊知識太復雜了。你可以自己查一下。后面我會(huì )把demo上傳到github,大家自己看看。
直接看我們的核心Processor類(lèi):
<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