Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 625|回复: 0

Android 界面滑动卡顿分析与解决方案(入门)

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-4-18 13:11:47 | 显示全部楼层 |阅读模式

    Android 界面滑动卡顿分析与解决方案(入门)

    导致Android界面滑动卡顿主要有两个原因:
    1.UI线程(main)有耗时操作
    2.视图渲染时间过长,导致卡顿

    目前只讲第1点,第二点相对比较复杂待以后慢慢研究。

    众所周知,界面的流畅度主要依赖FPS这个值,这个值是通过(1s/渲染1帧所花费的时间)计算所得,FPS值越大视频越流畅,所以就需要渲染1帧的时间能尽量缩短。正常流畅度的FPS值在60左右,即渲染一帧的时间不应大于17ms。

    先看一个例子:

    Video1,该应用通过一组URL加载网络上的图片并显示在ListView中。从视频中可以看到滑动ListView存在一定的卡顿的现象。

    分析工具1:Android systrace

    打开Android开发工具中的DDMS,选中应用所在的进程并点击倒数第二个图标出现如下界面:
    android systrace

    填上相关信息后点击OK,此时Android systrace已经开始工作,你只需要正常操作手机复现卡顿现象即可。

    Android systrace 停止以后就会根据你上图中的设置生成trace.xml文件(注:此文件在火狐浏览器中无法正常浏览,需使用chrome)。

    打开trace.xml后观察surfaceflinger存在很多断断续续,分布不够均匀。
    2

    放大看
    3
    在某间隔出的时间是48ms,这远远的大于17ms,正是这个原因直接导致了界面的卡顿感。

    接着再看:
    4
    此处也存在不规则分布,可以看到主要的耗时操作在:

    5

    obtainView和decodeBitmap上面,看到这两个方法似乎是找到了罪魁祸首。那我们就看看这两个方法是在哪里被调用的。

    通过查找源代码知道obtainView这个方法被定义在AbsListView.java这个类中,主要作用就是获得一个和数据绑定过的视图,在这个方法中调用到了

     mAdapter.getView(position, transientView, this);
    

    这个方法大家再熟悉不过了,因为在自定义Adapter的时候都要去重写getView方法,那我们可以推测卡顿的原因很有可能出自getView。毕竟这个方法里有我们自己写的很大一坨代码。

    接着再看getView中最主要的方法

     mCacheWrapper.getBitmapFromCache(url, mHandler, position,2);
    

    具体实现是:

        public void getBitmapFromCache(final String urlString, Handler handler,
                final int position, final int scale) {
    
            final String key = hashKeyForString(urlString);
            final Message msg = handler.obtainMessage();
            Bitmap bitmapCache = mMemoryCache.get(key);
    
            if (bitmapCache != null) {
                msg.arg1 = position;
                msg.obj = bitmapCache;
                msg.sendToTarget();
                Log.d(TAG, "memory include the key");
                return;
            }
    
            //InputStream is = getInputStreamFromCache(key);
            //if (is != null) {
            //    Bitmap bitmap = getBitmap(is, key, scale);
            //    if (bitmap != null) {
            //        mMemoryCache.put(key, bitmap);
            //        msg.arg1 = position;
            //        msg.obj = bitmap;
            //        msg.sendToTarget();
            //        Log.d(TAG, "disk include the key");
            //    }
            //    return;
            //}
            mExecutor.execute(new Runnable() {
                @Override
                public void run() {
    
                    if (mCurrentTask.contains(key)) {
                        Log.d(TAG, "the key of task is still execute");
                        return;
                    }
                    mCurrentTask.add(key);
                   
    
                    boolean isOK = write2Cache(urlString);
                    if (isOK) {
                        InputStream is = getInputStreamFromCache(key);
                        Log.d(TAG, "the file is write to disk cache");
                        is = getInputStreamFromCache(key);
                        if (is != null) {
                            Bitmap bitmap = getBitmap(is, key, scale);
                            if (bitmap != null) {
                                mMemoryCache.put(key, bitmap);
                                msg.arg1 = position;
                                msg.obj = bitmap;
                                msg.sendToTarget();
                            }
                        }
                    }
                    mCurrentTask.remove(key);
                }
            });
        }
    

    看上面这段代码发现加注释代码存在一定的嫌疑,因为它在主线程中做了IO操作和bitmap的decode操作。

    我们稍微修改一下上面这段代码:

        public void getBitmapFromCache(final String urlString, Handler handler,
                final int position, final int scale) {
    
            final String key = hashKeyForString(urlString);
            final Message msg = handler.obtainMessage();
            Bitmap bitmapCache = mMemoryCache.get(key);
    
            if (bitmapCache != null) {
                msg.arg1 = position;
                msg.obj = bitmapCache;
                msg.sendToTarget();
                Log.d(TAG, "memory include the key");
                return;
            }
    
            mExecutor.execute(new Runnable() {
                @Override
                public void run() {
    
                    if (mCurrentTask.contains(key)) {
                        Log.d(TAG, "the key of task is still execute");
                        return;
                    }
                    //mCurrentTask.add(key);
                    //InputStream is = getInputStreamFromCache(key);
                    //if (is != null) {
                    //    Bitmap bitmap = getBitmap(is, key, scale);
                    //    if (bitmap != null) {
                    //        mMemoryCache.put(key, bitmap);
                    //        msg.arg1 = position;
                    //        msg.obj = bitmap;
                    //        msg.sendToTarget();
                    //        Log.d(TAG, "disk include the key");
                    //    }
                    //    mCurrentTask.remove(key);
                    //    return;
                    //}
    
                    boolean isOK = write2Cache(urlString);
                    if (isOK) {
                        Log.d(TAG, "the file is write to disk cache");
                        is = getInputStreamFromCache(key);
                        if (is != null) {
                            Bitmap bitmap = getBitmap(is, key, scale);
                            if (bitmap != null) {
                                mMemoryCache.put(key, bitmap);
                                msg.arg1 = position;
                                msg.obj = bitmap;
                                msg.sendToTarget();
                            }
                        }
                    }
                    mCurrentTask.remove(key);
                }
            });
        }
    

    将置灰出的代码移动到主线程以外,在看看滑动流畅读video2.mp4
    可以看到卡顿现象已经没有了,那罪魁祸首就是在主线程中有IO操作和bitmap的decode操作引起的。

    上述分析过程跳跃性比较大,这里再推荐一种简单直观的方法:

    分析工具2:Method Profiling.

    还是打开DDMS,选中你的应用,点击第六个图标,
    6

    这边默认OK
    7

    点击OK开始抓取,接着滑动手机复现卡顿现象。最后再次点击第六个钮即可。
    8

    这里只看上图中的main就可以了。
    9

    点击main方法后会展开它的父方法(即调用main的方法)和它的子方法(即在main中调用的方法)。这里一般点击后面百分数较大的那个子方法(百分数表示方法执行所占用的cpu时间)。
    接着要做的就是一步一步往下点,直到找到我们要找的耗时操作。

    最后我们还是来到了这边
    10

    上图已经定位了decodeStream方法,再往下走也是进到framework没意义了。剩下的就是怎么将decodeStream放到主线程以外的线程的事情了。

    总结:

    主线程中不要放置耗时的操作,耗时操作可以扔Thread再通过Handler与主线程同步或使用AsyncTask来完成耗时操作。

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-15 12:27 , Processed in 0.073534 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表