IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Swift之深入解析可选类型Optional的底层原理 -> 正文阅读

[移动开发]Swift之深入解析可选类型Optional的底层原理

Optional 简介

可选类型伴随着Swift诞生,在原有的Objective-C语言中不存在,究其原因,是因为Swift是类型安全的语言,而OC则是弱类型语言,OC中 str字符串既可以是nil,也可以是字符串,而Swift中,这两种状态是不能同时存在的。

首先我们先看下Objective-C与Swift语言对于可选nil的不同理解:

  • Objective-C中的nil:表示缺少一个合法的对象,是指向不存在对象的指针,对结构体、枚举等类型不起作用(会返回NSNotFound)
  • Swift中的nil:表示任意类型的值缺失,是一个确定的值,要么是该类型的一个值要么什么都没有(即为nil)
  • 可选项,一般也叫做可选类型,它允许将值设置为nil
  • 在类型名称后面加个问号?来定义一个可选项
var name: String = "Jack"
name = nil

var age: Int?
age = 10
age = nil
  • 在类型和 ?之间没有空格
  • 在Swift中Optional(可选类型)是一个含有两种情况的枚举,None 和 Some(T),用来表示可能有或可
  • 当你声明一个可选变量或者可选属性的时候没有提供初始值,它的值会默认为 nil。
  • 可选项遵照 LogicValue 协议,因此可以出现在布尔环境中。在这种情况下,如果可选类型T?包含类型为T的任何值(也就是说它的值是 Optional.Some(T) ),这个可选类型等于 true,反之为 false。
  • 使用"!"强制解析获取可选类型的值(不建议直接使用)

一旦确定可选值包含值,可以通过在可选值名称的末尾添加感叹号(!)来访问其内部值。这被称为强制解包一个可选的值

class Person {
    ///存储属性
    var name:String
    var age:Int
    ///初始化
    init(name:String,age:Int) {
        self.name = name
        self.age  = age
    }
}

let status:Int? = 1
var defaultAddress:String? = "Apple"
var student:Person?

var tempAddress:String = defaultAddress! ///使用!进行强制解析
print(tempAddress)
  • 使用操作符!去获取值为nil的可选变量会有运行时错误。
  • 你可以用可选链接和可选绑定选择性执行可选表达式上的操作。如果值为nil,任何操作都不会执行,也不会有运行报错。
  • 叹号(!)表示"我知道一定有值,请使用它",但是当你判断错误,可选值为nil时使用(!)进行强制解析,会有运行错误。所以在使用强制解包之前,一定要确保一个可选值不为
    nil。
class Person {
    ///存储属性
    var name:String
    var age:Int
    ///初始化
    init(name:String,age:Int) {
        self.name = name
        self.age  = age
    }
}

let status:Int? = 1

var defaultAddress:String? = "Apple"
if defaultAddress != nil { //!= 或 == 可以判断是否为nil
    print(defaultAddress!); ///确定了有值,再做赋值
} else {
    print("值为nil,请做异常处理")
}

var student:Person?

可选类型类似于Objective-C中指针的nil值,但是nil只对类(class)有用,而可选类型对所有的类型都可用,并且更安全。

Optional使用

强制解析

class Person {
    ///存储属性
    var name:String
    var age:Int
    ///初始化
    init(name:String,age:Int) {
        self.name = name
        self.age  = age
    }
}

let status:Int? = 1

var defaultAddress:String? = "Apple"
if defaultAddress != nil { //!= 或 == 可以判断是否为nil
    print(defaultAddress!); ///确定了有值,再做赋值
} else {
    print("值为nil,请做异常处理")
}

var student:Person?
  • 强制解析可选值,使用感叹号(!)

注意:
使用!来获取一个不存在的可选值会导致运行时错误。使用!来强制解析值之前,一定要确定可选包含一个非nil的值。

自动解析

你可以在声明可选变量时使用感叹号(!)替换问号(?)。这样可选变量在使用时就不需要再加一个感叹号(!)来获取值,它会自动解析。

class Person {
    ///存储属性
    var name:String
    var age:Int
    ///初始化
    init(name:String,age:Int) {
        self.name = name
        self.age  = age
    }
}

let status:Int= 1

var defaultAddress:String= "Apple"
if defaultAddress != nil { 
    print(defaultAddress); 
} else {
    print("值为nil,请做异常处理")
}

var student:Person

可选绑定(推荐使用)

使用可选绑定(optional binding)来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量。可选绑定可以用在if和while语句中来对可选类型的值进行判断并把值赋给一个常量或者变量。

class Person {
    ///存储属性
    var name:String
    var age:Int
    ///初始化
    init(name:String,age:Int) {
        self.name = name
        self.age  = age
    }
}

let status:Int? = 1

var defaultAddress:String? = "Apple"
if let tempAddress = defaultAddress {
    print(tempAddress) 如果defaultAddress有值的话,就会赋值给tempAddress,然后使用
} else {
    print("字符串为nil")
}

var student:Person?
  • 如果转换成功,那么 tempAddress 常量可以在 if 语句的第一个分支中使用。它已经被初始化为包含在非可选的值中,所以没有必要使用
    ! 后缀来访问它的值。
  • 你可以使用可选绑定的常量和变量。如果你想在 if 语句的第一个分支内操作 tempAddress 的值,你可以写 if var
    tempAddress,使得可选值作为一个变量而非常量。

你可以根据需要在单个 if 语句中包含尽可能多的可选绑定和布尔条件,并用逗号分隔。如果可选绑定中的任何值为 nil,或者任何布尔条件的计算结果为 false,则整个 if 语句的条件被认为是错误的。以下 if 语句是等价的:

class Person {
    ///存储属性
    var name:String
    var age:Int
    ///初始化
    init(name:String,age:Int) {
        self.name = name
        self.age  = age
    }
}

let status:Int? = 1

var defaultAddress:String? = "Apple"
if let tempAddress = defaultAddress, let tempStatus = status {
    print(tempAddress) 如果defaultAddress有值的话,就会赋值给tempAddress,然后使用
    print(tempStatus)
} else {
    print("字符串为nil")
}

var student:Person?

隐式解包

  • 在某些情况下,可选项一旦被设定值之后,就会一直拥有值
  • 在这种情况下,可以去掉检查,也不必每次访问的时候都进行解包,因为它能确定每次访问的时候都有值
  • 可以在类型后面加个感叹号!定义一个隐式解包的可选项
let num1: Int! = 10 //如果能确定某个变量会一直不为nil,可以使用这种声明方式,
let num2: Int = num1 // 可以直接使用,系统会自动解包后赋值,当然也可以使用强制解包方式,但是num1的本质还是一个optional
if num1 != nil {
    print(num1 + 6)
}

if let num3 = num1 {
    print(num3)
}

如果给隐式解包类型的optional变量赋值nil,当该变量被赋值给其他变量/常量式,会出现 运行时报错

let num4: Int! = nil
let num5: Int = num4  -->注意,程序执行完这一句才会报错,在执行这一句的时候,会对num4进行隐式解包,结果发现它内部是nil,因此把nil取出来赋值给  num5: Int 的时候就报错了,因为 num5 不能为空
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

上述的案例,让人感觉使用带!的optional存在很多风险,其实用下面的方式莫不是更保险点吗,

var num5: Int = 10
let num6: int = num5

这样至少在程序运行之前,就能看到错误,如果有的话。

实际上,我们大部分场景下,也都是推荐使用带?的optional,那用!声明的optional存在的意义是什么呢?

  • 如果你提供一套api给外界使用,并且期望使用者严格遵守你的要求不要传nil过来,并且认为使用者在错误使用的时候而导致程序直接报错崩溃就是你期待的,那么你可以使用这种用法。除此之外,还是不用为妙。

Optional 源码分析

Optional 的定义

  • Optional.swift 源码中,Optional定义如下

请添加图片描述

  • Optional是通过enum实现的,Optional本质是枚举。
  • 两个case,nil和some。
  • 关联值就是传进来的值。
  • 本质上?是语法糖,下面的写法等价:
var age: Int? = 10
var age1:Optional<Int> = Optional(10)

switch age {
    case .none:
        print("nil")
    case .some(10):
        print("\(10)")
    default:
        print("unKnown")
}

print(age == age1)
10
true
  • 由于是Optional所以需要强制解包
var age: Int? = nil
print(age!)

如果age没有值!会直接crash。Fatal error: Unexpectedly found nil while unwrapping an Optional value:

  • if let/guard let 可选绑定

if let

var age: Int? = nil
//相当于把age变量的值拿出来给到temp
if let temp = age {
    print("\(temp)")
} else {
    print("nil")
}

guard let

var age: Int? = 10

func test() -> Any {
    guard let temp = age else {
        return "error"
    }
    print(temp)
    return temp
}

test()

Equatable 协议(Optional 遵循了Equatable)

Optional 遵循了Equatable 协议,重写了==方法

extension Optional: Equatable where Wrapped: Equatable {
  @inlinable
  public static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
      return l == r
    case (nil, nil):
      return true
    default:
      return false
    }
  }
}
var age: Int? = 10
var age1:Optional<Int> = Optional(10)

print(age == age1)

前面age和age1能够比较就是因为Optional遵循了Equatable协议重载了==运算符。

自定义数据类型要进行==需要遵循Equatable协议

struct HotpotCat {
    var age: Int
    var name: String
}

extension HotpotCat: Equatable {}

var hp = HotpotCat(age: 18, name: "hotpot")
var hp1 = HotpotCat(age: 18, name: "hotpot")
print(hp == hp1)
true
  • 在HotpotCat中我们并没有实现==方法,编译器帮我们默认实现了
  • 将上述代码生成SIL文件如下
struct HotpotCat {
  @_hasStorage var age: Int { get set }
  @_hasStorage var name: String { get set }
  init(age: Int, name: String)
}

extension HotpotCat : Equatable {
  @_implements(Equatable, ==(_:_:)) static func __derived_struct_equals(_ a: HotpotCat, _ b: HotpotCat) -> Bool
}

__derived_struct_equals实现:

// static HotpotCat.__derived_struct_equals(_:_:)
sil hidden @$s4main9HotpotCatV23__derived_struct_equalsySbAC_ACtFZ : $@convention(method) (@guaranteed HotpotCat, @guaranteed HotpotCat, @thin HotpotCat.Type) -> Bool {
// %0 "a"                                         // users: %13, %6, %3
// %1 "b"                                         // users: %15, %7, %4
// %2 "self"                                      // user: %5
bb0(%0 : $HotpotCat, %1 : $HotpotCat, %2 : $@thin HotpotCat.Type):
 //两个变量分别给到a和b(HotpotCat结构体)
  debug_value %0 : $HotpotCat, let, name "a", argno 1 // id: %3
  debug_value %1 : $HotpotCat, let, name "b", argno 2 // id: %4
  debug_value %2 : $@thin HotpotCat.Type, let, name "self", argno 3 // id: %5
  // //结构体中取出age(Int)
  %6 = struct_extract %0 : $HotpotCat, #HotpotCat.age // user: %8
  %7 = struct_extract %1 : $HotpotCat, #HotpotCat.age // user: %9
  // Int结构体中取出value
  %8 = struct_extract %6 : $Int, #Int._value      // user: %10
  %9 = struct_extract %7 : $Int, #Int._value      // user: %10
   //比较
  %10 = builtin "cmp_eq_Int64"(%8 : $Builtin.Int64, %9 : $Builtin.Int64) : $Builtin.Int1 // user: %11
  //  //相等bb1,否则bb4
  cond_br %10, bb1, bb4                           // id: %11

bb1:                                              // Preds: bb0
  %12 = metatype $@thin String.Type               // user: %18
   //HotpotCat结构体中取出name
  %13 = struct_extract %0 : $HotpotCat, #HotpotCat.name // users: %20, %18, %14
  retain_value %13 : $String                      // id: %14
  %15 = struct_extract %1 : $HotpotCat, #HotpotCat.name // users: %19, %18, %16
  retain_value %15 : $String                      // id: %16
  // function_ref static String.== infix(_:_:)
   //String自己的==方法
  %17 = function_ref @$sSS2eeoiySbSS_SStFZ : $@convention(method) (@guaranteed String, @guaranteed String, @thin String.Type) -> Bool // user: %18
  %18 = apply %17(%13, %15, %12) : $@convention(method) (@guaranteed String, @guaranteed String, @thin String.Type) -> Bool // user: %21
  release_value %15 : $String                     // id: %19
  release_value %13 : $String                     // id: %20
   //比较String是否相同
  %21 = struct_extract %18 : $Bool, #Bool._value  // user: %22
  //相等 bb2,否则bb3
  cond_br %21, bb2, bb3                           // id: %22

bb2:                                              // Preds: bb1
 //构造bool值-1
  %23 = integer_literal $Builtin.Int1, -1         // user: %24
  %24 = struct $Bool (%23 : $Builtin.Int1)        // user: %25
  br bb5(%24 : $Bool)                             // id: %25

bb3:                                              // Preds: bb1
//构造bool值0
  %26 = integer_literal $Builtin.Int1, 0          // user: %27
  %27 = struct $Bool (%26 : $Builtin.Int1)        // user: %28
  br bb5(%27 : $Bool)                             // id: %28

bb4:   
//构造bool值0                                           // Preds: bb0
  %29 = integer_literal $Builtin.Int1, 0          // user: %30
  %30 = struct $Bool (%29 : $Builtin.Int1)        // user: %31
  br bb5(%30 : $Bool)                             // id: %31

// %32                                            // user: %33
bb5(%32 : $Bool):                                 // Preds: bb2 bb3 bb4
  return %32 : $Bool                              // id: %33
} // end sil function '$s4main9HotpotCatV23__derived_struct_equalsySbAC_ACtFZ'
  • 1.取两个结构体a、b
  • 2.比较a.age 与 b.age
  • 3.取a.name与b.name,并且调用String自己的判等方法
  • 4.构造Int1类型的数据,相等-1,不相等0。

底层通过Int1类型的数据生成Bool类型:

  %23 = integer_literal $Builtin.Int1, -1         // user: %24
  %24 = struct $Bool (%23 : $Builtin.Int1)        // user: %25
  br bb5(%24 : $Bool)   

所以也可以简单说Swift中的Bool是-1(true)和0(false)。

Bool.Swift源码:

请添加图片描述

  • 如果结构体里面嵌套结构体,内嵌结构体也要遵循Equatable协议

请添加图片描述

只有MakeCatBar与HotpotCat 都遵循Equatable协议 才能使用==运算符

  • 上面我们分析的是结构体,那么如果HotpotCat是class呢?

请添加图片描述

  • class 必须自行实现==方法
class HotpotCat {
    var age: Int
    var name: String
    init(age:Int,name:String) {
        self.age = age
        self.name = name
    }
}

extension HotpotCat: Equatable {
    static func == (lhs: HotpotCat, rhs: HotpotCat) -> Bool {
            return lhs.age == rhs.age && lhs.name == rhs.name
        }
}

var hp = HotpotCat.init(age: 18, name: "hotpot")
var hp1 = HotpotCat.init(age: 18, name: "hotpot")
var hp2 = hp
//比较值相等
print(hp == hp1)
//比较地址
print(hp === hp1)
print(hp === hp2)
true
false
true

如果判断两个对象是否是同一个实例对象需要使用===

  • == 与===对比

==相等于equal to,用于判断两个值是否相等
=== 用于判断两个是否是同一个实例对象(内存地址是否一致)

Comparable 协议

Comparable 协议遵循了Equatable协议支持更多的比较方式

public protocol Comparable : Equatable {
    static func < (lhs: Self, rhs: Self) -> Bool

    static func <= (lhs: Self, rhs: Self) -> Bool

    static func >= (lhs: Self, rhs: Self) -> Bool

    static func > (lhs: Self, rhs: Self) -> Bool
}

Comparable自动实现了Equatable

struct HotpotCat {
    var age: Int
    var name: String
}


extension HotpotCat: Comparable {
    static func < (lhs: HotpotCat, rhs: HotpotCat) -> Bool {
            return lhs.age < rhs.age && lhs.name < lhs.name
        }
}

var hp = HotpotCat(age: 21, name: "hotpot1")
var hp1 = HotpotCat(age: 20, name: "hotpot")
print(hp > hp1)

在这里结构体必须实现<,编译器会通过<自动实现其它运算符。当然实现>,不实现<不行,由于源码中其它运算法是通过<来实现的:

public protocol Comparable: Equatable {

  static func < (lhs: Self, rhs: Self) -> Bool

  static func <= (lhs: Self, rhs: Self) -> Bool

  static func >= (lhs: Self, rhs: Self) -> Bool

  static func > (lhs: Self, rhs: Self) -> Bool
}

extension Comparable {

  @inlinable
  public static func > (lhs: Self, rhs: Self) -> Bool {
    return rhs < lhs
  }
  
  @inlinable
  public static func <= (lhs: Self, rhs: Self) -> Bool {
    return !(rhs < lhs)
  }

  @inlinable
  public static func >= (lhs: Self, rhs: Self) -> Bool {
    return !(lhs < rhs)
  }
}

空合运算符(??)

  • ??运算符在optional源码中有两个:
@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
    rethrows -> T {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?)
    rethrows -> T? {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

一个返回T,一个返回T?,这里为什么有两个?看个例子就明白了

var age: Int? = nil
var age2: Int? = 10
var temp = age ?? age2

print(temp)
  • 可以看到??的返回值是和age2相关的。age2是什么类型就返回什么类型,age2是可选类型就返回T?,否则返回T。

请添加图片描述

  • 空合运算符后面返回值的类型,还要与当前类型保持一致,当前是Int类型,就不能返回字符串

可选链

可选链是一个调用和查询可选属性、方法和下标的过程,它可能为 nil 。如果可选项包含值,属性、方法或者下标的调用成功;如果可选项是 nil ,属性、方法或者下标的调用会返回 nil 。多个查询可以链接在一起,如果链中任何一个节点是 nil ,那么整个链就会得体地失败。

class Hotpot {
    var name: String?
    var cat: Cat?
    func test() {
        print("test")
    }
}

class Cat {
    var catName: String?
    func ctTest() {
        print("ctTest")
    }
}

var cat: Cat? = nil
var hp: Hotpot? = Hotpot()
let temp = hp?.cat?.catName
hp?.test()
hp?.cat?.ctTest()

unsafelyUnwrapped

在optional源码中还有一个unsafelyUnwrapped,实现如下:

 /// The wrapped value of this instance, unwrapped without checking whether
  /// the instance is `nil`.
  ///
  /// The `unsafelyUnwrapped` property provides the same value as the forced
  /// unwrap operator (postfix `!`). However, in optimized builds (`-O`), no
  /// check is performed to ensure that the current instance actually has a
  /// value. Accessing this property in the case of a `nil` value is a serious
  /// programming error and could lead to undefined behavior or a runtime
  /// error.
  ///
  /// In debug builds (`-Onone`), the `unsafelyUnwrapped` property has the same
  /// behavior as using the postfix `!` operator and triggers a runtime error
  /// if the instance is `nil`.
  ///
  /// The `unsafelyUnwrapped` property is recommended over calling the
  /// `unsafeBitCast(_:)` function because the property is more restrictive
  /// and because accessing the property still performs checking in debug
  /// builds.
  ///
  /// - Warning: This property trades safety for performance.  Use
  ///   `unsafelyUnwrapped` only when you are confident that this instance
  ///   will never be equal to `nil` and only after you've tried using the
  ///   postfix `!` operator.
  @inlinable
  public var unsafelyUnwrapped: Wrapped {
    @inline(__always)
    get {
      if let x = self {
        return x
      }
      _debugPreconditionFailure("unsafelyUnwrapped of nil optional")
    }
  }

unsafelyUnwrapped 和!强制解包一样。

var age: Int? = 20
print(age.unsafelyUnwrapped)

区别在于:

请添加图片描述

这里的-O,是指target -> Build Setting -> Optimization Level设置成-O时,如果使用的是age.unsafelyUnwrapped,则不检查这个变量是否为nil,

1、设置Optimization Level 为Fastest, Smallest[-Os]

2、edit Scheme -> Run -> Info -> Build Configuration改为release模式,然后再次运行发现,没有崩溃,与官方所说是一致的
请添加图片描述

as as? as!

  • as 将类型转换为其他类型
var age: Int = 10

var age1 = age as Any
print(age1)

var age2 = age as AnyObject
print(age2)

<!--打印结果-->
10
10
  • as? 将类型转换为 其他可选类型
var age: Int = 10
//as?
//as? 不确定类型是Double,试着转换下,如果转换失败,则返回nil
var age3 = age as? Double
print(age3)

<!--打印结果-->
nil

此时的age3的类型是Double?

  • as! :强制转换为其他类型
var age: Int = 10
//as! 强制转换为其他类型
var age4 = age as! Double
print(age4)

运行结果如下,会崩溃

请添加图片描述

查看以下代码的SIL文件

var age: Int = 10
var age3 = age as? Double
var age4 = age as! Double

请添加图片描述

总结

  • Optional的本质是enum,所以可以使用模式匹配来匹配Optional的值
  • Optional的解包方式有两种:

1、强制解包:一旦为nil,程序会崩溃
2、可选值绑定:if let (只能在if流程的作用域内访问)、guard let

  • Equatable协议:
  • 对于swift标准库中的绝大部分类型都默认实现了Equatable协议
  • 对于自定义Struct类型,仅需要遵守Equatable协议
  • 对于自定义class类型,除了需要遵守Equatable协议,还需要自己实现Equatable协议的方法
  • 区分 == vs ===

== 相当于 equal to,用于判断两个值是否相等
=== 是用来判断 两个对象是否是同一个实例对象(即内存地址指向是否一致)

  • Comparable协议:

对于自定义类型,需要遵循Comparable协议,并重写运算符
??空运算符:??只有两种类型,一种是T,一种是T?,主要是与 ?? 后面的返回值有关(即简单来说,就是??后是什么类型,??返回的就是什么类型)

  • 可选链:允许在一个链上来访问当前的属性/方法,如果为nil,则不会执行?后的属性/方法
  • unsafelyUnwrapped:与强制解包类似,但是如果项目中设置target -> Build Setting -> Optimization Level设置成-O时,如果使用的是age.unsafelyUnwrapped,则不检查这个变量是否为nil
  • 区分 as、as?、 as!

as 将类型转换为其他类型
as? 将类型转换为 其他可选类型
as! 强制转换为其他类型
使用建议:能确定使用as!,不能确定使用as?

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-10-06 12:20:40  更:2021-10-06 12:21:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 14:34:53-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码