日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

拥有百万粉丝的大牛讲述学Android的历程程。看看你缺了哪些?

發(fā)布時間:2023/12/18 Android 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 拥有百万粉丝的大牛讲述学Android的历程程。看看你缺了哪些? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

作者:鴻洋

原文鏈接:https://mp.weixin.qq.com/s/tg96p50alrqAtRih8a3AhA

我覺得這篇文章講的比較仔細,希望更多人能看到,能學習到一些什么是最好的,也不枉我推薦一波,加油!

前言

上個周末是雙休,我決定來顛覆一下大家的認知。

在平時的Android開發(fā)中,如果一個新手遇到一個這樣的錯:

android.view.ViewRootImpl$CalledFromWrongThreadException:? Only?the?original?thread?that?created?a?view?hierarchy?can?touch?its?views.at?android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8066)at?android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1297)at?android.view.View.requestLayout(View.java:23147)

你作為一只老鳥,嘴角露出一絲微笑:

“小兄弟,你這個是沒有在UI線程執(zhí)行UI操作導致的錯誤,你搞個UI線程的handler.post一下就好了”。

但是…

我今天要說,真是是只有UI線程才能更新UI嗎?

你作為一只老鳥,肯定立馬腦子里閃過:

我知道你這文章寫啥了,又要在Activity#onCreate,去搞個線程執(zhí)行TextView#setText,然后發(fā)現(xiàn)更新成功了,是不是?

這多年以前我就看過這樣的文章,ViewRootImpl還沒創(chuàng)建而已。

看你們這么強,我這個文章沒法寫下去了…

但是我這個人專治各種不服好吧,我換個問題:

UI線程更新UI就不會出現(xiàn)上面的錯誤了嗎?

好了,開講。

下面是一個應屆小哥小奇寫需求的故事。

注意本文代碼為應屆小哥角度所寫,為了引出問題及原理,不要隨意參考,另外如果嘗試復現(xiàn)相關(guān)代碼,務必看好每一個字符,甚至xml里面的屬性都很關(guān)鍵

小哥的需求

需求很簡單,就是:

  • 點擊一個按鈕;
  • Server會下發(fā)一個問題,客戶端Dialog展示;
  • 在Dialog交互回答問題;
  • 是不是很簡答。

    小哥怒寫一波代碼:

    package?com.example.testviewrootimpl;import?androidx.appcompat.app.AppCompatActivity;import?android.os.Bundle; import?android.view.View; import?android.widget.Button;public?class?MainActivity?extends?AppCompatActivity?{private?Button?mBtnQuestion;@Overrideprotected?void?onCreate(Bundle?savedInstanceState)?{super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mBtnQuestion?=?findViewById(R.id.btn_question);mBtnQuestion.setOnClickListener(new?View.OnClickListener()?{@Overridepublic?void?onClick(View?view)?{requestAQuestion();}});}private?void?requestAQuestion()?{new?Thread(){@Overridepublic?void?run()?{try?{Thread.sleep(1000);}?catch?(InterruptedException?e)?{e.printStackTrace();}//?模擬服務器請求,返回問題String?title?=?"鴻洋帥氣嗎?";showQuestionInDialog(title);}}.start();}private?void?showQuestionInDialog(String?title)?{} }

    很簡單吧,點擊按鈕,新啟動一個線程去模擬網(wǎng)絡(luò)請求,結(jié)果拿到后,把問題展示在Dialog。

    下面開始寫Dialog的代碼:

    public?class?QuestionDialog?extends?Dialog?{private?TextView?mTvTitle;private?Button?mBtnYes;private?Button?mBtnNo;public?QuestionDialog(@NonNull?Context?context)?{super(context);setContentView(R.layout.dialog_question);mTvTitle?=?findViewById(R.id.tv_title);mBtnYes?=?findViewById(R.id.btn_yes);mBtnNo?=?findViewById(R.id.btn_no);}public?void?show(String?title)?{mTvTitle.setText(title);show();} }

    很簡答,就一個標題,兩個按鈕。

    <RelativeLayout?xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tv_title"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="24dp"android:textStyle="bold"tools:text="鴻洋丑的一匹?鴻洋丑的一匹?鴻洋丑的一匹?鴻洋丑的一匹?"?/><Buttonandroid:id="@+id/btn_yes"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@id/tv_title"android:layout_marginTop="10dp"android:text="是的"></Button><Buttonandroid:id="@+id/btn_no"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignTop="@id/btn_yes"android:layout_alignParentRight="true"android:layout_marginLeft="20dp"android:layout_toRightOf="@id/btn_yes"android:text="不是"></Button></RelativeLayout>

    然后我們在showQuestionInDialog讓它show出來。

    private?void?showQuestionInDialog(String?title)?{QuestionDialog?questionDialog?=?new?QuestionDialog(this);questionDialog.show(title); }

    你們猜結(jié)果怎么著…

    崩潰了…

    第一次崩潰

    應屆生小齊迎來了第一次工作中的崩潰…

    我們先停下來。

    上面的代碼很簡單吧,那么我想問各位為什么會崩潰呢?憑各位多年的經(jīng)驗。

    猜想:

    new?Thread(){puublic?void?run(){show("...");}}public?void?show(String?title)?{mTvTitle.setText(title);show(); }

    上面new Thread模擬數(shù)據(jù),沒有切到UI線程就show Dialog了,而且執(zhí)行了TextView#setText,肯定是在非UI線程更新UI導致的。

    很有道理,絕不是一個人會這么猜測吧。

    下面我們看真正報錯的原因:

    Process:?com.example.testviewrootimpl,?PID:?10544 java.lang.RuntimeException:?Can't?create?handler?inside?thread?Thread[Thread-2,5,main]?that?has?not?called?Looper.prepare()at?android.os.Handler.<init>(Handler.java:207)at?android.os.Handler.<init>(Handler.java:119)at?android.app.Dialog.<init>(Dialog.java:133)at?android.app.Dialog.<init>(Dialog.java:162)at?com.example.testviewrootimpl.QuestionDialog.<init>(QuestionDialog.java:17)at?com.example.testviewrootimpl.MainActivity.showQuestionInDialog(MainActivity.java:46)at?com.example.testviewrootimpl.MainActivity.access$100(MainActivity.java:10)at?com.example.testviewrootimpl.MainActivity$2.run(MainActivity.java:40)

    Can’t create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()

    雖然猜錯了,但是依舊有點熟悉的感覺,以前大家在子線程彈toast的時候是不是見過類似的錯誤。

    作為一個老鳥,遇到這個問題,肯定是不在UI線程彈Dialog,但是應屆小哥處理問題的方式就不同了。

    瞎貓遇到死耗子

    小哥,直接把報錯信息扔進Google,不,百度:

    點開第一篇CSDN的博客:

    然后迅速舉一反三,在剛才show Dialog的方法中增加:

    private?void?showQuestionInDialog(String?title)?{Looper.prepare();?//?增加部分QuestionDialog?questionDialog?=?new?QuestionDialog(this);questionDialog.show(title);Looper.loop();?//?增加部分 }

    解決問題就是這么簡單,嘴角露出一絲對自己滿意的笑容。

    再次運行App…

    這里大家再停一下。

    憑各位多年的經(jīng)驗,我想再問一句,這次還會崩潰嗎?

    會嗎?

    猜想:

    這代碼治標不治本,還是沒有在UI線程執(zhí)行相關(guān)代碼,還是會崩,而卻剛才的show里面還有TextView#setText操作

    有點道理。

    看一下運行效果:

    沒有崩潰…

    是不是有一絲的郁悶?

    沒關(guān)系,作為擁有多年經(jīng)驗的老鳥,總能立馬想到解釋的理由:

    大家都知道在Activity#onCreate的時候,我們開個線程去執(zhí)行Text#setText也不會崩潰,原因是ViewRootImpl那時候還沒初始化,所以這次沒崩潰也是這個原因。

    對應源碼解釋是這樣的:

    #?Dialog源碼 public?void?show()?{//?省略一堆代碼mWindowManager.addView(mDecor,?l); }

    我們首次創(chuàng)建的Dialog,第一次調(diào)用show方法,內(nèi)部確實會執(zhí)行mWindowManager.addView,這個代碼會執(zhí)行到:

    #?WindowManagerImpl @Override public?void?addView(@NonNull?View?view,?@NonNull?ViewGroup.LayoutParams?params)?{applyDefaultToken(params);mGlobal.addView(view,?params,?mContext.getDisplay(),?mParentWindow); }

    這個mGlobal對象是WindowManagerGlobal,我們看它的addView方法:

    #?WindowManagerGlobal? public?void?addView(View?view,?ViewGroup.LayoutParams?params,Display?display,?Window?parentWindow)?{//?省略了一堆代碼root?=?new?ViewRootImpl(view.getContext(),?display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);//?do?this?last?because?it?fires?off?messages?to?start?doing?thingstry?{root.setView(view,?wparams,?panelParentView);}?catch?(RuntimeException?e)?{//?BadTokenException?or?InvalidDisplayException,?clean?up.if?(index?>=?0)?{removeViewLocked(index,?true);}throw?e;} }

    果然立馬有new ViewRootImpl的代碼,你看ViewRootImpl沒有創(chuàng)建,所以這和Activity那個是一個情況。

    好像有那么點道理哈…

    我們繼續(xù)往下看。

    應屆小哥要繼續(xù)做需求了。

    一個隱藏的問題

    接下來的需求很奇怪,就是當詢問"鴻洋帥氣嗎?"的時候,如果你點擊不是,那么Dialog不消失,在問題的末尾再加一個?號,如此循環(huán),永不關(guān)閉。

    這難不倒我們的小哥:

    mBtnNo.setOnClickListener(new?View.OnClickListener()?{@Overridepublic?void?onClick(View?view)?{String?s?=?mTvTitle.getText().toString();mTvTitle.setText(s+"?");} });

    運行效果:

    很完美。

    如果我問,你覺得這個代碼有問題嗎?

    你往上看了幾眼,就這兩行代碼有個雞兒問題,可能有空指針?

    當然不是。

    我稍微修改一下代碼:

    mBtnNo.setOnClickListener(new?View.OnClickListener()?{@Overridepublic?void?onClick(View?view)?{String?s?=?mTvTitle.getText().toString();mTvTitle.setText(s+"?");boolean?uiThread?=?Looper.myLooper()?==?Looper.getMainLooper();Toast.makeText(getContext(),"Ui?thread?=?"?+?uiThread?,?Toast.LENGTH_LONG).show();} });

    每次點擊的時候,我彈了個Toast,輸出當前線程是不是UI線程。

    看下效果:

    發(fā)現(xiàn)問題了嗎?

    出乎自己的意料嗎?

    我們在非UI線程一直在更新TextView的text。

    這個時候,你不能跟我扯什么ViewRootImpl還沒有創(chuàng)建了吧?

    別急…

    還有更刺激的。

    更刺激的事情

    我再改一下代碼:

    private?Handler?sUiHandler?=?new?Handler(Looper.getMainLooper());public?QuestionDialog(@NonNull?Context?context)?{super(context);setContentView(R.layout.dialog_question);mBtnNo.setOnClickListener(new?View.OnClickListener()?{@Overridepublic?void?onClick(View?view)?{sUiHandler.post(new?Runnable()?{@Overridepublic?void?run()?{String?s?=?mTvTitle.getText().toString();mTvTitle.setText(s+"?");}});}});}

    我搞了個UI線程的handler,然后post一下Runnable,確保我們的TextView#setText在UI線程執(zhí)行,嚴謹而又優(yōu)雅。

    再停一下,以各位多年經(jīng)驗,這次會崩潰嗎?

    按照我寫博客的套路,這次肯定是演示崩潰呀,不然博客怎么往下寫。

    好像是這個道理…

    我們跑一下效果:

    點擊了幾下,沒崩…

    作為擁有多年經(jīng)驗的老鳥,總能立馬想到解釋的理由:

    UI線程更新怎么會崩潰呀(言語中有一絲不自信)。

    是嗎?

    我們多點擊幾次:

    崩潰了…

    但是剛才在沒有添加UiHandler.post之前可沒有崩潰喲。

    這個結(jié)果,我都得把代碼露出來了,怕你們說我演你們…

    好了,再停一停。

    我又要問大家一個問題了,這次你猜是什么崩潰?

    是不是求我別搞你們了,直接揭秘吧。

    com.example.testviewrootimpl?E/AndroidRuntime:?FATAL?EXCEPTION:?mainProcess:?com.example.testviewrootimpl,?PID:?18323android.view.ViewRootImpl$CalledFromWrongThreadException:?Only?the?original?thread?that?created?a?view?hierarchy?can?touch?its?views.at?android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188)at?android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421)at?android.view.View.requestLayout(View.java:24434)at?android.view.View.requestLayout(View.java:24434)at?android.view.View.requestLayout(View.java:24434)at?android.view.View.requestLayout(View.java:24434)at?android.widget.RelativeLayout.requestLayout(RelativeLayout.java:380)at?android.view.View.requestLayout(View.java:24434)at?android.widget.TextView.checkForRelayout(TextView.java:9667)at?android.widget.TextView.setText(TextView.java:6261)at?android.widget.TextView.setText(TextView.java:6089)at?android.widget.TextView.setText(TextView.java:6041)at?com.example.testviewrootimpl.QuestionDialog$1$1.run(QuestionDialog.java:38)at?android.os.Handler.handleCallback(Handler.java:883)at?android.os.Handler.dispatchMessage(Handler.java:100)at?android.os.Looper.loop(Looper.java:214)at?android.app.ActivityThread.main(ActivityThread.java:7319)at?java.lang.reflect.Method.invoke(Native?Method)at?com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)at?com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)

    那個熟悉的身影回來了:

    Only the original thread that created a view hierarchy can touch its views.

    但是!

    但是!

    這次可是在切換到UI線程拋出來的。

    對應我開頭的靈魂拷問:

    UI線程更新UI就不會出現(xiàn)上面的錯誤了嗎?

    是不是在一股懵逼又刺激的感覺中無法自拔…

    還有更刺激的事情…嗯,篇幅問題,本篇我們就到這了,更刺激的事情我們下次再寫。

    別怕,沒完,我總得告訴你們?yōu)槭裁窗伞?/p>

    小做揭秘

    其實這一切的根源都在于我們長久的一個錯誤的概念。

    注意下面每一句話都很關(guān)鍵,請降低閱讀速度。

    就是UI線程才能UI線程,這是不對的,為什么這么說呢?

    Only the original thread that created a view hierarchy can touch its views.

    這個異常是在ViewRootImpl里面拋出的對吧,我們再次來審視一下這段代碼:

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

    其實就幾行代碼。

    我們仔細看一下,他這個錯誤信息并不是:

    Only the UI Thread …?而是?Only the original thread。

    對吧,如果真的想強制為Only the Ui Thread,上面的if語句應該寫成:

    if(UI Thread != Thread.currentThread()){}

    而不是mThread。

    根本原因說完了。

    我再帶大家看下源碼解析:

    這個mThread是什么?

    是ViewRootImpl的成員變量,我們重點應該關(guān)注它什么時候賦值的:

    public?ViewRootImpl(Context?context,?Display?display)?{mContext?=?context;mThread?=?Thread.currentThread();}

    在ViewRootImpl構(gòu)造的時候賦值的,賦值的就是當前的Thread對象。

    也就是說,你ViewRootImpl在哪個線程創(chuàng)建的,你后續(xù)的UI更新就需要在哪個線程執(zhí)行,跟是不是UI線程毫無關(guān)系。

    對應到上面的例子,我們中間也有段貼源碼的地方。

    恰好說明了:

    Dialog的ViewRootImpl,其實是在執(zhí)行show()方法的時候創(chuàng)建的,而我們的Dialog的show放在子線程里面,所以導致后續(xù)View更新,執(zhí)行到ViewRootImpl#checkThread的時候,都在子線程才可以。

    這就說明了,為什么我們剛才切到UI線程去執(zhí)行TextView#setText為啥崩了。

    這里有個思考題,注意我們上面演示的時候,切到UI線程執(zhí)行setText沒有立馬崩潰,而是執(zhí)行了好幾次之后才崩潰的,為什么呢?自己想。

    大家可能還有個一問題:

    ViewRootImpl怎么和View關(guān)聯(lián)起來的

    其實我們看報錯堆棧很好找到相關(guān)代碼:

    com.example.testviewrootimpl?E/AndroidRuntime:?FATAL?EXCEPTION:?mainProcess:?com.example.testviewrootimpl,?PID:?18323android.view.ViewRootImpl$CalledFromWrongThreadException:?Only?the?original?thread?that?created?a?view?hierarchy?can?touch?its?views.at?android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188)at?android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421)at?android.view.View.requestLayout(View.java:24434)

    報錯的堆棧都是由View.requestLayout觸發(fā)到ViewRootImpl的。

    我們直接看這個方法:

    public?void?requestLayout()?{if?(mParent?!=?null?&&?!mParent.isLayoutRequested())?{mParent.requestLayout();} }

    注意里面這個mParent變量,它的類型是ViewParent接口。

    見名知意。

    我要問你一個View的mParent是什么,你肯定會回答是它的父View,也就是個ViewGroup。

    對,沒錯。

    public?abstract?class?ViewGroup? extends?View? implements?ViewParent{}

    ViewGroup確實實現(xiàn)了ViewParent接口。

    但是還有個問題,一個界面的最最最上面那個ViewGroup它的mParent是誰?

    對吧,總不能還是ViewGroup吧,那豈不是沒完沒了了。

    所以,ViewParent還有另外一個實現(xiàn)類,叫做ViewRootImpl。

    現(xiàn)在明白了吧。

    按照ViewParent的體系,我們的界面結(jié)構(gòu)是這樣的。

    嗯,我還是寫坨代碼吧:

    還是剛才Dialog,當我們點擊No的時候,我們打印下ViewParent體系:

    mBtnNo.setOnClickListener(new?View.OnClickListener()?{@Overridepublic?void?onClick(View?view)?{printViewParentHierarchy(mTvTitle,?0);} });private?void?printViewParentHierarchy(Object?view,?int?level)?{if?(view?==?null)?{return;}StringBuilder?sb?=?new?StringBuilder();for?(int?i?=?0;?i?<?level;?i++)?{sb.append("\t");}sb.append(view.getClass().getSimpleName());Log.d("lmj",?sb.toString());if?(view?instanceof?View)?{printViewParentHierarchy(((View)?view).getParent(),?level?+?1);}}

    很簡單,我們就打印mTbTitle,一直往上的ViewParent體系。

    D/lmj:?AppCompatTextView D/lmj:??RelativeLayout D/lmj:??????FrameLayout D/lmj:??????????FrameLayout D/lmj:??????????????DecorView D/lmj:??????????????????ViewRootImpl

    看到?jīng)],最底部的是誰。

    是它,是它,就是它,我們的ViewRootImpl。

    所以當你的TextView觸發(fā)requestLayout,會輾轉(zhuǎn)到ViewRootImpl的requestLayout,然后再到它的checkThread,而checkThread判斷的并非是UI線程和當前線程對比,而是mThread和當前線程對比。

    總結(jié)

    以上是生活随笔為你收集整理的拥有百万粉丝的大牛讲述学Android的历程程。看看你缺了哪些?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。