热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android性能优化之UI卡顿优化

Android应用性能优化性能优化分类卡顿优化内存优化电量优化网络优化启动优化、安装包体积优化官网性能优化指导(https:d

Android应用性能优化

性能优化分类

  1. 卡顿优化
  2. 内存优化
  3. 电量优化
  4. 网络优化
  5. 启动优化、安装包体积优化

官网性能优化指导(https://developer.android.com/topic/performance/index.html)

卡顿优化

卡顿:从用户角度说,App操作起来缓慢,响应不及时,列表滑动一顿一顿的,动画刷新不流畅等等一些直观感受。从系统角度来说,屏幕刷新的帧率不稳定,无法保证每秒绘制60帧,也就是说有掉帧的情况发生。

掉帧检测方案

Looper

Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。
下面看Looper.loop()方法源码:


    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;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            //处理消息前,打印开始日志
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            msg.target.dispatchMessage(msg);

            //处理完消息后,打印结束日志
            if (logging != null) {
                logging.println("<<<< + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

我们可以根据消息处理前后的日志输出作为检测点,计算出消息处理的耗时,如果超出16ms,说明发生了卡顿,此时就可以把UI线程的堆栈日志打印出来。


Looper.getMainLooper().setMessageLogging(new Printer() {
            private static final String START = ">>>>> Dispatching";
            private static final String END = "<<<<;

            @Override
            public void println(String x) {
                if (x.startsWith(START)) {
                    UiBlockLogMonitor.getInstance().startMonitor();
                }
                if (x.startsWith(END)) {
                    UiBlockLogMonitor.getInstance().stopMonitor();
                }
            }
        });

不过,由于系统定制的原因,打印出来的日志标识不一定标准,所以可以改为判断第一次日志输出和第二次日志输出。

Choreographer.FrameCallback

Choreographer官方说明(https://developer.android.com/reference/android/view/Choreographer.html)

Choreographer 编舞者,协调动画、输入和绘图的时间(api >= 16)。

Choreographer从显示子系统接收定时脉冲(如垂直同步),然后安排下一帧的渲染工作。
在开发中,我们并不直接使用Choreographer,当我们想要检测是否有丢帧发生时,可以利用Choreographer.FrameCallback回调的方式,获取每一帧开始绘制的时间,通过计算两帧之间的时间差,如果大于16ms,说明发生了丢帧。

//为Choreographer设置一个回调,当一帧开始渲染时触发。
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            long lastFrameTimeNanos = 0;
            long currentFrameTimeNanos = 0;

            @Override
            public void doFrame(long frameTimeNanos) {
                if (lastFrameTimeNanos == 0) {
                    lastFrameTimeNanos = frameTimeNanos;
                }
                currentFrameTimeNanos = frameTimeNanos;
                long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos - lastFrameTimeNanos, TimeUnit.NANOSECONDS);
                lastFrameTimeNanos = currentFrameTimeNanos;
                if (diffMs == 0) {
                    diffMs = (long) 16.7;
                }

                if (isShowFPS) {
                    long current = System.currentTimeMillis();
                    if (current - mLastFPSRefreshTs > refreshInterval) {
                        int fps = (int) (1000 / diffMs);
                        refreshFPS(fps);
                        mLastFPSRefreshTs = current;
                    }
                }

                if (diffMs > 16.7f) {
                    long droppedCount = (long) (diffMs / 16.7f);
                    if (droppedCount > 1) {
                        System.out.println("掉帧数 : " + droppedCount);
                    }
                }

                if (UiBlockLogMonitor.getInstance().isMonitor()) {
                    UiBlockLogMonitor.getInstance().stopMonitor();
                }

                if (isDetectContinue) {
                    UiBlockLogMonitor.getInstance().startMonitor();
                    Choreographer.getInstance().postFrameCallback(this);
                }
            }
        });

问题检测工具

当发生掉帧时,需要判断是什么原因导致了UI线程耗时过程或阻塞。这时需要借助一些开发工具来帮助定位。

systrace

systrace 官方说明:https://developer.android.com/studio/command-line/systrace.html
systrace.py 是一个命令行工具,位于 ../sdk/platform-tools/systrace目录下。在应用运行时,它可以帮助我们收集和分析所有进程的计时信息,包含了CPU调度、应用线程、磁盘活动等Android内核数据,然后生成一份HTML报告。

systace对检测应用UI表现非常有效,因为它可以分析你的代码和帧率来识别出问题区域,然后提出可能的解决方案。示例

如果其中的Expensive measure/layout 或 Long View#draw() 警告特别多,可能是因为页面层级比较深,导致测量、布局和渲染时间过长,从而引起掉帧。

检测布局层级是否太深最有效的工具就是开发者选项中的GPU过度绘制模式了,这个稍后会讲到。

systrace对每一种警告类型都做出了解释:

Scheduling delay
渲染一帧的工作被推迟了几个毫秒,从而导致了不合格。确保UI线程上的代码不会被其他线程上完成的工作阻塞,并且后台线程(例如,网络或位图加载)在android.os.Process#THREAD_PRIORITY_BACKGROUND中运行或更低,因此它们不太可能中断UI线程。

Expensive measure/layout pass
测量/布局花费了很长时间,导致掉帧,要避免在动画过程中触发重新布局。

Long View#draw()
记录无效的绘图命令花费了很长时间,在View或Drawable自定义视图时,要避免做耗时操作,尤其是Bitmap的分配和绘制。

Expensive Bitmap uploads
修改或新创建Bitmap视图要传送给GPU,如果像素总数很大,这个操作会很耗时。因此在每一帧中要尽量减少Bitmap变更的次数。

Inefficient View alpha usage
将alpha设置为半透明值(0

traceview

TraceView 是 Android SDK 中内置的一个工具,它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。我们可以在Android Profiler或DDMS中启动它。

使用这个工具最关键的地方就是要理解各个统计维度的含义:

方法执行时间
Incl Cpu Time: 执行方法X及子方法占用Cpu的时间
Excl Cpu Time: 执行方法X占用Cpu时间,不包含子方法

Incl Real Time: 执行方法X及子方法总时间
Excl Real Time: 执行方法x总时间

Cpu Time/Call: 每次执行方法X占用Cpu时间
Real Time/Call: 每次执行方法X总时间

占用CPU比例

Incl Cpu Time%
Excl Cpu Time%
Incl Real Time%
Excl Real Time%

以上各个时间占Cpu执行耗时的百分比

调用次数

Calls + Recur Calls/Total: 方法X调用次数和递归调用次数

使用时只需要关注 Incl Real Time、Real Time/Call、Calls + Recur Calls/Total这三个指标即可,找出应用包名下的耗时方法调用后加以优化。

GPU过度绘制调试模式

开发者选项 -> 调试GPU过度绘制

  • 原色:没有过度绘制
  • 蓝色:过度绘制1次
  • 绿色:过度绘制2次
  • 粉色:过度绘制3次
  • 红色:过度绘制4次或更多

请注意,这些颜色是半透明的,因此,您在屏幕上看到的确切颜色取决于您的界面内容。

可以通过此功能查看哪些页面的布局层级过深。

常见卡顿原因及解决方案

过度绘制

去除不必要的背景色
1. 设置窗口背景色为通用背景色,去除根布局背景色。
2. 若页面背景色与通用背景色不一致,在页面渲染完成后移除窗口背景色
3. 去除和列表背景色相同的Item背景色

布局视图树扁平化
1. 移除嵌套布局
2. 使用merge、include标签
3. 使用性能消耗更小布局(TableLayout、ConstraintLayout)

减少透明色,即alpha属性的使用
1. 通过使用半透明颜色值(#77000000)代替

其他
1. 使用ViewStub标签,延迟加载不必要的视图
2. 使用AsyncLayoutInflater异步解析视图

主线程耗时操作

  1. Json数据解析耗时(Cache类)
  2. 文件操作(获取所属渠道名称)
  3. Binder通信(获取系统属性(mac地址))
  4. 正则匹配(Hybird 通信)
  5. 相机操作:初始化、预览、停止预览、释放(反扫)
  6. 组件初始化(推送)
  7. 循环删除、创建View(更多页面)
  8. WebView首次初始化

处理方案评估:
异步 > 缓存 > 替代方案 > 保持原状

异步:
1. 登录、退出登录后的数据处理
2. 相机操作
3. 组件初始化

示例:

    //异步启动消息推送服务
    private void startPushAsync(Context context) {
        Subscription startPushSub = Observable.unsafeCreate(subscriber -> {
            MyPushManager.getInstance().startPush(context);
            MyPushManager.getInstance().connectHwPushAgent(mInteraction.getActivity());
        }).compose(executorTransformer.transformer())
                .subscribe(new DefaultSubscriber(context) {
                    @Override
                    protected void onFinally() {
                        super.onFinally();
                    }
                });
        addSubscription(startPushSub);
    }

缓存:
1. Cache类
2. 系统属性

示例:

//获取应用渠道标识
public static String getChannel(Context context) {
        if (!TextUtils.isEmpty(APP_CHANNEL)) {
            return APP_CHANNEL;
        }

        ...        

         zipfile = new ZipFile(sourceDir);
            Enumeration entries = zipfile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = ((ZipEntry) entries.nextElement());
                String entryName = entry.getName();
                if (entryName.contains(start_flag)) {
                    channel = entryName.replace(start_flag, "");
                    break;
                }
            }
}

替代方案:
1. Hybird通信中的一处正则匹配
2. 更多页面采用RecycleView嵌套

示例:


    private void dispatchMessage(WVJBMessage message) {
        String messageJSON = message2JSONObject(message).toString();
        //使用JSONObject的quote方法,代替正则替换,效率更高
        messageJSON = JSONObject.quote(messageJSON);
        messageJSON = messageJSON.substring(1, messageJSON.length() - 1);

        log("SEND", messageJSON);
        executeJavascript("WebViewJavascriptBridge._handleMessageFromObjC('"
                + messageJSON + "');");
    }

    旧版本
    String messageJSON = message2JSONObject(message).toString()
                .replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"")
                .replaceAll("\'", "\\\\\'").replaceAll("\n", "\\\\\n")
                .replaceAll("\r", "\\\\\r").replaceAll("\f", "\\\\\f");

保持原状:
1. WebView首次初始化耗时

提前加载一个WebView窗口 (没必要)
异步初始化WebView (不支持)

主线程挂起

  1. 异步线程与主线程竞争CPU资源

设置异步线程优先级为Process.THREAD_PRIORITY_BACKGROUND,减少与主线程的竞争。
有两种设置优先级的方式:Thread.currentThread().setPriority() 和 Process.setThreadPriority(),两种设置方式相互独立,应该使用后者。


Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND);

同时可以提高主线程的优先级

Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
  1. 频繁GC使主线程挂起

后续内存优化

冷启动白屏

设置欢迎页窗口背景为应用Logo

优化效果

测试设备:手机低配版(512M Rom)未安装第三方App
测试标准:最大、平均掉帧数
测试App:我厂App release版

商米低配版 优化前 优化后
最大掉帧数 65帧 48帧
平均掉帧数 15帧 7帧

推荐阅读
  • 我创建了一个新的AWSSSO(使用内部IDP作为身份源,因此不使用ActiveDirectory)。我能够登录AWSCLI、AWSGUI,但 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 开发笔记:Java是如何读取和写入浏览器Cookies的
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java是如何读取和写入浏览器Cookies的相关的知识,希望对你有一定的参考价值。首先我 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
  • Postgresql备份和恢复的方法及命令行操作步骤
    本文介绍了使用Postgresql进行备份和恢复的方法及命令行操作步骤。通过使用pg_dump命令进行备份,pg_restore命令进行恢复,并设置-h localhost选项,可以完成数据的备份和恢复操作。此外,本文还提供了参考链接以获取更多详细信息。 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • 简述在某个项目中需要分析PHP代码,分离出对应的函数调用(以及源代码对应的位置)。虽然这使用正则也可以实现,但无论从效率还是代码复杂度方面考虑ÿ ... [详细]
  • 阅读spring5源码DefaultSingletonBeanRegistry类遇到问题发现SpringBean中存在大量回调机制和aware接口,于是特意去了解 ... [详细]
  • 工作经验谈之-让百度地图API调用数据库内容 及详解
    这段时间,所在项目中要用到的一个模块,就是让数据库中的内容在百度地图上展现出来,如经纬度。主要实现以下几点功能:1.读取数据库中的经纬度值在百度上标注出来。2.点击标注弹出对应信息。3 ... [详细]
  • 五、RabbitMQ Java Client基本使用详解
    JavaClient的5.x版本系列需要JDK8,用于编译和运行。在Android上,仅支持Android7.0或更高版本。4.x版本系列支持7.0之前 ... [详细]
author-avatar
jiuyueling
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有