Android 动态加载初探

引言

一直对安全方面挺感兴趣的,以前拜读过《白帽子讲web安全》,现在做移动端Android测试,在一次关于热埋点的学习中接触到了Xposed,之后也会关注一点Android 安全方面的东西,最近学习了一点Android动态加载的机制,跟大家分享一下

一点资源

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://blog.csdn.net/jiangwei0910410003/article/details/48415225
http://blog.csdn.net/itfootball/article/details/49806269
http://blog.csdn.net/androidsecurity/article/details/8664778
http://blog.csdn.net/jiangwei0910410003/article/details/17679823
还有一些工具: https://mp.weixin.qq.com/s?__biz=MjM5NjA0NjgyMA==&mid=2651061026&idx=1&sn=ae6aea9af3b082ea543e1051728bd0bc&scene=1&srcid=0530rqezgS1iLqK0W8tT8909&key=f5c31ae61525f82ea7ae094c2f43f3300c4f648faee145a20953616868278e738c3c425d7098c2cdfab164fbfe02339a&ascene=0&uin=MjQwNzI1NDc1&devicetype=iMac+MacBookPro12%2C1+OSX+OSX+10.10.5+build14F1605)&version=11020201&pass_ticket=ytjLePanA95PuTtMOQpYePApF6e0GJEgj%2BtLjnU6Q4Y%3D(

一点概念

  • java程序是跑在虚拟机中的,原生java虚拟机叫JVM(当然也分很多厂家,很多种类),Android也是java程序,但Android的虚拟机,叫Dalvik,Android 5.0 之后,舍弃了Dalvik,用了ART(Android Runtime)
  • 一般来说,虚拟机使用java类的方法如下:java源程序(.java文件)在经过java编译器编译之后,就被转换成java字节码(JVM对应.class文件,Dalvik对应.dex文件),类加载器负责读取java字节码,并转换成java.lang.Class的一个实例,每个这样的实例用来表示一个java类
  • 动态加载:在运行时动态加载或者重载类

类加载器

上面说到了类加载器,其实动态加载机制的核心就是类加载器的使用,在java里它是一种机制—-反射(Reflection)
类加载器的基本职责,就是根据一个指定的类的名称,找到或者生成其对应的字节码,然后从这些字节码中定义出一个java类,即java.lang.Class的一个实例
说到类加载器,或者反射机制,涉及到下面三个类:

  • java.lang.Class
  • java.lang.ClassLoader
  • java.lang.reflect

应用

在常见的java项目(J2SE,J2EE)中,特别是跨项目合作的场景,经常会把B方的业务代码编译后的class文件打成一个jar包,A在需要时动态加载jar包里的类。又或者要实现配置型的项目,会把需要加载的类(通常是一个接口)的名字写在一个配置文件中,自己程序里写关于这个接口的各种实现,运用动态加载机制,用父类(那个接口)引用指向子类(各种实现)对象,利用面向对象中的多态,来最大程度的实现可配置

但是

在Android里并不能用常规java项目中那种自定义ClassLoader,重写ClassLoader里各种方法的方式来实现动态加载,主要原因有以下两点:

  1. Android中ClassLoader的defineClass方法具体是调用VMClassLoader的defineClass本地静态方法。而这个本地方法除了抛出一个“UnsupportedOperationException”之外,什么都没做,甚至连返回值都为空
  2. 在Android中,一个类不仅仅是一个类,为啥?比如Android里面的组件之一的Activity,它在Android整体框架下,是有各种生命周期,内置了各种回调、监听器的,但是动态加载只能根据类名找到或者生成对应的字节码,然后从字节码中定义一个java类,一个普普通通的java类,即加载出来它就不再是什么Activity,Service了

肿么办

在Android里,我们不是自己重写ClassLoader,而是用下面这两种ClassLoader来实现动态加载,他们也都是ClassLoader的子类

  1. DexClassLoader(提供了一个释放.dex文件的路径)
  2. PathClassLoader(默认,提供完整路径后会把’.’替换成’/’)

Demo

关于动态加载,google一下就会有挺多例子的,那我们就先实现一个最基本的例子,了解一下动态加载它到底是怎么一回事儿,它可以用来做什么
原材料准备:

  • Eclipse
  • Android Studio
  • Android SDK

首先,在Eclipse里建一个项目,项目结构长这样:

我新建了一个Android 5的lib,里面放的是Android SDK里api level=22的android.jar,各个版本大家可以到自己Android SDK路径下的platforms里面找
项目里有一个package,com.fenfenzhong.interfaces,这是沿用了普通java项目的做法,即先定义接口,到时候可以根据这个接口写各种不同的实现类,充分利用多态
还有另外一个package,com.fenfenzhong.impl,这个是专门放实现类

com.fenfenzhong.interfaces.IDynamic

package com.fenfenzhong.interfaces;
import android.app.Activity;

public interface IDynamic {

public void init(Activity activity);

public void clickMe();

}

com.fenfenzhong.impl.Dynamic

package com.fenfenzhong.impl;
import android.app.Activity;
import android.widget.Toast;
import com.fenfenzhong.interfaces.IDynamic;

//动态类的实现

public class Dynamic implements IDynamic {
private Activity mActivity;

@Override
public void init(Activity activity) {
mActivity = activity;

}

@Override
public void clickMe() {
Toast.makeText(mActivity, "hello dynamic", 1500).show();
}


}

将上面的com.fenfenzhong.impl.Dynamic 这个动态类打包,一般jar包里放的是字节码,是编译好的.class文件,但那是为了不暴露源码,这里我们直接打包源文件,即.java文件

打好的jar包,我命名为dynamic.jar

然后我们把这个jar包放到Android SDK build-tools 下某个版本里(随意),build-tools里有很多很厉害的工具,比如aapt,zipalign,但这次我们要用dx这个工具
执行
dx --dex --output=dynamic_for_android.jar dynamic.jar
这条指令的操作就是把dynamic.jar先打成一个.dex文件,以便Dalvik虚拟机能认识,再把这个.dex打成一个jar包,即dynamic_for_android.jar,这个保管好,待会要用

接下来,我们需要一个连接宿主项目和动态实现类的桥梁,当然就是com.fenfenzhong.interfaces.IDynamic这个接口啦,再打一个jar包

命名为dynamic_interface.jar

在Android Studio新建一个项目,叫DynamicApp,把dyncmic_interface.jar导入到项目的libs里面

这个项目相当简单,就一个按钮,但我们想实现的是:点击这个按钮的时候,调用的是动态类中的实现方法

package com.fenfenzhong.dynamicapp;

import android.content.Context;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.fenfenzhong.interfaces.IDynamic;

import java.io.File;

import dalvik.system.DexClassLoader;

public class MainActivity extends AppCompatActivity {

private IDynamic dy;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button clickMeBtn = (Button) findViewById(R.id.click_me);

String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "dynamic_for_android.jar";
Log.i("dex", dexPath);
Context context=getApplicationContext();
File dexOutputDir = context.getDir("dex", 0);
// String dexOutputDirs = Environment.getExternalStorageDirectory().toString();//如果使用这种outputdir的话,可能报异常

DexClassLoader dcl = new DexClassLoader(dexPath,dexOutputDir.getAbsolutePath(),null,getClassLoader());
try {
Class dynamicClass = dcl.loadClass("com.fenfenzhong.impl.Dynamic");
dy = (IDynamic)dynamicClass.newInstance();
if(dy != null) {
dy.init(MainActivity.this);
}
} catch (Exception e) {
e.printStackTrace();
}

clickMeBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(dy != null) {
dy.clickMe();
}else {
Toast.makeText(getApplicationContext(), "cannot load class", Toast.LENGTH_LONG).show();
}
}
});
}
}

以上便是这个项目所有的代码,在最外层定义一个private IDynamic dy,这个dy就是父类(接口)引用
主要从Sring dexPath这里说起:

  • 我们先定义dexPath为外部存储根目录下的dynamic_for_android.jar,即指定到时候从这里去拿dex文件
  • dexOutputDir这个变量主要是指定释放dex文件后的路径,因为我们使用DexClassLoader,上面我说到,这种ClassLoader需要这么一个路径
  • DexClassLoader dcl 这里,就初始化了一个实例,叫dcl,根据上面对ClassLoader的描述,我们可以指定一个类名,通过这个实例去找,或者生成该类的字节码
  • 利用反射机制的newInstance()方法初始化一个对象,并用父类引用dy 指向该对象,到此多态的实现条件已满足
  • 这个Activity里有一个按钮,但它的onClick方法,调用的是dy的clickMe()

好了,我们把那个dynamic_for_android.jar包push到sd卡的根目录,执行命令:
adb push dynamic_for_android.jar sdcard/

然后,运行一下程序

点击按钮

成功啦!!看到这个还是非常兴奋的!

总结

虽然只是一个特别小的demo,但里面的一些步骤,一些坑,一些思想,只有一点点踩过来才能体会,Android自身的框架决定了其动态加载不同于普通java项目,在这个demo里我们是直接把实现类push到sdcard的根目录,但如果从网上动态下载呢?反射的机制能让我们动态的定制功能,现在很多应用的换肤,添加模块都是通过这种方式,在安全方面,它还被应用到apk的加固,这个之后慢慢写~