Android 主线程一定是 UI 线程吗?
背景知识:Android 线程模型基础
在 Android 开发中,线程管理是保证应用流畅性和稳定性的核心。官方文档中曾提到:"Main thread is also sometimes called the UI thread."(主线程有时也被称为 UI 线程)。然而,这句话并非在所有场景下都绝对成立。理解主线程(Main Thread)与 UI 线程(UI Thread)的区别,对于深入掌握 Android 框架机制至关重要。
在线程注解的章节中,Google 给出了更严谨的定义:构建工具会将 @MainThread 和 @UiThread 注解视为可互换,允许从 @MainThread 方法调用 @UiThread 方法,反之亦然。但是,如果不同线程上具有多个视图的系统应用程序的界面线程可能会与主线程不同。因此,建议开发者使用 @UiThread 为与应用的视图层次结构关联的方法添加注解,并使用 @MainThread 仅为与应用生命周期关联的方法添加注解。
核心源码深入:ActivityThread 初始化流程
为了探究这一结论,我们从 Activity#runOnUiThread(Runnable) 入手,查看系统源码。
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
这段代码逻辑非常清晰:它判断当前线程是否等于 mUiThread。如果不是,则通过 Handler 发送消息到 UI 线程处理;如果是,则直接执行。这里的焦点在于 mUiThread 属性的初始化位置。
在 Activity.java 的 attach 方法中,我们可以找到 mUiThread 的赋值过程:
@UnsupportedAppUsage
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,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
attachBaseContext(context);
mUiThread = Thread.currentThread();
mMainThread = aThread;
}
这里有两个关键属性:
mUiThread:直接被赋值为当前线程(即执行 attach 方法的线程)。
mMainThread:其值来自于 attach 方法的参数 aThread,类型为 ActivityThread。
这两个属性从名字上看就可以理解为什么会有这种说法了,因为它俩是两个对象。mUiThread 代表当前运行上下文中的线程实例,而 mMainThread 代表 ActivityThread 进程的主线程句柄。
ActivityThread 的入口点分析
attach 方法的调用栈通常位于 ActivityThread.java 的 performLaunchActivity 或 handleLaunchActivity 中。在这个调用链中,mMainThread 传入的是 this 对象,即当前的 ActivityThread 实例。我们需要查看 ActivityThread 对象的创建位置,主要有两个入口:
- main(String[] args):普通应用程序的启动入口。
- systemMain():系统进程(SystemServer)的启动入口。
main 方法详解
public static void main(String[] args) {
AndroidOs.install();
CloseGuard.setEnabled(false);
Environment.initForCurrentUser();
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);
initializeMainlineModules();
Process.setArgV0("<pre-initialized>");
Looper.prepareMainLooper();
long startSeq = 0;
if (args != null) {
for (int i = args.length - 1; i >= 0; --i) {
if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
startSeq = Long.parseLong(
args[i].substring(PROC_START_SEQ_IDENT.length()));
}
}
}
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException();
}
这个 main 方法中,有我们熟悉的 Handler 机制中的 Looper。在 Looper.loop() 方法后一行是抛出主线程 loop 异常退出的 RuntimeException。这证明了我们的应用程序中的主线程 Looper 退出后会导致的异常,这也是为什么主线程不能阻塞的原因。
main 方法的调用,在应用程序进程启动流程中的 RuntimeInit.applicationInit(...) 中最后的 findStaticMain 方法中,通过反射调用的。这确保了每个应用进程都会独立初始化自己的 ActivityThread 实例。
systemMain 方法详解
public static ActivityThread systemMain() {
if (!ActivityManager.isHighEndGfx()) {
ThreadedRenderer.disable(true);
} else {
ThreadedRenderer.enableForegroundTrimming();
}
ActivityThread thread = new ActivityThread();
thread.attach(true, 0);
return thread;
}
从备注看,就可以知道这个方法和系统进程有关系。systemMain 方法的调用是在 SystemServer 中:
private void createSystemContext() {
ActivityThread activityThread = ActivityThread.systemMain();
mSystemContext = activityThread.getSystemContext();
mSystemContext.setTheme(DEFAULT_SYSTEM_THEME);
final Context systemUiContext = activityThread.getSystemUiContext();
systemUiContext.setTheme(DEFAULT_SYSTEM_THEME);
}
它的调用栈是:
frameworks/base/services/java/com/android/server/SystemServer.java#run
frameworks/base/services/java/com/android/server/SystemServer.java#main
根源来自于 SystemServer 的 main 方法,这个方法是 SystemServer 进程启动的最主要的方法。
ActivityThread#attach 方法差异分析
从代码中,两者都是创建了一个 ActivityThread 对象,然后调用了 ActivityThread 的 attach 方法,两者参数不同:
thread.attach(false, startSeq);
thread.attach(true, 0);
去 ActivityThread 查看 attach 方法:
@UnsupportedAppUsage
private void attach(boolean system, long startSeq) {
sCurrentActivityThread = this;
mSystemThread = system;
if (!system) {
android.ddm.DdmHandleAppName.setAppName("<pre-initialized>", UserHandle.myUserId());
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManager.getService();
try {
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
BinderInternal.addGcWatcher(new Runnable() {
@Override public void run() {
if (!mSomeActivitiesChanged) {
return;
}
Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) {
mSomeActivitiesChanged = false;
try {
ActivityTaskManager.getService().releaseSomeActivities(mAppThread);
} (RemoteException e) {
e.rethrowFromSystemServer();
}
}
}
});
} {
android.ddm.DdmHandleAppName.setAppName(, UserHandle.myUserId());
{
mInstrumentation = ();
mInstrumentation.basicInit();
ContextImpl.createAppContext(, getSystemContext().mPackageInfo);
mInitialApplication = context.mPackageInfo.makeApplication(, );
mInitialApplication.onCreate();
} (Exception e) {
( + e.toString(), e);
}
}
ViewRootImpl.
(Configuration globalConfig) -> {
(mResourcesManager) {
(mResourcesManager.applyConfigurationToResourcesLocked(globalConfig,
)) {
updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
mResourcesManager.getConfiguration().getLocales());
(mPendingConfiguration ==
|| mPendingConfiguration.isOtherSeqNewer(globalConfig)) {
mPendingConfiguration = globalConfig;
sendMessage(H.CONFIGURATION_CHANGED, globalConfig);
}
}
}
};
ViewRootImpl.addConfigCallback(configChangedCallback);
}
在这个方法中,第一个参数 system 标志着当前是不是系统应用,第二个参数也只在非系统应用程序的创建过程中用到。所以,从这里可以看出系统应用程序和普通应用程序是有区别的,两者对主线程绑定操作是不一样的。
两者的不同之处
从 main 和 systemMain 的调用来源可以分析出,两者的不同之处:
main 方法用于在应用程序创建过程中绑定 ActivityThread。
systemMain 方法用于 SystemServer 中,绑定 ActivityThread。
而从 ActivityThread#attach 方法中,可以分析出:
普通的应用程序,将当前 ActivityThread 的 ApplicationThread 和参数 startSeq 通过 IActivityManager.attachApplication(mAppThread, startSeq) 绑定到了 AMS,这里是个 Binder 通信,并且,还添加了一个关于 GC 的 Binder 监听。所以普通应用程序需要 Binder 绑定到另一个进程中。
而 SystemServer,首先创建了一个 Instrumentation,它的作用是监控系统与应用程序的交互,在这里它只是把 ActivityThread 对象保存了起来,然后创建了一个 ContextImpl,然后通过 ContextImpl.mPackageInfo.makeApplication() 创建了一个 Application,并调用了 onCreate 方法。整个流程没有 Binder 通信,说明就是在当前进程中进行的。
这里我一直有个疑问,SystemServer 算是一个系统应用程序吗? 对于传统的 Android 教程中,一直把 SystemServer 和进程的概念绑定在一起,一般提到 SystemServer,就会特指 SystemServer 进程。
首先进程和应用程序是两个概念。SystemServer 进程的创建是在 Zygote 进程启动过程的 ZygoteInit.startSystemServer 方法,在这个方法中,Zygote 执行了 Zygote.forkSystemServer,内部会调用到 nativeForkSystemServer 方法,就是在这里 fork 出了进程。Fork 出 SystemServer 进程后,会对 SystemServer 进行进行一些初始化的处理,最终调用的是 handleSystemServerProcess 方法。在这个方法中实际调用了 Zygote.zygoteInit,而在 zygoteInit 方法中,有以下操作:
ZygoteInit.nativeZygoteInit();
RuntimeInit.applicationInit(targetVersion, argv, classLoader);
RuntimeInit.applicationInit 内部调用了 SystemServer 类的 main 函数,也就是说,SystemServer 类 和 SystemServer 进程,是两个东西。SystemServer 类的代码作为应用程序运行在 SystemServer 进程中。
开发者实践建议
基于上述源码分析,我们在日常开发中需要注意以下几点:
- UI 更新规范:虽然主线程通常是 UI 线程,但在涉及跨进程通信或系统级服务时,不要默认认为所有主线程都能直接操作 View。始终使用
runOnUiThread 或 Handler 确保线程安全。
- 注解使用:遵循 Google 的建议,对视图相关方法使用
@UiThread,对生命周期相关方法使用 @MainThread,这样能更好地利用静态检查工具发现潜在的线程问题。
- SystemServer 隔离:了解 SystemServer 的特殊性,避免在系统服务层误用应用层的 UI 线程假设。SystemServer 拥有主线程,但可能不具备完整的 UI 线程上下文。
总结
每当一个新的应用程序启动时,ActivityThread 的 main 方法就会被执行。主线程正在那里初始化,并且启动 Activity 时,会通过 ActivityThread#performLaunchActivity 方法,内部调用到 Activity#attach 方法:
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,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
mUiThread = Thread.currentThread();
mMainThread = aThread;
}
这也是我们最开始分析 mUiThread 的来源时提到的位置。在这里,Activity 将 UI 线程初始化为当前线程,而这个方法的调用就是在 ActivityThread 自身中;并且 mMainThread 的值来自参数 aThread,这里传入的参数也是 ActivityThread 自身,所以,这里的 mMainThread 一定是与 mUiThread 相等的。
但 SystemServer 例外,它也创建并持有了一个 ActivityThread 对象。SystemServer 作为 Android 系统的一部分,也是作为一个应用程序运行的。但是这个应用程序是特殊的存在,它也需要配置一个 ActivityThread。
private void createSystemContext() {
ActivityThread activityThread = ActivityThread.systemMain();
mSystemContext = activityThread.getSystemContext();
mSystemContext.setTheme(DEFAULT_SYSTEM_THEME);
final Context systemUiContext = activityThread.getSystemUiContext();
systemUiContext.setTheme(DEFAULT_SYSTEM_THEME);
}
在 SystemServer 中,并没有把 ActivityThread 保存起来,也没有类似 mUiThread 的属性,所以,它只是有一个主线程,没有 UI 线程。强迫症的角度来说,这里的确是主线程不是 UI 线程。
还有令人在意的一点是这里 ActivityThread 有两个方法获取 Context:getSystemContext() 和 getSystemUiContext()。SystemContext 后续会用在各种 ManagerService 中使用。SystemUiContext 由 SystemContext 衍生出来,拓展了关于 UI 的一些能力。
SystemUiContext System Context to be used for UI. This Context has resources that can be themed. that the created system UI context shares the same LoadedApk as the system context.
主线程是不是 UI 线程的答案也就明确了,在应用程序中是,在 SystemServer 中,也有一个主线程,但是没有 UI 线程的概念。所以在应用程序开发中,主线程一定是 UI 线程没什么问题。