| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android 布局优化是真的难,从入门到放弃…,作为一名Android面试者你应该知道的 -> 正文阅读 |
|
[移动开发]Android 布局优化是真的难,从入门到放弃…,作为一名Android面试者你应该知道的 |
双缓冲机制 看完上面的流程图,我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制)。 如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢? 有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢? 所以,在屏幕刷新中,Android系统引入了双缓冲机制。 GPU只向Back Buffer中写入绘制数据,且GPU会定期交换Back Buffer和Frame Buffer,交换的频率也是60次/秒,这就与屏幕的刷新频率保持了同步。 虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,CPU并不能保证在16.6ms内就完成绘制数据的计算,所以这里系统又做了一个处理。 当你的应用正在往Back Buffer中填充数据时,系统会将Back Buffer锁定。 如果到了GPU交换两个Buffer的时间点,你的应用还在往Back Buffer中填充数据,GPU会发现Back Buffer被锁定了,它会放弃这次交换。 这样做的后果就是手机屏幕仍然显示原先的图像,这就是我们常常说的掉帧。 布局加载原理 由上面可知,导致掉帧的原因是CPU无法在16.6ms内完成绘制数据的计算。 而之所以布局加载可能会导致掉帧,正是因为它在主线程上进行了耗时操作,可能导致CPU无法按时完成数据计算。 布局加载主要通过setContentView来实现,我们就不在这里贴源码了,一起来看看它的时序图。 我们可以看到,在setContentView中主要有两个耗时操作:
以上两点就是布局加载可能导致卡顿的原因,也是布局的性能瓶颈。 ========================================================================= 我们如果需要优化布局卡顿问题,首先最重要的就是:确定定量标准。 所以我们首先介绍几种获取布局文件加载耗时的方法。 常规获取 首先介绍一下常规方法: val?start?=?System.currentTimeMillis() setContentView(R.layout.activity_layout_optimize) val?inflateTime?=?System.currentTimeMillis()?-?start 这种方法很简单,因为setContentView是同步方法,如果想要计算耗时,直接将前后时间计算相减即可得到结果了。 AOP(Aspectj,ASM) 上面的方式虽然简单,但是却不够优雅,同时代码有侵入性,如果要对所有Activity测量时,就需要在基类中复写相关方法了,比较麻烦了。 下面介绍一种AOP的方式计算耗时。 @Around(“execution(*?android.app.Activity.setContentView(…))”) public?void?getSetContentViewTime(ProceedingJoinPoint?joinPoint)?{ Signature?signature?=?joinPoint.getSignature(); String?name?=?signature.toShortString(); long?time?=?System.currentTimeMillis(); 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,一个是反射。 所以我们的优化思路一般有两个:
AsyncLayoutInflater方案
简单的说我们知道默认情况下?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.s
etContentView(this,?R.layout.activity_main); X2C优点
X2C问题
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的性能对比。
不过由于AnKo已经停止维护了,这里不建议大家使用,了解原理即可。 AnKo建议大家使用Jetpack Compose来替代使用。 Compose方案 Compose 是 Jetpack 中的一个新成员,是 Android 团队在2019年I/O大会上公布的新的UI库,目前处于Beta阶段。 Compose使用纯kotlin开发,使用简洁方便,但它并不是像Anko一样对ViewGroup的封装。 Compose 并不是对 View 和 ViewGroup 这套系统做了个上层包装来让写法更简单,而是完全抛弃了这套系统,自己把整个的渲染机制从里到外做了个全新的。 |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 2:49:14- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |