中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

Android非UI線(xiàn)程更新UI的探索

2018-07-20    來(lái)源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢(shì)上線(xiàn)!快速搭建集群,上萬(wàn)Linux鏡像隨意使用

眾所周知,在Android中如果在非UI線(xiàn)程更新UI的話(huà),會(huì)拋出異常:

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

因此我們很自然地認(rèn)為只能在UI線(xiàn)程更新UI了。但是在實(shí)際開(kāi)發(fā)中,有時(shí)可能有在非UI線(xiàn)程更新UI的需求,如:想通過(guò)非UI線(xiàn)程來(lái)預(yù)加載View。因此本文將探索在非UI線(xiàn)程更新UI的方式。

checkThread突破口

首先來(lái)找下突破口。從上面提到的異常開(kāi)始切入,拋出該異常的代碼如下: android.view.ViewRootImpl#checkThread

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

這個(gè)方法在View更新的一些關(guān)鍵操作中都會(huì)調(diào)用,如layout,invalid,focus等。而這里的判斷條件是如果 ViewRootImpl 中的 mThread 的值和當(dāng)前調(diào)用的線(xiàn)程不一樣,就拋出異常。而 mThread 賦值是在 ViewRootImpl 構(gòu)造時(shí):

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

這里是將 mThread 直接賦值為構(gòu)造調(diào)用的當(dāng)前線(xiàn)程。再看看 ViewRootImpl 的構(gòu)造調(diào)用的地方是:

android.view.WindowManagerGlobal#addView

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    // ...
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        // ...
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }
    // ...
}

這個(gè) WindowManagerGlobal 其實(shí)就是 WindowManager 的具體實(shí)現(xiàn)。也就是android.view.WindowManager#addView,最終都會(huì)調(diào)用到這里。

在平時(shí) View 操作最多的 Activity 中,當(dāng) Activity resume 時(shí)系統(tǒng)會(huì)將 DecorView 添加到 Window 中,代碼如下:

android.app.ActivityThread#handleResumeActivity

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {
    // ...
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
        final Activity a = r.activity;
        // ...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }
        }
        // ...
    }
    // ...
}

這段邏輯概括起來(lái)就是:

ActivityThread.handleResumeActivity -> WindowManagerGlobal.addView -> new ViewRootImpl -> ViewRootImpl.mThread = Thread.currentThread()

這里都是UI線(xiàn)程調(diào)用的,而 ViewRootImpl.mThread 也就賦值為UI線(xiàn)程,因此在 Activity 中的 View,我們都是只能在UI線(xiàn)程更新的,如果在非UI線(xiàn)程更新的話(huà),就無(wú)法通過(guò) checkThread 檢查。

回到開(kāi)頭提到的問(wèn)題,如果想在非UI線(xiàn)程更新UI,拆分下,大致分為兩步:

  1. 在非UI線(xiàn)程創(chuàng)建 View;
  2. 在非UI線(xiàn)程更新 View。

而這兩步的關(guān)鍵都在怎么通過(guò) checkThread 這個(gè)檢查。

能否在非UI線(xiàn)程創(chuàng)建View?

首先來(lái)看第一個(gè)問(wèn)題,能否在非UI線(xiàn)程創(chuàng)建View。

從上面對(duì) checkThread 的分析可以知道,checkThread 只存在于 ViewRootImpl 中,而ViewRootImpl 是當(dāng)我們通過(guò) WindowManager 向 Window 中添加 View 的時(shí)候才構(gòu)造的一個(gè) rootView。只要我們不向 Window 中添加 View,那么也就不會(huì)觸發(fā) checkThread。

因此在非UI線(xiàn)程創(chuàng)建 View 理論上是可行的。無(wú)論是通過(guò)直接 new View,還是通過(guò) LayoutInflater 。

能否在非UI線(xiàn)程更新View?

上面只是通過(guò)非UI線(xiàn)程來(lái)創(chuàng)建 View,那么在非UI更新 View 是否可行呢?這里就涉及到在更新UI時(shí)怎么通過(guò) checkThread 的檢查。

從上面的分析可以得知,如果 ViewRootImpl.mThread 的值和當(dāng)前更新UI調(diào)用的線(xiàn)程是一樣的,那么就不會(huì)拋出異常。

那么試想,如果 ViewRootImpl.mThread 的值是非UI線(xiàn)程,而且更新UI也是在同一個(gè)非UI線(xiàn)程中,那我們是不是就可以通過(guò) checkThread 檢查了呢?

同時(shí)還有個(gè)問(wèn)題,怎么將 ViewRootImpl.mThread 賦值為一個(gè)非UI線(xiàn)程?

做過(guò)懸浮窗開(kāi)發(fā)或者對(duì) WMS 源碼熟悉的應(yīng)該知道,通過(guò) Context 可以獲得一個(gè) WindowManager 對(duì)象,顧名思義,它就是用來(lái)操作 Window 的,Activity 也正是通過(guò)它顯示在 Window 上的。

結(jié)合上面的分析,只要我們將 WindowManager.addView 這一步放到非UI線(xiàn)程去做,那么 ViewRootImpl.mThread 必然指向的是當(dāng)前調(diào)用的非UI線(xiàn)程,后續(xù)自然就可以在這個(gè)非UI線(xiàn)程去更新這個(gè)View了。

示例

下面通過(guò)一個(gè)示例來(lái)驗(yàn)證下上述的想法:

一個(gè)簡(jiǎn)單的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</FrameLayout>

非UI線(xiàn)程創(chuàng)建View和更新View的示例:

public static void showViewInNonUiThread(final Activity context) {
    final HandlerThread handlerThread = new HandlerThread("view_test");
    handlerThread.start();
    final Handler handler = new Handler(handlerThread.getLooper());
    handler.post(new Runnable() {
        @Override
        public void run() {
            WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
            lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
            lp.gravity = Gravity.LEFT | Gravity.TOP;
            lp.format = PixelFormat.RGBA_8888;
            lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
            lp.token = context.getWindow().getDecorView().getWindowToken();
            lp.packageName = context.getPackageName();

            WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            View contentView = LayoutInflater.from(context).inflate(R.layout.layout_test, null);
            final Button button = (Button) contentView.findViewById(R.id.button);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(v.getContext(), "test click", Toast.LENGTH_SHORT).show();
                    button.setText("test update ui3.");
                }
            });
            button.setText("test update ui.");
            handler.post(new Runnable() {
                @Override
                public void run() {
                    button.setText("test update ui2.");
                }
            });
            windowManager.addView(contentView, lp);
        }
    });
}

將上述代碼在 Activity 中運(yùn)行下,可以正常的顯示,而且點(diǎn)擊事件,View 的更新都能正常執(zhí)行,同時(shí)不影響UI線(xiàn)程的正常運(yùn)行。因此該方案理論上是可行的。

穩(wěn)定性

該方案本人在項(xiàng)目的測(cè)試環(huán)境上已經(jīng)做過(guò)一些場(chǎng)景的應(yīng)用,暫未發(fā)現(xiàn)任何問(wèn)題,但是不排除有未知的風(fēng)險(xiǎn),畢竟這不是常規(guī)的方案。

因?yàn)?Android 系統(tǒng)默認(rèn)所有的 View 都在UI線(xiàn)程更新,因此不存在線(xiàn)程間的同步問(wèn)題。但是如果需要使用多線(xiàn)程來(lái)創(chuàng)建更新 View 的話(huà),多線(xiàn)程的問(wèn)題不得不考慮,比如靜態(tài)變量的同步問(wèn)題。

如:Android 中使用最廣泛的 TextView,其內(nèi)部使用 android.text.TextLine 來(lái)表示一行文本,同是負(fù)責(zé) TextView 的繪制,而這個(gè)類(lèi)內(nèi)部就有個(gè)靜態(tài)的 cache :TextLine#sCached,不過(guò)好在其內(nèi)部對(duì) sCached 的所有操作都已經(jīng)加鎖。

但是不排除系統(tǒng)中還有其他控件中有未知的坑。

應(yīng)用

  1. 性能優(yōu)化:對(duì) View 的預(yù)加載,可以使用非UI線(xiàn)程來(lái)實(shí)例化 View ,然后放到UI線(xiàn)程去更新,節(jié)省 View 創(chuàng)建的開(kāi)銷(xiāo)。
  2. 浮層:如果有些浮層本身存在大量復(fù)雜的繪制操作,而為了避免和UI繪制搶占資源,可以將其放到非UI線(xiàn)程來(lái)做,如:視頻小窗。

 

來(lái)自:https://techblog.toutiao.com/2017/08/16/untitled-5/

 

標(biāo)簽: isp 代碼

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:初學(xué)者指南:神經(jīng)網(wǎng)絡(luò)在自然語(yǔ)言處理中的應(yīng)用

下一篇:如何打造一個(gè)日均PV千萬(wàn)級(jí)別的大型系統(tǒng)?