├── 5 道刁钻的 Activity 生命周期面试题.md ├── Activity LaunchMode.md ├── Android APP 的安装过程.md ├── Android Canvas 绘制小黄人.md ├── Android OpenGl ES 基础入门知识.md ├── Android 方法插桩 plugin 开发实践.md ├── Android 知识简记 .md ├── Android 自定义 View | 剑气加载.md ├── Application.md ├── Builder 模式.md ├── DispatchTouchEvent.md ├── GUI 系统综述.md ├── LeakCanary 一只优雅的金丝雀.md ├── Low Memory Killer.md ├── README.md ├── RecyclerView 的复用机制.md ├── Service 启动流程.md ├── Service 详解.md ├── ServiceManager 的工作原理.md ├── Zygote 启动流程.md ├── Zygote 工作原理.md ├── bindService.md ├── img ├── 1.jpeg ├── 1240-20200630001324067.png ├── 1240-20200630001333060.png ├── 1240-20200630001341005.png ├── 1240-20200630001349746.png ├── 1240-20200630001356962.png ├── 1240.png ├── 15.jpg ├── 16.jpg ├── 17.jpg ├── 18.jpg ├── 20.jpg ├── 23种设计模式.png ├── 640.jpeg ├── InterpreterUML.jpg ├── Screenshot_1593358817.png ├── Service 创建流程图.jpg ├── ape_fwk_graphics.png ├── bindService.jpg ├── bindServiceLifeFix.jpg ├── bindServiceLifecycle.png ├── binder.png ├── binder_code.png ├── binder_design.jpg ├── binder_driver.jpg ├── binder_mmap.jpg ├── binder_oneway.png ├── binder_proc.png ├── bufferqueue.png ├── builder.jpg ├── circle 11.gif ├── circle 12.gif ├── circle 13.gif ├── circle 14.gif ├── fd.png ├── getimgdata.jpeg ├── image-20200628233753147.png ├── image-20200707233520753.png ├── image-20200721005441905.png ├── image-20200721225839304.png ├── image-20200811230828501.png ├── image-20200817232940570.png ├── image-20200907203547588.png ├── image-20200917223121021.png ├── image-20200917232430861.png ├── image-20200917234119283.png ├── image-20201224090119744.png ├── image-20201224121659744.png ├── image-20201230124630002.png ├── image-20210116220618932.png ├── image-20210408232901834.png ├── image-20210620231859052.png ├── image-20210727225916008.png ├── image-20210728143840779.png ├── image-20210728144033706.png ├── image-20210728144155409.png ├── image-20210728144401013.png ├── image-20210728145422282.png ├── image-20210921002044181.png ├── image-20210921002129703.png ├── image-20210921002239797.png ├── image-20210921002857005.png ├── image-20210921004428266.png ├── image-20210921010008348.png ├── pqmn.jpg ├── qrcode.jpg ├── serviceLifecycle.png ├── strip.gif ├── viewGroupUml.jpg ├── webp ├── 中介者模式.jpg ├── 举个栗子.jpg ├── 事件分发图.png ├── 六大设计原则.png ├── 原型模式.jpg ├── 吐槽代码1.png ├── 命令模式.jpg ├── 备忘录模式.jpg ├── 工厂方法模式.jpg ├── 抽象工厂模式.jpg ├── 时间轴.png ├── 模板模式.jpg ├── 盒子.jpg ├── 组合 uml.jpg ├── 观察者模式.jpg ├── 访问者.jpg ├── 责任链模式.jpg └── 迭代器模式.jpg ├── jetpack └── JetPack | App Startup.md ├── startActivity 都不知道还敢来面试?.md ├── 一组视频时间轴控件.md ├── 三个编码小技巧.md ├── 中介者模式.md ├── 你了解 Android 系统启动流程吗?.md ├── 你真的懂 Context 吗?.md ├── 做了三年安卓,我竟然不知道什么是 UI 线程.md ├── 单例的线程安全及序列化问题.md ├── 原型模式.md ├── 命令模式.md ├── 备忘录模式.md ├── 大厂内推.md ├── 如何绕过 Android 8.0 startService 限制?.md ├── 如何跨进程传递大图.md ├── 工厂模式.md ├── 年轻人不讲码德,我大意了,没有 Review.md ├── 怎么解决引用计数 GC 的循环引用问题?.md ├── 投稿.md ├── 抽象工厂.md ├── 按下 Home 键后发生了什么.md ├── 掌握 binder 机制?先搞懂这几个关键类!.md ├── 掌握 binder 机制?别想绕开 binder 驱动源码分析!.md ├── 掌握 binder 机制?驱动核心源码详解分享.md ├── 效率 | macOS 双击安装 APK .md ├── 文件描述符.md ├── 模板模式.md ├── 状态模式.md ├── 看你简历上写熟悉 AIDL,说一说 oneway 吧.md ├── 策略模式.md ├── 简洁明了的刘海屏适配方案.md ├── 组合模式.md ├── 观察者模式.md ├── 解释器模式.md ├── 访问者模式.md ├── 说一说 Android 消息机制.md ├── 说说你对 binder 驱动的了解?.md ├── 谈谈你对 ThreadLocal 的理解.md ├── 谈谈你对 binder 的理解?这样回答才过关.md ├── 责任链模式.md ├── 迭代器模式.md ├── 非主进程的主线程耗时会 ANR 吗.md └── 面向对象的六大原则.md /5 道刁钻的 Activity 生命周期面试题.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 今天我们一起来看五道 Activity 生命周期的面试题,相信看完之后面试官再问到相关的问题,你就能胸有成竹了。 4 | 5 | ## A Activity 打开 B Activity 时都有哪些生命周期回调。 6 | 7 | 这道题相信很多同学都有遇到过,简单 A.onPause -> B.onCreate -> B.onStart -> B.onResume -> A.onStop . 8 | Naive ! 这样回答只是及格,因为仅在 B Activity 的 launchMode 为 standard 或者 B Activity 没有可复用的实例时是这样的。 9 | 10 | 当 B Activity 的 launchMode 为 singleTop 且 B Activity 已经在栈顶时(一些特殊情况如通知栏点击、连点),此时只有 B 页面自己有生命周期变化: 11 | B.onPause -> B.onNewIntent -> B.onResume 12 | 13 | 当 B Activity 的 launchMode 为 singleInstance ,singleTask 且对应的 B Activity 有可复用的实例时,生命周期回调是这样的: 14 | A.onPause -> B.onNewIntent -> B.onRestart -> B.onStart -> B.onResume -> A.onStop -> ( 如果 A 被移出栈的话还有一个 A.onDestory) 15 | 16 | 另外当 B 的主题为透明时,不会调用 B.onStop 17 | 18 | 把几种情况都回答出来就能加分啦,同时也要做好聊 launchMode 的准备。 19 | 20 | ## 弹出 Dialog 对生命周期有什么影响 21 | 22 | 我们知道,生命周期回调都是 AMS 通过 Binder 通知应用进程调用的;而弹出 Dialog、Toast、PopupWindow 本质上都直接是通过 WindowManager.addView() 显示的(没有经过 AMS),所以不会对生命周期有任何影响。 23 | 24 | 如果是启动一个 Theme 为 Dialog 的 Activity , 则生命周期为: 25 | A.onPause -> B.onCreate -> B.onStart -> B.onResume 26 | 注意这边没有前一个 Activity 不会回调 onStop,因为只有在 Activity 切到后台不可见才会回调 onStop;而弹出 Dialog 主题的 Activity 时前一个页面还是可见的,只是失去了焦点而已所以仅有 onPause 回调。 27 | 28 | ## Activity 在 onResume 之后才显示的原因是什么 29 | 30 | 虽然我们设置 Activity 的布局一般都是在 onCreate 方法里调用 setContentView 。里面是直接调用 window 的 setContentView,创建一个 DecorView 用来包住我们创建的布局。详情如下: 31 | 32 | 33 | ```java 34 | PhoneWindow.java 35 | public void setContentView(int layoutResID) { 36 | if (mContentParent == null) { 37 | installDecor(); 38 | } 39 | ... 40 | // 加载布局,添加到 mContentParent 41 | // mContentParent 又是 DecorView 的一个子布局 42 | mLayoutInflater.inflate(layoutResID, mContentParent); 43 | } 44 | ``` 45 | 46 | 然而这一步只是加载好了布局,生成一个 ViewTree , 具体怎么把 ViewTree 显示出来,答案就在下面: 47 | 48 | ```java 49 | ActivityThread.java 50 | public void handleResumeActivity(...){ 51 | // onResume 回调 52 | ActivityClientRecord r = performResumeActivity(...) 53 | final Activity a = r.activity; 54 | if (r.window == null && !a.mFinished && willBeVisible) { 55 | r.window = r.activity.getWindow(); 56 | View decor = r.window.getDecorView(); 57 | ViewManager wm = a.getWindowManager(); 58 | wm.addView(decor, l);// 重点 59 | } 60 | } 61 | ``` 62 | 63 | WindowManager 的 addView 方法最终将 DecorView 添加到 WMS ,实现绘制到屏幕、接收触屏事件。具体的调用链如下: 64 | 65 | ```java 66 | WindowManagerImpl.addView 67 | -> WindowManagerGlobal.addView 68 | -> ViewRootImpl.setView 69 | -> ViewRootImpl.requestLayout() // 执行 View 的绘制流程 70 | // 通过 Binder 调用 WMS ,WMS 会添加一个 Window 相关的对象 71 | // 应用端通过 mWindowSession 调用 WMS 72 | // WMS 通过 mWindow (一个 Binder 对象) 调用应用端 73 | mWindowSession.addToDisplay(mWindow) 74 | ``` 75 | 76 | 综上,在 onResume 回调之后,会创建一个 ViewRootImpl ,有了它之后应用端就可以和 WMS 进行双向调用了。同时也是通过 ViewRootImpl 从 WMS 申请 Surface 来绘制 ViewTree 。 77 | 78 | ## onActivityResult 在哪两个生命周期之间回调 79 | 80 | onActivityResult 不属于 Activity 的生命周期,一般被问到这个问题时大家都会懵逼。其实答案很简单,onActivityResult 方法的注释中就写着答案:**You will receive this call immediately before onResume() when your activity is re-starting.** 跟一下代码(TransactionExecutor.execute 有兴趣的可以自己打断点跟一下),会发现 onActivityResult 回调先于该 Activity 的所有生命周期回调,从 B Activity 返回 A Activity 的生命周期调用为: 81 | `B.onPause -> A.onActivityResult -> A.onRestart -> A.onStart -> A.onResume` 82 | 83 | ## onCreate 方法里写死循环会 ANR 吗 84 | 85 | ANR 的四种场景: 86 | 87 | 1. Service TimeOut: service 未在规定时间执行完成: 前台服务 20s,后台 200s 88 | 2. BroadCastQueue TimeOut: 未在规定时间内未处理完广播:前台广播 10s 内, 后台 60s 内 89 | 3. ContentProvider TimeOut: publish 在 10s 内没有完成 90 | 4. Input Dispatching timeout: 5s 内未响应键盘输入、触摸屏幕等事件 91 | 92 | 我们可以看到,Activity 的生命周期回调的阻塞并不在触发 ANR 的场景里面,所以并不会直接触发 ANR。只不过死循环阻塞了主线程,如果系统再有上述的四种事件发生,就无法在相应的时间内处理从而触发 ANR。 93 | 94 | 95 | 96 | ## 总结 97 | 98 | Activity 的生命周期很基础而且也很重要,这也是面试常问的原因。相关的面试题可以涉及到 framework 的一些知识,平常在处理一些问题的时候最好不要只是打下日志看下结果,多钻进去源码看看,才能有更多收获,也记得更牢。 99 | 100 | -------------------------------------------------------------------------------- /Activity LaunchMode.md: -------------------------------------------------------------------------------- 1 | # Activity 的 LaunchMode 及使用场景 2 | 3 | `android:launchMode` 有关应如何启动 Activity 的指令。共有四种模式可与 `Intent` 对象中的 Activity 标记(`FLAG_ACTIVITY_*` 常量)协同工作,以确定在调用 Activity 处理 Intent 时应执行的操作。启动模式有以下四种: 4 | 5 | #### standard 标准模式 6 | 7 | 这是默认模式,每次激活 Activity 时都会创建 Activity 实例,并放入任务栈中。使用场景:大多数 Activity。 8 | 9 | * 本应用打开 activity 实例放入本应用,即发送 intent 的 task 栈的顶部。Activity A 启动了 Activity B (standard), 则 B 运行在 A 所在的栈中。 10 | * 用 ApplicationContext 去启动 standard 模式的 Activity 会报错,由于非 Activity 的 Context 并没有任务栈,所以新开的 Activity 不知道应该放哪,解决方法是指定 FLAG_ACTIVITY_NEW_TASK,这样启动的时候就会为它创建一个新的 Task,实际是以 singleTask 启动的 11 | * 跨应用打开仍在同一个栈中,但是 recent app 是两个页面; 如果 intent flag 带有`FLAG_ACTIVITY_RESET_TASK_IF_NEEDED`,则跨应用会在同一个 recent app 12 | 13 | #### singleTop 栈顶复用 14 | 15 | 如果在当前任务的栈顶正好存在该 Activity 的实例,就重用该实例( 会调用实例的 onNewIntent() ),否则就会创建新的实例并放入栈顶,即使栈中已经存在该 Activity 的实例,只要不在栈顶,都会创建新的实例。使用场景如新闻类或者阅读类 App 的内容页面。 16 | 17 | #### singleTask 栈内复用 18 | 19 | 会在系统中查找属性值 affinity 等于它的属性值 taskAffinity 的任务栈,如果存在则在该这个任务栈中启动,否则就在建新任务栈(affinity 值为它的 taskAffinity)启动(FLAG_ACTIVITY_NEW_TASK 同样的情况) 20 | 如果在任务栈中已经有该 Activity 的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈(即 singleTask 有 clearTop 的效果)。如果栈中不存在该实例,将会创建新的实例放入栈中。 21 | 22 | 适用于项目首页。 23 | 24 | 会使 flag`FLAG_ACTIVITY_RESET_TASK_IF_NEEDED` 失效 25 | 26 | 例子: 27 | 28 | * 目前任务栈 S1 中情况为 ABC,此时 Activity D 以 singleTask 请求启动,需要的任务栈 S2,由于 S2, D 均不存在,则系统先创建任务栈 S2,再创建 D 放入 S2 中。 29 | * 若 D 需要的是 S1,其它情况同上述,则系统直接创建 D,放入栈 S1 中 30 | * 若 D 需要的是 S1,且 S1 的情况 ADBC,此时 D 不会重新创建,而是移除 BC,D 即为栈顶,调用 onNewIntent 31 | 32 | #### singleInstance 单实例模式 33 | 34 | 加强版的 singleTask, 具有所有的特性,与 singleTask 唯一的区别 系统不会在 singleInstance activity 的 task 栈中放入任何其他的 activity 实例,**单独位于一个 task 中** 35 | 36 | singleInstance 不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,由于 B 是自己一个任务栈,而 AC 默认情况下是同一个栈,这样回退的时候就是 C -> A -> B 可能会让用户懵逼。 37 | 38 | 应用场景:来电界面、Launcher 页面 39 | 40 | 41 | #### 启动模式的使用方式 42 | 43 | 1. Manifest.xml 静态指定,在代码中跳转时会依照指定的模式来创建 Activity 44 | 45 | ``` 46 | 49 | ``` 50 | 51 | 2. 启动时在 Intent 中动态指定 52 | 53 | ``` 54 | Intent intent = new Intent(); 55 | intent.setClass(context, MainActivity.class); 56 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 57 | context.startActivity(intent); 58 | ``` 59 | 60 | 差别: 61 | 62 | * 优先级方法 2 动态指定较高,同时存在时以 2 为准; 63 | * 范围:方法 1 无法指定 FLAG_ACTIVITY_CLEAR_TOP,方法 2 无法指定 singleInstance 模式 64 | 65 | #### Activity 的 Flags 66 | 67 | * FLAG_ACTIVITY_NEW_TASK 如果 taskAffinity 一样则与标准模式一样新启动一个 Activity,如果不一样则新建一个 task 放该 Activity 68 | * FLAG_ACTIVITY_SINGLE_TOP 与 SingleTop 效果一致 69 | * FLAG_ACTIVITY_CLEAR_TOP 销毁目标 Activity 和它之上的所有 Activity,重新创建目标 Activity,+ FLAG_ACTIVITY_SINGLE_TOP 效果与 SingleTask 效果一致 70 | * FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 具有此标记位的 Activity 不会出如今历史 Activity 的列表中 71 | 72 | 其它 flag 查文档 73 | 74 | #### activity stack 、 task 75 | 76 | * stack 用栈的方式管理 Activity,这样后入的 activity 会被先移除 77 | * Task 是指将相关的 Activity 组合到一起,以 Activity Stack 的方式进行管理,使得多个应用的 Activity 可以组合共同完成一个任务(Task)。 78 | 79 | #### Taskaffinity 80 | 81 | > 值为字符串,必须包含分隔符. 82 | 83 | 用来控制 Activity 所属的任务栈,可以翻译为任务相关性。设置 activity 的启动模式为 singleTask 或 singleInstance 才能生效(其实 singleInstance 本来就会在新 Task 中)。 84 | 不同的 TaskStack, Affinity 可以一样。 85 | 86 | 1. 通过在 Manifest.xml 配置 87 | 88 | ``` 89 | 92 | ``` 93 | 2. Intent 动态设置 94 | 95 | 如果在 xml 设未设置 launchMode,只设置了 taskAffinity 则需要在 intent 时指定相关的 launchMode 的 flag 96 | 97 | ``` 98 | Intent intent = new Intent(aAvtivity.this, bActivity.class); 99 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 100 | startActivity(intent); 101 | ``` 102 | 103 | 当 TaskAffinity 生效时,如已经存在相应名称的任务栈,则不会新建栈,而是在该栈的栈顶建立相应 activity;如果没有相应名称的任务栈,就会建立对应名称的新的任务栈。 104 | 105 | #### 与 allowTaskReparenting 混用 106 | 107 | 在应用 A 中启动了应用 B 的 ActivityC,若 allowTaskReparenting 为 true,则 C 会从 A 的栈中移到 B 的任务栈。此时从 home,打开应用 B 显示的不是主界面,而是 ActivityC 108 | 109 | 110 | 最后说一下,查看当前任务栈的 adb 命令 adb shell dumpsys activity activities 111 | 112 | -------------------------------------------------------------------------------- /Android OpenGl ES 基础入门知识.md: -------------------------------------------------------------------------------- 1 | 相信大家都听过大名鼎鼎的 OpenGL,但可能大多数人没有实践使用过,本文就来介绍一下 Android OpenGl ES 的基础入门知识。 2 | 3 | 或许你在工作中不会用到,*但为你个人成长着想一下*,扩展自己的知识广度,总归是有利无弊的,你说对吧? 4 | 5 | 本文分为以下四部分介绍: 6 | - OpenGL 基础概念 7 | - OpenGL 坐标系理解 8 | - OpenGL 渲染管线 9 | - OpenGL 着色语言 10 | 11 | *建议收藏本文哦~* 12 | 13 | ## OpenGL 基础概念 14 | 15 | #### OpenGL 16 | OpenGL 即 Open Graphics Library,是一个功能强大、调用方便的底层图形库,它定义了跨编程语言、跨平台的专业图形程序接口,可用于二维或三维图像的处理与渲染。 17 | 18 | OpenGL 是跨平台的,除了它纯粹专注的渲染外,其他内容在每个平台上都要有它的具体实现,比如上下文环境和窗口的管理就交由各个设备自己来完成。 19 | #### OpenGL ES 20 | OpenGL ES (OpenGL for Embedded Systems)是三维图形 API OpenGL 的子集,针对手机、PDA 和游戏主机等嵌入式设备而设计。 21 | 22 | Android 对应 OpenGL ES 的版本支持如下: 23 | - Android 1.0 开始支持 OpenGL ES 1.0 及 1.1 24 | - Android 2.2 开始支持 OpenGL ES 2.0 25 | - Android 4.3 开始支持 OpenGL ES 3.0 26 | - Android 5.0 开始支持 OpenGL ES 3.1 27 | 28 | 其中 OpenGL ES 1.0 是以 OpenGL 1.3 规范为基础的,OpenGL ES 1.1 是以 OpenGL 1.5 规范为基础的,而 OpenGL ES 2.0 基于 OpenGL 2.0 实现。 29 | 30 | 2.x 版本相比 1.x 版本有较大差异,1.x 版本为 fixed function pipeline,即固定管线硬件,而 2.x 版本为 programmable pipeline,可编程管线硬件。 31 | 32 | 固定管线中原本由系统做的一部分工作,在可编程管线中必须需要自己写程序实现,具体程序为 vertex shader(顶点着色器)和 fragment shader(片元着色器)。 33 | 34 | #### OpenGL 上下文 35 | OpenGL 是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信息的数据结构就可以看作 OpenGL 的上下文。 36 | 37 | 调用任何 OpenGL 函数前,必须已经创建了 OpenGL Context,GL Context 存储了 OpenGL 的状态变量以及其他渲染有关的信息。 38 | 39 | OpenGL 是个状态机,有很多状态变量,是个标准的过程式操作过程,改变状态会影响后续所有操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。 40 | 41 | OpenGL 采用 Client-Server 模型来解释 OpenGL 程序,即 Server 存储 GL Context(可能不止一个),Client 提出渲染请求,Server 给予响应,一般 Server 和 Client 都在我们的 PC 上,但 Server 和 Client 也可以是通过网络连接。 42 | 43 | 之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的 OpenGL 渲染工作也将结束。 44 | 45 | #### EGL 46 | 在 OpenGL 的设计中,OpenGL 是不负责管理窗口的,窗口的管理交由各个设备自己来完成,具体来讲,IOS 平台上使用 EAGL 提供本地平台对 OpenGL 的实现,在 Android 平台上使用 EGL 提供本地平台对 OpenGL 的实现。 47 | 48 | EGL 是 OpenGL ES 和 Android 底层平台视窗系统之间的接口,在 OpenGL 的输出与设备屏幕之间架接起一个桥梁,承担了为 OpenGL 提供上下文环境以及管理窗口的职责。 49 | 50 | EGL 为双缓冲工作模式,即有一个 Back Frame Buffer 和一个 Front Frame Buffer,正常绘制的目标都是 Back Frame Buffer,绘制完成后再调用 eglSwapBuffer API,将绘制完毕的 FrameBuffer 交换到 Front Frame Buffer 并显示出来。 51 | 52 | 从代码层面来看,OpenGL ES 的 opengles 包下定义了平台无关的绘图指令,EGL(javax.microedition.khronos.egl) 53 | 则定义了控制 displays,contexts 以及 surfaces 的统一的平台接口。 54 | 55 | - Display(EGLDisplay) 是对实际显示设备的抽象 56 | - Surface(EGLSurface)是对用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer、Stencil Buffer、Depth Buffer 57 | - Context(EGLContext)存储 OpenGL ES 绘图的一些状态信息 58 | 59 | ![](/img/1.jpeg) 60 | 61 | *使用 EGL 绘图的一般步骤:* 62 | 63 | 1. 获取 EGLDisplay 对象 64 | 2. 初始化与 EGLDisplay 之间的连接 65 | 3. 获取 EGLConfig 对象 66 | 4. 创建 EGLContext 实例 67 | 5. 创建 EGLSurface 实例 68 | 6. 连接 EGLContext 和 EGLSurface 69 | 7. 使用 GL 指令绘制图形 70 | 8. 断开并释放与 EGLSurface 关联的 EGLContext 对象 71 | 9. 删除 EGLSurface 对象 72 | 10. 删除 EGLContext 对象 73 | 11. 终止与 EGLDisplay 之间的连接 74 | 75 | 76 | 一般来说在 Android 平台上开发 OpenGL ES 应用,无需按照上述步骤来绘制图形,可以直接使用 GLSurfaceView 控件,该控件提供了对 Display、Surface 以及 Context 的管理,大大简化了开发流程。 77 | 78 | #### OpenGL 纹理 79 | 80 | 纹理(Texture)是一个 2D 图片(甚至也有 1D 和 3D 的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的 3D 的房子上,这样你的房子看起来就像有砖墙外表了。 81 | 82 | 因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。 83 | 84 | ## OpenGL 坐标系理解 85 | 86 | OpenGL 要求输入的顶点坐标都是标准化设备坐标,即每个顶点的 x、y、z 都在 -1 到 1 之间,由标准化设备坐标转换为屏幕坐标的过程中会经历变换多个坐标系统,在这些特定的坐标系中,一些操作和计算可以更加方便。 87 | 88 | ![](/img/15.jpg) 89 | 90 | #### 局部坐标 91 | 顶点坐标起始于局部空间(Local Space),在这里称为局部坐标,是以物体某一点为原点而建立的,该坐标系仅对该物体适用,用来简化对物体各部分坐标的描述。物体放到场景中时,各部分经历的坐标变换相同,相对位置不变。 92 | 93 | #### 世界坐标 94 | 局部坐标通过模型矩阵进行位移、缩放、旋转,将物体从局部变换到世界空间,并和其他物体一起相对于世界的原点摆放。 95 | 96 | #### 观察坐标 97 | 将世界空间坐标转化为用户视野前方的坐标,通常是由一系列的位移和旋转的组合(观察矩阵)来完成。 98 | 99 | #### 裁剪坐标 100 | 坐标到达观察空间之后,通过投影矩阵会将指定范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0),所有在范围外的坐标会被裁剪掉。 101 | 102 | #### 屏幕坐标 103 | 将裁剪坐标位于(-1.0, 1.0)范围的坐标变换到由 glViewport 函数所定义的坐标范围内,最后变换出来的坐标将会送到光栅器,将其转化为片段。 104 | 105 | 106 | ## OpenGL 渲染管线 107 | OpenGL 渲染管线流程为:顶点数据 -> 顶点着色器 -> 图元装配 -> 几何着色器 -> 光栅化 -> 片段着色器 -> 逐片段处理 -> 帧缓冲 108 | 109 | ![](/img/16.jpg) 110 | 111 | OpenGL 渲染管线的流程其实就是 OpenGL 引擎渲染图像的流程,也就是说 OpenGL 引擎一步一步的将图片渲染到屏幕上的过程,渲染管线可以分为以下几个阶段: 112 | 113 | #### 1.指定几何对象 114 | 首先要了解几何图元的概念,几何图元就是点、直线、三角线等几何对象,在提供了顶点坐标后,还要确定具体要画的是点、线段还是三角形,这就要确定具体执行的绘制指令。比如 OpenGL 提供给开发者的绘制方法 glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有: 115 | 116 | **GL_POINTS**:以点的形式进行绘制,通常用在绘制粒子效果的场景。 117 | **GL_LINES**:以线的形式进行绘制,通常用于绘制直线的场景。 118 | **GL_TRIANGLE_STRIP**:以三角形的形式进行绘制,所有二维图像的渲染都会使用这种方式。 119 | 120 | 具体选用哪一种绘制方式决定了 OpenGL 渲染管线的第一阶段应如何去绘制几何图元,这就是第一阶段指定几何对象。 121 | 122 | #### 2.顶点处理 123 | 不论上面的几何图元是如何指定的,所有的几何数据都将会通过这个阶段。这个阶段的操作内容有:根据模型视图(即根据几何图元创建的物体)和投影矩阵进行变换来改变顶点的位置,根据纹理坐标与纹理矩阵来改变纹理坐标的位置,如果设计三维的渲染,还要处理光照计算和法线变换。 124 | 125 | 关键的操作就是顶点坐标变换及光照处理,每个顶点是分别单独处理的。这个阶段所接受的数据是每个顶点的属性特征,输出的则是变换后的顶点数据。 126 | 127 | #### 3.图元组装 128 | 在顶点处理之后,顶点的全部属性都已经被确定。在这个阶段顶点将会根据应用程序设定的图元规则如 GL_POINTS 、GL_TRIANGLES(三角形) 等被组装成图元。 129 | 130 | #### 4.珊格化操作 131 | 在图元组装后会传递过来图元数据,到目前为止,这些图元信息还只是顶点而已:顶点处都还没有“像素点”、直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。 132 | 133 | 这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元称为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 134 | 135 | 片元的属性则是图元上的顶点数据等经过插值而确定的,这就是珊格化操作,也就是确定好每一个片元是什么。 136 | 137 | #### 5.片元处理 138 | 珊格化操作构造了像素点,这个阶段就是处理这些像素点,根据自己的业务处理(比如提亮、饱和度调节、对比度调节、高斯模糊等)来变换这个片元的颜色。 139 | 140 | #### 6.逐片段处理 141 | 进行剪切、Alpha 测试、 模版测试、深度测试、混合等处理,这些操作将会最后影响其在帧缓冲区的颜色值。 142 | 143 | #### 7.帧缓冲操作 144 | 此阶段主要执行帧缓冲的写入操作,也是渲染管线的最后一步,负责将最终的像素点写到帧缓冲区。 145 | 146 | 上面提到 OpenGL ES 2.0 版本相比之前版本,提供了可编程的着色器来代替 1.x 版本渲染管线的某些阶段,具体为: 147 | 148 | - Vertex Shader(顶点着色器)用于替换顶点处理阶段 149 | - Fragment Shader(片元着色器)用于替换片元处理阶段 150 | 151 | ## OpenGL 着色语言 152 | OpenGL 着色语言 GLSL 全称为 OpenGL Shading Language,是为了实现着色器的功能而向开发人员提供的一种开发语言,语法与 C 语言类似,下面分为以下几点来学习 GLSL: 153 | 154 | #### 1.基本数据类型 155 | 156 | - void:空类型,即不返回任何值 157 | - bool:布尔类型,true/false 158 | - int:带符号的整数,signed integer 159 | - float:带符号的浮点数,signed scalar 160 | - vec2、vec3、vec4:n-维浮点数向量 161 | - bvec2、bvec3、bvec4:n-维布尔向量 162 | - ivec2、ivec3、ivec4:n-维整数向量 163 | - mat2、mat3、mat4:2x2、3x3、4x4 浮点数矩阵 164 | - sampler2D:2D 纹理 165 | - samplerCube:盒纹理 166 | 167 | *其中 float 可指定精度:* 168 | 169 | - high:32bit,一般用于顶点坐标 170 | - medium:16bit,一般用于纹理坐标 171 | - low:8bit,一般用于颜色表示 172 | 173 | #### 2.变量修饰符 174 | 175 | - none:(默认的可省略)本地变量,可读可写,函数的输入参数既是这种类型 176 | - const:声明变量或函数的参数为只读类型 177 | - attribute:用于保存顶点或法线数据,它可以在数据缓冲区中读取数据,仅能用于顶点着色器 178 | - uniform:在运行时 shader 无法改变 uniform 变量,一般用来放置程序传递给 shader 的变换矩阵,材质,光照参数等等,可用于顶点着色器和片元着色器 179 | - varying:用于修饰从顶点着色器向片元着色器传递的变量 180 | 181 | 182 | 要注意全局变量限制符只能为 const、attribute、uniform 和 varying 中的某一个,不可复合。 183 | 184 | #### 3.内置变量 185 | 186 | GLSL 程序使用一些特殊的内置变量与硬件进行沟通,他们大致分成两种,一种是 input 类型,他负责向硬件(渲染管线)发送数据;另一种是 output 类型,负责向程序回传数据,以便编程时需要。 187 | 188 | *顶点着色器中 output 类型的内置变量如下:* 189 | - highp vec4 gl_Position:放置顶点坐标信息 190 | - mediump float gl_PointSize:需要绘制点的大小,(只在gl.POINTS模式下有效) 191 | 192 | *片元着色器中 input 类型的内置变量如下:* 193 | - mediump vec4 gl_FragCoord;:片元在 framebuffer 画面的相对位置 194 | - bool gl_FrontFacing:标志当前图元是不是正面图元的一部分 195 | - mediump vec2 gl_PointCoord:经过插值计算后的纹理坐标,点的范围是0.0到1.0 196 | 197 | *片元着色器中 output 类型的内置变量如下:* 198 | - mediump vec4 gl_FragColor:设置当前片点的颜色 199 | - mediump vec4 gl_FragData[n]:设置当前片点的颜色,使用glDrawBuffers数据数组 200 | 201 | #### 4.内置常量 202 | 203 | GLSL 提供了一些内置的常量,用来说明当前系统的一些特性。有时我们需要针对这些特性,对 shader 程序进行优化,让程序兼容度更好。 204 | 205 | *顶点着色器中的内置常量如下:* 206 | - const mediump int gl_MaxVertexAttribs >= 8:顶点着色器中可用的最大 attributes 数 207 | - const mediump int gl_MaxVertexUniformVectors >= 128:顶点着色器中可用的最大 uniform vectors 数 208 | - const mediump int gl_MaxVaryingVectors >= 8:顶点着色器中可用的最大 varying vectors 数 209 | - const mediump int gl_MaxVertexTextureImageUnits >= 0:顶点着色器中可用的最大纹理单元数 210 | - const mediump int gl_MaxCombinedTextureImageUnits >= 8:表示最多支持多少个纹理单元 211 | 212 | *片元着色器中的内置常量如下:* 213 | - const mediump int gl_MaxTextureImageUnits >= 8:片元着色器中能访问的最大纹理单元数 214 | - const mediump int gl_MaxFragmentUniformVectors >= 16:片元着色器中可用的最大 uniform vectors 数 215 | - const mediump int gl_MaxDrawBuffers = 1:表示可用的 drawBuffers 数,在 OpenGL ES 2.0 中这个值为 1, 在将来的版本可能会有所变化 216 | 217 | 上面这些值的大小取决于 OpenGL ES 在某设备上的具体实现。 218 | 219 | #### 5.内置函数 220 | 221 | - 通用函数:abs、floor、min、max 等,参数可传入 float/vec2/vec3/vec4 类型 222 | - 角度函数:sin、cos 等,参数可传入 float/vec2/vec3/vec4 类型 223 | - 指数函数:pow、log 等,参数可传入 float/vec2/vec3/vec4 类型 224 | - 几何函数:distance、dot 等,参数可传入 float/vec2/vec3/vec4 类型 225 | - 矩阵函数:matrixCompMult,参数传入 mat 类型 226 | - 向量函数:lessThan、equal 等,参数可传入 vec2/vec3/vec4 类型 227 | - 纹理函数:texture2D、texture2DProj 等 228 | 229 | 230 | -------------------------------------------------------------------------------- /Android 方法插桩 plugin 开发实践.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 3 | 在做应用启动速度优化时,需先了解启动阶段做了哪些耗时任务,分析 Application 的 attachBaseContext、onCreate 等关键方法,统计它们内部调用到的其他方法耗时。 4 | 5 | 分析要结合 systrace 工具,因为不仅要知道方法的 wall time,还要知道 cpu time,这样才能知道是否属于 cpu 密集型任务,然后针对任务类型进行调整或线程调度。 6 | 7 | 需求很清晰,在要统计的方法调用前插桩加入 TraceCompat.beginSection(),调用后加入 TraceCompat.endSection()。需求也很简单,我们可以很快的使用 aspect、javassist 或 asm 实现。 8 | 9 | 但是,这次是方法插桩 + systrace,需为此开发一个插件;下次是方法插桩 + 耗时统计,就得再开发一个插件。为什么插件一定要与插桩逻辑绑定呢? 10 | 11 | **为什么没有一款插件,只提供方法插桩能力,不写死插桩逻辑,而是由使用者自由的定制插桩逻辑呢?** 12 | 13 | 基于这个痛点,我们来开发一款可自由定制插桩逻辑的插件。 14 | 15 | ## AOP 方案选择 16 | 17 | 首先是 AOP 技术方案的选择,aspect、javassist 还是 asm ? 在思考了一秒钟后,我决定选择 asm,理由很简单:性能高、逼格高。 18 | 19 | 那要基于 gradle 及 asm 原生 API,从零开发吗? NO,有些轮子不能造,我们要当坐在马车上驰骋的人! 20 | 21 | 所以我最终选择基于 ByteX 开发。 22 | 23 | ByteX 与 Jetpack StartUp 有异曲同工之妙。 24 | 25 | Startup 针对多个三方库各自使用 ContentProvider 初始化导致拖慢启动速度的问题,提供了一个 ContentProvider 来集中运行所有依赖项的初始化;ByteX 针对多个功能插件各自进行 transform 导致拖慢编译速度的问题,提供了一个宿主插件 transform,集中处理所有的 transform。 26 | 27 | ByteX 对 Transform 及 ASM 相关 API 做了封装,大大节省了插件开发的工作量,我们无需处理 class/jar 的 IO 操作,只需关注想要进行的 hook 逻辑即可。 28 | 29 | 所以,通过性能及开发成本两个维度的考量,基于 ByteX 开发一些有意义的插件,是一个不错的选择。 30 | 31 | ## trace-plugin 插件 32 | 33 | 成果先行,目前 trace-plugin 已开发完并发布,见:https://github.com/yhaolpz/ByteXPlugin/tree/master/trace,可查看插件源码及接入方式。 34 | 35 | 使用姿势很简单,仅 @TraceClass、@TraceMethod 两个注解而已: 36 | 37 | #### @TraceClass 为类注解,可配置: 38 | 39 | ```java 40 | //指定方法插桩实现类 41 | Class methodTrace() default TimeTrace.class; 42 | //是否要追踪此类中所有方法 43 | boolean traceAllMethod() default false; 44 | //是否要追踪方法内部调用到的方法 45 | boolean traceInnerMethod() default false; 46 | ``` 47 | 48 | #### @TraceMethod 为方法注解,可配置: 49 | 50 | ```java 51 | //是否要追踪此方法 52 | boolean trace() default true; 53 | //是否要追踪方法内部调用到的方法 54 | boolean traceInnerMethod() default false; 55 | ``` 56 | 57 | **举个例子** 58 | 59 | Test 类中有 m1()、m2()、m3() 三个方法: 60 | 61 | ```java 62 | public class Test{ 63 | public static void m1() { 64 | m2(); 65 | OtherClass.m4(); 66 | } 67 | public static void m2() { 68 | } 69 | public static void m3() { 70 | } 71 | } 72 | ``` 73 | 74 | *追踪 m1() 耗时:* 75 | 76 | ```java 77 | @TraceClass 78 | public class Test{ 79 | @TraceMethod 80 | public static void m1() {... 81 | ``` 82 | 83 | *追踪类中所有方法耗时:* 84 | 85 | ```java 86 | @TraceClass(traceAllMethod = true) 87 | public class Test{... 88 | ``` 89 | 90 | *追踪类中所有方法耗时,但不包括 m1():* 91 | 92 | ```java 93 | @TraceClass(traceAllMethod = true) 94 | public class Test{ 95 | @TraceMethod(trace = false) 96 | public static void m1() {... 97 | ``` 98 | 99 | *追踪 m1() 方法内部调用到的方法,即 m2()、OtherClass.m4() 的耗时:* 100 | 101 | ```java 102 | @TraceClass 103 | public class Test{ 104 | @TraceMethod(trace = false,traceInnerMethod = true) 105 | public static void m1() { 106 | m2(); 107 | OtherClass.m4(); 108 | } 109 | ... 110 | ``` 111 | 112 | *自定义追踪插桩处理:* 113 | 114 | ```java 115 | //继承自 IMethodTrace 方法实现自己的插桩处理,例如 systrace 追踪处理: 116 | public class CustomSysTrace implements IMethodTrace { 117 | @Override 118 | public void onMethodEnter(String className, String methodName, String methodDesc, String outerMethod) { 119 | TraceCompat.beginSection(className + "#" + methodName); 120 | } 121 | @Override 122 | public void onMethodEnd(String className, String methodName, String methodDesc, String outerMethod) { 123 | TraceCompat.endSection(); 124 | } 125 | } 126 | 127 | //在类注解中指定即可 128 | @TraceClass(methodTrace = CustomSysTrace.class) 129 | public class Test{...} 130 | ``` 131 | 132 | ## 自定义插桩处理实现原理 133 | 134 | 其实非常简单,插件内部对于需要插桩的方法会统一调用到 TraceRecord 类进行处理: 135 | 136 | ```java 137 | public class TraceRecord { 138 | //插桩方法执行前会统一调到这里 139 | public static void onMethodEnter(String traceImplClass, 140 | String className, 141 | String methodName, String methodDesc, 142 | String outerMethod) { 143 | getMethodTrace(traceImplClass).onMethodEnter(className, 144 | methodName, methodDesc, outerMethod); 145 | } 146 | //插桩方法执行完后会统一调到这里 147 | public static void onMethodEnd(String traceImplClass, 148 | String className, 149 | String methodName, String methodDesc, 150 | String outerMethod) { 151 | getMethodTrace(traceImplClass).onMethodEnd(className, 152 | methodName, methodDesc, outerMethod); 153 | } 154 | } 155 | ``` 156 | 157 | traceImplClass 就是我们在类注解中指定的自定义插桩逻辑实现类,比如 CustomSysTrace ,然后在 getMethodTrace() 中实例化: 158 | 159 | ```java 160 | private static IMethodTrace getMethodTrace(String traceImplClass) { 161 | IMethodTrace methodTrace = sMethodTraceMap.get(traceImplClass); 162 | if (methodTrace == null) { 163 | try { 164 | methodTrace = (IMethodTrace) Class.forName(traceImplClass).newInstance(); 165 | } catch (Exception e) { 166 | e.printStackTrace(); 167 | } 168 | } 169 | sMethodTraceMap.put(traceImplClass, methodTrace); 170 | return methodTrace; 171 | } 172 | ``` 173 | 174 | ## 最后 175 | 176 | 有了简洁、优雅、易用、功能强大的 trace-plugin 插件,以后再也不怕方法插桩了,你想插什么就插什么。 -------------------------------------------------------------------------------- /Android 自定义 View | 剑气加载.md: -------------------------------------------------------------------------------- 1 | > 看到一个非常炫的加载效果,文章中大佬是通过 css 实现的,今天咱们来用 Android 的自定义 View 来实现一下 2 | > 3 | > https://juejin.cn/post/7001779766852321287 4 | 5 | #### 这是理想中的效果 6 | 7 | ![src=http___image.17173.com_bbs_v1_2012_12_01_1354372326576.gif&refer=http___image.17173.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c61adacf68a542b5889a532205d6a552~tplv-k3u1fbpfcp-watermark.awebp) 8 | 9 | #### 这是现实的效果 10 | 11 | ![circle 11](img/circle 11.gif) 12 | 13 | 不能说百分百还原吧,只能说精髓 14 | 15 | img 16 | 17 | ## 开工 18 | 19 | 这个效果仔细看,就是有三个类似月牙形状的元素进行循环转动,我们只需要拆解出一个月牙来做效果即可,最后再将三个月牙组合起来就可以达到最终效果了 20 | 21 | ## 月牙 22 | 23 | #### 先画一个圆 24 | 25 | image-20210921002044181 26 | 27 | #### 再画个大一丢丢的 28 | 29 | ![image-20210921002129703](img/image-20210921002129703.png) 30 | 31 | #### 再把这个大圆往右移一丢丢,裁切出来的左右两个都是月牙 32 | 33 | ![image-20210921002239797](img/image-20210921002239797.png) 34 | 35 | #### 实现 36 | 37 | 老司机应该一眼就能看出来,只要在两次绘制圆中只要使用一个叠加模式就能达到裁剪出一个月牙的效果了。那么是什么模式呢,我去搜索一下~ 38 | 39 | 当当当,就是它~ `PorterDuff.Mode.DST_OUT` 40 | 41 | ![image-20210921002857005](img/image-20210921002857005.png) 42 | 43 | 相关源码如下: 44 | 45 | ```kotlin 46 | canvas.drawColor(Color.BLACK) 47 | 48 | val layerId = 49 | canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG) 50 | val halfW = width / 2f 51 | val halfH = height / 2f 52 | val radius = min(width, height) / 3f 53 | 54 | paint.color = Color.WHITE 55 | canvas.drawCircle(halfW, halfH, radius, paint) 56 | paint.color = Color.BLACK 57 | paint.xfermode = xfermode 58 | canvas.drawCircle(halfW, halfH - 0.05f * radius, radius * 1.01f, paint) 59 | canvas.restoreToCount(layerId) 60 | paint.xfermode = null 61 | ``` 62 | 63 | #### 运行起来我们就得到了一弯浅浅的月牙 64 | 65 | ![image-20210921004428266](img/image-20210921004428266.png) 66 | 67 | ## 立体空间变化 68 | 69 | 我们可以看出效果图里的每一个月牙并不是那么方正,而是有一定的空间旋转,再加上绕着 Z 轴旋转。这里需要利用 Camera 与 Matrix 实现3D效果(相关知识可参考:https://www.jianshu.com/p/34e0fe5f9e31) 70 | 71 | #### 我们先给它在 x 轴转 35 度 ,y 轴转 -45 度(参考开头文章的数据) 72 | 73 | ```kotlin 74 | rotateMatrix.reset() 75 | camera.save() 76 | camera.rotateX(35F) 77 | camera.rotateY(-45F) 78 | camera.getMatrix(rotateMatrix) 79 | camera.restore() 80 | val halfW = width / 2f 81 | val halfH = height / 2f 82 | 83 | rotateMatrix.preTranslate(-halfW, -halfH) 84 | rotateMatrix.postTranslate(halfW, halfH) 85 | canvas.concat(rotateMatrix) 86 | ``` 87 | 88 | 运行效果如下,从普通的月牙变成了帅气的剑气 89 | 90 | ![image-20210921010008348](img/image-20210921010008348.png) 91 | 92 | 93 | 94 | ## 动画 95 | 96 | 我们上面做了固定角度的 X, Y 轴的旋转,这个时候我们只要加上一个 Z 轴的调转动画,这个剑气就动起来了。 97 | 98 | ```kotlin 99 | val anim = ValueAnimator.ofFloat(0f, -360f).apply { 100 | // Z 轴是逆时针,取负数,得到顺时针的旋转 101 | interpolator = null 102 | repeatCount = RotateAnimation.INFINITE 103 | duration = 1000 104 | 105 | addUpdateListener { 106 | invalidate() 107 | } 108 | } 109 | // 省略前面已写代码... 110 | camera.rotateZ(anim.animatedValue as Float) 111 | 112 | // 在合适的地方启动动画 113 | view.anim.start() 114 | ``` 115 | 116 | 运行效果: 117 | 118 | ![circle 12](img/circle 12.gif) 119 | 120 | ## 举一反三 121 | 122 | 有了这一个完整的剑气旋转,只要再来两道,组成完整的剑气加载就可以了。 123 | 124 | #### 将前面的代码抽象成一个方法 125 | 126 | ```KOTLIN 127 | private fun drawSword(canvas: Canvas, rotateX: Float, rotateY: Float) { 128 | val layerId = 129 | canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG) 130 | rotateMatrix.reset() 131 | camera.save() 132 | camera.rotateX(rotateX) 133 | camera.rotateY(rotateY) 134 | camera.rotateZ(anim.animatedValue as Float) 135 | camera.getMatrix(rotateMatrix) 136 | camera.restore() 137 | 138 | val halfW = width / 2f 139 | val halfH = height / 2f 140 | 141 | rotateMatrix.preTranslate(-halfW, -halfH) 142 | rotateMatrix.postTranslate(halfW, halfH) 143 | canvas.concat(rotateMatrix) 144 | canvas.drawCircle(halfW, halfH, radius, paint) 145 | paint.xfermode = xfermode 146 | canvas.drawCircle(halfW, halfH - 0.05f * radius, radius * 1.01f, paint) 147 | canvas.restoreToCount(layerId) 148 | paint.xfermode = null 149 | } 150 | ``` 151 | 152 | #### 绘制三道剑气 153 | 154 | ``` kotlin 155 | verride fun onDraw(canvas: Canvas) { 156 | super.onDraw(canvas) 157 | canvas.drawColor(Color.BLACK) 158 | // 偏移角度来源开关文章 159 | drawSword(canvas,35f, -45f) 160 | drawSword(canvas,50f, 10f) 161 | drawSword(canvas,35f, 55f) 162 | } 163 | ``` 164 | 165 | #### 跑起来看看 166 | 167 | ![circle 13](img/circle 13.gif) 168 | 169 | Emm... 这动画也太整齐划一了 170 | 171 | #### 错开三道剑气 172 | 173 | 在 Z 轴旋转上,我们给每道剑气一个初始值的旋转值(360/3 = 120),这样它们就能均匀的错开了。 174 | 175 | 相关实现如下: 176 | 177 | ```kotlin 178 | private fun drawSword(canvas: Canvas, rotateX: Float, rotateY: Float, startValue: Float) { 179 | //... 省略未改动代码 180 | camera.rotateZ(anim.animatedValue as Float + startValue) 181 | //... 省略未改动代码 182 | } 183 | 184 | override fun onDraw(canvas: Canvas) { 185 | super.onDraw(canvas) 186 | canvas.drawColor(Color.BLACK) 187 | drawSword(canvas,35f, -45f, 0f) 188 | drawSword(canvas,50f, 10f, 120f) 189 | drawSword(canvas,35f, 55f, 240f) 190 | } 191 | ``` 192 | 193 | #### 最终效果 194 | 195 | ![circle 14](img/circle 14.gif) 196 | 197 | 和我们开头预期的效果图一模一样 198 | 199 | 完整代码:https://github.com/samwangds/DemoFactory/blob/master/app/src/main/java/demo/com/sam/demofactory/view/SwordLoadingView.kt 200 | 201 | ## End 吹欺汀 202 | 203 | 值此中秋佳节,祝各位有知识又有头发~ 204 | 205 | img -------------------------------------------------------------------------------- /Application.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | 5 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 6 | 7 | **面试官**:说说 Application 的作用。 8 | 9 | 😎:Application 是应用进程创建后就会创建的系统组件,所以可以用它来做一些初始化操作;Application 生命周期和应用进程一样长,所以可以用来给类库提供 Context; 因为在所有 Context 可以获得 Application 所以可以用来保存和传递全局变量。 10 | 11 | **面试官**:你平常开发会把全局变量放在 Application ? 那应用在后台被回收,重新打开的时候值丢失怎么办? 12 | 13 | 😎:会啊,很方便, 做一下容错判空就可以了 14 | 15 | **面试官**:好的,回去等通知吧 16 | 17 | 18 | 19 | --- 20 | 21 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 22 | 23 | **面试官**:说说对 Application 的理解 24 | 25 | 😨:作用:做初始化操作、提供上下文。另外 Application 是一个 Context ,它直接继承了 ContextWrapper ;这个 ContextWrapper 的成员变量 mBase 可以用来存放系统实现的 ContextImpl,这样我们在调用 Application 的 Context 方法时,都是通过静态代理的方式最终调用到 ContextImpl 的方法。我们调用 ContextWrapper 的 getBaseContext 方法就能拿到 ContextImpl 的实例 26 | 27 | **面试官**:你平常开发会把全局变量放在 Application ? 那应用在后台被回收,重新打开的时候值丢失怎么办? 28 | 29 | 😨:不会,保存全局变量用静态变量,或单例可以把它们聚集在更合适的位置。 30 | 避免应用被回收数据丢失,可以页面传递参数时,通过 Intent 传递参数,这样被回收后打开重新从 Intent 取参还是有值的。数据量大的话也可以考虑数据持久化;另一个方法是通过 `onSaveInstanceState` 和 `onRestoreInstanceState` 分别在被回收时保存相应的数据以及在重新打开时恢复数据。 31 | 32 | **面试官**:讲一下 Application 的生命周期吧 33 | 34 | 😨:相比 Activity ,Application 的生命周期简直不要太简单。首先创建的时候会调用构造函数,然后系统准备好 ContextImpl 通过 `attachBaseContext( Context )` 方法注入到 Application,接着调用我们最熟悉的 onCreate 方法。API 里还有一个 `onTerminate` 方法在进程被杀死的时候会回调,不过仅在模拟器生效,就不需要关注了。 35 | 36 | **面试官**:那你能接着说一下 Application 的初始化流程吗? 37 | 38 | 😨:基本上就是上面说的那些,再细没有去了解了 39 | 40 | **面试官**:好的,回去等通知吧 41 | 42 | --- 43 | 44 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 45 | 46 | **面试官**:说一下 Application 的初始化流程 47 | 48 | 🤔️:Application 的初始化是在应用进程创建完成后: 49 | 50 | * ActivityThread 调用 AMS 的 Binder 对象( IActivityManager )的 attachApplication 方法 51 | * AMS 收到请求后再去调用 ActivityThread 的 bindApplication 方法 52 | * ActivityThread 这边收到请求再组装一个 AppBindData 对象,把所有参数封装进去,再通过 handler 发到主线程执行 53 | * 主线程 loop 到这条消息,调用 handleBindApplication 来真正处理初始化 Application 54 | 55 | handleBindApplication 和我们谈 “Context” 那次,Activity 的初始化差不多。回顾一下: 56 | 57 | * ClassLoader 加载 Application 类,实例化 58 | * 初始化 Applicaction 用的 ContextImpl 59 | * 通过 Application.attach( Context ) 方法,调用 `attachBaseContext( Context )` 将 ContextImpl 注入到 Application 60 | * 最后调用 Application.OnCreate() 61 | 62 | 这样 Application 就初始化完成了 63 | 64 | **面试官**:为什么进程创建完成不直接调 handleBindApplication 去创建 Application 呢,又去 AMS 那边绕了一圈 65 | 66 | 🤔️:调用 AMS 的 attachApplication 不仅仅是为了创建 Application ,还有在进程创建前可能调用了应用的四大组件却没办法启动;现在进程创建好了,创建好 Application 也要处理这些待启动的组件。所以需要通过 AMS 统一调度,如果 Application 的创建及 onCreate 回调耗时的话,也会影响这些待启动组件的启动时间 67 | 68 | **面试官**:可以,我们再来聊聊别的。 69 | 70 | --- 71 | 72 | > 看完了这三位同学的面试表现,你有什么感想呢?欢迎关注 “Android 面试官” 在公众号后台留言讨论 73 | 74 | ![img](img/qrcode.jpg) 75 | 76 |
听说🤔️同学也关注我了哦
77 | 78 | -------------------------------------------------------------------------------- /Builder 模式.md: -------------------------------------------------------------------------------- 1 | > Builder 模式是一步一步创建一个复杂对象的创建型模式,可以更精细 的控制对象的构造流程。该模式是为了将构建复杂对象的过程和它的部件解耦,使得构建过程和部件的表示隔离开来。 2 | 3 | 一个复杂的对象会有很多的组成部分,如汽车,有车轮、方向盘、发动机、还有各种小部件等,如何将这些部件装配成一辆汽车,这个装配过程很复杂,对于这种情况,为了在构建过程中隐藏实现细节,就可以使用 Builder 模式将部件和组装过程分离,使得构建过程和部件都可以自由扩展,两者之间的耦合也降到最底。 4 | 5 | ## 定义 6 | 7 | 将一个复杂对象的构建过程与表示分离,使得同样的构建过程可以创建出该对象的不同表示 8 | 9 | ## 使用场景 10 | 11 | - 相同的方法,不同的执行顺序,产生不同的事件结果时 12 | - 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时 13 | - 产品类非常复杂,或产品顺中的调用顺序不同产生了不同的作用时 14 | - 初化一个对象特别复杂,如参数多且很多参数具有默认值时 15 | 16 | ## UML 17 | 18 | ![builder](img/builder.jpg) 19 | 20 | - Product: 产品类,产品的抽象类 21 | - Builder: 抽象 Builder 类,规范产品的组建 22 | - ConcreteBuilder: 具体的 Builder 类 23 | - Director: 统一组装过程 24 | 25 | ## Demo 26 | 27 | 做为程序猿,在老家就等于是一个修电脑的,电脑的组装过程比较复杂,并且组装的顺序是不固定的。我们就把这个过程简化为构建主机,安装操作系统,连接显示器这 3 个部分,然后通过 Director 和具体的 Buidler 来构建计算机对象。 28 | 29 | 先写一个计算机类,即 Product 角色 30 | 31 | ```KOTLIN 32 | abstract class Computer { 33 | var mainBoard: String? = null // 主板 34 | var display: String? = null // 显示器 35 | var OS: String? = null // 操作系统,null 时未安装操作系统 36 | 37 | //安装操作系统 38 | abstract fun installOS() 39 | 40 | override fun toString(): String { 41 | return "Computer info: (mainBoard=$mainBoard, display=$display, OS=$OS)" 42 | } 43 | } 44 | 45 | class MacBook : Computer() { 46 | override fun installOS() { 47 | OS = "macOS Big Sur 11.0.1" 48 | } 49 | } 50 | ``` 51 | 52 | 再写相关的 Builder 类 53 | 54 | ```kotlin 55 | abstract class Builder { 56 | // 安装主板 57 | abstract fun setupMainBoard(mainBoard: String): Builder 58 | // 连接显示器 59 | abstract fun connectDisplay(display: String): Builder 60 | // 安装操作系统 61 | abstract fun installOS(): Builder 62 | // 创建一台装好的电脑 63 | abstract fun create(): Computer 64 | } 65 | 66 | class MacBookBuilder : Builder() { 67 | val computer = MacBook() 68 | 69 | override fun setupMainBoard(mainBoard: String): Builder { 70 | computer.mainBoard = mainBoard 71 | return this 72 | } 73 | 74 | override fun connectDisplay(display: String): Builder { 75 | computer.display = display 76 | return this 77 | } 78 | 79 | override fun installOS(): Builder { 80 | computer.installOS() 81 | return this 82 | } 83 | 84 | override fun create(): Computer { 85 | return computer 86 | } 87 | } 88 | ``` 89 | 90 | 接下来是我们不太熟悉的 Director 类,负责构造 Computer 91 | 92 | ```kotlin 93 | class Director(val builder: Builder) { 94 | fun constructMackBook(mainBoard: String, display: String) { 95 | builder.setupMainBoard(mainBoard) 96 | .connectDisplay(display) 97 | .installOS() 98 | } 99 | } 100 | ``` 101 | 102 | 最后就是测试代码和运行结果啦~ 103 | 104 | ```kotlin 105 | fun main() { 106 | val builder = MacBookBuilder() 107 | val director = Director(builder) 108 | // 来个 M1 + 4K 的 MackBook 一定很爽 109 | director.constructMackBook("苹果主板 M1 芯片版", "4K 显示器") 110 | // 最后打印一下组件好的电脑信息 111 | print(builder.create()) 112 | } 113 | 114 | // ------ 运行结果 ------ 115 | Computer info: (mainBoard=苹果主板 M1 芯片版, display=4K 显示器, OS=macOS Big Sur 11.0.1) 116 | ``` 117 | 118 | 在这个 Demo 中,通过具体的 MacBookBuilder 来构建 MacBook 对象,而 Director 封装了构建 复杂对象的过程,对外隐藏细节 。Builder 和 Director 一起将一个复杂对象的构建与它的表示分离,使得同样的构建 肥屁股退款可以创建不同的对象。 119 | 120 | 不过,在实际开发过程中,为了方便运用,Director、抽象的 Builder 经常会被省略,直接一个 Builder 清清爽爽: 121 | 122 | ```kotlin 123 | MacBookBuilder().setupMainBoard(mainBoard) 124 | .connectDisplay(display) 125 | .installOS() 126 | .create() 127 | ``` 128 | 129 | ## Android 源码中的应用 130 | 131 | 在 Android 源码中,最常用到的 Builder 模式就是 AlertDialog.Builder,可以使用该 Builder 来构建复杂的 AlertDialog 对象: 132 | 133 | ```kotlin 134 | AlertDialog.Builder(context) 135 | .setIcon(R.drawable.video_edit_icon_compare_bg) 136 | .setTitle("Title") 137 | .setMessage("Message") 138 | .setPositiveButton("OK", dialogOnClickListener) 139 | .setNegativeButton("Cancel", dialogOnClickListener) 140 | .create() 141 | .show() 142 | ``` 143 | 144 | 通过 Builder 对象来组装 Dialog 的各个部分,如 Title, Buttons, Message 等,将 Dialog 的构造和表示进行分离。 145 | 146 | AlertDialog.Builder 同时扮演了前文提到的 Builder, ConcreteBuilder, Director 的角色,简化了 Builder 模式的设计。当模块比较稳定时可以精简,不必照搬 GOF 上的经典实现,更灵活的运用设计模式,这一点上 Android 的源码很值得我们去学习。 147 | 148 | ## Kotlin 的默认参数 149 | 150 | 我们在说应用场景时,有这样一个场景:初化一个对象特别复杂,如参数多且很多参数具有默认值时。使用 Kotlin 的默认参数,能更方便的实现,少写很多代码。 151 | 152 | ```KOTLIN 153 | // 带有默认参数的构造函数 154 | class Computer ( 155 | var mainBoard: String = "苹果主板", // 自家定制的主板 156 | var display: String? = "2K Retina",// 显示器 2K+ 157 | var OS: String = "MacOS" // 自带 Mac 158 | ) 159 | 160 | // 可以在构造函数里,传任意 0-N 个参数 161 | fun main() { 162 | Computer( 163 | mainBoard = "Intel 主板", 164 | OS = "Windows" 165 | ) 166 | } 167 | ``` 168 | 169 | ## 小结 170 | 171 | Builder 模式在 Android 开发中较为常用,通常作为配置类的构建 器将配置的构建和表示分离开来,同时也将配置从目标类中隔离开来,避免过多的 setter 方法。Builder 模式常见的实现开工是通过调用链实现,这样使得代码更简洁,易懂。 172 | 173 | #### 优点 174 | 175 | - 良好的封装,使客户端不必知道产品内部组成的细节 176 | - Builder 独立,容易扩展 177 | 178 | #### 缺点 179 | 180 | - 额外产生的类,在创建产品对象时额外消费内存 -------------------------------------------------------------------------------- /DispatchTouchEvent.md: -------------------------------------------------------------------------------- 1 | # 必问的事件分发,你答得上来吗 2 | 3 | Android touch 事件的分发,是面试中最常被问到的问题之一。我们来看看 😎、😨 和 🤔️ 三位同学是怎么回答的吧 4 | 5 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 6 | 7 | **面试官**:讲讲 Android 的事件分发机制 8 | 9 | 😎:当用户手指触摸屏幕时,Android 会将对应的事件包装成一个事件对象 MotionEvent 从 ViewTree 的顶部至上而下地分发传递。用户从手指接触屏幕至离开屏幕会产生一系列的事件,事件是以 down 开始,up 或 cancel 结束,中间无数个 move ; **一个事件的分发顺序是:Activity 到 ViewGroup 再到 View** 10 | 11 | **面试官**:事件分发的过程用到哪些方法 12 | 13 | 😎:首先是 dispatchTouchEvent 执行事件分发的方法,整个事件分发的过程就是在递归这个方法; 14 | 15 | 然后就是 onTouchEvent 消费方法,View 响应事件、ScrollView 响应滚动事件就是在这里面实现 16 | 17 | **面试官**:还有一个拦截方法呢? 18 | 19 | 😎:什么拦截方法,分发关拦截什么事?(糟糕背的答案忘了) 20 | 21 | **面试官**:哦,没事,回去等通知吧。 22 | 23 | 24 | 25 | --- 26 | 27 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 28 | 29 | **面试官**:事件分发的过程用到哪些方法 30 | 31 | 😨:有 dispatchTouchEvent 、onTouchEvent 、 onInterceptTouchEvent ;ViewGroup 在调用 dispatchTouchEvent 进行事件分发时,会调用 `onInterceptTouchEvent` ,来判断是否能拦截这个事件。相应如果不想 ViewGroup 拦截事件,可以调用 `ViewGroup.requestDisallowInterceptTouchEvent` 方法,传 true 就是别拦给我传,false 你开心就拦吧;常用来解决一些嵌套 View 的事件冲突。 32 | 33 | **面试官**:说一下这些方法的关系 34 | 35 | 😨:比如 ScrollView 用户手指点击下去时,Down 事件会被子 View 消费,这样如果紧接着用户手指直接抬起那这个子 View 就消费这个完整的事件序列,一般是点击事件;而如果接下去用户的手指进行滑动产生 Move事件,那就必须要由 ScrollView 来响应滚动事件了,为了能达到这个效果 ScrollView 在 dispatchTouchEvent( Move ) 时,调用 onInterceptTouchEvent 返回了 true 来实现拦截事件,不再向子 View 分发。 36 | 37 | 看一下伪代码 38 | ``` 39 | // 事件分发到某个具体的 ViewGroup,会直接调用 dispatchTouchEvent() 方法 40 | public boolean dispatchTouchEvent(MotionEvent ev) { 41 | //代表是否消费事件 42 | boolean consume = false; 43 | 44 | if (onInterceptTouchEvent(ev)) { 45 | // 如果 onInterceptTouchEvent() 返回 true 则代表当前 View 拦截了事件 46 | // 则该事件则会交给当前View进行处理 47 | // 即调用 onTouchEvent() 方法去处理事件 48 | consume = onTouchEvent (ev) ; 49 | } else { 50 | // 如果 onInterceptTouchEvent() 返回 false 则代表当前 View 不拦截事件 51 | // 则该事件则会继续传递给它的子元素 52 | // 子元素的 dispatchTouchEvent() 就会被调用,重复上述过程 53 | // 直到事件被最终处理为止 54 | consume = child.dispatchTouchEvent(ev); //遍历处理 55 | } 56 | return consume; 57 | } 58 | ``` 59 | 60 | **面试官**:你这伪代码虽然通俗易懂,但是省略了太多逻辑了,子 View 在消费掉 Down 事件后,后续的事件都给会传递给它,你知道是怎么实现的吗 61 | 62 | 😨:具体怎么实现没关注 63 | 64 | **面试官**:好的,回去等通知吧。 65 | 66 | 67 | 68 | --- 69 | 70 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 71 | 72 | **面试官**:讲讲 Android 的事件分发机制 73 | 74 | 🤔️:说起来太费劲了,上神图,放大了认真看: 75 | 76 | ![事件分发图](img/事件分发图.png) 77 | 78 | **面试官**:子 View 在消费掉 Down 事件后,后续的事件都给会传递给它,你知道是怎么实现的吗 79 | 80 | 🤔️:ViewGroup 里面用了一个成员变量 mFirstTouchTarget 来保存消费事件的子 View 信息,因为安卓是支持多指操作的,所以这个 mFirstTouchTarget 是一个 TouchTarget 的链表。在View 的 dispatchTouchEvent 可以分为三个阶段:判断是否需要拦截; 分发事件找到消费事件的子 View,更新到 mFirstTouchTarget;根据是否拦截和 mFirstTouchTarget 再次分发事件。 81 | 82 | 再细节我们就要到源码里看实现了,以下为 API 28 ViewGroup 的 dispatchTouchEvent 部分源码: 83 | 84 | 1. 判断是否需要拦截 85 | 86 | ``` 87 | final boolean intercepted; 88 | if (actionMasked == MotionEvent.ACTION_DOWN 89 | || mFirstTouchTarget != null) { 90 | // disallowIntercept 就是 requestDisallowInterceptTouchEvent 设置的 91 | // 根据 disallowIntercept 和 onInterceptTouchEvent 决定intercepted 92 | final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 93 | if (!disallowIntercept) { 94 | intercepted = onInterceptTouchEvent(ev); 95 | ev.setAction(action); // restore action in case it was changed 96 | } else { 97 | intercepted = false; 98 | } 99 | } else { 100 | // 不是 Down 事件 并且之前的事件没有被子 View 捕获,就可以直接拦截 101 | intercepted = true; 102 | } 103 | ``` 104 | 105 | 2. 分发事件找到消费事件的子 View 106 | 107 | ``` 108 | if (!canceled && !intercepted) { 109 | if (actionMasked == MotionEvent.ACTION_DOWN || ...) { 110 | // 只分发 Down 事件(省略的为多指或鼠标的情况) 111 | for (int i = childrenCount - 1; i >= 0; i--) { 112 | ... 113 | //调用 dispatchTransformedTouchEvent 方法将事件分发给子 View 114 | if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { 115 | ... 116 | // 如果事件被子 View 消费,更新 mFirstTouchTarget 117 | newTouchTarget = addTouchTarget(child, idBitsToAssign); 118 | break; 119 | } 120 | ... 121 | } 122 | } 123 | } 124 | ``` 125 | 3. 根据拦截结果和 mFirstTouchTarget 再次分发事件。 126 | ``` 127 | if (mFirstTouchTarget == null) { 128 | // 没有子 View 消费事件,则传入 null 去分发,最终调用的是自身的 onTouchEvent 方法,进行处理 touch 事件 129 | handled = dispatchTransformedTouchEvent(ev, canceled, null, 130 | TouchTarget.ALL_POINTER_IDS); 131 | } else { 132 | TouchTarget predecessor = null; 133 | TouchTarget target = mFirstTouchTarget; 134 | while (target != null) { 135 | final TouchTarget next = target.next; 136 | if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { 137 | handled = true; //已经处理了的避免重复分发 138 | } else { 139 | //如果 intercepted 就取消 cancelChild,这便是拦截子 View 事件的原理 140 | final boolean cancelChild = resetCancelNextUpFlag(target.child) 141 | || intercepted; 142 | if (dispatchTransformedTouchEvent(ev, cancelChild, 143 | target.child, target.pointerIdBits)) { 144 | //内部会比较 pointerIdBits 和当前事件的 pointerIdBits,一致才会处理 145 | //这便是 Down 事件处理后后续事件都交给该 View 处理的原理 146 | handled = true; 147 | } 148 | } 149 | ... 150 | } 151 | } 152 | ``` 153 | 好了,终于说完了 154 | 155 | **面试官**:太多了,能总结下吗? 156 | 157 | 🤔️:好吧,我们来复习一下: 158 | 159 | * 判断是否需要拦截 —> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截; 160 | * 在 DOWN 事件中将 touch 事件分发给子 View —> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值; 161 | * 最后一步,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为 null,决定是自己处理 touch 事件,还是再次分发给子 View。 162 | * DOWN 事件是事件序列的起点;决定后续事件由谁来消费处理; 163 | * mFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构; 164 | * CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。 165 | * 如果一个事件最后所有的 View 都不处理的话,最终回到 Activity 的 onTouchEvent 方法里面来。 166 | 167 | **面试官**:可以,我们再来聊聊别的。 168 | 169 | ---- 170 | 171 | > 看完了这三位同学的面试表现,你有什么感想呢?欢迎关注 “Android 面试官” 在公众号后台留言讨论,另外今天留一些简单的思考题,如果答不出来的建议收藏文章多看几遍,或者在公众号后台回复“事件分发思考题”。 172 | > 173 | > * 如果一个事件序列的 ACTION_DOWN 事件被 ViewGroup 拦截,此时子 View 调用 requestDisallowInterceptTouchEvent 方法有没有用? 174 | >* ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗? 175 | > * 当 View Disable 时,会消费事件吗? 176 | > -------------------------------------------------------------------------------- /GUI 系统综述.md: -------------------------------------------------------------------------------- 1 | GUI(Graphical User Interface)即图形用户界面,官方架构图如下: 2 | 3 | ![](/img/ape_fwk_graphics.png) 4 | 5 | 下面分别介绍上图中的几个重要角色: 6 | 7 | #### IMAGE STREAM PRODUCERS 图像流生产方 8 | 9 | 生成图形缓冲区以供消耗的任何内容,例如 OpenGL ES、Canvas 2D 和 mediaserver 视频解码器都是图像流生产方。 10 | 11 | #### IMAGE STREAM CONSUMERS 图像流消耗方 12 | 13 | 图像流最常见的消耗方是 SurfaceFlinger,该系统服务接收来自于多个源的数据缓冲区,组合它们,并将它们发送给显示设备。 14 | 15 | 除了 SurfaceFlinger,OpenGL ES 应用也可以消耗图像流,例如相机应用会消耗相机预览图像流,另外非 GL 应用也可以消耗图像流,例如 ImageReader 类。 16 | 17 | SurfaceFlinger 使用 OpenGL 和 Hardware Composer 来合成一组 Surface。 18 | 19 | #### WindowManager 20 | 21 | WindowManager 会控制 window 对象,window 是用于容纳视图对象的容器。 22 | 23 | window 对象由 Surface 对象提供支持。WindowManager 会监督生命周期、输入和聚焦事件、屏幕方向、转换、动画、位置、变形、Z 轴顺序等窗口事件。 24 | 25 | WindowManager 会将所有窗口元数据发送到 SurfaceFlinger,以便 SurfaceFlinger 可以使用这些数据合成 Surface。 26 | 27 | #### Surface 28 | 29 | 无论开发者使用什么渲染 API,一切内容都会渲染到 Surface 上,Surface 即供 UI 应用程序绘制图像的 "画板",承载应用要渲染的图像数据。 30 | 31 | 应用端可以使用 OpenGL ES 、Vulkan 或 Canvas API 渲染到 Surface 上。 32 | 33 | #### Hardware Composer 硬件混合渲染器(HWC) 34 | 35 | 用于确定组合缓冲区的最有效方式,作为 HAL 硬件抽象层,其实现是基于特定设备的,通常由屏幕硬件设备制造商 (OEM) 完成。 36 | 37 | SurfaceFlinger 在收集可见层的所有缓冲区后,便会询问 HWC 应如何进行合成。如果 HWC 将层合成类型标记为客户端合成,则 SurfaceFlinger 会合成这些层,然后 SurfaceFlinger 会将输出缓冲区传递给 HWC。 38 | 39 | #### Gralloc 40 | 41 | 包括 fb 和 gralloc 两个设备,fb 负责打开内核中的 FrameBuffer、初始化配置,并提供了 post、setSwapInterval 等操作接口;gralloc 负责管理帧缓冲区的分配和释放。 42 | 43 | 作为 HAL,上层都会通过 Gralloc 来访问内核显示设备的帧缓冲区。 44 | 45 | ### BufferQueue 与图像数据流 46 | 47 | 图像流由生产方流向消耗方,这种典型的生产者-消费者模型都是需要一个缓冲区队列的,BufferQueue 就是这个队列,它将图像流生产方与消耗方结合在一起,并且可以调解图像流从生产方到消耗方的固定周期。 48 | 49 | ![](/img/bufferqueue.png) 50 | 51 | 如图,生产方通过 dequeue 向 BufferQueue 申请空闲的缓冲区,将图像数据存放进去,然后通过 queue 移交给 BufferQueue。 52 | 53 | 消耗方通过 acquire 从 BufferQueue 中获取图像数据缓冲区,然后进行合成显示或处理后,再将缓冲区交还给 BufferQueue。 54 | 55 | 对应到显示场景,应用程序作为生产方将图像数据交给 BufferQueue;SurfaceFlinger 则作为消耗方从 BufferQueue 中取出来,然后合成图像数据。 -------------------------------------------------------------------------------- /Low Memory Killer.md: -------------------------------------------------------------------------------- 1 | 你一定听说过 Android 的应用保活,可能也知道几种保活方案,但这是一件没有门槛的事,任何人都可以轻易的从网上搜到,我们的目标应该是成为方案的创造者或改进者,而不仅是搬运工。 2 | 3 | ## LMK 是什么? 4 | 5 | Linux Kernel 有自己的内存监控机制 OOMKiller。当系统的可用内存达到临界值时,OOMKiller 就会按照优先级从低到高杀掉进程。 6 | 7 | 优先级该如何衡量呢?OOMKiller 会综合进程当前消耗内存、进程占用 CPU 时间、进程类型等因素,对进程实时评分。分值存储在 /proc/{PID}/oom_score 中,可通过 cat 命令查看。分值越低的进程,优先级越高,被杀死的概率越小。 8 | 9 | 基于 Linux Kernel 的 OOMKiller 思想,Android 系统拓展出了自己的内存监控体系,相比 Linux 达到临界值才触发,Android 实现了**不同梯级**的 Killer,并为此开发了专门的驱动,名为 Low Memory Killer。 10 | 11 | ## LMK 的梯级规则 12 | 13 | LMK 的源码位于内核的 /drivers/staging/android/Lowmemorykiller.c,Lowmemorykiller.c 中有如下定义: 14 | ```objectivec 15 | static int lowmem_adj[6] = {0, 1, 6, 12}; 16 | static int lowmem_adj_size = 4; //页大小 17 | 18 | //元素使用时以 lowmem_adj_size 为单位,即乘以 lowmem_adj_size 19 | static size_t lowmem_minfree[6] = { 20 | 3 * 512, //6MB 21 | 2 * 1024, //8MB 22 | 4 * 1024, //16MB 23 | 16 * 1024,//64MB 24 | }; 25 | ``` 26 | lowmem_minfree 定义了可用内存容量对应的不同梯级。lowmem_adj 与 lowmem_minfree 中的梯级一一对应,表示处于某梯级时需要被处理的 adj 值。adj 值用来描述进程的优先级,取值范围为 -17~15,数字越小表示进程优先级越高,被杀死的概率越小。 27 | 28 | 比如当可用内存低于 64MB 时,即 lowmem_minfree 第 4 梯级,对应于 lowmem_adj 的 12,那就会清理掉优先级低于 12(即 adj>12)的进程。 29 | 30 | ## 自定义梯级 31 | 32 | 上面这两个数组中梯级的定义只是系统的预定义值,Android 系统还提供了相应的文件供我们修改这两组值,路径为: 33 | 34 | ```objectivec 35 | /sys/module/lowmemorykiller/parameters/adj 36 | /sys/module/lowmemorykiller/parameters/minfree 37 | ``` 38 | 可以在 init.rc (系统启动时由 init 进程解析的一个脚本) 中,这样修改: 39 | ```objectivec 40 | write /sys/module/lowmemorykiller/parameters/adj 0, 8 41 | write /sys/module/lowmemorykiller/parameters/minfree 1024, 4096 42 | ``` 43 | 44 | 除了启动时机,也能在系统运行时改变。ActivityManagerService 在运行时会根据当前的系统配置自动调整 adj 和 minfree,以尽可能适配不同的硬件设备,它的 updateOomLevels 方法也是通过修改上面两个文件来实现的。 45 | 46 | ## adj 47 | 48 | 了解了 Low Memory Killer 的梯级规则后,来看下 Android 进程的 adj(Adjustment) 值含义: 49 | 50 | | ADJ | 说明 | 51 | |--|--| 52 | | HIDDEN_APP_MAX_AD = 15 | 只运行了不可见 Activity 的进程 | 53 | | HIDDEN_APP_MIN_ADJ = 9 | 只运行了不可见 Activity 的进程 | 54 | | SERVICE_B_ADJ = 8 | B list of Service | 55 | | PREVIOUS_APP_ADJ = 7 | 用户的上一个产生交互的进程 | 56 | | HOME_APP_ADJ = 6 | Launcher 进程 | 57 | | SERVICE_ADJ = 5 | 当前运行了 application service 的进程 | 58 | | BACKUP_APP_ADJ = 4 | 用于承载 backup 相关操作的进程 | 59 | | HEAVY_WEIGHT_APP_ADJ = 3 | 重量级应用程序进程 | 60 | | PERCEPTIBLE_APP_ADJ = 2 | 能被用户感觉但不可见,如后台运行的音乐播放器 | 61 | | VISIBLE_APP_ADJ = 1 | 有前台可见的 Activity | 62 | | FOREGROUND_APP_ADJ = 0 | 当前正在前台运行与用户交互的进程 | 63 | | PERSISTENT_PROC_ADJ = -12 | Persistent 性质的进程,如 telephony | 64 | | SYSTEM_ADJ = -16 | 系统进程 | 65 | 66 | 除了表格中系统的评定规则,我们有没有办法改变某一进程的 adj 值呢?和修改上面的 adj、minfree 梯级类似,进程的 adj 值也可以通过写文件的方式来修改的,路径为 /proc/{PID}/oom_adj,比如在 init.rc 中是这样修改的: 67 | ```objectivec 68 | write /proc/1/oom_adj -16 69 | ``` 70 | 71 | > 掌握了 LMK 机制,再看应用保活中规避被杀的方案,一像素 Activity 也好,前台 Service 也罢,不过是降低 adj 罢了 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 关注公众号,为你解答各种 Android 面试问题 2 | 3 | ![](/img/qrcode.jpg) 4 | 5 | -------------------------------------------------------------------------------- /Service 启动流程.md: -------------------------------------------------------------------------------- 1 | 😎 😨 🤔️ :好久不见,甚是想念 2 | 3 | --- 4 | 5 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 6 | 7 | **面试官**:说一说 Service 的启动流程 8 | 9 | 😎:直接 startService 就启动了鸭 10 | 11 | **面试官**:就这样? 12 | 13 | 😎:你也太小看我了,还有 bindService 啊,也会自动启动 14 | 15 | **面试官**: emm,深入点呢? 16 | 17 | 😎:不好意思,没深入过。。。 18 | 19 | **面试官**:没事,回去准备下,下次还问你 Service 20 | 21 | --- 22 | 23 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 24 | 25 | **面试官**:说一说 Service 的启动流程 26 | 27 | 😨:Service 作为四大组件之一,当然也是通过 AMS 去管理创建流程的。一个应用进程通过 startService 调用 AMS,然后 AMS 再判断这个 Service 是否已经启动,若未启动则通知应用去启动 Service,或已经启动则直接通知客户端回调 onStartCommand 28 | 29 | **面试官**:这一流程中,你只说了 starService ,用 bindService 会有什么不同吗? 30 | 31 | 😨:没有吧,只有这一套流程 32 | 33 | **面试官**:好的,回去等通知吧 34 | 35 | --- 36 | 37 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 38 | 39 | **面试官**:说一说 Service 的启动流程 40 | 41 | 🤔️:我来画一下详细的流程图,这样就一目了然了: 42 | 43 | ![](img/Service 创建流程图.jpg) 44 | 45 | **面试官**:怎么判断的 Service 和应用进程是否启动呢? 46 | 47 | 🤔️:代码走起: 48 | 49 | ```java 50 | Context.startService(Intent) 51 | -> ContextImpl.startService(Intent) 52 | -> ContextImpl.startServiceCommon(...) 53 | -> ActivityManager.getService().startService(...) 54 | // AMS: ActivityManagerService 55 | -> AMS.startService(...) 56 | // ActiveServices: AMS.mServices 57 | -> ActiveServices.startServiceLocked(...) // 1 58 | -> ActiveServices.startServiceInnerLocked(...) 59 | -> ActiveServices.bringUpServiceLocked(...) // 2 60 | ``` 61 | 62 | bringUpServiceLocked 这就是我们要看的方法,但是在讲它之前,我们先看前面的 startServiceLocked 方法 63 | 64 | ```java 65 | ComponentName startServiceLocked(Intent service, ...) { 66 | // 根据 Intent 获取 ServiceRecord 对象 67 | // 每个 Service 在 AMS 都要对应有一个 ServiceRecord 对象 68 | // 作用如其名 就是用来记录一些 Service 的信息 69 | ServiceLookupResult res = retrieveServiceLocked(service, ...) 70 | ServiceRecord r = res.record; 71 | 72 | // pendingStarts 后续回调 onStartCommand 73 | r.pendingStarts.add(new ServiceRecord.StartItem(r, ...)); 74 | 75 | return startServiceInnerLocked(smap, service, r, ...); 76 | } 77 | ``` 78 | 讲这个方法主要是为了知道 bringUpServiceLocked 里的 ServiceRecord 是怎么来的,接下来就可以看 bringUpServiceLocked 了 79 | 80 | ```JAVA 81 | private String bringUpServiceLocked(ServiceRecord r, ...) { 82 | // r.app 就是表示 Service 所在的进程,真正启动 Service 时会给它赋值 83 | // Service 已启动的话 这个进程非空,且已经就绪 84 | if (r.app != null && r.app.thread != null) { 85 | // Service 已启动则通过 pendingStarts 触发客户端回调 onStartCommand 86 | sendServiceArgsLocked(r, execInFg, false); 87 | return null; 88 | } 89 | ... 90 | // 找到 Service 对就的进程记录 91 | ProcessRecord app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false); 92 | if (app != null && app.thread != null) { 93 | // 进程就绪才真正去启动 Service 94 | realStartServiceLocked(r, app, execInFg); 95 | return null; 96 | } 97 | 98 | // Not running -- get it started, and enqueue this service record 99 | // to be executed when the app comes up. 100 | if (app == null) { 101 | // 进程没启动则去启动进程 102 | app=mAm.startProcessLocked(procName, r.appInfo,...) 103 | } 104 | if (!mPendingServices.contains(r)) { 105 | // 记录,进程启动就绪后会再去启动 Service 106 | mPendingServices.add(r); 107 | } 108 | } 109 | ``` 110 | 通过以上源码解读可以知道:AMS 是通过其持有 Service 对应的 ServiceRecord 来判断 Service 是否已启动,同时进程是通过类似的 ProcessRecord 来判断是否已启动 111 | 112 | **面试官**:通过 bindService 启动的流程会有什么不同吗 113 | 114 | 🤔️:我们知道通过 bindService 启动时 Service 是不会回调 onStartCommand 的。 115 | 116 | 来跟一下 bindService 的调用栈,其实和 startService 很类似,这里做一些省略 117 | 118 | ```JAVA 119 | ContextImpl.bindServiceCommon(...) 120 | -> ActivityManager.getService().bindService(...) 121 | -> AMS.bindService(...) 122 | -> ActiveServices.bindServiceLocked(...) 123 | 124 | int bindServiceLocked(IApplicationThread caller,...) { 125 | // * Flag for {@link #bindService}: automatically create the service as long as the binding exists. 126 | if ((flags&Context.BIND_AUTO_CREATE) != 0) { 127 | // 这个方法上面已经说过了,会在需要时启动 Service 128 | bringUpServiceLocked(s,...) 129 | } 130 | } 131 | ``` 132 | 我们注意到 bindService 最终也是通过 bringUpServiceLocked 去启动 Service,但是调用链中没有调用 startServiceLocked 这个方法,也就没有操作 r.pendingStarts , 自然不会回调 onStartCommand 了。 133 | 134 | **面试官**:bindService 还会做一些事,不过我们今天只聊 Service 的启动流程,下次再聊吧。 -------------------------------------------------------------------------------- /Service 详解.md: -------------------------------------------------------------------------------- 1 | # Service 详解 2 | 3 | >Service 是四大组件之一,后台运行的解决方案,适合那些不需要和用户交互还要长期运行的任务。 4 | Service 的运行不依赖于任何用户界面,即使 app 被切到后台,Service 仍能够正常运行。当某个程序进程被杀掉时,所有依赖于该进程的 Service 也会停止 5 | 6 | 使用时必须在清单文件中声明服务如: 7 | ``` 8 | 9 | ... 10 | 11 | 12 | ... 13 | 14 | 15 | ``` 16 | 17 | #### Service 与 Thread 的区别 18 | 19 | * Thread 是 cpu 执行的最小单元,它是分配 cpu 的基本单位,用来执行异步操作 20 | * Service 是安卓的一种机制,运行于主线程的 Service 是 Context 的子类,可以调用 Context 的所有方法。可以通过 startService, stopService, bindService, unbindService 来控制它,也可以在 Service 里注册 BroadcastReceiver 在其它地方发送广播来控制它。这些都是 Thread 做不到的 21 | 22 | #### 生命周期 23 | 24 | ![serviceLifecycle](img/serviceLifecycle.png) 25 | * onCreate()
26 | 首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand(), onBind() 之前)。 27 | * onStartCommand()
28 | 当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果您实现此方法,则在服务工作完成后,需要由您通过调用 stopSelf() 或 stopService() 来停止服务。(如果您只想提供绑定,则无需实现此方法。) 29 | * onBind()
30 | 当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC(跨进程调用远程函数))时,系统将调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,供客户端用来与服务进行通信。请务必实现此方法,但如果您并不希望允许绑定,则应返回 null。 31 | * onDestroy()
32 | 当服务不再使用且将被销毁时,系统将调用此方法。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等。 这是服务接收的最后一个调用。 33 | * 如果一个 Service 被 startService 方法多次启动,onCreate 方法只会调用一次,onStartCommand 方法可能会调用多次(对应 startService 调用的次数),即系统只会创建一 Service 的一个实例,所以只需要调用一次 stopService 34 | * 被启动又被绑定的 Service,也必须调用(stopSelf 或 stopService)+(unbindService 或 Context 回收自动 unbind)才能停止服务(不分先后顺序) 35 | * 系统资源不足,系统有可能直接结束服务 36 | 37 | #### 启动方式 38 | * startService 只是启动 Service,启动它的组件(如 Activity)和 Service 并没有关联,只有当 Service 调用 stopSelf 或者其他组件调用 stopService 服务才会终止。 39 | * bindService 方法启动 Service,其他组件可以通过回调获取 Service 的代理对象和 Service 交互,而这两方也进行了绑定,当启动方销毁时,Service 也会自动进行 unBind 操作,当发现所有绑定都进行了 unBind 时才会销毁 Service。 40 | * startService() 方法将立即返回,且 Android 系统调用服务的 onStartCommand() 方法并向其传递 Intent。(切勿直接调用 onStartCommand()) 41 | 42 | #### 停止 Service 43 | 44 | * 一旦请求使用 stopSelf() 或 stopService() 停止服务,系统就会尽快销毁服务。(Service.stopSelf 等同于 Context.stopService) 45 | * 如果服务同时处理多个 onStartCommand() 请求,则您不应在处理完一个启动请求之后停止服务,因为您可能已经收到了新的启动请求(在第一个请求结束时停止服务会终止第二个请求)。为了避免这一问题,您可以使用 stopSelf(int) 确保服务停止请求始终基于最近的启动请求。也就说,在调用 stopSelf(int id) 时,传递与停止请求的 ID 对应的启动请求的 ID(传递给 onStartCommand() 的 startId) 。然后,如果在您能够调用 stopSelf(int) 之前服务收到了新的启动请求, ID 就不匹配,服务也就不会停止。 46 | * 当服务被终止的时候,用户是无感知的。所以可用于不需要运行或终止提示的服务,如天气更新,日期同步,邮件同步等。 47 | 48 | #### IntentService 49 | > Service 的子类,它使用工作线程逐一处理所有启动请求。如果您不要求服务同时处理多个请求,这是最好的选择。 您只需实现 onHandleIntent() 方法即可,该方法会接收每个启动请求的 Intent,使您能够执行后台工作。 50 | 51 | IntentService 特性: 52 | 53 | * 创建默认的工作线程,用于在应用的主线程外执行传递给 onStartCommand() 的所有 Intent。 54 | * 创建工作队列,用于将一个 Intent 逐一传递给 onHandleIntent() 实现,这样您就永远不必担心多线程问题。 55 | * 在处理完所有启动请求后停止服务,因此您永远不必调用 stopSelf()。 56 | * 提供 onBind() 的默认实现(返回 null)。 57 | * 提供 onStartCommand() 的默认实现,可将 Intent 依次发送到工作队列和 onHandleIntent() 实现。 58 | * 通过封装 Handler 和 Thread 实现异步任务 59 | 60 | #### onStartCommand 61 | 62 | onStartCommand() 方法必须返回整型数。整型数是一个值,用于描述系统应该如何在服务终止的情况下继续运行服务(如上所述,IntentService 的默认实现将为您处理这种情况,不过您可以对其进行修改)。从 onStartCommand() 返回的值必须是以下常量之一: 63 | 64 | * START_NOT_STICKY 65 | 如果系统在 onStartCommand() 返回后终止服务,则除非有挂起 Intent 要传递,否则系统不会重建服务。这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。 66 | 67 | * START_STICKY 68 | 如果系统在 onStartCommand() 返回后终止服务,则会重建服务并调用 onStartCommand(),但绝对不会重新传递最后一个 Intent。相反,除非有挂起 Intent 要启动服务(在这种情况下,将传递这些 Intent ),否则系统会通过空 Intent 调用 onStartCommand()。这适用于不执行命令、但无限期运行并等待作业的媒体播放器(或类似服务)。 69 | 70 | * START_REDELIVER_INTENT 71 | 如果系统在 onStartCommand() 返回后终止服务,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand()。任何挂起 Intent 均依次传递。这适用于主动执行应该立即恢复的作业(例如下载文件)的服务。 72 | 73 | 74 | #### bindService & unbindService 75 | 绑定服务允许应用组件通过调用 bindService() 与其绑定,以便创建长期连接. 76 | 77 | 如需与 Activity 和其他应用组件中的服务进行交互,或者需要通过进程间通信 (IPC) 向其他应用公开某些应用功能,则应创建绑定服务。 78 | 79 | 要创建绑定服务,必须实现 onBind() 回调方法以返回 IBinder,用于定义与服务通信的接口。然后,其他应用组件可以调用 bindService() 来检索该接口,并开始对服务调用方法。服务只用于与其绑定的应用组件,因此如果没有组件绑定到服务,则系统会销毁服务(您不必按通过 onStartCommand() 启动的服务那样来停止绑定服务)。 80 | 81 | 要创建绑定服务,首先必须定义指定客户端如何与服务通信的接口。 服务与客户端之间的这个接口必须是 IBinder 的实现,并且服务必须从 onBind() 回调方法返回它。一旦客户端通过 ServiceConnection.onServiceConnected()收到 IBinder,即可开始通过该接口与服务进行交互。 82 | 83 | 实现绑定服务时,最重要的环节是定义您的 onBind() 回调方法返回的 IBinder,用以提供客户端用来与服务进行交互的编程接口。 您可以通过三种方法定义接口: 84 | * 扩展 Binder 类: 85 | Service 仅供自有应用专用,且与客户端位于相同进程。不以这种方式创建接口的唯一原因是,您的服务被其他应用或不同的进程占用。 86 | 使用方法: 87 | * 在您的服务中,创建一个可满足下列任一要求的 Binder 实例: 88 | * 包含客户端可调用的公共方法 89 | * 返回当前 Service 实例,其中包含客户端可调用的公共方法 90 | * 或返回由服务承载的其他类的实例,其中包含客户端可调用的公共方法 91 | * 从 onBind() 回调方法返回此 Binder 实例。 92 | * 在客户端中,从 onServiceConnected() 回调方法接收 Binder,并使用提供的方法调用绑定服务。 93 | 94 | 示例:客户端可以调用 getServices 方法,获取 Service 的引用与其交互 95 | ``` 96 | public class LifecycleTestService extends Service { 97 | 98 | private TestThread mTestThread; 99 | private final IBinder mBinder = new MyBinder(); 100 | 101 | public class MyBinder extends Binder{ 102 | LifecycleTestService getServices(){ 103 | return LifecycleTestService.this; 104 | } 105 | } 106 | 107 | @Nullable 108 | @Override 109 | public IBinder onBind(Intent intent) { 110 | return mBinder; 111 | } 112 | ``` 113 | 114 | * 使用 Messenger: 115 | 如需让接口跨不同的进程工作,则可使用 Messenger 为服务创建接口。服务可以这种方式定义对应于不同类型 Message 对象的 Handler。此 Handler 是 Messenger 的基础,后者随后可与客户端分享一个 IBinder,从而让客户端能利用 Message 对象向服务发送命令。此外,客户端还可定义自有 Messenger,以便服务回传消息。 116 | 这是执行进程间通信 (IPC) 的最简单方法,因为 Messenger 会在单一线程中创建包含所有请求的队列,这样您就不必对服务进行线程安全设计。 117 | 118 | * 使用 AIDL: 119 | AIDL(Android 接口定义语言)执行所有将对象分解成原语的工作,操作系统可以识别这些原语并将它们编组到各进程中,以执行 IPC。之前采用 Messenger 的方法实际上是以 AIDL 作为其底层结构。如上所述,Messenger 会在单一线程中创建包含所有客户端请求的队列,以便服务一次接收一个请求。不过,如果您想让服务同时处理多个请求,则可直接使用 AIDL。 在此情况下,您的服务必须具备多线程处理能力,并采用线程安全式设计。(多数应用“都不会”使用 AIDL 来创建绑定服务,因为它可能要求具备多线程处理能力,并可能导致实现的复杂性增加。因此,AIDL 并不适合大多数应用,本文也不会阐述如何将其用于您的服务。) 120 | 121 | 当客户端被销毁时,它将取消与服务的绑定,但您应该始终在完成与服务的交互时或您的 Activity 暂停时取消绑定,以便服务能够在未被占用时关闭。 122 | 123 | #### 管理 bindService 的生命周期 124 | 125 | 同时使用 bindService 和 startService 126 | ![bindServiceLifecycle](img/bindServiceLifecycle.png) 127 | 128 | #### 前台 Service 129 | 由于后台服务优先级相对比较低,当系统出现内存不足的情况下 130 | 前台服务被认为是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。 前台服务必须为状态栏提供通知,这意味着除非服务停止或从前台删除,否则不能清除通知。 131 | 如: 132 | 133 | - 应该将从服务播放音乐的音乐播放器设置为在前台运行,这是因为用户明确意识到其操作。 134 | - 状态栏中的通知可能表示正在播放的歌曲,并允许用户启动 Activity 来与音乐播放器进行交互。 135 | - 天气应用 136 | 137 | 要请求让服务运行于前台,请调用 startForeground()。此方法取两个参数:唯一标识通知的整型数和状态栏的 Notification。例如: 138 | ``` 139 | Notification notification = new Notification(R.drawable.icon, getText(R.string.ticker_text), 140 | System.currentTimeMillis()); 141 | Intent notificationIntent = new Intent(this, ExampleActivity.class); 142 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); 143 | notification.setLatestEventInfo(this, getText(R.string.notification_title), 144 | getText(R.string.notification_message), pendingIntent); 145 | startForeground(ONGOING_NOTIFICATION_ID, notification); 146 | ``` 147 | 148 | 要从前台删除服务,请调用 stopForeground()。此方法取一个布尔值,指示是否也删除状态栏通知。 此方法绝对不会停止服务。 但是,如果您在服务正在前台运行时将其停止,则通知也会被删除。 149 | 150 | ``` 151 | void stopForeground(boolean removeNotification) 152 | ``` 153 | #### 总结 154 | 今天整理和学习了 Service 的使用、生命周期、IntentService、前台后台服务等一些常用的知识点,也是面试中常问的基础点,学会的话点个赞吧~ 155 | -------------------------------------------------------------------------------- /ServiceManager 的工作原理.md: -------------------------------------------------------------------------------- 1 | ServiceManager 是 Android 系统中重要的组成部分,我们有必要理解它的工作原理,本文从三个方面介绍: 2 | 3 | - 1.ServiceManager 概述 4 | - 2.ServiceManager 启动原理 5 | - 3.服务的注册与查询 6 | 7 | ## ServiceManager 概述 8 | 9 | Binder 是 Android 中使用最广泛的 IPC 机制,正因为有了 Binder,Android 系统中形形色色的进程与组件才能真正统一成有机的整体。Binder 通信机制与 TCP/IP 有共通之处,其组成元素可以这样来类比: 10 | 11 | - binder 驱动 -> 路由器 12 | - ServiceManager -> DNS 13 | - Binder Client -> 客户端 14 | - Binder Server -> 服务器 15 | 16 | ServiceManager 是为了完成 Binder Server 的 Name(域名)和 Handle(IP 地址)之间对应关系的查询而存在的,它主要包含的功能: 17 | 18 | **注册**:当一个 Binder Server 创建后,应该将这个 Server 的 Name 和 Handle 对应关系记录到 ServiceManager 中 19 | 20 | **查询**:其他应用可以根据 Server 的 Name 查询到对应的 Service Handle 21 | 22 | 但 ServiceManager 自身也是一个 Binder Server(服务器),怎么找到它的 "IP 地址"呢?Binder 机制对此做了特别规定:ServiceManager 在 Binder 通信过程中的 Handle 永远是 0。 23 | 24 | ## ServiceManager 启动原理 25 | 26 | Android 系统第一个启动的 init 进程解析 init.rc 脚本时构建出系统的初始运行状态,Android 系统服务大多是在这个脚本中描述并被相继启动的,包括 zygote、mediaserver、surfaceflinger 以及 servicemanager 等,其中 servicemanager 描述如下: 27 | 28 | ```objectivec 29 | #init.rc 30 | service servicemanager /system/bin/servicemanager 31 | class core 32 | user system 33 | group system 34 | critical 35 | onrestart restart healthd 36 | onrestart restart zygote 37 | onrestart restart media 38 | onrestart restart surfaceflinger 39 | onrestart restart drm 40 | ``` 41 | 42 | 可以看到,当 ServiceManager 发生问题重启时,其他 healthd、zygote、media 等服务也会被重启。ServiceManager 服务启动后会执行 service_manager.c 的 main 函数,关键代码如下: 43 | 44 | ```objectivec 45 | //frameworks/native/cmds/servicemanager/service_manager.c 46 | int main(){ 47 | bs = binder_open(128*1024); 48 | if (binder_become_context_manager(bs)) { 49 | ... 50 | } 51 | ... 52 | binder_loop(bs, svcmgr_handler); 53 | return 0; 54 | } 55 | ``` 56 | 57 | 其中三个函数对应了 ServiceManager 初始化的三个关键工作: 58 | 59 | 1. binder_open():打开 binder 驱动并映射内存块大小为 128KB 60 | 2. binder_become_context_manager():将自己设置为 Binder "DNS" 管理者 61 | 3. binder_loop():进入循环,等待 binder 驱动发来消息 62 | 63 | 下面分别来分析这三个函数,首先来看 binder_open() 是怎么打开 binder 驱动并映射内存的: 64 | ```objectivec 65 | struct binder_state *binder_open(size_t mapsize){ 66 | struct binder_state *bs; 67 | struct binder_version vers; 68 | bs = malloc(sizeof(*bs)); 69 | ... 70 | //打开 binder 驱动,最终调用 binder_open() 函数 71 | bs->fd = open("/dev/binder", O_RDWR | O_CLOEXEC); 72 | ... 73 | //获取 Binder 版本,最终调用 binder_ioctl() 函数 74 | ioctl(bs->fd, BINDER_VERSION, &vers) 75 | ... 76 | //将虚拟内存映射到 Binder,最终调用 binder_mmap() 函数 77 | bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0); 78 | ... 79 | return bs; 80 | } 81 | ``` 82 | 83 | 再来看 binder_become_context_manager() 是怎么将自己设置为 Binder "DNS 管理者的": 84 | 85 | ```objectivec 86 | int binder_become_context_manager(struct binder_state *bs){ 87 | //发送 BINDER_SET_CONTEXT_MGR 命令,最终调用 binder_ioctl() 函数 88 | return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0); 89 | } 90 | ``` 91 | 92 | 最后来看 binder_loop() 是怎么循环等待并处理 binder 驱动发来的消息: 93 | 94 | ```objectivec 95 | void binder_loop(struct binder_state *bs, binder_handler func){ 96 | int res; 97 | //执行 BINDER_WRITE_READ 命令所需的数据格式: 98 | struct binder_write_read bwr; 99 | uint32_t readbuf[32]; //每次读取数据的大小 100 | readbuf[0] = BC_ENTER_LOOPER; 101 | //先将 binder 驱动的进入循环命令发送给 binder 驱动: 102 | binder_write(bs, readbuf, sizeof(uint32_t)); 103 | for (;;) { //进入循环 104 | bwr.read_size = sizeof(readbuf); 105 | //读取到的消息数据存储在 readbuf 106 | bwr.read_buffer = (uintptr_t) readbuf; 107 | //执行 BINDER_WRITE_READ 命令读取消息数据 108 | res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr); 109 | if (res < 0) { 110 | ALOGE("binder_loop: ioctl failed (%s)\n", strerror(errno)); 111 | break; 112 | } 113 | //处理读取到的消息数据 114 | res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func); 115 | ... 116 | } 117 | } 118 | ``` 119 | BINDER_WRITE_READ 命令既可以用来读取数据也可以写入数据,具体是写入还是读取依赖 binder_write_read 结构体的 write_size 和 read_size 哪个大于 0,上面代码通过 bwr.read_size = sizeof(readbuf) 赋值,所以是读取消息。 120 | 121 | binder_parse() 方法内部处理由 binder 驱动主动发出的、一系列 BR_ 开头的命令,包括上面提到过的 BR_TRANSACTION、BR_REPLY 等,简化后的代码如下: 122 | 123 | ```objectivec 124 | int binder_parse(struct binder_state *bs, struct binder_io *bio, 125 | uintptr_t ptr, size_t size, binder_handler func){ 126 | switch(cmd) { 127 | case BR_TRANSACTION: { 128 | ... 129 | res = func(bs, txn, &msg, &reply); //处理消息 130 | //返回处理结果 131 | inder_send_reply(bs, &reply, txn->data.ptr.buffer, res); 132 | ... 133 | break; 134 | } 135 | case BR_REPLY: {...} 136 | case BR_DEAD_BINDER: {...} 137 | ... 138 | } 139 | } 140 | ``` 141 | 对于 BR_TRANSACTION 命令主要做了两个工作,一是调用 func() 具体处理消息;二是调用 inder_send_reply() 将消息处理结果告知给 binder 驱动,注意这里的 func 是由 service_manager.c main 函数中传过来的方法指针,也就是 svcmgr_handler() 方法。 142 | 143 | ## 服务注册与查询 144 | 145 | 经过上面 ServiceManager 服务启动的过程分析,已经知道由 binder 驱动主动发过来的 BR_TRANSACTION 命令最终在 service_manager.c 的 svcmgr_handler() 方法中处理,那服务的注册与查询请求想必就是在这个方法中实现的了,确实如此,简化后的关键代码如下: 146 | 147 | ```objectivec 148 | int svcmgr_handler(struct binder_state *bs, 149 | struct binder_transaction_data *txn, 150 | struct binder_io *msg, 151 | struct binder_io *reply){ 152 | switch(txn->code) { 153 | case SVC_MGR_GET_SERVICE: 154 | case SVC_MGR_CHECK_SERVICE: 155 | //查询服务,根据 name 查询 Server Handle 156 | handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid); 157 | return 0; 158 | case SVC_MGR_ADD_SERVICE: 159 | //注册服务,记录服务的 name(下面的参数 s) 与 handle 160 | if (do_add_service(bs, s, len, handle, txn->sender_euid, 161 | allow_isolated, txn->sender_pid)) 162 | return -1; 163 | break; 164 | case SVC_MGR_LIST_SERVICES: { 165 | //查询所有服务,返回存储所有服务的链表 svclist 166 | si = svclist; 167 | while ((n-- > 0) && si) 168 | si = si->next; 169 | if (si) { 170 | bio_put_string16(reply, si->name); 171 | return 0; 172 | } 173 | return -1; 174 | } 175 | bio_put_uint32(reply, 0); 176 | return 0; 177 | } 178 | ``` 179 | 注册的服务都会存储在 svclist 链表上,do_add_service() 就是将服务插入到 svclist 链表上记录下来,do_find_service() 方法则遍历 svclist 查找对应的服务。 180 | 181 | svcmgr_handler() 方法执行完后会进一步调用 inder_send_reply() 将执行结果回复给 binder 驱动,然后进入下一轮循环继续等待处理消息。 182 | 183 | ## 总结 184 | 185 | ServiceManager 在 init.rc 中描述,由 init 进程启动,运行在一个单独的进程。 186 | 187 | ServiceManager 启动后主要做了三件事:1. 打开 binder 驱动并映射内存;2. 将自己设置为 "服务大管家";3. 循环等待 binder 驱动发来的消息。 188 | 189 | ServiceManager 通过记录服务 Name 和 Handler 的关系,提供服务注册与查询功能。 190 | 191 | 对 Client 来说,ServiceManager 也是一个 Service,Client 可以直接获取到这个 Service,因为它的句柄值固定为 0。 192 | 193 | ![](/img/binder_code.png) 194 | 195 | >如上图,这里有一份按模块分好的 Binder 源码,并有关键步骤注释。关注公众号 **Android 面试官** 留言:binder,即可获得此 Binder 学习必备源码~ -------------------------------------------------------------------------------- /Zygote 启动流程.md: -------------------------------------------------------------------------------- 1 | > Zygote 单词意思是“受精卵”,寓意着它可以“孕育”出一个“新生命”。在 Android 中大多数的进程和系统进程都是通过 Zygote 来生成的。 2 | 3 | ## Zygote 的作用 4 | 5 | 其实这个作用前面简介就已经概括了,Zygote 就两个作用 6 | 7 | - 启动 SystemServer 8 | - 孵化应用进程 9 | 10 | 通过 Zygote fork 出这些进程,这些进程就可以直接继承 Zygote 准备好的资源,比如: 11 | 12 | - 常用类 13 | - JNI 函数 14 | - 主题资源 15 | - 共享库 16 | 17 | 直接继承资源,避免重新去加载这些资源,从而提高了系统的性能。 18 | 19 | ## Zygote 的启动流程 20 | 21 | init 进程是安卓启动的第一个进程,而 Zygote 是由 init 进程解析 rc 脚本时启动的。早期的 Android 版本中 Zygote 的启动命令直接被写在 init.rc 中。但随着硬件升级,Android 系统同时面对 32 位和 64 位的机器,因而对 Zygote 的启动也需要根据不同的情况区分对待: 22 | 23 | ```c++ 24 | /* system/core/rootdir/init.rc */ 25 | import /init.${ro.hardware}.rc 26 | import /init.${ro.zygote}.rc 27 | ``` 28 | 29 | 在 init.rc 文件中,根据系统属性 ro.zygote 的具体值,加载不同的描述 Zygote 的 rc 脚本,比如: 30 | 31 | - init.zygote32.rc -> 32 位的机器 32 | - init.zygote32_64.rc -> Primary Arch 33 | - init.zygote64_32.rc -> Second Arch 34 | - init.zygote64.rc -> 64 位的机器 35 | 36 | #### init.zygote64.rc 37 | 38 | 以 64 位的 rc 文件为例,我们来看下里面的内容: 39 | 40 | ``` 41 | service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server 42 | class main 43 | priority -20 44 | user root 45 | group root readproc reserved_disk 46 | socket zygote stream 660 root system 47 | onrestart write /sys/android_power/request_state wake 48 | onrestart write /sys/power/state on 49 | onrestart restart audioserver 50 | onrestart restart cameraserver 51 | onrestart restart media 52 | onrestart restart netd 53 | onrestart restart wificond 54 | writepid /dev/cpuset/foreground/tasks 55 | ``` 56 | 57 | zygote 进程的启动方式是 fork + execve 系统调用: 58 | 59 | ```c++ 60 | pid_t pid = fork(); 61 | // 调用fork 启动子进程 62 | // 会返回两次 63 | if(pid == 0) { 64 | // fork 出来的子进程 65 | // 加载新的二进程程序,来替换继承的父进程资源 66 | execve(path, argv, env) 67 | } else { 68 | // 父进程 69 | } 70 | ``` 71 | 72 | 这个脚本里最核心的就是第一行了,拆分一下得到信息(也就是启动进程后需要的 execve 参数 ): 73 | 74 | - ServiceName: zygote 75 | - Path: /system/bin/app_process64 76 | - Arguments: -Xzygote /system/bin --zygote --start-system-server 77 | 78 | 从 zygote 的 path 路径可以看出,它所在的程序名叫“app_process64”,不像 ServiceManager 是在一个独立的程序中。通过指定 --zygote 参数 ,app_process 可以识别出用户是否需要启动 zygote。 79 | 80 | 在我们这个场景中,init.rc 指定了 --zygotę 选项, 因而 app_process 接下来将启动 ZygoteInit,并传入 start-system-server 作为参数之一标记需要启动 System Server, 接下来 ZygoteInit 会运行于 Java 虚拟机之上(在此之前都是在 native 层运行, app_process 会去启动虚拟机)。我们来看下相关代码: 81 | 82 | ```JAVA 83 | //frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 84 | public static void main(String argv[]) { 85 | //.... 86 | for (int i = 1; i < argv.length; i++) { 87 | // 读取参数 88 | if ("start-system-server".equals(argv[i])) { 89 | startSystemServer = true; // 需要启动 SystemServer 90 | } else if ("--enable-lazy-preload".equals(argv[i])) { 91 | enableLazyPreload = true; 92 | } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { 93 | socketName = argv[i].substring(SOCKET_NAME_ARG.length()); 94 | } else ... 95 | } 96 | // 注册 socket 97 | zygoteServer.registerServerSocketFromEnv(socketName); 98 | // 预加载种类资源 99 | preload(bootTimingsTraceLog); 100 | if (startSystemServer) { 101 | // 去启动 systemServer 102 | Runnable r = forkSystemServer(abiList, socketName, zygoteServer); 103 | // {@code r == null} in the parent (zygote) process, and {@code r != null} in the 104 | // child (system_server) process. 105 | if (r != null) { 106 | r.run(); 107 | return; 108 | } 109 | } 110 | // loops forever in the zygote. 111 | caller = zygoteServer.runSelectLoop(abiList); 112 | // We're in the child process and have exited the select loop. 113 | // Proceed to execute the command. 114 | if (caller != null) { 115 | caller.run(); 116 | } 117 | } 118 | ``` 119 | 120 | 精简下这段代码并不复杂,它主要完成以下三项工作: 121 | 122 | - 注册一个 Socket:
123 | Zygote 是孵化器,一时有新程序需要运行时,系统会通过这个 Soket (名称:ANDROID_SOCKET_zygote) 通知 Zygote,并由它负责实际的进程孵化过程。 124 | - 预加载各种资源, preload 方法里包含:
125 | - preloadClasses(); 126 | - preloadResources(); 127 | - preloadOpenGL(); 128 | - preloadSharedLibraries(); 129 | - preloadTextResources(); 130 | - 启动一个 Loop 循环,去处理 Socket 接收到的事件 131 | 132 | 至此,Zygote 便启动完成了。至于 Zygotę 具体是怎么启动 SystemServer 和孵化应用进程的,感兴趣的不妨点赞在看,我们后面再谈。 -------------------------------------------------------------------------------- /img/1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/1.jpeg -------------------------------------------------------------------------------- /img/1240-20200630001324067.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/1240-20200630001324067.png -------------------------------------------------------------------------------- /img/1240-20200630001333060.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/1240-20200630001333060.png -------------------------------------------------------------------------------- /img/1240-20200630001341005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/1240-20200630001341005.png -------------------------------------------------------------------------------- /img/1240-20200630001349746.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/1240-20200630001349746.png -------------------------------------------------------------------------------- /img/1240-20200630001356962.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/1240-20200630001356962.png -------------------------------------------------------------------------------- /img/1240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/1240.png -------------------------------------------------------------------------------- /img/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/15.jpg -------------------------------------------------------------------------------- /img/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/16.jpg -------------------------------------------------------------------------------- /img/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/17.jpg -------------------------------------------------------------------------------- /img/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/18.jpg -------------------------------------------------------------------------------- /img/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/20.jpg -------------------------------------------------------------------------------- /img/23种设计模式.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/23种设计模式.png -------------------------------------------------------------------------------- /img/640.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/640.jpeg -------------------------------------------------------------------------------- /img/InterpreterUML.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/InterpreterUML.jpg -------------------------------------------------------------------------------- /img/Screenshot_1593358817.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/Screenshot_1593358817.png -------------------------------------------------------------------------------- /img/Service 创建流程图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/Service 创建流程图.jpg -------------------------------------------------------------------------------- /img/ape_fwk_graphics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/ape_fwk_graphics.png -------------------------------------------------------------------------------- /img/bindService.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/bindService.jpg -------------------------------------------------------------------------------- /img/bindServiceLifeFix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/bindServiceLifeFix.jpg -------------------------------------------------------------------------------- /img/bindServiceLifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/bindServiceLifecycle.png -------------------------------------------------------------------------------- /img/binder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/binder.png -------------------------------------------------------------------------------- /img/binder_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/binder_code.png -------------------------------------------------------------------------------- /img/binder_design.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/binder_design.jpg -------------------------------------------------------------------------------- /img/binder_driver.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/binder_driver.jpg -------------------------------------------------------------------------------- /img/binder_mmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/binder_mmap.jpg -------------------------------------------------------------------------------- /img/binder_oneway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/binder_oneway.png -------------------------------------------------------------------------------- /img/binder_proc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/binder_proc.png -------------------------------------------------------------------------------- /img/bufferqueue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/bufferqueue.png -------------------------------------------------------------------------------- /img/builder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/builder.jpg -------------------------------------------------------------------------------- /img/circle 11.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/circle 11.gif -------------------------------------------------------------------------------- /img/circle 12.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/circle 12.gif -------------------------------------------------------------------------------- /img/circle 13.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/circle 13.gif -------------------------------------------------------------------------------- /img/circle 14.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/circle 14.gif -------------------------------------------------------------------------------- /img/fd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/fd.png -------------------------------------------------------------------------------- /img/getimgdata.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/getimgdata.jpeg -------------------------------------------------------------------------------- /img/image-20200628233753147.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200628233753147.png -------------------------------------------------------------------------------- /img/image-20200707233520753.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200707233520753.png -------------------------------------------------------------------------------- /img/image-20200721005441905.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200721005441905.png -------------------------------------------------------------------------------- /img/image-20200721225839304.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200721225839304.png -------------------------------------------------------------------------------- /img/image-20200811230828501.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200811230828501.png -------------------------------------------------------------------------------- /img/image-20200817232940570.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200817232940570.png -------------------------------------------------------------------------------- /img/image-20200907203547588.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200907203547588.png -------------------------------------------------------------------------------- /img/image-20200917223121021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200917223121021.png -------------------------------------------------------------------------------- /img/image-20200917232430861.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200917232430861.png -------------------------------------------------------------------------------- /img/image-20200917234119283.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20200917234119283.png -------------------------------------------------------------------------------- /img/image-20201224090119744.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20201224090119744.png -------------------------------------------------------------------------------- /img/image-20201224121659744.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20201224121659744.png -------------------------------------------------------------------------------- /img/image-20201230124630002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20201230124630002.png -------------------------------------------------------------------------------- /img/image-20210116220618932.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210116220618932.png -------------------------------------------------------------------------------- /img/image-20210408232901834.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210408232901834.png -------------------------------------------------------------------------------- /img/image-20210620231859052.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210620231859052.png -------------------------------------------------------------------------------- /img/image-20210727225916008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210727225916008.png -------------------------------------------------------------------------------- /img/image-20210728143840779.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210728143840779.png -------------------------------------------------------------------------------- /img/image-20210728144033706.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210728144033706.png -------------------------------------------------------------------------------- /img/image-20210728144155409.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210728144155409.png -------------------------------------------------------------------------------- /img/image-20210728144401013.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210728144401013.png -------------------------------------------------------------------------------- /img/image-20210728145422282.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210728145422282.png -------------------------------------------------------------------------------- /img/image-20210921002044181.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210921002044181.png -------------------------------------------------------------------------------- /img/image-20210921002129703.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210921002129703.png -------------------------------------------------------------------------------- /img/image-20210921002239797.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210921002239797.png -------------------------------------------------------------------------------- /img/image-20210921002857005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210921002857005.png -------------------------------------------------------------------------------- /img/image-20210921004428266.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210921004428266.png -------------------------------------------------------------------------------- /img/image-20210921010008348.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/image-20210921010008348.png -------------------------------------------------------------------------------- /img/pqmn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/pqmn.jpg -------------------------------------------------------------------------------- /img/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/qrcode.jpg -------------------------------------------------------------------------------- /img/serviceLifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/serviceLifecycle.png -------------------------------------------------------------------------------- /img/strip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/strip.gif -------------------------------------------------------------------------------- /img/viewGroupUml.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/viewGroupUml.jpg -------------------------------------------------------------------------------- /img/webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/webp -------------------------------------------------------------------------------- /img/中介者模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/中介者模式.jpg -------------------------------------------------------------------------------- /img/举个栗子.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/举个栗子.jpg -------------------------------------------------------------------------------- /img/事件分发图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/事件分发图.png -------------------------------------------------------------------------------- /img/六大设计原则.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/六大设计原则.png -------------------------------------------------------------------------------- /img/原型模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/原型模式.jpg -------------------------------------------------------------------------------- /img/吐槽代码1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/吐槽代码1.png -------------------------------------------------------------------------------- /img/命令模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/命令模式.jpg -------------------------------------------------------------------------------- /img/备忘录模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/备忘录模式.jpg -------------------------------------------------------------------------------- /img/工厂方法模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/工厂方法模式.jpg -------------------------------------------------------------------------------- /img/抽象工厂模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/抽象工厂模式.jpg -------------------------------------------------------------------------------- /img/时间轴.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/时间轴.png -------------------------------------------------------------------------------- /img/模板模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/模板模式.jpg -------------------------------------------------------------------------------- /img/盒子.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/盒子.jpg -------------------------------------------------------------------------------- /img/组合 uml.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/组合 uml.jpg -------------------------------------------------------------------------------- /img/观察者模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/观察者模式.jpg -------------------------------------------------------------------------------- /img/访问者.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/访问者.jpg -------------------------------------------------------------------------------- /img/责任链模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/责任链模式.jpg -------------------------------------------------------------------------------- /img/迭代器模式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samwangds/AndroidInterview/50b02f53da425cd31da8a22f94f322ae988ab544/img/迭代器模式.jpg -------------------------------------------------------------------------------- /startActivity 都不知道还敢来面试?.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | 5 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 6 | 7 | **面试官**️:说说 startActivity 吧 8 | 9 | 😎:startActivity 有什么好说的,intent 里指定一下你想启动的界面,直接调 startActivity 方法就行了呗 10 | 11 | **面试官**:了解 AMS 吗,说说 startActivity 流程中 AMS 的作用 12 | 13 | 😎:AMS 是 Framework 层的东西,系统工程师需要掌握,我是应用开发工程师,没必要了解的 14 | 15 | **面试官**:好的,回去等通知吧 16 | 17 | --- 18 | 19 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 20 | 21 | **面试官**:说说 startActivity 吧 22 | 23 | 😨:具体的执行流程我不清楚,但我知道大概是要跨进程和 AMS 通信的 24 | 25 | **面试官**:startActivity 方法一定会发生跨进程吗? 26 | 27 | 😨:嗯... 启动本进程中的 Activity 的不需要跨进程,启动其他进程的 Activity 才需要 28 | 29 | **面试官**:好的,回去等通知吧 30 | 31 | --- 32 | 33 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 34 | 35 | **面试官**:说说 startActivity 吧 36 | 37 | 🤔️:startActivity 主要就是应用进程与 system_server 进程的 AMS 通信,AMS 是实际来管理 Activity 组件的,负责处理启动模式,维护 Activity 栈等工作。startActivity 的大概流程就是由应用进程 IPC 调用到 AMS,AMS 处理完这些工作后再 IPC 回到应用进程,创建 Activity 的实例,回调 Activity 的生命周期。 38 | 39 | **面试官**:通过什么实现跨进程的呢? 40 | 41 | 🤔️:都是通过 AIDL 接口,App 进程到 systemserver 进程是通过 IActivityServerManager.aidl ,systemserver 到 App 进程通过 IApplicationThread.aidl 42 | 43 | **面试官**:startActivity 时前后 Activity 的生命周期是怎样的? 44 | 45 | 🤔️:旧 Activity 的 onPause 会先执行,然后新 Activity 依次执行 onCreate、onStart、onResume,随后再执行旧 Activity 的 onStop... 46 | 47 | **面试官**:旧 Activity 的 onPause 一定会先执行吗,为什么? 48 | 49 | 🤔️:这主要是 AMS 来控制的,它会先后将前一个 Activity 的 onPause 事务和新 Activity 的启动事务发送给 App 进程,而在 App 端由 IApplicationThread.aidl 接受到后,会入队到 ActivityThread.H 的消息队列中,这个也是主线程消息队列,在队列上自然就实现了先后顺序的控制 50 | 51 | **面试官**:了解插件化吗,知道怎么启动一个插件中的 Activity 吗? 52 | 53 | 🤔️:主要需要解决的问题是 Activity 未在 manifest 中注册的问题,因为在 AMS 中会检查 Activity 是否注册,而这个检查逻辑处于 systemserver 进程,我们是无法 hook 的,可以在 manifest 中提前注册一个占位 Activity,然后 startActivity 时进入到 systemserver 进程之前,hook 把未注册的 Activity 改为占位 Activity,AMS 检测就可以通过,然后再回到 App 进程后,把这个占位 Activity 再换成插件 Activity 54 | 55 | **面试官**:可以,我们再来聊聊别的。 -------------------------------------------------------------------------------- /一组视频时间轴控件.md: -------------------------------------------------------------------------------- 1 | ![时间轴](../img/android公众号/时间轴.png) 2 | 3 | > 之前说会分享实用的自定义控件,一晃好长时间过去了,抽空把项目迭代不用的控件整理出来,分享一下。 4 | 5 | ## 项目的主要内容有: 6 | 7 | #### 标签,可显示文字/图片 8 | 9 | #### 主轴,按时间轴缩放值抽取一定的帧显示 10 | 11 | #### 时间选择控件,可以选择标签/视频的时长 12 | 13 | 14 | 15 | 查看原文跳转到 github 源码 16 | 17 | -------------------------------------------------------------------------------- /三个编码小技巧.md: -------------------------------------------------------------------------------- 1 | 在阅读 Framework 源码时会看到一些简洁而优雅的编码写法,下面分享三个非常简单的 "小技巧"。 2 | 3 | ## 更灵活的单例 4 | 5 | 在获取 AMS、WMS 等远程 binder 服务时,Framework 中会通过一个 Singleton 缓存它的单例,该类十分简单,如下: 6 | 7 | ```java 8 | public abstract class Singleton { 9 | private T mInstance; 10 | protected abstract T create(); 11 | public final T get() { 12 | synchronized (this) { 13 | if (mInstance == null) { 14 | mInstance = create(); 15 | } 16 | return mInstance; 17 | } 18 | } 19 | } 20 | ``` 21 | 缓存 WMS binder 服务: 22 | ```java 23 | private static Singleton sWindowManager = 24 | new Singleton() { 25 | protected IWindowManager create() { 26 | return IWindowManager.Stub 27 | .asInterface(ServiceManager.getService("window")); 28 | } 29 | }; 30 | ``` 31 | 这种泛型单例相比一般的饿汉、DCL 方式有什么优点呢?它可以更灵活的定义单例的范围,可以在某模块中实现单例,而不再是整个进程。 32 | 33 | 如果让你讲一下 Framework 中涉及的设计模式,是不是又多了一个答案呢? 34 | 35 | ## 监听 GC 36 | 37 | 如何在代码中监听程序发生 GC 呢? 38 | 39 | 你可能会想到创建一个弱引用,判断其被回收就代表发生了 GC,那要何时判断呢?无限循环判断岂不是增加了 CPU 的消耗? 40 | 41 | 别忘了存在感很低的 finalize 方法,Framework 中是这样实现的: 42 | 43 | ```java 44 | public class GcWatchDog { 45 | private static WeakReference sGcWatcher 46 | = new WeakReference<>(new GcWatcher()); 47 | private static final ArrayList sGcWatchers 48 | = new ArrayList<>(); 49 | private static Runnable[] sTmpWatchers = new Runnable[1]; 50 | private static long sLastGcTime; 51 | static final class GcWatcher { 52 | @Override 53 | protected void finalize() throws Throwable { 54 | sLastGcTime = SystemClock.uptimeMillis(); 55 | synchronized (sGcWatchers) { //锁的粒度尽量小 56 | sTmpWatchers = sGcWatchers.toArray(sTmpWatchers); 57 | } 58 | for (Runnable sTmpWatcher : sTmpWatchers) { 59 | if (sTmpWatcher != null) { 60 | sTmpWatcher.run(); 61 | } 62 | } 63 | //重建实现继续监听 64 | sGcWatcher = new WeakReference<>(new GcWatcher()); 65 | } 66 | } 67 | //添加 GC 监听 68 | public static void addGcWatcher(Runnable watcher) { 69 | synchronized (sGcWatchers) { 70 | sGcWatchers.add(watcher); 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## 打印方法耗时 77 | 78 | 你是怎么记录程序执行耗时的呢?除了 System.currentTimeMillis() - startTimeMs 外还有什么方式呢?在 Framework 中可以看到大量的 trace 语句,比如 SystemServiceManager 中的 startService 方法: 79 | 80 | ```java 81 | public T startService(Class serviceClass) { 82 | try { 83 | final String name = serviceClass.getName(); 84 | Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, 85 | "StartService " + name); 86 | ...省略 87 | } finally { 88 | Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); 89 | } 90 | } 91 | ``` 92 | traceBegin 方法为 @hide,我们无法直接使用,但是可以通过 beginSection、endSection 方法实现: 93 | 94 | Trace beginSection 方式: 95 | ```java 96 | Trace.beginSection("tagName"); 97 | doAWork(); 98 | Trace.endSection(); 99 | ``` 100 | 101 | 如上使用 Trace beginSection 方式,可以通过 systrace 生成一个 trace.html,然后可以指定查看 "tagName" 相关的执行信息。 102 | 103 | 另外还可以通过 Debug startMethodTracing 方式: 104 | ```java 105 | Debug.startMethodTracing(); 106 | doBWork(); 107 | Debug.startMethodTracing(); 108 | ``` 109 | 当使用 Debug startMethodTracing 方式时,会生成一个 trace 文件,然后可以用 DDMS 工具打开 trace 文件,查看具体的方法耗时。 110 | 111 | 这两种方式不仅是代码层面,而是需要结合 systrace 等工具一同使用。 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /中介者模式.md: -------------------------------------------------------------------------------- 1 | > 近来长租公寓频频暴雷,租房中这些暴雷的扮演的是租房者与房东之间的中介者。不过在单纯的代码世界,中介者模式不但不会暴雷,而且还很好用。 2 | 3 | ## 定义 4 | 5 | 中介者模式( Mediator Pattern)属于行为型模式;中介者包装了一系列对象相互作用的方式,使得这些对象不必显式的相互作用,从而使它们可以松散耦合,而且可以独立地改变它们之间的交互。 6 | 7 | ## 使用场景 8 | 9 | - 对象间的交互 操作很多,且每个对象的行为操作都依赖彼此时,为了防止在修改一个对象的行为时,同时涉及修改很多其它对象的行为 10 | - 中介者模式将对象之间的多对多关系,变成中介者与这些对象的一对多关系,降低系统复杂性,提高扩展性。 11 | - 多个类相互耦合,形成了网状结构。使用中介者模式之后将网状结构分离为星型结构。 12 | 13 | ![img](https://images0.cnblogs.com/blog/383187/201409/131719258716505.png) 14 | 15 | ![img](https://images0.cnblogs.com/blog/383187/201409/131726284813262.png) 16 | 17 | ## UML 18 | 19 | ![中介者模式](img/中介者模式.jpg) 20 | 21 | 从生活中例子自然知道,中介者模式设计两个具体对象,一个是用户类(同事类),另一个是中介者类,根据针对接口编程原则,则需要把这两类角色进行抽象,所以中介者模式中就有了4类角色,它们分别是: 22 | 23 | - Mediator: 抽象中介者角色 24 | - 具体中介者角色 25 | - 抽象同事类 26 | - 具体同事类 27 | 28 | 中介者类是起到协调各个对象的作用,而抽象中介者角色中则需要保存各个对象的引用。 29 | 30 | ## 举例 31 | 32 | 我以生活中的租房为例,角色有租客、中介、房东。为了简便我们直接省略了抽象层: 33 | 34 | ```KOTLIN 35 | // 租客 36 | class Tenant(var mediator: Mediator) { 37 | fun renting() { 38 | println("租客去找中介租房子") 39 | mediator.renting() 40 | } 41 | 42 | fun pay() { 43 | println("租客付房租给中介") 44 | } 45 | } 46 | 47 | // 房东 48 | class Landlord(var mediator: Mediator) { 49 | fun collection() { 50 | println("房东去找中介要房租") 51 | mediator.collection() 52 | } 53 | fun rentOut() { 54 | println("房东通过中介把房子租出去了") 55 | } 56 | } 57 | 58 | class Mediator { 59 | var landlord: Landlord? = null 60 | var tenant: Tenant? = null 61 | 62 | fun renting() { 63 | landlord?.rentOut()?: println("没有房东出租房子,要睡大街了~") 64 | } 65 | 66 | fun collection() { 67 | tenant?.pay() ?: print("房子还未出租出去,没钱收哦~") 68 | } 69 | } 70 | ``` 71 | 72 | 来运行一下测试代码 73 | 74 | ```kotlin 75 | fun main() { 76 | // 先要有个中介 77 | val mediator = Mediator() 78 | // 来了个租客找中介 79 | val tenant = Tenant(mediator) 80 | mediator.tenant = tenant 81 | tenant.renting() // 还没有房东的时候租房 82 | 83 | val landlord = Landlord(mediator) 84 | mediator.landlord = landlord 85 | tenant.renting() // 有了房东再租房 86 | landlord.collection() 87 | 88 | println("\n租客退租啦!") 89 | mediator.tenant = null 90 | landlord.collection() 91 | } 92 | 93 | // 运行结果 ----- 94 | 租客去找中介租房子 95 | 没有房东出租房子,要睡大街了~ 96 | 租客去找中介租房子 97 | 房东通过中介把房子租出去了 98 | 房东去找中介要房租 99 | 租客付房租给中介 100 | 101 | 租客退租啦! 102 | 房东去找中介要房租 103 | 房子还未出租出去,没钱收哦~ 104 | ``` 105 | 106 | 和现实生活一样,租客只要找到中介就能租房子,而不用管谁有房子。 107 | 108 | 同样房东只要找中介就能出租房子;租客退租之后中介又会再找新的租客,房东只管收房租就可以了 109 | 110 | ## 安卓中的应用 111 | 112 | Android 源码中的命名相当规范,所以我们要找中介者模式,只要在源码中搜索 **Mediator**, 其中就有一个 `KeyguardViewMediator`类 113 | 114 | ```JAVA 115 | public class KeyguardViewMediator extends SystemUI { 116 | private AlarmManager mAlarmManager; 117 | private AudioManager mAudioManager; 118 | private StatusBarManager mStatusBarManager; 119 | .... 120 | } 121 | ``` 122 | 123 | - KeyguardViewMediator 是锁屏业务的中介者; 124 | - 里面的各种 XXManager 是“同事类” 125 | 126 | 127 | 128 | ## 总结 129 | 130 | - 优点:减少多个类之间的联系,使之逻辑变清晰,松耦合 131 | - 缺点:类太多时中介者可能太庞大(考虑拆分多个中介者);类之间的联系本身不复杂时,加入中介者可能使逻辑结构反而变得复杂。 132 | 133 | 要写好你的“中介者”,这样你的代码才不会暴雷哦~ -------------------------------------------------------------------------------- /你了解 Android 系统启动流程吗?.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 5 | 6 | **面试官**️:你了解 Android 系统启动流程吗? 7 | 8 | 😎:系统第一个启动的是 init 进程,然后 init 进程会再启动 Zygote、service manager、system_server 这些比较关键的服务进程。 9 | 10 | **面试官**:system_server 是由 init 进程启动的吗? 11 | 12 | 😎:是的,但不用在意这些细节,没啥用。 13 | 14 | **面试官**:好的,回去等通知吧 15 | 16 | --- 17 | 18 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 19 | 20 | **面试官**:你了解 Android 系统启动流程吗? 21 | 22 | 😨:系统首先会启动 init 进程,然后 init 进程会通过 init.rc 脚本做一些初始化工作,启动一些比较重要的服务进程,包括 Zygote、service manager 等。 23 | 24 | **面试官**:system_server 进程是什么时候启动的? 25 | 26 | 😨:system_server 是在 Zygote 进程中启动的。 27 | 28 | **面试官**:为什么要在 Zygote 中启动,而不是由 init 直接启动呢? 29 | 30 | 😨:嗯... 这个不清楚了,我只是大概了解关键服务进程的启动顺序,再深入的就没有去学过了 31 | 32 | **面试官**:好的,回去等通知吧 33 | 34 | --- 35 | 36 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 37 | 38 | **面试官**:你了解 Android 系统启动流程吗? 39 | 40 | 🤔️:当按电源键触发开机,首先会从 ROM 中预定义的地方加载引导程序 BootLoader 到 RAM 中,并执行 BootLoader 程序启动 Linux Kernel, 然后启动用户级别的第一个进程: init 进程。 41 | 42 | init 进程会解析 init.rc 脚本做一些初始化工作,包括挂载文件系统、创建工作目录以及启动系统服务进程等,其中系统服务进程包括 Zygote、service manager、media 等。 43 | 44 | 在 Zygote 中会进一步去启动 system_server 进程,然后在 system_server 进程中会启动 AMS、WMS、PMS 等服务,等这些服务启动之后,AMS 中就会打开 Launcher 应用的 home Activity,最终就看到了手机的 "桌面"。 45 | 46 | **面试官**:system_server 为什么要在 Zygote 中启动,而不是由 init 直接启动呢? 47 | 48 | 🤔️:Zygote 作为一个孵化器,可以提前加载一些资源,这样 fork() 时基于 Copy-On-Write 机制创建的其他进程就能直接使用这些资源,而不用重新加载。比如 system_server 就可以直接使用 Zygote 中的 JNI 函数、共享库、常用的类、以及主题资源。 49 | 50 | **面试官**:为什么要专门使用 Zygote 进程去孵化应用进程,而不是让 system_server 去孵化呢? 51 | 52 | 🤔️:首先 system_server 相比 Zygote 多运行了 AMS、WMS 等服务,这些对一个应用程序来说是不需要的。另外进程的 fork() 对多线程不友好,仅会将发起调用的线程拷贝到子进程,这可能会导致死锁,而 system_server 中肯定是有很多线程的。 53 | 54 | **面试官**:能说说具体是怎么导致死锁的吗? 55 | 56 | fork() 时只会把调用线程拷贝到子进程、其他线程都会立即停止,那如果一个线程在 fork() 前占用了某个互斥量,fork() 后被立即停止,这个互斥量就得不到释放,再去请求该互斥量就会发生死锁了。 57 | 58 | **面试官**:Zygote 为什么不采用 Binder 机制进行 IPC 通信? 59 | 60 | 🤔️:Binder 机制中存在 Binder 线程池,是多线程的,如果 Zygote 采用 Binder 的话就存在上面说的 fork() 与 多线程的问题了。 61 | 62 | 其实严格来说,Binder 机制不一定要多线程,所谓的 Binder 线程只不过是在循环读取 Binder 驱动的消息而已,只注册一个 Binder 线程也是可以工作的,比如 service manager 就是这样的。 63 | 64 | 实际上 Zygote 尽管没有采取 Binder 机制,它也不是单线程的,但它在 fork() 前主动停止了其他线程,fork() 后重新启动了。 65 | 66 | **面试官**:可以,我们再来聊聊别的。 -------------------------------------------------------------------------------- /你真的懂 Context 吗?.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 5 | 6 | **面试官**:Android 开发经常接触到的 Context 是什么呢? 7 | 8 | 😎:Context 是一个关于应用环境的抽象类,它的实现由安卓系统提供。用来访问一些应用内资源、类,也可以调用系统服务开启 Activity 、Service 、发送和接收广播等 9 | 10 | **面试官**:那一个应用里有几个 Context 呢? 11 | 12 | 😎:四大组件里 Activity 和 Service 都是 Context , 应用的 Context 数就是 Activity 、Service、Application 的个数之和,顺便说一下 Application 如果是多进程应用就会有多个 13 | 14 | **面试官**:那它们有什么区别呢? 15 | 16 | 😎:好像都差不多,平常开发的时候用哪个 Context 效果都一样,主要不同就是 Application 的生命周期和应用一样,所以在初始化一些第三方库的时候如果要传 Context 要用 Application 17 | 18 | **面试官**:说到 Application , getApplication 和 getApplicationContext 有什么区别? 19 | 20 | 😎:没区别~ 21 | 22 | **面试官**:好的,回去等通知吧 23 | 24 | --- 25 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 26 | 27 | **面试官**:Android 有哪些类型的 Context ,它们有什么区别 28 | 29 | 😨:应用里有 Activity 、Service、Application 这些 Context ,我们先说说它们的共同点,它们都是 ContextWrapper 的子类,而 ContextWrapper 的成员变量 mBase 可以用来存放系统实现的 ContextImpl,这样我们在调用如 Activity 的 Context 方法时,都是通过静态代理的方式最终调用到 ContextImpl 的方法。我们调用 ContextWrapper 的 getBaseContext 方法就能拿到 ContextImpl 的实例 30 | 31 | 再说它们的不同点,它们有各自不同的生命周期;在功能上,只有 Activity 显示界面,正因为如此,Activity 继承的是 ContextThemeWrapper 提供一些关于主题,界面显示的能力,间接继承了 ContextWrapper ; 而 Applicaiton 、Service 都是直接继承 ContextWrapper ,所以我们要记住一点,凡是跟 UI 有关的,都应该用 Activity 作为 Context 来处理,否则要么会报错,要么 UI 会使用系统默认的主题。 32 | 33 | **面试官**: 在 Activity 里,this 和 getBaseContext 有什么区别 34 | 35 | 😨:this 呢,指的就是 Activity 本身的这个实例,而 getBaseContext ,是 Activity 间接继承的 ContextWrapper 的一个方法,用来返回系统提供的 ContextImpl 对象 36 | 37 | 38 | **面试官**:getApplication 和 getApplicationContext 有什么区别? 39 | 40 | 😨:用起来没区别,都是返回应用的 Application 对象;但是从来源上, getApplication 是 Activity 、 Service 里的方法, 而 getApplicationContext 则是 Context 里的抽象方法,所以能调用到的它们的地方不一样。 41 | 42 | **面试官**:ContextImpl 实例是什么时候生成的,在 Activity 的 onCreate 里能拿到这个实例吗 43 | 44 | 😨:这个都是系统处理的,具体时机没有跟进去看。onCreate 是 Activity 的第一个生命周期回调,应该还没准备好,不能获取吧。 45 | 46 | **面试官**:好的,回去等通知吧 47 | 48 | --- 49 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 50 | 51 | **面试官**:ContextImpl 实例是什么时候生成的,在 Activity 的 onCreate 里能拿到这个实例吗 52 | 53 | 🤔️:先说结论,可以。我们开发的时候,经常会在 onCreate 里拿到 Application,如果用 getApplicationContext 取,最终调用的就是 ContextImpl 的 getApplicationContext 方法,如果调用的是 getApplication 方法,虽然没调用到 ContextImpl ,但是返回 Activity 的成员变量 mApplication 和 ContextImpl 的时机是一样的。 54 | 55 | 再说下它的原理,Activity 真正开始启动是从 ActivityThread.performLaunchActivity 开始的,这个方法做了这些事: 56 | * 调用 createBaseContextForActivity 方法去创建 ContextImpl 57 | * 通过 ClassLoader 去加载目标 Activity 的类,从而创建 对象 58 | * 从 packageInfo 里获取 Application 对象 59 | * 调用 activity.attach ( contextImpl , application) 这个方法就把 Activity 和 Application 以及 ContextImpl 关联起来了,就是上面结论里说的时机一样 60 | * 最后调用 activity.onCreate 生命周期回调 61 | 62 | 通过以上的分析,我们知道了 Activity 是先初始化 Context ,再创建类,最后调用 onCreate , 从而得出问题的答案。 63 | 不仅 Activity 是这样, Application 、Service 里的 Context 初始化也都是这样的。 64 | 65 | **面试官**:那 ContentProvider 里的 Context 又是什么时候初始化的呢? 66 | 67 | 🤔️:ContentProvider 本身不是 Context ,但是它有一个成员变量 mContext ,是通过构造函数传入的。那么这个问题就变成了,ContentProvider 什么时候创建。 68 | 应用创建 Application 是通过调用 ActivityThread.handleBindApplication 方法,这个方法的相关流程有: 69 | * 创建 Application 70 | * 初始化 Application 的 Context 71 | * 调用 installContentProviders 并传入刚创建好的 Application 来创建 ContentProvider 72 | * 调用 Application.onCreate 73 | 74 | 得出结论,ContentProvider 的 Context 是在 Applicaiton 创建之后,但是 onCreate 方法调用之前初始化的 75 | 76 | **面试官**:四大组件就剩 BroadcastReceiver ,说说它方法里的 Context 是哪来的 77 | 78 | 🤔️:广播接收器,分动态注册和静态注册。 79 | 动态注册很简单,在调用 Context.registerReceiver 动态注册 BroadcastReceiver 时,会生成一个 ReceiverDispatcher 会持有这个 Context ,这样当有广播分发到它时,调用 onReceiver 方法就可以把 Context 传递过去了。当然,这也是为什么不用的时候要 unregisterReceiver 取消注册,不然这个 Context 就泄漏了哦。 80 | 静态注册时,在分发的时候最终调用的是 ActivityThread.handleReceiver ,这个方法直接通过 ClassLoader 去创建一个 BroadcastReceiver 的对象,而传递给 onReceiver 方法的 Context 则是通过 context.getReceiverRestrictedContext() 生成的一个以 Application 为 mBase 的 ContextWrapper 。注意这边的 Context 不是 Application 哈。 81 | 82 | **面试官**:可以,我们再来聊聊别的。 83 | 84 | --- 85 | 86 | > 看完了这三位同学的面试表现,你有什么感想呢?欢迎关注 “Android 面试官” 在公众号后台留言讨论 87 | 88 | ![img](img/qrcode.jpg) 89 | 90 |
听说🤔️同学也关注我了哦
91 | 92 | -------------------------------------------------------------------------------- /做了三年安卓,我竟然不知道什么是 UI 线程.md: -------------------------------------------------------------------------------- 1 | **面试官**:说说什么是 UI 线程? 2 | 3 | 😏:就是用来刷新 UI 所在的线程嘛 4 | 5 | **面试官**:多说点 6 | 7 | 😏:UI 是单线程刷新的,如果多个线程可以刷新 UI 就无所谓是不是 UI 线程了,单线程的好处是,UI 框架里不需要到处上锁,做线程同步,写起来也比较简单有效 8 | 9 | **面试官**:你说的这个 UI 线程,它到底是哪个线程?是主线程吗? 10 | 11 | 😏:拿 Activity 来说,我们在 Activity 里异步做完耗时操作,要刷新 UI 可以调用 Activity.runOnUiThread 方法,在 UI 线程中执行,那么我们看下这个方法自然就知道 UI 线程是哪个线程了。 12 | 13 | ```java 14 | public final void runOnUiThread(Runnable action) { 15 | if (Thread.currentThread() != mUiThread) { 16 | mHandler.post(action); 17 | } else { 18 | action.run(); 19 | } 20 | } 21 | ``` 22 | 这个方法会判断当前是不是在主线程,不是呢就通过 mHandler 抛到主线程去执行。 23 | 这个 mHandler 是 Activity 里的一个全局变量,在 Activity 创建的时候通过无参构造函数 `new Handler()` 一起创建了。因为是无参,所以创建时用的哪个线程,Handler 里的 Looper 用的就是哪个线程了。创建 Activity 是在应用的主线程,因此 mHandler.post 去执行的线程也是主线程。 24 | 刚也说 了,runOnUiThread 方法里,先判断是不是在 UI 线程,这个 mUiThread 又是什么时候赋值的呢,答案还在 Activity 的源码里 25 | ```java 26 | final void attach(Context context, ...) { 27 | ...省略无关代码 28 | mUiThread = Thread.currentThread(); 29 | } 30 | ``` 31 | 在 Activity.attach 方法里,我们把当前线程赋值给 mUiThread,那当前线程是什么线程呢,也是主线程。至于为什么创建 Activity 和 attach 都是主线程,那又是另外一个故事了 32 | 通过前面的分析,我们知道了,对于 Activity 来讲 UI 线程就是主线程 33 | 34 | **面试官**:所以你的结论是 UI 线程就是主线程? 35 | 36 | 😏:这是你说的,记住这个开发的时候不会错,但是不够准确。在子线程里刷新 UI 的时候会抛一个异常 `ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.` 大意是只有最初始创建 View 层级关系的线程可以 touch view,这里指的也就是 ViewRootImpl 创建时所在的线程,严格来说这个线程不一定是主线程。这一点呢,读 View.post 方法也可以得到相同的结论。所以对于 View 来说,UI 线程就是 ViewRootImpl 创建时所在的线程,Activity 的 DecorView 对应的 ViewRootImpl 是在主线程创建的 37 | 38 | **面试官**:这个 ViewRootImpl 什么时候创建 39 | 40 | 😏:Activity 创建好之后,应用的主线程会调用 ActivityThread.handleResumeActivity,这个方法会把 Activity 的 DecorView 添加到 WindowManger 里,就是在这个时候创建的 ViewRootImpl 41 | 42 | **面试官**:那可以在异步线程刷新 View 吗? 43 | 44 | 😏:刚才我们说了,只要是 ViewRootImpl 创建的线程就可以 touch view,然后 WindowManger.addView 的时候又会去创建 ViewRootImpl,所以我们只要在子线程调用 WindowManger.addView,这个时候添加的这个 View,就只能在这个子线程刷新了,这个子线程就是这个 View 的 UI 线程了。 45 | 46 | **面试官**:好,我们再聊点别的 -------------------------------------------------------------------------------- /单例的线程安全及序列化问题.md: -------------------------------------------------------------------------------- 1 | 单例模式可以说是最简单的设计模式了,但也有一些细节需要注意。本文从几个问题出发,分析一下单例模式的各种姿势,以便在今后使用时游刃有余,**面试时从容不迫**。 2 | 3 | - 饿汉方式到底有多饿? 4 | - 静态内部类为什么是延迟加载的? 5 | - 枚举方式单例是延迟加载的吗? 6 | - 饿汉、静态内部类、枚举方式单例为什么是线程安全的? 7 | - 序列化为什么会破坏单例模式? 8 | - 怎么防止序列化破坏单例模式? 9 | - 枚举方式单例是怎么避免序列化破坏的? 10 | 11 | ## 饿汉方式 12 | 13 | 先来看一下饿汉方式单例: 14 | 15 | ```java 16 | public class Singleton { 17 | 18 | private static Singleton instance = new Singleton(); 19 | 20 | private Singleton() { } 21 | 22 | public static Singleton getInstance() { 23 | return instance; 24 | } 25 | } 26 | ``` 27 | 饿汉方式往往会跟懒汉方式一起提起,对比而言,懒汉方式具有延迟实例化的优点。这容易让人对饿汉方式有一个恶劣的刻板印象:它的性能很不好,没有使用它的时候它就会初始化,白白占用资源! 28 | 29 | 来思考一下,饿汉方式单例到底有多“饿”?它到底什么时候会初始化呢?我们知道类加载的时候会初始化静态资源,所以饿汉方式的实例化时机就是类加载时机,回顾一下类加载的时机: 30 | 31 | - 使用 new 关键字实例化 32 | - 调用一个类的静态方法 33 | - 读取一个类的静态字段(被 final 修饰已在编译期把结果放在常量池的静态字段除外) 34 | 35 | 如果你的单例只暴露了 getInstance() 方法,没有其他的 public static 成员或方法,那饿汉方式就同懒汉方式一样都是延迟加载的。 36 | 37 | 如果你认为只暴露 getInstance() 方法是单例规范的写法,那就放心的使用饿汉方式吧! 38 | 39 | 如果别人 diss 了你的饿汉方式,那就 diss back 让他去复习类加载机制! 40 | 41 | 如果别人玷污了你的单例,添加了其他的 public static 方法/字段,那就算了放弃吧... 42 | 43 | ## 静态内部类 44 | 45 | 静态内部类方式单例如下: 46 | 47 | ```java 48 | public class Singleton { 49 | 50 | private static class SingletonHolder { 51 | private static final Singleton INSTANCE = new Singleton(); 52 | } 53 | 54 | private Singleton() {} 55 | 56 | public static Singleton getInstance() { 57 | return SingletonHolder.INSTANCE; 58 | } 59 | } 60 | ``` 61 | 62 | 相比饿汉方式,这种方式即使加载了 Singleton 类,也不会创建 Singleton 实例,因为 Singleton 的静态变量放到了内部类中,只有内部类被加载了,Singleton 实例才会被创建。 63 | 64 | 如果别人在你的 SingletonHolder 中添加了 public static 方法/字段,请一定要下重手。 65 | 66 | ## 枚举 67 | 68 | 枚举方式实现的单例如下: 69 | 70 | ```java 71 | public enum Singleton { 72 | 73 | INSTANCE; 74 | 75 | public static Singleton getInstance() { 76 | return INSTANCE; 77 | } 78 | } 79 | ``` 80 | 81 | 这种形式我们无从下手,反编译后就明白了,相当于: 82 | 83 | ```java 84 | public class Singleton { 85 | 86 | public static final Singleton INSTANCE; 87 | 88 | static{ 89 | INSTANCE = new Singleton(); 90 | } 91 | } 92 | ``` 93 | 94 | 可以看到,枚举方式实现的单例和饿汉方式差不多,实例化时机就是类加载时机。 95 | 96 | ## 线程安全 97 | 98 | 类加载的逻辑位于 synchronized 代码块中,是线程安全的,而饿汉、静态内部类以及枚举方式实现的单例实例化都处于类加载时机,所以它们都是线程安全的。 99 | 100 | 懒汉方式的初始化与类加载时机无关,所以要自行保证线程安全。 101 | 102 | # 序列化 103 | 104 | 我们期望单例模式可以实现只创建一个实例,通过特殊手段创建出其他的实例,就对单例模式造成了破坏,序列化就会破坏单例模式。 105 | 106 | 假如我们的单例实现了 serializable 接口,反序列化时会通过反射调用无参构造方法创建一个新的实例,这时就要重写 readResolve 方法防止序列化破坏单例,如下: 107 | 108 | ```java 109 | public class Singleton implements Serializable { 110 | 111 | private static class SingletonHolder { 112 | private static final Singleton INSTANCE = new Singleton(); 113 | } 114 | 115 | private Singleton() { 116 | } 117 | 118 | public static Singleton getInstance() { 119 | return SingletonHolder.INSTANCE; 120 | } 121 | 122 | //防止序列化破坏单例模式 123 | public Object readResolve() { 124 | return SingletonHolder.INSTANCE; 125 | } 126 | } 127 | ``` 128 | 129 | 普通 Java 类的反序列化时中,会通过反射创建新的实例。而枚举在序列化的时候仅是将枚举对象的 name 属性输出到结果中,反序列化的时候通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制进行定制的:禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法。 130 | 131 | 也就是说,枚举的反序列化不是通过反射实现的,所以不会导致单例模式被破坏的问题。 132 | 133 | 最后总结回答开头提出的问题: 134 | 135 | - 饿汉方式到底有多饿? 136 | 取决于类什么时候加载 137 | - 静态内部类为什么是延迟加载的? 138 | 取决于内部类什么时候加载 139 | - 枚举方式单例是延迟加载的吗? 140 | 与饿汉方式一样,取决于类什么时候加载 141 | - 饿汉、静态内部类、枚举方式单例为什么是线程安全的? 142 | 因为它们在类加载时实例化,而类加载是线程安全的 143 | - 序列化为什么会破坏单例模式? 144 | 普通 Java 类反序列化时,会通过反射创建新的实例 145 | - 怎么防止序列化破坏单例模式? 146 | 重写 readResolve 方法或使用枚举 147 | - 枚举方式单例是怎么避免序列化破坏的? 148 | 依赖枚举自身特殊的序列化机制 149 | -------------------------------------------------------------------------------- /原型模式.md: -------------------------------------------------------------------------------- 1 | > 原型模式是一个创建型的模式,原型二字表明了该模式应该有一个样板实例,用户从这个对象中复制出一个内部属性一致的对象,俗称“克隆”,被复制的实例就是我们所称的原型。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可以使程序更加高效 2 | 3 | ## 定义 4 | 5 | 用原型实例指定创建对象的种类,并通过复制这些原型创建新的对象。 6 | 7 | ## 使用场景 8 | 9 | - 类初始化需要消耗非常多的资源,包括数据 、硬件资源等,通过原型复制避免这些消耗 10 | - 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限 11 | - 一个对象需要提供给其它对象访问,而且各个调用者可能都需要修改其值,可以考虑用原型模式复制多个对象供调用者使用,即保护性拷贝 12 | 13 | 注意:通过实现 Cloneable 接口的原型模式在调用 clone 函数来克隆实例,并不一定比 new 操作来得快。只有当 new 构造对象较为耗时或者说成本较高时,通过 clone 才有收益。 14 | 15 | 当然 实现原型模式也不一定非要实现 Cloneable 接口,其它实现方式我们后面会具体说明。 16 | 17 | ## UML 18 | 19 | ![原型模式](img/原型模式.jpg) 20 | 21 | 原型模式整体比较简单,涉及的角色较少: 22 | 23 | - Client: 客户端用户,调用方 24 | - Prototype:抽象类或接口,声明具备 clone 的能力 25 | - ConcretePrototype: 具体的原型类 26 | 27 | ## Code 28 | 29 | ![image-20210408232901834](img/image-20210408232901834.png) 30 | 31 | 我们以文档拷贝为例来简单演示一下原型模式,创建一个文档,经过一段时间编辑后含有大量的图文内容,但是因为各种原因需要对该文档做进一步编辑 ,可是编辑 后文档是否会审美观点采用也不确认有可能之前的版本更好。安全起见,需要将当前文档拷贝一份,然后再在文档副本上进行修改,这其实也是一种保护性的拷贝。如此这个原始的文档就是我们所说的原型。 32 | 33 | 通过 WordDocument 模拟文档中的基本元素:文字和图片 34 | 35 | ```kotlin 36 | // Cloneable 接口代表 Prototype 角色 37 | // WordDocument 文档,为 ConcretePrototype 角色 38 | class WordDocument : Cloneable { 39 | 40 | // 文本 41 | var text: String = "" 42 | // 图片列表 43 | var imgs: ArrayList = ArrayList() 44 | 45 | public override fun clone(): WordDocument { 46 | val doc = super.clone() 47 | return doc as WordDocument 48 | } 49 | 50 | fun showDocument() { 51 | println("-------- Word Document Start --------") 52 | println("Text:$text") 53 | println("Images:${imgs.joinToString(",")}") 54 | println("-------- Word Document End --------") 55 | } 56 | } 57 | ``` 58 | 59 | 接下来我们调用测试代码,即 Client 端调用 60 | 61 | ```KOTLIN 62 | fun main() { 63 | // 新建文档 64 | val originDoc = WordDocument() 65 | // 编辑的文档 66 | originDoc.text = "好的开始是成功的一半" 67 | originDoc.imgs.add("Img 1") 68 | originDoc.imgs.add("Img 2") 69 | originDoc.imgs.add("Img 3") 70 | originDoc.showDocument() 71 | 72 | // 以原始文档为原型,拷贝出副本 73 | val doc2 = originDoc.clone() 74 | // 修改文档副本 75 | doc2.text = "${doc2.text}, 然而还是要修改!" 76 | doc2.showDocument() 77 | 78 | // 修改副本不会影响原始文档 79 | originDoc.showDocument() 80 | } 81 | ``` 82 | 83 | 输出结果 : 84 | 85 | ```properties 86 | -------- Word Document Start -------- 87 | Text:好的开始是成功的一半 88 | Images:Img 1,Img 2,Img 3 89 | -------- Word Document End -------- 90 | -------- Word Document Start -------- 91 | Text:好的开始是成功的一半, 然而还是要修改! 92 | Images:Img 1,Img 2,Img 3 93 | -------- Word Document End -------- 94 | -------- Word Document Start -------- 95 | Text:好的开始是成功的一半 96 | Images:Img 1,Img 2,Img 3 97 | -------- Word Document End -------- 98 | ``` 99 | 100 | 我们看到 doc2 的修改并不会影响到 originDoc 的内容,这就保证了 origin 的安全性。 101 | 102 | 不过这里需要注意的是,通过 clone 拷贝对象时并不会执行构造函数,因此,如果在构造函数中有一些特殊的需要初始化的类型,需要自己在重新的 clone 方法里补充相关逻辑。 103 | 104 | ## 浅拷贝和深拷贝 105 | 106 | 上述原型模式的实现实际上只是一个浅拷贝,也称为影子拷贝。这份拷贝实际上并不是将原始文档的所有字段都重新构造,而是将副本文档的字段 引用原始文档的字段。即 originDoc.imgs 和 doc2.imgs 指向的其实是同一个 List 对象:如果我们调用 docs.img.add("新增图片"),那么在原始文档 originDoc.imgs 也会有这个新增的图片。 107 | 108 | 既然浅拷贝有这个问题,那么我们只需要采用深拷贝,即在拷贝对象时,对引用型的字段也要采用拷贝的形式,而不是单纯引用的形式: 109 | 110 | ```kotlin 111 | public override fun clone(): WordDocument { 112 | val doc = super.clone() as WordDocument 113 | doc.imgs = this.imgs.clone() as ArrayList 114 | return doc 115 | } 116 | ``` 117 | 118 | 这里我们只需要处理 imgs, 而不需要处理 text, 因为 text 是 String 类型,是不可变的,这样我们在给 text 赋新值时,是将其引用指向了新对象,而不是修改原来的 String 对象,这也是为什么浅拷贝时改 doc2.text 不会影响 originDoc.text 的原因。 119 | 120 | 现在我们来改一下测试代码: 121 | 122 | ```kotlin 123 | fun main() { 124 | // 新建文档 125 | val originDoc = WordDocument() 126 | // 编辑的文档 127 | originDoc.text = "好的开始是成功的一半" 128 | originDoc.imgs.add("Img 1") 129 | originDoc.imgs.add("Img 2") 130 | originDoc.imgs.add("Img 3") 131 | originDoc.showDocument() 132 | 133 | // 以原始文档为原型,拷贝出副本 134 | val doc2 = originDoc.clone() 135 | // 修改文档副本 136 | doc2.text = "${doc2.text}, 然而还是要修改!" 137 | doc2.imgs.add("新增图片") 138 | doc2.showDocument() 139 | 140 | // 修改副本不会影响原始文档 141 | originDoc.showDocument() 142 | } 143 | ``` 144 | 145 | 运行结果: 146 | 147 | ```properties 148 | -------- Word Document Start -------- 149 | Text:好的开始是成功的一半 150 | Images:Img 1,Img 2,Img 3 151 | -------- Word Document End -------- 152 | -------- Word Document Start -------- 153 | Text:好的开始是成功的一半, 然而还是要修改! 154 | Images:Img 1,Img 2,Img 3,新增图片 155 | -------- Word Document End -------- 156 | -------- Word Document Start -------- 157 | Text:好的开始是成功的一半 158 | Images:Img 1,Img 2,Img 3 159 | -------- Word Document End -------- 160 | ``` 161 | 162 | 结果与我们预期的一致,originDoc 里并没有新增的图片。 163 | 164 | 原型模式是非常简单的一个模式,它的核心就是对原始对象进行拷贝。在这个过程中需要注意的一点就是深、浅拷贝的问题。为了减少错误,建议使用原型模式时尽可能使用深拷贝。 165 | 166 | 另外一个小技巧就是,如果字段太多,clone 方法里面需要处理每个字段,非常繁琐,而且新增字段时可能遗漏。我们可以巧妙的利用序列化,将对象序列化后再序列化回来生成新的对象(不过会牺牲性能)。 167 | 168 | ## 总结 169 | 170 | 原型模式本质上就是对象的拷贝,在 java 中就是 clone 方法的实现和调用,这里就不过多举例了。 171 | 172 | #### 优点 173 | 174 | - 原型模式是在内存中二进制流的拷贝,要比直接 new 一个对象性能好很多。特别是在一个循环体内产生大量对象时,原型模式可以充分的发挥其优点 175 | - 保护性拷贝,通过返回一个新的克隆对象,防止原始对象被修改 176 | 177 | #### 缺点 178 | 179 | 直接在内存拷贝,其构造函数不会执行,实际开发中可能有潜在问题。 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /命令模式.md: -------------------------------------------------------------------------------- 1 | > 在操作系统中,我们点击“关机”命令,系统会执行一系列的操作,如先暂停处理事件,保存系统一些配置,然后结束程序进程,最后调用内核命令关闭计算机等。对于这一系列命令,用户不用去管,用户只需点击系统的关机按钮即可完成如上一系列的命令。而我们的命令模式(Command Pattern),是行为型设计模式之一,其核心也大体如此 2 | 3 | ## 定义 4 | 5 | 将一个请求封装成一个对象,从而让用户使用不同的请求把客户端参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。 6 | 7 | ## 使用场景 8 | 9 | - 需要抽象出待执行的动作,然后以参数的开工提供出来。(类似回调) 10 | - 在不同的时刻指定、排列和执行请求,一个命令对象可以有与初始请求无关的我现在期 11 | - 需要支持取消操作 12 | - 支持修改日志功能,当系统崩溃时,这些修改可以被重作一次 13 | - 需要支持事务操作 14 | 15 | ## UML 16 | 17 | ![命令模式](img/命令模式.jpg) 18 | 19 | - Receiver:接收者类角色 20 | 该类负责具体实施或执行一个请求,也就执行具体逻辑的角色。以上面的“关机”为例,其实接收角色就是真正执行各项关机逻辑的底层代码。任何一个类都可以成为一个接收者,而在接收者类中封装具体操作逻辑在 action 方法中 21 | - Command: 命令角色 22 | 定义所有具体命令类的抽象接口 23 | - ConcreteCommand: 具体命令角色 24 | 该类实现了 Command 接口,在 execute 方法中调用接收者角色的相关方法,在接收者和命令执行的具体行为之间加以弱耦合。而 execute 则通常称为执行方法,如“关机”的操作实现,具体还包括很多相关的操作,如保存数据 ,关闭文件,结束进程等。如果 将这一系列具体的逻辑处理看作是接收者,那么调用这些具体逻辑的方法就可以看作是执行方法。 25 | - Invoker: 请求者角色 26 | 该类的职责就是调用命令对象执行具体的请求,相关的方法也是 action 方法。在“关机”例子中,关机菜单命令一般就对应一个关机方法,我们点击了关机菜单后,就由这个方法去调用具体的命令执行具体的逻辑。 27 | - Client: 客户端角色 28 | 具体操作命令的角色,“关机”例子中对应的就是人了。 29 | 30 | 这个类图描述了一个命令模式通用的代码: 31 | 32 | #### 接收者类 33 | 34 | ```kotlin 35 | package com.sam.designpatterns 36 | 37 | // 命令模式 38 | // 接收者类 39 | class Receiver { 40 | // 真正执行具体命令逻辑的方法 41 | fun action() { 42 | print("执行具体操作") 43 | } 44 | } 45 | // 抽象命令接口 46 | interface Command { 47 | // 命令接口的执行方法 48 | fun execute() 49 | } 50 | 51 | /** 52 | * 具体的命令类 53 | * receiver: 持有一个接收者对象的引用 54 | */ 55 | class ConcreteCommand( 56 | private val receiver: Receiver 57 | ) : Command { 58 | override fun execute() { 59 | receiver.action() 60 | } 61 | } 62 | 63 | // 请求者类 64 | class Invoker(private val command: Command) { 65 | fun action() { 66 | // 调用具体命令对象,执行具体命令 67 | command.execute() 68 | } 69 | } 70 | 71 | class Client { 72 | fun main() { 73 | // 构造一个接收者对象 74 | val receiver = Receiver() 75 | // 根据接收者对象构造一个命令对象 76 | val command = ConcreteCommand(receiver) 77 | // 根据具体的命令构造请求者对象 78 | val invoker = Invoker(command) 79 | // 执行请求方法 80 | invoker.action() 81 | } 82 | } 83 | ``` 84 | 85 | 到这里,我们会发现,命令模式就是将行为调用者与实现者解耦。命令模式总体来说并不难,只是相对比较繁琐。 86 | 87 | ## Android 源码中的实现 88 | 89 | Android 中关于命令模式的应用虽然不少,但是都不算典型,很多都有一定的变种。一个相对典型的例子是:Android 的事件机制中底层逻辑对事件的转发处理。Android 的每一种事件在屏幕上产生后,都会经由底层逻辑转换成一个 NotifyArgs 对象,这个 NotifyArgs 本身并无任何实现,只定义了抽象的方法体。这里我们不会过多涉及底层实现,仅用伪代码描述一下类、方法名以及相互关系。 90 | 91 | ```c++ 92 | struct NotifyArgs { 93 | virtual ~NotifyArgs() {} 94 | virtual void notify(listenter) 95 | } 96 | ``` 97 | 98 | 这就相当于是抽象命令,以具体的按键事件为例 99 | 100 | ```c++ 101 | struct NotifyKeyArgs: Public NotifyArgs { 102 | nsects_t eventTime; 103 | int32_t deviceId; 104 | ... 105 | } 106 | ``` 107 | 108 | NotifyKeyArgs 或其它子类就是具体命令角色,那接下来谁是请求者的角色则是由 Android 事件机制中一个重要的角色 InputDispatcher 来承担,具体事件命令将事件转发给 InputDispatcher,并由其来封装具体的事件操作。我们还是以按键事件为例: 109 | 110 | ```c++ 111 | // frameworks/native/services/inputflinger/InputDispatcher.cpp 112 | void InputDispatcher::notifyKey(const NotifyKeyArgs* args) { 113 | KeyEvent event; 114 | event.initialize(args->deviceId, args->source, args->action, 115 | flags, keyCode, args->scanCode, metaState, 0, 116 | args->downTime, args->eventTime); 117 | 118 | // 通过 NatvieInputManager 在 Event 入队前做一些处理 119 | mPolicy->interceptKeyBeforeQueueing(&event, /*byref*/ policyFlags); 120 | 121 | ... 122 | KeyEntry* newEntry = new KeyEntry(args->eventTime, args->source, 123 | args->action, flags, keyCode,...) 124 | // 事件放入队尾 125 | needWake = enqueueInboundEventLocked(newEntry); 126 | } 127 | ``` 128 | 129 | 事件机制对命令模式应用的逻辑还算明朗,除了上述命令角色和请求角色之外 ,接收者角色则由具体的硬件驱动承担。而关于事件机制 的更详细介绍在此不细说,有兴趣的可以阅读我们以前发过的[按下 Home 键后发生了什么](按下 Home 键后发生了什么.md) 130 | 131 | ## 总结 132 | 133 | #### 优点 134 | 135 | 命令模式给我们带来了更弱的耦合性,更灵活的控制性以及更好的扩展性 136 | 137 | #### 缺点 138 | 139 | 命令模式充分体现了几乎所有设计模式的通病:类的膨胀,大量的衍生类。在实际使用过程中,可以适当的简化,比如省略掉接收者等 -------------------------------------------------------------------------------- /备忘录模式.md: -------------------------------------------------------------------------------- 1 | 备忘录模式(Memento Pattern)是一种行为型模式,用于保存一个对象的某个状态,以便在适当的时候恢复对象到此状态。 2 | 3 | ## 定义 4 | 5 | 在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。 6 | 7 | ## 使用场景 8 | 9 | 需要保存一个对象在某一时刻的状态或部分状态。比如撤销重做、游戏存档 10 | 11 | ## UML 12 | 13 | ![备忘录模式](img/备忘录模式.jpg) 14 | 15 | - Originator: 原始角色,负责创建备忘录,记录、恢复自身的内部状态。可以根据需要决定 Memento 存储哪些状态。 16 | - Memento:备忘录,包含了要被恢复的对象的状态。 17 | - Caretaker: 负责存储备忘录,不能对备忘录的内容进行操作和访问,只能将备忘录传递给其它对象。 18 | 19 | ## Demo 20 | 21 | 我们模拟实现一个游戏存档程序,简单的模拟生命值和等级两个属性: 22 | 23 | ```kotlin 24 | // 备忘录 25 | class Memento( 26 | var life: Int,// 存生命值,否则下线重新上线就满血复活了 27 | var level: Int // 存等级,否则一下线就白玩了 28 | ) 29 | ``` 30 | 31 | 然后是游戏的主体 32 | 33 | ```KOTLIN 34 | // 游戏主体,是 UML 中的 Originator 35 | class Game { 36 | var life = 100 //生命值 37 | var level = 1 //等级 38 | val random = Random() 39 | 40 | fun play() { 41 | println("开始打怪升级") 42 | life -= random.nextInt(50) 43 | level += random.nextInt(5) 44 | println("打完收工") 45 | } 46 | 47 | fun quit() { 48 | println("--------------------") 49 | println("退出游戏") 50 | println("退出时的属性,${toString()}") 51 | println("--------------------") 52 | } 53 | 54 | override fun toString(): String { 55 | return "生命值:$life, 等级:$level" 56 | } 57 | 58 | fun createMemento(): Memento { 59 | println("保存游戏属性~~~") 60 | return Memento(life, level) 61 | } 62 | fun restore(memento: Memento) { 63 | life = memento.life 64 | level = memento.level 65 | println("读取存档后的属性,${toString()}") 66 | } 67 | } 68 | ``` 69 | 接下来我们写备忘录存储和读取操作: 70 | 71 | ```KOTLIN 72 | class Caretaker { 73 | private lateinit var memento: Memento 74 | fun archive(memento: Memento) { 75 | //存档模拟,实际上应该系列化到文件,或上传到服务端 76 | this.memento = memento 77 | println("存档完成~~~") 78 | } 79 | 80 | fun loadMemento(): Memento { 81 | return memento 82 | } 83 | } 84 | ``` 85 | 86 | 最后看客户端的测试代码: 87 | ```kotlin 88 | fun main() { 89 | val game = Game() 90 | game.play() 91 | 92 | val caretaker = Caretaker() 93 | caretaker.archive(game.createMemento()) 94 | game.quit() 95 | 96 | // 重新打开游戏。 97 | val newGame = Game() 98 | newGame.restore(caretaker.loadMemento()) 99 | } 100 | 101 | --------- 运行结果 --------- 102 | 开始打怪升级 103 | 打完收工 104 | 保存游戏属性~~~ 105 | 存档完成~~~ 106 | -------------------- 107 | 退出游戏 108 | 退出时的属性,生命值:53, 等级:1 109 | -------------------- 110 | 读取存档后的属性,生命值:53, 等级:1 111 | ``` 112 | 113 | 游戏存档并没有存储整个 Game 对象,而是通过 Memento 保存 Game 中的属性,再存储 Memento 对象,最终 Memento 对象的操作则是交给了 Caretaker 对象。在这个过程中,各个角色职责清晰、单一,代码也比较简单,屏蔽了外界对 Game 的直接访问,在满足功能的同时也不破坏其封装性。 114 | 115 | ## Android 源码中的使用 116 | 117 | 在 Android 中,最常接触到的备忘录模式的应用是 Activity 中状态的保存,也就是 onSaveInstanceState 和 onRestoreInstanceState 。方法的调用时机如下: 118 | 119 | ![img](https://upload-images.jianshu.io/upload_images/2066935-5c781cf339feaf89.png?imageMogr2/auto-orient/strip|imageView2/2/w/469/format/webp) 120 | 121 | #### onSaveInstanceState 调用时机 122 | 123 | 当用户离开当前 Activity ,且这个页面有可能意外被系统回收时就会调用 onSaveInstanceState 来保存 Activity 的状态,以便后续可以恢复。那什么叫可能被系统回收? 124 | 125 | - 可能:系统内存不足时会按优先级回收,[详见LMK](Low Memory Killer.md) 126 | - 意外:主动调 finish,或者按系统返回键就不是意外,而是意料之中要销毁的。 127 | 128 | 综上除了主动调用 finish, 或者按返回键,其余情况离开当前 Activity ,如 Home 键、打开新 Activity 等都会调用 onSaveInstanceState 方法,将 Activity 的状态保存到一个 Bundle 对象。 129 | 130 | #### onRestoreInstanceState 调用时机 131 | 132 | 这个的时机就比较简单了,只有当 Activity 被意外回收了(该情况下必然调用过了 onSaveInstanceState),再回到该 Activity 时,会重新创建 Activity 实例,再将之前存的 Bundle 对象传递给 Activity 的 onRestoreInstanceState() 方法以及 onCreate() 方法,我们可以选择在这两个方法中选择合适的时机还原 Activity 的状态。 133 | 134 | #### onSaveInstanceState 存了什么 135 | 136 | 看下源码 137 | 138 | ```JAVA 139 | protected void onSaveInstanceState(@NonNull Bundle outState) { 140 | // 1. 存储当前窗口的 View 树的状态 141 | outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState()); 142 | 143 | // 2. 存储 Fragment 的状态,会递归调用 Fragment.onSaveInstanceState 144 | Parcelable p = mFragments.saveAllState(); 145 | if (p != null) { 146 | outState.putParcelable(FRAGMENTS_TAG, p); 147 | } 148 | if (mAutoFillResetNeeded) { 149 | outState.putBoolean(AUTOFILL_RESET_NEEDED, true); 150 | getAutofillManager().onSaveInstanceState(outState); 151 | } 152 | // 3. 如果设置了 Activity 的 ActivityLifecycleCallbacks 153 | // 分发调用这些 callbacks 的 onSaveInsgtanceState 方法 154 | dispatchActivitySaveInstanceState(outState); 155 | } 156 | ``` 157 | 158 | 当然,除了保存以上这些状态,我们往往还会重写 onSaveInstance 来保存一些其它状态。 159 | 160 | ## 小结 161 | 162 | #### 优点 163 | 164 | - 为用户提供了一种可以恢复状态的机制,可以比较方便地回到某个历史状态 165 | - 实现了信息的封装,使用户不需要关心状态保存的细节 166 | 167 | #### 缺点 168 | 169 | - 消耗资源,每保存一次都会消耗一定的存储。(方案:仅保存最近一次) 170 | - 状态过多时保存比较麻烦。(方案:借助 注解 AOP / JSON 自动处理) 171 | 172 | 此外我们还介绍了 Activity 的状态保存机制。 173 | 174 | 最后,大家新年快乐。 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /大厂内推.md: -------------------------------------------------------------------------------- 1 | 感谢读者朋友一直以来对**Android 面试官**的支持,我们联系了在各大厂里的朋友,为广大读者提供一个内推的渠道。 2 | 3 | 目前支持的公司如下: 4 | 5 | | 公司 | 城市 | 岗位 | 6 | | ---- | ---------- | --------------- | 7 | | 美图 | 北京、厦门 | Android、iOS... | 8 | | 头条 | | | 9 | | 美团 | | | 10 | | 铃盛 | | | 11 | | 网龙 | | | 12 | | 酷狗 | | | 13 | | 美柚 | 厦门 | | 14 | 15 | 16 | 17 | 感兴趣的,直接加我微信吧。 18 | 19 | ![image-20201230124630002](img/image-20201230124630002.png) -------------------------------------------------------------------------------- /如何跨进程传递大图.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | 5 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 6 | 7 | **面试官**:如何跨进程传递大图 8 | 9 | 😎:很简单,把图片存到 SD 卡,然后把路径传过去,在别的进程读出来这不就完事了嘛。 10 | 11 | **面试官**:这个需要文件操作,效率不行,有别的方法吗? 12 | 13 | 😎:Bitmap 实现了 Parcelable 接口,可以通过 Intent.putExtra(String name, Parcelable value) 方法直接放 Intent 里面传递。 14 | 15 | **面试官**:那这个方法有什么缺点? 16 | 17 | 😎:Bitmap 太大就会抛异常,所以我更喜欢传路径 18 | 19 | **面试官**:为什么会抛异常? 20 | 21 | 😎:..... 22 | 23 | **面试官**:好的,回去等通知吧 24 | 25 | 26 | 27 | --- 28 | 29 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 30 | 31 | **面试官**:Intent 直接传 Bitmap 会有什么问题? 32 | 33 | 😨:Bitmap 太大会抛 TransactionTooLargeException 异常,原因是:底层判断只要 Binder Transaction 失败,且 Intent 的数据大于 200k 就会抛这个异常了。(见:android_util_Binder.cpp 文件 signalExceptionForError 方法) 34 | 35 | **面试官**:为什么 Intent 传值会有大小限制。 36 | 37 | 😨:应用进程在启动 Binder 机制时会映射一块 1M 大小的内存,所有正在进行的 Binder 事务共享这 1M 的缓冲区 。当使用 Intent 进行 IPC 时申请的缓存超过 1M - 其他事务占用的内存时,就会申请失败抛 TransactionTooLargeException 异常了。 (哼,不会像上次一样答不出来了。见:“谈谈你对 binder 的理解?这样回答才过关”) 38 | 39 | **面试官**:如何绕开这个限制呢? 40 | 41 | 😨:通过 AIDL 使用 Binder 进行 IPC 就不受这个限制,具体代码如下: 42 | 43 | ```java 44 | Bundle bundle = new Bundle(); 45 | bundle.putBinder("binder", new IRemoteGetBitmap.Stub() { 46 | @Override 47 | public Bitmap getBitMap() throws RemoteException { 48 | return mBitmap; 49 | } 50 | }); 51 | intent.putExtras(bundle); 52 | ``` 53 | 54 | **面试官**:这是什么原理呢? 55 | 56 | 😨:还没去细看 57 | 58 | **面试官**:好的,回去等通知吧 59 | 60 | --- 61 | 62 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 63 | 64 | **面试官**:为什么通过 putBinder 的方式传 Bitmap 不会抛 TransactionTooLargeException 异常 65 | 66 | 🤔️:这个问题,我们先来看下,底层在 IPC 时是怎么把 Bitmap 写进 Parcel 的。 67 | 68 | ```c++ 69 | Android - 28 Bitmap.cpp 70 | static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ...) { 71 | // 拿到 Native 的 Bitmap 72 | auto bitmapWrapper = reinterpret_cast(bitmapHandle); 73 | // 拿到其对应的 SkBitmap, 用于获取 Bitmap 的像素信息 74 | bitmapWrapper->getSkBitmap(&bitmap); 75 | 76 | int fd = bitmapWrapper->bitmap().getAshmemFd(); 77 | if (fd >= 0 && !isMutable && p->allowFds()) { 78 | // Bitmap 带了 ashmemFd && Bitmap 不可修改 && Parcel 允许带 fd 79 | // 就直接把 FD 写到 Parcel 里,结束。 80 | status = p->writeDupImmutableBlobFileDescriptor(fd); 81 | return JNI_TRUE; 82 | } 83 | 84 | // 不满足上面的条件就要把 Bitmap 拷贝到一块新的缓冲区 85 | android::Parcel::WritableBlob blob; 86 | // 通过 writeBlob 拿到一块缓冲区 blob 87 | status = p->writeBlob(size, mutableCopy, &blob); 88 | 89 | // 获取像素信息并写到缓冲区 90 | const void* pSrc = bitmap.getPixels(); 91 | if (pSrc == NULL) { 92 | memset(blob.data(), 0, size); 93 | } else { 94 | memcpy(blob.data(), pSrc, size); 95 | } 96 | } 97 | ``` 98 | 99 | 接下来我们看一下 writeBlob 是怎么获取缓冲区的(注意虽然方法名写着 write , 但是实际往缓冲区写数据是在这个方法执行之后) 100 | 101 | ```c++ 102 | Android - 28 Parcel.cpp 103 | // Maximum size of a blob to transfer in-place. 104 | static const size_t BLOB_INPLACE_LIMIT = 16 * 1024; 105 | 106 | status_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob* outBlob) 107 | { 108 | if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) { 109 | // 如果不允许带 fd ,或者这个数据小于 16K 110 | // 就直接在 Parcel 的缓冲区里分配一块空间来保存这个数据 111 | status = writeInt32(BLOB_INPLACE); 112 | void* ptr = writeInplace(len); 113 | outBlob->init(-1, ptr, len, false); 114 | return NO_ERROR; 115 | } 116 | 117 | // 另外开辟一个 ashmem,映射出一块内存,后续数据将保存在 ashmem 的内存里 118 | int fd = ashmem_create_region("Parcel Blob", len); 119 | void* ptr = ::mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 120 | ... 121 | // parcel 里只写个 fd 就好了,这样就算数据量很大,parcel 自己的缓冲区也不用很大 122 | status = writeFileDescriptor(fd, true /*takeOwnership*/); 123 | outBlob->init(fd, ptr, len, mutableCopy); 124 | return status; 125 | } 126 | ``` 127 | 128 | 通过上面的分析,我们可以看出,同一个 Bitmap 写入到 Parcel 所占的缓冲区大小和 Pacel 的 allowFds 有关。 129 | 130 | 直接通过 Intent 传 Bitmap 容易抛 TransactionTooLargeException 异常,就是因为 Parcel 的 allowFds = false,直接把 Bitmap 写入缓冲区占用了较大的内存。 131 | 132 | 接下来,我们来看一下,allowFds 是什么时候被设置成 false 的呢: 133 | 134 | ```JAVA 135 | // 启动 Activity 执行到 Instrumentation.java 的这个方法 136 | public ActivityResult execStartActivity(..., Intent intent, ...){ 137 | ... 138 | intent.prepareToLeaveProcess(who); 139 | ActivityManager.getService().startActivity(...,intent,...) 140 | } 141 | 142 | // Intent.java 143 | public void prepareToLeaveProcess(boolean leavingPackage) { 144 | // 这边一层层传递到最后设置 Parcel 的 allowfds 145 | setAllowFds(false); 146 | .... 147 | } 148 | ``` 149 | 150 | **面试官**:太多了,你懂的。 151 | 152 | 🤔️:总结一下:较大的 bitmap 直接通过 Intent 传递容易抛异常是因为 Intent 启动组件时,系统禁掉了文件描述符 fd 机制 , bitmap 无法利用共享内存,只能拷贝到 Binder 映射的缓冲区,导致缓冲区超限, 触发异常; 而通过 putBinder 的方式,避免了 Intent 禁用描述符的影响,bitmap 写 parcel 时的 allowFds 默认是 true , 可以利用共享内存,所以能高效传输图片。 153 | 154 | **面试官**:可以,我们再来聊聊别的。 -------------------------------------------------------------------------------- /工厂模式.md: -------------------------------------------------------------------------------- 1 | > 生活里80%的痛苦来源于打工,但是我知道,如果不打工,就会有100%的痛苦,所以在打工和没钱之间,我选择打工! 2 | 3 | 早安,打工人,今天我们来看一下打工人中应用最广泛的“工厂模式” 4 | 5 | ## 定义 6 | 7 | 工厂方法模式( Factory Pattern)属于创建型模式,定义一个用于创建对象的接口,让子类决定实例化哪个类。 8 | 9 | ## 使用场景 10 | 11 | - 任何需要生成复杂对象的地方 12 | - 直接 new 可以创建的对象无需使用工厂模式 13 | 14 | ## UML 15 | 16 | ![工厂方法模式](img/工厂方法模式.jpg) 17 | 18 | - Factory:工厂的抽象类,工厂方法模式的核心,定义创建对象方法的接口 19 | - ConcreteFactory: 具体的工厂类,实现具体的业务逻辑 20 | - Product: 抽象产品类 21 | - ConcreteProduct: 具体产品类 22 | 23 | ## 举例 24 | 25 | ![img](https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3774655993,3022129994&fm=26&gp=0.jpg) 26 | 27 | 我是富土康3号流水线的张全蛋,英文名叫Micheal Jack,来看下俺们工厂是怎么生产: 28 | 29 | ## CODE 30 | 31 | ```KOTLIN 32 | // 众所周知我们厂代工手机 33 | abstract class Phone(val name: String) { 34 | // 省略:打电话,发短信,刚上冲浪等功能 35 | } 36 | //今年新出的水果 12 系列也是这边生产的呢 37 | class IPhone12 : Phone("iPhone 12") 38 | class IPhone12Pro: Phone("iPhone 12 pro") 39 | 40 | // 抽象的工厂类 41 | abstract class Factory(val name: String) { 42 | // 泛型相当于 java 中的 43 | abstract fun createPhone(cls: Class):T 44 | } 45 | 46 | class Factory3 : Factory("富土康三号") { 47 | override fun createPhone(cls: Class): T { 48 | // 实现比较复杂的创建方式(比如一大堆复杂或者从各处取来的参数) 49 | // 这边简单的使用反射生成对象演示 50 | return Class.forName(cls.name).newInstance() as T 51 | } 52 | } 53 | ``` 54 | 55 | 来运行一下测试代码 56 | 57 | ```kotlin 58 | fun main() { 59 | // 开启流水线 60 | val factory = Factory3() 61 | println("来到工厂:${factory.name}流水线") 62 | val iPhone12 = factory.createPhone(IPhone12::class.java) 63 | println("生产了一个 ${iPhone12.name}") 64 | val iPhone12Pro = factory.createPhone(IPhone12Pro::class.java) 65 | println("又生产了一个 ${iPhone12Pro.name}") 66 | } 67 | 68 | // 运行结果 ----- 69 | 来到工厂:富土康三号流水线 70 | 生产了一个 iPhone 12 71 | 又生产了一个 iPhone 12 pro 72 | ``` 73 | 74 | 当然我们也可以继续扩展 Factory 扩展一个专门生产 Pad 的工厂。这种拥有多个工厂的方式我们也称为多工厂模式。 75 | 76 | 然而,大部分的时候我们的工厂只有一个,这时候就可以把抽象类简化掉,然后把工厂方法改成静态的即可: 77 | 78 | ```kotlin 79 | object StaticFactory { 80 | fun createPhone(cls: Class): T { 81 | return Class.forName(cls.name).newInstance() as T 82 | } 83 | } 84 | 85 | val iPhone12 = StaticFactory.createPhone(IPhone12::class.java) 86 | val iPhone12Pro = StaticFactory.createPhone(IPhone12Pro::class.java) 87 | ``` 88 | 89 | 这种方式又称为简单工厂模式或静态工厂模式 90 | 91 | ## 安卓中的应用 92 | 93 | - BitmapFactory 里的一系列 decodeXXX 都是静态工厂方法,会返回一个 Bitmap 94 | - ViewModelProvider.Factory (抽象工厂类) 接口里面有一个 create 方法,返回 ViewModel;FragmentManagerViewModel, LoaderViewModel(具体工厂类) 里有具体的实现 95 | 96 | ## 总结 97 | 98 | - 优点: 99 | - 工厂方法模式完全符合设计原则,降低了对象之间的耦合。高层模块只需要知道产品的抽象类,其他的实现都不需要关心。 100 | - 良好的封装性,代码结构清晰。扩展性好。 101 | 102 | - 缺点:每次我们为工厂方法模式添加新的产品时就要编写一个新的产品类。同时还要引入抽象层,这必然会导致类结构的复杂化 103 | - 注意简单工厂或静态工厂模式都是工厂方法模式的一种简化版本,并不是直接在 23 种设计模式之中。 -------------------------------------------------------------------------------- /年轻人不讲码德,我大意了,没有 Review.md: -------------------------------------------------------------------------------- 1 | > 现在的年轻程序猿,不讲码德,来 copy,来乱打键盘,被发现了才说对不,对不起,我不懂规矩。我说你这可不是乱打的,训练有素,油备而来,就是要来把项目搞得不好维护! 2 | 3 | 以下排名不分先后 4 | 5 | #### 奇葩命名 6 | 7 | abcd, asdf, a1, tmp, shangpin, TabFragmement 8 | 9 | 其实我本人英文也不好,但是现在翻译软件这么方便,不会就翻译一下啊,而且 AS 都给你提示`Typo: In word 'XXX' ` 难道你是瞎吗? 10 | 11 | #### 无意义的代码 12 | 13 | ![](img/吐槽代码1.png) 14 | 15 | 截图项目中真实遇到 16 | 17 | #### 伪装欺诈 18 | 19 | 比如方法名叫 `login()` 结果方法里根本不是去登录,而是去注册。 20 | 21 | 再比如`RelativeLayout frameLayout = findviewById(xxxx)` 你就说你这玩意到底是啥? 22 | 23 | #### 奇葩注释 24 | 25 | - 不写注释,自以为自己的代码精妙无比,无需注释,提测后有 bug 自己都看不懂了 26 | - 说谎注释,注释的东西根本和代码写得不一样,当然可能不是故意的只是更新了代码没更新注释 27 | - 废话注释,//不要问我为什么这样写,我也不知道; i+1;//这边需要+1 28 | 29 | #### Copy & Paste 30 | 31 | 几乎一样的代码拷得到处都是,emmmm, 公司又不是按代码量开工资,你抽取复用一下会死? 32 | 33 | #### 大量嵌套 34 | 35 | 可以在一行代码上使用超过10层的小括号() ,或是在一个函数里使用超过20层的语句嵌套{},把嵌套的if else 转成 [? :] 也是一件很NB的事。 36 | 37 | 地狱回调也是表现之一 38 | 39 | #### 舍不得删代码 40 | 41 | 没调用的方法不删,注释掉的代码不删,留着当传家宝? 42 | 43 | #### 混乱参数 44 | 45 | 同一套接口,有的时候`rawRectangle(height, width)` 有的时候 `drawRectangle(width, height)` 46 | 47 | 如果你不理解,那再举个例子,时间单位有时候用秒有时候用毫秒,而且方法名或参数名上看不出来任何差别。 48 | 49 | 项目里还遇到过有个 SDK,不同接口给的坐标系一会左上角为原点,一会左下角为原点。导致我绘制的时候总得先试试,薛定谔的原点,不绘制上去永远不知道原点在哪。 50 | 51 | #### 借口 52 | 53 | 写烂代码从来都是说时间太赶;写 todo , never do; 54 | 55 | #### 脑洞代码 56 | 57 | ````KOTLIN 58 | fun sort(arr: Array) { 59 | for (i in arr) { 60 | thread { 61 | Thread.sleep(i.toLong()) 62 | print("$i,") 63 | }.start() 64 | } 65 | } 66 | ```` 67 | 68 | 这个是个段子,凑下数。 69 | 70 | #### 最后 71 | 72 | 我劝写过上述代码的人耗子尾汁,好好反思,以后不要再犯这样的聪明,小聪明! 73 | 74 | 阿,呃,猿们要以和为贵... 要讲码德,不要搞窝里斗,给我点个赞再走,谢谢朋友们! 75 | 76 | -------------------------------------------------------------------------------- /怎么解决引用计数 GC 的循环引用问题?.md: -------------------------------------------------------------------------------- 1 | >引用计数方式 GC 存在循环引用问题,导致无法辨别无用对象,而 GC ROOT 方式不存在循环引用的问题 2 | 3 | 引用计数和 GC ROOT 的实现机理很易理解,面试时大家都能流利应答,那怎么才能脱颖而出呢?思考一个问题:**不通过 GC ROOT,仍使用引用计数方式,怎么解决它的循环引用问题?** 4 | 5 | 解答此问题前,通过目标驱动法来想象一下,若 Get 了此知识点,可以这样应用到面试中: 6 | 7 | *面试官:* 说一下垃圾回收机制吧 8 | 9 | *我:* ...可以通过强、弱引用计数结合方式解决引用计数的循环引用问题,实际上 Android 的**智能指针**就是这样实现的... 10 | 11 | ## 智能指针 12 | 13 | 智能指针在整个 Android 工程中使用很广泛,在 binder 相关源码可以看到 sp、wp 类型的引用: 14 | 15 | ```objectivec 16 | sp result = new BpBinder(handle); 17 | 18 | wp result = new BpBinder(handle); 19 | ``` 20 | 21 | sp 即 strong pointer 强指针引用;wp 是 weak pointer 弱指针引用。 22 | 23 | 在 Java 中我们不用关心对象的销毁及内存释放,GC 机制会自动辨别回收无用对象,而 **智能指针** 就是 native 层一个小型的 GC 实现。 24 | 25 | 智能指针以引用计数的方式来标识无用对象,使用智能指针的对象需继承自 RefBase,RefBase 中维护了此对象的强引用数量和弱引用数量。 26 | 27 | 强指针 sp 重载了 "=" 运算符,在引用其他对象时将强引用计数 +1,在 sp 析构函数中将强引用计数 -1,当强引用计数减至 0 时销毁引用的对象,这样就实现了对象的自动释放。 28 | 29 | 弱指针引用其他对象时将弱引用计数 +1,在 wp 析构函数中将弱引用计数 -1,当强引用计数为 0 时,不论弱引用计数是否为 0 都销毁引用的对象。 30 | 31 | ## 如何解决循环引用问题 32 | 33 | 只靠强引用计数方式,会存在循环引用的问题,导致对象永远无法被释放,弱引用就是专门用来解决循环引用问题的: 34 | 35 | **若 A 强引用了 B,那 B 引用 A 时就需使用弱引用,当判断是否为无用对象时仅考虑强引用计数是否为 0,不关心弱引用计数的数量** 36 | 37 | 这样就解决了循环引用导致对象无法释放的问题,但这会引发野指针问题:当 B 要通过弱指针访问 A 时,A 可能已经被销毁了,那指向 A 的这个弱指针就变成野指针了。在这种情况下,就表示 A 确实已经不存在了,需要进行重新创建等其他操作 38 | 39 | ## 智能指针自定义规则 40 | 41 | 智能指针并不是固定的 "当强引用计数为 0 时,不论弱引用计数是否为 0 都销毁引用的对象" ,而是可以自定义规则。RefBase 提供了 extendObjectLifetime() 方法,可以用来设置引用计数器的规则,不同规则对删除目标对象的时机判断也是不一样的,包括以下三种规则: 42 | 43 | *OBJECT_LIFETIME_STRONG*:只有在这个对象内存空间中的强计数器值为 0 的时候才会销毁对象 44 | 45 | *OBJECT_LIFETIME_WEAK*:只有在这个对象内存空间中的强计数器和弱计数器的值都为 0 的时候才会销毁对象 46 | 47 | *OBJECT_LIFETIME_MASK*:不管这两个计数器是不是都为 0,都不销毁对象,即与一般指针无异,还是要自己手动去释放对象 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /投稿.md: -------------------------------------------------------------------------------- 1 | ## 征稿启事 2 | 3 | 本公众号欢迎大家投稿,如果你希望你的文章可以被更多人看到,直接将文章链接发我邮箱即可 **33691286@qq.com**,也可以加我QQ好友,邮件主题请注明 **投稿**,谢谢。 4 | 5 | 文章要求: 6 | 7 | 1. 百分百原创 8 | 2. 文章能给读书带来提高 9 | 3. 文章符合本公众号的调性(偏主观) 10 | 4. 推广类的文章请后台回复 “商务合作” 11 | 12 | 带来的福利: 13 | 14 | 1. 推文将注明你的署名,及原文链接 15 | 2. 更多人能够发现你的文章,间接提升自己的知名度 16 | 3. 帮你审阅文章,检查文章是否有错误 17 | 18 | 如果你希望你的文章能够被更多的人看到,能够帮助到更多的人,赶快来投稿吧~~ 19 | 20 | 一般我会在24小时内回复您,如果比较忙,会在周末统一阅读和回复。 21 | 22 | -------------------------------------------------------------------------------- /抽象工厂.md: -------------------------------------------------------------------------------- 1 | > 上次我们聊了打工人的“工厂模式”,每个工厂生产的都是具体的产品;而今天要说的抽象工厂生产出来的产品是不确定的。 2 | 3 | ## 定义 4 | 5 | 为创建一组相关或者相互依赖的对象提供一个接口,而不需要指定它们的具体类。 6 | 7 | ## 使用场景 8 | 9 | 定义太抽象我们举个例子,苹果公司生产的 iPhone, iPad 都可以发邮件,看视频,但由于平台特性不一样,实现的效果和交互逻辑也不一样,这个时候我们就可以使用抽象工厂模式来生产。 10 | 11 | ## UML 12 | 13 | ![抽象工厂模式](img/抽象工厂模式.jpg) 14 | 15 | - Factory: 抽象工厂,声明了一组创建一种产品的方法,每一个方法对应一种产品。上图中定义了两个分别用于创建产品 A 和产品 B 16 | - ConcreteFactory: 具体工厂,它实现了在抽象工厂中定义的创建产品的方法,生成一组具体的产品。这些产品构成一个产品种类 17 | - Product: 抽象产品类 18 | - ProductA、ProductB: 具体产品 19 | 20 | ## CODE 21 | 22 | 我们还是以之前工厂模式的例子,富土康流水线 23 | 24 | ```kotlin 25 | // 众所周知我们厂代工手机 26 | abstract class Phone(val name: String) { 27 | // 省略:打电话,发短信,刚上冲浪等功能 28 | } 29 | //今年新出的水果 12 系列也是这边生产的呢 30 | class IPhone12 : Phone("iPhone 12") 31 | class IPhone12Pro: Phone("iPhone 12 pro") 32 | 33 | // 抽象的工厂类 34 | abstract class Factory(val name: String) { 35 | abstract fun createIPhone(): Phone 36 | abstract fun createIPhonePro(): Phone 37 | } 38 | 39 | class Factory4 : Factory("富土康四号") { 40 | override fun createIPhone(): Phone { 41 | return IPhone12() 42 | } 43 | 44 | override fun createIPhonePro(): Phone { 45 | return IPhone12Pro() 46 | } 47 | } 48 | ``` 49 | 50 | 来运行一下测试代码 51 | 52 | ```kotlin 53 | fun main() { 54 | // 开启流水线 55 | val factory = Factory4() 56 | println("来到工厂:${factory.name}流水线") 57 | val iPhone12 = factory.createIPhone() 58 | println("生产了一个 ${iPhone12.name}") 59 | val iPhone12Pro = factory.createIPhonePro() 60 | println("又生产了一个 ${iPhone12Pro.name}") 61 | } 62 | 63 | // 运行结果 ----- 64 | 来到工厂:富土康四号流水线 65 | 生产了一个 iPhone 12 66 | 又生产了一个 iPhone 12 pro 67 | ``` 68 | 69 | ## 工厂模式 vs 抽象工厂模式 70 | 71 | 都用同样的例子,我们可以看出各种工厂模式的核心区别。 72 | 73 | - 简单工厂模式:一个工厂类使用静态方法创建不同类型的对象 74 | - 工厂方法模式:一个具体的工厂类负责创建一个具体的对象 75 | - 抽象工厂模式:一个具体的工厂负责创建一系列相关的对象 76 | 77 | ## 另一个示例 78 | 79 | 前面的示例很好的表示了一个具体工厂负责创建一系列相关的对象,但是对抽象工厂的是如何抽象的不是很说得很清楚,下面我们来举一个手机零件生产的例子 80 | 81 | ```kotlin 82 | // ProductA 屏幕 83 | interface IScreen { 84 | //点亮屏幕 85 | fun lightUp() 86 | } 87 | class IPhoneScreen : IScreen { 88 | override fun lightUp() { 89 | println("iPhone 小屏") 90 | } 91 | } 92 | class IPadScreen : IScreen { 93 | override fun lightUp() { 94 | println("iPad 大屏") 95 | } 96 | } 97 | 98 | // ProductB CPU 99 | interface ICpu { 100 | fun compute() 101 | } 102 | class IPhoneCpu : ICpu { 103 | override fun compute() { 104 | println("A14") 105 | } 106 | } 107 | class IPadCpu : ICpu { 108 | override fun compute() { 109 | println("A12Z") 110 | } 111 | } 112 | 113 | //抽象工厂 114 | interface AbstractFactory { 115 | fun produceScreen(): IScreen 116 | fun produceCpu(): ICpu 117 | } 118 | class IPhoneFactory : AbstractFactory { 119 | override fun produceScreen(): IScreen { 120 | return IPhoneScreen() 121 | } 122 | override fun produceCpu(): ICpu { 123 | return IPhoneCpu() 124 | } 125 | } 126 | class IPadFactory : AbstractFactory { 127 | override fun produceScreen(): IScreen { 128 | return IPadScreen() 129 | } 130 | override fun produceCpu(): ICpu { 131 | return IPadCpu() 132 | } 133 | } 134 | ``` 135 | 136 | 接下来运行测试代码 137 | 138 | ```kotlin 139 | fun main() { 140 | println("来到 IPhone 工厂") 141 | var factory: AbstractFactory = IPhoneFactory() 142 | var screen = factory.produceScreen() 143 | print("生产了:") 144 | screen.lightUp() 145 | var cpu = factory.produceCpu() 146 | print("生产了:") 147 | cpu.compute() 148 | 149 | println("来到 IPad 工厂") 150 | factory = IPadFactory() 151 | screen = factory.produceScreen() 152 | print("生产了:") 153 | screen.lightUp() 154 | cpu = factory.produceCpu() 155 | print("生产了:") 156 | cpu.compute() 157 | } 158 | 159 | // 运行结果 160 | 来到 IPhone 工厂 161 | 生产了:iPhone 小屏 162 | 生产了:A14 163 | 来到 IPad 工厂 164 | 生产了:iPad 大屏 165 | 生产了:A12Z 166 | ``` 167 | 168 | 这个例子相对之前的复杂了一些,而抽象工厂就是定义了 iPhone 工厂和 iPad 工厂生产方法的**抽象工厂** -------------------------------------------------------------------------------- /效率 | macOS 双击安装 APK .md: -------------------------------------------------------------------------------- 1 | ## 痛点 2 | 3 | 遇到特定版本的 BUG 时,我们需要安装相应的 APK 。这时候我们需要: 4 | 5 | 1. 打开终端 6 | 2. 输入 `adb install -r ` 7 | 3. 把 apk 拖入终端 或者输入完整的 apk 文件路径 8 | 4. 回车运行 9 | 10 | ![img](http://5b0988e595225.cdn.sohucs.com/images/20180727/e4fb1efb996d4a0bbd93cbfa16668e26.gif) 11 | 12 | ## 本方法效果 13 | 14 | todo 录屏 15 | 16 | ## 准备 17 | 18 | - 一台 macOS (我的版本是:11.0.1) 19 | - 安装并配置好 adb 环境变量(终端下输入 adb devices 能看到设备) 20 | - (Windows 类似的找双击执行 adb 命令的方法,本文略) 21 | 22 | 其原理是通过 macOS自带`Automator`可以自定义执行`adb install`命令来实现安装 APK 23 | 24 | ## 步骤 25 | 26 | #### 1 . 打开应用程序里的自动操作 27 | 28 | ![image-20210727225916008](img/image-20210727225916008.png) 29 | 30 | #### 2. 选择应用程序 31 | 32 | ![image-20210728143840779](img/image-20210728143840779.png) 33 | 34 | #### 3. 编辑 AppleScript 35 | 36 | ![image-20210728144033706](img/image-20210728144033706.png) 37 | 38 | ``` 39 | on run {input, parameters} 40 | tell application "Terminal" 41 | set thePath to POSIX path of input as string 42 | do script "adb install -r " & thePath 43 | activate 44 | end tell 45 | end run 46 | ``` 47 | 48 | 填写完成后 Command+S 保存 49 | 50 | ![image-20210728144155409](img/image-20210728144155409.png) 51 | 52 | 保存结果就是一个可以运行安装命令的应用 53 | 54 | #### 4. 更改 APK 的默认打开方式 55 | 56 | 双击 APK 文件,选取应用程序 57 | 58 | ![image-20210728145422282](img/image-20210728145422282.png) 59 | 60 | 61 | 62 | ![image-20210728144401013](img/image-20210728144401013.png) 63 | 64 | 勾选始终用此方式打开,点击打开按钮 65 | 66 | #### 5. 此时已经全部配置完成,双击任何 apk 能达到一样的效果,省下来的时间帮我点个赞吧。 67 | 68 | -------------------------------------------------------------------------------- /文件描述符.md: -------------------------------------------------------------------------------- 1 | 在平时的 Android 开发中,你与文件描述符打过交道吗?一些知识点会涉及到文件描述符,比如: 2 | 3 | - mmap 函数的文件描述符参数 4 | - epoll 机制对文件描述符的限制问题 5 | - ... 6 | 7 | 这时,如果让你说说对文件描述符的了解,你能回答上来吗? 8 | 9 | 回答不上来也没关系,来,集中注意力,我们一起来学习: 10 | 11 | --- 12 | 13 | Linux 中一切都可以看作文件,包括普通文件、链接文件、Socket 以及设备驱动等,对其进行相关操作时,都可能会创建对应的文件描述符。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符。 14 | 15 | 文件描述符与文件是什么关系呢?下图 Linux 中的三张表可以体现: 16 | 17 | ![](/img/fd.png) 18 | 19 | - **进程级别的文件描述符表**:内核为每个进程维护一个文件描述符表,该表记录了文件描述符的相关信息,包括文件描述符、指向打开文件表中记录的指针。 20 | 21 | - **系统级别的打开文件表**:内核对所有打开文件维护的一个进程共享的打开文件描述表,表中存储了处于打开状态文件的相关信息,包括文件类型、访问权限、文件操作函数(file_operations)等。 22 | 23 | - **系统级别的 i-node 表**:i-node 结构体记录了文件相关的信息,包括文件长度,文件所在设备,文件物理位置,创建、修改和更新时间等,"ls -i" 命令可以查看文件 i-node 节点 24 | 25 | 文件描述符是一种系统资源,可以通过以下命令来查看文件描述符的上限: 26 | ```shell script 27 | #查看所有进程允许打开的最大 fd 数量 28 | 126|generic_x86:/ # cat /proc/sys/fs/file-max 29 | 174139 30 | 31 | #查看所有进程已经打开的 fd 数量以及允许的最大数量 32 | generic_x86:/ # cat /proc/sys/fs/file-nr 33 | 11040 0 174139 34 | 35 | #查看单个进程允许打开的最大 fd 数量. 36 | generic_x86:/ # ulimit -n 37 | 32768 38 | ``` 39 | 也可以查看某进程当前已使用的 fd : 40 | ```shell script 41 | #查看某进程(进程 id 为 15077)已经打开的 fd 42 | generic_x86:/ # ls -l /proc/15077/fd/ 43 | total 0 44 | lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 0 -> /dev/null 45 | lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 1 -> /dev/null 46 | lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 35 -> /dev/binder 47 | lrwx------ 1 u0_a136 u0_a136 64 2020-04-09 01:01 44 -> socket:[780404] 48 | lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 55 -> /dev/ashmem 49 | lrwx------ 1 u0_a136 u0_a136 64 2020-04-15 23:04 60 -> /dev/ashmem 50 | ... 51 | ``` 52 | 53 | 上面这个进程是一个 Android 应用进程,所以能看到 ashmem、binder 等 Android 特有设备文件相关的 fd 。再来看一个实际打开磁盘文件的例子: 54 | ```java 55 | File file = new File(getCacheDir(), "testFdFile"); 56 | FileOutputStream out = new FileOutputStream(file); 57 | ``` 58 | 59 | 执行上面代码后会申请一个对应的 fd: 60 | ```shell script 61 | # ls -l /proc/{pid}/fd/ 62 | ... 63 | l-wx------ u0_a55 u0_a55 2020-04-16 00:24 995 -> /data/data/com.example.test/cache/testFdFile 64 | ... 65 | ``` 66 | 67 | 实际开发中,可能会遇到 fd 资源超过上限导致的 "Too many open files" 之类的问题,一般都是因为没有及时释放掉 fd,比如上面代码中 FileOutputStream 没有关闭,若循环执行超过单个进程允许打开的最大 fd 数量,程序就会出现异常。 68 | 69 | --- 70 | 71 | 通过上面对文件描述符的学习,你一定能回答 "说说对文件描述符的了解" 这个问题了。 72 | 73 | 学无止境,又一个问题出现了:**怎么排查 fd 泄漏的原因** ,你能回答上来吗? -------------------------------------------------------------------------------- /模板模式.md: -------------------------------------------------------------------------------- 1 | 模板模式是一个关于继承的设计模式,每一个被继承的父类都是一个模板,模板中的步骤是固定的,而不固定的步骤在具体子类再实现。 2 | 3 | ## 定义 4 | 5 | 在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。 6 | 7 | ## 使用场景 8 | 9 | - 多个子类有公有的方法,并且逻辑基本相同 10 | - 把核心的算法设计为模板方法,其它相关方法由子类实现 11 | - 重构时,将相同的代码抽取到父类中,然后通过钩子函数约束其行为 12 | 13 | ## UML 14 | 15 | ![模板模式](img/模板模式.jpg) 16 | 17 | - AbsTemplate:抽象的模板类,在调用 execute 时内部会调用 stepOne Two Three 三个方法 18 | - ConcreteImplA, ConcreteImplB : 具体的实现类,可以对 stepOne Two Three 三个方法进行定制、重写 19 | 20 | ## 举例 21 | 22 | 刚刚过去的国庆 8 天假,想必各有各的精彩,但是更多人却是用一天为模板过 8 天,我们来看下: 23 | 24 | ## CODE 25 | 26 | ```KOTLIN 27 | abstract class Template { 28 | fun todayLife() { // 模板人的一天 29 | stepOne() 30 | stepTwo() 31 | stepThree() 32 | stepFour() 33 | } 34 | 35 | private fun stepOne() { 36 | // 公用的方法 37 | print("起床,吃饭") 38 | } 39 | abstract fun stepTwo() 40 | abstract fun stepThree() 41 | abstract fun stepFour() 42 | } 43 | 44 | // 肥宅 45 | class Boy : Template() { 46 | override fun stepTwo() { 47 | print(" -> 打游戏") 48 | } 49 | 50 | override fun stepThree() { 51 | print(" -> 玩手机") 52 | } 53 | 54 | override fun stepFour() { 55 | print(" -> 睡觉") 56 | } 57 | } 58 | 59 | // 爱旅游的文青 60 | class Girl : Template() { 61 | override fun stepTwo() { 62 | print(" -> 上车睡觉") 63 | } 64 | 65 | override fun stepThree() { 66 | print(" -> 下车拍照") 67 | } 68 | 69 | override fun stepFour() { 70 | print(" -> 修图发朋友圈") 71 | } 72 | } 73 | ``` 74 | 75 | 来运行一下测试代码 76 | 77 | ```java 78 | fun main() { 79 | print("10.1 国庆来啦!来看看宅男们都在干嘛:\n") 80 | Boy().todayLife() 81 | print("\n来瞅瞅妹子们在干嘛:\n") 82 | Girl().todayLife() 83 | print("\n10.8 国庆结束了!真想再给祖国过一次生日") 84 | } 85 | 86 | // 运行结果 ----- 87 | 88 | 10.1 国庆来啦!来看看宅男们都在干嘛: 89 | 起床,吃饭 -> 打游戏 -> 玩手机 -> 睡觉 90 | 来瞅瞅妹子们在干嘛: 91 | 起床,吃饭 -> 上车睡觉 -> 下车拍照 -> 修图发朋友圈 92 | 10.8 国庆结束了!真想再给祖国过一次生日 93 | ``` 94 | 95 | 模板既很好的共用了相同的部分,又可以实现各自不同的部分,就像我们的生活一样,都要吃饭睡觉,却也有各自的精彩,听懂的掌声。 96 | 97 | ## 安卓中的应用 98 | 99 | 100 | AsyncTask 这个类就使用了模板模式,我们继承它,然后实现`doInBackground`方法,在里面去做一些耗时操作;当然还可以重写`onPreEcecute`, `onPostExecute` 方法。而在使用的时候,我们只需要调用其 `execute`方法即可 101 | 102 | 另一个比较常用的例子是 Activity 的生命周期,在 Activity 类里面已经定义好了相关的方法,当我们需要处理 Activity 从启动到显示的过程,我们就可以去重写 `onCreate`, `onStart`,`onResume` 等相关生命周期的方法 103 | 104 | ## 优缺点 105 | 106 | #### 优点: 107 | 108 | 1. 封装不可变的部分,扩展可变的部分 109 | 2. 提取公共部分代码,简洁利于维护 110 | 111 | #### 缺点: 112 | 113 | 个人觉得没啥缺点,硬要说的话:分散代码,阅读难度增加 -------------------------------------------------------------------------------- /状态模式.md: -------------------------------------------------------------------------------- 1 | 状态模式中的行为是由状态来决定的,不同的状态下有不同的行为。 2 | 3 | ## 定义 4 | 5 | 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。 6 | 7 | 这个定义听起来有点绕,其实还是那句话,不同的状态有不同的行为,这样当状态改变时,就像变了个类一样。 8 | 9 | ## 使用场景 10 | 11 | 1. 一个对象的行为取决于它的状态,并且它在运行时根据状态改变它的行为。 12 | 2. 代码中包含大量与对象状态有关条件语句。 13 | 14 | 核心就是状态,在一堆行为中能抽象出不同的状态,自然就能适用状态模式 15 | 16 | ## UML 17 | 18 | ![image-20200917223121021](img/image-20200917223121021.png) 19 | 20 | - Context:环境类,用来设置当前状态和执行该状态下行为 21 | - State: 状态的接口,表示该状态下的行为 22 | - StateA, StateB 具体的状态实现类,实现不同状态下的不同行为 23 | 24 | ## 举例 25 | 26 | 比如我们平常就有上班和下班两种状态 27 | 28 | 上班时在努力工作,上班时间过了状态就变成下班了。 29 | 30 | 下班时休息学习,下班时间结束就变成上班了 31 | 32 | ## CODE 33 | 34 | ```KOTLIN 35 | // 状态接口,用来表示不同状态下该做什么事 36 | interface State { 37 | fun doSomeThing() 38 | } 39 | //两个具体的状态 40 | class WorkingState : State { 41 | override fun doSomeThing() { 42 | print("上班:认真工作") 43 | } 44 | } 45 | 46 | class OffDutySate : State { 47 | override fun doSomeThing() { 48 | print("下班:关注 Android 面试官,学习") 49 | } 50 | } 51 | 52 | // 环境类 人,有不同的状态 53 | class Man { 54 | private var state: State = OffDutySate() 55 | 56 | fun startWork() { 57 | state = WorkingState() 58 | } 59 | 60 | fun offDuty() { 61 | state = OffDutySate() 62 | } 63 | 64 | fun doSomeThing() { 65 | state.doSomeThing() 66 | } 67 | } 68 | 69 | ``` 70 | 71 | 来运行一下测试代码 72 | 73 | ```java 74 | fun main() { 75 | val man = Man() 76 | println("打卡上班,这时候做点什么呢") 77 | man.startWork() 78 | man.doSomeThing() 79 | 80 | println("到点下班,这时候做点什么呢") 81 | man.offDuty() 82 | man.doSomeThing() 83 | } 84 | 85 | // 运行结果 ----- 86 | 87 | 打卡上班,这时候做点什么呢 88 | 上班:认真工作,积累福报 89 | 到点下班,这时候做点什么呢 90 | 下班:关注 Android 面试官,学习 91 | ``` 92 | 93 | 不出所料,人在处理上班和下班两种状态时,行为完全不同,就像换了个人似的。(换了个类似的) 94 | 95 | 这便是最简单的状态模式了。 96 | 97 | ## 安卓中的应用 98 | 99 | WIFI 管理里的各种状态 100 | 101 | com.android.server.wifi.WifiStateMachine 102 | 103 | ![image-20200917232430861](img/image-20200917232430861.png) 104 | 105 | 这些状态都继承自 com.android.internal.util.State; 不同状态下,行为也不一样 106 | 107 | ```java 108 | public class State implements IState { 109 | @Override 110 | public void enter() { 111 | // 进入该状态做的一些事 112 | } 113 | 114 | @Override 115 | public void exit() { 116 | // 退出该状态做的一些事 117 | } 118 | 119 | @Override 120 | public boolean processMessage(Message msg) { 121 | // 在该状态下处理各种消息事件 122 | return false; 123 | } 124 | } 125 | ``` 126 | 127 | ## 优缺点 128 | 129 | #### 优点: 130 | 131 | 1. 封装了转换规则。 132 | 2. 枚举可能的状态,在枚举状态之前需要确定状态种类。 133 | 3. 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 134 | 4. 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 135 | 5. 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。 136 | 137 | #### 缺点: 138 | 139 | 1. 状态模式的使用必然会增加系统类和对象的个数。 140 | 2. 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 141 | 3. 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。 142 | 143 | ## 状态模式 VS 策略模式 144 | 145 | 如果看过之前 [策略模式](http://mp.weixin.qq.com/s?__biz=MzIzOTkwMDY5Nw==&mid=2247485061&idx=1&sn=accab42132923ed3e0ae0baa649315ce&chksm=e92247f3de55cee53a5f0c8eaf6a1f12ee6a376f215eb0ea3c821eeb91735743ef3b2ca3254f#rd) 文章,应该会发现两者非常相似,简直是亲兄弟,我们先看看两者的通用类图: 146 | 147 | ![image-20200917234119283](img/image-20200917234119283.png) 148 | 149 | **状态模式(左) 策略模式(右)** 150 | 151 | 可以看出状态模式和策略模式的类结构是一致的。不过在模式应用上有着本质的不同。 152 | 153 | 策略模式重在整个算法的替换:如之前文章中举例的,冒泡排序、快速排序都可以作为一个单独的排序算法进行替换应用。 154 | 155 | 而状态模式则是通过状态来改变形为,其不同状态组合起来是共同的整体。就像大部分人都有着上班下班两种状态,只是时间长短可能不一样而已。 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /看你简历上写熟悉 AIDL,说一说 oneway 吧.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 5 | 6 | **面试官**️:看你简历上写熟悉 AIDL,说一说 oneway 吧 7 | 8 | 😎:oneway 是什么?跟 AIDL 没关系吧,我熟悉 AIDL 指的是使用 AIDL 接口进行跨进程通信。 9 | 10 | **面试官**:AIDL 接口的方法可以用 oneway 修饰符来修饰,了解过这个修饰符的作用吗? 11 | 12 | 😎:可以提高性能吗?我觉得没必要使用,我从没用过,程序也跑的好好的,没出过什么问题。 13 | 14 | **面试官**:好的,回去等通知吧 15 | 16 | --- 17 | 18 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 19 | > 20 | **面试官**:看你简历上写熟悉 AIDL,说一说 oneway 吧 21 | 22 | 😨:用 oneway 修饰的 AIDL 接口方法,是单向调用,不需要等待另一个进程的返回结果,所以方法的返回类型也只允许是 void. 23 | 24 | **面试官**:怎么理解 "单向调用" ,有了解过它的实现原理吗? 25 | 26 | 😨:由应用进程到服务进程是通过 binder 驱动进行 IPC 通信的,单向的意思应该是应用进程只向 binder 驱动发送一次数据就结束返回,不再等待回复数据;而不用 oneway 修饰的方法需要等待 binder 驱动与服务端通信完后,再回复数据给应用端。 27 | 28 | **面试官**:只向 binder 驱动发送数据吗?binder 驱动有没有回复应用? 29 | 30 | 😨:嗯... 我理解的是既然不需要返回值,所以没有回复吧 31 | 32 | **面试官**:好的,回去等通知吧 33 | 34 | --- 35 | 36 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 37 | > 38 | **面试官**:看你简历上写熟悉 AIDL,说一说 oneway 吧 39 | 40 | 🤔️:oneway 主要有两个特性:异步调用和串行化处理。异步调用是指应用向 binder 驱动发送数据后不需要挂起线程等待 binder 驱动的回复,而是直接结束。像一些系统服务调用应用进程的时候就会使用 oneway,比如 AMS 调用应用进程启动 Activity,这样就算应用进程中做了耗时的任务,也不会阻塞系统服务的运行。 41 | 42 | 串行化处理是指对于一个服务端的 AIDL 接口而言,所有的 oneway 方法不会同时执行,binder 驱动会将他们串行化处理,排队一个一个调用。 43 | 44 | **面试官**:有了解过相关的 binder 协议吗? 45 | 46 | 🤔️:了解过,图会更直观一些,我来画一下图吧,首先是非 oneway 的情况: 47 | 48 | ![](/img/binder.png) 49 | 50 | 如果是 oneway 的话,客户端就不需要挂起线程等待: 51 | 52 | ![](/img/binder_oneway.png) 53 | 54 | 涉及到的 binder 命令也有规律,由外部发送给 binder 驱动的都是 BC_ 开头,由 binder 驱动发往外部的都是 BR_开头。 55 | 56 | **面试官**:怎么理解客户端线程挂起等待呢?有没有实际占用 CPU 的调度? 57 | 58 | 🤔️:这里的挂起相当于 Thread 的 sleep,是真正的"休眠",底层调用的是 wait_event_interruptible() Linux 系统函数。 59 | 60 | **面试官**:你是从哪里了解到 wait_event_interruptible() 函数的呢? 61 | 62 | 🤔️:在学习 Handle 机制的时候,Handle 中最关键的地方就是 Looper 的阻塞与唤醒,阻塞是调用了 nativePollOnce() 方法,当时对它的底层实现感兴趣,就去了解了一下,也学习到 Linux 用来实现阻塞/唤醒的 select、poll 和 epoll 机制 63 | 64 | **面试官**:可以,我们再来聊聊别的。 -------------------------------------------------------------------------------- /策略模式.md: -------------------------------------------------------------------------------- 1 | 思考一下:实现某一功能可以有多种算法或策略,我需要根据条件选择相应的算法或策略来完成该功能。如:排序算法,可以使用冒泡排序、归并排序、快速排序等。要怎么实现呢? 2 | 3 | 很多同学想,写个类,每个方法是一种排序算法,然后再封装一个方法写 if...else... 来判断要用哪个算法,从而调用对应的方法。显然这样写会使这个类很臃肿,而且如果我要增加一个算法比如堆排序,就必须往里面加一个方法,然后在 if...else... 后面再加一个 else if. 这就明显违反了开闭原则和单一职责原则。 4 | 5 | 今天我们介绍的策略模式,就能优雅的实现这一需求。 6 | 7 | ## 定义 8 | 9 | 策略模式定义了一系列的算法,并将每一个算法封装起来,使他们可以相互替换。策略模式让算法独立于使用它的客户而独立变化。 10 | 11 | ## 使用场景 12 | 13 | * 针对同一类型问题的多种处理方式,仅仅是具体行为有差异 14 | * 需要安全地封装多种同一类型的操作时 15 | * 出现同一抽象类有多个子类(或接口的实现有多个),而又需要用 if-else 或 switch-case 来选择具体的实现时 16 | 17 | ## UML 18 | 19 | ![image-20200721005441905](img/image-20200721005441905.png) 20 | 21 | - Context 用来操作策略的上下文环境 22 | - Stragety 策略的抽象 23 | - ConcreteStragetyA、ConcreteStragetyB 具体策略的实现 24 | 25 | ## 实现 26 | 27 | 1. 将需要的算法或代码块抽象成接口或抽象类 28 | 2. 在原类中增加该算法接口的成员变量和 setter 29 | 3. 通过多种方式实现该算法接口 30 | 4. 把需要用到的算法实现通过 setter 注入到原类中,这样运行时会使用该方法 31 | 32 | ![img](img/举个栗子.jpg) 33 | 34 | 话说东汉末年分三国,烽火连天不休
周瑜想用美人钓来刘备,押住好换荆州
诸葛亮让赵云护送,并带着三个锦囊应对 35 | 36 | 显然这三个锦囊就是用来应对的策略,而为什么要用锦囊装着不告诉赵云(策略执行者),那是因为诸葛亮作为一个资深程序员,深知**迪米特法则**(aka 最少知识法则)。赵云只要在合适的时机打开锦囊并执行就可以了。 37 | 38 | ## Shut up and show me the code 39 | 40 | 锦囊接口 - 策略抽象 41 | 42 | ```KOTLIN 43 | interface Strategy { 44 | fun name(): String 45 | 46 | // 算法策略方法 47 | fun algorithm() 48 | } 49 | ``` 50 | 51 | 三个锦囊 - 具体策略 52 | 53 | ```JAVA 54 | class StrategyFirst : Strategy { 55 | override fun name(): String { 56 | return "第一个锦囊" 57 | } 58 | 59 | override fun algorithm() { 60 | println("拜见乔国老,并把刘备娶亲的事情搞得东吴人尽皆知") 61 | } 62 | } 63 | 64 | class StrategySecond : Strategy { 65 | override fun name(): String { 66 | return "第二个锦囊" 67 | } 68 | 69 | override fun algorithm() { 70 | println("用谎言(曹操打荆州)骗泡在温柔乡里的刘备回去荆州") 71 | } 72 | } 73 | 74 | class StrategyThird : Strategy { 75 | override fun name(): String { 76 | return "第三个锦囊" 77 | } 78 | 79 | override fun algorithm() { 80 | println("让孙尚香摆平东吴的追兵,她是孙权妹妹,东吴将领惧她三分") 81 | } 82 | } 83 | ``` 84 | 85 | 长山赵子龙 - 策略执行者 86 | 87 | ```kotlin 88 | class Context(val name: String) { 89 | var strategy: Strategy? = null 90 | 91 | fun doStrategy() { 92 | println("$name 打开了 ${strategy?.name()}:") 93 | strategy?.algorithm() 94 | } 95 | } 96 | ``` 97 | 98 | Action ! 导演开拍,诸葛亮是编剧 99 | 100 | ```kotLin 101 | fun main() { 102 | val childDragon = Context("赵云") 103 | // 到东吴打开第一个锦囊 104 | println("赵云领命,带了500 士兵护送刘备前去东吴。到了南徐") 105 | 106 | childDragon.strategy = StrategyFirst() 107 | childDragon.doStrategy() 108 | println("吴国太见刘备,同意婚事,假戏真做\n") 109 | 110 | // 到年底打开第二个锦囊 111 | println("刘备新婚度蜜月,忘回荆州,赵云几次劝告也无用,到年底") 112 | childDragon.strategy = StrategySecond() 113 | childDragon.doStrategy() 114 | println("次日江边祭祖,孙尚香一起逃往荆州\n") 115 | 116 | // 有追兵无路可走时打开第三个锦囊 117 | println("周瑜派兵追赶,拦住去路") 118 | childDragon.strategy = StrategyThird() 119 | childDragon.doStrategy() 120 | println("孙尚香怒斥追兵,追兵让路,周瑜后赶上时,关二爷带着援兵赶到\n") 121 | } 122 | ``` 123 | 124 | 最后我们来看戏 125 | 126 | ![image-20200721225839304](img/image-20200721225839304.png) 127 | 128 | ## 在安卓源码中应用 129 | 130 | 时间插值器 TimeInterpolator ,根据时间流逝的百分比来计算当前属性值的百分比。系统提供: 131 | - 线性插值器 LinearInterpolator 132 | - 加速减速插值器 AccelerateDecelerateInterpolator 133 | - 减速插值器 DecelerateInterpolator 。 134 | 即可以将不同的插值策略运行到属性动画中 135 | 136 | ## 最后 137 | 138 | 用好策略模式,你就是程序员中的“再世诸葛”。 -------------------------------------------------------------------------------- /简洁明了的刘海屏适配方案.md: -------------------------------------------------------------------------------- 1 | 网上关于刘海屏适配的文章不少,可讲清楚的却没几篇,大多是拷贝文档、长篇大论,甚至热情的贴图告诉你什么是刘海屏,到最后你仍不确定到底是怎样的一个适配方案,才能让你的 app 真正的适配所有的刘海屏机型。 2 | 3 | 看到这篇文章你就无需再怨恨各大厂商的跟风“刘海”了,因为刘海屏的适配十分简单。 4 | 5 | 6 | 7 | ok,废话说完了,开始适配。 8 | 9 | 首先要清楚的是哪些界面需要适配刘海屏: 10 | - 有状态栏的界面:刘海区域会显示状态栏,无需适配 11 | - 全屏界面:刘海区域可能遮挡内容,需要适配 12 | 13 | 如果你的应用里所有界面都有状态栏,那么恭喜你,你不用做任何操作,状态栏就那么自然的显示在刘海区域,毫无违和,刘海屏已适配完毕,可以点叉出去了。 14 | 15 | 不幸的是,你的应用中很大几率会有全屏界面,所谓的刘海屏适配,也正是针对这些全屏界面。 16 | 17 | 如果你什么都不做,默认规则不允许全屏界面内容显示到刘海区域,即刘海屏区域会保留一条黑边,你的全屏界面会在刘海下方展示,这看起来好像也是可以接受的,然后你竟说服产品达成共识,“无为而治”才是最强大的刘海屏适配方案! 18 | 19 | 但有些手机厂商(譬如oppo)不开心了,我辛辛苦苦研发的刘海屏手机,你们这些开发者竟直接放弃刘海区域!然后就在你的全屏界面下方加了一条提示:“全屏显示”,当用户点击开启后,强行把你的全屏界面显示到刘海区域,然后一切都乱套了... 20 | 21 | 嗯~ “无为而治”行不通。 22 | 23 | 只能允许全屏界面内容显示到刘海区域了,参考各大厂商的适配文档,我们可以知道如何允许,比如华为机型只需在 AndroidManifest 中配置: 24 | ``` 25 | 28 | ``` 29 | 配置后,华为机型上的全屏界面就会显示到刘海区域了,但这个刘海,是可能挡住我们全屏界面中的内容的。这时需要将全屏界面中的视图元素适当下移,保证不会被刘海遮挡住,就 ok 了。 30 | 31 | 这里我们搞清楚:允许全屏界面内容显示到刘海区域的机型,才需要将全屏界面中的视图元素适当下移。 32 | 33 | 比如若只允许华为机型全屏界面内容显示到刘海区域,那只有华为的刘海屏机型才需要将全屏界面中的视图元素适当下移,其他厂商的刘海屏机型则不需要下移。 34 | 35 | 如果允许华为、小米、oppo、vivo 全屏界面内容显示到刘海区域,那么华为、小米、oppo、vivo 刘海屏机型需要将全屏界面中的视图元素适当下移。 36 | 37 | 另外也不一定要通过全屏界面中的视图元素适当下移方式来适配刘海屏,如果产品形态允许的话,你也可以让该界面显示状态栏啊。 38 | 39 | 至此刘海屏适配完毕,是不是很简单!? 40 | 41 | 最后代码奉上,拿走不谢: 42 | 43 | #### 允许全屏界面内容显示到刘海区域配置: 44 | 45 | ``` 46 | 47 | 50 | 51 | 52 | 55 | 56 | 57 | 60 | ``` 61 | 上面在 AndroidManifest 的配置在 Android 9.0 之前有效,9.0 系统针对刘海屏制定了新的 api,默认保留一条黑边,即不允许绘制到刘海区域。所以如果你还没有适配 Android 9.0,那在判断是否是允许全屏界面内容显示到刘海区域的刘海屏机型时,就要加上版本判断。 62 | 63 | #### 判断是否是允许全屏界面内容显示到刘海区域的刘海屏机型: 64 | 65 | ```java 66 | public class CutoutUtil { 67 | 68 | private static Boolean sAllowDisplayToCutout; 69 | 70 | /** 71 | * 是否为允许全屏界面显示内容到刘海区域的刘海屏机型(与AndroidManifest中配置对应) 72 | */ 73 | public static boolean allowDisplayToCutout() { 74 | if (sAllowDisplayToCutout == null) { 75 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) { 76 | // 9.0系统全屏界面默认会保留黑边,不允许显示内容到刘海区域 77 | return sAllowDisplayToCutoutDevice = false; 78 | } 79 | Context context = App.get(); 80 | if (hasCutout_Huawei(context)) { 81 | return sAllowDisplayToCutout = true; 82 | } 83 | if (hasCutout_OPPO(context)) { 84 | return sAllowDisplayToCutout = true; 85 | } 86 | if (hasCutout_VIVO(context)) { 87 | return sAllowDisplayToCutout = true; 88 | } 89 | if (hasCutout_XIAOMI(context)) { 90 | return sAllowDisplayToCutout = true; 91 | } 92 | return sAllowDisplayToCutout = false; 93 | } else { 94 | return sAllowDisplayToCutout; 95 | } 96 | } 97 | 98 | 99 | /** 100 | * 是否是华为刘海屏机型 101 | */ 102 | @SuppressWarnings("unchecked") 103 | private static boolean hasCutout_Huawei(Context context) { 104 | if (!Build.MANUFACTURER.equalsIgnoreCase("HUAWEI")) { 105 | return false; 106 | } 107 | try { 108 | ClassLoader cl = context.getClassLoader(); 109 | Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); 110 | if (HwNotchSizeUtil != null) { 111 | Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen"); 112 | return (boolean) get.invoke(HwNotchSizeUtil); 113 | } 114 | return false; 115 | } catch (Exception e) { 116 | return false; 117 | } 118 | } 119 | 120 | /** 121 | * 是否是oppo刘海屏机型 122 | */ 123 | @SuppressWarnings("unchecked") 124 | private static boolean hasCutout_OPPO(Context context) { 125 | if (!Build.MANUFACTURER.equalsIgnoreCase("oppo")) { 126 | return false; 127 | } 128 | return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism"); 129 | } 130 | 131 | /** 132 | * 是否是vivo刘海屏机型 133 | */ 134 | @SuppressWarnings("unchecked") 135 | private static boolean hasCutout_VIVO(Context context) { 136 | if (!Build.MANUFACTURER.equalsIgnoreCase("vivo")) { 137 | return false; 138 | } 139 | try { 140 | ClassLoader cl = context.getClassLoader(); 141 | @SuppressLint("PrivateApi") 142 | Class ftFeatureUtil = cl.loadClass("android.util.FtFeature"); 143 | if (ftFeatureUtil != null) { 144 | Method get = ftFeatureUtil.getMethod("isFeatureSupport", int.class); 145 | return (boolean) get.invoke(ftFeatureUtil, 0x00000020); 146 | } 147 | return false; 148 | } catch (Exception e) { 149 | return false; 150 | } 151 | } 152 | 153 | /** 154 | * 是否是小米刘海屏机型 155 | */ 156 | @SuppressWarnings("unchecked") 157 | private static boolean hasCutout_XIAOMI(Context context) { 158 | if (!Build.MANUFACTURER.equalsIgnoreCase("xiaomi")) { 159 | return false; 160 | } 161 | try { 162 | ClassLoader cl = context.getClassLoader(); 163 | @SuppressLint("PrivateApi") 164 | Class SystemProperties = cl.loadClass("android.os.SystemProperties"); 165 | Class[] paramTypes = new Class[2]; 166 | paramTypes[0] = String.class; 167 | paramTypes[1] = int.class; 168 | Method getInt = SystemProperties.getMethod("getInt", paramTypes); 169 | //参数 170 | Object[] params = new Object[2]; 171 | params[0] = "ro.miui.notch"; 172 | params[1] = 0; 173 | return (Integer) getInt.invoke(SystemProperties, params) == 1; 174 | } catch (Exception e) { 175 | return false; 176 | } 177 | } 178 | 179 | } 180 | ``` 181 | 182 | 上面提到,不一定要通过全屏界面中的视图元素适当下移方式来适配刘海屏,如果产品形态允许的话,也可以让该界面显示状态栏。 183 | 184 | 显示状态栏的方案是较为通用简单的,或者说,在一个应用中,一些全屏界面往往是允许使用显示状态栏的方案来适配的,如果你考虑使用这种方案,那便会是这种效果: 185 | 186 | - 在你的应用中,你期望某些全屏界面在刘海屏机型上必须全屏展示,那你就自行将界面元素适当下移,从而避免被刘海遮挡;而某些全屏界面不是非要全屏显示,允许在刘海屏机型显示状态栏,那就通过显示状态栏的方式,从而避免被刘海遮挡。 187 | 188 | 为了实现这种效果,我们需要标记区分哪些界面必须全屏展示、哪些界面允许显示状态栏。这里提供一种实现方式,让允许显示状态栏的界面 Activity 继承一个接口,比如: 189 | 190 | ```java 191 | public interface CutoutAdapt { 192 | } 193 | ``` 194 | 195 | 然后在 ActivityLifecycleCallbacks 回调,统一适配允许通过显示状态栏的全屏界面: 196 | 197 | ```java 198 | @Override 199 | public void onActivityStarted(Activity activity) { 200 | // 如果是允许全屏显示到刘海屏区域的刘海屏机型 201 | if (CutoutUtil.allowDisplayToCutout()) { 202 | if (isFullScreen(activity)) { 203 | // 如果允许通过显示状态栏方式适配刘海屏 204 | if (activity instanceof CutoutAdapt) { 205 | // 显示状态栏 206 | StatusBarUtil.showStatusbar(activity.getWindow()); 207 | } else { 208 | // 需自行将该界面视图元素下移,否则可能会被刘海遮挡 209 | } 210 | } else { 211 | // 非全屏界面无需适配刘海屏 212 | } 213 | } 214 | } 215 | ``` 216 | -------------------------------------------------------------------------------- /组合模式.md: -------------------------------------------------------------------------------- 1 | > 组合模式(Composite Pattern)也称为部分整体模式(Part-Whole Pattern)结构型设计模式之一 2 | 3 | 它将一组对象看作是一个对象处理,并根据一个树状结构来组合对象,然后提供统一的方法去访问相应对象,以此忽略掉对象与对象集合之间的差别。 4 | 5 | 举个例子,公司的组织结构,总公司下面有行政部与研发部,而且总公司下面还有一个子公司。这个子公司也有独立的行政部和研发部。用树状图表示如下: 6 | 7 | ![image-20210620231859052](img/image-20210620231859052.png) 8 | 9 | 虽然子公司也包含行政部与研发部,但是从总公司的角度来看子公司就是一个独立的个体,与总公司的行政部和研发部平级,我们用一个盒子的形式来表达会更加直观 : 10 | 11 | ![盒子](img/盒子.jpg) 12 | 13 | 可以看到,虽然总公司和子公司的本质不一样,但是它在我们的组织结构 中是一样的,我们可以把它们看作是一个抽象的公司。这样的一个结构 就是组合模式的雏形。 14 | 15 | ## 定义 16 | 17 | 将对象组合树形结构,以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。 18 | 19 | ## 使用场景 20 | 21 | - 表示对象的部分-整体层次结构时 22 | - 从一个整体中能够独立出部分模块或功能的场景 23 | 24 | ## UML 25 | 26 | ![组合 uml](img/组合 uml.jpg) 27 | 28 | - Component: 抽象节点,为组合中的对象声明接口。实现所有类共有的接口的缺省行为 29 | - Composite: 定义有子节点的那些枝干节点的行为,存储子节点 30 | - Leaf: 在组合中表示叶子节点对象,叶子节点没有子节点 31 | - Client: 通过 Component 接口,操纵组合节点的对象 32 | 33 | ## Demo 34 | 35 | 将这个 UML 的翻译成代码如下: 36 | 37 | 抽象节点 38 | 39 | ```KOTLIN 40 | abstract class Component(protected val name: String) { 41 | // 具体逻辑方法由子类实现 42 | abstract fun doSomeThing() 43 | // 添加子节点 44 | abstract fun addChild(child: Component) 45 | // 移除子节点 46 | abstract fun removeChild(child: Component) 47 | // 获取对应下标的子节点 48 | abstract fun getChild(index: Int): Component 49 | } 50 | ``` 51 | 52 | 枝干节点: 53 | 54 | ```kotlin 55 | class Composite(name: String) : Component(name) { 56 | private val components = mutableListOf() 57 | override fun doSomeThing() { 58 | println(name) 59 | for (component in components) { 60 | component.doSomeThing() 61 | } 62 | } 63 | 64 | override fun addChild(child: Component) { 65 | components.add(child) 66 | } 67 | 68 | override fun removeChild(child: Component) { 69 | components.remove(child) 70 | } 71 | 72 | override fun getChild(index: Int): Component { 73 | return components[index] 74 | } 75 | } 76 | ``` 77 | 78 | 叶子节点,则更加简单: 79 | 80 | ``` 81 | class Leaf(name: String) : Component(name) { 82 | override fun doSomeThing() { 83 | println(name) 84 | } 85 | 86 | override fun addChild(child: Component) { 87 | throw UnsupportedOperationException("叶子节点没有子节点") 88 | } 89 | 90 | override fun removeChild(child: Component) { 91 | throw UnsupportedOperationException("叶子节点没有子节点") 92 | } 93 | 94 | override fun getChild(index: Int): Component { 95 | throw UnsupportedOperationException("叶子节点没有子节点") 96 | } 97 | } 98 | ``` 99 | 100 | 最后是客户端调用的测试代码 101 | 102 | ```kotlin 103 | fun main() { 104 | // 构造一个根节点 105 | val root = Composite("Root") 106 | 107 | // 构造两个枝干节点 108 | val branch1 = Composite("Branch1") 109 | val branch2 = Composite("Branch2") 110 | 111 | // 构造两个叶子节点 112 | val leaf1 = Leaf("Leaf1") 113 | val leaf2 = Leaf("Leaf2") 114 | 115 | // 将叶子节点添加至枝干节点中 116 | branch1.addChild(leaf1) 117 | branch2.addChild(leaf2) 118 | 119 | // 将枝干节点添加于根节点中 120 | root.addChild(branch1) 121 | root.addChild(branch2) 122 | 123 | // 执行方法 124 | root.doSomeThing() 125 | } 126 | ``` 127 | 128 | 很明显,这就是一个树的实现,而最后的执行代码就是树的“先序遍历”。执行的结果如下: 129 | 130 | ```properties 131 | Root 132 | Branch1 133 | Leaf1 134 | Branch2 135 | Leaf2 136 | ``` 137 | 138 | 这个例子可能还是有点抽象 139 | 140 | 那试想下另一个例子:把文件夹与子文件夹还有具体的文件代入到上述的组件中。如此一看文件系统也是一种典型的组合模式的例子。 141 | 142 | ## Android 源码中的应用 143 | 144 | 在 Android 源码中,几乎每天都会用到的 View , ViewGroup 的嵌套组合便是经典的组合模式。简单的画一下类图: 145 | 146 | ![viewGroupUml](img/viewGroupUml.jpg) 147 | 148 | 在 Android 中,容器一定是 ViewGroup, 只有 ViewGroup 才能包含其它的 View。比如 LinearLayout 里面可以包含其它 TextView, FrameLayout 等。但是反过来,TextView 是不能包含其它 View 的,因为 TextView 继承于 View, 不是一个容器(ViewGroup)。 149 | 150 | 这种 View 的视图层级 中使用到的是安全的组合模式。 151 | 152 | 对应的,上面 demo 里将组合所使用的方法全定义在抽象节点类中的方式称为透明的组合模式。 153 | 154 | ## 小结 155 | 156 | 平常在 Android 开发中组合模式的应用不多,其更适用于对一些界面 UI 的架构设计上,当然 UI 架构一般相应的如 AWT, Android, iOS 各自的 FrameWork 又都会提供,开发者并不需要自己去实现。 157 | 158 | #### 优点 159 | 160 | - 可以清楚的定义分层次的复杂对象,表示对象的部分或全部层次;它让高层模块忽略了层次的差异,方便对整个结构进行控制 161 | - 高层模块可以一致的使用一个组合结构 或其中单个对象,简化了高层模块的代码 162 | - 增加桂构件和叶子构件都很方便,无需理发现有类库,符合“开闭原则” 163 | 164 | #### 缺点 165 | 166 | - 新增构件时不好对枝干中的构件类型进行限制 ,不能依赖类型系统来施加约束。因为在设计时,它们都来自于相同的抽象 167 | 168 | ## End 吹斯汀 169 | 170 | 三个程序员被要求穿过一片田地,到达另一侧的房子。 171 | 菜鸟程序员目测了一下之间很短的距离,说:“不远!我只要十分钟。” 172 | 资深程序员看了一眼田地,想了一会,说:“我应该能在一天内过去。”菜鸟程序员很惊讶。 173 | 大神程序员看了一眼田地,说:“看起来要十分钟,但我觉得十五分钟应该够了。” 资深程序员冷笑了一声。 174 | 菜鸟程序员出发了,但只过了一会,地雷爆炸了,炸出了巨大的洞。这下他必须偏移预定的路线,原路返回,反复尝试穿过田地。最后他花了两天到达目的地,到的时候颤颤发抖,还受了伤。 175 | 资深程序员一出发就匍匐前进,仔细地拍打地面,寻找地雷,只有在安全的时候才前进。他在一天的时间内小心谨慎地缓慢爬过了这片地,只触发了几个地雷。 176 | 大神程序员出发之后径直穿过了田地,十分果断。他只用了十分钟就到了另一边。 177 | “你是怎么做到的?”另外两个人问道,“那些地雷怎么没有伤到你?” 178 | “很简单,”他回答道,“我最初就没有埋地雷。” -------------------------------------------------------------------------------- /观察者模式.md: -------------------------------------------------------------------------------- 1 | - ## 观察者模式 2 | 3 | >定义对象间的一种一对多的依赖关系,使得当每一个对象改变状态时,所有依赖于它的对象都会得到通知。 4 | 5 | 观察者模式想必大家都非常熟悉,这个模式最重要的作用就是解耦,将观察者和被观察者解耦,使得它们之前的依赖变小甚至毫无依赖。 6 | 7 | ## UML 8 | 9 | ![](img/观察者模式.jpg) 10 | 11 | 被观察者:Observable 12 | 13 | 观察者:Observer 14 | 15 | 具体实现类或子类:ConcreteObservable、ConcreteObserver 16 | 17 | ## 简单实现 18 | 19 | ```kotlin 20 | /** 21 | * 观察者:努力搬砖的程序员 22 | */ 23 | class Coder(var name: String) : Observer { 24 | override fun update(o: Observable, arg: Any) { 25 | println("hi $name,$arg") 26 | } 27 | } 28 | 29 | /** 30 | * 观察者:没有梦想的咸鱼 31 | */ 32 | class Fish: Observer { 33 | override fun update(o: Observable, arg: Any) { 34 | println("咸鱼划水中~~~~~~~~~~~~~~~~") 35 | } 36 | } 37 | 38 | /** 39 | * 被观察者:老板 40 | */ 41 | class Boss : Observable() { 42 | fun postMsg(msg: String?) { 43 | setChanged() //标记数据改变 44 | notifyObservers(msg) //通知 45 | } 46 | } 47 | 48 | fun main() {// 场景:老板通知程序员 49 | val boss = Boss() 50 | val fish = Fish() 51 | val coder1 = Coder("程序猿") 52 | val coder2 = Coder("程序媛") 53 | 54 | boss.addObserver(fish) 55 | boss.addObserver(coder1) 56 | boss.addObserver(coder2) 57 | 58 | boss.postMsg("客户明天要,今天全加班") 59 | boss.postMsg("有bug,谁的锅,赶紧 Fix!") 60 | boss.postMsg("公司困难,这个月工资延迟发") 61 | } 62 | ``` 63 | 64 | ![](img/image-20200817232940570.png) 65 | 66 | ## 总结 67 | 68 | - 优点:观察者与被观察者解耦,应对业务变化;增强系统灵活性、可扩展性 69 | - 缺点:java 中消息通知默认是顺序执行,一个观察者卡顿可能影响整体效率(采用异步可解) 70 | - 观察者模式简单易用,在 Android 源码中也应用广泛:Adapter 的 notifyDataSetChange ; OnClickListener 等事件监听;LiveData 等等太多了 -------------------------------------------------------------------------------- /解释器模式.md: -------------------------------------------------------------------------------- 1 | > 解释器模式(Interpreter Pattern)是一种用得比较少的行为模式,其提供了一种解释语言的语法或表达式的方式,该模式定义了一个表达式接口,通过该接口解释一个特定的上下文。常用的设计模式大部分人都会,这种冷知识才是装逼利器,接下来我们一起来学习吧。 2 | 3 | ## 定义 4 | 5 | 给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 6 | 7 | 可能很多读者根本看不懂这句话的意思,我们来举个例子。 8 | 给定一个语言(正则表达式),定义它的文法的一种表示(如 \d+),并定义一个解释器,该解释器用来解释语言中的句子(解释器也可以简单的理解为翻译机)。\d+ 翻译成自然语言就是一个或多个的数字(如 1,2,12358)。这样一来你是否对解释器模式的定义有了更进一步的理解了。 9 | 10 | ## 使用场景 11 | 12 | #### 如果某个简单的语言需要解释执行而且可以将该语言中的语句表示为一个抽象语法树时,可以考虑使用 13 | 14 | 举个例子:一个简单含有加减运算的数学表达式:p+q+m-n, 像这样的表达式其构成只有两种:一是以 p, q, m, n 这类的具体参数表示的符号,其无法再被推导,也称为终结符号。另一种则是以"+" 和 "-"构成的算术运算符,在运算符的两边总能找到有意义的具体计算参数,我们也称其为非终结符。p+q+m-n 我们也可以将其表示为一棵抽象语法树,如下图: 15 | 16 | ![](img/pqmn.jpg) 17 | 18 | #### 在某些特定的领域出现不断重复的问题时,可以将该领域的问题转化为一种语法规则下的语句,然后构建解释器来解释该语句 19 | 20 | 这个就比较好理解了,比如将阿拉伯数字转换成中文数字;再比如将小写英文单词转换为全大写的。这些其实就是一个不断重复的问题,就可以尝试使用解释器模式 21 | 22 | ## UML 23 | 24 | ![InterpreterUML](img/InterpreterUML.jpg) 25 | 26 | 27 | 28 | - AbstractExpression: 抽象表达式 29 | 声明一个抽象的解释操作父类,并定义一个抽象的解释方法。其具体实现在各个具体的子类 30 | - TerminalExpression: 终结符表达式 31 | 实现文法中与终结符有关的解释操作。文法中每个终结符都有一个具体的表达式与之对应 32 | - NonterminalExpression: 非终结符表达式 33 | 实现方文法中与非终结符有关的解释操作 34 | - Context: 上下文环境类,包含解释器之外的全局信息 35 | - Client: 客户类。解析表达式,构建抽象语法树,执行具体的解释操作等 36 | 37 | ## 简单实现 38 | 39 | 我们来实现前文提到的例子(p + q + m - n),用解释器模式对算术表达式进行解释 40 | 41 | 我们先来定义需要用的解释器: 42 | 43 | ```kotlin 44 | abstract class ArithmeticExpression { 45 | /** 46 | * 抽象的解析方法 47 | * 具体的解析逻辑同具体的子类实现 48 | * @return 解析得到具体的值 49 | */ 50 | abstract fun interpret(): Int 51 | } 52 | 53 | // 数字解释器,仅仅是为了解释数字 54 | class NumberExpression(val value: Int) : ArithmeticExpression() { 55 | override fun interpret(): Int { 56 | return value 57 | } 58 | } 59 | 60 | // 运算符号抽象解释器 61 | abstract class OperatorExpression( 62 | // 存储运算符两边的数字 63 | val exp1: ArithmeticExpression, 64 | val exp2: ArithmeticExpression 65 | ) : ArithmeticExpression() 66 | 67 | // 加法运算解释器 68 | class AdditionExpression(exp1: ArithmeticExpression, exp2: ArithmeticExpression) 69 | : OperatorExpression(exp1, exp2) { 70 | override fun interpret(): Int { 71 | return exp1.interpret() + exp2.interpret() 72 | } 73 | } 74 | 75 | // 减法运算解释器 76 | class SubtractionExpression(exp1: ArithmeticExpression, exp2: ArithmeticExpression) 77 | : OperatorExpression(exp1, exp2) { 78 | override fun interpret(): Int { 79 | return exp1.interpret() - exp2.interpret() 80 | } 81 | } 82 | ``` 83 | 84 | 除了解释器之外 ,我们创建一个 Calculator 来处理计算的相关业务 85 | 86 | ```KOTLIN 87 | class Calculator { 88 | 89 | // 计算表达式如 calc("1 + 1") 90 | fun calc(expression: String) :Int{ 91 | // 存储并操作所有相关的解释器 92 | val stack = Stack() 93 | val elements = expression.split(" ") 94 | var i = 0 95 | while (i <= elements.lastIndex) { 96 | val element = elements[i] 97 | when (element) { 98 | "+", "-" -> { // 是符号 99 | // 栈顶的解释器作为计算式左边的一个数字解释器 100 | val exp1 = stack.pop() 101 | // 数组下一个元素为计算式右边的数字解释器 102 | val exp2 = NumberExpression(elements[++i].toInt()) 103 | val operatorExpression = if (element == "+") { 104 | AdditionExpression(exp1, exp2) 105 | } else { 106 | SubtractionExpression(exp1, exp2) 107 | } 108 | stack.push(operatorExpression) 109 | } 110 | else -> { // 是数字 111 | stack.push(NumberExpression(element.toInt())) 112 | } 113 | } 114 | i++ 115 | } 116 | 117 | return stack.pop().interpret() 118 | } 119 | } 120 | ``` 121 | 122 | 这里要注意的是,为了简化问题,我们约定表达式字符串的每个元素之间必须使用空格隔开。如"1 + 1"的表达式是合法的,而"1 +1"则不合法. 123 | 124 | 最后我们运行一下测试代码: 125 | 126 | ```kotlin 127 | fun main() { 128 | val calculator = Calculator() 129 | val express = "2021 + 3 + 1 - 1359" 130 | print("$express = ${calculator.calc(express)}") 131 | } 132 | 133 | // 输出结果: 134 | 2021 + 3 + 1 - 1359 = 666 135 | ``` 136 | 137 | ## Android 源码中的实现 138 | 139 | 对 Android 来说,解释器模式的应用并不多见,我们也很难在系统源码中找到其经典实现,但是,我们依然可以在一些地方看到对解释器模式的应用。 140 | 141 | 所有的 Android 开发应该者对 AndroidManifest 很熟悉,这是应用的配置文件。如果说 App 是一本书的话,这个配置文件就相当于书的目录,其中包含了四大组件,权限等的声明定义。那么在 Android 中,是如何读取这个配置文件的呢?答案就在 PackageParser 这个类里。该类为 AndroidManifest 里的每一个组件标签创建了相应的类用于存储相关信息。 142 | 143 | ```java 144 | package android.content.pm; 145 | public class PackageParser { 146 | // 以下其实是 public final static class,省略 147 | class Activity extends Component{} 148 | class Permission extends Component{} 149 | class PermissionGroup extends Component{} 150 | class Provider extends Component {} 151 | class Service extends Component{} 152 | ... 153 | } 154 | ``` 155 | 156 | 如上所示,PackageParser 为 Activity, Service, Provider 等构件在其内部以内部类的方式创建了对应的类。按照解释器模式的定义,这些类对应 AndroidManifest.xml 里的一个标签,也就是一条文法。在对该配置文件的解释时充分去用了解释器模式分离实现,解释执行的特性。涉及到的方法如下,不做过深入的分析: 157 | 158 | ```java 159 | PackageManagerService.scanPackageLI(...) 160 | PackageParser.parsePackage(...) 161 | PackageParser.parseActivity(...) 162 | PackageParser.parseProvider(...) 163 | ... 164 | ``` 165 | 166 | 在 Android 中,解析某个 Apk 文件会调用到 PMS 的 scanPackageLI 方法,解析后的信息再保存回 PMS。而 PMS 会通过调用 PackageParser 类的相关方法去解析 AndroidManifest 里的各个节点信息。 167 | 168 | ## 小结 169 | 170 | #### 优点 171 | 172 | 灵活的扩展性,当我们想对文法规则进行扩展延伸时,只需要增加相应的非终结符解释器,并在构建 抽象语法树时,使用到新增的解释器对象进行具体的解释即可。 173 | 174 | #### 缺点 175 | 176 | 第一条文法对应至少一个解释器,会生成大量类,增加维护难度;同时过于复杂的文法,构建其抽象语法树会显得异常烦琐,甚至有可能需要构建多棵抽象语法树,因此对于过复杂文法并不推荐使用解释器模式。 177 | 178 | -------------------------------------------------------------------------------- /访问者模式.md: -------------------------------------------------------------------------------- 1 | > 访问者模式是一种将数据操作与数据结构分享的设计模式,是 23 种设计模式中最复杂的一个 2 | 3 | ## 定义 4 | 5 | 访问者模式(Visitor Pattern) 封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。 6 | 7 | ## 使用场景 8 | 9 | 1. 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。 10 | 2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。 11 | 12 | ## UML 13 | 14 | ![img](img/webp) 15 | 16 | - Visitor:抽象的访问者类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。 17 | - ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。 18 | - Element:抽象的元素类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。 19 | - ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。 20 | - ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。 21 | 22 | ## 举个例子 23 | 年底,CEO, CTO 开始评员工绩效了,员工分为工程师和经理;假定 CEO 关注的是工程师的 KPI 和经理的 KPI 以及经理的新产品的数量;而 CTO 只关注工程师的代码量(没想到真的有这样的公司)、新产品数量。CEO, CTO (访问者)访问不同的数据(员工报表)了。 24 | ```KOTLIN 25 | // 员工基类 26 | abstract class Staff(var name: String) { 27 | var kpi: Int 28 | init { 29 | kpi = Random().nextInt(10) 30 | } 31 | // 接受 Visitor 的访问 32 | abstract fun accept(visitor: Visitor) 33 | } 34 | ``` 35 | 36 | Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码: 37 | 38 | ```KOTLIN 39 | class Engineer(name: String) : Staff(name) { 40 | override fun accept(visitor: Visitor) { 41 | visitor.visit(this) 42 | } 43 | // 代码行数,随机10万内模拟 44 | fun getCodeLine(): Int { 45 | return Random().nextInt(100_000) 46 | } 47 | } 48 | 49 | class Manager(name: String) : Staff(name) { 50 | // 一年做的产品数量,随机10以内模拟 51 | var products: Int = Random().nextInt(10) 52 | override fun accept(visitor: Visitor) { 53 | visitor.visit(this) 54 | } 55 | } 56 | ``` 57 | 58 | 从工程师和经理的代码我们也可以看出,访问者接口需要有能够访问这两个类型方法: 59 | 60 | ```kotlin 61 | interface Visitor { 62 | // 访问工程师类型 63 | fun visit(engineer: Engineer) 64 | 65 | // 访问经理类型 66 | fun visit(manager: Manager) 67 | } 68 | ``` 69 | 70 | 工程师、经理数据,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断: 71 | 72 | ```KOTLIN 73 | // 非访问者模式示例 74 | class Visitor { 75 | fun visit(staff: Staff) { 76 | if (staff is Engineer) { 77 | // do something for Engineer 78 | }else if (staff is Manager) { 79 | // do something for Manager 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | 这这就导致了 **if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护**,当类型较多时,这个方法就会很复杂 86 | 87 | 好了,接着写访问者模式的代::CEO, CTO 的代码 88 | 89 | ```kotlin 90 | // 前面说过了,CEO 关注的是: 91 | // 工程师的 KPI 和经理的 KPI 以及经理的新产品的数量 92 | class CEO : Visitor { 93 | override fun visit(engineer: Engineer) { 94 | println("工程师:${engineer.name}, KPI: ${engineer.kpi}") 95 | } 96 | 97 | override fun visit(manager: Manager) { 98 | println("经理:${manager.name}, KPI: ${manager.kpi}, 新产品数:${manager.products}") 99 | } 100 | } 101 | 102 | // CTO 关注:而 CTO 只关注工程师的代码量、新产品数量 103 | class CTO : Visitor { 104 | override fun visit(engineer: Engineer) { 105 | println("工程师:${engineer.name}, 代码量: ${engineer.getCodeLine()} 行") 106 | } 107 | 108 | override fun visit(manager: Manager) { 109 | println("经理:${manager.name}, 新产品数:${manager.products}") 110 | } 111 | } 112 | ``` 113 | 114 | 把员工添加到业务表类中,封装方法,公司高层可以一键查看所有员工的业绩: 115 | 116 | ```kotlin 117 | // 员工业务报表 118 | class BusinessReport { 119 | val staffs: MutableList = mutableListOf( 120 | Engineer("Erich Gamma"), 121 | Engineer("Richard Helm"), 122 | Engineer("Ralph Johnson"), 123 | Engineer("John Vlissides"), 124 | Manager("GoF") 125 | ) 126 | fun showReport(visitor: Visitor) { 127 | for (staff in staffs) { 128 | staff.accept(visitor) 129 | } 130 | } 131 | } 132 | ``` 133 | 134 | 最后编写测试代码: 135 | 136 | ```kotlin 137 | fun main() { 138 | val businessReport = BusinessReport() 139 | println("=========== CEO看报表 ===========") 140 | val ceo = CEO() 141 | businessReport.showReport(ceo) 142 | println("=========== CTO看报表 ===========") 143 | val cto = CTO() 144 | businessReport.showReport(cto) 145 | } 146 | ``` 147 | 148 | 运行结果: 149 | 150 | ```kotlin 151 | =========== CEO看报表 =========== 152 | 工程师:Erich Gamma, KPI: 7 153 | 工程师:Richard Helm, KPI: 5 154 | 工程师:Ralph Johnson, KPI: 1 155 | 工程师:John Vlissides, KPI: 3 156 | 经理:GoF, KPI: 7, 新产品数:9 157 | =========== CTO看报表 =========== 158 | 工程师:Erich Gamma, 代码量: 97528 行 159 | 工程师:Richard Helm, 代码量: 27825 行 160 | 工程师:Ralph Johnson, 代码量: 38738 行 161 | 工程师:John Vlissides, 代码量: 8273 行 162 | 经理:GoF, 新产品数:9 163 | ``` 164 | 165 | 这个例子对应 UML 中的角色 166 | 167 | - Staff 是 Element 168 | - Engineer, Manager 是 ConcreteElement 169 | - CEO, CTO 是 ConcreteVisitor 170 | - BusinessReport 是 ObjectStructure 171 | - 测试代码 则是 Client 172 | 173 | ## Android 中的应用 174 | 175 | 编译时注解的核心原理依赖 APT (Annotation Processing Tools) 实现,而这个 APT 与 176 | 177 | #### Element 178 | 179 | 对编译器来说,代码中的无线结构 是基本不变的,如:包、类、函数、字段、类型参数、变量。所以在 JDK 中为这些元素定义了一个基类,也就是 Element 类。它还有几个子类 :PackageElement, TypeElement, ExecutableElement, VariableElement, TypeParameterElement。我们来看一下 Element 的部分代码: 180 | 181 | ```JAVA 182 | // javax.lang.model.element.Element 183 | public interface Element extends AnnotatedConstruct { 184 | ElementKind getKind(); 185 | 186 | Set getModifiers(); 187 | 188 | Name getSimpleName(); 189 | 190 | R accept(ElementVisitor var1, P var2); 191 | } 192 | ``` 193 | 194 | 这个 Element 的接口中,最显眼的就是 `accept`方法了,而这个方法接收的 ElementVisitor 就是访问者了 195 | 196 | #### Visitor 197 | 198 | ```java 199 | // javax.lang.model.element.ElementVisitor 200 | public interface ElementVisitor { 201 | R visit(Element var1, P var2); 202 | 203 | R visit(Element var1); 204 | 205 | R visitPackage(PackageElement var1, P var2); 206 | 207 | R visitType(TypeElement var1, P var2); 208 | 209 | R visitVariable(VariableElement var1, P var2); 210 | 211 | R visitExecutable(ExecutableElement var1, P var2); 212 | 213 | R visitTypeParameter(TypeParameterElement var1, P var2); 214 | 215 | R visitUnknown(Element var1, P var2); 216 | } 217 | ``` 218 | 219 | 在 ElementVisitor 中定义了多个 visit 接口,每个接口处理一种元素类型,这就是典型的访问者模式! 220 | 221 | 而具体的访问者有:SimpleElementVisitor6, ElementKindVisitor6 等等,这里就不展开说了,有兴趣的直接去看源码 222 | 223 | #### 流程 224 | 225 | 首先,编译器将代码抽象成一个代码元素的树,然后在编译时对整棵树进行遍历访问,每个元素都有一个 `accept` 函数接受访问都的访问,每个访问都中有对应的 `visit`函数 ,在每个`visit`中进行不同的处理,这样就达到了差异处理的效果,同时将数据结构分离,使得每个类型的职责单一,易于升级维护。JDK 还特意预留了 visitUnknown 接口来应对 Java 语言后续 发展可能添加元素类型的问题,灵活的化解了访问都模式的缺点。 226 | 227 | ## 总结 228 | 229 | #### 优点 230 | 231 | 1. **各角色职责分离,符合单一职责原则** 232 | 通过UML类图和上面的示例可以看出来,Visitor、ConcreteVisitor、Element 、ObjectStructure,职责单一,各司其责。 233 | 2. **具有优秀的灵活性,扩展性** 234 | 如果需要增加新的访问者,增加实现类 ConcreteVisitor 就可以快速扩展。 235 | 3. **使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化** 236 | 员工属性(数据结构)和 CEO、CTO 访问者(数据操作)的解耦。 237 | 238 | #### 缺点 239 | 240 | 1. **具体元素对访问者公布细节,违反了迪米特原则** 241 | CEO、CTO 需要调用具体员工的方法。 242 | 2. **具体元素变更时导致修改成本大** 243 | 变更员工属性时,多个访问者都要修改。 244 | 3. **违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没依赖抽象** 245 | 访问者 visit 方法中,依赖了具体员工的具体方法。 246 | 247 | 我们要根据具体情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,是否需要经常定义新的操作,使用访问者模式是否能优化我们的代码,而不是使我们的代码变得更复杂。 248 | 249 | GoF 对访问者模式的描述为:大多数情况下你不需要使用,但是当你一旦需要使用它时,那你就真的需要它了。 -------------------------------------------------------------------------------- /说一说 Android 消息机制.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | 5 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 6 | 7 | **面试官**:说一说 Android 的消息机制 8 | 9 | 😎:就是可以通过 Handler 发送消息,这个消息会在 Handler 指定的线程处理。处理线程默认为 new Handler 时所在的线程;也可以通过构造函数传入一个 Looper ,则处理线程为 Looper 所在的线程。 10 | 11 | **面试官**:这个消息机制有哪些应用场景? 12 | 13 | 😎:在子线程处理完耗时操作,可以通过 Handler 发送消息,在主线程处理界面刷新;另外一点就是可以发送延时消息,用来做一些延时操作。 14 | 15 | **面试官**:你知道延时消息的原理吗? 16 | 17 | 😎:不知道,会用不就行了? 18 | 19 | **面试官**:好的,回去等通知吧 20 | 21 | 22 | 23 | --- 24 | 25 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 26 | 27 | **面试官**:说一说 Android 的消息机制 28 | 29 | 😨:Android 的消息机制主要是指 Handler 的运行机制以及 Handler 所附带的 MessageQueue 和 Looper 的工作流程。消息的处理流程是这样的: 30 | 31 | 1. 通过 Handler 的 sendXXX 或 postXXX 系列方法发送一条消息 32 | 2. 这条消息插入到 MessageQueue 中的指定位置 33 | 3. Looper.loop() 不断的调用 MessageQueue.next() 取出当前需要处理的消息,若当前无消息则阻塞到有新消息 34 | 4. 取出来的消息,通过 Handler.dispatchMessage 进行分发处理 35 | 36 | **面试官**:你知道延时消息的原理吗? 37 | 38 | 😨:发送的消息 Message 有一个属性 when 用来记录这个消息需要处理的时间,when 的值:普通消息的处理时间是当前时间;而延时消息的处理时间是当前时间 + delay 的时间。Message 会按 Message.when 递增插入到 MessageQueue ,也就是越早时间的排在越前面。 39 | 40 | 接下来就是消息的处理了,Looper.loop() 不断调用 MessageQueue.next() 取出当前需要处理的消息,而 next() 方法会判断队头消息的 Message.when ,如果时间还没到,就休眠到指定时间;如果当前时间已经到了,就返回这个消息,交给 Handler 去分发。这样就实现处理延时消息了。 41 | 42 | **面试官**:Handler 是怎么分发消息的? 43 | 44 | 😨:简单的只需要看源码 45 | 46 | ``` java 47 | public void dispatchMessage(Message msg) { 48 | if (msg.callback != null) { 49 | // callback 的值为通过 Handler.post(Runnable) 方法发送消息时传入的 Runnable 50 | message.callback.run(); 51 | } else { 52 | if (mCallback != null) { 53 | // 这个 mCallback 是调用在 Handler 构造函数时可选传入的 54 | // 传入 CallBack 就省得为了重载 handleMessage 而新写一个 Handler 的子类 55 | if (mCallback.handleMessage(msg)) { 56 | return; 57 | } 58 | } 59 | // 最后就是交给 Handler 自己处理 Message 啦 60 | handleMessage(msg); 61 | } 62 | } 63 | ``` 64 | 65 | 很有意思的一个点,mCallback 处理完如果返回 false,还是会继续往下走,再交给 Handler.handleMessage 处理的。所以这边可以通过反射去 hook 一个 Handler ,可以监听 Handler 处理的每个消息,也可以改 msg 里面的值。 66 | 67 | **面试官**:主线程的 Looper.loop() 在死循环,为什么没有 ANR 呢? 68 | 69 | 😨:这个还真没想过 70 | 71 | **面试官**:好的,回去等通知吧 72 | 73 | --- 74 | 75 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 76 | 77 | **面试官**:主线程的 Looper.loop() 在死循环,为什么没有 ANR 呢? 78 | 79 | 🤔️:首先 ANR 的根本原因是:应用未在规定的时间内处理 AMS 指定的任务才会 ANR。AMS 调用到应用端的 Binder 线程,应用再将任务封装成 Message 发送到主线程 Handler ,Looper.loop() 通过 MessageQueue.next() 拿到这个消息进行处理。如果不能及时处理这个消息呢,肯定是因为在它前面有耗时的消息处理,或者因为这个任务本身就很耗时。所以 ANR 不是因为 loop 循环,而是因为主线程中有耗时任务。 80 | 81 | **面试官**:主线程的 Looper.loop() 在死循环,会很消耗资源吗? 82 | 83 | 🤔️: Looper.loop() 通过 MessageQueue.next() 取当前需要处理的消息,如果当前没有需要处理的消息呢,会调用 nativePollOnce 方法让线程进入休眠。当消息队列没有消息时,无限休眠;当队列的第一个消息还没到需要处理的时间时,则休眠时间为 Message.when - 当前的时间。这样在空闲的时候主线程也不会消耗额外的资源了。而当有新消息入队时 enqueueMessage 里会判断是否需要通过 nativeWake 方法唤醒主线程来处理新消息。唤醒最终是通过往 eventfd 发起一个写操作,这样主线程就会收到一个可读事件进而从休眠状态被唤醒。 84 | 85 | **面试官**:你知道 IdleHandler 吗? 86 | 87 | 🤔️:IdleHandler 是通过 MessageQueue.addIdleHandler() 来添加到 MessageQueue 的,前面提到当 MessageQueue.next() 当前没有需要处理消息的时候就会进入休眠,而在进入休眠之前呢,就会调用 IdleHandler 接口里的 boolean queueIdle() 方法。这个方法的返回 true 则调用后保留,下次队列空闲时还会继续调用;而如果返回 false 调用完就被 remove 了。可以用来做延时加载,而且是在空闲时加载,不像 Handler.postDelayed 需要指定延时的时间。 88 | 89 | **面试官**:可以,我们再来聊聊别的。 90 | 91 | --- 92 | 93 | > 看完了这三位同学的面试表现,你有什么感想呢?欢迎关注 “Android 面试官” 在公众号后台留言讨论 94 | 95 | 96 | -------------------------------------------------------------------------------- /说说你对 binder 驱动的了解?.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题:说说你对 binder 驱动的了解。这个问题虽有些 "面试造火箭" 的无奈,可难点就是亮点、价值所在,是筛选面试者的有效手段。如果让你回答,你能说出多少呢?我们来看看 😎、😨 和 🤔️ 三位同学的回答如何吧 2 | 3 | --- 4 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 5 | 6 | **面试官**️:说说你对 binder 驱动的了解 7 | 8 | 😎:binder 驱动是很底层的东西,在系统内核中,是 binder 机制的基石。 9 | 10 | **面试官**:没了吗?把你了解的都说一下 11 | 12 | 😎:直接让我说了解不好回答啊,还是问我问题吧 13 | 14 | **面试官**:好,你刚才提到了系统内核,那介绍一下用户空间和内核空间吧 15 | 16 | 😎:不知道,这东西了解了也没什么用啊!我对业务开发 API 比较了解,比如 RecycleView 布局,我写的贼溜~ 17 | 18 | **面试官**:好的,回去等通知吧 19 | 20 | --- 21 | 22 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 23 | 24 | **面试官**:说说你对 binder 驱动的了解 25 | 26 | 😨:binder 机制分为四部分,binder 驱动、Service Manager、客户端、服务端。类比网络通信,Service Manager 是 DNS,binder 驱动就是路由器,它运行在内核空间,不同进程间通过 binder 驱动才能通信。 27 | 28 | **面试官**:为什么 binder 驱动要运行在内核空间?可以移到用户空间吗? 29 | 30 | 😨:不行,两个进程的进程空间有不同的虚拟地址映射规则,内存是不共享的,无法直接通信。Linux 把进程空间划分为用户空间和内核空间,分别运行用户程序和系统内核。 31 | 32 | 用户空间和内核空间虽也是隔离的,但可以通过 copy_from_user 将数据从用户空间拷贝到内核空间,通过 copy_to_user 将数据从内核空间拷贝到用户空间。 33 | 34 | 所以 binder 驱动要处于内核空间,才能实现两个进程间的通信。一般的 IPC 方式需要分别调用这两个函数,数据就拷贝了两次,而 binder 将内核空间与目标用户空间进行了 mmap,只需调 copy_from_user 拷贝一次即可。 35 | 36 | **面试官**:从用户空间如何调用内核空间的 binder 驱动呢? 37 | 38 | 😨:这个不了解了,我没看过 binder 源码,只是知道大概的通信方式 39 | 40 | **面试官**:那你对 binder 驱动还有哪些了解,都说说吧 41 | 42 | 😨:嗯... 没有了 43 | 44 | **面试官**:好的,回去等通知吧 45 | 46 | --- 47 | 48 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 49 | 50 | **面试官**:说说你对 binder 驱动的了解 51 | 52 | 🤔️:简单画张图吧: 53 | 54 | ![](/img/binder_driver.jpg) 55 | 56 | 对 Binder 机制来说,它是 IPC 通信的路由器,负责实现不同进程间的数据交互,是 Binder 机制的核心;对 Linux 系统来说,它是一个字符驱动设备,运行在内核空间,向上层提供 /dev/binder 设备节点及 open、mmap、ioctl 等系统调用。 57 | 58 | **面试官**:你提到了驱动设备,那先说说 Linux 的驱动设备吧 59 | 60 | 🤔️:Linux 把所有的硬件访问都抽象为对文件的读写、设置,这一"抽象"的具体实现就是驱动程序。驱动程序充当硬件和软件之间的枢纽,提供了一套标准化的调用,并将这些调用映射为实际硬件设备相关的操作,对应用程序来说隐藏了设备工作的细节。 61 | 62 | Linux 驱动设备分为三类,分别是字符设备、块设备和网络设备。字符设备就是能够像字节流文件一样被访问的设备。对字符设备进行读/写操作时,实际硬件的 I/O 操作一般也紧接着发生。字符设备驱动程序通常都会实现 open、close、read 和 write 系统调用,比如显示屏、键盘、串口、LCD、LED 等。 63 | 64 | 块设备指通过传输数据块(一般为 512 或 1k)来访问的设备,比如硬盘、SD卡、U盘、光盘等。网络设备是能够和其他主机交换数据的设备,比如网卡、蓝牙等设备。 65 | 66 | 字符设备中有一个比较特殊的 misc 杂项设备,设备号为 10,可以自动生成设备节点。Android 的 Ashmem、Binder 都属于 misc 杂项设备。 67 | 68 | **面试官**:看过 binder 驱动的 open、mmap、ioctl 方法的具体实现吗? 69 | 70 | 🤔️:它们分别对应于驱动源码 binder.c 中的 binder_open()、binder_mmap()、binder_ioctl() 方法,binder_open() 中主要是创建及初始化 binder_proc ,binder_proc 是用来存放 binder 相关数据的结构体,每个进程独有一份。 71 | 72 | binder_mmap() 的主要工作是建立应用进程虚拟内存在内核中的一块映射,这样应用程序和内核就拥有了共享的内存空间,为后面的一次拷贝做准备。 73 | 74 | binder 驱动并不提供常规的 read()、write() 等文件操作,全部通过 binder_ioctl() 实现,所以 binder_ioctl() 是 binder 驱动中工作量最大的一个,它承担了 binder 驱动的大部分业务。 75 | 76 | **面试官**:仅 binder_ioctl() 一个方法是怎么实现大部分业务的? 77 | 78 | 🤔️:binder 机制将业务细分为不同的命令,调用 binder_ioctl() 时传入具体的命令来区分业务,比如有读写数据的 BINDER_WRITE_READ 命令、 Service Manager 专用的注册为 DNS 的命令等等。 79 | 80 | BINDER_WRITE_READ 命令最为关键,其细分了一些子命令,比如 BC_TRANSACTION、BC_REPLY 等。BC_TRANSACTION 就是上层最常用的 IPC 调用命令了,AIDL 接口的 transact 方法就是这个命令。 81 | 82 | **面试官**:binder 驱动中要实现这些业务功能,必然要用一些数据结构来存放相关数据,比如你上面说 binder_open() 方法时提到的 binder_proc,你还知道其他的结构体吗? 83 | 84 | 🤔️:知道一些,比如: 85 | 86 | | 结构体 | 说明 | 87 | |--|--| 88 | | binder_proc | 描述使用 binder 的进程,当调用 binder_open 函数时会创建 | 89 | | binder_thread | 描述使用 binder 的线程,当调用 binder_ioctl 函数时会创建 | 90 | | binder_node | 描述 binder 实体节点,对应于一个 serve,即用户态的 BpBinder 对象 | 91 | | binder_ref | 描述对 binder 实体节点的引用,关联到一个 binder_node | 92 | | binder_buffer | 描述 binder 通信过程中存储数据的Buffer | 93 | | binder_work | 描述一个 binder 任务 | 94 | | binder_transaction | 描述一次 binder 任务相关的数据信息 | 95 | | binder_ref_death | 描述 binder_node 即 binder server 的死亡信息 | 96 | 97 | 其中主要结构体引用关系如下: 98 | 99 | ![](/img/binder_proc.png) 100 | 101 | **面试官**:可以,我们再来聊聊别的。 102 | 103 | >这个问题虽有些 "面试造火箭" 的无奈,可难点就是亮点、价值所在,是筛选面试者的有效手段。如果问你这个问题,你能回答多少呢? 104 | 105 | ![](/img/binder_code.png) 106 | 107 | >如上图,这里有一份按模块分好的 Binder 源码,并有关键步骤注释。关注公众号 **Android 面试官** 留言:binder,即可获此 Binder 学习必备源码~ -------------------------------------------------------------------------------- /谈谈你对 ThreadLocal 的理解.md: -------------------------------------------------------------------------------- 1 | ThreadLocal 对有些同学来说可能有点陌生,今天我们就一起来学习一下。 2 | 3 | ## What 4 | 5 | ThreadLocal 直译就是**线程本地变量**,意思是 ThreadLocal 中填充的变量属于**当前**线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。 6 | 7 | ## How 8 | 9 | ThreadLocal 最主要的作用就是线程隔离,在 Android 的消息机制中,很重要的一个角色 Looper,其中每个线程都要有自己的 Looper,这就是通过 ThreadLocal 实现的。直接看代码 10 | 11 | ```java 12 | public final class Looper { 13 | // sThreadLocal.get() will return null unless you've called prepare(). 14 | static final ThreadLocal sThreadLocal = new ThreadLocal(); 15 | 16 | private static void prepare(boolean quitAllowed) { 17 | if (sThreadLocal.get() != null) { 18 | throw new RuntimeException("Only one Looper may be created per thread"); 19 | } 20 | // Setter 21 | sThreadLocal.set(new Looper(quitAllowed)); 22 | } 23 | 24 | /** 25 | * Return the Looper object associated with the current thread. Returns 26 | * null if the calling thread is not associated with a Looper. 27 | */ 28 | public static @Nullable Looper myLooper() { 29 | // Getter 返回当前线程的 Looper 30 | return sThreadLocal.get(); 31 | } 32 | } 33 | ``` 34 | 35 | 用法也是相当的简单,变量最重要的无非就是 Setter 和 Getter . 这样我们就能在任意线程准备好自己的 Looper,也能获取到准备好的 Looper (线程内的单例)。 36 | 37 | ## Why 38 | 39 | 接下来我们看下这么简单的接口,背后是怎么实现变量在线程间隔离的。 40 | 41 | #### Setter 42 | 43 | ````java 44 | public class ThreadLocal { 45 | /** 46 | * Sets the current thread's copy of this thread-local variable 47 | * to the specified value. 48 | * 49 | * @param value the value to be stored in the current thread's copy of 50 | * this thread-local. 51 | */ 52 | public void set(T value) { 53 | Thread t = Thread.currentThread(); 54 | // 获取当前线程关联的 ThreadLocalMap 55 | ThreadLocalMap map = getMap(t); 56 | if (map != null) 57 | map.set(this, value); 58 | else 59 | // 取到的 Map 为空就去创建,同时把这个值作为第一个值存储到 map 60 | createMap(t, value); 61 | } 62 | 63 | void createMap(Thread t, T firstValue) { 64 | t.threadLocals = new ThreadLocalMap(this, firstValue); 65 | } 66 | } 67 | ```` 68 | 69 | 接下来我们来看下 ThreadLocalMap 又是什么 70 | 71 | ````JAVA 72 | static class ThreadLocalMap { 73 | // 用来关联 ThreadLocal 和 其对应的 value 74 | static class Entry extends WeakReference> { 75 | /** The value associated with this ThreadLocal. */ 76 | Object value; 77 | 78 | Entry(ThreadLocal k, Object v) { 79 | // 注意这里的 key 就是 ThreadLocal k,实际上 Entry 对它仅持有一个弱引用 80 | super(k); 81 | value = v; 82 | } 83 | } 84 | 85 | private Entry[] table; 86 | 87 | private void set(ThreadLocal key, Object value) { 88 | 89 | Entry[] tab = table; 90 | int len = tab.length; 91 | // 通过 TheadLocal 的 hash 值去确定 Entry 该存放的位置 92 | int i = key.threadLocalHashCode & (len-1); 93 | 94 | for (Entry e = tab[i];//目标位置 95 | e != null; // 循环至目标位置为空 96 | e = tab[i = nextIndex(i, len)]) { // 目标位置不可用继续找下一个位置,到结尾则 index 跳到 0 97 | ThreadLocal k = e.get(); 98 | 99 | if (k == key) { //同一个 key,直接替换值即可 100 | e.value = value; 101 | return; 102 | } 103 | 104 | if (k == null) { 105 | // key 已经过时被回收了,替换这个 Entry,并且该方法也会去检查和清除其它过期的 Entry 106 | replaceStaleEntry(key, value, i); 107 | return; 108 | } 109 | } 110 | 111 | tab[i] = new Entry(key, value); 112 | int sz = ++size; 113 | if (!cleanSomeSlots(i, sz) && sz >= threshold) 114 | rehash(); // 整理 + 扩容 115 | } 116 | } 117 | ```` 118 | 119 | 我们可以看到,这个 ThreadLocalMap 比 HashMap 的实现简单得多,只是简单的通过一个数组来保存 keyValue 的组合 entry,并且在 hash 值计算得出来的 Index 已经有数据时也仅需要简单的找下一个位置即可。另外这个 Hash 值的计算也是非常的简单: 120 | 121 | ```JAVA 122 | public class ThreadLocal { 123 | // 每次 new 一个 ThreadLocal ,它的 hash 值都会递增 124 | private final int threadLocalHashCode = nextHashCode(); 125 | private static AtomicInteger nextHashCode = new AtomicInteger(); 126 | // hash 值累加的常量 127 | private static final int HASH_INCREMENT = 0x61c88647; 128 | /** 129 | * Returns the next hash code. 130 | * 这个 hash 值和它本身没多少关系,仅和它是第几个被初始化的有关系。感觉挺有意思的 131 | */ 132 | private static int nextHashCode() { 133 | return nextHashCode.getAndAdd(HASH_INCREMENT); 134 | } 135 | } 136 | ``` 137 | 138 | 小结一下: 139 | 140 | - 每个 Thread 都有一个 ThreadLocalMap 里面存放了这个 Thread 的所有 ThreadLocal 141 | - 这个 map 会在 ThreadLocal 设置值的时候去懒加载,再将 ThreadLocal 作为 key 要存的值作为 value 一起放入 map 142 | - 通过 ThreadLocal 的 hash 值去确定需要 value 放在 Map 的哪个位置 143 | 144 | #### Getter 145 | 146 | 有了前面的铺垫,我们再来看 Get 方法就轻松了很多 147 | ```JAVA 148 | public class ThreadLocal { 149 | 150 | public T get() { 151 | Thread t = Thread.currentThread(); 152 | ThreadLocalMap map = getMap(t); 153 | if (map != null) { 154 | // 从 map 取 entry,前面看完了 set 方法,这边不用看源码也知道 155 | // 通过 ThreadLocal 的 hash 值计算 index, index 取到的 key 值与 当前 ThreadLocal 一致则为目标值返回;否则取下一个 index,直到取到的 entry 为空则直接返回空。 156 | ThreadLocalMap.Entry e = map.getEntry(this); 157 | if (e != null) { 158 | @SuppressWarnings("unchecked") 159 | T result = (T)e.value; 160 | return result; 161 | } 162 | } 163 | // 还未设置过值的时候默认初始化方法的值 164 | return setInitialValue(); 165 | } 166 | 167 | private T setInitialValue() { 168 | T value = initialValue(); 169 | ... // 省略 map 为空时初始化方法 170 | map.set(this, value); 171 | .... 172 | return value; 173 | } 174 | 175 | // 默认值为空 176 | protected T initialValue() { 177 | return null; 178 | } 179 | } 180 | ``` 181 | ## 内存泄漏? 182 | 我们看到在 ThreadLocalMap.Entry 里,key 是 WeakReference>,弱引用,也就是这个 ThreadLocal 有可能被回收,如果已经被回收了,那就无法在 map 中 getValue,也就无法清除这个 value 的引用会造成 value 的泄露。不过这种场景还是比较少的,我们需要注意: 183 | 1. ThreadLocalMap 的 Setter / Getter 方法中就包含了一些清理已回收的 key 的逻辑 184 | 2. 该 key 被回收时,这个 Thread 的生命周期可能也结束了,map 也一起回收了就无所谓泄露了 185 | 3. 如果 Thread 生命周期较长,那么我们在使用完 ThreadLocal 时要主动调用 ThreadLocal.remove 方法,即可以在这个线程的 Map 移除对应的 Entry,从而避免泄露 186 | 187 | 188 | ## 总结 189 | 1. ThreadLocal 的作用是变量的线程隔离,同一个 ThreadLocal 在不同的线程中调用 get 方法会返回不同的 value 190 | 2. Thread 里有一个 ThreadLocalMap,用来存储本线程用到的 ThreadLocal 和 Value,每个线程都有自己的 ThreadLocalMap 就实现了变量线程隔离 191 | 3. ThreadLocalMap 本质上是用一个 Entry 数组来保存数据的,Entry 的 key 是 ThreadLocal 的弱引用,value 就是 ThreadLocal 对象 set 的值 192 | 4. ThreadLocalMap 里通过 ThreadLocal 的 hash 值来计算 Index, 当 index 冲突里继续找下一个可用的 index 193 | 5. ThreadLocal 有可能引起内存泄漏,确认使用结束可以调用 remove 移除相关引用 194 | 195 | 本文源码基于**Android-28** 196 | 197 | -------------------------------------------------------------------------------- /谈谈你对 binder 的理解?这样回答才过关.md: -------------------------------------------------------------------------------- 1 | 面试官提了一个问题,我们来看看 😎、😨 和 🤔️ 三位同学的表现如何吧 2 | 3 | --- 4 | >😎 自认为无所不知,水平已达应用开发天花板,目前月薪 10k 5 | 6 | **面试官**️:谈谈你对 binder 的理解 7 | 8 | 😎:binder 是用来跨进程通信的,可以分为 client、server、binder 驱动以及 service manager 四部分。 9 | 10 | **面试官**:一次拷贝原理知道吗? 11 | 12 | 😎:不太清楚,其实对应用开发来说,没必要知道的。 13 | 14 | **面试官**:好的,回去等通知吧 15 | 16 | --- 17 | 18 | >😨 业余时间经常打游戏、追剧、熬夜,目前月薪 15k 19 | 20 | **面试官**:谈谈你对 binder 的理解 21 | 22 | 😨:binder 是一种 IPC 方式,相比于 Linux 原有的管道、共享内存、Socket 等,它通过 mmap 实现一次拷贝,比 Socket 、管道传输速度更快,比共享内存更安全可控,是 Android 系统中主要的 IPC 通信方式。 23 | 24 | **面试官**:Intent 传参有大小限制,这跟 binder 有关系吗? 25 | 26 | 😨:嗯... 应该有关系吧 27 | 28 | **面试官**:binder 是如何限制这个大小的? 29 | 30 | 😨:这个不了解,我还没有深入看过相关源码。 31 | 32 | **面试官**:好的,回去等通知吧 33 | 34 | --- 35 | 36 | >🤔️ 坚持每天学习、不断的提升自己,目前月薪 30k 37 | 38 | **面试官**:谈谈你对 binder 的理解 39 | 40 | 🤔️:binder 是 Android 中主要的跨进程通信方式,binder 驱动和 service manager 分别相当于网络协议中的路由器和 DNS,并基于 mmap 实现了 IPC 传输数据时只需一次拷贝。 41 | 42 | binder 包括 BinderProxy、BpBinder 等各种 Binder 实体,以及对 binder 驱动操作的 ProcessState、IPCThreadState 封装,再加上 binder 驱动内部的结构体、命令处理,整体贯穿 Java、Native 层,涉及用户态、内核态,往上可以说到 Service、AIDL 等,往下可以说到 mmap、binder 驱动设备,是相当庞大、繁琐的一个机制。 43 | 44 | 我自己来谈的话,一天时间都不够,还是问我具体的问题吧。 45 | 46 | **面试官**:基于 mmap 又是如何实现一次拷贝的? 47 | 48 | 🤔️:其实很简单,我来画一个示意图吧: 49 | 50 | ![](/img/binder_mmap.jpg) 51 | 52 | Client 与 Server 处于不同进程有着不同的虚拟地址规则,所以无法直接通信。而一个页框可以映射给多个页,那么就可以将一块物理内存分别与 Client 和 Server 的虚拟内存块进行映射。 53 | 54 | 如图, Client 就只需 copy_from_user 进行一次数据拷贝,Server 进程就能读取到数据了。另外映射的虚拟内存块大小将近 1M (1M-8K),所以 IPC 通信传输的数据量也被限制为此值。 55 | 56 | **面试官**:怎么理解页框和页? 57 | 58 | 🤔️:页框是指一块实际的物理内存,页是指程序的一块内存数据单元。内存数据一定是存储在实际的物理内存上,即页必然对应于一个页框,页数据实际是存储在页框上的。 59 | 60 | 页框和页一样大,都是内核对内存的分块单位。一个页框可以映射给多个页,也就是说一块实际的物理存储空间可以映射给多个进程的多个虚拟内存空间,这也是 mmap 机制依赖的基础规则。 61 | 62 | **面试官**:简单说下 binder 的整体架构吧 63 | 64 | 🤔️:再来画一个简单的示意图吧,这是一个比较典型的、两个应用之间的 IPC 通信流程图: 65 | 66 | ![](/img/binder_design.jpg) 67 | 68 | Client 通过 ServiceManager 或 AMS 获取到的远程 binder 实体,一般会用 **Proxy** 做一层封装,比如 ServiceManagerProxy、 AIDL 生成的 Proxy 类。而被封装的远程 binder 实体是一个 **BinderProxy**。 69 | 70 | **BpBinder** 和 BinderProxy 其实是一个东西:远程 binder 实体,只不过一个 Native 层、一个 Java 层,BpBinder 内部持有了一个 binder 句柄值 handle。 71 | 72 | **ProcessState** 是进程单例,负责打开 Binder 驱动设备及 mmap;**IPCThreadState** 为线程单例,负责与 binder 驱动进行具体的命令通信。 73 | 74 | 由 Proxy 发起 transact() 调用,会将数据打包到 Parcel 中,层层向下调用到 BpBinder ,在 BpBinder 中调用 IPCThreadState 的 transact() 方法并传入 handle 句柄值,IPCThreadState 再去执行具体的 binder 命令。 75 | 76 | 由 binder 驱动到 Server 的大概流程就是:Server 通过 IPCThreadState 接收到 Client 的请求后,层层向上,最后回调到 **Stub** 的 onTransact() 方法。 77 | 78 | 当然这不代表所有的 IPC 流程,比如 Service Manager 作为一个 Server 时,便没有上层的封装,也没有借助 IPCThreadState,而是初始化后通过 binder_loop() 方法直接与 binder 驱动通信的。 79 | 80 | **面试官**:可以,我们再来聊聊别的。 -------------------------------------------------------------------------------- /责任链模式.md: -------------------------------------------------------------------------------- 1 | > 前面讲到面试必问的事件分发,那有的同学就说了,好不容易弄懂了,要是刚好碰上不问的面试官呢?别急,有招。 2 | > 我们知道,事件分发的 dispatchTouchEvent 递归,其实是典型的责任链模式,那么当面试官问到你用过或者熟悉哪些设计模式就可以说说责任链模式。这不就引过来了嘛。 3 | 4 | ## 责任链模式 5 | 6 | Chain of Responsibility Pattern ,顾名思义,就是用来处理相关事务责任的一条执行链,链上的每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。 7 | 8 | ## 使用场景 9 | 10 | * 多个对象可以处理同一请求,具体哪个对象处理是在运行时动态决定的。 11 | 比如:找对象(请求),一个个相亲(相亲对象是处理者),相到两边都觉得合适的就可以完成请求了,当然了,也有可能所有相亲对象都没看上你(人艰不拆)。 12 | * 需要动态指定一组对象处理请求。 13 | 再比如:找到对象了要结婚(请求),需要很多人(处理者)同意,老丈人检查一下人品形象不错同意了,丈母娘检查一下有房有车同意了,~~老天爷检查一下八字匹配同意了~~,民政局检查一下双方未婚同意了,请求成功。其中有一个中断就请求失败了。 14 | 15 | 我真是个比喻鬼才。 16 | 这种链式结构,每个节点都可以拆开再连接,可以很方便的调整处理的节点数、优先级等。 17 | 18 | ## UML 类图 19 | 20 | ![责任链模式](img/责任链模式.jpg) 21 | 22 | * Handler:抽象处理者,声明一个处理请求的方法,并在其中持有下一个处理者的引用 23 | * ConcreteHandler: 具体处理者,对请求进行处理,如果不能处理则将该请求转发给下一个处理者 24 | * 说明下这边的请求,简化成一个 int 表示,在实际场景可以封装请求抽象类,再扩展出不同的请求类型。 25 | 26 | ## shut up and show me the code 27 | 28 | 由于前面的鬼才比喻太过复杂,写成代码完全不现实。我们还是来说请假的情况吧,示例代码使用 kotlin ,没学过 kotlin 的也能看懂。 29 | 30 | 假条就是请求: 31 | 32 | ```kotlin 33 | data class Request( 34 | val leaveDay: Float, //请假时长 35 | val msg: String //请假原因 36 | ) 37 | ``` 38 | 39 | 各领导就是处理者: 40 | 41 | ```kotlin 42 | open class Handler( 43 | val name: String, //处理者名字 44 | val leaveLimit: Float //能审批的最长时间 45 | ){ 46 | var next: Handler? = null 47 | 48 | open fun handRequest(request: Request) { 49 | if (request.leaveDay <= leaveLimit) { 50 | println("${request.msg} -> 请假,$name 准了") 51 | } else { 52 | println("$name:超过${leaveLimit}天,让领导处理") 53 | next?.handRequest(request) 54 | } 55 | } 56 | } 57 | 58 | //可以根据需求扩展 Handler 59 | class FinalHandler(name: String) : Handler(name, Float.MAX_VALUE) { 60 | override fun handRequest(request: Request) { 61 | println("$name:${request.msg} -> 找财务领下工资,不用来了!") 62 | } 63 | } 64 | 65 | ``` 66 | 67 | 然后处理者组成责任链,向第一个节点开始发送请求 68 | 69 | ```kotlin 70 | fun main() { 71 | val leader = Handler("直接上级", 1f) 72 | val manager = Handler("经理", 30f) 73 | val generalManager = Handler("总经理", 365f) 74 | val boss = FinalHandler("老板") 75 | 76 | //组链 77 | leader.next = manager 78 | manager.next = generalManager 79 | generalManager.next = boss 80 | 81 | leader.handRequest(Request(1f, "头疼脑热")) //脑热得隔离观察哈 82 | println("-------------") 83 | leader.handRequest(Request(14f, "老婆生娃")) 84 | println("-------------") 85 | leader.handRequest(Request(365f, "世界辣么大,我想去看看")) 86 | println("-------------") 87 | leader.handRequest(Request(3650f, "不想上班")) 88 | } 89 | ``` 90 | 91 | 大功告成,运行看下结果: 92 | 93 | ``` 94 | 头疼脑热 -> 请假,直接上级 准了 95 | ------------- 96 | 直接上级:超过1.0天,让领导处理 97 | 老婆生娃 -> 请假,经理 准了 98 | ------------- 99 | 直接上级:超过1.0天,让领导处理 100 | 经理:超过30.0天,让领导处理 101 | 世界辣么大,我想去看看 -> 请假,总经理 准了 102 | ------------- 103 | 直接上级:超过1.0天,让领导处理 104 | 经理:超过30.0天,让领导处理 105 | 总经理:超过365.0天,让领导处理 106 | 老板:不想上班 -> 找财务领下工资,不用来了! 107 | ``` 108 | 109 | 110 | 111 | 通过责任链,每一级别都能发挥自己的责任,对请求进行处理,处理不了的就交给下一节点处理。这个链节点个数不限,就算你有再多领导也不用怕啦,反正找直接上级请假,假条会一级级传上去的。 112 | 113 | 进一步,如果不是请假,是报销呢?这时候可以扩展请求类 Request 。链还是那条链却可以处理不同的请求,是不是很灵活呢? 114 | 115 | ## 总结 116 | 117 | 世界是不完美的,责任链模式也一样。 118 | 119 | 优点显而易见:可以对请求和处理者进行解耦,提高代码灵活性。 120 | 121 | 缺点:遍历的处理太多,可能影响性能;调试麻烦需要每一级都跟进去看。 122 | 123 | 124 | 125 | 好了,说完责任链,记得告诉你的面试官事件分发就是一种责任链,把 MotionEvent (请求) 一层层分发,找到能消费它的 View (处理者) 。 126 | 127 | >祝大家周末愉快,欢迎关注 “Android 面试官” 在公众号后台留言交流你最喜欢的设计的模式 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /迭代器模式.md: -------------------------------------------------------------------------------- 1 | > 迭代器模式(Iterator Pattern) 又称游标(Cursor) 模式,是行为型设计模式之一。 2 | 3 | 迭代器模式源于对容器的访问,如 Java 中的 List, Map, 数组等,我们对容器内对象的访问必然涉及遍历算法。如果将遍历算法封装在容器中,那么对于容器类来说就承担了过多的功能;而如果不提供遍历方法让使用者自己去实现,又会让容器的内部细节暴露无遗。此时,迭代器模式应运而生,在客户访问类与容器体之间插入一个第三者——迭代器,很好的解决了上述的弊端。 4 | 5 | ## 定义 6 | 7 | 提供一种方法顺序访问一个容器对象中的各个元素,而又不需要暴露该对象的内部细节。 8 | 9 | ## 使用场景 10 | 11 | - 遍历一个容器对象 12 | - 对外提供一个可遍历的列表 13 | 14 | ## UML 15 | 16 | ![](img/迭代器模式.jpg) 17 | 18 | - Iterator: 迭代器接口,负责定义、访问遍历元素的接口 19 | - ConcreteIterator: 具体的迭代器类 20 | - Aggregate: 容器类接口 21 | - ConcreteAggregate: 具体容器类 22 | 23 | 几乎所有的高级语言的容器类都为我们提供了相应的迭代器,而开发者也几乎不会自己去实现一个迭代器,我们直接来看下迭代器的用法: 24 | 25 | ```java 26 | List lst = new ArrayList(); 27 | lst.add("aaa"); 28 | lst.add("bbb"); 29 | lst.add("ccc"); 30 | lst.add("ddd"); 31 | Iterator iterator = lst.iterator(); 32 | while(iterator.hasNext()) { 33 | System.out.println(iterator.next()); 34 | } 35 | ``` 36 | 37 | 看明白这个 UML 中迭代器的各个角色,接下来直接来看一下源码中的迭代器模式实现。 38 | 39 | ## Android 源码中的实现 40 | 41 | Android 源码中,也有了各种数据结构体,如 List, Map 所包含的迭代器外,还有提供迭代器来遍历访问各种数据,最典型的例子就是数据库查询使用的 Cursor。当我们使用 SQLiteDatabase 的 query 方法查询数据库时,会返回一个 Cursor 游标对象,这个 Cursor 的实现就是一个典型的迭代器。 42 | 43 | ```java 44 | package android.database; 45 | public interface Cursor extends Closeable { 46 | // 将游标移到当前遍历数据的下一行位置 47 | boolean moveToNext(); 48 | 49 | // 以及其它一系列 getXXX 方法,用于取出当前行的数据 50 | int getInt(int columnIndex); 51 | 52 | //... 省略其它方法 53 | } 54 | ``` 55 | 56 | ## ConcurrentModificationException 57 | 58 | 由于迭代器本身非常简单,这边说个我们在使用 Iterator 经常会遇到的异常做为文章内容补充。 59 | 60 | 复现路径: 61 | 62 | ```java 63 | List list = new ArrayList<>(); 64 | list.add("aaa"); 65 | list.add("bbb"); 66 | list.add("ccc"); 67 | list.add("ddd"); 68 | Iterator iterator = list.iterator(); 69 | while (iterator.hasNext()) { 70 | String s = iterator.next(); 71 | if (s.equals("bbb")) { 72 | list.remove(s); 73 | } 74 | } 75 | ``` 76 | 77 | ![image-20201224090119744](img/image-20201224090119744.png) 78 | 79 | 可能很少人会直接写这样的代码,但是这样呢: 80 | 81 | ```java 82 | for (String s : list) { 83 | if (s.equals("bbb")) { 84 | list.remove(s); 85 | } 86 | } 87 | ``` 88 | 89 | 其实这种增强的 for 循环,实际上就是 IDE 在编译出的 class 文件也是使用 Iterator 遍历,因而也会抛异常 90 | 91 | ![image-20201224121659744](img/image-20201224121659744.png) 92 | 93 | #### 原因 94 | 95 | 在 List 的基类中有这样一个属性 96 | 97 | ```JAVA 98 | java.util.AbstractList 99 | public abstract class AbstractList extends AbstractCollection implements List { 100 | // The number of times this list has been structurally modified. 101 | protected transient int modCount = 0; 102 | } 103 | ``` 104 | 105 | 而 List 在生成 Iterator 的时候会把 modCount 存到 `java.util.AbstractList.Itr#expectedModCount` 里面,遍历时如果操作了 List, 即 modCount 与 Iterator 中的 expectedModCount` 对应不上的话,便主动抛出异常了。 106 | 107 | ```JAVA 108 | java.util.AbstractList.Itr 109 | // 这是 AbstractList 的内部类 110 | private class Itr implements Iterator { 111 | int expectedModCount = modCount; 112 | public E next() { 113 | checkForComodification(); 114 | //... 115 | } 116 | final void checkForComodification() { 117 | if (modCount != expectedModCount) 118 | throw new ConcurrentModificationException(); 119 | } 120 | } 121 | ``` 122 | 123 | #### 解决方案 124 | 125 | 1. 单线程出线该问题处理比较简单,删除时调用` java.util.Iterator#remove` 126 | ```java 127 | public void remove() { 128 | // ... 129 | try { 130 | AbstractList.this.remove(lastRet); 131 | if (lastRet < cursor) 132 | cursor--; 133 | lastRet = -1; 134 | expectedModCount = modCount; 135 | } catch (IndexOutOfBoundsException e) { 136 | throw new ConcurrentModificationException(); 137 | } 138 | } 139 | ``` 140 | 2. 多线程遇到该问题时,则需要用线程同步,在遍历时锁住 List, 防止被操作。或直接使用 JDK 提供的 `java.util.concurrent.CopyOnWriteArrayList` 作为容器 141 | 142 | ## 小结 143 | 144 | #### 优点 145 | 146 | - 支持以不同的方式去遍历一个容器对象 147 | - 对外界隐藏容器对象的实现细节 148 | 149 | #### 缺点 150 | 151 | - 类增加了,麻烦。 152 | 153 | #### 另外我们今天还介绍了 ConcurrentModificationException 异常产生的原因和解决方案 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /非主进程的主线程耗时会 ANR 吗.md: -------------------------------------------------------------------------------- 1 | > 思考一个问题,一个 Service 运行在独立的进程里,在这个 Service 的 onCreate 方法里执行耗时操作会造成 ANR 吗? 2 | 3 | ## 直接说结论 4 | 5 | 会,但是不会有 ANR 的弹窗。好了赶时间、或者背答案的选手可以退出去了。 6 | 7 | ## 基础知识 8 | 9 | #### ANR 的四种场景: 10 | 11 | 1. Service TimeOut: service 未在规定时间执行完成: 前台服务 20s,后台 200s 12 | 2. BroadCastQueue TimeOut: 未在规定时间内未处理完广播:前台广播 10s 内, 后台 60s 内 13 | 3. ContentProvider TimeOut: publish 在 10s 内没有完成 14 | 4. Input Dispatching timeout: 5s 内未响应键盘输入、触摸屏幕等事件 15 | 16 | ANR 的根本原因是:应用未在规定的时间内处理 AMS 指定的任务才会 ANR。 17 | 18 | 所以 Service 未在指定时间内执行完成而且非主进程的 Service 仍然需要通过 AMS 进行通信。这也能说明一定会产生 ANR 的。 19 | 20 | ## 实验 21 | 22 | 理论上是会,那我们就写个小 demo 来试一下。 23 | 24 | 写一个 Service: 25 | 26 | ```kotlin 27 | class NoZuoNoDieService: Service() { 28 | override fun onBind(intent: Intent?): IBinder? { 29 | return null 30 | } 31 | 32 | override fun onCreate() { 33 | super.onCreate() 34 | // 小睡会模拟耗时 35 | Thread.sleep(21_000) 36 | } 37 | } 38 | ``` 39 | 40 | 然后在 AndroidManifest 里声明在独立进程 41 | 42 | ```XML 43 | 47 | ``` 48 | 49 | 最后我们把这个 Service 调起来作死 50 | 51 | ```kotlin 52 | startService(Intent(this, NoZuoNoDieService::class.java)) 53 | ``` 54 | 55 | 应用并没有 ANR 弹窗,不过 logcat 有 ANR 相关信息,看一下 trace 文件: 56 | 57 | ```log 58 | ----- pid 14608 at 2021-03-23 19:56:20 ----- 59 | Cmd line: demo.com.sam.demofactory:whatever 60 | ... 省略无关信息 61 | "main" prio=5 tid=1 Sleeping 62 | | group="main" sCount=1 dsCount=0 flags=1 obj=0x73f13a80 self=0x78e7cc2a00 63 | | sysTid=14608 nice=0 cgrp=default sched=0/0 handle=0x796c96d9a8 64 | | state=S schedstat=( 57816925 3067496 80 ) utm=2 stm=3 core=4 HZ=100 65 | | stack=0x7fe1d03000-0x7fe1d05000 stackSize=8MB 66 | | held mutexes= 67 | at java.lang.Thread.sleep(Native method) 68 | - sleeping on <0x0a71f3e8> (a java.lang.Object) 69 | at java.lang.Thread.sleep(Thread.java:373) 70 | - locked <0x0a71f3e8> (a java.lang.Object) 71 | at java.lang.Thread.sleep(Thread.java:314) 72 | at demo.com.sam.demofactory.service.NoZuoNoDieService.onCreate(NoZuoNoDieService.kt:15) 73 | at android.app.ActivityThread.handleCreateService(ActivityThread.java:3426) 74 | at android.app.ActivityThread.-wrap4(ActivityThread.java:-1) 75 | at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1744) 76 | at android.os.Handler.dispatchMessage(Handler.java:106) 77 | at android.os.Looper.loop(Looper.java:171) 78 | at android.app.ActivityThread.main(ActivityThread.java:6611) 79 | at java.lang.reflect.Method.invoke(Native method) 80 | at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) 81 | at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) 82 | ``` 83 | 84 | ## 没有弹窗的 ANR 85 | 86 | 首先来复习一下 [Service 的启动流程](Service 启动流程.md) 87 | 88 | ![](img/Service 创建流程图.jpg) 89 | 90 | AMS 真正去启动 Service 调用的是 ActiveServices.realStartServiceLocked 方法: 91 | 92 | ```JAVA 93 | // API 29 com.android.server.am.ActiveServices 94 | private final void realStartServiceLocked(ServiceRecord r, 95 | ProcessRecord app, boolean execInFg) throws RemoteException { 96 | ... 97 | bumpServiceExecutingLocked(r, execInFg, "create"); 98 | } 99 | 100 | private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) { 101 | ... 102 | scheduleServiceTimeoutLocked(r.app); 103 | } 104 | 105 | // 通过 Handler 延时发一条消息,延时时间则为 Service 触发的 ANR 时长 106 | // SERVICE_TIMEOUT = 20*1000 107 | // SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; 108 | void scheduleServiceTimeoutLocked(ProcessRecord proc) { 109 | if (proc.executingServices.size() == 0 || proc.thread == null) { 110 | return; 111 | } 112 | Message msg = mAm.mHandler.obtainMessage( 113 | ActivityManagerService.SERVICE_TIMEOUT_MSG); 114 | msg.obj = proc; 115 | mAm.mHandler.sendMessageDelayed(msg, 116 | proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT); 117 | } 118 | ``` 119 | 120 | 如果 Service 在规定时间内启动完成,则这个消息会被 remove 掉,我们今天要看一下超时之后,收到这个消息是怎么处理的。 121 | 122 | ```JAVA 123 | // com.android.server.am.ActivityManagerService.MainHandler 124 | final class MainHandler extends Handler { 125 | @Override 126 | public void handleMessage(Message msg) { 127 | switch (msg.what) { 128 | case SERVICE_TIMEOUT_MSG: { 129 | mServices.serviceTimeout((ProcessRecord)msg.obj); 130 | } break; 131 | } 132 | } 133 | ``` 134 | 135 | mServices 是 ActiveServices: 136 | 137 | ```JAVA 138 | // com.android.server.am.ActiveServices#serviceTimeout 139 | void serviceTimeout(ProcessRecord proc) { 140 | ... 141 | if (timeout != null && mAm.mProcessList.mLruProcesses.contains(proc)) { 142 | Slog.w(TAG, "Timeout executing service: " + timeout); 143 | ... 144 | mAm.mHandler.removeCallbacks(mLastAnrDumpClearer); 145 | mAm.mHandler.postDelayed(mLastAnrDumpClearer, LAST_ANR_LIFETIME_DURATION_MSECS); 146 | anrMessage = "executing service " + timeout.shortInstanceName; 147 | } 148 | ... 149 | if (anrMessage != null) { 150 | proc.appNotResponding(null, null, null, null, false, anrMessage); 151 | } 152 | } 153 | ``` 154 | appNotResponding 就是最终处理 ANR 逻辑的代码了 155 | ```JAVA 156 | // com.android.server.am.ProcessRecord 157 | void appNotResponding(String activityShortComponentName, 158 | ApplicationInfo aInfo, 159 | ...) { 160 | ... 161 | if (isMonitorCpuUsage()) { 162 | mService.updateCpuStatsNow(); 163 | } 164 | ... 165 | if (isSilentAnr() && !isDebugging()) { 166 | kill("bg anr", true); 167 | return; 168 | } 169 | 170 | // Bring up the infamous App Not Responding dialog 171 | Message msg = Message.obtain(); 172 | msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG; 173 | msg.obj = new AppNotRespondingDialog.Data(this, aInfo, aboveSystem); 174 | 175 | mService.mUiHandler.sendMessage(msg); 176 | } 177 | ``` 178 | 179 | 如果 isSilentAnr() 为 true,则直接杀死所在进程,不会发送显示 ANR 弹窗的消息。来看下相关方法 180 | 181 | ```JAVA 182 | boolean isSilentAnr() { 183 | return !getShowBackground() && !isInterestingForBackgroundTraces(); 184 | } 185 | private boolean getShowBackground() { 186 | return Settings.Secure.getInt(mService.mContext.getContentResolver(), 187 | Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0; 188 | } 189 | private boolean isInterestingForBackgroundTraces() { 190 | // The system_server is always considered interesting. 191 | if (pid == MY_PID) { 192 | return true; 193 | } 194 | 195 | // A package is considered interesting if any of the following is true : 196 | // 197 | // - It's displaying an activity. 198 | // - It's the SystemUI. 199 | // - It has an overlay or a top UI visible. 200 | // 201 | // NOTE: The check whether a given ProcessRecord belongs to the systemui 202 | // process is a bit of a kludge, but the same pattern seems repeated at 203 | // several places in the system server. 204 | return isInterestingToUserLocked() || 205 | (info != null && "com.android.systemui".equals(info.packageName)) 206 | || (hasTopUi() || hasOverlayUi()); 207 | } 208 | ``` 209 | 210 | 我们得到两个结论: 211 | 212 | - ANR_SHOW_BACKGROUND 可以在**开发者选项**里配置显示所有“应用程序无响应”,开启后即使是该场景也会有 ANR 弹窗 213 | - 未开启上述配置情况下,如果是后台进程,则不显示 ANR 弹窗 214 | 215 | ## 小结 216 | 217 | 会有这个问题,是因为项目在新版本上线时,真的发生了这个问题。有时候一念之间,以为是非主进程就可以胡作非为。最后收获了一个 TOP 1 的 ANR 问题。虽然对用户产生的影响不大,但是对技术的侮辱性极强。 218 | -------------------------------------------------------------------------------- /面向对象的六大原则.md: -------------------------------------------------------------------------------- 1 | > “世上唯一不变的就是永远在变”,产品经理指着刚改的需求对我说。我一个弱小无助的程序猿又能怎样呢,吵个半天产品的需求有了,你的代码呢?所以我们在写代码的时候要为不可预料的需求变动做好准备,好在有大师们提出的非常好的 6 大设计原则以及 23 种设计模式,可以助我们“封装”变化,用风骚灵动的代码来实现变动的需求。 2 | ## 六大原则 3 | 4 | ![六大原则](img/六大设计原则.png) 5 | * Single Responsibility Principle:单一职责原则 6 | * Open Closed Principle:开闭原则 7 | * Liskov Substitution Principle:里氏替换原则 8 | * Law of Demeter:迪米特法则 9 | * Interface Segregation Principle:接口隔离原则 10 | * Dependence Inversion Principle:依赖倒置原则 11 | 12 | 取所有不重复的首字母:SOLID(solid,稳定的) 13 | 14 | ### 单一职责原则 15 | 16 | 类的职责应该单一,一个方法只做一件事。职责划分清晰了,每次改动到最小单位的方法或类。尽量做到**只有一个原因引起变化**。 17 | 18 | 如何划分一个类的或一个函数的职责,需要根据个人经验、具体的业务逻辑而定。但是它也有一些基本指导原则: 19 | 20 | 1. 两个完全不一样的功能不应该放一个类中 21 | 2. 一个类中应该是一组相关性很高的函数、数据的封装 22 | 3. 应该不断审视自己的代码,根据具体业务、功能对类进行拆分,优化代码 23 | 24 | ### 里氏替换原则 25 | 26 | **所有引用基类的地方,必须能够使用其子类直接替换。** 这个原则与面向对象的继承特性密切相关 27 | 28 | 1. 子类必须实现父类的所有方法 (继承的特性,子类拥有父类的所有方法) 29 | 2. 子类可以有自己的个性 (重写) 30 | 3. 覆盖或实现父类的方法时,入参可以放大(如:父类的参数 HashMap , 子类参数可以为 Map); 输出可以被缩小(如父类 return Map, 子类 return HashMap 31 | 32 | ### 依赖倒置 33 | 34 | 1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象. 35 | 不可分割的原子逻辑就是低层模式,原子逻辑组装成的就是高层模块。 36 | 37 | 2. 抽象不应该依赖细节 38 | java 中,抽象 -> 接口或抽象类;细节 -> 实现类 39 | 40 | 3. 细节应该依赖抽象 41 | 42 | 精简一下就是 **面向接口编程** ,是面向对象设计的精髓之一,可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险, 在 java 中的表现: 43 | 44 | - 模块间依赖通过抽象(接口)发生,实现类之间不直接依赖 45 | - 接口或抽象类不依赖于实现类 46 | - 实现类依赖于接口或抽象类 47 | 48 | ### 接口隔离原则 49 | 50 | 客户端不应该依赖它不需要的接口。(此处接口分为实例接口即类,类接口 interface,所以此处的接口在 java 中是类和接口的意思) 51 | 建立单一的接口,不要建立臃肿庞大的接口,与 SRP 的区别:SRP 针对职责,从业务逻辑划分;ISP 是要求接口的方法尽量少。 52 | 53 | 1. 接口尽量小 54 | 2. 接口要高内聚 55 | 高内聚:提高接口、类、模块的处理能力,减少对外的交互,要求在接口中尽少公布 public 方法,减少对外承诺也有利于降低成本 56 | 3. 定制服务,针对不同的用户提供优良的服务,只提供访问者需要的方法。如不同权限的用户给于不同的操作接口。 57 | 4. 接口设计有限度的 58 | 接口粒度越小,越灵活,但是结构却越复杂,所以要有个度 59 | 60 | ### 迪米特法则 61 | 62 | 也称为最少知识原则(Least Knowledge Principle,LKP): 63 | **一个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求** 64 | 65 | 1. 只和朋友交流。 66 | 例:老师叫班长清点学生,老师应该只和班长有耦合,而不应该和学生有耦合。 67 | 2. 朋友之间也是有距离的。 68 | 耦合的类不要把太多方法暴露给其它类,否则改动要修改的地方太多。(高内聚) 69 | 3. 自己的就是自己的 70 | 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。 71 | 72 | ### 开闭原则 73 | 74 | **对扩展开放,对修改封闭** 75 | 76 | 例:商品打折,可以更改原来的商品类的 getPrice() 方法,也可以增加一个子类,重写getPrice方法,通过高层模块,在打折时使用子类。显然方法2拓展更灵活。 77 | 78 | 好处:原来的代码不用更改,而且还可以复用原来的代码 79 | 80 | 如何应用: 81 | 1. 抽象约束:通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放 82 | * 拓展类不应该出现接口中不存在的 public 方法 83 | * 参数、引用对象类型尽量使用接口或抽象类 84 | * 抽象层尽量保持稳定 85 | 2. 元数据(metadata)控制模块行为 86 | 元数据:用来描述环境和数据的数据。如:可以通过配置文件来完成业务更改。 87 | 3. 制定项目章程(制定和遵守规则) 88 | 4. 封装变化 89 | 90 | 将变化抽象封装到接口,这样新的变化就可以生成新的接口实例,传入到系统。23 种设计模式即是从各个不同角度对变化进行封装。 91 | 92 | ## 设计模式 93 | 94 | 模式是一个可重用的方案,可应用于软件设计中常见问题。 95 | 96 | ### 设计模式的好处 97 | 98 | 1. 模式是行之有效的解决方法:久经考验的 99 | 2. 模式可以很容易地重用 100 | 3. 模式善于表达 101 | 102 | ### 23 种设计模式 103 | 104 | ![](img/23种设计模式.png) 105 | 106 | 我们接下来也会在常规 Android 技术文章更新的中,穿插着对这 23 种设计模式进行详细介绍,敬请期待。等不及想学习的小伙伴,推荐书籍《设计模式之禅》、《Android 源码设计模式解析与实战》 107 | 108 | 109 | 110 | 111 | 112 | --------------------------------------------------------------------------------