记一次Android OOM问题

一.问题背景:

新平台固件测试后,反馈一个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",也不会造成内存溢出

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android OOM(Out of Memory)是一种常见的运行时异常,指的是应用程序内存不足的错误。当应用程序试图使用超过系统分配给它的内存时,就会出现这种异常。这可能是由于应用程序在后台加载大量数据、存储过多的对象或图像,或者由于系统资源管理器分配的内存不足所致。 为了解决Android OOM问题,您可以采取以下几种策略: 1. 优化您的代码以减少内存使用量:使用正确的数据类型,避免创建不必要的对象,限制图像和资源的数量,以及优化后台加载过程等。 2. 回收不再使用的内存:当您的应用程序不再需要使用某些内存时,应该及时回收它们。这可以通过调用垃圾回收器(Garbage Collector)来完成。 3. 避免在主线程上执行耗时操作:如果您的应用程序在主线程上执行耗时操作(如大量数据处理),这可能导致系统资源管理器超载,从而引发OOM异常。应该将这些操作移至后台线程。 4. 使用内存分析工具:内存分析工具可以帮助您识别内存泄漏和无效内存引用等问题,从而避免OOM异常的发生。 5. 配置您的应用程序以适应不同的内存配置:如果您正在开发一个需要大量内存的应用程序,您应该考虑在AndroidManifest.xml文件中配置您的应用程序以适应不同的内存配置。例如,您可以设置您的应用程序需要的最低和最高内存限制。 请注意,解决Android OOM问题是一个复杂的过程,需要您仔细分析和优化您的代码。如果您遇到了OOM问题,建议寻求专业的帮助或与开发社区进行讨论。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值