try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(“aop inflate”,name + " cost " + (System.currentTimeMillis() - time));
}
上面用的Aspectj,比较简单,上面的注解的意思是在setContentView方法执行内部去调用我们写好的getSetContentViewTime方法。
这样就可以获取相应的耗时。
我们可以看下打印的日志:
I/aop inflate: AppCompatActivity.setContentView(…) cost 69
I/aop inflate: AppCompatActivity.setContentView(…) cost 25
这样就可以实现无侵入的监控每个页面布局加载的耗时。
具体源码可见文末。
获取任一控件耗时
有时为了更精确的知道到底是哪个控件加载耗时,比如我们新添加了自定义View,需要监控它的性能。
我们可以利用setFactory2来监听每个控件的加载耗时。
首先我们来回顾下setContentView方法:
public final View tryCreateView(@Nullable View parent, @NonNull String name,
…
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;
}
…
return view;
}
在真正进行反射实例化xml结点前,会调用mFactory2的onCreateView方法。
这样如果我们重写onCreateView方法,在其前后加上耗时统计,即可获取每个控件的加载耗时。
private fun initItemInflateListener(){
LayoutInflaterCompat.setFactory2(layoutInflater, object : Factory2 {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
val time = System.currentTimeMillis()
val view = delegate.createView(parent, name, context, attrs)
Log.i(“inflate Item”,name + " cost " + (System.currentTimeMillis() - time))
return view
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return null
}
})
}
如上所示:真正的创建View的方法,仍然是调用delegate.createView,我们只是其之前与之后做了埋点。
注意,initItemInflateListener需要在onCreate之前调用。
这样就可以比较方便地实现监听每个控件的加载耗时。
=========================================================================
布局加载慢的主要原因有两个,一个是IO,一个是反射。
所以我们的优化思路一般有两个:
-
侧面缓解(异步加载)。
-
根本解决(不需要IO,反射过程,如X2C,Anko,Compose等)。
AsyncLayoutInflater方案
AsyncLayoutInflater 是来帮助做异步加载 layout 的,inflate(int, ViewGroup, OnInflateFinishedListener) 方法运行结束之后 OnInflateFinishedListener 会在主线程回调返回 View;这样做旨在 UI 的懒加载或者对用户操作的高响应。
简单的说我们知道默认情况下 setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:Xml的解析、View的反射创建等过程同样是在UI线程执行的,AsyncLayoutInflater就是来帮我们把这些过程以异步的方式执行,保持UI线程的高响应。
使用如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new AsyncLayoutInflater(AsyncLayoutActivity.this)
.inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
setContentView(view);
}
});
// 别的操作
}
这样做的优点在于将UI加载过程迁移到了子线程,保证了UI线程的高响应。
缺点在于牺牲了易用性,同时如果在初始化过程中调用了UI可能会导致崩溃。
X2C方案
X2C是掌阅开源的一套布局加载框架。
它的主要是思路是在编译期,将需要翻译的layout翻译生成对应的java文件,这样对于开发人员来说写布局还是写原来的xml,但对于程序来说,运行时加载的是对应的java文件。
这就将运行时的开销转移到了编译时。
如下所示,原始xml文件:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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”
android:paddingLeft=“10dp”>
<include
android:id=“@+id/head”
layout=“@layout/head”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_centerHorizontal=“true” />
<ImageView
android:id=“@+id/ccc”
style=“@style/bb”
android:layout_below=“@id/head” />
X2C 生成的 Java 文件:
public class X2C_2131296281_Activity_Main implements IViewCreator {
@Override
public View createView(Context ctx, int layoutId) {
Resources res = ctx.getResources();
RelativeLayout relativeLayout0 = new RelativeLayout(ctx);
relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0,0,0);
View view1 =(View) new X2C_2131296283_Head().createView(ctx,0);
RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
view1.setLayoutParams(layoutParam1);
relativeLayout0.addView(view1);
view1.setId(R.id.head);
layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);
ImageView imageView2 = new ImageView(ctx);
RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics())));
imageView2.setLayoutParams(layoutParam2);
relativeLayout0.addView(imageView2);
imageView2.setId(R.id.ccc);
layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);
return relativeLayout0;
}
}
使用时如下所示,使用X2C.setContentView替代原始的setContentView即可。
// this.setContentView(R.layout.activity_main);
X2C.setContentView(this, R.layout.activity_main);
X2C优点
-
在保留xml的同时,又解决了它带来的性能问题。
-
据X2C统计,加载耗时可以缩小到原来的1/3。
X2C问题
-
部分属性不能通过代码设置,Java不兼容。
-
将加载时间转移到了编译期,增加了编译期耗时。
-
不支持kotlin-android-extensions插件,牺牲了部分易用性。
Anko方案
Anko是JetBrains开发的一个强大的库,支持使用kotlin DSL的方式来写UI,如下所示:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
MyActivityUI().setContentView(this)
}
}
class MyActivityUI : AnkoComponent {
override fun createView(ui: AnkoContext) = with(ui) {
verticalLayout {
val name = editText()
button(“Say Hello”) {
onClick { ctx.toast(“Hello, ${name.text}!”) }
}
}
}
}
如上所示,Anko使用kotlin DSL实现布局,它比我们使用Java动态创建布局方便很多,主要是更简洁,它和拥有xml创建布局的层级关系,能让我们更容易阅读。
同时,它去除了IO与反射过程,性能更好,以下是Anko与XML的性能对比。
以上数据来源于:https://medium.com/android-news/400-faster-layouts-with-anko-da17f32c45dd
不过由于AnKo已经停止维护了,这里不建议大家使用,了解原理即可。
AnKo建议大家使用Jetpack Compose来替代使用。
Compose方案
Compose 是 Jetpack 中的一个新成员,是 Android 团队在2019年I/O大会上公布的新的UI库,目前处于Beta阶段。
Compose使用纯kotlin开发,使用简洁方便,但它并不是像Anko一样对ViewGroup的封装。
Compose 并不是对 View 和 ViewGroup 这套系统做了个上层包装来让写法更简单,而是完全抛弃了这套系统,自己把整个的渲染机制从里到外做了个全新的。
可以确定的是,Compose是取代XML的官方方案。
Compose的主要优点就在于它的简单好用,具体来说就是两点:
-
它的声明式 UI。
-
去掉了 xml,只使用 Kotlin 一种语言。
由于本文并不是介绍Compose的,所以就不继续介绍Compose了,总得来说,Compose是未来android UI开发的方向,读者可以自行查阅相关资料。
==========================================================================
上面介绍了不少布局加载优化方法,而我最后在项目中最后都没有使用,这就是从真从入门到放弃。
总得来说有以下几个原因:
-
有些方式(如AsyncLayoutInflater,X2C)牺牲了易用性,虽然性能提升了,但是开发变得麻烦了。
-
Anko使用上比较方便同时性能较高,但是比起XML方式改动很大,同时Anko已经放弃维护了,在团队中推动难度大。
-
Compose是未来android UI开发的方向,但目前仍处于Beta阶段,相信在Release后,会成为我们替换XML的有效手段。
-
还有最主要的一点是,针对我们的项目,布局加载耗时并不是主要耗时的地方,所以优化收益不大,可以将精力投入到其他地方。
如下所示,我们将setConteView前后时间相减,得到布局加载时间。
而onWindowFocusChanged是Activity真正可见时间,将其与onCreate时间相减,可得页面显示时间。
在我们的项目中测试效果如下:
android 5.0
I/Log: inflateTime:33
I/Log: activityShowTime:252
I/Log: inflateTime:11
I/Log: activityShowTime:642
I/Log: inflateTime:83
I/Log: activityShowTime:637
android 10.0
I/Log: inflateTime:11
I/Log: activityShowTime:88
I/Log: inflateTime:5
I/Log: activityShowTime:217
I/Log: inflateTime:27
I/Log: activityShowTime:221
我在android5.0手机与10.0手机上分别做了测试,在我们的项目中布局加载耗时并不很长,同时它们在整个页面可见过程中,占得比例也并不高。
所以得出结论:针对我们项目,布局加载耗时并不是主要耗时的地方,优化收益不大。
这就是从入门到放弃的原因。
一些常规优化手段
上面介绍了一些改动比较大的方案,其实我们在实际开发中也有些常规的方法可以优化布局加载。
比如优化布局层级,避免过度绘制等,这些简单的手段可能正是可以应用到项目中的。
优化布局层级及复杂度
学习福利
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
**
[外链图片转存中…(img-hcGX0hJ9-1714683199764)]
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-mGMkNHjv-1714683199764)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!