【小学生Android】一、我们来谈谈Android的线程安全以及只有主线程才能修改UI

参考文章:
http://blog.csdn.net/zhaokaiqiang1992/article/details/43410351
http://blog.csdn.net/aigestudio/article/details/43449123
http://blog.csdn.net/happy_horse/article/details/52529140
https://www.zhihu.com/question/24764972
http://blog.csdn.net/xyh269/article/details/52728861

我们知道,Android的UI操作不是线程安全的,“只有主线程才能操作UI”,同时主线程对UI操作有一定的时间限制(最长5秒),所以一些费时的操作应该交给独立的线程(子线程)来执行。
这里涉及到几个概念先来解释一下,熟悉的老司机可以直接跳过啦。(PS:话说老司机也不会来这看我这小学生的博客呀,嘿嘿~ 木事儿,我就YY一下,无伤大雅)
1.线程安全VS线程不安全

首先明确一个概念性的问题,线程安全与否说的是方法,而不是线程。可以说某个方法是线程安全的,而不能说某个线程是安全的。线程没有安全与不安全之说。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用.这样做的好处是,不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

2.UI操作不是线程安全的

Android中的View提供了invalidate()方法实现界面刷新,但是这个方法是线程不安全的。参见源码:

/**
     * This must be called from a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
     */
    public void invalidate() {
        invalidate(true);
    }
    

注解说的很清楚“This must be called from a UI thread.”这个方法只能UI线程调用,所以说更新UI操作是线程不安全的。对吗?
对也不对。
对-结论是对的。
不对-注解说了非UI线程可以调用postInvalidate()方法啊“To call from a non-UI thread, call postInvalidate()”看到这名字有没想起什么来?post打头的方法其常用的不多。我先想到的是http请求的post(),其次就是handler的post()和postDelayed().http请求显然不相关,那么跟handler有没有关系呢?看下源码:

public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

这里判断不为空,调用了 ViewRootImpl的dispatchInvalidateDelayed()方法
并没有handler啊,别急 我们接着看这个dispatchInvalidateDelayed()

  public void dispatchInvalidateDelayed(View view, long delayMilliseconds){
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

看到这个mHandler有没觉得倍感亲切呢,哈哈,sendMessageDelayed() 这个方法想必大家闭着眼睛都会写吧?!
结论:
1).postInvalidate()方法虽然可以从子线程调用,但是其本质还是通过handler发送到主线程执行的.
2).invalidate()只能UI线程调用.
3).UI操作不是线程安全的

3.只有主线程才能操作UI

在非UI线程中调用invalidate()可能会导致显示异常,可能在非UI线程中刷新界面的时候,其他非UI线程也在刷新界面,这样就导致多个界面刷新的操作不能同步,导致显示异常.

这里的“只有主线程才能操作UI”为什么要加引号呢,首先加引号并不到表这句话不正确,那还了得!这么多年坚信的真理,哪能说破就破?之所以这么说,之所以会有这篇文章,是收到了文章开头提到的博客的启发。这里我贴下我的Demo核心代码,我们来做个试验。马克思教导我们,“实践是检验真理的唯一标准!”如果是真理就能经得住实践的检验。闲话不说,上代码。

activity_thread_ui.xml布局 TextView的文本是“Hello World!”

	<Button
        android:id="@+id/btnUpdate"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="子线程 更新UI" />
   
    <TextView
        android:id="@+id/tvText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World" />    

ThreadUIActivity代码

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_ui);
        new UpdateThread().start();//
    }
    class UpdateThread extends Thread {
        @Override
        public void run() {
            setText();
        }
    }
    private void setText() {
        tvText.setText("Update Yeah!");
    }

猜想下运行结果,会报错吗?CalledFromWrongThreadException?Only the original thread that created a view hierarchy can touch its views.?并没有!运行截图:

子线程Oncreate())更新TextView

我这里简单解释下原因。
1).CalledFromWrongThreadException是在invalidate()时,调用ViewRootImpl的checkThread()方法时候抛出的。

void checkThread() {  
       if (mThread != Thread.currentThread()) {  
           throw new CalledFromWrongThreadException(  
           "Only the original thread that created a view hierarchy can touch its views.");  
       }  
   }  

2)在onCreate()方法中View还没画出来,开启子线程,调用setText(),相当于只是设置TextView的一个属性,并不会调用invalidate(),也就不会调用ViewRootImpl的checkThread(),也就不会报错。
3)进一步解释是这样的,**“一个APP然可以有多个Activity,但是每个Activity只会有一个Window一个DecorView一个ViewRootImpl。并且 ViewRootImpl是在onResume()方法被执行后才会被生成。”**所以在onCreate()的时候 ViewRootImpl 都还没有创建呢,包括它的checkThread(), 此时子线程更新TextView的setText()能顺利执行。
哈哈,想必我这小学生的表述会有人没看懂,没关系,咱们有外援啊,怕啥!以上3点大家有疑问的话,推荐大家去看赵凯强和爱哥的大作,分析的很到位!必要的话留个言,咱们一起交流学习。
http://blog.csdn.net/zhaokaiqiang1992/article/details/43410351
http://blog.csdn.net/aigestudio/article/details/43449123

这里我想跟着爱哥的思路,进一步解放思想。
1).“每个Activity只会有一个Window一个DecorView一个ViewRootImpl。并且 ViewRootImpl是在onResume()方法被执行后才会被生成。”
2)“扩展一下问题一个APP是否可以拥有多个根视图呢?答案是肯定的。但是!在framework的默认实现中有且仅有一个根视图,那就是DecorView.”
我们换个角度来讲理解上面的话:
a).一个Activity只有一个ViewRootImpl,它是在主线程创建的。
ViewRootImpl的checkThread()方法,检查currentThread 是否是主线程,不是抛出CalledFromWrongThreadException异常。
这里的主线程 就是创建该ViewRootImpl的线程*。
请教了一下教我们语文的数学老师,这句话还可以这么说:
ViewRootImpl的checkThread()方法,检查currentThread 是否是创建该ViewRootImpl的线程。
b).那这个ViewRootImpl 能不能由子线程创建呢?如果能,那么checkThread()在“检查currentThread 是否是创建该ViewRootImpl的线程”就不会再抛异常了,这是显而易见的。
查了下资料,喜忧参半。忧的是 这个ViewRootImpl是个隐藏类,不能直接在代码中创建;喜的是我们还可以“曲线救国”通过WindowManager的addView()来间接创建ViewRootImpl。(这里为什么“addView() 可以创建ViewRootImpl”,挖个坑,回头再填)那么,下面上代码。

     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_ui);
        new UpdateThread().start();//
        ButterKnife.bind(this);
        initSources();
    }
     private TextView tx;
    private WindowManager wm;
    private WindowManager.LayoutParams params;
    private int num;
    private boolean hasAdded;

    private void initSources() {
        tx = new TextView(ThreadUIActivity.this);
        wm = ThreadUIActivity.this.getWindowManager();//获取WindowManager 
        //创建LayoutParams,暂不关心这个参数,主要设置显示风格 透明度 显示位置等
        params = new WindowManager.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
                0, 100, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.TRANSLUCENT);
    }
	class UpdateThreadWithPrepareManager extends Thread {
        @Override
        public void run() {
            Looper.prepare();//这里需要Looper 否则异常
            addWindow();
            Looper.loop();
        }
    }
    private void addWindow() {
        if (hasAdded) {
            wm.removeView(tx);
        }
        num++;
        tx.setText("Add Yeah! " + num);
        wm.addView(tx, params);//addView 间接创建ViewRootImpl
        hasAdded = true;
    }
 

是可以的.
颤抖吧,骚年!来看下效果!

这里写图片描述

可以多次添加.每次计数器+1

这里写图片描述

分析:
1.通过在子线程里调用WindowManager的addView()方法,间接创建ViewRootImpl.这种情况下,ViewRootImpl的checkThread() 检查当前线程就是创建ViewRootImpl的线程,所以并不会异常。
2.同一个View 只能添加一次,如果需要反复add同一个View ,需先调用remove()方法。这就是hasAdded存在的意义。

总结一下:
1.Android的UI操作遵循单线程模型,其刷新View的方法invalidate()
是线程不安全的。
2.在进行UI操作的时候,ViewRootImpl的chekThread()方法负责检查当前线程是否是创建ViewRootImpl的线程,如果不是抛出CalledFromWrongThreadException异常。
3.默认情况下“每个Activity只会有一个Window一个DecorView一个ViewRootImpl。它是在主线程创建的。
4.子线程也可以创建ViewRootImpl,这样其chekThread()方法怎么检查也不会异常!这样我们就可通过该ViewRootImpl实现对UI的更新。

保持一颗好奇心,去探索Android OS,我是旋涡小学生,欢迎各位大神批评指正。谢谢

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值