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 方法。这部分内容我们就留在下一节在分析了。

参考资料

相关推荐
网络研究院1 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下1 小时前
android navigation 用法详细使用
android
小比卡丘4 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭5 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss6 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.7 小时前
数据库语句优化
android·数据库·adb
GEEKVIP9 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model200511 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏68911 小时前
Android广播
android·java·开发语言
与衫12 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql