Public number: program ape development center hope to write something that can help you ๐Ÿคฃ๐Ÿคฃ

First, write first

Here’s what you need to know before you get started:

  • There is a compiled Android source code, now AS basic can meet, begin to follow the steps, a deeper understanding
  • Knowledge of Binder mechanisms
  • This article is based on API 26, which version of the source code is not important, the general process has no essential difference
  • From the time the user touches the desktop icon to the time the Activity starts

Introduction to Key Classes

  • ActivityManagerService: AMS is one of the most core services in Android, mainly responsible for the startup, switching, scheduling of the four major components of the system and the management and scheduling of application process. Its responsibilities are similar to the process management and scheduling module in the operating system, and it is also a Binder implementation class. Application processes can invoke system services through Binder mechanisms.
  • ActivityThread: Entry class for the application. The system opens the message loop queue by calling the main function. The thread of ActivityThread is called the main thread of the application (UI thread).
  • Instrumentation: tool class, it is used to monitor the application and system interaction, wrapped in ActivityManagerService calls, some plug-in solutions are implemented through the hook class.
  • ActivityStarter: a tool class for starting an Activity that handles various flags for starting an Activity.
  • ActivityStackSupervisor: The stack of activities that manage all applications, where mFocusedStack is the current application’s Activity stack.

Application Process Introduction

  • In most cases, each Android application runs in its own Linux process. When some of the application’s code needs to run, the system creates this process for the application and keeps it running until it is no longer needed and the system needs to reclaim its memory for use by other applications.
  • The life cycle of an application process is not directly controlled by the application itself, but is determined by the system by a combination of factors such as what parts of the application the system knows are running, how important those parts are to users, and the total amount of memory available in the system. This is a basic feature of Android that is very unique.
  • When an application component is started and no other components are running, the Android system uses a single thread of execution to start a new Linux process for the application. By default, all components of the same application run in the same process and thread (called the “main” thread). If an application component starts and the application already has a process (because other components of the application exist), the component starts within that process and uses the same thread of execution. However, you can schedule other components in your application to run in a separate process and create additional threads for any process.
  • Each application process is like a Sandbox. Android assigns a UID to each application. Note that the UID is different from the User ID in Linux.
  • Android uses remote procedure calls (RPC) to provide an interprocess communication (IPC) mechanism, in which the system remotely executes (in other processes) methods called by activities or other application components and returns all results to the caller. Therefore, you need to decompose the method call and its data to a level recognized by the operating system, transfer it from the local process and address space to the remote process and address space, and then reassemble and execute the call in the remote process. The return value is then transmitted back in the opposite direction. Android provides all the code needed to perform these IPC transactions, so you just need to focus on defining and implementing the RPC programming interface.

To help you understand the concept of a process, here’s a diagram:

Ii. Process analysis

Here’s a sketch of the process:

The following is a detailed diagram of the process to take you through the complete startup process and the classes involved:

Here is Gityuan’s system startup architecture diagram to help you understand, just look at the top half of the diagram:

Third, an overview of the

To put it simply, there are four steps to start an Activity from the user’s finger touch to the desktop icon:

  1. When you click on the desktop App, the Launcher process is the process in which the Launcher is located, and the remote process is started with the Binder to send messages to the system_server process.
  1. In system_server, the operation to start a process is called firstActivityManagerService#startProcessLocked()Method that is called internallyProcess.start(android.app.ActivityThread); The Zygote process is then told to fork the child process, i.e. the APP process, through socket communication. After the process is created, load the ActivityThread and executeActivityThread#main()Methods;
  1. In the app process,main()The ActivityThread method instantiates the ActivityThread and creates ApplicationThread, Looper, Handler objects, and callsActivityThread#attach(false)Methods communicate with Binder, called within methodsActivityManagerService#attachApplication(mAppThread)Method to inform ActivityManagerService of thread information, and Looper starts the loop;
  1. Back in system_server,ActivityManagerService#attachApplication(mAppThread)Method called internallythread#bindApplication() ๅ’Œ mStackSupervisor#attachApplicationLocked()We’ll go through these two methods in turn; 4.1thread#bindApplication()Method calledActivityThread#sendMessage(H.BIND_APPLICATION, data)Method, and finally got toActivityThread#handleBindApplication()To create the Application object and then callApplication#attach(context)To bind the Context, which is called after the Application object is createdmInstrumentation#callApplicationOnCreate()performApplication#onCreate()Life cycle; 4.2mStackSupervisor#attachApplicationLocked()Method callapp#thread#scheduleLaunchActivity() ๅณ ActivityThread#ApplicationThread#scheduleLaunchActivity()Method, and then throughActivityThread#sendMessage(H.LAUNCH_ACTIVITY, r)Method, and finally got toActivityThread#handleLaunchActivity(), which creates the Activity object and then invokes itactivity.attach()Method, and then callmInstrumentation#callActivityOnCreate()performActivity#onCreate()Life cycle;

Four, the source code call exploration

For each step in the first flowchart of this article, take a step-by-step look at how the source code is invoked:

STEP 1

The user clicks on the app icon;

STEP 2

To capture the click event, call Activity#startActivity();

STEP 3

Activity#startActivity() calls Instrumentation;

Activity.java

@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); } } public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) { if (mParent == null) { options = transferSpringboardActivityOptions(options); Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options); ยทยทยท} else {ยทยทยท}}Copy the code

STEP 4

Instrumentation sends messages to the system_server process via Binder communication, calling ActivityManager#getService()#startActivity(), An implementation of ActivityManager#getService() is ActivityManagerService;

Instrumentation.java

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; }Copy the code

ActivityManager.java

public static IActivityManager getService() { return IActivityManagerSingleton.get(); } private static final Singleton<IActivityManager> IActivityManagerSingleton = new Singleton<IActivityManager>() { @Override protected IActivityManager create() { final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE); final IActivityManager am = IActivityManager.Stub.asInterface(b); return am; }};Copy the code

STEP 5

ActivityManagerService calls ActivityStarter;

ActivityManagerService.java

    @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) {
        enforceNotIsolatedCaller("startActivity");
        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
                userId, false, ALLOW_FULL_ONLY, "startActivity", null);
        // TODO: Switch to user app stacks here.
        return mActivityStarter.startActivityMayWait(caller, -1, callingPackage, intent,
                resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
                profilerInfo, null, null, bOptions, false, userId, null, null,
                "startActivityAsUser");
    }

Copy the code

STEP 6

ActivityStarter calls ActivityStackSupervisor;

final int startActivityMayWait(IApplicationThread caller, int callingUid, String callingPackage, Intent intent, String resolvedType, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, WaitResult outResult, Configuration globalConfig, Bundle bOptions, boolean ignoreTargetSecurity, int userId, IActivityContainer iContainer, TaskRecord inTask, String reason) {ยทยทยท int res = startActivityLocked(Caller, intent, ephemeralIntent, resolvedType, aInfo, rInfo, voiceSession, voiceInteractor, resultTo, resultWho, requestCode, callingPid, callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, options, ignoreTargetSecurity, componentSpecified, outRecord, container, inTask, reason); ยทยทยท} int startActivityLocked(IApplicationThread caller, Intent Intent, Intent ephemeralIntent, String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, ActivityStackSupervisor.ActivityContainer container, TaskRecord inTask, String Reason) {ยทยทยท mLastStartActivityResult = startActivity(Caller, intent, ephemeralIntent, ephemeralIntent) resolvedType, aInfo, rInfo, voiceSession, voiceInteractor, resultTo, resultWho, requestCode, callingPid, callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, options, ignoreTargetSecurity, componentSpecified, mLastStartActivityRecord, container, inTask); ... return mLastStartActivityResult; } private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent, String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, ActivityStackSupervisor.ActivityContainer container, TaskRecord) {ยทยทยท return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true, options, inTask, outActivity); } private int startActivity(final ActivityRecord r, ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask, ActivityRecord outActivity []) {... try {mService. MWindowManager. DeferSurfaceLayout (); result = startActivityUnchecked(r, sourceRecord, voiceSession, voiceInteractor, startFlags, doResume, options, inTask, outActivity); } finally {ยทยทยท} ยทยทยท} private int startActivityUnchecked(Final ActivityRecord r, ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask, ActivityRecord[] outActivity) {ยทยทยท if (mDoResume) {Final ActivityRecord topTaskActivity = mStartActivity.getTask().topRunningActivityLocked(); if (! mTargetStack.isFocusable() || (topTaskActivity ! = null && topTaskActivity.mTaskOverlay && mStartActivity ! = topTaskActivity)) {...} else {ยท ยท ยท mSupervisor. ResumeFocusedStackTopActivityLocked (mTargetStack mStartActivity, mOptions); }} else {ยทยทยท} ยทยท}Copy the code

STEP 7

ActivityStackSupervisor calls ActivityStack;

boolean resumeFocusedStackTopActivityLocked( ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) { if (targetStack ! = null && isFocusedStack(targetStack)) { return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions); }, return false. }Copy the code

STEP 8

ActivityStack calls back to ActivityStackSupervisor;

boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) {... try {... result = resumeTopActivityInnerLocked (prev, options); } finally {ยทยทยท} ยทยทยท return result; } private Boolean resumeTopActivityInnerLocked (ActivityRecord prev, ActivityOptions options) {... the if (next app! = null && next.app.thread ! = null) {...} else {ยท ยท ยท mStackSupervisor. StartSpecificActivityLocked (next, true, true); }...}Copy the code

STEP 9

ActivityStackSupervisor calls back to ActivityManagerService, which determines whether the process to start the App exists and notifies the process to start the Activity. Otherwise, the process is created first.

Void startSpecificActivityLocked (ActivityRecord r, Boolean andResume, Boolean checkConfig) {... the if (app! = null && app.thread ! // If the process already exists, notify the process to start components realStartActivityLocked(r, app, andResume, checkConfig); return; } the catch (RemoteException e) {...}} / / or process is created first mService. StartProcessLocked (r.p rocessName, r.i show nfo. ApplicationInfo, true, 0, "activity", r.intent.getComponent(), false, false, true); }Copy the code

STEP 10

Then we look at the case where the process has not yet been created, and we see that the final call here is Process#start() to start the process;

ActivityManagerService.java

final ProcessRecord startProcessLocked(String processName, ApplicationInfo info, boolean knownToBeDead, int intentFlags, String hostingType, ComponentName hostingName, boolean allowWhileBooting, boolean isolated, boolean keepIfLarge) { return startProcessLocked(processName, info, knownToBeDead, intentFlags, hostingType, hostingName, allowWhileBooting, isolated, 0 /* isolatedUid */, keepIfLarge, null /* ABI override */, null /* entryPoint */, null /* entryPointArgs */, null /* crashHandler */); } final ProcessRecord startProcessLocked(String processName, ApplicationInfo info, boolean knownToBeDead, int intentFlags, String hostingType, ComponentName hostingName, boolean allowWhileBooting, boolean isolated, int isolatedUid, boolean keepIfLarge, String abiOverride, String entryPoint, String[] entryPointArgs, Runnable crashHandler (app, hostingType, hostingNameStr, abiOverride, entryPoint, entryPointArgs); ยทยทยท} Private final void startProcessLocked(ProcessRecord app, String hostingType, String hostingNameStr, String abiOverride, String entryPoint, String [] entryPointArgs) {... the if (entryPoint = = null) entryPoint = "android. App. ActivityThread"; Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " + app.processName); checkTime(startTime, "startProcess: asking zygote to start proc"); ProcessStartResult startResult; If (hostingType.equals("webview_service")) {ยทยทยท} else {startResult = process. start(entryPoint, app.processName, uid, uid, gids, debugFlags, mountExternal, app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet, app.info.dataDir, invokeWith, entryPointArgs); }...}Copy the code

STEP 11

ActivityManagerService tells the Zygote process to fork the app process via socket communication.

STEP 12

Execute ActivityThread#main() to instantiate the ActivityThread and create ApplicationThread, Looper, and Hander objects. The ActivityThread#attach(false) method is called for Binder communication and Looper starts the loop;

ActivityThread.java

Public static void main(String[] args) {ยทยทยท prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); }... stars. The loop (); ยทยทยท} private void attach(Boolean system) {ยทยทยท ยท if (! System) {... final IActivityManager MGR. = ActivityManager getService (); try { mgr.attachApplication(mAppThread); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } ยทยทยท} else {ยทยทยท} ยทยทยท}Copy the code

Back in system_server, The ActivityManagerService#attachApplication(mAppThread) method internally calls thread#bindApplication() and Mstackcontainer #attachApplicationLocked() these two methods.

STEP 13

The thread#bindApplication() method calls the ActivityThread#sendMessage(h.bin_application, data) method, We end up at ActivityThread#handleBindApplication(), which creates the Application object, and then calls Application#attach(context) to bind the context, Call mInstrumentation#callApplicationOnCreate() to execute Application#onCreate();

ActivityManagerService.java

@Override public final void attachApplication(IApplicationThread thread) {synchronized (this) {ยทยทยท ยท attachApplicationLocked(thread, callingPid); ยทยทยท}} Private final Boolean attachApplicationLocked(IApplicationThread thread, Int pid) {ยทยทยท try {ยทยทยท ยท if (app.instr! = null) { thread.bindApplication(processName, appInfo, providers, app.instr.mClass, profilerInfo, app.instr.mArguments, app.instr.mWatcher, app.instr.mUiAutomationConnection, testMode, mBinderTransactionTrackingEnabled, enableTrackAllocation, isRestrictedBackupMode || ! normalMode, app.persistent, new Configuration(getGlobalConfiguration()), app.compat, getCommonServicesLocked(app.isolated), mCoreSettingsObserver.getCoreSettingsLocked(), buildSerial); } else { thread.bindApplication(processName, appInfo, providers, null, profilerInfo, null, null, null, testMode, mBinderTransactionTrackingEnabled, enableTrackAllocation, isRestrictedBackupMode || ! normalMode, app.persistent, new Configuration(getGlobalConfiguration()), app.compat, getCommonServicesLocked(app.isolated), mCoreSettingsObserver.getCoreSettingsLocked(), buildSerial); } ยทยทยท} catch (Exception E) {ยทยทยท} ยทยทยท // See if the top Visible activity is waiting to run in this process... if (normalMode) { try { if (mStackSupervisor.attachApplicationLocked(app)) { didSomething = true; }} Catch (Exception e) {ยทยทยท}} ยทยทยท}Copy the code

ActivityThread#ApplicationThread.java

Public final void bindApplication(String processName, ApplicationInfo appInfo, ยทยทยท sendMessage(h.bind_application, data); }Copy the code

ActivityThread.java

private 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) {ยทยทยท mH. SendMessage (MSG); }Copy the code

Let’s look at the handleMessage() method of mH;

ActivityThread#H.java

Public void handleMessage(Message MSG) {ยทยทยท switch (MSG. What) {ยทยทยท case BIND_APPLICATION: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication"); AppBindData data = (AppBindData)msg.obj; handleBindApplication(data); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; ......}}Copy the code

Create the mInstrumentation object and call data#info#makeApplication to create the Application object;

ActivityThread.java

Private void handleBindApplication(AppBindData data) {ยทยทยท Try {// Create Application instance Application App = data.info.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app; ... try {mInstrumentation. CallApplicationOnCreate (app); } the catch (Exception e) {...}} finally...} {...}Copy the code

LoadedApk.java

public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { if (mApplication ! = null) { return mApplication; } ยทยทยท ยท Application app = null; ... try {... app = mActivityThread. MInstrumentation. NewApplication (cl, appClass, appContext); ยทยทยท} catch (Exception e) {ยทยทยท} ยทยท if (instrumentation! = null) {try {/ / execution application # onCreate () life cycle instrumentation. CallApplicationOnCreate (app); } Catch (Exception e) {ยทยทยท}} ยทยทยท ยท return app; }Copy the code

STEP 14

The mStackcontainer #attachApplicationLocked() method is called appthread #scheduleLaunchActivity() Using the ActivityThread#ApplicationThread#scheduleLaunchActivity() method, and then using the ActivityThread#sendMessage(h.launch_activity, r) method, You end up at ActivityThread#handleLaunchActivity(), which creates an Activity object and calls the activity.attach() method, Call mInstrumentation#callActivityOnCreate() to execute the Activity#onCreate() lifecycle;

ActivityStackSupervisor.java

boolean attachApplicationLocked(ProcessRecord app) throws RemoteException { for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {ยทยทยท for (int stackNdx = stacks. Size () - 1; stackNdx >= 0; --stackNdx) {hr! = null) { if (hr.app == null && app.uid == hr.info.applicationInfo.uid && processName.equals(hr.processName)) { try { if  (realStartActivityLocked(hr, app, true, true)) { didSomething = true; }} Catch (RemoteException e) {ยทยทยท}}}}} ยทยทยท} Final Boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app, boolean andResume, Boolean checkConfig) throws RemoteException {... try {... app. Thread. ScheduleLaunchActivity (new Intent (r.i ntent), r.appToken, System.identityHashCode(r), r.info, // TODO: Have this take the merged configuration instead of separate global and // override configs. mergedConfiguration.getGlobalConfiguration(), mergedConfiguration.getOverrideConfiguration(), r.compat, r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle, r.persistentState, results, newIntents, ! andResume, mService.isNextTransitionForward(), profilerInfo); Catch (RemoteException E) {RemoteException e}Copy the code

ActivityThread#ApplicationThread.java

@Override public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward, ProfilerInfo ProfilerInfo) {ยทยทยท sendMessage(h.launch_activity, r); }Copy the code

ActivityThread.java

private 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) {ยทยทยท mH. SendMessage (MSG); }Copy the code

Let’s also look at the handleMessage() method of mH;

ActivityThread#H.java

public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); Switch (msg.what) {case LAUNCH_ACTIVITY: {ยทยทยท ยท handleLaunchActivity(r, NULL, "LAUNCH_ACTIVITY"); ...} break; ......}}Copy the code

ActivityThread.java

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String Reason) {ยทยทยท Activity a = performLaunchActivity(r, customIntent); ยทยทยท} Private Activity performLaunchActivity(ActivityClientRecord R, Intent customIntent) {ยทยทยท Activity Activity = null; Try {/ / Java objects created Activity. Lang. This cl = appContext. GetClassLoader (); activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); ยทยทยท ยท} Catch (Exception e) {ยทยทยท ยท} try {ยทยทยท ยท if (activity! = null) {ยทยทยท Activity. Attach (appContext, this, getInstrumentation(), R.toy, R.I.dent, app, R.I.ntent, R.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); ... / / execution Activity# onCreate () life cycle if (r.i sPersistable ()) {mInstrumentation. CallActivityOnCreate (activity, r.s Tate. r.persistentState); } else { mInstrumentation.callActivityOnCreate(activity, r.state); Catch (SuperNotCalledException E) {catch (Exception E) {ยทยทยท}Copy the code

By now, the entire APP startup process and source code call have been analyzed. If you think the article helps you, please connect three keys

Public number: program ape development center hope to write something that can help you ๐Ÿคฃ๐Ÿคฃ