插件开发可以提高一款软件的可扩展性,我个人认为他就是一种化整为零的思想。就跟电脑的主板上留了不同的接口一样,只要插上相应的硬件就可以实现具体的功能。

Android插件开发要具备以下的基本功

android 插件 IDsCreate Android 插件化开发实例_插件

sharedUserId就是主程序和插件程序进行通讯的唯一标识UID.

下面是我做的一个模拟更换皮肤的案例,首先创建一个主程序和两个插件程序如下图:

android 插件 IDsCreate Android 插件化开发实例_Test_02


然后找三张不同的图片分别放入三个应用对应的目录下,保证三张图片的命名和尺寸完全相同(我的在mipmap-xxhdpi目录下)。

接下来在三个应用的清单文件中配置相同的sharedUserId:

一般插件程序都是没有启动图标的,所以我们在manifest文件中做如下配置:
将LAUNCHER改为DEFAULT

<category android:name="android.intent.category.DEFAULT"/>

准备工作做完就开始上代码了,主要代码如下:

/**
 *@author jiangrongtao
 
 *github:https://github.com/jiangrongtao/jiangrongtao.github.io
 *
 * created at 2016/6/6 15:58
*/

public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener, View.OnClickListener {
    private static final String TAG = "MainActivity";
    private MainActivity mContext;
    private PopupWindow mPopupWindow;
    private ArrayList<Map<String, String>> mPluginList;
    private ImageView mImage;
    private Button mGetPlugin;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        mContext=this;
        mImage= (ImageView) findViewById(R.id.iv_image);
        mGetPlugin=(Button)findViewById(R.id.bt_getPlugin);
        mGetPlugin.setOnClickListener(mContext);
    }

    /**
     *  获取插件列表
     * @return 返回插件集合
     */
    private List<Map<String,String>> findPluginList() {
        //创建存放插件的集合
        mPluginList=new ArrayList<Map<String,String>>();
        //获取包管理者
        PackageManager packageManager = mContext.getPackageManager();
        //获取所有的安装包
        List<PackageInfo> installedPackages = packageManager.getInstalledPackages(PackageManager.GET_ACTIVITIES);
        try {
            //获取当前安装包信息
            PackageInfo currentPackageInfo = packageManager.getPackageInfo(getPackageName(), 0);

          for(PackageInfo installedPackage: installedPackages){
              String packageName = installedPackage.packageName;
              String sharedUserId = installedPackage.sharedUserId;//分享的唯一标识
              Log.e(TAG, "packageName: "+packageName);
              Log.e(TAG, "sharedUserId: "+sharedUserId);
              if(null==sharedUserId||!sharedUserId.equals(currentPackageInfo.sharedUserId)||packageName.equals(getPackageName())){
                  //以上条件不属于当前应用的插件
                  continue;
              }
              Map<String ,String > pluginMap=new HashMap<String,String>();
              String label = installedPackage.applicationInfo.loadLabel(packageManager).toString();
              pluginMap.put("packageName",packageName);
              pluginMap.put("label",label);
              mPluginList.add(pluginMap);
          }

        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return mPluginList;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Map<String, String> map = mPluginList.get(position);
        Drawable drawable = getPluginRes(map);
        if(drawable==null){
            Toast.makeText(mContext, "资源不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        mImage.setImageDrawable(drawable);
        mPopupWindow.dismiss();
    }

    /**
     * 获取插件资源
     * @param map
     * @return
     */
    private Drawable getPluginRes(Map<String, String> map) {
        Drawable drawable=null;
        String packageName = map.get("packageName");
        try {
            //通过包名获取插件上下文
            Context mPluginContext = mContext.createPackageContext(packageName, CONTEXT_IGNORE_SECURITY);
            int resId=findPluginRes(mPluginContext,packageName);
            drawable = mPluginContext.getResources().getDrawable(resId);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return drawable;
    }

    /**
     * 查找插件中对应资源的Id
     * @param mPluginContext
     * @param packageName
     * @return
     */
    private int findPluginRes(Context mPluginContext, String packageName) {
        int resid=R.mipmap.main_bg;//默认为当前值
        /**
         * PathClassLoader 一个类加载器  PathClassLoader extends BaseDexClassLoader
         * mPluginContext.getPackageResourcePath()获取包资源路径
         */
        PathClassLoader pathClassLoader=new PathClassLoader(mPluginContext.getPackageResourcePath(),PathClassLoader.getSystemClassLoader());
        try {
            /**
             * R$mipmap  在字节码文件中表示drawable是R的内部类
             * 反射获取到了mipmap字节码文件
             */
            Class<?> aClass = Class.forName(packageName + ".R$mipmap", true, pathClassLoader);
            //获取字段 getDeclaredFields获取所有的字段
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field field:declaredFields) {
                //获取字段名
                String name = field.getName();
                if ("main_bg".equals(name)){
                    try {
                        //获取该字段的值
                        resid = field.getInt(name);
                        break;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return resid;
    }

    /**
     * 获取皮肤
     * @param v
     */
    @Override
    public void onClick(View v) {
        if(mPopupWindow==null){
            View contentView = LayoutInflater.from(mContext).inflate(R.layout.popup_layout, null);
            mPopupWindow=new PopupWindow(contentView, LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.WRAP_CONTENT);
            mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));//设置背景
            //查找插件列表
            List<Map<String, String>> pluginList = findPluginList();
            Log.e(TAG, "pluginList: "+pluginList.size() );
            if(pluginList==null||pluginList.size()==0){
                Toast.makeText(mContext, "没有可用插件", Toast.LENGTH_SHORT).show();
                return;
            }
            ListView pluginListView= (ListView) contentView.findViewById(R.id.list_item);
            SimpleAdapter adapter=new SimpleAdapter(mContext,pluginList,android.R.layout.simple_dropdown_item_1line,new String[]{"label"},new int[]{android.R.id.text1});
            //显示插件列表
            pluginListView.setAdapter(adapter);
            pluginListView.setOnItemClickListener(mContext);
            mPopupWindow.setWidth(600);
            mPopupWindow.setHeight(150*pluginList.size());
            mPopupWindow.setFocusable(true);
            mPopupWindow.setOutsideTouchable(true);
            mPopupWindow.showAsDropDown(v,0,20);
            mPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
                @Override
                public void onDismiss() {
                    if (mPopupWindow!=null){
                        mPopupWindow=null;
                    }
                }
            });
        }
    }
}

点击切换皮肤会去检查是否具有当前应用的插件,如果有弹出一个popupWindow,我们可以选择不同的选项进行切换,具体效果如下:

选择插件A

android 插件 IDsCreate Android 插件化开发实例_android_03


选择插件B

android 插件 IDsCreate Android 插件化开发实例_ide_04

上面有这个么一个类PathClassLoader,具体可以参考

通过上面的小案例我们应该有举一反三地能力,只要对PathClassLoader,PackageManager这两个api熟悉,对java的反射机制足够熟悉,那么其他的资源我们也可以获取和更换,说不定还可以给自己的微信开发一些小表情呢。


2020年4月1日补充

  • 我们也可以通过 getPackageCodePath来获取java字节码,做到java资源的共享
    首先创建在插件里面各创建一个Test类
public class Test {
    public static String getName(){
        return "Hello World plugin_a";
    }
}

public class Test {
    public static String getName(){
        return "Hello World plugin_b";
    }
}

然后我么去通过反射获取这个方法的返回值

/**
     * 加载java字节码文件,调用getName方法      核心API getPackageCodePath()
     * @param mPluginContext
     * @param packageName
     */
    private void findPluginGetName(Context mPluginContext, String packageName) {
        PathClassLoader pathClassLoader =
         new PathClassLoader(mPluginContext.getPackageCodePath(),
         PathClassLoader.getSystemClassLoader());
        try {
            Class<?> aClass = Class.forName(packageName + ".Test", true, pathClassLoader);
            Object mInstance = aClass.newInstance();
            Method mGetName = aClass.getMethod("getName", null);
            String name= (String) mGetName.invoke(mInstance);
            mGetPlugin.setText(name);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 加载颜色资源
<color name="color_red">#ff0000</color>
/**
     * 加载颜色资源
     * @param mPluginContext
     * @param packageName
     * @return
     */
    private int findPluginColorRes(Context mPluginContext, String packageName) {
        int resid = View.NO_ID;//默认为当前值
        PathClassLoader pathClassLoader = 
        new PathClassLoader(mPluginContext.getPackageResourcePath(), 
          PathClassLoader.getSystemClassLoader());
        try {
            Class<?> aClass = Class.forName(packageName + ".R$color", true, pathClassLoader);
            //获取字段 getDeclaredFields获取所有的字段
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field field : declaredFields) {
                //获取字段名
                String name = field.getName();
                if ("color_red".equals(name)) {
                    try {
                        //获取该字段的值
                        resid = field.getInt(name);
                        break;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return resid;
    }

效果如下

完整代码如下

public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener, View.OnClickListener {
    private static final String TAG = "MainActivity";
    private MainActivity mContext;
    private PopupWindow mPopupWindow;
    private ArrayList<Map<String, String>> mPluginList;
    private ImageView mImage;
    private Button mGetPlugin;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        mContext = this;
        mImage = (ImageView) findViewById(R.id.iv_image);
        mGetPlugin = (Button) findViewById(R.id.bt_get_plugin);
        mGetPlugin.setOnClickListener(mContext);
    }

    /**
     * 获取插件列表
     *
     * @return 返回插件集合
     */
    private List<Map<String, String>> findPluginList() {
        //创建存放插件的集合
        mPluginList = new ArrayList<Map<String, String>>();
        //获取包管理者
        PackageManager packageManager = mContext.getPackageManager();
        //获取所有的安装包
        List<PackageInfo> installedPackages = packageManager.getInstalledPackages(PackageManager.GET_ACTIVITIES);
        try {
            //获取当前安装包信息
            PackageInfo currentPackageInfo = packageManager.getPackageInfo(getPackageName(), 0);

            for (PackageInfo installedPackage : installedPackages) {
                String packageName = installedPackage.packageName;
                String sharedUserId = installedPackage.sharedUserId;//分享的唯一标识
                Log.e(TAG, "packageName: " + packageName);
                Log.e(TAG, "sharedUserId: " + sharedUserId);
                if (null == sharedUserId || !sharedUserId.equals(currentPackageInfo.sharedUserId) || packageName.equals(getPackageName())) {
                    //以上条件不属于当前应用的插件
                    continue;
                }
                Map<String, String> pluginMap = new HashMap<String, String>();
                String label = installedPackage.applicationInfo.loadLabel(packageManager).toString();
                pluginMap.put("packageName", packageName);
                pluginMap.put("label", label);
                mPluginList.add(pluginMap);
            }

        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return mPluginList;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Map<String, String> map = mPluginList.get(position);
        Drawable drawable = getPluginRes(map);
        if (drawable == null) {
            Toast.makeText(mContext, "资源不存在", Toast.LENGTH_SHORT).show();
            return;
        }
        mImage.setImageDrawable(drawable);
        mPopupWindow.dismiss();
    }

    /**
     * 获取插件资源
     *
     * @param map
     * @return
     */
    private Drawable getPluginRes(Map<String, String> map) {
        Drawable drawable = null;
        String packageName = map.get("packageName");
        try {
            //通过包名获取插件上下文
            Context mPluginContext = mContext.createPackageContext(packageName, CONTEXT_IGNORE_SECURITY);
            int resId = findPluginRes(mPluginContext, packageName);
            drawable = mPluginContext.getResources().getDrawable(resId);

            int colorDrawable = mPluginContext.getResources().getColor(findPluginColorRes(mPluginContext, packageName));
            mGetPlugin.setTextColor(colorDrawable);

            findPluginGetName(mPluginContext, packageName);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return drawable;
    }

    /**
     * 查找插件中对应资源的Id
     *
     * @param mPluginContext
     * @param packageName
     * @return
     */
    private int findPluginRes(Context mPluginContext, String packageName) {
        int resid = R.drawable.bg_theme;//默认为当前值
        /**
         * PathClassLoader 一个类加载器  PathClassLoader extends BaseDexClassLoader
         * mPluginContext.getPackageResourcePath()获取包资源路径
         */
        PathClassLoader pathClassLoader = new PathClassLoader(mPluginContext.getPackageResourcePath(), PathClassLoader.getSystemClassLoader());
        try {
            /**
             * R$drawable  在字节码文件中表示drawable是R的内部类
             * 反射获取到了mipmap字节码文件
             */
            Class<?> aClass = Class.forName(packageName + ".R$drawable", true, pathClassLoader);
            //获取字段 getDeclaredFields获取所有的字段
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field field : declaredFields) {
                //获取字段名
                String name = field.getName();
                if ("bg_theme".equals(name)) {
                    try {
                        //获取该字段的值
                        resid = field.getInt(name);
                        break;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return resid;
    }

    /**
     * 加载颜色资源
     * @param mPluginContext
     * @param packageName
     * @return
     */
    private int findPluginColorRes(Context mPluginContext, String packageName) {
        int resid = View.NO_ID;//默认为当前值
        PathClassLoader pathClassLoader = new PathClassLoader(mPluginContext.getPackageResourcePath(), PathClassLoader.getSystemClassLoader());
        try {
            Class<?> aClass = Class.forName(packageName + ".R$color", true, pathClassLoader);
            //获取字段 getDeclaredFields获取所有的字段
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field field : declaredFields) {
                //获取字段名
                String name = field.getName();
                if ("color_red".equals(name)) {
                    try {
                        //获取该字段的值
                        resid = field.getInt(name);
                        break;
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return resid;
    }

    /**
     * 加载java字节码文件,调用getName方法      核心API getPackageCodePath()
     * @param mPluginContext
     * @param packageName
     */
    private void findPluginGetName(Context mPluginContext, String packageName) {
        PathClassLoader pathClassLoader = new PathClassLoader(mPluginContext.getPackageCodePath(), PathClassLoader.getSystemClassLoader());
        try {
            Class<?> aClass = Class.forName(packageName + ".Test", true, pathClassLoader);
            Object mInstance = aClass.newInstance();
            Method mGetName = aClass.getMethod("getName", null);
            String name= (String) mGetName.invoke(mInstance);
            mGetPlugin.setText(name);

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

    /**
     * 获取皮肤
     *
     * @param v
     */
    @Override
    public void onClick(View v) {
        if (mPopupWindow == null) {
            View contentView = LayoutInflater.from(mContext).inflate(R.layout.popup_layout, null);
            mPopupWindow = new PopupWindow(contentView, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
            mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));//设置背景
            //查找插件列表
            List<Map<String, String>> pluginList = findPluginList();
            Log.e(TAG, "pluginList: " + pluginList.size());
            if (pluginList == null || pluginList.size() == 0) {
                Toast.makeText(mContext, "没有可用插件", Toast.LENGTH_SHORT).show();
                return;
            }
            ListView pluginListView = (ListView) contentView.findViewById(R.id.list_view);
            SimpleAdapter adapter = new SimpleAdapter(mContext, pluginList, android.R.layout.simple_dropdown_item_1line, new String[]{"label"}, new int[]{android.R.id.text1});
            //显示插件列表
            pluginListView.setAdapter(adapter);
            pluginListView.setOnItemClickListener(mContext);
            mPopupWindow.setWidth(getWindow().getWindowManager().getDefaultDisplay().getWidth());
            mPopupWindow.setHeight(150 * pluginList.size());
            mPopupWindow.setFocusable(true);
            mPopupWindow.setOutsideTouchable(true);
            mPopupWindow.showAsDropDown(v, 0, 20);
            mPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
                @Override
                public void onDismiss() {
                    if (mPopupWindow != null) {
                        mPopupWindow = null;
                    }
                }
            });
        }
    }
}

main_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/bt_get_plugin"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="获取插件皮肤" />
      <ImageView
          android:id="@+id/iv_image"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:src="@drawable/bg_theme"
          />
</LinearLayout>

popup_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>