Android避坑指南,發現了一個極度不安全的操作
最近發現微信多了個專輯功能,可以把一系列的原創文章聚合,剛好我每周都會遇到很多同學問我各種各樣的問題,部分問題還是比較有意義的,我會在周末詳細的寫demo驗證,簡單擴展一下寫成文章分享給大家。
1. 先看一個問題
來一起看一段代碼:
- public class Student {
- private Student() {
- throw new IllegalArgumentException("can not create.");
- }
- public String name;
- }
我們如何通過Java代碼創建一個Student對象?
我們先想下通過Java創建對象大概有哪些方式:
-
new Student() // 私有
-
反射調用構造方法 //throw ex
-
反序列化 // 需要實現相關序列化接口
-
clone // 需要實現clone相關接口
-
...
好了,已經超出我的知識點范疇了。
不免心中嘀咕:
這題目太偏了,毫無意義,而且文章標題是 Android 避坑指南,看起來毫無關系
是的,確實很偏,跳過這個問題,我們往下看,看看是怎么在Android開發過程中遇到的,而且看完后,這個問題就迎刃而解了。
2. 問題的來源
上周一個群有個小伙伴,遇到了一個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的
那么上面的代碼,我運行起來結果是什么呢?
-
報錯,畢竟沒有傳name的值;
-
不報錯,name 默認值為"";
-
不報錯,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的源碼中去找答案了。
找到其怎么做的,其實就相當于解答了我們文首的問題。
3. 追查原因
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個流程:
-
newDefaultConstructor
-
newDefaultImplementationConstructor
-
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的使用一定要慎重。
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
具體可以參考美團的這篇文章。
好了,到這里就真相大白了。
原因是我們Person沒有提供默認的構造方法,Gson在沒有找到默認構造方法時,它就直接通過Unsafe的方法,繞過了構造方法,直接構建了一個對象。
到這里,我們收獲了:
-
Gson是如何構建對象的?
-
我們在寫需要Gson轉化為對象的類的時候,一定要記得有默認的構造方法,否則雖然不報錯,但是很不安全!
-
我們了解到了還有這種Unsafe黑科技的方式構造對象。
4. 回到文章開始的問題
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()
- }
輸出:
- shy
成功構建。
5.
Unsafe 一點用沒有?看到這里,大家可能最大的收獲就是了解Gson構建對象流程,以及以后寫Bean的時候會注意提供默認的無參構造方法,尤其在使用Kotlin `data class `的時候。
那么剛才我們所說的Unsafe方法在Android上就沒有其他實際用處嗎?
這個類,提供了類似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++完成的代碼。
好了,從一位朋友遇到的問題,由此引發了一整篇文章的討論,希望你能有所收獲。