热更新Tinker研究(一):运行tinker-sample-android
热更新Tinker研究(二):结合源码学习Dex格式
热更新Tinker研究(三):加载补丁
热更新Tinker研究(四):TinkerLoader
热更新Tinker研究(五):Application的隔离
热更新Tinker研究(六):TinkerPatchPlugin
热更新Tinker研究(七):Dex的patch文件生成
热更新Tinker研究(八):res和so的patch文件生成
热更新Tinker研究(九):Dex文件的patch
热更新Tinker研究(十):Res文件的patch
热更新Tinker研究(十一):so文件的patch
合成补丁后如何在启动后对应用进行更改呢,处理这个事情的主要类是TinkerLoader,对应dex、res、so文件分别是TinkerDexLoader,TinkerResourceLoader以及TinkerSoLoader。
为了更好地控制热更新的过程,以及保存热更新后的结果内容,需要在data目录下保存一些信息和生成patch结果产物。以下是data/data/packageName/下的目录树
.
├── cache
├── code_cache
│ └── com.android.opengl.shaders_cache
├── program_cache
├── shared_prefs
│ └── tinker_own_config_tinker.sample.android.xml
├── tinker
│ ├── info.lock
│ ├── patch-f7435e89
│ │ ├── dex
│ │ │ ├── classes.dex.jar
│ │ │ └── test.dex.jar
│ │ ├── lib
│ │ │ └── lib
│ │ │ ├── arm64-v8a
│ │ │ │ └── libHelloJNI.so
│ │ │ ├── armeabi
│ │ │ │ └── libHelloJNI.so
│ │ │ ├── armeabi-v7a
│ │ │ │ └── libHelloJNI.so
│ │ │ └── x86
│ │ │ └── libHelloJNI.so
│ │ ├── odex
│ │ │ ├── classes.dex.dex
│ │ │ └── test.dex.dex
│ │ ├── patch-f7435e89.apk
│ │ └── res
│ │ └── resources.apk
│ └── patch.info
└── tinker_temp
└── patch.retry
其中shared_prefs目录下文件的作用和sharedpreference的作用类似,用于保存键值对。tinker/patch.info主要保存的printfinger的信息,也就是设备相关信息和一些文件的md5值。/tinker_temp/patch.retry保存的是一些重试信息。tinker/patch-XXX表示某个版本的patch文件,下面有新生成的dex,so,以及resources.apk等。
该文件主要保存升级信息
#from old version:f7435e89150d55742dfdfc0d3bd52e38 to new version:f7435e89150d55742dfdfc0d3bd52e38
#Thu Apr 06 14:30:35 GMT+08:00 2017
print=GIONEE/GN8002/GIONEE_BBL7516A\:6.0/MRA58K/1466615086\:eng/release-keys
new=f7435e89150d55742dfdfc0d3bd52e38
old=f7435e89150d55742dfdfc0d3bd52e38
该文件保存重试信息
#Thu Apr 06 14:30:22 GMT+08:00 2017
times=2
md5=f7435e89150d55742dfdfc0d3bd52e38
该文件的作用类似于Android的sharedpreference,用xml保存键值对。
<map>
<int name="safe_mode_count" value="0" />
map>
在加载补丁目录时,需要根据patch.info,也就是new的md5的前8个字符。
public static String getPatchVersionDirectory(String version) {
if (version == null || version.length() != ShareConstants.MD5_LENGTH) {
return null;
}
return ShareConstants.PATCH_BASE_NAME + version.substring(0, 8);
}
tinker是一种典型的java派别的热修复做法,典型可以参考
Qzone热更新方案
原理就是去修改dexElements,将new.dex插入到前面,由于在查找类时,会顺序查找,这样就达到了热修复的目的。具体可见安卓App热补丁动态修复技术介绍。不过Tinker目前的new.dex是一个全量使用的情况,也就是直接把所有dex文件都拿过来,所以会有空间占用比较大的情况。不过这种保守的做法可以避免CLASS_ISPREVERIFIED标记校验问题以及插桩引起的内存地址混乱问题。同时在art虚拟机中,dex2oat已经将类的各个地址写死,所以采用插桩方式很可能会导致地址混乱。所以qZone方式既有性能问题,也可能导致错误。
dex的加载主要由TinkerDexLoader负责,除了利用反射加载dex以外,如果系统进行了OTA升级,还会进行dex的优化。
这里动态加载dex的基本原理是利用反射,但是由于不同版本的PathClassLoader机制不一样,这里也需要分版本处理:
这三个版本中核心原理都是去修改BaseDexClassLoader中的dexList(DexPathList类型),PatchClassLoader继承BaseClassLoader,BaseClassLoader继承ClassLoader。
实际上也即是修改DexPathList中dexElements,
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";
/** class definition context */
private final ClassLoader definingContext;
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;
......
}
这三个版本不同的地方也只是在不同版本之间,makePathElements方法的查找参数不同。
v4中的PatchClassLoader
public class PathClassLoader extends ClassLoader {
private final String path;
private final String libPath;
/*
* Parallel arrays for jar/apk files.
*
* (could stuff these into an object and have a single array; * improves clarity but adds overhead)
*/
private final String[] mPaths;
private final File[] mFiles;
private final ZipFile[] mZips;
private final DexFile[] mDexs;
......
}
这里主要通过mPaths、mFiles、mZips、mDexs四个数组来控制dex的动态加载,所以只需要利用反射去改变这四个数组的值即可。
V4.install()核心代码
ShareReflectUtil.expandFieldArray(loader, "mPaths", extraPaths);
ShareReflectUtil.expandFieldArray(loader, "mFiles", extraFiles);
ShareReflectUtil.expandFieldArray(loader, "mZips", extraZips);
try {
ShareReflectUtil.expandFieldArray(loader, "mDexs", extraDexs);
} catch (Exception e) {
}
在Android N以上版本,会采用一种parent classLoader的方式,也就是将originClassLoader作为parent,
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
class AndroidNClassLoader extends PathClassLoader {
static ArrayList oldDexFiles = new ArrayList<>();
PathClassLoader originClassLoader;
private AndroidNClassLoader(String dexPath, PathClassLoader parent) {
super(dexPath, parent.getParent());
originClassLoader = parent;
}
......
//根据不同的情况使用不同的classLoader
public Class findClass(String name) throws ClassNotFoundException {
// loader class use default pathClassloader to load
if (name != null && name.startsWith("com.tencent.tinker.loader.") && !name.equals("com.tencent.tinker.loader.TinkerTestDexLoad")) {
return originClassLoader.loadClass(name);
}
return super.findClass(name);
}
......
}
这里不同的类会才用不同的classLoader,如果需要查找Loader相关类,就会从原始classLoader加载,也就是从baseApk中加载,否则就从新生成的AndroidNClassLoader中加载,也就是从new.dex中加载。引入parent classLoader的目的是因为Android N版本会有混合编译,这里可以让缓存失效,避免地址混乱问题,具体可以看下面。
Android N混合编译与对热补丁影响解析
对于art平台,ota升级后app的boot image已经改变,也就是缓存的热代码,由于厂商只进行了ClassN的优化,所以这里进行一个全量的dex2oat的优化操作。
if (isSystemOTA) {
parallelOTAResult = true;
parallelOTAThrowable = null;
Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");
TinkerParallelDexOptimizer.optimizeAll(
legalFiles, optimizeDir,
new TinkerParallelDexOptimizer.ResultCallback() {
long start;
@Override
public void onStart(File dexFile, File optimizedDir) {
start = System.currentTimeMillis();
Log.i(TAG, "start to optimize dex:" + dexFile.getPath());
}
@Override
public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
// Do nothing.
Log.i(TAG, "success to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
}
@Override
public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
parallelOTAResult = false;
parallelOTAThrowable = thr;
Log.i(TAG, "fail to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
}
}
);
if (!parallelOTAResult) {
Log.e(TAG, "parallel oat dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, parallelOTAThrowable);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_PARALLEL_DEX_OPT_EXCEPTION);
return false;
}
}
加载resource,实际上也是加载外部的apk中的资源。根本原理也是去利用反射更改控制资源文件的类的字段的值。
决定从什么目录去加载资源文件,主要由LoaderApk中mRes来控制。
public final class LoadedApk {
private static final String TAG = "LoadedApk";
private final ActivityThread mActivityThread;
private final ApplicationInfo mApplicationInfo;
final String mPackageName;
private final String mAppDir;
private final String mResDir; //控制去什么目录加载资源文件
private final String[] mSharedLibraries;
private final String mDataDir;
private final String mLibDir;
private final File mDataDirFile;
private final ClassLoader mBaseClassLoader;
private final boolean mSecurityViolation;
private final boolean mIncludeCode;
private final DisplayAdjustments mDisplayAdjustments = new DisplayAdjustments();
Resources mResources;
private ClassLoader mClassLoader;
private Application mApplication;
private final ArrayMap> mReceivers
= new ArrayMap>();
private final ArrayMap> mUnregisteredReceivers
= new ArrayMap>();
private final ArrayMap> mServices
= new ArrayMap>();
private final ArrayMap> mUnboundServices
= new ArrayMap>();
int mClientCount = 0;
......
}
而在某些版本中,可能保存在“android.app.ActivityThread$PackageInfo”中,这里不展开讨论。
在AcitivityThread中,下面这些类分别保存着LoadApk的引用,
public final class ActivityThread {
/** @hide */
public static final String TAG = "ActivityThread";
......
// These can be accessed by multiple threads; mPackages is the lock.
// XXX For now we keep around information about all packages we have
// seen, not removing entries from this map.
// NOTE: The activity and window managers need to call in to
// ActivityThread to do things like update resource configurations,
// which means this lock gets held while the activity and window managers
// holds their own lock. Thus you MUST NEVER call back into the activity manager
// or window manager or anything that depends on them while holding this lock.
final ArrayMap> mPackages
= new ArrayMap>();
final ArrayMap> mResourcePackages
= new ArrayMap>();
final ArrayList mRelaunchingActivities
= new ArrayList();
Configuration mPendingConfiguration = null;
private final ResourcesManager mResourcesManager;
......
}
所以我们要更改res文件的加载目录,只需要去遍历mPackages和mResourcePackages中LoadApk,然后修改它们的mRes字段就可以了。
for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
Object value = field.get(currentActivityThread);
for (Map.Entry> entry
: ((Map>) value).entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (externalResourceFile != null) {
resDir.set(loadedApk, externalResourceFile);
}
}
}
所以资源的加载都是由Resouces类来控制,而关于asset目录,由Rescources类里面的mAssets来控制
public class Resources {
static final String TAG = "Resources";
......
/*package*/ final AssetManager mAssets;
......
}
AssetManager代码如下,
public final class AssetManager {
......
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
int res = addAssetPathNative(path);
return res;
}
......
}
可以看到只需要增加我们指定的path就可以了,但是由于考虑到第三方ROM,
需要将Resources中mAssets引用的指向修改掉。
// Baidu os
if (assets.getClass().getName().equals("android.content.res.BaiduAssetManager")) {
Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager");
newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance();
} else {
newAssetManager = AssetManager.class.getConstructor().newInstance();
}
重新指向mAssets
// Create a new AssetManager instance and point it to the resources installed under
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
ensureStringBlocksMethod.invoke(newAssetManager);
for (WeakReference wr : references) {
Resources resources = wr.get();
//pre-N
if (resources != null) {
// Set the AssetManager of the Resources instance to our brand new one
try {
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// N
Object resourceImpl = resourcesImplFiled.get(resources);
// for Huawei HwResourcesImpl
Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
clearPreloadTypedArrayIssue(resources);
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
so的动态加载比较简单,原理也是利用反射,得到“nativeLibraryDirectories”,最后在libDirs里面加入自定义的folder目录。
Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List libDirs = (List) nativeLibraryDirectories.get(dexPathList);
libDirs.add(0, folder);
Field systemNativeLibraryDirectories =
ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List systemLibDirs = (List) systemNativeLibraryDirectories.get(dexPathList);
Method makePathElements =
ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);
ArrayList suppressedExceptions = new ArrayList<>();
libDirs.addAll(systemLibDirs);
而在API4-13的版本,主要是获取“libraryPathElements”。
private static final class V4 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
String addPath = folder.getPath();
Field pathField = ShareReflectUtil.findField(classLoader, "libPath");
StringBuilder libPath = new StringBuilder((String) pathField.get(classLoader));
libPath.append(':').append(addPath);
pathField.set(classLoader, libPath.toString());
Field libraryPathElementsFiled = ShareReflectUtil.findField(classLoader, "libraryPathElements");
List libraryPathElements = (List) libraryPathElementsFiled.get(classLoader);
libraryPathElements.add(0, addPath);
libraryPathElementsFiled.set(classLoader, libraryPathElements);
}
}
分平台合成