【Android】动态加载实现的简单demo

概念▪说明

动态加载:

此处的动态加载是指从服务端或者其他地方获取jar包,并在运行时期,加载jar包,并与 jar包互相调用。
本例中,为了方便演示,将要动态加载的jar放到了assets目录下,在程序运行时期,将其加载到/data/data/pkgname/files下,来模拟从服务端获取

为什么要动态加载:

1.    减少应用安装包体积, 程序包很大时,将部分模块打成jar包,动态的从服务端获取,然后加载

2.    方便升级维护,将主要功能放入动态jar包中,主应用(不包含动态jar包的部分)主要来维护动态jar,包括jar的加载,升级等。升级应用,可以更新动态jar包,用户在不重新安装的情况下,就能做到部分(强调部分,是因为它比较适用于部分功能升级)升级

3.    插件化开发,动态jar包可以当做插件来开发,在应用中,需要什么功能,就下载什么插件,如一些皮肤主题类的功能,可以作为插件功能来开发,用户更换皮肤或者主题时,只需要下载和更新对应的插件就行,如:桌面系统(不同的桌面主题),锁屏(不同的锁屏界面和风格)

4.    感觉还有好多好处,不一一列举了........

 

以上概念就不亲自去写了,引用自Android动态加载,直接上干货,先从最简单的DEMO开始。

DEMO实现:

    首先要搞清楚原理和实现方法,众所周知,Android程序实际是运行在 Dalvik/ART虚拟机上的,而Dalvik/ART虚拟机执行其特有的文件格式——dex字节码。DEX文件通常在打包APK的过程中会生成(我们把一个APK文件解压也可以看到DEX文件,对经常进行反编译的同学来说,DEX文件是基础中的基础),在APK构建出来之后,通常我们是无法去修改里面的DEX文件的,但是,我们可以通过加载外部(SD卡或者此应用files目录)的DEX文件,通过DexClassLoader和PathClassLoader这两个类加载器加载DEX文件中的类,通过反射(invoke)的方法来调用类里的方法。而由外部加载的DEX文件我们可以在不卸载、不重新安装APK的情况下进行替换的,通过替换外部加载的DEX文件,实现动态加载(更新)我们APK的目的。

     分析完了原理,咱们就动手开始实践,从一个最简单的实例开始,该实例把需要动态加载的jar包放在assets目录下,再把该JAR包通过代码copy到/data/user/0/包名/files/目录下,最后通过DexClassLoader去动态加载这个外部的DEX文件。

    首先我们要生成这个DEX文件,生成步骤如下:

1.    随便写一个类并打包成JAR。在类里写一个方法,方便接下来展示效果。我这里就写了一个单例模式的toast方法,简单粗暴,加载成功之后,效果是直接toast这一段话。

package com.example.dongtai;

import android.content.Context;
import android.widget.Toast;

public class ToastUtil {
	private Context mContext;
	
	public ToastUtil(Context mContext){
		this.mContext = mContext;
	}
	
	private static ToastUtil mInstance;
	
	public static ToastUtil getInstance(Context context){
		if (mInstance == null) {
    		synchronized (ToastUtil.class) {
				if (mInstance == null) {
					mInstance = new ToastUtil(context);
				}
			}
		}
		return mInstance;
	}
	
	public void showToast(){
		Toast.makeText(mContext,"动态加载", 1000).show();
	}

}

2.    进入到SDK的 【build-tools\SDK版本号】 目录下,(我的是C:\Users\zhhr\AppData\Local\Android\sdk\build-tools\25.0.1),输入CMD进入到控制台,再把我们需要动态加载的JAR包copy进去,输入命令  【dx--dex --output=需要生成的DEX名 原始JAR包名】 (我的命令是(dx --dex--output=dongtai_dex.jar dongtai.jar)),之后,我们就拿到了由这个JAR包编译来的DEX文件。如下图:


3.    新建一个工程,这个工程就是我们需要动态加载的工程,把我们上一步生成的DEX文件copy进main目录下的assets文件夹里,如下图:


4.    开始进行动态加载,代码如下:

 

package com.example.zhhr.dynamicloaddexdemo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;

public class LoadUtil {
	private DexClassLoader mDexClassLoader;

	private Class<?> mToastClass;
	private Object mInstanceObject;
	private Method getInstanceMethod,showToastMethod;

	private final String mOriginPath = "dongtai_dex.jar";//assets的原始文件
	private final String mOutPath = "/dongtai_dex.jar";//file目录下的文件


	private static LoadUtil mInstance;

	private Context mContext;

	public LoadUtil(Context context){
		mContext = context;
	}
	/**
	 * 获取类型实例
	 * @return
	 */
	public static LoadUtil getInstance(Context context){
		if (mInstance == null) {
			synchronized (LoadUtil.class) {
				if (mInstance == null) {
					mInstance = new LoadUtil(context);
				}
			}
		}
		return mInstance;
	}

	/**
	 * 初始化
	 */
	@SuppressLint("NewApi") public boolean init() {
		try {
			descryptFile(mContext);
			String destFilePath = mContext.getFilesDir().getAbsolutePath() + mOutPath;
			File opFile = new File(destFilePath);
			Log.d("zhhr", opFile.getAbsolutePath());
			if (!opFile.exists()) {
				return false;
			}
			//首先获取实例
			mDexClassLoader = new DexClassLoader(opFile.toString()
					, mContext.getFilesDir().getAbsolutePath()
					, null
					, ClassLoader.getSystemClassLoader().getParent());
			//加载其中的类
			mToastClass = mDexClassLoader.loadClass("com.example.dongtai.ToastUtil");
			//获取到单例模式的方法
			getInstanceMethod = mToastClass.getMethod("getInstance",Context.class);
			showToastMethod = mToastClass.getMethod("showToast");
			//获取实例对象
			mInstanceObject = getInstanceMethod.invoke(mToastClass,mContext);
			//执行showTosat方法
			showToastMethod.invoke(mInstanceObject);


		} catch (Exception e) {
			Log.d("zhhr", e.toString());
			return false;
		}
		return true;
	}

	/**
	 * 将assets目录下的资源拷贝到file目录下
	 * @param context
	 * @throws IOException
	 */
	private boolean descryptFile(Context context) throws IOException{
		File destFile = new File(context.getFilesDir().getAbsolutePath() + mOutPath);
		if (destFile.exists()) {
			long s = 0;
			FileInputStream fis = null;
			fis = new FileInputStream(destFile);
			s = fis.available();
			if (s > 20) {// 文件大小,方式重复拷贝dex文件,20是随便给的
				return true;
			}
		}
		InputStream assetsFileInputStream = null;
		try {
			assetsFileInputStream = context.getAssets().open(mOriginPath);
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (!destFile.getParentFile().exists()) {
			destFile.getParentFile().mkdirs();
		}
		destFile.createNewFile();
		FileOutputStream fos = new FileOutputStream(destFile);
		int readNum = 0;
		while((readNum = assetsFileInputStream.read()) != -1){
			fos.write(readNum);
		}
		fos.close();
		assetsFileInputStream.close();
		return true;
	}

}

 

 

  

    4.1 我这里用最精简的代码实现了动态加载,首先是通过descryptFile这个方法把assets目录下的文件copy到/data/user/0/com.example.zhhr.dynamicloaddexdemo/files/目录下,再通过DexClassLoader这个类去加载到该目录下的DEX文件。

DexClassLoader初始化时参数意思如下:

·       第一个参数指的是我们要加载的 dex 文件的路径,它有可能是多个 dex 路径,取决于我们要加载的 dex 文件的个数,多个路径之间用 : 隔开。

·       第二个参数指的是优化后的 dex 存放目录。实际上,dex 其实还并不能被虚拟机直接加载,它需要系统的优化工具优化后才能真正被利用。优化之后的 dex 文件我们把它叫做 odex (optimizeddex,说明这是被优化后的 dex)文件。其实从 class 到 dex 也算是经历了一次优化,这种优化的是机器无关的优化,也就是说不管将来运行在什么机器上,这种优化都是遵循固定模式的,因此这种优化发生在 apk 编译。而从 dex 文件到odex 文件,是机器相关的优化,它使得 odex 适配于特定的硬件环境,不同机器这一步的优化可能有所不同,所以这一步需要在应用安装等运行时期由机器来完成。需要注意的是,在较早版本的系统中,这个目录可以指定为外部存储中的目录,较新版本的系统为了安全只允许其为应用程序私有存储空间(/data/data/apk-package-name/)下的目录,一般我们可以通过 Context#getDir(StringdirName) 得到这个目录。

·       第三个参数的意义是库文件的的搜索路径,一般来说是 .so 库文件的路径,也可以指明多个路径。

·       第四个参数就是要传入的父加载器,一般情况我们可以通过 Context#getClassLoader() 得到应用程序的类加载器然后把它传进去。

这里只介绍类的使用方法,源码的研究可以参考文章:Android动态加载入坑指南

 

4.2 得到DexClassLoader实例之后,再通过loadclass(类名)方法得到需要调用的类名。

4.3 再通过getMethod方法得到需要需要使用的方法

·       第一个参数指的是需要调用的方法名

·       第二个参数指定了需要反射调用的方法中的参数类型,我动态加载的类中,getInstance方法需要传入一个Context类型的参数,所以第二个参数指定为 Context.class,如果有多个参数,则用数组的形式,如需要传入上下文和字符串,则第二个参数为 new Class[]{Context.class,String.class})

4.4 获取到需要使用的方法之后,再通过invoke方法反射调用。

·       第一个参数是调用该方法的对象

·       第二个至第N个参数是该反射方法需要传入的参数,如不需要传入参数,则可以不填写

代码就介绍到这里,通过先反射getinstance方法,拿到实例之后,再调用 showToast方法,既可以实现动态加载。

 

以上的代码比较简单粗暴,已经上传到GITHUB,是最简单的动态加载的实现。接下来我会继续更新动态加载的一些进阶文章。

 https://github.com/zhhr1122/DynamicDemo

 



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值