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

Android非UI線程更新UI的探索

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

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

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

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

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

checkThread突破口

首先來找下突破口。從上面提到的異常開始切入,拋出該異常的代碼如下: 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)用的線程不一樣,就拋出異常。而 mThread 賦值是在 ViewRootImpl 構(gòu)造時(shí):

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

這里是將 mThread 直接賦值為構(gòu)造調(diào)用的當(dāng)前線程。再看看 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);
            }
        }
        // ...
    }
    // ...
}

這段邏輯概括起來就是:

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

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

回到開頭提到的問題,如果想在非UI線程更新UI,拆分下,大致分為兩步:

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

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

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

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

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

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

能否在非UI線程更新View?

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

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

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

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

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

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

示例

下面通過一個(gè)示例來驗(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線程創(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線程的正常運(yùn)行。因此該方案理論上是可行的。

穩(wěn)定性

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

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

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

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

應(yīng)用

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

 

來自: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īng)用

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