AndroID的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。所以AndroID中规定只能在UI线程中访问UI。
但是有没有极端的情况?使得我们在子线程中访问UI也可以使程序跑起来呢?接下来我们用一个例子去证实一下。
新建一个工程,activity_main.xml布局如下所示:
<?xml version="1.0" enCoding="utf-8"?><relativeLayout xmlns:androID="http://schemas.androID.com/apk/res/androID" androID:layout_wIDth="match_parent" androID:layout_height="match_parent" > <TextVIEw androID:ID="@+ID/main_tv" androID:layout_wIDth="wrap_content" androID:layout_height="wrap_content" androID:textSize="18sp" androID:layout_centerInParent="true" /></relativeLayout>
很简单,只是添加了一个居中的TextVIEw
MainActivity代码如下所示:
public class MainActivity extends AppCompatActivity { private TextVIEw main_tv; @OverrIDe protected voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentVIEw(R.layout.activity_main); main_tv = (TextVIEw) findVIEwByID(R.ID.main_tv); new Thread(new Runnable() { @OverrIDe public voID run() { main_tv.setText("子线程中访问"); } }).start(); }}
也是很简单的几行,在onCreate方法中创建了一个子线程,并进行UI访问 *** 作。
点击运行。你会发现即使在子线程中访问UI,程序一样能跑起来。结果如下所示:
咦,那为嘛以前在子线程中更新UI会报错呢?难道真的可以在子线程中访问UI?
先不急,这是一个极端的情况,修改MainActivity如下:
public class MainActivity extends AppCompatActivity { private TextVIEw main_tv; @OverrIDe protected voID onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentVIEw(R.layout.activity_main); main_tv = (TextVIEw) findVIEwByID(R.ID.main_tv); new Thread(new Runnable() { @OverrIDe public voID run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printstacktrace(); } main_tv.setText("子线程中访问"); } }).start(); }}
让子线程睡眠200毫秒,醒来后再进行UI访问。
结果你会发现,程序崩了。这才是正常的现象嘛。抛出了如下很熟悉的异常:
androID.vIEw.VIEwRootImpl$CalledFromWrongThreadException: Only the original thread that created a vIEw hIErarchy can touch its vIEws.
at androID.vIEw.VIEwRootImpl.checkThread(VIEwRootImpl.java:6581)
at androID.vIEw.VIEwRootImpl.requestLayout(VIEwRootImpl.java:924)
……
作为一名开发者,我们应该认真阅读一下这些异常信息,是可以根据这些异常信息来找到为什么一开始的那种情况可以访问UI的。那我们分析一下异常信息:
首先,从以下异常信息可以知道
at androID.vIEw.VIEwRootImpl.checkThread(VIEwRootImpl.java:6581)
这个异常是从androID.vIEw.VIEwRootImpl的checkThread方法抛出的。
那现在跟进VIEwRootImpl的checkThread方法瞧瞧,源码如下:
voID checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a vIEw hIErarchy can touch its vIEws."); }}
只有那么几行代码而已的,而mThread是主线程,在应用程序启动的时候,就已经被初始化了。
由此我们可以得出结论:
在访问UI的时候,VIEwRootImpl会去检查当前是哪个线程访问的UI,如果不是主线程,那就会抛出如下异常:
Only the original thread that created a vIEw hIErarchy can touch its vIEws
这好像并不能解释什么?继续看到异常信息
at androID.vIEw.VIEwRootImpl.requestLayout(VIEwRootImpl.java:924)
那现在就看看requestLayout方法,
@OverrIDepublic voID requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); }}
这里也是调用了checkThread()方法来检查当前线程,咦?除了检查线程好像没有什么信息。那再点进scheduleTraversals()方法看看
voID scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalbarrIEr = mHandler.getLooper().getQueue().postsyncbarrIEr(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedinputdispatch) { scheduleConsumeBatchedinput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); }}
注意到postCallback方法的的第二个参数传入了很像是一个后台任务。那再点进去
final class TraversalRunnable implements Runnable { @OverrIDe public voID run() { doTraversal(); }}
找到了,那么继续跟进doTraversal()方法。
voID doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncbarrIEr(mTraversalbarrIEr); if (mProfile) { DeBUG.startMethodTracing("VIEwAncestor"); } performTraversals(); if (mProfile) { DeBUG.stopMethodTracing(); mProfile = false; } }}
可以看到里面调用了一个performTraversals()方法,VIEw的绘制过程就是从这个performTraversals方法开始的。PerformTraversals方法的代码有点长就不贴出来了,如果继续跟进去就是学习VIEw的绘制了。而我们现在知道了,每一次访问了UI,AndroID都会重新绘制VIEw。这个是很好理解的。
分析到了这里,其实异常信息对我们帮助也不大了,它只告诉了我们子线程中访问UI在哪里抛出异常。
而我们会思考:当访问UI时,VIEwRootImpl会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在MainActivity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢??
唯一的解释就是执行onCreate方法的那个时候VIEwRootImpl还没创建,无法去检查当前线程。
那么就可以这样深入进去。寻找VIEwRootImpl是在哪里,是什么时候创建的。好,继续前进
在ActivityThread中,我们找到handleResumeActivity方法,如下:
final voID handleResumeActivity(IBinder token, boolean clearHIDe, boolean isForward, boolean reallyResume) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIDler(); mSomeActivitIEsChanged = true; // Todo Push resumeArgs into the activity for consIDeration ActivityClIEntRecord r = performResumeActivity(token, clearHIDe); if (r != null) { final Activity a = r.activity; //代码省略 r.activity.mVisibleFromServer = true; mNumVisibleActivitIEs++; if (r.activity.mVisibleFromClIEnt) { r.activity.makeVisible(); } } //代码省略 }
可以看到内部调用了performResumeActivity方法,这个方法看名字肯定是回调onResume方法的入口的,那么我们还是跟进去瞧瞧。
public final ActivityClIEntRecord performResumeActivity(IBinder token, boolean clearHIDe) { ActivityClIEntRecord r = mActivitIEs.get(token); if (localLOGV) Slog.v(TAG, "Performing resume of " + r + " finished=" + r.activity.mFinished); if (r != null && !r.activity.mFinished) { //代码省略 r.activity.performResume(); //代码省略 return r;}
可以看到r.activity.performResume()这行代码,跟进 performResume方法,如下:
final voID performResume() { performRestart(); mFragments.execPendingActions(); mLastNonConfigurationInstances = null; mCalled = false; // mResumed is set by the instrumentation mInstrumentation.callActivityOnResume(this); //代码省略}
Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:
public voID callActivityOnResume(Activity activity) { activity.mResumed = true; activity.onResume(); if (mActivityMonitors != null) { synchronized (mSync) { final int N = mActivityMonitors.size(); for (int i=0; i<N; i++) { final ActivityMonitor am = mActivityMonitors.get(i); am.match(activity, activity, activity.getIntent()); } } }}
找到了,activity.onResume()。这也证实了,performResumeActivity方法确实是回调onResume方法的入口。
那么现在我们看回来handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后,
会来到这一块代码:
r.activity.mVisibleFromServer = true;mNumVisibleActivitIEs++;if (r.activity.mVisibleFromClIEnt) { r.activity.makeVisible();}
activity调用了makeVisible方法,这应该是让什么显示的吧,跟进去探探。
voID makeVisible() { if (!mWindowAdded) { VIEwManager wm = getwindowManager(); wm.addVIEw(mDecor, getwindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(VIEw.VISIBLE);}
往WindowManager中添加DecorVIEw,那现在应该关注的就是WindowManager的addVIEw方法了。而WindowManager是一个接口来的,我们应该找到WindowManager的实现类才行,而WindowManager的实现类是WindowManagerImpl。
找到了WindowManagerImpl的addVIEw方法,如下:
@OverrIDepublic voID addVIEw(@NonNull VIEw vIEw, @NonNull VIEwGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.addVIEw(vIEw, params, mdisplay, mParentwindow);}
里面调用了WindowManagerGlobal的addVIEw方法,那现在就锁定
WindowManagerGlobal的addVIEw方法:
public voID addVIEw(VIEw vIEw, VIEwGroup.LayoutParams params, display display, Window parentwindow) { //代码省略 VIEwRootImpl root; VIEw panelParentVIEw = null; //代码省略 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) { // BadTokenException or InvalIDdisplayException, clean up. synchronized (mlock) { final int index = findVIEwLocked(vIEw, false); if (index >= 0) { removeVIEwLocked(index, true); } } throw e; }}
终于击破,VIEwRootImpl是在WindowManagerGlobal的addVIEw方法中创建的。
回顾前面的分析,总结一下:
VIEwRootImpl的创建在onResume方法回调之后,而我们一开篇是在onCreate方法中创建了子线程并访问UI,在那个时刻,VIEwRootImpl是没有创建的,无法检测当前线程是否是UI线程,所以程序没有崩溃一样能跑起来,而之后修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后VIEwRootImpl已经创建了,可以执行checkThread方法检查当前线程。
这篇博客的分析如题目一样,AndroID中子线程真的不能更新UI吗?在onCreate方法中创建的子线程访问UI是一种极端的情况,这个不仔细分析源码是不知道的。我是最近看了一个面试题,才发现这个。
从中我也学习到了从异常信息中跟进源码寻找答案,你呢?
总结以上是内存溢出为你收集整理的Android中子线程真的不能更新UI吗?全部内容,希望文章能够帮你解决Android中子线程真的不能更新UI吗?所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)