關於Android的渲染機制,大廠面試官最喜歡問的7個問題【建議收藏】
前言
渲染機制是Android作業系統很重要的一環,本系列通過介紹應用從啟動到渲染的流程,揭祕Android渲染原理。
問題
1.vsync如何協調應用和SurfaceFlinger配合來完成UI渲染、顯示,App接收vsync後要做哪些工作?
2.requestLayout和invalidate區別?
3.performTraversals到底是幹什麼了?
4.surfaceflinger怎麼分發vsync訊號的?
5.app需要主動請求vsync訊號,sw sync才會分發給app?
6.surfaceview顯示視訊的時候,視訊會一直頻繁重新整理介面,為什麼整個UI介面沒有卡頓?
7.app是如何構建起上面這套機制的?
如果對於上面的幾個問題沒有非常確認、清晰的答案可以繼續看下去,本文通過詳細介紹渲染機制解答上面的問題。
Vsync訊號
Android在“黃油計劃”中引入的一個重要機制就是:vsync,引入vsync本質上是要協調app生成UI資料和SurfaceFlinger合成影象,app是資料的生產者,surfaceflinger是資料的消費者,vsync引入避免Tearing現象。vsync訊號有兩個消費者,一個是app,一個是surfaceflinger,這兩個消費者並不是同時接收vsync,而是他們之間有個offset。
vsync-offset引入原因
上面提到hw vsync訊號在目前的Android系統中有兩個receiver,App + SurfaceFlinger,hw sync會轉化為sw sync分別分發給app和sf,分別稱為vsync-app和vsync-sf。app和sf接收vsync會有一個offset,引入這個機制的原因是提升“跟手性”,也就是降低輸入響應延。
如果app和sf同時接收hw sync,從上面可以看到需要經過vsync * 2的時間畫面才能顯示到螢幕,如果合理的規劃app和sf接收vsync的時機,想像一下,如果vsync-sf比vsync-app延遲一定時間,如果這個時間安排合理達到如下效果就能降低延遲:
SufaceFlinger工作機制
組成架構
-
EventControlThread: 控制硬體vsync的開關
-
DispSyncThread: 軟體產生vsync的執行緒
-
SF EventThread: 該執行緒用於SurfaceFlinger接收vsync訊號用於渲染
-
App EventThread: 該執行緒用於接收vsync訊號並且上報給App程序,App開始畫圖
- HW vsync, 真實由硬體產生的vsync訊號
- SW vsync, 由DispSync產生的vsync訊號
- vsync-sf, SF接收到的vsync訊號
- vsync-app, App接收到的vsync訊號
應用程式基本架構
Android應用程序核心組成
上圖列舉了Android應用程序側的幾個核心類,PhoneWindow的構建是一個非常重要的過程,應用啟動顯示的內容裝載到其內部的mDecor,Activity(PhoneWindow)要能接收控制也需要mWindowManager發揮作用。ViewRootImpl是應用程序運轉的發動機,可以看到ViewRootImpl內部包含mView、mSurface、Choregrapher,mView代表整個控制元件樹,mSurfacce代表畫布,應用的UI渲染會直接放到mSurface中,Choregorapher使得應用請求vsync訊號,接收訊號後開始渲染流程,下面介紹上圖構建的流程。
應用啟動流程圖(下文稱該圖為P0)
程序啟動
應用冷啟動第一步就是要先建立程序,這跟linux類似C/C++程式是一致的,Android亦是通過fork來孵化應用程序,我們知道Linux fork的子程序繼承父程序很多的資源,即所謂的COW。應用程序同樣會從其父程序zygote處繼承資源,比如art虛擬機器例項、預載入的class/drawable資源等,以付出一些開機時間為代價,一來能夠節省記憶體,二來能夠加速應用效能,下面結合systrace介紹Android如何啟動一個應用程序,應用啟動第一個介入的管理者是AMS,應用啟動過程中AMS發現沒有process建立,就會請求zygote fork程序,下圖就是AMS中建立程序的耗時:
AMS(ActivityManagerService)請求zygote建立程序的流程如下:
##ActvityManager:startProcessLocked
private final void startProcessLocked(ProcessRecord app, String hostingType,
String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs) {
boolean isActivityProcess = (entryPoint == null);
if (entryPoint == null) entryPoint = "android.app.ActivityThread";
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +app.processName);
checkTime(startTime, "startProcess: asking zygote to start proc");
ProcessStartResult startResult;
if (hostingType.equals("webview_service")) {
startResult = startWebView(entryPoint,
app.processName, uid, uid, gids, debugFlags, mountExternal,
app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
app.info.dataDir, null, entryPointArgs);
} else {
startResult = Process.start(entryPoint,
app.processName, uid, uid, gids, debugFlags, mountExternal,
app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
app.info.dataDir, invokeWith, entryPointArgs);
}
checkTime(startTime, "startProcess: returned from zygote!");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
前面systrace列印的proc建立時間就是來自與此,Process.start是請求zygote來建立建立程序,這其中有幾個很重要問題,比如新建程序入口函式在哪?這個新建程序如何做到建立以後能夠不退出,且能不斷響應外部輸入的等,接下來介紹下入口函式這個點,正如C/C++跑起來去找main函式一樣,可以看到startProcess函式有個entrypoint引數:
if (entryPoint == null) entryPoint = "android.app.ActivityThread";
原來程序啟動以後就會先去執行ActivityThread:main這個入口,應用自此開始了自己啟動流程,這點systrace展示的非常清晰:
看到上面PostFork色塊,很明顯是Process建立成功後的列印,然後程式碼繼續執行到ZygoteInit,ZygoteInit真正來查詢entrypoint,應用程式跳轉到ActivityThread.Main開始執行:
public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
if (RuntimeInit.DEBUG) {
Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
RuntimeInit.redirectLogStreams();
RuntimeInit.commonInit();
ZygoteInit.nativeZygoteInit();
return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
}
上面程式碼RuntimeInit.applicationInit內部執行findStaticMain查詢入口函式:
protected static Runnable applicationInit(int targetSdkVersion, String[] argv,
ClassLoader classLoader) {
// If the application calls System.exit(), terminate the process
// immediately without running any shutdown hooks. It is not possible to
// shutdown an Android application gracefully. Among other things, the
// Android runtime shutdown hooks close the Binder driver, which can cause
// leftover running threads to crash before the process actually exits.
nativeSetExitWithoutCleanup(true);
// We want to be fairly aggressive about heap utilization, to avoid
// holding on to a lot of memory that isn't needed.
VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);
final Arguments args = new Arguments(argv);
// The end of of the RuntimeInit event (see #zygoteInit).
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
// Remaining arguments are passed to the start class's static main
return findStaticMain(args.startClass, args.startArgs, classLoader);
}
OK,至此進入systrace顯示ActivityThread.main函式執行,也就是達到了P0的第3步驟。
ActivityThread物件
ActivityThread main執行的第一件事是呼叫AMS的attacApplicationLock(P0 :6)向大管家彙報:“程序已經啟動好了,繼續往下啟動吧”。AMS收到彙報就回調了(P0:7)ActvityThread的bindApplication,這裡“繫結”理解起來比較抽象,到底是要把哪些東西跟應用程式“繫結”起來呢?其實是把app本身的“上下文(context)”資訊跟剛剛建立的程序繫結起來,噢,又出來一個“上下文(context)”概念,用大白話講就是應用的apk包包含應用的所有身家資訊,這些個資訊就可以稱為是應用的“上下文(context)”,應用可以通過這個Context訪問自己的家當,此處會建立Application Context(具體關於應用程式幾種context區別自行google,此處不予展開)
private void handleBindApplication(AppBindData data) {
mBoundApplication = data;
mConfiguration = new Configuration(data.config);
mCompatConfiguration = new Configuration(data.config);
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
updateLocaleListFromAppContext(appContext,
mResourcesManager.getConfiguration().getLocales());
if (ii != null) {
final ApplicationInfo instrApp = new ApplicationInfo();
ii.copyTo(instrApp);
instrApp.initForUser(UserHandle.myUserId());
final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
appContext.getClassLoader(), false, true, false);
final ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
final ComponentName component = new ComponentName(ii.packageName, ii.name);
mInstrumentation.init(this, instrContext, appContext, component,
data.instrumentationWatcher, data.instrumentationUiAutomationConnection);
} else {
mInstrumentation = new Instrumentation();
}
Application app;
try {
app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
try {
mInstrumentation.callApplicationOnCreate(app);
}
}
}
上面回撥到應用程式Application.onCreate函式,很多應用會在此處做初始化動作,如果初始化模組過多可以考慮延遲載入,應用繼續啟動來到P0:12/P0:13
Activity物件
Activity的構建開始視窗顯示之旅,上面“Android應用程序核心組成”架構圖中可以看到Activity核心是PhoneWindow,P0圖中步驟13 performLauncherActivity中包含了14/15兩個重要的操作,attach函式建立了“PhoneWindow”,這個視窗具體承載了什麼資訊?用大白話來說點選啟動一個應用以後,可以說是顯示了一個”視窗”(Window),這個“視窗”至少要承載兩個功能:
-
顯示內容
-
可以操作
視窗顯示的內容就是android的佈局(layout),佈局資訊需要有個“房間”存放,PhoneWindow:mDecor就是這個“房間”,attach首先將佈局的“房間”建好,等到後續15 onCreate呼叫到就會呼叫setContentView使用應用程式開發者提供的佈局(layout)“裝飾、填充”這個“房間”。
“房間”填充、裝飾好後,還需要能夠接收使用者的操作,這就要看PhoneWindow中mWindowManager物件,這個物件最終包含一個ViewRootImpl物件,“視窗”正是因為構建了ViewRootImpl才安裝上了發動機。
attach函式
final void attach(...) {
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken,
mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
mWindowManager最後是一個WindowManagerImpl物件,WindowManagerImpl物件的mParentWindow對應了Activity中的PhoneWindow物件。
setWindowManager函式
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
//this物件對應Activity中的PhoneWindow物件
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
OK,上面的perfomrLaunchActivity一頓操作已經完成兩個“視窗(Activity)”中兩個重要變數的初始化,流程走到15 Activity:onCreate函式。
onCreate函式
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
空的HellWorld工程都預設包含上面兩行程式碼,setContentView就是作業系統給開發機會告訴系統“到底讓我顯示什麼?”就是這麼簡單的一行程式碼很可能就是導致應用效能卡頓,那麼setContentView幹啥了?
setContentView函式
該函式的作用就是使用佈局檔案填充“房間”mDecor,如果佈局檔案非常複雜會導致“房間”裝飾的費時費力(豪裝),裝修過程中從原理說就是講佈局檔案activity_main中的控制元件例項化,Android這個過程稱作inflate,systrace展示如下:
上面只是作業系統從讓開發給填充、裝飾了房間,但是這個房間還沒“開燈”,看不見,也沒開門(視窗無法操作),因為需要真正把這個視窗註冊到WindowManagerService後,WMS同SurfaceFlinger取得聯絡才能看到,後面我們來分析這個視窗是如何開燈顯示,並且能開門迎客接收按鍵訊息的。
隨後應用啟動流程來到handleResumeActivity:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
...;
//回撥應用程式的onResume
r = performResumeActivity(token, clearHide, reason);
...;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
}
}
上面performResumeActivity會回撥應用程式的onResume函式,從這裡可以看到onResume被回撥時使用者是看不到視窗的。wm.addView是重點,這一步就要把“房間”亮燈,也就是把視窗註冊到wms中著手顯示出來,並且開門接收使用者操作,這裡是呼叫的WindowManagerImpl.java:addView:
addView函式
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
...;
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
從這裡開始建立應用程序最核心的:ViewRootImpl類,它負責與WMS通訊,負責管理Surface,負責觸發控制元件的測量、佈局、繪製,同時也是輸入事件的中轉站,可以說ViewRootImpl是整個控制元件系統運轉的中樞,應用程序中最為重要的一個元件,有了ViewRootImpl這個窗口才能開始渲染被使用者看到,並且接受使用者操作(開燈、開門)。
ViewRootImpl剖析
上面的框架圖提到ViewRootImpl有個非常重要的物件Choreographer,整個應用佈局的渲染依賴這個物件發動,應用要求渲染動畫或者更新畫面佈局時都會用到Choreographer,接收vsync訊號也依賴於Choreographer,我們以一個View控制元件呼叫invalidate函式來分析應用如何接收vsync、以及如何更新UI的。
Activity中的某個控制元件呼叫invalidate以後,會逆流到根控制元件,最終到達呼叫到ViewRootImpl.java : Invalidate
invalidate函式
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
scheduleTraversals函式
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
從上面的程式碼看到Invalidate最終呼叫到mChoreographer.postCallback,這程式碼的含義:應用程式請求vsync訊號,收到vsync訊號以後會呼叫mTraversalRunnable,接下來看下應用程式如何通過Choreographer接收vsync訊號:
//Choreographer.java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//應用請求vsync訊號以後,vsync訊號分發就會回撥到這裡
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
Log.d(TAG, "Received vsync from secondary display, but we don't support "
+ "this case yet. Choreographer needs a way to explicitly request "
+ "vsync for a specific display to ensure it doesn't lose track "
+ "of its scheduled vsync.");
scheduleVsync();
return;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
上面onVsync會往訊息佇列放一個訊息,通過下面的FrameHandler進行處理:
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
從systrace中我們經常看到doFrame就是從上面的doFrame列印,這說明應用程式收到了vsync訊號要開始渲染布局了,圖示如下:
doFrame函式就開始一次處理input/animation/measure/layout/draw,doFrame程式碼如下:
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
比如上面呼叫Invalidate的時候已經post了一個CALLBACK_TRAVERSAL型別的Runnable,這裡就會執行到那個Runnable也就是mTraversalRunnable:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
performTraversal函式
doTraversal內部會呼叫大名鼎鼎的:performTraversal,這裡app就可以進行measure/layout/draw三大流程。需要注意的時候Android在5.1引入了renderthread執行緒,可以講draw操作從UIThread解放出來,這樣做的好處是,UIThread將繪製指令sync給renderthread以後可以繼續執行measure/layout操作,非常有利於提升裝置操作體驗,如下:
上面就是應用程序收到vsync訊號之後的渲染UI的大概流程,可以看到app程序收到vsync訊號以後就開始其measure/layout/draw三大流程,這裡面就會回撥應用的應用各個空間的onMeasure/onLayout/onDraw,這個部分是在UIThread完成的。UIThread在完成上述步驟以後會繪製指令(DisplayList)同步(sync)給RenderThread,RenderThread會真正的跟GPU通訊執行draw動作,systrace圖示如下:
上圖中看到doFrame下面會有input/anim(時間短色塊比較小)、measure、layout、draw,結合上面的程式碼分析就清楚了app收到vsync訊號的行為,measure/layout/draw的具體分析就涉及到控制元件系統相關的內容,這塊內容本文不作深入分析,提一下draw這個操作,使用硬體加速以後draw部分只是在UIThread中收集繪製命令而已,不做真正的繪製操作,該部分後續開一篇介紹硬體加速和hwui的文章做介紹。
APP為什麼滑動卡頓、不流暢
這裡我們指UI/Render執行緒裡面的卡頓,因為這裡才涉及Android的核心原理,非UIThread的執行邏輯導致的卡頓需要根據具體業務場景分析,比如影視播放卡頓可能是播放器原因,可能是網路原因等等。UIThread的卡頓有如下幾類的原因:
後臺程序CPU消耗高
如果CPU被後臺程序或者執行緒消耗,前臺的應用流暢性勢必會受影響,這點也是很容易被忽略的。
複雜的控制元件樹
複雜的佈局不僅會導致inflate時間變長,同時也會導致traversal時間變長,如果traversal + renderthread 的渲染部分不能在16ms內完成就出現掉幀現象,佈局優化可以參考前面啟動效能文章。
不合理requestLayout
requestLayout顧名思義就是應用佈局發生變化,需要重新進行measure/layout/draw的流程,比invalidate呼叫更重,invalidate只是標記一個“髒區域”,不需要執行meausre/layout呼叫,只需要重繪即可。requestLayout呼叫意味著頻繁的traversal動作,此時肯定會導致卡頓掉幀問題。
UIThread block
UIThread被block的因素多種多樣,有binder block、IO block等等,具體見應用啟動效能分析文章;前面問題小結中提到了一個問題:surfaceview重新整理為什麼使用者介面沒有卡頓?原因是surfaceview擁有獨立的surface畫布(從surfaceview這個名字就能知道),所以surfaceview可以在開發者自建的thread中重新整理,這樣視訊重新整理就不會影響到uithread。GLSurfaceView更高階一些,控制元件本身就會建立子執行緒。理解這個以後其實可以更多的擴充套件思路,比如GLSurfaceView本質上就是將UI資料當成紋理,放在子執行緒中傳入GPU,按照此思路我們是否有辦法將Bitmap等資料也放到子執行緒傳入GPU,其實也是可以的,也就是下文提到的“非同步紋理”,可以將圖片資料放在開發者自定義的執行緒中渲染,Android有很多好玩的控制元件,比如TextureView,SurfaceTexture,把所有這些控制元件原理都理解以後對擴充套件優化思路有很大幫助,本文不再纖細介紹了。
RenderThread block
這個原因很少有文章提到,流暢的應用渲染需要16ms,但是具體這個16ms要做哪些事情,如下圖:
可以看到一個vsync的16ms要UIThread + RenderThread配合完成才能保證流暢的體驗,UIThread是執行traversal呼叫,RenderThread其中很重要的一個操作是跟GPU通訊將圖片上傳GPU,上傳圖片期間UI Thread也是block狀態,所以魔盒、TV瀑布流的桌面、影視無法實現邊滑動邊上傳渲染圖片,實現過了非同步渲染的機制將圖片非UI/RT Thread,實現邊滑動邊出圖的效果。
總結和展望
本文從程式碼層面,把應用程序啟動和渲染的流程走讀了一遍,理解了Android的渲染原理對於理解其他UI框架或者引擎有比較好的借鑑意義,比如研究google的flutter框架時會更輕鬆:
上圖從網路上搜到的flutter 框架的流程圖,這個流程是不是有點像套娃戰術,同樣是vsync訊號、UI執行緒,GPU執行緒(也就是android的renderthread)兩執行緒加速效能。Android的UI 執行緒的draw最終只負責將繪製操作轉化為繪製指令(DisplayList),真正負責和GPU互動來繪製的是RenderThread,flutter其實看到也是同樣的思路,UI執行緒繪製構建LayerTree同步給GPU執行緒,GPU執行緒通過Skia庫跟GPU互動。
推薦閱讀:
位元組跳動8年老Android面試官談;Context都沒弄明白憑什麼拿高薪?
做了六年Android,終於熬出頭了,15K到31K全靠這份高階面試題+解析
位元組、騰訊,阿里Android高階面試真題彙總,會一半隨便進大廠