Android进阶——Handler的应用之解决Only the original thread that created a view hierarchy can touch its views

本文介绍了一个关于Android UI线程安全的问题,即在非UI线程中更新UI导致的Onlytheoriginalthreadthatcreatedaviewhierarchycantouchitsviews错误。文章详细分析了问题产生的原因,并给出了两种解决方案:一种是在发起请求时指定运行在主线程;另一种是利用Handler进行线程间的通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

今天同事在使用RxAndroid+Retrofit来请求服务器并根据返回的数据动态更新界面时,碰到一个问题Only the original thread that created a view hierarchy can touch its views,他是参照我其他业务的小框架来做的,然后他问我怎么回事?

一、Android UI操作概述

众所周知Android中相关的view和控件操作都不是线程安全的,所以Android才会禁止在非UI线程更新UI,对于显式的非法操作,比如说直接在Activity里创建子线程,然后直接在子线程中操作UI等,Android会直接异常退出,并提示should run on UIThread之类的错误日志信息。而对于隐式的非法操作,App不会直接简单粗暴地异常退出,只是出现奇怪的结果,Only the original thread that created a view hierarchy can touch its views便是一个例子,字面意思是只有创建视图层次结构的原始线程才能操作它的View,明显是线程安全相关的。

二、出现Bug的情形

首先,M层发送Http请求至服务端:

public class MMessageIml implements MMessage {
    MessagePresenterFinishListener listener=null;
    public  MMessageIml(MessagePresenterFinishListener listener){
        this.listener=listener;
    }
    @Override
    public void initLogData() {
        AppContext.getAppContext().getServer().initLog(new HashMap<String, String>())
                .subscribeOn(Schedulers.io())
                .subscribe(new SmartSubscriber<LogListResult>() {
                    @Override
                    public void onSuccess(LogListResult listResult) {
                        listener.onSuccess(listResult);
                        AppUtil.showErroLog("FRG","LOGM"+Thread.currentThread().getName());
                    }

                    @Override
                    public void onFailure(String message) {
                        listener.onFailed(message);
                    }
                });
    }
}

接着,服务器响应Http请求并成功返回数据,进入onSuccess回调,进入到P层,P层直接调用V层的UI方法

public class MessagePresenterIml implements MessagePresenter,MessagePresenterFinishListener {
    private MMessageIml iml=null;
    private MessageFragment fragment=null;
    public MessagePresenterIml(MessageFragment fragment){
        this.iml=new MMessageIml(this);
        this.fragment=fragment;
    }
    @Override
    public void initLogData() {
        iml.initLogData();
    }

    @Override
    public void onSuccess(LogListResult list) {
        //fragment.initRecycleViewOnSucess(list);
        fragment.init("loading");//获取数据成功之后,会进到这个方法,然后由Fragment对象调用对应更新UI的方法,看起来没有任何问题
        AppUtil.showErroLog("FRG","LOGP"+Thread.currentThread().getName());
    }

    @Override
    public void onFailed(String msg) {
        //fragment.initViewOnFailed();
        fragment.init("erro");

    }
}

在Fragment执行更新UI,

public class MessageFragment extends Fragment implements NavTopView.NavImageOnClickListener {


    public MessageFragment() {
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        fragmentView = inflater.inflate(R.layout.fragment_message, container, false);
        ButterKnife.bind(this, fragmentView);
        init();
        return fragmentView;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        initLayout("loading");
        getLogImfo();
        super.onActivityCreated(savedInstanceState);
    }

    /**
    *更新UI
    */
    public void initLayout(String tag){
        switch (tag){
            case "loading":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.VISIBLE);//对应loading界面的根布局的id
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.GONE);
                ImageView imageView = (ImageView) fragmentView.findViewById(R.id.loading);
                Glide.with(getActivity()).load(R.mipmap.loding_gif).asGif().thumbnail(0.1f).into(imageView);
                break;
            case "no_data":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.VISIBLE);
                break;
            case "erro":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.VISIBLE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.GONE);
                break;
            case "normal":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.VISIBLE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.GONE);
            default:
                break;
        }
    }

    private void init() {
        presenter=new MessagePresenterIml(this);
        navMainMessage.setTitle("我的日志");
        navMainMessage.setNavOnClickListener(this);
        navMainMessage.hideImage(true, false);
        navMainMessage.setImageRight(R.mipmap.dele);
    }

    public void getLogImfo() {
        presenter.initLogData();
    }
}

最后这是对应的布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.xiaoi.app.zkSmartHome.view.fragment.FacFragment">

    <com.xiaoi.app.zkSmartHome.view.widget.NavTopView
        android:id="@+id/nav_main_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycle_log"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/nav_main_message">

    </android.support.v7.widget.RecyclerView>
    <include
        layout="@layout/view_error"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

    <include
        layout="@layout/view_loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

    <include
        layout="@layout/view_not_data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

</RelativeLayout>

这大概就是所有的逻辑基于MVP模式的,一开始他问我,为什么参照我的来写,我其他的业务没有问题,他这个就有问题了,当时我也觉得很奇怪,因为他说都是模仿我其他业务的逻辑来写的,奇怪的是这个bug并不会像其他直接在非UI线程做操作,直接异常退出或者编译不通过那样简单粗暴,甚至运行的时候还能完成部分UI更新,后面我把他当前的线程名打印出来,才发现根本原因就是因为非UI线程做了UI操作,进而报出了Only the original thread that created a view hierarchy can touch its views。一开始虽然从字面意思理解就确定就是UI线程安全相关的问题,但是我一下子想不通为什么同样的结构我的业务没有问题,他的出错了后来仔细查看了他的代码才发现,问题出现在了他发起请求的时候

//这是我发起Http请求
public class MFacDevListIml implements MFacDevList {
    private FacFinishedListener facFinishedListener;
    public MFacDevListIml(FacFinishedListener listener) {
        facFinishedListener=listener;
    }
    @Override
    public void getDeviceList() {
             AppContext.getAppContext().getServer().getDeviceList(new HashMap<String, String>())
                .subscribeOn(Schedulers.io())//指定Http请求运行在io线程
                .observeOn(AndroidSchedulers.mainThread())//指定运行在main线程
                .subscribe(new SmartSubscriber<DeviceListResult>() {

                    @Override
                    public void onSuccess(DeviceListResult deviceListResult) {
                        facFinishedListener.onGetDevListSuccess(deviceListResult);
                    }

                    @Override
                    public void onFailure(String message) {
                        facFinishedListener.onGetDevListFail(message);
                    }
                });
    }
    }

对于RxAndroid+Retrofit架构的,只需要在发起请求的时候指定运行在main线程即可。

AppContext.getAppContext().getServer().initLog(new HashMap<String, String>())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())//指定运行在main线程就可以解决了
                .subscribe(new SmartSubscriber<LogListResult>() {
                    @Override
                    public void onSuccess(LogListResult listResult) {
                        listener.onSuccess(listResult);
                        AppUtil.showErroLog("FRG","LOGM"+Thread.currentThread().getName());
                    }

                    @Override
                    public void onFailure(String message) {
                        listener.onFailed(message);
                    }
                });

但如果只是采用这种方案,只是针对RxAndroid+Retrofit的,对于更通用的解决方案应该是利用Handler进行线程间的通信

三、利用Handler实现线程间的通信完成UI更新

在Android中Handler使用的场合可以大致分为两种:安排messages和runnables在将来的某个时间点执行将action入队以备在一个不同的线程中执行。也就是所谓的线程间通信。比如当你创建子线程时,你可以在你的子线程中拿到父线程中创建的Handler对象,那么久可以通过该Handler对象向父线程的消息队列发送消息了。由于Android要求在UI线程中更新界面,因此,可以通过该方法在其它线程中更新UI。利用Handler实现UI更新的方式有很多种,这里只总结两种方式。

1、构造Handler,并在Handler.Callback回调接口里更新UI

  • 在View层构造Handler对象并传入Handler.Callback回调接口
  • 重写Handler.Callback回调接口里的handleMessage方法处理接收到消息
  • 把Handler对象公开出去供其他线程调用并发送Message

构造Handler对象并重写处理Message的方法

public class MessageFragment extends Fragment implements NavTopView.NavImageOnClickListener {

    @Bind(R.id.nav_main_message)
    NavTopView navMainMessage;
    @Bind(R.id.recycle_log)
    RecyclerView recycleLog;
    private LogAdapter adapter;
    private List<LogImfo> list = new ArrayList<>();
    private View fragmentView;
    private MessagePresenter presenter=null;
   /*构造一个Handler,主要作用有:1)供非UI线程发送Message  2)处理Message并完成UI更新*/
    public Handler uiHandler=new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what){
                case 0:
                    setRecycleView(list);
                    initLayout("normal");
                    break;
                case 1:
                    initLayout("erro");
                    break;
                case 2:
                    initLayout("no_data");
                    break;
                default:
                    break;

            }
            return false;
        }
    });
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        fragmentView = inflater.inflate(R.layout.fragment_message, container, false);
        ButterKnife.bind(this, fragmentView);
        init();
        return fragmentView;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        initLayout("loading");
        getLogImfo();
        super.onActivityCreated(savedInstanceState);
    }

    public void initLayout(String tag){
        switch (tag){
            case "loading":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.VISIBLE);
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.GONE);
                ImageView imageView = (ImageView) fragmentView.findViewById(R.id.loading);
                Glide.with(getActivity()).load(R.mipmap.loding_gif).asGif().thumbnail(0.1f).into(imageView);
                break;
            case "no_data":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.VISIBLE);
                break;
            case "erro":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.VISIBLE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.GONE);
                break;
            case "normal":
                fragmentView.findViewById(R.id.d_ll_loading_view).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.recycle_log).setVisibility(View.VISIBLE);
                fragmentView.findViewById(R.id.d_ll_network_error).setVisibility(View.GONE);
                fragmentView.findViewById(R.id.ll_not_data).setVisibility(View.GONE);
            default:
                break;
        }
    }

    private void init() {
        presenter=new MessagePresenterIml(this);
        navMainMessage.setTitle("我的日志");
        navMainMessage.setNavOnClickListener(this);
        navMainMessage.hideImage(true, false);
        navMainMessage.setImageRight(R.mipmap.dele);
    }

    public void getLogImfo() {
        presenter.initLogData();
    }

    public void initRecycleViewOnSucess(LogListResult listResult){
        if(listResult.getList().size()==0){
            AppUtil.showErroLog("LogImf","no_data");
            uiHandler.sendEmptyMessage(2);
        }else {
            AppUtil.showErroLog("LogImf","setRecycleView");
            list=listResult.getList();
            uiHandler.sendEmptyMessage(0);
        }
    }

    public void initViewOnFailed(){
        uiHandler.sendEmptyMessage(1);
    }
    。。。 
}

调用Handler对象发送Message

public class MessagePresenterIml implements MessagePresenter,MessagePresenterFinishListener {
    private MMessageIml iml=null;
    private MessageFragment fragment=null;
    public MessagePresenterIml(MessageFragment fragment){
        this.iml=new MMessageIml(this);
        this.fragment=fragment;
    }
    @Override
    public void initLogData() {
        iml.initLogData();
    }

    @Override
    public void onSuccess(LogListResult list) {
        fragment.initRecycleViewOnSucess(list);
        AppUtil.showErroLog("FRG","LOGP"+Thread.currentThread().getName());
    }

    @Override
    public void onFailed(String msg) {
        fragment.initViewOnFailed();
    }
}

2、构建Runnable对象,并在Runnable中更新UI。

  • 在主线程中创建Handler对象(因为创建于主线程中便于更新UI)。
  • 构建Runnable对象,在Runnable中更新界面
  • 在非UI线程的run方法中向UI线程post runnable对象来更新UI


    public class RunnableHandlerActivity extends Activity implements OnClickListener{  
        private Button btnDown=null;  
        private txtName txtName=null;  
        private String content=null;  
        private Handler handler=null;  

        @Override  
        public void onCreate(Bundle savedInstanceState) {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.main);  
            //在主线程中初始化Handler 对象
            handler=new Handler();  

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

        @Override  
        public void onClick(View v) {  
            //模拟download         
            final DownFiles df=new DownFiles("http://192.168.65.223:8080/downLoadServer/a.txt");  
            txtName.setText("正在下载......");  
            new Thread(){  
                public void run(){    
                    content=df.downLoadFiles();       
                    handler.post(udpUIRunnable); //向Handler post runnable对象
                    }                     
            }.start();                        
        }   

       // 构建Runnable对象,并在runnable中更新UI  
        Runnable   udpUIRunnable=new  Runnable(){  
            @Override  
            public void run() {  
                txtName.setText("the Content is:"+content); //更新UI  
            }     
        }; 
    }  
### Android 更新 UI解决方案 在 Android 开发中,`Only the original thread that created a view hierarchy can touch its views` 是一个常见的错误提示。这是因为 Android 的视图组件(如 `TextView`, `Button` 等)只能由创建它们的主线程(也称为 UI 线程)来操作。如果尝试从其他线程直接修改这些视图,则会抛出此异常。 为了安全地更新 UI 组件,可以采用以下几种方法: #### 方法一:使用 Handler 通过 `Handler` 将耗时任务的结果传递回主线程并执行 UI 更新逻辑。这种方式适用于需要在线程间通信的情况[^1]。 ```java final Handler myHandler = new Handler(); (new Thread(new Runnable() { @Override public void run() { final String res = doLongOperation(); // 执行耗时操作 myHandler.post(new Runnable() { // 切换到主线程 @Override public void run() { updateUI(res); // 安全地更新 UI } }); } })).start(); ``` #### 方法二:使用 AsyncTask(已废弃) 虽然官方已经不推荐使用 `AsyncTask`,但在某些旧项目中仍然可能遇到它。它的设计初衷是为了简化后台任务与 UI 交互的过程。 ```java class MyTask extends AsyncTask<Void, Void, String> { @Override protected String doInBackground(Void... params) { return doLongOperation(); // 耗时操作 } @Override protected void onPostExecute(String result) { super.onPostExecute(result); updateUI(result); // 主线程更新 UI } } new MyTask().execute(); ``` 注意:自 Android API Level 30 起,`AsyncTask` 已被标记为过时,建议改用更现代的方式替代[^4]。 #### 方法三:使用 Lambda 表达式配合 View.post() 对于简单的场景,可以直接利用 `View.post()` 来调度运行于主线程的任务。 ```java ((Button)findViewById(R.id.Button01)).setOnClickListener(v -> { new Thread(() -> { int result = doLongOperation(); // 子线程完成工作 v.post(() -> updateUI(result)); // 返回主线程更新界面 }).start(); }); ``` 这种方法简洁明了,在单次调用的情况下尤为适用[^3]。 #### 方法四:借助 LiveData 和 ViewModel 当应用架构较为复杂或者涉及数据绑定时,推荐使用 Jetpack 提供的 `LiveData` 和 `ViewModel` 组合方案。这种模式不仅能够实现响应式的 UI 更新机制,还能自动处理配置更改带来的生命周期问题。 ```kotlin // Kotlin 示例代码 val liveDataResult: MutableLiveData<String> = MutableLiveData() fun performBackgroundWork() { lifecycleScope.launch(Dispatchers.IO) { val result = doLongOperation() // IO 线程上的长时间计算 withContext(Dispatchers.Main) { liveDataResult.value = result // 发布新值给观察者 } } } liveDataResult.observe(this@YourActivity, Observer { updatedValue -> updateUI(updatedValue) // 接收通知后刷新显示内容 }) ``` 以上方式均能有效规避跨线程访问引发的问题,开发者应根据实际需求选择最适合的技术手段[^5]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CrazyMo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值