1. 程式人生 > >Android 系統 Settings 啟動流程詳解

Android 系統 Settings 啟動流程詳解

Settings簡介

  Settings 是 Android 系統自帶的一個很重要的應用,給使用者提供了操作 Android 系統功能的介面。它裡面包含了 Wireless & network,device,personal 以及 system 等幾大塊的功能設定。在 Android 原始碼中,該應用位於 packages/apps/Settings 下。該應用的原始碼是相當複雜的,設計思想很是先進,很難完全講清楚,筆者也是讀了好幾遍原始碼再綜合了幾篇部落格才勉強懂了Settings其啟動流程的大體思路。通過部落格記錄下來以加深理解和印象,同時分享給大家。
  

Settings 啟動流程詳解

1.直接跳轉子介面

  首先找到 Settings 目錄,其目錄結構如下,檔案太多,無法完展開。
  這裡寫圖片描述
  
這裡通過每個資料夾的命名可以大概知道,每個包的大體作用是什麼。由於本文主要講解啟動流程,所以先不管這些。我們先找到 Settings 的啟動類,通常我們可以從清單檔案中得知該應用的啟動類,如下圖:
這裡寫圖片描述

從圖中可以清楚的看到,Settings 的啟動類為 Settings。從 Settings 原始碼中我們找到了Settings.java檔案。但是,開啟這個檔案後,會感到了一臉懵逼。如下圖:
這裡寫圖片描述

該類中都是些空實現的靜態內部類,沒有任何與介面載入相關的內容。這是為什麼呢?看上面有句英文註釋就明白了,意思是這些子類是為了啟動特定獨立的 Settings 選項而建立的,例如在某個應用裡需要設定無線那麼只需要啟動無線對應的類就可以了,而沒必要開啟settings應用再點選wifi設定項進行設定。再看此類繼承於 SettinggsActivity,這時我們就應該可以想到,初始化介面應該在它父類 SettinggsActivity 裡完成的。為了方便講解,我們先以wifi設定頁面WifiSettingsActivity 的直接跳轉為例,詳細講解這個啟動流程。懂了這個之後,其他子頁面的啟動自然就明白了。

接下來我們在清單檔案中找到 WifiSettingsActivity 的定義如下:
  這裡寫圖片描述
  
其中有 meta-data 的標籤使用,從這個標籤的 key-value 來看,很明顯可以認為WifiSettings的具體實現應該是由 WifiSettings 這個 Fragment 來佈局渲染的。然後我們回到 SettingsActivity 中,找到 onCreate() 方法如下:
這裡寫圖片描述

可以看到,一進入 oncreate 裡有個 getMetaData(), 這和我們之前看到的清單檔案裡的meta似乎有某種聯絡,點進去看,程式碼如下:
這裡寫圖片描述

可以看到,這個函式的主要作用就是從 Activity 標籤中獲取 meta-data 標籤中key為 com.android.settings.FRAGMENT_CLASS 的值,並將其賦值給 mFragmentClass 這個私有變數。
以 WifiSettingsActivity為 例,從這個 Activity 中 meta-data 標籤中獲取的資訊為 com.android.settings.wifi.WifiSettings,即mFragmentClass=”com.android.settings.wifi.WifiSettings”。
getMetaData() 執行完後緊接著執行了 getIntent(),getMetaData() 上面有句註釋 should happen before any call to getIntent。意思是 getIntent() 必須在 getMetaData() 之後執行,其實這也有原因的,點進 getIntent() 方法看看就知道了。程式碼具體如下:
這裡寫圖片描述

繼續看 getStartingFragmentClass():
這裡寫圖片描述

從原始碼看以看出,getIntent 的作用就是構造了一個 Intent,並且給它增加了一個特殊的鍵值對,key為”:settings:show_fragment”,value為 mFragmentClass 指定的 Fragment 類名。
之所以要先執行getMetaData,是因為 mFragmentClass 賦值是在 getMeatData 中進行的。

明白之後我們繼續分析onCreate()方法:

    final ComponentName cn = intent.getComponent();
    final String className = cn.getClassName();// 本例中,className為WifiSettingsActivity
    mIsShowingDashboard = className.equals(Settings.class.getName()); //因此這裡為false
        ...
        ...
            setContentView(mIsShowingDashboard ?
           R.layout.settings_main_dashboard : R.layout.settings_main_prefs);//本例中這裡選擇了後者
          ...
          ...
            } else {
                if (!mIsShowingDashboard) {//因為mIsShowingDashboard為false,所以會到這裡
                    ....
    //initialArguments通過賦值儲存了meta-data中指定的com.android.settings.wifi.WifiSettings
      Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
                   switchToFragment(initialFragmentName, initialArguments, true, false,
                            mInitialTitleResId, mInitialTitle, false);//走到這裡進行fragment替換
               } else {
                    // No UP affordance if we are displaying the main Dashboard
                    mDisplayHomeAsUpEnabled = false;
                    // Show Search affordance
                    mDisplaySearch = true;
                    mInitialTitleResId = R.string.dashboard_title;
                   switchToFragment(DashboardSummary.class.getName(), null, false, false,
                            mInitialTitleResId, mInitialTitle, false);
                }
            }

我們來具體看一下 switchToFragment() 方法:
這裡寫圖片描述

通過 FragmentTransaction 的 replace 方法,將Fragment的佈局在 R.id.main_content 指定的位置進行渲染。

2.主介面啟動流程

上面講的是沒有通過點選 Settings 主介面的選項而直接開啟子介面的啟動過程,下面我們介紹通過點選setting主介面的選項進入子介面的過程。
  通過前面的講解我們知道,mIsShowingDashboard 的值( true/false )是確實載入主介面還是子介面的唯一條件。我們回到相關程式碼:

    final ComponentName cn = intent.getComponent();
    final String className = cn.getClassName();// 因為從主介面啟動,所以這裡className為Settings
    mIsShowingDashboard = className.equals(Settings.class.getName()); //因此這裡變成了true
        ...
        ...
            setContentView(mIsShowingDashboard ?
           R.layout.settings_main_dashboard : R.layout.settings_main_prefs);//選擇了前者
          ...
          ...
          } else {
                if (!mIsShowingDashboard) {//因為mIsShowingDashboard為true,不走這裡了
                    ....

                   Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
                   switchToFragment(initialFragmentName, initialArguments, true, false,
                            mInitialTitleResId, mInitialTitle, false);//
               } else {//從主介面進入,走這裡
                    // No UP affordance if we are displaying the main Dashboard
                    mDisplayHomeAsUpEnabled = false;
                    // Show Search affordance
                    mDisplaySearch = true;
                    mInitialTitleResId = R.string.dashboard_title;
                   switchToFragment(DashboardSummary.class.getName(), null, false, false, mInitialTitleResId, mInitialTitle, false);//接下來重點分析這裡
                }
            }

從上面分析可以知道如果從主介面進入的話 switchToFragment 會將當前頁面替換成 DashboardSummary,我們看一下 DashboardSummary.java 的程式碼:
這裡寫圖片描述

這是一個 fragment,在 onCreateView 裡,填充了 dashboard.xml. 來看一下這個佈局:
這裡寫圖片描述

這是一個垂直可滾動的線性結構,很容易聯想到我們手機裡的設定主頁面,的確如此。再繼續看DashboardSummary 程式碼,在 onResume() 裡:
這裡寫圖片描述

有 SendReBuildUI(),點進去檢視:
這裡寫圖片描述

原來裡面是在發訊息,找到訊息的接收者:
這裡寫圖片描述

終於發現了裡面的 reBuildUI 的方法:

private void rebuildUI(Context context) {
      if (!isAdded()) {
          Log.w(LOG_TAG, "Cannot build the DashboardSummary UI yet as the Fragment is not added");
          return;
      }

      long start = System.currentTimeMillis();
      final Resources res = getResources();

      mDashboard.removeAllViews();
    //(1)這裡呼叫SettingActivity的getDashboardCategories,也就是載入整個Setting的內容
      List<DashboardCategory> categories =
              ((SettingsActivity) context).getDashboardCategories(true);//注意該方法

      final int count = categories.size();

      for (int n = 0; n < count; n++) {
          DashboardCategory category = categories.get(n);

          View categoryView = mLayoutInflater.inflate(R.layout.dashboard_category, mDashboard,
                  false);

          TextView categoryLabel = (TextView) categoryView.findViewById(R.id.category_title);
          categoryLabel.setText(category.getTitle(res));

          ViewGroup categoryContent =
                  (ViewGroup) categoryView.findViewById(R.id.category_content);

          final int tilesCount = category.getTilesCount();
          for (int i = 0; i < tilesCount; i++) {
              DashboardTile tile = category.getTile(i);
//(2)建立DashboardTileView,也就是每個Setting的內容
              DashboardTileView tileView = new DashboardTileView(context);
              updateTileView(context, res, tile, tileView.getImageView(),
                      tileView.getTitleTextView(), tileView.getStatusTextView());

              tileView.setTile(tile);

              categoryContent.addView(tileView);
          }

          // Add the category
          mDashboard.addView(categoryView);
      }
      long delta = System.currentTimeMillis() - start;
      Log.d(LOG_TAG, "rebuildUI took: " + delta + " ms");
  }

接下來對上面兩處註釋進行說明:
(1)處 rebuildUI 裡呼叫 getDashboardCategories() 方法,該方法如下:

這裡寫圖片描述

這個方法裡又呼叫了 buildDashboardCategories() 方法:
這裡寫圖片描述

看到這裡終於明白了,裡面有個對 dashboard_categories.xml 的處理, loadCategoriesFromResource() 方法就不看了,它的作用是解析 dashboard_categories.xml 這個 xml 檔案。我們看一下dashboard_categories.xml 吧:
這裡寫圖片描述

部分截圖,沒有截圖,因為內容太多了。不過從這區域性就可以看出這對應的就是我們設定主頁面的各個選項。
這裡寫圖片描述

(2)處將通過 for 迴圈遍歷而來的資料通過建立 DashboardTileView 最終全部存入到 mDashboard 這個佈局中,至此整個 Setting 模組的介面佈局已經完成了。

在 DashboardTileView.java 裡,有個 onclick 方法,這就是 settings 主頁面每個子選項的點選事件了,通過點選進入不同的子設定選項,如 wifi,藍芽等。
這裡寫圖片描述

至此,Settings 的啟動方式講解完了,下面附一張自己手畫的一張 Settings 啟動流程的草圖,畫的比較醜,湊合的看….:
這裡寫圖片描述