几年前初次接触 Xcode,就觉得它的默认的代码模板很不好:
- 文件开头都有个注释,包含创建人及创建时间。但实际上这些信息都应该是版本控制系统管理的范畴,不应该写在代码中。
- 中文系统下,根据区域设置的不同,开头的注释里,文件创建时间可能包含汉字「年」。一个汉字夹杂在周围的英文注释里非常不伦不类。
当时毕竟还是 Too Young,刚接触 OS X,照着奇怪的教程跑去 /Applications/Xcode.app
里乱改一气,虽然达到了目的,但总觉得有些怪味道,就不了了之了。
最近终于有机会重新折腾一下 Xcode,就又查了一下相关的内容。不得不说,Xcode 这方面文档真是太不友好了 =,=
自定义文件模板
/Applications/Xcode.app
里是应用自己的原生内容,不应该擅自修改。要修改文件模板,应该把对应的位于 /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates
的 .xctemplate
模板文件夹复制到 ~/Library/Developer/Xcode/Templates
改个名字后,再加以修改。
# 创建文件夹结构(默认没有 Templates 及往后的文件夹)
mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates/Source
# 复制需要修改的 .xctemplate 文件夹
# 这里是在 Source 分类下,将 Swift File 模板复制为 Empty Swift File 模板
cp -R /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Source/Swift\ File.xctemplate/ ~/Library/Developer/Xcode/Templates/Source/Empty\ Swift\ File.xctemplate/
这样,修改 Empty Swift File.xctemplate/___FILEBASENAME___.swift
就可以把讨厌的注释去掉了。
这里没法修改原有模板,也没法覆盖。即便使用与 Xcode 自带模板相同的名称,也只会在新建文件的模板选择对话框里出现两个同名的模板,非常蛋疼。
细枝末节
由于相关文档不足,我又有点强迫症,于是尝试了一些东西,比如上面说的「使用与 Xcode 默认模板相同的名字没法覆盖默认模板」。
上面路径中的 ~/Library/Developer/Xcode/Templates
为用户自定义模板的根目录,这个目录下的 .xctemplate
文件夹会被 Xcode 加载。Xcode 模板中的「分类」与此处 .xctemplate
的父目录有关。
Templates/Foo.xctemplate
:Templates 分类
Templates/Source/Foo.xctemplate
:Source 分类
Templates/File Templates/Source/Foo.xctemplate
:Source 分类
虽然最后两种在效果上没什么区别,但还是推荐使用最后一种,与 Xcode.app
中的目录结构对应起来,会比较方便管理。
另外,使用向导填写模板参数的高级用法需要查找官方 Reference,不过粗略找了一下也没有找到官方说明,很是沮丧 :( 不过反正目前用不到 :)
新项目中第一次开始作死使用 Python 3,在测试中遇到一件奇怪的事情。
系统中有一个邮件发送模块,直接在命令行中手动跑 Worker 的话邮件可以成功发送,而一旦用 supervisor 运行则无法发送邮件。日志中显示在邮件发送库中抛出了如下异常:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce6' in position 0: surrogates not allowed
Surrogate
所以,日志里这个 surrogate 是个啥?
Surrogate 是 Unicode 中位于 BMP 外的一组不会有对应字符的码位,Python 3 中使用这些码位来「代表」无法编码的字节。
如果你明白「锟斤拷」是怎么来的,那么就一定能理解 Python 的 replace
编解码错误处理机制:无法解码的字节会被替换成 U+FFFD
,无法编码的码位会被替换成 ?
。这样做有一个明显的缺点就是不可逆。Python 3 中新增的 surrogateescape
则是一种可逆的错误处理机制,利用 Surrogate 码位保存无法解码的字节,编码时则将其还原为对应的原始字节。
例子中的报错正是因为 0xE6
无法使用 ascii
解码,而被解为了 U+DCE6
。
'\udce6'.encode('ascii', 'surrogateescape')
>>> b'\xe6'
b'\xe6'.decode('ascii', 'surrogateescape')
>>> '\udce6'
知道了原因,用这种方法解码异常信息中的完整字符串就找到了罪魁祸首:邮件发送者的配置。
Python 3 的环境变量与编码
邮件发送者是使用 显示名称 <邮箱地址>
的格式在环境变量中设置的,显示名称部分包含了汉字。
Python 3 中,使用 os.getenv
或者 os.environ
获得到的环境变量的值都是 str
类型。在类 Unix 系统中,解码过程是使用 sys.getfilesystemencoding()
和 'surrogateescape'
错误处理来实现的。而 sys.getfilesystemencoding()
的取值与 nl_langinfo(COODESET)
相关,即 LC_CTYPE
环境变量。
所以,导致这部分环境变量包含 Surrogate 的原因就是 LC_TYPE
指定的编码不正确。
当环境变量 LC_CTYPE
、LC_ALL
、LANG
均没有显式定义时,各个操作系统有各自的 LC_CTYPE
的缺省值(比如 C
)。这种情况下,通过 sys.getfilesystemencoding()
通常会得到 ascii
,环境变量中包含非 ASCII 字符就会得到上面的 Surrogate。
真正的问题
到了这里,问题根源和解决方法基本上已经水落石出了。
前面说手动跑 Worker 可以但是 supervisor 跑 Worker 就不行,这是因为被 supervisor 管理的子进程并不会单独开 Shell 而是继承 supervisord
的 Shell,所以基本的环境变量也有不同。
解决方法便是在 supervisor 配置中修改 environment,加入 LC_CTYPE
或者 LC_ALL
或者 LANG
环境变量的配置。
附,LANG
、LC_ALL
、LC_TYPE
什么的之间到底是个啥关系:查找 LC_X
的值的过程是:LC_ALL
有值就用 LC_ALL
的值;否则 LC_X
有值就用 LC_X
的值;否则 LANG
有值就用 LANG
的值;否则就用操作系统的默认值。
以上。
参考文档
标题剧透预警,然而已经晚了 :)
事情是这样的:这两天在写某 Flask App 的用户模块,访问邮箱验证邮件里的链接时,SQLAlchemy 在 db.session.add(user)
时有一定几率抛出异常:
AssertionError: A conflicting state is already present in the identity map for key (, (UUID('12345678-1234-1234-1234-123456789abc'),))
重现模式也很奇怪,同一个链接,首次访问时几乎都是异常,而刷新一下重新访问,一切就平平稳稳通过了。但是如果开新的浏览器(新的隐身窗口)访问,似乎无论是不是首次访问都没有问题。
问题是什么
断言内容直译是「目前的 Identity Map 中已存在与之冲突的状态」。
SQLAlchemy 是数据库和 Python 的中间层,Identity Map 即数据库对象与 Python 对象的映射表。我们之所以可以做这样的判断:
new_user = User(id=42)
db.session.add(new_user)
user = User.query.get(42)
assert user is new_user
就是 Identity Map 的功劳。
如果试图往 Identity Map 里加入两个不同的 Python 对象,但这两个 Python 对象都映射到同一个数据库对象,自然就不科学了,因为打破了其中的不变量。
foo = Model(id=42)
db.session.add(foo) # OK
bar = Model(id=42)
db.session.add(bar) # Fail
问题的成因
系统中有一个 get_user_by_id
的函数用以从数据库查找用户。这个函数额外加了个自己实现的 @cache
装饰器,用 Redis 做了缓存。
发送验证邮件时,因为其它部分的逻辑对用户信息做了修改,导致缓存被清空。于是下一次对 get_user_by_id
的调用一定会走数据库。
点击邮件中的验证链接后,访问验证页面。由于在 app.before_request
中插入了用 Session 中的 user_id
通过 get_user_by_id
获取用户信息的操作,做了数据库访问, Identity Map 中就保存了这一份 UserModel
,暂且称作 A。
正式的验证页面逻辑,需要从验证链接中提取到这条链接对应的 user_id
,继而通过 get_user_by_id
获得对应的 UserModel
,此时的 UserModel
是从 Redis 中反序列化而来的,暂且称作 B。
A 和 B 虽然指向同一个数据库对象,但其实是不同的 Python 对象。通过 db.session.add
把 B 加入 Session,就会与 Identity Map 里原有的 A 发生冲突。
所以第二次访问该链接,由于都是从 Cache 加载的对象,Identity Map 一直是空的,就不会有如何问题;打开新浏览器,由于没有登录,也不会触发 app.before_request
中的数据库操作,所以也不会有问题。
问题的解决?
有一个解决方法,是新开一个 get_user_by_id_for_update(user_id)
从数据库加载,与此同时,正好可以在该函数内部给数据库请求加上 with_for_update()
给该记录加锁。
其它的选项还包括:
- 在
@cache
里为带 Cache 的函数都加上 force
参数的支持
- 个人觉得,
force
参数的「跳过缓存」语义暴露了实现细节,相比之下, get_model_by_id_for_update
的「修改」语义比它高到不知道哪里去了
- 统一把
update_model(model, **kwargs)
改成 update_model(model_id, **kwargs)
的形式,在内部做数据库查询
- 个人觉得,将
update_model
改为接受 model_id
的做法则有点本末倒置的意思
还是用 merge
更好啦
距离原来这篇博客发出已经一年多了,期间一直在遵循着上面所说的解决办法。但最近因为需要自己为 SQLAlchemy 封装 Write-through Cache 的轮子,又仔细阅读了 SQLAlchemy 的文档,发现在出现此种状况也可以选择使用 session.merge(obj, load=False)
将对象直接放入本 session 的 identity map 中。
session.merge
会首先根据对象的主键在 identity map 中查找对象,如果没有找到,则从数据库中加载一个进来。然后,会将传入对象的状态同步给 identity map 中的对象。
添加 load=False
参数,就会跳过「从数据库中加载一个」的步骤,直接在 identity map 中新建一个对象。由于对对象的状态有严格要求,官方文档 中只推荐从缓存中加载时使用。不过这正是我们所需要的,哈哈。
以上。
最近终于找到本有兴趣的书啦,就是这本《C++ 并发编程实战》。我会找一些自己觉得没想到、好玩、或者有自己想法的地方整理下。
顺带吐个槽:In Action 系列的封面人物真心诡异,还是 O'reilly 萌萌哒的奇怪可爱小动物看着顺心。
join()
的异常安全
std::thread
要求在析构函数被调用时,该对象必须没有与之相关联的线程,否系统分分钟自尽给你看(调用 std::terminate()
)。
一般情况下,我们主要需要注意的地方是析构前对 join()
或者 detach()
的调用。如果要分离线程,线程启动后就可以立即调用 detach()
,基本不会带来什么问题。而 join()
则没法立即调用,这样就会带来可能会被忽视的异常安全问题:线程启动后,如果某处抛出异常,可能把本应执行的 join()
跳过,导致程序自行退出。
void foo()
{
std::thread t(thread_func);
do_something(); // 如果抛出异常,就可能把下面的 join() 跳过
t.join();
}
把 do_something();
用 try
/ catch
包裹起来,并在 catch
里调 t.join()
最后把异常重新 throw
出去的方案固然可行,但对于 C++ 而言,更好的方案是 RAII:
class thread_guard
{
public:
explicit thread_guard(std::thread& t)
: thread_(t)
{
}
~thread_guard()
{
if (thread_.joinable()) { // join() 前对象必须满足 joinable
thread_.join();
}
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
private:
std::thread& thread_;
};
void foo()
{
std::thread t(thread_func);
thread_guard g(t);
do_something();
}
当然你也可以考虑把 std::thread
移动进去而不是传引用,书里管那种东西叫 scoped_thread
,调用时不必为 std::thread
对象单独开一个名字。
线程传参二三事
给线程函数传参是通过 std::thread
的构造函数完成的,其原理与 std::bind
类似。所有参数先在当前线程被复制 / 移动到对象内部,然后在新线程中完成对线程函数的调用。
这两步的划分会导致会被一些人说「坑」(虽然实际上是他们自己没注意)。
比如从「被复制」到「实际调用线程函数」之间可能并不连续,那么被复制的参数届时是否还有效就可能成为问题,例如:
void thread_func(std::string const& s);
void foo(int bar)
{
char buffer[1024];
sprintf(buffer, "%d", bar);
std::thread t(thread_func, buffer);
t.detach();
}
前面说过构造函数仅负责原样复制 / 移动参数,所以 char *
到 std::string
的转换是在新线程中的第二步时做的,于是例子中 t
对象构造函数暂存的其实是指向栈上的 buffer
的指针。而由于 detach()
,foo
返回后可能新线程还未正式启动。如果 thread_func
真正被调用时 foo
已经返回,那么这个指针参数就无效了。
一个解法是把代码改成 std::thread t(thread_func, std::string(buffer));
这样构造函数就会把实参 std::string
移动进去(即便在 std::string
无法移动只能复制的平行宇宙,这样做也是安全的)。
前几周周末百无聊赖,决定利用周末开始一个开发 Android 微博客户端的漫长路程。主要是为了熟悉下 Android Studio 和「更 Android 的网络通信方式」。这周末由于公司项目的事情,没什么空搞这货,只给时间线加了一个下拉刷新的功能。
SwipeRefreshLayout
光是下拉刷新的话,其实用不着第三方库,安卓自带就有一个工具类 SwipeRefreshLayout
,全名唤作 android.support.v4.widget.SwipeRefreshLayout
,可以用作一个可滚动的视图(比如 ScrollView
、ListView
)的容器,从而为这个视图提供下拉刷新功能。而且理论上根据系统的不同会有不同的表现形式,比如早期的就是 ActionBar 下边缘的横线动画,Material Design 后则是下拉的小圆形动画。
首先在 Gradle 中需要加入对 Support Library v4 的引用:
compile 'com.android.support:support-v4:21.0.0'
当然如果你的工程中已经间接引用了的话也可以不写,比如 v7 就依赖于 v4。
顾名思义,SwipeRefreshLayout
是一种 Layout,可以直接在 XML 文件里用,比如:
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listview_timeline"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
此时,如果把 listview_timeline
拉过头,就会触发 SwipeRefreshLayout
的 onRefresh
事件,并在界面上显示加载动画。
通过为 SwipeRefreshLayout
设置 setOnRefreshListener
,即可监听刷新事件,重新加载数据。加载完成后,调用 setRefreshing(false)
即可终止刷新动画。
当然写完你就会发现这个刷新图标默认是纯黑色箭头,和我们在 Google+ 以及最近更新的 Chrome 里看到的画风不一样啊。实际上用 setColorSchemeResources
方法就可以为这个动画设置一系列颜色了。
Otto 的跨线程调用
因为我的客户端是在 Service
中请求时间线的,即在 Activity 中触发下拉刷新后,是直接开个新的 Service
去做网络请求。如果要跨组件传递「刷新完成」这样的消息,作为一个新手的我感觉会非常复杂。于是就想起来用 Otto。
Otto 是 Square 公司开源的一个事件总线库(Event Bus)。顺带说一句 Square 公司(不是 Enix 的那个 Square ;))贡献了很多优质精巧的 Android 开源库,比如 Picasso 和 Retrofit。
用起来大致就是先全局定一个 Bus
对象,比如写一个单例 BusProvider
;再为每个事件定义一个类,比如 TimelineUpdatedEvent
(空类即可);最后就可以在事件生成处——比如 Service
里——写:
BusProvider.getInstance().post(new TimelineUpdatedEvent());
而在事件接收处——比如 Activity
里——写:
@Override
public void onPause() {
super.onPause();
BusProvider.getInstance().unregister(this);
}
@Override
public void onResume() {
super.onResume();
BusProvider.getInstance().register(this);
}
@Subscribe
public void onTimelineUpdated(TimelineUpdatedEvent event) {
// 时间线已更新,停止刷新动画
}
然而光这样做却有一个问题:Otto 为了保持简洁,要求所有 post
发生在主线程中,而在 Service
中调用显然不在主线程,从而会导致抛出异常。所幸我们可以用 Android 的 Handler
和 Looper
在 Otto 默认的 Bus
周围再封装一层:
public class MainThreadBus extends Bus {
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public void post(final Object event) {
if (Looper.myLooper() == Looper.getMainLooper()) {
super.post(event);
} else {
handler.post(new Runnable() {
@Override
public void run() {
MainThreadBus.super.post(event);
}
});
}
}
}
思路很简单,post
前先检查自己是否在主线程,如果不是,让关联到主线程的 Handler
去调用原来的 post
。
- «
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- ...
- 15
- »