前段时间到阿里巴巴参加支付宝技术分享沙龙,看到支付宝在Android使用插件化的技术,挺好奇的。正好这几天看到了农民伯伯的相关文章,因此简单整理了下,有什么错误希望大神指正。
核心类
1.1 DexClassLoader类
可以加载jar/apk/dex,可以从SD卡中加载为安装的apk。
1.2 PathClassLoader类
只能加载已经安装到Android系统中的apk文件。
一、正文
1.1 动态加载jar
类似于eclipse的插件化实现, 首先定义好接口, 用户实现接口功能后即可通过动态加载的方式载入jar文件, 以实现具体功能。注意, 这里的jar包需要经过android dx工具的处理 , 否则不能使用。
首先我们定义如下接口 :
package com.example.interf; /** * @Title: ILoader.java * @Package com.example.loadjardemo * @Description: 通用接口, 需要用户实现 * @version V1.0 */ public interface ILoader { public String sayHi(); }
用户需实现,该接口, 并且将工程导出为jar包的形式。
示例如下 :
public class JarLoader implements ILoader { public JarLoader() { } @Override public String sayHi() { return "I am jar loader."; } }
最后, 实现功能的代码打包成jar包 :
首先选中工程, 右键后选择“导出”, 然后选择“java”-----“jar文件”, 然后将你的具体功能实现类导出为jar,文件名为loader.jar,如下图所示 :
将打包好的jar拷贝到SDK安装目录android-sdk-windowsplatform-tools下,DOS进入这个目录,执行如下命令:
dx --dex --output=loader_dex.jar loader.jar
然后将loader_dex.jar放到android手机中, 这里我们放到SD卡根目录下。
动态加载代码 :
/** * * @Title: loadJar * @Description: 项目工程中必须定义接口, 而被引入的第三方jar包实现这些接口,然后进行动态加载 。 * 相当于第三方按照接口协议来开发, 使得第三方应用可以以插件的形式动态加载到应用平台中。 * @return void * @throws */ private void loadJar(){ final File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().toString() + File.separator + "loader_dex.jar"); BaseDexClassLoader cl = new BaseDexClassLoader(Environment.getExternalStorageDirectory().toString(), optimizedDexOutputPath, optimizedDexOutputPath.getAbsolutePath(), getClassLoader()); Class libProviderClazz = null; try { // 载入JarLoader类, 并且通过反射构建JarLoader对象, 然后调用sayHi方法 libProviderClazz = cl.loadClass("com.example.interf.JarLoader"); ILoader loader = (ILoader)libProviderClazz.newInstance(); Toast.makeText(MainActivity.this, loader.sayHi() , Toast.LENGTH_SHORT).show(); } catch (Exception exception) { // Handle exception gracefully here. exception.printStackTrace(); } }
效果如下图所示 :
1.2 加载未安装的apk
首先新建一个Android项目, 定义如下接口 :
public interface ISayHello { public String sayHello() ; }
定义一个Activity实现该接口, 如下:
package com.example.loaduninstallapkdemo; import android.app.Activity; import android.os.Bundle; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.widget.Toast; /** * * @ClassName: UninstallApkActivity * @Description: 这是被动态加载的Activity类 * */ public class UninstallApkActivity extends Activity implements ISayHello{ private View mShowView = null ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mShowView = findViewById(R.id.show) ; mShowView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(UninstallApkActivity.this, "这是已安装的apk被动态加载了", Toast.LENGTH_SHORT).show(); } }) ; } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } @Override public String sayHello(){ return "Hello, this apk is not installed"; } }
然后将该编译生apk, 并且将该apk拷贝到SD卡根目录下。
动态加载未安装的apk
/** * * @Title: loadUninstallApk * @Description: 动态加载未安装的apk * @return void * @throws */ private void loadUninstallApk(){ String path = Environment.getExternalStorageDirectory() + File.separator; String filename = "LoadUninstallApkDemo.apk"; // 4.1以后不能够将optimizedDirectory设置到sd卡目录, 否则抛出异常. File optimizedDirectoryFile = getDir("dex", 0) ; DexClassLoader classLoader = new DexClassLoader(path + filename, optimizedDirectoryFile.getAbsolutePath(), null, getClassLoader()); try { // 通过反射机制调用, 包名为com.example.loaduninstallapkdemo, 类名为UninstallApkActivity Class mLoadClass = classLoader.loadClass("com.example.loadunstallapkdemo.UninstallApkActivity"); Constructor constructor = mLoadClass.getConstructor(new Class[] {}); Object testActivity = constructor.newInstance(new Object[] {}); // 获取sayHello方法 Method helloMethod = mLoadClass.getMethod("sayHello", null); helloMethod.setAccessible(true); Object content = helloMethod.invoke(testActivity, null); Toast.makeText(MainActivity.this, content.toString(), Toast.LENGTH_LONG).show(); } catch (Exception e) { e.printStackTrace(); } }
DexClassLoader 注意点 :
A class loader that loads classes from .jar
and .apk
files containing a classes.dex
entry. This can be used to execute code not installed as part of an application.
This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getDir(String, int)
to create such a directory:
File dexOutputDir = context.getDir("dex",0);
Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks.
效果如图 :
1.3 加载已安装的apk
将1.2中的apk安装到手机中,我的例子中,该apk的包名为“com.example.loaduninstallapkdemo”,Activity名为"UninstallApkActivity". 加载代码如下 :
/**
*
* @Title: loadInstalledApk
* @Description: 动态加载已安装的apk
* @return void
* @throws
*/
private void loadInstalledApk() {
try {
String pkgName = "com.example.loaduninstallapkdemo";
Context context = createPackageContext(pkgName,
Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE) ;
// 获取动态加载得到的资源
Resources resources = context.getResources() ;
// 过去该apk中的字符串资源"tips", 并且toast出来,apk换肤的实现就是这种原理
String toast = resources.getString(resources.getIdentifier("tips", "string", pkgName) ) ;
Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show() ;
Class cls = context.getClassLoader().loadClass(pkgName + ".UninstallApkActivity") ;
// 跳转到该Activity
startActivity(new Intent(context, cls)) ;
} catch (NameNotFoundException e) {
e.printStackTrace();
}catch (ClassNotFoundException e) {
Log.d("", e.toString()) ;
}
}
效果如图:
消息被Toast出来, 并且跳转到了目标Activity.