边独立,边面试,两手都要抓,两手都要硬。
1. 给定数组array与一个数字num,要求从array中找出两个数,其和为num,并返回这两个数的下标
fun findTwoSum(array: IntArray, num: Int): Pair<Int, Int>? {
// 创建一个哈希表来存储数组中的数字及其索引
val map = mutableMapOf<Int, Int>()
// 遍历数组
for (i in array.indices) {
val complement = num - array[i]
// 检查哈希表中是否存在这个补数
if (map.containsKey(complement)) {
// 如果存在,返回这个数及其补数的索引
return Pair(map[complement]!!, i)
}
// 将当前数字及其索引存入哈希表
map[array[i]] = i
}
// 如果没有找到符合条件的数对,返回null
return null
}
解读:
- 创建一个map,在遍历的过程中,报错所有数字以及其对应的坐标,数字为键,坐标为值;
- 遍历时,计算出num与当前元素的差值,然后试图从map中索引,如果有索引,则命中,如果没有则继续遍历;
- map的作用,相当于取代了双层遍历法其中的一层遍历;
2. jvm如何判断一个对象可以被回收?
- 引用计数法 优点:实现简单,时间复杂度低; 缺点:无法解决循环引用问题;
- 可达性分析算法 通过GC Roots开始标记所有可达的对象;可以被标记的,认为是活的对象,未被标记的,则认为已经不需要,可以被回收;
2.1 有哪些可以作为GC Root?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即Native方法)引用的对象。
2.2 垃圾回收的过程是怎样的?
有三种策略:
- 标记清除:首先标记所有可达对象,然后清除所有未被标记的对象;
- 复制算法:将存活的对象,从一块内存复制到另外一块内存,然后清空原来的内存;
- 标记-压缩: 首先标记所有可达的对象,然后将所有存活的对象压缩到内存的一端,清除便捷以外的空间;
3. Android的Handler是如何造成内存泄露的?
非静态内部类或者匿名内部类,会隐式的持有外部类的对象,如果在Activity中声明一个Handler,而Handler执行延迟任务,在任务结束前,Activity已经被销毁了,则Activiey泄露了;
#### 3.1 如何解决Handler的内存泄露?
- 将
Handler
声明为静态内部类,并且使用弱引用持有外部类的引用; - 在Activity的onDestroy中,使用
Handler
的removeCallbacksAndMessages
方法,移除所有未执行的消息和回调;
#### 3.2 有没有非Activity的其他内存泄露的场景?
- 单例持有外部对象;
- 静态持有外部对象;
- 未取消注册监听器或者回调;
- WebView导致内存泄露;
- 异步任务; 总结来说,就是生命周期长的对象,持有了一个生命周期短的对象的引用;
### 4. 应用启动白屏如何排查?如何解决?
- 在应用入口,比如Application的onCreate、启动Activity的onCreate中,做耗时检查,如果耗时异常,具体检查代码;
- 如果是Activity的onCreate中耗时长,检查是否为布局过于复杂,造成的布局解析耗时长,可以使用异步布局处理;
- 如果为系统机制问题导致,可以配置主题或者使用Splash库来做过渡;
5. 线上问题如何排查?
- 运营种子用户,提前内测,按照用户反馈,复现问题场景;
- 集成bugly等线上日志抓取工具,按照机型、系统等信息,复现问题场景;
- 如果有可能,可以通过热更新修复问题;
6. 不同任务类型(IO密集型/计算密集型)任务,如何分配线程池策略?
IO密集型任务
特点 IO密集型任务主要涉及等待外部资源(如文件系统、网络请求、数据库操作等),因此大部分时间处于等待状态,而非消耗CPU。
策略 对于IO密集型任务,可以使用较大的线程池,因为线程在等待IO操作完成时不会消耗大量CPU资源。
线程池配置 通常,线程池的大小可以设置为CPU核心数的2倍或更多。一个常见的公式是:
线程池大小 = CPU核心数 * 2
或更大,根据实际情况调整。
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class IOIntensiveTaskExample {
private static final int N_THREADS = Runtime.getRuntime().availableProcessors() * 2;
public static void main(String[] args) {
ExecutorService ioThreadPool = Executors.newFixedThreadPool(N_THREADS);
for (int i = 0; i < 100; i++) {
ioThreadPool.submit(() -> {
// 模拟IO操作
performIOOperation();
});
}
ioThreadPool.shutdown();
}
private static void performIOOperation() {
try {
// 模拟IO操作
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
计算密集型任务
特点 计算密集型任务主要消耗CPU资源,执行过程中几乎不涉及等待时间,因此可以充分利用CPU。
策略 对于计算密集型任务,线程池的大小应设置为接近CPU核心数。过多的线程会导致频繁的上下文切换,反而降低性能。
线程池配置 通常,线程池的大小可以设置为CPU核心数。一个常见的公式是:
线程池大小 = CPU核心数
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class CPUIntensiveTaskExample {
private static final int N_THREADS = Runtime.getRuntime().availableProcessors();
public static void main(String[] args) {
ExecutorService cpuThreadPool = Executors.newFixedThreadPool(N_THREADS);
for (int i = 0; i < 100; i++) {
cpuThreadPool.submit(() -> {
// 模拟计算操作
performCPUTask();
});
}
cpuThreadPool.shutdown();
}
private static void performCPUTask() {
// 模拟计算操作
for (int i = 0; i < 1000000; i++) {
Math.pow(i, 2);
}
}
}
混合任务 在实际应用中,有些任务可能既包含IO操作又包含计算操作。这种情况下,可以考虑使用不同的线程池分别处理不同类型的任务,或者根据任务的主要特征选择适当的线程池配置。
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class MixedTaskExample {
private static final int IO_THREADS = Runtime.getRuntime().availableProcessors() * 2;
private static final int CPU_THREADS = Runtime.getRuntime().availableProcessors();
public static void main(String[] args) {
ExecutorService ioThreadPool = Executors.newFixedThreadPool(IO_THREADS);
ExecutorService cpuThreadPool = Executors.newFixedThreadPool(CPU_THREADS);
for (int i = 0; i < 100; i++) {
ioThreadPool.submit(() -> {
// 模拟IO操作
performIOOperation();
});
cpuThreadPool.submit(() -> {
// 模拟计算操作
performCPUTask();
});
}
ioThreadPool.shutdown();
cpuThreadPool.shutdown();
}
private static void performIOOperation() {
try {
// 模拟IO操作
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void performCPUTask() {
// 模拟计算操作
for (int i = 0; i < 1000000; i++) {
Math.pow(i, 2);
}
}
}
总结
IO密集型任务:使用较大的线程池,通常为CPU核心数的2倍或更多,以应对大量的IO等待时间。 计算密集型任务:使用较小的线程池,接近CPU核心数,以充分利用CPU资源,避免过多的线程上下文切换。 混合任务:根据任务的主要特征,分别使用不同的线程池处理IO操作和计算操作。 通过合理配置线程池,可以有效提高应用程序的性能和资源利用率。