Android避坑指南,Gson与Kotlin碰撞出一个不安全的操做

本文已经受权「鸿洋」公众号原创首发。html

最近发现微信多了个专辑功能,能够把一系列的原创文章聚合,恰好我每周都会遇到不少同窗问我各类各样的问题,部分问题仍是比较有意义的,我会在周末详细的写demo验证,简单扩展一下写成文章分享给你们。java

固然不鼓励你们随便私聊我问问题,你们能够去星球提问,公众号后台回复「星球」就能看到入口了,那里有5000多人,我毕竟仍是有工做要忙。android

先看一个问题

来一块儿看一段代码:json

public class Student  {
    private Student() {
        throw new IllegalArgumentException("can not create.");
    }
    public String name;
}
复制代码

咱们如何经过Java代码建立一个Student对象?api

咱们先想下经过Java建立对象大概有哪些方式:缓存

  1. new Student() // 私有
  2. 反射调用构造方法 //throw ex
  3. 反序列化 // 须要实现相关序列化接口
  4. clone // 须要实现clone相关接口
  5. ...

好了,已经超出个人知识点范畴了。安全

难免心中嘀咕:bash

这题目太偏了,毫无心义,并且文章标题是 Android 避坑指南,看起来毫无关系服务器

是的,确实很偏,跳过这个问题,咱们往下看,看看是怎么在Android开发过程当中遇到的,并且看完后,这个问题就迎刃而解了。微信

问题的来源

上周一个群有个小伙伴,遇到了一个Kotlin写的Bean,在作Gson将字符串转化成具体的Bean对象时,发生了一个不符合预期的问题。

由于是他们项目的代码,我就不贴了,我写了个相似的小例子来替代。

对于Java Bean,kotlin能够用data class,网上也有不少博客表示:

在 Kotlin 中,不须要本身动手去写一个 JavaBean,能够直接使用 DataClass,使用 DataClass 编译器会默默地帮咱们生成一些函数。

咱们先写个Bean:

data class Person(var name: String, var age: Int) {


}
复制代码

这个Bean是用于接收服务器数据,经过Gson转化为对象的。

简化一下代码为:

val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
复制代码

咱们传递了一个json字符串,可是没有包含key为name的值,而且注意:

在Person中name的类型是String,也就是说是不容许name=null的

那么上面的代码,我运行起来结果是什么呢?

  1. 报错,毕竟没有传name的值;
  2. 不报错,name 默认值为"";
  3. 不报错,name=null;

感受1最合理,也符合Kotlin的空安全检查。

验证一下,修改一下代码,看一下输出:

val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
println(person.name )
复制代码

输出结果:

null
复制代码

是否是有些奇怪,感受意外绕过了Kotlin的空类型检查。

因此那位出问题的同窗,在这里以后数据就出了问题,致使一直排查困难。

咱们再改一下代码:

data class Person(var name: String, var age: Int): People(){

}
复制代码

咱们让Person继承自People类:

public class People {

    public People(){
        System.out.println("people cons");
    }

}
复制代码

在People类的构造方法中打印日志。

咱们都清楚,正常状况下,通常构造子类对象,必然会先执行父类的构造方法。

运行一下:

没有执行父类构造方法,但对象构造出来了

这里能够猜到,Person对象的构建,并非常规的构建对象,没有走构造方法。

那么它是怎么作到的呢?

那只能去Gson的源码中取找答案了。

找到其怎么作的,其实就至关于解答了咱们文首的问题。

追查缘由

Gson这样构造出一个对象,可是没有走父类构造这种,若是真是的这样,那么是极其危险的。

会让程序彻底不符合运行预期,少了一些必要逻辑。

因此咱们提早说一下,你们不用太惊慌,并非Gson很容易出现这样的状况,而是刚好上例的写法碰上了,咱们一会会说清楚。

首先咱们把Person这个kotlin的类,转成Java,避免背后藏了一些东西:

# 反编译以后的显示
public final class Person extends People {
   @NotNull
   private String name;
   private int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public final int getAge() {
      return this.age;
   }

   public final void setAge(int var1) {
      this.age = var1;
   }

   public Person(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

   // 省略了一些方法。
}

复制代码

能够看到Person有一个包含两参的构造方法,而且这个构造方法中有name的空安全检查。

也就是说,正常经过这个构造方法构建一个Person对象,是不会出现空安全问题的。

那么只能去看看Gson的源码了:

Gson的逻辑,通常都是根据读取到的类型,而后找对应的TypeAdapter去处理,本例为Person对象,因此会最终走到ReflectiveTypeAdapterFactory.create而后返回一个TypeAdapter。

咱们看一眼其内部代码:

# ReflectiveTypeAdapterFactory.create
@Override 
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
	Class<? super T> raw = type.getRawType();
	
	if (!Object.class.isAssignableFrom(raw)) {
	  return null; // it's a primitive! } ObjectConstructor<T> constructor = constructorConstructor.get(type); return new Adapter<T>(constructor, getBoundFields(gson, type, raw)); } 复制代码

重点看constructor这个对象的赋值,它一眼就知道跟构造对象相关。

# ConstructorConstructor.get
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
    final Type type = typeToken.getType();
    final Class<? super T> rawType = typeToken.getRawType();
	
	// ...省略一些缓存容器相关代码

    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }

    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
    if (defaultImplementation != null) {
      return defaultImplementation;
    }

    // finally try unsafe
    return newUnsafeAllocator(type, rawType);
  }
复制代码

能够看到该方法的返回值有3个流程:

  1. newDefaultConstructor
  2. newDefaultImplementationConstructor
  3. newUnsafeAllocator

咱们先看第一个newDefaultConstructor

private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
    try {
      final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
      if (!constructor.isAccessible()) {
        constructor.setAccessible(true);
      }
      return new ObjectConstructor<T>() {
        @SuppressWarnings("unchecked") // T is the same raw type as is requested
        @Override public T construct() {
            Object[] args = null;
            return (T) constructor.newInstance(args);
            
            // 省略了一些异常处理
      };
    } catch (NoSuchMethodException e) {
      return null;
    }
  }
复制代码

能够看到,很简单,尝试获取了无参的构造函数,若是可以找到,则经过newInstance反射的方式构建对象。

追随到咱们的Person的代码,其实该类中只有一个两参的构造函数,并无无参构造,从而会命中NoSuchMethodException,返回null。

返回null会走newDefaultImplementationConstructor,这个方法里面都是一些集合类相关对象的逻辑,直接跳过。

那么,最后只能走:**newUnsafeAllocator ** 方法了。

从命名上面就能看出来,这是个不安全的操做。

newUnsafeAllocator最终是怎么不安全的构建出一个对象呢?

往下看,最终执行的是:

public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
//   public Object allocateInstance(Class<?> type);
// }
try {
  Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
  Field f = unsafeClass.getDeclaredField("theUnsafe");
  f.setAccessible(true);
  final Object unsafe = f.get(null);
  final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
  return new UnsafeAllocator() {
    @Override
    @SuppressWarnings("unchecked")
    public <T> T newInstance(Class<T> c) throws Exception {
      assertInstantiable(c);
      return (T) allocateInstance.invoke(unsafe, c);
    }
  };
} catch (Exception ignored) {
}
  
// try dalvikvm, post-gingerbread use ObjectStreamClass
// try dalvikvm, pre-gingerbread , ObjectInputStream

}

复制代码

能够看到Gson在没有找到无参的构造方法后,经过sun.misc.Unsafe构造了一个对象。

注意:Unsafe该类并非全部的Android 版本中都包含,不过目前新版本都包含,因此Gson这个方法中有3段逻辑都是用来生成对象的,你能够认为3重保险,针对不一样平台。 本文测试设备:Android 29模拟器

咱们这里暂时只讨论sun.misc.Unsafe,其余的其实一个意思。

sun.misc.Unsafe和许API?

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操做的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提高Java运行效率、加强Java语言底层资源操做能力方面起到了很大的做用。但因为Unsafe类使Java语言拥有了相似C语言指针同样操做内存空间的能力,这无疑也增长了程序发生相关指针问题的风险。在程序中过分、不正确使用Unsafe类会使得程序出错的几率变大,使得Java这种安全的语言变得再也不“安全”,所以对Unsafe的使用必定要慎重。 tech.meituan.com/2019/02/14/…

具体能够参考美团的这篇文章。

好了,到这里就真相大白了。

缘由是咱们Person没有提供默认的构造方法,Gson在没有找到默认构造方法时,它就直接经过Unsafe的方法,绕过了构造方法,直接构建了一个对象。

到这里,咱们收获了:

  1. Gson是如何构建对象的?
  2. 咱们在写须要Gson转化为对象的类的时候,必定要记得有默认的构造方法,不然虽然不报错,可是很不安全!
  3. 咱们了解到了还有这种Unsafe黑科技的方式构造对象。

回到文章开始的问题

Java中咋么构造一个下面的Student对象呢?

public class Student  {
    private Student() {
        throw new IllegalArgumentException("can not create.");
    }
    public String name;
}
复制代码

咱们模仿Gson的代码,编写以下:

try {
    val unsafeClass = Class.forName("sun.misc.Unsafe")
    val f = unsafeClass.getDeclaredField("theUnsafe")
    f.isAccessible = true
    val unsafe = f.get(null)
    val allocateInstance = unsafeClass.getMethod("allocateInstance", Class::class.java)
    val student = allocateInstance.invoke(unsafe, Student::class.java)
    (student as Student).apply {
        name = "zhy"
    }
    println(student.name)
} catch (ignored: Exception) {
    ignored.printStackTrace()
}
复制代码

输出:

zhy
复制代码

成功构建。

Unsafe 一点用没有?

看到这里,你们可能最大的收获就是了解Gson构建对象流程,以及之后写Bean的时候会注意提供默认的无参构造方法,尤为在使用Kotlin data class的时候。

那么刚才咱们所说的Unsafe方法就没有其余实际用处吗?

这个类,提供了相似C语言指针同样操做内存空间的能力。

你们都知道在Android P上面,Google限制了app对hidden API的访问。

可是,Google不能限制本身对hidden API访问对吧,因此它本身的相关类,是容许访问hidden API的。

那么Google是如何区分是咱们app调用,仍是它本身调用呢?

经过ClassLoader,系统认为若是ClassLoader为BootStrapClassLoader则就认为是系统类,则放行。

那么,咱们突破P访问限制,其中一个思路就是,搞一个类,把它的ClassLoader换成BootStrapClassLoader,从而能够反射任何hidden api。

怎么换呢?

只要把这个类的classLoader成员变量设置为null就能够了。

参考代码:

private void testJavaPojie() {
	try {
	  Class reflectionHelperClz = Class.forName("com.example.support_p.ReflectionHelper");
	  Class classClz = Class.class;
	  Field classLoaderField = classClz.getDeclaredField("classLoader");
	  classLoaderField.setAccessible(true);
	  classLoaderField.set(reflectionHelperClz, null);
	} catch (Exception e) {
		  e.printStackTrace();
	}
}
来自:https://juejin.im/post/5ba0f3f7e51d450e6f2e39e0

复制代码

可是这样有个问题,上面的代码用到了反射修改一个类的classLoader成员,假设google有一天把反射设置classLoader也彻底限制掉,就不行了。

那么怎么办?原理仍是换ClassLoader,可是咱们不走Java反射的方式了,而是用Unsafe:

@Keep
public class ReflectWrapper {
 
    //just for finding the java.lang.Class classLoader field's offset @Keep private Object classLoaderOffsetHelper; static { try { Class<?> VersionClass = Class.forName("android.os.Build$VERSION"); Field sdkIntField = VersionClass.getDeclaredField("SDK_INT"); sdkIntField.setAccessible(true); int sdkInt = sdkIntField.getInt(null); if (sdkInt >= 28) { Field classLoader = ReflectWrapper.class.getDeclaredField("classLoaderOffsetHelper"); long classLoaderOffset = UnSafeWrapper.getUnSafe().objectFieldOffset(classLoader); if (UnSafeWrapper.getUnSafe().getObject(ReflectWrapper.class, classLoaderOffset) instanceof ClassLoader) { Object originalClassLoader = UnSafeWrapper.getUnSafe().getAndSetObject(ReflectWrapper.class, classLoaderOffset, null); } else { throw new RuntimeException("not support"); } } } catch (Exception e) { throw new RuntimeException(e); } } } 来自做者区长:一种纯 Java 层绕过 Android P 私有函数调用限制的方式,一文。 复制代码

Unsafe赋予了咱们操做内存的能力,也就能完成一些平时只能依赖C++完成的代码。

好了,从一位朋友遇到的问题,由此引起了一整篇文章的讨论,但愿你能有所收获。

感谢郭霖,淡蓝色星期三,天空等朋友。

相关文章
相关标签/搜索