久久综合给合久久狠狠狠974色|亚洲成熟丰满熟妇高潮xxxxx|国产又黄又黄又大又粗又爽的视频|日韩久久久精品无码一区二区三区|中文字幕无码乱人伦一区二区三区|国产成人无码区免费内射一片色欲|亚洲av无码久久精品一区二区三区

主線程修改UI也會(huì)崩潰?這個(gè)坑踩得懷疑人生

2020-04-01 06:24:51  閱讀:-  來源:

前言

某天早晨,吃完早餐,坐回工位,打開電腦,開啟 chrome,進(jìn)入友盟頁面,發(fā)現(xiàn)了一個(gè)崩潰信息:

  java.lang.RuntimeException: Unable to resume activity   {com.youdao.youdaomath/com.youdao.youdaomath.view.PayCourseVideoActivity}:   android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that   created a view hierarchy can touch its views.      at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3824)      at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3856)      at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:51)      at  android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:   145)      at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)      at android.os.Handler.dispatchMessage(Handler.java:106)      at android.os.Looper.loop(Looper.java:201)      at android.app.ActivityThread.main(ActivityThread.java:6806)      at java.lang.reflect.Method.invoke(Native Method)      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)  Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original   thread that created a view hierarchy can touch its views.      at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8000)      at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)      at android.view.View.requestLayout(View.java:23147)      at android.view.View.requestLayout(View.java:23147)      at android.widget.TextView.checkForRelayout(TextView.java:8914)      at android.widget.TextView.setText(TextView.java:5736)      at android.widget.TextView.setText(TextView.java:5577)      at android.widget.TextView.setText(TextView.java:5534)      at android.widget.Toast.setText(Toast.java:332)      at com.youdao.youdaomath.view.common.CommonToast.showShortToast(CommonToast.java:40)      at com.youdao.youdaomath.view.PayCourseVideoActivity.checkNetWork(PayCourseVideoActivity.java:137)      at com.youdao.youdaomath.view.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)      at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1413)      at android.app.Activity.performResume(Activity.java:7400)      at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3816)

一眼看上去似乎是比較常見的子線程修改UI的問題。并且是在Toast上面報(bào)出的,常識(shí)告訴我Toast在子線程彈出是會(huì)報(bào)錯(cuò),但是應(yīng)該是提示Looper沒有生成的錯(cuò),而不應(yīng)該是上面所報(bào)出的錯(cuò)誤。那么會(huì)不會(huì)是生成Looper以后報(bào)的錯(cuò)的?


一、

所以我先做了一個(gè)demo,如下:

    @Override    protected void onResume() {        super.onResume();        Thread thread = new Thread(new Runnable() {            @Override            public void run() {                Toast.makeText(MainActivity.this,"子線程彈出Toast",Toast.LENGTH_SHORT).show();            }        });        thread.start();    }

運(yùn)行一下,果不其然崩潰掉,錯(cuò)誤信息就是提示我必須準(zhǔn)備好looper才能彈出toast:

    java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()        at android.widget.Toast$TN.(Toast.java:393)        at android.widget.Toast.(Toast.java:117)        at android.widget.Toast.makeText(Toast.java:280)        at android.widget.Toast.makeText(Toast.java:270)        at com.netease.photodemo.MainActivity$1.run(MainActivity.java:22)        at java.lang.Thread.run(Thread.java:764)

接下來就在toast里面準(zhǔn)備好looper,再試試吧:

        Thread thread = new Thread(new Runnable() {            @Override            public void run() {                Looper.prepare();                Toast.makeText(MainActivity.this,"子線程彈出Toast",Toast.LENGTH_SHORT).show();                Looper.loop();            }        });        thread.start();

運(yùn)行發(fā)現(xiàn)是能夠正確的彈出Toast的:

!image](//upload-images.jianshu.io/upload_images/10239771-212081efa66d5c73.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/454/format/webp)

那么問題就來了,為什么會(huì)在友盟中出現(xiàn)這個(gè)崩潰呢?

二、

然后仔細(xì)看了下報(bào)錯(cuò)信息有兩行重要信息被我之前略過了:

  at com.youdao.youdaomath.view.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)
  t android.widget.Toast.setText(Toast.java:332)

發(fā)現(xiàn)是在主線程報(bào)了Toast設(shè)置Text的時(shí)候的錯(cuò)誤。這就讓我很納悶了,子線程修改UI會(huì)報(bào)錯(cuò),主線程也會(huì)報(bào)錯(cuò)?感覺這么多年Android白做了。這不是最基本的知識(shí)么?于是我只能硬著頭皮往源碼深處看了:先來看看Toast是怎么setText的:

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,            @NonNull CharSequence text, @Duration int duration) {        Toast result = new Toast(context, looper);        LayoutInflater inflate = (LayoutInflater)                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);        tv.setText(text);        result.mNextView = v;        result.mDuration = duration;        return result;    }

很常規(guī)的一個(gè)做法,先是inflate出來一個(gè)View對(duì)象,再從View對(duì)象找出對(duì)應(yīng)的TextView,然后TextView將文本設(shè)置進(jìn)去,所以感覺似乎一點(diǎn)問題都沒有。那么既然出現(xiàn)了這個(gè)錯(cuò)誤,總得有原因吧,或許是自己源碼看漏了?那就重新再看一遍ViewRootImpl#checkThread方法吧:

    void checkThread() {        if (mThread != Thread.currentThread()) {            throw new CalledFromWrongThreadException(                    "Only the original thread that created a view hierarchy can touch its views.");        }    }

這一看,還真的似乎給我了一點(diǎn)頭緒,系統(tǒng)在checkThread的時(shí)候并不是將Thread.currentThread和MainThread作比較,而是跟mThread作比較,那么有沒有一種可能mThread是子線程?一想到這里,我就興奮了,全類查看mThread到底是怎么初始化的:

    public ViewRootImpl(Context context, Display display) {        ...代碼省略...        mThread = Thread.currentThread();       ...代碼省略...    }

可以發(fā)現(xiàn)全類只有這一處對(duì)mThread進(jìn)行了賦值。那么會(huì)不會(huì)是子線程初始化了ViewRootimpl呢?似乎我之前好像也沒有研究過Toast為什么會(huì)彈出來,所以順便就先去了解下Toast是怎么show出來的好了:

    /**     * Show the view for the specified duration.     */    public void show() {        if (mNextView == null) {            throw new RuntimeException("setView must have been called");        }        INotificationManager service = getService();        String pkg = mContext.getOpPackageName();        TN tn = mTN;        tn.mNextView = mNextView;        try {            service.enqueueToast(pkg, tn, mDuration);        } catch (RemoteException e) {            // Empty        }    }

調(diào)用Toast的show方法時(shí),會(huì)通過Binder獲取Service即NotificationManagerService,然后執(zhí)行enqueueToast方法(NotificationManagerService的源碼就不做分析),然后會(huì)執(zhí)行Toast里面如下方法:

        @Override        public void show(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "SHOW: " + this);            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();        }

發(fā)送一個(gè)Message,通知進(jìn)行show的操作:

        @Override        public void show(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "SHOW: " + this);            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();        }

在Handler的handleMessage方法中找到了SHOW的case,接下來就要進(jìn)行真正show的操作了:

        public void handleShow(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView                    + " mNextView=" + mNextView);            // If a cancel/hide is pending - no need to show - at this point            // the window token is already invalid and no need to do any work.            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {                return;            }            if (mView != mNextView) {                // remove the old view if necessary                handleHide();                mView = mNextView;                Context context = mView.getContext().getApplicationContext();                String packageName = mView.getContext().getOpPackageName();                if (context == null) {                    context = mView.getContext();                }                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);                // We can resolve the Gravity here by using the Locale for getting                // the layout direction                final Configuration config = mView.getContext().getResources().getConfiguration();                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());                mParams.gravity = gravity;                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {                    mParams.horizontalWeight = 1.0f;                }                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {                    mParams.verticalWeight = 1.0f;                }                mParams.x = mX;                mParams.y = mY;                mParams.verticalMargin = mVerticalMargin;                mParams.horizontalMargin = mHorizontalMargin;                mParams.packageName = packageName;                mParams.hideTimeoutMilliseconds = mDuration ==                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;                mParams.token = windowToken;                if (mView.getParent() != null) {                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);                    mWM.removeView(mView);                }                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);                // Since the notification manager service cancels the token right                // after it notifies us to cancel the toast there is an inherent                // race and we may attempt to add a window after the token has been                // invalidated. Let us hedge against that.                try {                    mWM.addView(mView, mParams);                    trySendAccessibilityEvent();                } catch (WindowManager.BadTokenException e) {                    /* ignore */                }            }        }

代碼有點(diǎn)長,我們最需要關(guān)心的就是mWm.addView方法。相信看過ActivityThread的同學(xué)應(yīng)該知道m(xù)Wm.addView方法是在ActivityThread的handleResumeActivity里面也有調(diào)用過,意思就是進(jìn)行ViewRootImpl的初始化,然后通過ViewRootImp進(jìn)行View的測量,布局,以及繪制。看到這里,我想到了一個(gè)可能的原因:那就是我的Toast是一個(gè)全局靜態(tài)的Toast對(duì)象,然后第一次是在子線程的時(shí)候show出來,這個(gè)時(shí)候ViewRootImpl在初始化的時(shí)候就會(huì)將子線程的對(duì)象作為mThread,然后下一次在主線程彈出來就出錯(cuò)了吧?想想應(yīng)該是這樣的。

三、

所以繼續(xù)做我的demo來印證我的想法:

    @Override    protected void onResume() {        super.onResume();        Thread thread = new Thread(new Runnable() {            @Override            public void run() {                Looper.prepare();                sToast = Toast.makeText(MainActivity.this,"子線程彈出Toast",Toast.LENGTH_SHORT);                sToast.show();                Looper.loop();            }        });        thread.start();    }    public void click(View view) {        sToast.setText("主線程彈出Toast");        sToast.show();    }

做了個(gè)靜態(tài)的toast,然后點(diǎn)擊按鈕的時(shí)候彈出toast,運(yùn)行一下:

主線程修改UI也會(huì)崩潰?這個(gè)坑踩得懷疑人生


image

發(fā)現(xiàn)竟然沒問題,這時(shí)候又開始懷疑人生了,這到底怎么回事。ViewRootImpl此時(shí)的mThread應(yīng)該是子線程啊,沒道理還能正常運(yùn)行,怎么辦呢?debug一步一步調(diào)試吧,一步一步調(diào)試下來,發(fā)現(xiàn)在View的requestLayout里面parent竟然為空了:

主線程修改UI也會(huì)崩潰?這個(gè)坑踩得懷疑人生

然后在仔細(xì)看了下當(dāng)前 View是一個(gè)LinearLayout,然后這個(gè)View的子View是TextView,文本內(nèi)容是"主線程彈出toast",所以應(yīng)該就是Toast在new的時(shí)候inflate的布局

  View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);

在Android源碼社區(qū)中搜索"transient_notification"找到了對(duì)應(yīng)的toast布局文件,打開一看,果然如此:

          

也就是說此時(shí)的View已經(jīng)是頂級(jí)View了,它的parent應(yīng)該就是ViewRootImpl,那么為什么ViewRootImpl是null呢,明明之前已經(jīng)show過了。看來只能往Toast的hide方法找原因了

四、

所以重新回到Toast的類中,查看下Toast的hide方法(此處直接看Handler的hide處理,之前的操作與show類似):

  public void handleHide() {              if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);              if (mView != null) {                  // note: checking parent() just to make sure the view has                  // been added...  i have seen cases where we get here when                  // the view isn't yet added, so let's try not to crash.                  if (mView.getParent() != null) {                      if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);                      mWM.removeViewImmediate(mView);                  }                  // Now that we've removed the view it's safe for the server to release                  // the resources.                  try {                      getService().finishToken(mPackageName, this);                  } catch (RemoteException e) {                  }                  mView = null;             }          }

此處調(diào)用了mWm的removeViewImmediate,即WindowManagerImpl里面的removeViewImmediate方法:

    @Override    public void removeViewImmediate(View view) {        mGlobal.removeView(view, true);    }

會(huì)調(diào)用WindowManagerGlobal的removeView方法:

  public void removeView(View view, boolean immediate) {          if (view == null) {              throw new IllegalArgumentException("view must not be null");          }          synchronized (mLock) {              int index = findViewLocked(view, true);              View curView = mRoots.get(index).getView();              removeViewLocked(index, immediate);              if (curView == view) {                  return;              }              throw new IllegalStateException("Calling with view " + view                      + " but the ViewAncestor is attached to " + curView);          }      }

然后調(diào)用removeViewLocked方法:

  private void removeViewLocked(int index, boolean immediate) {          ViewRootImpl root = mRoots.get(index);          View view = root.getView();          if (view != null) {              InputMethodManager imm = InputMethodManager.getInstance();              if (imm != null) {                  imm.windowDismissed(mViews.get(index).getWindowToken());              }          }          boolean deferred = root.die(immediate);          if (view != null) {              //此處調(diào)用View的assignParent方法將viewParent置空              view.assignParent(null);              if (deferred) {                  mDyingViews.add(view);              }          }      }

所以也就是說在Toast時(shí)間到了以后,會(huì)調(diào)用hide方法,此時(shí)會(huì)將parent置成空,所以我剛才試的時(shí)候才沒有問題。那么按道理說只要在Toast沒有關(guān)閉的時(shí)候點(diǎn)擊再次彈出toast應(yīng)該就會(huì)報(bào)錯(cuò)。所以還是原來的代碼,再來一次,這次不等Toast關(guān)閉,再次點(diǎn)擊:

主線程修改UI也會(huì)崩潰?這個(gè)坑踩得懷疑人生

果然如預(yù)期所料,此時(shí)在主線程彈出Toast就崩潰。

五、

那么問題原因找到了:是在項(xiàng)目子線程中有彈出過Toast,然后Toast并沒有關(guān)閉,又在主線程彈出了同一個(gè)對(duì)象的toast,會(huì)造成崩潰。此時(shí)內(nèi)心有個(gè)困惑:如果是子線程彈出Toast,那我就需要寫Looper.prepare方法和Looper.loop方法,為什么我自己一點(diǎn)印象都沒有。于是我全局搜索了Looper.prepare,發(fā)現(xiàn)并沒有找到對(duì)應(yīng)的代碼。所以我就全局搜索了Toast調(diào)用的地方,發(fā)現(xiàn)在JavaBridge的回調(diào)當(dāng)中找到了:

    class JSInterface {        @JavascriptInterface        public void handleMessage(String msg) throws JSONException {            LogHelper.e(TAG, "msg::" + msg);            JSONObject jsonObject = new JSONObject(msg);            String callType = jsonObject.optString(JS_CALL_TYPE);            switch (callType) {                ...代碼省略..                case JSCallType.SHOW_TOAST:                    showToast(jsonObject);                    break;                default:                    break;            }        }    }    /**     * 彈出吐司     * @param jsonObject     * @throws JSONException     */    public void showToast(JSONObject jsonObject) throws JSONException {        JSONObject payDataObj = jsonObject.getJSONObject("data");        String message = payDataObj.optString("data");        CommonToast.showShortToast(message);    }

但是看到這段代碼,又有疑問了,我并沒有在Javabridge的回調(diào)中看到有任何準(zhǔn)備Looper的地方,那么為什么Toast沒有崩潰掉?所以在此處加了一段代碼:

    class JSInterface {        @JavascriptInterface        public void handleMessage(String msg) throws JSONException {            LogHelper.e(TAG, "msg::" + msg);            JSONObject jsonObject = new JSONObject(msg);            String callType = jsonObject.optString(JS_CALL_TYPE);            Thread currentThread = Thread.currentThread();            Looper looper = Looper.myLooper();            switch (callType) {                ...代碼省略..                case JSCallType.SHOW_TOAST:                    showToast(jsonObject);                    break;                default:                    break;            }        }    }

并且加了一個(gè)斷點(diǎn),來查看下此時(shí)的情況:

主線程修改UI也會(huì)崩潰?這個(gè)坑踩得懷疑人生

確實(shí)當(dāng)前線程是JavaBridge線程,另外JavaBridge線程中已經(jīng)提前給開發(fā)者準(zhǔn)備好了Looper。所以也難怪一方面奇怪自己怎么沒有寫Looper的印象,一方面又很好奇為什么這個(gè)線程在開發(fā)者沒有準(zhǔn)備Looper的情況下也能正常彈出Toast。


總結(jié)

至此,真相終于找出來了。相比較發(fā)生這個(gè)bug 的原因,解決方案就顯得非常簡單了。只需要在CommonToast的showShortToast方法內(nèi)部判斷是否為主線程調(diào)用,如果不是的話,new一個(gè)主線程的Handler,將Toast扔到主線程彈出來。這樣就會(huì)避免了子線程彈出。

PS:本人還得吐槽一下 Android,Android官方一方面明明宣稱不能在主線程以外的線程進(jìn)行UI的更新,另一方面在初始化ViewRootImpl的時(shí)候又不把主線程作為成員變量保存起來,而是直接獲取當(dāng)前所處的線程作為mThread保存起來,這樣做就有可能會(huì)出現(xiàn)子線程更新UI的操作。從而引起類似我今天的這個(gè)bug。

結(jié)尾

其實(shí)對(duì)于程序員來說,要學(xué)習(xí)的知識(shí)內(nèi)容、技術(shù)有太多太多,要想不被環(huán)境淘汰就只有不斷提升自己,從來都是我們?nèi)ミm應(yīng)環(huán)境,而不是環(huán)境來適應(yīng)我們!

附上我的Android核心技術(shù)學(xué)習(xí)大綱,關(guān)注我,私信我【安卓】

vx:xx13414521

主線程修改UI也會(huì)崩潰?這個(gè)坑踩得懷疑人生

沁源县| 谢通门县| 同江市| 抚顺市| 宁都县| 淮南市| 襄城县| 商水县| 晴隆县| 金寨县| 海林市| 图们市| 澎湖县| 永年县| 巴东县| 台北县| 广平县| 凤庆县| 永宁县| 锡林浩特市| 黄冈市| 定安县| 鄢陵县| 开化县| 阿克苏市| 达拉特旗| 芦山县| 黄陵县| 思茅市| 锡林浩特市| 萨迦县| 塔河县| 建始县| 冷水江市| 遂昌县| 兴化市| 阿图什市| 清丰县| 墨玉县| 尚志市| 克山县|