Activity的启动流程这一篇够了

来了小伙子,先自我介绍一下吧

  • 我叫***, 我精通Android系统的.....

什么?你精通Android系统?来,你给我说下Activity的启动流程。

Activity的启动过程是系统中比较有代表意义的过程,涉及到了各个进程之间的相互交互,以及生命周期的回调控制,这也是为什么在面试过程出现频率这么高的原因之一。

Activity的启动流程在API28以后变成了事务启动的方式,相比之前版本的逻辑有了一些变化,但是万变不离其宗,大致流程还是类似的,只是增加了一些类,用来更好的划分职责,更优的处理逻辑。


在开始之前,我们先简单普及下基础知识。

Activity 启动主要涉及到3个进程。

    1. 系统进程 SystemServer (负责管理整个framework,是Zygote孵化的第一个进程)
    1. App进程(App进程是用户点击桌面icon时,通过Launcher进程请求SystemServer,再调用Zygote孵化的)
    1. Zygote进程(所有进程孵化都由Zygote完成,而Zygote是init进程的子进程,也由init进程孵化)
    1. 如果点击桌面icon启动还会涉及到 Launcher进程(Zygote孵化的第一个应用进程)

进程之间靠什么通信?

我们都知道进程与进程之间是数据隔离的,无法相互访问数据,所以进程之间通信是靠Binder来完成的。

面试官可能会问你 为什么会用Binder通信,Binder相比Socket有什么优势呢?

  • 我想都没想直接说 1次拷贝,因为1次拷贝啊

然而果然被追问了 为啥Binder能做到1次拷贝,而其他的技术是2次拷贝

  • 我当时比较年轻,虽然我不知道具体细节,但是我知道内存映射。 我直接拍脑门就扯 因为Binder在server端于内核中通过mmap技术建立了内存映射,当我们Client与Server通信的时候,只需要把Client端的通信数据拷贝到内核中与Server映射好的内存区域就相当于拷贝到Server端了........

好,可以描述下具体的映射怎么做的么?....

  • 大大大哥,我真不知道了。。。。

Activity启动流程主要包含几步?

我们以点击Launcher的一个icon为开始,整体扯一下Activity的启动过程,桌面其实就是LauncherApp的一个Activity

    1. 当点击Launcher的icon开始,Launcher进程会像AMS发送点击icon的启动信息(这些信息就是在AndroidMainifest.xml中标签定义的启动信息,数据由PackageManagerService解析出来)
    1. AMS收到信息后会先后经过ActivityTaskManagerService->ActivityStartController->ActivityStarter内部类Request,然后把信息存到Request中,并通知Launcher进程让Activity休眠(补充个小知识点,这个过程会检测Activity在AndroidMainifest.xml的注册,如果没有注册就报错了)
    1. Launcher进程的ApplicationThread对象收到消息后调用handlePauseActivity()进行暂停,并通知AMS已经暂停。

    实现细节:ActivityThread.sendMessage()通过ActivityThread的H类发送Handler消息,然后触发 mTransactionExecutor.execute(transaction), 执行过程中依赖ActivityClientRecord.mLifecycleState数值并通过ClientTransactionHandler抽象类的实现(ActivityThread)进行分发。

    注 :ActivityClientRecord.mLifecycleState(-1 ~ 7分别代表 UNDEFINED, PRE_ON_CREATE, ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY, ON_RESTART)

    1. AMS收到Launcher的已暂停消息后,会检查要启动的Activity所在的进程是否已经启动了,如果已经启动了就打开,如果未启动则通过Process.start(android.app.ActivityThread)来启动一个新的进程。
    1. 进程创建好以后,会调用ActivityThread.main(),初始化MainLooper,并创建Application对象。然后Instrumentation.newApplication()反射创建Application,创建ContextImpl通过Application的attach方法与Application进行绑定,最终会调用Instrumentation.callApplicationOnCreate执行Application的onCreate函数进行一些初始化的工作。完成后会通知AMS进程已经启动好了。

      通知过程:通过IActivityManager.attachApplication(IApplicationThread thread, long startSeq),将Application对象传入AMS

    1. AMS收到app进程启动成功的消息后,从ActivityTaskManagerService中取出对应的Activity启动信息, 并通过ApplicationThreadProxy对象,调用其scheduleTransaction(ClientTransaction transaction)方法,具体要启动的Activity都在ClientTransaction对象中。
    1. app进程的ApplicationThread收到消息后会调用ActiivtyThread.sendMessage(),通过H发送Handler消息,在handleMessage方法的内部又会调用 mTransactionExecutor.execute(transaction);具体参考第3步

最终调用performLaunchActivity方法创建activity和context并将其做关联,然后通过mInstrumentation.callActivityOnCreate()->Activity.performCreate()->Activity.onCreate()回调到了Activity的生命周期。


Activity启动过程主要涉及哪些类

  • 为了防止后续大量的源码分析过程中影响整体的链路关系,在分析完源码后,我总结了一下相关类,以及调用方法,具体看以下描述。

启动一个Activity一般通过startActivity()

startActivity(new Intent(OneActivity.this,TwoActivity.class));
  • Activity startActivity() startActivityForResult()

  • Instrumentation 用于实现应用程序检测代码的基类。当在打开程序指令的时候运行,这个类将在任何应用程- 序代码之前为您实例化,可以监视系统与应用程序的所有交互。在AndroidManifest.xml文件的标记。 execStartActivity()

  • ActivityManagerService startActivity() startActivityAsUser()

  • ActivityStarter 用于解释如何启动活动。此类记录所有逻辑,用于确定如何将意图和标志转换为Activity以及关联的任务和堆栈。 execute() startActivity() startActivityUnchecked()

  • ActivityStackSupervisor resumeFocusedStackTopActivityLocked()

  • ActivityStack 单个Activity堆栈的状态和管理 resumeTopActivityUncheckedLocked() resumeTopActivityInnerLocked()

  • ActivityStackSupervisor Activity堆栈管理 startSpecificActivityLocked() realStartActivityLocked()

  • ClientTransaction 一种容器,它保存一系列消息(比如声明周期的状态),这些消息可以发送给client。 ClientTransaction.obtain(app.thread, r.appToken)//初始化 addCallback((LaunchActivityItem.obtain(new Intent(r.intent),...)

  • ClientLifecycleManager //该类能够组合多个client生命周期转换请求/回调,并将它们作为单个事务执行 scheduleTransaction(clientTransaction)

  • ClientTransaction schedule()

  • ApplicationThread scheduleTransaction()

  • ActivityThread 它管理应用程序进程中主线程中执行的调度和执行活动、广播以及活动管理器请求的其他操作。 scheduleTransaction() sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);

  • ClientTransactionHandler //ActivityThread 继承 ClientTransactionHandler,所以调用了父类scheduleTransaction() scheduleTransaction()

  • TransactionExecutor 以正确的顺序管理事务执行 execute() executeCallbacks() transaction.getCallbacks().get(i).execute()

  • LaunchActivityItem 请求启动Activity execute()

  • ActivityThread handleLaunchActivity() performLaunchActivity()

  • Instrumentation callActivityOnCreate()

  • Activity onCreate()

 

关于Android进阶、架构设计、NDK、跨平台、底层源码,KT,Flutter,以及面试的资料在我的Github上面可自行查看

项目地址:Githubicon-default.png?t=LBL2https://github.com/hunanmaniu/AndroidNotes

 

源码层分析整个链路

我们先看下正常启动Activity的方式,一般我们都会通过以下的方式启动一个新的Activity。

startActivity(new Intent(OneActivity.this,TwoActivity.class));

其实这是在Activity中的调用方式,调用的即是父类Activity的startActivity()方法,因参数不同分为两个方法,具体如下

  @Override
    public void startActivity(Intent intent) {
        this.startActivity(intent, null);
    }

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

最终调用的是startActivityForResult()

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

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                //主要看这里mInstrumentation为Instrumentation对象
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
        } 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()

  public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
       ...
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            //核心在这一句
            int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }

ActivityManager.getService()这个获取的是谁? 逻辑是从IActivityManagerSingleton.get()获取,那IActivityManagerSingleton又是谁? IActivityManagerSingleton是这么定义的Singleton IActivityManagerSingleton get取出来的是IActivityManager,看这个大写I开头就知道是一个接口,实际调用过的是它的实现ActivityManagerService。 ActivityManagerService. 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());
    }

    @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) {
        return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
                resultWho, requestCode, startFlags, profilerInfo, bOptions, userId,
                true /*validateIncomingUser*/);
    }

    public final int startActivityAsUser(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, int userId,
            boolean validateIncomingUser) {
        enforceNotIsolatedCaller("startActivity");

        userId = mActivityStartController.checkTargetUser(userId, validateIncomingUser,
                Binder.getCallingPid(), Binder.getCallingUid(), "startActivityAsUser");

        // TODO: Switch to user app stacks here.
        return mActivityStartController.obtainStarter(intent, "startActivityAsUser")
                .setCaller(caller)
                .setCallingPackage(callingPackage)
                .setResolvedType(resolvedType)
                .setResultTo(resultTo)
                .setResultWho(resultWho)
                .setRequestCode(requestCode)
                .setStartFlags(startFlags)
                .setProfilerInfo(profilerInfo)
                .setActivityOptions(bOptions)
                .setMayWait(userId)
                .execute();

    }

mActivityStartController.obtainStarter实际调用的是ActivityStarter.execute(),连带调用到ActivityStarter.startActivity()

 private int startActivity(final ActivityRecord r, ActivityRecord sourceRecord,
                IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
                int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
                ActivityRecord[] outActivity) {
        int result = START_CANCELED;
        try {
            mService.mWindowManager.deferSurfaceLayout();
            result = startActivityUnchecked(r, sourceRecord, voiceSession, voiceInteractor,
                    startFlags, doResume, options, inTask, outActivity);
        } finally {
            ...     
        }
        postStartActivityProcessing(r, result, mTargetStack);
        return result;
    }

ActivityStarter.startActivityUnchecked()连带调用ActivityStackSupervisor.resumeFocusedStackTopActivityLocked();

 boolean resumeFocusedStackTopActivityLocked(
            ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) {

        if (!readyToResume()) {
            return false;
        }

        if (targetStack != null && isFocusedStack(targetStack)) {
            //主要看这里
            return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
        }

        final ActivityRecord r = mFocusedStack.topRunningActivityLocked();
        if (r == null || !r.isState(RESUMED)) {
            mFocusedStack.resumeTopActivityUncheckedLocked(null, null);
        } else if (r.isState(RESUMED)) {
            // Kick off any lingering app transitions form the MoveTaskToFront operation.
            mFocusedStack.executeAppTransition(targetOptions);
        }
        return false;
    }

targetStack为ActivityStack对象,ActivityStack.resumeTopActivityUncheckedLocked()

 boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) {
        if (mStackSupervisor.inResumeTopActivity) {
            // 防止递归的
            return false;
        }

        boolean result = false;
        try {
            // Protect against recursion.
            mStackSupervisor.inResumeTopActivity = true;
            //主要看这
            result = resumeTopActivityInnerLocked(prev, options);
            if (next == null || !next.canTurnScreenOn()) {
                checkReadyForSleep();
            }
        } finally {
            mStackSupervisor.inResumeTopActivity = false;
        }

        return result;
    }

ActivityStack.resumeTopActivityInnerLocked()调用了mStackSupervisor.startSpecificActivityLocked(next, true, true);其中mStackSupervisor为ActivityStackSupervisor。 ActivityStackSupervisor.startSpecificActivityLocked()中调用 ActivityStackSupervisor.realStartActivityLocked()

  final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
            boolean andResume, boolean checkConfig) throws RemoteException {
...
  // Schedule transaction.
   mService.getLifecycleManager().scheduleTransaction(clientTransaction);
}

注意这个clientTransaction对象,通过这种方式初始化

    //app.thread为IApplicationThread  
    final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,  r.appToken);

     // 注意下这个LaunchActivityItem.obtain
    clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
                        System.identityHashCode(r), r.info,
                        // TODO: Have this take the merged configuration instead of separate global
                        // and override configs.
                        mergedConfiguration.getGlobalConfiguration(),
                        mergedConfiguration.getOverrideConfiguration(), r.compat,
                        r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
                        r.persistentState, results, newIntents, mService.isNextTransitionForward(),
                        profilerInfo));

ClientLifecycleManager.scheduleTransaction(clientTransaction);

    void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
        final IApplicationThread client = transaction.getClient();
        transaction.schedule();
        if (!(client instanceof Binder)) { 
            transaction.recycle();
        }
    }

transaction.schedule();就要找到

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

mClient即是以上描述的ApplicationThread,因此我们跟进ApplicationThread.scheduleTransaction()

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

我靠,调用了ActivityThread.scheduleTransaction(transaction),但是ActivityThread并没有scheduleTransaction(),所以我们找他继承的类ClientTransactionHandler,发现ClientTransactionHandler果然有scheduleTransaction()

   /** Prepare and schedule transaction for execution. */
    void scheduleTransaction(ClientTransaction transaction) {
        transaction.preExecute(this);
        sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
    }

以上消息通过ActivityThread H对象进行发送具体解析也在ActivityThread中

  case EXECUTE_TRANSACTION:
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    mTransactionExecutor.execute(transaction);
                    if (isSystem()) { 
                        transaction.recycle();
                    }
                    // TODO(lifecycler): Recycle locally scheduled transactions.
                    break;

我们可以看下TransactionExecutor.execute(transaction);

    //首先,所有回调将按照它们在列表中出现的顺序执行。如果回调需要特定的执行前或执行后状态,
   //则客户端将相应地进行转换。然后客户端将循环到最终的生命周期状态(如果提供)。
   //否则,它将保持在回调所需的初始状态或最后状态。
    public void execute(ClientTransaction transaction) {
        final IBinder token = transaction.getActivityToken();
        log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token);

        executeCallbacks(transaction);

        executeLifecycleState(transaction);
        mPendingActions.clear();
        log("End resolving transaction");
    }

这里我们主要看下executeCallbacks()方法

  /** Transition to the final state if requested by the transaction. */
  public void executeCallbacks(ClientTransaction transaction) {
//transaction.getCallbacks()会获取clientTransaction.addCallbacks()的数据。
        final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
        if (callbacks == null) {
            // No callbacks to execute, return early.
            return;
        }
        log("Resolving callbacks");

        final IBinder token = transaction.getActivityToken();
        ActivityClientRecord r = mTransactionHandler.getActivityClient(token);

        // In case when post-execution state of the last callback matches the final state requested
        // for the activity in this transaction, we won't do the last transition here and do it when
        // moving to final state instead (because it may contain additional parameters from server).
        final ActivityLifecycleItem finalStateRequest = transaction.getLifecycleStateRequest();
        final int finalState = finalStateRequest != null ? finalStateRequest.getTargetState()
                : UNDEFINED;
        // Index of the last callback that requests some post-execution state.
        final int lastCallbackRequestingState = lastCallbackRequestingState(transaction);

        final int size = callbacks.size();
        for (int i = 0; i < size; ++i) {
            final ClientTransactionItem item = callbacks.get(i);
            log("Resolving callback: " + item);
            final int postExecutionState = item.getPostExecutionState();
            final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
                    item.getPostExecutionState());
            if (closestPreExecutionState != UNDEFINED) {
                cycleToPath(r, closestPreExecutionState);
            }

            item.execute(mTransactionHandler, token, mPendingActions);
            item.postExecute(mTransactionHandler, token, mPendingActions);
            if (r == null) {
                // Launch activity request will create an activity record.
                r = mTransactionHandler.getActivityClient(token);
            }

            if (postExecutionState != UNDEFINED && r != null) {
                // Skip the very last transition and perform it by explicit state request instead.
                final boolean shouldExcludeLastTransition =
                        i == lastCallbackRequestingState && finalState == postExecutionState;
                cycleToPath(r, postExecutionState, shouldExcludeLastTransition);
            }
        }
    }

transaction.getCallbacks()就是上述过程中ClientTransaction创建过程赋值的LaunchActivityItem对象,因此主要看下LaunchActivityItem.execute()

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

此时的client为ActivityThread,因此调用ActivityThread.handleLaunchActivity()

   @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        if (r.profilerInfo != null) {
            mProfiler.setProfiler(r.profilerInfo);
            mProfiler.startProfiling();
        }

        // Make sure we are running with the most recent config.
        handleConfigurationChanged(null, null);

        if (localLOGV) Slog.v(
            TAG, "Handling launch of " + r);

        // Initialize before creating the activity
        if (!ThreadedRenderer.sRendererDisabled) {
            GraphicsEnvironment.earlyInitEGL();
        }
        WindowManagerGlobal.initialize();

        final Activity a = performLaunchActivity(r, customIntent);
}
 /**  Core implementation of activity launch. */
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }

        ComponentName component = r.intent.getComponent();
        if (component == null) {
            component = r.intent.resolveActivity(
                mInitialApplication.getPackageManager());
            r.intent.setComponent(component);
        }

        if (r.activityInfo.targetActivity != null) {
            component = new ComponentName(r.activityInfo.packageName,
                    r.activityInfo.targetActivity);
        }

        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);
          ...
                activity.mCalled = false;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                if (!activity.mCalled) {     
                }
                r.activity = activity;
            }
            r.setState(ON_CREATE);
            mActivities.put(r.token, r);
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            }
        }

        return activity;
    }

主要跟进mInstrumentation.callActivityOnCreate()

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

Activity.performCreate()

 final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        mCanEnterPictureInPicture = true;
        restoreHasCurrentPermissionRequest(icicle);
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }
        writeEventLog(LOG_AM_ON_CREATE_CALLED, "performCreate");
        mActivityTransitionState.readState(icicle);

        mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
                com.android.internal.R.styleable.Window_windowNoDisplay, false);
        mFragments.dispatchActivityCreated();
        mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
    }

至此已经回调到了Activity的onCreate()方法,Activity也就正式启动了,后续就是对应的声明周期回调。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>