为何Handler无法配合UI线程工作?Handler与Looper使用问题咨询
嘿,这个问题我太熟悉了!你碰到的Only the original thread that created a view hierarchy can touch its views错误,是Android开发里非常常见的线程规则问题,咱们先搞清楚原因,再给你适配现有代码的解决方案。
问题根源分析
Android有个硬性规则:所有UI控件的修改操作,必须在创建它们的主线程(也就是UI线程)中执行。
看你的代码,ConsumeThread是一个独立的后台线程,你在这个线程里初始化了Handler并启动了Looper,这意味着handleMessage方法的执行上下文是这个后台线程。而你在someLongOperation里直接调用mTextView.setText(),相当于在后台线程操作UI控件,完全违反了Android的UI线程安全规则,所以才会抛出这个致命异常。
解决办法
我给你提供几种适配你现有代码结构的解决方案,你可以根据需求选择:
方案1:用主线程Handler更新UI
在MainActivity中创建一个绑定主线程Looper的Handler,后台线程完成耗时操作后,通过这个Handler发送消息让主线程去更新UI。
修改后的完整代码示例:
public class MainActivity extends AppCompatActivity { Button mButton; TextView mTextView; ConsumeThread mConsumeThread; // 主线程Handler,默认绑定主线程Looper private Handler mMainHandler = new Handler() { @Override public void handleMessage(Message msg) { int result = msg.arg1; // 这里是主线程上下文,可以安全更新UI mTextView.setText("Operation's code is " + result); } }; class ConsumeThread extends Thread{ public Handler mHandler; @Override public void run() { Looper.prepare(); mHandler = new Handler(){ @Override public void handleMessage(Message msg){ int arg = msg.what; someLongOperation(arg); } }; Looper.loop(); } private void someLongOperation(int arg){ // 执行耗时操作(后台线程) arg += 1000; // 发送消息给主线程Handler更新UI Message updateMsg = mMainHandler.obtainMessage(); updateMsg.arg1 = arg; mMainHandler.sendMessage(updateMsg); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView)findViewById(R.id.txt_view); mButton = (Button) findViewById(R.id.button); mConsumeThread = new ConsumeThread(); mConsumeThread.start(); mButton.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { if(mConsumeThread.mHandler != null){ Message msg = mConsumeThread.mHandler.obtainMessage(10); mConsumeThread.mHandler.sendMessage(msg); } } }); } }
方案2:使用runOnUiThread快速切换到主线程
如果不想额外创建主线程Handler,可以直接在后台线程中调用Activity的runOnUiThread方法,把更新UI的逻辑包裹进去,代码改动更小:
只需要修改someLongOperation方法:
private void someLongOperation(int arg){ // 执行耗时操作(后台线程) arg += 1000; // 切换到主线程更新UI MainActivity.this.runOnUiThread(new Runnable() { @Override public void run() { mTextView.setText("Operation's code is " + arg); } }); }
方案3:简化结构(适合简单场景)
如果你的耗时操作逻辑不复杂,其实可以不用额外创建后台线程的Looper,直接在主线程Handler中嵌套子线程执行耗时操作,不过这种嵌套写法可读性稍差,适合简单场景:
// 直接在MainActivity中初始化主线程Handler private Handler mHandler = new Handler(); // 修改按钮点击事件逻辑 mButton.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { // 后台线程执行耗时操作 int arg = 10; arg +=1000; // 切回主线程更新UI mHandler.post(new Runnable() { @Override public void run() { mTextView.setText("Operation's code is " + arg); } }); } }).start(); } });
总结
核心原则永远是:耗时操作放后台线程,UI更新必须回主线程。你的代码原本把UI更新放在了后台线程的Handler回调里,这是问题根源,只要把更新UI的逻辑切换回主线程就可以解决啦。
内容的提问来源于stack exchange,提问作者Stanly T

