Android 子线程中更新UI 详解
来源:
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();
}
}