
引言一個被低估的基石現在提到 Android 列表大家第一反應都是 RecyclerView 甚至 Compose 的 LazyColumn。BaseAdapter那不就是老古董了嗎不少開發者對它的印象還停留在“面試才用得上”的階段。但在實際工作中大量老項目仍然依賴 ListView BaseAdapter理解它的底層邏輯是你解決歷史遺留 Bug、優化老舊列表性能的唯一鑰匙。更重要的是BaseAdapter 背后的復用機制、適配器模式和性能優化思想是 Android View 體系的抽象基礎。讀懂了它你再看 RecyclerView 和 Compose就會有一種“原來如此”的通透感。本文不會只扔給你幾個代碼片段而是從源碼、設計模式、性能調優、面試陷阱、工程封裝等維度用近兩萬字的篇幅把 BaseAdapter 掰開揉碎講清楚。無論你是正準備面試的初學者還是接手了十年陳釀項目的工程師這篇文章都值得你收藏。1. 基礎篇重新認識 Adapter 家族1.1 繼承體系全景圖在講 BaseAdapter 之前必須先把整個適配器家族的譜系梳理清楚。因為很多時候你并不是直接 new 一個 BaseAdapter而是在 ArrayAdapter、CursorAdapter 等子類上踩坑。它們的繼承關系如下android.widget.Adapter ├── ListAdapter │ └── BaseAdapter │ ├── ArrayAdapterT │ ├── CursorAdapter │ │ └── SimpleCursorAdapter │ └── SimpleAdapter └── SpinnerAdapter └── BaseAdapter (同樣被 Spinner 使用)重點關注 BaseAdapter它是一個抽象類實現了 ListAdapter 和 SpinnerAdapter 兩個接口。這意味著同一套 Adapter 可以同時給 ListView、GridView、Spinner、Gallery 使用。這也是為什么我們在很多老代碼里看到同一個 Adapter 被傳給了不同控件——它的抽象層次足夠高。ArrayAdapter 和 SimpleAdapter 雖然方便但內部邏輯非常死板一旦你的 Item 布局稍微復雜一點、或者數據結構不是簡單的字符串/Map就必須自己繼承 BaseAdapter 來寫。1.2 四個必須重寫的方法任何一個自定義 BaseAdapter不看源碼也要背熟下面四個方法方法簽名作用調用場景int getCount()返回數據源總條數每次測量、布局、滾動都會調用頻率極高Object getItem(int position)獲取某位置的數據對象點擊事件、數據綁定等調用頻率中等long getItemId(int position)獲取某位置的穩定 ID當 ListView 需要判斷兩個條目是否為同一數據時調用View getView(int position, View convertView, ViewGroup parent)創建或復用 Item 視圖整個列表顯示期間調用最多是性能優化的主戰場這四個方法是 BaseAdapter 的靈魂任何一行的實現出現問題輕則崩潰重則列表性能跌入深淵。我們后面的所有優化、封裝、面試考點全都是圍繞它們展開的。1.3 從零實現一個最簡單的 Adapter以及它為什么不行下面是很多人寫的第一個 BaseAdapter。它看起來沒什么毛病數據也能正常顯示但當你用它加載上千條數據時App 就開始原形畢露public class NaiveAdapter extends BaseAdapter { private ListString items; private LayoutInflater inflater; public NaiveAdapter(Context context, ListString items) { this.inflater LayoutInflater.from(context); this.items items; } Override public int getCount() { return items.size(); } Override public Object getItem(int pos) { return items.get(pos); } Override public long getItemId(int pos) { return pos; } Override public View getView(int pos, View convertView, ViewGroup parent) { View row inflater.inflate(R.layout.item_text, parent, false); TextView tv row.findViewById(R.id.tv_content); tv.setText(items.get(pos)); return row; } }這段代碼的致命問題有兩個一是每次 getView 都在 inflate二是每次都在 findViewById。對于一屏只能顯示七八條的設備上下快速滑動時會產生成百上千次布局膨脹和視圖查找。GC 瘋狂工作主線程不斷卡頓。如果你剛好在做性能優化Profile 工具里那根紫紅色的 Layout Measure 柱子源頭大概率就在這里。2. 復用篇convertView 與 ViewHolder 的演進之路2.1 convertView系統遞給你的復用護照getView 方法的第二個參數 convertView不是隨便命名的。它代表系統回收的一個 Item View。當列表頂部的一個條目被完全滑出屏幕ListView 內部的復用池RecycleBin并不會立刻銷毀這個 View而是把它暫存起來。等到底部需要顯示新的條目時這個暫存的 View 就會作為 convertView 傳入 getView 中。你要做的事情就是判斷 convertView 是否為 null如果是 null 才 inflate否則直接復用只更新數據。Override public View getView(int pos, View convertView, ViewGroup parent) { if (convertView null) { convertView inflater.inflate(R.layout.item_text, parent, false); } TextView tv convertView.findViewById(R.id.tv_content); tv.setText(items.get(pos)); return convertView; }這段代碼解決了布局膨脹的問題但 findViewById 依然每次都在執行。當你的 Item 里有五六個控件時每滑一次就調用五六次 findViewById依然不夠理想。2.2 ViewHolder把 findViewById 徹底鎖死在創建階段ViewHolder 的思想可以用一句話概括用空間換時間把布局里所有子 View 的引用緩存到一個對象里掛載在 convertView 上。這樣一來只要 convertView 不是 null就可以直接從它的 tag 里取出 ViewHolder完全跳過 findViewById。static class ViewHolder { TextView tvTitle; ImageView ivIcon; TextView tvDesc; } Override public View getView(int pos, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView inflater.inflate(R.layout.item_complex, parent, false); holder new ViewHolder(); holder.tvTitle convertView.findViewById(R.id.tv_title); holder.ivIcon convertView.findViewById(R.id.iv_icon); holder.tvDesc convertView.findViewById(R.id.tv_desc); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } ItemData data items.get(pos); holder.tvTitle.setText(data.getTitle()); holder.tvDesc.setText(data.getDesc()); // 圖片異步加載 return convertView; }ViewHolder 現在幾乎是每個 Android 工程師必備的基本功但在早期 Android 開發中這可是面試的必考題。事實上RecyclerView 直接把這個模式做成了強制的 API說明這個模式的正確性無可爭議。2.3 RecycleBin 源碼級剖析你可能會好奇convertView 到底是從哪來的答案在 AbsListView 的內部類 RecycleBin 中。雖然我們不應該依賴內部實現寫業務代碼但理解它的原理能幫你解釋很多奇怪的滑動行為。// AbsListView.RecycleBin 簡化關鍵代碼 class RecycleBin { private View[] mActiveViews new View[0]; private ArrayListView mScrapViews; View getScrapView(int position) { // 先嘗試從活躍視圖數組里取適用于數據未變化時的快速位置匹配 // 否則從復用池列表中取出最后一個 if (mScrapViews.size() 0) { return mScrapViews.remove(mScrapViews.size() - 1); } return null; } void addScrapView(View scrap, int position) { // 當一個 View 完全滑出屏幕后根據當前 adapter 的 view type 放入對應池子 int viewType mAdapter.getItemViewType(position); mScrapViews[viewType].add(scrap); } }這里有兩個值得注意的點第一mActiveViews 用于暫存當前屏幕上的活躍 View當數據沒有變化且布局尺寸不變時系統可以直接從數組里按 position 取回 View甚至不需要調用 getView——這是 ListView 比 ScrollView 快的一個原因。第二當存在多種 ViewType 時mScrapViews 不是一個簡單的列表而是一個數組每種 type 擁有獨立的復用池。這個設計直接關聯到后面的多布局適配。3. 多布局篇getItemViewType 的真正含義3.1 為何需要多布局絕大多數 App 列表都不是只有一種 Item 布局。聊天界面有發送和接收兩種氣泡設置頁有開關項、跳轉項、標題項新聞列表可能插入廣告卡片。這些場景都需要 Adapter 支持多種 ViewType。BaseAdapter 提供了兩個關鍵方法來實現多布局int getItemViewType(int position)返回當前位置的布局類型標識返回值必須是 0 到 getViewTypeCount()-1 之間的整數。int getViewTypeCount()聲明總共有幾種布局類型默認返回 1。如果你不覆寫這兩個方法所有 View 都視為同一種類型。后果就是一個左對齊氣泡的 View 被回收后復用時你可能用 findViewById 去找一個只有右對齊布局才有的控件直接 NPE。更隱蔽的情況是兩個布局里相同 id 的控件類型不同findViewById 拿到錯誤類型的 View強轉失敗程序崩潰。3.2 多布局的復用池隔離機制當你正確覆寫了 getViewTypeCount 返回 3 時ListView 內部會初始化三個獨立的 mScrapViews 列表。類型為 0 的 View 永遠不會被當作 convertView 傳給類型為 1 的 getView 調用。這就是“隔離”的具體實現。實現多布局的典型代碼模板如下public class ChatAdapter extends BaseAdapter { private static final int TYPE_SENT 0; private static final int TYPE_RECEIVED 1; private static final int TYPE_TIMESTAMP 2; private ListMessage messages; Override public int getViewTypeCount() { return 3; } Override public int getItemViewType(int position) { Message msg messages.get(position); if (msg.isTimestamp()) return TYPE_TIMESTAMP; return msg.isSentByMe() ? TYPE_SENT : TYPE_RECEIVED; } Override public View getView(int pos, View convertView, ViewGroup parent) { int type getItemViewType(pos); ViewHolder holder; if (convertView null) { int layoutId (type TYPE_SENT) ? R.layout.item_sent : (type TYPE_RECEIVED) ? R.layout.item_received : R.layout.item_timestamp; convertView inflater.inflate(layoutId, parent, false); holder new ViewHolder(convertView, type); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } holder.bind(messages.get(pos)); return convertView; } // ... getCount / getItem / getItemId 略 }注意 ViewHolder 的創建邏輯必須根據 type 來初始化不同的子 View 引用否則你仍然可能在綁定數據時拿到 null。3.3 getViewTypeCount 的隱含約定有一點很少被提及getViewTypeCount 的返回值必須始終是一個正整數而且一旦返回ListView 內部就會分配等量的復用池。如果中途你讓 getViewTypeCount 返回的值變大比如從 3 變成 4系統不會重新分配池子getItemViewType 返回 3 時就可能發生越界。因此ViewType 的數量應該在 Adapter 構造時就完全確定并且再也不會變化。4. 性能篇讓你的 ListView 幀率跑滿 60fps4.1 布局層級——被忽視的滑動殺手有了 ViewHolder 之后findViewById 的消耗已經不是瓶頸真正的性能瓶頸轉移到了測量和繪制階段。Item 布局越深onMeasure 和 onLayout 的遞歸計算量就越大。如果一個列表每個 Item 的布局都是 LinearLayout 套 LinearLayout那性能基本無解。改進建議優先使用 ConstraintLayout 減少嵌套層級。如果必須使用 LinearLayout盡量控制在兩層以內。使用merge標簽作為布局根節點但要配合 inflate 時傳入 parent 參數。避免在 Item 中使用 RelativeLayout 做復雜相對定位——它的測量可能觸發兩次 layout。!-- 推薦一層 ConstraintLayout 搞定復雜布局 -- androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content ImageView android:idid/iv_avatar ... / TextView android:idid/tv_name ... / TextView android:idid/tv_msg ... / /androidx.constraintlayout.widget.ConstraintLayout4.2 異步加載與線程安全getView 方法運行在主線程任何耗時操作都會直接導致丟幀。網絡請求、數據庫查詢、大圖解碼都必須完全異步。我們用圖片加載舉例Override public View getView(int pos, View convertView, ViewGroup parent) { ViewHolder holder getViewHolder(convertView, parent); holder.tvName.setText(data.get(pos).getName()); // Glide 內部自動處理了 cancel、占位和復用錯位 Glide.with(holder.ivAvatar.getContext()) .load(data.get(pos).getAvatarUrl()) .placeholder(R.drawable.ic_placeholder) .error(R.drawable.ic_error) .into(holder.ivAvatar); return holder.getConvertView(); }如果你不用成熟的圖片框架而是自己開線程下載圖片就必須額外處理“View 被復用時取消上一個下載任務”的問題否則就會出現經典的圖片錯位閃爍。4.3 對象創建與 GC 壓力不要小看在 getView 里 new 一個對象帶來的開銷。即使只是一個 OnClickListener如果你給每個 Item 都 new 一次滑動時大量的匿名內部類對象會讓 GC 頻繁觸發。正確做法有幾種把 ClickListener 提升為 Adapter 的成員變量通過 view.getTag() 或者 position 判斷當前點擊的是哪一項。使用一個靜態 Handler 或者單例的 Listener 實例數據綁定時不創建新對象。字符串格式化、DecimalFormat 等創建成本高的操作提前在數據層做好getView 中只做純綁定。// 反例每次都在創建匿名內部類 holder.btnDelete.setOnClickListener(v - deleteItem(pos)); // 推薦成員變量 通過 tag 獲取 position private View.OnClickListener deleteListener v - { int pos (int) v.getTag(); deleteItem(pos); }; Override public View getView(int pos, View convertView, ViewGroup parent) { // ... holder.btnDelete.setTag(pos); holder.btnDelete.setOnClickListener(deleteListener); // ... }4.4 分頁與增量更新BaseAdapter 沒有內置分頁機制但你應該在數據層面做好控制。不要把幾萬條數據全部 load 到內存再塞給 Adapter。一般的做法是使用一個 ArrayList 作為數據緩存初始只加載前 50 條。監聽 ListView 的 OnScrollListener當最后一個可見條目接近數據緩存末尾時異步加載下一頁追加到 ArrayList 然后調用 notifyDataSetChanged。如果你需要更平滑的體驗可以手工計算出新增條目在列表中的位置范圍可惜 BaseAdapter 沒有類似 notifyItemRangeInserted 的精準通知方法所以還是得承受一次全局刷新。這也是 RecyclerView 出現的一個重要推動力——分頁加載時全局刷新的代價太大。5. 面試篇那些年關于 BaseAdapter 的送命題5.1 getView 到底會被調用多少次這道題幾乎沒有標準答案但可以從幾個角度分析首次加載時getView 一般會被調用屏幕可顯示條目數 1 次因為 ListView 會多預加載一個 Item 用于測量和滑動緩沖。在測量階段同一個 position 可能被多次調用——ListView 可能需要先拿到一個 View 測量高度再根據整體布局方案重新請求一次。調用 notifyDataSetChanged 之后所有當前屏幕上的 Item 都會重新走 getView之前緩存的 View 全部作廢。滑動過程中每滑入一個新條目getView 就調用一次convertView 非 null且不保證 position 是遞增的因為來回滑動時你可能看到 position 前后跳躍。如果你在面試中能深入到這個粒度加上對 mActiveViews 的理解面試官基本會對你刮目相看。5.2 notifyDataSetChanged 的代價有多大簡而言之代價是 O(n)其中 n 是當前屏幕上的條目數。更糟的是它會讓整個列表里所有和 RecycleBin 相關的緩存失效所有 mActiveViews 清空所有回收 View 被打上“臟”標記。下一次 layout 時屏幕上的每一個條目哪怕數據根本沒變也要重新走一次 getView。這也是為什么 RecyclerView 引入了 DiffUtil 和精準通知——只對真正變化的條目觸發重新綁定。5.3 getItem 和 getItemId 到底有什么用很多開發者只在 getView 里調用 data.get(position)從來不依賴 getItem這其實會埋雷。因為 ListView 內部在一些場景下會調用 getItem 來判定數據集是否變化。getItemId 則用于區分兩個 position 是否代表同一個邏輯數據。如果你覆寫了 getItemId 并返回了唯一標識比如數據庫主鍵系統可以在 notifyDataSetChanged 后通過比對 id 來判定某些 Item 實際上沒有變化從而減少部分刷新操作。可惜的是這個優化在 ListView 上的表現并不穩定到了 RecyclerView 才被穩定地利用。5.4 多布局復用池的隔離面試官還想聽什么你可以進一步展開如果 getViewTypeCount 返回 5但 getItemViewType 只返回 0~3會發生什么答案是ListView 會為類型 4 創建一個空的復用池雖然不會崩潰但浪費了內存。另外不同類型之間雖然復用池隔離但 RecyclerView 中的 RecycledViewPool 默認是共享的你可以通過 setMaxRecycledViews 控制每種 type 的最大緩存數——這一點在對比兩者時是很加分的細節。6. 封裝篇打造你自己的 BaseListAdapter6.1 封裝目標任何超過兩個 Adapter 的項目都值得做一層封裝。我們希望達到的效果是不必再手寫 ViewHolder 內部類。不必在每個 getView 里寫 findViewById。支持多布局時子類只需要聲明 type 和對應布局不需要處理 convertView 復用細節。內置 Item 點擊回調不污染 Activity。6.2 通用 ViewHolderpublic class ViewHolder { private SparseArrayView views new SparseArray(); private View convertView; private ViewHolder(View convertView) { this.convertView convertView; convertView.setTag(this); } public static ViewHolder get(View convertView, ViewGroup parent, int layoutId) { if (convertView null) { convertView LayoutInflater.from(parent.getContext()) .inflate(layoutId, parent, false); return new ViewHolder(convertView); } return (ViewHolder) convertView.getTag(); } public T extends View T getView(int viewId) { View v views.get(viewId); if (v null) { v convertView.findViewById(viewId); views.put(viewId, v); } return (T) v; } public View getConvertView() { return convertView; } // 便捷鏈式方法 public ViewHolder setText(int viewId, String text) { ((TextView) getView(viewId)).setText(text); return this; } public ViewHolder setImageRes(int viewId, int resId) { ((ImageView) getView(viewId)).setImageResource(resId); return this; } }6.3 抽象通用 Adapterpublic abstract class CommonAdapterT extends BaseAdapter { protected Context context; protected ListT data; private OnItemClickListenerT clickListener; public CommonAdapter(Context context, ListT data) { this.context context; this.data data; } Override public int getCount() { return data null ? 0 : data.size(); } Override public T getItem(int pos) { return data.get(pos); } Override public long getItemId(int pos) { return pos; } protected abstract int getItemLayoutId(int pos); protected abstract void convert(ViewHolder holder, T item, int pos); Override public View getView(int pos, View convertView, ViewGroup parent) { ViewHolder holder ViewHolder.get(convertView, parent, getItemLayoutId(pos)); convert(holder, getItem(pos), pos); holder.getConvertView().setOnClickListener(v - { if (clickListener ! null) clickListener.onItemClick(getItem(pos), pos); }); return holder.getConvertView(); } public void setOnItemClickListener(OnItemClickListenerT listener) { this.clickListener listener; } public interface OnItemClickListenerT { void onItemClick(T item, int position); } }使用這個封裝你只需要寫兩樣東西布局 ID 和 convert 方法。任何只包含一種布局的列表三分鐘就能寫完 Adapter。6.4 支持多布局的擴展版本如果你需要多布局只需要再增加三個抽象方法public abstract class MultiCommonAdapterT extends BaseAdapter { // ... 同前 ... protected abstract int getViewTypeCount_(); protected abstract int getItemViewType_(int pos); protected abstract int getLayoutIdByType(int type); protected abstract void convert(ViewHolder holder, T item, int pos, int type); Override public int getViewTypeCount() { return getViewTypeCount_(); } Override public int getItemViewType(int pos) { return getItemViewType_(pos); } Override public View getView(int pos, View convertView, ViewGroup parent) { int type getItemViewType(pos); ViewHolder holder ViewHolder.get(convertView, parent, getLayoutIdByType(type)); convert(holder, getItem(pos), pos, type); return holder.getConvertView(); } }經過這一層抽象團隊里任何人寫 Adapter 都只需要關注數據和布局復用邏輯被徹底鎖死在基類里。7. 踩坑篇生產環境中的那些詭異 Bug7.1 CheckBox 選中狀態滿天飛這是新人最容易踩的坑列表每個 Item 有一個 CheckBox你選中了第 2 條滑動到底部再回來發現第 10 條也被選中了。原因很簡單View 被復用時CheckBox 的選中狀態沒有被重置數據源里也沒有記錄哪些位置被選中。解決方式在數據源維護一個 SetInteger 記錄被選中的 positionconvert 方法中顯式調用 setChecked。7.2 EditText 輸入內容漂移當 Item 包含 EditText 時你輸入了一些文字滑動幾下內容就不翼而飛甚至跑到了另一個 Item 上。根本原因是 EditText 的內容沒有寫回數據模型。解決方案是為 EditText 設置 TextWatcher在 afterTextChanged 里更新數據源對應 position 的數據。注意防止死循環在代碼里 setText 時先移除 TextWatcher設置完再加回來或者通過 tag 標記跳過回調。7.3 內存泄漏三劍客Adapter 持有 Activity 引用而 ListView 又持有 Adapter形成了 Activity → ListView → Adapter → Activity 的引用鏈。如果 Activity 銷毀時沒有清空 ListView 的 Adapter就會泄漏。最佳實踐Adapter 用靜態內部類Context 使用 ApplicationContext但注意 inflate 主題問題。在 onDestroy 中調用 listView.setAdapter(null)。回調使用 WeakReference 包裝。7.4 快速滑動時圖片還是錯位即使用了 Glide在極端快速滑動時可能因異步回調時序問題導致短暫錯位。這并不是 Glide 的 Bug而是你手動給 ImageView 設置了默認圖片或動畫后沒有正確處理 View 復用時的 cancel。解決方案在 ViewHolder 中持有 Request在 View 離開屏幕時可以在 Adapter 里維護一個 Map 或者利用 View 的 onDetachedFromWindow取消之前的請求。8. 進階篇BaseAdapter vs RecyclerView.Adapter 全面對比維度BaseAdapter (ListView)RecyclerView.Adapter復用機制手動 convertView ViewHolder 模式強制 ViewHolder內置緩存局部刷新僅 notifyDataSetChangednotifyItemInserted / Removed / Changed / Moved動畫無內置支持ItemAnimator 實現增刪移動動畫布局管理僅垂直列表LayoutManager 支持線性、網格、瀑布流項裝飾只支持 dividerItemDecoration 自定義繪制緩存層級單級緩存 (mScrapViews)四級緩存 (mAttachedScrap, mCachedViews, ViewCacheExtension, RecycledViewPool)代碼規范自由度高易寫出差性能代碼強約束引導最佳實踐總結一句話新項目永遠用 RecyclerView。但你如果還在維護老代碼或者面試時被問到“ListView 和 RecyclerView 有什么區別”上面的每一行你都要能展開講三分鐘。9. 設計哲學篇BaseAdapter 中的軟件工程思想9.1 享元模式復用池的本質RecycleBin 是享元模式在 Android 中最經典的落地之一。通過共享有限數量的 View 實例避免了大批量創建和銷毀的昂貴開銷。如果你做過 Java 后端可以把它類比為數據庫連接池如果你寫游戲這就是對象池。理解這個模式你就知道為什么“不復用”的列表在大數據量下會直接崩盤。9.2 適配器模式統一接口的價值BaseAdapter 屏蔽了底層數據源可能是 List、Cursor、數組的差異為 ListView 暴露了一個統一的接口。這種解耦讓 ListView 可以毫不關心數據從哪來你甚至可以寫一個 Adapter 從網絡流式讀取數據。這也是為什么你能把同一個 Adapter 傳給 ListView 和 Spinner——適配器抹平了消費端的差異。9.3 從 BaseAdapter 到 RecyclerView再到 Compose技術的演進有一條清晰的主線BaseAdapter 階段解決“有得用”的問題定義了 Android 列表編程的范式。RecyclerView 階段解決“用得好”的問題引入插件化、局部刷新和動畫。Compose 階段解決“寫得爽”的問題聲明式 UI 徹底消滅了 Adapter 和 ViewHolder 的概念。如果你能從 BaseAdapter 一路走下來你會非常自然地理解 RecyclerView 的每一項設計決策也會對 Compose 的“去 Adapter 化”有更深的理解。這就是基礎的力量。10. 總結看了近兩萬字我們最后收一收。BaseAdapter 不是一個過時的類庫它是一座橋梁。一端連著早期 Android 開發者的血淚教訓另一端連著現代 RecyclerView 和 Compose 的設計源頭。理解它你掌握的不僅僅是四個方法和一個復用池而是整個 Android View 體系關于性能、復用和解耦的設計脈絡。如果你的項目還在用 ListView這篇文章里的封裝代碼和優化清單可以直接拿去用。如果你正在準備面試今晚把文章里提到的面試題再看一遍明天面試時你就能把面試官問住。如果這是你 Android 學習路的起點恭喜你你已經站在了最重要的那塊基石上。