Android多线程消息处理机制(二) Looper、Thread专题续

上一篇文章介绍了Looper、Thread原理。由于篇幅原因,遗留了一个问题没有解答,今天给Looper收尾总结。
回顾上章:
1、Looper不是每个线程都有,在线程中需要Looper消息循环,就必须调用Looper.prepare()方法,创建一个looper对象。
2、我想创建一个handler,发送和处理的消息由这个子线程的looper和messagequeue管理,就必须使用该线程的looper,故handler的创建必须在Looper.prepare()之后。关于handler的详细问题,下章讲解。
3、经过上面2步骤,只能说明此时子线程有了looper、messageQueue和handler。让消息循环跑起来是Looper.loop()方法调起的。loop()方法里面是个死循环,loop()后面的代码之后等到Looper.quit()调用才会执行。
4、一旦一个线程成为了looper线程,就别指望这个线程能做别的事情了。他会无休止的抽取与他的looper关联的handler发送的message。
5、MainThread的looper是不能quit的,要不然app还怎么玩。


以上就是上章内容,不难。遗留问题:
1、是否只有ui线程才可以更新ui?子线程可以吗?
2、是否只要是looper线程,就都可以更新ui?


之前我也一直以为只要是looper线程就可以更新ui,直到昨天写上篇文章时,发现抛异常了:“CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.”
于是查阅资料发现有蹊跷,知道的人很少,网上大多文章都比较肤浅。包括我前任项目经理告诉我的也是错的。
我们先来看几个案例:

一、这种情况子线程可以修改button的text属性吗?

public class Main2Activity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        button = (Button) findViewById(R.id.buttontext);

        new MyThread().start();
    }
    public class MyThread extends Thread{
        @Override
        public void run() {
            button.setText("子线程修改的");
        }
    }
}
二、这种情况子线程可以修改button的text属性吗?

public class Main2Activity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        button = (Button) findViewById(R.id.buttontext);

    }

    @Override
    protected void onStart() {
        super.onStart();
        new MyThread().start();
    }

    public class MyThread extends Thread{
        @Override
        public void run() {
            button.setText("子线程修改的");
        }
    }
}
三、这种情况子线程可以修改button的text属性吗?
public class Main2Activity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        button = (Button) findViewById(R.id.buttontext);

    }

    @Override
    protected void onResume() {
        super.onResume();
        new MyThread().start();
    }

    public class MyThread extends Thread{
        @Override
        public void run() {
            button.setText("子线程修改的");
        }
    }
}
四、这种情况子线程可以修改button的text属性吗?
public class Main2Activity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        button = (Button) findViewById(R.id.buttontext);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new MyThread().start();
            }
        });
    }

    public class MyThread extends Thread{
        @Override
        public void run() {
            button.setText("子线程修改的");
        }
    }
}
五、这种情况子线程可以修改button的text属性吗?
public class Main2Activity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        button = (Button) findViewById(R.id.buttontext);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new MyThread().start();
            }
        });
    }
    public class MyThread extends Thread{
        @Override
        public void run() {
            Looper.prepare();
            button.setText("子线程修改的");
            Looper.loop();
        }
    }
}
什么情况?
一二三都可以?
四不行?
五也不行?五里面不是有Looper吗?


我也是一头雾水,根据网友提示,从两个方向入手:
1、报错原因和位置查询。
2、从app启动流程分析。


我们先按照方法1着手。
报的什么错?
“CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.”
“在错误的线程中调用导致的异常:只有创建一个视图层次结构的原始线程才能触摸到它的视图”。
意思是说子线程不能修改主线程创建的view,不管你有没有looper循环。哪开始为什么有可以修改呢?
找异常所在的位置去。网友提示说在ViewRoot类中,由于api版本的升高,早已没有这个类了,变成了现在的ViewRootImpl类,(我的sdk中的位置D:\AndroidStudio_SDK\sources\android-23\android\view\ViewRootImpl.java)进入搜索异常信息位置:


ViewRootImpl.java,6554行:
void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
mThread与Thread.currentThread()不想等就挂了?我的思路是:这个方法不是静态的,需要通过对象调用。去看看谁调用了这个类的构造方法,然后调用了checkThread()方法,即知道了来龙去脉。


mThread赋值位置358行:
public ViewRootImpl(Context context, Display display) {
        
        //...
        mThread = Thread.currentThread();
        //...
    }

那在哪儿构造了ViewRootImpl呢?

ArrayList<ViewRootImpl> views = WindowManagerGlobal.getInstance().getRootViews(getActivityToken());

去activity类中搜索下,发现确实有,还找到了一个与之相关联的WindowManagerGlobal类。


getWindow().peekDecorView().getViewRootImpl()
当找到下面这句代码时发现,与View类有关,因为getWindow().peekDecorView()返回的是View。


这么一缕,发现WindowManagerGlobal、View、Activity这几个类脱不了干系,可能跟ViewGroup、ViewParent、ActivityThread、ApplicationThread等类也有牵连。


WindowManagerGlobal类和ViewRootImpl一样,都是被系统隐藏的。去sdk源码位置查看,与ViewRootImpl在同一位置。
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
发现WindowManagerGlobal类里面有这个定义,那就去看看mRoots在哪里有add方法

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // ...
        }

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) {
                                mRoots.get(i).loadSystemProperties();
                            }
                        }
                    }
                };
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            }

            //...

            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            //...
            throw e;
        }
    }
发现WindowManagerGlobal类单例模式,看谁调用这个类的addView方法。接下来重点找:
view.getViewRootImpl()和WindowManagerGlobal.getInstance()方法的流向。


先看View类:
public ViewRootImpl getViewRootImpl() {
        if (mAttachInfo != null) {
            return mAttachInfo.mViewRootImpl;
        }
        return null;
    }
查找mAttachInfo在哪儿赋值的:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
	//...
}
View的绘制,貌似不好弄了。
找WindowManagerGlobal.getInstance()方法的流向,貌似也不行。这条线断了,那就从app的启动流程说起吧。


app的启动流程参考 http://blog.chinaunix.net/uid-24439730-id-373948.html
很详细,里面提到Launcher.java类。说到这,其实任何app里面都有个launcher的任务栈,
参考: http://blog.csdn.net/fesdgasdgasdg/article/details/52067427


内核启动,到launcher,再到app的ActivityThread的main入口方法,调起looper循环。
调起ActivityThread的scheduleLaunchActivity方法启动activity


//........................

一路走来调用oncreate、onstart、onresume等生命周期方法。直到最后的绘制方法:

handleResumeActivity() {

    --r.window.getDecorView();
	//开始把DecorView添加进Window
    --wm.addView(decor, l);

	// Tell the activity manager we have resumed.
            if (reallyResume) {
                try {
                    ActivityManagerNative.getDefault().activityResumed(token);
                } catch (RemoteException ex) {
                }
            }

 }

由于app启动过程非常复杂,大量的用到了动态代理和远程调度,再深入分析有难度。
通过以上分析,结合其他资料,或许可以得出以下结论:
app启动后,创建activity,同时调用oncreate、onstsrt、onresume生命周期方法,然后开始添加view到windowManager里面去,间接的关联了WindowManagerGlobal。在开始绘制的时候调用了view的dispatchAttachedToWindow()。然后就会创建ViewRootImpl对象,通过动态代理调用了里面的checkThread()方法,后续的任何修改ui的操作都会执行此方法做判断,故有开篇的奇怪现象。
根据上篇的实验,在子线程中创建的view,然后添加到windowManager中,通过系统动态代理间接调用了WindowManagerGlobal.getInstance().addView( )方法,于是也创建了ViewRootImpl对象。ViewRootImpl对象的mThread属性会获取当前线程作为其值。当在子线程中修改刚刚创建的view时,会判断当前线程是否等于ViewRootImpl对象的mThread属性。切好相等,于是没报错。这样的解释是否有些牵强?还行。
有网上资料说子线程能否修改ui,关键因素在于当前view关联的ViewRootImpl是否属于当前线程,也就是上面这个原理。
精力能力有限,欢迎留言探讨。




参考资料:
Android中Activity启动过程探究:http://www.codeceo.com/article/android-activity-start.html
android 应用的启动流程分析 :http://blog.chinaunix.net/uid-24439730-id-373948.html
Android里子线程真的不能刷新UI吗?:http://blog.csdn.net/imyfriend/article/details/6877959
Android子线程在没有ViewRoot的情况下能刷新UI吗?:http://blog.csdn.net/imyfriend/article/details/6877962


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值