1. 程式人生 > >Android-Application被回收引發空指標異常分析(消滅全域性變數)

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方法。不然在反序列化的時候回來得物件和原來的物件不是同個物件
  • 程式碼顯得臃腫難看

參考資料:《App研發錄》