当前位置:网站首页>开箱即用-使用异步加载布局来优化页面启动速度的几种方案
开箱即用-使用异步加载布局来优化页面启动速度的几种方案
2022-08-02 10:13:00 【锐湃】

Android 异步加载布局的几种实现
场景如下:当我们启动一个 Activity 的时候,如果此页面的布局太过复杂,或者是一个很长的表单,此时加载布局,执行页面转场动画,等操作都是在主线程,可能会抢Cpu资源,导致主线程block住,感知就是卡顿。
要么是点了跳转按钮,但是等待1S才会出现动画,要么是执行动画的过程中卡顿。有没有什么方式能优化此等复杂页面的启动速度,达到秒启动?
我们之前讲动画的时候就知道,转场动画是无法异步执行的,那么我们能不能再异步加载布局呢?试试!
一、异步加载布局
LayoutInflater 的 inflate 方法的几种重载方法,大家应该都会的。这里我直接把布局加载到容器中试试。
lifecycleScope.launch {
val start = System.currentTimeMillis()
async(Dispatchers.IO) {
YYLogUtils.w("开始异步加载真正的跟视图")
val view = layoutInflater.inflate(R.layout.include_pensonal_turn_up_rate, mBinding.rootView,false)
val end = System.currentTimeMillis()
YYLogUtils.w("加载真正布局耗时:" + (end - start))
}
}
复制代码果不其然是报错的,不能在子线程添加View
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
因为线程操作UI有 checkThread 的校验,添加布局操作改变了UI,校验线程就无法通过。
那么我们只在子线程创建布局,然后再主线程添加到容器中行不行?试试!
lifecycleScope.launch {
val start = System.currentTimeMillis()
val rootView = async(Dispatchers.IO) {
YYLogUtils.w("开始异步加载真正的跟视图")
val view = mBinding.viewStubRating.viewStub?.inflate()
val end = System.currentTimeMillis()
YYLogUtils.w("加载真正布局耗时:" + (end - start))
view
}
if (rootView.await() != null) {
val start1 = System.currentTimeMillis()
mBinding.llRootContainer.addView(rootView.await(), 0)
val end1 = System.currentTimeMillis()
YYLogUtils.w("添加布局耗时:" + (end1 - start1))
}
复制代码这样还真行,打印日志如下:
开始异步加载真正的跟视图 加载真正布局耗时:809 添加布局耗时:22
既然可行,那我们是不是就可以通过异步网络请求+异步加载布局,实现这样一样效果,进页面展示Loading占位图,然后异步网络请求+异步加载布局,当两个异步任务都完成之后展示布局,加载数据。
private fun inflateRootAndData() {
showStateLoading()
lifecycleScope.launch {
val start = System.currentTimeMillis()
val rootView = async(Dispatchers.IO) {
YYLogUtils.w("开始异步加载真正的跟视图")
val view = layoutInflater.inflate(R.layout.include_pensonal_turn_up_rate, null)
val end = System.currentTimeMillis()
YYLogUtils.w("加载真正布局耗时:" + (end - start))
view
}
val request = async {
YYLogUtils.w("开始请求用户详情数据")
delay(1500)
true
}
if (request.await() && rootView.await() != null) {
mBinding.llRootContainer.addView(rootView.await(), 0)
showStateSuccess()
popupProfile()
}
}
}
复制代码完美实现了秒进复杂页面的功能。当然有同学说了,自己写的行不行哦,会不会太Low,好吧,其实官方自己也出了一个异步加载布局框架,一起来看看。
二、AsyncLayoutInflater
部分源码如下:
public final class AsyncLayoutInflater {
private static final String TAG = "AsyncLayoutInflater";
LayoutInflater mInflater;
Handler mHandler;
InflateThread mInflateThread;
public AsyncLayoutInflater(@NonNull Context context) {
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
@NonNull OnInflateFinishedListener callback) {
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
}
InflateRequest request = mInflateThread.obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
mInflateThread.enqueue(request);
}
private Callback mHandlerCallback = new Callback() {
@Override
public boolean handleMessage(Message msg) {
InflateRequest request = (InflateRequest) msg.obj;
if (request.view == null) {
request.view = mInflater.inflate(
request.resid, request.parent, false);
}
request.callback.onInflateFinished(
request.view, request.resid, request.parent);
mInflateThread.releaseRequest(request);
return true;
}
};
}
复制代码其实也没有什么魔法,就是启动了一个线程去加载布局,然后通过handler发出回调,只是线程内部多了一些任务队列和任务池。和我们直接用协程异步加载布局主线程添加布局是一样样的。
既然说到这里了,我们就用 AsyncLayoutInflater 实现一个一样的效果
var mUserProfile: String? = null
var mRootBinding: IncludePensonalTurnUpRateBinding? = null
private fun initData() {
showStateLoading()
YYLogUtils.w("开始异步加载真正的跟视图")
if (mBinding.llRootContainer.childCount <= 1) {
AsyncLayoutInflater(mActivity).inflate(R.layout.include_pensonal_turn_up_rate, null) { view, _, _ ->
mRootBinding = DataBindingUtil.bind<IncludePensonalTurnUpRateBinding>(view)?.apply {
click = clickProxy
}
mBinding.llRootContainer.addView(view, 0)
popupData2View()
}
}
YYLogUtils.w("开始请求用户详情数据")
CommUtils.getHandler().postDelayed({
mUserProfile = "xxx"
showStateSuccess()
popupData2View()
}, 1200)
}
private fun popupData2View() {
if (mUserProfile != null && mRootBinding != null) {
//加载数据
}
}
复制代码同样的是并发异步任务,异步加载布局和异步请求网络数据,然后都完成之后展示成功的布局,并显示数据。
他的效果和性能与上面协程自己写的是一样的。这里就不多说了。
当然 AsyncLayoutInflater 也有很多限制,相关的改进大家可以看看这里。
三、ViewStub 的占位
看到这里大家心里应该有疑问,你说的这种复杂的布局,我们都是使用 ViewStub 来占位,让页面能快速进入,完成之后再进行 ViewStub 的 inflate ,你整那么多花活有啥用!
确实,相信大家在这样的场景下确实用的比较多的都是使用 ViewStub 来占位,但是当 ViewStub 的布局比较大的时候 还是一样卡主线程,只是从进入页面前卡顿,转到进入页面后卡顿而已。
那我们再异步加载 ViewStub 不就行了嘛
private fun inflateRootAndData() {
showStateLoading()
lifecycleScope.launch {
val start = System.currentTimeMillis()
val rootView = async(Dispatchers.IO) {
YYLogUtils.w("开始异步加载真正的跟视图")
val view = mBinding.viewStubRating.viewStub?.inflate()
val end = System.currentTimeMillis()
YYLogUtils.w("加载真正布局耗时:" + (end - start))
view
}
val request = async {
YYLogUtils.w("开始请求用户详情数据")
delay(1500)
true
}
if (request.await() && rootView.await() != null) {
val start1 = System.currentTimeMillis()
mBinding.llRootContainer.addView(rootView.await(), 0)
val end1 = System.currentTimeMillis()
YYLogUtils.w("添加布局耗时:" + (end1 - start1))
showStateSuccess()
popupPartTimeProfile()
}
}
}
复制代码是的,和 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。
/**
* 异步加载布局的 ViewStub
*/
class AsyncViewStub @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr), CoroutineScope by MainScope() {
var layoutId: Int = 0
var mView: View? = null
init {
initAttrs(attrs, context)//初始化属性
}
private fun initAttrs(attrs: AttributeSet?, context: Context?) {
val typedArray = context!!.obtainStyledAttributes(
attrs,
R.styleable.AsyncViewStub
)
layoutId = typedArray.getResourceId(
R.styleable.AsyncViewStub_layout,
0
)
typedArray.recycle()
}
fun inflateAsync(block: (View) -> Unit) {
if (layoutId == 0) throw RuntimeException("没有找到加载的布局,你必须在xml中设置layout属性")
launch {
val view = withContext(Dispatchers.IO) {
LayoutInflater.from(context).inflate(layoutId, null)
}
mView = view
//添加到父布局
val parent = parent as ViewGroup
val index = parent.indexOfChild([email protected])
val vlp: ViewGroup.LayoutParams = layoutParams
view.layoutParams = vlp //把 LayoutParams 给到新view
parent.removeViewAt(index) //删除原来的占位View
parent.addView(view, index) //把新有的View替换上去
block(view)
}
}
fun isInflate(): Boolean {
return mView != null
}
fun getInflatedView(): View? {
return mView
}
override fun onDetachedFromWindow() {
cancel()
super.onDetachedFromWindow()
}
}
复制代码自定义属性
<!-- 异步加载布局 -->
<declare-styleable name="AsyncViewStub">
<attr name="layout" format="reference" />
</declare-styleable>
复制代码使用
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.guadou.cs_cptservices.widget.AsyncViewStub
android:id="@+id/view_stub_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout="@layout/include_part_time_job_detail_activity" />
<ImageView .../>
<TextView .../>
...
</FrameLayout>
复制代码那么我们之前怎么使用 ViewStub 的 inflate,现在就怎么使用 AsyncViewStub ,只是从之前的主线程加载布局改变为子线程加载布局。
//请求工作详情数据-并加载真正的布局
private fun initDataAndRootView() {
if (!mBinding.viewStubRoot.isInflate()) {
val start1 = System.currentTimeMillis()
mBinding.viewStubRoot.inflateAsync { view ->
val end1 = System.currentTimeMillis()
YYLogUtils.w("添加布局耗时:" + (end1 - start1))
mRootBinding = DataBindingUtil.bind<IncludePartTimeJobDetailActivityBinding>(view)?.apply {
click = mClickProxy
}
initRV()
checkView2Showed()
}
}
//并发网络请求
requestDetailData()
}
//这里请求网络数据完成,只展示顶部图片和标题和TabView和ViewPager
private fun requestDetailData() {
mViewModel.requestJobDetail().observe(this) {
checkView2Showed()
}
}
//查询异步加载的布局和异步的远端数据是否已经准备就绪
private fun checkView2Showed() {
if (mViewModel.mPartTimeJobDetail != null && mRootBinding != null) {
mRootBinding?.setVariable(BR.viewModel, mViewModel)
showStateSuccess()
initPager()
popupData2Top()
}
}
复制代码实现的效果如下(静态页面+模拟请求):

不好意思,没改之前的时候没有保存到图,之前的效果是点击会卡顿到900毫秒到1秒的时间进入页面,比较卡顿。
当然,我们这个页面不算是很复杂的页面,在低端的手机上,也只是卡顿900毫秒。
我们还有更复杂的页面,比如炒鸡复杂的表单页面,目测是卡顿2秒左右才进入页面,后面我会针对性的对页面进行类似的优化。比如我会把复杂的页面分为多个子布局,异步加载布局的时候可以使用多个异步任务来持续优化加载速度。
总结
Ok, 这里仅仅是提供了另一种方案,切勿生搬硬套,就一定要把所有的页面,都改造一番,毕竟这么用增加了使用成本和风险。
总的来说,如果你的页面并不是很复杂,也没必要使用此方法,当然了,如果你的页面确实很复杂,并且在查找一些优化的方式,那你不妨一试,确实能起到一定的优化作用。

作者:newki
链接:https://juejin.cn/post/7125996361232678943
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
边栏推荐
- Spearman's correlation coefficient
- php组件漏洞
- 你认同这个观点吗?大多数企业的数字化都只是为了缓解焦虑
- LayaBox---TypeScript---Three slash instructions
- R language ggplot2 visualization: use the ggbarplot function of the ggpubr package to visualize the horizontal column chart (bar chart), use the orientation parameter to set the column chart to be tra
- 斯皮尔曼相关系数
- How to choose a truly "easy-to-use, high-performance" remote control software
- Using the TCP protocol, will there be no packet loss?
- 行为型模式-策略模式
- 软件测试之发现和解决bug
猜你喜欢

日元疲软令游戏机在日本变身“理财产品”:黄牛大赚

MySql千万级分页优化,快速插入千万数据方法

npm ERR! 400 Bad Request - PUT xxx - Cannot publish over previously published version “1.0.0“.

【OpenCV】-霍夫变换

matlab-day02

Spearman's correlation coefficient

新“内卷”席卷科技圈,Google CEO 要求 174000 员工提高工作效率!

npm ERR! 400 Bad Request - PUT xxx - Cannot publish over previously published version “1.0.0“.

关于缓存数据的探讨

MSYS2 QtCreator Clangd 代码分析找不到 mm_malloc.h的问题补救
随机推荐
新“内卷”席卷科技圈,Google CEO 要求 174000 员工提高工作效率!
你认同这个观点吗?大多数企业的数字化都只是为了缓解焦虑
身为程序猿——谷歌浏览器的这些骚操作你真的废吗!【熬夜整理&建议收藏】[通俗易懂]
npm ERR! 400 Bad Request - PUT xxx - Cannot publish over previously published version “1.0.0“.
LayaBox---TypeScript---三斜线指令
R language ggplot2 visualization: use the ggbarplot function of the ggpubr package to visualize the horizontal column chart (bar chart), use the orientation parameter to set the column chart to be tra
Supervised learning of Li Hang's "Statistical Learning Methods" Notes
iNFTnews | Seeing the two sides of the metaverse, what is the true Internet and the Internet of value?
牛客网项目17节生成验证码 刷新验证码一直没反应
Linux system uninstall, install, upgrade, migrate clickHouse database
神通数据库,批量插入数据的时候失败
软件测试之发现和解决bug
使用较广泛的安全测试工具有哪些?
【术语科普】关于集成工作台那些难懂的词儿,看这篇秒懂!
logo 图标(php图片加文字水印)
如何安装dosbox(pycharm详细安装教程)
Smoothing of time series data in R language: smoothing time series data to remove noise using the dpill function and locpoly function of the KernSmooth package
【云原生】快出数量级的性能是怎样炼成的?就提升了亿点点
8月份的.NET Conf 活动 专注于 .NET MAUI
LayaBox---TypeScript---Mixins