Keep Velocity High | kvh 的个人博客

分享 kvh 对于技术、创业的理解和实践

0%

写手

长久以来,我们团队都缺乏一位优秀的写手。目前,一些公关稿,都是我们的 CEO 和我勉强冒充写手顶着上。

我们之前也找过几位做技术的朋友写过稿子,出来之后,总是觉得事儿是写明白了,读着总像一个使用说明书,勾不起人的分享欲望。

上周,我们外部找了一位以前做市场的人来写手写一篇公关稿,谁想到,这个过程,最终成为公关稿奇遇记。

这位同志,下称成为写手 X,某次活动认识的,自己也运营了若干自媒体。这哥们就是那种不断往我的朋友圈转发自己的自媒体文章赚阅读量但从来不跟我打招呼的家伙。我想着以后没准能用着,忍了。这不,我们要从外部找人写,自然就想到这位仁兄了。

初步接触了下,看了一下之前写的稿子,格局文本都不错,觉得还可以啊!报了价,还了价,愉快地决定了。

X 同学对我们产品还是比较了解的,还约了一个时间跟我们 CEO 聊了一次,回去写稿了,周五给稿。嗯,果然是知道我们周六加班,可以配合他修改 -_-||

周五到了,初稿发了过来,乍一看,还不错呀!整体的格局都有了:标题党、一开头咋呼、联系现实痛苦、给出良药、夸大颠覆、蹭巨头。套路很全啊!

本着负责任的态度,我决定细读一下文章。不看不知道,一看吓一跳,这篇套路十足的文章,文字细节却是漏洞百出。下面列举的,可能是小学生作文常见的毛病:

  • 错别字
  • 病句
  • 英文缩写前后不对应
  • 升华极端逻辑不通

文章,是改出来的

这是鲁迅说的话,我记住了。

因为这个人是我介绍的,用周鸿祎那句话:自己约的炮,含泪都要打完啊!

想着初稿嘛,可能人家也没有太用力,于是我也很好说话的指出了若干问题,让他再改一次。

第一版

很快的改好了,发过来,错别字改了。但是语法和句法还是有问题啊!

例如:没有成本招聘专业的测试人员

这是一个用词错误啊,成本应该是预算吧?

另外还有大量的口语化叙述,关系到 xxx、关系到 xxx、关系到 xxx,太口语化了有没有!我要的是正式的公关稿啊!

一些段落,不够凝练(嗯,其实是啰嗦,我很客气吧?)。

我作了一些标注给他,但是没法全做,因为太多问题了!打回去再改。

第二版

几个小时后,又发来一版,看表已经是晚上11点了。为了充分发扬本司的快速行动的作风,我爬起来打开电脑看稿子。我败给他了。我提的问题,都没怎么改啊!实在不能忍,我挑灯夜战,把全文都改完了,发回给他。

我期望他能够理解我的意思,把我改的地方,都写进去——钱得人家自己挣吧,我不能都给人家整全了啊!

第三版

周六下午了,终于发过来。我发现他没有认真理解我修改地方,只是简单拷贝,所以搞得有些地方前言不搭后语了。我有些我无语,积攒了很久的怨气终于爆发了,我说哥们,你能够认真点么?

第四版

因为原定周一发,哥们已经开始想要一半的费用了。稿子写成这样,还好意思着急要钱?!

我们再提出还有错别字问题,回答是:错别字很正常,不影响阅读就行。我三观快被颠覆了。哥们,你是要将我拉低到你的层次,然后用你的熟练度打败我么?

……
以上省略吐槽三千字。

含泪打完

博客写到这里,我已经在手动操刀整篇文章的修改了,我实在无法指望哥们能帮我们写好这篇稿子,我相信他已经认为自己足够努力了,无奈文字功底实在太差。

稍后我会把文章贴上来。

后记

各位看到这篇博客的文章,如果有认识优秀的写手,请帮我们介绍引荐下啊。兼职或者全职我们都墙裂欢迎!

有朋友建议在用户中征稿,嗯,这个或许可以试一下。

出发

在经历过了多轮的 APP 开发/测试/上线/运营周期之后,我们觉得 APP Bug 反馈环节始终十分低效,我们要来改变一下这个状态。于是有了 bugtags.com

一年

从去年六月正式立项,八月中旬内测,九月中旬正式上线以来,bugtags 已经走过了快一年。

还记得去年八月中,我们忐忑的发给身边的朋友试用,没想到好评不断,一开始设置的邀请码申请机制,没几天就被迫取消了——因为太多人申请了,实在处理不过来。

做产品或者技术的人,最幸福的事情,莫过于有数以万计的人使用自己的产品,在 Bugtags,我们每天都享受着这样的幸福。

产品

我们内部就是用 Bugtags 来进行流程管理,也就是所谓的 eat our own dog food,在使用自己的产品时候,能发现很多问题。

Bugtags 是不断在进化的,修复一些早期的 Bug,添加一些使得产品完备的功能,同时加入一些新功能,例如网络监控、测试用例、实时调试等。

在添加功能的时候,我们始终记得我们创造 Bugtags 的部分初衷:JIRA 这类产品太重太复杂了!我们也在不断的自省:我们是否正在把 Bugtags 变成我们曾经鞭挞的那种产品?

我们始终小心翼翼的改进产品,增加功能却让它处于恰当的位置,在细节打磨用户体验。

技术

在产品上线的早期,我们每天接受大量的用户反馈,主要是在集成上的问题,SDK 的兼容性问题等。我们总是会趴在群上,第一时间进行解决。今年以来,大部分问题,都已经整理出答案了。在这个解答问题的过程中,我们的对系统的理解,也在不断的加深。

春节以来,随着使用用户增多,服务器的压力不断增大,终于在某个周末,服务几乎不可用了。我们的后端团队使用了多种组合方案,把流量扛住了。真正体会到了没有经过流量冲刷的后端,那不叫好后端

我们始终在使用业界的主流技术,拥抱开源。在开源系统达不到要求的时候,会自己定制一些模块。

团队

我们核心团队合作时间已经很长了,每个人的角色都很明确。我作为技术的负责人,本身也要承担较重的开发任务,说实话是没有太多时间进行管理的。这就要求,我们的团队每一个人,都是高度的自我驱动,知道自己该做什么,该怎么去做好,事实上,我们每个人都是这样的。

我们坚持了每天早晨的站会,目的是信息透明,提早暴露问题。

每当有用户夸奖我们的产品做得好,我们就会截图放在群里面,让团队每个人都感受到这份荣光,用户的夸奖,属于每一个人。

每一位新加入团队的成员,我都会面试,除了业务能力,我也会格外强调我们团队的文化:用户为中心的态度,积极快速做事的风格

我们在六月的最后两周时间,小团队实现了实时调试功能的快速上线,充分体现了我们的战斗力。

未来

今年四月以来,我们推出了收费版。到目前为止,我们对成绩还是相当满意的。这更加坚定了我们持续创新和加强用户服务的动力。

今天晚上,在一个由拼图商业评论发起的企业级 Saas 服务市场白皮书中,我们名列其中。其实这是出乎我们意料之外的。

这一年,我们几乎没怎么做推广,到现在,类似大疆无人机这类独角兽公司,新浪、网易、Microstrategy 等这类上市公司,以及各行各业数万家企业都在使用我们的产品,甚至有业界某些大公司,在像素级学习我们的产品

无论如何,能得到业界的认可,我们十分的高兴。

随着产品的深入,我们发现可以深挖的点还是不少,创业就是这样,不断的创新、调整方向、坚持,周而复始。

下一年,我们希望继续增强品牌,使得可以触达到更多的用户。

我们希望可以推出更多更强大的实用服务,帮助开发者更好的开发,测试,上线,运营。

未来将来,让我们一起期待!

沉浸式多语言博客

假期的时候,打算改造下现有的博客系统。目标之一是实现多语言(主要是中英)切换,要求:沉浸式阅读

查找了一下,Hexo 有如下几种工具与国际化有关:

逐一尝试

官方国际化机制:failed

更多是在解决模板翻译问题

hexo-generator-i18n 插件:failed

可以通过指定如下参数,对响应的模块,产生多语言文件夹。

1
2
3
i18n:
type: [page, post]
generator: [index, archive, category, tag]

最大的问题,是会在 tag 和 archive 等文件夹中,将多个语言的博客文章混在一起,达不到沉浸式阅读的要求。

hexo-multilingual:failed

通过在将 Hexo blog 的 package.json 中的 dependencies 从:

1
2
3
"hexo-generator-archive": "^0.1.4",
"hexo-generator-category": "^0.1.3",
"hexo-generator-index": "^0.2.0",

替换成:

1
2
3
"hexo-generator-multilingual-archive": "^0.2.0",
"hexo-generator-multilingual-category": "^0.3.2",
"hexo-generator-multilingual-index": "^0.3.1",

实现在生成的页面中,添加当前语言的信息。

通过在source文件夹下添加 _data 文件夹和如下文件:

1
2
3
4
5
6
7
8
9
├── source
│   ├── _data
│   │   ├── config_cn.yml
│   │   ├── config_en.yml
│   │   ├── theme_cn.yml
│   │   └── theme_en.yml
│   ├── _posts
│   │   ├── cn
│   │   └── en

来实现配置的覆盖和区分。

其中 config_xx.yml 中的配置,可以覆盖根目录下 _config 文件的配置,

theme_xx.yml 可以覆盖主题 themes/xxx/_config文件的配置。

这个方案,进行较为整齐的分割,解决了一部分沉浸式阅读的问题,但是有个重要的 Bug,运行

1
hexo serve

能够很好的进行语言之间的隔离,但是运行

1
hexo g

导出的public文件夹里面的文件,有如下问题:

  • 会有一些链接没有添加 lang 信息————死链
  • 配置覆盖也失效————完全搞砸了
  • hexo 的其他插件,都是基于单语言前提的,这意味着,需要同步改造所有的插件,才可以达到完美的效果————维护成本高,oh no!

个人不打算花费很多的在这个事情上面。

Nginx 实现代理:bingo

我的博客是使用 Hexo 生成了静态文件,使用 Nginx 进行代理的。回想起,Nginx 在 vhost 配置中实现根据 location 规则使用不同的alias

总体思路是:

  • 建立两个独立的博客,分别处理中文和英文,以达到真正的隔离处理和沉浸式阅读
  • 使用 Nginx 来实现转向和定位

最后方案如下。

建立两个博客目录

1
2
├── kvh.io.cn
├── kvh.io.en

分别配置中文和英文的博客,这样所有的插件都可以用了,注意 _config文件的配置分别为:

1
2
3
language: 
- cn
root: /cn

1
2
3
language: 
- en
root: /en

然后你就可以愉快的写博客了。

Nginx 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {

root /home/changbinhe/kvh.io.cn/public;

location /cn {
alias /www/kvh.io.cn/public;
}

location /en {
alias /www/kvh.io.en/public;
}

error_page 404 = @missing;

location @missing {
rewrite .* / permanent;
}
}

对 Nginx 配置感兴趣的朋友,可以参考一下这里

最终效果

可以看我的博客:http://kvh.io

总结

本文使用了多个 Hexo 博客,加上 Nginx 配置访问路径定位不同的文件目录的方式,实现了完全的隔离处理和沉浸式阅读的多语言博客。

有问题?在文章下留言或者加 qq 群:453503476,希望能帮到你。

Bugtags V1.2.7 引入了 NDK SO 库,在集成的时候,遇到不同的 SO 库打包到 APK 时,安装在某些机器上,出现 java.lang.UnsatisfiedLinkError 加载失败。

为此,深究了一下原理,和给出了解决方案。

原理

Android 系统本质是一个经过改造的 Linux 系统。最早,Android 系统只支持 ARMv5 的 CPU 构架,随着 Android 系统的发展,又加入了 ARMv7 (2010), x86 (2011), MIPS (2012), ARMv8, MIPS64 和 x86_64 (2014)。

每一种 CPU 构架,都定义了一种 ABI(Application Binary Interface),ABI 决定了二进制文件如何与系统进行交互。

一般情况下,你不需要关注这些。当你的 APP 中用到了些包含 SO 库第三方库,或者自己使用 NDK 来实现了某些功能,你就需要认真阅读接下来的教程。

NDK SO 支持不同的 CPU 构架

在使用 NDK 开发包含 c/c++ 代码的 SO 库的时候,你可以选择输出支持如下 ABI CPU 构架:

1
2
3
4
5
6
7
armeabi
armeabi­v7a
arm64­v8a
x86
x86_64
mips
mips64

Bugtags 的 NDK 库支持如上所有的 CPU 构架:

bugtags-ndk

但不是所有人的开发者提供的 NDK 库都支持所有的 CPU 构架:

other-ndk

上面的这个开发者提供的库,就只支持 armeabi。

其实一般情况下,是没有问题的,x86 的设备,也会兼容 armeabi 的 SO 库。

合并打包到 APK 中

如果不做任何设置,Android 的构建系统会把这些来自不同开发者的 SO 库都合并在一起,打进 APK 压缩包中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── AndroidManifest.xml
├── classes.dex
├── lib
│   ├── arm64-v8a
│   │   └── libBugtags.so
│   ├── armeabi
│   │   ├── libhyphenate.so
│   │   └── libBugtags.so
│   ├── armeabi-v7a
│   │   └── libBugtags.so
│   ├── mips
│   │   └── libBugtags.so
│   ├── mips64
│   │   └── libBugtags.so
│   ├── x86
│   │   └── libBugtags.so
│   └── x86_64
│   └── libBugtags.so
├── res

系统安装 APK

根据官方 ndk-abi 文档, Android 系统在安装一个 APK 的时候,会考虑当前的设备的 CPU 构架和配置(称为所谓的 primary-abi 和 secondary-abi),去该 APK 文件的对应文件夹去寻找 SO 库。

假设当前设备是 x86 机器,会优先去 lib/x86 文件夹下寻找 SO 库:

1
lib/<primary-abi>/lib<name>.so

如果找不到,同时定义了 secondary-abi,则去如下文件夹寻找:

1
lib/<secondary-abi>/lib<name>.so

如果找到了,就将文件拷贝到 APK 的安装目录的如下文件夹中:

1
/lib/lib<name>.so

找不到对应的 SO,安装正常,但是当这个 SO 在运行时被使用时,会崩溃。

问题来了

可能你已经发现问题了,当一个 APK 是这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── AndroidManifest.xml
├── classes.dex
├── lib
│   ├── arm64-v8a
│   │   └── libBugtags.so
│   ├── armeabi
│   │   ├── libhyphenate.so
│   │   └── libBugtags.so
│   ├── armeabi-v7a
│   │   └── libBugtags.so
│   ├── mips
│   │   └── libBugtags.so
│   ├── mips64
│   │   └── libBugtags.so
│   ├── x86
│   │   └── libBugtags.so
│   └── x86_64
│   └── libBugtags.so
├── res

同时 APK 被安装到一个 x86 的设备上的时候,以上的寻找过程,将会失败,运行时,将出现如下报错:

1
2
D/xxx   (10674): java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/xxx-2/base.apk"],nativeLibraryDirectories=[/data/app/xxx-2/lib/x86, /vendor/lib, /system/lib]]] couldn't find "libirdna_sdk.so"
D/xxx (10674): at java.lang.Runtime.loadLibrary(Runtime.java:366)

此处,笔者有点费解,既然在 x86 文件夹中找不到,应该去 armeabi 文件夹中自动寻找啊,此处留一个 TODO,需要接下来去确认是否是某些机器的原因。

解决方案

准则

NDK SO 开发者应该遵循一个准则:支持所有的平台,否则将会搞砸你的用户。

NDK SO 使用者应该遵循一个准则:要么支持所有平台,要么都不支持。

然而,事与愿违,因为种种原因(遗留 SO、芯片市场占有率、APK 包大小等),并不是所有人都遵循这样的原则。

折中方案

Android Studio

  • Android Gradle 插件中,可以使用如下方式对 abi 进行过滤:
1
2
3
4
5
6
7
8
9
10
11
12
android {
...

defaultConfig {
...
ndk {
// 设置支持的 SO 库构架,注意这里要根据你的实际情况来设置
abiFilters 'armeabi'// 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
}
}

}

关键行:

1
abiFilters 'armeabi'// 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'

根据你的 APP 中使用的 SO 库所支持的构架具体情况,你可以进行具体设置。最终输出的 apk 中,将会包含你所选择的 abi。

像前面举出的例子,就应该只允许 armeabi。

  • 如果在添加 “abiFilter” 之后 Android Studio 出现以下提示:
1
NDK integration is deprecated in the current plugin. Consider trying the new experimental plugin

则在项目根目录的 gradle.properties 文件中添加:

1
android.useDeprecatedNdk=true

Eclipse

Eclipse 中,你需要手动控制你的工程中的这个文件夹里面的内容:

eclipse-libs

以达到上述的原则,使得在不同的构架的设备上运转正常。

具体操作,是将取你所有的 so 库所支持的构架的交集,移除其他。

lib a:

1
2
3
armeabi/a.so
armeabi­v7a/a.so
arm64­v8a/a.so

lib b:

1
2
3
armeabi/b.so
armeabi­v7a/b.so
x86/b.so

交集:

1
2
armeabi/a.so
armeabi­v7a/a.so

参考文献

What you should know about .so files

关于Android的.so文件你所需要知道的)

ABI Management

有问题?在文章下留言或者加 qq 群:453503476,希望能帮到你。

如果你习惯了命令行,你会爱上它的,因为它简单、直接,深入。

命令行

很多做 Android 开发不久的同学,习惯于使用图形界面,对命令行操作很陌生甚至恐惧。遇到 AS 运行错误,束手无策。

AS 为了确保易用性,也在 UI 界面上屏蔽了很多命令行运行的细节,导致很多人觉得 AS 难用。

这种情况,我在解决用户集成使用 Bugtags SDK 的问题的时候,经常能遇到。其实 GUI 界面的操作,绝大部分情况下,也是基于命令工具的。如果你习惯了命令行,你会爱上它的,因为它简单、直接,深入。

典型错误

AS 刚推出的时候,stackoverflow 上询问最多的问题,便是进入项目的时候,一直处于:

1
Gradle: resolve dependancies '_debugCompile'

状态,一直无法前进,到底 IDE 在做什么呢?看不出来。

一句命令行

当用户遇到问题时,我最常提醒用户使用的是在项目根目录下,运行如下命令行:

1
2
3
4
5
mac:
./gradlew clean build --info > bugtags.log

windows:
gradlew.bat clean build --info > bugtags.log

这个命令行的意思,是运行 clean 和 build 两个 gradle task,并且打开 info 参数使得输出更多的信息,最终把所有输出的信息,输出到项目根目录下的 bugtags.log 文件。用户把这个文件发给我,我根据这个输出文件,通常就能分析出问题所在。

假设命令行去除重定向输指令:

1
./gradlew clean build --info

信息将会输出在控制台,刚才提到的那个典型错误,可能是这样的:

cmd-output

其实是在下载一个比较大的文件,不用惊慌,你要做的就是 just wait! 至于是在下载什么。我想在下一篇详细描述。

如果你对基本的命令行知识有所了解,前面就已经足够了,如果你想了解更多,请继续。

扩展

在哪运行

当我给出这个命令的时候,最常见的问题,就是在哪运行。答案是控制台(Terminal)。

控制台

在 mac 下,有 terminal(bash/zsh 等),在 windows 下,则是 powershell 或者 cmd。

关键一点:

1
2
├── gradlew
├── gradlew.bat

AS 在使用 Gradle 的时候,为了灵活,或者为了应对 Gradle 系统的快速迭代,推荐使用在项目根目录中放置 Gradle 的 wrapper:gradlew 来实现对不同版本的使用。

因此,在控制台运行命令,主要是跟 gradlew 打交道。这个 wrapper,在 mac 下是一个具有执行权限的文件:gradlew,在 windows 下,是一个批处理文件:gradlew.bat

通常,mac 下在当前目录下运行可执行文件是这样:

1
./gradlew xxx

windows 下在当前目录下运行批处理文件是这样:

1
gradlew.bat xxx

Terminal 插件

AS(Intellij IDEA)已经做了一个很实用的插件:

as-terminal

点击 Terminal,AS 会帮你完成下面的操作:

  • 模拟打开 terminal
  • cd 到当前项目根目录下

快速定位文件夹

IDE 还支持将项目中的某个文件夹拖放到 Terminal 窗口中实现快速定位到这个文件夹:

terminal-drag-location

使用 help

要知道都有哪些 gradle 命令运行的参数,可以使用:

1
2
3
4
$ ./gradlew --help

USAGE: gradlew [option...] [task...]
...

来获取。下面列举几个重要的参数。

build 某个指定 module

AS 推荐的结构是 multiple project 结构,即一个 project 下,管理多个 module,如果每次都要 build 全部的 project 的话,有点浪费时间,则可以使用 -p module 参数,其中 module 是你要 build 的 module:

1
$ ./gradlew -p app clean build

明确指定不执行某个 task

Gradle 的命令存在依赖,例如 build task,是依赖于一系列的其他的 task,如果想要指定不执行某个 task,则可以使用 -x task 参数,其中 task 是要忽略的那个,这个参数可以传递多次。

1
$ ./gradlew build -x test -x lint

总结

Gradle 的命令行还有很多其他技巧,上面只是列举到了本人日常用到最多的几个。有兴趣可以留言深入讨论。

参考资料

mac-terminal

windows-terminal

有问题?在文章下留言或者加 qq 群:453503476,希望能帮到你。

Build Variant

android gradle 插件,允许对最终的包以多个维度进行组合。

1
BuildVariant = ProductFlavor x BuildType

两个维度

最常见的就是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
productFlavors {
pro {
}

fre {
}
}
lintOptions {
abortOnError false
}

buildTypes {
debug {
}
release {
}
}

其中,buildTypes 一般都会有 debug 或者release,标示编译的类型,通常在混淆代码、可调式、资源压缩上做一些区分。
productFlavor 则为了满足“同一个project,根据一个很小的区分,来打不同的包”这个需求。

这两个维度的组合,会产生如下包:

  • proDebug
  • proRelease
  • freDebug
  • proRelease

更多的维度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
flavorDimensions 'abi', 'version'

productFlavors {
pro {
dimension 'version'
}

fre {
dimension 'version'
}

arm {
dimension 'abi'
}

mips {
dimension 'abi'
}
}

buildTypes {
debug {
}
release {
}
}

productFlavor 本身定义了2个维度,记上 buildType,则有三个维度,会产生如下的包:

  • armProDebug
  • armProRelease
  • armFreDebug
  • armFreRelease
  • mipsProDebug
  • mipsProRelease
  • mipsFreDebug
  • mipsFreRelease

其中每个维度组合,都可以设置本身的 dependency、test source。下面做一个举例。

Flavor 与 Dependency

需求

module 中有若干个 flavors,例如:fre 和 pro,分别依赖不同的库,这些库有的是本地 jar 库,有的是远程库。

方案

flavor-dependency

遍历 Build Variant

需求

Bugtags 的 android sdk,有一个自动上传符号表功能, 在最初,是这样配置的:

1
2
3
4
5
6
apply plugin: 'com.bugtags.library.plugin'
bugtags {
appKey "APP_KEY"
appSecret "APP_SECRET"
mappingUploadEnabled false
}

后来,我们增加了一个 beta-live 的机制,用来区分测试和上线的 APP,这样,同一个 APP,就有两套 APP_KEY 和 APP_SECRET 了,很明显上方的配置方式就不在适用。

方案

android gradle 插件提供了 android.applicationVariants 索引来遍历所有的 build variant
后来,我们采取了一个方案,遍历 Build Variant,设置 extension 信息来兼容这种需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
afterEvaluate {
android.applicationVariants.each { variant ->
def bugtagsAppKey = null;
def bugtagsAppSecret = null;

if (variant.name.contains("debug")) {
bugtagsAppKey = 'APP_KEY_BETA'
bugtagsAppSecret = 'APP_SECRET_BETA'
} else if (variant.name.contains("release")) {
bugtagsAppKey = 'APP_KEY_LIVE'
bugtagsAppSecret = 'APP_SECRET_LIVE'
}

variant.ext.bugtagsAppKey = bugtagsAppKey
variant.ext.bugtagsAppSecret = bugtagsAppSecret
}
}

apply plugin: 'com.bugtags.library.plugin'

总结

本文主要是介绍了 build variant 的概念,还介绍了两个日常应用案例。希望对大家有帮助。

参考资料

android-build-tool

有问题?在文章下留言或者加 qq 群:453503476,希望能帮到你。

节前我在朋友圈发了一个问题:有的人说文字是1维,图片是2维,视频是3维,那移动直播是几维?

其实这是一个严肃的问题,但是相当一部分的朋友,都是回答了,三围。

这个回答很逗,但基本反映了现状和大部分的看法。除了三围,其实我在映客里面看到了更多。

移动直播具有成熟的盈利模式,也有着一部分共享经济的特质,是一个很好的生意,甚至可能会成为一个好的媒体。

风潮来袭

移动直播风潮在2016年疯狂来袭,仅 bugtags 的用户里面,就有好几家。 朋友圈里面的个把投资人,纷纷把跟过的直播 APP 的案子写在了自己的微信签名上。各种科技媒体,传播着各种直播 APP 融资消息,各大巨头加入战局的消息不绝于耳。移动直播,已然血海一片。之后传来某些 APP 涉黄被批。

嗯,果然是什么火什么会被禁。

初体验

过去一年,埋头 Bugtags 创业,写代码,做推广,琢磨盈利。猛然一抬头,发现自己已经错过了好多新鲜玩意。在51假期前一天,下了一个映客,体验一把。

几年前体验过 yy 和 9158的秀场,深感这种盈利模式的直接。但是这个市场不是已经很清楚了么,怎么最近又来一波疯狂的融资?

登陆进去之后玩了五分钟,我就感觉到了的确强烈的不一样———这一切都是因为移动。

映客的界面很简单,三个 tab:主播列表、自己开始直播、设置。

在列表中选择一个挺文艺范(后来才知道,最近在严打,小清新其实不应该是主流)封面进入直播间,一个漂亮的小女孩,正拿着手机,对耳机麦说话,看背景,是在外面喝咖啡,时不时还有些风来吹乱头发,她在讲述一些自己的经历,同时也在跟粉丝互动。

这种小清新的主题,肯定是不能够吸引主流的消费人群的(你懂的),于是我上滑,切到下一个主播,穿着比较的性感,一群人在刷着礼物,说着一些露骨的话,助理管理员帮助禁言。

我假装一个渴望女神的屌丝,为了博女神一笑,apple pay 充了6块钱(流程竟然如此的顺畅),得到60金币,开始送礼物了,就挑最便宜的:樱花雨,1金币一个。颤抖着送出了一个,期待着女神能够念到我的名字。什么?她竟然没看到!不对啊,刷得不够多?继续,连击!刷了20个!女神终于看到了,说,谢谢 xxx 的樱花雨,我心里瞬间得到了一丝的快感。

一会儿,主播说她要出去了,说在出租车上再开直播。

深入

在接下来的两天,我刷了不下百个主播,再充了几次钱,基本上搞清楚了套路。

分成

1人民币=10金币(映票),主播每得到1影票,平台分成 50%-70%。

主播

映客毕竟是个后起的平台,他需要从别的平台上挖人,相当一部分映客就是从秒拍之类的平台迁移过来的。

有些主播不一定是为了赚金币,因为他们送出的钱比自己赚到自己的多得多。

如果花钱能够得到粉丝的崇拜,玩直播或者比玩游戏更来得好玩。

光自己玩是不行的,得有几个人相互捧。

女孩子不一定需要裸露,但一定要漂亮,有个剃着平头的漂亮女孩子,也很受欢迎。

男人有市场,有些球员或者艺人,也很多人捧。

随时随地都可以直播,吃饭、逛街、喝咖啡、旅行,只要你愿意,都可以直播出来。

当主播

要体验,当然少不了亲自当一回主播。直播开始之前,映客产品设计上希望用户能够把这个直播的地址分享到社交网络。

直播的视频美颜功能很好,多糟的皮肤,都可以磨的好看。

开始直播之后,会有一些机器人进来捧场,显的不那么的落寞。

对于老男人而言,颜值不行,只能靠才艺,我选择唱个歌。

映客内置了歌曲功能,唱个 beyond 的大地吧。我老婆说混响做的相当不错,比原唱还好听,她还帮我送了几次樱花雨。我当然也得学着说:感谢宝宝的樱花雨!

观众竟然还会上涨,但还是机器人,没有人互动。

倒卖流量

主播当然不甘于只给映客打工。一切流量都得变现,于是他们的签名上,大多有微信号。

他们鼓励粉丝加好友,加微信。

有的是微商,有的是代购,有的些你懂的。

平台的变迁

移动互联网又改变了一个 PC 上成熟的生态,移动直播新 APP 们,完全可能从传统的秀场那里抢得一部分生意。

作为消费者,我面向的是一个个手机前鲜活的主播,这个人可能就是你以前认识的一个朋友,一个会写字的人,一个爱旅行的人,一个爱唠叨的人。只要你有才艺,只要你肯分享,理论上都可以成为网红。

我甚至喜欢主播拿着手机进行直播的晃动感觉,也不是很讨厌偶尔网络的延迟,因为那让我感觉更加真实,比以前9158上化好妆,端坐在电脑前的直播,真实多了。

不过话说回来,但是新人出头,真的没有那么容易。理论上出现在热门榜才有大量的曝光机会。需要一定的粉丝量、同时在线量、映票数才可以出现在热门榜。跟 AppStore 排名一样,新人需要刷榜才上得去,想维持住,得有过硬的才艺或者颜值。

后记

节前我在朋友圈发了一个问题:有的人说文字是1维,图片是2维,视频是3维,那移动直播是几维?

其实这是一个严肃的问题,但是相当一部分的朋友,都是回答了,三围。

这个回答很逗,但基本反映了现状和大部分的看法。除了三围,其实我在映客里面看到了更多。

移动直播具有成熟的盈利模式,也有着一部分共享经济的特质,是一个很好的生意,甚至可能会成为一个好的媒体。

官方文档给出了比较详细的实现步骤,本文的脉络会跟官方文档差不了太多,额外增补实际例子和一些实践经验。文中的代码已经托管到了 github 项目中。

需求

默认的 Android 打包插件会把 apk 命名成 module-productFlavor-buildType.apk,例如 app-official-debug.apk,并且会把包文件发布到固定的位置: module/build/outputs/apk 有的时候,这个命名风格并不是你所要的,你也想讲 apk 输出到别的目录。咱们通过 gradle 插件来实现自定义。这个插件的需求是:

  • 输入一个名为 nameMap 的 Closure,用来修改 apk 名字
  • 输入一个名为 destDir 的 String,用于输出位置

原理简述

插件之于 Gradle

根据官方文档定义,插件打包了可重用的构建逻辑,可以适用于不同的项目和构建过程。

Gradle 提供了很多官方插件,用于支持 Java、Groovy 等工程的构建和打包。同时也提供了自定义插件的机制,让每个人都可以通过插件来实现特定的构建逻辑,并可以把这些逻辑打包起来,分享给其他人。

插件的源码可以使用 Groovy、Scala、Java 三种语言,笔者不会 Scala,所以平时只是使用 Groovy 和 Java。前者用于实现与 Gradle 构建生命周期(如 task 的依赖)有关的逻辑,后者用于核心逻辑,表现为 Groovy 调用 Java 的代码。

另外,还有很多项目使用 Eclipse 或者 Maven 进行开发构建,用 Java 实现核心业务代码,将有利于实现快速迁移。

插件打包方式

Gradle 的插件有三种打包方式,主要是按照复杂程度和可见性来划分:

Build script

把插件写在 build.gradle 文件中,一般用于简单的逻辑,只在该 build.gradle 文件中可见,笔者常用来做原型调试,本文将简要介绍此类。

buildSrc 项目

将插件源代码放在 rootProjectDir/buildSrc/src/main/groovy 中,只对该项目中可见,适用于逻辑较为复杂,但又不需要外部可见的插件,本文不介绍,有兴趣可以参考此处

独立项目

一个独立的 Groovy 和 Java 项目,可以把这个项目打包成 Jar 文件包,一个 Jar 文件包还可以包含多个插件入口,将文件包发布到托管平台上,供其他人使用。本文将着重介绍此类。

Build script 插件

首先来直接在 build.gradle 中写一个 plugin:

1
2
3
4
5
6
7
8
9
10
11
class ApkDistPlugin implements Plugin<Project> {

@Override
void apply(Project project) {
project.task('apkdist') << {
println 'hello, world!'
}
}
}

apply plugin: ApkDistPlugin

命令行运行

1
2
3
$ ./gradlew -p app/ apkdist
:app:apkdist
hello, world!

这个插件创建了一个名为 apkdist 的 task,并在 task 中打印。

插件是一个类,继承自 org.gradle.api.Plugin 接口,重写 void apply(Project project) 方法,这个方法将会传入使用这个插件的 project 的实例,这是一个重要的 context。

接受外部参数

通常情况下,插件使用方需要传入一些配置参数,如 bugtags 的 SDK 的插件需要接受两个参数:

1
2
3
4
bugtags {
appKey "APP_KEY" //这里是你的 appKey
appSecret "APP_SECRET" //这里是你的 appSecret,管理员在设置页可以查看
}

同样,ApkDistPlugin 这个 plugin 也希望接受两个参数:

1
2
3
4
5
6
7
apkdistconf {
nameMap { name ->
println 'hello,' + name
return name
}
destDir 'your-distribution-dir'
}

参数的内容后面继续完善。那这两个参数怎么传到插件内呢?

org.gradle.api.Project 有一个 ExtensionContainer getExtensions() 方法,可以用来实现这个传递。

声明参数类

声明一个 Groovy 类,有两个默认值为 null 的成员变量:

1
2
3
4
class ApkDistExtension {
Closure nameMap = null;
String destDir = null;
}

接受参数

1
project.extensions.create('apkdistconf', ApkDistExtension);

要注意,create 方法的第一个参数就是你在 build.gradle 文件中的进行参数配置的 dsl 的名字,必须一致;第二个参数,就是参数类的名字。

获取和使用参数

在 create 了 extension 之后,如果传入了参数,则会携带在 project 实例中,

1
2
3
4
def closure = project['apkdistconf'].nameMap;
closure('wow!');

println project['apkdistconf'].destDir

进化版本一:参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ApkDistExtension {
Closure nameMap = null;
String destDir = null;
}

class ApkDistPlugin implements Plugin<Project> {

@Override
void apply(Project project) {

project.extensions.create('apkdistconf', ApkDistExtension);

project.task('apkdist') << {
println 'hello, world!'

def closure = project['apkdistconf'].nameMap;
closure('wow!');

println project['apkdistconf'].destDir
}
}
}

apply plugin: ApkDistPlugin

apkdistconf {
nameMap { name ->
println 'hello, ' + name
return name
}
destDir 'your-distribution-directory'
}

运行结果:

1
2
3
4
5
$ ./gradlew -p app/ apkdist
:app:apkdist
hello, world!
hello, wow!
your-distribution-directory

独立项目插件

代码写到现在,已经不适合再放在一个 build.gradle 文件里面了,那也不是我们的目的。建立一个独立项目,把代码搬到对应的地方。

理论上,IntelliJ IDEA 开发插件要比 Android Studio 要方便一点点,因为有对应 Groovy module 的模板。但其实如果我们了解 IDEA 的项目文件结构,就不会受到这个局限,无非就是一个 build.gradle 构建文件加 src 源码文件夹。

最终项目的文件夹结构是这样:

Java-Library

下面我们来一步步讲解。

创建项目

在 Android Studio 中新建 Java Library module “plugin”

修改 build.gradle 文件

添加 Groovy 插件和对应的两个依赖。

1
2
3
4
5
6
7
8
//removed java plugin 
apply plugin: 'groovy'

dependencies {
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
compile fileTree(dir: 'libs', include: ['*.jar'])
}

修改项目文件夹

src/main 项目文件下:

  • 移除 java 文件夹,因为在这个项目中用不到 java 代码
  • 添加 groovy 文件夹,主要的代码文件放在这里
  • 添加 resources 文件夹,存放用于标识 gradle 插件的 meta-data

建立对应文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── build.gradle
├── libs
├── plugin.iml
└── src
└── main
├── groovy
│   └── com
│   └── asgradle
│   └── plugin
│   ├── ApkDistExtension.groovy
│   └── ApkDistPlugin.groovy
└── resources
└── META-INF
└── gradle-plugins
└── com.asgradle.apkdist.properties

注意:

  • groovy 文件夹中的类,一定要修改成 .groovy 后缀,IDE 才会正常识别。
  • resources/META-INF/gradle-plugins 这个文件夹结构是强制要求的,否则不能识别成插件。

com.asgradle.apkdist.properties 文件

如果写过 Java 的同学会知道,这是一个 Java 的 properties 文件,是 key=value 的格式。这个文件内容如下:

1
implementation-class=com.asgradle.plugin.ApkDistPlugin

按其语义推断,是指定这个插件的入口类。

  • 英文敏感的同学可能会问了,为什么这个文件的承载文件夹是叫做 gradle-plugins,使用复数?没错,这里可以指定多个 properties 文件,定义多个插件,扩展性一流,可以参考 linkedin 的插件的组织方式。

  • 使用这个插件的时候,将会是这样:

    1
    apply plugin:'com.asgradle.apkdist'

    因此,com.asgradle.apkdist 这个字符串在这里,又称为这个插件的 id,不允许跟别的插件重复,取你拥有的域名的反向就不会错。

将 plugin module 传到本地 maven 仓库

参考上一篇:拥抱 Android Studio 之四:Maven 仓库使用与私有仓库搭建,和对应的 demo 项目,将包传到本地仓库中进行测试。

添加 gradle.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PROJ_NAME=gradleplugin
PROJ_ARTIFACTID=gradleplugin
PROJ_POM_NAME=Local Repository

LOCAL_REPO_URL=file:///Users/changbinhe/Documents/Android/repo/

PROJ_GROUP=com.as-gradle.demo

PROJ_VERSION=1.0.0
PROJ_VERSION_CODE=1

PROJ_WEBSITEURL=http://kvh.io
PROJ_ISSUETRACKERURL=https://github.com/kevinho/Embrace-Android-Studio-Demo/issues
PROJ_VCSURL=https://github.com/kevinho/Embrace-Android-Studio-Demo.git
PROJ_DESCRIPTION=demo apps for embracing android studio

PROJ_LICENCE_NAME=The Apache Software License, Version 2.0
PROJ_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
PROJ_LICENCE_DEST=repo

DEVELOPER_ID=your-dev-id
DEVELOPER_NAME=your-dev-name
DEVELOPER_EMAIL=your-email@your-mailbox.com

在 build.gradle 添加上传功能

1
2
3
4
5
6
7
8
9
10
apply plugin: 'maven'

uploadArchives {
repositories.mavenDeployer {
repository(url: LOCAL_REPO_URL)
pom.groupId = PROJ_GROUP
pom.artifactId = PROJ_ARTIFACTID
pom.version = PROJ_VERSION
}
}

上传可以通过运行:

1
$ ./gradlew -p plugin/ clean build uploadArchives

在 app module 中使用插件

在项目的 buildscript 添加插件作为 classpath

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
repositories {
maven{
url 'file:///Users/your-user-name/Documents/Android/repo/'
}
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-alpha3'
classpath 'com.as-gradle.demo:gradleplugin:1.0.0'
}
}

在 app module 中使用插件:

1
apply plugin: 'com.asgradle.apkdist'

命令行运行:

1
2
3
4
5
$ ./gradlew -p app apkdist
:app:apkdist
hello, world!
hello, wow!
your-distribution-directory

可能会遇到问题

1
2
Error:(46, 0) Cause: com/asgradle/plugin/ApkDistPlugin : Unsupported major.minor version 52.0
<a href="openFile:/Users/your-user-name/Documents/git/opensource/embrace-android-studio-demo/s5-GradlePlugin/app/build.gradle">Open File</a>

应该是本机的 JDK 版本是1.8,默认将 plugin module 的 groovy 源码编译成了1.8版本的 class 文件,放在 Android 项目中,无法兼容。需要对 plugin module 的 build.gradle 文件添加两个参数:

1
2
sourceCompatibility = 1.6
targetCompatibility = 1.6

真正的实现插件需求

读者可能会观察到,到目前为止,插件只是跑通了流程,并没有实现本文提出的两个需求,

那接下来就具体实现一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ApkDistPlugin implements Plugin<Project> {

@Override
void apply(Project project) {

project.extensions.create('apkdistconf', ApkDistExtension);

project.afterEvaluate {

//只可以在 android application 或者 android lib 项目中使用
if (!project.android) {
throw new IllegalStateException('Must apply \'com.android.application\' or \'com.android.library\' first!')
}

//配置不能为空
if (project.apkdistconf.nameMap == null || project.apkdistconf.destDir == null) {
project.logger.info('Apkdist conf should be set!')
return
}

Closure nameMap = project['apkdistconf'].nameMap
String destDir = project['apkdistconf'].destDir

//枚举每一个 build variant
project.android.applicationVariants.all { variant ->
variant.outputs.each { output ->
File file = output.outputFile
output.outputFile = new File(destDir, nameMap(file.getName()))
}
}
}
}
}

必须指出,本文插件实现的需求,其实可以直接在 app module 的 build.gradle 中写脚本就可以实现。这里做成插件,只是为了做示范。

上传到 bintray 的过程,就不再赘述了,可以参考拥抱 Android Studio 之四:Maven 仓库使用与私有仓库搭建

后记

至此,这系列开篇的时候挖下的坑,终于填完了。很多人借助这系列的讲解,真正理解了 Android Studio 和它背后的 Gradle、Groovy,笔者十分高兴。笔者也得到了很多读者的鼓励和支持,心中十分感激。

写博客真的是一个很讲究执行力和耐力的事情,但既然挖下了坑,就得填上,对吧?

这半年来,个人在 Android 和 Java 平台上也做了更多的事情,也有了更多的体会。

AS 系列,打算扩充几个主题:

  • Proguard 混淆
  • Java & Android Testing
  • Maven 私有仓库深入
  • 持续集成
  • ……待发掘

记得有人说,只懂 Android 不懂 Java,是很可怕的。在这半年以来,笔者在工作中使用 Java 实现了一些后端服务,也认真学习了 JVM 字节码相关的知识并把它使用到了工作中。在这个过程中,真的很为 Java 平台的活力、丰富的库资源、几乎无止境的可能性所折服。接下来,会写一些跟有关的学习体会,例如:

  • Java 多线程与锁
  • JVM 部分原理
  • 字节码操作
  • Java 8部分特性
  • ……待学习

随着笔者工作的进展,我也有机会学习使用了别的语言,例如 Node.js,并实现了一些后端服务。这个语言的活力很强,一些比 Java 现代的地方,很吸引人。有精力会写一写。

因为业务所需,笔者所经历的系统,正在处于像面向服务的演化过程中,我们期望建立统一的通讯平台和规范,抽象系统的资源,拆分业务,容器化。这是一个很有趣的过程,也是对我们的挑战。笔者也希望有机会与读者分享。

一不小心又挖下了好多明坑和无数暗坑,只是为了激励自己不断往前。在探索事物本质的旅途中,必然十分艰险,又十分有趣,沿途一定风光绚丽,让我们共勉。

参考文献

官方文档

系列导读

本文是笔者《拥抱 Android Studio》系列第四篇,其他篇请点击:

拥抱 Android Studio 之一:从 ADT 到 Android Studio

拥抱 Android Studio 之二:Android Studio 与 Gradle 深入

拥抱 Android Studio 之三:溯源,Groovy 与 Gradle 基础

拥抱 Android Studio 之四:Maven 公共仓库使用与私有仓库搭建

拥抱 Android Studio 之五:Gradle 插件使用与开发

有问题?在文章下留言或者加 qq 群:453503476,希望能帮到你。
想要及时收到最新博客文章,请关注:

番外

笔者 kvh 在开发和运营 bugtags.com,这是一款移动时代首选的 bug 管理系统,能够极大的提升 app 开发者的测试效率,欢迎使用、转发推荐。

笔者目前关注点在于移动 SDK 研发,后端服务设计和实现。

我们团队长期求 PHP 后端研发,有兴趣请加下面公众号勾搭:

bugtags

下面咱们就来聊聊技术人的素养:如何更好的提出技术问题。
笔者从事 Bugtags.com 开发运营以来,除了开发任务以外,最重要的工作就是在 qq 群里面回答用户的问题。

这半年来,少说也接待了上千个用户了。笔者发现,有相当一部分的用户,提问方式和技巧都有问题,这样导致了我们额外的客服量,也使得自己的问题得不到及时满意的回答。

下面咱们就来聊聊技术人的素养:如何更好的提出技术问题。

直接了当

有的用户,喜欢先问:『有人在吗?』

其实这个问句真的是很无意义,有问题直接提出来就好,有工作人员或者热心人看到了,能回答的自然就会回答,为什么还需要问有没有人在家呢?

目的明确

有的用户,其实只是对某些技术点感兴趣,但是表现出来,是要给我们反馈 bug。这样我们就很摸不着头脑。

是吐槽、建议、反馈、赞扬还是学习?请一上来就说明。

有价值

列举一些个人认为没有价值的问题:

  • PHP 是不是最好的语言?
  • VIM 还是 Emacs 好?
  • 学 Android 有没有前途?

这种问题,没有讨论的价值。

已尝试求解

其实大部分我们遇到的技术问题,只要在谷歌或者百度上查找,大多能找到答案。

大部分情况下,使用 Bugtags 遇到的问题,都能通过我们的帮助文档解决。

但是我发现中国人真的很着急,文档也不看,帮助也不看,上来就问。

问题着眼点小

切忌提一些特别大的问题,例如『安卓如何管理内存』,这种是需要一些列文章才能阐述清楚的问题,显然是不适合在 QQ 群上提问的。

背景信息充足

举个例子,Bugtags SDK 支持 Android 和 iOS,但是大部分用户提问的时候,会忽略了这个信息。同样,常用的操作系统,也有 OSX 和 Windows,很多人也会忽略。

这里面其实有个思维盲点,提出问题的人,会忽略一些显而易见的环境差异,认为别人了解背景信息,具有跟他一样的运行环境和操作步骤。

问题描述信息充分

这个也需要换位思考,假设你是一个热心人,尝试帮助群上的人解答问题。是不是希望问题越准确越好?

一些要点请备齐:

  • 软硬环境及版本
  • 操作步骤
  • 期待结果
  • 错误信息,最好是文本而不是截图
  • 设备运行的 log
  • 现场截图
  • 已尝试过的解决方案
  • 怀疑的点

重现问题的 Demo

  • 创建你自己的 demo 程序,操作要友好
  • 加上使用说明,描述你所遇到的问题,具体环境,操作步骤,帮助别人快速重现你的问题
  • 打包你的 demo,上传到 github 或者百度云盘,让别人可以很快下载到

一个范例

stack-overflow

总结

提问技巧,也是属于沟通技巧之一。

笔者认为要达到有效的沟通,双方都需要有同理心,要换位思考。

愿这篇文章能为大家带来一些启发,能够收获更多满意的答案。