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++完成的代碼。

好了,從一位朋友遇到的問題,由此引起了一整篇文章的討論,但願你能有所收穫。

感謝郭霖,淡藍色星期三,天空等朋友。

相關文章
相關標籤/搜索