Android 源码 <Activity> 桌面启动二 [6]

Android Activity的启动流程源码解析(8.0) 一,写在前面        Activity是Android四大组件之一,用于直接跟用户进行交互,本篇文章将介绍Activity的启动流程。用户启动Activity的方式大致有两种:一种是在桌面点击应用程序的图标,进入应用程序的主界面;另一种是在应用程序中,进入一个新的Activity。前者,桌面其实是系统应用launcher的界面,点击应用程序图标,会进行应用程序的主界面,实质是从一个应用的A 阅读详情

在这里插入图片描述

如果能够用一张图对逝去的一周留个痕迹。

Read The Fucking Source Code. —— Linus

站在’巨人’的肩膀上开始自己的旅途。—— 佚名

愉快的周末,从打开💻开始,到骑行归来结束。—— 佚名

注: 本系列文章源码基于 Android 11-r21 master 分支

继篇 ——— Android 源码 <Activity> 桌面启动一 [5]

startActivityUnchecked

//ActivityStarter.java
private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
        IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
        int startFlags, boolean doResume, ActivityOptions options, Task inTask,
        TaskFragment inTaskFragment, boolean restrictedBgActivity,
        NeededUriGrants intentGrants) {
    int result = START_CANCELED;
    boolean startResultSuccessful = false;
    final Task startedActivityRootTask;

    //TRANSIT_OPEN:创建一个之前不存在的新窗口,并且让窗口可见
    //transitType 还会影响窗口绘制消息延迟时间,默认是 5秒,如果是 chenge 类型延时将缩短到 2 秒
    //也会把 windowContain 添加到集合中,等待窗口绘制
    final TransitionController transitionController = r.mTransitionController;
    Transition newTransition = (!transitionController.isCollecting()
            && transitionController.getTransitionPlayer() != null)
            ? transitionController.createTransition(TRANSIT_OPEN) : null;
    RemoteTransition remoteTransition = r.takeRemoteTransition();
    if (newTransition != null && remoteTransition != null) {
        newTransition.setRemoteTransition(remoteTransition);
    }
    transitionController.collect(r);
    final boolean isTransient = r.getOptions() != null && r.getOptions().getTransientLaunch();
    
    
    try {
        //延迟窗口测量,又使用一个单独的变量 mDeferDepth++,控制测量、绘制次数,避免递归循环
        mService.deferWindowLayout();
        //‼️又是一个启动阶段
        result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
                startFlags, doResume, options, inTask, inTaskFragment, restrictedBgActivity,
                intentGrants);
        startResultSuccessful = ActivityManager.isStartResultSuccessful(result);
        final boolean taskAlwaysOnTop = options != null && options.getTaskAlwaysOnTop();
        // Apply setAlwaysOnTop when starting an Activity is successful regardless of creating
        // a new Activity or recycling the existing Activity.
        if (taskAlwaysOnTop && startResultSuccessful) {
            final Task targetRootTask =
                    mTargetRootTask != null ? mTargetRootTask : mTargetTask.getRootTask();
            targetRootTask.setAlwaysOnTop(true);
        }
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
        //‼️无论成功失败与否,启动解释都应该分发出去
        startedActivityRootTask = handleStartResult(r, result);
        //延时窗口测量将被恢复
        mService.continueWindowLayout();
        mSupervisor.mUserLeaving = false;
    }

    postStartActivityProcessing(r, result, startedActivityRootTask);

    return result;
}

about TransitionType

@IntDef(prefix = { "TRANSIT_" }, value = {
        TRANSIT_NONE,
        TRANSIT_OPEN,                 //创建一个新的窗口,并且使其可见
        TRANSIT_CLOSE,                //可见的窗口被关闭(finished 或 destroyed)
        TRANSIT_TO_FRONT,             //不可见的窗口将变为可见
        TRANSIT_TO_BACK,              //可见的窗口变为不可见
        TRANSIT_RELAUNCH,
        TRANSIT_CHANGE,               //可见窗口发生改变(比如屏幕方向、大小改变)
        TRANSIT_KEYGUARD_GOING_AWAY,  //(已废弃)
        TRANSIT_KEYGUARD_OCCLUDE,     //键盘锁定
        TRANSIT_KEYGUARD_UNOCCLUDE,   //键盘解锁
        TRANSIT_PIP,                  //画中画
        TRANSIT_WAKE,                 //(正在打开?)
        TRANSIT_FIRST_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
@interface TransitionType {}

startActivityInner

//ActivityStarter.java
int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
        IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
        int startFlags, boolean doResume, ActivityOptions options, Task inTask,
        TaskFragment inTaskFragment, boolean restrictedBgActivity,
        NeededUriGrants intentGrants) {
    setInitialState(r, options, inTask, inTaskFragment, doResume, startFlags, sourceRecord,
            voiceSession, voiceInteractor, restrictedBgActivity);

    //确定 activity 所启动的任务栈应该是 NEW_TASK 还是在已有的任务栈启动
    computeLaunchingTaskFlags();
    computeSourceRootTask();
    mIntent.setFlags(mLaunchFlags);

    //如果请求已经开始,应该冻结最近任务列表,等待下次更新
    final Task prevTopTask = mPreferredTaskDisplayArea.getFocusedRootTask();
    final Task reusedTask = getReusableTask();
    if (mOptions != null && mOptions.freezeRecentTasksReordering()
            && mSupervisor.mRecentTasks.isCallerRecents(r.launchedFromUid)
            && !mSupervisor.mRecentTasks.isFreezeTaskListReorderingSet()) {
        mFrozeTaskList = true;
        mSupervisor.mRecentTasks.setFreezeTaskListReordering();
    }

    //计算是否有符合条件的任务栈可以复用,否则应该创建新的任务栈
    final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();
    final boolean newTask = targetTask == null;
    mTargetTask = targetTask;

    //确定启动参数,比如 windowType
    computeLaunchParams(r, sourceRecord, targetTask);

    //又是一番启动限制,在任务栈层面限制启动🚫
    int startResult = isAllowedToStart(r, newTask, targetTask);
    if (startResult != START_SUCCESS) {
        return startResult;
    }

    //复用任务栈
    final ActivityRecord targetTaskTop = newTask
            ? null : targetTask.getTopNonFinishingActivity();
    if (targetTaskTop != null) {
        /*
            1、resumeTargetRootTaskIfNeeded
            2、mRootWindowContainer.resumeFocusedTasksTopActivities
        */
        startResult = recycleTask(targetTask, targetTaskTop, reusedTask, intentGrants);
        if (startResult != START_SUCCESS) {
            return startResult;
        }
    } else {
        mAddingToTask = true;
    }

    /*
        1、如果启动的 activity 是在任务栈中已存在,则只需启动一次,并调用 activity 的 onNewIntent 方法即可
        2、回调方法 deliverNewIntent(top, intentGrants); ActivityRecorder#deliverNewIntentLocked
        3、mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
        NewIntentItem.obtain(ar, mState == RESUMED));
    */
    final Task topRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
    if (topRootTask != null) {
        startResult = deliverToCurrentTopIfNeeded(topRootTask, intentGrants);
        if (startResult != START_SUCCESS) {
            return startResult;
        }
    }

    //还是一样,如果不存在则创建,如果存在则复用
    if (mTargetRootTask == null) {
        mTargetRootTask = getLaunchRootTask(mStartActivity, mLaunchFlags, targetTask, mOptions);
    }
    if (newTask) {
        final Task taskToAffiliate = (mLaunchTaskBehind && mSourceRecord != null)
                ? mSourceRecord.getTask() : null;
        setNewTask(taskToAffiliate);
    } else if (mAddingToTask) {
        addOrReparentStartingActivity(targetTask, "adding to task");
    }

    //启动的目标任务栈有了,直接看 activity 启动
    final Task startedTask = mStartActivity.getTask();
    final boolean isTaskSwitch = startedTask != prevTopTask && !startedTask.isEmbedded();
    
    //启动
    mTargetRootTask.startActivityLocked(mStartActivity,
            topRootTask != null ? topRootTask.getTopNonFinishingActivity() : null, newTask,
            isTaskSwitch, mOptions, sourceRecord);
    if (mDoResume) {
        final ActivityRecord topTaskActivity = startedTask.topRunningActivityLocked();
        //如果本次启动的 activity 所在任务栈中并没有获得焦点,并且当前启动的不是本次想启动的,也要确保它显示(它可能是更重要的 activity 抢先显示呢)
        if (!mTargetRootTask.isTopActivityFocusable()
                || (topTaskActivity != null && topTaskActivity.isTaskOverlay()
                && mStartActivity != topTaskActivity)) {
                
            mTargetRootTask.ensureActivitiesVisible(null /* starting */,
            mTargetRootTask.mDisplayContent.executeAppTransition();
        } else {
            //如果本次启动的 activity 所在任务栈中并已获得焦点,如果该任务栈没有显示在最前则 moveToFront
            if (mTargetRootTask.isTopActivityFocusable()
                    && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) {
                mTargetRootTask.moveToFront("startActivityInner");
            }
            
            //这里和上述 recycleTask 相似,最终也会执行到这个方法。(把 activity 转移为可见状态)
            mRootWindowContainer.resumeFocusedTasksTopActivities(
                    mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
        }
    }
    
    //启动完毕需要更新最近任务栈等
    mRootWindowContainer.updateUserRootTask(mStartActivity.mUserId, mTargetRootTask);
    mSupervisor.mRecentTasks.add(startedTask);
    mSupervisor.handleNonResizableTaskIfNeeded(startedTask,
            mPreferredWindowingMode, mPreferredTaskDisplayArea, mTargetRootTask);

    return START_SUCCESS;
}

isAllowedToStart

检查 activity 是否可以在已有的任务栈或者新的任务栈中启动。

//ActivityStarter.java
private int isAllowedToStart(ActivityRecord r, boolean newTask, Task targetTask) {
    //❌1、没有包名是不允许的(每个 activity 都有所属的包)
    if (mStartActivity.packageName == null) {
        if (mStartActivity.resultTo != null) {
            mStartActivity.resultTo.sendResult(INVALID_UID, mStartActivity.resultWho,
                    mStartActivity.requestCode, RESULT_CANCELED,
                    null /* data */, null /* dataGrants */);
        }
        ActivityOptions.abort(mOptions);
        return START_CLASS_NOT_FOUND;
    }

    /*
        1、应用处于 instrument 状态时,应该取消启动
        2、如果是 VR 显示ID或者默认显示ID,允许启动
        3、launchMode != SINGLE_TASK && launchMode != SINGLE_INSTANCE 属于已有启动状态,应该取消启动
    */
    if (r.isActivityTypeHome()) {
        if (!mRootWindowContainer.canStartHomeOnDisplayArea(r.info, mPreferredTaskDisplayArea,
                true /* allowInstrumenting */)) {
            return START_CANCELED;
        }
    }

    /*
        1、❌如果是新的任务栈,从后台启动的 activity 是不允许的
        2、❌如果调用者 uid 不是当前程序(当前任务栈),启动时不允许的
        3、❌如果是需要创建新的任务栈,从后台启动的 activity 是不允许的
    */
    boolean blockBalInTask = (newTask
            || !targetTask.isUidPresent(mCallingUid)
            || (LAUNCH_SINGLE_INSTANCE == mLaunchMode && targetTask.inPinnedWindowingMode()));
    // mRestrictedBgActivity:严格把控 activity 的启动🚫(该条件前一篇有提到)
    if (mRestrictedBgActivity && blockBalInTask
            && handleBackgroundActivityAbort(mStartActivity)) {
        return START_ABORTED;
    }

    //还是在不断限制启动,条件苛刻啊
    final boolean isNewClearTask =
            (mLaunchFlags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))
                    == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
    if (!newTask) {
        if (mService.getLockTaskController().isLockTaskModeViolation(targetTask,
                isNewClearTask)) {
            return START_RETURN_LOCK_TASK_MODE_VIOLATION;
        }
    } else {
        if (mService.getLockTaskController().isNewTaskLockTaskModeViolation(mStartActivity)) {
            return START_RETURN_LOCK_TASK_MODE_VIOLATION;
        }
    }

    if (mInTaskFragment != null && !canEmbedActivity(mInTaskFragment, r, newTask, targetTask)) {
        return START_PERMISSION_DENIED;
    }

    //✅否则,是启动是允许的
    return START_SUCCESS;
}

canEmbedActivity

是否可以嵌入?activity 嵌入?

//ActivityStarter.java
private boolean canEmbedActivity(@NonNull TaskFragment taskFragment, ActivityRecord starting,
        boolean newTask, Task targetTask) {
    final Task hostTask = taskFragment.getTask();
    if (hostTask == null) {
        return false;
    }

    //✅如果是系统应用,是允许嵌入启动的
    final int hostUid = hostTask.effectiveUid;
    if (UserHandle.getAppId(hostUid) == Process.SYSTEM_UID) {
        return true;
    }

    //❌如果不是当前应用进程启动,是不允许的
    if (hostUid != starting.getUid()) {
        return false;
    }

    //❌如果不是同一个任务栈(主任务栈)中启动,也是不允许的
    return !newTask && (targetTask == null || targetTask == hostTask);
}

startActivityLocked

//Task.java
void startActivityLocked(ActivityRecord r, @Nullable ActivityRecord focusedTopActivity,
        boolean newTask, boolean isTaskSwitch, ActivityOptions options,
        @Nullable ActivityRecord sourceRecord) {
    Task rTask = r.getTask();
    
    final boolean allowMoveToFront = options == null || !options.getAvoidMoveToFront();
    final boolean isOrhasTask = rTask == this || hasChild(rTask);
    
    //启动的 activity 不能是阻塞的,否则将抛出异常
    Task task = null;
    if (!newTask && isOrhasTask) {
        final ActivityRecord occludingActivity = getOccludingActivityAbove(r);
        if (occludingActivity != null) {
            rTask.positionChildAtTop(r);
            ActivityOptions.abort(options);
            return;
        }
    }

    //允许移动到前台,并且不是桌面程序、是最近任务列表任务栈、任务栈已有activity
    if ((!isHomeOrRecentsRootTask() || hasActivity()) && allowMoveToFront) {
        boolean doShow = true;
        if (newTask) {
            if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
                resetTaskIfNeeded(r, r);
                doShow = topRunningNonDelayedActivityLocked(null) == r;
            }
        } else if (options != null && options.getAnimationType()
                == ActivityOptions.ANIM_SCENE_TRANSITION) {
            doShow = false;
        }
        if (r.mLaunchTaskBehind) {
            r.setVisibility(true);
            ensureActivitiesVisible(null, 0, !PRESERVE_WINDOWS);
            mDisplayContent.executeAppTransition();
        } else if (SHOW_APP_STARTING_PREVIEW && doShow) {
            Task baseTask = r.getTask();
            //‼️启动
            final ActivityRecord prev = baseTask.getActivity(
                    a -> a.mStartingData != null && a.showToCurrentUser());
            r.showStartingWindow(prev, newTask, isTaskSwitch,
                    true /* startActivity */, sourceRecord);
        }
    } else {
        //第一个启动的 activity 无需花里胡哨的动画
        ActivityOptions.abort(options);
    }
}

showStartingWindow

//ActivityRecord.java
void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch,
        boolean startActivity, ActivityRecord sourceRecord) {
    //覆盖时,不会显示
    if (mTaskOverlay) {
        return;
    }
    
    //共享元素转换时,不限制(共享元素:Android 动画部分)
    if (mPendingOptions != null
            && mPendingOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
        return;
    }

    final CompatibilityInfo compatInfo =
            mAtmService.compatibilityInfoForPackageLocked(info.applicationInfo);

    //是否使用启动页样式
    mSplashScreenStyleEmpty = shouldUseEmptySplashScreen(sourceRecord, startActivity);

    /*
        1、我们这就是启动 activity,所以 startActivit = true,那么将会获取启动主题
        (也就是 Android 高版本每个应用启动都会显示的开屏页?)
        2、开屏主题是可以重写的,首先尝试获取是否重新了开屏主题,将获取主题资源名称
        3、如果没有重写,将会通过 ATMS 根据包名和用户ID获取主题资源名称
        4、如果获取到开屏主题资源名称,那么将根据包名通过 createPackageContext 创建上下文,
           接着根据上下文和主题名称获取资源ID(0 表示使用默认的开屏主题)
    */
    final int splashScreenTheme = startActivity ? getSplashscreenTheme() : 0;
    
    //这里会评估应该使用 theme 主题还是 splashScreenTheme 主题
    final int resolvedTheme = evaluateStartingWindowTheme(prev, packageName, theme,
            splashScreenTheme);


    final boolean activityCreated =
            mState.ordinal() >= STARTED.ordinal() && mState.ordinal() <= STOPPED.ordinal();
    //如果不是新的任务栈,activity 也还没创建,那么本次是热启动
    final boolean newSingleActivity = !newTask && !activityCreated
            && task.getActivity((r) -> !r.finishing && r != this) == null;

    //‼️启动
    final boolean scheduled = addStartingWindow(packageName, resolvedTheme,
            compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
            prev, newTask || newSingleActivity, taskSwitch, isProcessRunning(),
            allowTaskSnapshot(), activityCreated, mSplashScreenStyleEmpty);
}

addStartingWindow

//ActivityRecord.java
boolean addStartingWindow(String pkg, int resolvedTheme, CompatibilityInfo compatInfo,
        CharSequence nonLocalizedLabel, int labelRes, int icon, int logo, int windowFlags,
        ActivityRecord from, boolean newTask, boolean taskSwitch, boolean processRunning,
        boolean allowTaskSnapshot, boolean activityCreated, boolean useEmpty) {
    //窗口被冻结,不能显示
    if (!okToDisplay()) {
        return false;
    }

    if (mStartingData != null) {
        return false;
    }

    //已有窗口在显示,不能再显示了
    final WindowState mainWin = findMainWindow();
    if (mainWin != null && mainWin.mWinAnimator.getShown()) {
        return false;
    }

    final TaskSnapshot snapshot =
            mWmService.mTaskSnapshotController.getSnapshot(task.mTaskId, task.mUserId,
                    false /* restoreFromDisk */, false /* isLowResolution */);
                    
     //STARTING_WINDOW_TYPE_NONE、STARTING_WINDOW_TYPE_SNAPSHOT、STARTING_WINDOW_TYPE_SPLASH_SCREEN
    final int type = getStartingWindowType(newTask, taskSwitch, processRunning,
            allowTaskSnapshot, activityCreated, snapshot);

    //逐渐的,这里似乎更多的是和 window 窗口相关(麻了麻了,我只想看 activity 相关,细节太难了)
    if (type == STARTING_WINDOW_TYPE_SNAPSHOT) {
        if (isActivityTypeHome()) {
            mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
            if ((mDisplayContent.mAppTransition.getTransitFlags()
                    & WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0) {
                return false;
            }
        }
        
        //【分支一】
        return createSnapshot(snapshot, typeParameter);
    }


    ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SplashScreenStartingData");
    mStartingData = new SplashScreenStartingData(mWmService, pkg,
            resolvedTheme, compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
            getMergedOverrideConfiguration(), typeParameter);
            
    //【分支二】
    scheduleAddStartingWindow();
    return true;
}

上述无论是分支一、分支二,都会走到同一个方法scheduleAddStartingWindow。

scheduleAddStartingWindow

//ActivityRecord.java
void scheduleAddStartingWindow() {
    if (StartingSurfaceController.DEBUG_ENABLE_SHELL_DRAWER) {
        mAddStartingWindow.run();
    } else {
        //把消息添加到队列最前面优先处理?
        if (!mWmService.mAnimationHandler.hasCallbacks(mAddStartingWindow)) {
            //mWmService:WindowManagerService
            //mAnimationHandler:final Handler mAnimationHandler = new Handler(AnimationThread.getHandler().getLooper());
            mWmService.mAnimationHandler.postAtFrontOfQueue(mAddStartingWindow);
        }
    }
}

这里的事情和 window 窗口关系密切,surface 看着绘制相关。

//ActivityRecord.java

private final AddStartingWindow mAddStartingWindow = new AddStartingWindow();

private class AddStartingWindow implements Runnable {

    @Override
    public void run() {
        //略略略,看不出它干了啥
    }
}

窗口相关的到此为止吧,再进入看不懂了。我更关注的是 activity 声明周期回调,可迟迟没有看见💔


那么这我们姑且他成功地把 activity 添加到 window 上,现在是时候回头看看启动成功后做了些什么? 所以我们回到 ActivityStarter.java,自然还是回到这里 (从哪来,回哪去吧)

The callback [onNewIntent]

这里讲 Activity onNewIntent(Intent intent) 生命周期回调,其实像 onCreat、onResume 等也是相似的,其他的不重复。

deliverToCurrentTopIfNeeded

//ActivityStarter.java
int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
        IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
        int startFlags, boolean doResume, ActivityOptions options, Task inTask,
        TaskFragment inTaskFragment, boolean restrictedBgActivity,
        NeededUriGrants intentGrants) {
        

    //如果正在启动的 activity 和任务栈顶部的 activity 相同
    final Task topRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
    if (topRootTask != null) {
        //顶部是同一个 activity,无需重复创建,启动一次就可以,也就是我们知道的应该回调 onNewIntent
        startResult = deliverToCurrentTopIfNeeded(topRootTask, intentGrants);
        if (startResult != START_SUCCESS) {
            return startResult;
        }
    }       
}

deliverToCurrentTopIfNeeded

private int deliverToCurrentTopIfNeeded(Task topRootTask, NeededUriGrants intentGrants) {
    //获取当前栈顶 activity
    final ActivityRecord top = topRootTask.topRunningNonDelayedActivityLocked(mNotTop);
    
    //activity 相同、启动用户相同、栈顶复用、启动目标
    final boolean dontStart = top != null
            && top.mActivityComponent.equals(mStartActivity.mActivityComponent)
            && top.mUserId == mStartActivity.mUserId
            && top.attachedToProcess()
            && ((mLaunchFlags & FLAG_ACTIVITY_SINGLE_TOP) != 0
            || LAUNCH_SINGLE_TOP == mLaunchMode)
            && (!top.isActivityTypeHome() || top.getDisplayArea() == mPreferredTaskDisplayArea);
    if (!dontStart) {
        return START_SUCCESS;
    }

    top.getTaskFragment().clearLastPausedActivity();
    //activity 显示,后面看
    if (mDoResume) {
        mRootWindowContainer.resumeFocusedTasksTopActivities();
    }
    
    //这里会进入 ActivitRecord
    deliverNewIntent(top, intentGrants);
    return START_DELIVERED_TO_TOP;
}

deliverNewIntentLocked

//ActivityRecord.java
final void deliverNewIntentLocked(int callingUid, Intent intent, NeededUriGrants intentGrants,

    if ((mState == RESUMED || mState == PAUSED || isTopActivityWhileSleeping)
            && attachedToProcess()) {
        try {
            ArrayList<ReferrerIntent> ar = new ArrayList<>(1);
            ar.add(rintent);
            //开始调用声明周期相关,通过发送一个客户端事务 ClientTransaction
            //getLifecycleManager -> ClientLifecycleManager 声明周期相关回调都会通过它
            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
                    NewIntentItem.obtain(ar, mState == RESUMED));
            unsent = false;
        } catch (RemoteException e) {
            Slog.w(TAG, "Exception thrown sending new intent to " + this, e);
        } catch (NullPointerException e) {
            Slog.w(TAG, "Exception thrown sending new intent to " + this, e);
        }
    }
    
    if (unsent) {
        addNewIntentLocked(rintent);
    }
}
//ClientLfecycleManager.java
void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
    //创建一个事物之后开始执行,关键是这个是事务(binder)事务传递数据的,或则事务发送后将在哪里处理事务???
    final IApplicationThread client = transaction.getClient();
    transaction.schedule();
    if (!(client instanceof Binder)) {
        transaction.recycle();
    }
}

ClientTransaction.schedule

ClientTransaction: A container that holds a sequence of messages, which may be sent to a client. This includes a list of callbacks and a final lifecycle state.

//ClientTransaction.java

/*
    1、IApplicationThread 这是一个标准的 aidl 接口,接口实现自然是 IApplicationThread.Sub
    2、实现类在 ActivityThread,private class ApplicationThread extends IApplicationThread.Stub 
*/
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);
}

ApplicationThread.scheduleTransaction

//ActivityThread.java
private class ApplicationThread extends IApplicationThread.Stub{

    @Override
    public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
        //在当前类文件搜索没看到方法定义,差点怀疑人生;然后看看 ActivitThread 还有父类,那方法定义就在父类了
        ActivityThread.this.scheduleTransaction(transaction);
    }
}
public final class ActivityThread extends ClientTransactionHandler
        implements ActivityThreadInternal {  
}
public abstract class ClientTransactionHandler {

    void scheduleTransaction(ClientTransaction transaction) {
        transaction.preExecute(this);
        //发送消息,那者就明确很多了
        sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
    }
//ActivityThread.java
class H extends Handler {
    
    public static final int EXECUTE_TRANSACTION = 159;

    public void handleMessage(Message msg) {
        switch (msg.what) {
            case EXECUTE_TRANSACTION:
                //回头看看 schedule 的注释,下一步应该会到哪里去执行,其实别人是写得很清楚的(熟能生巧,初看确实一头雾水)
                final ClientTransaction transaction = (ClientTransaction) msg.obj;
                mTransactionExecutor.execute(transaction);
                if (isSystem()) {
                    transaction.recycle();
                }
            break;
        }
     }
}

TransactionExecutor.excute

//TransactionExecutor.java
public void execute(ClientTransaction transaction) {
    if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Start resolving transaction");

    //似乎每一个 activity 都有一个 token,还不清楚从何而来
    //初学 Android 时,关于 activity token 的报错估计你也遇到过
    final IBinder token = transaction.getActivityToken();
    if (token != null) {
        final Map<IBinder, ClientTransactionItem> activitiesToBeDestroyed =
                mTransactionHandler.getActivitiesToBeDestroyed();
        final ClientTransactionItem destroyItem = activitiesToBeDestroyed.get(token);
        if (destroyItem != null) {
            if (transaction.getLifecycleStateRequest() == destroyItem) {
                activitiesToBeDestroyed.remove(token);
            }
            if (mTransactionHandler.getActivityClient(token) == null) {
                return;
            }
        }
    }
    
    //这个暂不关注,重点看看下面的生命周期回调吧
    executeCallbacks(transaction);
    
    /*
        1、cycleToPath(r, lifecycleItem.getTargetState(), true , transaction);  -> performLifecycleSequence
        
        //看了一圈,着两个实现应该是在子类
        2、lifecycleItem.execute(mTransactionHandler, token, mPendingActions);
        3、lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions);
    */
    executeLifecycleState(transaction);
    mPendingActions.clear();
    if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "End resolving transaction");
}
//TransactionExecutor.java
private void performLifecycleSequence(ActivityClientRecord r, IntArray path,
        ClientTransaction transaction) {
    final int size = path.size();
    for (int i = 0, state; i < size; i++) {
        state = path.get(i);
        
        //是吧,几个生命周期的回调都有;那就奇怪了怎么没有 onNewIntent,他不算是生命周期函数吗?
        //咚咚咚咚(敲黑板~)
        /*
            1、onNewIntent 不是生命周期回调方法,只是声明周期回调过程中可能被执行的一个方法
            2、如果满足某些条件,根据经验我们知道这个方法在 onResume 之前会执行
            3、那么我们猜测(我看了代码再来猜测的😏),该方法的回调有没有可能在 handleResumeActivity 里面执行?那就去看看吧!
        */
        switch (state) {
            case ON_CREATE:
                mTransactionHandler.handleLaunchActivity(r, mPendingActions,
                        null /* customIntent */);
                break;
            case ON_START:
                mTransactionHandler.handleStartActivity(r, mPendingActions,
                        null /* activityOptions */);
                break;
            case ON_RESUME:
                mTransactionHandler.handleResumeActivity(r, false /* finalStateRequest */,
                        r.isForward, "LIFECYCLER_RESUME_ACTIVITY");
                break;
            case ON_PAUSE:
                mTransactionHandler.handlePauseActivity(r, false /* finished */,
                        false /* userLeaving */, 0 /* configChanges */, mPendingActions,
                        "LIFECYCLER_PAUSE_ACTIVITY");
                break;
            case ON_STOP:
                mTransactionHandler.handleStopActivity(r, 0 /* configChanges */,
                        mPendingActions, false /* finalStateRequest */,
                        "LIFECYCLER_STOP_ACTIVITY");
                break;
            case ON_DESTROY:
                mTransactionHandler.handleDestroyActivity(r, false /* finishing */,
                        0 /* configChanges */, false /* getNonConfigInstance */,
                        "performLifecycleSequence. cycling to:" + path.get(size - 1));
                break;
            case ON_RESTART:
                mTransactionHandler.performRestartActivity(r, false /* start */);
                break;
            default:
                throw new IllegalArgumentException("Unexpected lifecycle state: " + state);
        }
    }
}

ClientTransactionHandler 是个抽象方法,所以声明周期回调还得找 ClientTransactionHandler 的实现类 ActivitThread。

绕一圈又回来,这这这~~~

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmoogVEI-1661130959026)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/69c055d008d74b45bd1205352739f8b9~tplv-k3u1fbpfcp-watermark.image?)]

ActivitThread.performResumeActivity

//ActivityThread.java
public boolean performResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        String reason) {

    if (r.activity.mFinished) {
        return false;
    }
    
    //已经 resumed 就不重复了
    if (r.getLifecycleState() == ON_RESUME) {
        if (!finalStateRequest) {
            final RuntimeException e = new IllegalStateException(
                    "Trying to resume activity which is already resumed");
        }
        return false;
    }
    
    if (finalStateRequest) {
        r.hideForNow = false;
        r.activity.mStartedActivity = false;
    }
    
    try {
        r.activity.onStateNotSaved();
        r.activity.mFragments.noteStateNotSaved();
        checkAndBlockForNetworkAccess();
        //看到没,满足某些条件情况下是会走 onNewIntent 
        if (r.pendingIntents != null) {
            deliverNewIntents(r, r.pendingIntents);
            r.pendingIntents = null;
        }
        
        if (r.pendingResults != null) {
            deliverResults(r, r.pendingResults, reason);
            r.pendingResults = null;
        }
        
        //onResume 回调在这里,本次我们暂不关注
        r.activity.performResume(r.startsNotResumed, reason);

        //把状态设置一下
        r.state = null;
        r.persistentState = null;
        r.setState(ON_RESUME);

        reportTopResumedActivityChanged(r, r.isTopResumedActivity, "topWhenResuming");
    } catch (Exception e) {
        if (!mInstrumentation.onException(r.activity, e)) {
            throw new RuntimeException("Unable to resume activity "
                    + r.intent.getComponent().toShortString() + ": " + e.toString(), e);
        }
    }
    return true;
}
//ActivityThread.java
private void deliverNewIntents(ActivityClientRecord r, List<ReferrerIntent> intents) {
    final int N = intents.size();
    for (int i=0; i<N; i++) {
        ReferrerIntent intent = intents.get(i);
        intent.setExtrasClassLoader(r.activity.getClassLoader());
        intent.prepareToEnterProcess(isProtectedComponent(r.activityInfo),
                r.activity.getAttributionSource());
        r.activity.mFragments.noteStateNotSaved();
        //最后还是交给 ‘大管家’ 执行啊,回到了那个熟悉的对象
        mInstrumentation.callActivityOnNewIntent(r.activity, intent);
    }
}

Instrumentation.callActivityOnNewIntent

//Instrumentation.java
public void callActivityOnNewIntent(Activity activity, ReferrerIntent intent) {
    final String oldReferrer = activity.mReferrer;
    try {
        if (intent != null) {
            activity.mReferrer = intent.mReferrer;
        }
        callActivityOnNewIntent(activity, intent != null ? new Intent(intent) : null);
    } finally {
        activity.mReferrer = oldReferrer;
    }
}
//Instrumentation.java
public void callActivityOnNewIntent(Activity activity, Intent intent) {
    //什么?Instrumentation 不熟悉?
    //那 activity 总该熟悉了吧
    activity.performNewIntent(intent);
}
//Activity.java
final void performNewIntent(@NonNull Intent intent) {
    mCanEnterPictureInPicture = true;
    onNewIntent(intent);
}
//Activity.java
protected void onNewIntent(Intent intent) {
}

就到这里结束吧,看起来没那么像样。虽然是 桌面启动,后来越感觉像是 Activity 启动。都乱套了

可是,桌面不也是一个 activity 吗🤔️

在这里插入图片描述

就到这吧,桌面启动顺带 Activity 启动(虽然 Activity 启动事实上还是有区别的)

Android 视图过渡控制器使用指南 通过 Clouda API 的视图过渡控制器,你可以轻松实现各种视图过渡效果,为你的 Android 应用增添动态和交互性。按照上述步骤,你可以集成和使用 Clouda API 的视图过渡控制器,并根据自己的需要定义和执行过渡效果。在你的 Android 项目中,创建一个 ViewTransitionController 对象,用于管理视图过渡效果的展示和隐藏。一旦你定义了过渡效果,你可以将它们添加到 ViewTransitionController 中,并指定要应用过渡效果的视图。 阅读详情

相关推荐

Activity启动完整流程分析-基于Android14源码

本文分析了Android 14中Activity启动的完整流程,重点剖析了应用进程与系统服务(AMS/ATMS)之间的交互机制。主要内容包括: 整体架构概览: 应用进程侧(ActivityThread、ApplicationThread等)与系统服务侧(AMS/ATMS)的组件分工 通过Binder IPC和LocalServices两种通信方式实现跨进程和进程内交互 详细调用链路: 应用进程启动入口ActivityThread.main() 向AMS注册的attach流程 AMS处理应用注册的核心逻辑 涉

保持专注,保持热爱 1622

Android 12系统源码_窗口动画(二)应用程序间的切换动画

结合Android12系统源码梳理不同屏幕设备对象间的切换动画的动画样式来源。

AfinalStone的专栏 2016

保姆级教程:用Cadence Sigrity Power DC搞定HI3516A板卡电源压降仿真(含常见报错解决)

本文提供了一份详细的Cadence Sigrity Power DC直流压降仿真教程,特别针对HI3516A板卡设计。从环境准备、器件建模到仿真执行和结果解读,全面覆盖了电源完整性分析的关键步骤,并提供了常见报错的解决方案,帮助工程师快速掌握直流压降分析技术。

weixin_42569329的博客 322

Android 12 S WindowManager Transition动画介绍

如果活动在同一任务中启动新活动,则现有活动的活动记录属于打开的应用程序,同时,新活动的另一个活动记录属于关闭的应用程序。//有些情况下,我们打开/关闭一个新的task/activity,但在现实中,只有一个半透明的活动在现有activities的顶部打开/关闭。//在以下情况下允许应用状态更新和动画:activity 正在转换可见性状态,或者activity 在我们有机会播放过渡动画之前被标记为隐藏并且正在退出,或者这是一个打开的应用程序并且窗口正在被替换(例如,自由格式窗口到普通窗口)。

weixin_41028555的博客 2545

开发者体验度量:构建DevEx评估体系的方法论与工具

在深入讨论度量方法前,我们需要明确什么是开发者体验(DevEx)。许多人将其简化为"开发工具好不好用",这是一个危险的误解。开发者体验是指开发者在完成工作过程中的整体感受和效率,它由三大核心维度组成:工具与基础设施:开发、测试、部署工具链的效能流程与协作:工作流程的顺畅度和团队协作的效率文化与成长:支持实验、学习和专业发展的环境这三个维度相互作用,共同决定了开发者能否高效且愉悦地完成工作。开发者体验不是一个目的地,而是一段持续的旅程。

2401_88760782的博客 1429

金蝶云星空应用开发初级考试题目汇总

摘要:本文为金蝶云星空应用开发初级考试题目汇总

2401_86113757的博客 3799

【Android14 ShellTransitions】(四)Transition收集动画参与者

这一节的内容在WMCore中,现在Transition已经走到COLLECTING状态了,并且可以收集动画参与者了。那么Transition是在什么时候去收集动画参与者?回到我们最初的ActivityStarter.startActivityUnchecked:在调用了TransitionController.createAndStartCollecting后,紧接着就是调用了TransitionController.collect。

ukynho的博客 2708

低成本实现webhook接收端[python]

Webook本质上也是API,只不过是反向调用,即前端不主动发送请求,完全由后端推送。简单来说,Webhook是一个接受HTTP POST或是GET,PUT,DELETE的URL,一个实现了Webhook的API提供商就是在当事件发生的时候会向配置的URL发送信息,与请求-响应式不同,使用Webhook可以实时接收到变化。Ps:Webhook可能会发出大量的请求,确保接受的URL不被泄露或者应用能及时处理这些请求。

锡城小凯的博客 1万+

【Android14 ShellTransitions】(三)Transition移动为COLLECTING状态

SyncGroup的相关内容我们在以前WindowContainerTransaction系列文章中那篇关于BLASTSyncEngine里其实已经比较细致的讲过了,不过这里还是再看下。

ukynho的博客 1873

无法找到来自源 nvlddmkm 的事件 ID 14 的描述

具体错误是: 无法找到来自源 nvlddmkm 的事件 ID 14 的描述。本地计算机上未安装引发此事件的组件,或者安装已损坏。可以安装或修复本地计算机上的组件。 出现这样错误的原因有很多了,有可能是硬件有可能是软件。由于我是刚配置的电脑,起初非常怀疑是自己组装的问题,折腾了好久,心态爆炸!!!先说我报这样错误的情况,以及解决办法,供后面人参考。 ...

SeventhBlue 6万+

长上下文技术的底层逻辑:Attention、压缩、缓存与跨页推理

如果把模型的参数视为“固化智能”,动态智能(Dynamic Intelligence)——模型不必依赖训练时的记忆,而是可以随时调用外部信息。这一点正在改变大模型的范式:Prompt 不再是输入,而是“操作系统事件”文档不再是知识,而是“模型的临时记忆”模型不再是一段神经网络,而是一个“具备阅读循环的推理引擎”当我们把上下文从 128K 拉到百万级,我们做的不是扩容,而是让模型第一次拥有阅读、回溯、定位、跨页分析的能力。这是一种新的智能。

aifs2025的博客 997

Android 源码 <Activity> 桌面启动一 [5]

Read The Fucking Source Code. —— Linus 站在'巨人'的肩膀上开始自己的旅途。—— 佚名 愉快的周末,从打开💻开始,到骑行归来结束。—— 佚名

Primer5 1244

IDEA出现闪退或打不开的解决方法

本身项目比较大,为了打开这个软件,可调节IDEA中安装的bin目录下有个。打开IDEA的时候过一会便闪退,可以再IDEA的右下角看到如下提示。(如果没有该提示,软件右下角也会有个红色感叹号,点开查看原因即可)闪退的多数原因大致是out of memory,内存溢出。查看任务管理器中的进程:(发现内存随时有溢出的情况)但也需要注意,该参数并不是越大越好,适中即可。只有个别原因是需要管理员权限就可执行!可以尽量减少软件的启动。

码农研究僧的博客 5943

Android Activity启动流程一:从Intent到Activity创建

Activity 是 android 四大组件之一,很有必要知道它的启动过程,我们在上一篇文章中介绍了 APP的启动流程,里面大概讲到了Activity的 启动流程,在本文中,我们系统的再总结一下,当成一个记录。

fumeidonga的博客 6378

TRAE智能体开发:TRAE概述

TRAE(/treɪ/)深度融合 AI 能力,是一名能够理解需求、调用工具并独立完成各类开发任务的“AI 开发工程师”,帮助你高效推进每一个项目。

王国平的博客 1261

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Android Activity启动过程详解

ctivity启动的流程分为两部分:一是在activity中通过startActivity(Intent intent)方法启动一个Activity;二是我们在桌面通过点击应用图标启动一个App然后显示Activity;第二种方式相较于第一种方式更加全面和复杂,所以本文会以第二种流程来分析。应用的启动流程大致分为下面五个阶段:1,应用启动进程 Launcher 调用 ATMS 系统进程的过程2,ATMS 向 AMS 发送创建应用进程的过程。

allen_xu_2012_new的博客 1万+

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

从Android 8.0源码的角度剖析APP的启动过程

文章目录1. 应用进程启动过程1.1 AMS向Zygote进程发起请求1.2 Zygote进程接受请求2. Activity启动过程2.1 应用程序的Appication启动过程2.2 应用程序的根Activity启动过程 在从Android 8.0源码的角度剖析Android系统启动过程(2)一文中,我们了解到当ActivitiyManagerService服务被启动后,就会通过Intent以...

老蒋也疯狂 7705

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Android10.0系统启动之Launcher(桌面)启动流程-[Android取经之路]

摘要:上一节我们讲完了Android10.0的ActivityManagerService的启动流程,在AMS的最后启动了Launcher进程,今天我们就来看看Launcher的真正启动流程。 阅读本文大约需要花费50分钟。 文章的内容主要还是从源码进行分析,虽然又臭又长,但是如果想要学习Android系统源码,这是必要走的路,没有捷径。 相对于碎片学习,我更倾向于静下心来花费1个小...

IngresGe 的专栏 1万+

Android | Activity 启动流程分析

Activity类是 android 应用的关键组件,在日常开发中,绝对少不了组件。既然用了这么久,你知道他的启动流程🐴?作为一个应用层开发者,大多数人可能觉得学习这些对日常开发可能没有太大帮助。但是多了解一下 framework 的代码还是很有必要的,了解系统组件机制,对于一些问题我们也能快速的定位找到问题的所在点,并且在面试的时候也是一个加分项。本文基于 Android 12 版本源码,从作为切入点,对整个启动流程进行分析。

Android_XG的博客 1900

Android Application创建到Activity启动(launcher启动和startActivity启动)

文章目录点击桌面应用程序创建application和activity流程点击 startActivity() 时的调用流程问题: 为什么启动时会先调用栈顶activity的onPause() 点击桌面应用程序创建application和activity流程 首先从APP程序入口启动 main 函数开始。看下 ActivityThread 的 main 方法。 main方法中做了一些 Loop 的初始化,Application绑定流程的调用等,具体看代码:代码中有备注,省略了部分代码 public

Lud的博客 6028

死磕Android_App 启动过程(含 Activity 启动过程)

文章目录1. 前言1.1 简单介绍一下主要的类2. 应用的启动过程2.1 Launcher简介2.2 启动应用源码分析3. 启动Activity3.1 启动进程3.2 启动主线程3.3 创建Application3.4 创建Activity参考 1. 前言 Activity是日常开发中最常用的组件,系统给我们做了很多很多的封装,让我们平时用起来特别简单,很顺畅.但是你有没有想过,系统内部是如何启动...

潇风寒月 3181

android源码学习- APP启动流程(android12源码)

百度一搜能找到很多讲APP启动流程的,但是往往要么就是太老旧(还是基于android6去分析的),要么就是不全(往往只讲了整个流程的一小部分)。所以我结合网上现有的文章,以及源码的阅读和调试,力求写出一篇最完整,最详细,最通俗易懂的文章,来讲清楚在android上,APP是如何启动的。.........

分享+记录 9169

Android framework 源码分析一Activity启动流程(android 8.0)

Android framework 源码分析之Activity启动流程android 8.0概述Activity的启动流程 概述 Android系统对四大组件的工作过程进行了很大程度的封装,方便开发者使用,Activity作为Android中四大组件之一,直接跟用户进行交互,对于用户来说activity就是应用的一切,系统对其工作流程当然也做了很多封装,本篇文章将介绍Activity的启动流程,在...

马彦虎的博客 7089

深度详解 Android R(11.0)Activity 启动过程

文章目录前言一、Launcher进程请求ATMS1. Launcher 桌面的 App 图标入口2. Launcher ## startActivitySafely() 方法二、ATMS发送创建应用进程请求三、Zygote进程接受请求并孵化应用进程四、应用进程启动ActivityThread1.引入库2.读入数据总结 前言 面试中经常会被问到,让面试者简述一下 Activity 的启动流程,这个问题是考察面试者对源码的理解程度,要征服面试官获得满意的 offer 还是需要一步步深入去探索一下源码,梳理启动

neuHenry 6478

Activity启动流程解析-基于安卓8.0系统源码

    阅读源码是每个安卓工程师必经的过程,本文介绍的是Activity的启动流程。从问题入手,找到源码的主要线索,不会纠结在细节处,等到需要具体分析Activity其他特性和功能时,可以快速定位,再仔细分析。这篇文章基于安卓8.0源码,8.0版本Activity启动流程的代码跟之前版本有所修改,但可认为是谷歌工程师对代码进行重构,实际核心代码和思路是一致的。    下面是Activity启动的代...

yao_zhanlie的博客 4918

Android Framework源码——Activity启动流程

Android Framework源码——Activity启动流程

Gaga246的博客 3072
上一篇: Android 源码 <Activity> 桌面启动一 [5]
下一篇: 拖动安装包直接安装到手机
Primer@.com
博客等级 码龄9年 524粉丝 292原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值