Android中的性能优分为以下几个方面:

  • 布局优化
  • 绘制优化
  • 网络优化
  • 安装包优化
  • 内存优化
  • 卡顿优化
  • 耗电优化
  • ListView/RecycleView及Bitmap/图片优化
  • 数据库SQLite优化
  • 启动优化
  • 数据结构优化
  • 稳定性优化

一.布局优化

在Android种系统对View进行测量、布局和绘制时,都是通过对View数的遍历来进行操作的。如果一个View数的高度太高就会严重影响测量、布局和绘制的速度。Google也在其API文档中建议View高度不宜超过10层。现在版本种Google使用RelativeLayout替代LineraLayout作为默认根布局,目的就是降低LineraLayout嵌套产生布局树的高度,从而提高UI渲染的效率。

布局优化的本质就是减少View的层级。常见的布局优化方案如下:

  • 在LinearLayout和RelativeLayout都可以完成布局的情况下优先选择LinearLayout,可以减少View的层级,但是注意相同组件可能RelativeLayout绘制时间长
  • 使用 < include > 标签将常用的布局组件共同的部分抽取出来,以便复用。
  • 通过 < ViewStub > 标签来加载不常用的布局,延迟加载(需要的时候在activity中加载出来)
  • 使用 < Merge > 标签来减少布局的嵌套层次

二.绘制优化:

绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方面:

1.onDraw中不要创建新的局部对象。

因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率。

2.onDraw方法中不要做耗时的任务,

不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。
按照Google官方给出的性能优化典范中的标准,View的绘制频率保证60fps是最佳的,这就要求每帧绘制时间不超过16ms(16ms = 1000/60),虽然程序很难保证16ms这个时间,但是尽量降低onDraw方法中的复杂度总是切实有效的。

三.网络优化:

1. 原因

网络优化不好,造成用户流量消耗大,耗电快,用户等待时间长体验差等。

2. 工具

Network Monitor,Charles,Fiddler,Stecho。

3. 优化手段
  • 接口设计
    API设计要合理。
    使用GZIP压缩。
    选择合适的数据格式:json,xml,protocol Buffer。
  • 图片处理
    图片下载: 使用缩略图。
    使用WebP图片。
    根据设备规格,指定图片尺寸请求图片。 使用完善的合适的图片加载框架:Glide,Picasso等。
    图片上传:一般要支持断点续传。
  • 网络缓存
    适当缓存,可让App看起来更快。
    使用DiskLruCache。
  • 打包网络请求
    网络状况好(如:WiFi状态下),可一次异步发起多个业务模块的数据请求。
  • 监听相关状态变化
    休眠状态(即:熄屏状态下),尽量不要发起网络请求。
    充电状态,可适当做一些必要的网络请求,但要控制频率。
    弱网状态下,可压缩和减少数据传输量;不要自动加载图片,用占位图显示;页面视图先显示,网络请求延迟提交。
  • 优化网络请求机制
    划分网络请求的优先级,同一页面,同一模块,重要的数据优先请求。
    网络差,减少请求量;网络好,提高请求量。
    合并网络请求,减少请求次数。比如,本地埋点数据,无需实时上报,可先本地缓存,再根据上报策略,选择合适时机一并上报。
  • IP直连和HttpDns
    IP直连,省去DNS解析时间。
    使用HttpDns,防止运营商域名劫持或跨网访问问题。
  • 开启 keep-alive进行连接复用;
  • 优化请求频率
    使用本地缓存,让App在离线状态也能使用。
    优先使用缓存;当没有缓存或缓存过期,再请求网络数据。

常见的网络优化方案如下:

  • 尽量减少网络请求,能够合并的就尽量合并
  • 避免DNS解析,根据域名查询可能会耗费上百毫秒的时间,也可能存在DNS劫持的风险。可以根据业务需求采用增加动态更新IP的方式,或者在IP方式访问失败时切换到域名访问方式。
  • 大量数据的加载采用分页的方式
  • 网络数据传输采用GZIP压缩
  • 加入网络数据的缓存,避免频繁请求网络
  • 上传图片时,在必要的时候压缩图片

四.安装包优化

安装包优化的核心就是减少apk的体积,常见的方案如下:

  • 减少应用中不必要的资源文件,比如图片,在不影响APP效果的情况下尽量压缩图片,有一定的效果
  • 在使用了SO库的时候优先保留v7版本的SO库,删掉其他版本的SO库。原因是在2018年,v7版本的SO库可以满足市面上绝大多数的要求,可能八九年前的手机满足不了,但我们也没必要去适配老掉牙的手机。实际开发中减少apk体积的效果是十分显著的,如果你使用了很多SO库,比方说一个版本的SO库一共10M,那么只保留v7版本,删掉armeabi和v8版本的SO库,一共可以减少20M的体积。
  • res资源优化
    (1)只使用一套图片,使用高分辨率的图片。
    (2)UI设计在ps安装TinyPNG插件,对图片进行无损压缩。
    (3)svg图片:一些图片的描述,牺牲CPU的计算能力的,节省空间。使用的原则:简单的图标。
    (4)图片使用WebP(https://developers.google.com/speed/webp/)的格式(Facebook、腾讯、淘宝在用。)缺点:加载相比于PNG要慢很多。
    但是配置比较高。工具:http://isparta.github.io/
    (5)使用tintcolor(android - Change
    drawable color programmatically)实现按钮反选效果。
  • 代码优化
    (1)实现功能模块的逻辑简化
    (2)Lint工具检查无用文件将无用的资源列在“UnusedResources: Unused resources”,删除。
    (3)移除无用的依赖库。
  • lib资源优化
    (1)动态下载的资源。
    (2)一些模块的插件化动态添加。
    (3)so文件的剪裁和压缩。
  • assets资源优化
    (1)音频文件最好使用有损压缩的格式,比如采用opus、mp3等格式,但是最好不要使用无损压缩的音乐格式
    (2)对ttf字体文件压缩,可以采用FontCreator工具只提取出你需要的文字。比如在做日期显示时,其实只需要数字字体,但是使用原有的字体库可能需要10MB大小,如果只是把你需要的字体提取出来生成的字体文件只有10KB
  • 代码混淆。
    使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
  • 插件化
    可将功能模块放服务器,需要用时再加载。
  • 7z极限压缩
    具体请参考微信的安接包压缩,实现实现原理,有时间再分析;

五.Android内存优化

1.Android内存管理机制

Android应用都是在Android虚拟机上运行的,内存分配和垃圾回收都是由Android虚拟机来完成的。

1.1 Java对象的声明周期

创建-使用-销毁(包括:不可见-不可达-收集-终结-对象再分配)。

Android系统内存分配,实际上是对堆的分配和释放。

1.2 内存回收机制

年轻代、老年代、持久代。

  • 年轻代
    所有新生成的对象都放在年轻代。
    年轻代分为一个Eden区和两个Survivor区。
    GC时,当Eden区满时,还存活的对象会被复制到其中一个Survivor区(A)。
    当这个Survivor区(A)也满时,就会被复制到另一个Survivor区(B)。
    当Survivor区(B)也满时,从第一个Survivor(B)复制过来并且还存活的对象,就会被复制到老年代。
  • 老年代
    在年轻代经历了N次垃圾回收仍然存活的到对象,就被放到老年代。
  • 持久代
    主要存放静态文件,比如Java类,方法等。
    持久代对垃圾回收没有明显影响。
    如果持久代空间太小,可通过-XX:MaxPermSize =< N配置。
1.3 GC收集器原理:
  • 可达性算法:从GCRoot对象为起点,向下搜索,可到达的对象是称为GC可达,GC收集器不会回收,不可到达的对象称为不GC不可达,是GC收集器回收的对象。
  • GCRoot对象:
    (1)虚拟机栈(栈帧找那个的局部变量表)中的对象;
    (2)方法区中类静态变量引用的对象;
    (3)方法区中常量引用的对象。
2.常见的内存泄漏:

Android的内存优化在我看来分为两点:避免内存泄漏、扩大内存,其实就是开源节流。

其实内存泄漏的本质就是较长生命周期的对象引用了较短生命周期的对象。

2.1 内存泄露

内存泄漏:堆上分配的对象已经不会再使用,但是GC收集器无法对其进行回收,此对象被强应用所引用 。

  • 静态变量导致的内存泄漏
    详情:一个静态变量又是非静态内部类会一直持有对外部类的引用,导致外部类Activity无法被回收。
    解决办法:将内部类设为静态内部类或独立出来;使用context.getApplicationContext()。
  • 单例模式导致的内存泄漏
    详情:单例传入参数this来自Activity,使得持有对Activity的引用。
    解决办法:传参context.getApplicationContext()
  • 属性动画导致的内存泄漏
    详情:没有在onDestroy()中停止无限循环的属性动画,使得View持有了Activity。
    解决办法:在Activity.onDestroy()中调用Animator.cancel()停止动画。
  • Handler导致的内存泄漏
    详情:Message持有对Handler的引用,而非静态内部类的Handler又隐式持有对外部类Activity的引用,使得引用关系会保持至消息得到处理,从而阻止了Activity的回收。
    解决办法:使用静态内部类+WeakReference弱引用;当外部类结束生命周期时清空消息队列。
  • 线程导致的内存泄漏
    详情:AsyncTask/Runnable以匿名内部类的方式存在,会隐式持有对所在Activity的引用。
    解决办法:将AsyncTask和Runnable设为静态内部类或独立出来;在线程内部采用弱引用保存Context引用。
  • 资源未关闭导致的内存泄漏
    详情:未及时注销资源导致内存泄漏,如BraodcastReceiver、File、Cursor、Stream、Bitmap等。
    解决办法:在Activity销毁的时候要及时关闭或者注销。例如:
    BraodcastReceiver:调用unregisterReceiver()注销;
    Cursor,Stream、File:调用close()关闭;
    Bitmap:调用recycle()释放内存(2.3版本后无需手动)。
  • Adapter导致的内存泄漏
    详情:不使用缓存而只依靠getView() 每次重新实例化Item,会给gc制造压力。
    解决办法:在构造Adapter时使用缓存的convertView。
  • WebView导致的内存泄漏。
    详情:WebView比较特殊,即使是调用了它的destroy方法,依然会导致内存泄漏。
    解决办法:其实避免WebView导致内存泄漏的最好方法就是让WebView所在的Activity处于另一个进程中,当这个Activity结束时杀死当前WebView所处的进程即可,我记得阿里钉钉的WebView就是另外开启的一个进程,应该也是采用这种方法避免内存泄漏。
  • 集合类泄漏
    详情:比如全局map等有静态应用,最后没有做删除。
    解决办法:在onDestry时回收不需要的集合。
2.2 扩大内存

为什么要扩大我们的内存呢?有时候我们实际开发中不可避免的要使用很多第三方商业的SDK,这些SDK其实有好有坏,大厂的SDK可能内存泄漏会少一些,但一些小厂的SDK质量也就不太靠谱一些。那应对这种我们无法改变的情况,最好的办法就是扩大内存。

扩大内存通常有两种方法:

  • 一个是在清单文件中的Application下添加largeHeap="true"这个属性,另一个就是同一个应用开启多个进程来扩大一个应用的总内存空间。
  • 第二种方法其实就很常见了,比方说我使用过个推的SDK,个推的Service其实就是处在另外一个单独的进程中。

Android中的内存优化总的来说就是开源和节流,开源就是扩大内存,节流就是避免内存泄漏。

2.3 检测、分析内存泄漏的工具
  • MemoryMonitor:随时间变化,内存占用的变化情况
  • MAT:输入HRPOF文件,输出分析结果
    a. Histogram:查看不同类型对象及其大小
    b.DominateTree:对象占用内存及其引用关系
    c.MAT使用教程
  • LeakCanary:实时监测内存泄漏的库(LeakCanary原理)

六.卡顿优化

1.卡顿场景

可分为四个大的方向:

  • UI UI包括绘制和渲染。
  • 启动 启动可分为冷启动、热启动。
  • 跳转 跳转包括页面间跳转和前后台切换。
  • 响应 包括:点击、滑动、系统事件、按键。
2.卡顿原因

Android 应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。总的来说造成卡顿的原因有如下几种:

  • UI的绘制。主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。
  • 数据处理上。导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,
    (1)是数据在主线程处理,这个是初级工程师会犯的错误;
    (2)是数据处理占用 CPU 高,导致主线程拿不到时间片;
    (3)是内存增加导致 GC 频繁,从而引起卡顿。
    引起卡顿的原因很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上显示来达到用户,归根到底就是显示有问题,所以,要解决卡顿,就要先了解
    Android 系统的渲染机制。
  • UI的过度绘制。绘制的页面有几层View,底层View都是隐藏的,这种的还绘制的话就会造成过度绘制
3.andriod的渲染机制

要解决卡顿,就要先了解 Android 系统的渲染机制。要在屏幕上显示,其实要经过一系列的过程,Android 应用程序把经过测量、布局、绘制后的 surface 缓存数据,通过 SurfaceFlinger 把数据渲染到显示屏幕上, 通过 Android 的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕上。

这里我们先介绍一个名词:FPS。FPS 表示每秒传递的帧数。在理想情况下,60 FPS 就感觉不到卡,这意味着每个绘制时长应该在16 ms 以内。但是 Android 系统很有可能无法及时完成那些复杂的页面渲染操作。Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的 60FPS。如果某个操作花费的时间是 24ms ,系统在得到 VSYNC 信号时就无法正常进行正常渲染,这样就发生了丢帧现象。那么用户在 32ms 内看到的会是同一帧画面,这种现象在执行动画或滑动列表比较常见,还有可能是你的 Layout 太过复杂,层叠太多的绘制单元,无法在 16ms 完成渲染,最终引起刷新不及时。

android的View的绘制流程大家应该都知道,都是要经过三大核心步骤:Measure、Layout、Draw。具体是如何实现的建议看一下View的源码,这里我就不多说了;如果绘制的层级深,页面复杂,在Measure、Layout这二个步骤要花费大量的时间;这样也会造卡顿现象;

4.andriod卡顿优化方案
  • 不要在主线程进行网络访问/大文件的IO操作
  • 绘制UI时,尽量减少绘制UI层次;减少不必要的view嵌套,可以用Hierarchy Viewer工具来检测,后面会详细讲;
  • 当我们的布局是用的FrameLayout的时候,我们可以把它改成merge,可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout)
  • 提高显示速度,使用ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。
  • 在view层级相同的情况下,尽量使用 LinerLayout而不是RelativeLayout;因为RelativeLayout在测量的时候会测量二次,而LinerLayout测量一次,可以看下它们的源码;
  • 删除控件中无用的属性;
  • 布局复用.比如listView 布局复用
  • 尽量避免过度绘制(overdraw),比如:背景经常容易造成过度绘制。由于我们布局设置了背景,同时用到的MaterialDesign的主题会默认给一个背景。这时应该把主题添加的背景去掉;还有移除
    XML 中非必须的背景
  • 自定义View优化。使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。也是避免过度绘制.
  • 启动优化,启动速度的监控,发现影响启动速度的问题所在,优化启动逻辑,提高应用的启动速度。比如闪屏页面,合理优化布局,加载逻辑优化,数据准备.
  • 合理的刷新机制,尽量减少刷新次数,尽量避免后台有高的 CPU 线程运行,缩小刷新区域。
5.andriod卡顿优化所用到的工具

性能问题并不容易复现,也不好定位,但是真的碰到问题就需要借助相应的的调试工具,下面介绍比较常用的调试工具。

1.Profile GPU Rendering

在手机开发者模式下,有一个卡顿检测工具叫做:Profile GPU Rendering,看图:

android性能优化 面试 android的性能优化_android性能优化 面试

上图中的绿线代表16ms,要确保一秒内打到60fps,你需要确保这些帧的每一条线都在绿色的16ms标记线之下.任何时候你看到一个竖线超过了绿色的标记现,你就会看到你的动画有卡顿现象产生.

下面的柱状图蓝色代表测量绘制的时间,当你看到蓝色的线很高的时候,有可能是因为你的一堆视图突然变得无效了(即需要重新绘制),或者你的几个自定义视图的onDraw函数过于复杂.

柱状图红色代表执行的时间,这部分是Android进行2D渲染 Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List.这些API有效地将数据发送到GPU,最终在屏幕上显示出来.当你看到红色的线非常高的时候,这些复杂的自定义View就是罪魁祸首:

橙色部分表示的是处理时间,或者说是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,那就意味着你给GPU太多的工作,太多的负责视图需要OpenGL命令去绘制和处理.

总之,保持动画流畅的关键就在于让这些垂直的柱状条尽可能地保持在绿线下面,任何时候超过绿线,你就有可能丢失一帧的内容.

2.Debug GPU overDraw过度绘制检测

在手机开发者模式下,有一个过度绘制检测工具叫做:Debug GPU overDraw,看图:

android性能优化 面试 android的性能优化_android性能优化 面试_02


android性能优化 面试 android的性能优化_android性能优化 面试_03


原色:没有过度绘制

蓝色:1 次过度绘制

绿色:2 次过度绘制

粉色:3 次过度绘制

红色:4 次及以上过度绘制

七.耗电优化

在移动设备中,电池的重要性不言而喻,没有电什么都干不成。对于操作系统和设备开发商来说,耗电优化一致没有停止,去追求更长的待机时间,而对于一款应用来说,并不是可以忽略电量使用问题,特别是那些被归为“电池杀手”的应用,最终的结果是被卸载。因此,应用开发者在实现需求的同时,需要尽量减少电量的消耗。
耗电的原因其实很多,这里我就讲一下几种优化方案,优化方案的反面就是他的原因了,几种优化方案如下:

  • 合理的使用wake_lock锁,wake_lock锁主要是相对系统的休眠(这里就是为了省电,才做休)而言的,意思就是我的程序给CPU加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题。所以微信里面是有大量使用到了wake_lock锁。
  • 使用jobScheduler2,集中处理一些网络请求,有些不用很及时的处理可以放在充电的时候处理,比如,图片的处理,APP下载更新等等;
  • 计算优化,避开浮点运算等。
  • 数据在网络上传输时,尽量压缩数据后再传输,建议用FlatBuffer序列化技术,这个比json效率高很多倍,不了解FlatBuffer,建议找资料学习一下。
andriod耗电分析所用到的工具

在 Android5.0 以前,在应用中测试电量消耗比较麻烦,也不准确,5.0 之后专门引入了一个获取设备上电量消耗信息的 API:Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,是一款图形化数据分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况,最后提供一些可供参考电量优化的方法。

Battery Historian耗电分析工具

android性能优化 面试 android的性能优化_内存泄漏_04


Battery Historian耗电分析工具的开源地址;具体如何使用这里不细讲;

八.ListView/RecycleView及Bitmap/图片优化

ListView/RecycleView的优化思想主要从以下几个方面入手:

  • 使用ViewHolder模式来提高效率
  • 异步加载:耗时的操作放在异步线程中
  • ListView/RecycleView的滑动时停止加载和分页加载

Bitmap/图片优化

  • 主要是对加载图片进行压缩,避免加载图片多大导致OOM出现。
  • 对图片本身进行操作。尽量不要使用setImageBitmap、setImageResource、BitmapFactory.decodeResource来设置一张大图,因为这些方法在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存.
  • 图片进行缩放的比例,SDK中建议其值是2的指数值,值越大会导致图片不清晰。
  • 不用的图片记得调用图片的recycle()方法

九. 数据库SQLite优化

优化手段
1. 索引
  • 概念: 索引是对数据库表中一列或多列数据进行排序的一种数据结构。可理解为一个指向表中数据的指针,与一本书的目录类似。
  • 优点: 加快表中数据查询速度。
  • 缺点: 创建索引本身也会造成资源开销。
  • 类别:
    表索引:CREATE INDEX index_name ON table_name。
    单列索引:CREATE INDEX index_name ON table_name(column_name)。
    唯一索引:CREATE UNIQUE INDEX index_name ON table_name(column_name)。
    组合索引:CREATE INDEX index_name ON table_name(column1,column2)。
    主键索引:ALTER TABLE table_name ADD CONSTRAINT index_name PRIMARY KEY(primaryKey)。
  • 总结: 合理使用索引,可加快数据库表数据的查询速率。
2. 事务
  • 概念: 对数据库原子性的操作。
  • 优点: 为数据的整体性执行带来可靠安全性,为更新和删除操作带来很大优化。
  • 总结: 保证数据的完整性,安全性,提高数据更新,删除操作的效率。
其他手段
  • 尽量少用cursor.getColumnIndex()。
  • 用StringBuilder(非线程安全)或StringBuffer(线程安全)来拼接字符串。
  • 查询时,只返回需要的数据或结果。
  • cursor使用后要及时关闭。

十. 启动优化

  • 通过启动加载逻辑优化。可以采用分布加载、异步加载、延期加载策略来提高应用启动速度。
  • 数据准备。数据初始化分析,加载数据可以考虑用线程初始化等策略。
  • 异步加载一:Application中加入异步线程
  • 异步加载二:MainActivity中加入异步线程
  • 延迟加载功能:首屏绘制完成之后加载
  • 动态加载布局:主布局文件优化
  • 主布局文件深度优化
  • 功能代码深度优化

启动优化具体参考Android APP启动以及性能优化

十一. 数据结构优化

  • 使用Android自带的spareArray来代替HashMap;
  • 比如HashMap和ArrayMap,优先使用ArrayMap;
  • 优先使用基本类型,而非包装类
  • 减少占内存较大的枚举的使用
  • 采用三级缓存机制:LRUCache
  • 图片压缩:inSampleSize、RGB_565替换RGB_8888

十二. 稳定性优化

Android 应用的稳定性定义很宽泛,影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用,比较常用的解决方式如下:

  • 提高代码质量。比如开发期间的代码审核,看些代码设计逻辑,业务合理性等。
  • 代码静态扫描工具。常见工具有Android Lint、Findbugs、Checkstyle、PMD等等。
  • Crash监控。把一些崩溃的信息,异常信息及时地记录下来,以便后续分析解决。
  • Crash上传机制。在Crash后,尽量先保存日志到本地,然后等下一次网络正常时再上传日志信息