11、联系人头像:读取头像、保存头像、Bitmap压缩与Glide加载

好,咱们今天来聊聊联系人头像。这个功能说实话,每个做通讯录的App都绕不开。我个人觉得,头像处理是联系人系统里最「接地气」也最容易踩坑的地方。你想想看,用户拍一张照片,可能几兆大小,直接往数据库里塞?那性能直接崩了。

所以这一章,我会带你走一遍完整的流程:从ContactsContract.Contacts.Photo读取头像,到保存到Data表,再到Bitmap压缩,最后用Glide优雅加载。嗯,每一步都有讲究。

核心知识点一览:

  • 读取系统联系人头像(两种方式)
  • 保存头像到ContactsContract.Data表
  • Bitmap压缩策略(质量压缩+尺寸压缩)
  • Glide加载头像的最佳实践
读取头像 ContactsContract.Photo Bitmap压缩 质量+尺寸压缩 保存到Data表 PHOTO字段 Glide加载头像 缓存+圆形变换 显示头像 ImageView ① 读取 ② 压缩 ③ 保存 ④ 加载 ⑤ 展示

11.1 读取联系人头像

系统联系人的头像,其实存在两个地方。一个是ContactsContract.Contacts表的PHOTO_ID字段,另一个是ContactsContract.Data表的PHOTO字段。我个人习惯直接查Data表,因为更直接,少一次关联查询。

来看代码:

// 方式一:通过PHOTO_ID查询(不推荐,多一次查询)
String photoId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_ID));
// 然后拿着photoId再去Data表查...

// 方式二:直接查Data表(推荐)
String[] projection = new String[]{
    ContactsContract.Data.CONTACT_ID,
    ContactsContract.Data.PHOTO
};
String selection = ContactsContract.Data.CONTACT_ID + " = ? AND " +
    ContactsContract.Data.MIMETYPE + " = ?";
String[] selectionArgs = new String[]{
    contactId,
    ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE
};
Cursor dataCursor = getContentResolver().query(
    ContactsContract.Data.CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    null
);
if (dataCursor != null && dataCursor.moveToFirst()) {
    byte[] photoBytes = dataCursor.getBlob(
        dataCursor.getColumnIndex(ContactsContract.Data.PHOTO)
    );
    if (photoBytes != null) {
        Bitmap bitmap = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
        // 拿到bitmap了
    }
    dataCursor.close();
}

💡 小提示:PHOTO字段存的是byte[],直接拿BitmapFactory解码就行。但注意,有些系统联系人可能没有头像,photoBytes会是null,记得判空。

11.2 保存头像到Data表

保存头像,说白了就是往Data表里插入一条MIMETYPE为Photo.CONTENT_ITEM_TYPE的记录。这里有个坑——同一个联系人只能有一条Photo记录。如果你重复插入,系统会报错。

我建议的做法是:先查一下有没有已有的Photo记录,有就更新,没有才插入。

private void saveContactAvatar(Context context, long contactId, byte[] compressedBytes) {
    ContentValues values = new ContentValues();
    values.put(ContactsContract.Data.RAW_CONTACT_ID, getRawContactId(context, contactId));
    values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
    values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, compressedBytes);

    // 先尝试更新
    int updated = context.getContentResolver().update(
        ContactsContract.Data.CONTENT_URI,
        values,
        ContactsContract.Data.RAW_CONTACT_ID + " = ? AND " +
        ContactsContract.Data.MIMETYPE + " = ?",
        new String[]{
            String.valueOf(getRawContactId(context, contactId)),
            ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE
        }
    );

    // 如果更新条数为0,说明没有旧记录,执行插入
    if (updated == 0) {
        context.getContentResolver().insert(
            ContactsContract.Data.CONTENT_URI,
            values
        );
    }
}

⚠️ 注意:这里用的是RAW_CONTACT_ID,不是CONTACT_ID。因为Data表关联的是RawContact。你需要先通过ContactId查到对应的RawContactId。我曾经在这里卡了半天,一直插不进去,后来才发现是ID搞错了。

11.3 Bitmap压缩——别让大图撑爆数据库

用户从相册选一张照片,可能是3MB、5MB甚至更大。直接存到Data表?数据库会哭的。而且系统对PHOTO字段有大小限制,我记得大概是64KB左右?不同厂商可能不一样。所以压缩是必须的。

我一般做两步压缩:

  1. 尺寸压缩:把Bitmap缩放到一个合理的尺寸,比如256x256。联系人头像不需要太大。
  2. 质量压缩:用JPEG格式,质量设到70-80,肉眼几乎看不出差别。
public static byte[] compressAvatar(Bitmap original) {
    // 第一步:尺寸压缩
    int targetSize = 256;
    float scale = Math.min(
        (float) targetSize / original.getWidth(),
        (float) targetSize / original.getHeight()
    );
    int newWidth = Math.round(original.getWidth() * scale);
    int newHeight = Math.round(original.getHeight() * scale);
    Bitmap scaledBitmap = Bitmap.createScaledBitmap(original, newWidth, newHeight, true);

    // 第二步:质量压缩
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 75, baos);
    byte[] result = baos.toByteArray();

    // 别忘了回收
    if (scaledBitmap != original) {
        scaledBitmap.recycle();
    }
    return result;
}

📊 压缩效果参考:

压缩步骤 原始大小 压缩后大小 耗时
仅尺寸压缩(256x256) 3.2 MB ~180 KB ~15ms
尺寸+质量压缩(75%) 3.2 MB ~25 KB ~20ms

你看,从3.2MB压到25KB,缩小了100多倍,而且肉眼几乎看不出区别。这就是压缩的魅力。

11.4 Glide加载头像——优雅且高效

头像存好了,怎么加载?用Glide。为什么不用Picasso或者直接BitmapFactory?Glide有几个天然优势:

  • 自动缓存:内存缓存+磁盘缓存,不用自己写LRU
  • 圆形变换:一行代码搞定圆形头像
  • 占位图:加载中显示默认头像
  • 生命周期感知:Activity销毁自动取消加载

加载联系人头像的典型写法:

// 从Uri加载
Uri contactUri = ContentUris.withAppendedId(
    ContactsContract.Contacts.CONTENT_URI, contactId
);
Uri photoUri = Uri.withAppendedPath(contactUri, 
    ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
);

Glide.with(context)
    .load(photoUri)
    .placeholder(R.drawable.default_avatar)
    .error(R.drawable.default_avatar)
    .circleCrop()  // 圆形裁剪
    .skipMemoryCache(false)  // 启用内存缓存
    .diskCacheStrategy(DiskCacheStrategy.ALL)  // 磁盘缓存
    .into(imageView);

💡 个人经验:如果你发现Glide加载系统联系人头像特别慢,可以试试直接传byte[]。把之前从Data表查到的photoBytes用.load(photoBytes)加载,跳过Uri解析那一步。我在项目里试过,速度能快30%左右。

11.5 避坑指南

做头像功能,我踩过的坑不少。挑几个典型的说说:

  • OOM问题:直接解码大图会内存溢出。一定要先通过BitmapFactory.OptionsinSampleSize做采样压缩,或者像我上面那样先缩放到小尺寸。
  • 权限问题:Android 11以上,读取联系人头像需要READ_CONTACTS权限,写入需要WRITE_CONTACTS。别忘了动态申请。
  • ContentProvider事务:批量保存头像时,建议用ContentProviderOperation批量操作,不要一条一条insert,效率差很多。
  • Glide缓存失效:如果用户更新了头像,Glide可能还显示旧图。这时候可以调用Glide.with(context).clear(imageView)清除缓存,或者给Uri加一个版本号参数。

⚠️ 我曾经遇到一个线上bug:用户反馈头像不更新。查了半天,发现是Glide的磁盘缓存搞的鬼。头像换了,但Uri没变,Glide直接返回了缓存。解决方案是在Uri后面拼接一个时间戳参数,比如photoUri + "?t=" + System.currentTimeMillis(),强制Glide重新加载。

11.6 小结

头像处理,说难不难,说简单也不简单。核心就三步:读取、压缩、保存。加载交给Glide就行。但每一步都有细节,尤其是压缩那一步,做不好就是OOM或者数据库爆掉。

我个人建议,把压缩逻辑封装成一个工具类,统一处理。这样不管是从相册选图、拍照还是从网络下载,都走同一个压缩流程,省心又安全。

好,这一章就到这里。头像搞定了,你的联系人系统看起来就专业多了。


公众号:蓝海资料掘金营,微信deep3321