[TOC]
View 是 Android 應用開發中相當重要的角色,重要程度絲毫不亞于四大組件,甚至遠高于 Broadcast 和 ContentProvider。本文為學習《Android 開發藝術探索》中 View 的工作原理后,自己對于 View 的三大流程的源碼分析。
# ViewRoot
Activity在添加Window時,會先創建一個ViewrRootImpl,并通過ViewRootImpl來完成界面更新和Window的添加操作。View 的繪制流程從 ViewRootImpl 的 performTraversals 方法開始,在其內部調用View的measure、layout 和 draw 方法將一個 View 繪制出來。
這個過程如下圖所示:

每一個視圖的繪制過程都必須經歷三個最主要的階段,即onMeasure()、onLayout()和onDraw()。從父布局開始往子布局依次繪制。
# 測量measure
View的measure方法在ViewGroup中被調用,ViewGroup會傳遞寬高兩個方向的測量說明書MeasureSpec過來,View根據MeasureSpec設置自身的測量寬高。
```java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {}
```
## LayoutParams的概念
```java
// View.java
protected ViewGroup.LayoutParams mLayoutParams;
```
每個View都有一個ViewGroup.LayoutParams屬性,該屬性是給父布局ViewGroup在測量、布局、繪制子View時使用的,告訴父容器自己想要被布局成什么樣。
ViewGroup.LayoutParams是ViewGroup的一個靜態內部類,描述了當前ViewGroup的子View可以配置哪些屬性。ViewGroup的LayoutParams僅僅描述了View的寬、高,ViewGroup的子類可以在LayoutParams中添加一些其他的屬性,比如LinearLayout的LayoutParams增加了weight屬性和gravity屬性。
在LinearLayout測量、布局以及繪制子View時,都會從子View的LayoutParams中取出相關屬性的值進行使用。
## MeasureSpec的概念
MeasureSpec 代表一個32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode 記錄規格,SpecSize 記錄大小。
SpecMode 有三種模式:
* EXACTLY:父視圖希望 View 的大小由 specSize 決定
* AT_MOST:View 最大只能是 specSize 中指定的大小
* UNSPECIFIED:View 的大小沒有限制
每一個 View 都會有自己的 MeasureSpec,可以理解為測量說明書。每個View 的 MeasureSpec 都由父視圖提供給它,所以 View 的測量大小會受父視圖的影響。
View在拿到父視圖傳遞過來的兩個MeasureSpec后,怎么設定自己的寬高呢,一起來看看:
## View 的 measure 過程
View的measure方法會調用自身的onMeasure方法,在onMeasure方法中設置自身的測量寬高。想要改變View的測量寬高時,可以重寫onMeasure方法,onMeasure方法源碼如下:
```java
/**
* 兩個參數為寬高兩個方向的 MeasureSpec(測量說明書)
* 由 measure 方法傳遞過來,View 的 measure 方法則是在 ViewGroup 的 measurechildren 方法中調用
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 設置測量出來的寬/高
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
```
先調用getDefaultSize方法取到自己的測量寬高,再調用setMeasuredDimension方法進行設置,依次來看:
1、getDefaultSize 方法是關鍵的方法,用來從父布局傳來的MeasureSpec中獲取自己的測量寬/高,源碼如下:
```java
/**
* @param size:View 的默認大小
* @param measureSpec:子 View 的 measureSpec(父視圖給過來的)
* @return 此 View 的測量寬/高
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
```
可以看到:
* 測量模式 為 UNSPECIFIED 模式時,此 View 的測量值為默認值,默認值通過getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()方法獲得
* 測量模式 為 AT_MOST 模式和 EXACTLY 模式時,此 View 的測量值為父視圖所給 measureSpec 中的 specSize
getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法類似,選擇一個看源碼:
```java
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
```
可以看出:
* View 未指定背景時,View 的寬度為 mMinWidth,即 android:minWidth 屬性所指定的值(默認為 0)
* View 指定背景時,View 的寬度為 mMinWidth 和背景最小寬度 這兩者中的較大值
2、開始設置測量寬高,setMeasuredDimension方法如下:
```java
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
// 設置測量寬、測量高的值
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
//測量出來的寬高設置給 View 的兩個成員變量:mMeasuredWidth 和 mMeasuredHeight
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
//...
}
```
通過上面代碼看到,View從父布局傳遞來的MeasureSpec中,獲取SpecMode和SpecSize來確定自身的大小。那么父布局又是如何給子View生成MeasureSpec的呢?來看看ViewGroup的測量過程。
## ViewGroup 的 measure 過程
ViewGroup 繼承自View類,為一個抽象類,因此沒有重寫 View 的 onMeasure 方法,都留給子類去實現。但是提供了測量子元素的方法 measureChildren。
ViewGroup 的子類可以重寫 onMeasure 方法,調用 measureChildren 方法,來測量所有的子View:
ViewGroup在onMeasure方法中設置自身的測量寬高,同時通過父布局給自己的MeasureSpec,來為自己的子View設置MeasureSpec。
```java
// 這兩個參數為ViewGroup從它的父布局那里獲取來的MeasureSpec,
// 本來是用來設置ViewGroup自己的測量寬、高的,現在需要用到這兩個參數來為子View生成MeasureSpec
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
```
其中measureChild方法如下:
```java
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
// 獲取子View的LayoutParams
final LayoutParams lp = child.getLayoutParams();
// 獲取子元素的 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 調用子元素View的 measure 方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
```
可以看到,先是得到子元素的 LayoutParams,然后根據 MeasureSpec、自身的padding以及子元素的 LayoutParams 來計算出子元素的 MeasureSpec。getChildMeasureSpec方法源碼如下:
```java
/**
* @param spec ViewGroup的 MeasureSpec
* @param padding ViewGroup的padding 值
* @param childDimension 子元素自身設定的寬、高值
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 得到ViewGroup的測量模式和測量值
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// ViewGroup的剩余空間
int size = Math.max(0, specSize - padding);
// 子元素的測量值和測量模式
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// ViewGroup為精準模式
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 子元素設置了固定的寬高值時
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子元素想和ViewGroup一樣大,就讓它一樣大
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子元素想自己確定它的大小,但是不能比ViewGroup更大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ViewGroup為最大值模式時
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 子元素想設置成一個具體的值,讓它設
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子元素想和ViewGroup一樣大,但是ViewGroup大小還不確定呢
// 要約束子元素不能比ViewGroup還大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子元素想決定它自己的大小,可以,但是不能比ViewGroup還大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ViewGroup問我們我們想多大(ViewGroup為未指定模式時)
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子元素想指定一個具體的值,讓它去吧
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子元素想和ViewGroup一樣大,來確定下
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子元素想自己確定它的大小,幫它確定下
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//返回子元素的 MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
```
可以看到,首先拿到ViewGroup的 SpecMode 和 SpecSize,再根據ViewGroup的 SpecMode 的不同,給子 View 設置不同的 MeasureSpec。
當子 View 的寬高設置不同的值時,它的 MeasureSpec 也不同:
* View 采用固定寬高時,不管ViewGroup的 MeasureSpec 是什么,View 的 MeasureSpec 都是 Exactly 模式并且大小等于其 LayoutParams 中的設置大小
* View 寬高是 match_parent 時:ViewGroup是精準模式,View 也是精準模式,大小是ViewGroup的剩余空間;ViewGroup是最大化模式時,View 也是最大化模式,且大小也是ViewGroup的剩余空間
* View 寬高是 wrap_content 時,不管ViewGroup是精準模式還是最大化模式,View 的模式總是最大化模式且大小均為ViewGroup的剩余空間
## 問題拓展
1、在上面的分析中可以看到,當 View 的 SpecMode 為 AT_MOST 和 EXACTLY 時,測量值均為 specSize。因此就有一個問題:
當View配置wrap\_content時,View的SpecMode總是為AT_MOST,SpecSize總是為ViewGroup的剩余空間。View在根據MeasureSpec設置測量寬高時,就會設置成ViewGroup的剩余空間,與期望的wrap\_content不符
解決方案為自定義View時重寫onMeasure方法,在View的SpecMode為AT_MOST時,為View指定一個寬高。ImageView、TextView等都是如此操作,可參考其源碼。
以 TextView 的源碼為例:
```java
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
```
2、另一個問題,根布局沒有父視圖,MeasureSpec由哪里來呢?一起來看看。
在 ViewRootImpl 的 performTraversals 方法中通過如下方式獲取MeasureSpec:
```java
// lp.width和lp.height在創建ViewGroup實例的時候已被賦值為MATCH_PARENT
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
```
getRootMeasureSpec方法源碼如下:
```java
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
```
可以看到根布局的 MeasureSpec 由 windowSize 和 ViewGroup.LayoutParams 決定。由于第二個參數為MATCH_PARENT,所以根布局的SpecMode為EXACTLY模式,SpecSize為windowsSize,也就是根布局會充滿全屏。
## 整體總結【重點關注】
1、不管是View還是ViewGroup,每個人都會有一個父布局給過來的MeasureSpec,用來設置自己的寬高
* 測量模式為AT_MOST或EXACTLY時,測量寬高值為MeasureSpec中的specSize
* 測量模式為UNSPECIFIED時,測量寬高值為默認值
2、View在onMeasure方法中,根據從父布局ViewGroup那里得到的MeasureSpec,來設置自身的寬高。
3、同樣,ViewGroup也是在onMeasure方法中設置自己的寬高。
4、ViewGroup在onMeasure方法中設置自己寬高之前會多做一步,循環為所有子View設置MeasureSpec,并調用每個子View的measure方法,讓子View測量自己的寬高。
5、ViewGroup根據自己的MeasureSpec、padding以及子View的LayoutParams,為子View設置MeasureSpec。
* 子View設置為固定寬高時,子View的specMode為:Exactly,specSize為:LayoutParams中設置的值,子View的測量寬高為LayoutParams中設置的值。
* 子View設置為match_parent時,子View的specMode為:等同ViewGroup的specMode,specSize為:ViewGroup的剩余空間,子View的測量寬高為ViewGroup的剩余空間。
* 子View設置為wrap_content時,子View的specMode為:AT_MOST,specSize為:ViewGroup的剩余空間,子View的測量寬高為ViewGroup的剩余空間。
6、自定義ViewGroup通常會重寫onMeasure方法,在onMeasure方法中為所有子View生成MeasureSpec并測量子View寬高。
# layout
## View 的 layout 方法
View的layout方法會為View自己及所有的childView指定位置,View 的 layout 方法源碼如下:
```java
/**
* l:View 左側坐標
* t:View 上方坐標
* r:View 右側坐標
* b:View 下方坐標
* 以上均指相對于父視圖
*/
public void layout(int l, int t, int r, int b) {
// 設置當前View的位置
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 設置每個childView的位置
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
//...
}
}
```
兩個關鍵方法,一個是setFrame方法設置當前View的位置,一個是onLayout方法設置childView的位置,分別來看看:
1、setFrame方法源碼如下:
```java
protected boolean setFrame(int left, int top, int right, int bottom) {
//...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
int newWidth = right - left;
int newHeight = bottom - top;
// 設置當前View的位置(相對于父布局的)
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
// 當前View大小改變,我們可以重寫onSizeChanged方法做相應處理
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
//請求重新繪制View
invalidate(sizeChanged);
invalidateParentCaches();
}
}
return changed;
}
```
2、在ViewGroup代碼中,onLayout方法是一個抽象方法,子類需要實現此方法,在實現中計算出每個childView的位置。
```java
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
```
以LinearLayout為例:
```java
// LinearLayout的onLayout方法
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
// 以縱向為例
void layoutVertical(int left, int top, int right, int bottom) {
int childTop;
int childLeft;
int width = right - left;
int childRight = width - mPaddingRight;
int childSpace = width - paddingLeft - mPaddingRight;
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() != GONE) {
// 這里使用了childView的測量寬、高的值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//...中間省略部分設置childLeft、childTop的代碼
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//...
}
}
}
// 調用childView的layout方法進行位置設定
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
```
## 整體總結【重點關注】
1、View的layout方法中,調用setFrame方法為View自己指定位置,調用onLayout方法為所有的childView指定位置
2、setFrame方法為mLeft、mTop、mRight、mBottom幾個屬性賦值,來確定View自己相對于父布局的位置
3、ViewGroup的onLayout方法是一個抽象方法,子類需要實現此方法,并在實現中根據ViewGroup的布局邏輯計算出每個childView的位置,然后調用childView的layout方法進行子View布局
# draw
1、View的draw方法
View的draw方法有兩個實現,一個參數的draw方法是被三個參數的方法調用的,三個參數的方法是被父布局ViewGroup的drawChild方法調用的。
```java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {}
public void draw(Canvas canvas) {}
```
```java
// 由ViewGroup的drawChild方法調用
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
//...
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
//...
} else {
//...
draw(canvas);
}
}
}
```
一個參數的draw方法源碼如下:
```java
public void draw(Canvas canvas) {
// 繪制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
if (!verticalEdges && !horizontalEdges) {
// 繪制View內容
if (!dirtyOpaque) onDraw(canvas);
// 繪制childView
dispatchDraw(canvas);
//...
// 繪制裝飾(前景內容)
onDrawForeground(canvas);
// 繪制默認焦點高亮顯示
drawDefaultFocusHighlight(canvas);
return;
}
// 我們通過重寫onDraw方法,拿到Canvas來繪制我們想要繪制的內容。
protected void onDraw(Canvas canvas) {}
```
View的繪制過程有如下幾步:
* 繪制背景
* 繪制View內容
* 繪制childView
* 繪制前景
* 繪制默認焦點高亮顯示
View中的dispatchDraw方法是一個空方法,ViewGroup中有相應的實現。
2、ViewGroup繪制childView
```java
// 這個方法用于繪制childView
protected void dispatchDraw(Canvas canvas) {
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
//...
more |= drawChild(canvas, transientChild, drawingTime);
}
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
```
在dispatchDraw方法中循環調用了drawChild方法,也就是調用了每個childView的draw方法,將childView繪制出來:
```java
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
```
## 整體總結【重點關注】
1、View的draw方法按流程分別繪制背景、content、childView、前景、默認焦點等。其中onDraw方法繪制View的內容。
2、如果是ViewGroup,會調用dispatchDraw方法繪制childView。
3、ViewGroup的dispatchDraw方法,會調用所有的childView的draw方法進行繪制子View。
4、可通過重寫View的onDraw方法,拿到Canvas來繪制想要繪制的內容。
# 參考文檔
[Android 視圖繪制流程完全解析,帶你一步步深入了解 View 系列文章](http://blog.csdn.net/guolin_blog/article/details/16330267)
[Android 開發藝術探索](https://book.douban.com/subject/26599538/)
- 導讀
- Java知識
- Java基本程序設計結構
- 【基礎知識】Java基礎
- 【源碼分析】Okio
- 【源碼分析】深入理解i++和++i
- 【專題分析】JVM與GC
- 【面試清單】Java基本程序設計結構
- 對象與類
- 【基礎知識】對象與類
- 【專題分析】Java類加載過程
- 【面試清單】對象與類
- 泛型
- 【基礎知識】泛型
- 【面試清單】泛型
- 集合
- 【基礎知識】集合
- 【源碼分析】SparseArray
- 【面試清單】集合
- 多線程
- 【基礎知識】多線程
- 【源碼分析】ThreadPoolExecutor源碼分析
- 【專題分析】volatile關鍵字
- 【面試清單】多線程
- Java新特性
- 【專題分析】Lambda表達式
- 【專題分析】注解
- 【面試清單】Java新特性
- Effective Java筆記
- Android知識
- Activity
- 【基礎知識】Activity
- 【專題分析】運行時權限
- 【專題分析】使用Intent打開三方應用
- 【源碼分析】Activity的工作過程
- 【面試清單】Activity
- 架構組件
- 【專題分析】MVC、MVP與MVVM
- 【專題分析】數據綁定
- 【面試清單】架構組件
- 界面
- 【專題分析】自定義View
- 【專題分析】ImageView的ScaleType屬性
- 【專題分析】ConstraintLayout 使用
- 【專題分析】搞懂點九圖
- 【專題分析】Adapter
- 【源碼分析】LayoutInflater
- 【源碼分析】ViewStub
- 【源碼分析】View三大流程
- 【源碼分析】觸摸事件分發機制
- 【源碼分析】按鍵事件分發機制
- 【源碼分析】Android窗口機制
- 【面試清單】界面
- 動畫和過渡
- 【基礎知識】動畫和過渡
- 【面試清單】動畫和過渡
- 圖片和圖形
- 【專題分析】圖片加載
- 【面試清單】圖片和圖形
- 后臺任務
- 應用數據和文件
- 基于網絡的內容
- 多線程與多進程
- 【基礎知識】多線程與多進程
- 【源碼分析】Handler
- 【源碼分析】AsyncTask
- 【專題分析】Service
- 【源碼分析】Parcelable
- 【專題分析】Binder
- 【源碼分析】Messenger
- 【面試清單】多線程與多進程
- 應用優化
- 【專題分析】布局優化
- 【專題分析】繪制優化
- 【專題分析】內存優化
- 【專題分析】啟動優化
- 【專題分析】電池優化
- 【專題分析】包大小優化
- 【面試清單】應用優化
- Android新特性
- 【專題分析】狀態欄、ActionBar和導航欄
- 【專題分析】應用圖標、通知欄適配
- 【專題分析】Android新版本重要變更
- 【專題分析】唯一標識符的最佳做法
- 開源庫源碼分析
- 【源碼分析】BaseRecyclerViewAdapterHelper
- 【源碼分析】ButterKnife
- 【源碼分析】Dagger2
- 【源碼分析】EventBus3(一)
- 【源碼分析】EventBus3(二)
- 【源碼分析】Glide
- 【源碼分析】OkHttp
- 【源碼分析】Retrofit
- 其他知識
- Flutter
- 原生開發與跨平臺開發
- 整體歸納
- 狀態及狀態管理
- 零碎知識點
- 添加Flutter到現有應用
- Git知識
- Git命令
- .gitignore文件
- 設計模式
- 創建型模式
- 結構型模式
- 行為型模式
- RxJava
- 基礎
- Linux知識
- 環境變量
- Linux命令
- ADB命令
- 算法
- 常見數據結構及實現
- 數組
- 排序算法
- 鏈表
- 二叉樹
- 棧和隊列
- 算法時間復雜度
- 常見算法思想
- 其他技術
- 正則表達式
- 編碼格式
- HTTP與HTTPS
- 【面試清單】其他知識
- 開發歸納
- Android零碎問題
- 其他零碎問題
- 開發思路