Unity 打包APK 閃退:不能在子線程中調用**方法

問題出現情景如下:

在接入廣告sdk的時候,調用播放廣告的時候,判斷廣告已經預加載成功,此時去播放廣告的時候程序閃退,報錯如下

在查閱相關資料後,得知更新UI是要主線程來更新的,即UI線程更新。如果在主線線程之外的線程中直接更新頁面顯示常會報錯。拋出異常:android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
解決辦法有多種,我採用的是runOnUiThread();

代碼如下

public void Show(String codeId){
        wangmengID = codeId;
        if(mttRewardVideoAd != null){
            System.out.println(MainActivity.TAG + "穿山甲/" + "視頻可播放");
            mttRewardVideoAd.setShowDownLoadBar(true);
            mttRewardVideoAd.setRewardAdInteractionListener(m_adlistener);
            //在播放廣告的時候會刷新界面,刷新UI只能在主線程中進行,不然會閃退
            //所以在runOnUiThread中播放廣告
            UnityPlayer.currentActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mttRewardVideoAd.showRewardVideoAd(UnityPlayer.currentActivity);
                    mttRewardVideoAd = null;
                }
            });
        }
}

具體原理在網上有很多相關資料:

自己從別處摘抄過來防止在遇到這種情況

---------------------------------------------------------------------------------------------------------------------------------------------------------------------

更新UI是要主線程來更新的,即UI線程更新。如果在主線線程之外的線程中直接更新頁面顯示常會報錯。拋出異常:android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Activity.runOnUiThread(Runnable)把更新ui的代碼創建在Runnable中,然後在需要更新ui時,把這個Runnable對象傳給Activity.runOnUiThread(Runnable)。 這樣Runnable對像就能在ui程序中被調用。如果當前線程是UI線程,那麼行動是立即執行。如果當前線程不是UI線程,操作是發佈到事件隊列的UI線程

    public static void showToastSafe(final Activity activity, 
            final String text, final int duration) {
        // 方法1 activity.runOnUiThread
        activity.runOnUiThread(new Runnable() {
            
            @Override
            public void run() {
                Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
            }
        });
    }
 

handler.post
    public static void showToastSafe(final Context context, final String text) {
        // 方法2  handler.post  內部其實是做handler的流程  sendMsg之類的  功能是一樣的  調用更簡練
        new Handler().post(new Runnable() {
            
            @Override
            public void run() {
                Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
            }
        });
    }
 

imageview.postDelayed(new Runnable() {  

        @Override  
        public void run() {  
            Intent mIntent = new Intent(MainActivity.this,  
                    SecondActivity.class);  
            startActivity(mIntent);  
            finish();  

        }  
    }, 2000); 1
 

1,如果post方法是handler的,則Runnable執行在handler依附線程中,可能是主線程,也可能是其他線程。
2,如果post方法是View的,則一定是運行在主線程中的,因爲所有view都自帶一個handler,所有handler都有post方法,所以它的Runnable是運行在主線程中的
先從最簡單的runOnUiThread()來看源碼:

 public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }

這是runOnUiThread()方法的源碼,從源碼可以看到,當一個Runnable進來時,判斷當前線程是否是主線程,如果是主線程直接調用其run()方法了,不是主線程則調用handler的post()回到了handler。不管是onResume還是OnCreate裏調用runOnUiThread()只要是主線程中調用,其Runnable的run()方法立刻就執行。

再看一下view.post()的源碼:

public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().post(action);
        return true;
    }

它先判斷了一個叫attachInfo是不是爲null,如果不是null,調用了Handler的post()方法回到了handler,那麼mAttachInfo是一個什麼東東呢?下面我們再看一段代碼:

/**
     * Returns true if this view is currently attached to a window.
     */
    public boolean isAttachedToWindow() {
        return mAttachInfo != null;
    }ViewRootImpl

如上mAttachInfo是用來判斷是否attached到window的,這裏留一個疑問即view什麼時候attached到window上的呢?當爲null的時候調用ViewRootImpl.getRunQueue().post(action),那麼它的調用時機是什麼呢?通過跟蹤源碼可以得到在ViewRootImpl的一系列方法中,比如draw(boolean)等。而ViewRootImpl.getRunQueue().post(action)的處理action是handler.postDelayed(handlerAction.action, handlerAction.delay)最後也迴歸到了handler。

拋開runOnUiThread不說因爲上面已經說了該方法在主線程立刻會執行的,Handler().post()是在生命週期onResume之後執行的,而view.post()是在onAttachedWindow之後執行,也就是說ViewRootImpl.getRunQueue().post(action)是在onAttachedWindow之後執行的。由此我們得出一個結論,當程序啓動一個activity時,OnCreate、onStart、onResume任務都添加到了主線程Looper的messageQueue中,在這個三個生命週期使用handler.post()都添加到messageQueue隊列尾部,等待執行。而View.post(),最終也會添加到messageQueue隊列中,等待onAttachedToWindow執行之後執行。

當在onResume之後點擊某一button時打印的日誌,由此看見當前麪條件都滿足時,在調用這三個post()方法時,都添加到messageQueue中,在每一個任務量小的時基本是同時執行的。那麼我們就可以得出一個結論,在界面繪製成功以後再調用這三個方法時,當在子線程中調用時其效果是一樣的,當在主線程中runOnUiThread是立刻執行該任務,而其他兩個是加載到messageQueue隊尾,當前面任務全部執行完畢再執行。

 

參考資料:

view.post和Handler.post區別:

https://blog.csdn.net/johnlee175/article/details/52369173

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

activity_main.xml:

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
    <TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>
MainActivity:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final View view = findViewById(R.id.text_view);
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            System.err.println(Thread.currentThread() + " + Activity.runOnUiThread(Runnable)");
        }
    });
    if (view != null) {
        view.post(new Runnable() {
            @Override
            public void run() {
                System.err.println(Thread.currentThread() + " + View.post(Runnable)");
            }
        });
    }
}


自定義TextView覆寫的”生命週期”回調方法:

onAttachedToWindow();
onDetachedFromWindow();
onWindowVisibilityChanged(int visibility);
onWindowFocusChanged(boolean hasWindowFocus);
onVisibilityChanged(View changedView, int visibility);
onFinishInflate();
onLayout(boolean changed, int left, int top, int right, int bottom);
onMeasure(int widthMeasureSpec, int heightMeasureSpec);
onDraw(Canvas canvas);
onSizeChanged(int w, int h, int oldw, int oldh);


下面是日誌: 
launch:

after first onMeasure, getMeasuredWidth() and getMeasuredHeight() worked;
onSizeChanged or after, getMeasuredWidth()/getMeasuredHeight()/getWidth()/getHeight() worked, until after onDetachedFromWindow;
08-30 12:33:07.680 9816-9816/com.test W/System.err: onFinishInflate
08-30 12:33:07.681 9816-9816/com.test W/System.err: Thread[main,5,main] + Activity.runOnUiThread(Runnable)
08-30 12:33:07.708 9816-9816/com.test W/System.err: onAttachedToWindow
08-30 12:33:07.708 9816-9816/com.test W/System.err: onWindowVisibilityChanged
08-30 12:33:07.708 9816-9816/com.test W/System.err: onVisibilityChanged
08-30 12:33:07.712 9816-9816/com.test W/System.err: onMeasure
08-30 12:33:07.771 9816-9816/com.test W/System.err: onMeasure
08-30 12:33:07.771 9816-9816/com.test W/System.err: onSizeChanged
08-30 12:33:07.771 9816-9816/com.test W/System.err: onLayout
08-30 12:33:07.802 9816-9816/com.test W/System.err: Thread[main,5,main] + View.post(Runnable)
08-30 12:33:07.802 9816-9816/com.test W/System.err: onWindowFocusChanged
08-30 12:33:07.804 9816-9816/com.test W/System.err: onMeasure
08-30 12:33:07.804 9816-9816/com.test W/System.err: onLayout
08-30 12:33:07.804 9816-9816/com.test W/System.err: onDraw


home:

08-30 12:35:38.628 9816-9816/com.test W/System.err: onWindowFocusChanged
08-30 12:35:38.747 9816-9816/com.test W/System.err: onWindowVisibilityChanged
08-30 12:35:38.989 9816-9816/com.test W/System.err: onVisibilityChanged



re-foreground:

08-30 12:35:59.959 9816-9816/com.test W/System.err: onVisibilityChanged
08-30 12:35:59.967 9816-9816/com.test W/System.err: onWindowVisibilityChanged
08-30 12:35:59.988 9816-9816/com.test W/System.err: onWindowFocusChanged
08-30 12:36:00.001 9816-9816/com.test W/System.err: onDraw


back:

08-30 12:36:25.576 9816-9816/com.test W/System.err: onWindowFocusChanged
08-30 12:36:25.668 9816-9816/com.test W/System.err: onWindowVisibilityChanged
08-30 12:36:25.678 9816-9816/com.test W/System.err: onDetachedFromWindow


re-launch or rotate-screen:

08-30 12:36:50.175 9816-9816/com.test W/System.err: onFinishInflate
08-30 12:36:50.175 9816-9816/com.test W/System.err: Thread[main,5,main] + Activity.runOnUiThread(Runnable)
08-30 12:36:50.182 9816-9816/com.test W/System.err: onAttachedToWindow
08-30 12:36:50.182 9816-9816/com.test W/System.err: onWindowVisibilityChanged
08-30 12:36:50.182 9816-9816/com.test W/System.err: onVisibilityChanged
08-30 12:36:50.183 9816-9816/com.test W/System.err: onMeasure
08-30 12:36:50.219 9816-9816/com.test W/System.err: onMeasure
08-30 12:36:50.220 9816-9816/com.test W/System.err: onSizeChanged
08-30 12:36:50.220 9816-9816/com.test W/System.err: onLayout
08-30 12:36:50.222 9816-9816/com.test W/System.err: Thread[main,5,main] + View.post(Runnable)
08-30 12:36:50.222 9816-9816/com.test W/System.err: onWindowFocusChanged
08-30 12:36:50.232 9816-9816/com.test W/System.err: onMeasure
08-30 12:36:50.232 9816-9816/com.test W/System.err: onLayout
08-30 12:36:50.232 9816-9816/com.test W/System.err: onDraw


lock-screen or show-status-panel:

08-30 12:38:12.882 9816-9816/com.test W/System.err: onWindowFocusChanged


unlock-screen or hide-status-panel:

08-30 12:38:12.882 9816-9816/com.test W/System.err: onWindowFocusChanged


關於View.post和Activity.runOnUiThread的札記
先看Activity的runOnUiThread的源碼:

final Handler mHandler = new Handler();
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}


可以看出, 如果當前是主線程則直接執行, 否則立即投遞到主線程Handler執行; 
再看View的post的源碼:

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // Assume that post will succeed later
    ViewRootImpl.getRunQueue().post(action);
    return true;
}


如果mAttachInfo不爲null, 則投遞到它的Handler中執行, 否則加入到ViewRootImpl.getRunQueue()這個隊列裏. 
那麼什麼時候, mAttachInfo不爲null呢? 這裏我直接給出答案: (請按照此流程瀏覽源碼)

我們跳過一些路徑, 從Activity的onResume()開始說, 它是在handleResumeActivity()方法裏的performResumeActivity()裏完成的. 
在這之後, handleResumeActivity()裏, 有調用WindowManagerImpl.addView()將PhoneWindow.mDecorView(DecorView類型, 繼承自FrameLayout)添加到窗體中. 
然後路徑到了WindowManagerGlobal.addView()(一個 Activity對應一個WindowManagerImpl, 但WindowManagerGlobal進程唯一), 這裏new ViewRootImpl(), 調用此對象的setView(). 
setView()裏賦值ViewRootImpl的mAttachInfo, 然後首次調用requestLayout(), 此方法裏調用scheduleTraversals(), 進而調用 performTraversals(). 
performTraversals()裏調用頂層view的dispatchAttachedToWindow(), 因爲頂層view是FrameLayout, 所以調用ViewGroup的dispatchAttachedToWindow(), 裏面遍歷調用所有子view的dispatchAttachedToWindow(), 並傳入mAttachInfo(一直傳遞), 直到調用View類的dispatchAttachedToWindow(). 
此時, mAttachInfo不再爲null, 而且各層的onAttachedToWindow()得到回調. 
另外, onAttachedToWindow()真正執行順序依次是 View.onAttachedToWindow(), ViewGroup.onAttachedToWindow(), DecorView.onAttachedToWindow()(當然表面的調用順序是反過來的, 使用super執行). 
所以你在Activity的onCreate()裏調用view.post, 實際走的是:ViewRootImpl.getRunQueue().post(action); 看代碼:

static final class RunQueue {
        private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();

        void post(Runnable action) {
            postDelayed(action, 0);
        }

        void postDelayed(Runnable action, long delayMillis) {
            HandlerAction handlerAction = new HandlerAction();
            handlerAction.action = action;
            handlerAction.delay = delayMillis;

            synchronized (mActions) {
                mActions.add(handlerAction);
            }
        }

        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();
            }
        }
    }


也就是post只是加入到了數組中, 關鍵是什麼時候調用executeActions(Handler)? 
答案還是在ViewRootImpl的performTraversals()裏調用, 而且還是使用mAttachInfo的Handler. 
其實, 這個Handler是在ViewRootImpl的構造方法中傳遞給mAttachInfo(AttachInfo)的構造器的, 這個Handler在ViewRootImpl中使用主線程Looper構建, 屬於ViewRootHandler實例.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章