從Launcher分析ClassLoader與Multidex的不解情緣

前言

本篇文章比較長,涉及到Launcher啟動APPClassLoader工作過程以及Multidex解決方案,可以靜下心來看,整個看完一套流程很多東西自然就相通了~~

Launcher

首先 Launcher這個東西我們不用把他想的太復雜,可以簡單認為它就是一個桌面應用,也就是說它也是個Activity,擁有Activity同樣的生命周期,系統(tǒng)啟動的時候啟動了Launcher這個桌面應用。

image.png

我們再看一下LauncherActivity的源碼,這個Activity是個抽象類,并不是實際我們看到展示在桌面上的Activity,桌面的Activity應該是繼承了LauncherActivity

//抽象類
public abstract class LauncherActivity extends ListActivity

@Override
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        //拿到PackageManager用于獲取所有安裝包信息
        mPackageManager = getPackageManager();

        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
            requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
            setProgressBarIndeterminateVisibility(true);
        }
        onSetContentView();

        mIconResizer = new IconResizer();
        
        mIntent = new Intent(getTargetIntent());
        mIntent.setComponent(null);
        //設置適配器
        mAdapter = new ActivityAdapter(mIconResizer);

        setListAdapter(mAdapter);
        getListView().setTextFilterEnabled(true);

        updateAlertTitle();
        updateButtonText();

        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
            setProgressBarIndeterminateVisibility(false);
        }
    }
//適配器
private class ActivityAdapter extends BaseAdapter implements Filterable {
        private final Object lock = new Object();
        private ArrayList<ListItem> mOriginalValues;

        protected final IconResizer mIconResizer;
        protected final LayoutInflater mInflater;

        protected List<ListItem> mActivitiesList;

        private Filter mFilter;
        private final boolean mShowIcons;
        
        public ActivityAdapter(IconResizer resizer) {
            mIconResizer = resizer;
            mInflater = (LayoutInflater) LauncherActivity.this.getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
            mShowIcons = onEvaluateShowIcons();
            mActivitiesList = makeListItems();
        }
}
//獲取Items
 public List<ListItem> makeListItems() {
        // Load all matching activities and sort correctly
        List<ResolveInfo> list = onQueryPackageManager(mIntent);
        onSortResultList(list);

        ArrayList<ListItem> result = new ArrayList<ListItem>(list.size());
        int listSize = list.size();
        for (int i = 0; i < listSize; i++) {
            ResolveInfo resolveInfo = list.get(i);
            result.add(new ListItem(mPackageManager, resolveInfo, null));
        }

        return result;
    }

//ListItem的數據結構
    public static class ListItem {
        public ResolveInfo resolveInfo;
        //appname,對應AndroidManifest Application label ?
        public CharSequence label;
        //圖標
        public Drawable icon;
        //包名
        public String packageName;
        public String className;
        public Bundle extras;
}
//ResolveInfo數據結構
public ResolveInfo(ResolveInfo orig) {
        activityInfo = orig.activityInfo;
        serviceInfo = orig.serviceInfo;
        providerInfo = orig.providerInfo;
        filter = orig.filter;
        priority = orig.priority;
        preferredOrder = orig.preferredOrder;
        match = orig.match;
        specificIndex = orig.specificIndex;
        labelRes = orig.labelRes;
        nonLocalizedLabel = orig.nonLocalizedLabel;
        icon = orig.icon;
        resolvePackageName = orig.resolvePackageName;
        noResourceId = orig.noResourceId;
        iconResourceId = orig.iconResourceId;
        system = orig.system;
        targetUserId = orig.targetUserId;
        handleAllWebDataURI = orig.handleAllWebDataURI;
        isInstantAppAvailable = orig.isInstantAppAvailable;
        instantAppAvailable = isInstantAppAvailable;
    }

以上這段代碼已經很清晰了,LauncherActivity中通過PackageManager拿到適配器的所有數據ListItem列表,單個ListItem中有些關鍵數據label、icon、packageName、resolveInfo,從ResolveInfo的數據結構來看,這個是不是就是我們要啟動的那個Activity的所有信息?我查找了一下ResolveInfo的引用,只有在PackageManagerService里面查到相關引用,PackageManagerService是管理所有包的service

private ResolveInfo chooseBestActivity(Intent intent, String resolvedType,
            int flags, List<ResolveInfo> query, int userId) {
        if (query != null) {
            final int N = query.size();
            if (N == 1) {
                return query.get(0);
            } else if (N > 1) {
                final boolean debug = ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0);
                // If there is more than one activity with the same priority,
                // then let the user decide between them.
                ResolveInfo r0 = query.get(0);
                ResolveInfo r1 = query.get(1);
                if (DEBUG_INTENT_MATCHING || debug) {
                    Slog.v(TAG, r0.activityInfo.name + "=" + r0.priority + " vs "
                            + r1.activityInfo.name + "=" + r1.priority);
                }
                // If the first activity has a higher priority, or a different
                // default, then it is always desirable to pick it.
                if (r0.priority != r1.priority
                        || r0.preferredOrder != r1.preferredOrder
                        || r0.isDefault != r1.isDefault) {
                    return query.get(0);
                }
                // If we have saved a preference for a preferred activity for
                // this Intent, use that.
                ResolveInfo ri = findPreferredActivityNotLocked(intent, resolvedType,
                        flags, query, r0.priority, true, false, debug, userId);
                if (ri != null) {
                    return ri;
                }
                // If we have an ephemeral app, use it
                for (int i = 0; i < N; i++) {
                    ri = query.get(i);
                    if (ri.activityInfo.applicationInfo.isInstantApp()) {
                        final String packageName = ri.activityInfo.packageName;
                        final PackageSetting ps = mSettings.mPackages.get(packageName);
                        final long packedStatus = getDomainVerificationStatusLPr(ps, userId);
                        final int status = (int)(packedStatus >> 32);
                        if (status != INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK) {
                            return ri;
                        }
                    }
                }
                ri = new ResolveInfo(mResolveInfo);
                ri.activityInfo = new ActivityInfo(ri.activityInfo);
                ri.activityInfo.labelRes = ResolverActivity.getLabelRes(intent.getAction());
                // If all of the options come from the same package, show the application's
                // label and icon instead of the generic resolver's.
                // Some calls like Intent.resolveActivityInfo query the ResolveInfo from here
                // and then throw away the ResolveInfo itself, meaning that the caller loses
                // the resolvePackageName. Therefore the activityInfo.labelRes above provides
                // a fallback for this case; we only set the target package's resources on
                // the ResolveInfo, not the ActivityInfo.
                final String intentPackage = intent.getPackage();
                if (!TextUtils.isEmpty(intentPackage) && allHavePackage(query, intentPackage)) {
                    final ApplicationInfo appi = query.get(0).activityInfo.applicationInfo;
                    ri.resolvePackageName = intentPackage;
                    if (userNeedsBadging(userId)) {
                        ri.noResourceId = true;
                    } else {
                        ri.icon = appi.icon;
                    }
                    ri.iconResourceId = appi.icon;
                    ri.labelRes = appi.labelRes;
                }
                ri.activityInfo.applicationInfo = new ApplicationInfo(
                        ri.activityInfo.applicationInfo);
                if (userId != 0) {
                    ri.activityInfo.applicationInfo.uid = UserHandle.getUid(userId,
                            UserHandle.getAppId(ri.activityInfo.applicationInfo.uid));
                }
                // Make sure that the resolver is displayable in car mode
                if (ri.activityInfo.metaData == null) ri.activityInfo.metaData = new Bundle();
                ri.activityInfo.metaData.putBoolean(Intent.METADATA_DOCK_HOME, true);
                return ri;
            }
        }
        return null;
    }

其中chooseBestActivity為什么會有多個ResolveInfo ?,舉個例子,我們在調用系統(tǒng)分享的時候經常會彈窗分享到哪里,因為是通過隱式啟動調用的,就可能會存在多個符合規(guī)則的分享頁面供用戶選擇

image.png

以上關于ResolveInfo只是個人看法,還存在疑問,如有異議請及時指正,感激不敬~~

Launcher 啟動APP

當用戶點擊圖標的時候,就相當于LauncherActivity去啟動另外一個Activity,關于Activity的啟動這里稍微解釋一下,主要流程如下

https://blog.csdn.net/u012267215/article/details/91406211.png

在Activity啟動流程中,有個關鍵的點是當需要啟動的Activity的進程還沒有啟動需要通過Zygote去fork一個新的進程,移步AMS Activity啟動源碼

###ActivityManagerService.java
 @Override
        public void startProcess(String processName, ApplicationInfo info,
                boolean knownToBeDead, String hostingType, ComponentName hostingName) {
            try {
                if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "startProcess:"
                            + processName);
                }
                synchronized (ActivityManagerService.this) {
                    startProcessLocked(processName, info, knownToBeDead, 0 /* intentFlags */,
                            new HostingRecord(hostingType, hostingName),
                            false /* allowWhileBooting */, false /* isolated */,
                            true /* keepIfLarge */);
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            }
        }

###ProcessList.java
@GuardedBy("mService")
    final boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
            String abiOverride) {
        return startProcessLocked(app, hostingRecord,
                false /* disableHiddenApiChecks */, false /* mountExtStorageFull */, abiOverride);
    }

private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, String entryPoint,
            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
            String seInfo, String requiredAbi, String instructionSet, String invokeWith,
            long startTime) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +
                    app.processName);
            checkSlow(startTime, "startProcess: asking zygote to start proc");
            final Process.ProcessStartResult startResult;
            if (hostingRecord.usesWebviewZygote()) {
                startResult = startWebView(entryPoint,
                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                        app.info.dataDir, null, app.info.packageName,
                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});
            } else if (hostingRecord.usesAppZygote()) {
                final AppZygote appZygote = createAppZygoteForProcessIfNeeded(app);

                startResult = appZygote.getProcess().start(entryPoint,
                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                        app.info.dataDir, null, app.info.packageName,
                        /*useUsapPool=*/ false,
                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});
            } else {
                startResult = Process.start(entryPoint,
                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                        app.info.dataDir, invokeWith, app.info.packageName,
                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});
            }
            checkSlow(startTime, "startProcess: returned from zygote!");
            return startResult;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        }
    }

###Process.java
public static ProcessStartResult start(@NonNull final String processClass,
                                           @Nullable final String niceName,
                                           int uid, int gid, @Nullable int[] gids,
                                           int runtimeFlags,
                                           int mountExternal,
                                           int targetSdkVersion,
                                           @Nullable String seInfo,
                                           @NonNull String abi,
                                           @Nullable String instructionSet,
                                           @Nullable String appDataDir,
                                           @Nullable String invokeWith,
                                           @Nullable String packageName,
                                           @Nullable String[] zygoteArgs) {
        return ZYGOTE_PROCESS.start(processClass, niceName, uid, gid, gids,
                    runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                    abi, instructionSet, appDataDir, invokeWith, packageName,
                    /*useUsapPool=*/ true, zygoteArgs);
    }

#ZygoteProcess.java
private Process.ProcessStartResult startViaZygote(@NonNull final String processClass,
                                                      @Nullable final String niceName,
                                                      final int uid, final int gid,
                                                      @Nullable final int[] gids,
                                                      int runtimeFlags, int mountExternal,
                                                      int targetSdkVersion,
                                                      @Nullable String seInfo,
                                                      @NonNull String abi,
                                                      @Nullable String instructionSet,
                                                      @Nullable String appDataDir,
                                                      @Nullable String invokeWith,
                                                      boolean startChildZygote,
                                                      @Nullable String packageName,
                                                      boolean useUsapPool,
                                                      @Nullable String[] extraArgs)
                                                      throws ZygoteStartFailedEx {
        ArrayList<String> argsForZygote = new ArrayList<>();

        // --runtime-args, --setuid=, --setgid=,
        // and --setgroups= must go first
        argsForZygote.add("--runtime-args");
        argsForZygote.add("--setuid=" + uid);
        argsForZygote.add("--setgid=" + gid);
        argsForZygote.add("--runtime-flags=" + runtimeFlags);
        if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) {
            argsForZygote.add("--mount-external-default");
        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_READ) {
            argsForZygote.add("--mount-external-read");
        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_WRITE) {
            argsForZygote.add("--mount-external-write");
        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_FULL) {
            argsForZygote.add("--mount-external-full");
        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_INSTALLER) {
            argsForZygote.add("--mount-external-installer");
        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_LEGACY) {
            argsForZygote.add("--mount-external-legacy");
        }
        ...
        synchronized(mLock) {
            // The USAP pool can not be used if the application will not use the systems graphics
            // driver.  If that driver is requested use the Zygote application start path.
            return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),
                                              useUsapPool,
                                              argsForZygote);
        }
    }

 @GuardedBy("mLock")
    private void attemptConnectionToPrimaryZygote() throws IOException {
        if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
            primaryZygoteState =
                    ZygoteState.connect(mZygoteSocketAddress, mUsapPoolSocketAddress);

            maybeSetApiBlacklistExemptions(primaryZygoteState, false);
            maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
            maybeSetHiddenApiAccessStatslogSampleRate(primaryZygoteState);
        }
    }

ActivityManagerService.startProcess跳轉到ProcessList.startProcess,再經過Process.start跳轉到ZygoteProcess.startViaZygote的,最終在attemptConnectionToPrimaryZygote通過socket通信的方式讓Zygote進程fork出一個新的進程,并根據傳遞的”android.app.ActivityThread”字符串,反射出該對象并執(zhí)行ActivityThread的main方法對其進行初始化.

Launcher的分析到此先告一段落,如若想了解詳細的Activity啟動流程,點擊傳送門->Activity啟動流程

image.png

ClassLoader

JVM類加載機制

根(Bootstrap)類加載器:該加載器沒有父加載器。它負責加載虛擬機的核心類庫,如java.lang.*等。例如java.lang.Object就是由根類加載器加載的。根類加載器從系統(tǒng)屬性sun.boot.class.path所指定的目錄中加載類庫。根類加載器的實現依賴于底層操作系統(tǒng),屬于虛擬機的實現的一部分,它并沒有繼承java.lang.ClassLoader類。

擴展(Extension)類加載器:它的父加載器為根類加載器。它從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫,如果把用戶創(chuàng)建的JAR文件放在這個目錄下,也會自動由擴展類加載器加載。擴展類加載器是純Java類,是java.lang.ClassLoader類的子類。

系統(tǒng)(System)類加載器:也稱為應用類加載器,它的父加載器為擴展類加載器。它從環(huán)境變量classpath或者系統(tǒng)屬性java.class.path所指定的目錄中加載類,它是用戶自定義的類加載器的默認父加載器。系統(tǒng)類加載器是純Java類,是java.lang.ClassLoader類的子類。

ClassLoader的雙親委托模式:classloader 按級別分為三個級別:最上級 : bootstrap classLoader(根類加載器) ; 中間級:extension classLoader (擴展類加載器) 最低級 app classLoader(應用類加載器)

雙親委托的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啟動類加載器,只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索范圍中沒有找到對應的類)時,子加載器才會嘗試自己去加載,這樣可以避免重復以及無效加載

image.png
DVM類加載機制

主要是由BaseDexClassLoader的兩個子類 PathClassLoader、DexClassLoader 來完成。繼承自Java的ClassLoader。
PathClassLoader :用來加載系統(tǒng)類和應用類。只能加載已安裝的apk。
DexClassLoader :用來加載jar、apk、dex文件。從SD卡中加載未安裝的apk,插件化的主要加載實現

image.png

ClassLoader.java

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    //委托雙親加載
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

BaseDexClassLoader.java

 public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
 @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //從pathList的Element數組中找類,找不到就報ClassNotFoundException
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

BaseDexClassLoader的構造方法,只是用這些參數,創(chuàng)建了一個DexPathList的實例,DexPathList 使用 Element 數組存儲了所有的 dex 信息,在 dex 被存到 Element 數組后,所有的類都會在 Element 數組中尋找,不會再次從文件中加載,此處可作用于熱修復以及下文介紹的Multidex合并

DexPathList.java

 public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    // save dexPath for BaseDexClassLoader
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions,definingContext);
    ...
}

DexPathList 通過 makeDexElements 得到了一個 Element[]類型的 dexElements對象數組,里面存放了app的所有dex相關信息

PathClassLoader.java

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

DexClassLoader.java

 public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}

PathClassLoaderDexClassLoader從上面看來只有一個參數的區(qū)分,DexClassLoader多傳了一個optimizedDirectory路徑,這個是用來緩存我們需要加載的dex文件的,其實也能猜想到,PathClassLoader用來加載安裝過的APK,那么這個Dex的緩存路徑其實已經是已知的,但是DexClassLoader加載的是沒有安裝過的APK所以需要指定Dex的緩存路徑,我們再跟蹤一下DexFile的源碼:

DexFile .java

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

DexClassLoader.java

 public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}

static jint DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {    
    ...
    const DexFile* dex_file;    
    if (outputName.c_str() == NULL) {// 如果outputName為空,則dex_file由sourceName確定
        dex_file = linker->FindDexFileInOatFileFromDexLocation(dex_location, dex_location_checksum); 
    } else {// 如果outputName不為空,則在outputName目錄中去尋找dex_file
        std::string oat_location(outputName.c_str());    
        dex_file = linker->FindOrCreateOatFileForDexLocation(dex_location, dex_location_checksum, oat_location);  
    }    
    ...
    return static_cast<jint>(reinterpret_cast<uintptr_t>(dex_file));    
}

const DexFile* ClassLinker::FindOrCreateOatFileForDexLocation(const std::string& dex_location,uint32_t dex_location_checksum,const std::string& oat_location) {
    WriterMutexLock mu(Thread::Current(), dex_lock_);   // 互鎖
    return FindOrCreateOatFileForDexLocationLocked(dex_location, dex_location_checksum, oat_location);
}

const DexFile* ClassLinker::FindOrCreateOatFileForDexLocationLocked(const std::string& dex_location,uint32_t dex_location_checksum,const std::string& oat_location) {
    const DexFile* dex_file = FindDexFileInOatLocation(dex_location,dex_location_checksum,oat_location);
    if (dex_file != NULL) {
        // 如果順利打開,則返回
        return dex_file;
    }
    const OatFile* oat_file = OatFile::Open(oat_location, oat_location, NULL,!Runtime::Current()->IsCompiler());
    if (oat_file == NULL) {
        return NULL;
    }
    const OatFile::OatDexFile* oat_dex_file = oat_file->GetOatDexFile(dex_location, &dex_location_checksum);
    if (oat_dex_file == NULL) {
        return NULL;
    }
    const DexFile* result = oat_dex_file->OpenDexFile();
    return result;
}

在 DexFile 的構造方法中,調用了 openDexFile 去生成一個 mCookie,可以看到,不管是哪個類型的ClassLoader,最終都會調用 native 方法 openDexFileNative 來實現具體的加載邏輯。判斷傳入的 outputName 是否為空,分別執(zhí)行不同的方法

1.當 outputName 不為空時【DexClassLoader】

執(zhí)行FindOrCreateOatFileForDexLocation函數,通過 outputName拿到 oat_location ,然后嘗試調用 FindDexFileInOatLocation 從 oat_location 中尋找到 dex ,這就是我們經常用到到熱修復的原理了,通過在sd卡中存放新的補丁dex/jar/apk替代舊的,來實現更新。

2.當 outputName 為空時【PathClassLoader】

執(zhí)行 FindDexFileInOatFileFromDexLocation 函數,從 dex_location 中拿到 dex 文件,這個 dex_location 也就是 BaseDexClassLoader 的 dexPath 參數中分割出來的某個存放文件的路徑。在 Android 中,系統(tǒng)使用 PathClassLoader 來加載apk中的dex存放到Element數組中,因此apk中的classes.dex都是通過它來加載的。

到這里我們對經常接觸的PathClassLoaderDexClassLoader、BaseDexClassLoader、都有了一定程度的了解,現在思考一個問題我們在上文通過Launcher啟動一個APP,那肯定是通過PathClassLoader來加載所有的Dex,我有點好奇這個PathClassLoader是什么時候被加載的呢?我們繼續(xù)跟蹤源碼,我們現在ClassLoader的構造函數中打個斷點,然后Debug APP

image.png

看一下調用鏈

image.png

這個結果一幕了然,接著上文說到Zygotefork出我們的進程之后,走到ZygoteInit.java中的main方法

@UnsupportedAppUsage
    public static void main(String argv[]) {
        ZygoteServer zygoteServer = null;

        /
        Runnable caller;
        try {
          ...
            zygoteServer = new ZygoteServer(isPrimaryZygote);
            ...
            Log.i(TAG, "Accepting command socket connections");

            // The select loop returns early in the child process after a fork and
            // loops forever in the zygote.
            caller = zygoteServer.runSelectLoop(abiList);
        } catch (Throwable ex) {
            Log.e(TAG, "System zygote died with exception", ex);
            throw ex;
        } finally {
            if (zygoteServer != null) {
                zygoteServer.closeServerSocket();
            }
        }

        // We're in the child process and have exited the select loop. Proceed to execute the
        // command.
        if (caller != null) {
            caller.run();
        }
    }

通過zygoteServer.runSelectLoop拿到caller,這是一個MethodAndArgsCaller,這里面有個反射方法

static class MethodAndArgsCaller implements Runnable {
        /** method to call */
        private final Method mMethod;

        /** argument array */
        private final String[] mArgs;

        public MethodAndArgsCaller(Method method, String[] args) {
            mMethod = method;
            mArgs = args;
        }

        public void run() {
            try {
                mMethod.invoke(null, new Object[] { mArgs });
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            } catch (InvocationTargetException ex) {
                Throwable cause = ex.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else if (cause instanceof Error) {
                    throw (Error) cause;
                }
                throw new RuntimeException(ex);
            }
        }
    }

通過這個方法跳轉到我們熟悉的ActivityThreadmain,這里看到一些眼熟的東西,創(chuàng)建主線程的Looper,通過H綁定Application,創(chuàng)建ContextImpl,創(chuàng)建ApplicationLoaders,這里面會創(chuàng)建parentCloassLoader,也就是BootClassLoader

image.png

將parentCloassLoader傳遞給ClassLoaderFactory, 查看ClassLoaderFactory.createClassLoader

public static ClassLoader createClassLoader(String dexPath,
            String librarySearchPath, ClassLoader parent, String classloaderName,
            List<ClassLoader> sharedLibraries) {
        ClassLoader[] arrayOfSharedLibraries = (sharedLibraries == null)
                ? null
                : sharedLibraries.toArray(new ClassLoader[sharedLibraries.size()]);
        if (isPathClassLoaderName(classloaderName)) {
            return new PathClassLoader(dexPath, librarySearchPath, parent, arrayOfSharedLibraries);
        } else if (isDelegateLastClassLoaderName(classloaderName)) {
            return new DelegateLastClassLoader(dexPath, librarySearchPath, parent,
                    arrayOfSharedLibraries);
        }

        throw new AssertionError("Invalid classLoaderName: " + classloaderName);
    }

嘿嘿,是不是看見我們要找的PathClassLoader了,到這里關于ClassLoader差不多也介紹完了~

image.png

Multidex分析

Multidex的由來:Android系統(tǒng)在安裝應用時會將dex文件轉換成odex,odex相當于從安裝包中將dex解壓出來同時在文件格式上有一定優(yōu)化執(zhí)行效率會比dex高,這個過程稱之為DexOpt。在早期的Android系統(tǒng)中,DexOpt有一個問題,DexOpt會把dex中所有類的方法id檢索起來,存在一個鏈表結構里面,但是這個鏈表的長度是用一個short類型來保存的,short為兩個byte也就是16位,2的16次方即65536,導致了方法id的數目不能夠超過65536個。隨著Android的迅速發(fā)展一個APP所有的方法數肯定會超過65536,所以google推出了Multidex方案去解決這個問題。

使用google的方案:
defaultConfig {
       //開啟multidex
        multiDexEnabled true
    }
dependencies {
   //multidex庫依賴
    implementation 'com.android.support:multidex:1.0.3'
}
public class MineApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //Multidex合并
        MultiDex.install(base);
    }
}

Multidex只在API 19及之前的低版本設備才存在這個問題,隨著后續(xù)高版本的覆蓋這類機型會越來越少,所以這里不做Multidex源碼分析,有興趣的話可以參考Multidex源碼分析,我們之前講到BaseDexClassLoader中會將dex 文件解析出來放在pathList中,Multidex.install的主要步驟:

1.反射獲取ClassLoader中的pathList字段
2.反射調用DexPathList對象中的makeDexElements方法,將剛剛提取出來的zip文件包裝成Element對象
3.將包裝成的Element對象擴展到DexPathList中的dexElements數組字段里
4.makeDexElements中有dexopt的操作,是一個耗時的過程,產物是一個優(yōu)化過的odex文件

google方案碰到的問題

黑屏、假死(ANR):隨著APP越來越大,最終分包會得到很多dex,而且odex是一個非常耗時的操作,所以假死是經常碰到的問題
LinearAlloc限制:dexopt使用LinearAlloc來存儲應用的方法信息。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB或16MB。當方法數量過多導致超出緩沖區(qū)大小時,會造成dexopt崩潰,由于此類機型占比非常低這個問題也可以認為它不是個問題

google方案優(yōu)化之一:異步進程加載Multidex

另起一個多進程的Activity來加載Multidex,同時讓主進程處于休眠狀態(tài),加載進程加載完畢之后自行關閉,主進程在進行后續(xù)操作,直接擼代碼吧

MineApplication .java

public class MineApplication extends Application {
    private final String TAG = getClass().getSimpleName();
    private File multidexFlagFile = null;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //主進程并且vm不支持多dex的情況下才使用 Multidex
        if (ProcessUtil.isMainAppProcess(base) && MultidexUtils.supportMultidex()) {
            loadMultiDex(base);
        }
    }

    private void loadMultiDex(final Context context) {
        createMultidexFlagFile(context);
        //啟動多進程去加載MultiDex
        Intent intent = new Intent(context, LoadMultidexActivity.class);
        intent.putExtra(LoadMultidexActivity.MULTIDEX_FLAG_FILE, multidexFlagFile.getPath());
        intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
        //阻塞當前線程,在Application中是不會出現anr的
        while (multidexFlagFile.exists()) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long startTime = System.currentTimeMillis();
        MultiDex.install(context);
        Log.e(TAG, TAG + " const:" + (System.currentTimeMillis() - startTime));
    }

    private void createMultidexFlagFile(Context context) {
        try {
            multidexFlagFile = new File(context.getCacheDir().getAbsolutePath(), "multidex_flag.tmp");
            if (!multidexFlagFile.exists()) {
                Log.d(TAG, "crate multidex flag file success");
                multidexFlagFile.createNewFile();
            }
        } catch (Throwable th) {
            th.printStackTrace();
        }
    }
}

Application主要邏輯

1.判斷時是否主進程,因為開啟了多進程加載而我們只需要在主進程進行加載
2.判斷是否需要進行Multidex,此判斷copy Multidex.install()源碼
3.創(chuàng)建臨時文件充當Multidex.install加載完成的flag,當加載進程加載完畢時會將此文件刪除
4.啟動LoadMultidexActivity
5.阻塞Application,直到文件flag被刪除
6.重新調用Multidex.install,多進程ClassLoader不同,此步驟只是為了替換ClassLoader,由于dexopt等耗時操作都在加載進程中完成了,所以這個步驟會非???/p>

LoadMultidexActivity.class

public class LoadMultidexActivity extends Activity {
    public static final String MULTIDEX_FLAG_FILE = "MULTIDEX_FLAG_FILE";
    String path = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_load_multidex);
        path = getIntent().getStringExtra(MULTIDEX_FLAG_FILE);
        new Thread(new Runnable() {
            @Override
            public void run() {
                long time = System.currentTimeMillis();
                MultiDex.install(LoadMultidexActivity.this);
                try {
                    //模擬加載耗時
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.e("MineApplication", "LoadMultidexActivity const:" + (System.currentTimeMillis() - time));
                File file = new File(path);
                if (file.exists()) {
                    file.delete();
                    Log.e("MineApplication", "delete flag file");
                }
                finish();
            }
        }).start();
    }

    @Override
    public void onBackPressed() {
          //不響應返回鍵
    }
}

1.另開一條線程去處理Multidex.install防止ANR
2.不響應返回鍵,防止用戶關閉該Activity
3.處理完刪除flag文件同時關閉Activity

源碼Demo傳送門

此方案碰到的問題:ClassNotFoundException,在Multidex.install合并之前APP會先啟動一個主Dex,假如LoadMultidexActivity不在主Dex,ClassLoader.findClass就會拋出這個問題,可以通過配置multiDexKeepFile或者multiDexKeepProguard解決,也就是指定某些類放入主Dex

defaultConfig {
        multiDexEnabled true
        multiDexKeepFile file('maindexlist.keep')
    }

maindexlist.keep 文件內容

com/code/multidexinstall/LoadMultidexActivity.class
com/code/multidexinstall/LoadMultidexActivity$1.class

對于此方案有些人會懷疑Application被阻塞時如果有廣播進來會不會導致ANR,我也寫了個demo去驗證這個問題,結果表明這個問題時不存在的,廣播確實能收到,但是時在Multidex.install之后才打印了相關日志,斷點一下廣播的接受流程


image.png

廣播也是通過Loop然后通過H轉發(fā)到ActivityThread中,由于此時主線程處于休眠狀態(tài)時被阻塞的,廣播在阻塞時是進不來的。

google方案優(yōu)化之二:腳本自定義分包,自定義Multidex加載邏輯

這種方案涉及到的點比較多,要求較高,主要邏輯為
1:編寫腳本干預分包流程將部分一級頁面打入主dex
2:將Multidex.install流程另起service去操作,Application中不進行等待直接跳進主界面,由于主界面的Activity都在maindex中所以不會出現ClassNotFoundException
3.防止在Multidex.install完成之前用戶點擊跳轉不在maindex中的頁面,Hook Instrumentation.execStartActivity(和插件化異曲同工),展示loading等待Multidex.install完成在做跳入
具體參考:https://tech.meituan.com/2015/06/15/mt-android-auto-split-dex.html

Uncle_D.png

本文參考以下內容:https://blog.csdn.net/black_dreamer/article/details/81665728
https://blog.csdn.net/csdn_aiyang/article/details/76199823
http://www.itdecent.cn/p/a4e68316d8eb
http://gityuan.com/2017/03/19/android-classloader/
https://blog.csdn.net/u012267215/article/details/91406211
http://www.itdecent.cn/p/e164ee033928
https://tech.meituan.com/2015/06/15/mt-android-auto-split-dex.html

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容