随笔

Android应用性能问题,只有当应用到达一定的规模之后才能显现出来,而且规模越大越是平时不注重的问题就会表现出来。当然了实际上大多都是老生常谈的东西,只是在最开始懒得遵守或觉得实现起来麻烦懒得去做。这也是一个长久的话题,在此简单回顾一下,可能没有什么条理性,请包涵。

一、 内存控制

关于内存的控制在java的各个方向上实际上都有提及。桌面领域遇到过一些(做过一段swing/awt),服务器端遵循一定的习惯也不会出什么大问题,当然了如果是大数据那就是另一回事了。Android方面应用的内存问题会随着应用在前台的运行时间慢慢暴露出来,对于一般的应用来说即便暴露出来也就是被系统杀死只要不是严重影响功能和使用体验,一般用户也懒得管。

但是如果做系统级应用,比如启动器那就比较严重。毕竟一直都在内存中运行,任何一次崩溃使用者都明确的感受的到,进而对整个系统做出不好的评价,甚至连累硬件部门。通过一段时间的测试发现了一些小问题。

1. Bitmap

BitmapImageView一个是图像数据一个是展示容器,同时ImageView和界面Activity(说的直白一点就是Activity的Context)相关联。而这一切的开始就是Context创建,Activity展示到前台。这其中最直白的感觉就是Context在界面元素中全部传了个遍,也就是每人持有一份,这点之后说明。

读取

Bitmap的来源问题在下一节性能中说明。Bitmap所有的Drawable对象在此都当做Bitmap进行处理,不要直接使用View#setBackgroundResource(res:int)等相关方法,不要直接把任何R.drawable.*直接设置在可现实元素上,当然如果你明确知道这个drawable在可控范围内,或者是极小的9png资源,那么也可以偷懒一下。

本人感觉最好的方法是显示图片的容器全部封装起来,叫xxItem,xxView都无所谓,然后对图片进行统一处理。本地资源直接转换成Bitmap,根据系统运行状态,硬件环境等在可接受范围内进行压缩或降低图片质量的处理。当然网络过来的图片一般经过某些异步处理框架之后,根据其自己的设置对图片大小和质量已经进行了一定的处理。此时注意使用NinePatch#isNinePatchChunk(bitmap:byte[])9png进行额外处理,这样至少在图片显示的时候不会因为内存不足被系统杀死

回收

使用手动回收,我实现的应用里面有大量图片展示功能,这里对于Bitmap的手动回收是必不可少的,实际效果见下图。

在界面销毁之后立刻手动Bitmap#recycle()并置空当前元素对Bitmap的引用,当下一次请求内存分配时供虚拟机进行回收。这样即便是在大量的不同图片瞬间显示和销毁,比如快速滚动列表,内存使用也在可一定范围内浮动(当然这里有一定的特殊性,就是展示的是海报一样网络图片,内存消耗惊人)。这样经受住了长时间的不停界面切换和连续的图片加载,实际内存与可用内存都维持在一个比较安全的范围内,每次gc之后基本都恢复到正常水平。而放着不管的美好想法根本不切实际,主动调用System.gc()更是邪道,而那些使用所谓的特殊方法强制激发内存回收,我觉得还是省了吧。

但是手动回收Bitmap其实风险很大,特别是对于一些循环使用的界面如AdapterView的一些子类,和与一些图片加载框架配合使用的时候。很容易在主动回收之后出现绘图失败,直接导致应用崩溃。所以必须保证任何地方不能存在对于Bitmap的引用,而是完全由图片展示容器持有,并做好在可循环界面中的图像重新加载功能。比较简单的做法是覆盖ViewGroup#onAttachedToWindow()ViewGroup#onDetachedFromWindow()方法。

当然了,这是一个长期工作,比如增加新功能和修改某bug之后,内存使用有出现微妙的变化,仿佛一些内存回收被阻碍了一般。这种时候就要不定期进行测试,尽快找出原因。当然了,内存问题不仅仅是Bitmap引起,还有一些积少成多有可能视而不见的原因。

2. memory leak

memory leak,也就是内存泄露,这又是一个老生常谈的问题,也许正式因为如此,不少人会无视吧。

内部类与弱引用

这点在java中就是存在的问题,普及程度却是低的惊人。这个问题在Android开发中就更加突出出来。在使用IDEA家族的开发环境时在创建内部android.os.Handler时总是会提醒可能造成内存泄露,请使用静态类实现。但是还是老话题,静态类多麻烦,内部类直接引用外部成员多方便。

所以问题来了,内部类会隐式的持有外部类的对象,进而能直接对外部类成员进行访问,即便是私有private也同样。这样造成的后果是如果内部类对象与某个异步或后台线程关联,那么在外部对象要销毁时,其因持有外部类对象会造成外部对象无法回收的问题。这在Activity中尤为致命,比如上面说的Handler问题。还有一些比如全局静态单例对象,如果在Activity中初始化,并直接传入默认的Context对象,那么对于Activity的回收简直是灾难。

1
2
3
4
5
6
7
8
9
10
11
12
public class BaseActivity extend Activity{
.....
private TextView mTextView;

Handler mHandler = new Handler(){
public void handleMessage(Message msg) {
String text = ...
mTextView.setText(text);
}
}
....
}

这时候mHandler一般会和一个异步线程关联,线程也持有mHandler更坏的情况是线程持有BaseActivity对象。正常情况下线程执行完成使用mHandler切换到UI线程更新界面,用户看完退出。但是如果因为网络或I/O等原因,用户没等到界面更新直接退出,那么线程在运行其持有mHandler进而持有BaseActivity,导致界面无法回收。如果线程出现什么问题一直没有停止那么在当前虚拟机中就会一直存在,当然也是极端情况。

这时候就不得不提一下java中的引用关系:

  1. 强引用(StrongReference):强引用就是默认情况下的引用关系,这种引用只要存在java虚拟机就不会对其进行回收,直到内存溢出,当前调用线程挂掉(如果是主线程,虚拟机和字节退出)。大部分的内存溢出问题都是来自与此,对于android再说,如果此异常出现在UI线程中,那么应用也会直接退出。
  2. 软引用(SoftReference):软引用简单来说就是对于其的垃圾回收条件是内存不足,如果可以内存依旧剩余,那么就不会对齐进行回收。也正是因为这个特性软引用会当做缓存使用。
  3. 弱引用(WeakReference):弱引用就是仅仅是存在这个引用声明,虚拟机在做内存回收计算时完全忽略此引用。如果不存在其他强引用或满足条件的软引用,则虚拟机直接回收此对象。
  4. 虚引用(PhantomReference):虚引用在虚拟机眼中就是没有引用,其最大的作用在于可以获得对象被回收时的通知,进而实现一些特殊功能。

在Android当中存在很多的软引用和弱引用,而对于Handler场景来说最适合的就是弱引用 - WeakReference ,在此我们实现一个比较通用的WeakHandler

1
2
3
4
5
6
7
8
9
10
public class WeakHandler<T> extends Handler{
WeakReference<T> mReference;
public WeakHandler(T t){
mReference = new WeakReference<T>(t);
}

public T get(){
return mReference.get();
}
}

这里将直接引用也好,隐式应用也好转换成弱引用,使用WeakReference 。那么修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BaseActivity extends Activity{
.....
private TextView mTextView;
private WeakHandler<BaseActivity> mHandler;

public void setContentView(int layoutResID){
super.setContentView(layoutResID);
mHandler = new WeakHandler<BaseActivity>(this);
....
}

private static class EventHandler extends WeakHandler<BaseActivity>{
public EventHandler(BaseActivity activity){
super(activity);
}
public void handleMessage(Message msg) {
BaseActivity activity = get();
if(activity == null) return;
String text = ...
activity.mTextView.setText(text);
}
}
....
}

这种情况下,当界面销毁后虚拟机检测到此界面发现没有引用关系,在垃圾回收中将其回收。

其他的问题,像什么内存抖动之类的。如果编程习惯可以的话,应该不是问题了。

二、性能

性能问题最直观的感受就是启动速度与交互顺畅度,也就是常说的如丝般顺滑什么的…

1. 线程与I/O

线程切换控制不好是造成界面不流畅的主要原因,这个问题实际不太容易找出问题。归根结底还是一个说了很久的原则,就是在UI线程中不要做耗时的操作,如阻塞式网络请求和直接I/O操作还有大一些的数据处理(比如图像操作,有些甚至需要使用本地代码实现)。这点最简单的跑出方法还是使用StrictMode

1
2
3
4
5
6
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()  
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());

Activity#onCreate()中使用,确保在初始化时开始监控。其中各种detectXX方法会对指定的操作进行监控,并会很直观的把相应信息打印出来。除了线程之外还有StrictMode.setVmPolicy()等用于内存和数据库监控,真是的非常便利。其监控确实非常严格,比如在UI线程中创建文件、同步读写配置文件、直接操作数据库都会被查不来。

2. 动态布局

耗时的布局操作实际上非常影响性能,这里就说最直观的。如果直接new出一个ViewGroup相关子类并进行各种addView(v: view)removeView(v: View)操作还可以接受,但是如果使用LayoutInflater#inflate (parser: XmlPullParser, root: ViewGroup)等方法那才是关键。我记得不知道在多少地方看到说inflate方法非常耗时,但是直到遇到性能问题才去关注这些。

Choreographer Skipped xxx frames!,这个问题实际非常影响使用体验。在排除以上线程,I/O操作等问题后,问题来到了界面动态初始化上。实际上解决inflate耗时问题本质就是减少操作,循环使用控件比如AdapterView那种,新的Recyclerview更是效果拔群。

话是这么说,但是总会遇到一些特殊的情况。比如显示界面非常大,同时显示项目有非常多,循环加载总是感觉非常微妙。经过测试还是觉得静态展示效果最好,但是又考了到更新。如果偷懒的话直接删掉重新add,但是性能问题出来了提示跳过xxx帧。

回到开始,这个问题的本质还是inflate耗时,那么尽量减少不不可以了。就像最开始说的,所以展示界面全部封装提供统一接口数据来了直接刷新到控件中。比如XXXView#reload(data)这样,界面没有进行实例化操作,在做一些细节优化,效果简直就是如丝般顺滑。但是数据更新总会遇到与旧数据个数不相符的问题,如果这个问题比较严重,那么就使用对象池.

对象池仅仅是一个思想,不一定非要弄一个XXXPool出来。

android中对象池实际应用还是比较多的,其中最为人所知应该的Handler/Message。其中Handler#obtainMessage()就是一个非常明显的取对象的方法。如果是缓存普通对象的话应该没什么问题,但是缓存控件的话又会遇到Context引用问题,控件状态问题,重新layout问题等等。

总之遇到这类问题除非非常奇葩,不然还是推荐使用循环使用的AdapterView来实现。