CPU的任务繁多,做逻辑计算外,还要做内存管理、显示操作,因此在实际运算的时候性能会大打折扣,在没有GPU的时代,不能显示复杂的图形,其运算速度远更不上今天复杂三维游戏的要求。即使CPU的工作频率超过2GH或更高,对它绘制图形提高也不大,这时GPU的设计就出来了。结构图对比如下:
黄色:Control控制器,用于协调控制整个CPU的运行,包括取出指令、控制其他模块的运行等;
绿色:ALU(Arithmetic Logic Unit),是算术逻辑单元,用于运行数学、逻辑运算;
橙色:Cache和DRAM分别为缓存和RAM,用于存储信息。
从结构图可以看出,CPU的控制器较为复杂,而ALU数量较少,因此CPU擅长各种复杂的逻辑运算,但不擅长数学尤其是浮点运算。
结合到Android的UI绘制流程可以简单的理解为如下关系:
CPU计算画图的方法得到矢量图,而GPU通过像素填充,将矢量图转为位图,绘制显示出来。这个过程就是栅格化。
矢量图:又叫向量图,是用一系列计算机指令来描述和记录一幅图,一幅图可以理解为一些列由点、线、面等组成的子图,它所记录的是对象的几何形状、线条粗细和色彩等
位图:位图又叫点阵图或像素图,计算机屏幕上的图像是由像素构成的,每个点用二进制数据来描述其颜色与宽度等信息,这些点是离散的,类似于点阵,多个像素的色彩组合形成了图像,称之为位图
Android系统每隔16ms发出VSYNC信号(1000ms/60=16.66ms),触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着计算渲染的大多数操作都必须在16ms内完成。这里只是简单的提一下,有个初步的认识,后续会专门用一篇文章来详细分析Android的屏幕刷新机制。
public class MainActivity extends AppCompatActivity {
private long mStartFrameTime = 0;
private int mFrameCount = 0;
private static final long MONITOR_INTERVAL = 160L; //单次计算FPS使用160毫秒
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
private static final long MAX_INTERVAL = 1000L; //设置计算fps的单位时间间隔1000ms,即fps/s;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getFPS();
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStartFrameTime == 0) {
mStartFrameTime = frameTimeNanos;
}
long interval = frameTimeNanos - mStartFrameTime;
if (interval > MONITOR_INTERVAL_NANOS) {
double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
Log.d(TAG, "doFrame: fps = " + fps);
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
当一帧画面绘制渲染时间超过16ms的时候,垂直同步机制会让显示器硬件等待GPU完成栅格化绘制宣传操作,这样就导致这一帧画面,多停留了16ms,甚至更多,这样就造成了用户看起来画面卡顿。
UI绘制渲染需要尽量在16ms内完成,这段时间主要被两件事情占用:
所以要缩短这两部分时间,也就是说要减少对象对象转换的次数,以及传递数据的次数。具体要如何减少这两部分时间,以至于尽量在16ms内完成绘制渲染呢?
减少xml转换成对象的时间
减少重复绘制的时间
布局加载源码流程
上图就是布局加载的大致流程图,有兴趣的可以参考一下,自行查阅源码,通过反射生成view的核心代码如下:
Sdk/sources/android-29/android/view/LayoutInflate.java
@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
...
constructor = clazz.getConstructor(mConstructorSignature);
...
constructor.setAccessible(true);
...
sConstructorMap.put(name, constructor);
...
final View view = constructor.newInstance(args);
}
LayoutInflate是创建view的一个Hook,我们通常对Hook的理解就是,它是一个挂钩,将自己的代码钩在上面,当执行原始方法时,也会执行添加的代码,进而达到修改相关逻辑的效果。比如你要实现一个具有特殊效果的TextView,是不是要对原生的TextView进行替换和更改,这种方案不太优雅,而且工作量也较大。就可以考虑使用LayoutInflate.Factory进行全局的替换,通过它来Hook并判断,如果加载的是TextView,替换成自定义的TextView。
首先将xml布局文件加载到内存,即xml布局文件解析的过程是一个IO过程,如果布局文件较大,读取到内存,这是耗性能的。其次,加载到内存后,创建view对象是通过反射实现的,如果xml布局里面控件较多,就会多次使用反射,这一点也是耗性能的。
Sdk/sources/android-29/android/view/LayoutInflate.java
public interface Factory {
/**
* // 1 当我们填充布局的时候,可以调用这个Hook(挂钩),来自定义xml布局文件里面的标记名称
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
*
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.// 2 需要填充的标记名称(控件名称)
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
@Nullable
View onCreateView(@NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs);
}
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; note that this may be null.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
@Nullable
View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs);
}
注释1处,的英文注释翻译过来大意就是,当我们填充布局的时候,可以调用这个Hook(挂钩),来自定义xml布局文件里面的标记名称,方法参数name表示需要填充的标记名称(控件名称)。而Factory2继承Factory,功能更强,方法多了一个参数,parent表示父容器,即创建的view,摆放的地方。所以加载布局的过程中都是优先使用的Factory2。先分析到这里,有个初步的认识,后面会分析具体的使用。
如果代码是自己写的,我们就比较清楚页面布局的复杂情况,但是如果是你接手别人的代码,想要快速的定位哪些页面比较复杂,或是哪些页面是要进行布局优化的重点对象,一个比较简单的思路,就是通过AOP,选取每个页面的setContentView作为切面点,监测页面加载布局的耗时,根据耗时情况来优先找到耗时较长的页面进行分析处理。
@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.d("xpf", name + " 耗时:" + (System.currentTimeMillis() - time));
}
笔者先后打开了两个Activity,监测它们的setContentVIew方法耗时情况如下:分别为225ms和23ms
这里为了方便演示,xml布局中就放了几个ImageView,就不具体分析了,实际监测的时候,就找到setContentView耗时较长的页面,检查布局结构。
如果想要获取每一个具体控件的加载耗时,该从何处入手呢?如果加入过多的监测逻辑,有那么多布局和控件,侵入性太强显然是不可取的。这种场景就可以使用前面提到过的LayoutInflate.Factory了,它是加载创建view的过程中对View的Hook,加上时间戳,就能获取每一个控件加载的耗时情况了。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
//场景1:全局替换自定义TextView
if (TextUtils.equals("TextView", name)) {
//创建自定义TextView
//MyTextView myTextView = new MyTextView();
//return myTextView;
}
//场景2:获取每一个控件的加载时间
long time = System.currentTimeMillis();
//Hook要加载创建的view
View view = getDelegate().createView(parent, name, context, attrs);
Log.d(TAG, name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return null;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
对于场景1:只需要在基类BaseActivity中添加逻辑,其它页面继承即可实现全局替换自定义TextView的需求了。
对于场景2:Hook创建加载的view,在前后加上时间戳,就能获取每一个控件的加载的耗时情况了,记得返回hook的view。
有个小细节提一下,这里用的是LayoutInflaterCompat,通常情况下后缀Compat表示有更好的兼容效果,如果有Compat兼容类还是尽量选择兼容类。下面就是MainActivity页面的控件加载耗时情况:
看到这里,相信你已经掌握了,布局加载耗时监测和控件加载耗时监测,这样就能有针对性的进行优化了。
前面提到系统加载xml布局的过程有两大性能痛点:
既然我们暂时想不到从根本上替代IO过程和反射的办法,那就想办法从侧面解决,如果走进setContentView里面了解过UI绘制流程,就会发现这两个过程都是在主线程执行的,我们能否让它们在子线程执行,减少主线程耗时,间接达到加快布局加载过程。接下来介绍谷歌提供的AsyncLayoutInflate,简称异步inflate:
public class MainActivity extends AppCompatActivity{
@Override
protected void onCreate(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);
mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
mRecyclerView.setAdapter(mNewsAdapter);
mNewsAdapter.setOnFeedShowCallBack(MainActivity.this);
}
});
//setContentView(R.layout.activity_main);
}
AsyncLayoutInflate通过异步加载的方式,减轻了主线程的耗时,这只是缓解了布局加载因为IO操作,反射创建view对象造成布局加载慢的问题,有没有什么方式可以避免这两个过程呢?那就是通过java代码的方式写布局,代替xml方式写布局。没有了加载xml到内存的IO过程,通过new 的方式代替反射常见view,可以说是从根本上解决了这两点造成的性能问题。但是这种方式解决了性能上的问题,也会带来新的问题,那就是不便于开发,可维护性差,xml布局方式比较简单,便于预览。后者就不一样了。所以说没有完美的方案,只有适合的场景,可以根据自身实际情况进行选择和取舍。有没有方案即保留xml方式的优点,又能避免它的性能缺陷呢?下面要介绍的就是这样一款第三方框架X2C。
X2C介绍
X2C的使用
@Xml(layouts = "activity_main")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
X2C.setContentView(this, R.layout.activity_main);
}
但是X2C也有小缺点,那就是部分属性Java不支持,失去了相关体统兼容类(AppCompat)
UI绘制的三大流程回顾:
测量:确定大小
布局:确定位置
绘制:绘制视图
在进行绘制的时候会先进行测量,调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到了子元素中了,这样就完成了一次measure过程,接着子元素就会重复父容器的measure过程,如此反复就完成了整个View数的遍历。同理另外两个过程也是相似的,都有这样遍历的过程。有了对绘制流程的简单认识,那么那些步骤会造成性能瓶颈呢?
在进行布局编码的时候有一些基本准则就是减少View树层级,其次就是布局尽量宽而浅,避免窄而深。
ConstraintLayout就有一定的优势
其它优化的小细节
GPU的绘制过程,就像刷墙一样,一层层的进行,16ms刷一次,这样就会造成图层覆盖的现象,也就是无用的图层(用户看不见的区域)还被绘制在底层,造成不必要的浪费。
GPU过度绘制的常见情形
在手机的开发者选项里,有OverDraw检测工具,调试GPU过度绘制工具。颜色代表渲染的图层情况。
蓝色:没有过度绘制
淡绿:过度绘制两次
淡红:过度绘制三次
深红:过度绘制四次