海量数据处理利器 Roaring BitMap 原理介绍

作者:来自 vivo 互联网服务器团队- Zheng Rui

本文结合个人理解梳理了BitMap及Roaring BitMap的原理及使用,分别主要介绍了Roaring BitMap的存储方式及三种container类型及Java中Roaring BitMap相关API使用。

一、引言

在进行大数据开发时,我们可以使用布隆过滤器和Redis中的HyperLogLog来进行大数据的判重和数量统计,虽然这两种方法节省内存空间并且效率很高,但是也存在一些误差。如果需要100%准确的话,我们可以使用BitMap来存储数据。

BitMap 位图索引数据结构被广泛地应用于数据存储和数据搜索中,但是对于存储较为分散的数据时,BitMap会占用比较大的内存空间,因此我们更偏向于使用 Roaring BitMap稀疏位图索引进行存储。同时,Roaring BitMap广泛应用于数据库存储和大数据引擎中,例如Hive,Spark,Doris,Kylin等。

下文将分别介绍 BitMap 和 Roaring BitMap 的原理及其相关应用。

二、BitMap原理

BitMap的基本思想就是用bit位来标记某个元素对应的value,而key就是这个元素。

例如,在下图中,是一个字节代表的8位,下标为1,2,4,6的bit位的值为1,则该字节表示{1,2,4,6}这几个数。

图片

在Java中,1个int占用4个字节,如果用int来存储这四个数字的话,那么将需要4 * 4 = 16字节。

BitMap可以用于快速排序,查找,及去重等操作。优点是占用内存少(相较于数组)和运算效率高,但是缺点也非常明显,无法存储重复的数据,并且在存储较为稀疏的数据时,浪费存储空间较多。

三、Roaring BitMap 原理

3.1 存储方式

为了解决BitMap存储较为稀疏数据时,浪费存储空间较多的问题,我们引入了稀疏位图索引Roaring BitMap。Roaring BitMap 有较高的计算性能及压缩效率。下面简单介绍一下Roaring BitMap的基本原理。

Roaring BitMap处理int型整数,将32位的int型整数分为高16位和低16位分别进行处理,高16位作为索引分片,而低16位用于存储实际数据。其中每个索引对应一个数据桶(bucket),那么一共可以包含2^16 = 65536个数据块。每个数据桶使用container容器来存储低16位的部分,每个数据桶最多存储2^16 = 65536个数据。

图片

如上图所示,高16位作为索引查找具体的数据块,当前索引值为0,低16位作为value进行存储。

Roaring BitMap在进行数据存储时,会先根据高16位找到对应的索引key(二分查找),低16位作为key对应的value,先通过key检查对应的container容器,如果发现container不存在的话,就先创建一个key和对应的container,否则直接将低16位存储到对应的container中。

Roaring BitMap的精妙之处在于使用不同类型的container,接下来将对其进行介绍。

3.2 container类型

1.ArrayContainer

顾名思义,ArrayContainer直接采用数组来存储低16位数据,没有采用任何数据压缩算法,适合存储比较稀疏的数据,在Java中,使用short数组来存储,并且占用的内存空间大小和数据量成线性关系。由于short为2字节,因此n个数据为2n字节。ArrayContainer采用二分查找定位有序数组中的元素,因此时间复杂度为O(logN)。ArrayContainer的最大数据量为4096, 4096 * 2b = 8kb。

2.BitMapContainer

BitMapContainer采用BitMap的原理,就是一个没有经过压缩处理的普通BitMap,适合存储比较稠密的数据,在Java中使用Long数组存储低16位数据,每一个bit位表示一个数字。由于每个container需要存储2^16 = 65536个数据,如果通过BitMap进行存储的话,需要使用2^16个bit进行存储,即8kb的数据空间。

可以从下图中看出ArrayContainer和BitMapContainer的内存空间使用关系,当数据量小于4096时,使用ArrayContainer比较合适,当数据量大于等于4096时,使用BitMapContainer更佳。

图片

因为BitMap直接使用位运算,所以BitMapContainer的时间复杂度为O(1)。

3.RunContainer

RunContainer采用Run-Length Encoding 行程长度编码进行压缩,适合存储大量连续数据。Java中使用short数组进行存储。连续bit位程度越高的话越节省存储空间,最佳场景下(65536个数据全为1)只需要存储4字节。最差场景为所有数据都不连续,所有存储数据位置为奇数或者偶数,这种场景需要存储128kb。由于采用二分查找算法定位元素,因此时间复杂度为O(logN)。

行程长度编码即的原理是对连续出现的数字进行压缩,只记录初始数字和后续连续数量。

例如:[1,2,3,4,5,8,9,10]使用编码后的数据为[1,4,8,2]。

Java 里可以使用runOptinize()方法来对比RunContainer和其他两个Container存储空间大小,如果使用RunContainer存储空间更佳则会进行转化。

根据上面三个Container类型我们可以得知如何进行选择:

  1. Container默认使用ArrayContainer,当元素数量超过4096时,会由ArrayContainer转换BitMapContainer。

  2. 当元素数量小于等于4096时,BitMapContainer会逆向转换回ArrayContainer。

  3.  正常增删元素不会使Container直接变成RunContainer,而需要用户进行优化方法调用才会转换为最节省空间的Container。

3.3 Roaring BitMap 相关源码

介绍完Roaring BitMap的三种container类型以后,让我们了解一下,Roaring BitMap的相关源码。这里介绍一下Java中增加元素的源码实现。

public void add(final int x) {
    final short hb = Util.highbits(x);
    final int i = highLowContainer.getIndex(hb);
    if (i >= 0) {
      highLowContainer.setContainerAtIndex(i,
          highLowContainer.getContainerAtIndex(i).add(Util.lowbits(x)));
    } else {
      final ArrayContainer newac = new ArrayContainer();
      highLowContainer.insertNewKeyValueAt(-i - 1, hb, newac.add(Util.lowbits(x)));
    }
  }

Roaring BitMap首先获取添加元素的高16位,然后再调用getIndex获取高16位对应的索引,如果索引大于0,表示已经创建该索引对应的container,故直接添加相应的元素低16位即可;否则的话,说明该索引对应的container还没有被创建,先创建对应的ArrayContainer,再进行元素添加。值得一提的是,在getIndex方法中,使用了二分查找来获取索引值,所以时间复杂度为O(logn)。

// 包含一个二分查找
protected int getIndex(short x) {
  // 在二分查找之前,我们先对常见情况优化。
  if ((size == 0) || (keys[size - 1] == x)) {
    return size - 1;
  }
  // 没有碰到常见情况,我们只能遍历这个列表。
  return this.binarySearch(0, size, x);
}

对于元素添加,三种Container提供了不同的实现方式,下面将分别介绍。

1. ArrayContainer

if (cardinality == 0 || (cardinality > 0
          && toIntUnsigned(x) > toIntUnsigned(content[cardinality - 1]))) {
    if (cardinality >= DEFAULT_MAX_SIZE) {
      return toBitMapContainer().add(x);
    }
    if (cardinality >= this.content.length) {
      increaseCapacity();
    }
    content[cardinality++] = x;
  } else {
    int loc = Util.unsignedBinarySearch(content, 0, cardinality, x);
    if (loc < 0) {
      // 当标签中元素数量等于默认最大值时,把ArrayContainer转换为BitMapContainer
      if (cardinality >= DEFAULT_MAX_SIZE) {
        return toBitMapContainer().add(x);
      }
      if (cardinality >= this.content.length) {
        increaseCapacity();
      }
      System.arraycopy(content, -loc - 1, content, -loc, cardinality + loc + 1);
      content[-loc - 1] = x;
      ++cardinality;
    }
  }
  return this;
}

ArrayContainer把添加元素分成两种场景,一种走二分查找,另外一种不走二分查找。

第一种场景:不走二分查找。

当基数为0或者值大于container中的最大值,可以直接添加,因为content数组是有序的,最后一个是最大值。

当基数大于等于默认最大值4096时,ArrayContainer将转换为BitMapContainer。如果基数大于content的数组长度的话,需要将content进行扩容。最后进行赋值即可。

第二种场景:走二分查找。

先通过二分查找找到对应的插入位置,如果返回loc大于等于0,说明存在,直接返回即可,如果小于0才进行后续插入。后续操作同上,当基数大于等于默认最大值4096时,ArrayContainer将转换为BitMapContainer。如果基数大于content的数组长度的话,需要将content进行扩容。最后通过拷贝数组将元素插入到content数组中。

2. BitMapContainer

public Container add(final short i) {
  final int x = Util.toIntUnsigned(i);
  final long previous = BitMap[x / 64];
  long newval = previous | (1L << x);   BitMap[x / 64] = newval;
  if (USE_BRANCHLESS) {
    cardinality += (previous ^ newval) >>> x;
  } else if (previous != newval) {
    ++cardinality;
  }
  return this;
}

BitMap数组为BitMapContainer的存储容器存放数据的内容,数据类型为long,在这里我们只需要找到x在BitMap中的位置,并且把相应的bit位置1即可。x/64就是找到对应long的旧值,1L<<x 就是把对应的bit位置为1,再跟旧值进行或操作,就可以得到新值,再将这个新值存回到bitmap数组即可。<="" span="">

3. RunContainer

public Container add(short k) {
   
  int index = unsignedInterleavedBinarySearch(valueslength, 0, nbrruns, k);
  if (index >= 0) {
    return this;// already there
  }
  index = -index - 2;
  if (index >= 0) {
    int offset = toIntUnsigned(k) - toIntUnsigned(getValue(index));
    int le = toIntUnsigned(getLength(index));
    if (offset <= le) {
      return this;
    }
    if (offset == le + 1) {
      // we may need to fuse
      if (index + 1 < nbrruns) {
        if (toIntUnsigned(getValue(index + 1)) == toIntUnsigned(k) + 1) {
          // indeed fusion is needed
          setLength(index,
              (short) (getValue(index + 1) + getLength(index + 1) - getValue(index)));
          recoverRoomAtIndex(index + 1);
          return this;
        }
      }
      incrementLength(index);
      return this;
    }
    if (index + 1 < nbrruns) {
      // we may need to fuse
      if (toIntUnsigned(getValue(index + 1)) == toIntUnsigned(k) + 1) {
        // indeed fusion is needed
        setValue(index + 1, k);
        setLength(index + 1, (short) (getLength(index + 1) + 1));
        return this;
      }
    }
  }
  if (index == -1) {
    // we may need to extend the first run
    if (0 < nbrruns) {
      if (getValue(0) == k + 1) {
        incrementLength(0);
        decrementValue(0);
        return this;
      }
    }
  }
  makeRoomAtIndex(index + 1);
  setValue(index + 1, k);
  setLength(index + 1, (short) 0);
  return this;
}

RunContainer中的两个数据结构,nbrruns表示有多少段行程,数据类型为int,valueslength数组表示所有的行程,数据类型为short。

  1. 首先,使用二分查找+顺序查找在valueslength数组中查找元素k的插入位置index。如果查找到的index结果大于等于0那就说明k是某个行程起始值,已经存在,直接返回。

  2. -index-2是为了指向前一个行程起始值的索引。

  3. 接下来是一些偏移量和索引值的判断,主要是为了确认k是否落在上一个行程里,或者外面,如果落在上一个行程里,则直接返回,否则需要新建一个行程或者就近与一个行程混合并且将行程长度加1。

3.4 BitMap 和 Roaring BitMap 存储情况对比

public static void count(Integer inputSize) {         RoaringBitMap BitMap = new RoaringBitMap();         BitMap.add(0L, inputSize);
 
        //获取BitMap个数
        int cardinality = BitMap.getCardinality();
 
        //获取BitMap压缩大小
        int compressSizeIntBytes = BitMap.getSizeInBytes();
 
        //删除压缩(移除行程编码,将container退化为BitMapContainer 或 ArrayContainer)         BitMap.removeRunCompression();
 
        //获取BitMap不压缩大小
        int uncompressSizeIntBytes = BitMap.getSizeInBytes();
 
        System.out.println("Roaring BitMap个数:" + cardinality);
        System.out.println("最好情况,BitMap压缩大小:" + compressSizeIntBytes / 1024 + "KB");
        System.out.println("最坏情况,BitMap不压缩大小:" + uncompressSizeIntBytes / 1024 / 1024 + "MB");
 
        BitSet bitSet = new BitSet();
        for (int i = 0; i < inputSize; i++) {
            bitSet.set(i);
        }
        //获取BitMap大小
        int size = bitSet.size();
 
        System.out.println("BitMap个数:" + bitSet.length());
        System.out.println("BitMap大小:" + size / 8 / 1024 / 1024 + "MB");
    }

上述代码使用了Java内置的BitMap(BitSet) 和 Roaring BitMap进行存储大小对比,输出结果如下所示。

  • Roaring BitMap个数:1000000000

  • 最好情况,BitMap压缩大小:149KB

  • 最坏情况,BitMap不压缩大小:119MB

  • Roaring BitMap个数:1000000000

  • BitMap大小:128MB

可以发现,Roaring BitMap的压缩性能效果非常好,同等情况下,是BitMap占用内存的近一千分之一。在退化成BitMapContainer/arrayContainer之后也仍然比使用基本的BitMap存储效果好一些。

四、Roaring BitMap 使用

4.1 Java 中相关 API 使用

在Java中,Roaring BitMap提供了交并补差集等操作,如下代码所示,列举了Java中roaing BitMap的相关API使用方式。

//添加单个数字
public void add(final int x)

//添加范围数字
public void add(final long rangeStart, final long rangeEnd)

//移除数字
public void remove(final int x)

//遍历RBM
public void forEach(IntConsumer ic)

//检测是否包含
public boolean contains(final int x)

//获取基数
public int getCardinality()

//位与,取两个RBM的交集,当前RBM会被修改
public void and(final RoaringBitMap x2)

//同上,但是会返回一个新的RBM,不会修改原始的RBM,线程安全
public static RoaringBitMap and(final RoaringBitMap x1, final RoaringBitMap x2)

//位或,取两个RBM的并集,当前RBM会被修改
public void or(final RoaringBitMap x2)

//同上,但是会返回一个新的RBM,不会修改原始的RBM,线程安全
public static RoaringBitMap or(final RoaringBitMap x1, final RoaringBitMap x2)

//异或,取两个RBM的对称差,当前RBM会被修改
public void xor(final RoaringBitMap x2)

//同上,但是会返回一个新的RBM,不会修改原始的RBM,线程安全
public static RoaringBitMap xor(final RoaringBitMap x1, final RoaringBitMap x2)

//取原始值和x2的差集,当前RBM会被修改
public void andNot(final RoaringBitMap x2)

//同上,但是会返回一个新的RBM,不会修改原始的RBM,线程安全
public static RoaringBitMap andNot(final RoaringBitMap x1, final RoaringBitMap x2)

//序列化
public void serialize(DataOutput out) throws IOException
public void serialize(ByteBuffer buffer)

//反序列化
public void deserialize(DataInput in) throws IOException
public void deserialize(ByteBuffer bbf) throws IOException

对于序列化来说,Roaring BitMap官方定义了一套序列化规则,用来保证不同语言实现的兼容性。

图片

Java中可以使用serialize方法进行序列化,deserialize方法进行反序列化。

4.2 业务实际场景应用

Roaring BitMap可以用来构建大数据标签,针对类型特征来创建对应的标签。

在我们的业务场景中,有很多需要基于人群标签进行交并补集运算的场景,下面以一个场景为例,我们需要计算每天某个设备接口 在设备标签A上的查询成功率,因为设备标签A中的设备不是所有都活跃在网的,所以我们需要将设备标签A与每日日活人群标签取交集,得到的交集大小才能用作成功率计算的分母,另外拿查询成功的标签人群做分子来进行计算即可,查询时长耗时为1s。

假如没有使用标签保存集合之前,我们需要在hive表中查询出同时满足当天在网的活跃用户和设备A的用户数量,查询时长耗时在几分钟以上。两种方式相比之下,使用Roaring BitMap查询的效率更高。

图片

五、总结

本文结合个人理解梳理了BitMap及Roaring BitMap的原理及使用,分别主要介绍了Roaring BitMap的存储方式及三种container类型及Java中Roaring BitMap相关API使用,如有不足和优化建议,也欢迎大家批评指正。

参考资料:

  • Chambi S , Lemire D , Kaser O , et al.

    Better BitMap performance with Roaring 

    BitMaps[J]. Software—practice & Experience, 2016, 46(5):709-719.

  • https://RoaringBitMap.org/

  • GitHub - RoaringBitmap/RoaringFormatSpec: Specification of the compressed-bitmap Roaring format

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/726255.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Raycaster--当物体放在容器中并做了转换,交点坐标不对的问题。

交点坐标问题 问题解决x关键点 总结 问题 子代放在了一个容器里&#xff0c;容器做了旋转、位移。 递归获得了最近的相交子代获取到的交点坐标并不是想要的交点坐标。 经过可视化观察&#xff0c;很像是没转换之前的坐标点。 解决x 在 Three.js 中&#xff0c;当你使用 Rayc…

详细介绍如何解决vcomp140.dll丢失的步骤,分享几种vcomp140.dll修复方法

当这个vcomp140.dll文件丢失时&#xff0c;可能会导致相关程序运行出错甚至无法运行。很多用户可能会遇到vcomp140.dll丢失的问题&#xff0c;但是这并不是不可解决的困难。接下来就和大家分享几种解决vcomp140.dll丢失的方法&#xff0c;给大家详细的关于如何解决vcomp140.dll…

matplotlib 做饼图

饼图可以很好地帮助用户快速了解整体市场数据的占比分配 import matplotlib.pyplot as pltexplode (0,0.1,0,0) labels Frogs,Hogs,Dogs,Logs sizes [15, 30, 45, 10] fig,ax plt.subplots() # colors 设置图形颜色 ;pctdistance&#xff1a;设置百分比标签与圆心的距离&am…

MacBook Air M3的电脑怎么样 新买MacBook Air提示内存不足 苹果电脑内存不够用怎么办

Apple的MacBook Air系列一直是轻薄便携笔记本电脑的代表&#xff0c;最新推出的MacBook Air M3因其出色的性能和优雅的设计而受到广泛关注。然而&#xff0c;许多用户在购买全新的MacBook Air后反应他们遇到了内存不足的提示。 本文将探讨MacBook Air M3的电脑怎么样&#xff0…

【MySQL】事务二

事务二 1.数据库并发的场景2.读-写2.1 3个记录隐藏字段2.2 undo日志2.3 模拟 MVCC2.4 Read View2.5 RR 与 RC的本质区别 3.读-读4.写-写 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我…

示例:应用DependencyPropertyDescriptor监视依赖属性值的改变

一、目的&#xff1a;开发过程中&#xff0c;经常碰到使用别人的控件时有些属性改变没有对应的事件抛出&#xff0c;从而无法做处理。比如TextBlock当修改了IsEnabled属性我们可以用IsEnabledChanged事件去做对应的逻辑处理&#xff0c;那么如果有类似Background属性改变我想找…

太湖远大毛利率下滑:研发费用率远低同行,募投项目合理性疑点重重

《港湾商业观察》黄懿 6月20日&#xff0c;浙江太湖远大新材料股份有限公司&#xff08;以下简称“太湖远大”&#xff0c;873743.NQ&#xff09;即将迎来过会。 2023年11月30日&#xff0c;太湖远大所提交的上市申请材料正式获北交所受理&#xff0c;保荐机构为招商证券&…

DataWorks Copilot:大模型时代数据开发的新范式

导读 DataWorks 是阿里云一站式智能化数据开发与治理平台&#xff0c;支持搭配MaxCompute/Hologres/AnalyticDB/StarRocks/EMR/CDH 等大数据引擎&#xff0c;为企业构建数据仓库、数据湖以及湖仓一体&#xff08;Lakehouse&#xff09;现代数据架构提供数据平台产品解决方案。…

数据结构_二叉树

目录 一、树型结构 二、二叉树 2.1 概念 2.2 特殊的二叉树 2.3 二叉树的性质 2.4 二叉树的存储 2.5 遍历二叉树 2.6 操作二叉树 总结 一、树型结构 树是一种非线性的数据结构&#xff0c;它是由 n(n>0) 个有限结点组成一个具有层次关系的集合&#xff0c;一棵 n 个…

程控漏电流测试电阻箱的应用

程控漏电流测试电阻箱是用于测量和控制电流的设备&#xff0c;广泛应用于电力系统、电子设备、自动化设备等领域。它的主要功能是通过改变电阻值来控制电流的大小&#xff0c;从而实现对设备的保护和控制。 程控漏电流测试电阻箱在电力系统中有着重要的应用&#xff0c;电力系统…

调教NewspaceGPT之GPT4o实战

NewspaceGPT地址&#xff1a;https://newspace.ai0.cn 需求一&#xff1a;我需要一个创意logo 我的问题 我觉得我的描述对一个设计人员来说时精准的&#xff0c;但是不具体的。 需求描述&#xff1a;我需要一个logo。 表现司法公正和司法数字化&#xff0c;人工智能化 。 Ne…

微信聊天记录导出为电脑文件实操教程(附代码)

写在前面 最近&#xff0c;微信中加的群有点多&#xff0c;信息根本看不过来。如果不看&#xff0c;怕遗漏了有价值的信息&#xff1b;如果一条条向上翻阅&#xff0c;实在是太麻烦。 有没有办法一键导出所有聊天记录&#xff1f; 一来翻阅更方便一点&#xff0c;二来还可以…

乐鑫esp32系列睡眠模式下蓝牙连接功耗测试,新支持ESP-C6,启明云端乐鑫代理商

本教程适用于ESP32-S3、ESP32-C3、ESP32-C6&#xff1b; 睡眠模式介绍 ESP32系列常见的休眠方式有三种&#xff0c;分别为Modem-sleep、Light-sleep 和 Deep-sleep。 Modem-sleep模式&#xff1a;CPU 正常工作&#xff0c;可以对时钟进行配置。 进入 Modem-sleep 模式后&…

康谋分享丨从CAN到CAN FD:ADTF在汽车网络中的应用

来源&#xff1a;康谋分享丨从CAN到CAN FD&#xff1a;ADTF在汽车网络中的应用 原文链接&#xff1a;https://mp.weixin.qq.com/s/qCrsXV0D8No3bH6QsgupHg 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; #CAN #CAN FD #ADTF 随着汽车电子技术的发展&#xff0c;车辆…

直播电商源码(直播带货,短视频带货,DIY首页,商城运营)

随着互联网技术的飞速发展&#xff0c;直播电商已成为数字营销的新宠儿。直播电商源码作为支撑这一商业模式的技术基础&#xff0c;其重要性不言而喻。本文将深入探讨直播电商源码的概念、功能以及在现代电商领域的应用。 直播电商源码概述 直播电商源码&#xff0c;简而言之…

【C++题解】1324 - 扩建鱼塘问题

问题&#xff1a;1324 - 扩建鱼塘问题 类型&#xff1a;分支问题 题目描述&#xff1a; 有一个尺寸为 mn 的矩形鱼塘&#xff0c;请问如果要把该鱼塘扩建为正方形&#xff0c;那么它的面积至少增加了多少平方米&#xff1f; 输入&#xff1a; 两个整数 m 和 n 。 输出&…

北京大学数字普惠金融指数(2011-2022年)

北京大学数字普惠金融指数&#xff08;2011-2022年&#xff09;&#xff0c;包含省市县三级数据 数据年限&#xff1a;省级、地级市&#xff08;2011-2022年&#xff09;&#xff1b;区县&#xff08;2014-2022年&#xff09; 数据格式&#xff1a;excel、pdf 数据来源&#xf…

作为老司机,网站啥调性,第一屏就看出来了,还需往里看么。

网站的第一屏指的是用户在打开网站时首先看到的内容区域。这个区域通常包括网站的头部、导航栏、主要的视觉元素和重要的信息。第一屏的设计和呈现方式会对用户的第一印象产生重要影响&#xff0c;因此它能够决定网站的整体调性。 以下是一些原因解释为什么第一屏决定了网站的整…

[SAP ABAP] MESSAGE消息处理

常用的MESSAGE命令的字符 信息类型描述EError 出现错误消息&#xff0c;应用程序在当前点暂停 WWarning 出现警告消息&#xff0c;用户必须按Enter键才能继续应用程序 IInformation 将打开一个弹出窗口&#xff0c;其中包含消息文本&#xff0c;用户必须按Enter键才能继续 SSu…

忘记 iPhone 密码:如果忘记密码,如何解锁 iPhone

为了提高个人数据的安全性&#xff0c;用户通常会为不同的帐户和设备创建不同的复杂密码。虽然较新的 iPhone 型号具有生物识别和面部解锁功能&#xff0c;但这些功能并不总是有效 - 如果您忘记了 iPhone 的密码&#xff0c;您可能会遇到麻烦。 iPhone 用户和 Android 用户一样…