├── .DS_Store ├── Cronet编译与使用 ├── .DS_Store ├── 1.png ├── 2.png ├── 3.png ├── QUIC.png ├── QUIC_over_http.png ├── README.md ├── compaire.png ├── http2.png ├── 构建jar.png └── 构建so.png ├── HTTP版本变迁以及HTTP-QUIC ├── .DS_Store ├── OSI.png ├── README.md ├── device.png ├── http_code.png ├── https.png ├── protocol.png ├── relation.png ├── tcp_ip.png └── 构建APK.png ├── IoT ├── IOT.png ├── README.md ├── car1.jpg ├── car2.jpg ├── car3.jpg ├── city1.jpg ├── city2.jpg ├── city3.jpg ├── health1.jpg ├── health2.jpg ├── home1.jpg ├── home2.png ├── xiaomi.jpg ├── 物联网-体系.xmind └── 移动天镜目前还缺的功能点和资源.png ├── andorid 插件化和热修复 └── README.md ├── android M 运行时权限 ├── 650671-3297bcf7a0e7f34b.png ├── README.md ├── device-2016-07-24-133244.png ├── device-2016-07-24-134935.png ├── device-2016-07-24-150814.png └── device-2016-08-15-102724.png ├── android maven搭建maven私服及其应用 ├── 1471504237.jpeg └── README.md ├── flutter 工程化实践 ├── README.md ├── aar.jpg ├── aar2.jpg ├── apk.jpg ├── arm.jpg ├── arm2.jpg ├── arm_sh.jpg ├── assets.jpg ├── dependencies.jpg ├── flutter_build.jpg ├── flutter_gradle.jpg ├── icu.jpg ├── pom.jpg ├── pub.jpg ├── pub_source.jpg ├── so.jpg └── vm.jpg ├── gralde 插件 ├── README.md └── resource │ ├── project.jpg │ ├── test1.jpg │ ├── test2.jpg │ └── upload.jpg ├── kotlin中的协程 ├── .DS_Store ├── 1.jpg ├── 10.png ├── 11.png ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.png ├── 9.png ├── README.md ├── 协程.gliffy └── 协程.png ├── 产品心里学 ├── README.md └── 产品心理学.png ├── 使用RemoteView自定义notification ├── 1.png ├── 2.png ├── 3.jpg ├── 4.png ├── 5.jpg ├── 6.JPG ├── 7.JPG ├── README.md └── image-2018-10-16-17-56-00-532.png ├── 基于Gradle Transform 和 ASM 实现Android应用的AOP编程 ├── README.md └── resources │ ├── 1.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ ├── 9.jpg │ └── build.png └── 数据收集埋点以及android无埋点方式统计原生和H5点击事件 ├── 1-2.png ├── 2.jpg ├── 3.png ├── 4.png └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/.DS_Store -------------------------------------------------------------------------------- /Cronet编译与使用/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/.DS_Store -------------------------------------------------------------------------------- /Cronet编译与使用/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/1.png -------------------------------------------------------------------------------- /Cronet编译与使用/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/2.png -------------------------------------------------------------------------------- /Cronet编译与使用/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/3.png -------------------------------------------------------------------------------- /Cronet编译与使用/QUIC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/QUIC.png -------------------------------------------------------------------------------- /Cronet编译与使用/QUIC_over_http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/QUIC_over_http.png -------------------------------------------------------------------------------- /Cronet编译与使用/README.md: -------------------------------------------------------------------------------- 1 | # 编译Cronet及其在Android中的使用 2 | 3 | 之前在看HTTP3.0-QUIK协议,了解到它的强大后迫不及待的想体验一下QUIC协议在Android上的应用,一番搜索之后发现Cronet库支持QUIC协议,所以打算使用它来进行QUIC协议的使用。 4 | 5 | ### Cronet介绍 6 | Cronet是作为库提供给Android应用程序的**Chromium**网络堆栈, Cronet利用多种技术来减少延迟并提高应用程序需要工作的网络请求的吞吐量。 7 | 8 | Cronet Library每天处理数百万人使用的应用程序请求,例如YouTube,Google App,Google Photos和Maps - Navigation&Transit。 9 | 10 | 11 | Cronet具有以下特点: 12 | 13 | 1. Protocol support 14 | 15 | Cronet本身支持HTTP,HTTP / 2和QUIC协议。 16 | 2. Request prioritization 17 | 18 | 该库允许您为请求设置优先级标记。服务器可以使用优先级标记来确定处理请求的顺序。 19 | 3. Resource caching 20 | 21 | Cronet可以使用内存或磁盘缓存来存储在网络请求中检索到的资源。后续请求将自动从缓存中提供。 22 | 4. Asynchronous requests 23 | 24 | 默认情况下,使用Cronet Library发出的网络请求是异步的。在等待请求返回时,不会阻止您的工作线程。 25 | 26 | 5. Data compression 27 | 28 | Cronet使用Brotli压缩数据格式支持数据压缩。 29 | 30 | ### Cronet编译 31 | 网上有一些别人编好的现成的CRONET库[CRONET库](https://github.com/lizhangqu/cronet),在MAVEN上也能找到相关的库[maven](https://mvnrepository.com/artifact/org.chromium.net),android 官方也提供了直接使用Cronet的方式。 32 | ``` 33 | dependencies { 34 | implementation 'com.google.android.gms:play-services-cronet:16.0.0' 35 | } 36 | ``` 37 | 但是为了体验一下整个编译过程,而且后期后可能会对源码做出修改和剪裁,所以打算手动编译一遍。 38 | 39 | 首先需要安装了ubantu的linux机器(因为手上只有mac,所以用之前搭的谷歌云,结果由于对硬件要求比较高,变成了收费模式,瞬间把赠送的金额用完了,心疼😭),机器要求如下: 40 | 41 | * A **64-bit** Intel machine with at least **8GB** of RAM. More than 16GB is highly recommended. 42 | 43 | * At least **100GB** of free disk space. 44 | 45 | * You must have **Git** and **Python v2** installed already. 46 | 47 | * Most development is done on **Ubuntu** (currently **16.04**, Xenial Xerus). 48 | 49 | #### 安装设置**depot_tools** 50 | 51 | ```shell 52 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git 53 | ``` 54 | 55 | 设置环境变量 56 | 57 | ```shell 58 | export PATH="$PATH:depot_tools的path" 59 | ``` 60 | 61 | 62 | #### **拉取代码** 63 | 64 | ```shell 65 | mkdir ~/chromium && cd ~/chromium 66 | ``` 67 | ```shell 68 | fetch --nohooks android 69 | ``` 70 | 这里可能要花很长时间因为包很大的(还好我用的是谷歌云海外机器,网速峰值能达到60M 均值也在30M左右,羡慕)。 71 | 72 | 73 | #### 依赖下载 74 | 75 | 76 | 当下载结束后会产生一个隐藏文件 .gclient 和 一个src的文件夹, 77 | 78 | ```shell 79 | cd src 80 | ``` 81 | 82 | 然后设置目标平台为Android 83 | 84 | ``` 85 | echo "target_os = [ 'android' ]" >> ../.gclient 86 | ``` 87 | 88 | 然后下载Android相关依赖 89 | 90 | ``` 91 | gclient sync 92 | ``` 93 | 当代码同步结束后 94 | 95 | ```shell 96 | build/install-build-deps-android.sh 97 | ``` 98 | 99 | 当install-build-deps至少执行一次成功之后,执行 100 | 101 | ```shell 102 | gclient runhooks 103 | ``` 104 | 105 | #### 编译 106 | 107 | 当以上所有执行成功之后我们开始真正的编译工作。 108 | 109 | 由于整个Chromium的代码C++编写,同时是使用ninja作为编译工具 110 | 111 | 我们首先使用 `gn` 生成ninja 文件 112 | 113 | ```shell 114 | ./components/cronet/tools/cr_cronet.py gn --out_dir=out/Cronet 115 | ``` 116 | 117 | 这样生成一个`out/Cronet`文件夹,里面包含gn所需要的文件,如果不指定 `--out_dir=out/Cronet`,则默认生成`out/Debug` 文件夹;若果添加`--x86`选项则会生成X86架构的库,输出文件夹在`out/Debug-x86` 118 | 119 | 以上默认是debug模式的库,如果要生成release的库则应该如下: 120 | 121 | ```shell 122 | ./components/cronet/tools/cr_cronet.py gn --release 123 | ``` 124 | 125 | 会生成`out/Release`文件夹 126 | 127 | 然后使用ninja进行编译 128 | 129 | ```shell 130 | ninja -C out/Cronet cronet_package 131 | ``` 132 | 其中out/Cronet 是生成的相应的问题夹,比如release模式下则为ninja -C out/Release cronet_package 133 | 134 | #### 编译 135 | 136 | 编译成功后会产生相应的so和jar文件 137 | ![MacDown logo](./构建so.png) 138 | ![MacDown logo](./构建jar.png) 139 | 140 | 141 | 我们主要使用的是**libcronet.xxx.xx.so**和**cronet\_api.jar**,**cronet\_imple\_common\_base\_java.jar**,**cronet\_imple\_native\_base\_java.jar** 142 | 143 | 144 | ### Cronet使用 145 | 146 | 147 | #### 1. 创建和配置CronetEngine的实例 148 | 149 | ```kotlin 150 | val myBuilder = CronetEngine.Builder(context) 151 | val cronetEngine: CronetEngine = myBuilder.build() 152 | ``` 153 | 154 | #### 2. 实现请求回调 155 | 156 | ```kotlin 157 | class MyUrlRequestCallback : UrlRequest.Callback() { 158 | 159 | override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) { 160 | val httpStatusCode = info?.httpStatusCode 161 | if (httpStatusCode == 200) { 162 | // The request was fulfilled. Start reading the response. 163 | request?.read(myBuffer) 164 | } else if (httpStatusCode == 503) { 165 | // The service is unavailable. You should still check if the request 166 | // contains some data. 167 | request?.read(myBuffer) 168 | } 169 | responseHeaders = info?.allHeaders 170 | } 171 | 172 | override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, byteBuffer: ByteBuffer?) { 173 | // The response body is available, process byteBuffer. 174 | ... 175 | 176 | // Continue reading the response body by reusing the same buffer 177 | // until the response has been completed. 178 | byteBuffer?.clear() 179 | request?.read(myBuffer) 180 | } 181 | 182 | override fun onFailed(p0: UrlRequest?, p1: UrlResponseInfo?, p2: CronetException?) { 183 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 184 | } 185 | 186 | override fun onSucceeded(p0: UrlRequest?, p1: UrlResponseInfo?) { 187 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 188 | } 189 | 190 | override fun onRedirectReceived(request: UrlRequest?, info: UrlResponseInfo?, newLocationUrl: String?) { 191 | // Determine whether you want to follow the redirect. 192 | ... 193 | 194 | if (shouldFollow) { 195 | request?.followRedirect() 196 | } else { 197 | request?.cancel() 198 | } 199 | } 200 | 201 | ``` 202 | 203 | #### 3. 创建请求 204 | 205 | ```kotlin 206 | val executor: Executor = Executors.newSingleThreadExecutor() 207 | 208 | val requestBuilder = cronetEngine.newUrlRequestBuilder( 209 | "https://www.example.com", 210 | MyUrlRequestCallback(), 211 | executor 212 | ) 213 | 214 | val request: UrlRequest = requestBuilder.build() 215 | 216 | request.start() 217 | 218 | ``` 219 | 220 | ### 结合OKHTTP使用 221 | 222 | 我们可以拦截OkHTTP的请求然后将请求交给Cronet来执行请求 223 | 224 | ```kotlin 225 | class QUICInterceptor: Interceptor { 226 | if (!QUICDroid.enable) { 227 | return chain.proceed(chain.request()) 228 | } 229 | 230 | val req = chain.request() 231 | 232 | val url = req.url().url() 233 | 234 | // covert okhttp request to cornet request 235 | val connection = QUICDroid.engine.openConnection(url) as HttpURLConnection 236 | ...... 237 | } 238 | ``` 239 | 240 | ###QUIC协议数据对比 241 | 242 | 为了验证QUIC协议比HTTP2.0性能更好,我分别使用**Cronet开启QUIC协议** VS **使用http2.0** 来下载同样的16张图片,由于QUIC协议必须服务端也支持,所以目前使用的图片是同时提供QUIC和HTTP2.0支持的腾讯云CDN图片进行下载测试。 243 | 244 | ```kotlin 245 | private val IMGS = arrayOf( 246 | "https://stgwhttp2.kof.qq.com/1.jpg", 247 | "https://stgwhttp2.kof.qq.com/2.jpg", 248 | "https://stgwhttp2.kof.qq.com/3.jpg", 249 | "https://stgwhttp2.kof.qq.com/4.jpg", 250 | "https://stgwhttp2.kof.qq.com/5.jpg", 251 | "https://stgwhttp2.kof.qq.com/6.jpg", 252 | "https://stgwhttp2.kof.qq.com/7.jpg", 253 | "https://stgwhttp2.kof.qq.com/8.jpg", 254 | "https://stgwhttp2.kof.qq.com/01.jpg", 255 | "https://stgwhttp2.kof.qq.com/02.jpg", 256 | "https://stgwhttp2.kof.qq.com/03.jpg", 257 | "https://stgwhttp2.kof.qq.com/04.jpg", 258 | "https://stgwhttp2.kof.qq.com/05.jpg", 259 | "https://stgwhttp2.kof.qq.com/06.jpg", 260 | "https://stgwhttp2.kof.qq.com/07.jpg", 261 | "https://stgwhttp2.kof.qq.com/08.jpg" 262 | ) 263 | ``` 264 | 使用OKhttp进行http2.0下载测试: 265 | 266 | ![MacDown logo](./http2.png) 267 | 268 | 269 | 使用QUIC进行下载测试: 270 | 271 | 首先我们要开启QUIC协议 272 | 273 | ```kotlin 274 | CronetEngine.Builder(applicationContext) 275 | .enableQuic(true) 276 | .build() 277 | ``` 278 | 279 | ![MacDown logo](./QUIC.png) 280 | 281 | 为了避免OKHttp本身优化的问题,我们为QUIC提供了hook OKHttp用的Interceptor,此数数据均为QUIC over OKHttp的测试结果 282 | 283 | ![MacDown logo](./QUIC_over_http.png) 284 | 285 | 最终对比数据为: 286 | 287 | ![MacDown logo](./compaire.png) 288 | 289 | 可以看到不管是HTTP2.0还是QUIC在首次建立连接耗时都比较长,之后经过keep-alive,多路复用之后,后面的图片下载速度有所提高;总体上采用QUIC协议会比HTTP2.0性能更好。 290 | 291 | 292 | 以上测试均为在联通4G模式下的测试,网络状况比较好,对于弱网和高延迟的网络没有做实际测试,从网上找了一份测试数据如下: 293 | 294 | BatWifi-Guest (公共Wifi) 295 | 296 | 此为高延时的网络情况 297 | 298 | (Download 5.7Mbps; Upload 2.8Mbps; Ping 466ms (delay 200ms)) 299 | 300 | ![MacDown logo](./1.png) 301 | 302 | 303 | Dtac@地铁 304 | 305 | 较为正常的4G网络 306 | 307 | (Download 2.3Mbps; Upload 4.7Mbps; Ping 68ms) 308 | 309 | ![MacDown logo](./2.png) 310 | 311 | 312 | True@地铁 313 | 314 | 延时稍高的4G网络 315 | 316 | (Download 6.3Mbps; Upload 9.7Mbps; Ping 123ms) 317 | 318 | ![MacDown logo](./3.png) 319 | 320 | 321 | 当丢包率达到20%以后,HTTP2.0基本处于超时状态,无法完成测试。 322 | 323 | 从数据中可以看到,QUIC和HTTP2.0均会明显受到丢包率和延时影响,但两者对HTTP的影响程度远大于QUIC。QUIC总体性能优于HTTP,且在高丢包高延时下都能有不错的表现,而HTTP则在15%以上丢包情况下基本处于不可用状态。 324 | 325 | 从测试结果来看,QUIC拥有比HTTP2.0更好的网络性能,特别是在弱网(高延时、高丢包)下。但因为其基于UDP,因此在不同ISP及不同时段下表现并不稳定,比如在夜间网络高峰期,中国电信对UDP有限制,QUIC表现不如HTTP,而联通则相差不大,需要在不同国家做更多对比测试,线上也需要有实时监控与动态切换策略。 326 | -------------------------------------------------------------------------------- /Cronet编译与使用/compaire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/compaire.png -------------------------------------------------------------------------------- /Cronet编译与使用/http2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/http2.png -------------------------------------------------------------------------------- /Cronet编译与使用/构建jar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/构建jar.png -------------------------------------------------------------------------------- /Cronet编译与使用/构建so.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/Cronet编译与使用/构建so.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/.DS_Store -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/OSI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/OSI.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/README.md: -------------------------------------------------------------------------------- 1 | # HTTP版本变迁 2 | 3 | # OSI模型 4 | 了解HTTP之前,我们先回忆一下网络OSI模型和TCP模型 5 | 6 | OSI: 7 | 8 | ![MacDown logo](./osi.png) 9 | 10 | TCP/IP: 11 | 12 | 13 | ![MacDown logo](./tcp_ip.png) 14 | 15 | 对应关系: 16 | ![MacDown logo](./relation.png) 17 | 18 | ![MacDown logo](./device.png) 19 | 20 | 对应的协议: 21 | ![MacDown logo](./protocol.png) 22 | 23 | HTTP(超文本传输协议,HyperText Transfer Protocol)是建立在TCP协议之上的一种应用层网络协议。默认使用80端口,建立之初目的是为了将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器。 **互联网通信发展史其实是人类与RTT-Round Trip Time斗争的历史。** 24 | 25 | HTTP版本对比: 26 | 27 | 功能 |HTTP 1.0 | HTTP 1.1 |SPDY:HTTP 1.x(SPDY位于HTTP之下,TCP和SSL之上)| HTTP 2.0 | HTTP 3.0 QUIC(Quick UDP Internet Connections) 28 | ----------|------------- | ------------- |-----------|--------|--------- 29 | **缓存处理** | If-Modified-Since,Expires | 新增 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 30 | **范围请求**||引入range头域,支持断点续传||| 31 | **错误通知的管理** | | 新增24个状态码| | 32 | **Host头处理** |每个服务器绑定一个ip|支持host头域:可以将请求发往同一台服务器上的不同网站(虚拟机的兴起)|| 33 | **长连接**|每次创建tcp|默认开启keep-alive;|| 34 | **多路复用**|一次请求-响应,建立一个连接,用完关闭;每一个请求都要建立一个连接;|引入pipelining:同一个TCP连接中,客户端可以同时发送多个请求。(但是请求按顺序的)|多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。|多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行|对线头阻塞(HOL)问题的解决更为彻底。基于TCP的HTTP/2,尽管从逻辑上来说,不同的流之间相互独立,不会相互影响,但在实际传输方面,数据还是要一帧一帧的发送和接收,一旦某一个流的数据有丢包,则同样会阻塞在它之后传输的其它与它毫不相干的流的数据的传输。而基于UDP的QUIC协议则可以更为彻底地解决这样的问题,让不同的流之间真正的实现相互独立传输,互不干扰。 35 | **header压缩**|||压缩头减少数据;采用DEFLATE算法|采用HPACK算法 36 | **请求优先级**|||SPDY允许给每个request设置优先级,确保资源优先加载|支持 37 | **服务端推送**|||服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤 | 支持 38 | **HTTPS** |||基于HTTPS的加密协议传输,大大提高了传输数据的可靠性 (强制使用)|支持明文 HTTP 传输 也支持加密传输 39 | **二进制**|基于文本解析|基于文本解析|基于文本解析|基于二进制解析,更健壮 40 | **底层通讯协议**|TCP| TCP |SSL| TCP/SSL|UDP 41 | **连接迁移**|基于TCP的协议,由于切换网络之后,IP会改变,因而之前的连接不可能继续保持|同前|同前|同前|基于UDP的QUIC协议,则可以内建与TCP中不同的连接标识方法,从而在网络完成切换之后,恢复之前与服务器的连接。 42 | 43 | 44 | # HTTP3.0-over-QUIC 45 | 46 | Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势: 47 | 48 | 1. 减少了 TCP 三次握手及 TLS 握手时间。 49 | 2. 改进的拥塞控制。 50 | 3. 避免队头阻塞的多路复用。 51 | 4. 连接迁移。 52 | 5. 前向冗余纠错。 53 | 54 | ### 为什么需要QUIC? 55 | * **中间设备的僵化** 56 | 57 | 可能是 TCP 协议使用得太久,也非常可靠。所以我们很多中间设备,包括防火墙、NAT 网关,整流器等出现了一些约定俗成的动作。 58 | 59 | 比如有些防火墙只允许通过 80 和 443,不放通其他端口。NAT 网关在转换网络地址时重写传输层的头部,有可能导致双方无法使用新的传输格式。整流器和中间代理有时候出于安全的需要,会删除一些它们不认识的选项字段。 60 | 61 | TCP 协议本来是支持端口、选项及特性的增加和修改。但是由于 TCP 协议和知名端口及选项使用的历史太悠久,中间设备已经依赖于这些潜规则,所以对这些内容的修改很容易遭到中间环节的干扰而失败。 62 | * **依赖于操作系统的实现导致协议僵化** 63 | 64 | TCP 是由操作系统在内核西方栈层面实现的,应用程序只能使用,不能直接修改。虽然应用程序的更新迭代非常快速和简单。但是 TCP 的迭代却非常缓慢,原因就是操作系统升级很麻烦。 65 | 66 | 现在移动终端更加流行,但是移动端部分用户的操作系统升级依然可能滞后数年时间。PC 端的系统升级滞后得更加严重,windows xp 现在还有大量用户在使用,尽管它已经存在快 20 年。 67 | 68 | 服务端系统不依赖用户升级,但是由于操作系统升级涉及到底层软件和运行库的更新,所以也比较保守和缓慢。 69 | 70 | 这也就意味着即使 TCP 有比较好的特性更新,也很难快速推广。比如 TCP Fast Open。它虽然 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它。 71 | 72 | * **建立连接的握手延迟大** 73 | 74 | 不管是 HTTP1.0/1.1 还是 HTTPS,HTTP2,都使用了 TCP 进行传输。HTTPS 和 HTTP2 还需要使用 TLS 协议来进行安全传输。这就出现了两个握手延迟: 75 | 76 | 1. TCP 三次握手导致的 TCP 连接建立的延迟。 77 | 78 | 2. TLS 完全握手需要至少 2 个 RTT 才能建立,简化握手需要 1 个 RTT 的握手延迟。 79 | 80 | 对于很多短连接场景,这样的握手延迟影响很大,且无法消除。 81 | 82 | * **队头阻塞** 83 | 84 | 队头阻塞主要是 TCP 协议的可靠性机制引入的。TCP 使用序列号来标识数据的顺序,数据必须按照顺序处理,如果前面的数据丢失,后面的数据就算到达了也不会通知应用层来处理。 85 | 86 | 另外 TLS 协议层面也有一个队头阻塞,因为 TLS 协议都是按照 record 来处理数据的,如果一个 record 中丢失了数据,也会导致整个 record 无法正确处理。 87 | 88 | 所以 QUIC 协议选择了 UDP,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟,同时在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。 89 | 90 | ### QUIC的改进 91 | * **连接建立延时低** 92 | 93 | 0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。那什么是 0RTT 建连呢?这里面有两层含义: 94 | 95 | 1. 传输层 0RTT 就能建立连接。 96 | 2. 加密层 0RTT 就能建立加密连接。 97 | 98 | ![MacDown logo](./rtt.jpg) 99 | 100 | 101 | 102 | * **改进的拥塞控制** 103 | 104 | TCP 的拥塞控制实际上包含了四个算法:`慢启动`,`拥塞避免`,`快速重传`,`快速恢复`。 105 | QUIC 协议当前默认使用了 TCP 协议的 Cubic 拥塞控制算法 [6],同时也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等拥塞控制算法。 106 | 107 | * 可插拔 108 | * 单调递增的 Packet Number 109 | 110 | TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。
111 | 112 | QUIC 同样是一个可靠的协议,它使用 Packet Number 代替了 TCP 的 sequence number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值。而 TCP 呢,重传 segment 的 sequence number 和原始的 segment 的 Sequence Number 保持不变,也正是由于这个特性,引入了 Tcp 重传的歧义问题。 113 | 114 | * 不允许 Reneging 115 | 116 | 什么叫 Reneging 呢?就是接收方丢弃已经接收并且上报给 SACK 选项的内容 [8]。TCP 协议不鼓励这种行为,但是协议层面允许这样的行为。主要是考虑到服务器资源有限,比如 Buffer 溢出,内存不够等情况。 117 | Reneging 对数据重传会产生很大的干扰。因为 Sack 都已经表明接收到了,但是接收端事实上丢弃了该数据。 118 | QUIC 在协议层面禁止 Reneging,一个 Packet 只要被 Ack,就认为它一定被正确接收,减少了这种干扰。 119 | * 更多的 Ack 块 120 | * Ack Delay 时间 121 | * **基于 stream 和 connecton 级别的流量控制** 122 | 123 | QUIC 的流量控制 [22] 类似 HTTP2,即在 Connection 和 Stream 级别提供了两种流量控制。为什么需要两类流量控制呢?主要是因为 QUIC 支持多路复用。 124 | 125 | 1. Stream 可以认为就是一条 HTTP 请求。 126 | 2. Connection 可以类比一条 TCP 连接。多路复用意味着在一条 Connetion 上会同时存在多条 Stream。既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。 127 | 128 | QUIC 实现流量控制的原理比较简单: 129 | 130 | 通过 window_update 帧告诉对端自己可以接收的字节数,这样发送方就不会发送超过这个数量的数据。 131 | 132 | 通过 BlockFrame 告诉对端由于流量控制被阻塞了,无法发送数据。 133 | 134 | * **没有队头阻塞的多路复用** 135 | 136 | 1. QUIC 最基本的传输单元是 Packet,不会超过 MTU 的大小,整个加密和认证过程都是基于 Packet 的,不会跨越多个 Packet。这样就能避免 TLS 协议存在的队头阻塞。 137 | 2. Stream 之间相互独立,比如 Stream2 丢了一个 Pakcet,不会影响 Stream3 和 Stream4。不存在 TCP 队头阻塞。 138 | 139 | 当然,并不是所有的 QUIC 数据都不会受到队头阻塞的影响,比如 QUIC 当前也是使用 Hpack 压缩算法 [10],由于算法的限制,丢失一个头部数据时,可能遇到队头阻塞。 140 | 141 | * **加密认证的报文** 142 | 143 | TCP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击。 144 | 145 | 但是 QUIC 的 packet 可以说是武装到了牙齿。除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。 146 | 147 | * **连接迁移** 148 | 149 | 一条 TCP 连接 [17] 是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。 150 | 151 | 比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。 152 | 153 | 又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。 154 | 155 | 156 | 那 QUIC 是如何做到连接迁移呢?很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。 157 | 158 | 由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。 159 | 160 | * **证书压缩** 161 | * **前向冗余纠错** 162 | 163 | QUIC协议的每个数据包除了本身的数据以外,会带有其他数据包的部分数据,在少量丢包的情况下,可以使用其他数据包的冗余数据完成数据组装而无需重传,从而提高数据的传输速度。具体实现类似于RAID5,将N个包的校验和(异或)建立一个单独的数据包发送,这样如果在这N个包中丢了一个包可以直接恢复出来。 164 | 165 | ### QUIC现状 166 | 167 | 目前QUIC还没有正式定稿,但是目前已经有不少公司使用了HTTP QUIC,大多还没有全线使用,只是局部使用,并且还会向下兼容http2.0;同时在运营商层面有部分会限制UDP的贷款从而导致QUIC表现不是特别满意。 168 | 169 | [Uber 使用 QUIC(HTTP/3 协议)协议来提升其应用性能](https://juejin.im/entry/5ce3d11b6fb9a07ecd3d3117) 170 | 171 | [QUIC 在微博中的落地思考](https://www.infoq.cn/article/2018/03/weibo-quic) 172 | 173 | [QQ 空间已在生产环境中使用 QUIC 协议](https://www.infoq.cn/article/2017/10/qzone-quic-practise) 174 | 175 | [使用 LiteSpeed 轻松为网站开启 HTTP/3 实践](https://www.mf8.biz/use-http-3/) 176 | 177 | [Web服务器快速启用QUIC协议](https://my.oschina.net/u/347901/blog/1647385) 178 | 179 | 180 | # HTTP与HTTPS 181 | 182 | 功能 |HTTP | HTTPS | 183 | ----------|------------- | ------------- 184 | 概念 |是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少 | 是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL | 185 | 作用|数据传输|一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。| 186 | 协议|基于tcp|基于TSL在tcp之上| 187 | 报文|明文|密文| 188 | 证书|不需要|CA申请或者自颁发(自己颁发的证书需要客户端验证通过,才可以继续访问)| 189 | 端口|80|443| 190 | 加密方式|无|握手非对称,报文对称| 191 | 192 | ### HTTPS通信过程 193 | 194 | #### 概念 195 | 196 | ##### 对称加密 197 | 198 | 即通信的双方都使用同一个秘钥进行加解密 199 | 200 | ##### 非对称加密 201 | 202 | 私钥 + 公钥= 密钥对 203 | 即用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密 204 | 其实这个很容易理解,因为通信双方的手里都有一套自己的密钥对,通信之前双方会先把自己的公钥都先发给对方 205 | 然后对方再拿着这个公钥来加密数据响应给对方,等到到了对方那里,对方再用自己的私钥进行解密,就这么简单 206 | 207 | ##### 证书 208 | 假设,此时在客户端和服务器之间存在一个中间人,这个中间人只需要把原本双方通信互发的公钥,换成自己的公钥,这样中间人就可以轻松解密通信双方所发送的所有数据,为解决上述中间人问题,于是后来就出现了证书,简单来讲就是找了一个大家公认的中介,来证明我就是我,你就是你的问题,防止中间被篡改。 209 | 210 | 证书中就包括个人的基本信息和最重要的公钥 211 | 212 | ##### 数字签名 213 | 214 | 乍一眼看去,上面的方案貌似还不错,但证书在传输的过程中如果被篡改了呢,所以后来就又出现了数字签名 215 | 216 | 简单来讲,就是将公钥和个人信息(证书)用一个Hash算法生成一个消息摘要 217 | 这个Hash算法有个极好的特性,只要输入数据有一点点变化,那生成的消息摘要就会有巨变,能有效防止别人篡改数据 218 | 219 | ##### CA 220 | 221 | 但这还是有个问题,如果中间人直接把整个原始信息依然可以伪造消息摘要 222 | 所以就出现了CA 223 | 224 | 这时CA再用它的私钥对消息摘要加密,形成签名,并把原始信息和数据签名进行合并,即所谓的`数字证书` 225 | 226 | 这样,当别人把他的证书发过来的时候,我再用同样的Hash算法,再次生成消息摘要 227 | 然后用CA的公钥对数字签名解密,得到CA创建的消息摘要,两者一比,就知道中间有没有被人篡改了 228 | 229 | 230 | 231 | #### 过程 232 | 233 | 234 | ![MacDown logo](./https.png) 235 | 236 | 237 | 238 | -> 客户端向服务端发送请求 239 | -> 服务端返回数字证书 240 | -> 客户端用自己的CA[主流的CA机构证书一般都内置在各个主流浏览器中]公钥去解密证书,如果证书有问题会提示风险 241 | -> 如果证书没问题客户端会生成一个对称加密的随机秘钥然后再和刚刚解密的服务器端的公钥对数据进行加密,然后发送给服务器端 242 | -> 服务器端收到以后会用自己的私钥对客户端发来的对称秘钥进行解密 243 | -> 之后双方就拿着这个对称加密秘钥来进行正常的通信 244 | 245 | 246 | #### 在Android的应用 247 | 248 | 一个典型的例子 249 | 250 | ``` 251 | URL url = new URL("https://google.com"); 252 | HttpsURLConnection urlConnection = url.openConnection(); 253 | InputStream in = urlConnection.getInputStream(); 254 | ``` 255 | 256 | 此时使用的是默认的`SSLSocketFactory`,与下段代码使用的`SSLContext`是一致的 257 | 258 | ``` 259 | private synchronized SSLSocketFactory getDefaultSSLSocketFactory() { 260 | try { 261 | SSLContext sslContext = SSLContext.getInstance("TLS"); 262 | sslContext.init(null, null, null); 263 | return defaultSslSocketFactory = sslContext.getSocketFactory(); 264 | } catch (GeneralSecurityException e) { 265 | throw new AssertionError(); // The system has no TLS. Just give up. 266 | } 267 | } 268 | ``` 269 | 270 | 默认的 SSLSocketFactory 校验服务器的证书时,会信任设备内置的100多个根证书。 271 | 272 | 273 | SSL 握手开始后,会校验服务器的证书,那么其实就是通过 `X509ExtendedTrustManager` 做校验的,更一般性的说是 `X509TrustManager` 274 | 275 | ``` 276 | /** 277 | * The trust manager for X509 certificates to be used to perform authentication 278 | * for secure sockets. 279 | */ 280 | public interface X509TrustManager extends TrustManager { 281 | 282 | public void checkClientTrusted(X509Certificate[] chain, String authType) 283 | throws CertificateException; 284 | 285 | public void checkServerTrusted(X509Certificate[] chain, String authType) 286 | throws CertificateException; 287 | 288 | public X509Certificate[] getAcceptedIssuers(); 289 | } 290 | ``` 291 | 那么最后校验服务器证书的过程会落到 checkServerTrusted 这个函数. 292 | 293 | **自定义信任策略** 294 | 295 | ``` 296 | // 取到证书的输入流 297 | InputStream is = new FileInputStream("anchor.crt"); 298 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 299 | Certificate ca = cf.generateCertificate(is); 300 | 301 | // 创建 Keystore 包含我们的证书 302 | String keyStoreType = KeyStore.getDefaultType(); 303 | KeyStore keyStore = KeyStore.getInstance(keyStoreType); 304 | keyStore.load(null); 305 | keyStore.setCertificateEntry("anchor", ca); 306 | 307 | // 创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点 308 | String algorithm = TrustManagerFactory.getDefaultAlgorithm(); 309 | TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); 310 | trustManagerFactory.init(keyStore); 311 | TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); 312 | 313 | 314 | //创建一个KeyManager 315 | KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509"); 316 | kmf.init(keyStore, clientCertPassword.toCharArray()); 317 | KeyManager[] keyManagers = kmf.getKeyManagers(); 318 | 319 | // 用 TrustManager,KeyManager 初始化一个 SSLContext 320 | SSLContext sslContext = SSLContext.getInstance("TLS"); 321 | sslContext.init(keyManagers, trustManagers, null); 322 | return sslContext.getSocketFactory(); 323 | ``` 324 | 325 | **域名校验** 326 | 327 | SSL层只负责校验证书的真假,对于所有基于SSL的应用层协议,需要自己来校验证书实体的身份,因此Android默认的域名校验则由OkHostnameVerifier实现的 328 | 329 | 从 HttpsUrlConnection 的代码可见一斑: 330 | 331 | ``` 332 | static { 333 | try { 334 | defaultHostnameVerifier = (HostnameVerifier) 335 | Class.forName("com.android.okhttp.internal.tls.OkHostnameVerifier") 336 | .getField("INSTANCE").get(null); 337 | } catch (Exception e) { 338 | throw new AssertionError("Failed to obtain okhttp HostnameVerifier", e); 339 | } 340 | } 341 | ``` 342 | 如果校验规则比较特殊,可以传入自定义的校验规则给 HttpsUrlConnection。 343 | 344 | ``` 345 | HostnameVerifier hnv=new HosernameVerifier(){ 346 | @Override 347 | public boolean verify(String hostname,SSLSession session){ 348 | if("youhostname".equals(hostname)){ 349 | return true; 350 | }else{ 351 | HostnameVerifier hv=HttpsURLConnection.getDefaultHostnameVerifier(); 352 | return hv.verify(hostname,session); 353 | } 354 | } 355 | } 356 | ``` 357 | 358 | 参考: 359 | 360 | [https://www.cnblogs.com/amyzhu/p/8285300.html](https://www.cnblogs.com/amyzhu/p/8285300.html) 361 | 362 | [https://www.jianshu.com/p/bb3eeb36b479](https://www.jianshu.com/p/bb3eeb36b479) 363 | 364 | [https://zhuanlan.zhihu.com/p/32553477](https://zhuanlan.zhihu.com/p/32553477) 365 | 366 | [https://www.infoq.cn/article/2017/10/qzone-quic-practise](https://www.infoq.cn/article/2017/10/qzone-quic-practise) 367 | 368 | [https://www.infoq.cn/article/2018/03/weibo-quic](https://www.infoq.cn/article/2018/03/weibo-quic) 369 | 370 | [https://www.wolfcstech.com/2019/03/27/quic_2019_03_27/](https://www.wolfcstech.com/2019/03/27/quic_2019_03_27/) -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/device.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/http_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/http_code.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/https.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/https.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/protocol.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/relation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/relation.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/tcp_ip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/tcp_ip.png -------------------------------------------------------------------------------- /HTTP版本变迁以及HTTP-QUIC/构建APK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/HTTP版本变迁以及HTTP-QUIC/构建APK.png -------------------------------------------------------------------------------- /IoT/IOT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/IOT.png -------------------------------------------------------------------------------- /IoT/README.md: -------------------------------------------------------------------------------- 1 | # IoT调研 2 | 3 | ## overview 4 | 所谓物联网,在中国也称为传感网,指的是将各种信息传感设备与互联网结合起来而形成的一个巨大网络。物联网是新一代信息技术的重要组成部分。其英文名称是“The Internet of things”。由此,顾名思义,“物联网就是物物相连的互联网”。这有两层意思:第一,物联网的核心和基础仍然是互联网,是在互联网基础上的延伸和扩展的网络;第二,其用户端延伸和扩展到了任何物品与物品之间,进行信息交换和通信。因此,物联网的定义是通过`射频识别(RFID)`、`红外感应器`、`全球定位系统`、`激光扫描器`等信息传感设备,按约定的协议,把任何物品与互联网相连接,进行信息交换和通信,以实现对物品的`智能化识别`、`定位`、`跟踪`、`监控和管理`的一种网络。 5 | 6 | 物联网作为新一代的信息技术的重要组成部分,有三个方面的特征: 7 | 8 | 首先,物联网具有互联网特征。对需要所用的物联网技术联网的物体来说一定要有能够实现互联互通的互联网支撑。 9 | 10 | 其次,物联网技术具有识别与通信特征。接入联网的物体一定要具备自动识别的功能和物物通信的功能(M2M)的功能。 11 | 12 | 最后,物联网技术具有智能化特性。使用物联网技术形成的网络应该具有自动化,自我反馈和只能控制的功能。 13 | 14 | 在物联网体系架构中,三层的关系可以理解为: 15 | 16 | 1. 感知层是物联网的皮肤和五官–识别物体,采集信息。感知层包括二维码标签和识读器、RFID标签和读写器、摄像头、GPS等,主要作用是识别物体,采集信息,与人体结构中皮肤和五官的作用相似。 17 | 18 | 2. 网络层是物联网的神经中枢和大脑–信息传递和处理。网络层包括通信与互联网的融合网络、网络管理中心和信息处理中心等。网络层将感知层获取的信息进行传递和处理,类似于人体结构中的神经中枢和大脑。 19 | 20 | 3. 应用层是物联网的“社会分工”—与行业需求结合,实现广泛智能化。应用层物联网与行业专业技术的深度融合,与行业需求结合,实现行业智能化,这类似于人的社会分工,最终构成人类社会。 21 | 22 | 23 | ![MacDown logo](./IOT.png) 24 | 25 | 26 | ## 应用篇 27 | 28 | 物联网的三个核心环节在于零部件、整机和内容及运维服务,生态圈的投资机会主要在两类公司:**一类是采用硬件、软件和服务一体化商业模式的公司,一类是具有新型硬件核心元器件能力的公司**。 29 | 30 | 物联网硬件市场空间大,细分领域商业模式各不相同:物联网市场空间到2018年将达到4.4万亿美元,CAGR22%。细分领域商业模式:1)一般消费类具有时尚行业特质,硬件和运维服务可以实现盈利;2) 医疗器械类可以从病患、医生、医院、药厂和保险公司多方获得盈利;3)智能家居类可以通过硬件直接盈利、硬件及运维服务和平台收费三种方式盈利;4)游戏 类的硬件和虚拟消费均可获得收入;5)车载类则通过精准营销和保险公司补贴来获利;6)智慧社区、工业追溯等也各具不同盈利模式。7)智慧农业,形成从育前、育中,育后销售等生态闭环。 31 | 目前大多的发展方向和切入点如下: 32 | 33 | ### 智能农业 34 | 农业物联网,作为整个物联网发展中非常重要的一部分,国家也相当重视,《中华人民共和国国民经济和社会发展地十二个五年规划刚要》指出,要“推动物联网关键技术研发和在重点领域的应用示范”。一、研发。二、应用。2009年8月7日,温家宝总理在视察无锡时提出了“感知中国”的理念,物联网自此发展迅速。农业物联网方面,利用信息传感设备去感知作物的生长环境,是一个不容忽视的领域。 35 | 36 | 国内企业也都相继摩拳擦掌,跃跃欲试,有些先锋企业已经有了比较成熟的产品。抛开物联网的概念不谈,农业现代化、农机自动化,再加上目前的互联网技术以及移动智能终端的大发展,农业物联网一定会有一个非常大的发展空间。那么农业物联网的创业方向有哪些呢? 37 | 38 | 1. 大部分农业方向的物联网的公司目前还只是简单的监测,展示,客户一般都是相关的政府部门,学校研究所之类,基本都是示范性工程项目,对农产品的产量销量几乎没有帮助,你可以跟这大部分公司一样,跑跑关系,接些单,赚些钱。
39 | 40 | 2. 你也可以做一个大的平台,解决三个问题:1,为政府将农业信息化。2,为农民增产增量增收并帮农民将农产品卖出去。3,为消费者提供健康安全放心的农产品。比如做了一套农业物联网的平台,农民可以实时了解农作物的生长状态,平台里有农业专家帮你分析,设备可以将农产品调整到最佳的生长环境,平台里做一个农产品的商城,并且农产品都是可追溯的。
41 | 42 | 3. 另外还有一些新型的农业方向,比如鱼菜共生系统与物联网的结合。
43 | 44 | 农业物联网方向很多,但是目前发展很迅速的是农业环境的监测和检测。这是农业物联网的发展方向,也是国家规划方向。目前中国大部分地区加速城镇改造,将土地整合集中承包给大型企业进行农场运营。所以这块需要很多土地环境检测和监控设备。 45 | 46 | ###### 农业物联网应用发展现状 47 | 48 | **在农业资源监测和利用领域**,美国和欧洲主要利用资源卫星对土地利用信息进行实时监测,并将其结果发送到各级监测站,进入信息融合与决策系统,实现大区域农业的统筹规划。例如,美国加州大学洛杉矶分校建立的林业资源环境监测网络,通过对加州地区的森林资源进行实时监测,为相应部门提高实时的资源利用信息,为统筹管理林业提供支撑。我国主要将GPS定位技术与传感技术相结合,实现农业资源信息的定位与采集;利用无线传感器网络和移动通信技术,实现农业资源信息的传输;利用GIS技术实现农业资源的规划管理等。例如杭州电子科技大学学者研究了基于无线传感器网络的湿地水环境数据视频监测系统,该系统实现对湿地全天候的实时监测,具有数据分析与处理,并对污染等突发事件和环境急剧变化所影响的水域的水环境状况实时报警等功能。 49 |
50 |   **在农业生态环境监测领域**,美国、法国和日本等一些国家主要综合运用高科技手段构建先进农业生态环境监测网络,通过利用先进的传感器感知技术、信息融合传输技术和互联网技术等建立覆盖全国的农业信息化平台,实现对农业生态环境的自动监测,保证农业生态环境的可持续发展。例如,美国已形成了生态环境信息采集-信息传输处理-信息发布的分层体系结构。法国利用通信卫星技术对灾害性天气进行预报,对病虫害进行测报。我国研制了地面监测站和遥感技术结合的墒情监测系统,建立了农业部至各省、重点地县的农业环境监测网络系统等一批环境监测系统,实现对农业环境信息的实时监测。例如我国每年通过农业环境监测网络开展农业环境常规监测工作,获取监测数据10万多个;融合智能传感器技术的墒情监测系统已在贵阳、辽宁、黑龙江、河南、南京等地推广应用。 51 |
52 |   **在农业生产精细管理领域**,美国、澳大利亚、法国、加拿大等一些国家在大田粮食作物种植精准作业、设施农业环境监测和灌溉施肥控制、果园生产不同尺度的信息采集和灌溉控制、畜禽水产精细化养殖监测网络和精细养殖等方面应用广泛。例如,2008年,法国建立了较为完备的农业区域监测网络,指导施肥、施药、收获等农业生产过程。荷兰VELOS智能化母猪管理系统在荷兰以及欧美许多国家得到广泛应用,能够实现自动供料、自动管理、自动数据传输和自动报警。泰国初步形成了小规模的水产养殖物联网,解决了RFID技术在水产品领域的应用难题。我国在涉及田间环境土壤信息获取、联合收获机自动测产、农田作物产量空间差异分布图自动生产和农业机械作业监控等大田粮食作物生产方面;在设施农业环境数据采集、发布,调控等设施农业生产方面;在果园监测、水肥控制、节水灌溉自动化等果园精准管理方面;在养殖环境监控、健康养殖等畜禽水产养殖等方面研发了一批系统,且应用成效显著。例如国家农业信息化工程技术研究中心成功研制了基于GNSS、GIS、GPRS等技术的农业作业机械远程监控调度系统,可优化农机资源分配,避免农机盲目调度。中国农业大学建立了蛋鸡健康养殖网络系统和水产养殖环境智能监控系统。 53 |
54 |   **在农产品安全溯源领域**,国外发达国家在动物个体编号识别、农产品包装标识及农产品物流配送等方面应用广泛。例如加拿大肉牛2001年起使用一维条形码耳标,目前已过渡到使用电子耳标。2004年日本基于RFID技术构建了农产品追溯试验系统,利用RFID标签,实现对农产品流通管理和个体识别。我国开展了以提高农产品和食品安全为目标的溯源技术研究和系统建设,研发了农产品流通体系监管技术。例如北京、上海、南京、四川、广州、天津等地相继采用条码、IC卡、RFID等技术建立了农产品质量安全溯源系统。浙江大学、北京市农业信息中心等单位研究开发了车载端冷链物流信息监测系统。 55 | 56 | ###### 农业物联网产业发展现状 57 | 58 | 农业物联网产业链主要包括三方面内容:传感设备、传输网络、应用服务。 59 |
60 |   **在传感设备方面**,国外发达国家从农作物的育苗、生产、收获一直到储藏缓解,传感器技术得到了较为广泛的应用,包括`温度传感器、湿度传感器、光传感器`等各种不同应用目标的农用传感器。在农业机械的试验、生产、制造过程中也广泛应用了传感器技术。RFID广泛应用在农畜产品安全生产监控、动物识别与跟踪、农畜精细生产系统和农产品流通管理等方面,并由此形成了自动识别技术与装备制造产业。 61 |     
62 |   **在传输网络方面**,国外已在无线传感器网络领域初步推出相关产品并得到示范应用。如美国加州Grape Networks公司为加州中央谷地区的农业配置了“全球最大的无线传感器网络”;2002年,英特尔研究中心采用跟踪方法采集了因州海岸的大鸭岛上的生态环境信息。国外互联网与移动通讯网在农业领域得到广泛的应用。2004年,佐治亚州的两个农产已经用上了与无线互联网配套的远距离视频系统和GPS定位技术,分别监控蔬菜的包装和灌溉系统。美国已建成世界最大的农业计算机网络系统,该系统覆盖美国国内46个州,用户可通过计算机便可共享网络中的信息资源。 63 |   
64 |   **在应用服务方面**,SOA(Service Oriented Architecture)即服务导向架构,自1996年Gartner提出以来受到了IT业界的热捧,产业化进程不断加快。2006年以来,IBM、BEA、甲骨文等一批软件厂商开发推出了一系列实施方案并部署了一些成功案例,使得SOA进入现实的脚步在不断加快。同年,IBM全球SOA解决方案中心在北京和印度成立,定制各个行业的模块化SOA解决方案,并结合IBM服务咨询和软件力量全方位实施,这意味着IBM已经在SOA产业化方面抢先一步。BEA也宣布推出“360度平台”以进一步巩固其在中间件领域的优势,而微软和甲骨文也纷纷发力中间件市场,竞争进一步加快SOA产业化进程。 65 |    66 |    67 | ##### 案例 68 | 69 | 1. [大棚环境远程监控系统](http://www.yfnywlw.com/al/xm/273.html) 70 | 2. [水产、畜牧养殖管理系统](http://www.yfnywlw.com/fa/scyz/212.html) 71 | 3. [农田水肥一体化智能监控系统](http://www.yfnywlw.com/fa/sfyt/214.html) 72 | 4. [农林“四情”监测预警系统](http://www.yfnywlw.com/fa/ssny/59.html) 73 | 74 | 75 | ### 智能医疗 76 | 在物联网的加持下,医疗健康继续成为人们的焦点话题。在人口老龄化的大背景下,医疗服务亟需完善。令人振奋的是智能医疗已经渐行渐近,同时可穿戴智能设备行业亦正在快速成长,大量企业的蜂拥而至也突显了此市场的巨大魅力,到2018年,中国智能医疗市场规模将超过一百亿元,市场前景看好。在不久的将来医疗行业将融入更多人工智慧、传感技术等高科技,使医疗服务走向真正意义的智能化,推动医疗事业的繁荣发展。业内人士认为,下一个十年,从诊断、监护、治疗到给药的医药细分领域将开启智能化时代,医疗器械行业向便携化、智能化发展是大势所趋。在这样的背景下,现代移动互联、穿戴式设备、大数据等新兴技术与新商业模式的结合,传统的医疗器械或被移动医疗、穿戴医疗、商业保险、大数据等新兴技术颠覆,智慧医疗尤其是可穿戴设备将成为未来重点投资领域。 77 | 78 | 目前在物联网与医疗健康的存在和发展形势如下: 79 | 80 | 1. **智能医疗设备**
81 | 随着人们对自身健康状况的日益关注和各地养老产业园、养老社区的迅速兴起,与养老产业配套的掌上监护仪、一体式监护仪等医疗设备和轮椅、血压计、血糖仪等家用医疗设备需求巨大。医疗器械行业未来看好的方向包括慢性病相关领域、中高端进口替代和移动智能医疗、掌上监护仪、一体式监护仪等医疗监护设备。另外,轮椅和血压计、血糖仪等家用医疗设备对老人监控身体状况至关重要,同样需求巨大。 “**可穿戴设备的微型化、便携化,将为医疗器械行业带来一场革命。**” 可穿戴类医疗**最大的潜力不在于硬件,而在于其用户黏性,企业通过可穿戴设备监测到的服务患者数据,可以为医院、药企和其他产业链相关者收集医疗云端“大数据”,由此衍生出新的商业模式。** 82 | 83 | 以心脏病监测为例,一次心电图难以捕捉到有效的诊断依据,可穿戴式的设备可以很方便帮助患者监测并记录心电数据,能够及时发现常规心电图不易发现的心率失常和心肌缺血,是临床分析病情的重要依据。借助无线动态心电图监测,通过其所收集的数据送达云端,医生可以轻松访问这些数据。 84 | 85 | 高血压是严重威胁生命健康的心血管疾病。借助可穿戴式医疗设备可以24小时动态地监测用户的血压数据,向医生提供不同时段的血压数据信息。MIT在2009年就开发出了能长时间连续测量的“可穿戴式血压计”,能够24小时连续测量血压。 86 | 87 | 传统的血糖检测是通过监控餐后或空腹血糖,但是餐后或空腹血糖的测定,只反映患者的某一具体时间的血糖水平。而可穿戴式医疗设备可以实施动态血糖监测,可以更好掌握血糖的变化,帮助患者及时发现问题,并且降低糖尿病并发症的风险。目前谷歌正在研发测血糖的隐形眼镜,可以衡量佩戴者眼泪中的葡萄糖水平,有效地帮助糖尿病人监控血糖水平。 88 | 89 | ##### 案例 90 | [RFID助力医院监控跟踪儿科血液制品](http://iot.ofweek.com/2017-02/ART-132209-11000-30108838.html) 91 | 92 | [智能机器人“糖宝” ](http://news.hc3i.cn/art/201704/38689.htm) 93 | 94 | 95 | [帕金森患者智能勺子](http://news.cctv.com/2016/12/17/ARTIeLjP1Ns6ArSAoRGKRMBS161217.shtml) 96 | 97 | [床旁监测设备-Wearable Biosensor](http://news.hc3i.cn/art/201603/35722.htm) 98 | 99 | 100 | 2. **医疗健康大数据服务平台** 101 | 102 | 从麦肯锡分析大数据价值,提出大数据时代到现在不过短短5年时间,大数据好像突然间就进入了我们的生活,人人嘴边都挂着大数据。 马云在《2015年致投资者公开信》中所说“人类已经从IT时代步入DT时代”。而硅谷也流传着这样一句话 “你正坐在一座数据金山上”。 103 | 104 | 如今HealthTech行业内的一个共识:健康数据将成为数字医疗时代的重要资产。 105 | 有效的健康数据,不仅代表着对于每个患者的个性化处理方案,更意味着对于整个产业的变革与颠覆。 106 | 107 | 针对数据的`采集`、`分析`、`处理`,将成为主导数字医疗发展的重要方向。事实上,近年受到资本狂热追捧的基因组排序,也是人体众多医疗数据采集的一种。根据Global Market Insights的最新统计,十年之内,数字医疗的市场规模将从513亿美元飙升至3790亿美元。 108 | 109 | 110 | 数字医疗时代对医疗数据的需求包括三个方面:**医疗级数据采集**,**数据存储与管理**,**以及数据分析**。这三个方面也正是许多高科技医疗企业努力的方向。 111 | 112 | 新时代的数据采集,不仅要做到收集具有临床价值、及时的人体生理健康信息,还要尽可能地符合用户的使用习惯。“厂商们需要努力找到一个‘最佳路径’——最不干扰用户的日常生活,又能完成医疗级精度的数据采集的目标。”李江博士说。值得注意的是,“最佳路径”代表着未来重要的人体数据入口,这正是许多传统的厂商开始发力医疗级智能可穿戴设备的原因之一。 113 | 114 | 数据存储与管理也是包罗万象,其中包括了各种传感器数据的存储和管理,个人用药习惯,EHR(电子健康病历)和EMR(电子病历记录)等等,能够记录一个人几乎所有的健康以及行为和获得的医疗服务数据。未来,这些数据组成的个人电子病历也会保存在云端,让医疗机构能够随时调用。 115 | 116 | 数据分析与挖掘部分,医疗健康领域产生海量的数据,和其他领域相比,相应的数据挖掘工作却显得严重滞后。为提高医疗健康水平,医疗领域需要找到一个能够有效处理大数据的方法。数据挖掘技术在医疗实践中的应用是一个过程,而不是一次性的任务,随着时间的推移,健康数据会越积越多,数据间的关联性也会更加复杂,因此行之有效的医疗数据挖掘技术,对于医学领域的发展有着重大的现实意义。小到监测体温来确定患者是否发烧和预测康复情况,大到用人工智能来分析X光片,通过DNA数据分析患癌几率等等,都需要利用分析工具,从数据中获取有效的结果。 117 | 118 | 例如基于以上数据的采集,挖掘,我们可以为患者提供个性化医疗推荐,可以为患者生成专属的治疗方案,充分挖掘患者间、疾病间和医生间的关系,为疾病的科学诊断保驾护航。由于患者的个性差异和疾病间的共性共存,同时在许多层面还存在着某种联系,个性化医疗推荐系统就是运用数据挖掘的方法找出并建立其中的联系,根据关联建立患者、疾病和临床数据间的模型,针对病人的治疗经历、基因、遗传、环境、生活方式等信息挖掘出适合该患者的个性化治疗方案侧,个性化推荐流程。如图:![MacDown logo](./health1.jpg) 119 | 120 | 121 | 对于数字医疗国家也是相当的重视,从国务院办公厅印发的[《关于促进和规范健康医疗大数据应用发展的指导意见》](https://36kr.com/p/5048603.html)中我们可以看出国家层面的对健康大数据的规划和展望。 122 | 123 | 基于以上我们可以提供出医疗健康大数据服务平台.医疗健康大数据服务平台是一个包含多个业务系统、多个自身管理软件、是一系列软、硬件和人员、政策支持的综合系统体系,统一建设医疗健康云计算服务中心,集中存储居民医疗卫生信息和居民电子健康档案等数据,满足社会大众、医务工作者、各级卫生主管部门、第三方机构的应用需求。 医疗健康大数据服务平台总体架构如下图所示 ![MacDown logo](./health2.jpg) 124 | 125 | 126 | ######1、展现层 127 | 128 | 负责对用户提供医疗健康信息、以及分析与挖掘信息服务,支持4大类用户,包括:社会公众、医务工作者、卫生主管部门和第三方机构。通过本平台,既可以获得医疗健康数据服务结果展示,也可以获得医疗健康数据分析与挖掘服务结果展示。本平台对外提供 Web页面接入方式或移动通讯终端(android、iOS)接入方式。 129 | 130 | ######2、服务层 131 | 132 | 服务层主要是平台建设过程中能够提供的所有应用相关服务。应用服务大致可分为业务应用类服务、数据资源类服务、工具软件类服务和其他类服务。业务应用类服务主要面向不同的用户提供解决具体业务功能需要,主要包括公众服务、医院诊疗服务、综合卫生服务、大数据分析服务等;数据类服务按业务所划分的各类数据服务。工具软件类服务主要提供给数据的维护和采集、清洗、整合、分析、统计等。 133 | 134 | ######3、资源层 135 | 136 | 资源层负责医疗健康大数据和数据分析与挖掘相关应用资源的一体化存储和管理。资源层又可分为三层:**虚拟化业务管理平台、虚拟化数据管理平台和物理资源层。**其中: 137 | 138 | 物理资源层提供各种数据资源、应用资源的实际存储,包括:医疗健康相关的所有数据,建设的数据资源中心和应用服务资源中的所有资源。本层将提供关系数据库系统、非关系数据库、数据仓库等多种类型的数据管理系统。 139 | 140 | 虚拟化数据管理平台采用虚拟化技术对所有物理资源进行封装,对上层提供各种虚拟化资源。对内部,虚拟化数据管理平台通过异构式数据集成与管理、虚拟化资源调度、数据划分、负载均衡、实时备份监控、故障恢复等多种手段保证整个平台的高性能、高可用性、高可扩展性。 141 | 142 | 虚拟化业务管理平台负责对所有的应用服务相关资源进行管理和调度。根据功能,它又可以划分为:虚拟化数据资源中心和虚拟化应用服务组件资源中心。其中:数据资源中心针对不同的需求,对不同业务部门不同结构数据进行分析、抽取、加工,形成面向主题的综合数据,为组织内各个层面的人员提供高效的、用于宏观决策的各种信息。应用服务资源中心应用服务组件资源中心通过提供数据挖掘等服务,使卫生行业管理者们能够利用各种历史数据和现在的数据进行各种复杂分析、预测和辅助决策。 143 | 144 | 145 | 146 | 机遇总是伴随着挑战,智能医疗虽然拥有广阔的发展空间和良好的发展机遇,但是也存在诸多潜在的挑战和问题。 147 | 如用户习惯的有待培养;急需建立管理规范行业标准;远程医疗等费用支付与保险理赔的依据问题;医疗数据共享以及数据安全,个人隐私问题;智能医疗的专业人才队伍培养等等。 148 | 149 | ### 智慧城市 150 | 智慧城市的概念最早源于IBM提出的“智慧地球”这一理念,此前类似的概念还有数字城市等。2008年11月,恰逢2007年-2012年环球金融危机伊始,IBM在美国纽约发布的《智慧地球:下一代领导人议程》主题报告所提出的“智慧地球”,即把新一代信息技术充分运用在各行各业之中。 151 | 152 | 具体地说,“智慧”的理念就是通过新一代信息技术的应用使人类能以更加精细和动态的方式管理生产和生活的状态,通过把传感器嵌入和装备到全球每个角落的供电系统、供水系统、交通系统、建筑物和油气管道等生产生活系统的各种物体中,使其形成的物联网与互联网相联,实现人类社会与物理系统的整合,而后通过超级计算机和云计算将物联网整合起来,即可实现。此后这一理念被世界各国所接纳,并作为应对金融海啸的经济增长点。同时,发展智慧城市被认为有助于促进城市经济、社会与环境、资源协调可持续发展,缓解“大城市病”,提高城镇化质量。 153 | 154 | 基于国际上的智慧城市研究和实践,“智慧”的理念被解读为不仅仅是智能,即新一代信息技术的应用,更在于人体智慧的充分参与。**推动智慧城市形成的两股力量,一是以物联网、云计算、移动互联网为代表的新一代信息技术,二是知识社会环境下逐步形成的开放城市创新生态。**一个是技术创新层面的技术因素,另一个则是社会创新层面的社会经济因素。但总的来说,**智慧城市以云计算中心为核心,实现全面感知、互联互通、数据共享和高效服务**,具备以下几个方面的技术特点: 155 | 156 | ######全面感知: 157 | 通过各种终端、摄像头、传感器等收集和获取各种信息,各种感知设备是智慧城市的神经末梢。 158 | 159 | ######互联互通: 160 | 各类宽带有线、无线网络技术的发展为城市中物与物、人与物、人与人的全面互联、互通、互动提供了基础条件,智慧城市通过有线及无线设备,实现各种终端的无线接入,并承载到相应的业务网络。 161 | 162 | ######数据共享: 163 | 基于新一代数据中心的云计算、物联网及运营支撑系统,对收集到的数据进行存储、处理和转发,支撑上层具体业务应用。同时,数据开放性也是衡量智慧城市的关键标准,通过数据支撑层打破各业务系统之间的条块分割,实现数据的横向联合和共享应用。 164 | 165 | ######高效服务: 166 | 智慧城市的最终应用是服务,为市民、企业和政府管理部门提供各种服务。 167 | 168 | ![MacDown logo](./city1.jpg) 169 | ![MacDown logo](./city2.jpg) 170 | 171 | 172 | 173 | ###### 发展趋势 174 | 随着国家城镇化建设以及十三五期间城镇信息化建设的规划,智慧城市建设的市场潜力非常之大。2014年,我国智慧城市IT投资规模达2,060亿元,较2013年同期增长17.0%;2015年,我国智慧城市IT投资规模达2,480亿元,较2014年同期增长20.4%。预计,2017年我国智慧城市IT投资规模将达到3,752亿元,未来五年(2017-2021)年均复合增长率约为31.12%,2021年IT投资规模将达到12,341亿元。2017年我国智慧城市市场规模将达到6.0万亿元,未来五年(2017-2021)年均复合增长率约为32.64%,2021年市场规模将达到18.7万亿元。仅仅智能路灯的 175 | 176 | 面对如此之大的市场规模和潜力,国内外各大科技公司也在积极的布局和参与智慧城市发展: 177 | 178 | **谷歌** 179 | 谷歌不仅在人工智能及智能技术研发上成绩卓越,并逐渐渗透进智慧城市建设,Google Y实验室,致力于先进技术及产品研发,解决机场甚至城市是如何运作的新方法,从而完成创建智能城市模型的超前设想。 180 | 谷歌在智能家居、人工智能、图像与语音识别领域。通过一系列并购、开放平台的建立、软件硬件一体化来打造这个生态系统。 181 | 182 | **微软** 183 | 2013年7月10日微软在全球合作伙伴大会上发布智慧城市“Citynext”计划,在Citynext,微软将运用云计算、移动设备、海量数据分析平台、社交网络等软硬件技术,并整合合作伙伴的资源,解决方案有城市指标仪表板(CityNext Dashboard)、智能交通(Intelligent Traffic)、智能市民服务大厅(Smart Citizen Service Hall)、智能健康护理(Intelligent Healthcare)。 184 | 185 | **甲骨文** 186 | 甲骨文全公司有近万产品线,其中就有与最近流行的物联网、智能城市密切相关。“甲骨文智能城市”项目的关注点主要包括三个方面:服务提交平台、智能平台、集成和IT基础设施平台。其主要目的是要实现重要信息的共享,例如在各级政府范围内公布某个可能有犯罪记录的游客的护照信息。 187 | 甲骨文参与了世界各地不同智慧城市或相关领域如云计算、电子政务、电子商务/两化融合、物联网等方面的建设。 188 | 189 | **IBM** 190 | 2008年11月IBM提出“智慧地球”概念,2009年8月,IBM又发布了《智慧地球赢在中国》计划书,正式揭开 IBM智慧地球中国战略的序幕。 191 | IBM 智慧城市的使命就是要提供各种流程、系统和产品,促进城市发展和可持续性,为其居民、经济以及城市赖以生存的生态大环境带来利益。通过应用信息技术(IT)规划、设计、建造和运营城市基础设施,改善生活质量和经济福利。 192 | 193 | **SAP** 194 | SAP目前已全面转型云服务,加入云服务阵营。推出基于SAPHANA云平台的解决方案及服务。实时数据分析平台HANA的推出,使得本来专注于企业级软件的SAP成为一个平台供应商,从而涉猎很多“以前不可能去碰”的创新应用领域,智慧城市就是其中一个重要领域。 195 | 196 | **英特尔** 197 | 英特尔智慧城市推动产业伙伴协同创新,针对不同地区发展的特色需求打造智慧城市解决方案。2014年中,英特尔实施了一项被称作“智能城市美国”的试验性项目,这也是英特尔在美国的第一个类似项目。除此之外,英特尔收购Altera进军物联网,或成该领域领头羊,并大举进攻可智能设备及车联网的研发设计,力图打造一个智能物联生态。 198 | 199 | **三星SDS** 200 | 三星SDS通过对中心平台(UBI-center)的开发和建设,为城市提供了一个智慧的大脑,城市中政府,企业,市民等等元素都在这个大脑的领导下高效运行,真正意义上做到了打破信息孤岛及综合管理。 201 | 202 | **通用电气** 203 | GE通用电气公司正在美国的圣地亚哥、加利福尼亚、杰克逊维尔、佛罗里达等地区建立智能路灯引导系统,监控停车位和交通路况。在圣地亚哥,GE部署了4000个带有摄像监控的智能LED路灯,路灯可以监控追踪城市停车位情况,并将数据发送到GE的云平台Predix,用户通过手机App,可查找最近的空置停车位。该智能灯还可以追踪道路的交通情况,每年还可节省25万美金的电力开销。目前,GE对Predix云平台的投资已经超过10亿美元,并希望将该平台开放给第三方开发者,以此打造更多智慧城市的App。 204 | 205 | **高通** 206 | 高通去年宣布串联旗下多家公司(QTI),包括高通技术、高通创锐讯、高通生命,以及高通互联体验公司(QCE),宣布在物联网、智慧家庭、智慧城市、穿戴装置、汽车电子与医疗照护等市场已经就位。其互联网领域的All seen联盟,现有超140家企业加入,联盟成员已发表75项支持All joyn 架构的产品。 207 | 208 | **华为** 209 | 在智慧城市上,华为的战略已经清晰,在率先提出**“一云二网三平台”**的智慧城市整体架构解决方案的同时将业务定位于“聚焦ICT基础设施。”在“云”上,华为的云数据中心具有分布式架构、开源平台、云产业链各环节能力最全的特点;在“网”上,华为以有线+无线组成的敏捷网络,构建城市无处不在的宽带;作为NB-IoT标准的引领者,华为还为物联网提供业界最轻量级的物联网操作系统LiteOS,在联接“物”的数量、广度及超低功耗表现上领先业界,此外,华为还提倡合作共赢,与伙伴共同提供大数据服务支撑平台、ICT业务应用使能平台、城市运营管理平台等。 210 | 华为在传感领域则推出自主研发的Boudica物联网芯片、IoT-OS物联操作系统,华为在大数据平台层提供分布式的数据处理系统FusionInsight,在提供海量数据的存储,分析和查询能力的同时支持从数据孤岛向数据融合的演进,通过数据共享与交换+大数据集成管理,支撑城市大数据应用,构建智慧城市生态圈。 211 | 212 | 去年,**华为成立了智慧城市业务部**,此举表明其在该领域已经有了较为深刻的认识。2017年,华为将在城市IOC、智慧水务等物联网应用,以及政务、教育、医疗等民生领域加大投资,并且会投入上亿元基金用于与合作伙伴的联合解决方案研发、联合营销以及人才培养,打造智慧城市生态圈。 213 | 214 | 此外,华为还联合生态伙伴发布了9大联合解决方案:奥格&华为海绵城市雨洪管理联合解决方案、超图&华为智慧城市GIS云联合解决方案、广通&华为城市公共信息资源共享交换平台联合解决方案、华傲&华为城市大数据平台联合解决方案、软通动力&华为城市移动门户联合解决方案、泰豪&华为智慧城市能耗监测节能减排云联合解决方案、太极股份&华为智慧政务联合解决方案、未来国际&华为基于政务云的新型智慧城市联合解决方案、易智瑞&华为智慧城市GIS云联合解决方案,覆盖新型智慧城市建设多个方面。 215 | 216 | ![MacDown logo](./city3.jpg) 217 | 218 | 219 | **中兴** 220 | 221 | 在中兴通讯今年发布的《M-ICT2.0白皮书》中,将物联网定为开拓未来的五大战略方向之一,并将物联网具体施行的战略概括为**“两平三横四纵”,**“两平”即重点打造生态圈和资本两大支撑平台,在构建开放的连接、管理和应用平台,为上下游产业链的客户提供服务的同时,SmartIoT OS系统还为IoT终端提供智能化方案,结合大数据和云计算能力,帮助伙伴挖掘每个“BIT”的价值。ZTE AnyLink新的城市物联网公共云平台,该平台破除了城市管理中各部门的信息壁垒,使其全面协同,对城市完成全面的感知;同时,采用先进快速的大数据等技术进行科学有效的深度分析处理,支撑城市管理新服务深度,创生新的城市智慧。 222 | “三横”则是在终端、网络及IoT PaaS三个层面布局,这原本就是中兴通讯所擅长的领域,在网络上,中兴通过优化在短距离、广域网以及城域和核心网的技术,满足IoT应用的差异化需求;作为NB-IOT技术的重要贡献者之一,中兴通讯在NB-IOT上走得比华为更远,在积极与国内三大运营商合作,进行NB-IoT的规范制订、技术试验和试点建设的同时,与移动合作完成了业界首家基于3GPP NB-IoT标准协议的技术验证演示,二者还一起在浙江乌镇开通首例符合3GPP标准的NB-IOT蜂窝物联网智能停车业务。此外,中兴通讯在LoRa技术上不遗余力,具备提供全套LoRa网络解决方案的能力。 223 | “四纵”主要聚焦在智慧城市、智慧家庭、工业互联网、车联网四大垂直领域,这几大领域的应用是中兴通讯物联网核心技术的延伸,均取得不瞩的成绩。此外,中兴通过不断地合纵连横,建立GIA联盟,联盟成员可享受中兴通讯全球物联网专利技术的优先授权及商机共享,从而吹响了构建生态联盟的号角! 224 | 225 | 在智慧城市的时间上:今年年初中兴通讯联合上海电信,还有上海电信研究院一起在上海世博园做了一个**NB—IOT的智能井盖**的实际应用的试点。这个试点实际上基于现有NB—IOT的新技术的对现有智能井盖进行改造,达到对智能井盖进行实时监控。通过方案解决了井盖自动的监控、检测、判定还有报警问题,同时还可以通过把传统井盖的一些资产管理,告警管理还有一些维护管理等等功能,也内置在这一套基于NB—IOT改造的智能井盖的应用系统之中,完成了对井盖全方位在线的监控,同时完全简化传统智能井盖的复杂的模式。 226 | 227 | 228 | **联想** 229 | 230 | 作为智慧城市深度参与者的联想,首次提出了**建设新型智慧城市的道与法**。所谓“道”即新型智慧城市建设和理念和目标;所谓“法”即新型智慧城市建设的方式和方法。 231 | 232 | 在联想看来,智慧城市是一个巨系统,涵盖城市管理中的各个领域,因此联想秉承国家新型智慧城市建设标准及建设目标,通过对众多城市管理者的深入访谈调研,提出了以城市管理大数据处理中心为运营基础,城市综合运营管理中心为管理窗口,及城市创新体验中心为展示窗口的城市运营核心理念。 233 | 234 | 联想认为,智慧城市建设总体可以归纳为城市治理及产业升级两方面,在规划建设时应针对不同城市特点予以不同的建设方法,才能更有效地保证城市平稳运营并快速提升产业的创新发展及经济造血功能。所以,联想也提出了贯彻双态IT建设理念的新型智慧城市建设思路,具体包括: 235 | 首先,针对城市治理中确保服务稳定、持续创新的要求,以稳态建设为指导思想,深入调研分析各类政务应用, 采用恰当的信息化手段,逐步提升公众服务能力。 236 | 其次,针对产业升级中利用新长板理论,在优势产业上快速突破的要求,以敏态建设为指导思想,快速结合当地优势,在品牌塑造、“互联网 +”营销、生态圈打造等多个方面,集中力量快速提升品牌效应。 237 | 238 | 239 | **阿里** 240 | 241 | 2月28日,重庆市政府与阿里巴巴集团、蚂蚁金服集团签订战略合作协议,共同推进西部创新中心建设以及“互联网+”行动计划。根据协议,三方将发挥各自优势,在云计算、大数据、电商、物流、新型智慧城市、普惠金融服务等领域加强合作。 242 | 243 | 阿里巴巴集团将提供基于云计算、大数据、物联网的先进技术和解决方案,在重庆打造“YunOS生态圈”,围绕智能语音交互、智能物联网、车联网、智能家居等领域,重点推动汽车、手机、电脑、电视等智能终端产业创业创新。 244 | 245 | 蚂蚁金服集团董事长彭蕾表示,要深度参与“信用重庆”建设,助力重庆市建立和完善新型智慧城市和精准社会治理的信用基础。积极推动重庆农村金融服务,打造农村金融示范县,为重庆精准扶贫贡献力量。 246 | 目前,支付宝“城市服务”在重庆已开通5大类共40项在线服务,涵盖政务、医疗、车主、交通等各领域。 247 | 248 | ###### 存在的问题 249 | 虽然目前各大科技公司都在智慧城市方面进行了很多的投资和布局,但是目前智慧城市还不成熟,还存在诸多问题: 250 | 251 | 1. 是顶层设计缺乏创新。从现在看,大部分智慧城市建设都面临顶层设计困扰。由于大中城市智慧城市建设早,投资空间大及建设复杂度高等原因,各个领域分开建设,缺少顶层设计思考,导致了大中城市智慧城市建设千城一面、缺少亮点。 252 | 2. 是投资回报难以衡量。从过去的经验看,很多城市都遇到投资效益问题。从信息化基础设施、平台建设直至上层应用建设,智慧城市建设需要在前期进行一定程度的先期投入。但很多城市在智慧城市建设过程中,也都遇到了投资回报或不明确、或无法界定、或投资模式不成熟的难题。 253 | 3. 是新兴技术不易融合。统计显示,智慧城市在建设过程中都趋向采用全新技术。新兴技术首先在商业领域得到了广泛应用,已经逐渐成熟的云计算、大数据、物联网等第三平台技术,支撑起各行业的业务发展对信息化的需求。不过,未来如何利用新兴技术来支撑智慧城市的建设,增强城市持续发展能力,如何在建设过程中更好地融合各方需求将成为一大考验。 254 | 4. 是传统的智慧城市建设侧重于技术和管理,忽视了“技术”与“人”的互动、“信息化”与“城市有机整体”的协调,导致了“信息烟囱”、“数据孤岛”,公共数据难以互联互通,市民感知度较差等问题。 255 | 5. 数据碎片化和不透明化。滴滴高级副总裁兼工程技术委员会主席章文嵩介绍,滴滴在中国每日订单量有2000多万,中国做智慧交通有巨大的数据资源优势。他同时指出,如果数据的价值不能评估,就很难做数据交换,数据资源的优势也发挥不出来。政府可以推动整体数据生态的建设,开放一些可安全公开的数据,和企业的数据整合在一起,搭建云数据平台。过去搞孤岛型建设,使得很多数据在各部门、各行业间被孤立隔离,智慧城市建设目前的关键就是解决该问题。 256 | 6. 群众参与感不强,未能打通联系群众的最后一公里。 257 | 目前的智慧城市建设多是从供给端出发,通过供应商提供后台产品和应用,以政府服务平台得以展现,真正能够连接到“服务接受者”的“智慧”种类并不多。从对各省内部分政府公务员与事业单位工作人员的抽样调查来看,多数公务人员都对智慧城市建设的现状与相关信息了解甚少。对智慧城市建设来说,公务人员既是智慧服务的“接受者”也是智慧服务的“提供者”,对智慧城市建设的参与和了解是非常有必要的。目前这种缺乏“地气”的现状亟待改善。此外,智慧城市不仅意味着硬件的完善,更要求该地居民IT素质、环保意识、城市创新能力、人才吸引力等软性综合实力的提升。因此,要注重城市人才的培养,要积极引导院校和企业关注大数据技术演进、承担关键技术和系统的创新研发工作,以创新技术的推广应用带动智慧城市产业链集聚和发展。 258 | 259 | 260 | 因此在智慧城市建设中,**互联网企业主要扮演着两个重要角色:连接政府与居民的桥梁、以及提供数据整合与大数据分析的技术提供商**。对于普通居民来讲,评价公共服务的主要标准有两个:一个是获得服务的便捷性,一个是服务的效率和质量,国内BAT三大互联网公司也对城市公共服务涉足已久,路径也颇为类似:**一开始是水电煤缴费,再后来是缴纳违章罚款,而后则是服务于交通(网约车、共享单车等等)、医疗等民生领域**。智能终端为公众方便地获得城市服务提供了入口。但仅有这个并不够,如果后端的效率得不到提升,服务请求便捷了反倒是麻烦。这时就**需要云计算和大数据来实现应用协同和数据融通**。 261 | 互联网企业参与智慧城市建设,不是简单地把政务服务放在网上或手机上,而是深度的整合底层系统、数据与服务平台及入口的整体解决方案。这样才更能给城市管理、公共服务的全方位改进带来帮助。 262 | 263 | 264 | 265 | ### 智能家居 266 | 267 | 几年之前的冬天,每天下班回家都没有热水洗澡,需要拖着疲惫的身体打开热水器,等着水烧开才能去洗澡。那时候我就想要是热时期能够远程控制就好了。下班时候远程打开,到家就有热水洗澡。在后来看了美国大片《钢铁侠》被里面强大而贴心机器人管家而震撼。 268 | 到了今天智能化的家用电器已经不再是理想化的东西了,它已经出现在我们的生活中了。 269 | ![MacDown logo](./home1.jpg) 270 | 作为物联网在家居方面的垂直化领域的应用,智能家居与普通家居相比,不仅具有传统的居住功能,还兼备建筑、网络通信、信息家电、设备自动化为一体的综合性功能,为人们提供高效、舒适、安全、便利、环保的居住环境,以及全方位的信息交互功能。帮助家庭与外部保持信息交流畅通,优化人们的生活方式,帮助人们有效安排时间,增强家居生活的安全性,甚至为各种能源费用节约资金。 271 | 272 | 273 | 智能家居的实现形式是首先实现传统家居产品的智能化和连接功能,再通过智能家居控制系统进行统一控制。智能家居的**第一阶段是实现远程控制**,通过手机App等 控制家居产品。**第二阶段是智能化**,感应人体和环境,通过记录和学习从而实现自动调节。例如:Nest的自动调温器产品会根据用户的使用特性分析调温,自动 感应无人在家时就关闭空调等。智能家居可以给用户带来安全、便利和节能等诸多的好处。 274 | 275 | 智能家居的盈利模式分为三类:**硬件直接盈利、硬件及运维服务和平台类** 276 | 277 | **硬件直接盈利**通过产品的差异化收取更高的硬件费用,即厂商若能够提供更智能化、更受消费者认可的产品,则可以享受更高的溢价。例如:美菱智能冰箱的定价可以比普通冰箱高1000-2000元。房地产开发商为了实现精装房的高端定位也常常愿意给智能家居硬件买单。
278 | 279 | **硬件及运维服务**的产品包括安防摄像头、智能电视等,以Dropcam为例,摄像头产品单价为149美元,7天云存储服务是每月收费9.99美元,或每年99美元,而1个月云存储服务则是月付29.95美元,或年付295美元。运维服务的盈利能力甚至高于硬件。
280 | 281 | **平台类公司收取加入平台的费用**,例如:苹果推出HomeKit,其它公司产品接入HomeKit需要向苹果支付一定的费用。 282 | 283 | 284 | 285 | 目前国内在智能家居领域拥有良好的生态体系的是`小米`和`360`. 286 | 287 | 在智能家居这块`小米`布局很早,崛起很迅速,其快速发展得益于强大的互联网思维,定位发烧友手机并形成了独特的粉丝文化,发展路径也从供应链到渠道再到云服务,在这块形成了自己良好的生态。 288 | 289 | ![MacDown logo](./xiaomi.jpg) 290 | 291 | 292 | 安全为本 360发力智能家居市场 293 | 294 | 针对IOT时代的到来,360公司也已推出了系列**智能家居安全产品**, 360随身Wifi首创细分品类累计销量接近3000万,儿童卫士上线3个月销量便超过50万成为国内销量最高的智能手表,1万台安全路由器P1仅用10秒售罄,屡创国内智能家居产品的热销纪录。 295 | 296 | 和小米一样,360 在市场布局节奏上也是围绕自身产品、外界合作到开放平台的“三部曲”展开。 297 | 298 | 1)全面覆盖360旗下的智能产品,如360儿童卫士、360随身Wifi、360安全路由、360智能摄像机等等; 299 | 300 | 2)与传统家电企业达成深度合作。据介绍,未来360将继续扩大合作范围,与更多的家电商展开跨界合作。 301 | 302 | 3)360还将对一系列没有自建云平台能力、产品线比较单一的企业进行全面的技术开放与扶持,包括平台开放(好的产品允许使用360品牌,在市场中共同打造爆品)、能力开放(包括360云开放、用户和流量开放、营销平台开放、芯片组开放以及超级APP开放,从各层面对合作企业予以扶持)和资本开放。在《360智能家居战略》中,360表示将投资100亿打造100家以上有前景的初创型企业和项目。 303 | 304 | 此次除了360生活助手外,智能摄像头、儿童卫士、车行记录仪、安全路由器等360旗下的其他重磅智能终端设备也悉数亮相。虽然看似散乱,但其实包括生活助手在内的每一款软件、硬件全部围绕“家庭”这个最常见的生活场景,并使之因智能变得更和谐、更温暖。 305 | 306 | 从终端领域布局,用生活助手分发服务,360已基本创建了一个完整的智慧家居生活生态链。而有了智能终端的支持,360生活助手平台将有望成为国内最大的“智慧生活服务中心”。 307 | 308 | **总之:** 309 | 310 | 智能家居产业是一个产业链集合,不是单一的公司能够包揽得了的,从下图可以清晰地看到整个产业链的各个环节: 311 | ![MacDown logo](./home2.png) 312 | 313 | 未来,白电企业和创业型公司负责智能设备的研发和生产,智能单品采用通用的模块(实质上是遵循通用的协议),由智能控制中心对家庭智能设备进行统一控制和管理,并将数据上传到云端,在云端进行数据分析,返回解决方案,用户既可以通过操作设备与家庭中的智能设备进行控制,也可以通过云服务进行傻瓜式的控制。 314 | 315 | [国外一些智能家居厂商](https://m.taihuoniao.com/topic/view-103454-0.html) 316 | 317 | [9大国外智能家居科技产品对比](http://smarthome.ofweek.com/2014-08/ART-91009-8120-28862720_2.html) 318 | 319 | 320 | ### 车联网 321 | 322 | 车联网就是:车内是个局域网,车跟车组成车际网,车网与互联网相连,三者基于统一的协议,实现人、车、路、云之间数据互通,并最终实现智能交通、智能汽车、智能驾驶等功能。 323 | 324 | **车联网能够满足什么需求** 325 | 326 | 你是否有过这样的经历。自己所在的车道堵的一动不动,旁边车道上的汽车一辆又一辆的快速驶过,其实只是因为你所在车道的前方发生了事故。如果未来云端保存了高精度地图数据,每辆汽车都具备GPS定位和一颗“眼睛”,汽车就可以将发现的异常情况上报云端。那么,当你即将驶过事发路段时,就可以收到预警,提前变更你的行驶车道或路线,也就能够躲避拥堵。同时道路上的每一辆汽车都可以变成一名“电子警察”,这个时候交通违法甚至整个社会的犯罪率都会大幅降低(可参考“速7”中的天眼系统)。 327 | 328 | 每天下班准备回家时,你是否希望既不用提前出门,又能够最快的到家?当车联网时代到来后,只要每一位车主在下班前,将希望到家的时间输入手机App,通过云端大数据的计算,分析出你经过的每条路段,在某个时间可能产生的车流量,再结合道路通过能力及周边道路状况,计算出最适合您的出发时间和路线。只要按照导航规划路线与速度行驶,就可以实现早回家的愿望。 329 | 330 | 你在开车使用手机时,是否担心危险?车联网到来后,汽车能够通过自身传感器主动探索周边环境。当发现可能发生碰撞时,立即发出预警甚至主动为您减速,以规避危险。当你听音乐时有电话呼入,汽车可以自动降低音量。 331 | 332 | 当车内空气质量恶化时,汽车可以根据外面天气情况(甚至太阳所处位置、风向),自动为你打开合适的车窗或启动空调。当你等红灯时发现旁边兰博基尼里坐了一位美女时……总之,车联网时代到来后,驾驶会变得更加智能、更有乐趣。 333 | 334 | **我们现在处于车联网的什么阶段?** 335 | 336 | 如果将车联网划分阶段,我觉得可以参考对移动互联网的发展历程。 337 | 第一阶段,手机(不是指大哥大)在2000年前进入国内,那时只能满足高端人群的需求。 338 | 339 | 进入到第二阶段后,手机平民化,价格降低、产能提升、品牌增多,并逐渐覆盖了每个家庭。但此时,手机满足的依旧是打电话、发短信点对点的沟通需求。 340 | 341 | 接下来到了第三阶段,诺基亚、moto、多普达等智能手机诞生,有了wap、移动梦网和java应用。手机不仅可以满足点对点的沟通需求,还能够把个人与世界连接在一起,此时移动互联网露出水面。 342 | 343 | 最后到了第四阶段,也就是现在,伴随科研能力的提升、基础设施的完善、传感器进一步变小以及工业制造水平的快速发展,苹果安卓智能手机所带来的触屏交互体验横空出世,移动互联网真正到来,并汹涌的席卷一切。 344 | 345 | 346 | 我认为,现在的车联网还只是刚刚进入第三阶段。即已经完成了基本普及,并初具联网能力,但还远未达到大规模爆发的时代。以苹果、谷歌、BAT为代表的互联网巨头,以及无数家伴随智能硬件大潮而起的新创公司,在2014年纷纷进入车联网领域,甚至包括像大众、通用、PSA集团之类的整车厂也开始逐渐转型。其大体分为三条路径: 347 | 348 | 1. 在OBD和Can总线上做文章:最典型的就是腾讯2014年5月推出的“腾讯路宝盒子”。通过插在汽车上的路宝盒子(OBD设备)获取汽车数据,如里程、油耗、速度、驾驶行为等信息。将这些信息通过路宝盒子传到手机、云端,并经过大数据分析后,为车主提供服务与应用。这条道路一般能实现的功能大致有:保养推送、UBI保险、驾驶行为纠正以及车辆远程监控(门、窗、灯的状态)。如果能够进一步获得私有协议及CAN总线操控能力,还可以实现对车辆的控制。比如,炎炎夏日,只要出门前通过手机查看一下车内温度,并遥控空调的开启,就可以在进入车内时体验冰爽的感觉。这条路径受到的限制是,不同厂家,OBD及Can总线通讯协议各不相同,并且对外完全封闭,单纯由互联网公司来做,很难达到普适性(路宝盒子就在车辆适配这一方面做了大量工作,现在已经可以兼容市面超过90%的车辆)。如果单纯依靠破解,又会产生法律及安全风险。但种受限,需要具备强大的谈判能力或更为统一的汽车行业标准出台,才会有所突破。 349 | 2. 在车机上做文章:国外以苹果carplay、android auto为代表,国内以百度Carnet为代表的产品。可以将手机的内容投射到车机屏幕上,让车机更具灵活性和延展性,旨在改变车内的视听娱乐体验。但我个人觉得,这条路与目前智能电视所面临的问题相似。它只解决了从手机屏幕向另一块屏幕转移的问题,却没有根本的解决人机交互问题。依旧需要用户用手指在车机屏幕或手机屏幕上点点划划进行操作,即便具备语音操控功能,但在现阶段能提供的帮助也还有限。从长远来看,它需要的是车内传感器、人机交互和肢体操控能力的进一步发展与普及,并结合HUD技术(Head Up Display),让用户的眼睛能够脱离开手机、车机,只需要专心盯着路面即可实现任何操作。(可参考漫威系列,钢铁侠、神盾据特工等作品里的情节) 350 | 3. 直接在汽车上做文章:以阿里&上汽、乐视汽车为代表的产品,甚至直接打出无人驾驶概念。估计短时间还无法问世,比较期待。(我个人认为,无人驾驶是车联网的终极形态)以上无论哪一种方式,相对于整个车联网而言,都只是万里长征的一小步。 351 | 352 | 353 | **车联网什么时候能够到来?** 354 | 355 | 需要具备以下客观环境: 356 | 357 | 1. 更统一的汽车行业标准出台。不同汽车品牌之间统一通信协议及标准配置,并可以有条件的对外开放,这样就可以解决互联网普适性的问题。“封闭的大门”早晚会被互联网攻破。 358 | 2. 高精度地图的全面普及。每条道路的宽度、坡度、周边环境全部数字化。云端能够获知每辆汽车最微小的变化。 359 | 3. 传感器技术需要进一步发展。激光、雷达、摄像等设备的价格和体积还需降低,同时能力需要更强。军方的顶尖技术能够进入民用领域。 360 | 4. 更快的网络传输及ECU处理速度。车辆与云端交互的时间变得更短,车内本地的计算能力大幅增强。 361 | 5. 后装智能车载硬件的快速发展与普及,对车厂形成倒逼。如同移动互联网汹涌来袭,传统汽车厂商企业不得不接受这种改变,并主动寻求合作,探寻车联网的发展之路。 362 | 6. 市场保有汽车快速淘汰,新车都安装先进的传感器。(如果,马车与汽车如果同在高速上奔跑,即便开着法拉利,也很难提升得了速度。) 363 | 364 | 365 | 366 | ###### 车联网产业分析 367 | 368 | 车联网产业链的三层架构。完整的车联网产业链涉及的环节较多,主要包括通信芯片 /模块提供商、外部硬件提供商、 RFID及传感器提供商、系统集成商、应用设备和软件提供商、电信运营商、服务提供商、汽车生产商等;总体而言,可以分为运营层、应用层和管理层三层架构。 369 | 370 | ![MacDown logo](./car1.jpg) 371 | 372 | TSP占据产业链核心地位。Telematics服务提供商即 TSP( Telematics Service Provider)在Telematics 产业链居于核心地位,上接汽车、车载设备制造商、网络运营商,下接内容提供商。谁掌控了 Telematics服务提供商,谁就能掌握 Telematics产业的控制权,因此, Telematics服务提供商也成为了汽车制造商、电信运营商、 GPS运营商及汽车影音导航厂商力争的角色。 373 | 374 | **TSP目前格局:汽车厂商 VS 互联网公司汽车厂商:** 375 | 376 | 车厂把主要精力放在车载娱乐系统的屏幕上,作为汽车的生产者,它们可以深入地整合软硬件,但同时研发周期也更长。车厂的互联网解决方案肯定会更全面、更深入。互联网巨头:苹果、谷歌相信依靠自身的强大影响力,足以与车厂抗衡,最终进入汽车的中控中心;木有操作系统的互联网公平年公司如腾讯的路宝等于切入了车联网的后装市场,用配件来实现汽车智能。与车企相比,互联网的高速迭代、大数据的云端分析能力是互联网公司的优势。可能的趋势:汽车厂商的系统并未统一,各大厂商都有自己的车载系统,通用有On-start、宝马有idrive等等,车主并不愿意去熟悉不同的系统,既然在手机和平板电脑上iOS、Android已经双雄通吃,车联网系统未来很有可能与手机趋同。 377 | 378 | **TSP核心——OBD和地图导航OBD**:OBD是车载诊断系统(On-Board Diagnostic)的英文缩写。通俗来说,OBD就像医生的听诊器和B超设备,医生通过听诊器可以知道你的心跳,通过B超可以知道心脏功能是否正常。OBD 装置监测多个系统和部件,包括 发动机、催化转化器、颗粒捕集器、氧传感器、排放控制系统、燃油系统、GER 等 。在车联网环境下,汽车驾驶过程的每一分钟都会创造大量的数据(工业互联网概念):行驶速度、司机操作、设备运行等情况都会转化为数据,有效地收集并利用这些数据将为汽车企业创造大量的价值,主要用途如下: 379 | 380 | 1. 通过监测汽车运行状况降低保修成本,提高安全性能,提示车主维修、降低行驶速度或提示车厂及时召回; 381 | 2. 根据车辆信息推广增值服务,包括基于车主情况、车辆位臵、行车习惯定向推送的广告; 382 | 3. 出售数据给第三方,如保险公司、广告公司以及4S 店**,北美最大的汽车保险公StateFarm 与车联网服务提供商Hughes 早在2011 年便展开合作,给保险以更精准的定价**。 383 | 384 | **地图导航:**地图的能力和车联网服务息息相关。在传统车载设备中,地图往往是离线的,起到的作用局限于目的地导航。而在车联网中,地图将成为用户生活服务的入口,地图的重要性大幅提升。地图作为O2O的入口,受到各大互联网公司的重视。从阿里投资易图通、全资收购高德,到百度收购长地万方,腾讯收购科菱航睿等,都表明了地图的重要性。5月22日,腾讯更是大手笔地宣布投资11.73亿元人民币收购地图厂商四维图新7800万股。 385 | 386 | 387 | **车联网主流模式比较** 388 | ![MacDown logo](./car2.jpg) 389 | 390 | 腾讯也积极介入OBD市场,推出路宝盒子(路宝盒子的工作原理是,将盒子插入汽车的通用接口之后,手机便可以通过蓝牙的方式与之连接,10秒钟之内汽车的数据会借由路宝App传到腾讯的云端,手机在其中起到了传输介质的作用。通过云端的数据分析,为用户提供即时的导航、车辆诊断和油耗分析等服务。比如,当汽车出现故障,手机会告诉用户故障的详情和紧急程度。对于腾讯路宝而言,与车厂相比最大的竞争优势在于,消费者想要感受车联网的便捷,并不需要花高价购买新车,或者更换一套全新的车载系统,只需将一个智能硬件插在车内。),用于监测汽车驾驶数据. 391 | 392 | 布局路宝和入股四维图新表明了腾讯全力抢占车联网入口的决心 ,路宝走后装模式,而四维深耕前装车厂车联网,二者本身并无直接冲突 。对腾讯来说,抢占入口,获得用户,搭建生态系统是其最核心目的。“互联网公司布局车联网,主要是为了从地图切入,进而植入本地生活服务、地理位置服务、搜索等应用,可谓醉翁之意不在酒。”腾讯的马喆人也承认,目前腾讯地图上的POI(Point of Interest)信息和点评内容皆来自于大众点评,将来路宝和大众点评之间存在着非常大的想象空间。也就是OBD+LBS模式。 393 | 394 | 395 | ###### 商业模式 396 | 397 | OBD(车载诊断系统)的商业模式主要有两个:精准营销和保险市场。 398 | 399 | (1)精准营销:解决用户汽车后市场痛点,获取精准营销数据
400 |   用户痛点在于售后服务价格不透明。以往OBD获得的汽车诊断信息主要被4S店或维修厂商获得,现在通过后装以及部分前装市场提供的OBD抬头显示器,消费者 也可以获得汽车的数据,知道故障原因,避免被维修厂商漫天要价;发生故障时可以预警,避免重大事故。OBD硬件现在普遍盈利能力有限,主要通过OBD获得 的用户数据可以为广告商等提供更精准的营销服务,例如OBD可以记录用户经常到达的地点,提供该地点周围的生活类广告。 401 | 402 | (2)保险市场:激励规范驾驶数据用作分级保险
403 |   404 |  羊毛出在狗身上,猪来买单的盈利模式。车联网产业链划分为5个角色:车厂、车主、网络运营商、技术提供商(软硬件)、内容提供商。这其中技术提供商可以通过 补贴用户来抢占汽车内的显示屏市场,从而抢占用户,再利用用户数据实现盈利。目前最受追捧的便是与保险合作,将用户驾驶行为习惯数据销售给保险公司,保险 公司根据数据设立分层级的保费机制,激励规范驾驶行为。因此,消费者获得的羊毛出在技术提供商身上,但由保险公司为其买单。 405 | 406 | ![MacDown logo](./car3.jpg) 407 | 408 | ### 其他一些物联网新兴的点: 409 | [阿里巴巴、中兴、中国联通共同打造物联网区块链框架](http://mt.sohu.com/20170407/n487072946.shtml) 410 | 411 | [IBM:为认知 IoT 应用程序实现区块链](https://www.ibm.com/developerworks/cn/cloud/library/cl-blockchain-for-cognitive-iot-apps-trs/index.html) 412 | 413 | [如何用区块链技术加密 IoT](https://www.linkedin.com/pulse/securing-internet-things-iot-blockchain-ahmed-banafa) 414 | 415 | [Android Things给物联网设备带来基于TensorFlow的机器学习和计算机视觉](http://www.infoq.com/cn/news/2017/02/android-things-dev-preview-2) 416 | 417 | [IBM和福布斯发布的2017年物联网5大发展趋势](http://mt.sohu.com/it/d20170204/125460971_472880.shtml) 418 | 419 | [2016年物联网行业十大新闻事件](http://iot.ofweek.com/2016-12/ART-132209-8440-30085634.html) 420 | 421 | [解读2016物联网:巨头割据,安全问题凸显](http://www.infoq.com/cn/articles/2016-review-iot) 422 | 423 | [2016年最具影响力的十大并购](http://www.leiphone.com/news/201611/XF2WXjr3byBfgZEr.html) 424 | 425 | [可由语音控制外加能进行人脸识别的自主飞行无人机](https://www.oreilly.com.cn/ideas/?p=717) 426 | 427 | [爱立信成功演示基于NB-IoT(窄带物联网)的智能停车系统](http://network.chinabyte.com/195/13779195.shtml) 428 | 429 | ## 技术篇 430 | 431 | 432 | ### 开发 433 | **一.** [intel IoT平台](https://www.codeproject.com/articles/895740/intel-iot-developer-kit-v-is-here) 434 | 435 | 英特尔® IoT,它提供一个端到端平台,使尚未连接的得以连接 — 允许来自数十亿台设备、传感器和数据库的数据能跨行业被安全地收集、交换、存储和分析。
436 |
437 | 主要优点: 438 | 439 | 1. 安全性:在数据最容易被攻击的地方开始实施基于硬件和软件的安全性的紧密结合,以提供受信任的数据。 440 | 2. 互操作性:利用技术实施无缝通信,帮助加快上市速度,并降低部署和维护 IoT 解决方案的成本。 441 | 3. 可扩展性:以英特尔® Quark™ 到英特尔® 至强™ 和基于英特尔® 处理器的设备、网关和数据中心解决方 案实现从边缘到云可扩展的计算。 442 | 4. 可管理性:从传感器到数据中心获得先进的数据管理和分析。 443 | 444 | **二.** [Google Android Things IoT开发平台](https://developer.android.com/things/hardware/index.html)
445 | Google 希望将 Android 普及到用户家中的每一个角落,而伴随着物联网(IoT)的大潮,该公司刚 刚推 出了全新的 Android Things 平台。 446 | Build connected devices for a wide variety of consumer, retail, and industrial applications 是他们的目标。 447 | 主要优点:
448 | 449 | 1. Get Familiar with Android Development 和android开发很相似,android开发者上手快,能吸引大量的开发人员。 450 | 2. 支持众多硬件厂商产品 `Intel Edison` `Intel Joule` `NXP Argon` `NXP Pico` `Raspberry Pi 3`等等 451 | ,此外,不仅支持 Google 自家的平台,同时也对 iOS 持有包容的态度 452 | 3. 在 Android Things 上的开发,可以通过相同的 Android 标准开发工具完成。如此一来,有经验的开发者们可以很快搞定一款新产品并将之推向市场 453 | 454 | 455 | **三.** [微软Azure IoT平台](https://azure.microsoft.com/en-us/suites/iot-suite/) 456 | 457 | 平台定位: 458 | 连接设备、其它 M2M 资产和人员,以便在业务和操作中更好地利用数据。[更多说明](http://www.cnblogs.com/kinging/articles/5865037.html) 459 | 460 | 461 | **四. **[IMB Watson IoT Platform](https://www.ibm.com/internet-of-things/platform/watson-iot-platform/) 462 | 463 | * Watson IoT Platform 提供对 IoT 设备和数据的强大应用程序访问,可快速编写分析应用程序、可视化仪表板和移动 IoT 应用程序。 464 | * Watson IoT Platform可以执行强大的设备管理操作,并存储和访问设备数据,连接各种设备和网关设备。 465 | * Watson IoT Platform 通过使用 MQTT 和 TLS,提供与设备之间的安全通信。 466 | * Watson IoT Platform使应用程序与已连接的设备、传感器和网关进行通信并使用由它们收集的数据。应用程序可以使用实时 API 和 REST API 来与设备进行通信。 467 | 468 | 469 | **五.** [亚马逊AWS IoT](https://aws.amazon.com/cn/iot-platform/) 470 | 471 | AWS IoT 是一款托管的云平台,使互联设备可以轻松安全地与云应用程序及其他设备交互。AWS IoT 可支持数十亿台设备和数万亿条消息,并且可以对这些消息进行处理并将其安全可靠地路由至 AWS 终端节点和其他设备。借助 AWS IoT,您的应用程序可以随时跟踪所有设备并与其通信,即使这些设备未处于连接状态也不例外。 472 | 473 | 借助 AWS IoT,您可以轻松使用 AWS Lambda、Amazon Kinesis、Amazon S3、Amazon Machine Learning、Amazon DynamoDB、Amazon CloudWatch、AWS CloudTrail 和内置 Kibana 集成的 Amazon Elasticsearch Service 等 AWS 服务来构建 IoT 应用程序,以便收集、处理和分析互连设备生成的数据并对其执行操作,且无需管理任何基础设施. 474 | 475 | 476 | **六.** [Ablecloud物联网自助开发和大数据云平台](http://www.cnblogs.com/ibrahim/p/ablecloud-iot.html) 477 | 478 | 平台定位 479 | 面向IoT硬件厂商,提供设备联网与管理、远程查看控制、定制化云端功能开发、海量硬件数据存储与分析等基础设施,加速硬件实现联网智能化。 480 | 481 | 482 | **七.** [QQ物联](http://iot.open.qq.com/) 483 | 484 | “QQ物联智能硬件开放平台”发布,将QQ账号体系及关系链、QQ消息通道能力等核心能力,提供给可穿戴设备、智能家居、智能车载、传统硬件等领域合作伙伴,实现用户与设备及设备与设备之间的互联互通互动,充分利用和发挥腾讯QQ的亿万手机客户端及云服务的优势,更大范围帮助传统行业实现互联网化 485 | 486 | 487 | **八.** [百度 IoT Hub](https://cloud.baidu.com/product/iot.html) 488 | 489 | 物接入(IoT Hub)是一个全托管的云服务,帮助建立设备与云端之间安全可靠的双向连接,以支撑海量设备的数据收集、监控、故障预测等各种物联网场景。 490 | 491 | **九.**[华为LiteOS](http://www.huawei.com/minisite/iot/cn/liteos.html) 492 | 493 | 494 | Huawei LiteOS 是华为面向IoT领域,构建的"统一物联网操作系统和中间件软件平台",以`轻量级`(内核小于10k)、`低功耗`(1节5号电池最多可以工作5年),`快速启动`,`互联互通`,`安全`等关键能力,为开发者提供 "一站式" 完整软件平台,有效降低开发门槛、缩短开发周期。 495 | 496 | Huawei LiteOS 目前主要应用于智能家居、穿戴式、车联网、智能抄表、工业互联网等 IoT 领域的智能硬件上。 497 | 498 | [相关代码地址](https://github.com/LITEOS) 499 | 500 | **十.** [Arduino](http://www.huawei.com/minisite/iot/cn/liteos.html) 501 | 502 | Arduino是一款便捷灵活、方便上手的开源电子原型平台。包含硬件(各种型号的Arduino板)和软件(Arduino IDE) 503 | 504 | **跨平台** 505 | Arduino IDE可以在Windows、Macintosh OS X、Linux三大主流操作系统上运行,而其他的大多数控制器只能在Windows上开发。 506 | 507 | **简单清晰** 508 | Arduino IDE基于processing IDE开发。对于初学者来说,极易掌握,同时有着足够的灵活性。Arduino语言基于wiring语言开发,是对 avr-gcc库的二次封装,不需要太多的单片机基础、编程基础,简单学习后,你也可以快速的进行开发。 509 | 510 | **开放性** 511 | Arduino的硬件原理图、电路图、IDE软件及核心库文件都是开源的,在开源协议范围内里可以任意修改原始设计及相应代码。 512 | 513 | **发展迅速** 514 | Arduino不仅仅是全球最流行的开源硬件,也是一个优秀的硬件开发平台,更是硬件开发的趋势。Arduino简单的开发方式使得开发者更关注创意与实现,更快的完成自己的项目开发,大大节约了学习的成本,缩短了开发的周期。 515 | 因为Arduino的种种优势,越来越多的专业硬件开发者已经或开始使用Arduino来开发他们的项目、产品;越来越多的软件开发者使用Arduino进入硬件、物联网等开发领域;大学里,自动化、软件,甚至艺术专业,也纷纷开展了Arduino相关课程。 516 | 517 | 518 | **十一.** [MQTT](http://mqtt.org/) 519 | 520 | MQTT是M2M/IoT的网络连接协议: 521 | MQTT is a **machine-to-machine (M2M)/"Internet of Things" connectivity protocol**. It was designed as an extremely lightweight publish/subscribe messaging transport. It is useful for connections with remote locations where a small code footprint is required and/or network bandwidth is at a premium. 522 | 523 | 524 | **十二.** [LPWAN](https://en.wikipedia.org/wiki/LPWAN#Platforms_and_technologies) 525 | 526 | 低功耗广域物联网(LPWAN)是为物联网应用中的M2M通信场景优化的,由电池供电的,低速率、超低功耗、低占空比的,以星型网络覆盖的,支持单节点最大覆盖可达100公里的蜂窝汇聚网关的远程无线网络通讯技术。 527 | 该技术是近年国际上一种革命性的物联网接入技术,具有远距离、低功耗、低运维成本等特点,与WiFi蓝牙、ZigBee等现有技术相比,LPWAN真正实现了大区域物联网低成本全覆盖. 528 | 529 | 530 | **十三.** [NB-IoT](http://baike.baidu.com/item/NB-IoT/19420464) 531 | 532 | 基于蜂窝的窄带物联网(Narrow Band Internet of Things, NB-IoT)成为万物互联网络的一个重要分支。NB-IoT构建于蜂窝网络,只消耗大约180KHz的带宽,可直接部署于GSM网络、UMTS网络或LTE网络,以降低部署成本、实现平滑升级。[1] 533 | 534 | NB-IoT是IoT领域一个新兴的技术,支持低功耗设备在广域网的蜂窝数据连接,也被叫作低功耗广域网(LPWA)。NB-IoT支持待机时间长、对网络连接要求较高设备的高效连接。据说NB-IoT设备电池寿命可以提高至至少10年,同时还能提供非常全面的室内蜂窝数据连接覆盖. 535 | 536 | 目前部分共享单车已经采用该技术使得自行车具备位置定位的防盗功能,以及其他的信息上报和跟踪功能。 537 | 538 | **十四.** [Zigbee](https://en.wikipedia.org/wiki/ZigBee) 539 | 540 | ZigBee是基于IEEE802.15.4标准的低功耗局域网协议。根据国际标准规定,ZigBee技术是一种短距离、低功耗的无线通信技术。这一名称(又称紫蜂协议)来源于蜜蜂的八字舞,由于蜜蜂(bee)是靠飞翔和“嗡嗡”(zig)地抖动翅膀的“舞蹈”来与同伴传递花粉所在方位信息,也就是说蜜蜂依靠这样的方式构成了群体中的通信网络。其特点是近距离、低复杂度、自组织、低功耗、低数据速率。主要适合用于自动控制和远程控制领域,可以嵌入各种设备。简而言之,ZigBee就是一种便宜的,低功耗的近距离无线组网通讯技术。ZigBee是一种低速短距离传输的无线网络协议。ZigBee协议从下到上分别为物理层(PHY)、媒体访问控制层(MAC)、传输层(TL)、网络层(NWK)、应用层(APL)等。其中物理层和媒体访问控制层遵循IEEE 802.15.4标准的规定。 541 | 542 | **十五.** [Liota](https://github.com/vmware/liota) 543 | 544 | Liota是VMware开源的IoT网关应用程序框架。通过在IoT设备与云应用之间建立安全的网关通信,分析输入数据流并控制远程设备,Liota框架使得应用程序的开发变得更简单。除了开源,Liota还实现了供应商无关性。Iyer称:“普适的Liota可以通过不同模块与任何数据中心组件交互,支持任何IoT网关所使用的所有传输协议,Liota可以与任何其他IoT系统配合使用,对供应商的选择无要求,这一特性有效解决了IoT市场上面临的一个最大问题。” 545 | 546 | Liota SDK使用Python语言开发,可部署在任何支持Python的网关平台上。 547 | 548 | **十六.** [Gladys](https://github.com/GladysProject/Gladys) 549 | 550 | 是一个基于 Node.js 与 Raspberry Pi 的智能家居助手。它可以连接到你家中的所有设备,以及日历,并且还可以为你提供大量的 API。它可以在你起床前唤醒 Philips Hue、准备好音乐,还能准备好咖啡机,打开百叶窗等等的操作。 551 | 552 | **十七.**[netdata](https://github.com/firehol/netdata) 553 | 554 | NetData 是一个用于分布式实时性能和健康监控的系统,可以用于实时监控物联网设备。它使用了现代化的交互式Web仪表板,为其运行的系统(包括 Web 和数据库服务器等应用程序)提供无与伦比的实时洞察信息。 555 | 556 | **十八.** [Docker、MQTT、InfluxDB 与 Grafana 搭分布式物联网平台](http://air.imag.fr/index.php/Developing_IoT_Mashups_with_Docker,_MQTT,_Node-RED,_InfluxDB,_Grafana) 557 | 558 | 该教程的目标是,快速构建从传感器,到数据与实时分析的最小 IoT 技术栈。里面用到的硬件有,各类传感器、嵌入式IoT网关、Raspberry Pi、AWS 云服务等。教程中使用了 Grafana 提供数据分析服务、Node-RED 作为可视化编程工作、InfluxDB 作为数据库、运行在 Raspberry Pi 上的 Docker 作为快速部署工作。 559 | 560 | **十九.** [Progressive Web Apps(google PWA) and the IoT](https://iceddev.com/blog/introducing-pagenodes/) 561 | 562 | 文章介绍了完全基于浏览器的物联网平台Pagenodes设计思路,它基于node-red、并用Google的Progressive Web App理念实现。通过使用Progressive Web App,用户可以直接在浏览器上Web USB访问USB,通过Web Bluetooth访问蓝牙,通过Web Push直接推送消息等等功能。 563 | 564 | 565 | **二十.** [IoT 开发最佳实践](https://www.ibm.com/developerworks/cn/iot/iot-mobile-practices-iot-success/index.html?ca=drs) 566 | 567 | 在设计物联网系统的时候,需要注意到的API设计、原型设计、如何连接等问题。设计API时,我们需要解耦和服务化API。在设计物联网系统原型时,我们应该尽快构建出原型,并授受反馈来改进系统。同时,我们还需要考虑在不同的环境下使用网络的问题。 568 | 569 | ### 安全 570 | 571 | 随着互联网和物联网的普及人们对于信息安全的日益重视,物联网安全远远不止关系家里的温度计、智能灯泡等智能家居小设备,还有特斯拉这样的联网汽车,更可能涉及到更加严重的后果。美国遭受黑客攻击最频繁的公司就是智能电网Smart Grid,而金融机构和医院更是经常被黑客窃取数据或者威胁敲诈。目前美国绝大部分服务都已经联网,但企业IT保护能力却非常孱弱。举例来说,可穿戴智能胰岛素泵Insulin Pump极大化解了糖尿病人的烦恼,但这款设备被黑客攻击之后甚至有可能远程威胁患者生命安全;如果像不久前的《速度与激情8》中的场景,利用汽车互联的漏洞控制汽车的场景在现实中上演,那后果不可想象。 由于目前物联网还没有统一的标准和协议,现有的物联网在安全方面还有所欠缺,也正是这个原因催生了很多专注于物联网安全的公司和产品。 572 | 573 | 1. **[IoT技术架构与安全威胁](http://www.broadview.com.cn/article/62)** 574 | 575 | 1. [ZingBox](https://www.zingbox.com/).Enable the interner of trusted things 576 |
当前的防火墙安全防护思路是“发现坏人,分析病毒特性,然后下次拒绝侵入”,而他们的思路是“物联网设备通常功能和简单,所以不能简单去发现和抓住坏人,而是基于云端搜集信息进行行为分析,判断好人(正常运转情况)该是什么状况,建立数学模式进行描述。这样一旦发现不正常状况就进行上报封堵,然后通过沙盒等机制包括设备的健康运转。” 577 | 578 | Zingbox已经完成了产品Demo,目前正在与高通、Marvel、博通和飞思卡尔等芯片巨头商谈合作,希望自己的产品可以并入到系统集成商推向市场。他们还希望与家庭网关设备合作,将自己产品装在这些中间设备上。 579 | 580 | 2. 360。 581 | 在IoT时代,从云端数据、网络安全、硬件本身、传感器等, 安全性存在于智能家居的方方面面,不仅关系到人的信息和隐私安全,甚至也关系到人生健康安全,可见,智能家居安全问题无疑更为严峻。因此,360启动智能家居战略,基于现有安全、大数据、云服务等核心竞争优势,从平台开放、合作 扶持等多角度进行产业及跨界的聚合,将助力智能家居市场安全产业链条更快更好地发展。此举也符合360未来对自身的定位,**即从最大的“互联网安全公司”转变成为最大的“安全互联网公司”。 582 | ** 583 | 584 | 3. [腾讯领御守护计划](http://slab.qq.com/news/kuaixun/1007.html) 585 | 586 | 计划方案包括硬件认证设备、TUSI认证标准和领御守护平台,重点关注打造人与人、人与机器、机器与机器之间的安全应用与生态,为移动支付、智能家居行业搭建了安全开放平台。其中,[QKey](http://qkey.qq.com/)是作为用户在物联网时代的安全守护者的价值而存在;基于硬件和密码学算法的一套身份认证及移动支付鉴权标准——TUSI认证标准,则主要是为产业链各方提供了设计软件、智能硬件和智能家居设备的统一标准,让物联网共享统一的安全防护能力,与领御守护平台一起为整个物联网产业提供了统一的标准和安全防护能力。 587 | 588 | 3. [Project Sopris](https://www.microsoft.com/en-us/research/project/sopris/) 589 | 590 | 微软推出主打安全性的 Project Sopris 低成本物联网设备. 591 | 592 | 4. [OK Bitcoin Fullnode OS ](https://github.com/BitcoinFullnode/ROKOS-OK-Bitcoin-Fullnode) 593 | 594 | OK Bitcoin Fullnode OS 是一个面向 Raspberry Pi、Pine64 + 及IoT设备的加密操作系统。它在 Core OS、 Flavors OS 上集成了不同的加密货币/技术,如:Bitcoin, OKCash, Open Bazaar。 595 | 596 | 5. [如何用区块链技术加密 IoT](https://www.linkedin.com/pulse/securing-internet-things-iot-blockchain-ahmed-banafa) 597 | 598 | 文章介绍了如何使用区块链技术加密 IoT。通过利用区块链技术,物联网解决方案可以在IoT网络中的设备之间实现安全,无信赖的消息传递。 599 | 600 | 6. IBM Watson IoT Platform 的 API 安全性设计 601 | 602 | 1. [保护 IoT 设备和网关](https://www.ibm.com/developerworks/cn/iot/library/iot-trs-secure-iot-solutions1/index.html) 603 | 604 | 605 | 2. [保护在网络上传输的 IoT 数据](https://www.ibm.com/developerworks/cn/iot/library/iot-trs-secure-iot-solutions2/index.html) 606 | 607 | 3. [保护 IoT 应用程序](https://www.ibm.com/developerworks/cn/iot/library/iot-trs-secure-iot-solutions3/index.html) 608 | 609 | 7. [亚马逊和 Microchip 合作,开发 IoT 设备安全芯片](http://www.leiphone.com/news/201612/IEbGf4FDzzpeZPcT.html) 610 | 611 | AWS-ECC508 芯片的设计方案,是为物联网设备和云基础设施提供端到端的安全保护。它利用了亚马逊的人工身份验证系统,后者能在任何指令、数据被接收之前,为云服务和设备验明正身。而这基于秘钥:直到现在,创建这样的密码身份要依靠制造商,而它们一般是为设备品牌代工的生产厂商。它们秘密生成密钥,然后安全、隐蔽地沿着制造链传递下去。但 AWS-ECC508 芯片另辟蹊径:它会生成自己的密钥,由亚马逊认证。 612 | 613 | 8. [美国国土安全部(DHS)发布《物联网安全的战略原则》](https://www.easyaq.com/news/1441298949.shtml) 614 | * 在设计阶段结合安全:“经济驱动力使得企业将设备推入市场时很少考虑安全。这给恶意攻击者创造大量机会操控联网设备的信息流”。 615 | * 启用安全更新和漏洞管理:即使安全从一开就内置存在,但在产品部署后发现产品漏洞很常见。这些漏洞能通过补丁、安全更新和漏洞管理策略缓解。 616 | * 建立在可靠的安全最佳实践之上:传统网络安全中许多经过验证的实践可以作为提升物联网安全的出发点。 617 | * 根据影响优先考虑安全措施:数据泄露的风险和后果大不相同,这取决于联网设备。因此,专注破坏、泄露或恶意活动的潜在后果对决定物联网生态系统的安全方向尤为重要。 618 | * 提升透明度:在可能的情况下,开发人员和制造商需要了解供应链,因此他们能识别软件和硬件组件,并了解任何相关漏洞。增强意识能帮助制造商和工业消费者识别安全措施应用的位置和具体方法。 619 | * 连接需仔细谨慎:考虑物联网的使用和物联网被破坏的相关风险,物联网消费者,尤其工业企业应该仔细并谨慎考虑是否需持续连网。 620 | 621 | 9. [Mirai物联网僵尸攻击深度解析](http://www.freebuf.com/articles/terminal/117927.html) 622 | 623 | 文章从黑客Anna-senpai在GitHub上开源的Mirai源码,来对Mirai物联网僵尸攻击进行深度解析。Mirai通过扫描网络中的Telnet等服务来进行传播,其感染通过黑客配置服务来实施,这个服务被称为Load。黑客的另外一个服务器C&C服务主要用于下发控制指令,对目标实施攻击。 624 | 625 | 10. [golix](https://github.com/Muterra/doc-golix) 626 | 627 | 项目设计了一个端对端物联网安全协议Golix。它为基于代理的分布式网络提供了一个端对端加密方案,特别适合于物联网设备。 628 | 629 | 11. [matrixssl](https://github.com/matrixssl/matrixssl) 630 | 631 | matrixssl是一个面向物联网设备的SSL/TLS实现,并能为每个连接都保持低功耗。其只需要不到50kb的硬盘空间,它的客户端和服务端都可以支持通过 TLS 1.2、交叉认证、会话恢复以及RSA,ECC,AES,SHA1,SHA-256等的实现。 632 | 633 | 12. [创业公司Afero推出解决物联网通信安全的平台](https://www.afero.io/release/afero-launches-platform-to-securely-connect-the-internet-of-things-in-the) 634 | 635 | 该平台使用一个安全的蓝牙智能模块同连接到Afero云的移动手机进行通信。蓝牙智能模块同Afero云之间的所有通信都会加密 636 | 637 | 13. [MQTT安全篇](http://dataguild.org/?p=6866) 638 | 639 | 文章介绍了如何在使用MQTT协议时使用一些加密手段,如TLS、认证、用户名密码、证书等等 640 | 641 | 642 | -------------------------------------------------------------------------------- /IoT/car1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/car1.jpg -------------------------------------------------------------------------------- /IoT/car2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/car2.jpg -------------------------------------------------------------------------------- /IoT/car3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/car3.jpg -------------------------------------------------------------------------------- /IoT/city1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/city1.jpg -------------------------------------------------------------------------------- /IoT/city2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/city2.jpg -------------------------------------------------------------------------------- /IoT/city3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/city3.jpg -------------------------------------------------------------------------------- /IoT/health1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/health1.jpg -------------------------------------------------------------------------------- /IoT/health2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/health2.jpg -------------------------------------------------------------------------------- /IoT/home1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/home1.jpg -------------------------------------------------------------------------------- /IoT/home2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/home2.png -------------------------------------------------------------------------------- /IoT/xiaomi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/xiaomi.jpg -------------------------------------------------------------------------------- /IoT/物联网-体系.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/物联网-体系.xmind -------------------------------------------------------------------------------- /IoT/移动天镜目前还缺的功能点和资源.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/IoT/移动天镜目前还缺的功能点和资源.png -------------------------------------------------------------------------------- /andorid 插件化和热修复/README.md: -------------------------------------------------------------------------------- 1 | # android插件化技术 2 | 3 | 之前写在[单独的博客](https://github.com/carl1990/Android-DynamicAPK-Plugin)上面 -------------------------------------------------------------------------------- /android M 运行时权限/650671-3297bcf7a0e7f34b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/android M 运行时权限/650671-3297bcf7a0e7f34b.png -------------------------------------------------------------------------------- /android M 运行时权限/README.md: -------------------------------------------------------------------------------- 1 | #Android M 运行时权限 2 | 3 | Android M 已经发布很久时间了,对于一些最新特性比较感兴趣,刚好最近入手了一台入手了一台nexus 5x ,在上面安装了公司的项目发现直接跪了,后来一看log是由于一个权限引起的,但是我一看所需要的权限我已经在manifest中申请了啊,后来一想Android M对应用的权限作出了一些改变,接下来就让我们来学习一下Android M 运行时权限 4 | 5 | ## 新的运行时权限 6 | android的权限系统一直是首要的安全概念,因为这些权限只在安装的时候被询问一次。一旦安装了,app可以在用户毫不知晓的情况下访问权限内的所有东西。 7 | 难怪一些坏蛋利用这个缺陷恶意收集用户数据用来做坏事了! 8 | android小组也知道这事儿。7年了!权限系统终于被重新设计了。在android6.0棉花糖,app将不会在安装的时候授予权限。取而代之的是,app不得不在运行时一个一个询问用户授予权限`但是也不是所有的需要的权限都会询问用户的,只有一些比较重要的危及手机和用户安全的操作,才会需要在运行时动态授权`。如图:
9 | 10 | ![icon](./device-2016-07-24-133244.png) 11 | 12 | **ps:** 权限询问对话框不会自己弹出来。开发者不得不自己调用。如果开发者要调用的一些函数需要某权限而用户又拒绝授权的话,函数将抛出异常直接导致程序崩溃 13 | 14 | 另外,用户也可以随时在设置里取消已经授权的权限。 15 | ![icon](./device-2016-07-24-134935.png) 16 | 17 | 作为一个用户当然是欢天喜地了,但是作为一个android developer 是不是菊花一紧啊?这意味着我们不能像以前那样直接调用方法了,你不得不为每个需要的地方检察权限,否则app就崩溃了! 18 | 是的。我不能哄你说这是简单的事儿,但是这套机制只有在你设置了`targetSdkVersion 23`,并且在运行在android 6.0以上的系统,这套运行时权限规则才会生效。 19 | 20 | ## 已经发布的应用APP要怎么办 21 | 22 | 新运行时权限可能已经让你开始恐慌了。“hey,伙计!我三年前发布的app可咋整呢。如果他被装到android 6.0上,我的app会崩溃吗?!?”
23 | 莫慌张,放轻松。android小队又不傻,肯定考虑到了这情况。如果app的targetSdkVersion 低于 23,那将被认为app没有用23新权限测试过,那将被继续使用旧有规则:用户在安装的时候不得不接受所有权限,安装后app就有了那些权限咯!然后app像以前一样奔跑!**注意,此时用户依然可以取消已经同意的授权!**用户取消授权时,android 6.0系统会警告,但这不妨碍用户取消授权。问题又来了,这时候你的app崩溃吗?
24 | 善意的主把这事也告诉了android小组,当我们在targetSdkVersion 低于23的app调用一个需要权限的函数时,这个权限如果被用户取消授权了的话,不抛出异常。但是他将啥都不干,结果导致函数返回值是null或者0.
25 | 别高兴的太早。尽管app不会调用这个函数时崩溃,返回值null或者0可能接下来依然导致崩溃。 26 | 好消息(至少目前看来)是这类取消权限的情况比较少,我相信很少用户这么搞。如果他们这么办了,后果自负咯。
27 | 但从长远看来,我相信还是会有大量用户会关闭一些权限。我们app不能在新设备完美运行这是不可接受的。 28 | 怎样让他完美运行呢,你最好修改代码支持最新的权限系统,而且我建议你立刻着手搞起! 29 | 代码没有成功改为支持最新运行时权限的app,不要设置targetSdkVersion 23 发布,否则你就有麻烦了。只有当你测试过了,再改为targetSdkVersion 23 。
30 | **警告:**现在你在android studio新建项目,targetSdkVersion 会自动设置为 23。如果你还没支持新运行时权限,我建议你首先把targetSdkVersion 降级到22 31 | 32 | ## PROTECTION_NORMAL类权限 33 | 34 | android.permission.ACCESS_LOCATION_EXTRA_COMMANDS 35 | android.permission.ACCESS_NETWORK_STATE 36 | android.permission.ACCESS_NOTIFICATION_POLICY 37 | android.permission.ACCESS_WIFI_STATE 38 | android.permission.ACCESS_WIMAX_STATE 39 | android.permission.BLUETOOTH 40 | android.permission.BLUETOOTH_ADMIN 41 | android.permission.BROADCAST_STICKY 42 | android.permission.CHANGE_NETWORK_STATE 43 | android.permission.CHANGE_WIFI_MULTICAST_STATE 44 | android.permission.CHANGE_WIFI_STATE 45 | android.permission.CHANGE_WIMAX_STATE 46 | android.permission.DISABLE_KEYGUARD 47 | android.permission.EXPAND_STATUS_BAR 48 | android.permission.FLASHLIGHT 49 | android.permission.GET_ACCOUNTS 50 | android.permission.GET_PACKAGE_SIZE 51 | android.permission.INTERNET 52 | android.permission.KILL_BACKGROUND_PROCESSES 53 | android.permission.MODIFY_AUDIO_SETTINGS 54 | android.permission.NFC 55 | android.permission.READ_SYNC_SETTINGS 56 | android.permission.READ_SYNC_STATS 57 | android.permission.RECEIVE_BOOT_COMPLETED 58 | android.permission.REORDER_TASKS 59 | android.permission.REQUEST_INSTALL_PACKAGES 60 | android.permission.SET_TIME_ZONE 61 | android.permission.SET_WALLPAPER 62 | android.permission.SET_WALLPAPER_HINTS 63 | android.permission.SUBSCRIBED_FEEDS_READ 64 | android.permission.TRANSMIT_IR 65 | android.permission.USE_FINGERPRINT 66 | android.permission.VIBRATE 67 | android.permission.WAKE_LOCK 68 | android.permission.WRITE_SYNC_SETTINGS 69 | com.android.alarm.permission.SET_ALARM 70 | com.android.launcher.permission.INSTALL_SHORTCUT 71 | com.android.launcher.permission.UNINSTALL_SHORTCUT 72 | 只需要在AndroidManifest.xml中简单声明这些权限就好,安装时就授权。不需要每次使用时都检查权限,而且用户不能取消以上授权。 73 | 74 | 75 | ## Dangerous Permissions 76 | 77 | ![icon](./650671-3297bcf7a0e7f34b.png) 78 | 79 | 只有这些权限会在第一次需要的时候弹出提示框,同一组的任何一个权限被授权了,其他权限也自动被授权。例如,一旦WRITE_CONTACTS被授权了,app也有READ_CONTACTS和GET_ACCOUNTS了。 80 | 81 | ## 特殊权限 82 | 自己在多项目的时候遇到了这样一个问题,被坑了很久,其实挺简单的只是自己之前不了解,所以这里也写出来做个提醒。 83 | 这类权限主要是指:
84 | 85 | 1. SYSTEM_ALERT_WINDOW,设置悬浮窗,进行一些黑科技 86 | 87 | 2.WRITE_SETTINGS 修改系统设置 88 | 89 | 90 | 这两个权限不能像其他Dangerous permission一样去处理,需要使用`startActivityForResult`来启动一个系统设置的fragment 91 | 例如需要修改系统设置: 92 | 93 | if (!Settings.System.canWrite(activity)) { 94 | AlertDialog.Builder builder = new AlertDialog.Builder(activity); 95 | builder.setTitle("信息确认"); 96 | builder.setMessage("需要授权修改系统设置的权限"); 97 | builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { 98 | @Override 99 | public void onClick(DialogInterface dialog, int which) { 100 | Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS); 101 | intent.setData(Uri.parse("package:" + activity.getPackageName())); 102 | activity.startActivityForResult(intent,requestCode); 103 | 104 | } 105 | }); 106 | builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { 107 | 108 | @Override 109 | public void onClick(DialogInterface dialog, int which) { 110 | dialog.dismiss(); 111 | } 112 | }); 113 | AlertDialog dialog = builder.create(); 114 | dialog.show(); 115 | } 116 | 117 | 118 | 他会打开一个该应用请求修改系统设置的页面: 119 | ![icon](./device-2016-08-15-102724.png) 120 | 121 | 操作完之后在`onActivityResult`中处理结果即可 122 | 123 | 124 | @Override 125 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 126 | super.onActivityResult(requestCode, resultCode, data); 127 | if (requestCode == REQUEST_CODE) { 128 | if (Settings.System.canWrite(this)) { 129 | Log.i(LOGTAG, "onActivityResult granted"); 130 | } 131 | } 132 | } 133 | 134 | 135 | 136 | ## 让你的app支持新运行时权限 137 | 138 | 是时候让我们的app支持新权限模型了,从设置compileSdkVersion and targetSdkVersion 为 23开始吧. 139 | 140 | ``` 141 | android { 142 | compileSdkVersion 23 143 | ... 144 | 145 | defaultConfig { 146 | ... 147 | targetSdkVersion 23 148 | ... 149 | } 150 | ``` 151 | 152 | 例如我想使用照相机 153 | 154 | ``` 155 | 156 | final private int REQUEST_CODE_ASK_PERMISSIONS = 100; 157 | 158 | private void takePhoto() { 159 | int hasuerPhotoPermission = checkSelfPermission(Manifest.permission.CAMERA); 160 | if (hasuerPhotoPermission != PackageManager.PERMISSION_GRANTED) { 161 | requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS}, 162 | REQUEST_CODE_ASK_PERMISSIONS); 163 | return; 164 | } 165 | yourMethod() 166 | } 167 | ``` 168 | 如果已有权限,yourMethod()会执行。否则,requestPermissions被执行来弹出请求授权对话框。 169 | **被用来检查和请求权限的方法分别是Activity的checkSelfPermission和requestPermissions。这些方法在api23引入。** 170 | 171 | 172 | 不论用户同意还是拒绝,activity的onRequestPermissionsResult会被回调来通知结果(通过第三个参数) 173 | 174 | ``` 175 | @Override 176 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 177 | switch (requestCode) { 178 | case REQUEST_CODE_ASK_PERMISSIONS: 179 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 180 | // Permission Granted 181 | yourMethod(); 182 | } else { 183 | // Permission Denied 184 | Toast.makeText(MainActivity.this, "permission Denied", Toast.LENGTH_SHORT) 185 | .show(); 186 | } 187 | break; 188 | default: 189 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 190 | } 191 | } 192 | ``` 193 | 194 | ## 在fragment中使用 195 | 现在很多应用在开发中会大量的使用fragment来作为开发,所以在这里也要讲一下在fragment中的使用。 196 | 197 | 1. 在Fragment中申请权限,不要使用ActivityCompat.requestPermissions, 直接使用Fragment的requestPermissions方法,否则会回调到Activity的 onRequestPermissionsResult. 198 | 2. 如果在Fragment中嵌套Fragment,在子Fragment中使用requestPermissions方 法,onRequestPermissionsResult不会回调回来,建议使用 getParentFragment().requestPermissions方法, 199 | 这个方法会回调到父Fragment中的onRequestPermissionsResult,加入以下代码可以把回调透传到子Fragment 200 | 201 | ``` 202 | @Override 203 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 204 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 205 | List fragments = getChildFragmentManager().getFragments(); 206 | if (fragments != null) { 207 | for (Fragment fragment : fragments) { 208 | if (fragment != null) { 209 | fragment.onRequestPermissionsResult(requestCode,permissions,grantResults); 210 | } 211 | } 212 | } 213 | } 214 | ``` 215 | 216 | ## 处理不在提醒 217 | 218 | 如果用户拒绝某授权。下一次弹框,用户会有一个“不再提醒”的选项的来防止app以后继续请求授权。 219 | ![icon](./device-2016-07-24-150814.png) 220 | 如果这个选项在拒绝授权前被用户勾选了。下次为这个权限请求requestPermissions时,对话框就不弹出来了,结果就是,app啥都不干。 221 | 这将是很差的用户体验,用户做了操作却得不到响应。这种情况需要好好处理一下。在请求requestPermissions前,我们需要检查是否需要展示请求权限的提示通过activity的shouldShowRequestPermissionRationale,代码如下: 222 | 223 | ``` 224 | if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) { 225 | if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { 226 | showMessageOKCancel("You need to allow access to Camera",new DialogInterface.OnClickListener() { 227 | @Override 228 | public void onClick(DialogInterface dialog, int which) { 229 | requestPermissions(new String[] {Manifest.permission.CAMERA},REQUEST_CODE_ASK_PERMISSIONS);} 230 | }); 231 | return; 232 | } 233 | requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS}, 234 | REQUEST_CODE_ASK_PERMISSIONS); 235 | return; 236 | } 237 | ``` 238 | 当一个权限第一次被请求和用户标记过不再提醒的时候,我们写的对话框被展示。 239 | 后一种情况,onRequestPermissionsResult 会收到PERMISSION_DENIED ,系统询问对话框不展示。这样就会给用户一个良好的体验 240 | 241 | ## 一次请求多个权限 242 | 当然了有时候需要好多权限,我们可以改进上面的做法,来做到一次请求多个权限。不要忘了为每个权限检查“不再提醒”的设置。话不多说还是直接上代码: 243 | 244 | public static final int REQUEST_CODE_ASK_PERMISSIONS = 100; 245 | final List permissionsList = new ArrayList<>(); 246 | 247 | 248 | ``` 249 | 250 | setRequestCodeAskPermissions(); 251 | if (permissionsList.size() > 0) { 252 | for (String s : permissionsList) { 253 | mActivity.requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),REQUEST_CODE_ASK_PERMISSIONS); 254 | } 255 | } else { 256 | bankCardDection(requestCode); 257 | } 258 | ``` 259 | 260 | ``` 261 | private List setRequestCodeAskPermissions() { 262 | List permissionsNeeded = new ArrayList<>(); 263 | if (!addPermission(permissionsList, Manifest.permission.READ_PHONE_STATE)) 264 | permissionsNeeded.add("read phone"); 265 | if (!addPermission(permissionsList, Manifest.permission.ACCESS_NETWORK_STATE)) 266 | permissionsNeeded.add("Read network"); 267 | if (!addPermission(permissionsList, Manifest.permission.CAMERA)) 268 | permissionsNeeded.add("CAMERA"); 269 | return permissionsNeeded; 270 | } 271 | 272 | ``` 273 | 274 | ``` 275 | 276 | private boolean addPermission(List permissionsList, String permission) { 277 | if (mActivity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { 278 | permissionsList.add(permission); 279 | // Check for Rationale Option 280 | if (!mActivity.shouldShowRequestPermissionRationale(permission)) 281 | return false; 282 | } 283 | } 284 | return true; 285 | 286 | ``` 287 | 然后就需要在回调方法中去检测这个权限是否已经被授予 288 | 289 | ``` 290 | @Override 291 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 292 | switch (requestCode) { 293 | case VerifiedManager.REQUEST_CODE_ASK_PERMISSIONS: { 294 | Map perms = new HashMap<>(); 295 | perms.put(Manifest.permission.READ_PHONE_STATE, PackageManager.PERMISSION_GRANTED); 296 | perms.put(Manifest.permission.ACCESS_NETWORK_STATE, PackageManager.PERMISSION_GRANTED); 297 | perms.put(Manifest.permission.CAMERA, PackageManager.PERMISSION_GRANTED); 298 | for (int i = 0; i < permissions.length; i++) 299 | perms.put(permissions[i], grantResults[i]); 300 | if (perms.get(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED 301 | && perms.get(Manifest.permission.ACCESS_NETWORK_STATE) == PackageManager.PERMISSION_GRANTED 302 | && perms.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED 303 | ) { 304 | // All Permissions Granted 305 | ZAIDBankCardSDKManager.getInstance(this).verifyBankCard(this, INTO_BANK_CARDSCAN_PAGE); 306 | } else { 307 | // Permission Denied 308 | Toast.makeText(this, "Some Permission is Denied", Toast.LENGTH_SHORT) 309 | .show(); 310 | } 311 | } 312 | break; 313 | default: 314 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 315 | } 316 | } 317 | 318 | ``` 319 | 这样就会一次弹出多个权限请求的对话框。 320 | 321 | 322 | ## 低版本兼容 323 | 324 | 以上代码在android 6.0以上运行没问题,但是23 api之前就不行了,因为没有那些方法。 325 | 粗暴的方法是检查版本 326 | 327 | ``` 328 | if (Build.VERSION.SDK_INT >= 23) { 329 | // Marshmallow+ 330 | } else { 331 | // Pre-Marshmallow 332 | } 333 | ``` 334 | 335 | 但是太复杂,我建议用v4兼容库,已对这个做过兼容,用这个方法代替 336 | 337 | * ContextCompat.checkSelfPermission() 338 | 被授权函数返回PERMISSION_GRANTED,否则返回PERMISSION_DENIED ,在所有版本都是如此 339 | * ActivityCompat.requestPermissions() 340 | 这个方法在M之前版本调用,OnRequestPermissionsResultCallback 直接被调用,带着正确的 PERMISSION_GRANTED或者 PERMISSION_DENIED 。 341 | * ActivityCompat.shouldShowRequestPermissionRationale() 342 | 在M之前版本调用,永远返回false 343 | 344 | 我们也可以在Fragment中使用,用v13兼容包:FragmentCompat.requestPermissions() and FragmentCompat.shouldShowRequestPermissionRationale().和activity效果一样。 345 | 346 | " 347 | ## 第三方库 348 | 看到上面的实现过程相信大家一定都很头疼。所幸,这世界上总是有那么一群牛逼的人的存在他们已经帮我们实现了相应的库。 349 | 350 | [PermissionsDispatcher](https://github.com/hotchemi/PermissionsDispatcher) 351 | 使用标注的方式,动态生成类处理运行时权限,目前还不支持嵌套Fragment。 352 | 353 | [RxPermissions](https://github.com/tbruyelle/RxPermissions) 354 | 基于RxJava的运行时权限检测框架 355 | 356 | [Grant](https://github.com/anthonycr/Grant) 357 | 简化运行时权限的处理,比较灵活 358 | 359 | 360 | ## 总结 361 | 新的运行时权限,可能会对用户的隐私保护的更好,但是它对我们开发者来说绝对是是一件头疼的事情,我们在使用和兼容的过程中一定要多加小心。不过看完这篇文章我想你一定对Android M 运行时权限有了一个比较全面的认识的。 -------------------------------------------------------------------------------- /android M 运行时权限/device-2016-07-24-133244.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/android M 运行时权限/device-2016-07-24-133244.png -------------------------------------------------------------------------------- /android M 运行时权限/device-2016-07-24-134935.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/android M 运行时权限/device-2016-07-24-134935.png -------------------------------------------------------------------------------- /android M 运行时权限/device-2016-07-24-150814.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/android M 运行时权限/device-2016-07-24-150814.png -------------------------------------------------------------------------------- /android M 运行时权限/device-2016-08-15-102724.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/android M 运行时权限/device-2016-08-15-102724.png -------------------------------------------------------------------------------- /android maven搭建maven私服及其应用/1471504237.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/android maven搭建maven私服及其应用/1471504237.jpeg -------------------------------------------------------------------------------- /android maven搭建maven私服及其应用/README.md: -------------------------------------------------------------------------------- 1 | # 搭建maven私服及其在android的使用以及Gradle中的条件编译 2 | 最近在公司做SDK开发,由于要区分对内版本和对外版本,功能和代码上有所不同。所以每次改动需要提供两套SDK.目前还是手动打包,分发。所以想把一些模块抽出来自由组合,并且改善发布流程。 3 |
4 | 之前写过一篇这样的文章 5 | [创建maven私有仓库及其在Android Gradle 中的使用](http://techtalk.alo7.com/?p=220) 6 | 7 | 在此基础上做一些补充:
8 | 9 | ## 生成AAR的源码追踪以及生成Doc 10 | 对于内部使用的aar,我们希望能够附带源码,这样在debug和查看修改是否起作用的时候能够方便一些。所以在打包aar的同时,映射source Code.方便我们开发具体操作如下 11 | 12 | task androidJavadocs(type: Javadoc) { 13 | source = android.sourceSets.main.java.srcDirs 14 | 15 | } 16 | 17 | task androidJavadocsJar(type: Jar) { 18 | classifier = 'javadoc' 19 | from androidJavadocs.destinationDir 20 | 21 | } 22 | 23 | task androidSourcesJar(type: Jar) { 24 | classifier = 'sources' 25 | from android.sourceSets.main.java.srcDirs 26 | } 27 | 28 | artifacts { 29 | // archives packageReleaseJar 30 | archives androidSourcesJar 31 | archives androidJavadocsJar 32 | } 33 | 34 | 35 | ## 条件编译 36 | 先看一下我在项目中的需求:
37 | 38 | 1. debug不提供某功能 39 | 2. release提供该功能 40 | 3. 该功能是一个通用模块在众多SDK都会用到 41 | 3. 不提供该功能不引入对应的代码 42 | 43 | 大家都知道在IOS中可以进行条件编译,但是在Java中很困难了。在针对不同版本提供不同功能上这边确实会比较坑 **这边不仅仅是是功能不同,由于是提供的SDK所以希望尽可能的减小体积,没有提供的功能,对应的代码也不要引入。所以通过设置运行时条件来做这个功能就无法实现**,经过研究我们可以在gradle编译脚本中针对不同的buildType以及Flavor设置不同的sourceSets,但是我的AAR本身有引入了其他的的AAR(根据不同的版本会使用不同的aar),所幸目前只生产两个版本,如果以后还需更多版本估计就要跪了(因为只提供`releaseCompile` `debugComplie`), 44 | 45 | 只对指定版本引入功能组件,例如只有在release中提供该功能 46 | 47 | releaseCompile ('com.zhongan.mobile:SDKAuthCheck:1.0.3-SNAPSHOT@aar') { 48 | transitive = true; 49 | changing = true 50 | } 51 | 52 | 53 | 然后对不同的版本指定不同的代码 54 | 55 | 56 | sourceSets { 57 | 58 | release { 59 | java.srcDirs = ['src/release','src/main/java'] // 使用release对应的代码 60 | resources.srcDirs = ['src/main/res'] 61 | aidl.srcDirs = ['src'] 62 | renderscript.srcDirs = ['src'] 63 | res.srcDirs = ['res'] 64 | assets.srcDirs = ['assets'] 65 | } 66 | 67 | debug { 68 | java.srcDirs = ['src/debug','src/main/java'] // 使用debug对应的代码 69 | resources.srcDirs = ['src/main/res'] 70 | aidl.srcDirs = ['src'] 71 | renderscript.srcDirs = ['src'] 72 | res.srcDirs = ['res'] 73 | assets.srcDirs = ['assets'] 74 | } 75 | } 76 | 77 | 78 | 对应的文件目录结构如下: 79 | ![1471504237.jpeg](./1471504237.jpeg) 80 | 81 | **PS:不要修改debug 和 release 文件中的包名** 82 | 83 | 这样就可以满足该要求了 84 | -------------------------------------------------------------------------------- /flutter 工程化实践/README.md: -------------------------------------------------------------------------------- 1 | # Flutter工程化实践(Android平台) 2 | 3 | ### 前言 4 | 前不久Flutter刚好发布1.0 releas版本,而公司刚好在这方面有打算去实践,因此我迎来了flutter实践的机会。 5 | 关于Flutter的一些介绍、和其他平台的对比、编译和运行原理我就不在这里介绍,网上有大堆的资料可供学习、参考。这篇文章主要是介绍我在android平台上flutter集成以及工程化工程中遇到一些问题。 6 | 7 | ### 目标 8 | 对于flutter的使用目前通常有两种做法: 9 | 10 | 1. 纯Flutter的项目 11 | 2. flutter和native的混合项目 12 | 13 | 对于第一种方式来说更适合一个全新的从头开的APP,而且这中做法也不需要太android或者iOS开发介入。
14 | 对于第二种做法:官方介绍的`flutter create -t module my_flutter`的方式,是在本身的android项目中添加了一个flutter的module,进行依赖编译的。这样的方式就存在以下问题: 15 | 16 | 1. 构建打包问题:引入Flutter后,Native工程因对其有了依赖和耦合,从而无法独立编译构建。在Flutter环境下,工程的构建是从Flutter的构建命令开始,执行过程中包含了Native工程的构建,开发者要配置完整的Flutter运行环境才能走通整个流程; 17 | 2. 混合编译带来的开发效率的降低:在转型Flutter的过程中必然有许多业务仍使用Native进行开发,工程结构的改动会使开发无法在纯Native环境下进行,而适配到Flutter工程结构对纯Native开发来说又会造成不必要的构建步骤,造成开发效率的降低。 18 | 19 | 20 | 针对以上的问题,也借鉴了一些其他平台的类似的操作: 21 | 决定将对原始的flutter的项目进行改造,**实现输出AAR的方式**,让native工程依赖。 22 |
从而达到: 23 | 24 | 1. flutter 和 native分别独立编写,编译互不影响; 25 | 2. native能够方便的集成flutter包并且使用; 26 | 3. flutter项目可以方便移植到任何模块去 27 | 28 | ### 过程 29 | 30 | ##### 一、改造成lib项目 31 | 在一个flutter工程中,默认的情况下是android打包构建输出的是一个APK,为了能够达成AAR的我们将android包下面的工程该成一个lib项目,修改build.gradle文件 32 | 33 | ``` 34 | apply plugin: 'com.android.library' 35 | //apply plugin: 'com.android.application' 36 | ``` 37 | ``` 38 | //applicationId "com.example.fluttersocial" 39 | 40 | ``` 41 | 42 | 之后通过`./gradlew build` 构建之后会得到如下产物 43 | ![](./aar.jpg) 44 | 45 | ##### 二、集成运行 46 | 把刚才得到的AAR的引入到Android native的工程中(依赖过程略过),通过Activity直接打开flutter工程中的MainActivity,会得到一个这样的崩溃信息: 47 | 48 | 16:32:36.124 21199-21199/com.example.androidwithflutter3 A/flutter: [FATAL:flutter/fml/icu_util.cc(95)] Check failed: context->IsValid(). Must be able to initialize the ICU context. Tried: /data/user/0/com.example.androidwithflutter3/app_flutter/icudtl.dat 49 | 16:32:36.124 21199-21199/com.example.androidwithflutter3 A/libc: Fatal signal 6 (SIGABRT), code -6 in tid 21199 (oidwithflutter3) 50 | 51 | 发现是context初始化失败,并且ICU有关的。 52 | 然后我们通过解压对比同一个type的aar和release发现 53 | 54 | aar: 55 | ![](./aar2.jpg) 56 | 57 | apk: 58 | ![](./apk.jpg) 59 | 60 | aar比apk在assets中少了一个fluter_shared文件夹,其中包含着icudtl.dat. 61 | 62 | **目前还不清楚flutter在编译打包过程中针对两种输出中为什么会存在这样的差异??** 63 | 64 | icu库是一个什么东西呢? 65 | 支持最新的Unicode标准。 66 | 不同代码页的字符集转换。 67 | 本地化数据,如:数字、日期、货币等等。 68 | 语言相关的字符串处理,如:排序、搜索等等。 69 | 正则表达式支持。 70 | 语言转换。 71 | 阿拉伯语、希伯来语、印度语、泰语等文字排版。 72 | 文本分词。 73 | 同时这个文件是在flutter引擎中的 74 | ![](./icu.jpg) 75 | 76 | 所以我们可以把这个文件copy出来放在flutter工程中的android的assets目录下 77 | ![](./assets.jpg) 78 | 重新打包即可,再次运行就不会报这个错误了。 79 | 80 | **ps:这个做法比较low,但是由于目前还不熟悉flutter的打包和编译过程,所以暂时没有编译打包方面去做相应的复制和改动,后面希望可以通过脚本的方式在编译的过程中来自动化的处理这一步操作。** 81 | 82 | ##### 三、arm支持 83 | 当把我们的集成到我们的我们真实的项目中的时候,发现会报这样的一个错误: 84 | 85 | 86 | AndroidRuntime: java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.androidwithflutter3-1/base.apk"],nativeLibraryDirectories=[/data/app/ccom.example.androidwithflutter3/lib/arm, /data/app/com.example.androidwithflutter3-1/base.apk!/lib/armeabi, /vendor/lib, /system/lib]]] couldn't find "libflutter.so" 87 | 88 | at java.lang.Runtime.loadLibrary (Runtime.java:367) 89 | at java.lang.System.loadLibrary (System.java:1076) 90 | at io.flutter.view.FlutterMain.startInitialization (FlutterMain.java:172) 91 | at io.flutter.view.FlutterMain.startInitialization (FlutterMain.java:149) 92 | at io.flutter.app.FlutterApplication.onCreate (FlutterApplication.java:22) 93 | at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1037) 94 | at android.app.ActivityThread.handleBindApplication (ActivityThread.java:6496) 95 | at android.app.ActivityThread.access$1800 (ActivityThread.java:229) 96 | at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1887) 97 | at android.os.Handler.dispatchMessage (Handler.java:102) 98 | at android.os.Looper.loop (Looper.java:148) 99 | at android.app.ActivityThread.main (ActivityThread.java:7406) 100 | at java.lang.reflect.Method.invoke (Native Method) 101 | at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:1230) 102 | at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1120) 103 | 解压aar发现在`jni`下面只有**armeabi-v7a**的文件夹 104 | 105 | 同时因为项目中设置了abiFilters 106 | 107 | ndk { 108 | abiFilters "armeabi" 109 | } 110 | 所以我们需要使得在输出aar兼容armeabi架构。 111 | 112 | 我们先来看flutter 支持那些架构 113 | ![](./arm.jpg) 114 | 115 | 可以看到官方只提供了四种CPU架构的SO库:`arm`、`arm64-v8a`、`x86`和`x86-64`,其中x86系列只支持Debug模式 116 | 而在`arm`架构中提供的是armeabi-v7a 117 | ![](./arm2.jpg) 118 | 119 | 然后我们再来看看flutte是如何指定的使用那个架构的,在flutter中的`“pagkages/flutter_tools/gralde/flutter.gradle”`有如下逻辑: 120 | ![](./flutter_gradle.jpg) 121 | 可以看出在编译打包过程中会更具指定平台来决定解压engins下的那个jar包,在android平台下默认情况的是使用arm的下的flutter.jar的所以我们可以针对这点进行修改使得aar兼容armeabi 122 | 123 | 具体操作如下: 124 | ![](./arm_sh.jpg) 125 | 126 | 将arm下各个包中的flutter解压 修改其中的jni的文件名 `armeabi-v7a` -> `armeabi` 这样在输出的时候就会变成 armeabi了,(由于armeabi-v7a向下兼容了armeabi,只是在指令方面做了一些优化)之后打包的AAR之后就变成了 127 | ![](./so.jpg) 128 | 129 | 再次集成之后就不会出现这个错误了,类似的我们想同时兼容`armeabi-v7a` `armeabi`` arm64-v8a` 可以在修改相应的脚本保留对应的文件夹下的动态库。 130 | 131 | ##### 四、native跳转flutter页面 132 | 根据官方文档上提供的资料我们可以有如下两种方式从native打开一个flutter page: 133 | 134 | View flutterView = Flutter.createView( 135 | MainActivity.this, 136 | getLifecycle(), 137 | "route1" 138 | ); 139 | 或者 140 | 141 | FragmentTransaction tx = getSupportFragmentManager().beginTransaction(); 142 | tx.replace(R.id.someContainer, Flutter.createFragment("route1")); 143 | tx.commit(); 144 | 145 | 原以为`Flutter` 和 `FlutterFragment`类是包含在flutter jar中的,可以通过输出的aar直接使用的;结果发现并没有存在这样类,只好先通过`flutter create -t module my_flutter`方式创建一个submodule 看看这两个类的来源,发现这两个其实是flutter module 生成的 只是对 `FlutterMain`做了一个简单的封装,所以就简单的将其copy到flutter项目中android文件夹,重新打包这样在主项目中就可以引用到,并且实现页面的跳转到功能。 146 | 147 | ##### 五、第三方插件管理 148 | 在Flutter项目的迭代过程中,我们native项目在更新AAR的过程中发现,会有一些AAR找不到,比如`shared_preferences`、`image_picker`等,但是在我们主工程中发现完全没有引用这些库啊,那么他到底是从哪里的来的呢?我使用 `./gradlew app_crm:dependencies ` 查看相应依赖树: 149 | ![](./dependencies.jpg) 150 | 发现相应的这些库都是由flutter引来引用的。为了进一步确定这个问题我们查看flutter aar 在maven 中的pom文件: 151 | ![](./pom.jpg) 152 | 可以看出的确是在flutter的aar中引入了一些三方库。 153 | 154 | 在flutter项目中第三方依赖是库的管理是由`pub`管理的,其中依赖的申明都放在`pubspec.yaml`中,最终会生成一个`pubspec.lock`文件,对依赖进行说明 155 | ![](./pub.jpg) 156 | 157 | 我们可以看到他们`依赖属性`,`名称`,`版本` `source的形式:host和SDK`等信息。而我发现那些没找到的库都是 source:host的方式的库,他是将整个项目的源码拉下来的,并且在远端没有aar或者jar存在的,而对应的source 都存放在存放在本地的.pub仓库中 158 | ![](./pub_source.jpg) 159 | 160 | 但是在构建主AAR的过程flutter会将相应的第三方插件打包成AAR: 161 | ![](./flutter_build.jpg) 162 | 163 | 那么我们的重点就是两个: 164 | 165 | 1. 将AAR上传到远端或者私有maven仓库中 166 | 2. 使得对应的AAR有和Flutter AAR中pom中dependencies节点一致的pom文件,这样的才可以能够自动匹配 167 | 168 | 在解决问题的过程中分别尝试了如下方法: 169 | 170 | 1. **uploadArchives 上传多个AAR**:但是由于他的多个aar只是针对同一个构建的不同flavor,不能够达到目标 171 | 2. **uploadArchives 依赖子任务**:但是uploadArchives没法依赖子任务,所以也没成功。 172 | 3. **单开一个任务上传第三方插件包**:存在两个问题一是单独执行任务生成的pom信息会和主AAR不一致;且已经在子任务中我们无法再次开启一个uploadArchives task。对于第二个问题 经过同事提醒可以使用mvn的方式上传到指定远端仓库 173 | 174 | 最终我在uploadArchives中根据的dependencies信息结合mvn命令(关于mvn 是需要先安装,配置的大家自行搜索,以及mvn的 deploy命令),将第三方插件上传到远程仓库,具体代码如下: 175 | 176 | uploadArchives { 177 | configuration = configurations.archives 178 | repositories { 179 | mavenDeployer { 180 | android.libraryVariants.all { variant -> 181 | def _flavorBuildTypeName = "release" 182 | addFilter(_flavorBuildTypeName) { artifact, file -> 183 | true 184 | } 185 | snapshotRepository(url: "${artifactory_contextUrl}/libs-snapshot-local") { 186 | authentication(userName: artifactory_user, password: artifactory_password) 187 | } 188 | repository(url: "${artifactory_contextUrl}/libs-release-local") { 189 | authentication(userName: artifactory_user, password: artifactory_password) 190 | } 191 | pom(_flavorBuildTypeName).artifactId = "flutter-"+project.archivesBaseName + "-" + _flavorBuildTypeName 192 | pom(_flavorBuildTypeName).version = "0.1.3-SNAPSHOT" 193 | pom(_flavorBuildTypeName).groupId = "com.ymm.lib" 194 | pom(_flavorBuildTypeName).name = 'lib_flutter' 195 | pom(_flavorBuildTypeName).packaging = 'aar' 196 | pom(_flavorBuildTypeName).withXml { 197 | def root = asNode() 198 | def depsNode = root["dependencies"][0] ?: root.appendNode("dependencies") 199 | def addDep = { 200 | if (it.group == null) return // Avoid empty dependency nodes 201 | def dependencyNode = depsNode.appendNode('dependency') 202 | dependencyNode.appendNode('groupId', it.group) 203 | dependencyNode.appendNode('artifactId', it.name) 204 | dependencyNode.appendNode('version', it.version) 205 | if (it.hasProperty('optional') && it.optional) { 206 | dependencyNode.appendNode('optional', 'true') 207 | } 208 | //生成依赖包的mvn 部署 209 | def dir = file('../../build/') 210 | def repoUrl = "${artifactory_contextUrl}/libs-snapshot-local" 211 | dir.eachFileRecurse { file -> 212 | if (file.name.contains(it.name) && file.name.endsWith("-release.aar")) { 213 | println file.name + "++++++++++++++++++" 214 | println it.group + "+++++++++++++++++" 215 | println it.name + "+++++++++++++++++" 216 | println it.version + "+++++++++++++++++" 217 | 218 | def command = "mvn deploy:deploy-file" 219 | .concat(" -DgroupId=${it.group}") 220 | .concat(" -DartifactId=${it.name}") 221 | .concat(" -Dversion=${it.version}") 222 | .concat(" -Dpackaging=aar") 223 | .concat(" -Dfile=${file.path}") 224 | .concat(" -Durl=${repoUrl}") 225 | .concat(" -DgeneratePom=true") 226 | 227 | def proc = command.execute() 228 | proc.waitFor() 229 | 230 | } 231 | } 232 | 233 | 234 | } 235 | configurations.api.allDependencies.each addDep 236 | configurations.implementation.allDependencies.each addDep 237 | } 238 | 239 | } 240 | } 241 | } 242 | } 243 | 244 | 245 | 至此我们基本上达成了我们native和flutter工程分离开发的目标,并且能够良好的运行起Flutter,但是对于flutter还有下面一些疑问。 246 | 247 | ### 问题与思考 248 | 249 | #### 1. 多个flutter包的集成问题 250 | 251 | 因为目前我们的项目里面只用到一个flutter工程,如果后面由flutter实现的模块很多,分裂成多个Flutter工程的话,这样会就输出到多个AAR,那么会存在以下的问题: 252 | 1. class的重复 253 | 2. 动态库.so文件的冲突 254 | 3. dart编译后的文件覆盖导致功能丢失的问题 255 | 256 | 关于问题一、二,主要是flutter.jar 和 flutter.so中的文件冲突,我们可以通过将flutter底层抽出库一份让各个flutter工程共享,最终在主工程中值使用一份,当然这也需要结合我们之前看到的flutter.gradle文件中的打包逻辑和改造每个aar的打包过程。 257 | 258 | 关于问题三之所以会发生是因为每个flutter工程的Dart文件最终都会编译成相应的文件,我们这里以release包作为说明,他会有如下几个文件: 259 | 260 | ![](./vm.jpg) 261 | 分别对应isolate的数据集合和指针以及Dart VM相关的东西。 262 | 这个文件最终都会copy到APK包的assets目录,重名文件会覆盖掉,那么在多个flutter aar 下面 最终只会保留一份,丢失其他的。 263 | 264 | 那么有人可能会想到我们可以更改每个一个工程的对应文件的名称,这很OK是一个很正确的思路,但是我们将要面临两个新的问题是: 265 | 266 | * 修改flutter的编译引擎,以便修改每个项目输出的文件名 267 | * 修改Dart VM的加载器,以便能够加载指定的文件 268 | 269 | 就目前的调研来看,还没发现在哪里能够修改这两个操作,以及将来实现过程中会遇到什么坑。 270 | 271 | *所以建议是:dart实现的类都在统一的一个工程中实现,按照模块划分包,输出一个aar,在加载的时候通过不同的route打开不同的功能模块或者page* 272 | 273 | #### 2. 动态化的可能性 274 | 原则上flutter本身是不支持动态加载的,而他所提供的debug版本下 `hot reload`机制,也是通过在debug环境下使用的jit加载方式实现的,并且还有这样的限制: 275 | 276 | * 编译错误,如果修改后的Dart代码无法通过编译,Flutter会在控制台报错,这时需要修改对应的代码。 277 | * 控件类型从StatelessWidget到StatefulWidget的转换,因为Flutter在执行热刷新时会保留程序原来的state,而某个控件从stageless→stateful后会导致Flutter重新创建控件时报错“myWidget is not a subtype of StatelessWidget”,而从stateful→stateless会报错“type 'myWidget' is not a subtype of type 'StatefulWidget' of 'newWidget'”。 278 | * 全局变量和静态成员变量,这些变量不会在热刷新时更新。 279 | * 修改了main函数中创建的根控件节点,Flutter在热刷新后只会根据原来的根节点重新创建控件树,不会修改根节点。 280 | * 某个类从普通类型转换成枚举类型,或者类型的泛型参数列表变化,都会使热刷新失败。 281 | * 热刷新无法实现更新时,执行一次热重启(Hot Restart)就可以全量更新所有代码,同样不需要重启App,区别是restart会将所有Dart代码打包同步到设备上,并且所有状态都会重置。 282 | 283 | 而我们这里的的动态化,指的是像web、rn这个样的Flutter部分能够APP在不发版本的情况的更新.官方虽说不支持,但是我们在android平台下还是可以做到的: 284 | 285 | 基于上一个问题中我们提到:dart部分的文件最终都被编译成了`isolate*data/instr`和`vm*data/instr`这样的文件,而这些文件最终都会被放在APK的`assets`目录下,而当APK安装到手机上的时候这部分文件不会被做任何改动,于此同时android也开放出了针对assets目录下的文件读写。 286 | 287 | **所以,我们可以通过文件下发的方式将dart部分编译后的文件覆盖写入到`assets`目录下,以便实现Dart动态化的更新,其限制在于新更改的Dart中相较于上一版本,没有引入一些由java实现的第三方插件** 288 | 289 | 当然要做好一个Flutter的动态更新,我们可能还需要**文件下发**,**文件校验**,**版本监控**,**错误回滚**等一系列的辅助配套设施 290 | 291 | 292 | -------------------------------------------------------------------------------- /flutter 工程化实践/aar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/aar.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/aar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/aar2.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/apk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/apk.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/arm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/arm.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/arm2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/arm2.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/arm_sh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/arm_sh.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/assets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/assets.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/dependencies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/dependencies.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/flutter_build.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/flutter_build.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/flutter_gradle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/flutter_gradle.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/icu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/icu.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/pom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/pom.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/pub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/pub.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/pub_source.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/pub_source.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/so.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/so.jpg -------------------------------------------------------------------------------- /flutter 工程化实践/vm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/flutter 工程化实践/vm.jpg -------------------------------------------------------------------------------- /gralde 插件/README.md: -------------------------------------------------------------------------------- 1 | # GradlePlugin 2 | 3 | ## 前言 4 | 最近在研究一些android中使用AOP的方式进行埋点的技术,其中很多都使用到了在编译时进行代码处理,大多数都是使用了自定义的gradle插件技术,所以一直比较好奇这个gradle插件是如何实现,经过学习和实践之后特此做一个总结 5 | 6 | ## 实现 7 | 8 | ### 1.创建plugin工程 9 | AS中是没有专门的plugin工程的,所以在这里要特别处理一下 10 | 11 | 1.先创建一个普通的android项目 12 | 2. 然后新建一个module,该module作为插件项目,module的类型不用特别关心 13 | 3. 将module的内容删除,只保留build.gralde 和 src/main目录 14 | 4. 我们都知道gradle是基于groovy语言,插件开发也不例外,所以在src/mian下面新建一个目录groovy 15 | 5. groovy本身是基于java的所以它的结构也有点像,有一个包名在项目中如:com.carl.plugin 然后在该包下面创建groovy文件如:MyPlugin.groovy 16 | 17 | ### 2. 实现Plugin 18 | 为了使该项目能够编译且实现gradle插件功能,我们先要在该model保留的`build.gradle`文件中添加如下内容 19 | 20 | apply plugin: 'groovy' 21 | 22 | 23 | dependencies { 24 | //gradle sdk 25 | compile gradleApi() 26 | //groovy sdk 27 | compile localGroovy() 28 | } 29 | 30 | repositories { 31 | mavenCentral() 32 | } 33 | 34 | 然后编写刚才创建的`MyPlugin.groovy`文件,最主要是使其实现**`Plugin`**接口,在例子中只是打印了日志 35 | 36 | public class MyPlugin implements Plugin { 37 | 38 | void apply(Project project) { 39 | println("========================"); 40 | println("hello gradle plugin!"); 41 | println("========================"); 42 | } 43 | } 44 | 现在我们已经实现了插件类了,**但是最重要的是告诉`gradle`我们实现的是哪一个插件也就是向gradle声明插件**,因此,需要在src/main目录下创建`resources/META-INF/gradle-plugins`目录,最后在该目录下创建一个properties文件,**注意这个文件的命名,你可以随意取名,但是后面使用这个插件的时候,会用到这个名字。比如,你取名为myPlugin.properties,而在其他build.gradle文件中使用自定义的插件时候则需写成:apply plugin: 'myPlugin'**,然后在该文件中指明实现插件的自定义类: 45 | `implementation-class=com.carl.plugin.MyPlugin` 46 | 47 | 到此项目结构如下: 48 | ![MacDown Screenshot](./resource/project.jpg) 49 | 50 | ### 3. 打包上传Plugin 51 | 如何打包上传到maven这里就不在展开了请参考另一篇文章[here](http://techtalk.alo7.com/?p=220)
52 | 53 | 在这里学到了一种不不使用命令行的的构建方式,直接在AS通过按钮操作:点击AndroidStudio右侧的gradle工具可以看到如下: 54 | ![MacDown Screenshot](./resource/upload.jpg) 55 | 双击uploadArchives就可以代替原来的命令行的方式进行打包上传 56 | 57 | ### 4.使用Plugin,验证功能 58 | 关于第三方仓库的使用在3节已经介绍过了这里不再赘述,重点关注插件使用 59 | 与aar使用不同,需要在项目的build.gradle文件中的配置 60 | 61 | dependencies { 62 | //格式为-->group:module:version 63 | classpath 'com.carl.mobile:plugin:1.0.1-SNAPSHOT' 64 | } 65 | 66 | 引用的jar是以classpath作为前缀的。 67 | 68 | 然后在app module中 使用插件 69 | 70 | apply plugin: 'myPlugin' //这个名字之前有重点介绍过 71 | 72 | 73 | 接下来验证plugin有没有生效,首先`clean project`,然后通过命令行编译项目`./gradlew build --info 74 | ` 会在终端中看到刚才在plugin中输出的语句 75 | ![MacDown Screenshot](./resource/test1.jpg) 76 | 并且在编译过程的生命周期中能够看到自定义plugin相应的处理流程 77 | ![MacDown Screenshot](./resource/test2.jpg) 78 | 79 | 80 | 81 | 至此,一个自定义gradle plugin的开发和使用就完成,后面我会基于这个Plugin进行一些实际功能的开发。 82 | 83 | 84 | [demo 地址](https://github.com/carl1990/GradlePlugin)
85 | -------------------------------------------------------------------------------- /gralde 插件/resource/project.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/gralde 插件/resource/project.jpg -------------------------------------------------------------------------------- /gralde 插件/resource/test1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/gralde 插件/resource/test1.jpg -------------------------------------------------------------------------------- /gralde 插件/resource/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/gralde 插件/resource/test2.jpg -------------------------------------------------------------------------------- /gralde 插件/resource/upload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/gralde 插件/resource/upload.jpg -------------------------------------------------------------------------------- /kotlin中的协程/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/.DS_Store -------------------------------------------------------------------------------- /kotlin中的协程/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/1.jpg -------------------------------------------------------------------------------- /kotlin中的协程/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/10.png -------------------------------------------------------------------------------- /kotlin中的协程/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/11.png -------------------------------------------------------------------------------- /kotlin中的协程/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/2.jpg -------------------------------------------------------------------------------- /kotlin中的协程/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/3.jpg -------------------------------------------------------------------------------- /kotlin中的协程/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/4.jpg -------------------------------------------------------------------------------- /kotlin中的协程/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/5.jpg -------------------------------------------------------------------------------- /kotlin中的协程/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/6.jpg -------------------------------------------------------------------------------- /kotlin中的协程/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/7.jpg -------------------------------------------------------------------------------- /kotlin中的协程/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/8.png -------------------------------------------------------------------------------- /kotlin中的协程/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/9.png -------------------------------------------------------------------------------- /kotlin中的协程/README.md: -------------------------------------------------------------------------------- 1 | # kotlin中的协程 2 | 3 | ### 什么是协程(Coroutine)? 4 | 在说明协程之前我们先来看看一个段子: 5 | ![MacDown logo](./1.jpg) 6 | ![MacDown logo](./2.jpg) 7 | ![MacDown logo](./3.jpg) 8 | ![MacDown logo](./4.jpg) 9 | ![MacDown logo](./5.jpg) 10 | ![MacDown logo](./6.jpg) 11 | ![MacDown logo](./7.jpg) 12 | 13 | 这可能是一个玩笑,可是对于一个使用java语言作为开发语言的Android Developer来说是很真实的。相信如果没有学习其他一些语言的同学可能是真的没有听说过这个概念。那么协程到底是什么呢?我们先来回顾一下计算机中常用的进程、线程。 14 | 15 | * 进程 16 | 17 | 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。 18 | 19 | * 线程 20 | 21 | 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。 22 | 23 | * 协程 24 | 25 | 协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单 26 | 27 | 协程的开发人员 Roman Elizarov 是这样描述协程的:**协程就像非常轻量级的线程**。**线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的**。所以**协程也像用户态的线程**,非常轻量级,一个线程中可以创建任意个协程。 28 | 29 | 总结下来协程就是:**由开发控制,简化异步编程,轻量级的异步实现方式**。 30 | kotlin也在1.3中正式转正了协程,之前一直作为实验性功能在迭代。 31 | 32 | 33 | ### 协程的亮点 34 | 35 | 既然协程是为了异步而生的那么我们就以此为例来看看协程的亮点在哪里,在异步编程中最为常见的场景是:在后台线程执行一个复杂任务,然后通知UI线程更新。 36 | 通常的写法是这样的 37 | 38 | ```kotlin 39 | request.execute(callback) 40 | callback = { 41 | onSuccess = { res -> 42 | runOnUIThread() { 43 | //TODO 44 | } 45 | }, 46 | onFail = { error -> 47 | // TODO 48 | } 49 | } 50 | ``` 51 | 或者我们使用RXJava的方式进行如下: 52 | ```kotlin 53 | request.subscribe(subscriber) 54 | ... 55 | subscriber = ... 56 | 57 | request.subScribeOn(Androd.Mian).subscribe({ 58 | // TODO Success 59 | }, { 60 | // TODO Error 61 | }) 62 | ``` 63 | 但是在kotlin我们可以这样写 64 | 65 | ```kotlin 66 | fun getData(): Data { ... } 67 | fun showData(data: Data) { ... } 68 | launch { 69 | val result = withContext(Dispatchers.IO) { 70 | getData() 71 | } 72 | if (result.isSuccessful) { 73 | showData(result) 74 | } 75 | } 76 | ``` 77 | 我们可以看到使用kotlin之后我们没有用回调,就像是同步操作一样,写着异步代码。 78 | 79 | 如果这个例子还不够直观我们再来看一个场景:在后台线程执行一个复杂任务,下一个任务依赖于上一个任务的执行结果,所以必须等待上一个任务执行完成后才能开始执行。 80 | 看下面代码中的三个函数,后两个函数都依赖于前一个函数的执行结果。 81 | ```kotlin 82 | fun requestToken(): Token { 83 | // makes request for a token & waits 84 | return token // returns result when received 85 | } 86 | fun createPost(token: Token, item: Item): Post { 87 | // sends item to the server & waits 88 | return post // returns resulting post 89 | } 90 | fun processPost(post: Post) { 91 | // does some local processing of result 92 | } 93 | ``` 94 | 三个函数中的操作都是耗时操作,因此不能直接在 UI 线程中运行,而且后两个函数都依赖于前一个函数的执行结果,三个任务不能并行运行,该如何解决这个问题呢? 95 | 96 | * 回调 97 | 98 | ```kotlin 99 | fun requestTokenAsync(cb: (Token) -> Unit) { ... } 100 | fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... } 101 | fun processPost(post: Post) { ... } 102 | fun postItem(item: Item) { 103 | requestTokenAsync { token -> 104 | createPostAsync(token, item) { post -> 105 | processPost(post) 106 | } 107 | } 108 | } 109 | ``` 110 | * Future或者promise 111 | 112 | ```kotlin 113 | fun requestTokenAsync(): CompletableFuture { ... } 114 | fun createPostAsync(token: Token, item: Item): CompletableFuture { ... } 115 | fun processPost(post: Post) { ... } 116 | fun postItem(item: Item) { 117 | requestTokenAsync() 118 | .thenCompose { token -> createPostAsync(token, item) } 119 | .thenAccept { post -> processPost(post) } 120 | .exceptionally { e -> 121 | e.printStackTrace() 122 | null 123 | } 124 | } 125 | ``` 126 | 127 | * RX 方式 128 | 129 | ```kotlin 130 | fun requestToken(): Token { ... } 131 | fun createPost(token: Token, item: Item): Post { ... } 132 | fun processPost(post: Post) { ... } 133 | fun postItem(item: Item) { 134 | Single.fromCallable { requestToken() } 135 | .map { token -> createPost(token, item) } 136 | .subscribe({ post -> processPost(post) }, // onSuccess 137 | { e -> e.printStackTrace() } // onError) 138 | } 139 | 140 | ``` 141 | * kotlin 协程 142 | 143 | ```kotlin 144 | suspend fun requestToken(): Token { ... } // 挂起函数 145 | suspend fun createPost(token: Token, item: Item): Post { ... } // 挂起函数 146 | fun processPost(post: Post) { ... } 147 | fun postItem(item: Item) { 148 | GlobalScope.launch { 149 | val token = requestToken() 150 | val post = createPost(token, item) 151 | processPost(post) 152 | // 需要异常处理,直接加上 try/catch 语句即可 153 | } 154 | } 155 | ``` 156 | 使用协程后的代码非常简洁,以顺序的方式书写异步代码,不会阻塞当前 UI 线程,错误处理也和平常代码一样简单。 157 | 158 | ### kotlin中的协程 159 | 160 | 通过上面协程的例子来介绍kotlin中协程相关的一些基本概念和常用的方法: 161 | 162 | * **挂起函数** 163 | 164 | 用`suspend`修饰的方法称之为挂起函数,挂起函数能够以与普通函数相同的方式获取参数和返回值,但是调用函数可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起),挂起函数挂起协程时,不会阻塞协程所在的线程。挂起函数执行完成后会恢复协程,后面的代码才会继续执行。但是挂起函数只能在协程中或其他挂起函数中调用。所以suspend修饰符可以标记普通函数、扩展函数和 lambda 表达式。 165 | 166 | * **CoroutineScope 和 CoroutineContext** 167 | 168 | `CoroutineScope`,可以理解为协程本身,包含了 CoroutineContext。 169 | 170 | `CoroutineContext`,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。 171 | 172 | * **CoroutineDispatcher** 173 | 174 | `CoroutineDispatcher`,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有四种标准实现**Dispatchers.Default**、**Dispatchers.IO**,**Dispatchers.Main**和**Dispatchers.Unconfined**,Unconfined 就是不指定线程。 175 | 176 | launch函数定义如果不指定CoroutineDispatcher或者没有其他的ContinuationInterceptor,默认的协程调度器就是Dispatchers.Default,Default是一个协程调度器,其指定的线程为共有的线程池,线程数量至少为 2 最大与 CPU 数相同。 177 | 178 | * **Job & Deferred** 179 | 180 | Job,任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期,它有三种状态: 181 | `isActive`,`isCompleted`,`isCancelled`。 182 | 183 | Job 完成时是没有返回值的,如果需要返回值的话,应该使用 Deferred,它是 Job 的子类public interface Deferred : Job。 184 | 185 | * **Coroutine 构建器** 186 | 187 | CoroutineScope.launch函数属于协程构建器 Coroutine builders,Kotlin 中还有其他几种 Builders,负责创建协程。 188 | 189 | 1. CoroutineScope.launch {}:是最常用的 Coroutine builders,不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}。 190 | ```kotlin 191 | fun postItem(item: Item) { 192 | GlobalScope.launch(Dispatchers.Main) { // 在 UI 线程创建一个新协程 193 | val token = requestToken() 194 | val post = createPost(token, item) 195 | processPost(post) 196 | } 197 | } 198 | ``` 199 | 2. runBlocking {}:是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main函数和测试设计的。 200 | 201 | 3. withContext {}:不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成. 202 | ```kotlin 203 | fun login(userName: String, passWord: String) { 204 | launch { 205 | val response = withContext(Dispatchers.IO) { repository.login(userName, passWord) } 206 | executeResponse(response, { mLoginUser.value = response.data }, { errMsg.value =response.errorMsg }) 207 | } 208 | } 209 | ``` 210 | 4. async {}: CoroutineScope.async {}可以实现与 launch builder 一样的效果,在后台创建一个新协程,唯一的区别是它有返回值,因为CoroutineScope.async {}返回的是 Deferred 类型。 211 | 212 | ```kotlin 213 | GlobalScope.launch(Dispatchers.IO) { 214 | //Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. 215 | val result = async { 216 | delay(2000) 217 | }.await() 218 | show(result) 219 | } 220 | ``` 221 | 获取CoroutineScope.async {}的返回值需要通过await()函数,它也是是个挂起函数,调用时会挂起当前协程直到 async 中代码执行完并返回某个值。 222 | 223 | 我初步整理了一下协程整个体系大概是这个样子的:(不包含dispatcher 和 excutor部分) 224 | 225 | ![MacDown logo](./协程.png) 226 | 227 | 228 | 229 | ### 原理和高级用法 230 | 1. **挂起与恢复** 231 | 232 | * 挂起函数工作原理 233 | 234 | 协程的内部实现使用了 Kotlin 编译器的一些编译技术,当挂起函数调用时,背后大致细节如下: 235 | 挂起函数或挂起 lambda 表达式调用时,都有一个隐式的参数额外传入,这个参数是Continuation类型,封装了协程恢复后的执行的代码逻辑。 236 | 237 | 比如: 238 | 239 | ```kotlin 240 | suspend fun requestToken(): Token { ... } 241 | ``` 242 | 243 | 在JVM中是这样的: 244 | 245 | ```kotlin 246 | Object requestToken(Continuation cont) { ... } 247 | ``` 248 | 249 | **协程内部实现不是使用普通回调的形式,而是使用状态机来处理不同的挂起点**,比如之前的postItem大致的 CPS(Continuation Passing Style) 代码为 250 | 251 | ```java 252 | // 编译后生成的内部类大致如下 253 | final class postItem$1 extends SuspendLambda ... { 254 | public final Object invokeSuspend(Object result) { 255 | ... 256 | switch (this.label) { 257 | case 0: 258 | this.label = 1; 259 | token = requestToken(this) 260 | break; 261 | case 1: 262 | this.label = 2; 263 | Token token = result; 264 | post = createPost(token, this.item, this) 265 | break; 266 | case 2: 267 | Post post = result; 268 | processPost(post) 269 | break; 270 | } 271 | } 272 | } 273 | ``` 274 | 275 | 上面代码中**每一个挂起点和初始挂起点对应的 Continuation 都会转化为一种状态,协程恢复只是跳转到下一种状态中**。挂起函数将执行过程分为多个 Continuation 片段,并且利用状态机的方式保证各个片段是顺序执行的。 276 | 277 | * 挂起函数可能会挂起协程 278 | 279 | 挂起函数使用 CPS style 的代码来挂起协程,保证挂起点后面的代码只能在挂起函数执行完后才能执行,所以挂起函数保证了协程内的顺序执行顺序。 280 | 281 | ```kotlin 282 | fun postItem(item: Item) { 283 | GlobalScope.launch { 284 | // async { requestToken() } 新建一个协程,可能在另一个线程运行 285 | // 但是 await() 是挂起函数,当前协程执行逻辑卡在第一个分支,第一种状态,当 async 的协程执行完后恢复当前协程,才会切换到下一个分支 286 | val token = async { requestToken() }.await() 287 | // 在第二个分支状态中,又新建一个协程,使用 await 挂起函数将之后代码作为 Continuation 放倒下一个分支状态,直到 async 协程执行完 288 | val post = aync { createPost(token, item) }.await() 289 | // 最后一个分支状态,直接在当前协程处理 290 | processPost(post) 291 | } 292 | } 293 | 294 | ``` 295 | await()挂起函数挂起当前协程,直到异步协程完成执行,但是这里并没有阻塞线程,是使用状态机的控制逻辑来实现。而且挂起函数可以保证挂起点之后的代码一定在挂起点前代码执行完成后才会执行,挂起函数保证顺序执行,所以异步逻辑也可以用顺序的代码顺序来编写。 296 |
注意挂起函数不一定会挂起协程,如果相关调用的结果已经可用,库可以决定继续进行而不挂起,例如async { requestToken() }的返回值Deferred的结果已经可用时,await()挂起函数可以直接返回结果,不用再挂起协程。 297 | 298 | * 挂起函数不会阻塞线程 299 | 300 | 挂起函数挂起协程,并不会阻塞协程所在的线程,例如协程的delay()挂起函数会暂停协程一定时间,并不会阻塞协程所在线程,但是Thread.sleep()函数会阻塞线程。 301 | 302 | ```kotlin 303 | fun main(args: Array) { 304 | // 创建一个单线程的协程调度器,下面两个协程都运行在这同一线程上 305 | val dispatcher = newSingleThreadContext("wm") 306 | // 启动协程 1 307 | GlobalScope.launch(dispatcher) { 308 | println("the first coroutine") 309 | delay(200) 310 | println("the first coroutine") 311 | } 312 | // 启动协程 2 313 | GlobalScope.launch(dispatcher) { 314 | println("the second coroutine") 315 | delay(100) 316 | println("the second coroutine") 317 | } 318 | // 保证 main 线程存活,确保上面两个协程运行完成 319 | Thread.sleep(500) 320 | } 321 | ``` 322 | 结果为: 323 | ``` 324 | the first coroutine 325 | 326 | the second coroutine 327 | 328 | the second coroutine 329 | 330 | the first coroutine 331 | ``` 332 | 从上面结果可以看出,当协程 1 暂停 200 ms 时,线程并没有阻塞,而是执行协程 2 的代码,然后在 200 ms 时间到后,继续执行协程 1 的逻辑。所以挂起函数并不会阻塞线程,这样可以节省线程资源,协程挂起时,线程可以继续执行其他逻辑。 333 | 334 | * 挂起函数恢复 335 | 336 | 协程的所属的线程调度,主要是由协程的`CoroutineDispatcher`控制,`CoroutineDispatcher`可以指定协程运行在某一特定线程上、运作在线程池中或者不指定所运行的线程。所以协程调度器可以分为*Confined dispatcher*和*Unconfined dispatcher*,*Dispatchers.Default*、*Dispatchers.IO*和*Dispatchers.Main*属于Confined dispatcher,都指定了协程所运行的线程或线程池,挂起函数恢复后协程也是运行在指定的线程或线程池上的,而Dispatchers.Unconfined属于Unconfined dispatcher,协程启动并运行在 Caller Thread 上,但是只是在第一个挂起点之前是这样的,挂起恢复后运行在哪个线程完全由所调用的挂起函数决定。 337 | 338 | ```kotlin 339 | fun main(args: Array) = runBlocking { 340 | launch { // 默认继承 parent coroutine 的 CoroutineDispatcher,指定运行在 main 线程 341 | println("main runBlocking: I'm working in thread ${Thread.currentThread().name}") 342 | delay(100) 343 | println("main runBlocking: After delay in thread ${Thread.currentThread().name}") 344 | } 345 | launch(Dispatchers.Unconfined) { 346 | println("Unconfined : I'm working in thread ${Thread.currentThread().name}") 347 | delay(100) 348 | println("Unconfined : After delay in thread ${Thread.currentThread().name}") 349 | } 350 | } 351 | ``` 352 | 353 | 结果如下: 354 | 355 | ``` 356 | Unconfined : I'm working in thread main 357 | main runBlocking: I'm working in thread main 358 | Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor 359 | main runBlocking: After delay in thread main 360 | 361 | ``` 362 | 其中协程是如何创建以及进行调度,限于篇幅不做过多介绍想了解可以自行阅读源码。 363 | 其中关键方法和类: 364 | 365 | coroutine.start(),createCoroutineUnintercepted(), intercepted(),resumeCancellableWithException(), withCoroutineContext() 366 | 367 | DispatchedContinuation ,ContinuationInterceptor,CoroutineDispatcher,CoroutineScheduler 368 | 369 | 3. **delay和yield** 370 | 371 | delay的作用是延迟执行协程中代码,其实现为 372 | 373 | ```kotlin 374 | public suspend fun delay(timeMillis: Long) { 375 | if (timeMillis <= 0) return // don't delay 376 | return suspendCancellableCoroutine sc@ { cont: CancellableContinuation -> 377 | cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont) 378 | } 379 | } 380 | ``` 381 | 382 | delay 使用**suspendCancellableCoroutine**挂起协程,而协程恢复的一般情况下是关键在DefaultExecutor.scheduleResumeAfterDelay(),其中实现是schedule(DelayedResumeTask(timeMillis, continuation)),其中的关键逻辑是将 DelayedResumeTask 放到 DefaultExecutor 的队列最后,在延迟的时间到达就会执行 DelayedResumeTask,那么该 task 里面的实现是什么: 383 | 384 | ```kotlin 385 | override fun run() { 386 | // 直接在调用者线程恢复协程 387 | with(cont) { resumeUndispatched(Unit) } 388 | } 389 | ``` 390 | 391 | `yield()`的作用是挂起当前协程,然后将协程分发到 Dispatcher 的队列,这样可以让该协程所在线程或线程池可以运行其他协程逻辑,然后在 Dispatcher 空闲的时候继续执行原来协程。简单的来说就是让出自己的执行权,给其他协程使用,当其他协程执行完成或也让出执行权时,一开始的协程可以恢复继续运行。 392 | 393 | ```kotlin 394 | fun main(args: Array) = runBlocking { 395 | launch { 396 | repeat(3) { 397 | println("job1 repeat $it times") 398 | yield() 399 | } 400 | } 401 | launch { 402 | repeat(3) { 403 | println("job2 repeat $it times") 404 | yield() 405 | } 406 | } 407 | } 408 | ``` 409 | 结果如下: 410 | 411 | ``` 412 | job1 repeat 0 times 413 | job2 repeat 0 times 414 | job1 repeat 1 times 415 | job2 repeat 1 times 416 | job1 repeat 2 times 417 | job2 repeat 2 times 418 | ``` 419 | 420 | 其实现为: 421 | 422 | ```kotlin 423 | public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont -> 424 | val context = uCont.context 425 | context.checkCompletion() 426 | val cont = uCont.intercepted() as? DispatchedContinuation ?: return@sc Unit 427 | if (!cont.dispatcher.isDispatchNeeded(context)) { 428 | return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit 429 | } 430 | cont.dispatchYield(Unit) 431 | COROUTINE_SUSPENDED 432 | } 433 | ``` 434 | 我们可以发现共同点就是:**挂起函数的关键是调用suspendCoroutineUninterceptedOrReturn函数包装,然后在异步逻辑完成时调用resume手动恢复协程**。 435 | 436 | 我们可以手动尝试将异步网络回调封装为挂起函数 437 | 438 | ```kotlin 439 | suspend fun Call.await(): T = suspendCoroutine { cont -> 440 | enqueue(object : Callback { 441 | override fun onResponse(call: Call, response: Response) { 442 | if (response.isSuccessful) { 443 | cont.resume(response.body()!!) 444 | } else { 445 | cont.resumeWithException(ErrorResponse(response)) 446 | } 447 | } 448 | override fun onFailure(call: Call, t: Throwable) { 449 | cont.resumeWithException(t) 450 | } 451 | }) 452 | } 453 | ``` 454 | 455 | 上面的await()的扩展函数调用时,首先会挂起当前协程,然后执行enqueue将网络请求放入队列中,当请求成功时,通过cont.resume(response.body()!!)来恢复之前的协程。 456 | 457 | 458 | 4. **协程关系** 459 | 460 | 在job的源码中有这样一段注释 461 | ![MacDown logo](./8.png) 462 | 463 | 所以协程之间存在父子关系,他们之间的关系如下: 464 | * 父协程手动调用cancel()或者异常结束,会立即取消它的所有子协程。 465 | * 父协程必须等待所有子协程完成(处于完成或者取消状态)才能完成。 466 | * 子协程抛出未捕获的异常时,默认情况下会取消其父协程。 467 | 468 | 在`AbstractCoroutine`的 Start()方法中我们会看到一开始就初始化了父任务 469 | 470 | ```kotlin 471 | public fun start(start: CoroutineStart, block: suspend () -> T) { 472 | initParentJob() 473 | start(block, this) 474 | } 475 | ``` 476 | 477 | ```kotlin 478 | internal fun initParentJobInternal(parent: Job?) { 479 | check(parentHandle == null) 480 | if (parent == null) { 481 | parentHandle = NonDisposableHandle 482 | return 483 | } 484 | parent.start() // make sure the parent is started 485 | @Suppress("DEPRECATION") 486 | val handle = parent.attachChild(this) 487 | parentHandle = handle 488 | // now check our state _after_ registering (see tryFinalizeSimpleState order of actions) 489 | if (isCompleted) { 490 | handle.dispose() 491 | parentHandle = NonDisposableHandle // release it just in case, to aid GC 492 | } 493 | } 494 | ``` 495 | 可以看到最关键的流程是`parent.attachChild` 496 | 497 | ```kotlin 498 | @Suppress("OverridingDeprecatedMember") 499 | public final override fun attachChild(child: ChildJob): ChildHandle { 500 | /* 501 | * Note: This function attaches a special ChildHandleNode node object. This node object 502 | * is handled in a special way on completion on the coroutine (we wait for all of them) and 503 | * is handled specially by invokeOnCompletion itself -- it adds this node to the list even 504 | * if the job is already cancelling. For cancelling state child is attached under state lock. 505 | * It's required to properly wait all children before completion and provide linearizable hierarchy view: 506 | * If child is attached when job is already being cancelled, such child will receive immediate notification on 507 | * cancellation, but parent *will* wait for that child before completion and will handle its exception. 508 | */ 509 | return invokeOnCompletion(onCancelling = true, handler = ChildHandleNode(this, child).asHandler) as ChildHandle 510 | } 511 | 512 | ``` 513 | invokeOnCompletion()方法如下: 514 | 515 | ```java 516 | public final override fun invokeOnCompletion( 517 | onCancelling: Boolean, 518 | invokeImmediately: Boolean, 519 | handler: CompletionHandler 520 | ): DisposableHandle { 521 | var nodeCache: JobNode<*>? = null 522 | loopOnState { state -> 523 | when (state) { 524 | is Empty -> { // EMPTY_X state -- no completion handlers 525 | if (state.isActive) { 526 | // try move to SINGLE state 527 | val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it } 528 | if (_state.compareAndSet(state, node)) return node 529 | } else 530 | promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine 531 | } 532 | is Incomplete -> { 533 | val list = state.list 534 | if (list == null) { // SINGLE/SINGLE+ 535 | promoteSingleToNodeList(state as JobNode<*>) 536 | } else { 537 | var rootCause: Throwable? = null 538 | var handle: DisposableHandle = NonDisposableHandle 539 | if (onCancelling && state is Finishing) { 540 | synchronized(state) { 541 | // check if we are installing cancellation handler on job that is being cancelled 542 | rootCause = state.rootCause // != null if cancelling job 543 | // We add node to the list in two cases --- either the job is not being cancelled 544 | // or we are adding a child to a coroutine that is not completing yet 545 | if (rootCause == null || handler.isHandlerOf() && !state.isCompleting) { 546 | // Note: add node the list while holding lock on state (make sure it cannot change) 547 | val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it } 548 | if (!addLastAtomic(state, list, node)) return@loopOnState // retry 549 | // just return node if we don't have to invoke handler (not cancelling yet) 550 | if (rootCause == null) return node 551 | // otherwise handler is invoked immediately out of the synchronized section & handle returned 552 | handle = node 553 | } 554 | } 555 | } 556 | if (rootCause != null) { 557 | // Note: attachChild uses invokeImmediately, so it gets invoked when adding to cancelled job 558 | if (invokeImmediately) handler.invokeIt(rootCause) 559 | return handle 560 | } else { 561 | val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it } 562 | if (addLastAtomic(state, list, node)) return node 563 | } 564 | } 565 | } 566 | else -> { // is complete 567 | // :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, 568 | // because we play type tricks on Kotlin/JS and handler is not necessarily a function there 569 | if (invokeImmediately) handler.invokeIt((state as? CompletedExceptionally)?.cause) 570 | return NonDisposableHandle 571 | } 572 | } 573 | } 574 | } 575 | 576 | ``` 577 | 他会根据root的状态来进行不同的处理,具体注释已经很清楚了就不多说了。 578 | 579 | 这样我们就清除协程父子关系,以及他们之间的互相影响。 580 | 581 | 5. **异常处理** 582 | 583 | 协程中排除异常时一般都在逻辑运算中,而在协程中的三层包装中,逻辑运算发生在第二层的`BaseContinuationImpl`中`resumeWith()`函数中的`invokeSuspend`运行。 584 | 585 | ```kotlin 586 | // This implementation is final. This fact is used to unroll resumeWith recursion. 587 | public final override fun resumeWith(result: Result) { 588 | // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume 589 | var current = this 590 | var param = result 591 | while (true) { 592 | // Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure 593 | // can precisely track what part of suspended callstack was already resumed 594 | probeCoroutineResumed(current) 595 | with(current) { 596 | val completion = completion!! // fail fast when trying to resume continuation without completion 597 | val outcome: Result = 598 | try { 599 | val outcome = invokeSuspend(param) 600 | if (outcome === COROUTINE_SUSPENDED) return 601 | Result.success(outcome) 602 | } catch (exception: Throwable) { 603 | Result.failure(exception) 604 | } 605 | releaseIntercepted() // this state machine instance is terminating 606 | if (completion is BaseContinuationImpl) { 607 | // unrolling recursion via loop 608 | current = completion 609 | param = outcome 610 | } else { 611 | // top-level completion reached -- invoke and return 612 | completion.resumeWith(outcome) 613 | return 614 | } 615 | } 616 | } 617 | } 618 | ``` 619 | 从try {} catch {}语句来看,首先协程运算过程中所有未捕获异常其实都会在第二层包装中被捕获,然后会通过AbstractCoroutine.resumeWith(Result.failure(exception))进入到第三层包装中,所以协程的第三层包装不仅维护协程的状态,还处理协程运算中的未捕获异常。 620 | 621 | 在上面我们讲到 当子协程发生未捕获的异常时,父协程也会被取消,我们看看系统是如何处理的。 622 | 在`AbstractCoroutine`中的`resumeWith`方法中最终会调用到`tryMakeCompleting`方法 623 | 624 | ```kotlin 625 | private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?, mode: Int): Int { 626 | if (state !is Incomplete) 627 | return COMPLETING_ALREADY_COMPLETING 628 | /* 629 | * FAST PATH -- no children to wait for && simple state (no list) && not cancelling => can complete immediately 630 | * Cancellation (failures) always have to go through Finishing state to serialize exception handling. 631 | * Otherwise, there can be a race between (completed state -> handled exception and newly attached child/join) 632 | * which may miss unhandled exception. 633 | */ 634 | if ((state is Empty || state is JobNode<*>) && state !is ChildHandleNode && proposedUpdate !is CompletedExceptionally) { 635 | if (!tryFinalizeSimpleState(state, proposedUpdate, mode)) return COMPLETING_RETRY 636 | return COMPLETING_COMPLETED 637 | } 638 | // get state's list or else promote to list to correctly operate on child lists 639 | val list = getOrPromoteCancellingList(state) ?: return COMPLETING_RETRY 640 | // promote to Finishing state if we are not in it yet 641 | // This promotion has to be atomic w.r.t to state change, so that a coroutine that is not active yet 642 | // atomically transition to finishing & completing state 643 | val finishing = state as? Finishing ?: Finishing(list, false, null) 644 | // must synchronize updates to finishing state 645 | var notifyRootCause: Throwable? = null 646 | synchronized(finishing) { 647 | // check if this state is already completing 648 | if (finishing.isCompleting) return COMPLETING_ALREADY_COMPLETING 649 | // mark as completing 650 | finishing.isCompleting = true 651 | // if we need to promote to finishing then atomically do it here. 652 | // We do it as early is possible while still holding the lock. This ensures that we cancelImpl asap 653 | // (if somebody else is faster) and we synchronize all the threads on this finishing lock asap. 654 | if (finishing !== state) { 655 | if (!_state.compareAndSet(state, finishing)) return COMPLETING_RETRY 656 | } 657 | // ## IMPORTANT INVARIANT: Only one thread (that had set isCompleting) can go past this point 658 | require(!finishing.isSealed) // cannot be sealed 659 | // add new proposed exception to the finishing state 660 | val wasCancelling = finishing.isCancelling 661 | (proposedUpdate as? CompletedExceptionally)?.let { finishing.addExceptionLocked(it.cause) } 662 | // If it just becomes cancelling --> must process cancelling notifications 663 | notifyRootCause = finishing.rootCause.takeIf { !wasCancelling } 664 | } 665 | // process cancelling notification here -- it cancels all the children _before_ we start to to wait them (sic!!!) 666 | notifyRootCause?.let { notifyCancelling(list, it) } 667 | // now wait for children 668 | val child = firstChild(state) 669 | if (child != null && tryWaitForChild(finishing, child, proposedUpdate)) 670 | return COMPLETING_WAITING_CHILDREN 671 | // otherwise -- we have not children left (all were already cancelled?) 672 | if (tryFinalizeFinishingState(finishing, proposedUpdate, mode)) 673 | return COMPLETING_COMPLETED 674 | // otherwise retry 675 | return COMPLETING_RETRY 676 | } 677 | ``` 678 | 当发生异常的时候会调用`notifyCancelling`方法 679 | 680 | ```kotlin 681 | private fun notifyCancelling(list: NodeList, cause: Throwable) { 682 | // first cancel our own children 683 | onCancelling(cause) 684 | notifyHandlers>(list, cause) 685 | // then cancel parent 686 | cancelParent(cause) // tentative cancellation -- does not matter if there is no parent 687 | } 688 | ``` 689 | 690 | 会通知`cancelParent` 691 | 692 | ```kotlin 693 | private fun cancelParent(cause: Throwable): Boolean { 694 | // CancellationException is considered "normal" and parent is not cancelled when child produces it. 695 | // This allow parent to cancel its children (normally) without being cancelled itself, unless 696 | // child crashes and produce some other exception during its completion. 697 | if (cause is CancellationException) return true 698 | if (!cancelsParent) return false 699 | return parentHandle?.childCancelled(cause) == true 700 | } 701 | ``` 702 | 703 | 所以出现未捕获异常时,首先会取消所有子协程,然后可能会取消父协程。而有些情况下并不会取消父协程,一是当异常属于 CancellationException 时,二是使用`SupervisorJob`和`supervisorScope`时,子协程出现未捕获异常时也不会影响父协程,它们的原理是重写 `childCancelled()` 为 704 | 705 | ``` override fun childCancelled(cause: Throwable): Boolean = false。 706 | ``` 707 | 708 | 在`tryMakeCompleting`中还有一个关键方法是`tryFinalizeFinishingState` 709 | 710 | 其中关键步骤为: 711 | 712 | ```java 713 | if (finalException != null) { 714 | val handled = cancelParent(finalException) || handleJobException(finalException) 715 | if (handled) (finalState as CompletedExceptionally).makeHandled() 716 | } 717 | ``` 718 | 上面代码中if (finalException != null && !cancelParent(finalException))语句可以看出,除非是 SupervisorJob 和 supervisorScope,一般协程出现未捕获异常时,不仅会取消父协程,一步步取消到最根部的协程,而且最后还由最根部的协程(Root Coroutine)处理协程。 719 | 720 | `handleJobException` 方法在 `StandaloneCoroutine`中实现为: 721 | 722 | ```kotlin 723 | override fun handleJobException(exception: Throwable): Boolean { 724 | handleCoroutineException(context, exception) 725 | return true 726 | } 727 | ``` 728 | 729 | ```kotlin 730 | public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) { 731 | // Invoke exception handler from the context if present 732 | try { 733 | context[CoroutineExceptionHandler]?.let { 734 | it.handleException(context, exception) 735 | return 736 | } 737 | } catch (t: Throwable) { 738 | handleCoroutineExceptionImpl(context, handlerException(exception, t)) 739 | return 740 | } 741 | // If handler is not present in the context or exception was thrown, fallback to the global handler 742 | handleCoroutineExceptionImpl(context, exception) 743 | } 744 | ``` 745 | 746 | 可以看出先有自己的ExceptionHandler处理,如果不存在或者处理过程中发生了异常则交由`handleCoroutineExceptionImpl`处理,这是一个internal 方法 由c实现。这里就不继续跟下去了。 747 | 748 | 以上我们就看到协程整个异常的处理过程,以及当发生异常后是如何取消父协程的。 749 | 5. **并发** 750 | 751 | 协程在运行时只是线程中的一块代码,线程的并发处理方式都可以用在协程上。不过协程还提供两种特有的方式,一是不阻塞线程的互斥锁Mutex,一是通过 ThreadLocal 实现的协程局部数据。 752 | 753 | * Mutex
754 | 在线程中锁都是阻塞式的,没获得锁时就没法执行相应的逻辑,而协程可以通过挂起函数解决这个问题,没有锁就挂起协程,获取后再恢复协程,挂起协程时并没有阻塞线程,可以执行其他逻辑。这就是互斥锁`Mutex`,它与synchronized关键字有些类似,还提供了`withLock`扩展函数,替代常用的mutex.lock; try {...} finally { mutex.unlock() }: 755 | 756 | ```kotlin 757 | fun main(args: Array) = runBlocking { 758 | val mutex = Mutex() 759 | var counter = 0 760 | repeat(10000) { 761 | GlobalScope.launch { 762 | mutex.withLock { 763 | counter ++ 764 | } 765 | } 766 | } 767 | println("The final count is $counter") 768 | } 769 | ``` 770 | 多个协程竞争的应该是同一个Mutex互斥锁。 771 | 772 | * 局部数据 773 | 774 | 在线程中我们使用ThreadLocal作为线程局部数据,每个线程中的数据都是独立的。kotlin中协程可以通过`ThreadLocal.asContextElement()`扩展函数实现协程局部数据,每次协程切换会恢复之前的值。 775 | 776 | ```kotlin 777 | fun main(args: Array) = runBlocking { 778 | val threadLocal = ThreadLocal().apply { set("Init") } 779 | printlnValue(threadLocal) 780 | val job = GlobalScope.launch(threadLocal.asContextElement("launch")) { 781 | printlnValue(threadLocal) 782 | threadLocal.set("launch changed") 783 | printlnValue(threadLocal) 784 | yield() 785 | printlnValue(threadLocal) 786 | } 787 | job.join() 788 | printlnValue(threadLocal) 789 | } 790 | ``` 791 | 792 | ```kotlin 793 | private fun printlnValue(threadLocal: ThreadLocal) { 794 | println("${Thread.currentThread()} thread local value: ${threadLocal.get()}") 795 | } 796 | ``` 797 | 其结果如下: 798 | 799 | ```java 800 | Thread[main,5,main] thread local value: Init 801 | Thread[DefaultDispatcher-worker-1,5,main] thread local value: launch 802 | Thread[DefaultDispatcher-worker-1,5,main] thread local value: launch changed 803 | Thread[DefaultDispatcher-worker-2,5,main] thread local value: launch 804 | Thread[main,5,main] thread local value: Init 805 | ``` 806 | 为什么在yied()之后值会变调呢? 807 | 808 | 在`ThreadContextElement`文件中的`asContextElement`方法如下: 809 | 810 | ```kotlin 811 | public fun ThreadLocal.asContextElement(value: T = get()): ThreadContextElement = 812 | ThreadLocalElement(value, this) 813 | ``` 814 | ![MacDown logo](./9.png) 815 | 816 | 其中`updateThreadContext`和`restoreThreadContext`分别用来更新和重置value. 817 | 818 | 那这两个方法又是何时被触发的呢?我们知道在yeid()之后会切换到另一个协程中,最终会调用到该协程的`resumeWith()`方法 819 | ![MacDown logo](./10.png) 820 | 821 | 不管最终是Dispatched.run()、DisptchedContinuation.resumeWith() 、DisptchedContinuation.resumeUndispatched(),最终都会执行`withCoroutineContext()`方法。 822 | ![MacDown logo](./11.png) 823 | 可以看见最终在这里都会重置TreadLocal的值。 824 | 825 | 所以 ThreadContextElement 并不能跟踪所有ThreadLocal对象的访问,而且每次挂起时更新的值将丢失。最重要的牢记它的原理:**启动和恢复时保存ThreadLocal在当前线程的值,并修改为 value,挂起和结束时修改当前线程ThreadLocal的值为之前保存的值**。 826 | 827 | 整篇文章我们就基本介绍什么是协程,协程的基本使用,以及部分原理。希望能对对家有所帮助,同时欢迎指正,后面会写一个对应实践的demo. 828 | 829 | 参考: 830 | 831 | [https://www.itcodemonkey.com/article/4620.html](https://www.itcodemonkey.com/article/4620.html). 832 | 833 | [https://johnnyshieh.me/posts/kotlin-coroutine-introduction/](https://johnnyshieh.me/posts/kotlin-coroutine-introduction/). 834 | 835 | 836 | 837 | -------------------------------------------------------------------------------- /kotlin中的协程/协程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/kotlin中的协程/协程.png -------------------------------------------------------------------------------- /产品心里学/README.md: -------------------------------------------------------------------------------- 1 | # 产品心理学 2 | 3 | ![aa](./产品心理学.png) -------------------------------------------------------------------------------- /产品心里学/产品心理学.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/产品心里学/产品心理学.png -------------------------------------------------------------------------------- /使用RemoteView自定义notification/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/1.png -------------------------------------------------------------------------------- /使用RemoteView自定义notification/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/2.png -------------------------------------------------------------------------------- /使用RemoteView自定义notification/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/3.jpg -------------------------------------------------------------------------------- /使用RemoteView自定义notification/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/4.png -------------------------------------------------------------------------------- /使用RemoteView自定义notification/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/5.jpg -------------------------------------------------------------------------------- /使用RemoteView自定义notification/6.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/6.JPG -------------------------------------------------------------------------------- /使用RemoteView自定义notification/7.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/7.JPG -------------------------------------------------------------------------------- /使用RemoteView自定义notification/README.md: -------------------------------------------------------------------------------- 1 | # Android 使用RemoteViews自定义通知栏 2 | 3 | ### 4 | 最近接到一个需求,应用需要使用常驻通知栏显示一些自定义的view,并伴随这个一系列操作和视图更新,因此在这里记录一下。 5 | 6 | #### 定义RemoteViews 7 | 8 | 1. 首先定义一个视图的XML文件,此处略去不表
9 | **PS:RemotesView只支持一些系统特定的view,并不支持一些自定义View.如下:** 10 | 11 | ![MacDown logo](./1.png) 12 | 13 | 2. 通过该XMl生成RemotesView,并且添加到notification中去 14 | 15 | 16 | 17 | mRemoteViews = new RemoteViews(ContextUtil.get().getPackageName(), R.layout.notification_normal_layout); 18 | 19 | 20 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this); 21 | builder.setSmallIcon(R.mipmap.icon_notification); 22 | builder.setContent(mRemoteViews); 23 | builder.setWhen(System.currentTimeMillis()); 24 | 25 | 26 | #### 更新视图 27 | 28 | RemotesView并不能直接获取到相应的View去设置相关属性它提供如下一些API去更新视图: 29 | [传送门](https://developer.android.com/reference/android/widget/RemoteViews#public-methods_3) 30 | 31 | 之后需要使用之前构建的notification去通知刷线 32 | 33 | NotificationManager notificationManager = (NotificationManager) ContextUtil.get().getSystemService(Context.NOTIFICATION_SERVICE); 34 | notificationManager.notify(FOREGROUND_ID, notification); 35 | 36 | ### 设置点击事件---跳转 37 | 因为RemoteView无法获取view,也就无法想传统的View一样设置点击事件 38 | 他是通过 39 | 40 | public void setOnClickPendingIntent (int viewId, 41 | PendingIntent pendingIntent) 42 | 去设置点击事件的,这里请注意一下`PendingIntent`,官方文档看着会比较懵逼,而网上会有一些误导性的文档介绍它 [比较好的一个解释](https://stackoverflow.com/questions/2808796/what-is-an-android-pendingintent) 和两张理解他:
43 | ![MacDown logo](./3.jpg) 44 | 45 | ![MacDown logo](./2.png) 46 | 47 | ps :PendingIntent 只能调用起三种组件: 48 | 49 | Activity
50 | Service
51 | Broadcast
52 | 53 | 所以,我们先构建出一个打开Activity的 PanddingItent 54 | 55 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 56 | 57 | 将其设置到相应的view上,此时点击view就会执行相应的跳转。 58 | 59 | ### 坑 60 | 61 | #### 坑一:通知栏点击无法收起 62 | 后面由于在点击RemoteViews上的视图的时候系统跳转的同时,更新视图,所以之前构建的打开的Activity的方法就无法做到了,此时只好通过构建广播的方式来处理跳转和视图更新 63 | 64 | Intent intent = null; 65 | intent = new Intent("XXXXX_NOTIFY_ACTION"); 66 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 67 | 68 | 然后通过RecieVer来处理 69 | 70 | 71 | public class NotificationforReceiver extends BroadcastReceiver { 72 | @Override 73 | public void onReceive(Context context, Intent intent) { 74 | String action = intent.getAction(); 75 | if (action.equals("COM_YMM_DRIVER_NOTIFY_ACTION")) { 76 | switch (intent.getStringExtra("id")) { 77 | case NotificationData.NOTIFICATION_TYPE_CARGO: 78 | context.startActivity(Router.route(context, Uri.parse("xxxx"))); 79 | NotificationData notificationCargo = NotificationViewHelper.get().getNotificationCargo(); 80 | notificationCargo.setMessageCount(0); 81 | NotificationViewHelper.get().newsIncoming(notificationCargo); 82 | break; 83 | case NotificationData.NOTIFICATION_TYPE_CHAT: 84 | context.startActivity(Router.route(context, Uri.parse("xxxx"))); 85 | break; 86 | case NotificationData.NOTIFICATION_TYPE_ORDER: 87 | context.startActivity(Router.route(context, Uri.parse("xxxxx"))); 88 | NotificationData notificationOrder = NotificationViewHelper.get().getNotificationOrder(); 89 | notificationOrder.setMessageCount(0); 90 | NotificationViewHelper.get().newsIncoming(notificationOrder); 91 | break; 92 | default: 93 | break; 94 | 95 | } 96 | } 97 | } 98 | } 99 | 100 | 101 | 但是此时出现了一个坑就是: 102 | **点击了通知之后Notification的Statusbar不会自动收起来了**,无法及时看到页面的跳转,用户体验不是很好,所以只好强制收起**StatusBar** 103 | 104 | public static void collapseStatusBar(Context context) { 105 | try { 106 | Object statusBarManager = context.getSystemService("statusbar"); 107 | Method collapse; 108 | 109 | if (Build.VERSION.SDK_INT <= 16) { 110 | collapse = statusBarManager.getClass().getMethod("collapse"); 111 | } else { 112 | collapse = statusBarManager.getClass().getMethod("collapsePanels"); 113 | } 114 | collapse.invoke(statusBarManager); 115 | } catch (Exception localException) { 116 | localException.printStackTrace(); 117 | } 118 | 119 | } 120 | 121 | 122 | 并且在manifeast中添加如下权限: 123 | 124 | 125 | 126 | 127 | 这样就会在点击的时候响应处理的同时自动收起通知栏 128 | 129 | #### 坑二:pendingIntent 设置的Extra数据数据收不到 130 | 当我给不同view设置点击事件的时候,创建了同一个intent,根据view id不同向intent中设置数据: 131 | 132 | private PendingIntent getPendingIntent(Context context, int resID) { 133 | Intent intent = null; 134 | intent = new Intent("COM_YMM_CONSIGNOR_NOTIFY_ACTION"); 135 | switch (resID) { 136 | case R.id.order_layout: 137 | intent.putExtra("id", NotificationData.NOTIFICATION_TYPE_ORDER); 138 | 139 | break; 140 | case R.id.chat_layout: 141 | intent.putExtra("id", NotificationData.NOTIFICATION_TYPE_CHAT); 142 | break; 143 | case R.id.cargo_layout: 144 | intent.putExtra("id", NotificationData.NOTIFICATION_TYPE_CARGO); 145 | break; 146 | } 147 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 148 | return pendingIntent; 149 | } 150 | 151 | 152 | 然而我点击不同事件的时候在receiver中收到的id都是一样的,导致bug,后来研究了官方文档以及PenddingIntent的flag 153 | 154 | 在设定PendingIntent时第四个参数flag值时,一定要细心理解: 155 | 156 | * FLAG_CANCEL_CURRENT:如果当前系统中已经存在一个相同的PendingIntent对象,那么就将先将已有的PendingIntent取消,然后重新生成一个PendingIntent对象。 157 | * FLAG_NO_CREATE:如果当前系统中不存在相同的PendingIntent对象,系统将不会创建该PendingIntent对象而是直接返回null。 158 | *FLAG_ONE_SHOT:该PendingIntent只作用一次。在该PendingIntent对象通过send()方法触发过后,PendingIntent将自动调用cancel()进行销毁,那么如果你再调用send()方法的话,系统将会返回一个SendIntentException。 159 | * FLAG_UPDATE_CURRENT:如果系统中有一个和你描述的PendingIntent对等的PendingInent,那么系统将使用该PendingIntent对象,但是会使用新的Intent来更新之前PendingIntent中的Intent对象数据,例如更新Intent中的Extras。 160 | 161 | 当发送两个包含相同的PendingIntent的Notification,发现其中一个可以点击触发,第一个点击没有任何反应。 162 | 创建一个PendingIntent对象,都是通过getActivity、getBroadcast、getService方法来获取的。如果传递给getXXX方法的Intent对象的Action是相同的,Data也是相同的,Categories也是相同的,Components也是相同的,Flags也是相同的),如果之前获取的PendingIntent对象还有效的话,那么后获取到的PendingItent并不是一个新创建的对象,而是对前一个对象的引用。 163 | 164 | 如果我们只是想通过设置不同的Extra来生成不同的PendingIntent对象是行不通的,因为PendingIntent对象由系统持有,并且系统只通过刚才在上面提到的几个要素来判断PendingIntent对象是否是相同的,那么如果我们想在每次更新PendingIntent对象的话,怎么做呢? 165 | 166 | 1. 在调用getXXX方法之前,先调用NotificationManager.cancel(notifyId)方法,将之前发送的PendingIntent对象从系统中移除 167 | 2. 也可以在调用getXXX方法时,将第二参数RequestCode设置成不同的值,这样每次就会创建新的PendingIntent对象 168 | 3. 为每一个点击事件生成不同的Intent 169 | 170 | 171 | private static PendingIntent getPendingIntent(Context context, int resID) { 172 | switch (resID) { 173 | case 1: 174 | Intent intent = new Intent("COM_YMM_CONSIGNOR_NOTIFY_ACTION_ORDER"); 175 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 176 | return pendingIntent; 177 | case 2: 178 | Intent intent2 = new Intent("COM_YMM_CONSIGNOR_NOTIFY_ACTION_CHAT"); 179 | PendingIntent pendingIntent2 = PendingIntent.getBroadcast(context, 0, intent2, PendingIntent.FLAG_CANCEL_CURRENT); 180 | return pendingIntent2; 181 | case 3: 182 | Intent intent3 = new Intent("COM_YMM_CONSIGNOR_NOTIFY_ACTION_CARGO"); 183 | PendingIntent pendingIntent3 = PendingIntent.getBroadcast(context, 0, intent3, PendingIntent.FLAG_CANCEL_CURRENT); 184 | return pendingIntent3; 185 | default: 186 | return null; 187 | } 188 | } 189 | 190 | #### 坑三:点击没反应 191 | 1. 对于getActivity,返回的PendingIntent递交给别的应用程序执行,这样就脱离了原始应用程序所在的task栈。 192 | getActivity最后的flag参数要设置成`Intent.FLAG_ACTIVITY_NEW_TASK`,才能成功启动PendingIntent中包含的activity。 193 | 2. 对于broadcast而言,因为PendingIntent是递交给别的应用程序执行,所以接收Broadcast的receiver必须设置**“export=true”**,才能接收到广播。但是有些手机上,经过测试即使“export=false”也还是能接收到广播,可能是OEM厂商对系统有所修改。但是建议最好设置成“export=true”。 194 | 3. 这个最恶心的问题也是坑了好久的,在某些机型(比如我用的`VIVO NEX`)会有严格的权限管理,禁止**后台弹出界面**,所以需要去打开相应的权限之后点击才能打开应用相应的页面。 195 | ![MacDown logo](./4.png) 196 | 197 | 198 | 199 | #### 坑四:通知栏文字颜色在部分机型通知栏为暗色的时候看不清楚 200 | 201 | 先看一下在不同手机上的表现: 202 | 203 | ![MacDown logo](./5.jpg) 204 | 205 | 在黑色上面的文字显示不清楚,但是别人家的应用在不同在亮色背景和暗色背景上表现的都很好,所以。。。 206 |
解决方案:尝试获取通知栏的主题颜色看看,根据该颜色去动态改变设置通知栏中文字的颜色,代码如下: 207 | 208 | ``` 209 | public static boolean isDarkNotificationTheme(Context context) { 210 | return !isSimilarColor(Color.BLACK, getNotificationColor(context)); 211 | } 212 | ``` 213 | 214 | /** 215 | * 获取通知栏颜色 216 | * 217 | * @param context 218 | * @return 219 | */ 220 | public static int getNotificationColor(Context context) { 221 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context); 222 | Notification notification = builder.build(); 223 | int layoutId = notification.contentView.getLayoutId(); 224 | ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null, false); 225 | if (viewGroup.findViewById(android.R.id.title) != null) { 226 | return ((TextView) viewGroup.findViewById(android.R.id.title)).getCurrentTextColor(); 227 | } 228 | return findColor(viewGroup); 229 | } 230 | 231 | private static boolean isSimilarColor(int baseColor, int color) { 232 | int simpleBaseColor = baseColor | 0xff000000; 233 | int simpleColor = color | 0xff000000; 234 | int baseRed = Color.red(simpleBaseColor) - Color.red(simpleColor); 235 | int baseGreen = Color.green(simpleBaseColor) - Color.green(simpleColor); 236 | int baseBlue = Color.blue(simpleBaseColor) - Color.blue(simpleColor); 237 | double value = Math.sqrt(baseRed * baseRed + baseGreen * baseGreen + baseBlue * baseBlue); 238 | if (value < 180.0) { 239 | return true; 240 | } 241 | return false; 242 | } 243 | 244 | 245 | private static int findColor(ViewGroup viewGroupSource) { 246 | int color = Color.TRANSPARENT; 247 | LinkedList viewGroups = new LinkedList<>(); 248 | viewGroups.add(viewGroupSource); 249 | while (viewGroups.size() > 0) { 250 | ViewGroup viewGroup1 = viewGroups.getFirst(); 251 | for (int i = 0; i < viewGroup1.getChildCount(); i++) { 252 | if (viewGroup1.getChildAt(i) instanceof ViewGroup) { 253 | viewGroups.add((ViewGroup) viewGroup1.getChildAt(i)); 254 | } else if (viewGroup1.getChildAt(i) instanceof TextView) { 255 | if (((TextView) viewGroup1.getChildAt(i)).getCurrentTextColor() != -1) { 256 | color = ((TextView) viewGroup1.getChildAt(i)).getCurrentTextColor(); 257 | } 258 | } 259 | } 260 | viewGroups.remove(viewGroup1); 261 | } 262 | return color; 263 | } 264 | 265 | 266 | 然后在通知栏的remoteView中设置相应的颜色 267 | 268 | mRemoteViews.setTextColor(R.id.notification_time, isDarkNotificationTheme(ContextUtil.get()) == true ? Color.WHITE : Color.BLACK); 269 | 270 | 271 | 再来验证一下效果: 272 | ![](./6.JPG) 273 | ![](./7.JPG) 274 | 275 | 276 | #### 坑五,在某些手机上当按home键之后通过通知栏进入应用会比较慢 277 | 一开始我们测试了一些手机发现在大多数手机上不存在这样的问题,打开还是很快的,只有在oppo的手机上会存在这样的问题,本来打算不解决了但是一是QA的较真下一是觉得这里面肯定有android系统机制的问题想了解这个问题,所以就研究了一下发现: 278 | 在谷歌的 Android API Guides 中,特意提醒开发者不要在后台启动 activity,包括在 Service 和 BroadcastReceiver 中,这样的设计是为了避免在用户毫不知情的情况下突然中断用户正在进行的工作,在  [http://developer.android.com/guide/practices/seamlessness.html#interrupt](http://developer.android.com/guide/practices/seamlessness.html#interrupt) 中有如下解释: 279 | 280 | 281 | **That is, don't call startActivity() from BroadcastReceivers or Services running in the background. Doing so will interrupt whatever application is currently running, and result in an annoyed user. Perhaps even worse, your Activity may become a "keystroke bandit" and receive some of the input the user was in the middle of providing to the previous Activity. Depending on what your application does, this could be bad news.** 282 | 283 | 即便如此,手机厂商的开发者们在开发基于系统级的应用的时候,可能仍然需要有从 Service 或 BroadcastReceiver 中 startActivity 的需求,往往这样的前提是连这样的 Service 或 BroadcastReceiver 也是由用户的某些操作而触发的,Service 或 BroadcastReceiver 只是充当了即将启动 activity 之前的一些代理参数检查工作以便决定是否需要 start 该 activity。 284 | 285 | 除非是上述笔者所述的特殊情况,应用开发者都应该遵循 “不要从后台启动 Activity”准则。 286 | 287 | 一个需要特别注意的问题是,特例中所述的情况还会遇到一个问题,**就是当通过 home 键将当前 activity 置于后台时,任何在后台startActivity 的操作都将会延迟 5 秒**,除非该应用获取了 **"android.permission.STOP_APP_SWITCHES"** 权限。 288 | 289 | 关于延迟 5 秒的操作在 com.android.server.am.ActivityManagerService 中的 stopAppSwitches() 方法中,系统级的应用当获取了 "android.permission.STOP_APP_SWITCHES" 后将不会调用到这个方法来延迟通过后台启动 activity 的操作,事实上 android 原生的 Phone 应用就是这样的情况,它是一个获取了"android.permission.STOP_APP_SWITCHES" 权限的系统级应用,当有来电时,一个从后台启动的 activity 将突然出现在用户的面前,警醒用户有新的来电,这样的设计是合理的。  290 | 291 | 所以,当你需要开发类似 Phone 这样的应用时,需要做如下工作: 292 | 293 | 1. root 你的手机; 294 | 2. 在 AndroidManifest.xml 中添加 "android.permission.STOP_APP_SWITCHES"  用户权限; 295 | 3. 将你开发的应用程序 push 到手机的 /system/app 目录中。 296 | 297 | 同时在stackoverflow上也提供了一个方法用于解决延迟5s启动的方式 298 | 299 | So instead of this 300 | 301 | Intent intent = new Intent(context, A.class); 302 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 303 | context.startActivity(intent); 304 | 305 | just do this 306 | 307 | Intent intent = new Intent(context, A.class); 308 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 309 | PendingIntent pendingIntent = 310 | PendingIntent.getActivity(context, 0, intent, 0); 311 | try { 312 | pendingIntent.send(); 313 | } catch (PendingIntent.CanceledException e) { 314 | e.printStackTrace(); 315 | } 316 | 317 | 经验证,确实有效 318 | 319 | #### 坑六 320 | 在线上环境的时候从buglg上面看到了一个如下的错误: 321 | 322 | #269468 android.os.TransactionTooLargeException 323 | data parcel size 576640 bytes 324 | 325 | com.xiwei.logistics.service.NotificationViewHelper.updateNotify(TbsSdkJava) 326 | 327 | 具体堆栈如下: 328 | 329 | 1.java.lang.RuntimeException:android.os.TransactionTooLargeException: data parcel size 634184 bytes 330 | 2 android.app.NotificationManager.notifyAsUser(NotificationManager.java:323) 331 | 3 ...... 332 | 4 Caused by: 333 | 5 android.os.TransactionTooLargeException:data parcel size 634184 bytes 334 | 6 android.os.BinderProxy.transactNative(Native Method) 335 | 7 android.os.BinderProxy.transact(Binder.java:802) 336 | 8 android.app.INotificationManager$Stub$Proxy.enqueueNotificationWithTag(INotificationManager.java:1422) 337 | 9 android.app.NotificationManager.notifyAsUser(NotificationManager.java:320) 338 | 10 android.app.NotificationManager.notify(NotificationManager.java:289) 339 | 11 android.app.NotificationManager.notify(NotificationManager.java:273) 340 | 看到问题文发生在 `NotificationManager.notifyAsUser` 341 |
只好去看源码喽,进入源码看到: 342 | 343 | 344 | */ 345 | public void notifyAsUser(String tag, int id, Notification notification, UserHandle user) 346 | { 347 | int[] idOut = new int[1]; 348 | INotificationManager service = getService(); 349 | String pkg = mContext.getPackageName(); 350 | // Fix the notification as best we can. 351 | Notification.addFieldsFromContext(mContext, notification); 352 | if (notification.sound != null) { 353 | notification.sound = notification.sound.getCanonicalUri(); 354 | if (StrictMode.vmFileUriExposureEnabled()) { 355 | notification.sound.checkFileUriExposed("Notification.sound"); 356 | } 357 | } 358 | fixLegacySmallIcon(notification, pkg); 359 | ...... 360 | 361 | 这里有一个调用了**`Notification.addFieldsFromContext(mContext, notification);`** 362 | 363 | 364 | /** 365 | * @hide 366 | */ 367 | public static void addFieldsFromContext(ApplicationInfo ai, int userId, 368 | Notification notification) { 369 | notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, ai); 370 | notification.extras.putInt(EXTRA_ORIGINATING_USERID, userId); 371 | } 372 | 373 | 可以看到在这里是往传入的notification的extras数据中添加相应的数据,Extras是一个**Bundle**,熟悉的开发同学可能都知道Bundle的数据大小是有限制的 374 | 375 | 376 | **“The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all 377 | transactions in progress for the process. Consequently this exception can be thrown when 378 | there are many transactions in progress even when most of the individual transactions are of 379 | moderate size.”** 380 | 381 | 所以问题知道了,我们再来看调用;之前每次更新我都使用了同一个RemoteView的实例,并且每次都会更新相应的文字,图标等,然后他所有的操作都会走到RemoteViews的`addAction()`方法 382 | 383 | 384 | private void addAction(Action a) { 385 | if (hasLandscapeAndPortraitLayouts()) { 386 | throw new RuntimeException("RemoteViews specifying separate landscape and portrait" + 387 | " layouts cannot be modified. Instead, fully configure the landscape and" + 388 | " portrait layouts individually before constructing the combined layout."); 389 | } 390 | if (mActions == null) { 391 | mActions = new ArrayList(); 392 | } 393 | mActions.add(a); 394 | 395 | // update the memory usage stats 396 | a.updateMemoryUsageEstimate(mMemoryUsageCounter); 397 | } 398 | 399 | 400 | 可以看到这里使用的是一个ArrayList,一直在填充数据,会导致mActions不断扩容变大,当在进行`NotificationManager.notifyAsUser`操作,由于需要跨进程通信,会将notify 和 remoteViews 进行序列化,从而导致这个问题 401 | 402 | 解决方案:更新的notify的时候需要每次创建一个新的RemoteViews,并且将新的remoteView 传递给notification 403 | 404 | private void updateNotify() { 405 | try { 406 | notification = ForegroundService.initNotification(ContextUtil.get(), mRemoteViews); 407 | if (notification != null) { 408 | NotificationManager notificationManager = (NotificationManager) ContextUtil.get().getSystemService(Context.NOTIFICATION_SERVICE); 409 | notificationManager.notify(FOREGROUND_ID, notification); 410 | } 411 | } catch (Exception e) { 412 | e.printStackTrace(); 413 | YmmLogger.commonLog().page("long_notification").elementId("create").view().param("error", e.getMessage()).enqueue(); 414 | } 415 | } 416 | 417 | #### 坑七 418 | 419 | 在bugly上面还看到很多关于remoteView nullPoint /mAction的数组越界的问题。 420 | 421 | 422 | java.lang.NullPointerException 423 | 424 | Attempt to invoke virtual method 'boolean android.widget.RemoteViews$Action.hasSameAppInfo(android.content.pm.ApplicationInfo)' on a null object reference 425 | 426 | 解析原始 427 | 1 android.widget.RemoteViews.writeToParcel(RemoteViews.java:3840) 428 | 2 android.app.Notification.writeToParcelImpl(Notification.java:2304) 429 | 3 android.app.Notification.writeToParcel(Notification.java:2248) 430 | 4 android.app.INotificationManager$Stub$Proxy.enqueueNotificationWithTag(INotificationManager.java:1404) 431 | 5 android.app.NotificationManager.notifyAsUser(NotificationManager.java:323) 432 | 6 android.app.NotificationManager.notify(NotificationManager.java:292) 433 | 7 android.app.NotificationManager.notify(NotificationManager.java:276) 434 | 435 | 还有 436 | 437 | java.lang.ArrayIndexOutOfBoundsException 438 | 439 | length=8; index=8 440 | 441 | 解析原始 442 | 1 android.util.ArraySet.freeArrays(ArraySet.java:214) 443 | 2 android.util.ArraySet.add(ArraySet.java:394) 444 | 3 android.app.Notification.- android_app_Notification_lambda$1(Notification.java:1956) 445 | 4 android.app.Notification$-void_writeToParcel_android_os_Parcel_parcel_int_flags_LambdaImpl0.onMarshaled(Notification.java) 446 | 5 android.app.PendingIntent.writeToParcel(PendingIntent.java:1051) 447 | 6 android.widget.RemoteViews$SetOnClickPendingIntent.writeToParcel(RemoteViews.java:773) 448 | 7 android.widget.RemoteViews.writeToParcel(RemoteViews.java:3509) 449 | 8 android.app.Notification.writeToParcelImpl(Notification.java:2026) 450 | 9 android.app.Notification.writeToParcel(Notification.java:1963) 451 | 10 android.app.INotificationManager$Stub$Proxy.enqueueNotificationWithTag(INotificationManager.java:872) 452 | 11 android.app.NotificationManager.notifyAsUser(NotificationManager.java:313) 453 | 12 android.app.NotificationManager.notify(NotificationManager.java:284) 454 | 13 android.app.NotificationManager.notify(NotificationManager.java:268) 455 | 14 com.xiwei.logistics.service.NotificationViewHelper.updateNotify(TbsSdkJava) 456 | 457 | 458 | 类似的日志还有很多,大多发生在remoteView做序列化,针对`mAction`的读写和反序列化操作的的上。 459 | 460 | 最终经过分析(其实看了每一个发生错误的地方的源码,都没看出来为什么,也检查了代码调用的地方也没防发现问题,差点就放弃了,因为这些崩溃量不大,但是还好我没放弃你),其实是**`多线程问题`**导致的,在我刷新这个remoteView的时候有两个数据来源,分别在不同的子线程中更新remoteView,而ArrayList刚好是一个非线程安全的集合类。 461 | 462 | **解决方案:** 463 | 464 | 465 | 因为我们没办法去修改remoteView的源代码,所以也不能将非线程安全的list换成线程安全的换成线程安全的,我们只好**将所有针对RemoteView的操作都放在同一个线程进行了(主线程或者子线程都可以)**。 466 | -------------------------------------------------------------------------------- /使用RemoteView自定义notification/image-2018-10-16-17-56-00-532.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/使用RemoteView自定义notification/image-2018-10-16-17-56-00-532.png -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/README.md: -------------------------------------------------------------------------------- 1 | # 基于Gradle Transform 和 ASM 实现Android应用的AOP编程 2 | 3 | ### 前言 4 | 在这个大数据的时代,作为软件服务提供方会尽可能的去收集一些用户使用过程中的数据以便优化服务,Android平台作为目前用户量最大的一个载体为人们提供各种软件服务,因此在Android平台上的软件数据采集就显得尤为重要(在此我们先不讨论用户隐私和数据滥用的问题),但是一些传统的方式需要手动在代码中添加一些代码,当项目过大的情况下会发生遗漏,工作量大等情况发生,同时针对第三方提供的一些SDK服务我们无法添加代码的情况。幸好,在Android平台下我们可以借助一些其他的手段来完成此项工作,比如在编译期间扫描整个项目的class文件,在需要修改和添加的地方进行改动,此篇文章介绍了一种基于`gradle transhform API`和字节码修改库`ASM`实现在Android中的AOP编程方式 5 | 6 | ### 认识Transform 7 | 先来看一下官方对于`Transform`的定义
8 |
9 | ![Mou icon](./resources/1.jpg) 10 | 11 | 重点在于在 **能够处理dex之前的已经编译好的class文件**,**多个tansform之间执行无序**,**需要配合gradle插件使用** 12 | 13 | 那么回顾一下Android的编译过程 14 | ![Mou icon](./resources/build.png) 15 |
16 | 可以看见transform能发挥作用的地方就是在 `.class Files`这一步,它的每一次处理都是一个输入处理和输出的过程,并且输出地址不是由开发者任意指定的而是由输入内容、作用范围等因素由`TransformOutputProvider `提供的。 17 | ![Mou icon](./resources/2.png) 18 | 19 | 在代码中`Transform`是一个虚类 20 | ```java 21 | public abstract class Transform { 22 | public Transform() { 23 | } 24 | 25 | public abstract String getName(); 26 | 27 | public abstract Set getInputTypes(); 28 | 29 | public Set getOutputTypes() { 30 | return this.getInputTypes(); 31 | } 32 | 33 | public abstract Set getScopes(); 34 | 35 | public Set getReferencedScopes() { 36 | return ImmutableSet.of(); 37 | } 38 | 39 | /** @deprecated */ 40 | @Deprecated 41 | public Collection getSecondaryFileInputs() { 42 | return ImmutableList.of(); 43 | } 44 | 45 | public Collection getSecondaryFiles() { 46 | return ImmutableList.of(); 47 | } 48 | 49 | public Collection getSecondaryFileOutputs() { 50 | return ImmutableList.of(); 51 | } 52 | 53 | public Collection getSecondaryDirectoryOutputs() { 54 | return ImmutableList.of(); 55 | } 56 | 57 | public Map getParameterInputs() { 58 | return ImmutableMap.of(); 59 | } 60 | 61 | public abstract boolean isIncremental(); 62 | 63 | /** @deprecated */ 64 | @Deprecated 65 | public void transform(Context context, Collection inputs, Collection referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { 66 | } 67 | 68 | public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { 69 | this.transform(transformInvocation.getContext(), transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), transformInvocation.getOutputProvider(), transformInvocation.isIncremental()); 70 | } 71 | 72 | public boolean isCacheable() { 73 | return false; 74 | } 75 | } 76 | ``` 77 | 看到函数`transform`,我们还没有具体实现这个函数,这个函数就是具体如何处理输入和输出。`getScopes`函数定义了输入范围可以是整个项目所有类,也可以是自己项目中的类。 78 | 79 | 80 | ### 实现自定义Transform 81 | 82 | 我们在插件项目中(如何实现gradle插件请看另一篇[文章](https://github.com/carl1990/AndroidLearnBlog/tree/master/gralde%20%E6%8F%92%E4%BB%B6))集成Transform类实现transform方法 83 | ```groovy 84 | @Override 85 | void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { 86 | this.transform(transformInvocation.getContext(), transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), transformInvocation.getOutputProvider(), transformInvocation.isIncremental()); 87 | //开始计算消耗的时间 88 | Logger.info("||=======================================================================================================") 89 | Logger.info("|| 开始计时 ") 90 | Logger.info("||=======================================================================================================") 91 | def startTime = System.currentTimeMillis() 92 | transformInvocation.getInputs().each { 93 | TransformInput input -> 94 | input.jarInputs.each { 95 | JarInput jarInput -> 96 | String destName = jarInput.file.name 97 | /** 截取文件路径的md5值重命名输出文件,因为可能同名,会覆盖*/ 98 | def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8) 99 | if (destName.endsWith(".jar")) { 100 | destName = destName.substring(0, destName.length() - 4) 101 | } 102 | /** 获得输出文件*/ 103 | File dest = transformInvocation.getOutputProvider().getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR) 104 | Logger.info("||-->开始遍历特定jar ${dest.absolutePath}") 105 | 106 | def modifiedJar = modifyJarFile(jarInput.file, transformInvocation.context.getTemporaryDir()) 107 | Logger.info("||-->结束遍历特定jar ${dest.absolutePath}") 108 | if (modifiedJar == null) { 109 | modifiedJar = jarInput.file 110 | } 111 | FileUtils.copyFile(modifiedJar, dest) 112 | } 113 | 114 | input.directoryInputs.each { 115 | DirectoryInput directoryInput -> 116 | File dest = transformInvocation.getOutputProvider().getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) 117 | File dir = directoryInput.file 118 | if (dir) { 119 | HashMap modifyMap = new HashMap<>() 120 | dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { 121 | File classFile -> 122 | if (!name.endsWith("R.class") 123 | && !name.endsWith("BuildConfig.class") 124 | && !name.contains("R\$")) { 125 | File modified = modifyClassFile(dir, classFile, transformInvocation.context.getTemporaryDir()) 126 | if (modified != null) { 127 | //key为相对路径 128 | modifyMap.put(classFile.absolutePath.replace(dir.absolutePath, ""), modified) 129 | } 130 | } 131 | 132 | } 133 | FileUtils.copyDirectory(directoryInput.file, dest) 134 | modifyMap.entrySet().each { 135 | Map.Entry en -> 136 | File target = new File(dest.absolutePath + en.getKey()) 137 | 138 | FileUtils.copyFile(en.getValue(), target) 139 | en.getValue().delete() 140 | 141 | } 142 | } 143 | } 144 | 145 | 146 | } 147 | //计算耗时 148 | def cost = (System.currentTimeMillis() - startTime) / 1000 149 | Logger.info("||=======================================================================================================") 150 | Logger.info("|| 计时结束:费时${cost}秒 ") 151 | Logger.info("||=======================================================================================================") 152 | 153 | } 154 | ``` 155 | 在这个方法中我们分别遍历了jar包中的class文件和目录中的class文件,并查找到我们想要修改的类,对他进行读写、修改操作,也就是上文提到的 输入-->处理-->输出操作,在此期间所有的输入线相关信息由`TransformInput`提供,输出相关信息由`TransformOutputProvider`提供。具体的修改操作我将会在下一章节中讲到。 156 | 157 | ### Transform的使用 158 | 开始的时候我们就讲到Transform需要配合gradle插件使用,其实使用起来很简单,只需要把它注册在Plugin中就好了,在自定义的Plugin中的apply方法中: 159 | ```groovy 160 | def android = project.extensions.getByType(AppExtension) 161 | MyTransform myTransform = new MyTransform() 162 | android.registerTransform(myTransform) 163 | ``` 164 | 即可注册上该Transform 165 | 166 | 167 | ### ASM介绍 168 | 169 | [ASM](https://asm.ow2.io/)是一款基于java字节码层面的代码分析和修改工具。无需提供源代码即可对应用嵌入所需debug代码,用于应用API性能分析。ASM可以直接产生二进制class文件,也可以在类被加入JVM之前动态修改类行为。其结构如下:
170 | ![Mou icon](./resources/3.png) 171 | 172 | * **Core** 为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换 173 | * **Tree**提供了Java字节码在内存中的表现 174 | * Analysis为存储在tree包结构中的java方法字节码提供基本的数据流统计和类型检查算法 175 | * **Commons**提供一些常用的简化字节码生成转化和适配器 176 | * Util包含一些帮助类和简单的字节码修改,有利于在开发或者测试中使用 177 | * XML提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化。 178 | 179 | ASM的处理过程也是一个典型的生产者和消费者模式,这点比较契合Transform的转换输入输出模型。 180 | 其提供关键的几个类来实现字节码的访问和修改: 181 | 182 | * **ClassReader**:该类用来解析编译过的class字节码文件。 183 | * **ClassWriter**:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。 184 | * **ClassVisitor**:主要负责 “拜访” 类成员信息。其中包括标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块。 185 | * **AdviceAdapter**:实现了MethodVisitor接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。 186 | 187 | 其流程如下: 188 | 189 | 190 | ![Mou icon](./resources/4.png) 191 | 192 | ![Mou icon](./resources/5.png) 193 | 194 | 195 | ### 使用ASM修改Class 196 | 197 | 1. 读取相应的类文件 198 | 在上面Transform章节中我们通过遍历相应的class文件,然后对其进行读取转换为字节流 199 | 200 | ```java 201 | private static File modifyClassFile(File dir, File classFile, File tempDir) { 202 | File modified = null 203 | try { 204 | //路径转换将 xxx/xxx/xxx -> xxx.xxx.xxx 并且去掉后缀.class 205 | String className = com.carl.plugins.TextUtil.path2ClassName(classFile.absolutePath.replace(dir.absolutePath + File.separator, "")) 206 | byte[] sourceClassBytes = IOUtils.toByteArray(new FileInputStream(classFile)) 207 | byte[] modifyClassBytes = CarlModify.modifyClasses(sourceClassBytes) 208 | if (modifyClassBytes) { 209 | modified = new File(tempDir, className.replace('.', '') + '.class') 210 | if (modified.exists()) { 211 | modified.delete() 212 | } 213 | modified.createNewFile() 214 | new FileOutputStream(modified).write(modifyClassBytes) 215 | } 216 | } catch (Exception e) { 217 | e.printStackTrace() 218 | } 219 | return modified 220 | } 221 | 222 | ``` 223 | 224 | 2.使用ASM相应的类来处理该类的字节流文件 225 | 226 | ```java 227 | 228 | static byte[] modifyClass(byte[] srcByteCode) { 229 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 230 | ClassVisitor classVisitor = new CarlClassVisitor(classWriter); 231 | ClassReader classReader = new ClassReader(srcByteCode); 232 | classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); 233 | return classWriter.toByteArray(); 234 | } 235 | ``` 236 | 237 | ClassWriter有`COMPUTE_MAXS:`让系统自动为我们计算栈和本地变量的大小 和`COMPUTE_FRAMES:`指定系统自动为我们计算栈帧的大小 两个模式。 238 | 239 | ClassReader有`SKIP_CODE` `SKIP_DEBUG` `SKIP_FRAMES` `EXPAND_FRAMES` 四种模式。 240 | 241 | SKIP_CODE不访问方法实体,对于只需要获取class框架的场景非常适用 242 | 243 | SKIP_Debug不会访问debug信息了。 244 | 245 | SKIP_FRAMES 跳过stack map frames 246 | 247 | EXPAND_FRAMES 不再压缩frames 248 | 249 | 然后主要的修改的操作都在自定义的ClassVisitor方法中,ClassVisitor提供以下方法用来操作类的不同内容 250 | ![Mou icon](./resources/6.jpg) 251 | 252 | 我的实现中主要处理了visitMethod()方法 253 | 254 | ```java 255 | 256 | @Override 257 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 258 | MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions); 259 | MethodVisitor adapter = null; 260 | 261 | if ((isHintClass && ASMUtil.isMatchingMethod(name, desc))) { 262 | //指定方法名,根据满足的类条件和方法名 263 | System.out.println("||-----------------开始修改方法:" + name + "--------------------------"); 264 | try { 265 | adapter = ASMUtil.getMethodVisitor(mInterfaces, mClassName, superName, methodVisitor, access, name, desc); 266 | } catch (Exception e) { 267 | e.printStackTrace(); 268 | adapter = null; 269 | } 270 | } else { 271 | System.out.println("||---------------------查看修改后方法:" + name + "-----------------------------"); 272 | adapter = new CarlMethodVisitor(methodVisitor, access, name, desc); 273 | } 274 | if (adapter != null) { 275 | return adapter; 276 | } 277 | return methodVisitor; 278 | } 279 | ``` 280 | 281 | 其中首先过滤相应的类和方法: 282 | 283 | 过滤类 284 | 285 | ```java 286 | 287 | public static boolean isMatchingClass(String className, String[] interfaces) { 288 | boolean isMeetClassCondition = isMatchingInterfaces(interfaces, "android/view/View$OnClickListener"); 289 | //剔除掉以android开头的类,即系统类,以避免出现不可预测的bug 290 | if (className.startsWith("android")) { 291 | return false; 292 | } 293 | // 是否满足实现的接口 294 | if (isMeetClassCondition || className.contains("Fragment")) { 295 | isMeetClassCondition = true; 296 | } 297 | if (isMeetClassCondition || className.contains("Activity")) { 298 | isMeetClassCondition = true; 299 | } 300 | return isMeetClassCondition; 301 | } 302 | ``` 303 | 304 | 305 | 过滤方法 306 | ```java 307 | static boolean isMatchingMethod(String name, String desc) { 308 | if ((name.equals("onClick") && desc.equals("(Landroid/view/View;)V")) 309 | || (name.equals("onResume") && desc.equals("()V")) 310 | || (name.equals("onPause") && desc.equals("()V")) 311 | || (name.equals("setUserVisibleHint") && desc.equals("(Z)V")) 312 | || (name.equals("onHiddenChanged") && desc.equals("(Z)V")) 313 | ) { 314 | return true; 315 | } else { 316 | return false; 317 | } 318 | } 319 | ``` 320 | 321 | 然后对方法进行修改,修改的地方AMS提供了一个`AdviceAdapter`类,其提供的方法有: 322 | ![Mou icon](./resources/7.jpg) 323 | 324 | 请重点注意onMethodEnter() 和 onMetheodExit() 方法。这两个方法见名知意,是在方法进入和方法退出的回调,我主要的修改的就是在这里进行的。 325 | 326 | ```java 327 | 328 | static MethodVisitor getMethodVisitor(String[] interfaces, String className, String superName, 329 | MethodVisitor methodVisitor, int access, String name, String desc) { 330 | MethodVisitor adapter = null; 331 | 332 | if (name.equals("onClick") && isMatchingInterfaces(interfaces, "android/view/View$OnClickListener")) { 333 | System.out.println("||* visitMethod * onClick"); 334 | adapter = new CarlMethodVisitor(methodVisitor, access, name, desc) { 335 | @Override 336 | protected void onMethodEnter() { 337 | super.onMethodEnter(); 338 | 339 | // ALOAD 25 获取方法的第一个参数 340 | methodVisitor.visitVarInsn(ALOAD, 1); 341 | // INVOKESTATIC INVOKESTATIC 342 | methodVisitor.visitMethodInsn(INVOKESTATIC, "com/carl/asmtest/tack/TrackHelper", "onClick", "(Landroid/view/View;)V", false); 343 | } 344 | }; 345 | return adapter; 346 | } 347 | 348 | ``` 349 | 350 | 首先更具方法名和该类所实现的接口判断是否进行处理,这里我处理的是点击事件方法,然后创建一个自定义的`AdviceAdapter`,在方法的退出事件,添加自己的代码。 351 | 352 | `TrackHelper`的实现如下: 353 | ![Mou icon](./resources/9.jpg) 354 | 对于`"(Landroid/view/View;)V"`这个东西叫做方法的描述符,我们可以先编译该类,然后通过`javap -s `命令查看相应的方法的描述,之所以需要这个是为了确定方法的唯一性,因为java中可能存在重载方法。 355 | ![Mou icon](./resources/8.jpg) 356 | 357 | 这种方式在JNI中C调用Java方法需要的描述是一样的,是否C也是通过操作内存中的字节码来执行JAVA方法的呢? 358 | 359 | 这样呢,我们就在项目中的onclick(View view)方法调用结束的时候插入了TrackHelper.onClick(View view)这段代码。 360 | 361 | ### 结果验证 362 | 把该插架项目打包后,在demo APP中使用该插件,如何使用不再赘述。编译项目我们可以看见如下关键log 363 | ![Mou icon](./resources/11.jpg) 364 | 可以看见在我设定的目标类中和目标方法中做了相应的操作的。 365 | 366 | PS:由于会遍历类和对类进行操作,这样做的后果之一就是会使项目的编译速度降低,对于我的测试项目遍历JAR和项目class分辨耗时如下: 367 | 368 | 遍历项目class 369 | ![Mou icon](./resources/12.jpg) 370 | 371 | 遍历jar 372 | ![Mou icon](./resources/13.jpg) 373 | 374 | 我在activity的onresume() 和 onPause() 以及点击事件中分别添加了相应的方法,运行起来测试项目,可以看到是我们预想的结果 375 | ![Mou icon](./resources/14.png) 376 | 377 | 378 | 好了,到这里我们就完成了借助Gradel transform 和 ASM 实现了android平台在编译期间修改Class的方式实现AOP编程,希望你能都利用它完成更多有意思的事情。 379 | 380 | [demo](https://github.com/carl1990/ASMTest)地址 -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/1.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/11.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/12.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/13.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/14.png -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/2.png -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/3.png -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/4.png -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/5.png -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/6.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/7.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/8.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/9.jpg -------------------------------------------------------------------------------- /基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/基于Gradle Transform 和 ASM 实现Android应用的AOP编程/resources/build.png -------------------------------------------------------------------------------- /数据收集埋点以及android无埋点方式统计原生和H5点击事件/1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/数据收集埋点以及android无埋点方式统计原生和H5点击事件/1-2.png -------------------------------------------------------------------------------- /数据收集埋点以及android无埋点方式统计原生和H5点击事件/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/数据收集埋点以及android无埋点方式统计原生和H5点击事件/2.jpg -------------------------------------------------------------------------------- /数据收集埋点以及android无埋点方式统计原生和H5点击事件/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/数据收集埋点以及android无埋点方式统计原生和H5点击事件/3.png -------------------------------------------------------------------------------- /数据收集埋点以及android无埋点方式统计原生和H5点击事件/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carl1990/AndroidLearnBlog/4526e079b769bd4d0af0d4535ef1db32bdb0c217/数据收集埋点以及android无埋点方式统计原生和H5点击事件/4.png -------------------------------------------------------------------------------- /数据收集埋点以及android无埋点方式统计原生和H5点击事件/README.md: -------------------------------------------------------------------------------- 1 | # 数据收集埋点及android无埋点方式统计原生和H5点击事件实现 2 | ## 前言 3 | 从移动端角度来说,我相信在同一家公司中,不同团队对于前端埋点需求都是不一样的。简单来说产品角度更加注重的是埋点所带来的大量用户行为数据,能否通过一定数据漏斗分析挖掘找到当前产品的问题,并能对当下产品模型制定一定改善计划或策略。指标当然也是完全迥异的,挖掘潜在需求、分解不同用户群日常习惯、提高用户留存、减少页面间流失率、分析当前用户群画像等。而从运营角度来说,找到与产品调性较为匹配的投放渠道、估算不同渠道之间拉取新增的实际成本、运营创意方向的选择与取舍,热点事件借势营销策略等。这些都是我们随时要面临的问题,都需要不断的判断与决策。而决策的依据是什么?-**数据**
4 | 那么数据从何而来呢?当然是我们在前端的收集而来啦?一个典型的数据平台大概是这个样子的:
5 | ![Mou icon](./1-2.png) 6 | 所以数据采集的也是该系统的核心。数据采集是否丰富,采集的数据是否准确,采集是否及时,都直接影响整个数据平台的应用的效果。 7 | 8 | ## 前端埋点技术介绍 9 | 10 | ### 1. 代码埋点 11 | 代码埋点很早就出现了,而且目前来说应用的的比较多,像国内的 `百度统计` `腾讯MAT` `阿里的Umeng`目前提供的API都是这种方式的。
12 | 13 | 它的技术原理也很简单,在APP或者界面初始化的时候,初始化第三方数据分析服务商的SDK,然后在某个事件发生时就调用SDK里面相应的数据发送接口发送数据。例如,我们想统计APP里面某个按钮的点击次数,则在APP的某个按钮被点击时,可以在这个按钮对应的 OnClick 函数里面调用SDK提供的数据发送接口来发送数据. 14 | 15 | 以`Umeng`为例 16 | 17 | public void onResume() { 18 | super.onResume(); 19 | MobclickAgent.onResume(this); //统计时长 20 | } 21 | 22 | public void onPause() { 23 | super.onPause(); 24 | MobclickAgent.onPause(this); 25 | } 26 | 27 | 事件统计如下: 28 | 29 | MobclickAgent.onEvent(Context context, String eventId); 30 | 31 | 32 | 从上面这两个例子可以看出,代码埋点的优点是一方面使用者控制精准,可以非常精确地选择什么时候发送数据;同时使用者可以比较方便地设置自定义属性、自定义事件,传递比较丰富的数据到服务端。
33 | 代码埋点上都面临一个问题-成本高。首先埋点地方过多,因为不同的版本验证问题不同不易于管理。每一个控件的埋点都需要添加相应的手工代码,不仅工作量大,而且限定了必须是技术人员才能完成.漏埋无法获取到数据 34 | ![Mou icon](./2.jpg) 35 | 36 | ### 2. 可视化埋点 37 | 从前端埋点到可视化埋点其实是一个非常顺理成章的演进。既然埋点代价大,每一个埋点都需要写代码,那么,就参考 Visual Studio 等一系列现代 IDE 的做法,用可视化交互手段来代替写代码即可;既然每次埋点更新都需要等待APP的更新,那么,就参考现在很多手游的做法,把核心代码和配置、资源分开,在APP启动的时候通过网络更新配置和资源即可。
38 | 正是出于这种自然而然的做法,在国外,以 [Mixpanel](https://github.com/mixpanel) 为首的数据分析服务商,都相继提供了可视化埋点的方案,Mixpanel将之称作为 codeless。而国内的 `TalkingData`、`诸葛IO` 等也都提供了类似的技术手段。 39 | ![Mou icon](./3.png) 40 | 在Mixpanel官方事例中,从如上界面截图可以看到,当你点击底部电影选项右上角分享按钮时,在弹出的增加锚点窗口中,设置点击这个按钮将发送的是 “Shared movie clip” 事件。然后点击 Deploy 按钮,把这个配置下发到客户端。那么在嵌入了 Mixpanel 的 SDK 的这个 APP中 ,则自动会在 APP 启动时或者客户端定时获取的方式,更新后台设置的锚点统计配置。当配置完成在真实的用户使用时,点击这个分享按钮就会真正地发送 “Shared movie clip” 事件到后台,且实时可见。 41 | 42 | **那这种方式是如何实现的呢?** 43 | 44 | 简单来说在客户端集成了Mixpanel Sdk之后,Sdk会定时(例如每秒)做一次截图。在截图的同时,Mixpanel会从 keyWindow 对象开始进行遍历它的所有subviews()集合,得到当前视图下所有 UIView、UIResponder 对象的层级关系。Mixpanel会根据截图和UI元素的可视化信息来重新进行页面渲染,并且根据控件的类型,来识别哪些控件是可以增加可埋点的,并且可以在后台操作。当使用者在后台的截屏画面上点击了某个可埋点的控件时,后台会根据使用者做一些事件关联方面的配置,并且将配置信息进行保存和部署到客户端。客户端在开启或定时获取后台锚点配置之后,则会根据新的锚点配置采集数据。整个过程部署都是实时的。 45 | 46 | ### 无埋点 47 | 与可视化埋点一样,“无埋点”这个方案也出来的比较早,[Heap](https://heapanalytics.com/)作为一个第三方数据分析服务商,在2013年就已经推出了“无埋点”这个技术方案。而如果不局限于第三方,`百度`在2009年就已经有了`点击猴子`这个技术,用无埋点的方案分析一个页面各个元素的点击情况;在2011年,百度质量部也推出了一项内部服务,用以录制安卓 App 的全部操作,并且进行回放,以便找出 App 崩溃的原因;而豌豆荚大约也在2013年左右,在自己的 App 内部,添加了对所有控件的操作情况的记录。第三方数据分析服务GrowingIO 在2015年,也推出了类似于 Heap 的服务。 48 | ![Mou icon](./4.png)
49 | 从界面上看,和可视化埋点很像。而从实际的实现上看,二者的区别就是可视化埋点先通过界面配置哪些控件的操作数据需要收集;“无埋点”则是先尽可能收集所有的控件的操作数据,然后再通过界面配置哪些数据需要在系统里面进行分析。 50 | 51 | “无埋点”相比可视化埋点的优点,一方面是解决了数据“回溯”的问题,例如,在某一天,突然想增加某个控件的点击的分析,如果是可视化埋点方案,则只能从这一时刻向后收集数据,而如果是“无埋点”,则从部署 SDK 的时候数据就一直都在收集了;另一方面,“无埋点”方案也可以自动获取很多启发性的信息,例如,“无埋点”可以告诉使用者这个界面上每个控件分别被点击的概率是多大,哪些控件值得做更进一步的分析等等。 52 | 53 | 万事皆有利弊无埋点方式的缺点就是不能灵活的自定义属性数据,数据收集和传输的压力(因为它会无差别收集所有界面的点击事件) 54 | 55 | ##android自动埋点统计实现方式 56 | 近日开发出的SDK采用了无埋点的统计点击事件的方式。原理主要根据android的`事件分发`和 `视图判断`,众所周知Activity中的UI布局是嵌套的,通过`findViewById(android.R.id.content)`可以拿到activity的根视图。android系统的点击事件的传递是由父视图向子视图传递,然后再传到具体的控件中。如果不了解android系统的事件分发机制可以先看一下[android 事件分发机制](http://blog.csdn.net/guolin_blog/article/details/9097463)。 57 | 58 | 好了,有了以上的知识准备开始上代码了,对外提供一个Activity,让接入的app的baseActivity继承自该activity 59 | 60 | 在activity中重写`dispatchTouchEvent(MotionEvent ev)`方法 61 | 62 | public boolean dispatchTouchEvent(MotionEvent ev) { 63 | if (ev.getAction() == MotionEvent.ACTION_UP) { 64 | View view = searchClickView(findViewById(android.R.id.content), ev); 65 | if (view != null && (view.hasOnClickListeners())) { // 此处必须判断是否设置了onClickListner 66 | *****记录对应的log****** 67 | } 68 | } 69 | return super.dispatchTouchEvent(ev); 70 | } 71 | 72 | 做hit-test 查找当前当前点击事件发生在哪一个view区域内 73 | 74 | private boolean isInClickView(View view, MotionEvent event) { 75 | float clickX = event.getRawX(); 76 | float clickY = event.getRawY(); 77 | //如下的view表示Activity中的子View或者控件 78 | int[] location = new int[2]; 79 | view.getLocationOnScreen(location); 80 | int x = location[0]; 81 | int y = location[1]; 82 | int width = view.getWidth(); 83 | int height = view.getHeight(); 84 | if ((clickX >= x && clickX <= (x + width)) && 85 | (clickY >= y && clickY <= (y + height))) { 86 | return true; //这个条件成立,则判断点击时间发生在这个view区域内 87 | } 88 | return false; 89 | } 90 | 91 | 遇到viewGroup递归遍历,同时只对可见(enable)的view 进行该处理 92 | 93 | private View searchClickView(View view, MotionEvent event) { 94 | View clickView = null; 95 | if (isInClickView(view, event) && 96 | view.getVisibility() == View.VISIBLE) { //这里一定要判断View是可见的 97 | if (view instanceof ViewGroup) { //遇到一些Layout之类的ViewGroup,继续遍历它下面的子View 98 | ViewGroup group = (ViewGroup) view; 99 | for (int i = group.getChildCount() - 1; i >= 0; i--) { 100 | View chilView = group.getChildAt(i); 101 | clickView = searchClickView(chilView, event); 102 | if (clickView != null) { 103 | return clickView; 104 | } 105 | } 106 | } 107 | clickView = view; 108 | } 109 | return clickView; 110 | } 111 | 112 | 113 | 这样我们就可以获取到每一个页面中的view的点击事件,实现自动埋点、无差别统计。虽然onClick()比较主流但是不排除有些地方我们会使用`onTouch()`的方式为view处理响应事件,这个上面的方式就统计不到了。为了支持ontouch的统计,就需要像`view.hasOnClickListeners()`一样去判断该view是否设置了onTouchListener,但android并没有提供这样的API只能自己想办法。 114 | 先看一下`setOnTouchListener()` 115 | 116 | public void setOnTouchListener(OnTouchListener l) { 117 | getListenerInfo().mOnTouchListener = l; 118 | } 119 | 120 | 最终信息保存在`ListenerInfo`中的`mOnTouchListener`意味着我们只要拿到ListenerInfo.mOnTouchListener看看它有没有值就好了 121 | static class ListenerInfo { 122 | /** 123 | * Listener used to dispatch focus change events. 124 | * This field should be made private, so it is hidden from the SDK. 125 | * {@hide} 126 | */ 127 | protected OnFocusChangeListener mOnFocusChangeListener; 128 | 129 | /** 130 | * Listeners for layout change events. 131 | */ 132 | private ArrayList mOnLayoutChangeListeners; 133 | 134 | protected OnScrollChangeListener mOnScrollChangeListener; 135 | 136 | /** 137 | * Listeners for attach events. 138 | */ 139 | private CopyOnWriteArrayList mOnAttachStateChangeListeners; 140 | 141 | /** 142 | * Listener used to dispatch click events. 143 | * This field should be made private, so it is hidden from the SDK. 144 | * {@hide} 145 | */ 146 | public OnClickListener mOnClickListener; 147 | 148 | /** 149 | * Listener used to dispatch long click events. 150 | * This field should be made private, so it is hidden from the SDK. 151 | * {@hide} 152 | */ 153 | protected OnLongClickListener mOnLongClickListener; 154 | 155 | /** 156 | * Listener used to build the context menu. 157 | * This field should be made private, so it is hidden from the SDK. 158 | * {@hide} 159 | */ 160 | protected OnCreateContextMenuListener mOnCreateContextMenuListener; 161 | 162 | private OnKeyListener mOnKeyListener; 163 | 164 | private OnTouchListener mOnTouchListener; 165 | 166 | private OnHoverListener mOnHoverListener; 167 | 168 | private OnGenericMotionListener mOnGenericMotionListener; 169 | 170 | private OnDragListener mOnDragListener; 171 | 172 | private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener; 173 | 174 | OnApplyWindowInsetsListener mOnApplyWindowInsetsListener; 175 | } 176 | 177 | 178 | 这是一个View中的一个静态内部类,而mOnTouchListener是一个private的类成员变量。所以第一时间想到用反射去做这件事,集体代码如下: 179 | 180 | private boolean hasOnTouchListener(View view) { 181 | boolean result = false; 182 | Field listenerInfoField = null; 183 | try { 184 | listenerInfoField = Class.forName("android.view.View").getDeclaredField("mListenerInfo"); 185 | if (listenerInfoField != null) { 186 | listenerInfoField.setAccessible(true); 187 | } 188 | Object myLiObject = null; 189 | myLiObject = listenerInfoField.get(view); 190 | 191 | // get the field mOnClickListener, that holds the listener and cast it to a listener 192 | Field listenerField = null; 193 | listenerField = Class.forName("android.view.View$ListenerInfo").getDeclaredField("mOnTouchListener"); 194 | if (listenerField != null && myLiObject != null) { 195 | listenerField.setAccessible(true); 196 | View.OnTouchListener myListener = (View.OnTouchListener) listenerField.get(myLiObject); 197 | if (myListener != null) { 198 | return true; 199 | } 200 | } 201 | } catch (NoSuchFieldException e) { 202 | e.printStackTrace(); 203 | } catch (ClassNotFoundException e) { 204 | e.printStackTrace(); 205 | } catch (IllegalAccessException e) { 206 | e.printStackTrace(); 207 | } 208 | 209 | return result; 210 | } 211 | 212 | 最后在`dispatchTouchEvent`中多加一条判断: 213 | 214 | @Override 215 | public boolean dispatchTouchEvent(MotionEvent ev) { 216 | if (ev.getAction() == MotionEvent.ACTION_UP) { 217 | View view = searchClickView(findViewById(android.R.id.content), ev); 218 | if (view != null && (view.hasOnClickListeners())|| hasOnTouchListener(view)) { 219 | *****记录对应的log****** 220 | } 221 | } 222 | return super.dispatchTouchEvent(ev); 223 | } 224 | 225 | ** 这样我们就可以同时统计onClick 和 onTouch 事件,让统计数据更加全面。** 226 | 227 | 228 | ## android中实现H5页面的自动埋点 229 | 现在大多数的APP都是混合型的,H5是必不可少的。为了尽可能用统一规则来收集数据的同时不去影响H5团队的开发,不用他们去通过代码埋点,所以就希望数据的收集对H5是透明、无感的。经过研究通过注入JS的方式是可以做到。 230 | 231 | 实现一个customer的`webviewClient`重写它的`onPageFinished(WebView view, String url)` 232 | 在该方法中注入对应的JS代码。 233 | 234 | public class StatisticsWebviewClient extends WebViewClient { 235 | private final static String LOADJS = "inject.js"; 236 | 237 | @Override 238 | public void onPageFinished(WebView view, String url) { 239 | super.onPageFinished(view, url); 240 | Util.webViewLoadLocalJs(view, LOADJS); 241 | } 242 | } 243 | 244 | 注入JS的方式如下: 245 | 246 | public static void webViewLoadLocalJs(WebView view, String path) { 247 | String jsContent = assetFile2Str(view.getContext(), path); 248 | view.loadUrl("javascript:" + jsContent); 249 | } 250 | 251 | 对应的JS代码如下: 252 | 253 | (function(){ 254 | var scriptTag = ""; 257 | $("body").append(scriptTag); 258 | console.log("++++++++++inject finish++++++"); 259 | })(); 260 | 261 | 实际注入的代码如下: 262 | 263 | document.body.addEventListener('click', function(e){ 264 | var mergeObj = { 265 | title: document.title, 266 | cookie: document.cookie 267 | ********你想收集的数据 268 | }; 269 | JSInterface.statisticsmethod(JSON.stringify(mergeObj))//这是android定义的统计方法 270 | }); 271 | 272 | 273 | 在对应的webView中添加如下实现: 274 | 275 | addJavascriptInterface(new JSInterface(this.getContext()), "statisticsmethod"); // 与刚才注入的JS想对应 276 | setWebViewClient(new 自定义的webviewClient()); 277 | 278 | 279 | JSInterface 的实现 280 | 281 | public class JSInterface { 282 | Context context; 283 | public JSInterface(Context context){ 284 | this.context = context; 285 | } 286 | 287 | 288 | @JavascriptInterface 289 | public void statisticsmethod(String str) { 290 | *****记录log 291 | } 292 | } 293 | 294 | 295 | 这样也就实现了android中H5的自动埋点事件统计 296 | 297 | 298 | ## 总结 299 | 各种方案各有利弊选型和使用还需慎重衡量。各种方案、各种厂商推出的产品总有一款能够满足你的需求,实在不行就自己实现一套满足需求SDK。 --------------------------------------------------------------------------------