今日头条Android面试

​点击关注异步图书,置顶公众号html

天天与你分享 IT好书 技术干货 职场知识前端


首先说一下,今日头条的面试主要分为三轮到四轮,若是是旺季面三轮,首先是基础面试,基本面试通常10个题左右,最近面试了一下今日头条的移动Android资深工程师,记录下。
java

第一面是北京的开发进行视频面试,有理论和编程题组成。用的是在线编程工具,以下图。 android

​第一面

1,请编程实现单例模式,懒汉和饱汉写法。c++

​2,请编程实现Java的生产者-消费者模型 程序员

​看到这个有点懵逼,要是大学毕业的时候写这个确定没问题,这都工做多年,这也只能按照本身的思路写了。这里使用synchronized锁以及wait notify实现一个比较简单的。关于更多的知识能够zhuanlan.zhihu.com/p/20300609 面试

3,HashMap的内部结构? 内部原理?
关于HashMap的问题,再也不详述,这方面的资料也挺多,很少须要注意的是Java1.7和1.8版本HashMap内部结构的区别。 算法


4,请简述Android事件传递机制, ACTION_CANCEL事件什么时候触发?
关于第一个问题,不作任何解释。
关于ACTION_CANCEL什么时候被触发,系统文档有这么一种使用场景:在设计设置页面的滑动开关时,若是不监听ACTION_CANCEL,在滑动到中间时,若是你手指上下移动,就是移动到开关控件以外,则此时会触发ACTION_CANCEL,而不是ACTION_UP,形成开关的按钮停顿在中间位置。
意思是当滑动的时候就会触发,不知道你们搞没搞过微信的长按录音,有一种状态是“松开手指,取消发送”,这时候就会触发ACTION_CANCEL。spring

5,Android的进程间通讯,Liunx操做系统的进程间通讯。
关于这个问题也是被问的不少,此处也不作解释。编程

6,JVM虚拟机内存结构,以及它们的做用。
这个问题也比较基础,JVM的内存结构以下图所示。

​能够经过下面的问题来学习:

www.cnblogs.com/jiyukai/p/6…
www.zhihu.com/question/65…

7,简述Android的View绘制流程,Android的wrap_content是如何计算的。

8,有一个整形数组,包含正数和负数,而后要求把数组内的全部负数移至正数的左边,且保证相对位置不变,要求时间复杂度为O(n), 空间复杂度为O(1)。例如,{10, -2, 5, 8, -4, 2, -3, 7, 12, -88, -23, 35}变化后是{-2, -4,-3, -88, -23,5, 8 ,10, 2, 7, 12, 35}。

要实现上面的效果有两种方式:
第一种:两个变量,一个用来记录当前的遍历点,一个用来记录最左边的负数在数组中的索引值。而后遍历整个数组,遇到负数将其与负数后面的数进行交换,遍历结束,便可实现负数在左,正数在右。

第二种:两个变量记录左右节点,两边分别开始遍历。左边的节点遇到负值继续前进,遇到正值中止。右边的节点正好相反。而后将左右节点的只进行交换,而后再开始遍历直至左右节点相遇。这种方式的时间复杂度是O(n).空间复杂度为O(1)

​显然,第二种实现的难点比较高,不过只要此种知足条件。

第二面

1,bundle的数据结构,如何存储,既然有了Intent.putExtra,为啥还要用bundle。

bundle的内部结构实际上是Map,传递的数据能够是boolean、byte、int、long、float、double、string等基本类型或它们对应的数组,也能够是对象或对象数组。当Bundle传递的是对象或对象数组时,必须实现Serializable 或Parcelable接口。

2,android的IPC通讯方式,是否使用过
这方面的资料比较多,也不方便阐述

3,Android的多点触控如何传递
核心类

4,asynctask的原理
AsyncTask是对Thread和Handler的组合包装。
blog.csdn.net/iispring/ar…
5,android 图片加载框架有哪些,对比下区别 主要有4种:Android-Universal-Image-Loader、Picasso、Glide和Fresco

Android-Universal-Image-Loader

优势:支持下载进度监听(ImageLoadingListener) * 可在View滚动中暂停图片加载(PauseOnScrollListener) * 默认实现多种内存缓存算法(最大最早删除,使用最少最早删除,最近最少使用,先进先删除,固然本身也能够配置缓存算法)
缺点:2015年以后便再也不维护,该库使用前须要进行配置。


Picasso

优势:包较小(100k) * 取消不在视野范围内图片资源的加载 * 使用最少的内存完成复杂的图片转换 * 自动添加二级缓存 * 任务调度优先级处理 * 并发线程数根据网络类型调整 * 图片的本地缓存交给同为Square出品的okhttp处理,控制图片的过时时间。
缺点:
功能较为简单,自身没法实现“本地缓存”功能。


Glide

优势:多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video) * 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求) * 高效处理Bitmap(bitmap的复用和主动回收,减小系统回收压力) * 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不一样,使得内存开销是Picasso的一半)。
缺点:方法较多较复杂,由于至关于在Picasso上的改进,包较大(500k),影响不是很大。


Fresco

缺点:最大的优点在于5.0如下(最低2.3)的bitmap加载。在5.0如下系统,Fresco将图片放到一个特别的内存区域(Ashmem区) * 大大减小OOM(在更底层的Native层对OOM进行处理,图片将再也不占用App的内存) * 适用于须要高性能加载大量图片的场景。
缺点:包较大(2~3M) * 用法复杂 * 底层涉及c++领域

5,主线程中的Looper.loop()一直无限循环为何不会形成ANR?
ActivityThread.java 是主线程入口的类,ActivityThread.java 的main函数的内容以下。

​而后再看Looper.loop()的源码,能够发现:

显然,ActivityThread的main方法主要就是作消息循环,一旦退出消息循环,那么你的应用也就退出了。那么这个死循环不会形成ANR异常呢?

说明:由于Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,若是它中止了,应用也就中止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。也就说咱们的代码其实就是在这个循环里面去执行的,固然不会阻塞了。来看一下handleMessage的源码:

​能够看见Activity的生命周期都是依靠主线程的Looper.loop,当收到不一样Message时则采用相应措施。

若是某个消息处理时间过长,好比你在onCreate(),onResume()里面处理耗时操做,那么下一次的消息好比用户的点击事件不能处理了,整个循环就会产生卡顿,时间一长就成了ANR。

总结:Looer.loop()方法可能会引发主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生ANR异常。

6,图片框架的一些原理知识

7,其余的一些Android的模块化开发,热更新,组件化等知识。

Android面试之主流框架

在Android面试的时候,常常会被问到一些Android开发中用到的一些开发框架,如常见的网络请求框架Retrofit/OkHttp,组件通讯框架EventBus/Dagger2,异步编程RxJava/RxAndroid等。本文给你们整理下上面的几个框架,以备面试用。

EventBus

EventBus是一个Android发布/订阅事件总线,简化了组件间的通讯,让代码更加简介,可是若是滥用EventBus,也会让代码变得更加辅助。面试EventBus的时候通常会谈到以下几点:

(1)EventBus是经过注解+反射来进行方法的获取的

注解的使用:@Retention(RetentionPolicy.RUNTIME)表示此注解在运行期可知,不然使用CLASS或者SOURCE在运行期间会被丢弃。
经过反射来获取类和方法:由于映射关系其实是类映射到全部此类的对象的方法上的,因此应该经过反射来获取类以及被注解过的方法,而且将方法和对象保存为一个调用实体。

(2)使用ConcurrentHashMap来保存映射关系

调用实体的构建:调用实体中对于Object,也就是实际执行方法的对象不该该使用强引用而是应该使用弱引用,由于Map的static的,生命周期有可能长于被调用的对象,若是使用强引用就会出现内存泄漏的问题。

说明:并发编程实践中,ConcurrentHashMap是一个常常被使用的数据结构,相比于Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap在线程安全的基础上提供了更好的写并发能力,但同时下降了对读一致性的要求。详情能够查看下面的文章:
www.importnew.com/22007.html

(3)方法的执行

使用Dispatcher进行方法的分派,异步则使用线程池来处理,同步就直接执行,而UI线程则使用MainLooper建立一个Handler,投递到主线程中去执行。

Retrofit

首先要明确EventBus中最核心的就是动态代理技术。

Java中的动态代理:

首先动态代理是区别于静态代理的,代理模式中须要代理类和实际执行类同时实现一个相同的接口,而且在每一个接口定义的方法先后都要加入相同的代码,这样有可能不少方法代理类都须要重复。而动态代理就是将这个步骤放入运行时的过程,一个代理类只须要实现InvocationHandler接口中的invoke方法,当须要动态代理时只须要根据接口和一个实现了InvocationHandler的代理对象A生成一个最终的自动生成的代理对象A*。这样最终的代理对象A*不管调用什么方法,都会执行InvocationHandler的代理对象A的invoke函数,你就能够在这个invoke函数中实现真正的代理逻辑。

动态代理的实现机制实际上就是使用Proxy.newProxyInstance函数为动态代理对象A生成一个代理对象A*的类的字节码从而生成具体A*对象过程,这个A*类具备几个特色,一是它须要实现传入的接口,第二就是全部接口的实现中都会调用A的invoke方法,而且传入相应的调用实际方法(即接口中的方法)。

Retrofit中的动态代理

Retrofit中使用了动态代理是不错,可是并非为了真正的代理才使用的,它只是为了动态代理一个很是重要的功能,就是“拦截”功能。咱们知道动态代理中自动生成的A*对象的全部方法执行都会调用实际代理类A中的invoke方法,再由咱们在invoke中实现真正代理的逻辑,实际上也就是A*的全部方法都被A对象给拦截了。
而Retrofit的功能就是将代理变成像方法调用那么简单。

​再用这个retrofit对象建立一个ServiceApi对象,并经过getAuthor函数来调用函数。

​也就是一个网络调用你只须要在你建立的接口里面经过注解进行设置,而后经过retrofit建立一个api而后调用,就能够自动完成一个Okhttp的Call的建立。Retrofit的create()函数的代码以下:

​咱们能够看出怎么从接口类建立成一个API对象?就是使用了动态代理中的拦截技术,经过建立一个符合此接口的动态代理对象A*,那A呢?就是这其中建立的这个匿名类了,它在内部实现了invoke函数,这样A*调用的就是A中的invoke函数,也就是被拦截了,实际运行invoke。而invoke就是根据调用的method的注解(,从而生成一个符合条件的Okhttp的Call对象,并进行真正的请求。

Retrofit做用

Retrofit其实是为了更方便的使用Okhttp,由于Okhttp的使用就是构建一个Call,而构建Call的大部分过程都是类似的,而Retrofit正是利用了代理机制带咱们动态的建立Call,而Call的建立信息就来自于你的注解。

OkHttp3

关于OkHttp3的内容你们能够访问下面的博客连接:OkHttp3源码分析。该文章主要从如下几个方面来说解OkHttps相关的内容:
OkHttp3源码分析[综述]
OkHttp3源码分析[复用链接池]
OkHttp3源码分析[缓存策略]
OkHttp3源码分析[DiskLruCache]
OkHttp3源码分析[任务队列]

请求任务队列

Okhttp使用了一个线程池来进行异步网络任务的真正执行,而对于任务的管理采用了任务队列的模型来对任务执行进行相应的管理,有点相似服务器的反向代理模型。Okhttp使用分发器Dispatcher来维护一个正在运行任务队列和一个等待队列。若是当前并发任务数量小于64,就放入执行队列中而且放入线程池中执行。而若是当前并发数量大于64就放入等待队列中,在每次有任务执行完成以后就在finally块中调用分发器的finish函数,在等待队列中查看是否有空余任务,若是有就进行入队执行。Okhttp就是使用任务队列的模型来进行任务的执行和调度的。

复用链接池

Http使用的TCP链接有长链接和短链接之分,对于访问某个服务器的频繁通讯,使用短链接势必会形成在创建链接上大量的时间消耗;而长链接的长时间无用保持又会形成资源你的浪费。Okhttp底层是采用Socket创建流链接,而链接若是不手动close掉,就会形成内存泄漏,那咱们使用Okhttp时也没有作close操做,实际上是Okhttp本身来进行链接池的维护的。在Okhttp中,它使用相似引用计数的方式来进行链接的管理,这里的计数对象是StreamAllocation,它被反复执行aquire与release操做,这两个函数实际上是在改变Connection中的List<WeakReference<StreamAllocation>>大小。List中Allocation的数量也就是物理socket被引用的计数(Refference Count),若是计数为0的话,说明此链接没有被使用,是空闲的,须要经过淘汰算法实现回收。

在链接池内部维护了一个线程池,这个线程池运行的cleanupRunnable其实是一个阻塞的runnable,内部有一个无限循环,在清理完成以后调用wait进行等待,等待的时间由cleanup的返回值决定,在等待时间到了以后再进行清理任务。相关代码以下:

​其中,Cleanup函数的执行过程以下:

遍历Deque中全部的RealConnection,标记泄漏的链接;

若是被标记的链接知足(空闲socket链接超过5个&&keepalive时间大于5分钟),就将此链接从Deque中移除,并关闭链接,返回0,也就是将要执行wait(0),提醒马上再次扫描;

若是(目前还能够塞得下5个链接,可是有可能泄漏的链接(即空闲时间即将达到5分钟)),就返回此链接即将到期的剩余时间,供下次清理;

若是(所有都是活跃的链接),就返回默认的keep-alive时间,也就是5分钟后再执行清理;

若是(没有任何链接),就返回-1,跳出清理的死循环。

说明:“并发”==(“空闲”+“活跃”)==5,而不是说并发链接就必定是活跃的链接。

如何标记空闲的链接呢?咱们前面也说了,若是一个链接身上的引用为0,那么就说明它是空闲的,那么就要使用pruneAndGetAllocationCount来计算它身上的引用数,如同引用计数过程。
其实标记引用为0的算法很简单,就是遍历它的List<Reference<StreamAllocation>>,删除全部已经为null的弱引用,剩下的数量就是如今它的引用数量,pruneAndGetAllocationCount函数的源码以下:

​RxJava

从15年开始,前端掀起了一股异步编程的热潮,在移动Android编程过程当中,常常会听到观察者与被观察者等概念。

观察者与被观察者通讯

Observable的经过create函数建立一个观察者对象。

​Observable的构造函数以下:

​建立了一个Observable咱们记为Observable1,保存了传入的OnSubscribe对象为onSubscribe,这个很重要,后面会说到。

onSubscribe方法

Rxjava的变换过程

在RxJava中常常会数据转换,如map函数,filtmap函数和lift函数。


lift函数

​咱们能够看到这里咱们又建立了一个新的Observable对象,咱们记为Observable2,也就是说当咱们执行map时,实际上返回了一个新的Observable对象,咱们以后的subscribe函数实际上执行再咱们新建立的Observable2上,这时他调用的就是咱们新的call函数,也就是Observable2的call函数(加粗部分),咱们来看一下这个operator的call的实现。这里call传入的就是咱们的Subscriber1对象,也就是调用最终的subscribe的处理对象。

call函数

这里的transformer就是咱们在map调用是传进去的func函数,也就是变换的具体过程。那看以后的onSubscribe.call(回到call中),这里的onSubscribe是谁呢?就是咱们Observable1保存的onSubscribe对象,也就是咱们前面说很重要的那个对象。而这个o(又回来了)就是咱们的Subscriber1,这里能够看出,在调用了转换函数以后咱们仍是调用了一开始的Subscriber1的onNext,最终事件通过转换传给了咱们的结果。

线程切换过程(Scheduler)

RxJava最好用的特色就是提供了方便的线程切换,但它的原理归根结底仍是lift,使用subscribeOn()的原理就是建立一个新的Observable,把它的call过程开始的执行投递到须要的线程中;而 observeOn() 则是把线程切换的逻辑放在本身建立的Subscriber中来执行。把对于最终的Subscriber1的执行过程投递到须要的线程中来进行。

​从图中能够看出,subscribeOn() 和 observeOn() 都作了线程切换的工做(图中的 “schedule…” 部位)。不一样的是, subscribeOn()的线程切换发生在 OnSubscribe 中,即在它通知上一级 OnSubscribe 时,这时事件尚未开始发送,所以 subscribeOn() 的线程控制能够从事件发出的开端就形成影响;而 observeOn() 的线程切换则发生在它内建的 Subscriber 中,即发生在它即将给下一级 Subscriber 发送事件时,所以 observeOn() 控制的是它后面的线程。

为何subscribeOn()只有第一个有效?
由于它是从通知开始将后面的执行所有投递到须要的线程来执行,可是以后的投递会受到在它的上级的(可是执行在它以后)的影响,若是上面还有subscribeOn() ,又会投递到不一样的线程中去,这样就不受到它的控制了。

本文来源于异步社区,做者:xiangzhihong ,做品《今日头条Android面试》,未经受权,禁止转载。

推荐阅读

2018年5月新书书单(文末福利)

2018年4月新书书单

异步图书最全Python书单

一份程序员必备的算法书单

第一本Python神经网络编程图书

​长按二维码,能够关注咱们哟

天天与你分享IT好文。

点击阅读原文,查看更多信息

阅读原文​