}
}
英语不是很好,下面我简单翻译一下?
dexPath: 字符串变量,包含类和资源的jar/apk文件列表,用File.pathSeparator分隔,在Android上默认为“:”。
optimizedDirectory:不推荐使用此参数,貌似是一个废弃的参数,据说是.dex文件的解压路径,自API级别26起不再生效,那么26之前是怎么用的呢,查了一下是通过 context.getCodeCacheDir()。
librarySearchPath: 包含native库的目录列表,C/C++库存放的路径,用File.pathSeparator分隔;可能为null。
parent: 父类加载器ClassLoader.
再看一下调用的父类BaseDexClassLoader的构造方法及核心方法
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
显然看出DexPathList这个成员对象的重要性,初始化构造方法的时候实例化DexPathList对象,同时,BaseDexClassLoader重写了父类findClass()方法,通过该方法进行类查找的时候,会委托给pathList对象的findClass()方法进行相应的类查找,下面继续查看DexPathList类的findClass方法:
final class DexPathList {
private Element[] dexElements;
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
...
}
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
return elements;
}
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
}
DexPathList构造方法被调用的时候其实就是通过makeDexElements方法把dexPath进行遍历,依次加载每个dex文件,然后通过数组Element[]存放,而在DexPathList类的findClass调用的时候,通过遍历Element[]的dex文件,在通过DexFile类的loadClassBinaryName()来加载类,如果不为空那么代表加载成功,并且返回class,否则返回null。
下面再来看一下基类的ClassLoader是如何实现的吧
public abstract class ClassLoader {
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
c = findClass(name);
}
}
return c;
}
}
这明显就是一个双亲委派模型,在类加载的时候,首先去查找这个类之前是否已经被加载过,如果加载过直接返回,否则委托父类加载器去查找,如果父类加载器找不到那么就去系统的BootstrapClass去查找,到最后还是找不到的话,那么就自己亲自上阵查找了。这样就避免了重复加载,实现了更加安全。
好了总结一下DexClassLoader的加载过程:loadClass->findClass->BaseDexClassLoader.findClass->DexPathList.findClass->loadDexFile->DexFile.loadClassBinaryName->DexFile.defineClass,大体上就这样么个过程吧。
[]( )资源加载
Android系统加载资源都是通过Resource资源对象来进行加载的,因此只需要添加资源(即apk文件)所在路径到AssetManager中,即可实现对插件资源的访问。
/**
-
Create a new AssetManager containing only the basic system assets.
-
Applications will not generally use this method, instead retrieving the
-
appropriate asset manager with {@link Resources#getAssets}. Not for
-
use by applications.
- @hide
*/
public AssetManager() {
final ApkAssets[] assets;
synchronized (sSync) {
createSystemAssetsInZygoteLocked();
assets = sSystemApkAssets;
}
mObject = nativeCreate();
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(hashCode());
}
// Always set the framework resources.
setApkAssets(assets, false /invalidateCaches/);
}
不难发现AssetManager的构造方法是@hide隐藏的api,所以不能直接使用,这里肯定是需要通过反射啦,不过有人说Android P不是对系统的隐藏Api做出了限制,因此插件化估计要凉凉,但是我想说现在一些主流的插件化技术基本都已经适配了Android9.0了,所以无需担心。下面先简单贴出Android资源的加载流程。关于插件化的资源加载可以参考下滴滴VirtualApk资源的加载思想 (传送门)
class ContextImpl extends Context {
//...
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration) {
//....
Resources resources = packageInfo.getResources(mainThread);
//....
}
//...
}
这里不去关注packageInfo是如何生成的,直接跟踪到下面去.
public final class LoadedApk {
private final String mResDir;
public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
CompatibilityInfo compatInfo, ClassLoader baseLoader,
boolean securityViolation, boolean includeCode, boolean registerPackage) {
final int myUid = Process.myUid();
aInfo = adjustNativeLibraryPaths(aInfo);
mActivityThread = activityThread;
mApplicationInfo = aInfo;
mPackageName = aInfo.packageName;
mAppDir = aInfo.sourceDir;
mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
// 注意一下这个sourceDir,这个是我们宿主的APK包在手机中的路径,宿主的资源通过此地址加载。
// 该值的生成涉及到PMS,暂时不进行分析。
// Full path to the base APK for this application.
//....
}
//....
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
//....
}
进入到ActivityThread.getTopLevelResources()的逻辑中
public final class ActivityThread {
Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
//我们暂时只关注下面这一段代码
AssetManager assets = new AssetManager();
if (assets.addAssetPath(resDir) == 0) { //此处将上面的mResDir,也就是宿主的APK在手机中的路径当做资源包添加到AssetManager里,则Resources对象可以通过AssetManager查找资源,此处见(老罗博客:Android应用程序资源的查找过程分析)
return null;
}
// 创建Resources对象,此处依赖AssetManager类来实现资源查找功能。
r = new Resources(assets, metrics, getConfiguration(), compInfo);
}
}
从上面的代码中我们知道了我们常用的Resources是如何生成的了,那么理论上插件也就按照如此方式生成一个Resources对象给自己用就可以了。
[]( )组件的加载
这个其实不能一概而论,因为Android拥有四大组件,分别为Activity、Service、ContentProvider、BoradCastRecevier,每个组件的属性及生命周期也不一样,所以关于插件中加载的组件就需要分别研究每个组件是如何加载的了。
简单拿Activity组件来说,现在一些主流的方式基本上都是通过“坑位”的思想,这个词最早据说也是来源于360,总的来说,先占坑,因为我们宿主app的Manifest中是不会去申请插件中的Activity的,那我就先占一个坑,欺骗系统,然后替换成插件中的Activity。这里可能需要多个坑位,因为一些资源属性都是可以动态配置的。比如launchMode、process、configChanges、theme等等。
这里还需要了解一下Activity的启动流程,这里我们可以简单看一下。
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
可以看出,我们平时startActivity其实都是通过调用startActivityForResult(),我们接下来继续看
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。