本文首发于微信公众号「Android开发之旅」,欢迎关注android
手机渲染主要依赖于两个硬件:CPU和GPU,其中CPU主要负责计算显示内容,其中包括视图建立、布局计算、图片解码和文本绘制等。GPU主要负责栅格化(UI元素绘制到屏幕上),好比将Button、Bitmap拆分红不一样的像素进行显示,最后完成绘制。性能优化
手机上显示的文字就是先经过CPU换算成纹理后在交给GPU进行渲染。而图片的显示首先经过CPU进行计算,而后再加载到内存中,传给GPU进行渲染。bash
咱们都知道Android系统每隔16ms就会发出Vsync信号(具体是由RootViewImpl类发起)触发UI渲染,即要求每一帧都要在16ms内渲染完成,因此无论你的布局逻辑多么的复杂,你都要在16ms内绘制完成,不然就会出现界面卡顿的现象。微信
咱们市面上绝大部分Android手机的屏幕刷新频率基本都是60Hz,由于60Hz每秒是人眼和大脑之间合做的极限,就像动画每秒24帧同样。app
这个咱们在启动优化中讲过具体的使用,这里呢,咱们主要关注他的Frames一行,显示绿色圆点表示正常,显示黄色或者红色表示出现了丢帧,出现丢帧的状况的时候咱们须要去查看Alerts栏。框架
这个是Android Studio自带的检测工具,在Tools栏目下。它能够帮助咱们查看视图的层次结构。async
从图中咱们能够看到左侧一览显示布局的层级。ide
choreoGrapher能够帮助咱们获取应用的FPS,即上文中的60Hz,而且能够线上使用,具有实时性。可是有一点须要注意的是必须API 16后使用。以下代码:工具
private var mStartFrameTime: Long = 0
private var mFrameCount = 0
private val MONITOR_INTERVAL = 160L //单次计算FPS使用160毫秒
private val MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L
private val MAX_INTERVAL = 1000L //设置计算fps的单位时间间隔1000ms,即fps/s;
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getFPS()
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private fun getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return
}
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (mStartFrameTime == 0L) {
mStartFrameTime = frameTimeNanos
}
val interval = frameTimeNanos - mStartFrameTime
if (interval > MONITOR_INTERVAL_NANOS) {
val fps = (mFrameCount.toLong() * 1000L * 1000L).toDouble() / interval * MAX_INTERVAL
Log.i("fps", fps.toString())
mFrameCount = 0
mStartFrameTime = 0
} else {
++mFrameCount
}
Choreographer.getInstance().postFrameCallback(this)
}
})
}
复制代码
执行代码后输出:布局
fps: 60.0158955700371
fps: 60.00346688030940
fps: 60.01226146521353
fps: 59.98537016806971
fps: 60.00205735054243
复制代码
每次打印的数据都在60左右,说明页面刷新没有出现卡顿。
咱们常常写的XML布局文件是如何被加载的呢?又是如何显示出来的?下面就带着你们顺着源码往下看,这里就不截图了,读者朋友们看完本章后本身能够去熟悉下这块代码。
首先要从setContentView方法开始提及了,其中调用了getDeleate().setContentView(resid)方法,接着调用了 LayoutInflater.from(this.mContext).inflate(resId, contentParent)来填充布局,这个API咱们你们应该都很熟悉了吧。紧接着调用getLayout方法,在getlayout方法中经过loadXmlResourceParser加载并解析XML布局文件,后面调用createViewFromTag方法,根据标签建立相对应为view,具体view的建立则是由Factory或者Factory2来完成的,首先先判断了Factory2为否为null,不为null,则用其建立view,不然就判断Factory是否为null,不为null,则由其建立。若是两个都为null,则不建立view,紧接着判断了mPrivateFactory是否为null,这里须要说明的是mPrivateFactory是一个隐藏的API只有framework才能调用,若是都没建立,那么view则由后续逻辑经过onCreateView或者createView经过反射来建立。具体流程图以下:
从上面的分析中咱们能够看出加载布局是有瓶颈的。其中有两个瓶颈分别是在布局文件解析的时候是一个IO过程,这确定是比较耗时的。再一个就是最后建立View的时候是经过反射的方式进行的。既然是反射性能确定也是有影响的,后面咱们也是围绕这两点进行布局加载的优化。
咱们作优化的前提就是得知道哪里是比较耗时的,因此检测耗时的UI仍是蛮重要的。只有知道问题在哪了才能针对性的解决它。这里讲到检测耗时,读过我启动优化一文的读者确定能想到至少两种方式,一种是手动埋点,另一种就是AOP的方式。手动埋点呢就是在setContentView方法的先后执行的地方手动打点。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LaunchRecord.startRecord()
setContentView(R.layout.activity_main)
LaunchRecord.endRecord("setContentView")
}
复制代码
打印:
===setContentView===170
复制代码
这种方式呢不够优雅并且对代码有侵入性。
下面咱们看下AOP的方式,操做和启动优化一文中的同样。
@Around("call(* 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.d("ContentViewTime", name + " cost " + (System.currentTimeMillis() - time));
}
复制代码
控制台打印:
ContentViewTime: MainActivity.setContentView(..) cost 74
复制代码
以上两种方法都是获取所有布局被加载完成后的时间,那么若是想获取单个控件的加载耗时如何作呢?这里给你们介绍LayoutInflaterCompat.setFactory2方式(你们之后看到带有Compat字段的都是兼容的API),其使用必须在super.onCreate以前调用。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
long start = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
long cost = System.currentTimeMillis() - start;
Log.d("onCreateView", "==" + name + "==cost==" + cost);
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
复制代码
控制台打印:
onCreateView: ==LinearLayout=cost==16
onCreateView: ==ViewStub=cost==0
onCreateView: ==FrameLayout=cost==0
onCreateView: ==android.support.v7.widget.ActionBarOverlayLayout=cost==0
onCreateView: ==android.support.v7.widget.ContentFrameLayout=cost==0
onCreateView: ==android.support.v7.widget.ActionBarContainer=cost==0
onCreateView: ==android.support.v7.widget.Toolbar=cost==0
onCreateView: ==android.support.v7.widget.ActionBarContextView=cost==0
onCreateView: ==android.support.constraint.ConstraintLayout=cost==0
onCreateView: ==TextView=cost==3
onCreateView: ==ImageView=cost==24
复制代码
LayoutInflaterCompat.setFactory2的API不只仅是能够统计View建立的时间,其实咱们还能够用来替换系统控件的操做,好比某一天产品经理提了一个需求要咱们将应用的TextView统一改为某种样式,咱们就可使用这种方式来作。如:
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if(TextUtils.equals("TextView",name)){
//替换为咱们本身的TextView
}
return null;//返回自定义View
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
复制代码
只要咱们在基类Activity的onCreate中定义这个方法,就能够实现相关效果。
基于布局加载的两个性能问题,谷歌给咱们提供了一个类AsyncLayoutInflater,它能够从侧面解决布局加载耗时的问题,AsyncLayoutInflater是在工做线程中加载布局,加载好后会回调到主线程,这样能够节省主线程的时间。这个类没有包含在SDK中,须要咱们在gradle中配置,如:
implementation 'com.android.support:asynclayoutinflater:28.0.0-alpha1'
复制代码
使用:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null,
new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
setContentView(view); //view以及加载完成
//能够在这里findViewById相关操做
}
});
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main); //这里就不用设置布局文件了
}
}
复制代码
咱们在inflate的时候就将布局文件设置给AsyncLayoutInflater,因此下面咱们就不须要在setContentView了。
上面说的AsyncLayoutInflater是从侧面解决布局加载耗时问题,那么咱们如何从根本上解决这个问题呢?主要问题就是咱们书写的XML文件须要加载解析和绘制,那若是咱们不使用XML文件写布局文件,问题是否是就解决?在Android中,还有另一种方式来写布局文件,那就是Java代码,经过Java代码来写布局,本质上是解决了性能问题,可是不便于开发,没有实时预览,并且可维护性太差。那么若是能有一种解决方式就是,咱们开发人员仍是正常写 XML文件,可是在加载的时候加载的是Java代码,那这样是否是很完美了。
因此下面给你们介绍一个新的框架:X2C,这是掌阅开源的一个框架,它保留了XML的优势,同时解决了性能问题,开发人员写XML文件,加载的时候只加载Java代码。
X2C的原理就是经过APT编译期时将XML翻译为Java代码。使用也很简单,首先gradle配置:
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
复制代码
Java代码使用:
@Xml(layouts = "activity_main")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main); //这里就不用设置布局文件了
}
}
复制代码
编译以后会在build/generated/source/apt/debug/ 下面生成相关的文件。如咱们的activity_main的布局文件会被翻译为:
Resources res = ctx.getResources();
ConstraintLayout constraintLayout0 = new ConstraintLayout(ctx);
TextView textView1 = new TextView(ctx);
ConstraintLayout.LayoutParams layoutParam1 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
textView1.setId(R.id.mTextView);
layoutParam1.topMargin= (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100,res.getDisplayMetrics())) ;
textView1.setText("Hello World!");
layoutParam1.leftToLeft = 0 ;
layoutParam1.rightToRight = 0 ;
layoutParam1.topToTop = 0 ;
layoutParam1.validate();
textView1.setLayoutParams(layoutParam1);
constraintLayout0.addView(textView1);
ImageView imageView2 = new ImageView(ctx);
ConstraintLayout.LayoutParams layoutParam2 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParam2.topMargin= (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,20,res.getDisplayMetrics())) ;
imageView2.setImageResource(R.mipmap.ic_launcher);
layoutParam2.leftToLeft = 0 ;
layoutParam2.rightToRight = 0 ;
layoutParam2.topToBottom = R.id.mTextView ;
layoutParam2.validate();
imageView2.setLayoutParams(layoutParam2);
constraintLayout0.addView(imageView2);
复制代码
运行APP,效果也是同样的。
X2C虽好,但也有一些问题,就是部分属性Java不支持,并且失去了系统的兼容性(AppCompat)。因此若是要带到线上使用,那么就要兼容不一样的版本,因此须要定制化修改源码。
咱们知道视图的绘制一般经历三个阶段,测量,肯定view的大小。布局,肯定view的具体位置包括view和viewGroup等。绘制,将view绘制完成。不论是测量、布局仍是绘制,每个阶段都是比较耗时的,都是自上而下的遍历每个view,在某些场景下还会触发屡次,好比嵌套使用RelativeLayout布局。
因此为了减小三个阶段的耗时,咱们须要减小view树的层级,不要嵌套使用RelativeLayout布局,不在嵌套使用的LinearLayout中使用weight属性。适当的使用merge标签,它能够减小一个view层级,可是必须使用在根view上。
这里推荐你们使用ConstraintLayout布局,ConstraintLayout几乎实现了彻底扁平化的布局,并且在构建复杂布局上面性能更高,同时他还具有了RelativeLayout和LinearLayout的特性,使用很方便。
同时咱们在书写布局的时候还要注意避免过分绘制。Android手机在开发者选项中有个功能叫:调试GPU过分绘制。打开后手机界面会有一层蒙版,其中蓝色表示能够接受,红色表色出现过分绘制了。那咱们如何避免过分绘制呢?首先是去掉多余的背景色,减小复杂shape的使用,避免层级叠加,在用自定义view的时候使用ClipRect屏蔽被遮盖view的绘制。
还有其余的一些优化视图绘制,好比使用Viewstub,它是一个高效的占位符,能够用来延迟加载view布局。还有就是咱们在onDraw中避免建立较大的对象和作耗时的操做等等。
以上就是相关布局优化相关的操做,也是从耗时到优化各个阶段的说明和操做。读者朋友们在看完本章节后,本身动手实践下,只有实际实践了才能发现问题,加深本身印象。