为了学习一下动态加载技术,以及几种ClassLoader

首先

让我们愉快地了解一下app的启动流程
附图(图片来自网络)
p1

大致上就是这样,按照非虫书上的讲法,简化来看,应用启动->Binder IPC->System server->Zygote->fork自身创建Dalvik实例,然后来到Dalvik这边,首先应该是PathClassLoader,将所有的类找出来放进一个全局哈希表里,然后字节码校验生成odex,然后findClass找到主类加载,作为程序入口开始执行程序;

按照上图的说法,Click事件会调用startActivity(Intent),通过Binder IPC机制,调用到ActivityManagerService

  1. PackageManager的resolveIntent()收集这个Intent对象的指向信息,存储在一个Intent对象中;
  2. 通过grantUriPermissionLocked()方法验证用户是否有足够的权限调用该Intent对象指向的Activity;
  3. 若有权限,AMS检查,并在新的task中启动Activity;

检查进程的ProcessRecord是否存在,如果ProcessRecord是null,AMS会创建新的进程来实例化目标Activity

创建进程

ActivityManagerService调用startProcessLocked()创建新的进程,该方法通过socket通道传递参数给Zygote进程。Zygote进程fork自身,调用Zygote.main()方法,来实例化ActivityThread对象,然后在动态加载的时候我们是替换ActivityThread类中的一个ClassLoader类型的变量来使我们指定的apk运行;

实例化ActivityThread对象并返回新进程的pid;

ActivityThread随后依次调用Looper.prepareLoop()和Looper.loop()来开启消息循环;

绑定Application

一般逆出来看到主类继承自Application,首先考虑是否加壳

接下来要做的就是将进程和指定的Application绑定起来;
通过ActivityThread对象中调用bindApplication()方法来完成;
该方法通过发送一个BIND_APPLICATION消息到消息队列中,最终通过handleBindApplication()方法处理消息;
调用makeApplication()方法来加载App带classes到内存中;

启动Activity

经过前两个步骤,系统有了该Application的进程,后面的调用顺序就是普通的从一个已经存在的进程中启动一个新线程的activity了;

realStartActivity(),会调用application线程对象(也就是大厉害的ActivityThread)中的scheduleLaunchActivity()来发送LAUNCH_ACTIVITY消息到消息队列中,通过handleLaunchActivity()来处理该消息;

Zygote fork的第一个子进程就是SystemServer,这个类非常重要,系统里面重要的服务都是在这个进程里面开启的,比如ActivityManagerService、PackageManagerService、Window Manager Service等等;
系统初始化完成之后,就会进入到系统桌面,即Launcher,其实Launcher也是个app,继承自Activity

动态加载

开始了解一下动态加载activity,因为看到之前的dex壳,使用到了这个技术,而小马哥也将到过这里,所以来深入了解一下;

需要提前了解类加载器的知识;

Android中的各种类加载器

重要:

  • DexClassLoader
  • PathClassLoader

DexClassLoader

加载dex/jar/apk

继承自ClassLoader类,是类加载器的鼻祖,这个类下只有一个构造函数;

参数:

  1. dexPath:加载apk/dex/jar路径的;
  2. dexOutDir:dex的输出路径;
  3. libPath:加载的时候需要用到的lib库,一般不用;
  4. parent:给DexClassLoader指定父加载器

PathClassLoader

只能加载/data/app中的apk,也就是安装到手机上的apk,所以安装到手机上的apk在/data/app目录下

一般程序都是安装了,再打开,此时PathClassLoader就去加载指定的apk(解压成dex,然后优化成odex);

同样继承自ClassLoader类,但是构造函数没有指定dex输出位置,因为,是固定的,/data/dalvik-cache

系统类加载器

Context.class.getClassLoader();

加载器是BootClassLoader,继承自ClassLoader

应用程序默认加载器

getClassLoader();

默认加载器是PathClassLoader,还有apk路径,libPath(/var/lib、/system/lib)

系统类加载器

也就是ClassLoader的加载器

ClassLoader.getSystemClassLoader();

其实还是PathClassLoader,只是加载apk路径是/system/app/xxx.apk

循环查看调用栈

ClassLoader classLoader = getClassLoader();
// ClassLoader classLoader = ClassLoader.getSystemClassLoader();
while (classLoader != null){
Log.i("herbwen", classLoader);
classLoader = classLoader.getParent();
}

发现默认加载器PathClassLoader和 系统加载器的父类都是BootClassLoader

动态加载Activity

再次明确一下目的,是要动态加载Activity(免安装运行
PathClassLoader是默认加载器,但是加载插件一般使用DexClassLoader;
Android四大组件都有一个特点就是它们有自己的启动流程和生命周期;使用DexClassLoader加载进来的activity是不会涉及到任何启动流程和生命周期的概念,就是一个普普通通的类,启动肯定会出错;

我们需要让加载进来的Activity有启动流程和生命周期,加载Activity时,有一个很重要的类:LoadedApk.java,负责加载一个Apk程序,类内部有一个mClassLoader变量,负责加载一个Apk程序,只要获取到这个类加载器即可;

// LoadedApk下
private ClassLoader mClassLoader;

// 其实应该是PathClassLoader传入的,动态加载的时候改为DexClassLoader传入

Ps:正常情况下,找关于一个Apk或者是Activity相关信息的时候,特别是启动流程的时候,肯定会去寻找:ActivityThread.java这个类,很重要,内部信息多;Java程序运行的入口main方法;

app程序的执行入口

  1. 如果是项目级别的,就是Application的onCreate方法,这样的话,需要在Manifest.xml中的application标签内加上android:name属性指定到那个类
  2. 如果是稍微底层一点的,就是ActivityThread中的main方法

    // ActivityThread.java

    private static ActivityThread sCurrentActivityThread;

final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<String, WeakReference<LoadedApk>>();

这个mPackages存放Apk包名和LoadedApk的映射关系;

这里需要看看大牛RefInvoke的实现
基本三板斧

// invoke static method
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method, pareType);
return method.invoke(null, pareValues)

// get field object
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);

// set field object
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, fieldValue)

先到这吧,记录一下问题,Class找不到,不知道是哪里出了问题,明天再看看
当我发现,那个示例能用的时候,我忍不了了,这是什么辣鸡,后面果断猜测,编译版本的问题,我的AS基本上是最新的SDK了,能将就就将就吧;

我,终于终于是搞定这个插件了,但是现在还只是可以调用函数;
LayoutInflater这边有个问题
p2

查后无果,以后再解决吧;

问题

先记录一下问题,搞了老半天……
搞到最后都忘了我是为了搞清楚动态加载的机制才去实现这个简单的插件的;

一开始是DexClassLoader对象loader调用loadClass的时候,找不到主类,也就是"com.herbwen.pluginactivity.MainActivity",这很蛋疼,然后逆出来的xml和网上教程都告诉我,需要加个权限到AndroidManifest.xml

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

读写外部设备!
博客教程里说,将apk放在/data/data/com.herbwen.hostactivity/cache下即可,搞笑呢吧,根本读不到,而且声明的权限是外部设备,所以将apk放在sdcard里即可解决这种问题;

因为layout加载不进去,这里读过原作者的博文之后了解到,有个setLayout的方法,将LayoutInflater的结果传入,可能就调到那个插件的页面了,不过这里的sdk看来是实现不了了;

分析

插件项目

package com.herbwen.pluginactivity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i("Herrrb", "插件调用成功");
    }
    public static void PluginLog(String s){
        Log.i("Herrrb", s);
    }
}

这就是所有了,可以在宿主项目中实践onCreate调用和外部public方法的调用;

本来还有activity之类的,因为获取不了LayoutInflater,所以也就没用了

宿主项目

一定要注意首先在AndroidManifest.xml中声明上述的权限和插件MainActivity的activity标签;

详情对照GitHub,https://github.com/Herrrb/DynamicPlugin
先在onCreate里获取layout的id、上下文和包的路径

Log.i("Herrrb", "resIds:" + R.layout.activity_main);
Log.i("Herrrb", "context1:" + this.getApplicationContext());
Log.i("Herrrb", "context2:" + this.getPackageResourcePath());

然后设置点击事件相应,相应就是调用插件;这里设置的是textView的点击;
主要的调试就是在click()这个点击事件回调中进行的;
首先拿到sd卡的路径,配置包名

String filesDir = Environment.getExternalStorageDirectory().getPath();
String libPath = filesDir + File.separator + "Plugin.apk";

然后调用DexClassLoader,将路径设置到sd卡下;

DexClassLoader loader = new DexClassLoader(libPath, filesDir, filesDir, getClassLoader());

这里最后一个参数,是DexClassLoader的parent,需要设置成PathClassLoader类;
需要替换PathClassLoader为DexLoader,但是PathClassLoader是系统本身的默认类加载器,即mClassLoader的值,如果单独将DexClassLoader设置为mClassLoader的话,会出错;
所以一定要将DexClassLoader的父加载器设置成PathClassLoader,因为类加载器是符合双亲委派机制的;
之后是loadClass方法,加载MainActivity,然后getMethod得到我们希望提前调用的方法,其实在这里可以对插件中的全局变量进行预定义;
然后就是getMethod返回的Method对象来调用invoke,执行插件中的方法;

class clazz = null;
try {
    clazz = loader.loadClass("com.herbwen.pluginactivity.MainActivity");
    Method method = clazz.getMethod("PluginLog", String.class);
    method.invoke(null, "插件外部调用成功");
} catch (Throwable e){
    Log.i("Herrrb", "error: " + Log.getStackTraceString(e));
}

然后执行loadApkClassLoader方法,传入DexClassLoader对象;

这个loadApkClassLoader很关键;
首先获取路径

String filesDir = this.getCacheDir().getAbsolutePath();

Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[]{}, new Object[]{});

String packageName = this.getPackageName();
// 得到的是当前的包名,后面再配合插件的ClassLoader

ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldObject("android.app.ActivityThread", currentActivityThread, "mPackages");
// 这里按照上面的源码获取到映射关系mPackages

WeakReference wr = mPackages.get(packageName);
// 拿到LoadedApk对象

RefInvoke.setFieldObject("android.app.LoadedApk", "mClassLoader", wr.get(), dloader);
// 将LoadedApk类中的,mClassLoader变量,由原来的LoadedApk对象更换为我们自己的ClassLoader

p3

从logcat中可以看出成果;

源码及apk见GitHub:https://github.com/Herrrb/DynamicPlugin

用法

adb install xxx.apk

adb push Plugin.apk /sdcard

monitor或者Android Studio运行查看Logcat,filter可填写Tag:Herrrb即可