Skip to content
limpoxe edited this page Apr 23, 2021 · 2 revisions

其他使用指南

1. 如何使非独立插件依赖其他插件

例:插件A依赖插件B,则需在插件A的manifest文件中的application节点下增加如下配置:

    <uses-library android:name="这里填写被依赖的插件B的包名" android:required="true" />
   
    并在插件A的build.gradle文件中使用provided添加对插件B的jar的依赖。
    
   此处uses-library与其原始含义无关,仅作一个配置项取巧使用。
   限制:被插件依赖的插件只可以包含class和Manifest和assets等文件,不可以携带资源文件。可参考demo中的pluginTestBase工程

2. 如何使独立插件依赖其他插件

同上。

3. 如何将插件中的Fragment嵌入宿主中的Activity中

首先,需要将此宿主Activity的android:process属性配置为插件进程(插件的代码需在插件进程运行,因此fragment的所在的Activity也需要在插件进程中)

然后,在此插件的AndroidManifest.xml配置节点:

       <meta-data android:tag="exported-fragment" android:name="这里为此Fragment设置一个唯一标识符Id" android:value="这里为此Fragment类全名"/>
   此配置的目的是为了框架能够快速查找到目标Fragment的类名,而不必对所有插件进行遍历。
        
    在宿主Activity中创建此插件Fragment的实例:
    Class clazz = PluginLoader.loadPluginFragmentClassById(这里填写Fragment的唯一标识符Id);
    if (clazz != null) {
        Fragment fragment = (Fragment) clazz.newInstance();
    }
    可参考demo中的plugintest工程。
    另:根据对宿主Activity的配置,此Fragment的写法又分为两种,见下文。

4. 如何将插件的Fragment嵌入其他插件的Activity中

同上。区别在于此插件Activity无需再配置进程属性,因为插件就是运行在插件进程中。

5. 插件UI可通过Fragment、pluginView或者直接使用Activity来实现

    如果是Fragment,又分为3种:
      1、Fragment运行在宿主中的任意的Activity中
      2、Fragment运行在宿主中的指定的Activity中
      3、Fragment运行在插件中的任意Activity中

    首先需要明确的是,无论是哪种情况,插件Fragment中的Context都必须使用插件自身的Context。
    之所以有上述3种情况,即是根据在插件Fragment中获取Context的方式不同来区分。
    
    第1种,在插件Fragment中通过常规方法获取到的Context都是宿主Context,不可用于插件。
    所以,需要约束Fragment中凡是要使用context的地方,都需要使用通过
          PluginLoader.getDefaultPluginContext(FragmentClass)或者context.createPackageContext("插件包名")
    来获取的插件context。
    注意,对这种Fragment中调用其中View.getContext()返回的类型不是其所在的Activity,强转Activity会出错。其真实类型是PluginContextTheme。
    若需要获取所在的Activity,需要通过View.getParent.getContext() 返回其所在的Activity对象, getParent的目的是拿到宿主控件

    第2种,由于是"运行在宿主中的指定的Activity中",因此,仅需在指定的Activity上添加@PluginContainer注解,框架会根据此注解自动修正Activity的Context。
    使得在插件Fragment中通过常规方法获取到的Context即是插件Context,可直接使用。也即对Fragment无约束。
    
    第3种,对Activity和Fragment都无特别要求。
      
    总结:
       如果插件Fragment不是运行在自己的插件提供的Activity中,则要么约束Fragment的context的获取方法,要么约束其运行时的容器Activity的Context(通
       过@PluginContainer注解指明使用哪个插件的Context)
       
       如果插件Fragment是运行在自己的插件提供的Activity中,则对Fragment和Activity都无特别要求。
           
    demo中都有例子。

6. 如何将插件中的View是嵌入宿主中的Activity中使用

鉴于实际情况中一些特殊场景可能不适合使用Fragment,框架也支持直接将插件View嵌入宿主Activity中使用。

首先,需要将此宿主Activity的android:process属性配置为插件进程,在宿主的布局文件中嵌入pluginView 节点,通过class="View的类全名"来指定使用的控件,

其他用法和普通控件无异,具体参考demo

    这些可以被嵌入插件view的宿主Activity,需要在Activity的class上添加@PluginContainer注解(无需指插件包名,插件包名通过pluginView节点指定),
    框架借此来识别pluginVie标签并解析出插件包名。
    
    注意,在嵌入式插件View中调用getContext()返回的类型不是其所在的Activity,而是插件PluginContextTheme类型。
    若想获得所在宿主的Activity对象,需要使用((PluginContextTheme)View.getContext()).getOuter()获取,
    或者通过View.getParent.getContext()来获取, getParent的目的是拿到宿主控件。

7. 如何在插件使用宿主中定义的主题

    插件中可以直接使用宿主中定义的主题。
    例如,宿主中定义了一个主题为AppHostTheme,那么插件的Manifest文件中可直接使用android:theme="@style/AppHostTheme"来使用宿主主题
    如果插件要扩展宿主主题,也可以直接使用。例如,在插件的style.xml中定义一个主题PluginTheme:<style name="PluginTheme" parent="AppHostTheme" />
    以上写法,IDE中可能会标红,但是不影响编译和运行。

8. 如何在插件使用宿主中定义的其他资源

分为在代码中和在资源文件中两种。代码中直接通过R即可使用。资源中,插件中可以直接使用宿主中定义的资源,但是写法和直接使用插件自己的资源略有不同, 通常应写为:

    android:label="@string/string_in_plugin"
    
    但是上面只适用资源在插件中的情况,如果资源是定义在宿主中,应使用下面的写法
    
    android:label="@*xx.xx.xx:string/string_in_host"
    
    其中,xx.xx.xx是宿主包名

    以上写法,IDE中可能会标红,但是不影响编译和运行。

注意本条与上一条区别:插件中使用宿主主题,可以直接使用,但是使用宿主资源,需要带上*packageName:前缀

9. 如何在插件中对外提供函数式服务(非Service组件,支持同进程和跨进程)

    插件和外界交互,除了可以使用标准api交互之外,还提供了函数式服务
    插件发布一个函数服务,只需要在AndroidManifest.xml配置节点
    <meta-data android:tag="exported-service" android:name="service名称"
                   android:value="service的实现类"
                   android:label="service的接口"/>
    其他插件或者宿主即可调用此服务,具体参考demo

10. 如何在插件中获取宿主包名

插件getPackageName返回的是插件包名。如果要在插件中获取宿主包名,使用框架提供的FakeUtil.getHostPackageName()函数,参数为插件的context,例如插件activity或者插件Application

11. 如何在插件中使用需要appkey的三方sdk

     插件中使用需要appkey的sdk,例如将微信分享、百度地图sdk、友盟分享sdk集成到插件,请参考demo:baidumapsdk,wxsdklibrary,仔细阅读。
     
     需要使用appKey的sdk,通常需要使得sdk能同时正确的取到下面3个值,
         1、packageName
         2、meta-data
         3、signatures
     SDK取到这3个值以后,会去它自身的服务器上验证appkey。
     
     然而,在插件中调用getPackageName等等相应的系统api,得到的是插件的packageName,插件的meta-data,以及插件的signatures。
     
     所以,在sdk平台上注册appkey时直接使用插件的包名,签名,然后将appkey的配置埋入插件的meta-data, 
     此种情况无需特别配置。插件集成此sdk即可正常使用,例如百度地图SDK即符合此种情形。
     
     但是,实际应用中仍然会存在下面2种情况:
           1、可能sdk需要通过其自身的app来进行校验或者交互。例如微信分享sdk,它需要唤醒微信App,再由微信App和宿主App进行验证和交互(第三方app要唤起插件中的静态组件必须由宿
              主程序进行桥接,方法请参看wxsdklibrary工程的用法),绕过了插件。
              此种sdk在平台上注册appkey时必须使用宿主的包名
           
           2、可能由于特殊原因,在sdk平台上注册appkey时已经使用了宿主的包名,业务上不能再更换使用插件的包名进行注册。
           
           以上两种情况,sdk在拿到插件的Context以后(通常是在sdk的init方法里面传入的插件Application),
           sdk借助插件Context取不到正确的packageName、meta-data、signatures。正确的值全部在宿主中,插件拿到的全部是插件自己的。
           
     sdk在获取packageName、meta-data、signatures这3个信息,都是通过传入的Context调用其getXXXX或者context.getPackageManager().getXXX来获取。
     因此,针对这两种case,需要在初始化插件sdk是,传入fakeContext而不是插件的Context来欺骗sdk,使其能拿到正确信息。
    
     在demo中,微信sdk插件的FakeContext,即是用来解决上面所说的第一种情况。
     
     demo中的fakeContext重写了需要的相关方法。
     
     如要使用此类插件,请务必先完全理解上述解释,以及为何使用FakeContext可以达成目的。然后遇到各种相关问题都可迎刃而解。

12. 如何混淆宿主和插件

若需要混淆宿主,请参考PluginMain工程下的混淆配置,以防止宿主混淆后框架异常。

若需要混淆非独立插件,步骤如下:
         
     1、在宿主中开启混淆编译
     2、在插件中开启混淆编译
     3、宿主和插件的proguardRule文件中配置配置禁止压缩:-dontshrink
     
        因为若宿主中的某些类或者方法,没有在宿主中使用过,则宿主在混淆的时候可能会删除了这些类和方法。
        然而插件有可能会使用这些被删减的类和方法,这种情况需要在编译时宿主禁用代码压缩
        
     执行这3个步骤之后,编译出来的非独立插件即为混淆后的插件
     
     若混淆后出现运行时异常,请检查临时文件是否存在不该存在的类或者少了需要的类。
     文件位于build/tmp/jarUnzip/host, 以及build/tmp/jarUnzip/plugin;
     host目录为宿主编译出来混淆后的jar包解压后目录,等同于对宿主反编译后得到class目录
     plugin为插件编译出来混淆后的jar包解压后目录。正常情况下host目录的内容应该为plugin目录内容的子集。
     且host目录存在的每一个文件,必定在plugin相同路径下存在,否则很可能是依赖配置错误或者mapping文件配置错误,会导致脚本在做diff时出现遗漏而引起class异常
     插件最终的混淆后jar包,即是通过这两个目录diff后从plugin中剔除了所有在host中存在的文件后压缩而成。
     
     插件混淆后的jar包和diff后的jar包,在插件outputs目录下都有备份。
     
     ---------------------------------------------------
     以Demo为例,启用PluginTest插件的Debug版本的混淆,方法如下:
        1、修改PluginMain工程的build.gradle中的buildTypes.debug.minifyEnabled为true
        2、修改PluginTest工程的build.gradle中的buildTypes.debug.minifyEnabled为true
        3、检查宿主和插件的proguard-rules.pro文件,确保插件和宿主的混淆规则中都配置了禁止压缩:-dontshrink
        4、在settings.gradle中注释掉PluginTest2; 
        5、clean && assembleDebug

        这里需要注意的是插件开启混淆以后,需要在插件的proguard里面增加对插件Fragment的keep,否则如果此fragment没有在插件自身
        使用,仅作为嵌入宿主使用,则progurad可能误以为这个类在插件中没有被使用过而被精简掉

13. 如何使外部应用或者系统可以直接通过插件组件的Intent打开插件

由于插件并没有正常安装到系统中,插件组件的Intent不能被系统识别,因此外部应用或者系统需要直接唤起插件组件时,需要将插件Intent在宿主的Manifest中        
也预置一份,并在IntentFilter增加STUB_EXACT配置,如:
    //添加Receeiver桥接
    <receiver android:name="com.example.plugintest.receiver.BootCompletedReceiver"
          android:process=":plugin">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
            <action android:name="android.intent.action.ACTION_SHUTDOWN"/>
        </intent-filter>
        <!--下面是额外添加的配置项,作用是使得框架将此组件配置识别为插件组件 -->
        <intent-filter>
            <action
                android:name="${applicationId}.STUB_EXACT" />
            <category
                android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </receiver>
    
    //添加Activity桥接
    <activity
        android:name="com.example.pluginmain.wxapi.WXEntryActivity"
        android:process=":plugin"
        android:exported="true">
        <!--下面是额外添加的配置项,作用是使得框架将此组件配置识别为插件组件 -->
        <intent-filter>
            <action
                android:name="${applicationId}.STUB_EXACT" />
            <category
                android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>

    //添加provider桥接--如果插件中的provider只是提供给插件或者宿主使用,不需要添加桥接
    //插件的provider要提供给其他app调用时需要添加,典型的如FileProvider
    <provider
        android:name="com.limpoxe.fairy.core.bridge.ProviderClientUnsafeProxy$ProviderClientUnsafeProxy0"
        android:authorities="a.b.c.fileprovider"
        android:process=":plugin"
        android:grantUriPermissions="true"
        android:exported="false">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepath" />
    </provider>

   可以参考demo        

14. 如何使插件返回宿主包名

默认情况下,插件getPackageName返回插件包名。虽然框架中会在一些系统api上将packageName修正为宿主的,
但是仍然可能有些特殊原因,比如在插件中接了一些三方sdk,sdk中有些api框架没有修正,导致出现packageName错误。

这种情况下,如果通过上面提到的fakeContext无法解决,可以使用useHostPackageName="true"这个配置指定插件使用宿主包名。
将此配置加在插件manifest文件的<manifest/>节点中
Demo参考Admob。

15. 如何添加自定义的Stub

框架内置的stub模版有限,特别是对Activity来说,配置组合起来会比较多,若插件stub模版不满足要求时,可以通过添加自定义的stub映射处理器来进行stub映射
使用 FairyGlobal.registStubMappingProcessor() 来添加自定义的stub映射处理器

先在宿主的manifest添加stub:
```Xml
        <activity
        android:name="${applicationId}.stub.XXX" //名字随便写
        android:exported="true"
        android:process=":plugin"
	      
    //添加一些需要的配置	      
        android:process=":plugin"
        android:screenOrientation="sensor"
        android:configChanges="orientation"
	      
        android:theme="@android:style/Theme">
    </activity>
```

定义一个映射器(实现部分仅供参考,方便理解接口含义)
```Java

public class TestCoustProcessor implements StubMappingProcessor {

    private android.util.Pair<String, String> pair;

    @Override
    public int getType() {
	return TYPE_ACTIVITY;
    }

    @Override
    public String bindStub(PluginDescriptor pluginDescriptor, String pluginComponentClassName) {
	if (pluginComponentClassName.equals("x.y.z.in.pulgin.ABC")) {//填写要绑定的插件中的组件名称
	    String stub = "xx.xx.xx.in.host.XXX"; //填写在宿主manifest增加的stub
	    pair = new Pair<>(stub, pluginComponentClassName);
	    return pair.first;
	}
	return null;
    }

    @Override
    public void unBindStub(String stubClassName, String pluginStubClass) {
	if (pair != null && pair.first.equals(stubClassName) && pair.second.equals(pluginStubClass)) {
	    pair = null;
	}
    }

    @Override
    public boolean isStub(String stubClassName) {
	String stub = "xx.xx.xx.in.host.XXX"; //填写在宿主manifest增加的stub
	return stubClassName.equals(stub);
    }

    @Override
    public String getBindedPluginClassName(String stubClassName) {
	if (pair != null && pair.first.equals(stubClassName)) {
	    return pair.second;
	}
	return null;
    }
}

```

在调用框架初始化函数之前,注册这个映射器
```
FairyGlobal.registStubMappingProcessor(new TestCoustProcessor());
```

注意事项

1、非独立插件中的class不能同时存在于宿主和插件程序中
  
   如果插件和宿主共享依赖库,常见的如supportv4,那么编译插件的时候不可将共享库编译到插件当中,包括共享库的代码以及R文件。
   只需在编译时以provided方式添加到classpath中,公共库仅参与编译,不参与打包。参看demo。

2、若插件中包含so,则需要在宿主的相应目录下添加至少一个so文件,以确保插件和宿主支持的so种类完全相同

   例如:插件包含armv7a、arm64两种so文件,则宿主必须至少包含一个armv7a的so以及一个arm64的so。
   若宿主本身不需要so文件,可随意创建一个假so文件放在宿主的相应目录下。例如pluginMain工程中的libstub.so其实只是一个txt文件。

   需要占位so的原因是,宿主在安装时,系统会扫描宿主中的so的(根据文件夹判断)类型,决定宿主在哪种cpu模式下运行、并保持到系统设置里面。
   (系统源码可查看com.android.server.pm.PackageManagerService.setBundledAppAbisAndRoots()方法)
   例如32、64、还是x86等等。如果宿主中不包含so,系统默认会选择一个最适合当前设备的模式。
   那么问题来了,如果系统默认选择的模式,和将来下载安装的插件中支持的so模式不匹配,则会出现so异常。

   因此需要提前通知系统,宿主需要在哪些cpu模式下运行。提前通知的方式即内置占位so。

3、框架中对非独立插件做了签名校验。如果宿主是release模式,要求插件的签名和宿主的签名一致才允许安装。
   这是为了验证插件来源合法性

4、需要在android studio中关闭instantRun选项。因为instantRun会替换apk的application配置导致框架异常

5、对非独立插件而言,在宿主中定义的所有主题,在非独立插件中都可以直接使用
   不要在非独立插件中定义和宿主中相同的主题名称,否则会报各种重复定义的错误(其他资源可以同名重复,例如color、drawable)
   最常见的错误,例如新建一个宿主工程后,studio会默认创建一个AppTheme主题,
   再新建一个非独立插件工程,studio也会默认创建一个AppTheme主题,此时插件中编译就会报重复定义错误,
   因为非独立插件已经包含了所有宿主中定义的主题, 此时只需要删除调插件中的AppTheme主题即可

6、本项目除master分支外,其他分支不会更新维护。

7、如果是非独立插件, 需要先编译宿主, 再编译插件, 
   如果是非独立插件, 需要先编译宿主, 再编译插件
   如果是非独立插件, 需要先编译宿主, 再编译插件
   
   重要的事情讲3遍!遇到编译问题请先编译宿主, 再编译插件。因为从配置可以看出非独立插件编译时需要依赖宿主编译时的输出物
   
以上所有内容及更多详情可以参考Demo