插件化开发
一, 这篇博客我希望你认真一步步往下看写,那么就一定可以实现自己的插件化开发,学习到大型企业牛逼的插件化架构,从原理到项目,如果不能够实现那么联系我。希望带给帮助,我很荣幸的。(对于不知道插件化的朋友可以去百度了解)
项目分为两个模块:
1,从原理讲起写个Demon。
2,然后去学会使用大型企业的插件化框架并附上Demon,我看了好多人的博客,我希望我的博客能给哪些刚刚从事android编程的哥们一写简单易懂图文混搭一步步集成并作出效果。
模块一:,实现主app加载一个插件app的Activity效果如下:
如下图:我为了显示比较高大上,布局截图了支付宝里面的界面。看看淘票票和滴滴出行这两个按钮点击各自跳进不同的activity里面。从而实现了插件化。
一,阻碍:首先我们(支付宝)App打开之后,在我们的手机目录下有两个插件apk(淘票票)和(滴滴打车),如何从主App跳转到这个目录下的apk的Activity呢?
1,主项目app里面是没有apk的Activity和class文件,上下文,资源。
2, 我们知道acitivity之间跳转需要在清单文件AndroidManifest.xml中需要注册。
二,解决:根据一阻碍分析,我们来解决上面这两个问题。
1,对于加载apk资源和class:估计接触过DexClassLoader很多人都知道这个动态加载类,谷歌大大还是很人性化的。我们可以通过它来加载我们插件apk里面的资源。
2,我们需要一个中间的依赖库提供一套接口(这套接口必须具有主App里面activity的生命周期并让插件activity实现它),让主项目和插件Modle都依赖它,从而主App可以跳转到它(依赖库),它作为一个中间者Activity,在它的onCreate里面来初始化插件Activity,调用插件Activity的onCreate方法就实现了启动了。
三,开始撸代码:
一,新建项目,添加依赖库和两个插件(淘票票,滴滴出行)Modle
中间依赖库用来定义我们的一套标准假装为插件Activity:File->New ->New Modle
接下来就是淘票票和滴滴出行两个Modle了如下图:New ->New Modle:
最后看看项目目录:
二,让主Activity和两个插件Modle来依赖我们的中间标准依赖库:
三,依赖完成我们来写代码部分:
首先我们去主项目和Modle中写好布局:如下:
(1)新建一个PluginManager.java类用来加载我们的淘票票和滴滴出行插件apk,并获取插件apk的基本信息和资源等把它放在我们的主项目或者依赖项目中都行。代码如下:
package com.example.ls.pluginstudydemon;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
/**
* 作者:王飞
* 邮箱:1276998208@qq.com
* create on 2018/6/14 16:23
*/
public class PluginManager {
//用来加载我们的插件apk类
private DexClassLoader dexclassLoader;
//加载插件的资源文件图片呀,xml等
private Resources resources;
private Context context;
private static PluginManager ourInstance = new PluginManager();
//获取插件安装包信息如Activity的类明等
private PackageInfo packageInfo;
public PackageInfo getPackageInfo() {
return packageInfo;
}
public static PluginManager getInstance() {
return ourInstance;
}
public void setContext(Context context){
this.context=context;
}
public DexClassLoader getClassLoader() {
return dexclassLoader;
}
public Resources getResources() {
return resources;
}
private PluginManager() {
}
//路径
public void loadPath(String path) {
File dexoutFile = context.getDir("dex", Context.MODE_PRIVATE);
//能加载外置卡的能力了
dexclassLoader = new DexClassLoader(path, dexoutFile.getAbsolutePath(), null, context.getClassLoader());
PackageManager packageManager=context.getPackageManager();
packageInfo=packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
//需要AssetManager
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager,path);
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
(2)我们需要定义一个标准接口让插件apk的Activity实现它,并且最后在伪装的依赖项目空壳Activity中将插件Activity强制转换为定义的接口并调用插件activity的onCreate实现启动接口代码如下:
package com.example.pluginstand;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
/**
* 作者:王飞
* 邮箱:1276998208@qq.com
* create on 2018/6/14 15:43
*/
public interface PluginInterface {
//注入上下文
public void attach(Activity proxyActivity);
public void onCreate(Bundle savedInstanceState);
public void onStart() ;
public void onResume();
public void onPause() ;
public void onStop() ;
public void onDestroy();
public void onSaveInstanceState(Bundle outState);
public boolean onTouchEvent(MotionEvent event) ;
public void onBackPressed();
}
(3) 我们在依赖项库中新建空壳ProxyActivity用来作为伪装的插件Activity并在onCreate中开启插件Activity的onCreate实现启动:
package com.example.pluginstand;
import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.example.pluginstand.PluginInterface;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 作者:王飞
* 邮箱:1276998208@qq.com
* create on 2018/6/14 16:16
* <p>
* 空壳 这里加载的资源和类都是插件里面的而且生命周期都是 插件生命周期都是插件的,所以实质启动了插件的activity
* <p>
* 耳机
*/
public class ProxyActivity extends Activity {
/*
* 需要加载的插件里的class
* */
private String className;
PluginInterface pluginInterface;
private String keyString;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
className = getIntent().getStringExtra("className");
keyString=getIntent().getStringExtra("keys");
try {
Class activityClass = getClassLoader().loadClass(className);
Constructor constructor = activityClass.getConstructor(new Class[]{});
//调用私有的Activity构造函数进行实例化对象。
Object instance = constructor.newInstance(new Object[]{});
//这里强行转换Activity为定义的接口,因为我们已经在插件apk里面实现了这个接口。
pluginInterface = (PluginInterface) instance;
pluginInterface.attach(this);
//传入一些信息。这里如果需要住Activity传入插件activity数据可以通过Bundle传递哦
Bundle bundle = new Bundle();
bundle.putString("keys",keyString);
//这里调用开启插件Activity
pluginInterface.onCreate(bundle);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
protected void onStart() {
//实质启动了插件activity的方法
pluginInterface.onStart();
super.onStart();
}
@Override
protected void onPause() {
//实质启动了插件activity的方法
pluginInterface.onPause();
super.onPause();
}
@Override
protected void onDestroy() {
//实质启动了插件activity的方法
pluginInterface.onDestroy();
super.onDestroy();
}
/*
* 加载Activity
*
* class 滴滴出行和
*
* 资源 淘票票
* */
//加载小黄车的资源和类
@Override
public Resources getResources() {
//让这个空壳Activity中加载了插件apk的资源
return PluginManager.getInstance().getResources();
}
//加载弟弟和淘票票的资源和类
@Override
public ClassLoader getClassLoader() {
//让这个空壳Activity中加载了插件apk的类从而实现开启的是真正意义上的插件ac。
return PluginManager.getInstance().getClassLoader();
}
}
(4) 我们需要在插件Modle的Activity中去实现接口,很重要的是通过 public void attach(Activity proxyActivity);注入上下文,并使用主项目中的acitivy的方法,才能和我们项目Activity进行交互,所以为了方便,我们建立BaseActivity替换所有插件中的Activity所需方法:
package com.example.plugina;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import com.example.pluginstand.PluginInterface;
/**
* 作者:王飞
* 邮箱:1276998208@qq.com
* create on 2018/6/14 15:51
*/
public class BaseActivity extends Activity implements PluginInterface{
protected Activity that;
@SuppressLint("NewApi")
@Nullable
@Override
public Intent getParentActivityIntent() {
return that.getParentActivityIntent();
}
@Override
public void attach(Activity proxyActivity) {
that = proxyActivity;
}
@Override
public Context getBaseContext() {
return that.getBaseContext();
}
@Override
public Context getApplicationContext() {
return that.getApplicationContext();
}
@Override
public void onCreate(Bundle savedInstanceState) {
}
@Override
public void setContentView(View view) {
that.setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
that.setContentView(view, params);
}
@Override
public void setContentView(int layoutResID) {
that.setContentView(layoutResID);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
that.addContentView(view, params);
}
@Override
public <T extends View> T findViewById(int id) {
return that.findViewById(id);
}
@Override
public Intent getIntent() {
return that.getIntent();
}
@Override
public ClassLoader getClassLoader() {
return that.getClassLoader();
}
@Override
public Resources getResources() {
return that.getResources();
}
@NonNull
@Override
public LayoutInflater getLayoutInflater() {
return that.getLayoutInflater();
}
@NonNull
@Override
public MenuInflater getMenuInflater() {
return that.getMenuInflater();
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return that.getSharedPreferences(name, mode);
}
@Override
public ApplicationInfo getApplicationInfo() {
return that.getApplicationInfo();
}
@Override
public WindowManager getWindowManager() {
return that.getWindowManager();
}
@Override
public Window getWindow() {
return that.getWindow();
}
@Override
public Object getSystemService(@NonNull String name) {
return that.getSystemService(name);
}
@Override
public void finish() {
that.finish();
}
@Override
public void onBackPressed() {
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return that.onCreateOptionsMenu(menu);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
}
@Override
public void onStart() {
}
@Override
public void onResume() {
}
@Override
public void onPause() {
}
@Override
public void onStop() {
}
@Override
public void onDestroy() {
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
}
@Override
protected void onRestart() {
}
@Override
public void onSaveInstanceState(Bundle outState) {
}
}
(5) 最后我们去主Activity中进行加载ap并调用(这里很重要的是手机6.0之后权限问题,如果手机是6.0之前的可以在清单文件中注册就行,否则动态申请权限,我一个手机是root权限的不需要动态也行)
package com.example.ls.pluginstudydemon;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import com.example.pluginstand.PluginManager;
import com.example.pluginstand.ProxyActivity;
import java.io.File;
public class MainActivity extends AppCompatActivity {
//6.0之后动态必须权限,这里估计很多人都怕坑了。很多博客也没写明白导致很多人最后运行失败。
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//这个注入上下文别忘记了
PluginManager.getInstance().setContext(this);
//权限动态申请。对于8.0之后没测过哦
verifyStoragePermissions(this);
}
public static void verifyStoragePermissions(Activity activity) {
try {
//检测是否有写的权限
int permission = ActivityCompat.checkSelfPermission(activity,
"android.permission.WRITE_EXTERNAL_STORAGE");
if (permission != PackageManager.PERMISSION_GRANTED) {
// 没有写的权限,去申请写的权限,会弹出对话框
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void jumpTaopiaoPiao(View view) {
File file = new File(Environment.getExternalStorageDirectory(), "taopiaopiao-debug.apk");
PluginManager.getInstance().loadPath(file.getAbsolutePath());
//跳转到空壳activity启动插件apk
Intent intent = new Intent(this,ProxyActivity.class);
intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);
intent.putExtra("keys","我是主Activity里面的数据哦!");
startActivity(intent);
}
public void jumpDidi(View view) {
File file = new File(Environment.getExternalStorageDirectory(), "didichuxing-debug.apk");
PluginManager.getInstance().loadPath(file.getAbsolutePath());
//跳转apk
Intent intent = new Intent(this,ProxyActivity.class);
intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);
startActivity(intent);
}
}
(6) 我们去主项目的清单文件中注册ProxActivity并且加读写权限,然后运行两个插件apk
运行didichuxing我们会在目录下找到apk文件,这里会奔溃因为上下文原因不用管,然后粘贴复制这两个apk到我们的手机目录下:
接下来删除手机桌面的插件app然后运行主项目:如下所示:
希望帮助到你,下一篇我将直接使用360插件化架构,隔绝插件和项目之间的任何联系,你只需要写你的插件化Demon和项目,四大组件随便调用,我感觉目前来说使用起来最爽的哦。如果帮助到你请你点赞分享,或者不明白的地方可以加我qq1276998208我会对你负责的0o0。
我的博客地址:https://blog.csdn.net/m0_37667770/article/details/80716073#comments
项目地址:https://github.com/luhenchang/PluginStudyDemon.git