Android 极简反射教程

作者: _qisen | 来源:发表于2016-07-15 11:12 被阅读1890次

    原文链接:http://www.woaitqs.cc/2016/07/14/android-reflection/

    光的反射

    Java 反射简介

    Java 程序的运行需要相应的环境(Java Runtime Environment), 而这其中最有名的就是 JVM,JVM 提供了动态运行字节码的能力,除了 JVM 帮我们做链接、加载字节码相关的事情外,也通过反射提供给我们动态修改的能力。反射使得我们能够在运行时,不知道类名、方法名的情况下查看其中的接口、方法和字段等信息,另一方面,也可以动态调用方法、新建对象,甚至篡改字段值。

    那介绍了反射是干嘛之后,反射能在实际的工作中发挥什么作用吗?Android 系统在设计的时候,出于安全和架构的考虑,利用了 Java 权限相关的东西(private,package等等,以及 @hide 这个注解)使得我们无法访问某些字段,或者方法。但在实际开发过程中,这些隐藏的字段或者方法却能提供给我们非常想要的特性。在这种矛盾的情况下,反射就能满足我们的需求,像是打开隐藏关卡的一把钥匙。

    总结起来就是,反射提供了一种与 Class 文件进行动态交互的机制。例如在下面的入口函数中,就可以看到 HashMapClass 里所有的方法。

    public class HashMapClass extends HashMap {
    
      public static void main(String[] args) {
        Method[] methods = HashMapClass.class.getMethods();
    
        for (Method method : methods) {
          System.out.println("method name is " + method.getName());
        }
      }
    
    }
    

    Class 类简介

    在进行接下来的反射教程中,首先应该了解 Class Object。Java 中所有的类型,包括 int、float 等基本类型,都有与之相关的 Class 对象。如果知道对应的 Class name,可以通过 Class.forName() 来构造相应的 Class 对象,如果没有对应的 class,或者没有加载进来,那么会抛出 ClassNotFoundException 对象。

    Class 封装了一个类所包含的信息,主要的接口如下:

        try {
          Class mClass = Class.forName("java.lang.Object");
    
          // 不包含包名前缀的名字
          String simpleName = mClass.getSimpleName();
    
          // 类型修饰符, private, protect, static etc.
          int modifiers = mClass.getModifiers();
          // Modifier 提供的一些用于判读类型的静态方法.
          Modifier.isPrivate(modifiers);
    
          // 父类的信息
          Class superclass = mClass.getSuperclass();
    
          // 构造函数
          Constructor[] constructors = mClass.getConstructors();
    
          // 字段类型
          Field[] fields = mClass.getFields();
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
        }
    

    常用反射方法

    下面列举一些反射常见的应用场景,主要从 Student 这个类进行入手。

    public class Student {
    
      private final String name;
    private int grade = 1;
    
      public Student(String name) {
        this.name = name;
      }
    
      public String getName() {
        return name;
      }
    
      private int getGrade() {
        return grade;
      }
      
      private void goToSchool() {
        System.out.println(name + " go to school!");
      }
    }
    

    1)反射构建 Student 对象

    try {
      Class studentClass = Student.class;
      
      // 参数类型为一个 String 的构造函数
      Constructor constructor = studentClass.getConstructor(new Class[]{String.class});
      
      // 实例化 student 对象
      Student student = (Student)constructor.newInstance("Li Lei");
      System.out.print(student.getName());
    } catch (ReflectiveOperationException e) {
      e.printStackTrace();
    }
    

    2)反射修改私有变量

    try {
      Student student = new Student("Han MeiMei");
      System.out.println("origin grade is " + student.getGrade());
    
      Class studentClass = Student.class;
      // 获取声明的 grade 字段,这里要注意 getField 和 getDeclaredField 的区别
      Field gradeField = studentClass.getDeclaredField("grade");
      
      // 如果是 private 或者 package 权限的,一定要赋予其访问权限
      gradeField.setAccessible(true);
      
      // 修改 student 对象中的 Grade 字段值
      gradeField.set(student, 2);
      System.out.println("after reflection grade is " + student.getGrade());
    
    } catch (ReflectiveOperationException e) {
      e.printStackTrace();
    }
    

    3)反射调用私有方法

    try {
      Student student = new Student("Han MeiMei");
      
      // 获取私有方法,同样注意 getMethod 和 getDeclaredMethod 的区别
      Method goMethod = Student.class.getDeclaredMethod("goToSchool", null);
      // 赋予访问权限
      goMethod.setAccessible(true);
    
      // 调用 goToSchool 方法。
      goMethod.invoke(student, null);
    
    } catch (ReflectiveOperationException e) {
      e.printStackTrace();
    }
    

    Android 反射应用示例

    学以致用,现在我们来通过实际的例子,来看看如何利用 Java 的反射特性来完成一些牛逼的功能。设想我们想通过插件的方式,来启动一个未注册的 Activity,这就会涉及到很多问题,其中之一就是如何赋予这些插件 Activity 生命周期。这个例子就是通过反射的方式,来手动地进行 Activity 生命周期的通知。

    1)了解并熟悉代码细节

    要实现上述的功能,第一步就是要知道 Activity 的生命周期是如何运作的,要对代码细节有所了解。因为反射所操作的对象是具体的 Class 对象,如果不清楚源码细节,反射将无从说起。

    篇幅所限,具体的原理又较为复杂,这里列出链接 Android 插件化原理解析——Activity生命周期管理, 有兴趣的同学可以自行查看,在这只进行大体上的说明。

    Activity 生命周期与 ActivityThread 息息相关,我们来进行各个突破,先看看 Activity 是怎么启动的。当需要启动 Activity 时,ActivityManagerService 会通过 Binder 机制向 ActivityThread 发送消息,经过链式地调用后,会执行到scheduleLaunchActivity 这个方法,我们看看其内部的实现。

    // we use token to identify this activity without having to send the
    // activity itself back to the activity manager. (matters more with ipc)
    @Override
    public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
            ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
            CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
            int procState, Bundle state, PersistableBundle persistentState,
            List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
            boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
    
        updateProcessState(procState, false);
    
        ActivityClientRecord r = new ActivityClientRecord();
    
        r.token = token;
        r.ident = ident;
        r.intent = intent;
        r.referrer = referrer;
        r.voiceInteractor = voiceInteractor;
        r.activityInfo = info;
        r.compatInfo = compatInfo;
        r.state = state;
        r.persistentState = persistentState;
    
        r.pendingResults = pendingResults;
        r.pendingIntents = pendingNewIntents;
    
        r.startsNotResumed = notResumed;
        r.isForward = isForward;
    
        r.profilerInfo = profilerInfo;
    
        r.overrideConfig = overrideConfig;
        updatePendingConfiguration(curConfig);
    
        sendMessage(H.LAUNCH_ACTIVITY, r);
    }
    

    注意最后的 sendMessage 方法,说明内部是采用的 handler 机制来进行通信的。在这篇文章中 Android 应用进程启动流程 提及到当应用进程启动后,会调用 ActivityThread 的 main 方法,并在这个方法中进行相应的消息循环初始化,其后在主线程上的消息传递都是通过 ActivityThread 中的 H 这个内部来进行初始化,这里的 H 就是可能的突破口之一。

    private class H extends Handler {
        public static final int LAUNCH_ACTIVITY         = 100;
        public static final int PAUSE_ACTIVITY          = 101;
        public static final int PAUSE_ACTIVITY_FINISHING= 102;
        public static final int STOP_ACTIVITY_SHOW      = 103;
        public static final int STOP_ACTIVITY_HIDE      = 104;
        public static final int SHOW_WINDOW             = 105;
        public static final int HIDE_WINDOW             = 106;
        public static final int RESUME_ACTIVITY         = 107;
        public static final int SEND_RESULT             = 108;
        public static final int DESTROY_ACTIVITY        = 109;
        public static final int BIND_APPLICATION        = 110;
        public static final int EXIT_APPLICATION        = 111;
        public static final int NEW_INTENT              = 112;
        public static final int RECEIVER                = 113;
        public static final int CREATE_SERVICE          = 114;
        public static final int SERVICE_ARGS            = 115;
        public static final int STOP_SERVICE            = 116;
        
        //......
        
        public void handleMessage(Message msg){
            // ...
        }
    }
    

    既然发现通信是通过 H 这个 Handler 来完成的,那么再看看 Handler 的实现原理,这里也有一篇文章供参考, Android Handler机制全解析 。Handler 在内部维护着一个 callback 对象,当有消息发生时,会通过这个 callback 往外发送消息。

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    

    如果能够替换 callback 变量,这样消息就可以传递到替换后的 callback 里了。这样是否能达到我们的目的?

    2) 验证是否满足反射条件

    如果我们反射的对象,拥有多个实例,那么我们就需要在不同的地方进行处理,显然这样会额外增加实现的复杂度,因而反射尽量在 单例或者静态 实例上完成,代码的复杂度能提升不少。

    在前面提到了通过替换 callback 的方式,这样是否可行,我们来验证下。首先 H 是放置在 ActivityThread 这个类里面的,而 ActivityThread 运行的线程就是主线程,我们知道每一个应用都拥有一个主线程,因而这里的 ActivityThread 只存在一份,进而也可以保证 H 的唯一性。

    另一方面,ActivityThread 在内部也维护了 currentActivityThread 这个变量,虽然由于 API 的访问限制,不能直接访问,但也同样可以由反射拿到。

    至此,可以证明这种方式理论上是可以成功的。

    3)实施代码细节,并在合适的地方进行代码注入

    首先,我们自定义出自定义的 callback 对象,这个 callback 作为 H 中 callback 的代理,这里需要注意的是 msg 的定义要和底层保持一致,代码如下:

    public class LaunchCallback implements Handler.Callback {
    
      public static final int LAUNCH_ACTIVITY = 100;
    
      private Handler.Callback originCallback;
    
      public LaunchCallback(Handler.Callback originCallback) {
        this.originCallback = originCallback;
      }
    
      @Override
      public boolean handleMessage(Message msg) {
    
        if (msg.what == LAUNCH_ACTIVITY) {
          Toast.makeText(
              VApp.getApp().getApplicationContext(),
              "activity is going to launch! ", Toast.LENGTH_SHORT).show();
        }
    
        return originCallback.handleMessage(msg);
      }
    
    }
    

    通过前文提及的反射方法,将运行的 callback 替换为自定义的 callback,代码如下:

    public class InjectTool {
    
      public static void dexInject() {
        try {
    
          // 通过反射调用 ActivityThread 的静态方法, 获取 currentActivityThread
          Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
          Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
          currentActivityThreadMethod.setAccessible(true);
          Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    
          // 获取 currentActivityThread 这个示例中的 mH
          Field handlerField = activityThreadClass.getDeclaredField("mH");
          handlerField.setAccessible(true);
          Handler handler = (Handler) handlerField.get(currentActivityThread);
    
          // 修改 mH 中的 callback 字段
          Field callbackField = Handler.class.getDeclaredField("mCallback");
          callbackField.setAccessible(true);
          Handler.Callback callback = (Handler.Callback) callbackField.get(handler);
    
          callbackField.set(handler, new LaunchCallback(callback));
        } catch (IllegalArgumentException | NoSuchMethodException | IllegalAccessException
            | InvocationTargetException | ClassNotFoundException | NoSuchFieldException e) {
          e.printStackTrace();
        }
      }
    
    }
    

    在 Application 中进行注入,完成修改的落地

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        InjectTool.dexInject();
    }
    

    实际运行的效果,如图所示:

    activity launch

    activity 其他的生命周期也可以同样处理,这里就不再赘述了。

    4)反射使用总结

    在通过反射实现相关功能的时候,第一件事情就是认真地阅读源码,理清其中的脉络,其后找寻其中的突破点,这些点一般为 static 方法或者单例对象,最后才是代码落地。

    相关文章

      网友评论

      • 快乐石头111:楼主,举实例中originCallback是为空的吧,所以是不是应该在LaunchCallback 的handleMessage中判断一下,如果为null,就直接返回false,否则会报错。
        _qisen:在我代码里面是对这个进行赋值了的,这里没有展示所有源代码。你挺细心的,哈哈
      • CaptainJno:楼主,我看了你的几篇文章写的都很棒,但是文章链接跳转到 http://www.woaitqs.cc 下相关界面的链接,全部报错 404 。麻烦解决一下
        _qisen:@CaptainJno 谢谢你的提醒,已经全部修复了(不排除有遗漏)。
      • karonl:一直不太明白,反射不就违反了定好的规则了吗?我们用反射去使用某个apk的私有方法,这对apk意味不安全,作为apk如何避免被反射?混淆?
        _qisen:@karonl 这个和root关系不大,可以调用一些系统不对外开放的API
        karonl:大概明白,那反射是不是可以实现部分需要root的功能?
        _qisen:@karonl 反射只是会针对系统API进行,一般你应用层的调用API是不会被反射的,混淆后对应的方法名会改变,没法反射的。
      • 单刀土豆:配图不错:smile:
        _qisen:@单刀土豆 那就是内容很差了,哈哈哈

      本文标题:Android 极简反射教程

      本文链接:https://www.haomeiwen.com/subject/ggfyjttx.html