当前位置: 首页 > 科技观察

惊天秘密!从Thread开始,揭秘Android线程通信的窍门和主线程的阴谋

时间:2023-03-20 22:40:15 科技观察

背景介绍在Android开发过程中,我们几乎离不开线程。但是你对线程了解多少呢?它的***操作背后隐藏着多少秘密?线程如何通信码字和传递信息?Looper、Handler和MessageQueue幕后发生了什么。本期,让我们从Thread开始,逐步探寻这条独特的线程链背后的秘密。注意,大部分分析都在代码中,请密切关注代码!本节从Tread的创建过程开始,一步步分析Thread的创建过程。话不多说,直接看代码。线程创建起点init()//创建Thread的公共构造函数,都调用这个私有的init()方法。让我们看看发生了什么。/****@param线程组*@param是我们平时接触最多的Runnable同学*@param指定线程的名字*@param指定线程栈的大小*/privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize){Threadparent=currentThread();//先获取当前运行的线程。对于这个Native函数,暂时先不管它是怎么做的。黑箱思维,哈哈!if(g==null){g=parent.getThreadGroup();//如果没有指定ThreadGroup,则获取父线程的TreadGroup}g.addUnstarted();//将ThreadGroup中的就绪线程计数器增加一。注意此时线程还没有真正加入到ThreadGroup中。this.group=g;//分配Thread实例的组。从这里开始,线程就有了ThreadGroup。this.target=target;//为Thread实例设置Runnable。以后会在start()时执行。this.priority=parent.getPriority();//设置线程的优先级权重为父线程的权重this.daemon=parent.isDaemon();//根据是否为守护线程判断Thread实例是否为守护线程父线程是守护线程。setName(name);//设置线程的名字init2(parent);//Nani?又一次初始化,参数还是父线程。不着急,我以后会看的。/*StashthespecifiedstacksizeincasetheVMcares*/this.stackSize=stackSize;//设置线程的堆栈大小tid=nextThreadID();//线程的id。这是一个静态变量。调用此方法将自增并用作线程ID。}第二次init2()至此,我们的Thread就完成了初始化,Thread的几个重要的成员变量也被赋值了。启动线程并驱动!通常,我们这样启动一个线程。ThreadthreadDemo=newThread(()->{});threadDemo.start();那么start()的背后隐藏着怎样不可告人的秘密呢?是人性的扭曲吗?还是道德沦丧?让我们一起点击进入它的start()。探索start()背后的秘密。//正如我们所看到的,这个方法被锁定了。原因是为了防止开发者在其他线程中调用同一个Thread实例的这个方法,从而尽可能避免抛出异常。//这个方法之所以能够执行我们传入的Runnable中的run()方法,是因为JVM调用了Thread实例的run()方法。publicsynchronizedvoidstart(){//检查线程状态是否为0,如果为0说明是新状态,即还没有started()。如果不为0,则抛出异常。//也就是说,我们有一个Thread实例,我们只能调用一次start()方法。if(threadStatus!=0)thrownewIllegalThreadStateException();//从这里开始,真正的线程被添加到ThreadGroup组中。再次重复,只是增加了nUnstartedThreads计数器,并没有添加线程。//同时,当线程启动时,nUnstartedThreads计数器会为-1。因为就绪状态的线程少了一个!group.add(this);started=false;try{nativeCreate(this,stackSize,daemon);//又是一个Native方法。这是由JVM处理的,将调用Thread实例的run()方法。started=true;}finally{try{if(!started){group.threadStartFailed(this);//如果线程没有启动成功,则将该Thread从ThreadGroup中移除,nUnstartedThreads计数器加1.}}catch(Throwableignore){}}}嗯,最本质的功能是原生的,我们先把它当成一个黑盒子。只知道它可以调用Thread实例的run()方法。那我们就来看看run()方法有哪些神奇的作用?黑色实验上方的实验表明我们可以将Thread用作Runnable。几种常见的线程方法(操作)Thread.sleep()不可告人的秘密我们通常使用Thread.sleep()的频率比较高,所以我们一起研究下Thread.sleep()被调用时会发生什么。在开始之前,我们先介绍一个概念——纳秒。1纳秒=十亿分之一秒。可见用它计时会非常准确。但是由于设备的限制,这个值有时不是那么准确,但是比起毫秒级的控制粒度还是要小很多的。//通常我们调用Thread.sleep(long)***来调用这个方法,最后一个有点奇怪的参数是纳秒。//你可以在纳秒级控制线程。publicstaticvoidsleep(longmillis,intnanos)throwsInterruptedException{//下面三个检测毫秒和纳秒设置是合法的。如果(millis<0){thrownewIllegalArgumentException("millis<0:"+millis);}if(nanos<0){thrownewIllegalArgumentException("nanos<0:"+nanos);}if(nanos>999999){thrownewIllegalArgumentException("nanos>999999:"+nanos);}if(millis==0&&nanos==0){if(Thread.interrupted()){//当休眠时间为0时,检测线程是否中断,并清除中断螺纹标记的状态。这是一个本地方法。thrownewInterruptedException();//如果线程设置为中断状态为true(调用Thread.interrupt())。然后他会抛出异常。如果在捕获此异常后返回线程,则线程停止。//需要注意的是调用Thread.sleep()后,调用isInterrupted()的结果始终为False。别忘了Thread.interrupted()也会在检测的同时清空标记位置!}return;}longstart=System.nanoTime();//类似于System.currentTimeMillis()。但是得到的是纳秒,可能不准确。longduration=(millis*NANOS_PER_MILLI)+nanos;Objectlock=currentThread().lock;//获取当前线程的锁。synchronized(lock){//同步当前线程的锁对象while(true){sleep(lock,millis,nanos);//这是另一个Native方法,也会抛出InterruptedException。//据我估计,调用这个函数sleep的时长是不确定的。longnow=System.nanoTime();longelapsed=now-start;//计算线程休眠了多长时间if(elapsed>=duration){//如果当前休眠时长满足我们的需求,则退出循环,休眠结束。break;}duration-=elapsed;//减去睡眠时间,重新计算睡眠时间。start=now;millis=duration/NANOS_PER_MILLI;//重新计算毫秒部分nanos=(int)(duration%NANOS_PER_MILLI);//重新计算微秒部分}}}通过上面的分析,我们可以知道make的核心方法threadsleep是一个Nativefunctionsleep(lock,millis,nanos),它的睡眠时间往往是不确定的。因此,Thread.sleep()方法使用了一个循环,每次检查睡眠时长是否满足要求。同时需要注意的是,如果在调用sleep()方法时将线程的中断状态设置为true,则在睡眠周期开始前会抛出InterruptedException。Thread.yield()到底隐藏了什么?这种方法是本机的。调用该方法可以提示cpu,当前线程会放弃当前的cpu使用权,与其他线程竞争新的cpu使用权。当前线程可能会也可能不会再次执行。只是酱汁。无处不在的wait()是什么?你一定经常看到,不管是哪个对象实例,最底层都会有几个名为wait()的方法。等待?它们到底是怎样的存在,让我们一起点击看看。哦,不说了,都是Native函数。然后看看文档是什么。根据文档的描述,wait()是配合notify()、notifyAll()来实现线程间通信的,也就是同步。在线程中调用wait()必须在同步代码块中调用,否则会抛出IllegalMonitorStateException。因为wait()函数需要释放对应对象的锁。当线程执行到wait()时,对象会将当前线程放入自己的线程池,释放锁,然后阻塞在这个地方。直到对象调用notify()或notifyAll(),线程才能重新获得,或者可能获得对象的锁,然后继续执行后面的语句。出色地。..好吧,让我们解释一下notify()和notifyAll()的区别。notify()调用notify()后,对象会从自己的线程池中随机选择一个线程(即调用对象上的wait()函数的线程)将其唤醒。即一次只能唤醒一个线程。如果在多线程的情况下,只调用一次notify(),那么只能唤醒一个线程,其他线程在notifyAll()之后总会调用notifyAll(),对象会唤醒自己线程池中的所有线程,然后这些线程会一起去抢对象的锁。看看Looper、Handler、MessageQueue的爱恨情仇。以前我们可能写过这样的代码:很多同学都知道,在一个线程(Android主线程除外)中使用Handler时,必须放在Looper中。在prepare()和Looper.loop()之间。否则将抛出RuntimeException。但是你为什么要这样做呢?让我们一起来看看这其中的内幕吧。从Looper.prepare()调用Looper.prepare()时发生了什么?经过上面的分析,我们已经知道Looper.prepare()调用之后发生了什么。但是问题来了!sThreadLocal是一个静态的ThreadLocal实例(Android中ThreadLocal的范式固定为Looper)。也就是说,当前进程中的所有线程共享这个ThreadLocal。那么,既然Looper.prepare()是一个静态方法,那么Looper是如何确定现在应该绑定到哪个线程的呢?让我们深入了解一下。让我们看一下ThreadLocal的get()和set()方法。创建一个HandlerHandler可以用来实现线程间的通信。在Android中,当我们在子线程中完成数据处理后,往往需要通过Handler通知主线程更新UI。通常我们在一个线程中使用newHandler()来创建一个Handler实例,但是它是如何知道它应该处理那个线程的任务的。下面一起来看看Handler。publicHandler(){this(null,false);}publicHandler(Callbackcallback,booleanasync){//可以看到最终调用了这个方法。如果(FIND_POTENTIAL_LEAKS){finalClassklass=getClass();if((klass.isAnonymousClass()||klass.isMemberClass()||klass.isLocalClass())&&(klass.getModifiers()&Modifier.STATIC)==0){Log.w(TAG,"ThefollowingHandlerclassshouldbestaticorleaksmightoccur:"+klass.getCanonicalName());}}mLooper=Looper.myLooper();//关键点!这里Handler绑定到当前Thread的Looper上。Looper.myLooper()是从ThreadLocale中取出当前线程的Looper。if(mLooper==null){//如果子线程中的newHandler()之前没有调用过Looper.prepare(),那么当前线程的Looper还没有创建。会抛出这个异常。thrownewRuntimeException("Can'tcreatehandlerinsidethreadthathasnotcalledLooper.prepare()");}mQueue=mLooper.mQueue;//将Looper的MessageQueue分配给Handler。mCallback=callback;mAsynchronous=async;}Looper.loop()我们都知道Handler创建后,需要调用Looper.loop(),否则给Handler发送消息也没用!接下来,我们就来看看Looper究竟有什么神奇之处,可以准确地将消息发送给Handler进行处理。publicstaticvoidloop(){finalLooperme=myLooper();//这个方法之前有提到过,就是获取当前线程中的Looper对象。if(me==null){//如果没有Looper.prepare(),会报错!thrownewRuntimeException("NoLooper;Looper.prepare()wasn'tcalledonthisthread.");}finalMessageQueuequeue=me.mQueue;//获取Looper的MessageQueue成员变量,Looper创建时是new的。//这是一个Native方法,作用是检查当前线程是否属于当前进程。并将跟踪其真实身份。//在IPC机制中,该方法用于清除IPCThreadState的pid和uid信息。并返回一个身份,方便使用restoreCallingIdentity()来恢复。Binder.clearCallingIdentity();finallongident=Binder.clearCallingIdentity();for(;;){//重点(敲黑板)!这是一个死循环,等待提取和发送消息。Messagemsg=queue.next();//从MessageQueue中提取一条消息。至于如何获得,我们稍后再看。if(msg==null){//Nomessageindicatesthatthemessagequeueisquitting.return;}//这必须是本地变量,incaseaUIeventsetstheloggerfinalPrinterlogging=me.mLogging;if(logging!=null){logging.println(">>>>>Dispatchingto"+msg.target+""+msg.callback+":"+msg.what);}finallongtraceTag=me.mTraceTag;//获取MessageQueue的tracetagif(traceTag!=0){Trace.traceBegin(traceTag,msg.target.getTraceName(msg));//开始跟踪本线程MessageQueue中的当前消息,是一个Native方法。}try{msg.target.dispatchMessage(msg);//尝试向Message绑定的Handler发送消息}finally{if(traceTag!=0){Trace.traceEnd(traceTag);//这个和Trace一样.traceBegin()配套使用。}}if(logging!=null){logging.println("<<<<