Android 页面异步加载优化的几种方案
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:没错!皇叔开了个训练营
作者:newki
来源:https://juejin.cn/post/7125996361232678943
Android 异步加载布局的几种实现
场景如下:当我们启动一个 Activity
的时候,如果此页面的布局太过复杂,或者是一个很长的表单,此时加载布局,执行页面转场动画,等操作都是在主线程,可能会抢Cpu资源,导致主线程block住,感知就是卡顿。
要么是点了跳转按钮,但是等待1S才会出现动画,要么是执行动画的过程中卡顿。有没有什么方式能优化此等复杂页面的启动速度,达到秒启动?
我们之前讲动画的时候就知道,转场动画是无法异步执行的,那么我们能不能再异步加载布局呢?试试!
一、异步加载布局
LayoutInflater 的 inflate 方法的几种重载方法,大家应该都会的。这里我直接把布局加载到容器中试试。
1 lifecycleScope.launch {
2
3 val start = System.currentTimeMillis()
4
5 async(Dispatchers.IO) {
6 YYLogUtils.w("开始异步加载真正的跟视图")
7
8 val view = layoutInflater.inflate(R.layout.include_pensonal_turn_up_rate, mBinding.rootView,false)
9
10 val end = System.currentTimeMillis()
11
12 YYLogUtils.w("加载真正布局耗时:" + (end - start))
13
14 }
15
16 }
果不其然是报错的,不能在子线程添加View
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original
thread that created a view hierarchy can touch its views.
因为线程操作UI有 checkThread 的校验,添加布局操作改变了UI,校验线程就无法通过。
那么我们只在子线程创建布局,然后再主线程添加到容器中行不行?试试!
1 lifecycleScope.launch {
2
3 val start = System.currentTimeMillis()
4
5 val rootView = async(Dispatchers.IO) {
6 YYLogUtils.w("开始异步加载真正的跟视图")
7
8 val view = mBinding.viewStubRating.viewStub?.inflate()
9 val end = System.currentTimeMillis()
10
11 YYLogUtils.w("加载真正布局耗时:" + (end - start))
12
13 view
14 }
15
16
17 if (rootView.await() != null) {
18 val start1 = System.currentTimeMillis()
19 mBinding.llRootContainer.addView(rootView.await(), 0)
20 val end1 = System.currentTimeMillis()
21 YYLogUtils.w("添加布局耗时:" + (end1 - start1))
22
23 }
这样还真行,打印日志如下:
开始异步加载真正的跟视图 加载真正布局耗时:809 添加布局耗时:22
既然可行,那我们是不是就可以通过异步网络请求+异步加载布局,实现这样一样效果,进页面展示Loading占位图,然后异步网络请求+异步加载布局,当两个异步任务都完成之后展示布局,加载数据。
1 private fun inflateRootAndData() {
2
3 showStateLoading()
4
5 lifecycleScope.launch {
6
7 val start = System.currentTimeMillis()
8
9 val rootView = async(Dispatchers.IO) {
10 YYLogUtils.w("开始异步加载真正的跟视图")
11 val view = layoutInflater.inflate(R.layout.include_pensonal_turn_up_rate, null)
12 val end = System.currentTimeMillis()
13
14 YYLogUtils.w("加载真正布局耗时:" + (end - start))
15
16 view
17 }
18
19 val request = async {
20 YYLogUtils.w("开始请求用户详情数据")
21 delay(1500)
22 true
23 }
24
25 if (request.await() && rootView.await() != null) {
26 mBinding.llRootContainer.addView(rootView.await(), 0)
27 showStateSuccess()
28
29 popupProfile()
30 }
31
32 }
33 }
完美实现了秒进复杂页面的功能。当然有同学说了,自己写的行不行哦,会不会太Low,好吧,其实官方自己也出了一个异步加载布局框架,一起来看看。
二、AsyncLayoutInflater
部分源码如下:
1public final class AsyncLayoutInflater {
2 private static final String TAG = "AsyncLayoutInflater";
3
4 LayoutInflater mInflater;
5 Handler mHandler;
6 InflateThread mInflateThread;
7
8 public AsyncLayoutInflater(@NonNull Context context) {
9 mInflater = new BasicInflater(context);
10 mHandler = new Handler(mHandlerCallback);
11 mInflateThread = InflateThread.getInstance();
12 }
13
14 @UiThread
15 public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
16 @NonNull OnInflateFinishedListener callback) {
17 if (callback == null) {
18 throw new NullPointerException("callback argument may not be null!");
19 }
20 InflateRequest request = mInflateThread.obtainRequest();
21 request.inflater = this;
22 request.resid = resid;
23 request.parent = parent;
24 request.callback = callback;
25 mInflateThread.enqueue(request);
26 }
27
28 private Callback mHandlerCallback = new Callback() {
29 @Override
30 public boolean handleMessage(Message msg) {
31 InflateRequest request = (InflateRequest) msg.obj;
32 if (request.view == null) {
33 request.view = mInflater.inflate(
34 request.resid, request.parent, false);
35 }
36 request.callback.onInflateFinished(
37 request.view, request.resid, request.parent);
38 mInflateThread.releaseRequest(request);
39 return true;
40 }
41 };
42
43}
其实也没有什么魔法,就是启动了一个线程去加载布局,然后通过handler发出回调,只是线程内部多了一些任务队列和任务池。和我们直接用协程异步加载布局主线程添加布局是一样样的。
既然说到这里了,我们就用 AsyncLayoutInflater 实现一个一样的效果
1 var mUserProfile: String? = null
2 var mRootBinding: IncludePensonalTurnUpRateBinding? = null
3
4 private fun initData() {
5 showStateLoading()
6
7 YYLogUtils.w("开始异步加载真正的跟视图")
8 if (mBinding.llRootContainer.childCount <= 1) {
9 AsyncLayoutInflater(mActivity).inflate(R.layout.include_pensonal_turn_up_rate, null) { view, _, _ ->
10 mRootBinding = DataBindingUtil.bind<IncludePensonalTurnUpRateBinding>(view)?.apply {
11 click = clickProxy
12 }
13 mBinding.llRootContainer.addView(view, 0)
14
15 popupData2View()
16 }
17 }
18
19 YYLogUtils.w("开始请求用户详情数据")
20 CommUtils.getHandler().postDelayed({
21 mUserProfile = "xxx"
22 showStateSuccess()
23 popupData2View()
24 }, 1200)
25 }
26
27 private fun popupData2View() {
28 if (mUserProfile != null && mRootBinding != null) {
29 //加载数据
30 }
31 }
同样的是并发异步任务,异步加载布局和异步请求网络数据,然后都完成之后展示成功的布局,并显示数据。
他的效果和性能与上面协程自己写的是一样的。这里就不多说了。
当然 AsyncLayoutInflater
也有很多限制,相关的改进大家可以看看这里。
三、ViewStub 的占位
看到这里大家心里应该有疑问,你说的这种复杂的布局,我们都是使用 ViewStub 来占位,让页面能快速进入,完成之后再进行 ViewStub 的
inflate ,你整那么多花活有啥用!
确实,相信大家在这样的场景下确实用的比较多的都是使用 ViewStub 来占位,但是当 ViewStub 的布局比较大的时候
还是一样卡主线程,只是从进入页面前卡顿,转到进入页面后卡顿而已。
那我们再异步加载 ViewStub 不就行了嘛
1 private fun inflateRootAndData() {
2
3 showStateLoading()
4
5 lifecycleScope.launch {
6
7 val start = System.currentTimeMillis()
8
9 val rootView = async(Dispatchers.IO) {
10 YYLogUtils.w("开始异步加载真正的跟视图")
11
12 val view = mBinding.viewStubRating.viewStub?.inflate()
13 val end = System.currentTimeMillis()
14
15 YYLogUtils.w("加载真正布局耗时:" + (end - start))
16
17 view
18 }
19
20 val request = async {
21 YYLogUtils.w("开始请求用户详情数据")
22 delay(1500)
23 true
24 }
25
26 if (request.await() && rootView.await() != null) {
27 val start1 = System.currentTimeMillis()
28 mBinding.llRootContainer.addView(rootView.await(), 0)
29 val end1 = System.currentTimeMillis()
30 YYLogUtils.w("添加布局耗时:" + (end1 - start1))
31 showStateSuccess()
32
33 popupPartTimeProfile()
34 }
35
36 }
37 }
是的,和 LayoutInflater 的 inflate 一样,无法在子线程添加布局。
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original
thread that created a view hierarchy can touch its views. at
android.view.ViewRootImpl.checkThread(ViewRootImpl.java:10750) at
android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:2209)
ViewStub 的 inflate() 方法内部, replaceSelfWithView() 调用了
requestLayout,这部分checkThread。
那我们像 LayoutInflater 那样,子线程加载布局,在主线程添加进去?
这个嘛,好像还真没有。
那我们自己写一个?好像还真能。
四、AsyncViewStub 的定义与使用
其实很简单的实现,我们就是仿造 LayoutInflater 那样子线程加载布局,在主线程添加布局嘛。
自定义View如下,继承方式实现一个协程作用域,内部实现子线程加载布局,主线程替换占位View。关于自定义协程作用域相关的问题如果不了解的,可以看看我之前的[协程系列文章](https://juejin.cn/post/7121132393922035720
"https://juejin.cn/post/7121132393922035720")。
1/**
2 * 异步加载布局的 ViewStub
3 */
4class AsyncViewStub @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
5 View(context, attrs, defStyleAttr), CoroutineScope by MainScope() {
6
7 var layoutId: Int = 0
8 var mView: View? = null
9
10 init {
11 initAttrs(attrs, context)//初始化属性
12 }
13
14 private fun initAttrs(attrs: AttributeSet?, context: Context?) {
15 val typedArray = context!!.obtainStyledAttributes(
16 attrs,
17 R.styleable.AsyncViewStub
18 )
19
20 layoutId = typedArray.getResourceId(
21 R.styleable.AsyncViewStub_layout,
22 0
23 )
24
25 typedArray.recycle()
26 }
27
28
29 fun inflateAsync(block: (View) -> Unit) {
30
31 if (layoutId == 0) throw RuntimeException("没有找到加载的布局,你必须在xml中设置layout属性")
32
33 launch {
34
35 val view = withContext(Dispatchers.IO) {
36 LayoutInflater.from(context).inflate(layoutId, null)
37 }
38
39 mView = view
40
41 //添加到父布局
42 val parent = parent as ViewGroup
43 val index = parent.indexOfChild(this@AsyncViewStub)
44 val vlp: ViewGroup.LayoutParams = layoutParams
45 view.layoutParams = vlp //把 LayoutParams 给到新view
46
47 parent.removeViewAt(index) //删除原来的占位View
48 parent.addView(view, index) //把新有的View替换上去
49
50 block(view)
51 }
52 }
53
54 fun isInflate(): Boolean {
55 return mView != null
56 }
57
58 fun getInflatedView(): View? {
59 return mView
60 }
61
62 override fun onDetachedFromWindow() {
63 cancel()
64 super.onDetachedFromWindow()
65 }
66}
自定义属性
1 <!-- 异步加载布局 -->
2 <declare-styleable name="AsyncViewStub">
3 <attr name="layout" format="reference" />
4 </declare-styleable>
使用
1 <FrameLayout
2 android:layout_width="match_parent"
3 android:layout_height="match_parent">
4
5 <com.guadou.cs_cptservices.widget.AsyncViewStub
6 android:id="@+id/view_stub_root"
7 android:layout_width="match_parent"
8 android:layout_height="match_parent"
9 app:layout="@layout/include_part_time_job_detail_activity" />
10
11
12 <ImageView .../>
13
14 <TextView .../>
15
16 ...
17
18 </FrameLayout>
那么我们之前怎么使用 ViewStub 的 inflate,现在就怎么使用 AsyncViewStub ,只是从之前的主线程加载布局改变为子线程加载布局。
1 //请求工作详情数据-并加载真正的布局
2 private fun initDataAndRootView() {
3 if (!mBinding.viewStubRoot.isInflate()) {
4 val start1 = System.currentTimeMillis()
5 mBinding.viewStubRoot.inflateAsync { view ->
6 val end1 = System.currentTimeMillis()
7 YYLogUtils.w("添加布局耗时:" + (end1 - start1))
8 mRootBinding = DataBindingUtil.bind<IncludePartTimeJobDetailActivityBinding>(view)?.apply {
9 click = mClickProxy
10 }
11
12 initRV()
13 checkView2Showed()
14 }
15 }
16
17 //并发网络请求
18 requestDetailData()
19 }
20
21 //这里请求网络数据完成,只展示顶部图片和标题和TabView和ViewPager
22 private fun requestDetailData() {
23 mViewModel.requestJobDetail().observe(this) {
24 checkView2Showed()
25 }
26 }
27
28 //查询异步加载的布局和异步的远端数据是否已经准备就绪
29 private fun checkView2Showed() {
30 if (mViewModel.mPartTimeJobDetail != null && mRootBinding != null) {
31
32 mRootBinding?.setVariable(BR.viewModel, mViewModel)
33
34 showStateSuccess()
35
36 initPager()
37 popupData2Top()
38 }
39 }
实现的效果如下(静态页面+模拟请求):
不好意思,没改之前的时候没有保存到图,之前的效果是点击会卡顿到900毫秒到1秒的时间进入页面,比较卡顿。
当然,我们这个页面不算是很复杂的页面,在低端的手机上,也只是卡顿900毫秒。
我们还有更复杂的页面,比如炒鸡复杂的表单页面,目测是卡顿2秒左右才进入页面,后面我会针对性的对页面进行类似的优化。比如我会把复杂的页面分为多个子布局,异步加载布局的时候可以使用多个异步任务来持续优化加载速度。
总结
Ok, 这里仅仅是提供了另一种方案,切勿生搬硬套,就一定要把所有的页面,都改造一番,毕竟这么用增加了使用成本和风险。
总的来说,如果你的页面并不是很复杂,也没必要使用此方法,当然了,如果你的页面确实很复杂,并且在查找一些优化的方式,那你不妨一试,确实能起到一定的优化作用。
为了防止失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号👇