Activity Startup过程分析

本文详细分析了Android中Activity的启动过程,从点击桌面图标开始,涉及Activity.startActivity、Activity.startActivityForResult、Instrumentation.execStartActivity等多个步骤,直到Activity的创建和显示。通过时序图和代码解析,阐述了Activity启动时涉及到的关键组件和方法,包括Launcher、Intent、ActivityManagerService等的交互细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Activity Startup

Contents

SequenceDiagram..4

Startfrom click shortcut on Launcher.4

Activity.startActivity.6

Activity.startActivityForResult.7

Instrumentation.execStartActivity.8

Step7: ActivityManagerService.startActivity.9

Step8: ActivityStack.startActivityMayWait.10

Step9: ActivityStack. startActivityLocked.12

Step10: ActivityStack.startActivityUncheckedLocked.17

Step11: ActivityStack. startActivityLocked.25

Step12: ActivityStack.resumeTopActivityLocked.28

Step13: ActivityStack.startPausingLocked.36

Step14: ApplicationThread. schedulePauseActivity.39

Step15: ActivityThread.queueOrSendMessage.39

Step16: ActivityThread.Handler.handleMessage.39

Step17: ActivityThread.handlePauseActivity.40

Step18: ActivityThread.performPauseActivity.41

Step19: ActivityManagerService.activityPaused().42

Step20: ActivityStack.activityPaused().42

Step21: ActivityStack.completePauseLocked().43

Step22: ActivityStack.resumeTopActivityLocked().44

Step23: ActivityStack.startSpecificActivityLocked ().44

Step24: ActivityManagerService. startProcessLocked().45

Step25: ActivityManagerService.startProcessLocked().47

Step27: ActivityThread.main.49

Step28: ActivityThread.attach(false).50

Step 29: ActivityManagerService.attachApplication.50

Step 30: ActivityManagerService.attachApplicationLocked.51

Step31: ActivityStack. realStartActivityLocked.56

Step32: ActivityThread.scheduleLaunchActivity.58

Step33: ActivityThread. queueOrSendMessage(H.LAUNCH_ACTIVITY, r);58

Step34: ActivityThread.Handler.handleMessage.58

ActivityThread.handleLaunchActivity(r, null).58

 

 


 

启动一个新Activity可以通过2种方式,通过点击桌面图标;或者调用startActivity()/StartActivityForResult()。这个文档里主要讨论通过点击桌面应用图标的方式启动Activity。

为了讲解方便,把要启动的Activity命名为MyActivity 。在AndroidManifest.xml文件中做如下配置:

<activity android:name=".MyActivity"   

     android:label="@string/app_name">   

      <intent-filter>   

        <actionandroid:name="android.intent.action.MAIN" />   

        <categoryandroid:name="android.intent.category.LAUNCHER" />   

   </intent-filter>   

</activity>

先看看时序图有个大概的印象,下面就具体详细分析每一步是如何实现的。

 


在Android系统中,应用程序是由Launcher启动起来的,其实,Launcher本身也是一个应用程序,就是我们所熟知的Home。其它的应用程序安装后,就会在Launcher的界面上出现一个相应的图标,点击这个图标时,Launcher就会把对应的应用程序启动起来。

在onClick()方法中不仅需要处理某个应用的启动,还包括针对点击folder和AllAppsButton的处理。

http://opengrok.sonyericsson.net/jellybean/xref/jb-lagan/packages/apps/Launcher2/src/com/android/launcher2/Launcher.java

1845    /**

1846     * Launches the intent referred by theclicked shortcut.

1847     *

1848     * @param vThe view representing the clicked shortcut.

1849     */

1850    public voidonClick(View v) {

...

//Tag:

Unlike IDs, tags are not used toidentify views. Tags are essentially an extra piece of information that can beassociated with a view. They are most often used as a convenience to store datarelated to views in the views themselves rather than by putting them in aseparate structure.

1861        Objecttag = v.getTag();

1862        if (taginstanceof ShortcutInfo) {

1863            //Open shortcut

1864            finalIntent intent = ((ShortcutInfo) tag).intent;

1865            int[]pos = new int[2];

1866           v.getLocationOnScreen(pos);

1867            intent.setSourceBounds(newRect(pos[0], pos[1],

1868                   pos[0] + v.getWidth(), pos[1] + v.getHeight()));

1869           //这个函数调用是Activity启动的入口

1870            boolean success =startActivitySafely(v, intent, tag);

1871        //设置图标被点击、选中的状态

1872            if(success && v instanceof BubbleTextView) {

1873               mWaitingForResume = (BubbleTextView) v;

1874                mWaitingForResume.setStayPressed(true);

1875            }

1876        } else if(tag instanceof FolderInfo) {

1877            if (vinstanceof FolderIcon) {

1878               FolderIcon fi = (FolderIcon) v;

1879               handleFolderClick(fi);

1880            }

1881        } else if(v == mAllAppsButton) {

1882            if(isAllAppsVisible()) {

1883               showWorkspace(true);

1884            }else {

1885               onClickAllAppsButton(v);

1886            }

1887        }

1888    }

 

2009    boolean startActivitySafely(View v, Intentintent, Object tag) {

2010        booleansuccess = false;

2011        try {

2012            success = startActivity(v, intent,tag);

2013        } catch(ActivityNotFoundException e) {

2014            Toast.makeText(this,R.string.activity_not_found, Toast.LENGTH_SHORT).show();

2015           Log.e(TAG, "Unable to launch. tag=" + tag + "intent=" + intent, e);

2016        }

2017        returnsuccess;

2018    }

下面继续看Launcher.startActivity()

1982    boolean startActivity(View v, Intentintent, Object tag) {

1983       intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

1984

1985        try {

1986            //Only launch using the new animation if the shortcut has not opted out (this isa

1987            //private contract between launcher and may be ignored in the future).

1988           boolean useLaunchAnimation = (v != null) &&

1989                   !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION);

//下面这段绿色部分是插入的注释

// useLaunchAnimation should betrue, because “INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION” is NOT set inpackages/apps/Launcher2/src/com/android/launcher2/LauncherProvider.java, 

483            if (version < 12) {

484                // Contact shortcuts need adifferent set of flags to be launched now

485                // The updateContactsShortcutschange is idempotent, so we can keep using it like

486                // back in the Donut days

487                updateContactsShortcuts(db);

488                version = 12;

489            }

 

1990            if(useLaunchAnimation) {

1991               ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0,

1992                       v.getMeasuredWidth(), v.getMeasuredHeight());

1993

1994               startActivity(intent, opts.toBundle());

1995            }else {

1996               startActivity(intent);

1997            }

1998           return true;

1999        } catch(SecurityException e) {

2000           Toast.makeText(this, R.string.activity_not_found,Toast.LENGTH_SHORT).show();

2001           Log.e(TAG, "Launcher does not have the permission to launch "+ intent +

2002                   ". Make sure to create a MAIN intent-filter for the correspondingactivity " +

2003                    "or use the exportedattribute for this activity. "

2004                   + "tag="+ tag + " intent=" + intent, e);

2005        }

2006        returnfalse;

2007    }

Activity.startActivity

这个函数里面直接调用startActivityForResult(),其中requestCode=-1表示不需要返回结果。

Activity.java

public void startActivity(Intentintent, Bundle options) {

        if (options!= null) {

            startActivityForResult(intent, -1, options);

        } else {

            // Notewe want to go through this call for compatibility with

            //applications that may have overridden the method.

           startActivityForResult(intent, -1);

        }

    }

Activity.startActivityForResult

这里的mMainThread是ActivityThread类型的Activity类成员变量,它代表的是应用程序的主线程,也就是Launcher应用程序运行的进程。这里通过mMainThread.getApplicationThread获得它里面的ApplicationThread成员变量,它是一个 Binder对象,后面我们会看到,ActivityManagerService会使用它来和ActivityThread来进行进程间通信。

这里的mToken也是Activity类的成员变量,它是一个Binder对象的远程接口。它在attch()方法中被赋值。

public void startActivityForResult(Intent intent, intrequestCode, Bundle options) {

        if (mParent== null) {// mParent == null表示启动的不是sub-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) {

                // Ifthis start is requesting a result, we can avoid making

                //the activity visible until the result is received.  Setting

                //this code during onCreate(Bundle savedInstanceState) or onResume() will keepthe

                //activity hidden during this time, to avoid flickering.

                //This can only be done when a result is requested because

                //that guarantees we will get information back when the

                //activity is finished, no matter what happens to it.

               mStartedActivity = true;

            }

        } else {

            if(options != null) {

               mParent.startActivityFromChild(this, intent, requestCode, options);

            } else {

                //Note we want to go through this method for compatibility with

                //existing applications that may have overridden it.

               mParent.startActivityFromChild(this, intent, requestCode);

            }

        }

    }

Instrumentation.execStartActivity

Instrumentation.java中相同名字的函数一共有2个,一个是为了启动Activity,另外一个是在Fragment中启动Activty

检查mActivityMonitors列表中是否已经有该Activity对象了,如果存在而且该对象处于block状态,则不启动该Activity

调用AMS.execStartActivity()把启动新Activity的任务通过IPC交给AMS来完成。至此程序的运行已经脱离了Launcher当前运行的进程,进入到AMS的线程中去了。

调用checkStartActivityResult检查异常返回结果。

Instrumentation.java

public ActivityResult execStartActivity(

            Contextwho, IBinder contextThread, IBinder token,Activity target,

            Intentintent, int requestCode, Bundle options) {

       IApplicationThread whoThread = (IApplicationThread) contextThread;

//首先检查启动队列中是否已经包含了相同的启动任务,如果已经存在就不需要再启动新的了

        if(mActivityMonitors != null) {

           synchronized (mSync) {

                finalint N = mActivityMonitors.size();

                for(int i=0; i<N; i++) {

                   final ActivityMonitor am = mActivityMonitors.get(i);

                   if (am.match(who, null, intent)) {

                       am.mHits++;

                       if (am.isBlocking()) {

                           return requestCode >= 0 ? am.getResult() : null;

                       }

                       break;

                    }

                }

            }

        }

        try {

           intent.setAllowFds(false);

           intent.migrateExtraStreamToClipData();

            int result =ActivityManagerNative.getDefault()

                .startActivity(whoThread,intent,

                       intent.resolveTypeIfNeeded(who.getContentResolver()),

                        token, target != null ?target.mEmbeddedID : null,

                        requestCode, 0, null,null, options);

           checkStartActivityResult(result, intent);

        } catch(RemoteException e) {

        }

        return null;

    }

Step 7: ActivityManagerService.startActivity

此处传入的参数列表:

IApplicationThread caller:

ApplicationThread类型的Binder实体

int callingUid:

-1

Intent intent:

 

String resolvedType:

这里的intent.resolveTypeIfNeeded返回这个intent的MIME类型,在这个例子中,没有AndroidManifest.xml设置MainActivity的MIME类型,因此,这里返回null。

 IBinder resultTo:

一个代表发出启动新Activity请求的Activity token

String resultWho:

target != null ? target.mEmbeddedID : null

这里的target不为null,但是target.mEmbddedID为null

 int requestCode:

 -1

int startFlags:

0

String profileFile:

null

ParcelFileDescriptor profileFd:

null

WaitResult outResult:

null

Configuration config:

null

Bundle options:

 

这个函数所做的主要工作:

通过调用enforceNotIsolatedCaller()判断当前的请求者Binder.getCallingUid()是否允许启动Activity

ActivityManagerService.java

public final int startActivity(IApplicationThread caller,

            Intent intent, String resolvedType, IBinderresultTo,

            StringresultWho, int requestCode, int startFlags,

            StringprofileFile, ParcelFileDescriptor profileFd, Bundle options) {

       enforceNotIsolatedCaller("startActivity");//安全检查,用户是否已经被隔离,这里的用户id是通过Binder.getCallingUid()取得的

        int userId =0;

        if(intent.getCategories() != null &&intent.getCategories().contains(Intent.CATEGORY_HOME)) {

            //Requesting home, set the identity to the current user

            // HACK!

            userId =mCurrentUserId;

        } else {

            // TODO:Fix this in a better way - calls coming from SystemUI should probably carry

            // thecurrent user's userId

            if(Binder.getCallingUid() < Process.FIRST_APPLICATION_UID) {

               userId = 0;

            } else {

               userId = Binder.getOrigCallingUser();

            }

        }

        returnmMainStack.startActivityMayWait(caller, -1, intent, resolvedType,

                resultTo, resultWho,requestCode, startFlags, profileFile, profileFd,

                null, null, options, userId);

    }

 

Step 8: ActivityStack.startActivityMayWait

此处传入的参数列表:

IApplicationThread caller:

ApplicationThread类型的Binder实体

int callingUid:

-1

Intent intent:

 

String resolvedType:

这里的intent.resolveTypeIfNeeded返回这个intent的MIME类型,在这个例子中,没有AndroidManifest.xml设置MainActivity的MIME类型,因此,这里返回null。

 IBinder resultTo:

一个代表发出启动新Activity请求的Activity token

String resultWho:

target != null ? target.mEmbeddedID : null

这里的target不为null,但是target.mEmbddedID为null

 int requestCode:

 -1

int startFlags:

0

String profileFile:

null

ParcelFileDescriptor profileFd:

null

WaitResult outResult:

null

Configuration config:

null

Bundle options:

 

int userId:

Binder.getOrigCallingUser()

这个函数所做的主要工作:

调用resolveActivity()从PackageMangerService那里获取activity信息aInfo

 通过调用ActivityManagementService.startActivityMayWait()获取于此Activity相关的Application信息,并且保存在aInfo.applicationInfo中

final int startActivityMayWait(IApplicationThread caller,int callingUid,

            Intentintent, String resolvedType, IBinder resultTo,

            StringresultWho, int requestCode, int startFlags, String profileFile,

           ParcelFileDescriptor profileFd, WaitResult outResult, Configurationconfig,

            Bundle options, int userId) {

        // Refusepossible leaked file descriptors

        if (intent !=null && intent.hasFileDescriptors()) {

            throw newIllegalArgumentException("File descriptors passed in Intent");

        }

        booleancomponentSpecified = intent.getComponent() != null; // componentSpecified=true

 

        // Don'tmodify the client's object!

        intent = newIntent(intent);

 

        // Collectinformation about the target of the Intent.

        ActivityInfoaInfo = resolveActivity(intent, resolvedType, startFlags,

               profileFile, profileFd, userId);

        if (aInfo !=null && mService.isSingleton(aInfo.processName, aInfo.applicationInfo)){

            userId =0;

        }

        aInfo =mService.getActivityInfoForUser(aInfo, userId);

 

        synchronized(mService) {

            intcallingPid;

//传入的参数callingUid=-1

            if(callingUid >= 0) {

               callingPid = -1;

            } else if(caller == null) {

               callingPid = Binder.getCallingPid();

               callingUid = Binder.getCallingUid();

            } else {

               callingPid = callingUid = -1;

            }

            //传入参数config=null,所以mConfigWillChange=false

           mConfigWillChange = config != null

                   && mService.mConfiguration.diff(config) != 0;

            if(DEBUG_CONFIGURATION) Slog.v(TAG,

                   "Starting activity when config will change = " +mConfigWillChange);

            //http://blog.chinaunix.net/uid-12469487-id-3050585.html

            finallong origId = Binder.clearCallingIdentity();

            //对于这个判断(aInfo.applicationInfo.flags&ApplicationInfo.FLAG_CANT_SAVE_STATE) != 0,因为aInfo.applicationInfo.flags中不包含ApplicationInfo.FLAG_CANT_SAVE_STATE标志,所以它的值=false

//下面这段不会执行

            if(mMainStack && aInfo != null &&

                   (aInfo.applicationInfo.flags&ApplicationInfo.FLAG_CANT_SAVE_STATE)!= 0) {

                …

            }

           

            int res =startActivityLocked(caller, intent, resolvedType,

                    aInfo, resultTo, resultWho,requestCode, callingPid, callingUid,

                    startFlags, options,componentSpecified, null);

           

            if(mConfigWillChange && mMainStack) { // mConfigWillChange=false

                // Ifthe caller also wants to switch to a new configuration,

                // do so now.  This allows a clean switch, as we are waiting

                //for the current activity to pause (so we will not destroy

                //it), and have not yet started the next activity.

               mService.enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION,

                       "updateConfiguration()");

               mConfigWillChange = false;

                if(DEBUG_CONFIGURATION) Slog.v(TAG,

                       "Updating to new configuration after starting activity.");

               mService.updateConfigurationLocked(config, null, false, false);

            }

           

           Binder.restoreCallingIdentity(origId);

           

            if(outResult != null) {  //outResult=null

                outResult.result = res;

                if(res == ActivityManager.START_SUCCESS) {

                   mWaitingActivityLaunched.add(outResult);

                   do {

                       try {

                           mService.wait();

                       } catch (InterruptedException e) {

                       }

                    }while (!outResult.timeout && outResult.who == null);

                }else if (res == ActivityManager.START_TASK_TO_FRONT) {

                    ActivityRecordr = this.topRunningActivityLocked(null);

                   if (r.nowVisible) {

                       outResult.timeout = false;

                       outResult.who = new ComponentName(r.info.packageName, r.info.name);

                        outResult.totalTime = 0;

                       outResult.thisTime = 0;

                    }else {

                       outResult.thisTime = SystemClock.uptimeMillis();

                       mWaitingActivityVisible.add(outResult);

                        do {

                           try {

                               mService.wait();

                           } catch (InterruptedException e) {

                           }

                       } while (!outResult.timeout && outResult.who == null);

                    }

                }

            }

           

            returnres;

        }

    }

Step 9: ActivityStack. startActivityLocked

这个函数所做的主要工作:

从传进来的参数caller得到调用者的进程信息,并保存在callerApp变量中,这里就是Launcher应用程序的进程信息了。

前面说过,参数resultTo是Launcher这个Activity里面的一个Binder对象,通过它可以获得Launcher这个启动者Activity的相关信息,保存在sourceRecord变量中。

通过launchFlags = intent.getFlags()获取launchFlags信息,对launchFlags中的各种flag进行判断和处理。针对当前场景下,我们只设置了NEW_TASK标志,所以这里的判断和检查都不适用。

判断callingPid和callingUid是否有权限启动新的Activity。

创建即将要启动的Activity的相关信息,并保存在(ActivityRecord)r变量中

如果有pending activities需要先处理,调用mService.doPendingActivityLaunchesLocked(),在doPendingActivityLaunchesLocked() 中会调用startActivityUncheckedLocked()进一步处理,也就是先启动pending acitivies。

上面的事情做完后,才执行startActivityUncheckedLocked()启动我们要启动的Activity。

2403    final intstartActivityLocked(IApplicationThread caller,

2404            Intent intent, String resolvedType,ActivityInfo aInfo, IBinder resultTo,

2405            String resultWho, int requestCode,

2406            int callingPid, int callingUid, intstartFlags, Bundle options,

2407            boolean componentSpecified,ActivityRecord[] outActivity) {

2408

2409        int err =ActivityManager.START_SUCCESS;

2410

2411       ProcessRecord callerApp = null;

//从传进来的参数caller得到调用者的进程信息,并保存在callerApp变量中,这里就是Launcher应用程序的进程信息了。

2412        if(caller != null) {

2413           callerApp = mService.getRecordForAppLocked(caller);

2414            if(callerApp != null) {

2415               callingPid = callerApp.pid;

2416                callingUid= callerApp.info.uid;

2417            }else {

2418               Slog.w(TAG, "Unable to find app for caller " + caller

2419                     + " (pid=" + callingPid + ") when starting: "

2420                     + intent.toString());

2421                err =ActivityManager.START_PERMISSION_DENIED;

2422            }

2423        }

2424

2425        if (err== ActivityManager.START_SUCCESS) {

2426            finalint userId = aInfo != null ? UserId.getUserId(aInfo.applicationInfo.uid) : 0;

2427           Slog.i(TAG, "START {" + intent.toShortString(true, true, true,false)

2428                   + " u=" + userId + "} from pid " + (callerApp !=null ? callerApp.pid : callingPid));

//更新env环境变量信息env->ReleaseStringUTFChars(activity, actStr);

2429           mActivityTrigger.activityStartTrigger(intent);

2430        }

2431

2432       ActivityRecord sourceRecord = null;

2433       ActivityRecord resultRecord = null;

//前面说过,参数resultTo是Launcher这个Activity里面的一个Binder对象,通过它可以获得Launcher这个Activity的相关信息,保存在sourceRecord变量中。

2434        if(resultTo != null) {    //resultTo !=null

2435            intindex = indexOfTokenLocked(resultTo);

2436            if(DEBUG_RESULTS) Slog.v(

2437               TAG, "Will send result to " + resultTo + " (index "+ index + ")");

2438            if(index >= 0) {

2439               sourceRecord = mHistory.get(index);

//resultCode=-1

2440               if (requestCode >= 0 && !sourceRecord.finishing) {

2441                   resultRecord = sourceRecord;

2442                }

2443            }

2444        }

2445

2446        intlaunchFlags = intent.getFlags();

2447      //条件不满足

2448        if((launchFlags&Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0

2449               && sourceRecord != null) {

2464        }

2465    //条件不满足

2466        if (err== ActivityManager.START_SUCCESS && intent.getComponent() == null) {

2467            // We couldn't find a class that canhandle the given Intent.

2468            //That's the end of that!

2469            err =ActivityManager.START_INTENT_NOT_RESOLVED;

2470        }

2471 //ainfo != null

2472        if (err== ActivityManager.START_SUCCESS && aInfo == null) {

2473            // Wecouldn't find the specific class specified in the Intent.

2474            //Also the end of the line.

2475            err =ActivityManager.START_CLASS_NOT_FOUND;

2476        }

2477 //条件不满足

2478        if (err!= ActivityManager.START_SUCCESS) {

2479            if(resultRecord != null) {

2480               sendActivityResultLocked(-1,

2481                   resultRecord, resultWho, requestCode,

2482                   Activity.RESULT_CANCELED, null);

2483            }

2484           mDismissKeyguardOnNextActivity = false;

2485           ActivityOptions.abort(options);

2486           return err;

2487        }

2488 //此处的启动当然有权限

2489        final intstartAnyPerm = mService.checkPermission(

2490               START_ANY_ACTIVITY, callingPid, callingUid);

2491        final intcomponentPerm = mService.checkComponentPermission(aInfo.permission, callingPid,

2492               callingUid, aInfo.applicationInfo.uid, aInfo.exported);

2493        if (startAnyPerm != PERMISSION_GRANTED&& componentPerm != PERMISSION_GRANTED) {

2494            if(resultRecord != null) {

2495               sendActivityResultLocked(-1,

2496                   resultRecord, resultWho, requestCode,

2497                    Activity.RESULT_CANCELED, null);

2498            }

2499           mDismissKeyguardOnNextActivity = false;

2500           String msg;

2501            if(!aInfo.exported) {

2502               msg = "Permission Denial: starting " + intent.toString()

2503                       + " from " + callerApp + " (pid=" + callingPid

2504                       + ", uid=" + callingUid + ")"

2505                       + " not exported from uid " + aInfo.applicationInfo.uid;

2506            }else {

2507                msg = "Permission Denial:starting " + intent.toString()

2508                       + " from " + callerApp + " (pid=" + callingPid

2509                       + ", uid=" + callingUid + ")"

2510                       + " requires " + aInfo.permission;

2511            }

2512           Slog.w(TAG, msg);

2513            thrownew SecurityException(msg);

2514        }

2515

2516        if(mMainStack) {

2517            if(mService.mController != null) {

2518               boolean abort = false;

2519               try {

2520                   // The Intent we give to the watcher has the extra data

2521                   // stripped off, since it can contain private information.

2522                   Intent watchIntent = intent.cloneFilter();

//mService.mController.activityStarting() always return true

2523                   abort = !mService.mController.activityStarting(watchIntent,

2524                           aInfo.applicationInfo.packageName);

2525                }catch (RemoteException e) {

2526                   mService.mController = null;

2527                }

2528

2529               if (abort) {

2530                   if (resultRecord != null) {

2531                       sendActivityResultLocked(-1,

2532                           resultRecord, resultWho,requestCode,

2533                           Activity.RESULT_CANCELED, null);

2534                   }

2535                   // We pretend to the caller that it was really started, but

2536                   // they will just get a cancel result.

2537                   mDismissKeyguardOnNextActivity = false;

2538                   ActivityOptions.abort(options);

2539                   return ActivityManager.START_SUCCESS;

2540                }

2541            }

2542        }

2543  //创建即将要启动的Activity的相关信息,并保存在r变量中

2544       ActivityRecord r = new ActivityRecord(mService, this, callerApp,callingUid,

2545               intent, resolvedType, aInfo, mService.mConfiguration,

2546               resultRecord, resultWho, requestCode, componentSpecified);

2547        if(outActivity != null) {

2548           outActivity[0] = r;

2549        }

2550 // 此场景下 条件不满足,因为mService.checkAppSwitchAllowedLocked() should  return true

2551        if(mMainStack) {

2552            if(mResumedActivity == null

2553                   || mResumedActivity.info.applicationInfo.uid != callingUid) {

2554               if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid,"Activity start")) {

2555                   PendingActivityLaunch pal = new PendingActivityLaunch();

2556                   pal.r = r;

2557                   pal.sourceRecord = sourceRecord;

2558                   pal.startFlags = startFlags;

2559                   mService.mPendingActivityLaunches.add(pal);

2560                    mDismissKeyguardOnNextActivity = false;

2561                   ActivityOptions.abort(options);

2562                   return ActivityManager.START_SWITCHES_CANCELED;

2563                }

2564            }

2565//是否允许Activity switch,这个变量在ActivityManagerService.java.stopAppSwitches()中被设置成false,与之个的一个变量是APP_SWITCH_DELAY_TIME,关于它的解释如下:

// Amount of time after a call to stopAppSwitches() duringwhich we will prevent further untrusted switches from happening.

2566            if(mService.mDidAppSwitch) {

2567               // This is the second allowed switch since we stopped switches,

2568               // so now just generally allow switches. Use case: user presses

2569               // home (switches disabled, switch to home, mDidAppSwitch now true);

2570               // user taps a home icon (coming from home so allowed, we hit here

2571               // and now allow anyone to switch again).

2572               mService.mAppSwitchesAllowedTime = 0;

2573            }else {

2574               mService.mDidAppSwitch = true;

2575            }

2576  //如果有pending activities需要先处理

2577           mService.doPendingActivityLaunchesLocked(false);

2578        }

2579

2580        err = startActivityUncheckedLocked(r,sourceRecord,

2581                startFlags, true, options);

2582        if(mDismissKeyguardOnNextActivity && mPausingActivity == null) {

2583            //Someone asked to have the keyguard dismissed on the next

2584            //activity start, but we are not actually doing an activity

2585            //switch...  just dismiss the keyguard now,because we

2586            //probably want to see whatever is behind it.

2587           mDismissKeyguardOnNextActivity = false;

2588           mService.mWindowManager.dismissKeyguard();

2589        }

2590        returnerr;

2591    }

 

Step 10: ActivityStack.startActivityUncheckedLocked

这个函数所做的主要工作:

这个函数的最重要工作就是确定Activity运行的Task,各种情况判断比较多,但是对于本文涉及的场景就简单些,因为我们的Activity在开始阶段的launcher.startActivity中就已经为intent设置了FLAG_ACTIVITY_NEW_TASK标志,所以最终就会创建一个新的Task。

首先获得intent的标志值,保存在launchFlags变量中。

根据Activity.launchMode来判断是否需要给当前Activity. launchFlags添加Intent.FLAG_ACTIVITY_NEW_TASK标志。为(target activity)r.launchFlags添加FLAG_ACTIVITY_NEW_TASK标志的条件包括:

1)sourceRecord == null

2)sourceRecord.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE

3)r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE ||r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK

 

满足(r.resultTo != null &&(launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK) != 0)条件,立刻返回结果给调用者。因为startActivityForResult()是不能跨越不同task的。

调用findTaskLocked()搜索当前有没有Task可以用来执行这个Activity。由于r.launchMode的值不为ActivityInfo.LAUNCH_SINGLE_INSTANCE,因此,它通过findTaskLocked函数来查找存不存这样的Task。如果是第一次运行,返回的结果是null,(只有当目标Activity存在而且在task顶端才满足查找条件)即taskTop为null,因此,需要创建一个新的Task来启动这个Activity。

检查需要启动的Activity与现在的topactivity是否相同?不同,因此不需要执行resume top activtity,进一步分析:resume top activity还必须满足其他条件:launchMode中还必须设置了FLAG_ACTIVITY_SINGLE_TOP或者.LAUNCH_SINGLE_TOP或者LAUNCH_SINGLE_TASK。

如果没有可以可用的task,给当前的Activity创建和赋值一个task.

r.setTask(new TaskRecord(mService.mCurTask, r.info, intent),null, true);

调用startActivityLocked(r, newTask,doResume, keepCurTransition, options)继续执行。

 

2603    final intstartActivityUncheckedLocked(ActivityRecord r,

2604            ActivityRecord sourceRecord, intstartFlags, boolean doResume,

2605            Bundle options) {

//doReume=true

2606        finalIntent intent = r.intent;

2607        final intcallingUid = r.launchedFromUid;

2608        final intuserId = r.userId;

2609 //函数首先获得intent的标志值,保存在launchFlags变量中。

2610        intlaunchFlags = intent.getFlags();

2611 //这个intent的标志值的位Intent.FLAG_ACTIVITY_NO_USER_ACTION没有置位,因此 ,成员变量mUserLeaving的值为true。

2612        // We'llinvoke onUserLeaving before onPause only if the launching

2613        //activity did not explicitly state that this is an automated launch.

2614       mUserLeaving = (launchFlags&Intent.FLAG_ACTIVITY_NO_USER_ACTION) ==0; // mUserLeaving=true

2615        if(DEBUG_USER_LEAVING) Slog.v(TAG,

2616               "startActivity() => mUserLeaving=" + mUserLeaving);

2617

2618        // If thecaller has asked not to resume at this point, we make note

2619        // ofthis in the record so that we can skip it when trying to find

2620        // thetop running activity.

2621        if(!doResume) {

2622           r.delayedResume = true;

2623        }

2624     //没有设置这个标志,所以notTop=null

2625       ActivityRecord notTop =(launchFlags&Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)

2626               != 0 ? r : null;

2627 //startFlags = 0 这个参数是从上面一路传下来的

2628        // If theonlyIfNeeded flag is set, then we can do this if the activity

2629        // beinglaunched is the same as the one making the call...  or, as

2630        // aspecial case, if we do not know the caller then we count the

2631        //current top activity as the caller.

2632        if ((startFlags&ActivityManager.START_FLAG_ONLY_IF_NEEDED)!= 0) {

2633           ActivityRecord checkedCaller = sourceRecord;

2634            if(checkedCaller == null) {

2635               checkedCaller = topRunningNonDelayedActivityLocked(notTop);

2636            }

2637            if(!checkedCaller.realActivity.equals(r.realActivity)) {

2638               // Caller is not the same as launcher, so always needed.

2639               startFlags &= ~ActivityManager.START_FLAG_ONLY_IF_NEEDED;

2640            }

2641        }

2642

2643        if(sourceRecord == null) {

2644            //This activity is not being started from another...  in this

2645            //case we -always- start a new task.

2646            if((launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {

2647               Slog.w(TAG, "startActivity called from non-Activity context;forcing Intent.FLAG_ACTIVITY_NEW_TASK for: "

2648                     + intent);

2649               launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;

2650            }

2651        } else if(sourceRecord.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {

2652            //The original activity who is starting us is running as a single

2653            //instance...  this new activity it isstarting must go on its

2654            // own task.

2655           launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;

2656        } else if(r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE

2657               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) {

2658            //The activity being started is a single instance...  it always

2659            //gets launched into its own task.

2660           launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;

2661        }

2662//我们刚好满足了下面这个条件,所以launcher就会通过onActivityResult()收到启动新activity结果的回调

2663        if(r.resultTo != null && (launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK)!= 0) {

2664            //For whatever reason this activity is being launched into a new

2665            // task...  yet the caller has requested a resultback.  Well, that

2666            // ispretty messed up, so instead immediately send back a cancel

2667            //and let the new task continue launched as normal without a

2668            //dependency on its originator.

2669           Slog.w(TAG, "Activity is launching as a new task, so cancellingactivity result.");

2670           sendActivityResultLocked(-1,

2671                   r.resultTo, r.resultWho, r.requestCode,

2672               Activity.RESULT_CANCELED, null);

2673           r.resultTo = null;

2674        }

2675

2676        booleanaddingToTask = false;

2677        booleanmovedHome = false;

2678       TaskRecord reuseTask = null;

//在要启动new Activity之初的Launcher.startActivity中,Intent.FLAG_ACTIVITY_NEW_TASK就已经被设置了,因此下面的语句将被执行

//  这段代码的逻辑是,当前有没有Task可以用来执行这个Activity。由于r.launchMode的值不为 ActivityInfo.LAUNCH_SINGLE_INSTANCE,因此,它通过findTaskLocked函数来查找存不存这样的Task, 这里返回的结果是null,即taskTop为null,因此,需要创建一个新的Task来启动这个Activity。

2679        if(((launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK) != 0 &&

2680               (launchFlags&Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0)

2681               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK

2682               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {

2683            // If bring to front is requested,and no result is requested, and

2684            // we can find a task that wasstarted with this same

2685            // component, then instead oflaunching bring that one to the front.

2686            if (r.resultTo == null) {

2687                // See if there is a task tobring to the front.  If this is

2688                // a SINGLE_INSTANCE activity,there can be one and only one

2689                // instance of it in thehistory, and it is always in its own

2690                // unique task, so we do aspecial search.

2691               ActivityRecord taskTop = r.launchMode !=ActivityInfo.LAUNCH_SINGLE_INSTANCE

2692                       ? findTaskLocked(intent, r.info)//条件是只有当目标Activity存在而且在task顶端。请牢记这个条件,只有这样后续的moveTaskToFrontLocked()才会成立

2693                       : findActivityLocked(intent, r.info);

2694               if (taskTop != null) { //当前场景中,我们假设这是第一次启动,所以taskTop=null

2695                   if (taskTop.task.intent == null) {

2696                       // This task was started because of movement of

2697                       // the activity based on affinity... now that we

2698                       // are actually launching it, we can assign the

2699                        // base intent.

2700                       taskTop.task.setIntent(intent, r.info);

2701                   }

2702                   // If the target task is not in the front, then we need

2703                   // to bring it to the front... except...  well, with

2704                   // SINGLE_TASK_LAUNCH it's not entirely clear.  We'd like

2705                   // to have the same behavior as if a new instance was

2706                   // being started, which means not bringing it to the front

2707                   // if the caller is not itself in the front.

2708                   ActivityRecord curTop = topRunningNonDelayedActivityLocked(notTop);

2709                   if (curTop != null && curTop.task != taskTop.task) {//如果当前最顶端运行的activity.task不是我们要恢复的task

2710                       r.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);

2711                       boolean callerAtFront = sourceRecord == null

2712                                || curTop.task== sourceRecord.task;

2713                       if (callerAtFront) {//如果是调用者在最顶端

2714                            // We really dowant to push this one into the

2715                            // user's face,right now.

2716                            movedHome = true;

2717                           moveHomeToFrontFromLaunchLocked(launchFlags);

2718                           moveTaskToFrontLocked(taskTop.task, r, options);

2719                            options = null;

2720                       }

2721                    }

2722                   // If the caller has requested that the target task be

2723                   // reset, then do so.

2724                   if ((launchFlags&Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {

2725                       taskTop =resetTaskIfNeededLocked(taskTop, r);

2726                   }

2727                   if ((startFlags&ActivityManager.START_FLAG_ONLY_IF_NEEDED)  != 0) {

2728                       // We don't need to start a new activity, and

2729                        // the client said not to doanything if that

2730                       // is the case, so this is it! And for paranoia, make

2731                       // sure we have correctly resumed the top activity.

2732                       if (doResume) {

2733                           resumeTopActivityLocked(null, options);

2734                       } else {

2735                           ActivityOptions.abort(options);

2736                       }

2737                       return ActivityManager.START_RETURN_INTENT_TO_CALLER;

2738                   }

2739                   if ((launchFlags &

2740                           (Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK))

2741                            == (Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK)){

2742                       // The caller has requested to completely replace any

2743                       // existing task with its new activity. Well that should

2744                       // not be too hard...

2745                       reuseTask = taskTop.task;

2746                       performClearTaskLocked(taskTop.task.taskId);

2747                       reuseTask.setIntent(r.intent, r.info);

2748                   } else if ((launchFlags&Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0

2749                            || r.launchMode ==ActivityInfo.LAUNCH_SINGLE_TASK

2750                            || r.launchMode ==ActivityInfo.LAUNCH_SINGLE_INSTANCE) {

2751                       // In this situation we want to remove all activities

2752                       // from the task up to the one being started.  In most

2753                       // cases this means we are resetting the task to its

2754                       // initial state.

2755                        ActivityRecord top =performClearTaskLocked(

2756                               taskTop.task.taskId, r, launchFlags);

2757                       if (top != null) {

2758                            if(top.frontOfTask) {

2759                                // Activity aliases may meanwe use different

2760                                // intents forthe top activity, so make sure

2761                                // the task nowhas the identity of the new

2762                                // intent.

2763                               top.task.setIntent(r.intent, r.info);

2764                            }

2765                           logStartActivity(EventLogTags.AM_NEW_INTENT, r, top.task);

2766                            top.deliverNewIntentLocked(callingUid,r.intent);

2767                       } else {

2768                            // A special case:we need to

2769                            // start theactivity because it is not currently

2770                            // running, and thecaller has asked to clear the

2771                            // current task tohave this activity at the top.

2772                            addingToTask =true;

2773                            // Now pretend likethis activity is being started

2774                            // by the top ofits task, so it is put in the

2775                            // right place.

2776                            sourceRecord =taskTop;

2777                       }

2778                   } else if (r.realActivity.equals(taskTop.task.realActivity)) {

2779                       // In this case the top activity on the task is the

2780                       // same as the one being launched, so we take that

2781                       // as a request to bring the task to the foreground.

2782                       // If the top activity in the task is the root

2783                       // activity, deliver this new intent to it if it

2784                       // desires.

2785                       if((launchFlags&Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0

2786                                &&taskTop.realActivity.equals(r.realActivity)) {

2787                           logStartActivity(EventLogTags.AM_NEW_INTENT, r, taskTop.task);

2788                            if (taskTop.frontOfTask) {

2789                               taskTop.task.setIntent(r.intent, r.info);

2790                            }

2791                           taskTop.deliverNewIntentLocked(callingUid, r.intent);

2792                        } else if(!r.intent.filterEquals(taskTop.task.intent)) {

2793                            // In this case weare launching the root activity

2794                            // of the task, butwith a different intent.  We

2795                           // should start a newinstance on top.

2796                            addingToTask =true;

2797                            sourceRecord =taskTop;

2798                       }

2799                   } else if ((launchFlags&Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)== 0) {

2800                   

<think>首先,用户的问题是关于无法启动企业微信应用程序(com.tencent.wework)在Android上,使用Appium进行自动化测试配置。用户提到了"Appium自动化测试配置",所以我需要基于Appium来解决这个问题。 从系统级指令,我需要: - 所有行内数学表达式用$...$,但在这个上下文中,可能没有数学表达式,所以我可以忽略。 - 独立公式用$$...$$,但同样,可能不需要。 - LaTeX语法正确:确保任何公式正确。 - 使用中文回答:整个回答用中文。 - 生成相关问题:在回答结束后,生成相关问题。 - 引用段落末尾添加引用标识:如[^1],但用户提供了引用,我需要参考它们。 用户提供的引用: - 引用[1]: Appium启动参数设置,具体针对企业微信的配置。 - 引用[2]: APP自动化之PO,描述了测试框架的结构。 - 引用[3]: 一个测试用例示例,使用Appium测试添加联系人。 用户的问题是:"无法启动 com.tencent.wework 应用程序 Android 解决方案 Appium 自动化测试配置"。所以,核心是解决启动失败的问题。 可能的启动失败原因: 1. Appium配置错误:例如,启动参数不正确。 2. 设备连接问题:设备未连接或ADB问题。 3. 应用未安装或版本不匹配。 4. Appium版本或依赖问题。 5. 权限问题。 基于引用[1],启动参数是: { "platformName": "Android", "platformVersion": "9.0", "appPackage": "com.tencent.wework", "appActivity": ".launch.WwMainActivity", "deviceName": "192.168.56.101:5555", "noReset": "true", "dontStopAppOnReset": "true" } 我需要检查这些参数是否正确。 常见解决方案: - 确保设备连接:使用`adb devices`检查设备是否列出。 - 验证appPackage和appActivity:使用`adb shell dumpsys window | findstr mCurrentFocus`获取当前活动。 - 更新Appium和驱动程序。 - 检查日志以获取错误信息。 回答结构: 1. 理解问题:确认用户遇到启动失败。 2. 分析可能原因。 3. 提供解决方案步骤。 4. 建议检查引用中的配置。 5. 生成相关问题。 由于用户提供了引用,我应该整合它们: - 引用[1]直接相关,是启动参数。 - 引用[2]和[3]是关于测试框架和用例,可能间接相关,但用户问题主要是启动失败,所以重点在配置。 最终回答: - 用中文。 - 结构清晰:介绍问题、原因、解决步骤。 - 结束时生成相关问题。 生成相关问题: - 基于主题:Appium配置、错误处理、企业微信特定问题等。 例如: 1. 如何获取企业微信的正确appActivity? 2. Appium启动失败常见错误有哪些? 3. 如何调试Appium会话启动问题? 确保回答真实可靠:基于Appium文档和常见实践。</think>### 解决 Appium 无法启动企业微信(com.tencent.wework)的 Android 自动化测试问题 当您在 Appium 自动化测试中遇到无法启动企业微信(`com.tencent.wework`)应用程序的问题时,这通常是由于配置错误、设备连接问题或应用环境不一致导致的。以下我将逐步分析原因并提供解决方案,帮助您快速修复问题。解决过程基于 Appium 的 Android 驱动机制和常见实践,同时参考您提供的引用内容。 #### 问题分析与常见原因 1. **启动参数配置错误**:Appium 的 Desired Capabilities(启动参数)必须精确匹配设备信息和应用详情。例如,`appPackage` 或 `appActivity` 值错误会导致 Appium 无法识别应用入口点[^1]。 2 **设备连接问题**:设备未通过 ADB 正确连接,或 `deviceName` 参数(如 `192.168.56.101:5555`)与实际设备 IP 或序列号不匹配。 3 **应用环境问题**:企业微信未安装、版本不兼容,或 Android 系统权限(如 USB 调试未开启)阻止了启动。 4 **Appium 或依赖项问题**:Appium 服务器版本过旧、WebDriverAgent 未正确初始化,或缺少必要的 SDK 工具。 5 **日志信息缺失**:未查看 Appium 日志,导致无法定位具体错误。 #### 解决方案步骤 按照以下步骤逐一排查和修复问题。每个步骤都基于实际测试经验,确保可靠性。 1. **验证设备连接和 ADB 状态** - 确保设备已通过 USB 或网络连接,并在开发者选项中启用 "USB 调试"。 - 打开命令行,运行 `adb devices` 检查设备是否列出。如果设备未显示,尝试重启 ADB 服务: ```bash adb kill-server adb start-server adb devices ``` - 确认 `deviceName` 参数与 `adb devices` 列出的设备 ID 一致。例如,如果设备显示为 `emulator-5554`,则更新启动参数中的 `deviceName`[^1]。 2. **检查并修正 Appium 启动参数** 参考您提供的引用[1],企业微信的标准启动参数为: ```json { "platformName": "Android", "platformVersion": "9.0", // 必须与设备 Android 版本一致 "appPackage": "com.tencent.wework", "appActivity": ".launch.WwMainActivity", "deviceName": "192.168.56.101:5555", // 替换为您的设备 ID "noReset": "true", // 防止每次重置应用 "dontStopAppOnReset": "true" // 避免停止应用 } ``` - **关键修正点**: - **验证 `appActivity`**:企业微信的入口 Activity 可能因版本更新而变化。使用 ADB 获取当前值: ```bash adb shell dumpsys window | findstr mCurrentFocus ``` 如果输出类似 `com.tencent.wework/.launch.LaunchSplashActivity`,则更新 `appActivity` 为 `.launch.LaunchSplashActivity`。 - **更新 `platformVersion`**:确保与设备系统版本匹配(如 Android 10 需改为 `"10.0"`)。 - **添加必要参数**:如果应用未安装,添加 `"app": "/path/to/wework.apk"`;如果权限不足,添加 `"autoGrantPermissions": "true"`。 3. **确保应用环境就绪** - 安装企业微信最新版:从官方渠道下载 APK,并使用 `adb install wework.apk` 手动安装。 - 检查应用权限:在设备设置中,为企业微信启用所有必要权限(如存储、位置)。 - 清除应用缓存:运行 `adb shell pm clear com.tencent.wework` 避免旧数据干扰。 4. **调试 Appium 会话** - 启动 Appium 服务器时启用详细日志: ```bash appium --log-level debug ``` - 运行测试脚本,捕获日志中的错误关键字(如 `NoSuchActivity` 或 `Device not found`)。 - 常见错误处理: - 如果出现 `Original error: Could not find a connected Android device`,更新 ADB 或重启设备。 - 如果出现 `An unknown server-side error occurred`,升级 Appium 到最新版本(如 v2.0+)并安装最新驱动程序: ```bash npm install -g appium appium driver install uiautomator2 ``` 5. **参考测试框架集成(基于引用[2]和[3])** 如果您使用 Page Object 模式(如引用[2]所述),确保基础类正确封装了启动逻辑。参考引用[3]的用例结构: ```python from app.page.app import AppPage class TestContact: def setup(self): self.app = AppPage() self.main = self.app.start().goto_main() # 确保 start() 方法正确调用 Appium ``` - 在 `AppPage` 类中,验证启动参数是否动态加载(如从配置文件读取)。 - 添加异常处理,在失败时捕获日志和截图(如引用[2]中提到的 `PNG` 目录)。 #### 预防措施 - **定期更新**:保持 Appium、ADB 和企业微信 APK 为最新版本。 - **使用模拟器测试**:如果真机不稳定,改用 Android Studio 模拟器,并设置相同参数。 - **文档参考**:Appium 官方文档([appium.io](https://appium.io/))提供完整配置指南。 通过以上步骤,大多数启动失败问题都能解决。如果问题仍存在,请提供 Appium 日志片段,我会进一步协助分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值