Quantcast
Channel: 落格博客
Viewing all 377 articles
Browse latest View live

小火箭 Shadowrocket 的四个高级配置

$
0
0

落格博客阅读完整排版的小火箭 Shadowrocket 的四个高级配置

使用小火箭很久了,很多朋友都说,小火箭 Shadowrocket 不稳定,老自己掉后台,这次我就来说说我是怎么使用它的。

以前 iOS 不支持这类 VPN 应用,每次在 iOS 上翻墙,都是一件让人煞费苦心的事情。后来 Surge 出现了……我第一时间就买了,当然,再后来中区下架 Surge,我也彻底换了美区的 Apple ID,就没有再买另外一份 Surge,转而买了相对便宜的 Shadowrocket (俗称小火箭,其实中区我也买了的,不过也下架了不是?)。

总之,要想让小火箭用的好,是需要一番配置的,它是专门用来翻墙的,所以在功能上更偏向这边,相对与 Surge,Shadowrocket 也能支持更多协议。

修改测速模式

在 Shadowrocket 的 Setting 里,你可以设置它的 Ping Method,可选 ICMP 和 TCP,我们选择后者,这样测速出来的结果更贴近实际效果而不仅仅是检测服务器是否在线可达。(当然代价是测速需要的时间更长了一点点,如果你添加的服务器比较多,那可能就需要多等一小会才能全部出结果。)

添加自动测速

Surge 有个很不错的功能就是能够给多个线路定期自动测速并切换到最快的线路上,这样可以避免在服务器挂了后我们还得手动跑去测速并切换另一条线路。其实 Shadowrocket 也是有的,在 Home 页面,Global Routing 功能里即可发现。

设置 Shadowrocket 自动测速并切换代理
  • 首先在 Speed Test 中添加一个测速组,在组里增加几条平时测速比较快的线路,然后开启测速服务;
  • 接下来创建一个 Scene,由于这里我们只是要进行自动测速,所以 Scene 选择 Default,Routing 选择 Config,Type 自然是选择 Group,然后在下方的 Config 选择你正在使用的配置即可,最后给 Scene 一个名字,保存并选中之;
  • 最后,在 Global Routing 中,选择 Scene。

这样,你的 Shadowrocket 就能每隔 600 秒(默认配置)进行一次测速并选择最快的线路了。

崩溃自动重启

比起 Surge,Shadowrocket 支持一个让人意外的功能—— On Demand。这其实是 VPN 的一个有意思的功能,它可以让 iOS 系统级监听你正在请求的域名,当遇到特定的域名时,就先启动 VPN,再请求这个域名。

通过 On Demand 功能让 Shadowrocket 自动重启

我们到 Shadowrocket 的 On Demand 功能中,启动这个功能,并关闭 Disconnect on Sleep,Network 选 Any。

Shadowrocket 已经帮你内置了一些常用的域名通配,比如谷歌、Twitter之类的,如果有必要,你也可以自己添加一些。这样,当你在上网时,如果 Shadowrocket 后台挂了,在你点击下一个链接时,它就能再次被系统后台启动。

这样,你就再也不会遇到刷推正刷的开心,突然所有图片消失,检查了大半天,从路由器到服务器,最后发现其实是 Shadowrocket 服务自己停了的尴尬场景了。

随时更新的代理策略

我曾在一篇文章中写过关于白名单的事情《是时候使用 PAC 白名单了》,但奈何这个列表实在太大,根本无法被 iOS 所容纳——实际上,在新的 Surge 中即使是我的黑名单,也会被提示列表过大。

为此,我专门制作了一个根据 IP 地址判断的白名单列表……总之,这个规则在这里 geoip_whitelist.conf ,在 Shadowrocket 的 Config 页面,最底部添加 Remote Files,遗憾的是只能添加一个感谢网友 僊 提醒,见评论,点击页面右上角加号即可添加更多远程配置了),总之,我们把这个地址添加进去,它就会自动下载了。

这个配置文件是兼容 Surge 和 Shadowrocket 的,不过 Shadowrocket 在添加后会提示你是否也要导入服务器信息,这里点取消,配置中内置的服务器信息只是一个提示内容。

这也是我比较喜欢 Shadowrocket 的地方,它把服务器数据和代理策略单独存放,可以很方便地修改配置和策略。

一键更新配置文件

这时你会发现这个地址被保留在了页面底部,今后你可以每隔一星期点击一次,选择 Use Config,Shadowrocket 就会自动下载并更新本地配置了。(我的配置每周自动更新一次,对于本文中的 geoip 策略来说,仅仅更新了去广告规则)

 

小火箭 Shadowrocket 的四个高级配置,首发于落格博客


swift4 urlSession get和post网络请求

$
0
0

落格博客阅读完整排版的swift4 urlSession get和post网络请求

废话不多说,直接上代码。

GET:

// 创建一个会话,这个会话可以复用
let session = URLSession(configuration: .default)
// 设置URL
let url = "http://127.0.0.1/api/"
var UrlRequest = URLRequest(url: URL(string: url)!)
// 创建一个网络任务
let task = session.dataTask(with: UrlRequest) {(data, response, error) in
    do {
        // 返回的是一个json,将返回的json转成字典r
        let r = try JSONSerialization.jsonObject(with: data!, options: []) as! NSDictionary
        print(r)
    } catch {
        // 如果连接失败就...
        print("无法连接到服务器")
        return
    }
}
// 运行此任务
task.resume()

POST:

// 这个session可以使用刚才创建的。
let session = URLSession(configuration: .default)
// 设置URL
let url = "http://127.0.0.1/api/"
var request = URLRequest(url: URL(string: url)!)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
// 设置要post的内容,字典格式
let postData = ["email":"user@xxx.com","password":"123456"]
let postString = postData.compactMap({ (key, value) -> String in
    return "\(key)=\(value)"
}).joined(separator: "&")
request.httpBody = postString.data(using: .utf8)
// 后面不解释了,和GET的注释一样
let task = session.dataTask(with: request) {(data, response, error) in
    do {
        let r = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
        print(r)
    } catch {
        print("无法连接到服务器")
        return
    }
}
task.resume()

 

swift4 urlSession get和post网络请求,首发于落格博客

DoT DoH 除了 DNSCrypt,你还可以了解一下更好的 DNS 加密方案

$
0
0

落格博客阅读完整排版的DoT DoH 除了 DNSCrypt,你还可以了解一下更好的 DNS 加密方案

三年前,我写了一篇文章《在 OS X 上 避免 DNS 泄露》来谈如何保护你的隐私,并避免 DNS 泄露,那时候主要用的技术是 dnscrypt——实际上后来我就放弃了这个方案,原因是公共服务器受干扰严重,配置复杂延迟高。现在三年过去了,我们来看看最新的 DoTDoH,实际上就是 DNS over TLSDNS over HTTPS

那些 DNS 加密方案

实际上我们早就已经谈过,DNS 是明文传输所有内容的,设计之初就没考虑安全这回事,甚至是抢答的,你问 A www.logcg.com 是哪个 IP 地址,A 还没来得及回复, B 抢答说 127.0.0.1 !于是,你就信了。

这也是如今 gfw 墙网站的常用手段,也是 DNS 污染的原理。总之,dnscrypt 出现了,但它使用了一个自定的协议,并配置需要互相交换密钥,这导致了很多麻烦。如今,又有了两个全新的 DNS 加密(显然也就反污染了),这次我们就来谈谈,他俩到底哪里不同。

DNS over TLS

TLS 加密实际上就是我们上网的 HTTPS 所用加密了,安全性得到了很好的保障——这东西如果失效了,那整个互联网估计也就完蛋了。

DoT 使用 853 端口,使用 TCP 进行传输——基本上可以理解为加密版本的普通 DNS 了。

现如今,DoT 已经有了相当成熟的客户端,使用

brew install stubby
 即可安装,再使用
sudo brew services start stubby
 就能启动了,stubby 推荐使用默认配置,已经集成了多个可信的 DoT 服务器。我这边测试查询速度为最慢 1 秒……是的,你还是需要一个前置的 DNS 缓存服务,比如 dnsmasq,这里我就直接用 Surge 充当了。

一些未来的疑惑

DoT 看起来很美妙,几乎是完成了我们对加密 DNS 的一切幻想,但有一点还是应当注意,在中国这样的国家,DoT 一旦流行起来,那么它对然不能再被污染,但却很容易被封禁——因为它有一个固定的独立端口,虽然别人不知道你在访问什么网站了,但却能够知道你在用 DoT ,干脆直接干扰你 TCP 包不就行了?

(当然,DoT 也是可以专门占用 443 端口就是了)

DNS over HTTPS

总之,混淆才是王道,虽然这样会让网管很头疼,但在严重审查的地区,还是值得一试的。尽管现在人们对于 DoH 的态度还颇有争议,但还是有很多互联网机构支持了它——直接使用 HTTP/2 或者 HTTPS 协议进行请求,这下你就很难专门把 DNS 流量单独分离出来进行干扰了。

尤其对于自建 DNS 服务器来说,甚至可以直接隐藏在网站之后!

要使用 DoH,使用

brew install cloudflare/cloudflare/cloudflared
 即可安装,运行命令 
sudo cloudflared proxy-dns
 来临时启动它进行测试,你可以看到它使用了两个上游服务器:
INFO[0000] Adding DNS upstream                           url="https://1.1.1.1/dns-query"
INFO[0000] Adding DNS upstream                           url="https://1.0.0.1/dns-query"

待服务启动后就可以尝试查询,第一次测试的时候我遇到了大量明显的报错:

ERRO[0051] failed to connect to an HTTPS backend "https://1.1.1.1/dns-query"  error="failed to perform an HTTPS request: Post https://1.1.1.1/dns-query: net/http: request canceled (Client.Timeout exceeded while awaiting headers)"
ERRO[0051] failed to connect to an HTTPS backend "https://1.1.1.1/dns-query"  error="failed to perform an HTTPS request: Post https://1.1.1.1/dns-query: net/http: request canceled (Client.Timeout exceeded while awaiting headers)"
ERRO[0090] failed to connect to an HTTPS backend "https://1.1.1.1/dns-query"  error="failed to perform an HTTPS request: Post https://1.1.1.1/dns-query: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)"

但结果还是正常返回了,第一次查询时间稳定在 300ms 以内,服务本身自带了缓存功能,第二次查询之类的自然就是 0ms 了。

测试成功后,需要给 cloudflared 进行配置,这样它才能以服务的形式自动启动:

mkdir -p /usr/local/etc/cloudflared
cat << EOF > /usr/local/etc/cloudflared/config.yml
proxy-dns: true
proxy-dns-upstream:
 - https://1.1.1.1/dns-query
 - https://1.0.0.1/dns-query
EOF

你可以看到,在

/usr/local/etc/cloudflared/config.yml
 文件中我们给了这两个默认的上游服务器,你也可以在这里添加更多。

总之,在创建好配置文件之后,我们再来执行命令将服务安装到系统当中:

sudo cloudflared service install
INFO[0000] Applied configuration from /usr/local/etc/cloudflared/config.yml
INFO[0000] Installing Argo Tunnel as an user launch agent
INFO[0000] Outputs are logged in /tmp/com.cloudflare.cloudflared.out.log and /tmp/com.cloudflare.cloudflared.err.log

现在,你就可以按 ctrl + c 停掉刚刚测试用的临时服务了,然后用命令启动系统服务:

sudo launchctl start com.cloudflare.cloudflared

 

系统配置

现在,不论是 DoT 还是 DoH,我们都已经启动了(注意,这两者你只能同时启动一个,因为 dns 都会占用本地的 53 端口)你只需要配置系统的 dns 解析到 127.0.0.1 即可。

额外的内容

如果你和我一样在使用 Surge,那么你可能会发现在开启了 Enhance Mode 之后似乎出现了 DNS 环路的问题,导致查询结果为空……似乎是 DNS 服务和 Surge 互相抢答了。这里我们要配置 Surge 不使用自定义 DNS,转而使用系统设置中的 DNS:

Surge 中设置使用系统 DNS 而不是自定义

 

结论

就目前来看,在中国使用 DoT 要相对稳定,但速度慢一点,使用 DoH 速度快些但干扰严重——这也可能和 DoH 公共服务器还不是很多有关(毕竟很容易就给你干掉 IP 了),总之,这两种手段都是自建服务器绝佳的选择,简单、快捷,还很轻松。

行文仓促,我目前已经在使用 argo tunnel ,也就是 cloudflared 了,自带 DNS 缓存,及时不使用前置 DNS 缓存也能很好的运行,过段时间我会来重新编辑这篇文章并附上效果体验。(希望不会因为频繁访问 1.1.1.1 而被禁掉)

 

参考文献:

  1. https://github.com/getdnsapi/stubby
  2. https://developers.cloudflare.com/argo-tunnel/reference/service/

DoT DoH 除了 DNSCrypt,你还可以了解一下更好的 DNS 加密方案,首发于落格博客

sed: 1: “…”: invalid command code

$
0
0

落格博客阅读完整排版的sed: 1: “…”: invalid command code

Linux 的朋友可能会对这个命令比较熟悉,它可以在脚本里快速和批量地对文本文档进行操作,比如改动某一行或者替换具体内容……

macOS 自然也是有这个命令的,但有一点不太一样,如果你执行 

sed -i
 ,那么多半你会得到一个奇怪的报错
sed: 1: “…”: invalid command code
 。

 

查询后得知,这个命令在 macOS 上有一点变化,sed 允许你在替换时设定一个备份文件以避免数据丢失——这个选项在 Linux 中是可选的,但在 macOS 中是必选的,所以,在macOS中 

sed -i
 会多一个参数,如果你没给,则导致整体参数少一个,然后报错。
 ~/ sed -i 's/1/1/g' a
sed: 1: "a": command a expects \ followed by text
 ~/ sed -i '.bak' 's/1/1/g' a

 

延伸阅读

  1. “invalid command code .” error from sed after running find and sed on Mavericks
  2. sed: 1: “…”: invalid command code on Mac OS

sed: 1: “…”: invalid command code,首发于落格博客

落格输入法 macOS 2 是如何为 VoiceOver 进行优化的

$
0
0

落格博客阅读完整排版的落格输入法 macOS 2 是如何为 VoiceOver 进行优化的

在两年前,我曾写过一篇名为《ios 为视障用户支持 VoiceOver》的文章,里边主要介绍了 iOS 端该如何为 VoiceOver 进行必要的支持,后来我又开发了 macOS 端的落格输入法,但很遗憾由于 macOS 自身系统 bug,第三方输入法根本无法获得 VoiceOver 焦点(主要是 10.13 及以下版本),所以我也就没有过多关注——甚至直到这款输入法整个生命周期结束也没能实现 VoiceOver 的支持,实在是让人遗憾。

落格输入法 macOS 2

不过还好,到 macOS 10.14 Mojave,神奇的事情出现了,这个 bug 完全“反转”了,它并没有修复,但以另一种方式呈现——系统自带的中文输入法无法获取 VoiceOver 焦点,反而第三方输入法可以了!😆

总之,这次我也得以能够为 VoiceOver 用户提供支持 :)

直到现在,落格输入法 macOS 2 也已经正式上架销售了一段时间,产品也相对第一个版本完善了许多,我觉得是时候写一篇文章来粗略总结一下 macOS 平台对 VoiceOver 支持的重点。

判定

要支持 VoiceOver,你就必须能够判断系统当前是否正在运行 VoiceOver,不同于 iOS,macOS 并不直接给出 VoiceOver 是否在运行的判断 API,也就是说,从官方的态度来看,还是希望视障用户能和明眼用户得到尽可能一致的用户体验的。但总之,针对视障用户做特定的额外优化还是必要的,我们可以通过读取系统的偏好设置来判断 VoiceOver 是否打开——但这也有缺点,比如用户打开了 VoiceOver,但不经过系统偏好设置直接关闭了 VoiceOver,那此时我们依旧得到的是“开启”的状态(对 VoiceOver 状态特别敏感的开发者需要注意):

func NSIsVoiceOverRunning() -> Bool {
    
    if let flag = CFPreferencesCopyAppValue("voiceOverOnOffKey" as CFString, "com.apple.universalaccess" as CFString) {
        if let voiceOverOn = flag as? Bool {
            return voiceOverOn
        }
    }
    
    return false
}

 

设置工具方面

如果你都使用了系统提供的控件,那么设置项和 iOS 没什么区别,值得一提的就是系统自带的圆圈问号按钮,也就是在一些 app 里常见的 help 按钮,它默认的名称叫“help”,而 storyboard 中修改的地方叫做“Description”,这让人多少觉得有些混淆,作为对比,iOS 则对应的输入框,名称叫 “title”。

候选栏

对于输入法来说,候选栏可能是最常用的一个界面了,这也是它主要的 UI 部分。为了提供更完善的自定义和更优秀的显示性能,实际上我并没有使用 macOS

InputMethodKit
 中自带的那个候选栏,那个候选栏除了能简单“显示候选”这一个功能外,其他所有自定义选项都是无效的,更别提这些选项之外的自定义功能了,更是不可能。为此我自己实现了一个候选栏,但是在 VoiceOver 下,读取的时候是这样的顺序:

1→第一个候选内容→候选的背景→2→第二个候选的内容→3→……

这并不是一个很好的体验,尤其是当选中的内容是123这些数字的时候,用户甚至不知道上屏的是什么——实际上你的输入法也不知道要选中什么。

NSAccessibilityGroup 协议

这个问题主要是 VoiceOver 把你 View 都给遍历了,它无法区分哪些有用哪些没用,这时候我们用一个 View 包裹这些元素(实际上你本来也应该这么做,毕竟这是一个“候选”对象,对吧?),然后让你的 View 遵循 

NSAccessibilityGroup
 这个协议,这时对 VoiceOver 来说,它就是一个唯一的元素了,VoiceOver 不会继续遍历改 View 下的 SubView。

不过,这时候新的问题产生了,这些 Group 没了内容,因为是自定义的 Group,我们需要手动来为这个 Group 赋值。

要给 Group 赋值,不同于 iOS,macOS 下你还是需要用方法来实现而不是直接给属性赋值,比如

self.setAccessibilityTitle("候选1")
 但事实上这样赋值也不会生效,我不知道这是对于输入法的特殊需求,还是说其实是个系统的 bug,总之,我尝试重写这些方法,最终成功了:
override func accessibilityLabel() -> String? {

        return "候选1"
    }

这样,当用户移动 VoiceOver 焦点到对应的候选上时,VoiceOver 就会读出候选的内容了(这里就是“候选1”)。当然,这还不够,你会发现视觉上 VoiceOver 的方框是跑偏的,所以你还需要设置它的位置,尽管也许视障用户并不介意这些小细节:

override func accessibilityFrame() -> NSRect {
        if let r = self.superview?.window?.convertToScreen(self.frame) {
            return r
        }
        return self.frame
    }

这里我们尝试获取当前候选栏的 window,并将候选的位置信息转换到全局屏幕坐标并返回(VoiceOver 要的坐标必须是屏幕级别),这样,VoiceOver 的焦点方框也就对正了。

最后是 Group 的身份问题,如果我们不设置,那么用户即使把 VoiceOver 焦点移动到了对应的候选上,按下空格后可能上屏的依旧是第一个,因为 VoiceOver 有一套它自己的控制快捷键,所以,这里也要支持一下:

override func accessibilityRole() -> NSAccessibility.Role? {
        return .button//设置为 button 角色,这样才能允许交互
    }
override func accessibilityPerformPress() -> Bool {
        
        return true//返回 true 表示接受了本次操作
    }

这样候选栏部分基本就完成了。

发送 NSAccessibility 通知

如同 iOS 一样,有时候我们需要给 VoiceOver 发送通知以触发刷新,macOS 也支持你这么做:

extension NSAccessibility.Notification {
    ……
    public static let applicationHidden: NSAccessibility.Notification

    public static let focusedUIElementChanged: NSAccessibility.Notification

    ……
}

与 iOS 不同的是,桌面级别的通知多了很多,这里我只用到了上面列举的两个,前者用来通知候选栏已经隐藏,后者用来触发 VoiceOver 刷新。

用户体验

状态提示

不同于系统自带中文输入法,落格输入法和其他第三方输入法一样,是支持中英文模式切换的,这其实对视障用户带来了不小的困惑,因为他们不知道当前的状态是什么,并且视觉上的小方块提示也无法通知到他们,在与视障用户交流之后,我做了一个简单的状态提示音,在切换到中文或者英文时,发出不同的提示音用以通知。

另外,由于中英切换的那个提示小方块实际上也是一个 window,那就存在一个问题,如果你切换中英文模式或者繁简功能,那么这个小方块的出现会抢夺 VoiceOver 焦点,这时候就需要让它对 VoiceOver 不可见:

w.setAccessibilityHidden(true)
w.setAccessibilityElement(false)
w.setAccessibilityEnabled(false)

无限候选栏

通常,候选栏是翻页设计的,为了方便视障用户(其实有时候明眼用户也用得到)选字,落格输入法 macOS 2 支持自动翻页,比如当前有 16 个候选,候选栏默认显示了 6 个(第一页),那么你移动高亮到第六个的时候继续移动,会自动翻页并定位到第二页的第一个。

解字不读词

如同明眼人一样,视障用户也会追求输入的效率,VoiceOver 在读候选词的时候会先把整个候选完整读一遍,然后再逐字解释,这对整句输入用户来说可能就很方便,一次输入好几个字,然后听一遍发现没大问题,就发送了,根本不用等后边的逐字解释,方便的很——但这样有个弊端就是中文字符同音太多了,经常会导致一些错字在里面,就是明眼人都在所难免,何况是靠听(比如:帮我占个座尾巴)。对于喜欢打单或是打词的用户,其实只解释字要更高效,在 空山新雨 的建议下我添加了落格输入法 macOS 2 特有的“只解字不读词”模式,不论是落格解释库还是经典解释库,都可以直接支持,方便的很。

已知问题

当然,最终还是少不了一些遗留问题:

候选栏的名称改不了,兴许是因为输入法是个后台应用导致的?不论为 window 设置什么名字,VoiceOver 下永远读的是“application”。

另一个是输入完会触发全文朗读的问题,虽然聊天发言估计问题严重性不大,但如果你是在写一篇长篇的文章,那就有点让人崩溃了……但遗憾的是,我没有任何办法能够打断这个过程,目前唯一的解是用户输入完自己手动移动一下光标……

还有一个记不太清的问题,在 macOS 上,主动发通知让 VoiceOver 朗读内容似乎是无法生效的。

 

落格输入法 macOS 2 是如何为 VoiceOver 进行优化的,首发于落格博客

办公软件好,人人少不了。Office 365,走你!

$
0
0

落格博客阅读完整排版的办公软件好,人人少不了。Office 365,走你!

办公软件这东西,文字处理、表格编辑、还有著名的“ppt”幻灯片,毕业后我几乎就没再碰过微软系了,偶尔需要文字编辑用的也是苹果的 iWork 系列(当然写论文什么的就别想了),总之,这次我要软一次,推一下 Office 365.

其实就是微软的办公套件,各位可能还在用盗版——这次是绝佳的机会把它洗白——价格足够便宜。

 

利益相关

我的 落格输入法 macOS 2 的中国区销售一直是由 数码荔枝正版软件商城 进行独家代理销售的,这次数码荔枝拿到了 Office 的销售资格,真的是很厉害很不容易了,除了微软官方旗舰店外,他们现在可能是整个淘宝店铺中极少数能直接获得微软官方授权的店铺。

优惠

从 3 月 4 日至 3 月 31 日,在 数码荔枝正版软件商城 入手 Office 365 个人 / 家庭版,分别可享受高达 8.8 与 8 折优惠!

  • Office 365 个人版:349 元(可供1个人,共计 5 台设备使用)
  • Office 365 家庭版:399 元(供至多6人,共计 30 台设备使用)

更难能可贵的是,购买 Office 365 还能获赠 AdGuard 或 XMind: ZEN 订阅授权~

买就送

这里,奉上老夫 py 来的独家折上折优惠链接,再减 5 元!

 

全家桶,一应俱全

Office 365 是基于云的订阅服务,包含 Word、Excel、PowerPoint、Outlook、Access、Publisher 这 6 款经典组件。活动策划、财务分析、年终汇报、收发邮件…一步到位。

微软办公全家桶

1TB OneDrive 空间,文件随便存

Office 365 提供 1TB OneDrive 云存储空间,你可以通过它来备份各种电脑重要文件,和手机中的照片、视频。

OneDrive

要知道,一年单独购买 50GB OneDrive 空间仍需 180 元(15 元*12 月),而 Office 365 附赠的 1TB 空间,相当于前者的 20 倍。

随时随地,移动办公

相比于只能激活 1 台电脑的买断制 Office 套件,Office 365 个人版支持一个账户激活 5 台设备,你的 PC、Mac、iPhone、iPad 或 Android 设备都能一起使用。

家庭车,妥。

Office 365 家庭版支持多达 6 名用户同时使用,也就是说,你的每位家人都能激活自己的 5 台设备,并拥有 1TB 的 OneDrive 空间(共 30 台设备、6TB OneDrive 空间)。

家庭车

这次,我软了,首先它很实惠……其次它真的很值。办公室写报告、学校学生写论文,实在是居家旅行之必备良品,买吧。

Office 365 个人 / 家庭版特惠链接 >

注:

Office 365 支持的系统版本为 Windows 7/macOS 10.10 及以上;
原有的 OneNote 组件在 Office 365 中不提供,但用户可在 OneNote 官网或应用商店免费下载使用;
Access 与 Publisher 两款组件仅在 Windows 系统中提供。

办公软件好,人人少不了。Office 365,走你!,首发于落格博客

Swift 里的数组去重方案

$
0
0

落格博客阅读完整排版的Swift 里的数组去重方案

在使用 Swift 进行开发落格输入法时,我遇到了一个很有意思的问题——去重

众所周知,输入法的候选在计算出来后总会有可能是重复的选项(比如码表和词库中都有某个词,也许他们编码不同,但字是一样的之类),这时候就需要去重,但又要保持候选的先后顺序不变。

别人的解决方案

如果你去网上找,那么你可能找到的是这样的:

extension Array where Element : Hashable {
    var unique: [Element] {
        return Array(Set(self))
    }
}

来源:https://stackoverflow.com/questions/27624331/unique-values-of-array-in-swift

这样的:

extension Array where Element: Hashable {
    func removingDuplicates() -> [Element] {
        var addedDict = [Element: Bool]()

        return filter {
            addedDict.updateValue(true, forKey: $0) == nil
        }
    }

    mutating func removeDuplicates() {
        self = self.removingDuplicates()
    }
}

来源:https://www.hackingwithswift.com/example-code/language/how-to-remove-duplicate-items-from-an-array

或者是这样的:

extension Array where Element: Equatable {
    mutating func removeDuplicates() {
        var result = [Element]()
        for value in self {
            if !result.contains(value) {
                result.append(value)
            }
        }
        self = result
    }
}

来源:https://medium.com/if-let-swift-programming/swift-array-removing-duplicate-elements-128a9d0ab8be

问题

我们先说上文中的第一个代码块,他直接用了

Set
 ,众所周知,这东西和字典(
Dictionary
 )一个样,实际上是没有顺序的,所以你把
Array
  转换为
Set
  再转换回
Array
  确实达到了去重的目的,但极有可能让原本的顺序错乱。

在 Swift 中,

Set
  在一定程度上是能够保持顺序的,比如上文来源网页中的例子实际上就是保持了原本的顺序的,这只能是一个巧合,因为它并不保证任何内容都是原来的顺序的。

第二个代码块的实现实际上已经非常不错了,事实上它是我在写这篇文章的时候意外发现的一种实现方式,利用的就是

Array
  的
filter
 方法并使用一个
Dictionary
  进行过滤,它占用了一点点额外的空间,但无可厚非。

第三个代码块问题在于直接用

Array
  的
contains
 方法来判断存在,这是一种非常不好的方式,实际上我在在字符串中 快速查找一文中曾讨论过,Swift 中几乎所有
contains
 都没有同等类型的
index
  或者
range
  来的快,何况
Array
  的查找速度是 O(n)。

更高端的解决方案

实际上,你还有更高级的解决方案,比如传说中的“有序字典”,不过很遗憾的是 Swift 基础框架中并没有给出这样高端的算法,比如一个红黑树实现。不过,在我找到几个红黑树进行比较之后,实际上这种高级算法更慢(也许是因为我的具体场景无法体现高级算法的优势,比如需要去重的数量不够多?)

最终实现

总之,我最后还是根据我的经验实现了我自己的版本,实际上上文中的第二个代码块(用字典实现的那个)已经很不错了,不过,这里还是贴出我的代码,并对其讨论:

extension Array where Element:Hashable {
    var unique:[Element] {
        var uniq = Set<Element>()
        uniq.reserveCapacity(self.count)
        return self.filter {
            return uniq.insert($0).inserted
        }
    }
}

我的实现中,使用了

Set
  作为过滤器,因为它主要就是干这个的 :)

也有人说 Set 实际上就是红黑树实现,这里我是要打个问号的,Swift 中的 Set 具体是用什么算法实现的我并没有查到,但似乎并不是红黑树。

这里重要的地方有两点,首先是

.reserveCapacity
 ,实际上对于 Swift 来说,
Array
 、
Dictionary
 、
Set
 这类集合类型的空间是动态分配的,绝大部分时间你不需要手动去为它设定容量大小,但这里我们追求速度,所以我手动为它设定了
Array
  的长度,因为这里目的是去重,所以最大长度绝对不会超出原本数据的大小,这样就避免了
Set
  在去重过程中添加元素导致扩容,这个扩容的过程,实际上也是有很大的时间成本的。

另外一点是

Set
  的
.insert
 ,通常我们会忽略掉
.insert
 的返回值,毕竟它的声明就是包含了
@discardableResult
 的,但如果你仔细观察,会发现这个方法返回一个元组,类型是
(inserted: Bool, memberAfterInsert: Set<Element>.Element)
 ,它告诉了你,这个元素是否成功插入,以及这个元素本身——也就是说,如果已经存在,那么插入失败。

这样,我们就得到了一个快速、精简的 Swift

unique
  方法。

讨论

实际上,乍一看我的这个实现似乎也没什么了不起的地方,毕竟,也就是个集合罢了,那么,如果是直接一点的实现,那么大多数人可能会想到类似的,比如这样:

var unique:[Element] {
        var uniq = Set<Element>()
        var result:[Element] = []
        
        for v in self {
            guard uniq.firstIndex(of: v) == nil else {continue}
            uniq.insert(v)
            result.append(v)
        }
        
        return result
    }

这个算是上边实现的繁琐版本,但它很直观,这里我使用了

firstIndex(of:
 来判断存在,要比
contains
 快那么一点点,具体效果如何呢?我们来用这样的的代码测试一下:
let data = [1,2,3,4,5,6,7,8,9,0,9,8,7,6,5,4,3,2,1,4,2,3,5,6,7,45,3,43,99,687,8,5,3]
var date = Date()

for _ in 0..<9999 {
    let a = data.unique
}
print(Date().timeIntervalSince(date)*1000)

我们对一个随手写的数组进行去重 9999 次,计算时间,得到的结果单位是毫秒(ms):

167.54305362701416

看起来挺快的,对吗?那么我们来用同样的测试代码,试试上文中提到的用字典实现去重,我把代码再贴过来:

var unique:[Element] {
        var addedDict = [Element: Bool]()
        return filter {
            addedDict.updateValue(true, forKey: $0) == nil
        }
    }

同样去重 9999 次,得到时间:

142.61806011199951
 看起来不错呢,快了 25 毫秒!

最后,是我的实现:

var unique:[Element] {
        var uniq = Set<Element>()
        uniq.reserveCapacity(self.count)
        return self.filter {
            return uniq.insert($0).inserted
        }
    }

结果:

103.64902019500732
 ms

 

当然,本文的讨论是有前提限制的,比如数据量不会很大。还有就是主要针对 Swift 这个编程语言,换到其他语言,兴许就还会有更方便更高效的算法也说不定(主要是 Swift 里坑太多……),另,之前对于毫秒单位搞错,现已更正,超尴尬的。

Swift 里的数组去重方案,首发于落格博客

iOS 独立开发:管理你的兑换码

$
0
0

落格博客阅读完整排版的iOS 独立开发:管理你的兑换码

作为 iOS 开发者的你,肯定是知道 Promo codes 这个东西的,也就是我们常说的兑换码。(当然,作为 iOS 用户兴许你也对此不陌生)

这次,我们就来看看,这个看似无穷无尽的兑换码,到底有哪些限制。

有效期

苹果后台生成的兑换码有效期一直是一个谜,虽然官方的说法的 4 周,也就是 28 天,但实际上如果这期间你的 app 更新了,那么兑换码很可能就会失效[1]。

 

反过来如果你发的码是新版本,但你的新版本还没有上架,那么用户兑换后得到的就是预购模式,当你上架新版本后,兑换到的 App 会自动下载安装。

兑换行为

你可以获取 App 的兑换码、App 内购的兑换码以及  TV 的兑换码,但  TV 的兑换码必须在 iOS 设备上进行兑换。

用户通过兑换码获得的 App,和正常购买一样,会出现在已购列表中,开发者后台会看到一个销售记录(显然没有收入),唯一的不同是这个用了码的用户不能在 App Store 里对这个 App 进行评分和评论。

如果兑换的是 内购 项目,那么分两种情况,1、App 免费,那么 iOS 会先帮你下载,再兑换;2、App 收费,用户得先购买 App 然后兑换。

如果兑换的是自动续费的订阅,那么这个订阅【不会】在到期后自动续费。

数量

对于普通 App 的购买兑换,每个 App 在每个平台有 100 个码可获取,更新版本即可刷新数量,没有上限。

对于内购项目的兑换,每个内购项目拥有 100 个码可用,每【半年】刷新一次[2],如果你有很多个内购项目,那么所有项目总共可生成码为 1000 个,用完等半年。


[1] 兑换码在生成后 App 更新了,根据经验,这个码会失效无法兑换。但苹果官方的文档中并没有提及这种情况。

[2] 固定每年1月1号和6月1号刷新。

 

参考文献

  1. https://help.apple.com/app-store-connect/#/dev50869de4a
  2. https://help.apple.com/app-store-connect/#/dev1e322b132

iOS 独立开发:管理你的兑换码,首发于落格博客


让 iOS macOS 中文字体实现视觉垂直居中

$
0
0

落格博客阅读完整排版的让 iOS macOS 中文字体实现视觉垂直居中

在开发落格输入法 macOS 的时候,我遇到了一个比较奇葩的问题,这个问题一直困扰我到现在——当有些地方需要垂直居中显示一排文字的时候,如何让这些字真正的“居中”?

 

乍看之下这似乎没什么道理,垂直居中嘛……等等,macOS 上的

NSTextField
  还真没有办法让你的一行文字垂直居中……🤷‍♂️

第一代方案

后来,我参考网上的一些解决方案,自己动手写了一个

NSTextFieldCell
  的子类,这样文本就能真正地实现垂直居中了,代码如下:
final class VerticallyCenteredTextFieldCell: NSTextFieldCell {
    func adjustedFrame(toVerticallyCenterText rect: NSRect) -> NSRect {
        // super would normally draw text at the top of the cell
        var titleRect = super.titleRect(forBounds: rect)
        
        let minimumHeight = self.cellSize(forBounds: rect).height
        titleRect.origin.y += (titleRect.height - minimumHeight) / 2
        titleRect.size.height = minimumHeight
        
        return titleRect
    }
    
    override func edit(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, event: NSEvent?) {
        super.edit(withFrame: adjustedFrame(toVerticallyCenterText: rect), in: controlView, editor: textObj, delegate: delegate, event: event)
    }
    
    override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) {
        super.select(withFrame: adjustedFrame(toVerticallyCenterText: rect), in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength)
    }
    
    override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
        super.drawInterior(withFrame: adjustedFrame(toVerticallyCenterText: cellFrame), in: controlView)
    }
    
    override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
        super.draw(withFrame: cellFrame, in: controlView)
    }
}

 

第二代方案

后来为了提升布局效率,我不再使用

NSTextField
 ,而是直接在
NSView
  上绘制文本,不过原理还是一样的,根据字体视觉居中来设置 y 值偏移量。

但是好景不长,很快,落格输入法就要支持候选栏自定义任意字体了,这下就很糟糕了——因为并不是所有的字体都有着同样的高度,比如同是 18 号大小的中文字,系统字体和系统自带娃娃体字体高度就是不同的:

相比与系统自带,娃娃体明显靠下

你看,同样大小的字号,右侧娃娃体明显比左侧系统自带字体要矮一些——关键是变矮后,他们都是基于字体基准线来排布的,导致娃娃体这个字体在实际视觉中“靠下”了。

事实上类似娃娃体这样的字体有很多,比如宋体、楷体等中文字体几乎全都是不尽相同的低矮设计,这样你就很难找到一个通用的偏移量去垂直居中文字。

第三代方案

这次,既然已经直接绘制文本了,那么我就从字体属性本身下手,查看字体声明,我发现了一个有趣的属性

boundingRectForFont
 ,这个属性返回一个表示文字字符边界的矩形,这就很有意思了,不同的字,字符的高度不同,边界的高度也就不一样。

所以我在每次布局时都创建一个相同字号的系统字体,获取字体边界,再获取用户自定义的字体的边界,两者取高度差,就是用户字体和系统自带字体的字符高度差了。然后,再根据上文中的思路,调整对应偏移量,即可得到真正的视觉垂直居中布局:

动态计算偏移量后实现任意字体视觉上垂直居中

 

 

让 iOS macOS 中文字体实现视觉垂直居中,首发于落格博客

Ubuntu 超快部署 wireguard 服务端

$
0
0

落格博客阅读完整排版的Ubuntu 超快部署 wireguard 服务端

新出的 Wireguard 很多人都想尝试,这里 VPN 到底适不适合用来翻墙我们先不讨论,先来看看怎么快速在 vps 上起一个 wireguard 服务。很多人听说这个服务配置起来特别复杂,所以望而却步,实际上很简单。

环境

这里我用最新的 ubuntu 18.04.2 来配置,首先你得有一个 vps,创建好后最好按照我的 购买了VPS之后你应该做足的安全措施里配置ssh的证书访问。

安装

wireguard 是有为 ubuntu 提供安装包的,但并没有集成在官方源里,所以我们要自己添加 ppa,然后安装。

add-apt-repository ppa:wireguard/wireguard

apt upgrade

apt install wireguard resolvconf -y

配置

进入配置目录

cd /etc/wireguard
 ,执行下面两条命令来生成密钥对:
wg genkey | tee server_privatekey | wg pubkey > server_publickey
wg genkey | tee client_privatekey | wg pubkey > client_publickey

开启流量转发:

echo 1 > /proc/sys/net/ipv4/ip_forward
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p

如果你想添加开机启动,那么:

systemctl enable wg-quick@wg0

使用 

ifconfig
 等命令查看你的网卡信息,找到那个使用外网的网卡,如果你有多个,就选其中一个,用来给wireguard服务监听。

你得到的结果大致如下:

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 128.199.152.999  netmask 255.255.192.0  broadcast 128.199.191.255
        inet6 fe80::bc10:f7ff:feb7:226b  prefixlen 64  scopeid 0x20<link>
        ether be:10:f7:b7:22:6b  txqueuelen 1000  (Ethernet)
        RX packets 23440  bytes 56095614 (56.0 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 9140  bytes 714939 (714.9 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 166  bytes 13998 (13.9 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 166  bytes 13998 (13.9 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

比如这里的例子,显然网卡名称就是

eth0
 ,要记下这个,一会写配置要用到。
echo "
  [Interface]
    PrivateKey = $(cat server_privatekey)
    Address = 10.0.0.1/24
    PostUp   = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
    PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
    ListenPort = 443
    DNS = 8.8.8.8
    MTU = 1420

  [Peer]
    PublicKey = $(cat client_publickey)
    AllowedIPs = 10.0.0.2/32 " > wg0.conf

直接生成配置,你也可以手动编写,不过就需要自己去读取密钥对了,注意名字,不要搞错。另外代码块中高亮的两行,之前我们查看的网卡名,就要写在这里面,如果你的网卡不是

eth0
 ,那么请自行手动替换。

考虑到密钥对都在服务器上,这里我们再顺便生成客户端配置:

echo "
[Interface]
  PrivateKey = $(cat client_privatekey)
  Address = 10.0.0.2/24
  DNS = 8.8.8.8
  MTU = 1420

[Peer]
  PublicKey = $(cat server_publickey)
  Endpoint = 128.199.152.999:443
  AllowedIPs = 0.0.0.0/0, ::0/0
  PersistentKeepalive = 25 " > client.conf

注意高亮行,把远端 IP 改成你服务器的 IP。

启动

至此,配置完成!

# 启动WireGuard
wg-quick up wg0

# 停止WireGuard
wg-quick down wg0

# 查看WireGuard运行状态
wg

就是这么简单~

多用户

首先停止服务

wg-quick down wg0

然后生成新用户的密钥对:

wg genkey | tee client0_privatekey | wg pubkey > client0_publickey

然后在服务端配置中增加,注意这是一条命令,不要分行执行:

echo "
[Peer]
  PublicKey = $(cat client0_publickey)
  AllowedIPs = 10.0.0.3/32" >> wg0.conf

注意高亮行,这个内网 IP 段要写个不同的,每一个 “Peer” 用一个 ip,比如上文用的是 10.0.0.2,这里就用 10.0.0.3,如果还需要更多用户,那么就以此类推。

创建客户端配置文件,这里就和上文中的一样了,只是注意密钥对用新的,这同样也是一个命令,不要分行执行:

echo "
[Interface]
  PrivateKey = $(cat client0_privatekey)
  Address = 10.0.0.3/24
  DNS = 8.8.8.8
  MTU = 1420

[Peer]
  PublicKey = $(cat server_publickey)
  Endpoint = 1.2.3.4:443
  AllowedIPs = 0.0.0.0/0, ::0/0
  PersistentKeepalive = 25 " > client0.conf

然后启动服务:

wg-quick up wg0

参考文献:

一个高速、安全、可以复活被墙IP的VPN —— WireGuard 服务端手动教程

WIREGUARD搭建和使用折腾小记

 

Ubuntu 超快部署 wireguard 服务端,首发于落格博客

df-dferh-01 中国区 Android 安装 Google Play Store 后报错 的 解决办法

$
0
0

落格博客阅读完整排版的df-dferh-01 中国区 Android 安装 Google Play Store 后报错 的 解决办法

在使用 Android 设备时,如果你购买的是一台中国区单独发行的定制版本 Android 设备,那么很有可能你的设备中并不内置谷歌套件,比如 Play Store。

这时候我们就需要自己安装它,安装的办法有很多,这里不再赘述。总之,一但你安装好,就会发现,即使开了代理,Play Store 也有可能在登录后无法正常加载内容(登录是正常的)。

此时会显示错误:df-dferh-01

遇到这个错误,那么多半你的设备是内置了谷歌服务的,且你的代理工具是根据规则进行分流,如果你使用全局的 vpn,那么应该不会遇到这个错误。

遇到这个错误的原因是内置的谷歌服务,是厂商经过定制的,他们会把谷歌的域名设定为 cn 域名,即 googleapis.cn,很遗憾,这个域名会被解析到中国某公司,显然,你的 Play Store 得不到任何数据——于是就报错了。

解决办法

修改你的代理工具规则,加入一条,让 googleapis.cn 走代理即可。

当然,你也可以修改路由器的 dns,把 googleapis.cn 强行解析到 googleapis.com。

df-dferh-01 中国区 Android 安装 Google Play Store 后报错 的 解决办法,首发于落格博客

落格输入法是如何处理按键消息的

$
0
0

落格博客阅读完整排版的落格输入法是如何处理按键消息的

要做一款移动设备上的软键盘,那么怎么处理用户的点击位置,就是你遇到的第一个难题,在这个问题上,我也走了很长的路。

我把落格输入法开发以来的触控逻辑大致分类为三个阶段,现在分别来讲讲设计思路,希望能够对你有所帮助。

第一代触控引擎

显然,对于一个初学者来说,没什么比系统控件更好用的了,功能全,速度也不慢,业务逻辑完善,所以,落格输入法的第一代消息处理就是用的

UIButton
 的
TouchUpInside
 消息。因为一开始我甚至用的是 xib 构建键盘布局,所以直接使用了
@IBAction func buttonTouchUpInside(_ sender: UIButton)
  这样的声明,你一看就懂了,对吧?处理很方便,用户点击了哪个 Button,那么传进来的就是哪个,不需要做额外的判断,系统都帮你搞定了一切。但很快就遇到了第一个问题——按下按钮后程序总要执行一会,于是 UI 就会卡顿,很明显从按下按键到候选出现,会有延迟。

为了避免输入法业务逻辑干扰(实际上是阻挡)UI 更新,我改为在后台处理进一步的消息,实际上你也应该总是这么做——永远不要在主线程处理业务逻辑。

DispatchQueue.global(qos: .userInitiated).async {
            self.input(button:button)
        }

但这样又引入了另一个问题,当用户点击按键间隔太短速度太快时,按键处理的顺序会错乱,于是,我把异步改为同步,这样业务逻辑还是在后台线程处理,但会严格按照调用的顺序依次执行(这确保了用户按键以实际顺序进行处理)

DispatchQueue.global(qos: .userInitiated).sync {
            self.input(button:button)
        }

注意第一行,async → sync。

第二代触控引擎

第一代引擎正常工作了很久,但最终还是遇到了另一个问题:当用户点击更加快的时候,某些后台逻辑处理不正常。

这实际上是由于

UIButton
 自身触控处理机制冲突造成的,当用户点击屏幕键盘速度太快,实际上短时间内同时按下了两个按钮,此时主线程自身是互相阻止的,只有当用户两个手指都离开屏幕,消息才会发送,即
@IBAction func buttonTouchUpInside(_ sender: UIButton)
 被立即调用两次。在极短时间内,两次连续调用,虽然是有严格顺序的,但每个按键消息都会处理候选栏的刷新,这就需要异步更改 UI,这就导致了一些处理逻辑异常——在 UI 刷新完成之前,下一个消息已经开始执行。

解决的思路有两个,要么把业务逻辑的一些判断改为和 UI 不关联,要么想办法让系统能够处理用户同时按下多个

UIButton
 的情况。 ——显然,应该从后者入手,于是,我在开发 落格输入法 X 时做了一个按键缓存机制,它不再使用
TouchUpInside
 ,而是
TouchDown
 和
TouchUp
 :
var pendingKey:UIButton?
@IBAction func keyboardKeyDown(_ sender:UIButton) {
        if sender == pendingKey {return}
        
        if let pending = pendingKey {
            keyboardKeyConfirm(pending)
        }
        pendingKey = sender
    }

@IBAction func keyboardKeyUp(_ sender: UIButton) {
        
        guard let pending = pendingKey, sender.tag == pending.tag else {return}
        keyboardKeyConfirm(pending)
        pendingKey = nil
    }
func keyboardKeyConfirm(_ sender:UIButton) {
        buttonTouchUpInside(sender)
    }

注意

buttonTouchUpInside
 就是上一代的处理逻辑,并没有什么变化,这么写是为了让你明白我的变更思路,这样当用户按下一个键,那么就立即缓存它,当用户再按下一个键,如果已经有缓存了,那么就处理它,并将新的加入缓存;当用户抬起手指,那么就处理缓存的那个按键。如此一来,所有的按键都会得到处理,并且不会被
TouchUpInside
 这个信号阻拦——因为我已经不再使用它了。

由于更改了信号获取源头(从

TouchUpInside
 改为
TouchDown
 +
TouchUp
 ),对用户来说“手感”变化很大。

第三代触控引擎

第二代实际上已经工作的很好,但在 落格输入法 X 上架之前,我们内部测试就发现了另一个问题——实际上并不能说是新发现的,因为它一直都存在,那就是“q”和“p”的问题。在新的 iOS 系统当中,似乎是为了避免和系统手势冲突,iOS 为屏幕边缘的

5
 个像素做了保留处理,当你点击到屏幕边缘的时候(即按按键“q”或者“p”时稍微靠边了点),
TouchDown
 这个消息是不会立即被触发的。

它会被延迟到你抬起手指的那一刻,然后和

TouchUp
 一起发送给键盘。这就导致了用户正常打字的时候,遇到这两个位置,总是会感觉“卡顿”了一下,因为视觉和声音反馈上,确实是延迟到你抬起手指的那一刻而不是按下就立即触发。

如果是在 app 当中,你可以这样做:

override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
        return [.left,.right]
    }

但显然,在键盘中这个代码并不生效。总之,为了让即将上架的 落格输入法 X 更加具有竞争力,我只好硬着头皮继续想办法。

我在尝试了很多方案之后,我终于找到了一个能获取信号的控件——

UITapGestureRecognizer
 。

不是用它来进行标准识别点击——这同样是没用的,必须使用它的

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool
 这个代理方法,只有它能够越过系统屏蔽,正确获得
TouchDown
 调用,而不是被延迟到用户抬起手指的那一刻。

对应地,我又使用

UIView
 本身的
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
 来获取
TouchUp
 行为,如此一来,就可以参考上一代引擎的逻辑实现了:
class TouchLayer:UIView,UIGestureRecognizerDelegate {
    var tapGR = UITapGestureRecognizer()
    init(keyboard:KeyboardViewController) {
        super.init(frame: CGRect.zero)
        tapGR.delegate = self
        self.addGestureRecognizer(tapGR)
    }
    var currentTouch:UITouch?
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let t = currentTouch {
            touchUpInside(t)
        }
    }
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if let t = currentTouch {
            touchUpInside(t)
        }
        touchDown(touch)
        return true

    }
}

其他代码略过不表,这样操作之后,把这个

TouchLayer
 覆盖在键盘上方即可——当然,你也可以在你的
Button
 子类里进行这样的操作,然后单独处理。这里我则盖在键盘上方,进行统一处理了。

为此,我不得不对键盘的业务逻辑进行了一番调整和重构……直到去年年底,我写了一篇文章 落格输入法 X 是如何处理屏幕边缘延迟问题的

总之,这就是现在线上版本 落格输入法 X 在使用的触控逻辑,目前来看,一切良好。如果说缺点,大概就是从二代升级三代代价实在是太大了,这几乎是 落格输入法 X 与 经典版 的重点区别之一了。

 

落格输入法是如何处理按键消息的,首发于落格博客

从 cns11643 中文标准全字库生成仓颉和速成官方码表

$
0
0

落格博客阅读完整排版的从 cns11643 中文标准全字库生成仓颉和速成官方码表

一直以来,我对仓颉速成这对难兄难弟是很有意见的……网上流传的码表版本繁多,但名字都叫“仓颉”,和五笔对比起来,好歹五笔还分个 86 和 98,再新一点还有 新世纪 等等,但仓颉没有,不论什么版本的仓颉,都叫“仓颉”。

这就很尴尬,每个人用的仓颉码表都不一样,但不一样在哪里,他自己也不知道。

总之, 全字库(cns11643) 提供了一种官方的通用的中文编码,本来这个东西是用来弥补 utf8 之类对中文表达能力不足的,就像 gb18030,不过有趣的是它的数据包里还包含了仓颉编码,中文发音、偏旁部首、笔画数量、笔画过程等等数据,所以拿来生成的仓颉码表,应该算是“官方”码表了吧。

转码

下载后数据包中都有说明,找到

Open_Data/MapingTables/Unicode
 目录,里边有三个编码映射文件,我们需要先把编码映射字典处理出来,不然直接用全字库编码是人类不可读的。

比如这样:

15-604E	455C
15-607A	4787
15-6133	93B9
15-6143	93BF
15-6172	9BCF
15-6177	9D64
15-617C	9EBF
15-6253	3C07
15-6278	3F05
15-627B	3F06
15-636E	45F9
15-6377	89B8
15-6434	4953

编码很简单,全字库编码对应 unicode 编码,我们只需要将对应的 unicode 编码转换成人类可读的文字即可,关键语句如下(Python 3):

for line in file.readlines():
    line = line.strip()

    first,last = line.split('\t')
    last = ('\\u'+last).encode().decode('unicode-escape')
    target.write('{}\t{}\n'.format(first,last))

这里注意第五行,我们给 unicode 码前加入

\u
 前缀,让他变成一个 unicode 的 escape,然后所有的 unicode 编码规则的字符串就转换成了“\uffff”的形状,接下来把字符串 encode 成二进制,再用 unicode 解码即可生成人类可读的中文字符了。

生成后就是如下样子:

14-546F	恸
14-5470	恹
14-5471	恺
14-5472	恻
14-5473	恽
14-5474	㦳
14-5475	㧛
14-5476	㧝
14-5477	挘
14-5478	挜
14-5479	挝

当然,也包含了这个样子:

14-237A	ﯪC
14-2422	ﯪ8
14-242B	ﯪ5
14-242E	ﯪ4
14-2430	ﯪ2
14-2432	ﯪ1
14-243A	ﯪ0
14-243B	ﯩF
14-243C	ﯩE
14-243D	ﯩD
14-243E	ﯩC

这些是因为全字库的文本范围比 utf8 表达的范围大导致的,你无法在普通设备上看到这些文字,所以就显示成乱码了。总之,我们也不需要这些字,毕竟大部分设备上也无法显示,简单来说,直接判断字符数量,超过1就跳过就行了。

速成

这样,我们就得到了全字库对应中文字符的表格,现在可以生成仓颉码表了,生成后有 9 万多字,但实际上能显示的还是 2 万左右,同样的道理,去掉无法表达的字符,剩下的就是“官方码表”了。

至于说速成,速成是仓颉的简化,基本规则就是仓颉编码超过两位则取头尾……

即 a→a、ab→ab、abc→ac、abcd→ad……以此类推。

这下,我们就得到了全字库速成和全字库仓颉码表。

授权

全字库是开放授权的,注明出处即可,参考授权声明。任何人都可以在开放平台下载这个字库。

参考文献

  • 全字库官网 https://www.cns11643.gov.tw

从 cns11643 中文标准全字库生成仓颉和速成官方码表,首发于落格博客

禁用 Android File Transfer 自动启动

$
0
0

落格博客阅读完整排版的禁用 Android File Transfer 自动启动

Android File Transfer 是个好东西,可以在 macOS 中方便地给安卓手机传输文件等等,但有一点不好就是这东西会在后台驻留进程来监控usb插入,让人很不爽。

尤其是在插入 Android 设备时,自动弹出窗口,还连接失败(因为你来不及在手机上点授权),十分讨厌。

要禁用自动启动,首先打开 活动管理器,在里边搜索 Android File Transfer Agent,关闭这个进程。

然后到 系统偏好设置→用户和组→登录项(Login Items)中删除它。

你以为这就万事大吉了?还不够!当你启动 Android File Transfer 时,这个 agent 会一同启动,结果就是如果你不重启你的mac,那么 Android File Transfer又会一直弹出。

接下来,我们再去给这个进程改名,一共有两个位置:

/Applications/Android File Transfer.app/Contents/Helpers/Android File Transfer Agent.app

/Users/r0uter/Library/Application Support/Google/Android File Transfer/Android File Transfer Agent.app

 

在这两个目录下,都把

Android File Transfer Agent.app
 改成别的名字即可。

(当然,如果你更新了这个 app,那么所有操作要重新执行一次)

参考文献

Disable auto start for Android File Transfer

禁用 Android File Transfer 自动启动,首发于落格博客

macOS app 实现自动化 notarize 脚本

$
0
0

落格博客阅读完整排版的macOS app 实现自动化 notarize 脚本

根据苹果官方的说明,自 macOS 10.15 起,所有从互联网下载的未进行 notarize 的 app,默认将无法被打开,所以在 App Store 外分发的 app,也必须在发布前将 app 上传到苹果的服务器进行处理。

使用 Xcode 自带 archive 工具可以很方便地进行 notarize,但这个操作无法实现自动化处理,为了方便分发,我将 落格输入法 macOS 2 的分发流程都做成了自动化脚本,现在,只好给脚本添加自动化提交功能了。

命令工具

苹果官方实际上有提供cli 命令,首先你需要运行

xcode-select --install
 来安装支持,然后我们使用
$ xcrun altool --notarize-app --primary-bundle-id "" --username "" --password "" --file ""
 来上传app给苹果服务器;使用
$ xcrun altool --notarization-info -u ""
 来轮询检查处理结果(尽管官方说在一小时内,但一般很快,几分钟就搞定);最后使用
$ xcrun stapler staple ""
 来给文件盖章。

我应该提交哪些文件?

首先,我们要搞明白应该提交哪些(个)文件到服务器,比如,你有个 Great.app 这个编译结果,然后你可能还有一个 Great.pkg,用来给用户安装 Great.app,最后,为了分发方便,你可能还会把这个 pkg 文件放入 Great.dmg 中,这样,我们就有了三个文件:Great.app,Great.pkg,Great.dmg 。

实际上,Great.app 就是个目录,所以,如果你仅仅分发 app,那么你需要将 Great.app 打包成 Great.app.zip,然后再上传到苹果的服务器进行 notarize。

总之,如果你和我一样,一次有这么三个互相包含的文件,那么你【只需要】上传嵌套最多的 Great.dmg 即可,苹果的服务器会自动将你的 dmg 文件打开,取出 pkg,然后取出 app,并为【三者】完成 notarize。

这样,当 notarize 完成,我们虽然没有上传 app 和 pkg,但依旧可以为这两个文件单独完成 staple。

项目设定

实际上,当你在使用 Xcode 自带 archive 进行 notarize 时,它为你完成了很多工作,如果我们自己使用命令,则需要进行额外的配置,打开你的 Xcode 项目,project 的 Build Settings 中,设置代码签名包含时间戳,这是 notarize 必须的操作:

为所有项目签名增加时间戳

另外还有:

去掉debug文件,注意debug模式下不要去掉不然你就不能debug了
去掉debug文件,注意debug模式下不要去掉不然你就不能debug了

上传

xcrun altool --notarize-app --primary-bundle-id "app bundle id" --username "your appleid" --password "one-time-password" --file "Great.dmg" -itc_provider "your team id"  &> tmp

这里有几点要注意,首先上传结果输出到

tmp
 文件供稍后获取查询id,注意使用
&>
 而不是
>
 ,后者无法把输出的内容放入
tmp
 ;

对于

-itc_provider "your team id"
 这个参数,如果你的 Apple ID 下只有一个开发者账号,那就不需要这个参数了,如果你和我一样,Apple ID除了自己的开发者账号,还加入了别人的组,那你就有了多个“
provider
”需要手动指明是上传到哪个,要查看你的
provider
 ,到 App Store Connect,登录后在右上角点击菜单,选择编辑账号信息,你能找到一个叫做“Team ID”的字段,里边的内容就是;

对于

--primary-bundle-id "app bundle id"
 ,就是你app的 bundle id,如果你上传的是 Great.app.zip,那么这个参数不是必须的;

还有就是注意

--password "one-time-password"
  这个参数,要生成一次性密码。

等待并完成

总之,上传成功之后,我们会得到

tmp
 文件最后一行:
RequestUUID = 2EFE2717-52EF-43A5-96DC-0797E4CA1041

依靠这个 UUID,我们可以使用命令来检查 notarize 的状态实现等待,一旦成功,就可以 staple 了。

uuid=`cat tmp | grep -Eo '\w{8}-(\w{4}-){3}\w{12}$'`
while true; do
    echo "checking for notarization..."

    xcrun altool --notarization-info "$uuid" --username "Apple ID" --password "one time password" &> tmp
    r=`cat tmp`
    t=`echo "$r" | grep "success"`
    f=`echo "$r" | grep "invalid"`
    if [[ "$t" != "" ]]; then
        echo "notarization done!"
        xcrun stapler staple "Great.app"
        xcrun stapler staple "Great.dmg"
        echo "stapler done!"
        break
    fi
    if [[ "$f" != "" ]]; then
        echo "$r"
        return 1
    fi
    echo "not finish yet, sleep 2m then check again..."
    sleep 120
done

实际上返回的内容是这样的:

RequestUUID: 2EFE2717-52EF-43A5-96DC-0797E4CA1041
Date: 2018-07-02 20:32:01 +0000
Status: invalid
LogFileURL: https://osxapps.itunes.apple.com/...
Status Code: 2 
Status Message: Package Invalid

但我们只检测文中是否包含

success
 即可,一旦包含,就使用
xcrun stapler staple "Great.app"
 来完成 notarize。

接下来,就是原本的操作了,生成 sparkle 更新包,上传分发。

 

参考文献

macOS app 实现自动化 notarize 脚本,首发于落格博客


为何我的 Fastlane 上传那么慢?

$
0
0

落格博客阅读完整排版的为何我的 Fastlane 上传那么慢?

自动化

使用自动化工具处理 iOS 的 TestFlight 分发,是一件很惬意的事情,它能帮你节省很多时间,减少大量人工干预——尤其是像这种需要等很久才需要人工操作一下的事情,虽然并不会让你觉得很累,但大量的时间就这样浪费掉了。

使用 fastlane 一键编译、上传,然后等待苹果服务器处理完成,全自动发布 TestFlight,本来是一个很棒的操作,但由于网络环境的变化,我的 fastlane 上传速度变得越来越慢,最终慢到 100M 带宽的网络中,上传速度只有 10 KB/s。

重置 itmstransporter

由于 fastlane 实际上是使用 itmstransporter 来进行上传的,那么如果你遇到上传相关错误,可以尝试重置这个工具(当然,在我这个情况下就无法解决问题)

cd ~  
mv .itmstransporter/ .old_itmstransporter/  
"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter"

参考文献:https://stackoverflow.com/questions/47433541/app-store-upload-is-really-slow-and-hogging-my-internet-connection

itmstransporter 的三种传输模式

实际上,itmstransporter 是有三种上传模式的,这也就是为什么你在使用 Xcode 手动上传 app 时,会有一个步骤叫做“协商上传模式”。但显然,这个协商功能没那么“智能”。

如果你依赖 itmstransporter 的自动选择(默认就是如此),那么最快的上传模式会最先被使用,但一旦这个模式失败,itmstransporter 就会转用其他备用方案了(实际上如果再试试,也许还是能用的)

DAV

这个模式是最慢的模式,其实就和直接 http 上传没什么区别(会比web上传更慢一些),如果你的网络质量比较差,延迟高等,那么落到 dav 是没跑的。

但是,有一点,如果你在公司等有防火墙的网络环境,那么 dav 可能是唯一不被封锁的上传方式——它不需要特殊端口。

Signiant

这是一个 SaaS 加速传输协议,官方宣称比 http 传输快 200%,使用这个协议则需要防火墙开启 TCP 和 UDP 的 44001 端口,并且 UDP 攻击防护有可能会对 signiant 造成影响。

Aspera

Aspera 在业内极其出名,很多企业网络、云平台都在使用使用 Aspera 家的 fasp 协议快速传输数据,它在绝大多数网络条件下拥有稳定高速传输,“通常需要 26 小时将一份体积为24GB的文档传输至世界的另一端,而 Aspera 能做到只需 30 秒”。

同样,它也要求防火墙开启 TCP 和 UDP 的 33001 端口,并且 UDP 攻击防护有可能会对 Aspera 造成影响。

配置

我们的目的就是要让 fastlane 调用 itmstransporter 时使用 Aspera 进行上传,避免使用其他两个协议,如果是直接使用 itmstransporter ,你大可以直接设置参数,但使用 fastlane 该怎么办呢?

在你的

upload_to_testflight
 上一行,写
ENV["DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS"] = "-t Aspera"
 即可,这样,就设定了环境变量,让 itmstransporter 只使用 Aspera 进行上传,一旦失败,则不会再使用其他方式进行尝试,这样在上传时,你就会遇到:
The session's status is FAILED and the error description is 'failed to authenticate (19)'

不过,如果你耐心等待,就会发现上传并没有停止,最终,你会得到:

Although errors occurred during execution of iTMSTransporter, it returned success status.

另外,上传的速度也恢复到了巅峰。

如果你真的遇到 Aspera 被防火墙阻止而无法上传的情况,那么就吧 Aspera 改成 DAV。

速度优化

默认来说,Aspera 不会对上传进行限速,也就是说它会尽可能利用你的上传带宽,但这样有一个弊端,那就是上传的时候会导致网络上行带宽被占满,其他人、其他软件都无法联网。所以给 Aspera 增加一个带宽限制是个不错的选择(另外,据说加了带宽限定后速度反而是略有提升的)。

总之,在

"-t Aspera"
 中增加
-k
 参数,后接你期望的带宽,单位是 bit per second,即
-k 10000
 就是 10M 带宽,
-k 200000
 就是 200M 带宽,具体多少合适,这需要你自己去实际测试,这也和你的路由器性能有关。

比如,我要设置 20M 带宽,那么最终的代码就是这样的:

ENV["DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS"] = "-t Aspera -k 20000"

另:

-k
 这个参数对 signiant 模式同样适用,单位和规则都是一样的。

参考文献

Transporter User Guide 1.13

 

为何我的 Fastlane 上传那么慢?,首发于落格博客

使用 Mailgun 创建你的免费域名邮箱

$
0
0

落格博客阅读完整排版的使用 Mailgun 创建你的免费域名邮箱

创建一个网站很容易,但要有一个和自己域名关联的邮箱地址 域名邮箱,似乎比较麻烦。对于中国站长来说,“QQ 域名邮箱” 似乎是唯一的选择,当然如果付费使用,那么使用 Gmail 提供的域名邮箱也是非常好的,可对于个人站长来说,仅仅用于少量验证的 “webmaster@yourdomain.com” 邮箱真的不值得持续为它付费(何况价格还很高)。

现在我们就来试试通过 Mailgun 创建免费的域名邮箱。

注册一个账号

首先,你需要到 https://www.mailgun.com 注册一个账号来使用他们的服务,好消息是新用户只要验证了付款信息,就可以得到免费套餐,并不需要付费。免费套餐每月有 10000 封邮件的额度,对于个人站长来说,太足够了,对吧?

添加路由规则

总之, Mailgun 实际上是一款提供高度灵活 API 的邮件代发服务——当然他们也可以帮你收邮件。现在我们到后台的“Receiving”页面,点击右上角的“Create Route”来添加一条路由,匹配规则选择“Match Recipient”,内容就是你的域名邮箱地址;下方勾选“Forward”,内容就填你现在在用的个人地址,比如一个 Gmail 邮箱。

注意,这里添加转发地址不需要验证,直接就能生效。

Mailgun 添加路由条目
Mailgun 添加路由条目

如上图所示,其他地方均留空默认即可。

底部点击创建后,需要到左侧重新点击“Receiving”才能返回查看列表:

添加好要接受邮件的域名邮箱地址

添加域名绑定

现在,转发规则已经完成,但你还不能直接给这个地址发邮件,因为你的邮局无法找到它,我们到“Sending”→“Domain”页面,点击右上角的“Add New Domain”,输入你的域名裸域,Mailgun 会提示你再考虑一下要不要用子域名,不用管它,其他默认,点击“Add Domain”即可。

添加好了域名,Mailgun 就会给你一些对应的解析条目添加到你的域名解析列表里,这里就不再赘述,按照提示的内容一条一条全部添加即可。添加后回来点击页面底部验证按钮,要点击两次才能成功验证。

添加 Gmail 别名

现在,实际上你已经可以使用你的 Gmail 收取你域名邮箱的邮件了——所有发送给你域名邮箱的邮件都会自动转发给你设定在路由规则里的个人邮箱。

Mailgun 的免费套餐中,10000 额度对应的实际上是发送的数量,收取邮件是不计算的。

不过,如果你想要让你的Gmail以这个域名邮箱的身份发送邮件,我们还要再做一步。

在 Mailgun 的“Sending”→“Domain Settings”页面下,选择“SMTP credentials”标签页,点击右上角“New SMTP User”,用户名填“webmaster”,创建后右上角会提示你一个一次性密码,先不要动,稍后备用。

打开你的Gmail 网页版,选择“Settings”→“Accounts and Import”标签页:

Gmail 添加 Mailgun 发件服务器

选择“Add another email address”,服务器填

smtp.mailgun.org
 ,名字你自己起一个好记的相当于“备注”,用户名就是
webmaster@yourdomain.com
 ,密码就是 Mailgun 页面右上角那个一次性密码,其他默认,点下一步,Gmail 会给你这个邮箱发送验证码——显然,这个邮件会直接转发回你的 Gmail 邮箱,在里边找到验证码回来填入点确定就好了。

当然,你也可以直接点击邮件中的验证链接,没毛病。

这下,你就可以在用 Gmail 发送邮件时选择这个域名邮箱地址进行发送了。

 

参考文献

奇技淫巧——整合Gmail与Mailgun实现免费域名邮箱

 

使用 Mailgun 创建你的免费域名邮箱,首发于落格博客

String.count vs NSString.length

$
0
0

落格博客阅读完整排版的String.count vs NSString.length

通常来讲,Swift 里的

String
  是和
NSString
  桥接的,比如我曾写过 NSString 和 String 究竟 有什么区别 ?,总之这里我们主要来讨论一下,String
count
  和 NSString
length
  到底有什么区别。

String.count

String.count
  实际上是
String.Characters.count
  (Swift 早期版本),Swift 里的
String
 ,早期
count
  属性就是单纯的字符数量,这和
NSString
  是一样的,但很快就加入了扩展字形集群特性,比如一个 Emoji 表情,它的编码长度是普通字符的两倍,但这是视觉上的【一个】字符,于是它在
String
  里算【一个】字符。

比如:

let s = "🐶"

print(s.count)

// result is 1

Note that Swift’s use of extended grapheme clusters for Character values means that string concatenation and modification may not always affect a string’s character count.

注意 Swift 为 Character 值使用的扩展字形集群意味着字符串的创建和修改可能不会总是影响字符串的字符统计数。

NSString.length

对于很有历史感的

NSString
  来说,就没那么复杂了,它就是字符的数组,所以它只会单纯计算字符数量,由于一个 Emoji 就是用两个字符的长度表达的,所以在这里,长度为
2
 :
let s = "🐶"

print((s as NSString).length)

//result is 2

兼容性

如果不注意这个问题,就会遇到很多奇怪的小错误,比如在设定富文本颜色的时候,会导致文本末尾异常,会导致 Emoji 乱码等等。因为富文本是

NSAttributedString
 ,它的长度计算是基于 UTF16 字符长度的,而不是合并后的【视觉】字符长度:
let s = "🐶"

let att = NSMutableAttributedString(string: s)
att.addAttributes([.foregroundColor:NSColor.red], range: NSRange(location: 0, length: s.count))
print(att)

// result is: �{
    NSColor = "sRGB IEC61966-2.1 colorspace 1 0 0 1";
}�{
}

注意高亮行,这里

NSRange(location: 0, length: s.count)
  使用了
String
  的
count
  属性,读出的长度应该是
1
 ,但
NSMutableAttributedString
  是以 UTF16 字符长度做计算,所以字符串长度应该是
2
 ,结果导致为半个 Emoji 字符添加颜色,输出内容为乱码。
let s = "🐶"

let att = NSMutableAttributedString(string: s)
att.addAttributes([.foregroundColor:NSColor.red], range: NSRange(location: 0, length: (s as NSString).length))
print(att)

//result is: 🐶{
    NSColor = "sRGB IEC61966-2.1 colorspace 1 0 0 1";
}

将字符串转换为

NSString
  后获取
length
  则得到了正确结果。

Swift 里的 UTF16 字符串长度

那么,每次使用,都要明显地写成

("" as NSString).length
  的形式吗?虽然写个
extension
  也不是不行,不过,我们其实也可以直接从
String
  原生地获取这个长度:
let s = "🐶"

print(s.utf16.count)

//result is 2

讨论

StringNSString 有很多名称类似但功能相同的方法,但得到的结果却可能并不完全一致,要小心 Swift 对字符串的处理,这些问题往往会在一些细节的地方体现出来,比如输入法移动光标的 API,其中移动

1
  长度,是
1
  UTF16 字符长度,比如 Emoji,计数是 2 而不是 1,如果不注意这个细节,就会导致一些自动化功能在遇到 Emoji 表情或者某些生僻中文字符时计算出错,因为这些符号视觉上是一个字,但实际上使用了可变长的多个 UTF16 字符长度。

 

延伸阅读

https://stackoverflow.com/a/36268059

https://stackoverflow.com/a/29833042

https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html

https://www.cnswift.org/strings-and-characters

String.count vs NSString.length,首发于落格博客

落格输入法 macOS 2 是如何实现免重启激活输入法的

$
0
0

落格博客阅读完整排版的落格输入法 macOS 2 是如何实现免重启激活输入法的

在 macOS 上,安装输入法一直是一个噩梦,要打开系统输入法目录,要把输入法拖拽进这个目录,过程中还要输入密码,全手动也就罢了,还得重启后系统才能识别,真的是让人头疼的不行。

后来大家逐渐意识到这个问题,落格输入法也提供了 macOS 标准的 pkg 安装包。

说是这么说,但实际上似乎还是有办法让系统自动加载输入法的,比如搜狗就做到了免重启,安装后不光不需要重启系统就能识别,甚至安装包还能帮你自动选好搜狗输入法!

他们究竟是怎么做到的不得而知,我也对此进行了我自己的研究。

配置文件

总之,一切都是从配置文件开始的,首先我找到了系统要读取的配置文件,这个文件决定了用户当前激活哪些输入法,选中的是哪个输入法:

~/Library/Preferences/com.apple.HIToolbox.plist

在这个 plist 中,有

AppleEnabledInputSources
 字段,这里边包含了当前都启用了哪些输入法,比如搜狗输入法、落格输入法、系统拼音等等;
AppleInputSourceHistory
 字段则包含了用户的切换历史,似乎和自动切换有关;
AppleSelectedInputSources
 字段则表明了当前用户选中的输入法,比如你当前用落格输入法在打字,那么这里就是落格输入法,如果你切换到了英文键盘,那么这里就会变成英文键盘。

理论上,我们编辑这个文件,就可以改变系统的配置,但显然,这个配置文件不是实时读取的,我们编辑该文件后,用终端命令重启偏好管理系统和 Finder:

killall cfprefsd && killall Finder

运气好的话,就已经 ok 了。

——-事实上,一般你不会运气好。

显然,想要保险的话,还是重启一下才行。总之,这已经是一个进步了,毕竟重启完不需要用户手动添加输入法了对吧?

系统级别 API

那么,能不能有其他更优雅的方案呢?最终我在 Carbon 框架找到了答案:

/*
*===============================================================================
*   Install/register an input source
*===============================================================================
*/
/*
 *  TISRegisterInputSource()
 *  
 *  Summary:
 *    Registers the new input source(s) in a file or bundle so that a
 *    TISInputSourceRef can immediately be obtained for each of the new
 *    input source(s).
 *  
 *  Discussion:
 *    This allows an installer for an input method bundle or a keyboard
 *    layout file or bundle to notify the system that these new input
 *    sources should be registered. The system can then locate the
 *    specified file or bundle and perform any necessary cache rebuilds
 *    so that the installer can immediately call
 *    TISCreateInputSourceList with appropriate properties (e.g.
 *    BundleID or InputSourceID) in order to get TISInputSourceRefs for
 *    one or more of the newly registered input sources. 
 *    
 *    This can only be used to register the following: 
 *    
 *    - Keyboard layout files or bundles in "/Library/Keyboard
 *    Layouts/" or "~/Library/Keyboard Layouts/" (available to all
 *    users or current user, respectively). Such keyboard layouts, once
 *    enabled, are selectable. 
 *    
 *    - Input method bundles in the new "/Library/Input Methods/" or
 *    "~/Library/Input Methods/" directories (available to all users or
 *    current user, respectively). 
 *    
 *    Note: Input method bundles can include private non-selectable
 *    keyboard layouts for use with
 *    TISSetInputMethodKeyboardLayoutOverride. These are registered
 *    automatically when the input method is registered, and do not
 *    need to be separately registered. 
 *    
 *    Security: Any code that calls TISRegisterInputSource is part of
 *    an application or service that has already been validated in some
 *    way (e.g. by the user).
 *  
 *  Parameters:
 *    
 *    location:
 *      CFURLRef for the location of the input source(s), a file or
 *      bundle.
 *  
 *  Result:
 *    Error code: paramErr if location is invalid or the input
 *    source(s) in the specified location cannot be registered;
 *    otherwise noErr.
 *  
 *  Availability:
 *    Mac OS X:         in version 10.5 and later in Carbon.framework
 *    CarbonLib:        not available
 *    Non-Carbon CFM:   not available
 */
extern OSStatus 
TISRegisterInputSource(CFURLRef location)                     AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER;

对于 Swift 来说,也能无痛调用,我们在安装好输入法后,只需要将输入法 app 文件地址传给这个函数,它就可以在系统中注册输入法,这下无需重启就能让系统识别到输入法了:

let url = URL(fileURLWithPath: "/Library/Input Methods/LogInputMac2.app")
let err = TISRegisterInputSource(url as CFURL)
guard err != paramErr else {return}

记得 import Carbon

接下来,我还想让系统自动把我的输入法添加的切换列表里(即激活输入法):

/*
 *  TISEnableInputSource()
 *  
 *  Summary:
 *    Enables the specified input source.
 *  
 *  Discussion:
 *    TISEnableInputSource is mainly intended for input methods, or for
 *    applications that supply their own input sources (e.g.
 *    applications that provide keyboard layouts or palette input
 *    methods, and keyboard input methods that provide their own
 *    keyboard layouts and/or input modes). It makes the specified
 *    input source available in UI for selection. 
 *    
 *    For TISEnableInputSource to succeed, the input source must be
 *    capable of being enabled (kTISPropertyInputSourceIsEnableCapable
 *    must be true). Furthermore, if the input source is an input mode,
 *    its parent must already be enabled for the mode to become enabled.
 *  
 *  Result:
 *    Returns an error code: paramErr if the input source cannot be
 *    enabled, else noErr.
 *  
 *  Availability:
 *    Mac OS X:         in version 10.5 and later in Carbon.framework
 *    CarbonLib:        not available
 *    Non-Carbon CFM:   not available
 */
extern OSStatus 
TISEnableInputSource(TISInputSourceRef inputSource)           AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER;

这个函数调用起来就没那么容易了,主要的难点就是这个

TISInputSource
 没办法直接创建,所以要用到另一个函数来搜索,我们已经在系统中注册了落格输入法,接下来就是在所有的可用输入法中找到落格输入法这个输入源,如果直接穷举所有可用输入源,会很慢(API 原话),我们需要加入条件搜索:
let conditions = NSMutableDictionary()
        conditions.setValue(“落格输入法id”, forKey: kTISPropertyBundleID as String)
        
        guard let array = TISCreateInputSourceList (conditions, true)?.takeRetainedValue() as? [TISInputSource] else {
            return
        }

如无意外,我们就可以拿到落格输入法的输入源了,这里有两个,一个是输入法,一个是用来真正给用户能输入的输入模式(在用户角度来看这两者是同一个,因为落格输入法就只有一个输入模式——中英文模式是这个唯一模式的两个不同状态)。

接下来就是添加了:

TISEnableInputSource(source)
TISSelectInputSource(source)

这下,用户就可以在安装完成后立即在右上角点击切换到落格输入法——运气好的话,连切换都不需要。

讨论

这个方法看上去很美好,似乎从 macOS 10.5 开始就能用,但实际测试来看只有最新的 macOS Mojave 10.14 是有效的,我在 10.13 上测试无效,不论是改配置还是调用系统 API,全都无动于衷,我倾向于是我这个特定系统版本的 bug,因为 API 调用后右上角的图标已经时落个输入法的图标,但选择列表中并没有落格输入法,甚至注册都失败了。

总之,对于 10.14 的新用户来说,安装落格输入法的体验变得极佳。

落格输入法 macOS 2 是如何实现免重启激活输入法的,首发于落格博客

使用 ClamAV 和 Linux Malware Detection (LMD) 保护你的服务器

$
0
0

落格博客阅读完整排版的使用 ClamAV 和 Linux Malware Detection (LMD) 保护你的服务器

本文原创于落格博客,点击查看原文

 

通常,说起病毒木马,人们可能都会想起 Windows,实际上只要是操作系统,就会有漏洞,那么只要这个操作系统有人用,那就一定会有人利用这个漏洞来开发恶意软件(比如说病毒木马)。总之,由于 Windows 操作系统的用户远远大于 macOS 和各种类 Unix,于是很多人就觉得后者甚至是对病毒免疫的。

实际上并不是,虽然 Linux 有着完善的权限管理,但特定的恶意软件依旧有可能入侵——尤其是服务器。

之前我写过一篇购买了VPS之后你应该做足的安全措施,这次我们就一起来看看怎么在服务器上扫描病毒或木马。

CalmAV

CalmAV 是一款开源的的病毒扫描引擎,它有每日更新的病毒特征库,且可以直接从 Ubuntu 源进行安装:

apt install clamav

安装时就已经内置了最新的病毒特征库,不过 ClamAV 还有一个工具叫freshcalm,它用来更新病毒特征库,不过通常你不需要手动执行它,默认它就已经以系统服务的形式启动了,所以你不需要关心病毒库的更新,是全自动增量更新的。

总之,安装好后就可以用命令

clamscan -r --bell -i /home
 来执行一次扫描了,扫描的路径可随意设定,你也可以设置为根目录/从而实现全盘扫描。其中–bell -i表示遇到可疑文件会警报并显示出文件路径。
使用 CalmAV 扫描 /home
使用 CalmAV 扫描 /home

Linux Malware Detection (LMD)

顾名思义,它是专门在 Linux 环境下检测恶意软件的开源工具,尤其是那些 PHP 后门之类的恶意软件,它都能检测出来,另外,如果你按照上文安装了 ClamAV,那么 LMD 就可以直接调用 ClamAV 的引擎快速扫描。

不过 LMD 并没有提供 Ubuntu 源安装选项,我们需要自己从官方下载安装包进行安装:

wget http://www.rfxn.com/downloads/maldetect-current.tar.gz
tar -xvf maldetect-current.tar.gz
cd maldetect-1.6.4
./install sh

注意高亮行,这里版本号要和你实际下载的版本号对应,不然找不到解压缩后的目录。

安装后,就可以执行了

maldet –a
 该命令后可跟要扫描的目录路径,如果不加,就默认是/home,扫描结果类似如下:
使用 LMD 扫描 /home
使用 LMD 扫描 /home

 

使用 ClamAV 和 Linux Malware Detection (LMD) 保护你的服务器,首发于落格博客

Viewing all 377 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>