本文以授權個人公衆號「鴻洋」原創首發。
1.概述
在Android開發過程中,我們基本每天都在寫各種各樣的xml佈局文件,然後app會在運行時,將我們的佈局文件轉化成View顯示在界面上。
這個轉化,主要就是解析xml佈局文件,然後根據xml的中每個View標籤,將:
- 標籤名-> View的名稱
- 各種屬性 -> AttributeSet對象
然後反射調用View兩個參數的構造方法。
這也是爲什麼,我們在自定義控件的時候,如果需要在xml使用,需要複寫其兩參的構造函數。
這個設計確實極具擴展性,但是也引入了一定的性能問題。
可以很明顯的看到xml文件到View這個過程中,涉及到一些耗時操作:
- io 操作,xml解析;
- 反射;
尤其是真實項目中,一些頁面佈局元素非常多,那麼整個頁面幾十個控件可能都需要去反射生成。
所以很多時候,一些核心頁面,爲了提升構建速度,我們會考慮直接用代碼生成,來替代xml寫法,這樣做帶來一個最大的問題就是可維護性急劇下降。
在既想要可維護性又想要運行時效率的情況下,很多開發者想到,xml畢竟是非常有規律的文件,我們可以在編譯時解析成View,運行時直接拿到View,就能避免IO操作以及反射操作了。
確實,想法非常完美,github上也有一個由掌閱發佈的開源庫:
https://github.com/iReaderAndroid/X2C
x2c的想法非常好,基本上徹底解決了我上面提出的兩個耗時問題,但是引入了新的問題,就是兼容性和穩定性。
而且x2c生成代碼使用了apt,apt一個都是針對本module去做一些事情,涉及到複雜的module間依賴,就會遇到很多問題,x2c在apt這方面應該也做了很多處理,但是這些處理在遇到很多項目在編譯期做各種編譯優化的時候,就會摩擦出一些火花。
本文也會涉及到apt,因爲不涉及資源,也遇到一些問題,下文會說。
當然如果能夠引入x2c,並可自維護的情況下,其實是挺好的,我非常支持這個方案,就是有一定風險。
注:本文不討論到底哪個方案牛逼,博客更多的還是爲了學習,重點還是吸收每個方案包含的知識點,擴充自己的可用知識庫。
2.退一步
剛纔我們說了,完全託管xml->View這一過程具有一定的風險,那麼我們是否可退一步來看這個問題呢?
既然xml文件到View這個過程中,涉及到兩個耗時點:
- io 操作,xml解析;
- 反射;
xml解析我們不太好乾涉,這種看起來風險就高的東西還是交給Google自己吧,而且底層還涉及到有一些xmlblock的緩存邏輯。
那隻剩下一個反射操作了,這是個軟柿子嗎?
我們有辦法去除發射邏輯嗎?
當然有,大家肯定都再熟悉不過了。
如果關注本號,我們在16年就寫過:
通過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怎麼辦呢?
可以看到我們面臨兩個問題:
- 如何收集項目中在xml中使用到的View;
- 如何保證寫出的View生成代碼,能夠兼容項目的正常迭代;
3. 確定方案
到這裏目標已經確定了。
在 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風險了。
4. 開始實施
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的目錄:
注意裏面有個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<String> viewSet = new HashSet<>()
// 自己根據輸出幾個添加
List<String> 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的名稱。
2. apt 生成代理類
有了所有用到的View的名稱,接下來我們利用apt生成一個代理類,以及代理方法。
要用到apt,那麼我們需要新建3個模塊:
- ViewOptAnnotation: 存放註解;
- ViewOptProcessor:放註解處理器相關代碼;
- 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<String> getSupportedAnnotationTypes() {
Set<String> 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<String> 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<String> 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<String> 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);
case "androidx.core.widget.NestedScrollView":
return new androidx.core.widget.NestedScrollView(context, attrs);
case "android.widget.Space":
return new android.widget.Space(context, attrs);
case "androidx.appcompat.widget.DialogTitle":
return new androidx.appcompat.widget.DialogTitle(context, attrs);
case "androidx.appcompat.widget.ButtonBarLayout":
return new androidx.appcompat.widget.ButtonBarLayout(context, attrs);
case "androidx.appcompat.widget.ActionMenuView":
return new androidx.appcompat.widget.ActionMenuView(context, attrs);
case "androidx.appcompat.view.menu.ExpandedMenuView":
return new androidx.appcompat.view.menu.ExpandedMenuView(context, attrs);
case "Button":
return new Button(context, attrs);
case "androidx.appcompat.widget.ActionBarContainer":
return new androidx.appcompat.widget.ActionBarContainer(context, attrs);
case "TextView":
return new TextView(context, attrs);
case "ImageView":
return new ImageView(context, attrs);
case "Space":
return new Space(context, attrs);
case "androidx.appcompat.widget.FitWindowsFrameLayout":
return new androidx.appcompat.widget.FitWindowsFrameLayout(context, attrs);
case "androidx.appcompat.widget.ContentFrameLayout":
return new androidx.appcompat.widget.ContentFrameLayout(context, attrs);
case "CheckedTextView":
return new CheckedTextView(context, attrs);
case "DateTimeView":
return new DateTimeView(context, attrs);
case "androidx.appcompat.widget.ActionBarOverlayLayout":
return new androidx.appcompat.widget.ActionBarOverlayLayout(context, attrs);
case "androidx.appcompat.view.menu.ListMenuItemView":
return new androidx.appcompat.view.menu.ListMenuItemView(context, attrs);
case "androidx.appcompat.widget.ViewStubCompat":
return new androidx.appcompat.widget.ViewStubCompat(context, attrs);
case "RadioButton":
return new RadioButton(context, attrs);
case "com.example.testviewopt.view.MyMainView4":
return new com.example.testviewopt.view.MyMainView4(context, attrs);
case "com.example.testviewopt.view.MyMainView3":
return new com.example.testviewopt.view.MyMainView3(context, attrs);
case "View":
return new View(context, attrs);
case "com.example.testviewopt.view.MyMainView2":
return new com.example.testviewopt.view.MyMainView2(context, attrs);
case "androidx.appcompat.widget.ActionBarContextView":
return new androidx.appcompat.widget.ActionBarContextView(context, attrs);
case "com.example.testviewopt.view.MyMainView1":
return new com.example.testviewopt.view.MyMainView1(context, attrs);
case "ViewStub":
return new ViewStub(context, attrs);
case "ScrollView":
return new ScrollView(context, attrs);
case "Chronometer":
return new Chronometer(context, attrs);
case "androidx.constraintlayout.widget.ConstraintLayout":
return new androidx.constraintlayout.widget.ConstraintLayout(context, attrs);
case "CheckBox":
return new CheckBox(context, attrs);
case "androidx.appcompat.view.menu.ActionMenuItemView":
return new androidx.appcompat.view.menu.ActionMenuItemView(context, attrs);
case "FrameLayout":
return new FrameLayout(context, attrs);
case "RelativeLayout":
return new RelativeLayout(context, attrs);
case "androidx.appcompat.widget.Toolbar":
return new androidx.appcompat.widget.Toolbar(context, attrs);
case "LinearLayout":
return new LinearLayout(context, attrs);
}
return null;
}
}
看起來很完美…
不過目前是報錯狀態,報什麼錯呢?
錯誤: 找不到符號
return new Button(context,attrs);
^
符號: 類 Button
位置: 類 ViewOpt__ViewCreator__Proxy
我們注意到這些系統控件沒有導包。
比如Button,應該是:android.widget.Button。
那麼我們可以選擇
import android.widget.*
不過有個問題,你會發現,android的View並不是都在android.widget下,例如View在android.view下,WebView在android.webkit下面。
所以我們要把這三個包都導入。
這個時候,你會不會有疑問,系統也只能通過xml拿到TextView,他咋知道是android.widget.LinearLayout還是android.view.LinearLayout?
難不成一個個嘗試反射?
是的,你沒猜錯,LayoutInflater運行時的對象爲:PhoneLayoutInflater,你看源碼就知道了:
public class PhoneLayoutInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
public PhoneLayoutInflater(Context context) {
super(context);
}
protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
public LayoutInflater cloneInContext(Context newContext) {
return new PhoneLayoutInflater(this, newContext);
}
}
循環拼接前綴遍歷…
不過怎麼沒看到android.view.這個前綴,嗯,在super.onCreateView裏面:
#LayoutInflater
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
ok,這個時候,你可能還會遇到一些系統hide View找不到的情況,主要是因爲你本地的android.jar裏面沒有那些hide View對應的class,所以編譯不過,這種極少數,你可以選擇在剛纔過濾的List裏面添加一下。
好了,到這裏我們的代理類:
ViewOpt__ViewCreator__Proxy
生成了。
3. 編寫生成View的代碼
@ViewOptHost
public class ViewOpt {
private static volatile IViewCreator sIViewCreator;
static {
try {
String ifsName = ViewOpt.class.getName();
String proxyClassName = String.format("%s__ViewCreator__Proxy", ifsName);
Class proxyClass = Class.forName(proxyClassName);
Object proxyInstance = proxyClass.newInstance();
if (proxyInstance instanceof IViewCreator) {
sIViewCreator = (IViewCreator) proxyInstance;
}
} catch (Throwable e) {
e.printStackTrace();
}
}
public static View createView(String name, Context context, AttributeSet attrs) {
try {
if (sIViewCreator != null) {
View view = sIViewCreator.createView(name, context, attrs);
if (view != null) {
Log.d("lmj", name + " 攔截生成");
}
return view;
}
} catch (Throwable ex) {
ex.printStackTrace();
}
return null;
}
}
其實就是反射我們剛纔的生成的代理類對象,拿到它的實例。
然後強轉爲IViewCreator對象,這樣我們後續直接 sIViewCreator.createView 調用就可以了。
這裏大家有沒有看到一個知識點:
就是爲什麼apt生成的代理類,總會讓它去繼承某個類或者實現每個接口?
這樣在後續調用代碼的時候就不需要反射了。
有了生成View的邏輯,然後注入到mPrivaryFactory就可以了,其實就是我們的Activity,找到你項目中的BaseActivity:
public class BaseActivity extends AppCompatActivity {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = ViewOpt.createView(name, context, attrs);
if (view != null) {
return view;
}
return super.onCreateView(parent, name, context, attrs);
}
}
流程結束。
運行下,可以看下log:
2020-05-31 18:07:26.300 31454-31454/? D/lmj: LinearLayout 攔截生成
2020-05-31 18:07:26.300 31454-31454/? D/lmj: ViewStub 攔截生成
2020-05-31 18:07:26.300 31454-31454/? D/lmj: FrameLayout 攔截生成
2020-05-31 18:07:26.305 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarOverlayLayout 攔截生成
2020-05-31 18:07:26.306 31454-31454/? D/lmj: androidx.appcompat.widget.ContentFrameLayout 攔截生成
2020-05-31 18:07:26.311 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContainer 攔截生成
2020-05-31 18:07:26.318 31454-31454/? D/lmj: androidx.appcompat.widget.Toolbar 攔截生成
2020-05-31 18:07:26.321 31454-31454/? D/lmj: androidx.appcompat.widget.ActionBarContextView 攔截生成
2020-05-31 18:07:26.347 31454-31454/? D/lmj: androidx.constraintlayout.widget.ConstraintLayout 攔截生成
對應的佈局:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
有沒有很奇怪…
哪來的LinearLayout,ViewStub這些?
其實就是我們Activity的decorView對應的佈局文件裏面的。
爲啥沒有TextView?
因爲TextView並support庫攔截了,生成了AppcompatTextView,也是new的,早不需要走反射邏輯了。
ok,初步完工。
5. 一個潛在的問題
經過gradle,apt,以及對於LayoutInflater流程的瞭解,我們把相關知識拼接在一起,完成了這次佈局優化。
是不是還挺有成就感的。
不過,如果大家有對apt特別熟悉的,應該會發現一個潛在的問題。
什麼問題呢?
我們現在新建兩個library的module,讓:
app implementation lib1
lib1 implementation lib2
在lib2裏面寫個自定義控件。
我們在lib2中自定義一個控件Lib2View,然後在lib2的layout中引用。
lib2的xml:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Lib2Activity">
<com.example.lib2.Lib2View
android:layout_width="match_parent"
android:layout_height="match_parent"></com.example.lib2.Lib2View>
</androidx.constraintlayout.widget.ConstraintLayout>
然後我們再次執行app:assembleDebug
:
你會發現,報錯了:
ViewOpt__ViewCreator__Proxy.java:47: 錯誤: 找不到符號
return new com.example.lib2.Lib2View(context,attrs);
^
符號: 類 Lib2View
位置: 程序包 com.example.lib2
錯誤的原因是,雖然我們收集到了Lib2View,也生成了相關方法,代碼我們訪問不到這個類。
爲什麼訪問不到呢?
因爲我們用了implementation,再看一眼:
app implementation lib1
lib1 implementation lib2
implementation隔離了app對lib2的類引用,雖然打包後大家都能正常訪問,但是在編譯期間是訪問不到的。
這就是我說的apt存在的問題,apt主要是針對單個module的,對於這種多module並不是很合適。
所以,如果用Transfrom來做相關類生成,就不會有類似問題。
但是,博客我都寫到這了,你讓我換方案?
如果我們改成api,或者之前的compile,都能編譯通過。
那麼能不能在打包到時候,把implementation動態的換成api呢?
經過一頓對gradle API的摸索,發現是支持的:
project.afterEvaluate {
android.libraryVariants.all { variant ->
def variantName = variant.name
def depSet = new HashSet()
tasks.all {
if ("assemble${variantName.capitalize()}".equalsIgnoreCase(it.name)) {
project.configurations.each { conf ->
if (conf.name == "implementation") {
conf.dependencies.each { dep ->
depSet.add(dep)
project.dependencies.add("api", dep)
}
depSet.each {
conf.dependencies.remove(it)
}
}
}
}
}
}
}
我們可以將implementation,添加到api中。
將上述腳本apply到你的library的build.gradle中即可。
還留了一些問題
文中我們是以assembleDebug來演示的,那麼release打包怎麼辦呢?
release只需要修改一個地方,就是merger.xml,不在mergeDebugResources下了,而在mergeReleaseResources下面了。
其次因爲你的代理類需要反射,注意keep相關類就好了。
這個我就不特別幫大家處理了,如果release你搞不定,我建議你別實施這個方案了,先學習文章中相關知識點吧。
6. Google也在做類似的事情
看來Google也意識到layout構建的耗時了。
可以看到Google在做View Compiler的相關事情,不過目前尚未開啓,對應到運行時源碼中,應該是:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
可以看到inflate中多了一個tryInflatePrecompiled方法,看起來是可以直接給一個layout id,返回一個構建好的View。
期待後續該方案上線。
7. 總結
最近推送了非常多的性能優化的文案,大家也吐槽都不是實戰,感覺像理論。
於是花了一點時間分享一個View構建這一塊的優化給大家,可以看到就這麼點功能,其實我們涉及到了:
- gradle 構建相關知識;
- apt 相關知識;
- LayooutFactory相關知識;
雖然我們遇到了一些挫折,比如文章最後說到的apt生成的類,無法訪問非傳遞依賴模塊中的類,不過我們還是解決了。
有時候遇到問題,我就安慰自己:
感覺又能學到一點東西了
沒什麼一直順利的事情,正確面對問題,只有不斷的遇到問題、解決問題,才能成長。
另外,建議大家日常積累知識點,不要看到文章,發現自己不熟悉就不想看,看到自己其實早就清楚的,看的津津有味…
demo地址:
https://github.com/hongyangAndroid/ViewOptDemo