Android-Application被回收引发空指针异常分析(消灭全局变量)

问题描述

App切换到后台后,一段时间不操作,再切回来,很容易就发生崩溃(配置低的手机这种问题出现更频繁)。究其原因,是因为常常把对象存储在Application里面,而App切换到后台后,进程很容易就被系统回收了,下次切换回来的时候App页面再重建,但是系统重建的App对于原来存储的全局变量却无能为力。

示例工程

例如:有这样的场景,在App登陆页面登录成功后,把接口返回的用户信息(用户名,电话,服务器返回用于后续网络请求的口令-Token)存储起来,方便下次使用。

1.创建存储用户信息的UserInfoBean

/** 用户信息 */
public class UserInfoBean {
    private String name;
    private String tel;
    private String token;

    public UserInfoBean(String name, String tel, String token) {
        super();
        this.name = name;
        this.tel = tel;
        this.token = token;
    }

    @Override
    public String toString() {
        return "UserInfoBean [name=" + name + ", tel=" + tel + ", token="
                + token + "]";
    }
}

2.因为很多页面都有可能会设计到使用网络访问,获取用户信息,于是把它存储到Application中。

public class XApp extends Application {
    private UserInfoBean userinfo;

    public UserInfoBean getUserinfo() {
        return userinfo;
    }

    public void setUserinfo(UserInfoBean userinfo) {
        this.userinfo = userinfo;
    }
}

3.模拟登录成功,存储接口返回的UserInfoBean

public class LoginActivity extends Activity {

    private Button btnLogin;
    private ProgressDialog pdLogin;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        pdLogin = new ProgressDialog(this, ProgressDialog.THEME_HOLO_LIGHT);
        pdLogin.setMessage("登陆中...");
        btnLogin = (Button) findViewById(R.id.btnLogin);
        btnLogin.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // 弹出等待对话框 模拟登录耗时操作
                pdLogin.show();
                btnLogin.getHandler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        pdLogin.dismiss();
                        // 存储数据
                        UserInfoBean userInfo = new UserInfoBean("Tony",
                                "17011110000", "tokenabcdefg");
                        ((XApp) getApplication()).setUserinfo(userInfo);
                        MainActivity.actionStart(LoginActivity.this);
                    }
                }, 1500);
            }
        });
    }
}

4.获取Application中的UserInfoBean使用

public class MainActivity extends Activity {

    private Button btnShowUserInfo;
    private UserInfoBean userInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnShowUserInfo = (Button) findViewById(R.id.btnShowUserInfo);
        btnShowUserInfo.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                userInfo = ((XApp) getApplicationContext()).getUserinfo();
                Toast.makeText(getApplicationContext(), userInfo.toString(),
                        Toast.LENGTH_LONG).show();
            }
        });
    }

    public static void actionStart(Context context) {
        context.startActivity(new Intent(context, MainActivity.class));
    }
}

情景重现

模拟切换到后台,App进程被系统回收的场景

  1. 开启应用,进入登录页,登录成功跳转到主页
  2. 按Home键退出应用
  3. 使用DDMS-Stop Process结束进程
  4. 回到应用中,正常使用(注:现在处于一个新的Application中,没有之前操作存储的数据了)
    出现崩溃

解决办法

从Application获取数据的时候使用空判断,只能防止不崩溃,数据还是获取不到

                userInfo = ((XApp) getApplicationContext()).getUserinfo();
                if (null != userInfo) {
                    // do something
                }

使用页面数据传递使用Intent携带,不再从全局变量里面获取(推荐

可以解决问题,建议新项目这样做,但是项目如果已经上线,重构这一块问题稍显麻烦

public class MainActivity extends Activity {

    private Button btnShowUserInfo;
    private UserInfoBean userInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //从getIntent中获取
        userInfo = (UserInfoBean) getIntent().getSerializableExtra("bean");
        setContentView(R.layout.activity_main);
        btnShowUserInfo = (Button) findViewById(R.id.btnShowUserInfo);
        btnShowUserInfo.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {

                Toast.makeText(getApplicationContext(), userInfo.toString(),
                        Toast.LENGTH_LONG).show();
            }
        });
    }

    //定义给,外部调用启动MainActivity
    public static void actionStart(Context context, UserInfoBean bean) {
        Intent intent = new Intent(context, MainActivity.class);
        intent.putExtra("bean", bean);
        context.startActivity(intent);
    }
}

把对象序列化到本地,如果为空再从本地读出来

1.创建对象存储和读取工具类

public class StreamUtil {
    public static final void saveObject(String path, Object saveObject) {
        FileOutputStream fOps = null;
        ObjectOutputStream oOps = null;
        File file = new File(path);
        try {
            fOps = new FileOutputStream(file);
            oOps = new ObjectOutputStream(fOps);
            oOps.writeObject(saveObject);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseUtils.close(oOps);
            CloseUtils.close(fOps);
        }
    }

    public static final Object restoreObject(String path) {
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        Object obj = null;
        File file = new File(path);
        if (!file.exists()) {
            return null;
        }
        try {
            fis = new FileInputStream(file);
            ois = new ObjectInputStream(fis);
            obj = ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseUtils.close(fis);
            CloseUtils.close(ois);
        }
        return obj;

    }

    static class CloseUtils {
        public static void close(Closeable stream) {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.对象保存

/** 用户信息 */
public class UserInfoBean implements Serializable {
    public static final String TAG = "UserInfoBean";
    private static final long serialVersionUID = 1L;
    private String name;
    private String tel;
    private String token;

    public UserInfoBean(String name, String tel, String token) {
        super();
        this.name = name;
        this.tel = tel;
        this.token = token;
        save();
    }

    private void save() {
        StreamUtil.saveObject(XApp.getCacheFile() + TAG, this);
    }

    // App退出的时候,清空本地存储的对象,否则下次使用的时候还会存有上次遗留的数据
    public void reset() {
        this.name = null;
        this.tel = null;
        this.token = null;
        save();
    }
}

3.从Application中读取

public class XApp extends Application {
    private UserInfoBean userinfo;

    /** 因为每次App被回收重建的时候都会执行onCreate方法,mContext对象永远不会为空 */
    public static XApp mContext;

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = this;
    }

    public UserInfoBean getUserinfo() {
        // 从本地读取
        if (null == userinfo) {
            userinfo = (UserInfoBean) StreamUtil.restoreObject(getCacheFile()
                    + UserInfoBean.TAG);
        }
        return userinfo;
    }

    public void setUserinfo(UserInfoBean userinfo) {
        this.userinfo = userinfo;
    }

    public static String getCacheFile() {
        return mContext.getCacheDir().getAbsolutePath();
    }
}

注意事项

1.App退出的时候需要执行,UserInfoBean的reset方法清除存储的数据,否则下次进入App的时候,可能会得到上次遗留下的脏数据
2.在使用userInfo的时候还是需要加上空判断,因为还是会存在userInfo为空,从本地磁盘读取同样为空的情况

userInfo = ((XApp) getApplicationContext()).getUserinfo();
                if (userInfo != null) {
                    Toast.makeText(getApplicationContext(),
                            userInfo.toString(), Toast.LENGTH_LONG).show();
                }

3.如果使用UserInfoBean的set方法修改数据,修改后需要同步本地存储的数据

    public void setName(String name) {
        this.name = name;
        save();
    }

    public void setTel(String tel) {
        this.tel = tel;
        save();
    }

    public void setToken(String token) {
        this.token = token;
        save();
    }

重构代码

不足

  1. 代码混乱,在UserInfoBean类中操作数据,在Application类中仍然操作读取数据,显得冗余。reset方法放在Application类显得冗余,放在具体对象实体类中又不容易查找,不符合面向对象开发的-单一职责原则。考虑设计一个单例的全局变量类统一操作这一类的数据
  2. 对象从序列化和反序列化是一个磁盘操作,现在每次修改对象数据都会进行一次这样的操作,磁盘操作本身就存在风险,多次操作风险变高了。
  3. 对于不支持序列化数据格式如HashMap

重构代码

/** * 保存全局对象的单例 */
public class SaveInstance implements Serializable, Cloneable {

    public final static String TAG = "SaveInstance";
    private static final long serialVersionUID = 1L;

    private static SaveInstance instance;

    public static SaveInstance getInstance() {
        if (null == instance) {
            Object obj = StreamUtil.restoreObject(XApp.getCacheFile() + TAG);
            if (null == obj) {
                obj = new SaveInstance();
                StreamUtil.saveObject(XApp.getCacheFile() + TAG, obj);
            }
            instance = (SaveInstance) obj;
        }
        return instance;
    }

    private UserInfoBean userInfo;
    private String title;
    private HashMap<String, Object> map;

    public UserInfoBean getUserInfo() {
        return userInfo;
    }

    public String getTitle() {
        return title;
    }

    public HashMap<String, Object> getMap() {
        return map;
    }

    /** 是否需要保存到本地 */
    public void setUserInfo(UserInfoBean userInfo, boolean needSave) {
        this.userInfo = userInfo;
        if (needSave) {
            save();
        }
    }

    public void setTitle(String title, boolean needSave) {
        this.title = title;
        if (needSave) {
            save();
        }
    }

    /** * 把不支持序列化的对象转换成String类型存储 */
    public void setMap(HashMap<String, Object> map, boolean needSave) {
        this.map = new HashMap<String, Object>();
        if (null == map) {
            StreamUtil.saveObject(XApp.getCacheFile() + TAG, this);
            return;
        }
        Set set = map.entrySet();
        Iterator it = set.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry) it.next();
            this.map.put(String.valueOf(entry.getKey()),
                    String.valueOf(entry.getValue()));
        }
        if (needSave) {
            save();
        }
    }

    private void save() {
        StreamUtil.saveObject(XApp.getCacheFile() + TAG, this);
    }

    // App退出的时候,清空本地存储的对象,否则下次使用的时候还会存有上次遗留的数据
    public void reset() {
        this.userInfo = null;
        this.title = null;
        this.map = null;
        save();
    }

    // -----------以下3个方法用于序列化-----------------
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    // 保证单例序列化后不产生新对象
    public SaveInstance readResolve() throws ObjectStreamException,
            CloneNotSupportedException {
        instance = (SaveInstance) this.clone();
        return instance;
    }

    private void readObject(ObjectInputStream ois) throws IOException,
            ClassNotFoundException {
        ois.defaultReadObject();
    }
}

后序

  • 使用这种方式一定程度上可以解决已有代码出现,App后台回收引发空指针异常的问题,但是这个方式解决的核心是使用磁盘操作,很容易引发ANR,这始终是一个那么可靠的临时方案
  • 使用了单例模式,那么在序列化的时候就应该实现Cloneable接口,加入readResolve,readObject,clone方法。不然在反序列化的时候回来得对象和原来的对象不是同个对象
  • 代码显得臃肿难看

demo下载地址

参考资料:《App研发录》

你可能感兴趣的:(异常,全局变量,空指针异常,应用崩溃,全局变量存储)