目录
一、介绍
1.1、冷启动和热启动
1.2、启动速度测量
1.2.1、TraceCompat
二、优化方式
2.1、MultiDex启动优化
2、 子进程中执行dexOpt
2.2、启动窗口优化(设置主题)
2.3、Allpication,ContentProvider优化
2.4、Activity优化
2.4.1、Activity View的加载流程
2.4.2、懒加载IdleHandler
2.4.3、异步加载布局AsyncLayoutInflater
2.5、Fragement优化
2.6、布局优化
2.6.1、诊断过度绘制,优化过度绘制
2.6.2、合理使用控件Constaintlayout
ViewStub
2.6.3、去掉window的默认背景
3.6.4、去掉其他不必要的背景有时候为了方便会先给Layout设置一个整体的背景,再给子View设置背景,这里也会造成重叠,如果子View宽度mach_parent,可以看到完全覆盖了Layout的一部分,这里就可以通过分别设置背景来减少重绘。再比如如果采用的是selector的背景,将normal状态的color设置为“@android:color/transparent",也同样可以解决问题。这里只简单举两个例子,我们在开发过程中的一些习惯性思维定式会带来不经意的Overdraw,所以开发过程中我们为某个View或者ViewGroup设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子View上,而不是图方便直接设置在根View上。
3.6.5、ClipRect
返回的结果,就是标准的应用程序的启动时间
应用进程不存在的情况下,从点击桌面应用图标,到应用启动(冷启动),会经历以下流程:
目前 Android 5.0 以上的设备已经自身支持了 MultiDex 功能,也就是说在安装 apk 的时候,系统已经会帮我们把 apk 里面的所有 dex 文件都做好 Optimize 处理,所以不需要我们在代码里启用 MultiDex 了。但是对于 Android 5.0 以下的设备,依然要求我们启用 MultiDex。而这些系统的设备在第一次运行 App 的时候,需要对所有的 Secondary Dexes 文件都进行一次解压以及 Optimize 处理(生成 odex 文件),这段时间会有明显的耗时,所有会产生明显的卡顿现象。
配置:
android {
defaultConfig {
...
multiDexEnabled true // Enable MultiDex.
//定义main dex中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
...
}
...
}
dependencies {
implementation 'androidx.multidex:multidex:2.0.0'
}
1、在Application的attachBaseContext启动新进程执行dexOpt
protected void attachBaseContext(Context base) {
// 只有5.0以下需要执行 MultiDex.install
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
MULTI_DEX = MULTI_DEX + "_" + getVersionCode(base);
if (SystemUtil.isInMainProcess(base)) {
// 判断有没有执行过dexOpt
if (!dexOptDone(base)) {
preLoadDex(base);
}
}
if (!KwaiApp.isMultiDeXProcess(base)) {
MultiDex.install(base);
}
}
super.attachBaseContext(base);
}
/**
* 是否进行过DexOpt操作。
*
* @param context
* @return
*/
private boolean dexOptDone(Context context) {
SharedPreferences sp = context.getSharedPreferences(MULTI_DEX, MODE_MULTI_PROCESS);
return sp.getBoolean(MULTI_DEX, false);
}
/**
* 在单独进程中提前进行DexOpt的优化操作;主进程进入等待状态。
*
* @param base
*/
public void preLoadDex(Context base) {
// 在新进程中启动PreLoadDexActivity
Intent intent = new Intent(base, PreLoadDexActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
base.startActivity(intent);
while (!dexOptDone(base)) {
try {
// 主线程开始等待;直到优化进程完成了DexOpt操作。
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class PreLoadDexActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
// 取消掉系统默认的动画。
overridePendingTransition(0, 0);
setContentView(R.layout.tv_splash_layout);
new Thread() {
@Override
public void run() {
try {
// 在子线程中调用
MultiDex.install(getApplication());
SharedPreferences sp = getSharedPreferences(App.MULTI_DEX, MODE_MULTI_PROCESS);
sp.edit().putBoolean(App.MULTI_DEX, true).commit();
finish();
} catch (Exception e) {
finish();
}
}
}.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
System.exit(0);
}
}
在等待第一帧显示的时间里,可以加入一些配置以增加体验,比如加入Activity的background,这个背景会在显示第一帧前提前显示在界面上。
1、android:theme配置
2.、在MainActivity中加载布局前把AppTheme重新设置给MainActivity:
(1)设置背景图Theme
通过设置一张背景图。 当程序启动时,首先显示这张背景图,避免出现黑屏
(2)设置透明Theme
通过把样式设置为透明,程序启动后不会黑屏而是整个透明了,等到界面初始化完才一次性显示出来
两者对比:
Theme1 程序启动快,界面先显示背景图,然后再刷新其他界面控件。给人刷新不同步感觉。
Theme2 给人程序启动慢感觉,界面一次性刷出来,刷新同步。
Application生命周期中也不要启动其他的组件如:service、contentProvider、
BroadcastReceiver.
StartUp:https://www.pianshen.com/article/10031906037/
StartUp 通过ContentProvider是Android四大组件之一,这个组件相对来说是比较重量级的。也就是说,本来我的初始化操作可能是一个非常轻量级的操作,依赖于ContentProvider之后就变成了一个重量级的操作了代码中定义了一个LibraryA
类并且实现了Initializer< Dependency
>接口。这个时候我们看到dependencies()方法返回的就不是空列表了,而是包含了LibraryA
的一个列表,这样LibraryA
要想初始化,必须先初始化Dependency
。
假设你的应用程序依赖了LibraryA
,并且需要在程序一开始启动时就初始化LibraryA
,定义一个LibraryA
类并且实现Initializer
接口:
代码中定义了一个LibraryA
类并且实现了Initializer< Dependency
>接口。这个时候我们看到dependencies()方法返回的就不是空列表了,而是包含了LibraryA
的一个列表,这样LibraryA
要想初始化,必须先初始化Dependency
。
自动启动:
tools:node="merge"
属性是为了确保清单合并工具可能造成的冲突问题
手动:
禁用自动初始化后,你可以使用AppInitializer
手动初始化组件和它的依赖。还是看下官方代码:
Activity是 用户操作的可视化界面;它为用户提供了一个放置视图和交互操作的窗口。采用setContentView的方法提供。因此,可以理解Activity、Window、View三者关系为。Activity提供Window ,View被添加到Window中。
1、Activity在被创建之初,调用了attach方法,这个时候,为Activity创建了一个PhoneWindow, 并且为PhoneWindow设置了事件交互的回调。
2、紧接着Activity的onCreate()方法被回掉。这里也就到了我们经常复写方法,我们在OnCreate()之中,调用setContentView(id)。
3、在setContentView(), PhoneWindow 创建了一个顶级视图 DecorView (FrameLayout)的子类。
4、紧接着,DecorView会依据一些feature(类似NO_ACTICON_BAR)来,添加一个layout。这个Layout中包含了Title、content。其中content也是FrameLayout,也就是我们在setContentView(id),将视图添加的父容器。
在应用启动的时候,为了加快启动速度,往往需要把一些比较重的操作放到子线程中,或者是延时加载。将任务放在子线程中是一个比较简单并且看起来有效的操作,但是呢,也不能太过于依赖子线程,它虽然不会阻塞主线程,但是却会跟主线程抢占CPU,当子线程很多并且任务很重的时候,也还是会拖慢主线程的。
以前一直在想Android为什么不在Activity或者Fragment中提供一个接口,让我们可以在主线程空闲的时候去执行一些操作,后来发现真的有,但这个接口不是在Activity和Fragment中,而是在MessageQueue中,在MessageQuque
public staticinterface IdleHandler{
boolean
queueIdle();
}
简单来说,就是当MessageQueue中没有更多的消息的时候就会回调queueIdle()
这个方法,返回true的话,当MessageQueue中没有消息的时候还会继续回调这个方法,返回false则会在执行完之后移除掉这个监听。
原理就是这么简单了,接下来就是动手优化代码了,代码也很简单。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ...
//
拿到主线程的MessageQueue
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
//
在这里去处理你想延时加载的东西
delayLoad();
//
最后返回false,后续不用再监听了。
return false;
}
});
}
new AsyncLayoutInflater(this).inflate(R.layout.launcher,null, new AsyncLayoutInflater.OnInflateFinishedListener(){
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
setContentView(view);
}
});
https://www.jianshu.com/p/2201a107d5b5?utm_campaign=hugo
在没有添加懒加载之前,只要使用 add+show+hide 的方式控制并显示 Fragment, 那么不管 Fragment 是否嵌套,在初始化后,如果只调用了add+show,同级下的 Fragment 的相关生命周期函数都会被调用。且调用的生命周期函数如下所示:
onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume
Fragment 完整生命周期:onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
什么是同级 Frament 呢?看下图
上图中,都是使用 add+show+hide 的方式控制 Fragment,
在上图两种模式中:
观察上图我们可以发现,同级的Fragment_1、Fragment_2、Fragment_3 都调用了 onAttach...onResume 系列方法,也就是说,如果我们没有对 Fragment 进行懒加载处理,那么我们就会无缘无故的加载一些并不可见的 Fragment , 也就会造成用户流量的无故消耗(我们会在 Fragment 相关生命周期函数中,请求网络或其他数据操作)。
在Fragment变为可见时都会执行onResume()方法,我们可以利用这一点来实现懒加载,基本思路有两点:
abstract class LazyFragment : Fragment() {
/**
* 是否执行懒加载
*/
private var isLoaded = false
/**
* 当前Fragment是否对用户可见
*/
private var isVisibleToUser = false
/**
* 当使用ViewPager+Fragment形式会调用该方法时,setUserVisibleHint会优先Fragment生命周期函数调用,
* 所以这个时候就,会导致在setUserVisibleHint方法执行时就执行了懒加载,
* 而不是在onResume方法实际调用的时候执行懒加载。所以需要这个变量
*/
private var isCallResume = false
/**
* 是否调用了setUserVisibleHint方法。处理show+add+hide模式下,默认可见 Fragment 不调用
* onHiddenChanged 方法,进而不执行懒加载方法的问题。
*/
private var isCallUserVisibleHint = false
override fun onResume() {
super.onResume()
isCallResume = true
if (!isCallUserVisibleHint) isVisibleToUser = !isHidden
judgeLazyInit()
}
private fun judgeLazyInit() {
if (!isLoaded && isVisibleToUser && isCallResume) {
lazyInit()
Log.d(TAG, "lazyInit:!!!!!!!”)
isLoaded = true
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
isVisibleToUser = !hidden
judgeLazyInit()
}
override fun onDestroyView() {
super.onDestroyView()
isLoaded = false
isVisibleToUser = false
isCallUserVisibleHint = false
isCallResume = false
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
this.isVisibleToUser = isVisibleToUser
isCallUserVisibleHint = true
judgeLazyInit()
}
abstract fun lazyInit()
}
过度绘制:屏幕上某一像素点在一帧中被重复绘制多次
分类(根据层度):
无过度绘制(一个像素只被绘制了一次) (原色)
过度绘制x1(一个像素被绘制了两次) (蓝色)
过度绘制x2(一个像素被绘制了三次) (绿色)
过度绘制x3(一个像素被绘制了四次) (粉色)
过度绘制x4+(一个像素被绘制了五次以上) (红色)
Android调试机中的开发者选项中开启【调试GPU过度绘制】
https://developer.android.google.cn/reference/android/support/constraint/ConstraintLayout
用Constaintlayout的话最多就两个层级了,不像Relative和Linear一样一层嵌套一层的。
1.尽量多使用 ConstraintLayout、RelativeLayout、LinearLayout
2.尽量使用 ConstraintLayout
3.在布局的层级相同的情况下,使用 LinearLayout 代替 RelativeLayout
4.在布局复杂或者层级过深的时候,使用 RelativeLayout 代替 LinearLayout 使界面层级扁平化,降低层级
使用include 和merge标签减少复用布局而产生的布局嵌套,使用ViewStub懒加载减少渲染元素
优点:
它是一个轻量级的View,是一个看不见的,不占布局位置,占用资源非常小的控件。我们可以在ViewStub下指定要加载的布局并指定布局id,当我们需要该布局显示的时候,只需要调用ViewStub的inflate()即可。或者setVisibility();缺点:
ViewStub的inflate()只能调用一次,多次调用会有异常抛出。也就是说我们只能对ViewStub加载的布局控制一次1.将可重复使用的布局通过include标签与merge标签搭配进行使用
2.尽量少添加不必要的背景,减少过度绘制
3.布局能扁平化的扁平化,尽量不要增加布局层级
4.适当的时侯某些控件使用懒加载ViewStub
5.使用Hierarchy View 工具或者Lint工具来进行app的检测
当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。去掉window的背景可以在onCreate()中setContentView()之后调用
getWindow().setBackgroundDrawable(null);
或者在theme中添加
android:windowbackground="null";
为了解决Overdraw的问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少消耗。但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。