Project RC

一种标准阿西的设计与实现。

FusionCache 实现思路和使用方法

创建于
分类:Dev
标签:AndroidFusionCacheCache

前几天看《Android 开发艺术探索》关于 Bitmap 加载的那章,里面讲解了一个内存和磁盘二级缓存的 ImageLoader 的实现方法,然后就突然想自己写一个内存+磁盘的混合缓存框架,正好平时上课闲得慌,就在课上构思,最终在昨天完成了这个 FusionCache。虽然注释写得挺多,但还是怕以后看不懂,所以这里记录一下实现思路。

实现思路

首先这个缓存应该基于键值对,使用起来方便嘛,作为一个缓存,本身就是为了增强应用使用体验而用的,并不是什么逻辑上的核心部件,那么就应该用起来越简单越好。

并且缓存应当可以分别设置内存和磁盘缓存的容量上限,也就是说要计算每个对象在内存和磁盘中所占空间。

由于不是所有类的对象都能存到磁盘中,也不是所有对象都能成功计算内存占用,所以需要限制支持存入缓存的对象类型,参考 ASimpleCache,支持 StringJSONObjectJSONArraybyte[]BitmapDrawableSerializable

考虑到如果做混合缓存,一定得分别独立实现内存缓存和磁盘缓存,为了降低耦合,这两个类肯定是互相不知道对方的存在的,但是作为缓存,它们的 API 应该相似,包括整体的混合缓存,也应该使用相似的 API,于是写了个 Cache 接口来统一之。本来考虑有可能某些接口可以预先给出默认实现而不用依赖于具体的缓存类,于是加了个 AbstractCache 类用来给具体类继承,不过现在暂时留空了。FusionCacheMemCacheDiskCache 都是继承自 AbstractCache

并且,这个混合缓存遵循实现以下动态机制:

  • 插入对象时:
  • 优先放进内存缓存,如果对象放进去时,为了腾出空间而删除了较老的缓存,则把这些删除掉的缓存放进磁盘缓存;
  • 如果对象大小超过了内存缓存的最大容量(无法放进内存缓存),则放进磁盘缓存;
  • 如果对象大小超过了磁盘缓存的最大容量,则不缓存。

  • 取出对象时:

  • 如果对象在内存缓存,则直接取出返回;
  • 如果对象在磁盘缓存,则取出后放进内存缓存(原磁盘缓存中的缓存文件不删除),并返回结果;
  • 如果对象不存在,则返回 null

  • 删除对象时,内存和磁盘缓存中所有对应于要删除的键的缓存都将被删除。

  • 清空缓存时,所有内存和磁盘缓存,以及磁盘缓存目录都会被删除。

这样一个动态机制通过内部适时地调用内存缓存和磁盘缓存来实现,所以下面分别独立实现这两种缓存。

内存缓存

Android 系统内置了一个 LruCache 类用来实现 LRU 算法的内存缓存,它内部用了一个 LinkedHashMap 来存对象的强引用,显然这其实可以直接用来作内存缓存。

但是由于从整体上看,混合缓存需要能够在内存缓存满了的情况下,把较老的那些由 LRU 算法淘汰掉的对象转存到磁盘缓存,内置的 LruCache 没法做到这一点,它只会在删除较老缓存对象时调用 void entryRemoved(boolean, K, V, V) 来通知其子类,一次一个值。为了实现在内存缓存满了的情况下,put() 方法能够返回一个被删除了的对象的列表,考虑继承 LruCache 来扩展,并且由于 entryRemoved() 必须是删除一个对象调用一次,就是说得在第一次删除之前做一个标记,然后在最后一次删除结束,也就是 put 完毕之后,提供被删除的对象列表,于是新增 mRecentlyEvictedEntryListmMarkRecentlyEvicted 成员变量,但开关标记和清空列表只能从外部操作,如果在 MemCache 类里面做这件事,会导致耦合比较高,于是用一个 LruCacheWrapper 类来包装扩展的 ExtendedLruCacheLruCacheWrapper 对外提供一个可以获取到被删除的对象列表的 put() 方法,即 V put(K, V, List<Entry<K, V>>),于是 MemCache 只需要直接使用这个包装类即可,不用担心内部是怎么实现的。

之前说到需要计算每个对象的空间占用,由于其实内存缓存和磁盘缓存内部都使用了 LruCacheWrapper,而对象在内存和在磁盘上占用的大小是不一样的,所以这个大小计算工作应该拿出来放在内存缓存和磁盘缓存中分别实现,于是 LruCacheWrapper 内部声明了一个 Delegate 接口用来实现 sizeOf(),而 ExtendedLruCache 里的 sizeOf() 只需要调用这个接口的实现就行。

搞清楚了内部 LruCacheWrapper 的实现,接下来实现 Cache 接口的方法就很简单了,只需要考虑到特别提供一个 package only 的能够获取被删除的对象列表的 put() 方法,其它就是简单的调用 LruCacheWrapper

磁盘缓存

这里磁盘缓存没有用 DiskLruCache,而是直接操作缓存文件。

为了实现 LRU 算法,其实只要继续复用 LruCacheWrapper 就好了,不仅有了 LRU,而且还有了缓存容量控制,只不过这里 LruCacheWrapper 里面不存对象强引用,只是存缓存文件的大小即可,方便后续其它操作时候正确计算缓存总大小的变化,而实际的缓存操作,就是按不同对象类型写入到文件、读取文件、删除文件即可,另外需要实现 LruCacheWrapperentryRemoved() 从而在磁盘缓存满了的时候,把最老的缓存文件删掉。

另外,磁盘缓存由于也采用 LRU,因此为了下次启动时能恢复缓存对象的次序,需要维护一个日志文件,因此通过 saveJournal()restoreJournal() 来保存和恢复日志文件。

混合缓存

其实混合缓存只要弄清楚在内存和磁盘缓存之间转移对象的逻辑,就非常容易实现。

为了方便使用,可以让用户选择开启或不开启混合模式,如果开了,那么就按前面讲的动态机制来执行操作,如果不开启,那么就只能通过 getMemCache()getDiskCache() 来分别使用内存和磁盘缓存。

具体的 put、get、remove、clear 操作的实现只需要根据动态机制的逻辑来调用相应的内存缓存和磁盘缓存的方法即可。

另外,提供一个将内存中的缓存全部存入磁盘缓存的方法,毕竟在退出应用的时候,我们不希望丢掉内存缓存(比如有 Bitmap 在里面,由于经常使用,还没有存到磁盘缓存的情况)。

使用方法

根据上面的实现思路实现了之后,就可以很方便地使用缓存,因为 API 实在太简单了……如下:

FusionCache cache = new FusionCache(
        getApplicationContext(),
        4 * 1024 * 1024, // 缓存容量的单位是字节
        50 * 1024 * 1024,
        true // 开启混合缓存模式,默认为 true
);

cache.put("string", "This is a string.");
cache.put("jsonObject", new JSONObject("{}"));
cache.put("jsonArray", new JSONArray("[]"));
cache.put("bytes", new byte[10]);
cache.put("bitmap", Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
cache.put("drawable", getDrawable(R.mipmap.ic_launcher));

String string = cache.getString("string");
JSONObject jsonObject = cache.getJSONObject("jsonObject");
JSONArray jsonArray = cache.getJSONArray("jsonArray");
byte[] bytes = cache.getBytes("bytes");
Bitmap bitmap = cache.getBitmap("bitmap");
Drawable drawable = cache.getDrawable("drawable");

cache.saveMemCacheToDisk(); // 将内存缓存中的内容全部保存到磁盘缓存, 一般在应用退出时调用

cache.remove("bitmap");
cache.clear();

得益于都实现了 Cache 接口,MemCacheDiskCache 的使用方法也几乎和上面的一样,只是构造方法有所不同。

最后

其实这个缓存框架实现起来难度真的不大,不过这次注释、文档都写得很齐全,还是很爽的哈,自我感觉相比以前有了挺大的进步。虽然客观上来说,距离那些菊苣们还是有较大差距,但不管怎么说,只要自己在不断进步,就是最好了。

FusionCache 实现思路和使用方法

在 OS X 上编译 AOSP 源码

创建于 更新于
分类:Dev
标签:AndroidAOSP

昨天本来是想把 AOSP 的源码下下来方便查阅的,然后莫名其妙就突然打算自己编译一下试试,然后就编译了,中间遇到一些坑,在这里记录一下。

1. 准备环境

按道理来说官方是推荐用 Ubuntu LTS 来编译的,不过我也没装,所以就用 OS X 了,AOSP 官网也是有 OS X 的环境配置教程的(Setting up a Mac OS build environment)。过程基本和官网的教程一致。

1.1 创建区分大小写的分区

OS X 默认的文件系统是「case-preserving but case-insensitive」的,也就是文件名保留大小写,但实际并不区分,而 AOSP 建议用区分大小写的文件系统,于是需要创建新分区,运行:

hdiutil create -type SPARSE -fs 'Case-sensitive Journaled HFS+' -size 100g ~/android.dmg

-type SPARSE 表示在使用中动态增加镜像的大小(而不是一次性占满),-size 100g 指定大小,官方声称「A size of 25GB is the minimum to complete the build」,不要信,我一开始创建的 50GB 都不够用,建议至少 100GB。

之后 ~ 目录(当然这个目录可以自己指定)下会生成一个 android.dmg.sparseimage。如果后期发现不够用,需要扩大空间,运行下面命令(注意别忘了先推出镜像):

hdiutil resize -size <new-size-you-want>g ~/android.dmg.sparseimage

1.2 安装其它依赖程序

JDK 不用说,开发 Android 应用肯定是装了的。

另外还需要装 Xcode,是因为编译中需要用到一个 OS X SDK 以及一些工具链。装好 Xcode 检查一下你的环境变量里面有没有 MAC_SDK_VERSION 这个变量,如果没有,手动设置一下,赋值为你的 OS X 版本,比如 export MAC_SDK_VERSION=10.11,因为编译时需要根据这个环境变量找到 Xcode 里面那个 OS X SDK 的路径。

然后 gmakegnupg 等程序,这些我是用 brew 装的,官方让用 ports,反正一个道理:

brew install libsdl gnupg coreutils findutils gnu-sed pngcrush xz

make 应该是 OS X 自带了的,没有的话,上面再加装一个 gmake。后面几个程序官网并没有要装,但是我在第一次编译时候遇到没有 xz 命令然后编译跪了的情况,所以后来搜了一些其它教程里面要装的就一起装上了,免得又编译到一半说缺依赖。

注意有一个坑,OS X 自带的 cURL 是基于 SecureTransport(运行 curl --version 如果输出里面包含「SecureTransport」那就是了),但是编译 Android M 以上会用到 Jack,然后 Jack 需要基于 OpenSSL 的 cURL,于是和自带的不兼容,解决办法是用 brew 再装一个 OpenSSL 的:

brew install curl --with-openssl
export PATH=$(brew --prefix curl)/bin:$PATH

注意新装的这个不会直接覆盖系统自带的,所以需要手动改一下 PATH 去改变默认的 cURL,也就是上面第二行干的事,把它放到 .profile 之类的里面,运行 curl --version 检查一下,输出里面有「OpenSSL」而不是「SecureTransport」就表示成功。

1.3 设置 ccache

就是简单按照官网的指示 Optimizing a build environment,没什么异常。

2. 下载源码

可以用 清华 TUNA 源中科大 LUG 源

操作过程与官网教程(Downloading the Source)一致,除了把 Git 仓库链接换成镜像源而已。

源码很大,没记错有 20+GB,没什么注意事项,中间尽量别断网就行(虽然我断了一次,重新 repo sync 也没有什么奇怪的事发生)。

3. 编译

编译要求 Bash shell,其它 shell 不行。

. build/envsetup.sh
lunch aosp_arm-eng

官网给的代码的编译目标是针对模拟器的,也就是这里的 aosp_arm-eng,如果你需要给真机编译,需要填相应的编译目标(也可以先直接 lunch 然后选),具体的 Nexus 设备对应的编译目标参数见 Selecting a device build

如果你打算编译真机的镜像,还需要额外下载相应机型的二进制驱动文件,如果编译 master 分支,在 Binaries Preview for Nexus Devices 下载,如果是指定的版本分支,在 Google's Nexus driver page 下载。下载好的压缩文件解压后是一些脚本,放到 AOSP 源码的主目录分别运行,根据提示需要输入「I ACCEPT」来同意协议,运行结束后主目录下会多出 vendor 文件夹,里面就是相应的驱动。

然后就可以开始编译了:

make -j8

这里 -j 后面的数字一般设置成 CPU 线程数的一倍或两倍,用来支持多线程编译,比如四核心八线程的 CPU 可以用 make -j8make -j16

注意如果开启了多线程编译,编译中出错停止的话,出错的输出可能不在最下方,你需要判断到底是哪些输出提示了出错。

4. 刷机

编译成功之后,镜像文件在 out/target/product/hammerhead,这里的 hammerhead 也就是设备代号,我编译目标选的 Nexus 5,所以是 hammerhead,如果选的是模拟器则是 generic

编译时自动设置了一些环境变量(比如 ANDROID_PRODUCT_OUT),所以完成后不用切换目录,直接 adb reboot bootloaderfastboot flashall -w 就可以刷进手机了,而如果编译目标是模拟器,则直接 emulator 就可以启动模拟器。

5. 遇到并解决的问题

这里记录我在编译过程中遇到的问题,在上面的过程记录中,我已经在可能出问题的地方写了正确的做法,如果你是自己编译时遇到问题,也许可以在这里找到解决办法。

5.1 报错找不到目录 -mmacosx-version-min=10.6

这个报错我搜了很久都没有找到解决办法,然后去源码的 build 目录搜关键词,找到了 build/soong/cc/x86_darwin_host.go 这个文件,分析后明白了,这个错误是因为在构建 shell 命令的时候,因为没找到 OS X SDK 路径,于是那个值返回了空,然后本来这条编译命令应该有这么一串参数 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -mmacosx-version-min=10.11,但是中间那个 SDK 的路径没拿到,OS X 版本也没拿到,于是变成了 -isysroot -mmacosx-version-min=10.6,导致报错,根源就是环境变量里没有 MAC_SDK_VERSION,运行 export MAC_SDK_VERSION=10.11 解决。

5.2 报错 Unsupported curl, please use a curl not based on SecureTransport

无法启动 Jack server,完整报错信息:

FAILED: /bin/bash -c "(prebuilts/sdk/tools/jack-admin install-server prebuilts/sdk/tools/jack-launcher.jar prebuilts/sdk/tools/jack-server-4.8.ALPHA.jar  2>&1 || (exit 0) ) && (JACK_SERVER_VM_ARGUMENTS=\"-Dfile.encoding=UTF-8 -XX:+TieredCompilation\" prebuilts/sdk/tools/jack-admin start-server 2>&1 || exit 0 ) && (prebuilts/sdk/tools/jack-admin update server prebuilts/sdk/tools/jack-server-4.8.ALPHA.jar 4.8.ALPHA 2>&1 || exit 0 ) && (prebuilts/sdk/tools/jack-admin update jack prebuilts/sdk/tools/jacks/jack-2.28.RELEASE.jar 2.28.RELEASE || exit 47; prebuilts/sdk/tools/jack-admin update jack prebuilts/sdk/tools/jacks/jack-3.36.CANDIDATE.jar 3.36.CANDIDATE || exit 47; prebuilts/sdk/tools/jack-admin update jack prebuilts/sdk/tools/jacks/jack-4.7.BETA.jar 4.7.BETA || exit 47 )"
Unsupported curl, please use a curl not based on SecureTransport
Jack server installation not found
Unsupported curl, please use a curl not based on SecureTransport
Unsupported curl, please use a curl not based on SecureTransport

这是因为 cURL 版本和 Jack 工具链不兼容,在 这里 找到解决办法,brew install curl --with-openssl 来重新安装一个基于 OpenSSL 的 cURL,并修改环境变量以覆盖系统自带的版本:export PATH=$(brew --prefix curl)/bin:$PATH

5.3 其它关于 Jack server 的错误

具体错误信息没有记下来,总之有时候会因为其它原因 Jack server 启动不了,一种可能性是因为已存在 ~/.jack-server,把它删掉,并运行 jack-admin kill-server 杀掉进程(如果它确实在运行的话)。

5.4 磁盘空间不够

官网说 25GB 就够了,但其实远远不够,建议分 100GB。

5.5 报错 pointer being freed was not allocated

完整报错信息:

FAILED: /bin/bash -c "(mkdir -p out/target/product/hammerhead/obj/PACKAGING/recovery_patch_intermediates/ ) && (PATH=out/host/darwin-x86/bin:\$PATH out/host/darwin-x86/bin/imgdiff out/target/product/hammerhead/boot.img out/target/product/hammerhead/recovery.img out/target/product/hammerhead/obj/PACKAGING/recovery_patch_intermediates/recovery_from_boot.p )"
imgdiff(84118,0x7fff7443e000) malloc: *** error for object 0x10fdebf8a: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
failed to reconstruct target deflate chunk 1 [(null)]; treating as normal
chunk 0: type 0 start 0 len 8452106
chunk 1: type 2 start 8452106 len 2593792
chunk 2: type 0 start 9897581 len 403
Construct patches for 3 chunks...
patch   0 is 206 bytes (of 8452106)
patch   1 is 618751 bytes (of 1445475)
patch   2 is 118 bytes (of 0)
chunk   0: normal   (         0,    8452106)         206
chunk   1: deflate  (   8452106,    2443136)      618751  (null)
chunk   2: raw      (  10895242,        118)
/bin/bash: line 1: 84118 Abort trap: 6

这个也是搜了很久未果,仔细观察发现是 out/host/darwin-x86/bin/imgdiff 这个程序运行时出现了多次释放同一个指针的问题,没找到好的解决办法,于是索性找到它的源码 bootable/recovery/applypatch/imgdiff.cpp,根据报错前后的输出,找到相应的代码段,是在 main 函数结尾的地方,把 free(patch_data) 以及之前的一个循环里释放指针的代码给注释掉,然后重新编译。

5.6 报错 error: cannot define category for undefined class 'NSUserActivity'

完整报错信息:

FAILED: /bin/bash -c "(prebuilts/misc/darwin-x86/ccache/ccache prebuilts/clang/host/darwin-x86/clang-2812033/bin/clang++    -I external/valgrind/include -I external/valgrind -I external/libchrome -I out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates -I out/host/darwin-x86/gen/SHARED_LIBRARIES/libchrome_intermediates -I libnativehelper/include/nativehelper \$(cat out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/import_includes) -isystem system/core/include -isystem system/media/audio/include -isystem hardware/libhardware/include -isystem hardware/libhardware_legacy/include -isystem hardware/ril/include -isystem libnativehelper/include -isystem frameworks/native/include -isystem frameworks/native/opengl/include -isystem frameworks/av/include -isystem frameworks/base/include -isystem out/host/darwin-x86/obj/include -c  -fno-exceptions -Wno-multichar -fPIC -funwind-tables -D__STDC_FORMAT_MACROS -D__STDC_CONSTANT_MACROS -O2 -g -fno-strict-aliasing -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk -mmacosx-version-min=10.8 -DMACOSX_DEPLOYMENT_TARGET=10.8 -integrated-as -fstack-protector-strong -m32 -msse3 -DANDROID -fmessage-length=0 -W -Wall -Wno-unused -Winit-self -Wpointer-arith -DNDEBUG -UDEBUG -D__compiler_offsetof=__builtin_offsetof -Werror=int-conversion -Wno-reserved-id-macro -Wno-format-pedantic -Wno-unused-command-line-argument -fcolor-diagnostics   -target i686-apple-darwin  -Wsign-promo -Wno-inconsistent-missing-override   -Wall -Werror -D__ANDROID_HOST__ -DDONT_EMBED_BUILD_METADATA -D_FILE_OFFSET_BITS=64 -Wno-deprecated-declarations -fPIC -D_USING_LIBCXX -std=gnu++14 -nostdinc++  -Werror=int-to-pointer-cast -Werror=pointer-to-int-cast  -Werror=address-of-temporary -Werror=null-dereference -Werror=return-type    -MD -MF out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/sys_info_mac.d -o out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/sys_info_mac.o external/libchrome/base/sys_info_mac.mm ) && (cp out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/sys_info_mac.d out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/sys_info_mac.P; sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\\\\$//' -e '/^\$/ d' -e 's/\$/ :/' < out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/sys_info_mac.d >> out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/sys_info_mac.P; rm -f out/host/darwin-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/sys_info_mac.d )"
In file included from external/libchrome/base/sys_info_mac.mm:20:
external/libchrome/base/mac/sdk_forward_declarations.h:505:12: error: cannot define category for undefined class 'NSUserActivity'
@interface NSUserActivity (YosemiteSDK)
           ^
../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/System/Library/Frameworks/AppKit.framework/Headers/NSApplication.h:21:8: note: forward declaration of class here
@class NSUserActivity;
       ^
1 error generated.

研究了错误输出以及出错的这条命令之后发现 external/libchrome/base/sys_info_mac.mmexternal/libchrome/base/mac/sdk_forward_declarations.h 这两个文件里面会根据不同的 OS X 系统版本做一些不同的事情,可以看到出错的这条命令里面有 -mmacosx-version-min=10.8 -DMACOSX_DEPLOYMENT_TARGET=10.8 这样一些参数,这和问题 5.1 有点像,问题同样出在 build/soong/cc/x86_darwin_host.go 这个文件,查看该文件发现 mmacosx-version-minDMACOSX_DEPLOYMENT_TARGET 这两个参数是由 macSdkVersion 这个变量来的,而这个变量默认值被设置成了 darwinSupportedSdkVersions[0],而 darwinSupportedSdkVersions 是一个数组,内容如下:

darwinSupportedSdkVersions = []string{
    "10.8",
    "10.9",
    "10.10",
    "10.11",
}

默认是第一个元素也就是 10.8,而我 OS X 版本是 10.11,于是把 pctx.StaticVariable("macSdkVersion", darwinSupportedSdkVersions[0]) 改成 pctx.StaticVariable("macSdkVersion", darwinSupportedSdkVersions[3]),重新编译,就好了。

5.7 编译出来的镜像刷到手机后无法开机,卡在 Google 标志

这是因为没有打包设备的二进制驱动文件,需要额外下载并重新编译镜像。

如果编译 master 分支,在 Binaries Preview for Nexus Devices 下载,如果是 指定的版本分支,在 Google's Nexus driver page 下载。下载好的压缩文件解压后是一些脚本,放到 AOSP 源码的主目录分别运行,根据提示需要输入「I ACCEPT」来同意协议,运行结束后主目录下会多出 vendor 文件夹,里面就是相应的驱动,然后重新运行编译即可。

6. 参考资料

在 OS X 上编译 AOSP 源码

用 Let's Encrypt 免费签发 SSL 证书

创建于 更新于
分类:Ops
标签:SSLLet's Encrypt

昨天尝试给博客加上了 SSL,用的 Let's Encrypt,这玩意可以免费签发有效期 90 天的 SSL 证书,使用也挺简单的,这里总结一下。

安装 Let's Encrypt

git clone https://github.com/certbot/certbot
cd certbot
./certbot-auto

第一次执行 ./certbot-auto 会安装各种依赖环境,会比较慢。

如果你用的是 Apache,那么直接 ./certbot-auto --apache 就会全自动配置,中间会提示你选择需要开启 SSL 的站点之类的。如果你用的是其它服务器软件或者需要手动获取证书,看下面。

获取 SSL 证书

这里只讨论 Manual 模式

首先要把需要签证书的域名解析到当前服务器的 IP,这里以 ssl.r-c.im 演示,然后运行 ./certbot-auto certonly --manual -d ssl.r-c.im,这里 ssl.r-c.im 换成要签的域名,也可以同时签多个域名,只需要加多个 -d 选项,如 ./certbot-auto certonly --manual -d ssl.r-c.im -d ssl2.r-c.im

耐心等待,这里可能需要等较长时间,直到出现下图这样:

image

选「Yes」,然后会出现类似于下面这样的内容:

Make sure your web server displays the following content at
http://ssl.r-c.im/.well-known/acme-challenge/7huixD-nddpR3c0T6aKt_MXqrpox4brEU4yA2rLNOIY before continuing:

7huixD-nddpR3c0T6aKt_MXqrpox4brEU4yA2rLNOIY.j0H7Twj9gLcy7RfbIMiW1qBaOJNa88UfRKlp0D96CaI

If you don't have HTTP server configured, you can run the following
command on the target server (as root):

mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
cd /tmp/letsencrypt/public_html
printf "%s" 7huixD-nddpR3c0T6aKt_MXqrpox4brEU4yA2rLNOIY.j0H7Twj9gLcy7RfbIMiW1qBaOJNa88UfRKlp0D96CaI > .well-known/acme-challenge/7huixD-nddpR3c0T6aKt_MXqrpox4brEU4yA2rLNOIY
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \
"import BaseHTTPServer, SimpleHTTPServer; \
s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.serve_forever()"
Press ENTER to continue

这里需要进行进行 ACME Challenge,按它的提示,如果已有 HTTP 服务器就确保 http://ssl.r-c.im/.well-known/acme-challenge/7huixD-nddpR3c0T6aKt_MXqrpox4brEU4yA2rLNOIY 这个路径返回的内容是 7huixD-nddpR3c0T6aKt_MXqrpox4brEU4yA2rLNOIY.j0H7Twj9gLcy7RfbIMiW1qBaOJNa88UfRKlp0D96CaI,如果没有就用 Python 临时搭一个(直接复制它给的命令运行即可)。

HTTP 服务器配置好之后,按回车继续,等它验证通过即获得了 SSL 证书,默认放在了 /etc/letsencrypt/live/ssl.r-c.im/ 目录下。如果后面证书到期了只需要重新进行这一步即可。

在 Nginx 上配置 SSL

新建配置文件如下:

server {
    listen 80;
    server_name ssl.r-c.im;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443;
    server_name ssl.r-c.im;

    ssl                  on;
    ssl_certificate      /etc/letsencrypt/live/ssl.r-c.im/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/ssl.r-c.im/privkey.pem;

    ssl_session_timeout  5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    location / {
        ...
    }
}

这里把 HTTP 的请求重定向到了 HTTPS,当然你也可以不这么做,配置完运行 nginx -s reload 即可。

用 Let's Encrypt 免费签发 SSL 证书

Android 调用系统相机拍摄照片

创建于
分类:Dev
标签:JavaAndroid

1. 获取缩略图

使用 Intent 可以很方便地调用系统相机,通过 startActivityForResult 方法启动,代码如下:

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, REQUEST_CODE); // REQUEST_CODE 为预先定义的 int 常量

重载 Activity 的 onActivityResult 方法,可以获取返回的结果中的 Bitmap 并显示,代码如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (resultCode == RESULT_OK && requestCode == REQUEST_CODE) {
        Bundle bundle = data.getExtras();
        Bitmap bitmap = (Bitmap) bundle.get("data");
        mImageView.setImageBitmap(bitmap); // mImageView 为预先定义的 ImageView
    }
}

出于性能方面的考虑,onActivityResult 中得到 data 里的 Bitmap 并不是完整照片,而是压缩过的很不清晰的缩略图。

2. 存储完整图片

只能获取缩略图显然大多数情况下不符合我们的预期,有个办法就是先把拍摄的照片存储下来,然后再按路径读取它并显示,需要修改一下调用系统相机的代码:

mFilePath = getExternalFilesDir(null).getPath() + "/temp.png"; // mFilePath 为预先声明的成员变量
Uri picUri = Uri.fromFile(new File(mFilePath));
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);
startActivityForResult(intent, REQUEST_CODE);

第 4 行代码指定了图片存储的地址,这里的 filePath 是 SD 卡根目录下 Android/data/<PackageName>/files/temp.png

此时拍摄完照片后,完整图片会被存储到指定位置,同样可以在 onActivityResult 方法中读取,代码如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (resultCode == RESULT_OK && requestCode == REQUEST_CODE) {
        try (FileInputStream fis = new FileInputStream(mFilePath)) {
            Bitmap bitmap = BitmapFactory.decodeStream(fis);
            mImageView.setImageBitmap(bitmap1);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

由于这里图片存储的路径是在应用自己的外部 data 目录中,较新版本的 Android 中无需 READ_EXTERNAL_STORAGE 权限,详见 Android存储访问及目录

有一个比较坑的点就是,如果你试图在 intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri) 这里把存储路径指定到应用的内部 data 目录 /data/data/<PackageName> 中,调用系统相机拍了照之后点确定会卡在那没反应,稍微想一下就会发现,这里调用的系统相机显然是不能直接访问我们应用的内部 data 目录的,所以需要把存储路径指定在外部存储中。(见 Camera not working/saving when using Cache Uri as MediaStore.EXTRA_OUTPUT

Android 调用系统相机拍摄照片

在 Android Studio 中使用 Android NDK

创建于
分类:Dev
标签:JavaAndroidNDK

1. 环境

Android Studio 从 1.3 版本开始加入了 NDK 支持(见 Android NDK Preview),所以需要使用 NDK 的话,需更新到 >=1.3 的版本,本文中使用的是 1.5 版(写此文时最新版)。

NDK 版本要求 ndk-r10e(写此文时最新版),由于众所周知的原因,Android Studio 自带的 SDK 管理器下载 NDK 可能会失败,可以单独下载(可在 这里 下载)。

本文的操作系统环境为 OS X,不过 Windows 和 Linux 中的操作应该也相似。

2. 配置 Android 项目

首先在 Android Studio 创建一个新的项目,从

image

里的「Project Structure」或工具栏上的

image

进入「Project Structure」配置窗口,在「Android NDK Location」那里手动选一下之前下载的 ndk 的目录,然后点「OK」保存。

接着我们需要修改三个文件,分别是 ./build.gradle./app/build.gradle./gradle/wrapper/gradle-wrapper.properties,修改的内容如下:

  • ./gradle/wrapper/gradle-wrapper.properties

distributionUrl 的 gradle 版本设置为 2.8,即 distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip

  • ./build.gradle

把 gradle 版本改为实验版:将 classpath 'com.android.tools.build:gradle:1.5.0' 改为 classpath 'com.android.tools.build:gradle-experimental:0.4.0'(0.4.0 为写此文时最新版),改完之后文件内容应该类似于下面这样:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle-experimental:0.4.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  • ./app/build.gradle

这个文件改动比较复杂,首先 apply plugin: 'com.android.application' 改为 apply plugin: 'com.android.model.application',然后把 android { } 整个放在一个 model { } 中,然后把 android { } 中所有属性都加上 = 等号,然后 buildTypes { } 放到 android { } 的外面,也就是在 model { } 中和 android { } 并列,并在前面加上 android.,然后 android { } 里的 defaultConfig 后加上 .with,然后在 minSdkVersiontargetSdkVersion 后面加上 .apiLevel,然后把 android.buildTypes { }proguardFiles 那行改为 proguardFiles.add(file("proguard-rules.pro")),然后在 model { } 中加上下面的代码:

android.ndk {
    moduleName = "ndkdemo"
}

全部改完内容类似于下面这样:

apply plugin: 'com.android.model.application'

model {
    android {
        compileSdkVersion = 23
        buildToolsVersion = "23.0.2"

        defaultConfig.with {
            applicationId = "com.demo.ndk"
            minSdkVersion.apiLevel = 14
            targetSdkVersion.apiLevel = 23
            versionCode = 1
            versionName = "1.0"
        }
    }

    android.buildTypes {
        release {
            minifyEnabled = false
            proguardFiles.add(file("proguard-rules.pro"))
        }
    }

    android.ndk {
        moduleName = "ndkdemo"
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
}

3. 编写 C 代码

./app/src/main 目录中创建一个名为 jni 的文件夹,然后在 MainActivity.java 中加入导入动态库、声明 native 函数、测试的代码,MainActivity.java 内容如下:

package com.demo.ndk;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("ndkdemo");
    }

    private native int test();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = (TextView) findViewById(R.id.textView);
        tv.setText(String.valueOf(test()));
    }
}

这时候点 native 函数声明边上的红色灯泡,会提示「Create function ...」来创建相应的 C 函数,点击后就会自动在 jni 目录下生成一个 ndkdemo.c 文件,内容如下:

#include <jni.h>

JNIEXPORT jint JNICALL
Java_com_demo_ndk_MainActivity_test(JNIEnv *env, jobject instance) {

    // TODO

}

此时修改这个函数的内容即可,这里简便起见直接返回一个整数 3,编译运行即可看到文本框里显示「3」。

4. 如何用 C++ 写 native 函数

如果要用 C++ 写 native 函数的话,直接把 ndkdemo.c 后缀改成 cpp 是不行的,还需要把所有函数声明(如果没有声明就把定义)都放在 extern "C" { } 中,如下:

#include <jni.h>

extern "C" {

JNIEXPORT jint JNICALL
Java_com_demo_ndk_MainActivity_test(JNIEnv *env, jobject instance) {
    return 3;
}

}

如果需要使用 STL,则要在 ./app/build.gradle 中的 android.ndk { } 里加入 stl = "gnustl_shared"(见 C++ Library Support)。

5. 参考资料

在 Android Studio 中使用 Android NDK