Android AMS 完全剖析 —— Activity 管理之启动过程情景分析4

本文基于 android-10.0.0_r41 版本讲解

上一节说到调用 mRootActivityContainer.resumeFocusedStacksTopActivities onPause 前一个 Activity。

需要注意的一点是: resumeFocusedStacksTopActivities 从方法名看上去,好像是要 resume 目标 Activity,实际上,当前情景下,只是 pause 启动端 Activity,也就是 pause Launcher Activity

接下来我们来看源码实现:

java 复制代码
    // frameworks/base/services/core/java/com/android/server/wm/RootActivityContainer.java
    boolean resumeFocusedStacksTopActivities(
            ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) {
        
        if (!mStackSupervisor.readyToResume()) {
            return false;
        }

        boolean result = false;
        if (targetStack != null && (targetStack.isTopStackOnDisplay()
                || getTopDisplayFocusedStack() == targetStack)) {
            // 关注点
            result = targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
        }

        
        // ......

        return result;
    }

经过一些状态的判断后,接着调用 resumeTopActivityUncheckedLocked 方法:

java 复制代码
    // /frameworks/base/services/core/java/com/android/server/wm/ActivityStack.java

    // 注意当前方法所在的对象是 targetStack 就是待启动的目标 Activity 所在的 ActivityStack
    boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) {
        
        if (mInResumeTopActivity) {
            // Don't even start recursing.
            return false;
        }

        boolean result = false;
        try {
            // Protect against recursion.
            mInResumeTopActivity = true;

            // 关注点
            result = resumeTopActivityInnerLocked(prev, options);


            // resume 过程中,如果锁屏了,sleep 当前 Activity
            // 当前是 onPause 过程,关系不大
            // When resuming the top activity, it may be necessary to pause the top activity (for
            // example, returning to the lock screen. We suppress the normal pause logic in
            // {@link #resumeTopActivityUncheckedLocked}, since the top activity is resumed at the
            // end. We call the {@link ActivityStackSupervisor#checkReadyForSleepLocked} again here
            // to ensure any necessary pause logic occurs. In the case where the Activity will be
            // shown regardless of the lock screen, the call to
            // {@link ActivityStackSupervisor#checkReadyForSleepLocked} is skipped.
            final ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);
            if (next == null || !next.canTurnScreenOn()) {
                checkReadyForSleep();
            }
        } finally {
            mInResumeTopActivity = false;
        }

        return result;
    }

接着调用 resumeTopActivityInnerLocked 方法,需要注意的是:

  • 这个时候,内部的数据结构(ActivityRecord TaskRecord ActivityStack)都设置好了。
  • resumeTopActivityInnerLocked 是 ActivityStack 对象的成员函数,当前方法所在的对象是 targetStack ,就是待启动的目标 Activity 所在的 ActivityStack
  • 方法参数 pre 名字有点迷惑,实际上 pre 对应的 Activity 是目标待启动 Activity
java 复制代码
    // /frameworks/base/services/core/java/com/android/server/wm/ActivityStack.java

    /*
        ActivityRecord prev:目标 Activity 的相关信息
        ActivityOptions options:额外附加信息
    */
    @GuardedBy("mService")
    private boolean resumeTopActivityInnerLocked (ActivityRecord prev, ActivityOptions options) {
        
        // 如果系统还未启动完毕,那 AMS 还不能正常工作,所以也不能显示 Activity,主要是为防止没有开机启动完成
        if (!mService.isBooting() && !mService.isBooted()) {
            // Not ready yet!
            return false;
        }


        // 就是目标 Activity,和参数中的 pre 是同一个对象
        // 因为内部数据结构(ActivityRecord TaskRecord ActivityStack)都安排好了,所以 topRunningActivityLocked 获取到的就是目标 Activity
        // 但是实际上那个目标 Activity 还没有 Running
        ActivityRecord next = topRunningActivityLocked(true /* focusableOnly */);

        final boolean hasRunningActivity = next != null;

        // ......

        // 当前可能存在一些正处于 Intializing 状态的 ActivityRecord,
	    // 如果这些 ActivityRecord 不是位于栈顶,而且正在执行窗口启动动画,
	    // 那么,就需要取消这些 Activity 的启动动画。
        mRootActivityContainer.cancelInitializingActivities();


        //这个变量是表示是否回调 Activity 中的 onUserLeaveHint 和 onUserInteraction 函数
        // Remember how we'll process this pause/resume situation, and ensure
        // that the state is reset however we wind up proceeding.
        boolean userLeaving = mStackSupervisor.mUserLeaving;
        mStackSupervisor.mUserLeaving = false;

        //......

        next.delayedResume = false;
        
        final ActivityDisplay display = getDisplay();

        
        // ......

        /*
			在 mStackSupervisor 中存在很多的数据结构,用来统一管理 ActivityRecord 的状态
	    	
            mStoppingActivities 记录了当前所有处于 Stopping 状态的 ActivityRecord

	    	mGoingToSleepActivities 记录了当前所有要进入休眠状态的 ActivityRecord
	    	
            在某些场景下,待显示的 ActivityRecord 可能处于这些数组中,需要从中剔除
		*/
        // The activity may be waiting for stop, but that is no longer
        // appropriate for it.
        mStackSupervisor.mStoppingActivities.remove(next);
        mStackSupervisor.mGoingToSleepActivities.remove(next);
        next.sleeping = false;

        if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Resuming " + next);

        // ......

        /*
			setLaunchSource 设置待启动的 Activity 的信息
			跟进 setLaunchSource 源码发现它最终会获取一个 WakeLock,保证在显示 Activity 的过程中,系统不会进行休眠状态
		*/
        mStackSupervisor.setLaunchSource(next.info.applicationInfo.uid);

        boolean lastResumedCanPip = false;
        ActivityRecord lastResumed = null;
        final ActivityStack lastFocusedStack = display.getLastFocusedStack();

        if (lastFocusedStack != null && lastFocusedStack != this) { // 进入
            // So, why aren't we using prev here??? See the param comment on the method. prev doesn't
            // represent the last resumed activity. However, the last focus stack does if it isn't null.
            lastResumed = lastFocusedStack.mResumedActivity;
            if (userLeaving && inMultiWindowMode() && lastFocusedStack.shouldBeVisible(next)) {
                // The user isn't leaving if this stack is the multi-window mode and the last
                // focused stack should still be visible.
                if(DEBUG_USER_LEAVING) Slog.i(TAG_USER_LEAVING, "Overriding userLeaving to false"
                        + " next=" + next + " lastResumed=" + lastResumed);
                userLeaving = false;
            }
            lastResumedCanPip = lastResumed != null && lastResumed.checkEnterPictureInPictureState(
                    "resumeTopActivity", userLeaving /* beforeStopping */);
        }
        // If the flag RESUME_WHILE_PAUSING is set, then continue to schedule the previous activity
        // to be paused, while at the same time resuming the new resume activity only if the
        // previous activity can't go into Pip since we want to give Pip activities a chance to
        // enter Pip before resuming the next activity.
        final boolean resumeWhilePausing = (next.info.flags & FLAG_RESUME_WHILE_PAUSING) != 0
                && !lastResumedCanPip; // false

        /* 
		  开始 Pause Activity
 		*/
        boolean pausing = getDisplay().pauseBackStacks(userLeaving, next, false);
        if (mResumedActivity != null) { // 不进入
            if (DEBUG_STATES) Slog.d(TAG_STATES,
                    "resumeTopActivityLocked: Pausing " + mResumedActivity);
           
            pausing |= startPausingLocked(userLeaving, false, next, false);
        }
        if (pausing && !resumeWhilePausing) {
            if (DEBUG_SWITCH || DEBUG_STATES) Slog.v(TAG_STATES,
                    "resumeTopActivityLocked: Skip resume: need to start pausing");
            // At this point we want to put the upcoming activity's process
            // at the top of the LRU list, since we know we will be needing it
            // very soon and it would be a waste to let it get killed if it
            // happens to be sitting towards the end.
            if (next.attachedToProcess()) {
                next.app.updateProcessInfo(false /* updateServiceConnectionActivities */,
                        true /* activityChange */, false /* updateOomAdj */);
            }
            if (lastResumed != null) { // 进入这个分支
                lastResumed.setWillCloseOrEnterPip(true);
            }
            // 返回
            return true;
        } 

        // ......
        return true;
    }

接着开始执行 pauseBackStacks 方法, pause 前一个 Activity。

java 复制代码
    // services/core/java/com/android/server/wm/ActivityDisplay.java
    boolean pauseBackStacks(boolean userLeaving, ActivityRecord resuming, boolean dontWait) {
        boolean someActivityPaused = false;
        for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
            final ActivityStack stack = mStacks.get(stackNdx);
            final ActivityRecord resumedActivity = stack.getResumedActivity();
            if (resumedActivity != null
                    && (stack.getVisibility(resuming) != STACK_VISIBILITY_VISIBLE
                        || !stack.isFocusable())) {
                if (DEBUG_STATES) Slog.d(TAG_STATES, "pauseBackStacks: stack=" + stack +
                        " mResumedActivity=" + resumedActivity);
                someActivityPaused |= stack.startPausingLocked(userLeaving, false, resuming,
                        dontWait);
            }
        }
        return someActivityPaused;
    }

pauseBackStacks 的任务就是通过循环找到栈顶 Activity 是 resumed 状态且非 Focusable 的 ActivityStack,然后调用 ActivityStack 的 startPausingLocked 方法:

我们假定的情景下,这里的 ActivityStack 就是 Launcher App 所在的 ActivityStack

java 复制代码
    // services/core/java/com/android/server/wm/ActivityStack.java

    final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping,
            ActivityRecord resuming, boolean pauseImmediately) {
        if (mPausingActivity != null) { // mPausingActivity 为 null,不进入
            Slog.wtf(TAG, "Going to pause when pause is already pending for " + mPausingActivity
                    + " state=" + mPausingActivity.getState());
            if (!shouldSleepActivities()) {
                // Avoid recursion among check for sleep and complete pause during sleeping.
                // Because activity will be paused immediately after resume, just let pause
                // be completed by the order of activity paused from clients.
                completePauseLocked(false, resuming);
            }
        }

        // mResumedActivity 就是 Launcher Activity
        ActivityRecord prev = mResumedActivity;

        if (prev == null) {
            if (resuming == null) {
                Slog.wtf(TAG, "Trying to pause when nothing is resumed");
                mRootActivityContainer.resumeFocusedStacksTopActivities();
            }
            return false;
        }

        // resuming 是待启动的 Activity
        if (prev == resuming) { // 不进入
            Slog.wtf(TAG, "Trying to pause activity that is in process of being resumed");
            return false;
        }

        if (DEBUG_STATES) Slog.v(TAG_STATES, "Moving to PAUSING: " + prev);
        else if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Start pausing: " + prev);
        mPausingActivity = prev; // Laucher
        mLastPausedActivity = prev;
        // null
        mLastNoHistoryActivity = (prev.intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY) != 0
                || (prev.info.flags & ActivityInfo.FLAG_NO_HISTORY) != 0 ? prev : null;
        
        // 设置状态,会发起 Binder RPC 调用到 app 通知状态改变,不是重点
        prev.setState(PAUSING, "startPausingLocked");
        // 记录一个时间值,表示目标 Activity 开始 active 了
        prev.getTaskRecord().touchActiveTime();
        clearLaunchTime(prev);

        mService.updateCpuStats();

        if (prev.attachedToProcess()) { // 进入分支
            if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Enqueueing pending pause: " + prev);
            try {
                EventLogTags.writeAmPauseActivity(prev.mUserId, System.identityHashCode(prev),
                        prev.shortComponentName, "userLeaving=" + userLeaving);

                // 走这里,发起 Binder RPC 调用,通知 App 执行 onPause
                mService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
                        prev.appToken, PauseActivityItem.obtain(prev.finishing, userLeaving,
                                prev.configChangeFlags, pauseImmediately));
            } catch (Exception e) {
                // Ignore exception, if process died other code will cleanup.
                Slog.w(TAG, "Exception thrown during pause", e);
                mPausingActivity = null;
                mLastPausedActivity = null;
                mLastNoHistoryActivity = null;
            }
        }

        // ...... 
    }

接着调用 mService.getLifecycleManager().scheduleTransaction 向 App 端发起 Binder RPC 调用:

java 复制代码
    // /frameworks/base/core/java/android/app/servertransaction/ClientTransaction.java
    /**
     * Schedule a single lifecycle request or callback to client activity.
     * @param client Target client.
     * @param activityToken Target activity token.
     * @param stateRequest A request to move target activity to a desired lifecycle state.
     * @throws RemoteException
     *
     * @see ClientTransactionItem
     */
    void scheduleTransaction(@NonNull IApplicationThread client, @NonNull IBinder activityToken,
            @NonNull ActivityLifecycleItem stateRequest) throws RemoteException {
        final ClientTransaction clientTransaction = transactionWithState(client, activityToken,
                stateRequest);
        scheduleTransaction(clientTransaction);
    }

接着调用重载 scheduleTransaction:

java 复制代码
    // /frameworks/base/core/java/android/app/servertransaction/ClientTransaction.java
    void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
        final IApplicationThread client = transaction.getClient();
        // 关注点
        transaction.schedule();
        if (!(client instanceof Binder)) {
            // If client is not an instance of Binder - it's a remote call and at this point it is
            // safe to recycle the object. All objects used for local calls will be recycled after
            // the transaction is executed on client in ActivityThread.
            transaction.recycle();
        }
    }    

接着调用 schedule:

java 复制代码
 // /frameworks/base/core/java/android/app/servertransaction/ClientTransaction.java
    /** Target client. */
    private IApplicationThread mClient;

    /**
     * Schedule the transaction after it was initialized. It will be send to client and all its
     * individual parts will be applied in the following sequence:
     * 1. The client calls {@link #preExecute(ClientTransactionHandler)}, which triggers all work
     *    that needs to be done before actually scheduling the transaction for callbacks and
     *    lifecycle state request.
     * 2. The transaction message is scheduled.
     * 3. The client calls {@link TransactionExecutor#execute(ClientTransaction)}, which executes
     *    all callbacks and necessary lifecycle transitions.
     */
    public void schedule() throws RemoteException {
        mClient.scheduleTransaction(this);
    }

层层调用到 IApplicationThread 的 scheduleTransaction 方法,这是一个 Binder RPC 调用:

目前只远程调用 App 端的 scheduleTransaction 方法,pause 前一个 Activity,scheduleTransaction 方法是 oneway 的,所以很可能前一个 Activity 还没有 onPause, 该方法就返回了

它会远程调用到 Activity 端:

java 复制代码
// /frameworks/base/core/java/android/app/ActivityThread.java
@Override
public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
    ActivityThread.this.scheduleTransaction(transaction);
}

/** Prepare and schedule transaction for execution. */
void scheduleTransaction(ClientTransaction transaction) {
    transaction.preExecute(this);
    sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
}

这里会发送一个 Message,ActivityThread 中的 Handler 中会处理到这个 Message:

java 复制代码
// /frameworks/base/core/java/android/app/ActivityThread.java
        public void handleMessage(Message msg) {
                case EXECUTE_TRANSACTION:
                    // 从 Message 中取出消息
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    // 执行消息
                    mTransactionExecutor.execute(transaction);
                    if (isSystem()) {
                        // Client transactions inside system process are recycled on the client side
                        // instead of ClientLifecycleManager to avoid being cleared before this
                        // message is handled.
                        transaction.recycle();
                    }
                    // TODO(lifecycler): Recycle locally scheduled transactions.
                    break;            
        }
  • 从 Message 中取出消息
  • 执行消息

接着我们来看执行消息的过程:

java 复制代码
    // /frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java
    public void execute(ClientTransaction transaction) {
        if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Start resolving transaction");

        // ......

        // 执行回调
        executeCallbacks(transaction);

        // 执行生命周期
        executeLifecycleState(transaction);
        mPendingActions.clear();
        if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "End resolving transaction");
    }

接着调用 executeLifecycleState 来执行生命周期:

java 复制代码
    // /frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java
        /** Transition to the final state if requested by the transaction. */
    private void executeLifecycleState(ClientTransaction transaction) {

        // 拿到 ActivityLifecycleItem,这里是子类 PauseActivityItem
        final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest();
        
        // ......

        // token 是一个索引值
        // 通过 token 可以拿到 ActivityClientRecord,这个对象是 App 端的,用于描述描述一个 Activity
        final IBinder token = transaction.getActivityToken();
        final ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
        if (DEBUG_RESOLVER) {
            Slog.d(TAG, tId(transaction) + "Resolving lifecycle state: "
                    + lifecycleItem + " for activity: "
                    + getShortActivityName(token, mTransactionHandler));
        }

        if (r == null) {
            // Ignore requests for non-existent client records for now.
            return;
        }

        // Cycle to the state right before the final requested state.
        cycleToPath(r, lifecycleItem.getTargetState(), true /* excludeLastState */, transaction);

        // Execute the final transition with proper parameters.
        // 执行生命周期
        lifecycleItem.execute(mTransactionHandler, token, mPendingActions);
        // 执行生命周期后的操作也要关心
        lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions);
    }

接着执行 PauseActivityItem 的 execute:

java 复制代码
    // /frameworks/base/core/java/android/app/servertransaction/PauseActivityItem.java
    @Override
    public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
        // 执行回调
        client.handlePauseActivity(token, mFinished, mUserLeaving, mConfigChanges, pendingActions,
                "PAUSE_ACTIVITY_ITEM");
        Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
    }

client 的具体类型是 ActivityThread 接着执行到 ActivityThread 的 handlePauseActivity 方法:

java 复制代码
// /frameworks/base/core/java/android/app/ActivityThread.java

    @Override
    public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, PendingTransactionActions pendingActions, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        if (r != null) {
            if (userLeaving) { // userLeaving 相关的回调
                performUserLeavingActivity(r);
            }

            r.activity.mConfigChangeFlags |= configChanges;
            // 关注点
            performPauseActivity(r, finished, reason, pendingActions);

            // Make sure any pending writes are now committed.
            if (r.isPreHoneycomb()) {
                QueuedWork.waitToFinish();
            }
            mSomeActivitiesChanged = true;
        }
    }

接着调用 performPauseActivity:

java 复制代码
    // /frameworks/base/core/java/android/app/ActivityThread.java

    /**
     * Pause the activity.
     * @return Saved instance state for pre-Honeycomb apps if it was saved, {@code null} otherwise.
     */
    private Bundle performPauseActivity(ActivityClientRecord r, boolean finished, String reason,
            PendingTransactionActions pendingActions) {
        
        //......

        // 关注点
        performPauseActivityIfNeeded(r, reason);

        //......
    }

接着调用 performPauseActivityIfNeeded

java 复制代码
    // /frameworks/base/core/java/android/app/ActivityThread.java
    private void performPauseActivityIfNeeded(ActivityClientRecord r, String reason) {
        
        // ......

        try {
            r.activity.mCalled = false;
            // 关注点
            mInstrumentation.callActivityOnPause(r.activity);
            if (!r.activity.mCalled) {
                throw new SuperNotCalledException("Activity " + safeToComponentShortString(r.intent)
                        + " did not call through to super.onPause()");
            }
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            if (!mInstrumentation.onException(r.activity, e)) {
                throw new RuntimeException("Unable to pause activity "
                        + safeToComponentShortString(r.intent) + ": " + e.toString(), e);
            }
        }
        r.setState(ON_PAUSE);
    }

接着会调用 Activity 的 callActivityOnPause:

java 复制代码
    // /frameworks/base/core/java/android/app/Instrumentation.java
    /**
     * Perform calling of an activity's {@link Activity#onPause} method.  The
     * default implementation simply calls through to that method.
     * 
     * @param activity The activity being paused.
     */
    public void callActivityOnPause(Activity activity) {
        activity.performPause();
    }

接着执行 Activity 的 performPause 方法:

java 复制代码
// /frameworks/base/core/java/android/app/Activity.java
    final void performPause() {
        dispatchActivityPrePaused();
        mDoReportFullyDrawn = false;
        mFragments.dispatchPause();
        mCalled = false;
        onPause();
        writeEventLog(LOG_AM_ON_PAUSE_CALLED, "performPause");
        mResumed = false;
        if (!mCalled && getApplicationInfo().targetSdkVersion
                >= android.os.Build.VERSION_CODES.GINGERBREAD) {
            throw new SuperNotCalledException(
                    "Activity " + mComponent.toShortString() +
                    " did not call through to super.onPause()");
        }
        dispatchActivityPostPaused();
    }

在这里就会执行到 Activity 的 onPause 生命周期方法。

生命周期执行完后,接下来回到之前的 executeLifecycleState 方法中:

java 复制代码
        /** Transition to the final state if requested by the transaction. */
    private void executeLifecycleState(ClientTransaction transaction) {
        // 拿到 ActivityLifecycleItem,这里是子类 PauseActivityItem
        final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest();
        
        // ......

        // 执行生命周期
        lifecycleItem.execute(mTransactionHandler, token, mPendingActions);
        lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions);
    }

在调用 execute 方法,执行完 onPause 生命周期方法后,接着会调用到 lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions);

java 复制代码
    @Override
    public void postExecute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        if (mDontReport) {
            return;
        }
        try {
            // TODO(lifecycler): Use interface callback instead of AMS.
            ActivityTaskManager.getService().activityPaused(token);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

在这里,会远程调用到 atms 的 activityPaused 方法。这部分内容我们就留在下一节在分析了。

参考资料

相关推荐
松仔log7 小时前
Java中级——组合和继承
android·java·开发语言
我命由我1234510 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian23311 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_9557596211 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马11 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone11 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone11 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子15 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker15 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone15 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常