Initialization of the process

Let me review, at that time, the Activity to start the process in the article, startSpecificActivityLocked detection process whether to open a section, when I was ignored when no process, this is because ActivityThread initialization, StartProcessLocked must go first to initialize the process.

        mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
                "activity", r.intent.getComponent(), false, false, true);
Copy the code
@GuardedBy("this") 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) { long startTime = SystemClock.elapsedRealtime(); ProcessRecord app; if (! isolated) { app = getProcessRecordLocked(processName, info.uid, keepIfLarge); checkTime(startTime, "startProcess: after getProcessRecord"); if ((intentFlags & Intent.FLAG_FROM_BACKGROUND) ! = 0) {... } else { mAppErrors.resetProcessCrashTimeLocked(info); if (mAppErrors.isBadProcessLocked(info)) { ... mAppErrors.clearBadProcessLocked(info); if (app ! = null) { app.bad = false; } } } } else { ... } // We don't have to do anything more if: // (1) There is an existing application record; and // (2) The caller doesn't think it is dead, OR there is no thread // object attached to it so we know it couldn't have crashed; and // (3) There is a pid assigned to it, so it is either starting or // already running. if (app ! = null && app.pid > 0) { if ((! knownToBeDead && ! app.killed) || app.thread == null) { ... app.addPackage(info.packageName, info.versionCode, mProcessStats); . return app; } // An application record is attached to a previous process, // clean it up now. killProcessGroup(app.uid, app.pid); handleAppDiedLocked(app, true, true); } String hostingNameStr = hostingName ! = null ? hostingName.flattenToShortString() : null; if (app == null) { app = newProcessRecordLocked(info, processName, isolated, isolatedUid); if (app == null) { ... return null; } app.crashHandler = crashHandler; app.isolatedEntryPoint = entryPoint; app.isolatedEntryPointArgs = entryPointArgs; } else { app.addPackage(info.packageName, info.versionCode, mProcessStats); } if (! mProcessesReady && ! isAllowedWhileBooting(info) && ! allowWhileBooting) { if (! mProcessesOnHold.contains(app)) { mProcessesOnHold.add(app); } return app; } final boolean success = startProcessLocked(app, hostingType, hostingNameStr, abiOverride); return success ? app : null; }Copy the code

There are several possibilities for performing the action of starting a process:

  • 1. There is no birth process before there is one.
  • 2. The process has been created, but it needs to be restarted because of some problems such as crash or the same process is installed.

Therefore, getProcessRecordLocked will first get the corresponding ProcessRecord. This object is actually the object that represents the process in AMS. This object will be stored in a SparseArray because it happens to be stored with a UID of type int as the key.

If the ProcessRecord is found, it has already been created. Therefore, in order to return the process to its original state, it clears the internal error stack, reads the latest process version number from ApplicationInfo, and kills the process group. HandleAppDiedLocked handles the death of the App, mainly clearing TaskRecord. Record all active activities in ActivityStackSupervisor and destroy the Surface in the corresponding process WMS.

If the corresponding ProcessRecord is not found, newProcessRecordLocked is called to create a new process object and add the current ProcessRecord to the mProcessMap of type SparseArray.

The next core is another overloaded function, startProcessLocked.

AMS.startProcessLocked

private final boolean startProcessLocked(ProcessRecord app, String hostingType, String hostingNameStr, boolean disableHiddenApiChecks, String abiOverride) { if (app.pendingStart) { return true; } long startTime = SystemClock.elapsedRealtime(); if (app.pid > 0 && app.pid ! = MY_PID) { checkTime(startTime, "startProcess: removing from pids map"); synchronized (mPidsSelfLocked) { mPidsSelfLocked.remove(app.pid); mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); } app.setPid(0); }... final String entryPoint = "android.app.ActivityThread"; return startProcessLocked(hostingType, hostingNameStr, entryPoint, app, uid, gids, runtimeFlags, mountExternal, seInfo, requiredAbi, instructionSet, invokeWith, startTime); } catch (RuntimeException e) { ... return false; }}Copy the code

There are a lot of bootstrap parameters set in the middle, and we just need to focus on the rest of the code. In this case, if the ProcessRecord is not empty, we will disassemble the bomb whose ANR is PROC_START_TIMEOUT_MSG implemented by Handler.

In the setting of many parameters entryPoint, one of the key parameters. The string is set to the android app. ActivityThread. This design also applies to Flutter, telling you what the hook class and method is.

    private boolean startProcessLocked(String hostingType, String hostingNameStr, String entryPoint,
            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
            String seInfo, String requiredAbi, String instructionSet, String invokeWith,
            long startTime) {
        app.pendingStart = true;
        app.killedByAm = false;
        app.removed = false;
        app.killed = false;
        final long startSeq = app.startSeq = ++mProcStartSeqCounter;
        app.setStartParams(uid, hostingType, hostingNameStr, seInfo, startTime);
        if (mConstants.FLAG_PROCESS_START_ASYNC) {
            mProcStartHandler.post(() -> {
                try {
                    synchronized (ActivityManagerService.this) {
                        final String reason = isProcStartValidLocked(app, startSeq);
                        if (reason != null) {
                            app.pendingStart = false;
                            return;
                        }
                        app.usingWrapper = invokeWith != null
                                || SystemProperties.get("wrap." + app.processName) != null;
                        mPendingStarts.put(startSeq, app);
                    }
                    final ProcessStartResult startResult = startProcess(app.hostingType, entryPoint,
                            app, app.startUid, gids, runtimeFlags, mountExternal, app.seInfo,
                            requiredAbi, instructionSet, invokeWith, app.startTime);
                    synchronized (ActivityManagerService.this) {
                        handleProcessStartedLocked(app, startResult, startSeq);
                    }
                } catch (RuntimeException e) {
...
                    }
                }
            });
            return true;
        } else {
...
            return app.pid > 0;
        }
    }
Copy the code

In this method, the fork process is divided into two steps:

  • 1. The normal startProcess calls process. start to communicate with Zygote and fork a new Process. If the probe is hostType is WebView, call startWebView to start the WebView
  • 2. HandleProcessStartedLocked processing subsequent processing.

After incubating with Zygote, an App process is created that calls the main method of the ActivityThread class. And webviews are managed in a similar way by incubating a WebViewZygote object and having the opportunity to talk to you about it.

AMS.handleProcessStartedLocked

After the above Socket communication, we can accurately obtain whether the incubation process is successful, to do the subsequent processing.

private boolean handleProcessStartedLocked(ProcessRecord app, int pid, boolean usingWrapper, long expectedStartSeq, boolean procAttached) { mPendingStarts.remove(expectedStartSeq); . mBatteryStatsService.noteProcessStart(app.processName, app.info.uid); . try { AppGlobals.getPackageManager().logAppProcessStartIfNeeded(app.processName, app.uid, app.seInfo, app.info.sourceDir, pid); } catch (RemoteException ex) { // Ignore } if (app.persistent) { Watchdog.getInstance().processStarted(app.processName, pid); }... app.setPid(pid); app.usingWrapper = usingWrapper; app.pendingStart = false; ProcessRecord oldApp; synchronized (mPidsSelfLocked) { oldApp = mPidsSelfLocked.get(pid); }... synchronized (mPidsSelfLocked) { this.mPidsSelfLocked.put(pid, app); // Load ANR bomb if (! procAttached) { Message msg = mHandler.obtainMessage(PROC_START_TIMEOUT_MSG); msg.obj = app; mHandler.sendMessageDelayed(msg, usingWrapper ? PROC_START_TIMEOUT_WITH_WRAPPER : PROC_START_TIMEOUT); } } return true; }Copy the code

During the process, the power service collects the startup process logs and stores the PID and ProcessRecord to mPidsSelfLocked of type MAP. Finally, a delayed message of PROC_START_TIMEOUT_MSG is set and sent to the Handler. And the delayed event is 10 seconds.

What if we tried to top this event?

The process startup timeout is abnormal

case PROC_START_TIMEOUT_MSG: { ProcessRecord app = (ProcessRecord)msg.obj; synchronized (ActivityManagerService.this) { processStartTimedOutLocked(app); }}Copy the code
    private final void processStartTimedOutLocked(ProcessRecord app) {
        final int pid = app.pid;
        boolean gone = false;
        synchronized (mPidsSelfLocked) {
            ProcessRecord knownApp = mPidsSelfLocked.get(pid);
            if (knownApp != null && knownApp.thread == null) {
                mPidsSelfLocked.remove(pid);
                gone = true;
            }
        }

        if (gone) {
                    pid, app.uid, app.processName);
            removeProcessNameLocked(app.processName, app.uid);
            if (mHeavyWeightProcess == app) {
                mHandler.sendMessage(mHandler.obtainMessage(CANCEL_HEAVY_NOTIFICATION_MSG,
                        mHeavyWeightProcess.userId, 0));
                mHeavyWeightProcess = null;
            }
            mBatteryStatsService.noteProcessFinish(app.processName, app.info.uid);

            cleanupAppInLaunchingProvidersLocked(app, true);
            mServices.processStartTimedOutLocked(app);
            app.kill("start timeout", true);
...
            removeLruProcessLocked(app);
            if (mBackupTarget != null && mBackupTarget.app.pid == pid) {
                mHandler.post(new Runnable() {
                @Override
                    public void run(){
                        try {
                            IBackupManager bm = IBackupManager.Stub.asInterface(
                                    ServiceManager.getService(Context.BACKUP_SERVICE));
                            bm.agentDisconnected(app.info.packageName);
                        } catch (RemoteException e) {
                            // Can't happen; the backup manager is local
                        }
                    }
                });
            }
            if (isPendingBroadcastProcessLocked(pid)) {

                skipPendingBroadcastLocked(pid);
            }
        } else {

        }
    }
Copy the code

You can see that when the startup process timeout event is executed, the following events are executed:

  • 1. Clear the caches in mPidsSelfLocked, clear the caches in mProcesMap, and in the case of a heavyweight process, clear the caches stored in the AMS heavyweight process
  • 2. Close the process information in the power log and clear the contentProviders and Services that need to be loaded after the process is initialized
  • 3. Then execute the kill method of ProcessRecord to clear it from the Lru cache.
  • 4. Close the links to those backup services, and finally clear the broadcast that the current process is listening for and preparing to send.

Therefore, process startup also has ANR. Exit is also understood when the process starts more than 10 seconds after the event. So how do we defuse this bomb? Let’s talk about the first method of ActivityThread after a process is born.

Initialization of ActivityThread

    public static void main(String[] args) {
...
        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
Copy the code

The Main method of ActivityThread initializes a prepareMainLooper and loops events.

In this process, you can see the Looper log printing I talked about in my previous article Handler. In the process, ActivityThread’s attch method is called to further bind. What is bound here? Let’s look at the attach method first.

ActivityThread.attach

private void attach(boolean system, long startSeq) { sCurrentActivityThread = this; mSystemThread = system; if (! system) { ViewRootImpl.addFirstDrawHandler(new Runnable() { @Override public void run() { ensureJitEnabled(); }}); android.ddm.DdmHandleAppName.setAppName("<pre-initialized>", UserHandle.myUserId()); RuntimeInit.setApplicationObject(mAppThread.asBinder()); final IActivityManager mgr = ActivityManager.getService(); try { mgr.attachApplication(mAppThread, startSeq); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } // Watch for getting close to heap limit. BinderInternal.addGcWatcher(new Runnable() { @Override public void run() { if (! mSomeActivitiesChanged) { return; } Runtime runtime = Runtime.getRuntime(); long dalvikMax = runtime.maxMemory(); long dalvikUsed = runtime.totalMemory() - runtime.freeMemory(); if (dalvikUsed > ((3*dalvikMax)/4)) { if (DEBUG_MEMORY_TRIM) Slog.d(TAG, "Dalvik max=" + (dalvikMax/1024) + " total=" + (runtime.totalMemory()/1024) + " used=" + (dalvikUsed/1024)); mSomeActivitiesChanged = false; try { mgr.releaseSomeActivities(mAppThread); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}}}); } else { .... } // add dropbox logging to libcore DropBox.setReporter(new DropBoxReporter()); ViewRootImpl.ConfigChangedCallback configChangedCallback = (Configuration globalConfig) -> { synchronized (mResourcesManager) { // We need to apply this change to the resources immediately, because upon returning // the view hierarchy will be informed about it. if (mResourcesManager.applyConfigurationToResourcesLocked(globalConfig, null /* compat */)) { updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(), mResourcesManager.getConfiguration().getLocales()); // This actually changed the resources! Tell everyone about it. if (mPendingConfiguration == null || mPendingConfiguration.isOtherSeqNewer(globalConfig)) { mPendingConfiguration = globalConfig; sendMessage(H.CONFIGURATION_CHANGED, globalConfig); }}}}; ViewRootImpl.addConfigCallback(configChangedCallback); }Copy the code

Once we have the basics, it’s easier to read this passage:

  • 1. Register the viewrotimpl callback when onDraw is called for the first time to enable jit mode for the virtual machine. Note that this JIT is not the Skia SkSL JIT mode, just the VIRTUAL machine JIT mode. In Android 9.0, however, it is null.
  • 2. Bind ApplicationThread to AMS
  • 3. BinderInternal continuously listens for usage in the Java heap and releases a portion of the Activity if the usage exceeds 3/4
  • 4. Add the DropBox printer. DropBox actually uses DropBoxManagerService, which logs to /data/ System/DropBox. Generally record some one behavior, such as ANR, crash, watchdog(abnormal system process), native_crash, lowmem(low memory), strict_mode wait log.
  • 5. Add the callback when the resource environment is configured. You can see that resources are switched according to the Locale Locale

So let’s take a look at the second point and see what happens in this process.

Binding ApplicationThread

            final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
Copy the code

You can see here that you are actually binding ApplicationThread to AMS.

private class ApplicationThread extends IApplicationThread.Stub
Copy the code

ApplicationThread is actually a Binder object.

For those familiar with the Activity startup process, IActivityManager stands for ActivityManagerService. Binder is no longer shown how it works, we will look directly at ActivityManagerService.

The AMS attachApplicationLocked

@Override public final void attachApplication(IApplicationThread thread, long startSeq) { synchronized (this) { int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); attachApplicationLocked(thread, callingPid, callingUid, startSeq); Binder.restoreCallingIdentity(origId); }}Copy the code

The use of the Binder. ClearCallingIdentity and Binder. GetCallingPid usually used with restoreCallingIdentity. This is used when we need to communicate with Binder to our own processes. The two Clear methods simply empty the Pid and Uid of the Binder on the remote end and return to the Java layer. RestoreCallingIdentity sets these two objects back.

The core method here is attachApplicationLocked.

AMS.attachApplicationLocked

private final boolean attachApplicationLocked(IApplicationThread thread, int pid, int callingUid, long startSeq) { // Find the application record that is being attached... either via // the pid if we are running in multiple processes, or just pull the // next app record if we are emulating process with anonymous threads. ProcessRecord app; long startTime = SystemClock.uptimeMillis(); if (pid ! = MY_PID && pid >= 0) { synchronized (mPidsSelfLocked) { app = mPidsSelfLocked.get(pid); } } else { app = null; } // It's possible that process called attachApplication before we got a chance to // update the internal state. if (app  == null && startSeq > 0) { final ProcessRecord pending = mPendingStarts.get(startSeq); if (pending ! = null && pending.startUid == callingUid && handleProcessStartedLocked(pending, pid, pending.usingWrapper, startSeq, true)) { app = pending; }}... if (app.thread ! = null) { handleAppDiedLocked(app, true, true); } // Tell the process all about itself. final String processName = app.processName; try { AppDeathRecipient adr = new AppDeathRecipient( app, pid, thread); thread.asBinder().linkToDeath(adr, 0); app.deathRecipient = adr; } catch (RemoteException e) { ... return false; } app.makeActive(thread, mProcessStats); app.curAdj = app.setAdj = app.verifiedAdj = ProcessList.INVALID_ADJ; app.curSchedGroup = app.setSchedGroup = ProcessList.SCHED_GROUP_DEFAULT; app.forcingToImportant = null; updateProcessForegroundLocked(app, false, false); app.hasShownUi = false; app.debugging = false; app.cached = false; app.killedByAm = false; app.killed = false; app.unlocked = StorageManager.isUserKeyUnlocked(app.userId); mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info); List<ProviderInfo> providers = normalMode ? generateApplicationProvidersLocked(app) : null; if (providers ! = null && checkAppInLaunchingProvidersLocked(app)) { Message msg = mHandler.obtainMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG); msg.obj = app; mHandler.sendMessageDelayed(msg, CONTENT_PROVIDER_PUBLISH_TIMEOUT); } try { ... if (app.isolatedEntryPoint ! = null) { ... } else if (app.instr ! = null) { ... } 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, isAutofillCompatEnabled); } if (profilerInfo ! = null) { profilerInfo.closeFd(); profilerInfo = null; } checkTime(startTime, "attachApplicationLocked: immediately after bindApplication"); updateLruProcessLocked(app, false, null); checkTime(startTime, "attachApplicationLocked: after updateLruProcessLocked"); app.lastRequestedGc = app.lastLowMemory = SystemClock.uptimeMillis(); } catch (Exception e) { ... return false; } // Remove this record from the list of starting applications. mPersistentStartingProcesses.remove(app); if (DEBUG_PROCESSES && mProcessesOnHold.contains(app)) Slog.v(TAG_PROCESSES, "Attach application locked removing on hold: " + app); mProcessesOnHold.remove(app); boolean badApp = false; boolean didSomething = false; // 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) { Slog.wtf(TAG, "Exception thrown launching activities in " + app, e); badApp = true; } } // Find any services that should be running in this process... if (! badApp) { try { didSomething |= mServices.attachApplicationLocked(app, processName); checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked"); } catch (Exception e) { ... } } // Check if a next-broadcast receiver is in this process... if (! badApp && isPendingBroadcastProcessLocked(pid)) { try { didSomething |= sendPendingBroadcastsLocked(app); checkTime(startTime, "attachApplicationLocked: after sendPendingBroadcastsLocked"); } catch (Exception e) { ... } } // Check whether the next backup agent is in this process... if (! badApp && mBackupTarget ! = null && mBackupTarget.app == app) { ... notifyPackageUse(mBackupTarget.appInfo.packageName, PackageManager.NOTIFY_PACKAGE_USE_BACKUP); try { thread.scheduleCreateBackupAgent(mBackupTarget.appInfo, compatibilityInfoForPackageLocked(mBackupTarget.appInfo), mBackupTarget.backupMode); } catch (Exception e) { ... }}... if (! didSomething) { updateOomAdjLocked(); } return true; }Copy the code

After the compression process, we only need to focus on the code logic.

  • 1. Obtain the ProcessRecord object corresponding to the PID from mPidsSelfLocked
  • 2. If can’t find the corresponding ProcessRecord, then try showed by handleProcessStartedLocked may add missing, try to add to the cache. You don’t usually get in here.
  • 3. If the current ProcessRecord contains Thread objects, the process has already been started and has undergone a process restart. Therefore, handleAppDiedLocked will need to clean up all caches left in AMS and WMS by the process
  • 4. Bind bind bind death monitor; Also set the data in ProcessRecord, binding the ApplicationThread to ProcessRecord.
  • 5. Run the mHandler. removeMessages command to remove the process startup timeout bomb
  • 6. Call ApplicationThread’s bindApplication method across processes.
  • 7. Start activities, services, Broadcast, and ContentProviders that have previously been active in TaskRecord. In this case, the process restarts because of a crash.
  • 8. Adjust adj. This value is the activity level of the current process.

ApplicationThread.bindApplication

public final void bindApplication(String processName, ApplicationInfo appInfo, List<ProviderInfo> providers, ComponentName instrumentationName, ProfilerInfo profilerInfo, Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, IUiAutomationConnection instrumentationUiConnection, int debugMode, boolean enableBinderTracking, boolean trackAllocation, boolean isRestrictedBackupMode, boolean persistent, Configuration config, CompatibilityInfo compatInfo, Map services, Bundle coreSettings, String buildSerial, boolean autofillCompatibilityEnabled) { if (services ! = null) { if (false) { // Test code to make sure the app could see the passed-in services. for (Object oname : services.keySet()) { if (services.get(oname) == null) { continue; // AM just passed in a null service. } String name = (String) oname; // See b/79378449 about the following exemption. switch (name) { case "package": case Context.WINDOW_SERVICE: continue; } if (ServiceManager.getService(name) == null) { Log.wtf(TAG, "Service " + name + " should be accessible by this app"); } } } // Setup the service cache in the ServiceManager ServiceManager.initServiceCache(services); } setCoreSettings(coreSettings); AppBindData data = new AppBindData(); data.processName = processName; data.appInfo = appInfo; data.providers = providers; data.instrumentationName = instrumentationName; data.instrumentationArgs = instrumentationArgs; data.instrumentationWatcher = instrumentationWatcher; data.instrumentationUiAutomationConnection = instrumentationUiConnection; data.debugMode = debugMode; data.enableBinderTracking = enableBinderTracking; data.trackAllocation = trackAllocation; data.restrictedBackupMode = isRestrictedBackupMode; data.persistent = persistent; data.config = config; data.compatInfo = compatInfo; data.initProfilerInfo = profilerInfo; data.buildSerial = buildSerial; data.autofillCompatibilityEnabled = autofillCompatibilityEnabled; sendMessage(H.BIND_APPLICATION, data); }Copy the code

In this process, the ServiceManager is actually initialized with a hidden SystemService management class that takes the string from AMS and initializes it.

Learn to now appear two can get from the App application side of the system SystemServer process in the service.

  • SystemServiceRegistry This is where we get our common context. getService
  • ServiceManager a system service cache that is not intended for developers

So what’s the relationship between these two? In fact, I have read my previous articles. After SystemServer initializes services, it adds each service to the process’s ServiceManager using addService. In other words, it stores a map value with key as the service name and value as IBinder.

SystemServiceRegistry is a developer-oriented IBinder data structure that caches keys and values. You can obtain the value from context. getSystemService. Initialize AppBindData and send BIND_APPLICATION to the main thread Handler.

ActivityThread.handleBindApplication

private void handleBindApplication(AppBindData data) { // Register the UI Thread as a sensitive thread to the runtime. . synchronized (mResourcesManager) { /* * Update the system configuration since its preloaded and might not * reflect configuration changes. The configuration object passed * in AppBindData can be safely assumed to be up to date */ mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo); mCurDefaultDisplayDpi = data.config.densityDpi; // This calls mResourcesManager so keep it within the synchronized block. applyCompatConfiguration(mCurDefaultDisplayDpi); } data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo); if (agent ! = null) { handleAttachAgent(agent, data.info); } /** * Switch this process to density compatibility mode if needed. */ if ((data.appInfo.flags&ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES) == 0) { mDensityCompatMode = true; Bitmap.setDefaultDensity(DisplayMetrics.DENSITY_DEFAULT); } updateDefaultDensity(); . final InstrumentationInfo ii; if (data.instrumentationName ! = null) { try { ii = new ApplicationPackageManager(null, getPackageManager()) .getInstrumentationInfo(data.instrumentationName, 0); } catch (PackageManager.NameNotFoundException e) { ... } mInstrumentationPackageName = ii.packageName; mInstrumentationAppDir = ii.sourceDir; mInstrumentationSplitAppDirs = ii.splitSourceDirs; mInstrumentationLibDir = getInstrumentationLibrary(data.appInfo, ii); mInstrumentedAppDir = data.info.getAppDir(); mInstrumentedSplitAppDirs = data.info.getSplitAppDirs(); mInstrumentedLibDir = data.info.getLibDir(); } else { ii = null; } final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); updateLocaleListFromAppContext(appContext, mResourcesManager.getConfiguration().getLocales()); . if (SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)) { BaseDexClassLoader.setReporter(DexLoadReporter.getInstance()); }... // Continue loading instrumentation. if (ii ! = null) { ... } else { mInstrumentation = new Instrumentation(); mInstrumentation.basicInit(this); }... // Allow disk access during application and provider setup. This could // block processing ordered broadcasts, but later processing would // probably end up doing the same disk access. Application app; . try { // If the app is being launched for full backup or restore, bring it up in // a restricted environment with the base application class. app = data.info.makeApplication(data.restrictedBackupMode, null); // Propagate autofill compat state app.setAutofillCompatibilityEnabled(data.autofillCompatibilityEnabled); mInitialApplication = app; // don't bring up providers in restricted mode; they may depend on the // app's custom Application class if (! data.restrictedBackupMode) { if (! ArrayUtils.isEmpty(data.providers)) { installContentProviders(app, data.providers); // For process that contains content providers, we want to // ensure that the JIT is enabled "at some point". mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000); } } // Do this after providers, since instrumentation tests generally start their // test thread at this point, and we don't want that racing. try { mInstrumentation.onCreate(data.instrumentationArgs); } catch (Exception e) { ... } try { mInstrumentation.callApplicationOnCreate(app); } catch (Exception e) { ... } } finally { ... }... }}Copy the code

This method is a bit long, but let’s focus on the core logic:

  • 1. First get the mResourcesManager instance (instantiated by the ActivityThread constructor) and configure the dPI for the underlying resources. For more details, see the resource loading series
  • 2. Then use getPackageInfoNoCheck to get the LoadedApk object, which is essentially an in-memory representation of APK discussed in plugins
  • 3. Create a ContextImpl with createAppContext, which is the first Context created in the entire App process
  • 4. Create Instrumentation objects, which are usually used as intermediate keys for automated testing.
  • 5. Create the Application object with makeApplication, and then start all ContextProvider objects with installContentProviders
  • 6. Callback the ApplicationCreate method of the mInstrumentation, which is finally called into the onCreate of the Application.

In the process, we see a little bit of confusion. Application executes once in bindApplication. We actually created it again in FormLaunchActivity when we created the Activity.

Application app = r.packageInfo.makeApplication(false, mInstrumentation);
Copy the code

And in bindApplication

app = data.info.makeApplication(data.restrictedBackupMode, null);
Copy the code

What’s the difference between the two? Assume that this is a normal process that starts from scratch, not a restart. At this point the restrictedBackupMode flag represents the backup flag bit at reboot time. This flag bit defaults to false when we start the process for the first time.

To understand the LoadedApk object more thoroughly, let’s first look at how Android creates this object with getPackageInfoNoCheck

GetPackageInfoNoCheck create LoadedApk

    public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
            CompatibilityInfo compatInfo) {
        return getPackageInfo(ai, compatInfo, null, false, true, false);
    }
Copy the code

GetPackageInfoNoCheck finally calls the getPackageInfo method.

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { final boolean differentUser = (UserHandle.myUserId() ! = UserHandle.getUserId(aInfo.uid)); synchronized (mResourcesManager) { WeakReference<LoadedApk> ref; if (differentUser) { // Caching not supported across users ref = null; } else if (includeCode) { ref = mPackages.get(aInfo.packageName); } else { ref = mResourcePackages.get(aInfo.packageName); } LoadedApk packageInfo = ref ! = null ? ref.get() : null; if (packageInfo == null || (packageInfo.mResources ! = null && ! packageInfo.mResources.getAssets().isUpToDate())) { ... packageInfo = new LoadedApk(this, aInfo, compatInfo, baseLoader, securityViolation, includeCode && (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) ! = 0, registerPackage); if (mSystemThread && "android".equals(aInfo.packageName)) { packageInfo.installSystemApplicationInfo(aInfo, getSystemContext().mPackageInfo.getClassLoader()); } if (differentUser) { // Caching not supported across users } else if (includeCode) { mPackages.put(aInfo.packageName, new WeakReference<LoadedApk>(packageInfo)); } else { mResourcePackages.put(aInfo.packageName, new WeakReference<LoadedApk>(packageInfo)); } } return packageInfo; }}Copy the code

There’s really not much special processing going on in this process. Essentially LoadedApk holds all the information in the package that ApplicationInfo parses from AMS. And save LoadedApk to the map with weak references in mResourcePackages.

Initializes the Application

public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { if (mApplication ! = null) { return mApplication; } Application app = null; String appClass = mApplicationInfo.className; if (forceDefaultAppClass || (appClass == null)) { appClass = "android.app.Application"; } try { java.lang.ClassLoader cl = getClassLoader(); if (! mPackageName.equals("android")) { initializeJavaContextClassLoader(); } ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); app = mActivityThread.mInstrumentation.newApplication( cl, appClass, appContext); appContext.setOuterContext(app); } catch (Exception e) { ... } mActivityThread.mAllApplications.add(app); mApplication = app; if (instrumentation ! = null) { try { instrumentation.callApplicationOnCreate(app); } catch (Exception e) { if (! instrumentation.onException(app, e)) { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); throw new RuntimeException( "Unable to create application " + app.getClass().getName() + ": " + e.toString(), e); } } } // Rewrite the R 'constants' for all library apks. SparseArray<String> packageIdentifiers = getAssets(mActivityThread) .getAssignedPackageIdentifiers(); final int N = packageIdentifiers.size(); for (int i = 0; i < N; i++) { final int id = packageIdentifiers.keyAt(i); if (id == 0x01 || id == 0x7f) { continue; } rewriteRValues(getClassLoader(), packageIdentifiers.valueAt(i), id); } return app; }Copy the code

You can see that an Application object is cached globally in the LoadApk object. After each subsequent call to this method to read, this method is called to fetch the Application object directly from the cache.

The default class is available when the Application class is not set, or the default Application is enforced when the backup service is enabled.

This can be broken down into several steps:

  • 1. GetClassLoader load this, if the system is applied loads another this initializeJavaContextClassLoader
  • 2. CreateAppContext Creates a context object
  • 3. The newApplication reflection generates the Application object, calls the attachBaseContext method, and binds the Context and Application to each other
  • 4. If instrumentation is passed in, the application. onCreate method will be called
  • 5. RewriteRValues Rewrites the ID of the shared resource library. The packageId of the shared resource library is usually 0x00. The id of the shared resource library is obtained according to the compile order and the load order. For this purpose, the Android system makes a LookupTable mapping. The function of rewriteRValues is to reflect the onResourceLoaded method of R files in the shared resource library and override the packageID so that the app can look it up. Further analysis will be done later.

Then start with the loading of the classLoader.

GetClassLoader Loads Android ClassLoader
public ClassLoader getClassLoader() { synchronized (this) { if (mClassLoader == null) { createOrUpdateClassLoaderLocked(null /*addedPaths*/); } return mClassLoader; }}Copy the code
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) { if (mPackageName.equals("android")) { if (mClassLoader ! = null) { return; } if (mBaseClassLoader ! = null) { mClassLoader = mBaseClassLoader; } else { mClassLoader = ClassLoader.getSystemClassLoader(); } mAppComponentFactory = createAppFactory(mApplicationInfo, mClassLoader); return; } if (! Objects.equals(mPackageName, ActivityThread.currentPackageName()) && mIncludeCode) { try { ActivityThread.getPackageManager().notifyPackageUse(mPackageName, PackageManager.NOTIFY_PACKAGE_USE_CROSS_PACKAGE); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } } if (mRegisterPackage) { try { ActivityManager.getService().addPackageDependency(mPackageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } final List<String> zipPaths = new ArrayList<>(10); final List<String> libPaths = new ArrayList<>(10); boolean isBundledApp = mApplicationInfo.isSystemApp() && ! mApplicationInfo.isUpdatedSystemApp(); final String defaultSearchPaths = System.getProperty("java.library.path"); final boolean treatVendorApkAsUnbundled = ! defaultSearchPaths.contains("/vendor/lib"); if (mApplicationInfo.getCodePath() ! = null && mApplicationInfo.isVendor() && treatVendorApkAsUnbundled) { isBundledApp = false; } makePaths(mActivityThread, isBundledApp, mApplicationInfo, zipPaths, libPaths); String libraryPermittedPath = mDataDir; if (isBundledApp) { libraryPermittedPath += File.pathSeparator + Paths.get(getAppDir()).getParent().toString(); libraryPermittedPath += File.pathSeparator + defaultSearchPaths; } final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths); . final String zip = (zipPaths.size() == 1) ? zipPaths.get(0) : TextUtils.join(File.pathSeparator, zipPaths); boolean needToSetupJitProfiles = false; if (mClassLoader == null) { .. mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath, libraryPermittedPath, mBaseClassLoader, mApplicationInfo.classLoaderName); mAppComponentFactory = createAppFactory(mApplicationInfo, mClassLoader); . needToSetupJitProfiles = true; } if (! libPaths.isEmpty() && SystemProperties.getBoolean(PROPERTY_NAME_APPEND_NATIVE, true)) { ... try { ApplicationLoaders.getDefault().addNative(mClassLoader, libPaths); } finally { ... } } List<String> extraLibPaths = new ArrayList<>(3); String abiSuffix = VMRuntime.getRuntime().is64Bit() ? "64" : ""; if (! defaultSearchPaths.contains("/vendor/lib")) { extraLibPaths.add("/vendor/lib" + abiSuffix); } if (! defaultSearchPaths.contains("/odm/lib")) { extraLibPaths.add("/odm/lib" + abiSuffix); } if (! defaultSearchPaths.contains("/product/lib")) { extraLibPaths.add("/product/lib" + abiSuffix); } if (! extraLibPaths.isEmpty()) { ... try { ApplicationLoaders.getDefault().addNative(mClassLoader, extraLibPaths); } finally { ... } } if (addedPaths ! = null && addedPaths.size() > 0) { final String add = TextUtils.join(File.pathSeparator, addedPaths); ApplicationLoaders.getDefault().addPath(mClassLoader, add); needToSetupJitProfiles = true; } if (needToSetupJitProfiles && ! ActivityThread.isSystem()) { setupJitProfileSupport(); }}Copy the code

One of the things you actually do in this process is plug-in the Android ClassLoader component mentioned above.

There is no Application ClassLoader object at the beginning of the Application process. The ClassLoader object unique to the Android Application is loaded only when the Application is instantiated. You can also see that if the package name is the android start of the system, the application’s ClassLoader object will not be loaded.

When the current App is identified as BundleApp, it indicates that part of the code is in Google service, and the directory for downloading dex in the future will be set in advance, and then it will be loaded from there.

Then do the following two things:

  • 1. ApplicationLoaders. GetDefault (.) getClassLoader according to parse from PMS ApplicationInfo all the resources in the path, generate a Application of this
  • 2. ApplicationLoaders. GetDefault () get so addNative added in the system, as well as the application of the so. And call the so library in the system
ApplicationLoaders.getDefault().getClassLoader
    private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
                                       String librarySearchPath, String libraryPermittedPath,
                                       ClassLoader parent, String cacheKey,
                                       String classLoaderName) {

        ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

        synchronized (mLoaders) {
            if (parent == null) {
                parent = baseParent;
            }

            if (parent == baseParent) {
                ClassLoader loader = mLoaders.get(cacheKey);
                if (loader != null) {
                    return loader;
                }

                ClassLoader classloader = ClassLoaderFactory.createClassLoader(
                        zip,  librarySearchPath, libraryPermittedPath, parent,
                        targetSdkVersion, isBundled, classLoaderName);

                GraphicsEnvironment.getInstance().setLayerPaths(
                        classloader, librarySearchPath, libraryPermittedPath);

                mLoaders.put(cacheKey, classloader);
                return classloader;
            }

            ClassLoader loader = ClassLoaderFactory.createClassLoader(
                    zip, null, parent, classLoaderName);
            return loader;
        }
    }
Copy the code

Parent is null when creating an Application. Parent is the parent that gets the getSystemClassLoader.

BaseParent must be the parent. Try from mLoaders through key (the apk package path name) find a cache, finally through ClassLoaderFactory. Create this createClassLoader, finally added to the cache.

Then it’s worth taking a look at a few classLoaders to see if they are what I said in plug-in.

The composition of this
public abstract class ClassLoader { static private class SystemClassLoader { public static ClassLoader loader = ClassLoader.createSystemClassLoader(); } private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); } @CallerSensitive public static ClassLoader getSystemClassLoader() { return SystemClassLoader.loader; }}Copy the code

GetSystemClassLoader gets a SystemClassLoader from a singleton, and the system loader is the PathClassLoader. This object is inherited as follows:

class PathClassLoader extends BaseDexClassLoader 
Copy the code
public class BaseDexClassLoader extends ClassLoader 
Copy the code

The Classloader base where you can see everything at this point is BaseDexClassLoader, and DexClassLoader is used to load external dex files as well. The PathClassLoader is the default class loader initialized in the system and used to load the dex file in the path. Therefore, in the system application, the default is PathClassLoader.

For a moment, the parent of getSystemClassLoader is actually the BootLoader, which starts the class loader as shown above. Next look at the createClassLoader method in the ClassLoader factory.

ClassLoaderFactory.createClassLoader
public static boolean isPathClassLoaderName(String name) { return name == null || PATH_CLASS_LOADER_NAME.equals(name) ||  DEX_CLASS_LOADER_NAME.equals(name); } public static ClassLoader createClassLoader(String dexPath, String librarySearchPath, ClassLoader parent, String classloaderName) { if (isPathClassLoaderName(classloaderName)) { return new PathClassLoader(dexPath, librarySearchPath, parent); } else if (isDelegateLastClassLoaderName(classloaderName)) { return new DelegateLastClassLoader(dexPath, librarySearchPath, parent); } throw new AssertionError("Invalid classLoaderName: " + classloaderName); } public static ClassLoader createClassLoader(String dexPath, String librarySearchPath, String libraryPermittedPath, ClassLoader parent, int targetSdkVersion, boolean isNamespaceShared, String classloaderName) { final ClassLoader classLoader = createClassLoader(dexPath, librarySearchPath, parent, classloaderName); boolean isForVendor = false; for (String path : dexPath.split(":")) { if (path.startsWith("/vendor/")) { isForVendor = true; break; } } String errorMessage = createClassloaderNamespace(classLoader, targetSdkVersion, librarySearchPath, libraryPermittedPath, isNamespaceShared, isForVendor); return classLoader; }Copy the code

In this factory, you can see that if the name passed is null or the name is PathClassLoader or DexClassLoader the PathClassLoader is created by default, And through the native method createClassloaderNamespace loading this native layer.

Classloaders have a mechanism called parent delegation. The current ClassLoader does not immediately look for the class, but keeps delegating to the root of the class, and eventually finds the lower class.

If you look at it from the perspective of parent delegation, essentially in a general App application, one layer is BootClassLoader which means Bootstrap, and the other layer is PathClassLoader which is our App class loader.

Android R file overload

There’s a method called rewriteRValues that a lot of people ignore. In general, this method is rarely used. However, if you follow my previous article on resource loading, you will know that there is a special kind of library in Android, which is a resource-sharing library with no code. The packageID of this library is usually 0x00. LookUpTable is built from the ground up to map packageids that are inconsistent between compile and load times. However, there is no way to find the resource properly because we normally load the resource sharing library through r.xx.xx. Therefore, you need to merge the contents of the resource sharing library into the current package name.

private void rewriteRValues(ClassLoader cl, String packageName, int id) { final Class<? > rClazz; try { rClazz = cl.loadClass(packageName + ".R"); } catch (ClassNotFoundException e) { return; } final Method callback; try { callback = rClazz.getMethod("onResourcesLoaded", int.class); } catch (NoSuchMethodException e) { return; } Throwable cause; try { callback.invoke(null, id); return; } catch (IllegalAccessException e) { cause = e; } catch (InvocationTargetException e) { cause = e.getCause(); } throw new RuntimeException("Failed to rewrite resource references for " + packageName, cause); }Copy the code

You can see that the onResourcesLoaded method in the R file corresponding to the current package name is called by reflection. If we open r. Java, we will not find this method at all. R.java exists only if it is linked to the resource sharing library.

To this end, I looked through the source code of AAPT, the packaging tool of AS, and had the opportunity to try to go over the source code in detail. Here is the sequence diagram of the process generated by a resource R file:

As can be seen from the figure above, the core logic is as follows: When aAPT’s main method is parsed to the resource referenced in p parameter, doPackage will be called. The first attempt is to call writeSymbolClass package to generate r. Java file, and assign the int value of the resource in each R file to the value.

One of the core logic in doPackage is

err = writeResourceSymbols(bundle, assets, assets->getPackage(), true,            
bundle->getBuildSharedLibrary() || bundle->getBuildAppAsSharedLibrary());
Copy the code

You can see that if you compile a shared library, the flag bit is turned on by the aAPT resource packaging command: “-shared-lib”.

So what does this method do? Its core logic is as follows:

Class<? >[] declaredClasses = rClazz.getDeclaredClasses(); for (Class<? > clazz : declaredClasses) { try { if (clazz.getSimpleName().equals("styleable")) { for (Field field : clazz.getDeclaredFields()) { if (field.getType() == int[].class) { rewriteIntArrayField(field, id); } } } else { for (Field field : clazz.getDeclaredFields()) { rewriteIntField(field, id); } } } catch (Exception e) { ... }}Copy the code

The packageId value in R file is overwritten by reflection. The invalid 0x00 is overwritten to the packageId value after the current repository is loaded. Then the App can access the resource sharing file normally.

conclusion

The following is done during process initialization:

  • 1. Assign a ProcessRecord object to each generated process and cache it with a PID key
  • 2. Each process has its own ANR, but the process does not have its own ANR box. During this process, AMS will have a 10-second delay event. If the process does not dismantle the bomb after starting, it will exit.

During ActivityThread initialization, the following is done:

  • 1. Bind the mediator ApplicationThread in ActivityThread, which acts as cross-process communication, to the AMS counterpart ProcessRecord.
  • 2. Bind death listening again
  • 3. The demolition process starts the bomb
  • 4. Invoke bindApplication to initialize the Application
  • 5. Start the four components that have been started

During Application initialization, the following steps are performed:

  • 1. Load the resource configuration
  • 2. Generate LoadedApk objects
    1. The makeApplication loads the PathClassLoader, creating a ContextImpl object
  • 4. Reflection generates an Application object and invokes attachBaseContext
    1. RewriteRValues Overwrites the packageID in the resource library.
  • 6. Call Applcation’s onCreate method.

thinking

With this logic in mind, let’s talk about how MultiDex works. As we all know, There are 65536 ways to load dex on Android, and the general solution is MultiDex. What’s going on inside the library? Why do we need to place the install method in attachBaseContext when using it?

It’s pretty simple in essence, and what you’re really doing is loading the dex. This process is the dexElements object in the pathList object of the Hook PathClassLoader, which loads the smaller dex file into the main dex.

In essence, the idea is to break the whole into parts. If you have a dex more than 65536, you can break it into several small ones and load them in one by one, so as to break through the limit in a disguised way.

Now that we know how MultiDex works, it’s easy to understand why we put the install method in attachBaseContext. Because attachBaseContext is the earliest replacement in the entire App lifecycle. You can load your own PathClassLoader without even calling application.oncreate without affecting your business.

So what’s the downside? ANR lag occurs when dex files are loaded in the main thread. A number of teams have attempted to solve this problem by using asynchronous threads or asynchronous processes to call the install method, but the dex has not yet been loaded into memory, but when you want to call it, the class cannot find an exception.

To deal with this problem, some vendors are hook Activity startup processes that will display a middle page or wait when the class is not found.

With an open mind, if only to improve the startup speed, you can actually use Redex to rearrange the dex used at the beginning into the main dex and the dex used less into other sub-dex, so that asynchronous calls to multidex. install can skip these unnatural interactions.

Author: yjy239 links: www.jianshu.com/p/2b1d43ffe… The copyright of the book belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.