影响Android应用程序性能的,主要从两个方面看,一个是内存,一个是CPU使用率。这篇文档主要是记录一下自己在开发过程中的一些经验,以及学习官方发布的性能优化指导的总结。
内容主要是以下几点:
- Android 应用程序的内存分配
- 内存泄漏原因以及常见场景
- 内存使用优化以及Android原生容器
- 常见工具与命令
Android应用程序的内存分配由虚拟机划分。主要分为以下几种
- 程序计数器: 线程私有的一小块内存空间,主要用来记录当前线程所执行的行号
- Java虚拟机栈:线程私有的一块内存空间,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 本地方法栈:与虚拟机栈类似,不过服务的是native代码的方法
- Java堆: 线程共享的内存区,所有的java对象实例都在这里分配内存,是内存管理的主要区域。
- 方法区:线程共享的内存区,用于存储已被虚拟机加载的类信息,常量(final),静态变量(static),JIT编译器编译后的代码等数据
- 直接内存:非虚拟机运行时数据区的一部分,但也有可能被经常使用到,如java nio的ByteBuffer.allocateDirect. 或者在本地代码中申请的内存。
内存泄漏原因以及常见场景:
内存泄漏(Memory Leak)是指分配的内存在不再使用后,无法被释放的现象。主要原因是因为创建的对象在不再使用之后,无法被垃圾回收器(GC)回收。为了维持多任务的功能环境,Android为每一个app都设置了一个硬性的heap size限制。准确的heap size限制会因为不同设备的不同RAM大小而各有差异。如果app已经到了heap的限制大小并且再尝试分配内存的话,会引起OOM的错误。我们可以通过该命令来获取当前堆内存限制大小“getprop dalvik.vm.heapgrowthlimit”
,也可以在代码中使用ActivityManager
的getMemoryClass
来查询。 特殊情况下,应用需要申请大内存,则我们可以在AndroidManifest.xml中设置”largeHeap = true”, 那么此时决定该应用程序的最大可分配内存的属性值为” dalvik.vm.heapsize”
,对应代码中可以用ActivityManager
的getLargeMemoryClass
方法查询。
从上面的应用内存分配的介绍可以看出,我们在写应用程序时,内存消耗比较多的是Java堆与直接内存。java堆中的内存泄漏,主要是跟对象的生命周期异常有关,通常是一个短生命的对象在死亡时其引用依旧被长生命周期的对象持有。直接内存中产生的内存泄漏,主要原因在于没有手动释放内存。了解了原因,下面来总结一下开发过程中容易产生内存泄漏的多个场景。
-
使用static修饰的变量引用对象
从内存分配的介绍可以知道,static修饰的变量内存被分配在方法区,一般来说,这块属于GC回收中的永久代,其生命周期与应用进程一致,因此我们如果我们使用static变量引用对象时,如果不手动释放,则该对象无法被回收,造成内存泄漏。
-
使用单例
单例的生命周期,因其所创建自身的对象为静态属性,所以它的生命周期等同进程周期,因此在引用其他短周期的对象时,应该注意在不使用对象时,及时清空引用。在应用程序中,我们使用最多的则是Context传递,因此在单例中使用Context, 应使用Application的Context, 使两者的生命周期相同。public class SingleInstance{ private static volatile SingleInstance mInstance; public Context mContext; public SingleInstance(Context context){ this.mContext = context; } public static SingleInstance getInstance(Context context){ if(mInstance == null){ synchronized (SingleInstance.class){ mInstance = new SingleInstance(context.getApplicationContext()); } } return mInstance; }
-
错误使用bitmap
Bitmap在应用程序开发过程中,极容易成为占用内存的大户,如其操作的图片资源过大,则容易引起内存溢出, 即使使用资源文件小的图片,如无正确释放资源,也容易让应用程序频繁GC,降低程序的运行速度。 这部分,我们可以通过设置Bitmap创建的显示参数来优化, 配置代码如下所示。另外我们也可以使用google推介的开源库Glide, 它有着跟随页面周期、支持gif和webp、支持多种数据源等特点.BitmapFactory.Options options = new BitmapFactory.Options(); // option A: 某些场景下使用降低分辨率的方式减少图片的内存占用。 options.inSampleSize = 8; options.inPreferredConfig = Bitmap.Config.RGB_565; //option B: 复用Bitmap options.inBitmap = currentBitmap; Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.pic, options);
-
匿名内部类
匿名内部类默认持有外部类的引用,因此在使用匿名内部类时,容易产生临时性的内存泄漏。具体情境在于我们在Activity或者Service当中定义Handler时,通常情况下,我们的定义方式如下:private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); } };
如果我们在Android Studio中编写该代码时,工具会提示我们在UI线程中如此使用Handler可能会存在内存泄漏的风险。原因是因为主线程中的Handler使用的是主线程的Looper,且其还持有外部Activity或者Service的引用。 主线程的Looper生命周期等同于进程。因此当Activity或者Service被销毁时,如果此时Handler的MessageQueue队列事件没有清空,则其Activity或者Service的对象在Gc触发的情况下,不会被回收,直到Handler将其队列中的事件处理完毕,在下一次Gc时,才可能回收Activity或者Serivce对象。 解决方式为将Handler定义为静态类,并将所依赖的对象作为弱引用存在与Handler当中, 修改后的使用方式如下:
private static class H extends Handler{ private WeakReference<MainActivity> ref; public H(MainActivity activity){ ref = new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity activity = ref.get(); if(activity != null){ //do something } } }
-
直接内存的申请,因为不受虚拟机的管理,所以需要手动分配或者回收内存。 在C/C++中,内存分分配与回收,通过malloc/free, new/delete等关键完成,每块内存的分配,必须要对应一个回收的逻辑。
内存使用优化以及Android原生容器:
- 字符串操作
大量的字串拼接会引起内存抖动,引起系统频繁GC,影响性能,故在无须线程安全的情况下,当拼接大量的字串时,使用StringBuilder对象来操作。 当应用在线程安全的场景时,使用StringBuffer来拼接字串。 - 频繁创建对象
在某些需要频繁创建对象的场景下,我们经常使用new来实现,此举高频创建对象不仅耗费资源,且效率不高,因此我们可以应用对象池技术,加强对象的复用率,来达到节省资源与效率的目的。android中提供了Pools.java类来实现对象池。其原理为初始化一个由程序动态提供的对象池大小的对象数组。 在需要对象时调用acquire()方法,获得一个对象, 首先我们需要判断获取的对象是否为空,为空的情况下,则创建一个新的对象,在该对象使用完成时,再使用release()方法将对象赋值到数组中,以便下次复用,使用方式如下:
除了SimplePool以外,还有应用于线程同步的SynchronizedPool,使用方式与SimplePool一致,具体可查看源码 android.util.Pools。Pools.SimplePool<Person> pools = new Pools.SimplePool<>(32); public Person obtain(){ Person person = pools.acquire(); return person == null ? new Person() : person; } public void recycle(Person person){ //reset Person person.name = ""; person.age = 0; //put the Person to pool pools.release(person); }
- 避免使用枚举类型
枚举类型一般比常量所需的内存要大两倍以上,假设我们有这样一份代码,编译之后的dex大小是2556 bytes,在此基础之上,添加一些如下代码,这些代码使用普通static常量相关作为判断值:
增加上面那段代码之后,编译成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。假如换做使用enum,情况如下:
使用enum之后的dex大小是4188 bytes,相比起2556增加了1632 bytes,增长量是使用static int的13倍。不仅仅如此,使用enum,运行时还会产生额外的内存占用,如下图所示:
常用工具与命令:
1.句柄泄漏
句柄泄漏也是影响程序稳定性的原因之一; 因为Android基于linux的内核,其对进程的句柄打开数量有限制,可使用
cat /proc/pid/limits
命令查看如下:
对应为Max open files值,超过这个值,则在使用过程中会提示"Too many open files"错误,并且现有的句柄通信均无法正常工作。 files实际包含,普通文件,设备节点,Socket,pipe等。 查看句柄泄漏,可在在proc文件系统中,跟踪这个问题,就需要查看句柄在程序结束一个流程时,其产生的句柄能够正确释放。首先查询对应进程的pid, 然后在/proc/pid/fd中查看多次流程运行后,句柄的创建以及释放情况。
2. 实时查看程序CPU使用率
CPU的使用率,也是衡量我们程序性能的一个重要指标,我们可以使用top命令来实时查看CPU的使用率大小。
命令:top –s cpu –d 1 -m 4 这条命令的意思是每秒打印整个系统中,CPU使用率前4的进程。详细的top命令使用方式如下:
Usage: top [ -m max_procs ] [ -n iterations ] [ -d delay ] [ -s sort_column ] [ -t ] [ -h ]
-m num Maximum number of processes to display.
-n num Updates to show before exiting.
-d num Seconds to wait between updates.
-s col Column to sort by (cpu,vss,rss,thr).
-H Show threads instead of processes.
-h Display this help screen.
3. 使用StrictMode
StrictMode 是Android中集成的一个性能检测类,在开发过程中,程序中集成该功能,能够在开发过程中方便的检测出进程间出现的内存泄漏,句柄泄漏,以及主线程的性能问题。其使用方式如下,在开发版本中,在Application的onCreate函数中,集成StrictMode
public class MyApplication extends Application {
@Override
public void onCreate() {
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.detectLeakedSqlLiteObjects()
.penaltyLog()
.build());
super.onCreate();
}
}
用于检测整个程序中的未关闭的对象,以及数据库操作对象内存泄漏等问题。
在Activity或者Service中,设置ThreadPolicy,来检测主线程耗时操作。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
//.penaltyDeath()
.penaltyDialog()
.build());
}
该工具提供两种策略,分别检测主线程中是否存在耗时操作以及检测虚拟机中是否存在内存泄漏,句柄泄漏等性能问题。对于ThreadPolicy而言,其功能定义如下:
方法 | 说明 |
---|---|
detectDiskReads() | 检测磁盘读取数据操作 |
detectDiskWrites() | 检测磁盘写数据操作 |
detectNetwork() | 检测网络操作 |
detectCustomSlowCalls() | 检测耗时调用 |
detectResourceMismatches() | 检测资源不匹配 |
detectAll() | 检测以上所有 |
当检测到违反ThreadPolicy策略时,其处理方式定义如下:
方法 | 说明 |
---|---|
penaltyLog() | 输出错误日志 |
penaltyDropBox() | 输出日志到DropBox中 |
penaltyFlashScreen() | 闪烁屏幕 |
penaltyDialog() | 弹出警告框 |
penaltyDeath() | 杀死进程 |
penaltyDeathOnNetwork() | 主线程中检测到网络操作时,杀死进程 |
对于VmPolicy的功能定义,如下:
方法 | 说明 |
---|---|
detectActivityLeaks() | 检测Activity泄漏 |
detectLeakedClosableObjects() | 检测可关闭的对象泄漏 |
detectLeakedSqlLiteObjects() | 检测Sqlite对象引起的内存泄漏 |
detectLeakedRegistrationObjects() | 检测BroadcastReceiver/ServiceConnection泄漏 |
detectFileUriExposure() | 检测 可能产生FileUriExposed的问题,从Android 7.0开始,谷歌收回了访问文件的权限,即一个应用提供自身资源文件给其它应用使用时,如果给出 file://xxx 这样格式的URI的话,谷歌会认为目标应用不具备访问此文件的权限,便会抛出 FileUriExposedException 的异常 |
detectCleartextNetwork() | 检测网络传输是否加密 |
detectAll() | 检测以上所有 |
当检测到违反Policy中的策略其处理方式定义如下:
方法 | 说明 |
---|---|
penaltyLog() | 输出错误日志 |
penaltyDropBox() | 输出日志到DropBox中 |
penaltyDeathOnFileUriExposure() | 检测到有FileUriExposure问题时,杀死进程 |
penaltyDeathOnCleartextNetwork() | 有明文进行网络传输时,杀死进程 |
penaltyDeath() | 杀死进程 |
4. 其他工具
LeakCanary:一款优秀的内存泄漏检测库
Android Memory Profile: Android Studio中已经集成,可以实时查看内存动态,网络流量,CPU使用率的工具,非常棒。