布局优化
优化的目的是减少层级,让布局扁平化,以降低绘制的时间,提高布局的复用性节省开发和维护成本
减少层级
RelativeLayout会对子View做两次测量,在RelativeLayout中子View的排序方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中View的顺序不一样,在确定子View的位置时,需要先给所有子View做一次排序。
LinearLayout,如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高,在布局上Re不如Li快。
但是如果层级太深,还是用Re来减少布局层次,布局层次深会增加内存消耗,甚至引起栈溢出问题,总结就是能用层级相同就用Li,如果用两层Li才行的那就用一层的Re。
merge的使用
场合1:在自定义View中使用,父元素尽量是FrameLayout或者LinearLayout
场合2:在Activity中整体布局,根元素需要是FrameLayout
原理是在Android布局的源码中,如果是 merge标签,那么直接将其中的元素添加到merge标签Parent中,这样就保证了不会引入额外的层级。
使用merge的要求
- merge只能用在布局xml文件的根元素
- 使用Merge加载一个布局时,必须指定一个ViewGroup作为父元素,并且要设置加载的attchToRoot参数为true(参照inflate(int,viewGroup,boolean))
- 不能在ViewStub中使用,因为ViewStub的inflate中没有attachToRoot的设置
在Ac总布局中用merge,但又想设置整体的属性话可以不用setContentView,用id/content把FrameLayout取出来,在代码中手动加载布局,如果层级压力不大(<10级)就没有必要这样干了。
提高显示速度
当布局中的子布局多的时候,有时候不需要所有元素都展示出来,一般的思路是通过visable属性来控制元素的隐藏与显示,但是这样效率低,原因是仍然在布局中,系统会解析。
可以考虑用ViewStub,这是个轻量级的View,不占用布局位置,占用资源非常小的视图对象,加载布局的时候只有ViewStub会被初始化,当ViewStub设置为可见时,或者调用了ViewStub.inflate()或者ViewStub.setVisibility时,ViewStub所指向的布局才会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。
注意:
- 只能加载一次,之后会被置空,所以不试用于按需求显示隐藏的情况
- ViewStub只能用来加载一个布局文件,而不是具体的一个View,要操作View还是得用visibility
- ViewStub中不能嵌套merge标签
- 在程序运行期间,被加载后就不会有变化,除非销毁该页面重新加载
布局复用include
都懂,不多说
对布局优化的总结
⭐️布局的层级越少,加载速度越快
⭐️减少同一层级控件的数量,加载速度会变快
⭐️一个控件的属性越少,解析越快,删除控件中的无用属性
⭐️可复用的组件抽取后用<include />
⭐️使用<ViewStub />加载不常用布局
⭐️使用merge减少布局的层次
⭐️尽可能少用wrap_content,这个会增加布局measure时的计算成本,已知宽高为固定值的时候,不用wrap_content
避免过度绘制
过度绘制(Overdraw) 是指在屏幕上的某个像素在同一帧的时间内被绘制了多次,比如多层次的UI结构中,如果不可见的UI也在做绘制的操作,就会导致某些像素区被绘制了多次,从而浪费CPU,GPU资源
主要原因:
- xml布局的控件有重叠且都设置了背景
- View的自绘onDraw里有同一个区域被绘制多次
过度重绘排查
在开发者选项中打开 显示GPU过度重绘开关,然后打开app根据界面颜色判断重绘的严重程度
严重程度排序:无色 蓝色 绿色 淡红 深红
一般来说淡红不超过屏幕1/4可以接受,深红的会严重影响性能,需要优化
如何避免
1.布局上的优化
- 移除xml非必须的背景
- 移除window默认的背景
- 按需显示占位的背景图
当自定义布局有一个全屏的背景时,DecorView的背景就没用了,但是会产生一次Overdraw,可以移除
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
this.getWindow().setBackgroundDrawable(null);
}
2.自定义View的优化
虽然自定义View能减少Layout的层级,但是实际绘制的时候也是会过度重绘的,因为对于有些过度复杂的自定义View,系统无法检测onDraw中执行了啥操作,无法监控并自动优化。
在自定义View中可以通过canvas.clipRect()来帮助系统识别可见的区域,这个方法可以指定一块矩形区域,在区域内才会被绘制,其他区域会被忽视,可以很好的帮助有多组重叠组件的View控制显示区域,再区域外的绘制指令都不会执行,节约了CPU与GPU的资源,但是部分内容在区域内的组件仍然会得到绘制,可以用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过非举行区域内的绘制操作。
启动优化
说一下Application,启动Application的时候,系统会创建一个进程ID叫PID,所有的ac都会在这个进程运行,在Application创建时初始化全局变量,同一个应用的所有ac都能取到变量值,因为application是全局单例且生命周期最长的,所以在不同的ac或service中获取的都是同一个对象,因此在安卓中要避免用静态变量来存储长久保存的值,可以用Application,但是不建议用太多的全局变量
应用启动流程
- 冷启动:系统创建一个新的进程,所以回初始化Application,然后经过一系列的测量布局绘制把ac显示在街面上
- 热启动:从已有的进程启动,不再初始化application,只包含ac的生命周期
启动耗时检测
1.通过adb shell am
adb shell am start -W [packageName]/[packageName.AppStartActivity]
可以得到三个时间
- ThisTime 一般跟TotalTime一样,如果启动的时候开了个透明的ac处理点事,再显示主页面会比TotalTime小点
- TotalTime 创建进程+Application初始化+ac初始化到界面显示的时间
- WaitTime 比TotalTime大点,包括了系统影响的时间
2.通过代码打点
上代码
public class TimeMonitor {
private String TAG = "TimeMonitor";
private int monitorId = -1;
//保存一个耗时统计模块的各种耗时,tag对应某一阶段的时间
private HashMap<String, Long> timeTag = new HashMap<>();
private long startTime = 0;
public TimeMonitor(int id) {
Log.d(TAG, "TimeMonitor: id=" + id);
monitorId = id;
}
public int getMonitorId() {
return monitorId;
}
public void startMonitor() {
// 启动前清除以前数据,避免统计错误
if (timeTag.size() > 0) {
timeTag.clear();
}
startTime = System.currentTimeMillis();
}
//打一次点
public void recodingTimeTag(String tag) {
// 检查是否保存过相同的tag
if (timeTag.get(tag) != null) {
timeTag.remove(tag);
}
long time = System.currentTimeMillis() - startTime;
Log.d(TAG, "recodingTimeTag: time :" + time);
timeTag.put(tag, time);
}
public void end(String tag, boolean writeLog) {
recodingTimeTag(tag);
end(writeLog);
}
public void end(boolean writeLog) {
if (writeLog) {
// 写入到本地文件
}
testShowData();
}
public void testShowData() {
if (timeTag.size() <= 0) {
Log.e(TAG, "testShowData: timeTag is empty!!");
return;
}
for (String tag : timeTag.keySet()) {
Log.d(TAG, "testShowData: tag=" + timeTag.get(tag));
}
}
public HashMap<String, Long> getTimeTag() {
return timeTag;
}
}
上边代码不仅可以用在应用启动的耗时统计,还能统计其他模块,比如ac和Fragment的启动耗时
流程:创建这个对象时,传入一个自定义且唯一的统计模块或生命周期流程ID,一个TimeMonitor对应一个ID。end表示这个监控的流程结束,可以统计一系列数据上传到服务器,用来监控外网的实际启动情况。
下边简单封装一下
public class TimeMonitorManager {
private static TimeMonitorManager timeMonitorManager = null;
private static Context context = null;
private HashMap<Integer, TimeMonitor> timeMonitorList = null;
public synchronized static TimeMonitorManager getInstance() {
if (timeMonitorManager == null) {
timeMonitorManager = new TimeMonitorManager();
}
return timeMonitorManager;
}
public TimeMonitorManager() {
timeMonitorList = new HashMap<>();
}
//初始化某个打点模块
public void resetTimeMonitor(int id) {
if (timeMonitorList.get(id) != null) {
timeMonitorList.remove(id);
}
getTimeMonitor(id);
}
//获取打点器
public TimeMonitor getTimeMonitor(int id) {
TimeMonitor timeMonitor = timeMonitorList.get(id);
if (timeMonitor == null) {
timeMonitor = new TimeMonitor(id);
timeMonitorList.put(id, timeMonitor);
}
return timeMonitor;
}
}
一般打点的地方用在生命周期节点,比如Application的onCreate或者ac,Fragment的onCreate,onResume等
还有就是数据库初始化,读取本地数据这些。
启动优化方案
总体上看,启动主要完成了三件事
UI布局,绘制和数据准备
从UI布局和启动加载逻辑两个方向优化,达到降低启动耗时的目的。
1.UI布局的启动过程:启动应用 -> Application初始化 -> AppStartActivity -> HomePageActivity 在AppStartActivity中做广告或品牌展示的时候进行底层模块的初始化或者数据的预拉取等,同时减少ac的层级避免过度绘制
2.启动加载逻辑优化
启动点的四个维度
- 必要且耗时:启动初始化,用线程来初始化
- 必要不耗时:首页初始化
- 不必要耗时:数据上报、插件初始化
- 不必要不耗时:用的时候再加载
按需实现加载逻辑,采取分布加载、异步加载、延期加载策略