xposed实现插件代码更新同时避免重启系统方案

话说,刚开始接触xposed,就对这个频繁重启不太爽。我很少使用动态调试的方式去跟踪apk,因为处理反调试,开启调试环境(重编、或者开启系统debug开关等)特别麻烦。但是纯粹使用xposed拦截的方式去跟踪业务逻辑呢,修改一个日志打印都需要重启Android系统。所以我就想,能不能做到更新插件代码,不重启Android系统,也让系统生效呢?然后我去研究了xposed源码,和Android相关源码,算是得到了一个比较好的方案。

 

目前热加载在我的工具包里面使用非常频繁了,我们大量的破解工作都是基于这个热加载框架实现。https://gitee.com/virjar/xposedhooktool

 

下面讲述我如何找到热加载方案,以及如何实现的。

1. xposed加载插件的逻辑分析。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/**

     * Load a module from an APK by calling the init(String) method for all classes defined

     * in assets/xposed_init.摘抄自xposed源码,其中apk为插件apk路径,如 /data/app/com.virjar.xposedhooktool-1/base.apk

     */

    private static void loadModule(String apk, ClassLoader topClassLoader) {

        Log.i(TAG, "Loading modules from " + apk);

        DexFile dexFile;

        try {

            dexFile = new DexFile(apk);

        catch (IOException e) {

            Log.e(TAG, "  Cannot load module", e);

            return;

        }

 

    //....省略代码

        ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);

//....省略代码

                        if (moduleInstance instanceof IXposedHookLoadPackage)

                            XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));

//....省略代码

    }

xposed在系统启动的时候,就分析了各个xposed模块。然后将其注入到了受精卵进程里面,使用的是PathClassloader。PathClassLoader和我们写的xposed模块对应的apk文件关联。这样任何一个apk执行,其vm环境都存在xposed模块的代码

2. 当宿主app启动的时候发生了什么

当我们注入的宿主apk启动的时候,由于进程是重受精卵fork而来,所以vm的环境中,已经存在了模块代码,代码和我们的插件apk地址关联。但是这个关联有不确定性,由于系统启动的时候,已经创建了classLoader。系统启动后,我们刷新xposed模块的apk,宿主apk里面的classLoader,会加载最新的xposed模块的apk么?答案是不会的,他只会使用第一次关联的那个xposed模块apk。这就是为啥我们更新xposed模块代码之后,必须重启才能生效的根本原因。

3. 为啥pathclassloader不会加载最新的apk。

xposed实现插件代码更新同时避免重启系统方案_第1张图片

上图是Android源码中,关于DexFile加载的描述。当一个apk对应的dex文件被打开时,将会使用dexopt,将dex进行优化,变成odex文件,并且存放到/data/dalvik-cache中。然后真正打开的文件,永远是dalvik-cache里面的odex文件。

4. 如果一个apk被占用(其他进程打开了),这个时候重装apk。将会发生么?

1

2

3

/data/app/com.virjar.xposedhooktool-1/base.apk

/data/app/com.virjar.xposedhooktool-2/base.apk

/data/app/com.virjar.xposedhooktool/base.apk

覆盖安装一个apk,如果原来的apk正在使用中。那么将会使用新的apk路径存放,也就是说,安装路径将会和第一次的安装路径不一样。上面的案例是在小米note上面的实验。

5. 如何修复这个关联关系,让xposed框架加载最新的插件代码

这里我们看到,可能的方案,就是删除dalvik-cache里面的缓存,这样Android系统就会重新生成这个缓存文件。重启宿主,就会使用最新的apk代码?这个思路其实有问题。

1. xposed模块代码在受精卵的时候注入,这个时候,文件已经打开。我们知道,如果一个进程在删除文件之前打开那个文件,那么进程仍然持有被删除的文件的fd,只是删除后,其他进程再次打开文件,将会找不到删除掉的文件。

2. 覆盖安装模式,apk不是安装在第一次安装的路径。在上一点我们提到,apk安装路径可能由/data/app/com.virjar.xposedhooktool-1/base.apk变成了/data/app/com.virjar.xposedhooktool-2/base.apk,这个时候xposed变量里面存储的还是老的地址。所以也是打开不了新的apk的。

 

所以,如果我们能够计算出最新的apk安装地址,然后使用一个新的classLoader关联新的apk,然后通过这个classLoader里面的class去注入代码。那么是不是就可以永远使用最新的apk,避免重启Android系统了呢?

6.实现代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

package com.virjar.xposedhooktool.hotload;

 

import android.annotation.SuppressLint;

import android.app.Application;

import android.content.Context;

import android.content.pm.PackageInfo;

import android.content.pm.PackageManager;

import android.util.Log;

 

import com.google.common.collect.Maps;

 

import org.apache.commons.io.IOUtils;

import org.apache.commons.lang3.StringUtils;

import org.xmlpull.v1.XmlPullParser;

 

import java.io.File;

import java.io.InputStream;

import java.util.concurrent.ConcurrentMap;

import java.util.zip.ZipFile;

 

import brut.androlib.res.decoder.AXmlResourceParser;

import dalvik.system.PathClassLoader;

import de.robv.android.xposed.IXposedHookLoadPackage;

import de.robv.android.xposed.XC_MethodHook;

import de.robv.android.xposed.XposedBridge;

import de.robv.android.xposed.XposedHelpers;

import de.robv.android.xposed.callbacks.XC_LoadPackage;

 

/**

 * XposedInit

 

 * 请注意,该类是热加载入口,不允许直接访问工程其他代码,只要访问过的类,都不能实现热加载

 *

 * @author [email protected]

 */

public class XposedInit implements IXposedHookLoadPackage {

 

    @Override

    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {

        XposedHelpers.findAndHookMethod(Application.class"attach", Context.classnew XC_MethodHook() {

 

            //由于集成了脱壳功能,所以必须选择before了

            @Override

            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

                hotLoadPlugin(lpparam.classLoader, (Context) param.args[0], lpparam);

            }

        });

    }

 

    public static String packageName(ClassLoader classLoader) {

        Object element = bindApkLocation(classLoader);

        if (element == null) {

            return null;

        }

        //原文件可能已被删除,直接打开文件无法得到句柄,所以只能去获取持有删除文件句柄对象

        ZipFile zipFile = (ZipFile) XposedHelpers.getObjectField(element, "zipFile");

        return findPackageName(zipFile);

    }

 

    private static ClassLoader replaceClassloader(Context context, XC_LoadPackage.LoadPackageParam lpparam) {

        ClassLoader classLoader = XposedInit.class.getClassLoader();

        if (!(classLoader instanceof PathClassLoader)) {

            XposedBridge.log("classloader is not PathClassLoader: " + classLoader.toString());

            return classLoader;

        }

 

        //find the apk location installed in android system,this file maybe a dex cache mapping(do not the real installed apk)

        Object element = bindApkLocation(classLoader);

        if (element == null) {

            return classLoader;

        }

        File apkLocation = (File) XposedHelpers.getObjectField(element, "zip");

        //原文件可能已被删除,直接打开文件无法得到句柄,所以只能去获取持有删除文件句柄对象

        ZipFile zipFile = (ZipFile) XposedHelpers.getObjectField(element, "zipFile");

        if (zipFile == null && apkLocation.exists()) {

            try {

                zipFile = new ZipFile(apkLocation);

            catch (Exception e) {

                //ignore

            }

        }

//        if (zipFile == null) {

//            return classLoader;

//        }

        String packageName = findPackageName(zipFile);

        if (StringUtils.isBlank(packageName)) {

//            XposedBridge.log("can not find package name  for this apk ");

//            return classLoader;

            //先暂时这么写,为啥有问题后面排查

            packageName = "com.virjar.xposedhooktool";

        }

 

        //find real apk location by package name

        PackageManager packageManager = context.getPackageManager();

        if (packageManager == null) {

            XposedBridge.log("can not find packageManager");

            return classLoader;

        }

 

        PackageInfo packageInfo = null;

        try {

            packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA);

        catch (PackageManager.NameNotFoundException e) {

            //ignore

        }

        if (packageInfo == null) {

            XposedBridge.log("can not find plugin install location for plugin: " + packageName);

            return classLoader;

        }

 

        //check if apk file has relocated,apk location maybe change if xposed plugin is reinstalled(system did not reboot)

        //xposed 插件安装后不能立即生效(需要重启Android系统)的本质原因是这两个文件不equal

 

        //hotClassLoader can load apk class && classLoader.getParent() can load xposed framework and android framework

        //使用parent是为了绕过缓存,也就是不走系统启动的时候链接的插件apk,但是xposed框架在这个classloader里面持有,所以集成

 

        return createClassLoader(classLoader.getParent(), packageInfo);

    }

 

    @SuppressLint("PrivateApi")

    private void hotLoadPlugin(ClassLoader ownerClassLoader, Context context, XC_LoadPackage.LoadPackageParam lpparam) {

        boolean hasInstantRun = true;

        try {

            XposedInit.class.getClassLoader().loadClass(INSTANT_RUN_CLASS);

        catch (ClassNotFoundException e) {

            //正常情况应该报错才对

            hasInstantRun = false;

        }

        if (hasInstantRun) {

            Log.e("weijia""  Cannot load module, please disable \"Instant Run\" in Android Studio.");

            return;

        }

 

        ClassLoader hotClassLoader = replaceClassloader(context, lpparam);

//        if (hotClassLoader == XposedInit.class.getClassLoader()) {

//            //这证明不需要实现代码替换,或者热加载框架作用失效

//            //XposedBridge.log("热加载未生效");

//        }

        // check  Instant Run, 热加载启动后,需要重新检查Instant Run

        hasInstantRun = true;

        try {

            hotClassLoader.loadClass(INSTANT_RUN_CLASS);

        catch (ClassNotFoundException e) {

            //正常情况应该报错才对

            hasInstantRun = false;

        }

        if (hasInstantRun) {

            Log.e("weijia""  Cannot load module, please disable \"Instant Run\" in Android Studio.");

            return;

        }

 

        try {

            Class aClass = hotClassLoader.loadClass("com.virjar.xposedhooktool.hotload.HotLoadPackageEntry");

            Log.i("weijia""invoke hot load entry");

            aClass

                    .getMethod("entry", ClassLoader.class, ClassLoader.class, Context.class, XC_LoadPackage.LoadPackageParam.class)

                    .invoke(null, ownerClassLoader, hotClassLoader, context, lpparam);

        catch (Exception e) {

            if (e instanceof ClassNotFoundException) {

                InputStream inputStream = hotClassLoader.getResourceAsStream("assets/hotload_entry.txt");

                if (inputStream == null) {

                    XposedBridge.log("do you not disable Instant Runt for Android studio?");

                else {

                    IOUtils.closeQuietly(inputStream);

                }

            }

            XposedBridge.log(e);

        }

    }

 

    private static final String INSTANT_RUN_CLASS = "com.android.tools.fd.runtime.BootstrapApplication";

    private static ConcurrentMap classLoaderCache = Maps.newConcurrentMap();

 

    /**

     * 这样做的目的是保证classloader单例,因为宿主存在多个dex的时候,或者有壳的宿主在解密代码之后,存在多次context的创建,当然xposed本身也存在多次IXposedHookLoadPackage的回调

     *

     * @param parent      父classloader

     * @param packageInfo 插件自己的包信息

     * @return 根据插件apk创建的classloader

     */

    private static PathClassLoader createClassLoader(ClassLoader parent, PackageInfo packageInfo) {

        if (classLoaderCache.containsKey(packageInfo.applicationInfo.sourceDir)) {

            return classLoaderCache.get(packageInfo.applicationInfo.sourceDir);

        }

        synchronized (XposedInit.class) {

            if (classLoaderCache.containsKey(packageInfo.applicationInfo.sourceDir)) {

                return classLoaderCache.get(packageInfo.applicationInfo.sourceDir);

            }

            XposedBridge.log("create a new classloader for plugin with new apk path: " + packageInfo.applicationInfo.sourceDir);

            PathClassLoader hotClassLoader = new PathClassLoader(packageInfo.applicationInfo.sourceDir, parent);

            classLoaderCache.putIfAbsent(packageInfo.applicationInfo.sourceDir, hotClassLoader);

            return hotClassLoader;

        }

    }

 

 

    /**

     * File name in an APK for the Android manifest.

     */

    private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";

 

    private static Object bindApkLocation(ClassLoader pathClassLoader) {

        // 不能使用getResourceAsStream,这是因为classloader双亲委派的影响

//        InputStream stream = pathClassLoader.getResourceAsStream(ANDROID_MANIFEST_FILENAME);

//        if (stream == null) {

//            XposedBridge.log("can not find AndroidManifest.xml in classloader");

//            return null;

//        }

 

        // we can`t call package parser in android inner api,parse logic implemented with native code

        //this object is dalvik.system.DexPathList,android inner api

        Object pathList = XposedHelpers.getObjectField(pathClassLoader, "pathList");

        if (pathList == null) {

            XposedBridge.log("can not find pathList in pathClassLoader");

            return null;

        }

 

        //this object is  dalvik.system.DexPathList.Element[]

        Object[] dexElements = (Object[]) XposedHelpers.getObjectField(pathList, "dexElements");

        if (dexElements == null || dexElements.length == 0) {

            XposedBridge.log("can not find dexElements in pathList");

            return null;

        }

 

        return dexElements[0];

        // Object dexElement = dexElements[0];

 

        // /data/app/com.virjar.xposedhooktool/base.apk

        // /data/app/com.virjar.xposedhooktool-1/base.apk

        // /data/app/com.virjar.xposedhooktool-2/base.apk

        // return (File) XposedHelpers.getObjectField(dexElement, "zip");

    }

 

    private static String findPackageName(ZipFile zipFile) {

        if (zipFile == null) {

            return null;

        }

        InputStream stream = null;

        try {

            stream = zipFile.getInputStream(zipFile.getEntry(ANDROID_MANIFEST_FILENAME));

            AXmlResourceParser xpp = new AXmlResourceParser(stream);

            int eventType;

            //migrated form ApkTool

            while ((eventType = xpp.next()) > -1) {

                if (XmlPullParser.END_DOCUMENT == eventType) {

                    return null;

                else if (XmlPullParser.START_TAG == eventType && "manifest".equalsIgnoreCase(xpp.getName())) {

                    // read  for package:

                    for (int i = 0; i < xpp.getAttributeCount(); i++) {

                        if (StringUtils.equalsIgnoreCase(xpp.getAttributeName(i), "package")) {

                            return xpp.getAttributeValue(i);

                        }

                    }

                }

            }

            return null;

        catch (Exception e) {

            XposedBridge.log(e);

            return null;

        finally {

            //不能关闭zipFile

            IOUtils.closeQuietly(stream);

        }

    }

}

7.原理描述

我使用一个永远不变的class,作为加载器。这个Class的功能就是寻找最新的apk安装路径,然后构造新的classLoader,然后调用hook入口。

其中apk安装路径计算,使用PackageManager,这个也就是xposed-installer里面的逻辑。

需要注意的是,classLoader有双亲委派机制,如果我们的classLoader使用xposed创建的classLoader作为parent的话,加载的class都会以super为主。我们的新代码将不会生效(因为虚拟机里面,父classLoader能够加载的类,不允许子classLoader来加载)

而且,热加载入口,不能直接调用业务入口,因为classLoader有一个隐式加载Class的过程,会使用当然Class的classLoader加载将要访问的class。当前classLoader加载的class,依然是老的apk里面的代码。

所以通过新的classLoader显示加载业务入口,然后使用反射调用他。

8.实例代码地址:

https://gitee.com/virjar/xposedhooktool/blob/master/app/src/main/java/com/virjar/xposedhooktool/hotload/XposedInit.java

你可能感兴趣的:(Android)