App架构师实践指南六之性能优化三
内存性能优化
1、内存机制和原理
1.1 内存管理
内存时一个基础又高深的话题,从认识内存到使用内存,再到管理内存,伴随着编程生涯。
程序本身只是一个内存中数据不断迁移和CPU不断进行数值运算的过程,一层层高级语言和软件工程将这个复杂过程更加条理有序地去组织了,避免了“重复制造车轮”的繁琐,但内存问题的本身是不可避免的。
1.2 Android内存机制
Android本身既支持java,又支持C/C++,框架上又基于Linux上承接Android Framework。
1.2.1 java内存机制
---java内存区域。java内存区域可划分为方法区、堆、栈以及程序计数器。
------方法区(Method Area)。默认最大容量64MB,存放类的结构(方法和属性)、静态成员等,运行时的常量池,被所有线程共享的内存区域,属于持久代。
------堆(Heap)。默认最大容量是64MB,存放对象持有的数据,同时保持对原类的引用,被所有线程共享的内存区域。
------栈(Stack)。分虚拟机栈(JVM Stacks)和本地方法栈(Native Method Stacks),前者用于存储局部变量表、动态连接、操作数、方法出口等信息,有两种可能的java异常---StackOverFlowError和OutOfMemoryError,为java方法服务;而后者为Native方法服务。默认最大容量为1MB,方法调用结束后,java虚拟机会回收栈占用的内存,线程私有内存区域。
------程序计数器(Program Counter Register)。可以看作是当前线程执行字节码的行号指示器,位于CPU中,程序不能直接对其操作,每个线程都有独立的程序计数器,线程私有内存区域。
---GC。Garbage Collection/Collector,垃圾回收/回收器,用于分配内存确保被引用对象保留在内存中,以及回收不存在引用关系的对象内存,基本算法是分代收集,针对内存区域中的本地方法栈和堆进行回收,新生代、旧生代和长久代采取不同的GC算法。
---java引用。JDK1.2+,采用强、软、弱、虚4种引用来标记不同的对象。
------强引用(Strong Reference)。永远不会被回收的对象。
------软引用(Soft Reference)。可被回收的对象,由JVM内存紧张与否决定。
------弱引用(Weak Reference)。一定需要被回收的对象。
------虚引用(Phantom Reference)。可忽略,用于作跟踪记录,辅助finalize函数使用。
1.2.2 C++内存机制
C/C++内存空间。C/C++内存空间由栈区、堆区、全局/静态存储区、常量存储区和程序代码区组成。
---栈区。存储执行函数的参数和局部变量等,容量有限,效率很高,由程序自动分配和释放。
---堆区。由程序手动分配和释放。C中采用malloc/free,C++中采用new/delete进行分配和释放,堆大小无限制,由OS内存空间大小决定。
---全局/静态存储区。存放全局变量和静态变量等区域。
---常量存储。存放常量等区域,不允许修改。
---程序代码区。存放程序等二进制代码。
堆生长方向。栈是逆生长,先进栈所分配等内存空间地址更大,堆上顺序生长,先进栈所分配等内存空间地址更小。注意:无论是堆还是栈,指针指向的所分配的某一块内存的首地址永远是这块内存中最小的。
1.2.3Android内存管理
1.2.3.1 Android中包括Native和Java两类进程,Native进程基于C/C++实现,是不包含Davlik实例的进程;java进程基于java语言,是运行在Davlik/ART虚拟机上的进程。Android中每个App默认情况下上运行在一个独立进程中,这个独立进程上从Zygote fork出来的VM进程,即每个App运行在独立的VM空间。
1.2.3.2 Davlik与ART.
---Android 4.4以前使用基于Davlik虚拟机的VM,Android 4.4+引入ART,Android 5.0正式将ART作为默认VM.
---Davlik不同于java虚拟机,执行的是dex文件而非class文件,采用JIT技术。在应用启动时,JIT通过进行连续的性能分析来优化程序代码的执行。在程序执行过程中,Dalvik不断地进行将字节码翻译成机器码的工作。
---ART,Android Runtime,引入了AOT(Ahead-Of-Time)预编译技术,提升了GC效率,支持更多开发调试技巧,具有更长续航能力,提升了APP运行性能。
1.2.3.3 内存限制。不同厂商APP内存限制不同,存放在system/build.prop中。可以在ADB Shell环境中采用cat /system/build.prop命令获取。如下为Nexus5,Android6.0手机获取的信息。headpstatize决定堆分配的初始大小,heapgrowthlimit决定受控下的极限堆大小,heapsize决定堆堆最大值(需要manifest中指定android:largeHeap为true。)若要突破heapsize限制,可以创建子进程(android:process)或者使用jni在native heap中申请空间。
1.2.3.4 App应用切换。Android系统不会在用户切换不同应用时做内存交换的操作,相反,Android会把那些不在前台可见的进程放到LRU缓存中,主要便于在应用再次切回时快速响应。该缓存占用一定的内存,对系统性能有一定影响。
1.2.3.5 App进程级别。Android GC时会针对不同进程级别采取优先级别,根据重要程度从大到小依次为前台进程、可见进程、服务进程、后台进程和空进程。
2、内存分析工具
3、泄漏和溢出
分析内存问题的本质是找出内存被谁占用了,找出内存占用大的对象,找出其关联,跟踪GC可达路径,从而定位谁让这个大对象存活着。
3.1 内存溢出(Out of Memory,OOM)
定义:对象内存占用超过了分配内存大小,内存越界,即内存不够了。
原因:
---内存泄漏导致。内存泄漏对象越来越多时,内存泄漏会导致内存溢出。
---大内存对象。如Android中的Bitmap或加载超大图像资源等。
3.2 内存泄漏(Memory Leaks)
定义:由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
通俗点讲就是,程序申请内存后,没有释放已经申请到的内存,始终占用着,内存使用完后没有归还,被分配的对象可达却无用。
3.2.1 Android中常见的内存泄漏
---长时间保持对Activity、Context、View、Drawable和其他对象的引用。
------Activity使用静态成员。建议使用静态的Activity、View等。
------用Context处理Thread、第三方库初始化等异步程序时,这些异步程序等生命周期可能大于Activity的生命周期,导致Activity无法被回收,造成内存泄漏。
------建议与View无关的操作,Context尽量使用Application Context。
---内部类。与非静态内部类相似,持有外部类的引用导致内存泄漏。
---持有对象的时间超出需要的时间/引用对象没有释放(注意持有对象的生命周期)。
------register对象后缺少对应的unregister操作,如广播等。
------集合对象未清理,资源对象未关闭。如Cursor、File等资源。
------static滥用。当static用于修饰大内存占用对象时,会导致该对象无法回收,造成内存泄漏。
------bitmap使用完后没有回收。
---不良代码。
4、内存性能优化
4.1 内存度量
4.1.1 ActivityManager.MemoryInfo()方法:可以得到当前系统剩余内存及判断是否处于低内存运行,腾讯GT等工具采取的方式。
4.1.2 ActivityManager的getProcessMemoryInfo(int[] pids)方法:得到的MemoryInfo所描述的内存使用情况比较详细,数据的单位所KB.
4.1.3 Debug的getMemoryInfo()、getNativeHeapSize()、getNativeHeapAllocatedSize()、getNativeHeapFreeSize()方法。
4.1.4通过adb相关命令获取,具体有以下几种不同的方法。
---adb shell dumpsys meminfo|grep pkg_name or pid命令,可以直接获取具体进程的内存信息。
---adb shell procrank|grep pkg_name命令,可以获取VSS、RSS、USS、PSS。
------VSS(Virtual Set Size),虚拟耗用内存(包含共享库占用的内存)。
------RSS(Resident Set Size),实际使用的物理内存(包含共享库占用的内存)。
------PSS(Proportional Set Size),实际使用的物理内存(比例分配共享库占用的内存)。
------USS(Unique Set Size),进程独自占用的物理内存(不包含共享库占用的内存)。
一般来说,内存占用大小有如下规律:VSS >= RSS >=PSS >= USS.
---adb shell cat/proc/meminfo命令,可以获取系统整个内存的大致使用情况。
---adb shell px -x命令,可以得到内存信息VSIZE和RSS.
4.2Android与java内存性能优化
---Service的使用。
------尽量少用Service,当后台任务运行完成后,要及时关闭Service,否则由于Service的保持运行状态,导致其占用的内存不会释放。
------用IntentService取代Service,当后台任务完成时,自动结束服务本身。
---UI不可见或内存紧张时,释放内存。在Activity的回调方法onTrimMemory(int level)中,根据level的不同释放内存。
------进程不在缓存中。根据TRIM_MEMORY_RUNNING_MODERATE、TRIM_MEMORY_RUNNING_LOW和TRIM_MEMORY_RUNNING_CRITICAL状态进行处理。
------进程在LRU缓存中,根据TRIM_MEMORY_BACKGROUND、TRIM_MEMORY_MODERATE和TRIM_MEMORY_COMPLETE状态进行处理。
---恰当使用Bitmap。加载Bitmap时尽量保证分辨率和屏幕分辨率对应,大分辨率Bitmap需要进行压缩处理。Android2.3(API 10)以下系统需要手动recycle(Bitmap像素存储在Native内存中)。
---使用SparseArray、SparseBooleanArray和LongSparseArray等优化的数据容器代替HashMap。
---使用static const代替enum。
---非必要情况下,少用抽象。
---对于序列化数据,使用nano protobuf
---尽量少使用依赖注入框架。
---使用ProGuard去除不必要的代码。
---apk打包签名时,使用zipalign工具对齐。
---使用多进程。
---GC主动调用。
---finally调用和重写。
---最后,养成好的编码习惯。
4.3 C/C++常见内存问题
---未初始化的内存和变量。malloc分配的内存不会自动初始化,可在声明的同时进行初始化。
---空指针。使用前先判空,空指针访问会产生segment fault错误。不要忘记为数组和动态内存赋初值。
---野指针。free或delete释放内存后,立即将指针设置为NULL.
---内存覆盖。
---内存越界。避免数组或指针的下标越界,特别要当心发生“多1”或“少1”的操作。
---内存泄漏。动态内存的申请和释放必须配对,防止内存泄漏。
4.4 Android经典内存泄漏实例
如下所示LeakActivity,涉及非静态内部类、匿名内部类等典型内存泄漏场景及正确书写方式。