今天同事在使用RxAndroid+Retrofit来请求服务器并根据返回的数据动态更新界面时,碰到一个问题Only the original thread that created a view hierarchy can touch its views,他是参照我其他业务的小框架来做的,然后他问我怎么回事?
众所周知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,明显是线程安全相关的。
首先,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())
.subscribeOn(Schedulers.io())
.subscribe(new SmartSubscriber() {
@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();
}
}
最后这是对应的布局文件
"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" />
.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">
.support.v7.widget.RecyclerView>
"@layout/view_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
"@layout/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
"@layout/view_not_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
这大概就是所有的逻辑基于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())
.subscribeOn(Schedulers.io())//指定Http请求运行在io线程
.observeOn(AndroidSchedulers.mainThread())//指定运行在main线程
.subscribe(new SmartSubscriber() {
@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())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())//指定运行在main线程就可以解决了
.subscribe(new SmartSubscriber() {
@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进行线程间的通信。
在Android中Handler使用的场合可以大致分为两种:安排messages和runnables在将来的某个时间点执行 和将action入队以备在一个不同的线程中执行。也就是所谓的线程间通信。比如当你创建子线程时,你可以在你的子线程中拿到父线程中创建的Handler对象,那么久可以通过该Handler对象向父线程的消息队列发送消息了。由于Android要求在UI线程中更新界面,因此,可以通过该方法在其它线程中更新UI。利用Handler实现UI更新的方式有很多种,这里只总结两种方式。
构造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 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();
}
}
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
}
};
}