【Android中级工程师】性能优化

来源:互联网 发布:浪潮软件股票行情 编辑:程序博客网 时间:2024/04/28 02:47

前言

最近公司培训期的项目接近尾声,之前在大学做项目都是把功能做完,写篇总结就算结束了,现在开始工作了还是觉得需要做点什么来优化一下项目。网上有很多关于性能优化的文章和方法,参考着他们的文章操作和学习,在此做点总结。

参考文章:
http://blog.csdn.net/yanbober/article/details/48394201
http://mp.weixin.qq.com/s/tG5CD8PcmOWTzYtT-YTGNQ
http://blog.csdn.net/gs12software/article/details/51173392

用户体验是评估产品的一个非常重要的指标,而影响用户体验的主要是这款产品有什么功能,这些功能好不好用。那么我们产品的性能将极大的影响用户体验,如果一款产品用户用着一卡一顿的,页面切换还要2s来刷新,那么这款产品必将是失败的,所以性能优化非常必要!
性能优化主要包括五个方面:内存优化、UI优化、线程优化、代码优化、电量优化,我们来逐个分析。

一、内存优化

Android的内存管理机制类似于java的垃圾回收机制GC,关于java的GC可以参考这篇文章。Android是基于linux的,每个Android程序都执行在自己的虚拟机中,那么显然每个虚拟机都有自己的内存阀值,程序运行的内存必须小于阀值才能正常工作。我们运行的程序在自己的虚拟机中内存管理基本就是遵循Java的内存管理机制了,系统在特定的情况下主动进行垃圾回收。但是要注意的一点就是在Android系统中执行垃圾回收(GC)操作时所有线程(包含UI线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些GC垃圾回收一般都会有明显的log打印出回收类型,常见的如下:

GC_MALLOC——内存分配失败时触发;GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发;GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

内存泄漏

众所周知,在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;

造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;

内存溢出

内存溢出和内存泄漏可以说是好伙伴,我们知道内存泄露一般影响就是导致应用卡顿,但是极端的影响是使应用挂掉。前面也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,那么超过阀值这种就称为内存溢出(OOM)。
内存溢出的主要导致原因有如下几类:

应用代码存在内存泄露,长时间积累无法释放导致OOM;应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值OOM;

可以发现,无论哪种类型,导致内存溢出(Out Of Memory Error)的核心原因就是应用的内存超过阈值了。这两种情况我们可以通过下面两张图来加深理解:
这里写图片描述
这里写图片描述
第二种情况一般比较好发现,通过Log即可以看到导致OOM位置。

内存分析

我们可以通过Android Monitor来查看分析:Android–>System Information–>MemoryUsage
这里写图片描述
我们可以按顺序一个一个activity去查看,从图中我们可以看到内存的占用,如果内存占用过大也可以从图中看出来。当然还有其他工具,有兴趣可以去上面给的链接里面学习。

优化建议

避免内存泄漏:

  1. 尽量使用ApplicationContext,而避免使用Activity Context,防止对它长时间的占用无法释放
  2. 使用Animation时,在Activity的ondestory()方法中调用Animation.cancle()进行停止。
  3. 非静态内部类的静态实例容易造成内存泄漏;即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理
  4. 警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。
  5. 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
  6. 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。
  7. 不要在执行频率很高的方法或者循环里创建对象。

避免内存溢出:

  1. 时刻记得不要加载过大的Bitmap对象,一般要进行裁剪和压缩,不过我们一般都用fresco或Glide开源库进行加载。
  2. 优化界面交互过程中频繁的内存使用;譬如在列表等操作中只加载可见区域的Bitmap、滑动时不加载、停止滑动后再开始加载。
  3. 对批量加载等操作进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等。
  4. 尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用style等资源达到节约内存。
  5. 尽量使用线程池替代多线程操作,这样可以节约内存及CPU占用率。
  6. 尽量管理好自己的Service、Thread等后台的生命周期,不要浪费内存占用。
  7. 尽可能的不要使用依赖注入,中看不中用。
  8. 尽量在做一些大内存分配等可疑内存操作时进行try catch操作,避免不必要的应用闪退。
  9. 尽量的优化自己的代码,减少冗余,进行编译打包等优化对齐处理,避免类加载时浪费内存。

二、UI优化

UI呈现了我们应用的外貌,UI设计师们使劲浑身解数让我们的外貌端庄大方美丽,如果因为我们程序员的原因导致了“脸部抽搐”,那我们和UI的关系可就更加恶劣了。UI的流畅和卡顿极大的决定了用户体验,所以我们必须对UI优化进行重视。

UI卡顿

人类肉眼能够分辨的连贯性,Android将流畅度的帧率定义为60fps,换算成时间间隔为16ms,所以需要尽量保证每次在16ms内处理完所有的CPU与GPU计算、绘制、渲染等操作,否则会造成丢帧卡顿问题。当然我们上面提到过,虚拟机在执行GC垃圾回收操作时所有线程(包括UI线程)都需要暂停,当GC垃圾回收完成之后所有线程才能够继续执行。也就是说当在16ms内进行渲染等操作时如果刚好遇上大量GC操作则会导致渲染时间明显不足,也就从而导致了丢帧卡顿问题。

卡顿的原因主要有:

  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。

卡顿分析

对于UI性能的分析可以通过开发者选项中的GPU过度绘制工具来进行分析。在设置->开发者选项->调试GPU过度绘制(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面过度绘制进行分析):
这里写图片描述
可以发现,开启后在我们想要调试的应用界面中可以看到各种颜色的区域,具体含义如下:

颜色 含义 无色 right-aligned 蓝色 1x过度绘制 绿色 2x过度绘制 淡红色 3x过度绘制 红色 4x(+)过度绘制

由于过度绘制指在屏幕的一个像素上绘制多次(譬如一个设置了背景色的TextView就会被绘制两次,一次背景一次文本;这里需要强调的是Activity设置的Theme主题的背景不被算在过度绘制层级中),所以最理想的就是绘制一次,也就是蓝色(当然这在很多绚丽的界面是不现实的,所以大家有个度即可,我们的开发性能优化标准要求最极端界面下红色区域不能长期持续超过屏幕三分之一,可见还是比较宽松的规定),因此我们需要依据此颜色分布进行代码优化,譬如优化布局层级、减少没必要的背景、暂时不显示的View设置为GONE而不是INVISIBLE、自定义View的onDraw方法设置canvas.clipRect()指定绘制区域或通过canvas.quickreject()减少绘制区域等。


我们还可以通过使用HierarchyViewer来分析UI性能。
通过命令行启动HierarchyViewer,当然如果没有安装它会提示您安装。

xxx@ThinkPad:~$ hierarchyviewer   //通过命令启动HierarchyViewer

选中一个Window界面item,然后点击右上方Hierarchy window或者Pixel Perfect window(这里不介绍,主要用来检查像素属性的)即可操作。
这里写图片描述

一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级,左下角可以输入View的id直接自动跳转到中间显示;Save as PNG用来把左侧树保存为一张图片;Capture Layers用来保存psd的PhotoShop分层素材;右侧剧中显示选中View的当前属性状态;右下角显示当前View在Activity中的位置等;左下角三个进行切换;Load View Hierarchy用来手动刷新变化(不会自动刷新的)。当我们选择一个View后会如下图所示:
这里写图片描述
类似上图可以很方便的查看到当前View的许多信息;上图最底那三个彩色原点代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲染时间,红色和黄色的点代表速度渲染较慢的View(当然了,有些时候较慢不代表有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)。

在自定义View的性能调试时,HierarchyViewer上面的invalidate Layout和requestLayout按钮的功能更加强大,它可以帮助我们debug自定义View执行invalidate()和requestLayout()过程,我们只需要在代码的相关地方打上断点就行了,接下来通过它观察绘制即可。

可以发现,有了HierarchyViewer调试工具,我们的UI性能分析变得十分容易,这个工具也是我们开发中调试UI的利器,在平时写代码时会时常伴随我们左右,更多工具可以通过上面链接学习。

优化建议

  1. 布局优化;尽量使用include、merge、ViewStub标签,尽量不存在冗余嵌套及过于复杂布局(譬如10层就会直接异常),尽量使用GONE替换INVISIBLE,使用weight后尽量将width和heigh设置为0dp减少运算,Item存在非常复杂的嵌套时考虑使用自定义Item View来取代,减少measure与layout次数等。
  2. 列表及Adapter优化;尽量复用getView方法中的相关View,不重复获取实例导致卡顿,列表尽量在滑动过程中不进行UI元素刷新等。
  3. 背景和图片等内存分配优化;尽量减少不必要的背景设置,图片尽量压缩处理显示,尽量避免频繁内存抖动等问题出现。
  4. 自定义View等绘图与布局优化;尽量避免在draw、measure、layout中做过于耗时及耗内存操作,尤其是draw方法中,尽量减少draw、measure、layout等执行次数。
  5. 避免ANR,不要在UI线程中做耗时操作,遵守ANR规避守则,譬如多次数据库操作等。

三、线程优化

线程阻塞

每一个应用程序都是一个进程,而进程中有多个线程在执行任务,其中主线程只负责执行UI操作,而所有耗时操作例如I/O文件存储,数据库存储,网络请求等到应该在子线程当中执行,如果在主线程中执行耗时操作而导致主线程停滞阻塞,则将导致ANR异常,这是Android的多线程。而ANR异常的判定是程序在5s或10s内没有响应用户的操作,也就是主线程停止了5s或10s。

优化工具

  1. AsyncTask
    为UI线程与工作线程之间进行快速处理的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的场景。当使用AsyncTask时,有几个重要的性能方面的问题要记住。 首先,默认情况下,应用程序将其创建的所有AsyncTask对象推送到单个线程中。 因此,它们以串行方式执行,和主线程类似,特别耗时的工作组会阻塞队列。 因此,我们建议你只使用AsyncTask处理持续时间短于5ms的任务。

  2. HandlerThread
    HandlerThread实际上是一个长时间运行的线程,它从队列中抓取工作,并对其进行操作。它为某些回调方法或者等待某些执行任务的执行设置一个专属的线程,并提供线程任务的调度机制。一般情况下我们在新建的Thread中进行耗时操作,任务完成后通过sendMessage()方法给handler发消息,因为handler是在主线程中定义的,所以可以在handler中进行UI操作。

  3. ThreadPoolExecutor
    对于高度并行,分布式的任务,我们可以使用ThreadPoolExecutor来实现。它可以把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。当然,使用这个类需要特别注意同时并发线程数量的控制,理论上来说,我们可以设置任意你想要的并发数量,但是这样做非常的不好。因为CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU能够同时执行的阈值,CPU就需要花费精力来判断到底哪些线程的优先级比较高,需要在不同的线程之间进行调度切换。一旦同时并发的线程数量达到一定的量级,这个时候CPU在不同线程之间进行调度的时间就可能过长,反而导致性能严重下降。另外需要关注的一点是,每开一个新的线程,都会耗费至少64K+的内存。为了能够方便的对线程数量进行控制,ThreadPoolExecutor为我们提供了初始化的并发线程数量,以及最大的并发数量进行设置。当然,这个类我们用上的概率确实不高。

  4. IntentService
    IntentService适合执行由Ui触发的后台任务。并可以把这些任务执行的情况通过一定的机制反馈给UI。默认的Service是执行在主线程的,可是通常情况下,这很容易影响到程序的绘制性能(抢占了主线程的资源,可能会导致卡顿慢)。除了前面介绍过的AsyncTask与HandlerThread,我们还可以选择使用IntentService来实现异步操作。IntentService继承自普通Service同时又在内部创建了一个HandlerThread,在onHandlerIntent()的回调里面处理扔到IntentService的任务。所以IntentService就不仅仅具备了异步线程的特性,还同时保留了Service不受主页面生命周期影响的特点。
    当然,使用IntentService还是需要注意以下几点:

  5. 因为IntentService内置的是HandlerThread作为异步线程,所以每一个交给IntentService的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。
  6. 通常使用到IntentService的时候,我们会结合使用BroadcastReceiver把工作线程的任务执行结果返回给主UI线程。使用广播容易引起性能问题,我们可以使用LocalBroadcastManager来发送只在程序内部传递的广播,从而提升广播的性能。我们也可以使用runOnUiThread()快速回调到主UI线程。
  7. 包含正在运行的IntentService的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的。

四、代码优化

代码冗乱

代码的冗余和混乱一般和用户体验的关系好像不大,但是作为一名程序员,不仅要能写出漂亮的程序,当然代码也得漂亮。每个公司都有自己的代码规范,比如命名规范、注释规范、架构规范等等,我在开发当中是比较注意代码规范的,因为现在的开发基本也不是一个人在做,必须保证别人之后在维护你的项目的时候能更加容易看懂你的代码,所以代码优化很重要。当然,代码没有统一的规范,每个公司都有自己不同的标准,每个人都有自己的习惯。例如,我一般喜欢给点击事件的activity实现onClicklistener接口,然后实现它的方法onClick()来编写监听事件,因为这样好看很多。当然除了代码的混乱还有代码的冗余,比如那些没有使用的变量,资源等,如果用不上就删掉,有强迫症的人都不喜欢有多余的东西。

优化工具

Android Studio内置了有自带的代码检查工具Lint。打开Analyze->Inspect Code–>Whole project ->Unused resource 点击开始检测,等待一下后会发现如下结果:
这里写图片描述
逐个点开可以看到它的优化建议,当然如果你实在有强迫症就一个一个把它干掉吧!

五、电量优化

耗电概念

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

优化建议

  1. 在需要网络的应用中,执行某些操作前尽量先进行网络状态判断。
  2. 在网络应用传输中使用高效率的数据格式和解析方法,譬如JSON等。
  3. 在传输用户反馈或者下载OTA升级包等不是十分紧急的操作时尽量采用压缩数据进行传输且延迟到设备充电和WIFI状态时进行。
  4. 在有必要的情况下尽量通过PowerManager.WakeLock和JobScheduler来控制一些逻辑操作达到省电优化。
  5. 对定位要求不太高的场景尽量使用网络定位,而不是GPS定位。
  6. 对于定时任务尽量使用AlarmManager,而不是sleep或者Timer进行管理。
  7. 尽可能的减少网络请求次数和减小网络请求时间间隔。
  8. 后台任务要尽可能少的唤醒CPU,譬如IM通信的长连接心跳时间间隔、一些应用的后台定时唤醒时间间隔等要设计合理。
  9. 特殊耗电业务情况可以进行弹窗等友好的交互设计提醒用户该操作会耗用过多电量。
阅读全文
0 0
原创粉丝点击