电一闪的博客

平凡亦有极致体现

搭建企业级app分发服务器

| Comments

一、配置证书:

1、生成key

  在apache2目录下,新建ssl目录

sudo mkdir /private/etc/apache2/ssl
cd /private/etc/apache2/ssl
sudo ssh-keygen -f server.key

  这里不设置RSA口令

2、生成请求文件

sudo openssl req -new -key server.key -out request.csr

3、生成SSL证书

sudo openssl x509 -req -days 365 -in request.csr -signkey server.key -out server.crt

  至此,生成/private/etc/apache2/ssl/目录下”request.csr” “server.crt” “server.key” “server.key.pub”四个文件

二、配置apache让其支持https

1、修改/private/etc/apache2/httpd.conf文件,去掉下面三行前面的注释符号

LoadModule ssl_module libexec/apache2/mod_ssl.so
LoadModule socache_shmcb_module libexec/apache2/mod_socache_shmcb.so
Include /private/etc/apache2/extra/httpd-ssl.conf
Include /private/etc/apache2/extra/httpd-vhosts.conf

2、修改/private/etc/apache2/extra/httpd-ssl.conf ,去掉下面两行前面的 ‘#’

SSLCertificateFile "/private/etc/apache2/ssl/server.crt"
SSLCertificateKeyFile "/private/etc/apache2/ssl/server.key"

3、修改/private/etc/apache2/extra/httpd-vhosts.conf,在 ‘NameVirtualHost * :80’ 后面添加:

NameVirtualHost *:443

  在文件末尾添加:

<VirtualHost *:443>
SSLEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
SSLCertificateFile /private/etc/apache2/ssl/server.crt
SSLCertificateKeyFile /private/etc/apache2/ssl/server.key
ServerName localhost
DocumentRoot "(此为Apache工作根目录)"
</VirtualHost>

三、架设企业级应用服务。

1、生成install.html

  代码如下所示

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
    * {
        font-size: 52px;
    }

    a {
        line-height: 2.5;
        padding: 20px;
    }
</style>
安装步骤:<br/>
1、首次安装需要点击<a href="https://192.168.1.239/server.crt" id="text">“安装证书”</a><br/>
2、否则直接点击 <br/>
<a href="itms-services://?action=download-manifest&url=https://192.168.1.239/g0/mlf.plist" id="text">“安装平台版app”</a> <br/>

2、手机需先安装server.crt。

  因此拷贝/private/etc/apache2/ssl/server.crt到Apache根目录下,install.html中编写超链接指向该证书文件,供设备下载安装。

3、将mlf.plist mlf.ipa mlf.png存放在自定义目录下,并在install.html中指向它们。

  mlf.plist代码如下所示

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
    <dict>
        <key>assets</key>
        <array>
            <dict>
                <key>kind</key>
                <string>software-package</string>
                <key>url</key>
                <string>https://192.168.1.239/g0/mlf.ipa</string>
            </dict>
            <dict>
                <key>kind</key>
                <string>full-size-image</string>
                <key>needs-shine</key>
                <true/>
                <key>url</key>
                <string>https://192.168.1.239/g0/mlf.png</string>
            </dict>
            <dict>
                <key>kind</key>
                <string>display-image</string>
                <key>needs-shine</key>
                <true/>
                <key>url</key>
                <string>https://192.168.1.239/g0/mlf.png</string>
            </dict>
        </array>
        <key>metadata</key>
        <dict>
            <key>bundle-identifier</key>
            <string>cn.meilif.mlfinhouse</string>
            <key>bundle-version</key>
            <string>2.0</string>
            <key>kind</key>
            <string>software</string>
            <key>title</key>
            <string>美丽范</string>
        </dict>
    </dict>
</array>
</dict>
</plist>

Swift与Objective-c混编

| Comments

背景

  适时搞点新鲜玩意,增加挑战性,同时学以致用。

混编第一步:创建桥接头文件,让Swift调用OC

  添加一个swift文件到工程,这时会提示是否创建一个桥接头文件。如下图所示:

  自动创建的桥接文件命名格式为ProductName-Bridging-Header.h,默认在当前添加Swift文件的同一目录(如要更换文件存放目录,在project settings中搜索,修改指定项的值给予新路径即可)。该文件中用于导入给swift文件使用的OC类,使用#import “xxx.h”的OC语法导入,文件名及内容如下所示。(导入OC头文件一直都没有提示,But it works.)


  导入后,在swift文件中,便可用swift的方式调用。如下图

混编第二步:导入swift头文件,让OC调用Swift

  第一步完成后可在Swift中调用OC类,而反向调用应该怎么做呢?答案是,导入Xcode自动生成的隐式Swift类的头文件。如下图:

  这个隐式文件由Xcode自动生成,且只需导入它,便可调用所有swift类。需要注意的是,在必要的.m文件中再导入,防止头文件引用循环,也加快编译速度。
  基本规则如下表所示:

调用Swift代码 调用OC代码
Swift代码 无需声明 依赖ProductModuleName-Bridging-Header.h文件
OC代码 #import “ProductModuleName-Swift.h” #import “Header.h”

混编第三步:定义类和协议遵从

  使用Swift创建一个UIBaseViewController的子类,需要使用如下代码声明class。

class MLF_Tab_Me_MyCardsViewController: UIBaseViewController {

}

  如果要实现某个协议,则继续在UIBaseViewController后面用逗号分隔协议,比如:

class MLF_Tab_Me_MyCardsViewController: UIBaseViewController, UITableViewDelegate, UITableViewDatasource {
    var mainTableView : UITableView!
}

  传统OC中,若采用纯代码初始化界面,一般都是在viewDidLoad中完成;但swift默认必须在调用super.init构造方法前就初始化新增成员。不过像上述代码中添加mainTableView那样,使用implicitly unwrapped optional(隐式解析可选)类型,可以延迟到viewDidLoad时初始化,默认初始值为nil。

混编第四步:适应新的Json反序列化机制

  Swift的Json反序列化,对类型要求十分严格。由于Swift的原生String类型没有足够的方法转数字类型,因此很有必要将原生对象转换为NSString或NSNumber。而这样的类型转换,必须对原生的可选类型对象做force unwrapping(强制解析)才能type casting(类型转换)。对一个nil值做force unwrapping操作,会引发运行时错误。

fatal error: unexpectedly found nil while unwrapping an Optional value

  不知道会不会有人问,为什么要用可选类型?因为只有可选类型才能被赋值为nil。假若给非可选类型显式赋值nil,编译时报错;隐式赋值nil,引发运行时错误。如下图,程序运行完31行后就crash了。

  从程序健壮性角度而论,客户端是不能假定某个key一定可以取到value的,因此反序列化后对象为nil是常有的现象。OC中的运行时动态类型机制,决定了类型检查相对宽松,给nil发送消息,运行时会自动忽略该条消息。
  另外,OC中NSString和NSNumber几乎有相同的取值方法,且基于运行时动态类型,json反序列化时无需检查对象类型便可直接调用那些同名的取值方法。比如给NSString或NSNumber对象发送longLongValue消息,都能作为int64_t类型取出值;对象为nil也没关系,取出的值为0。但在Swift中,类型如果错误,会直接报运行时错,导致crash。
  综上述,这样的规则可以让人感觉出,Swift更加注重安全性,要求更多地用代码显式表明行为。个人认为,如何在遵循Swift新规则的前提下,减少条件表达式的代码量,将是一项值得研究的议题,也可能是Swift后续版本可能优化的地方。

Swift学习笔记

| Comments

背景

  swift2.0本月刚刚发布,在1.0基础上,增加了些许新特性,改变了1.0中少量语法。

Swift 1.0理解畅谈

一、优势

  1、if x = y是错误代码,在Swift里会报编译错。
  2、溢出运算符、运算符重载。
  3、swift可以对浮点数取余 8%2.5结果为0.5。
  4、OC时代

return str1 != nil ? str1 : defaultStr

  现在swift里可以简化为

return str1 ?? defaultStr。  

  5、Unicode字符标量方便表示,比如 let oneUniChar = “\u{2665}”。
  6、作为基础类型之一的String,其赋值时可视作深拷贝(实际大多数情况下不是),又经swift编译器优化,在必要时才会深拷贝,以提高性能。
  7、向数组中加入元素可以直接用运算符+=,前提是保证被加入的元素与数组本身的元素类型保持一致。
  8、可以方便地将数组中某一范围的元素,替换为另一数组,且前后数量可以不匹配。例如

var elements = ["lym1", "lym2", "lym3"]
elements[1...2] = ["lym4"]

  这样数组elements就被修改为[“lym1”, “lym4”]。
  9、for-in循环支持区间、序列、集合、系列遍历,使用_可以方便控制只循环次数且不对值访问。
  10、使用switch不再需要break语句,且case的条件判断可以使用if语句样式的条件表达式(例如区间、元组、字符串),因此原来各种条件判断、计算结果非整型的表达式,现在都可以使用switch来提高判断效率。
  11、switch语句默认不再有贯穿case的做法(case之间不写break),而是显式地使用fallthrough关键字,不丢弃贯穿特性,同时意图更加清晰。
  12、使用标签标注“循环”或“switch代码段”,方便地跳出多重循环(第一次了解这个概念是在学习java时,可以说是借鉴)。
  13、swift中简化了函数重载,C++中函数重载需要多次函数定义,而OC原本不支持函数重载,现swift使用函数默认参数值即可在一次函数定义中实现函数重载。例如,方法定义:

func testOverrideMethod(value1:Int16, value2:Int16 = 2) {
    print("testOverrideMethod, value1:\(value1), value2:\(value2)")
}  
testOverrideMethod(1, value2: 22)  

  如果给参数设置了默认值,那么调用方法时必须指明参数名;否则调用时可以不用写名”value2:“。
  14、swift中函数传值时,如果外部对象真正的值想在函数体中被修改,使用inout关键字修饰变量,用以替代指针的指针这样的复杂概念“**”,例如:

func swap (inout value1 : Int16, inout value2 : Int16) {
    var tempValue = value1
    value1 = value2
    value2 = tempValue
}
var value1 = 1, value2 = 2
//打印结果"value1:1, value2:2"
print("value1:\(value1), value2:\(value2)") 
swap(&value1, &value2)
//打印结果"value1:2, value2:1"
print("value1:\(value1), value2:\(value2)")

  15、swift中的枚举可支持任意类型,包括但不限于Int、String、Bool、Tuple(元组),且可以在一个枚举定义中支持任意多种成员类型。
  16、swift拥有“可失败构造器”。在OC中,常常碰到先alloc分配内存,再initWithXXX传递参数初始化,可在某些条件下(比如参数类型错误)无法初始化有意义的对象,为instance分配内存就造成了时间空间浪费。具体做法是,swift类构造器的init关键字后紧跟问号,方法定义写成init?(xxx),在满足失败条件时,返回nil。
  17、与OC同时期的C++早就有了泛型的概念,而OC一直缺乏,现在swift补上了这一空缺。

二、陷阱

  1、使用countElements计算字符串长度是基于unicode,而NSString的length方法则是基于UTF-16,因此NSString的length属性在被swift的String访问时,会成为utf16count。
  2、在swift中构造的类,OC中无法继承。
  3、为了避免循环引用,在OC中导入swift类,需要在.h文件中用@class声明,.m文件中导入。
  4、swift用于判断两个类实例对象是否为同一对象的运算符为”===“和”!==“,分别名为“等价于”和“不等价于”。而实例具体内容是否相等需要根据类中定义的“等于”(==)方法判定,该方法属运算符重载。
  5、swift中的String、Array、Dictionary类型均为结构体类型,赋值时为深拷贝赋值;不同于OC中NSString、NSArray、NSDictionary的浅拷贝,即复制引用。不过swift会在后台完成性能优化,以确保在有需要时才做深拷贝。
  6、swift的构造器,构造顺序与OC相反。OC中必须先调用父类初始化方法,才能初始化子类变量;而swift必须先给子类未初始化的变量赋值,再调用父类指定构造器(Desinated)。其本质原因为,swift加强了构造安全,目的是准确分配内存。具体步骤为:

a. 初始化子类中所有引入的存储型属性 
b. 调用父类指定构造器。按此步骤,直到祖先类最顶端的指定构造器调用完成,才算子类实例完全初始化,此时self成为一个有效的引用型变量
c. 自顶向下,让各个子类有机会修改继承来的存储型变量。

  反观OC,在子类初始化时,自下向上调用初始化方法,先使self成为有效对象,再为子类变量分配内存。无所谓孰优孰劣,只是swift在构造器的安全性上略有提升。
  7、在定义子类时,想要继承父类所有构造器,必须没有定义任何子类构造函数;或子类覆盖实现了所有父类指定构造器(Desinated)。这样设计的目的,个人猜想是为了让子类更纯粹,不暴露无关的父类构造器以致混淆类定义,强化类的目的性。
  8、便利构造器(Convenience)不可继承,且实现中必须以调用指定构造器结束(Desinated)。它在类中起辅助作用,目的是减少构造器中的重复代码。
  9、swift中的前缀运算符与后缀运算符,必须紧跟变量,中间不能插空格。例如,OC中常写i ++,但在swift中必须i++。

三、与OC对比

  1、swift有简单明了的函数声明。首先,它有内部参数名,在调用时会被当作占位符。

  这一块乍看起来像JS,但仔细观察,会发现一些OC的影子。比如倒数第二行带占位符的函数调用,参数名后跟“:value”,是典型的OC风格。 不仅于此,为了防止代码编写完成后由于没有内部参数名而难以阅读,swift还增加了外部参数名机制。代码如下

  ”from”和”say”作为外部参数名,用于显式描述参数用途。另,在内部参数名前使用#标注,即可同时作为内外部参数名。
  2、我觉得,swift彻底把“方法”(函数)变成了“block”(命名闭包)。比如这段代码:

  根据传入值的true or false,返回在函数内部定义的函数变量。使用OC的block实现如下,是否有异曲同工之妙?

  3、swift的尾随闭包非常简便易用,但简写的方法非常跳跃。(小屏请横屏查看)

代码:

//闭包作为函数的最后一个参数时,可以改写为尾随闭包
//定义一个函数,其参数是一个类型为Int64,返回值为Void的闭包closure
func someFuncForTestClosure (closure:(Int64) -> Void) {
    closure(4)
}
//调用已定义的函数,并定义闭包作为参数
someFuncForTestClosure ({ (para) -> Void in
    print("调用了closure,参数为\(para)")
    print("调用了closure,参数为\(para)")
})
//如果闭包特别复杂特别长,尾随闭包作用凸显
someFuncForTestClosure (){ (para) -> Void in
    print("调用了closure,参数为\(para)")
    print("调用了closure,参数为\(para)")
    print("调用了closure,参数为\(para)")
    print("调用了closure,参数为\(para)")
    print("调用了closure,参数为\(para)")
}
//尾随闭包前的函数参数括号也可以省略
//不理解缩写过程咋一看还以为是函数定义
someFuncForTestClosure { (para) -> Void in
    print("调用了closure,参数为\(para)")
    print("调用了closure,参数为\(para)")
}
//简直丧心病狂,还可以简写为
someFuncForTestClosure { 
    print("调用了closure,参数为\($0)")
    print("调用了closure,参数为\($0)")
}

  4、swift中,可变类型和常量类型用关键字声明区分,var代表变量,let代表常量。而原来OC中使用两种类NSXXX和NSMutableXXX定义是否可变。

  5、swift三种元类型在修改self值时的区别。其中class修改self或实例属性最简单;struct需要将关键字mutating作为方法前缀才能实现;enum枚举无法实现,因其无法定义存储型变量,只能定义case。

//class类
class Point {
    var x = 0.0, y = 0.0
    //class中的函数,对self的值想怎么改就怎么改
    func moveByX(deltaX: Double, y deltaY: Double) {
        self.x += deltaX
        self.y += deltaY
    }
    init(x:Double, y:Double) {
        self.x = x
        self.y = y
    }
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveByX(2.0, y: 3.0)
println("The point is now at (\(somePoint.x), \(somePoint.y))")
// 输出 "The point is now at (3.0, 4.0)"

//struct结构体
struct Point_struct {
    var x = 0.0, y = 0.0
    //要修改self的值,必须在声明前添加mutating关键字
    mutating func moveByX(deltaX: Double, y deltaY: Double) {
        self.x += deltaX
        self.y += deltaY
    }
    init(x:Double, y:Double) {
        self.x = x
        self.y = y
    }
}
var somePoint_struct = Point_struct(x: 1.0, y: 1.0)
somePoint_struct.moveByX(2.0, y: 3.0)
println("The point is now at (\(somePoint_struct.x), \(somePoint_struct.y))")
// 输出 "The point is now at (3.0, 4.0)"

//枚举,无法实现
enum Point_enum {
    //报错:枚举定义不能包含存储属性
    var x = 0.0, y = 0.0
    mutating func moveByX(deltaX: Double, y deltaY: Double) {
        self.x += deltaX
        self.y += deltaY
    }
    init(x:Double, y:Double) {
        self.x = x
        self.y = y
    }
}

App日志查看器

| Comments

背景

  实际开发中,有在生产环境里碰到诸多疑难杂症的棘手问题。对于问题,最佳解决手法之一是坏点检测,一层一层诊断,抽丝剥茧。那么诊断依据——日志,就是一种强大的执行手段。

最终效果

日志按时间戳倒序展示,提供关键字搜索

日志文件选取

日志文件选取器

日志文件选取

设计思想

一、文件结构:

  1、第一次构思的文件结构,就是简单的顺序写入文本文件,每一条记录加入一些格式化符号,增加辨识度。比如:

2015-06-01 21:35:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:36:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:37:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:38:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:39:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:39:07 应用特征名 日志级别【日志内容】\r\n  

  那么问题来了,作为文件,只有两种写方式:覆盖写 or 尾部添加。覆盖写不适用于保存日志,而尾部添加如上所示效果,较新的日志,排在底部。疯狂滚动是一种不可理喻的交互行为(开发人员要懂得节约用户的时间,无论用户是谁)。那有什么方法可以改进?
  2、第二次构思的文件结构,便是数据库建表。用数据库存储日志,可以顺序存储,倒序取出,最终展示很容易做到最近写入的展示在顶部。

2015-06-01 21:39:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:38:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:37:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:36:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:35:03 应用特征名 日志级别【日志内容】\r\n  
2015-06-01 21:34:07 应用特征名 日志级别【日志内容】\r\n  

  表结构为:

CREATE TABLE IF NOT EXISTS 'log' ("  
"[rowid] integer PRIMARY KEY ASC AUTOINCREMENT DEFAULT 1,"  
"[dateTimeString] char(30),"//日期字符串  
"[logLevelString] char(30),"//日志等级字符串  
"[appName] varchar(50),"//消息动向类型:接受或者发送  
"[logContent] text,"//日志内容,AES128加密字符串  
"[logLevel] integer,"//日志级别  
"[timestamp] double"//时间戳,用于倒序查询  
")"  

  此外,为了避免日志内容被不法用户导出获取秘钥及服务器接口地址,对每一条日志内容做AES128加密保存

二、日志查看体验

  1、第一次构思,确定搜索是必不可少的。例如,以前碰到一个”登录奖励”提示框偶尔不弹出的问题。该功能根据接收到的消息内容,弹框展示。离线时走APNS通道,在线时用实时消息承载。偏偏debug模式下的实时消息承载100%正常,发布模式下的APNS偶尔异常,但又不是每次必现,这种情况只能打印日志进文件,记录接收到的消息内容。到这里,就出现了体验痛点。从一堆log里,挑出某“关键字”的消息内容,如大海捞针,只能copy到电脑的文本编辑器中全文搜索。
  2、第二次构思,需要将搜索到的关键字高亮。在第一次构思方案下,搜索的体验仍然不尽如意,在UITextView大篇幅的文本中搜索,滚屏频繁根本不知道当前匹配到的关键字在哪个位置。因此,高亮可有效解决此问题,将UITextView中的文本全部改为NSAttributedString。
  3、第三次构思,关键字匹配忽略大小写。手机上查看日志,区分大小写的输入十分繁琐,且查看日志一般没有将关键字精确到大小写的场景,因此忽略大小写的搜索,可以减少输入代价。
  4、第四次构思,搜索完毕,停留并提示一次“已经到末尾”,然后将搜索起始位置设置到开头。在不知情的场景下,搜索下一个匹配对象,是无法获知前方有多少匹配数的,因此在没有可匹配对象后,提示,并在末尾停留一次,可以很友好的展示搜索结果。

三、日志文件管理

  1、首先,日志文件数量和体积必须要做限制。规则如下:日志文件以日期命名,内容以天为单位存储在一个文件里;文件数大于某个指定值,比如10,删除当前日志文件数一半的较旧日志文件;单个日志文件大小不作限制;每次写日志前,确保写入时的日期的对应文件存在。
  2、其次,日志查看页面支持在app内直接切换日志文件,并动态加载。使用之前写好的,给社交分享使用的IconSheet控件,方便展示文件图标和文件名。如下图:
  日志文件选取
  3、最后,支持日志内容热加载。每次点击一个日志文件,首先关闭文件选取器,主界面开启进度指示,启动子线程,并在子线程池中将数据库读取任务放入FMDatabaseQueue,读取完成解密日志内容,还原为一个完整的NSAttributedString对象,在主线程中把得到的数据填入UITextView,同时关闭进度指示器,提示“新的内容已加载完成”(日志文件一多,解密耗费时间越多)。

至此,一个自行设计的日志查看器,开发完毕。

关于我

雷一鸣,英文名Jerry。1990年9月生人,2007年考入南京工程学院,就读计算机应用专业。

※2011年毕业后加入南京新迪杰软件科技有限公司,完成冬冬出行帮手系列应用iPad、iPhone版本(现已下架)。
※2013年5月加入深圳四方精创股份有限公司,悲情的是刚刚成立的移动开发项目组全年没有接到一单像样的项目。
※2014年3月进入深圳有信网络技术有限公司,主要负责iOS客户端的功能开发,产品为手机网络电话“有信”。
※2015年3月进入深圳美丽范科技有限公司参与创业,主要负责iOS客户端功能开发、产品建议和体验优化,依靠更多的自主性挖掘产品亮点。

正努力朝着新的方向前进——学习技术、分享技术、运用技术,做出用户喜爱、生活中不可获缺的产品,真正为用户带来价值。