根Activity工作过程

Activity的启动过程分为两种,一种是根Activity的启动过程,另一种是普通Activity的启动过程。根Activity指的是应用程序启动的第一个Activity,根Activity的启动过程一般情况下也可以理解为应用程序的启动过程。普通Activity指的是除应用程序启动的第一个Activity之外的其他Activity。

知识点

在这里插入图片描述

一、Launcher请求AMS过程

时序图

在这里插入图片描述

Launcher 启动后会将已安装应用程序的快捷图标显示到桌面上,这些应用程序的快捷图标就是启动根Activity的入口,当我们点击某个应用程序的快捷图标时,就会通过Launcher请求AMS来启动该应用程序。

桌面应用程序的快捷图标被点击时,就会调用Launcher的startActivitySafely方法。

Launcher#startActivitySafely
   public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
   ...
        // 设置intent的falg为NEW_TASK
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         // 开启activity(具体实现在Activity.java中)
         startActivity(intent, optsBundle);
         ...
    }
Activity#startActivity

Activity 类的成员变量 mInstrumentation 的类型为 Instrumentation,它用来监控应用程序和系统之间的交互操作。

Activity 类的成员变量 mMainThread 的类型为 ActivityThread,用来描述一个应用程序进程。系统每当启动一个应用程序进程时,都会在它里面夹在一个 ActivityThread 类实例,并且会将这个 ActivityThread 类实例保存在每一个在该进程中启动的 Activity 组件的父类 Activity 的成员变量 mMainThread 中。ActivityThread 类的成员函数 getApplicationThread 用来获取它内部的一个类型为 ApplicationThread 的 Binder 本地对象。在 Instrumentation 的 execStartActivity 时会作为参数传递给 ActivityManagerService,这样 AMS 接下来就可以通过它来通知 Launcher 组件进入 Paused 状态了。

Activity 类的成员变量 mToken 的类型为 Binder,它是一个 Binder 代理对象,指向了 AMS 中一个类型为 ActivityRecord 的 Binder 本地对象。每一个已经启动的 Activity 组件在 AMS 中都有一个对应的 ActivityRecord 对象,用来维护对应的 Activity 组件的运行状态以及信息。同样也作为参数传递给了 AMS,这样 AMS 就可以获得 Launcher 组件的详细信息了。


  public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            startActivityForResult(intent, -1);
        }
  }

  public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
        startActivityForResult(intent, requestCode, null);
    }
    

//  startActivity(Intent intent) 最终会调用这个方法。传到这里的requestCode = -1 
//表示launcher不需要知道启动activity后的结果。
  public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
            //mParent是Activity类型的,表示当前Activity的父类。
            //因为目前根Activity还没有创建出来,因此,mParent==null成立
        if (mParent == null) {
      
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
          
        }else{
        
        }
    }
Instrumentation#execStartActivity
public class Instrumentation {
	public ActivityResult execStartActivity(...) {
	 int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
	}
}

首先调用ActivityManager的getService方法来获取AMS的代理对象,接着调用它的startActivity方法,来通知 AMS 将一个 Activity 组件启动起来。

ActivityManager#getService
 public static IActivityTaskManager getService() {
        return IActivityTaskManagerSingleton.get();
    }
   private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
            new Singleton<IActivityTaskManager>() {
                @Override
                protected IActivityTaskManager create() {
                    final IBinder b = 
                    //得到名为“activity”的Service引用,也就是IBinder类型的AMS的引用
                    ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
                    //将获得的IBinder 引用转换成IActivityManager类型的对象
                    return IActivityTaskManager.Stub.asInterface(b);
                }
            };

二、AMS 到ApplicationThread的调用过程

Launcher请求AMS后,代码逻辑已经进入AMS中,接着是AMS到ApplicationThread的调用流程。具体的调用方法&时序图如下。

时序图

在这里插入图片描述

AMS#startActivity
  @Override
    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());
    }

AMS的startActivity方法中返回了startActivityAsUser方法,可以发现startActivityAsUser方法比startActivity方法多了一个参数UserHandle.getCallingUserId(),这个方法会获得调用者的UserId,AMS根据这个UserId来确定调用者的权限。接下来看startActivityAsUser方法

AMS#startActivityAsUser
 @Override
    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) {
            //1、判断调用者进程是否被隔离,如果被隔离则抛出SecurityException异常
        enforceNotIsolatedCaller("startActivity");
        //2、检查调用者是否有权限,如果没有权限也会抛出SecurityException异常
        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
                userId, false, ALLOW_FULL_ONLY, "startActivity", null);
        
        return mActivityStarter.startActivityMayWait(caller, -1, callingPackage, intent,
                resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
                profilerInfo, null, null, bOptions, false, userId, null, null,
                "startActivityAsUser");
    }

最终调用了ActivityStarter的startActivityMayWait方法,倒数第二个参数类型为TaskRecord,代表启动的Activity所在的栈。最后一个参数"startActivityAsUser"代表启动的理由。

ActivityStarter#startActivityMayWait

ActivityStarter是Android 7.0中新加入的类,它是加载Activity的控制类,会收集所有的逻辑来决定如何将Intent和Flags转换为Activity,并将Activity和Task以及Stack相关联

final int startActivityMayWait(IApplicationThread caller, int callingUid,
            String callingPackage, Intent intent, String resolvedType,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int startFlags,
            ProfilerInfo profilerInfo, WaitResult outResult,
            Configuration globalConfig, Bundle bOptions, boolean ignoreTargetSecurity, int userId,
            IActivityContainer iContainer, TaskRecord inTask, String reason) {

// 内部主要处理了reason 参数,reson为空直接抛出异常。
// 其内部最终调用ActivityStarter的startActivity
 int res = startActivityLocked(caller, intent, ephemeralIntent, resolvedType,
                    aInfo, rInfo, voiceSession, voiceInteractor,
                    resultTo, resultWho, requestCode, callingPid,
                    callingUid, callingPackage, realCallingPid, realCallingUid, startFlags,
                    options, ignoreTargetSecurity, componentSpecified, outRecord, container,
                    inTask, reason);
...
 return res;

}
ActivityStarter#startActivity
 private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
            String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
            String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
            ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified,
            ActivityRecord[] outActivity, ActivityStackSupervisor.ActivityContainer container,
            TaskRecord inTask) {

  ProcessRecord callerApp = null;
  //1、caller 实际是Launcher所在的应用程序进程的ApplicationThread对象,这个值是
  //一路传递过来的
        if (caller != null) {
         //2、获得launcher进程的 ProcessRecord 类型对象callerApp。
         //ProcessRecord 用来描述应用和程序进程。作用与ActivityRecord 类似。
            callerApp = mService.getRecordForAppLocked(caller);
            if (callerApp != null) {
                callingPid = callerApp.pid;
                callingUid = callerApp.info.uid;
            } else {
                Slog.w(TAG, "Unable to find app for caller " + caller
                        + " (pid=" + callingPid + ") when starting: "
                        + intent.toString());
                err = ActivityManager.START_PERMISSION_DENIED;
            }
        }

...
   //3、 ActivityRecord:Activity的描述类,记录activity的一些信息。
  ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
                callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
                resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
                mSupervisor, container, options, sourceRecord);
        if (outActivity != null) {
            outActivity[0] = r; //4、 吧ActivityRecord 赋值给ActivityRecord[],
            //数组名outActivity,这个数组作为参数向下传递。
        }

...

//startActivity 内部主要调用了startActivityUnchecked方法。
 return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true,
                options, inTask, outActivity);
}
ActivityStarter#startActivityUnchecked

startActivityUnchecked 方法主要处理与栈管理相关的逻辑

private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
            ActivityRecord[] outActivity) {

...

// 1、Launcher 启动根Activity时会将flag设置为FLAG_ACTIVITY_NEW_TASK,
//这样会满足如下判断.便接着执行setTaskFromReuseOrCreateNewTask 方法。
 int result = START_SUCCESS;
        if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask
                && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {
            newTask = true;
            //setTaskFromReuseOrCreateNewTask 内部会创建一个新的TaskRecord,
            //用来描述Activity的任务栈。同时这个方法内部会创建新的activity任务栈。
            result = setTaskFromReuseOrCreateNewTask(
                    taskToAffiliate, preferredLaunchStackId, topStack);
        }

...
//2、 最后调用ActivityStackSupervisor的resumeFocusedStackTopActivityLocked方法
 mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity,
                        mOptions);

}
ActivityStackSupervisor#resumeFocusedStackTopActivityLocked
  boolean resumeFocusedStackTopActivityLocked(
            ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) {
        if (targetStack != null && isFocusedStack(targetStack)) {
            return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
        }
        //1、获取要启动activity所在栈的,栈顶的,不是处于停止状态的ActivityLocked
        final ActivityRecord r = mFocusedStack.topRunningActivityLocked();
        
        //2、 当ActivityRecord 不为null 或者 要启动activity状态不为RESUMED 
        //执行ActivityStack的resumeTopActivityUncheckedLocked。
        //对于即将启动的activity这个条件是满足的
        if (r == null || r.state != RESUMED) {
            mFocusedStack.resumeTopActivityUncheckedLocked(null, null);
        } else if (r.state == RESUMED) {
            // Kick off any lingering app transitions form the MoveTaskToFront operation.
            mFocusedStack.executeAppTransition(targetOptions);
        }
        return false;
    }
ActivityStack
 boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) {
        if (mStackSupervisor.inResumeTopActivity) {
            // Don't even start recursing.
            return false;
        }

        boolean result = false;
        try {
            // Protect against recursion.
            mStackSupervisor.inResumeTopActivity = true;
            //继续调用
            result = resumeTopActivityInnerLocked(prev, options); 
        } finally {
            mStackSupervisor.inResumeTopActivity = false;
        }
 
        mStackSupervisor.checkReadyForSleepLocked();

        return result;
    }
 private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
 
 //最终调用ActivityStackSupervisor的startSpecificActivityLocked
mStackSupervisor.startSpecificActivityLocked(next, true, true);
if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked();
        return true;
}
ActivityStackSupervisor#startSpecificActivityLocked
 void startSpecificActivityLocked(ActivityRecord r,
            boolean andResume, boolean checkConfig) {
     
        //1、获取即将启动的activity所在应用进程
        ProcessRecord app = mService.getProcessRecordLocked(r.processName,
                r.info.applicationInfo.uid, true);

        r.getStack().setLaunchTime(r);

        if (app != null && app.thread != null) {// 2、假如应用进程已经运行
            try {
                if ((r.info.flags&ActivityInfo.FLAG_MULTIPROCESS) == 0
                        || !"android".equals(r.info.packageName)) {
                    // Don't add this if it is a platform component that is marked
                    // to run in multiple processes, because this is actually
                    // part of the framework so doesn't make sense to track as a
                    // separate apk in the process.
                    app.addPackage(r.info.packageName, r.info.applicationInfo.versionCode,
                            mService.mProcessStats);
                }
                //3、执行realStartActivityLocked,吧activity所在进程参数app 传递下去。
                realStartActivityLocked(r, app, andResume, checkConfig);
                return;
            } catch (RemoteException e) {
                Slog.w(TAG, "Exception when starting activity "
                        + r.intent.getComponent().flattenToShortString(), e);
            }

            // If a dead object exception was thrown -- fall through to
            // restart the application.
        }

        mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
                "activity", r.intent.getComponent(), false, false, true);
    }
ActivityStackSupervisor#realStartActivityLocked

这段代码指的就是要在目标应用程序进程启动Activity。当前代码逻辑运行在AMS 所在的进程(SystemServer 进程)中,通过ApplicationThread来与应用程序进程进行Binder通信,换句话说,ApplicationThread是AMS所在进程(SystemServer进程)和应用程序进程的通信桥梁

1、app.thread 是IApplicationThread 类型
2、ActivityThread 的内部类ApplicationThread 是IApplicationThread 的实现类。
3、ApplicationThread 继承了IApplicationThread.Stub
4、app指的是传入的要启动的Activity所在的应用程序进程

 final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
            boolean andResume, boolean checkConfig) throws RemoteException {
...


    app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                    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, !andResume,
                    mService.isNextTransitionForward(), profilerInfo);
...

return true;
}

最终代码调用就进入了ApplicationThread 的scheduleLaunchActivity 中》》》

三、ActivityThread启动Activity的过程

时序图

在这里插入图片描述
应用程序进程创建后会运行代表主线程的实例ActivityThread,其中ApplicationThread是ActivityThread 的内部类。

ApplicationThread#scheduleLaunchActivity

scheduleLaunchActivity方法将启动Activity的参数封装成ActivityClientRecord,通过消息机制向H(ActivityThread的内部类)这个Handler发送启动activity的消息。

  public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

            updateProcessState(procState, false);

            ActivityClientRecord r = new ActivityClientRecord();

            r.token = token;
            r.ident = ident;
            r.intent = intent;
            r.referrer = referrer;
            r.voiceInteractor = voiceInteractor;
            r.activityInfo = info;
            r.compatInfo = compatInfo;
            r.state = state;
            r.persistentState = persistentState;

            r.pendingResults = pendingResults;
            r.pendingIntents = pendingNewIntents;

            r.startsNotResumed = notResumed;
            r.isForward = isForward;

            r.profilerInfo = profilerInfo;

            r.overrideConfig = overrideConfig;
            updatePendingConfiguration(curConfig);

            sendMessage(H.LAUNCH_ACTIVITY, r);
        }

 private void sendMessage(int what, Object obj, int arg1, int arg2, int seq) {
        if (DEBUG_MESSAGES) Slog.v(
                TAG, "SCHEDULE " + mH.codeToString(what) + " arg1=" + arg1 + " arg2=" + arg2 +
                        "seq= " + seq);
        Message msg = Message.obtain();
        msg.what = what;
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = obj;
        args.argi1 = arg1;
        args.argi2 = arg2;
        args.argi3 = seq;
        msg.obj = args;
        mH.sendMessage(msg); // 调用 handler的sendMessage方法发送消息。
    }
H 类&handleMessage

H类为 ActivityThread的内部类并继承自Handler,是应用程序进程中主线程的消息管理类。

这里为啥使用handler的消息机制?
因为ApplicationThread是一个Binder,它的调用逻辑运行在Binder线程池中,所以这里需要用H将代码的逻辑切换到主线程中。这样代码逻辑就运行在应用进程的主线程中啦。

 private class H extends Handler {
 
        public static final int LAUNCH_ACTIVITY         = 100;
        public static final int PAUSE_ACTIVITY          = 101;
        public static final int PAUSE_ACTIVITY_FINISHING= 102;
        public static final int STOP_ACTIVITY_SHOW      = 103;
        ...
        
  public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    //1、获取收到的消息对象 ActivityClientRecord 
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                   // 2、ActivityClientRecord的packageInfo 对象类型是LoadedApk.
                   //应用程序进程要启动Activity时需要将该Activity所属的APK加载进来,
                   //而LoadedApk就是用来描述已加载的APK文件的
                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                     //3、继续调用
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                
                ...

}

ActivityThread#handleLaunchActivity
 private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
...

        // 1、启动activity
        Activity a = performLaunchActivity(r, customIntent);

        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            reportSizeConfigurations(r);
            Bundle oldState = r.state;
            // 2、将activity状态置为resume
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

            if (!r.activity.mFinished && r.startsNotResumed) {
                performPauseActivityIfNeeded(r, reason);

                if (r.isPreHoneycomb()) {
                    r.state = oldState;
                }
            }
        } else {// 3、如何activity 为空 通知ams停止启动activity
            // If there was an error, for any reason, tell the activity manager to stop us.
            try {
                ActivityManager.getService()
                    .finishActivity(r.token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }

}
ActivityThread#performLaunchActivity
   private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
   //1、ActivityInfo:用于存储代码以及AndroidManifes设置的Activity和Receiver
   //节点信息,比如Activity的theme和launchMode
 ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
        //2、获取APK文件的描述类LoadedApk
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }
//3、获取要启动的Activity的ComponentName类,
//在ComponentName 类中保存了该Activity的包名和类名
        ComponentName component = r.intent.getComponent();
        ...
        // 4、创建要启动Activity的上下文环境
  ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            //5、根据ComponentName中存储的Activity类名,
            //用类加载器来创建该Activity的实例
            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);
            }
        }
        ...
        //6、创建Application,makeApplication 方法内部会调用Application的onCreate方法
  Application app = r.packageInfo.makeApplication(false, mInstrumentation);
    ...

//7、初始化Activity,在attach方法中会创建Window对象(PhoneWindow)并与Activity自身进行关联。
 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);
}

...

activity.mCalled = false;
                if (r.isPersistable()) {
                // 8、调用Instrumentation的callActivityOnCreate方法来启动Activity
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
Instrumentation#callActivityOnCreate
 public void callActivityOnCreate(Activity activity, Bundle icicle) {
        prePerformCreate(activity);
        activity.performCreate(icicle);//Activity 的performCreate
        postPerformCreate(activity);
    }
Activity#performCreate
 final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        restoreHasCurrentPermissionRequest(icicle);
        onCreate(icicle, persistentState); //调用onCreate 方法
        mActivityTransitionState.readState(icicle);
        performCreateCommon();
    }

总结

根Activity启动过程中会涉及4个进程,分别是Zygote进程、Launcher进程、AMS所在进程(SystemServer进程)、应用程序进程。如果是普通Activity启动过程会涉及几个进程呢?答案是两个,AMS所在进程和应用程序进程。

根Activity启动过程中涉及的进程

在这里插入图片描述

根Activity启动#时序图

在这里插入图片描述

The end

参考:
安卓进阶解密:微信读书版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值