布局优化
布局优化的思想很简单,就是尽量减少布局文件的层级。首先删除布局中无用的控件和层级,其次有选择地使用性能较低的ViewGroup,比如RelativeLayout。如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout。
布局优化的另外一种手段是采用<include>标签、<merge>标签和ViewStub。<include>标签主要用于布局重用,<merge>标签一般和<include>配合使用,它可以降低减少布局的层级,而ViewStub则提供了按需加载的功能,当需要时才会将ViewStub中的布局加载到内存,这提高了程序的初始化效率。
<include>标签只支持以android:layout_开头的属性,比如android:layout_width、android:layout_height,其他属性是不支持的,比如android:background。当然,android:id这个属性是个特例,如果<include>指定了这个id属性,同时被包含的布局文件的根元素也指定了id属性,那么以<include>指定的id属性为准。
<merge>标签一般和<include>标签一起使用从而减少布局的层级,例如当前布局的根布局是垂直的LinearLayout,而要<include>进来的布局的根布局也是垂直的LinearLayout,这个时候<include>的布局久可以使用<merge>标签。
ViewStub继承了View,它非常轻量级且宽/高都是0,因此它本身不参与任何的布局和绘制过程。ViewStub的意义在于按需加载所需的布局文件,在实际开发中,有很多布局文件在正常情况下不会显示,比如网络异常时的界面,这个时候就没有必要在整个界面初始化的时候将其加载进来,通过ViewStub就可以做到在使用的时候再加载,提高了程序初始化时的性能。下面是一个ViewStub的示例:
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/layout_network_error"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
其中stub_import是ViewStub的id,而panel_import是layout/layout_network_error这个布局的根元素的id。如何做到按需加载呢?在需要加载ViewStub中的布局时,可以按照如下两种方式进行:
findViewById(R.id.stub_import).setVisibility(View.VISIBLE);
//或者
View inflate = ((ViewStub) findViewById(R.id.stub_import)).inflate();
当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,这个时候ViewStub就不再是整个布局结构中的一部分了。另外,目前ViewStub还不支持<merge>标签。
绘制优化
绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方面。首先,onDraw中不要创建新的局部对象,这是因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象。另外一方面,onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。
内存泄露优化
内存泄露的优化分为两个方面,一方面是在开发过程中避免写出有内存泄露的代码,另一方面是通过一些分析工具比如MAT来找出潜在的内存泄露继而解决。
-
场景1:静态变量导致的内存泄露
一个简单的例子就算Activity里面的静态变量持有当前Activity对象,导致Activity无法释放。
-
场景2:单例模式导致的内存泄露
单例模式所带来的内存泄露是我们容易忽视的,如下所示。首先提供一个单例模式的TestManager, TestManager可以接收外部的注册并将外部的监听器存储起来。
public class TestManager {
private List<OnDataArrivedListener> listeners = new ArrayList<>();
private TestManager() {
}
//使用内部静态类的方式构造单例模式
private static class SingletonHolder {
public static final TestManager INSTANCE = new TestManager();
}
public static TestManager getInstance() {
return SingletonHolder.INSTANCE;
}
public synchronized void registerListener(OnDataArrivedListener listener) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
public synchronized void unRegisterListener(OnDataArrivedListener listener) {
listeners.remove(listener);
}
public interface OnDataArrivedListener {
void onDataArrived(Object data);
}
}
接着再让Activity实现OnDataArrivedListener接口并向TestManager注册监听,如下所示。下面的代码由于缺少解注册的操作所以会引起内存泄露,泄露的原因是Activity的对象被单例模式的TestManager所持有,而单例模式的特点是其生命周期和Application保持一致,因此Activity对象无法被及时释放。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestManager.getInstance().registerListener(this);
}
-
场景3:属性动画导致的内存泄露
属性动画中有一类无限循环的动画,如果在Activity中播放此类动画且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看到动画效果了,并且这个时候Activity的View会被动画持有,而View又持有了Activity,最终Activity无法释放。解决方法是在Activity的onDestroy中调用animator.cancel()来停止动画。
ANR日志分析
Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BroadcastReceiver如果10秒钟之内还未执行完操作也会出现ANR。在实际开发中,ANR是很难从代码上发现的,如果在开发过程中遇到了ANR,那么怎么定位问题呢?其实当一个进程发生ANR了以后,系统会在/data/anr目录下创建一个文件traces.txt,通过分析这个文件就能定位出ANR的原因。
下面的代码也会导致ANR,原因是这样的,在Activity的onCreate中开启了一个线程,在线程中执行testANR(),而testANR()和initView()都被加了同一个锁,为了百分之百让testANR()先获得锁,特意在执行initView()之前让主线程休眠了10ms,这样一来initView()肯定会因为等待testANR()所持有的锁而被同步住,这样就产生了一个稍微复杂些的ANR。这个ANR是很参考意义的,这样的代码很容易在实际开发中出现,尤其是当调用关系比较复杂时,这个时候分析ANR日志就显得异常重要了。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
testANR();
}
}).start();
SystemClock.sleep(10);
initView();
}
private synchronized void initView() {
}
private synchronized void testANR() {
SystemClock.sleep(30 * 1000);
}
为了分析问题,需要从traces文件着手,导出traces文件。在studio的Terminal依次输入 cd data/anr 跳转到anr目录 再输入 ls 查看所有文件 然后 Ctrl+D 先退出 接着输入 adb pull /data/anr/traces.txt E:/work/ 将文件pull到电脑E盘的work文件夹下
adb pull /data/anr/traces.txt E:/work/
注意,导出traces.txt文件,手机要有root权限,可用模拟器测试。 traces文件一般是非常长的,下面是traces文件的部分内容:
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 obj=0x75374000 self=0xb1e8b400
| sysTid=3978 nice=0 cgrp=default sched=0/0 handle=0xb6198534
| state=S schedstat=( 0 0 0 ) utm=3 stm=9 core=0 HZ=100
| stack=0xbf446000-0xbf448000 stackSize=8MB
| held mutexes=
at com.zzr.crashhandlerdemo.MainActivity.initView(MainActivity.java:-1)
- waiting to lock <0x0e8a93c1> (a com.zzr.crashhandlerdemo.MainActivity) held by thread 12
at com.zzr.crashhandlerdemo.MainActivity.onCreate(MainActivity.java:34)
at android.app.Activity.performCreate(Activity.java:6662)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2599)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2707)
at android.app.ActivityThread.-wrap12(ActivityThread.java:-1)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1460)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
"Thread-2" prio=5 tid=12 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x12ce8550 self=0xa6531e00
| sysTid=4002 nice=0 cgrp=default sched=0/0 handle=0x9aef2920
| state=S schedstat=( 0 0 0 ) utm=0 stm=0 core=1 HZ=100
| stack=0x9adf0000-0x9adf2000 stackSize=1038KB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x019f254c> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:371)
- locked <0x019f254c> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:313)
at android.os.SystemClock.sleep(SystemClock.java:120)
at com.zzr.crashhandlerdemo.MainActivity.testANR(MainActivity.java:42)
- locked <0x0e8a93c1> (a com.zzr.crashhandlerdemo.MainActivity)
at com.zzr.crashhandlerdemo.MainActivity.access$000(MainActivity.java:15)
at com.zzr.crashhandlerdemo.MainActivity$1.run(MainActivity.java:30)
at java.lang.Thread.run(Thread.java:761)
上面的情况稍微复杂一些,需要逐步分析。首先看主线程,如下所示。可以看得出主线程在initView方法中正在等待一个锁<0x0e8a93c1>,这个锁的类型是一个MainActivity对象,并且这个锁已经被线程id为12(即tid=12)的线程持有了,因此需要再看一下线程12的情况。
at com.zzr.crashhandlerdemo.MainActivity.initView(MainActivity.java:-1)
- waiting to lock <0x0e8a93c1> (a com.zzr.crashhandlerdemo.MainActivity) held by thread 12
"Thread-2" prio=5 tid=12 Sleeping
at com.zzr.crashhandlerdemo.MainActivity.testANR(MainActivity.java:42)
- locked <0x0e8a93c1> (a com.zzr.crashhandlerdemo.MainActivity)
tid是12的线程就是“Thread-2”,就是它持有了主线程所需的锁,可以看出“Thread-2”正在sleep, sleep的原因是MainActivity的42行,即testANR方法。这个时候可以发现testANR方法和主线程的initView方法都加了synchronized关键字,表明它们在竞争同一个锁,即当前Activity的对象锁,这样一来ANR的原因就明确了,接着就可以修改代码了。