Android面试刨根问底之常用源码篇(一)(1),2024一位Android中级程序员的跳槽面经

而在做&运算时,如果选用非2n的数时,n-1转换为二进制,不能保证后几位全为1,这样做在&hash的运算中,不能做到均匀分布。违背了(n-1)&hash的初衷。

(16)10 = 24 = (10000)2

(16-1)10 =(1111)2

假设n的值非2x值,10

(10-1)10 =(1001)2

(19-1)10 =(10011)2

10011

&1111

=(11)2=(3)10

10011

&1001

=(1)2=(1)10

同样的%运算,19%16 = 3 ,19%10 = 9。

任意一个数与(1111)2做&运算,都不会因为(1111)2的值而影响到运算结果。

2. 如果初始化HashMap的时候定义大小为非2x会影响到计算吗?

答案是,肯定不会,这种情况JAVA的工程师肯定考虑到了。

源码中我们可以看到,传入的capacity只是影响到了threshold的值,而threshold的值还是通过tableSizeFor()确定的。

public HashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

public HashMap(int initialCapacity, float loadFactor) {

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

if (loadFactor <= 0 || Float.isNaN(loadFactor))

throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

this.loadFactor = loadFactor;

this.threshold = tableSizeFor(initialCapacity);

}

在tableSizeFor()方法中。

static final int tableSizeFor(int cap) {

// cap=10

int n = cap - 1;

// n =9 1001

n |= n >>> 1;

// (1001)|(0100)=1101

n |= n >>> 2;

//(1101)|(0011)=1111

n |= n >>> 4;

// (1111)|(0000)=1111

n |= n >>> 8;

// (1111)|(0000)=1111

n |= n >>> 16;

// (1111)|(0000)=1111

//return n+1 = (10000)=16

//确保threshold 为16, 2的4次幂

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

在putVal()方法中,如果第一次添加值,那么table==null,会进入到resize()方法中,这个时候,就会用到threshold创建新的Node数组。

final Node<K,V>[] resize() {

Node<K,V>[] oldTab = table;

//第一次添加值,table==null; oldCap = 0;

int oldCap = (oldTab == null) ? 0 : oldTab.length;

//将threshold的值设置为oldThr,下面创建table的时候用到

int oldThr = threshold;

int newCap, newThr = 0;

if (oldCap > 0) {

}

else if (oldThr > 0)

//通过threshold设置新数组容量

newCap = oldThr;

else {

}

if (newThr == 0) {

}

threshold = newThr;

@SuppressWarnings({“rawtypes”,“unchecked”})

//通过threshold设置table的初始容量

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab;

return newTab;

}

通过以上操作,不论初始化HashMap的时候,传入的容量是多少,都能保证HashMap的容量是2x。

Handler源码分析

===========

一直在纠结一个事,因为自己不爱看大段的文字。

自己写总结的时候到底要不要贴上部分源码。

后来硬着头皮加上了,因为源码里很多东西比自己写的清楚。

RTFSC

相关概念

Handler Message MessageQueue Looper ThreadLocal

Handler机制的完整流程

  1. Message#obtain()

  2. Handler#

  3. Handler#send/post

  4. MQ#enqueueMessage() *消息的排序

  5. Looper#prepareMainLooper()

  6. Looper#prepare()

  7. ThreadLocal机制

  8. Looper#loop()

  9. MQ#next() *延迟消息的处理

  10. Handler#dispatchMessage()

Message#obtain()

message中的变量自己去看源码,target,callback,when

从handler或者是message的源码中都可以看到,生成Message的最终方法都是调用obtain。

ps:如果你非要用Message的构造方法,那么看清楚他的注释,构造方法上面的注释写的也很清楚,

/**

  • Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).

*/

public Message() {

}

下面来分析一波obtain()方法:

  1. 为什么上来就是一个同步?

任意线程都可以创建message,所以为了维护好内部的messge池,加锁

  1. sPool是个什么东西

字面上看是个池子,但是从定义上看,是一个Message。为什么还要说成一个message池呢?因为Message内部有个next变量,Message做成了一个链表的形式。这个池子怎么存储message呢?稍后分析源码。

通过读obtain()的源码,结合链表的知识,很容易理解Message中Spool的原理。

public static final Object sPoolSync = new Object();

private static Message sPool;

private static int sPoolSize = 0;

/**

  • Return a new Message instance from the global pool. Allows us to

  • avoid allocating new objects in many cases.

*/

public static Message obtain() {

synchronized (sPoolSync) {

if (sPool != null) {

Message m = sPool;

sPool = m.next;

m.next = null;

m.flags = 0; // clear in-use flag

sPoolSize–;

return m;

}

}

return new Message();

}

通过查看调用链,我们能够看到在MQ中enqueueMessage调用了recycle(),而recyle中也是通过链表的形式对sPool进行维护。源码简单易懂

下面来看下sPool是怎么维护的。

在recycleUnchecked()同样也是加了锁的。然后就是用链表的形式维护这个池子,size++

public void recycle() {

if (isInUse()) {

if (gCheckRecycle) {

}

return;

}

recycleUnchecked();

}

/**

  • Recycles a Message that may be in-use.

  • Used internally by the MessageQueue and Looper when disposing of queued Messages.

*/

void recycleUnchecked() {

synchronized (sPoolSync) {

if (sPoolSize < MAX_POOL_SIZE) {

next = sPool;

sPool = this;

sPoolSize++;

}

}

}

Handler

Handler类的源码总共不超过1000行,并且大部分都是注释,所以我们看该类源码的时候,更多的是看他的注释。静下心来看源码

  • 构造方法

  • callback对象

  • dispatchMessage

Handler发送消息(send/post)

Handler发送消息的方式分为两种:

1.post

2.send

不论是post还是send(其他方法)方式,最终都会调用到sendMessageAtTime/sendMessageAtFrontOfQueue。执行equeueMessage,最终调用MQ#enqueueMessage(),加入到MQ中。

1. post方式

以post方式发送消息,参数基本上都是Runnable(Runnable到底是什么,这个要搞懂)。post方式发送的的消息,都会调用getPostMessage(),将runnable封装到Message的callbak中,调用send的相关方法发送出去。

ps:个人简单、误导性的科普Runnable,就是封装了一段代码,哪个线程执行这个runnable,就是那个线程。

2. send方式

以send方式发送消息,在众多的重载方法中,有一类比较容易引起歧义的方法,sendEmptyMessageXxx(),这类方法并不是说没有用到message,只是在使用的时候不需要传递,方法内部帮我们包装了一个Message。另一个需要关注的点是: xxxDelayed() xxxAtTime()

1.xxxDelayed()

借助xx翻译,得知 delayed:延迟的,定时的,推迟 的意思,也就是说,借助这个方法我们能做到将消息延迟发送。e.g:延迟三秒让View消失。ps:在我年幼无知的时候,总是搞懵这个方法,不会用。

在这个方法的参数中,我们看到如果传入的是毫秒值,那么会在delayMillis的基础上与SystemClock.uptimeMillis()做个加法。然后执行sendMessageAtTime()。

SystemClock.uptimeMillis() 与 System.currentTimeMillis()的区别自己去研究。

public final boolean sendMessageDelayed(Message msg, long delayMillis)

{

if (delayMillis < 0) {

delayMillis = 0;

}

return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);

}

2.xxxAtTime()

在这个方法就更简单易懂了,执行的具体时间需要使用者自己去计算。

在Handler内的equeueMessage中,第一行的msg.target = this;,将handler自身赋值到msg.target,标记了这个msg从哪来,这个要注意后面会用到

MQ#enqueueMessage()

这个方法那是相当的关键

在此之前,我们一直鼓捣一个参数delayMillis/uptimeMillis,在这个方法里参数名变为了when,标明这个message何时执行,也是MQ对Message排序存储的依据。MQ是按照when的时间排序的,并且第一个Message最先执行。

在省去了众多目前不关心的代码后,加上仅存的一点数据结构的知识,得到msg在MQ中的存储形式。

mMessages位于队列第一位置的msg,新加入到msg会跟他比较,然后找到合适的位置加入到队列中。

ps:记得在一次面试中,面试官问到延迟消息的实现思路,我照着源码说了一下。但是被问到:**每次新加入消息,都要循环队列,找到合适的位置插入消息,那么怎么保证执行效率。**我不知道他这么问是想考我优化这个东西的思路,还是他觉得我说错了。就犹豫了一下,没有怼回去。

boolean enqueueMessage(Message msg, long when) {

synchronized (this) {

msg.markInUse();

msg.when = when;

Message p = mMessages;

boolean needWake;

if (p == null || when == 0 || when < p.when) {

msg.next = p;

mMessages = msg;

needWake = mBlocked;

} else {

needWake = mBlocked && p.target == null && msg.isAsynchronous();

Message prev;

for (;😉 {

prev = p;

p = p.next;

if (p == null || when < p.when) {

break;

}

if (needWake && p.isAsynchronous()) {

needWake = false;

}

}

msg.next = p; // invariant: p == prev.next

prev.next = msg;

}

}

return true;

}

以上几步,我们只是将要执行的msg加入到了队列中。接下来分析下什么时候执行msg。

再接再厉,马上就看到暑光了。

Looper#prepareMainLooper()

借助十几年英语学习积累下来的词汇量,加上我出色的看源码能力。看懂了这个方法的注释及Android系统在哪里执行了此方法。

面试被问到怎么在子线程创建Looper?

仔细看注释。Initialize the current thread as a looper…See also: {@link #prepare()}

这个方法,作为开发人员不需要调用它,但是作为一个高级技工还是要多少了解一点的,系统在三个位置调用了此方法,但是我只关心了AndroidThread这个类,AndroidThread是个啥,自己去看吧。

/**

  • Initialize the current thread as a looper, marking it as an

  • application’s main looper. The main looper for your application

  • is created by the Android environment, so you should never need

  • to call this function yourself. See also: {@link #prepare()}

*/

public static void prepareMainLooper() {

prepare(false);

synchronized (Looper.class) {

if (sMainLooper != null) {

throw new IllegalStateException(“The main Looper has already been prepared.”);

}

sMainLooper = myLooper();

}

}

Looper#prepare()

面试的时候经常被问到一个线程可以有多个looper吗?

看源码注释就得到了答案。

throw new RuntimeException("Only one Looper may be created per thread");

怎么保证每个线程只有一个looper呢?这里用到了ThreadLocal。

在自己创建的子线程中,如果想创建Looper,那么只需要调用Looper.prepare(),就会为当前线程创建一个looper了。

private static void prepare(boolean quitAllowed) {

if (sThreadLocal.get() != null) {

throw new RuntimeException(“Only one Looper may be created per thread”);

}

sThreadLocal.set(new Looper(quitAllowed));

}

ThreadLocal机制

ThreadLocal是个什么东西呢,他是个复杂的机制,毕竟从JAVA1.2就加入了机制,保证了每个线程自己的变量…

本人简单的、带有误导性的科普是:

类似一个Map,key是当前线程id,value就是你要保存的值

一定要自己深入了解该机制

Looper#loop()

这个方法也很关键,消息能够执行,起了很大作用。虽然个人感觉能看的代码很少,但是都很精炼啊。

  1. 获取looper,得到MQ

  2. 循环MQ得到可执行的msg

  3. 通过msg自身,去到他该去的地方msg.target.dispatchMessage(msg);

  4. recycleUnchecked(),维护Message池

ps:曾经年少的我一度认为Looper就是主线程,完全因为这个loop()方法,当时看到在AndroidThread#main()中执行了Looper.loop(),而学过JAVA的都知道main()里面,如果没有耗时、子线程等其他操作,基本上执行到最后一行,就结束了。

但是为什么APP起来了,main()里面那么几行代码执行结束后,没有死掉呢。就是因为loop()里面有个for(;😉,当MQ中没有msg,那么会一直循环下去。

现在想来,还是太年轻了。这个只是一方面原因,其他线程也会调用Looper.prepare(),为自己创建looper,然后执行Looper.loop(),循环自己的MQ。

发现还是要多了解,多学习。

MQ#next()

这个方法负责把队列中的msg取出来,给到looper去执行。

这个方法也是一个for(;😉,当取到第一个msg的时候,如果没有到他该执行的时间,那么就等着,一直等,死等。得到可以执行的msg后,给到Looper。里面还有些native的方法,大家自己去看next()源码吧。

Handler#dispatchMessage()

在Looper#loop()中MQ#next()得到了msg,有这么一行msg.target.dispatchMessage(msg);,在之前讲到了这个target是发送msg的那个handler(多个handler的情况下区分)。根据不同情况,对msg进行分发。如果有callback对象(post方式发送消息,或者new Handler(runnable)),就去执行Runnable.run()。其他情况回调到handleMessage(),在创建handler的地方处理这个msg。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后,面试前该准备哪些资源复习?

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一下我面试期间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《Android开发七大模块核心知识笔记》

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

《960全网最全Android开发笔记》

面试字节两轮后被完虐,字节面试官给你的技术面试指南,请查收

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
注Android)**
[外链图片转存中…(img-a65ixxBY-1712623299690)]

最后,面试前该准备哪些资源复习?

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一下我面试期间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《Android开发七大模块核心知识笔记》

[外链图片转存中…(img-3siuINfo-1712623299690)]

[外链图片转存中…(img-Mf2KqmRZ-1712623299691)]

《960全网最全Android开发笔记》

[外链图片转存中…(img-KbKTvLDk-1712623299691)]

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-Ippft67a-1712623299691)]

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值