热更新Tinker研究(四):TinkerLoader
来源:互联网 发布:2015网络热门词汇 编辑:程序博客网 时间:2024/05/20 13:06
热更新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
热更新Tinker研究(四):TinkerLoader
合成补丁后如何在启动后对应用进行更改呢,处理这个事情的主要类是TinkerLoader,对应dex、res、so文件分别是TinkerDexLoader,TinkerResourceLoader以及TinkerSoLoader。
一、data目录下的tinker相关文件
为了更好地控制热更新的过程,以及保存热更新后的结果内容,需要在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等。
文件示例
tinker/patch.info
该文件主要保存升级信息
#from old version:f7435e89150d55742dfdfc0d3bd52e38 to new version:f7435e89150d55742dfdfc0d3bd52e38#Thu Apr 06 14:30:35 GMT+08:00 2017print=GIONEE/GN8002/GIONEE_BBL7516A\:6.0/MRA58K/1466615086\:eng/release-keysnew=f7435e89150d55742dfdfc0d3bd52e38old=f7435e89150d55742dfdfc0d3bd52e38
tinker_temp/patch.retry
该文件保存重试信息
#Thu Apr 06 14:30:22 GMT+08:00 2017times=2md5=f7435e89150d55742dfdfc0d3bd52e38
shared_prefs/tinker_own_config_tinker.sample.android.xml
该文件的作用类似于Android的sharedpreference,用xml保存键值对。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?><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); }
二、Dex的load
tinker是一种典型的java派别的热修复做法,典型可以参考
Qzone热更新方案
原理就是去修改dexElements,将new.dex插入到前面,由于在查找类时,会顺序查找,这样就达到了热修复的目的。具体可见安卓App热补丁动态修复技术介绍。不过Tinker目前的new.dex是一个全量使用的情况,也就是直接把所有dex文件都拿过来,所以会有空间占用比较大的情况。不过这种保守的做法可以避免CLASS_ISPREVERIFIED标记校验问题以及插桩引起的内存地址混乱问题。同时在art虚拟机中,dex2oat已经将类的各个地址写死,所以采用插桩方式很可能会导致地址混乱。所以qZone方式既有性能问题,也可能导致错误。
dex的加载主要由TinkerDexLoader负责,除了利用反射加载dex以外,如果系统进行了OTA升级,还会进行dex的优化。
installDexes
这里动态加载dex的基本原理是利用反射,但是由于不同版本的PathClassLoader机制不一样,这里也需要分版本处理:
v23、v19、v14
这三个版本中核心原理都是去修改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 (API 4-13)
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) { }
AndroidNClassLoader
在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 混合编译导致补丁机制失效
Android N混合编译与对热补丁影响解析
SystemOTA的影响
对于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; } }
三、res的load
加载resource,实际上也是加载外部的apk中的资源。根本原理也是去利用反射更改控制资源文件的类的字段的值。
res目录的更改
决定从什么目录去加载资源文件,主要由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); } } }
asset目录的修改
所以资源的加载都是由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的load
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); } }
tinker优缺点分析
优点
- 开发透明; 开发者无需关心是否在补丁版本,他可以随意修改,不由框架限制;
- 性能影响较小; 对比市面上其他框架,性能影响较小。
- 完整支持; 支持代码,So 库以及资源的修复,可以发布功能。
- 补丁大小较小; 补丁大小较小,提高升级率。
- 稳定,兼容性好; 微信的数亿用户的使用。
- 可配置性高;框架的很多类可以扩展定制。
缺点
- Android N的支持不完美:不同的虚拟机都采用全量补丁,会使AndroidN的混合编译退化,使用了动态加载(实际上全量加载),会对性能有较大影响。
- patch后的空间占用大:由于使用全量补丁,合成后新的文件占用空间比较大。
tinker未来发展趋势
分平台合成
- 热更新Tinker研究(四):TinkerLoader
- 热更新Tinker研究(六):TinkerPatchPlugin
- 热更新Tinker研究(一):运行tinker-sample-android
- 热更新Tinker研究(三):加载补丁
- 热更新Tinker研究(五):Application的隔离
- 热更新Tinker研究(九):Dex文件的patch
- 热更新Tinker研究(十):Res文件的patch
- 热更新Tinker研究(十一):so文件的patch
- 热更新Tinker 的研究与集成
- Android热更新(Tinker)
- 热更新Tinker研究(二):结合源码学习Dex格式
- 热更新Tinker研究(七):Dex的patch文件生成
- 热更新Tinker研究(八):res和so的patch文件生成
- 集成tinker热更新
- Tinker热更新
- Android热更新(3)-Bugly&Tinker 热更新实战!
- Android 热更新之tinker
- 微信tinker热更新
- Django项目开发举例之用户界面表单(6)
- 【Linux】 JDK安装及配置 (tar.gz版)
- android sqlite数据库并发问题的详细描述和解决方案
- 微信多图上传,解决android多图上传失败问题
- 【Tensorflow】tf.reshape 函数
- 热更新Tinker研究(四):TinkerLoader
- 在C/C++中调用Java代码
- 接口测试-postman入门基础
- MyBatis参数传递的问题
- 解析地图api工具类
- java爬虫 抓取国家统计局:统计用区划代码和城乡划分代码(抓取省市区镇县办事处村委会数据)生成json
- linux 消息队列机制
- 静态联编与动态联编
- 封装JedisClient.提供API实现对redis的操作