Android “退一步”的布局加载优化

根目录: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个模块:

  1. ViewOptAnnotation: 存放注解;

  2. ViewOptProcessor:放注解处理器相关代码;

  3. 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

生成了。

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特别熟悉的,应该会发现一个潜在的问题。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

分享读者

作者2013年java转到Android开发,在小厂待过,也去过华为,OPPO等大厂待过,18年四月份进了阿里一直到现在。

被人面试过,也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长,而且极易碰到天花板技术停滞不前!

我们整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。

主要包括腾讯,以及字节跳动,阿里,华为,小米,等一线互联网公司主流架构技术。

腾讯T3架构师学习专题资料

如果你觉得自己学习效率低,缺乏正确的指导,可以一起学习交流!

我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

是还挺有成就感的。

不过,如果大家有对apt特别熟悉的,应该会发现一个潜在的问题。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-hI70mYMv-1711765193346)]

[外链图片转存中…(img-Y9rVUFYt-1711765193346)]

[外链图片转存中…(img-54UQqHra-1711765193346)]

[外链图片转存中…(img-nLBWbhCO-1711765193347)]

[外链图片转存中…(img-bg0QNjy3-1711765193347)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

分享读者

作者2013年java转到Android开发,在小厂待过,也去过华为,OPPO等大厂待过,18年四月份进了阿里一直到现在。

被人面试过,也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长,而且极易碰到天花板技术停滞不前!

我们整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。

主要包括腾讯,以及字节跳动,阿里,华为,小米,等一线互联网公司主流架构技术。

[外链图片转存中…(img-VE9rW6x0-1711765193347)]

如果你觉得自己学习效率低,缺乏正确的指导,可以一起学习交流!

我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值