欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 子线程中更新UI 详解

程序员文章站 2022-06-10 23:36:02
...

来源:

https://blog.csdn.net/shift_wwx/article/details/81012146


前言:

Android 官方有句话:“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.

这句话可能会给人误解,认为android 中ui 的操作必须要在UI 线程中进行,但这里通过android 的source code 最终会发现其实通过子线程也是可以做到的,只不过需要了解其中的详细的流程。

另外,这里强调的是在子线程中更新UI,而不是通过子线程异步的方式去更新UI 线程中的UI(有点绕)。

https://developer.android.com/guide/components/processes-and-threads

这里说的其实是通过线程去异步更新UI,最终还是通过UI 线程去更新,跟这里的主题不一样的。


实例引路:

public class ShowThreadUI extends Activity implements OnClickListener {
    private static final String TAG = "ShowThreadUI";

    private Button mTestButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.show_thread_ui);

        mTestButton = (Button) findViewById(R.id.show_1);

        initTestButton();
    }

    private void initTestButton() {
        mTestButton.setOnClickListener(this);

        Button show_2 = (Button) findViewById(R.id.show_2);
        show_2.setOnClickListener(this);

        Button show_3 = (Button) findViewById(R.id.show_3);
        show_3.setOnClickListener(this);
    }

    private void testShow1() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                WindowManager windowManager = getWindowManager();
                TextView textView = new TextView(getApplicationContext());
                textView.setText("test 1");
                textView.setTextColor(0x54FF9F);
                windowManager.addView(textView, new WindowManager.LayoutParams());
            }
        }).start();
    }
    
    private void testShow2() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                mTestButton.setText("button text changed");
            }
        }).start();
    }
    
    private void testShow3() {
        new TestThread().start();
    }
    
    class TestThread extends Thread{
        @Override
        public void run() {
            Looper.prepare();
            TextView tx = new TextView(ShowThreadUI.this);
            tx.setText("show me, show me");
            tx.setTextColor(0x0000EE);
            tx.setGravity(Gravity.CENTER);
            WindowManager wm = ShowThreadUI.this.getWindowManager();
            WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                    250, 150, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                    WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
            wm.addView(tx, params);
            Looper.loop();
        }
    }

    @Override
    public void onClick(View view) {
        int id = view.getId();
        switch (id) {
            case R.id.show_1:
                testShow1();
                break;
            case R.id.show_2:
                testShow2();
                break;
            case R.id.show_3:
                testShow3();
                break;
            default:
                break;
        }
    }
}

1、点击button 1

这个时候会报错:

--------- beginning of crash
11-07 06:11:24.118  2296  2398 E AndroidRuntime: FATAL EXCEPTION: Thread-2
11-07 06:11:24.118  2296  2398 E AndroidRuntime: Process: com.shift.testapp, PID: 2296
11-07 06:11:24.118  2296  2398 E AndroidRuntime: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at android.os.Handler.<init>(Handler.java:204)
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at android.os.Handler.<init>(Handler.java:118)
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at android.view.ViewRootImpl$ViewRootHandler.<init>(ViewRootImpl.java:3679)
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at android.view.ViewRootImpl.<init>(ViewRootImpl.java:4012)
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:346)
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at com.shift.testapp.ShowThreadUI$1.run(ShowThreadUI.java:46)
11-07 06:11:24.118  2296  2398 E AndroidRuntime: 	at java.lang.Thread.run(Thread.java:764)

从堆栈信息来看最终会在 ViewRootImpl 中的ViewRootHandler 触发错误,先来看下ViewRootHandler:

    final class ViewRootHandler extends Handler {
        @Override
        public String getMessageName(Message message) {

在构造的时候出错的,来看下Handler 的204行:

    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

可以看到最终原因是Handler 中获取Looper 为null。

重新梳理流程,button 1 点击的时候会新开一个线程,在这个线程里创建了UI,WindowManager在addView 的时候会创建ViewRootImpl,其中的Handler 必须要依赖线程中的Looper,Android异步消息处理线程之----Looper+MessageQueue+Handler 中提到Handler 是运行在创建它的线程中,而每个Handler 中的Looper 需要跟Thread 一一对应,换句话说,就是Thread中的Handler 必须有个跟Thread对应的Looper,而这里显然是为null。

结论,通过WindowManger addView 方式创建UI 的时候,需要伴随着创建Looper,Handler 需要。


2、点击button 2

这个时候会报错:

--------- beginning of crash
11-07 06:12:01.827  2422  2471 E AndroidRuntime: FATAL EXCEPTION: Thread-2
11-07 06:12:01.827  2422  2471 E AndroidRuntime: Process: com.shift.testapp, PID: 2422
11-07 06:12:01.827  2422  2471 E AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7334)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1165)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.widget.TextView.checkForRelayout(TextView.java:8531)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.widget.TextView.setText(TextView.java:5394)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.widget.TextView.setText(TextView.java:5250)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at android.widget.TextView.setText(TextView.java:5207)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at com.shift.testapp.ShowThreadUI$2.run(ShowThreadUI.java:55)
11-07 06:12:01.827  2422  2471 E AndroidRuntime: 	at java.lang.Thread.run(Thread.java:764)

在ViewRootImpl requestLayout 的时候会调用checkThread:

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
mThread 现在是创建ViewRootImpl 时候的Thread,而这里Thread.currentThread 现在是当前运行的Thread,上面的button 1 中再WindowManger addView 的时候会创建ViewRootImpl,那在activity 中正常运行情况下是什么时候呢?下面会继续说明的。

结论,线程之前创建的View或者UI,在线程中是无法更新的,只有在创建UI的线程中更新该UI。


3、点击button 3

顺利运行,跟button 1 中流程唯一区别就是添加了Looper,证明了button 1 中说到的结论。


修改实例

在原来实例的基础上,我们进行一个修改,如下:

public class ShowThreadUI extends Activity implements OnClickListener {
    private static final String TAG = "ShowThreadUI";

    private Button mTestButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.show_thread_ui);

        mTestButton = (Button) findViewById(R.id.show_1);
        testShow4();

        initTestButton();
    }

	...
	...

    private void testShow4() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                mTestButton.setText("Text changed");
            }
        }).start();
    }

}

在原先的基础上多加了testShow4(),也是在子线程中更新UI。这个时候有人可能看出来,这里跟上面的button 2 处理的流程是一样的,应该会报一样的错误吧?实际并非如此,这里运行之后是正常的。并不是说在onCreate() 中创建子线程就可以正常运行,如果在setText() 函数之前加上200ms 的延时,修改代码如下:

    private void testShow4() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                mTestButton.setText("Text changed");
            }
        }).start();
    }

运行结果如下,会同样出现报错:

11-05 00:04:36.256  1937  1976 E AndroidRuntime: FATAL EXCEPTION: Thread-2
11-05 00:04:36.256  1937  1976 E AndroidRuntime: Process: com.shift.testapp, PID: 1937
11-05 00:04:36.256  1937  1976 E AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7334)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1165)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.view.View.requestLayout(View.java:21999)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.widget.TextView.checkForRelayout(TextView.java:8531)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.widget.TextView.setText(TextView.java:5394)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.widget.TextView.setText(TextView.java:5250)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at android.widget.TextView.setText(TextView.java:5207)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at com.shift.testapp.ShowThreadUI$3.run(ShowThreadUI.java:76)
11-05 00:04:36.256  1937  1976 E AndroidRuntime: 	at java.lang.Thread.run(Thread.java:764)
11-05 00:04:36.273   692  3641 W ActivityManager:   Force finishing activity com.shift.testapp/.ShowThreadUI

这里的200ms 延迟为什么会导致结果有这么大的反差,那主要是看在启动activity 的时候onCreate() 之后的200ms 中做了些什么了。如果不加延时,将testShow4() 放到onResume 中是否也能正常呢?

    @Override
    protected void onResume() {
        super.onResume();
        
        testShow4();
    }
答案是可以运行的。那也就是说子线程更新UI 跟Activity 的生命周期并没有直接关系,而是跟Activity 启动过程中某个特殊流程有关系。

从log 中我们看到setText() 最后是触发ViewRootImpl 的checkThread() 函数,要求创建View 的Thread 必须是跟更新View 的Thread 是一个,那结合上面onCreate()、onResume() 可行性,大胆猜测下在这个时候并没有创建ViewRootImpl,更不会调用到其中的checkThread() 函数。

下面来只需要确认ViewRootImpl 是什么时候创建的即可。

1、handleLaunchActivity

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {

        ...
        ...

        // Make sure we are running with the most recent config.
        handleConfigurationChanged(null, null); //onConfigurationChanged就是这里

        ...
        ...

        WindowManagerGlobal.initialize(); //获取WMS

        Activity a = performLaunchActivity(r, customIntent); //onCreate()就是在这里,第一次onStart也在这里

        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            reportSizeConfigurations(r);
            Bundle oldState = r.state;
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

            ...
            ...

        }
		
	...
	...
    }

详细的流程可以看下source code,这里主要确认onCreate() 中并没有View 相关的创建,其中:

WindowManagerGlobal.initialize();

这个应该是在系统刚起来的时候初始化,系统起来之后应该不需要。

performLaunchActivity() 中activity.attach() 也很重要,很多初始化工作,例如,window、mUiThread等等。


2、handleResumeActivity

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

        ...
        ...

        // TODO Push resumeArgs into the activity for consideration
	//onRestart、onStart、onResume都在这里,当然,如果不是在stop状态,直接onResume,onStart会在launch的时候触发
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {

            ...
            ...

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                ...
                ...

                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }

            ...
            ...

        }
    }

详细的流程看source code,主要是一些初始化的工作,例如window、DectorView等等,如果是stopped 状态会在这里调用到onRestart、onStart、onResume 等流程。

主要来看下Activity 中的makeVisible():

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

最后看下WindowManager 中addView():(具体看WindowManagerImpl)

    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        android.util.SeempLog.record_vg_layout(383,params);
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

会调用到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);

            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

最后ViewRootImpl 是在这里创建的,所以,在上面onResume() 中调用testShow4() 也是可以的,因为onResume() 也是在ViewRootImpl 创建之前。

其实,从这段code 还可以发现,只要在View 创建的时候,也就是在root.setView() 之前利用子线程都是可以的。



总结

1、子线程是可以更新UI的,并不是一定要在UI线程中

2、子线程更新UI 跟Activity 的生命周期并没有直接关系,onResume 的时候也能通过子线程更新UI

2、子线程更新UI 需要在Handler 中进行(如果button 1的线程),所以,给子线程必须要配备一个对应的Looper



附加:

1、访问UI 线程的几种方法

详细可以参考:https://developer.android.com/guide/components/processes-and-threads


2、在AppOps中利用子线程更新UI

    public void showDialog(Context context, AppOpsService service, int code, int uid, String packageName) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                if (mDialog == null) {
                    mDialog = new PermissionDialog(context, service, code, uid, packageName);//一定要在子线程中创建UI
                }
                mDialog.show();
                Looper.loop();
            }
        }).start();
    }
public class PermissionDialog extends BasePermissionDialog {
    private final int mOpCode;
    private final String mPackageName;
    private final AppOpsService mService;
    private final View mCustomView;
    private final int mUid;
    private final Context mContext;

    private boolean mIsRemembered = false;
    private String mTitleLabel;

    private static final int ALLOWED_REQ = 0x2;
    private static final int IGNORED_REQ = 0x4;
    private static final int IGNORED_REQ_TIMEOUT = 0x8;
    private static final long TIMEOUT_WAIT = 5 * 1000;

    private PermissionsTimer mTimer;

    public PermissionDialog(Context contextId, AppOpsService opsService,
                            int code, int uid, String packageName) {
        super(contextId);
        mOpCode = code;
        mUid = uid;
        mService = opsService;
        mPackageName = packageName;
        mContext = contextId;
        mCustomView = getLayoutInflater().inflate(R.layout.permission_confirmation_dialog, null);

        WindowManager.LayoutParams paraDef = getWindow().getAttributes();
        paraDef.setTitle(mContext.getString(R.string.appops_note_dialog_title));
        paraDef.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR
                | WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
        getWindow().setAttributes(paraDef);

        initDialog();
    }
	
}