轉載請注明出處:http://blog.csdn.net/a740169405/article/details/69668957
簡述:
寫這篇文章的緣由是最近項目中查內存泄漏時,發現最終原因是由于異步線程調用View的的post方法導致的。
為何我會使用異步線程調用View的post方法,是因為項目中需要用到很多復雜的自定義布局,需要提前解析進入內存,防止在主線程解析導致卡頓,具體的實現方法是在Application啟動的時候,使用異步線程解析這些布局,等需要使用的時候直接從內存中拿來用。
造成內存泄漏的原因,需要先分析View的post方法執行流程,也就是文章前半部分的內容
文章內容:
- View#post方法作用以及實現源碼
- View#post與Handler#post的區別
- 分析View#post方法導致的內存泄漏
post方法分析
看看View的post方法注釋:
Causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread
意思是將runnable加入到消息隊列中,該runnable將會在用戶界面線程中執行,也就是UI線程。這解釋,和Handler的作用差不多,然而事實并非如此。
再看看post方法的源碼:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 如果當前View加入到了window中,直接調用UI線程的Handler發送消息
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
// View未加入到window,放入ViewRootImpl的RunQueue中
ViewRootImpl.getRunQueue().post(action);
return true;
}
分兩種情況,當View已經attach到window,直接調用UI線程的Handler發送runnable。如果View還未attach到window,將runnable放入ViewRootImpl的RunQueue中。
那么post到RunQueue里的runnable什么時候執行呢,又是為何當View還沒attach到window的時候,需要post到RunQueue中。
View#post與Handler#post的區別
其實,當View已經attach到了window,兩者是沒有區別的,都是調用UI線程的Handler發送runnable到MessageQueue,最后都是由handler進行消息的分發處理。
但是如果View尚未attach到window的話,runnable被放到了ViewRootImpl#RunQueue中,最終也會被處理,但不是通過MessageQueue。
ViewRootImpl#RunQueue源碼注釋如下:
/**
* The run queue is used to enqueue pending work from Views when no Handler is
* attached. The work is executed during the next call to performTraversals on
* the thread.
* @hide
*/
大概意思是當視圖樹尚未attach到window的時候,整個視圖樹是沒有Handler的(其實自己可以new,這里指的handler是AttachInfo里的),這時候用RunQueue來實現延遲執行runnable任務,并且runnable最終不會被加入到MessageQueue里,也不會被Looper執行,而是等到ViewRootImpl的下一個performTraversals時候,把RunQueue里的所有runnable都拿出來并執行,接著清空RunQueue。
由此可見RunQueue的作用類似于MessageQueue,只不過,這里面的所有
runnable最后的執行時機,是在下一個performTraversals到來的時候,MessageQueue里的消息處理的則是下一次loop到來的時候。RunQueue源碼:
static final class RunQueue {
// 存放所有runnable,HandlerAction是對runnable的包裝對象
private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();
// view沒有attach到window的時候,View#post最終調用到這
void post(Runnable action) {
postDelayed(action, 0);
}
// view沒有attach到window的時候,View#postDelay最終調用到這
void postDelayed(Runnable action, long delayMillis) {
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;
synchronized (mActions) {
mActions.add(handlerAction);
}
}
// 移除一個runnable任務,
// view沒有attach到window的時候,View#removeCallbacks最終調用到這
void removeCallbacks(Runnable action) {
final HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
while (actions.remove(handlerAction)) {
// Keep going
}
}
}
// 取出所有的runnable并執行,接著清空RunQueue集合
void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}
// 對runnable的封裝類,記錄runnable以及delay時間
private static class HandlerAction {
Runnable action;
long delay;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HandlerAction that = (HandlerAction) o;
return !(action != null ? !action.equals(that.action) : that.action != null);
}
@Override
public int hashCode() {
int result = action != null ? action.hashCode() : 0;
result = 31 * result + (int) (delay ^ (delay >>> 32));
return result;
}
}
}
再看看RunQueue里的消息處理位置,ViewRootImpl#performTraversals:
private void performTraversals() {
// ....
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
// ....
}
也就是說,當View沒有被attach到window的時候,最后runnable的處理不是通過MessageQueue,而是ViewRootImpl自己在下一個performTraversals到來的時候執行。
為了驗證RunQueue里的runnable是在下一個performTraversals到來的時候執行的,做一個測試(在Activity的onCreate方法中):
// Activity的跟布局
ViewGroup viewGroup = (ViewGroup) getWindow().getDecorView();
// 自己new的一個View,等待attach到window中
final View view = new View(getApplicationContext()) {
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// view執行了layout
Log.e(TAG, "view layout");
}
};
// 在View未attach到window上之前,
// 使用Handler#post發送一個runnable(最終到了MessageQueue中)
mHandler.post(new Runnable() {
@Override
public void run() {
// 獲取View的寬高,查看View是否已經layout
Log.e(TAG, "MessageQueue runnable, view width = " + view.getWidth() + " height = " + view.getHeight());
}
});
// 在View未attach到window上之前,
// 使用View#post發送一個runnable(最終到了ViewRootImpl#RunQueue中)
view.post(new Runnable() {
@Override
public void run() {
// 獲取View的寬高,查看View是否已經layout
Log.e(TAG, "RunQueue runnable, view width = " + view.getWidth() + " height = " + view.getHeight());
}
});
// 將view添加到window中
viewGroup.addView(view);
Log:
打印出來的日志說明:
- 使用handler#post的runnable最先執行,此時View還未layout,無法獲取view的寬高。
- 接著view的onLayout方法執行,表示view完成了位置的布置,此時可以獲取寬高。
- view#post的runnable最后執行,也就是說view已經layout完成才執行,此時能夠獲取View的寬高。
這里提一下,下一次performTraversals到來的時候,View可能attach到了window上,也可能未attach到window上,也就是代碼最后不執行addView動作,使用view#post的runnable仍然無法獲取View的寬高,修改如下:
// viewGroup.addView(view);
Log:
我們經常碰到一個問題,就是new一個View之后,通過addView添加到視圖樹或者是在Activity的onCreate方法里調用setContentView方法。緊接著,我們想獲取View的寬高,但是因為view的measure和layout都還未執行,所以是獲取不到寬高的。
view#post的一個作用是,在下一個performTraversals到來的時候,也就是view完成layout之后的第一時間獲取寬高。
View#post方法導致的內存泄漏
分析泄漏之前需要查看ViewRootImpl里的RunQueue成員變量定義以及創建過程:
// 用ThreadLocal對象來保存ViewRootImpl的RunQueue實例
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
// 如果當前線程沒有創建RunQueue實例,創建并保存在sRunQueues中
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}
首先這里的ThreadLocal內部持有的實例是線程單利的,也就是不同的線程調用sRunQueues.get()得到的不是同一個對象。
ViewRootImpl使用ThreadLocal來保存RunQueue實例,一般來說,ViewRootImpl#getRunQueue都是在UI線程使用,所以RunQueue實例只有一個。UI線程的對象引用關系:
UIThread是應用程序啟動的時候,新建的一個線程,生命周期與應用程序一致,也就是說UI線程對應的RunQueue實例是無法被回收的,但是無所謂,因為每次ViewRootImpl#performTraversals方法被調用時都會把RunQueue里的所有Runnable對象執行并清除。
接著,如果是異步線程調用了View#post方法:
new Thread(new Runnable() {
@Override
public void run() {
new View(getApplicationContext()).post(new Runnable() {
@Override
public void run() {
}
});
}
}).start();
這里的的對象引用關系:
這里定義的Thread只是一個臨時對象,并沒有被GC-Root持有,是可以被垃圾回收器回收的,那么我們post出去的Runnable只是不會被執行而已,最后還是會被回收,并不會造成內存泄漏。
但是如果,這個Thread是一個靜態變量的話,那么我們使用異步線程post出去的Runnable也就泄漏了,如果這些runnable又引用了View對象或者是Activity對象,就會造成更大范圍的泄漏。
雖然,Thread被定義成靜態變量的情況很少出現。但是線程池被定義成靜態變量卻常常出現,例如我們應用程序中,經常會定義一些靜態線程池對象用來實現線程的復用,比如下面的這個線程池管理類GlobalThreadPool:
public class GlobalThreadPool {
private static final int SIZE = 3;
private static ScheduledExecutorService mPool;
public static ScheduledExecutorService getGlobalThreadPoolInstance() {
if (mPool == null) {
synchronized (GlobalThreadPool.class) {
if (mPool == null) {
mPool = Executors.newScheduledThreadPool(SIZE);
}
}
}
return mPool;
}
/**
* run a thead ,== new thread
*/
public static void startRunInThread(Runnable doSthRunnable) {
getGlobalThreadPoolInstance().execute(doSthRunnable);
}
}
接著再把異步處理調用View#post的代碼改改:
GlobalThreadPool.startRunInThread(new Runnable() {
@Override
public void run() {
new View(MainActivity.this).post(new Runnable() {
@Override
public void run() {
}
});
}
});
這樣的話,對象引用關系就變成了:
導出的heap文件hprof查看對象引用關系:
最后,回到文章開頭簡述中說的,項目中使用異步線程解析布局文件,當解析的布局文件的時候,如果布局文件中包含TextView,這時候,android系統4.4-5.2的機器,就會出現內存泄漏,具體為什么往下看。
- TextView的構造方法調用用了setText方法。
- setText方法又調用了notifyViewAccessibilityStateChangedIfNeeded方法。
- notifyViewAccessibilityStateChangedIfNeeded方法又創建了一個SendViewStateChangedAccessibilityEvent對象,緊接著又調用了SendViewStateChangedAccessibilityEvent對象的runOrPost方法。
- runOrPost方法最終又調用了View的post方法。
上面這一大串流程,導致的結果就是異步線程調用了View的post方法,如果這里的線程是核心線程,也就是一直會存在于線程池中的線程,并且線程池又是靜態的,就導致使用異步線程創建多個TextView相當于是往異步線程的RunQueue中加入多個Runnable,而Runable又引用了View,導致View的泄漏。
泄漏的對象引用關系和上面主動調用View的post方法類似。
至于為什么4.4-5.2的機器才會泄漏,是因為4.4-5.2的系統,View中notifyViewAccessibilityStateChangedIfNeeded方法并沒有判斷View是否attach到了window,直到google發布的android_6.0系統才修復該問題,該問題可以說是google的問題,因為google官方在Support_v4包中就提供了異步線程加載布局文件的框架,具體參閱:android.support.v4.view.AsyncLayoutInflater
傳送門:https://developer.android.com/reference/android/support/v4/view/AsyncLayoutInflater.html
總結:
- 當View已經attach到window,不管什么線程, 調用View#post 和 調用Handler#post效果一致
- 當View尚未attach到window,主線程調用View#post發送的runnable將在下一次performTraversals到來時執行,而非主線程調用View#post發送的runnable將無法被執行。
- 可以通過在主線程調用View#post發送runnable來獲取下一次performTraversals時視圖樹中View的布局信息,如寬高。
- 如果調用View#post方法的線程對象被GC-Root引用,則發送的runnable將會造成內存泄漏。
更新(2019年09月17日)
如果需要解決該問題,可以通過反射來置空的方式解決,但是置空代碼需要創建View的子線程執行,這里需要特別注意。
/**
* 切記此方法需要在創建View的子線程中調用
*/
private void resolveLeak() {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
// 主線程不需要處理
return;
}
try {
Class<?> viewRootImpl = Class.forName("android.view.ViewRootImpl");
Field sRunQueuesField = viewRootImpl.getDeclaredField("sRunQueues");
if (sRunQueuesField != null) {
sRunQueuesField.setAccessible(true);
ThreadLocal threadLocal = (ThreadLocal) sRunQueuesField.get(viewRootImpl);
if (threadLocal != null) {
threadLocal.set(null);
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}