溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Android的Toast問題怎么解決

發(fā)布時(shí)間:2022-01-11 17:17:50 來源:億速云 閱讀:120 作者:iii 欄目:開發(fā)技術(shù)

這篇“Android的Toast問題怎么解決”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Android的Toast問題怎么解決”文章吧。

1.回顧

  1. Toast 系統(tǒng)如何構(gòu)建窗口(通過系統(tǒng)服務(wù)NotificationManager來生成系統(tǒng)窗口)

  2. Toast 異常出現(xiàn)的原因(系統(tǒng)調(diào)用 Toast的時(shí)序紊亂)

2.解決思路

基于第一篇的知識(shí),我們知道,Toast 的窗口屬于系統(tǒng)窗口,它的生成和生命周期依賴于系統(tǒng)服務(wù) NotificationManager。一旦 NotificationManager 所管理的窗口生命周期跟我們本地的進(jìn)程不一致,就會(huì)發(fā)生異常。那么,我們能不能不使用系統(tǒng)的窗口,而使用自己的窗口,并且由我們自己控制生命周期呢?事實(shí)上, SnackBar 就是這樣的方案。不過,如果不使用系統(tǒng)類型的窗口,就意味著你的Toast 界面,無法在其他應(yīng)用之上顯示。(比如,我們經(jīng)??吹降囊粋€(gè)場(chǎng)景就是你在你的應(yīng)用出調(diào)用了多次 Toast.show函數(shù),然后退回到桌面,結(jié)果發(fā)現(xiàn)桌面也會(huì)彈出 Toast,就是因?yàn)橄到y(tǒng)的 Toast 使用了系統(tǒng)窗口,具有高的層級(jí))不過在某些版本的手機(jī)上,你的應(yīng)用可以申請(qǐng)權(quán)限,往系統(tǒng)中添加 TYPE_SYSTEM_ALERT 窗口,這也是一種系統(tǒng)窗口,經(jīng)常用來作為浮層顯示在所有應(yīng)用程序之上。不過,這種方式需要申請(qǐng)權(quán)限,并不能做到讓所有版本的系統(tǒng)都能正常使用。
如果我們從體驗(yàn)的角度來看,當(dāng)用戶離開了該進(jìn)程,就不應(yīng)該彈出另外一個(gè)進(jìn)程的 Toast 提示去干擾用戶的。Android 系統(tǒng)似乎也意識(shí)到了這一點(diǎn),在新版本的系統(tǒng)更新中,限制了很多在桌面提示窗口相關(guān)的權(quán)限。所以,從體驗(yàn)上考慮,這個(gè)情況并不屬于問題。

“那么我們可以選擇哪些窗口的類型呢?”

  1. 使用子窗口: 在 Android 進(jìn)程內(nèi),我們可以直接使用類型為子窗口類型的窗口。在 Android 代碼中的直接應(yīng)用是 PopupWindow 或者是 Dialog 。這當(dāng)然可以,不過這種窗口依賴于它的宿主窗口,它可用的條件是你的宿主窗口可用

  2. 采用 View 系統(tǒng): 使用 View 系統(tǒng)去模擬一個(gè) Toast 窗口行為,做起來不僅方便,而且能更加快速的實(shí)現(xiàn)動(dòng)畫效果,我們的 SnackBar 就是采用這套方案。這也是我們今天重點(diǎn)講的方案


“如果采用 View 系統(tǒng)方案,那么我要往哪個(gè)控件中添加我的 Toast 控件呢?”

Android進(jìn)程中,我們所有的可視操作都依賴于一個(gè) Activity 。 Activity 提供上下文(Context)和視圖窗口(Window) 對(duì)象。我們通過 Activity.setContentView 方法所傳遞的任何 View對(duì)象 都將被視圖窗口( Window) 中的 DecorView 所裝飾。而在 DecorView 的子節(jié)點(diǎn)中,有一個(gè) idandroid.R.id.contentFrameLayout 節(jié)點(diǎn)(后面簡(jiǎn)稱 content 節(jié)點(diǎn)) 是用來容納我們所傳遞進(jìn)去的 View 對(duì)象。一般情況下,這個(gè)節(jié)點(diǎn)占據(jù)了除了通知欄的所有區(qū)域。這就特別適合用來作為 Toast 的父控件節(jié)點(diǎn)。

“我什么時(shí)機(jī)往這個(gè)content節(jié)點(diǎn)中添加合適呢?這個(gè) content 節(jié)點(diǎn)什么時(shí)候被初始化呢?”

根據(jù)不同的需求,你可能會(huì)關(guān)注以下兩個(gè)時(shí)機(jī):

  1. Content 節(jié)點(diǎn)生成

  2. Content 內(nèi)容顯示

實(shí)際我們只需要將我們的 Toast 添加到 Content 節(jié)點(diǎn)中,只要滿足第一條即可。如果你是為了完成性能檢測(cè),測(cè)量或者其他目的,那么你可能更關(guān)心第二條。 那么什么情況下 Content 節(jié)點(diǎn)生成呢?剛才我們說了,Content 節(jié)點(diǎn)包含在我們的 DecorView 控件中,而 DecorView 是由 ActivityWindow對(duì)象所持有的控件。WindowAndroid 中的實(shí)現(xiàn)類是 PhoneWindow,(這部分代碼有興趣可以自行閱讀) 我們來看下源碼:

//code PhoneWindow.java
@Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) { //mContentParent就是我們的 content 節(jié)點(diǎn)
            installDecor();//生成一個(gè)DecorView
        } else {
            mContentParent.removeAllViews();
        }
        mLayoutInflater.inflate(layoutResID, mContentParent);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

PhoneWindow 對(duì)象通過 installDecor 函數(shù)生成 DecorView 和 我們所需要的 content 節(jié)點(diǎn)(最終會(huì)存到 mContentParent) 變量中去。但是, setContentView 函數(shù)需要我們主動(dòng)調(diào)用,如果我并沒有調(diào)用這個(gè) setContentView 函數(shù),installDecor 方法將不被調(diào)用。那么,有沒有某個(gè)時(shí)刻,content 節(jié)點(diǎn)是必然生成的呢?當(dāng)然有,除了在 setContentView 函數(shù)中調(diào)用installDecor外,還有一個(gè)函數(shù)也調(diào)用到了這個(gè),那就是:

//code PhoneWindow.java
@Override
    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
}

而這個(gè)函數(shù),將在 Activity.findViewById 的時(shí)候調(diào)用:

//code Activity.java
public View findViewById(@IdRes int id) {
        return getWindow().findViewById(id);
}
//code Window.java
public View findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
}

因此,只要我們只要調(diào)用了 findViewById 函數(shù),一樣可以保證  content  被正常初始化。這樣我們解釋了第一個(gè)”就緒”(Content 節(jié)點(diǎn)生成)。我們?cè)賮砜聪碌诙€(gè)”就緒”,也就是 Android 界面什么時(shí)候顯示呢?相信你可能迫不及待的回答不是 onResume 回調(diào)的時(shí)候么?實(shí)際上,在 onResume 的時(shí)候,根本還沒處理跟界面相關(guān)的事情。我們來看下 Android 進(jìn)程是如何處理 resume 消息的:
(注: AcitivityThreadAndroid 進(jìn)程的入口類, Android 進(jìn)程處理 resume 相關(guān)消息將會(huì)調(diào)用到 AcitivityThread.handleResumeActivity 函數(shù))

//code AcitivityThread.java
void handleResumeActivity(...) {
    ...
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    // 之后會(huì)調(diào)用call onResume
    ...
    View decor = r.window.getDecorView();
    //調(diào)用getDecorView 生成 content節(jié)點(diǎn)
    decor.setVisibility(View.INVISIBLE);
    ....
    if (r.activity.mVisibleFromClient) {
       r.activity.makeVisible();//add to WM 管理
    }
    ...
}
//code Activity.java
  void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

Android 進(jìn)程在處理 resume 消息的時(shí)候,將走以下的流程:

  1. 調(diào)用 performResumeActivity 回調(diào) ActivityonResume 函數(shù)

  2. 調(diào)用 WindowgetDecorView 生成 DecorView 對(duì)象和 content 節(jié)點(diǎn)

  3. DecorView納入 WindowManager (進(jìn)程內(nèi)服務(wù))的管理

  4. 調(diào)用 Activity.makeVisible 顯示當(dāng)前 Activity

按照上述的流程,在 Activity.onResume 回調(diào)之后,才將控件納入本地服務(wù) WindowManager 的管理中。也就是說, Activity.onResume 根本沒有顯示任何東西。我們不妨寫個(gè)代碼驗(yàn)證一下:

//code DemoActivity.java

public DemoActivity extends Activity {
   private View view ;

    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new View(this);
        this.setContentView(view);
    }
    @Override
    protected void onResume() {
        super.onResume();
        Log.d("cdw","onResume :" +view.getHeight());// 有高度是顯示的必要條件
    }
}

這里,我們通過在 onResume 中獲取高度的方式驗(yàn)證界面是否被繪制,最終我們將輸出日志:

D cdw     : onResume :0

那么,界面又是在什么時(shí)候完成的繪制呢?是不是在 WindowManager.addView 之后呢?我們?cè)?onResume之后會(huì)調(diào)用Activity.makeVisible,里面會(huì)調(diào)用 WindowManager.addView。因此我們?cè)?code>onResume 里post一個(gè)消息就可以檢測(cè)WindowManager.addView 之后的情況:

@Override
    protected void onResume() {
        super.onResume();
        this.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Log.d("cdw","onResume :" +view.getHeight());
            }
        });
}

//控制臺(tái)輸出:
01-02 21:30:27.445  2562  2562 D cdw     : onResume :0

從結(jié)果上看,我們?cè)?WindowManager.addView 之后,也并沒有繪制界面。那么,Android的繪制是什么時(shí)候開始的?又是到什么時(shí)候結(jié)束?

Android 系統(tǒng)中,每一次的繪制都是通過一個(gè) 16ms 左右的 VSYNC 信號(hào)控制的,這種信號(hào)可能來自于硬件也可能來自于軟件模擬。每一次非動(dòng)畫的繪制,都包含:測(cè)量,布局,繪制三個(gè)函數(shù)。而一般觸發(fā)這一事件的的動(dòng)作有:

  1. View 的某些屬性的變更

  2. View 重新布局Layout

  3. 增刪 View 節(jié)點(diǎn)

當(dāng)調(diào)用 WindowManager.addView 將空間添加到 WM 服務(wù)管理的時(shí)候,會(huì)調(diào)用一次Layout請(qǐng)求,這就觸發(fā)了一次 VSYNC 繪制。因此,我們只需要在 onResumepost 一個(gè)幀回調(diào)就可以檢測(cè)繪制開始的時(shí)間:

 @Override
    protected void onResume() {
        super.onResume();
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                //TODO 繪制開始
            }
        });
    }

我們先來看下 View.requestLayout 是怎么觸發(fā)界面重新繪制的:

//code View.java
public void requestLayout() {
        ....
        if (mParent != null) {
            ...
            if (!mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
        }
    }

View 對(duì)象調(diào)用 requestLayout 的時(shí)候會(huì)委托給自己的父節(jié)點(diǎn)處理,這里之所以不稱為父控件而是父節(jié)點(diǎn),是因?yàn)槌丝丶?,還有 ViewRootImpl 這個(gè)非控件類型作為父節(jié)點(diǎn),而這個(gè)父節(jié)點(diǎn)會(huì)作為整個(gè)控件樹的根節(jié)點(diǎn)。按照我們上面說的委托的機(jī)制,requestLayout 最終將會(huì)調(diào)用到 ViewRootImpl.requestLayout

//code ViewRootImpl.java
@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();//申請(qǐng)繪制請(qǐng)求
        }
    }

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            ....
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//申請(qǐng)繪制
            ....
        }
    }

ViewRootImpl 最終會(huì)將 mTraversalRunnable 處理命令放到 CALLBACK_TRAVERSAL 繪制隊(duì)列中去:

 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();//執(zhí)行布局和繪制
        }
}

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            ...
            performTraversals();
            ...
        }
    }

mTraversalRunnable 命令最終會(huì)調(diào)用到 performTraversals() 函數(shù):

private void performTraversals() {
    final View host = mView;
    ...
    host.dispatchAttachedToWindow(mAttachInfo, 0);//attachWindow
    ...
    getRunQueue().executeActions(attachInfo.mHandler);//執(zhí)行某個(gè)指令
    ...
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    host.measure(childWidthMeasureSpec, childHeightMeasureSpec);//測(cè)量
    ....
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//布局
    ...
    draw(fullRedrawNeeded);//繪制
    ...
}

performTraversals 函數(shù)實(shí)現(xiàn)了以下流程:

  1. 調(diào)用 dispatchAttachedToWindow 通知子控件樹當(dāng)前控件被 attach 到窗口中

  2. 執(zhí)行一個(gè)命令隊(duì)列 getRunQueue

  3. 執(zhí)行 meausre 測(cè)量指令

  4. 執(zhí)行 layout 布局函數(shù)

  5. 執(zhí)行繪制 draw

這里我們看到一句方法調(diào)用:

getRunQueue().executeActions(attachInfo.mHandler);

這個(gè)函數(shù)將執(zhí)行一個(gè)延時(shí)的命令隊(duì)列,在 View 對(duì)象被 attachView樹之前,通過調(diào)用 View.post 函數(shù),可以將執(zhí)行消息命令加入到延時(shí)執(zhí)行隊(duì)列中去:

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

getRunQueue().executeActions 函數(shù)執(zhí)行的時(shí)候,會(huì)將該命令消息延后一個(gè)UI線程消息執(zhí)行,這就保證了執(zhí)行的這個(gè)命令消息發(fā)生在我們的繪制之后:

//code RunQueue.java
 void executeActions(Handler handler) {
            synchronized (mActions) {
                ...
                for (int i = 0; i < count; i++) {
                    final HandlerAction handlerAction = actions.get(i);
                    handler.postDelayed(handlerAction.action, handlerAction.delay);//推遲一個(gè)消息
                }
            }
        }

所以,我們只需要在視圖被 attach 之前通過一個(gè) View 來拋出一個(gè)命令消息,就可以檢測(cè)視圖繪制結(jié)束的時(shí)間點(diǎn):

//code DemoActivity.java
 @Override
    protected void onResume() {
        super.onResume();
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                start = SystemClock.uptimeMillis();
                log("繪制開始:height = "+view.getHeight());
            }
        });
    }

    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        view = new View(this);
        view.post(new Runnable() {
            @Override
            public void run() {
                log("繪制耗時(shí):"+(SystemClock.uptimeMillis()-start)+"ms");
                log("繪制結(jié)束后:height = "+view.getHeight());
            }
        });
        this.setContentView(view);
    }
//控制臺(tái)輸出:
01-03 23:39:27.251 27069 27069 D cdw     : --->繪制開始:height = 0
01-03 23:39:27.295 27069 27069 D cdw     : --->繪制耗時(shí):44ms
01-03 23:39:27.295 27069 27069 D cdw     : --->繪制結(jié)束后:height = 1232

我們帶著我們上面的知識(shí)儲(chǔ)備,來看下SnackBar是如何做的呢:

3.Snackbar

SnackBar 系統(tǒng)主要依賴于兩個(gè)類:

  1. SnackBar 作為門面,與業(yè)務(wù)程序交互

  2. SnackBarManager 作為時(shí)序管理器, SnackBarSnackBarManager 的交互,通過 Callback 回調(diào)對(duì)象進(jìn)行

SnackBarManager 的時(shí)序管理跟 NotifycationManager 的很類似不再贅述

SnackBar 通過靜態(tài)方法 make 靜態(tài)構(gòu)造一個(gè) SnackBar:

public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
            @Duration int duration) {
        Snackbar snackbar = new Snackbar(findSuitableParent(view));
        snackbar.setText(text);
        snackbar.setDuration(duration);
        return snackbar;
    }

這里有一個(gè)關(guān)鍵函數(shù) findSuitableParent ,這個(gè)函數(shù)的目的就相當(dāng)于我們上面的 findViewById(R.id.content) 一樣,給 SnackBar 所定義的 Toast 控件找一個(gè)合適的容器:

private static ViewGroup findSuitableParent(View view) {
        ViewGroup fallback = null;
        do {
            if (view instanceof CoordinatorLayout) {
                return (ViewGroup) view;
            } else if (view instanceof FrameLayout) {
                if (view.getId() == android.R.id.content) {//把 `Content` 節(jié)點(diǎn)作為容器
                    ...
                    return (ViewGroup) view;
                } else {
                    // It's not the content view but we'll use it as our fallback
                    fallback = (ViewGroup) view;
                }
            }
            ...
        } while (view != null);

        // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
        return fallback;
    }

我們發(fā)現(xiàn),除了包含 CoordinatorLayout 控件的情況, 默認(rèn)情況下, SnackBar 也是找的 Content 節(jié)點(diǎn)。找到的這個(gè)父節(jié)點(diǎn),作為 Snackbar 構(gòu)造器的形參:

private Snackbar(ViewGroup parent) {
        mTargetParent = parent;
        mContext = parent.getContext();
        ...
        LayoutInflater inflater = LayoutInflater.from(mContext);
        mView = (SnackbarLayout) inflater.inflate(
                R.layout.design_layout_snackbar, mTargetParent, false);
        ...
    }

Snackbar 將生成一個(gè)  SnackbarLayout 控件作為 Toast 控件。最后當(dāng)時(shí)序控制器 SnackBarManager 回調(diào)返回的時(shí)候,通知 SnackBar 顯示,即將 SnackBar.mView 增加到 mTargetParent 控件中去。

這里有人或許會(huì)有疑問,這里使用強(qiáng)引用,會(huì)不會(huì)造成一段時(shí)間內(nèi)的內(nèi)存泄漏呢?
假如你現(xiàn)在彈了 10 個(gè) Toast ,每個(gè) Toast 的顯示時(shí)間是 2s 。也就是說你的最后一個(gè) SnackBar 將被 SnackBarManager 持有至少 20s。而 SnackBar 中又存在有父控件 mTargetParent 的強(qiáng)引用。相當(dāng)于在這20s內(nèi), 你的mTargetParent 和它所持有的 Context (一般是 Activity)無法釋放

這個(gè)其實(shí)是不會(huì)的,原因在于 SnackBarManager 在管理這種回調(diào) callback 的時(shí)候,采用了弱引用。

private static class SnackbarRecord {
        final WeakReference<Callback> callback;
    ....
}

但是,我們從 SnackBar 的設(shè)計(jì)可以看出,SnackBar無法定制具體的樣式: SnackBar 只能生成 SnackBarLayout 這種控件和布局,可能并不滿足你的業(yè)務(wù)需求。當(dāng)然你也可以變更 SnackBarLayout 也能達(dá)到目的。不過,有了上面的知識(shí)儲(chǔ)備,我們完全可以寫一個(gè)自己的 Snackbar

4.基于Toast的改法

從第一篇文章我們知道,我們直接在 Toast.show 函數(shù)外增加 try-catch 是沒有意義的。因?yàn)?Toast.show 實(shí)際上只是發(fā)了一條命令給 NotificationManager 服務(wù)。真正的顯示需要等 NotificationManager 通知我們的 TN 對(duì)象 show 的時(shí)候才能觸發(fā)。NotificationManager 通知給 TN 對(duì)象的消息,都會(huì)被 TN.mHandler 這個(gè)內(nèi)部對(duì)象進(jìn)行處理

//code Toast.java 

private static class TN {

    final Runnable mHide = new Runnable() {// 通過 mHandler.post(mHide) 執(zhí)行
            @Override
            public void run() {
                handleHide();
                mNextView = null;
            }
        };

    final Handler mHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    IBinder token = (IBinder) msg.obj;
                    handleShow(token);// 處理 show 消息
                }
    };
}

NotificationManager 通知給 TN 對(duì)象顯示的時(shí)候,TN 對(duì)象將給 mHandler 對(duì)象發(fā)送一條消息,并在 mHandlerhandleMessage 函數(shù)中執(zhí)行。 當(dāng)NotificationManager 通知 TN 對(duì)象隱藏的時(shí)候,將通過 mHandler.post(mHide) 方法,發(fā)送隱藏指令。不論采用哪種方式發(fā)送的指令,都將執(zhí)行 HandlerdispatchMessage(Message msg) 函數(shù):

//code Handler.java
public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);// 執(zhí)行 post(Runnable)形式的消息
        } else {
            ...
            handleMessage(msg);// 執(zhí)行 sendMessage形式的消息
        }
    }

因此,我們只需要在 dispatchMessage 方法體內(nèi)加入 try-catch 就可以避免 Toast 崩潰對(duì)應(yīng)用程序的影響:

public void dispatchMessage(Message msg) {
    try {
        super.dispatchMessage(msg);
    } catch(Exception e) {}
}

因此,我們可以定義一個(gè)安全的 Handler 裝飾器:

private static class SafelyHandlerWarpper extends Handler {

        private Handler impl;

        public SafelyHandlerWarpper(Handler impl) {
            this.impl = impl;
        }

        @Override
        public void dispatchMessage(Message msg) {
            try {
                super.dispatchMessage(msg);
            } catch (Exception e) {}
        }

        @Override
        public void handleMessage(Message msg) {
            impl.handleMessage(msg);//需要委托給原Handler執(zhí)行
        }
}

由于 TN.mHandler 對(duì)象復(fù)寫了 handleMessage 方法,因此,在 Handler 裝飾器里,需要將 handleMessage 方法委托給 TN.mHandler 執(zhí)行。定義完裝飾器之后,我們就可以通過反射往我們的 Toast 對(duì)象中注入了:

public class ToastUtils {

    private static Field sField_TN ;
    private static Field sField_TN_Handler ;
    static {
        try {
            sField_TN = Toast.class.getDeclaredField("mTN");
            sField_TN.setAccessible(true);
            sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");
            sField_TN_Handler.setAccessible(true);
        } catch (Exception e) {}
    }

    private static void hook(Toast toast) {
        try {
            Object tn = sField_TN.get(toast);
            Handler preHandler = (Handler)sField_TN_Handler.get(tn);
            sField_TN_Handler.set(tn,new SafelyHandlerWarpper(preHandler));
        } catch (Exception e) {}
    }

    public static void showToast(Context context,CharSequence cs, int length) {
        Toast toast = Toast.makeText(context,cs,length);
        hook(toast);
        toast.show();
    }
}

我們?cè)儆玫谝徽轮械拇a測(cè)試一下:

public void showToast(View view) {
        ToastUtils.showToast(this,"hello", Toast.LENGTH_LONG);
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {}
}

等 10s 之后,進(jìn)程正常運(yùn)行,不會(huì)因?yàn)?Toast 的問題而崩潰。

以上就是關(guān)于“Android的Toast問題怎么解決”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI