怎么 classloader 加载一个java源文件

怎么 classloader 加载一个java源文件,第1张

1.类加载器深入剖析

Java虚拟机与程序的生命周期 :

当我们执行一个java程序的时候 , 会启动一个JVM进程 , 当程序执行完之后 , JVM进程就消亡了

在如下情况下JVM将结束声明周期 :

System.exit(int)方法 , 当执行这个方法的时候 , 虚拟机会退出 这个方法传入一个整形参数 , 这个参数是状态吗 : 如果这个整形是 0 的话 , 就是正常退出 , 如果不是0的话 , 就是异常退出

程序正常结束

程序执行过程中 , 遇到了异常或错误 , 而异常终止 : 如果我们在程序中出现了异常 , 而不去处理 , 会将异常一直抛给main函数 , main函数会团悉段将异常抛给JVM , JVM如果处理不了异常 , JVM就会异常退出

由于 *** 作系统出现错误导致JVM进程终止 : JVM所依赖的平台出现错误 , 导致JVM终止

2.类的加载,连接和初始化

加载 : 查找并加载类的二进制数据 , 将class字节文件加载到内存中

连接 :

-

验证

: 确保被加载的类的正确性 , 使用javac 编译工具生成的字节码文件能通过验证 , 如果不是由javac编译生成的字节码文件 , 如果自己生成的字节码文件不符合JVM虚拟机对字节码文件的要求的话 , 可能会出现验证通不过的情况 比如说随便拿一个文件 , 将后缀名直接修改为.class , 这样的字节码文件肯定不合法

-

准备

: 为类的静态变量分配内存 , 并将其初始化为默认值

-

解析

: 把类中的符号引陆轿用转为直接引用

初始化 : 为类的静态变量赋予正确的初始值(正确的值指的是用户赋的值)

-好像这个与连接阶段的准备有些重复 , 在连接的准备阶段只是赋予初始变量 , 如果用户给这塌誉个变量赋了初始值 , 那么这个变量在连接的准备阶段仍然会赋予初始值

-在这个阶段 , 才会真正的将初始值赋给静态变量

Java程序对类的使用方式有 主动使用 和 被动使用

所有的JVM实现 , 必须在每个类或者接口 , 被java程序 “首次主动使用” 时才初始化他们

主动使用 :

创建类的实例

访问某个类或接口的静态变量 , 或者对该静态变量赋值

调用类的静态方法

反射 : Class.forName(“类名”)

初始化一个类的子类 , 看做对父类的主动使用

java虚拟机启动的时候 , 被标明启动类的类 , 即包含main方法的类 , 程序的入口

除了上面6种主动使用之外 , 其它的情况均为被动使用 , 其它情况都不会执行第三步初始化

3.类的加载

(1)概念

类的加载 : 指的是将类的.class文件中的二进制数据读入到内存中 , 将其放在运行时数据区的方法区内 , 然后再堆区创建一个java.lang.Class对象 , 用来封装类在方法区内的数据结构

反射 : 反射就是跟句堆区的字节码文件 , 获取方法去的数据结构

解析 : Class对象是由JVM自己创建的 , 所有的对象都是经过Class对象创建 , 这个Class对象是反射的入口, 通过Class对象 , 可以关联到目标class字节码文件的内部结构

所有的类对应的Class对象都是唯一的一个 , 这个类是由JVM进行创建的 , 并且只有JVM才会创建Class对象

类加载的最终产品是位于堆区中的Class对象 , Class对象封装了类在方法区内的数据结构 , 并且向Java程序员提供了访问方法区内的数据结构的接口(反射用的接口)

(2)加载.class文件的方式

从本地系统中直接加载 : 编译好的.class字节码文件直接从硬盘中加载

通过网络下载.class文件 : 将class字节码文件放在网络空间中 , 使用URLClassLoader来加载在网络上的.class字节码文件 , 使用默认的父亲委托机制加载字节码文件

从zip , jar 等压缩文件中加载字节码文件 : 在开发的时候 , 导入jar包 , 就是这种方式

从专有的数据库中提取字节码文件

将java源文件动态编译为字节码文件

(3)类加载器

l Java虚拟机自带的类加载器 :

-根类加载器 ( Bootstrap ) : 是C++写的 , 程序员无法再java代码中获取这个类 , 如果使用getClassLoader()方法获取到的是一个null值

package jvm

Java代码

public class ClassLoaderTest {

public static void main(String[] args) throws Exception {

//java.lang包下的类使用的是跟类加载器进行加载的

Class clazz = Class.forName("java.lang.String")

System.out.println(clazz.getClassLoader())

//自定义的类使用的是应用类加载器(系统加载器)

Class clazz2 = Class.forName("jvm.C")

System.out.println(clazz2.getClassLoader())

}

}

class C{}

执行结果 :

null

Java代码

sun.misc.Launcher$AppClassLoader@1372a1a

-扩展类加载器 ( Extension ) : Java编写

-系统类加载器(应用加载器) ( System ) : Java编写

用户自定义的类加载器 :

-自定义的类加载器都是java.lang.ClassLoader子类

-用户可以定制类的加载方式

String类是由根类加载器进行加载的 , 我们可以调用Class对象的

关于代理中创建对象的类加载器 : 创建代理对象的时候 , 动态创建一个类 , 然后使用指定的类加载器将这个类加载到内存中 , 然后用加载到内存中的类生成代理对象

创建代理对象的方法 : newProxyInstance(ClassLoader loader , Class [] Interfaces , InvocationHandler h )

loader 是定义的代理类的类加载器 , 中间的接口数组是代理类的要实现的接口列表 , h 是指派方法调用的调用处理程序

类加载器并不需要在某个类被 “首次主动使用” 时再加载它 :

-预加载机制 : JVM规范允许类加载器在预料某个类将要被使用的时就预先加载它

-报错时机 : 如果在预加载的过程中遇到了字节码文件缺失或者存在错误的情况 , 类加载器会在程序首次主动使用(上面提到的六种情况)该类的时候报错(LinkageError错误)

-不报错时机 : 如果这个错误的字节码文件所对应的类一直没有被使用 , 那么类加载器就不会报告错误 ,即便有错误也不会报错

LinkageError : 这个错误是Error的子类 , 程序不能处理这些错误 , 这些错误都是由虚拟机来处理 , 这个错误表示出错的是子类 , 在一定程序上依赖于另一个类 , 在编译了前面一个类的时候 , 与后面所依赖的类出现了不兼容的情况

例如 : 我们使用了jdk 1.6 在编译一个程序 , 但是运行环境是jre1.5的 , 就会出现LinkageError错误

4.类的连接

(1)定义

类被加载之后 , 就进入链接阶段 链接 : 将已读入内存的二进制数据合并到虚拟机的运行时环境中去

链接顾名思义就是讲类与类之间进行关联 , 例如我们在类A中调用了类B , 在链接过程中 , 就将A与B进行链接 ,将面向对象语言转化为面向过程语言

(2)类的验证

类文件的结构检查 : 确保类文件遵从java类文件的固定格式 , 开始类的描述 , 声明 , 方法调用格式等

语义检查 : 确保类本身符合java语言的语法规定 , 比如final类型的类没有子类 , final类型的方法没有被覆盖 ,在eclipse中这种错误编译的时候不能通过 , 但是通过其他的方法可以生成错误的字节码文件 , 这里就是检测恶意生成的字节码文件

字节码验证 : 确保字节码流可以被JVM安全的执行 , 字节码流代表java方法(包括静态方法和实例方法) , 它是由被称作 *** 作码的单字节指令组成的序列 , 每一个 *** 作码后面跟着一个或多个 *** 作数 , 字节码验证步骤会检查每个 *** 作码是否合法 , 即是否有着合法的 *** 作数

下面是指令码组成的序列 , 类似于微指令 :

Jvm编译指令代码代码

// Compiled from ByteToCharCp1122.java (version 1.5 : 49.0, super bit)

public class sun.io.ByteToCharCp1122 extends sun.io.ByteToCharSingleByte {

// Field descriptor #17 Lsun/nio/cs/ext/IBM1122

private static final sun.nio.cs.ext.IBM1122 nioCoder

// Method descriptor #18 ()Ljava/lang/String

// Stack: 1, Locals: 1

public java.lang.String getCharacterEncoding()

0 ldc <String "Cp1122">[1]

2 areturn

Line numbers:

[pc: 0, line: 25]

// Method descriptor #2 ()V

// Stack: 2, Locals: 1

public ByteToCharCp1122()

0 aload_0 [this]

1 invokespecial sun.io.ByteToCharSingleByte() [25]

4 aload_0 [this]

5 getstatic sun.io.ByteToCharCp1122.nioCoder : sun.nio.cs.ext.IBM1122 [23]

8 invokevirtual sun.nio.cs.ext.IBM1122.getDecoderSingleByteMappings() : java.lang.String [27]

11 putfield sun.io.ByteToCharSingleByte.byteToCharTable : java.lang.String [24]

14 return

Line numbers:

[pc: 0, line: 28]

[pc: 4, line: 29]

[pc: 14, line: 30]

// Method descriptor #2 ()V

// Stack: 2, Locals: 0

static {}

0 new sun.nio.cs.ext.IBM1122 [15]

3 dup

4 invokespecial sun.nio.cs.ext.IBM1122() [26]

7 putstatic sun.io.ByteToCharCp1122.nioCoder : sun.nio.cs.ext.IBM1122 [23]

10 return

Line numbers:

[pc: 0, line: 22]

}

l 二进制兼容性的验证 : 确保相互引用的类之间协调一致的 例如在A类的a()方法中调用B类的b()方法 , JVM在验证A类的时候 , 会验证B类的b()方法 , 加入b()方法不存在 , 或者版本不兼容(A,B两类使用不同的JDK版本编译) , 会抛出NoSuchMethodError错误

(3)准备阶段

在准备阶段 , JVM为类的静态变量分配内存空间 , 并设置默认的初始值 . 例如下面的Sample类 , 在准备阶段 ,为int类型的静态变量分配4个字节 , 并赋予初始值 0 为long 类型的静态变量 b , 分配8个字节 , 并赋予初始值 0

PS : 在java中基本类型变量占用的空间是一定的 , java运行在JVM上的 , 在C中 , 就要根据平台变化而变化了

public class Sample {

Java代码

private static int a = 1

private static long b

static {

b = 2

}

(4)类的解析

在解析阶段 , JVM 会把类的二进制数据中的符号引用替换为直接引用 , 例如在A类中的a()方法引用B类中的b()方法

在A类的二进制数据中包含了一个对B类的b()方法的符号引用 , 这个符号引用由b()方法的全名和相关的描述符组成 , 在Java解析阶段 , 就会把这个符号引用替换为指针 , 这个指针就是C语言中的指针了 , 该指针指向B类的b()方法在方法区中的内存位置 , 这个指针就是直接引用

5.类的初始化

在初始化阶段 , Java虚拟机执行类的初始化 *** 作 , 为类的静态变量赋予初始值 , 在程序中 , 静态变量初始化有两种途径 :

直接在声明处进行初始化 , 例如下面的Sample中的 变量a

在静态代码块中进行初始化 , 例如下面的Sample中的变量b

Java代码

public class Sample {

private static int a = 1

private static long b

static {

b = 2

}

}

6.面试题介绍

Java代码

public class PrepareOrInit {

public static void main(String[] args) {

Singleton singleton = Singleton.getInstance()

System.out.println(singleton.count1)

System.out.println(singleton.count2)

}

}

class Singleton{

private static Singleton singleton = new Singleton()

public static int count1

public static int count2 = 0

private Singleton(){

count1 ++

count2 ++

}

public static Singleton getInstance(){

return singleton

}

}

执行结果 : 10

分析 : 这段代码与类的链接中的准备阶段 和 初始化阶段 有关系 , 准备阶段是给静态的字段赋予默认值 , 初始化阶段给静态变量赋予正确的值 , 即用户的值

在主函数中 , 调用了类的静态方法 , 相当于主动使用 , 这里调用了类的静态方法

之后进行连接的准备 *** 作 , 给类中的静态变量赋予初值 , singleton值为null , count1 与 count2 值为0

执行初始化 *** 作 , 给类中的静态变量赋予正确的值 , 给singleton变量赋予正确的值 , 调用构造方法 , 此时count1与 count2执行自增 *** 作 , 两者都变成1 , 然后执行count1的赋予正确值 *** 作 , 这里用户没有赋值 *** 作 , count2 用户进行了 赋值为0的 *** 作 , 0将原来的1覆盖掉了 , 因此结果为 1 , 0

Java代码

public class PrepareOrInit {

public static void main(String[] args) {

Singleton singleton = Singleton.getInstance()

System.out.println(singleton.count1)

System.out.println(singleton.count2)

}

}

class Singleton{

public static int count1

public static int count2 = 0

private static Singleton singleton = new Singleton()

private Singleton(){

count1 ++

count2 ++

}

public static Singleton getInstance(){

return singleton

}

}

执行结果 : 1 1

在准备阶段count1 和 count2 都赋值为0 , 然后在初始化阶段 , 全部赋值为1

问 如果我把我的class文件加密 在运行时用指定的类加载器(class loader)装入并解密它 这样子能防止被反编译吗? 答 防止JAVA字节码反编译这个问题在java语言雏形期就有了 尽管市面上存在一些反编译的工具可以利用 但是JAVA程序员还是不断的努力寻找新的更有效的方法来保护他们的智慧结晶 在此 我将详细给大家解释这一直来在论坛上有争议的话题 Class文件能被很轻松的重构生成JAVA源文件与最初JAVA字节码的设计目的和商业交易有紧密地联系 另外 JAVA字节码被设计成简洁 平台独立性 网络灵活性 并且易于被字节码解释器和JIT (just in time)/HotSpot 编译器所分析 可以清楚地了解程序员的目的 Class文件要比JAVA源文件更易于分析 如配差指果不能阻止被反编译的话 至少可以通过一些方法来增加它的困难性 例如: 在庆绝一个分步编译里 你可以打乱Class文件的数据以使其难读或者难以被反编译成正确的JAVA源文件 前者可以采用极端函数重载 后者用 *** 作控制流建立控制结构使其难以恢复正常次序 有更多成功的商业困惑者采用这些或其他的技术来保护自己的代码 不幸的是 哪种方法都必须改变JVM运行的代码 并且许多用户害怕这种转化会给他们的程序带来新的Bug 而且 方法和字段重命名会调用反射从而使程序停止工作 改变类和包的名字会破坏其他的JAVA APIS(JNDI URL providers etc) 除了改变名字 如果字节码偏移量和源代码行数之间的关系改变了 在恢复这有异常的堆栈将很困难 于是就有了一些打乱JAVA源代码的选项 但是这将从本质上导致一系列问题的产生 加密而不打乱 或许上述可能会使你问 假如我把字节码加密而不是处理字节码 并且JVM运行时自动将它解密并装入类加载器 然后JVM运行解密后的字节码文件 这样就不会被反编译了对吗?考虑到你是第一个提出这种想法的并且它又能正常运行 我表示遗憾和不幸 这种想法是错误的 下面是一个简单的类编码器 为了阐明这种思想 我采用了一个实例和一个很通用的类加载器来运行它 该程序包括两个类 public class Main{public static void main (final String [] args){  System out println ( secret result = + MySecretClass mySecretAlgorithm ())}} // End of classpackage deimport java util Randompublic class MySecretClass{/** * Guess what the secret algorithm just uses a random number generator */public static int mySecretAlgorithm (){return (int) s_random nextInt ()}private static final Random s_random = new Random (System currentTimeMillis ())} // End of class我想通过加密相关的培配class文件并在运行期解密来隐藏de MySecretClass的执行 用下面这个工具可以达到效果(你可以到这里下载Resources) public class EncryptedClassLoader extends URLClassLoader{public static void main (final String [] args)throws Exception{if ( run equals (args [ ]) &&(args length >=  )){// Create a custom loader that will use the current loader as// delegation parent:final ClassLoader appLoader =new EncryptedClassLoader (EncryptedClassLoader class getClassLoader () new File (args [ ]))// Thread context loader must be adjusted as well:Thread currentThread () setContextClassLoader (appLoader)final Class app = appLoader loadClass (args [ ])final Method appmain = app getMethod ( main new Class [] {String [] class})final String [] appargs = new String [args length ]System arraycopy (args appargs appargs length)appmain invoke (null new Object [] {appargs})}else if ( encrypt equals (args [ ]) &&(args length >= )){ encrypt specified classes }elsethrow new IllegalArgumentException (USAGE)}/** * Overrides java lang ClassLoader loadClass() to change the usual parent child * delegation rules just enough to be able to snatch application classes * from under system classloader s nose */public Class loadClass (final String name final boolean resolve)throws ClassNotFoundException{if (TRACE) System out println ( loadClass ( + name + + resolve + ) )Class c = null// First check if this class has already been defined by this classloader// instance:c = findLoadedClass (name)if (c == null){Class parentsVersion = nulltry{// This is slightly unorthodox: do a trial load via the// parent loader and note whether the parent delegated or not// what this acplishes is proper delegation for all core// and extension classes without my having to filter on class name: parentsVersion = getParent () loadClass (name)if (parentsVersion getClassLoader () != getParent ())c = parentsVersion}catch (ClassNotFoundException ignore) {}catch (ClassFormatError ignore) {}if (c == null){try{// OK either c was loaded by the system (not the bootstrap// or extension) loader (in which case I want to ignore that// definition) or the parent failed altogethereither way I// attempt to define my own version:c = findClass (name)}catch (ClassNotFoundException ignore){// If that failed fall back on the parent s version// [which could be null at this point]:c = parentsVersion}}}if (c == null)throw new ClassNotFoundException (name)if (resolve)resolveClass (c)return c}/** * Overrides java new URLClassLoader defineClass() to be able to call * crypt() before defining a class */protected Class findClass (final String name)throws ClassNotFoundException{if (TRACE) System out println ( findClass ( + name + ) )// class files are not guaranteed to be loadable as resources// but if Sun s code does it so perhaps can mine final String classResource = name replace ( / ) + class final URL classURL = getResource (classResource)if (classURL == null)throw new ClassNotFoundException (name)else{InputStream in = nulltry{in = classURL openStream ()final byte [] classBytes = readFully (in)lishixinzhi/Article/program/Java/hx/201311/25555

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

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

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

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

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

那么我们先看下ClassLoader

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

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

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

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

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

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

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

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

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

但是在Android当中的实现:

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

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

在ClassLoader类中findClass:

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

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

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

那么从上面分析得到

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

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

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

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

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

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

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

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


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/tougao/12234863.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-22
下一篇 2023-05-22

发表评论

登录后才能评论

评论列表(0条)

保存