首页 > 其他分享 >从setContentView分析Android加载布局的流程

从setContentView分析Android加载布局的流程

时间:2022-11-22 11:36:19浏览次数:39  
标签:setContentView styleable Window internal Android null com android 加载


一.概述 在Activity中,我们基本都会用到setContentView方法,这个方法是干啥的想必大家都知道,把我们写好的布局文件显示到界面上。今天我们就去看看底层的源码,分析一下到底是如何实现的。 二.分析

1.Activity$getWindow()

public class MainActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

我们先点进去MainActivity 中的setContentView方法,可以进入到Activity中,看到如下的代码:

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

在Activity的setContentView中调用了两个方法,我们只分析第一个,我们看看getWindow()返回的是一个什么东西,

public Window getWindow() {
return mWindow;
}

是一个Window对象mWindow,大家注意,Window是一个抽象类,并且Window中的setContentView是一个抽象方法,那么Activity中肯定就有一个Window抽象类的实现,我们查找代码发现 在Activity的attach方法中为mWindow对象赋值:

mWindow = PolicyManager.makeNewWindow(this);

我们可以看到这里调用了PolicyManager中的静态方法makeNewWindow,我们继续点进去,

public final class PolicyManager {
private static final String POLICY_IMPL_CLASS_NAME =
"com.android.internal.policy.impl.Policy";

private static final IPolicy sPolicy;

static {
// Pull in the actual implementation of the policy at run-time
try {
Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
sPolicy = (IPolicy)policyClass.newInstance();
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
} catch (InstantiationException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
}
}

// Cannot instantiate this class
private PolicyManager() {}

// The static methods to spawn new policy-specific objects
public static Window makeNewWindow(Context context) {
return sPolicy.makeNewWindow(context);
}

我们看到其实是调用了sPolicy对象的makeNewWindow方法,那么这个sPolicy对象是什么呢?很明显的可以看到是在上面通过反射得到的Policy的一个对象,所以我们接下来进入到Policy这个类中,寻找makeNewWindow这个方法,我们看到如下的代码,

public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}

最终返回的是PhoneWindow这样一个对象,通过着一系列的查找,也就是说Activity中的getWindow()方法得到的其实是一个PhoneWindow对象,其实PhoneWindow是Window的唯一实现类,我们在进入到Window这个类的源码中的时候官方已经告诉我们了,

* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {

2.PhoneWindow$setContentView
由于我们平时写的setContentView最终是调用了PhoneWindow里面的
setContentView,所以我们看看这里面的代码,

@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
//将我们的布局文件添加到mContentParent中,这个mContentParent是根布局中id为content的一个FrameLayout
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}

首先判断mContentParent是否为null,mContentParent是什么呢?接下来会分析,一开始条件成立,进入installDecor()方法。

private void installDecor() {
if (mDecor == null) {
///创建DecorView
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
// 找到系统默认的布局文件,添加到DecorView中,返回该布局文件中id为content的一个容器,我们通过setContentView设置的布局就是添加到这个容器中
mContentParent = generateLayout(mDecor);

// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();

mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
if (mTitleView != null) {
mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(com.android.internal.R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
} else {
mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
if (mActionBar != null) {
mActionBar.setWindowCallback(getCallback());
if (mActionBar.getTitle() == null) {
mActionBar.setWindowTitle(mTitle);
}
final int localFeatures = getLocalFeatures();
if ((localFeatures & (1 << FEATURE_PROGRESS)) != 0) {
mActionBar.initProgress();
}
if ((localFeatures & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
mActionBar.initIndeterminateProgress();
}

boolean splitActionBar = false;
final boolean splitWhenNarrow =
(mUiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;
if (splitWhenNarrow) {
splitActionBar = getContext().getResources().getBoolean(
com.android.internal.R.bool.split_action_bar_is_narrow);
} else {
splitActionBar = getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowSplitActionBar, false);
}
final ActionBarContainer splitView = (ActionBarContainer) findViewById(
com.android.internal.R.id.split_action_bar);
if (splitView != null) {
mActionBar.setSplitView(splitView);
mActionBar.setSplitActionBar(splitActionBar);
mActionBar.setSplitWhenNarrow(splitWhenNarrow);

final ActionBarContextView cab = (ActionBarContextView) findViewById(
com.android.internal.R.id.action_context_bar);
cab.setSplitView(splitView);
cab.setSplitActionBar(splitActionBar);
cab.setSplitWhenNarrow(splitWhenNarrow);
} else if (splitActionBar) {
Log.e(TAG, "Requested split action bar with " +
"incompatible window decor! Ignoring request.");
}

// Post the panel invalidate for later; avoid application onCreateOptionsMenu
// being called in the middle of onCreate or similar.
mDecor.post(new Runnable() {
public void run() {
// Invalidate if the panel menu hasn't been created before this.
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!isDestroyed() && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_ACTION_BAR);
}
}
});
}
}
}

在代码的第三行,我们看到

mDecor = generateDecor();

我们进入generateDecor这个方法,

protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}

这里创建了一个DecorView对象,DecorView是PhoneWindow里面的一个内部类,继承自FrameLayout,到目前为止,setContentView
方法里生成一个FrameLayout类型的DecorView组件,我们继续向下看,有这样的代码

if (mContentParent == null) {
mContentParent = generateLayout(mDecor);

把 DecorView 对象 mDecor 作为参数传递给 generateLayout方法得到 mContentParent。generateLayout()方法中的代码实现如下:

protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.

TypedArray a = getWindowStyle();

//省去一些代码
/**以下这些是Activity 窗口属性特征的设置*/
//窗口是否浮动,一般用于Dialog窗口是否浮动:是否显示在布局的正中间。
mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
//设置窗口是否支持标题栏,隐藏显示标题栏操作在此处。
if (a.getBoolean(com.android.internal.R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(com.android.internal.R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
//ActionBar导航栏是否不占布局空间叠加显示在当前窗口之上。
if (a.getBoolean(com.android.internal.R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}

if (a.getBoolean(com.android.internal.R.styleable.Window_windowActionModeOverlay, false)) {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
//当前Activity是否支持全屏
if (a.getBoolean(com.android.internal.R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}

if (a.getBoolean(com.android.internal.R.styleable.Window_windowOverscan, false)) {
setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN&(~getForcedWindowFlags()));
}

if (a.getBoolean(com.android.internal.R.styleable.Window_windowShowWallpaper, false)) {
setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags()));
}

if (a.getBoolean(com.android.internal.R.styleable.Window_windowEnableSplitTouch,
getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB)) {
setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH&(~getForcedWindowFlags()));
}

a.getValue(com.android.internal.R.styleable.Window_windowMinWidthMajor, mMinWidthMajor);
a.getValue(com.android.internal.R.styleable.Window_windowMinWidthMinor, mMinWidthMinor);
if (a.hasValue(com.android.internal.R.styleable.Window_windowFixedWidthMajor)) {
if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue();
a.getValue(com.android.internal.R.styleable.Window_windowFixedWidthMajor,
mFixedWidthMajor);
}
if (a.hasValue(com.android.internal.R.styleable.Window_windowFixedWidthMinor)) {
if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();
a.getValue(com.android.internal.R.styleable.Window_windowFixedWidthMinor,
mFixedWidthMinor);
}
if (a.hasValue(com.android.internal.R.styleable.Window_windowFixedHeightMajor)) {
if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue();
a.getValue(com.android.internal.R.styleable.Window_windowFixedHeightMajor,
mFixedHeightMajor);
}
if (a.hasValue(com.android.internal.R.styleable.Window_windowFixedHeightMinor)) {
if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue();
a.getValue(com.android.internal.R.styleable.Window_windowFixedHeightMinor,
mFixedHeightMinor);
}

final Context context = getContext();
final int targetSdk = context.getApplicationInfo().targetSdkVersion;
final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
final boolean targetHcNeedsOptions = context.getResources().getBoolean(
com.android.internal.R.bool.target_honeycomb_needs_options_menu);
final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);

if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) {
addFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY);
} else {
clearFlags(WindowManager.LayoutParams.FLAG_NEEDS_MENU_KEY);
}

if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
if (a.getBoolean(
com.android.internal.R.styleable.Window_windowCloseOnTouchOutside,
false)) {
setCloseOnTouchOutsideIfNotSet(true);
}
}

WindowManager.LayoutParams params = getAttributes();

if (!hasSoftInputMode()) {
params.softInputMode = a.getInt(
com.android.internal.R.styleable.Window_windowSoftInputMode,
params.softInputMode);
}

if (a.getBoolean(com.android.internal.R.styleable.Window_backgroundDimEnabled,
mIsFloating)) {
/* All dialogs should have the window dimmed */
if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
}
if (!haveDimAmount()) {
params.dimAmount = a.getFloat(
android.R.styleable.Window_backgroundDimAmount, 0.5f);
}
}

if (params.windowAnimations == 0) {
params.windowAnimations = a.getResourceId(
com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
}

// The rest are only done if this window is not embedded; otherwise,
// the values are inherited from our container.
if (getContainer() == null) {
if (mBackgroundDrawable == null) {
if (mBackgroundResource == 0) {
mBackgroundResource = a.getResourceId(
com.android.internal.R.styleable.Window_windowBackground, 0);
}
if (mFrameResource == 0) {
mFrameResource = a.getResourceId(com.android.internal.R.styleable.Window_windowFrame, 0);
}
if (false) {
System.out.println("Background: "
+ Integer.toHexString(mBackgroundResource) + " Frame: "
+ Integer.toHexString(mFrameResource));
}
}
mTextColor = a.getColor(com.android.internal.R.styleable.Window_textColor, 0xFF000000);
}

// Inflate the window decor.

int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
com.android.internal.R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = com.android.internal.R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = com.android.internal.R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
com.android.internal.R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = com.android.internal.R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
com.android.internal.R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = com.android.internal.R.layout.screen_action_bar;
} else {
layoutResource = com.android.internal.R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = com.android.internal.R.layout.screen_simple_overlay_action_mode;
} else {
//默认布局文件.
layoutResource =
com.android.internal.R.layout.screen_simple;
// System.out.println("Simple!");
}

mDecor.startChanging();
//通过布局添加器LayoutInflater获取layoutResource布局,
View in = mLayoutInflater.inflate(layoutResource, null);
//将XML资源为layoutResource的布局添加到decor容器里面,至此PhoneWindow 内部类DecorView就添加了子布局
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
//此处很重要,通过findViewById找到 contentParent容器,也是该方法的返回值。
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}

if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
ProgressBar progress = getCircularProgressBar(false);
if (progress != null) {
progress.setIndeterminate(true);
}
}

// Remaining setup -- of background and title -- that only applies
// to top-level windows.
if (getContainer() == null) {
Drawable drawable = mBackgroundDrawable;
if (mBackgroundResource != 0) {
drawable = getContext().getResources().getDrawable(mBackgroundResource);
}
mDecor.setWindowBackground(drawable);
drawable = null;
if (mFrameResource != 0) {
drawable = getContext().getResources().getDrawable(mFrameResource);
}
mDecor.setWindowFrame(drawable);

// System.out.println("Text=" + Integer.toHexString(mTextColor) +
// " Sel=" + Integer.toHexString(mTextSelectedColor) +
// " Title=" + Integer.toHexString(mTitleColor));

if (mTitleColor == 0) {
mTitleColor = mTextColor;
}

if (mTitle != null) {
setTitle(mTitle);
}
setTitleColor(mTitleColor);
}

mDecor.finishChanging();

return contentParent;
}

以上代码代比较复杂,主要做了以下几件事情。
(1)初始化窗口的特征,是否显示标题栏,是否全屏,是否支持ActionBar浮动等等。
(2)通过LayoutInflater 将默认根xml布局转换成view
(3)将找到的View添加到DecorView根布局当中。
(4)从根布局中找到id为R.id.content的 contentParent 容器。也就是当前方法的返回值。
接下来我们看看这个com.android.internal.R.layout.screen_simple布局文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<FrameLayout android:id="@android:id/title_container"
android:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
</FrameLayout>
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

我们的DecorView就添加了上面的布局,线性布局LinearLaout里包含两个组件,ViewStub是懒加载,默认不显示,FrameLayout是什么呢?看看id=content,就是我们下面这行找到的contentParent

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

那么这个父容器 contentParent有什么作用呢?
我们回到下面的两行代码,都是之前出现过的

//获得父容器
mContentParent = generateLayout(mDecor);
mLayoutInflater.inflate(layoutResID, mContentParent);

这里通过LayoutInflater将 setContentView(layoutResID)传进来的布局id加载到 父容器mContentParent中,至此,setContentView就将布局添加到Activity里面了。

现在我们来梳理一下流程:

Activity setContentView—>Window setContentView—>PhoneWindow setContentView—->PhoneWindow installDecor—–>PhoneWindow generateLayout——>PhoneWindow mLayoutInflater.inflate(layoutResID, mContentParent);

Activity 类中有一个Window抽象类的实现PhoneWindow类,该类中有个内部类DecorView,继承自FrameLayout,在DecorView容器中添加了根布局,根布局中包含了一个id为 contnet的FrameLayout 内容布局,我们的Activity加载的布局xml最后添加到 id为content的FrameLayout布局当中了。用一个图来描述,如下:

从setContentView分析Android加载布局的流程_window对象


标签:setContentView,styleable,Window,internal,Android,null,com,android,加载
From: https://blog.51cto.com/u_10847930/5877248

相关文章

  • 从源码的角度理解Android消息处理机制
    总结与Handler共同作用的有Looper,MessageQueue,Message。我么接下来从源码的角度看看整个过程的大概实现。首先说一下每个对象的作用:Looper:消息轮询循器,不断的从消息队......
  • Android命令行查看内存情况
    在学习如何查看内存前先来介绍几个概念VSS-VirtualSetSize虚拟耗用内存(包含共享库占用的内存)RSS-ResidentSetSize实际使用物理内存(包含共享库占用的内存)PSS......
  • Android实现3D页面加载进度条动画
    一.概述最近在研究公司的代码,发现每次切换页面时做的进度条效果还挺不错的,所以想深入研究一下,今天就带大家来看看到底是如何实现的,首先上效果图二.实现上面的进度条最大的......
  • Android使用MAT分析内存泄露
    一.概述首先来普及一下什么是内存泄露:内存泄露是指对象的内存在分配之后无法通过程序的执行逻辑释放对该对象的引用,导致不能回收该对象所占内存。内存泄露会导致以下情......
  • Android网络类型判断
    在Android开发中,我们有时候需要根据当前网络的类型去做一些操作,下面看看如何判断当前网络的状况:publicclassNetWorkUtil{privatestaticConnectivityManagermanage......
  • Picasso加载图片
    picasso是Square公司开源的一个Android图形缓存库,地址​​http://square.github.io/picasso/​​,可以实现图片下载和缓存功能。仅仅只需要一行代码就能完全实现图片的异步加......
  • Golang学习日志 ━━ 理解依赖包的管理(mod/非mod)和加载方式(项目路径、相对路径、绝对
    go有很多种方法调用依赖包,mod又加入了对包的版本管理。方式太多不免有令人迷惑和混乱的地方,希望本文能帮助大家了解目前使用规则一、mod/非mod管理方式go提供了两种项目......
  • Pytorch入门(4)—— Tensor和Module的保存与加载
    参考:动手学深度学习注意:由于本文是jupyter文档转换来的,代码不一定可以直接运行,有些注释是jupyter给出的交互结果,而非运行结果!!文章目录​​1.读写Tensor​​​​2.读写......
  • Unity :Android局域网通信、UDP广播实现控制器开关(继电器)
    要解决的问题:1.UDP广播:因为平板控制的不仅仅是继电器,还有其他电脑2.继电器接收的是16进制指令,所以要把12个按钮1-12转换成16进制,要不然一个个的发送太麻烦,这里是指令的理......
  • [Android开发学iOS系列] TableView展现一个list
    TableView基础本文讲讲TableView的基本使用.顺便介绍一下delegation.TableView用来做什么TableView用来展示一个很长的list.和Android中的RecyclerView不同,iOS中的......