74

性能优化全方面解析

 6 years ago
source link: https://mp.weixin.qq.com/s/mb3PqLN7PLgnzDYlAevNBg
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

性能优化全方面解析

快进阶的 码农职场 2017-12-15 00:00 Posted on



阅读文本大概需要 8.8 分钟。

  目的

公司的新需求终于解决完了,离测试和发布还有段时间,第一次体验了下没需求没bug的感觉,真是舒爽~然后翻了翻有什么可以学的。无意翻到了Android后期发展的五大趋势。一、性能优化。二、高级UI。三、JNI/NDK开发。四、架构师。五、RN开发。这也许将会是我的进阶趋势。早已知道在瓶颈期的我,似乎看到了突破的希望的。初级进阶中级也好,中级进阶高级也罢,现在的市场无非是根据经验规定的,根据能力的少之又少。

其实,关注我的或者在群里的小伙伴也知道,UI那块我问题不大。但是高级UI就有难度了。我们先不管他,一个一个来。先从性能优化来。其实我是拒绝写这篇文章的。为什么?性能优化的分类很多,一个分类写一篇感觉篇幅量很小,结合在一起写有感觉很大。而我目前打算整体的整理一下。

那么我们先分析下性能优化有那几个方面:一、内存优化。二、UI优化(布局优化和绘制优化)。三、速度的优化(线程优化/网络优化)。四、电量优化。五、启动优化。应该就这些了。那么这只是五大方面,里面还结合了各种细节方面的。不急,我们下面一个个的介绍。

内存优化

关于性能优化我们可以不知道其他的,但一定要知道内存优化。因为内存泄漏可以Android的常客。那么什么是内存泄漏呢?内存不在GC的掌控范围之内了。那么java的GC内存回收机制是什么?某对象不在有任何引用的时候才会进行回收。那么GC回收机制的原理是什么?又或者说可以作为GC Root引用点的是啥?或许有人听不懂我在讲啥。我们先来看张图。

当我们向上寻找,一直寻找到GC Root的时候,此对象不会进行回收,例如,一个Activity。那么如果我们向上寻找,直到找到GC Root对象的时候,就说明它是不可以回收的,例如,我定义了一个int a;但是这个数据,我整个页面或者说整个项目都没有用到,则这个对象会被GC掉。

GC的引用点

  1. java栈中引用的对象

  2. 方法静态引用的对象

  3. 方法常量引用的对象

  4. Native中JNI引用的对象

  5. Thread——“活着的”线程

如何判断

那么我们如何判断一个对象是一个垃圾对象,可以讲他进行回收呢?举了小例子教你们如何区分:

一般在学校吃饭,我们有两种情况,第一:吃完饭就直接走人,碗筷留给阿姨来收拾处理。

第二:吃完之后把碗筷放到收盘处直接进行回收。

但我们是个有素质的人,一般采用第二种情况,但根据想法,我们更倾向于第一种。

那么一般在饭店或者KFC中,都是第一种情况。

那么此时,问题来了,如果我已经吃完饭,然后我并没有离开饭店,做在位置上和朋友吹吹牛逼,谈谈理想,聊聊人生。

那么桌上那一堆碗筷是收还是不收?讲道理是不能收的。虽然实际也是不能收的。因为顾客是上帝~~~

So,我们如何判断一个对象是一个可回收的垃圾对象呢?这是我们的一个主观的判断。但是有种情况我们是必须要考虑到的,没错,就是内存过多无法释放的时候,会直接导致OOM。整个项目boom炸了。什么鬼?outofmemory。没错就是它。

内存溢出

分析原因

我们需要分析内存溢出的原因,我们先来看一张图: 

内存泄漏一般导致应用卡顿,极端情况会导致项目boom。Boom的原因是因为超过内存的阈值。 
原因主要有两方面:

  • 代码存在泄漏,内存无法及时释放导致oom(这个我们后面说)

  • 一些逻辑消耗了大量内存,无法及时释放或者超过导致oom

所谓消耗大量的内存的,绝大多数是因为图片加载。这是我们oom出现最频繁的地方。我前面有写过图片加载的方法,一个是控制每次加载的数量,第二,保证每次滑动的时候不进行加载,滑动完进行加载。一般情况使用先进后出,而不是先进先出。不过一般我们图片加载都是使用fresco或者Glide等开源库。 
我们来看下下面两张图: 

对比两张图,我们可以在第一张的情况出现了oom情况,我们通过log打印发现,处理的好像没什么问题,换句话说,如果我不放那0.8M的图片。然后继续不停的操作同样会出现OOM,然而我们就蒙了。没什么图片加载怎么就这么崩掉了。

首先,我们确定我们项目或者某几个类里面是否存在内存溢出的问题。我们可以通过如下方法:

  • Android–>System Information–>MemoryUsage查看Object里面是否有没有被释放的Views和Activity

  • 命令行模式:adb shell dumpsys meminfo 包名 -d

就那我公司的项目举例把。首先,我们在这边可以看到memory。CPU和net的使用情况。 

我们找到Object。看看我们内存的消耗情况。 

我们可以通过看Memory Monitor工具。 检查一个一个的动作。(比如Activity的跳转)。反复多次执行某一个操作,不断的通过这个工具查看内存的大概变化情况。 前后两个内存变化增加了不少。

我们可以更仔细的查找泄漏的位置,在AS里面使用 Heap SnapShot工具(堆栈快照)。如图所示: 

我们点击后,他会进行一段时间的监控,然后会生成一个文件。我们点击我们package tree view。 

我们找到自己项目的包名。然后进行进一步的分析。首先看一下2个列表的列名到底指的什么。 

实例化对象的详细信息: 

我们来随便的看一下内存中的数量: 

这还是我们刚进手机,一个bean就被调用了这么多次。简直可怕。这个我们可以通过内存分析工具解决的。

内存分析工具

性能优化工具:

  • Heap SnapShot工具

  • Heap Viewer工具

  • LeakCanary工具

  • MAT工具

  • TraceView工具(Device Monitor)

第三方分析工具:

  • MemoryAnalyzer

  • GT Home

  • iTest 

    因为我没有这些工具,无法进行演示。

注意事项

  • 我们尽量不要使用Activity的上下文,而是使用application的上下文,因为application的生命周期长,进程退出时才会被销毁。所以,单例模式是最容易造成内存溢出的原本所在,因为单例模式的生命周期的应该和application的生命周期一样长,而不是和Activity的相同。

  • Animation也会导致内存溢出,为什么?因为我们是通过view来进行演示的,导致view被Activity持有,而Activity又持有view。最后因为Activity无法释放,导致内存泄漏。解决方法是在Activity的ondestory()方法中调用Animation.cancle()进行停止,当然一些简单的动画我们可以通过自定义view来解决。至少我现在已经很少使用Animation了。没有一个动画是自定义view解决不了的。如何有,那就是两个~~~。

UI优化

UI优化主要包括布局优化以及view的绘制优化。不急,我们接下来一个一个慢慢看~~。先说下UI的优化到底是什么?有些时候我们打开某个软件,会出现卡顿的情况。这就是UI的问题。那么我们想一下,什么情况会导致卡顿呢?一般是如下几种情况:

  1. 人为在UI线程中做轻微耗时操作,导致UI线程卡顿;

  2. 布局Layout过于复杂,无法在16ms内完成渲染;

  3. 同一时间动画执行的次数过多,导致CPU或GPU负载过重;

  4. View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;

  5. View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;

  6. 内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;

  7. 冗余资源及逻辑等导致加载和执行缓慢;

  8. 臭名昭著的ANR;

可以看见,上面这些导致卡顿的原因都是我们平时开发中非常常见的。有些人可能会觉得自己的应用用着还蛮OK的,其实那是因为你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的App你就会发现很多性能问题。

布局优化

GPU绘制

我们对于UI性能的优化还可以通过开发者选项中的GPU过度绘制工具来进行分析。在设置->开发者选项->调试GPU过度绘制(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面过度绘制进行分析): 

这图看着太乱,我们来一张简洁明了的图: 

我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。

可以发现,开启后在我们想要调试的应用界面中可以看到各种颜色的区域,具体含义如下: 

Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。

如果布局中既能采用RealtiveLayout和LinearLayout,那么直接使用LinearLayout,因为Relativelayout的布局比较复杂,绘制的时候需要花费更多的CPU时间。如果需要多个LinearLayout或者Framelayout嵌套,那么可采用Relativelayout。因为多层嵌套导致布局的绘制有大部分是重复的,这会减少程序的性能。

GPU呈现模式分析

我们依旧打开设置–>开发者选项–>GPU呈现模式分析–>在屏幕上显示为条形图,如图所示: 

当然,也可以在执行完UI滑动操作后在命令行输入如下命令查看命令行打印的GPU渲染数据(分析依据:Draw + Process + Execute = 完整的显示一帧时间 < 16ms):



adb shell dumpsys gfxinfo [应用包名]

随着界面的刷新,界面上会以实时柱状图来显示每帧的渲染时间,柱状图越高表示渲染时间越长,每个柱状图偏上都有一根代表16ms基准的绿色横线,每一条竖着的柱状线都包含三部分(蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间),只要我们每一帧的总时间低于基准线就不会发生UI卡顿问题(个别超出基准线其实也不算啥问题的)。就简单的看下我们公司项目刚启动的时候:  

突然就有那么一种想吐槽的感觉…..我记得之前我做了瘦身的优化,但是要让我做性能优化,我觉得应该没那么简单……..

代码优化

Android Studio和IntellJ idead都有自带的代码检查工具。打开Analyze->Run Inspection by Name… –>unused resource 点击开始检测,等待一下后会发现如下结果: 

我们还可以这样,将鼠标放在代码区点击右键->Analyze->Inspect Code–>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现如下结果: 

当然,我这只是截取了少一部分,我们看下下面那个提示:@param v tag description is missing 。意味着v的类型缺少了,要么补上介绍,要么直接删除。

上面那两种方法是最容易找到代码缺陷以及无用代码的地方。所以尽情的入坑去填坑把~~~

绘制优化

那么什么是绘制优化?绘制优化主要是指View的Ondraw方法需要避免执行大量的操作。我将分为了2个方面。

  • ondraw方法不需要创建新的局部对象,这是因为ondraw方法是实时执行的,这样会产品大量的临时对象,导致占用了更多内存,并且使系统不断的GC。降低了执行效率。

  • Ondraw方法不需要执行耗时操作,在ondraw方法里少使用循环,因为循环会占用CPU的时间。导致绘制不流畅,卡顿等等。Google官方指出,view的绘制帧率稳定在60dps,这要求每帧的绘制时间不超过16ms(1000/60)。虽然很难保证,但我们需要尽可能的降低。

60dps是目前最合适的图像显示速度,也是绝大部分Android设备设置的调试频率,如果在16ms内顺利完成界面刷新操作可以展示出流畅的画面,而由于任何原因导致接收到VSYNC信号的时候无法完成本次刷新操作,就会产生掉帧的现象,刷新帧率自然也就跟着下降(假定刷新帧率由正常的60fps降到30fps,用户就会明显感知到卡顿)。So,前面我们说GPU的时候也谈到了这个。总的而言,感觉还是蛮重要的…..

网络优化

线程是我们项目中不可缺少的重要部分,因为我们大多数数据都是从网络获取的。So,线程这个是必备用品。 
我们依旧可以通过Memory下面的Net进行网络的监听: 

ANR问题

相信这个问题在座的各种没少遇到过,那么什么是ANR?application not responding。应用程序无响应。那么一般什么时候会出现ANR。Android官方规定:activity如果5s内无响应事件(屏幕触摸事件或者键盘输入事件)。BroadcastReceiver如果在10s内无法处理完成。Service如果20s内无法处理完成。这三种情况会导致ANR。用张简洁的图来介绍把。看起来方便~~ 

线程优化

上面说的三种导致ANR的情况,绝大多数就是因为线程阻塞导致的。那么我们应该如何处理呢?Android系统为我们提供了若干组工具类来解决此问题。

  • Asynctask:为UI线程与工作线程之间进行快速处理的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的场景。

  • HandlerThread:为某些回调方法或者等待某些执行任务的执行设置一个专属的线程,并提供线程任务的调度机制。

  • ThreadPool:把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。

  • IntentService:适合执行由Ui触发的后台任务。并可以把这些任务执行的情况通过一定的机制反馈给UI。

网络请求耗时会给用户带来卡顿的产品体验,虽然可以使用Loading提升用户体验,但属于治标不治本。例如,当网络差的时候我们公司的项目一个loading就是10多s。甚至更多…..我就记得我当时面试之前下了一次我们公司的项目,因为网差的问题…一个loading一分多钟。。当时砸手机的冲动都有了,别说卸软件了….

一般多线程的情况我们可以通过Asynctask处理。(这玩意我真没怎么用过- -)我前面有说过annotation。这是google官方推出的注解。比bufferknife强大很多。这个可以快捷方便的处理多线程而且不会导致线程阻塞,而且你也可以控制线程的顺序,例如我要执行完线程A后,根据线程A的某个参数来执行线程B。以此类推…..

至于线程池么,最多的还是要说道图片加载了~~。图片加载用三方就行了~想看详细介绍,我前面有说,当然除了这个还有下载操作。这就和IntentService有关联了。一般下载我很少涉及到。。用过几次android原生的downloadmanager。。感觉略坑。

KO网络优化

现在讲网络优化的重点了…重点..重点…,一般用到网最最最主要的是什么?时间!!速度!!成功率!!,时间!!速度!!成功率!!,时间!!速度!!成功率!!重要的事说三遍哈。

这已经不是第一次在此文提到图片了。可见图片的重要性!!

  • 使用WebP格式;同样的照片,采用WebP格式可大幅节省流量,相对于JPG格式的图片,流量能节省将近 25% 到 35 %;相对于 PNG 格式的图片,流量可以节省将近80%。最重要的是使用WebP之后图片质量也没有改变。So,去和后台的小伙伴们商量吧~~~

  • 使用缩略图,我在前面写图片加载有说过,就是控制他的inside和option。然后进行图片缩放。压缩?讲道理….我并不知道网络图片怎么压缩,but,我会缩放啊~~反正也不会失真。啦啦啦~咬我啊?

网络请求处理

我们可以对服务端返回数据进行缓存,设定有效时间,有效时间之内不走网络请求,减少流量消耗。对网络的缓存可以参见HttpResponseCache。

在某些情况,我们尽量少使用GPS定位,如果条件允许,尽可能使用网络定位。

下载、上传,我们尽可能使用断点,说个简单的,我在公司,准备下一个500M的游戏,但是下到200M的时候我下班了,此时没有了无线网,我们可以回家后用无线继续下载。So,断点续传,断点下载也是我们的必修课~,所以我前面单独提了一篇断点续传的文章。

刷新数据时,尽可能使用局部刷新,而不是全局刷新,第一、界面会闪屏一下,网差的界面直接白屏一段时间也不是不可能。第二、流量的使用!!我又要拿我们公司项目搞事情了。一个闪屏的缓存60+M。。。没错,就是60+M。简直可怕,我清个3、5次缓存,在打开个3、5次。好了,2分钟时间,我一个月流量就没了。。。So,我前面提到的网络缓存很重要,至于会不会加在项目中,我还是要看了在说- - 一个不小心,整个项目炸了都有可能。。。

启动优化

众所周知,一个好的产品,除了功能强大,好的性能也必不可少。有调查显示,近50%的受访者因为apk太大而拒绝使用,近40%的受访者会因为APP性能差而卸载,性能也是造成APP用户沮丧的头号原因。

安卓应用的启动方式分为三种:冷启动、暖启动、热启动,不同的启动方式决定了应用UI对用户可见所需要花费的时间长短。顾名思义,冷启动消耗的时间最长。基于冷启动方式的优化工作也是最考验产品用户体验的地方。谈及优化之前,我们先看看这三种启动方式的应用场景,以及启动过程中系统都做了些什么工作。

冷启动

为什么说冷启动是耗时最长的。冷启动是在启动应用前,系统没有获取到当前app的activity、Service等等。例如,第一次启动app。又或者说杀死进程后第一次启动。那么对比其他两种方式。冷启动自然是耗时最久的。

应用发生冷启动时,系统一定会执行下面的三个任务:

  • 开始加载并启动应用

  • 应用启动后,显示一个空白的启动窗口(启动闪屏页)

  • 创建应用信息

那么创建应用信息,系统就需要做一屁股的事:

  • application的初始化

  • 启动UI线程

  • 创建Activity

  • 导入视图(inflate view)

  • 计算视图大小(onmesure view)

  • 得到视图排版(onlayout view)

  • 绘制视图(ondraw view)

这其中有两个 creation 工作,分别为 Application 和 Activity creation。他们均在 View 绘制展示之前。所以,在应用自定义的 Application 类和 第一个 Activity 类中,onCreate() 方法做的事情越多,冷启动消耗的时间越长。

暖启动

当应用中的 Activities 被销毁,但在内存中常驻时,应用的启动方式就会变为暖启动。相比冷启动,暖启动过程减少了对象初始化、布局加载等工作,启动时间更短。但启动时,系统依然会展示闪屏页,直到第一个 Activity 的内容呈现为止。

热启动

相比暖启动,热启动时应用做的工作更少,启动时间更短。热启动产生的场景很多,常见如:用户使用返回键退出应用,然后马上又重新启动应用。

我们先对比下三种启动的时间对比: 
冷启动: 

暖启动 : 

热启动: 

我们可以看到三者的明显的差距,一个冷启动将近一分钟,反正我是不想看,每次跑项目都好慢~那么我们应该怎么做?看到有些人介绍说改变项目的theme。把它改成launcher的theme。但我觉得,这种做测试的确没问题。但是一般项目都会有闪屏页。然后从闪屏跳转到首页。我们可以按照大多数的项目来改善。怎么说的,我们可以看到一般项目都有倒计时显示。也就是说倒计时结束就自动进入首页。或者可以直接跳过进入首页。也就是说我们可以通过此方法来进行,也就是说只要他倒计时结束,不管请求是否全部获取完我们都直接进入首页。我们可以在闪屏页进行一些必要的加载,例如用户信息,定位等等,那么至于其他的,我们可以进入主页进行预加载。就和热更新一样,在用户不知情的情况下,默默的更新bug。So,对于一些网络请求,例如广告之类的。我们可以通过此方法进行预加载。

我们还可以这样,闪屏页我们把他当作一个fragment嵌套在MainActivity中,那么我们可以在进入闪屏时直接预加载主页的view。倒计时我们把闪屏页remove掉直接显示首页。

通过上面的介绍,我们对启动优化有了一定的了解,其实总结的话很简单。就是减少耗时操作,总结如下:

  • 主线程中涉及到Shareperference能否在非UI线程执行。

  • Application的创建过程中尽量少的进行耗时操作。

  • 减少布局的层次,并且生命周期回调的方法中尽量减少耗时的操作。

电量优化

有了UI优化、内存优化、代码优化、网络优化之后我们在来说说应用开发中很重要的一个优化模块—–电量优化。

其实大多数开发者对电量优化的重视程度极低,其实提到性能优化想到的就是内存优化,但我们不能忽视其他的优化,电量优化其实还是必要的,例如爱奇艺、优酷等等的视频播放器以及音乐播放器。众所周知,音乐和视频其实是耗电量最大的。如果用户一旦发现我们的应用非常耗电,不好意思,他们大多会选择卸载来解决此类问题。为此,我们需要进行优化。

如何优化

其实我们把上面那四种优化解决了,就是最好的电量优化。So,对于电量优化,我在此提一些建议:

  • 需要进行网络请求时,我们需先判断网络当前的状态。

  • 在多网络请求的情况下,最好进行批量处理,尽量避免频繁的间隔网络请求。

  • 在同时有wifi和移动数据的情况下,我们应该直接屏幕移动数据的网络请求,只有当wifi断开时在调用,因为,wifi请求的耗电量远比移动数据的耗电量低的低。

  • 后台任务要尽可能少的唤醒CPU。(比方说,锁屏时,QQ的消息提示行就是唤醒了CPU。但是它的提示只有在你打开锁屏或者进行充电时才会进行提示。)

优化总结

性能优化是我们进阶的毕竟之路。So,我们必须要会,至于“会”到什么程度,就要看个人理解了。其实,上面介绍的只是性能问题的冰山一角,真正的优化,我们是在项目中总结出来的。但,我们不能一味的追求优化,就例如我,现在只是在进行优化的总结,而对于真正的实行,并没有开始,因为,优化是有风险的,一个不小心,整个项目都可能炸了。所以这就需要你的经验,以及各种总结,在改进行优化的地方先进行优化,看看效果如何,例如,UI的优化以及代码的优化。可以先拿一些网上的开源项目进行优化等等。最后,尽情的享受优化把~~~

《Android艺术探索》

Android应用开发性能优化完全分析

性能优化典范

双十二技术哥

google官方优化视频



iOS 赞赏通道

Image



代码人生,一飞冲天。

Image


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK