一:ShareUserId模式
由于公司项目需要,2012年曾简单的设计了一套插件化框架,经过几次修改直到现在,这个框架已逐步稳定,不过现在看起来的话,该框架并不能算是真正意义上的插件化,因为它的流程是一个主程序管理着一套类似应用商店的逻辑,而“商店”里的程序作为“插件”,只能在主程序中被调起,但仍避免不了一个很严重的问题:“插件”不能免安装运行,对于用户来说,只是将程序的调起界面从系统桌面移动到了主程序中而已。
但该框架并不是一无是处的,比如它可以让插件自己定义它所需要的启动运行参数让主程序提供这些数据、可以使用主程序的数据库及资源等,但这些特性只是通过一些技巧去实现的只是让它看起来像插件一样。
这种模式的关键还是在于 android:sharedUserId 统一来达到数据共享,而主程序也是通过过滤SharedUserId对手机终端已安装的应用进行插件识别
关键代码:遍历识别插件
public static List<PluginInfoBean> getLocalPlugin(Context context,String userid){
List<PluginInfoBean> plugins
= new ArrayList<PluginInfoBean>(); //插件集合
PluginInfoBean plug = null;
PackageManager pm = context.getPackageManager();
ApplicationInfo pkginf = new ApplicationInfo ();
List<PackageInfo> pkgs =
pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
/**
* 遍历本地apk
*/
for(PackageInfo pkg:pkgs){
String packageName = pkg.packageName; //包名
String sharedUserId = pkg.sharedUserId; //共享ID
pkginf = pkg.applicationInfo; //apk信息
// 过滤非插件apk
if(!userid.equals(sharedUserId)
||UserCache.packageName.equals(packageName)){
continue; }
String prcessName = pkg.applicationInfo.processName; //进程名
String version = pkg.versionName; //插件版本
/**
* 反射插件方法,获得插件信息
*/
try {
Context con;
con = context
.createPackageContext(packageName, Context.CONTEXT_INCLUDE_CODE
|Context.CONTEXT_IGNORE_SECURITY); // 创建插件句柄
con = createContext(context,packageName);
String appName;
final Class cla=con
.getClassLoader()
.loadClass(packageName +".PluginMainClass"); // 反射插件方法类
final Object o=cla.newInstance(); // 实例化
appName = (String)cla.getMethod("getName").invoke(o); // 获取插件名称
plug = new PluginInfoBean();
plug.setAppName(appName); //设置插件名称
plug.setPackageName(packageName); //设置插件包名
plug.setIcon(pkginf.loadIcon(pm)); //设置插件图标
plug.setVersion(version); //设置插件版本
plug.setIsIntalled(true); //设置安装状态为已安装
plugins.add(plug); //添加至插件信息bean集合
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return plugins;
}
而关于插件,则按照常规开发模式进行开发即可,只需在AndroidManifest文件中设置与主程序相同的ShareUserId
二:HTML + WebView插件化模式
严格意义讲,这种方式已脱离了android开发的范畴,但不失为是一种很好的模式,原生APP作为程序入口平台,而相关的业务处理均交付给后台处理,具有很高的可扩展性。
三:DexClassLoader 动态加载APK技术
由于一个APK在未安装的情况下是无法运行的,常规的反射只能反射出java的class文件中的类,而对于一个Activity,是无法创建生命周期的,网上有很多人通过DexClassLoader类加载器去实现APK class文件的动态加载,但由于存在以下几个问题,使得传统的开发模式和流程受到了影响:
1. 未安装的Activity类通过反射调用无法拥有完整的生命周期,即使通过模拟生命周期来处理Activity事务及跳转,也存在很大的弊端(不适合拥有复杂UI交互逻辑的应用)
2. 未安装的APK的Resources无法直接通过R文件进行访问
3. 未安装的APK的AndroidManifest是无法使用的
对于上述问题,我的处理思路大致如下:
1. 在Application启动之初,将默认的类加载器替换为我们自定义的DexClassLoader类加载器
核心代码:
Context mBase = new ClassFieldGetter<Context>(this, c_mBase).get();
Object mPackageInfo = new ClassFieldGetter<Object>(mBase, c_mPackageInfo)
.get();
ClassFieldGetter<ClassLoader> sClassLoader = new ClassFieldGetter<ClassLoader>(
mPackageInfo, c_mClassLoader);
ClassLoader mClassLoader = sClassLoader.get();
ORIGINAL_LOADER = mClassLoader;
FrameClassLoader cl = new FrameClassLoader(mClassLoader);
sClassLoader.set(cl);
这样一来,我们的APP就能够加载经过处理后的APK即 .dex文件了,并能正常的加载里面的.class文件
2. 检测APP运行状态,动态更改Application的Resources对象引用,这部分代码实现和替换类加载器类似,也是反射Application对象字段进行替换的。值得一提的是当插件关闭时,需要反射调用在主程序中定义的方法来进行自身注销,这里的注销才做可以根据需求自行定义,但必须要做的是更改当前运行时Resources
3. 由于Activity无法正常的建立生命周期,所以我编写了一个“代理Activity” 类:
public class ProxyActivity extends Activity
ProxyActivity关键方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // 将Activity堆栈
ActivityStack.add(this); // 获取PluginActivity类名
pluginActivityName = getClassName(this);
// 如果获取成功,则通过插件Activity工厂提供类实例
if(pluginActivityName != null){
classInstance = new PluginActivityFactory().orderParams(pluginActivityName,this);
}
// 调用Activity onCreate方法
if(classInstance != null)
classInstance.onCreate(savedInstanceState);
}
private String getClassName(Activity activity){
Bundle bundle = activity.getIntent().getExtras();
String className = null; // 解析插件自定义Activity注册文件,获取已定义的所有Activity类名
ConfigInfo.activities =
GetActivityInfo.getInstance(this).parseXml();
if(bundle != null){
className = bundle.getString("classname");
} else {
for(ActivityInfo ai : ConfigInfo.activities){
if(ai.getIsMain()) {
className = ai.getName();
break;
}
}
}
return className;
}
这个ProxyActivity是一个Activity,它是存放在插件APP中的,但是AndroidManifest注册是在主框架的AndroidManifest.xml文件中完成的,这样一来,插件APP中所有的继承自Activity的类均可由ProxyActivity控制生命周期,而Activity本身则变成了一个普通的java类,而所有这些普通java类全部继承自一个基类PluginActivity,PluginActivity也是一个纯粹的java类,他将Activity大部分事件与方法定义在内,供ProxyActivity调用,例如:
public View findViewById(int id) {
// TODO Auto-generated method stub
return proxyActivity.findViewById(id);
}
public void finish() {
// TODO Auto-generated method stub
proxyActivity.finish();
}
ProxyActivity负责为PluginActivity提供生命周期与Context上下文,PluginActivity则负责模拟UI java类的生命周期
这种模式存在的问题: 关于插件的权限,是无法在插件中声明的,所以尽可能的将权限提前定义在主程序中
我已经完成了上面的大部分工作,并为主程序与插件程序各提供了相应的开发包,下面我提供一些Demo代码来演示一下这种模式的开发便捷性:
主程序:
DexMainActivity.java
package com.example.dledemo;
import java.util.List;
import org.link.dexframe.DLE;
import org.link.dexframe.DLEEngine;
import org.link.dexframe.DLEInitiator;
import org.link.dexframe.DLP;
import org.link.dexframe.structure.Plugin;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.view.Menu;
import android.view.MotionEvent;
import android.widget.ListView;
public class DexMainActivity extends Activity {
private ListView list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dexactivity_main);
/**
* DLE框架初始化
*/
DLE.init(this, getApplication());
/**
* 获取已安装于私有目录/data/data/[package_name]/下的插件信息列表
*/
List<Plugin> pluginList = DLE.initiator.getInstalledPlugin(DLE.engine);
// 构造插件listView
list = (ListView) findViewById(R.id.pluginlist);
PluginListAdapter adapter = new PluginListAdapter(this,pluginList);
list.setAdapter(adapter);
}
}
PluginListAdapter.java
package com.example.dledemo;
import java.util.List;
import org.link.dexframe.DLE;
import org.link.dexframe.structure.Plugin;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
@SuppressLint("NewApi")
public class PluginListAdapter extends BaseAdapter{
private Context mContext;
private List<Plugin> data;
public PluginListAdapter(Context _context, List<Plugin> _data){
mContext = _context;
data = _data;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return data.size();
}
@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return data.get(arg0);
}
@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return arg0;
}
@Override
public View getView(int position, View contentView, ViewGroup parent) {
// TODO Auto-generated method stub
ViewHolder holder = null;
if(contentView == null){
LayoutInflater inflater = LayoutInflater.from(mContext);
contentView = inflater.inflate(R.layout.plugin_list, null);
holder = new ViewHolder(contentView);
holder.icon.setBackground(data.get(position).getIcon());
holder.name.setText(data.get(position).getName());
contentView.setTag(holder);
} else {
holder = (ViewHolder) contentView.getTag();
}
final int pos = position;
contentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// TODO Auto-generated method stub // 启动插件
Plugin plugin = data.get(pos);
DLE.initiator.startPlugin(plugin, DLE.engine);
}
});
return contentView;
}
protected class ViewHolder{
protected ImageView icon;
protected TextView name;
protected ViewHolder(View view){
icon = (ImageView) view.findViewById(R.id.icon);
name = (TextView) view.findViewById(R.id.name);
}
}
}
插件:
MainActivity.java
package com.example.dlpdemoone;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.link.dexplugin.activity.PluginActivity;
import org.link.dexplugin.activity.factory.ActivityInfo;
import org.link.dexplugin.utils.ReflexInvoker;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends PluginActivity {
private TextView hello;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* 使用插件自己的数据库(验证插件resources已完全替换)
*/
copyDbfile(proxyActivity,"plugindb.mp3");
hello = (TextView)findViewById(R.id.hello);
hello.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub // 在插件程序入口类中跳转到第二个Activity(与传统调用模式基本无异,只需增加待跳转的Activity类名参数)
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("classname", "com.example.dlpdemoone.SecondActivity");
intent.putExtras(bundle);
startActivity(intent);
}
});
}
/**
* 复制数据库文件到私有目录下
* @param context
* @param filename
* @return
*/
public boolean copyDbfile(Context context, String filename) {
if (!(new File("/data/data/com.example.dledemo/plugindb.mp3").exists())) {
InputStream in = null;
FileOutputStream out = null;
try {
in = (InputStream) context.getResources().getAssets()
.open(filename);
out = context.openFileOutput(filename, Context.MODE_PRIVATE);
byte[] buffer = new byte[8192];
int count = 0;
// 开始复制dictionary.db文件
while ((count = in.read(buffer)) > 0) {
out.write(buffer, 0, count);
}
} catch (Exception ioe) {
ioe.printStackTrace();
return false;
}
try {
in.close();
out.close();
} catch (IOException e1) {
e1.printStackTrace();
return false;
}
}
return true;
}
}
SecondActivity.java
package com.example.dlpdemoone;
import org.link.dexplugin.activity.PluginActivity;
import android.app.Activity;
import android.os.Bundle;
public class SecondActivity extends PluginActivity{
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
}
}
在这种模式下,主程序与插件的开发模式与传统开发模式无异,并能使插件无需安装,直接调用,并能拥有完整的生命周期并可以通过R定位使用自己的Resources