文章目录
- 一、Android为什么不能在子线程更新UI?
- 二、为什么Android中要设计为只能在UI线程中去更新UI?
- 三、如果不在UI线程中更新UI,可能会出现什么问题呢?
- 四、ViewRootImp是在onActivityCreated方法后面创建的吗?
- 五、为什么一开始在Activity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢
- 六、Android中子线程真的不能更新UI吗?
- 七、在实际开发中,如何将子线程的任务结果传递到UI线程进行更新?
- 七、使用子线程更新UI有实际应用场景吗
- 八、扩展阅读
一、Android为什么不能在子线程更新UI?
viewRootImpl
对象是在Activity
中的onResume
方法执行完成之后,View
变得可见时才创建的,之前的操作是没有进行线程检查的,所以没有报错。但是ViewRootImpl
创建之后,由于进行了checkThread
操作,所以就不能在子线程更改UI
了。
当访问 UI
时,ViewRootImpl
会调用 checkThread
方法去检查当前访问 UI
的线程是否为创建 UI
的那个线程,如果不是。则会抛出异常。
当然可以,从系统源码的角度来解释为什么 Android 中子线程不能直接更新 UI。
ViewRootImpl
的创建
在 Android 应用的生命周期中,ViewRootImpl
对象是在 Activity
的视图变得可见时被创建的。ViewRootImpl
是负责管理视图层次结构、处理测量、布局和绘制的核心类。
ActivityThread.java
中的 handleResumeActivity
方法是一个关键点:
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
// ...省略部分代码...
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;
wm.addView(decor, l);
// 在这里创建 ViewRootImpl 并关联 DecorView
}
// ...省略部分代码...
}
在 handleResumeActivity
方法中,当 Activity
被恢复到前台时,会创建 ViewRootImpl
并关联到 DecorView
(根视图)。
checkThread
方法
在 ViewRootImpl
内部,每当你尝试操作 UI 时,都会调用 checkThread
方法来检查当前线程是否为主线程。
ViewRootImpl.java
中的 checkThread
方法:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
checkThread
方法会比较当前线程 Thread.currentThread()
和创建 ViewRootImpl
时的线程 mThread
。如果它们不相同,则抛出 CalledFromWrongThreadException
异常。
- 触发
checkThread
检查的示例
当你在子线程中尝试更新 UI 时,例如通过调用 TextView.setText
方法,就会触发上述检查。以下是一个简化版本的示例:
public void setText(CharSequence text) {
// ...省略部分代码...
checkForThreadViolation();
// ...省略部分代码...
}
private void checkForThreadViolation() {
if (mDispatchSystemThreadChecks && Looper.myLooper() != mViewRootImpl.mThread.getLooper()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
在 setText
方法中,会调用 checkForThreadViolation
来检查当前线程是否为主线程。如果不是,则抛出 CalledFromWrongThreadException
。
- 小结
从系统源码的角度来看,Android 强制 UI 操作在主线程中进行的原因主要包含以下几个方面:
-
ViewRootImpl
的创建:ViewRootImpl
在Activity
前台显示时创建,并关联到视图层次结构中。
-
checkThread
方法:ViewRootImpl
内部的checkThread
方法会检查所有 UI 操作是否在主线程进行。- 如果检测到非主线程操作 UI,就会抛出
CalledFromWrongThreadException
异常。
这种设计确保了 UI 操作的安全性和一致性,因为 Android 的视图系统并不是线程安全的,在多线程操作下可能会导致不可预期的行为或崩溃。因此,强制在主线程中操作 UI 是保证应用稳定性的重要机制。
二、为什么Android中要设计为只能在UI线程中去更新UI?
面试官: “为什么Android中要设计为只能在UI线程中去更新UI呢?”
面试者: “这是为了保证UI操作的线程安全性和一致性。具体来说,有以下几个原因:”
-
线程安全:
- Android的UI组件并不是线程安全的。如果多个线程同时操作同一个UI组件,可能会导致数据竞争、死锁等问题,造成应用程序的不稳定甚至崩溃。
-
一致性和响应性:
- 主线程负责处理用户输入、绘制界面等操作。如果允许在子线程中更新UI,可能会导致绘制过程和用户交互的不一致,出现界面闪烁、控件状态异常等问题。将所有UI更新操作限制在主线程,可以确保用户界面的状态和操作的一致性。
-
绘制机制:
- Android的UI绘制机制是单线程的,主线程负责周期性地刷新界面。如果子线程直接修改UI,可能会在绘制过程中打断绘制流程,导致界面显示错误或绘制不完整。
-
简化开发:
- 强制在主线程更新UI,可以简化开发者的工作,避免开发者自己处理复杂的线程同步问题,从而减少Bug的产生,提高应用的稳定性和可维护性。
三、如果不在UI线程中更新UI,可能会出现什么问题呢?
面试官: “如果不在UI线程中更新UI,可能会出现什么问题呢?”
面试者: “如果在非UI线程中更新UI,可能会出现以下问题:”
-
界面更新不及时:
- 由于绘制和更新操作可能被打断,界面可能不会及时反映最新的状态,用户体验会变差。
-
应用崩溃:
- 非UI线程操作UI组件会导致数据竞争等问题,可能会触发异常,导致应用崩溃。
-
界面闪烁或不稳定:
- 多线程操作UI会造成绘制过程不一致,导致界面闪烁、控件状态异常等问题。
四、ViewRootImp是在onActivityCreated方法后面创建的吗?
实际上,ViewRootImpl
并不是在 onActivityCreated
方法后创建的。它是在 Activity
的视图变得可见时被创建的。让我们来看一下 Activity
的启动过程中关于 ViewRootImpl
的创建时机。
-
当调用
startActivity
启动一个新的Activity
时,系统会执行一系列的生命周期方法,包括onCreate
、onStart
、onResume
等。 -
在
onCreate
方法中,通常会设置布局资源并通过setContentView
方法加载布局文件,这时候并不会立即创建ViewRootImpl
。 -
当
Activity
进入前台,即将对用户可见时,系统会调用onResume
方法。在onResume
方法中,会进行一系列的准备工作,包括创建ViewRootImpl
对象并关联到DecorView
上,从而使得整个视图层次结构能够与窗口管理器进行交互。
因此,ViewRootImpl
的创建时机应该是在 onResume
方法中,在 Activity
进入前台并将对用户可见时。这时候才会触发 ViewRootImpl
的创建和与 DecorView
的关联,从而开始处理视图的测量、布局、绘制等操作。
五、为什么一开始在Activity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢
在 Activity
的生命周期中,虽然严格来说 UI 操作应该在主线程中进行,但在某些情况下,比如在 onCreate
方法中立即创建子线程并访问 UI 元素,程序可能不会立刻崩溃。这是因为在 onCreate
方法中,视图层次结构还没有完全初始化完毕。让我们来分析这个现象。
1. Activity 生命周期概述
当一个 Activity
被创建时,系统会依次调用以下生命周期方法:
onCreate
onStart
onResume
2. 视图层次结构的初始化
在 onCreate
方法中,通常会调用 setContentView
来设置布局文件:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 在这里创建子线程并访问UI
}
setContentView
会将 XML 布局文件解析成对应的视图对象,并添加到 Activity
的视图层次结构中,但此时视图层次结构还没有完全初始化和展示。
3. 子线程访问 UI 的时机
如果你在 onCreate
方法中创建子线程并立即访问 UI 元素,例如:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
TextView textView = findViewById(R.id.textView);
textView.setText("Hello from thread");
}
}).start();
}
这种情况下,子线程很可能在视图层次结构完全初始化之前就尝试访问 UI 元素。由于 ViewRootImpl
还没有完全创建或关联到视图树上,checkThread
检查可能尚未触发。因此,虽然这段代码理论上是不安全的,但在特定时机下不会立刻引发异常。
4. 潜在问题
尽管上述情况可能不会立刻导致崩溃,但它仍然是不安全和错误的做法。任何在子线程中访问 UI 的操作都有可能导致难以调试的竞态条件和不一致性问题。一旦视图层次结构初始化完成,后续在子线程中访问 UI 将触发 ViewRootImpl
中的线程检查机制,抛出 CalledFromWrongThreadException
异常。
正确的做法
为了确保线程安全,所有的 UI 更新操作都应该在主线程中进行。可以使用 Handler
或 runOnUiThread
方法在主线程中执行 UI 更新,例如:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
TextView textView = findViewById(R.id.textView);
textView.setText("Hello from thread");
}
});
}
}).start();
}
这样可以确保所有的 UI 操作都是在主线程中进行的,避免了潜在的多线程问题。
六、Android中子线程真的不能更新UI吗?
在 Android 开发中,我们经常听到一句话:“只能在主线程(UI线程)中更新UI”。但实际上,任何线程都可以更新自己创建的UI,只是需要满足特定的条件:
- 在
ViewRootImpl
创建之前
在 ViewRootImpl
(负责管理视图树的根视图)创建之前,可以随意在任何线程中修改 UI,因为此时不会进行线程检查(即不会执行 checkThread
方法)。
- 在
ViewRootImpl
创建之后
一旦 ViewRootImpl
创建完成后,你必须保证“创建 ViewRootImpl
的操作”和“更新 UI 的操作”在同一个线程中。换句话说,需要在同一个线程中调用 ViewManager
的 addView
和 updateViewLayout
方法。
注:
ViewManager
是一个接口,WindowManager
接口继承了它。我们通常通过WindowManager
(具体实现为WindowManagerImpl
)来进行视图的添加、删除和更新操作。
此外,在对应的线程中,还需要创建 Looper
并调用 Looper
的 loop
方法,开启消息循环,使得该线程能够处理消息队列中的消息。
示例代码
下面是一个简单的示例,展示如何在子线程中更新UI。
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private ViewGroup rootLayout;
private static final int UPDATE_UI = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootLayout = findViewById(R.id.root_layout);
// 创建一个子线程
new Thread(new Runnable() {
@Override
public void run() {
// 创建一个 Looper 并启动消息循环
Looper.prepare();
// 创建一个 Handler 用于在子线程中更新UI
Handler handler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == UPDATE_UI) {
// 更新 UI
TextView textView = new TextView(MainActivity.this);
textView.setText("Hello from the child thread!");
rootLayout.addView(textView);
}
}
};
// 发送更新 UI 的消息
handler.sendEmptyMessage(UPDATE_UI);
// 开启消息循环
Looper.loop();
}
}).start();
}
}
在这个示例中,我们在 onCreate
方法中启动了一个子线程。在子线程中,我们创建了一个 Looper
并启动了消息循环。然后,我们使用 Handler
在子线程中发送消息并更新 UI。
通过这种方式,我们可以确保在子线程中安全地更新UI。但通常情况下,为了避免复杂性和潜在的错误,建议还是在主线程中操作UI。
希望这个版本能帮助您更好地理解在 Android 中子线程更新 UI 的问题。
七、在实际开发中,如何将子线程的任务结果传递到UI线程进行更新?
面试官: “你刚才提到的原因都很清楚。那你能说一下,在实际开发中,如何将子线程的任务结果传递到UI线程进行更新吗?”
面试者: “当然,以下是几种常见的方法:”
- new Handler()
Button button = new Button(this);
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1) {
button.setText("子线程更新UI");
}
}
};
new Thread(new Runnable() {
@Override
public void run() {
// Message和Handler均可获得msg
// Message msg = handler.obtainMessage();
Message msg = Message.obtain();
msg.what = 1;
msg.arg1 = 10;
handler.sendMessage(msg);
}
}).start();
- new Handler.Callback()
Button button = new Button(this);
private Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == 1) {
button.setText("子线程更新UI");
}
return true;
}
};
Handler handler = new Handler(callback);
new Thread(new Runnable() {
@Override
public void run() {
// Message和Handler均可获得msg
// Message msg = handler.obtainMessage();
Message msg = Message.obtain();
msg.what = 1;
msg.arg1 = 11;
handler.sendMessage(msg);
}
}).start();
-
new Handler().post(Runnable r)
- Handler可以将任务从子线程传递到主线程,在主线程中处理消息。
Handler handler = new Handler(Looper.getMainLooper()); new Thread(new Runnable() { @Override public void run() { // 执行后台任务 final String result = performTask(); // 在主线程更新UI handler.post(new Runnable() { @Override public void run() { textView.setText(result); } }); } }).start();
-
new Handler().postDelayed(Runnable r, long delayMillis)
Handler handler = new Handler(Looper.getMainLooper()); new Thread(new Runnable() { @Override public void run() { // 执行后台任务 final String result = performTask(); // 在主线程更新UI handler.postDelayed(new Runnable() { @Override public void run() { textView.setText(result); } }, 3000); } }).start();
-
使用Activity.runOnUiThread():
- 直接在主线程执行更新UI的代码。
runOnUiThread(new Runnable() { @Override public void run() { textView.setText("更新后的文本"); } });
-
使用View.post():
- 将任务提交到View的消息队列,在主线程执行。
textView.post(new Runnable() { @Override public void run() { textView.setText("更新后的文本"); } });
-
使用View.post():
- 将任务提交到View的消息队列,在主线程执行。
textView.postDelay(new Runnable() { @Override public void run() { textView.setText("更新后的文本"); } },3000);
-
使用AsyncTask:
- 在后台线程执行任务,并在主线程更新UI。
new AsyncTask<Void, Void, String>() { @Override protected String doInBackground(Void... params) { // 执行后台任务 return performTask(); } @Override protected void onPostExecute(String result) { // 在主线程更新UI textView.setText(result); } }.execute();
-
EventBus
定义一个事件类:// 定义一个事件类 public class MessageEvent { public final String message; public MessageEvent(String message) { this.message = message; } }
在主活动(或任何需要更新UI的地方)中,注册和接收事件:
import android.os.Bundle; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; public class MainActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.text_view); // 注册EventBus EventBus.getDefault().register(this); // 模拟子线程发布事件 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); // 模拟耗时操作 EventBus.getDefault().post(new MessageEvent("Hello from background thread!")); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(MessageEvent event) { // 在主线程中接收事件并更新UI textView.setText(event.message); } @Override protected void onDestroy() { super.onDestroy(); // 取消注册EventBus EventBus.getDefault().unregister(this); } }
在这个例子中:
- 定义事件类:我们定义了一个简单的
MessageEvent
类,用于传递字符串消息。 - 注册EventBus:在
MainActivity
的onCreate
方法中,我们使用EventBus.getDefault().register(this)
来注册EventBus。 - 发布事件:在一个子线程中,我们通过
EventBus.getDefault().post(new MessageEvent("..."))
来发布事件。 - 接收事件:使用
@Subscribe
注解来标记一个方法,这个方法就是用来接收事件的。在这里,我们指定threadMode = ThreadMode.MAIN
,这表示该方法将在主线程中执行,从而可以安全地更新UI。 - 取消注册:在
onDestroy
方法中,我们使用EventBus.getDefault().unregister(this)
来取消注册,以避免内存泄漏。
通过这种方式,我们可以轻松地在子线程中发布事件,并在主线程中接收并处理这些事件,从而安全地更新UI。
- 定义事件类:我们定义了一个简单的
-
Rxjava
Disposable disposable = Observable.fromCallable(new Callable<String>() {
@Override
public String call() throws Exception {
return performTask();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
@Override
public void accept(String result) throws Exception {
textView.setText(result);
}
});
// 记得在适当的时候释放资源
disposable.dispose();
面试官: “非常好,你对为什么只能在UI线程更新UI以及如何在实际开发中实现这一点有很清楚的理解。你的回答非常完整。”
七、使用子线程更新UI有实际应用场景吗
Android 中的 SurfaceView
通常会通过一个子线程来进行页面的刷新。
如果我们的自定义 View
需要频繁刷新,或者刷新时数据处理量比较大,那么可以考虑使用 SurfaceView
来取代 View
SurfaceView
是 Android 中用于实现高效绘图的视图组件,它适合于需要频繁刷新或进行复杂绘制操作的场景。与普通的 View
不同,SurfaceView
提供了一个独立的绘图表面,这个表面可以在独立的线程上进行绘制操作。
SurfaceView 刷新UI的机制
-
独立绘图表面:
SurfaceView
创建了一个独立的绘图表面(Surface),这个表面可以直接由子线程来访问和更新。这是SurfaceView
的关键特性,它允许在不影响主线程(UI线程)的情况下进行绘图操作。 -
SurfaceHolder:
SurfaceView
通过SurfaceHolder
提供对其绘图表面的访问接口。SurfaceHolder
提供了一系列的方法来锁定和解锁画布,用于在独立的线程上进行绘制。
使用子线程刷新UI的示例
- 定义 SurfaceView 子类
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder surfaceHolder;
private DrawingThread drawingThread;
public MySurfaceView(Context context) {
super(context);
surfaceHolder = getHolder();
surfaceHolder.addCallback(this);
drawingThread = new DrawingThread(surfaceHolder);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
drawingThread.setRunning(true);
drawingThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Handle changes here
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
drawingThread.setRunning(false);
while (retry) {
try {
drawingThread.join();
retry = false;
} catch (InterruptedException e) {
// Retry stopping the thread
}
}
}
}
- 定义绘图线程
class DrawingThread extends Thread {
private SurfaceHolder surfaceHolder;
private boolean running;
public DrawingThread(SurfaceHolder surfaceHolder) {
this.surfaceHolder = surfaceHolder;
running = false;
}
public void setRunning(boolean running) {
this.running = running;
}
@Override
public void run() {
while (running) {
Canvas canvas = null;
try {
canvas = surfaceHolder.lockCanvas();
synchronized (surfaceHolder) {
// Perform drawing operations on the canvas here
canvas.drawColor(Color.BLACK); // Example: Clear the canvas with black color
}
} finally {
if (canvas != null) {
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
}
}
主要要点
-
SurfaceHolder.Callback:
MySurfaceView
实现了SurfaceHolder.Callback
接口,以便接收SurfaceView
表面创建、变更和销毁的事件。这些回调方法用于控制绘图线程的启动和停止。 -
DrawingThread:绘图线程 (
DrawingThread
) 在独立的线程中运行,避免阻塞主线程。在run
方法中,线程会不断地锁定画布进行绘制,然后解锁并提交绘制结果。 -
线程控制:在
surfaceCreated
方法中启动绘图线程,在surfaceDestroyed
方法中停止绘图线程。这确保了在SurfaceView
被创建和销毁时正确地管理线程的生命周期。
通过这种方式,SurfaceView
可以利用子线程进行UI的刷新操作,从而提高绘图的效率,减少对主线程的干扰。不过,需要注意的是,在进行多线程绘图时,要小心处理线程同步问题,以避免竞争和死锁等问题。