前景提要:根据现阶段业务需求,大致分析安卓设置页面的图标是如何生成的,由于本人源码阅读经验缺乏,分析过程仅供参考。与以往的app开发分析具体页面不同,adb dumpsys是无法直接拿到对应页面的,因为其底层多是由Fragment构建而成,遂以断点调试为主要手段。
代码地址:http://androidxref.com/9.0.0_r3/xref/packages/apps/Settings/src/com/android/settings/dashboard/
概括分析
-
找相关类
通过断点调试,发现了点击进入设置页面时主要与三个关键的类有关,分别是DashboardData、DashboardAdapter、DashboardSummary。通过观察它们的大概方法类,可以得出上图所示的关系。设置页面相当于一个大型的RecycleView,DashboardData封装了相关数据,通过使用建造者模式让其他类更改其内部不同且繁杂的对象时更加便利,同时将构造委托给Builder对象,实现更加灵活的构造方式。DashboardAdapter中实现根据不同的组件生成对应的ViewHodler,并绑定对应的事件。DashboardSummary则是数据展示的关键类,在onCreateView中,它设置相关样式、绑定Adapter。要找到页面相关的节点,在onCreateView方法上加断点试试。
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { long startTime = System.currentTimeMillis(); final View root = inflater.inflate(R.layout.dashboard, container, false); mDashboard = root.findViewById(R.id.dashboard_container); mLayoutManager = new LinearLayoutManager(getContext()); mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); if (bundle != null) { int scrollPosition = bundle.getInt(STATE_SCROLL_POSITION); mLayoutManager.scrollToPosition(scrollPosition); } mDashboard.addItemDecoration(new ListDiv(getContext())); mDashboard.setLayoutManager(mLayoutManager); mDashboard.setHasFixedSize(true); mDashboard.setListener(this); mDashboard.setDetachListener(this); mDashboard.setItemAnimator(new DashboardItemAnimator()); 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(); if (DEBUG_TIMING) { Log.d(TAG, "onCreateView took " + (System.currentTimeMillis() - startTime) + " ms"); } return root; }
-
找到关键实体类
第一步中找关键类帮我们确定了该页面所在的位置,在第二步就要找我们需要的这些设置内容到底是包含在哪些实体类当中,是怎么被封装渲染到页面的。上述得知,数据的封装是在DashboardData中,观察它的构造方法。
private DashboardData(Builder builder) { mCategory = builder.mCategory; mConditions = builder.mConditions; mSuggestions = builder.mSuggestions; mConditionExpanded = builder.mConditionExpanded; mItems = new ArrayList<>(); buildItemsData(); }
可以发现有三种主要实体类,Category、Condition、Suggestion。依旧迷惑,进入buildItemsData()瞅瞅,主要的一个执行方法是addToItemList,看起来是要将这些类都加到一个叫Item的list数组中,而且还带了R.layout.***组件,这里是关键,逐一看看哪一个比较契合我们在设置页面看到的布局格式,我的发现是R.layout.dashboard_tile,这个组件的布局和settings里面tile的排列方法一模一样,由此得知,是Category封装了我们的设置节点。
183 private void buildItemsData() { 184 final List<Condition> conditions = getConditionsToShow(mConditions); 185 final boolean hasConditions = sizeOf(conditions) > 0; 186 187 final List<Suggestion> suggestions = getSuggestionsToShow(mSuggestions); 188 final boolean hasSuggestions = sizeOf(suggestions) > 0; 189 190 /* Suggestion container. This is the card view that contains the list of suggestions. 191 * This will be added whenever the suggestion list is not empty */ 192 addToItemList(suggestions, R.layout.suggestion_container, 193 STABLE_ID_SUGGESTION_CONTAINER, hasSuggestions); 194 195 /* Divider between suggestion and conditions if both are present. */ 196 addToItemList(null /* item */, R.layout.horizontal_divider, 197 STABLE_ID_SUGGESTION_CONDITION_DIVIDER, hasSuggestions && hasConditions); 198 199 /* Condition header. This will be present when there is condition and it is collapsed */ 200 addToItemList(new ConditionHeaderData(conditions), 201 R.layout.condition_header, 202 STABLE_ID_CONDITION_HEADER, hasConditions && !mConditionExpanded); 203 204 /* Condition container. This is the card view that contains the list of conditions. 205 * This will be added whenever the condition list is not empty and expanded */ 206 addToItemList(conditions, R.layout.condition_container, 207 STABLE_ID_CONDITION_CONTAINER, hasConditions && mConditionExpanded); 208 209 /* Condition footer. This will be present when there is condition and it is expanded */ 210 addToItemList(null /* item */, R.layout.condition_footer, 211 STABLE_ID_CONDITION_FOOTER, hasConditions && mConditionExpanded); 212 213 if (mCategory != null) { 214 final List<Tile> tiles = mCategory.getTiles(); 215 for (int i = 0; i < tiles.size(); i++) { 216 final Tile tile = tiles.get(i); 217 addToItemList(tile, R.layout.dashboard_tile, Objects.hash(tile.title), 218 true /* add */); 219 } 220 } 221 }
具体分析
确定了封装图标列表的实体类,那么接下来应该顺藤摸瓜直接就一个按图索骥,看到底是哪里给它传入了值。还是在DashboardSummary的onCreateView中进行断点调试,省去样式设置的代码,找寻有嫌疑的构造方法。有一个地方有嫌疑:
210 mAdapter = new DashboardAdapter(getContext(), bundle,
211 mConditionManager.getConditions(), mSuggestionControllerMixin, getLifecycle());
212 mDashboard.setAdapter(mAdapter);
可能是在创建Adapter的时候,就将数据生成并传入了?在传统app开发里面,我们都这么干。在102行,我们可以看到他直接就从Bundle中取出了category,这么顺畅不免让人质疑,资源来自缓存?缓存在DashboardAdapter中也只是来自Dashboard.getCategory(),可见这一块并不是数据的产生地,不过是一个页面恢复时重现数据的逻辑而已,pass!
85 public DashboardAdapter(Context context, Bundle savedInstanceState,
86 List<Condition> conditions, SuggestionControllerMixin suggestionControllerMixin,
87 Lifecycle lifecycle) {
88
89 DashboardCategory category = null;
90 boolean conditionExpanded = false;
91
92 mContext = context;
93 final FeatureFactory factory = FeatureFactory.getFactory(context);
94 mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
95 mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
96 mCache = new IconCache(context);
97 mSuggestionAdapter = new SuggestionAdapter(mContext, suggestionControllerMixin,
98 savedInstanceState, this /* callback */, lifecycle);
99
100 setHasStableIds(true);
101
102 if (savedInstanceState != null) {
103 category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST);
104 conditionExpanded = savedInstanceState.getBoolean(
105 STATE_CONDITION_EXPANDED, conditionExpanded);
106 }
107
108 if (lifecycle != null) {
109 lifecycle.addObserver(this);
110 }
111
112 mDashboardData = new DashboardData.Builder()
113 .setConditions(conditions)
114 .setSuggestions(mSuggestionAdapter.getSuggestions())
115 .setCategory(category)
116 .setConditionExpanded(conditionExpanded)
117 .build();
118 }
继续Debug断点调试,发现了最后一个处理逻辑 rebuildUI() 开了一个工作线程,执行updateCategory(),看起来有戏,继续跟进。由于嵌套极深,遂瞄准主要逻辑一下下深入。
为了看起来更为清晰,现将嵌套关系转换为流程图:
@VisibleForTesting
void rebuildUI() {
ThreadUtils.postOnBackgroundThread(() -> updateCategory());
}
@WorkerThread
void updateCategory() {
final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(
CategoryKey.CATEGORY_HOMEPAGE);
mSummaryLoader.updateSummaryToCache(category);
mStagingCategory = category;
if (mSuggestionControllerMixin == null) {
ThreadUtils.postOnMainThread(() -> mAdapter.setCategory(mStagingCategory));
return;
}
if (mSuggestionControllerMixin.isSuggestionLoaded()) {
Log.d(TAG, "Suggestion has loaded, setting suggestion/category");
ThreadUtils.postOnMainThread(() -> {
if (mStagingSuggestions != null) {
mAdapter.setSuggestions(mStagingSuggestions);
}
mAdapter.setCategory(mStagingCategory);
});
} else {
Log.d(TAG, "Suggestion NOT loaded, delaying setCategory by " + MAX_WAIT_MILLIS + "ms");
mHandler.postDelayed(() -> mAdapter.setCategory(mStagingCategory), MAX_WAIT_MILLIS);
}
}
@Override
public DashboardCategory getTilesForCategory(String key) {
return mCategoryManager.getTilesByCategory(mContext, key);
}
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);
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();
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);
}
}
接下来就是重量级,经过艰辛的挖掘,找到了获取Categories的方法,首先该方法的大致逻辑是先获取相关tiles,然后根据tiles创建对应的category,别忘了它的注释,非常的巧妙,categoryDefinedInManifest,说明它极其可能是通过扫描Manifest从而创建的对象。带着这个疑问,观察一下getTilesForAction()方法,发现第三个参数似曾相识,SETTINGS_ACTION,其值为 "com.android.settings.action.SETTINGS",在Manifest里面每个在设置页面展示的图标都会包含这个参数,同时包含了决定其位置的优先级。
/**
* Build a list of DashboardCategory.
*
* @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
* represent this category (eg: .Settings$DeviceSettings)
* @param extraAction additional intent filter action to be usetileutild to build the dashboard
* categories
*/
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.
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);
Log.d(LOG_TAG, "Couldn't find category " + tile.category);
}
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;
}
接下来就验证到底是不是从Manifest中取的数据,进入getTilesForAction方法,接下来又是一层层嵌套,铁头挖就完事。
private static void getTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
String settingPkg)
{
getTilesForAction(context, user, action, addedCache, defaultCategory, outTiles,
requireSettings, requireSettings, settingPkg);
}
private static void getTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
boolean usePriority, String settingPkg)
{
Intent intent = new Intent(action);
if (requireSettings)
{
intent.setPackage(settingPkg);
}
getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
usePriority, true, true);
}
public static void getTilesForIntent(
Context context, UserHandle user, Intent intent,
Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon)
{
getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
usePriority, checkCategory, forceTintExternalIcon, false /* shouldUpdateTiles */);
}
public static void getTilesForIntent(
Context context, UserHandle user, Intent intent,
Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon,
boolean shouldUpdateTiles)
{
PackageManager pm = context.getPackageManager();
List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
Map<String, IContentProvider> providerMap = new HashMap<>();
for (ResolveInfo resolved : results)
{
if (!resolved.system)
{
// Do not allow any app to add to settings, only system ones.
continue;
}
ActivityInfo activityInfo = resolved.activityInfo;
Bundle metaData = activityInfo.metaData;
String categoryKey = defaultCategory;
// Load category
if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY))
&& categoryKey == null)
{
Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
+ intent + " missing metadata "
+ (metaData == null ? "" : EXTRA_CATEGORY_KEY));
continue;
}
else
{
categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
Log.d(LOG_TAG, "find " + categoryKey + " " + metaData.get("com.android.settings.FRAGMENT_CLASS") + " pri " + resolved.priority
+ " for " + resolved.activityInfo.name + " meta pri " + metaData.getInt(EXTRA_PRI_KEY, 0)
+ "meta com.android.settings.summary_settings " + metaData.getString("com.android.settings.summary_settings"));
}
Pair<String, String> key = new Pair<String, String>(activityInfo.packageName,
activityInfo.name);
Tile tile = addedCache.get(key);
if (tile == null)
{
tile = new Tile();
tile.intent = new Intent().setClassName(
activityInfo.packageName, activityInfo.name);
tile.category = categoryKey;
if(resolved.system)
{
tile.priority = usePriority ? (resolved.priority > 0 ? resolved.priority + 100 : resolved.priority): metaData.getInt(EXTRA_PRI_KEY, 0);
}
else
{
tile.priority = metaData.getInt(EXTRA_PRI_KEY, 0);
}
tile.metaData = activityInfo.metaData;
updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
pm, providerMap, forceTintExternalIcon);
if (DEBUG)
{
Log.d(LOG_TAG, "Adding tile " + tile.title + " pri " + tile.priority + " usePriority " + usePriority);
}
addedCache.put(key, tile);
}
else if (shouldUpdateTiles)
{
updateSummaryAndTitle(context, providerMap, tile);
}
if (!tile.userHandle.contains(user))
{
tile.userHandle.add(user);
}
if (!outTiles.contains(tile))
{
outTiles.add(tile);
}
}
}
果然,找到了PackageManager!!!根据我们传入的SETTINGS_ACTION查找包含改字符串的Activities,从第11行开始遍历这些包含的Activity,判断是否是系统软件,提取Activity的信息和Manifest中定义的变量。接着便是将这些信息赋值给tile,然后将tile加入到上面我形容为重量级的方法getCategories中的titles中,然后顺理成章的将这些titles遍历转换为category,再根据metaData中的Priority进行排序,如此便返回了categories。
PackageManager pm = context.getPackageManager();
List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
最后由DashboardSummary中的update方法,执行对应的mAdapter.setCategoty完成赋值,如此才算是完完全全的将所有列表中的图标载入到UI线程,可谓不容易啊。
@WorkerThread
void updateCategory() {
final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(
CategoryKey.CATEGORY_HOMEPAGE);
mSummaryLoader.updateSummaryToCache(category);
mStagingCategory = category;
if (mSuggestionControllerMixin == null) {
ThreadUtils.postOnMainThread(() -> mAdapter.setCategory(mStagingCategory));
return;
}
if (mSuggestionControllerMixin.isSuggestionLoaded()) {
Log.d(TAG, "Suggestion has loaded, setting suggestion/category");
ThreadUtils.postOnMainThread(() -> {
if (mStagingSuggestions != null) {
mAdapter.setSuggestions(mStagingSuggestions);
}
mAdapter.setCategory(mStagingCategory);
});
} else {
Log.d(TAG, "Suggestion NOT loaded, delaying setCategory by " + MAX_WAIT_MILLIS + "ms");
mHandler.postDelayed(() -> mAdapter.setCategory(mStagingCategory), MAX_WAIT_MILLIS);
}
}
难点总结
- 是怎么在众多的代码中锁定概括分析中的三个类为主要相关类?前人指路、正确的命名风格、通用的代码设计风格。
- 怎么得知Category为这些图标的实体类?这个真的要好好思考,debug或许是一种手段,但是思想依然离不开生命周期。
- 界面数据的产生与封装,逻辑极其复杂,业务与逻辑杂糅?UI线程不IO数据,那可以想想有没有子线程在哪里偷偷执行。