前言
本文带领大家自己实现自己的简易插件化
本文采用插桩的方式实现。
用户通过下载插件的方式,实现打开这个插件的activity,实现按需下载,按需下载使用插件,这里因为时间关系,游戏下载的步骤:手动上传到sd卡根目录假装用户下载了这个apk。
一、Android插件化是什么?
提到插件化,就不得不提起方法数超过65535的问题,我们可以通过Dex分包来解决,同时也可以通过使用插件化开发来解决。插件化的概念就是由宿主APP去加载以及运行插件APP。
1. 下面是一些插件化的优势:
- 在一个大的项目里面,为了明确的分工,往往不同的团队负责不同的插件APP,这样分工更加明确
- 各个模块封装成不同的插件APK,不同模块可以单独编译,提高了开发效率。
- 解决了上述的方法数超过限制的问题。
- 可以通过上线新的插件来解决线上的BUG,达到“热修复”的效果。
- 减小了宿主APK的体积。
2. 下面是插件化开发的缺点:
- 插件化开发的APP不能在Google Play上线,也就是没有海外市场。
二、使用步骤
这里我们暂时就做一个demo,假设有这样一个需求,需要从主activity跳转到一个loginActivity,然后在点击loginActivity里的按钮,实现在插件内跳转。
1.创建一个新项目
这一步想必大家都会做,就是使用android studio 创建一个新项目就可以了。
2.创建Moudle(login)
既然是插件话,我们就要把业务登录模块挪出来,所以建立一个Login Module,这也是正常的业务逻辑通过下面的方法,名字叫login.
然后我们在创建一个library,这个library就是我们实现插装的核心库,创建方式跟上面的方式一样,选择那个AndroidLibrary就像,名字取名为plugin_core,这样的你工程就如下面的图片一样。
接下来我们就看一下这些里面都放了啥
app模块里面其实就是一个MainActivity,代码如下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//跳转到login插件的方法
public void jumpActivity(View view) {
//这个PluginManager是我们app依赖的pulgin_core库里的一个方法,将在下面给出
PluginManager.init(getApplicationContext());
//这里的login.apk,是我们把login插件打包生成后的apk文件,然后上传到手机的sd卡根目录
//下假装用户下载了这个插件)
PluginManager.getInstance().loadDex(Environment.getExternalStorageDirectory()+"/login.apk");
PackageInfo packageInfo = PluginManager.getInstance().getPackageInfo();
ActivityInfo[] activities = packageInfo.activities;
if(activities == null || activities.length == 0){
return;
}
//其实这里的name是PluginActivity的全路径(包名+类名)
String name = activities[0].name;
Intent intent = new Intent(this, PluginActivity.class);
intent.putExtra("className",name);
startActivity(intent);
}
app下的manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.suyong.tinker">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!--这个是插件activity,需要在这里声明,因为所有下面的插件将依靠这个类实现启动-->
<activity android:name="com.suyong.plugin_core.PluginActivity" />
</application>
</manifest>
再看一下我们的login模块了有啥?有两个activity 代码如下,这里为了方便展示,就写在一起了,实际上这是两个独立的activity,loginActivity里面的按钮可以启动twoActivity.
/**
BaseActivity 是我plugin_core的类
我规定所有的插件Activity需要继承这个BaseActivity,
否则不能启动这个插件。
**/
//LoginActivity.class
import com.suyong.plugin_core.BaseActivity;
public class LoginActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
findViewById(R.id.jump).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(that,twoActivity.class));
}
});
}
}
//twoActivity.class
public class twoActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_two);
}
}
login下的manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.suyong.login">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".twoActivity"></activity>
</application>
</manifest>
3.plugin_core插件核心库
下面贴一下这个library里所有类,注意看注释。
- BaseActivity
package com.suyong.plugin_core;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
//所有的插件类需要继承这个BaseActivity,否则无法启动
public class BaseActivity extends AppCompatActivity implements PluginInterface{
//这个that是最重要的上下文,是PluginActivity的实列,这样我们就可以意识到
//我们所有的系统级的api都是通过调用that里的方法实现的,因为我们所有的插件
//别看都是继承与BaseActivity,但BaseActivity的方法都是调用的that,所以所有
//的插件activity都是共享PluginActivity上下文。
protected Activity that;
@Override
public void attach(Activity activity) {
that = activity;
}
@Override
public void setContentView(int layoutResID) {
that.setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
that.setContentView(view);
}
@Override
public <T extends View> T findViewById(int id) {
return that.findViewById(id);
}
@Override
public Intent getIntent() {
return that.getIntent();
}
@Override
public Resources getResources() {
return that.getResources();
}
@Override
public WindowManager getWindowManager() {
return that.getWindowManager();
}
@Override
public Window getWindow() {
return that.getWindow();
}
@Override
public void startActivity(Intent intent) {
Intent i = new Intent();
i.putExtra("className",intent.getComponent().getClassName());
that.startActivity(i);
}
@NonNull
@Override
public LayoutInflater getLayoutInflater() {
return that.getLayoutInflater();
}
@Override
public ClassLoader getClassLoader() {
return that.getClassLoader();
}
@SuppressLint("MissingSuperCall")
@Override
public void onCreate(Bundle savedInstance) {
}
@SuppressLint("MissingSuperCall")
@Override
public void onStart() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onResume() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onPause() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onStop() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onDestroy() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onSaveInstanceState(Bundle outState) {
}
//这个千万不要调用that.onBackPressed(),不然会出现应用死循环,一直调用onBackPressed()
@Override
public void onBackPressed(){
}
@Override
public boolean onTouchEvent(MotionEvent event){
return true;
}
@Override
public ApplicationInfo getApplicationInfo() {
return that.getApplicationInfo();
}
}
- PluginActivity
package com.suyong.plugin_core;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.MotionEvent;
import androidx.appcompat.app.AppCompatActivity;
//插件化路由框架
//所有的插件类启动都是调用这个方法,通过在intent
//添加哦 className 传递要启动activity的全路径。
public class PluginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstance){
super.onCreate(savedInstance);
String className = getIntent().getStringExtra("className");
try {
//通过全路径解析出class
Class<?> aClass = PluginManager.getInstance().getDexClassLoader().loadClass(className);
//实例化这个class
Object activity = aClass.newInstance();
if(activity instanceof BaseActivity){
//这里就判断了非继承自BaseActivity的插件将不能启动
PluginInterface instance = (PluginInterface) activity;
//把上下文传给BaseActivity,方便插件类调用上下文
instance.attach(PluginActivity.this);
//启动插件类的onCreate()方法。
instance.onCreate(savedInstance);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance().getDexClassLoader();
}
@Override
public Resources getResources() {
return PluginManager.getInstance().getResources();
}
@Override
public void startActivity(Intent intent) {
String className = intent.getStringExtra("className");
//注意这里哦,我们是启动了自己,className存放了我们要启动的类的全路径
Intent i = new Intent(PluginActivity.this,PluginActivity.class);
i.putExtra("className",className);
super.startActivity(i);
}
}
- PluginInterface
public interface PluginInterface {
void attach(Activity activity);
void onCreate(Bundle savedInstance);
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
void onBackPressed();
boolean onTouchEvent(MotionEvent event);
void onSaveInstanceState(Bundle outState);
}
- PluginManager
package com.suyong.plugin_core;
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.Method;
import dalvik.system.DexClassLoader;
/**
我们这个类的目的就是为了记载外部的Apk,为什么呢,因为插件类实际上只是以文件的新式存在,app启动时候系统并不会加载外部插件类,所以我们要手动通过这个类加载外部的apk.
**/
public class PluginManager {
private static Context mContext;
//加载外部的DexClassLoader
private DexClassLoader mDexClassLoader;
//资源管理
private Resources mResources;
//包信息管理类
private PackageInfo mPackageInfo;
//一定要先调用这个方法,不然会报错
public static void init(Context context) {
mContext = context;
}
private static PluginManager instance = new PluginManager();
private PluginManager() {
}
public static PluginManager getInstance() {
return instance;
}
//这个方法就是用来加载指定路径的外部Apk路径的
public void loadDex(String apkPath) {
//创建缓存路径
File file = mContext.getDir("plugin", Context.MODE_PRIVATE);
mDexClassLoader = new DexClassLoader(apkPath, file.getAbsolutePath(), null, mContext.getClassLoader());
mPackageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager,apkPath);
mResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
public DexClassLoader getDexClassLoader() {
return mDexClassLoader;
}
public Resources getResources() {
return mResources;
}
public PackageInfo getPackageInfo() {
return mPackageInfo;
}
}
实现效果:
可以看到成功启动,也可以返回,这与平常没什么两样,记住多看注释哦!
总结
这里只是用了插装的方法实现了简易的插件化,并不是特别好的方法,大家可以采用插件化框架,肯定比我这个兼容性 实用性要强许多,时间不早了,溜了溜了,欢迎留言。
PS:纯属个人理解,希望指正!