写点什么

Android 应用开发性能优化完全分析,flutter 蓝牙开发

用户头像
Android架构
关注
发布于: 20 小时前

| CPU Time/Call | 当前方法调运 CPU 时间与调运次数比,即当前方法平均执行 CPU 耗时时间; |


| Real Time/Call | 当前方法调运真实时间与调运次数比,即当前方法平均执行真实耗时时间;(重点关注) |


有了对上面 Traceview 图表的一个认识之后我们就来看看具体导致 UI 性能后该如何切入分析,一般 Traceview 可以定位两类性能问题:


  • 方法调运一次需要耗费很长时间导致卡顿;

  • 方法调运一次耗时不长,但被频繁调运导致累计时长卡顿。


譬如我们来举个实例,有时候我们写完 App 在使用时不觉得有啥大的影响,但是当我们启动完 App 后静止在那却十分费电或者导致设备发热,这种情况我们就可以打开 Traceview 然后按照 Cpu Time/Call 或者 Real Time/Call 进行降序排列,然后打开可疑的方法及其 child 进行分析查看,然后再回到代码定位检查逻辑优化即可;当然了,我们也可以通过该工具来 trace 我们自定义 View 的一些方法来权衡性能问题,这里不再一一列举喽。


可以看见,Traceview 能够帮助我们分析程序性能,已经很方便了,然而 Traceview 家族还有一个更加直观强大的小工具,那就是可以通过 dmtracedump 生成方法调用图。具体做法如下:


dmtracedump -g result.png target.trace //结果 png 文件 目标 trace 文件


通过这个生成的方法调运图我们可以更加直观的发现一些方法的调运异常现象。不过本人优化到现在还没怎么用到它,每次用到 Traceview 分析就已经搞定问题了,所以说 dmtracedump 自己酌情使用吧。


PS 一句,Android Studio 新版本除过 DDMS 以外在 CPU 视图的左侧已经集成了 Traceview(start Method Tracing)功能,只是用起来还是没有 DDMS 的方便实用([这里有一篇 AS MT 个人觉得不错的分析文章(引用自网络,链接属于原作者功劳)](


)),如下图:


2-3-7 使用 Systrace 进行分析优化

Systrace 其实有些类似 Traceview,它是对整个系统进行分析(同一时间轴包含应用及 SurfaceFlinger、WindowManagerService 等模块、服务运行信息),不过这个工具需要你的设备内核支持 trace(命令行检查/sys/kernel/debug/tracing)且设备是 eng 或 userdebug 版本才可以,所以使用前麻烦自己确认一下。


我们在分析 UI 性能时一般只关注图形性能(所以必须选择 Graphics 和 View,其他随意),同时一般对于卡顿的抓取都是 5s,最多 10s。启动 Systrace 进行数据抓取可以通过两种方式,命令行方式如下:


python systrace.py --time=10 -o mynewtrace.html sched gfx view wm


图形模式:


打开 DDMS->Capture system wide trace using Android systrace->设置时间与选项点击 OK 就开始了抓取,接着操作 APP,完事生成一个 trace.html 文件,用 Chrome 打开即可如下图:



在 Chrome 中浏览分析该文件我们可以通过键盘的 W-A-S-D 键来搞定,由于上面我们在进行 trace 时选择了一些选项,所以上图生成了左上方相关的 CPU 频率、负载、状态等信息,其中的 CPU N 代表了 CPU 核数,每个 CPU 行的柱状图表代表了当前时间段当前核上的运行信息;下面我们再来看看 SurfaceFlinger 的解释,如下:



可以看见上面左边栏的 SurfaceFlinger 其实就是负责绘制 Android 程序 UI 的服务,所以 SurfaceFlinger 能反应出整体绘制情况,可以关注上图 VSYNC-app 一行可以发现前 5s 多基本都能够达到 16ms 刷新间隔,5s 多开始到 7s 多大于了 15ms,说明此时存在绘制丢帧卡顿;同时可以发现 surfaceflinger 一行明显存在类似不规律间隔,这是因为有的地方是不需要重新渲染 UI,所以有大范围不规律,有的是因为阻塞导致不规律,明显可以发现 0 到 4s 间大多是不需要渲染,而 5s 以后大多是阻塞导致;对应这个时间点我们放大可以看到每个部分所使用的时间和正在执行的任务,具体如下:



可以发现具体的执行明显存在超时性能卡顿(原点不是绿色的基本都代表存在一定问题,下面和右侧都会提示你选择的帧相关详细信息或者 alert 信息),但是遗憾的是通过 Systrace 只能大体上发现是否存在性能问题,具体问题还需要通过 Traceview 或者代码中嵌入 Trace 工具类等去继续详细分析,总之很蛋疼。


PS:如果你想使用 Systrace 很轻松的分析定位所有问题,看明白所有的行含义,你还需要具备非常扎实的 Android 系统框架的原理才可以将该工具使用的得心应手。

2-3-8 使用 traces.txt 文件进行 ANR 分析优化

ANR(Application Not Responding)是 Android 中 AMS 与 WMS 监测应用响应超时的表现;之所以把臭名昭著的 ANR 单独作为 UI 性能卡顿的分析来说明是因为 ANR 是直接卡死 UI 不动且必须要解掉的 Bug,我们必须尽量在开发时避免他的出现,当然了,万一出现了那就用下面介绍的方法来分析吧。


我们应用开发中常见的 ANR 主要有如下几类:


  • 按键触摸事件派发超时 ANR,一般阈值为 5s(设置中开启 ANR 弹窗,默认有事件派发才会触发弹框 ANR);

  • 广播阻塞 ANR,一般阈值为 10s(设置中开启 ANR 弹窗,默认不弹框,只有 log 提示);

  • 服务超时 ANR,一般阈值为 20s(设置中开启 ANR 弹窗,默认不弹框,只有 log 提示);


当 ANR 发生时除过 logcat 可以看见的 log 以外我们还可以在系统指定目录下找到 traces 文件或 dropbox 文件进行分析,发生 ANR 后我们可以通过如下命令得到 ANR trace 文件:


adb pull /data/anr/traces.txt ./


然后我们用 txt 编辑器打开可以发现如下结构分析:


//显示进程 id、ANR 发生时间点、ANR 发生进程包名


----- pid 19073 at 2015-10-08 17:24:38 -----


Cmd line: com.example.yanbo.myapplication


//一些 GC 等 object 信息,通常可以忽略


......


//ANR 方法堆栈打印信息!重点!


DALVIK THREADS (18):


"main" prio=5 tid=1 Sleeping


| group="main" sCount=1 dsCount=0 obj=0x7497dfb8 self=0x7f9d09a000


| sysTid=19073 nice=0 cgrp=default sched=0/0 handle=0x7fa106c0a8


| state=S schedstat=( 125271779 68162762 280 ) utm=11 stm=1 core=0 HZ=100


| stack=0x7fe90d3000-0x7fe90d5000 stackSize=8MB


| held mutexes=


at java.lang.Thread.sleep!(Native method)


  • sleeping on <0x0a2ae345> (a java.lang.Object)


at java.lang.Thread.sleep(Thread.java:1031)


  • locked <0x0a2ae345> (a java.lang.Object)


//真正导致 ANR 的问题点,可以发现是 onClick 中有 sleep 导致。我们平时可以类比分析即可,这里不详细说明。


at java.lang.Thread.sleep(Thread.java:985)


at com.example.yanbo.myapplication.MainActivity$1.onClick(MainActivity.java:21)


at android.view.View.performClick(View.java:4908)


at android.view.View$PerformClick.run(View.java:20389)


at android.os.Handler.handleCallback(Handler.java:815)


at android.os.Handler.dispatchMessage(Handler.java:104)


at android.os.Looper.loop(Looper.java:194)


at android.app.ActivityThread.main(ActivityThread.java:5743)


at java.lang.reflect.Method.invoke!(Native method)


at java.lang.reflect.Method.invoke(Method.java:372)


at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)


at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)


......


//省略一些不常关注堆栈打印


......


至此常见的应用开发中 ANR 分析定位就可以解决了。


2-4 应用 UI 性能分析解决总结




可以看见,关于 Android UI 卡顿的性能分析还是有很多工具的,上面只是介绍了应用开发中我们经常使用的一些而已,还有一些其他的,譬如 Oprofile 等工具不怎么常用,这里就不再详细介绍。


通过上面 UI 性能的原理、原因、工具分析总结可以发现,我们在开发应用时一定要时刻重视性能问题,如若真的没留意出现了性能问题,不妨使用上面的一些案例方式进行分析。但是那终归是补救措施,在我们知道上面 UI 卡顿原理之后我们应该尽量从项目代码架构搭建及编写时就避免一些 UI 性能问题,具体项目中常见的注意事项如下:


  • 布局优化;尽量使用 include、merge、ViewStub 标签,尽量不存在冗余嵌套及过于复杂布局(譬如 10 层就会直接异常),尽量使用 GONE 替换 INVISIBLE,使用 weight 后尽量将 width 和 heigh 设置为 0dp 减少运算,Item 存在非常复杂的嵌套时考虑使用自定义 Item View 来取代,减少 measure 与 layout 次数等。

  • 列表及 Adapter 优化;尽量复用 getView 方法中的相关 View,不重复获取实例导致卡顿,列表尽量在滑动过程中不进行 UI 元素刷新等。

  • 背景和图片等内存分配优化;尽量减少不必要的背景设置,图片尽量压缩处理显示,尽量避免频繁内存抖动等问题出现。

  • 自定义 View 等绘图与布局优化;尽量避免在 draw、measure、layout 中做过于耗时及耗内存操作,尤其是 draw 方法中,尽量减少 draw、measure、layout 等执行次数。

  • 避免 ANR,不要在 UI 线程中做耗时操作,遵守 ANR 规避守则,譬如多次数据库操作等。


当然了,上面只是列出了我们项目中常见的一些 UI 性能注意事项而已,相信还有很多其他的情况这里没有说到,欢迎补充。还有一点就是我们上面所谓的 UI 性能优化分析总结等都是建议性的,因为性能这个问题是一个涉及面很广很泛的问题,有些优化不是必需的,有些优化是必需的,有些优化掉以后又是得不偿失的,所以我们一般着手解决那些必须的就可以了。


**【工匠若水 [http://blog.csdn.net/yanbober](


) 转载请注明出处。[点我开始 Android 技术交流](


)】**


3 应用开发 Memory 内存性能分析优化


========================


说完了应用开发中的 UI 性能问题后我们就该来关注应用开发中的另一个重要、严重、非常重要的性能问题了,那就是内存性能优化分析。Android 其实就是嵌入式设备,嵌入式设备核心关注点之一就是内存资源;有人说现在的设备都在堆硬件配置(譬如国产某米的某兔跑分手机、盒子等),所以内存不会再像以前那么紧张了,其实这句话听着没错,但为啥再牛逼配置的 Android 设备上有些应用还是越用系统越卡呢?这里面的原因有很多,不过相信有了这一章下面的内容分析,作为一个移动开发者的你就有能力打理好自己应用的那一亩三分地内存了,能做到这样就足以了。关于 Android 内存优化,这里有一篇 Google 的[官方指导文档](


),但是本文为自己项目摸索,会有很多不一样的地方。


3-1 Android 内存管理原理




系统级内存管理:


Android 系统内核是基于 Linux,所以说 Android 的内存管理其实也是 Linux 的升级版而已。Linux 在进程停止后就结束该进程,而 Android 把这些停止的进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台 service 与 Thread 等单独 UI 线程的进程)不会影响整体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载。


再直白点就是说 Android 系统级内存管理机制其实类似于 Java 的垃圾回收机制,这下明白了吧;在 Android 系统中框架会定义如下几类进程、在系统内存达到规定的不同 level 阈值时触发清空不同 level 的进程类型。



可以看见,所谓的我们的 Service 在后台跑着跑着挂了,或者盒子上有些大型游戏启动起来就挂(之前我在上家公司做盒子时遇见过),有一个直接的原因就是这个阈值定义的太大,导致系统一直认为已经达到阈值,所以进行优先清除了符合类型的进程。所以说,该阈值的设定是有一些讲究的,额,扯多了,我们主要是针对应用层内存分析的,系统级内存回收了解这些就基本够解释我们应用在设备上的一些表现特征了。


应用级内存管理:


在说应用级别内存管理原理时大家先想一个问题,假设有一个内存为 1G 的 Android 设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把 1G 内存吃光然后整个系统运行瘫痪呢?


哈哈,其实 Google 的工程师才不会这么傻的把系统设计这么差劲。为了使系统不存在我们上面假想情况且能安全快速的运行,Android 的框架使得每个应用程序都运行在单独的进程中(这些应用进程都是由 Zygote 进程孵化出来的,每个应用进程都对应自己唯一的虚拟机实例);如果应用在运行时再存在上面假想的情况,那么瘫痪的只会是自己的进程,不会直接影响系统运行及其他进程运行。


既然每个 Android 应用程序都执行在自己的虚拟机中,那了解 Java 的一定明白,每个虚拟机必定会有堆内存阈值限制(值得一提的是这个阈值一般都由厂商依据硬件配置及设备特性自己设定,没有统一标准,可以为 64M,也可以为 128M 等;它的配置是在 Android 的属性系统的/system/build.prop 中配置 dalvik.vm.heapsize=128m 即可,若存在 dalvik.vm.heapstartsize 则表示初始申请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才可以正常运行。


接着我们运行的 App 在自己的虚拟机中内存管理基本就是遵循 Java 的内存管理机制了,系统在特定的情况下主动进行垃圾回收。但是要注意的一点就是在 Android 系统中执行垃圾回收(GC)操作时所有线程(包含 UI 线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些 GC 垃圾回收一般都会有明显的 log 打印出回收类型,常见的如下:


  • GC_MALLOC——内存分配失败时触发;

  • GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发;

  • GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;

  • GC_EXTERNAL_ALLOC——外部内存分配失败时触发;


通过上面这几点的分析可以发现,应用的内存管理其实就是一个萝卜一个坑,坑都一般大,你在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,否则就装不下了。


3-2 Android 内存泄露性能分析




有了关于 Android 的一些内存认识,接着我们来看看关于 Android 应用开发中常出现的一种内存问题—-内存泄露。

3-2-1 Android 应用内存泄露概念

众所周知,在 Java 中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;但是,如果在对象的生命周期本来该被垃圾回收时这个对象还被别的对象所持有引用,那就会导致内存泄漏;这样的后果就是随着我们的应用被长时间使用,他所占用的内存越来越大。如下就是一个最常见简单的泄露例子(其它的泄露不再一一列举了):


public final class MainActivity extends Activity {


private DbManager mDbManager;


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


//DbManager 是一个单例模式类,这样就持有了 MainActivity 引用,导致泄露


mDbManager = DbManager.getInstance(this);


}


}


可以看见,上面例子中我们让一个单例模式的对象持有了当前 Activity 的强引用,那在当前 Acvitivy 执行完 onDestroy()后,这个 Activity 就无法得到垃圾回收,也就造成了内存泄露。


内存泄露可以引发很多的问题,常见的内存泄露导致问题如下:


  • 应用卡顿,响应速度慢(内存占用高时 JVM 虚拟机会频繁触发 GC);

  • 应用被从后台进程干为空进程(上面系统内存原理有介绍,也就是超过了阈值);

  • 应用莫名的崩溃(上面应用内存原理有介绍,也就是超过了阈值 OOM);


造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;可以发现,应用内存泄露是个相当棘手重要的问题,我们必须重视。

3-2-2 Android 应用内存泄露察觉手段

知道了内存泄露的概念之后肯定就是想办法来确认自己的项目是否存在内存泄露了,那该如何察觉自己项目是否存在内存泄露呢?如下提供了几种常用的方式:


| 察觉方式 | 场景 |


| --- | --- |


| AS 的 Memory 窗口 | 平时用来直观了解自己应用的全局内存情况,大的泄露才能有感知。 |


| DDMS-Heap 内存监测工具 | 同上,大的泄露才能有感知。 |


| dumpsys meminfo 命令 | 常用方式,可以很直观的察觉一些泄露,但不全面且常规足够用。 |


| leakcanary 神器 | 比较强大,可以感知泄露且定位泄露;实质是 MAT 原理,只是更加自动化了,当现有代码量已经庞大成型,且无法很快察觉掌控全局代码时极力推荐;或者是偶现泄露的情况下极力推荐。 |


AS 的 Memory 窗口如下,详细的说明这里就不解释了,很简单很直观(使用频率高):



DDMS-Heap 内存监测工具窗口如下,详细的说明这里就不解释了,很简单(使用频率不高):



dumpsys meminfo 命令如下(使用频率非常高,非常高效,我的最爱之一,平时一般关注几个重要的 Object 个数即可判断一般的泄露;当然了,adb shell dumpsys meminfo 不跟参数直接展示系统所有内存状态):



leakcanary 神器使用这里先不说,下文会专题介绍,你会震撼的一 B。有了这些工具的定位我们就能很方便的察觉我们 App 的内存泄露问题,察觉到以后该怎么定位分析呢,继续往下看。

3-2-3 Android 应用内存泄露 leakcanary 工具定位分析

leakcanary 是一个开源项目,一个内存泄露自动检测工具,是著名的 GitHub 开源组织 Square 贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些 bug,不过正常使用百分之九十情况是 OK 的,其核心原理与 MAT 工具类似。


关于 leakcanary 工具的配置使用方式这里不再详细介绍,因为真的很简单,[详情点我参考官方教程学习使用即可](


)。


PS:之前在优化性能时发现我们有一个应用有两个界面退出后 Activity 没有被回收(dumpsys meminfo 发现一直在加),所以就怀疑可能存在内存泄露。但是问题来了,这两个 Activity 的逻辑十分复杂,代码也不是我写的,相关联的代码量也十分庞大,更加郁闷的是很难判断是哪个版本修改导致的,这时候只知道有泄露,却无法定位具体原因,使用 MAT 分析解决掉了一个可疑泄露后发现泄露又变成了概率性的。可以发现,对于这种概率性的泄露用 MAT 去主动抓取肯定是很耗时耗力的,所以决定直接引入 leakcanary 神器来检测项目,后来很快就彻底解决了项目中所有必现的、偶现的内存泄露。


总之一点,工具再强大也只是帮我们定位可能的泄露点,而最核心的 GC ROOT 泄露信息推导出泄露问题及如何解决还是需要你把住代码逻辑及泄露核心概念去推理解决。

3-2-4 Android 应用内存泄露 MAT 工具定位分析

Eclipse Memory Analysis Tools([点我下载](


))是一个专门分析 Java 堆数据内存引用的工具,我们可以使用它方便的定位内存泄露原因,核心任务就是找到 GC ROOT 位置即可,哎呀,关于这个工具的使用我是真的不想说了,自己搜索吧,实在简单、传统的不行了。


PS:这是开发中使用频率非常高的一个工具之一,麻烦务必掌握其核心使用技巧,虽然 Android Studio 已经实现了部分功能,但是真的很难用,遇到问题目前还是使用 Eclipse Memory Analysis Tools 吧。


原谅我该小节的放荡不羁!!!!(其实我是困了,呜呜!)

3-2-5 Android 应用开发规避内存泄露建议

有了上面的原理及案例处理其实还不够,因为上面这些处理办法是补救的措施,我们正确的做法应该是在开发过程中就养成良好的习惯和敏锐的嗅觉才对,所以下面给出一些应用开发中常见的规避内存泄露建议:


  • Context 使用不当造成内存泄露;不要对一个 Activity Context 保持长生命周期的引用(譬如上面概念部分给出的示例)。尽量在一切可以使用应用 ApplicationContext 代替 Context 的地方进行替换(原理我前面有一篇关于 Context 的文章有解释)。

  • 非静态内部类的静态实例容易造成内存泄漏;即一个类中如果你不能够控制它其中内部类的生命周期(譬如 Activity 中的一些特殊 Handler 等),则尽量使用静态类和弱引用来处理(譬如 ViewRoot 的实现)。

  • 警惕线程未终止造成的内存泄露;譬如在 Activity 中关联了一个生命周期超过 Activity 的 Thread,在退出 Activity 时切记结束线程。一个典型的例子就是 HandlerThread 的 run 方法是一个死循环,它不会自己结束,线程的生命周期超过了 Activity 生命周期,我们必须手动在 Activity 的销毁方法中中调运 thread.getLooper().quit();才不会泄露。

  • 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。

  • 创建与关闭没有成对出现造成的泄露;譬如 Cursor 资源必须手动关闭,WebView 必须手动销毁,流等对象必须手动关闭等。

  • 不要在执行频率很高的方法或者循环中创建对象,可以使用 HashTable 等创建一组对象容器从容器中取那些对象,而不用每次 new 与释放。

  • 避免代码设计模式的错误造成内存泄露。


关于规避内存泄露上面我只是列出了我在项目中经常遇见的一些情况而已,肯定不全面,欢迎拍砖!当然了,只有我们做到好的规避加上强有力的判断嗅觉泄露才能让我们的应用驾驭好自己的一亩三分地。


3-3 Android 内存溢出 OOM 性能分析




上面谈论了 Android 应用开发的内存泄露,下面谈谈内存溢出(OOM);其实可以认为内存溢出与内存泄露是交集关系,具体如下图:



下面我们就来看看内存溢出(OOM)相关的东东吧。

3-3-1 Android 应用内存溢出 OOM 概念

上面我们探讨了 Android 内存管理和应用开发中的内存泄露问题,可以知道内存泄露一般影响就是导致应用卡顿,但是极端的影响是使应用挂掉。前面也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,这里我们就来看看这个问题—–内存溢出(OOM–OutOfMemoryError)。


内存溢出的主要导致原因有如下几类:


  • 应用代码存在内存泄露,长时间积累无法释放导致 OOM;

  • 应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值 OOM;


可以发现,无论哪种类型,导致内存溢出(OutOfMemoryError)的核心原因就是应用的内存超过阈值了。

3-3-2 Android 应用内存溢出 OOM 性能分析

通过上面的 OOM 概念和那幅交集图可以发现,要想分析 OOM 原因和避免 OOM 需要分两种情况考虑,泄露导致的 OOM,申请过大导致的 OOM。


内存泄露导致的 OOM 分析:


这种 OOM 一旦发生后会在 logcat 中打印相关 OutOfMemoryError 的异常栈信息,不过你别高兴太早,这种情况下导致的 OOM 打印异常信息是没有太大作用,因为这种 OOM 的导致一般都如下图情况(图示为了说明问题数据和场景有夸张,请忽略):



从图片可以看见,这种 OOM 我们有时也遇到,第一反应是去分析 OOM 异常打印栈,可是后来发现打印栈打印的地方没有啥问题,没有可优化的余地了,于是就郁闷了。其实这时候你留心观察几个现象即可,如下:


  • 留意你执行触发 OOM 操作前的界面是否有卡顿或者比较密集的 GC 打印;

  • 使用命令查看下当前应用占用内存情况;


确认了以上这些现象你基本可以断定该 OOM 的 log 真的没用,真正导致问题的原因是内存泄露,所以我们应该按照上节介绍的方式去着手排查内存泄露问题,解决掉内存泄露后红色空间都能得到释放,再去显示一张 0.8M 的优化图片就不会再报 OOM 异常了。


不珍惜内存导致的 OOM 分析:


上面说了内存泄露导致的 OOM 异常,下面我们再来看一幅图(数据和场景描述有夸张,请忽略),如下:



可见,这种类型的 OOM 就很好定位原因了,一般都可以从 OOM 后的 log 中得出分析定位。


如下例子,我们在 Activity 中的 ImageView 放置一张未优化的特大的(30 多 M)高清图片,运行直接崩溃如下:


//抛出 OOM 异常


10-10 09:01:04.873 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"


10-10 09:01:04.940 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"


//堆栈打印


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: FATAL EXCEPTION: main


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Process: com.example.application, PID: 11703


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.application/com.example.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2610)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2684)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.access$800(ActivityThread.java:177)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1542)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:111)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.os.Looper.loop(Looper.java:194)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:5743)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at java.lang.reflect.Method.invoke(Method.java:372)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)


//出错地点,原因是 21 行的 ImageView 设置的 src 是一张未优化的 31M 的高清图片


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>


10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.view.LayoutInflater.createView(LayoutInflater.java:633)


通过上面的 log 可以很方便的看出来问题原因所在地,那接下来的做法就是优化呗,降低图片的相关规格即可(譬如使用 BitmapFactory 的 Option 类操作等)。


PS:提醒一句的是记得应用所属的内存是区分 Java 堆和 native 堆的!

3-3-3 Android 应用规避内存溢出 OOM 建议

还是那句话,等待 OOM 发生是为时已晚的事,我们应该将其扼杀于萌芽之中,至于如何在开发中规避 OOM,如下给出一些我们应用开发中的常用的策略建议:


  • 时刻记得不要加载过大的 Bitmap 对象;譬如对于类似图片加载我们要通过 BitmapFactory.Options 设置图片的一些采样比率和复用等,具体做法[点我参考官方文档](


),不过过我们一般都用 fresco 或 Glide 开源库进行加载。


  • 优化界面交互过程中频繁的内存使用;譬如在列表等操作中只加载可见区域的 Bitmap、滑动时不加载、停止滑动后再开始加载。

  • 有些地方避免使用强引用,替换为弱引用等操作。

  • 避免各种内存泄露的存在导致 OOM。

  • 对批量加载等操作进行缓存设计,譬如列表图片显示,Adapter 的 convertView 缓存等。

  • 尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用 style 等资源达到节约内存。

  • 对于有缓存等存在的应用尽量实现 onLowMemory()和 onTrimMemory()方法。

  • 尽量使用线程池替代多线程操作,这样可以节约内存及 CPU 占用率。

  • 尽量管理好自己的 Service、Thread 等后台的生命周期,不要浪费内存占用。

  • 尽可能的不要使用依赖注入,中看不中用。

  • 尽量在做一些大内存分配等可疑内存操作时进行 try catch 操作,避免不必要的应用闪退。

  • 尽量的优化自己的代码,减少冗余,进行编译打包等优化对齐处理,避免类加载时浪费内存。


可以发现,上面只是列出了我们开发中常见的导致 OOM 异常的一些规避原则,还有很多相信还没有列出来,大家可以自行追加参考即可。


3-4 Android 内存性能优化总结




无论是什么电子设备的开发,内存问题永远都是一个很深奥、无底洞的话题,上面的这些内存分析建议也单单只是 Android 应用开发中一些常见的场景而已,真正的达到合理的优化还是需要很多知识和功底的。


合理的应用架构设计、设计风格选择、开源 Lib 选择、代码逻辑规范等都会决定到应用的内存性能,我们必须时刻头脑清醒的意识到这些问题潜在的风险与优劣,因为内存优化必须要有一个度,不能一味的优化,亦不能置之不理。


**【工匠若水 [http://blog.csdn.net/yanbober](


) 转载请注明出处。[点我开始 Android 技术交流](


)】**


4 Android 应用 API 使用及代码逻辑性能分析


=============================


在我们开发中除过常规的那些经典 UI、内存性能问题外其实还存在很多潜在的性能优化、这种优化不是十分明显,但是在某些场景下却是非常有必要的,所以我们简单列举一些常见的其他潜在性能优化技巧,具体如下探讨。


4-1 Android 应用 String/StringBuilder/StringBuffer 优化建议




字符串操作在 Android 应用开发中是十分常见的操作,也就是这个最简单的字符串操作却也暗藏很多潜在的性能问题,下面我们实例来说说。


先看下面这个关于 String 和 StringBuffer 的对比例子:


//性能差的实现


String str1 = "Name:";


String str2 = "GJRS";


String Str = str1 + str2;


//性能好的实现


String str1 = "Name:";


String str2 = "GJRS";


StringBuffer str = new StringBuilder().append(str1).append(str2);


通过这个例子可以看出来,String 对象(记得是对象,不是常量)和 StringBuffer 对象的主要性能区别在于 String 对象是不可变的,所以每次对 String 对象做改变操作(譬如“+”操作)时其实都生成了新的 String 对象实例,所以会导致内存消耗性能问题;而 StringBuffer 对象做改变操作每次都会对自己进行操作,所以不需要消耗额外的内存空间。


我们再看一个关于 String 和 StringBuffer 的对比例子:


//性能差的实现


StringBuffer str = new StringBuilder().append("Name:").append("GJRS");


//性能好的实现


String Str = "Name:" + "GJRS";


在这种情况下你会发现 StringBuffer 的性能反而没有 String 的好,原因是在 JVM 解释时认为


String Str = "Name:" + "GJRS";就是String Str = "Name:GJRS";,所以自然比 StringBuffer 快了。


可以发现,如果我们拼接的是字符串常量则 String 效率比 StringBuffer 高,如果拼接的是字符串对象,则 StringBuffer 比 String 效率高,我们在开发中要酌情选择。当然,除过注意 StringBuffer 和 String 的效率问题,我们还应该注意另一个问题,那就是 StringBuffer 和 StringBuilder 的区别,其实 StringBuffer 和 StringBuilder 都继承自同一个父类,只是 StringBuffer 是线程安全的,也就是说在不考虑多线程情况下 StringBuilder 的性能又比 StringBuffer 高。


PS:如果想追究清楚他们之间具体细节差异,麻烦自己查看实现源码即可。


4-2 Android 应用 OnTrimMemory()实现性能建议




OnTrimMemory 是 Android 4.0 之后加入的一个回调方法,作用是通知应用在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验(冷启动速度是热启动的 2~3 倍)。系统会根据当前不同等级的内存使用情况调用这个方法,并且传入当前内存等级,这个等级有很多种,我们可以依据情况实现不同的等级,这里不详细介绍,但是要说的是我们应用应该至少实现如下等级:


  • TRIM_MEMORY_BACKGROUND


内存已经很低了,系统准备开始根据 LRU 缓存来清理进程。这时候如果我们手动释放一些不重要的缓存资源,则当用户返回我们应用时会感觉到很顺畅,而不是重新启动应用。


可以实现 OnTrimMemory 方法的系统组件有 Application、Activity、Fragement、


Service、ContentProvider;关于 OnTrimMemory 释放哪些内存其实在架构阶段就要考虑清楚哪些对象是要常驻内存的,哪些是伴随组件周期存在的,一般需要释放的都是缓存。


如下给出一个我们项目中常用的例子:


@Override


public void onTrimMemory(int level) {


if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {


clearCache();


}


}


通常在我们代码实现了 onTrimMemory 后很难复显这种内存消耗场景,但是你又怕引入新 Bug,想想办法测试。好在我们有一个快捷的方式来模拟触发该水平内存释放,如下命令:


adb shell dumpsys gfxinfo packagename -cmd trim value


packagename 为包名或者进程 id,value 为 ComponentCallbacks2.java 里面定义的值,可以为 80、60、40、20、5 等,我们模拟触发其中的等级即可。


4-3 Android 应用 HashMap 与 ArrayMap 及 SparseArray 优化建议




在 Android 开发中涉及到数据逻辑部分大部分用的都是 Java 的 API(譬如 HashMap),但是对于 Android 设备来说有些 Java 的 API 并不适合,可能会导致系统性能下降,好在 Google 团队已经意识到这些问题,所以他们针对 Android 设备对 Java 的一些 API 进行了优化,优化最多就是使用了 ArrayMap 及 SparseArray 替代 HashMap 来获得性能提升。


HashMap:


HashMap 内部使用一个默认容量为 16 的数组来存储数据,数组中每一个元素存放一个链表的头结点,其实整个 HashMap 内部结构就是一个哈希表的拉链结构。HashMap 默认实现的扩容是以 2 倍增加,且获取一个节点采用了遍历法,所以相对来说无论从内存消耗还是节点查找上都是十分昂贵的。


SparseArray:


SparseArray 比 HashMap 省内存是因为它避免了对 Key 进行自动装箱(int 转 Integer),它内部是用两个数组来进行数据存储的(一个存 Key,一个存 Value),它内部对数据采用了压缩方式来表示稀疏数组数据,从而节约内存空间,而且其查找节点的实现采用了二分法,很明显可以看见性能的提升。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android应用开发性能优化完全分析,flutter蓝牙开发