日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

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

發布時間:2025/7/14 Android 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android主线程耗时动画卡顿,Android性能优化实战之界面卡顿 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原標題: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性能优化实战之界面卡顿的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。