android 不能在子线程中更新ui的讨论和分析

问题描述

做过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解析与骗取QQ密码案例分析:界面 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解析与骗取QQ密码案例分析博客中介绍到 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 就可以在其他线程更新,具体的大家可以去网上了解一下相关资料。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容