1. 程式人生 > >淺談Android之Activity 視窗顯示流程介紹(二)

淺談Android之Activity 視窗顯示流程介紹(二)

7.3 Activity Décorview佈局(layout)

Measure確定Décor View以及child views的大小,layout則是確定child view在其parent view中的顯示區域,只有layout結束,view的left,right,top,bottom值才會被設定,getWidth和getHeight兩個函式才會返回view最終的寬高值

對FrameLayout來說,由於child view的大小確定了,那麼再確定它們在parentview中的顯示區域其實通過parent view的padding值,child view的margin值以及寬高就可以計算出

當然RelativeLayout和LinearLayout會相對複雜點,因為childviews之間會存在佈局關聯,本文只對FrameLayout的實現做簡單介紹,至於RelativeLayout和LinearLayout大家可自行看程式碼分析

在ViewRootImpl中呼叫performMeasure完成對Décor View的measure後,接著呼叫

PerformLayout觸發佈局操作,其內部主要呼叫:

host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

Host變數儲存的就是Décor View,layout的四個變數依次對應left,top,right,bottom

FrameLayout和ViewGroup都沒有對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;

}

這個函式先呼叫setFrame儲存位置資料,然後呼叫onLayout進行佈局操作,最後看這個view是否有設定layoutchangelistener,如果有,呼叫回撥通知佈局已經發生改變

先看setFrame:

//View.java

protected boolean setFrame(int left, int top, int right, int bottom) {

        boolean changed = false;

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {

            changed = true;

            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;

            int oldHeight = mBottom - mTop;

            int newWidth = right - left;

            int newHeight = bottom - top;

            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position

            invalidate(sizeChanged);

            mLeft = left;

            mTop = top;

            mRight = right;

            mBottom = bottom;

            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;

            if (sizeChanged) {

                sizeChange(newWidth, newHeight, oldWidth, oldHeight);

            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {

                mPrivateFlags |= PFLAG_DRAWN;

                invalidate(sizeChanged);

                invalidateParentCaches();

            }

            // Reset drawn bit to original value (invalidate turns it off)

            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;

            notifySubtreeAccessibilityStateChangedIfNeeded();

        }

        return changed;

    }

如果left,right,top,bottom值都沒變,那就說明佈局沒變,返回changed為false,如果變了,則將對應的值都儲存到mLeft,mRight,mTop, mBottom,然後呼叫invalidate通知view進行重繪

接著看onLayout的實現:

//FrameLayout.java

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

        layoutChildren(left, top, right, bottom, false /* no force left gravity */);

}

直接呼叫layoutChildren:

void layoutChildren(int left, int top, int right, int bottom,

                                  boolean forceLeftGravity) {

        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();

        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();

        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        mForegroundBoundsChanged = true;

        for (int i = 0; i < count; i++) {

            final View child = getChildAt(i);

            if (child.getVisibility() != GONE) {

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();

                final int height = child.getMeasuredHeight();

                int childLeft;

                int childTop;

                int gravity = lp.gravity;

                if (gravity == -1) {

                    gravity = DEFAULT_CHILD_GRAVITY;

                }

                final int layoutDirection = getLayoutDirection();

                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);

                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {

                    case Gravity.CENTER_HORIZONTAL:

                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +

                        lp.leftMargin - lp.rightMargin;

                        break;

                    case Gravity.RIGHT:

                        if (!forceLeftGravity) {

                            childLeft = parentRight - width - lp.rightMargin;

                            break;

                        }

                    case Gravity.LEFT:

                    default:

                        childLeft = parentLeft + lp.leftMargin;

                }

                switch (verticalGravity) {

                    case Gravity.TOP:

                        childTop = parentTop + lp.topMargin;

                        break;

                    case Gravity.CENTER_VERTICAL:

                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +

                        lp.topMargin - lp.bottomMargin;

                        break;

                    case Gravity.BOTTOM:

                        childTop = parentBottom - height - lp.bottomMargin;

                        break;

                    default:

                        childTop = parentTop + lp.topMargin;

                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);

            }

        }

}

邏輯很簡單,通過拿到child view的measure width和measure height,然後根據gravity和margin值計算出child view在parent view中的left,right,top,bottom值,最後呼叫

child.layout

如果child view是ViewGroup,則重走本節流程,依次反覆執行,直到全部child view都layout完成為止

7.4 Activity Décorview繪製(draw)

Layout結束後,每一個child view在其parent view的位置都已經固定,接下去就可以開始繪製child view的圖形資料了

在ViewRootImpl中呼叫performLayout完成佈局後,接著呼叫performDraw,其最終會呼叫drawSoftware,該函式會通過mSurface拿到Canvas,接著呼叫mView.draw()並傳入canvas開始Décor view的繪製

由於Decor view的所有child views是共用Decor view這一塊top canvas的,對每一個view來說,它們在繪製其child view或者content data時,最簡單的當然是使用相對座標,也就是將其左上角設定為座標原點,這樣就可以讓view只需專注於child view或者content data的繪製,而無需關心座標原點的調整以及恢復

如果把Décor view作為世界地圖,那top canvas肯定擁有初始的單位矩陣, childview或者content data就是世界中的顯示元素,元素顯示位置和方式的調整,都是通過矩陣(matrix)的配置來實現的

後續View繪製程式碼的分析,為了簡化邏輯以便於理解,我們假定系統不支援硬體加速,也就是mAttachInfo.mHardwareAccelerated為false

Draw(Canvas canvas)是View繪製的入口函式,它跟measure和layout一樣,基於view tree做完整的遞迴呼叫,它會按順序做如下事情

1)  繪製view的background

2)  呼叫onDraw繪製view的content

3)  呼叫dispatchDraw繪製view的children

4)  繪製scrollbar等

其中1, 2, 4都是基於傳入的canvas做介面繪製,那canvas的座標切換肯定是由第3步

dispatchDraw來完成了

dispatchDraw的預設實現是空的,也就是啥也沒做,只有ViewGroup重新實現了改方法,也就是說,這個方法,只會ViewGroup有效

接著我們看其在ViewGroup中的實現:

//ViewGroup.java

protected void dispatchDraw(Canvas canvas) {

        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);

        final int childrenCount = mChildrenCount;

        final View[] children = mChildren;

        int flags = mGroupFlags;

        ……

        int clipSaveCount = 0;

        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;

        if (clipToPadding) {

            clipSaveCount = canvas.save();

           canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,

mScrollX + mRight - mLeft - mPaddingRight,

mScrollY + mBottom - mTop - mPaddingBottom);

        }

        // We will draw our child's animation, let's reset the flag

        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;

        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

        ……

        for (int i = 0; i < childrenCount; i++) {

            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;

            final View child = (preorderedList == null)

                    ? children[childIndex] : preorderedList.get(childIndex);

            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {

                more |= drawChild(canvas, child, drawingTime);

            }

        }

       ……

}

由於mGroupFlags在initViewGroup時被設定了FLAG_CLIP_TO_PADDING,所以這裡

clipToPadding肯定為true,接著呼叫canvas .clipRect根據padding和scroll值來對canvas的操作區域進行裁剪,接著遍歷所有childview,依次呼叫drawChild

接著看drawChild的實現:

//ViewGroup.java

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

        return child.draw(canvas, this, drawingTime);

}

直接呼叫child.draw函式,這個函式也在View.java預設被實現,跟上頭提過的draw(canvas)函式不同的是,它有三個引數,除了canvas外,還傳入了parent viewgroup和drawing time:

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

        boolean usingRenderNodeProperties = mAttachInfo != null && mAttachInfo.mHardwareAccelerated;

        boolean more = false;

        final boolean childHasIdentityMatrix = hasIdentityMatrix();

        final int flags = parent.mGroupFlags;

        ……

        final boolean hasNoCache = cache == null || hasDisplayList;

        final boolean offsetForScroll = cache == null && !hasDisplayList &&

                layerType != LAYER_TYPE_HARDWARE;

        int restoreTo = -1;

        if (!usingRenderNodeProperties || transformToApply != null) {

            restoreTo = canvas.save();

        }

        if (offsetForScroll) {

            canvas.translate(mLeft - sx, mTop - sy);

        } else {

           if (!usingRenderNodeProperties) {

canvas.translate(mLeft, mTop);

}

            if (scalingRequired) {

                if (usingRenderNodeProperties) {

                    // TODO: Might not need this if we put everything inside the DL

                    restoreTo = canvas.save();

                }

                // mAttachInfo cannot be null, otherwise scalingRequired == false

                final float scale = 1.0f / mAttachInfo.mApplicationScale;

                canvas.scale(scale, scale);

            }

        }

        ……

        if (!usingRenderNodeProperties) {

            // apply clips directly, since RenderNode won't do it for this draw

            if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN

                    && cache == null) {

                if (offsetForScroll) {

                    canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop));

                } else {

                    if (!scalingRequired || cache == null) {

                        canvas.clipRect(0, 0, mRight - mLeft, mBottom - mTop);

                    } else {

                        canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());

                    }

                }

            }

            if (mClipBounds != null) {

                // clip bounds ignore scroll

                canvas.clipRect(mClipBounds);

            }

        }

        ……

            if (!layerRendered) {

                if (!hasDisplayList) {

                    // Fast path for layouts with no backgrounds

                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {

                        mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                        dispatchDraw(canvas);

                    } else {

                        draw(canvas);

                    }

                } else {

                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                    ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);

                }

            }

        } else if (cache != null) {

            ……

        }

        if (restoreTo >= 0) {

            canvas.restoreToCount(restoreTo);

        }

        ……

        mRecreateDisplayList = false;

        return more;

    }

drawingTime和parent viewgroup的作用在下一節再介紹

由於在layout的時候,view在其parent view中的top-left座標已經確定,所以這裡的座標切換相對來說就非常簡單,首先呼叫canvas.save()備份canvas當前的matrix/clip資料,接著呼叫canvas.translate(mLeft,mTop)進行原點切換,最後呼叫draw(canvas)讓view在當前canvas上進行內容繪製,繪製成功後,呼叫canvas.restoreToCount(restoreTo)恢復之前canvas備份的matrix/clip資料

7.5 ViewAnimation原理介紹

通過上一節我們知道,View在parent view中的位置或者展示方式是通過matrix來完成的,

那如果我在view每一次繪製的時候,按照一定的規律更改matrix的值,這樣就可以達到view在parent view的一個序列圖形顯示效果,也就是說一個針對view的動畫效果就出來了,這個好像是廢話,動態檢視不都是這麼來的麼^_^

Android提供了TranslateAnimation等類用來封裝對每一幀對應matrix的計算

接下去通過程式碼做下簡單介紹,為view設定並開始播放animation

TranslateAnimation tAnim = new TranslateAnimation(0, 400, 0, 0);

tAnim.setDuration(2000); 

view.startAnimation(tAnim);

建立TranslateAnimation,然後呼叫startAnimation開發播放

//View.java

public void startAnimation(Animation animation) {

animation.setStartTime(Animation.START_ON_FIRST_FRAME);

setAnimation(animation);

invalidateParentCaches();

invalidate(true);

}

先通過animation.setStartTime(Animation.START_ON_FIRST_FRAME)設定animation的啟動時間為Animation.START_ON_FIRST_FRAME,也就是說,animation的第一幀被繪製的時間,即為start time

然後呼叫setAnimation(animation)將該animation設定為viewmCurrentAnimation

最後呼叫invalidate(true)強制view重畫。

View重畫後,上一節說過,帶有parentdrawingTimedraw函式會被呼叫:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

……

Transformation transformToApply = null;

boolean concatMatrix = false;

……

final Animation a = getAnimation();

if (a != null) {

more = drawAnimation(parent, drawingTime, a, scalingRequired);

concatMatrix = a.willChangeTransformationMatrix();

if (concatMatrix) {

mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;

}

transformToApply = parent.getChildTransformation();

} else {

……

}

concatMatrix |= !childHasIdentityMatrix;

……

float alpha = usingRenderNodeProperties ? 1 : (getAlpha() * getTransitionAlpha());

if (transformToApply != null || alpha < 1 ||  !hasIdentityMatrix() ||

(mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) == PFLAG3_VIEW_IS_ANIMATING_ALPHA) {

if (transformToApply != null || !childHasIdentityMatrix) {

int transX = 0;

   int transY = 0;

if (offsetForScroll) {

transX = -sx;

transY = -sy;

}

if (transformToApply != null) {

if (concatMatrix) {

  if (usingRenderNodeProperties) {

renderNode.setAnimationMatrix(transformToApply.getMatrix());

} else {

// Undo the scroll translation, apply the transformation matrix,

                      // then redo the scroll translate to get the correct result.

canvas.translate(-transX, -transY);

canvas.concat(transformToApply.getMatrix());

canvas.translate(transX, transY);

}

parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;

}

……

}

   ……

if (a != null && !more) {

if (!hardwareAccelerated && !a.getFillAfter()) {

onSetAlpha(255);

}

parent.finishAnimatingView(this, a);

}

……

return more;

}

drawingTime,其實就是指這次draw操作的觸發時間,也就是ViewRootImpl呼叫

drawSoftware的時間:

//ViewRootImpl.java

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,

boolean scalingRequired, Rect dirty) {

……

attachInfo.mDrawingTime = SystemClock.uptimeMillis();

……

return true;

}

至於parent ViewGroup,這邊看到,其內部會有一個變數mChildTransformation儲存當前某一child viewanimation對應的transformation片段資料

draw函式中,先通過呼叫drawAnimation並傳入parentanimationdrawingTime來來得到當前的時間下對應的transformation片段資料,並儲存到parent ViewGroup

mChildTransformation中,然後drawAnimation內部還會判斷在當前transformation片段資料執行完後,是否還存在animation片段資料?如果有,則會呼叫invalidate觸發view的下一次繪製,這樣才能保持animation不會中斷

draw函式,接著呼叫parent.getChildTransformation()獲取當前的transformation資料,然後獲取其Matrix並設定到canvas

最後判斷more是否為false,如果是false,說明animation已經結束,呼叫

parent.finishAnimatingView(this,a)執行animation結束相關掃尾程式碼

由於android View都是在主執行緒的完成繪製的,而主執行緒的負載又不是均衡的,所以會導致View animation的幀率無法得到保障,當然,對於短時的動畫來說影響不大,但是對於長時間並且比較複雜的動畫,建議還是使用Surface來繪製,然後開一條執行緒來按照你想要的幀率來觸發重繪

相關推薦

AndroidActivity 視窗顯示流程介紹

7.3 Activity Décorview佈局(layout) Measure確定Décor View以及child views的大小,layout則是確定child view在其parent view中的顯示區域,只有layout結束,view的left,right,t

AndroidActivity 視窗顯示流程介紹

7 Activity 視窗顯示流程介紹 Activity 視窗顯示,其實就是Décor View繪製並顯示的過程,但是在繪製之前,Décor View需要先做下面兩件事情: 1)  確定Décor View的大小 2)  對Décor View進行佈局操作,也就是確定Déc

AndroidActivity Decor View建立流程介紹

6 Activity DecorView建立流程介紹 上頭已經完整的介紹了Activity的啟動流程,Activity是如何繫結Window,Window的décor view是如何通過ViewRootImpl與WMS建立關聯的,也就是說,整個框架已經有了,唯一缺的就是Ac

AndroidActivity觸控事件傳輸機制介紹

8 Activity觸控事件傳輸機制介紹 當我們觸控式螢幕幕的時候,程式會收到對應的觸控事件,這個事件是在app端去讀取的嗎?肯定不是,如果app能讀取,那會亂套的,所以app不會有這個許可權,系統按鍵的讀取以及分發都是通過WindowManagerService來完成

AndroidApp視窗檢視管理

5 App視窗檢視管理 WindowManagerGlobal負責管理App所有要新增到WMS的視窗,介面即為上頭的addView 首先,對於App本地視窗來說,其最核心的資料無非就兩個,一個是Window Parameters,另一個就是視窗的DécorView,一個負

HBase原始碼分析HRegion上compact流程分析

  2016年03月03日 21:38:04 辰辰爸的技術部落格 閱讀數:2767 版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/lipeng_bigdata/article/details/50791205

【CSS】css中格式化上下文BFC、IFC

Inline Formatting Context Inline Formatting Context的縮寫就是IFC。中文名叫,行內格式化上下文。行內框參與IFC。 什麼是行框? 在IFC中,每個框都是一個接一個地水平排列,起點是包含塊的頂部,水平方向

android解析包時出現錯誤

這次的原因不同,再記錄下public class DownloadTask { /** * @param path下載地址 * @param filePath儲存路徑 * @param progressDialog進度條 * @return * @t

Android OTA升級原理和流程分析---update.zip差分包問題的解決

Android OTA升級原理和流程分析(二)—update.zip差分包問題的解決 在上一篇末尾提到的生成差分包時出現的問題,現已解決,由於最近比較忙,相隔的時間也比較長,所以單列一個篇幅提示大家。這個問題居然是原始碼中的問題,可能你已經制作成功了,不過我的

Android用surface直接顯示yuv資料

上一篇文章主要是參照AwesomePlayer直接用SoftwareRenderer類來顯示yuv,為了能用到這個類,不惜依賴了libstagefright、libstagefright_color_conversion等動態靜態庫,從而造成程式具有很高的耦合度,也不便

上門洗車APP --- Androidclient開發 網絡框架封裝介紹

glob imp success rgb sed error margin p s 再次 上門洗車APP --- Androidclient開發 之 網絡框架封裝介紹(二)前幾篇博文中給大家介紹了一下APP中的基本業務及開發本項目使用的網絡架構:上門洗車APP ---

SpringMVC架構與工作流程

MVC模式是在Java的Web應用開發中非常常用的模式。MVC全名是Model View Controller,是模型(model)-檢視(view)-控制器(controller)的縮寫,一種軟體設計典範,用一種業務邏輯、資料、介面顯示分離的方法組織程式碼,將

AndroidSurfaceFlinger相關介紹

3.3 Surface Java層相關封裝 主要介紹三個類,對應如下: Java C++ SurfaceSession.java SurfaceComposeClient 對應JNI檔案為: android_view_surfacesession.cpp

Android apk動態載入機制的研究 資源載入和activity生命週期管理

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

HTTP中Get與Post的區別

     Http定義了與伺服器互動的不同方法,最基本的方法有4種,分別是GET,POST,PUT,DELETE。URL全稱是資源描述符,我們可以這樣認為:一個URL地址,它用於描述一個網路上的資源,而HTTP中的GET,POST,PUT,DELETE就對應著對這個

Android9.0 Activity啟動流程分析

文章目錄 1、ActivityThread的main函式 2. AMS的attachApplication函式 2.1 Part-I 2.2 Part-II 2.2.1 ApplicationThread的bindApp

快取一致性原則和Java記憶體模型JMM

Java記憶體模型(JMM)是一個概念模型,底層是計算機的暫存器、快取記憶體、主記憶體和CPU等。 多處理器環境下,共享資料的互動硬體裝置之間的關係: JMM: 從以上兩張圖中,談一談以下幾個概念: 1.快取一致性協議(MESI): 由於每個處

ArcGIS移動開發中的基本變數1:MapView、Map、Layers、GraphicsOverlay

一、MapView  二、Map        Map,程式設計中即ArcGISMap,可譯為地圖物件,主要用來承載地圖資料,ArcGISMap包含製圖資料圖層以及其它能夠定義地圖資訊的資料(例如basemaps底圖、popups彈出視窗、renderer渲染

Android使用Font Awesome顯示小圖示

Android中傳統的顯示圖示的方式 在平常的開發中,如果我們需要在介面上顯示某個小圖示,比如搜尋按鈕,返回按鈕,這時我們需要美工給我們切對應的png圖片,並放進對應的drawable資料夾中,這樣隨著圖示的越來越多,APK體積也會越來越大。 什麼是Fo

Android OTA升級原理和流程分析

這篇及以後的篇幅將通過分析update.zip包在具體Android系統升級的過程,來理解Android系統中Recovery模式服務的工作原理。我們先從update.zip包的製作開始,然後是Android系統的啟動模式分析,Recovery工作原理,如何從