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 就可以在其他線程更新,具體的大家可以去網上了解一下相關資料。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容