AbooJan Blog

专注移动开发


  • 首页

  • 归档

  • 分类

  • 关于

运行时小结

发表于 2017-03-11 | 分类于 iOS开发

第一部分

objc_msgSend 这是Objective-C的方法调用的核心,它可调用一个类的所有方法,不管它有没有暴露出来。

例如:
TestObj.h 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

@interface TestObj : NSObject

@property (nonatomic, assign) NSInteger userId;
@property (nonatomic, copy) NSString *userName;
@property (nonatomic, assign) BOOL gender;


+ (NSInteger)test;
+ (NSString *)testWithArg:(NSInteger)arg1 arg2:(NSString *)arg2;

- (BOOL)isChnage;

@end
TestObj.m 文件:

@implementation TestObj
+ (NSInteger)test
{
NSLog(@"这是测试方法");

return 6;
}

+ (NSString *)testWithArg:(NSInteger)arg1 arg2:(NSString *)arg2
{
NSLog(@"测试方法 : %ld -- %@", arg1, arg2);

return @"test";
}

+ (void)test2
{
NSLog(@"内部方法");
}

- (BOOL)isChnage
{
NSLog(@“有没有改变");

return NO;
}

- (NSString *)changeUserName:(NSString *)userName
{
return @"修改后的名字";
}

@end

以下是调用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// 直接调用类方法,返回值需要强转,也可以不处理返回值
NSInteger test1 = (NSInteger)objc_msgSend(objc_getClass("TestObj"), sel_registerName("test"));

NSLog(@"测试1:%ld", test1);

// 调用有参数的类方法
NSString *test2 = (NSString *) objc_msgSend(objc_getClass("TestObj"), @selector(testWithArg:arg2:), 6, @"消息");

NSLog(@"测试2:%@", test2);

// 直接调用TestObj里面的类方法,尽管它没有暴露出来, 这就是运行时的厉害之处
objc_msgSend(objc_getClass("TestObj"), sel_registerName("test2"));

// 调用方法
TestObj *testObj = [TestObj new];
objc_msgSend(testObj, sel_registerName("isChnage"));


第二部分

无论目标类有没有将方法暴露出来,一样可以同过运行时将它遍历出来,并可以找出方法的入参.

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

unsigned int methodCount;
Method *methods = class_copyMethodList(objc_getClass("TestObj"), &methodCount);
for (int i = 0; i < methodCount; i++) {

Method tempMethod = methods[i];

NSLog(@“方法名称:%s", sel_getName(method_getName(tempMethod)));
NSLog(@“入参个数:%d", method_getNumberOfArguments(tempMethod));
NSLog(@“%s”, method_getTypeEncoding(tempMethod));
NSLog(@“返回值类型:%s”, method_copyReturnType(tempMethod));

IMP imp = method_getImplementation(tempMethod);
NSLog(@"IMP: %@", imp_getBlock(imp));

NSLog(@"---入参----\n");

unsigned int argCount = method_getNumberOfArguments(tempMethod);
for (int j = 0; j < argCount; j++) {
NSLog(@"%s", method_copyArgumentType(tempMethod, j));
}
}

// 手动释放,避免内存泄露
free(methods);

1
2
1. 方法的入参默认会有两个,这是系统运行时添加进去的。
2. 运行时的入参和返回的数据类型跟OC 的数据类型不同,以下是苹果官方提供的类型对应列表

第三部分

通过运行时,可以获取方法的变量,并且可以通过映射,动态修改变量名称,MJExtension 框架就是利用了这一特点,所以它可以通过MJExtensionConfig 文件来修改参数的映射。

参考:http://www.cnblogs.com/ludashi/p/4673935.html

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13

unsigned int propertyCount;
objc_property_t *propertys = class_copyPropertyList(objc_getClass("TestObj"), &propertyCount);
for (int i = 0; i < propertyCount; i++) {
objc_property_t tempProperty = propertys[i];

NSLog(@“成员变量名称:%s", property_getAttributes(tempProperty));
NSLog(@"成员变量描述:%s”, property_getAttributes(tempProperty));
}

// 释放
free(propertys);


第四部分

一个对象可以绑定任何一个对象,包括 block。

涉及到的方法如下:

1
2
3
4
5
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)  // 设置绑定对象

objc_getAssociatedObject(id object, const void *key) // 获得绑定对象

objc_removeAssociatedObjects(id object) // 移除绑定对象

绑定对象的时候,需要根据对象的属性,设置不同的关联策略,也就是Objc的内存管理的引用计数机制,包括有:

1
2
3
4
5
6
7
8
9
OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */

OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.

OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.

OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.

OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// 绑定对象
TestObj2 *a = [TestObj2 new];
a.userID = 1001;
a.userName = @"AbooJan";
a.books = @[@"Harry", @"永无止境", @"编程思想"];

_test = [[TestObj alloc] init];
objc_setAssociatedObject(_test, "testObj", a, OBJC_ASSOCIATION_RETAIN);
objc_setAssociatedObject(_test, "testNum", @(666), OBJC_ASSOCIATION_ASSIGN);

// 获得绑定对象
TestObj2 *a = objc_getAssociatedObject(_test, "testObj”);
NSNumber *testNum = objc_getAssociatedObject(_test, "testNum");

NSLog(@"%@", [a description]);
NSLog(@"%ld", [testNum integerValue]);


第五部分

Method Swizzling,本身Objc的方法调用是通过消息转发机制来实现的,既然如此,就可以通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

/*
* 1. 如果是一个子类,一般在 initialize 方法中替换方法
* 2. 如果是一个分类,一般在 load 方法中实行替换
*
* 在以上两个方法中替换,是保证它在一启动的时候就实行替换
*
*
* load和initialize有很多共同特点,下面简单列一下:

* 1.在不考虑开发者主动使用的情况下,系统最多会调用一次
* 2.如果父类和子类都被调用,父类的调用一定在子类之前
* 3.都是为了应用运行提前创建合适的运行环境
* 4.在使用时都不要过重地依赖于这两个方法,除非真正必要
*
*
* +load 能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。
* +initialize 在其所属类的方法被调用或类初始化时会被调用,否则它可能永远不会被调用
*
*/
+ (void)load
{
NSLog(@"load");
}

+ (void)initialize
{
// dispatch_once是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,
// 保证线程的安 全,避免并发引发问题,认为它是Method Swizzling 的最佳实现


static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 第一种初始化方法
Class target = [self class];

Method originalMethod = class_getInstanceMethod(target, sel_registerName("description"));
Method swizzMethod = class_getInstanceMethod(target, sel_registerName("swizzle_description"));


// 第二种初始化方法
// Class aClass = object_getClass((id)self);
//
// Method originalMethod = class_getClassMethod(aClass, @selector(description));
// Method swizzledMethod = class_getClassMethod(aClass, @selector(swizzle_description));


/*
* object_getClass((id)self) 与 [self class] 返回的结果类型都是 Class,
* 但前者为元类,后者为其本身,因为此时 self 为 Class 而不是实例。
*/


/*
* 如果类中不存在要替换的方法,那就先用class_addMethod和class_replaceMethod函数添加
* 和替换两个方法的实现;如果类中已经有了想要替换的方法,那么就直接调
* 用 method_exchangeImplementations函数交换了两个方法的 IMP。
* 个人建议将要替换的方法实现出来.
*/


// BOOL didAddMethod = class_addMethod(target, method_getName(originalMethod), method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
//
// if (didAddMethod) {
//
// class_replaceMethod(target, method_getName(swizzMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
//
// }else{
// method_exchangeImplementations(originalMethod, swizzMethod);
// }


// 如果确定已添加替换的方法,直接执行替换就可以了
method_exchangeImplementations(originalMethod, swizzMethod);

});
}

- (NSString *)description
{
NSLog(@"原来的方法");

return [super description];
}

- (void)swizzle_description
{
NSLog(@"这是替换的方法");

// 本身替换的方法只会执行一次,当执行以下方法的时候,它就会调用该类原来的方法了
[self aboo_description];


// 如果调用以下方法,就会进入死循环
// [self description];
}

Demo: https://github.com/AbooJan/RunTimeDemo.git

怎样学习一项新的技能

发表于 2017-03-11 | 分类于 随笔

我本身是iOS开发工程师,最近应公司要求,需要学习前端开发技术,趁着周末总结一下这段时间的学习过程。

在小学学加减法的时候,老师通常会先让我们明白:什么是加法呀、什么是减法呀,然后会写几个简单的例子让我们看看,1+1它等于2,2-1它等于1,为什么呢?因为一根手指跟另一根手指凑一块,加起来有2个手指;这里有2个苹果,拿走了一个,就剩下一个了。当我们掌握加减法它的计算规则之后,老师就会给类似的加减法题让我们去练习,从1位数的加减一步一步到多位数的加减,经过不断的练习之后,我们就可以完全掌握这个加减法了。

对于学习一项新技能的过程,我把它给简化为3个阶段:

1
2
3
1. 建立概念,理解这到底是个什么东西。
2. 学习实例,掌握它是以一个怎样的规则进行的。
3. 参与实践,从简单到复杂一步步完全掌握这项技能。

《异类》的作者格拉德威尔提出个理论叫一万小时理论,一万小时的有效实践可以让一个菜鸟蜕变成一位大神。所以对于一项技能,从上手到精通,是需要经过一个漫长的实践过程的,需要从实践中一步一步形成自己解决问题的方法论,在以后遇到问题时就能快速解决。

这也是为什么大部分公司的招聘标准里面有工作年限这个要求吧,尤其是大公司,没个3年的工作经验它把你直接Pass掉。但其实很多人即使他工作了个3、5年,能力依然停留在当初参加工作时1、2年的工作水平。

我觉得机器学习的基本思想也类似这样,比如自动驾驶,首先需要建立一个数学模型去处理汽车自动驾驶这个问题,一开始建立的数学模型是很粗糙、有很大误差的,那怎么去解决这些问题呢?答案就是通过大量的训练。在大量的训练过程中,会遇到各种各样的问题,为了解决遇到的问题,就需要不断调整当初建立的数学模型,从而找到问题的最优解。

对于一项新的技能,上手不难,难的是把它精通。而精通的过程是很漫长的,需要好好把心沉下来一步一步往前走。在这日新月异的互联网时代,单凭一项技能是很难在互联网行业长久发展的,需要让自己不断成长、不断学习,共勉!

互联网项目开发流程总结

发表于 2017-03-11 | 分类于 随笔
  1. 需求的来源由市场、运营、产品等提供,每个需求点都有优先等级数,这个优先级数可能根据用户反馈情况、运营活动、市场营销等。

  2. 根据开发周期的长短,产品经理从需求池提取一定量优先级高的需求整理成文档,出原型图,提前发给开发人员查阅。定开发周期的时候最好不要太长,一般2个星期左右的时间会比较好,小步快走。

  3. 等过了第3步之后,如果开发时间总和超出之前指定的dead line ,产品经理需要对需求做部分删减,从而能够在指定时间内完成上线。

  4. 在第4步Api 制定的时候,我还是比较喜欢前端跟服务端一起商定接口数据返回字段,之前有好几次遇到服务端提供的字段并不能完成需求功能的情况,这样很容易出现做无用工的情况。

  5. 在产品进入开发阶段,每个版本要分模块来进行开发,一个模块开发完之后就丢给测试部门测试,开发部则进入下一个模块的开发,边开发边改bug 。待下一个模块提测的时候,也作为上一个模块的回归测试。分模块提测的时候,UI设计师也要抽一点时间来验UI。

  6. 在开发期间,产品经理除了回答和完善当前版本的需求,要开始准备下一个版本的需求了。对于UI设计师,要跟着产品经理走,也要着手准备下一个版本的UI 设计了。对于测试人员,要提前准备好测试用例和测试数据。

  7. 版本上线之后,要做项目总结,对版本开发进行复盘,每一次都要统计版本的bug 数量和版本质量,开发过程中暴露的问题要及时提出并总结经验。

  8. 围绕着产品,最好有一个统一的协作工具,集开发、测试、产品一体,自己公司内部搭也好,用在线的工具也好,它主要起一个辅助作用,提高效率。

  9. 对于开发以外的其他部门,部门自己要提前做好规划,到了什么时间点做什么事情,这样提的需求才好分优先级。

  10. 项目启动之初,会比较困难些,需要做好各种规范,但跑起来之后就可以像流水线一样,一个版本接一个版本迭代开发,整体效率就会上来了。

Vapor 实战5

发表于 2016-10-04 | 分类于 Vapor

服务端拿到的客户端请求都是 Request 类型,它是框架 HTTP 里面的一个类。

一个 Request 实例,包好以下几个基本属性:

1
2
3
4
5
6
public var method: Method
public var uri: URI
public var parameters: Node
public var headers: [HeaderKey: String]
public var body: Body
public var data: Content

Vapor支持的 HTTP 请求方法包括:

1
2
3
4
5
6
7
8
9
10
11
12
public enum Method {
    case delete
    case get
    case head
    case post
    case put
    case connect
    case options
    case trace
    case patch
    case other(method: String)
}

URI

例如一个HTTP请求:https://www.google.com/search?query=vapor#fragments
它对应的属性的值如下:

1
2
3
4
5
let scheme = request.uri.scheme;  // https
let host = request.uri.host; // www.google.com
let path = request.uri.path; // /search
let query = request.uri.query; // query=vapor
let fragment = request.uri.fragment; // fragments

Headers

可以通过 request.headers["key"] 来获取对应的请求头部信息,例如:

1
2
let contentType = request.headers["Content-Type"];
let token = request.headers["Authorization"];

请求体

拿到客户端传过来的值有多种方法,可以是:

1
2
3
4
5
- request.data["key"]
- request.query?["key"]
- request.parameters["key"]
- request.body
- request.json["key"]

request.data["key"] 是常用的获取传值的方法。

Vapor 实战4

发表于 2016-10-04 | 分类于 Vapor

Demo中用的是Mongo DB,首先需要到它的官网下载安装程序:https://www.mongodb.com。
安装配置好之后就可以进行以下操作了。

  1. 下载Mongo DB数据库连接驱动,在 Package.swift 中添加下载地址:

    1
    .Package(url: "https://github.com/vapor/mongo-provider.git", majorVersion: 1, minor: 0)
  2. 在项目的文件夹 Config 下创建数据库配置文件 mongo.json, 在里面填写以下内容:

    1
    2
    3
    4
    5
    6
    7
    { 
    "user": "数据库用户名",
    "password": "用户名密码",
    "database": "数据库名称",
    "port": "端口号",
    "host": "数据库运行地址"
    }
  3. 在数据模型中实现协议 Preparation 的2个方法,示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static func prepare(_ database: Database) throws {
            
            try database.create("Users") { users in
                users.id()
                users.string("name")
                users.string("phone")
                users.string("pw")
                users.bool("gender")
                users.int("age")
            }
        }
        
        static func revert(_ database: Database) throws {
            try database.delete("Users")
        }
  4. 如果本身数据库模型对应的表中没有存在,需要在模型中添加以下成员变量:

    1
    var exists: Bool = false;
  5. 在 main.swift 中需要对数据模型和数据库驱动做声明,示例:

    1
    let drop = Droplet(preparations: [User.self], providers: [VaporMongo.Provider.self]);
  6. 完成以上步骤之后,就可以在控制器或其他地方进行数据库的 CRUD 操作了。

demo

https://github.com/AbooJan/VaporDemo

Vapor 实战3

发表于 2016-10-04 | 分类于 Vapor

Vapor网络框架的代码设计模式是遵循MVC的,新建文件的时候,你需要把它放进对应的文件夹,不然 vapor build 的时候会提示错误,看它的代码文件结构:

1
2
3
4
5
// M
.
├── App
. └── Models
. └── User.swift
1
2
3
4
5
6
// V
.
├── App
└── Resources
└── Views
└── user.html
1
2
3
4
5
// C
.
├── App
. └── Controllers
. └── UserController.swift

Model

新建的数据模型类需要继承自 Fluent 框架里面的 Model 类,一方面是方便 JSON 数据的转化,另一方面是方便与数据库连接操作。

继承自 Model 的类需要注意以下几点:

  • 必须添加一个 id 成员变量:var id:Node?
  • 必须实现以下2个方法:
    1
    2
    3
    init(node: Node, in context: Context)

    func makeNode(context: Context)
  • 必须实现协议 Preparation 里面的2个方法,是用于做数据库操作的,如果不需进行数据库操作,直接空实现就可以了:
    1
    2
    3
    func prepare(_ database: Database)

    func revert(_ database: Database)

View

视图文件存放在文件夹 Resources 的 View 子文件夹内,它可以是 html 文件,也可以是标签型语言的文件,模板项目里面的视图文件则是 leaf 后缀的。

视图文件写好之后,则可以通过 drop.view.make() 函数访问,例如:

1
2
3
drop.get("html") { request in 
return try drop.view.make("index.html")
}

Controller

控制器主要是方便代码解耦,把不同的业务逻辑放到不同的控制器里面。

一个简单的控制器可以像下面那样:

1
2
3
4
5
6
7
8
final class HelloController { 
func sayHello(_ req: Request) throws -> ResponseRepresentable {
guard let name = req.data["name"] else {
throw Abort.badRequest
}
return "Hello, \(name)"
}
}

然后在 main.swift 声明以上控制器方法:

1
2
let hc = HelloController();
drop.get("hello", hc.sayHello);

这样运行项目就可以通过 http://{host}/hello 访问到 sayHello 方法了。

demo

https://github.com/AbooJan/VaporDemo

Vapor 实战2

发表于 2016-10-04 | 分类于 Vapor

初始化项目 vapor new NetworkTest ,vapor 工具箱会生成模板项目。

  1. 项目的所有配置文件都放在 Config 文件夹中,它是支持环境模式配置的,比如分 production 、development 等模式。不同环境的配置文件处于不同的文件夹下,但是文件名相同,例如:
    环境切换.png

  2. 运行项目的时候就可以通过 --env= 命令来切换运行环境,例如运行在 production 环境之下:

    1
    vapor run --env=production
  3. 所有Config文件夹里面的配置文件都可以通过 app.config 获取,语法 app.config[<#file-name#>, <#path#>, <#to#>, <#file#>] , 例如 servers.json 文件:

    1
    2
    3
    4
    5
    6
    7
    {
    "default": {
    "port": 3080,
    "host": "10.0.0.66",
    "securityLayer": "none"
    }
    }

    获取里面的 host 内容:

    1
    let host = app.config["servers", "http", "host"].string
  4. 动态获取命令行输入的值,例如运行时命令行输入:

    1
    vapor run server --mongo-password=666666

    需要获取输入的 mongo-password ,则可以这样获取:

    1
    let mongoPassword = app.config["cli", "mongo-password"].string
  5. 配置服务器运行的地址和端口,则可以通过修改 servers.json 里面的 host 和 port 字段。

Vapor 实战1

发表于 2016-10-03 | 分类于 Vapor

Vapor 是一个使用Swift 3.0 开发的服务器网络框架,它可以运行在macOS和Ubuntu上面,它的官网:http://vapor.codes/。

环境搭建

  1. 首先系统的Swift版本必须升级到3.0, 可以使用命令查看。
    swift-version.png

  2. 安装Vapor工具箱,可以使用命令

    1
    curl -sL toolbox.vapor.sh | bash

    或者是(前提是已经安装好 Homebrew)

    1
    brew install vapor/tap/toolbox
  3. 安装好之后,输入命令可以看到相关提示
    vapor.png

Hello World

  1. 使用命令 vapor new hello ,即可在当前目录创建模板项目。
    初始化项目.png

  2. 项目目录结构
    项目文件结构.png

  3. 使用命令 vapor build ,然后 vapor run ,运行当前项目,打开查看效果:http://localhost:8080

  4. 命令 vapor xcode 可以把当前项目编译成 Xcode 项目,方便查看。

Cocoapod 个人库创建、提交、更新

发表于 2016-02-12 | 分类于 iOS开发

step1

  • 在提交到自己的库到Cocoapod仓库之前,需要先注册用户

    • 在终端执行命令:

      1
      pod trunk register 用户邮箱 ‘用户名' --description='设备名称'
    • 执行完以上命令后,填写的邮箱会收到一封来自Cocoapod的邮件,点击链接即可注册成功

    • 在终端执行命令来检查是否注册成功:pod trunk me

step2

  • 本地创建podspec文件

    • 在本地创建库文件夹,目录结构按照:

      1
      2
      3
      4
      ├── Specs  
      └── [SPEC_NAME]
      └── [VERSION]
      └── [SPEC_NAME].podspec
    • 进入到目标文件夹,执行命令:pod spec create 库名称

    • 打开创建的podspec,按照里面的提示填写相关信息,例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
       Pod::Spec.new do |s|
      s.name = "AJPickerTextField"
      s.version = "1.0.0"
      s.summary = "弹出Picker选择框的自定义TextField"
      s.description = <<-DESC
      弹出Picker选择框的自定义TextField,将UIPickerView跟UITextField融合,简单化接口,功能比较简单。
      DESC
      s.homepage = "https://github.com/AbooJan/AJPickerTextField"
      s.license = "MIT"
      s.license = { :type => "MIT", :file => "LICENSE" }
      s.author = { "AbooJan" => "aboojaner@gmail.com" }
      s.platform = :ios
      s.platform = :ios, "7.0"
      s.source = { :git => "https://github.com/AbooJan/AJPickerTextField.git", :tag => "1.0.0" }
      s.source_files = "AJPickerTextField/AJPickerTextField/*.{h,m}"
      s.requires_arc = true
      end
    • 执行命令检查podspec文件的正确性:

      1
      pod spec lint 库名称.podspec --allow-warnings 

      如果检查通过会显示:

      1
      库名称.podspec passed validation
    • 检查通过后,就可以推送到cocoapod仓库了,执行命令:

      1
      pod trunk push 库名称.podspec --allow-warnings
    • 查看是否提交成功,可以到github spec里面查看是否有目标库目录:https://github.com/CocoaPods/Specs/tree/master/Specs

step3

如果要更新版本,需要更新podspec文件,然后推送到cocoapod仓库。

step4

每次提交到cocoapod仓库的时候,开源库git版本控制需要打一个tag,然后执行检查命令的时候才能找到目标代码文件。

12
AbooJan

AbooJan

岭深常得蛟龙在,梧高自有凤凰栖

19 日志
6 分类
10 标签
GithHub
© 2018 AbooJan
由 Hexo 强力驱动
主题 - NexT.Pisces