前言
全球手機市場中,Android/ target=_blank class=infotextkey>安卓和IOS一直占著主流市場,iphone手機給人的感覺就是流暢,而安卓手機卻一直是卡頓的代名詞
其實,安卓機剛上手時還是速度飛快的,并且基于它開放性的原則,受到多數用戶的喜愛,但通病是:運行一段時間后,反應變慢、容易卡頓,這也是iOS用戶不肯換安卓最主要的原因
Android出現使用不流暢,卡頓的主要原因
CPU使用率過高
● 手機固件有缺陷,導致CPU使用率始終過高,這時您刷一個穩定點的ROM就好了
● 開啟了過多的程序;這時您可以使用進程管理程序清理一下后臺進程
● 某個程序由于設計不當或者不兼容導致占用大量CPU資源,這時您可以使用手機安全衛士體檢里的運行監測(只勾選這個)查看當前所有正在運行程序的CPU占用,找到消耗資源特別多的,結束或者卸載它
● 您執行的某一個操作可能導致CPU過高(有時候也可能是查看CPU占用這個操作)
● CPU使用率查看軟件不準確,這時您可以用多個軟件查看
系統內存使用率過高
系統內存分為物理內存和虛擬內存
● 物理內存就是系統硬件提供的內存大小,是真正的內存;在linux下還有一個虛擬內存的概念,虛擬內存就是為了滿足物理內存的不足而提出的策略,它是利用磁盤空間虛擬出的一塊邏輯內存,用作虛擬內存的磁盤空間被稱為 交換空間(Swap Space)
● linux的內存管理采取的是分頁存取機制,為了保證物理內存能得到充分的利用,內核會在適當的時候將物理內存中不經常使用的數據塊自動交換到虛擬內存中,而將經常使用的信息保留到物理內存。而進行這種交換所遵循的依據是“LRU”算法(Least Recently Used,最近最少使用算法)
● 無論是使用桌面操作系統還是移動操作系統,很多人都喜歡隨時關注內存,一旦發現內存使用率過高就難受,忍不住的要殺進程以釋放內存。這種習慣很大程度上都是源自windows系統,當然這在Windows下也確實沒錯。然而很多人在使用Linux系統時仍然有這個習慣,甚至到了Android系統下,也改不掉(尤其是Android手機剛出現的幾年),Clean Master等各種清理軟件鋪天蓋地。毫不客氣的說,Windows毒害了不少人!當然,這也不能怪Windows,畢竟Windows的普及率太高了,而大部分普通用戶(甚至一些計算機相關人員)又不了解Windows和Linux在內存管理方面的差別
屏幕刷新丟幀
刷新頻率
● 屏幕的刷新頻率(Refresh Rate),就是一秒內屏幕刷新的次數
● 我們知道,在某一個時刻,將圖像數據涂到屏幕上我們就能直觀地看到一幅靜態的畫面,但這顯然不能滿足用戶需求。我們需要看到的是屏幕上的動畫——即不斷切換的連續銜接的畫面。在動畫術語中,每一張這樣的銜接畫面被稱作幀。也就是說,為了看到動畫,我們需要以恒定的速度取到連續的幀,并將幀涂到屏幕上
如上,要顯示屏幕動畫,我們要設置兩個時機
● 時機一:生成幀,產生了新的畫面(幀),將其填充到 FrameBuffer 中,這個過程由 CPU(計算繪制需求)和 GPU(完成數據繪制)完成
● 時機二:顯示幀,顯示屏顯示完一幀圖像之后,等待一個固定的間隔后,從 FrameBuffer 中取下一幀圖像并顯示,這個過程由 GPU 完成
對于設備而言,其屏幕的刷新頻率 就相當于顯示幀的時機和速度,可以看做是額定不變的(而生成幀速度對應我們通常說的幀率)
刷新頻率這個參數是手機出廠就決定的,取決于硬件的固定參數;目前大多數設備的刷新率是 60Hz,也就是一秒刷新60次,所以每次屏幕刷新的過程占用時間就是16ms(1000/60)左右,這個是固定參數,運行過程中,不會發生改變
UI線程阻塞
當一個應用程序啟動之后,android系統會為這個應用創建一個主線程;這個線程非常重要,它負責渲染視圖,分發事件到響應監聽器并執行,對界面進行輪詢監聽;因此,一般也叫做“UI線程”
● android系統不會給應用程序的多個元素組件,建立多個線程來執行;一個視圖Activity中的多個view組件運行在同一個UI線程中;因此,多個view組件的監聽器的執行可能會相互影響
● 例如:當在UI線程中執行耗時操作,比如訪問網絡,訪問數據庫等,則會導致UI線程阻塞;當UI線程阻塞,則屏幕就會出現卡死情況;這樣用戶體驗非常差;當線程阻塞超過5秒以后,android系統有可能進行干預,彈出對話框詢問是否關閉應用程序
Android 繪制UI方式
把圖形直接繪制到畫布上(Canvas對象),這種方法可以通過獨立的線程來管理surfaceView對象,并由獨立線程來管理繪制過
● Android中的圖形系統采用 Client/Server 架構。Server (即SurfaceFlinger)主要由 C++ 代碼編寫而成。Client 端代碼分為兩部分,一部分是由 JAVA 提供的供應用程序使用的 API,另一部分則是用 C++ 寫成的底層實現
● 每個應用可能有一個或多個surface(含surface的情況下),surfaceFlinger是本地服務,用于管理surface的創建、銷毀、zorder合成。View及其子類(如TextView, Button)要畫在surface上。每個surface創建一個Canvas對象 (但屬性時常改變),用來管理view在surface上的繪圖操作,如畫點畫線。每個canvas對象對應一個bitmap,存儲畫在surface上的內容。當然這里還有個Layer的概念,在后面創建surface流程中我們再介紹
surface 創建
SurfaceControl surfaceControl = winAnimator.createSurfaceLocked();
if (surfaceControl != null)
{
outSurface.copyFrom(surfaceControl);
if (SHOW_TRANSACTIONS) Slog.i(TAG,
" OUT SURFACE " + outSurface + ": copied");
}
else {
// For some reason there isn't a surface. Clear the
// caller's object so they see the same state.
outSurface.release();
}
Surface的繪制
在Android系統刷新過程中ViewRoot會調用performTraversals方法并依次調用performMeasure、performLayout、performDraw。在performDraw中會區分是否支持硬件加速,如果支持直接通過OPENGL做硬件加速繪制,如果不支持則走軟件繪制。因為我們在獨立線程繪制過程中一般走的是軟件繪制。這里對軟件繪制的方法做介紹以掌握如何在獨立線程中繪制UI
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
Canvas canvas;
try {
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
canvas = mSurface.lockCanvas(dirty);
// The dirty rectangle can be modified by Surface.lockCanvas()
//noinspection ConstantConditions
if (left != dirty.left || top != dirty.top || right != dirty.right ||
bottom != dirty.bottom) {
attachInfo.mIgnoreDirtyState = true;
}
// TODO: Do this in native
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
Log.e(TAG, "Could not lock surface", e);
// Don't assume this is due to out of memory, it could be
// something else, and if it is something else then we could
// kill stuff (or ourself) for no reason.
mLayoutRequested = true; // ask wm for a new surface next time.
return false;
}
try {
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are Applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
if (!canvas.isOpaque() || yoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
attachInfo.mDrawingTime = SystemClock.uptimeMillis();
mView.mPrivateFlags |= View.PFLAG_DRAWN;
if (DEBUG_DRAW) {
Context cxt = mView.getContext();
Log.i(TAG, "Drawing: package:" + cxt.getPackageName() +
", metrics=" + cxt.getResources().getDisplayMetrics() +
", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
}
ry {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
mView.draw(canvas);
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
if (!attachInfo.mSetIgnoreDirtyState) {
// Only clear the flag if it was not set during the mView.draw() call
attachInfo.mIgnoreDirtyState = false;
}
}
} finally {
try {
surface.unlockCanvasAndPost(canvas);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Could not unlock surface", e);
mLayoutRequested = true; // ask wm for a new surface next time.
//noinspection ReturnInsideFinallyBlock
return false;
}
if (LOCAL_LOGV) {
Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
}
}
return true;
}
其中關鍵就是canvas = mSurface.lockCanvas(dirty) 與 surface.unlockC anvasAndPost(canvas);先lockCanvas,繪制UI,最后通過unlockCanvasAndPost通知surfaceFlinger先做zorder組合顯示。
lockCanvas(dirty) 就是通過JNI調用nativeLockCanvas返回一個Canvas下面看nativeLockCanvas的實現。
sttic void nativeLockCanvas(JNIEnv* env, jclass clazz, jint nativeObject, jobject canvasObj, jobject dirtyRectObj) { sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
if (!isSurfaceValid(surface)) {
doThrowIAE(env);
return;
}
// get dirty region
Region dirtyRegion;
if (dirtyRectObj) {
Rect dirty;
dirty.left = env->GetIntField(dirtyRectObj, gRectClassInfo.left);
dirty.top = env->GetIntField(dirtyRectObj, gRectClassInfo.top);
dirty.right = env->GetIntField(dirtyRectObj, gRectClassInfo.right);
dirty.bottom = env->GetIntField(dirtyRectObj, gRectClassInfo.bottom);
if (!dirty.isEmpty()) {
dirtyRegion.set(dirty);
}
} else {
dirtyRegion.set(Rect(0x3FFF, 0x3FFF));
}
ANativeWindow_Buffer outBuffer;
Rect dirtyBounds(dirtyRegion.getBounds());
status_t err = surface->lock(&outBuffer, &dirtyBounds);
dirtyRegion.set(dirtyBounds);
if (err < 0) {
const char* const exception = (err == NO_MEMORY) ?
OutOfResourcesException :
"java/lang/IllegalArgumentException";
jniThrowException(env, exception, NULL);
return;
}
// Associate a SkCanvas object to this surface
env->SetIntField(canvasObj, gCanvasClassInfo.mSurfaceFormat, outBuffer.format);
SkBitmap bitmap;
ssize_t bpr = outBuffer.stride * bytesPerPixel(outBuffer.format);
bitmap.setConfig(convertPixelFormat(outBuffer.format), outBuffer.width, outBuffer.height, bpr);
if (outBuffer.format == PIXEL_FORMAT_RGBX_8888) {
bitmap.setIsOpaque(true);
}
if (outBuffer.width > 0 && outBuffer.height > 0) {
bitmap.setPixels(outBuffer.bits);
} else {
// be safe with an empty bitmap.
bitmap.setPixels(NULL);
}
SkCanvas* nativeCanvas = SkNEW_ARGS(SkCanvas, (bitmap));
swapCanvasPtr(env, canvasObj, nativeCanvas);
SkRegion clipReg;
if (dirtyRegion.isRect()) { // very common case
const Rect b(dirtyRegion.getBounds());
clipReg.setRect(b.left, b.top, b.right, b.bottom);
} else {
size_t count;
Rect const* r = dirtyRegion.getArray(&count);
while (count) {
clipReg.op(r->left, r->top, r->right, r->bottom, SkRegion::kUnion_Op);
r++, count--;
}
}
nativeCanvas->clipRegion(clipReg);
if (dirtyRectObj) {
const Rect& bounds(dirtyRegion.getBounds());
env->SetIntField(dirtyRectObj, gRectClassInfo.left, bounds.left);
env->SetIntField(dirtyRectObj, gRectClassInfo.top, bounds.top);
env->SetIntField(dirtyRectObj, gRectClassInfo.right, bounds.right);
env->SetIntField(dirtyRectObj, gRectClassInfo.bottom, bounds.bottom);
}
}
在JNI層實現的就是通過surface獲取到Layer中的buffer,并生成一個skiabitmap, Android 2D軟件繪圖使用skia作為核心引擎,這個bitmap的存儲空間為Layer buffer。繪制的UI就是寫入到這個buffer中,繪制好后通過 unlockCanvasAndPost通知surfaceflinger輸出顯示。如果是獨立線程繪制UI,那么流程與上描述基本一致。但需要注意的是如果獨立線程繪制的話,surface可通過surfaceView來獲取
private void draw() {
try {
// 步驟1
canvas = sfh.lockCanvas(); // 得到一個canvas實例
// 步驟2
canvas.drawColor(Color.WHITE);// 刷屏
canvas.drawText("test", 100, 100, paint);// 畫文字文本
} catch (Exception ex) {
} finally {
// 步驟3
if (canvas != null)
sfh.unlockCanvasAndPost(canvas); // 將畫好的畫布提交
}
}
總結
Android 原生系統是一個不斷進化的過程 , 每個版本都會解決非常多的性能問題 , 同時也會引進一些問題 ; 到了手機廠商這里 , 由于硬件差異和軟件定制 , 會在系統中加入大量的自己的代碼 , 這無疑也會影響系統的性能 . 同樣由于 Android 的開放 , App 的質量和行為也影響著整機的用戶體驗.
本篇主要列出了自身的實現問題導致的流暢性問題 , Android 最大的問題就是質量良莠不齊 , 不同于 App Store 這樣的強力管理市場 , Android App 不僅可以在 google Play 上面進行安裝 , 也可以在其他的軟件市場上面安裝 , 甚至可以下載安裝包自行安裝 , 可以說上架的門檻非常低 , 那么質量就只能由 開發者自己來把握了
有需要文中代碼的同學,可以順手給我點贊評論支持一下
獲取方式:私信我發送“進階”
技術是無止境的,你需要對自己提交的每一行代碼、使用的每一個工具負責,不斷挖掘其底層原理,才能使自己的技術升華到更高的層面
Android 架構師之路還很漫長,與君共勉
PS:有問題歡迎指正,可以在評論區留下你的建議和感受;
歡迎大家點贊評論,覺得內容可以的話,可以轉發分享一下