美文网首页Android那些事Android技术知识程序员
莫往Applicaotion存缓存/app被系统回收之后再打开发

莫往Applicaotion存缓存/app被系统回收之后再打开发

作者: 阿敏其人 | 来源:发表于2015-11-25 22:01 被阅读3372次

    本文出自 “阿敏其人” 简书博客,转载或引用请注明出处。

    先上结论:

    • 1、尽量不要往Application里面存放缓存数据(因为app用application缓存且使用全局数据,当这个app为后台程序时又被系统gc回收后我们再次打开app,就会导致空指针)

    • 2、app被gc回收后再点击该app打开的会是回收前最后可见的activity(比如我们 app甲先后依次打开了a,b,c这三个activity,这时a,b,c都是在同一个线程的,这时我们按下home键,利用DDMS强制关闭我们程序甲的process,其实也就是模拟被gc回收了。这个时候我们在打开程序甲,会发现打开的是程序甲这的c这个activity,这个时候c是在一个新的线程里面的,而不是在原来的线程里面的)

    有图有真相。
    下图演示因为当安卓app被系统gc回收后我们再次打开app产生空指针导致数据丢失的现象

    application缓存丢失演示.gif

    上图程序没挂是因为我们缓存在application的值我们这是用作显示,null就是空了,当我们使用了这个值马上程序就会应为空指针而挂掉。

    下面我们使用以下给gc回收的缓存在application的值

    application空指针演示.gif

    一、不要往Application存缓存数据

    当然这不是绝对化,只是这么建议。

    在你的App中的很多地方都需要使用到数据信息,它可能是一个session token,一次费时计算的结果等等,通常为了避免Activity之间传递数据的开销,会将这些数据通过持久化来存储。

    有人建议将这些数据放在Application对象中方便所有的Activity访问,这个解决方案简单、优雅并且是……完全错误的。

    你如果你将数据缓存到Application对象中,那么有可能你的程序最终会由于一个NullPointerException异常而崩溃掉。

    1、数据存在Application被gc后数据丢失的demo演示

    下面通过demo演示因为数据存放在application导致数据丢失的情况:

    1、新建一个类MyApplication继承自Application,弄一个那么的属性设置对应的get和set方法,并且让我们的程序的清单文件的application采用我们的MyApplication
    2、弄三个acitivity,MainActivity有一个按钮,点击则打开FirstActivity,而Activity的onCreate方法干的事就是给MyApplicatipn的name属性set一个值,比如“Try Text”,接着就直接打开我们的SecondActivity
    3、SecondAcrivity直接有一个TextView控件,这个控件用于显示从MyApplication取出来的那么的值。

    嗯就是这样子,正常情况下,我们进入MainActivity,点击 “打开FirstActivity” 只会就进入FirstActivity之后瞬间进入SecondActivity。SecondActivity正常显示FirstActivity给存储在MyApplication里面的值。

    这个是没什么问题的。这个时候,我们按下home键,然后利用DDMS把当前app杀掉(模拟给gc回收),然后在打开app,会发现SecondActivity显示的name值变为null,数据丢失了。一旦使用这个值就报空指针。

    接下来贴一些代码:

    MyApplication

    public class MyApplication extends Application{
    
        @Override
        public void onCreate() {
            super.onCreate();
        }
    
        String name;
    
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    
    

    MainActivity

    public class MainActivity extends Activity {
    
        private TextView mTvOpenFirst;
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mTvOpenFirst= (TextView) findViewById(R.id.mTvOpenFirst);
    
            mTvOpenFirst.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent openFirst = new Intent(MainActivity.this,FirstActivity.class);
                    
                    startActivity(openFirst);
                }
            });
            
        }
    }
    
    

    FirstActivity

    
    public class FirstActivity extends Activity{
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_first);
    
            MyApplication app = (MyApplication) getApplication();
            app.setName("Try Text");
    
            startActivity(new Intent(this, SecondActivity.class));
        }
    }
    

    SecondActivity

    public class SecondActivity extends Activity{
    
        private TextView mTv;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_second);
            mTv= (TextView) findViewById(R.id.mTv);
    
            MyApplication app = (MyApplication) getApplication();
            mTv.setText("HELLO " + app.getName());
    
            
            Log.d("fixmothod", "是否为空:" + (app.getName() == null));
            // app.getName()为null,调用下面这句代码就会引发空指针
            //boolean  equals= app.getName().equals("123");
    
    
        }
    
    
    }
    
    

    去掉备注的boolean equals= app.getName().equals("123");再来一遍引发的报错图

    空指针.png

    2、原因分析

    因为当我们重新打开这个app的时候,系统会为我们重新实例化一个新的Application对象,Application对象并不是始终在内存中的,它有可能会由于系统内存不足而被杀掉。但Android在你恢复这个应用时并不是重新开始启动这个应用,它会创建一个新的Application对象并且启动上次用户离开时的activity以造成这个app从来没有被kill掉得假象。

    说到底,重新点击被杀掉的app时,会实例化一个新的Application对象,然后启动被杀死的app在被杀死之前的所停留的最后一个activity。

    二、app被系统回收之后再打开发生了什么

    android独特的内存机制,导致系统内存不足时会销毁后台的应用,这里我们研究一下应用被销毁后重新加载时的情形

    1、问题:打开被gc杀死的程序后,存在Application等得全局静态变量全部被重置

    一个安卓应用A先后打开3个Activity:
    a --> b --> c (此时的a、b、c是在同一个线程的)
    这个时候如果来了一个电话, 接电话的过程中, 手机内存不够, 那么应用A将会被系统回收

    当打完电话,再次进入应用A的时候会发生下面的事情:

    • 1, 系统会重新加载c,而且是在新线程中
    • 2, 现在点返回关闭c,系统就会重新加载b,而且是在新的线程中(跟c不是一个线程)
    • 3, 现在点返回关闭b,系统就会重新加载a,而且是在新的线程中(跟c,b的线程都不相同)
      (就是先打开c,finish了c时加载b,finish了b时加载a)

    注意: 这个时候应用中的全局静态变量将全部重置(有默认值的为默认值,没有默认值的为null)

    这就使得出现了错误的数据

    2 解决办法:

    方法1, 不使用全局变量

    不使用全局变量, 放在Application中也不行, 因为重新加载的a,b,c的页面不在一个线程中,Application不唯一了 (有点不成文)

    方法2,强制关闭非存储初值的界面,先开启能够先赋值的界面(一般都是登录界面)

    结束b和c, 只重新加载a ,在a中重新初始化数据 (a往往是登录界面)

    办法2的实现:
    在b,c等所有 非a的activity 中的onCreate里面加上下面的代码

    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        if (isNull(Config.UserID)) {  
            if (null != savedInstanceState) {  
                // activity由系统打开 (是由于手机内存不够,activity在后台被系统回收,再打开时出现的现象)  
                // 因为系统加载的所有的Activity不在同一个线程,所以要结束除了loginActivity之外的其他线程  
                android.os.Process.killProcess(android.os.Process.myPid());  
            } else {  
                this.finish();  
            }  
            return;  
        }  
        // ...其他代码  
    }  
    

    代码的原理:
    因为重新加载的a,b,c都在不同的线程中,所以我们先后舍弃c,b的线程,那么a就会重新加载

    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_login);  
        if (null != savedInstanceState) {  
            toast("程序被系统回收,需要重新登录!");  
            //activity由系统加载的时候savedInstanceState不为空  
        }  
        //...其他代码  
    }  
      
    

    关于以上的方法二这个是在实际app中使用。就不做具体分析了。

    方法3,onCreate中添加异常处理 ,遇到异常的时候,就重启程序。

    Intent i = getBaseContext().getPackageManager()    
          .getLaunchIntentForPackage(getBaseContext().getPackageName());    
    i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);    
    startActivity(i);  
    

    方法4、利用savedInstanceState和onRestoreInstanceState

    这个方法便捷针对本demo看起来挺便捷的,但是使用的时候要结合具体,因为写起来太麻烦。

    我们的Activity可以复写savedInstanceState和onRestoreInstanceState这两个方法,简单说,这个两个方法正常情况下不会被调用,onSaveInstanceState方法只有在程序以外退出或Activity异常终止的时才会被调用,用于保存数据

    • savedInstanceState只有在程序以外退出或Activity异常终止的时才会被调用,用于保存数据。
    • onRestoreInstanceState则用来恢复数据,这些数据就是savedInstanceState那里拿来的。

    我们只要修改下SecondActivity就可以看到效果:

    
    public class SecondActivity extends Activity{
    
        private TextView mTv;
        MyApplication app;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_second);
            mTv= (TextView) findViewById(R.id.mTv);
    
            app= (MyApplication) getApplication();
            mTv.setText("HELLO " + app.getName());
    
        }
    
        /**
         * onSaveInstanceState和onRestoreInstanceState一般都是配合着使用的
         * 方法只有在程序以外退出或Activity异常终止的时才会被调用,用于保存数据
         * @param outState
         */
        @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            // 键值对 key value
            outState.putString("save_name",app.getName());
    
        }
    
    
        /**
         * onRestoreInstanceState
         * 用于恢复在以外发生之前保存的数据
         * @param savedInstanceState
         */
        @Override
        protected void onRestoreInstanceState(Bundle savedInstanceState) {
            super.onRestoreInstanceState(savedInstanceState);
            // 获取 key 所对应的 value
            String save_name = savedInstanceState.getString("save_name");
            mTv.setText("HELLO" + save_name);
            Log.d("fixmothod", "app.getName()是否为空1: " + app.getName());
            app.setName(save_name);
            Log.d("fixmothod", "app.getName()是否为空2: " + app.getName());
    
    
        }
    }
    
    
    利用onSaveInstanceState.gif

    三、附上源码

    module说明图

    module说明.png

    project下载链接

    本篇完。


    参考链接:
    不要在Android的Application对象中缓存数据!

    android系统内存不足时,应用被系统回收发生了什么?

    如何处理android程序变为后台程序,系统回收资源,再次打开时,程序因为null指针等崩溃


    相关文章

      网友评论

      • wangneng:谷歌推荐在application中存值.application活着app就活着,application挂了app也就结束了.静态成员变量有可能回收,但application不会.如果被真的gc干掉,可以在application的onCreat中重新进行初始化.临时存储的东西放在application中属于浪费内存,应该存在数据库或者sp里面吧
        阿敏其人:1、kill之后application会不会被干掉onCreate周期走一下就看得见
        2、觉得合适的值你可以适当onCreate重新赋值,上文也说了。
        3、文中只是建议不要把值放在application,而不是绝对化嘛。
        尽量不放,使用合适即可。
      • 金馆长说:关于重启后C B A都会创建一个新线程是不是有问题?我试了下kill重启后,C>B>A 他们都是在一个线程,而且系统没有理由为它们各自开启一个线程。
        one_cup:这个根本就是伪逻辑,怎么可能会有多个线程,毕竟主线程就只有一个,系统只允许在主线程更新UI,那B,A不是主线程setContentView不就线程检查崩溃了吗?虽然我没有按照作者的操作实践过,不过这明显就是有问题的啊。
      • 3Zero:还有,fragment里的静态变量也会这样
      • a8a45c81d805:你好,请问多次重启以后,在最后一个activity按系统的返回键,会正常回到桌面吗?
      • d2d2fcaa6491:那为什么有的手机就不会呢,,,,能解答一下嘛,,我现在喷到了这个问题。。百分之95的手机不会出现这个问题,但是有几个手机就会,
        1e7d1c126444:@妙木山仙人i 有很多手机厂商修改过系统回收机制,这么做的目的是为了省电。但是大部分的手机还是没有修改的,开着省电模式,多测试几次就会出现这个情况了。但是正常情况下一般是不会被回收的,所以我们正常的用户使用的时候基本感觉不到。或者说很少会遇到这样的情况。
      • for_in:为什么安卓的技术文章发在iOS这里?
        阿敏其人:@GitHub搬运工 我没投到ios哈
      • LostAbaddon:还有啊,这句:“利用MMDS强制关闭我们程序甲的process”,人家是叫“DDMS”的吧。。。
        阿敏其人:@塔塔酱 对的DDMS,打错了,我改下
      • LostAbaddon:“数据存在Application被gc后数据都是的demo演示”
        这个标题明显读着不通顺啊,应该是“数据存在Application被gc后数据都是NULL的demo演示”类似这样才对吧?
        阿敏其人:@塔塔酱 哈哈哈,谢谢哈,昨晚写完没有检查文字的错误,出现了一些错别字
      • LostAbaddon:另一点啊,既然是结论,来一句问句“app被gc回收后再次点击该app发生了什么”这个也不妥啊。。。
      • LostAbaddon:“因为app用application缓存切使用全局数据”
        开头这句里的“切”是什么意思?“且”吧?
      • 曾樑:我上学时候咋就没有这种教程勒:scream:

      本文标题:莫往Applicaotion存缓存/app被系统回收之后再打开发

      本文链接:https://www.haomeiwen.com/subject/ycarhttx.html