【Android】Android动态加载Jar、APK的实现
本文介绍Android中动态加载Jar、APK的实现。而主要用到的就是DexClassLoader这个类。大家都知道Android和普通的Java虚拟机有差别,它只能加载经过处理的dex文件。而加载这个dex文件可以通过DexClassLoader 和 PathClassLoader 两个类来实现这个方法。然而PathClassLoader只能加载已经安装到Android系统中的apk文件。接下来,会介绍在Android中如何动态加载Jar、如何加载未安装的APK,如何加载已经安装的APK。
1.Android如何动态加载Jar
动态加载Jar主要是用于在APP的热更新、插件化开发方面,在进行加载之前,首先需要生成Jar文件。测试的jar包定义了一个接口和一个实现类。需要注意定义接口的步骤是必不可少的,在后面利用反射加载的时候就要利用到这个接口。
定义ILoader接口:
package com.example.interf; public interface ILoader { public String sayHi(); }
定义JarLoader类:
package com.example.interf; public class JarLoader implements ILoader { @Override public String sayHi() { return "来自动态加载的Jar"; } }
然后打包为Jar文件
这里笔者导出为Loader.jar文件。有一点需要注意,就是不要把ILoader.jar接口打包进去,因为后期可能会包错: java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation ,出现这个错误的原因就是打包的Jar中有接口,然后在下面的文件中又定义了接口,出现了两个接口文件,所以报错。如果这里把ILoader接口打包进去,那么在下面的测试中就不要再定义相同的ILoader接口了。
到这里我们就把Jar文件打包成功了,接下来了需要把这个Jar文件用dx工具进行处理,dx工具在Android SDK 的tools中已经提供了,一般在android-SDK/build-tools目录下。
将上面的Loader.jar文件拷贝一份到dx同级的目录下,然后执行如下命令:
dx --dex --output=Loader_dex.jar Loader.jar
然后将生成的Loader_dex.jar文件,拷贝到手机的SD根目录下面(手机SD的根目录就是:/storage/emulated/0,读者也可以使用 Environment.getExternalStorageDirectory() 查看)
接下来就可以使用如下的代码进行加载:
其中ILoader.java接口与Loader.jar中的ILoader接口保持一直。
package com.example.test; import java.io.File; import com.example.interf.ILoader; import dalvik.system.DexClassLoader; import android.os.Bundle; import android.os.Environment; import android.app.Activity; import android.util.Log; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadJar(); } /** * @Title LoadJar * @Description 项目工程中必须定义接口(包名都要一致), 而被引入的第三方jar包实现这些接口,然后进行动态加载 。 * 相当于第三方按照接口协议来开发, 使得第三方应用可以以插件的形式动态加载到应用平台中。 * @return void */ private void loadJar(){ File dexoutputdir = getDir("dex1",0); String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "Loader_dex.jar"; DexClassLoader loader = new DexClassLoader(dexPath,dexoutputdir.getAbsolutePath(),null,getClassLoader()); try { Class clz = loader.loadClass("com.example.interf.JarLoader"); ILoader iShowToast = (ILoader) clz.newInstance(); Toast.makeText(this,iShowToast.sayHi(),Toast.LENGTH_LONG).show(); } catch (Exception e){ Log.d("dd",e.toString()); } } }
在这个类中定义的核心方法是loadJar()
private void loadJar(){ File dexoutputdir = getDir("dex1",0); String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "Loader_dex.jar"; DexClassLoader loader = new DexClassLoader(dexPath,dexoutputdir.getAbsolutePath(),null,getClassLoader()); try { Class clz = loader.loadClass("com.example.interf.JarLoader"); ILoader iShowToast = (ILoader) clz.newInstance(); Toast.makeText(this,iShowToast.sayHi(),Toast.LENGTH_LONG).show(); } catch (Exception e){ Log.d("dd",e.toString()); } }
接下来笔者解释一下上面这个方法中核心类DexClassLoader。
此处需要注意DexClassLoader的四个参数:
参数1 dexPath:待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限( <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ),否则会报与报错,Android4.4 KitKat及以后的版本需要此权限,之前的版本不需要该权限。
参数2 optimizedDirectory:解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写(安全性考虑),所以只能放在data/data下。本文getDir(“dex1”, 0)会在/data/data/**package/下创建一个名叫”app_dex1“的文件夹,其内存放的文件是自动生成Loader_dex.dex;需要注意,data/data文件夹只有在手机root之后,才看得到。
参数3 libraryPath:指向包含本地库(so)的文件夹路径,可以设为null。
参数4 parent:父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
效果图:
到这里动态加载Jar就结束了。笔者接下来总结一下思路,首先把jar文件经过dx工具处理,然后把处理后的文件放到手机的SD根目录下面,然后利用反射加载调用方法。上面其实只是实现热更新的一半,加载的Jar文件完全可以从服务器下载手机后,然后再在手机端加载,这样可以对手机上的APP进行实时的更新以及防止反编译。
2.如何加载未安装的APK
上面介绍了如何动态加载jar文件,接下来介绍如何加载未安装的APK。
首先新建一个Android项目:
定义一个接口ISayHello.java
package com.example.loaduninstallapkdemo; public interface ISayHello { public String sayHello(); }
然后新建Activity,实现ISayHello接口:
package com.example.loaduninstallapkdemo; import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity implements ISayHello{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public String sayHello() { return "Hello, this apk is not installed"; } }
然后把该工程的APK拷贝到手机的SD根目录下面,
接下来就可以使用如下的代码进行动态加载了,下面的加载过程和上面的类似,只是不再需要定义接口了。
package com.example.test; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import dalvik.system.DexClassLoader; import android.os.Bundle; import android.os.Environment; import android.app.Activity; import android.view.Menu; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadUnInstallAPK(); } private void loadUnInstallAPK(){ String path = Environment.getExternalStorageDirectory() + File.separator; String filename = "UninstallApkActivity.apk"; // 4.1以后不能够将optimizedDirectory设置到sd卡目录, 否则抛出异常. File optimizedDirectoryFile = getDir("dex", 0) ; DexClassLoader classLoader = new DexClassLoader(path + filename, optimizedDirectoryFile.getAbsolutePath(), null, getClassLoader()); try { // 通过反射机制调用, 包名为com.example.loaduninstallapkdemo, 类名为MainActivity Class mLoadClass = classLoader.loadClass("com.example.loaduninstallapkdemo.MainActivity"); 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(); } } }
效果图:
3.如何加载已经安装的APK
在介绍了如何动态加载jar,加载未安装的APK后,接下来介绍如何加载已经安装的APK,
首先将制作一个简单的APK,然后把它安装的手机上面。
package com.example.installapkdemo; import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
将该APK安装到手机后,接下来就可以进行加载了。
同样和加载未安装的APK类似,项目中也不需要定义接口。
package com.example.test; import android.os.Bundle; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.util.Log; import android.view.Menu; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadInstalledApk(); } private void loadInstalledApk(){ try { String pkgName = "com.example.installapkdemo"; Context context = createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE) ; // 获取动态加载得到的资源 Resources resources = context.getResources() ; // 获取该apk中的字符串资源"hello_world", 并且toast出来,apk换肤的实现就是这种原理 String toast = resources.getString(resources.getIdentifier("hello_world", "string", pkgName) ) ; Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show() ; Class cls = context.getClassLoader().loadClass(pkgName + ".MainActivity"); // 跳转到该Activity startActivity(new Intent(context, cls)) ; } catch (NameNotFoundException e) { e.printStackTrace(); }catch (ClassNotFoundException e) { Log.d("", e.toString()) ; } } }
效果图:
原文链接: