知乎 · 热度

热度 · 共 216 条 · 第 1/8 页
回答2016-01-14

想写个 App 练手,有什么有趣的 API 接口推荐吗?

因为国内并没有什么有趣且透明免费的接口,所以只能祭出 Fiddler +dex2jar + jd-gui 大法。别说 Web Service API,连 so 库接口我都逆向过。

可以参考下:

[微票儿 APP 接口逆向](http://nekocode.github.io/Weipiao Apk Decompile/)[老司机 APP 逆向](http://nekocode.github.io/Sextube Decompile/)

所有逆向出来的接口,原则上只可用于学术研究,不可用于任何其他用途。

答主可以尝试下找几个简单有趣的内容类 APP 尝试逆向接口,并 build 个第三方客户端。

例如逆向「知乎日报」的 API:

https://github.com/izzyleung/ZhihuDailyPurify/wiki/%E7%9F%A5%E4%B9%8E%E6%97%A5%E6%8A%A5-API-%E5%88%86%E6%9E%90 m

当然,更有趣的是自己写个 backend:

http://zhuanlan.zhihu.com/kotandroid/20488077

❤️ 466💬 51
文章2016-07-26

Android 开发者们,Intel 搞了大新闻啦!

新闻新闻~Intel 最近搞出了个大新闻啦,Reddit 下已经开始火热讨论起来了:

什么鬼?Intel 也开始搞跨平台开发了?不过看这名字 Multi-OS Engine 感觉应该挺牛扳的。

?,来看看官网的介绍:

Time-saving, productivity technology to create Android* and iOS* apps

  • Use Java* coding to deliver native performance app
  • Ahead of time compilation to native code
  • Build, debug and deploy with Android Studio* integration

这意味着 可以使用 Java 开发 iOS 应用!并且能在在 Android Studio 中进行构建甚至调试应用!

OK,赶紧申请测试。

申请完测试后会提供插件的下载链接。下载并安装完插件后重启 Android Studio 就可以在菜单新建 Multi-OS Engine Project 工程了:

随便创建了个测试项目,但是我并没有看到所谓的:

  • Design native iOS* UI with extended Android Studio* UI designer capabilities

难道还在开发中?还是说我的使用姿势不正确??

点运行按钮会自动编译并在虚拟机中运行:

结语

整体走了一遍开发流程,体验还算 OK,在 iOS 虚拟机上也能流畅运行。不过看到生成的执行文件有 100 多 M 就有点萎了= =。

有人说还不如 Xamarin。不过说实话,WP 开发者也就那一丢丢人,切换 IDE 的成本还可以接受,但是一提到要切换语言那可不好商量啊。而 Intel 这次主动来亲和 Android 开发者说不定反而真能搞起来,基本和 Android 开发保持无缝的体验啊。

总而言之,可以期待 Release 版。

补充

官方 Sample 代码放上 Github 了 ,目前有 Java 和 Kotlin 版本的。从一些 Sample 中我们可以窥探到,我们可以把业务逻辑等可复用的代码写在一个 common lib 中,然后可以在 Android 和 iOS 中共同调用。

❤️ 432💬 100
回答2016-05-19

Python 的练手项目有哪些值得推荐?

好吧,三十多个人关注我,没几个点赞的,。

---邪恶的分隔符---

你试试关注我 :) 你会收到一条来自 Python 脚本发送的私信哦~

没用到啥难的东西,很适合练手:

GitHub - nekocode/zhihuSayHi: Say Hi to your new followers in Zhihu.

不过,。。首先你得懂点反编译:

#Decompile# 搞搞知乎 Beta 版~ - 『Android 还可以这样开发』 - 知乎专栏

❤️ 331💬 36
回答2015-12-19

你见过最美的程序是什么?

安利一下我大

Shadertoy

!上面都是搞 GE、Shader 和算法的大神们!

对显卡性能有信心的可以点开上面的链接测试 fps :)

里面很多还是可以转化为产品的(OpenGL,WebGL,OpenGL ES)

例如上面这个我把它移植到 OpenGL ES 了

❤️ 305💬 21
回答2015-11-27

你见过哪些令你瞠目结舌的 Android 代码技巧?

它在本地开了个后台 Http 服务器,用于监听本地(或外部)的 Http 请求。

这有一个很流氓的用处,就是只要 WebView 对本地 Http 服务器发个 Ajax 请求,就可以让百度 SDK 做些 Native 操作(启动,安装应用等)。

发散一下思维,这样就可以在微信内部浏览器打开外部应用了。例如知乎 APP 只要在本地开个 Http 服务器监听 Ajax 请求,然后知乎网页版往 localhost 发一条 Ajax 请求,本地 Http 服务器接受到 Ajax 请求就打开 APP 客户端,这样就可以实现微信内部浏览器(或其他 WebView)跳转到知乎 APP 客户端了。。

这样子来修改 View 的属性

textView.text = "hello"

这样子来处理事件

override fun onTouch(v: View, event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> showToast("ACTION_DOWN ")
        MotionEvent.ACTION_MOVE -> showToast("ACTION_MOVE ")
    }
    return true
}

这样子来使用 intent

val intent = intentFor<OtherActivity>(
    "model" to Model(5, 0),
    "str" to "xxx"
)
intent.singleTop()
startActivity(intent)

仅仅几行代码就定义了一个拥有单个 Fragment 的 Activity

public class MainActivity : SingleFragmentActivity() {
    override val fragmentClass = TestFragment::class.java

    override val fragmentBundle = {
        null
    }
}

这样子来储存本地数据(持久化)

Storage["test"] = Model(5, 1)
  • 常用灰度调色板(不算膛目结舌,但是很常用)

使用 iptables 截获连接到本热点的手机的请求报文(http,https,dns)并选择性进行转发。这里的用处就是可以实现各种页面劫持,例如可以提供一个登录网页,连接到该热点的用户必须要登录后才可以继续使用网络。

  • 各种热上线,热补丁技术

安卓App热补丁动态修复技术介绍 - MAGI的专栏 - 知乎专栏

mmin18/AndroidDynamicLoader · GitHub

alibaba/AndFix · GitHub

❤️ 276💬 27
回答2015-12-13

如何防止 Android App 被反编译后接口泄露?

混淆是无法混淆代码中的字符串的,所以混淆并不能防止 URL 接口地址泄露。其次,URL 接口地址泄露是无可避免的,抓下包就知道地址了,不需要反编译。

另外,题主你的目的有问题,不应该是如何隐藏接口地址,而应该是如何提高接口安全(例如如何避免接口通讯内容被泄露),根据我多次抓包以及反编译国内一些比较出名 APP 的经验,总结出以下几点能提高接口安全的方法:

  • 使用 HTTPS 进行通讯,提高通讯内容被第三方捕获的难度
  • 对通讯内容进行加密
  • 加解密算法使用 C/C++ 实现,通过 JNI 进行调用

记得 Bilibili 和 虾米音乐 都是使用 HTTPS 与服务端进行通讯的。

而如果题主真的连 URL 路由(理解为接口的 Action)都要隐藏的话,有两种做法:

  • 后端只提供一个接口,前端使用 Post Request - Response 的方式通讯,接口 Action 放在 Request Body 中,然后对 Body 进行加密
  • 干脆就别用 HTTP 协议了,用 Socket 实现自定义协议吧~

❤️ 249💬 30
文章2018-07-19

知乎安卓客户端启动优化 - Retrofit 代理

背景

知乎 Android 客户端作为一个比较大型的应用,由于功能不断地迭(zeng)代(jia),启动速度也会受到影响,为了提升用户体验,知乎移动平台团队把提高 App 启动速度定为了的一个长期而且重要的 OKR,于是我们在今年的第二季度,重点对客户端的启动做了一系列的优化。

虽然在性能优化相关领域我们还处于刚开始的阶段,但是在优化过程中我们还是总结出了一些经验可以拿出来分享。所以,今天我们来分享其中一次和 Retrofit 相关的优化经历。

开始之前

我们在做性能优化时,很多人可能苦恼于怎么去检验或者说量化最终优化的效果。这里面其实是有学问的,通常我们会选用系统输出的信息来作为指标,例如众所周知的用 GPU 渲染柱状图来作为 UI 是否卡顿的指标。为什么要选系统输出的信息?一来采集更方便,不需要自己写代码去测量,二来是我们还能采集到其他 App 的信息,方便与其他竞品做横向对比。

而启动速度的话,我们会选用系统打印的 Activity 启动 Log 作为指标:

107-11 15:09:32.519 1440-1502/? I/ActivityManager: Displayed com.zhihu.android/.app.ui.activity.MainActivity: +1s412ms (total +1s978ms)

Log 数据的含义可以看 Stack Overflow 上的这个 回答 ,我们主要取 App 冷启动到第一个 Activity 显示出来的时间。

言归正传,在这次优化开始之前,为了能和最终的数据做对比,我们需要先测出 App 优化前的启动时间。通过对 App 进行多次冷启动并记录 Log 里的 Total duration,得出了 App 在优化前的平均启动时间为 1.905s(数据来自 OnePlus 3T):

Log 输出

发现 & 分析问题

我们想要找究竟是哪些代码导致启动变慢,光靠 Review 代码是做不到的,因为一段代码的执行效率除了受各种内因(CPU / IO 操作密集,锁…)的影响,还有可能受其他外因(系统资源争夺、GC…)的影响。所以想要快速准确地找出问题,最好的方法是让代码真正执行起来,然后去 Profile(监测)代码运行时的情况。

而 Profiling 其实是一门很大的学问,需要用到大量的工具,包括 Android 系统、SDK、甚至 IDE 提供的一些接口或工具,熟练运用这些工具去分析和发现性能问题是做性能优化的必备技能。而针对今天知乎 App 启动的 Profiling,我会简单介绍其中一部分的工具:

Method Tracing

Method Tracing,就是跟踪 App 某段时间内所有调用过的方法,这是测量代码执行性能常用的方式之一,我们可以通过它来查出 App 启动时具体都调用了方法,都花了多长时间。

这个功能是 Android 系统提供的,我们可以通过在代码里手动调用 android.os.Debug.startMethodTracing() 和 stopMethodTracing() 方法来开始和结束 Tracing,然后系统会把 Tracing 的结果保存到手机的 .trace 文件里。详情可以看 官方文档

此外,除了通过写代码来 Trace,我们也有更方便的方式。例如也可以通过 Android Studio Profiler 里的 Method Tracer 来进行。但是,针对 App 的冷启动,我们则通常会用 Android 系统自带的 Am 命令来进行,因为它能准确的在 App 启动的时候就开始 Trace:

# 启动指定 Activity,并同时进行采样跟踪
adb shell am start -n com.zhihu.android/com.zhihu.android.app.ui.activity.MainActivity --start-profiler /data/local/tmp/zhihu-startup.trace --sampling 1000

当 App 冷启动完毕后(首个 Activity 已经绘制到屏幕上),使用以下命令手动终止跟踪,并拉取 .trace 文件到本机的当前目录下:

# 终止跟踪
adb shell am profile stop
 
# 拉取 .trace 文件到本机当前目录
adb pull /data/local/tmp/zhihu-startup.trace .

拿到 .trace 文件之后,下一步就是进行可视化了。可以直接拖动 .trace 文件到 Android Studio 里打开,但 Android Studio 目前版本对 .trace 文件的可视化和交互做得还不怎么样,所以不推荐。在这里推荐使用 Android Device Monitor 里的 Traceview 来打开,详情可以查阅 官方文档

用 Traceview 打开之后的截图:

Traceview 截图

所有跟踪到的方法会默认按照实际总耗时从多到少向下排序,点击某个方法可以看到它的所有父调用方法和子调用方法。通过从上往下一条条排查,可以看到在知乎 App 启动时 UserInfoInitialization 类的 initUserInfo() 方法竟然耗时超过 600ms:

initUserInfo() 方法

159 (0x68f8) 方法

我们来看看这个 initUserInfo() 方法的代码:

private void initUserInfo() {
    NetworkUtils.createService(ProfileService.class)
            .getSelf(AppInfo.getAppId())
            // ...
            .subscribe(response -> {
                // ...
            }, Debug::e);
}

NetworkUtils.createService() 是我们自己封装的一个方法,内部调用 Retrofit 来获取 ProfileService 的动态代理类,而 getSelf() 则是动态代理类提供的一个方法。从 Traceview 中可以看到,159 (0x68f8) 这个是运行时生成的代理方法,所以可以判断这里的耗时是因为调用 getSelf() 引起的。

继续往下跟踪:

responseBodyConverter() 方法

_newReader() 方法

可以看出来,最终主要的耗时在 Jackson 库的几个方法上,而 Jackson 库是我们是用来给 Retrofit 序列化和反序列化数据(例如 Response Body)用的。

接下来就是深入追查原因了。通过查看 Retrofit 的代码可以知道,在第一次调用 Retrofit 的某个动态代理方法时,Retrofit 会新建一个 ServiceMethod 实例来储存该代理方法相关的一些数据,包括一个用来转换 Response Body 的 Converter。而在新建这个 Converter 的时候,则会根据 Body 反序列化结果对应的 Java Model 类来生成一个 ObjectReader,如果对应的 Java Model 类特别复杂,那么新建 ObjectReader 的时间也会特别长(内部会进行一堆反射操作)。而恰恰我们 getSelf() 方法返回的 Java Model 是一个字段极其多的类,所以造成第一次调用该代理方法时特别耗时。

问题的原因我们已经通过 Method Tracing 找到了,接下来我们可以通过另外一个工具来看看问题发生时,App 和系统的真实情况。

Systrace

Method Tracing 虽然是找出耗时方法的利器,但是执行 Method Tracing 时会严重拖慢 App 的执行速度,即便使用采样跟踪,测量得到的结果和实际结果肯定还是有很大偏差,只能作为参考。而且 Method Tracing 对锁、GC、资源匮乏等其他因素的追查显得十分无力。所以,我们可以借助另一个 Google 官方极力推荐的工具 -「Systrace」来跟踪 App 实际运行时的情况。详细介绍可以查看 官方文档

Systrace 的原理是通过 Android 系统自带的 atrace 工具来捕获系统以及 App 的一些关键信息,然后通过 Chrome 浏览器来进行可视化。

接下来,我们将通过 Systrace 来跟踪知乎 App 在启动时执行 Retrofit 代理方法的整个过程。首先,我们需要做一些准备:

  1. 为了让结果更接近真实情况,我们需要为 Release 包开启 App Tracing 的功能,详情可以查看 这里
  2. 给 Retrofit 的 loadServiceMethod() 方法添加 Tracing Section(PS:可以借助 JarFilterPlugin 来修改 Retrofit 的内部代码):
ServiceMethod<?, ?> loadServiceMethod(Method method) {
    Trace.beginSection("Retrofit:" + method.getName());
    // ...
    Trace.endSection();
    return result;
}
  1. 添加 Proguard 规则,保证 method.getName() 获取到正确的方法名:
-keepclassmembernames class * {
    @retrofit2.http.GET *;
    @retrofit2.http.POST *;
    @retrofit2.http.PUT *;
    @retrofit2.http.DELETE *;
}

做完准备后用 Gradle 命令打出一个上线标准的 Release 包并安装上手机,然后就可以开始用 Systrace 来跟踪了:

systrace -a com.zhihu.android app view res am sched dalvik

开始跟踪后,冷启动 App,等到首个 Activity 可见之后点击回车结束跟踪。跟踪结束后 Systrace 会把跟踪结果保存到当前目录下的 trace.html 文件,使用 Chrome 打开后:

Chrome 截图

通过 Chrome 可以很直观地看到 App 在启动时的各种关键的 Section。此外,我们还能看系统 CPU 每个时期的使用率以及每个核心上执行的线程:

CPU 使用率和每个核心上执行的线程

通过选定某个线程上面的线程状态条,我们可以查看某段时间或 Section 里线程的运行状态:

线程状态

从上图可以看出,在「Retrofit:getSelft」这个 Section 里,UI Thread 基本都是 Running 状态,说明 getSelft() 内部执行的都是 CPU 密集的操作,很少进入等待状态。这也说明了 getSelft() 方法的耗时是实打实的,不是由于其他原因(例如锁等待)造成。而且有一点需要注意,这里测出来的实际耗时是 250 多毫秒,而 Method Tracing 测量出来的耗时是 650 毫秒,由此可以看出 Method Tracing 的测量结果误差还是很大的。

接着我们通过搜索「Retrofit:」关键字可以看到,在启动期间会不止一次调用到 Retrofit 的 loadServiceMethod() 方法:

搜索结果

而且有很多还是在 UI 线程上调用的,但是其实这些操作没必要放在 UI 线程上,我们需要想办法把所有 loadServiceMethod() 的调用都扔到其他线程上。

解决问题

耗时的原因已经找到了,而优化的思路就是把 loadServiceMethod() 的调用扔到非 UI 线程上。例如,针对 getSelf() 这个方法,我们可以使用 Observable 的 defer() 操作符把它放到非 UI 线程上去异步执行:

private void initUserInfo() {
    Observable.defer(() ->
            NetworkUtils.createService(ProfileService.class).getSelf(AppInfo.getAppId())
                    .subscribeOn(Schedulers.io())
    )
            .subscribeOn(Schedulers.single())
            // ...
            .subscribe(response -> {
                // ...
            }, Debug::e);
 
}

这是针对单处地方的改法,但要知道,启动的时候不单单只有这一处会调用到 Retrofit 的 loadServiceMethod(),要把所有地方都加上 defer() 操作符的化修改量有点大。所以有没有更好的办法呢?

答案就是二次动态代理:

public final class Net {
    public static <T> createService(Class<T> service) {
        // ...
        return createWrapperService(mRetrofit, service);
    }

    private static <T> T createWrapperService(Retrofit retrofit, Class<T> service) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(),
               new Class<?>[]{service}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, @Nullable Object[] args)
                throws Throwable {
                if (method.getReturnType() == Observable.class) {
                    // 如果方法返回值是 Observable 的话,则包一层再返回
                    return Observable.defer(() -> {
                        final T service = getRetrofitService();
                        // 执行真正的 Retrofit 动态代理的方法
                        return ((Observable) getRetrofitMethod(service, method)
                                .invoke(service, args))
                                .subscribeOn(Schedulers.io());
                    })
                            .subscribeOn(Schedulers.single());
                }
                // 返回值不是 Observable 的话不处理
                final T service = getRetrofitService();
                return getRetrofitMethod(service, method).invoke(service, args);
           }
           // ...
        });
    }
}

原理就是再创建一层动态代理,然后把底层 Retrofit 代理方法的调用包进新的 Observable 里再返回。这么改的好处是可以一劳永逸,让所有调用的地方无需做任何更改,减少了代码的修改量。

经过这么优化之后,我们重新测试了一遍启动时长,最终得出优化后的启动时间大概快了 400ms。可以看出,这次优化是成功的。

结语

我们这次优化经历到这里就结束啦,可以看到我们整篇文章里的「发现 & 分析问题」这一章占的篇幅是最大的,这其实也反映了我们在做性能优化时的常态,大多数时间都会花在 Profiling 上,当遇到优化瓶颈时,更可能花大量时间去通过各种工具抽丝剥茧地分析问题。

其实上面也讲到了,Profiling 是一门大学问,本次分享只是挑了些比较简单但是关键的方式和工具来讲,实际在进行 Profile 的时候会用到更多的知识,有时候甚至需要自己写工具去辅助定位问题。希望本篇文章能起到抛砖引玉的作用,让大家能了解到做启动优化时的一些常用思路。此外,该次只是我们优化知乎 Android 客户端启动速度的其中一次经历,知乎移动平台团队还在不断地为优化启动速度做努力~也希望未来还能分享更多的经历给大家。

由于本人的水平有限,如有错误和疏漏,欢迎各位同学指正。

另外,知乎移动平台团队也在招人中,欢迎各位小伙伴的加入,和我们一起做一些酷事情!具体招聘信息在这里 https://app.mokahr.com/apply/zhihu#/job/7b1b32c2-f30c-4638-93ce-09c2ac9a52d8

❤️ 176💬 32
文章2016-08-25

#Android# 你还在写麻烦的 Adapter 么?

这期文章,推荐一个类库 ItemPool ,它能很大程度上减少你的 Adapter 数量,它是一个解耦实现,能把 ViewHolder/ItemView 解耦出来。请记住,这让所有 ItemView 变得可复用。

前言

自从 Android 的 RecyclerView 组件发布以来,RecyclerView 成为了 Android 开发中实现容器视图的首选。要实现一个 RecyclerView,必须为其提供一个 RecyclerView.Adapter。我们来看看一个普通 Adapter 的写法:

public class CommonAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final List<String> list;

    public CommonAdapter(List<String> sampleData) {
        this.list = sampleData;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View item = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_test, parent, false);
        return new ViewHolder(item);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof ViewHolder) {
            ((ViewHolder) holder).bindData(list.get(position));
        }
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private TextView text;

        public ViewHolder(View itemView) {
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.textView);
        }

        public void bindData(String data) {
            text.setText(data);
        }
    }
}

这段代码已经十分简单了,但它目前只包含一种类型的 ItemView,当我们需要更多不同的 ItemView 时,需要添加新的 ViewHolder 并更改 onCreateViewHolder() 和 onBindViewHolder() 函数下的代码。

这暂且来说还没有什么大碍。但是当你要实现另外一个类似的 Adapter 时,假设里面有一些在旧 Adapter 中已经出现过的 ItemView,你会发现你没有办法复用之前写过的代码,你只能重新定义一个 Adapter 类并复制旧的 ViewHolder 代码到新的 Adapter 上。这执行起来十分机械,并且当某个 ItemView 发生更改时,你需要同步更改所有对应 Adapter 中的 ViewHolder。

我们需要做点工作把 ViewHolder 代码从 Adapter 中抽离并独立出来,而 ItemPool 能帮你完成这点工作。

ItemPool

使用 ItemPool 后,我们把写 ViewHolder 改变成了写 Item。不同的 Item 用来将不同类型的 Data 映射到相应的 ItemView 上。更重要的是,这些 Item 是可复用的,这意味着你在另外一个 RecyclerView 中也能直接使用该 Item。一个经典的 Item 如下:

public class TestItem extends Item<String> {
    TextView textView;

    @NonNull
    @Override
    public View onCreateItemView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        View itemView = inflater.inflate(R.layout.item_test, parent, false);
        textView = (TextView) itemView.findViewById(R.id.textView);
        return itemView;
    }

    @Override
    public void onBindItem(@NonNull final RecyclerView.ViewHolder holder, @NonNull String s, ItemEventHandler eventHandler) {
        textView.setText(s);
    }
}

上面除去 IDE 自动生成的代码,实际上仅仅需要自己写 5 行代码。这个 Item 通过 onCreateItemView() 函数来生成某个 ItemView,并挂钩了 String 这个数据类型,然后通过 onBindItem() 函数来将 String 类型的数据传递给 ItemView 进行视图更新。

好的,接下来我们可以给某个 RecyclerView 组装 Item 了,它看起来是这样的:

ItemPool items = new ItemPool();
items.addType(TestItem.class);
items.addType(TestItem2.class);

items.add(new Header());
items.add("A");
items.add("B");

items.attachTo(recyclerView);

可以看出来我们并不要再写 Adapter 了,甚至也不用额外定义一个 DataList 了,它通过以下的工作流程来生成对应的 ItemView:

简而言之就是将指定类型的数据用指定类型的 Item 来展示。这十分容易理解,你只要记住哪个 Item 对应哪个数据类型,并往 ItemPool 中塞数据就行了。

更详细的描述可以移步 README 文档。

末尾

当然,它并不是万能的,一些情况下依然推荐使用 Adapter 实现。但不管怎样,大多数情况下你都能用它,并且为你减少几杯咖啡的工作时间~

(≡ω≡.)

❤️ 153💬 22
文章2016-05-28

数库科技 Android 开发准则

该份开发准则来源并作用于数库科技 Android 开发组,转载请标明出处。欢迎对 使用 Kotlin 进行工作有兴趣的开发者联系我并投递简历(也招 iOS、Web 前后端、设计)~文章内容首发于我的 Git Blog (知乎的排版实在太烂了,要仔细看的话还是建议到 Github 上看 =_=),并将长期在 Github 上进行维护。

总览

设计

  • 定义好调色板,所有颜色 只能 从调色板中获取,任何地方都应该避免硬编码
  • 图标可以考虑使用流行图标字体库(Fontawesome)
  • 开发前对一遍设计稿,定好所有 Dimen,尽量使用 Dimen 板
  • Multiple State(多状态) Drawable 命名规则:android-selector-chapek
  • 设计规范越完整,开发越容易工作

开发

  • 必须写注释(行内注释,函数注释以及类注释等),Doc Gen 选用 Dodoka
  • 善用 TODO,FIXME 进行标注
  • 必须写单元测试(V/M 层都需要)
  • 使用 CI(持续集成)进行远程构建
  • 使用 Lint 工具进行代码静态检查
  • 收集各种常用 Lib (log, bugly, …)
  • 可以添加多一层 Lib 层,用来对大多数第三方库进行一层包裹(Wrap),方便日后更换或拓展
  • 所有基于事件响应的场景尽量使用 Rx 来实现,包括 View 的事件响应(可参考 ReactiveAndroid
  • 所有调试用的 Log 请用使用 Debug 作为 Flag 进行输出,Release 环境下必须使用混淆去掉所有 Log 的代码
  • 上架前必须进行 混淆 和 签名
  • 使用 Redex 等工具对 Dex 文件进行优化(也可使用 (redex-plugin)[https://github.com/timmutton/redex-plugin])
  • 使用 Nimbledroid 进行应用性能分析
  • 适当使用依赖注入(常用的模块,需要单元测试的模块)
  • 使用 Fragment 来构建页面内容,使用 Activity 来管理 Fragment
  • 尽量使用 Anko DSL 来创建视图

架构

MVP,Flux/Redux。请参考 Kotgo

层次流程

  • View -> Model -> Presenter:View 和高复用性的 Model 同时开发,Presenter 最后开发。
  • 前期 View 层开发需要用到的数据全部使用 ViewObject,需要什么属性就定义什么属性,以后在 Presenter 层进行DO 到 VO 再到 View 的 Convert(转换)过程。(VO 中可以使用 data: Any 属性携带 DO)
  • DO 到 VO 的转化过程请不要在 UI 线程进行操作。(可以在 Presenter 中使用 Rx 的 Map 操作在非主线程调度器上进行转换)
  • 所有 VO,DO 只能保存在 Presenter 层内,View 层最多只能保存 VO 的引用!(另外要注意,Adapter 应当放在 Presenter 层内)
  • View 层不能接触 Model 层的任何数据和接口!
  • 页面跳转 放到 Presenter 层中。
  • View 和 Presenter 之间是双向依赖,所以通过接口解藕,便于进行 UI Mock 测试,而 Presenter 和 Model 是单向依赖,可以直接编写单元测试来测试 Model。

Flux 的一些思想

  • FP 的思想很适合前端:Rx 在 Android 领域的火爆验证了这一点(对事件或数据的流加工)
  • Pure function:Function 不影响外部变量(不产生副作用),且给定输入,输出不变。
  • Map,Reduce:对数据的流处理,任意流都可以通过 Map&Reduce 加工成任意流。

Git 协同守则

  • 拉取 dev 分支到本地 Liveneeq 文件夹
mkdir Liveneeq
cd Liveneeq
git init
git remote add -t dev -f origin git@git.thecampus.cc:onecampus/liveneeq-android.git
git checkout -b dev origin/dev
  • 每个人建立带下划线的自己全名的分支,例如 _yangfan
git checkout -b _yangfan
  • 在该分支上进行开发,定期进行 Commit(可使用 tmp 前缀来表示临时提交),确保代码在云端
git add .
git commit -m "tmp 3/18 ***"
git push origin _yangfan

git add .
git commit -m "tmp 3/19 ***"
git push origin _yangfan
  • 完成阶段性功能或页面后,使用 rebase 或者 reset 重建 Commit 历史,确保所有 tmp commit 被合并删除
git rebase -i <COMMIT_HASH>
# ...
git rebase --continue
  • 需要提交到 dev 分支时,需要针对 dev 在个人分支上进行 Rebase 操作,并处理冲突
git fetch
git rebase origin/dev
# ...
  • rebase 完成后 在本机进行构建和测试,测试通过后使用 -f 参数强制 Push 到远程分支
git push -f origin _yangfan
  • 在 Gitlab 上提交 Merge Request(/Pull Request) 到 dev 分支,等待 Master 进行 Code Review

Notice

  • 每次 Commit 要保证粒度足够细,包含的更改和描述一致,且可编译运行
  • 提交 PR 前如果确保当前分支在 dev 分支 HEAD 处的话可以不进行 Rebase
  • dev 分支将处于 protected 状态,非不得已要执行 force push 的话,要提交通知所有开发成员

编码准则

参考并修改自 Android-Best-PracticesAndroid-Guideline

Kotlin 源代码

对类文件使用 驼峰命名法 。包名使用 小写连写 ,单词较多可以使用 _ 分割符。

Property 定义与命名规范

对 Property 的定义应该放在文件的首位,另外请注意 Kotlin 可视修饰符和 Java 的不同 ,并且遵守以下规范:

  • 要注意 Kotlin 的默认可视修饰符为 public
  • Kotlin 的 internal 修饰符,可以让目标对象只在同一 Module(IDE 下的 Module) 下可访问(例如创建一个插件 Module 的时候,可以使用 internal 对外隐藏一些实现细节)
  • 静态常量命名字母全部大写,单词之间用下划线分隔,且必须使用 const val 修饰符
  • Android SDK中诸如 SharedPreferences,Bundle 和 Intent 等,都采用 key-value 的方式进行赋值,当使用这些组件的时候,key 必须被 private const val 所修饰,并以 KEY_ 作为前缀。
  • Android 下的组件以及控件尽量以 类型 的缩写小写字母作为前缀,例如以下一些可选的前缀(可依此类推):

示例:

internal class TestActivity: Activity() {
    compainion object {
        const val CONSTANT: Int = 0
        private const val KEY_ARG_TITLE = "title"
    }
    val title: String = "Title"
    var listSize: Int? = null
    private var frgHomepage: Fragment? = null
}

Kotlin 语言相关

  • 理解好 Kotlin 中的 Function 类型 ,理解 inline 和 infix 修饰符,掌握 Kotlin 中的 ExtensionsDSL 的定义 ,领悟 Function 在 Kotlin 的地位(第一公民)。
  • 看完并理解 stdlib
  • ByteArray、ShortArray、IntArray 等并不继承于 Array,它们在 Jvm 中表现为 byte[]… ,所以应该更倾向于选择它们。
  • 使用 Any 而不是 Object。(注意 Lint 的提示,也会建议使用 Any)
  • 用好 Pair 和 Triple 来避免某些情况新建类。
  • 使用好注解:@Deprecated(标注不推荐的对象)、@ReplaceWith(标注能进行替换的代码块)。
  • 注意好 Throwable、Exception 和 Error 的区别,对于可捕捉的错误应该使用 Exception 而不是 Throwable。
  • 理解好 apply()、let()、with()、to()、repeat() 的糖用法。
  • 使用 val localA = A!! // or checkNotNull(A) 将 Nullable 变量转换为 NotNull 类型的 Local Scope 变量。

Log 输出规范

使用 Log 类打印一些重要的信息对开发者而言是很重要的事情,切记不要使用 Toast 来做信息打印。

VERBOSE 和 DEBUG 类型的 Log 不应该出现在 Release 版本中,INFORMATION、WARNING 和 ERROR 类型的 Log 可以留下来,因为这些信息的输出能够帮助我们快速地定位问题所在,当然前提是,需要隐藏重要的信息输出,如,用户手机号,邮箱等。

只在 Debug 环境中输出日志的小技巧:

if (BuildConfig.DEBUG) Log.d(TAG, "The value of x is " + x)

类成员排序规范

关于这个并没有硬性要求,不过好的排序方式,能够提高可读性和易学性。这里给出一些排序建议:

  1. 常量
  2. 字段
  3. 构造函数
  4. 被重写的函数(不区分修饰符类型)
  5. 被 private 修饰的函数
  6. 被 public 修饰的函数
  7. 被定义的内部类或者接口

资源文件(Resources)

  • 资源等 .xml 文件应该采用 小写字母_下划线 的组合形式,并遵循前缀表明类型的习惯,形如 type_name.xml。
  • res/values 目录下的文件可以任意命名,但前提是该文件能够明确表达职责所属,因为起作用的并不是文件本身,而是内部的标签属性。(例如你可以定义 strings_home.xml、colors_home.xml 之类的)

Lyout 相关

  • 布局(Layout)文件命名方式:

布局文件应该与 Android 组件的命名相匹配,以组件类型作为前缀,并且能够清晰的表达意图所在。基本规则如下:

值得一提的是,一些布局文件需要通过 Adapter 填充,如 ListView,Recyclerview 等列表视图,这种场景下,布局的命名应该以 item_ 作为前缀。另外还有一种比较常见的情况,一个布局文件作为另一个布局文件的一部分而存在,或者使用了include,merge 等标签的布局,可以使用 partial_、include_ 或者 merge_ 作为前缀,这一类布局的命名同样应该清晰的表达其意图。

  • Id 命名方式:

控件 Id 的命名应该以该控件类型的缩写作为前缀,和 代码中的控件名保持一致

对于如何排版一个布局文件,请尽量遵循以下规范:

  • 每个属性独占一行,缩进四个空格
  • android:id 作为第一个属性存在
  • 如果存在 style 属性,则紧随 id 之后
  • 如果不存在 style 属性,则 android:layout_xxx 紧随 id 之后
  • 当布局中的一个元素不再包含子元素时,另起一行,使用自闭合标签 />,方便调整和添加新的属性
  • 善用 IDE 的 Reformat Code 功能,尽量在编辑完 XML 文件后进行格式化

示例如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/tvTitle"
        style="@style/FancyText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        tools:text="This is title."
        />

    <include layout="@layout/partial_header" />

</LinearLayout>
  • 避免层级冗余的嵌套。

Layout 结构优化方面,应尽量避免深层次的布局嵌套,这不仅会引发性能瓶颈,还会带来项目维护上的麻烦。在书写布局之前应该对 ViewTree 充分的分析,善用 标签 减少层级嵌套,或者使用 Hierarchy Viewer 等 UI 优化工具对 Layout 进行分析与优化。可参考 Optimizing Your UIOptimizing Layout Hierarchies

Style、Theme 相关

Style 与 Theme 的命名统一使用 驼峰命名法 (首字母大写)。使用多个 Style 文件而不是全部写在 styles.cml 里,如:style_home.xml,style_item_details.xml,styles_forms.xml 等。

几乎每个项目都需要适当的使用 Style 文件,因为对于一个视图来说有一个重复的外观是很常见的。在应用中对于大多数文本内容,最起码你应该有一个通用的 Style文 件,例如:

<style name="ContentText">
    <item name="android:textSize">@dimen/font_normal</item>
    <item name="android:textColor">@color/basic_black</item>
</style>

应用到 TextView 中:

<TextView
    style="@style/ContentText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/price"
    />

你或许需要为按钮控件做同样的事情,不要停止在那里。将一组相关的和重复的属性放到一个通用的 Style 中。

对于控件的 android:layout_xxx 等属性应该在 Layout 中定义,同时其它属性 android:xxx 应放在 style 中。核心准则是保证 Layout 属性(position, margin, size 等)和 content 属性(text, src 等)在布局文件中,同时将所有的外观细节属性(color, padding, font)放在 Style 文件中。

使用 Designtime Attributes(tools 标签)

  • 布局预览应使用 tools:xxx 相关属性,避免 android:text 等硬编码的出现,具体可参考 Designtime Attributes 。示例如下:
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:text="Home Link" 
    />

Drawable 相关

  • 常规 Drawable(图像)文件命名方式:

  • 常规 icon(图标)文件命名方式:

  • 常规 selector states(选中状态)文件命名方式:

要注意的是,Selector 的一些状态是可以叠加的,所以可以产生 btn_order_disabled_focused.9.png 这类命名。

永远使用 android-selector-chapek 这个插件来生成相应的 Selector Drawable XML 文件,而不应该手工创建。

Color 相关

colors.xml 文件就像个 “调色板”,只映射颜色的 ARGB 值,不应该存在其他类型的数值,不要使用它为不同的按钮来定义 ARGB 值。应该像下面:

<resources>
    <!-- grayscale -->
    <color name="white"     >#FFFFFF</color>
    <color name="gray_light">#DBDBDB</color>
    <color name="gray"      >#939393</color>
    <color name="gray_dark" >#5F5F5F</color>
    <color name="black"     >#323232</color>

    <!-- basic colors -->
    <color name="green"     >#27D34D</color>
    <color name="blue"      >#2A91BD</color>
    <color name="orange"    >#FF9D2F</color>
    <color name="red"       >#FF432F</color>
</resources>

对同一色调,不同色域进行定义时,像 “brand_primary”、“brand_secondary”、 “brand_negative” 这样的命名也是不错的选择。

值得一提的是,这样规范的颜色很容易修改或重构,App 一共使用了多少种不同的颜色变会得非常清晰。

Dimen 相关

我们应该像对待 colors.xml 一样对待 dimens.xml 文件,与定义颜色调色板无异,也应该定义一个规范字体大小的 “字号板”。

一个很好的建议:

<resources>

    <!-- font sizes -->
    <dimen name="font_larger">22sp</dimen>
    <dimen name="font_large">18sp</dimen>
    <dimen name="font_normal">15sp</dimen>
    <dimen name="font_small">12sp</dimen>

    <!-- typical spacing between two views -->
    <dimen name="spacing_huge">40dp</dimen>
    <dimen name="spacing_large">24dp</dimen>
    <dimen name="spacing_normal">14dp</dimen>
    <dimen name="spacing_small">10dp</dimen>
    <dimen name="spacing_tiny">4dp</dimen>

    <!-- typical sizes of views -->
    <dimen name="button_height_tall">60dp</dimen>
    <dimen name="button_height_normal">40dp</dimen>
    <dimen name="button_height_short">32dp</dimen>

</resources>

同样的,在定义 margin 和 padding 时,可以使用 spacing_xxx 作为前缀对其命名,而不是像对待 String 字符串那样直接写值。这样写的好处是,使组织结构和修改风格甚至布局变得非常容易。

String 相关

String 命名的前缀应该能够清楚地表达它的功能职责,如,registration_email_hint,registration_name_hint。如果一个 Sting 不属于任何模块,这也就意味着它是通用的,应该遵循以下规范:

❤️ 142💬 16
回答2021-08-14

使用React hooks如何只让下面这段代码的子组件只render一次?

作为给 ant design 贡献过代码的人,可以告诉你 antd 内部是用的 forwardRef 和 useImperativeHandle 来实现这个效果。这也是目前仅通过内部 hooks 来实现的最正确的做法。

看了一圈这个问题下的回答,只有 @maxcalibur 回答到这两个 function,但是却一个赞都没(黑人问号)。其他人要么直接说没法实现,要么通过各种绕弯子来实现,。。

另外拓展一下,用我这个基于观察者模式共享 state 的库来做也是可以实现的:

GitHub - nekocode/use-shared-state: React hook for sharing state or notifying event between components. Just like the widget controller in Flutter.

去看一下我写的 readme 下的 live example 就知道了,目前已在几家大大小小的公司中投产了。

还有一些其他不算方法的方法:

  1. 子组件直接用父组件传的 props 而不是用内部自己维护的 state 来渲染,然后通过 callback 反馈给父组件来刷新传进来的 props 也是可以的,但是子组件一定要自己维护 state 的情况就不能这么用了。
  2. 再另外,父组件渲染时改变一下子组件的 key 来强制重新创建 dom 也是可以实现题主要的效果的,只不过性能上会更差(狗头)。

那些说没法实现的不是被打脸么。

========= 08/17 更新 =========

感谢 @Dcab 和 @Liuyl 在评论区的讨论。@maxcalibur 的答案里之所以组件 B 没包 memo 也能最终只渲染一次,是因为 react 框架的 auto batchupdate 将两次 setState 优化为只 update 一次父组件了。

另外补充一下,forwardRef 和 useImperativeHandle 是用来将子组件的内部状态或方法挂载到 ref 上(通常在父组件创建)。

而要实现父组件刷新时子组件不跟随刷新的话,目前比较好的选择有:

  1. 用 memo 方法包裹子组件,然后确保传给子组件的 props 不变。
  2. 将子组件的创建(createElement)放到父组件的 render function 外,这也是 constate 这个库的基本原理之一,举个例子:
// 仅为大概,其余细节请自行脑补补齐
const A = React.forwardRef((_, ref) => {
  useImperativeHandle(ref, ...);
});
const B = React.forwardRef(({ children }, ref) => {
  const onClick = () => { ref.update() }
  return <>
    {children}
  </>
});
const App = () => {
  const ref = useRef(null);
  return <B ref={ref}>
    <A ref={ref} />
  </B>
}
❤️ 139💬 18
文章2016-01-11

How to use Python to build a RESTful Web Service

由于知乎目前限制单人仅能开通单个专栏,所以关于文章主题的所有文字都会写在该单篇文章中(避免污染专栏),目前处于长篇连载且停滞状态,待续。。

Github Repo: nekocode/tornaREST · GitHub

Preface

我是一名 Android 开发工程师,我在用 Kotlin 和 Java 写着 Android 应用,可是我也很喜欢 Python,我用它来写一些网页应用、工具。这次,我打算使用 Python 实现一个提供 RESTful Service 的后端,它能为各种前端提供 API。

Python 是一门很强大、很容易入门的语言,但是它不是一门简单的语言,它在语法上有很多可以玩的 trick(虽然很多情况下你并不需要掌握这些 trick)。它很强大,你能通过它用更少的时间来做更多的事情,你能花更多时间关注更高层的东西(业务逻辑)。『人生苦短,我用 Python』,当然我也相信,大多数对某种语言有信仰的人,都是用着该门语言在做合适的事情。Java、Ruby、C/C++ 也是如此。

Framework

Tornado + MongoDB + Redis

Tornado

它实现了一个高效的非阻塞异步 IO 的网络模型,另外一些明星级别的工业产品(Fackbook & 知乎)也选择了它。它不像 Django 一样庞大,但是它提供了构建一个基础 Web Server 所需的大多数工具,并且得力于异步 IO 的支持,它很适合作为一个 RESTful API Server 的中间件。

MongoDB

它是新兴 NoSQL 领域的一批黑马,我对于 SQL 数据库并没有太多的经验,所以我更愿意尝试新领域的产品。我认为 MongoDB 比起 MySQL 有更吸引我的点:

  • 更高的性能。
  • 宽松的结构,更容易拓展。

NoSQL 是一场技术革命运动,已经有很多 Startup 选择使用 MongoDB。Why not have a try?

Redis

它是一个高性能的 Key-Value 类型内存数据库(也支持对数据进行持久化)。它很适合对一些使用量高,实时性要求高数据进行缓存。我将用它来储存 Token,消息队列以及 Feeds。

API Doc

一份详细的 API 文档是必须的。我使用 apidoc 来为代码自动生成 API 文档,另外我自己维护了一个分支 nekocode/apidoc ,它修复了一些问题(issue #394 ),以及把接口测试器改为使用 urlencode 进行 POST 和 PUT(默认是使用 JSON)。

详细的效果可以查看 [我跟进的一个社交后台的 Doc](http://nekocode.github.io/apidoc sample/)。你也可以使用 Slate 来生成文档,它更加漂亮,但是不像 apidoc 一样提供接口测试器。

Collections

MongoDB 使用 Collection 来储存同一类别的数据,类似于 SQL 中的 Table。现在,我们首先为我们的 Backend Service 添加一系列最基础的用户操作接口。

我使用了 MotorEngine 来作为 MongoDB 的 ORM 框架,他是针对 在 Tornado 使用 MongoEngine 的一个 Port,使其能够异步访问 MongoDB。我首先通过它来定义一些需要的数据库对象:

class BaseDocument(Document):
    def to_dict(self):
        data = super(Document, self).to_son()
        data['id'] = self._id
        return data

class User(BaseDocument):
    mobile = StringField(required=True)
    password = StringField(required=True)
    followers = ListField(ReferenceField(reference_document_type='collections.User'), default=[])
    create_time = DateTimeField(required=True, auto_now_on_insert=True, auto_now_on_update=False)

    # ...

    def to_dict(self):
        data = super(User, self).to_dict()
        del data['password']
        return data

注意,我们首先定义了一个 BaseDocument,并添加了一个 to_dict 方法,它将 Document 对象转换为一个 dict,并将隐藏的 ObjectId 添加入字典,方便向客户端返回 JSNO 对象时的序列化。

这里的获取到的 dict 不能直接用 json 的 dumps 进行序列化,可以借助 bson.json_util 进行序列化,但是它把一些 BSON Object 包裹起来序列化,这样输出的数据很丑,我们还是模仿着 bson.json_util 自己实现一个 json_util 来进行序列化吧(因为篇幅我隐藏了一些代码):

def _json_convert(obj):
    if hasattr(obj, 'iteritems') or hasattr(obj, 'items'):  # PY3 support
        return SON(((k, _json_convert(v)) for k, v in obj.iteritems()))
    elif hasattr(obj, '__iter__') and not isinstance(obj, string_types):
        return list((_json_convert(v) for v in obj))
    try:
        return default(obj)
    except TypeError:
        return obj


def default(obj):
    if isinstance(obj, ObjectId):
        return str(obj)
    if isinstance(obj, datetime.datetime):
        if obj.utcoffset() is not None:
            obj = obj - obj.utcoffset()
        millis = int(calendar.timegm(obj.timetuple()) * 1000 +
                     obj.microsecond / 1000)
        return millis
    if bson.has_uuid() and isinstance(obj, bson.uuid.UUID):
        return obj.hex
    raise TypeError("%r is not JSON serializable" % obj)


def dumps(obj, *args, **kwargs):
    return json.dumps(_json_convert(obj), *args, **kwargs)

主要对 ObjectId/ datatime/ UUID 对象进行处理。

Router & BaseHandler

我们需要一个 BaseHandler 来封装一些基础的操作(例如格式化输出)。为了保持输出的一致性,我们还需要捕获一些异常,我们需要在路由设置上使用一些小技巧:

url = [
    (r'/api/user/login', LoginHandler),
    # ...
    (r'.*', APINotFoundHandler),
]

我们需要定义一个 APINotFoundHandler 用来捕获一些未定义路径的请求,并通过 JSON 以及 Http Status Code 返回 404 错误信息给用户。

而 BaseHandler 的部分代码应该是这样的:

class BaseHandler(RequestHandler):

    def __init__(self, application, request, **kwargs):
        RequestHandler.__init__(self, application, request, **kwargs)
        # TODO: REMOVE?
        self.access_control_allow()

    def access_control_allow(self):
        self.set_header('Content-Type', 'text/json')
        # 允许 JS 跨域调用
        self.set_header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
        self.set_header("Access-Control-Allow-Headers", "Content-Type, Depth, User-Agent, Token")
        self.set_header('Access-Control-Allow-Origin', '*')

    def get(self, *args, **kwargs):
        raise HTTPError(**errors.status_0)

    # 这里因为篇幅省略了复写 post/put/options/delete

    def options(self, *args, **kwargs):
        # TODO: REMOVE?
        self.write("")

    def write_error(self, _status_code, **kwargs):
        # TODO: REMOVE?
        self.access_control_allow()
        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
            # in debug mode, try to send a traceback
            lines = []
            for line in traceback.format_exception(*kwargs["exc_info"]):
                lines.append(line)
            self.finish(dumps({
                'reason': self._reason,
                'traceback': ''.join(lines)
            }))

        else:
            self.finish(dumps({
                'reason': self._reason,
            }))

    def write_json(self, data):
        self.finish(json_util.dumps(data))

    def is_logined(self):
        if 'Token' in self.request.headers:
            token = self.request.headers['Token']
            logined, uid = validate_token(token)

            if logined:
                # 已经登陆
                return uid

        # 尚未登陆
        raise HTTPError(**errors.status_2)

    @staticmethod
    def vaildate_id(id):
        if id is None or not ObjectId.is_valid(id):
            raise HTTPError(**errors.status_3)


class APINotFoundHandler(BaseHandler):
    def data_received(self, chunk):
        pass

    def get(self, *args, **kwargs):
        raise HTTPError(**errors.status_1)

    # 这里因为篇幅省略了复写 post/put/options/delete

    def options(self, *args, **kwargs):
        # TODO: REMOVE?
        self.access_control_allow()
        self.write("")

另外我使用了一个 errors 文件来储存所有的错误码以及对于的 Reason:

status_0 = dict(status_code=405, reason='Method not allowed.')
status_1 = dict(status_code=404, reason='API not found.')
# ...

还有些细节日后再聊,晚安~

❤️ 135💬 13
回答2017-01-26

你写过什么有趣的程序?

初中时候的班主任是语文老师,人特别苛刻,我和那时候的挚友都十分讨厌她。于是我突发奇想,用 vb6 写了个定时触发主板 beep beep 响的 exe 放到课室电脑的启动项中。

结果就是一上语文课电脑就会 bb 响,老师怎么调音量都还是会响(因为是主板发声)。但是碍于要放 ppt 只能任它响一整节课……

后来玩了几节课后我就把它删了

❤️ 95💬 26
文章2016-04-26

#Android# 来谈谈 App 的 Cool-start

前言

关于在用户冷启动 App 时更友好的交互(安装后第一次运行/进程被杀掉第一次运行),有两种比较正确的做法。一种最流行的做法是使用 Splash Screen 来过渡应用初始化的时间段。

这也是比较简单的一种做法,但是很容易让 App 造成割裂感,虽然仅仅在冷启动的时候才会显示,但是对于不常驻留后台的应用,每次冷启动时,真的挺令人反感的。讲真,Splash Screen 是最令我反感的交互设计之一。

所以我推荐直接进入 App 主页面,而不是使用 Splash Screen ,但是如何处理好冷启动 UI 阻塞时的交互呢?

Google 官方推荐借助 Starting Window(默认开启)。默认配置下系统通常在点击 App Launcher Icon 后显示一个 Starting Window(Preview Window)来在应用初始化期间展示给用户(当作点击反馈)。

但是应该注意的是,默认 Starting Window 的背景是纯白色的,我们需要对 windowBackground 属性进行修改才能实现上面的效果。

正言

动画效果:

我建议你预览上面的动画效果。我最早在 MaterialColdStart 看到利用 9Patch 来设置背景,但是它有些繁杂(需要手工生成 9Patch 图)。我在 Kotgo 的 Dev 分支中使用 Layer-list 实现了效果,它是这样的:

<!-- drawable/bg_startingwindow.xml -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
            android:opacity="opaque" >

    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/color_primary"/>
        </shape>
    </item>

    <item android:gravity="bottom" android:top="75dp">
        <shape android:shape="rectangle">
            <solid android:color="@color/white"/>
        </shape>
    </item>

</layer-list>

然后我们可以新建一个 Style 来对 Launcher Activity 生效了!!

<style name="Launcher" parent="AppTheme">
    <item name="android:windowBackground">@drawable/bg_startingwindow</item>
</style>

尾言

很庆幸我在一些应用上看到了类似的做法(例如 新版知乎,v2ex+),也很庆幸有人和我一样不喜欢 Splash Screen,希望这篇文章能帮助到和我有一样想法的人。至于示例代码,可以在 Kotgo 的 Dev 分支上拿到: GitHub - nekocode/kotgo at dev

顺便透漏下,下一篇文章我想讲的是如何使用 Single Activity,Multiple Fragments 架构来构建一个页面快速响应的应用!知乎也是这种架构的拥护者,想知道它有哪些优劣的话,请听下回分解。想要预览,也可以点击上面 Kotgo 的 Dev 分支查阅代码,新的版本也将基于这个架构!

❤️ 95💬 10
回答2016-09-24

GitHub 上有哪些,简单、易学的 Python 项目?

单文件,第三方的豆瓣 FM 播放器。你可以学到如何使用 requests 进行模拟登陆和进行数据请求。可以学到如何使用 urwid 构建命令行 UI。

更重要的是,用来听听歌也还不错~

项目地址:

GitHub - nekocode/doubanfm-py: 第三方豆瓣 FM 红心频道播放器。

哦,还有个项目

GitHub - nekocode/zhihuSayHi: Say Hi to your new followers in Zhihu.

感兴趣的话可以尝试对我的知乎帐号点下关注(不是骗关注啦~),你会收到奇奇怪怪的东西哈哈(逃

❤️ 93💬 17
回答2016-06-22

作为一名程序员,最大的成就感来自哪里?

写了个 Game Engine。顺便用自己的 Game Engine 写了一系列的游戏。

是的,为了方便编写游戏脚本,我还拓展了下 notepad++,当做个简易的 IDE 来使用。

引擎里有哪些看起来比较复杂的东西?自己定义了一套脚本(叫做 fscript),写了个简单的脚本解析器。写了个用在立绘切换的 Shader。搞了搞 FreeType。当初写引擎的时候,链表和 hash 表的实现都是自己手写的,那个酸爽~

做了个弹幕游戏。

游戏视频:

UP主自制游戏_实况解说

图形渲染、粒子系统、碰撞检测都是自己写的,还写了个光效叠加的 Shader。当然最屌的还是音频信息可视化,游戏所有对象和状态都和背景音乐有互动。

想想,这都是大二大三时候的事了。如果路子不走野的话,现在大概在做游戏开发或者搞图形、可视化相关的东西吧~ 哎,可惜了 ╮(╯▽╰)╭

❤️ 93💬 27
回答2015-10-18

有哪些堪称「神器」,却鲜为人知的软件/网站/互联网服务?

Create infographics & online charts

什么?你想做一些漂亮的图表,但是找不到好的工具?别瞎忙活了,你要找的就是它!!(答主的简历图表就是用它做的!)

  • 快搜 — 搜索快人一步 虫部落提供的搜索聚合网站,几十个不常见的垂直领域的搜索引擎,满足你的所有搜索需求!!(顺便提一下,里面有各种 Google Mirror,无法翻墙的朋友们可以稍微慰藉下自己)

    ___            ___            ___            ___   
   /\__\          /\  \          /\__\          /\  \  
  /:| _|_        /::\  \        /:/ _/_        /::\  \ 
 /::|/\__\      /::\:\__\      /::-"\__\      /:/\:\__\
 \/|::/  /      \:\:\/  /      \;:;-",-"      \:\/:/  /
   |:/  /        \:\/  /        |:|  |         \::/  / 
   \/__/          \/__/          \|__|          \/__/  

  • 虫部落导航 这里,只要你是个 IT 达人,里面的网站都值得你去逛一逛

  • 数据库编辑器:SQLite Editor 十分有用的一款移动端 SQLite 查询,修改软件。需要 root 权限,能够搜索并查看手机所有安装应用的 SQLite 库数据。

===================

蹲着坑,以后还有再补上

❤️ 89💬 8
回答2015-10-26

作为一名开发者我现在什么都会,什么都不精,怎么办?

曾经做过面试官的告诉你,你虽然讲了一大堆,但是让人觉得全都是懂一点而已,反而会显得 low。建议先纵向深度学习,然后再横向发展会好很多。

『做过平面设计,写过书籍,改过 bbs』 这些就别拿出来了,因为这些在面试官眼里就觉得毫无价值,有滥竽充数的怀疑,在没有把一项技能学到中上水平之前,私认为都不应该将其作为自身技术筹码,甚至不值一提。

技能广度很重要,但是,在尚未精通任何一个领域前,你的技能广度只能说明你是一个没有耐力,或者没有能力深入任何一个领域的人。

❤️ 86💬 18
回答2021-04-30

如何优雅地处理使用 React Context 导致的不必要渲染问题?

此处引用 @buhi 的回答:

一句话,react context是给你注入服务的,不是让你注入数据的,如果要注入具有数据的服务那你就注入个类似EventEmitter的东西,例如rxjs observable

其实,我们可以把目光放到隔壁 UI 构建方式和 React 差不多的 Flutter 身上,作为新浪潮它提供了一些新的思路,我们来看看 Flutter 下是怎么实现遥远组件间的状态共享的:

首先,Flutter 和 React 一样拥有类似 Context/Provider 的组件和接口:InheritedWidget,都能在组件树上很方便的共享数据,而无需通过 props 一层层往下传。

然后,Flutter 大部分官方组件都提供了一个叫做 Widget Controller 的东西,它的作用其实就有点类似上面 @buhi 提到 EventEmitter(在 Flutter 中的具体实现被叫做 ChangeNotifier),它使用了 观察者模式来分发数据/事件给监听的组件,从而实现 在组件以外对组件内的状态进行控制

在 React 下还原一下 Controller 的大概实现(部分为伪代码):

type Listener = (title: string) => void;

class Controller {
  private _listeners = new Array<Listener>();

  public addListener(listener: Listener) {
    this._listeners.push(listener);
  }

  public removeListener(listener: Listener) {
    this._listeners.remove(listener);
  }

  public updateTitle(title: string): void {
    for (const listener of this._listeners) {
      listener(title);
    }
  }
}

const Component: React.FC<{ controller: Controller }> = ({ controller }) => {
  const [title, setTitle] = useState<string>();

  const listener = useCallback((newTitle: string) => {
    setTitle(newTitle);
  }, [setTitle]);

  useEffect(() => {
    controller.addListener(listener);
    return () => {
      controller.removeListener(listener);
    };
  }, [controller, listener]);

  return <>{title}</>;
};

可以看出:

  1. 核心是观察者模式,Controller 是发布者,Component 是观察者(并且允许多个观察者观察一个发布者)
  2. Controller 可以触发组件内的 setState
  3. 组件内通过 useEffect hook 来在确保仅在挂载周期内对 Controller 进行监听

当我们有了 Controller 这种能对组件内状态进行控制的「代理」,那我们就可以结合 Context 把它分享出去,从而实现遥远组件之间的相互控制了。

我们只需保证 Controller 的实例不变(避免触发整个 Provider 树的更新),而当我们想要改变监听组件的内部 state 时,只需调用 Controller 实例内的 notify 方法(例如上例中的 updateTitle)即可。

当然,上面只是一个最小的例子,如果你对这种状态控制/分享方法感兴趣的话,可以看看鄙人封装的 hook:

nekocode/use-shared-state

目前已在多个生产项目中使用,效果十分满意:对比其他社区流行方案代码更羽量级且几乎无性能损耗,支撑起了我们大部分需要状态控制/分享的场景

(号外:仓库已被知名写作 App 「Typora」的作者 Star 了~

❤️ 82💬 10
文章2016-06-08

#Android# 如何组好队伍刷怪

文章转载以及所有图片 & 文字的引用需经本人同意。

前言

假设你带领着一个刚凑够人数的 Android Dev Group 准备一起打怪升级,Group 里队友们能力不等,有 Level 30 的,也有 Level 10 的。好的,那你要开始头疼一系列问题了,包括如何激发队友们的战斗热情(内驱力) ,提供一条有效的打怪练级线路(工作流水线),提高团队的整体输出(团队效率),降低团队产生的内耗等等= =

  • 基础结构

首先,这得是一个足够扁平化的团队,所以大家之间应该是队友的关系。互相之间的沟通应当没有上下级之间的障碍。

团队应该提倡 OKR 而不是 KPI 的原则,理想情况下是大家都应该在努力做自己喜欢的事,当然,努力的方向应当和公司方向不偏差太远。这种自下而上的驱动力叫内驱力。

  • 任务分配

好吧,要搞定任务分配首先你得构造一个粒度足够小的需求池,池里面有组件开发、页面开发、Bug 修复等需求,需求来自外部(PM、设计)或内部(组件抽象、代码重构),队友自行选择自己能完成的任务。

接到比较大的需求时,应当尽可能切碎再扔进需求池。粒度大的需求第一不好预估时间,第二是不可控因素太多(,深刻感悟 π__π )。粒度一到三天松紧合适。不应当按模块切分,应当先按组件切分。

  • 二进制库管理

为何需要个内网二进制仓库?一者是起到缓存作用,加快大家访问远程仓库的速度。二者是提供一个公司内部的组件仓库。

需求池有新的页面需求的时候,第一阶段应该进行的工作为[ 面向组件开发 ],由每个队友选择开发难度能接受的组件开发需求。接受需求者新建 Project 并开始开发 Library,开发完成后上传 Jar / Aar 到内网 Maven 仓库。

有必要的话,每个组件的负责人要长期维护并升级相应的组件以适应日后更广泛的需求。组件可由个人意志决定是否开源。

贡献组件的数量、质量和开发速度都会纳入绩效评价中。

  • 源码管理

开发第二阶段为 [ 面向页面开发 ],开发者选择组件库里的组件进行开发。使用 Pull Request 的模式进行代码混合,Leader 负责进行代码审查。

代码一旦混合进 Dev Branch 就会触发持续集成系统,构建失败的话 Leader 需要指派紧急修复给相应的开发者。构建成功会自动部署到相应的下载服务器进行测试分发。

代码质量和通过率会纳入绩效评价中。

  • 代码组织

界面开发中应遵循先开发 View 和 Model 层,最后开发 Presenter 层的协定(PS:上图来自公司使用的架构 KOTGO )。应当优先确保每个 Model 是可测试的。

避免 Lava Flow (熔岩流)的产生。在不规范的开发流程中,很容易会有不断往旧的类中填充适应新需求的耦合代码这种现象,最终逐渐形成难以维护的 Lava Flow。

熔岩流常见于耦合了业务逻辑的 Activity 或 Fragment 中,所以要避免熔岩流必须有明确的分层意识。而更有保障的做法是业务逻辑(Model 层)由专门的人开发,其他人只专注于页面开发。形成 [ { 大前端 <- 小后端 } <- 大后端 ]的开发模式。

  • 内驱力

赋予你的队友自主权。选择更高难度的挑战应当得到更丰厚的回报,但也要承担当中的风险。最好量化并告知所有挑战(需求)的难度和能获得的分数,分数累积到一定程度就能影响你的绩效甚至 Level。

尾言

尾言是最重要的广告时间~ (//▽//)

Come on,想要尝试 Kotlin 的小伙伴可以试试 1.x 版本的 KOTGO 了。对 我们公司 感兴趣的可以私信我拿邮箱投递简历,喜欢文章的点个赞或者打点赏呗~(下一篇的预告是 Kotlin 投入生产几个月以来的感想~)

❤️ 74💬 21
文章2017-01-22

RxLifecycle: 绑定你的 Observable 到生命周期上

前言

RxJava 是 Android 开发中的一件神器,但我们在 Activity 或 Fragment 里使用 RxJava 的时候要注意,没在恰当时机执行 Dispose 或终止 Observable 的话会导致内存泄漏。下面这段代码中,Consumer 匿名内部类持有 Activity 的引用,当 Activity 销毁时 Observable 并没有被中止或切断与 Observer 的联系(未 Dispose),所以就造成了内存泄漏。

Observable.interval(0, 2, TimeUnit.SECONDS)
        .subscribeOn(Schedulers.computation())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<Long>() {
            @Override
            public void accept(Long n) throws Exception {
            }
        });

要避免这种情况的话,我们可以使用 Rx 官方提供的 Dispose 方法来在 Activity 销毁时手动切断 Observable 与 Observer 的联系(但是 Observable 还会继续发射数据):

private Disposable disposable;

@Override
public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
    super.onCreate(savedInstanceState, persistentState);

    disposable = Observable.interval(0, 2, TimeUnit.SECONDS)
            .subscribeOn(Schedulers.computation())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<Long>() {
                @Override
                public void accept(Long n) throws Exception {
                }
            });
}

@Override
protected void onDestroy() {
    super.onDestroy();
    disposable.dispose();
}

这样写的体验是割裂的,你需要保存一堆 Disposable 的引用(当然你也可以使用 CompositeDisposable),然后在销毁时再「手工进行」Dispose。

探索

有没有更优雅些的办法呢?答案是有的。在 Rx 官方提供的一些操作符中可以让我们提前中止 Observable 与 Observer 的联系,例如 TakeUtil 操作符:

假设 Observable.takeUtil(ObservableB).subscribe(Observer) 。当 ObservableB 发射第一个数据时,Observable 立刻被 Complete,同时立刻调用了 Observer 的 onComplete() 方法,观察结束后立刻释放对 Observer 的引用。

于是,脑洞大开的我们可以创建一个如下的变换操作符:

public class BindLifecycleTransformer<T> implements ObservableTransformer<T, T> {
    private final BehaviorProcessor<Integer> lifecycleBehavior;

    public BindLifecycleObservableTransformer(@NonNull BehaviorProcessor<Integer> lifecycleBehavior) {
        this.lifecycleBehavior = lifecycleBehavior;
    }

    @Override
    public ObservableSource<T> apply(Observable<T> upstream) {
        return upstream.takeUntil(
                lifecycleBehavior.skipWhile(new Predicate<Integer>() {
                    @Override
                    public boolean test(@LifecyclePublisher.Event Integer event) throws Exception {
                        return event != LifecyclePublisher.ON_DESTROY_VIEW &&
                                event != LifecyclePublisher.ON_DESTROY &&
                                event != LifecyclePublisher.ON_DETACH;
                    }
                }).toObservable()
        );
    }
}

接下来只要在 Activity 里创建一个用于记录当前所在生命周期的 LifecycleBehavior,然后就可以使用 compose(new BindLifecycleObservableTransformer(lifecycleBehavior)) 来绑定你的 Observable 到 Activity 的生命周期上了。

但是这样还是需要手动维护个 LifecycleBehavior。有没有办法连 LifecycleBehavior 都不写呢?答案依然是有的。

我们可以通过一个独立的 HeadlessFragment 来维护 LifecycleBehavior,然后想监听哪个 Activity 或 Fragment 的生命周期的话,只要将 HeadlessFragment 插入其中就行了,FragmentManager 会主动同步 HeadlessFragment 与父 Activity/Fragment 之间的生命周期。

RxLifecycle

我们将上述的工作封装成了一个开源库 RxLifecycle ⇦猛戳,你可以在其中查阅上面提到的所有代码。它允许你仅用一句话绑定你的 Observable 到 Activity/Fragment 的生命周期上:

Observable.interval(0, 2, TimeUnit.SECONDS)
        .compose(RxLifecycle.bind(MainActivity.this).<Long>withObservable())
        .subscribeOn(Schedulers.computation())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Consumer<Long>() {
            @Override
            public void accept(Long n) throws Exception {
            }
        });

我们在仓库中还提供了个 DEMO ,以便详细介绍 RxLifecycle 的全部功能。

❤️ 72💬 2
回答2015-12-03

Android开发中,Fragment真的有大家说的那么不堪吗?

来填坑了~

你们确定用对了 Fragment 了么?:) 居然还有说 Fragment 失去出现意义的,请先弄明白 Fragment 出现的意义是什么好不。

Fragment 的出现一方面是为了缓解 Activity 任务过重的问题,另一方面是为了处理在不同屏幕上 UI 组件的布局问题,而且它还提供了一些新的特性(例如 Retainable)来处理一些在 Activity 中比较棘手的问题。

  • Fragment 拥有和 Activity 一致的生命周期,它和 Activity 一样被定义为 Controller 层的类。有过中大型项目开发经验的开发者,应该都会遇到过 Activity 过于臃肿的情况,而 Fragment 的出现就是为了缓解这一状况,可以说 它将屏幕分解为多个「Fragment(碎片)」(这句话很重要),但它又不同于 View,它干的实质上就是 Activity 的事情,负责控制 View 以及它们之间的逻辑。
  • 将屏幕碎片化为多个 Fragment 后,其实 Activity 只需要花精力去管理当前屏幕内应该显示哪些 Fragments,以及应该对它们进行如何布局就行了。这是一种组件化的思维,用 Fragment 去组合了一系列有关联的 UI 组件,并管理它们之间的逻辑,而 Activity 负责在不同屏幕下(例如横竖屏)布局不同的 Fragments 组合。
  • 这种碎片不单单能管理可视的 Views,它也能执行不可视的 Tasks,它提供了 retainInstance 属性,能够在 Activity 因为屏幕状态发生改变(例如切换横竖屏时)而销毁重建时,依然保留实例。这示意着我们能在 RetainedFragment 里面执行一些在屏幕状态发生改变时不被中断的操作。例如在 ToastAndroid/StartActivity.kt at master 我使用了 RetainedFragment 来缓存在线音乐文件,它在横竖屏切换时依然维持下载进度,并通过一个 DialogFragment 来展示进度。

其实从 Google 推出 Support 包来支持在早些版本实现 Fragment 这一行为就可以看出,谷歌认为 Fragment 比 Activity 更适合管理屏幕组件,因为 Activity 在单屏下无法细分,注定了如果使用 Activity 来管理页面所有 UI 组件的话会造成组件耦合以及不可复用,代码也臃肿地会令人头痛。总而言之,我认为使用 Fragment 绝对是一种好习惯。

4 reasons to use Android Fragments

,这篇文章解释了在四个地方应该使用 Fragments。

另附 Fragment 的正确用法,遇到坑的都可以去好好理解下:

nekocode/ToastAndroid · GitHub

❤️ 71💬 12
文章2016-11-26

#Android# 轮子杂评 1

工作中常常会用到一些开源的轮子,但真的是每个都如人气般质量高么?你真的敢用么?该系列文章为你揭开一些高人气轮子的内幕。

挺多 Star 的。但是首先该动画用 View 来实现就要扣分了,实际上像此类无交互(例如不需要处理 Touch 事件)的纯动画不应该使用 View 作为容器,直接用 Drawable 实现就行了(能避免基类问题)。再仔细看了下代码,妈蛋,直接是个 ViewGroup 然后内嵌几个子 View,通过 属性动画来实现的(用的还是使用反射的 ObjectAnimator),差评。

这个动画挺多的所以 Star 数也更多,但同样直接扔了个 View 出来(虽然你也可以用它内部的 Indicator,因为 Indicator 继承的 Drawable)。在 Indicator 里面借助多个 ValueAnimator 来实现数值变化,好处是用起来简单,不需要自己计算动画的中间值,而且可以使用 ArgbEvaluator 和 RectEvaluator 等这类系统提供的插值器。

然而但这并不是 Drawable 推荐实现动画的方法,更正确的做法是使用 scheduleSelf() 方法。可以参考这里 An example showing how to create and use a Drawable that animates. 虽然 ValueAnimator 和 scheduleSelf() 本质上都是通过线程内 Looper 的定时消息实现的,但是 scheduleSelf() 将动画的播放权交给所在的 View,View 可以决定是否播放 Drawable 的动画,而用 ValueAnimator 的话则需自己处理。

不至于太差,中评。(工作上要用到其中一个类似的动画,但个人最终还是没敢用这个轮子,自己造了)

❤️ 68💬 27
回答2015-11-23

Kotlin是不是没做尾递归优化?

请使用 tailrec 关键字:

tailrec fun sum(n: Int, res: Int): Int {
    if (n <= 0) {
        return res
    } else {
        return sum(n-1,res+n)
    }
}

fun main(args: Array<String>) {
    println(sum(5000000,0))
}

执行结果:

关于为什么需要显式修饰才进行尾递归优化(来自:

M6.2 Available

):

❤️ 66💬 10
回答2017-05-18

如何看待 Google 宣布 Kotlin 正式成为 Android 的开发语言?

被智子锁禁锢了多年的 Android 开发玩家终究有了解脱。要知道 Java 是语言界中十分保守的一门语言,大多数 Android 开发者对语言界流行 Feature 缺乏认识,例如对 FP 毫不了解,而实际上 FP 近年来被逐渐证明很适合解决前端的一些问题,这直接导致了大多数人对 Android 开发的理解只停留在三方库应用的层面上。而 Google 这支强心针打得正是时候,这次钦定必然掀起一股浪潮,让更多人对现代语言以及语言思想有所感知。

❤️ 63💬 29
文章2017-01-15

#Android# 五分钟,科普下 DSL

android 开发中官方选择了使用 xml(layout、drawable 等) 来描述 view。但实际上 xml 的表达能力有限,只用 xml 来描述 view 几乎不可能,它只解决了 ui 中的描述问题,而大部分逻辑问题依然依赖于用代码解决。

你仔细思考下的话会发现目前前端开发的一个相似的套路,使用 html、css、xml 等更具描述能力的 external dsl(domain-specific language )来描述界面,然后使用代码来解决界面上的一些逻辑问题。这些 external dsl 用于将数据配置跟代码逻辑分离开来,它们能让你轻易实现数据的跨语言、夸应用、跨平台,例如你可以用任意一门语言在任意一个系统下再开发个 parser,对 android layout xml 进行再描述。

当然,解析起 xml 来肯定要比直接跑代码慢好几个 level,所以甚至有很多人抛弃 xml,坚持使用 java 来构造布局,但这明显不是一个好的选择。后来一些现代语言加入了 internal dsl 这种东西,它赋予你在代码中写 dsl 的能力:

linearlayout {
    textview {
        layoutwidth "match_parent"
        layoutheight "wrap_content"
        text "Hello"
    }
}

你不用显式地去构造任何实例,这看起来像在写一些配置。但它和配置不一样,因为你甚至能把逻辑也参杂在里面:

linearlayout {
    textview {
        // ...
        text if (firstword) "Hello" else "World"
    }
}

看起来挺棒的是吧?这种感觉有点像在代码里面写 xml、或者在 xml 里写代码(想想官方给出的 databinding)。它同时具备描述能力和逻辑能力。

可以看看代码构建领域的一个例子:maven 和 gradle。以前 maven 加 shell script 的组合,已经开始逐渐被具备写 dsl 能力的 groovy 给取代了。再看看 web 前端吧,之前大热的 reactjs 的 jsx、近期的 binding.scala,它们也都是 dsl 的应用。

那 internal dsl 有缺点么?没法跨语言。想在 android 中尝试 internal dsl 怎么办?赶紧用 kotlin 啊。

另外,kotlin 也有 dsl 的话为什么还需要 groovy?也确实有人有一样的想法:gradle/gradle-script-kotlin

❤️ 62💬 27
回答2015-10-20

GitHub 上有哪些有关图像处理或是机器学习的有趣项目?

Github 上有关注的的话:

非 Github,一些站点:

  • Shadertoy我最喜欢的视觉技术网站之一,shader 编程共享网站,里面很多很酷的例子。有一些已经被我移植到 opengl es 上了(详见:nekocode/murmur · GitHub
  • OpenProcessing :图像处理,可视化的话 也不要忘记了 Processing ,这个站点上很多很酷的,甚至可交互的例子,有点 java 底子,又想研究可视化技术的话,它是一个十分经典而且不错的选择
  • GLSL.io – Open Collection of GLSL Transitions :也是一个 glsl shader 分享网站,主要关注于图片切换效果上
❤️ 60💬 4
回答2015-12-16

有哪些新生代没见过或者无法理解的 Windows 95/98 时代的事情?

出生时家里就有电脑了,老爸是做电脑培训的(教五笔、教 WPS/OFFICE、教 Visual FoxPro),他偶尔也接一些小企业的项目,用 Visual FoxPro 写一些企业管理软件。而我呢,从小就被他影响,只要是电脑相关的事物都很感兴趣(大概我在小学一年纪时,老爸就在尝试教我 Foxbase 了,然并卵,学不会 ╮(╯▽╰)╭)。最早的时候,店里的电脑(有两排)装的都是 DOS,老爸把电脑都用一种奇怪的线缆连接起来,然后一台主机每天在运行一个程序,整个屏幕没有文字,只有像贪吃蛇一样的粗线条在屏幕上不断移动(有谁知道是什么的话私信一下我)。

记得当初最早玩的游戏是 DOS 下的「波斯王子」(小学之前),每次要玩的时候都在 DOS 下输命令,可是那时候我还小,只能每次要玩的时候就叫老爸,让他来帮忙打开游戏:

玩过了很多关卡,可是始终没有玩到结局,记得有一些关于剧情的动画,一个巫师一个女主,具体是怎样真的想不起来了,只觉得很多关卡真的很难玩。。

那个时候店里的学员很多都是来学打字的,家里的电脑都装了五笔速成等练字软体,也有一些打字游戏,具体真记不起来了:

那时候我妈在店里帮忙培训,她是教五笔的,她打字也可快了,一分钟四五十个字的速度吧!也教 WPS(DOS 时期的 WPS),她整天让我背五笔的键盘表,可是我一直背不出来,所以也不会打中文,心智没到达那个程度啊。。

上了小学后,老爸培训班里就有一电脑排装上了 Windows95 了!刚接触到 GUI OS 和鼠标的我,仿佛发现了新大陆。DOS 对我来说实在是无法操纵,基本就在玩几个打字游戏。但是 Windows 的 GUI 就不同了,什么都可以捣鼓一下,那时候 Windows 下很多应用的图标都像下面这种:

很多图表看上去很像游戏图标好伐!!!所以我都喜欢点一下,看看有什么出来。

在那个时代做得最多的应该就是玩游戏了,至今仍然记得的有下面这些:

  • 仙剑奇侠传

这真的是我到现在还能清晰记住剧情和 BGM 的 RPG GAME,再后来玩过很多游戏,都很难超越它在我心目中的地位了,无论是剧情、画风、BGM 都代表了当时国产游戏的最高境界,现在这么用心做游戏的商家真的很少了。幸好当初老爸买的是正版 :) 无憾。

  • 金庸群侠传

这个游戏超耐玩的好不!!!玩了很久,通了很多管,但也不知道结局是如何。只记得前期招田伯光入队很吊好不,还有就是可以开个船到处游荡,有沙漠,有各种山洞,还有一个泥潭迷阵过了超久。。。

当时真的玩过很多游戏,不过记不住了,其他一些游戏是比较后玩的(Windows ME 以后)所以就不写了,先占坑记起来再写。

现在想想,当初家里有电脑真的对我影响超大,虽然并没有成为能和同伴交流的谈资(大多数同龄人家里都没有电脑,而且大概到我五六年级时网吧才开始流行),但是它真的成为了我童年记忆很重要的一部分,除去正常校园生活外,最期待的就是打开电脑的那一刻。所以很感谢我的老爸以及老爸的电脑们,让我这个智商并不高的儿子在未来很多年后对于电脑的热爱以及了解都远超很多人。这大概就是所谓的「起跑线」吧。

❤️ 57💬 29
文章2017-12-13

深入简出 RxJava

网络上很多关于 RxJava 的文章都是基于「方法论」的,很少从实现原理的角度去透析。本文希望通过深入简出地描述 RxJava 的一些重要原理,让读者大概知道 RxJava 是如何 Work 的。

核心对象

ReactiveX 是基于观察者模式设计的,核心对象只有 Observable 和 Observer。它们最简单的代码为:

interface Observable {
    void subscribe(Observer observer);
}

interface Observer {
    void onNext(T t);
}

Observable 的核心方法是 subscribe(),它接收一个 Observer。当调用 subscribe() 的时候,就开始通过调用 Observer 的 onNext() 方法发射数据。

上下游

以下代码中:

ob1 = Observable.create(Func1);
ob2 = ob1.map(Func2);
ob3 = ob2.subscribeOn(SchedulerA);

ob1 是 ob2 的上游,ob3 是 ob2 的下游。可以看出,对 Observable 进行一次「操作」后会得到一个新的 Observable。

官方定义:Doubt about the terms Upstream vs Downstream · Issue #5022 · ReactiveX/RxJava

操作符

Rx 里的操作符,例如上面的 map(Func2),其内部实现是这样(官方命名稍有不同):

ob2 = new MapObservable(ob1, Func2);

所以上一小节的代码可以写成:

ob1 = new CreateObservable(Func1);
ob2 = new MapObservable(ob1, Func2);
ob3 = new SubscribeOnObservable(ob2, SchedulerA);

可以说 Rx 里最重要的是「组合操作符,加工数据流」。

官方操作符文档:ReactiveX - Operators

操作符的内部实现的核心思想是「Wrap」,例如我们可以通过 Wrap Observable 来实现一个最简单的没有任何操作的操作符:

class NoOpObservable {
    Observable upstream;

    NoOpObservable(Observable upstream) {
        this.upstream = upstream;
    }

    void subscribe(Observer downstream) {
        upstream.subscribe(downstream);
    }
}

可以看到,在我们 NoOpObservable 的核心方法 subscribe() 里,我们直接通过调用上游 Observable 的 subscribe() 方法,把下游的 Observer 往上游传。这样,我们就成功把下游和上游之间建立联系了。

接下来我们增加难度,通过 Wrap Observer(注意是 Observer)来实现一个对数据流进行加工的操作符。举个例子,MapObservable 就是一个能对数据流进行加工的操作符,它在构造时传入一个 Func 参数,类型为:

(T) -> U

这个 Func 的作用,就是把上游发射的数据 T 加工成 U 然后继续往下游传递。例如,我们可以使用 Func 把上游发射的数据转换为 String 再传递给下游:

(T t) -> t.toString();

前面有说到,Observable 是通过调用 onNext() 来向 Observer 发射数据的,为了避免 Observable 直接向最下游的 Observer 直接发射数据(因为我们还要进行加工),所以我们需要对 Observer 也进行 Wrap,于是我们可以把 MapObservable 设计成这样:

class MapObservable {
    MapObservable(Observable upstream, Func mapFunc) { /* ... */ }

    void subscribe(Observer downstream) {
        upstream.subscribe(new WrapObserver(downstream, mapFunc));
    }
 
    class WrapObserver() {
        WrapObserver(Observer downstream, Func mapFunc) { /* ... */ }

        void onNext(T data) {
            U newData = mapFunc.apply(data);
            downstream.onNext(newData);
        }
    }
}

subscribeOn & observeOn

这是 RxJava 中最常用,但又不好理解的两个操作符。因为它们自身不对数据进行任何加工,而是对其它操作符产生「副作用」。

为了更好地理解这两个操作符,我制作了个动画来展示它们的工作流程:

转载图片需经作者同意

图中分别有 A B C 三个不同的 Scheduler,它们会把 Runnable/Func 扔到不同的线程上去执行,而上图中不同的颜色代表被不同的 Scheduler 执行。

可以看到,subscribeOn() 其实影响的是上游操作符中的 subscribe() 操作,而 observeOn() 影响的是下游操作符中的 onNext() 操作(这里是泛指,当然还包括 onComplete()、onError() 等)。

所以,想要确定操作符上的某个 Func 是在哪个 Scheduler 上工作时,先要确定这个 Func 是在操作符上的 subscribe() 还是 onNext() 上执行的。例如 Create 操作符传入的 Func 是在 subscribe() 上执行的,所以它优先受下游最近的 subscribeOn() 调度。而 Map 操作符传入的 Func 是在 onNext() 上执行的,所以它优先受上游最近的 observeOn() 调度。

再例如 Collect 操作符:

.collect(()->V func1, (T)->U func2)

传入的 func1 是在 subscribe() 上执行的,而 func2 是在 onNext() 上执行,所以 func1 和 func2 可能被两个不同的 Scheduler 调度。

❤️ 56💬 11
回答2022-09-10

2022年了nest.js在国内怎么样?

在国内四个不同团队用过 nest.js,其中有三个团队是我主导用的。我们主要技术栈是 nest.js + typeorm + graphql,选择 nest.js 主要是因为它是 node.js 下目前最靠谱的选择。

至于为什么选择 node.js,主要原因有几个:

  1. 在非计算密集的场景下,会比其他很多主流后端语言更容易写出高性能的代码。举个例子,async 相较于 multi-thread,以前在写 java 时,除非用 rx 之流,不然一些复杂的异步问题都不好解决,用上多线程的话,很多时候不合理的锁、线程间交互会严重影响整体耗时,而 js/ts 的 async/await 对这类问题基本是降维打击。而后端恰恰大部分是 io 密集的场景,使用多线程模型并没有优势。
  2. 语言优势。js 有着庞大的生态、社区、开发群体。而 ts 有着现代语言里最强大的类型系统,用于开发大型系统是完全没问题的,而各种现代化的语法糖能提高开发效率、代码可读性。
  3. 使用 graphql 的「无奈之举」。在目前所有的 graphql 的服务端实现里,node.js 下的是最完善、成熟的。

个人感觉 node.js 其实是创业团队前期很不错的选择,性能上依托于 v8 基本是脚本语言里最快的了,另外在 serverless 领域里,js 也是首要支持的语言。

❤️ 54💬 16
文章2016-11-17

#Android# Docker 与自动化

前言

在之前写过的两篇文章中(如何组好队伍刷怪Android 与 Docker )粗略提到一些可以针对团队的提高开发效率、控制开发质量的工具,例如 Nexus 仓库和持续集成(CI)服务。

之前公司的代码托管在自己搭建的 Gitlab 上,所以当时 CI 服务用的是 Gitlab CI,无需自己搭建。而新公司代码托管在 Github 上,虽然有 Travis CI 等服务,但是仅对开源仓库免费,所以还是自己搭建好些。So 我选择了 Jenkins。

二进制仓库

都搞定后,我们工程中所有二进制都使用自己搭的私有仓库进行代理了,像这样:

buildscript {
    repositories {
        maven {
            url "${ZBD_NEXUS_REPO}/jcenter/"
        }
    }

    dependencies {
        // ...
    }
}

allprojects {
    repositories {
        maven {
            url "${ZBD_NEXUS_REPO}/jcenter/"
        }
        maven {
            url "${ZBD_NEXUS_REPO}/jitpack.io/"
        }
    }
}

它带来的好处是巨大的,一来是起到缓存的作用(要知道 Jcenter 或 Maven Central 在国内的访问速度有多慢)。不管是开发者还甚至是 CI 服务,只要缓存一次,再次请求时都是用的缓存,基本上几秒钟就能同步完整个工程用的所有二进制。

二来是一些私有的二进制也能上传到仓库上。例如我们公司的项目用到并修改了 ijkplayer 库,在我来之前,他们把 ijkplayer 的代码和编译好的二进制都扔进 Git 仓库中,导致我入职时 Git 仓库已经高达 4G 大小了。。。要知道,二进制这种东西是不应该出现在代码仓库中的。

好了,现在搭了 Nexus 后,先把 ijkplayer 那 Part 的代码从主项目中移出来,然后将编译生成的二进制自动上传到 Nexus 仓库上,主项目直接从上面 Pull 编译好的二进制。

持续集成

使用 Jenkins 提供的 CI 服务也给我们的开发带来了巨大收益:自动构建新的代码变动、自动设置 Github 的 Commit Status(构建失败的 Commit 无法混合进 Dev / Master 分支)。

在 Branchs 上可以查看所有分支的构建状况:

提交的 Pull Request 需要通过构建才能进行混合:

构建失败的 Commit 是无法混合进保护分支的:

利用持续集成服务还能实现更多的功能,例如构建成功后自动上传二进制、自动发邮件通知等,它实现了真正意义的 自动化

着手搭建

为什么用 Docker?只给你一台服务器,你能在一天内搭好所有东西么(笑?

下面的脚本记录我搭建的一些过程,其中 docker-android 是我自己写的镜像,包含以下工具和环境: Oracle Java 8 / Android Platform SDK 23 & 24 / Android Build Tools 23.0.3 & 24.0.3 / Pre-installed Gradle version 3.1

#!/bin/sh

# 此份脚本仅用于服务部署,如需进行服务迁移请使用 Docker 的容器备份功能
curl -sSL https://get.daocloud.io/docker | sh
service docker start
docker pull sonatype/nexus3
docker pull jenkins
docker pull daocloud.io/nekocode/docker-android:1.6

mkdir -p /hi/ci/android/jenkins-data

docker run -d \
    -p 8081:8081 \
    --name nexus \
    sonatype/nexus3
docker run -d \
    -u root \
    -v $(which docker):/usr/bin/docker \
    -v /hi/ci/android/jenkins-data/:/var/jenkins_home/workspace/ \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /usr/lib64/libsystemd-journal.so.0:/usr/lib/x86_64-linux-gnu/libsystemd-journal.so.0 \
    -v /usr/lib64/libsystemd-id128.so.0:/usr/lib/x86_64-linux-gnu/libsystemd-id128.so.0 \
    -v /usr/lib64/libdevmapper.so.1.02:/usr/lib/x86_64-linux-gnu/libdevmapper.so.1.02 \
    -v /usr/lib64/libgcrypt.so.11:/usr/lib/x86_64-linux-gnu/libgcrypt.so.11 \
    -v /usr/lib64/libdw.so.1:/usr/lib/x86_64-linux-gnu/libdw.so.1 \
    -p 8082:8080 \
    --name jenkins \
    jenkins 

在 Jenkins 下 Android 工程我也打算使用 Docker 来构建,所以要实现 Docker in docker 的功能,于是上面的脚本在开启 Jenkins 容器时必须做些挂载设置。而 Jenkins 中构建 Android 工程的命令可以写成这样:

docker run --rm -v /hi/ci/android/jenkins-data/:/workspace -w /workspace/${JOB_NAME} daocloud.io/nekocode/docker-android:1.6 gradle clean app:assembleRelease

如果签名使用的 Keystore 密码配置放在 Properties 文件中,而且加入了 .gitignore 列表的话(这是个好习惯),可以使用类似下面的命令在构建前自动在工作目录生成需要的 Properties 文件:

echo "KEY_ALIAS=xxx\nKEYSTORE_PASSWORD=xxx\nKEY_PASSWORD=xxx" > /var/jenkins_home/workspace/${JOB_NAME}/keystore.properties

对了,要想 Jenkins 从 Github 的私有仓库成功拉下代码的话,还得为 Jenkins 生成用于访问 Github 私有仓库的 SSH 密钥对(可以自由替换下面的 projectname 字符串):

docker exec -it -u root jenkins /bin/bash

# 生成 Github Deploy Key
ssh-keygen -t rsa -f id_rsa.projectname_android
chmod 755 *
su jenkins
eval "$(ssh-agent -s)"
ssh-add id_rsa.projectname_android

# 把 Public Key 设置到 Github repository 的 Deploy Key 中就行了
cat id_rsa.projectname_android.pub

另外,要让 Jenkins 将构建状态显示到 Github Commit Status 上的话还得在个人账户上生成个 Access Token,并在 Jenkins 配置好。

❤️ 54💬 8