面试题 - Android - 系统 SDK 相关

211 阅读9分钟

我会逐个详细回答这些问题。由于内容较多,我们先回答前几个问题:

1. Android 系统架构组成

Android 系统架构自下而上分为五层:

  1. Linux 内核层

    • 提供底层驱动
    • 内存管理
    • 进程管理
    • 安全机制
  2. 硬件抽象层(HAL)

    • 为硬件提供标准接口
    • 使上层不必关心底层硬件实现
  3. 原生系统库和Android运行时环境

    • 核心库
    • ART(Android Runtime)
    • 系统C/C++库
  4. 应用框架层

    • Activity Manager
    • Window Manager
    • Content Provider
    • View System
    • Package Manager
  5. 应用层

    • 系统应用
    • 第三方应用

2. SharedPreferences 线程安全性

SharedPreferences 的线程安全性分析:

// SharedPreferences 的读操作是线程安全的
Map<String, ?> map = sharedPreferences.getAll();

// 写操作需要注意
SharedPreferences.Editor editor = sharedPreferences.edit();

commit() 和 apply() 的区别:

  1. commit()

    • 同步写入
    • 有返回值
    • 会阻塞主线程
  2. apply()

    • 异步写入
    • 无返回值
    • 不会阻塞主线程
    • 先写入内存,后写入磁盘

推荐使用场景:

  • 需要立即知道写入结果:使用 commit()
  • 一般情况:使用 apply()

3. Serializable 和 Parcelable 的区别

对比表格:

特性SerializableParcelable
实现难度简单相对复杂
效率较低
使用场景本地存储、网络传输内存间数据传递
原理反射序列化接口

示例代码:

// Serializable 实现
public class User implements Serializable {
    private String name;
    private int age;
}

// Parcelable 实现
public class User implements Parcelable {
    private String name;
    private int age;
    
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(age);
    }
    
    protected User(Parcel in) {
        name = in.readString();
        age = in.readInt();
    }
    
    public static final Creator<User> CREATOR = new Creator<User>() {...}
}

4. Android 7.0 新特性

主要特性:

  1. 文件共享变更
// 7.0之前
Uri photoUri = Uri.fromFile(file);

// 7.0之后
Uri photoUri = FileProvider.getUriForFile(context, 
    "包名.fileprovider", file);
  1. 多窗口支持
<!-- AndroidManifest.xml -->
<activity 
    android:resizeableActivity="true"
    android:supportsPictureInPicture="true"/>
  1. 通知栏增强
  2. 后台优化
  3. 电池优化
  4. Java 8 特性支持

5. ArrayMap 和 HashMap 的区别

主要区别:

  1. 数据结构

    • HashMap: 哈希表实现
    • ArrayMap: 两个数组实现(一个存储hash值,一个存储key-value)
  2. 内存占用

// HashMap 每个实例占用约32字节
HashMap<String, String> hashMap = new HashMap<>();

// ArrayMap 每个实例占用约24字节
ArrayMap<String, String> arrayMap = new ArrayMap<>();
  1. 使用场景
    • HashMap: 大数据量(1000以上)
    • ArrayMap: 小数据量(1000以下)

详细解释

1. 数据结构对比

HashMap 结构:

graph TD
    A[HashMap] --> B[数组 + 链表/红黑树]
    B --> C[数组]
    B --> D[链表]
    B --> E[红黑树]
    C --> F[当链表长度>8时]
    F --> G[转换为红黑树]

ArrayMap 结构:

graph TD
    A[ArrayMap] --> B[两个数组]
    B --> C[mHashes数组]
    B --> D[mArray数组]
    C --> E[存储key的hash值]
    D --> F[存储key-value对]

2. 工作原理详解

HashMap 工作原理:
// 简化的HashMap原理示意
public class HashMap<K,V> {
    // 存储数据的数组
    Node<K,V>[] table;
    
    public V put(K key, V value) {
        // 1. 计算hash值
        int hash = hash(key);
        // 2. 计算数组下标
        int index = hash & (table.length - 1);
        // 3. 如果发生碰撞,形成链表或红黑树
        if (collision) {
            // 处理碰撞
        }
    }
}

存储示意图:

索引:  0    1    2    3    4    5
     +----+----+----+----+----+----+
     | ↓  |    | ↓  |    | ↓  |    |
     +----+----+----+----+----+----+
       |         |         |
       ↓         ↓         ↓
     [k1,v1]  [k2,v2]   [k3,v3]
       ↓         ↓
     [k4,v4]  [k5,v5]
ArrayMap 工作原理:
public class ArrayMap<K,V> {
    // 存储hash值的数组
    int[] mHashes;
    // 存储key-value的数组
    Object[] mArray;
    
    public V put(K key, V value) {
        // 1. 计算hash值
        int hash = key == null ? 0 : key.hashCode();
        // 2. 二分查找位置
        int index = binarySearch(mHashes, hash);
        // 3. 插入数据
        // hash值存入mHashes
        // key-value存入mArray
    }
}

存储示意图:

mHashes数组:
     +----+----+----+----+
     | h1 | h2 | h3 | h4 |  (hash值按大小排序)
     +----+----+----+----+

mArray数组:
     +----+----+----+----+----+----+----+----+
     | k1 | v1 | k2 | v2 | k3 | v3 | k4 | v4 |
     +----+----+----+----+----+----+----+----+

3. 主要区别总结

  1. 内存占用:
// HashMap每个节点都需要额外的空间存储链表信息
class Node<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;  // 额外的引用
}

// ArrayMap直接使用数组,更节省内存
int[] mHashes;       // 仅存储hash值
Object[] mArray;     // 直接存储key-value
  1. 查找效率:

HashMap查找] --> O(1)时间复杂度

ArrayMap查找 --> O(log n)时间复杂度

  1. 扩容机制:
// HashMap扩容
newCapacity = oldCapacity << 1  // 扩容为2倍

// ArrayMap扩容
if (size < 4) newCapacity = 4;
else if (size < 8) newCapacity = 8;
else newCapacity = size + (size >> 1);  // 扩容为1.5倍

4. 使用建议

// 数据量小(<1000)时使用ArrayMap
ArrayMap<String, User> userMap = new ArrayMap<>();  // 省内存

// 数据量大时使用HashMap
HashMap<String, User> userMap = new HashMap<>();    // 查询快

性能对比:

  • 数据量小时:ArrayMap 内存占用更少,性能接近
  • 数据量大时:HashMap 查询性能更好

5. 实际应用场景

// 场景1:配置信息存储
ArrayMap<String, String> config = new ArrayMap<>();
config.put("theme", "dark");
config.put("language", "zh");

// 场景2:大量数据缓存
HashMap<String, Bitmap> imageCache = new HashMap<>();
imageCache.put("url1", bitmap1);
imageCache.put("url2", bitmap2);

这样的解释是否清晰易懂?如果还有任何疑问,请随时询问。

7. SparseArray 代替 HashMap 的原因

主要优势:

  1. 内存效率

    • 避免装箱拆箱
    • 数组实现,内存占用更小
  2. 适用场景

// 不推荐
HashMap<Integer, Object> map = new HashMap<>();

// 推荐
SparseArray<Object> array = new SparseArray<>();

详细讲解

让我用通俗易懂的方式详细解释 SparseArray 的底层原理。

1. 数据结构对比

想象一下两个不同的储物柜:

HashMap 的储物柜:

[门牌号(key)] -> [物品(value)]
   1001     ->    "手机"
   1002     ->    "钱包"
   1003     ->    "钥匙"

SparseArray 的储物柜:

位置索引:  0    1    2
键数组:  [1001, 1002, 1003]  // 按顺序排列的int数组
值数组:  ["手机","钱包","钥匙"] // 对应的Object数组

2. 核心实现原理

public class SparseArray<E> {
    // 两个基本数组
    private int[] mKeys;        // 存储键(必须是int类型)
    private Object[] mValues;   // 存储值
    private int mSize;          // 实际大小
    
    // 二分查找实现
    private int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;
        while (lo <= hi) {
            int mid = (lo + hi) >>> 1;
            int midVal = array[mid];
            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;
            }
        }
        return ~lo;  // 找不到时返回应该插入的位置
    }
}

3. 工作流程图

graph TD
    A[查找/插入key] --> B{是否为空?}
    B -->|是| C[直接插入到数组末尾]
    B -->|否| D[二分查找key位置]
    D --> E{是否找到?}
    E -->|是| F[更新对应位置的value]
    E -->|否| G[移动数组元素并插入]

4. 性能优势

  1. 内存效率
// HashMap 的一个整数键值对
class Entry<K,V> {
    final int hash;       // 4字节
    final Integer key;    // 16字节(对象)
    V value;             // 4字节起
    Entry<K,V> next;     // 4字节
}
// 总计:至少28字节/条

// SparseArray 的一个整数键值对
int key;     // 4字节
Object value; // 4字节
// 总计:8字节/条
  1. 避免装箱拆箱
// HashMap 需要装箱
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "value");  // 1 被装箱为 Integer 对象

// SparseArray 直接使用基本类型
SparseArray<String> array = new SparseArray<>();
array.put(1, "value");  // 直接使用 int

5. 使用场景示例

// 推荐场景:键为int的小数据量场景
SparseArray<User> userArray = new SparseArray<>();
userArray.put(userId, user);           // 插入
User user = userArray.get(userId);     // 查找
userArray.remove(userId);              // 删除

// 特别适合的场景
SparseArray<View> viewCache = new SparseArray<>();
viewCache.put(R.id.button, buttonView);  // 视图缓存

6. 性能对比总结

操作HashMapSparseArray
内存占用
随机访问O(1)O(log n)
插入操作O(1)O(n)
删除操作O(1)O(n)
适用场景大数据量小数据量(<1000)

7. 最佳实践建议

// 好的使用方式
private SparseArray<View> viewCache = new SparseArray<>(10); // 指定初始容量
viewCache.put(viewId, view);

// 避免频繁增删的场景
for (int i = 0; i < 1000; i++) {
    // 不推荐在循环中频繁操作 SparseArray
    sparseArray.put(i, value);  // 每次插入都可能需要数组移动
}

8. 关键优势总结

  1. 内存效率高

    • 避免了HashMap的Entry对象开销
    • 避免了Integer的装箱开销
  2. 适合特定场景

    • key为int类型
    • 数据量较小
    • 内存敏感的场景
  3. Android系统优化

    • Android系统内部大量使用
    • 针对移动设备优化
    • 与Android系统配合更好

通过以上详细解释,我们可以看到 SparseArray 是Android系统专门针对内存优化设计的一个数据结构,特别适合移动设备中键为整型的小数据量场景。虽然在某些操作上性能不如 HashMap,但在特定场景下的内存优势使它成为了Android开发中的重要工具。

8. PathClassLoader 和 DexClassLoader 区别

对比:

类加载器加载路径使用场景
PathClassLoader/data/app系统类和已安装应用
DexClassLoader自定义路径动态加载dex/jar/apk

示例代码:

// DexClassLoader 使用示例
DexClassLoader dexClassLoader = new DexClassLoader(
    dexPath,     // dex文件路径
    optimizedDirectory,  // 优化后的dex文件存放路径
    librarySearchPath,  // native库的搜索路径
    parent      // 父类加载器
);

9. HttpClient 与 HttpUrlConnection 的区别

主要区别:

  1. API设计

    • HttpClient: 面向对象,功能丰富
    • HttpURLConnection: 简单轻量
  2. 使用示例

// HttpURLConnection (推荐)
URL url = new URL("http://example.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();

// HttpClient (已废弃)
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet("http://example.com");
HttpResponse response = client.execute(request);

替代原因:

  1. 维护成本高
  2. 版本兼容性问题
  3. 体积较大

10. Lifecycle 原理和使用场景

Lifecycle 是 Android Jetpack 组件之一,用于管理生命周期。

原理图:

           onCreate()
              ↓
           onStart()
              ↓
           onResume()
              ↓
           onPause()
              ↓
           onStop()
              ↓
           onDestroy()

使用示例:

class MyObserver : LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        // 处理 onResume 事件
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        // 处理 onPause 事件
    }
}

// 在 Activity 中使用
lifecycle.addObserver(MyObserver())

使用场景:

  1. 生命周期感知组件
  2. 避免内存泄漏
  3. 数据持久化
  4. 后台任务管理

11. Android 签名机制

Android 签名版本对比:

版本特点算法
V1(JAR签名)Android 7.0以下SHA1withRSA
V2(APK签名)Android 7.0引入整个文件块验证
V3Android 9.0引入支持密钥轮替
V4Android 11引入支持外部存储

签名过程:

graph LR
A[生成密钥对] --> B[生成keystore文件]
B --> C[使用私钥签名]
C --> D[使用公钥验证]

12. Android APK构建流程

构建流程图:

graph TD
A[Java源码] --> B[Java编译器]
B --> C[.class文件]
C --> D[dx工具]
D --> E[.dex文件]
E --> F[apkbuilder]
F --> G[未签名的APK]
G --> H[签名工具]
H --> I[最终APK]

关键步骤代码示例:

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt')
            signingConfig signingConfigs.release
        }
    }
}

13. Android 8.0、9.0新特性

Android 8.0:

  1. 通知渠道
NotificationChannel channel = new NotificationChannel(
    CHANNEL_ID,
    "通知渠道名称",
    NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
  1. 后台限制
  2. 画中画模式
  3. 自适应图标

Android 9.0:

  1. 刘海屏适配
<meta-data
    android:name="android.notch_support"
    android:value="true"/>
  1. 限制后台应用访问传感器
  2. 限制非SDK接口访问

14. Android 10适配要点

主要变更:

  1. 暗黑模式
<!-- 主题支持 -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
  1. 分区存储
// 适配示例
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    // 使用 MediaStore API
    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DISPLAY_NAME, "图片.jpg");
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    Uri uri = context.getContentResolver().insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
  1. 后台定位限制
  2. 设备唯一标识符限制

15. APK安装过程

安装流程:

graph TD
A[复制APK到指定目录] --> B[解析AndroidManifest.xml]
B --> C[验证签名]
C --> D[dex优化]
D --> E[注册四大组件]
E --> F[完成安装]

16. Java与JS互调

Android调用JS:

// 方式1:通过WebView
webView.loadUrl("javascript:function()");

// 方式2:通过接口注入
webView.addJavascriptInterface(new JsInterface(), "android");

JS调用Android:

// 方式1:通过接口调用
window.android.method();

// 方式2:通过URL Scheme
location.href = "myapp://method?param=value";

优化方案:

  1. 使用WebView池
  2. 预加载WebView
  3. 数据预加载

17. JNI实现Java与C++互调

基本流程:

// Java代码
public class NativeClass {
    static {
        System.loadLibrary("native-lib");
    }
    
    // 声明native方法
    public native String stringFromJNI();
}
// C++代码
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_NativeClass_stringFromJNI(
    JNIEnv* env,
    jobject /* this */) {
    return env->NewStringUTF("Hello from C++");
}

18. App启动流程

启动流程图:

graph TD
A[点击图标] --> B[Launcher进程]
B --> C[AMS]
C --> D[Zygote进程]
D --> E[fork新进程]
E --> F[ActivityThread]
F --> G[Application onCreate]
G --> H[Activity onCreate]

关键代码:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 应用初始化
    }
}

优化建议:

  1. 延迟初始化
  2. 异步初始化
  3. 减少主线程耗时操作
  4. 使用启动器