几年前,系统厂商在给第三方或者兄弟部门,提供SDK时,大多数方案是提供通用的SDK源码方案。所谓的SDK源码,无非是写了很多文件,定义了很多接口,然后打包成一个jar文件,最后可能对外的接口不混淆,内部实现混淆一下,提供给其他人使用。随着业务的发展,比如说平台越来越多,SDK的最终实现可能都不一样,但是如果还是使用这种方案,是不现实的。
这时,SDK接口业务和实现的分离变得非常重要,假如给客户或者兄弟部门提供的SDK只是一个jar包,里面只是有普通的定义实现,但是真正的实现和普通APP无关,真正的实现是与平台相关,这样就可以实现SDK接口业务和实现的分离。分离后优点就不多说了。
但是怎么才能实现业务和实现分离呢?这时候,想到java的ClassLoader类加载机制,可不可以通过这个机制来实现这种方案呢?答案是肯定的。以Android6.0为例:
在Android中存在PathClassLoader.java和DexClassLoader.java 两种,它们均继承BaseDexClassLoader.java,位于aosp/libcore/dalvik/src/main/java/dalvik/system/目录下,这两个类加载器有什么区别呢,熟悉热修复或者插件化的同学都知道,可以用DexClassLoader来加载其他路径下的jar包或者apk文件,而PathClassLoader只能加载安装好的apk。其实,看了它们的源码就知道了。两个类的构造方法中,都是调用父类的构造方法,但是第二个参数,一个是new File(optimizedDirectory),另一个是null。也就是说DexClassLoader可以通过第二个参数传入 不同路径的apk或者jar包,从而能够加载这些,最终实现很多功能的。
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getCodeCacheDir();
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
* <ul>
* <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
* <li>Raw ".dex" files (not inside a zip file).
* </ul>
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
熟悉Android开发同学一定知道,可以在AndroidManifest.xml中添加uses-library依赖的库,比如
<uses-library android:name="xxxImpl"
android:required="false"/>
在系统中的话,在/system/etc/permissions/下面放一个xxxImpl.xml,xml的内容,如下所示
<permissions>
<library name="xxx"
file="/system/framework/xxxImpl.jar" />
</permissions>
但是,这个library是在什么时候加载的呢?为什么这么做就能起作用呢?其实,我们可以这样做,在一个类中,打印
Log.i("jin", this.getClass().getClassLoader().toString());
Log.i("xxx", this.getClass().getClassLoader().toString());
_______________________________________________________________
xxx : dalvik.system.PathClassLoader[DexPathList[
[
zip file "/system/framework/org.apache.http.legacy.boot.jar",
zip file "/system/framework/xxxImpl.jar",
zip file "/data/app/com.xxx.settings-
xXZARhRPaWBVZloW2Ri_HQ==/base.apk"],
nativeLibraryDirectories=
[/data/app/com.xxx.settings-xXZARhRPaWBVZloW2Ri_HQ==/lib/arm, /system/fake-libs,
/data/app/com.xxx.settings-xXZARhRPaWBVZloW2Ri_HQ==/base.apk!/lib/armeabi,
/system/lib, /product/lib]]
]
从log中可以很清晰的看到,我们类的classLoader是dalvik.system.PathClassLoader,然后加载的DexPathList中包含两部分,一部分是zip file,另一部分是nativeLibraryDirectories。并且可以看到,我们在AndroidManifest.xml中配置的 <uses-library android:name="xxxImpl" android:required="false"/>,在我们这个apk之前,有同学会问,这个顺序很重要吗?我们看看DexPathList.java中的代码,在findClass时候,会遍历dexElements,这个dexElements就是通过对路径分割,然后构造出的一个对象,如果找到了这个class,就会直接返回。如果有同名的class,加载顺序在原始的apk之前,那么就会实现替代原始apk的功能。其实很多热修复框架,比如Tinker,就是通过这个原理来实现的。
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;
}
接着说这个uses-library依赖的库,在上面的log中能清晰的看到,xxxImpl.jar是加载到了我们这个apk的zip路径中,而且是比我们的apk还要早,那么是谁完成这个功能的呢?
zip file "/system/framework/org.apache.http.legacy.boot.jar",
zip file "/system/framework/xxxImpl.jar",
zip file "/data/app/com.xxx.settings-xXZARhRPaWBVZloW2Ri_HQ==/base.apk"
答案在LoadedApk.java中,首先通过PackageParser.java在apk安装时候,对AndroidManifest.xml进行解析,然后将相应的信息存储到PackageParser$Pakcage这个类中,然后在app启动generateApplicationInfo时候,将这个PackageParser$Pakcage中的成员变量赋值给ApplicationInfo 的sharedLibraryFiles 。
4777 public static ApplicationInfo generateApplicationInfo(Package p, int flags,
4778 PackageUserState state, int userId) {
4779 if (p == null) return null;
4780 if (!checkUseInstalledOrHidden(flags, state)) {
4781 return null;
4782 }
4783 if (!copyNeeded(flags, p, state, null, userId)
4784 && ((flags&PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS) == 0
4785 || state.enabled != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED)) {
4786 // In this case it is safe to directly modify the internal ApplicationInfo state:
4787 // - CompatibilityMode is global state, so will be the same for every call.
4788 // - We only come in to here if the app should reported as installed; this is the
4789 // default state, and we will do a copy otherwise.
4790 // - The enable state will always be reported the same for the application across
4791 // calls; the only exception is for the UNTIL_USED mode, and in that case we will
4792 // be doing a copy.
4793 updateApplicationInfo(p.applicationInfo, flags, state);
4794 return p.applicationInfo;
4795 }
4796
4797 // Make shallow copy so we can store the metadata/libraries safely
4798 ApplicationInfo ai = new ApplicationInfo(p.applicationInfo);
4799 ai.uid = UserHandle.getUid(userId, ai.uid);
4800 ai.dataDir = Environment.getDataUserPackageDirectory(ai.volumeUuid, userId, ai.packageName)
4801 .getAbsolutePath();
4802 if ((flags & PackageManager.GET_META_DATA) != 0) {
4803 ai.metaData = p.mAppMetaData;
4804 }
4805 if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0) {
4806 ai.sharedLibraryFiles = p.usesLibraryFiles;
4807 }
4808 if (state.stopped) {
4809 ai.flags |= ApplicationInfo.FLAG_STOPPED;
4810 } else {
4811 ai.flags &= ~ApplicationInfo.FLAG_STOPPED;
4812 }
4813 updateApplicationInfo(ai, flags, state);
4814 return ai;
4815 }
然后在LoadedApk中的构造函数中,将ApplicationInfo 的sharedLibraryFiles赋值给LoadedApk的mSharedLibraries中,
mSharedLibraries = aInfo.sharedLibraryFiles;
紧接着,在LoadedApk的getClassLoader方法中,有个zipPaths,这个zipPaths先执行zipPaths.add(mAppDir);然后执行
if (mSharedLibraries != null) {
for (String lib : mSharedLibraries) {
if (!zipPaths.contains(lib)) {
zipPaths.add(0, lib);
}
}
}
注意这里mAppDir就是apk的sourcePath,然后zipPaths.add(0, lib),所以mSharedLibraries要排在了apk的sourcePath前面。
258 public ClassLoader getClassLoader() {
259 synchronized (this) {
260 if (mClassLoader != null) {
261 return mClassLoader;
262 }
263
264 if (mIncludeCode && !mPackageName.equals("android")) {
265 // Avoid the binder call when the package is the current application package.
266 // The activity manager will perform ensure that dexopt is performed before
267 // spinning up the process.
268 if (!Objects.equals(mPackageName, ActivityThread.currentPackageName())) {
269 final String isa = VMRuntime.getRuntime().vmInstructionSet();
270 try {
271 ActivityThread.getPackageManager().performDexOptIfNeeded(mPackageName, isa);
272 } catch (RemoteException re) {
273 // Ignored.
274 }
275 }
276
277 final List<String> zipPaths = new ArrayList<>();
278 final List<String> apkPaths = new ArrayList<>();
279 final List<String> libPaths = new ArrayList<>();
280
281 if (mRegisterPackage) {
282 try {
283 ActivityManagerNative.getDefault().addPackageDependency(mPackageName);
284 } catch (RemoteException e) {
285 }
286 }
287
288 zipPaths.add(mAppDir);
289 if (mSplitAppDirs != null) {
290 Collections.addAll(zipPaths, mSplitAppDirs);
291 }
292
293 libPaths.add(mLibDir);
294
295 /*
296 * The following is a bit of a hack to inject
297 * instrumentation into the system: If the app
298 * being started matches one of the instrumentation names,
299 * then we combine both the "instrumentation" and
300 * "instrumented" app into the path, along with the
301 * concatenation of both apps' shared library lists.
302 */
303
304 String instrumentationPackageName = mActivityThread.mInstrumentationPackageName;
305 String instrumentationAppDir = mActivityThread.mInstrumentationAppDir;
306 String[] instrumentationSplitAppDirs = mActivityThread.mInstrumentationSplitAppDirs;
307 String instrumentationLibDir = mActivityThread.mInstrumentationLibDir;
308
309 String instrumentedAppDir = mActivityThread.mInstrumentedAppDir;
310 String[] instrumentedSplitAppDirs = mActivityThread.mInstrumentedSplitAppDirs;
311 String instrumentedLibDir = mActivityThread.mInstrumentedLibDir;
312 String[] instrumentationLibs = null;
313
314 if (mAppDir.equals(instrumentationAppDir)
315 || mAppDir.equals(instrumentedAppDir)) {
316 zipPaths.clear();
317 zipPaths.add(instrumentationAppDir);
318 if (instrumentationSplitAppDirs != null) {
319 Collections.addAll(zipPaths, instrumentationSplitAppDirs);
320 }
321 zipPaths.add(instrumentedAppDir);
322 if (instrumentedSplitAppDirs != null) {
323 Collections.addAll(zipPaths, instrumentedSplitAppDirs);
324 }
325
326 libPaths.clear();
327 libPaths.add(instrumentationLibDir);
328 libPaths.add(instrumentedLibDir);
329
330 if (!instrumentedAppDir.equals(instrumentationAppDir)) {
331 instrumentationLibs = getLibrariesFor(instrumentationPackageName);
332 }
333 }
334
335 apkPaths.addAll(zipPaths);
336
337 if (mSharedLibraries != null) {
338 for (String lib : mSharedLibraries) {
339 if (!zipPaths.contains(lib)) {
340 zipPaths.add(0, lib);
341 }
342 }
343 }
344
345 if (instrumentationLibs != null) {
346 for (String lib : instrumentationLibs) {
347 if (!zipPaths.contains(lib)) {
348 zipPaths.add(0, lib);
349 }
350 }
351 }
352
353 final String zip = TextUtils.join(File.pathSeparator, zipPaths);
354
355 // Add path to libraries in apk for current abi
356 if (mApplicationInfo.primaryCpuAbi != null) {
357 for (String apk : apkPaths) {
358 libPaths.add(apk + "!/lib/" + mApplicationInfo.primaryCpuAbi);
359 }
360 }
361
362 final String lib = TextUtils.join(File.pathSeparator, libPaths);
363
364 /*
365 * With all the combination done (if necessary, actually
366 * create the class loader.
367 */
368
369 if (ActivityThread.localLOGV)
370 Slog.v(ActivityThread.TAG, "Class path: " + zip + ", JNI path: " + lib);
371
372 // Temporarily disable logging of disk reads on the Looper thread
373 // as this is early and necessary.
374 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
375
376 mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
377 mBaseClassLoader);
378
379 StrictMode.setThreadPolicy(oldPolicy);
380 } else {
381 if (mBaseClassLoader == null) {
382 mClassLoader = ClassLoader.getSystemClassLoader();
383 } else {
384 mClassLoader = mBaseClassLoader;
385 }
386 }
387 return mClassLoader;
388 }
389 }
讲了这么多,其实想要说的是SDK业务和实现的分离,就可以采取下面这种方式,
(1)写一个抽象类,有很多接口,然后一个DefaultImpl的实现类,实现前面的接口,但是里面没有具体的实现内容,然后一个proxy类。这些类打包成一个xxx.jar包,给其他app调用。app的AndroidManifest中加入 <uses-library android:name="xxxImpl" android:required="false"/>
(2)写一个Impl类,继承自1中的抽象类,然后实现具体的接口逻辑。有一个proxy类,注意1,2中proxy类是同样类名包名的,啧啧,大家应该知道为什么是同样包名类名吧。然后打包成xxxImpl.jar。
(3)/system/etc/permissions/下面放一个xxxImpl.xml,在/system/framework/下加入xxxImpl.jar。
这样,最关键的是xxx.jar和xxxImpl.jar两个jar包是怎么通过一个同名的Proxy类关联起来的呢?
我们看示例代码,在普通App中,我们可以这样访问
TestManager testManager = TestManager.getInstance();
testManager.test();
#####
public static TestManager getInstance() {
try {
return (TestManager)(TestContext.getInstance().getInstanceByClass(TestManager.class));
} catch (Exception e) {}
return null;
}
public static TestContext getInstance() {
if ((testContext == null)) {
synchronized (TestContext.class) {
if (testContext == null) {
testContext = PolicyFactory.getPolicy().makeTestContext();
}
}
}
return testContext;
}
public static PolicyBase getPolicy() {
if (sPolicy == null) {
synchronized (PolicyFactory.class) {
if (sPolicy == null) {
sPolicy = new Policy();
}
}
}
return sPolicy;
}
//xxx.jar中 sdk 中的Policy
public TestContext makeTestContext() {
return new TestContextDefaultImpl(true);
}
//xxxImpl.jar中的Policy
public TestContext makeTestContext() {
return new TestContextImpl();
}
由于xxxImpl.jar优先加载,所以在TestManager.getInstance()的调用过程中,会使用xxxImpl.jar中的Policy类,得到的是TestContextImpl类的实例对象,如果没有xxxImpl.jar这个jar包,则会得到TestContextDefaultImpl类的实例对象。这样,通过Policy类处于两个jar包中,并且包名也一样,通过类加载机制,实现了SDK实现和业务的分离。