Android 不能在子執行緒中更新 UI 的討論和分析

Shawn_Dut發表於2016-11-22

問題描述

  做過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)---代理模式,裡面有詳細介紹到):

Android 不能在子執行緒中更新 UI 的討論和分析
這裡寫圖片描述

接著進行完一系列的操作之後會回撥到 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)---橋接模式,其實這裡是有用到橋接模式的):

Android 不能在子執行緒中更新 UI 的討論和分析
這裡寫圖片描述

而 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 的計算和繪製,我們只是在一個比較高的層次上概括性地梳理了它的整個脈絡,它的簡化結構:

Android 不能在子執行緒中更新 UI 的討論和分析
這裡寫圖片描述

接下來的繪製過程我在這就不說了,感興趣的我這推薦一篇非常好的部落格:blog.csdn.net/jacklam200/… Graphics Architecture](download.csdn.net/detail/zhao…
  回到“ 為什麼最後會呼叫到 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);複製程式碼

  這個問題就差不多清楚了,其他的可以再看看老羅的部落格:blog.csdn.net/luoshengyan…

主執行緒與子執行緒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 是怎麼新增的呢,老羅的另一篇部落格 blog.csdn.net/luoshengyan… 中介紹到: “對於非輸入法視窗、非桌布視窗以及非 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 函式將其退出。這樣就成功顯示了

Android 不能在子執行緒中更新 UI 的討論和分析
這裡寫圖片描述

而且建立之後的 view 只能在子執行緒中修改,不能在主執行緒中修改,要不然會丟擲最開始的 ViewRootImpl$CalledFromWrongThreadException。

擴充套件

  為什麼 android 會設計成只有建立 ViewRootImpl 的原始執行緒才能更改 ui 呢?這就要說到 Android 的單執行緒模型了,因為如果支援多執行緒修改 View 的話,由此產生的執行緒同步和執行緒安全問題將是非常繁瑣的,所以 Android 直接就定死了,View 的操作必須在建立它的 UI 執行緒,從而簡化了系統設計。
  有沒有可以在其他非原始執行緒更新 ui 的情況呢?有,SurfaceView 就可以在其他執行緒更新,具體的大家可以去網上了解一下相關資料。

相關文章