Android APP启动流程

女朋友是位程序员性格很活泼也很可爱,但是最近两天总见她愁眉不展的,询问下才知道原来是一次面试被问到了应用的启动流程Application、Activity的创建和生命周期是如何被触发的,看到她温润的小脸已不见往日的笑容,我决定通过源码的方式以点带面的带她体验一次完整的应用启动流程&创建过程。

先声明几个比较重要的类:

android.app.ActivityThread
android.app.Instrumentation
android.app.ActivityThread.ApplicationThread

这三个类组合在一起有一点MVP的思想在里面,M—>ApplicationThread,V—>ActivityThread,P—>Instrumentation

启动应用的主入口在ActivityThread的main函数

    public static void main(String[] args) {
        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        Looper.loop();
    }

在入口处调用prepareMainLooper后会创建一个quitAllowed为false既不运行退出的消息循环,最后调用loop使它轮询起来。其次是手动new了一个ActivityThread并调用了attach方法开始执行创建过程,下面看attach的实现。

    private void attach(boolean system, long startSeq) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        if (!system) {
            final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        } else {
			...
        }
    }

其中的system主要判断是不是系统应用,很显然上一步传入的是false所以会走if里面的逻辑,通过getService获取到一个AMS的代理,而其真正的实现在ActivityManagerService里面,调用mgr.attachApplication时传入了一个mAppThread,这个对象就是上面提到的android.app.ActivityThread.ApplicationThread类。

    @Override
    public final void attachApplication(IApplicationThread thread, long startSeq) {
        if (thread == null) {
            throw new SecurityException("Invalid application interface");
        }
        synchronized (this) {
            int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();
            final long origId = Binder.clearCallingIdentity();
            attachApplicationLocked(thread, callingPid, callingUid, startSeq);
            Binder.restoreCallingIdentity(origId);
        }
    }

在该方法中主要生成了一些相关的id进而又调用了本类中的attachApplicationLocked方法

    @GuardedBy("this")
    private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
            int pid, int callingUid, long startSeq) {
		。。。
		thread.bindApplication(processName, appInfo, providerList, null, profilerInfo,
                        null, null, null, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
                        new Configuration(app.getWindowProcessController().getConfiguration()),
                        app.compat, getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked(),
                        buildSerial, autofillOptions, contentCaptureOptions,
                        app.mDisabledCompatChanges);
		return true;
	}

方法的内容较长前面都是在执行一些逻辑判断和数据准备,其中核心代码就是thread.bindApplication,这个thread就是刚才传入的android.app.ActivityThread.ApplicationThread类

        @Override
        public final void bindApplication(String processName, ApplicationInfo appInfo,
                ProviderInfoList providerList, ComponentName instrumentationName,
                ProfilerInfo profilerInfo, Bundle instrumentationArgs,
                IInstrumentationWatcher instrumentationWatcher,
                IUiAutomationConnection instrumentationUiConnection, int debugMode,
                boolean enableBinderTracking, boolean trackAllocation,
                boolean isRestrictedBackupMode, boolean persistent, Configuration config,
                CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
                String buildSerial, AutofillOptions autofillOptions,
                ContentCaptureOptions contentCaptureOptions, long[] disabledCompatChanges) {
            ...省略代码
            AppBindData data = new AppBindData();
            data.processName = processName;
            data.appInfo = appInfo;
            data.providers = providerList.getList();
            data.instrumentationName = instrumentationName;
            data.instrumentationArgs = instrumentationArgs;
            data.instrumentationWatcher = instrumentationWatcher;
            data.instrumentationUiAutomationConnection = instrumentationUiConnection;
            data.debugMode = debugMode;
            data.enableBinderTracking = enableBinderTracking;
            data.trackAllocation = trackAllocation;
            data.restrictedBackupMode = isRestrictedBackupMode;
            data.persistent = persistent;
            data.config = config;
            data.compatInfo = compatInfo;
            data.initProfilerInfo = profilerInfo;
            data.buildSerial = buildSerial;
            data.autofillOptions = autofillOptions;
            data.contentCaptureOptions = contentCaptureOptions;
            data.disabledCompatChanges = disabledCompatChanges;
            sendMessage(H.BIND_APPLICATION, data);
        }

该方法主要创建了一个AppBindData数据管理类,将数据进行装载,最后调用当前类中的sendMessage方法

    void sendMessage(int what, Object obj) {
        sendMessage(what, obj, 0, 0, false);
    }
    private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
        if (DEBUG_MESSAGES) {
            Slog.v(TAG,
                    "SCHEDULE " + what + " " + mH.codeToString(what) + ": " + arg1 + " / " + obj);
        }
        Message msg = Message.obtain();
        msg.what = what;
        msg.obj = obj;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        if (async) {
            msg.setAsynchronous(true);
        }
        mH.sendMessage(msg);
    }

通过调用多态的sendMessage方法后最终会调用到一个内部Handler的handleMessage中去

class H extends Handler {
	public void handleMessage(Message msg) {
		switch (msg.what) {
			case BIND_APPLICATION:
                    AppBindData data = (AppBindData)msg.obj;
                    handleBindApplication(data);
                    break;
		}
		。。。省略代码
	}
}

在Handler中又会调用到外部类ActivityThread的handleBindApplication方法

    @UnsupportedAppUsage
    private void handleBindApplication(AppBindData data) {
    	。。。省略代码
    	Application app;
        try {
            // If the app is being launched for full backup or restore, bring it up in
            // a restricted environment with the base application class.
            app = data.info.makeApplication(data.restrictedBackupMode, null);
            mInitialApplication = app;
            try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                if (!mInstrumentation.onException(app, e)) {
                    throw new RuntimeException(
                      "Unable to create application " + app.getClass().getName()
                      + ": " + e.toString(), e);
                }
            }
        } finally {
            。。。省略代码
        }
    }

通过一些列的判断和准备后会调用data.info.makeApplication方法,也就是android.app.LoadedApk类中的makeApplication

    @UnsupportedAppUsage
    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
        Application app = null;
        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }
		。。省略代码
        try {
            final java.lang.ClassLoader cl = getClassLoader();
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            // The network security config needs to be aware of multiple
            // applications in the same process to handle discrepancies
            NetworkSecurityConfigProvider.handleNewApplication(appContext);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
        } catch (Exception e) {
            if (!mActivityThread.mInstrumentation.onException(app, e)) {
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                throw new RuntimeException(
                    "Unable to instantiate application " + appClass
                    + ": " + e.toString(), e);
            }
        }
        mActivityThread.mAllApplications.add(app);
        mApplication = app;

        if (instrumentation != null) {
            try {
                instrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                if (!instrumentation.onException(app, e)) {
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    throw new RuntimeException(
                        "Unable to create application " + app.getClass().getName()
                        + ": " + e.toString(), e);
                }
            }
        }
        return app;
    }
    public @NonNull Application instantiateApplication(@NonNull ClassLoader cl,
            @NonNull String className)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return (Application) cl.loadClass(className).newInstance();
    }
    public void callApplicationOnCreate(Application app) {
        app.onCreate();
    }

这里的appClass就是你项目中自定义的application,如果没有自定义则直接使用Android默认的app.Application,接着通过mActivityThread.mInstrumentation.newApplication()通过反射的方式创建一个实例赋值给app,之后再投过instrumentation.callApplicationOnCreate(app)方法调用application的onCreate方法。

到这我们应用的Application就创建成功了,调用onCreate方法后就开始执行我们应用的一些初始化配置

下面再来看Activity
回到ActivityManagerService的attachApplicationLocked方法里面

    @GuardedBy("this")
    private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
            int pid, int callingUid, long startSeq) {
		。。。省略代码
		// 创建Application
		thread.bindApplication(processName, appInfo, providerList, null, profilerInfo,
                        null, null, null, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
                        new Configuration(app.getWindowProcessController().getConfiguration()),
                        app.compat, getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked(),
                        buildSerial, autofillOptions, contentCaptureOptions,
                        app.mDisabledCompatChanges);
         。。。省略代码
         // 创建Activity
        if (normalMode) {
            try {
                didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
            } catch (Exception e) {
                Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
                badApp = true;
            }
        }
		return true;
	}

在attachApplicationLocked里创建完application后紧接着又调用mAtmInternal.attachApplication来执行activity的创建,这里的mAtmInternal实现类是:com.android.server.wm.ActivityTaskManagerService

        @HotPath(caller = HotPath.PROCESS_CHANGE)
        @Override
        public boolean attachApplication(WindowProcessController wpc) throws RemoteException {
            synchronized (mGlobalLockWithoutBoost) {
                if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
                    Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "attachApplication:" + wpc.mName);
                }
                try {
                    return mRootWindowContainer.attachApplication(wpc);
                } finally {
                    Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                }
            }
        }

在attachApplication(WindowProcessController wpc)中紧接着又调用了com.android.server.wm.RootWindowContainer类中的attachApplication方法

    boolean attachApplication(WindowProcessController app) throws RemoteException {
    	boolean didSomething = false;
        for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
        	final ActivityStack stack = display.getFocusedStack();
           。。。省略代码
            final PooledFunction c = PooledLambda.obtainFunction(
                    RootWindowContainer::startActivityForAttachedApplicationIfNeeded, this,
                    PooledLambda.__(ActivityRecord.class), app, stack.topRunningActivity());
           。。。省略代码
        }
        return didSomething;
    }
    private boolean startActivityForAttachedApplicationIfNeeded(ActivityRecord r,
            WindowProcessController app, ActivityRecord top) {
            。。。省略代码
        try {
            if (mStackSupervisor.realStartActivityLocked(r, app, top == r /*andResume*/,
                    true /*checkConfig*/)) {
                mTmpBoolean = true;
            }
        } catch (RemoteException e) {
            mTmpRemoteException = e;
            return true;
        }
        return false;
    }

在for循环中得到stack进而调用了本类当中的startActivityForAttachedApplicationIfNeeded方法,在startActivityForAttachedApplicationIfNeeded中又通过对象引用调用了com.android.server.wm.ActivityStackSupervisor类当中的realStartActivityLocked

    boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc,
            boolean andResume, boolean checkConfig) throws RemoteException {
            。。。省略代码
            final ClientTransaction clientTransaction = ClientTransaction.obtain(
                    proc.getThread(), 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, proc.getReportedProcState(),
                    r.getSavedState(), r.getPersistentSavedState(), results, newIntents,
                    dc.isNextTransitionForward(), proc.createProfilerInfoIfNeeded(),
                    r.assistToken, r.createFixedRotationAdjustmentsIfNeeded()));

            // Set desired final state.
            final ActivityLifecycleItem lifecycleItem;
            if (andResume) {
                lifecycleItem = ResumeActivityItem.obtain(dc.isNextTransitionForward());
            } else {
                lifecycleItem = PauseActivityItem.obtain();
            }
            clientTransaction.setLifecycleStateRequest(lifecycleItem);
            // Schedule transaction.
            mService.getLifecycleManager().scheduleTransaction(clientTransaction);
            。。。省略代码
	}

这个方法很长,但创建的核心代码就上面这些:先创建了一个ClientTransaction把ApplicationThread传递进去,之后添加了一个LaunchActivityItem作为创建时的回调,后续又添加了一个生命周期的item,最后调用ClientLifecycleManager的scheduleTransaction(clientTransaction)方法开始处理。

    void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
        final IApplicationThread client = transaction.getClient();
        transaction.schedule();
    }

接着调用ClientTransaction的schedule方法

    public void schedule() throws RemoteException {
        mClient.scheduleTransaction(this);
    }

这里的mClient就是创建ClientTransaction时传递的ApplicationThread

    @Override
    public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
        ActivityThread.this.scheduleTransaction(transaction);
    }

在ApplicationThread中又调用了ActivityThread继承自父类的scheduleTransaction,紧接着又发送了一个handler事件

    void scheduleTransaction(ClientTransaction transaction) {
        transaction.preExecute(this);
        sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
    }
class H extends Handler {
	public void handleMessage(Message msg) {
		。。。省略代码
		switch (msg.what) {
			case EXECUTE_TRANSACTION:
                 final ClientTransaction transaction = (ClientTransaction) msg.obj;
                 mTransactionExecutor.execute(transaction);
                 if (isSystem()) {
                     transaction.recycle();
                 }
                 break;
		}
		。。。省略代码
	}
}

通过引用调用了android.app.servertransaction.TransactionExecutor类中的execute(transaction)方法

    public void execute(ClientTransaction transaction) {
        。。。省略代码
        executeCallbacks(transaction);
        executeLifecycleState(transaction);
       
    }

其中executeCallbacks是执行创建任务,executeLifecycleState是执行生命周期任务。先来看创建

    @VisibleForTesting
    public void executeCallbacks(ClientTransaction transaction) {
    	。。。省略代码
        final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
        final int size = callbacks.size();
        for (int i = 0; i < size; ++i) {
            final ClientTransactionItem item = callbacks.get(i);
            item.execute(mTransactionHandler, token, mPendingActions);
        }
       。。。省略代码
    }

先获取了所有的回调对象,然后调用回调对象的execute方法,这里的item实际就是在创建ClientTransaction时传入的LaunchActivityItem

    @Override
    public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
        ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
                mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
                mPendingResults, mPendingNewIntents, mIsForward,
                mProfilerInfo, client, mAssistToken, mFixedRotationAdjustments);
        client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
    }

先创建一个ActivityClientRecord对象,再调用client.handleLaunchActivity,因为ActivityThread继承自ClientTransactionHandler,这里的client就是ActivityThread。

    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        。。。省略代码
        final Activity a = performLaunchActivity(r, customIntent);
        return a;
    }

再来看performLaunchActivity方法

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
        ComponentName component = r.intent.getComponent();
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            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 app = r.packageInfo.makeApplication(false, mInstrumentation);
            if (activity != null) {
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
                appContext.getResources().addLoaders(
                        app.getResources().getLoaders().toArray(new ResourcesLoader[0]));

                appContext.setOuterContext(activity);
                if (customIntent != null) {
                    activity.mIntent = customIntent;
                }
                r.lastNonConfigurationInstances = null;
                checkAndBlockForNetworkAccess();
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }
                activity.mCalled = false;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                r.activity = activity;
                mLastReportedWindowingMode.put(activity.getActivityToken(),
                        config.windowConfiguration.getWindowingMode());
            }
            r.setState(ON_CREATE);
            synchronized (mResourcesManager) {
                mActivities.put(r.token, r);
            }
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
        }
        return activity;
    }

先调用mInstrumentation.newActivity通过反射创建出一个activity实例,接着又给activity设置label、theme、intnet等属性,最后又调用了mInstrumentation.callActivityOnCreate方法通知。

    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        prePerformCreate(activity);
        activity.performCreate(icicle);
        postPerformCreate(activity);
    }

在callActivityOnCreate中会接着调用activity的performCreate方法

    @UnsupportedAppUsage
    final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        dispatchActivityPreCreated(icicle);
		。。省略代码
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }
    }

在performCreate中调用的onCreate方法就是我们的业务入口的onCreate

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

到此activity就创建完成了

再来看生命周期的执行逻辑
android.app.servertransaction.TransactionExecutor

    public void execute(ClientTransaction transaction) {
        。。。省略代码
        executeCallbacks(transaction);
        executeLifecycleState(transaction);
       
    }

executeCallbacks是执行创建任务,executeLifecycleState是执行生命周期任务。

    private void executeLifecycleState(ClientTransaction transaction) {
        final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest();
        if (lifecycleItem == null) {
            // No lifecycle request, return early.
            return;
        }
        // Execute the final transition with proper parameters.
        lifecycleItem.execute(mTransactionHandler, token, mPendingActions);
        lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions);
    }

其中的lifecycleItem就是创建ClientTransaction对象时设置的ResumeActivityItem,来看ResumeActivityItem中的execute方法。

    @Override
    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);
    }

这里的client并不默认,就是ActivityThread的引用。

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
		。。。省略代码
		// TODO Push resumeArgs into the activity for consideration
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        if (r == null) {
            // We didn't actually resume the activity, so skipping any follow-up actions.
            return;
        }
    @VisibleForTesting
    public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
            String reason) {
        final ActivityClientRecord r = mActivities.get(token);
        if (r.getLifecycleState() == ON_RESUME) {
            return null;
        }
        if (finalStateRequest) {
            r.hideForNow = false;
            r.activity.mStartedActivity = false;
        }
        try {
            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 r;
    }

可以看出在ActivityThread中通过两次调用后最终调用了activity本身的performResume方法,来看Activity的performResume方法

   final void performResume(boolean followedByPause, String reason) {
   		...省略代码
        performRestart(true /* start */, reason);
        mCalled = false;
        // mResumed is set by the instrumentation
        mInstrumentation.callActivityOnResume(this);
        ...省略代码
    }

先是调用了performRestart进行restart和start生命周期的调用,之后又调用了mInstrumentation.callActivityOnResume(this)方法

   final void performRestart(boolean start, String reason) {
    		...省略代码
            mCalled = false;
            mInstrumentation.callActivityOnRestart(this);
            if (start) {
                performStart(reason);
            }
        }
    }
   final void performStart(String reason) {
        。。。省略代码
        mInstrumentation.callActivityOnStart(this);
        。。。省略代码
    }

不难发现上面几个方法的调用最终都通过对象引用掉了Instrumentation类中的方法,来看下它的具体实现。

    public void callActivityOnStart(Activity activity) {
        activity.onStart();
    }

    public void callActivityOnRestart(Activity activity) {
        activity.onRestart();
    }

    public void callActivityOnResume(Activity activity) {
        activity.mResumed = true;
        activity.onResume();
     	...省略代码
    }

通过调用activity本身的onRestart,onStart,onResume方法进而实现了Activity生命周期在不同时机的回调。
带着女票体验了整个创建过程后明显的感觉到了她的变化,愁容已经渐渐舒展开了,还说晚上要给我做饭吃,哈哈~真是幸甚老夫!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值