Android 原生语言为java,java文件通过java虚拟机后会变为.class文件,java文件虽然能在JVM中运行,但是想在Android运行环境中执行还需要特殊处理,那就是dx处理,它会对.class文件翻译、重构、解释、压缩等操作。
dx 处理会使用到一个工具 dx.jar,这个文件位于 SDK 中,具体的目录大致为 你的sdk根目录/build-tools/任意版本里面。使用 dx 工具处理.class 文件,执行完之后就会生成一个.dex文件,这个 .dex 文件就可以直接在 Android 运行时环境执行,一般可以通过 PathClassLoader 去加载 dex 文件。
PathClassLoader 和 DexClassLoader的区别:前者只能加载内存中已经安装的apk中的dex,而后者可以加载sd卡中的apk/jar ,因此 DexClassLoader 是热修复和插件化的基础。
Android 中,apk 安装时,系统会使用 PathClassLoader 来加载apk文件中的dex,PathClassLoader的构造方法中,调用父类的构造方法,实例化出一个 DexPathList ,DexPathList 通过 makePathElements 在所有传入的dexPath 路径中,找到DexFile,存入 Element 数组,在应用启动后,所有的类都在 Element 数组中寻找,不会再次加载。
在热更新时,实现 DexClassLoader 子类,传入要更新的dex/apk/jar补丁文件路径(如sd卡路径中存放的patch.jar),通过反射拿到 DexPathList,得到补丁 Element 数组,再从Apk原本安装时使用的 PathClassLoader 中拿到旧版本的 Element 数组,合并新旧数组,将补丁放在数组最前面,这样一个类一旦在补丁 Element 中找到,就不会再次加载,这样就能替换旧 Element 中的旧类,实现热更新。
插件化主要的两个问题,一个是资源,一个是Activity,Activity也会加载很多资源文件,本篇文章重点从加载资源文件说起,进一步说说 AssetManager 的 addAssetPath 方法来实现资源的插件化。
Android资源文件分为两类,一种是res目录下存放的可编译性文件,编译时,系统会自动在R.java中生成资源文件的十六进制
第二种是assets目录下存放的资源文件,因为apk在编译的时候不会编译assets下的资源文件,所以不能通过R.XX的方式访问,因为apk下载后不会解压到本地,所以我们无法直接获取到assets的绝对路径,这时候就要借助AssetManager类的open方法来获取assets目录下的文件资源了。
AssetManager中有个addAssetPath(String path)方法,在App启动的时候,会把当前apk的路径传递进去,接下来AssetManager和Resources 就能访问apk的所有资源了,由于addAssetPath方法是不对外的,我们只能通过反射的方式获取,然后把插件apk的路径传递过去,就把插件的资源添加到资源池中了。
package com.demo.dynamicdemo11;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
import opencv.wanmao.com.mylibrary.IDynamic;
public class MainActivity extends AppCompatActivity {
private TextView tv;
private Button button;
private String apkName = "plugin1.apk"; //apk名称
private AssetManager mAssetManager;
private Resources mResources;
private Resources.Theme mTheme;
private String dexpath = null; //apk文件地址
private File fileRelease = null; //释放目录
private DexClassLoader classLoader = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.tv);
button = findViewById(R.id.btn_6);
File extractFile = this.getFileStreamPath(apkName);
dexpath = extractFile.getPath();
fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE
classLoader = new DexClassLoader(dexpath,
fileRelease.getAbsolutePath(), null, getClassLoader());
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadResources();
Class mLoadClassDynamic = null;
// Dynamic
try {
mLoadClassDynamic = classLoader.loadClass("opencv.wanmao.com.plug1.Dynamic");
Object dynamicObject = mLoadClassDynamic.newInstance();
IDynamic dynamic = (IDynamic) dynamicObject;
String content = dynamic.getStringForResId(MainActivity.this);
Log.e("----demo-->>", "msg:>>>>>>>" + content);
tv.setText(content);
Toast.makeText(getApplicationContext(), content + "", Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e("----demo-->>", "msg:" + e.getMessage());
}
}
});
}
//反射获取AssetManager并执行addAssetPath方法
//反射获取AssetManager 并间接获取得到Resources
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexpath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
mResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@Override
public AssetManager getAssets() {
if (mAssetManager == null) {
Log.e("----demo-->>", "mAssetManager is null");
return super.getAssets();
}
Log.e("----demo-->>", "mAssetManager is not null");
return mAssetManager;
}
@Override
public Resources getResources() {
if (mResources == null) {
Log.e("----demo-->>", "mResources is null");
return super.getResources();
}
Log.e("----demo-->>", "mResources is not null");
return mResources;
}
@Override
public Resources.Theme getTheme() {
if (mTheme == null) {
Log.e("----demo-->>", "Theme is null");
return super.getTheme();
}
Log.e("----demo-->>", "Theme is not null");
return mTheme;
}
}
在此记录下Android中的路径问题:
Context.getFilesDir
获取路径:/data/user/0/应用包名/files
该目录是应用的文件存储目录,应用被卸载时,该目录一同被系统删除。默认存在,默认具备读写权限(6.0系统可以不用向用户申请)
Context.getCacheDir
获取路径:/data/user/0/应用包名/cache
该目录是应用的文件缓存目录,应用被卸载时,该目录一同被系统删除。默认存在,默认具备读写权限。不同于getFileDir,该目录下的文件在系统内存紧张时,会被清空文件,来腾出空间供系统使用,著名的图片加载库ImageLoader就是在没有外置存储读写权限时使用此文件夹。getFileDir,不会因为系统内存不足而被清空。(6.0系统可以不用向用户申请)
Context.getObbDir
获取路径:/storage/emulated/0/Android/obb/应用包名
该目录是应用的数据存放目录,一般被用来存放游戏数据包obb文件。默认存在,可读写(6.0系统可以不用向用户申请)
Context.CodeCacheDir
获取路径:/data/user/0/应用包名/code_cache
默认存在,可读写。(6.0系统可以不用向用户申请)
Context.getExternalFilesDir
获取路径:(以下载目录为准) /storage/emulated/0/Android/data/应用包名/files/Download
默认存在,可读写。(6.0系统可以不用向用户申请)
Context.getExternalCacheDir
获取路径:/storage/emulated/0/Android/data/应用包名/cache
默认存在,可读写。(6.0系统可以不用向用户申请)
Context.getDatabasePath
获取路径:/data/user/0/应用包名/databases/参数名
默认不存在,可读写。(6.0系统可以不用向用户申请)
Context.getDir
获取路径:/data/user/0/应用包名/app_参数名
默认存在,可读写。分为Private等三个权限,private代表仅能自己访问。(6.0系统可以不用向用户申请)
Context.getPackageCodePath
获取路径:/data/app/应用包名-1/base.apk
默认存在,获取apk包路径
Context.getRootDirectory
获取路径:/system
默认存在,不可读写(除非具备root权限)
Environment.getExternalStorageDirectory
获取路径:/storage/emulated/0
默认存在,声明权限则可读写(6.0和以后系统还需要向用户申请同意才可以)
Environment.getExternalStoragePublicDirectory
获取路径:/storage/emulated/0/Download(以下载目录为例)
默认存在,声明权限则可读写(6.0和以后系统还需要向用户申请同意才可以)
Context.getFileStreamPath
获取路径:/data/data/应用包名/files/download(示例download)
该目录是应用的文件存储目录,应用被卸载时,该目录一同被系统删除。默认存在,默认具备读写权限(6.0系统可以不用向用户申请
File f1=Environment.getDataDirectory(); // /data
File f2=Environment.getDownloadCacheDirectory(); // /cache
File f3=Environment.getRootDirectory(); // /system
File f4= context.getCacheDir(); // /data/data/com.example.fileexiststest/cache
File f5= context.getDatabasePath("abc.db"); // /data/data/com.example.fileexiststest/databases/abc.db
File f6= context.getFilesDir(); // /data/data/com.example.fileexiststest/files
File f7= context.getFileStreamPath("test2.txt"); // /data/data/com.example.fileexiststest/files/test2.txt