首页 文章 精选 留言 我的

精选列表

搜索[面试],共4912篇文章
优秀的个人博客,低调大师

面试不再怕-说透动静态代理!

追溯 学一个技术,要知道技术因何而产生,才能有学下去的目标和动力,才能更好的理解 首先,要明确为什么要存在代理呢? 存在一个常见的需求:怎样在不修改类A代码的情况下,在调用类A的方法时进行一些功能的附加与增强呢? 先不考虑什么代理不代理的,我们设计一个简单的实现方案: 新创建一个类B,类B组合类A,在类B中创建一个方法b,方法b中调用类A中的方法a,在调用前和调用后都可以添加一些自定义的附加与增强代码。 当有需求需要调用类A的方法a并且想要添加一个附加功能时,就去调用类B的方法b即可实现上述需求; 下面为了便于理解,附上伪代码: // 定义类A public class ClassA{ public void methoda(){ System.out.println("我是方法a!"); } } // 定义类B public class ClassB{ // 组合ClassA ClassA A; public ClassB(ClassA A){ this.A = A; } @Override public void methodb(){ System.out.println("我是方法a的附加功能代码,我执行啦~!"); A.methoda(); System.out.println("我是方法a的附加功能代码,我完成啦~!"); } } 下面,让我们来调用一下ClassB的methodb方法,则会产生以下输出: 我是方法a的附加功能代码,我执行啦~! 我是方法a! 我是方法a的附加功能代码,我完成啦~! 可以发现,方法a执行了,并且在没有修改类A代码的前提下,为方法a附加了其他的功能; 不难吧,其实上述的代码就是一个最简单的代理模式了 代理存在的意义:使用代理模式可以在不修改别代理对象代码的基础上,通过扩展代理类,进行一些功能的附加与增强 代理种类 代理分为静态代理和动态代理,其涉及的设计模式就是代理模式本尊了,代理模式一般包含几种元素,如下图: 主题接口(subject):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法; 真实主题(RealSubject):真正实现业务逻辑的类; 代理类(Proxy):用来代理和封装真实主题; 客户端(Client):使用代理类和主题接口完成一些工作。 为了更好的理解,我们将上述实现的最简易版的代理完善一下,添加接口,代理类也实现相应的被代理类的接口,实现同一个方法,伪代码如下: // 被代理类的接口(上图中subject) public interface ImpA{ void methoda(); } // 定义类A(上图中RealSubject) public class ClassA implements ImpA{ public void methoda(){ System.out.println("我是方法a!"); } } // 定义类B(上图中Proxy) public class ClassB implements ImpA { // 组合ClassA ImpA A; public ClassB(ClassA A){ this.A = A; } // 重写被代理类的方法 @Override public void methoda(){ System.out.println("我是方法a的附加功能代码,我执行啦~!"); A.methoda(); System.out.println("我是方法a的附加功能代码,我完成啦~!"); } } // 客户端类(上图中Client) public class Main{ // 创建被代理对象实例 ImpA A = new ClassA(); // 构造器注入被代理对象实例 ImpA B = new ClassB(A); // 调用代理方法 B.methoda(); } 静态代理 所谓静态代理也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。 上面的代码就是实现了一个静态代理; 其实静态代理就已经能够满足上述需求了,为什么还需要动态代理呢? 这里就涉及到静态代理的两个缺点了 代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,在程序规模稍大时静态代理代理类就会过多会造成代码混乱 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度。 基于上述两个问题,动态代理诞生了~ 动态代理 动态代理是在程序运行时,通过反射获取被代理类的字节码内容用来创建代理类 具体什么是动态代理呢? 名词:动态,动态在程序中就是表达在程序运行时就根据配置自动的生成代理类并且代理类和被代理类是在运行时才确定相互之间的关系; 在JDK中包含两种动态代理的实现机制:JDK Proxy 和 CGLib; 下面我们以JDK Proxy为例,讲解一下动态代理和根据源码分析并简单说一下应用场景 JDK Proxy JDK Proxy动态代理,api在包java.lang.reflect下,大家可能发现了,为什么在反射的包下呢?这个问题我们下面的源码分析会解决; 其核心api包含两个重要的核心接口和类:一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),简单说就这两个简单的很,这两个是我们实现动态代理所必需的用到的,下面简单介绍一下两个类: java.lang.reflect.Proxy(Class) :Proxy是 Java 动态代理机制的主类,提供一组静态方法来为一组接口动态地生成代理类及其对象。包含以下四个静态方法: static InvocationHandler getInvocationHandler(Object proxy) 该方法用于获取指定代理对象所关联的调用处理器 static Class getProxyClass(ClassLoader loader, Class[] interfaces) 该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象 static boolean isProxyClass(Class cl) 该方法用于判断指定类对象是否是一个动态代理类 static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h) 该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例,包含下面的参数: loader 指定代理类的ClassLoader加载器 interfaces 指定代理类要实现的所有接口 h: 表示的是当这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上 该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例 java.lang.reflect.InvocationHandler(interface) : InvocationHandler是上述newProxyInstance方法的InvocationHandler h参数传入,负责连接代理类和委托类的中间类必须实现的接口 它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。 上述就是动态代理两个核心的方法,不太明白?先别急,我们先用上述实现一个动态代理,你先看一下 还是以上述的案例从静态代理来改造为动态代理,实现动态代理主要就两步,假设还是存在上述的ImplA、ClassA 1:创建一个处理器类实现InvocationHandler接口,重写invoke方法,伪代码如下: public class MyHandler implements InvocationHandler{ // 标识被代理类的实例对象 private Object delegate; // 构造器注入被代理对象 public MyHandler(Object delegate){ this.delegate = delegate; } // 重写invoke方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("被代理方法调用前的附加代码执行~ "); // 真实的被代理方法调用 method.invoke(delegate, args); System.out.println("被代理方法调用后的附加代码执行~ "); } } 好了,这样一个处理器就搞定了,当我们在调用被代理类的方法时,就是去执行上述重写的invoke方法,下面创建一个ClassA的代理类 2:创建代理类,并调用被代理方法 public class MainTest{ public static void main(String[] args) { // 创建被代理对象 ImplA A = new ClassA(); // 创建处理器类实现 InvocationHandler myHandler = new MyHandler(A); // 重点! 生成代理类, 其中proxyA就是A的代理类了 ImplA proxyA = (ImplA)Proxy.newProxyInstance(A.getClass().getClassLoader(), A.getClass().getInterfaces(), myHandler); // 调用代理类的代理的methoda方法, 在此处就会去调用上述myHandler的invoke方法区执行,至于为什么,先留着疑问,下面会说清楚~ proxyA.methoda(); } } 好了,至此一个动态代理就构建完成了,执行代码,会发现输出: 被代理方法调用前的附加代码执行~ 我是方法a! 被代理方法调用后的附加代码执行~ 太简单了有木有,这里总结一下动态代理的优缺点: 优点: 动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。 动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。 接口增加一个方法,除了所有实现类需要实现这个方法外,动态代理类会直接自动生成对应的代理方法。 缺点: JDK proxy只能对有实现接口的类才能代理,也就是说没有接口实现的类,jdk proxy是无法代理的,为什么呢?下面会解答. 有什么解决方案吗? 当然有,还有一种动态代理的方案:CGLib,它是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,对于final类或方法,是无法继承的,和jdk proxy基本思想是相似的,毕竟都是动态代理的实现方案嘛,在这篇文章就不做详解了,博主会在其他的博文单独介绍这个nb的框架 上述带大家搞了一遍动态代理和静态代理的应用;在这过程中,你有没有想过,动态代理是怎么实现的呢? 下面我们就从源码的角度分析一下,解决大家的疑问。 源码分析 在开始分析的时候,我希望大家带着几个问题去阅读,可以帮助大家更好的理解: 问题1:代理类为什么可以在运行的时候自动生成呢?如何生成的呢? 问题2:为什么调用代理类的相应的代理方法就可以调用到InvocationHandler实现类的invoke方法呢? 问题3:为什么jdk proxy只支持代理有接口实现的类呢? ps :为了提升阅读体验,让大家有一个更清晰的认知,以下源码会将一些异常处理和日志打印代码删除,只保留主干代码,请知悉~ 我们就从两个核心:InvocationHandler和Proxy来进行分析整个脉络,他们都在java.lang.reflect包下 InvocationHandler源码 public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } 上述就是InvocationHandler的源码,没什么其他的就是一个接口,里面有一个待实现方法invoke,处理类实现此接口重写invoke方法 Proxy源码 public class Proxy implements java.io.Serializable { // 处理类实例 变量 protected InvocationHandler h; // 用于存储 已经通过动态代理获取过的代理类缓存 private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(),new ProxyClassFactory()); // 私有无参构造,使得只能通过传入InvocationHandler参数来创建该对象 private Proxy() {} // 保护 构造函数,入参InvocationHandler处理类 protected Proxy(InvocationHandler h) { Objects.requireNonNull(h); this.h = h; } public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces) throws IllegalArgumentException{ ... } public static boolean isProxyClass(Class<?> cl) { ... } public static InvocationHandler getInvocationHandler(Object proxy) throws IllegalArgumentException { ... } // 生成代理类的实现方法 public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException{ ... } // 各种私有方法 private ... ... } Proxy类的整体的架构就类似于上述,InvocationHandler h参数和两个构造函数、四个上述已经介绍过的共有方法,还有一系列的私有方法,getProxyClass、isProxyClass、getInvocationHandler功能就和上面介绍的一样,就不再详细介绍了 我们下面来主要看一下newProxyInstance方法 newProxyInstance方法,我在方法内添加上了对应的注释: public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException{ // 1. 克隆对应的接口,用于代理类实现的接口数组 final Class<?>[] intfs = interfaces.clone(); ... /* * Look up or generate the designated proxy class. 源码中的介绍 * 2. 查找或者生成指定的代理类, 下面会详细介绍 */ Class<?> cl = getProxyClass0(loader, intfs); /* * Invoke its constructor with the designated invocation handler. * 3. 上面代码已经生成了代理类 cl,cl其中包含一个参数为传入的InvocationHandler h的构造函数, 获取该构造函数并通过该构造函数创建一个类的实例对象并返回 */ try { // 4. 通过《反射》获取参数为InvocationHandler的构造函数 final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; // 5. 判断构造函数是否为私有的,如果为私有的则需要设置私有可访问权限 if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } // 6. 通过上述获取的构造函数创建对应的 实例对象,并返回!over~ return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) { // 各种异常处理 } } 在上面的代码中,我简单的标注了一下每行代码的作用,下面我们来详细分析一下; 代理类的字节码生成逻辑 我们知道,在加载jvm前,java文件都已经被编译成了class字节码文件, 然后jvm通过类加载器将字节码文件加载到jvm中; 我们的代理类也是这样,不同的是动态代理的类是在程序运行时产生的,我们要做的就是如何在程序运行的时候,通过被代理类的字节码生成代理类的字节码! 我们接下来详细分析newProxyInstance方法: 在newProxyInstance中调用了Class<?> cl = getProxyClass0(loader, intfs);语句生成了代理类的字节码,此处调用了getProxyClass0方法,传入了指定的类加载器和对应要实现的接口 那么, 我们看看getProxyClass0方法的实现: private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } // proxyClassCache是WeakCache弱引用缓存类,如果之前就生成过对应的代理类就从缓存中取,如果没生成过就重新生成 return proxyClassCache.get(loader, interfaces); } proxyClassCache是Proxy类中的静态变量,是WeakCache类,里面封装了两个类KeyFactory、ProxyClassFactory,都是BiFunction函数式接口(如果不清楚函数式接口,请自行google); 将其拿过来看一下proxyClassCache = new WeakCache<>(new KeyFactory(),new ProxyClassFactory()); 其中调用了proxyClassCache.get(loader, interfaces)方法的实现 未避免代码过长,只粘贴了核心代码: public V get(K key, P parameter) { ... // 这部分主要是获取对应的 函数式接口,如果不明白函数式接口,google一下吧~ Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter)); Supplier<V> supplier = valuesMap.get(subKey); Factory factory = null; while (true) { // 此处为什么是while循环呢, 主要是supplier不为空的话,则执行下面的语句赋值后,再循环执行下一次则supplier不为空 if (supplier != null) { // 如果存在对应的函数式接口, 调用函数式接口对应的代码 // 重点!!!调用函数式接口!! V value = supplier.get(); if (value != null) { return value; } } if (factory == null) { // 创建一个 专门创建代理类字节码的工厂类,实现类是ProxyClassFactory factory = new Factory(key, parameter, subKey, valuesMap); } if (supplier == null) { supplier = valuesMap.putIfAbsent(subKey, factory); if (supplier == null) { // 将supplier赋值factory supplier = factory; }}}} } 总结一下上述方法的流程: 接着ProxyClassFactory.apply方法看一下: public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); // 获取接口对应的接口class对象 for (Class<?> intf : interfaces) { Class<?> interfaceClass = null; try { interfaceClass = Class.forName(intf.getName(), false, loader); } if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { } } String proxyPkg = null; int accessFlags = Modifier.PUBLIC | Modifier.FINAL; // 判断是否包含公有的接口对象,判断是否可以通过jdk proxy的方式进行生成代理类 for (Class<?> intf : interfaces) { int flags = intf.getModifiers(); if (!Modifier.isPublic(flags)) { accessFlags = Modifier.FINAL; String name = intf.getName(); int n = name.lastIndexOf('.'); String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); if (proxyPkg == null) { proxyPkg = pkg; } else if (!pkg.equals(proxyPkg)) { } } } // 如果没有公有接口类,需要使用CGLib来实现。。。 if (proxyPkg == null) { proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; } // 组装代理类的类名称 long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; // 重点!! 此处生成代理类的字节码数组 byte[] proxyClassFile = ProxyGenerator.generateProxyClassproxyName, interfaces, accessFlags); try { // 通过类加载器将字节码数组加载到JVm的方法区中生成Class对象! return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { } } 上述的 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); 为生成代理类字节码数组的方法,调用的方法中调用了generateClassFile方法; private byte[] generateClassFile() { // 首先,默认代理的三个方法:hashCodeequalstoString this.addProxyMethod(hashCodeMethod, Object.class); this.addProxyMethod(equalsMethod, Object.class); this.addProxyMethod(toStringMethod, Object.class); // 获取所有的要被代理类实现的接口 Class[] var1 = this.interfaces; int var2 = var1.length; int var3; Class var4; // 遍历上述获取的接口 for(var3 = 0; var3 < var2; ++var3) { // 赋值: 将接口的Class对象赋值! var4 = var1[var3]; // 通过“反射”获取所有方法 Method[] var5 = var4.getMethods(); int var6 = var5.length; for(int var7 = 0; var7 < var6; ++var7) { Method var8 = var5[var7]; // 将方法添加到 要被代理的方法中 this.addProxyMethod(var8, var4); } } // 获取要代理方法后,开始组装字节码 var14.writeInt(-889275714); var14.writeShort(0); var14.writeShort(this.accessFlags); var14.writeShort(this.cp.getClass(dotToSlash(this.className))); // 注意!!! .... 此处省略了绝大部分 字节码的组装过程,只给出了几行代码展示一下字节码的组装 // 最终返回组装好的字节码文件 return var13.toByteArray(); } } 在generateClassFile中,你会发现里面全部是重组字节码的代码, 主要是获取被代理类字节码和操作类InvocationHandler字节码组装出代理类的字节码,在重组的过程因为是在运行时进行了代理类的创建,无法像往常一样new一个被代理类的实例获取他的方法,让代理类进行调用。 获取字节码后,接下来就要将代理类的字节码加载进JVM中了,这里调用的是一个return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length) 其中的defineClass0是一个本地native 方法,传入了代理类名称、类加载器、代理类的字节码文件、文件长度参数,从而将字节码加载进JVM中! 代码如下: private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len); 将代理类的字节码加载进JVM后,会在方法区内生成一个Class对象,标识这个代理类; 代理类的实例生成逻辑 上面,我们知道了通过字节码技术生成了代理类字节码,并通过类加载器将字节码文件加载到了JVM的方法区中生成了一个Class对象,我们如何在运行时获取这个Class对象的实例呢? 只有获取了对象实例才可以使用不是~ 还是回到newProxyInstance方法中,上面我们分析了Class<?> cl = getProxyClass0(loader, intfs)这部分逻辑,生成了Class对象cl,下面生辰该实例代码,过程很简单,相关逻辑我就直接在代码中注释了 // 定义构造函数的参数类型,下面的一个语句使用 private static final Class<?>[] constructorParams = { InvocationHandler.class }; // 通过反射获取上述获取的Class对象的带参构造函数,参数必须是上述定义的 InvocationHandler.class类型 final Constructor<?> cons = cl.getConstructor(constructorParams); // 检查权限,如果是私有权限,设为可被访问 if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } // 通过构造函数传入对应 处理类h 参数,生成实例! return cons.newInstance(new Object[]{h}); 上述就是生成实例的代码,生成实例后newProxyInstance就返回该实例了,就可以使用了~ 反射:在运行时获取被代理类的字节码 那如何才能在运行时获取到被代理类的构造函数、方法、属性等字节码呢? 此时“反射!”登场了!我们通过反射可以在运行时获取到类的所有信息,所有哦。 定义: JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。 比如,在上述所说的组装代理类字节码时,在获取被代理类的所有方法时,就调用了Method[] var5 = var4.getMethods(); 反射中的getMethods方法,通过反射获取到了被代理类的所有方法,这样我们就可以在运行时获取到任何类的所有的字节码信息了! 从而可以组装出我们想要的代理类字节码! 所以说,反射也为动态代理的实现提供了理论支持!!因为只有在运行时能获取到对应类的信息,才可以通过信息创造出对应的我们所需要的代理类; 源码分析总结 总而言之,动态代理的理论支持是可以通过反射机制在运行时获取到类的所有信息,如果运行时获取不到被代理类的信息,那还咋生成代理类。 动态代理的大致流程: 通过上述流程。我们就获得了一个代理类对象了,调用代理类对应的方法,就会执行我们规定的执行逻辑,实现对被代理类的运行时动态增强和扩展! 此时,我们再拿出刚开始我们用JDK proxy实现的动态代理代码中的生成代理类的代码:ImplA proxyA = (ImplA)Proxy.newProxyInstance(A.getClass().getClassLoader(), A.getClass().getInterfaces(), myHandler) 每个参数的作用,是不是就很清晰了 问题解答 上面动态代理实现流程,我们可以回答上述的第一个代理类为什么可以在运行的时候自动生成呢?如何生成的呢? 问题了 对于第二个为什么调用代理类的相应的代理方法就可以调用到InvocationHandler实现类的invoke方法呢?和第三个为什么jdk proxy只支持代理有接口实现的类呢?问题,我们需要反编译一下我们通过字节码技术产生的代理类,如下: final class $Proxy0 extends Proxy implements ImplA { private static Method m1; private static Method m3; private static Method m2; private static Method m0; static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.test.ImplA").getMethod("methoda"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { // .. } } public $Proxy0(InvocationHandler var1) throws { super(var1); } // 需要被加强的方法methoda public final void methoda() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final boolean equals(Object var1) throws { // 省略部分代码。。。 return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } public final String toString() throws { // 省略部分代码。。。 return (String)super.h.invoke(this, m2, (Object[])null); } public final int hashCode() throws { // 省略部分代码。。。 return (Integer)super.h.invoke(this, m0, (Object[])null); } } 上述代码包含几个关键点: 方法为final类型,不可再被继承 代理名称为 $Proxy 代理类前缀 + 递增数字 继承动态代理的核心类Proxy, 实现了我们指定的接口ImplA 一个带参构造方法$Proxy0(InvocationHandler var1) 传入InvocationHandler内部调用了父类的Proxy的构造函数 methoda、toString、hashCode、equals全部调用的传入的InvocationHandler参数的 invoke方法!!! 现在回答第二个问题为什么调用代理类的相应的代理方法就可以调用到InvocationHandler实现类的invoke方法呢? 显而易见,代理类内部的代理方法全部显式调用的InvocationHandler实现类的invoke方法 第三个问题为什么jdk proxy只支持代理有接口实现的类呢? 因为代理类在使用JDK proxy方式生成代理类时,默认继承Proxy类,又因为java语言是单继承不支持多继承,那怎样才能标识我要代理什么类型的类或是代理什么方法呢? 接口呗,java支持接口的多继承,多少个都ok~ 好了,上述将动态代理的使用方式 和 实现原理统一过了一遍,也回答了几个容易疑惑的问题,下面我们简单说下动态代理在现实的java框架大家庭中的一些典型应用 动态代理的应用 spring aop : 这可以说是spring框架中最典型的应用了,通过动态代理在运行时产生代理类,完成对被代理类的增强和功能附加 RPC框架的实现 : 远程过程调用,RPC使得调用远程方法和调用本地方法一样,这是怎么搞的呢?服务方对外放出服务的接口api,调用方拿到接口api,通过动态代理的方式生成一个代理类,代理类的处理类的invoke方法可以通过websocket连接远程服务器调用对应的远程接口; 这样我们再用代理对象进行调用对应方法时时,就像调用本地方法一样了 mybatis框架中 : mapper.xml中编写sql语句,mapper.java接口写上对应的方法签名;我们直接调用mapper.java中的方法就可以执行对应的sql语句,有没有想过为什么? 框架使用动态代理创建一个mapper.java的代理对象,代理对象的处理类invoke中执行sql,就ok了 总结 代理分为静态代理和动态代理,动态代理的两种实现方式:JDK Proxy和CGLib,动态代理的核心反射机制,通过反射在运行时获取被代理类字节码和处理类字节码,动态代理代理类的生成通过重组字节码的方式。 原创不易,有收获的话,“老铁”们, 点赞一下呗~ 关于博文有任何问题请不吝评论,感谢 参考:JDK源码,https://www.jianshu.com/p/861223789d53 最后,更多技术博文请关注:

优秀的个人博客,低调大师

Android高级面试题资料(持续更新)

2019年阿里云双十一拼团地址:https://www.aliyun.com/1111/2019/home 1. ThreadLocal的理解 可以保证线程的安全。在多个线程共享相同的数据的时候,会为每个线程创建单独的副本,在单独的副本上进行数据的操作,不会对其它线程的数据产生影响,保证了线程安全。 2. HashMap HashSet HashTable的区别? 都是集合,底层都是Hash算法实现的。HashMap是Hashtable的替代品,这两个都是双列集合,而HashSet是单列集合。HashMap线程不安全、效率高、可以存储null键和null值;Hashtable线程安全,效率低,不可以存储null键和null值。 3. 如何让HashMap可以线程安全? HashMap 在并发执行 put 操作时会引起死循环,导致 CPU 利用率接近100%。因为多线程会导致 HashMap 的 Node 链表形成环形数据结构,一旦形成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。使用下面三种替换方式:HashtableConcurrentHashMapSynchronized Map 4. Android对HashMap做了优化后推出的新的容器类是什么? SparseArray它要比 HashMap 节省内存,某些情况下比HashMap性能更好,按照官方问答的解释,主要是因为SparseArray不需要对key和value进行auto-boxing(将原始类型封装为对象类型,比如把int类型封装成Integer类型),结构比HashMap简单(SparseArray内部主要使用两个一维数组来保存数据,一个用来存key,一个用来存value)不需要额外的额外的数据结构(主要是针对HashMap中的HashMapEntry而言的)。 5. Java多线程之间如何通信 等待唤醒机制 6. 线程池的实现机制 向线程池提交任务,会依次启动核心线程,如果提交的任务数超过了核心线程数,会将任务保存到阻塞队列中,如果阻塞队列也满了,且继续提交任务,则会创建新线程执行任务,直到任务数达到最大线程数。此时如果再提交任务的话会抛出异常或者直接丢弃任务。通过Executor.execute()无法得到返回值,通过ExecutorService.submit()可以得到返回值。 7. RxJava中map和flatmap操作符的区别及底层实现 Map返回的是结果集,flatmap返回的是包含结果集的Observable。Map只能一对一,flatmap可以一对多、多对多。RxJava是通过观察者模式实现的。 8. 对消息机制中Looper的理解 Looper在消息机制中扮演的角色是创造无限循环从Messagequeue中取得消息然后分发。 9. 单例模式有哪些实现方式 饿汉模式(线程安全,调用效率高,但是不能延时加载)懒汉模式(线程安全,调用效率不高,但是能延时加载)双重检测锁模式(由于JVM底层模型原因,偶尔会出问题,不建议使用)静态内部类式(线程安全,调用效率高,可以延时加载)枚举类(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用) 10. 通过静态内部类实现单例模式有哪些优点 线程安全,调用效率高,可以延时加载 11. synchronized volatile关键字有什么区别?以及还有哪些同样功能的关键字 (1) volatile是变量修饰符,而synchronized则作用于一段代码或者方法。(2) volatile只是在线程内存和main memory(主内存)间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。const、final、lock 12. 界面卡顿的原因有哪些? UI线程(main)有耗时操作视图渲染时间过长,导致卡顿 13. 造成OOM/ANR 的原因? OOM: (1)不恰当地使用static关键字 (2)内部类对Activity的引用 (3)大量Bitmap的使用会导致程序包运行时的内存消耗变大 (4)游标Cursor对象用完应该及时关闭 (5)加载对象过大 (6)相应资源过多,来不及释放。ANR: (1)在5秒内没有响应输入的事件(IO操作耗时、数据库操作复杂耗时、主线程非主线程产生死锁等待、网络加载/图片操作耗时、硬件操作耗时) (2)BroadcastReceiver在10秒内没有执行完毕(Service binder数量达到上限、Service忙导致超时无响应) 14. Activity与Fragment生命周期有何联系 在创建的过程中,是Activity带领着Fragment,在销毁的过程中,是Fragment带领着Activity。这里写图片描述 15. Glide三级缓存 内存缓存,磁盘缓存、网络缓存(由于网络缓存严格来说不算是缓存的一种,故也称为二级缓存)。缓存的资源分为两种:原图(SOURCE)、处理图(RESULT)(默认)。内存缓存:默认开启的,可以通过调用skipMemoryCache(true)来设置跳过内存缓存,缓存最大空间:每个进程可用的最大内存*0.4。(低配手机0.33)磁盘缓存:分为四种:ALL(缓存原图)、NONE(什么都不缓存)、SOURCE(只缓存原图)、RESULT(之后处理图),通过diskCacheStrategy(DiskCacheStrategy.ALL)来设置,缓存大小250M。 16. MVC、MVP、MVVM的原理 (1) MVC,Model View Controller,是软件架构中最常见的一种框架,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示。当用户发出事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。这里写图片描述(2) MVP是MVC的演化。MVP的model层相对于MVC是一样的,而activity和fragment不再是controller层,而是纯粹的view层,所有关于用户事件的转发全部交由presenter层处理。presenter层充当了桥梁的作用,用于操作view层发出的事件传递到presenter层中,presenter层去操作model层,并且将数据返回给view层。这里写图片描述(3) MVVM和MVP的区别貌似不大,只不过是presenter层换成了viewmodel层,还有一点就是view层和viewmodel层是相互绑定的关系,这意味着当你更新viewmodel层的数据的时候,view层会相应的变动ui。这里写图片描述 17. 数据库的操作类型有哪些,如何导入外部数据库? (1) 增删改查(2) 将外部数据库放在项目的res/raw目录下。因为安卓系统下数据库要放在data/data/packagename/databases的目录下,然后要做的就是将外部数据库导入到该目录下,操作方法是通过FileInputStream读取外部数据库,再用FileOutputStrean把读取到的东西写入到该目录下。 18. 是否使用过 IntentService,作用是什么, AIDL 解决了什么问题? (1) IntentService继承自Service。由于Service运行在主线程,无法进行耗时操作。所以你需要在Service中开启一个子线程,并且在子线程中运行。为了简化这一操作,Android中提供了IntentService来进行这一处理。通过查看IntentService的源码可以看到,在onCreate中,我们开启了一个HandlerThread线程,之后获取HandlerThread线程中的Looper,并通过这个Looper创建了一个Handler。然后在onStart方法中通过这个Handler将intent与startId作为Message的参数进行发送到消息队列中,然后交由Handler中的handleMessage中进行处理。由于在onStart方法是在主线程内运行的,而Handler是通过工作者线程HandlerThread中的Looper创建的。所以也就是在主线程中发送消息,在工作者接收到消息后便可以进行一些耗时的操作。(2) 进程间通信 19. 是否使用过本地广播,和全局广播有什么差别? 本地广播的数据在本应用范围内传播,不用担心隐私数据泄露的问题。不用担心别的应用伪造广播,造成安全隐患。相比在系统内发送全局广播,它更高效。 20. Activity、 Window、 View 三者的差别, fragment 的特点? (1) Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图) LayoutInflater像剪刀,Xml配置像窗花图纸。(2) a. Fragment可以作为Activity界面的一部分组成出现; 可以在一个Activity中同时出现多个Fragment,并且一个Fragment也可以在多个Activity中使用; 在Activity运行过程中,可以添加、移除或者替换Fragment; Fragment可以响应自己的输入事件,并且有自己的生命周期,它们的生命周期会受宿主Activity的生命周期影响。 21. Handler、 Thread 和 HandlerThread 的差别 从Android中Thread(java.lang.Thread -> java.lang.Object)描述可以看出,Android的Thread没有对Java的Thread做任何封装,但是Android提供了一个继承自Thread的类HandlerThread(android.os.HandlerThread -> java.lang.Thread),这个类对Java的Thread做了很多便利Android系统的封装。android.os.Handler可以通过Looper对象实例化,并运行于另外的线程中,Android提供了让Handler运行于其它线程的线程实现,也是就HandlerThread。HandlerThread对象start后可以获得其Looper对象,并且使用这个Looper对象实例Handler。 22. 低版本 SDK 实现高版本 api 自己实现或使用注解@TargetApi annotation 23. launch mode 应用场景 (1) standard:标准的启动模式。 这里写图片描述(2) singleTop:单一顶部模式 如果Activity已经被开启,并且处于任务栈的栈顶,就不会创建新的Activity,而是复用这个已经开启的Activity。为了防止出现一些奇怪的用户体验,推荐使用单一顶部模式,整个任务栈可以有多个实例存在.应用场景:短信发送界面.这里写图片描述(3)singletask:单一任务栈 在整个任务栈里面只允许有一个当前Activity的实例存在如果要开启的Activity在任务栈中已经存在,直接复用这个已经存在的Activity,并且把这个Activity上面的所有的其他Activity给清空应用场景:如果一个Activity非常消耗内存和cpu资源,建议把这个Activity做成singletask的模式。浏览器的browserActivity这里写图片描述(4)singleinstance:单一实例. 整个手机操作系统只有一个实例存在,并且是运行在自己单独的任务栈里面.应用场景:通话界面的Activity这里写图片描述 24. touch 事件传递流程 事件处理包括三种情况,分别为:传递—-dispatchTouchEvent()函数、拦截——onInterceptTouchEvent()函数、消费—-onTouchEvent()函数和OnTouchListener。Android事件传递流程:(1) 事件都是从Activity.dispatchTouchEvent()开始传递(2) 事件由父View传递给子View,ViewGroup可以通过onInterceptTouchEvent()方法对事件拦截,停止其向子view传递(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子View没有消费事件,事件会反向往上传递,这时父View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到Activity的onTouchEvent()函数。(4) 如果View没有对ACTION_DOWN进行消费,之后的其他事件不会传递过来,也就是说ACTION_DOWN必须返回true,之后的事件才会传递进来(5) OnTouchListener优先于onTouchEvent()对事件进行消费 View不处理事件流程图View不处理事件流程图 View处理事件流程图View处理事件流程图 事件拦截事件拦截 25.Android性能优化 一、代码优化 1.使用AndroidLint分析结果进行相应优化2.不使用枚举及IOC框架,反射性能低3.常量加static4.静态方法5.减少不必要的对象、成员变量6.尽量使用线程池7.适当使用软引用和弱引用8.尽量使用静态内部类,避免潜在的内存泄露9.图片缓存,采用内存缓存LRUCache和硬盘缓存DiskLRUCache10.Bitmap优化,采用适当分辨率大小并及时回收 二、布局优化 避免OverDraw过渡绘制优化布局层级避免嵌套过多无用布局当我们在画布局的时候,如果能实现相同的功能,优先考虑相对布局,然后在考虑别的布局,不要用绝对布局。使用标签把复杂的界面需要抽取出来使用标签,因为它在优化UI结构时起到很重要的作用。目的是通过删减多余或者额外的层级,从而优化整个Android Layout的结构。核心功能就是减少冗余的层次从而达到优化UI的目的!ViewStub 是一个隐藏的,不占用内存空间的视图对象,它可以在运行时延迟加载布局资源文件。 三、ListView和GridView优化 1.采用ViewHolder复用convertView2.避免在getView中执行耗时操作3.列表在滑动状态时不加载图片4.开启硬件加速 26.Android内存泄漏 内存泄漏简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。可能的原因有:1.注册没取消造成内存泄露,如:广播2.静态变量持有Activity的引用3.单例模式持有Activity的引用4.查询数据库后没有关闭游标cursor5.构造Adapter时,没有使用 convertView 重用6.Bitmap对象不在使用时调用recycle()释放内存7.对象被生命周期长的对象引用,如activity被静态集合引用导致activity不能释放8.使用Handler造成的内存泄露 原文地址:http://cloud.yundashi168.com/archives/964

优秀的个人博客,低调大师

2.2019Android高级面试题总结

说下你所知道的设计模式与使用场景 a.建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。使用场景比如最常见的AlertDialog,拿我们开发过程中举例,比如Camera开发过程中,可能需要设置一个初始化的相机配置,设置摄像头方向,闪光灯开闭,成像质量等等,这种场景下就可以使用建造者模式装饰者模式:动态的给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。装饰者模式可以在不改变原有类结构的情况下曾强类的功能,比如Java中的BufferedInputStream 包装FileInputStream,举个开发中的例子,比如在我们现有网络框架上需要增加新的功能,那么再包装一层即可,装饰者模式解决了继承存在的一些问题,比如多层继承代码的臃肿,使代码逻辑更清晰观察者模式:代理模式:门面模式:单例模式:生产者消费者模式: java语言的特点与OOP思想 这个通过对比来描述,比如面向对象和面向过程的对比,针对这两种思想的对比,还可以举个开发中的例子,比如播放器的实现,面向过程的实现方式就是将播放视频的这个功能分解成多个过程,比如,加载视频地址,获取视频信息,初始化解码器,选择合适的解码器进行解码,读取解码后的帧进行视频格式转换和音频重采样,然后读取帧进行播放,这是一个完整的过程,这个过程中不涉及类的概念,而面向对象最大的特点就是类,封装继承和多态是核心,同样的以播放器为例,一面向对象的方式来实现,将会针对每一个功能封装出一个对象,吧如说Muxer,获取视频信息,Decoder,解码,格式转换器,视频播放器,音频播放器等,每一个功能对应一个对象,由这个对象来完成对应的功能,并且遵循单一职责原则,一个对象只做它相关的事情 说下java中的线程创建方式,线程池的工作原理。 java中有三种创建线程的方式,或者说四种1.继承Thread类实现多线程2.实现Runnable接口3.实现Callable接口4.通过线程池线程池的工作原理:线程池可以减少创建和销毁线程的次数,从而减少系统资源的消耗,当一个任务提交到线程池时 首先判断核心线程池中的线程是否已经满了,如果没满,则创建一个核心线程执行任务,否则进入下一步 判断工作队列是否已满,没有满则加入工作队列,否则执行下一步 判断线程数是否达到了最大值,如果不是,则创建非核心线程执行任务,否则执行饱和策略,默认抛出异常 说下handler原理 Handler,Message,looper和MessageQueue构成了安卓的消息机制,handler创建后可以通过sendMessage将消息加入消息队列,然后looper不断的将消息从MessageQueue中取出来,回调到Hander的handleMessage方法,从而实现线程的通信。 从两种情况来说,第一在UI线程创建Handler,此时我们不需要手动开启looper,因为在应用启动时,在ActivityThread的main方法中就创建了一个当前主线程的looper,并开启了消息队列,消息队列是一个无限循环,为什么无限循环不会ANR?因为可以说,应用的整个生命周期就是运行在这个消息循环中的,安卓是由事件驱动的,Looper.loop不断的接收处理事件,每一个点击触摸或者Activity每一个生命周期都是在Looper.loop的控制之下的,looper.loop一旦结束,应用程序的生命周期也就结束了。我们可以想想什么情况下会发生ANR,第一,事件没有得到处理,第二,事件正在处理,但是没有及时完成,而对事件进行处理的就是looper,所以只能说事件的处理如果阻塞会导致ANR,而不能说looper的无限循环会ANR 另一种情况就是在子线程创建Handler,此时由于这个线程中没有默认开启的消息队列,所以我们需要手动调用looper.prepare(),并通过looper.loop开启消息 主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。 内存泄漏的场景和解决办法 1.非静态内部类的静态实例非静态内部类会持有外部类的引用,如果非静态内部类的实例是静态的,就会长期的维持着外部类的引用,组织被系统回收,解决办法是使用静态内部类2.多线程相关的匿名内部类和非静态内部类匿名内部类同样会持有外部类的引用,如果在线程中执行耗时操作就有可能发生内存泄漏,导致外部类无法被回收,直到耗时任务结束,解决办法是在页面退出时结束线程中的任务3.Handler内存泄漏Handler导致的内存泄漏也可以被归纳为非静态内部类导致的,Handler内部message是被存储在MessageQueue中的,有些message不能马上被处理,存在的时间会很长,导致handler无法被回收,如果handler是非静态的,就会导致它的外部类无法被回收,解决办法是1.使用静态handler,外部类引用使用弱引用处理2.在退出页面时移除消息队列中的消息4.Context导致内存泄漏根据场景确定使用Activity的Context还是Application的Context,因为二者生命周期不同,对于不必须使用Activity的Context的场景(Dialog),一律采用Application的Context,单例模式是最常见的发生此泄漏的场景,比如传入一个Activity的Context被静态类引用,导致无法回收5.静态View导致泄漏使用静态View可以避免每次启动Activity都去读取并渲染View,但是静态View会持有Activity的引用,导致无法回收,解决办法是在Activity销毁的时候将静态View设置为null(View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity)6.WebView导致的内存泄漏WebView只要使用一次,内存就不会被释放,所以WebView都存在内存泄漏的问题,通常的解决办法是为WebView单开一个进程,使用AIDL进行通信,根据业务需求在合适的时机释放掉7.资源对象未关闭导致如Cursor,File等,内部往往都使用了缓冲,会造成内存泄漏,一定要确保关闭它并将引用置为null8.集合中的对象未清理集合用于保存对象,如果集合越来越大,不进行合理的清理,尤其是入股集合是静态的9.Bitmap导致内存泄漏bitmap是比较占内存的,所以一定要在不使用的时候及时进行清理,避免静态变量持有大的bitmap对象10.监听器未关闭很多需要register和unregister的系统服务要在合适的时候进行unregister,手动添加的listener也需要及时移除 如何避免OOM? 1.使用更加轻量的数据结构:如使用ArrayMap/SparseArray替代HashMap,HashMap更耗内存,因为它需要额外的实例对象来记录Mapping操作,SparseArray更加高效,因为它避免了Key Value的自动装箱,和装箱后的解箱操作2.便面枚举的使用,可以用静态常量或者注解@IntDef替代3.Bitmap优化:a.尺寸压缩:通过InSampleSize设置合适的缩放 b.颜色质量:设置合适的format,ARGB_6666/RBG_545/ARGB_4444/ALPHA_6,存在很大差异c.inBitmap:使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张Bitmap在Heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小,但复用存在一些限制,具体体现在:在Android 4.4之前只能重用相同大小的Bitmap的内存,而Android 4.4及以后版本则只要后来的Bitmap比之前的小即可。使用inBitmap参数前,每创建一个Bitmap对象都会分配一块内存供其使用,而使用了inBitmap参数后,多个Bitmap可以复用一块内存,这样可以提高性能4.StringBuilder替代String: 在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”5.避免在类似onDraw这样的方法中创建对象,因为它会迅速占用大量内存,引起频繁的GC甚至内存抖动6.减少内存泄漏也是一种避免OOM的方法 说下Activity的启动模式,生命周期,两个Activity跳转的生命周期,如果一个Activity跳转另一个Activity再按下Home键在回到Activity的生命周期是什么样的 启动模式 Standard模式:Activity可以有多个实例,每次启动Activity,无论任务栈中是否已经有这个Activity的实例,系统都会创建一个新的Activity实例SingleTop模式:当一个singleTop模式的Activity已经位于任务栈的栈顶,再去启动它时,不会再创建新的实例,如果不位于栈顶,就会创建新的实例SingleTask模式:如果Activity已经位于栈顶,系统不会创建新的Activity实例,和singleTop模式一样。但Activity已经存在但不位于栈顶时,系统就会把该Activity移到栈顶,并把它上面的activity出栈 SingleInstance模式:singleInstance模式也是单例的,但和singleTask不同,singleTask只是任务栈内单例,系统里是可以有多个singleTask Activity实例的,而singleInstance Activity在整个系统里只有一个实例,启动一singleInstanceActivity时,系统会创建一个新的任务栈,并且这个任务栈只有他一个Activity 生命周期 onCreate onStart onResume onPause onStop onDestroy 两个Activity跳转的生命周期 1.启动A onCreate - onStart - onResume 2.在A中启动B ActivityA onPause ActivityB onCreate ActivityB onStart ActivityB onResume ActivityA onStop 3.从B中返回A(按物理硬件返回键) ActivityB onPause ActivityA onRestart ActivityA onStart ActivityA onResume ActivityB onStop ActivityB onDestroy 4.继续返回 ActivityA onPause ActivityA onStop ActivityA onDestroy onRestart的调用场景 (1)按下home键之后,然后切换回来,会调用onRestart()。(2)从本Activity跳转到另一个Activity之后,按back键返回原来Activity,会调用onRestart();(3)从本Activity切换到其他的应用,然后再从其他应用切换回来,会调用onRestart(); 说下Activity的横竖屏的切换的生命周期,用那个方法来保存数据,两者的区别。触发在什么时候在那个方法里可以获取数据等。 是否了SurfaceView,它是什么?他的继承方式是什么?他与View的区别(从源码角度,如加载,绘制等)。 SurfaceView中采用了双缓冲机制,保证了UI界面的流畅性,同时SurfaceView不在主线程中绘制,而是另开辟一个线程去绘制,所以它不妨碍UI线程;SurfaceView继承于View,他和View主要有以下三点区别:(1)View底层没有双缓冲机制,SurfaceView有;(2)view主要适用于主动更新,而SurfaceView适用与被动的更新,如频繁的刷新(3)view会在主线程中去更新UI,而SurfaceView则在子线程中刷新;SurfaceView的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha() View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢。SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快,Camera预览界面使用SurfaceView。GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用。 如何实现进程保活 a: Service设置成START_STICKY kill 后会被重启(等待5秒左右),重传Intent,保持与重启前一样b: 通过 startForeground将进程设置为前台进程, 做前台服务,优先级和前台应用一个级别,除非在系统内存非常缺,否则此进程不会被 killc: 双进程Service: 让2个进程互相保护对方,其中一个Service被清理后,另外没被清理的进程可以立即重启进程d: 用C编写守护进程(即子进程) : Android系统中当前进程(Process)fork出来的子进程,被系统认为是两个不同的进程。当父进程被杀死的时候,子进程仍然可以存活,并不受影响(Android5.0以上的版本不可行)联系厂商,加入白名单e.锁屏状态下,开启一个一像素Activity 说下冷启动与热启动是什么,区别,如何优化,使用场景等。 app冷启动: 当应用启动时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用, 这个启动方式就叫做冷启动(后台不存在该应用进程)。冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。 app热启动: 当应用已经被打开, 但是被按下返回键、Home键等按键时回到桌面或者是其他程序的时候,再重新打开该app时, 这个方式叫做热启动(后台已经存在该应用进程)。热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application 冷启动的流程当点击app的启动图标时,安卓系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后会依次创建和初始化Application类、创建MainActivity类、加载主题样式Theme中的windowBackground等属性设置给MainActivity以及配置Activity层级上的一些属性、再inflate布局、当onCreate/onStart/onResume方法都走完了后最后才进行contentView的measure/layout/draw显示在界面上 冷启动的生命周期简要流程:Application构造方法 –> attachBaseContext()–>onCreate –>Activity构造方法 –> onCreate() –> 配置主体中的背景等操作 –>onStart() –> onResume() –> 测量、布局、绘制显示 冷启动的优化主要是视觉上的优化,解决白屏问题,提高用户体验,所以通过上面冷启动的过程。能做的优化如下: 减少onCreate()方法的工作量 不要让Application参与业务的操作 不要在Application进行耗时操作 不要以静态变量的方式在Application保存数据 减少布局的复杂度和层级 减少主线程耗时 为什么冷启动会有白屏黑屏问题?原因在于加载主题样式Theme中的windowBackground等属性设置给MainActivity发生在inflate布局当onCreate/onStart/onResume方法之前,而windowBackground背景被设置成了白色或者黑色,所以我们进入app的第一个界面的时候会造成先白屏或黑屏一下再进入界面。解决思路如下 1.给他设置windowBackground背景跟启动页的背景相同,如果你的启动页是张图片那么可以直接给windowBackground这个属性设置该图片那么就不会有一闪的效果了 <style name=``"Splash_Theme"` `parent=``"@android:style/Theme.NoTitleBar"``>` <item name=``"android:windowBackground"``>@drawable/splash_bg</item>` <item name=``"android:windowNoTitle"``>``true``</item>` </style>` 2.采用世面的处理方法,设置背景是透明的,给人一种延迟启动的感觉。,将背景颜色设置为透明色,这样当用户点击桌面APP图片的时候,并不会"立即"进入APP,而且在桌面上停留一会,其实这时候APP已经是启动的了,只是我们心机的把Theme里的windowBackground的颜色设置成透明的,强行把锅甩给了手机应用厂商(手机反应太慢了啦) <style name=``"Splash_Theme"` `parent=``"@android:style/Theme.NoTitleBar"``>` <item name=``"android:windowIsTranslucent"``>``true``</item>` <item name=``"android:windowNoTitle"``>``true``</item>` </style>` 3.以上两种方法是在视觉上显得更快,但其实只是一种表象,让应用启动的更快,有一种思路,将Application中的不必要的初始化动作实现懒加载,比如,在SpashActivity显示后再发送消息到Application,去初始化,这样可以将初始化的动作放在后边,缩短应用启动到用户看到界面的时间 Android中的线程有那些,原理与各自特点 AsyncTask,HandlerThread,IntentService AsyncTask原理:内部是Handler和两个线程池实现的,Handler用于将线程切换到主线程,两个线程池一个用于任务的排队,一个用于执行任务,当AsyncTask执行execute方法时会封装出一个FutureTask对象,将这个对象加入队列中,如果此时没有正在执行的任务,就执行它,执行完成之后继续执行队列中下一个任务,执行完成通过Handler将事件发送到主线程。AsyncTask必须在主线程初始化,因为内部的Handler是一个静态对象,在AsyncTask类加载的时候他就已经被初始化了。在Android3.0开始,execute方法串行执行任务的,一个一个来,3.0之前是并行执行的。如果要在3.0上执行并行任务,可以调用executeOnExecutor方法 HandlerThread原理:继承自Thread,start开启线程后,会在其run方法中会通过Looper创建消息队列并开启消息循环,这个消息队列运行在子线程中,所以可以将HandlerThread中的Looper实例传递给一个Handler,从而保证这个Handler的handleMessage方法运行在子线程中,Android中使用HandlerThread的一个场景就是IntentService IntentService原理:继承自Service,它的内部封装了HandlerThread和Handler,可以执行耗时任务,同时因为它是一个服务,优先级比普通线程高很多,所以更适合执行一些高优先级的后台任务,HandlerThread底层通过Looper消息队列实现的,所以它是顺序的执行每一个任务。可以通过Intent的方式开启IntentService,IntentService通过handler将每一个intent加入HandlerThread子线程中的消息队列,通过looper按顺序一个个的取出并执行,执行完成后自动结束自己,不需要开发者手动关闭 ANR的原因 1.耗时的网络访问2.大量的数据读写3.数据库操作4.硬件操作(比如camera)5.调用thread的join()方法、sleep()方法、wait()方法或者等待线程锁的时候6.service binder的数量达到上限7.system server中发生WatchDog ANR8.service忙导致超时无响应9.其他线程持有锁,导致主线程等待超时10.其它线程终止或崩溃导致主线程一直等待 三级缓存原理 当Android端需要获得数据时比如获取网络中的图片,首先从内存中查找(按键查找),内存中没有的再从磁盘文件或sqlite中去查找,若磁盘中也没有才通过网络获取 LruCache底层实现原理: LruCache中Lru算法的实现就是通过LinkedHashMap来实现的。LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系,对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还做些调整Entry顺序链表的工作。LruCache中将LinkedHashMap的顺序设置为LRU顺序来实现LRU缓存,每次调用get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用put插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。 说下你对Collection这个类的理解。 Collection是集合框架的顶层接口,是存储对象的容器,Colloction定义了接口的公用方法如add remove clear等等,它的子接口有两个,List和Set,List的特点有元素有序,元素可以重复,元素都有索引(角标),典型的有 Vector:内部是数组数据结构,是同步的(线程安全的)。增删查询都很慢。ArrayList:内部是数组数据结构,是不同步的(线程不安全的)。替代了Vector。查询速度快,增删比较慢。LinkedList:内部是链表数据结构,是不同步的(线程不安全的)。增删元素速度快。 而Set的是特点元素无序,元素不可以重复HashSet:内部数据结构是哈希表,是不同步的。Set集合中元素都必须是唯一的,HashSet作为其子类也需保证元素的唯一性。判断元素唯一性的方式:通过存储对象(元素)的hashCode和equals方法来完成对象唯一性的。如果对象的hashCode值不同,那么不用调用equals方法就会将对象直接存储到集合中;如果对象的hashCode值相同,那么需调用equals方法判断返回值是否为true,若为false, 则视为不同元素,就会直接存储;若为true, 则视为相同元素,不会存储。如果要使用HashSet集合存储元素,该元素的类必须覆盖hashCode方法和equals方法。一般情况下,如果定义的类会产生很多对象,通常都需要覆盖equals,hashCode方法。建立对象判断是否相同的依据。 TreeSet:保证元素唯一性的同时可以对内部元素进行排序,是不同步的。判断元素唯一性的方式:根据比较方法的返回结果是否为0,如果为0视为相同元素,不存;如果非0视为不同元素,则存。TreeSet对元素的排序有两种方式:方式一:使元素(对象)对应的类实现Comparable接口,覆盖compareTo方法。这样元素自身具有比较功能。方式二:使TreeSet集合自身具有比较功能,定义一个比较器Comparator,将该类对象作为参数传递给TreeSet集合的构造函数 说下AIDL的使用与原理 aidl是安卓中的一种进程间通信方式说下你对广播的理解说下你对服务的理解,如何杀死一个服务。服务的生命周期(start与bind)。是否接触过蓝牙等开发设计一个ListView左右分页排版的功能自定义View,说出主要的方法。-说下binder序列化与反序列化的过程,与使用过程是否接触过JNI/NDK,java如何调用C语言的方法-如何查看模拟器中的SP与SQList文件。如何可视化查看布局嵌套层数与加载时间。你说用的代码管理工具什么,为什么会产生代码冲突,该如何解决说下你对后台的编程有那些认识,聊些前端那些方面的知识。说下你对线程池的理解,如何创建一个线程池与使用。说下你用过那些注解框架,他们的原理是什么。自己实现过,或是理解他的工作过程吗?说下java虚拟机的理解,回收机制,JVM是如何回收对象的,有哪些方法等一些java与Android源码相关知识等大学成绩大学那些专业,你哪方面学得好单片机,嵌入式,电子线路。毕业设计什么,几个人实现的,主要功能是什么还有些其他硬件相关知识自己的职业规划与发展方向

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。