从Launcher分析ClassLoader与Multidex的不解情缘

前言

本篇文章比较长,涉及到Launcher启动APPClassLoader工作过程以及Multidex解决方案,可以静下心来看,整个看完一套流程很多东西自然就相通了~~

Launcher

首先 Launcher这个东西我们不用把他想的太复杂,可以简单认为它就是一个桌面应用,也就是说它也是个Activity,拥有Activity同样的生命周期,系统启动的时候启动了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 mOriginalValues;

        protected final IconResizer mIconResizer;
        protected final LayoutInflater mInflater;

        protected List 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 makeListItems() {
        // Load all matching activities and sort correctly
        List list = onQueryPackageManager(mIntent);
        onSortResultList(list);

        ArrayList result = new ArrayList(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中有些关键数据labeliconpackageNameresolveInfo,从ResolveInfo的数据结构来看,这个是不是就是我们要启动的那个Activity的所有信息?我查找了一下ResolveInfo的引用,只有在PackageManagerService里面查到相关引用,PackageManagerService是管理所有包的service

private ResolveInfo chooseBestActivity(Intent intent, String resolvedType,
            int flags, List 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 ?,举个例子,我们在调用系统分享的时候经常会弹窗分享到哪里,因为是通过隐式启动调用的,就可能会存在多个符合规则的分享页面供用户选择

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 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”字符串,反射出该对象并执行ActivityThread的main方法对其进行初始化.

Launcher的分析到此先告一段落,如若想了解详细的Activity启动流程,点击传送门->Activity启动流程

image.png

ClassLoader

JVM类加载机制

根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。例如java.lang.Object就是由根类加载器加载的。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。

扩展(Extension)类加载器:它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。

系统(System)类加载器:也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。

ClassLoader的双亲委托模式:classloader 按级别分为三个级别:最上级 : bootstrap classLoader(根类加载器) ; 中间级:extension classLoader (扩展类加载器) 最低级 app classLoader(应用类加载器)

双亲委托的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载,这样可以避免重复以及无效加载

image.png
DVM类加载机制

主要是由BaseDexClassLoader的两个子类 PathClassLoader、DexClassLoader 来完成。继承自Java的ClassLoader。
PathClassLoader :用来加载系统类和应用类。只能加载已安装的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 suppressedExceptions = new ArrayList();
    //从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的构造方法,只是用这些参数,创建了一个DexPathList的实例,DexPathList 使用 Element 数组存储了所有的 dex 信息,在 dex 被存到 Element 数组后,所有的类都会在 Element 数组中寻找,不会再次从文件中加载,此处可作用于热修复以及下文介绍的Multidex合并

DexPathList.java

 public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;
    ArrayList suppressedExceptions = new ArrayList();
    // 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从上面看来只有一个参数的区分,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(reinterpret_cast(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 是否为空,分别执行不同的方法

1.当 outputName 不为空时【DexClassLoader】

执行FindOrCreateOatFileForDexLocation函数,通过 outputName拿到 oat_location ,然后尝试调用 FindDexFileInOatLocation 从 oat_location 中寻找到 dex ,这就是我们经常用到到热修复的原理了,通过在sd卡中存放新的补丁dex/jar/apk替代旧的,来实现更新。

2.当 outputName 为空时【PathClassLoader】

执行 FindDexFileInOatFileFromDexLocation 函数,从 dex_location 中拿到 dex 文件,这个 dex_location 也就是 BaseDexClassLoader 的 dexPath 参数中分割出来的某个存放文件的路径。在 Android 中,系统使用 PathClassLoader 来加载apk中的dex存放到Element数组中,因此apk中的classes.dex都是通过它来加载的。

到这里我们对经常接触的PathClassLoaderDexClassLoaderBaseDexClassLoader、都有了一定程度的了解,现在思考一个问题我们在上文通过Launcher启动一个APP,那肯定是通过PathClassLoader来加载所有的Dex,我有点好奇这个PathClassLoader是什么时候被加载的呢?我们继续跟踪源码,我们现在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,这里看到一些眼熟的东西,创建主线程的Looper,通过H绑定Application,创建ContextImpl,创建ApplicationLoaders,这里面会创建parentCloassLoader,也就是BootClassLoader

image.png

将parentCloassLoader传递给ClassLoaderFactory, 查看ClassLoaderFactory.createClassLoader

public static ClassLoader createClassLoader(String dexPath,
            String librarySearchPath, ClassLoader parent, String classloaderName,
            List 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系统在安装应用时会将dex文件转换成odex,odex相当于从安装包中将dex解压出来同时在文件格式上有一定优化执行效率会比dex高,这个过程称之为DexOpt。在早期的Android系统中,DexOpt有一个问题,DexOpt会把dex中所有类的方法id检索起来,存在一个链表结构里面,但是这个链表的长度是用一个short类型来保存的,short为两个byte也就是16位,2的16次方即65536,导致了方法id的数目不能够超过65536个。随着Android的迅速发展一个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及之前的低版本设备才存在这个问题,随着后续高版本的覆盖这类机型会越来越少,所以这里不做Multidex源码分析,有兴趣的话可以参考Multidex源码分析,我们之前讲到BaseDexClassLoader中会将dex 文件解析出来放在pathList中,Multidex.install的主要步骤:

1.反射获取ClassLoader中的pathList字段
2.反射调用DexPathList对象中的makeDexElements方法,将刚刚提取出来的zip文件包装成Element对象
3.将包装成的Element对象扩展到DexPathList中的dexElements数组字段里
4.makeDexElements中有dexopt的操作,是一个耗时的过程,产物是一个优化过的odex文件

google方案碰到的问题

黑屏、假死(ANR):随着APP越来越大,最终分包会得到很多dex,而且odex是一个非常耗时的操作,所以假死是经常碰到的问题
LinearAlloc限制:dexopt使用LinearAlloc来存储应用的方法信息。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃,由于此类机型占比非常低这个问题也可以认为它不是个问题

google方案优化之一:异步进程加载Multidex

另起一个多进程的Activity来加载Multidex,同时让主进程处于休眠状态,加载进程加载完毕之后自行关闭,主进程在进行后续操作,直接撸代码吧

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.创建临时文件充当Multidex.install加载完成的flag,当加载进程加载完毕时会将此文件删除
4.启动LoadMultidexActivity
5.阻塞Application,直到文件flag被删除
6.重新调用Multidex.install,多进程ClassLoader不同,此步骤只是为了替换ClassLoader,由于dexopt等耗时操作都在加载进程中完成了,所以这个步骤会非常快

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转发到ActivityThread中,由于此时主线程处于休眠状态时被阻塞的,广播在阻塞时是进不来的。

google方案优化之二:脚本自定义分包,自定义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
https://www.jianshu.com/p/a4e68316d8eb
http://gityuan.com/2017/03/19/android-classloader/
https://blog.csdn.net/u012267215/article/details/91406211
https://www.jianshu.com/p/e164ee033928
https://tech.meituan.com/2015/06/15/mt-android-auto-split-dex.html

你可能感兴趣的:(从Launcher分析ClassLoader与Multidex的不解情缘)