问题描写叙述
做过android开发基本都遇见过 ViewRootImpl$CalledFromWrongThreadException,上网一查,得到结果基本都是仅仅能在主线程中更改 ui。子线程要改动 ui 仅仅能 post 到主线程或者使用 handler 之类。可是细致看看exception的描写叙述并非这种。“Only the original thread that created a view hierarchy can touch its views”。仅仅有创建该 view 布局层次的原始线程才干够改动其所属 view 的布局属性,所以“仅仅能在主线程中更改 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:6498)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:954)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:4643)
at android.view.View.invalidateInternal(View.java:11775)
at android.view.View.invalidate(View.java:11739)
at android.view.View.invalidate(View.java:11723)
at android.widget.TextView.checkForRelayout(TextView.java:7002)
at android.widget.TextView.setText(TextView.java:4073)
at android.widget.TextView.setText(TextView.java:3931)
at android.widget.TextView.setText(TextView.java:3906)
at com.android.sample.HomeTestActivity$1.run(HomeTestActivity.java:114)
at java.lang.Thread.run(Thread.java:818)
相关博客介绍:
android 不能在子线程中更新ui的讨论和分析:Activity 打开的过程分析。
java/android 设计模式学习笔记(9)—代理模式:AMS 的相关类图和介绍。
android WindowManager解析与骗取QQpassword案例分析:界面 window 的创建过程;
java/android 设计模式学习笔记(8)—桥接模式:WMS 的相关类图和介绍;
android IPC通信(下)-AIDL:AIDL 以及 Binder 的相关介绍;
Android 动态代理以及利用动态代理实现 ServiceHook:ServiceHook 的相关介绍。
Android TransactionTooLargeException 解析,思考与监控方案:TransactionTooLargeException 的解析以及监控方案。
问题分析
我们依据 exception 的 stackTrace 信息,了解一下源代码,以 setText 为例。假设 textview 已经被绘制出来了,调用 setText 函数。会调用到 View 的 invalidate 函数,当中又会调用到 invalidateInternal 函数,接着调用到 parent.invalidateChildInParent 函数。当中 parent 对象就是父控件 ViewGroup。最后会调用到 ViewRootImpl 的 invalidateChildInParent 函数,为什么最后会调用到 ViewRootImpl 类中呢。这里就须要说到布局的创建过程了:
Activity的启动和布局创建过程
先分析一下 Activity 启动过程。startActivity 和 startActivityForResult 函数用来启动一个 activity。最后他们终于都会调用到一个函数
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options)
中,接着函数中会调用 Instrumentation 的 execStartActivity 方法,该函数中会调用 ActivityManagerNative.getDefault().startActivity 方法,ActivityManagerNative 类的定义
public abstract class ActivityManagerNative extends Binder implements IActivityManager
该类继承自 Binder 并实现了 IActivityManager 这个接口,IActivityManager 继承自 IInterface 接口。用过 AIDL 的应该知道。基本和这个结构类似,所以肯定是用来跨进程通信的,ActivityManagerService 类也是继承自 ActivityManagerNative 接口,因此 ActivityManagerService 也是一个 Binder 实现子类,他是 IActivityManager 接口的详细实现类。getDefault 函数是通过一个 Singleton 对象对外提供。他最后返回的是 ActivityManagerService 的 IBinder 对象,所以 startActivity 方法终于实现是在 ActivityManagerService 类中(这里讲的比較简单,假设大家对相关类层次结构和调用方式感兴趣的,能够看看我的博客: java/android 设计模式学习笔记(9)—代理模式,里面有详细介绍到):
接着进行完一系列的操作之后会回调到 IApplicationThread 中,这个接口也是继承自 IInterface 接口,它是作为服务端接收 AMS 的指令而且运行。是 ActivityThread 与 AMS 链接的桥梁,这个类是在哪作为桥梁的呢,在应用刚启动的时候会调用 ActivityThread.main 函数(详细的能够看看博客:Android TransactionTooLargeException 解析,思考与监控方案)。在 main 函数中会调用 :
ActivityThread thread = new ActivityThread();
thread.attach(false);
然后 attach 方法:
final ApplicationThread mAppThread = new ApplicationThread();
.....
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManagerNative.getDefault();
try {
mgr.attachApplication(mAppThread);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
能够看到这里通过 AIDL 调用。将 ApplicationThread 对象设置进了 AMS 中来作为 AMS 和 应用进程的桥梁。为什么须要这个 ApplicationThread 桥梁呢,由于 AMS 的职责是管理 Activity 的生命周期和栈。所以非常多时候都是 AMS 主动调用到应用进程,不是简单的一个应用进程调用系统进程 Service 而且返回值的过程,所以必须要让 AMS 持有一个应用进程的相关对象来进行调用。这个对象就是 ApplicationThread 对象。
ApplicationThreadNative 虚类则实现了 IApplicationThread 接口,在该虚类中的 onTransact 函数中,依据 code 不同会进行不同的操作,最后 ActivityThread 类的内部类 ApplicationThread 继承自 ApplicationThreadNative 类,终于的实现者就是 ApplicationThread 类,在 ApplicationThreadNative 中依据 code 进行不同操作的实现代码都在 ApplicationThread 类中,这个过程运行到最后会回调到 ApplicationThread 类中的 scheduleLaunchActivity 方法:
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
....
sendMessage(H.LAUNCH_ACTIVITY, r);
}
终于给 H 这个 Handler 类发送了一个 message(关于 H 类能够去看看博客 Android TransactionTooLargeException 解析,思考与监控方案),当中调用了的 handleLaunchActivity 方法:
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;
if (r.profilerInfo != null) {
mProfiler.setProfiler(r.profilerInfo);
mProfiler.startProfiling();
}
// Make sure we are running with the most recent config.
handleConfigurationChanged(null, null);
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
// Initialize before creating the activity
WindowManagerGlobal.initialize();
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
reportSizeConfigurations(r);
Bundle oldState = r.state;
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
if (!r.activity.mFinished && r.startsNotResumed) {
// The activity manager actually wants this one to start out paused, because it
// needs to be visible but isn't in the foreground. We accomplish this by going
// through the normal startup (because activities expect to go through onResume()
// the first time they run, before their window is displayed), and then pausing it.
// However, in this case we do -not- need to do the full pause cycle (of freezing
// and such) because the activity manager assumes it can just retain the current
// state it has.
performPauseActivityIfNeeded(r, reason);
// We need to keep around the original state, in case we need to be created again.
// But we only do this for pre-Honeycomb apps, which always save their state when
// pausing, so we can not have them save their state when restarting from a paused
// state. For HC and later, we want to (and can) let the state be saved as the
// normal part of stopping the activity.
if (r.isPreHoneycomb()) {
r.state = oldState;
}
}
} else {
// If there was an error, for any reason, tell the activity manager to stop us.
try {
ActivityManagerNative.getDefault()
.finishActivity(r.token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
这种方法通过 performLaunchActivity 方法获取到一个 Activity 对象。在 performLaunchActivity 函数中会调用该 activity 的 attach 方法。这种方法把一个 ContextImpl 对象 attach 到了 Activity 中。非常典型的装饰者模式:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
....
mLastNonConfigurationInstances = lastNonConfigurationInstances;
if (voiceInteractor != null) {
if (lastNonConfigurationInstances != null) {
mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
} else {
mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
Looper.myLooper());
}
}
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}
window 是通过以下方法获取的
mWindow = new PhoneWindow(this)
创建完 Window 之后,activity 会为该 Window 设置回调,Window 接收到外界状态改变时就会回调到 activity 中。在 activity 中会调用 setContentView() 函数,它是调用 window.setContentView() 完毕的。终于的详细操作是在 PhoneWindow 中,PhoneWindow 的 setContentView 方法第一步会检測 DecorView 是否存在,假设不存在,就会调用 generateDecor 函数直接创建一个 DecorView。第二步就是将 activity 的视图加入到 DecorView 的 mContentParent 中;第三步是回调 activity 中的 onContentChanged 方法通知 activity 视图已经发生改变。
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Window.Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
这些步骤完毕之后。DecorView 还没有被 WindowManager 正式加入到 Window 中。接着会调用到 ActivityThread 类的 handleResumeActivity 方法将顶层视图 DecorView 加入到 PhoneWindow 窗体。activity 的视图才干被用户看到:
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
.....
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
.....
}
DecorView 和 Window 的关系代码中已经非常清楚了,接下来分析一下 addView 方法。当中最关键的代码是:
ViewManager wm = a.getWindowManager();
....
wm.addView(decor, l);
而 a.getWindowManager 调用到的是 Activity.getWindowManager:
/** Retrieve the window manager for showing custom windows. */
public WindowManager getWindowManager() {
return mWindowManager;
}
这个值是在上面的 attach 方法里面设置的:
mWindow = new PhoneWindow(this);
.....
mWindowManager = mWindow.getWindowManager();
所以我们跟踪 PhoneWindow 里面的 getWindowManager 方法:
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
.....
/**
* Set the window manager for use by this Window to, for example,
* display panels. This is <em>not</em> used for displaying the
* Window itself -- that must be done by the client.
*
* @param wm The window manager for adding new windows.
*/
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
.....
/**
* Return the window manager allowing this Window to display its own
* windows.
*
* @return WindowManager The ViewManager.
*/
public WindowManager getWindowManager() {
return mWindowManager;
}
setWindowManager 函数是在哪里调用到呢,还是 Activity.attach 方法:
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0)
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE) 这个返回的是什么呢?我们先看看 context 对象是什么。是 attach 函数的第一个參数。好,我们回到 ActivityThread 类调用 activity.attach 函数的地方:
Context appContext = createBaseContextForActivity(r, activity);
......
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window);
看看 createBaseContextForActivity 函数:
private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
int displayId = Display.DEFAULT_DISPLAY;
try {
displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, r.token, displayId, r.overrideConfig);
appContext.setOuterContext(activity);
Context baseContext = appContext;
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
// For debugging purposes, if the activity's package name contains the value of
// the "debug.use-second-display" system property as a substring, then show
// its content on a secondary display if there is one.
String pkgName = SystemProperties.get("debug.second-display.pkg");
if (pkgName != null && !pkgName.isEmpty()
&& r.packageInfo.mPackageName.contains(pkgName)) {
for (int id : dm.getDisplayIds()) {
if (id != Display.DEFAULT_DISPLAY) {
Display display =
dm.getCompatibleDisplay(id, appContext.getDisplayAdjustments(id));
baseContext = appContext.createDisplayContext(display);
break;
}
}
}
return baseContext;
}
可见,这里返回的是一个 ContextImpl 对象,而且这个对象会被 Activity 调用 attachBaseContext(context);
方法给设置到 mBase 对象里面,典型的装饰者模式。所以终于肯定是调用到了 ContextImpl 类的 getSystemService 函数:
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
然后调用到 SystemServiceRegistry.getSystemService 函数,我们来看看 SystemServiceRegistry 类的相关几个函数:
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();
......
static {
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
}
.....
/**
* Gets a system service from a given context.
*/
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ?
fetcher.getService(ctx) : null;
}
.......
/**
* Statically registers a system service with the context.
* This method must be called during static initialization only.
*/
private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
我们这里能够清楚的看到,SystemServiceRegistry 类中有一个静态块代码。用来注冊所以主要的 Service ,比如 alarm,notification 等等等。当中的 WindowManager 就是通过这个注冊进去的,注意到这里返回的是一个 WindowManagerImpl 对象,所以 PhoneWindow 的 setWindowManager 函数 的 wm 对象就是 WindowManagerImpl 对象,这就是一个典型的桥接模式。WindowManager 接口继承自 ViewManager 接口。终于实现类是 WindowManagerImpl 类(感兴趣的能够去看看我的博客: java/android 设计模式学习笔记(8)—桥接模式,事实上这里是实用到桥接模式的):
而 PhoneWindow 的 setWindowManager 则是在上面的 Activity.attach 方法中调用到的:
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
所以这里的
该类并没有直接实现 Window 的三大操作,而是所有交给了 WindowManagerGlobal 来处理,WindowManagerGlobal 以单例模式 的形式向外提供自己的实例,在 WindowManagerImpl 中有例如以下一段代码:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getinstance();
所以 WindowManagerImpl 将 addView 操作交给 WindowManagerGlobal 来实现,WindowManagerGlobal 的 addView 函数中创建了一个 ViewRootImpl 对象 root,然后调用 ViewRootImpl 类中的 setView 成员方法:
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
.....
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) {
....
}
setView 方法完毕了三件事情,将外部參数 DecorView 赋值给 mView 成员变量、标记 DecorView 已加入到 ViewRootImpl、调用 requestLayout 方法请求布局。那么继续跟踪代码到 requestLayout() 方法:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
scheduleTraversals 函数实际是 View 绘制的入口,该方法会通过 WindowSession 使用 IPC 方式调用 WindowManagerService 中的相关方法去加入窗体(这里我就不做详细介绍了,感兴趣的去看看我上面提到的博客: java/android 设计模式学习笔记(8)—桥接模式 和博客 Android TransactionTooLargeException 解析。思考与监控方案)。scheduleTraversals 函数最后会调用到 doTraversal 方法。doTraversal 方法又调用 performTraversals 函数,performTraversals 函数就非常熟悉了,他会去调用 performMeasure。performLayout 和 performDraw 函数去进行 view 的计算和绘制,我们仅仅是在一个比較高的层次上概括性地梳理了它的整个脉络,它的简化结构:
接下来的绘制过程我在这就不说了,感兴趣的我这推荐一篇非常好的博客:http://blog.csdn.net/jacklam200/article/details/50039189,讲的真的非常详细,或者能够看看这个英文资料Android Graphics Architecture。
回到“ 为什么最后会调用到 ViewRootImpl 类中” 这个问题,从上面能够理解到。每一个 Window 都相应着一个 View 和一个 ViewRootImpl。Window 和 View 是通过 ViewRootImpl 来建立关联的,所以 invalidateChildInParent 会一直 while 循环直到调用到 ViewRootImpl 的 invalidateChildInParent 函数中:
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
....
parent = parent.invalidateChildInParent(location, dirty);
....
} while (parent != null);
这个问题就几乎相同清楚了。其它的能够再看看老罗的博客:http://blog.csdn.net/luoshengyang/article/details/8223770。
主线程与子线程ui讨论
上面分析了 Activity 的启动和布局创建过程。当中知道 Activity 的创建须要新建一个 ViewRootImpl 对象,看看 ViewRootImpl 的构造函数:
public ViewRootImpl(Context context, Display display) {
.....
mThread = Thread.currentThread();
.....
}
在初始化一个 ViewRootImpl 函数的时候,会调用 native 方法,获取到该线程对象 mThread。接着 setText 函数会调用到 requestLayout 方法(TextView 绘制出来之后,调用 setText 才会去调用 requestLayout 方法,没有绘制出来之前。在子线程中调用 setText 是不会抛出 Exception):
public void requestLayout() {
.....
checkThread();
.....
}
....
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
所以如今 “不能在子线程中更新 ui” 的问题已经非常清楚了。无论 startActivity 函数调用在什么线程,ActivityThread 的内部函数运行是在主线程中的:
/**
* This manages the execution of the main thread in an
* application process, scheduling and executing activities,
* broadcasts, and other operations on it as the activity
* manager requests.
*/
public final class ActivityThread {
....
}
所以 ViewRootImpl 对象的创建也是在主线程中。这就是说一个 activity 的相应 ViewRootImpl 对象中的 mThread 一定是代表主线程,这就是“为什么不能在子线程中操作 UI 的”答案的解释,问题解决!。!
可是不是说这个答案不严谨么?是的,可不能够在子线程中加入 Window,而且创建 ViewRootImpl 呢?当然能够,在子线程中创建一个 Window 就能够,思路是在子线程中调用 WindowManager 加入一个 view。类似于
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
params.format = PixelFormat.TRANSPARENT;
params.gravity = Gravity.CENTER;
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
....
windowManager.addView(v, params);
android WindowManager解析与骗取QQpassword案例分析博客中介绍到 activity 和 dialog 不是系统层级的 Window,我们能够使用 WindowManager 来加入自己定义的系统 Window,那么问题又来了,系统级别 Window 是怎么加入的呢,老罗的还有一篇博客 http://blog.csdn.net/luoshengyang/article/details/8498908 中介绍到: “对于非输入法窗体、非壁纸窗体以及非 Activity 窗体来说。它们所相应的 WindowToken 对象是在它们添加到 WindowManagerService 服务的时候创建的……假设參数 attrs 所描写叙述的一个 WindowManager.LayoutParams 对象的成员变量 token 所指向的一个 IBinder 接口在 WindowManagerService 类的成员变量 mTokenMap 所描写叙述的一个 HashMap 中没有一个相应的 WindowToken 对象,而且该 WindowManager.LayoutParams 对象的成员变量 type 的值不等于 TYPE_INPUT_METHOD、TYPE_WALLPAPER,以及不在FIRST_APPLICATION_WINDOW 和LAST_APPLICATION_WINDOW,那么就意味着这时候要添加的窗体就既不是输入法窗体,也不是壁纸窗体和 Activity 窗体。因此,就须要以參数 attrs 所描写叙述的一个 WindowManager.LayoutParams 对象的成员变量 token 所指向的一个 IBinder 接口为參数来创建一个 WindowToken 对象,而且将该 WindowToken对象保存在 WindowManagerService 类的成员变量 mTokenMap 和 mTokenList 中。
”。
了解上面之后,换一种思路。就能够在子线程中创建 view 而且加入到 windowManager 中。
实现
有了思路之后,既能够来实现相关代码了:
new Thread(new Runnable() {
@Override
public void run() {
showWindow();
}
}).start();
......
private void showWindow(){
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
params.format = PixelFormat.TRANSPARENT;
params.gravity = Gravity.CENTER;
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
LayoutInflater inflater = LayoutInflater.from(this);
v = (RelativeLayoutWithKeyDetect) inflater.inflate(R.layout.window, null);
.....
windowManager.addView(v, params);
}
运行一下。报错:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
at android.view.ViewRootImpl$ViewRootHandler.<init>(ViewRootImpl.java:3185)
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:3483)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:261)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at com.android.grabqqpwd.BackgroundDetectService.showWindow(BackgroundDetectService.java:208)
at com.android.grabqqpwd.BackgroundDetectService.access$100(BackgroundDetectService.java:39)
at com.android.grabqqpwd.BackgroundDetectService$1.run(BackgroundDetectService.java:67)
at java.lang.Thread.run(Thread.java:818)
这是由于 ViewRootImpl 类内部会新建一个 ViewRootHandler 类型的 mHandler 用来处理相关消息。所以假设线程没有 Looper 是会报错的,加入 Looper,改动代码:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
showWindow();
handler = new Handler(){
@Override
public void dispatchMessage(Message msg) {
Looper.myLooper().quit();
L.e("quit");
}
};
Looper.loop();
}
}).start();
创建 Looper 之后,须要在必要时候调用 quit 函数将其退出。
这样就成功显示了
而且创建之后的 view 仅仅能在子线程中改动,不能在主线程中改动,要不然会抛出最開始的 ViewRootImpl$CalledFromWrongThreadException。
扩展
为什么 android 会设计成仅仅有创建 ViewRootImpl 的原始线程才干更改 ui 呢?这就要说到 Android 的单线程模型了。由于假设支持多线程改动 View 的话,由此产生的线程同步和线程安全问题将是非常繁琐的,所以 Android 直接就定死了。View 的操作必须在创建它的 UI 线程。从而简化了系统设计。
有没有能够在其它非原始线程更新 ui 的情况呢?有,SurfaceView 就能够在其它线程更新,详细的大家能够去网上了解一下相关资料。