├── images ├── java_compile_with_desugar.png └── screenshot-20151127-support-percent.png ├── notes ├── tricks │ └── 0002-gc-watcher.asc ├── knowledge │ ├── 0003-service-notes.asc │ ├── 0010-support-percent.asc │ ├── 0001-support-annotations.asc │ ├── 0005-custom-lint.asc │ ├── 0013-gradle-support-in-aosp-build.asc │ ├── 0009-android-keystore-system.asc │ ├── 0012-android-java8.adoc │ └── 0011-android-theme-and-style.asc ├── android-changes │ ├── android-8.0 │ │ └── 0014-android-8.0-changes.asc │ └── android-6.0 │ │ ├── 0008-android-6.0-apis.asc │ │ └── 0007-android-6.0-changes.asc ├── arch │ └── 0006-android-arch.asc ├── aosp │ └── 0004-ArrayMap.asc ├── tools │ └── 0006-busybox-android.asc └── sourcecode │ └── 0015-retrofit.asc └── README.asc /images/java_compile_with_desugar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongce/AndroidDevNotes/HEAD/images/java_compile_with_desugar.png -------------------------------------------------------------------------------- /images/screenshot-20151127-support-percent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yongce/AndroidDevNotes/HEAD/images/screenshot-20151127-support-percent.png -------------------------------------------------------------------------------- /notes/tricks/0002-gc-watcher.asc: -------------------------------------------------------------------------------- 1 | = GC Watcher 2 | 3 | NOTE: 反馈与建议,请移步: 4 | https://github.com/yongce/AndroidDevNotes/issues/3 5 | 6 | 文章更新历史: 7 | 8 | * 2015/01/30 文章发布 9 | 10 | ''' 11 | 12 | 在看Android Framework源码时发现的一小段有意思的代码,涉及到几个有意思的知识点。 13 | 14 | 代码整理如下(来自类com.android.internal.os.BinderInternal): 15 | [source,java] 16 | ---- 17 | public class GcWatcher { 18 | static WeakReference mGcWatcher 19 | = new WeakReference(new GcWatcher()); 20 | static long mLastGcTime; 21 | 22 | private GcWatcher() { 23 | // nothing to do 24 | } 25 | 26 | public static void setupWatcher() { 27 | // nothing to do 28 | } 29 | 30 | @Override 31 | protected void finalize() throws Throwable { 32 | super.finalize(); 33 | mLastGcTime = System.currentTimeMillis(); 34 | mGcWatcher = new WeakReference(new GcWatcher()); 35 | } 36 | } 37 | ---- 38 | -------------------------------------------------------------------------------- /README.asc: -------------------------------------------------------------------------------- 1 | = 《安卓开发札记》 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | 12 | 尝试写一些有关Android开发的文章,把一些知识与经验分享出来供大家参考。 13 | 14 | 文章所涉及的主题,主要包括应用架构与工程、开发知识、工具使用、代码质量、性能优化、AOSP源码、开源项目,等等。 15 | 16 | [NOTE] 17 | 反馈与建议,请移步: 18 | https://github.com/yongce/AndroidDevNotes/issues/1 19 | 20 | ## 目录 21 | 22 | ### 架构篇 23 | * link:notes/arch/0006-android-arch.asc[Android应用开发之技术构架] (?) 24 | 25 | ### 知识篇 26 | * link:notes/knowledge/0003-service-notes.asc[0003 Service Notes] (2015/02/06更新) 27 | * link:notes/knowledge/0005-custom-lint.asc[0005 自定义Android Lint规则] (2015/07/20更新) 28 | * link:notes/knowledge/0009-android-keystore-system.asc[0009 Android Keystore System] (2015/11/19) 29 | * link:notes/knowledge/0011-android-theme-and-style.asc[0011 Android Theme & Style] (2015/12/12) 30 | * link:notes/knowledge/0012-android-java8.adoc[0012 Android对Java 8的支持] (2020/5/30更新) 31 | * link:notes/knowledge/0013-gradle-support-in-aosp-build.asc[0013 Gradle Support in AOSP build] (2016/08/29) 32 | 33 | ### 工具篇 34 | * link:notes/tools/0006-busybox-android.asc[0006 编译Android版busybox] (2015/07/23更新) 35 | 36 | ### 代码质量 37 | 38 | ### 性能优化 39 | 40 | ### Android Changes 41 | * link:notes/android-changes/android-6.0/0007-android-6.0-changes.asc[0007 Android 6.0 Changes](2015/11/1更新) 42 | * link:notes/android-changes/android-6.0/0008-android-6.0-apis.asc[0008 Android 6.0 APIs](2015/11/1更新) 43 | * link:notes/android-changes/android-8.0/0014-android-8.0-changes.asc[0014 Android 8.0 Changes](半成品) 44 | 45 | ### AOSP源码阅读 46 | * link:notes/aosp/0004-ArrayMap.asc[0004 ArrayMap] (2015/06/14更新) 47 | 48 | ### Android Support Library 49 | * link:notes/knowledge/0001-support-annotations.asc[0001 Android Support Annotations库介绍] (2015/07/26更新) 50 | * link:notes/knowledge/0010-support-percent.asc[0010 Android Support Percent库介绍] (2015/11/27更新) 51 | 52 | ### 开源项目 53 | * link:notes/sourcecode/0015-retrofit.asc[0015 Retrofit2 源码解析] (?) 54 | 55 | ### 奇技淫巧 56 | * link:notes/tricks/0002-gc-watcher.asc[0002 GC Watcher] (2015/01/30更新) 57 | 58 | ## License 59 | 60 | 本仓库中的所有文章,均采用“Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)” 协议发布。可以到 http://creativecommons.org/licenses/by-nc-nd/4.0/ 查看该协议。 61 | -------------------------------------------------------------------------------- /notes/knowledge/0003-service-notes.asc: -------------------------------------------------------------------------------- 1 | = Service Notes 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | NOTE: 反馈与建议,请移步: 16 | https://github.com/yongce/AndroidDevNotes/issues/4 17 | 18 | 文章更新历史: 19 | 20 | * 2015/02/06 文章发布 21 | 22 | ''' 23 | 24 | 本文简单总结一下Service。 25 | 26 | == Service Binding 27 | 28 | === 关于Serivce#onBind() 29 | 30 | 当client第一次调用bindService()时,系统会回调该Service#onBind()方法,并把返回的IBinder对象缓存起来。此时,若有其它client也调用bindService(),那么系统会直接把缓存的IBinder对象传递给client。**也就是说,在Service#onCreate()和Service#onDestroy()期间,Service#onBind()方法会且仅会被调用一次,而不管有多少client来bind它,也不管Service#onUnbind()是否被回调了。** 31 | 32 | 当所有client都调用了unbindService(),Service#onUnbind()会被回调。如果此时Service#onUnbind()返回true,且Service#onDestroy()没有被调用(说明此Service被startService()过了),则下次client来bind Service时,Service#onRebind()会被回调。 33 | 34 | Service#onBind()和Service#onUnbind(),或者Service#onRebind()和Service#onUnbind(),是成对出现的,一一对应关系。 35 | 36 | === IBinder的三种实现方式 37 | 38 | ==== 扩展Binder类 39 | 40 | 如果Service和client在同一进程中,那么可以选择直接扩展Binder类,并添加相应的公开方法,而在client的ServiceConnection#onServiceConnected()中,直接强转IBinder对象为所定义的Binder类对象。 41 | 42 | Service示例代码: 43 | [source,java] 44 | ---- 45 | private final IBinder mBinder = new LocalBinder(); 46 | 47 | public class LocalBinder extends Binder { 48 | // Add the APIs 49 | } 50 | 51 | @Override 52 | public IBinder onBind(Intent intent) { 53 | return mBinder; 54 | } 55 | ---- 56 | 57 | client示例代码: 58 | [source,java] 59 | ---- 60 | private ServiceConnection mConnection = new ServiceConnection() { 61 | @Override 62 | public void onServiceConnected(ComponentName className, 63 | IBinder service) { 64 | LocalBinder binder = (LocalBinder) service; 65 | } 66 | }; 67 | ---- 68 | 69 | ==== Messenger 70 | 71 | 如果Service和client在不同进程中,但仅需要相互传送一些数据,且这些数据可以通过Message传送,那么可以选择使用Messenger。**由于Message最终是在Handler中处理的,因此所有client的请求无法并行执行。** 72 | 73 | ==== 使用AIDL 74 | 75 | 如果Service和client在不同进程中,可以选用AIDL,这样Service可以暴露一些接口给client调用。由于这些AIDL接口可以同时被多个client调用,因此需要实现为线程安全的。 76 | 77 | 如果client和Service在不同进程中,那么client拿到的IBinder对象是一个android.os.BinderProxy代理对象;如果client和Service在同一进程中,那么client拿到的IBinder对象就是Service#onBind()返回的IBinder对象。(参见后面的示例代码) 78 | 79 | 当Service进程中IBinder实现代码发生的异常时:a) 如果client和Service在不同进程中,那么此crash会被系统处理掉,不会传递到client(client和Service都感知不到此crash,在系统Log能够看到相应信息);b) 如果client和Service在同一进程中,那么此crash会直接传递到client(因为这里是client直接调用了Service中IBinder方法!)。(参见后面的示例代码) 80 | 81 | 默认情况下,调用远程IBinder方法时,client能够捕获到的唯一异常是DeadObjectException(一个特殊的RemoteException),表示这个IBinder连接挂了。如果要IBinder要向client传递异常,则需要通过android.os.Parcel#writeException()和android.os.Parcel#readException()来实现。(参见后面的示例代码) 82 | 83 | == 示例代码 84 | 85 | 提供了一个Demo应用,演示了IBinder的手动实现和上面提到的一些技术点。 86 | 87 | 代码地址:https://github.com/dxopt/BinderDemo 88 | 89 | == 参考资料 90 | 1. http://developer.android.com/guide/components/services.html 91 | 2. http://developer.android.com/guide/components/bound-services.html 92 | 3. http://developer.android.com/guide/components/aidl.html 93 | -------------------------------------------------------------------------------- /notes/android-changes/android-8.0/0014-android-8.0-changes.asc: -------------------------------------------------------------------------------- 1 | = Android 8.0 Changes 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | 官方文档:https://developer.android.com/preview/behavior-changes.html 16 | 17 | 在我看来,需要重点关注如下方面: 18 | 19 | * 对Broadcast的限制 20 | * 对后台Service的限制 21 | 22 | :numbered: 23 | 24 | == 所有应用 25 | 26 | === 后台运行限制 27 | 28 | 从Android M以来,Android每个版本都为提升Android设备的电池续航做了不少努力,同时也为应用带来了越来越多的限制。 29 | 30 | 在Android O中,如果应用进入cached状态(即没有任何正在运行的component),系统将自动释放wakelock。 31 | 32 | 33 | === 对Service的限制 34 | 35 | Android O不再允许background应用运行background service。 36 | 37 | 对于一个正在运行的应用,如果下列条件之一成立,那么该应用为foreground: 38 | 39 | * 它有一个可见的Activity(不论是started状态还是paused状态)。 40 | * 它有一个foreground service。 41 | * 有其它foreground应用连接到它(不论是通过bind service还是访问content provider)。 42 | 43 | 如果上述条件都不满足,那么该应用为background。 44 | 45 | 当一个应用为foreground时,它可以自由地运行foreground service和background service。 46 | 在它转变为background时,它有一个几分钟的时间窗口,在此期间它依然可以运行service。 47 | 在那时间窗口之后,该应用将为idle状态。此时,系统会停掉该应用的background service。 48 | 49 | 当一个应用为background时,如果发生了某些特定事件(如,收到FCM高优先级消息、收到SMS/MMS broadcast、 50 | 执行Notification的PendingIntent等),该应用也会有一个几分钟的时间窗口,在此期间,它可以无限制地运行service。 51 | 52 | TIP: 在OPP3版本中,测试结果表明,系统在等待1~1.5分钟后会stop background service。 53 | 54 | TIP: 不论应用是foreground还是background状态,其它应用都可以正常地去bind该应用的bound service。 55 | 56 | 在Android O中,创建foreground service的方式也发生了变化: 57 | 58 | 1. 由于Android O不允许background应用创建background service。因此 ,需要先调用Context#startForegroundService()来启动一个foreground service。 59 | 2. app需要在5秒内调用Service#startForeground()来展示用户可见的notification。 60 | 如果app没能在5秒内调用Service#startForeground(),系统将停掉该service,并把该应用置为ANR状态。 61 | 62 | 63 | == target SDK为Android 8.0及以上版本的应用 64 | 65 | === 对Broadcast的限制 66 | 67 | 当我们调用 ```Context#sendBroadcast()``` 发送broadcast时,这个broadcast要么是explicit,要么是implicit。 68 | 如何判断呢?如果一个broadcast有明确的接收app,那么其就是explicit(例如,指定了component,或者指定了包名); 69 | 如果一个broadcast没有明确的接收app,那么它就是implicit。 70 | 71 | 例如, 72 | 73 | [source, java] 74 | ---- 75 | // case 1: explicit 76 | Intent intent1 = new Intent(CommonConstants.ACTION_APP1_TEST_SELF); 77 | intent1.setPackage(getPackageName()); 78 | context.sendBroadcast(intent1); 79 | 80 | // case 2: explicit 81 | Intent intent2 = new Intent(); 82 | intent2.setComponent(cn); 83 | context.sendBroadcast(intent2); 84 | 85 | // case 3: implicit 86 | Intent intent3 = new Intent(CommonConstants.ACTION_APP1_TEST_NORMAL); 87 | context.sendBroadcast(intent3); 88 | 89 | // case 4: implicit with permission 90 | Intent inten4 = new Intent(CommonConstants.ACTION_APP1_TEST_PERM_SIGN); 91 | context.sendBroadcast(inten4, CommonConstants.PERM_SIGN); 92 | ---- 93 | 94 | 在Android O中,对Broadcast新增了如下限制: 95 | 96 | * 如果应用的target SDK为Android O或者以上版本,在AndroidManifest.xml中注册的 97 | BroadcastReceiver将无法再接收到implicit broadcast。 98 | 有一个例外情况,如果此implicit broadcast是通过"signature"保护级别的权限发送的,且目标应用也有此权限, 99 | 那么在AndroidManifest.xml中注册的Broadcast依然可以接收到此broadcast。 100 | 101 | 没有变化的地方: 102 | 103 | * 在AndroidManifest.xml中注册的BroadcastReceiver依然可以接收到explicit broadcast。 104 | * 通过Context#registerReceiver()注册的BroadcastReceiver依然可以接收到所有broadcast。 105 | * 如果app的target SDK为Android 8.0之前的版本,在AndroidManifest.xml中注册的BroadcastReceiver依然可以接收所有broadcast。 106 | 107 | TIP: 见 https://github.com/ycdev-demo/AndroidOExplorer[AndroidOExplorer] 中的测试代码。 108 | 109 | TIP: 即使app的target SDK为Android O之前,用户也可以在Settings中app详情页面强制禁用app在后台运行。 110 | -------------------------------------------------------------------------------- /notes/arch/0006-android-arch.asc: -------------------------------------------------------------------------------- 1 | = Android应用开发之技术构架 2 | :toc: 3 | :toc-placement: preamble 4 | :toclevels: 3 5 | 6 | 点位符。。。 7 | 8 | :numbered: 9 | 10 | == 主要问题及技术方案 11 | 12 | 13 | 14 | 这里,先列举一些应用在从小做大的过程中,面临的一些技术挑战。 15 | 16 | === 性能 17 | 18 | 当产品有了一定的用户规模和体量,性能问题就会逐渐被团队所重视。 19 | 20 | [cols="1,2a"] 21 | .性能问题 22 | |=== 23 | |问题 |技术方案 24 | 25 | |运行速度 26 | | 27 | * 性能分析 28 | * 优化UI布局 29 | * 优化算法和数据结构 30 | * 延迟加载(启动速度) 31 | 32 | |内存占用 33 | | 34 | * 防止内存泄露 35 | * 优化数据结构(例如,用SparseArray、ArrayMap等代替HashMap) 36 | * 优化图片的使用 37 | * 多进程架构:常驻进程与非常驻进程 38 | 39 | |流量消耗 40 | | 41 | * TrafficStats#setThreadStatsTag() 42 | * "/proc/net/xt_qtaguid/stats" 43 | 44 | |电量消耗 45 | |性能分析 46 | 47 | |=== 48 | 49 | === 质量 50 | 51 | [cols="1,2a"] 52 | .质量问题 53 | |=== 54 | |问题 |问题描述 55 | 56 | |Crash 57 | | 58 | * UncaughtExceptionHandler 59 | * 统计分析 60 | 61 | |代码质量 62 | | 63 | * Android Lint(包括自定义规则) 64 | * StrictMode 65 | * Android Annotations 66 | * 单元测试(Testing Support Library) 67 | 68 | |=== 69 | 70 | === 产品改进 71 | 72 | === 多进程架构 73 | 74 | 多进程架构,常驻进程与非常驻进程 75 | 76 | [cols="1,2a"] 77 | .多进程架构 78 | |=== 79 | |问题 |解决方案 80 | 81 | |SharedPreferences 82 | | 83 | * Wrapper封装 84 | * 实现多进程版“SharedPreferences” 85 | * 多进程访问转单进程访问(例如,通过Provider) 86 | 87 | |数据库 88 | | 89 | * Provider 90 | * StrictMode 91 | * Android Annotations 92 | * 单元测试(Testing Support Library) 93 | 94 | |=== 95 | 96 | === 其它典型问题 97 | 98 | 99 | [cols="1,2a"] 100 | .其它典型问题 101 | |=== 102 | |问题 |解决方案 103 | 104 | |APK包大小压缩 105 | | 106 | * 删除冗余资源(Android Lint or "shrinkResources") 107 | * 功能删减 108 | * 插件化,按需下载功能模块 109 | 110 | |方法数超出 111 | | 112 | * 代码精减 113 | * Multidex 114 | * 插件化 115 | 116 | |文件句柄用尽 footnote:[关于文件句柄用尽问题,请参考 https://github.com/dxopt/OpenFilesLeakTest] 117 | | 118 | * WebView:独立进程,按需创建 119 | * 小心调用Zip相关API 120 | 121 | |数据库操作异常 122 | | 123 | * 单进程:引用计数方式打开/关闭数据库 124 | * 多进程:ContentProvider 125 | 126 | |=== 127 | 128 | 129 | == 技术架构与选型 130 | 131 | 在启动一个产品的开发时,我们首先需要建立研发流程,然后再做技术架构与选型。 132 | 133 | === 研发流程管理 134 | 135 | [cols="1,2a"] 136 | .项目启动之研发流程管理 137 | |=== 138 | |关注点 |要点 139 | 140 | |源码管理 141 | | 142 | * 版本控制(例如,git) 143 | * Code Review(例如,gerrit、gitlab/PR) 144 | * 源码版本Tag管理 145 | 146 | |开发环境与构建系统 147 | | 148 | * 操作系统(Mac > Linux > Windows) 149 | * IDE & 编码规范(例如,Android Studio) 150 | * 构建系统 & CI(例如,Gradle & Jenkins) 151 | 152 | |产品运营支持 153 | | 154 | * 数据统计(包括Crash统计) 155 | * 应用版本号管理 156 | * 渠道号管理 157 | * 应用签名与打包系统 158 | 159 | |其它 160 | | 161 | * Bug跟踪系统 162 | * 测试用例管理系统 163 | * 需求管理系统 164 | 165 | |=== 166 | 167 | === 技术架构与选型 168 | 169 | 170 | [cols="1,2a"] 171 | .项目启动之技术架构与选型 172 | |=== 173 | |关注点 |要点 174 | 175 | |Android版本 176 | | 177 | * 确定minSdkVersion和targetSdkVersion 178 | 179 | |反逆向分析 180 | | 181 | * 代码混淆 182 | * 资源混淆 183 | * 代码加壳 184 | 185 | |基础代码库 186 | | 187 | * 工具类 188 | * 基础能力封装 189 | * Wrapper封装,便于管理和演进 190 | 191 | |公共UI库 192 | | 193 | * 制定和实施统一的UI规范 194 | * UI改版 195 | * 附属产品的UI统一和重用 196 | 197 | |=== 198 | 199 | == 技术演进 200 | 201 | === 方法论(空) 202 | 203 | 针对前面提到的主要问题和相应的技术方案。 204 | 如何做应用构架,以便能够平滑地进行构架演进。 205 | 206 | 技术跟踪 & 技术趋势把握 & 。。。 207 | 208 | === 技术选型(空) 209 | 210 | === 技术跟踪 211 | 212 | [cols="1,2,2a"] 213 | .技术跟踪 214 | |=== 215 | |关注点 |目标 |建议 216 | 217 | |Android版本及SDK 218 | |新特性和技术的价值挖掘;及时发现并解决产品的兼容性问题。 219 | | 220 | * 新的系统特性和API 221 | * 系统行为和API的改变 222 | 223 | |Android Support Library 224 | |了解重要API及使用场景 225 | | 226 | * v4 Support Library 227 | * v7 Support Library 228 | * Annotations Support Library 229 | * Design Support Library 230 | * Multidex Support Library 231 | * ... 232 | 233 | |Android Studio 234 | |关注新特性和内置工具,提升开发效率和质量。 235 | |Release Notes: http://tools.android.com/recent 236 | 237 | |Android Plugin for Gradle 238 | |关注新特性,适时引入项目,优化构建流程。 239 | |Release Notes: http://tools.android.com/tech-docs/new-build-system 240 | 241 | |性能优化工具 242 | |优化产品的各性能指标(运行速度、内存占用、流量消耗、电量消耗)。 243 | | 244 | * Hierarchy Viewer & Overdraw Debugger 245 | * Traceview & Systrace 246 | * Memory Profilers / MAT 247 | * Batterystats & Battery Historian 248 | * ... 249 | * 开发性能监控和分析工具 250 | 251 | |代码质量 252 | |引入适当的质量工具,提高代码质量。 253 | | 254 | * Android Lint(包括自定义规则) 255 | * StrictMode 256 | * Android Annotations 257 | * 单元测试(Testing Support Library) 258 | 259 | |=== 260 | 261 | 262 | === 技术演进(空) 263 | -------------------------------------------------------------------------------- /notes/knowledge/0010-support-percent.asc: -------------------------------------------------------------------------------- 1 | = Android Support Percent库介绍 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | 16 | Percent库的设计目标是支持基于百分比的View大小设置。 17 | 18 | 目前,Percent库仅支持在定制的ViewGroup中设置子View的大小为ViewGroup大小的一个百分比。 19 | 此外,子View的宽高也可以设定为一个比例关系。 20 | 21 | ''' 22 | 23 | 文章更新历史: 24 | 25 | * 2015/11/27 文章发布 26 | 27 | ''' 28 | 29 | :numbered: 30 | 31 | == 引入Percent库 32 | 33 | 直接修改build.gradle配置文件,添加该库的引用。例如: 34 | ---- 35 | dependencies { 36 | compile 'com.android.support:percent:23.1.1' 37 | } 38 | ---- 39 | 40 | 也可以通过Android Studio的Project Structure 41 | (File -> Project Structure,Dependencies)添加引用。 42 | 43 | == Percent库介绍 44 | 45 | === 示例代码及效果 46 | 47 | 先看一段示例代码: 48 | 49 | ``` 50 | 51 | 56 | 57 | 64 | 65 | 70 | 78 | 88 | 98 | 108 | 118 | 119 | 120 | 121 | 122 | ``` 123 | 124 | 运行截图如下: 125 | 126 | image::../../images/screenshot-20151127-support-percent.png[width="350"] 127 | 128 | === PercentFrameLayout 和 PercentRelativeLayout 129 | 130 | 分别基于FrameLayout和RelativeLayout的Percent版本,支持子View设置width、height 131 | 和margin的百分比大小,也支持子View的长宽比设置。 132 | 133 | 如果设置了width和height的百分比大小,还可以设置“android:layout_width”和 134 | “android:layout_height”的属性值为“wrap_content”。 135 | 此时,如果按百分比大小计算出来的子View大小不能够容纳子View的内容, 136 | 那么此时子View会放弃百分比大小,而采用“wrap_content”大小。 137 | 138 | === PercentLayoutHelper 139 | 140 | 如果需要在自己的ViewGroup中也支持基于百分比的大小设置, 141 | 那么可以借助PercentLayoutHelper来实现。 142 | 143 | 具体用法,请参考PercentLayoutHelper文档, 144 | 或者参考PercentFrameLayout和PercentRelativeLayout的实现代码。 145 | 146 | == 参考资料 147 | 148 | 官方文档: 149 | 150 | * http://developer.android.com/tools/support-library/features.html#percent 151 | * http://developer.android.com/reference/android/support/percent/package-summary.html 152 | 153 | Support库源码阅读和编译: 154 | 155 | * https://github.com/ycdev-aosp/frameworks-support (source-build分支) 156 | 157 | 示例代码: 158 | 159 | * https://github.com/ycdev-demo/SupportPercentDemo 160 | -------------------------------------------------------------------------------- /notes/aosp/0004-ArrayMap.asc: -------------------------------------------------------------------------------- 1 | = ArrayMap 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | NOTE: 反馈与建议,请移步: 16 | https://github.com/yongce/AndroidDevNotes/issues/5 17 | 18 | 文章更新历史: 19 | 20 | * 2015/06/14 文章发布 21 | 22 | ''' 23 | 24 | 以前就看过ArrayMap的实现,但今天需要用到时,却想不起来其内部实现,只好重新看一下。好记性不如烂笔头,记下来以便将来查看。 25 | 26 | ArrayMap源码信息: 27 | ---- 28 | AOSP Project: frameworks/base 29 | Project Tag: android-5.1.1_r4 30 | Files: 31 | + core/java/android/util/ArrayMap.java 32 | ---- 33 | 34 | :numbered: 35 | 36 | == 设计目的 37 | 38 | ArrayMap是一个用于存储“key/value”的数据结构,用于在某些场景下代替HashMap。相比HashMap,ArrayMap更加节省内存(使用数组的连续空间存储),但在速度上有所牺牲(使用二分查找,需要多次分配内存)。在存储的元素个数不多时,速度上的差距不太明显。 39 | 40 | 为了节省内存,当存储的元素个数变少时,ArrayMap可能会重新分配更小的数组来存储元素,从而进行内存收缩。 41 | 42 | == 实现原理 43 | 44 | ArrayMap内部使用两个数组来存储“key/value”数据和实现Mapping: 45 | 46 | * 一个int数组用于存储key的hash code,并且保持有序 47 | * 一个Object数组用于存储key/value对,与int数组的下标保持对应关系 48 | 49 | 当需要查找一个元素时,先计算key的hash code,然后在int数组中二分查找该key的hash code,从而找到该key在int数组中的下标index;此时,在Object数组中,key的下标为(index*2),value的下标为(index*2+1)。 50 | 51 | == 实现细节 52 | 53 | === 数组大小控制策略 54 | 55 | ==== 创建 56 | 57 | 默认创建的ArrayMap对象(不指定capacity),内部创建的数组大小为0。 58 | 59 | [source,java] 60 | ---- 61 | public ArrayMap() { 62 | mHashes = EmptyArray.INT; 63 | mArray = EmptyArray.OBJECT; 64 | mSize = 0; 65 | } 66 | ---- 67 | 68 | ==== 增长 69 | 70 | 在put时,如果数组中已经没有可用空间时,则需要重新分配更大的数组。其增长策略为:如果当前ArrayMap大小小于BASE_SIZE(当前实现设定为4),则分配一个大小为BASE_SIZE的数组;否则,如果ArrayMap大小小于BASE_SIZE*2,则分配一个大小为BASE_SIZE*2的数组;否则,分配一个大小为当前ArrayMap大小1.5倍的数组。 71 | 72 | [source,java] 73 | ---- 74 | if (mSize >= mHashes.length) { 75 | final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1)) 76 | : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); 77 | 78 | if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n); 79 | 80 | final int[] ohashes = mHashes; 81 | final Object[] oarray = mArray; 82 | allocArrays(n); 83 | 84 | if (mHashes.length > 0) { 85 | if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0"); 86 | System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); 87 | System.arraycopy(oarray, 0, mArray, 0, oarray.length); 88 | } 89 | 90 | freeArrays(ohashes, oarray, mSize); 91 | } 92 | ---- 93 | 94 | ==== 收缩 95 | 96 | 在remove时,如果数组大小大于BASE_SIZE*2,且ArrayMap大小小于1/3数组大小,那么会对数组进行收缩。其收缩策略为:如果当前ArrayMap大小大于BASE_SIZE*2,那么新的数组大小为ArrayMap大小的1.5倍;否则,新的数组大小为BASE_SIZE*2。 97 | 98 | [source,java] 99 | ---- 100 | if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { 101 | // Shrunk enough to reduce size of arrays. We don't allow it to 102 | // shrink smaller than (BASE_SIZE*2) to avoid flapping between 103 | // that and BASE_SIZE. 104 | final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2); 105 | 106 | if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n); 107 | 108 | final int[] ohashes = mHashes; 109 | final Object[] oarray = mArray; 110 | allocArrays(n); 111 | 112 | mSize--; 113 | if (index > 0) { 114 | if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0"); 115 | System.arraycopy(ohashes, 0, mHashes, 0, index); 116 | System.arraycopy(oarray, 0, mArray, 0, index << 1); 117 | } 118 | if (index < mSize) { 119 | if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize 120 | + " to " + index); 121 | System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index); 122 | System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1, 123 | (mSize - index) << 1); 124 | } 125 | } 126 | ---- 127 | 128 | ==== 总结 129 | 130 | 如果不指定capacity来创建ArrayMap对象,那么ArrayMap对象可以划分为三类: 131 | 132 | . 拥有可容纳BASE_SIZE个元素的数组 133 | . 拥有可容纳BASE_SIZE*2个元素的数组 134 | . 拥有可容纳比BASE_SIZE*2更多的元素的数组 135 | 136 | 137 | === 数组缓存 138 | 139 | 在使用过程中,ArrayMap可能需要多次分配内部的数组。为了避免频繁GC,ArrayMap类对释放的数组做了缓存,以便重复利用。 140 | 141 | [source,java] 142 | ---- 143 | /** 144 | * Caches of small array objects to avoid spamming garbage. The cache 145 | * Object[] variable is a pointer to a linked list of array objects. 146 | * The first entry in the array is a pointer to the next array in the 147 | * list; the second entry is a pointer to the int[] hash code array for it. 148 | */ 149 | static Object[] mBaseCache; 150 | static int mBaseCacheSize; 151 | static Object[] mTwiceBaseCache; 152 | static int mTwiceBaseCacheSize; 153 | ---- 154 | 155 | 缓存是用链表实现的,两个缓存链表的大小上限都为CACHE_SIZE(当前实现中设定为10)。一个缓存链表用于缓存大小为BASE_SIZE的数组,另一个缓存链表用于缓存大小为BASE_SIZE\*2的数组(跟前面的数组分配策略一致)。 156 | 157 | 分配数组时使用缓存的代码: 158 | 159 | [source,java] 160 | ---- 161 | if (mBaseCache != null) { 162 | final Object[] array = mBaseCache; 163 | mArray = array; 164 | mBaseCache = (Object[])array[0]; 165 | mHashes = (int[])array[1]; 166 | array[0] = array[1] = null; 167 | mBaseCacheSize--; 168 | if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes 169 | + " now have " + mBaseCacheSize + " entries"); 170 | return; 171 | } 172 | ---- 173 | 174 | 释放数组时加入缓存的代码: 175 | 176 | [source,java] 177 | ---- 178 | if (mBaseCacheSize < CACHE_SIZE) { 179 | array[0] = mBaseCache; 180 | array[1] = hashes; 181 | for (int i=(size<<1)-1; i>=2; i--) { 182 | array[i] = null; 183 | } 184 | mBaseCache = array; 185 | mBaseCacheSize++; 186 | if (DEBUG) Log.d(TAG, "Storing 1x cache " + array 187 | + " now have " + mBaseCacheSize + " entries"); 188 | } 189 | ---- 190 | 191 | 192 | == 点评 193 | 194 | ArrayMap是一个设计很精巧的数据结构,并且其实现代码的很多细节值得揣摩和学习(例如indexOf()方法的实现、缓存链表的实现)。 195 | -------------------------------------------------------------------------------- /notes/knowledge/0001-support-annotations.asc: -------------------------------------------------------------------------------- 1 | = Android Support Annotations库介绍 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | NOTE: 反馈与建议,请移步: 16 | https://github.com/yongce/AndroidDevNotes/issues/2 17 | 18 | 文章更新历史: 19 | 20 | * 2015/01/29 文章发布 21 | * 2015/07/26 更新文章,解读新增的annotations 22 | 23 | ''' 24 | 25 | 从Android Support Library v19.1开始,Support库新增了一个annotations子库。 26 | 这个库里面定义了一些annotation,可以被用于在代码中强制添加一些调用约束, 27 | 进而便于IDE对代码进行静态检查,以发现潜在的问题。 28 | 29 | 关于该库的官方介绍和用法,请看 30 | http://tools.android.com/tech-docs/support-annotations[这里] 和 31 | https://developer.android.com/tools/debugging/annotations.html[这里]。 32 | 33 | 本文简单总结一下其用法。 34 | 35 | :numbered: 36 | 37 | == 引入Annotations库 38 | 39 | 直接修改build.gradle配置文件,添加该库的引用。例如: 40 | ---- 41 | dependencies { 42 | compile 'com.android.support:support-annotations:22.2.0' 43 | } 44 | ---- 45 | 46 | 也可以通过Android Studio的Project Structure 47 | (File -> Project Structure,Dependencies)添加引用。 48 | 49 | NOTE: Annotations库并没有被上传到jcenter(其它Support库也是), 50 | 而是存放于Android SDK中的一个本地maven库中。 51 | 因此,如果需要在Java模块中使用Annotations库,则需要添加Support库的maven库: 52 | `maven { url '/extras/android/m2repository' }`。 53 | 54 | == annotations介绍 55 | 56 | === null相关 57 | 58 | 用于修饰Method的参数和返回值,以便IDE对相关的约束进行检查。列举如下: 59 | 60 | * @Nullable 表示允许为null 61 | * @NonNull 表示不允许为null 62 | 63 | 从官网抄一段例子: 64 | [source,java] 65 | ---- 66 | import android.support.annotation.NonNull; 67 | import android.support.annotation.Nullable; 68 | ... 69 | 70 | @Nullable 71 | @Override 72 | public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) { 73 | ... 74 | ---- 75 | 76 | 需要注意的是,对于一个变量或者返回值,除了可指定@NonNull和@Nullable之外, 77 | 还有第三种状态,即“未指定”。 78 | 79 | WARNING: 当一个方法可能会返回null时,我们有两种选择: 80 | 一是指定@Nullable,二是什么都不指定。 81 | 如果打算指定为@Nullable,这意味着所有调用者都需要检查返回值是否为null。 82 | 也许,这并不是期望的行为,可能更好的选择是什么都不指定。 83 | 84 | === 资源类型相关 85 | 86 | 用于修饰对资源ID的相关引用,以便IDE对相关的资源类型进行检查。列举如下: 87 | 88 | * @AnimRes 89 | * @AnimatorRes 90 | * @AnyRes 91 | * @ArrayRes 92 | * @AttrRes 93 | * @BoolRes 94 | * @ColorRes 95 | * @DimenRes 96 | * @DrawableRes 97 | * @FractionRes 98 | * @IdRes 99 | * @IntegerRes 100 | * @InterpolatorRes 101 | * @LayoutRes 102 | * @MenuRes 103 | * @PluralsRes 104 | * @RawRes 105 | * @StringRes 106 | * @StyleRes 107 | * @StyleableRes 108 | * @XmlRes 109 | 110 | 从官网抄一段例子: 111 | [source,java] 112 | ---- 113 | import android.support.annotation.StringRes; 114 | ... 115 | public abstract void setTitle(@StringRes int resId); 116 | ---- 117 | 118 | === 常量定义相关 119 | 120 | ==== @IntDef 121 | 122 | 我们经常使用int来定义一些常量来代替使用enum,可以使用这个annotation来添加相关的约束。 123 | 124 | 还是搬官网的例子: 125 | [source,java] 126 | ---- 127 | import android.support.annotation.IntDef; 128 | ... 129 | public abstract class ActionBar { 130 | ... 131 | @Retention(RetentionPolicy.SOURCE) 132 | @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) 133 | public @interface NavigationMode {} 134 | 135 | public static final int NAVIGATION_MODE_STANDARD = 0; 136 | public static final int NAVIGATION_MODE_LIST = 1; 137 | public static final int NAVIGATION_MODE_TABS = 2; 138 | 139 | @NavigationMode 140 | public abstract int getNavigationMode(); 141 | 142 | public abstract void setNavigationMode(@NavigationMode int mode); 143 | ---- 144 | 145 | 再搬一个例子,关于flags类型(可进行异或运算)的常量定义: 146 | [source,java] 147 | ---- 148 | @IntDef(flag=true, value={ 149 | DISPLAY_USE_LOGO, 150 | DISPLAY_SHOW_HOME, 151 | DISPLAY_HOME_AS_UP, 152 | DISPLAY_SHOW_TITLE, 153 | DISPLAY_SHOW_CUSTOM 154 | }) 155 | @Retention(RetentionPolicy.SOURCE) 156 | public @interface DisplayOptions {} 157 | ---- 158 | 159 | ==== @StringDef 160 | 161 | 该annotation用于修饰字符串常量,用法跟前面的@IntDef类似。 162 | 163 | === 线程相关 164 | 165 | 从Support库v22.2开始,加入了几个线程相关的annotations: 166 | 167 | * @MainThread 指定在主线程中执行 168 | * @UiThread 指定在UI线程中执行 169 | * @WorkerThread 指定在非UI线程中执行 170 | * @BinderThread 指定在Binder线程中执行 171 | 172 | 如果某个方法仅在某一类线程中调用,那么可以为其添加上面的annotation; 173 | 如果某个类的所有方法都仅在某一类线程中调用,那么可以为类添加线程annotation。 174 | 175 | ==== @MainThread vs @UiThread 176 | 177 | 一个进程中,仅有一个主线程。 178 | @MainThread就是指这个主线程,同时这个主线程也是@UiThread。 179 | 180 | 理论上,在一个进程中,可以有多个@UiThread,虽然很少见。 181 | 182 | 在实际使用中,可以遵循如下原则: 183 | 184 | * 生命周期相关的使用@MainThread(例如,AsyncTask#onProgressUpdate方法) 185 | * View体系使用@UiThread 186 | 187 | Android Studio、Android Lint等工具会把两者认为是可互换的, 188 | 因此不会区分两者。 189 | 190 | === 接受ARGB颜色值 191 | 192 | 如果一个方法的参数接受颜色资源ID,则可以使用前面的@ColorRes; 193 | 如果接受颜色的ARGB值,则可以使用@ColorInt。 194 | 195 | === 限定参数值范围 196 | 197 | ==== @FloatRange 198 | 199 | 对于float或double类型的参数,可以使用@FloatRange来指定取值范围: 200 | [source,java] 201 | ---- 202 | public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) { 203 | ---- 204 | 205 | ==== @IntRange 206 | 207 | 对于int或long类型的参数,可以使用@IntRange来指定取值范围。 208 | [source,java] 209 | ---- 210 | public void setAlpha(@IntRange(from=0,to=255) int alpha) { 211 | ---- 212 | 213 | ==== @Size 214 | 215 | 如果对数组、集合或者String的元素个数有所约束,则可以使用@Size: 216 | 217 | * @Size(min=N) 指定元素最小个数为N 218 | * @Size(max=N) 指定元素最大个数为N 219 | * @Size(N) 精确指定元素个数为N 220 | * @Size(multiple=N) 指定元素个数为N的倍数 221 | 222 | === 指定权限 223 | 224 | 可以通过@RequiresPermission来指定调用者需要拥有的权限, 225 | 一般只有系统代码中才会用得到。 226 | 227 | === 强制调用基类方法 228 | 229 | 如果允许一个方法被重写,但要求在子类的重写方法中调用基类的方法, 230 | 则可以使用@CallSuper: 231 | [source,java] 232 | ---- 233 | @CallSuper 234 | protected void onCreate(@Nullable Bundle savedInstanceState) { 235 | ---- 236 | 237 | === 强制使用返回值 238 | 239 | @CheckResult 适用于必须使用返回值的场景。例如: 240 | [source,java] 241 | ---- 242 | @CheckResult(suggest="#enforcePermission(String,int,int,String)") 243 | public abstract int checkPermission(@NonNull String permission, int pid, int uid); 244 | ---- 245 | 246 | === 测试可见 247 | 248 | 当仅为了测试,把类、方法或者字段的可见性放大时,可用@VisibleForTesting来修饰。 249 | 250 | === 混淆保留 251 | 252 | 同ProGuard的“-keep”,用于在代码混淆时阻止某些代码被混淆。 253 | 254 | WARNING: 此功能还在开发中,目前不可用。 255 | 256 | == 结语 257 | 258 | 在项目中支持这些annotation是比较繁琐的,就像为项目添加单元测试的支持一样。 259 | 但同样的,如果能够在项目中支持这些annotation,其带来的质量提升是很明显的。 260 | 特别是对于library project项目,建议在代码规范予以支持。 261 | -------------------------------------------------------------------------------- /notes/android-changes/android-6.0/0008-android-6.0-apis.asc: -------------------------------------------------------------------------------- 1 | = Android 6.0 APIs 2 | :toc: 3 | :toc-placement: preamble 4 | :toclevels: 3 5 | 6 | 官方文档: 7 | https://developer.android.com/about/versions/marshmallow/android-6.0.html 8 | 9 | 本文进行简单的翻译和总结。 10 | 11 | 在我看来,需要重点关注如下特性: 12 | 13 | * 指纹认证、确认凭据、App Linking、 14 | 直接分享等新特性值得重点关注,这些特性在产品中的运用可以为用户带来更好的体验; 15 | * 可接纳的存储设备这个特性,则可能会为现有代码带来麻烦,需要进行专项检查; 16 | * 对那些安全性敏感的应用(例如,支付类)可能需要关注助理API这个特性带来的影响; 17 | * 如果不需要,应该考虑显示关闭应用的自动数据备份特性。 18 | 19 | :numbered: 20 | 21 | == 指纹认证(Fingerprint Authentication) 22 | 23 | 新增了支持指纹认证的相关API。 24 | 25 | 在应用中要使用指纹认证,需要添加 USE_FINGERPRINT 权限: 26 | ``` 27 | 29 | ``` 30 | 然后,调用FingerprintManager#authenticate()向用户请求指纹认证。 31 | 32 | 请参考示例代码: 33 | 34 | * 对称密钥示例: https://github.com/googlesamples/android-FingerprintDialog 35 | * 非对称密钥示例: https://github.com/googlesamples/android-AsymmetricFingerprintDialog 36 | 37 | == 确认凭据(Confirm Credential) 38 | 39 | 现在,可以使用设备的解锁方式(PIN、手势图形和密码)来认证用户, 40 | 进而授权应用访问系统Keystore中存储的凭据。 41 | 42 | 示例代码: 43 | https://github.com/googlesamples/android-ConfirmCredential 44 | 45 | == App Linking 46 | 47 | App Linking技术允许你把应用和你所拥有一个web域名进行关联。 48 | 基于这种关联,系统在处理特定的web链接时,就可以直接打开你的应用, 49 | 而无需提示用户来选择使用哪个应用。 50 | 51 | 请参考相关Training: 52 | https://developer.android.com/training/app-links/index.html 53 | 54 | == 应用自动备份(Auto Backup for Apps) 55 | 56 | 现在,系统会自动为应用进行全数据备份和恢复。 57 | 58 | 要使用这个特性,只需要把Target API设置为API Level 23(Android 6.0), 59 | 无需做任何代码改动。如果用户删除了他们的Google账户, 60 | 那么在此账户中保存的备份数据也会被一同删除。 61 | 62 | 请参考相关Training: 63 | https://developer.android.com/training/backup/autosyncapi.html 64 | 65 | == 直接分享(Direct Share) 66 | 67 | 为了让用户的分享变得更加便捷和愉悦,新增了API支持“直接分享”。 68 | 69 | 这个功能好在哪?举个简单例子来理解。 70 | 假设我正在使用A应用,A应用也支持分享内容到外部应用。 71 | 如果我要分享一段内容给微信好友,在Android 6.0之前, 72 | 我需要先选择要分享到的应用“微信”,再在打开的微信中选择好友,然后分享。 73 | 而在Android 6.0中,如果微信添加了“直接分享”的功能, 74 | 那么我在A应用中就可以直接选择微信好友进行分享了。 75 | 76 | 示例代码: 77 | https://github.com/googlesamples/android-DirectShare 78 | 79 | == 语音交互(Voice Interactions) 80 | 81 | 新增加了语音交互API,与语音指令(Voice Actions)一起,可用来构建对话式语音体验。 82 | 83 | 可调用 Activity#isVoiceInteraction() 来确定是否是被语音指令启动的。 84 | 如果是,在应用中可以使用 VoiceInteractor 类来请求用户的语音确认、 85 | 从选项列表中进行选择,等等。 86 | 87 | 大多数语音交互是由用户的语音指令发起的,但并不全是。 88 | 例如,由语音交互启动的应用也可以通过Intent启动一个语音交互。 89 | 可以通过 Activity#isVoiceInteractionRoot() 来确定, 90 | 当前Activity是由用户语音指令启动的还是由另一个语音交互应用启动的。 91 | 92 | 参考: 93 | https://developers.google.com/voice-actions/interaction/ 94 | 95 | == 助理API(Assist API) 96 | 97 | Android 6.0引入了一种全新的应用使用方式,即通过助理。 98 | 为了使用这个特性,用户必须允许助理使用当前上下文。 99 | 一旦允许,用户可以在任意应用中通过长按“Home”键招唤助理。 100 | 101 | 你的应用可以通过设置 WindowManager.LayoutParams#FLAG_SECURE 102 | 参数来拒绝向助理分享当前上下文。除了由系统向助理传递的那些标准信息, 103 | 你的应用还可以通过 AssistContent 分享额外的信息给助理。 104 | 105 | == 可接纳的存储设备(Adoptable Storage Devices) 106 | 107 | 现在,用户可以接纳外部存储设备(例如,SD卡)。若接纳一个外部存储设备, 108 | 将会对此设备进行加密和格式化,使其用起来跟内部存储一样。 109 | 这个特性允许用户在存储设备之间移动应用及其数据。 110 | 在移动应用时,系统依然会尊重manifest中的 android:installLocation 属性。 111 | 112 | 当应用在内部和外部存储之间移动时,由于现在数据也进行了移动, 113 | 因此跟APK、应用数据相关的文件/目录路径都会发生变化。 114 | 因此,再也不能在代码中硬编码或者持久化相应的文件路径, 115 | 而是在需要时即时去获取当前的真实路径。 116 | 117 | 受影响的Context方法: 118 | ``` 119 | getFilesDir() 120 | getCacheDir() 121 | getCodeCacheDir() 122 | getDatabasePath() 123 | getDir() 124 | getNoBackupFilesDir() 125 | getFileStreamPath() 126 | getPackageCodePath() 127 | getPackageResourcePath() 128 | ``` 129 | 130 | 受影响的ApplicationInfo字段: 131 | ``` 132 | dataDir 133 | sourceDir 134 | nativeLibraryDir 135 | publicSourceDir 136 | splitSourceDirs 137 | splitPublicSourceDirs 138 | ``` 139 | 140 | 为了便于调试这个特性,可以通过如下命令,来启用对一个USB设备的采纳 141 | (此USB设备需要通过USB OTG线跟Android设备连接): 142 | 143 | ``` 144 | $ adb shell sm set-force-adoptable true 145 | ``` 146 | 147 | == 通知(Notifications) 148 | 149 | Notification相关的新增API: 150 | 151 | * NotificationManager#getActiveNotifications(): 152 | 用来找出当前应用活跃的通知。 153 | * CATEGORY_REMINDER: 用来把用户安排的提醒与其它事件和Alarm区别开来。 154 | * INTERRUPTION_FILTER_ALARMS 155 | * 添加对新增的Icon类的支持。 156 | 157 | 示例代码: 158 | https://github.com/googlesamples/android-ActiveNotifications 159 | 160 | == 蓝牙笔支持(Bluetooth Stylus Support) 161 | 162 | == 改善的蓝牙低功耗扫描(Improved Bluetooth Low Energy Scanning) 163 | 164 | 如果你的应用要执行蓝牙低功耗扫描,可以使用 ScanSettings.Builder#setCallbackType() 165 | 来指定不同的扫描任务类型,以更加节能。 166 | 167 | == Hotspot 2.0 Release 1 Support 168 | 169 | == 4K显示模式(4K Display Mode) 170 | 171 | 现在,系统允许应用在兼容的设备上申请把显示分辨率提升到4K渲染。 172 | 可以使用 Display.Mode 来查询显示设备的物理分辨率。 173 | 如果UI在一个较低的逻辑分辨率被绘制出来,那么 Display.Mode#getPhysicalWidth() 174 | 返回的物理分辨率和 Display#getSize() 返回的逻辑分辨率有可能是不一样的。 175 | 176 | 应用在运行时,可以通过设置 window 的属性 177 | WindowManager.LayoutParams#preferredDisplayModeId, 178 | 可以请求系统改变物理分辨率。当在4K显示模式时,UI继续以初始分辨率渲染, 179 | 然后被拉伸到4K,但SurfaceView对象可能以native分辨率来显示内容。 180 | 181 | == 主题化的ColorStateList(Themeable ColorStateLists) 182 | 183 | ColorStateList开始支持主题。 184 | 185 | Resources#getColorStateList()和Resources#getColor()已经被废弃, 186 | 请使用新的API Context#getColorStateList()和Context#getColor()来代替。 187 | 这些方法已经在Support v4的ContextCompat提供了兼容方法。 188 | 189 | == 音频特性(Audio Features) 190 | 191 | 音频处理相关的改进: 192 | 193 | * 支持MIDI协议,新增系列API android.media.midi。 194 | * 新增 AudioRecord.Builder 和 AudioTrack.Builder。 195 | * 用于关联音频和输入设备的API hook。 196 | 例如,可用于从游戏控制器或者Android TV的远程摇控发起的语音搜索。 197 | * 通过 AudioManager#getDevices() 可获取连接到当前系统的所有音频设备的列表。 198 | 也可以使用AudioDeviceCallback来监听音频设备的连接或者断开。 199 | 200 | == 视频特性(Video Features) 201 | 202 | 视频处理相关的改进: 203 | 204 | * 新增 MediaSync,用于同步渲染音视频 205 | * 新增 EVENT_SESSION_RECLAIMED 事件 206 | * 新增 ERROR_RECLAIMED 错误码 207 | * 新增 getMaxSupportedInstances() 208 | * 新增 setPlaybackParams() 209 | 210 | == 相机特性(Camera Features) 211 | 212 | 主要新增了API用于访问相机的闪光灯和图片处理。 213 | 214 | === 闪光灯API(Flashlight API) 215 | 216 | 如果设备的相机配置有闪光灯,现在可以调用 CameraManager#setTorchMode() 217 | 来直接打开或者关闭闪光灯(手电筒模式),而无须打开相机。 218 | 当相机变得不可用时,或者当其它保持手电筒模式的相机资源不可用时, 219 | 手电筒模式也会被关闭并变得不可用。 220 | 其它应用也可以调用 CameraManager#setTorchMode() 来关闭手电筒模式。 221 | 当最后一个打开手电筒模式的应用被关闭时,手电筒模式也会被关闭。 222 | 223 | 可以调用 CameraManager#registerTorchCallback() 224 | 来监听手电筒模式状态的变化。在回调被注册时,回调会被立即调用。 225 | 226 | === 再加工API(Reprocessing API) 227 | 228 | Camera2 API被扩充支持YUV颜色空间和私有的不透明格式图片的再加工。 229 | 可以调用 CameraManager#getCameraCharacteristics() 并传入 REPROCESS_MAX_CAPTURE_STALL 230 | 来确定当前设备是否支持再加工特性。 231 | 232 | == Android企业特性(Android for Work) 233 | 234 | 新增特性和API: 235 | 236 | * 增强对Corporate-Owned, Single-Use(COSU)设备的控制: 237 | ** 禁用或者重新启用屏保。 238 | ** 禁用或者重新启用状态栏(包括快捷设置、通知等)。 239 | ** 禁止或者重新启用安全启动。 240 | ** 充电时阻止屏幕自动关闭。 241 | 242 | * 静默安装/卸载应用(跟Google Play无关,无须Google账户),实现企业的应用部署需求。 243 | * 静默的企业证书访问。 244 | * 自动接受系统升级。 245 | 246 | * 受托证书安装。Profile或者Device Owner可以授权第三方应用调用如下DevicePolicyManager的证书管理API: 247 | ** getInstalledCaCerts() 248 | ** hasCaCertInstalled() 249 | ** installCaCert() 250 | ** uninstallCaCert() 251 | ** uninstallAllUserCaCerts() 252 | ** installKeyPair() 253 | 254 | * 数据使用跟踪。 255 | * 运行时权限管理。Profile或者Device Owner可以为所有应用的运行时权限请求, 256 | 设置一个权限策略(提示用户、自动授予或者静默拒绝)。 257 | * 设置中的VPN(Settings > More > VPN)。 258 | * Work状态通知。当managed profile中的应用在前台运行时, 259 | 状态栏会出现一个公文包图标,表示当前正在work profile中。 260 | -------------------------------------------------------------------------------- /notes/knowledge/0005-custom-lint.asc: -------------------------------------------------------------------------------- 1 | = 自定义Android Lint规则 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | NOTE: 反馈与建议,请移步: 16 | https://github.com/yongce/AndroidDevNotes/issues/6 17 | 18 | 文章更新历史: 19 | 20 | * 2015/07/20 文章发布 21 | 22 | ''' 23 | 24 | 本文介绍如何在Android项目中自定义lint规则,用来检测代码问题,或者保证编码规范的执行。 25 | 26 | :numbered: 27 | 28 | == 关于Android Lint 29 | 30 | 静态代码分析工具常被用来检测代码中的质量问题或者编码规范问题。 31 | lint footnote:[关于lint,可参考wikipedia https://en.wikipedia.org/wiki/Lint_(software)] 作为最早的静态代码分析 footnote:[关于静态代码分析工具,可参考wikipedia https://en.wikipedia.org/wiki/List_of_tools_for_static_code_analysis] 工具,已被用来作为静态代码分析工具的代名词 。因此,Android SDK也把其静态代码分析工具取名为Android Lint。 32 | 33 | 当初,在Android Lint footnote:[关于Android Lint的官方介绍,可以参考这两篇文章: http://developer.android.com/tools/help/lint.html 和 http://developer.android.com/tools/debugging/improving-w-lint.html] 被加入到Android SDK的时候,提供给开发者的IDE还是Eclipse/ADT组合。就像其它Android SDK中的工具一样,Android Lint并没有与Eclipse/ADT紧密结合 footnote:[我猜测主要应该是Eclipse本身的限制,这也是Android Team后来开发Android Studio的原因],而是作为一个独立工具存在。其用法如下: 34 | ---- 35 | lint [flags] 36 | ---- 37 | 在Android Studio出现后,Android Lint与IDE进行了很好地整合。因此,对于Android Studio/Gradle项目,不再建议直接使用独立的lint工具,而是直接在Android Studio中使用或者通过gradle来调用: 38 | ---- 39 | $ ./gradlew lint 40 | ---- 41 | 42 | Android Lint内置了很多lint规则,用来检测一些常见的代码问题(例如,正确性问题、安全问题、性能问题,等等)。同时,Android Lint也支持自定义lint规则,以便开发者灵活应用,更好地提升项目代码质量 footnote:[关于Androd自定义lint的官方介绍,可以参考这两篇文章: http://tools.android.com/tips/lint-custom-rules 和 http://tools.android.com/tips/lint/writing-a-lint-check] 。利用自定义lint规则,既可以用来在项目中检测代码质量问题,也可以用来保证编码规范的执行。 43 | 44 | == 开始Android Lint自定义之旅 45 | 46 | 首先,我们需要创建一个Java项目,用来输出包含自定义lint规则的jar。有了包含lint规则的jar后,我们有两种后续方案: 47 | 48 | * 方案一:把此jar拷贝到 ~/.android/lint/ 目录中(文件名任意)。此时,这些lint规则针对所有项目生效。 49 | * 方案二:继续创建一个Android library项目,用来输出包含lint.jar的aar;然后,让目标项目依赖此aar即可使自定义lint规则生效。 50 | 51 | 由于方案一是全局生效的策略,无法单独针对目标项目,用处不大。在工程实践中,我们主要使用方案二。 52 | 53 | 为了探索自定义Android Lint规则的使用,我创建了一个独立项目AndroidArch,Github地址:https://github.com/yongce/AndroidArch。 54 | 55 | === AndroidArch项目简介 56 | 57 | 我们在开发Android应用时,需要对一些系统API进行二次封装,以便制定统一的处理策略,或者方便将来演进技术方案。为此,项目中往往需要制定开发规范,让团队中所有开发人员遵守。而在工程实践中,如果没有相应的技术手段来保障,团队中难免会有人触犯所制定的开发规范。 58 | 59 | AndroidArch项目的主要目的,就是用来展示如何通过自定义lint规则来保证开发规范的实施。 60 | 61 | 在AndroidArch项目中,共有4个模块: 62 | 63 | * :archLib:Android library项目,包含开发规范所定义的一些基类和一些wrapper类 64 | * :archLintRules:Java项目,包含开发规范所对应的自定义lint规则 65 | * :archLintRulesAAR:Android library项目,仅用来输出包含lint.jar的aar 66 | * :demo:示例项目,用来测试自定义lint规则 67 | 68 | === 输出自定义lint规则 69 | 70 | 自定义lint规则是以一个jar包形式存在的。因此,我们只需要创建一个标准的Java项目即可,参见AndroidArch项目的模块“:archLintRules”。 71 | 72 | 该Java项目主要有两个重要组成部分: 73 | 74 | * 一个Lint注册类:继承自 com.android.tools.lint.client.api.IssueRegistry 的类,用于提供此jar中所有输出的lint规则 75 | * 若干自定义lint规则类:继承自 com.android.tools.lint.detector.api.Detector 类,在其中定义代码检查规则,并定义相应的 com.android.tools.lint.detector.api.Issue 对象。 76 | 77 | 在输出的jar包中,我们需要在META-INF/MANIFEST.MF清单文件中,添加一项“Lint-Registry”,用来指定该Lint注册类。例如,在模块“:archLintRules”生成的jar中包含如下信息: 78 | ---- 79 | $ cat META-INF/MANIFEST.MF 80 | Manifest-Version: 1.0 81 | Lint-Registry: me.ycdev.android.arch.lint.MyIssueRegistry 82 | ---- 83 | 类 me.ycdev.android.arch.lint.MyIssueRegistry 的定义如下: 84 | [source,java] 85 | ---- 86 | public class MyIssueRegistry extends IssueRegistry { 87 | @Override 88 | public List getIssues() { 89 | System.out.println("!!!!!!!!!!!!! ArchLib lint rules works"); 90 | return Arrays.asList( 91 | MyToastHelperDetector.ISSUE, 92 | MyBroadcastHelperDetector.ISSUE, 93 | MyBaseActivityDetector.ISSUE, 94 | MyIntentHelperDetector.ISSUE 95 | ); 96 | } 97 | } 98 | ---- 99 | 从上面的代码可以看到,IssueRegistry类的主要接口是#getIssues(),返回jar中所有输出的Issue。而每一个Issue对象,关联了一个Detector类,从而间接指定了所有支持的lint规则。 100 | 101 | 在写自定义lint规则时,我们既可以分析Java代码(Java源码文件或者.class文件),也可以分析XML文件(AndroidManifest.xml和各种XML资源文件)。在模块“:archLintRules”中,仅用到了Java源码文件分析。由于相关官方文档还处于缺失状态,Android Lint内置规则的源码成了主要的参考资料: 102 | ---- 103 | git clone https://android.googlesource.com/platform/tools/base.git 104 | ---- 105 | lint规则相关代码位于目录 lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks 。 106 | 107 | ==== build.gradle 108 | 109 | 为了在输出的jar中添加Lint注册类信息,我们可以通过build.gradle配置来实现。例如: 110 | ---- 111 | jar { 112 | manifest { 113 | attributes("Lint-Registry": "me.ycdev.android.arch.lint.MyIssueRegistry") 114 | } 115 | } 116 | ---- 117 | 118 | 为了便于生成aar的模块能够直接编译依赖生成jar的模块,我们需要在build.gradle中做一些特殊处理来协同这两个模块。例如: 119 | ---- 120 | 121 | /* 122 | * rules for providing "MyLintRules.jar" 123 | */ 124 | configurations { 125 | lintJarOutput 126 | } 127 | 128 | dependencies { 129 | lintJarOutput files(jar) 130 | } 131 | ---- 132 | 这里,创建了一个叫“lintJarOutput”的Gradle configuration,用于输出我们生成的jar包。在生成aar的模块的build.gradle中会引用此configuration。 133 | 134 | === 输出包含lint.jar的aar 135 | 136 | 由于lint jar无法直接在目标项目中使用(这应该是Android Lint值得改进的地方),但aar文件中可以包含一个“lint.jar” footnote:[参考aar文件格式:http://tools.android.com/tech-docs/new-build-system/aar-format]。因此,我们需要创建一个Android library项目,仅用来输出“lint.jar”。 137 | 138 | 创建的Android library项目,仅需要配置build.gradle文件即可。例如: 139 | ---- 140 | /* 141 | * rules for including "lint.jar" in aar 142 | */ 143 | configurations { 144 | lintJarImport 145 | } 146 | 147 | dependencies { 148 | lintJarImport project(path: ":archLintRules", configuration: "lintJarOutput") 149 | } 150 | 151 | task copyLintJar(type: Copy) { 152 | from (configurations.lintJarImport) { 153 | rename { 154 | String fileName -> 155 | 'lint.jar' 156 | } 157 | } 158 | into 'build/intermediates/lint/' 159 | } 160 | 161 | project.afterEvaluate { 162 | def compileLintTask = project.tasks.find { it.name == 'compileLint' } 163 | compileLintTask.dependsOn(copyLintJar) 164 | } 165 | ---- 166 | 这里,创建了一个叫“lintJarImport”的Gradle configuration,其引用了模块 “:archLintRules”的Gradle configuration “lintJarOutput”。 167 | 168 | 同时,对内置的Gradle task “compileLint”做了修改,让其依赖于我们定义的一个task “copyLintJar”。在task “copyLintJar”中,把模块 “:archLintRules”输出的jar包拷贝到了build/intermediates/lint/lint.jar。从而,生成了一个包含“lint.jar”的aar文件。 169 | 170 | === 在项目中使用lint.jar 171 | 172 | 有了带有“lint.jar”的aar,我们可以在任何项目中依赖于它,从而让自定义lint规则生效。例如,在AndroidArch项目的模块“:archLib”的build.gradle中,有如下依赖: 173 | ---- 174 | dependencies { 175 | compile project(':archLintRulesAAR') 176 | } 177 | ---- 178 | 从而,让自定义lint规则在模块“:archLib”中生效。 179 | 180 | 而模块“:demo”并没有直接依赖于模块“:archLintRulesAAR”,而是通过模块“:archLib”间接依赖的: 181 | ---- 182 | dependencies { 183 | compile project(':archLib') 184 | } 185 | ---- 186 | 187 | 现在,让我们跑跑lint看看: 188 | ---- 189 | $ ./gradlew lint 190 | ... 191 | :archLib:lint 192 | !!!!!!!!!!!!! ArchLib lint rules works 193 | Ran lint on variant release: 0 issues found 194 | Ran lint on variant debug: 0 issues found 195 | No issues found. 196 | ... 197 | :demo:lint 198 | !!!!!!!!!!!!! ArchLib lint rules works 199 | Ran lint on variant release: 13 issues found 200 | Ran lint on variant debug: 13 issues found 201 | ---- 202 | 可以看到,自定义lint规则生效了! 203 | 204 | ==== 相关Bug 205 | 206 | 在探索自定义lint时,发现了两个Android Gradle的Bug:第一个Bug已经修复(http://code.google.com/p/android/issues/detail?id=174808);第二个Bug还未修复(http://code.google.com/p/android/issues/detail?id=178699)。 207 | 208 | 如果遇到了aar中的“lint.jar”不能被正常加载,可以尝试通过下面的workaround解决,或者升级Android Gradle插件版本解决('com.android.tools.build:gradle:1.3.0-beta4'版本合入了此Bug的修复): 209 | ---- 210 | // workaround for the bug: http://code.google.com/p/android/issues/detail?id=174808 211 | project.afterEvaluate { 212 | tasks.matching { 213 | it.name.startsWith('lint') 214 | }.each { task -> 215 | task.doFirst { 216 | fileTree(project.buildDir) { 217 | include '**/jars/lint.jar' 218 | }.each { File file -> 219 | println "copy lint jar: " + file.absolutePath 220 | file.renameTo(new File(file.parentFile.parentFile, file.getName())) 221 | } 222 | } 223 | } 224 | } 225 | ---- 226 | 关于此Bug的细节、workaround工作原理和Android官方的最终修复方法,请参考bug报告中的记录。 227 | 228 | 对于第二个Bug,具体表现在Android Lint仅会在编译第一个模块时加载“lint.jar”。因此,当需要编译多个模块时,不同的编译顺利可能会导致“lint.jar”能够加载或者无法加载。workaround也很简单,只要保证第一个编译的模块加载了“lint.jar”即可。 229 | 230 | === 单元测试 231 | 232 | 目前,自定义lint规则的单元测试还需要依赖于Android源码,但应该很快就会有独立的库可用了。参见https://bintray.com/android/android-tools/com.android.tools.lint.lint-tests/view,但目前还没有可供下载的文件。 233 | 234 | == 致谢 235 | 236 | Linkedin团队的Cheng Yang同学的这篇文章 https://engineering.linkedin.com/android/writing-custom-lint-checks-gradle 给了很好的启发和开始。 237 | -------------------------------------------------------------------------------- /notes/android-changes/android-6.0/0007-android-6.0-changes.asc: -------------------------------------------------------------------------------- 1 | = Android 6.0 Changes 2 | :toc: 3 | :toc-placement: preamble 4 | :toclevels: 3 5 | 6 | 官方文档:http://developer.android.com/about/versions/marshmallow/android-6.0-changes.html 7 | 8 | 本文进行简单的翻译和总结。 9 | 10 | 在我看来,需要重点关注如下方面: 11 | 12 | * 运行时权限带来的影响,考虑如何去适应和调整代码。 13 | * 评估打盹模式和应用待机给产品带来的影响。 14 | * 是时候完全移除对Apache HTTP Client的依赖。(发现国内好多SDK都在用,难!) 15 | * 确保能够使用Android SDK r23编译你的代码。 16 | 17 | :numbered: 18 | 19 | == 运行时权限 20 | 21 | 在Android 6.0之前,对于应用所申请的权限,用户只能在安装应用时被动接受, 22 | 没有任何选择权利(除非放弃安装)。 23 | 24 | Android 6.0引入了新的权限模型,允许用户在应用运行期间授予或者拒绝权限请求。 25 | 用户还可以通过权限管理界面来授予或撤销应用所申请的权限。 26 | 有了这个新的权限机制,在应用安装和升级时,不再需要用户确认权限或者权限变更, 27 | 使整个用户体验更加顺畅。 28 | 29 | 对于开发者,如果你打算把应用的Target SDK变更为23或者更高(Android 6.0+), 30 | 那么需要首先修改代码,在运行时去检查和请求所需权限。具体做法,请参考 31 | http://developer.android.com/training/permissions/index.html 。 32 | 33 | == 打盹模式和应用待机 (Doze & App Standby) 34 | 35 | Android 6.0在省电优化方面引入了两个新的机制: 36 | 打盹模式(Doze)和应用待机(App Standby)。 37 | 38 | === 打盹模式(Doze) 39 | 40 | 当设备未充电,且屏幕也是关闭状态,只要过上一小段时间, 41 | 设备就会进入打盹模式(Doze Mode),以便让系统尽可能地保持在休眠状态。 42 | 在这种模式下,设备会周期性地苏醒过来一小会,以便应用和系统执行一些待执行的操作。 43 | 44 | === 应用待机(App Standby) 45 | 46 | 应用待机(App Standby)机制允许系统决定,当用户很少使用一个应用时,其是否处于闲置状态。 47 | 当用户一段时间不去使用该应用时,系统会把其标记为闲置状态。 48 | 49 | 当设备未充电时,对于处于闲置状态的应用,系统会禁用它们的网络访问, 50 | 也会挂起它们的同步等其它任务。 51 | 52 | 关于 Doze & App Standby的更详细介绍,请参考 53 | http://developer.android.com/training/monitoring-device-state/doze-standby.html 。 54 | 55 | == Apache HTTP Client 56 | 57 | Android 6.0已经移除了对Apache HTTP Client的支持,请使用HttpURLConnection来代替。 58 | 59 | 如果需要继续使用Apache HTTP,需要在build.gradle文件中添加一行(不建议再使用): 60 | ``` 61 | android { 62 | useLibrary 'org.apache.http.legacy' 63 | } 64 | ``` 65 | #(注:需要Gradle plugin v1.3.0+)# 66 | 67 | == BoringSSL 68 | 69 | 在2014年OpenSSL的Heartbleed事件(参考 http://heartbleed.com )发生之后, 70 | 忍了OpenSSL很久的那帮人终于找到了理由,开始了分裂OpenSSL的行动: 71 | 72 | |=== 73 | |fork版本 |描述 74 | 75 | |LibreSSL 76 | |OpenBSD的fork版本。据称,在fork后的第一星期, 77 | OpenBSD团队首先清理掉了9万多行OpenSSL的老旧代码。 78 | 79 | |BoringSSL 80 | |Google的fork版本。 81 | 82 | |=== 83 | 84 | 在BoringSSL项目启动一年多之后,#Android 6.0已正式使用BoringSSL代替OpenSSL。# 85 | 86 | 如果你的native代码直接使用了系统的OpenSSL库(libcrypto.so & libssl.so), 87 | 那么需要调整你的代码,确保在Android 6.0上也能工作。 88 | 89 | Android NDK一直未把OpenSSL纳入NDK API中。在native代码中,如果想要获取加密能力, 90 | 需要通过JNI调用Java的加密API;或者,自己引入其它加密库。 91 | 92 | 或许,将来有一天,BoringSSL有机会被纳入NDK API。 93 | 毕竟这是掌握在Google手中的棋子,想怎么玩就怎么玩。 94 | 95 | == 获取硬件标识 96 | 97 | 为了保护用户的隐私,Android 6.0移除了WiFi和蓝牙的硬件标识的程序访问接口。 98 | 现在,WifiInfo.getMacAddress() 和 BluetoothAdapter.getAddress() 两个方法均 99 | 返回一个常量值 “02:00:00:00:00:00”。 100 | 101 | 通过蓝牙和Wi-Fi扫描时,为了获取附近外部设备的硬件标识,你的应用必须拥有相应的权限: 102 | 103 | |=== 104 | |接口|所需权限 105 | 106 | |WifiManager.getScanResults() 107 | |需要拥有 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 两个权限。 108 | 109 | |BluetoothDevice.ACTION_FOUND 110 | |需要拥有 ACCESS_COARSE_LOCATION 权限。 111 | 112 | |BluetoothLeScanner.startScan() 113 | |需要拥有 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 两个权限。 114 | 115 | |=== 116 | 117 | TIP: 当运行Android 6.0的设备发起后台Wi-Fi或者蓝牙扫描时, 118 | 外部的设备是可以感知到这个动作的,但外部设备看到的是一个随机产生的MAC地址。 119 | 120 | == 通知(Notification) 121 | 122 | Android 6.0移除了 Notification.setLatestEventInfo() 接口。 123 | 现在,需要使用 Notification.Builder 来构建 Notification 对象。 124 | 当需要反复更新一个通知时,应该重用 Notification.Builder 对象, 125 | 通过调用 build() 方法来获取更新后的Notification对象。 126 | 127 | TIP: 通过 adb shell dumpsys notification 命令,不再打印出通知的文本信息。 128 | 可通过 adb shell dumpsys notification --noredact 命令打印出一个通知对象的文本。 129 | 130 | == AudioManager 131 | 132 | 通过 AudioManager 去直接设置音量或者关闭声音的操作不再被被支持。 133 | setStreamSolo() 方法已经被废弃,请改用 requestAudioFocus() 方法。 134 | 类似地,setStreamMute() 方法也已经被废弃,请改用 adjustStreamVolume() 方法, 135 | 传入参数 ADJUST_MUTE 或者 ADJUST_UNMUTE。 136 | 137 | == 文本选择 138 | 139 | 当用户选中一段文本时,我们可以在展示一个悬浮工具条(floating toolbar), 140 | 里面有剪切、复制、粘贴等操作。悬浮工具条的用户交互实现类似于上下文action bar。 141 | 142 | 关于这里的文本选择设计规范可参考 143 | http://www.google.com/design/spec/patterns/selection.html#selection-text-selection 。 144 | 145 | 具体用法可参考 146 | http://developer.android.com/about/versions/marshmallow/android-6.0-changes.html#behavior-test-selection 。 147 | 148 | 目前仅支持Android 6.0,Support库也还不支持悬浮工具条。 149 | 150 | == Android Keystore 151 | 152 | Android keystore provider不再支持DSA,但ECDSA依然被支持。 153 | 154 | 当安全锁屏被禁用或者重置时,那些不需要加密的Key将不再被删除, 155 | 而那些需要加密的Key将被删除。(不理解这些Key具体指什么Key) 156 | 157 | == Wi-Fi和网络 158 | 159 | 现在,只有你自己创建的WifiConfuration,你才能够去修改它的状态。 160 | 你不再被允许去修改或者删除其他用户或者应用创建的WifiConfiguration。 161 | 162 | 之前,应用可以通过 enableNetwok() 和 “disableAllOthers=true” 163 | 来强迫设备去连接特定的Wi-Fi网络,并从其它网络断开。 164 | 现在,设备不再从其它网络断开。 165 | 可以通过 multinetwork API(Network#openConnection() & Network#bindSocket() 166 | & ConnectivityManager#bindProcessToNetwork())来确保使用特定的网络。 167 | 168 | == Camera Service 169 | 170 | 之前,在Camera Service中访问共享资源是按照“先来,先服务”的策略。 171 | 现在,改为了按优先级处理的策略。具体情况如下: 172 | 173 | * 访问Camera子系统资源,包括打开、配置Camera设备,是基于Camara client应用进程的优先级 174 | 来服务的。用户可见或者有前台Activity的应用进程,被赋予了高优先级。 175 | 176 | * 当一个高优先级的应用试图访问Camera时,低优先级的活跃Camera client可能会被踢掉。 177 | 这个时候,被踢掉的Camera client的onError()(老Camera API) 178 | 或者onDisconnected()(Camera2 API)会被回调。 179 | 180 | * 当有适当的Camera硬件时,不同的应用进程能够独自且同时打开和使用不同的Camera设备。 181 | 然而,对于多进程的使用场景(同时访问可能引起Camera设备的严重性能下降或者服务问题), 182 | 现在能够被Camera service检测到并禁止。这个改动可能会导致踢掉低优先级client, 183 | 尽管没有其它应用试图访问Camera设备。 184 | 185 | * 切换当前用户,会导致前一个用户账号拥有的应用的活跃Camera client被踢开。 186 | 访问Camera仅限于当前设备用户所拥有的user profiles。 187 | 188 | == Runtime 189 | 190 | ART Runtime已经正确实现了Constructor#newInstance()的访问规则。 191 | 这个改动修复了之前Dalvik没有正确检测访问规则的问题。 192 | 如果你的应用调用了Constructor#newInstance(),并且想重写访问规则, 193 | 请调用setAccessible并传递参数true。 194 | 如果你的应用使用了 v7-appcompat库 或者 v7-recycleview库, 195 | 你必须更新你的应用使用最新的support库。 196 | 否则,请确保所有XML中引用的自定义类都已更新,以便其constructors可以被访问。 197 | 198 | Android 6.0也更新了动态链接器的行为。现在,动态链接器已经理解一个库的so名字和路径的差异 199 | (参见bug https://code.google.com/p/android/issues/detail?id=6670 ), 200 | 通过so名字搜索已经被正确实现。之前有错误的DT_NEEDED条目(通常是编译机文件系统中的绝对路径) 201 | 的应用是可以工作的,现在在加载时可能会失败。 202 | 203 | dlopen(3) RTLD_LOCAL 参数已经被正确实现。由于 RTLD_LOCAL 是默认的, 204 | 因此对那些没有显示使用 RTLD_LOCAL 的 dlopen(3) 调用也是有影响的。 205 | 通过 RTLD_LOCAL,后面通过 dlopen(3) 调用加载的库将不能够使用之前加载的库中的符号。 206 | 207 | 在之前的Android版本中,如果你的应用请求系统加载一个有text relocations的共享库时, 208 | 系统会显示一个警告但依然允许加载那个库。 209 | 从Android 6.0开始,如果你的应用Target SDK是23(或者更高),那么系统会拒绝加载该库。 210 | 关于text relocations,可参考此文档 211 | https://wiki.gentoo.org/wiki/Hardened/Textrels_Guide 。 212 | 213 | == APK包验证 214 | 215 | 现在,系统执行更严格的APK包验证。如果清单文件中声明了一个文件,但却不存在于APK中, 216 | 那么系统会认为这个APK包是破损的。如果APK中的任何文件被移除,必须对APK包进行重签名。 217 | 218 | == USB连接 219 | 220 | 现在,通过USB端口的设备连接被设置为仅充电模式。为了通过USB连接来访问设备和其中的内容, 221 | 用户必须显示授予权限。 222 | 223 | == 企业Android(Android for Work) 224 | 225 | Android 6.0包含了许多企业Android的改动。 226 | 227 | === 企业联系人 228 | 229 | 当用户查看通话记录时,Google Dialer Call Log能够展示企业联系人。 230 | 可调用并传递参数 true 给 DevicePolicyManager#setCrossProfileCallerIdDisabled(), 231 | 来阻止企业联系人出现在Google Dialer Call Log中。 232 | 仅当调用并传递参数 false 给 DevicePolicyManager.setBluetoothContactSharingDisabled() 时, 233 | 企业联系人才可以被蓝牙设备所访问(默认不可访问)。 234 | 235 | === Wi-Fi配置 236 | 237 | 如果一个企业档案(Work Profile)被删除时,那么其所添加的Wi-Fi配置也会被一同删除。 238 | 239 | 如果 Settings.Global#WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN 设置为非零, 240 | 那么由已激活的 Device Owners 所创建的任何Wi-Fi配置都不可以被用户修改或删除。 241 | 用户依然可以创建和修改他们自己的Wi-Fi配置。已激活的 Device Owners 242 | 有特权编辑或删除任何Wi-Fi配置,包括那些别人创建的。 243 | 244 | === WPC安装提示 245 | 246 | 在非 managed context 情况下添加Google帐户时,如果Google账户需要通过 247 | Work Policy Controller 应用 (WPC) 来管理,那么添加过程中会提示用户安装合适的WPC。 248 | 同样,通过 “Settings > Accounts” 和 设备初次使用引导程序 249 | 发起的添加帐户也会有这个提示。 250 | 251 | === DevicePolicyManager APIs 变化情况 252 | 253 | * 调用 setCameraDisabled() 仅影响当前调用用户使用相机。 254 | 在 managed profile 中调用该方法,不会影响主帐户中应用的相机使用。 255 | 256 | * setKeyguardDisabledFeatures()方法对 Profile Owners 和 Device Owners 都可用了。 257 | 258 | * Profile Owner 现在可以设置如下的keyguard限制: 259 | ** KEYGUARD_DISABLE_TRUST_AGENTS 和 KEYGUARD_DISABLE_FINGERPRINT, 260 | 能够影响该 profile 的parent user。 261 | ** KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS, 262 | 仅影响 managed profile 中应用产生的通知。 263 | 264 | * createAndInitializeUser() 和 createUser() 两个方法已被废弃。 265 | 266 | * (理解不了这句话)The setScreenCaptureDisabled() method now also blocks the assist structure when an app of the given user is in the foreground. 267 | 268 | * EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM 已默认使用SHA-256, 269 | 依然支持SHA-1以便向后兼容(但将来会放弃支持)。 270 | EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM 现在仅支持SHA-256。 271 | 272 | * (这句话写错了?)Device initializer APIs which existed in the Android 6.0 (API level 23) are now removed. 273 | 274 | * EXTRA_PROVISIONING_RESET_PROTECTION_PARAMETERS 已被移除, 275 | 以便 NFC dump provisioning 不可以用编程方式去解锁一个有恢复出厂设置保护的设备。 276 | 277 | * 在 managed device 的 NFC provisioning过程中, 278 | 现在可以使用 EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE 来传递数据给 device owner app, 279 | 280 | * Android企业API都为Android 6.0运行时权限做了优化,包括 Work profile、assist layer等。 281 | DevicePolicyManager中新的权限API不会影响 pre-M 应用。 282 | 283 | * 当用户退出由 ACTION_PROVISION_MANAGED_PROFILE 或者 ACTION_PROVISION_MANAGED_DEVICE 284 | 发起的设置流程时,系统会返回返回码 Activity#RESULT_CANCELED。 285 | 286 | === 其它API变化情况 287 | 288 | * Data usage:类 android.app.usage.NetworkUsageStats 改名为 NetworkStats。 289 | 290 | === Global settings中的变化情况 291 | 292 | * 如下设置不再通过 DevicePolicyManager#setGlobalSettings() 来设置: 293 | ** BLUETOOTH_ON 294 | ** DEVELOPMENT_SETTINGS_ENABLED 295 | ** MODE_RINGER 296 | ** NETWORK_PREFERENCE 297 | ** WIFI_ON 298 | 299 | * 如下设置可通过 DevicePolicyManager#setGlobalSettings()来设置: 300 | ** Settings.Global#WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN 301 | -------------------------------------------------------------------------------- /notes/knowledge/0013-gradle-support-in-aosp-build.asc: -------------------------------------------------------------------------------- 1 | = Gradle Support in AOSP build 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | 自从Android Studio 1.0的正式发布,开发Android应用的首选IDE非其莫属。 16 | Android官方也一直在为Android Studio开发新功能,使得Android应用开发更加便捷和高效。 17 | 然而,在AOSP代码中,所有Apps的构建却依然基于AOSP makefile。 18 | AOSP的二次开发者无法利用Android Studio/Gradle的强大功能和新特性来开发应用,这是一件令人痛苦的事情。 19 | 本文介绍一个在AOSP build中支持Android Studio/Gradle项目的方法,供参考。 20 | 21 | 文章更新历史: 22 | 23 | * 2016/8/29 文章发布 24 | 25 | ''' 26 | 27 | :numbered: 28 | 29 | == 基本思路 30 | 31 | 由于Android Studio/Gradle项目的构建是通过自身完成的,仅依赖于Android SDK和网络库。 32 | 如果我们能够通过某种方式触发Gradle项目的构建,再把其生成的APK以AOSP的*BUILD_PREBUILT*方式编译进ROM, 33 | 那么即可大功告成。 34 | 35 | 按照这种思路,关键点在于,如何使BUILD_PREBUILT依赖的APK包去依赖另外一个target X, 36 | 而这个target X可以触发Gradle项目的构建。为了便于管理Gradle项目,应当尽量减少Gradle项目的个数 37 | (例如,我们仅有一个,每个应用都是Gradle项目的一个module)。 38 | 39 | 另外一点,在AOSP二次开发时,特别是为多设备构建时,我们往往需要使用Overlay来做一些定制。 40 | 这个可以通过Gradle的buildTypes或者productFlavors的Overlay机制来完成。 41 | 42 | == 实现细节 43 | 44 | === Gradle项目组织 45 | 46 | 假设我们仅有一个Gradle项目,并假定其在AOSP代码中的目录为*vendor/xxx/YYYProject*, 47 | 该项目中有多个app module(假设为AppAaa,AppBbb, AppCcc等)。 48 | 49 | 第一件事情,我们在Gradle项目root目录中为其定义一个公共的build.gradle文件 50 | (假设叫“yyy_project_apps_common.gradle”),这个文件中定义和约定一些公共的构建规则。 51 | 例如: 52 | 53 | .vendor/xxx/YYYProject/yyy_project_apps_common.gradle 54 | [source, grovvy] 55 | ---- 56 | android { 57 | compileSdkVersion rootProject.ext.compileSdkVersion 58 | buildToolsVersion rootProject.ext.buildToolsVersion 59 | 60 | defaultConfig { 61 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 62 | } 63 | 64 | if (project.plugins.hasPlugin("com.android.application")) { 65 | signingConfigs { 66 | platformKey { 67 | storeFile file("${rootDir}/keystores/platform.keystore") 68 | storePassword "android" 69 | keyAlias "platform" 70 | keyPassword "android" 71 | } 72 | } 73 | 74 | buildTypes { 75 | debug { 76 | signingConfig signingConfigs.platformKey 77 | } 78 | 79 | release { 80 | signingConfig signingConfigs.platformKey 81 | 82 | minifyEnabled true 83 | proguardFiles getDefaultProguardFile('proguard-android.txt') 84 | if (new File("${project.projectDir}/proguard-project.txt").exists()) { 85 | proguardFiles 'proguard-project.txt' 86 | } else if (new File("${project.projectDir}/proguard-rules.pro").exists()) { 87 | proguardFiles 'proguard-rules.pro' 88 | } else { 89 | println "no ProGuard file found for project: " + project.name 90 | } 91 | } 92 | } 93 | } 94 | 95 | lintOptions { 96 | textReport true 97 | textOutput 'stdout' 98 | 99 | abortOnError true 100 | } 101 | 102 | compileOptions { 103 | sourceCompatibility JavaVersion.VERSION_1_7 104 | targetCompatibility JavaVersion.VERSION_1_7 105 | } 106 | } 107 | ---- 108 | 109 | 然后在各app module的build.gradle中,引入yyy_project_apps_common.gradle: 110 | 111 | .vendor/xxx/YYYProject/AppAaa/build.gradle 112 | [source, grovvy] 113 | ---- 114 | apply plugin: 'com.android.application' 115 | apply from: "${rootDir}/yyy_project_apps_common.gradle" 116 | ... 117 | ---- 118 | 119 | ==== 为ROM构建APK 120 | 121 | 默认情况下,Gradle构建生成的APK文件都在各module自己的build目录下, 122 | 为了便于在Android.mk文件中引用Gradle生成的APK文件, 123 | 我们最好把Gradle生成的APK文件统一输出到一个目录中(假设为${rootDir}/rom_apps_out)。 124 | 因此,我们添加一个特殊的buildType “romApps”,并做一些定制: 125 | 126 | .vendor/xxx/YYYProject/yyy_project_apps_common.gradle 127 | [source, grovvy] 128 | ---- 129 | android { 130 | ... 131 | 132 | buildTypes { 133 | ... 134 | 135 | romApps { 136 | initWith(buildTypes.release) 137 | } 138 | } 139 | 140 | applicationVariants.all { variant -> 141 | variant.outputs.each { output -> 142 | if (variant.buildType.name.equals('romApps')) { 143 | output.outputFile = new File(output.outputFile.parent, 144 | "${project.name}.apk") 145 | output.assemble.doLast { 146 | copy { 147 | from output.outputFile.getAbsolutePath() 148 | into "${rootDir}/rom_apps_out" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | ... 156 | } 157 | ---- 158 | 159 | === 编写AOSP makfile规则 160 | 161 | 在Gradle项目root目录中,添加一个Android.mk文件,用于触发APK的构建,并最终打包进ROM中。 162 | 163 | 先看最终的Android.mk,后面再解释: 164 | 165 | .vendor/xxx/YYYProject/Android.mk 166 | [source,makefile] 167 | ---- 168 | LOCAL_PATH := $(call my-dir) 169 | 170 | # Gradle project info 171 | GRADLE_PROJECT_ROOT := $(LOCAL_PATH) 172 | ROM_APPS_DIR := rom_apps_out 173 | 174 | # Rules to build apps 175 | .PHONY: buildRomApps 176 | buildRomApps: 177 | $(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) assembleRomApps 178 | 179 | $(GRADLE_PROJECT_ROOT)/$(ROM_APPS_DIR)/%.apk: buildRomApps 180 | @echo "APK file is ready for $@" 181 | 182 | # Rules to add prebuilt APKs to ROM 183 | # AppAaa 184 | include $(CLEAR_VARS) 185 | LOCAL_MODULE := XxxAppAaa 186 | LOCAL_SRC_FILES := $(ROM_APPS_DIR)/AppAaa.apk 187 | LOCAL_CERTIFICATE := platform 188 | LOCAL_MODULE_CLASS := APPS 189 | include $(BUILD_PREBUILT) 190 | 191 | # AppBbb 192 | include $(CLEAR_VARS) 193 | LOCAL_MODULE := XxxAppBbb 194 | LOCAL_SRC_FILES := $(ROM_APPS_DIR)/AppAbb.apk 195 | LOCAL_CERTIFICATE := platform 196 | LOCAL_MODULE_CLASS := APPS 197 | include $(BUILD_PREBUILT) 198 | 199 | # AppCcc 200 | include $(CLEAR_VARS) 201 | LOCAL_MODULE := XxxAppCcc 202 | LOCAL_SRC_FILES := $(ROM_APPS_DIR)/AppCcc.apk 203 | LOCAL_CERTIFICATE := platform 204 | LOCAL_MODULE_CLASS := APPS 205 | include $(BUILD_PREBUILT) 206 | ---- 207 | 208 | 在上面的Android.mk中,每一个BUILD_PREBUILT,都依赖于rom_apps_out目录下一个APK文件; 209 | 而rom_apps_out目录下的每一个APK文件,都依赖于“buildRomApps”这个target; 210 | 而“buildRomApps”这个target是一个伪目标,其会去执行gradle构建命令。 211 | 就这样,我们触发了Gradle项目的构建,并把其生成的APK文件打包进了ROM。 212 | 213 | ==== 依赖规则细节 214 | 215 | 在前面的Android.mk文件中,只要当Gradle项目构建完毕后,才会去触发所有的BUILD_PREBUILT构建; 216 | 并且,构建任何一个BUILD_PREBUILT,都需要先构建整个Gradle项目。 217 | 我们能否只去建构所需要的Gradle module?能做到,但不能这样做! 218 | 219 | 先看如何做到: 220 | 221 | .vendor/xxx/YYYProject/Android.mk 222 | [source,makefile] 223 | ---- 224 | $(GRADLE_PROJECT_ROOT)/$(ROM_APPS_DIR)/%.apk: 225 | $(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) :$*:assembleRomApps 226 | ---- 227 | 228 | 为什么不能?我们在构建ROM时,一般会使用多个线程来并行构建,以提升构建速度(例如,make -j10)。 229 | 这导致多个APK的BUILD_PREBUILT可能被同时触发,从而导致Gradle项目同时被多次触发。 230 | 如果Gradle项目中有共用的library module,那么可能会出现建构异常。 231 | 232 | === 支持Overlay 233 | 234 | 我们知道,Android Gradle Plugin是可以通过buildTypes或者productFlavors来支持Overlay的; 235 | 另一方面,在执行Gradle构建命令时,也可以通过命令行参数向build.gradle脚本传递参数。 236 | 因此,我们可以把AOSP的Overlay目录,通过Gradle构建命令的参数传入到build.gradle脚本中, 237 | 并通过buildTypes或者productFlavors来实现Overlay。 238 | 239 | 首先,为了能够在AOSP和Gradle之间来协作支持Overlay,我们需要做一些约定: 240 | 241 | 1. 在AOSP中定义一个变量ROM_APPS_OVERLAY_DIR,指向Overlay目录; 242 | 2. 在Overlay目录下,是各应用的目录名; 243 | 3. 在各应用目录下,有“res”等目录(一般我们只会用到res的Overlay)。 244 | 245 | 有了上述约定,支持Overlay的示例代码如下: 246 | 247 | .vendor/xxx/YYYProject/Android.mk 248 | [source, makefile] 249 | ---- 250 | ... 251 | 252 | # Overlay path (must be relative path because of Android Gradle Plugin) 253 | ifneq ($(ROM_APPS_OVERLAY_DIR),) 254 | GRADLE_OVERLAY_RES_DIR_PARAM := -PROM_RES_OVERLAY_DIR=../../../$(ROM_APPS_OVERLAY_DIR) 255 | endif 256 | 257 | ... 258 | 259 | .PHONY: buildTicwearApps 260 | buildTicwearApps: 261 | $(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) assembleRomApps $(GRADLE_OVERLAY_RES_DIR_PARAM) 262 | 263 | ... 264 | ---- 265 | 266 | 在上面Android.mk示例中,由于ROM_APPS_OVERLAY_DIR变量指向的目录名是相对AOSP代码根目录的, 267 | 而Gradle的Overlay又需要相对路径,因此在定义GRADLE_OVERLAY_DIR_PARAM变量时, 268 | 需要是一个有效的相对路径(跟上面这个Android.mk的相对路径相关)。 269 | 270 | .vendor/xxx/YYYProject/yyy_project_apps_common.gradle 271 | [source, grovvy] 272 | ---- 273 | android { 274 | ... 275 | 276 | sourceSets { 277 | romApps { 278 | if (project.hasProperty('ROM_RES_OVERLAY_DIR')) { 279 | def modulePath = projectDir.path.substring(rootDir.getParent().length()); 280 | def resOverlayDir = "${ROM_RES_OVERLAY_DIR}${modulePath}/res" 281 | if (new File(rootDir, resOverlayDir).exists()) { 282 | println "Overlay res dir found: " + resOverlayDir 283 | res.srcDir resOverlayDir 284 | } 285 | } 286 | } 287 | } 288 | 289 | ... 290 | } 291 | ---- 292 | 293 | === 支持Clean 294 | 295 | 为了支持“make clean”,我们需要把Gradle的clean动作加进去: 296 | 297 | .build/core/main.mk 298 | [source, makefile] 299 | ---- 300 | ... 301 | 302 | .PHONY: clean 303 | clean: 304 | @rm -rf $(OUT_DIR)/* 305 | vendor/xxx/YYYProject/gradlew -p vendor/xxx/YYYProject clean 306 | @echo "Entire build directory removed." 307 | 308 | ... 309 | ---- 310 | 311 | 另外,为了便于单独clean Gradle项目,可以为其添加一个clean target: 312 | 313 | .vendor/xxx/YYYProject/Android.mk 314 | [source, makefile] 315 | ---- 316 | ... 317 | 318 | .PHONY: cleanGradleApps 319 | cleanGradleApps: 320 | $(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) clean 321 | 322 | ... 323 | ---- 324 | 325 | == 优劣分析 326 | 327 | 先说好处,例举如下: 328 | 329 | 1. 在开发应用时,不再需要同时支持Gradle和AOSP makefile两种构建方式; 330 | 2. 开发调试应用的构建方式和最终ROM构建方式一致,不会出现因构建方式差异导致的问题; 331 | 3. 仅支持Gradle构建方式,可以使我们充分利用Android Studio、Android Gradle Plugin、 332 | Android Support Library、Android SDK等工具的新特性,来提升开发效率与质量 333 | (例如,Jack工具链、Java 8语言特性)。 334 | 335 | 再来看看不利的地方: 336 | 337 | 1. ROM构建时需要依赖于外部工具和网络(Android SDK和一些从网络上下载的工具包); 338 | 2. ROM构建速度可能变慢(Gradle构建步骤比AOSP makefile复杂很多); 339 | 3. 未知风险,需要实践才可知。。。 340 | -------------------------------------------------------------------------------- /notes/knowledge/0009-android-keystore-system.asc: -------------------------------------------------------------------------------- 1 | = Android Keystore System 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | Keystore是存储和访问密钥的一种重要管理方式,也是安全相关API的重要基础。 16 | 本文介绍Android中系统Keystore的使用。 17 | 18 | NOTE: 反馈与建议,请移步: 19 | https://github.com/yongce/AndroidDevNotes/issues/8 20 | 21 | 文章更新历史: 22 | 23 | * 2015/11/19 文章发布 24 | 25 | ''' 26 | 27 | :numbered: 28 | 29 | == Android Keystore System 30 | 31 | 早在Android 1.6,为了支持VPN,就已经添加了一个系统keystore。 32 | 后来,这个keystore也被用于支持WiFi认证。但是,应用却不被允许访问这个keystore。 33 | 如果应用想要使用keystore的功能,只能创建自己的keystore。 34 | 35 | Android 4.0的到来,改变了这一现状。利用新添加的API KeyChain, 36 | 普通应用也可以访问这个系统keystore。应用可以利用KeyChain来安装自己的凭据 37 | (credentials),也可以直接使用系统keystore中已有的凭据(公钥证书和私钥)。 38 | 39 | 随着Android 4.3的发布,应用可以在Android Keystore Provider 40 | 中创建和使用私有的密钥(仅自己可见)。这是一个定制的Java Security Provider 41 | (即“AndroidKeyStore”),可以通过现有加密API来使用。 42 | 43 | === 安全特性 44 | 45 | ==== 防止提取密钥 46 | 47 | Android Keystore中的密钥信息受到提取保护,有如下特点: 48 | 49 | * 密钥信息永远不会进入应用进程。即使应用进程被入侵了, 50 | 攻击者也无法在应用进程中提取密钥信息。 51 | * 密钥信息可存储在安全硬件中(Trusted Execution Environment, 52 | Secure Element)。如果生成key时启用了这个特性, 53 | 那么key的密钥信息永远不会被提取到安全硬件外部。即使OS被入侵了, 54 | 攻击者也无法把密钥信息从安全硬件中提取出来。 55 | 但不是所有Android设备都支持安全硬件,可通过KeyInfo.isInsideSecurityHardware()来确认。 56 | 57 | ==== Key使用认证 58 | 59 | 为了防止Android设备中的key被非授权使用,Android Keystore 60 | 允许应用在生成或导入key时指定认证方式。一旦key被生成或导入,其认证方式无法更改。 61 | 62 | === KeyChain还是Android Keystore Provider?如何选择 63 | 64 | 通过KeyChain安装的凭据,是全系统可共享访问的(所有应用都可以访问)。 65 | 当应用请求使用凭据时,系统会弹出一个选择确认界面, 66 | 由用户选择决定访问哪个已安装的凭据。 67 | 68 | 使用Android Keystore Provider,应用可以存储自己私有的凭据, 69 | 只有它自己可以访问。当然,也不需要用户介入来选择凭据。 70 | 71 | == Keychain 72 | 73 | KeyChain允许用户授权应用去访问系统keystore,来存储和使用凭据(credentials)。 74 | 75 | 具体而言,KeyChain提供了两方面的能力: 76 | 77 | * 允许应用在系统keystore中安装凭据 78 | * 允许应用使用系统keystore中的凭据 79 | 80 | === 安装凭据 81 | 82 | 在用户介入并确认的情况下,应用可以把自己的凭据安装到系统keystore中。 83 | 84 | 目前,支持两种格式的凭据的导入:a) X.509证书; b) PKCS#12 keystore。 85 | 86 | 安装凭据的步骤如下: 87 | 88 | * 通过 KeyChain#createInstallIntent() 创建一个Intent对象, 89 | 其extra数据中携带了要安装的凭据; 90 | * 调用startActivityForResult来调起系统的安装确认界面 91 | (PKCS#12 keystore需要输入密码); 92 | * 在onActivityForResult中检查返回值,来确认安装是否成功。 93 | 94 | 示例代码如下: 95 | ``` 96 | public void installCredentials(Activity cxt, int requestCode) { 97 | AssetManager assetMgr = cxt.getAssets(); 98 | try { 99 | // password: android 100 | InputStream in = assetMgr.open("apk.keystore.p12"); 101 | byte[] keystoreData = IoUtils.readAllBytes(in); 102 | Intent installIntent = KeyChain.createInstallIntent(); 103 | installIntent.putExtra(KeyChain.EXTRA_PKCS12, keystoreData); 104 | cxt.startActivityForResult(installIntent, requestCode); 105 | } catch (IOException e) { 106 | e.printStackTrace(); 107 | } 108 | } 109 | ``` 110 | 111 | === 使用凭据 112 | 113 | 只要系统中安装有凭据,应用就可以请求使用凭据的授权。 114 | 115 | 使用凭据的步骤如下: 116 | 117 | * 调用KeyChain#choosePrivateKeyAlias(),弹出系统的授权确认界面, 118 | 用户可以选择并授权应用访问某个key; 119 | * 通过前面拿到的key alias和KeyChain#getPrivateKey(),就可以使用私钥; 120 | * 通过前面拿到的key alias和KeyChain#getCertificateChain(),就可以拿到证书链。 121 | 122 | 示例代码如下: 123 | ``` 124 | public void requestCredentials(Activity cxt) { 125 | final Context appContext = cxt.getApplicationContext(); 126 | KeyChain.choosePrivateKeyAlias(cxt, 127 | new KeyChainAliasCallback() { 128 | public void alias(String alias) { 129 | AppLogger.d(TAG, "got key alias [" + alias + "]"); 130 | if (alias != null) { 131 | mKeyAlias = alias; 132 | new Thread() { 133 | @Override 134 | public void run() { 135 | checkPrivateKey(appContext); 136 | checkCertificateChain(appContext); 137 | } 138 | }.start(); 139 | } 140 | } 141 | }, 142 | new String[]{"RSA", "DSA"}, null, null, -1, null); 143 | } 144 | 145 | private void checkPrivateKey(Context cxt) { 146 | PrivateKey privateKey = null; 147 | try { 148 | privateKey = KeyChain.getPrivateKey(cxt, mKeyAlias); 149 | AppLogger.d(TAG, "private key: " + privateKey); 150 | if (privateKey == null) { 151 | return; 152 | } 153 | AppLogger.d(TAG, "format: " + privateKey.getFormat()); 154 | AppLogger.d(TAG, "alg: " + privateKey.getAlgorithm()); 155 | } catch (KeyChainException e) { 156 | e.printStackTrace(); 157 | } catch (InterruptedException e) { 158 | e.printStackTrace(); 159 | } 160 | } 161 | 162 | private void checkCertificateChain(Context cxt) { 163 | try { 164 | X509Certificate[] certificates = KeyChain.getCertificateChain(cxt, mKeyAlias); 165 | if (certificates == null) { 166 | return; 167 | } 168 | AppLogger.i(TAG, "cert count: " + certificates.length); 169 | for (X509Certificate cert : certificates) { 170 | AppLogger.i(TAG, "cert: " + cert); 171 | } 172 | } catch (KeyChainException e) { 173 | e.printStackTrace(); 174 | } catch (InterruptedException e) { 175 | e.printStackTrace(); 176 | } 177 | } 178 | ``` 179 | 180 | == Android Keystore Provider 181 | 182 | 要使用Android Keystore Provider,只需要在使用KeyStore、KeyPairGenerator、 183 | KeyGenerator等API时,使用“AndroidKeyStore”这个provider即可。 184 | 185 | 从Android Keystore Provider所支持的 186 | http://developer.android.com/training/articles/keystore.html#SupportedAlgorithms[算法列表] 187 | 可以看到,虽然Android 4.3就添加了“AndroidKeyProvider”, 188 | 但大多数算法都是在Android 6.0添加的。 189 | 190 | 如果应用卸载后再重新安装,那么应用之前在Android Keystore Provider 191 | 中创建的key都不再可以访问(没去分析实现细节,很可能是卸载时系统就清除了所有key, 192 | 也不排除是因为应用UID发生变化导致的)。 193 | 194 | === 枚举所有key 195 | 196 | 枚举Android Keystore Provider中所有的key,示例代码: 197 | ``` 198 | KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); 199 | ks.load(null); 200 | Enumeration allAliases = ks.aliases(); 201 | ``` 202 | 203 | === 生成公私密钥 204 | 205 | 示例代码: 206 | ``` 207 | KeyPairGenerator kpg = KeyPairGenerator.getInstance( 208 | KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); 209 | kpg.initialize(new KeyGenParameterSpec.Builder( 210 | mPrivateKeyAlias, 211 | KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) 212 | .setDigests(KeyProperties.DIGEST_SHA256, 213 | KeyProperties.DIGEST_SHA512) 214 | .build()); 215 | KeyPair kp = kpg.generateKeyPair(); 216 | ``` 217 | 218 | === 生成对称密钥 219 | 220 | 示例代码: 221 | ``` 222 | KeyGenerator kg = KeyGenerator.getInstance( 223 | KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); 224 | kg.init(new KeyGenParameterSpec.Builder( 225 | mSecretKeyAlias, 226 | KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 227 | .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 228 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 229 | .setKeySize(256) 230 | .build()); 231 | SecretKey key = kg.generateKey(); 232 | ``` 233 | 234 | === 签名及验证 235 | 236 | 示例代码: 237 | ``` 238 | KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); 239 | ks.load(null); 240 | 241 | KeyStore.Entry entry = ks.getEntry(mPrivateKeyAlias, null); 242 | if (!(entry instanceof KeyStore.PrivateKeyEntry)) { 243 | AppLogger.w(TAG, "Not an instance of a PrivateKeyEntry"); 244 | return; 245 | } 246 | 247 | byte[] data = "hahahaha.....".getBytes(); 248 | 249 | // sign 250 | Signature s = Signature.getInstance("SHA256withECDSA"); 251 | s.initSign(((KeyStore.PrivateKeyEntry) entry).getPrivateKey()); 252 | s.update(data); 253 | byte[] signature = s.sign(); 254 | 255 | // verify 256 | Signature s2 = Signature.getInstance("SHA256withECDSA"); 257 | s2.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate()); 258 | s2.update(data); 259 | boolean valid = s2.verify(signature); 260 | ``` 261 | 262 | === 使用对称密钥加密数据 263 | 264 | 示例代码: 265 | ``` 266 | KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); 267 | ks.load(null); 268 | 269 | KeyStore.Entry entry = ks.getEntry(mSecretKeyAlias, null); 270 | if (!(entry instanceof KeyStore.SecretKeyEntry)) { 271 | AppLogger.w(TAG, "Not an instance of a SecretKeyEntry"); 272 | return; 273 | } 274 | 275 | String data = "fofofo...fofofo...fofofo..."; 276 | KeyStore.SecretKeyEntry secretEntry = (KeyStore.SecretKeyEntry) entry; 277 | 278 | // encrypt 279 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); 280 | cipher.init(Cipher.ENCRYPT_MODE, secretEntry.getSecretKey()); 281 | byte[] encryptedData = cipher.doFinal(data.getBytes()); 282 | byte[] iv = cipher.getIV(); 283 | 284 | // decrypt 285 | Cipher cipher2 = Cipher.getInstance("AES/CBC/PKCS7Padding"); 286 | cipher2.init(Cipher.DECRYPT_MODE, secretEntry.getSecretKey(), 287 | new IvParameterSpec(iv)); 288 | byte[] decryptedData = cipher2.doFinal(encryptedData); 289 | String plaintext = new String(decryptedData); 290 | ``` 291 | 292 | === 用户认证 293 | 294 | 在创建key时,可以指定key在使用时需要用户认证。 295 | 这里,用户认证支持安全锁屏的部分方式(解锁图案、PIN码、密码和指纹)。 296 | 297 | 示例代码: 298 | ``` 299 | if (mUserAuth && mKeyguardManager.isKeyguardSecure()) { 300 | builder.setUserAuthenticationRequired(true) 301 | .setUserAuthenticationValidityDurationSeconds(30); 302 | } 303 | ``` 304 | 305 | 在使用key时,如果捕获到 UserNotAuthenticatedException 异常, 306 | 可以重新申请用户认证。示例代码: 307 | ``` 308 | KeyguardManager keyguardMgr = (KeyguardManager) cxt.getSystemService(Context.KEYGUARD_SERVICE); 309 | Intent intent = keyguardMgr.createConfirmDeviceCredentialIntent(null, null); 310 | if (intent != null) { 311 | cxt.startActivityForResult(intent, requestCode); 312 | } 313 | ``` 314 | 315 | 除了上面的基于时间的认证,还可以使用指纹认证。 316 | 317 | == 示例代码 318 | 319 | 示例代码项目地址:https://github.com/ycdev-demo/AndroidKeystoreDemo 320 | 321 | 在示例代码中,展示了如何使用 KeyChain 和 Android Keystore Provider 相关API。 322 | 323 | == Reference 324 | 325 | * http://developer.android.com/training/articles/keystore.html[Android Keystore System] 326 | * http://android-developers.blogspot.com/2012/03/unifying-key-store-access-in-ics.html[Unifying Key Store Access in ICS] 327 | -------------------------------------------------------------------------------- /notes/knowledge/0012-android-java8.adoc: -------------------------------------------------------------------------------- 1 | # Android对Java 8的支持 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | ''' 16 | 文章更新历史: 17 | 18 | * 2016/8/17 文章发布(Jack版) 19 | * 2017/11/18 更新(JDK版) 20 | * 2020/05/30 更新(AGP 4.0 library desugaring) 21 | 22 | ''' 23 | 24 | :numbered: 25 | 26 | ## 引子 27 | 28 | [NOTE] 29 | ==== 30 | 为了便于表述,本文将使用如下缩写: 31 | 32 | * "AS"代表Android Studio 33 | * "AGP"代表Android Gradle Plugin 34 | ==== 35 | 36 | 让我们把时间拉回到2016年:随着Android N、AS 2.2和AGP 2.2的发布,Android团队正式宣布支持Java 8的部分语言特性和标准库。 37 | 但当时存在一个非常大的问题,新的Jack工具链无法支持基于.class文件(Java字节码)的相关工具和库,包括AS的Instant Run功能。 38 | 39 | 随着Android团队宣布放弃Jack工具链,以及Android O、AS 3.0和AGP 3.0的发布,Android团队再次宣布对Java 8的支持。 40 | 由于这次是基于JDK实现,不再存在之前Jack工具链的问题,支持的语言特性也不再有Android API的限制。 41 | 42 | 随着AGP 4.0的发布,支持了Java 8 library desugaring,基本完成了对Java 8的全面支持。 43 | 44 | 请参考支持Java 8的官方文档: 45 | https://developer.android.com/studio/write/java8-support.html 。 46 | 47 | TIP: Android团队最初的设想是用收购来的Jack工具链代替JDK,包括对Java 8的支持。 48 | 但由于诸多困难,最终还是宣布放弃Jack,重新回到JDK,见官方博客: 49 | https://android-developers.googleblog.com/2017/03/future-of-java-8-language-feature.html 。 50 | 51 | ## 支持的Java 8语言特性 52 | 53 | 目前,支持如下Java 8语言特性,并且对Androd API无限制(即可在所有设备上运行): 54 | 55 | * Lambda表达式 56 | * Method引用 57 | * interface支持default method和static method 58 | * Type annotations 59 | * Repeatable annotations 60 | 61 | 在上面这些语言特性中,最常用的应该是前三个。 62 | 63 | ### Lambda表达式 64 | 65 | 我们在设计API时,有时会要求调用者传入一个回调接口。这在支持函数的语言中,很容易实现。 66 | 但在Java语言中,由于method无法脱离class独立存在, 67 | 只能定义一个只有一个method的interface来实现回调(例如,Runnable)。 68 | 69 | 在Java 8之前,在调用这类API时,一般做法是实现一个匿名类来实现该interface。 70 | 显然,这里的代码是冗余的,我们仅需要这个匿名类的那个唯一method的实现即可。 71 | 因此,在Java 8语言中终于引入了被其它语言广泛使用Lambda表达式。 72 | 73 | 有了Lambda表达式,我们的代码会简化很多,不再需要实现很多匿名类(但编译器可能会自动做这件事)。 74 | 75 | 例如,在没有Lambda表达式时,我们经常会写出这样的代码: 76 | ```java 77 | findViewById(R.id.lambda).setOnClickListener(new OnClickListener() { 78 | @Override 79 | public void onClick(View view) { 80 | new LambdaDemo().showUsage(); 81 | } 82 | }); 83 | ``` 84 | 85 | 而有了Lambda表达式,我们的代码会变成这样: 86 | ```java 87 | findViewById(R.id.lambda).setOnClickListener(iew -> new LambdaDemo().showUsage()); 88 | ``` 89 | 90 | #### 语法 91 | 92 | 关于Lambda表达式的语言细节,可以参见Java官方文档: 93 | https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html 94 | 95 | Lambda表达式的语法基本上是这样的: 96 | ```java 97 | (type1 param1, type2 param2, ...) -> {statements} 98 | ``` 99 | 其中,参数类型、参数外面的圆括号和方法语句块外面的大括号在某些情况下都是可以省略的, 100 | 只要编译器在语法分析时不会产生错误和岐义即可。 101 | 102 | 这个语言特性看看例子就会了,很容易上手。例如: 103 | ```java 104 | executeTask(() -> System.out.println("Hello, world #1")); 105 | executeTask(() -> { 106 | System.out.print("Hello,"); 107 | System.out.println("world #2"); 108 | }); 109 | 110 | executeTask(a -> a * 2); 111 | 112 | executeTask((int lhs, int rhs) -> lhs + rhs); 113 | 114 | executeTask((long lhs, long rhs) -> lhs * rhs); 115 | ``` 116 | 117 | ### Method引用 118 | 119 | 假如,我们有如下的API设计: 120 | ```java 121 | @FunctionalInterface // optional 122 | public interface Transformer { 123 | T transform(F obj); 124 | } 125 | 126 | public static List transform(List list, Transformer transformer) { 127 | ArrayList result = new ArrayList(list.size()); 128 | for (F obj : list) { 129 | result.add(transformer.transform(obj)); 130 | } 131 | return result; 132 | } 133 | ``` 134 | 135 | 另外,我们也定义了一个Person类: 136 | ```java 137 | public static class Person { 138 | public int age; 139 | public String name; 140 | 141 | public Person() {} 142 | 143 | private Person(int age) { 144 | this.age = age; 145 | this.name = "P-" + age; 146 | } 147 | 148 | public static Person from(int age) { 149 | return new Person(age); 150 | } 151 | 152 | public int compareByAge(Person other) { 153 | return age - other.age; 154 | } 155 | 156 | @Override 157 | public String toString() { 158 | return "Person[name=" + name + ", age=" + age + "]"; 159 | } 160 | } 161 | ``` 162 | 163 | 现在,我们需要根据一个代表age的int列表,生成一个Person列表。那么,代码可能是这样的: 164 | ```java 165 | private void showCode() { 166 | List ageList = Arrays.asList(1, 2, 3, 4, 5); 167 | List personList = transform(ageList, age -> Person.from(age)); 168 | } 169 | 170 | private void showCode2() { 171 | List ageList = Arrays.asList(1, 2, 3, 4, 5); 172 | List personList = transform(ageList, age -> new Person(age)); 173 | } 174 | ``` 175 | 176 | 你会发现,虽然用了lambda表达式,代码实现也有点冗余, 177 | 因为做的事情仅仅是去调用了另外一个已经存在的method或者constructor。 178 | “Method引用”就是为了解决这样的编码场景,它可以让我们更简单地去引用一些已存在方法。 179 | 180 | 有了Method引用,上面的代码就可以简化了: 181 | ```java 182 | private void showCode() { 183 | List ageList = Arrays.asList(1, 2, 3, 4, 5); 184 | List personList = transform(ageList, Person::from); 185 | } 186 | 187 | private void showCode2() { 188 | List ageList = Arrays.asList(1, 2, 3, 4, 5); 189 | List personList = transform(ageList, Person::new); 190 | } 191 | ``` 192 | 193 | #### 语法 194 | 195 | 还是先放官方文档: 196 | https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html 197 | 198 | Method引用共有4种类型,例举如下: 199 | 200 | * 对static method的引用 (ContainingClass::staticMethodName) 201 | * 对一个特定对象的instance method的引用 (containingObject::instanceMethodName) 202 | * 对一个特定类型的任意对象的instance method的引用 (ContainingType::methodName) 203 | * 对constructor的引用 (ClassName::new) 204 | 205 | 示例代码如下: 206 | ```java 207 | private void staticMethod(List ageList) { 208 | // ContainingClass::staticMethodName 209 | List personList = transform(ageList, Person::from); 210 | } 211 | 212 | private Person createPerson(int age) { 213 | return Person.from(age); 214 | } 215 | 216 | private void instanceMethod(List ageList) { 217 | // containingObject::instanceMethodName 218 | List personList = transform(ageList, this::createPerson); 219 | } 220 | 221 | private void typeMethod(List personList) { 222 | // ContainingType::methodName 223 | Collections.sort(personList, Person::compareByAge); 224 | } 225 | 226 | private void constructor(List ageList) { 227 | // ClassName::new 228 | List personList = transform(ageList, Person::new); 229 | } 230 | ``` 231 | 232 | ### Default and static interface methods 233 | 234 | 官方文档: 235 | https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html 236 | 237 | 在Java 8中,允许为interface添加有具体实现的method(即default method), 238 | 并且,**如果仅新增了default method,那么修改后的interface可以和修改前的interface保待二进制兼容**。 239 | 有了此特性,Library开发者就可以为interface增加default method, 240 | 而不需要Library使用者同步去修改interface的实现代码。 241 | 242 | 除了default method,还可以在interface中添加static method, 243 | 方便把跟该interface相关的static方法放在一起。 244 | 245 | 示例代码: 246 | ```java 247 | interface ItsAnInterface { 248 | void fun(); 249 | 250 | default void foo() { 251 | System.out.println("foo"); 252 | } 253 | 254 | static void bar(ItsAnInterface a) { 255 | a.fun(); 256 | a.foo(); 257 | } 258 | } 259 | ``` 260 | 261 | ### Repeatable annotations 262 | 263 | 官方文档: 264 | https://docs.oracle.com/javase/tutorial/java/annotations/repeating.html 265 | 266 | Repeatable annotations允许重复使用annotion。例如: 267 | ```java 268 | @Schedule(dayOfMonth="last") 269 | @Schedule(dayOfWeek="Fri", hour="23") 270 | public void doPeriodicCleanup() { ... } 271 | ``` 272 | 273 | ### Type annotations 274 | 275 | 官方文档: 276 | https://docs.oracle.com/javase/tutorial/java/annotations/type_annotations.html 277 | 278 | 在Java 8之前,annotation仅能在声明出现的地方使用,而现在可以在类型出现的地方使用。 279 | (话是这么说,但我还没明白其应用场景。。。) 280 | 281 | ## 支持的Java 8 API 282 | 283 | Android N新增了一些Java 8 API(即这些API只在Android N及更高版本中才可用)。 284 | AGP 4.0通过library desugaring技术,允许开发者在代码中使用这些API,并能够在低版本Android设备上运行 285 | (原理见后解释)。 286 | 287 | 288 | 这些新增API大体可分为两部分。 289 | 290 | 跟语言特性相关的API: 291 | 292 | * java.lang.FunctionalInterface 293 | * java.lang.annotation.Repeatable 294 | * java.lang.reflect.Method.isDefault() 295 | * 跟Repeable annotations相关的反射API,例如AnnotatedElement.getAnnotationsByType(Class) 296 | 297 | 流式编程相关的API: 298 | 299 | * java.util.stream (流式编程) 300 | * java.util.function (函数对象) 301 | * java.lang.Iterable#forEach() 302 | 303 | 如果想在项目中使用这些Java 8 API,有两种选择: 304 | 305 | 1. 如果app的minSdkVersion在Android N及更高的版本,则直接使用即可; 306 | 2. 如果app的minSdkVersion在Android N之下,则需要使用 AGP 4.0 的『Java 8 library desugaring』特性。 307 | 308 | ### java.util.stream(流式编程) 309 | 310 | 跟其它流式编程的方案和API相比,基本上没太大差别,看一个简单的示例代码即可: 311 | 312 | ```java 313 | class StreamDemo { 314 | static class Student { 315 | String name; 316 | int age; 317 | double weight; 318 | int height; 319 | 320 | Student(String name, int age, double weight, int height) { 321 | this.name = name; 322 | this.age = age; 323 | this.weight = weight; 324 | this.height = height; 325 | } 326 | 327 | @Override 328 | public String toString() { 329 | return "Student[name=" + name + ", age=" + age + ", weight=" + weight 330 | + ", height=" + height + "]"; 331 | } 332 | } 333 | 334 | void showUsage() { 335 | generateDate().stream() 336 | .filter(s -> s.age >= 16 && s.age <= 17) 337 | .map(s -> { 338 | s.name = s.name.toLowerCase(); 339 | return s; 340 | }) 341 | .sorted((lhs, rhs) -> { 342 | int result = lhs.age - rhs.age; 343 | if (result != 0) { 344 | return result; 345 | } else { 346 | return rhs.name.compareTo(lhs.name); 347 | } 348 | }) 349 | .forEach(System.out::println); 350 | } 351 | 352 | private List generateDate() { 353 | List results = new ArrayList<>(); 354 | results.add(new Student("S1", 16, 40.5, 158)); 355 | results.add(new Student("S2", 18, 50, 165)); 356 | results.add(new Student("S3", 17, 43.3, 160)); 357 | results.add(new Student("S4", 16, 42.7, 157)); 358 | results.add(new Student("S5", 15, 45, 163)); 359 | results.add(new Student("S6", 17, 55, 175)); 360 | results.add(new Student("S7", 16, 57, 178)); 361 | results.add(new Student("S8", 15, 48, 167)); 362 | return results; 363 | } 364 | } 365 | ``` 366 | 367 | ### java.util.function (函数对象) 368 | 369 | Java 8抽象了很多跟function相关的API,覆盖到了常见的操作。这里看一下 java.util.function.Predicate 的实现代码即可, 370 | 顺便也可看到FunctionalInterface、default method、static method打配合的场景。 371 | 372 | ```java 373 | @FunctionalInterface 374 | public interface Predicate { 375 | boolean test(T t); 376 | 377 | default Predicate and(Predicate other) { 378 | Objects.requireNonNull(other); 379 | return (t) -> test(t) && other.test(t); 380 | } 381 | 382 | default Predicate negate() { 383 | return (t) -> !test(t); 384 | } 385 | 386 | default Predicate or(Predicate other) { 387 | Objects.requireNonNull(other); 388 | return (t) -> test(t) || other.test(t); 389 | } 390 | 391 | static Predicate isEqual(Object targetRef) { 392 | return (null == targetRef) 393 | ? Objects::isNull 394 | : object -> targetRef.equals(object); 395 | } 396 | } 397 | ``` 398 | 399 | ## Java 8 library desugaring 400 | 401 | AGP 4.0带来了 402 | https://developer.android.com/studio/releases/gradle-plugin?buildsystem=cmake#j8-library-desugaring[Java 8 library desugaring] , 403 | 以解决Java 8 API在低版本Android设备中缺失的问题。 404 | 405 | 实现原理: 406 | 407 | 1. 在APK包中携带一个额外的dex文件(如classes2.dex),包含了这些Java 8 API的实现, 408 | 但包名前缀由 __java.__ 变为了 __j$.__ (如 java.util.stream.Stream 变为了 j$.util.stream.Stream)。 409 | 2. 代码中调用这些Java 8 API的地方也被相应地进行了替换。 410 | 411 | 这里的技术方案,之前在Android Support Library迁移到Android X时已经被应用过(解决第三方库的兼容性问题)。 412 | 413 | 也可以预见,将来Java的新API都可以通过library desugaring技术来实现支持。 414 | 415 | ### 对APK包大小的影响 416 | 417 | 用本文的demo项目做了简单的测试(AGP 4.0),debug版本中的Java 8 dex文件大小为900KB+, 418 | 而启用了R8的release版本中的Java 8 dex文件大小为400KB。 419 | 由于APK本身还会压缩,所以对APK大小的影响会更小一些。 420 | 421 | ### MultiDex 422 | 423 | 由于用到了MultiDex,在Android 5.0以下的设备中,可能会影响启动速度(如果之前没有使用MultiDex)。 424 | 425 | ## 启用Java 8 426 | 427 | 要在项目中启用Java 8,只需要做一点调整即可: 428 | ```java 429 | compileOptions { 430 | sourceCompatibility JavaVersion.VERSION_1_8 431 | targetCompatibility JavaVersion.VERSION_1_8 432 | } 433 | ``` 434 | 435 | 最后,借用一张Android官方文档上的图来解释对Java 8的支持方式: 436 | 437 | image::../../images/java_compile_with_desugar.png[width="600"] 438 | (原图见 https://developer.android.com/studio/images/write/desugar_2x.png) 439 | 440 | ### Java 8 library desugaring 441 | 442 | 贴上官方的示例配置: 443 | 444 | [source,java] 445 | ---- 446 | android { 447 | defaultConfig { 448 | // Required when setting minSdkVersion to 20 or lower 449 | multiDexEnabled true 450 | } 451 | 452 | compileOptions { 453 | // Flag to enable support for the new language APIs 454 | coreLibraryDesugaringEnabled true 455 | // Sets Java compatibility to Java 8 456 | sourceCompatibility JavaVersion.VERSION_1_8 457 | targetCompatibility JavaVersion.VERSION_1_8 458 | } 459 | } 460 | 461 | dependencies { 462 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5' 463 | } 464 | ---- 465 | 466 | ## Demo 467 | 468 | 最后的最后,请参考Demo项目: 469 | https://github.com/ycdev-demo/AndroidJava8Demo 。 470 | -------------------------------------------------------------------------------- /notes/tools/0006-busybox-android.asc: -------------------------------------------------------------------------------- 1 | = 编译Android版busybox 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | NOTE: 反馈与建议,请移步: 16 | https://github.com/yongce/AndroidDevNotes/issues/7 17 | 18 | 文章更新历史: 19 | 20 | * 2015/07/23 文章发布 21 | 22 | ''' 23 | 24 | 本文介绍如何为Android编译busybox,包括静态链接版和动态链接版。 25 | 26 | 如果仅是为了开发者自己在手机上使用,应该编译静态链接版本,这样兼容性好,但编译出的可执行性文件较大; 27 | 如果是需要在应用中内置一个busybox,这时应该编译动态链接版本,并且仅保留需要的功能,这样可执行文件非常小,但兼容性会差一些。 28 | 29 | :numbered: 30 | 31 | == 熟悉busybox官方源码 32 | 33 | 在为Android编译busybox之前,先熟悉下busybox官方代码和相应的编译方法。 34 | 35 | === 下载busybox官方源码 36 | 37 | 下载busybox源码: 38 | ---- 39 | $ git clone git://busybox.net/busybox.git 40 | ---- 41 | 42 | 下载完成后,可选择切换到最新的稳定分支上: 43 | ---- 44 | $ git branch -r 45 | ... 46 | origin/1_21_stable 47 | origin/1_22_stable 48 | origin/1_23_stable 49 | origin/HEAD -> origin/master 50 | origin/master 51 | 52 | [PWD: ~/work/opensrc/busybox/busybox] (master) 53 | $ git checkout origin/1_23_stable -b android 54 | 分支 android 设置为跟踪来自 origin 的远程分支 1_23_stable。 55 | 切换到一个新分支 'android' 56 | ---- 57 | 58 | === 编译Ubuntu本地版练手 59 | 60 | 先看看makefile的帮助信息: 61 | ---- 62 | [PWD: ~/work/opensrc/busybox/busybox] (android) 63 | $ make help 64 | ... 65 | Configuration: 66 | allnoconfig - disable all symbols in .config 67 | allyesconfig - enable all symbols in .config (see defconfig) 68 | config - text based configurator (of last resort) 69 | defconfig - set .config to largest generic configuration 70 | menuconfig - interactive curses-based configurator 71 | ... 72 | ---- 73 | 74 | 编译一个默认配置版本: 75 | ---- 76 | [PWD: ~/work/opensrc/busybox/busybox] (android) 77 | $ make defconfig 78 | HOSTCC scripts/basic/docproc 79 | HOSTCC scripts/kconfig/zconf.tab.o 80 | HOSTLD scripts/kconfig/conf 81 | scripts/kconfig/conf -d Config.in 82 | ... 83 | 84 | $ make clean busybox -j4 85 | ... 86 | 87 | $ ll busybox 88 | -rwxrwxr-x 1 yongce yongce 861752 7月 21 13:50 busybox 89 | 90 | $ ldd busybox 91 | linux-vdso.so.1 => (0x00007ffc82bb6000) 92 | libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa56aba6000) 93 | libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa56a7dc000) 94 | /lib64/ld-linux-x86-64.so.2 (0x00007fa56aeae000) 95 | 96 | $ ndk-depends busybox 97 | WARNING: Could not find library: libm.so.6 98 | WARNING: Could not find library: libc.so.6 99 | busybox 100 | libm.so.6 101 | libc.so.6 102 | 103 | $ ./busybox 104 | BusyBox v1.23.2 (2015-07-21 13:50:45 CST) multi-call binary. 105 | ... 106 | Currently defined functions: 107 | [, [[, acpid, add-shell, addgroup, adduser, adjtimex, arp, arping, ash, 108 | awk, base64, basename, beep, blkid, blockdev, bootchartd, brctl, bunzip2, 109 | ... 110 | 111 | $ ./busybox uname -a 112 | Linux yongce-XPS-8700 3.19.0-21-generic #21-Ubuntu SMP Sun Jun 14 18:31:11 UTC 2015 x86_64 GNU/Linux 113 | ---- 114 | 简单解释前面执行的动作:首先,使用命令“make defconfig”创建了一个包含默认配置的配置文件“.config”;然后,基于这个配置文件“.config”,编译生成了一个本地可执行的busybox,这是一个动态链接版本,大小大概800多KB。 115 | 116 | 接下来,让我们编译一个最小的版本(不包含任何命令): 117 | ---- 118 | $ make allnoconfig 119 | ... 120 | 121 | $ make clean busybox -j4 122 | ... 123 | 124 | $ ll busybox 125 | -rwxrwxr-x 1 yongce yongce 6112 7月 21 14:19 busybox* 126 | 127 | $ ./busybox 128 | BusyBox v1.23.2 (2015-07-21 14:19:19 CST) multi-call binary. 129 | ... 130 | Currently defined functions: 131 | ---- 132 | 可以看到,不包含任何命令的最小busybox,其动态链接版本大小为6KB左右。 133 | 134 | == 编译busybox的Android静态链接版 135 | 136 | 如果要编译busybox的Android静态链接版,可以用busybox官方代码,通过交叉编译工具链即可编译生成。 137 | 138 | === 安装交叉编译工具链 139 | 140 | 首先,查看可供安装的交叉编译工具(仅以arm为例): 141 | ---- 142 | $ uname -a 143 | Linux yongce-XPS-8700 3.19.0-21-generic #21-Ubuntu SMP Sun Jun 14 18:31:11 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux 144 | 145 | $ apt-cache search "^gcc-arm" 146 | gcc-arm-linux-gnueabihf - The GNU C compiler for armhf architecture 147 | gcc-arm-linux-androideabi - cross toolchain and binutils for Android/Bionic on ARM 148 | gcc-arm-none-eabi - GCC cross compiler for ARM Cortex-A/R/M processors 149 | gcc-arm-linux-gnueabi - The GNU C compiler for armel architecture 150 | ---- 151 | 152 | 这里,选择安装“gcc-arm-linux-gnueabi”: 153 | ---- 154 | $ sudo apt-get install gcc-arm-linux-gnueabi 155 | ---- 156 | 157 | 安装完成后,所有gcc命令带有前缀“arm-linux-gnueabi-”:。 158 | ---- 159 | $ ls -w 1 /usr/bin/arm-linux-gnueabi-* 160 | /usr/bin/arm-linux-gnueabi-gcc 161 | /usr/bin/arm-linux-gnueabi-gcc-4.7 162 | ... 163 | ---- 164 | 165 | === 编译busybox 166 | 167 | 生成默认配置的配置文件: 168 | ---- 169 | $ make defconfig 170 | ---- 171 | 172 | 修改配置文件“.config”,改成静态链接,并使用交叉编译工具链“gcc-arm-linux-gnueabi”:(可通过make menuconfig来配置,也可以直接修改.config文件) 173 | ---- 174 | $ make menuconfig 175 | ... 176 | 177 | $ git df 178 | diff --git a/.config b/.config 179 | ... 180 | -# CONFIG_STATIC is not set 181 | +CONFIG_STATIC=y 182 | ... 183 | -CONFIG_CROSS_COMPILER_PREFIX="" 184 | +CONFIG_CROSS_COMPILER_PREFIX="arm-linux-gnueabi-" 185 | ... 186 | ---- 187 | 188 | 编译: 189 | ---- 190 | $ make clean busybox -j4 191 | ... 192 | 193 | $ ll busybox 194 | -rwxrwxr-x 1 yongce yongce 2037416 7月 21 14:35 busybox* 195 | 196 | $ ndk-depends busybox 197 | busybox 198 | 199 | $ /home/pub/tools/android-ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-readelf -d busybox 200 | 201 | There is no dynamic section in this file. 202 | 203 | $ ./busybox 204 | bash: ./busybox: cannot execute binary file: 可执行文件格式错误 205 | ---- 206 | 可以看到,默认配置编译出来的busybox静态版本,大小为2MB左右。当然,在本地机器上是无法执行的。 207 | 208 | NOTE: 使用这种方法编译的busybox可在github上下载: 209 | https://github.com/yongce/DevTools/blob/7487ff04e190960e1db18c3b64072e8b1759b6a3/app/src/main/assets/busybox_static 。 210 | 211 | === 在手机上测试运行 212 | 213 | 先测试一个Android 2.3的手机: 214 | ---- 215 | $ adb shell getprop | grep fingerprint 216 | [ro.build.fingerprint]: [google/passion/passion:2.3.3/GRI40/102588:user/release-keys] 217 | 218 | $ adb push busybox /data/local/tmp/busybox 219 | 1307 KB/s (2037416 bytes in 1.521s) 220 | 221 | $ adb shell /data/local/tmp/busybox 222 | BusyBox v1.23.2 (2015-07-21 14:29:45 CST) multi-call binary. 223 | ... 224 | 225 | $ adb shell /data/local/tmp/busybox uname -a 226 | Linux localhost 2.6.38.8-SavagedZen-4N1-BFS+ #20110701 PREEMPT Fri Jul 1 23:57:52 SGT 2011 armv7l GNU/Linux 227 | ---- 228 | 229 | 让我们再测试一个Android 5.0的机器: 230 | ---- 231 | $ adb shell getprop | grep fingerprint 232 | [ro.build.fingerprint]: [google/occam/mako:5.0/LRX21T/1576899:user/release-keys] 233 | 234 | $ adb push busybox /data/local/tmp/busybox 235 | 4403 KB/s (2037416 bytes in 0.451s) 236 | 237 | $ adb shell /data/local/tmp/busybox 238 | BusyBox v1.23.2 (2015-07-21 14:29:45 CST) multi-call binary. 239 | ... 240 | 241 | $ adb shell /data/local/tmp/busybox uname -a 242 | Linux localhost 3.4.0-perf-g60eefcd #1 SMP PREEMPT Fri Oct 10 18:28:38 UTC 2014 armv7l GNU/Linux 243 | ---- 244 | 245 | === 编译一个最小的busybox 246 | 247 | ---- 248 | $ make allnoconfig 249 | ... 250 | 251 | $ make menuconfig 252 | ... 253 | 254 | $ git df 255 | diff --git a/.config b/.config 256 | ... 257 | -# CONFIG_STATIC is not set 258 | +CONFIG_STATIC=y 259 | ... 260 | -CONFIG_CROSS_COMPILER_PREFIX="" 261 | +CONFIG_CROSS_COMPILER_PREFIX="arm-linux-gnueabi-" 262 | ... 263 | 264 | $ make clean busybox -j4 265 | ... 266 | 267 | $ ll busybox 268 | -rwxrwxr-x 1 yongce yongce 509028 7月 21 15:01 busybox* 269 | 270 | $ ./busybox 271 | bash: ./busybox: cannot execute binary file: 可执行文件格式错误 272 | ---- 273 | 可以看到,不包含任何命令的最小静态链接版本也有500KB左右。 274 | 275 | == 编译busybox的Android动态链接版 276 | 277 | === PIE介绍 278 | 279 | Android 4.1引入了PIE(position-independent executables), 280 | 在此模式下,DL(Dynamic Linker)在加载动态链接库时,不再加载到一个固定地址上, 281 | 从而提高系统的安全性。 282 | 283 | Android 5.0强制启用了PIE,要求其上运行的可执行文件必须以PIE模式加载动态链接库。 284 | 因此,在Android 5.0上运行的可执行文件,如果是动态链接的,则必须以PIE模式编译。 285 | 286 | 例如,Android 5.0+的手机上,运行未启用PIE且动态链接的可执行文件会遇到如下错误提示: 287 | ---- 288 | $ adb shell /data/local/tmp/busybox 289 | error: only position independent executables (PIE) are supported. 290 | ---- 291 | 292 | 而在Android 4.1之前的手机上,运行启用了PIE的可执行文件则会遇到段错误: 293 | ---- 294 | $ adb shell /data/local/tmp/busybox 295 | [1] Segmentation fault /data/local/tmp/... 296 | ---- 297 | 298 | 因此,如果要编译动态链接的可执行文件,则至少需要编译两个版本, 299 | 分别针对Android 4.1之前的系统和Android 4.1及其后的系统(以Android 5.0为条件也可以)。 300 | 301 | === 使用Android NDK编译官方源码 302 | 303 | 由于Android NDK是官方提供的Android native程序编译工具,支持arm, x86, mips等架构。 304 | 因此,Android NDK算是编译动态链接版本程序的理想工具。 305 | 但由于busybox是按照Linux编程接口开发的,而Android NDK仅支持部分编程接口。 306 | 因此,在官方busybox源码基础上编译出来完整功能的busybox是很困难的(很多功能的代码都需要打补丁才能编译)。 307 | 308 | 回到初衷,我们之所以需要编译动态链接版本的busybox,是因为我们需要一个剪裁版的busybox,并且需要可执行文件尽可能的小。 309 | 因此,编译剪裁版busybox,使用Android NDK值得尝试的方案。 310 | 311 | 在busybox官方源码中,已经有了支持Android NDK的编译配置(文件configs/android_ndk_defconfig)。 312 | 这个Android版配置与默认配置的主要差别在一些编译选项上有所不同,如下面列出的5个选项: 313 | 314 | 315 | * CONFIG_CROSS_COMPILER_PREFIX和CONFIG_SYSROOT:分别指定交叉编译工具链前缀和sysroot目录。 316 | * CONFIG_EXTRA_CFLAGS:定义一些宏和指定一些编译选项。 317 | * CONFIG_EXTRA_LDFLAGS:指定一些连接选项 318 | * CONFIG_EXTRA_LDLIBS:指定需要动态链接的库 319 | 320 | 例如,我使用android-ndk-r10e编译busybox,相应的配置如下: 321 | ---- 322 | CONFIG_CROSS_COMPILER_PREFIX="/home/pub/tools/android-ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-" 323 | CONFIG_SYSROOT="/home/pub/tools/android-ndk/platforms/android-9/arch-arm" 324 | CONFIG_EXTRA_CFLAGS="-DANDROID -D__ANDROID__ -DSK_RELEASE -nostdlib -march=armv7-a -msoft-float -mfloat-abi=softfp -mfpu=neon -mthumb -mthumb-interwork -fpic -fno-short-enums -fgcse-after-reload -frename-registers" 325 | CONFIG_EXTRA_LDFLAGS="-fuse-ld=bfd -Xlinker -z -Xlinker muldefs -nostdlib -Bdynamic -Xlinker -dynamic-linker -Xlinker /system/bin/linker -Xlinker -z -Xlinker nocopyreloc -Xlinker --no-undefined ${SYSROOT}/usr/lib/crtbegin_dynamic.o ${SYSROOT}/usr/lib/crtend_android.o" 326 | CONFIG_EXTRA_LDLIBS="c gcc" 327 | ---- 328 | 329 | NOTE: 在上面的示例中,需要关注CONFIG_EXTRA_CFLAGS中的-march=armv7-a,这个选项仅支持armv7; 330 | 在CONFIG_EXTRA_LDFLAGS选项中,我添加了“-fuse-ld=bfd”,原因是android-ndk-r10e中ld有bug,参见: 331 | https://code.google.com/p/android/issues/detail?id=177690 。 332 | 333 | 例如,我仅启用了部分功能的测试情况: 334 | ---- 335 | $ make clean busybox 336 | ... 337 | 338 | $ ll busybox 339 | -rwxrwxr-x 1 yongce yongce 70872 7月 23 17:40 busybox* 340 | 341 | $ /home/pub/tools/android-ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-readelf -h busybox | grep "Type:" 342 | Type: EXEC (Executable file) 343 | 344 | $ /home/pub/tools/android-ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-readelf -d busybox | grep "Shared library:" 345 | 0x00000001 (NEEDED) Shared library: [libc.so] 346 | 347 | $ ndk-depends busybox 348 | busybox 349 | libc.so 350 | 351 | $ adb push busybox /data/local/tmp/busybox 352 | 974 KB/s (70872 bytes in 0.070s) 353 | 354 | $ adb shell /data/local/tmp/busybox 355 | BusyBox v1.23.2 (2015-07-23 17:40:17 CST) multi-call binary. 356 | BusyBox is copyrighted by many authors between 1998-2012. 357 | Licensed under GPLv2. See source distribution for detailed 358 | copyright notices. 359 | 360 | Usage: busybox [function [arguments]...] 361 | or: busybox --list 362 | or: function [arguments]... 363 | 364 | BusyBox is a multi-call binary that combines many common Unix 365 | utilities into a single executable. Most people will create a 366 | link to busybox for each function they wish to use and BusyBox 367 | will act like whatever it was invoked as. 368 | 369 | Currently defined functions: 370 | basename, cat, chgrp, chmod, chown, cp, cut, echo, egrep, env, fgrep, 371 | grep, id, ln, ls, mkdir, mv, pwd, readlink, rm, touch, uname, whoami 372 | 373 | ---- 374 | 375 | NOTE: “touch”命令不能启用“-h”选项,否则编译会失败。 376 | 在选择功能/编译的过程中,如果遇到编译错误,要么禁用无法编译的命令, 377 | 要么修改busybox代码来适应Android。网上也有非常多的相关命令的patch,可以参考。 378 | 379 | NOTE: 这里使用的完整配置文件可到github上查看: 380 | https://github.com/ycdev-fork/busybox/blob/023db9a8b299f60fd0803d9d7c20e1ea963a446d/configs/android_ndk_ycdev 。 381 | 382 | ==== 启用PIE编译 383 | 384 | 启用PIE比较简单,在CONFIG_EXTRA_CFLAGS中添加选项“-fPIE”, 385 | 在CONFIG_EXTRA_LDFLAGS中添加选项“-fPIE -pie”即可: 386 | ---- 387 | $ git df 388 | diff --git a/.config b/.config 389 | ... 390 | -CONFIG_EXTRA_CFLAGS="-DANDROID -D__ANDROID__ -DSK_RELEASE -nostdlib -march=armv7-a -msoft-float -mfloat-abi=softfp -mfpu=neon -mthumb -mthumb-interwork -fpic -fno-short-enums -fgcse-after-reload -frename-registers" 391 | -CONFIG_EXTRA_LDFLAGS="-fuse-ld=bfd -Xlinker -z -Xlinker muldefs -nostdlib -Bdynamic -Xlinker -dynamic-linker -Xlinker /system/bin/linker -Xlinker -z -Xlinker nocopyreloc -Xlinker --no-undefined ${SYSROOT}/usr/lib/crtbegin_dynamic.o ${SYSROOT}/usr/lib/crtend_android.o" 392 | +CONFIG_EXTRA_CFLAGS="-fPIE -DANDROID -D__ANDROID__ -DSK_RELEASE -nostdlib -march=armv7-a -msoft-float -mfloat-abi=softfp -mfpu=neon -mthumb -mthumb-interwork -fpic -fno-short-enums -fgcse-after-reload -frename-registers" 393 | +CONFIG_EXTRA_LDFLAGS="-fPIE -pie -fuse-ld=bfd -Xlinker -z -Xlinker muldefs -nostdlib -Bdynamic -Xlinker -dynamic-linker -Xlinker /system/bin/linker -Xlinker -z -Xlinker nocopyreloc -Xlinker --no-undefined ${SYSROOT}/usr/lib/crtbegin_dynamic.o ${SYSROOT}/usr/lib/crtend_android.o" 394 | ... 395 | ---- 396 | 397 | 编译成功后,可以通过readelf命令查看是否启用了PIE: 398 | ---- 399 | $ /home/pub/tools/android-ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-readelf -h busybox | grep "Type:" 400 | Type: DYN (Shared object file) 401 | ---- 402 | 403 | NOTE: 这里使用的完整配置文件可到github上查看: 404 | https://github.com/ycdev-fork/busybox/blob/023db9a8b299f60fd0803d9d7c20e1ea963a446d/configs/android_ndk_ycdev_pie 。 405 | 406 | === 使用Android源码环境编译CyanogenMod版busybox 407 | 408 | CyanogenMod移植了busybox,以使其可在Android源码环境中编译。 409 | 代码地址如下: 410 | ---- 411 | $ git clone https://github.com/CyanogenMod/android_external_busybox.git 412 | ---- 413 | 使用CyanogenMod版本的busybox,编译busybox的动态/静态版本都比较容易。但有两个主要缺点: 414 | 415 | * 相对官方代码有一定的版本滞后性,版本更新不够及时 416 | * 在对busybox裁剪时,没有官方的版本方便 417 | -------------------------------------------------------------------------------- /notes/sourcecode/0015-retrofit.asc: -------------------------------------------------------------------------------- 1 | # Retrofit2 源码解析 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | ''' 16 | 文章更新历史: 17 | 18 | * 2020/6/? 文章发布 19 | 20 | ''' 21 | 22 | :numbered: 23 | 24 | ## 引子 25 | 26 | https://github.com/square/retrofit[Retrofit] 是一个被工程师们广泛接受的网络库。 27 | 它在 https://github.com/square/okhttp[OkHttp] 的基础上, 28 | 提供了对开发者更加友好的APIs(最终还是通过 OkHttp 来执行网络请求)。 29 | 30 | [quote, https://square.github.io/retrofit/#introduction] 31 | ____ 32 | Retrofit turns your HTTP API into a Java interface. 33 | ____ 34 | 35 | 我们先看一段 OkHttp 官方的示例代码: 36 | 37 | [source,java] 38 | ---- 39 | // From: https://square.github.io/okhttp/#post-to-a-server 40 | public static final MediaType JSON 41 | = MediaType.get("application/json; charset=utf-8"); 42 | 43 | OkHttpClient client = new OkHttpClient(); 44 | 45 | String post(String url, String json) throws IOException { 46 | RequestBody body = RequestBody.create(json, JSON); 47 | Request request = new Request.Builder() 48 | .url(url) 49 | .post(body) 50 | .build(); 51 | try (Response response = client.newCall(request).execute()) { 52 | return response.body().string(); 53 | } 54 | } 55 | ---- 56 | 57 | 上面的代码是不是朴素无华?一看就懂。 58 | 59 | 我们再来看看 Retrofit 的示例代码: 60 | 61 | 1. 定义Service接口类,跟服务端的Restful APIs一一对应: 62 | + 63 | -- 64 | [source, java] 65 | ----- 66 | // Refer: https://developer.github.com/v3/ 67 | public interface GitHubService { 68 | @GET("/users/{user}") 69 | Call userProfile(@Path("user") String user); 70 | 71 | @GET("users/{user}/repos") 72 | Call> listRepos(@Path("user") String user); 73 | } 74 | ----- 75 | -- 76 | 77 | 1. 执行网络请求: 78 | + 79 | -- 80 | [source, java] 81 | ----- 82 | Retrofit retrofit = new Retrofit.Builder() 83 | .baseUrl("https://api.github.com/") 84 | .addConverterFactory(GsonConverterFactory.create()) 85 | .build(); 86 | 87 | GitHubService service = retrofit.create(GitHubService.class); 88 | 89 | Call> reposCall = service.listRepos("yongce"); 90 | List repos = reposCall.execute().body(); // 执行网络请求 91 | ----- 92 | -- 93 | 94 | 1. 还支持 RxJava: 95 | + 96 | -- 97 | [source, java] 98 | ----- 99 | public interface GitHubService { 100 | @GET("users/{user}/repos") 101 | Observable> listRepos(@Path("user") String user); 102 | } 103 | ----- 104 | 105 | 也支持Kotlin协程的 **supsend** 关键字: 106 | [source, java] 107 | ----- 108 | interface GitHubService { 109 | @GET("users/{user}/repos") 110 | suspend fun listRepos(@Path("user") user: String): List 111 | } 112 | 113 | // Called on main thread 114 | suspend fun refreshRepos() { 115 | val retrofit = Retrofit.Builder() 116 | .baseUrl("https://api.github.com/") 117 | .addConverterFactory(GsonConverterFactory.create()) 118 | .build() 119 | val service: GitHubService = retrofit.create() 120 | val repos: List = service.listRepos("yongce") 121 | // use 'repos' to update UI directly... 122 | } 123 | ----- 124 | 125 | -- 126 | 127 | 当第一眼看到上面的 Retrofit 示例代码时,是不是一下来了精神,心中的问号越来越多? 128 | 大家一定很好奇,这个魔法是怎么实现的,其技术原理如何? 129 | 但并不是每个人都愿意去分析源码,不论是因为没有时间,还是因为过于枯燥、耗时、乏味。 130 | 本文将为你呈现 Retrofit2 背后的技术原理和实现细节,不论你是仅想知道其技术原理, 131 | 还是也想自己去分析源码,都可以从本文有所收获。 132 | 133 | 本文的源码解析将分为如下几个部分: 134 | 135 | * 技术原理 136 | * 设计模式 137 | * 实现细节 138 | 139 | ### 源码导入 140 | 141 | 当前 Retrofit2 源码使用 Gradle 来构建,你在 IntelliJ IDEA 或者 Android Studio 142 | 中直接导入源码即可。 143 | 144 | [NOTE] 145 | ==== 146 | 本文所分析的源码版本如下: 147 | ---- 148 | $ git show HEAD 149 | commit 65de04d1da61f9e8124956f6fff8902fc81d05e2 150 | Merge: 9e597f2 56e15f8 151 | Author: Jake Wharton 152 | Date: Wed Jun 17 23:18:03 2020 -0400 153 | 154 | Merge pull request #3424 from clydebarrow/robovm 155 | 156 | Prevent use of Java 8 classes on RoboVM 157 | ---- 158 | ==== 159 | 160 | ## 技术原理 161 | 162 | TIP: 如果之前没有接触过 Retrofit 库,需要先花点时间去看看 163 | https://square.github.io/retrofit[Retrofit官方文档] , 164 | 并运行一些示例代码直观感受下。 165 | 166 | 当我们在思考 Retrofit 背后的技术原理时,可能会提出如下一些问题: 167 | 168 | 1. 下面这段代码的背后发生了什么?Service接口类的对象是如何被创建出来的? 169 | + 170 | -- 171 | [source,java] 172 | ---- 173 | GitHubService service = retrofit.create(GitHubService.class); 174 | ---- 175 | -- 176 | 177 | 1. 下面这段对Service接口类方法的调用,发生了什么事情?返回值似乎跟 OkHttp 的Call作用相似? 178 | + 179 | -- 180 | [source,java] 181 | ---- 182 | Call> reposCall = service.listRepos("yongce"); 183 | ---- 184 | -- 185 | 186 | 1. 下面这段代码似乎与OkHttp的Call相似?这里才真正执行网络请求? 187 | + 188 | -- 189 | [source,java] 190 | ---- 191 | List repos = reposCall.execute().body(); 192 | ---- 193 | -- 194 | 195 | 1. Service接口类中的方法返回值还可以是其它的?是如何支持其它类型的? 196 | + 197 | -- 198 | [source,java] 199 | ---- 200 | Observable> listRepos(@Path("user") String user); 201 | ---- 202 | -- 203 | 204 | 1. suspend函数似乎有点不一样?直接执行了网络请求,而不是返回一个类似Call的中间对象? 205 | 按照Kotlin suspend函数的实现惯例,应该可以直接在main线程调用而不会阻塞main线程? 206 | + 207 | -- 208 | [source,java] 209 | ---- 210 | suspend fun listRepos(@Path("user") user: String): List 211 | ---- 212 | -- 213 | 214 | 1. Converter.Factory 和 CallAdapter.Factory 是如何工作的? 215 | + 216 | -- 217 | [source,java] 218 | ---- 219 | Retrofit retrofit = new Retrofit.Builder() 220 | .baseUrl("https://api.github.com/") 221 | .addConverterFactory(GsonConverterFactory.create()) 222 | .addCallAdapterFactory(RxJavaCallAdapterFactory.createWithScheduler(io())) 223 | .build(); 224 | ---- 225 | -- 226 | 227 | 1. Retrofit 是如何把网络请求转发给 OkHttp 的? 228 | 1. Retrofit 是如何处理Service接口类中的那些注解的? 229 | 1. Retrofit 整个工作流程是怎样的?主要有哪些步骤? 230 | 231 | ### 动态代理 232 | 233 | 我们先来寻找下面代码的答案: 234 | [source,java] 235 | ---- 236 | GitHubService service = retrofit.create(GitHubService.class); 237 | ---- 238 | 239 | 240 | 当我们去查看 Retrofit 源码时,很快就能发现上面代码背后的真相: 241 | 242 | .Retrofit类源码片断 243 | [source,java] 244 | ---- 245 | public T create(final Class service) { 246 | validateServiceInterface(service); 247 | return (T) 248 | Proxy.newProxyInstance( 249 | service.getClassLoader(), 250 | new Class[] {service}, 251 | new InvocationHandler() { 252 | ...省略代码 253 | }); 254 | } 255 | ---- 256 | 257 | 原来是通过Java动态代理技术来创建Service接口类的对象的。 258 | 259 | 此时,似乎Retrofit背后的整个蓝图有点轮廓了? 260 | 261 | 1. 通过Java动态代理创建出Service接口类对象。 262 | 263 | 1. (猜测)在InvocationHandler中处理Service接口类的方法调用时,解析该方法的函数签名和注解, 264 | 根据调用传入的实参,构造出OkHttp网络请求所需要的参数(如URL、request body、headers等), 265 | 进一步构造出 okhttp3.Request 对象,然后通过 OkHttpClient#newCall() 构造出 okhttp3.Call 对象。 266 | 267 | 1. (猜测)根据Service接口类方法的返回类型,对 okhttp3.Call 对象进行处理: 268 | ** 转换为 retrofit2.Call 对象,并返回; 269 | ** 转换为 RxJava 的 Observerable 对象,并返回; 270 | ** 立即在后台开始执行网络请求,并将当前Kotlin协程挂起,等网络请求返回后恢复当前协程; 271 | ** 如何支持其它返回值类型?显然 Retrofit 自身不可能支持所有返回类型。 272 | 273 | ### Service接口类方法解析 274 | 275 | 我们继续看 Retrofit 中 InvocationHandler 的实现,其最终是需要调用 276 | ServiceMethod#parseAnnotations() 方法来解析当前Service接口类方法, 277 | 并把解析的结果(ServiceMethod对象)保存在缓存中,供下次直接使用。 278 | 然后,调用该ServiceMethod对象的 invoke() 方法来最终完成本次InvocationHandler的处理。 279 | 280 | ServiceMethod 类非常简单: 281 | [source,java] 282 | ---- 283 | abstract class ServiceMethod { 284 | static ServiceMethod parseAnnotations(Retrofit retrofit, Method method) { 285 | RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method); 286 | ...省略代码 287 | return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory); 288 | } 289 | 290 | abstract @Nullable T invoke(Object[] args); 291 | } 292 | ---- 293 | 294 | 从上面的代码可以看到,ServiceMethod类把方法解析的工作委托给了 RequestFactory#parseAnnotations() 295 | 和 HttpServiceMethod#parseAnnotations(),而 #invoke() 的实现交给了子类去完成(即 HttpServiceMethod类)。 296 | 297 | ServiceMethod只有一个直接子类 HttpServiceMethod 298 | 299 | [source,java] 300 | ---- 301 | abstract class HttpServiceMethod extends ServiceMethod { 302 | ...省略代码 303 | 304 | @Override 305 | final @Nullable ReturnT invoke(Object[] args) { 306 | Call call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter); 307 | return adapt(call, args); 308 | } 309 | 310 | protected abstract @Nullable ReturnT adapt(Call call, Object[] args); 311 | 312 | ...省略代码 313 | } 314 | ---- 315 | 316 | 在上面的代码可以看到,HttpServiceMethod#invoke() 先构造出一个 OkHttpCall 对象, 317 | 然后通过 HttpServiceMethod#adapt() 抽象方法把Call对象转换为Service接口类的返回类型对象。 318 | 似乎署光就在眼前! 319 | 320 | HttpServiceMethod有3个子类: 321 | 322 | * CallAdapted:支持 retrofit.Call、Java 8的CompletableFuture、RxJava的Obserable等返回类型 323 | * SuspendForResponse:支持suspend函数,返回类型为 Response 324 | * SuspendForBody:支持suspend函数,返回类型为 T 325 | 326 | ### 总结 327 | 328 | 让我们把前面的技术点串联在一起,看看Retrofit背后的技术原理全貌: 329 | 330 | 1. 通过Java动态代理技术,创建出Service接口类对象。 331 | 1. 当该Service接口类对象的方法被调用时(假设返回值类型为**_ReturnT_**), 332 | a. 解析该方法的函数签名及注解,结合Retrofit对象中的baseUrl参数,构建出此HTTP请求的相关参数。 333 | 在构建HTTP参数时,会通过Retrofit对象中的Converter.Factory,对参数进行必要的转换(例如,把Java对象转换为JSON对象)。 334 | a. 使用前面构建出的HTTP参数,进一步构建出OkHttp的Call对象。 335 | b. 通过Retrofit对象中的CallAdapter.Factory,把OkHttp的Call对象转换为**_ReturnT_**类型的对象。 336 | c. 此时,根据是否是suspend函数或者**_ReturnT_**的具体类型,有两种结果: 337 | * HTTP网络请求立即执行(例如,Kotlin suspend函数,Java 8的CompletableFuture)。 338 | * HTTP网络请求将由返回的**_ReturnT_**对象的某个操作触发(例如,retrofit2.Call、RxJava的Observable)。 339 | 3. 当Service接口类对象的方法调用返回时,有下列情况: 340 | a. 已经拿到服务器的返回结果(如,Kotlin suspend函数)。 341 | b. HTTP网络请求已经开始执行,正在等待结果返回的通知(如,Java 8的CompletableFuture)。 342 | c. HTTP网络请求未开始,由返回对象的进一步操作来触发(如,retrofit2.Call、RxJava的Observable)。 343 | 4. 当HTTP请求结果返回时,也会通过Retrofit对象中的Converter.Factory,对返回结果进行转换 344 | (例如,把JSON字符串转换为Java对象)。 345 | 5. 通过自定义CallAdapter.Factory,并把它的对象添加到Retrofit对象中,即可支持Service接口类方法的自定义返回类型。 346 | 6. 通过自定义Converter.Factory,并把它的对象添加到Retrofit对象中,即可支持自定义类型、注解的类型转换 347 | (Java对象和String、RequestBody、ResponseBody之间的转换)。 348 | 349 | ## 设计模式 350 | 351 | ## 实现细节 352 | 353 | ### Retrofit#validateServiceInterface() 354 | 355 | 在 Retrofit#create() 方法的实现中,会先调用 Retrofit#validateServiceInterface() 356 | 去验证传入的Service接口类是否合法。 357 | 358 | 默认情况下,只会验证Service接口类本身是否合法(包括其继承的interface), 359 | 例如,是否是interface,是否有泛型(不允许泛型); 360 | 而不会验证Service接口类中的方法是否合法,此时只会在该方法第一次被调用时才进行解析并验证。 361 | 362 | 可以调用 Retrofit.Builder#validateEagerly() 来强制启用对方法的解析和验证。 363 | 但这样做,可能有性能损耗,Retrofit#create()调用耗时也会更长,需要注意一下。 364 | 365 | ### Kotlin支持 366 | 367 | Retrofit 对 Kotlin 的支持主要是指在Service接口类中支持suspend函数, 368 | 见前面『引子』部分的示例代码。 369 | 370 | 要在Service接口类中支持suspend函数,需要解决如下问题: 371 | 372 | * 识别Service接口类中的suspend函数 373 | * 支持suspend函数的调用 374 | 375 | 我们知道,suspend函数在编译后,会在函数的参数列表最后增加一个 kotlin.coroutines.Continuation 类型的参数。 376 | Retrofit就是利用这个特点来识别suspend函数的,相应代码在 377 | retrofit2.RequestFactory.Builder#parseParameter() 这个方法中: 378 | [source,java] 379 | ---- 380 | if (allowContinuation) { 381 | try { 382 | if (Utils.getRawType(parameterType) == Continuation.class) { 383 | isKotlinSuspendFunction = true; 384 | return null; 385 | } 386 | } catch (NoClassDefFoundError ignored) { 387 | } 388 | } 389 | ---- 390 | 391 | 在 retrofit2.HttpServiceMethod#parseAnnotations() 方法中, 392 | 会根据是否为suspend函数来决定创建哪个 HttpServiceMethod 子类对象: 393 | [source,java] 394 | ---- 395 | if (!isKotlinSuspendFunction) { 396 | return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter); 397 | } else if (continuationWantsResponse) { 398 | return (HttpServiceMethod) 399 | new SuspendForResponse<>( 400 | requestFactory, 401 | callFactory, 402 | responseConverter, 403 | (CallAdapter>) callAdapter); 404 | } else { 405 | return (HttpServiceMethod) 406 | new SuspendForBody<>( 407 | requestFactory, 408 | callFactory, 409 | responseConverter, 410 | (CallAdapter>) callAdapter, 411 | continuationBodyNullable); 412 | } 413 | ---- 414 | 415 | 如果当前Service接口类的方法为suspend函数,那么对其调用会最终 416 | 417 | 在 KotlinExtensions.kt 文件中,定义了多个扩展函数, 418 | 主要供前面的 SuspendForResponse 和 SuspendForBody 这两个类来调用: 419 | 420 | [source,java] 421 | ---- 422 | inline fun Retrofit.create(): T = create(T::class.java) 423 | 424 | suspend fun Call.await(): T 425 | 426 | suspend fun Call.await(): T? 427 | 428 | suspend fun Call.awaitResponse(): Response 429 | ---- 430 | 431 | TIP: 作为 Retrofit 库的使用者,我们应该只会用到 Retrofit.create() 这个内联函数。 432 | 433 | ### Callback Executor 434 | 435 | 在构建Retrofit对象时,可以为其指定Callback Executor。如果没有指定,那么: 436 | 437 | * Android平台会使用默认的 MainThreadExecutor(主线程执行回调) 438 | * 其它平台无Callback Executor 439 | 440 | Callback Executor 用于控制 retrofit2.Call#enqueue() 在回调callback参数时在哪个线程执行 441 | (参见 retrofit2.DefaultCallAdapterFactory.ExecutorCallbackCall类 的实现)。 442 | 443 | ### Retrofit添加默认AdapterFactory重复的bug 444 | 445 | ### Converter 446 | 447 | #### 内置Converters 448 | 449 | Retrofit内置的 Converter.Factory 为 retrofit2.BuiltInConverters 。 450 | 如果当前平台支持Java 8 API(如Java 8+, Android N+),还有一个额外的 OptionalConverterFactory 。 451 | 452 | retrofit2.BuiltInConverters#responseBodyConverter()支持如下类型: 453 | 454 | * okhttp3.ResponseBody(此类型支持 @retrofit2.http.Streaming) 455 | * Void 456 | * kotlin.Unit 457 | 458 | #### ScalarsConverterFactory 459 | 460 | TIP: 需要添加库依赖:com.squareup.retrofit2:converter-scalars:${versions.retrofit} 461 | 462 | ScalarsConverterFactory 的 #requestBodyConverter() 和 #responseBodyConverter() 463 | 支持Java的基础类型(如String, boolean, Boolean, long, Long等)。 464 | 465 | #### 其它Converters 466 | 467 | Retrofit官方也提供了对JSON、protobuf、xml等数据格式的支持,需要单独添加库依赖。 468 | 469 | ### CallAdapter 470 | 471 | [source, java] 472 | ---- 473 | public interface CallAdapter { 474 | Type responseType(); 475 | T adapt(Call call); 476 | } 477 | ---- 478 | 479 | CallAdapter 用于将 Call 类型对象转换为 T 类型对象(这个T本身也可以是Call, 480 | 见 DefaultCallAdapterFactory 实现的 CallAdapter)。 481 | 482 | #### 内置Adapters 483 | 484 | Retrofit内置了两个 CallAdapter.Factory : 485 | 486 | * DefaultCallAdapterFactory:仅支持 retrofit2.Call 返回类型 487 | 488 | * CompletableFutureCallAdapterFactory:有两个 CallAdapter ,分别支持如下返回类型: 489 | ** CompletableFuture 490 | ** CompletableFuture> 491 | 492 | #### 其它Adapters 493 | 494 | Retrofit官方也提供了对RxJava的、Guava、Scala等库/语言的支持,需要单独添加库依赖。 495 | 496 | ### 网络请求发起时机 497 | 498 | 在Retrofit中,按网络请求发起时机,Service接口类方法可分为两类: 499 | 500 | * Service接口类方法被调用时立即发起网络请求: 501 | ** CompletableFutureCallAdapterFactory 支持的 CompletableFuture 返回类型 502 | ** GuavaCallAdapterFactory 支持的 ListenableFuture 返回类型 503 | ** ScalaCallAdapterFactory 支持的 Future 返回类型 504 | ** kotlin suspend函数 505 | 506 | * Service接口类方法返回对象的某个方法被调用时才会发起网络请求: 507 | ** DefaultCallAdapterFactory 支持的 Call 返回类型 508 | ** RxJava3CallAdapterFactory 等RxJava系列Factory支持的 Observable等返回类型 509 | -------------------------------------------------------------------------------- /notes/knowledge/0011-android-theme-and-style.asc: -------------------------------------------------------------------------------- 1 | = Android Theme & Style 2 | // Settings 3 | ifdef::env-github[] 4 | :note-caption: :paperclip: 5 | :tip-caption: :bulb: 6 | :important-caption: :exclamation: 7 | :caution-caption: :fire: 8 | :warning-caption: :warning: 9 | endif::[] 10 | // TOC 11 | :toc: 12 | :toc-placement: preamble 13 | :toclevels: 3 14 | 15 | 本文对Android的theme(主题)和style(样式)进行介绍和总结。 16 | 17 | NOTE: 反馈与建议,请移步: 18 | https://github.com/yongce/AndroidDevNotes/issues/9 19 | 20 | 文章更新历史: 21 | 22 | * 2015/12/12 文章发布 23 | 24 | ''' 25 | 26 | :numbered: 27 | 28 | == theme & style 介绍 29 | 30 | === 使用场景 31 | 32 | 在应用开发中,theme常用于在AndroidManifest.xml中修饰 33 | application或者activity节点(如果没有指定,则会使用一个默认theme), 34 | 而style常用于在layout文件中修饰View节点。 35 | 36 | 在Android系统中,有很多系统定义的theme和style,可以直接使用。 37 | 也可以定义自己的theme和style:既可以全新定义, 38 | 也可以继承自其它theme和style(包括自定义的和系统定义的)。 39 | 40 | 例如,在AndroidManifest.xml中使用theme,通过“android:theme”属性设置: 41 | [source,xml] 42 | ---- 43 | 47 | 51 | 52 | ---- 53 | 54 | 又如,在layout XML文件中使用style,通过“style”属性设置: 55 | [source,xml] 56 | ---- 57 | 60 | ---- 61 | 62 | === 如何定义 63 | 64 | theme和style都是在res/values/目录中的xml资源文件中定义的(文件名随意), 65 | 其资源类型都为“style”。因此从语法层面,theme和style在定义时没任何区别。 66 | 67 | style类型定义语法如下: 68 | [source,xml] 69 | ---- 70 | 71 | 72 | 80 | 81 | ---- 82 | 83 | 从style类型定义的语法可以看到,*style类型就是一个属性集合*。 84 | 85 | 示例代码: 86 | 87 | [source,xml] 88 | ---- 89 | 95 | ---- 96 | 97 | 为了便于表述,在后面讲theme和style,在不产生歧义的情况下, 98 | 会用style或者style类型泛指这两者。 99 | 100 | === 继承 101 | 102 | style类型允许有继承关系,继承者自动拥有被继承style所拥有的属性。 103 | style继承只允许继承自一个style,不支持多继承。 104 | 105 | 为了便于表述,这里把被继承的style称为parent style,新style称为child style。 106 | 107 | 在style继承时,child style自动继承parent style的所有属性。 108 | 同时,在child style中也可以重写parent style中的属性,还可以添加新的属性。 109 | 110 | 在style继承时,有两种方式可以指定其parent style。 111 | 112 | ==== 显示继承 113 | 114 | 第一种方式,通过“parent”属性显示指定parent style。示例代码如下: 115 | 116 | [source,xml] 117 | ---- 118 | 124 | ---- 125 | 126 | 在上面的示例代码中,定义了一个新的style “MyWidget.XxxText”, 127 | 并继承自系统style “@android:style/TextAppearance”。 128 | 129 | 在style继承时,由于"parent"属性的值只能是style类型, 130 | 因此可以把"@style/Xxxx”简化写成"Xxxx"。示例代码如下: 131 | [source,xml] 132 | ---- 133 | 136 | ---- 137 | 138 | ==== 隐式继承 139 | 140 | 第二种方式,通过“.”的前缀命名来隐式指定。示例代码如何: 141 | 142 | [source,xml] 143 | ---- 144 | 147 | ---- 148 | 149 | 在上面的示例代码中,定义了一个新的style “MyWidget.XxxText.Big”, 150 | 其继承自style “MyWidget.XxxText”。 151 | 152 | 由于使用了名称前缀的指定方式,在应用开发中使用隐式继承时, 153 | 只能用于继承其它自定义style。 154 | 155 | === 引用属性 156 | 157 | 大多数时候,我们在为属性指定属性值时,要么是直接给值,要么是引用一个资源。 158 | 而在引用资源时,通常是通过“@”来直接引用的(硬编码方式)。 159 | 还有另外一种资源引用方式,通过“?”动态引用当前已加载theme中的属性的值。 160 | 161 | 例如,在定义style类型时(*可以是styles也可以是themes*), 162 | 动态引用当前已加载theme中的属性的值: 163 | [source,xml] 164 | ---- 165 | 170 | ---- 171 | 172 | 又如,在使用style时,动态引用当前已加载主题(theme)中的属性的值: 173 | [source,xml] 174 | ---- 175 |