Android性能编程中的几个方面

Android性能优化

谈Android性能优化,总结起来分为四大问题:流畅、稳定、省电、省流量。

1、流畅

我们试着分析下APP操作起来感觉不流畅的原因:1、因为网络请求而等待感觉卡顿;2、绘制页面时无法及时显示而感觉卡顿。第一个原因受当时用户所在的环境影响较大,可能网络较差或者是服务器处理时间过长等,这个时候优化就得从网络数据缓存、压缩传输数据、使用DNS服务、后台服务器优化等方面着手。但是这方面APP端能做的有限,我们的重点还是关注页面绘制优化。

1.1、Android绘制原理

APP通过对View树的递归完成测量、布局和绘制之后会通过IPC将这一帧的数据发送给SurfaceFlinger服务进程,SurfaceFlinger拿到数据之后开始进行渲染,然后再刷新屏幕。这里需要注意的是屏幕的刷新机制:
Android每16ms发送VSNCY同步信号来发起一次屏幕的刷新。在显示内容的数据内存上采用了双缓冲机制,一个为前台缓冲区,一个为后台缓冲区。只有当另一个缓冲区准备好数据之后才会通知显示设备切换缓冲区中的数据,这样能有效的解决屏幕闪烁的问题。在收到VSNCY信号时,假如当前开始渲染帧A的数据,同时CPU开始处理下一帧B的数据,当下一次VSNCY信号到来时,显示设备应该切换至帧B,这时如果CPU还没处理完帧B的数据,那么显示设备依然只能显示A,也就是发生了丢帧的情况(从直观角度上来看就是卡顿)。当然如果渲染帧A用时时超过了16ms,同样的也会发生丢帧的情况。
那么通过简单的了解了绘制原理,我们就可以知道了导致卡顿的原因是:

  • 绘制一帧需要耗费很长时间
  • 主线程在16ms内无法完成数据的准备
1.2、优化绘制内容
1.2.1、布局优化
  • 减少层级
    因为层级越少,ViewTree在递归测量和绘制的时间就会越短。所以我们可以通过合理的使用RelativeLayout和LineaLayout,合理使用Merge标签(merge标签只能作为复用布局的root元素来使用)。关于查看layout层级关系时,我们可以通过hierarchy View工具来查看。

    • linearLayout
      如果设置了weight(权重的话)会测量两次 第一次是测量子View的宽高;第二次的测量,父视图会把剩余的宽度按照weight值的大小平均分配给相应的子视图。
    • relativeLayout
      会进行两次测量 分别是垂直方向和水平方向 来确定View的位置
  • 加快显示
    使用ViewStub标签,只有显示调用显示方法时才会加载。但只能加载一次并且加载显示之后不能在通过ViewStub进行操作了。

  • 布局复用
    使用include标签来引入一个统一的布局,方便维护修改。

1.2.2、避免过度绘制

过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。简单点就是看不见的部分就不用绘制了。例如两个View有重叠的部分,被覆盖的部分UI就不用绘制了。一般产生过度绘制的主要原因有:

  • xml布局时控件有重叠并且都要设置自己的背景
  • 自定义控件时同一个区域被绘制了多次
    在自定义View时,如果发现有重叠部分,可以使用clipRect()来控制绘制的区域。

检测是否过度绘制,可以使用开发者模式开发过度绘制选项,就可以看到过度绘制的区域。

1.3、检测卡顿

我们如何才能检测到UI是否发生了卡顿现象呢?如果你了解handler机制的话,那自然也就知道Looper对象。检测和监控卡顿我们可以利用Looper中的Printer对象来实现。

 public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        final long end;
        try {
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (slowDispatchThresholdMs > 0) {
            final long time = end - start;
            if (time > slowDispatchThresholdMs) {
                Slog.w(TAG, "Dispatch took " + time + "ms on "
                        + Thread.currentThread().getName() + ", h=" +
                        msg.target + " cb=" + msg.callback + " msg=" + msg.what);
            }
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        final long newIdent = Binder.clearCallingIdentity();
        msg.recycleUnchecked();
    }
}

在每个message处理的前后都会调用Printer.println()方法,如果主线程卡住了说明dispatchMessage内部出现了问题。根据这个我们就能检测出UI是否发生了卡顿。

2、内存优化

内存优化也是性能优化中的重要一项,当内存发生泄漏时就有可能导致OOM,而且当内存发生抖动也就是gc频繁时也会导致卡顿。因为gc时所有的线程都会停止工作,因此我们需要减少gc的频率。我们都知道gc的作用的回收无任何引用的对象占据的内存空间,那怎么来判断对象是否被引用了呢?gc会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。那么什么时候会引起gc呢?
Android系统中为了整个系统的内存控制需要,为每一个应用都设置了一个硬性的Dalvik heap size最大限制阀值。

  • 当分配内存时发现内存不够的情况下引起的gc;
  • 当内存达到一定的阀值时出发gc;
  • 显示的调用gc。

所以我们不应该频繁的显示的调用gc。

2.1、避免内存泄露

分配出去的内存,当不需要时对这部分内存没有收回,这时就发送了内存泄露。常见的内存泄露场景:

  • 资源性对象未关闭
    资源性对象(coursor、file等),在不使用的时候应该及时关闭他们。
  • 注册对象未注销
    如果事件注册后未注销,会导致观察者列表中维持着对象的引用,阻止垃圾回收,一般发生在注册广播接收器、注册观察者等。
  • 类的静态变量持有大数据对象
    静态变量长期维持对象的引用,阻止垃圾回收,会造成内存不足等问题。
  • 非静态内部类的静态实例
    非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被系统回收。
  • Handler临时性内存泄露
    Message发出之后会存放在messageQueue中,如果这个message还没来的及处理,Activity退出了,但是message对象的target是指向handle,这样就会导致Handler无法回收。如果这个handler是非静态的,还会导致Activity不会别回收。
  • 容器中的对象没清理造成的内存泄露
    通常把一些对象的引用加入集合中,在不需要该对象时,如果没有把它的引用从集合中清理掉,这个集合就会越来越大。
  • webView内存泄露
    我们都知道WebView都存在内存泄露的问题,在应用中只要使用一次webview,那这部分的内存就不会释放掉。通常解决方案就是为它单独开启一个进程。

2.2、内存优化

当然是不是没有内存泄露就可以了呢?不是的,前面也提到了每个应用的内存使用都是有限制的,所以在内存的使用上我们就得注意了。

  • 减少不必要的内存开销;
    尽量使用自动装箱对象,这样能避免创建相对应对象;在内存上根据相应场景对内存进行复用,比如视图的复用(listView)、Bitmap对象的复用。

  • 使用最优的数据类型
    ArrayMap相对于HashMap而言避免了过多的内存开销,因为ArrayMap内部使用两个小数组实现。还有比如SpareArray等。

  • 避免使用枚举
    我们知道枚举在转换成class时都是静态常量,相比于普通常量占用内存要高很多。可以使用Android提供的注解IntDef、StringDef等来实现类型安全。

  • 图片内存优化
    图片在移动开发中占用的内存是非常突出的,那么关于图片的内存优化我们能做些什么呢?一般从压缩图片来减少内存的使用:降低位图的规格(RGB_8888/RGB_565等规格);根据需要缩放图片和压缩图片质量等。

2.3、内存泄露检测

  • 借助第三方库LeakCanary检测内存泄露
  • 借助android studio提供的Android profile工具分析内存

3、数据存储优化

数据的存储方式主要分为ContentProvider、文件、sharedPreference及sqlite数据库四种数据存储方式。这里主要看SQLite数据库的一些关于性能的注意点。

  • 使用SQLiteStatement插入数据
    与使用普通的执行executeSql()而言,它的插入数据花费的时间更短,而且能在一定的程度上防止SQL语句的注入。
  • 使用事务
    频繁的插入数据,在没有显示的创建事务时,插入时会频繁的创建事务会影响插入的效率。显式创建事务时能更快的插入数据,另外开启事务能够保证原子性提交。

4、代码优化

尽管我们平时在写代码的时候需要时刻注意了代码编写规范,但是还是会有些小问题存在。那么我们就需要借助一些代码静态扫描工具来检测我们的代码,比使用Android studio自带的Android Lint工具、findbugs等。当然我们可能也需要收集一些我们代码没有捕获到的异常,我们可以利用Java虚拟机为每个进程都设置了一个UNcaughtExceptionHandler,实现这个接口就能收集到没有捕获的异常,然后返回到我们的服务器,根据这些异常再进行定位问题。

5、耗电优化

我们可以借助google提供的Android系统电量分析工具Battery Historian,通过这个可以直观的展示出手机的电量消耗过程。用法:

1、初始化Battery Historian;

adb shell dumpsys batterystats --enable full-wake-history  
adb dumpsys batterystats --reset  

2、 初始化完成之后,操作需要测量电量的一些场景;
3、保存数据

adb bugreport > bugreport.txt

保存完数据之后,可以借助Battery Historian工具来打开文件查看具体的耗电情况。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容