本文主要介绍一些有效的性能优化方法,包括布局优化、绘制优化、内存泄露优化、响应速度优化、ListView优化、Bitmap优化、线程优化以及一些性能优化的建议,同时还介绍了对ANR日志的分析方法以及内存泄露分析工具MAT的使用。
布局优化的思想很简单,就是尽量减少布局文件的层级,这个道理是浅显的,布局中的层级少了,这就意味着Android绘制时的工作量少了,那么程序的性能自然就高了。
如何进行布局优化呢?首先删除布局中无用的控件和层级,其次有选择的使用性能较低的ViewGroup,比如RelativeLayout。如果布局既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多的CPU时间。FrameLayout和LinearLayout一样都是一种简单高效的ViewGroup,因此可能考虑使用它们,但是很多时候单纯通过一个LinearLayout或者FrameLayout无法实现产品效果,需要通过嵌套的方式来完成。这种情况下还是建议采用RelativeLayout,因为ViewGroup的嵌套就相当于增加了布局的层级,同样会降低程序的性能。
布局优化的另一种手段是采用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/view_toolbar" />
<FrameLayout
android:id="@+id/fragment_containers"
android:layout_width="match_parent"
android:layout_height="match_parent" />
LinearLayout>
通过这种方式,就不用把view_toolbar这个布局文件再重复写一遍了,这就是
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:id="@+id/bt_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Button1" />
<Button
android:id="@+id/bt_2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Button2" />
merge>
ViewStub继承了View,它非常轻量级且宽高都是0,因此它本身不参与任何的布局和绘制过程。ViewStub的意义在于按需加载所需的布局文件,在实际开发中,有很多布局文件在正常情况下不会显示,比如网络异常时的界面,这个时候就没有必要再整个界面初始化的时候将其加载进来,通过ViewStub就可以做到在使用的时候再加载,提高了程序初始化时的性能。
<ViewStub
android:id="@+id/stub_import"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:inflatedId="@+id/panel_import"
android:layout="@layout/layout_network_error" />
其中stub_import是ViewStub的id,而panel_import是layout/layout_network_error这个布局的根元素的id。如何做到按需加载呢?在需要加载ViewStub中的布局时,可以按照如下两种方式进行:
((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
或者
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();
当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,这个时候ViewStub就不再是整个布局结构中的一部分了。另外,目前ViewStub还不支持
绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方面。
首先,onDraw中不要创建新的局部对象,这是因为onDraw方法可能被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁的gc,降低了程序的执行效率。
另一方面,onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。按照Google官方给出的性能优化典范中的标准,View的绘制帧率保证60fps是最佳的,这就要求每一帧的绘制时间不超过16ms,虽然程序有时候很难保证16ms,但是尽量降低onDraw方法的复杂度总是切实有效的。
内存泄露在开发过程中是一个需要重视的问题,这个需要开发人员有一定的经验和意识,一个是避免写出有内存泄露的代码,另一方面是通过一些分析工具比如MAT来找出潜在的内存泄露继而解决。
public class MainActivity extends Activity{
private static Context sContent;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sContent = this;
}
}
Activity无法正常销毁,因此静态变量sContext引用了它。
public class MainActivity extends Activity{
private static View sView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sView = new View(this);
}
}
上面代码改造下,同理sView是一个静态变量,它内部持有了当前Activity,所以Activity仍然无法释放。
单例模式带来的内存泄露是我们容易忽视的,例如一个单例TestManager可以接收外部的注册监听并存储起来。
public class TestManager{
private List mListeners = new ArrayList<>();
private static class Single{
public static final TestManager sInstance = new TestManager();
}
private TestManager(){ }
public static TestManager getInstance(){
return Single.sInstance;
}
public synchronized void registerListener(OnChangeListener listener){
if(!mListeners.contains(listener)){
mListeners.add(listener);
}
}
public synchronized void unregisterListener(OnChangeListener listener){
mListeners.remove(listener);
}
public interface OnChangeListener{
void onChange();
}
接着再让Activity实现OnChangeListener接口,并向TestManager注册监听,若缺少反注册的操作,那么就会造成内存泄露,因为TestManager持有Activity的对象。
从Android3.0开始,Google提供了属性动画,属性动画中有一类无限循环的动画,如果在Activity中播放此类动画而且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看到动画效果了。这时,Activity中的View会被动画持有,而View又持有了Activity,最终Activity无法释放
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button)findViewById(R.id.button1);
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0, 360).setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.start();
//animator.cancel(); // 没有调用停止动画
}
响应速度优化的核心思想是避免在主线程中做耗时操作,采用异步的方式执行耗时操作。响应速度过慢更多地体现在Activity的启动速度上面,如果主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至ANR。Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BoradcastReceiver如果10秒钟之内还未执行完操作也会出现ANR。在实际开发中,ANR是很难从代码上发现的,如果开发过程中遇到ANR,那么怎么定位问题呢?其实当一个进程发生ANR了以后,系统会在/data/anr目录下创建一个traces.txt,通过分析这个文件就能定位出ANR的原因了。
adb pull /data/anr/traces.txt
建模中… 等待更新
建模中… 等待更新
MAT的全称是Eclipse Memory Analyzer,它是一款强大的内存泄露分析工具,MAT不需要安装,下载后解压即可使用,下载地址为http://www.eclipse.org/mat/downloads.php。对于Eclipse来说,MAT也有插件版,但是不建议使用插件版,因为独立版使用起来更加方便,即使不安装Eclipse也可以正常使用,当然前提是具有内存分析后的hprof文件。
为了采用MAT来分析内存泄露,下面模拟一种简单的内存泄露
public class TestActivity extends Activity{
private static Context sContext;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
sContext = this;
}
}
编译安装,然后打开DDMS界面,其中AndroidStudio的DDMS位于Monitor中。接着用鼠标选中要分析的进程,然后使用待分析应用的一些功能,这样做是为了将尽量多的内存泄露暴露出来,然后点击Dump HPROF file这个按钮, 等待一小段时间即可导出一个hprof后缀的文件,
导出的hprof文件并不能使用它来进行分析,因为它不能被MAT直接识别,需要通过hprof-conv命令转换一下。hprof-conv命令是Android SDK提供的工具,它位于Android SDK的platform-tools目录下:
hprof-conv 源文件 输出文件
如: hprof-conv com.ctao.music.debug.hprof com.ctao.music.debug.conv.hprof
经过上面的步骤,接下来就可以直接通过MAT来进行内存分析了,通过菜单打开刚才转换后的com.ctao.music.debug.conv.hprof
MAT提供了很多功能,但是最常用的只有Histogram和DominatorTree,通过Histogram可以直接地看出内存中不同类型的buffer的数量和占用的内存大小,而DominatorTree则把内存中的对象安装从大到小的顺序进行排序,并且分析对象之间的引用关系,内存泄露分析就是通过DominatorTree来完成的。
为了分析内存泄露,我们需要分析DominatorTree里面的信息,一般这里是不会直接显示出来的,这个时候需要按照从大到小的顺序去排查一遍。如上图,选中一个占用最大的,然后单击右键->Path To GC Roots->exclude wake/soft references[排除弱引用和软引用,因为二者都有较大几率被gc回收,它们并不造成内存泄露]
如此就直接定位到我们的内存泄露对象sContenxt。另外MAT还有很多其他功能,这里就不再一一介绍了。读者直接体验吧。
学习摘录于《Android开发艺术探索》第15章