怎么收集项目中用到的所有的View的呢?
假设我们收集到了,手写的话,项目一般都是增量的,后续新增的View怎么办呢?
可以看到我们面临两个问题:
- 如何收集项目中在xml中使用到的View;
- 如何保证写出的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高级工程师学习资料!
上面所有学习内容,我愿意全部免费分享给大家。希望大家能多多支持。
开始实施
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的相关标签。
可以看到包含了我们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: r e s M e r g e F i l e . g e t A b s o l u t e P a t h ( ) = = = {resMergeFile.getAbsolutePath()} === 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个模块:
- 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 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方法:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
写在最后
由于本文罗列的知识点是根据我自身总结出来的,并且由于本人水平有限,无法全部提及,欢迎大神们能补充~
将来我会对上面的知识点一个一个深入学习,也希望有童鞋跟我一起学习,一起进阶。
提升架构认知不是一蹴而就的,它离不开刻意学习和思考。
**这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家,**梳理了多年的架构经验,筹备近1个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
领取方式:点击这里获取免费架构视频资料
最近还在整理并复习一些Android基础知识点,有问题希望大家够指出,谢谢。
希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!
转发+点赞+关注,第一时间获取最新知识点
…(img-knmwZJag-1711107407135)]
写在最后
由于本文罗列的知识点是根据我自身总结出来的,并且由于本人水平有限,无法全部提及,欢迎大神们能补充~
将来我会对上面的知识点一个一个深入学习,也希望有童鞋跟我一起学习,一起进阶。
提升架构认知不是一蹴而就的,它离不开刻意学习和思考。
**这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家,**梳理了多年的架构经验,筹备近1个月最新录制的,相信这份视频能给你带来不一样的启发、收获。
[外链图片转存中…(img-Sc4y3IX1-1711107407135)]
[外链图片转存中…(img-gtyyZ5fc-1711107407135)]
领取方式:点击这里获取免费架构视频资料
最近还在整理并复习一些Android基础知识点,有问题希望大家够指出,谢谢。
希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!
转发+点赞+关注,第一时间获取最新知识点
Android架构师之路很漫长,一起共勉吧!