引言

今天同事在使用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  
            }     
        }; 
    }  
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐