插件开发可以提高一款软件的可扩展性,我个人认为他就是一种化整为零的思想。就跟电脑的主板上留了不同的接口一样,只要插上相应的硬件就可以实现具体的功能。
Android插件开发要具备以下的基本功
sharedUserId就是主程序和插件程序进行通讯的唯一标识UID.
下面是我做的一个模拟更换皮肤的案例,首先创建一个主程序和两个插件程序如下图:
然后找三张不同的图片分别放入三个应用对应的目录下,保证三张图片的命名和尺寸完全相同(我的在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
选择插件B
上面有这个么一个类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"/>