onSaveInstanceState方法只会在Activity被异常终止的情况下调用。销毁时把Activity在onSaveInstanceState方法中保存的Bundle对象作为参数传递给onRestoreInstanceState和onCreate方法。对于时间的调用:onSaveInstanceState在onStop之前,onRestoreInstanceState在onStart之后。在这两个方法中,系统默认为我们做了一定的恢复操作,比如保护和恢复View的层次结构,用户输入的数据,LisView的滚动位置等。每一个View中都有这俩个方法。
通过Application去启动Standard模式的Activity的时候会报错,因为Application没有任务栈,解决的方法是设置一个标志位,这样启动的时候会为新的Activity以singleTask的模式启动。singleTask模式下如果被启动的Activity没有找到活动栈,就会新创建一个。
TaskAffinity 标识了一个Activity所需的任务栈的名字。任务栈也分为前台任务栈和后台任务栈
隐式调用的匹配规则:Action:必须有一个完全一致。 Category:Intent一旦设置Category,那么必须与规则中的任何一个category相同,当然,如果不设置category也可以通过。前提是过滤规则中必须加上DEFAULT这个规则
Binder通信中由于反序列化之后不是同一个对象,解注册会失败,这时候就需要用到RemoteCallbackList来存储客户端传来的注册的接口
如果明确知道某一个远程方法是耗时的,那么就在子线程执行,而如果是在UI线程去发起的远程请求的话,会导致ANR。所以最好避免客户端通过UI线程去访问远程方法。
Binder权限验证的方法 通过自定义permission,在onBind方法中去检查权限,或者是在binder类的onTransact方法中做验证,通过得到uid和pid。可以做一些验证,比如验证包名
View的绘制流程与Activty的生命周期不同步,所以不能简单的在生命周期中去获取View的宽高,可以使用View.post,或者onWindowFocusChanged回调接口。
记忆集的设计是为了解决跨代引用的问题。卡表是记忆集的一种实现。JVM通过写屏障,在引用赋值操作生成更新卡表的操作。
解决并发标记所产生的问题:增量更新和原始快照,也是应用写屏障,分别记录下删除引用和添加引用的操作,在GC Roots 树遍历结束后,以刚才改变地方为GC Roots,重新开始遍历。
垃圾收集器:
类加载机制:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。在类加载过程中,会经历:加载,验证,准备,解析,初始化,使用,卸载这几个生命周期
加载过程需要完成三件事情:1、通过类的权限定名获取类的二进制字节码字节流。2、将字节流代表的静态存储结构转化成方法区的运行时数据结构 3、生成类的class对象。 其中获取二进制流字节码文件是我们开发人员可控性最强的阶段
验证阶段的四个过程:1.文件格式验证:保证字节流能正确解析并存储于方法区内 2、 元数据验证:使其符合Java语法,比如不能继承fianl类。 3、对类的方法体(也就是Code属性)进行校验分析。 4、 符号引用验证,确保自己访问的资源可以访问到,不是private的。
准备阶段:为类中的变量分配内存并设置类变量初始值(如果是final static修饰的常量则直接赋值)。本应该在方法区中分配内存,但JDK8之后类变量随着Class对象一起存放在堆内存中(元空间),元空间使用的是本地内存,原则上和堆区共享内存区域。
解析阶段:将常量池内的符号引用替换为直接引用的过程,直接引用可以是一个指针、相对偏移量或者是能定位到目标的句柄。分为类解析、字段解析、方法解析、接口方法解析。本质都是对继承树自下而上进行搜索,找到具体的目标后返回对这个目标的引用。
初始化阶段,就是执行类构造器方法的过程,类构造器是javac编译器自动生成的产物。由静态代码块和类变量的赋值语句组成,顺序按照源代码申明的顺序开始。静态语句块只能访问到定义在静态语句块之前的变量。定义在他之后的变量,只能赋值,不能访问。最先执行方法的类一定是Object。clinit必须同步处理。
类加载器:通过一个类的权限定名来获取描述该类的二进制字节流,实现这个动作的代码被称为“类加载器”。
双亲委派模式:从JVM的角度看只有两种类加载器,启动类加载器(C++实现)和其他继承自类ClassLoader的java实现的类加载器。从开发人员的角度看Java类加载机制的话保持着三层类加载器、双亲委派的类加载架构:1.顶层:启动类加载器,负责加载lib包下的类库,用户不能直接引用。如果想把加载请求给引导类加载器去处理,那直接使用null代替即可。2. 拓展类加载器:负责加载lib\ext目录的类库,开发者可以直接使用这个类加载器。 3. 应用程序类加载器,是ClassLoader.getSystemClassLoader的返回值,负责加载用户类库上的所有java文件。
双亲委派模式的工作过程:当一个类加载器收到请求时,自己本身先不去处理,而是将请求委托给上一级,每一级都是如此。直到顶层类加载器,只有当父加载器反馈自己无法加载这个类,再讲请求向下传递。 作用:为了保证不同的类加载器去执行加载同一个class文件的请求时,能将请求传递给同一个类加载器,这样加载的类才是同一个类。保证了Java中的秩序不混乱,越基础的类交给越上层的类加载器
通过线程类加载器可以破坏双亲委派模式,当上层的基础类想要去加载底层类时,上层的类加载器是无法加载下层的类,这是只能破坏双亲委派模式,由上层类加载器去调用下层的类加载器。
JDK9模块化增加了一个平台化接口,替代了拓展类加载器。继承结构也发生了改变,三个类加载器都继承自BuiltinClassLoader。双亲委派模式还在维持,但是有一点发生变化,当平台类或者应用程序类加载器收到请求时,会在向上委托请求前,先判断一下该类是否能够归属到一个系统模块中。
Java 内存模型:所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作必须在工作内存进行,不能直接读写主内存中的数据。不同线程也不能直接访问对方工作内存中的变量。线程间变量的传递均需要通过主内存来完成。
volatile Java虚拟机提供的最轻量级的同步机制。它具有两个特性1:、保证了修饰的变量具有可见性 2、禁止指令重排序优化。Java 内存模型针对volatile的特殊规则:load、read动作相关联,必须连续且一块出现,store和write动作相关联,必须连续且一起出现。
Java内存模型定义了一些很常见的先行发生原则,比如start操作早于线程的每一个动作等等,这个先行发生原则和时间的先后没有关系。
主流的虚拟机每一个线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构。虚拟机自己是不会去干涉线程调度。
1. 主线程里的looper的无限循环为什么不会堵塞主线程,是不是非常会消耗系统资源?
2. 系统为什么不允许子线程中访问UI?
因为Android的UI控件是线程不安全的,如果多线程并发访问可能会导致UI控件出于不可预期的状态。
3. Handler是如何能够线程切换的
本质是因为线程间可以对内存共享,这样主线程和子线程和与对同一个主线程创建的Handler访问,子线程通过在主线程创建的mHandler,发送一个消息到MessageQueue中,然后再由loop循环取出消息,再由handler去处理这条消息,这个过程其实就已经是运行在主线程了。
4.说一下你对Android的事件分发机制的理解
常说的事件分发机制,一般指的是
5. 说一下关于屏幕适配的理解
dpi/dp = denist (设备无关)
dpi表示设备一英寸所含像素的个数
dp = px * 160 / dpi;
dpi / 160 = density
dp = px / denisy
早期的屏幕适配分为两种,一种是简单的通过dp加上布局的一些属性来简单的适配屏幕,这种会不可避免的导致一些手机上面的显示大小效果不一样,需要单独适配。第二种是通过宽高限定符写values文件,这种方法是没有容错率
之后的smartwidth适配,就是通过sw这个值。也是通过写values文件,每个values文件制定sw值,多少多少dp,Android 获取到当前屏幕的宽高之后,会针对sw的值向下取整。这样的方案是比较好的,有容错率,而且适配比较稳定,缺点就是dimens可能写太多占用内存。
5. Java 中new一个对象经历哪些过程
类加载》分配堆内存空间》空间初始化为0》调用方法构造函数
6. JVM根节点枚举
根节点枚举必须要STW,如果遍历所有引用去找GC Roots,很耗时吗。非常影响用户体验,所以就引出了OopMap这个数据结构,JVM是有办法直接得到哪些地方存放着对象引用的,记录下栈里和寄存器里哪些位置是引用。这样收集器就能直接知道这些信息。
因为很多条指令都会改变引用关系,就会导致OopMap变化,如果为每一条指令都生成OopMap,那么会非常消耗空间成本。所以就有了安全点的说法,线程必须运行到安全点才能暂停用户线程去执行GC。所以OopMap只需要在安全点保存引用关系即可。(主动式中断)。而为了解决用户线程不执行代码,就不发遇到安全点的问题,就引用了安全区域,确保在某一段代码片段中,引用关系不会发生变化。在安全区域中,只有完成根节点枚举操作的线程,才能离开安全区域。
7. 触发类初始化的场景有哪些
接口的初始化与类的初始化相仿,但是第三条不一样,接口初始化时不会初始化父接口
8. ThreadLocal 实现原理
TheadLocal是一个实现线程隔离数据的类,通过一个ThreadLocal可以在不同的线程之间存储数据,不同线程之间存储的数据互不影响。
实现原理:ThreadLocal最主要的就是get、set方法,而这两个方法其实都是对ThreadLocal的内部类ThreadLocalMap进行的操作,ThreadLocal只是一个空壳,而实际的逻辑都在ThreadLcoalMap中。ThradLocalMap的内部类Entry代表着一个键值对,key是Threadlocal(弱引用),value是我们自己设置的值。之后将这个entry存储到table[]数组中,这个Entry在数组中的位置是通过ThreadLocal的hashcode与数组长度按位与计算出的。那为什么这里可以实现存储数据线程隔离呢?因为ThreadLocalMap的实例是直接存储在Thread类中的(初始化的时机是第一次调用set方法)。之后通过不同线程拿到的Map也是当前线程自己的Map,多个线程就有多个ThreadLocalMap的实例,当然可以实现线程隔离。
9. Java 的线程的实现
在主流的虚拟机中,每一个Java线程都是直接映射到一个操作系统原生线程来实现的,是1:1的内核线程模型。切换和调度的成本较高,因为要从用户态和内核态之间进行切换。中间没有额外的结构。所以HotSpot自己是不会去干涉线程调度的。线程的调度方式分为协同式和抢占式:协同式的线程执行由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另一个线程上去。这样做会导致一个线程不告知系统进行线程切换,程序就会一直堵塞。抢占式调度是由系统来分配时间。当一个进程出了问题还能通过任务管理器杀掉,java是采用抢占式。
10.悲观锁与乐观锁?
互斥同步实际上是一种悲观锁,开发中常用的给线程加锁也是互斥同步。其总是认为只要不去做正确的同步措施,那么就肯定会发生问题。
乐观锁:随着硬件指令集的发展,产生了乐观锁:基于冲突检测的乐观并发策略,通俗的说不管风险,先进行操作,之后在进行检测,如果数据确实不正确,那么就进行其他的补偿策略,常用的做法就是不断重试,直到成功:对应到Java中的线程同步操作,就是Atomic原子类,我们直到多个线程对同一个变量进行自增操作是会出问题,出现的最后总数应该小于预期的值。而Atomic保证了线程安全,在Atomic的自增方法中就是使用了CAS,这个操作在底层必须是原子性的,这样才能保证检测有意义:incrementAndGet方法在一个无限循环中,不断尝试将一个比当前值大一的数赋值给自己,如果失败了,那就说明在执行CAS的操作时,就是已经发生了改变,于是就再次循环,直到设置成功为止,这就是典型的乐观锁,简单高效。
11. 锁优化机制
12. 说一下线程池的运作过程,有哪几种种类,他们的区别,以及实现原理
运作工程:
有哪几个种类
new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
SynchronousQueue
这个队列容量为1,当一个线程插入元素时,被堵塞,直到另外一个线程取出元素,取出元素的线程如果队列为空,也回堵塞。所以每当一个task到来,这个线程池就会直接创建新线程,新线程可以存活60s。这60s内线程可以复用。 new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
13. 线程池的拒绝策略
当前任务超过
拒绝策略是通过RejectedExecutionHandler接口来实现的,当构造线程池的时候最后一个参数就是这个接口,通过覆写rejectedExecution方法可以自定义拒绝策略
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
2.CallerRunsPolicy
只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
丢弃即将被执行的任务,并尝试再次提交当前任务。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
14. Minor GC和Full GC触发条件
Minor GC 的触发条件
Full GC 的触发条件
15. Synchronized 实现原理
这要分为Synchronized修饰代码块和修饰方法:
修饰方法:
在方法常量池的方法表结构中设置了ACC_SYNCHRONIZED标志位,JVM通过该标志位实现了方法的同步。调用该方法时,如果JVM监测到ACC_SYNCHRONIZED标志位,执行线程就可以获取到monitor,获取成功之后才能执行方法体,方法结束之后再释放monitor。
修饰代码块:
通过在class文件中代码块的前后分别加上字节码指令:monitorenter和monitorexit,执行enter指令时,会获取对象的锁,如果对象的锁没有锁定或者当前线程已经获取了那个对象的锁,锁的计数器就加1,反之减1,减为0就释放锁,如果获取锁对象失败,那么就进入堵塞状态。
16. HashMap实现原理
HashMap,本质是通过哈希表+拉链法解决冲突的方式来实现的
不同于JDK1.7的数组 + 链表的数据,JDK1.8使用了数组 + 链表 + 红黑树。下图是JDK1.7的图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F4pmS0Rk-1589361738547)(80DCC6C9888541E8920C8B05E24D4A29)]
HashMap使用Entry/Node来存储数据,每一个Node都是一个链表结点,通过Key计算出的hash值并进行位运算hash & (length-1)
,得出存储数组的下标,如果没有冲突,直接存入,如果发生冲突,就通过尾插法插入数组响应结点的链表,对于HashMap的默认参数,数组大小为16,负载因子为0.75。当当前数组的size(不为null的数量)超过数组大小 * 负载因子,那么就会进行扩容,扩容的代价相当大,会新建一个两倍大小的数组,将元素重新散列,所以负载因子不易太小,当然也不能太大,那样会导致大量的哈希冲突,导致链表很长。而且HashMap规定了数组的大小必须是2的n次方。JDK8中当链表长度超过8个时会转换成红黑树,Node结点转换为TreeNode。
17. ConcurrentHashMap实现原理
在JDK1.7 ,实现原理是通过segment数组与多个HashEntry,每一个segment 数组存储的是HashEntry数组 + 链表。ConcurrentHashMap要通过两次hash来找到元素。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SScYrUA6-1589361738554)(3570C3AA8E684D24A0AB989209A525F4)],有特色的put操作,其中用到CAS和ryLock,自旋锁的应用。
在JDK1.8,ConcurrentHashMap中Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap
18. LruCache 实现原理
首先LruCache是内存缓存类,根据最近最少使用算法,当缓存满时,先删除最少使用的元素。使用时先覆写sizeOf方法,放回每一次put键值对对应的占内存的大小。LruCache其实只有300多行代码,最近最少使用算法的实现都在LinkListHashMap中,LinkedHashMap在HashMap原有的结构上,通过一个链表来保存插入顺序,通过将构造函数中的accessOrder指定为true,LinkedhashMap每次通过get方法查询数据时就会将该数据插入链表的尾部,每次put方法后会调用trimSize方法,这个方法会先删除队头,这样就实现了最少使用算法。
19. 说一下Java 泛型的理解
JDK1.5提出来的概念,简化了我们的代码,在编译阶段保证代码的安全性,泛型实现了参数化类型的概念,使得类型可以作为参数适用于尽可能多的场景List>
相当于泛型类的父类,它可以去接受任何泛型类型的List。比如像下面这样:一个方法想要接受类型参数为man或者woman的List,那么只能这么写:
public void print(List<? extends Person> list) {
...
}
20. Java 反射
每次需要获取Class的对象时都使用Class.forName方法,或者需要调用Class对象上的方法时都调用getDeclaredMethod(String name, Class>... parameterTypes)或getMethod(String name, Class>… parameterTypes)方法获取Method对象,再调用其上的invoke(Object obj, Object… args)方法。,这样非常消耗资源,lass.forName方法的调用会执行Class类文件在整个类路径下的搜索,频繁调用比较影响性能。
Class对象上的getDeclaredMethod (String, Class>...)或getMethod(String, Class>…)方法的调用会执行Method对象在Class对象上的搜索,所以解决方法就是缓存。
21. SP的commit和apply的区别
每个SP实例对应着一个XML文件,sSharedPrefsCache存储的是File和SharedPreferencesImpl键值对,当加载SP时,在SP的构造方法中会先开启一个子线程将对应的XML数据加载到SP实例中。对于commit方法,一般情况下他的写入磁盘的操作会发生在当前线程,但是要先判断一下mDiskWritesInFlight == 1,也就是当前正在执行的写入磁盘的操作为1。否则还是通过QueuedWork异步执行。所以commit有返回值,同步堵塞知道有返回结果。apply()方法就是通过QueuedWork异步执行,他没有返回值。他发送消息有一个0.1s短暂的延迟,避免了频繁的磁盘写入操作
22. View绘制流程的起点
当Activity启动时,ActivityThead会调用handleResumeAcitivity,这个方法中会执行onResume()生命周期的回调,然后会将decorView与Activity的phoneWindow通过WindowManager建立连接,之后会创建ViewRoot对象,将其成员变量mView设置为decorview,然后通过一系列调用最终在ViewRootImpl的performTravaalse开始绘制流程。mView,也就是decorView是绘制的起点。
23. HTTP协议版本比较
24. HTTPs和HTTP的区别
23. TCP三次握手四次挥手(图片转载)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XjHFqIX1-1589361738563)(C97FB6A2DBE143B891067EFE65CB09A1)]
假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,“告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,“告诉Client端,好了,我这边数据发完了,准备好关闭连接了”。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,“就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!