避免與監聽器洩漏活動
如果在 Activity 中實現或建立偵聽器,請始終注意具有偵聽器註冊的物件的生命週期。
考慮一個應用程式,我們在使用者登入或登出時有幾個不同的活動/片段。這樣做的一種方法是擁有一個可以訂閱的 UserController
的單例例項,以便在使用者狀態發生變化時得到通知:
public class UserController {
private static UserController instance;
private List<StateListener> listeners;
public static synchronized UserController getInstance() {
if (instance == null) {
instance = new UserController();
}
return instance;
}
private UserController() {
// Init
}
public void registerUserStateChangeListener(StateListener listener) {
listeners.add(listener);
}
public void logout() {
for (StateListener listener : listeners) {
listener.userLoggedOut();
}
}
public void login() {
for (StateListener listener : listeners) {
listener.userLoggedIn();
}
}
public interface StateListener {
void userLoggedIn();
void userLoggedOut();
}
}
然後有兩個活動,SignInActivity
:
public class SignInActivity extends Activity implements UserController.StateListener{
UserController userController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.userController = UserController.getInstance();
this.userController.registerUserStateChangeListener(this);
}
@Override
public void userLoggedIn() {
startMainActivity();
}
@Override
public void userLoggedOut() {
showLoginForm();
}
...
public void onLoginClicked(View v) {
userController.login();
}
}
而 MainActivity
:
public class MainActivity extends Activity implements UserController.StateListener{
UserController userController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.userController = UserController.getInstance();
this.userController.registerUserStateChangeListener(this);
}
@Override
public void userLoggedIn() {
showUserAccount();
}
@Override
public void userLoggedOut() {
finish();
}
...
public void onLogoutClicked(View v) {
userController.logout();
}
}
此示例會發生的情況是,每次使用者登入然後再次登出時,都會洩漏 MainActivity
例項。洩漏的發生是因為在 UserController#listeners
中有對活動的引用。
請注意: 即使我們使用匿名內部類作為偵聽器,該活動仍會洩漏:
...
this.userController.registerUserStateChangeListener(new UserController.StateListener() {
@Override
public void userLoggedIn() {
showUserAccount();
}
@Override
public void userLoggedOut() {
finish();
}
});
...
活動仍然會洩漏,因為匿名內部類具有對外部類的隱式引用(在本例中為活動)。這就是為什麼可以從內部類呼叫外部類中的例項方法的原因。實際上,沒有引用外部類的唯一型別的內部類是靜態內部類。
簡而言之,非靜態內部類的所有例項都包含對建立它們的外部類的例項的隱式引用。
解決這個問題有兩種主要方法,可以通過新增一個從 UserController#listeners
中刪除偵聽器的方法,或者使用 WeakReference
來儲存偵聽器的引用。
備選方案 1:刪除偵聽器
讓我們從建立一個新方法 removeUserStateChangeListener(StateListener listener)
開始:
public class UserController {
...
public void registerUserStateChangeListener(StateListener listener) {
listeners.add(listener);
}
public void removeUserStateChangeListener(StateListener listener) {
listeners.remove(listener);
}
...
}
然後讓我們在 activity 的 onDestroy
方法中呼叫這個方法:
public class MainActivity extends Activity implements UserController.StateListener{
...
@Override
protected void onDestroy() {
super.onDestroy();
userController.removeUserStateChangeListener(this);
}
}
通過此修改,當使用者登入和登出時,MainActivity
的例項不再洩露。但是,如果文件不清楚,那麼下一個開始使用 UserController
的開發人員可能會錯過在活動被銷燬時需要取消註冊偵聽器,這導致我們採用第二種方法來避免這些型別的洩漏。
備選方案 2:使用弱引用
首先,讓我們首先解釋一下弱引用是什麼。顧名思義,弱引用對物件持有弱引用。與作為強引用的普通例項欄位相比,弱引用不會阻止垃圾收集器 GC 移除物件。在上面的例子中,如果 UserController
使用了 WeakReference
來引用聽眾,那麼這將允許 MainActivity
在被破壞後進行垃圾收集。
簡而言之,弱引用告訴 GC,如果沒有其他人對此物件有強引用,請繼續將其刪除。
讓我們修改 UserController
使用列表 WeakReference
來跟蹤它的聽眾:
public class UserController {
...
private List<WeakReference<StateListener>> listeners;
...
public void registerUserStateChangeListener(StateListener listener) {
listeners.add(new WeakReference<>(listener));
}
public void removeUserStateChangeListener(StateListener listenerToRemove) {
WeakReference referencesToRemove = null;
for (WeakReference<StateListener> listenerRef : listeners) {
StateListener listener = listenerRef.get();
if (listener != null && listener == listenerToRemove) {
referencesToRemove = listenerRef;
break;
}
}
listeners.remove(referencesToRemove);
}
public void logout() {
List referencesToRemove = new LinkedList();
for (WeakReference<StateListener> listenerRef : listeners) {
StateListener listener = listenerRef.get();
if (listener != null) {
listener.userLoggedOut();
} else {
referencesToRemove.add(listenerRef);
}
}
}
public void login() {
List referencesToRemove = new LinkedList();
for (WeakReference<StateListener> listenerRef : listeners) {
StateListener listener = listenerRef.get();
if (listener != null) {
listener.userLoggedIn();
} else {
referencesToRemove.add(listenerRef);
}
}
}
...
}
通過這種修改,是否刪除了偵聽器並不重要,因為 UserController
沒有對任何偵聽器的強引用。但是,每次編寫此樣板程式碼都很麻煩。因此,讓我們建立一個名為 WeakCollection
的泛型類:
public class WeakCollection<T> {
private LinkedList<WeakReference<T>> list;
public WeakCollection() {
this.list = new LinkedList<>();
}
public void put(T item){
//Make sure that we don't re add an item if we already have the reference.
List<T> currentList = get();
for(T oldItem : currentList){
if(item == oldItem){
return;
}
}
list.add(new WeakReference<T>(item));
}
public List<T> get() {
List<T> ret = new ArrayList<>(list.size());
List<WeakReference<T>> itemsToRemove = new LinkedList<>();
for (WeakReference<T> ref : list) {
T item = ref.get();
if (item == null) {
itemsToRemove.add(ref);
} else {
ret.add(item);
}
}
for (WeakReference ref : itemsToRemove) {
this.list.remove(ref);
}
return ret;
}
public void remove(T listener) {
WeakReference<T> refToRemove = null;
for (WeakReference<T> ref : list) {
T item = ref.get();
if (item == listener) {
refToRemove = ref;
}
}
if(refToRemove != null){
list.remove(refToRemove);
}
}
}
現在讓我們重新編寫 UserController
來代替使用 WeakCollection<T>
:
public class UserController {
...
private WeakCollection<StateListener> listenerRefs;
...
public void registerUserStateChangeListener(StateListener listener) {
listenerRefs.put(listener);
}
public void removeUserStateChangeListener(StateListener listenerToRemove) {
listenerRefs.remove(listenerToRemove);
}
public void logout() {
for (StateListener listener : listenerRefs.get()) {
listener.userLoggedOut();
}
}
public void login() {
for (StateListener listener : listenerRefs.get()) {
listener.userLoggedIn();
}
}
...
}
如上面的程式碼示例所示,WeakCollection<T>
刪除了使用 WeakReference
而不是普通列表所需的所有樣板程式碼。最重要的是:如果錯過了對 UserController#removeUserStateChangeListener(StateListener)
的呼叫,則監聽器及其引用的所有物件都不會洩漏。