iOS小技能:字典转模、xib的使用、自定义视图

2,591 阅读15分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

前言

案例:搭建九宫格

  1. 确定开发思路
  2. 搭建界面、编写代码
  3. 九宫格的布局
  4. 字典装模型(模型数据的处理,plist文件的加载)->实现按钮的监听方法
  5. 使用类方法加载xib,简化代码搭建界面
  6. 自定义视图,使用数据模型装配视图内容 若一个view的内部子控件比较多,通常会考虑自定义一个view 把内部的子控件创建屏蔽起来,不让外界关心

字典转模型


/** 通常实现字典实例化模型,都实现了以下模型的实例化方法*/
//使用字典实例化模型
- (instancetype) initWithDictionary :(NSDictionary *) appDictionary;
//类方法可以快速实例化一个对象--把代码放在它最应该呆的地方
+ (instancetype) appInfoWithDictionary : (NSDictionary *) appDictionary;
//返回plist文件对应的模型数组 ,使用懒加载

KVC的赋值

- (instancetype) initWithDictionary:(NSDictionary *)appDictionary{
    //self is object
    self = [super init];
    if (self) {//既然nil解析成NO,所以没有必要在条件语句比较。不要拿某样东西直接与YES比较,因为YES被定义为1
        //init local vars 将plist文件的信息在此处进行字典转模型
        //KVC (key value coding) 键值编码:是一种间接修改、读取对象属性的一种方法;KVC被称为cocoa的大招
        [self setValuesForKeysWithDictionary:appDictionary];//本质上是调用        self setValue:<#(nullable id)#> forUndefinedKey:(nonnull NSString *)
     
    }
    return self;
}

原文

kunnan.blog.csdn.net/article/det…

I、 基础知识

1.1 深复制&浅复制


深复制即内容拷贝

即源对象和副本指向的是不同的两个对象; 源对象的引用计数器不变,副本的引用计算器为1;

指针拷贝(浅复制)

源对象和副本指向同一个对象; 对象的引用计算器+1,相当于做了一次retain操作

1.2 代码重构(前提是已经实现了基本功能)

开发前:设定开发计划、步骤 开发过程中:每一个步骤告一段落之后,我们要暂停,进行代码审核,有针对性的重构(抽离重复代码,模型和视图各尽职责)

代码重构的原则: 把代码放在它最应该呆的地方

1、使用类方法实现字典实例化模型 (模型,通常是plist文件,网络)

使用类方法实例化模型数组

//类方法可以快速实例化一个对象--把代码放在它最应该呆的地方
+ (instancetype) appInfoWithDictionary : (NSDictionary *) appDictionary;
//返回plist文件对应的模型数组 ,使用懒加载
+ (NSArray *)appList;

2、使用类方法实例化视图对象,并用数据模型装配视图内容

用类方法进行视图的实例化

+ (instancetype) appView;//使用类方法加载xib
+ (instancetype) appViewWithAppInfo:(KNAppInfo *) appInfo;//使用类方法加载xib,参数用于视图的数据装配

1.3 九宫格计算方法

1)每一列的x值一样,x值由列号决定 2)每一行的y值一样,y值由行号决定

        //搭建界面,九宫格(以View为单元,内含UILabel、UIButton、UIImageView,同行和同列的位置关系 center.x = x+ width*0.5)
 
    //view 的封装,带有数据模型的构造器以便进行内部控件的数据装配 ;数据模型(plist-》字典-》模型)--自定义的Plist 通常放置于Supporting Files目录中
 
#define kAppViewWidth 80 //视图宽度
 
#define KAppViewHeight  90 //视图高度
 
#define kColumnCount 4 //每行的视图个数--总列数
 
#define kRowCount 5 // 每一列的视图个数--总行数
 
    //水平间距
 
    CGFloat marginX =(CGRectGetWidth(self.view.frame)- kColumnCount * kAppViewWidth)/(kColumnCount+1);
 
    //垂直间距
 
    CGFloat marginY = (CGRectGetHeight(self.view.frame)- kRowCount* KAppViewHeight)/(kRowCount+1);
 
     
 
    for (int i=0; i<28; i++) {
 
        //行号
 
        int  row = i/kColumnCount;
 
        //列号
 
        int column =i%kColumnCount;
 
        //位置
 
        CGFloat x=marginX+(marginX+kAppViewWidth)*column;//x值决定视图所在的列;
 
        CGFloat y= marginY+(marginY+KAppViewHeight)*row; //y值决定视图所在的行
 
        //创建视图
 
        UIView *appView = [[UIView alloc]initWithFrame: CGRectMake(x, y, kAppViewWidth, KAppViewHeight)];
 
        [appView setBackgroundColor:[UIColor redColor]];
 
        //将子视图添加至父视图
 
        [self.view addSubview:appView];
 
    }

1.4 内存分析(栈、堆的存储信息)

1、只读指针属性的分析(指向关系不可变,指向对象的内容可变)

不可变属性的值,若存储的是指针,则该属性对应的对象成员是可变的 只读指针属性的地址值不可变,意味的指针和指向的对象间的关系不可变,但被指向的对象内容是可变的

示例1:UIButton 对象有UILabel 、UIImageView 属性,都是readonly

即这两者的属性存储的指针地址是只读的,不可修改 ,但只读的指针指向的对象的属性成员是可以修改的

示例2:修改UIButton的只读属性titleLabel指针对应的对象属性font(readonly表示titleLabel的指针指不可修改,但label的font可以修改)

 [[downloadButton titleLabel] setFont: [UIFont systemFontOfSize:12]];
//设置UIButton的title的font,先获取UI Button的内部UILabel:@property (nullable,nonatomic,readonly,strong) UILabel     *titleLabel NS_AVAILABLE_IOS(3_0);
        //@property(nonatomic,strong) UIFont         *font              NS_DEPRECATED_IOS(2_0, 3_0) __TVOS_PROHIBITED;过时

示例3:a basic type & a class 的声明方式

a basic type是存储在栈区 class是栈区存储着堆区的对象对应的指针

//Change this:    
 CGFloat *marginX =(self.view.bounds.size.width - kColumnCount * kAppViewWidth)/(kColumnCount+1);
//to
 CGFloat *marginX =(self.view.bounds.size.width - kColumnCount * kAppViewWidth)/(kColumnCount+1); 
//Get rid of the asterisk. CGFloat is not a class, it is a typedef for double (a basic type).

1.5 instancetype简介

OC中,在IOS7之后主推instancetype。 swift语言中,绝大数的类的实例化,也都不需要指定类型。

C++的11版本中,也有类似的关键字auto类型 可见所有的语言都致力于,使语言更容易使用

instancetype 在类型表示上跟id一样(可以表示任何对象类型)

instancetype 主要用于类方法实例化对象时,让编译器主动推动对象的实际类型,以避免使用ID,造成开发中不必要的麻烦

1.instancetype 只能使用于返回值类型,不能像id一样用于参数类型;但instancetype比id多的一个好处是:

IDE会检查instancetype的真实类型 ,提前警告(Incompatible pointer types initializing 'NSString *' with an expression of type 'KNAppInfo *'),以避免使用ID造成开发中不必要的麻烦

2.Init方法应该遵循Apple生成代码模板的命名规则,返回类型应该使用instancetype而不是id。 当类构造方法被使用时,它应该返回类型是instancetype而不是id。这样确保编译器正确地推断结果类型,可避免运行时才报错。

属性不能使用New 开头进行命名

1.6 @synthesize的用法


@synthesize 中可以定义 与变量名不相同的getter和setter的命名,籍此来保护变量不会被不恰当的访问(主动指定属性使用的成员变量名称)


@property (nonatomic,strong,readonly) UIImage *image;//存储字典对应的图片对象
 
//实现文件
 
#import "KNAppInfo.h"
 
@implementation KNAppInfo
@synthesize image =_image;//@synthesize 中可以定义 与变量名不相同的getter和setter的命名,籍此来保护变量不会被不恰当的访问
 
/**
 
 懒加载模型的图片对象属性
 */
- (UIImage *)image{
    if (nil == _image) {//
        _image = [UIImage imageNamed:self.icon] ;
    }
    return _image;
}

1.7 @property

声明property的语法为:@property (参数1,参数2) 类型 名字;

1)参数主要分为三类: 读写属性: (readwrite/readonly 语意:(assign/retain/copy) 原子性: (atomicity/nonatomic)针对多线程问题 2)各参数意义如下: readwrite: 产生setter\getter方法 readonly: 只产生简单的getter,没有setter。 assign: 默认类型,setter方法直接赋值,而不进行retain操作 retain: setter方法对参数进行release旧值,再retain新值。 copy: setter方法进行Copy操作,与retain一样 nonatomic: 禁止多线程,变量保护,提高性能

developer.apple.com/library/mac…

II、字典转模型

  • 使用字典的坏处

通常取出和修改数据字典的数据,都要通过编写“字符串类型”的key值-》编辑器IDE没有智能提示、手动写key容易写错,且此时IDE不会有任何的警告和报错。 字典的使用例子

dict[@"name"] = @"Jack";
NSString *name = dict[@"name"];

  • 使用数据模型的好处

1)数据模型(专门用来存放数据的对象),使用数据模型表示数据更专业些 2)使用模型修改数据、读取数据均采用对象的属性,提高编写效率

2.1 字典转模型的实现步骤

1)字典转模型的过程,通常被封装在模型内部 2)模型应该提供一个“带有NSDictionary类型”参数的构造方法

- (instancetype)initWithDict:(NSDictionary*)dict;
+ (instancetype)xxxWithDict:(NSDictionary*)dict;

2.2 字典转模型的过程

  • [采用KVC(keyValueCoding)实现] plist文件解析-》字典数组(NSDictionary)-》模型数组(AppInfo)

使用字典实例化模型 (把代码放在它最应该呆的地方)

- (instancetype) initWithDictionary:(NSDictionary *)appDictionary{
 
    //self is object
 
    self = [super init];
 
    if (self) {//既然nil解析成NO,所以没有必要在条件语句比较。不要拿某样东西直接与YES比较,因为YES被定义为1
 
        //init local vars 将plist文件的信息在此处进行字典转模型
 
        //KVC (key value coding) 键值编码:是一种间接修改、读取对象属性的一种方法;KVC被称为cocoa的大招
 
        [self setValuesForKeysWithDictionary:appDictionary];     
 
    }
    return self;
}
 
//使用类方法实现“字典实例化模型”--地道的代码
+ (instancetype) appInfoWithDictionary:(NSDictionary *)appDictionary{
 
    //self is class
 
    return [[self alloc]initWithDictionary:appDictionary];//+ (instancetype)alloc    Description    Returns a new instance of the receiving class.   
 
}
@end


  • 使用KVC的注意事项:

1、plist文件中的键值名称必须与模型对象的属性名称一致 2、模型中的属性,可以不全部出现在plist文件中

III、 xib的使用

用于描述“某一块局部的”UI界面

1、在开发阶段面向开发者的是xib文件,当把应用装到iPhone时,xib文件会转成nib文件 2、xib 在Xcode 4.0 之前是主要的图形界面搭建工具,现在,xib仍是主流的界面开发技术;适合于开发小块的自定义视图

3.1xib 的加载方式

方式一

NSArray *objs = [[NSBundle mainBundle] loadNibNamed:@"KNAppView" owner:nil options:nil];//这个方法会创建xib中的所有对象,并且将对象按顺序放到objs数组中

方式二

UINib *nib = [UINib nibWithNibName:@"MJAppView" bundle:[NSBundle mainBundle]];//bundle参数可以为nil,默认就是main bundle
NSArray *objs = [nib instantiateWithOwner:nil options:nil];

3.2 xib与storyBoard的异同点

相同点:

都是用于描述UI界面,使用InterfaceBuilder工具来编辑

不同点:

1、xib是 轻量级的,用于描述局部的UI界面; 2、storyBoard是重量级的,用来描述整个应用软件的多个界面,并能展示多个界面间的跳转关系。

IV、自定义View

4.1 View的封装

通过 自定义视图来减少代码对xib视图层次的依赖性

1)若一个view的内部子控件比较多,通常会考虑自定义一个view

把内部的子控件创建屏蔽起来,不让外界关心

2) 外界可以传入对应的模型数据给view,view拿到模型数据之后,进行“内部子控件的数据”装配

4.2 UIView的常见属性和方法

设置按钮的文字字体

//设置按钮的文字字体,需要拿到按钮内部的label
btn.titleLabel.font = [UIFont systemFontOfSize:13];//
/**
常见的字体有:UIFont代表字体,常见创建方法有以下几个:
+ (UIFont *)systemFontOfSize:(CGFloat)fontSize; //系统默认字体
+ (UIFont *)boldSystemFontOfSize:(CGFloat)fontSize;  //粗体
+ (UIFont *)italicSystemFontOfSize:(CGFloat)fontSize;  //斜体
*/

V、搭建九宫格

确定开发思路->搭建界面、编写代码->九宫格的布局-> 字典装模型(模型数据的处理,plist文件的加载)->实现按钮的监听方法->使用类方法加载xib,简化代码搭建界面->自定义视图,使用数据模型装配视图内容

5.1 搭建九宫格的步骤

1)明确每一块用的是什么View; 2)明确每一个view之间的父子关系 3)先尝试逐个添加格子,最后考虑使用for循环进行搭建‘ 4)加载app数据,根据数据数量来创建对应个数的格子 5)添加格子内部的子控件 6)给格子内部的子控件装配数据

5.2 代码示例

模型的实现代码

//
 
//  KNAppInfo.m
  
#import "KNAppInfo.h"
 
@implementation KNAppInfo
 
/**
 
 问题: 使用readonly修饰成员变量的话,将不会生产带下划线的成员变量 undeclared identifier '_image',
 
 解决方法如下:@synthesize 合成指令 --主动指定属性使用的成员变量名称
 
 */
 
@synthesize image =_image;//@synthesize 中可以定义 与变量名不相同的getter和setter的命名,籍此来保护变量不会被不恰当的访问
 
/**
 
 懒加载模型的图片对象属性
 
 */
 
- (UIImage *)image{
 
    if (nil == _image) {//
 
        _image = [UIImage imageNamed:self.icon] ;
 
    }
 
    return _image;
 
}
 
//使用字典实例化模型 -- 把代码放在它最应该呆的地方
 
/**
 
 1.Init方法应该遵循Apple生成代码模板的命名规则,返回类型应该使用instancetype而不是id。
 
 当类构造方法被使用时,它应该返回类型是instancetype而不是id。这样确保编译器正确地推断结果类型。
 
 IDE提前报警:Incompatible pointer types initializing 'NSString *' with an expression of type 'KNAppInfo *
 
 --避免运行时才报错。
 
 2.关于instancetype的小结
 
 1》instancetype 主要用于类方法实例化对象时,让编译器主动推动对象的实际类型,以避免使用ID,造成开发中不必要的麻烦
 
 2》OC中,在IOS7之后主推instancetype
 
    swift语言中,绝大数的类的实例化,也都不需要指定类型
 
    C++的11版本中,也有类似的关键字auto类型--》所有的语言都致力于,使语言更容易使用
 
 3》instancetype 只能使用于返回值类型,不能当参数适用
 
 */
 
- (instancetype) initWithDictionary:(NSDictionary *)appDictionary{
 
    //self is object
 
    self = [super init];
 
    if (self) {//既然nil解析成NO,所以没有必要在条件语句比较。不要拿某样东西直接与YES比较,因为YES被定义为1
 
        //init local vars 将plist文件的信息在此处进行字典转模型
 
        //KVC (key value coding) 键值编码:是一种间接修改、读取对象属性的一种方法;KVC被称为cocoa的大招
 
        [self setValuesForKeysWithDictionary:appDictionary];//本质上是调用        self setValue:<#(nullable id)#> forUndefinedKey:(nonnull NSString *)
 
         
 
    }
 
    return self;
 
}
 
//使用类方法实现“字典实例化模型”--地道的代码
 
+ (instancetype) appInfoWithDictionary:(NSDictionary *)appDictionary{
 
    //self is class
 
    return [[self alloc]initWithDictionary:appDictionary];//+ (instancetype)alloc    Description    Returns a new instance of the receiving class.   
 
}
 
 
 
 
/**
 
 返回plist的数据模型数组
 
 */
 
+ (NSArray *)appList{
 
    //解析plist,得到字典数组
 
    NSArray *tmpDictionaryArray= [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"app" ofType:@"plist"]];
 
    //可变数组,用于临时存储模型数组
 
    NSMutableArray *tmpAppInfoArray = [NSMutableArray array];
 
    for (NSDictionary *dict in tmpDictionaryArray) {
 
        [tmpAppInfoArray addObject:[self appInfoWithDictionary:dict]];
    }
 
    return tmpAppInfoArray;
 
}
 
@end

模型的头文件

//
 
//  KNAppInfo.h
 
 
//
 
#import <Foundation/Foundation.h>
 
#import <UIKit/UIKit.h>
 
 
//模型数据
 
@interface KNAppInfo : NSObject
 
/**
 
 使用KVC的注意事项:
 
 1》plist中的键名称必学与模型属性名称保持一致
 
 2》模型中的属性可以不全部出现在plist中
 
 */
 
@property (nonatomic,copy) NSString *icon;//属性名称与plist文件的NSDictionary 的key保持一致,以便采用KVC(键值编码)
 
@property (nonatomic,copy) NSString *name;
 
/**
 
 @property
 
 1>生成getter、setter、带下划线的成员变量(纪录属性内容)
 
 2>使用readonly修饰成员变量的话,将不会生产带下划线的成员变量 undeclared identifier '_image'
 
 */
 
@property (nonatomic,strong,readonly) UIImage *image;//存储字典对应的图片对象
 
 
 
 
/**
 
 通常实现字典实例化模型,都实现了以下模型的实例化方法
 
*/
 
//使用字典实例化模型
 
- (instancetype) initWithDictionary :(NSDictionary *) appDictionary;
 
//类方法可以快速实例化一个对象--把代码放在它最应该呆的地方
 
+ (instancetype) appInfoWithDictionary : (NSDictionary *) appDictionary;
 
//返回plist文件对应的模型数组 ,使用懒加载
 
+ (NSArray *)appList;
@end

控制器的代码

//
 
#import "ViewController.h"
 
#import "KNAppInfo.h"
 
#import "KNAppView.h"
 
#define kAppViewWidth 80 //视图宽度
 
#define KAppViewHeight  90 //视图高度
 
#define kColumnCount 3 //每行的视图个数--总列数
 
#define kRowCount 4 // 每一列的视图个数--总行数
 
#define kStartY 1 //为了适配埋伏笔
 
@interface ViewController ()
 
@property (nonatomic,strong) NSArray *appInfoList;
 
@end
 
@implementation ViewController
 
//懒加载appInfoList,主要用于装配视图的数据,与视图控制器ViewController 关系不大,可以进行抽离
 
- (NSArray *)appInfoList{
 
    if (nil == _appInfoList) {
 
        _appInfoList = [KNAppInfo appList];//获取plist的数据模型数组
 
    }
 
    return _appInfoList;
 
}
 
 
 
 
- (void)viewDidLoad {
 
    [super viewDidLoad]; 
 
    //搭建界面,九宫格(以View为单元,内含UILabel、UIButton、UIImageView,同行和同列的位置关系 center.x = x+ width*0.5)
 
    //1.view 的封装,带有数据模型的构造器以便进行内部控件的数据装配 ;数据模型(plist-》字典-》模型)
 
    //水平间距
 
    CGFloat marginX =(CGRectGetWidth(self.view.frame)- kColumnCount * kAppViewWidth)/(kColumnCount+1);
 
    //垂直间距
 
    CGFloat marginY = (CGRectGetHeight(self.view.frame)- kRowCount* KAppViewHeight)/(kRowCount+1);
 
    //用plist文件的数据装配xib 的控件
 
    for (int i=0; i<self.appInfoList.count; i++) {
 
        //行号
 
        int  row = i/kColumnCount;
 
        //列号
 
        int column =i%kColumnCount;
 
        //位置
 
        CGFloat x=marginX+(marginX+kAppViewWidth)*column;//x值决定视图所在的列;
 
        CGFloat y=kStartY+marginY+(marginY+KAppViewHeight)*row; //y值决定视图所在的行
 
        //实例化视图
 
        KNAppView *appView = [KNAppView appViewWithAppInfo:self.appInfoList[i]];//使用类方法,进行加载xib,获取app view的同时,对appview 进行数据装配( 使用模型装配视图  --实现视图内部的细节)
 
        //设置位置
 
        [appView setFrame:CGRectMake(x, y, kAppViewWidth, KAppViewHeight)];
 
        //将子视图添加至父视图
 
        [self.view addSubview:appView];
 
    }   
 
}
 
@end

视图的头文件

//
 
 
 
 
 
#import <UIKit/UIKit.h>
 
@class KNAppInfo;
 
 
 
 
@interface KNAppView : UIView
 
 
 
 
//自定义视图的现实的数据来源于模型,即使用模型装配自定义视图的显示内容
 
@property (nonatomic,strong) KNAppInfo *appInfo;//视图对应的模型,是视图提供给外界的接口
 
 
 
 
+ (instancetype) appView;//使用类方法加载xib
 
+ (instancetype) appViewWithAppInfo:(KNAppInfo *) appInfo;//使用类方法加载xib,参数用于视图的数据装配
 
 
 
 
@end

视图的实现文件

//
 
 
 
 
#import "KNAppView.h"
 
#import "KNAppInfo.h"
 
 
 
 
@interface KNAppView ()
 
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
 
@property (weak, nonatomic) IBOutlet UILabel *appLable;
 
 
 
 
@end
 
 
 
 
@implementation KNAppView
 
/**
 
 使用类方法加载xib,并对视图进行数据装配
 
 */
 
+(instancetype)appViewWithAppInfo:(KNAppInfo *)appInfo {
 
    KNAppView *appview  = [self appView];//实例化视图
 
    [appview setAppInfo:appInfo];//使用模型装配app view
 
    return appview;//返回视图对象
 
}
 
 
 
 
/**
 
 使用类方法进行加载xib 视图,即实例化视图
 
 */
 
+(instancetype)appView{
 
    return [[[NSBundle mainBundle] loadNibNamed:@"KNAppView" owner:nil options:nil]lastObject];
 
}
 
 
 
 
//利用setter方法,实现视图的数据装配
 
- (void)setAppInfo:(KNAppInfo *)appInfo{
 
    _appInfo = appInfo;
 
    [self.appLable setText:_appInfo.name];
 
    [self.imageView setImage:_appInfo.image];
 
 
 
 
}
 
 
 
 
//弹出对应的app‘name
 
- (IBAction) downloadBUttonSelector:(UIButton *)button{
 
    //获取button对应的父视图的子控件label对应的text属性值
 
    //添加一个UILabel到界面上
 
    UILabel *appNameLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,500, 160, 40)];
 
    [appNameLabel setText:self.appInfo.name];
 
     
 
    [appNameLabel setTextAlignment:NSTextAlignmentCenter];
 
    //RGB  0 表示黑色;1 表示白色 The grayscale value of the color object, specified as a value from 0.0 to 1.0.     参数二: alpha 表示通明度 The opacity value of the color object, specified as a value from 0.0 to 1.0.
 
    [appNameLabel setBackgroundColor:[UIColor colorWithWhite:0.8 alpha:0.5]];
 
    [appNameLabel setNumberOfLines:2];
 
    [[self superview]  addSubview:appNameLabel];
 
     
 
    //禁用按钮
 
    [button setEnabled:NO];   
 
     
 
    //首尾式动画:修改对象的属性,frame、bounds、alpha;但其不容易监听动画的完成时间、不容易实现动画的嵌套
 
    // 初始透明度
 
    [appNameLabel setAlpha:0.0];//完全透明
 
    //^ 表示为block, 是一个预先准备好的代码块;可以当作参数传递,在需要的时候执行;块代码在oc中,使用非常普遍
 
    //块动画
 
    [UIView animateWithDuration:1.0f animations:^{
 
        //修改对象的属性
 
        [appNameLabel setAlpha:1.0];//动画内容
 
    } completion:^(BOOL finished) {
 
        //动画结束之后,删除控件
 
        [UIView animateWithDuration:1.0f animations:^{
 
            [appNameLabel setAlpha:0.0];
 
        } completion:^(BOOL finished) {
 
            [appNameLabel removeFromSuperview];
 
        }];
 
    }];
 
     
 
}
 
@end

VI、see also

Xcode使用小技巧

kunnan.blog.csdn.net/article/det…

🍅 联系作者: iOS逆向(公号:iosrev)


🍅 作者简介:CSDN 博客专家认证🏆丨全站 Top 50、华为云云享专家认证🏆、iOS逆向公号号主


🍅 简历模板、技术互助。关注我,都给你。