从android5.1 开始,google为用户提供了一种很方便的搜索功能,用户可以很方便的在settings中搜索setting里或者系统其他配置了指定继承自SearchIndexablesProvider的应用的设置选项,这样做极大的提高了搜索效率。
本文依据Android8.1为基础,粗线条梳理一下快速搜索的索引逻辑。
SettingsActivity.java,此函数在settings启动时执行,会对Index进行初始化,初始化的过程后文再分析:
if (mIsShowingDashboard|| mIsMainScreen) {
// Run the Index update only if we have some space
if (!Utils.isLowStorage(this)) {
long indexStartTime = System.currentTimeMillis();
Index.getInstance(getApplicationContext()).update();
if (DEBUG_TIMING) Log.d(LOG_TAG, "Index.update() took "
+ (System.currentTimeMillis() - indexStartTime) + " ms");
} else {
Log.w(LOG_TAG, "Cannot update the Indexer as we are running low on storage space!");
}
}
而且每次语言配置发生变化的时候也会重新初始化:
/**
* 切换当前用户或者当前语言发生变化被回调,系统为每个用户维护一个单独的search_index.db
*
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Index.getInstance(this).update();
}
接下来分析初始化的过程:
首先会创建单例Index,然后调用update():
public void update() {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
/**
* 查找系统中所有的配置了"android.content.action.SEARCH_INDEXABLES_PROVIDER"的Provider
*/
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
List<ResolveInfo> list =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final int size = list.size();
for (int n = 0; n < size; n++) {
final ResolveInfo info = list.get(n);
if (!isWellKnownProvider(info)) {
continue;
}
final String authority = info.providerInfo.authority;
final String packageName = info.providerInfo.packageName;
addIndexablesFromRemoteProvider(packageName, authority); //1
addNonIndexablesKeysFromRemoteProvider(packageName, authority);
}
mDataToProcess.fullIndex = true;
/**
* 上面的addIndexablesFromRemoteProvider会添加设置项到内存中的一个mDataToProcess对象里,
* updateInternal将该对象更新到数据库中
*/
updateInternal();
}
});
}
图一
系统中配置过PROVIDER_INTERFACE不止settings一处,因此返回的是一个List<ResolveInfo>数组,针对每个元素去执行1:
private boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
LogUtils.printIndexLog("addIndexablesFromRemoteProvider 2222");
try {
/**
* rank是按照指定算法计算出的一个值,用来搜索的时候展示给用户的优先级
*/
final int baseRank = Ranking.getBaseRankForAuthority(authority);
/**
* mBaseAuthority是com.android.settings,authority是其他APP的包名
*/
final Context context = mBaseAuthority.equals(authority) ?
mContext : mContext.createPackageContext(packageName, 0);
/**
* 构建搜索URI,根据authority来构建,对于settings,构建结果是
* content://com.android.settings/settings/indexables_xml_res
*/
final Uri uriForResources = buildUriForXmlResources(authority);
/**
* 两种添加到数据库的方式,以此为例
*/
addIndexablesForXmlResourceUri(context, packageName, uriForResources,
SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS, baseRank);
final Uri uriForRawData = buildUriForRawData(authority);
addIndexablesForRawDataUri(context, packageName, uriForRawData,
SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank);
return true;
} catch (PackageManager.NameNotFoundException e) {
Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
+ Log.getStackTraceString(e));
return false;
}
}
上面mBaseAuthority是Index创建时传入的this,也就是com.android.settings,然后构建了uri,并执行addIndexablesForXmlResourceUri:
private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
Uri uri, String[] projection, int baseRank) {
/**
* 根据context来获取resolver
*/
final ContentResolver resolver = packageContext.getContentResolver();
/**
* 这里调用了SearchIndexablesProvider的
*/
final Cursor cursor = resolver.query(uri, projection, null, null, null); //1
if (cursor == null) {
Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
return;
}
try {
final int count = cursor.getCount();
LogUtils.printIndexLog("addIndexablesForXmlResourceUri 33333 --- count = "+count);
if (count > 0) {
/**
* 解析cursor数据,并且添加到内存UpdateData的dataToUpdate属性上, dataToUpdate属性是一个list集合
*/
while (cursor.moveToNext()) {
final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK);
final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;
final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
final String targetPackage = cursor.getString(
COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
final String targetClass = cursor.getString(
COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
SearchIndexableResource sir = new SearchIndexableResource(packageContext);
sir.rank = rank;
sir.xmlResId = xmlResId;
sir.className = className;
sir.packageName = packageName;
sir.iconResId = iconResId;
sir.intentAction = action;
sir.intentTargetPackage = targetPackage;
sir.intentTargetClass = targetClass;
/**
* 添加内存
*/
addIndexableData(sir);
}
}
} finally {
cursor.close();
}
}
图二
1处过程非常复杂,首先resolver的类型是ApplicationContentResolver,父类位于frameworks/base/core/java/android/content/ContentResolver.java下:
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable Bundle queryArgs,
@Nullable CancellationSignal cancellationSignal) {
Preconditions.checkNotNull(uri, "uri");
IContentProvider unstableProvider = acquireUnstableProvider(uri); //1
if (unstableProvider == null) {
return null;
}
IContentProvider stableProvider = null;
Cursor qCursor = null;
try {
long startTime = SystemClock.uptimeMillis();
ICancellationSignal remoteCancellationSignal = null;
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
remoteCancellationSignal = unstableProvider.createCancellationSignal();
cancellationSignal.setRemote(remoteCancellationSignal);
}
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal); //2
}
......
}
图三
1处调用了acquireUnstableProvider()函数会调到其子类也就是上面所说的ApplicationContentResolver实现:
@Override
protected IContentProvider acquireProvider(Context context, String auth) {
return mMainThread.acquireProvider(context,
ContentProvider.getAuthorityWithoutUserId(auth),
resolveUserIdFromAuthority(auth), true);
}
而最终调用的是mMainThread,也就是该activity所在的ActivityThread:
public final IContentProvider acquireProvider(
Context c, String auth, int userId, boolean stable) {
final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable); //1
if (provider != null) {
return provider;
}
// There is a possible race here. Another thread may try to acquire
// the same provider at the same time. When this happens, we want to ensure
// that the first one wins.
// Note that we cannot hold the lock while acquiring and installing the
// provider since it might take a long time to run and it could also potentially
// be re-entrant in the case where the provider is in the same process.
ContentProviderHolder holder = null;
try {
holder = ActivityManager.getService().getContentProvider(
getApplicationThread(), auth, userId, stable); //2
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
if (holder == null) {
Slog.e(TAG, "Failed to find provider info for " + auth);
return null;
}
// Install provider will increment the reference count for us, and break
// any ties in the race.
holder = installProvider(c, holder, holder.info,
true /*noisy*/, holder.noReleaseNeeded, stable);
return holder.provider;
}
此处为获取provider的核心,首先调用1直接从acquireExistingProvider这个方法其实就是根据我们传过来的名称在一个map里面找,注意此时还是在同一个进程中,如果查找失败执行2跨进程调用AMS,实际上就是从一个远程的map中查找,AMS会把所有的ContentProvider都实例化出来,并且缓存在这个map中,当然如果得到provider后还是会缓存到本地map中的,这样下次可以直接从本地取。
查找出来后,继续看图三的2,调用query()方法,这时有个问题,这个查找出来的provider是哪个类呢?对settings来说其实就是SettingsSearchIndexablesProvider.java类,其继承SearchIndexablesProvider.java,下面是后者的query():
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
switch (mMatcher.match(uri)) {
case MATCH_RES_CODE:
return queryXmlResources(null);
case MATCH_RAW_CODE:
return queryRawData(null);
case MATCH_NON_INDEXABLE_KEYS_CODE:
return queryNonIndexableKeys(null);
default:
throw new UnsupportedOperationException("Unknown Uri " + uri);
}
}
进入第一个分支,调用queryXmlResources()方法,而queryXmlResources是在子类实现的,也就是位于SettingsSearchIndexablesProvider.java中:
public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
private static final String TAG = "SettingsSearchIndexablesProvider";
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor queryXmlResources(String[] projection) {
MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
//返回setting中所有需要索引的内容
Collection<SearchIndexableResource> values = SearchIndexableResources.values(); //1
for (SearchIndexableResource val : values) {
Object[] ref = new Object[7];
ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = null; // intent action
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = null; // intent target package
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
cursor.addRow(ref);
}
return cursor;
}
.......
}
我们来看1的实现:
public static Collection<SearchIndexableResource> values() {
return sResMap.values();
}
就是返回一个sResMap,那么sResMap是什么在哪初始化呢,还是在这个类中:
static{
......
sResMap.put(AdvancedSettings.class.getName(),
new SearchIndexableResource(
Ranking.getRankForClassName(AdvancedSettings.class.getName()),
NO_DATA_RES_ID,
AdvancedSettings.class.getName(),
R.drawable.ic_settings_advanced));
sResMap.put(SomatosensoryGesture.class.getName(),
new SearchIndexableResource(
Ranking.getRankForClassName(SensoryGesture.class.getName()),
NO_DATA_RES_ID,
SomatosensoryGesture.class.getName(),
R.drawable.ic_settings_advanced));
......
}
将初始化的过程置于static代码块中,保证了sResMap不为空,这样逻辑就清楚了,在设置中实现自己的provider并且将需要索引的内容初始化在HashMap中,在初始化时将索引取出封装成一个cusor,这就是图二中标注为1的实现过程,继续来看图二,正如注释所说,解析cursor数据添加到内存中的数组,至此图一中的1注释addIndexablesFromRemoteProvider()完成,回到图一,继续来看注释2 updateInternal(),此函数用来将内存中的数据更新到数据库:
private void updateInternal() {
synchronized (mDataToProcess) {
final UpdateIndexTask task = new UpdateIndexTask();
/**
* 拷贝一个mDataToProcess对象的副本,前面将数据添加到mDataToProcess对象中
*/
UpdateData copy = mDataToProcess.copy();
/**
* 执行UpdateIndexTask,UpdateIndexTask会将copy对象保存到数据库里
*/
task.execute(copy);
mDataToProcess.clear();
}
}
UpdateIndexTask继承AsyncTask:
@Override
protected Void doInBackground(UpdateData... params) {
try {
final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate;
final List<SearchIndexableData> dataToDelete = params[0].dataToDelete;
final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys;
final boolean forceUpdate = params[0].forceUpdate;
final boolean fullIndex = params[0].fullIndex;
final SQLiteDatabase database = getWritableDatabase();
if (database == null) {
Log.e(LOG_TAG, "Cannot update Index as I cannot get a writable database");
return null;
}
final String localeStr = Locale.getDefault().toString();
try {
database.beginTransaction();
if (dataToDelete.size() > 0) {
processDataToDelete(database, localeStr, dataToDelete);
}
if (dataToUpdate.size() > 0) {
/**
* 插入或者更新当前数据库内容
*/
processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys,
forceUpdate); //1
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
if (fullIndex) {
IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
}
} catch (SQLiteFullException e) {
Log.e(LOG_TAG, "Unable to index search, out of space", e);
} catch(Exception ex){
Log.e(LOG_TAG, "other error ", ex);
}
return null;
}
创建数据库,并且将需要索引的数据作为参数,继续调用注释1的processDataToUpdate():
private boolean processDataToUpdate(SQLiteDatabase database, String localeStr,
List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys,
boolean forceUpdate) {
boolean result = false;
final long current = System.currentTimeMillis();
final int count = dataToUpdate.size();
for (int n = 0; n < count; n++) {
final SearchIndexableData data = dataToUpdate.get(n);
try {
//更新每一条数据
indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys);
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot index: " + (data != null ? data.className : data)
+ " for locale: " + localeStr, e);
}
}
final long now = System.currentTimeMillis();
Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " +
(now - current) + " millis");
return result;
}
继续来看:
private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) {
if (data instanceof SearchIndexableResource) {
indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys); //1
} else if (data instanceof SearchIndexableRaw) {
indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
}
}
走1:
private void indexOneResource(SQLiteDatabase database, String localeStr,
SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource) {
if (sir == null) {
Log.e(LOG_TAG, "Cannot index a null resource!");
return;
}
final List<String> nonIndexableKeys = new ArrayList<String>();
//在前面HashMap中加入时会指定NO_DATA_RES_ID
if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) {
List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName);
if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) {
nonIndexableKeys.addAll(resNonIndxableKeys);
}
indexFromResource(sir.context, database, localeStr,
sir.xmlResId, sir.className, sir.iconResId, sir.rank,
sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass,
nonIndexableKeys);
} else {
if (TextUtils.isEmpty(sir.className)) {
Log.w(LOG_TAG, "Cannot index an empty Search Provider name!");
return;
}
/**
* 获取设置中索引的的类,例如com.android.settings.datausage.DataUsageSummary
*/
final Class<?> clazz = getIndexableClass(sir.className);
if (clazz == null) {
Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className +
"' should implement the " + Indexable.class.getName() + " interface!");
return;
}
// Will be non null only for a Local provider implementing a
// SEARCH_INDEX_DATA_PROVIDER field
/**
* 若类没有继承Indexable接口并且没有实现SEARCH_INDEX_DATA_PROVIDER则返回为null
*/
final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz);
if (provider != null) {
List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context);
if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) {
nonIndexableKeys.addAll(providerNonIndexableKeys);
}
indexFromProvider(mContext, database, localeStr, provider, sir.className,
sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys);
}
}
}
继续调用indexFromProvider()-indexFromResource(),就是一个层层剥离的状态,获取被初始化的hashmap中的每一个元素,获取每一个元素的xml,再来看:
private void indexFromResource(Context context, SQLiteDatabase database, String localeStr,
int xmlResId, String fragmentName, int iconResId, int rank,
String intentAction, String intentTargetPackage, String intentTargetClass,
List<String> nonIndexableKeys) {
/**
* 循环遍历当前布局,添加到db
*/
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
nodeName = parser.getName();
key = getDataKey(context, attrs);
if (nonIndexableKeys.contains(key)) {
continue;
}
title = getDataTitle(context, attrs);
keywords = getDataKeywords(context, attrs);
if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
summary = getDataSummary(context, attrs);
String entries = null;
if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
entries = getDataEntries(context, attrs);
}
// Insert rows for the child nodes of PreferenceScreen
updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries,
fragmentName, screenTitle, iconResId, rank,
keywords, intentAction, intentTargetPackage, intentTargetClass,
true, key, -1 /* default user id */);
} else {
String summaryOn = getDataSummaryOn(context, attrs);
String summaryOff = getDataSummaryOff(context, attrs);
if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) {
summaryOn = getDataSummary(context, attrs);
}
updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff,
null, fragmentName, screenTitle, iconResId, rank,
keywords, intentAction, intentTargetPackage, intentTargetClass,
true, key, -1 /* default user id */);
}
}
}
遍历xml中的每一个元素,调用updateOneRowWithFilteredData()->updateOneRow()去更新数据库:
private void updateOneRow(SQLiteDatabase database, String locale, String updatedTitle,
String normalizedTitle, String updatedSummaryOn, String normalizedSummaryOn,
String updatedSummaryOff, String normalizedSummaryOff, String entries, String className,
String screenTitle, int iconResId, int rank, String spaceDelimitedKeywords,
String intentAction, String intentTargetPackage, String intentTargetClass,
boolean enabled, String key, int userId) {
if (TextUtils.isEmpty(updatedTitle)) {
return;
}
// The DocID should contains more than the title string itself (you may have two settings
// with the same title). So we need to use a combination of the title and the screenTitle.
StringBuilder sb = new StringBuilder(updatedTitle);
sb.append(screenTitle);
int docId = sb.toString().hashCode();
ContentValues values = new ContentValues();
values.put(IndexColumns.DOCID, docId);
values.put(IndexColumns.LOCALE, locale);
values.put(IndexColumns.DATA_RANK, rank);
values.put(IndexColumns.DATA_TITLE, updatedTitle);
values.put(IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle);
values.put(IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn);
values.put(IndexColumns.DATA_SUMMARY_ON_NORMALIZED, normalizedSummaryOn);
values.put(IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff);
values.put(IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, normalizedSummaryOff);
values.put(IndexColumns.DATA_ENTRIES, entries);
values.put(IndexColumns.DATA_KEYWORDS, spaceDelimitedKeywords);
values.put(IndexColumns.CLASS_NAME, className);
values.put(IndexColumns.SCREEN_TITLE, screenTitle);
values.put(IndexColumns.INTENT_ACTION, intentAction);
values.put(IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage);
values.put(IndexColumns.INTENT_TARGET_CLASS, intentTargetClass);
values.put(IndexColumns.ICON, iconResId);
values.put(IndexColumns.ENABLED, enabled);
values.put(IndexColumns.DATA_KEY_REF, key);
values.put(IndexColumns.USER_ID, userId);
/**
* 更新或者插入当前数据到"prefs_index"表中
*/
database.replaceOrThrow(Tables.TABLE_PREFS_INDEX, null, values);
}
这就是一个完整的更新索引的过程了!