Android-插件化-插桩式

简介

插件化:属于动态加载技术(插件化、热修复),三种方式实现:1、插桩式;2、hook技术;3、反射(基本被淘汰了);
动态加载技术:在应用程序运行时,动态记载一些程序中原本不存在的可执行文件并运行这些文件里面的代码逻辑。可执行文件总的来说分为两种,一种是动态链接库so,另一种是dex相关文件(dex文件包含jar/apk文件)。

作用

插件化作用:主要用于解决应用越来越庞大的以及功能模块的解耦,应用间的接入,解决65536的内存限制,所以小项目中一般用的不多。
热修复的作用:主要用来修复bug。

插桩式(入侵性太强)

请看下图,我们来进行图文讲解思路

android 动态插桩 安卓插桩_android 动态插桩


启动插件apk里面的Activity

  1. 我们先创建几个公共模块,这个模块主要用于提供公共接口;主要用来做宿主回调插件apk的各种生命周期,以及传入宿主的上下文;
  2. android 动态插桩 安卓插桩_移动开发_02


  3. android 动态插桩 安卓插桩_移动开发_03


  4. android 动态插桩 安卓插桩_android_04

  5. 我们宿主app和插件apk都需要去依赖这个公共模块
    implementation project(path: ‘:pluginstand’) //宿主和插件apk中都需要去做依赖
  6. 在插件apk中,创建一个BaseActivity基类,去实现公共接口
package com.lk.taopiaopiao;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;

import com.lk.pluginstand.PayInterfaceActivity;

/**
* Activity的基类
*/
public class BaseActivity extends Activity implements PayInterfaceActivity {
   //获取传递宿主App传递进来的上下文
   protected Activity activity;

   @Override
   public void attach(Activity proxyActivity) {
       //宿主传递进来上下文
       this.activity =proxyActivity;
   }

   @Override
   public void setContentView(View view) {
       if(activity!=null) {    //如果是宿主调用
           activity.setContentView(view);
           return;
       }
       //不是宿主调用
       super.setContentView(view);
   }

   @Override
   public View findViewById(int id) {
       if(activity!=null) {    //如果是宿主调用
           return activity.findViewById(id);
       }
       //不是宿主调用
       return super.findViewById(id);
   }
   //重写启动Activity
   @Override
   public void startActivity(Intent intent) {
       Intent m  = new Intent();
       m.putExtra("className",intent.getComponent().getClassName());
       if(activity!=null) {    //如果是宿主调用
           activity.startActivity(m);
           return;
       }
           super.startActivity(intent);

   }
   //重写启动Service
   @Override
   public ComponentName startService(Intent service) {
       Intent m = new Intent();
       m.putExtra("serviceName", service.getComponent().getClassName());
       return activity.startService(m);
   }
   //重写广播注册
   @Override
   public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
       return activity.registerReceiver(receiver, filter);
   }
   //重写广播解绑
   @Override
   public void unregisterReceiver(BroadcastReceiver receiver) {
       activity.unregisterReceiver(receiver);
   }

   //重写广播发送
   @Override
   public void sendBroadcast(Intent intent) {
       activity.sendBroadcast(intent);
   }
   @Override
   public void setContentView(int layoutResID) {
       if(activity!=null) {    //如果是宿主调用
           activity.setContentView(layoutResID);
           return;
       }
       //不是宿主调用
       super.setContentView(layoutResID);
   }

   /**
    * 下面这些生命周期  其实是PayInterfaceActivity公共接口的实现
    * @param saveInstanceState
    */
   @Override
   public void onCreate(Bundle saveInstanceState) {

   }

   @Override
   public void onStart() {

   }

   @Override
   public void onResume() {

   }

   @Override
   public void onPause() {

   }

   @Override
   public void onStop() {

   }

   @Override
   public void onDestroy() {

   }

   @Override
   public void onSaveInstanceState(Bundle outState) {

   }
}

注:
1、这个activity的 基类,主要用来重写各种方法,因为插件apk没有安装,activity中没有生命周期也就是没有上下文context,所以我们需要通过公共接口定义的attach方法,把宿主的上下文传进来~~
2、需要用到上下文的方法,我们都进行重写,使用宿主的上下文来进行调用

  1. 插件apk中需要使用的activity都集成这个BaseActivity基类,方便使用需要的上下文。
package com.lk.taopiaopiao;

import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;


public class TaoMainActivity extends BaseActivity {

	MyReceiver myReceiver= new MyReceiver();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //findViewById调用的是父类重新的findViewById方法
        findViewById(R.id.img).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(activity,"插件",Toast.LENGTH_SHORT).show();
                //调用的是父类的startActivity方法
                //插件中跳转的activity 神奇的可以不需要注册进清单文件
                startActivity(new Intent(activity, SceondActivity.class));
                //插件中启动服务
                 startService(new Intent(activity, TestService.class));
                //插件中动态注册广播
                IntentFilter intentFilter  = new IntentFilter();
                intentFilter.addAction("com.lk.taopiaopiao.TaoMainActivity");
                registerReceiver(myReceiver , intentFilter);
            }
        });
        //onClick事件只能在代码中设置,因为没有上下文
        findViewById(R.id.sendBroad).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //插件中发送广播
                Intent intent = new Intent();
                intent.setAction("com.lk.taopiaopiao.TaoMainActivity");
                sendBroadcast(intent);
            }
        });
    }
    @Override
    public void onDestroy(){
		super.onDestroy();
		if(null!=myReceiver){						
			unregisterReceiver(myReceiver)
		}
	}

}

下面这个Activity,我们就当第二个界面,用于测试首页跳转进来

package com.lk.taopiaopiao;

import android.os.Bundle;

public class SceondActivity extends BaseActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_second);
    }
}
  1. 如开始的图上所述,我们还需要一个代理(占坑)的Activity,这个占坑的Activity,主要用来回调插件apk中的各种生命周期,以及替换类加载器、Resource还有最重要的一点就是跳转;
    特别要记住的就是,这个代理Activity的启动模式需要是默认的,他可能跳转的界面比较多的话,需要多个实例最好;
    还有这个Activity跳转就比较有意思,自己跳转自己…
package com.lk.plug;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.Bundle;

import com.lk.pluginstand.PayInterfaceActivity;

import java.lang.reflect.Constructor;

import androidx.annotation.Nullable;

/**
* 占坑的代理Activity  只能用标准的启动模式,不能使用其他的启动模式,这样才会有多个实例
*/
public class ProxyActivity extends Activity {
   //需要反射加载插件的全类名
   private String className;
   //代理对象
   PayInterfaceActivity payInterfaceActivity;
   ProxyBroadCast proxyBroadCast;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       //需要在跳转到界面的时候把全类名传递过来
       className = getIntent().getStringExtra("className");

       try {
           //反射这个类
           Class<?> aClass = getClassLoader().loadClass(className);
           //拿到这个类的构造方法
           Constructor constructor = aClass.getConstructor(new Class[]{});
           //创建这个对象(插件中需要启动的Activity) 括号里面是参数
           Object in = constructor.newInstance(new Object[]{});
           //因为这个对象实现了这个公共接口,所以我们强转成下
           //这里有人可能会问为什么不直接强转成Activity?
           //因为Activity很多生命周期是掉不了的,而且那样的话我们的上下文就传递不进去了
           payInterfaceActivity = (PayInterfaceActivity)in;
           //将上下文传递进去到这创建的对象中
           payInterfaceActivity.attach(ProxyActivity.this);
           //调用生命周期
           //如果要给这个对象传递参数,我们可以这种Bundle的方式传递进去,插件中onCreate方法中取出
//            Bundle bundle = new Bundle();
//            bundle.putString("ceshi","1");
//            payInterfaceActivity.onCreate(bundle);
           payInterfaceActivity.onCreate(savedInstanceState);
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

   /**
    * 重写加载类
    * 用来改写加载类的路径
    * @return
    */
   @Override
   public ClassLoader getClassLoader() {
       //这样就是得到插件中的类
       return PluginManager.getInstance().getDexClassLoader();
   }

   /**
    * 重写加载资源
    * 用来更改资源加载
    * @return
    */
   @Override
   public Resources getResources() {
       //这样就是得到插件中的Resource
       return PluginManager.getInstance().getResources();
   }

   /**
    * 因为插件apk中  没有上下文,所以需要去重新这个跳转的方法,
    * 这样在未加载的插件apk中才能进行跳转
    * @param intent
    */
   @Override
   public void startActivity(Intent intent) {
       String classMame = intent.getStringExtra("className");
       Intent intent1 = new Intent(this,ProxyActivity.class);
       intent1.putExtra("className",classMame);
       super.startActivity(intent1);
   }
   //重写启动服务,传递进去全类名
   @Override
   public ComponentName startService(Intent service) {
       String serviceName = service.getStringExtra("serviceName");
       Intent intent = new Intent(this, ProxyService.class);
       intent.putExtra("serviceName", serviceName);
       return super.startService(intent);
   }
   
   //重写启动广播,传递进去全类名
   @Override
   public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
       proxyBroadCast = new ProxyBroadCast(receiver.getClass().getName(),
               this)
       return super.registerReceiver(proxyBroadCast , filter);
   }

   //重写广播解绑
   @Override
   public void unregisterReceiver(BroadcastReceiver receiver) {
   		if(null!=proxyBroadCast ){
       		super.unregisterReceiver(proxyBroadCast );
       	}
   }

   @Override
   protected void onStart() {
       super.onStart();
       //调用插件对象的接口,对象中自己定义实现
       payInterfaceActivity.onStart();
   }

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


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

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

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

占坑的广播代理

package com.lk.plug;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;


import com.lk.pluginstand.PayInterfaceBroadcast;

import java.lang.reflect.Constructor;

public class ProxyBroadCast extends BroadcastReceiver {
    //需要加载插件的全类名
    private PayInterfaceBroadcast payInterfaceBroadcast;

    public ProxyBroadCast(String className, Context context) {

        try {
            Class<?> aClass = PluginManager.getInstance().getDexClassLoader().loadClass(className);
            Constructor constructor = aClass.getConstructor(new Class[]{});
            Object in = constructor.newInstance(new Object[]{});
            payInterfaceBroadcast = (PayInterfaceBroadcast) in;
            payInterfaceBroadcast.attach(context);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        payInterfaceBroadcast.onReceive(context, intent);
    }
}

占坑的service

package com.lk.plug;

import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;


import com.lk.pluginstand.PayInterfaceService;

import java.lang.reflect.Constructor;

public class ProxyService extends Service {
    private String serviceName;
    private PayInterfaceService payInterfaceService;
    @Override
    public IBinder onBind(Intent intent) {
        init(intent);
        return null;
    }

    /**
     * 初始化加载插件的Service
     * @param intent
     */
    private void init(Intent intent) {
        //获取传递进来的Service的全类名
        serviceName = intent.getStringExtra("serviceName");

        //加载service 类
        try {
            //插件TestService
            Class<?> aClass = getClassLoader().loadClass(serviceName);
            Constructor constructor = aClass.getConstructor(new Class[]{});
            Object in = constructor.newInstance(new Object[]{});
            //强转成接口
            payInterfaceService = (PayInterfaceService) in;
            //传递进去上下文
            payInterfaceService.attach(this);

            Bundle bundle = new Bundle();
            bundle.putInt("from", 1);
            payInterfaceService.onCreate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(payInterfaceService == null){
            init(intent);
        }
        return payInterfaceService.onStartCommand(intent, flags, startId);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        payInterfaceService.onUnbind(intent);
        return super.onUnbind(intent);
    }
}
  1. 加载插件
    这里我们跳过了下载apk的过程,你会发现很多插件都是在第一次点击的时候,进行进度条的样式在进行加载和加载,这样的话就可以是用户需要使用哪个就进行哪一个的下载,为用户节省内存,我们将插件apk移动到私有目录
package com.lk.plug;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    //插件加载按钮的点击事件
    public void load(View view) {
        //在这个加载插件之前,我们可以做下载插件apk并保存到自己想保存的目录下,保存成功后在进行插件加载
        loadPlugin();
    }

    /**
     * 加载插件
     */
    private void loadPlugin() {
        //getDir目录  是data/data/项目包名/app_plugin      这个app_会自动生成
        File filesDir = this.getDir("plugin", Context.MODE_PRIVATE);
        String name = "plugin.apk";
        String filePath = new File(filesDir, name).getAbsolutePath();
        File file = new File(filePath);
        //先看私有目录下是否有这个插件apk  如果有了 我们就删除(这个是否删除我们根据实际情况来做)
        if (file.exists()) {
            file.delete();
        }
        //以下就是讲sd卡里面的这个插件apk拷贝到私有目录下
        InputStream is = null;
        FileOutputStream os = null;
        try {
            Log.i("MainActivity", "加载插件 " + new File(Environment.getExternalStorageDirectory(), name).getAbsolutePath());
            is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
            os = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            File f = new File(filePath);
            if (f.exists()) {
                Toast.makeText(this, "插件加载成功", Toast.LENGTH_SHORT).show();
            }
            //调用加载
            PluginManager.getInstance().loadPath(this,name);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                os.close();
                is.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }

    public void click(View view) {
        Intent intent = new Intent(this,ProxyActivity.class);
        //传入过去插件中需要启动的activity的全类名
        //PackageInfo.activitys  这个素组里面存放的activity顺序是Manifast.xml中的顺序来的
        intent.putExtra("className",PluginManager.getInstance().getPackageInfo().activities[0].name);
        startActivity(intent);
    }
}
  1. 最关键的插件工具类来了;
    这个工具类里面,我们主要用到反射去获取插件apk的包名信息,dex加载器、Resource对象,不多说,上代码
package com.lk.plug;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
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.Field;
import java.lang.reflect.Method;
import java.util.List;

import dalvik.system.DexClassLoader;

/**
 * 插件处理工具类
 */
public class PluginManager {
    private static final PluginManager ourInstance = new PluginManager();

    private Resources resources;

    private DexClassLoader dexClassLoader;

    private PackageInfo packageInfo;

    public static PluginManager getInstance() {
        return ourInstance;
    }

    private PluginManager() {
    }

    /**
     * 加载插件apk
     * @param context
     * @param name  文件名
     */
    public void loadPath(Context context,String name){
        //拿到这个文件(pak)
        File filesDir = context.getDir("plugin", Context.MODE_PRIVATE);
        //得到这个文件的全路径
        String path = new File(filesDir,name).getAbsolutePath();
        //通过包名管理器去做加载  所以这里我们得到这个包名管理器
        PackageManager packageManager = context.getPackageManager();
        //得到插件的包名信息
        packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);

        //用来获取插件中的Activity
        File dexPath = context.getDir("dex", Context.MODE_PRIVATE);
        //第一个参数是文件路径;第二个参数是缓存文件的存放路径;
        // 第三个参数是library的路径;第四个参数是类加载器,通过上下文就可以拿到
        dexClassLoader = new DexClassLoader(path, dexPath.getAbsolutePath(), null, context.getClassLoader());

        //获取插件中的resource资源
        try {
            //得到资源管理器
            AssetManager assetManager = AssetManager.class.newInstance();
            //将插件里面的资源添加进去(把插件的apk传入到这个资源管理器中)
            //反射获取资源管理器的addAssetPath方法
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            //执行该方法,第一个参数是这个方法的对象,第二个参数是该方法的参数
            addAssetPath.invoke(assetManager,path);
            //获取插件中的Resources对象
            resources = new Resources(assetManager,
                    context.getResources().getDisplayMetrics(),
                    context.getResources().getConfiguration());

        } catch (Exception e) {
            e.printStackTrace();
        }

        //解析插件静态广播
        paserReceivers(context,path);
    }

    /**
     * 解析插件静态广播
     * @param context
     * @param path
     */
    private void paserReceivers(Context context, String path) {
        try {
            //反射获取PackageParser对象
            Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
            //创建PackageParser对象
            Object packageParser = packageParserClass.newInstance();
            //获取PackageParser中的parsePackage()函数
            Method parsePackageMethod =
                    packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
            //调用parsePackage() 返回Package对象
            Object packageObj = parsePackageMethod.invoke(packageParser, new File(path), PackageManager.GET_ACTIVITIES);
           //可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。 (注: 这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true))
            //通过Package 来获取这个对象中的成员变量(receivers)等价于receivers 的集合
            Field receiversField = packageObj.getClass().getDeclaredField("receivers");
            //通过Package对象得到receivers 的集合
            List receivers = (List) receiversField.get(packageObj);

            //获取Component 为的是获取IntentFilter集合
            Class<?> componentClass = Class.forName("android.content.pm.PackageParser$Component");
            Field intentsField = componentClass.getDeclaredField("intents");

            // 调用generateActivityInfo 方法, 把PackageParser.Activity 转换成
            Class<?> packageParser$ActivityClass = Class.forName("android.content.pm.PackageParser$Activity");
//            generateActivityInfo方法
            Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
            Object defaltUserState = packageUserStateClass.newInstance();
            Method generateReceiverInfo = packageParserClass.getDeclaredMethod("generateActivityInfo",
                    packageParser$ActivityClass, int.class, packageUserStateClass, int.class);

            //反射获取UserID
            Class<?> userHandler = Class.forName("android.os.UserHandle");
            Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
            int userId = (int) getCallingUserIdMethod.invoke(null);

            //等于循环清单文件中的广播节点
            for (Object activity : receivers) {
                ActivityInfo info = (ActivityInfo) generateReceiverInfo.invoke(packageParser, activity, 0, defaltUserState, userId);
                List<? extends IntentFilter> intentFilters =
                        (List<? extends IntentFilter>) intentsField.get(activity);
                //订单文件中的获取到广播
                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) dexClassLoader.loadClass(info.name).newInstance();
                for (IntentFilter intentFilter : intentFilters) {
                    //注册广播
                    context.registerReceiver(broadcastReceiver, intentFilter);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Resources getResources(){
        return resources;
    }

    public DexClassLoader getDexClassLoader(){
        return dexClassLoader;
    }

    public PackageInfo getPackageInfo(){
        return packageInfo;
    }
}

接下来我们来看看宿主app和插件apk里面的Manifast.xml清单文件

//宿主app的清单文件   ProxyActivity、ProxyService用来占坑
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.lk.plug">
    <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">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".ProxyActivity" />
        <service android:name=".ProxyService" />
    </application>

</manifest>
//插件apk的清单文件,注意!  我们只注册了一个Activity,跳转的Activity我们没有进行注册
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.lk.taopiaopiao">

    <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=".TaoMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

下面我们来看看效果

我们来点击宿主的加载插件按钮,插件加载成功了

android 动态插桩 安卓插桩_移动开发_05


我们接下来点击跳转,跳到插件apk当中去

android 动态插桩 安卓插桩_android 动态插桩_06


然后我们插件apk中的首页点击跳转到插件apk中的另一个页面去,这个界面是没有注册到清单文件中的哦,注意咯

android 动态插桩 安卓插桩_android 动态插桩_07


ClassLoad家族,继承关系图

android 动态插桩 安卓插桩_移动开发_08

DexClassLoader加载机制原理

android 动态插桩 安卓插桩_android 动态插桩_09


以上就是我们Activity的插桩,Service、Activity以及广播非常类似,思路基本是一样的,这只是简单般的插件化思路分析