您的位置:首页 > 移动开发

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

2016-05-19 18:39 411 查看

问题描述

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进程被系统回收的场景

开启应用,进入登录页,登录成功跳转到主页

按Home键退出应用

使用DDMS-Stop Process结束进程

回到应用中,正常使用(注:现在处于一个新的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();
}


重构代码

不足

代码混乱,在UserInfoBean类中操作数据,在Application类中仍然操作读取数据,显得冗余。reset方法放在Application类显得冗余,放在具体对象实体类中又不容易查找,不符合面向对象开发的-单一职责原则。考虑设计一个单例的全局变量类统一操作这一类的数据

对象从序列化和反序列化是一个磁盘操作,现在每次修改对象数据都会进行一次这样的操作,磁盘操作本身就存在风险,多次操作风险变高了。

对于不支持序列化数据格式如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研发录》

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息