[TOC]
View 是 Android 应用开发中相当重要的角色,重要程度丝毫不亚于四大组件,甚至远高于 Broadcast 和 ContentProvider。本文为学习《Android 开发艺术探索》中 View 的工作原理后,自己对于 View 的三大流程的源码分析。
# ViewRoot
Activity在添加Window时,会先创建一个ViewrRootImpl,并通过ViewRootImpl来完成界面更新和Window的添加操作。View 的绘制流程从 ViewRootImpl 的 performTraversals 方法开始,在其内部调用View的measure、layout 和 draw 方法将一个 View 绘制出来。
这个过程如下图所示:
![](https://img.kancloud.cn/15/3d/153d56e6b3f961b1f4c22bf36c230dc8_773x573.jpg)
每一个视图的绘制过程都必须经历三个最主要的阶段,即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零碎问题
- 其他零碎问题
- 开发思路