过度绘制是指屏幕上某个像素在同一帧的时间内被绘制多次,这回造成CPU及GPU资源的浪费。我们可以通过以下操作来开启检测过度绘制:开发者选项->调试GPU过度绘制->显示过度绘制区域。开启此功能后,屏幕上会出现一些带色块的区域,在优化界面时,我们应该尽量避免出现粉色或红色,这些色块的意义如下图所示:
那么,界面优化可以优化哪些方面呢?
当布局中的背景不是必要的时候应当进行移除,如通常情况下我们使用的Theme都会包含一个windowBackground,此时若我们在自己的布局中再给布局添加一个背景时就会造成一次过度绘制。这里,我们就可以把Theme的windowBackground给移除掉,我们可以通过以下两种方法中的一种进行移除:
(1)在Theme中设置
(2)在Activity的onCreate()方法中添加:
getWindow().setBackgroundDrawable(null);
过多的层级嵌套会导致过度绘制,从而降低性能,因此我们需要将布局的层次结构尽量扁平化。
在开发过程中,我们可以通过Layout Inspector去查看layout的层次结构。在AS中点击Tools>Android>Layout Inspector。然后在出现的Choose Process对话框中选择想要检查的应用进程即可。Layout Inspector会自动捕获快照,然后显示以下内容:
此外,我们还可以使用lint来协助优化布局性能。点击Analyse>Inspect Code即可。布局性能方面的信息位于Android>Lint>Performance下。
针对布局的层级,有如下优化方案。
include标签可以提高布局的复用性,单独的使用include标签不能起到减少布局层级的效果,但是include与merge配合使用是可以达到减少层级的效果的。当父布局与include的父布局相同时,include布局的父布局可用merge标签达到减少层级的效果。另外,如果一个布局的根布局为FrameLayout,那么此时也可以用merge标签代替,因为DecorView本身就是一个FrameLayout。
在项目中,有些比较复杂的布局在开始时不需要用到,此时我们可以通过ViewStub标签来实现在需要时再加载布局。使用ViewStub标签可以减少内存使用并加快渲染速度。如下是一个ViewStub标签的例子:
我们可以通过如下代码在需要时加载ViewStub相应的布局:
findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
//或者
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
需要注意的是,ViewStub标签不能与merge标签一起使用。
当能使用LinearLayout或FrameLayout的就不要使用RelativeLayout,因为RelativeLayout会测量每个子节点两次;但是如果使用RelativeLayout能减少层级嵌套的话,那么还是推荐使用RelativeLayout;ConstraintLayout的性能比RelativeLayout更好,推荐使用ConstraintLayout。
在onDraw()方法可能会被频繁调用,若在此方法中创建局部变量可能会产生大量的临时对象,造成频繁GC,导致内存抖动;
Android系统每隔16ms发送VSYNC信号触发UI渲染,在onDraw()中执行耗时操作可能导致掉帧或者卡顿。
内存泄露是指程序中不再使用的对象无法被GC识别正常回收,导致内存资源无法被释放;随着内存泄露的累积,可用的空闲内存空间会越来越少,GC会更容易被触发,GC进行时会停止其他线程的工作,可能导致界面卡顿等情况,当内存空间不够分配时就会发生内存溢出(OOM)。
在开发过程中,我们经常会碰到以下几种内存泄露的情况:
单例与静态变量的生命周期跟整个程序的生命周期一致。只要单例或静态变量没有被销毁或者置空,其对象就一直被保存引用,也就不会被回收,从而导致内存泄漏。
针对这种情况,我们有如下几种解决方案:
非静态内部类和匿名类默认持有外部类的引用,当非静态内部类或匿名类的生命周期比外部类的生命周期长时,就会发生内存泄露。以下是几种常见的此类内存泄露的情况:
通常我们都是使用内部类的形式来实现Handler的,此时编译器会有黄色警告提示这个对象可能造成内存泄露。Handler的内存泄露的根本原因在于:
此时引用关系链为Looper->MessageQueue->Message->Handler->Activity。因此,若此时退出Activity,由于上述引用关系,Activity无法被回收,从而造成内存泄露。
针对这种情况,解决方法为
private static class MyHalder extends Handler {
private WeakReference mWeakReference;
public MyHalder(Activity activity) {
mWeakReference = new WeakReference(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//...
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
如我们在使用匿名内部类启动一个线程时,匿名类会持有外部类的引用。当Activity销毁后,线程内的操作可能还在后台执行,此时就会发生内存泄漏。针对这种情况,解决方法为:
其他非静态内部类与匿名内部类的情况,都可以参照以上解决方法进行处理:一个是改成静态内部类,另一个就是保证内部类的生命周期不超过外部类。
集合添加元素后将会持有元素对象的引用,导致元素对象不能被GC回收,从而发生内存泄漏。解决方案为清空集合对象。代码如下所示:
list.clear();
list = null;
总之,此类情况造成的内存泄露要切记有开就有关。
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
//可选项,如果使用了support包中的fragments
debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
}
}
冷启动是指App在手机启动后第一次运行或者App进程被kill后再次启动。冷启动的必要条件是App进程不存在,这就意味着系统需要创建进程,App要初始化。
在冷启动的过程中,系统会完成三个任务:1、加载并启动应用程序;2、启动后立即显示应用程序的空白启动窗口;3、创建应用程序进程。当系统为我们创建了进程后,开始创建应用程序对象:1、启动主线程;2、创建Activity;3、加载布局;4、屏幕布局;5、执行初始绘制。应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用户可以开始使用该程序。至此,启动完成。
App进程存在,Activity可能因为内存不足而被回收。这时候启动App不需要重新创建进程,但Activity的onCreate()方法还是需要重新执行。
App进程存在,并且Activity对象任然存在内存中没有被回收。可以避免重复对象初始化,布局的渲染和加载。场景类似于你从当前App跳到另一个App一小段时间后重新切换回当前App,此时就属于热启动。
通常,启动慢的定义为:
无论是何种启动,我们的优化点都是:Application、Activity的创建及回调过程。
谷歌官方给出的建议是:
使用Activity的WindowBackground主题属性来为启动的Activity提供一个简单的drawable,这样在启动的时候,会先展示一个界面,等Activity加载完后,再去加载Activity的界面,从而产生一种快的感觉,但是这种方法治标不治本,具体的做法如下所示:
//定义一个style
//将启动Activity的theme设置为style
Application是程序的主入口,很多第三方SDK的初始化都需要在Application的onCreate()方法中执行,这样就增加了程序在初始化时的工作量,启动自然也就变慢了。针对这个问题,可以参考如下方法:
apk包的大小往往是影响用户是否下载的一个因素之一,因此优化安装包的大小也是非常有必要的。要优化apk包的大小,我们要先了解apk文件的构成,apk解压完成后包含以下文件:
apk大小的优化需要从代码和资源两个方面进行,具体的优化方案有:
参考资料: