当前位置: 首页 > 面试经验 >

字节跳动Android工程师面经|一、二面

优质
小牛编辑
130浏览
2023-03-28

字节跳动Android工程师面经|一、二面

一面

1. 抽象类和接口的区别

1)抽象类需要使用extends关键字继承,而接口需要使用implements实现。
2)抽象类的权限可以为public、protected、default,而接口权限必须为public。
3)抽象类既可以做方法申明也可以进行方法实现,而接口只能做方法申明。
可能有坑(面试官可能会问Java8新特性中的接口的默认方法)
4)抽象类中的变量为普通变量,而接口中只能有被public static final修饰的常量。
5)抽象类里可以没有抽象方法。若一个类中有抽象方法,则该类只能为抽象类。
6)抽象方法不能进行实现(接口中的方法也是如此),抽象方法不能被static修饰,也不能被private修饰。
7)抽象类可以单继承多实现,而接口只能继承接口,而且为多继承。

2.说下抽象类的继承和接口继承的各自侧重点

面试官在问了上一道题目后,又问了这道相似的题目,说明候选人在回答上一道题目的时候没有回答到点上。

3.hashmap的原理 能说说hashmap的扩容操作吗?扩容是new一个新的map还是在原来的基础上增加内存?

  • HashMap主干是通过类型为Entry的数组实现的(Entry<K,V>),在JDK1.8之前由Entry数组和链表组成,在JDK1.8及之后由Entry数组和红黑树组成。
  • HashMap内部有几个重要的变量字段:transient用来记录实际存储的键值对的个数。threshold表示扩容阈值,超过该值会触发扩容,计算公式为最大容量capacity * 负载因子loadFactory,默认为12。loadFactory表示负载因子,默认为0.75。
  • HashMap在扩容时会调用resize方法,该方***在addEntry方法中调用,也就是说在每次添加键值对的时候会进行扩容判断,具体的判断条件是当前存储的键值对(包括现在要添加的键值对)数量超过threshold的时候会触发扩容,扩容后的Entry数组长度为之前的2倍。
  • resize方法内部首先会判断若Entry数组已经扩到了最大值,则提高阈值threshold,返回。否则,则新建一个长度为之前2倍的Entry数组,将原来数组的数据转移到新数组中,最后重新计算阈值threshold。所以说扩容是新建了一个Entry数组。

4.说说classload(启动类加载器、扩展类加载器、应用类加载器) 能说说双亲委派模型吗?

Java的ClassLoader共分三种:第一种启动类加载器Bootstrap ClassLoader,用于加载Java核心类库,底层为native实现。第二种扩展类加载器Extension ClassLoader,用于加载\lib\ext目录下的类及被java.ext.dirs系统变量指定的类库。第三种应用类加载器Applicaiton ClassLoader,用于加载ClassPath所有指定的类库。还有一种是用户自定义的类加载器。这四类的级别从高到低依次为:Bootstrap ClassLoader、Extension ClassLoader、Applicaiton ClassLoader、用户自定义ClassLoader。
双亲委派模型的原理是当一个类加载器收到类加载请求时,首先不会尝试加载,而是请求委派给父类加载器加载,经过递归委派,最后会委派到Bootstrap ClassLoader。当父类无法完成加载请求时,则交给下一级子类来处理加载,若都无法完成加载,最会交给一开始的类加载器进行处理。子类加载器与父类加载器不是以继承的方式实现,而是通过组合的方式复用父类的代码。

5.启动一个活动A,接着在A中启动活动B,各自的生命周期变化?如果B活动是透明的呢?如果此时再启动第三个活动C,三个活动的生命周期变化?

首先会调用Activity A的onPause方法,然后依次调用Activity B的onCreate方法、onStart方法、onResume方法,最后调用Activity A的onStop方法。
如果Activity B是透明的,说明Activity A是可见的,则最后不会调用Activity A的onStop方法。
首先会调用Activity B的onPause方法,然后依次调用Activity C的onCreate方法、onStart方法、onResume方法,最后依次调用Activity B的onStop方法、Activity A的onStop方法。

6.handler原理。

首先在使用Handler之前需要调用Looper的静态方法prepare,该方法内部会创建一个Looper对象,并将其保存到ThreadLocal中,同时在创建Looper时,Looper的构造方法中会创建一个MessageQueue对象。MessageQueue对象是一个消息队列,共有两个方法。enqueueMessage方法用于将消息Message加入队尾。next方法用于从队首取出一个消息Message。
接下来会创建Handler对象,在Handler的构造方法中会调用Looper的myLooper方法,获取当前线程的Looper对象,接下来会从Looper对象中获取其中的MessageQueue对象,保存在Handler中。
当调用Handler对象的sendMessage方法时,内部会调用sendMessageDelayed方法,最终会调用MessageQueue对象的enqueueMessage方法,Message对象作为参数,同时Message对象的target变量会保存当前的Handler对象。
当调用Looper的静态方法loop时,内部会调用MessageQueue对象的next方法,从中取出Message对象。接下来会从Message对象的target变量中取出Handler对象。然后调用handler对象的dispatchMessage方法对消息进行分发。
消息的处理会优先处理消息Message中携带的Callback,构造Handler对象中传入的Callback次之,如果前面二者都为空,则会交给重写的方法handleMessage处理。

7.idealhandler了解吗?

IdealHandler用于在消息队列MessageQueue中Message为空的时候回调一次。
创建IdealHandler对象,需要重写queueIdle方法,该方法需要返回一个Boolean值。当返回为true时,下一次MessageQueue对象为空时,还会调用queueIdle方法。若返回false,则本次执行完queueIdle方法后会移除当前的IdealHandler对象,下一次MessageQueue对象为空时不会再调用queueIdle方法。
使用时需要调用当前Looper中保存的MessageQueue对象的addIdleHandler方法,IdealHandler对象作为参数。

8.view.post说一下。为什么是插入到消息队列的尾部?

当调用View的post方法时,当 View 还没有调用 attachedToWindow方法时,则post方法传进来的 Runnable 对象会先被缓存在 HandlerActionQueue中,等View对象调用了dispatchAttachedToWindow方法时,会调用mAttachInfo对象中mHandler变量保存的Handler对象执行被缓存起来的Runnable对象。从这以后到View调用detachedFromWindow方法这段期间,若再次调用View对象的post方法,则这些 Runnable对象直接交给mAttachInfo对象中mHandler变量保存的Handler对象执行。
因为事件序列可能存在依赖关系,所以需要排队,先来先执行。

9.view.post和handler.post的区别?

View对象中的Handler对象为主线程的Handler对象,内部会对执行时机进行控制(attachedToWindow之后),View对象的post方法内部实际也是调用主线程Handler对象的post方法。

10.消息的插入时间是怎么计算的?(开机时间再加上希望消息延迟的时间)

MessageQueue对象的enqueueMessage方***对加入到队列中消息进行排序,延迟最小的在前,延迟最大的在后。
当调用MessageQueue对象的next方法获取Message对象时,若Message对象需要延时,则调用nativePollOnce方法对Looper(当前线程)进行阻塞,当延时时间到了后会返回该Message对象。
当在阻塞的过程中,如果有新的不需要延时的Message对象加入,则会到被排序到队首,首先会调用nativeWake方法唤起当前线程去获取队首的Message对象,当该Message对象执行完,会继续判断新的队首的Message对象是否延时完毕,若没有则继续调用nativePollOnce方法方法进行延时。

11.怎么把主线程的消息传递到子线程?

原理都是在主线程中调用子线程的Handler对象的sendMessage方法。
Android中具体可以使用AsyncTask、HandlerThread、IntentService。

12.主线程的looper和handler是一起的吗? 子线程是否可以直接使用handler?

主线程Looper只有一个,但是Handler可以有多个。
子线程使用Handler需要先调用Looper的prepared方法,为当前线程创建Looper和MessageQueue对象。再调用Looper的loop方法对消息进行分发。

13.事件分发机制说一下。

一个完整的点击事件序列以DOWN事件开始,中间可能有多个MOVE,最终以UP或CANCEL结束。
当一个DOWN事件产生时,首先会调用Activity对象的dispatchTouchEvent方法,对点击事件做分发。接着会调用最顶级的View(一般为ViewGroup)的dispatchTouchEvent方法进一步分发,dispatchTouchEvent方法内部会调用onInterceptTouchEvent方法,判断是否对点击事件拦截,若返回false则不拦截,继续分发,最后交给最底层的View处理。若返回true,则表示处理事件。首先会判断View是否设置了OnTouchListener对象,如果设置了则调用OnTouchListener对象的onTouch方法,若该方法返回true,则表示点击事件被消耗(处理),若没有设置OnTouchListener对象或onTouch方法返回false,则会调用View的onTouchEvent方法进行处理,若当前的View不能处理,则交给它的父级View处理,若都不能处理,则会一直向上传递,最后在Activity对象的onTouchEvent中处理。
若一个View拦截了DOWN事件,则该事件序列中之后所有的事件都会交给该View进行处理。

14.场景题—— 一个scrollview嵌套两个recyclerview(recyclerview1,recyclerview2),这两个recyclerview的大小都是整个屏幕的大小,如何实现在recyclerview1中滑动完数据后,接着滑动recyclerview2中的数据?

思路:外层的父View为ScrollView,内部两个并列的子View为RecyclerView。当点击事件DOWN产生时,ScrollView不拦截,交给RecyclerView处理,如果RecyclerView没有滑动到底,则在收到DOWN事件后调用requestDisallowInterceptTouchEvent方法,参数为true。则之后所有的事件都交给RecyclerView处理。当RecyclerView滑动到底时,则在收到DOWN事件后调用requestDisallowInterceptTouchEvent方法,参数为false,之后所有的事件都交给ScrollView处理,切换到第二个RecyclerView,处理方式同一个RecyclerView。

15.算法题—— 有一个ViewGroup,该ViewGroup中又有子view或者子ViewGroup,按层级遍历输出每一层的view和viewgroup元素。(这里楼主大脑一片空白,面试官哥哥耐心的引导我去使用二叉树的层次遍历解决,面试官真好)
二叉树的广度遍历

public static class TreeNode{

    private int data;

    private TreeNode leftChild;

    private TreeNode rightChild;


    public void setData(int data) {

        this.data = data;

    }


    public void setLeftChild(TreeNode leftChild) {

        this.leftChild = leftChild;

    }


    public void setRightChild(TreeNode rightChild) {

        this.rightChild = rightChild;

    }


    public int getData() {

        return data;

    }


    public TreeNode getLeftChild() {

        return leftChild;

    }


    public TreeNode getRightChild() {

        return rightChild;

    }


    public static List<TreeNode> getChildren(TreeNode treeNode){ 
                    List<TreeNode> list = new ArrayList<TreeNode>(); 
                    if(treeNode.getLeftChild() != null) 
                            list.add(treeNode.getLeftChild()); 
                    if(treeNode.getRightChild() != null) 
                            list.add(treeNode.getRightChild()); 
                    return list; 

            }
}

    public static void breadthFirst(TreeNode treeNode) { 
            Deque<TreeNode> deque = new ArrayDeque<TreeNode>(); 
            deque.add(treeNode); 
            while(!deque.isEmpty()) { 
                    TreeNode node = deque.pollFirst(); 
                    System.out.println(node.getData()); 

                    List<TreeNode> list = TreeNode.getChildren(node); 
                    if(list!=null && !list.isEmpty()) { 
                            for(TreeNode t:list) { 
                                    deque.add(t); 
                            } 
                    } 
            } 

    }

二面

1.泛型讲一下。 什么是语法糖? 泛型的协变与逆变说一下。

泛型用于增加代码的可扩展性。Java中的泛型不同于C++等语言的真正的泛型,而是采用类型擦出机制实现泛型。即在编译时对类型进行检查,而在运行时丢弃了泛型的类型,最终都是对Object对象进行操作。需要注意
1)基本类型不能用作类型参数,类型参数必须是引用类型。必须使用包装类。
因为擦除后为Object类,基本类型不是继承Object类,而基本类型的包装类继承自Object。
2)不能用instanceof检测泛型,由于类型擦除的原因,任何泛型类型都会别擦除为它的原生
类型。
3)泛型类中,static方法和static域不能引用类的泛型变量。因为在类型擦除过后,类型参数
就不存在了。
4)不能创建一个泛型类型的实例。是非法的。
5)不能创建一个泛型数组,因为数组有严格的类型检查。通常用泛型容器,如来实现同样的
需求。
语法糖是一种为了使编写代码更简洁、可读性更高而产生的语法,但这种语法并没有影响语言本身,在运行时使用语法糖的代码和没有使用语法糖的代码性能和表现是等价的。
协变和逆变是一种对泛型类型的扩展。假设泛型类型为T,协变将泛型的类型T扩展到了T及其子类,逆变则将泛型扩展到了T及其父类。在Java中使用通配符?来表示协变和逆变。协变使用上通配符<? extends T>,逆变使用下通配符<? super T>。

2.手写一个简单的泛型方法。

public <T> T function(T param){
    return param;
}

3.volatile说一下。volatile的八大原子操作说一下。CAS操作说一下。手写生产者消费者。(这里得理解生产者消费者的设计原理,楼主在这也没答好)

volatile是Java中的一个用于修饰全局变量的关键字,表示变量的值在每次修改后,会立即通知其他CPU更新该属性的缓存,确保可见性。
volatile八大原子操作为:
1)read(读取):从主内存中读取数据
2)load(载入):将主内存读取到的数据写入到工作内存中
3)use(使用):从工作内存读取到数据来计算
4)assign(赋值):将计算好的值重新赋值到工作内存中
5)store(存储):将工作内存数据写入主内存
6)write(写入):将store过去的变量赋值给主内存中的变量
7)lock(锁定):将主内存变量加锁,标识为线程独占状态
8)unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
CAS全名为Compare And Swap,是一种轻量级锁的实现原理。具体原理为当内存值V与预期值A相同时,则将内存值V修改为新值B。CAS为非阻塞算法,即一个线程的失败或挂起不应该影响另一个线程的失败或挂起。CAS会引起ABA问题,即线程1在内存M处写入A,线程2在内存M处首先读取了A,又在内存M处写入了B,之后有写入了A,这是线程1使用CAS对内存M处的数值进行操作,虽然内存M最终的值仍为A,但实际上内存M是变化了的。

4.TCP的三次握手流程。 为什么是三次握手?

第一次握手:客户端发送请求报文段,SYN = 1,seq = x,客户端进入SYN_SEND状态。
第二次握手:服务端收到SYN报文段,并进行确认。服务端发送报文段ACK = x+1,SYN = 1,seq = y,服务端进入SYN_RCVD状态。
第三次握手: 客户端收到SYN和ACK报文段,并进行确认。客户端发送报文段ACK = y+1,发送完ACK报文段后,客户端进入ESHTABLISHED的状态。服务端收到ACK报文段并确认,之后进入ESHTABLISHED的状态。
三次握手完成,TCP连接完成。
为了数据可靠传输,高效传输。一次不可靠,四次浪费,两次可能会因为网络延迟造成服务端为过期的请求建立连接。

5.HTTPS讲一下。CA证书讲一下,CA证书是怎么保证服务器的公钥是没被篡改的呢?

Https由Http + SSL/TLS+加密+认证组成
Https采用混合加密机制:共享密钥加密+公开密钥加密,使用公开密钥加密方式交换共享密钥中要使用的密钥,确保安全的前提下,使用共享密钥方式进行加密
(a).共享密钥
优点:加密相对简单,效率高
缺点:安全性相对低
(b).公开密钥
优点:安全性高
缺点:加密复杂,效率较低
Https的公开密钥的认证过程(CA证书):
1)服务器把自己的公开密钥登陆至数字证书认证机构
2)数字证书认证机构用自己的私有密钥向服务器的公开密码署数字签名并颁发公钥证书
3)客户端拿到服务器的公钥证书后,使用数字认证机构的公开密钥向数字认证机构验证公钥证书上的数字签名,确保服务器公钥的真实性
4)认证成功后,客户端使用服务器的公开密钥对报文进行加密发送
5)服务器用私有密钥对报文解密

Https建立过程:

1)客户端发送Client Hello报文开始SSL通信
报文中包括:客户端支持的SSL版本,加密组件列表(加密算法,密钥长度等)
2)服务器发送Server Hello报文作为响应
报文中包括:服务器从客户端筛选出的SSL版本,加密组件列表
3)服务器发送Certificate报文
报文中包括:公开密钥证书
4)服务器发送Server Hello Done报文
最初阶段的SSL握手协商部分结束
5)客户端发送Client Key Exchange报文
报文中包括:Pre-master secret:使用公开密钥加密过的随机密码串
6)客户端发送Change Cipher Spec报文
提示服务器在此之后的通信采用Pre-master secret进行加密
7)客户端发送Finished报文
报文中包括:连接至今的全部报文的整体校验值。
8)若校验成功,服务器发送Change Cipher Spec报文
9)服务器发送Finished报文
10)SSL连接建立完成

Https缺点:

1)***L请求速度慢
2)由于加密,更加消耗硬件资源
3)购买证书,增加额外开销

6.hashmap讲一下。hashmap是有序的吗?

参考一面,HashMap不保证插入顺序,但是循环遍历时,输出顺序不会改变。

7.如果要实现线程安全的map,应该用什么数据结构? currenthashmap讲一下。

ConcurrentHashMap
(简单回答版本如下)

  • ConcurrentHashMap主干是通过类型为Segment的数组实现,Segment类继承自ReentrantLock,因此保证了线程安全。Segment数组中每一个元素称为一个段,即一个Segment对象。每个Segment对象中维持着一个HashEntry数组。一个HashEntry对象内部存储着键值对和指向下一个HashEntry对象的指针。因此,HashEntry数组的每一个元素实际上对应的是一条由HashEntry对象组成的链表。
  • ConcurrentHashMap中有几个重要的字段变量。第一个是loadFactory,代表负载因子,用于每个Segment对象内部的HashEntry数组的扩容,默认0.75。Segment数组不可扩容,数组长度默认为2。第二个是threshold,扩容的阈值,默认1.5。第三个是initialCapacity,代表初始容量,用于平均分给Segment数组中每个元素的键值对数量。第四个是concurrentLevel,代表分段数,默认为16。
  • 当实例化一个ConcurrentHashMap对象。在它的构造方法中,首先会有一个值为1的变量ssize,通过与concurrentLevel进行比较,若小于,则进行左移位的运算,每次移动一位,直到ssize的值大于concurrentLevel的大小。同时记录该过程的左移次数,保存到变量sshift中,即2的sshift次方等于ssize。接下来,计算变量segmentShift的值,等于32-sshift。计算变量segmentMask的值,等于ssize-1。计算HashEntry的长度cap,cap等于initialCapacity除以ssize,若cap乘以ssize的值小于initialCapacity,则cap自增1,这个操作可以确保初始化存储的键值对数量足够。最后,创建Segment数组,并只初始化第一个段的Segment对象。
  • 当调用put方法存储键值对的时候,首先会根据键值对的key使用hash算法计算出哈希值。接下来通过之前计算出的segmentShift、segmentMask和哈希值计算出该键值对应该存储到哪个段里。接着会检查并确保该段已经初始化,然后获取该段的Segment对象。最后调用Segment对象的put方法,实现键值对的存储。
  • 在Segment的put方法中首先会尝试获取锁,获取失败则等待获取锁。若获取到,则根据哈希值计算键值对应该存储在该段的HashEntry的具***置,若该位置超过了HashEntry的长度,则对HashEntry数组进行扩容。若没有,则将键值对封装成HashEntry对象,存储在对应位置的链表头部,最后释放锁。
  • 如果发生扩容,则会为该段新创建一个HashEntry数组,长度为之前的二倍,再将之前的数据填充回来。然后重新计算相关参数。
    当调用get方法获取键值对时,首先计算哈希值,找到对应的段。然后根据哈希值,找到该段HashEntry数组中的位置,遍历该位置的链表找到对应的值,最后返回。

8.说一下你熟悉的设计模式。

(挑会的说,会是指理解原理能写出代码的内种)单例模式、适配器模式、工厂模式、抽象工厂模式、包装模式。

9.工厂设计模式的原理是什么?

在不同条件下创建不同的实例对象。
定义一个用于创建对象的接口,让实现该接口的类决定实例化哪一个工厂类对象,使其创建过程延迟到了子类进行。

10.代理设计模式该怎么设计?

  • 创建接口A。
  • 创建实体类B,实现接口A。
  • 创建代理类C,实现接口A,并在C类的方法中提供B类对象的生成方法和调用方法。

11.网络抓包怎么做?怎么用最小的代价判断该请求是失败的?

  • 使用Charles
  • 请求返回的状态码(不太理解问的什么意思- _-)

12.五种状态码说一下。201是什么?302是什么?

  • 100~199:提示信息,收到请求,需请求者继续执行操作

  • 200~299:请求成功,已被成功接收并处理

  • 300~399:重定向,需进一步操作

  • 400~499:客户端错误,请求语法错误,或无法实现

  • 500~599:服务器错误,不能实现请求

  • 201 Created:成功请求并创建新的资源。有一个新的资源已经依据请求而建立,且其 URI 随Location 头信息返回。

  • 302 Found:临时性重定向,请求的资源被分配了新的URI,本次用这个URI。

如果有需要Android面试题的小伙伴,我已将其与答案已按照规范整理完成,大家可看文末或评论/私信,一起交流技术、进阶提升~


感谢阅读并祝你面试好运!

公众号:Android Jasper 专注分享面试题|面试技巧|Android学习资料。

#字节跳动##安卓工程师##Android##面经##面试#
 类似资料: