一.问题背景:
新平台固件测试后,反馈一个OOM的问题。问题日志如下:
3944 3959 I: Clamp target GC heap from 280MB to 256MB
3944 3959 I: Alloc concurrent copying GC freed 0(0B) AllocSpace objects, 0(0B) LOS objects, 0% free, 256MB/256MB, paused 173us total 383.632ms
3944 3944 I: WaitForGcToComplete blocked Alloc on HeapTrim for 4.630s
3944 3944 I: Starting a blocking GC Alloc
3944 3944 I: Starting a blocking GC Alloc
3944 3958 I: Waiting for a blocking GC Alloc
3944 4712 I: Waiting for a blocking GC Alloc
3944 3959 W: Throwing OutOfMemoryError "Failed to allocate a 144 byte allocation with 0 free bytes and 0B until OOM, target footprint 268435456, growth limit 268435456" (VmSize 1555288 kB, recursive case)
3944 3959 W: "FinalizerWatchdogDaemon" daemon prio=5 tid=9 Runnable
3944 3959 W: | group="system" sCount=0 dsCount=0 flags=2 obj=0x12c433c8 self=0xa8422000
3944 3959 W: | sysTid=3959 nice=4 cgrp=default sched=0/0 handle=0x89ae3230
3944 3959 W: | state=R schedstat=( 16335080529 328336875 3663 ) utm=1366 stm=266 core=2 HZ=100
3944 3959 W: | stack=0x899e0000-0x899e2000 stackSize=1040KB
3944 3959 W: | held mutexes= "mutator lock"(shared held)
3944 3959 W: at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68)
3944 3959 W: at java.lang.StringBuilder.<init>(StringBuilder.java:90)
3944 3959 W: at com.android.internal.os.RuntimeInit$LoggingHandler.uncaughtException(RuntimeInit.java:91)
3944 3959 W: at java.lang.Thread.dispatchUncaughtException(Thread.java:2181)
3944 3959 W: at java.lang.Daemons$FinalizerWatchdogDaemon.finalizerTimedOut(Daemons.java:483)
3944 3959 W: at java.lang.Daemons$FinalizerWatchdogDaemon.runInternal(Daemons.java:325)
3944 3959 W: at java.lang.Daemons$Daemon.run(Daemons.java:137)
3944 3959 W: at java.lang.Thread.run(Thread.java:919)
3944 3959 W:
3944 3959 I Process : Sending signal. PID: 3944 SIG: 9
测试代码:
void test() {
byte[] ucKey = {0, 1, 2, 3, 4, 5, -1};
for (int i = 0; i < ucKey.length; i++) {
for (int j = 0; j < 100000; j++) {
if (!stopTest()) {
Display.setInputResult(String.format("i=%d, j=%d\n", Integer.valueOf(i), Integer.valueOf(j)));
try {
testFunctionInterface(ucKey[i]); // 这里是一个被测试的接口,测试反馈该接口压力测试时出现OOM
} catch (Exception e) {
Display.printException(e);
fail(String.format("testFunctionInterface fail iRet=%d, ucKey=%d, j=%d\n", Integer.valueOf(e.exceptionCode), Byte.valueOf(ucKey[i]), Integer.valueOf(j)));
}
} else {
return;
}
}
}
}
二.问题分析
从日志可以明显看出是内存溢出,但是溢出是哪里造成的,从代码难以看出,这个时候就需要使用内存分析工具mat了。
1.抓取heap
可以使用命令行抓取
adb shell
am dumpheap 包名 /sdcard/dump.hprof
也可以使用Android Studio抓取:
打开"profilter"面板,选择需要分析的应用(注意这里应用如果是release的是没办法抓取的),点击"Memeory",选择"Capture heap dump"
接下来另存为抓取的heap,此时的heap还无法使用mat分析,需要进行转换
找到android sdk的platform-tools目录,有个hprof-conv.exe
hprof-conv.exe 源文件 目标文件
2.使用MAT分析
.下载MAT工具:网址?http://www.eclipse.org/mat/downloads.php
打开"MemoryAnalyzer.exe", File-Open Heap Dump
打开Leak Suspects
只看强应用对象
平时使用mat分析,一般都有多个可以点,然后需要自己一一排查,这次居然只有这么一个点,真的太幸运了。
?三.结合源码分析
经过上面的步骤我们可以发现溢出是在
android.view.textservice.SpellCheckerSession$SpellCheckerSessionListenerImpl
再次回到测试代码,Display.setInputResult是一个EditText用于显示内容,而被测接口跟view无关,我们去掉测试接口,再测试,发现仍会内存溢出,这里很明显是我们的EditText导致的。
这么看来是我们系统的拼写检查问题了,打开相关日志:
frameworks/base/core/java/android/service/textservice/SpellCheckerService.java
frameworks/base/core/java/android/view/textservice/SpellCheckerSession.java
frameworks/base/core/java/android/view/textservice/TextServicesManager.java
frameworks/base/services/core/java/com/android/server/textservices/TextServicesManagerService.java
使用新的固件再来看看:
D/SpellCheckerSession: queueing tasks in processOrEnqueueTask since the connection is not established. mPendingTasks.size()=1
W/TextServicesManagerService: bind service: com.android.inputmethod.latin/.spellcheck.AndroidSpellCheckerService
D/SpellCheckerSession: queueing tasks in processOrEnqueueTask since the connection is not established. mPendingTasks.size()=2
D/SpellCheckerSession: queueing tasks in processOrEnqueueTask since the connection is not established. mPendingTasks.size()=3
D/SpellCheckerSession: queueing tasks in processOrEnqueueTask since the connection is not established. mPendingTasks.size()=4
...
W/SpellCheckerService: onBind
W/TextServicesManagerService: getCurrentSpellCheckerSubtype: 0
W/TextServicesManagerService: onServiceConnectedInnerLocked: ComponentInfo{com.android.inputmethod.latin/com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService}
W/Binder: Caught a RuntimeException from the binder stub implementation.
java.lang.SecurityException: Failed to find provider user_dictionary for user 0; expected to find a valid ContentProvider for this authority
at android.os.Parcel.createException(Parcel.java:2071)
at android.os.Parcel.readException(Parcel.java:2039)
at android.os.Parcel.readException(Parcel.java:1987)
at android.content.IContentService$Stub$Proxy.registerContentObserver(IContentService.java:1234)
at android.content.ContentResolver.registerContentObserver(ContentResolver.java:2263)
at android.content.ContentResolver.registerContentObserver(ContentResolver.java:2251)
at com.android.inputmethod.latin.spellcheck.AndroidWordLevelSpellCheckerSession.<init>(AndroidWordLevelSpellCheckerSession.java:111)
at com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerSession.<init>(AndroidSpellCheckerSession.java:43)
at com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerSessionFactory.newInstance(AndroidSpellCheckerSessionFactory.java:23)
at com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.createSession(AndroidSpellCheckerService.java:138)
at android.service.textservice.SpellCheckerService$SpellCheckerServiceBinder.getISpellCheckerSession(SpellCheckerService.java:339)
at com.android.internal.textservice.ISpellCheckerService$Stub.onTransact(ISpellCheckerService.java:110)
at android.os.Binder.execTransactInternal(Binder.java:1045)
at android.os.Binder.execTransact(Binder.java:1018)
Caused by: android.os.RemoteException: Remote stack trace:
at com.android.server.content.ContentService.registerContentObserver(ContentService.java:344)
at android.content.IContentService$Stub.onTransact(IContentService.java:482)
at android.os.Binder.execTransactInternal(Binder.java:1045)
at android.os.Binder.execTransact(Binder.java:1018)
从日志中可以看到mPendingTasks.size在一直增大,在SpellCheckerSession.java中
private int mState = STATE_WAIT_CONNECTION;
private static final class SpellCheckerSessionListenerImpl
extends ISpellCheckerSessionListener.Stub {
public void getSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)
processOrEnqueueTask
}
public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
processOrEnqueueTask
}
private void processOrEnqueueTask(SpellCheckerParams scp) {
...
if (mState == STATE_WAIT_CONNECTION) {
...
mPendingTasks.offer(scp);
...
if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the"
+ " connection is not established."
+ " mPendingTasks.size()=" + mPendingTasks.size());
}
return;
...
}
public void onServiceConnected(ISpellCheckerSession session) {
...
mState = STATE_CONNECTED;
...
while (!mPendingTasks.isEmpty()) {
processTask(session, mPendingTasks.poll(), false);
}
...
}
}
就是因为没有调用onServiceConnected导致mPendingTasks一直没有被消费,最后出现内存溢出,接下来就看是什么原因导致连接不上服务的。
还是在SpellCheckerSession.java
private static final class InternalListener extends ITextServicesSessionListener.Stub {
private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
}
@Override
public void onServiceConnected(ISpellCheckerSession session) {
mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
}
}
/**
* @hide
*/
public ITextServicesSessionListener getTextServicesSessionListener() {
return mInternalListener;
}
在TextServicesManager的newSpellCheckerSession里调用了getSpellCheckerService,然后在TextServicesManagerService调用bindGroup.getISpellCheckerSessionOrQueueLocked,此时就到了上面日志中的at android.service.textservice.SpellCheckerService$SpellCheckerServiceBinder.getISpellCheckerSession(SpellCheckerService.java:339)
接下来就容易了,跟着日志看到
packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
AndroidWordLevelSpellCheckerSession(final AndroidSpellCheckerService service) {
mService = service;
final ContentResolver cres = service.getContentResolver();
mObserver = new ContentObserver(null) {
@Override
public void onChange(boolean self) {
mSuggestionsCache.clearCache();
}
};
cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
}
ContentService
public void registerContentObserver(Uri uri, boolean notifyForDescendants,
IContentObserver observer, int userHandle, int targetSdkVersion) {
if (observer == null || uri == null) {
throw new IllegalArgumentException("You must pass a valid uri and observer");
}
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
userHandle = handleIncomingUser(uri, pid, uid,
Intent.FLAG_GRANT_READ_URI_PERMISSION, true, userHandle);
final String msg = LocalServices.getService(ActivityManagerInternal.class)
.checkContentProviderAccess(uri.getAuthority(), userHandle);
if (msg != null) {
if (targetSdkVersion >= Build.VERSION_CODES.O) {
throw new SecurityException(msg);
} else {
if (msg.startsWith("Failed to find provider")) {
// Sigh, we need to quietly let apps targeting older API
// levels notify on non-existent providers.
} else {
Log.w(TAG, "Ignoring content changes for " + uri + " from " + uid + ": " + msg);
return;
}
}
}
synchronized (mRootNode) {
mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
uid, pid, userHandle);
if (false) Log.v(TAG, "Registered observer " + observer + " at " + uri +
" with notifyForDescendants " + notifyForDescendants);
}
}
可以看到在这里因为查找不到 "字典的内容提供者"?然后抛出了异常,经过定位原来是我们系统裁剪了UserDictionaryProvider导致的,将UserDictionaryProvider推到系统问题成功解决。
后记
1.newSpellCheckerSession还会判断系统语言,当我们设置成中文时,因为中文不在"拼写检查工具"支持的语言里,所以也不会造成内存溢出。
2.设置EditText的inputType为"textNoSuggestions",也不会造成内存溢出
|