Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿
原標(biāo)題:Android性能優(yōu)化實(shí)戰(zhàn)之界面卡頓
作者:紅橙Darren
https://www.jianshu.com/p/18bb507d6e62
今天是個(gè)奇怪的日子,有三位同學(xué)找我,都是關(guān)于界面卡頓的問題,問我能不能幫忙解決下。由于性能優(yōu)化涉及的知識點(diǎn)比較多,我一時(shí)半會也無法徹底回答。恰好之前在做需求時(shí)也遇到了一個(gè)卡頓的問題,因此今晚寫下這篇卡頓優(yōu)化的文章,希望對大家有所幫助。
1. 查找卡頓原因
從上面的現(xiàn)象來看,應(yīng)該是主線程執(zhí)行了耗時(shí)操作引起了卡頓,因?yàn)檎;瑒邮菦]問題的,只有在刷新數(shù)據(jù)的時(shí)候才會出現(xiàn)卡頓。至于什么情況下會引起卡頓,之前在自定義 View 部分已有詳細(xì)講過,這里就不在啰嗦。我們猜想可能是耗時(shí)引起的卡頓,但也不能 100% 確定,況且我們也并不知道是哪個(gè)方法引起的,因此我們只能借助一些常用工具來分析分析,我們打開 Android Device Monitor 。
圖:打開 Android Device Monitor
圖:查找耗時(shí)方法
2. RxJava 線程切換
我們找到了是高斯模糊處理耗時(shí)導(dǎo)致了界面卡頓,那現(xiàn)在我們把高斯模糊算法處理放入子線程中去,處理完后再次切換到主線程,這里采用 RxJava 來實(shí)現(xiàn)。
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);
}
});
關(guān)于響應(yīng)式編程思想和 RxJava 的實(shí)現(xiàn)原理大家可以參考以下幾篇文章:
第三方開源庫 RxJava - 基本使用和源碼分析
https://www.jianshu.com/p/3e8fa8db6db1
第三方開源庫 RxJava - 自己動手寫事件變換
https://www.jianshu.com/p/b3b0170152ff
第三方開源庫 RxJava - Android實(shí)用開發(fā)場景
https://www.jianshu.com/p/2bb332f39f7d
3. 高斯模糊算法分析
把耗時(shí)操作放到子線程中去處理,的確解決了界面卡頓問題。但這其實(shí)是治標(biāo)不治本,我們發(fā)現(xiàn)圖片加載處理異常緩慢,內(nèi)存久高不下有時(shí)可能會導(dǎo)致內(nèi)存溢出。接下來我們來分析一下高斯模糊的算法實(shí)現(xiàn):
看上面這幾張圖,我們通過怎樣的操作才能把第一張圖處理成下面這兩張圖?其實(shí)就是模糊化,怎么才能做到模糊化?我們來看下高斯模糊算法的處理過程。再上兩張圖:
所謂"模糊",可以理解成每一個(gè)像素都取周邊像素的平均值。上圖中,2是中間點(diǎn),周邊點(diǎn)都是1。"中間點(diǎn)"取"周圍點(diǎn)"的平均值,就會變成1。在數(shù)值上,這是一種"平滑化"。在圖形上,就相當(dāng)于產(chǎn)生"模糊"效果,"中間點(diǎn)"失去細(xì)節(jié)。
為了得到不同的模糊效果,高斯模糊引入了權(quán)重的概念。上面分別是原圖、模糊半徑3像素、模糊半徑10像素的效果。模糊半徑越大,圖像就越模糊。從數(shù)值角度看,就是數(shù)值越平滑。接下來的問題就是,既然每個(gè)點(diǎn)都要取周邊像素的平均值,那么應(yīng)該如何分配權(quán)重呢?如果使用簡單平均,顯然不是很合理,因?yàn)閳D像都是連續(xù)的,越靠近的點(diǎn)關(guān)系越密切,越遠(yuǎn)離的點(diǎn)關(guān)系越疏遠(yuǎn)。因此,加權(quán)平均更合理,距離越近的點(diǎn)權(quán)重越大,距離越遠(yuǎn)的點(diǎn)權(quán)重越小。對于這種處理思想,很顯然正太分布函數(shù)剛好滿足我們的需求。但圖片是二維的,因此我們需要根據(jù)一維的正太分布函數(shù),推導(dǎo)出二維的正太分布函數(shù):
圖:二維正太分布函數(shù)
圖:權(quán)重處理
if(radius < 1) { //模糊半徑小于1
return(null);
}
intw = bitmap.getWidth();
inth = bitmap.getHeight();
// 通過 getPixels 獲得圖片的像素?cái)?shù)組
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;
// 循環(huán)行
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;
// 循環(huán)每一列
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++) {
// 與上面代碼類似 ......
對于部分哥們來說,上面的函數(shù)和代碼可能看不太懂。我們來講通俗一點(diǎn),一方面如果我們的圖片越大,像素點(diǎn)也就會越多,高斯模糊算法的復(fù)雜度就會越大。如果半徑 radius 越大圖片會越模糊,權(quán)重計(jì)算的復(fù)雜度也會越大。因此我們可以從這兩個(gè)方面入手,要么壓縮圖片的寬高,要么縮小 radius 半徑。但如果 radius 半徑設(shè)置過小,模糊效果肯定不太好,因此我們還是在寬高上面想想辦法,接下來我們?nèi)タ纯?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);
// 關(guān)鍵在于這兩行代碼,如果沒有設(shè)置或者獲取不到圖片的寬高,就會加載原圖
inttargetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
inttargetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
// 計(jì)算壓縮比例
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 緩存
最后我們還可以再做一些優(yōu)化,數(shù)據(jù)沒有改變時(shí)不去刷新數(shù)據(jù),還有就是采用 LruCache 緩存,相同的高斯模糊圖像直接從緩存獲取。需要提醒大家的是,我們在使用之前最好了解其源碼實(shí)現(xiàn),之前有見到同事這樣寫過:
/**
* 高斯模糊緩存的大小 4M
*/
privatestaticfinal intBLUR_CACHE_SIZE = 4* 1024* 1024;
/**
* 高斯模糊緩存,防止刷新時(shí)抖動
*/
privateLruCache blurBitmapCache = newLruCache(BLUR_CACHE_SIZE);
// 偽代碼 ......
// 有緩存直接設(shè)置
Bitmap blurBitmap = blurBitmapCache. get(item.userResp.headPortraitUrl);
if(blurBitmap != null) {
recommendBgIv.setImageBitmap(blurBitmap);
return;
}
// 從后臺獲取,進(jìn)行高斯模糊后,再緩存 ...
這樣寫有兩個(gè)問題,第一個(gè)問題是我們發(fā)現(xiàn)整個(gè)應(yīng)用 OOM 了都還可以緩存數(shù)據(jù),第二個(gè)問題是 LruCache 可以實(shí)現(xiàn)比較精細(xì)的控制,而默認(rèn)緩存池設(shè)置太大了會導(dǎo)致浪費(fèi)內(nèi)存,設(shè)置小了又會導(dǎo)致圖片經(jīng)常被回收。第一個(gè)問題我們只要了解其內(nèi)部實(shí)現(xiàn)就迎刃而解了,關(guān)鍵問題在于緩存大小該怎么設(shè)置?如果我們想不到好的解決方案,那么也可以去參考參考 Glide 的源碼實(shí)現(xiàn)。
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 是根據(jù)每個(gè) App 的內(nèi)存情況,以及不同手機(jī)設(shè)備的版本和分辨率,計(jì)算出一個(gè)比較合理的初始值。關(guān)于 Glide 源碼分析大家可以看看這篇:第三方開源庫 Glide - 源碼分析(補(bǔ)):https://www.jianshu.com/p/223dc6205da2
5. 最后總結(jié)
工具的使用其實(shí)并不難,相信我們在網(wǎng)上找?guī)灼恼聦?shí)踐實(shí)踐,就能很熟練找到其原因。難度還在于我們需要了解 Android 的底層源碼,第三方開源庫的原理實(shí)現(xiàn)。個(gè)人還是建議大家平時(shí)多去看看 Android Framework 層的源碼,多去學(xué)學(xué)第三方開源庫的內(nèi)部實(shí)現(xiàn),多了解數(shù)據(jù)結(jié)構(gòu)和算法。真正的做到治標(biāo)又治本.
— — — END — — —返回搜狐,查看更多
責(zé)任編輯:
總結(jié)
以上是生活随笔為你收集整理的Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android intent短信,and
- 下一篇: android studio换主题,为A