Andrioid 性能优化基础

Posted xzj_2013

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Andrioid 性能优化基础相关的知识,希望对你有一定的参考价值。

为什么要进行性能优化

android设备作为一种移动设备,不管是在内存还是CPU的性能都受到一定的限制,无法做到像PC设备那样的超大内存和高性能CPU,鉴于这一点,就意味着android无法毫无限制的使用内存和CPU资源,过多的使用内存会导致程序内存泄露即OOM;而过多的使用CPU,一般指做大量的耗时任务,会导致设备变得卡顿甚至出现程序无响应的情况即ANR。因此,android程序的性能问题就变得异常突出,这对开发人员也提出了更高的要求。

Android的内存管理方式##

  1. Android的内存分配和回收机制
    一个进程对应一个虚拟机,可以通过dumpsys meminfo命令去查看指定的进程内存信息

    可以参考 http://blog.csdn.net/hudashi/article/details/7050897
    GC操作只有在Heap Free空间不够的情况下才会自动触发垃圾回收;
    GC操作触发时也是需要消耗CPU时间的,如果时间过长会影响程序的运行,同时在该操作触发时所有的线程都会被暂停;
  2. App内存限制机制
    每个App分配的最大内存限制,随不同设备而不同,可以通过ActivityManager的getMemoryClass方法获取到允许的最大内存;
    内存大户:图片;
    为什么要限制?原因在于Android设备是多任务系统,多个App可以同时存在,如果不做限制的话,容易导致内存被占光,导致其他APP无法运行;
  3. 切换应用时后台App清理机制
    App切换时使用的是Lru Cache算法-----缓存淘汰算法,Least Recently Used的缩写,即最近最久未使用的意思,最近使用的排在前面,最少可能的会被清理掉;
    onTrimMemory()回调,在内存变化时触发,在该方法中检测到内存不够时可以做相应处理;
  4. 监控内存的几种方法
    通过代码 Runtime的totalMemory freeMemory等方法获取
    通过Android Studio的Android Monitor工具或者Tools工具栏的Android Device Monitor可以看到相应的信息

性能优化的方法

为了解决性能问题,开发人员提出了一系列的优化方法,主要包括布局优化,绘制优化,内存泄露优化,响应速度优化,listview优化,BitMap优化,线程优化以及一些性能优化建议;

  • 布局优化
    其主体思想就是尽量减少布局文件的层级;很显然,层级减少,就意味着Android绘制的工作量少了,自然性能就高了。
    如何进行布局优化?
    首先删除布局中无用的控件和层级。
    其次有选择的使用性能较低的ViewGroup,如RelativeLayout。由于RelativeLayout的功能较复杂,其布局过程需要花费更多的CPU时间,而LinearLayout和FrameLayout是一种简单高效的布局。如果简单的Viewgroup不能实现,需要嵌套使用的方式来完成的场景下还是建议使用RelativeLayout,因为嵌套就意味着增加了布局的层级,同样会降低程序的性能;
    最后是采用标签,标签和ViewStub;
    标签主要用于布局重用
    标签一般和配合使用,可以减少布局层级
    ViewStub则提供按需加载功能,需要时才会将ViewStub的布局加载到内存,这提高了程序的初始化效率。
  • 绘制优化
    绘制优化是指View的onDraw方法要避免执行大量的操作
    首先onDraw中不要创建新的局部对象,因为ondraw可能会被频繁调用
    其次 onDraw不要做耗时任务,也不能执行成千上万的循环操作。大量的循环会抢占CPU时间片,造成View的绘制不流畅。按Google给出的标准View绘制帧率保持60fps是最佳,这样每帧的绘制时间不超过16ms。
  • 数据结构优化
    频繁字符串拼接使用StringBuilder
    ArrayMap,SparseArray代替HashMap
    内存抖动—频繁的创建短暂使用的对象,然后系统Gc后又重新创建,导致的内存不断变化类似锯齿图形样;
    软引用和强引用的使用
  • 内存泄露优化
    内存泄漏是开发过程中的一个需要重视的问题,但是由于内存泄露问题对开发人员的经验和开发意识有较高的要求,因此也是开发人员最容易犯的错误之一。
    主要存在内存泄露的场景:
      ①集合类泄漏
      ②单例/静态变量造成的内存泄漏
       ③匿名内部类/非静态内部类
       ④资源未关闭造成的内存泄漏
    内存泄露的优化分为两个方面:
    ①在开发过程中避免写出有内存泄漏的代码 
        1.由忘记释放分配的内存导致的。比如 Bitmap 或者Cursor这一类应该关闭或者释放的对象没有关闭
        2.当应用不再需要这个对象,当仍未释放该对象的所有引用。
        3.持有对象的强引用导致垃圾回收器无法在内存中回收这个对象
             
       在Android开发中,最容易引发的内存泄漏问题的是Context。比如Activity的Context,就包含大量的内存引用,例如View Hierarchies和其他资源。一旦泄漏了Context,也意味泄漏它指向的所有对象。Android机器内存有限,太多的内存泄漏容易导致OOM。
        所以在出现了OOM后,我们需要检测的就是内存中是否存在一些应该被释放而没被释放的对象;
       首先应该检查的就是Activity的内存泄露;
    检查是否存在全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。
       检查是否活在Activity生命周期之外的线程。没有清空对Activity的强引用。
       这时候我们需要重点查看有没有遇到下列的情况:
    • Static Activities:如果定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量,如果在activity销毁的时候没有清空这个静态变量,就导致内存泄漏。因为static变量是贯穿这个应用的生命周期的,这个静态activity就会一直存在在进程中不会被垃圾回收机制销毁;
    • Static Views:泄露原理类似于activity
    • Inner Classes:内部类的优势之一就是可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。
    • Anonymous Classes:相似地,匿名类也维护了外部类的引用。所以内存泄漏很容易发生
    • Handler:同样道理,Activity 是有生命周期的,如果将其附属的Handler传递给了不可预测生命周期的类或线程,容易造成Handler的宿主Activity无法回收,
    • Threads:同样道理,匿名的线程也会导致内存泄露的发生
    • TimerTask:只要是匿名类的实例,不管是不是在工作线程,都会持有Activity的引用,导致内存泄漏。
    • Sensor Manager:通过Context.getSystemService(int name)可以获取系统服务。这些服务工作在各自的进程中,帮助应用处理后台任务,处理硬件交互。如果需要使用这些服务,可以注册监听器,这会导致服务持有了Context的引用,如果在Activity销毁的时候没有注销这些监听器,会导致内存泄漏;

②在内存占用较高的时候dump内存,使用MAT分析  
   a. MAT分析heap的总内存占用大小来初步判断是否存在泄露
   打开 DDMS 工具,在Devices视图下面的列表选中你需要检测的进程,然后在左边 Devices 视图页面选中“Update Heap”图标,然后在右边切换到 Heap 视图,点击 Heap 视图中的“Cause GC”按钮,此时进程就开始检测。
   Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
   进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况;
   所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。
   操作流程如图:

    b. MAT分析hprof来定位内存泄露的原因所在
    点击Dump hprof file按钮后会生成一个.hprof 文件,这个文件需要通过mat插件才能打开;展现在我们的面前的界面如下:    
    Overview视图: 总览界面,会大体给出一些分析后初步的结论.
    Overview-Details部分:总结出当前这个Heapdump占用了多大的类存,其中涉及的 类有多少,对象由多少类加载器;如果有没有回收的对象,会有只一个连接可以直接参看(途中的Unreachable Objects Hsitogram
    Overview-BiggestObjects by Retained Size视图部分:会列举出Retained Size值最大的几个值,你可以将鼠标放到饼图中的扇叶上,可以在右侧看出详细信息;图中灰色区域,并不是我们需要关心的,他是除了大内存对象外的其他对象,我们需要关心的就是图中彩色区域,如图可以通过点击查看涉及这些对象的所有的相关信息。如图    
    histogram视图部分:histogram视图主要是查看某个类的实例个数,比如我们在检查内存泄漏时候,要判断是否频繁创建了对象,就可以来看对象的个数来看。也可以通过排序看出占用内存大的对象;如图  
    在某一项上右键打开菜单选择 list objects ->with incoming refs 将列出该类的实例,展示对象间的引用关系,如图   
    快速找出某个实例没被释放的原因,可以右健 Path to GC Roots–>exclue all phantom/weak/soft etc. reference;得到结果如图
    Dominator tree视图:该视图会以占用总内存的百分比来列举所有实例对象,注意这个地方是对象而不是类了,这个视图是用来发现大内存对象的。这些对象都可以展开查看更详细的信息,可以看到该对象内部包含的对象; 
    Leaks suspects视图:这个视图会展示一些可能的内存泄漏的点;通过点击detail获取可能泄露的类,点击弹出list objects ->with incoming refs 将列出该类的实例, Path to GC Roots–>exclue all phantom/weak/soft etc. reference去获取具体是哪个地方这个对象没有被释放;  
   c. MAT History对比
   为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去
   添加好后,打开 Compare Basket面板,得到结果
   点击右上角的 ! 按钮,将得到比对结果
   也可以对比两个对象集合,方法与此类似,都是将两个 Dump结果中的对象集合添加到Compare Basket中去对比。找出差异后用 Histogram查询的方法找出 GC Root,定位到具体的某个对象上
    具体参考MAT Histogram对比使用

  • 响应速度优化
    响应速度优化的核心思想就是避免在主线程中做耗时操作。
    如果有耗时操作,可以开启子线程执行,即采用异步的方式来执行耗时操作。
    如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至ANR。
    Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BroadcastReceiver如果10秒钟之内还未执行完操作也会出现ANR。
    为了避免ANR,可以开启子线程执行耗时操作,但是子线程不能更新UI,所以需要子线程与主线程进行通信来解决子线程执行耗时任务后,通知主线程更新UI的场景。关于这部分,需要掌握Handler消息机制,AsyncTask,IntentService等内容。
    然而,在实际开发中,ANR仍然不可避免的发生了,而且很难从代码上发现,这时候就要用到ANR日志分析。当一个进程发生了ANR之后,系统会在/data/anr目录下创建一个文件traces.txt,通过分析这个文件就能定位出ANR的原因。

  • ANR日志分析
    1. 重现ANR 异常
    2. 抓取logcat日志,分析导致ANR的类型

06-22 10:37:46.204  3547  3604 E ActivityManager: ANR in com.lz.smartcontrol    // ANR出现的进程包名
E ActivityManager: PID: 17027        // ANR进程ID
E ActivityManager: Reason: executing com.lz.smartcontrol/.MessagePushService    //导致ANR的原因
E ActivityManager: Load: 8.31 / 8.12 / 8.47
E ActivityManager: CPU usage from 0ms to 6462ms later:    //CPU在ANR发生后的使用情况
E ActivityManager:   61% 3547/system_server: 21% user + 39% kernel / faults: 13302 minor 2 major 
E ActivityManager:   0.2% 475/debuggerd: 0% user + 0.1% kernel / faults: 6086 minor 1 major
E ActivityManager:   10% 5742/com.android.phone: 5.1% user + 5.1% kernel / faults: 7597 minor 
E ActivityManager:   6.9% 5342/com.android.systemui: 2.1% user + 4.8% kernel / faults: 4158 minor 
E ActivityManager:   0.1% 477/debuggerd64: 0% user + 0.1% kernel / faults: 4013 minor
E ActivityManager:   5.7% 16313/org.code: 3.2% user + 2.4% kernel / faults: 2412 minor 
E ActivityManager:   3.7% 17027/com.lz.smartcontrol: 1.7% user + 2% kernel / faults: 2571 minor 6 major
E ActivityManager:   2.6% 306/cfinteractive: 0% user + 2.6% kernel                    ... ...
E ActivityManager:  +0% 17168/kworker/0:1: 0% user + 0% kernel 
E ActivityManager: 0% TOTAL: 0% user + 0% kernel + 0% softirq    // CUP占用情况
E ActivityManager: CPU usage from 5628ms to 6183ms later: 
E ActivityManager:   42% 3547/system_server: 17% user + 24% kernel / faults: 11 minor 
E ActivityManager:     12% 3604/ActivityManager: 1.7% user + 10% kernel 
E ActivityManager:     12% 3609/android.display: 8.7% user + 3.5% kernel
E ActivityManager:     3.5% 5304/Binder_6: 1.7% user + 1.7% kernel
E ActivityManager:     3.5% 5721/Binder_A: 1.7% user + 1.7% kernel
E ActivityManager:     3.5% 5746/Binder_C: 3.5% user + 0% kernel
E ActivityManager:     1.7% 3599/Binder_1: 0% user + 1.7% kernel
E ActivityManager:     1.7% 3600/Binder_2: 0% user + 1.7% kernel 
I ActivityManager: Killing 17027:com.lz.smartcontrol/u0a85 (adj 1): bg anr
I art     : Wrote stack traces to '/data/anr/traces.txt'    //art这个TAG打印对traces操作的信息
D GraphicsStats: Buffer count: 9
W ActivityManager: Scheduling restart of crashed com.lz.smartcontrol/.. in 1000ms

可以看到,Logcat日志信息中主要包含ANR的基本信息,包括哪些进程占用的CPU和发生了ANR的进程和类的信息,同时我们可以分析CPU使用率得知ANR的简单情况;如果CPU使用率很高,接近100%,可能是在进行大规模的计算更可能是陷入死循环;如果CUP使用率很低,说明主线程被阻塞了,并且当IOwait很高,可能是主线程在等待I/O操作的完成.

  1. 拉取anr log文件 通过adb pull /data/anr/traces.txt命令获取到traces.txt文件
  2. 分析traces.txt文件
    ANR Logcat信息可以帮助我们分析引发ANR的具体类的信息以及ANR的类型,但是却无法帮助我们定位到具体引发问题的代码行,这时候就需要借助ANR发生过程中生成的堆栈信息文件data/anr/trace.txt
    由于trace文件记录的是整个手机的ANR日志,所以我们需要根据进程名(包名)和ANR发生的时间找到对应日志,具体以pid 进程id开始,以pid进程id结束。由于阻塞始终会发生在主线程,因此我们需要关注线程名为main的线程状态。
    根据logcat分析的ANR类型和问题代码块,很容易我们就可以分析到导致ANR的具体原因

参考文章:
Eclipse MAT内存分析 https://mp.csdn.net/mdeditor/71082465
内存泄漏分析工具使用以及如何分析 https://blog.csdn.net/hpc19950723/article/details/53561659

以上是关于Andrioid 性能优化基础的主要内容,如果未能解决你的问题,请参考以下文章

Andrioid进程保护

Vue性能优化

Vue性能优化

Vue性能优化写法

数据库优化MySQL性能优化基础

关于render渲染优化