一间杂货铺~

03月 21

WKWebView长按图片识别二维码

引子

在默认情况下,长按WKWebView中的图片,会弹出image save sheet

image save sheet

关于这个image save sheet,在官方文档中可以得到印证:

Safari Web Content Guide -> Handling Events -> One-Finger Events章节中有这样一句话:However, if the user touches and holds an image, the image save sheet appears instead of an information bubble.

此处使用的是WKWebView,但为什么要提到Safari相关的内容?原因很简单,iOS中的Safari应用、SFSafariViewControllerWKWebView,底层使用的是相同的东西。

Handling Events这一章中,有提及一些具体的事件、如何阻止事件的默认行为,以及支持的事件类型。

参考链接:Safari Web Content Guide 之 Handling Events

思路

为了实现长按图片识别二维码这样的功能,就必须将系统默认的image save sheet屏蔽掉。

思路如下:

  1. 在网页DOM加载完成后,注入JS脚本
  2. JS脚本中,找到所有的image元素,并为它们添加touch相关的事件
  3. 长按后阻止其默认行为,获取image元素的src,将结果传递给native进行处理:下载图片,并识别,判断其是否包含二维码
  4. 如果包含二维码,则弹出相关的提示

注意事项

上面说的是touch相关的事件,了解前端的开发者应该会知道mouse相关的事件,这两者在手机端上的表现,还是有区别的:当使用mouse相关的事件时,如果手指一直按在屏幕上时,会导致无法触发函数的执行,只有松开手指后才会触发,而touch相关的事件,则不会出现这样的情况。

实现

实现预期的效果,会用到jQuery、处理JS回调的WKScriptMessageHandler

约定ScriptMessageHandler名称

这里约定其名称为webImgLongPressHandler

private let scriptMessageHandlerName: String = "webImgLongPressHandler"

    override func viewDidLoad() {
        super.viewDidLoad()

        let userContentController = WKUserContentController()
        userContentController.add(self, name: scriptMessageHandlerName)
    }

编写JS脚本

这里使用到了jQuery

$(document).ready(
    function() {
        var imageElements = document.images;
        for(var i = 0; i < imageElements.length; i++) {
            var imageElement = imageElements[i];
            var intervalID = 0;
            imageElement.ontouchstart = function(e) {
                // 阻止默认行为
                e.preventDefault();
                // 长按时间设置为1秒
                intervalID = window.setInterval(
                    function() {
                        window.clearInterval(intervalID);
                        // 将消息传递给native进行处理,这里使用的就是上面约定好的名称:webImgLongPressHandler
                        window.webkit.messageHandlers.webImgLongPressHandler.postMessage(e.target.src);
                    },
                    1000
                );
            };
            imageElement.ontouchend = function(e) {
                window.clearInterval(intervalID);
            };
            imageElement.ontouchcancel = function(e) {
                window.clearInterval(intervalID);
            }
        };
    }
);

将上面的脚本存放到名为image-element-long-press.js的文件中。

将脚本注入到WKWebView

let jqPath = Bundle.main.path(forResource: "jquery-3.1.1.min", ofType: "js")
let jsPath = Bundle.main.path(forResource: "image-element-long-press", ofType: "js")
let jqContent = try! String.init(contentsOfFile: jqPath!)
let jsContent = try! String.init(contentsOfFile: jsPath!)
let injectionContent = String.init(format: "%@\n%@", jqContent, jsContent)

let userScript = WKUserScript.init(source: injectionContent, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
userContentController.addUserScript(userScript)

let config = WKWebViewConfiguration.init()
config.userContentController = userContentController

let webView = WKWebView.init(frame: self.view.bounds, configuration: config)

处理识别

当长按图片时,就会发webImgLongPressHandler消息,native收到后进行处理:

// MARK: - WKScriptMessageHandler
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name ==  self.scriptMessageHandlerName {
        if let urlString = message.body as? String {
            if let url = URL.init(string: urlString) {
                let sessionConfig = URLSessionConfiguration.default
                let session = URLSession.init(configuration: sessionConfig)

                let dataTask = session.dataTask(with: url, completionHandler: { (data, response, error) in
                    if data != nil {
                        let img = UIImage.init(data: data!)
                        let codes = self.QRcodesInImage(img)
                        if codes != nil && codes!.count > 0 {
                            print("QRcodes: \(self.QRcodesInImage(img))")

                            DispatchQueue.main.async {
                                // 弹出提示
                                let sheet = UIAlertController.init(title: nil, message: nil, preferredStyle: .actionSheet)
                                sheet.addAction(UIAlertAction.init(title: "识别二维码", style: .default, handler: { (action) in
                                    var urls = [URL]()
                                    for code in codes! {
                                        if let url = URL.init(string: code) {
                                            if UIApplication.shared.canOpenURL(url) {
                                                urls.append(url)
                                            }
                                        }
                                    }
                                    let urlSheet = UIAlertController.init(title: nil, message: nil, preferredStyle: .actionSheet)
                                    for url in urls {
                                        let title = String.init(format: "打开%@", url.absoluteString)
                                        urlSheet.addAction(UIAlertAction.init(title: title, style: .default, handler: { (action) in
                                            UIApplication.shared.open(url, options: [:], completionHandler: nil)
                                        }))
                                    }
                                    urlSheet.addAction(UIAlertAction.init(title: "取消", style: .cancel, handler: { (action) in

                                    }))
                                    self.present(urlSheet, animated: true, completion: nil)
                                }))
                                sheet.addAction(UIAlertAction.init(title: "取消", style: .cancel, handler: { (action) in

                                }))
                                self.present(sheet, animated: true, completion: nil)
                            }
                        }
                    }
                })
                dataTask.resume()
            }
        }
    }
}

此处使用ZBarSDK进行二维码的识别(使用系统提供的API时,对于一张图片上有多个二维码的情况,虽然CIDetectorfeaturesInImage方法返回的是数组,但在测试时,调整了参数,返回的数组一直只包含一个元素):

private func QRcodesInImage(_ image: UIImage?) -> Array<String>? {
    if image == nil {
        return nil
    }
    let scanner = ZBarImageScanner()
    let barImg = ZBarImage.init(cgImage: image!.cgImage)
    let count = scanner.scanImage(barImg)
    if count == 0 {
        return nil
    }
    var codes = [String]()
    let symbolSet = scanner.results
    var symbol: OpaquePointer? = nil
    for i in 0 ..< symbolSet!.count {
        if i == 0 {
            symbol = zbar_symbol_set_first_symbol(symbolSet!.zbarSymbolSet)
        } else if symbol != nil {
            symbol = zbar_symbol_next(symbol)
        }
        if symbol != nil {
            let data = zbar_symbol_get_data(symbol)
            if let code = String.init(utf8String: data!) {
                codes.append(code)
            }
        }
    }
    return codes.count > 0 ? codes : nil
}

-webkit-touch-callout

在阻止默认行为时,上面的示例中使用的是preventDefault()函数。也有另外一个选择,就是使用-webkit-touch-callout

参见苹果官方文档:

Safari CSS Reference -> Supported CSS Properties -> User Interface

链接:Safari CSS Reference: -webkit-touch-callout

其它参考:

语法可参考:

也就是:

document.getElementById("id").style.property="值"

backgroundColorbackground-color是对应的,类似地,应是webkitTouchCallout-webkit-touch-callout相对应。

使用时,将其设置为none

imageElement.style.webkitTouchCallout = "none";

需要注意的是,preventDefault()-webkit-touch-callout,在效果上是有区别的。

二者都可以阻止默认的save image sheet

不同点如下:

  • 图片上产生的事件使用preventDefault()后,就无法选择该图片元素,无法通过双击该图片缩放网页

  • 将图片的样式中的-webkit-touch-callout设置为none,可以选择该图片元素,可以通过双击该图片缩放网页

个人觉得,在这种需求下,长按后界面中出现复制Menu是不太好看的,因此建议使用preventDefault()

附上代码链接:
https://github.com/Daniate/WKWebViewQRcode

03月 18

读取jar包内的资源文件

Java项目被打为可执行的jar包后,执行jar包时,如果读取相关的资源文件失败,可以按以下示例对源代码进行修改(既可以在IDE中运行项目时正确读取,也可以在执行jar包时正确读取):

Properties prop = new Properties();
String resourceName = "com/daniate/resource/configInPkg.properties";
//String resourceName = "configOutsidePkg.properties";
{
    // 最前面无斜杠
    InputStream input = this.getClass().getClassLoader().getResourceAsStream(resourceName);
    prop.load(input);
}
{
    // 最前面无斜杠
    InputStream input = this.getClass().getClassLoader().getResource(resourceName).openStream();
    prop.load(input);
}
{
    // 最前面有斜杠
    InputStream input = this.getClass().getResourceAsStream("/" + resourceName);
    prop.load(input);
}
{
    // 最前面有斜杠
    InputStream input = this.getClass().getResource("/" + resourceName).openStream();
    prop.load(input);
}

上面的四种写法任选其一。

如果使用了getClassLoader(),在调用getResourceAsStreamgetResource时,传入的参数就不能以/开始;反之,必须以/开始。

关于读取失败,可能是以下写法引起的(虽然在IDE中运行项目时,这些写法是可以读取到资源文件的):

{
    /**
     * 打成jar包后运行: Caused by:
     * java.io.FileNotFoundException:
     * file:\E:\workspace-netbeans\XX\dist\XX.jar!\configOutsidePkg.properties
     * (文件名、目录名或卷标语法不正确。)
     */
    URL url = this.getClass().getResource("/" + resourceName);
    Reader reader = new FileReader(url.getPath());
    prop.load(reader);
}
{
    /**
     * 打成jar包后运行: Caused by:
     * java.lang.IllegalArgumentException: URI is not hierarchical
     */
    URL url = this.getClass().getResource("/" + resourceName);
    Reader reader = new FileReader(new File(url.toURI()));
    prop.load(reader);
}
{
    /**
     * 打成jar包后运行:Caused by:
     * java.io.FileNotFoundException: src\config.properties
     * (系统找不到指定的路径。)
     */
    Reader reader = new FileReader(new File("src/" + resourceName));
    prop.load(reader);
}
03月 12

NetBeans相关问题

标签文字过小

更换了4K显示器后,NetBeans各个标签上的文字实在是太小了,即便是调整了控制面板->显示->设置自定义文本大小(DPI),设置成了200%,也还是小得看着不舒服。

搜索了一下,找到了解决办法,记录之:

修改NetBeans安装目录下的etc\netbeans.conf,在netbeans_default_options对应的值中追加--fontsize 24(前面需要使用空格隔开),然后重启NetBeans

netbeans_default_options="-J-client -J-Xss2m -J-Xms32m -J-Dapple.laf.useScreenMenuBar=true -J-Dapple.awt.graphics.UseQuartz=true -J-Dsun.java2d.noddraw=true -J-Dsun.java2d.dpiaware=true -J-Dsun.zip.disableMemoryMapping=true --fontsize 24"

参见:

在GlassFish中运行项目时,提示access denied

具体提示如下:

access denied ("java.net.SocketPermission" "localhost:1527" "listen,resolve")
java.security.AccessControlException: access denied ("java.net.SocketPermission" "localhost:1527" "listen,resolve")

解决方法:

找到jre目录,比如/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre,进入该目录下的lib/security目录,找到java.policy并编辑(如果存在权限问题,就使用sudo),在grant {...}块中追加下面的内容:

permission java.net.SocketPermission "localhost:1527", "listen";

编辑后,内容如下:

grant {
        // Allows any thread to stop itself using the java.lang.Thread.stop()
        // method that takes no argument.
        // Note that this permission is granted by default only to remain
        // backwards compatible.
        // It is strongly recommended that you either remove this permission
        // from this policy file or further restrict it to code sources
        // that you specify, because Thread.stop() is potentially unsafe.
        // See the API specification of java.lang.Thread.stop() for more
        // information.
        permission java.lang.RuntimePermission "stopThread";

        // allows anyone to listen on dynamic ports
        permission java.net.SocketPermission "localhost:0", "listen";

        // "standard" properies that can be read by anyone

        permission java.util.PropertyPermission "java.version", "read";
        permission java.util.PropertyPermission "java.vendor", "read";
        permission java.util.PropertyPermission "java.vendor.url", "read";
        permission java.util.PropertyPermission "java.class.version", "read";
        permission java.util.PropertyPermission "os.name", "read";
        permission java.util.PropertyPermission "os.version", "read";
        permission java.util.PropertyPermission "os.arch", "read";
        permission java.util.PropertyPermission "file.separator", "read";
        permission java.util.PropertyPermission "path.separator", "read";
        permission java.util.PropertyPermission "line.separator", "read";

        permission java.util.PropertyPermission "java.specification.version", "read";
        permission java.util.PropertyPermission "java.specification.vendor", "read";
        permission java.util.PropertyPermission "java.specification.name", "read";

        permission java.util.PropertyPermission "java.vm.specification.version", "read";
        permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
        permission java.util.PropertyPermission "java.vm.specification.name", "read";
        permission java.util.PropertyPermission "java.vm.version", "read";
        permission java.util.PropertyPermission "java.vm.vendor", "read";
        permission java.util.PropertyPermission "java.vm.name", "read";

        permission java.net.SocketPermission "localhost:1527", "listen";
};

参见:

正在启动 Tomcat 进程...正在等待 Tomcat...

现象:运行某个项目时,已输出org.apache.catalina.startup.Catalina.start Server startup in xxx ms,但也提示

Deploying on Apache Tomcat 8.0.33
    profile mode: false
    debug mode: false
    force redeploy: true
正在启动 Tomcat 进程...
正在等待 Tomcat...
启动 Tomcat 失败。

如果通过运行Tomcat脚本,可以启动,但又出现了上面的现象,很可能就是NetBeans中设置了代理:

代理设置

将其设置为无代理就可以了。

03月 12

一处JSON处理上的小细节

直接说JSON,感觉语义不太明确,最好还是说成JSON对象JSON字符串

假设前端通过ajax回调函数接收数据,参数名为data

$.post(
        "/xxx/yyy.do",
        $("#zzz-form").serialize(),
        function (data) {
            // to do something
        }
);

JSON对象

Content-Type被设置为application/json时:

resp.setContentType("application/json; charset=UTF-8");
try (PrintWriter out = resp.getWriter()) {
    out.print("{\"status\": " + result.getStatus() + ", \"reason\": \"" + result.getReason() + "\"}");
    out.flush();
}

前端收到的就是JSON对象,可以直接获取里面的属性:

function (data) {
    var reason = data.reason;
}

JSON字符串

Content-Type不是application/json时,比如是text/plain

resp.setContentType("text/plain; charset=UTF-8");
try (PrintWriter out = resp.getWriter()) {
    out.print("{\"status\": " + result.getStatus() + ", \"reason\": \"" + result.getReason() + "\"}");
    out.flush();
}

前端收到的就是JSON字符串,此时需要将其解析为JSON对象

function (data) {
    var json = JSON.parse(data);
    var reason = json.reason;
}
03月 01

NSLayoutAnchor

引子

苹果发布iOS 6.0时,为开发者提供了视图布局的利器——Auto Layout,用于替代Frame-Based Layout,以轻松方便地达到不同尺寸屏幕上界面的兼容适配。

Auto Layout对应着一套constraint-based layout system(基于约束的布局系统),这套系统所使用的策略如下:

item1.attribute1 = multiplier × item2.attribute2 + constant

上面出现的=,表示的是等于,而非赋值。另外,其中的=,也可以是>=<=

对应地,有一个名为NSLayoutConstraint的类,用于构建相关的约束,其用于构建约束的方法如下:

  1. + constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:
  2. + constraintsWithVisualFormat:options:metrics:views:

一眼看上去,就能发现,第一个方法正好与上面提到的策略是一致的。

第二个方法,使用了所谓的Visual Format Language,比起第一个方法,复杂难用程度直接上升了一个台阶。比如V:[topField]-10-[bottomField],其语法十分复杂,而且,改动视图对应的变量名称,也得修改这里面的内容,十分难用,因此,大多数开发者会选择使用第一种方法。

但很明显,如果视图中有很多约束,第一种方法用得多了,代码也是又臭又长,就显得不那么简洁明晰,因此也深受诟病,这就促使聪明的开发者创造出一些了不起的第三方类库,SnapKit出品的MasonrySnapKit就深受开发者的喜爱,已形成垄断,其它自动布局相关的第三方类库都在夹缝中生存。

NSLayoutAnchor简述

苹果应该是了解到了相应的弊端,在发布iOS 9.0时,又提供了NSLayoutAnchor这样一个用于创建NSLayoutConstraint的工厂类。

使用NSLayoutAnchor API能让代码更简洁明晰,也更加易读,另外也提供了额外的类型检查,可以避免创建非法无效的约束。例如:

// Creating constraints using NSLayoutConstraint
NSLayoutConstraint(item: subview,
                   attribute: .Leading,
                   relatedBy: .Equal,
                   toItem: view,
                   attribute: .LeadingMargin,
                   multiplier: 1.0,
                   constant: 0.0).active = true

NSLayoutConstraint(item: subview,
                   attribute: .Trailing,
                   relatedBy: .Equal,
                   toItem: view,
                   attribute: .TrailingMargin,
                   multiplier: 1.0,
                   constant: 0.0).active = true

// Creating the same constraints using Layout Anchors
let margins = view.layoutMarginsGuide
subview.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true
subview.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor).active = true

很明显,使用NSLayoutAnchor API确实很大程度地减少了代码量,也更加明晰。

注意:
尽管提供了类型检查,但还是有可能创建出非法无效的约束。比如,由于leadingAnchorleftAnchor都是NSLayoutXAxisAnchor,因此编译器允许在它们之间形成约束,但Auto Layout并不允许出现这样的混淆,因此,在运行时会出现崩溃。

在使用NSLayoutAnchor时,并不是直接使用这个类,而是使用其具体的子类:

  • NSLayoutXAxisAnchor用于创建水平约束
  • NSLayoutYAxisAnchor用于创建竖直约束
  • NSLayoutDimension用于创建与视图宽高相关的约束

UIKit/UIView.h中,可以看到以下内容,很明显,都是具体的子类:

@class NSLayoutXAxisAnchor,NSLayoutYAxisAnchor,NSLayoutDimension;
@interface UIView (UIViewLayoutConstraintCreation)
/* Constraint creation conveniences. See NSLayoutAnchor.h for details.
 */
@property(readonly, strong) NSLayoutXAxisAnchor *leadingAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutXAxisAnchor *trailingAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutXAxisAnchor *leftAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutXAxisAnchor *rightAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutYAxisAnchor *topAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutYAxisAnchor *bottomAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutDimension *widthAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutDimension *heightAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutXAxisAnchor *centerXAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutYAxisAnchor *centerYAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutYAxisAnchor *firstBaselineAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly, strong) NSLayoutYAxisAnchor *lastBaselineAnchor NS_AVAILABLE_IOS(9_0);

@end

在低版本系统中使用NSLayoutAnchor这样的API

我仿照NSLayoutAnchor API写了DGLayoutAnchor,可以在iOS 6.0及更高版本中使用,其使用方式与NSLayoutAnchor完全一模一样。

不过,得使用下面的这些锚点:

@interface UIView (DGLayoutAnchor)
@property (readonly, strong) DGLayoutXAxisAnchor *dg_leadingAnchor;
@property (readonly, strong) DGLayoutXAxisAnchor *dg_trailingAnchor;
@property (readonly, strong) DGLayoutXAxisAnchor *dg_leftAnchor;
@property (readonly, strong) DGLayoutXAxisAnchor *dg_rightAnchor;
@property (readonly, strong) DGLayoutYAxisAnchor *dg_topAnchor;
@property (readonly, strong) DGLayoutYAxisAnchor *dg_bottomAnchor;
@property (readonly, strong) DGLayoutDimension   *dg_widthAnchor;
@property (readonly, strong) DGLayoutDimension   *dg_heightAnchor;
@property (readonly, strong) DGLayoutXAxisAnchor *dg_centerXAnchor;
@property (readonly, strong) DGLayoutYAxisAnchor *dg_centerYAnchor;
@property (readonly, strong) DGLayoutYAxisAnchor *dg_firstBaselineAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutYAxisAnchor *dg_lastBaselineAnchor;
@property (readonly, strong) DGLayoutYAxisAnchor *dg_baselineAnchor;// same as `dg_lastBaselineAnchor`
@property (readonly, strong) DGLayoutXAxisAnchor *dg_leftMarginAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutXAxisAnchor *dg_rightMarginAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutYAxisAnchor *dg_topMarginAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutYAxisAnchor *dg_bottomMarginAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutXAxisAnchor *dg_leadingMarginAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutXAxisAnchor *dg_trailingMarginAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutXAxisAnchor *dg_centerXWithinMarginsAnchor NS_AVAILABLE_IOS(8_0);
@property (readonly, strong) DGLayoutYAxisAnchor *dg_centerYWithinMarginsAnchor NS_AVAILABLE_IOS(8_0);
@end

@interface UIViewController (DGLayoutAnchor)
@property (readonly, strong) DGLayoutGuideAnchor *dg_topLayoutGuideTopAnchor;
@property (readonly, strong) DGLayoutGuideAnchor *dg_topLayoutGuideBottomAnchor;
@property (readonly, strong) DGLayoutGuideAnchor *dg_bottomLayoutGuideTopAnchor;
@property (readonly, strong) DGLayoutGuideAnchor *dg_bottomLayoutGuideBottomAnchor;
@end

使用示例:

NSLayoutConstraint *lc1 = [v.dg_topAnchor equalTo:self.dg_topLayoutGuideBottomAnchor constant:10];
NSLayoutConstraint *lc2 = [v.dg_leadingAnchor equalTo:self.view.dg_leadingMarginAnchor];
NSLayoutConstraint *lc3 = [self.dg_bottomLayoutGuideTopAnchor equalTo:v.dg_bottomAnchor constant:10];
NSLayoutConstraint *lc4 = [v.dg_trailingAnchor equalTo:self.view.dg_trailingMarginAnchor];

总述

比起MasonrySnapKitNSLayoutAnchor API还是有些小巫见大巫,在这两个优秀的第三方类库面前,苹果所提供的创建约束的API完全不能在开发者中流行起来,它们完全处在MasonrySnapKit大厦的地基之中,早已被开发者所唾弃、抛弃。

02月 23

dispatch semaphore

简述

含义

dispatch_semaphore_t/DispatchSemaphore

表示计数信号量。

函数

Objective-C

  1. dispatch_semaphore_create
  2. dispatch_semaphore_wait
  3. dispatch_semaphore_signal

Swift

  1. public init(value: Int)
  2. public func wait()public func wait(timeout: DispatchTime) -> DispatchTimeoutResultpublic func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
  3. public func signal() -> Int

dispatch_semaphore_create

dispatch_semaphore_t dispatch_semaphore_create(long value)

创建计数信号量,value为其起始值。不能传入小于0的值。如果创建失败,则返回NULL

当两个线程需要协调特定事件的完成时,传入0
传入大于0的值,在管理有限资源池时是非常有用的,传入的值等于资源池的大小。

当不再需要信号量时,应当调用dispatch_release释放信号量(ARC下不能使用该函数)。

dispatch_semaphore_wait

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)

返回0,表示成功;否则,表示超时。

减小计数信号量。如果结果值小于0,这个函数在返回之前,会一直等待信号的发生。

dispatch_semaphore_signal

long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

增大计数信号量。如果之前的值小于0,该函数会唤醒一个正在使用dispatch_semaphore_wait函数等待的线程。

适用场景

在上面的dispatch_semaphore_create解释中,已经提到过。

  1. 对线程的运行进行协调
  2. 管理有限资源池

实践

协调线程

也就是控制两个线程中特定事件的完成顺序。应用场景,比如经典的『生产-消费模型』。

在调用dispatch_semaphore_create函数时,必须传入0

以下面的示例为基础:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        Thread.detachNewThreadSelector(#selector(_thread1(arg:)), toTarget: self, with: nil)
        Thread.detachNewThreadSelector(#selector(_thread2(arg:)), toTarget: self, with: nil)
    }

    @objc private func _thread1(arg: Any?) {
        for i in 0..<5 {
            print(#function + " \(i)")
        }
    }

    @objc private func _thread2(arg: Any?) {
        for i in 0..<5 {
            print(#function + " \(i)")
        }
    }

}

运行上面的代码,结果可能如下:

_thread2(arg:) 0
_thread1(arg:) 0
_thread2(arg:) 1
_thread1(arg:) 1
_thread2(arg:) 2
_thread1(arg:) 2
_thread2(arg:) 3
_thread1(arg:) 3
_thread2(arg:) 4
_thread1(arg:) 4

很明显,两个任务是交错运行的。

为了先执行完_thread2中的任务,再执行_thread1中的任务,使用信号量,就可以这样写:

class ViewController: UIViewController {

    let sema: DispatchSemaphore = DispatchSemaphore(value: 0)

    override func viewDidLoad() {
        super.viewDidLoad()

        Thread.detachNewThreadSelector(#selector(_thread1(arg:)), toTarget: self, with: nil)
        Thread.detachNewThreadSelector(#selector(_thread2(arg:)), toTarget: self, with: nil)
    }

    @objc private func _thread1(arg: Any?) {
        self.sema.wait()
        for i in 0..<5 {
            print(#function + " \(i)")
        }
    }

    @objc private func _thread2(arg: Any?) {
        for i in 0..<5 {
            print(#function + " \(i)")
        }
        self.sema.signal()
    }

}

运行结果就会是:

_thread2(arg:) 0
_thread2(arg:) 1
_thread2(arg:) 2
_thread2(arg:) 3
_thread2(arg:) 4
_thread1(arg:) 0
_thread1(arg:) 1
_thread1(arg:) 2
_thread1(arg:) 3
_thread1(arg:) 4

管理资源池

也就是协调有限资源的使用。

在创建信号量时,传入的值,要与可用的资源的数量一致。

拿园区小轿车停车为例,停车位属于有限的资源。

当停车位被占用后,其它小轿车是不能再占用该停车位的。

当园区中没有可用的停车位时,其它小轿车就得等待停车位上的小轿车离开后,才能去占用那个停车位。

class Car: NSObject {
    let identifier: String

    init(identifier: String) {
        self.identifier = identifier
    }

    override var description: String {
        get {
            return "{小车[\(self.identifier)]}"
        }
    }
}
/**
 停车位
 */
class ParkingSpace: NSObject {
    let identifier: String
    weak var car: Car? = nil

    var available: Bool {
        get {
            return self.car == nil
        }
    }

    init(identifier: String) {
        self.identifier = identifier
    }

    override var description: String {
        get {
            return "{停车位[\(self.identifier)]}"
        }
    }
}
class ViewController: UIViewController {

    let cars: [Car] = [
        Car.init(identifier: "京A 11111"),
        Car.init(identifier: "京B 22222"),
        Car.init(identifier: "京C 33333"),
        Car.init(identifier: "京D 44444"),
        Car.init(identifier: "京E 55555"),
    ]
    let parkingSpaces: [ParkingSpace] = [
        ParkingSpace.init(identifier: "001"),
        ParkingSpace.init(identifier: "002"),
        ParkingSpace.init(identifier: "003"),
    ]
    let parkingSpaceSemaphore: DispatchSemaphore
    let parkingSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        self.parkingSpaceSemaphore = DispatchSemaphore(value: self.parkingSpaces.count)

        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    required init?(coder aDecoder: NSCoder) {
        self.parkingSpaceSemaphore = DispatchSemaphore(value: self.parkingSpaces.count)

        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        for car in self.cars {
            Thread.detachNewThreadSelector(#selector(_carParkingThread(arg:)), toTarget: self, with: car)
        }
    }

    @objc private func _carParkingThread(arg: Any?) {
        if let car = arg as? Car {
            print("\(car)要来停车啦")
            self.parkingSpaceSemaphore.wait()
            // 由于同时可能存在多个可用的停车位,如果不加锁,查找到的停车位可能是同一个,两辆车不能同时占用同一个停车位
            self.parkingSemaphore.wait()
            var availablePSs: [ParkingSpace] = []
            for ps in self.parkingSpaces {
                if ps.available {
                    availablePSs.append(ps)
                }
            }
            // 由于是通过parkingSpaceSemaphore来通知有可用的停车位的,因此availablePSs中总会有元素,并不需要检查availablePSs.count是否大于0
            let ps = availablePSs[Int(arc4random_uniform(UInt32(availablePSs.count)))]
            ps.car = car
            print("\(car)占用了\(ps)")
            self.parkingSemaphore.signal()
            let ti = 1 + arc4random_uniform(5)
            sleep(ti)
            ps.car = nil
            // 模拟为分钟
            print("\(ti)分钟后,\(car)离开了\(ps)")
            self.parkingSpaceSemaphore.signal()
        }
    }

}

运行结果可能如下:

{小车[京B 22222]}要来停车啦
{小车[京A 11111]}要来停车啦
{小车[京D 44444]}要来停车啦
{小车[京C 33333]}要来停车啦
{小车[京E 55555]}要来停车啦
{小车[京B 22222]}占用了{停车位[003]}
{小车[京A 11111]}占用了{停车位[002]}
{小车[京D 44444]}占用了{停车位[001]}
1分钟后,{小车[京D 44444]}离开了{停车位[001]}
{小车[京C 33333]}占用了{停车位[001]}
4分钟后,{小车[京A 11111]}离开了{停车位[002]}
{小车[京E 55555]}占用了{停车位[002]}
5分钟后,{小车[京B 22222]}离开了{停车位[003]}
5分钟后,{小车[京C 33333]}离开了{停车位[001]}
3分钟后,{小车[京E 55555]}离开了{停车位[002]}
01月 02

CentOS 7简易安装、配置、汉化GitLab社区版

因为是简易安装,不是使用源码进行安装,因此会使用 GitLab已集成的几个软件,比如,NginxPostgreSQLRedis

安装

方式一(yum安装)

http://mirrors.tuna.tsinghua.edu.cn/help/gitlab-ce/这里,有详细的说明,不再赘述。

方式二(rpm安装)

可在下面的链接中找到相应版本的rpm安装包:

https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/

比如,要下载安装gitlab-ce-8.15.1-ce.0.el7.x86_64.rpm,可以使用如下命令:

curl -JO https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-8.15.1-ce.0.el7.x86_64.rpm

下载完成后,为这个rpm安装包添加可执行权限

chmod +x gitlab-ce-8.15.1-ce.0.el7.x86_64.rpm

然后安装:

sudo rpm -i gitlab-ce-8.15.1-ce.0.el7.x86_64.rpm

安装结束后,可通过gitlab-ctl status查看其状态。

配置

安装成功后,可能需要进行一些配置,需要配置的文件就是/etc/gitlab/gitlab.rb

主要是修改访问时所使用的url及端口号:

修改url

external_url修改为对应的域名,比如:

external_url 'http://gitlab.daniate.com'

修改端口号

GitLab默认使用的是8080端口,如果由于其它原因(被其它软件所占用了),就需要修改端口号,比如,将端口号修改为8091:

unicorn['port'] = 8091

另外,在使用浏览器访问网站时,如未指明端口号,则会使用80端口,如果由于其它原因(安装了Apache80端口已经被占用),就需要修改了nginx['listen_port']所对应的值,在使用浏览器访问GitLab时,就需要指定这个端口,比如:

nginx['listen_port'] = 8090

修改完成后,执行sudo gitlab-ctl reconfigure,让配置生效。

然后,通过http://gitlab.daniate.com:8090就可以访问服务器上的GitLab了,当然,这个url是我服务器上的,你得根据自己的具体情况进行配置。

汉化

https://git.oschina.net/lalala34/gitlab.git克隆到服务器上(前提是服务器上已经安装了git,如未安装,请查看其它资料进行安装;另外也可以使用变通的方式,比如,先克隆到本地,生成差异文件,然后上传到服务器):

git clone https://git.oschina.net/lalala34/gitlab.git

切换到克隆的目录下,执行git branch -a查看所有的分支,可以列出以下分支:

* master
  remotes/origin/8-10-stable
  remotes/origin/8-10-zh
  remotes/origin/8-11-stable
  remotes/origin/8-11-stable-zh
  remotes/origin/8-12-stable
  remotes/origin/8-12-stable-zh
  remotes/origin/8-13-stable
  remotes/origin/8-13-stable-zh
  remotes/origin/8-14-stable
  remotes/origin/8-14-stable-zh
  remotes/origin/8-15-stable
  remotes/origin/8-15-stable-zh
  remotes/origin/8-8-zh
  remotes/origin/8-9-stable
  remotes/origin/8-9-zh
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

查看当前安装的GitLab的版本:

cat /opt/gitlab/embedded/service/gitlab-rails/VERSION

假如使用的就是8.15.1这个版本,就使用8-15-stable-zh这个分支进行汉化。汉化需要生成差异文件:

git diff origin/8-15-stable origin/8-15-stable-zh > ~/gitlab-8-15-zh.diff

先将GitLab停止:

sudo gitlab-ctl stop

随后切换到/opt/gitlab/embedded/service/gitlab-rails目录,应用上面已生成的差异文件:

git apply ~/gitlab-8-15-zh.diff

最后,重启GitLab

sudo gitlab-ctl start

再次访问,就可以看到汉化之后的GitLab了。

11月 27

通过xcodebuild自动构建并发布Ad Hoc测试包

通过xcodebuild自动构建并发布Ad Hoc测试包

目标是通过一个shell脚本,完成构建及发布。

Ad Hoc发布证书及Provisioning Profile

在『钥匙串访问』中执行:

证书助理->从证书颁发机构请求证书:

证书助理->从证书颁发机构请求证书

填写证书信息:

此处,为了方便识别证书,其常用名称,被设置为了"Ad Hoc"。

填写证书信息

点击『继续』按钮:

会弹出certSigningRequest文件的保存路径。

生成certSigningRequest文件后,『钥匙串访问』中会多出名为"Ad Hoc"的专用密钥名为"Ad Hoc"的公用密钥,然后登录苹果开发者账号,进入『Certificates, Identifiers & Profiles』,在『Certificates』中添加证书,在第一步中会让用户选择需要创建哪种类型的证书,这里我们需要选择『Ad Hoc』。

  • 如果是个人开发者账号,界面会是这样:

个人开发者

  • 如果是企业开发者账号,界面会是这样:

企业开发者

注意:一个是App Store and Ad Hoc,一个是In-House and Ad Hoc。也就是说,发布到App Store用的证书也可用于Ad Hoc发布。我这里的不可用(显示为灰色),是因为之前已经创建了相关的证书。

后面会上传之前生成的certSigningRequest文件,然后生成证书,下载并双击证书,证书就会与『钥匙串访问』中之前多出的Ad Hoc专用密钥进行配对。

接着,需要生成Ad Hoc所用的Provisioning Profile

  • 如果是个人开发者账号,界面会是这样:

个人开发者

  • 如果是企业开发者账号,界面会是这样:

企业开发者

在随后的步骤中,会让选择App ID,对于Ad Hoc发布,可以使用明确的App ID,也可以使用包含通配符(*)的App ID。如果没有相应的App ID,可以在Identifiers->App IDs中进行添加:

Snip20161122_13.png

选择App ID后,会让选择certificates,勾选在之前生成的证书,然后就可以生成Provisioning Profile文件,下载并双击,系统会自动将这个文件复制到~/Library/MobileDevice/Provisioning Profiles目录,并为其重新命名,新名称会类似这样:86ceff27-1dff-40eb-8fd7-6072af8cb03b.mobileprovision,这样的名称会在xcodebuild命令中使用到,但不会使用到.mobileprovision扩展名。

使用xcodebuild构建项目

xcodebuild简介

用途

xcodebuild最主要的用途就是『构建Xcode项目及工作空间』,也有其它用途:

  • 列举信息(项目配置、SDK、xcodebuild版本)
  • 导出存档(从xcarchive存档导出ipamanifest.plist
  • 导入/导出本地化文件(localization

xcodebuild命令的详情使用说明,请自行在命令行中执行man xcodebuildxcodebuild -help进行查看。这里给出的用法示例,基本能够满足日常的使用。

项目构建

项目构建主要会使用xcodebuild的三种构建形式:

# 1
xcodebuild [-project name.xcodeproj]
    [[-target targetname] ... | -alltargets]
    [-configuration configurationname]
    [-sdk [sdkfullpath | sdkname]] [action ...]
    [buildsetting=value ...] [-userdefault=value ...]

# 2
xcodebuild [-project name.xcodeproj] -scheme schemename
    [[-destination destinationspecifier] ...]
    [-destination-timeout value]
    [-configuration configurationname]
    [-sdk [sdkfullpath | sdkname]] [action ...]
    [buildsetting=value ...] [-userdefault=value ...]

# 3
xcodebuild -workspace name.xcworkspace -scheme schemename
    [[-destination destinationspecifier] ...]
    [-destination-timeout value]
    [-configuration configurationname]
    [-sdk [sdkfullpath | sdkname]] [action ...]
    [buildsetting=value ...] [-userdefault=value ...]

如果没有使用workspace,也就是没有使用xcworkspace文件,就使用前两种形式。

如果使用了workspace,就使用第三种形式(比如,项目使用了CocoaPods)。

生成xcarchive存档

为了构建xcarchive,需要将action指定为archive,此时必须指定一个schemescheme名称可在Xcode菜单的Product->Scheme->Manage Schemes中查找到。

比如,你的项目名称为Ad-Hoc-Demo,Scheme为Ad-Hoc-Demo-Scheme且没有使用workspace,就可以使用如下命令,对项目进行archive

xcodebuild -project Ad-Hoc-Demo.xcodeproj -scheme Ad-Hoc-Demo-Scheme archive

但有可能出错。因为项目的配置可能是不正确的(比如证书、Provisioning ProfileBundle Identifier配置错误)。

有三种途径可以查看项目的配置,:

  1. 直接在Xcode中进行查看(最直观)
  2. XXX.xcodeproj/project.pbxproj文件中进行查看
  3. 通过xcodebuild-showBuildSettings选项进行查看(建议使用这种方式)。

建议通过-showBuildSettings选项进行查看,主要原因是之前遇到的一个坑,我在Xcode中已经将证书、Provisioning Profile正确地设置了(对于Xcode来说是正确的,对于xcodebuild来说就不一定是正确的),但在使用xcodebuild时还是报出错误:Provisioning profile "XXXDis" doesn't include signing certificate "iPhone Distribution: XXX.。之所以报出这样的错误,只是因为Xcode中的配置并没有与项目的XXX.xcodeproj/project.pbxproj文件中的配置完全保持一致,该文件中的PROVISIONING_PROFILE_SPECIFIERPROVISIONING_PROFILE所对应的Provisioning Profile文件并不是同一个。

为了让构建进行得更加顺利,需要指定一些[buildsetting=value ...],比如,指定前面提到的PROVISIONING_PROFILE_SPECIFIERPROVISIONING_PROFILE

PROVISIONING_PROFILE_SPECIFIER的值可以这样找到:Xcode->Preferences...->Accounts,然后选择一个Apple ID,再点击右下角的View Details,进入详情界面,其中的Provisioning Profiles那一列就是PROVISIONING_PROFILE_SPECIFIER

PROVISIONING_PROFILE_SPECIFIER

在这个界面中,右键点击某个Provisioning Profile,可以看到Show In Finder项,选择该项,可以直接进入~/Library/MobileDevice/Provisioning Profiles目录。

如果Provisioning Profile文件名为86ceff27-1dff-40eb-8fd7-6072af8cb03b.mobileprovision,就将PROVISIONING_PROFILE的值指定为"86ceff27-1dff-40eb-8fd7-6072af8cb03b"(不带后面的.mobileprovision):

xcodebuild -project Ad-Hoc-Demo.xcodeproj -scheme Ad-Hoc-Demo-Scheme PROVISIONING_PROFILE="86ceff27-1dff-40eb-8fd7-6072af8cb03b" PROVISIONING_PROFILE_SPECIFIER="Ad Hoc" archive

这样就可以构建出xcarchive存档,其默认的存放目录为~/Library/Developer/Xcode/Archives,我们可以修改存放目录,只需要指定-archivePath选项,并在其后面指定xcarchive存档存放路径(需要包含xcarchive存档的文件名)就可以了:

xcodebuild -project Ad-Hoc-Demo.xcodeproj -scheme Ad-Hoc-Demo-Scheme PROVISIONING_PROFILE="86ceff27-1dff-40eb-8fd7-6072af8cb03b" PROVISIONING_PROFILE_SPECIFIER="Ad Hoc" archive -archivePath ~/Ad-Hoc-Demo.xcarchive

自己指定xcarchive存档的存放路径,可以方便后续处理

actionarchive时,xcodebuild默认使用的Build ConfigurationRelease,不过我们可以根据实际情况进行设置(比如,使用生产环境还是测试环境,因为在默认情况下,Release配置中是没有设置DEBUG宏的,有些团队会根据这个宏,去连接不同的服务器),此时,需要使用到-configuration选项,可以指定ReleaseDebug

xcodebuild -project Ad-Hoc-Demo.xcodeproj -scheme Ad-Hoc-Demo-Scheme PROVISIONING_PROFILE="86ceff27-1dff-40eb-8fd7-6072af8cb03b" PROVISIONING_PROFILE_SPECIFIER="Ad Hoc" archive -archivePath ~/Ad-Hoc-Demo.xcarchive -configuration Debug

导出ipa

使用-exportArchive就可以从xcarchive存档导出ipamanifest.plist

不过需要指定其它3个选项,使用方式如下:

xcodebuild -exportArchive -archivePath xcarchivepath -exportPath
    destinationpath -exportOptionsPlist path
配置-exportOptionsPlist选项所需的plist文件

对于-exportOptionsPlist,需要创建一个plist文件,通过xcodebuild -help可以查看到该文件中可用的键及其描述,常用的有:

  • compileBitcode
  • manifest
  • method
  • teamID
  • uploadBitcode
  • uploadSymbols

其中,manifestmethodteamID是最重要的。

manifest对应的值是一个字典,这个字典需要包含appURLdisplayImageURLfullSizeImageURL这三个键。通过它,就可以生成一个名为manifest.plist文件,通过网页下载应用程序时,会使用这个manifest.plist文件。

method用于指明应该导出什么样的存档。

teamID可以在『钥匙串访问』中查看到:

teamID

导出ipamanifest.plist

创建并配置好-exportOptionsPlist选项所需的plist文件后,就可以通过xcodebuild -exportArchive导出ipamanifest.plist

生成下载页

在下载页面中,最重要的就是下载链接的href属性,该属性值的形式如下:

itms-services://?action=download-manifest&url=xxx/manifest.plist

其中的xxxmanifest.plist所在URL的某一部分,需要根据具体情况进行设置。

对于iOS 7.1及更高版本,manifest.plist所在URL必须使用https协议

将文件上传到服务器

只需要将下载页面、manifest.plistipa文件上传到服务器的某个目录下,并确保可以通过网站访问到该目录下的文件。

将以上步骤整合为脚本

需要将脚本保存在XXX.xcodeproj所在目录下(记得加执行权限)

脚本示例:

#!/bin/sh

#
#  Created by Daniate.
#

# 项目名称
PROJECT_NAME="xxx" # 请替换
# 工作空间名称
WS_NAME="xxx.xcworkspace" # 请替换
# Scheme
SCHEME_NAME="xxx" # 请替换
# 版本号
BUNDLE_VERSION=`grep -A1 'CFBundleShortVersionString' "./$PROJECT_NAME/Info.plist" | grep '<string>' | awk -F '<\/string>' '{print $1}' | awk -F '<string>' '{print $2}'`
# 当前日期时间
CURRENT_DATE_TIME=`date +%Y.%m.%d.%H.%M.%S`
# 由版本号及当前日期时间组成的目录名
EXPORT_FOLDER_NAME="v$BUNDLE_VERSION_$CURRENT_DATE_TIME"
# 文件导出目录
EXPORT_ROOT="./ad_hoc_production"
EXPORT_FULL_FOLDER_NAME="$EXPORT_ROOT/$PROJECT_NAME/$EXPORT_FOLDER_NAME"
# 归档路径
ARCHIVE_PATH="$EXPORT_FULL_FOLDER_NAME/$PROJECT_NAME.xcarchive"
# 域名或IP地址
DL_DOMAIN="https://xxx.cn" # 请替换
# 下载页面的名称
DL_HTML="dl.html"
# 下载页面所在的URL
DL_URL="$DL_DOMAIN/$PROJECT_NAME/$EXPORT_FOLDER_NAME/$DL_HTML"

# STEP 1. 清理以前构建所留下的文件
echo "正在执行 xcodebuild clean ..."
xcodebuild -workspace "$WS_NAME" -scheme "$SCHEME_NAME" clean  1>/dev/null
echo "xcodebuild clean 执行完毕"

# STEP 2. 构建归档
PROFILE="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # 请替换
PROFILE_SPECIFIER="xxx" # 请替换
TEAM_ID="xxxxxxxxxx" # 请替换
echo "正在执行 xcodebuild archive ..."
xcodebuild -workspace "$WS_NAME" -scheme "$SCHEME_NAME" -configuration Release PROVISIONING_PROFILE="$PROFILE" PROVISIONING_PROFILE_SPECIFIER="$PROFILE_SPECIFIER" archive -archivePath "$ARCHIVE_PATH"  1>/dev/null

if [ -d "$ARCHIVE_PATH" ] # 构建归档成功
then
    echo "执行 xcodebuild archive 成功"
else
    echo "执行 xcodebuild archive 失败"
    exit 1
fi

# STEP 3. 生成 exportOptionsPlist 选项所需的 plist 文件

EXPORT_OPTS_PLIST_NAME="export_opts.plist"
EXPORT_OPTS_PLIST_PATH="$EXPORT_FULL_FOLDER_NAME/$EXPORT_OPTS_PLIST_NAME"

echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
<dict>\n\
    <key>compileBitcode</key>\n\
    <false/>\n\
    <key>method</key>\n\
    <string>ad-hoc</string>\n\
    <key>teamID</key>\n\
    <string>$TEAM_ID</string>\n\
    <key>manifest</key>\n\
    <dict>\n\
        <key>appURL</key>\n\
        <string>$DL_DOMAIN/$PROJECT_NAME/$EXPORT_FOLDER_NAME/$SCHEME_NAME.ipa</string>\n\
        <key>displayImageURL</key>\n\
        <string>$DL_DOMAIN/display.png</string>\n\
        <key>fullSizeImageURL</key>\n\
        <string>$DL_DOMAIN/fullSize.png</string>\n\
    </dict>\n\
    <key>uploadBitcode</key>\n\
    <false/>\n\
    <key>uploadSymbols</key>\n\
    <false/>\n\
</dict>\n\
</plist>" > "$EXPORT_OPTS_PLIST_PATH"

if [ -s "$EXPORT_OPTS_PLIST_PATH" ]
then
    echo "成功生成 exportOptionsPlist 选项所需的 plist 文件"
else
    echo "生成 exportOptionsPlist 选项所需的 plist 文件失败"
    rm -rdf "$EXPORT_ROOT"
    exit 1
fi

# STEP 4. 导出 ipa、manifest.plist
echo "正在由归档导出ipa、manifest.plist文件 ..."
xcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" -exportPath "$EXPORT_FULL_FOLDER_NAME" -exportOptionsPlist "$EXPORT_OPTS_PLIST_PATH"  1>/dev/null

# 删除归档
rm -rdf "$ARCHIVE_PATH"
# 删除生成的export options plist
rm "$EXPORT_OPTS_PLIST_PATH"

if [ -s "$EXPORT_FULL_FOLDER_NAME/$SCHEME_NAME.ipa" -a -s "$EXPORT_FULL_FOLDER_NAME/manifest.plist" ]
then
    echo "成功导出 ipa、manifest.plist"
else
    echo "导出 ipa、manifest.plist 失败"
    rm -rdf "$EXPORT_ROOT"
    exit 1
fi

# STEP 5. 生成  ipa 下载页面
echo "正在生成 ipa 下载页面 ..."
echo "<!DOCTYPE html>\n\
<html>\n\
    <head>\n\
        <title>Ad-Hoc 内部测试</title>\n\
        <meta charset=\"UTF-8\">\n\
        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\
        <style>\n\
            p {\n\
                text-align: center;\n\
            }\n\
        </style>\n\
    </head>\n\
    <body>\n\
        <p>\n\
            <img src=\"../../Icon.png\">\n\
        </p>\n\
        <p>\n\
            $PROJECT_NAME\n\
        </p>\n\
        <p>\n\
            $EXPORT_FOLDER_NAME\n\
        </p>\n\
        <p>\n\
            <a href=\"itms-services://?action=download-manifest&url=$DL_DOMAIN/$PROJECT_NAME/$EXPORT_FOLDER_NAME/manifest.plist\">\n\
                点击安装\n\
            </a>\n\
        </p>\n\
    </body>\n\
</html>" > "$EXPORT_FULL_FOLDER_NAME/$DL_HTML"

if [ -s "$EXPORT_FULL_FOLDER_NAME/$DL_HTML" ]
then
    echo "成功生成 ipa 下载页面"
else
    echo "生成 ipa 下载页面失败"
    rm -rdf "$EXPORT_ROOT"
    exit 1
fi

echo "正在将相关文件上传至服务器 ..."

SERVER_USER="xxx" # 服务器用户名,请替换
SERVER_IP="xxx.xxx.xxx.xxx" # 服务器IP,请替换
SERVER_FOLDER="~/xxx" # 服务器某目录,请替换

scp -r "$EXPORT_ROOT/$PROJECT_NAME" "$SERVER_USER@$SERVER_IP:$SERVER_FOLDER"

# 上传完成后,删除目录
rm -rdf "$EXPORT_ROOT"

echo "成功上传,请使用 iPhone Safari 打开:$DL_DOMAIN/$PROJECT_NAME/$EXPORT_FOLDER_NAME/$DL_HTML 进行下载安装"
10月 31

ReactiveObjC-从RAC宏说起

这里说的ReactiveObjC,就是ReactiveCocoaObjective-C版本:

https://github.com/ReactiveCocoa/ReactiveObjC

从一个小例子开始

下面的代码,实现的效果是,当用户名输入框密码输入框都有内容时,登录按钮才会变得可用,否则,不可用。

NSArray *signals = @[
                     self.usernameTF.rac_textSignal,
                     self.passwordTF.rac_textSignal,
                     ];
RAC(self.loginBtn, enabled) = [RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
    username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    return @(username.length > 0 && password.length > 0);
}];

以上代码在经过预处理后,会被转化为:

NSArray *signals = @[
                     self.usernameTF.rac_textSignal,
                     self.passwordTF.rac_textSignal,
                     ];
[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(((void *)0))][@(((void)(__objc_no && ((void)self.loginBtn.enabled, __objc_no)), "enabled"))] = [RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
    username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    return @(username.length > 0 && password.length > 0);
}];

将其中的[@(((void)(__objc_no && ((void)self.loginBtn.enabled, __objc_no)), "enabled"))]简化一下,就变成了:

NSArray *signals = @[
                     self.usernameTF.rac_textSignal,
                     self.passwordTF.rac_textSignal,
                     ];
[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(((void *)0))][@"enabled"] = [RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
    username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    return @(username.length > 0 && password.length > 0);
}];

上面的代码实际上就是调用RACSubscriptingAssignmentTrampoline中的- setObject:forKeyedSubscript:方法:

[[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:((void *)0)] setObject:[RACSignal combineLatest:signals reduce:^id _Nonnull (NSString *username, NSString *password) {
    username = [username stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    password = [password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    return @(username.length > 0 && password.length > 0);
}] forKeyedSubscript:@"enabled"];

为了弄明白,上面的代码是如何展开的,就需要厘清RAC宏。

RAC宏

在厘清RAC宏之前,需要先弄明白其所涉及的所有宏定义。

先来看看它们是如何被定义的。

所有涉及的宏

RAC.png

在上面的图中,可以清晰地看出每个宏的定义,及其实现所依赖的宏。『请在新标签页新窗口中打开图像,以查看高清图』

宏定义释疑

描述
metamacro_concat_(A, B) 以字符串的形式连接A与B。示例:metamacro_concat_(1, 2),其结果为12
metamacro_concat(A, B) 同上
keypath1(PATH) 在它的实现中,逗号表达式中最前面的NO,会在运行时,导致&&之后的((void)PATH, NO))不会执行,节省了开销。示例:keypath1(self.enabled),结果为(((void)(NO && ((void)self.enabled, NO)), strchr(# self.enabled, '.') + 1)),也就是(((void)(NO && ((void)self.enabled, NO)), strchr("self.enabled", '.') + 1))(((void)(NO && ((void)self.enabled, NO)), ".enabled" + 1))(NO, "enabled")"enabled"。注意,这里的PATH,必须要有『.』,否则在运行的时候会导致崩溃:比如keypath1(enabled),会变成(((void)(NO && ((void)enabled, NO)), strchr(# enabled, '.') + 1)),导致找不到『.』,strchr(# enabled, '.')的结果为NULL,最终结果为NULL + 1,使用时会导致出现EXC_BAD_ACCESS
keypath2(OBJ, PATH) 在它的实现中,OBJ.PATH,会在编译期,进行相关的校验,如果OBJ没有对应的PATH,会出现编译错误;逗号表达式中最前面的NO,会在运行时,导致&&之后的((void)OBJ.PATH, NO))不会执行,节省了开销。示例:keypath2(self.loginBtn, enabled),结果为(((void)(NO && ((void)self.loginBtn.enabled, NO)), # enabled)),也就是(NO, "enabled")"enabled"
keypath(...) 会在编译期,对key path进行校验。返回由可变参数构成的key path。示例:keypath(self.loginBtn, enabled),结果为"enabled"
metamacro_at(N, ...) 返回索引为N的可变参数(索引以0开始)。必须至少提供N + 1个可变参数,N为区间[0, 20]中的整数。其展开后就是metamacro_atN(...),这里的N就是区间[0, 20]中的一个整数。示例:metamacro_at(3, 1, 3, 5, 7),因为第一个参数为3,因此必须至少提供4个可变参数(这里的可变参数1、3、5、7,总数量为4),其中,7的索引为3,因此,结果为7。来看看为什么结果是7:把metamacro_at(3, 1, 3, 5, 7)展开后就是metamacro_concat(metamacro_at, 3)(__VA_ARGS__) ,其中metamacro_concat(metamacro_at, 3)的结果为metamacro_at3,所以结果为metamacro_at3(1, 3, 5, 7)。在metamacros.h文件中,有20个metamacro_at扩展,分别是metamacro_at1metamacro_at2 ... metamacro_at19metamacro_at20,它们都使用了metamacro_head,其定义为#define metamacro_head(...) metamacro_head_(__VA_ARGS__, 0)metamacro_head_的定义是#define metamacro_head_(FIRST, ...) FIRST。再看看metamacro_at3的定义:#define metamacro_at3(_0, _1, _2, ...) metamacro_head(__VA_ARGS__),因此将metamacro_at3(1, 3, 5, 7)一步步展开,就是metamacro_head(7)metamacro_head_(7, 0),结果就是7
metamacro_argcount(...) 返回所传入的参数的总个数。必须至少提供1个参数。示例:metamacro_argcount(1),展开后就是metamacro_at(20, 1, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1),其结果为索引为20的可变参数,也就是1(注意,metamacro_at的第一个参数20,并不属于可变参数的一部分)
metamacro_if_eq(A, B) 检查A、B是否相等,如果相等,就展开后面的第一个参数列表,否则就展开后面的第二个参数列表
RAC_(TARGET, KEYPATH, NILVALUE) 展开后就是[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(TARGET) nilValue:(NILVALUE)][@keypath(TARGET, KEYPATH)]。示例:RAC_(self.loginBtn, enabled, nil),其结果就是[[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(nil)][@keypath(self.loginBtn, enabled)][[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(self.loginBtn) nilValue:(nil)][@"enabled"]
RAC(TARGET, ...) 因为其使用了metamacro_if_eq,因此,不是展开(RAC_(TARGET, __VA_ARGS__, nil)),就是展开(RAC_(TARGET, __VA_ARGS__))。其可变参数的个数,只能是1或2,如果是1个可变参数,就对应KEYPATH;如果是2个可变参数,就对应KEYPATHNILVALUE
10月 13

iOS中FPS的测量及其色彩表示

FPS的测量

对于FPS的测量,就是使用CADisplayLink计算某个时间段内某个方法的调用次数:

FPS = 调用次数 / 时间

#import <UIKit/UIKit.h>

@interface FPSMeasurer : NSObject
+ (FPSMeasurer *)sharedFPSMeasurer;
@property (nonatomic, getter=isPaused) BOOL paused;
@property (nonatomic, assign) NSTimeInterval reportInterval;// default is 1, >= 0.1
@property (nonatomic, copy) void (^reportBlock)(int fps, UIColor *fpsColor);
@end
#import "FPSMeasurer.h"
#import <QuartzCore/QuartzCore.h>

@implementation FPSMeasurer {
    CADisplayLink *_displayLink;
    NSTimeInterval _lastTimestamp;
    int _frames;
}

static FPSMeasurer *sharedFPSMeasurer = nil;

+ (FPSMeasurer *)sharedFPSMeasurer {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedFPSMeasurer = [[super allocWithZone:NULL] init];

        sharedFPSMeasurer->_reportInterval = 1;

        CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:sharedFPSMeasurer selector:@selector(displayLinkSelector:)];
        [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        sharedFPSMeasurer->_displayLink = displayLink;
    });
    return sharedFPSMeasurer;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [FPSMeasurer sharedFPSMeasurer];
}

- (void)displayLinkSelector:(CADisplayLink *)displayLink {
    ++_frames;
    if (_lastTimestamp < DBL_EPSILON) {
        _lastTimestamp = displayLink.timestamp;
    } else {
        NSTimeInterval time = displayLink.timestamp - _lastTimestamp;
        if (time > self.reportInterval) {
            int fps = (int)(_frames / time);
            _frames = 0;
            _lastTimestamp = displayLink.timestamp;
            if (self.reportBlock) {
                self.reportBlock(fps, [UIColor colorWithHue:fps / 180.0 saturation:1 brightness:1 alpha:1]);
            }
        }
    }
}

- (void)setReportInterval:(NSTimeInterval)reportInterval {
    if (_reportInterval != reportInterval) {
        if ([NSThread currentThread]) {
            _reportInterval = reportInterval > 0.1 ? reportInterval : 0.1;
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                _reportInterval = reportInterval > 0.1 ? reportInterval : 0.1;
            });
        }
    }
}

- (void)setPaused:(BOOL)paused {
    if (_paused != paused) {
        if ([NSThread currentThread]) {
            self->_displayLink.paused = paused;
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                self->_displayLink.paused = paused;
            });
        }
    }
}

@end

FPS的色彩表示

在用颜色展现FPS的高低时,通常是红色表示低帧率绿色表示高帧率;橙色、黄色、黄绿色,表示高低之间的帧率。

为了将帧率转化为相应的颜色,最简单的方式就是使用HSBHSL色彩空间。

在这两个色彩空间中,H都是色相,它的取值范围是360°的圆心角。360°对应红色60°对应黄色120°对应绿色

在将帧率转化为颜色时,只需要改变H即可。

iOS中,苹果提供了HSB色彩空间所对应的方法,HSB取值被限制在[0, 1.0]区间内。

iOS 10之后(含iOS 10),不会对HSB的取值进行限制,对于[0, 1]区间外的值,会使用扩展的色彩空间

因此,在将帧率转化为颜色时,H的值可以用FPS / 60.0 * 120.0 / 360.0进行计算,也就是FPS / 180.0SB固定为1.0(也可以固定为与1.0非常相近的值)。

[UIColor colorWithHue:FPS / 180.0 saturation:1 brightness:1 alpha:1]

由于HSLHSB有一些区别,如果使用的是HSLL需要取值0.5(也可以取与0.5非常相近的值)。