Android “退一步”的布局加载优化
其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:架构师筑基必备技能Android框架体系架构(高级UI+FrameWork源码)360°Androidapp全方位性能调优设计思想解读开源框架NDK模块开发移动架构师专题项目实战环节移动架构师不可不学习微信小程序混合开发的flutterAndroid学习的资料我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频
根目录: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: ${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 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方法:
private String generateClassInfoStr(HashSet 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 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
生成了。
@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,初步完工。
经过gradle,apt,以及对于LayoutInflater流程的了解,我们把相关知识拼接在一起,完成了这次布局优化。
是不是还挺有成就感的。
不过,如果大家有对apt特别熟悉的,应该会发现一个潜在的问题。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
最后说一下我的学习路线
其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:
- 架构师筑基必备技能
- Android框架体系架构(高级UI+FrameWork源码)
- 360°Androidapp全方位性能调优
- 设计思想解读开源框架
- NDK模块开发
- 移动架构师专题项目实战环节
- 移动架构师不可不学习微信小程序
- 混合开发的flutter
Android学习的资料
我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频,大概就下面这些,我就不全部写出来了,不然太长了影响大家的阅读。
330页PDF Android学习核心笔记(内含上面8大板块)
Android学习的系统对应视频
总结
我希望通过我自己的学习方法来帮助大家去提升技术:
-
1、多看书、看源码和做项目,平时多种总结
-
2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理
-
3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习
-
4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!
希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
43456377)]
[外链图片转存中…(img-vEpPtcFW-1711543456378)]
[外链图片转存中…(img-T8nAkNui-1711543456378)]
[外链图片转存中…(img-PO4NYaAH-1711543456379)]
[外链图片转存中…(img-oDqsoSsD-1711543456379)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
最后说一下我的学习路线
其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:
- 架构师筑基必备技能
- Android框架体系架构(高级UI+FrameWork源码)
- 360°Androidapp全方位性能调优
- 设计思想解读开源框架
- NDK模块开发
- 移动架构师专题项目实战环节
- 移动架构师不可不学习微信小程序
- 混合开发的flutter
[外链图片转存中…(img-uEduBlr1-1711543456379)]
Android学习的资料
我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频,大概就下面这些,我就不全部写出来了,不然太长了影响大家的阅读。
330页PDF Android学习核心笔记(内含上面8大板块)
[外链图片转存中…(img-ZQoP4eit-1711543456379)]
Android学习的系统对应视频
总结
我希望通过我自己的学习方法来帮助大家去提升技术:
-
1、多看书、看源码和做项目,平时多种总结
-
2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理
-
3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习
-
4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!
希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)