Android避坑指南,發(fā)現(xiàn)了一個(gè)極度不安全的操作
最近發(fā)現(xiàn)微信多了個(gè)專輯功能,可以把一系列的原創(chuàng)文章聚合,剛好我每周都會(huì)遇到很多同學(xué)問我各種各樣的問題,部分問題還是比較有意義的,我會(huì)在周末詳細(xì)的寫demo驗(yàn)證,簡(jiǎn)單擴(kuò)展一下寫成文章分享給大家。
1. 先看一個(gè)問題
來一起看一段代碼:
- public class Student {
 - private Student() {
 - throw new IllegalArgumentException("can not create.");
 - }
 - public String name;
 - }
 
我們?nèi)绾瓮ㄟ^Java代碼創(chuàng)建一個(gè)Student對(duì)象?
我們先想下通過Java創(chuàng)建對(duì)象大概有哪些方式:
- 
    
new Student() // 私有
 - 
    
反射調(diào)用構(gòu)造方法 //throw ex
 - 
    
反序列化 // 需要實(shí)現(xiàn)相關(guān)序列化接口
 - 
    
clone // 需要實(shí)現(xiàn)clone相關(guān)接口
 - 
    
...
 
好了,已經(jīng)超出我的知識(shí)點(diǎn)范疇了。
不免心中嘀咕:
這題目太偏了,毫無意義,而且文章標(biāo)題是 Android 避坑指南,看起來毫無關(guān)系
是的,確實(shí)很偏,跳過這個(gè)問題,我們往下看,看看是怎么在Android開發(fā)過程中遇到的,而且看完后,這個(gè)問題就迎刃而解了。
2. 問題的來源
上周一個(gè)群有個(gè)小伙伴,遇到了一個(gè)Kotlin寫的Bean,在做Gson將字符串轉(zhuǎn)化成具體的Bean對(duì)象時(shí),發(fā)生了一個(gè)不符合預(yù)期的問題。
因?yàn)槭撬麄冺?xiàng)目的代碼,我就不貼了,我寫了個(gè)類似的小例子來替代。
對(duì)于Java Bean,kotlin可以用data class,網(wǎng)上也有很多博客表示:
在 Kotlin 中,不需要自己動(dòng)手去寫一個(gè) JavaBean,可以直接使用 DataClass,使用 DataClass 編譯器會(huì)默默地幫我們生成一些函數(shù)。
我們先寫個(gè)Bean:
- data class Person(var name: String, var age: Int) {
 - }
 
這個(gè)Bean是用于接收服務(wù)器數(shù)據(jù),通過Gson轉(zhuǎn)化為對(duì)象的。
簡(jiǎn)化一下代碼為:
- val gson = Gson()
 - val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
 
我們傳遞了一個(gè)json字符串,但是沒有包含key為name的值,并且注意:
在Person中name的類型是String,也就是說是不允許name=null的
那么上面的代碼,我運(yùn)行起來結(jié)果是什么呢?
- 
    
報(bào)錯(cuò),畢竟沒有傳name的值;
 - 
    
不報(bào)錯(cuò),name 默認(rèn)值為"";
 - 
    
不報(bào)錯(cuò),name=null;
 
感覺1最合理,也符合Kotlin的空安全檢查。
驗(yàn)證一下,修改一下代碼,看一下輸出:
- val gson = Gson()
 - val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
 - println(person.name )
 
輸出結(jié)果:
- null
 
是不是有些奇怪, 感覺意外繞過了Kotlin的空類型檢查。
所以那位出問題的同學(xué),在這里之后數(shù)據(jù)就出了問題,導(dǎo)致一直排查困難。
我們?cè)俑囊幌麓a:
- data class Person(var name: String, var age: Int): People(){
 - }
 
我們讓Person繼承自People類:
- public class People {
 - public People(){
 - System.out.println("people cons");
 - }
 - }
 
在People類的構(gòu)造方法中打印日志。
我們都清楚,正常情況下,一般構(gòu)造子類對(duì)象,必然會(huì)先執(zhí)行父類的構(gòu)造方法。
運(yùn)行一下:
沒有執(zhí)行父類構(gòu)造方法,但對(duì)象構(gòu)造出來了
這里可以猜到, Person對(duì)象的構(gòu)建,并不是常規(guī)的構(gòu)建對(duì)象,沒有走構(gòu)造方法。
那么它是怎么做到的呢?
只能去Gson的源碼中去找答案了。
找到其怎么做的,其實(shí)就相當(dāng)于解答了我們文首的問題。
3. 追查原因
Gson這樣構(gòu)造出一個(gè)對(duì)象,但是沒有走父類構(gòu)造這種,如果真是的這樣,那么是極其危險(xiǎn)的。
會(huì)讓程序完全不符合運(yùn)行預(yù)期,少了一些必要邏輯。
所以我們提前說一下,大家不用太驚慌,并不是Gson很容易出現(xiàn)這樣的情況,而是恰好上例的寫法碰上了,我們一會(huì)會(huì)說清楚。
首先我們把Person這個(gè)kotlin的類,轉(zhuǎn)成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有一個(gè)包含兩參的構(gòu)造方法,并且這個(gè)構(gòu)造方法中有name的空安全檢查。
也就是說,正常通過這個(gè)構(gòu)造方法構(gòu)建一個(gè)Person對(duì)象,是不會(huì)出現(xiàn)空安全問題的。
那么只能去看看Gson的源碼了:
Gson的邏輯,一般都是根據(jù)讀取到的類型,然后找對(duì)應(yīng)的TypeAdapter去處理,本例為Person對(duì)象,所以會(huì)最終走到`ReflectiveTypeAdapterFactory.create`然后返回一個(gè)TypeAdapter。
我們看一眼其內(nèi)部代碼:
- # 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));
 - }
 
重點(diǎn)看constructor這個(gè)對(duì)象的賦值,它一眼就知道跟構(gòu)造對(duì)象相關(guān)。
- # ConstructorConstructor.get
 - public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
 - final Type type = typeToken.getType();
 - final Class<? super T> rawType = typeToken.getRawType();
 - // ...省略一些緩存容器相關(guān)代碼
 - 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個(gè)流程:
- 
    
newDefaultConstructor
 - 
    
newDefaultImplementationConstructor
 - 
    
newUnsafeAllocator
 
我們先看第一個(gè)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;
 - }
 - }
 
可以看到,很簡(jiǎn)單,嘗試獲取了無參的構(gòu)造函數(shù),如果能夠找到,則通過newInstance反射的方式構(gòu)建對(duì)象。
追隨到我們的Person的代碼,其實(shí)該類中只有一個(gè)兩參的構(gòu)造函數(shù),并沒有無參構(gòu)造,從而會(huì)命中NoSuchMethodException,返回null。
返回null會(huì)走newDefaultImplementationConstructor,這個(gè)方法里面都是一些集合類相關(guān)對(duì)象的邏輯,直接跳過。
那么,最后只能走: newUnsafeAllocator 方法了。
從命名上面就能看出來,這是個(gè)不安全的操作。
newUnsafeAllocator最終是怎么不安全的構(gòu)建出一個(gè)對(duì)象呢?
往下看,最終執(zhí)行的是:
- 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在沒有找到無參的構(gòu)造方法后,通過 sun.misc.Unsafe 構(gòu)造了一個(gè)對(duì)象。
注意:Unsafe該類并不是所有的Android 版本中都包含,不過目前新版本都包含,所以Gson這個(gè)方法中有3段邏輯都是用來生成對(duì)象的,你可以認(rèn)為3重保險(xiǎn),針對(duì)不同平臺(tái)。本文測(cè)試設(shè)備:Android 29模擬器
我們這里暫時(shí)只討論sun.misc.Unsafe,其他的其實(shí)一個(gè)意思。
`sun.misc.Unsafe`何許API?
Unsafe是位于sun.misc包下的一個(gè)類,主要提供一些用于執(zhí)行低級(jí)別、不安全操作的方法,如直接訪問系統(tǒng)內(nèi)存資源、自主管理內(nèi)存資源等,這些方法在提升Java運(yùn)行效率、增強(qiáng)Java語言底層資源操作能力方面起到了很大的作用。但由于Unsafe類使Java語言擁有了類似C語言指針一樣操作內(nèi)存空間的能力,這無疑也增加了程序發(fā)生相關(guān)指針問題的風(fēng)險(xiǎn)。在程序中過度、不正確使用Unsafe類會(huì)使得程序出錯(cuò)的概率變大,使得Java這種安全的語言變得不再“安全”,因此對(duì)Unsafe的使用一定要慎重。
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
具體可以參考美團(tuán)的這篇文章。
好了,到這里就真相大白了。
原因是我們Person沒有提供默認(rèn)的構(gòu)造方法,Gson在沒有找到默認(rèn)構(gòu)造方法時(shí),它就直接通過Unsafe的方法,繞過了構(gòu)造方法,直接構(gòu)建了一個(gè)對(duì)象。
到這里,我們收獲了:
- 
    
Gson是如何構(gòu)建對(duì)象的?
 - 
    
我們?cè)趯懶枰狦son轉(zhuǎn)化為對(duì)象的類的時(shí)候,一定要記得有默認(rèn)的構(gòu)造方法,否則雖然不報(bào)錯(cuò),但是很不安全!
 - 
    
我們了解到了還有這種Unsafe黑科技的方式構(gòu)造對(duì)象。
 
4. 回到文章開始的問題
Java中咋么構(gòu)造一個(gè)下面的Student對(duì)象呢?
- 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
 
成功構(gòu)建。
5.
Unsafe 一點(diǎn)用沒有?看到這里,大家可能最大的收獲就是了解Gson構(gòu)建對(duì)象流程,以及以后寫B(tài)ean的時(shí)候會(huì)注意提供默認(rèn)的無參構(gòu)造方法,尤其在使用Kotlin `data class `的時(shí)候。
那么剛才我們所說的Unsafe方法在Android上就沒有其他實(shí)際用處嗎?
這個(gè)類,提供了類似C語言指針一樣操作內(nèi)存空間的能力。
大家都知道在Android P上面,Google限制了app對(duì)hidden API的訪問。
但是,Google不能限制自己對(duì)hidden API訪問對(duì)吧,所以它自己的相關(guān)類,是允許訪問hidden API的。
那么Google是如何區(qū)分是我們app調(diào)用,還是它自己調(diào)用呢?
其中有一個(gè)辦法就是通過ClassLoader,系統(tǒng)認(rèn)為如果ClassLoader為BootStrapClassLoader則就認(rèn)為是系統(tǒng)類,則放行。
那么,我們突破P訪問限制,其中一個(gè)思路就是,搞一個(gè)類,把它的ClassLoader換成BootStrapClassLoader,從而可以反射任何hidden api。
怎么換呢?
只要把這個(gè)類的classLoader成員變量設(shè)置為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
 
但是這樣有個(gè)問題,上面的代碼用到了反射修改一個(gè)類的classLoader成員,假設(shè)google有一天把反射設(shè)置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);
 - }
 - }
 - }
 - 來自作者區(qū)長(zhǎng):一種純 Java 層繞過 Android P 私有函數(shù)調(diào)用限制的方式,一文。
 
Unsafe賦予了我們操作內(nèi)存的能力,也就能完成一些平時(shí)只能依賴C++完成的代碼。
好了,從一位朋友遇到的問題,由此引發(fā)了一整篇文章的討論,希望你能有所收獲。
















 
 
 







 
 
 
 