原标题:Android性能优化实战之界面卡顿

作者:红橙Darren

https://www.jianshu.com/p/18bb507d6e62

今天是个奇怪的日子,有三位同学找我,都是关于界面卡顿的问题,问我能不能帮忙解决下。由于性能优化涉及的知识点比较多,我一时半会也无法彻底回答。恰好之前在做需求时也遇到了一个卡顿的问题,因此今晚写下这篇卡顿优化的文章,希望对大家有所帮助。

1. 查找卡顿原因

从上面的现象来看,应该是主线程执行了耗时操作引起了卡顿,因为正常滑动是没问题的,只有在刷新数据的时候才会出现卡顿。至于什么情况下会引起卡顿,之前在自定义 View 部分已有详细讲过,这里就不在啰嗦。我们猜想可能是耗时引起的卡顿,但也不能 100% 确定,况且我们也并不知道是哪个方法引起的,因此我们只能借助一些常用工具来分析分析,我们打开 Android Device Monitor 。

图:打开 Android Device Monitor

图:查找耗时方法

2. RxJava 线程切换

我们找到了是高斯模糊处理耗时导致了界面卡顿,那现在我们把高斯模糊算法处理放入子线程中去,处理完后再次切换到主线程,这里采用 RxJava 来实现。

Observable.just(resource.getBitmap())

.map(bitmap -> {

// 高斯模糊

Bitmap blurBitmap = ImageUtil.doBlur(resource.getBitmap(), 100, false);

blurBitmapCache.put(path, blurBitmap);

returnblurBitmap;

}).subscribeOn(Schedulers.io())

.observeOn(AndroidSchedulers.mainThread())

.subscribe(blurBitmap -> {

if(blurBitmap != null) {

recommendBgIv.setImageBitmap(blurBitmap);

}

});

关于响应式编程思想和 RxJava 的实现原理大家可以参考以下几篇文章:

第三方开源库 RxJava - 基本使用和源码分析

https://www.jianshu.com/p/3e8fa8db6db1

第三方开源库 RxJava - 自己动手写事件变换

https://www.jianshu.com/p/b3b0170152ff

第三方开源库 RxJava - Android实用开发场景

https://www.jianshu.com/p/2bb332f39f7d

3. 高斯模糊算法分析

把耗时操作放到子线程中去处理,的确解决了界面卡顿问题。但这其实是治标不治本,我们发现图片加载处理异常缓慢,内存久高不下有时可能会导致内存溢出。接下来我们来分析一下高斯模糊的算法实现:

看上面这几张图,我们通过怎样的操作才能把第一张图处理成下面这两张图?其实就是模糊化,怎么才能做到模糊化?我们来看下高斯模糊算法的处理过程。再上两张图:

所谓"模糊",可以理解成每一个像素都取周边像素的平均值。上图中,2是中间点,周边点都是1。"中间点"取"周围点"的平均值,就会变成1。在数值上,这是一种"平滑化"。在图形上,就相当于产生"模糊"效果,"中间点"失去细节。

为了得到不同的模糊效果,高斯模糊引入了权重的概念。上面分别是原图、模糊半径3像素、模糊半径10像素的效果。模糊半径越大,图像就越模糊。从数值角度看,就是数值越平滑。接下来的问题就是,既然每个点都要取周边像素的平均值,那么应该如何分配权重呢?如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。对于这种处理思想,很显然正太分布函数刚好满足我们的需求。但图片是二维的,因此我们需要根据一维的正太分布函数,推导出二维的正太分布函数:

图:二维正太分布函数

图:权重处理

if(radius < 1) { //模糊半径小于1

return(null);

}

intw = bitmap.getWidth();

inth = bitmap.getHeight();

// 通过 getPixels 获得图片的像素数组

int[] pix = newint[w * h];

bitmap.getPixels(pix, 0, w, 0, 0, w, h);

intwm = w - 1;

inthm = h - 1;

intwh = w * h;

intdiv = radius + radius + 1;

intr[] = newint[wh];

intg[] = newint[wh];

intb[] = newint[wh];

intrsum, gsum, bsum, x, y, i, p, yp, yi, yw;

intvmin[] = newint[Math.max(w, h)];

intdivsum = (div + 1) >> 1;

divsum *= divsum;

intdv[] = newint[ 256* divsum];

for(i = 0; i < 256* divsum; i++) {

dv[i] = (i / divsum);

}

yw = yi = 0;

int[][] stack = newint[div][ 3];

intstackpointer;

intstackstart;

int[] sir;

intrbs;

intr1 = radius + 1;

introutsum, goutsum, boutsum;

intrinsum, ginsum, binsum;

// 循环行

for(y = 0; y < h; y++) {

rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;

// 半径处理

for(i = -radius; i <= radius; i++) {

p = pix[yi + Math.min(wm, Math.max(i, 0))];

sir = stack[i + radius];

// 拿到 rgb

sir[ 0] = (p & 0xff0000) >> 16;

sir[ 1] = (p & 0x00ff00) >> 8;

sir[ 2] = (p & 0x0000ff);

rbs = r1 - Math.abs(i);

rsum += sir[ 0] * rbs;

gsum += sir[ 1] * rbs;

bsum += sir[ 2] * rbs;

if(i > 0) {

rinsum += sir[ 0];

ginsum += sir[ 1];

binsum += sir[ 2];

} else{

routsum += sir[ 0];

goutsum += sir[ 1];

boutsum += sir[ 2];

}

}

stackpointer = radius;

// 循环每一列

for(x = 0; x < w; x++) {

r[yi] = dv[rsum];

g[yi] = dv[gsum];

b[yi] = dv[bsum];

rsum -= routsum;

gsum -= goutsum;

bsum -= boutsum;

stackstart = stackpointer - radius + div;

sir = stack[stackstart % div];

routsum -= sir[ 0];

goutsum -= sir[ 1];

boutsum -= sir[ 2];

if(y == 0) {

vmin[x] = Math.min(x + radius + 1, wm);

}

p = pix[yw + vmin[x]];

sir[ 0] = (p & 0xff0000) >> 16;

sir[ 1] = (p & 0x00ff00) >> 8;

sir[ 2] = (p & 0x0000ff);

rinsum += sir[ 0];

ginsum += sir[ 1];

binsum += sir[ 2];

rsum += rinsum;

gsum += ginsum;

bsum += binsum;

stackpointer = (stackpointer + 1) % div;

sir = stack[(stackpointer) % div];

routsum += sir[ 0];

goutsum += sir[ 1];

boutsum += sir[ 2];

rinsum -= sir[ 0];

ginsum -= sir[ 1];

binsum -= sir[ 2];

yi++;

}

yw += w;

}

for(x = 0; x < w; x++) {

// 与上面代码类似 ......

对于部分哥们来说,上面的函数和代码可能看不太懂。我们来讲通俗一点,一方面如果我们的图片越大,像素点也就会越多,高斯模糊算法的复杂度就会越大。如果半径 radius 越大图片会越模糊,权重计算的复杂度也会越大。因此我们可以从这两个方面入手,要么压缩图片的宽高,要么缩小 radius 半径。但如果 radius 半径设置过小,模糊效果肯定不太好,因此我们还是在宽高上面想想办法,接下来我们去看看 Glide 的源码:

privateBitmap decodeFromWrappedStreams(InputStream is,

BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,

DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, intrequestedWidth,

intrequestedHeight, boolean fixBitmapToRequestedDimensions,

DecodeCallbacks callbacks) throws IOException {

longstartTime = LogTime.getLogTime();

int[] sourceDimensions = getDimensions( is, options, callbacks, bitmapPool);

intsourceWidth = sourceDimensions[ 0];

intsourceHeight = sourceDimensions[ 1];

String sourceMimeType = options.outMimeType;

// If we failed to obtain the image dimensions, we may end up with an incorrectly sized Bitmap,

// so we want to use a mutable Bitmap type. One way this can happen is if the image header is so

// large (10mb+) that our attempt to use inJustDecodeBounds fails and we're forced to decode the

// full size image.

if(sourceWidth == -1|| sourceHeight == -1) {

isHardwareConfigAllowed = false;

}

intorientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);

intdegreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);

boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);

// 关键在于这两行代码,如果没有设置或者获取不到图片的宽高,就会加载原图

inttargetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;

inttargetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;

ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);

// 计算压缩比例

calculateScaling(

imageType,

is,

callbacks,

bitmapPool,

downsampleStrategy,

degreesToRotate,

sourceWidth,

sourceHeight,

targetWidth,

targetHeight,

options);

calculateConfig(

is,

decodeFormat,

isHardwareConfigAllowed,

isExifOrientationRequired,

options,

targetWidth,

targetHeight);

boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

// Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.

if((options.inSampleSize == 1|| isKitKatOrGreater) && shouldUsePool(imageType)) {

intexpectedWidth;

intexpectedHeight;

if(sourceWidth >= 0&& sourceHeight >= 0

&& fixBitmapToRequestedDimensions && isKitKatOrGreater) {

expectedWidth = targetWidth;

expectedHeight = targetHeight;

} else{

floatdensityMultiplier = isScaling(options)

? ( float) options.inTargetDensity / options.inDensity : 1f;

intsampleSize = options.inSampleSize;

intdownsampledWidth = ( int) Math.ceil(sourceWidth / ( float) sampleSize);

intdownsampledHeight = ( int) Math.ceil(sourceHeight / ( float) sampleSize);

expectedWidth = Math.round(downsampledWidth * densityMultiplier);

expectedHeight = Math.round(downsampledHeight * densityMultiplier);

if(Log.isLoggable(TAG, Log.VERBOSE)) {

Log.v(TAG, "Calculated target ["+ expectedWidth + "x"+ expectedHeight + "] for source"

+ " ["+ sourceWidth + "x"+ sourceHeight + "]"

+ ", sampleSize: "+ sampleSize

+ ", targetDensity: "+ options.inTargetDensity

+ ", density: "+ options.inDensity

+ ", density multiplier: "+ densityMultiplier);

}

}

// If this isn't an image, or BitmapFactory was unable to parse the size, width and height

// will be -1 here.

if(expectedWidth > 0&& expectedHeight > 0) {

setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);

}

}

// 通过流 is 和 options 解析 Bitmap

Bitmap downsampled = decodeStream( is, options, callbacks, bitmapPool);

callbacks.onDecodeComplete(bitmapPool, downsampled);

if(Log.isLoggable(TAG, Log.VERBOSE)) {

logDecode(sourceWidth, sourceHeight, sourceMimeType, options, downsampled,

requestedWidth, requestedHeight, startTime);

}

Bitmap rotated = null;

if(downsampled != null) {

// If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to

// the expected density dpi.

downsampled.setDensity(displayMetrics.densityDpi);

rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);

if(!downsampled. equals(rotated)) {

bitmapPool.put(downsampled);

}

}

returnrotated;

}

4. LruCache 缓存

最后我们还可以再做一些优化,数据没有改变时不去刷新数据,还有就是采用 LruCache 缓存,相同的高斯模糊图像直接从缓存获取。需要提醒大家的是,我们在使用之前最好了解其源码实现,之前有见到同事这样写过:

/**

* 高斯模糊缓存的大小 4M

*/

privatestaticfinal intBLUR_CACHE_SIZE = 4* 1024* 1024;

/**

* 高斯模糊缓存,防止刷新时抖动

*/

privateLruCache blurBitmapCache = newLruCache(BLUR_CACHE_SIZE);

// 伪代码 ......

// 有缓存直接设置

Bitmap blurBitmap = blurBitmapCache. get(item.userResp.headPortraitUrl);

if(blurBitmap != null) {

recommendBgIv.setImageBitmap(blurBitmap);

return;

}

// 从后台获取,进行高斯模糊后,再缓存 ...

这样写有两个问题,第一个问题是我们发现整个应用 OOM 了都还可以缓存数据,第二个问题是 LruCache 可以实现比较精细的控制,而默认缓存池设置太大了会导致浪费内存,设置小了又会导致图片经常被回收。第一个问题我们只要了解其内部实现就迎刃而解了,关键问题在于缓存大小该怎么设置?如果我们想不到好的解决方案,那么也可以去参考参考 Glide 的源码实现。

publicBuilder(Context context){

this.context = context;

activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);

screenDimensions = newDisplayMetricsScreenDimensions(context.getResources().getDisplayMetrics());

// On Android O+ Bitmaps are allocated natively, ART is much more efficient at managing

// garbage and we rely heavily on HARDWARE Bitmaps, making Bitmap re-use much less important.

// We prefer to preserve RAM on these devices and take the small performance hit of not

// re-using Bitmaps and textures when loading very small images or generating thumbnails.

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLowMemoryDevice(activityManager)) {

bitmapPoolScreens = 0;

}

}

// Package private to avoid PMD warning.

MemorySizeCalculator(MemorySizeCalculator.Builder builder) {

this.context = builder.context;

arrayPoolSize =

isLowMemoryDevice(builder.activityManager)

? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR

: builder.arrayPoolSizeBytes;

intmaxSize =

getMaxSize(

builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);

intwidthPixels = builder.screenDimensions.getWidthPixels();

intheightPixels = builder.screenDimensions.getHeightPixels();

intscreenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;

inttargetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);

inttargetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);

intavailableSize = maxSize - arrayPoolSize;

if(targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {

memoryCacheSize = targetMemoryCacheSize;

bitmapPoolSize = targetBitmapPoolSize;

} else{

floatpart = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);

memoryCacheSize = Math.round(part * builder.memoryCacheScreens);

bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);

}

if(Log.isLoggable(TAG, Log.DEBUG)) {

Log.d(

TAG,

"Calculation complete"

+ ", Calculated memory cache size: "

+ toMb(memoryCacheSize)

+ ", pool size: "

+ toMb(bitmapPoolSize)

+ ", byte array size: "

+ toMb(arrayPoolSize)

+ ", memory class limited? "

+ (targetMemoryCacheSize + targetBitmapPoolSize > maxSize)

+ ", max size: "

+ toMb(maxSize)

+ ", memoryClass: "

+ builder.activityManager.getMemoryClass()

+ ", isLowMemoryDevice: "

+ isLowMemoryDevice(builder.activityManager));

}

}

可以看到 Glide 是根据每个 App 的内存情况,以及不同手机设备的版本和分辨率,计算出一个比较合理的初始值。关于 Glide 源码分析大家可以看看这篇:第三方开源库 Glide - 源码分析(补):https://www.jianshu.com/p/223dc6205da2

5. 最后总结

工具的使用其实并不难,相信我们在网上找几篇文章实践实践,就能很熟练找到其原因。难度还在于我们需要了解 Android 的底层源码,第三方开源库的原理实现。个人还是建议大家平时多去看看 Android Framework 层的源码,多去学学第三方开源库的内部实现,多了解数据结构和算法。真正的做到治标又治本.

— — — END — — —返回搜狐,查看更多

责任编辑:

Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿相关推荐

  1. Android主线程耗时方法监控

    前言 相信各位大佬肯定在开发中遇到过这个问题:一顿操作猛如虎之后,总感觉程序有时候有点卡顿:或者自己自测得好好的,但是测试妹子的机器上却卡成狗. 那么,能不能做到运行APP之后,能自动地列出哪些类的哪 ...

  2. android主线程和子线程的区别

    android 主线程和子线程有什么区别 本文较为深入的分析了android中UI主线程与子线程.分享给大家供大家参考. 具体如下:在一个Android 程序开始运行的时候,会单独启动一个Proces ...

  3. iOS主线程耗时检测方案

    前言 主线程耗时是一个App性能的重要指标.主线程阻塞,立马会引起用户操作的卡顿,这是最直接的反应,所以是我们必须关注的一个性能点. 检测方案 Instrument - Time Profiler T ...

  4. css+动画优化,css3动画性能优化--针对移动端卡顿问题

    一.使用css,jquery,canvas制作动画 1.Canvas 优点:性能好,强大,支持多数浏览器(除了IE6.IE7.IE8),画出来的图形可以直接保存为 .png 或者 .jpg的图形: 缺 ...

  5. 太牛了!我把阿里、腾讯、字节跳动、美团等Android性能优化实战整合成了一个PDF文档

    安卓开发大军浩浩荡荡,经过近十年的发展,Android技术优化日异月新,如今Android 11.0 已经发布,Android系统性能也已经非常流畅,可以在体验上完全媲美iOS. 但是,到了各大厂商手 ...

  6. 【Android】Android主线程真的不让进行耗时操作吗?

    默认情况下,Android主线程(UI线程)不让进行网络请求,否则会抛出NetworkOnMainThreadException. 但是主线程还可以让程序员进行其它类型的耗时操作,比如读写磁盘数据.遍 ...

  7. Android手机为何不再卡顿?性能优化才是安卓起飞关键

    现在谈到挂载 Android系统的 手机,大部分人的印象早已不像前几年,几年前的安卓机那是用了一段时间就变得有点卡顿,用1年就卡的动弹不得,不得不每年都更换新机. 为什么以前会出现这种情况呢?其实主要 ...

  8. RxJava开发精要7 - Schedulers-解决Android主线程问题

    原文出自<RxJava Essentials> 原文作者 : Ivan Morgillo 译文出自 : 开发技术前线 www.devtf.cn 转载声明: 本译文已授权开发者头条享有独家转 ...

  9. Android 主线程与子线程区分和实践

    前言 这个是一个入门的话题但是也要通透的简单的说一下对于学过java或者Android的都知道,在代码中主线程不能进行耗时操作,子线程不能更新UI,比如在自定义view时,想要让View重绘,需要先判 ...

最新文章

  1. 【引用】ActionContext和ServletActionContext介绍
  2. C++win32平台日志类
  3. html %3c 不给转义,八个无法利用XSS漏洞的场景
  4. python计算矩阵方程_python/sympy求解矩阵方程的方法
  5. fpga mysql_FPGA的一些琐碎知识整理
  6. pytorch中的参数初始化方法
  7. 二叉树经典题之线索二叉树(中序)
  8. Windows中木马之后桌面被篡改的恢复方法
  9. 保障实时音视频服务体验,华为云原生媒体网络有7大秘籍
  10. 使用SpringMVC搭建第一个项目
  11. php apk 分包,H5打包出APP,分包
  12. MATLAB实现一组随机点分成两组,每组随机点分别与一个特定点连线
  13. 2020年10月最新免费加速下载百度网盘文件方法
  14. STM32采用串口DMA方式,发送数据
  15. 基于S变换的电压暂降检测研究及仿真设计
  16. DS18B20的CRC验证算法
  17. 牧牛区块链,区块链经济学应该关注的问题
  18. bootstrap-datetimepicker时间控件
  19. ABAP总结之二,BDC完整版
  20. JASS代码加翻译更新(第四篇)

热门文章

  1. 从零开始学习hadoop之发行版选择
  2. springMvc解决json中文乱码
  3. Computer Science Theory for the Information Age-3: 高维空间中的高斯分布和随机投影
  4. OSPF路由聚合实验(详细)
  5. VoIP系列:VoIP的劣势
  6. VB.NET实现DirectSound9 (6) 声音特效
  7. 仿联想商城laravel实战---7、lavarel中如何给用户发送邮件
  8. 使用 Chrome DevTools 调试 JavaScript
  9. sql中去掉换行符和回车符
  10. 大话Django之一:安装与启动