写点什么

往事只能回味!春招 Android 开发岗:我居然三天就拿到了 offer

用户头像
Android架构
关注
发布于: 19 小时前

[](

)2.1.5 Java 的常量池?不同 String 赋值方法,引用是否相等?


字面值是常量,在字节码中使用 id 索引,equals 相等引用不一定相等,Android 上 String 的构造函数会被虚拟机拦截,重定向到 StringFactory

[](

)2.1.6 HashMap 的实现?树化阈值?负载因子?


数组加链表加红黑树,默认负载因子0.75,树化阈值8,这部分比较常考,建议专门准备.(打个小广告 OWO,你也可以关注我的专栏,里面有一篇文章分析了 HashMap 和 ArrayMap)

[](

)2.1.7 Java 实现无锁同步


CAS 的实现如AtomicInteger等等

[](

)2.1.8 单例模式


  • 双重检查


public class Singleton {


private static volatile Singleton singleton;


private Singleton() {}


public static Singleton getInstance() {


if (singleton == null) {


synchronized (Singleton.class) {


if (singleton == null) {


singleton = new Singleton();


}


}


}


return singleton;


}


}


  • 反序列化安全,反射安全 枚举级单例,类加载时由 JVM 保证单例,反序列化不会生成新对象,另外一种反射安全是在构造函数中对单例进行检查如果存在则抛出异常.

[](

)2.1.9 锁的条件变量


信号量要与一个锁结合使用,当前线程要先获得这个锁,然后等待与这个锁相关联的信号量,此时该锁会被解锁,其他线程可以抢到这个锁,如果其他线程抢到了这个锁,那他可以通知这个信号量,然后释放该锁,如果此时第一个线程抢到了该锁,那么它将从等待处继续执行(应用场景,将异步回调操作封装为变为同步操作,避免回调地狱)


信号量与锁相比的应用场景不同,锁是服务于共享资源的,而信号量是服务于多个线程间的执行的逻辑顺序的,锁的效率更高一些.

[](

)2.1.10 ThreadLocal 原理


线程上保存着 ThreadLocalMap,每个 ThreadLocal 使用弱引用包装作为 Key 存入这个 Map 里,当线程被回收或者没有其他地方引用 ThreadLocal 时,ThreadLocal 也会被回收进而回收其保存的值

[](

)2.1.11 软引用,弱引用,虚引用


  • 软引用内存不够的时候会释放

  • 弱引用 GC 时释放

  • 虚引用,需要和一个引用队列联系在一起使用,引用了跟没引用一样,主要是用来跟 GC 做一些交互.

[](

)2.1.12 ClassLoader双亲委派机制


简单来说就是先把加载请求转发到父加载器,父加载器失败了,再自己试着加载

[](

)2.1.13 GC Roots 有这些


通过 System Class Loader 或者 Boot Class Loader 加载的 class 对象,通过自定义类加载器加载的 class 不一定是 GC Root:


  • 处于激活状态的线程

  • 栈中的对象

  • JNI 栈中的对象

  • JNI 中的全局对象

  • 正在被用于同步的各种锁对象

  • JVM 自身持有的对象,比如系统类加载器等。

[](

)2.1.14 GC 算法


| 名称 | 描述 | 优点 | 缺点 |


| --- | --- | --- | --- |


| 标记-清除算法 | 暂停除了 GC 线程以外的所有线程,算法分为“标记”和“清除”两个阶段,首先从 GC Root 开始标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。 | | 标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作 |


| 复制算法 | 将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉 | 这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效 | 复制算法的缺点显而易见,可使用的内存降为原来一半 |


| 标记-整理算法 | 标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的,标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。 | 标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高 | |


| 分代收集算法 | 是 java 的虚拟机的垃圾回收算法.基于编程中的一个事实,越新的对象的生存期越短,根据内存中对象的存活周期不同,将内存划分为几块,java 的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建 | | |


[](


)2.2 Android



[](

)2.2.1 Handler、MessageQueue 等一套东西讲一下,详细说了下源码。为什么主线程 loop 不会 ANR?


  • Android 线程模型就是消息循环,Looper 关联 MessageQueue,不断尝试从 MessageQueue 取出 Message 来消费,这个过程可能会被它自己阻塞.

  • 而 Handler 最终都调用 enqueueMessage(Message,when)入队的,延迟的实现时当前是时间加上延迟时间给消息指定一个执行的时间点,然后在 MessageQueue 找到插入位置,此时会判断是否需要唤醒线程来消费消息以及更新下次需要暂停的时间.

  • Message 知道要发到哪个 Handler 是因为 Message 把 Handler 保存到了 target.

  • Message 内部使用链表进行回收复用

[](

)2.2.2 View 事件以及 View 体系相关知识


建议看《Android 开发艺术探索》,这玩意三言两语讲不清楚

[](

)2.2.3 Android 中使用多线程的方法


  • 裸 new 一个 Thread(失控线程,不推荐)

  • RxJava 的调度器(io(优先级比低),密集计算线程(优先级比高,用于执行密集计算任务),安卓主线程, Looper 创建(实际上内部也是创建了 Handler))

  • Java Executor 框架的 Executors#newCachedThreadPool(),不会造成资源浪费,60 秒没有被使用的线程会被释放

  • AsyncTask,内部使用 FutureTask 实现,通过 Handler 将结果转发到主线程,默认的 Executor 是共用的,如果同时执行多个 AsyncTask,就可能需要排队,但是可以手动指定 Executor 解决这个问题,直接 new 匿名内部类会保存外部类的引用,可能会导致内存泄漏

  • Android 线程模型提供的 Handler 和 HandlerThread


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


使用 IntentService


  • IntentService 和 Service 的区别——没什么区别,其实就是开了个 HandlerThread,让它不要在主线程跑耗时任务

[](

)2.2.4 RecyclerView 复用缓存


建议看一下,这个可能会被问,不过我运气好没被问到.

[](

)2.2.5 Activity 启动流程


网上有很多相关的文章,可以自己结合源码去看一下,如果能讲个大概的话也是很加分的.

[](

)2.2.6 JNI(除非你自己说你会,否则不是很常考)


  • 可避免的内存拷贝,直接传递对象,到 C 层是一个 jobject 的指针,可以使用 jmethodID 和 jfiledID 访问方法和字段,无需进行内存拷贝,使用直接缓冲区也可以避免内存拷贝.

  • 无法避免的内存拷贝,基本类型数组,无法避免拷贝,因为 JVM 不信任 C 层的任何内存操作,特别是字符串操作,因为 Java 的字符串与 C/C++的字符串所使用的数据类型是不一样的 C/C++使用 char 一个字节(1 字节=8 位)或 wchar_t 是四字节.而 jstring 和 jchar 使用的是 UTF-16 编码使用双字节.(Unicode 是兼容 ASCII,但不兼容 GBK,需要自己转换)

  • 自己创建的局部引用一定要释放,否则一直持有内存泄漏

  • 非局部引用方法返回后就会失效,除非创建全局引用,jclass 是一个 jobject,方法外围使用时需要创建全局引用,jmethodID 和 jfiledID 不需要.

  • JNI 是通过 Java 方法映射到 C 函数实现的,如果使用这种方法,函数必须以 C 式接口导出(因为 C++会对名字做修饰处理),当然也可以在 JNI_OnLoad 方法中注册.

  • JNIEnv 是线程独立的,JNI 中使用 pthread 创建的线程没有 JNIEnv,需要 AttachCurrentThread 来获取 JNIEnv,不用时要 DetachCurrentThread


[](


)2.3 专业课



[](

)2.3.1 TCP 和 UDP 的根本区别?


数据报,流模式,TCP 可靠,包序不对会要求重传,UDP 不管,甚至不能保证送到

[](

)2.3.2 TCP 三次握手


这个被问的几率非常的大,几乎等于必问,建议专门花时间去看.

[](

)2.3.3 Http 和 Https


CA 证书,中间机构,公钥加密对称秘钥传回服务端,一个明文一个加密,SSL 层,中间人攻击,[参考 link](


)


[](


)2.4 ACM




对于 ACM,比较常考链表的题,不常刷算法的同学一定不要对其有抵触心理.


你可能会问为什么要 ACM?网上答案说的什么提高代码质量,能够更好地阅读别人的代码这些理由有一定道理,但对于我们去面试的人而言最重要的是 ACM 是面试官考察你编码能力的最直接的手段,所以不用说这么多废话刷题就够了.


刷题的话,建议去刷 leetcode,题号在 200 以内的,简单和中等难度,不建议刷困难,因为面试的时候基本就不会出,没人愿意在那里等你想一个半个小时的.


在面试官面前现场白板编程时,可以先把思路告诉面试官,写不写得出来是另外一回事,时间复杂度和空间复杂度是怎么来的一定要搞清楚.在编码时也不一定要写出最佳的时间和空间的算法,但推荐你写出代码量最少,思路最清晰的,这样面试官看得舒服,你讲得也舒服.


下面是我在网上收集或者是在实际中遇到过的 ACM 题,基本上在 leetcode 上也都有类似的.

[](

)2.4.1 数组、链表


  • 链表逆序(头条几乎是必考的)


public ListNode reverseList(ListNode head)


{


if (head == null)


{


return null;


}


if (head.next == null)


{


return head;


}


ListNode prev = null;


ListNode current = head;


while (current != null)


{


ListNode next = current.next;


current.next = prev;


prev = current;


current = next;


}


return prev;


}


  • 删除排序数组中的重复项


public int removeDuplicates(int[] nums)


{


int length = nums.length;


if (length == 0 || length == 1)


{


return length;


}


int size = 1;


int pre = nums[0];


for (int i = 1; i < length; )


{


if (nums[i] == pre)


{


i++;


} else


{


pre = nums[size++] = nums[i++];


}


}


return size;


}


  • 数组中找到重复元素

  • n 个长为 n 的有序数组,求最大的 n 个数

  • 用 O(1)的时间复杂度删除单链表中的某个节点 把后一个元素赋值给待删除节点,这样也就相当于是删除了当前元素,只有删除最后一个元素的时间为 o(N)平均时间复杂度仍然为 O(1)


public void deleteNode(ListNode node) {


ListNode next = node.next;


node.val = next.val;


node.next = next.next;


}


  • 删除单链表的倒数第 N 个元素 两个指针,第一个先走 N 步第二个再走,时间复杂度为 O(N),[参考 link](


)


public ListNode removeNthFromEnd(ListNode head, int n) {


if (head == null)


{


return null;


}


if (head.next == null)


{


return n == 1 ? null : head;


}


int size = 0;


ListNode point = head;


ListNode node = head;


do


{


if (size >= n + 1)


{


point = point.next;


}


node = node.next;


size++;


} while (node != null);


if (size == n)


{


return head.next;


}


node = point.next;


point.next = node == null ? null : node.next;


return head;


}


  • 从长序列中找出前 K 大的数字

  • 用数组实现双头栈


public static class Stack<T>


{


public Stack(int cap)


{


if (cap <= 0)


{


throw new IllegalArgumentException();


}


array = new Object[cap];


left = 0;


right = cap - 1;


}


private Object[] array;


private int left;


private int right;


public void push1(T val)


{


int index = left + 1;


if (index < right)


{


array[index] = val;


}


left = index;


}


@SuppressWarnings("unchecked")


public T pop1()


{


if (left > 0)


{


return (T)array[left--];


}


return null;


}


public void push2(T val)


{


int index = right - 1;


if (index > left)


{


array[index] = val;


}


right = index;


}


@SuppressWarnings("unchecked")


public T pop2()


{


if (right < array.length)


{


return (T)array[right++];


}


return null;


}


}


  • 两个链表求和,返回结果也用链表表示 1 -> 2 -> 3 + 2 -> 3 -> 4 = 3 -> 5 -> 7


public ListNode addTwoNumbers(ListNode node1, ListNode node2)


{


ListNode head = null;


ListNode tail = null;


boolean upAdd = false;


while (!(node1 == null && node2 == null))


{


ListNode midResult = null;


if (node1 != null)


{


midResult = node1;


node1 = node1.next;


}


if (node2 != null)


{


if (midResult == null)


{


midResult = node2;


} else


{


midResult.val += node2.val;


}


node2 = node2.next;


}


if (upAdd)


{


midResult.val += 1;


}


if (midResult.val >= 10)


{


upAdd = true;


midResult.val %= 10;


}


else


{


upAdd = false;


}


if (head == null)


{


head = midResult;


tail = midResult;


} else


{


tail.next = midResult;


tail = midResult;


}


}


if (upAdd)


{


tail.next = new ListNode(1);


}


return head;


}


  • 交换链表两两节点


public ListNode swapPairs(ListNode head)


{


if (head == null)


{


return null;


}


if (head.next == null)


{


return head;


}


ListNode current = head;


ListNode after = current.next;


ListNode nextCurrent;


head = after;


do


{


nextCurrent = after.next;


after.next = current;

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
往事只能回味!春招 Android 开发岗:我居然三天就拿到了offer