Android源码解析之——Settings

一、背景

公司安排我对Android P Settings的源码进行修改,屏蔽掉不需要的设置选项,添加我们产品所特有的设置选项。

二、准备工作

工欲善其事必先利其器,准备工作是不能缺少的,而一个好的IDE能帮你专注本职工作。这里我推荐使用Android Studio,当然,eclipse这种上个世纪的IDE也可以。

1、获得源码

首先,想方设法从你Android P源码里copy出Settings和settingslib这两部分源码,路径分别是platform/packages/apps/Settings和frameworks/base/packages/SettingsLib。

2、创建项目

然后用Android Studio新建个一个项目名为Settings,包名为com.android.settings的项目,再删除app/src/main中AndroidManifest.xml文件、res目录、以及java目录下的子目录(java目录保留),然后将你Settings的相应目录及文件copy进来。

3、新建Module

File->New->New Module->Android Library,创建一个名为settingslib,包名为:com.android.settingslib的module。后面操作与Settings操作类似,删除自动生成的目录,再将settingslib的相应文件及目录copy进去。

4、添加依赖,并设置用java1.8编译。

如图所示: 完成

三、总览流程

Android 9的Settings与Android 5相比,在架构上复杂了很多,其中变化最大的是一级设置列表, category解析流程

四、分析

第一步:分析AndroidManifest文件,可以看到Settings.java是应用的入口类。 打开Settings.java,里面是大量的子Activity,他们与Settings类似,都继承于SettingsActivity

打开SettingsActivity,它继承于SettingsDrawerActivity,而SettingsDrawerActivity内容不多,主要功能是 1、注册对应用安装、卸载、更新的事件广播,同时更新一级设置列表。

2、重写setContentView,让子类调用的setContentView时,将子类布局填充到自己content_frame中。

3、当接受到广播时,调用CategoriesUpdateTask,异步更新sTileBlacklist列表,并回调监听事件。

我们再回到SettingsActivity,从onCreate开始分析

java 复制代码
    @Override
    protected void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        Log.d(LOG_TAG, "Starting onCreate");
        long startTime = System.currentTimeMillis();

        //获得FeatureFactoryImpl
        final FeatureFactory factory = FeatureFactory.getFactory(this);

        //获得DashboardFeatureProviderImpl
        mDashboardFeatureProvider = factory.getDashboardFeatureProvider(this);

        // Should happen before any call to getIntent()
        //将子类在Manifest中com.android.settings.FRAGMENT_CLASS的值赋值给mFragmentClass
        getMetaData();

        //注意:这里getIntent()是自己重写过的方法,并不是Activity自带的getIntent()
        final Intent intent = getIntent();
        if (intent.hasExtra(EXTRA_UI_OPTIONS)) {
            getWindow().setUiOptions(intent.getIntExtra(EXTRA_UI_OPTIONS, 0));
        }

        // Getting Intent properties can only be done after the super.onCreate(...)
        //:settings:show_fragment
        final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);

        final ComponentName cn = intent.getComponent();
        final String className = cn.getClassName();//子类名

        //如果是Settings界面,即主界面
        mIsShowingDashboard = className.equals(Settings.class.getName());

        // 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 = this instanceof SubSettings ||
                intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false);

        // If this is a sub settings, then apply the SubSettings Theme for the ActionBar content
        // insets
        if (isSubSettings) {
            setTheme(R.style.Theme_SubSettings);
        }

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

        //内容区域View
        mContent = findViewById(R.id.main_content);

        getFragmentManager().addOnBackStackChangedListener(this);

        if (savedState != null) {
            // We are restarting from a previous saved state; used that to initialize, instead
            // of starting fresh.
            setTitleFromIntent(intent);

            ArrayList<DashboardCategory> categories =
                    savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
            if (categories != null) {
                mCategories.clear();
                mCategories.addAll(categories);
                setTitleFromBackStack();
            }
        } else {
            //运行fragment
            launchSettingFragment(initialFragmentName, isSubSettings, intent);
        }

//UI相关操作,略

最后几句从名字可以看出,通过调用launchSettingFragment来显示具体的界面,而第一个参数initialFragmentName,是通过getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT)获得,这里不经要问:EXTRA_SHOW_FRAGMENT的值从何而来?不可能每次都在startActivity中添加吧,这里,有一个巨大的坑:getIntent() ,它被重写了,以至于我刚开始看这里还有点懵逼。下面就是getIntent()的源码:

java 复制代码
    @Override
    public Intent getIntent() {
        Intent superIntent = super.getIntent();
        String startingFragment = getStartingFragmentClass(superIntent);//获得子类名
        // This is called from super.onCreate, isMultiPane() is not yet reliable
        // Do not use onIsHidingHeaders either, which relies itself on this method
        if (startingFragment != null) {
            Intent modIntent = new Intent(superIntent);
            modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
            Bundle args = superIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
            if (args != null) {
                args = new Bundle(args);
            } else {
                args = new Bundle();
            }
            args.putParcelable("intent", superIntent);
            modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
            return modIntent;
        }
        return superIntent;
    }

    /**
     * Checks if the component name in the intent is different from the Settings class and
     * returns the class name to load as a fragment.
     */
    private String getStartingFragmentClass(Intent intent) {
        if (mFragmentClass != null) return mFragmentClass;

        //其实就是获取子类名称
        String intentClass = intent.getComponent().getClassName();
        if (intentClass.equals(getClass().getName())) return null;//如果是当前父类名

        if ("com.android.settings.RunningServices".equals(intentClass)
                || "com.android.settings.applications.StorageUse".equals(intentClass)) {
            // Old names of manage apps.
            intentClass = ManageApplications.class.getName();
        }

        return intentClass;
    }

上面代码其实说白了,就是对super.getIntent()得到的intent进行封装,并将要跳转的Activity值赋值给EXTRA_SHOW_FRAGMENT。所以,onCreate中initialFragmentName值,其实就是每次跳转的目标Activity类名(是SettingsActivity的子类)。

接下来我们开始分析launchSettingFragment函数了。

java 复制代码
    @VisibleForTesting
    void launchSettingFragment(String initialFragmentName, boolean isSubSettings, Intent intent) {
        if (!mIsShowingDashboard && initialFragmentName != null) {//不是根界面,且指定了要显示的Fragment
            setTitleFromIntent(intent);

            Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
            switchToFragment(initialFragmentName, initialArguments, true, false,
                    mInitialTitleResId, mInitialTitle, false);
        } else {//是跟界面,或者没指定要显示的Fragment
            // Show search icon as up affordance if we are displaying the main Dashboard
            mInitialTitleResId = R.string.dashboard_title;

            switchToFragment(DashboardSummary.class.getName(), null /* args */, false, false,
                    mInitialTitleResId, mInitialTitle, false);
        }
    }

这里对两种情况进行分开处理,如果是根界面,就显示DashboardSummary,否则显示指定的Fragment。他们都通过switchToFragment进行实现。

java 复制代码
    /**
     * Switch to a specific Fragment with taking care of validation, Title and BackStack
     */
    private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
            boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) {
        Log.d(LOG_TAG, "Switching to fragment " + fragmentName);
        if (validate && !isValidFragment(fragmentName)) {//验证所需显示的fragmentName是否在SettingsGateway里注册
            throw new IllegalArgumentException("Invalid fragment for this activity: "
                    + fragmentName);
        }
        Fragment f = Fragment.instantiate(this, fragmentName, args);
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        transaction.replace(R.id.main_content, f);
        if (withTransition) {
            TransitionManager.beginDelayedTransition(mContent);
        }
        if (addToBackStack) {
            transaction.addToBackStack(SettingsActivity.BACK_STACK_PREFS);
        }
        if (titleResId > 0) {
            transaction.setBreadCrumbTitle(titleResId);
        } else if (title != null) {
            transaction.setBreadCrumbTitle(title);
        }
        transaction.commitAllowingStateLoss();
        getFragmentManager().executePendingTransactions();
        Log.d(LOG_TAG, "Executed frag manager pendingTransactions");
        return f;
    }

switchToFragment就简简单单将fragment填充到main_content中。

到目前为止,我们已经将大概的布局填充过程介绍完毕,现在我们开始以DashboardSummary为例(即一级设置列表),分析其数据是如何获得的。首先看onCreateView方法:

java 复制代码
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
        //、、、、、、
        //前面只是对布局中的RecyclerView设置参数等等,略掉
        mAdapter = new DashboardAdapter(getContext(), bundle,
                mConditionManager.getConditions(), mSuggestionControllerMixin, getLifecycle());
        mDashboard.setAdapter(mAdapter);
        mSummaryLoader.setSummaryConsumer(mAdapter);
        ActionBarShadowController.attachToRecyclerView(
                getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard);
        rebuildUI();//更新列表
        //、、、、、、
        return root;
    }

    @VisibleForTesting
    void rebuildUI() {
        ThreadUtils.postOnBackgroundThread(() -> updateCategory());
    }

    @WorkerThread
    void updateCategory() {
        final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(
                CategoryKey.CATEGORY_HOMEPAGE);
      //......................后面设置category...............
    }

通过mDashboardFeatureProvider.getTilesForCategory()方法获得Category,其参数CATEGORY_HOMEPAGE,实际是com.android.settings.category.ia.homepage ,这里可以看出,一级设置类别,是通过查找AndroidManifest文件里com.android.settings.category.ia.homepage属性获得。 而getTilesForCategory内部通过CategoryManager的getTilesByCategory方法实现。

java 复制代码
public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {
        return getTilesByCategory(context, categoryKey, TileUtils.SETTING_PKG);
    }

    public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey,
            String settingPkg) {
        tryInitCategories(context, settingPkg);
        //由此可以看出,上一行代码是获取Categories,并保存到mCategoryByKeyMap中
        return mCategoryByKeyMap.get(categoryKey);
    }

    private synchronized void tryInitCategories(Context context, String settingPkg) {
        // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange
        // happens.
        tryInitCategories(context, false /* forceClearCache */, settingPkg);
    }

    private synchronized void tryInitCategories(Context context, boolean forceClearCache,
            String settingPkg) {
        if (mCategories == null) {
            if (forceClearCache) {
                mTileByComponentCache.clear();
            }
            mCategoryByKeyMap.clear();
//这里正式获得Categories
            mCategories = TileUtils.getCategories(context, mTileByComponentCache,
                    false /* categoryDefinedInManifest */, mExtraAction, settingPkg);
            for (DashboardCategory category : mCategories) {
                mCategoryByKeyMap.put(category.key, category);
            }
            backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
            sortCategories(context, mCategoryByKeyMap);
            filterDuplicateTiles(mCategoryByKeyMap);
        }
    }

这里可以发现整个方法,是通过TileUtils.getCategories获得Categorie。

java 复制代码
    public static List<DashboardCategory> getCategories(Context context,
            Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest,
            String extraAction, String settingPkg) {
        final long startTime = System.currentTimeMillis();
        boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
                != 0;
        ArrayList<Tile> tiles = new ArrayList<>();
        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
        for (UserHandle user : userManager.getUserProfiles()) {
            // TODO: Needs much optimization, too many PM queries going on here.
            if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
                // Only add Settings for this user.
                //查找 com.android.settings.action.SETTINGS
                getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true,
                        settingPkg);
                getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
                        OPERATOR_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
                getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
                        MANUFACTURER_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
            }
            if (setup) {
                getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false,
                        settingPkg);
                if (!categoryDefinedInManifest) {
                    getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false,
                            settingPkg);
                    if (extraAction != null) {
                        getTilesForAction(context, user, extraAction, cache, null, tiles, false,
                                settingPkg);
                    }
                }
            }
        }

        HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
        for (Tile tile : tiles) {
            DashboardCategory category = categoryMap.get(tile.category);
            if (category == null) {
                category = createCategory(context, tile.category, categoryDefinedInManifest);
                if (category == null) {
                    Log.w(LOG_TAG, "Couldn't find category " + tile.category);
                    continue;
                }
                categoryMap.put(category.key, category);
            }
            category.addTile(tile);
        }
        ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
        for (DashboardCategory category : categories) {
            category.sortTiles();
        }
        Collections.sort(categories, CATEGORY_COMPARATOR);
        if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took "
                + (System.currentTimeMillis() - startTime) + " ms");
        return categories;
    }

五、一级设置列表小结

到此为止,我们可以清楚的知道一级菜单是如何解析出来的,首先,TileUtils对AndroidManifest文件进行解析,获取所有包含com.android.settings.action.SETTINGS 的Activity,然后在DashboardSummary类中,通过DashboardFeatureProvider的getTilesForCategory方法,获得所有包含com.android.settings.category.ia.homepage的类,再将其填充到列表。

这里我们有其它一些细节没有进行介绍: 1、所有继承于SettingsActivity并需要在一级设置列表显示的Activity,都需要在SettingsGateway的SETTINGS_FOR_RESTRICTED列表中声明 2、所有需要在SettingsActivity或其子Activity中设置的Fragment,都必须要在SettingsGateway的ENTRY_FRAGMENTS列表中声明,否则会抛出 Invalid fragment for this activity:xxxx异常

六、二级设置列表分析

这里内容比较简单,但内容也是最多的,其主要内容包括以下几种情况: 1、二级设置是列表 2、二级设置是界面 3、三级设置是界面 情况1、也是看到最多的情况,比如**网络和互联网界面

相关推荐
落落落sss17 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.38 分钟前
数据库语句优化
android·数据库·adb
GEEKVIP3 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20055 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6895 小时前
Android广播
android·java·开发语言
与衫6 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了12 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵13 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru18 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng19 小时前
android 原生加载pdf
android·pdf