Android SDK接口业务和实现分离

       几年前,系统厂商在给第三方或者兄弟部门,提供SDK时,大多数方案是提供通用的SDK源码方案。所谓的SDK源码,无非是写了很多文件,定义了很多接口,然后打包成一个jar文件,最后可能对外的接口不混淆,内部实现混淆一下,提供给其他人使用。随着业务的发展,比如说平台越来越多,SDK的最终实现可能都不一样,但是如果还是使用这种方案,是不现实的。

      这时,SDK接口业务和实现的分离变得非常重要,假如给客户或者兄弟部门提供的SDK只是一个jar包,里面只是有普通的定义实现,但是真正的实现和普通APP无关,真正的实现是与平台相关,这样就可以实现SDK接口业务和实现的分离。分离后优点就不多说了。

      但是怎么才能实现业务和实现分离呢?这时候,想到java的ClassLoader类加载机制,可不可以通过这个机制来实现这种方案呢?答案是肯定的。以Android6.0为例:

在Android中存在PathClassLoader.javaDexClassLoader.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实现和业务的分离。

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值