当前位置: 首页 > 科技观察

【宝贵经验】Android性能优化内存优化实践

时间:2023-03-11 20:27:57 科技观察

1.MemoryLeak内存泄漏:对于Java来说,是指new出来的Object放在了Heap上,无法被GC回收(内存中存在无法回收的对象);当发生内存泄漏时,主要表现为内存抖动,可用内存逐渐减少。1.1MemoryMonitorAndroidStudio内置的MemoryMonitor可以方便的观察堆内存的分配情况,可以粗略的观察是否存在MemoryLeak。频繁内存抖动,可能存在内存泄漏A:启动GC需要手动触发GC操作;B:DumpJavaHeap获取当前栈信息,生成.hprof文件,AndroidStudip会自动使用HeapViewer打开;一般用于检测操作后的内存泄漏C:StartAllocationTracking内存分配跟踪工具,用于跟踪一段时间内的内存分配使用情况,可以知道哪些对象在执行了一些操作后被分配了空间。它通常用于跟踪操作后的内存分配,并调整相关方法调用以优化应用程序性能和内存使用;D:剩余可用内存;E:已用内存。点击MemoryMonitor的DumpJavaHeap,会生成一个.hprof文件,AndroidStudio会自动用HeapViewer打开。hprofViewer打开.hprof文件左侧面板,解释:TotalCount该类的实例数HeapCount所选Heap中的实例数Sizeof每个实例占用的内存大小ShallowSize所有实例占用的内存大小该类的RetainedSize右面板说明该类所有实例分配的内存大小:Instance该类的所有实例对象(左边的TotalCount为15,所以这里有15个对象)Depth深度,最短从GCRoot指向此实例的链接NumberDominatingSize此实例可以控制的内存大小。这里可以看到MainActivity中有15个示例对象。怀疑是这里有问题。1.2MAT以上只能粗略的看有没有问题,但是要知道问题出在哪里,还需要用到MAT。转换生成的.hprof文件,然后用MAT打开;格式转换命令:hprof-conv转换原文件路径,用MAT.hprof打开文件路径注意以下Actions:Histogram可以列出每个object在内存中的名称和数量以及大小。DominatorTree将内存中的所有对象按大小排序,我们可以分析对象之间的引用结构。这两个函数一般用的最多。RetainedHeap表示这个对象和它持有的其他引用(包括直接和间接)占用的总内存使用Histogram:点击Histogram,在最上面的Regex中输入MainActivity进行正则匹配,所有包含“MainActivity”的对象都会被包含的所有对象都列出来了,第一行是MainActivity的实例。在要查看的对象上右击->Listobjects->withincomingreferences可以查看具体的MainActivity实例。右键单击要查看的对象实例->PathToGcRoots->excludeweakreference(排除软引用)。注意:this$0前面的图标左下角有一个圆圈,表示这个引用可以被GcRoots引用。由于MainActivity$LeakClass可以被GCRoots访问到,所以它不能被回收,所以它持有的其他引用也是不能被回收的,包括MainActivity和MainActivity中包含的其他资源。至此我们找到了内存泄漏的原因。使用DominatorTree使用上面的Histogram操作方法也可以找到具体的泄漏原因,这里不再赘述。注意:每个对象前面的图标圈并不一定代表它就是内存泄漏的原因。有些对象需要在内存中存活,需要区别对待。1.3LeakCanaryLeakCanary是Square出品的检测内存泄漏的库。将其集成到应用程序中后,您无需关心它。发生内存泄漏后,它会用Toast和通知栏弹窗提示你。它可以指出泄漏的引用路径并抓取当前堆栈。详细分析的信息。2.OutOfMemory2.1AndroidOOMAndroid系统中的每个进程都有一个最大内存限制。如果申请的内存资源超过这个限制,系统会抛出OOM错误。在Android2.x系统中,当dalvikallocated+externalallocated+newallocatedsize>=dalvikheap***value时会出现OOM。位图放在外部。在Android4.x系统中,取消了外部计数器,改用类似位图的分配方式,应用到dalvik的java堆中。只要分配+新分配的内存>=dalvikheap***值,就会出现OOM(艺术运行环境统计规则还是和dalvik一致)内存溢出是程序运行到一定阶段的最终结果。直接原因是剩余内存不能满足内存申请,接下来分析内存没了的间接原因:内存泄漏的存在可能导致可用内存越来越少;内存申请峰值超过该时间点系统剩余内存;(例如:手机单个进程最大可用内存为192M,当前分配内存为80M,此时申请5M内存,但当前时间点整个系统可用内存为只有3M。此时并没有超过单个进程的最大可用内存,但也会出现OOM)2.2避免AndroidOOM除了避免内存泄漏,根据《Manage Your App's Memory》,我们可以监控memory,在Activity中重写这个方法,根据不同的情况进行不同的处理:@OverridepublicvoidonTrimMemory(intlevel){super.onTrimMemory(level);}TRIM_MEMORY_RUNNING_MODERATE:你的应用程序正在运行,不会被列为killable。但是此时设备运行在低内存状态,系统开始触发杀死LRUCache中Process的机制。TRIM_MEMORY_RUNNING_LOW:您的应用程序正在运行并且未列为可终止。但设备运行在内存较低的状态下,应释放未使用的资源以提高系统性能。TRIM_MEMORY_RUNNING_CRITICAL:你的应用程序还在运行,但是系统已经杀死了LRUCache中的大部分进程,所以你应该立即释放所有非必要的资源。如果系统无法回收足够的RAM,系统将清除LRU缓存中的所有进程,并开始杀死之前认为无法杀死的进程,例如包含正在运行的Service的进程。当应用进程退回到后台并被缓存时,可能会收到onTrimMemory()返回的以下值之一:TRIM_MEMORY_BACKGROUND:系统运行在低内存状态,你的进程最少LRU缓存列表容易杀掉的位置。虽然你的应用进程被杀的风险不高,但系统可能已经开始杀掉LRU缓存中的其他进程。你应该释放容易回收的资源,这样你的进程才能存活,这样当用户返回到你的应用程序时,它可以快速恢复。TRIM_MEMORY_MODERATE:系统运行在低内存状态,你的进程已经接近LRU列表的中间。如果系统开始变得更加受内存限制,您的进程很可能会被终止。TRIM_MEMORY_COMPLETE:系统运行在低内存状态,你的进程在LRU列表中处于最脆弱的位置。您应该释放任何不影响应用程序恢复状态的资源。3.MemoryChurnMemoryChurn内存抖动:大量对象在短时间内被立即创建和释放。瞬间创建大量对象会严重占用YoungGeneration的内存区域。当达到阈值,剩余空间不够时,也会触发GC。系统在GC上花费的时间越多,它要做界面绘制或流式音频处理的时间就越少。即使每次分配的对象占用的内存很小,但是它们的叠加会增加Heap的压力,从而触发更多其他类型的GC。此操作可能会影响帧率并导致用户感知性能问题。DropFrameOccur时可能导致内存抖动的常见情况:在循环中创建临时对象;在onDraw中创建Paint或Bitmap对象;对象会导致内存抖动。4.BitmapBitmap的处理也是Android中的一个难点。当然,使用第三方框架会屏蔽这个难点。Bitmap的内存模型;Bitmap的加载、压缩、缓存等策略;版本兼容性等;关于Bitmap,我会专门写一篇文章来介绍,这里可以参考《Handling Bitmaps》。5.程序建议5.1节俭使用Service内存管理的最大错误之一是让Service一直运行。在后台使用服务时,除非需要被触发并执行任务,否则其他时间应该停止服务。另外需要注意的是,Service需要在工作完成后停止,以免造成内存泄漏。系统会倾向于保留Service所在的进程,这使得进程的运行成本非常高,因为系统没有办法为其他组件腾出Service占用的RAM空间,Service无法被调出。这样就减少了系统可以存放在LRU缓存中的进程数,会影响应用程序之间的切换效率,甚至会导致系统内存使用不稳定,导致无法维护所有当前正在运行的服务。推荐使用JobScheduler而不是持久化Service。还建议使用IntentService,它会在处理完分配给它的任务后尽快自行结束。5.2使用优化的采集AndroidAPI提供了一些优化的数据采集工具类,如SparseArray、SparseBooleanArray、LongSparseArray等,使用这些API可以使我们的程序更加高效。传统JavaAPI中提供的HashMap工具类效率相对较低,因为它需要为每个键值对提供一个对象入口,而SparseArray则避免了将基本数据类型转换为对象数据类型的时间。5.3谨慎面向抽象开发人员经常将抽象作为一种良好的编程习惯,因为抽象可以提高代码的灵活性和可维护性。但是,抽象会带来很大的开销:抽象需要额外的代码(不会被执行),并且还会映射到内存中,这会消耗更多的时间和内存空间。因此,如果面向抽象对您的代码没有显着好处,那么您应该避免使用它。例如:使用枚举通常比使用静态常量消耗两倍以上的内存。在Android开发中,我们应该尽量不要使用枚举。5.4使用nanoprotobufs序列化数据Protocolbuffers是Google为序列化数据设计的一种语言无关、平台无关、可扩展的数据描述语言。它类似于XML,但更轻、更快、更简单。如果使用protobufs来序列化和反序列化数据,建议在客户端使用nanoprotobufs,因为普通的protobufs会产生冗余代码,减少可用内存,增加APK大小,降低运行速度。5.5避免内存抖动垃圾回收通常不会影响应用程序的性能,但是短时间内的多次垃圾回收会消耗界面绘制的时间。系统在GC上花费的时间越多,它要做界面绘制或流式音频处理的时间就越少。通常内存抖动会导致多次GC。实际上,内存抖动表示一段时间内临时对象的分配。例如:在For循环中分配了多个临时对象,或者在onDraw()方法中创建了Paint和Bitmap对象,应用产生了大量的对象;这将很快耗尽年轻代的可用内存,导致GC发生。使用分析RAM使用情况中的工具来查找代码中的内存抖动。考虑将操作移出内部循环,或移入基于工厂的调度结构。5.6去除耗内存的库,减小Apk的大小检查Apk的大小,包括第三方库和嵌入资源,这会影响应用程序消耗的内存。可以通过减少冗余、非必要或大型组件、库、图像、资源、动画等来改善应用程序内存消耗。5.7使用Dagger2进行依赖注入如果您计划在应用程序中使用依赖注入框架,请考虑使用Dagger2.Dagger不使用反射来扫描应用程序的代码。Dagger对编译时注解技术的实现意味着它不需要不必要的运行时开销。而其他使用反射的依赖注入框架通常是扫描代码来初始化进程。此过程可能需要更多的CPU周期和RAM,并且可能会在应用程序启动时导致明显的卡顿。备注:之前的文档不推荐使用依赖注入框架,因为其实现原理是利用反射,演变成编译期注解后,就不会受到反射的影响了。5.8谨慎使用第三方库。许多开源库代码不是为移动设备编写的。如果它们用于移动设备,则可能不合适。即使是为Android设计的库也需要特别小心,尤其是当您不知道导入的库的作用时。例如,一个库使用nanoprotobufs,而另一个使用microprotobufs。这样,你的应用程序中就有了protobuf的两个实现。类似的冲突也可能出现在输出日志、加载图片、缓存等模块中。此外,不要为了一个或两个函数导入整个库。如果没有符合您需求的合适库,您应该考虑自己实现它,而不是导入一个庞大而全面的解决方案。6.其他6.1谨慎使用LargeHeap属性。您可以通过在清单的应用程序标签下添加属性largeHeap=true来为应用程序声明更大的堆空间(您可以通过getLargeMemoryClass()获得这个更大的堆大小阈值)。但是,声明较大的Heap阈值的初衷是针对少数消耗大量RAM的应用程序(例如大型图像编辑应用程序)。不要因为需要使用更多的内存就轻易请求一个大的HeapSize。只有当你清楚地知道哪里会使用大量内存以及为什么必须保留内存时,才使用大堆。使用额外的内存空间会影响系统整体的用户体验,并且会使得每次gc的运行时间变长。在任务切换时,系统的性能会大大降低。另外,largeheap并不一定会得到更大的heap。在一些严重受限的机器上,大堆的大小与正常堆的大小相同。6.2谨慎使用多进程多进程确实是一种可以帮助我们节省和管理内存的高级技术。如果你想使用它,你必须小心使用它,因为大多数应用程序不应该运行在多进程中。一旦使用不当,它甚至会增加额外的内存而不是帮助我们节省内存;同时,你需要了解多处理的缺点。这种技术更适合那些需要在后台完成一个独立任务的场景,可以完全脱离前端功能。下面是一个更适合使用多进程技术的场景。比如我们在做一个音乐播放软件,播放音乐这个功能应该是一个独立的功能。它不需要与UI有任何关系,即使软件关闭也是如此。它还应该能够正常播放音乐。如果此时我们只使用一个进程,即使用户关闭软件,音乐播放完全由Service控制,系统仍然会保留大量的UI内存。这种场景下,很适合使用两个进程,一个用于UI展示,一个用于后台持续播放音乐。6.3实现方法可能存在的问题:比如启动页的闪屏图片,Bitmap应该在展示完成后释放。某些实现似乎可以正常运行,但实际上可能会对内存产生影响。我正在使用堆查看器查看一个位图对象,发现一个地图在不应该加载的时候只是下载。使用HeapViewer可以直接查看Bitmap内存中不应加载的图片。通过查看代码,发现问题出在:这里下载的图片是作为另一个模块的图片使用的,但是下载的方式是使用图片加载器加载Bitmap。然后保存到本地;并且Bitmap对象保存后不释放。也类似:首页闪屏图片显示后,Bitmap对象要及时释放。6.4使用trycatch捕获对显示高清大图等OOM高危代码块进行trycatch,在catch块中加载非高清图片并进行相应的内存回收处理。请注意,OOM是一个OutOfMemoryError,不能使用Exception捕获。7、内存优化套路总结:(1)解决所有内存泄漏,集成LeakCanary,可以轻松定位90%的内存泄漏;通过反复进出可疑接口,观察内存增减,DumpJavaHeap获取当前栈信息,使用MAT进行分析。内存泄漏的常见情况可以参考《Android 内存泄漏分析心得》(2)避免内存抖动避免在循环中创建临时对象;避免在onDraw中创建Paint、Bitmap对象等。(3)Bitmap的使用使用第三方库加载图片一般不会造成内存问题,但是需要注意图片使用后的释放,而不是被动等待释放。使用优化后的数据结构,根据不同的内存状态使用onTrimMemory做相应的处理(4)Libraryuse去掉无用的库,反编译生成的Apk查看使用过的库,避免无用的Lib仍然进入到Apk中;避免引入庞大的库;使用Proguard进行混淆和压缩。