结合实际代码分析OutOfMemoryError

Android中OOM 的产生

当Android系统启动一个应用时即会创建一个虚拟机实例,同时向Android系统申请所需内存,然后把内存分为两部分,一部分为栈空间,存储一些全局引用和静态变量等值,该空间的分配与回收由系统机制决定,垃圾回收不作用在这块区域;另一部分为堆空间,里面存储是对象的实例,垃圾回收主要作用在这部分,回收的一个主要策略是检测堆中的对象在栈空间有无对应的引用。如果没有引用指向它,则会被优先回收,如果有引用指向则不会被回收。一旦剩余堆内存空间不够申请新对象时就会产生OutOfMemoryError异常。

Android中的虚拟机(Dalvik or ART)对每个应用可使用的最大内存空间做了限制,每台设备出厂之前厂家就对单个虚拟机实例可使用的最大内存进行了限定。这些信息储存在手机中 /system/build.prop配置文件中。
获取Nexus5X相关信息如下

    bullhead:/ $ cat /system/build.prop
    ...
    dalvik.vm.heapstartsize=8m 为一个应用初始分配的堆大小,越大意味着应用第一次启动时越流畅,但也意味着内存耗用越快。
    dalvik.vm.heapgrowthlimit=192m 单个应用可使用的最大内存堆大小。
    dalvik.vm.heapsize=512m 表示应用在manifest中配置android:largeHeap="true"时可使用的最大内存堆大小。
    dalvik.vm.heaptargetutilization=0.75
    dalvik.vm.heapminfree=512k
    dalvik.vm.heapmaxfree=8m

获取内存配置

    Log.e(TAG, "max memory = " + Runtime.getRuntime().maxMemory());
    Log.e(TAG, "free memory = " + Runtime.getRuntime().freeMemory());
    Log.e(TAG, "total memory = " + Runtime.getRuntime().totalMemory());

    ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
    Log.e(TAG, "memoryClass = " + Integer.toString(am.getMemoryClass()));
    Log.e(TAG, "largememoryClass = " + Integer.toString(am.getLargeMemoryClass()));

Runtime.getRuntime().maxMemory() 这个参数对应到build.prop中的信息就是在未设置largeHeap为true时会返回heapgrowthlimit的大小,而设置了largeHeap为true后,则返回heapsize大小。单位为Bytes。

getMemoryClass 所获得的大小不受largeHeap配置影响,永远是heapgrowthlimit中大小。而getLargeMemoryClass则为heapsize大小,两者单位都为M。

Nexus5X manifest中配置android:largeHeap="true"时数据如下,可以把这些数据与build.prop中的数据对应起来

    E/DebugYoga: max memory = 536870912
    E/DebugYoga: free memory = 229684
    E/DebugYoga: total memory = 9332092
    E/DebugYoga: memoryClass = 192
    E/DebugYoga: largememoryClass = 512

情况分析

实际开发过程中OOM最终都由两种情况产生。1.申请大量的内存导致OOM(常见情况加载大图/多图),2.内存泄漏导致可用内存越来越少(常见情况Context泄露)。

编码时我们会注意这些点,可是难免会有疏漏。项目中可以借助leakcanary库,当有内存泄漏时即可发出通知。leakcanary集成只集成debug模式,发布时去除可以减小APK大小。

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'

public class DebugYoga extends Yoga {
    public static final String TAG = "DebugYoga";

    @Override
    public void onCreate() {
        LeakCanary.install(this);
    }
}

内存泄漏时通知



资源下载时引起的内存泄漏,ProjectManager静态对象引用Activity对象,导致Activity关闭时无法回收。触发代码ProjectManager.getInstance().downloadOpenAdvertSource()

    EasyHttp.get(YogaHttpConfig.OPEN_ADVERT).params("action", 7 + "").execute(getLifecycleTransformer(), new YogaSimpleCallBack<List<OpenAdvertResponseResult>>() {

        @Override
        public void onSuccess(List<OpenAdvertResponseResult> openAdvertResponseResults) {
            // 判定广告集合非空且size大于0
            if (openAdvertResponseResults != null && openAdvertResponseResults.size() > 0) {
                // 获取mOpenAdvertResultBean
                mOpenAdvertResultBean = openAdvertResponseResults.get(0);
                // 判定mOpenAdvertResultBean不为null
                if (mOpenAdvertResultBean != null) {
                    ProjectManager.getInstance().downloadOpenAdvertSource(AdvertActivity.this, mOpenAdvertResultBean.getImage(), mOpenAdvertResultBean.getAds_type());
                }
            }
        }
    });

解决方案:采用BehaviorSubject在Activity关闭时,取消回掉关系。

    mOpenAdvertSubject
            .compose(this.<OpenAdvertResponseResult>getLifecycleTransformer())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<OpenAdvertResponseResult>() {
                @Override
                public void accept(OpenAdvertResponseResult result) throws Exception {
                    File file = new File(result.sourceFilePath);
                    dealDownloadSuccess(result, file, Uri.fromFile(file));
                }
            });
ConfigUtil.downloadOpenAdvertSource(mOpenAdvertResultBean, mOpenAdvertSubject);

静态引用Context对象引起内存泄漏,静态对象引用Context对象,静态对象生命周期长于Context对象生命周期,导致Context对象无法回收

private PublicDBManager(Context context) {
    mContext = context;
    initSqliteDB();
}
Context mContext;
public static PublicDBManager getInstence(Context context) {
    if (mForumesDBManager == null) {
        mForumesDBManager = new PublicDBManager(context);
    }
    mForumesDBManager.mContext = context;
    return mForumesDBManager;
}

解决方案:采用context.getApplicationContext()替换context对象

private PublicDBManager(Context context) {
    mContext = context.getApplicationContext();
    initSqliteDB();
}

定位时Activity对象引起内存泄漏,LocationManager对象生命周期长于Activity对象生命周期,导致Activity对象无法回收。

public void getLocations(final Activity activity) {
    // 声明LocationManager对象
    String contextService = Context.LOCATION_SERVICE;
    // 通过系统服务,取得LocationManager对象
    LocationManager loctionManager = (LocationManager) activity.getSystemService(contextService);
    // 监听位置变化,10秒一次,距离10米以上
    loctionManager.requestLocationUpdates(provider, 10000, 10, new LocationListener() {
            // 当位置变化时触发
            @Override
            public void onLocationChanged(Location location) {
                // 使用新的location更新TextView显示
                updateWithNewLocation(location, activity);
            }
     });
}

解决方案:若无需回掉至Activity,采用context.getApplicationContext()替换Activity对象,若需要回掉,采用BehaviorSubject在Activity关闭时,取消回掉关系。

public static void getLocations() {
    // 声明LocationManager对象
    String contextService = Context.LOCATION_SERVICE;
    // 通过系统服务,取得LocationManager对象
    LocationManager loctionManager = (LocationManager) Yoga.getInstance().getSystemService(contextService);
    // 监听位置变化,10秒一次,距离10米以上
    loctionManager.requestLocationUpdates(provider, 10000, 10, new LocationListener() {
        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
        // 当位置变化时触发
        @Override
        public void onLocationChanged(Location location) {
            // 使用新的location更新TextView显示
            updateWithNewLocation(location);
        }
    });
}

静态Activity对象引起内存泄漏,静态对象会一直存在整个应用生命周期,创建后无法回收。

public class FrameworkActivity extends TabActivity{
/** FrameworkActivity实例对象 */
public static FrameworkActivity mFrameInstance;
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mFrameInstance = this;
}

解决方案:禁止此种写法。FrameworkActivity页面采用静态Activity对象的原因有两个,1.TabActivity对象和子Activity对象交互。2,其他Activity对象和TabActivity对象交互。

  • TabActivity对象和子Activity对象交互,采用Activity activity = getParent()获取到TabActivity对象直接调用方法即可

    public void gotoYouZanHome() {
      Activity activity = getParent();
      if (activity instanceof FrameworkActivity) {
          ((FrameworkActivity) activity).setCurrentTab(2);
      }
    }
    
  • 其他Activity对象和TabActivity对象交互,采用Intent对象onNewIntent(Intent intent)进行交互

          Intent intent = new Intent(getContext(), FrameworkActivity.class);
          intent.setAction(FrameworkActivity.ACTION_SWITCH_TAB);
          intent.putExtra(ConstServer.POSITION, 3);
          startActivity(intent);
    
      protected void onNewIntent(Intent intent) {
          super.onNewIntent(intent);
              mEnterFrameIndex = intent.getIntExtra(ConstServer.POSITION, 0);
              if (ACTION_SWITCH_TAB.equals(intent.getAction())) {
                  // 设置当前Tab
                  setCurrentTab(mEnterFrameIndex);
              }
      }
    

总结

1,借助leakcanary库可以快速定位到发生泄漏的位置。
2,避免任何生命周期长于Activity的对象引用Activity对象,如必须绑定关系,需在Activity关闭时解除绑定关系
3,熟悉各个组件交互方式,禁止静态Activity对象写法
4,采用MVP模式避免网络引起的内存泄漏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343