JAVA:
- ArrayList源码实现?list线程安全?
arraylist底层是数组实现,有三个构造函数,分别是无参构造函数,初始化size构造函数,初始化数组构造函数,扩容方式是判断当前大小的1.5倍与最小值比较如果大于最小值就取前者,小于最小值就去最小值,arraylist线程不安全的,实现了4个接口1个抽象类;
list线程安全的是Collections .synchronizedList和CopyOrWriteArrayList,Vector;
综合考量Collections.synchronizedList的读,写,遍历性能最佳;CopyOrWriteArrayList写的性能最差;Vector遍历性能最差;
Collections.synchronizedList是对方法块加synchronized锁,不仅仅实用与ArrayList,其他list也可以,但是遍历的时候没有加,要手动处理;Vector是对所有add,remove等方法加锁;CopyOrWriteArrayList是每次add操作的时候做copy操作,最终是读写分离,最终一致的原则。
- HashMap实现?线程安全?
hashmap里面是数组结构,当出现hash冲突的时候采取的是链地址法,也就是节点链表的结构,当链表长度>=8,数组长度>=64的时候会把链表转化成红黑树结构,当链表长度>=8,数组长度<64的时候会优先进行数组扩容,如果红黑树大小<6,会自动转化成链表。hashmap中put()方法调用的是putVal(),putVal的时候会对key进行hash扰动,通过key的hashcode和数组长度进行位运算,减低hash冲突概率(也就是减低不同key得到同样的数组下标),也可以用再hash法等,putVal方法中主要是对数据插入,判断key是否存在,不存在则创建,存在则判断结构是node还是treenode,也就是判断当前key节点是链表还是红黑树结构进行处理操作,最后还会调用排序相关的函数afterNodeInsertion(),这个函数就是用于LinkHashMap排序操作的。
map线程安全的是ConcurrentHashMap和Collections.synchronizedMap,hashtable。Collections.synchronizedMap,hashtable实现类似,都是对所有方法加锁,但是如果在遍历的时候操作map会抛出异常;ConcurrentHashMap则完美解决这个问题,但是ConcurrentHashMap是一个hashmap,他是针对于节点进行加锁的方式,实现线程访问节点互不干扰,可以边读边写,并且读的时候不需要锁(hashtable等式对整个数组加锁,读写不能同时操作),Collections.synchronizedMap可以作用于所有的map线程安全操作。
- 红黑树?
首先,红黑树是平衡二叉树和2-3-4树的转变,二叉树(左边节点始终小于父节点,右边节点始终大于父节点,不足,容易出现不平衡状态,比如当前所有节点都比根节点小,就会形成链状结构),2-3-4树(2树节点有一个值,3树节点有两个值,4树节点有3个值,2树转红黑树作为子节点,3树转红黑树经过左倾或者右倾拆分值,4树形成红黑树标准格式一父黑二子红);
红黑树满足5个条件:a. 所有节点都是红黑两种类型,b. 所有叶节点都是黑节点,c. 根节点始终是黑节点,d. 每一条分支的黑节点数相等,e. 不可能出现连续两个红节点。在删除和插入的时候通过变化颜色和左旋右旋调整平衡达到目的。
- 泛型?
说到泛型第一想到的是list,我们再使用list的时候都会用到泛型,当然还有很多其他地方,我们知道泛型可以应用于类,方法,变量,泛型分协变,逆变和不变,协变对应我们的<? extend Work>,可读不可写,因为不确定具体实现类型是什么,可能会出现类型转换异常;逆变对应我们的<? super Work>,可写不建议读,同样,不确定类型;如果我们只读不写就使用extend边界,只写不读则用super边界更合适,如果可读可写则不适用任何边界,在我们使用中java是泛型擦除的,就是在运行时相同对象不同泛型参数获取类型一样,泛型参数会被擦除,对比而言kotlin则可以泛型实化,kotlin泛型实化操作可以使用inline修饰函数和reified关键字修饰泛型实现内联达到泛型实化的目的。kotlin中边界字符也和java有区别,分别对应out和in。
- List<?>和List<Object>区别:
List<?>可以有很多子类,List<Object>仅仅限制Object。
- 抽象类和接口区别?
抽象类abstract修饰,接口用interface修饰,抽象类可以包含抽象方法,普通方法,构造函数,抽象变量,方法可以是不同的范围权限,接口中方法智能是抽象的,并且默认是public abstract的,没有构造函数,变量是public static final修饰的,一个实现类可以实现多个接口但是只能单继承,接口主要用于架构实现的类别,抽象类主要用于抽象出公共实现。
- 线程和线程池,多线程并发?
线程:进程中的最小单元,依赖于CPU用于依次执行每一行指令。java中使用线程有两种方式,第一:
new Thread().start()
在start的时候去启动线程并且执行thread
中的run()
方法;第二:new Thread(new Runnable())
,传入runnable
对象,调用start
的时候执行传入的runnable
中的run()
函数。
线程池:用于管理进程中线程数量和启动的,线程池对线程管理处理分两方面,一方面是管理任务,另一方面是管理线程,当一个线程池中没有任务也没有线程的时候,获取到任务会去创建新的线程,如果线程池中线程数已经达到核心线程数的时候,新进任务会放入到任务队列中等待,当任务队列任务数满之后会判断当前线程池的最大线程数是否大于核心线程数,如果大于则创建新线程,当达到最大线程数之后,还有任务则线程池会采取拒绝策略,不在接收任务。java线程池默认定义有四种管理策略,1. 单线程策略,顾名思义,线程池只管理和创建一个线程,可以保证任务按顺序执行;2. 指定核心线程数和最大线程数,当达到设定值的时候任务队列等待或者拒绝等策略;3. 不做限时;4. 延时队列处理,顾名思义,对线程启动做延时排序。
多线程并发:首先提起多线程并发,则可以分为两个方面去理解,第一成员变量并发和函数代码块并发操作,函数并发,常见的两种方式是加synchronized和使用Lock锁的方式,在加锁的过程中当线程当一个线程释放锁之后会涉及到线程GC或者挂起的情况,如果当前线程还有存在的必要,则需要线程挂起,如果我们使用synchronized加锁方式则会涉及到Object中的wait()和notify(),notifyAll()函数,对加锁的函数或者方法块进行wait()操作,当前线程被其他线程再次唤醒的时候,则线程会继续唤醒位置开始执行。Lock加锁的方式需要手动创建锁,启动锁和销毁锁,并且线程等待和唤醒操作的时候也是用到Lock中获取Condition对象进行等待和唤醒操作,相对于synchronized而言Lock锁的优势是可以指定等待和唤醒操作。多线程过程中需要注意一点是sheep和wait的区别,sheep操作是不会释放锁的。第二就是成员变量的并发操作,多线程中要满足成员变量并发操作必须满足三点,原子性,可见性和有序性,首先我们要理解程序在运行中对变量改变的过程是其实是改变之后会把改变值存入当前线程的高速缓存中,由缓存存入到物理内存中,每个线程有各自的缓存,所以在同一个对象面对两个或者多个线程操作的时候,可能会出现缓存不一致和代码重排的情况,从而导致并发问题。例如当前两个线程同时对int i = 0,做i++的操作,如果出现缓存不一致的情况就会出现执行完毕之后i值只加了1,这是因为两个线程同时并发,缓存到自己的值都是0,并没有缓存到最新值。
volatile:作用是保证变量的可见性和有序性,但不能保证原子性,Amotic可以保证变量原子性。volatile保证可见性,是因为他可以让修改立即体现在物理内存,并且使得其他线程的该值告诉缓存失效,其他线程高速缓存失效后则就会读取到最新的物理内存中的值,volatile保证有序性,是因为volatile修饰的变量能保证在包含它的代码之前的代码完全执行完毕,之后的代码不会和它进行指令重排操作。
- ThreadLocal?
ThreadLocal主要作用是线程与线程之间数据隔离,需要注意的是ThreadLocal在set数据的时候其内部实现是为对当前线程的ThreadLocalMap进行数据设置,ThreadLocalMap是一个数组,key是当前的ThreadLocal的弱引用,value是我们传入的Object,ThreadLocal容易出现内存泄漏,因为我们不确定线程的生命周期,如果线程结束之后key因为是弱引用会被GC回收,然而value值是强引用就会导致内存泄漏,解决方法,第一可以线程结束后主动调用ThreadLocal的remove方法,第二使用final static修饰创建的ThreadLocal对象(推荐使用)。
- GC回收机制?
说起GC机制的时候首先得了解内存分配,java中内存分配主要分为共享区域(方法区,堆),线程私有区域(jvm虚拟机栈,本地方法栈,程序计数器);方法区:用于储存类相关信息,常量,静态变量等,堆:用于储存对象实例,分配空间,也是GC主要作用区域,jvm虚拟机栈:用于java方法执行,每一个方法执行的时候会在jvm虚拟机栈中生成一个栈帧,栈帧保存并且执行方法每一行指令并且返回方法值,本地方法栈:用于执行native函数,程序计数器:用于记录当前线程执行的函数,如果执行的是java方法,则记录的是字节码地址,如果是native方法则记录的是空。说完内存分配,然后就是我们GC过程中的策略,我们GC过程中大致分使用四种算法对对象进行回收,标记-清除,标记-整理,复制,年代记录四种。标记-清除算法:优点是检查消耗低,缺点是容易出现内存碎片,导致对接下来的内存分配影响;标记-整理算法:是在标记-清除算法基础上的优化,优点是不会存在前者的内存碎片问题,缺点是较为复杂,因为会做整理操作,把可用的进行整理;复制算法:优点是不会有内存碎片,缺点是会极大的降低内存使用率;年代记录算法:年代记录算法主要分为老年代和新生代,老年代每次回收率较低,新生代回收率较高,老年代使用标记-清除和标记-整理两种算法对其中的内存做优化,新生代用复制算法对内存进行整理。
- 类加载机制?
在java虚拟机中一个类加载过程分7步,加载-连接(验证-准备-解析)-初始化-使用-卸载。java中存在4中类型的类加载器,分别是BootstrapClassLoader,ExtClassLoader,AppClassLoader,UserClassLoader(自定义ClassLoader)。类在选择加载的过程中使用的是双亲委托机制进行加载的,也就是说通过向上委托父类的加载范围去寻找是否可加载,如果父类不可加载则再子类查找是否可加载,如果也无法查找到该类则抛出ClassFindExeption的异常。android虚拟机和java虚拟机大致相同,只是java虚拟机加载的是.class文件,android虚拟机加载的是.dex文件,android中的ClassLoader分别是PathClassLoader,DexClassLoader。
- TCP和UDP?
首先介绍区别:1. TCP是有连接的,UDP是无连接的;2. TCP可保证数据正确性和顺序,UDP容易出现丢包和不保证顺序;3. TCP主要针对于字节流传输,传输量大的数据,UDP是报文,传输数据量小;4. TCP请求头至少20个字节,UDP则只需要8个字节;5. TCP拥塞可控(拥塞控制采用了慢开始,拥塞控制,快重传,快恢复四种算法处理),UDP没有处理;6. TCP只支持一对一的通信,UDP支持一对一,一对多,多对一,多对多通信。
TCP连接:TCP会经过三次握手连接四次挥手断开;三次握手,主要是首先客户端A,服务端B,A,B主动取消关闭状态,A主动向B发送连接请求,带上SYN同步报文和seq序号;B收到A发送的同步连接信息后返回A同步SYN和确认码ACK,并且携带序号seq和确认号ack给到A进行确认,A收到B的信息后再次响应B进行确认连接成功,A再次发送确认给到B是因为为了确保此次连接有效性,避免因为网络等其他原因导致的A初次发送连接请求过程中延时,在B收到连接请求的时候,A因为没有及时收到此次连接B的确认码而判定此次连接发送失败,这个时候B对于A发出的失效连接作出确认,A不会做任何处理,也不会有数据传输,导致B的资源浪费,这也是三次握手而不是两次握手的原因。四次挥手断开连接,首先A向B发送FIN终止连接请求,B收到后响应A的请求进行确认,A收到B的确认之后断开A与B之间的信息传输连接,此时A无法向B再传输数据,单向断开传输连接,B仍然可以向A发送数据传输,所有接下来B要向A发送FIN的终止连接请求,由A给到断开确认,B收到A的断开确认后不再向A发送FIN终止请求,在A向B发送确认终止的过程中A要等待两个最长报文的时间,目的就是确定B已经收到A的断开确认,如果B没有收到A的断开确认,则会再次发送FIN终止请求,在这个时间内A如果没有收到B的再次请求才可以确定B正常收到断开连接,至此四次挥手保证整个连接断开。
- http?
http是建立在TCP上的超文本传输协议,采用明文方式传输,迭代版本有http1.0/http1.1/http2.0,其中http1.0是最简单的,请求无状态无连接,每次请求都需要重新连接TCP协议,http1.1优化1.0的问题,实现长连接并且增加了管道,实现了单个TCP同时请求多个http请求,但是服务器处理的是仍然是排队处理,http2.0则解决了排队处理的问题,如果发现当前的请求耗时较长则会返回以处理的数据然后处理接下来的请求,请求处理完在继续处理耗时请求。
- https?
https是在http的基础上做了安全验证操作,在我们tcp/ip和http之间加了一层SSL/TSL的安全层。我们知道http是不安全的,它容易被第三方监听,篡改,冒充身份,所以针对这些问题,https就做了解决,保证网络安全;监听,则使用对称加密+非对称加密;篡改:使用随机数加原文进行hash算法(通常使用md5);冒充身份:使用CA证书认证。
Android:
- 多线程handler原理机制,内存泄漏原理,sendMessage,post区别?
首先提到handler我们要知道他的核心类,handler(消息处理者),message(消息携带者),MessageQueue(消息储存者),Looper(连接器,消息取出者)。handler的初始目的是多线程中处理UI更新操作,整个流程是:创建message---》handler发送消息----》MessageQueue入栈消息----》Looper.loop不断循环取出消息---》交给到发送者handler处理消息。整个过程中handler通过Looper绑定消息处理的线程,loop不断的取出messagequeue中的消息给到msg.target(也就是发送者的handler)进行分发消息。分发过程中就会涉及到回调callback的问题,此处会有两个callback,一个是message中的callback,一个是handler中的callback,调用哪一个是根据我们handler发送消息的时候是使用sendmessage还是post而定的,根据源码可以得知,post的时候我们是把post的runnable赋值给message中的callback的,所以,分发消息回调callback的时候我们首先会判断message中的callback是否为空,如果不为空的话我们就回调message中的callback,也就是post传入的runable,否则就是handler中实现的callback;
我们知道handler中内存泄漏是典型的内存泄漏问题,内存泄漏的原因是activity结束的时候handler中还有消息未处理,也就是messagequeue不为null,所以message不能被回收,然而message中持有我们当前的handler,常规handler的创建方式内部类和匿名内部类,java中这两种方式都会默认持有外部类的引用,所以message--》handler--》activity,这种方式就导致GC无法回收。所以我们处理方式就可以有两种,第一,GC的时候强制解除handler和activity之间的持有关系,所以我们想到弱引用持有activity,GC的时候不管是否可以回收都会回收或者静态内部类的方式,因为静态内部类默认不会持有外部类的引用,当然我们两种结合起来最安全;第二,当我们activity结束的时候移除messagequeue中的所有消息,这样就没有可用消息,GC的时候就可以直接回收掉。
- handlerThread,IntentService?
首先理解这两个类,要知道他的目的何在?
handlerThread:我们常规想在子线程创建handler并且发送消息由子线程处理需要首先创建子线程,然后子线程中创建handler,并且Looper初始化和循环,这样才能达到需求,然而handlerThread目的就在于此,handlerThread是一个thread,里面给我们封装了初始化Looper和handler的操作,简化我们使用过程中的代码复杂程度;
intentService:同样我们常规想做后台下载或者IO操作,我们会创建一个service,然后在里面初始化thread,然而intentService也是简化作用,它里面会初始化handler,和handlerThread并且绑定,在我们通过startService的时候传入intent携带要处理的事务,逐个处理,当事务处理完毕自定结束。并且我们可以发现同时多个startService传入intent处理的时候生命周期中onCreate只执行一次,onStartCommond和onStart,onHandlerIntent会每次启动intentService的时候都会执行。我们通常需要子线程处理过程中或者完毕更新主线程UI,这个时候我们就可以通过intent携带messager对象,然后intentService中获取该对象把更新消息封装成message,messager.send(msg)的方式来实现更新UI线程,(messager初始化的时候传入了handler)。
- okhttp? retrifit?
首先,我们要理解两者的作用,OKhttp是我们网络请求的核心框架,retrofit只是为了便于开发者使用而对okhttp做的封装,因为okhttp返回结果只有一种Call类型,然而我们在网络请求过程中处理结果的方式有很多种,比如Rxjava,kotlin协程等等,还有对返回数据的处理方式,所以这个对请求格式和返回数据处理方式封装就是我们的retrofit。
okhttp:使用很简单,首先创建我们的连接对象okhttpclient,request对象,然后调用okhttpclient的newcall方法传入request,创建我们的call对象,然后执行call的enquene或者excute方法,进行异步或者同步请求,请求过程中不管同步异步都会经过我们拦截器对数据和请求进行处理也是我们okhttp的核心。okhttp拦截器主要分为7种类型,自定义拦截器---重试重定向拦截器---桥接拦截器---缓存拦截器---网络连接拦截器---网络拦截器---IO处理拦截器(callserviceintercept)顾名思义,各自对请求拦截作出不同的操作,最终实现我们一个网络请求的过程。
retrofit:我们知道他是一个封装okhttp请求和结果处理的框架,所以它主要就是对请求数据进行包装,返回数据进行处理操作。我们一般使用会创建retrofit对象,并且创建apiservice的接口,接口中是各种抽象的请求函数,比如我们使用协程的方式请求,我们函数就是supend修饰的函数,函数上会存在我们请求方式和请求地址,函数签名中是请求参数和返回类型,然后调用retrofit的方法创建具体请求对象,我们知道在创建retrofit的时候回添加adapter和converter,这两个的作用adapter适配器是作用于我们数据返回类型的适配,比如是协程还是Rxjava等的返回方式,converter是用于处理我们返回数据的格式,比如我们常用的gson处理返回数据。retrofit中是通过门面模式只提供一个口子的方式让开发者使用过程中尽可能的简单好用,最少知道原则(迪米特原则),使用代理模式,使用动态代理的方式去解析我们的apiservice。
- 事件分发机制?滑动冲突解决?
老生常谈的问题,首先事件分发机制我们要知道经历了哪些对象,顺序和主要函数,当手指点击屏幕那一刻开始时间就开始了,ViewRootImpl对象会封装一个当前点击事件的对象MotionEvent,通过Activity的dispatchTouchEvent调用window的superDispatchTouchEvent方法把事件传递给decordView,decordView是一个viewgroup,这就实现了时间由activity传递给viewgroup的过程,然后viewgroup调用dispatchTouchEvent方法对事件向子类进行分发,分发过程中,判断是否需要拦截操作,调用onInterceptTouchEvent拦截,如果不拦截则事件 继续向子view传递,子view的dispatchTouchEvent接收到事件后判断是否消费该事件,如果消费掉则调用onTouchEvent处理事件,反之则调用父类的onTouchEvent,由父类去处理,如果父类也不做处理的话最终会调用activity的onTouchEvent结束该事件,然后依次返回dispatch的值。
上面的分析事件分发机制中可以知道,我们事件拦截是解决冲突的关键点,我们解决滑动事件冲突分为两种方式,内部拦截法和外部拦截法,内部拦截法:就是子view通过requestDisaollowIntercepterTouchEvent控制父类是否拦截事件,并且重写父view的onInterceptTouchEvent方法,除了down事件其他事件全部返回true,表示只要调用了onInterceptTouchEvent函数则父类就拦截,然而是否调用这个函数则是由子view通过requestDisallowInterceptTouchEvent控制的。外部拦截法:通过重写父view的onInterceptTouchEvent直接对事件做拦截处理,注意的是不管内部拦截法还是外部拦截法都不能拦截down事件,因为down事件是一个事件的开始,如果开始都不会有何谈move和up事件。
- 自定义view?
硕大自定义view我们何以分为3类,分别是组合自定义view,继承系统组件自定义view(系统存在的控件,不包括view,viewgroup),继承view,viewgroup。然而前两者都只是在已有的基础上锦上添花的封装操作,最后一个才是让我们为所欲为的方式,你可以通过它绘制出脑洞大开的控件和动画。自定义view(继承view,viewgroup):自定义view,主要函数有mesure,onMesure,setMesureDimension,layout,onLayout,setFrame,draw,onDraw,以上就是我们在自定义过程中三部曲涉及到的主要函数,首相mesure是对初始化测量相关信息,比如mesureSpec值,初始化完成回调onMesure函数进行具体的测量操作,测量完毕之后调用setMesureDimension保存我们测量结果,如果是自定义viewgroup的话还会涉及到viewgroup中对子view遍历mesure方法,让子view测量自身,在自定义onMesure中最关键的就是对自身大小的判断,因为系统默认的测量效果是不能满足我们需求的,所以要在onMesure中对其做处理,如果是自定义viewgroup则需要根据父类的mode来决定子类的mode,如果是自定义view,则需要自身wrap_content的时候判断自身的尺寸和父类尺寸来决定最终大小;接下来是布局操作,layout的参数则是自身在父类中的位子,layout中调用setFrame对自身的尺寸位置做了摆放,然而接下来调用onLayout主要是针对于viewgroup中具体的布局类型做相应的处理,因为每一个父布局的摆放样式不一致,所有这个函数是一个空实现;draw这个函数是绘制的关键,它里面会根据由下向上的层次绘制控件,背景--》保存canvas--》绘制view--》绘制子view--》绘制边缘阴影--》绘制装饰层,分为以上6步,ondraw只是绘制view的函数实现而已。(特别注意MesureSpec初始值来自于viewRootImpl,所有事件和绘制初始值都来自于此类,此类是view的核心类)。
- 动画?
动画我们分为三类:帧动画,补间动画,属性动画;
帧动画:把每一帧图片在一定时间内串联起来播放,所以他是依赖于多张图片的合成动画;
补间动画:旋转,缩放,透明度,位移,组合,几种类型,主要是对于控件的一些基础属性进行动画操作;
属性动画:可以对控件所有可操作(当前属性有定义get,set)的属性加以动画效果,属性动画中我们通常有ValueAnimator,ObjectAnimator两种,其中ObjectAnimator是ValueAnimator的子类,是可以对对象不同属性做动画操作,ValueAnimator主要是给到动画时间范围值,属性动画中主要的两个影响动画的属性是插值器和估值器,插值器是作用与动画的整个时间段的速度,估值器是作用于动画的具体轨迹,系统提供了常用的插值器和估值器,我们也可以根据需求自定义他们。
kotlin
- 协程?
首先理解协程的定义,kotlin中协程是一套官方封装的线程api。类似于我们android处理多线程handler,java中的excuter(线程池);
优点:协程可以实现同一个作用与不同线程的切换操作,摒弃了线程的嵌套操作,协程中是非阻塞式的挂起函数,在我们协程作用域中调用suspend挂起函数可以实现线程切换之后自动切回当前线程,所以suspend的作用就是切换线程之后可以自动切回来,一般我们可以用withContext(Dispathers.IO)进行线程切换,实现我们不同线程执行时线性的而不是嵌套的。
- inline,noline?
inline:内联函数,使用场景一般作用域参数是lamda表达式的函数,原理是inline标记的函数jvm编译的时候会被编译到调用函数中,而不会在jvm栈中开启新的入栈出栈操作,减少栈的创建提高性能;inline修饰的函数可以直接使用return退出整个函数(外层包含该inline修饰的函数)。
noline:该修饰符主要用于外部函数是inline修饰的函数,内部函数是普通函数,此时外部函数的lamda形式参数就不能传递给普通函数,所以用该修饰符修饰,禁止内联操作。
- 高阶函数?
kotlin中允许函数以参数的形式进行传递,当函数作为参数末尾的时候我们可以将函数卸载方法参数外,以lamda表达式的形式。lamda在kotlin中是函数,和java8中的lamda不一样,java8中的lamda实质上只是匿名内部类的一种简便写法而已。