一、先记录一下使用过程中的要点:
1.宿主APP需要先运行一遍(编译APK或直接运行到手机),才能编译插件
2.宿主和插件的类名,资源名命名不要一样
3.插件apk如果放在SD卡,记得申请权限
4.插件打包必须签名
5.插件清单文件中有icon属性,插件中有application,不影响使用
6.插件的清单文件中可以带权限,宿主申请权限时同时要申请所有插件的权限
7.插件的样式都被宿主的application的样式取代了(有父子关系的样式好像可以生效)
我的解决方法时,application都是用noactionbar样式,然后需要单独样式的使用toolbar增加标题
8.增加资源文件后需要将宿主和插件都重新打包,不然各种崩溃(不知道有其它解决方法没)
这个packageid 在我们打包的过程中 就是在aapt 执行的时候用到,他有他自己的规范
1.PackageId:是包的Id值,Android中如果是第三方应用的话,这个值默认就是0x7F,系统应用的话就是0x01,具体我们可以后面看aapt源码得知,他占用两个字节
2.TypeId:是资源的类型Id值,一般Android中有这几个类型:attr,drawable,layout,dimen,string,style等,而且这些类型的值是从1开始逐渐递增的,而且顺序不能改变,attr=0x01,drawable=0x02....他占用两个字节。
3.EntryId:是在具体的类型下资源实体的id值,从0开始,依次递增,他占用四个字节。
资源ID(packageId+typeId+ItemValue)
高两个字节是代表PackageId的值,而且第三方app的默认值是0x7F,那么我们能不能修改这个值呢?比如,插件1中的资源Id中的PackageId为0x30,插件2中的资源Id中的PackageId为0x31...这样每个插件的资源就被划分了一定的区域值,同时保证不要和主工程中的0x7F冲突即可,那么这些值就可以从0x02~0x7E了,这个区间值我们都是可以使用的,为什么0x01不能用呢?因为他是系统应用的呀,所以我们就有0x7E-0x02=124个区间,
二、配置如下:
1.宿主
宿主工程根目录的build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.didi.virtualapk:gradle:0.9.8.6' // 这个是需要加的
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
宿主app的build.gradle如下:
apply plugin: 'com.android.application'
apply plugin: 'com.didi.virtualapk.host' // 这个是主项目中添加的
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.example.lhy.mainproject"
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.didi.virtualapk:core:0.9.8'
}
宿主继承Application的代码
public class BaseApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
@Override
public void onCreate() {
super.onCreate();
PluginManager pluginManager = PluginManager.getInstance(this);
//此处是当查看插件apk是否存在,如果存在就去加载(比如修改线上的bug,把插件apk下载到sdcard的根目录下取名为Demo.apk)
File apk = new File(Environment.getExternalStorageDirectory(), "plugin1.apk");
if (apk.exists()) {
try {
pluginManager.loadPlugin(apk);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
宿主主页(只是为了测试使用,要根据实际需要修改)
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private Button toPlugin1Btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
toPlugin1Btn = findViewById(R.id.main_btn1);
toPlugin1Btn.setOnClickListener(this);
// 权限检查
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},1);
return;
}else {
File apk = new File(Environment.getExternalStorageDirectory(), "plugin1.apk");
PluginManager pluginManager = PluginManager.getInstance(this);
if (apk.exists()) {
try {
pluginManager.loadPlugin(apk);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.main_btn1:
if (PluginManager.getInstance(MainActivity.this).getLoadedPlugin("com.example.lhy.pluginproject1") == null) {
Toast.makeText(MainActivity.this, "插件未加载", Toast.LENGTH_SHORT).show();
} else {
Intent intent = new Intent();
intent.setClassName("com.example.lhy.pluginproject1", "com.example.lhy.pluginproject1.Plugin1MainActivity");
startActivity(intent);
}
break;
default:
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length >0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
File apk = new File(Environment.getExternalStorageDirectory(), "plugin1.apk");
PluginManager pluginManager = PluginManager.getInstance(this);
if (apk.exists()) {
try {
pluginManager.loadPlugin(apk);
} catch (Exception e) {
e.printStackTrace();
}
} else {
Toast.makeText(MainActivity.this, "插件不存在", Toast.LENGTH_SHORT).show();
}
}
break;
default:
break;
}
}
}
2.插件工程:
根目录build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
app的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'com.didi.virtualapk.plugin'//注意这个是plugin结尾,宿主是以host结尾的
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.example.lhy.pluginproject1"
minSdkVersion 15
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
signingConfigs{
release{
storeFile file('../PluginProject1.jks')
storePassword "android"
keyAlias "pluginProdect1"
keyPassword "android"
}
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
virtualApk {
// 插件资源表中的packageId,需要确保不同插件有不同的packageId.
packageId = 0x06
// 宿主工程application模块的路径,插件的构建需要依赖这个路径,我这个宿主工程和插件工程在同一级目录下,所以下面这样写
targetHost = '../MainProject/app'
//默认为true,如果插件有引用宿主的类,那么这个选项可以使得插件和宿主保持混淆一致
applyHostMapping = true
}
宿主编译一遍之后再编译插件
也可以使用命令编译,gradlew clean assemblePlugin