Android手写占位式插件化框架之Activity通信、Service通信和BroadcastReceiver通信

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂,风趣幽默",感觉非常有意思,忍不住分享一下给大家。

👉点击跳转到教程

前言:

1、什么是插件化?

能运行的宿主APP去加载没有下载的APK文件,并使用APK文件里面的功能,这就叫插件化。

2、插件化的使用场景?

很多大厂APP内会有很多功能模块,但是包体积却很小,那么就用到了插件化技术,点击某个模块后,从服务器获取对应的APK文件,并使用其内部的功能。

实现后的效果图如下:

接下来手写实现占位式插件化框架之Activity之间的通信

根据上图首先定义一个项目叫PluginProject,之后再新建一个Android Library库名为:stander,然后再定义一个插件包名为:plugin_package

项目目录如下:

一、首先在stander库中,定义一个接口名为ActivityInterface,ServiceInterface,ReceiverInterface三个接口

1.1、ActivityInterface接口

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/14
 * @Description: 定义的Activity标准接口,需要什么方法可以再加
 */
public interface ActivityInterface {
    /**
     * 把宿主(app)的环境给插件
     *
     * @param appActivity 宿主的Activity
     */
    void insertAppContext(Activity appActivity);

    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onPause();

    void onStop();

    void onDestroy();
}

1.2 ServiceInteface接口

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/15
 * @Description: 宿主与插件间进行Service通信,标准接口
 */
public interface ServiceInterface {
    /**
     * 把宿主(app)的环境给插件
     *
     * @param service 宿主的Service
     */
    void insertAppContext(Service service);

    void onCreate();

    int onStartCommand(Intent intent, int flags, int startId);

    void onDestroy();
}

1.3、ReceiverInterface接口

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/15
 * @Description: 宿主与插件间进行广播通信标准接口
 */
public interface ReceiverInterface {
    void onReceive(Context context, Intent intent);
}

二、在宿主APP中,定义插件管理类PluginManager

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/14
 * @Description: 插件管理类,获取插件中的资源Resources和类加载器DexClassLoader
 */
public class PluginManager {
    private static final String TAG = PluginManager.class.getSimpleName();
    private static PluginManager pluginManager;
    private Context context;
    //Activity class
    private DexClassLoader dexClassLoader;
    private Resources resources;

    private PluginManager(Context context) {
        this.context = context;
    }

    public static PluginManager getInstance(Context context) {
        if (pluginManager == null) {
            synchronized (PluginManager.class) {
                if (pluginManager == null) {
                    pluginManager = new PluginManager(context);
                }
                return pluginManager;
            }
        }
        return pluginManager;
    }

    /**
     * 加载插件(2.1 Activity class, 2.2 layout)
     */
    public void loadPlugin() {
        try {
            //getExternalFilesDir:表示应用程序的私有目录
            File privateDir = context.getExternalFilesDir(null);
            //路径: /storage/emulated/0/Android/data/com.example.pluginproject/files
            Log.i(TAG, "privateDir: " + privateDir.getAbsolutePath());
            File file = new File(privateDir.getAbsolutePath() + File.separator + "p.apk");
            if (!file.exists()) {
                Log.d(TAG, "插件包,不存在");
                return;
            }
            String pluginPath = file.getAbsolutePath();
            //下面是加载插件里面的class
            //dexClassLoader 需要一个缓存目录 /data/data/当前应用的包名/pDir
            File fileDir = context.getDir("pDir", Context.MODE_PRIVATE);
            //fileDir.getAbsolutePath(): /data/user/0/com.example.pluginproject/app_pDir
            Log.d(TAG, "fileDir: " + fileDir.getAbsolutePath());
            //pluginPath:插件文件的路径,表示插件APK文件的位置。
            //fileDir.getAbsolutePath():表示应用程序的私有目录路径,作为DexClassLoader的第二个参数传递,用于指定Dex文件的输出目录。
            //null:表示没有指定库(Native Library)的路径,如果插件中有依赖的库文件,可以传入库目录的路径。
            //context.getClassLoader():获取应用程序的类加载器作为DexClassLoader的父类加载器。
            dexClassLoader = new DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, context.getClassLoader());
            //下面是加载插件里面的layout文件
            //加载资源
            AssetManager assetManager = AssetManager.class.newInstance();
            //我们执行此方法,为了把插件包的路径添加进去
            // public int addAssetPath(String path)
            Method method = assetManager.getClass().getMethod("addAssetPath", String.class);//类类型Class
            method.invoke(assetManager, pluginPath);//插件包的路径,pluginPath
            Resources r = context.getResources();//宿主的资源配置信息
            //特殊的resource,加载插件里面的资源的resource
            this.resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());//参数二和参数三,配置信息
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public ClassLoader getClassLoader() {
        return dexClassLoader;
    }

    public Resources getResources() {
        return resources;
    }
}

2.1然后在MainActivity定义两个按钮,分别为加载插件,和启动插件里面的Activity

bash 复制代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
                || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    protected void onStop() {
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    /**
     * 加载插件
     *
     * @param view
     */
    public void loadPlugin(View view) {
        PluginManager.getInstance(this).loadPlugin();
    }

    /**
     * 启动插件里面的Activity
     *
     * @param view
     */
    public void startPluginActivity(View view) {
        File privateDir = getExternalFilesDir(null);
        File file = new File(privateDir.getAbsolutePath() + File.separator + "p.apk");
        String path = file.getAbsolutePath();
        File file1 = new File(path);
        if (!file1.exists() || file1.isFile()) {
            Log.i("TAG", "插件包路径无效");
        }
        Log.i("TAG", "path: " + path);
        //获取插件包里面的Activity
        PackageManager packageManager = getPackageManager();
        PackageInfo packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
        ActivityInfo activityInfo = packageInfo.activities[1];
        //占位 代理Activity
        Intent intent = new Intent(this, ProxyActivity.class);
//        intent.putExtra("className", "com.example.plugin_package.PluginActivity");
        intent.putExtra("className", activityInfo.name);
        startActivity(intent);
    }
}

2.2 写代理类ProxyActivity,用代理类的上下文环境,实现插件包页面正常加载

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/14
 * @Description: 代理的Activity,代理/占位 插件里面的Activity
 */
public class ProxyActivity extends Activity {
    private static final String TAG = "ProxyActivity";

    @Override
    public Resources getResources() {
        return PluginManager.getInstance(this).getResources();
    }

    @Override
    public ClassLoader getClassLoader() {
        return PluginManager.getInstance(this).getClassLoader();
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //真正的加载,插件里面的Activity
        String className = getIntent().getStringExtra("className");
        Log.i(TAG, "className: " + className);
        try {
            Class<?> pluginActivityClass = getClassLoader().loadClass(className);
            //实例化插件包里面的Activity
            Constructor<?> constructor = pluginActivityClass.getConstructor(new Class[]{});
            Object pluginActivity = constructor.newInstance(new Object[]{});
            ActivityInterface activityInterface = (ActivityInterface) pluginActivity;
            //注入
            activityInterface.insertAppContext(this);
            Bundle bundle = new Bundle();
            bundle.putString("appName", "我是宿主传递过来的信息");
            //执行插件里面的onCreate()方法
            activityInterface.onCreate(bundle);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void startActivity(Intent intent) {
        String className = intent.getStringExtra("className");
        Intent proxyIntent = new Intent(this, ProxyActivity.class);
        proxyIntent.putExtra("className", className);//包名TestActivity
        //要给TestActivity进栈
        super.startActivity(proxyIntent);
    }

    @Override
    public ComponentName startService(Intent service) {
        String className = service.getStringExtra("className");
        Intent intent = new Intent(this, ProxyService.class);
        intent.putExtra("className", className);//ProxyService全类名
        return super.startService(intent);
    }

    @Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter intentFilter) {
        //MyReceiver全类名
        String pluginReceiverName = receiver.getClass().getName();
        //在宿主app注册广播
        return super.registerReceiver(new ProxyReceiver(pluginReceiverName), intentFilter);
    }

    @Override
    public void sendBroadcast(Intent intent) {
        super.sendBroadcast(intent);//发送广播到ProxyReceiver
    }
}

2.3、ProxyService类

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/15
 * @Description: 代理Service类,代理/占位插件中的Service
 */
public class ProxyService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String className = intent.getStringExtra("className");
        //com.example.plugin_package.TestService
        try {
            Class<?> testServiceClass = PluginManager.getInstance(this).getClassLoader().loadClass(className);
            Object testService = testServiceClass.newInstance();
            ServiceInterface serviceInterface = (ServiceInterface) testService;
            serviceInterface.insertAppContext(this);
            serviceInterface.onStartCommand(intent, flags, startId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

ProxyService需要在AndroidManifest.xml中注册

bash 复制代码
 <service android:name=".ProxyService" />

2.4、ProxyReceiver类

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/15
 * @Description: 能接收的广播接收者, 代理/占位,插件里面的BroadcastReceiver
 */
public class ProxyReceiver extends BroadcastReceiver {
    /**
     * 插件里面的MyReceiver全类名
     */
    private String pluginReceiverName;

    public ProxyReceiver(String pluginReceiverName) {
        this.pluginReceiverName = pluginReceiverName;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        //加载插件里面的MyReceiver
        try {
            Class myReceiverClass = PluginManager.getInstance(context).getClassLoader().loadClass(pluginReceiverName);
            //实例化Class
            Object myReceiver = myReceiverClass.newInstance();
            ReceiverInterface receiverInterface = (ReceiverInterface) myReceiver;
            receiverInterface.onReceive(context, intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三、插件包plugin_package中,首先实现BaseActivity类

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/14
 * @Description: 插件包中Activity基础类, 拿到宿主的上下文环境
 */
public class BaseActivity extends Activity implements ActivityInterface {
    private static final String TAG = "BaseActivity";
    /**
     * 宿主的环境
     */
    public Activity appActivity;

    @Override
    public void insertAppContext(Activity appActivity) {
        this.appActivity = appActivity;
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        String appName = savedInstanceState.getString("appName");
        Log.i(TAG, "appName: " + appName);
    }

    @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() {

    }

    public void setContentView(int resId) {
        appActivity.setContentView(resId);
    }

    public View findViewById(int id) {
        return appActivity.findViewById(id);
    }

    /**
     * 启动插件包内的第二个Activity:TestActivity
     *
     * @param intent 意图数据
     */
    public void startActivity(Intent intent) {
        Intent newIntent = new Intent();
        newIntent.putExtra("className", intent.getComponent().getClassName());
        appActivity.startActivity(newIntent);
    }

    public ComponentName startService(Intent serviceIntent) {
        Intent newIntent = new Intent();
        //serviceIntent.getComponent().getClassName() 这里拿到的是TestService的全类名
        newIntent.putExtra("className", serviceIntent.getComponent().getClassName());
        return appActivity.startService(newIntent);
    }

    /**
     * 注册广播
     *
     * @param receiver
     * @param intentFilter
     * @return
     */
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter intentFilter) {
        return appActivity.registerReceiver(receiver, intentFilter);
    }

    /**
     * 发送广播
     *
     * @param intent
     */
    public void sendBroadcast(Intent intent) {
        appActivity.sendBroadcast(intent);
    }
}

3.2 插件包中首页PluginActivity,代码如下

bash 复制代码
/**
 * 首先加载该页面PluginActivity
 */
public class PluginActivity extends BaseActivity {
    private static final String TAG = "PluginActivity";
    private static final String ACTION = "com.example.plugin_package.ACTION";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String appName = savedInstanceState.getString("appName");
        Log.i(TAG, "appName: " + appName);
        setContentView(R.layout.activity_plugin);
        Toast.makeText(appActivity, "我是插件", Toast.LENGTH_SHORT).show();
        //点击按钮跳转到TestActivity
        findViewById(R.id.btn_start_activity).setOnClickListener(v -> {
            startActivity(new Intent(appActivity, TestActivity.class));
        });
        //点击按钮跳转到TestService
        findViewById(R.id.btn_start_service).setOnClickListener(v -> {
            startService(new Intent(appActivity, TestService.class));
        });
        //插件内部注册插件的广播接收者
        findViewById(R.id.btn_register_receiver).setOnClickListener(v -> {
            IntentFilter filter = new IntentFilter();
            filter.addAction(ACTION);
            registerReceiver(new MyReceiver(), filter);
        });
        //插件内部发送插件的广播接收者
        findViewById(R.id.btn_send_receiver).setOnClickListener(v -> {
            Intent intent = new Intent();
            intent.setAction(ACTION);
            sendBroadcast(intent);
        });
    }
}

3.3 点击PluginActivity中的按钮,可以跳转到TestActivity,代码如下:

bash 复制代码
public class TestActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
    }
}

3.4、BaseService类

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/15
 * @Description: 基础Service继承标准库中ServiceInterface接口
 */
public class BaseService extends Service implements ServiceInterface {
    private Service appService;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void insertAppContext(Service appService) {
        this.appService = appService;
    }

    @Override
    public void onCreate() {

    }

    @SuppressLint("WrongConstant")
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return 0;
    }

    @Override
    public void onDestroy() {

    }
}

3.5、TestService类

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/15
 * @Description: 插件中的Service
 */
public class TestService extends BaseService {
    private static final String TAG = "TestService";

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //开启子线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        Log.i(TAG, "插件里面的服务正在执行中!");
                    }
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

3.6、插件中的广播接收者MyReceiver

bash 复制代码
/**
 * @Author: ly
 * @Date: 2023/7/15
 * @Description: 插件中的广播接收者
 */
public class MyReceiver extends BroadcastReceiver implements ReceiverInterface {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "我是插件里面的广播接收者,我收到了广播!", Toast.LENGTH_SHORT).show();
    }
}

一、这个是宿主APP启动插件Activity流程:

二、这个是插件中启动Activity的流程如下图:

三、下面是插件中启动Service的流程图如下:

四、插件中启动BroadcastReceiver,这是在插件中动态注册广播接收者,并在插件内部发送广播。

五、宿主app中获取插件包中静态注册的广播接收者StaticeReceiver,并在宿主app中发送静态广播请看这篇文章

Android手写占位式插件化框架之apk解析原理系统源码分析

六、编写代码后,将plugin_package包手动放到宿主app的私有目录下,便可以正常运行,在公司项目中会将插件包放到服务器用户使用某个功能模块的时候,会下载到本地。

七、具体问题思考

1、为什么在插件中不能使用this?

因为插件是没有在手机上安装的,是无法拥有组件环境的。

2、为什么要有代理的Activity?

由于插件中的Activity并不是一个能够运行的组件,所以需要代理的Activity去代替插件中的Activity(例如Activity进出栈)

3、这种插件化,在写插件开发的时候,有什么要注意的事项?

所有关于操作组件环境的地方,都必须使用宿主的环境。

相关推荐
zhangphil7 分钟前
Android从Drawable资源Id直接生成Bitmap,Kotlin
android·kotlin
HenCoder14 分钟前
【泛型 Plus】Kotlin 的加强版类型推断:@BuilderInference
android·java·开发语言·kotlin
虾球xz1 小时前
游戏引擎学习第12天
android·学习·游戏引擎
nnloveswc1 小时前
PET-文件包含-FINISHED
android
咸芝麻鱼2 小时前
Android Studio | 修改镜像地址为阿里云镜像地址,启动App
android·阿里云·android studio
小爬虫程序猿2 小时前
当API遇上“交通堵塞”:处理API限制的艺术
android·爬虫·python
Dnelic-2 小时前
Android Studio Gradle 配置 gradle-wrapper.properties
android·ide·gradle·android studio·自学笔记
kim56592 小时前
android studio 轮询修改对象属性(修改多个textview的text)
android·ide·android studio·轮询
勤奋的凯尔森同学3 小时前
Ubuntu24.04上安装和配置MariaDB
android·数据库·mariadb
清风徐来辽3 小时前
Android 国际化多语言标点符号的适配
android·国际化多语言标点符号