重温四大组件(三)—Activity的启动过程

前言

重温四大组件第四篇,这里分析一下Activity的启动过程。Activity的启动过程分为两种,一种是根Activity的启动,另外一种是普通Activity的启动过程。根Activity也就是默认启动的Activity(在AndroidMinifest.xml配置的启动Activity)。普通Activity指的是除根Activity的其他Activity。其中根Activity和普通Activity的启动过程略有区别,根Activity的启动过程涉及到了应用程序进程的启动过程。

接下来从根Activity的角度启动来说。

以下分析的代码基于Android 9.0

Activity启动过程中的数据结构

我们在看一些技术博客的时候经常会看到一些分析AMS的文章。AMS就是ActivityManagerService,顾名思义就是Activity的管理服务,但它不仅仅管理Activity,其他三个组件的启动也经由AMS。同时,在看Activity的启动过程之前,我们应该了解一些Binder的知识。Binder是Android系统中一种跨进程通信技术。在Activity的启动过程中就涉及到了Binder通信的过程。

Activity的启动过程,可以概括为Launcher到AMS(经过Binder通信),从AMS到ApplicationThread(通过Binder通信)。

以上介绍了AMS在Activity的启动中承担的作用。接下来,我们再介绍一些在Activity启动过程中的一些其他角色。

ActivityStack

从ActivityStack的命名可以看出这个是Activity栈相关的。它的作用是管理和记录一个Activity栈的Activity。接下来我们看下ActivityStack中有哪些属性。

名称类型说明
mServiceActivityManagerService目前AMS的引用
mRecentTasksRecentTasks记录一个最近使用的Activity列表
mPausingActivityActivityRecord目前停止状态的Activity信息
mResumedActivityActivityRecord目前resume状态的Activity信息

ActivityRecord

ActivityRecord是用来描述一个Activity的数据结构,它记录了Activity的所有信息。

名称类型说明
serviceActivityManagerService目前AMS的引用
infoActivityInfo主要记录了Activity在Manifest文件中的配置信息
launchedFromPidint启动Activity进程的pid
launchedFromUidint启动Activity进程的uid
taskAffinityStringActivity启动后所在的task
taskTaskRecord记录了所在task的信息
appProcessRecord记录了Activity启动所在进程的信息
stateActivityState当前Activity的状态
themeintActivity的主题

TaskRecord

TaskRecord是关于任务栈的描述。

名称类型说明
taskIdfinal int任务栈的唯一标识
affinityString任务栈的名称,也就是taskAffinity配置的名称
intentIntent启动这个任务栈的Intent
mActivitiesArrayList按照在任务栈中历史顺序排序的Activity
mStackActivityStack任务栈所在的Activity栈
mServiceActivityManagerService目前AMS的引用

ProcessRecord

ProcessRecord是关于应用进程的描述。

名称类型说明
uidint进程的uid
processNameString进程名称
infoApplicationInfo记录应用程序信息

在上面介绍了Activity启动过程中关于Activity启动过程中的一些信息记录。比如:Activity栈信息、任务栈信息以及进程信息等。

Activity的启动工程

Activity的启动过程是一个很复杂的过程,接下来的分析过程并不会把所有细节都分析到位。我们分析Activity的启动过程主要是为了学习Android系统对Activity组件的管理过程以及对应用进程的调度。

从Launcher到AMS

我们知道在Android系统中锁呈现的桌面就是一个应用程序。当用户点击桌面上的应用图标是就可以启动相应的应用程序。应用程序由Launcher启动,调用startActivitySafely()方法。

/**Launcher.java**/
 boolean startActivitySafely(Intent intent, Object tag) {
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        try {
            startActivity(intent);
            return true;
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
        } catch (SecurityException e) {
            Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
        }
        return false;
    }
复制代码

可以看到在startActivitySafely()中继续调用startActivity()方法启动Activity,并且在Intent中设置了flag为FLAG_ACTIVITY_NEW_TASK表示Activity将在一个新的任务栈中启动。这个方法就是在Activity中的。

/**Activity.java**/
public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }
复制代码

这里继续调用了startActivityForResult(),并且requestCode为-1表示Launcher不需要知道Activity的结果。

/**Activity.java**/
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            //继续启动Activity
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                mStartedActivity = true;
            }
            cancelInputsAndStartExitTransition(options);
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }
复制代码

在这里先判断mParent是否为null,然后根据结果选择走那一步。如果跟代码的话,可以看到mParent在attach()方法中或者setParent()方法中赋值,在根Activity的情况中mParent为null。所以继续调用 mInstrumentation.execStartActivity()。

/**Instrumentation.java**/
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, String target,
        Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        //......
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            //通过Binder接口获取AMS对象
            int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target, requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }
复制代码

在上面的方法中继续启动Activity,在这里我们可以看到通过ActivityManager.getService()我们就获取到了AMS对象。接下来,我们看下AMS的获取过程。

/**ActivityManager.java**/
public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }
private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };
复制代码

在getService()方法中通过IActivityManagerSingleton获取了AMS的对象。在创建IActivityManagerSingleton的方法中我们可以看到返回的是IActivityManager,IActivityManager就是使用了AIDL的方式生成的。

在AMS中处理Activity的启动

从这里开始,Activity的启动就开始在AMS中进行了。从上面的代码可以知道这个过程是通过Binder通信方式进行的。

/**ActivityManagerService.java**/
public final int startActivity(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) {
        return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
                resultWho, requestCode, startFlags, profilerInfo, bOptions,
                UserHandle.getCallingUserId());
    }
复制代码

在ActivityManagerService中通过startActivity()方法继续进行Activity的启动。接下来我们直接看最终的方法回调。

/**ActivityManagerService.java**/
public final int startActivityAsUser(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, int userId,
            boolean validateIncomingUser) {
        //......
        //从obtainStarter()中获取了ActivityStarter对象
        return mActivityStartController.obtainStarter(intent, "startActivityAsUser")
                .setCaller(caller)
                .setCallingPackage(callingPackage)
                .setResolvedType(resolvedType)
                .setResultTo(resultTo)
                .setResultWho(resultWho)
                .setRequestCode(requestCode)
                .setStartFlags(startFlags)
                .setProfilerInfo(profilerInfo)
                .setActivityOptions(bOptions)
                .setMayWait(userId) //设置mayWait标志
                .execute();

    }
复制代码

在上面的方法中,AMS通过startActivityAsUser()方法最终把Activity的启动过程转移到了ActivityStarter中。我们直接看下ActivityStarter的execute()方法。

/**ActivityStarter**/
int execute() {
        try {
            if (mRequest.mayWait) {
                return startActivityMayWait(mRequest.caller, mRequest.callingUid,
                       //....//);
            } else {
                return startActivity(mRequest.caller, mRequest.intent, //....//);
            }
        } finally {
            onExecutionComplete();
        }
    }
复制代码

这里根据mRequest.mayWait(表示我们应该等待启动请求的结果。)中的标志来判断走哪个逻辑分支。在上面的代码中调用setMayWait(userId),这里将mayWait设置为true。那么接下来我们继续看startActivityMayWait()方法。

private int startActivityMayWait(//....//) {
        //......
        // Collect information about the target of the Intent.
        ActivityInfo aInfo = mSupervisor.resolveActivity(intent, rInfo, startFlags, profilerInfo); //构建Activity的信息

        synchronized (mService) {
            final ActivityStack stack = mSupervisor.mFocusedStack; //获取Activity栈的信息
            stack.mConfigWillChange = globalConfig != null
                    && mService.getGlobalConfiguration().diff(globalConfig) != 0;
            //......
            final ActivityRecord[] outRecord = new ActivityRecord[1]; //获取Activity的信息
            //继续启动Activity
            int res = startActivity(caller, intent, ephemeralIntent, resolvedType, aInfo, rInfo,
                    voiceSession, voiceInteractor, resultTo, resultWho, requestCode, callingPid,
                    callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, options,
                    ignoreTargetSecurity, componentSpecified, outRecord, inTask, reason,
                    allowPendingRemoteAnimationRegistryLookup);

            Binder.restoreCallingIdentity(origId);
            //......
            return res;
        }
    }
复制代码

我们继续跟代码,接下会调用

private int startActivity(//....//) {
        //......
        ProcessRecord callerApp = null; //获取进程信息
        if (caller != null) {
            //caller是ActivityThread中的Binder接口,用过应用程序进程则不为null
            callerApp = mService.getRecordForAppLocked(caller); //获取进程信息
            if (callerApp != null) {
                callingPid = callerApp.pid;
                callingUid = callerApp.info.uid;
            } 
        }
        //......
        //创建启动Activity的信息类
        ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
                callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
                resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
                mSupervisor, checkedOptions, sourceRecord);
        if (outActivity != null) {
            outActivity[0] = r;
        }
        final ActivityStack stack = mSupervisor.mFocusedStack; //获取有焦点的Activity栈
        //......
        //继续启动Activity
        return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
                true /* doResume */, checkedOptions, inTask, outActivity);
    }
复制代码

在上面会看到根据IApplicationThread获取应用进程信息,这里如果应用进程没有被创建的时候,IApplicationThread是null,然后接下来的步骤会创建应用程序进程,这里不再详细说。接下来继续进行Activity启动的分析。

private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
            ActivityRecord[] outActivity) {
        //......
        int result = START_SUCCESS;
            if (mDoResume) {
            final ActivityRecord topTaskActivity =
                    mStartActivity.getTask().topRunningActivityLocked();
            if (!mTargetStack.isFocusable()
                    || (topTaskActivity != null && topTaskActivity.mTaskOverlay
                mTargetStack.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
                mService.mWindowManager.executeAppTransition();
            } else {
                if (mTargetStack.isFocusable() && !mSupervisor.isFocusedStack(mTargetStack)) {
                    mTargetStack.moveToFront("startActivityUnchecked");
                }
                //这里继续启动Activity
                mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity,
                        mOptions);
            }
        }
        return START_SUCCESS;
    }
复制代码

接下来启动Activity的过程转到ActivityStackSupervisor中。

/**ActivityStackSupervisor**/
boolean resumeFocusedStackTopActivityLocked(
            ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) {
        //这里从Activity栈中启动
        if (targetStack != null && isFocusedStack(targetStack)) {
            return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
        }
        //......
        return false;
    }
复制代码

上面的代码中继续启动Activity,这次启动过程转到ActivityStack中。

private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
        //......省略代码
      
       boolean pausing = mStackSupervisor.pauseBackStacks(userLeaving, next, false);
        if (mResumedActivity != null) {
            //这里如果mResumedActivity不为null,就停止mResumedActivity
            if (DEBUG_STATES) Slog.d(TAG_STATES,
                    "resumeTopActivityLocked: Pausing " + mResumedActivity);
            pausing |= startPausingLocked(userLeaving, false, next, false);
        }
        //next是ActivityRecord对象,next.app是ProcessRecord对象,next.app.thread是IApplicationThread
        if (next.app != null && next.app.thread != null) {
            //如果都不为null,说明应用进程已经被创建
           //......
            synchronized(mWindowManager.getWindowManagerLock()) {
                try {
                    mStackSupervisor.startSpecificActivityLocked(next, true, false);
                    return true;
                }
            }
        } else {
            应用进程没有被创建
            mStackSupervisor.startSpecificActivityLocked(next, true, true);
        }
        return true;
    }
复制代码

在上面的代码中我们可以看到有一个判断逻辑if (next.app != null && next.app.thread != null),这个是判断应用程序进程是否被创建,从代码中可以看到,无论应用进程是否被创建,都会调用startSpecificActivityLocked()方法。这里Activity的启动又转移到了ActivityStackSupervisor中。

除此之外,还有一个步骤是将mResumedActivity设置为停止状态,在我们查看Activity的生命周期时,一个Activity启动另外一个Activity的时候,在调用过Activity的pause生命周期函数时才会开始下一个Activity的生命周期。

/**ActivityStackSupervisor**/
void startSpecificActivityLocked(ActivityRecord r,
            boolean andResume, boolean checkConfig) {
        // Is this activity's application already running?
        //获取应用进程信息
        ProcessRecord app = mService.getProcessRecordLocked(r.processName,
                r.info.applicationInfo.uid, true);
        if (app != null && app.thread != null) {
            try {
                if ((r.info.flags&ActivityInfo.FLAG_MULTIPROCESS) == 0
                        || !"android".equals(r.info.packageName)) {
                    app.addPackage(r.info.packageName, r.info.applicationInfo.longVersionCode,
                            mService.mProcessStats);
                }
                //继续启动Activity
                realStartActivityLocked(r, app, andResume, checkConfig);
                return;
            } catch (RemoteException e) {
                
            }
        }
        //创建应用进程
        mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
                "activity", r.intent.getComponent(), false, false, true);
    }
复制代码

这里就看到了如果应用进程不存在就会通过AMS创建应用进程,这里不再展开分析。如果应用进程存在就继续启动Activity。调用了realStartActivityLocked方法。

/**ActivityStackSupervisor**/ 
final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
            boolean andResume, boolean checkConfig) throws RemoteException {
                //......
                // Create activity launch transaction.
                //Android 9.0的启动方式与其他版本的不同,这里换成了通过ClientTransaction的方式已启动。
                final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
                        r.appToken);
                clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
                        System.identityHashCode(r), r.info,
                        // TODO: Have this take the merged configuration instead of separate global
                        // and override configs.
                        mergedConfiguration.getGlobalConfiguration(),
                        mergedConfiguration.getOverrideConfiguration(), r.compat,
                        r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
                        r.persistentState, results, newIntents, mService.isNextTransitionForward(),
                        profilerInfo));

                // 这里用来处理Activity生命周期回调
                final ActivityLifecycleItem lifecycleItem;
                if (andResume) {
                    lifecycleItem = ResumeActivityItem.obtain(mService.isNextTransitionForward());
                } else {
                    lifecycleItem = PauseActivityItem.obtain();
                }
                clientTransaction.setLifecycleStateRequest(lifecycleItem);

                // Schedule transaction.
                mService.getLifecycleManager().scheduleTransaction(clientTransaction);
                ...

        return true;
    }
复制代码

在上面可以看到Android 9.0的启动方式与其他版本的不同,在Android 9.0里主要通过回调的方式启动Activity,最终会通过AMS中的ClientLifecycleManager.scheduleTransaction()方法调用ApplicationThread的scheduleTransaction(),最后的是执行的LaunchActivityItem的execute()方法。

/**TransactionExecutor**/
public void execute(ClientTransaction transaction) {
        final IBinder token = transaction.getActivityToken();
        //1.先执行CallBack
        executeCallbacks(transaction);
        //2.再执行Activity生命周期回调
        executeLifecycleState(transaction);
        mPendingActions.clear();
    }
复制代码
/**LaunchActivityItem**/
public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
        ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
                mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
                mPendingResults, mPendingNewIntents, mIsForward,
                mProfilerInfo, client);
        client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
        Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
    }
复制代码

可以看到这里调用了ClientTransactionHandler的handleLaunchActivity()方法。从这里Activity的启动过程就从AMS中的调用切换到了ActivityThread中去了。

从AMS到ActivityThread

上面说到,Activity的启动调用了ClientTransactionHandler的handleLaunchActivity()方法。我们可以发现ActivityThread是继承自ClientTransactionHandler的。在ClientTransactionHandler中handleLaunchActivity是抽象方法,我们直接在ActivityThread中看。

从这里开始将开始执行Activity的生命周期函数。

/**ActivityThread**/
public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        //......
        //初始化WindowManagerService
        WindowManagerGlobal.initialize();
        final Activity a = performLaunchActivity(r, customIntent);
        //......
        return a;
    }
复制代码

可以看到在这里继续调用了

/**ActivityThread**/
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
       //......
        Activity activity = null;
        try {
            //获取Activity实例
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            //......
        }

        try {
            //初始化Application
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

            if (activity != null) {
                //设置Context
                appContext.setOuterContext(activity);
                //调用Activity的attach()方法
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);

                if (r.isPersistable()) {
                    //调用onCreate()生命周期函数
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.mInstrumentation(activity, r.state);
                }
                //......
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme); //设置主题
                }
                //......
                r.activity = activity;
            }
            r.setState(ON_CREATE);
        }
        //......
        return activity;
    }
复制代码

在上面的代码可以看到执行完Activity的attach()方法后,继续执行Instrumentation的callActivityOnCreate()方法。

/**Instrumentation**/
public void callActivityOnCreate(Activity activity, Bundle icicle) {
        prePerformCreate(activity);
        activity.performCreate(icicle);
        postPerformCreate(activity);
    }
复制代码

这里可以看到,又执行了Activity的performCreate()方法。

/****/
final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        //....
        //调用onCreate()生命周期方法
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }
    }
复制代码

在上面的代码中调用了Activity的onCreate()生命周期方法。到这里,在上面讲到的TransactionExecutor的execute()方法中的回调已经执行完毕,接下来就是开始执行生命周期相关的回调。

我们在分析上面代码的时候知道,生命周期回调是一个ResumeActivityItem。

/**ResumeActivityItem**/ 
public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
        client.handleResumeActivity(token, true /* finalStateRequest */, mIsForward,
                "RESUME_ACTIVITY");
        Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
    }
复制代码

可以看到这里有调用了handleResumeActivity()方法。

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
      
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason); 
    }
复制代码
public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
            String reason) {
            //执行Activity的performResume
            r.activity.performResume(r.startsNotResumed, reason);

            r.state = null;
            r.persistentState = null;
            r.setState(ON_RESUME);
        } catch (Exception e) {
          
        }
        return r;
    }
复制代码

在上面的代码中调用了Activity的performResume()方法。进而调用Activity的onResume()方法。这时Activity就呈现到了界面。Activity也算启动完成。

总结

上面以根Activity的视角分析了Activity的启动过程。整个分析下来,感觉到Activity的启动还是十分复杂的,但是在也并不需要面面俱到,能够把握大体,流程理解Android在启动Activity的过程中是怎么调度的,这样就达到了我们的目的。

转载于:https://juejin.im/post/5d4eacb06fb9a06b017e3e7f

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值