调试缓慢的 ContentResolver 查询调用在 "context switch" 中花费大量时间
Debugging slow ContentResolver query calls spending lots of time in "context switch"
有时 Android 系统调用,例如 ContentResolver.query(...),可能比正常情况下花费 1000 倍的时间,例如,对于通常在 ~10 毫秒内完成的单个调用,大约需要 10 秒。我的问题是如何识别此类情况、了解发生原因并解决问题。
下面的示例是在 Android 系统上检索 "user profile"。在我的 Nexus 6 上,执行此操作通常需要大约 10 毫秒。旧设备同样很快。偶尔它可以超过 20 秒。使用 traceview,它显示几乎所有时间都花在 "context switch":
我的设备异常地有大约 7,000 个联系人。对于我请求的 "user profile",结果只有一行。使用 Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_DIRECTORY)
请求 - 我认为这是一个常见的优化案例。目前,查询是在主线程上完成的,这可能会使事情复杂化……Android 文档提到 "you should do queries asynchronously on a separate thread"。我将来会迁移到那个,但我怀疑这是原因,因为它以前运行良好。
另一个令人惊讶的因素是奇怪的一致性。我在应用程序中有多个查询。对于一系列运行,"user profile" 将一直很慢,然后又开始变快,我无法重现该问题。同样,不是用户个人资料的其他联系人查询会很快,然后变慢。在过去六个月中,所有查询都通过 Nexus 5 和 Nexus 6 设备快速进行 - 这仅出现在上周。
查询:
Uri uri = Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_DIRECTORY),
Log.d(TAG, "getOwner: " + uri.toString());
Cursor cur = mResolver.query(
uri,
PROJECTION, // 6 columns
null,
null,
Data.IS_PRIMARY + " DESC");
Log.d(TAG, "getOwner: query count - " + cur.getCount());
日志输出:
// NOTE 18 second gap between log entries
19:20:33.134 D/ConnectActivity﹕ getOwner: URI: content://com.android.contacts/profile/data
19:20:51.779 D/ConnectActivity﹕ getOwner: query count - 1
我仍然觉得这种行为令人惊讶,不明白为什么会这样。
调试和改进的级别(最后两个专门针对慢"context switch"):
- 查询改进:查询 Uri、投影大小和列索引缓存 more details
- 查询过滤:通过更好的过滤减少查询结果的大小
- 异步:慢查询不应该阻塞UI线程,使用
- Observe Logs:绕着减速看有没有什么蛛丝马迹more details on logs
- Traceview:识别调用花费时间的地方
- 上下文切换:说明
查询改进和过滤
在大多数情况下,查询正确的 Uri、减少投影中的列数和缓存列索引是主要的改进:Getting name and email from contact list is very slow
此示例用于从用户配置文件中获取名称:
Cursor cur = mResolver.query(
// Uri will only return user profile rather than everything in contacts
Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_DIRECTORY),
// Projection only has 2 columns to reduce the data returned
new String[] {
Data.MIMETYPE,
ContactsContract.Data.DISPLAY_NAME};
// Filter matches on specific mimetype to reduce result size
Data.MIMETYPE + "=?",
// Filter matches on specific mimetype to reduce result size
new String[]{StructuredName.CONTENT_ITEM_TYPE},
// No sorting may improve performance
null);
异步
Android Documentation suggests using a CursorLoader:
为了清楚起见,本节中的代码片段在 "UI thread" 上调用了 ContentResolver.query()。但是,在实际代码中,您应该异步进行查询在单独的线程上。执行此操作的一种方法是使用 CursorLoader class,加载器指南中对此有更详细的描述。此外,代码行只是片段;它们不显示完整的应用程序.
观察日志
您应该围绕减速进行大量观察以对其进行量化,并查看它是否只有导致原因的线索。 ContentResolver.query(...) 调用通常需要 10-100 毫秒,并且该范围内的任何内容都可以如上所述进行改进,但这是意料之中的。在 1-10 秒的范围内,这可能是由上下文切换到其他进程引起的。当发生上下文切换时,ART(Android 5.0 中的默认运行时)将输出一条日志行,指示上下文切换及其花费的时间:
Traceview
Android 的 traceview 可用于识别时间花在了哪里。基本用法是按如下方式包装您要分析的方法:
Debug.startMethodTracing("trace-name");
// Code to profile
Debug.stopMethodTracing();
接下来从设备中提取日志(我相信这是默认路径):
adb pull /storage/emulated/legacy/dmtrace.trace
Android Device Monitor can be used to open the file and view. From Android Studio, select Tools -> Android -> Android Device Monitor, then open the file. Often it is helpful to rearrange the Incl Real Time and Incl Cpu Time columns to the far left. These show the time taken by a method (and it's children methods) both as cpu time and real time (which also including blocking IO calls that aren't measured with cpu time in the app process - more details)。最可疑的条目是 Incl Real Time 大大超过 Incl Cpu Time。这意味着该方法花费了很长时间,但在您的代码中并没有做太多事情,所以它一定是被应用进程之外的东西阻止了。
你可以看到一个列表方法和每个方法的时间。从“0(顶层)”开始,单击三角形以查看 "children" 并将其打开。每个 child 将列出一个方法编号,您可以使用它在 traceview 中的其他地方查找方法并查看它们的 children,通过层次结构降序。寻找 Incl Real Time >> Incl Cpu Time 的情况,您通常会发现它以“(上下文切换)”结尾.这是拖慢您进程的长时间阻塞事件。
上下文切换
维基百科description:
在计算中,上下文切换是存储和恢复进程或线程的状态(上下文)的过程,以便稍后可以从同一点恢复执行。这使多个进程可以共享一个 CPU,这是多任务操作系统的基本特征。什么构成上下文由处理器和操作系统决定。
在 Android 的情况下,这表明 OS 已选择保留您的应用程序的状态,将其从 CPU 中删除,以便它可以执行另一个进程。当上下文切换结束时,进程可能会在 CPU 上恢复并继续执行。
有时 Android 系统调用,例如 ContentResolver.query(...),可能比正常情况下花费 1000 倍的时间,例如,对于通常在 ~10 毫秒内完成的单个调用,大约需要 10 秒。我的问题是如何识别此类情况、了解发生原因并解决问题。
下面的示例是在 Android 系统上检索 "user profile"。在我的 Nexus 6 上,执行此操作通常需要大约 10 毫秒。旧设备同样很快。偶尔它可以超过 20 秒。使用 traceview,它显示几乎所有时间都花在 "context switch":
我的设备异常地有大约 7,000 个联系人。对于我请求的 "user profile",结果只有一行。使用 Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_DIRECTORY)
请求 - 我认为这是一个常见的优化案例。目前,查询是在主线程上完成的,这可能会使事情复杂化……Android 文档提到 "you should do queries asynchronously on a separate thread"。我将来会迁移到那个,但我怀疑这是原因,因为它以前运行良好。
另一个令人惊讶的因素是奇怪的一致性。我在应用程序中有多个查询。对于一系列运行,"user profile" 将一直很慢,然后又开始变快,我无法重现该问题。同样,不是用户个人资料的其他联系人查询会很快,然后变慢。在过去六个月中,所有查询都通过 Nexus 5 和 Nexus 6 设备快速进行 - 这仅出现在上周。
查询:
Uri uri = Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_DIRECTORY),
Log.d(TAG, "getOwner: " + uri.toString());
Cursor cur = mResolver.query(
uri,
PROJECTION, // 6 columns
null,
null,
Data.IS_PRIMARY + " DESC");
Log.d(TAG, "getOwner: query count - " + cur.getCount());
日志输出:
// NOTE 18 second gap between log entries
19:20:33.134 D/ConnectActivity﹕ getOwner: URI: content://com.android.contacts/profile/data
19:20:51.779 D/ConnectActivity﹕ getOwner: query count - 1
我仍然觉得这种行为令人惊讶,不明白为什么会这样。
调试和改进的级别(最后两个专门针对慢"context switch"):
- 查询改进:查询 Uri、投影大小和列索引缓存 more details
- 查询过滤:通过更好的过滤减少查询结果的大小
- 异步:慢查询不应该阻塞UI线程,使用
- Observe Logs:绕着减速看有没有什么蛛丝马迹more details on logs
- Traceview:识别调用花费时间的地方
- 上下文切换:说明
查询改进和过滤
在大多数情况下,查询正确的 Uri、减少投影中的列数和缓存列索引是主要的改进:Getting name and email from contact list is very slow
此示例用于从用户配置文件中获取名称:
Cursor cur = mResolver.query(
// Uri will only return user profile rather than everything in contacts
Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_DIRECTORY),
// Projection only has 2 columns to reduce the data returned
new String[] {
Data.MIMETYPE,
ContactsContract.Data.DISPLAY_NAME};
// Filter matches on specific mimetype to reduce result size
Data.MIMETYPE + "=?",
// Filter matches on specific mimetype to reduce result size
new String[]{StructuredName.CONTENT_ITEM_TYPE},
// No sorting may improve performance
null);
异步
Android Documentation suggests using a CursorLoader:
为了清楚起见,本节中的代码片段在 "UI thread" 上调用了 ContentResolver.query()。但是,在实际代码中,您应该异步进行查询在单独的线程上。执行此操作的一种方法是使用 CursorLoader class,加载器指南中对此有更详细的描述。此外,代码行只是片段;它们不显示完整的应用程序.
观察日志
您应该围绕减速进行大量观察以对其进行量化,并查看它是否只有导致原因的线索。 ContentResolver.query(...) 调用通常需要 10-100 毫秒,并且该范围内的任何内容都可以如上所述进行改进,但这是意料之中的。在 1-10 秒的范围内,这可能是由上下文切换到其他进程引起的。当发生上下文切换时,ART(Android 5.0 中的默认运行时)将输出一条日志行,指示上下文切换及其花费的时间:
Traceview
Android 的 traceview 可用于识别时间花在了哪里。基本用法是按如下方式包装您要分析的方法:
Debug.startMethodTracing("trace-name");
// Code to profile
Debug.stopMethodTracing();
接下来从设备中提取日志(我相信这是默认路径):
adb pull /storage/emulated/legacy/dmtrace.trace
Android Device Monitor can be used to open the file and view. From Android Studio, select Tools -> Android -> Android Device Monitor, then open the file. Often it is helpful to rearrange the Incl Real Time and Incl Cpu Time columns to the far left. These show the time taken by a method (and it's children methods) both as cpu time and real time (which also including blocking IO calls that aren't measured with cpu time in the app process - more details)。最可疑的条目是 Incl Real Time 大大超过 Incl Cpu Time。这意味着该方法花费了很长时间,但在您的代码中并没有做太多事情,所以它一定是被应用进程之外的东西阻止了。
你可以看到一个列表方法和每个方法的时间。从“0(顶层)”开始,单击三角形以查看 "children" 并将其打开。每个 child 将列出一个方法编号,您可以使用它在 traceview 中的其他地方查找方法并查看它们的 children,通过层次结构降序。寻找 Incl Real Time >> Incl Cpu Time 的情况,您通常会发现它以“(上下文切换)”结尾.这是拖慢您进程的长时间阻塞事件。
上下文切换
维基百科description:
在计算中,上下文切换是存储和恢复进程或线程的状态(上下文)的过程,以便稍后可以从同一点恢复执行。这使多个进程可以共享一个 CPU,这是多任务操作系统的基本特征。什么构成上下文由处理器和操作系统决定。
在 Android 的情况下,这表明 OS 已选择保留您的应用程序的状态,将其从 CPU 中删除,以便它可以执行另一个进程。当上下文切换结束时,进程可能会在 CPU 上恢复并继续执行。