热门标签 | 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帧

推荐阅读
  • 自然语言处理(NLP)——LDA模型:对电商购物评论进行情感分析
    目录一、2020数学建模美赛C题简介需求评价内容提供数据二、解题思路三、LDA简介四、代码实现1.数据预处理1.1剔除无用信息1.1.1剔除掉不需要的列1.1.2找出无效评论并剔除 ... [详细]
  • JUC(三):深入解析AQS
    本文详细介绍了Java并发工具包中的核心类AQS(AbstractQueuedSynchronizer),包括其基本概念、数据结构、源码分析及核心方法的实现。 ... [详细]
  • 微信公众号推送模板40036问题
    返回码错误码描述说明40001invalidcredential不合法的调用凭证40002invalidgrant_type不合法的grant_type40003invalidop ... [详细]
  • 本文是Java并发编程系列的开篇之作,将详细解析Java 1.5及以上版本中提供的并发工具。文章假设读者已经具备同步和易失性关键字的基本知识,重点介绍信号量机制的内部工作原理及其在实际开发中的应用。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 深入解析CAS机制:全面替代传统锁的底层原理与应用
    本文深入探讨了CAS(Compare-and-Swap)机制,分析了其作为传统锁的替代方案在并发控制中的优势与原理。CAS通过原子操作确保数据的一致性,避免了传统锁带来的性能瓶颈和死锁问题。文章详细解析了CAS的工作机制,并结合实际应用场景,展示了其在高并发环境下的高效性和可靠性。 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • 在iOS开发中,基于HTTPS协议的安全网络请求实现至关重要。HTTPS(全称:HyperText Transfer Protocol over Secure Socket Layer)是一种旨在提供安全通信的HTTP扩展,通过SSL/TLS加密技术确保数据传输的安全性和隐私性。本文将详细介绍如何在iOS应用中实现安全的HTTPS网络请求,包括证书验证、SSL握手过程以及常见安全问题的解决方法。 ... [详细]
  • 从0到1搭建大数据平台
    从0到1搭建大数据平台 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 深入解析 Lifecycle 的实现原理
    本文将详细介绍 Android Jetpack 中 Lifecycle 组件的实现原理,帮助开发者更好地理解和使用 Lifecycle,避免常见的内存泄漏问题。 ... [详细]
  • Ansible:自动化运维工具详解
    Ansible 是一款新兴的自动化运维工具,基于 Python 开发,集成了多种运维工具(如 Puppet、CFEngine、Chef、Func 和 Fabric)的优点,实现了批量系统配置、程序部署和命令执行等功能。本文将详细介绍 Ansible 的架构、特性和优势。 ... [详细]
  • 本文介绍了如何利用HTTP隧道技术在受限网络环境中绕过IDS和防火墙等安全设备,实现RDP端口的暴力破解攻击。文章详细描述了部署过程、攻击实施及流量分析,旨在提升网络安全意识。 ... [详细]
  • poj 3352 Road Construction ... [详细]
  • 大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式
    大类|电阻器_使用Requests、Etree、BeautifulSoup、Pandas和Path库进行数据抓取与处理 | 将指定区域内容保存为HTML和Excel格式 ... [详细]
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社区 版权所有