最近发现微信多了一个相册功能,可以聚合一系列原创文章。刚好每周都会遇到很多同学问我各种问题,有些问题还挺有意义的,我周末会写一个详细的demo验证,简单展开成文章分享给大家。1、先看一道题,一起看一段代码:publicclassStudent{privateStudent(){thrownewIllegalArgumentException("cannotcreate.");}publicStringname;}我们如何通过Java代码创建一个Student对象?我们先想想Java中创建对象的大概方式:newStudent()//私有反射调用构造函数//throwex反序列化//需要实现相关的序列化接口clone//需要实现clone相关的接口...嗯,这超出了我的知识点范围。不禁在心里嘀咕:这个话题太偏了,没有意义,而且文章的标题是Android避坑指南,好像和它没什么关系。Android开发过程中遇到,看了下,解决了这个问题。2.问题根源上周,群里的小伙伴遇到了一个用Kotlin写的Bean。在做Gson将字符串转换成具体的Bean对象时,出现了意想不到的问题。因为是他们项目的代码,我就不贴了。我写了一个类似的小例子。对于JavaBeans,kotlin可以使用数据类。网上也有很多博客说:在Kotlin中,不需要自己写一个JavaBean,直接使用DataClass就可以了,DataClass编译器会默默的为我们生成一些函数。我们先写一个Bean:dataclassPerson(varname:String,varage:Int){}。这个Bean用来接收服务器数据,通过Gson转换成对象。将代码简化为:valgson=Gs??on()valperson=gson.fromJson("{\"age\":\"12\"}",Person::class.java)我们传递了一个json字符串,但是它不包含key为name的value,注意:Person中name的类型为String,即不允许name=null。那么当我运行上面的代码时,结果是什么呢?报错,毕竟name的值没有传过去;不报错,name默认值为"";没有报错,name=null;感觉1最合理,也符合Kotlin的nullsafetycheck。验证一下,修改代码,看输出:valgson=Gs??on()valperson=gson.fromJson("{\"age\":\"12\"}",Person::class.java)println(person.name)outputresult:null是不是有点奇怪,感觉不小心绕过了Kotlin的null类型检查。所以出问题的同学来了之后,数据就出现了问题,导致排查困难。让我们再次更改代码:dataclassPerson(varname:String,varage:Int):People(){}我们让Person继承自People类:publicclassPeople{publicPeople(){System.out.println("peoplecons");}}inPrint记录在People类的构造函数中。我们都知道,一般情况下,在构造子类对象时,必须先执行父类的构造方法。运行它:不执行父类的构造方法,而是构造对象。这里可以猜到Person对象的构造不是常规的构造对象,也没有构造方法。那么它是如何做到的呢?只能去Gson的源码中寻找答案了。弄清楚怎么做其实就相当于回答了我们文章开头的问题。3.找出原因Gson这样构造一个对象,但是没有按照父类构造。如果是这样的话,那是极其危险的。会让程序完全不符合运行预期,会缺失一些必要的逻辑。所以提前说一下,大家不用太恐慌,并不是Gson容易出现这种情况,只是正好写在上面的例子中,我们后面会说清楚。首先,我们将kotlin类Person转换为Java,以避免在其背后隐藏一些东西:var1,"");this.name=var1;}publicfinalintgetAge(){returnthis.age;}publicfinalvoidsetAge(intvar1){this.age=var1;}publicPerson(@NotNullStringname,intage){Intrinsics.checkParameterIsNotNull(name,"name");super();this.name=name;this.age=age;}//省略了部分方法。可以看到Person有一个带两个参数的构造函数,并且在这个构造函数中对name进行了null安全检查。也就是说,如果通过这种构造方法正常构造一个Person对象,是不会出现null安全问题的。那我们只能看Gson的源码了:Gson的逻辑一般都是根据读取到的类型,然后找到对应的TypeAdapter进行处理。在这种情况下,它是一个Person对象,因此它最终会转到“ReflectiveTypeAdapterFactory.create”,然后返回一个TypeAdapter。我们看一下它的内部代码:#ReflectiveTypeAdapterFactory.create@OverridepublicTypeAdaptercreate(Gsongson,finalTypeTokentype){Classraw=type.getRawType();if(!Object.class.isAssignableFrom(raw)){returnnull;//它是原始的!}ObjectConstructorconstructor=constructorConstructor.get(type);returnnewAdapter(constructor,getBoundFields(gson,type,raw));}Focusonconstructor对这个对象的赋值,一看就知道和对象的构造有关。#ConstructorConstructor.getpublicObjectConstructorget(TypeTokentypeToken){finalTypetype=typeToken.getType();finalClassrawType=typeToken.getRawType();//...省略部分缓存容器相关代码ObjectConstructordefaultConstructor=newDefaultConstructor(rawType);if(defaultConstructor!=null){returndefaultConstructor;}ObjectConstructordefaultImplementation=newDefaultImplementationConstructor(type,rawType);if(defaultImplementation!=null){returndefault//implementation};finallytryunsarefreturnnewUnsafeAllocator(type,rawType);}可以看到这个方法的返回值有3个过程:newDefaultConstructornewDefaultImplementationConstructornewUnsafeAllocator先看第一个newDefaultConstructorprivateObjectConstructornewDefaultConstructor(ClassrawType){try{finalConstructorconstructor=rawType.getDeclaredConstructor();if(!constructor.isAccessible()){constructor.setAccessible(true);}returnnewObjectConstructor(){@SuppressWarnings("unchecked")//Tisthesamerawtypeasisrequested@OverridepublicTconstruct(){Object[]args=null;return(T)constructor.newInstance(args);//省略了一些异常处理};}catch(NoSuchMethodExceptione){returnnull;}}可以看到很简单。尝试获取不带参数的构造函数。如果能找到,就通过newInstance反射构建对象,按照我们Person的代码来。其实只有一个二参构造器,没有无参构造器,所以会命中NoSuchMethodException,返回null。返回null将转到newDefaultImplementationConstructor。该方法包含一些集合相关对象的逻辑,直接跳过。那么,最终,我们只能走:newUnsafeAllocator方法。从命名可以看出,这是一个不安全的操作。newUnsafeAllocator最终是如何不安全地构造一个对象的呢?往下看,最后的执行是:publicstaticUnsafeAllocatorcreate(){//tryJVM//publicclassUnsafe{//publicObjectallocateInstance(Class>type);//}try{Class>unsafeClass=Class.forName("sun.misc.不安全");Fieldf=unsafeClass.getDeclaredField("theUnsafe");f.setAccessible(true);finalObjectunsafe=f.get(null);finalMethodallocateInstance=unsafeClass.getMethod("allocateInstance",Class.class);返回newUnsafeAllocator(){@Override@SuppressWarnings("unchecked")publicTnewInstance(Classc)throwsException{assertInstantiable(c);return(T)allocateInstance.invoke(unsafe,c);}};}catch(Exceptionignored){}//trydalvikvm,post-gingerbreaduseObjectStreamClass//trydalvikvm,pre-gingerbread,ObjectInputStream}可以看到Gson在没有找到不带参数的构造方法后,通过sun.misc.Unsafe构造了一个对象。注意:Unsafe类并不是所有Android版本都包含的,但是在当前的新版本中包含了,所以Gson方法中有3段用于生成对象的逻辑。你可以为不同的平台想出3种保险。本文测试设备:Android29模拟器。我们这里暂时只讨论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对象?publicclassStudent{privateStudent(){thrownewIllegalArgumentException("cannotcreate.");}publicStringname;}我们模仿Gson的代码,写成这样:try{valunsafeClass=Class.forName("sun.misc.Unsafe")valf=unsafeClass。getDeclaredField("theUnsafe")f.isAccessible=truevalunsafe=f.get(null)valallocateInstance=unsafeClass.getMethod("allocateInstance",Class::class.java)valstudent=allocateInstance.invoke(不安全,Student::class.java)(studentasStudent).apply{name="zhy"}println(student.name)}catch(ignored:Exception){ignored.printStackTrace()}输出:shy构建成功。5.Unsafe有用吗?看到这里,你最大的收获可能是理解了Gson构建对象的过程,以后写bean的时候会注意提供一个默认的无参构建方法,尤其是在使用Kotlin`dataclass`的时候。那么我们刚刚提到的Unsafe方法在Android上就没有其他实际用途了吗?这个类提供了类似于C语言指针的操作内存空间的能力。大家都知道,在AndroidP上,谷歌限制了应用访问隐藏的API。但是,Google不能限制它对隐藏API的访问,对吧,所以它自己的相关类允许访问隐藏API。那么Google是怎么区分是我们的app调用的还是自己调用的呢?其中一种方法是使用ClassLoader。系统认为如果ClassLoader是BootStrapClassLoader,就认为是系统类,然后释放。然后,我们突破P访问限制。其中一种思路是创建一个类,将其ClassLoader替换为BootStrapClassLoader,这样任何隐藏的api都可以体现出来。怎么改?只要将这个类的classLoader成员变量设置为null即可。参考代码:privatevoidtestJavaPojie(){try{ClassreflectionHelperClz=Class.forName("com.example.support_p.ReflectionHelper");ClassclassClz=Class.class;FieldclassLoaderField=classClz.getDeclaredField("classLoader");classLoaderField.setAccessible(true);classLoaderField.set(reflectionHelperClz,null);}catch(Exceptione){e.printStackTrace();}}来自:https://juejin.im/post/5ba0f3f7e51d450e6f2e39e0但是有个问题,上面的代码使用反射修改了一个类的classLoader成员,假设哪天谷歌彻底限制反射设置classLoader,就不行了。那么该怎么办?原理是改ClassLoader,但是我们不使用Java反射,而是使用Unsafe:参考代码:@KeeppublicclassReflectWrapper{//justforfindingthejava.lang.ClassclassLoaderfield'soffset@KeepprivateObjectclassLoaderOffsetHelper;static{try{Class>VersionClass=Class.forName("android.os.Build$VERSION");FieldsdkIntField=VersionClass.getDeclaredField("SDK_INT");sdkIntField.setAccessible(true);intsdkInt=sdkIntField.getInt(null);if(sdkInt>=28){FieldclassLoader=ReflectWrapper.class.getDeclaredField("classLoaderOffsetHelper");longclassLoaderOffset=UnSafeWrapper.getUnSafe().objectFieldOffset(classLoader);如果(UnSafeWrapper.getUnSafe().getObject(ReflectWrapper.class,classLoaderOffset)instanceofClassLoader){ObjectoriginalClassLoader=UnSafeWrapper().getUnSafe.getAndSetObject(ReflectWrapper.class,classLoaderOffset,null);}else{thrownewRuntimeException("notsupport");}}}catch(Exceptione){thrownewRuntimeException(e);}}}作者区长:纯Java层绕过AndroidP私有函数调用限制的方法,一篇。Unsafe给了我们操作内存的能力,也可以完成一些只有C++才能完成的代码。嗯,朋友遇到的问题引发了整篇文章的讨论,希望大家有所收获。