├── API简析 └── LruCache.md ├── Kotlin ├── Kotlin.md └── res │ ├── QQ截图20190303112453.png │ └── QQ截图20190303112506.png ├── README.md ├── 其他 ├── Android知识点随记.md ├── MarkNote版本1的.md ├── MarkNote版本2.md ├── res │ ├── Compressor.png │ ├── QQ截图20190718000949.png │ ├── Screenshot_20190718-003619.jpg │ ├── easymark.png │ ├── shortcuts.jpg │ └── themes.jpg └── 计算机视觉与Android.md ├── 响应式编程 ├── RxJava系列-4:RxJava源码分析.md ├── RxJava系列(1):一篇的比较全面的RxJava2方法总结.md ├── RxJava系列(2):Flowable和背压.md ├── RxJava系列(3):用RxJava打造EventBus.md └── res │ ├── RxJava.png │ ├── RxJava_Scheduler.png │ ├── RxJava_Switch.png │ ├── RxJava_Switch2.png │ └── combineLatest.png ├── 四大组件 ├── Activity.md ├── Broadcast.md ├── Fragment.md ├── Service.md └── res │ ├── ActivityThread示意图.png │ ├── activity_fragment_lifecycle.png │ ├── activity_life.png │ ├── activity_life2.png │ ├── fragment_lifecycle.png │ └── service_life.png ├── 图片加载 ├── Android相机最佳实践.md ├── Glide系列:Glide主流程源码分析.md ├── Glide系列:Glide的缓存的实现原理.md ├── Glide系列:Glide的配置和使用方式.md ├── res │ ├── QQ图片20190423230233.png │ ├── QQ截图20190312223345.png │ ├── glide_configuration.png │ ├── glide_into_stage1.jpg │ ├── glide_into_stage2.jpg │ ├── glide_into_stage3.jpg │ ├── glide_into_stage4.jpg │ ├── glide_load.jpg │ ├── glide_with.jpg │ └── 开发.png └── 图片压缩框架封装.md ├── 工作空间 ├── OOM优化.md ├── Tinker.md ├── URL编码问题.md ├── res │ ├── 2017-11-19 17-32-47.png │ └── 2017-11-19 17-33-49.png ├── 文章暂时存放.md ├── 百度定位API.md └── 第三方库整理.md ├── 开发工具 ├── ADB_常见的ADB指令总结.md ├── Gradle_常见的指令和配置总结.md ├── Keytool_常用的指令.md └── res │ ├── keytool_alias_name.jpg │ ├── keytool_alias_psd.jpg │ ├── keytool_list.jpg │ ├── keytool_list_after.jpg │ └── keytool_password.jpg ├── 异步编程 ├── Android多线程编程:IntentService和HandlerThread.md └── AsyncTask的使用和源码分析.md ├── 性能优化 ├── Android性能优化-ANR.md ├── Android性能优化-内存优化.md ├── Android性能优化-启动优化.md ├── Android性能优化-布局优化.md ├── Android相机Camera1资料.md ├── Android相机Camera2资料.md └── Android进程保活.md ├── 消息机制 ├── EventBus的源码分析.md ├── res │ ├── AIDL.png │ ├── AIDL_Location.png │ ├── AIDL_Manager.png │ ├── AMS.png │ ├── Binder.png │ ├── BinderAMS.png │ ├── BinderTerminology.png │ ├── Binder_流程.png │ ├── Binder内存映射.png │ ├── Full_IPC.png │ ├── Handler_Looper_Message.png │ ├── Handler_handle_message.png │ ├── Handler_send_message.png │ ├── binder_diver.jpg │ ├── binder_model.png │ ├── launcher.png │ ├── mmap.jpg │ └── 包结构.png ├── 线程通信:Handler、MessageQueue和Looper.md └── 跨进程通信:Binder机制.md ├── 混合开发 ├── ReactNative.md └── res │ ├── align-content.jpg │ ├── align-items.jpg │ ├── flex-direction.jpg │ ├── flex-wrap.jpg │ └── justify-content.jpg ├── 笔试面试 ├── Android高级软件工程师2017.md ├── Android高级面试_10_跨平台开发.md ├── Android高级面试_11_JNINDK.md ├── Android高级面试_12_各种三方库分析.md ├── Android高级面试_12_算法.md ├── Android高级面试_12_项目经验梳理.md ├── Android高级面试_1_Handler相关.md ├── Android高级面试_2_IPC相关.md ├── Android高级面试_3_语言相关.md ├── Android高级面试_4_虚拟机相关.md ├── Android高级面试_5_四大组件、系统源码等.md ├── Android高级面试_6_性能优化.md ├── Android高级面试_7_网络相关.md ├── Android高级面试_8_热修补插件化等.md ├── Android高级面试_9_网络基础.md ├── README.md ├── java │ ├── ArrayList、LinkedList、Vector.md │ ├── Collection包结构,与Collections的区别.md │ ├── HashMap和ConcurrentHashMap的区别,HashMap的底层源码.md │ ├── HashMap和HashTable的区别.md │ ├── Hashcode的作用.md │ ├── Java1.7与1.8新特性.md │ ├── Java的四种引用,强弱软虚,用到的场景.md │ ├── Map、Set、List、Queue、Stack的特点与用法.md │ ├── Object有哪些公用方法.md │ ├── Override和Overload的含义去区别.md │ ├── Static class 与 non static class的区别.md │ ├── String、StringBuffer与StringBuilder的区别.md │ ├── Switch能否用string做参数.md │ ├── TreeMap、HashMap、LindedHashMap.md │ ├── equals与==的区别.md │ ├── jvm-java 内存模型 以及各个分区具体内容.md │ ├── throw和throws有什么区别.md │ ├── wait()和sleep()的区别.md │ ├── 九种基本数据类型的大小以及他们的封装类.md │ ├── 内存溢出和内存泄露的区别.md │ └── 解析XML的几种方式的原理与特点:DOM、SAX、PULL.md ├── res │ ├── 687472.webp │ ├── 940884-20180423141951735-912699213.png │ ├── QQ图片20190425213636.jpg │ ├── QQ截图20190225213434.png │ ├── QQ截图20190226123631.png │ ├── bg2014092004.png │ ├── ipc.png │ ├── jvm_heap_region.jpg │ ├── tcp_3_hello.png │ └── tcp_4_bye.png ├── 今日头条Android面试.md └── 初级工程师.md ├── 系统架构 ├── Android应用启动过程.md ├── Android应用安装过程.md ├── Android打包过程.md ├── Android系统启动过程.md ├── Android系统架构.md ├── SurefaceView_and_TextureView.md ├── res │ └── Andriod系统架构图.jpg ├── 控件体系 │ ├── RV.md │ ├── RV各种效果实现.md │ ├── View体系详解:View的工作流程.md │ ├── View体系详解:坐标系、滑动事件和分发机制.md │ ├── View体系详解:自定义控件.md │ ├── resources │ │ ├── View动画属性详解.png │ │ ├── View动画框架.png │ │ ├── 事件分发机制.png │ │ ├── 事件分发机制原理.png │ │ ├── 坐标系.png │ │ ├── 属性动画.png │ │ └── 属性动画XML.png │ └── 动画体系详解.md └── 窗口机制 │ └── Android的Window管理机制.md ├── 网络访问 ├── OKHttp源码阅读.md ├── OkHttp异步请求.png ├── OkHttp请求时序图.png ├── Retrofit源码阅读.md ├── Retrofit的执行过程.png ├── Retrofit类图.png └── 责任链执行过程.png └── 高阶技术 ├── Android插件化.md ├── Dagger从集成到源码.md ├── JNI技术总结.md ├── res ├── 20140507203312765.jpg ├── Component.png ├── QQ截图20190227124949.png ├── QQ截图20190227125150.png ├── QQ截图20190302004456.png ├── QQ截图20190302122352.png ├── knife_gen.png ├── live_data.png ├── mvp_package.png ├── mvvm_package.png └── viewmodel-lifecycle.png ├── 探索Android架构设计.md ├── 注解在Android中的应用.md ├── 浅谈LiveData的通知过程.md └── 浅谈ViewModel生命周期控制.md /Kotlin/res/QQ截图20190303112453.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/Kotlin/res/QQ截图20190303112453.png -------------------------------------------------------------------------------- /Kotlin/res/QQ截图20190303112506.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/Kotlin/res/QQ截图20190303112506.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android [DEPRECATED] 2 | 3 | ## 1、目录 4 | 5 | ### 基础开发 6 | 7 | - 基础回顾 8 | - [Android 基础回顾:Activity 基础](四大组件/Activity.md) 9 | - [Android 基础回顾:Fragment 基础](四大组件/Fragment.md) 10 | - [Android 基础回顾:Service 基础](四大组件/Service.md) 11 | - [Android 基础回顾:Broadcast 基础](四大组件/Broadcast.md) 12 | 13 | - 开发语言 14 | - [Java 注解在 Android 中的应用](注解和依赖注入/注解在Android中的应用.md) 15 | - [Kotlin 基础知识梳理](Kotlin/Kotlin.md) 16 | - [在 Android 中使用 JNI 的总结](高阶技术/JNI技术总结.md) 17 | 18 | - 架构设计 19 | - [Android 应用架构设计探索:MVC、MVP、MVVM和组件化](结构设计/探索Android架构设计.md) 20 | - [浅谈 ViewModel 的生命周期控制](高阶技术/浅谈ViewModel生命周期控制.md) 21 | - [浅谈 LiveData 的通知机制](高阶技术/浅谈LiveData的通知过程.md) 22 | 23 | - 性能优化 24 | - [ANR](性能优化/Android性能优化-ANR.md) 25 | - [布局优化](性能优化/Android性能优化-布局优化.md) 26 | - [进程保活](性能优化/Android进程保活.md) 27 | - [启动优化](性能优化/Android性能优化-启动优化.md) 28 | - [内存优化](性能优化/Android性能优化-内存优化.md) 29 | 30 | - 开发环境 31 | - [常见的 ADB 指令总结](开发工具/ADB_常见的ADB指令总结.md) 32 | - [常见的 Gradle 指令和配置总结](开发工具/Gradle_常见的指令和配置总结.md) 33 | - [常见的 Keytool 指令总结](开发工具/Keytool_常用的指令.md) 34 | 35 | ### 系统源码 36 | 37 | - 核心流程 38 | - [Android 系统架构](系统架构/Android系统架构.md) 39 | - [Android 系统启动流程源码分析](系统架构/Android系统启动过程.md) 40 | - [Android 应用打包过程](系统架构/Android打包过程.md) 41 | - [Android 应用安装过程](系统架构/Android应用安装过程.md) 42 | 43 | - 消息机制 44 | - [Android 消息机制:Handler、MessageQueue 和 Looper](消息机制/线程通信:Handler、MessageQueue和Looper.md.md) 45 | - [Android IPC 机制:Binder 机制](消息机制/跨进程通信:Binder机制.md) 46 | 47 | - 异步编程 48 | - [AsyncTask 的使用和源码分析](异步编程/AsyncTask源码分析.md) 49 | - [Android 多线程编程:IntentService 和 HandlerThread](异步编程/Android多线程编程:IntentService和HandlerThread.md) 50 | 51 | - 窗口机制 52 | - [Android 的窗口管理机制](系统架构/窗口机制/Android的Window管理机制.md)(编辑中) 53 | 54 | - 控件体系 55 | - [View 体系详解:View的工作流程](系统架构/控件体系/View体系详解:View的工作流程.md) 56 | - [View 体系详解:坐标系、滑动事件和分发机制](系统架构/控件体系/View体系详解:坐标系、滑动事件和分发机制.md) 57 | - [Android 动画体系详解](系统架构/控件体系/动画体系详解.md) 58 | - [SurfaceView 与 TextureView 的区别](系统架构/SurefaceView_and_TextureView.md) 59 | 60 | - 部分 API 源码 61 | - [LruCache 的使用和源码分析](API简析/LruCache.md) 62 | 63 | ### 三方库源码 64 | 65 | - 网络框架 66 | - [网络框架 OkHttp 源码解析](网络访问/OKHttp源码阅读.md) 67 | - [网络框架 Retrofit 源码解析](网络访问/Retrofit源码阅读.md) 68 | 69 | - 图片加载框架 70 | - [Glide 系列-1:预热、Glide 的常用配置方式及其原理](图片加载/Glide系列:Glide的配置和使用方式.md) 71 | - [Glide 系列-2:主流程源码分析](图片加载/Glide系列:Glide主流程源码分析.md) 72 | - [Glide 系列-3:Glide 缓存的实现原理](图片加载/Glide系列:Glide的缓存的实现原理.md) 73 | 74 | - RxJava 75 | - [RxJava2 系列-1:一篇的比较全面的 RxJava2 方法总结](响应式编程/RxJava2系列·_一篇的比较全面的RxJava2方法总结.md) 76 | - [RxJava2 系列-2:Flowable 和背压](响应式编程/Flowable和背压.md) 77 | - [RxJava2 系列-3:使用 Subject](响应式编程/用RxJava打造EventBus.md) 78 | - [RxJava2 系列-4:RxJava 源码分析](响应式编程/RxJava系列-4:RxJava源码分析.md) 79 | 80 | - 其他框架 81 | - [消息机制 EventBus 源码解析](消息机制/EventBus的源码分析.md) 82 | - [Dagger 从集成到源码带你理解依赖注入框架](高阶技术/Dagger从集成到源码.md) 83 | 84 | ### Java 相关 85 | 86 | - 并发编程 87 | - [Java 并发编程:ThreadLocal 的使用及其源码实现](https://blog.csdn.net/github_35186068/article/details/83858944) 88 | 89 | - 设计模式 90 | - [观察者模式](https://blog.csdn.net/github_35186068/article/details/83754026) 91 | 92 | - 虚拟机 93 | - [内存管理](https://juejin.im/post/5b475e976fb9a04fa8671a45) 94 | - [虚拟机执行子系统](https://juejin.im/post/5b4a1fb7e51d4519213fd374) 95 | - [虚拟机内存模型与高效并发](https://juejin.im/post/5b4f48e75188251b1b448aa0) 96 | 97 | - 三方库 98 | - [时间库 JodaTime](https://blog.csdn.net/github_35186068/article/details/83754146) 99 | 100 | ### UI 相关 101 | 102 | - [自定义控件](系统架构/控件体系/View体系详解:自定义控件.md)(编辑中) 103 | 104 | ### 编程基础 105 | 106 | - 数据库 107 | - [MySQL 基础知识(全)](https://juejin.im/post/5a12d62bf265da431d3c4a01) 108 | 109 | ### 面试题 110 | 111 | > 通过面试题梳理知识点细节 112 | 113 | - [Android高级面试_1_Handler相关](笔试面试/Android高级面试_1_Handler相关.md) 114 | - [Android高级面试_2_IPC相关](笔试面试/Android高级面试_2_IPC相关.md) 115 | - [Android高级面试_3_语言相关](笔试面试/Android高级面试_3_语言相关.md) 116 | - [Android高级面试_4_虚拟机相关](笔试面试/Android高级面试_4_虚拟机相关.md) 117 | - [Android高级面试_5_四大组件、系统源码等](笔试面试/Android高级面试_5_四大组件、系统源码等.md) 118 | - [Android高级面试_6_性能优化](笔试面试/Android高级面试_6_性能优化.md) 119 | - [Android高级面试_7_三方库相关](笔试面试/Android高级面试_7_三方库相关.md) 120 | - [Android高级面试_8_热修补插件化等](笔试面试/Android高级面试_8_热修补插件化等.md) 121 | - [Android高级面试_9_网络基础](笔试面试/Android高级面试_9_网络基础.md) 122 | - [Android高级面试_10_跨平台开发](笔试面试/Android高级面试_10_跨平台开发.md) 123 | - [Android高级面试_11_JNINDK](笔试面试/Android高级面试_11_JNINDK.md) 124 | - [Android高级面试_12_项目经验梳理](笔试面试/Android高级面试_12_项目经验梳理.md) 125 | - [Android 中高级工程师面试题总结](笔试面试/Android高级软件工程师2017.md) 126 | 127 | ### 其他 128 | 129 | - [马克笔记—Android 端开源的 Markdown 笔记应用](其他/MarkNote版本1的.md) 130 | - [承上启下:Markdown 笔记应用 MarkNote 的重构之路](其他/MarkNote版本2.md) 131 | 132 | ## 2、资源整理 133 | 134 | 135 | -------------------------------------------------------------------------------- /其他/Android知识点随记.md: -------------------------------------------------------------------------------- 1 | 2 | ## 异常处理 3 | 4 | 对于未捕获的异常,借助 Thread 的静态方法来进行处理 5 | 6 | ```java 7 | public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) { 8 | defaultUncaughtExceptionHandler = eh; 9 | } 10 | ``` 11 | 12 | ## multidex 13 | 14 | 这是因为安装应用时,有一步是使用 DexOpt 对 Dex 进行优化。这个过程会生成一个 ODex 文件,执行 ODex 的效率会比直接执行 Dex 文件的效率要高很多。在早期的 Android 系统中,DexOpt 把每一个类的方法 id 检索起来,存在一个链表结构里面。但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536 个。尽管在新版本的 Android 系统中,修复了 DexOpt 的这个问题,但是我们仍然需要对低版本的 Android 系统做兼容。 15 | 16 | 为了解决方法数超限的问题,需要启用 multidex 将该 dex文 件拆成多个。 17 | 18 | ## 动态布局 19 | 20 | 就是指服务端使用 API 下发数据信息,然后客户端根据下发的信息进行动态布局。 21 | 22 | 另一层含义可能是在代码中进行动态布局而不是使用 XML 的方式。 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /其他/MarkNote版本1的.md: -------------------------------------------------------------------------------- 1 | # 马克笔记—Android 端开源的 Markdown 笔记应用 2 | 3 | ![App 导引](https://github.com/Shouheng88/MarkNote/blob/master/resources/images/app.png?raw=true) 4 | 5 | > 马克笔记是运行在Android设备上面的一款开源的Markdown笔记,它的功能开发得已经比较完善,已经能够满足大部分用户的需求。现在将其开源到Github上面,用来交流和学习。当然,更希望你能够参与到项目的开发当中,帮助马克笔记变得更加有用。 6 | 7 | ## 1、关于马克笔记 8 | 9 | 马克笔记是一款开源的Markdown笔记应用,它的界面设计采用了Google最新的Material Design风格。该笔记现在的功能已经比较完善,能够满足用户大多数场景的需求。开源该软件的目的是希望与更多的人交流和学习,同时也希望能够有人参与到项目的开发中,一起帮助马克笔记,让它变得更加有用。 10 | 11 | 你可以通过加入[Google+社区](https://plus.google.com/u/1/communities/102252970668657211916)来关注该软件开发的最新动态,并且可以参与Beta测试。 12 | 13 | 马克笔记现在已经发布到了[酷安网](https://www.coolapk.com/apk/178276)上面,也欢迎你下载和使用该软件。另外,笔者还开发了一款清单应用[多功能清单](https://www.coolapk.com/apk/185660),感兴趣的同学也可以了解一下。 14 | 15 | ## 2、应用展示图 16 | 17 | 这里是该应用的一些截图通过Photoshop调整之后得到的展示图,通过展示图,你大概可以了解一下该软件的主要功能和开发状态。在接下来的行文中,我会向你更详细地介绍它使用到的一些技术以及现在开发完成的一些功能和特性。 18 | 19 | ## 3、功能和特性 20 | 21 | 我把该软件当前已经支持的功能列了一个清单: 22 | 23 | |编号|功能| 24 | |:-:|:-| 25 | |1|基本的**添加、修改、归档、放进垃圾箱、彻底删除**操作| 26 | |2|基本的Markdown语法,外加**MathJax**等高级特性| 27 | |3|特色的**时间线**功能,通过类似于AOP的操作记录用户的操作信息| 28 | |4|多种形式的媒体数据,包括**文件、视频、音频、图片、手写和位置信息**等| 29 | |5|**多主题**,支持**夜间主题**,并且有多种可选的**主题色和强调色**| 30 | |6|多彩的**图表**用于统计用户的数据信息| 31 | |7|三种形式的**桌面小控件**,并且可以为每个笔记添加快捷方式| 32 | |8|允许你为笔记指定多个多彩的标签| 33 | |9|使用“树结构”模拟文件夹操作,支持**多层文件夹**,并可以进行层级的搜索| 34 | |10|允许将笔记**导出为PDF、TXT、MD格式的文本、HTML和图片**| 35 | |11|使用**应用独立锁**,加强数据安全| 36 | |12|允许用户**备份数据到外部存储空间和OneDrive**| 37 | |13|图片**自动压缩**,节省本地的数据存储空间| 38 | 39 | 将来希望开发和完善的功能: 40 | 41 | |编号|功能描述| 42 | |:-:|:-| 43 | |1|数据同步,本地的文件管理容易导致多平台的不一致,增加同步服务,能够实现多平台操作| 44 | |2|文件服务器,用于获取图片和文件的链接| 45 | |3|富文本编辑,即时的编辑预览| 46 | |4|允许添加闹钟,并且复选框可以编辑| 47 | |5|添加地图来展示用户的位置信息的变更| 48 | 49 | 你可以从[更新日志](app/src/main/res/raw/changelog.xml)中获取到软件的更新信息。 50 | 51 | ## 4、依赖和用到的一些技术 52 | 53 | 马克笔记用到了MVVM的设计模式,还用到了DataBinding等一系列技术。下面的表格中列出了用到的具体的依赖和简要的描述。在此,还要感谢这些开源项目的作者: 54 | 55 | |编号|依赖|描述| 56 | |:-:|:-|:-| 57 | |1|[arch.lifecycle]()|使用ViewModel+LiveData实现Model和View的解耦| 58 | |2|[Stetho](https://github.com/facebook/stetho)|Facebook开源的安卓调试框架| 59 | |3|[Fabric]()|错误跟踪,用户数据收集| 60 | |4|[RxBinding](https://github.com/JakeWharton/RxBinding)|| 61 | |5|[RxJava](https://github.com/ReactiveX/RxJava)|| 62 | |6|[RxAndroid](https://github.com/ReactiveX/RxAndroid)|| 63 | |7|[OkHttp](https://github.com/square/okhttp)|| 64 | |8|[Retrofit](https://github.com/square/retrofit)|| 65 | |9|[Glide](https://github.com/bumptech/glide)|| 66 | |10|[BRVAH](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)|非常好用的Recycler适配器| 67 | |11|[Gson](https://github.com/google/gson)|| 68 | |12|[Joda-Time](https://github.com/JodaOrg/joda-time)|Java时间库| 69 | |13|[Apache IO](http://commons.apache.org/io/)|文件操作库| 70 | |14|[Material dialogs](https://github.com/afollestad/material-dialogs)|| 71 | |15|[PhotoView](https://github.com/chrisbanes/PhotoView)|| 72 | |16|[Hello charts](https://github.com/lecho/hellocharts-android)|| 73 | |17|[FloatingActionButton](https://github.com/Clans/FloatingActionButton)|| 74 | |18|[HoloColorPicker](https://github.com/LarsWerkman/HoloColorPicker)|| 75 | |19|[CircleImageView](https://github.com/hdodenhof/CircleImageView)|| 76 | |20|[Changeloglib](https://github.com/gabrielemariotti/changeloglib)|日志信息| 77 | |21|[PinLockView](https://github.com/aritraroy/PinLockView)|锁控件| 78 | |22|[BottomSheet](https://github.com/Kennyc1012/BottomSheet)|底部弹出的对话框| 79 | |23|[Luban](https://github.com/Curzibn/Luban)|图片压缩| 80 | |24|[Flexmark](https://github.com/vsch/flexmark-java)|基于Java的Markdown文本解析| 81 | |25|[PrettyTime](https://github.com/ocpsoft/prettytime)|时间格式美化| 82 | 83 | 84 | 特别需要说明的一点是,马克笔记是在开发了一段时间之后重新引入的ViewModel,因为作者本人水平有限,或者对ViewModel理解不够深入,设计难免有不足的地方,还请批评指正。 85 | 86 | ### 数据库操作 87 | 88 | 对于数据库部分,笔者自己设计了一套数据的访问逻辑,这里使用到了模板和单例等设计模式。它的好处在于,当你想要向程序中添加一个数据库实体的时候,只需要很少的配置即可,可以省去很多的样板代码。而且,由于该项目的一些特殊需求,比如要记录统计信息等,所以就自己设计了一下。当然,可能性能上仍然有许多值得提升的地方,但笔者认为仍不失为一个简单的学习材料。 89 | 90 | ### Markdown解析 91 | 92 | 对于Markdown解析,可以使用js在webview里面解析,也可以像本项目一样在程序种用java进行解析。笔者认为使用Flexmark在java种解析的好处是更方便地对解析的功能进行拓展。如该软件中的MathJax的解析就是在Flexmark的基础上进行的拓展。 93 | 94 | ## 5、参与项目 95 | 96 | 正如一开始提及的那样,马克笔记仍然有许多不足,我希望可以有更多的人帮助马克笔记继续完善它的功能。当然,这并不勉强。如果你希望对该项目贡献代码,你可以fork该项目,并向该项目提交请求。你可以在[waffle.io](https://waffle.io/Shouheng88/NotePal)上面跟踪issue的开发状态。或者,你发现了该软件中存在的一些问题,你可以在issue中向开发者报告。如果有其他的需求,可以直接通过[邮箱](mailto:shouheng2015@gmail.com)邮件开发者。 97 | 98 | ## 6、项目地址 99 | 100 | 因为这篇文章是从Github的Readme文件中拷贝出来的,所以忘记加上Github地址了,抱歉。现在补上:[Github](https://github.com/Shouheng88/MarkNote) -------------------------------------------------------------------------------- /其他/res/Compressor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/其他/res/Compressor.png -------------------------------------------------------------------------------- /其他/res/QQ截图20190718000949.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/其他/res/QQ截图20190718000949.png -------------------------------------------------------------------------------- /其他/res/Screenshot_20190718-003619.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/其他/res/Screenshot_20190718-003619.jpg -------------------------------------------------------------------------------- /其他/res/easymark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/其他/res/easymark.png -------------------------------------------------------------------------------- /其他/res/shortcuts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/其他/res/shortcuts.jpg -------------------------------------------------------------------------------- /其他/res/themes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/其他/res/themes.jpg -------------------------------------------------------------------------------- /响应式编程/RxJava系列(2):Flowable和背压.md: -------------------------------------------------------------------------------- 1 | # RxJava2 系列 (2):背压和Flowable 2 | 3 | 背压(Back Pressure)的概念最初并不是在响应式编程中提出的,它最初用在流体力学中,指的是后端的压力, 4 | 通常用于描述系统排出的流体在出口处或二次侧受到的与流动方向相反的压力。 5 | 6 | 在响应式编程中,我们可以将产生信息的部分叫做上游或者叫生产者,处理产生的信息的部分叫做下游或者消费者。 7 | 试想如果在异步的环境中,生产者的生产速度大于消费者的消费速度的时候,明显会出现生产过剩的情景,这时候就需要消费者对多余的数据进行缓存, 8 | 但如果生产的信息数量过多,以至于超出缓存大小,就会出现缓存溢出,甚至可能造成内存耗尽。 9 | 10 | 我们可以制定一个数据丢失的规则,来丢失那些“可以丢失的数据”,以减轻缓存的压力。 11 | 在之前我们介绍了一些方法,比如`throttleXXX`、`debounce`、`sample`等,都是用来解决在生产速度过快的情况下的数据过滤的,它们指定了数据取舍的规则。 12 | 而在`Flowable`,我们可以通过`onBackpressureXXX`一系列的方法来制定当数据生产过快情况下的数据取舍的规则, 13 | 14 | 我们可以把这种处理方式理解成背压,所谓背压,在Rx中就是通过一种下游用来控制上游事件发射频率的机制(就像流体在出口受到了阻力一样)。 15 | 所以,如何理解背压呢?笔者认为,在力学中它是一种现象,在Rx中它是一种机制。 16 | 17 | 在这篇文章中,我们会先介绍背压的相关内容,然后我们再介绍一下`onBackpressureXXX`系列的方法。 18 | 19 | 关于RxJava2的基础使用和方法梳理可以参考:[RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结](https://juejin.im/post/5b72f76551882561354462dd) 20 | 21 | 说明:以下文章部分翻译自RxJava官方文档[Backpressure (2.0)](https://github.com/ReactiveX/RxJava/wiki/Backpressure-(2.0))。 22 | 23 | ## 1、背压机制 24 | 25 | 如果将生产和消费整体看作一个管道,生成看作上游,消费看作下游; 26 | 那么当异步的应用场景下,当生产者生产过快而消费者消费很慢的时候,可以通过背压来告知上游减慢生成的速度。 27 | 28 | 通常在进行异步的操作的时候会通过缓存来存储发射出的数据。在早期的RxJava中,这些缓存是无界的。 29 | 这意味着当需要缓存的数据非常多的时候,它们可能会占用非常多的存储空间,并有可能因为虚拟机不断GC而导致程序执行过慢,甚至直接抛出OOM。 30 | 在最新的RxJava中,大多数的异步操作内部都存在一个有界的缓存,当超出这个缓存的时候就会抛出`MissingBackpressureException`异常并结束整个序列。 31 | 32 | 然而,某些情况下的表现会有所不同,它们不会抛出`MissingBackpressureException`异常。比如下面的`range`操作: 33 | 34 | private static void compute(int i) throws InterruptedException { 35 | Thread.sleep(500); 36 | System.out.println("computing : " + i); 37 | } 38 | 39 | private static void testFlowable() throws InterruptedException { 40 | Flowable.range(1, MAX_LENGTH).observeOn(Schedulers.computation()).subscribe(FlowableTest::compute); 41 | 42 | Thread.sleep(500 * MAX_LENGTH); 43 | } 44 | 45 | 在这段代码中我们生成一段整数,然后每隔500毫秒执行依次计算操作。从输出的结果来看,在程序的实际执行过程中,数据的发射是串行的。 46 | 也就是发射完一个数据之后进入`compute`进行计算,等待500毫秒之后才发射下一个。 47 | 因此,在程序的执行过程中没有抛出异常,也没有过多的内存消耗。 48 | 49 | 而下面的这段代码就会在程序运行的时候立刻抛出`MissingBackpressureException`异常: 50 | 51 | PublishProcessor source = PublishProcessor.create(); 52 | source.observeOn(Schedulers.computation()).subscribe(v -> compute(v), Throwable::printStackTrace); 53 | for (int i = 0; i < 1_000_000; i++) source.onNext(i); 54 | Thread.sleep(10_000); 55 | 56 | 这是因为`PublishProcessor`底层会调用`PublishSubscription`,而后者实现了`AtomicLong`,它会通过判断引用的long是否为0来抛出异常,这个long型整数会在调用`PublishSubscription.request()`的时候被改写。前面的一个例子的原理就是当每次调用了观察者的`onNext`之后会调用`PublishSubscription.request()`来请求数据,这样相当于消费者会在消费完事件之后向生产者请求,因此整个序列的执行看上去是串行的,从而不会抛出异常。 57 | 58 | ## 2、onBackpressureXXX 59 | 60 | 大多数开发者在遇到`MissingBackpressureException`通常是因为使用`observeOn`方法监听了非背压的`PublishProcessor`, `timer()`, `interval()`或者自定义的`create()`。我们有以下几种方式来解决这个问题: 61 | 62 | ### 2.1 增加缓存大小 63 | 64 | `observeOn`方法的默认缓存大小是16,当生产的速率过快的时候,那么可能很快会超出该缓存大小,从而导致缓存溢出。 65 | 一种简单的解决办法是通过提升该缓存的大小来防止缓存溢出,我们可以使用`observeOn`的重载方法来设置缓存的大小。比如: 66 | 67 | PublishProcessor source = PublishProcessor.create(); 68 | source.observeOn(Schedulers.computation(), 1024 * 1024) 69 | .subscribe(e -> { }, Throwable::printStackTrace); 70 | 71 | 但是这种解决方案只能解决暂时的问题,当生产的速率过快的时候还是有可能造成缓存溢出,所以这不是根本的解决办法。 72 | 73 | ### 2.2 通过丢弃和过滤来减轻缓存压力 74 | 75 | 我们可以根据自己的应用的场景和数据的重要性,选择使用一些方法来过滤和丢弃数据。 76 | 比如,丢弃的方式可以选择`throttleFirst`, `throttleLast`, `throttleWithTimeout`等,还可以使用按照时间采样的方式来减少接受的数据。 77 | 78 | PublishProcessor source = PublishProcessor.create(); 79 | source.sample(1, TimeUnit.MILLISECONDS) 80 | .observeOn(Schedulers.computation(), 1024) 81 | .subscribe(v -> compute(v), Throwable::printStackTrace); 82 | 83 | 但是,这种方式仅仅用来减少下游接收的数据,当缓存的数据不断增加的时候还是有可能导致缓存溢出,所以,这也不是一种根本的解决办法。 84 | 85 | ### 2.3 onBackpressureBuffer() 86 | 87 | 这种无参的方法会使用一个无界的缓存,只要虚拟机没有抛出OOM异常,它就会把所有的数据缓存起来。 88 | 89 | Flowable.range(1, 1_000_000) 90 | .onBackpressureBuffer() 91 | .observeOn(Schedulers.computation(), 8) 92 | .subscribe(e -> { }, Throwable::printStackTrace); 93 | 94 | 上面的例子即使使用了很小的缓存也不会有异常抛出,因为`onBackpressureBuffer`会将发射的所有数据缓存起来,只会将一小部分的数据传递给`observeOn`。 95 | 96 | 这种处理方式实际上是不存在背压的,因为`onBackpressureBuffer`缓存了所有的数据,我们可以使用该方法的4个重载方法来对背压进行个性化设置。 97 | 98 | ### 2.4 onBackpressureBuffer(int capacity) 99 | 100 | 这个方法使用一个有界的缓存,当达到了缓存大小的时候会抛出一个`BufferOverflowError`错误。 101 | 通过这种方法可以增加默认的缓存大小,但是通过`observeOn`方法一样可以指定缓存的大小,因此,这个方法的应用变得越来越少。 102 | 103 | ### 2.5 onBackpressureBuffer(int capacity, Action onOverflow) 104 | 105 | 这方法除了可以指定一个有界的缓存还提供了一个,当缓存溢出的时候还会回调指定的Action。 106 | 但是这种回调的用途比较有限,因为它除了提供当前回调的栈信息以外提供不了任何有用的信息。 107 | 108 | ### 2.6 onBackpressureBuffer(int capacity, Action onOverflow, BackpressureOverflowStrategy strategy) 109 | 110 | 这个重载方法相对比较实用一些,它除了上面的那些功能之外,还指定了当缓存到达指定的缓存时的行为。 111 | 这里的`BackpressureOverflowStrategy`顾名思义是一个策略,它是一个枚举类型,预定义了三种枚举值,最终会在`FlowableOnBackpressureBufferStrategy`中根据指定的枚举类型选择不同的实现策略,因此,我们可以使用它来指定缓存溢出时候的行为。 112 | 113 | 下面是该枚举类型的三个值及其含义: 114 | 115 | 1. `ERROR`:当缓存溢出的时候会抛出一个异常; 116 | 2. `DROP_OLDEST`:当缓存发生溢出的时候,会丢弃最老的值,并将新的值插入到缓存中; 117 | 3. `DROP_LATEST`:当缓存发生溢出的时候,最新的值会被忽略,只有比较老的值会被传递给下游使用; 118 | 119 | 需要注意的地方是,后面的两种策略会造成下游获取到的值是不连续的,因为有一部分值会因为缓存不够被丢弃,但是它们不会抛出`BufferOverflowException`。 120 | 121 | ### 2.7 onBackpressureDrop() 122 | 123 | 这个方法会在数据达到缓存大小的时候丢弃最新的数据。可以将其看成是`onBackpressureBuffer`+`0 capacity`+`DROP_LATEST`的组合。 124 | 125 | 这个方法特别适用于那种可以忽略从源中发射出值的那种场景,比如GPS定位问题,定位数据会不断发射出来,即使丢失当前数据,等会儿一样能拿到最新的数据。 126 | 127 | component.mouseMoves() 128 | .onBackpressureDrop() 129 | .observeOn(Schedulers.computation(), 1) 130 | .subscribe(event -> compute(event.x, event.y)); 131 | 132 | 该方法还存在一个重载方法`onBackpressureDrop(Consumer onDrop)`,它允许我们传入一个接口来指定当某个数据被丢失时的行为。 133 | 134 | ### 2.8 onBackpressureLatest() 135 | 136 | 对应于`onBackpressureDrop()`的,还有`onBackpressureLatest()`方法,该方法只会保留最新的数据并会覆盖较老、没有分发的数据。 137 | 我们可以将其看成是`onBackpressureBuffer`+`1 capacity`+`DROP_OLDEST`的组合。 138 | 139 | 与`onBackpressureDrop()`不同的地方在于,当下游消费过慢的时候,这种方式总会存在一个缓存的值。 140 | 这种特别适用于那种数据的生产非常频繁,但是只有最新的数据会被消费的那种情形。比如,当用户点击了屏幕,那么我们倾向于只处理最新按下的位置的事件。 141 | 142 | component.mouseClicks() 143 | .onBackpressureLatest() 144 | .observeOn(Schedulers.computation()) 145 | .subscribe(event -> compute(event.x, event.y), Throwable::printStackTrace); 146 | 147 | 所以,总结一下: 148 | 149 | 1. `onBackpressureDrop()`:不会缓存任何数据,专注于当下,新来的数据来不及处理就丢掉,以后会有更好的; 150 | 2. `onBackpressureLatest()`:会缓存一个数据,当正在执行某个任务的时候有新的数据过来,会把它缓存起来,如果又有新的数据过来,那就把之前的替换掉,缓存里面的总是最新的。 151 | 152 | ## 3、总结 153 | 154 | 以上就是背压机制的一些内容,以及我们介绍了`Flowable`中的几个背压相关的方法。 155 | 实际上,RxJava的官方文档也有说明——`Flowable`适用于数据量比较大的情景,因为它的一些创建方法本身就使用了背压机制。 156 | 这部分方法我们就不再一一进行说明,因为,它们的方法签名和`Observable`基本一致,只是多了一层背压机制。 157 | 158 | 比较匆匆地整理完了背压的内容,但是我想这块还会有更加丰富的内容值得我们去发现和探索。 159 | 160 | 以上。 -------------------------------------------------------------------------------- /响应式编程/RxJava系列(3):用RxJava打造EventBus.md: -------------------------------------------------------------------------------- 1 | # RxJava2 系列 (3):使用 Subject 2 | 3 | 在这篇文章中,我们会先分析一下 RxJava2 中的 Subject ;然后,我们会使用 Subject 制作一个类似于 EventBus 的全局的通信工具。 4 | 5 | 在了解本篇文章的内容之前,你需要先了解 RxJava2 中的一些基本的用法,比如 Observable 以及背压的概念,你可以参考我的其他两篇文章来获取这部分内容:[《RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结》](https://juejin.im/post/5b72f76551882561354462dd)和[《RxJava2 系列 (2):背压和Flowable》](https://juejin.im/post/5b759b9cf265da283719d187)。 6 | 7 | ## 1、Subject 8 | 9 | ### 1.1 Subject 的两个特性 10 | 11 | Subject 可以同时代表 Observer 和 Observable,允许从数据源中多次发送结果给多个观察者。除了 onSubscribe(), onNext(), onError() 和 onComplete() 之外,所有的方法都是线程安全的。此外,你还可以使用 toSerialized() 方法,也就是转换成串行的,将这些方法设置成线程安全的。 12 | 13 | 如果你已经了解了 Observable 和 Observer ,那么也许直接看 Subject 的源码定义会更容易理解: 14 | 15 | ``` 16 | public abstract class Subject extends Observable implements Observer { 17 | 18 | // ... 19 | } 20 | ``` 21 | 22 | 从上面看出,Subject 同时继承了 Observable 和 Observer 两个接口,说明它既是被观察的对象,同时又是观察对象,也就是可以生产、可以消费、也可以自己生产自己消费。所以,我们可以项下面这样来使用它。这里我们用到的是该接口的一个实现 PublishSubject : 23 | 24 | public static void main(String...args) { 25 | PublishSubject subject = PublishSubject.create(); 26 | subject.subscribe(System.out::println); 27 | 28 | Executor executor = Executors.newFixedThreadPool(5); 29 | Disposable disposable = Observable.range(1, 5).subscribe(i -> 30 | executor.execute(() -> { 31 | try { 32 | Thread.sleep(i * 200); 33 | subject.onNext(i); 34 | } catch (InterruptedException e) { 35 | e.printStackTrace(); 36 | } 37 | })); 38 | } 39 | 40 | 根据程序的执行结果,程序在第200, 400, 600, 800, 1000毫秒依次输出了1到5的数字。 41 | 42 | 在这里,我们用 PublishSubject 创建了一个**主题**并对其监听,然后在线程当中又通知该主题内容变化,整个过程我们都只操作了 PublishSubject 一个对象。显然,使用 Subject 我们可以达到对一个指定类型的值的结果进行监听的目的——我们把值改变之后对应的逻辑写在 subscribe() 方法中,然后每次调用 onNext() 等方法通知结果之后就可以自动调用 subscribe() 方法进行更新操作。 43 | 44 | 同时,因为 Subject 实现了 Observer 接口,并且在 Observable 等的 subscribe() 方法中存在一个以 Observer 作为参数的方法(如下),所以,Subject 也是可以作为消费者来对事件进行消费的。 45 | 46 | public final void subscribe(Observer observer) 47 | 48 | 以上就是 Subject 的两个主要的特性。 49 | 50 | ### 1.2 Subject 的实现类 51 | 52 | 在 RxJava2 ,Subject 有几个默认的实现,下面我们对它们之间的区别做简单的说明: 53 | 54 | 1. `AsyncSubject`:只有当 Subject 调用 onComplete 方法时,才会将 Subject 中的**最后一个事件**传递给所有的 Observer。 55 | 2. `BehaviorSubject`:该类有创建时需要一个默认参数,该默认参数会在 Subject 未发送过其他的事件时,向注册的 Observer 发送;新注册的 Observer 不会收到之前发送的事件,这点和 PublishSubject 一致。 56 | 3. `PublishSubject`:不会改变事件的发送顺序;在已经发送了一部分事件之后注册的 Observer 不会收到之前发送的事件。 57 | 4. `ReplaySubject`:无论什么时候注册 Observer 都可以接收到任何时候通过该 Observable 发射的事件。 58 | 5. `UnicastSubject`:只允许一个 Observer 进行监听,在该 Observer 注册之前会将发射的所有的事件放进一个队列中,并在 Observer 注册的时候一起通知给它。 59 | 60 | 对比 PublishSubject 和 ReplaySubject,它们的区别在于新注册的 Observer 是否能够收到在它注册之前发送的事件。这个类似于 EventBus 中的 StickyEvent 即黏性事件,为了说明这一点,我们准备了下面两段代码: 61 | 62 | private static void testPublishSubject() throws InterruptedException { 63 | PublishSubject subject = PublishSubject.create(); 64 | subject.subscribe(i -> System.out.print("(1: " + i + ") ")); 65 | 66 | Executor executor = Executors.newFixedThreadPool(5); 67 | Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> { 68 | try { 69 | Thread.sleep(i * 200); 70 | subject.onNext(i); 71 | } catch (InterruptedException e) { 72 | e.printStackTrace(); 73 | } 74 | })); 75 | 76 | Thread.sleep(500); 77 | subject.subscribe(i -> System.out.print("(2: " + i + ") ")); 78 | 79 | Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown()); 80 | } 81 | 82 | private static void testReplaySubject() throws InterruptedException { 83 | ReplaySubject subject = ReplaySubject.create(); 84 | subject.subscribe(i -> System.out.print("(1: " + i + ") ")); 85 | 86 | Executor executor = Executors.newFixedThreadPool(5); 87 | Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> { 88 | try { 89 | Thread.sleep(i * 200); 90 | subject.onNext(i); 91 | } catch (InterruptedException e) { 92 | e.printStackTrace(); 93 | } 94 | })); 95 | 96 | Thread.sleep(500); 97 | subject.subscribe(i -> System.out.print("(2: " + i + ") ")); 98 | 99 | Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown()); 100 | } 101 | 102 | 它们的输出结果依次是 103 | 104 | PublishSubject的结果:(1: 1) (1: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5) 105 | ReplaySubject的结果: (1: 1) (1: 2) (2: 1) (2: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5) 106 | 107 | 从上面的结果对比中,我们可以看出前者与后者的区别在于新注册的 Observer 并没有收到在它注册之前发送的事件。试验的结果与上面的叙述是一致的。 108 | 109 | 其他的测试代码这不一并给出了,详细的代码可以参考[Github - Java Advanced](https://github.com/Shouheng88/Java-advanced)。 110 | 111 | ## 2、用 RxJava 打造 EventBus 112 | 113 | ### 2.1 打造 EventBus 114 | 115 | 清楚了 Subject 的概念之后,让我们来做一个实践——用 RxJava 打造 EventBus。 116 | 117 | 我们先考虑用一个全局的 PublishSubject 来解决这个问题,当然,这意味着我们发送的事件不是黏性事件。不过,没关系,只要这种实现方式搞懂了,用 ReplaySubject 做一个发送黏性事件的 EventBus 也非难事。 118 | 119 | 考虑一下,如果要实现这个功能我们需要做哪些准备: 120 | 121 | 1. **我们需要发送事件并能够正确地接收到事件。**要实现这个目的并不难,因为 Subject 本身就具有发送和接收两个能力,作为全局的之后就具有了全局的注册和通知的能力。因此,不论你在什么位置发送了事件,任何订阅的地方都能收到该事件。 122 | 2. **首先,我们要在合适的位置对事件进行监听,并在合适的位置取消事件的监听。如果我们没有在适当的时机释放事件,会不会造成内存泄漏呢?这还是有可能的。**所以,我们需要对注册监听的观察者进行记录,并提供注册和取消注册的方法,给它们在指定的生命周期中进行调用。 123 | 124 | 好了,首先是全局的 Subject 的问题,我们可以实现一个静态的或者单例的 Subject。这里我们选择使用后者,所以,我们需要一个单例的方式来使用 Subject: 125 | 126 | public class RxBus { 127 | 128 | private static volatile RxBus rxBus; 129 | 130 | private final Subject subject = PublishSubject.create().toSerialized(); 131 | 132 | public static RxBus getRxBus() { 133 | if (rxBus == null) { 134 | synchronized (RxBus.class) { 135 | if(rxBus == null) { 136 | rxBus = new RxBus(); 137 | } 138 | } 139 | } 140 | return rxBus; 141 | } 142 | } 143 | 144 | 这里我们应用了 DCL 的单例模式提供一个单例的 RxBus,对应一个唯一的 Subject. 这里我们用到了 Subject 的`toSerialized()`,我们上面已经提到过它的作用,就是用来保证 onNext() 等方法的线程安全性。 145 | 146 | 另外,因为 Observalbe 本身是不支持背压的,所以,我们还需要将该 Observable 转换成 Flowable 来实现背压的效果: 147 | 148 | public Flowable getObservable(Class type){ 149 | return subject.toFlowable(BackpressureStrategy.BUFFER).ofType(type); 150 | } 151 | 152 | 这里我们用到的背压的策略是`BackpressureStrategy.BUFFER`,它会缓存发射结果,直到有消费者订阅了它。而这里的`ofType()`方法的作用是用来过滤发射的事件的类型,只有指定类型的事件会被发布。 153 | 154 | 然后,我们需要记录订阅者的信息以便在适当的时机取消订阅,这里我们用一个`Map`类型的哈希表来解决。这里的`CompositeDisposable`用来存储 Disposable,从而达到一个订阅者对应多个 Disposable 的目的。`CompositeDisposable`是一个 Disposable 的容器,声称可以达到 O(1) 的增、删的复杂度。这里的做法目的是使用注册观察之后的 Disposable 的 dispose() 方法来取消订阅。所以,我们可以得到下面的这段代码: 155 | 156 | public void addSubscription(Object o, Disposable disposable) { 157 | String key = o.getClass().getName(); 158 | if (disposableMap.get(key) != null) { 159 | disposableMap.get(key).add(disposable); 160 | } else { 161 | CompositeDisposable disposables = new CompositeDisposable(); 162 | disposables.add(disposable); 163 | disposableMap.put(key, disposables); 164 | } 165 | } 166 | 167 | public void unSubscribe(Object o) { 168 | String key = o.getClass().getName(); 169 | if (!disposableMap.containsKey(key)) { 170 | return; 171 | } 172 | if (disposableMap.get(key) != null) { 173 | disposableMap.get(key).dispose(); 174 | } 175 | disposableMap.remove(key); 176 | } 177 | 178 | 最后,对外提供一下 Subject 的订阅和发布方法,整个 EventBus 就制作完成了: 179 | 180 | public void post(Object o){ 181 | subject.onNext(o); 182 | } 183 | 184 | public Disposable doSubscribe(Class type, Consumer next, Consumer error){ 185 | return getObservable(type) 186 | .subscribeOn(Schedulers.io()) 187 | .observeOn(AndroidSchedulers.mainThread()) 188 | .subscribe(next,error); 189 | } 190 | 191 | ### 2.2 测试效果 192 | 193 | 我们只需要在最顶层的 Activity 基类中加入如下的代码。这样,我们就不需要在各个 Activity 中取消注册了。然后,就可以使用这些顶层的方法来进行操作了。 194 | 195 | protected void postEvent(Object object) { 196 | RxBus.getRxBus().post(object); 197 | } 198 | 199 | protected void addSubscription(Class eventType, Consumer action) { 200 | Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, LogUtils::d); 201 | RxBus.getRxBus().addSubscription(this, disposable); 202 | } 203 | 204 | protected void addSubscription(Class eventType, Consumer action, Consumer error) { 205 | Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, error); 206 | RxBus.getRxBus().addSubscription(this, disposable); 207 | } 208 | 209 | @Override 210 | protected void onDestroy() { 211 | super.onDestroy(); 212 | RxBus.getRxBus().unSubscribe(this); 213 | } 214 | 215 | 在第一个 Activity 中我们对指定的类型的结果进行监听: 216 | 217 | addSubscription(RxMessage.class, rxMessage -> ToastUtils.makeToast(rxMessage.message)); 218 | 219 | 然后,我们在另一个 Activity 中发布事件: 220 | 221 | postEvent(new RxMessage("Hello world!")); 222 | 223 | 这样当第二个 Activity 中调用指定的发送事件的方法之后,第一个 Activity 就可以接收到发射的事件了。 224 | 225 | ## 总结 226 | 227 | 好了,以上就是 Subject 的使用,如果要用一个词来形容它的话,那么只能是“自给自足”了。就是说,它同时做了 Observable 和 Observer 的工作,既可以发射事件又可以对事件进行消费,可谓身兼数职。它在那种想要对某个值进行监听并处理的情形特别有用。因为它不需要你写多个冗余的类,只要它一个就完成了其他两个类来完成的任务,因而代码更加简洁。 228 | -------------------------------------------------------------------------------- /响应式编程/res/RxJava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/响应式编程/res/RxJava.png -------------------------------------------------------------------------------- /响应式编程/res/RxJava_Scheduler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/响应式编程/res/RxJava_Scheduler.png -------------------------------------------------------------------------------- /响应式编程/res/RxJava_Switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/响应式编程/res/RxJava_Switch.png -------------------------------------------------------------------------------- /响应式编程/res/RxJava_Switch2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/响应式编程/res/RxJava_Switch2.png -------------------------------------------------------------------------------- /响应式编程/res/combineLatest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/响应式编程/res/combineLatest.png -------------------------------------------------------------------------------- /四大组件/Activity.md: -------------------------------------------------------------------------------- 1 | # Android 基础回顾:Activity 基础 2 | 3 | ## 1、Activity 的生命周期 4 | 5 | ### 1.1 一般情况下的生命周期 6 | 7 | 下图是一般情况下一个 Activity 将会经过的生命周期的流程图: 8 | 9 | ![Activity的生命周期](res/activity_life.png) 10 | 11 | 关于上图中生命周期方法的说明: 12 | 13 | 1. **onCreate() / onDestroy()**:onCreate() 表示 Activity 正在被创建,可以用来做初始化工作;onDestroy() 表示 Activity 正在被销毁,可以用来做释放资源的工作; 14 | 2. **onStart() / onStop()**:onStart() 在 Activity 从不可见变成可见的时候被调用;onStop() 在 Activity 从可见变成不可见的时候被调用; 15 | 3. **onRestart()**:在 Activity 从不可见到变成可见的过程中被调用; 16 | 4. **onResume() / onPause()**:onResume() 在 Activity() 可以与用户交互的时候被调用,onPause() 在 Activity 不可与用户交互的时候被调用。 17 | 18 | 所以根据上面的分析,我们可以将Activity的生命周期概况为:**创建->可见->可交互->不可交互->不可见->销毁**。因此,我们可以得到下面的这张图: 19 | 20 | ### 1.2 特殊情况下的生命周期 21 | 22 | 这里我们总结一下在实际的使用过程中可能会遇到的一些 Acitivity 的生命周期过程: 23 | 24 | 1. **当用户打开新的 Activity 或者切换回桌面**:会经过的生命周期为 `onPause()->onStop()`。因为此时 Activity 已经变成不可见了,当然,如果新打开的 Activity 用了透明主题,那么 onStop() 不会被调用,因此原来的 Activity 只是不能交互,但是仍然可见。 25 | 2. **从新的 Activity 回到之前的 Activity 或者从桌面回到之前的 Activity**:会经过的生命周期为 `onRestart()->onStart()-onResume()`。此时是从 onStop() 经 onRestart() 回到 onResume() 状态。 26 | 3. 如果在上述 1 的情况下,进入后台的 Activity 因为内存不足被销毁了,那么当再次回到该 Activity 的时候,生命周期方法将会从 onCreate() 开始执行到 onResume()。 27 | 4. **当用户按下 Back 键时**:如果当前 Activity 被销毁,那么经过的生命周期将会是 `onPause()->onStop()->onDestroy()`。 28 | 29 | 具体地,当存在两个 Activity,分别是 A 和 B 的时候,在各种情况下,它们的生命周期将会经过: 30 | 31 | 1. **Back 键 Home 键** 32 | 1. 当用户点击 A 中按钮来到 B 时,假设 B 全部遮挡住了 A,将依次执行:`A.onPause()->B.onCreate()->B.onStart()->B.onResume->A.onStop()`。 33 | 2. 接1,此时如果点击 Back 键,将依次执行:`B.onPause()->A.onRestart()->A.onStart()->A.onResume()->B.onStop()->B.onDestroy()`。 34 | 3. 接2,此时如果按下 Back 键,系统返回到桌面,并依次执行:`A.onPause()->A.onStop()->A.onDestroy()`。 35 | 4. 接2,此时如果按下 Home 键(非长按),系统返回到桌面,并依次执行`A.onPause()->A.onStop()`。由此可见,Back 键和 Home 键主要区别在于是否会执行 onDestroy()。 36 | 5. 接2,此时如果长按 Home 键,不同手机可能弹出不同内容,Activity 生命周期未发生变化。 37 | 2. **横竖屏切换时 Activity 的生命周期** 38 | 1. 不设置 Activity 的 `android:configChanges` 时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次。 39 | 2. 设置 Activity 的 `android:configChanges=“orientation”` 时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次。 40 | 3. 设置 Activity 的 `android:configChanges=“orientation|keyboardHidden”` 时,切屏不会重新调用各个生命周期,只会执行 onConfiguration() 方法。 41 | 42 | ### 1.3 onSaveInstanceState() 和 onRestoreInstanceState() 43 | 44 | 当 Activity 被销毁的时候回调用 `onSaveInstanceState()` 方法来存储当前的状态。这样当 Activity 被重建的时候,可以在 `onCreate()` 和 `onRestoreInstanceState()` 中恢复状态。 45 | 46 | 对于 targetAPI 为 28 及以后的应用,该方法会在 `onStop()` 方法之后调用,对于之前的设备,这方法会在 `onStop()` 之前调用,但是无法确定是在 `onPause()` 之前还是之后调用。 47 | 48 | `onRestoreInstanceState()` 方法用来恢复之前存储的状态,它会在 `onStart()` 和 `onPostCreate()` 之间被调用。此外,你也可以直接在 `onCreate()` 方法中进行恢复,但是基于这个方法调用的时机,如果有特别需求,可以在这个方法中进行处理。 49 | 50 | ## 2、Activity 的启动模式 51 | 52 | Activity 共有四种启动模式: 53 | 54 | 1. **standard**:默认,每次启动的时候会创建一个新的实例,并且被创建的实例所在的栈与启动它的 Activity 是同一个栈。比如,A 启动了 B,那么 B 将会与 A 处在同一个栈。假如,我们使用 Application 的 Context 启动一个 Activity 的时候会抛出异常,这是因为新启动的 Activity 不知道自己将会处于哪个栈。可以在启动 Activity 的时候使用 `FLAG_ACTIVITY_NEW_TASK`。这样新启动的 Acitivyt 将会创建一个新的栈。 55 | 2. **singleTop**:栈顶复用,如果将要启动的 Activity 已经位于栈顶,那么将会复用栈顶的 Activity,并且会调用它的 `onNewIntent()`。常见的应用场景是从通知打开 Activity 时。 56 | 3. **singleTask**:单例,如果启动它的任务栈中存在该 Activity,那么将会复用该 Activity,并且会将栈内的、它之上的所有的 Activity 清理出去,以使得该 Activity 位于栈顶。常见的应用场景是启动页面、购物界面、确认订单界面和付款界面等。 57 | 4. **singleInstance**:这种启动模式会在启动的时候为其指定一个单独的栈来执行。如果用同样的intent 再次启动这个 Activity,那么这个 Activity 会被调到前台,并且会调用其 `onNewIntent()` 方法。 58 | 59 | ## 3、Activity 的 Flags 60 | 61 | 1. **FLAG_ACTIVITY_CLEAR_TOP** : 会清理掉该栈中位于 Activity 上面的所有的 Activity,通常与 FLAG_ACTIVITY_NEW_TASK 配合使用; 62 | 2. **FLAG_ACTIVITY_SINGLE_TOP**: 同样等同于 mainfest 中配置的 singleTop; 63 | 3. **FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS**: 对应于 mainfest 中的属性为`android:excludeFromRecents="true"`,当用户按了 “最近任务列表” 时,该任务不会出现在最近任务列表中,可达到隐藏应用的目的。 64 | 4. **FLAG_ACTIVITY_NO_HISTORY**: 对应于 mainfest 中的 `android:noHistory="true"`。这个 FLAG 启动的 Activity,一旦退出,它不会存在于栈中。 65 | 5. **FLAG_ACTIVITY_NEW_TASK**: 等同于 mainfest 中配置的 singleTask. 66 | 67 | 68 | -------------------------------------------------------------------------------- /四大组件/Broadcast.md: -------------------------------------------------------------------------------- 1 | # Android 基础回顾:Broadcast 基础 2 | 3 | ## 1、关于广播 4 | 5 | 广播是 Android 提供的一种全局通信机制。 6 | 7 | ### 1.1 分类 8 | 9 | 1. 按照注册方式:**静态注册和动态注册**两种; 10 | 2. 按照作用范围:**本地广播和普通广播**两种,普通广播是全局的,所有应用程序都可以接收到,容易会引起安全问题。本地广播只能够在应用内传递,广播接收器也只能接收应用内发出的广播; 11 | 3. 按照是否有序:**有序广播和无序广播**两种,无序广播各接收器接收的顺序无法确定,并且在广播发出之后接收器只能接收,不能拦截和进行其他处理,两者的区别主要体现在发送时调用的方法上。 12 | 13 | ### 1.2 实现 14 | 15 | #### 1.2.1 静态广播 16 | 17 | 注册,这里的 StaticBroadcastReceiver 是自定义类: 18 | 19 | ```xml 20 | 21 | 22 | 23 | 24 | 25 | ``` 26 | 27 | 我们可以将要实现的逻辑放在这个类的方法中进行执行: 28 | 29 | ```java 30 | public class StaticBroadcastReceiver extends BroadcastReceiver { 31 | 32 | @Override 33 | public void onReceive(Context context, Intent intent) { 34 | // Do something 35 | } 36 | } 37 | ``` 38 | 39 | 需要注意 Andrdoid 8.0 之后系统对广播进行了一些限制([官方文档](https://developer.android.google.cn/about/versions/oreo/android-8.0)),具体地: 40 | 41 | 1. 在 Android 8.0 的平台上,应用不能对大部分的广播进行静态注册,也就是说,不能在AndroidManifest 文件对**有些**广播进行静态注册(注意“有些”,因为不是所有的广播都不能注册)。 42 | 2. 当程序运行在后台的时候,静态广播中不能启动服务。比如之前实现闹钟的时候是监听时间变化来实现的,在 8.0 之后就会抛出异常。 43 | 44 | 解决方式是使用动态注册方式(一般情况下使用动态注册就好了)。 45 | 46 | #### 1.2.2 动态广播 47 | 48 | 与静态广播相似,但是不需要在 Manifest 中进行注册。 49 | 50 | ```java 51 | // 监听广播:一般在 Activity 的 onCreate() 方法中注册 52 | netWorkChangReceiver = new StaticBroadcastReceiver(); 53 | IntentFilter filter = new IntentFilter(); 54 | filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); 55 | registerReceiver(netWorkChangReceiver, filter); 56 | 57 | // 取消监听:然后在 Activity 的 onDestroy() 中取消注册 58 | unregisterReceiver(netWorkChangReceiver); 59 | ``` 60 | 61 | **注意当页面被销毁的时候需要取消注册广播!** 62 | 63 | #### 1.2.3 本地广播 64 | 65 | 本地广播的核心类是 LocalBroadcastManager,使用它的静态方法 `getInstance()` 获取一个单例之后就可以使用该单例的 `registerReceiver()`、`unregisterReceiver()` 和 `sendBroadcast()` 等方法来进行操作了。 66 | 67 | ```java 68 | // 获取单例 69 | localBroadcastManager = LocalBroadcastManager.getInstance(this); 70 | 71 | // 注册广播 72 | IntentFilter filter = new IntentFilter(); 73 | filter.addAction("me.shouheng.MyBroadcastReceiver"); 74 | localReceiver = new LocalReceiver(); 75 | localBroadcastManager.registerReceiver(localReceiver, filter); 76 | 77 | // 发送广播 78 | Intent intent = new Intent("me.shouheng.MyBroadcastReceiver"); 79 | localBroadcastManager.sendBroadcast(intent); 80 | 81 | // 取消注册 82 | localBroadcastManager.unregisterReceiver(localReceiver); 83 | ``` 84 | 85 | #### 1.2.4 有序广播 86 | 87 | 在 xml 中进行注册的时候通过 `android:priority` 指定一个范围在 -1000~1000 之间的整数来指定广播的接收顺序。优先级高的会先接收到,优先级相等的话则顺序不确定。并且前面的广播可以在方法中向 Intent 写入数据,后面的广播可以接收到写入的值。 88 | 89 | ```xml 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ``` 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /四大组件/Fragment.md: -------------------------------------------------------------------------------- 1 | # Android 基础回顾:Fragment 基础 2 | 3 | ## 1、Fragment 的生命周期 4 | 5 | ### 1.1 Fragment 的生命周期 6 | 7 | 下面是 Fragment 的生命周期的流程图: 8 | 9 | ![Fragment的生命周期](res/fragment_lifecycle.png) 10 | 11 | 从上图可以看出,Fragment 的生命周期相比于 Activity,在创建的过程中增加了 `onAttach()`、`onCreateView()` 和 `onActivityCreated()` 三个方法,在销毁的过程中增加了 `onDestroyView()` 和 `onDetach()` 两个方法。 12 | 13 | ### 1.2 Activity 与 Fragment 生命周期对应关系 14 | 15 | ![Activity 与 Fragment 生命周期对应关系](res/activity_fragment_lifecycle.png) 16 | 17 | 18 | -------------------------------------------------------------------------------- /四大组件/res/ActivityThread示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/四大组件/res/ActivityThread示意图.png -------------------------------------------------------------------------------- /四大组件/res/activity_fragment_lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/四大组件/res/activity_fragment_lifecycle.png -------------------------------------------------------------------------------- /四大组件/res/activity_life.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/四大组件/res/activity_life.png -------------------------------------------------------------------------------- /四大组件/res/activity_life2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/四大组件/res/activity_life2.png -------------------------------------------------------------------------------- /四大组件/res/fragment_lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/四大组件/res/fragment_lifecycle.png -------------------------------------------------------------------------------- /四大组件/res/service_life.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/四大组件/res/service_life.png -------------------------------------------------------------------------------- /图片加载/res/QQ图片20190423230233.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/QQ图片20190423230233.png -------------------------------------------------------------------------------- /图片加载/res/QQ截图20190312223345.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/QQ截图20190312223345.png -------------------------------------------------------------------------------- /图片加载/res/glide_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/glide_configuration.png -------------------------------------------------------------------------------- /图片加载/res/glide_into_stage1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/glide_into_stage1.jpg -------------------------------------------------------------------------------- /图片加载/res/glide_into_stage2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/glide_into_stage2.jpg -------------------------------------------------------------------------------- /图片加载/res/glide_into_stage3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/glide_into_stage3.jpg -------------------------------------------------------------------------------- /图片加载/res/glide_into_stage4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/glide_into_stage4.jpg -------------------------------------------------------------------------------- /图片加载/res/glide_load.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/glide_load.jpg -------------------------------------------------------------------------------- /图片加载/res/glide_with.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/glide_with.jpg -------------------------------------------------------------------------------- /图片加载/res/开发.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/图片加载/res/开发.png -------------------------------------------------------------------------------- /图片加载/图片压缩框架封装.md: -------------------------------------------------------------------------------- 1 | # 开源一个 Android 图片压缩框架 2 | 3 | 在我们的业务场景中,需要使用客户端采集图片,上传服务器,然后对图片信息进行识别。为了提升程序的性能,我们需要保证图片上传服务器的速度的同时,保证用于识别图片的质量。整个优化包括两个方面的内容: 4 | 5 | 1. 相机拍照的优化:包括相机参数的选择、预览、启动速度和照片质量等; 6 | 2. 图片压缩的优化:基于拍摄的图片和从相册中选择的图片进行压缩,控制图片大小和尺寸。 7 | 8 | 在本文中,我们主要介绍图片压缩优化,后续我们会介绍如何对 Android 的相机进行封装和优化。本项目主要基于 Android 自带的图片压缩 API 进行封装,结合了 Luban 和 Compressor 的优点,同时提供了用户自定义压缩策略的接口。该项目的主要目的在于,统一图片压缩框库的实现,集成常用的两种图片压缩算法,让你以更低的成本集成图片压缩功能到自己的项目中。 9 | 10 | ## 1、图片压缩的基础知识 11 | 12 | 对于一般业务场景,当我们展示图片的时候,Glide 会帮我们处理加载的图片的尺寸问题。但在把采集来的图片上传到服务器之前,为了节省流量,我们需要对图片进行压缩。 13 | 14 | 在 Android 平台上,默认提供的压缩有三种方式:质量压缩和两种尺寸压缩,邻近采样以及双线性采样。下面我们简单介绍下者三种压缩方式都是如何使用的: 15 | 16 | ### 1.1 质量压缩 17 | 18 | 所谓的质量压缩就是下面的这行代码,它是 Bitmap 的方法。当我们得到了 Bitmap 的时候,即可使用这个方法来实现质量压缩。它一般位于我们所有压缩方法的最后一步。 19 | 20 | ```java 21 | // android.graphics。Bitmap 22 | compress(CompressFormat format, int quality, OutputStream stream) 23 | ``` 24 | 25 | 该方法接受三个参数,其含义分别如下: 26 | 27 | 1. format:枚举,有三个选项 `JPEG`, `PNG` 和 `WEBP`,表示图片的格式; 28 | 2. quality:图片的质量,取值在 `[0,100]` 之间,表示图片质量,越大,图片的质量越高; 29 | 3. stream:一个输出流,通常是我们压缩结果输出的文件的流 30 | 31 | ### 1.2 邻近采样 32 | 33 | 邻近采样基于临近点插值算法,用像素代替周围的像素。邻近采样的核心代码只有下面三行, 34 | 35 | ```java 36 | BitmapFactory.Options options = new BitmapFactory.Options(); 37 | options.inSampleSize = 1; 38 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options); 39 | ``` 40 | 41 | 邻近采样核心的地方在于 `inSampleSize` 的计算。它通常是我们使用的压缩算法的第一步。我们可以通过设置 inSampleSize 来得到原始图片采样之后的结果,而不是将原始的图片全部加载到内存中,以防止 OOM。标准使用姿势如下: 42 | 43 | ```java 44 | // 获取原始图片的尺寸 45 | BitmapFactory.Options options = new BitmapFactory.Options(); 46 | options.inJustDecodeBounds = true; 47 | options.inSampleSize = 1; 48 | BitmapFactory.decodeStream(srcImg.open(), null, options); 49 | this.srcWidth = options.outWidth; 50 | this.srcHeight = options.outHeight; 51 | 52 | // 进行图片加载,此时会将图片加载到内存中 53 | options.inJustDecodeBounds = false; 54 | options.inSampleSize = calInSampleSize(); 55 | Bitmap bitmap = BitmapFactory.decodeStream(srcImg.open(), null, options); 56 | ``` 57 | 58 | 这里主要分成两个步骤,它们各自的含义是: 59 | 60 | 1. 先通过设置 Options 的 `inJustDecodeBounds` 为 true,来加载图片,以得到图片的尺寸信息。此时图片不会被加载到内存中,所以不会造成 OOM,同时我们可以通过 Options 得到原图的尺寸信息。 61 | 2. 根据上一步中得到的图片的尺寸信息,计算一个 inSampleSize,然后将 inJustDecodeBounds 设置为 false,以加载采样之后的图片到内存中。 62 | 63 | 关于 inSampleSize 需要简单说明一下:inSampleSize 代表压缩后的图像一个像素点代表了原来的几个像素点,例如 inSampleSize 为 4,则压缩后的图像的宽高是原来的 1/4,像素点数是原来的 1/16,inSampleSize 一般会选择 2 的指数,如果不是 2 的指数,内部计算的时候也会向 2 的指数靠近。所以,实际使用过程中,我们会通过明确指定 inSampleSize 为 2 的指数,来避免内部计算导致的不确定性。 64 | 65 | ### 1.3 双线性采样 66 | 67 | 邻近采样可以对图片的尺寸进行有效的控制,但是它存在几个问题。比如,当我需要把图片的宽度压缩到 1200 左右的时候,如果原始的图片的宽度压是 3200,那么我只能通过设置 inSampleSize 将采样率设置为 2 来将其压缩到 1600. 此时图片的尺寸比我们的要求要大。就是说,邻近采样无法对图片的尺寸进行更加精准的控制。如果需要对图片尺寸进行更加精准的控制,那么就需要使用双线性压缩了。 68 | 69 | 双线性采样采用双线性插值算法,相比邻近采样简单粗暴的选择一个像素点代替其他像素点,双线性采样参考源像素相应位置周围 2x2 个点的值,根据相对位置取对应的权重,经过计算得到目标图像。 70 | 71 | 它在 Android 中的使用也比较简单, 72 | 73 | ```java 74 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red); 75 | Matrix matrix = new Matrix(); 76 | matrix.setScale(0.5f, 0.5f); 77 | Bitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true); 78 | ``` 79 | 80 | 也就是对得到的 Bitmap 应用 `createBitmap()` 进行处理,并传入 Matrix 指定图片尺寸放缩的比例。该方法返回的 Bitmap 就是双线性压缩之后的结果。 81 | 82 | ### 1.4 图片压缩算法总结 83 | 84 | 在实际使用过程中,我们通常会结合三种压缩方式使用,一般使用的步骤如下, 85 | 86 | 1. 使用邻近采样对原始的图片进行采样,将图片控制到比目标尺寸稍大的大小,防止 OOM; 87 | 2. 使用双线性采样对图片的尺寸进行压缩,控制图片的尺寸为目标的大小; 88 | 3. 对上述两个步骤之后得到的图片 Bitmap 进行质量压缩,并将其输出到磁盘上。 89 | 90 | 当然,本质上 Android 图片的编码是由 [Skia](https://skia.org/index_zh) 库来完成的,所以,除了使用 Android 自带的库进行压缩,我们还可以调用外部的库进行压缩。为了追求更高的压缩效率,通常我们会在 Native 层对图片进行处理,这将涉及 JNI 的知识。笔者曾在之前的文章 [《在 Android 中使用 JNI 的总结》](https://juejin.im/post/5c79f5d0518825347a56275f) 中介绍过 Android 平台上 JNI 的调用的常规思路,感兴趣的同学可以参考下。 91 | 92 | ## 2、Github 上的开源的图片压缩库 93 | 94 | 现在 Github 上的图片压缩框架主要有 Luban 和 Compressor 两个。Star 的数量也比较高,一个 9K,另一个 4K. 但是,这两个图片压缩的库有各自的优点和缺点。下面我们通过一个表格总结一下: 95 | 96 | |框架|优点|缺点| 97 | |:-:|:-|:-| 98 | |Luban|据说是根据微信图片压缩逆推的算法|1.只适用于一般的图片展示的场景,无法对图片的尺寸进行精准压缩;2.内部封装 AsyncTaks 来进行异步的图片压缩,对于 RxJava 支持不好。| 99 | |Compressor|1.可以对图片的尺寸进行压缩;2.支持 RxJava。|1.尺寸压缩的场景有限,如果有特别的需求,则需要手动修改源代码;2.图片压缩采样的时候计算有问题,导致采样后的图片尺寸总是小于我们指定的尺寸| 100 | 101 | 上面的图表已经总结得很详细了。所以,根据上面的两个库各自的优缺点,我们打算开发一个新的图片压缩框架。它满足下面的功能: 102 | 103 | 1. 支持 RxJava:我们可以像使用 Compressor 的时候那样,指定图片压缩的线程和结果监听的线程; 104 | 2. 支持 Luban 压缩算法:Luban 压缩算法核心的部分只在于 inSampleSize 的计算,因此,我们可以很容易得将其集成到我们的新的库中。之所以加入 Luban,是为了让我们的库可以适用于一般图片展示的场景。用户无需指定图片的尺寸,用起来省心省力。 105 | 3. 支持 Compressor 压缩算法同时指定更多的参数:Compressor 压缩算法就是我们上述提到的三种压缩算法的总和。不过,当要压缩的宽高比与原始图片的宽高比不一致的时候,它只提供了一种情景。下文中介绍我们框架的时候会说明进行更详细的说明。当然,你可以在调用框架的方法之前主动去计算出一个宽高比,但是你需要把图片压缩的第一个阶段主动走一遍,费心费力。 106 | 4. 提供用户自定义压缩算法的接口:我们希望设计的库可以允许用户自定义压缩策略。在想要替换图片压缩算法的时候,通过链式调用的一个方法直接更换策略即可。即,我们希望能够让用户以最低的成本替换项目中的图片压缩算法。 107 | 108 | ## 3、项目整体架构 109 | 110 | 以下是我们的图片压缩框架的整体架构,这里我们只列举除了其中核心的部分代码。这里的 Compress 是我们的链式调用的起点,我们可以用它来指定图片压缩的基本参数。然后,当我们使用它的 `strategy()` 方法之后,方法将进入到图片压缩策略中,此时,我们继续链式调用压缩策略的自定义方法,个性化地设置各压缩策略自己的参数: 111 | 112 | ![项目整体架构](res/QQ截图20190312223345.png) 113 | 114 | 这里的所有的压缩策略都继承自抽线的基类 AbstractStrategy,它提供了两个默认的实现 Luban 和 Compressor. 接口 CompressListener 和 CacheNameFactory 分别用来监听图片压缩进度和自定义压缩的图片的名称。下面的三个是图片相关的工具类,用户可以调用它们来实现自己压缩策略。 115 | 116 | ## 4、使用 117 | 118 | 首先,在项目的 Gradle 中加入我的 Maven 仓库的地址: 119 | 120 | maven { url "https://dl.bintray.com/easymark/Android" } 121 | 122 | 然后,在你的项目的依赖中,添加该库的依赖: 123 | 124 | implementation 'me.shouheng.compressor:compressor:0.0.1' 125 | 126 | 然后,就可以在项目中使用了。你可以参考 Sample 项目的使用方式。不过,下面我们还是对它的一些 API 做简单的说明。 127 | 128 | ### 4.1 Luban 的使用 129 | 130 | 下面是 Luban 压缩策略的使用示例,它与 Luban 库的使用类似。只是在 Luban 的库的基础上,我们增加了一个 copy 的选项,用来表示当图片因为小于指定的大小而没有被压缩之后,是否将原始的图片拷贝到指定的目录。因为,比如当你使用回调获取图片压缩结果的时候,如果按照 Luban 库的逻辑,你得到的是原始的图片,所以,此时你需要额外进行判断。因此,我们增加了这个布尔类型的参数,你可以通过它指定将原始文件进行拷贝,这样你就不需要在回调中对是否是原始图片进行判断了。 131 | 132 | ```kotlin 133 | // 在 Compress 的 with() 方法中指定 Context 和 要压缩文件 File 134 | val luban = Compress.with(this, file) 135 | // 这里添加一个回调,如果你不使用 RxJava,那么可以用它来处理压缩的结果 136 | .setCompressListener(object : CompressListener{ 137 | override fun onStart() { 138 | LogUtils.d(Thread.currentThread().toString()) 139 | Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show() 140 | } 141 | 142 | override fun onSuccess(result: File?) { 143 | LogUtils.d(Thread.currentThread().toString()) 144 | displayResult(result?.absolutePath) 145 | Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show() 146 | } 147 | 148 | override fun onError(throwable: Throwable?) { 149 | LogUtils.d(Thread.currentThread().toString()) 150 | Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show() 151 | } 152 | }) 153 | // 压缩图片的名称工厂方法,用来指定压缩结果的文件名 154 | .setCacheNameFactory { System.currentTimeMillis().toString() } 155 | // 图片的质量 156 | .setQuality(80) 157 | // 上面基本的配置完了,下面指定图片的压缩策略为 Luban 158 | .strategy(Strategies.luban()) 159 | // 指定如果图片小于等于 100K 就不压缩了,这里的参数 copy 表示,如果不压缩的话要不要拷贝文件 160 | .setIgnoreSize(100, copy) 161 | 162 | // 按上面那样得到了 Luban 实例之后有下面两种方式启动图片压缩 163 | // 启动方式 1:使用 RxJava 进行处理 164 | val d = luban.asFlowable() 165 | .subscribeOn(Schedulers.io()) 166 | .observeOn(AndroidSchedulers.mainThread()) 167 | .subscribe { displayResult(it.absolutePath) } 168 | 169 | // 启动方式 2:直接启动,此时使用内部封装的 AsyncTask 进行压缩,压缩结果只能在上面的回调中进行处理了 170 | luban.launch() 171 | ``` 172 | 173 | ### 4.2 Compressor 的使用 174 | 175 | 下面是 Compressor 压缩策略的基本的使用,在调用 `strategy()` 方法指定压缩策略之前,你的任务与 Luban 一致。所以,如果你需要更换图片压缩算法的时候,直接使用 `strategy()` 方法更换策略即可,前面部分的逻辑无需改动,因此,可以降低你更换压缩策略的成本。 176 | 177 | ```kotlin 178 | val compressor = Compress.with(this, file) 179 | .setQuality(60) 180 | .setTargetDir("") 181 | .setCompressListener(object : CompressListener { 182 | override fun onStart() { 183 | LogUtils.d(Thread.currentThread().toString()) 184 | Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show() 185 | } 186 | 187 | override fun onSuccess(result: File?) { 188 | LogUtils.d(Thread.currentThread().toString()) 189 | displayResult(result?.absolutePath) 190 | Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show() 191 | } 192 | 193 | override fun onError(throwable: Throwable?) { 194 | LogUtils.d(Thread.currentThread().toString()) 195 | Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show() 196 | } 197 | }) 198 | .strategy(Strategies.compressor()) 199 | .setMaxHeight(100f) 200 | .setMaxWidth(100f) 201 | .setScaleMode(Configuration.SCALE_SMALLER) 202 | .launch() 203 | ``` 204 | 205 | 这里的 `setMaxHeight(100f)` 和 `setMaxWidth(100f)` 用来表示图片压缩的目标大小。具体的大小是如何计算的呢?在 Compressor 库中你是无法确定的,但是在我们的库中,你可以通过 `setScaleMode()` 方法来指定。这个方法接收一个整数类型的枚举,它的取值范围有 4 个,即 `SCALE_LARGER`, `SCALE_SMALLER`, `SCALE_WIDTH` 和 `SCALE_HEIGHT`,它们具体的含义我们会进行详细说明。这里我们默认的压缩方式是 SCALE_LARGER,也就是 Compressor 库的压缩方式。那么这四个参数分别是什么含义呢? 206 | 207 | 这里我们以一个例子来说明,假设有一个图片的宽度是 1000,高度是 500,简写作 (W:1000, H:500),通过 `setMaxHeight()` 和 `setMaxWidth()` 指定的参数均为 100,那么,就称目标图片的尺寸,宽度是 100,高度是 100,简写作 (W:100, H:100)。那么按照上面的四种压缩方式,最终的结果将是: 208 | 209 | - **SCALE_LARGER**:对高度和长度中较大的一个进行压缩,另一个自适应,因此压缩结果是 (W:100, H:50). 也就是说,因为原始图片宽高比 2:1,我们需要保持这个宽高比之后再压缩。而目标宽高比是 1:1. 而原图的宽度比较大,所以,我们选择将宽度作为压缩的基准,宽度缩小 10 倍,高度也缩小 10 倍。这是 Compressor 库的默认压缩策略,显然它只是优先使得到的图片更小。这在一般情景中没有问题,但是当你想把短边控制在 100 就无计可施了(需要计算之后再传参),此时可以使用 SCALE_SMALLER。 210 | - **SCALE_SMALLER**:对高度和长度中较大的一个进行压缩,另一个自适应,因此压缩结果是 (W:200, H:100). 也就是,高度缩小 5 倍之后,达到目标 100,然后宽度缩小 5 倍,达到 200. 211 | - **SCALE_WIDTH**:对宽度进行压缩,高度自适应。因此得到的结果与 SCALE_LARGER 一致。 212 | - **SCALE_HEIGHT**:对高度进行压缩,宽度自适应,因此得到的结果与 SCALE_HEIGHT 一致。 213 | 214 | ### 4.3 自定义策略 215 | 216 | 自定义一个图片压缩策略也是很简单的,你可以通过继承 SimpleStrategy 或者直接继承 AbstractStrategy 来实现: 217 | 218 | ```kotlin 219 | class MySimpleStrategy: SimpleStrategy() { 220 | 221 | override fun calInSampleSize(): Int { 222 | return 2 223 | } 224 | 225 | fun myLogic(): MySimpleStrategy { 226 | return this 227 | } 228 | 229 | } 230 | ``` 231 | 232 | 注意下,如果想要实现链式的调用,自定义压缩策略的方法需要返回自身。 233 | 234 | ## 5、最后 235 | 236 | 因为我们的项目中,需要把图片的短边控制到 1200,长变只适应,只通过改变 Luban 来改变采样率只能把边长控制到一个范围中,无法精准压缩。所以,我们想到了 Compressor,并提出了 SCALE_SMALLER 的压缩模式. 但是 Luban 也不是用不到,一般用来展示的图片的压缩,它用起来更加方便。因此,我们在库中综合了两个框架,其实代码量并不大。当然,为了让我们的库功能更加丰富,因此我们提出了自定义压缩策略的接口,也是用来降低压缩策略的更换成本吧。 237 | 238 | 最后项目开源在 Github,地址是:https://github.com/Shouheng88/Compressor. 欢迎 Star 和 Fork,为该项目贡献代码或者提出 issue :) 239 | 240 | 后续,笔者会对 Android 端的相机优化和 JNI 操作 OpenCV 进行图片处理进行讲解,感兴趣的关注作者呦 :) 241 | -------------------------------------------------------------------------------- /工作空间/OOM优化.md: -------------------------------------------------------------------------------- 1 | 2 | ### Oom 3 | 4 | OOM就是所谓的内存溢出(Out Of Memory),也就是说内存占有量超过了VM所分配的最大 5 | 6 | ### 出现OOM的原因 7 | 8 | 1. 加载对象过大 9 | 2. 相应资源过多,来不及释放 10 | 11 | ### 如何解决 12 | 13 | 1. 在内存引用上做些处理,常用的有软引用、强化引用、弱引用 14 | 2. 在内存中加载图片时直接在内存中作处理,如边界压缩 15 | 3. 动态回收内存 16 | 4. 优化Dalvik虚拟机的堆内存分配 17 | 5. 自定义堆内存大小 -------------------------------------------------------------------------------- /工作空间/Tinker.md: -------------------------------------------------------------------------------- 1 | # Tinker 热补丁的源码分析 2 | 3 | Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。 4 | 5 | Github https://github.com/Tencent/tinker 6 | 7 | 它主要包括以下几个部分: 8 | 9 | 1. gradle编译插件: `tinker-patch-gradle-plugin` 10 | 2. 核心sdk库: `tinker-android-lib` 11 | 3. 非gradle编译用户的命令行版本: `tinker-patch-cli.jar` 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /工作空间/URL编码问题.md: -------------------------------------------------------------------------------- 1 | 今天在使用URL访问别人的接口的时候,因为要查询的数据中包含中文就出现了问题。 2 | 3 | http://api.map.baidu.com/telematics/v3/weather?location=北京&output=json&ak=XXXX 4 | 5 | 使用 6 | 7 | URL url = new URL(address); 8 | connection = (HttpURLConnection) url.openConnection(); 9 | 10 | 来打开数据连接,但是始终获取不到数据。这是因为要访问的URL中包含中文“北京”,因而无法查询。 11 | 如何修改这个问题呢? 12 | 13 | 实际上,将上面的链接复制到浏览器进行访问是没有问题的。这时,如果细心的话就会发现,粘贴 14 | 之后被访问的连接并非粘贴过去的那串文字,其中的“北京”两个字变成了 15 | 16 | http://api.map.baidu.com/telematics/v3/weather?location=%E5%8C%97%E4%BA%AC&output=json&ak=XXXX 17 | 18 | 就是将“北京”连个字做了变化。所以,要想使用中文的URL的话,也应该将其中的中文进行转码之后再访问。 19 | 20 | 那么如何转码呢? 21 | 22 | String city = mEditText.getText().toString(); 23 | try { 24 | city = URLEncoder.encode(city, "utf-8"); 25 | } catch (UnsupportedEncodingException e) { 26 | e.printStackTrace(); 27 | } 28 | 29 | 使用ERLEncoder.encode即可。这样中文就变成上面那串带有%的字符串了。 30 | -------------------------------------------------------------------------------- /工作空间/res/2017-11-19 17-32-47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/工作空间/res/2017-11-19 17-32-47.png -------------------------------------------------------------------------------- /工作空间/res/2017-11-19 17-33-49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/工作空间/res/2017-11-19 17-33-49.png -------------------------------------------------------------------------------- /工作空间/文章暂时存放.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/工作空间/文章暂时存放.md -------------------------------------------------------------------------------- /工作空间/百度定位API.md: -------------------------------------------------------------------------------- 1 | 2 | ### 百度语音识别API异常: 3 | 4 | 异常1: 5 | 6 | java.lang.UnsatisfiedLinkError: No implementation found for void com.baidu.speech.core.BDSSDKLoader.SetLogLevel(int) (tried Java_com_baidu_speech_core_BDSSDKLoader_SetLogLevel and Java_com_baidu_speech_core_BDSSDKLoader_SetLogLevel__I) 7 | 8 | so文件存在冲突,只要保证libs文件夹下面只有armeabi一个文件夹即可。 9 | 可能之前因为想要兼容各不同的系统所以在使用百度定位的时候创建了多个文件夹,这里需要只留下armeabi一个文件夹。 10 | 11 | ## 使用百度API进行定位(Android Studio) 12 | 13 | ### 1.步骤1:获取密钥 14 | 15 | ### 2.步骤2:下载API开发包 16 | 17 | ### 3.步骤3:配置环境 18 | 19 | #### 3.1 导入jar文件 20 | 21 | 切换Android Studio的工作目录为Project,在lib文件夹下面导入解压后的API开发包。注意要将.so文件和jar文件同时导入,对jar文件还要在其上面右键单击,选择Add as Library,将其作为库导入进来。 22 | 23 | #### 3.2 配置gradle 24 | 25 | 对使用Android Studio开发的同学,仅仅做到这些还是不够的,还要在build.gradle(Modile:app)中添加 26 | 27 | sourceSets { 28 | main { 29 | jniLibs.srcDirs = ['libs'] 30 | } 31 | } 32 | 33 | 这里的作用是添加.so文件,没有添加.so文件或者添加了.so文件而没有在gradle中进行配置的同学,都无法正常使用百度进行定位。 34 | 35 | #### 3.3 在AndroidManifest.xml中进行配置 36 | 37 | 1.声明要使用的权限 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 2.在application中声明service组件 54 | 55 | 每个app拥有自己单独的定位 56 | 57 | 60 | 61 | 62 | 3.在application中声明APP KEY: 63 | 64 | 66 | 67 | ### 步骤4:添加代码 68 | 69 | 我们将与定位相关的代码全部放在一个工具类中 70 | 71 | public class LocationUtils { 72 | 73 | private static LocationUtils sInstance; 74 | 75 | private LocationClient mLocationClient; 76 | 77 | private BDLocationListener bdLocationListener; 78 | 79 | public static LocationUtils getInstance(Context mContext){ 80 | if (sInstance == null){ 81 | sychronized(LocationUtils.class) { 82 | if (sInstance == null) { 83 | sInstance = new LocationUtils(mContext.getApplicationContext()); 84 | } 85 | } 86 | } 87 | return sInstance; 88 | } 89 | 90 | private LocationUtils(Context mContext){ 91 | mLocationClient = new LocationClient(mContext); 92 | } 93 | 94 | public void locate(BDLocationListener mListener){ 95 | bdLocationListener = mListener; 96 | mLocationClient.registerLocationListener(bdLocationListener); 97 | LocationClientOption option = new LocationClientOption(); 98 | option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy); 99 | option.setIsNeedAddress(true); 100 | mLocationClient.setLocOption(option); 101 | mLocationClient.requestLocation(); 102 | mLocationClient.start(); 103 | } 104 | 105 | public void stop() { 106 | if (mLocationClient.isStarted()) { 107 | mLocationClient.stop(); 108 | if (bdLocationListener != null) { 109 | mLocationClient.unRegisterLocationListener(bdLocationListener); 110 | bdLocationListener = null; 111 | } 112 | } 113 | } 114 | } 115 | 116 | 这样每当我们需要定位的时候就可以这样调用: 117 | 118 | ToastUtils.showShortToast(getContext(), R.string.trying_to_get_location); 119 | LocationUtils.getInstance(getContext()).locate(new BDLocationListener() { 120 | @Override 121 | public void onReceiveLocation(BDLocation bdLocation) { 122 | // ... 然后当bdLocation不为空的时候,直接从bdLocation上面获取位置信息就可以了 123 | } 124 | }); 125 | 126 | ### 常见错误: 127 | 128 | #### 1.无法获取位置信息:在监听器当中没有获取到位置的详细信息 129 | 130 | 原因可能是: 131 | 132 | 1. 没有添加 133 | 134 | option.setIsNeedAddress(true); 135 | 这行代码是用来设置用户需不需要获取返回信息的,老版本的方法是使用 136 | 137 | option.setAddrType(“all”) 138 | 139 | 实际上,根据源代码这两个代码的功能是相同的,只是后者现在被抛弃了,使用前者即可。 140 | 141 | 2. 没有在AndroidManifest.xml文件中添加 142 | 143 | 144 | 145 | 出现上面的这种情况也是无法获取的。 146 | 147 | 3. 没有添加.so文件或者添加了.so文件但是没有在gralde文件中进行声明。前面提到过关于.so文件在gralde中声明的方法。 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /工作空间/第三方库整理.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/工作空间/第三方库整理.md -------------------------------------------------------------------------------- /开发工具/ADB_常见的ADB指令总结.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /开发工具/Gradle_常见的指令和配置总结.md: -------------------------------------------------------------------------------- 1 | # 常见的 Gralde 配置和指令总结 2 | 3 | ## 1、依赖相关 4 | 5 | ### 1.1 transitive = true 6 | 7 | ```groovy 8 | dependencies { 9 | implementation ("com.github.bumptech.glide:glide:4.8.0@aar") { 10 | transitive = true 11 | } 12 | } 13 | ``` 14 | 15 | 在后面加上 `@aar`,意指你只是下载该 `aar` 包,而并不下载该 `aar` 包所依赖的其他库,那如果想在使用 `@aar` 的前提下还能下载其依赖库,则需要添加 `transitive=true` 的条件。该属性的默认值是 false,表示你所添加的库的所依赖的其他库会被 Gradle 自动下载。 16 | 17 | ### 1.2 强制设置某个模块的版本 18 | 19 | ```groovy 20 | configurations.all { 21 | resolutionStrategy { 22 | force'org.hamcrest:hamcrest-core:1.3' 23 | } 24 | } 25 | ``` 26 | 27 | ### 1.3 强制排除某个依赖 28 | 29 | ```groovy 30 | configurations.all { 31 | exclude module: 'okhttp-ws' 32 | } 33 | ``` 34 | 35 | ### 1.3 输出模块的依赖树 36 | 37 | 按照下面的方式,这里的 `commons` 是模块名: 38 | 39 | ```groovy 40 | gradlew :commons:dependencies 41 | ``` 42 | 43 | 或者点击 AS 右侧的 `Gradle` 选择 `:commons` -> `Tasks` -> `android` -> `:commons:androidDependencies` 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /开发工具/Keytool_常用的指令.md: -------------------------------------------------------------------------------- 1 | # Keytool 常用的指令合集 2 | 3 | ### 获取 APK 的签名信息 4 | 5 | 方式 1:在命令行中输入下面的命令,即可获取 SHA1 签名信息: 6 | 7 | keytool -printcert -file C:\META-INF\CERT.RSA 8 | 9 | 这里的 `C:\META-INF\CERT.RSA` 是从 APK 包中解压出来的 `RSA` 文件的路径。 10 | 11 | 方式 2:或者使用下面的命令也可以达到相同的目的,并且不用对 APK 进行解压: 12 | 13 | keytool -printcert -jarfile example-release-v2.apk 14 | 15 | ### 显示签名文件的信息 16 | 17 | 在命令行输入下面的指令来显示签名文件的信息,这里的 `palm.jks` 是签名文件: 18 | 19 | keytool -list -v -keystore palm.jks 20 | 21 | 指令执行的结果是: 22 | 23 | ![显示签名文件信息](res/keytool_list.jpg) 24 | 25 | ### 修改签名文件的密码 26 | 27 | 在命令行输入下面的指令来修改签名文件的密码,这里的 `palm.jks` 是签名文件: 28 | 29 | keytool -storepasswd -keystore palm.jks 30 | 31 | 指令的执行结构如下,我们输入旧密码,然后两次输入并确认新密码之后,密码即修改成功: 32 | 33 | ![修改签名文件的密码](res/keytool_password.jpg) 34 | 35 | ### 修改签名文件的别名 36 | 37 | 在命令行输入下面的指令来修改签名文件的别名,这里的 `palm.jks` 是签名文件,`key0` 是之前的别名,`alias0` 是要修改成的别名: 38 | 39 | keytool -changealias -keystore palm.jks -alias key0 -destalias alias0 40 | 41 | 指令执行的结果如下,输入签名文件的密码之后,我们就成功地修改了别名: 42 | 43 | ![修改签名文件的别名](res/keytool_alias_name.jpg) 44 | 45 | 然后,我们使用下面的指令再来查看一下修改之后的签名文件的信息,以确认别名修改成功。如下图所示,别名已经被我们成功地修改成了 `alias0`: 46 | 47 | ![确认修改别名成功](res/keytool_list_after.jpg) 48 | 49 | ### 修改签名文件的别名的密码 50 | 51 | 在命令行输入下面的指令来修改签名文件的别名的密码,这里的 `palm.jks` 是签名文件,`alias0` 是签名文件的别名: 52 | 53 | keytool -keypasswd -keystore palm.jks -alias alias0 54 | 55 | 指令执行的结果如下所示,当我们输入完了签名文件的密码和原来的别名密码之后,输入两遍新的别名的密码即可: 56 | 57 | ![修改签名文件的别名的密码](res/keytool_alias_psd.jpg) 58 | 59 | 60 | -------------------------------------------------------------------------------- /开发工具/res/keytool_alias_name.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/开发工具/res/keytool_alias_name.jpg -------------------------------------------------------------------------------- /开发工具/res/keytool_alias_psd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/开发工具/res/keytool_alias_psd.jpg -------------------------------------------------------------------------------- /开发工具/res/keytool_list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/开发工具/res/keytool_list.jpg -------------------------------------------------------------------------------- /开发工具/res/keytool_list_after.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/开发工具/res/keytool_list_after.jpg -------------------------------------------------------------------------------- /开发工具/res/keytool_password.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/Android-notes/416fdc3de0eaebd7f679bfafee29f0148012b2ad/开发工具/res/keytool_password.jpg -------------------------------------------------------------------------------- /异步编程/Android多线程编程:IntentService和HandlerThread.md: -------------------------------------------------------------------------------- 1 | # Android 多线程编程:IntentService & HandlerThread 2 | 3 | 因为 Android 是使用 Java 开发的,所以当我们谈及 Android 中的多线程,必然绕不过 Java 中的多线程编程。但在这篇文章中,我们不会过多地分析 Java 中的多线程编程的知识。我们会在以后分析 Java 并发编程的时候分析 Java 中的多线程、线程池和并发 API 的用法。 4 | 5 | 我们先来总结一下 Android 多线程编程的演变过程:首先是 Java 的 Thread。因为本身在创建一个线程和销毁一个线程的时候会有一定的开销,当我们任务的执行时间相比于这个开销很小的时候,单独创建一个线程就显得不划算。所以,当程序中存在大量的、小的任务的时候,建议使用线程池来进行管理。但我们一般也很少主动去创建线程池,这是因为——也许是考虑到开发者自己去维护一个线程池比较复杂—— Android 中已经为我们设计了 AsyncTask。AsyncTask 内部封装了一个线程池,我们可以使用它来执行耗时比较短的任务。但 AsyncTask 也有一些缺点:1).如果你的程序中存在很多的不同的任务的时候,你可能要为每个任务定义一个 AsyncTask 的子类。2).从异步线程切换到主线程的方式不如 RxJava 简洁。所以,在实际开发的过程中,我通常使用 RxJava 来实现异步的编程。尤其是局部的优化、不值得专门定义一个 AsyncTask 类的时候,RxJava 用起来更加舒服。 6 | 7 | 上面的多线程创建的只是普通的线程,对系统来说,优先级比较低。在 Android 中还提供了 IntentService 来执行优先级相对较高的任务。启动一个 IntentService 任务的时候会将任务添加到其内部的、异步的消息队列中执行。此外,IntentService 又继承自 Service,所以这让它具有异步和较高的优先级两个优势。 8 | 9 | 在之前的文章中,我们已经分析过 AsyncTask、RxJava 以及用来实现线程切换的 Handler. 这里奉上这些文章的链接: 10 | 11 | - [《Android AsyncTask 源码分析》](https://juejin.im/post/5b65c71af265da0f9402ca4a) 12 | - [《RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结》](https://juejin.im/post/5b72f76551882561354462dd) 13 | - [《RxJava2 系列 (2):背压和Flowable》](https://juejin.im/post/5b759b9cf265da283719d187) 14 | - [《RxJava2 系列 (3):使用 Subject》](https://juejin.im/post/5b801dfa51882542cb409905) 15 | - [《Android 消息机制:Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb) 16 | 17 | 你可以通过以上的文章来了解这部分的内容。在本篇文章中,我们主要来梳理下另外两个多线程相关的 API,HandlerThread 和 IntentService。 18 | 19 | ## 1、异步消息队列:HandlerThread 20 | 21 | 如果你之前还没有了解过 Handler 的实现的话,那么最好通过我们上面的那篇文章 [《Android 消息机制:Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb) 了解一下。因为 HandlerThread 就是通过封装一个 Looper 来实现的。 22 | 23 | ### 1.1 HandlerThread 的使用 24 | 25 | HandlerThread 继承自线程类 Thread,内部又维护了一个 Looper,Looper 内又维护了一个消息队列。所以,我们可以使用 HandlerThread 来创建一个异步的线程,然后不断向该线程发送任务。这些任务会被封装成消息放进 HandlerThread 的消息队列中被执行。所以,我们可以用 HandlerThread 来创建异步的消息队列。 26 | 27 | 在使用 HandlerThread 的时候有两个需要注意的地方: 28 | 29 | 1. 因为 HandlerThread 内部的 Looper 的初始化和开启循环的过程都在 `run()` 方法中执行,所以,在使用 HandlerThread 之前,你必须调用它的 start() 方法。 30 | 2. 因为 HandlerThread 的 `run()` 方法使用 Looper 开启一个了无限循环,所以,当不再使用它的时候,应该调用它的 `quitSafely()` 或 `quit()` 方法来结束该循环。 31 | 32 | 在使用 HandlerThread 的时候只需要创建一个它的实例,然后使用它的 Looper 来创建 Handler 实例,并通过该 Handler 发送消息来将任务添加到队列中。下面是一个使用示例: 33 | 34 | myHandlerThread = new HandlerThread("MyHandlerThread"); 35 | myHandlerThread.start(); 36 | handler = new Handler( myHandlerThread.getLooper() ){ 37 | @Override 38 | public void handleMessage(Message msg) { 39 | // ... do something 40 | } 41 | }; 42 | handler.sendEmptyMessage(1); 43 | 44 | 这里我们创建了 HandlerThread 实例之后用它来创建 Handler 然后通过 Handler 把任务加入到消息队列中进行执行。 45 | 46 | 显然,使用 HandlerThread 可以很轻松地实现一个消息队列。你只需要在创建了 Handler 之后向它发送消息,然后所有的任务将被加入到队列中执行。当然,它也有缺点。因为所有的任务将会被按顺序执行,所以一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。 47 | 48 | ### 1.2 HandlerThread 源码解析 49 | 50 | 下面是该 API 的源码,实现也比较简单,我们直接通过注释来对主要部分进行说明: 51 | 52 | public class HandlerThread extends Thread { 53 | int mPriority; 54 | int mTid = -1; 55 | Looper mLooper; 56 | private @Nullable Handler mHandler; 57 | 58 | public HandlerThread(String name) { 59 | super(name); 60 | mPriority = Process.THREAD_PRIORITY_DEFAULT; 61 | } 62 | 63 | protected void onLooperPrepared() { } 64 | 65 | // 在这个方法开启了 Looper 循环,因为是一个无限循环,所以不适用的时候应该将其停止 66 | @Override 67 | public void run() { 68 | mTid = Process.myTid(); 69 | Looper.prepare(); 70 | synchronized (this) { 71 | mLooper = Looper.myLooper(); 72 | notifyAll(); 73 | } 74 | Process.setThreadPriority(mPriority); 75 | onLooperPrepared(); 76 | Looper.loop(); 77 | mTid = -1; 78 | } 79 | 80 | // 获取该 HandlerThread 对应的 Looper 81 | public Looper getLooper() { 82 | if (!isAlive()) { 83 | return null; 84 | } 85 | synchronized (this) { 86 | while (isAlive() && mLooper == null) { 87 | try { 88 | wait(); 89 | } catch (InterruptedException e) { } 90 | } 91 | } 92 | return mLooper; 93 | } 94 | 95 | public boolean quit() { 96 | Looper looper = getLooper(); 97 | if (looper != null) { 98 | looper.quit(); 99 | return true; 100 | } 101 | return false; 102 | } 103 | 104 | public boolean quitSafely() { 105 | Looper looper = getLooper(); 106 | if (looper != null) { 107 | looper.quitSafely(); 108 | return true; 109 | } 110 | return false; 111 | } 112 | 113 | // ... 无关代码 114 | } 115 | 116 | 上面的代码比较简单明了,在 run() 方法中初始化 Looper 并执行。如果 Looper 还没有被创建,那么当调用 `getLooper()` 方法获取 Looper 的时候会让线程阻塞。当 Looper 创建完毕之后会唤醒所有阻塞的线程继续执行。另外,就是两个停止 Looper 的方法。它们基本上就是对 Looper 进行了一层封装。 117 | 118 | ## 2、IntentService 119 | 120 | ### 2.1 使用 IntentService 121 | 122 | IntentService 继承自 Serivce,因此它比普通的多线程任务优先级要高。这使得它相比于普通的异步任务不容易被系统 kill 掉。它内部也是通过一个 Looper 来实现的,所以也是一种消息队列。在研究它的源码之前,我们先来看一下它的使用。 123 | 124 | IntentService 的使用是比较简单的,只需要:1).继承它并实现其中的 `onHandleIntent()` 方法;2). 将 IntentService 注册到 manifest 中;3). 像开启一个普通的服务那样开启一个 IntentService 即可。下面是该类的一个使用示例: 125 | 126 | public class FileRecognizeTask extends IntentService { 127 | 128 | public static void start(Context context) { 129 | Intent intent = new Intent(context, FileRecognizeTask.class); 130 | context.startService(intent); 131 | } 132 | 133 | public FileRecognizeTask() { 134 | super("FileRecognizeTask"); 135 | } 136 | 137 | @Override 138 | protected void onHandleIntent(@androidx.annotation.Nullable @Nullable Intent intent) { 139 | // 你的需要异步执行的业务逻辑 140 | } 141 | } 142 | 143 | OK,介绍完了 IntentService 的使用,我们再来分析一下它的源码。 144 | 145 | ### 2.2 IntentService 源码分析 146 | 147 | 实现 IntentService 的时候使用到了我们上面分析过的 HandlerThread. 首先,在 `onCreate()` 回调方法中创建了一个 HandlerThread,然后使用它的 Looper 创建了一个 ServiceHandler: 148 | 149 | HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); 150 | thread.start(); 151 | mServiceLooper = thread.getLooper(); 152 | mServiceHandler = new ServiceHandler(mServiceLooper); 153 | 154 | ServiceHandler 是 IntentSerice 的内部类,其定义如下: 155 | 156 | private final class ServiceHandler extends Handler { 157 | 158 | public ServiceHandler(Looper looper) { 159 | super(looper); 160 | } 161 | 162 | @Override 163 | public void handleMessage(Message msg) { 164 | onHandleIntent((Intent)msg.obj); 165 | stopSelf(msg.arg1); 166 | } 167 | } 168 | 169 | ServiceHandler 用来执行被添加到队列中的消息。它会回调 IntentService 中的 `onHandleIntent()` 方法,也就是我们实现业务逻辑的方法。当消息执行完毕之后,会调用 Service 的 `stopSelf(int)` 方法来尝试停止服务。注意这里调用的是 `stopSelf(int)` 而不是 `stopSelf()`。它们之间的区别是,当还存在没有完成的任务的时候 `stopSelf(int)` 不会立即停止服务,而 `stopSelf()` 方法会立即停止服务。 170 | 171 | IntentSerivce 的 `onCreate()` 方法会在第一次启动的时候被调用,来创建服务。而 `onStartCommond()` 方法会在每次启动的时候被调用。下面是该方法的定义。 172 | 173 | @Override 174 | public void onStart(@Nullable Intent intent, int startId) { 175 | Message msg = mServiceHandler.obtainMessage(); 176 | msg.arg1 = startId; 177 | msg.obj = intent; 178 | mServiceHandler.sendMessage(msg); 179 | } 180 | 181 | @Override 182 | public int onStartCommand(@Nullable Intent intent, int flags, int startId) { 183 | onStart(intent, startId); 184 | return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; 185 | } 186 | 187 | `onStartCommand()` 内部调用了 `onStart()` 来处理请求。在 `onStart()` 方法中会通过 mServiceHandler 创建一个消息,并将该消息发送给 mServiceHandler. 该消息会在被 mServiceHandler 放进消息队列中排队,并在合适的时机被执行。 188 | 189 | 因此,我们可以总结一下 IntentService 的工作过程:首先,当我们第一次启动 IntentService 的时候会初始化一个 Looper 和 Handler;然后调用它的 onStartCommond() 方法,把请求包装成消息之后发送到消息队列中等待执行;当消息被 Handler 处理的时候会回调 IntentService 的 `onHandleIntent()` 方法来执行。此时,如果又有一个任务需要执行,那么 IntentService 的 onStartCommond() 方法会再次被执行并把请求封装之后放入队列中。当队列中的所有的消息都执行完毕,并且没有新加入的请求,那么此时服务就会自动停止,否则服务还会继续在后台执行。 190 | 191 | 这里,同样也应该注意下,IntentService 中的任务是按照被添加的顺序来执行的。 192 | 193 | ## 总结 194 | 195 | 以上就是我们对 IntentService 和 HandlerThread 的分析。它们都是使用了 Handler 来实现,所以搞懂它们的前提是搞懂 Handler。关于 Handler,还是推荐一下笔者的另一篇文章 [《Android 消息机制:Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb)。 196 | 197 | 198 | ------ 199 | **如果您喜欢我的文章,可以在以下平台关注我:** 200 | 201 | - 个人主页:[https://shouheng88.github.io/](https://shouheng88.github.io/) 202 | - 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a) 203 | - Github:[https://github.com/Shouheng88](https://github.com/Shouheng88) 204 | - CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068) 205 | - 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113) 206 | 207 | 208 | -------------------------------------------------------------------------------- /性能优化/Android性能优化-ANR.md: -------------------------------------------------------------------------------- 1 | # Android 性能优化 - ANR 的原因和解决方案 2 | 3 | ## 1、出现 ANR 的情况 4 | 5 | 满足下面的一种情况系统就会弹出 ANR 提示 6 | 7 | 1. 输入事件(按键和触摸事件) 5s 内没被处理; 8 | 2. BroadcastReceiver 的事件 ( `onRecieve()` 方法) 在规定时间内没处理完 (前台广播为 10s,后台广播为 60s); 9 | 3. Service 前台 20s 后台 200s 未完成启动; 10 | 4. ContentProvider 的 `publish()` 在 10s 内没进行完。 11 | 12 | 通常情况下就是主线程被阻塞造成的。 13 | 14 | ## 2、ANR 的实现原理 15 | 16 | 以输入无响应的过程为例(基于 9.0 代码): 17 | 18 | 最终弹出 ANR 对话框的位置是与 AMS 同目录的类 `AppErrors` 的 `handleShowAnrUi()` 方法。这个类用来处理程序中出现的各种错误,不只 ANR,强行 Crash 也在这个类中处理。 19 | 20 | ```java 21 | // base/core/java/com/android/server/am/AppErrors.java 22 | void handleShowAnrUi(Message msg) { 23 | Dialog dialogToShow = null; 24 | synchronized (mService) { 25 | AppNotRespondingDialog.Data data = (AppNotRespondingDialog.Data) msg.obj; 26 | // ... 27 | 28 | Intent intent = new Intent("android.intent.action.ANR"); 29 | if (!mService.mProcessesReady) { 30 | intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY 31 | | Intent.FLAG_RECEIVER_FOREGROUND); 32 | } 33 | mService.broadcastIntentLocked(null, null, intent, 34 | null, null, 0, null, null, null, AppOpsManager.OP_NONE, 35 | null, false, false, MY_PID, Process.SYSTEM_UID, 0 /* TODO: Verify */); 36 | 37 | boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(), 38 | Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0; 39 | if (mService.canShowErrorDialogs() || showBackground) { 40 | dialogToShow = new AppNotRespondingDialog(mService, mContext, data); 41 | proc.anrDialog = dialogToShow; 42 | } else { 43 | MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR, 44 | AppNotRespondingDialog.CANT_SHOW); 45 | // Just kill the app if there is no dialog to be shown. 46 | mService.killAppAtUsersRequest(proc, null); 47 | } 48 | } 49 | // If we've created a crash dialog, show it without the lock held 50 | if (dialogToShow != null) { 51 | dialogToShow.show(); 52 | } 53 | } 54 | ``` 55 | 56 | 不过从发生 ANR 的地方调用到这里要经过很多的类和方法。最初抛出 ANR 是在 `InputDispatcher.cpp` 中。我们可以通过其中定义的常量来寻找最初触发的位置: 57 | 58 | ```C++ 59 | // native/services/inputflinger/InputDispatcher.cpp 60 | constexpr nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL; // 5 sec 61 | ``` 62 | 63 | 从这个类触发的位置会经过层层传递达到 `InputManagerService` 中 64 | 65 | ```java 66 | // base/services/core/java/com/android/server/input/InputManagerService.java 67 | private long notifyANR(InputApplicationHandle inputApplicationHandle, 68 | InputWindowHandle inputWindowHandle, String reason) { 69 | return mWindowManagerCallbacks.notifyANR( 70 | inputApplicationHandle, inputWindowHandle, reason); 71 | } 72 | ``` 73 | 74 | 这里的 `mWindowManagerCallbacks` 就是 `InputMonitor` : 75 | 76 | ```java 77 | // base/services/core/java/com/android/server/wm/InputMonitor.java 78 | public long notifyANR(InputApplicationHandle inputApplicationHandle, 79 | InputWindowHandle inputWindowHandle, String reason) { 80 | // ... 略 81 | 82 | if (appWindowToken != null && appWindowToken.appToken != null) { 83 | final AppWindowContainerController controller = appWindowToken.getController(); 84 | final boolean abort = controller != null 85 | && controller.keyDispatchingTimedOut(reason, 86 | (windowState != null) ? windowState.mSession.mPid : -1); 87 | if (!abort) { 88 | return appWindowToken.mInputDispatchingTimeoutNanos; 89 | } 90 | } else if (windowState != null) { 91 | try { 92 | // 使用 AMS 的方法 93 | long timeout = ActivityManager.getService().inputDispatchingTimedOut( 94 | windowState.mSession.mPid, aboveSystem, reason); 95 | if (timeout >= 0) { 96 | return timeout * 1000000L; // nanoseconds 97 | } 98 | } catch (RemoteException ex) { 99 | } 100 | } 101 | return 0; // abort dispatching 102 | } 103 | ``` 104 | 105 | 然后回在上述方法调用 AMS 的 `inputDispatchingTimedOut()` 方法继续处理,并最终在 `inputDispatchingTimedOut()` 方法中将事件传递给 `AppErrors` 106 | 107 | ```java 108 | // base/services/core/java/com/android/server/am/ActivityManagerService.java 109 | public boolean inputDispatchingTimedOut(final ProcessRecord proc, 110 | final ActivityRecord activity, final ActivityRecord parent, 111 | final boolean aboveSystem, String reason) { 112 | // ... 113 | 114 | if (proc != null) { 115 | synchronized (this) { 116 | if (proc.debugging) { 117 | return false; 118 | } 119 | 120 | if (proc.instr != null) { 121 | Bundle info = new Bundle(); 122 | info.putString("shortMsg", "keyDispatchingTimedOut"); 123 | info.putString("longMsg", annotation); 124 | finishInstrumentationLocked(proc, Activity.RESULT_CANCELED, info); 125 | return true; 126 | } 127 | } 128 | mHandler.post(new Runnable() { 129 | @Override 130 | public void run() { 131 | // 使用 AppErrors 继续处理 132 | mAppErrors.appNotResponding(proc, activity, parent, aboveSystem, annotation); 133 | } 134 | }); 135 | } 136 | 137 | return true; 138 | } 139 | ``` 140 | 141 | 当事件传递到了 `AppErrors` 之后,它会借助 Handler 处理消息也就调用了最初的那个方法并弹出对话框。 142 | 143 | 参考:[《Android ANR原理分析》](https://www.cnblogs.com/android-blogs/p/5718302.html) 144 | 145 | ## 3、ANR 的解决办法 146 | 147 | 上面分析了 ANR 的成因和原理,下面我们分析下如何解决 ANR. 148 | 149 | ### 1. 使用 adb 导出 ANR 日志并进行分析 150 | 151 | 发生 ANR的时候系统会记录 ANR 的信息并将其存储到 `/data/anr/traces.txt` 文件中(在比较新的系统中会被存储都 `/data/anr/anr_*` 文件中)。我们可以使用下面的方式来将其导出到电脑中以便对 ANR 产生的原因进行分析: 152 | 153 | adb root 154 | adb shell ls /data/anr 155 | adb pull /data/anr/ 156 | 157 | *在笔者分析 ANR 的时候使用上述指令尝试导出 ANR 日志的时候都出现了 Permission Denied。此时,你可以将手机 Root 之后导出,或者尝试修改文件的读写权限,或者在开发者模式中选择将日志导出到 sdcard 之后再从 sdcard 将日志发送到电脑端进行查看* 158 | 159 | ### 2. 使用 DDMS 的 traceview 进行分析 160 | 161 | 在 AS 中打开 DDMS,或者到 SDK 安装目录的 tools 目录下面使用 `monitor.bat` 打开 DDMS。 162 | 163 | TraceView 工具的使用可以参考这篇文章:[《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558) 164 | 165 | 这种定位 ANR 的思路是:**使用 TraceView 来通过耗时方法调用的信息定位耗时操作的位置**。 166 | 167 | 资料: 168 | 169 | - [《ANR 官方文档》](https://developer.android.com/topic/performance/vitals/anr) 170 | - [《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558) 171 | 172 | ### 3. 使用开源项目 ANR-WatchDog 来检測 ANR 173 | 174 | 项目地址是 [Github-ANR-WatchDog](https://github.com/SalomonBrys/ANR-WatchDog) 175 | 176 | 该项目的实现原理:创建一个检测线程,该线程不断往 UI 线程 post 一个任务,然后睡眠固定时间,等该线程又一次起来后检測之前 post 的任务是否运行了,假设任务未被运行,则生成 ANRError,并终止进程。 177 | 178 | ### 4. 常见的 ANR 场景 179 | 180 | 1. I/O 阻塞 181 | 2. 网络阻塞 182 | 3. 多线程死锁 183 | 4. 由于响应式编程等导致的方法死循环 184 | 5. 由于某个业务逻辑执行的时间太长 185 | 186 | ### 5. 避免 ANR 的方法 187 | 188 | 1. UI 线程尽量只做跟 UI 相关的工作; 189 | 2. 耗时的工作 (比如数据库操作,I/O,网络操作等),采用单独的工作线程处理; 190 | 3. 用 Handler 来处理 UI 线程和工作线程的交互; 191 | 4. 使用 RxJava 等来处理异步消息。 192 | 193 | 总之,一个原则就是:**不在主线程做耗时操作**。 194 | 195 | -------------------------------------------------------------------------------- /性能优化/Android性能优化-内存优化.md: -------------------------------------------------------------------------------- 1 | # Android 性能优化 - 内存优化 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /性能优化/Android性能优化-启动优化.md: -------------------------------------------------------------------------------- 1 | # Android 性能优化 - 启动优化 2 | 3 | ## 1、基础 4 | 5 | ### 1.1 启动的类型 6 | 7 | 首先是启动的三种类型: 8 | 9 | 1. **冷启动场景**:后台完全没有任何进程的情况下,启动最慢; 10 | 2. **温启动场景**:按返回键退回主界面再从主界面打开的情形,较快; 11 | 3. **热启动场景**:按 Home 键退回到主界面再从主界面打开的情形,最快。 12 | 13 | 应用启动的过程实际上也就是 Activity 启动的流程,所以具体涉及的源码不是我们这里的重点,你可以查找 Activity 启动流程相关的文章来了解源码。 14 | 15 | 其实优化应用的启动速度无非也就是在那几个生命周期方法中进行优化,不做太多耗时操作等:Application 的生命周期和 Activity 的生命周期。 16 | 17 | ### 1.2 启动速度的测量 18 | 19 | 当然,我们而已通过自己的感觉判断启动的快慢,但量化还是非常重要的,不然你都无法向 PM 交差不是。所以,我们有必要了解下 Android 中的启动速度是如何测量的。 20 | 21 | #### 方式 1:使用 ADB 22 | 23 | 获取启动速度的第一种方式是使用 ADB,使用下面的指令的时候在启动应用的时候会使用 AMS 进行统计。但是缺点是统计时间不够准确: 24 | 25 | ```shell 26 | adb shell am start -n {包名}/{包名}.{活动名} 27 | ``` 28 | 29 | #### 方式 2:代码埋点 30 | 31 | 在 Application 的 `attachBaseContext()` 方法中记录开始时间,第一个 Activity 的 `onWindowFocusChanged()` 中记录结束时间。缺点是统计不完全,因为在 `attachBaseContext()` 之前还有许多操作。 32 | 33 | #### 方式 3:TraceView 34 | 35 | 在 AS 中打开 DDMS,或者到 SDK 安装目录的 tools 目录下面使用 `monitor.bat` 打开 DDMS。 36 | 37 | TraceView 工具的使用可以参考这篇文章:[《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558) 38 | 39 | 通过 TraceView 主要可以得到两种数据:单次执行耗时的方法以及执行次数多的方法。但 TraceView 性能耗损太大,不能比较正确反映真实情况。 40 | 41 | #### 方式 4:Systrace 42 | 43 | Systrace 能够追踪关键系统调用的耗时情况,如系统的 IO 操作、内核工作队列、CPU 负载、Surface 渲染、GC 事件以及 Android 各个子系统的运行状况等。但是不支持应用程序代码的耗时分析。 44 | 45 | #### 方式 5:Systrace + 插桩 46 | 47 | 类似于 AOP,通过切面为每个函数统计执行时间。这种方式的好处是能够准确统计各个方法的耗时。 48 | 49 | 原理就是 50 | 51 | ```java 52 | public void method() { 53 | TraceMethod.i(); 54 | // Real work 55 | TraceMethod.o(); 56 | } 57 | ``` 58 | 59 | #### 方式 6:录屏 60 | 61 | 录屏方式收集到的时间,更接近于用户的真实体感。可以在录屏之后按帧来进行统计分析。 62 | 63 | ## 2、启动优化 64 | 65 | ### 2.1 一般解决办法 66 | 67 | #### 2.1.1 延迟初始化 68 | 69 | 一些逻辑,如果没必要在程序启动的时候就立即初始化,那么可以将其推迟到需要的时候再初始化。比如,我们可以使用单例的方式来获取类的实例,然后在获取实例的时候再进行初始化操作。 70 | 71 | **但是需要注意的是,懒加载要防止集中化,否则容易出现首页显示后用户无法操作的情形。可以按照耗时和是否必要将业务划分到四个维度:必要且耗时,必要不耗时,非必要但耗时,非必要不耗时。**然后对应不同的维度来决定是否有必要在程序启动的时候立即初始化。 72 | 73 | #### 2.1.2 防止主线程阻塞 74 | 75 | 一般我们也不会把耗时操作放在主线程里面,毕竟现在有了 RxJava 之后,在程序中使用异步代价并不高。这种耗时操作包括,大量的计算、IO、数据库查询和网络访问等。 76 | 77 | 另外,关于开启线程池的问题下面的话总结得比较好,除了一般意义上线程池和使用普通线程的区别,还要考虑应用启动这个时刻的特殊性: 78 | 79 | > 如何开启线程同样也有学问:Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService 等都各有利弊;例如通常情况下 ThreadPoolExecutor 比 Thread 更加高效、优势明显,但是特定场景下单个时间点的表现 Thread 会比 ThreadPoolExecutor 好:同样的创建对象,ThreadPoolExecutor 的开销明显比 Thread 大。 80 | > 81 | > 来自:https://www.jianshu.com/p/f5514b1a826c 82 | 83 | #### 2.1.3 布局优化 84 | 85 | 比如,之前我在使用 Fragment 和 ViewPager 搭配的时候,发现虽然 Fragment 可以被复用,但是如果通过 Adapter 为 ViewPager 的每个项目指定了标题,那么这些标题控件不会被复用。当 ViewPager 的条目比较多的时候,甚至会造成 ANR. 86 | 87 | 对于这种布局优化相关的东西,可以参考性能优化的 [Android性能优化-布局优化](Android性能优化-布局优化.md) 模块。 88 | 89 | #### 2.1.4 使用启动页面防止白屏 90 | 91 | 这种方法只是治标不治本的方法,就是在应用启动的时候避免白屏,可以通过设置自定义主题来实现。 92 | 93 | 这种实现方式可以参考我的开源项目 [MarkNote](https://github.com/Shouheng88/MarkNote) 的实现。 94 | 95 | ### 2.2 其他借鉴办法 96 | 97 | #### 2.2.1 使用 BlockCanary 检测卡顿 98 | 99 | BlockCanary 是一个开源项目,类似于 LeakCanary (很多地方也借鉴了 LeakCanary 的东西),主要用来检测程序中的卡顿,项目地址是 [Github-BlockCanary](https://github.com/markzhai/AndroidPerformanceMonitor). 它的原理是对 Looper 中的 `loop()` 方法打处的日志进行处理,通过一个自定义的日志输出 Printer 监听方法执行的开始和结束。可以通过该项目作者的文章来了解这个项目: 100 | 101 | [BlockCanary — 轻松找出Android App界面卡顿元凶](https://www.jianshu.com/p/cd7fc77405ac) 102 | 103 | #### 2.2.2 GC 优化 104 | 105 | GC 优化的思想就是减少垃圾回收的时间间隔,所以在启动的过程中不要频繁创建对象,特别是大对象,避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到 Native 实现。 106 | 107 | #### 2.2.3 类重排 108 | 109 | 如果我们的代码在打包的时候被放进了不同的 dex 里面,当启动的时候,如果需要用到的类分散在各个 dex 里面,那么系统要花额外的时间到各个 dex 里加载类。因此,我们可以通过类重排调整类在 Dex 中的排列顺序,把启动时用到的类放进主 dex 里。 110 | 111 | 目前可以使用 [ReDex](https://github.com/facebook/redex) 的 [Interdex](https://github.com/facebook/redex/blob/master/docs/Interdex.md) 调整类在 Dex 中的排列顺序。 112 | 113 | 可以参考下面这篇文章来了解类重拍在手 Q 中的应用以及他们遇到的各种问题: 114 | 115 | [Redex 初探与 Interdex:Andorid 冷启动优化](https://mp.weixin.qq.com/s/Bf41Kez_OLZTyty4EondHA?) 116 | 117 | #### 2.2.4 资源文件重排 118 | 119 | 对应于类重排,还有资源的重排。可以参考下阿里的资源重排优化方案: 120 | 121 | [支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能](https://mp.weixin.qq.com/s/79tAFx6zi3JRG-ewoapIVQ) 122 | 123 | 这种方案的原理时先通过测试找出程序启动过程中需要加载的资源,然后再打包的时候通过修改 7z 压缩工具将上述热点资源放在一起。这样,在系统进行资源加载的时候,这些资源将要用到的资源会一起被加载进程内存当中并缓存,减少了 IO 的次数,同时不需要从磁盘读取文件,来提高应用启动的速度。 124 | 125 | #### 2.2.5 类的加载 126 | 127 | 通过 Hook 来去掉应用启动过程中的 verify 来减少启动过程中的耗时。但是这种方式存在虚拟机兼容的问题,在 ART 虚拟机上面进行 Hook 需要兼容几个版本。 128 | 129 | 130 | 参考资料: 131 | 132 | - [App startup time](https://developer.android.com/topic/performance/vitals/launch-time) 133 | - [Android性能优化(一)之启动加速35%](https://www.jianshu.com/p/f5514b1a826c) 134 | - [爱奇艺技术分享:爱奇艺Android客户端启动速度优化实践总结](https://www.jianshu.com/p/bd3930316c8d) -------------------------------------------------------------------------------- /性能优化/Android性能优化-布局优化.md: -------------------------------------------------------------------------------- 1 | # Android 性能优化 - 布局优化 2 | 3 | ### 1. 合理选择 ViewGroup 4 | 5 | 在选择使用 Android 中的布局方式的时候应该遵循:**尽量少使用性能比较低的容器控件,比如 RelativeLayout,但如果使用 RelativeLayout 可以降低布局的层次的时候可以考虑使用**。 6 | 7 | Android 中的控件是树状的,降低树的高度可以提升布局性能。RelativeLayout 的布局比 FrameLayout、LinearLayout 等简单,因而可以减少计算过程,提升程序性能。 8 | 9 | *注:参见第 9 条,关于 ConstaintLayout 的介绍。* 10 | 11 | ### 2. 使用 `` 标签复用布局 12 | 13 | **多个地方共用的布局可以使用 `` 标签在各个布局中复用** 14 | 15 | ```xml 16 | 17 | ``` 18 | 19 | ### 3. 使用 `` 标签复用父容器 20 | 21 | **可以通过使用 `` 来降低布局的层次**。 `` 标签通常与 `` 标签一起使用, `` 作为可以复用的布局的根控件。然后使用 `` 标签引用该布局。 22 | 23 | ```xml 24 | 25 | 28 | 29 | 30 | 31 | 32 |