Android繪製流程
一、前言
1.1、C++介面庫
MFC、WTL、DuiLib、QT、Skia、OpenGL。
Android裡面的畫圖分為2D和3D兩種: 2D是由Skia 來實現的,3D部分是由OpenGL實現的。
1.2、Android基本概念
-
視窗
對使用者來說, 視窗就是手機螢幕, 包括下面的那些home、back按鍵、狀態列等。對於Activity來說, 視窗就是除系統狀態列和系統按鍵的螢幕區域, 有window之類的概念。對於wms來說, 它沒有什麼視窗的概念, 它能接受的只是一個個view而已。也就是Activity這裡還有Window這個概念, 但在wms那裡, 已經沒有window的概念了。
視窗型別分為應用程式視窗: 就是一般應用程式的視窗, 比如我們應用程式的Activity的視窗。子視窗: 一般在Activity裡面的視窗, 比如TabActivity。系統視窗: 系統的視窗, 比如輸入法、Toast、牆紙等等…系統視窗不需要對應任何Activity, 也不需要有父視窗, 對於應用程式而言, 理論上是無法建立系統視窗的, 因為所有的應用程式都沒有這個許可權, 然而系統程式卻可以建立系統視窗。WindowManager.LayoutParams裡面有關於各種視窗的type型別定義, type還有個含義就是視窗的z-order, 值越大, 顯示的位置越在上面。 -
Window、PhoneWindow
頂層視窗樣式和行為的抽象類, 概括了Android視窗的基本屬性和基本功能。該類例項的getDecorView()方法返回的DecorView被用來作為頂層檢視新增到WM中。
建立時機: ActivityThread.handleLaunchActivity —> ActivityThread.performLaunchActivity —>Activity.attach -
WindowManager、WindowManagerImpl、WindowManagerGlobal
與一個特定的Display相關聯, WindowManager主要用來管理視窗的一些狀態、屬性、view增加、刪除、更新、視窗順序、訊息收集和處理等。它面向的物件一端是螢幕, 另一端就是 view , 直接忽略我們以前的 Activity 或者 Dialog 之類的東東。WindowManager是一個介面類, 其真正的實現是WindowManagerImpl, 後者同時也是整個應用程式中所有Window的管理者。
-
Activity
Activity是支援顯示UI的, 但不直接管理view樹或者ViewRoot, Activity並沒有與這兩者產生直接的聯絡, 是通過中間 “Window”的物件。
建立過程: 1>、 使用代理模式啟動到ActivityManagerService中執行;
2>、 建立ActivityRecord到mHistory記錄中;
3>、 通過socket通訊到Zgote相關類建立process;
4>、通過ApplicatonThread與ActivityManagerService建立通訊;
5>、ActivityManagerService通知ActiveThread啟動Activity的建立;
6>、ActivityThread建立Activity加入到mActivities中並開始排程Activity執行;
7>、ActivityThread.handleLaunchActivity —> ActivityThread.performLaunchActivity -
ViewRoot、ViewRootImpl
任何顯示在裝置中的視窗如: Activity、Dialog等, 都包含一個ViewRoot例項。ViewRoot可以被理解為“View樹的管理者”, ViewRoot中的mView成員變數指向的就是它所管理的View樹的根。ViewRoot的核心任務就是與WindowManagerService進行通訊, 從ViewRootImpl到WMS間的通訊利用的是IWindowSession, 而反方向則是由IWindow來完成的。ViewRoot與ViewRootImpl的功能是一樣的, 只不過是Android不同版本的不同稱呼。
建立時機: ActivityThread.handleResumeActivity —> WindowManager.addView—> WindowManagerGlobal.addView新增一個view到VM中時, 與新增的view例項一一對應。 -
AcitivityManagerService
AMS提供了一個ArrayList mHistory來管理所有的activity, activity在AMS中的形式是ActivityRecord, task在AMS中的形式為TaskRecord, 程式在AMS中的管理形式為ProcessRecord。是個獨立的系統服務程式。
-
ActivityThread
管理應用程式的主執行緒的執行(相當於普通Java程式的main入口函式), 並根據AMS的要求(通過IApplicationThread介面, AMS為Client、ActivityThread.ApplicationThread為Server)負責排程和執行activities、broadcasts和其它操作。ActivityThread是每一個應用程式所在程式的主執行緒, 迴圈訊息處理。ActivityThread與AcitivityManagerService的通訊是屬於程式間通訊, 使用binder機制;
-
WindowManagerService
對系統中的所有視窗進行管理。WindowManager是執行在Application process中的, WindowManagerService是在system_server程式中執行, 兩者的通訊是通過中間的會話層IWindowSession來進行的。
附:相關簡化類結構
public class Activity {
private Window mWindow;
final void attach(...) {
mWindow = PolicyManager.makeNewWindow(this);
}
}
public class PhoneWindow extends Window{
private DecorView mDecor;
}
//它們分別用於表示View樹的根節點、ViewRoot以及Window的屬性
public final class WindowManagerGlobal {
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
}
public final class ActivityThread {
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
}
final void handleResumeActivity(...) {
}
}
二、Android訊息流與Activity中xml佈局檔案的解析
2.1 訊息流
WmS收到使用者訊息後需要把訊息派發到視窗, View類本身並不能直接接收WmS傳遞過來的訊息, 真正接收使用者訊息的必須是IWindow類, 而實現IWindow類的是ViewRoot.W類。
觸屏訊息 —-> WindowManagerService —-> ViewRoot —-> decor view —-> Activity —-> 傳遞給指定的View。
2.2 xml佈局檔案的解析相關類及流程
2.2.1 ViewGroup.LayoutParams類及其派生類
用來儲存xml中每個控制元件的屬性值。View通過LayoutParams類告訴其父檢視它想要的大小(即, 長度和寬度), 因此, 每個View都包含一個ViewGroup.LayoutParams類或者其派生類。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ... protected ViewGroup.LayoutParams mLayoutParams; ... }
ViewGroup子類可以實現自定義LayoutParams, 自定義LayoutParams提供了更好地擴充套件性, ViewGroup.LayoutParams及其常用派生類的類圖(部分類圖)如下:
2.2.3 xml佈局檔案的解析流程
LayoutInflater利用XML解析器將佈局檔案解析成一個完整的View樹, 所有Xxx.xml的佈局檔案都需要解析成一個完整的View樹。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
View result = root;
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, attrs, false);
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp
rInflate(parser, temp, attrs, true, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
return result;
}
從上面得知, 我們將View的AttributeSet屬性傳遞給generateLayoutParams()方法, 讓其構建合適地LayoutParams物件,並且初始化屬性值weight和height。但更重要的是, ViewGroup的子類可以過載generateLayoutParams方法, 返回特定的LayoutParams物件, 例如: 對於LinearLayout而言, 則是LinearLayout.LayoutParams物件。
三、Android繪製流程
3.1 繪製起點: DecorView新增到視窗上
我們知道Activity中的PhoneWindow物件會建立了一個DecorView(父類為FrameLayout)視窗頂層檢視, 然後通過LayoutInflater將xml內容佈局解析成View樹形結構新增到DecorView頂層檢視中id為content的FrameLayout父容器上面。到此, 我們已經知道Activity的content內容佈局最終會新增到DecorView視窗頂層檢視上面。那麼, DecorView是怎麼新增到視窗的呢?這時候我們不得不從Activity是怎麼啟動的說起, 當Activity初始化 Window和將佈局新增到PhoneWindow的內部類DecorView類之後, ActivityThread類會呼叫handleResumeActivity方法將頂層檢視DecorView新增到視窗上。
handlerResumeActivity方法的實現:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
...
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;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
}
...
}
WindowManagerImpl 中addView方法:
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
WindowManagerGlobal 中addView方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
///對每個視窗皆構建一個ViewRoot物件
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
////將視窗對應地view、root、wparams儲存在屬性集合中
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
// 呼叫ViewRoot物件去通知系統新增一個視窗
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
ViewRootImpl中setView的方法:
View mView; //所有視窗地root View
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mWindowAttributes.copyFrom(attrs); //儲存WindowManager.LayoutParams屬性值
attrs = mWindowAttributes;
...
int res; /* = WindowManagerImpl.ADD_OKAY; */
requestLayout(); //請求UI開始繪製。
mInputChannel = new InputChannel(); //建立一個InputChannel物件,接受訊息
try {
//通知WindowManagerService新增一個視窗,註冊一個事件監聽管道, 用來監聽: 按鍵(KeyEvent)和觸控(MotionEvent)事件。
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel);
}
...
}
}
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
...
try {
performTraversals();
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...
}
}
ViewRootImpl中的方法
private void performTraversals() {
...
windowSizeMayChange |= measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
...
}
3.2 開始繪製performTraversals
View系統的繪製流程會從ViewRootImpl的performTraversals()方法中開始, 每一個檢視的繪製過程都必須經歷三個最主要的階段onMeasure()、onLayout()和onDraw()。
3.2.1 第一步 onMeasure 決定View的大小
measure函式的作用是為整個View樹計算實際的大小, 設定每個View物件的佈局大小(“視窗”大小)。實際對應屬性就是View中的mMeasuredHeight(高)和mMeasureWidth(寬)。方法中引數widthMeasureSpec和heightMeasureSpec, 這兩個值分別用於確定檢視的寬度和高度的規格和大小。
MeasureSpec的值由specSize和specMode共同組成的, 其中specSize記錄的是大小, specMode記錄的是規格。
- EXACTLY 表示父檢視希望子檢視的大小應該是由specSize的值來決定的。子元素將被限定在給定的邊界裡而忽略它本身大小;
- AT_MOST 表示子檢視最多隻能是specSize中指定的大小, 開發人員應該儘可能小得去設定這個檢視, 並且保證不會超過specSize。
- UNSPECIFIED 表示開發人員可以將檢視按照自己的意願設定成任意的大小, 沒有任何限制。這種情況比較少見, 不太會用到。
View中的方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
final boolean isExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY &&
MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchingSize = isExactly &&
getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) &&
getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
if (forceLayout || !matchingSize &&
(widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec)) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
...
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
...
}
//View中onMeasure預設實現
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
...
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
...
}
measure()這個方法是final的, 因此我們無法在子類中去重寫這個方法, 說明Android是不允許我們改變View的measure框架的。然後在第9行呼叫了onMeasure()方法, 這裡才是真正去測量並設定View大小的地方。之後會在onMeasure()方法中呼叫setMeasuredDimension()方法來設定測量出的大小, 這樣一次measure過程就結束了。
當然, 一個介面的展示可能會涉及到很多次的measure, 因為一個佈局中一般都會包含多個子檢視,每個檢視都需要經歷一次measure過程。由父檢視在onMeasure中迴圈呼叫ViewGroup中的measureChildWithMargins實現子檢視的measure過程。
FrameLayout中方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
}
...
}
ViewGroup中的方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
3.2.2 第二步 onLayout 決定View在ViewGroup中的位置
ViewRootImpl的performTraversals()方法會在measure結束後繼續執行, 為檢視進行佈局的, 也就是確定檢視的位置。並呼叫View的layout()方法來執行此過程。
ViewRootImpl中的方法
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { ... final View host = mView; host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ... }
可以看到, 這裡還把剛才測量出的寬度和高度傳到了layout()方法中.
View中的方法:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
layout()方法接收四個引數, 分別代表著左、上、右、下的座標, 當然這些座標是相對於當前檢視的父檢視而言的。在layout()方法中, 首先會呼叫setFrame()方法來判斷檢視的大小是否發生過變化, 以確定有沒有必要對當前的檢視進行重繪, 同時還會在這裡把傳遞過來的四個引數分別賦值給mLeft、mTop、mRight和mBottom這幾個變數。
View中的onLayout()方法就是一個空方法, 因為onLayout()過程是為了確定檢視在佈局中所在的位置, 而這個操作應該是由佈局來完成的, 即父檢視決定子檢視的顯示位置。
ViewGroup中的方法
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
ViewGroup中的onLayout()方法是一個抽象方法, 這就意味著所有ViewGroup的子類都必須重寫這個方法。在onLayout()過程結束後, 我們就可以呼叫getWidth()方法和getHeight()方法來獲取檢視的寬高了。
getWidth()方法和getMeasureWidth()方法到底有什麼區別?
getMeasureWidth()方法在measure()過程結束後就可以獲取到了, 而getWidth()方法要在layout()過程結束後才能獲取到。另外, getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設定的, 而getWidth()方法中的值則是通過檢視右邊的座標減去左邊的座標計算出來的。
3.2.3 第三步 onDraw
measure和layout的過程都結束後, 接下來就進入到draw的過程了。
ViewRootImpl裡的方法
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
// Draw with software renderer.
final Canvas canvas;
...//設定canvas
mView.draw(canvas);
...
}
View中的方法:
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas` layers to prepare for fading
* 3. Draw view`s content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we`re done...
return;
}
...
}
protected void onDraw(Canvas canvas) {
}
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
*/
protected void dispatchDraw(Canvas canvas) {
}
onDraw為空方法, 因為每個檢視的內容部分肯定都是各不相同的, 這部分的功能需交給子類去實現。dispatchDraw這一步的作用是對當前檢視的所有子檢視進行繪製。但如果當前的檢視沒有子檢視, 那麼也就不需要進行繪製了。因此你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroup的dispatchDraw()方法中就會有具體的繪製程式碼。onDrawScrollBars 是對檢視的滾動條進行繪製。
四、其他
視窗的UI最終是需要通過SurfaceFlinger服務來統一渲染的, 而SurfaceFlinger服務在渲染視窗的UI之前, 需要計算基於各個視窗的Z軸位置來計算它們的可見區域。而WindowManagerService服務就是負責計算好每一個視窗的Z軸位置之後, 還需要將它們設定到SurfaceFlinger服務中去, 以便SurfaceFlinger服務可以正確地渲染每一個視窗的UI。
遺留問題:
- statusBar
參考文獻:
http://blog.csdn.net/guolin_blog/article/details/16330267
http://www.cnblogs.com/franksunny/archive/2012/04/20/2459738.html
http://blog.csdn.net/qinjuning/article/details/8051811
http://blog.csdn.net/qinjuning/article/details/8074262
http://bbs.51cto.com/thread-1072344-1.html
該文章來自阿里巴巴技術協會(ATA)
作者:耕耘
相關文章
- Android UI繪製流程及原理AndroidUI
- Android View繪製原理:繪製流程排程、測算等AndroidView
- Android進階(五)View繪製流程AndroidView
- 【Android進階】RecyclerView之繪製流程(三)AndroidView
- Android原始碼分析之View繪製流程Android原始碼View
- 繪製流程
- 靈魂畫師,Android繪製流程——Android高階UIAndroidUI
- 使用joinjs繪製流程圖(五)-流程圖繪製JS流程圖
- 探究Android View 繪製流程,Canvas 的由來AndroidViewCanvas
- Android View繪製流程看這篇就夠了AndroidView
- View的繪製二:View的繪製流程View
- Android系統原始碼分析--View繪製流程之-inflateAndroid原始碼View
- Android系統原始碼分析–View繪製流程之-setContentViewAndroid原始碼View
- Android系統原始碼分析--View繪製流程之-setContentViewAndroid原始碼View
- View 繪製流程分析View
- Android高階進階之路【一】Android中View繪製流程淺析AndroidView
- 審批流程圖怎樣繪製?繪製流程圖方法有哪些流程圖
- 流程圖製作: BPMN流程圖線上繪製流程圖
- 探究 Android View 繪製流程,Activity 的 View 如何展示到螢幕AndroidView
- 使用joinjs繪製流程圖(一)JS流程圖
- Flutter的繪製流程簡述Flutter
- Flutter之UI繪製流程二FlutterUI
- View繪製流程原始碼分析View原始碼
- Android OpenGLES繪製天空盒Android
- 使用靜態html繪製流程圖HTML流程圖
- View的繪製-measure流程詳解View
- flutter: 視窗初始與繪製流程Flutter
- 如何使用軟體繪製流程圖!流程圖
- 流程圖繪製軟體,流程圖製作軟體哪個好流程圖
- Android View的繪製過程AndroidView
- Android自定義View之(一)View繪製流程詳解——向原始碼要答案AndroidView原始碼
- 繪製流程圖的軟體下載流程圖
- 繪製流程圖的簡單軟體流程圖
- Flutter佈局和繪製流程淺析Flutter
- RecyclerView 原始碼分析(一) —— 繪製流程解析View原始碼
- 2018.03.15、View 繪製流程學習 筆記View筆記
- 使用joinjs繪製流程圖(七)-實戰-繪製流程圖+節點設定樣式+節點新增事件JS流程圖事件
- Android動畫實現繪製原理Android動畫
- Android View繪製原始碼分析 MeasureAndroidView原始碼