android插件化(四)Hook加载插件APK(ClassLoader方式)

Hook加载插件APK(ClassLoader方式)

前言

前面插件化一和二说了下插桩式加载未安装的APK,主要是重写了getResource和getClassloader两个方法来实现的。以及每个组件要实现一个接口,通过接口注入上下文来达到它的生命周期。

那么插桩式和hook式的实现方式有什么不同呢?

插桩式是怎么加载到插件中的class文件呢,是通过将将APK转化成插件的Classloader,然后想要加载插件的class文件,我们的去拿这个插件的classloader去loadClass。所以是有一个中间者的。

hook式呢是将插件apk融入到了我们的宿主apk,那直接在里面就可以直接loadClass了,在不用这个插件的ClassLoader了,这样的话对于插件和宿主就没什么区别了,不像插桩式有一个中间者。

那么要实现hook式 就要知道android中一个class文件式怎样被加载到内存中去的。其实就是通过PathClassLoader来加载的。

那么我们先看下ClassLoader

ClassLoader

任何一个java程序都是由一个或者多个class组成的,在程序运行时,需要将class文件加载到JVM中才可以使用,负责加载这些class文件的就是java的类加载机制。CLassLoader的作用就是加载class文件提供给程序运行时使用,每个Class对象内部都有一个ClassLoader来标示自己是有那个classLoade加载的。

Android app的所有的java文件都是通过PathClassLoader来加载的,那么它的父类是BaseDexClassLoader,还有一个兄弟类是DexClassLoader,那么他们有什么区别呢。

DexClassLoader类

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
PathClassLoader类
 public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

从上面可以看出这两个类的构造函数不同。(在26的源码中DexClassLoader中的optimizedDirectory也废弃了)

PathClassLoader:用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex

DexClassLoader:加载指定的dex以及jar、zip、apk中的classes.dex。

双亲委托机制

可以看到创建ClassLoader的时候需要接收一个CLassLoader parent的参数,这个parent的目的就在于实现类加载的委托。

某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,一次递归,如果父加载器可以完成加载任务,那么就返回,只有当父加载器无法完成加载任务时,才自己去加载。


protected Class loadClass(String name,boolean resolve) throws ClassNotFoundException{
  
  //检查class是否有被加载过
  Class c=findLoadedClass(name);
  
  if(c=null){
    long t0=System.nanoTime();
    try {
      // 父类不为null 调用父类的loadClass
      if(parent!!=null){
        c=parent.loadClass(name,false);
      }{
        //如果parent为null,则调用BootClassLoader进行加载
        c=findBootstrapClassOrNull(name);
      }
    }catch{
      
    }
    
    //父类都没找到 那就自己找
    if(c=null){
     c=findClass(name);
    }
  }
  return c;
}

因此我们自己创建的ClassLoader:newPathClassLoader("/sdcard/xx.dex",getClassLoader()),并不仅仅只能加载我们的xx.dex中的class。

需要注意的是,findBootstrapClassOrNull 这个方法,当parent为null的时候,去这个BootCLassLoader进行加载,

但是在Android当中的实现:

private Class findBootstrapClassOrNull(String name){
  return null;
}

所以new PathClassLoader("/sdcard/xx.dex",null),是不能加载Activity.class的。

上面分析了加载了一个class,是利用了双亲委托机制,那么要是都找不到那就开始调用自己的findCLass方法

findClass实现机制

在ClassLoader类中findClass:

protected Class<?> findClass(String name) throws ClassNotFoundException{
  throw new ClassNotFoundException(name);
}

任何ClassLoader的子类,都可以重写loadClass和findClass。如果你不想使用双亲委托,就重写loadClas修改实现,重写findClass则表示在双亲委托机制下,父ClassLoader都找不到class的情况下,定义自己去查找一个class。

而我们的PathClassLoader会自己负责加载Activity这样的类,利用双亲委托父类去加载activity,而我们的PathClassLoader没有重写findClass,是在它的父类里面。因此我们可以看看父类的findClass是如何实现的。

public BaseDexClassLoader(String dexPath,File optimizedDirectory,
                         String librarySearchPath,ClassLoader parent){
  super(parent);
  this.pathList=new DexPathList(this,dexPath,librarySearchPath,optimizeDirectory);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
  List<Throwable> suppressedExceptions=new ArrayList<Throwable>();
  //查找指定的class
  Class c=pathList.findClass(name,suppressedExeptions);
  if(c=null){
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" +name + "\" on path: " + pathList);
      for (Throwable t : suppressedExceptions) {
                   cnfe.addSuppressed(t);
        }
      throw cnfe;
  }
  return c;
}

可以看到加载PathClassLoader加载class,转化为从DexPathList中加载class了,那么我们看看DexPathList中的findClass

public DexPathList(ClassLoader definingContext,String dexPath,
                  String librarySearchPath,File optimizedDirectory){
  
  // splitDexPath 实现为返回的List<File> .add(dexPath)
  // makeDexElements会去List<File>.add(dexPath)中使用DexFile加载Dex文件发挥Element数组
  this.dexElements=makeDexElements(splitDexPath(dexpath),optimizedDirectory,
                                  suppressedExceptions,definingContext);
}

public Class findClass(String name,List<throwable> suppressed){
  //从element中获得代表dex的dexFile
  for(Element element : dexElements){
    DexFile dex=element.dexFile;
    if(dex!=null){
       //查找class
      Class clazz=dex.loadClassBinaryName(name,defingContext,suppressed);
      if(clazz!=null){
        reruen clazz;
      }
    }
    
     if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
     }
    return null;
  }
}


那么从上面分析得到

  • 在PathClassLoader加载class,在父类的委托下没有找到CLas下,会去BaseDexClassloader找
  • 在调用父类的findClass的时候,创建了DexPathList,那我们的dex文件给传进去
  • 在DexPathList中findClass的时候从数组中查找,如果查找到了直接返回,不会再往下查找。

到这里我们想要加载一个插件的apk ,其实最终加载的是一个dex文件(先说class文件,加载资源后面说),有没有办法吧这个dex文件给转化成一个Element对象,给放到Elemeng数组当中,这样直接就可以加载我们插件中的类了。

DexElement数组合并(也就是热修复)

  1. 首先是要拿到插件的路径

    String cachePath=context.getCacheDir.getAbsolutePath();
    String apkPath=context.Environment.getExernaltorageDirectory.getAbsolutePath+"/plugin.apk";
    DexClassLoader myClassLoaderObj=new DexClassLoader(apkPath,cachePath,cachePath,context.getClassLoader);
    

1、首先我们肯定是要得到插件APK的的中DexPathList对象中的dexElement数组

Class<?> myBaseDexClassLoaderClass=Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListField=myBaseDexClassLoaderClass.getDeclaredField("pathList");
myPathListField.setAccessible(true);

//接着得到插件pathList的Obj
Object myPathListObj=myPathListField.get(myClassLoaderObj);

Class<?> myPathListCLass=myPathListObj.getClass;
Field myDexElementField=myPathListClass.getDeclaredField("dexElements");
myDexElementField.setAccessible(true);

// 然后在从myPathListObj这个对象身上去dexElement数组
Object myDexElementArray=myDexElementField.get(myPathListObj);
//现在也拿到了插件的dex数组

2、插件的dexElements数组我们拿到了,那么是不是要开始拿我们系统里面的 ,我们反射获取,和上面的一样。

// 开始获取系统的,对象就是我们的上下文的classLoader
PathClassLoader sysClassLoader = (PathClassLoader) context.getClassLoader();
Class<?> sysDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field sysPathListField = sysDexClassLoaderClass.getDeclaredField("pathList"); sysPathListField.setAccessible(true);

//拿到我们系统的pathList的对象
Object sysPathListObj = sysPathListField.get(sysClassLoader);

Class<?> sysPathListClazz = sysPathListObj.getClass();
Field sysDexElements = sysPathListClazz.getDeclaredField("dexElements");
sysDexElements.setAccessible(true);

//  然后在获取系统的Element数组
Object sysElementsArray = sysDexElements.get(sysPathListObj);

3、上面我们获取到了系统和我们插件的dexElement数组,然后我们将这个数组合并到一个新的数组里面去,并且给注入到系统里面

int myLength = Array.getLength(myDexElementArray);
int sysLength = Array.getLength(sysElementsArray);
int newLength = myLength + sysLength;// 这个就是咱们要融合的新数组的长度

// 然后我们创建一个新的数组,因为是Object类型,所以我们使用反射包下提供的工具类


// 这个拿到的就是系统dexElement数组中对象的class类型
Class<?> sysElementClazz = sysElementsArray.getClass().getComponentType();
//创建一个新的数组 但是里面要穿一个数组里面的对象的class类型
Object newElementArray = Array.newInstance(sysElementClazz, newLength);

// 新数组创建好了,那么我开始合并
for (int i = 0; i < newLength; i++) {
    if (i < myLength) {
       // 插件的数组
       Array.set(newElementArray, i, Array.get(myDexElementArray, i));
     } else {
       //系统的数组
       Array.set(newElementArray, i, Array.get(sysElementsArray, i - myLength));
     }
}

// 现在就将两个dexElement数组全部都放到我们的新数组中去了
sysDexElements.set(sysPathListObj,newElementArray);

至此,加载插件的一个流程基本就完成了。但是上面只是处理了class文件,没有处理资源。资源的话我们也是采用hook的方式去实现

String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/plugin.apk";
try {
     assetManager = AssetManager.class.newInstance();
     Method assetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
     assetPathMethod.setAccessible(true);
     assetPathMethod.invoke(assetManager, apkPath);

            //手动实例化
      Method ensureStringBlocksMethod = assetManager.getClass().getDeclaredMethod("ensureStringBlocks");
      ensureStringBlocksMethod.setAccessible(true);
      ensureStringBlocksMethod.invoke(assetManager);
      resources = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());

     } catch (Exception e) {
         e.printStackTrace();
     }

在宿主的Application中hook这个方法,然后去重写getAsserts和getResources两个方法:

 @Override
    public AssetManager getAssets() {
        return assetManager == null ? super.getAssets() : assetManager;
    }

    @Override
    public Resources getResources() {
        return resources == null ? super.getResources() : resources;
    }

然后在插件的BaseActivity中继续重写getAssets和getResources两个方法

 @Override
    public Resources getResources() {
        if (getApplication()!=null && getApplication().getResources()!=null){
          return   getApplication().getResources();
        }
        return super.getResources();
    }

    @Override
    public AssetManager getAssets() {
        if (getApplication()!=null && getApplication().getAssets()!=null){
           return getApplication().getAssets();
        }
        return super.getAssets();
    }

这样就可以完成hook式加载一个未安装的APK了。至此基本就完成了插桩式和Hook式插件化的基本实现。(后面几篇是优化)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容