Android5.1源码分析系列Settings源码分析

Posted EndLessTwoWay

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android5.1源码分析系列Settings源码分析相关的知识,希望对你有一定的参考价值。

最近在看android源码,现在想做一个系列,专门对源码进行简单直接的分析。本人道行尚浅,希望大家能够进行批评,本人感激不尽。

Android5.1 Settints源码分析

1 概要

本文分析的文件目录:

/packages/apps/Settings/AndroidManifest.xml
/packages/apps/settings/src/com/android/settings/Settings.java
/packages/apps/settings/src/com/android/settings/SettingsActivity.java
/packages/apps/settings/res/layout/Settings_main_prefs.xml
/packages/apps/settings/src/com/android/settings/dashboard/DashboardSummary.java 
/packages/apps/settings/res/layout/Dashboard.xml
/packages/apps/settings/res/xml/Dashboard_categories.xml
/packages/apps/settings/src/com/android/settings/dashboard/DashboardTileView.java 

本文将从这几个文件进行简要的分析,对Settings的大致过程进行简要的分析。

2 分析过程

2.1 AndroidManifest.xml

在分析源码应用的过程中首先看它的清单文件。AndroidManifest.xml 是每个android程序中必须的文件。它位于整个项目的根目录,描述了package中暴露的组件(activities, services, 等等),他们各自的实现类,各种能被处理的数据和启动位置。 除了能声明程序中的Activities, ContentProviders, Services, 和Intent Receivers,还能指定permissions和instrumentation(安全控制和测试)。在Settings的清单文件中可以看到:

        <activity-alias android:name="Settings"
                android:taskAffinity="com.android.settings"
                android:label="@string/settings_label_launcher"
                android:launchMode="singleTask"
                android:targetActivity="Settings">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.APP_SETTINGS" />
            </intent-filter>
        </activity-alias>

可以看出其启动类是Settings。ok,现在来看Settings.java。

2.2 Settings.java

public class Settings extends SettingsActivity {

    /*
    * Settings subclasses for launching independently.
    */
    public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
    public static class WirelessSettingsActivity extends SettingsActivity { /* empty */ }
    public static class SimSettingsActivity extends SettingsActivity { /* empty */ }
    public static class TetherSettingsActivity extends SettingsActivity { /* empty */ }
    public static class VpnSettingsActivity extends SettingsActivity { /* empty */ }
    public static class DateTimeSettingsActivity extends SettingsActivity { /* empty */ }
    public static class StorageSettingsActivity extends SettingsActivity { /* empty */ }
    public static class WifiSettingsActivity extends SettingsActivity { /* empty */ }
    public static class WifiP2pSettingsActivity extends SettingsActivity { /* empty */ }
    public static class InputMethodAndLanguageSettingsActivity extends SettingsActivity { /* empty */ }
    public static class KeyboardLayoutPickerActivity extends SettingsActivity { /* empty */ }
    public static class InputMethodAndSubtypeEnablerActivity extends SettingsActivity { /* empty */ }
    public static class VoiceInputSettingsActivity extends SettingsActivity { /* empty */ }
    public static class SpellCheckersSettingsActivity extends SettingsActivity { /* empty */ }
    ...
    }

打开Settings这个类我们并没有找到一些方法,只有一些静态内部类。这些类能够直接被调用而启动。用来直接进行设置相关功能的启动类。ok,既然其直接继承SettingsActivity 类,那我们继续跳转到SettingsActivity .java。

2.3 SettingsActivity .java

public class SettingsActivity extends Activity
        implements PreferenceManager.OnPreferenceTreeClickListener,
        PreferenceFragment.OnPreferenceStartFragmentCallback,
        ButtonBarHandler, FragmentManager.OnBackStackChangedListener,
        SearchView.OnQueryTextListener, SearchView.OnCloseListener,
        MenuItem.OnActionExpandListener 

上述代码可以看到SettingsActivity继承了Activity类,之后我们进入onCreate中,看看其中到底做了什么。onCreate中的方法比较多,下面捡重点的进行分析。

     // Should happen before any call to getIntent()
        getMetaData();

首先看getMetaData()这个方法;

   private void getMetaData() {
        try {
            ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
                    PackageManager.GET_META_DATA);
            if (ai == null || ai.metaData == null) return;
            mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
        } catch (NameNotFoundException nnfe) {
            // No recovery
            Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());
        }
    }

由上述代码可以看出getMetaData()这个方法是为了获取mFragmentClass ,获取到就会启动相应的Activity。如果是直接启动Settings模块,则直接return,不会再去获取mFragmentClass 。ok,继续往下看:

        final ComponentName cn = intent.getComponent();
        final String className = cn.getClassName(); 
        mIsShowingDashboard = className.equals(Settings.class.getName());

这段代码可以看出,设置了一个标志,当前是不是我们的主界面,因为Settings继承Activity所以其值为true.
接着:

        // This is a "Sub Settings" when:
        // - this is a real SubSettings
        // - or :settings:show_fragment_as_subsetting is passed to the Intent
        final boolean isSubSettings = className.equals(SubSettings.class.getName()) ||
                intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false);

很明显这是一个二级设置,并且明显的判断出其值为false,以下有一个判断:

        // If this is a sub settings, then apply the SubSettings Theme for the ActionBar content insets
        if (isSubSettings) {
            // Check also that we are not a Theme Dialog as we don‘t want to override them
            final int themeResId = getThemeResId();
            if (themeResId != R.style.Theme_DialogWhenLarge &&
                    themeResId != R.style.Theme_SubSettingsDialogWhenLarge) {
                setTheme(R.style.Theme_SubSettings);
            }
        }

既然是我们设置的主界面,所以这一步省略,暂且不讲。
好的,我们接着看:

        setContentView(mIsShowingDashboard ?
                R.layout.settings_main_dashboard : R.layout.settings_main_prefs);

这一步很容易也很关键,加载一个xml,因为判断的mIsShowingDashboard为true,所以取settings_main_dashboard 这个xml,ok,我们就来看这个:
settings_main_dashboard.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/main_content"
             android:layout_height="match_parent"
             android:layout_width="match_parent"
             android:background="@color/dashboard_background_color"
             />

一看就明白,这个就是一个容器,用来为fragment布局,ok,这没啥看的,直接往下走:

 if (savedState != null) {
            // We are restarting from a previous saved state; used that to initialize, instead
            // of starting fresh.
            mSearchMenuItemExpanded = savedState.getBoolean(SAVE_KEY_SEARCH_MENU_EXPANDED);
            mSearchQuery = savedState.getString(SAVE_KEY_SEARCH_QUERY);

            setTitleFromIntent(intent);

            ArrayList<DashboardCategory> categories =
                    savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
            if (categories != null) {
                mCategories.clear();
                mCategories.addAll(categories);
                setTitleFromBackStack();
            }

            mDisplayHomeAsUpEnabled = savedState.getBoolean(SAVE_KEY_SHOW_HOME_AS_UP);
            mDisplaySearch = savedState.getBoolean(SAVE_KEY_SHOW_SEARCH);
            mHomeActivitiesCount = savedState.getInt(SAVE_KEY_HOME_ACTIVITIES_COUNT,
                    1 /* one home activity by default */);
        } else {
            if (!mIsShowingDashboard) {
                // Search is shown we are launched thru a Settings "shortcut". UP will be shown
                // only if it is a sub settings
                if (mIsShortcut) {
                    mDisplayHomeAsUpEnabled = isSubSettings;
                    mDisplaySearch = false;
                } else if (isSubSettings) {
                    mDisplayHomeAsUpEnabled = true;
                    mDisplaySearch = true;
                } else {
                    mDisplayHomeAsUpEnabled = false;
                    mDisplaySearch = false;
                }
                setTitleFromIntent(intent);

                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);
            }
        }

这段代码比较长,但是因为我们是第一次进行创建所以savedState ==null,之后mIsShowingDashboard又为true,所以代码直接看:

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这个Fragment,ok,那么我们直接追随到DashboardSummary这个Fragment。

2.4 DashboardSummary.java

直接打开这个fragment,并且定位到onCreateView方法:

    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        mLayoutInflater = inflater;
        mExt = UtilsExt.getMiscPlugin(this.getActivity());

        final View rootView = inflater.inflate(R.layout.dashboard, container, false);
        mDashboard = (ViewGroup) rootView.findViewById(R.id.dashboard_container);

        return rootView;
    }

这个fragment填充了dashboard的布局,ok,继续点进去看这个布局:
dashboard.xml:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/dashboard"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scrollbarStyle="insideOverlay"
    android:clipToPadding="false">

        <LinearLayout
                android:id="@+id/dashboard_container"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center_horizontal"
                android:paddingStart="@dimen/dashboard_padding_start"
                android:paddingEnd="@dimen/dashboard_padding_end"
                android:paddingTop="@dimen/dashboard_padding_top"
                android:paddingBottom="@dimen/dashboard_padding_bottom"
                android:orientation="vertical"
                />

</ScrollView>

这又是一个空布局,作为一个容器使用,可以看出Settings的选项视图应该就是显示在dashboard_container中的。
ok,继续往下走接着它会跑到onResume()方法:

public void onResume() {
        super.onResume();

        sendRebuildUI();

        final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
        filter.addDataScheme("package");
        getActivity().registerReceiver(mHomePackageReceiver, filter);
    }

从中可以看到过滤到了一些添加,移除应用之类的,但是我们要注意sendRebuildUI()这个方法:

      private void sendRebuildUI() {
        if (!mHandler.hasMessages(MSG_REBUILD_UI)) {
            mHandler.sendEmptyMessage(MSG_REBUILD_UI);
        }

其发送了一个空消息,主要看handle的执行接收执行和处理:

 public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_REBUILD_UI: {
                    final Context context = getActivity();
                    rebuildUI(context);
                } break;
            }

当接收到MSG_REBUILD_UI时,执行rebuildUI()这个里面的东西比较多,主要的作用就是重塑UI。这个方法稍后介绍,ok,看关键一句:
rebuildUI():

List<DashboardCategory> categories =
                ((SettingsActivity) context).getDashboardCategories(true);

这句为一个列表保存数据,ok,直接进入getDashboardCategories();
这时惊喜出现了,你会发现又返回到了SettingsActivity.java;ok,接着直接看:

2.5 SettingsActivity.java

看这个getDashboardCategories:

  public List<DashboardCategory> getDashboardCategories(boolean forceRefresh) {
        if (forceRefresh || mCategories.size() == 0) {
            buildDashboardCategories(mCategories);
        }
        return mCategories;
    }

ok,直接调用的是buildDashboardCategories()方法;直接进入:
buildDashboardCategories():

 private void buildDashboardCategories(List<DashboardCategory> categories) {
        categories.clear();
        loadCategoriesFromResource(R.xml.dashboard_categories, categories);
        updateTilesList(categories);
    }

好的,现在我们发现了这个非常重要的加载函数loadCategoriesFromResource(),可以看出其加载了dashboard_categories.xml布局。ok,直接点进去看:
dashboard_categories.xml:

<dashboard-categories
        xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- WIRELESS and NETWORKS -->
    <dashboard-category
            android:id="@+id/wireless_section"
            android:title="@string/header_category_wireless_networks" >

        <!-- Wifi -->
        <dashboard-tile
                android:id="@+id/wifi_settings"
                android:title="@string/wifi_settings_title"
                android:fragment="com.android.settings.wifi.WifiSettings"
                android:icon="@drawable/ic_settings_wireless"
                />

        <!--HetComm-->
        <dashboard-tile
                android:id="@+id/hetcomm_settings"
                android:icon="@drawable/ic_settings_hetcomm"
                android:title="@string/hetcom_setting_title">
            <intent android:action="com.android.settings.HETCOMM_SETTINGS" />
        </dashboard-tile>

        <!-- Bluetooth -->
        <dashboard-tile
                android:id="@+id/bluetooth_settings"
                android:title="@string/bluetooth_settings_title"
                android:fragment="com.android.settings.bluetooth.BluetoothSettings"
                android:icon="@drawable/ic_settings_bluetooth2"
                />

      <!--^_^   test_zxr    ^_^ -->
        <dashboard-tile
                android:id="@+id/test_zxr"
                android:title="@string/test_zxr"
                android:fragment="com.android.settings.DeviceInfoSettings"
                android:icon="@drawable/ic_settings_about"
                />

    </dashboard-category>        

...

这个就是我们整体的设置布局,加载完这个xml,获取到里面的DashboardCategory。然后再把这些item加到容器中去:
ok,现在就来看rebuildUI():

        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);

                DashboardTileView tileView = new DashboardTileView(context);
                updateTileView(context, res, tile, tileView.getImageView(),
                        tileView.getTitleTextView(), tileView.getStatusTextView());

                tileView.setTile(tile);
                if(tile != null && tile.extras != null && tile.extras.containsKey(CUSTOMIZE_ITEM_INDEX)){
                    int index = tile.extras.getInt(CUSTOMIZE_ITEM_INDEX, -1);
                    categoryContent.addView(tileView, index);
                } else {
                    categoryContent.addView(tileView);
                }
            }

            // Add the category
            mDashboard.addView(categoryView);

这段代码逻辑很简单,遍历categories这个列表来获取DashboardCategory对象,将所有DashboardCategory对象和DashboardCategory对象中的DashboardTile对象转化为视图对象并添加到主视图对象mDashboard中。ok。加载完view,settings的布局就ok了,接下来就是其实现了。

2.6 DashboardTileView.java

现在来看在View中的实现:打开DashboardTileView.java,可以看到其构造:

  public DashboardTileView(Context context, AttributeSet attrs) {
        super(context, attrs);

        final View view = LayoutInflater.from(context).inflate(R.layout.dashboard_tile, this);

        mImageView = (ImageView) view.findViewById(R.id.icon);
        mTitleTextView = (TextView) view.findViewById(R.id.title);
        mStatusTextView = (TextView) view.findViewById(R.id.status);
        mDivider = view.findViewById(R.id.tile_divider);

        setOnClickListener(this);
        setBackgroundResource(R.drawable.dashboard_tile_background);
        setFocusable(true);
    }

可以看到其定义了一些控件用来找到我们的图标,标题等等的定义,之后定义一些函数用来返回控件,好了,现在直接看onClick()方法:

   public void onClick(View v) {
        if (mTile.fragment != null) {
            Utils.startWithFragment(getContext(), mTile.fragment, mTile.fragmentArguments, null, 0,
                    mTile.titleRes, mTile.getTitle(getResources()));
        } else if (mTile.intent != null) {
            getContext().startActivity(mTile.intent);
        }
    }

可以看到其点击事件中启用了一个方法,从表面看来应该是启动一个Fragment,ok,那么我么继续跟进这个方法:

 /**
     * Start a new instance of the activity, showing only the given fragment.
     * When launched in this mode, the given preference fragment will be instantiated and fill the
     * entire activity.
     *
     * @param context The context.
     * @param fragmentName The name of the fragment to display.
     * @param args Optional arguments to supply to the fragment.
     * @param resultTo Option fragment that should receive the result of the activity launch.
     * @param resultRequestCode If resultTo is non-null, this is the request code in which
     *                          to report the result.
     * @param titleResId resource id for the String to display for the title of this set
     *                   of preferences.
     * @param title String to display for the title of this set of preferences.
     */
    public static void startWithFragment(Context context, String fragmentName, Bundle args,
            Fragment resultTo, int resultRequestCode, int titleResId,
            CharSequence title) {
        startWithFragment(context, fragmentName, args, resultTo, resultRequestCode,
                null /* titleResPackageName */, titleResId, title, false /* not a shortcut */);
    }

在这里我特意贴了注释,从源码的注释可以看出其实直接启动一个Activity,好的,我们现在继续跟进:

 public static void startWithFragment(Context context, String fragmentName, Bundle args,
            Fragment resultTo, int resultRequestCode, String titleResPackageName, int titleResId,
            CharSequence title, boolean isShortcut) {
        Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResPackageName,
                titleResId, title, isShortcut);
        if (resultTo == null) {
            context.startActivity(intent);
        } else {
            resultTo.startActivityForResult(intent, resultRequestCode);
        }
    }

很明显可以看出intent链接到onBuildStartFragmentIntent,现在跳转到onBuildStartFragmentIntent:

  public static Intent onBuildStartFragmentIntent(Context context, String fragmentName,
            Bundle args, String titleResPackageName, int titleResId, CharSequence title,
            boolean isShortcut) {
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setClass(context, SubSettings.class);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, fragmentName);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RES_PACKAGE_NAME,
                titleResPackageName);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, titleResId);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, title);
        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, isShortcut);
        return intent;
    }

看到它的启动信息,直接链接到SubSettings这个类,并且附带这个fragment的fragmentName,args,titleResPackageName,等信息直接跳转。ok,既然直接启动SubSettings,那么我们就看这个类:

public class SubSettings extends SettingsActivity

可以看到这个类继承了SettingsActivity,所以我们又得回到SettingsActivity的onCreate()方法,^_^。

2.7 SettingsActivity.java

OK,既然有跳到了SettingsActivity,那么我们就继续跟进^_^:
又到了这个标志位,是不是觉得非常熟悉呢:

mIsShowingDashboard = className.equals(Settings.class.getName());

哈哈,对了前面因为我们是第一次创建所以这个值返回的是true,而现在呢?我们的className已经改变了,所以返回false,ok,接着看:

setContentView(mIsShowingDashboard ? R.layout.settings_main_dashboard : R.layout.settings_main_prefs);

现在加载的却是settings_main_prefs这个界面。

final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);

获取传递过来的FRAGMENTname,接着:

switchToFragment(initialFragmentName, initialArguments, true, false,mInitialTitleResId, mInitialTitle, false);

又开始了我们的Fragment切换^_^。至此应该一轮结束。

3.小结

本文是我自己分析android源码的第一次完整的对一个模块的分析,也参考了网上的一些文章,还有自己的一些看法,在分析的过程中发现android源码分析却是非常不容易,跳过来跳过去,若是不能及时的跳出来,很容易陷进去,在分析的过程中一定要请教交流,不然很多时候却是很费解。这是我的源码分析的第一步,后续会持续跟新其他完整的模块,非常希望感兴趣的同学能够多多交流批评指正。

以上是关于Android5.1源码分析系列Settings源码分析的主要内容,如果未能解决你的问题,请参考以下文章

openGauss数据库源码解析系列文章—— SQL引擎源解析

Django的settings源码分析

Settings5.1源码分析

Android 5.0 Settings源码简要分析

Django的settings文件部分源码分析

Android4.42-Settings源码分析之蓝牙模块Bluetooth(上)