问题描述
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 + "]";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
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);
}
});
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
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));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
情景重现
模拟切换到后台,App进程被系统回收的场景
- 开启应用,进入登录页,登录成功跳转到主页
- 按Home键退出应用
- 使用DDMS-Stop Process结束进程
- 回到应用中,正常使用(注:现在处于一个新的Application中,没有之前操作存储的数据了)
出现崩溃
解决办法
从Application获取数据的时候使用空判断,只能防止不崩溃,数据还是获取不到
userInfo = ((XApp) getApplicationContext()).getUserinfo();
if (null != userInfo) {
}
使用页面数据传递使用Intent携带,不再从全局变量里面获取(推荐)
可以解决问题,建议新项目这样做,但是项目如果已经上线,重构这一块问题稍显麻烦
public class MainActivity extends Activity {
private Button btnShowUserInfo;
private UserInfoBean userInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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();
}
});
}
public static void actionStart(Context context, UserInfoBean bean) {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("bean", bean);
context.startActivity(intent);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
把对象序列化到本地,如果为空再从本地读出来
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();
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
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);
}
public void reset() {
this.name = null;
this.tel = null;
this.token = null;
save();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
注意事项
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);
}
public void reset() {
this.userInfo = null;
this.title = null;
this.map = null;
save();
}
@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();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
后序
- 使用这种方式一定程度上可以解决已有代码出现,App后台回收引发空指针异常的问题,但是这个方式解决的核心是使用磁盘操作,很容易引发ANR,这始终是一个那么可靠的临时方案
- 使用了单例模式,那么在序列化的时候就应该实现Cloneable接口,加入readResolve,readObject,clone方法。不然在反序列化的时候回来得对象和原来的对象不是同个对象
- 代码显得臃肿难看
参考资料:《App研发录》