Android-Application被回收引發空指標異常分析(消滅全域性變數)
阿新 • • 發佈:2018-12-30
問題描述
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方法。不然在反序列化的時候回來得物件和原來的物件不是同個物件
- 程式碼顯得臃腫難看