注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

还不会搭安卓架构?有这一篇就够了

前言用本篇文章理论知识和架构原则实践了一个 wanAndroid 项目,其中全部采用 kotlin 编写并抛弃了 Rxjava,因为 kotlin 可以完全替代他,github 本项目中汇总了业界知名的架构文章和一些项目帮你彻底理解架构。后续本项目将...
继续阅读 »

彻底理解Android架构

前言

用本篇文章理论知识和架构原则实践了一个 wanAndroid 项目,其中全部采用 kotlin 编写并抛弃了 Rxjava,因为 kotlin 可以完全替代他,github 本项目中汇总了业界知名的架构文章和一些项目帮你彻底理解架构。后续本项目将持续更新,并完善 wanAndorid 的所有功能。还会用 23 种设计模式在项目中实践,彻底理解设计模式在业务场景中的使用,欢迎持续关注 github

一、什么是架构

1.1 架构介绍

架构究竟是什么?如何更好的理解架构。我们知道中国文字博大精深可以说从文字的组成就能理解其含义。架构也不例外 “架构” 是由 “架” “构” 组成。

架:建造、搭设、支撑。 简称:整体结构
构:屋宇、供人居住的木、砖瓦构筑物。 简称:组件

整体结构和组件的组合就形成了架构。以 Android 架构为例子一个 APP 通常是有 class(类)组成,而这些 class 之间如何如何组合、相互之间如何发生作用,则是影响这个 APP 本身的关键点。细分的话可以分为类、接口(连接器)、任务流。所谓类就是组成架构的核心 “砖瓦”,而接口则是这些类之间通讯的路径、通讯的机制、通讯的期望结果。任务流则是描述系统如何使用类和接口完成某一项需求比如:一次网络请求。 上面介绍架构中提到了房屋、木头、砖瓦可见架构和建筑有着彼此的联系。

1.2 建筑学

上世纪 60 年代已经设计软件架构这个概念了,到了 90 年代软件架构这个概念才开始流行起来。而计算机的历史开始于上世纪五十年代相比建筑历史就非常短暂了,建筑工程从石器时代就开始了。人类在几千年的建筑设计实践中积累了大量的经验和教训,建筑设计基本上包含两点,一是建筑风格,二是建筑模式。独特的建筑风格和恰当选择的建筑模式,可以使它成为一个独一无二的建筑。

下图的照片显示了古代玛雅建筑:Chichen-Itza,九个巨大的石级堆垒而上,九十一级台阶(象征着四季的天数)夺路而出,塔顶的神殿耸入云天。所有的数字都如日历般严谨,风格雄浑。难以想象这是石器时代的建筑物。

1620390212_副本.png

英国首相丘吉尔说,我们构造建筑物,建筑也构造我们,英国下议院的会议厅较狭窄,无法使所有的下议院议员面向同一个方向入座,而必须分成两侧入座。丘吉尔认为,议员们入座的时候自然会选择与自己政见相同的人同时入座,而这就是英国政党制的起源。

二、架构设计目的

几乎所有的软件设计理念都可以在浩瀚的建筑学历史中找到。许多人认为 “形式必须服从功能”(你认同这种观点吗?欢迎在评论区留下你的看法)。而好的设计既有形式又有功能。比如我们的北京大兴国际机场大兴机场以航站楼为核心向四周延展从空中俯瞰就像是一只展翅欲飞的凤凰,以航站楼核心区为中心,分别向东北、东南、中南、西南、西北五个方向伸出了五条指廊,通往北京大兴国际机场的飞行区。这种从中心向四面八方延伸的设计,使航站楼中心点到最远端登机口的距离只有 600 米左右,旅客步行前往最多只需 8 分钟。

建筑的设计又有一定的目的性,而软件架构设计也同理。软件架构目的性大致可分为可扩展性、可定制化、可伸缩、可维护性:

1. 可扩展性: APP 必须能够在用户的 UV/PV 数量快速增加的情况下,保持软件合理的性能。只有这样在快速的从 0 到 1 的需求迭代中才能后顾无忧。

2. 可定制化: 在同一个软件系统中可能面向的用户群体是不同的、多样的,需要满足根据用户群的不同和市场需求的不同进行定制化。比如一个 APP 中某些功能只针对特定用户开放。

3. 可伸缩性: 在新技术出现的时候,一个软件系统应当允许接入新技术,从而对现有系统进行功能和性能的扩展。

4. 可维护性: 软件系统的维护包括两方面,一是修复现有的 bug,二是将新的迭代需求开发到现有系统中去。一个易于维护的系统可以有效地降低人力和物力。

三、实践一个 APP:玩 Android

1620390212_副本_副本.png

针对上面对架构的介绍,相信已经从陌生走向熟悉了。但是最重要的还是实践,伟大的毛主席曾经说过 你要想知道梨子的滋味,就要亲口尝一下。因此借用了 wanAndoird 开放 API 简单实现一个 APP 并概括上述架构的关键点,主要的功能点如下:

  • 首页是热搜文章的分类列表

  • 项目页面主要包括完整项目

  • 文章、项目点击可以查看详情

不知道还有没有印象上文提到了架构 “形式必须服从功能” 当然这不是权威的定义,可以作为参考。我们先不管是形式服从功能还是功能服从形式,可以结构化思维理解下这句话,架构大致可分为:形式、功能所以我们依次按照此两点进行搭建 wanAndroid 项目。

3.1 架构 - 形式

从形式本身而言包括两部分。一是事物外在的形状,二是内在的结构、组合方式。实际上,这两者为同一。内容如何内在组合,对外就自然有某种表现的形状。

我们打开项目的第一眼接触到和看到的就是我们项目的目录结构,更清晰更简洁的目录结构可以使我们更快的上手项目。这里主要分为两部分核心模块、业务功能模块:

核心模块主要有以下职责:

  • Dagger 依赖注入处理。
  • 扩展功能:各种 utils。
  • 基础层的抽象:BaseActivity、BaseViewModel 等
  • 第三库处理、网络异常处理等

业务功能模块主要有以下好处:

  • 高内聚性
  • 清晰的功能结构
  • 模块化
  • 功能隔离并封装

在主 APP 下进行了 core、features 的划分,业务模块并没有按照模块化的形式进行多 moudle 拆分而是聚合在 features 下,以包的形式进行了聚合,这样做的好处如下:

  • 更快的编译速度
  • 减少 maven 库的依赖冲突
  • 通用功能的重用性
  • 包的内聚力

可以看到我们并没有采用按照业务 module 进行模块化划分,因为我之前接触过一个项目拆分了 40 多个 module 可想而知项目一旦庞大起来坏处也就是暴露出来:

  • 编译一次项目高达 7/8 分钟,编译速度优化可以看我之前的文章(编译速度优化)
  • 项目中的 moudle 依赖纵横交错

当然我并不反对多 module 模块化的存在,因为任何模式都有利有弊,这取决于当前的项目的业务来抉择使用那种形式。此外项目中全部采用 kotlin 编写:

  • build.gradle.kts .kts 也是官方推崇的可以使 gradle 更加简化
  • buildSrc来处理 gradle 依赖

3.2 架构 - 功能

在玩 Android 中的业务点功能点主要有文章、项目获取,而这些功能点大部分都离不开网络请求和回调处理。这里不再描述 MVC、MVP、MVVM 的区别和如何选择,但是我可以说明一点是任何架构模式都没有最好、最优,只有最适合当前业务的才是好架构。现在 google 官方推崇的架构主要是 MVVM 所有我们主要说下 MVVM。更详细的可以查看官网文档 应用架构指南

MVVM 架构模式满足上文我们描述符合的架构设计的目的,同时也准守了官方给定的架构原则,架构原则大致有两点如下。可能光看这两个定义可能不太容易理解。所有我们用结构化思维的方式理解下,关注点分离就是将复杂问题做合理的分解,再研究分解的侧面,最后合成整体的解决方案。因此我们在 Activity 或 Fragment 不应该做业务逻辑而是把功能点拆分成需要最小的最优解,最后合并成整体方案。比如 mvvm 我们衍生出 ViewModel、LiveData、Model 等。

  1. 关注点分离 Activity 或 Fragment 中的代码应是处理界面和操作系统交互的逻辑应使这些类尽可能保持精简,这样可以避免许多与生命周期相关的问题。
  2. 通过模型驱动界面 模型是负责处理应用数据的组件。它们独立于应用中的 View 对象和应用组件,因此不受应用的生命周期以及相关的关注点的影响

MVVM 中每个组件仅依赖于其下一级的组件如:activity-->viewMoudle-->Repository。这时候你可能有疑惑,如果是单向依赖那网络请求的回调怎么处理?这里引出一个概念 “响应式编程” 结合 liveData 做处理其内部是观察者模式,并且关联视图的声明周期如:Activity、Fragment 或 Service。使用 LiveData 的好处如下:

  1. 不会发生内存泄漏 观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。
  2. 不会因 Activity 停止而导致崩溃 如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。
  3. 不再需要手动处理生命周期 界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。

3.3 UseCase

UseCase 是 Clean 架构中的一个概念,其中主要用于 UI 和数据层的连接同时也会进行 IO 的切换,这里可以看到本项目抛弃了 Rxjava 因为他完全可以用 Kotlin 来替代。

abstract class UseCase<out Type, in Params> where Type : Any {

abstract suspend fun run(params: Params): Either<Failure, Type>{

operator fun invoke(params: Params, onResult: (Either<Failure, Type>) -> Unit = {}) {
val job = GlobalScope.async(Dispatchers.IO) { run(params) }
GlobalScope.launch(Dispatchers.Main) { onResult(job.await()) }
}

class None
}
复制代码
复制代码

3.4 一个完整网络请求流程


  • View:一个网络请求的发送并订阅,处理 UI 数据。
  • ViewModel:为 View(Activity/Fragment) 提供数据,并处理业务逻辑。
  • LiveData:具有生命周期可观察的数据存储器类,LiveData 存储在 ViewModel 中
  • UseCases:用于连接 ViewModel 和 Model,并更新 LiveData。
  • Model:可以从网络、数据库或其他 API 获取数据

四、总结

我们可以体会到从架构理论定义到实践的过程相信你有了自己的理解和见解,但这只是一种实现方式,如果在满足架构设计目的和架构原则的情况下你有更好的实践方式或者有任何和架构项目的疑问点都可迎在评论区或者 Github 中留言讨论。这里我也有个疑问点就你认同形式必需服从功能?欢迎留下你的见解。

后续本项目将持续更新,并完善 wanAndorid 的所有功能。还会用 23 种设计模式在项目中实践,彻底理解设计模式在业务场景中的使用,欢迎持续关注。当其他的平台如后端、前端架构的搭建都是殊途同归的。但是我还是有几点建议:

  • 业务决定架构
  • 不要过度设计
  • 面向接口编程
  • 形式需服从功能

收起阅读 »

前端面试常问的基础(七)

1.IE6或更低版本最多20个cookie2.IE7和之后的版本最后可以有50个cookie。3.Firefox最多50个cookie4.chrome和Safari没有做硬性限制IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理coo...
继续阅读 »

1.IE6或更低版本最多20个cookie

2.IE7和之后的版本最后可以有50个cookie。

3.Firefox最多50个cookie

4.chrome和Safari没有做硬性限制

IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie。


优点:极高的扩展性和可用性


1.通过良好的编程,控制保存在cookie中的session对象的大小。

2.通过加密和安全传输技术(SSL),减少cookie被破解的可能性。

3.只在cookie中存放不敏感数据,即使被盗也不会有重大损失。

4.控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。


缺点:

1.`Cookie`数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。


2.安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。


3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。


在较高版本的浏览器中,js提供了sessionStorage和globalStorage。在HTML5中提供了localStorage来取代globalStorage。


html5中的Web Storage包括了两种存储方式:sessionStorage和localStorage。


sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。


而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。




收起阅读 »

前端面试常问的基础(六)

一、HTML5 CSS3CSS3有哪些新特性?1. CSS3实现圆角(border-radius),阴影(box-shadow),2. 对文字加特效(text-shadow、),线性渐变(gradient),旋转(transform)3.transform:r...
继续阅读 »
一、HTML5 CSS3
  1. CSS3有哪些新特性?
1. CSS3实现圆角(border-radius),阴影(box-shadow),
2. 对文字加特效(text-shadow、),线性渐变(gradient),旋转(transform)
3.transform:rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg);// 旋转,缩放,定位,倾斜
4. 增加了更多的CSS选择器  多背景 rgba
5. 在CSS3中唯一引入的伪元素是 ::selection.
6. 媒体查询,多栏布局
7. border-image
  1. html5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5
新特性:
1. 拖拽释放(Drag and drop) API
2. 语义化更好的内容标签(header,nav,footer,aside,article,section)
3. 音频、视频API(audio,video)
4. 画布(Canvas) API
5. 地理(Geolocation) API
6. 本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
7. sessionStorage 的数据在浏览器关闭后自动删除
8. 表单控件,calendar、date、time、email、url、search  
9. 新的技术webworker, websocket, Geolocation
移除的元素:
1. 纯表现的元素:basefont,big,center,font, s,strike,tt,u;
2. 对可用性产生负面影响的元素:frame,frameset,noframes;
支持HTML5新标签:
1. IE8/IE7/IE6支持通过 document.createElement 方法产生的标签,可以利用这一特性让这些浏览器支持 HTML5 新标签,浏览器支持新标签后,还需要添加标签默认的样式(当然最好的方式是直接使用成熟的框架、使用最多的是html5shim框架):
<!--[if lt IE 9]>
<script> src="http://html5shim.googlecode.com/svn/trunk/html5.js"</script>
<![endif]-->
如何区分:
DOCTYPE声明新增的结构元素、功能元素
  1. 本地存储(Local Storage )和cookies(储存在用户本地终端上的数据)之间的区别是什么?
Cookies:服务器和客户端都可以访问;大小只有4KB左右;有有效期,过期后将会删除;
本地存储:只有本地浏览器端可访问数据,服务器不能访问本地存储直到故意通过POST或者GET的通道发送到服务器;每个域5MB;没有过期数据,它将保留知道用户从浏览器清除或者使用Javascript代码移除
  1. 如何实现浏览器内多个标签页之间的通信?
调用 localstorge、cookies 等本地存储方式
  1. 你如何对网站的文件和资源进行优化?
文件合并
文件最小化/文件压缩
使用CDN托管
缓存的使用
  1. 什么是响应式设计?
它是关于网页制作的过程中让不同的设备有不同的尺寸和不同的功能。响应式设计是让所有的人能在这些设备上让网站运行正常
  1. 新的 HTML5 文档类型和字符集是?
答:HTML5文档类型:<!doctype html>
    HTML5使用的编码<meta charset=”UTF-8”>
  1. HTML5 Canvas 元素有什么用?
答:Canvas 元素用于在网页上绘制图形,该元素标签强大之处在于可以直接在 HTML 上进行图形操作。
  1. HTML5 存储类型有什么区别?
答:Media APIText Track APIApplication Cache APIUser InteractionData Transfer APICommand APIConstraint Validation APIHistory API
  1. H5+CSS3解决下导航栏最后一项掉下来的问题
  2. CSS3新增伪类有那些?
    p:first-of-type 选择属于其父元素的首个 <p> 元素的每个 <p> 元素。
    p:last-of-type  选择属于其父元素的最后 <p> 元素的每个 <p> 元素。
    p:only-of-type  选择属于其父元素唯一的 <p> 元素的每个 <p> 元素。
    p:only-child    选择属于其父元素的唯一子元素的每个 <p> 元素。
    p:nth-child(2)  选择属于其父元素的第二个子元素的每个 <p> 元素。
    :enabled、:disabled 控制表单控件的禁用状态。
:checked,单选框或复选框被选中。               
  1. 请用CSS实现:一个矩形内容,有投影,有圆角,hover状态慢慢变透明。
css属性的熟练程度和实践经验
  1. 描述下CSS3里实现元素动画的方法
动画相关属性的熟悉程度
  1. html5\CSS3有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5
HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,地理定位等功能的增加。
* 绘画 canvas 元素
  用于媒介回放的 video 和 audio 元素
  本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
  sessionStorage 的数据在浏览器关闭后自动删除
  语意化更好的内容元素,比如 article、footer、header、nav、section
  表单控件,calendar、date、time、email、url、search
  CSS3实现圆角,阴影,对文字加特效,增加了更多的CSS选择器  多背景 rgba
  新的技术webworker, websockt, Geolocation
移除的元素
纯表现的元素:basefont,big,center,font, s,strike,tt,u;
对可用性产生负面影响的元素:frame,frameset,noframes;
* 是IE8/IE7/IE6支持通过document.createElement方法产生的标签,
  可以利用这一特性让这些浏览器支持HTML5新标签,
  浏览器支持新标签后,还需要添加标签默认的样式:
* 当然最好的方式是直接使用成熟的框架、使用最多的是html5shim框架
<!--[if lt IE 9]>
<script> src="http://html5shim.googlecode.com/svn/trunk/html5.js"</script>
<![endif]-->
  1. 你怎么来实现页面设计图,你认为前端应该如何高质量完成工作一个满屏 品 字布局 如何设计?
* 首先划分成头部、body、脚部;。。。。。
* 实现效果图是最基本的工作,精确到2px;
  与设计师,产品经理的沟通和项目的参与
  做好的页面结构,页面重构和用户体验
  处理hack,兼容、写出优美的代码格式
  针对服务器的优化、拥抱 HTML5。
  1. 你能描述一下渐进增强和优雅降级之间的不同吗?
渐进增强 progressive enhancement:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
优雅降级 graceful degradation:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
  区别:优雅降级是从复杂的现状开始,并试图减少用户体验的供给,而渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要。降级(功能衰减)意味着往回看;而渐进增强则意味着朝前看,同时保证其根基处于安全地带。 
  “优雅降级”观点
  “优雅降级”观点认为应该针对那些最高级、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。
  在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。你可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。
  “渐进增强”观点
  “渐进增强”观点则认为应关注于内容本身。
  内容是我们建立网站的诱因。有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得“渐进增强”成为一种更为合理的设计范例。这也是它立即被 Yahoo! 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。
 
  那么问题了。现在产品经理看到IE6,7,8网页效果相对高版本现代浏览器少了很多圆角,阴影(CSS3),要求兼容(使用图片背景,放弃CSS3),你会如何说服他?
  1. 为什么利用多个域名来存储网站资源会更有效?
CDN缓存更方便 
突破浏览器并发限制 
节约cookie带宽 
节约主域名的连接数,优化页面响应速度 
防止不必要的安全问题
  1. 请谈一下你对网页标准和标准制定机构重要性的理解。
  (无标准答案)网页标准和标准制定机构都是为了能让web发展的更‘健康’,开发者遵循统一的标准,降低开发难度,开发成本,SEO也会更好做,也不会因为滥用代码导致各种BUG、安全问题,最终提高网站易用性。
 
  1. 请描述一下cookies,sessionStorage和localStorage的区别?  
  sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
web storagecookie的区别
Web Storage的概念和cookie相似,区别是它是为了更大容量存储设计的。Cookie的大小是受限的,并且每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽,另外cookie还需要指定作用域,不可以跨域调用。
除此之外,Web Storage拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookiegetCookie。但是Cookie也是不可以或缺的:Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地存储数据而生。
  1. 知道css有个content属性吗?有什么作用?有什么应用?
知道。css的content属性专门应用在 before/after 伪元素上,用来插入生成内容。最常见的应用是利用伪类清除浮动。
//一种常见利用伪类清除浮动的代码
.clearfix:after {
    content:"."; //这里利用到了content属性
    display:block;
    height:0;
    visibility:hidden;
    clear:both; }
.clearfix {
    *zoom:1;
}
after伪元素通过 content 在元素的后面生成了内容为一个点的块级素,再利用clear:both清除浮动。
  那么问题继续还有,知道css计数器(序列数字字符自动递增)吗?如何通过css content属性实现css计数器?
答案:css计数器是通过设置counter-reset 、counter-increment 两个属性 、及 counter()/counters()一个方法配合after / before 伪类实现。 
  1. 如何在 HTML5 页面中嵌入音频?
HTML 5 包含嵌入音频文件的标准方式,支持的格式包括 MP3、Wav 和 Ogg:
<audio controls> 
  <source src="jamshed.mp3" type="audio/mpeg"> 
   Your browser does'nt support audio embedding feature. 
</audio>
  1. 如何在 HTML5 页面中嵌入视频?
和音频一样,HTML5 定义了嵌入视频的标准方法,支持的格式包括:MP4、WebM 和 Ogg:
<video width="450" height="340" controls> 
  <source src="jamshed.mp4" type="video/mp4"> 
   Your browser does'nt support video embedding feature. 
</video> 
  1. HTML5 引入什么新的表单属性?
Datalist   datetime   output   keygen  date  month  week  time  number   range   emailurl
  1. CSS3新增伪类有那些?
 p:first-of-type 选择属于其父元素的首个 <p> 元素的每个 <p> 元素。
    p:last-of-type  选择属于其父元素的最后 <p> 元素的每个 <p> 元素。
    p:only-of-type  选择属于其父元素唯一的 <p> 元素的每个 <p> 元素。
    p:only-child    选择属于其父元素的唯一子元素的每个 <p> 元素。
    p:nth-child(2)  选择属于其父元素的第二个子元素的每个 <p> 元素。
    :enabled、:disabled 控制表单控件的禁用状态。
:checked,单选框或复选框被选中。
  1. ()描述一段语义的html代码吧。
HTML5中新增加的很多标签(如:<article>、<nav>、<header>和<footer>等)
就是基于语义化设计原则)  
< div id="header">
< h1>标题< /h1>
< h2>专注Web前端技术< /h2>
< /div>
语义 HTML 具有以下特性:
 
文字包裹在元素中,用以反映内容。例如:
段落包含在 <p> 元素中。
顺序表包含在<ol>元素中。
从其他来源引用的大型文字块包含在<blockquote>元素中。
HTML 元素不能用作语义用途以外的其他目的。例如:
<h1>包含标题,但并非用于放大文本。
<blockquote>包含大段引述,但并非用于文本缩进。
空白段落元素 ( <p></p> ) 并非用于跳行。
文本并不直接包含任何样式信息。例如:
不使用 <font> 或 <center> 等格式标记。
类或 ID 中不引用颜色或位置。
  1. cookie在浏览器和服务器间来回传递。 sessionStoragelocalStorage区别
sessionStorage和localStorage的存储空间更大;
sessionStorage和localStorage有更多丰富易用的接口;
sessionStorage和localStorage各自独立的存储空间;
  1. html5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5
* HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加。
* 绘画 canvas  
  用于媒介回放的 video 和 audio 元素
  本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
  sessionStorage 的数据在浏览器关闭后自动删除
  语意化更好的内容元素,比如 article、footer、header、nav、section
  表单控件,calendar、date、time、email、url、search  
  新的技术webworker, websockt, Geolocation
* 移除的元素
纯表现的元素:basefont,big,center,font, s,strike,tt,u;
对可用性产生负面影响的元素:frame,frameset,noframes;
支持HTML5新标签:
* IE8/IE7/IE6支持通过document.createElement方法产生的标签,
  可以利用这一特性让这些浏览器支持HTML5新标签,
  浏览器支持新标签后,还需要添加标签默认的样式:
* 当然最好的方式是直接使用成熟的框架、使用最多的是html5shim框架
<!--[if lt IE 9]>
<script> src="http://html5shim.googlecode.com/svn/trunk/html5.js"</script>
<![endif]-->
  1. 如何区分: DOCTYPE声明\新增的结构元素\功能元素
  2. 语义化的理解?
用正确的标签做正确的事情!
html语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;
在没有样式CCS情况下也以一种文档格式显示,并且是容易阅读的。
搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO。
使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解。
  1. HTML5的离线储存?
localStorage    长期存储数据,浏览器关闭后数据不丢失;
sessionStorage  数据在浏览器关闭后自动删除。
  1. 写出HTML5的文档声明方式
 
 <DOCYPE html>
  1. HTML5CSS3的新标签     
 
 
HTML5: nav, footer, header, section, hgroup, video, time, canvas, audio...
CSS3: RGBA, opacity, text-shadow, box-shadow, border-radius, border-image, 
border-color, transform...;
  1. 自己对标签语义化的理解
    在我看来,语义化就是比如说一个段落, 那么我们就应该用 <p>标签来修饰,标题就应该用 <h?>标签等。符合文档语义的标签。
 

收起阅读 »

前端面试常问的基础(五)

如何理解CSS的盒子模型?每个HTML元素都是长方形盒子。 (1)盒子模型有两种:IE盒子模型、标准W3C盒子模型;IE的content部分包含了border和pading。 (2)标准W3C盒模型包含:内容(content)、填充(padding)、边界(m...
继续阅读 »

如何理解CSS的盒子模型?

每个HTML元素都是长方形盒子。 (1)盒子模型有两种:IE盒子模型、标准W3C盒子模型;IE的content部分包含了border和pading。 (2)标准W3C盒模型包含:内容(content)、填充(padding)、边界(margin)、边框(border)。



link属于XHTML标签,而@import是CSS提供的。 (2)页面被加载时,link会同时被加载,而@import引用的CSS会等到页面被加载完再加载。 (3)import只在IE 5以上才能识别,而link是XHTML标签,无兼容问题。 (4)link方式的样式权重高于@import的权重。 (5)使用dom控制样式时的差别。当使用javascript控制dom去改变样式的时候,只能使用link标签,因为@import不是dom可以控制的。



id选择器(# myid) 类选择器(.myclassname) 标签选择器(div、h1、p) 相邻选择器(h1 + p) 子选择器(ul < li) 后代选择器(li a) 通配符选择器( * ) 属性选择器(a[rel = "external"]) 伪类选择器(a: hover, li: nth - child) 可继承: font-size font-family color, UL LI DL DD DT;

不可继承 :border padding margin width height ;

优先级就近原则,样式定义最近者为准,载入样式以最后载入的定位为准。 优先级为: !important > id > class > tag important 比 内联优先级高 CSS3新增伪类举例: p:first-of-type 选择属于其父元素的首个<p>元素的每个<p>元素。 p:last-of-type 选择属于其父元素的最后<p>元素的每个<p>元素。 p:only-of-type 选择属于其父元素唯一的<p>元素的每个<p>元素。 p:only-child 选择属于其父元素的唯一子元素的每个<p>元素。 p:nth-child(2) 选择属于其父元素的第二个子元素的每个<p>元素。 :enabled、:disabled 控制表单控件的禁用状态。 :checked 单选框或复选框被选中。




(1)png24为的图片在IE6浏览器上出现背景,解决方案是做成PNG8。

(2)浏览器默认的margin和padding不同,解决方案是加一个全局的*{margin:0;padding:0;}来统一。

(3)IE6双边距bug:块属性标签float后,又有横行的margin情况下,在IE 6显示margin比设置的大。

(4)浮动ie产生的双边距问题:块级元素就加display:inline;行内元素转块级元素display:inline后面再加display:table。 .bb{

background-color:#f1ee18; /*所有识别*/

.background-color:#00deff\9; /*IE6、7、8识别*/

+background-color:#a200ff; /*IE6、7识别*/

_background-color:#1e0bd1; /*IE6识别*/ }


BFC,块级格式化上下文,一个创建了新的BFC的盒子是独立布局的,盒子里面的子元素的样式不会影响到外面的元素。在同一个 BFC 中的两个毗邻的块级盒在垂直方向(和布局方向有关系)的 margin 会发生折叠。


W3C CSS 2.1 规范中的一个概念,它决定了元素如何对其内容进行布局,以及与其他元素的关系和相互作用。

display:none  隐藏对应的元素,在文档布局中不再给它分配空间,它各边的元素会合拢,

就当他从来不存在。


visibility:hidden  隐藏对应的元素,但是在文档布局中仍保留原来的空间。

Web Storage的概念和cookie相似,区别是它是为了更大容量存储设计的。Cookie的大小是受限的,并且每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽,另外cookie还需要指定作用域,不可以跨域调用。


除此之外,Web Storage拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookie,getCookie。


但是Cookie也是不可以或缺的:Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生


浏览器的支持除了IE7及以下不支持外,其他标准浏览器都完全支持(ie及FF需在web服务器里运行),值得一提的是IE总是办好事,例如IE7、IE6中的UserData其实就是javascript本地存储的解决方案。通过简单的代码封装可以统一到所有的浏览器都支持web storage。


localStorage和sessionStorage都具有相同的操作方法,例如setItem、getItem和removeItem等






收起阅读 »

前端面试常问的基础(四)

将元素定义为网格容器,并为其内容建立新的 网格格式上下文。值:grid :生成一个块级网格inline-grid :生成一个内联网格在Bootstrap中,栅格系统将容器均分为12份,再调整内外边距,结合媒体查询,造就了这一强大的...
继续阅读 »

将元素定义为网格容器,并为其内容建立新的 网格格式上下文。

值:

  • grid :生成一个块级网格
  • inline-grid :生成一个内联网格



在Bootstrap中,栅格系统将容器均分为12份,再调整内外边距,结合媒体查询,造就了这一强大的栅格系统。


flex布局

水平居中:

1. 行内元素,父元素 text-align : center

2. 块级元素有定宽,margin:0 auto;

3. 块级元素绝对定位,transform : translate(-50%,0);

4. 块级元素绝对定位,并且知道宽度, margin-left: - 宽度的一半            

5. 块级元素绝对定位,left:0;  right:0; margin:0 auto


垂直居中

1. 若元素是单行文本, 则可设置 line-height 等于父元素高度,原理见上面;

2. 若元素是行内块级元素, 基本思想是使用display: inline-block, vertical-align: middle和一个伪元素让内容块处于容器中央..parent::after, .son{ display:inline-block; vertical-align:middle; }

3. 使用flex, 在父元素上面添加.parent { display: flex; align-items: center;

4. 绝对定位的块用 transform: translate(0, -50%)

5. 绝对定位,并且有定高, margin-top : -高度的一半          注意不要用 margin-bottom,  不会生效的

6. 设置父元素相对定位(position:relative), 子元素如下css样式:.son{ position:absolute; height:固定; top:0; bottom:0; margin:auto 0; } 



重绘重排

重绘是一个元素的外观变化所引发的浏览器行为;

重排是引起DOM树重新计算的行为;


1、回流/重排


渲染树的一部分必须要更新且节点的尺寸发生了变化,会触发重排操作。每个页面至少在初始化的时候会有一次重排操作。


2、重绘


部分节点需要更新,但没有改变其形状,会触发重绘操作。




会触发重绘或回流/重排的操作


1、添加、删除元素(回流+重绘)


2、隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)


3、移动元素,如改变top、left或移动元素到另外1个父元素中(重绘+回流)


4、改变浏览器大小(回流+重绘)


5、改变浏览器的字体大小(回流+重绘)


6、改变元素的padding、border、margin(回流+重绘)


7、改变浏览器的字体颜色(只重绘,不回流)


8、改变元素的背景颜色(只重绘,不回流)


深入浏览器理解CSS animations 和 transitions的性能问题


GPU的快在于:

  1. 绘制位图到屏幕上
  2. 一遍又一遍地绘制相同的位图
  3. 将同一位图绘制到不同位置,执行旋转以及缩放处理

GPU 的慢在于:

  1. 将位图加载到它的内存中

在使用height,width,margin,padding作为transition的值时,会造成浏览器主线程的工作量较重,例如从margin-left:-20px渲染到margin-left:0,主线程需要计算样式margin-left:-19px,margin-left:-18px,一直到margin-left:0,而且每一次主线程计算样式后,合成进程都需要绘制到GPU然后再渲染到屏幕上,前后总共进行20次主线程渲染,20次合成线程渲染,20+20次,总计40次计算。


在使用css3 transtion做动画效果时,优先选择transform,尽量不要使用height,width,margin和padding。

transform为我们提供了丰富的api,例如scale,translate,rotate等等,但是在使用时需要考虑兼容性。但其实对于大多数css3来说,mobile端支持性较好,desktop端支持性需要格外注意。


物理像素(physical pixel) 

即:设备像素(device pixel)。 

本质是屏幕上的点,这个是跟设备有关系

CSS像素(css pixel) 

指的是CSS样式代码中使用的逻辑像素(或者叫虚拟像素)。 

软件要在设备上显示,css规定了长度单位(绝对单位和相对单位),比如:px 是一个 相对单位 ,相对的是 物理像素(physical pixel)

设备像素比(device pixel ratio) dpr 

公式:物理像素数(硬件) / 逻辑像素数(软件),即(物理像素/CSS像素)。 

在css中,可以通过 -webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio 和 -webkit-max-device-pixel-ratio 进行媒体查询,对不同dpr的设备,做一些样式适配。 

如: dpr = 2 时,1个CSS像素 = 4个物理像素。因为像素点都是正方形,所以当1个CSS像素需要的物理像素增多2倍时,其实就是长和宽都增加了2倍 


px em rem的区别

PX实际上就是像素,用PX设置字体大小时,比较稳定和精确。但是这种方法存在一个问题,当用户在浏览器中浏览我们制作的Web页面时,如果改变了浏览器的缩放,这时会使用我们的Web页面布局被打破。这样对于那些关心自己网站可用性的用户来说,就是一个大问题了。因此,这时就提出了使用“em”来定义Web页面的字体。


EM就是根据基准来缩放字体的大小。EM实质是一个相对值,而非具体的数值。这种技术需要一个参考点,一般都是以<body>的“font-size”为基准。如WordPress官方主题Twenntytwelve的基准就是14px=1em。

另外,em是相对于父元素的属性而计算的,如果想计算px和em之间的换算,输入数据就可以px和em相互计算。

Rem是相对于根元素<html>,这样就意味着,我们只需要在根元素确定一个参考值。







收起阅读 »

前端面试常问的基础(三)

 JS中浮点数精度误差解决如果有精度要求,可以用toFixed方法处理通用处理方案:把需要计算的数字乘以 10 的 n 次幂,换算成计算机能够精确识别的整数,然后再除以 10 的 n 次幂promises,observables,generator ...
继续阅读 »
 

 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象。

 通过new可以产生原对象的一个实例对象,而这个实例对象继承了原对象的属性和方法。因此, new存在的意义在于它实现了javascript中的继承,而不仅仅是实例化了一个对象!

  • JavaScript 中有哪些不同的函数调用模式? 详细解释。 提示:有四种模式,函数调用,方法调用,.call() 和 .apply()。
  1. 函数模式 fn()
  2. 方法模式 a.fn()
  3. 构造器模式 new
  4. 上下文模式 call apply

/*apply()方法*/两个参数 function.apply(thisObj[, argArray]) /*call()方法*/多个参数 function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);


  • 新 ECMAScript 提案

https://www.cnblogs.com/fundebug/p/what-is-new-in-javascript-for-2019.html

bigint

class 增加静态方法和属性 私有属性和方法


symbol值在序列化的过程中会被忽略或被转换成null



Fetch API 相对于传统的 Ajax 有哪些改进?

改进:promise 风格的api,async/await方式调用更友好,更简洁,错误处理更直观

缺点/难点:

  • fetch 是一种底层的 api,json传值必须转换成字符串,并且设置content-Type为application/json
  • fetch 默认情况下不会发送 cookie
  • 无法获取progress,也就是说无法用fetch做出有进度条的请求
  • 不能中断,我们知道传统的xhr是可以通过调用abort方法来终止我们的请求的

其实javasript的社区一直很活跃,相信上述问题很快会在未来的更新中解决


收起阅读 »

快来为你的照片添加个性标签吧!

搜索问题、话题或人… 问题 文章 代码 视频 活动· · ·ydhjhs发起Android快来为你的照片添加个性标签吧! 前言 需求图.png PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析: 使用用预览布局Surfa...
继续阅读 »

搜索问题、话题或人…
问题
文章
代码
视频
活动
· · ·
ydhjhs
发起
Android
快来为你的照片添加个性标签吧!


  1. 前言

需求图.png


PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析:


使用用预览布局SurfaceView,在不局上方使用控件的方式来进行设计,最后通过截图的方式将画面进行保存。


使用图片添加水印的方式来完成。



  1. 方法1 使用SurfaceView

我心想这不简单吗?于是开始一顿balabala的操作,结果到最后一步时发现,SurfaceView居然不能进行截图,截图下来的图片居然是一张黑色的。简单地说这是因为SurfaceView的特性决定的,我们知道安卓中唯一可以在子线程中进行绘制的view就只有Surfaceview了。他可以独立于子线程中绘制,不会导致主线程的卡顿,至于造成surfaceView黑屏的原因,可以移步这里
Android视图SurfaceView的实现原理分析。如果非要使用此方式时还是有三种思路来进行解决:
采用三种思路:


  1. 获取源头视频的截图作为SurfaceView的截图


  2. 获取SurfaceView的画布canvas,将canvas保存成Bitmap


  3. 直接截取整个屏幕,然后在截图SurfaceView位置的图



复制代码


但是我觉得这种方式太过繁琐,所以选择用添加水印的式来完成。



  1. 方法2 给拍照下来的图片添加水印

第一步:获取拍照权限








复制代码


这里使用到郭霖大佬的开源库PermissionX获取权限:


PermissionX.init(this)


.permissions(Manifest.permission.CAMERA,  Manifest.permission.RECORD_AUDIO)

.onExplainRequestReason { scope, deniedList ->

val message = "需要您同意以下权限才能正常使用"

scope.showRequestReasonDialog(deniedList, message, "确定", "取消")

}

.request { allGranted, grantedList, deniedList ->

if (allGranted) {

openCamera()

} else {

Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()

}

}

复制代码


第二步:拍照


android 6.0以后,相机权限需要动态申请。


// 申请相机权限的requestCode


private static final int PERMISSION_CAMERA_REQUEST_CODE = 0x00000012;


/**


* 检查权限并拍照。

* 调用相机前先检查权限。

*/

private void checkPermissionAndCamera() {


   int hasCameraPermission = ContextCompat.checkSelfPermission(getApplication(),

Manifest.permission.CAMERA);

if (hasCameraPermission == PackageManager.PERMISSION_GRANTED) {

//有调起相机拍照。

openCamera();

} else {

//没有权限,申请权限。

ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CAMERA},

PERMISSION_CAMERA_REQUEST_CODE);

}

}


/**


* 处理权限申请的回调。

*/

@Override


public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {


   if (requestCode == PERMISSION_CAMERA_REQUEST_CODE) {

if (grantResults.length > 0

&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

//允许权限,有调起相机拍照。

openCamera();

} else {

//拒绝权限,弹出提示框。

Toast.makeText(this,"拍照权限被拒绝",Toast.LENGTH_LONG).show();

}

}

}


复制代码


调用相机进行拍照


申请权限后,就可以调起相机拍照了。调用相机只需要调用startActivityForResult传一个Intent就可以了,但是这个Intent需要传递一个uri,用于保存拍出来的图片,创建这个uri时,各个Android版本有所不同,需要进行版本兼容。


//用于保存拍照图片的uri


private Uri mCameraUri;



// 用于保存图片的文件路径,Android 10以下使用图片路径访问图片

private String mCameraImagePath;



// 是否是Android 10以上手机

private boolean isAndroidQ = Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q;



/**

* 调起相机拍照

*/

private void openCamera() {

Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

// 判断是否有相机

if (captureIntent.resolveActivity(getPackageManager()) != null) {

File photoFile = null;

Uri photoUri = null;



if (isAndroidQ) {

// 适配android 10

photoUri = createImageUri();

} else {

try {

photoFile = createImageFile();

} catch (IOException e) {

e.printStackTrace();

}



if (photoFile != null) {

mCameraImagePath = photoFile.getAbsolutePath();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

//适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri

photoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", photoFile);

} else {

photoUri = Uri.fromFile(photoFile);

}

}

}



mCameraUri = photoUri;

if (photoUri != null) {

captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);

captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

startActivityForResult(captureIntent, CAMERA_REQUEST_CODE);

}

}

}



/**

* 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法

*/

private Uri createImageUri() {

String status = Environment.getExternalStorageState();

// 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储

if (status.equals(Environment.MEDIA_MOUNTED)) {

return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());

} else {

return getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());

}

}



/**

* 创建保存图片的文件

*/

private File createImageFile() throws IOException {

String imageName = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());

File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);

if (!storageDir.exists()) {

storageDir.mkdir();

}

File tempFile = new File(storageDir, imageName);

if (!Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(tempFile))) {

return null;

}

return tempFile;

}

复制代码


接收拍照结果


@Override


protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (requestCode == CAMERA_REQUEST_CODE) {

if (resultCode == RESULT_OK) {

if (isAndroidQ) {

// Android 10 使用图片uri加载

ivPhoto.setImageURI(mCameraUri);

} else {

// 使用图片路径加载

ivPhoto.setImageBitmap(BitmapFactory.decodeFile(mCameraImagePath));

}

} else {

Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();

}

}

}

复制代码


注意:


这两需要说明一下,Android 10由于文件权限的关系,显示手机储存卡里的图片不能直接使用图片路径,需要使用图片uri加载。


另外虽然我在这里对Android 10和10以下的手机使用了不同的方式创建uri 和加载图片,但其实Android 10创建uri的方式和使用uri加载图片的方式在10以下的手机是同样适用的。
android 7.0需要配置文件共享。

android:name="androidx.core.content.FileProvider"

android:authorities="${applicationId}.fileprovider"

android:exported="false"

android:grantUriPermissions="true">


android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/file_paths" />



复制代码


在res目录下创建文件夹xml ,放置一个文件file_paths.xml(文件名可以随便取),配置需要共享的文件目录,也就是拍照图片保存的目录。


<?xml version=”1.0” encoding=”utf-8”?>









name="images"

path="Pictures" />





复制代码


第三步:给拍照后得到的图片添加水印


@Override


protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (requestCode == CAMERA_REQUEST_CODE) {

if (resultCode == RESULT_OK) {

Bitmap mp;

if (isAndroidQ) {

// Android 10 使用图片uri加载

mp = MediaStore.Images.Media.getBitmap(this.contentResolver, t.uri);

} else {

// Android 10 以下使用图片路径加载

mp = BitmapFactory.decodeFile(uri);

}

//对图片添加水印 这里添加一张图片为示例:

ImageUtil.drawTextToLeftTop(this,mp,"示例文字",30,R.color.black,20,30)

} else {

Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();

}

}

}

复制代码


这里使用到一个ImageUtil工具类,我在这里贴上。如果需要使用可以直接拿走~


public class ImageUtil {


/**

* 设置水印图片在左上角

*

* @param context 上下文

* @param src

* @param watermark

* @param paddingLeft

* @param paddingTop

* @return

*/

public static Bitmap createWaterMaskLeftTop(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {

return createWaterMaskBitmap(src, watermark,

dp2px(context, paddingLeft), dp2px(context, paddingTop));

}



private static Bitmap createWaterMaskBitmap(Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {

if (src == null) {

return null;

}

int width = src.getWidth();

int height = src.getHeight();

//创建一个bitmap

Bitmap newb = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);// 创建一个新的和SRC长度宽度一样的位图

//将该图片作为画布

Canvas canvas = new Canvas(newb);

//在画布 0,0坐标上开始绘制原始图片

canvas.drawBitmap(src, 0, 0, null);

//在画布上绘制水印图片

canvas.drawBitmap(watermark, paddingLeft, paddingTop, null);

// 保存

canvas.save(Canvas.ALL_SAVE_FLAG);

// 存储

canvas.restore();

return newb;

}



/**

* 设置水印图片在右下角

*

* @param context 上下文

* @param src

* @param watermark

* @param paddingRight

* @param paddingBottom

* @return

*/

public static Bitmap createWaterMaskRightBottom(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingBottom) {

return createWaterMaskBitmap(src, watermark,

src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),

src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));

}



/**

* 设置水印图片到右上角

*

* @param context

* @param src

* @param watermark

* @param paddingRight

* @param paddingTop

* @return

*/

public static Bitmap createWaterMaskRightTop(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingTop) {

return createWaterMaskBitmap(src, watermark,

src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),

dp2px(context, paddingTop));

}



/**

* 设置水印图片到左下角

*

* @param context

* @param src

* @param watermark

* @param paddingLeft

* @param paddingBottom

* @return

*/

public static Bitmap createWaterMaskLeftBottom(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingBottom) {

return createWaterMaskBitmap(src, watermark, dp2px(context, paddingLeft),

src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));

}



/**

* 设置水印图片到中间

*

* @param src

* @param watermark

* @return

*/

public static Bitmap createWaterMaskCenter(Bitmap src, Bitmap watermark) {

return createWaterMaskBitmap(src, watermark,

(src.getWidth() - watermark.getWidth()) / 2,

(src.getHeight() - watermark.getHeight()) / 2);

}



/**

* 给图片添加文字到左上角

*

* @param context

* @param bitmap

* @param text

* @return

*/

public static Bitmap drawTextToLeftTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingTop) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

dp2px(context, paddingLeft),

dp2px(context, paddingTop) + bounds.height());

}



/**

* 绘制文字到右下角

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @return

*/

public static Bitmap drawTextToRightBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingBottom) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),

bitmap.getHeight() - dp2px(context, paddingBottom));

}



/**

* 绘制文字到右上方

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @param paddingRight

* @param paddingTop

* @return

*/

public static Bitmap drawTextToRightTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingTop) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),

dp2px(context, paddingTop) + bounds.height());

}



/**

* 绘制文字到左下方

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @param paddingLeft

* @param paddingBottom

* @return

*/

public static Bitmap drawTextToLeftBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingBottom) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

dp2px(context, paddingLeft),

bitmap.getHeight() - dp2px(context, paddingBottom));

}



/**

* 绘制文字到中间

*

* @param context

* @param bitmap

* @param text

* @param size

* @param color

* @return

*/

public static Bitmap drawTextToCenter(Context context, Bitmap bitmap, String text, int size, int color) {

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

paint.setColor(color);

paint.setTextSize(dp2px(context, size));

Rect bounds = new Rect();

paint.getTextBounds(text, 0, text.length(), bounds);

return drawTextToBitmap(context, bitmap, text, paint, bounds,

(bitmap.getWidth() - bounds.width()) / 2,

(bitmap.getHeight() + bounds.height()) / 2);

}



//图片上绘制文字

private static Bitmap drawTextToBitmap(Context context, Bitmap bitmap, String text, Paint paint, Rect bounds, int paddingLeft, int paddingTop) {

android.graphics.Bitmap.Config bitmapConfig = bitmap.getConfig();



paint.setDither(true); // 获取跟清晰的图像采样

paint.setFilterBitmap(true);// 过滤一些

if (bitmapConfig == null) {

bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;

}

bitmap = bitmap.copy(bitmapConfig, true);

Canvas canvas = new Canvas(bitmap);



canvas.drawText(text, paddingLeft, paddingTop, paint);

return bitmap;

}



/**

* 缩放图片

*

* @param src

* @param w

* @param h

* @return

*/

public static Bitmap scaleWithWH(Bitmap src, double w, double h) {

if (w == 0 || h == 0 || src == null) {

return src;

} else {

// 记录src的宽高

int width = src.getWidth();

int height = src.getHeight();

// 创建一个matrix容器

Matrix matrix = new Matrix();

// 计算缩放比例

float scaleWidth = (float) (w / width);

float scaleHeight = (float) (h / height);

// 开始缩放

matrix.postScale(scaleWidth, scaleHeight);

// 创建缩放后的图片

return Bitmap.createBitmap(src, 0, 0, width, height, matrix, true);

}

}



/**

* dip转pix

*

* @param context

* @param dp

* @return

*/

public static int dp2px(Context context, float dp) {

final float scale = context.getResources().getDisplayMetrics().density;

return (int) (dp * scale + 0.5f);

}

}


复制代码



  1. 最终实现的效果如下:

效果.jpg


5.总结


整体来说没有什么太大的问题,添加水印的原理就是通过Canvas绘制的方式将文字/图片添加到图片上。最后再将修改之后的图片呈现给用户。同时也记录下SurfaceView截图黑屏的问题。


作者:LiChengZe_Blog
链接:https://juejin.cn/post/6960579316191068197
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 收藏 分享 举报 2021-05-11
0 个评论
ydhjhs
写下你的评论…
发起人
柳天明柳天明
推荐内容
java设计模式:原型模式
算法与数据结构之数组
算法与数据结构之算法复杂度
算法与数据结构之链表
java设计模式:抽象工厂模式
java 设计模式:责任链模式与Android事件传递
java 设计模式:观察者
java 设计模式:模版方法
java 设计模式:策略模式
java 设计模式:工厂方法模式
一个开放、互助、协作、创意的社区
关于imGeek关于专职工程师值守社区财富榜赞助商友情链接热门标签
京ICP备14026002号-3
收起阅读 »

每次上线都出问题?现在他来了,上线之前测一测.

仿文墨天机命盘界面,自定义view 宫格实现是通过drawline画直线,拿到View的width和height的1/4,按照对应宫格宽度和高度进行偏移划线;因为中间的是占了4个小宫格矩形位置组成的大宫格矩形,因此需要分部分处理,观察图形后发现按照从上到下分为...
继续阅读 »

仿文墨天机命盘界面,自定义view 宫格实现是通过drawline画直线,拿到View的width和height的1/4,按照对应宫格宽度和高度进行偏移划线;因为中间的是占了4个小宫格矩形位置组成的大宫格矩形,因此需要分部分处理,观察图形后发现按照从上到下分为4部分最为合适,最上边申、酉、戌、亥四个宫位划分为TopTopArea,中上部分的未、子两个宫位划分为TopCenterArea,中下部分的午、丑两个宫位分为BottomCenterArea,最下边巳、辰、卯、寅四个宫位划分为BottomBottomArea。 x方向也是屏幕宽度方向,按照宫格所占宽度划线偏移。y方向也就是屏幕高度方向,从上往下按照宫格所占高度偏移划线。 宫格中的文字部分,按照申宫调用drawText绘制出文字,并通过Paint设置文字的大小,文字的位置,以及文字的颜色,酉、戌、亥宫中的文字布局位置通过申宫中的文字位置按照宫位所处的位置偏移即可,其它宫位原理亦如此,具体查看demo中的代码实现。

中宫需要注意的是它会有多行文字的显示,但是drawText 只能实现单行文字效果,那么要想实现多行代码效果,需要用到官方提供另外一个专门用来实现多行文字的函数StaticLayout,我们要展示的数据 val centerStrFromText ="姓名:匿名 ${info.性别} ${info.五行局}\n" + "真太阳时:${info.真太阳时}\n" + "钟表时间:${info.钟表时间}\n" + "农历:${info.年干}" + "${info.年支}年${info.农历月}月${info.农历日}日 ${info.时干}\n" + "命主:${info.命主} 身主:${info.身主} 子斗:${info.身宫}\n"] val centerStaticLayout = StaticLayout(centerStrFromText,mTextPaint,600,Layout.Alignment.ALIGN_NORMAL,1f,0f,true)

StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad) width 是文字区域的宽度,文字到达这个宽度后就会自动换行; align 是文字的对齐方向; spacingmult 是行间距的倍数,通常情况下填 1 就好; spacingadd 是行间距的额外增加值,通常情况下填 0 就好; includepad 是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。 多行文字偏移,需要配合canvas的translate方法实现,调用StaticLayout的draw方法进行绘制

添加点击事件,重写onTouchEvent方法,在 MotionEvent.ACTION_UP中判断用户点击位置是不是符合所在宫位的范围内如果是改变字体颜色,调用invalidate重新绘制其它宫位显示

实现效果1 实现效果2


代码下载:FortuneTelling-master.zip

收起阅读 »

看看微信是怎么处理图片的吧!

整体实现思路图片展示:PhotoView(大图支持双击放大)图片加载:Glide(加载网络图片、本地图片、资源文件)小图变大图时的实现:动画图片的下载:插入系统相册该控件采用自定义View的方式,通过一些基本的控件的组合,来形成一个具有大图预览的控件。上代码使...
继续阅读 »

整体实现思路

图片展示:PhotoView(大图支持双击放大)
图片加载:Glide(加载网络图片、本地图片、资源文件)
小图变大图时的实现:动画
图片的下载:插入系统相册

该控件采用自定义View的方式,通过一些基本的控件的组合,来形成一个具有大图预览的控件。上代码

使用方法

(1)在布局文件中引用该view

<com.demo.gallery.view.GalleryView
android:id="@+id/photo_gallery_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:animDuration="300"
app:saveText="保存至相册"
app:saveTextColor="#987622"/>

(2)具体使用方法
GalleryView galleryView = findViewById(R.id.photo_gallery_view);
galleryView.showPhotoGallery(index, List, ImageView);

到这里就结束了,就是这么简单!

具体实现

(1)先从showPhotoGallery(index, List, ImageView)这个方法讲起

int index:我们想要展示的一个图片列表中的第几个
List list: 我们要传入的要展示的图片类型list(支持网络图片、资源图片、本地图片(本地图片与网络图片其实都是一个字符串地址))

public class GalleryPhotoModel {

public Object photoSource;

public GalleryPhotoModel(@DrawableRes int drawableRes) {
this.photoSource = drawableRes;
}

public GalleryPhotoModel(String path) {
this.photoSource = path;
}

}

ImageView:即你点击想要展示的那个图片

(2)对传入GalleryView的数据进行处理

/**
* @param index 想要展示的图片的索引值
* @param photoList 图片集合(URL、Drawable、Bitmap)
* @param clickImageView 点击的第一个图片
*/
public void showPhotoGallery(int index, List<GalleryPhotoModel> photoList, ImageView clickImageView) {
GalleryPhotoParameterModel photoParameter = new GalleryPhotoParameterModel();
//图片
photoParameter.photoObj = photoList.get(index).photoSource;
//图片在list中的索引
photoParameter.index = index;
int[] locationOnScreen = new int[2];
//图片位置参数
clickImageView.getLocationOnScreen(locationOnScreen);
photoParameter.locOnScreen = locationOnScreen;
//图片的宽高
int width = clickImageView.getDrawable().getBounds().width();
int height = clickImageView.getDrawable().getBounds().height();
photoParameter.imageWidth = clickImageView.getWidth();
photoParameter.imageHeight = clickImageView.getHeight();
photoParameter.photoHeight = height;
photoParameter.photoWidth = width;
//scaleType
photoParameter.scaleType = clickImageView.getScaleType();
//将第一个点击的图片参数连同整个图片列表传入
this.setVisibility(View.VISIBLE);
post(new Runnable() {
@Override
public void run() {
requestFocus();
}
});
setGalleryPhotoList(photoList, photoParameter);
}

通过传递进来的ImageView,获取被点击View参数,并拼装成参数model,再进行数据的相关处理。

(3)GalleryView的实现机制

该View的实现思路主要是:最外层是一个RelativeLayout,内部有一个充满父布局的ImageView和ViewPager。ImageView用来进行图片的动画缩放,ViewPager用来进行最后的图片的展示。其实该View最主要的地方就是通过点击ImageView到最后ViewPager的展示的动画。接下来主要是讲解一下这个地方。先看一下被点击ImageView的参数Model。GalleryPhotoParameterModel

public class GalleryPhotoParameterModel {

//索引
public int index;
// 图片的类型
public Object photoObj;
// 在屏幕上的位置
public int[] locOnScreen = new int[]{-1, -1};
// 图片的宽
public int photoWidth = 0;
// 图片的高
public int photoHeight = 0;
// ImageView的宽
public int imageWidth = 0;
// ImageView的高
public int imageHeight = 0;
// ImageView的缩放类型
public ImageView.ScaleType scaleType;

}

3.1图片放大操作

private void handleZoomAnimation() {
// 屏幕的宽高
this.mScreenRect = GalleryScreenUtil.getDisplayPixes(getContext());
//将被缩放的图片放在一个单独的ImageView上进行单独的动画处理。
Glide.with(getContext()).load(firstClickItemParameterModel.photoObj).into(mScaleImageView);
//开启动画
mScaleImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//开始放大操作
calculateScaleAndStartZoomInAnim(firstClickItemParameterModel);
//
mScaleImageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
/**
* 计算放大比例,开启放大动画
*
* @param photoData
*/
private void calculateScaleAndStartZoomInAnim(final GalleryPhotoParameterModel photoData) {
mScaleImageView.setVisibility(View.VISIBLE);

// 放大动画参数
int translationX = (photoData.locOnScreen[0] + photoData.imageWidth / 2) - (int) (mScreenRect.width() / 2);
int translationY = (photoData.locOnScreen[1] + photoData.imageHeight / 2) - (int) ((mScreenRect.height() + GalleryScreenUtil.getStatusBarHeight(getContext())) / 2);
float scale = getImageViewScale(photoData);
// 开启放大动画
executeZoom(mScaleImageView, translationX, translationY, scale, true, new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}

@Override
public void onAnimationEnd(Animator animation) {
showOtherViews();
tvPhotoSize.setText(String.format("%d/%d", viewPager.getCurrentItem() + 1, photoList.size()));
}

@Override
public void onAnimationCancel(Animator animation) {

}

@Override
public void onAnimationRepeat(Animator animation) {

}
});
}

3.2 图片缩小操作

/**
* 计算缩小比例,开启缩小动画
*/
private void calculateScaleAndStartZoomOutAnim() {
hiedOtherViews();

// 缩小动画参数
int translationX = (firstClickItemParameterModel.locOnScreen[0] + firstClickItemParameterModel.imageWidth / 2) - (int) (mScreenRect.width() / 2);
int translationY = (firstClickItemParameterModel.locOnScreen[1] + firstClickItemParameterModel.imageHeight / 2) - (int) ((mScreenRect.height() + GalleryScreenUtil.getStatusBarHeight(getContext())) / 2);
float scale = getImageViewScale(firstClickItemParameterModel);
// 开启缩小动画
executeZoom(mScaleImageView, translationX, translationY, scale, false, new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}

@Override
public void onAnimationEnd(Animator animation) {
mScaleImageView.setImageDrawable(null);
mScaleImageView.setVisibility(GONE);
setVisibility(GONE);
}

@Override
public void onAnimationCancel(Animator animation) {}

@Override
public void onAnimationRepeat(Animator animation) {}
});
}

3.3 计算图片缩放的比例

private float getImageViewScale(GalleryPhotoParameterModel photoData) {
float scale;
float scaleX = photoData.imageWidth / mScreenRect.width();
float scaleY = photoData.photoHeight * 1.0f / mScaleImageView.getHeight();

// 横向图片
if (photoData.photoWidth > photoData.photoHeight) {
// 图片的宽高比
float photoScale = photoData.photoWidth * 1.0f / photoData.photoHeight;
// 执行动画的ImageView宽高比
float animationImageScale = mScaleImageView.getWidth() * 1.0f / mScaleImageView.getHeight();

if (animationImageScale > photoScale) {
// 动画ImageView宽高比大于图片宽高比的时候,需要用图片的高度除以动画ImageView高度的比例尺
scale = scaleY;
}
else {
scale = scaleX;
}
}
// 正方形图片
else if (photoData.photoWidth == photoData.photoHeight) {
if (mScaleImageView.getWidth() > mScaleImageView.getHeight()) {
scale = scaleY;
}
else {
scale = scaleX;
}
}
// 纵向图片
else {
scale = scaleY;
}
return scale;
}

3.4 执行动画的缩放

 /**
* 执行缩放动画
* @param scaleImageView
* @param translationX
* @param translationY
* @param scale
* @param isEnlarge
*/
private void executeZoom(final ImageView scaleImageView, int translationX, int translationY, float scale, boolean isEnlarge, Animator.AnimatorListener listener) {
float startTranslationX, startTranslationY, endTranslationX, endTranslationY;
float startScale, endScale, startAlpha, endAlpha;

// 放大
if (isEnlarge) {
startTranslationX = translationX;
endTranslationX = 0;
startTranslationY = translationY;
endTranslationY = 0;
startScale = scale;
endScale = 1;
startAlpha = 0f;
endAlpha = 0.75f;
}
// 缩小
else {
startTranslationX = 0;
endTranslationX = translationX;
startTranslationY = 0;
endTranslationY = translationY;
startScale = 1;
endScale = scale;
startAlpha = 0.75f;
endAlpha = 0f;
}

//-------缩小动画--------
AnimatorSet set = new AnimatorSet();
set.play(
ObjectAnimator.ofFloat(scaleImageView, "translationX", startTranslationX, endTranslationX))
.with(ObjectAnimator.ofFloat(scaleImageView, "translationY", startTranslationY, endTranslationY))
.with(ObjectAnimator.ofFloat(scaleImageView, "scaleX", startScale, endScale))
.with(ObjectAnimator.ofFloat(scaleImageView, "scaleY", startScale, endScale))
// ---Alpha动画---
// mMaskView伴随着一个Alpha减小动画
.with(ObjectAnimator.ofFloat(maskView, "alpha", startAlpha, endAlpha));
set.setDuration(animDuration);
if (listener != null) {
set.addListener(listener);
}
set.setInterpolator(new DecelerateInterpolator());
set.start();
}

改View的主要实现如上,在图片进行缩放的时候,要考虑的情况:短边适配、图片原尺寸的宽高、展示图片的ImageView的宽高比、横竖屏时屏幕的尺寸。在此非常感谢震哥的帮助、抱拳了!老铁。如有更多想法的小伙伴。请移步我的github GalleryView地址


代码下载:GalleryView-master.zip

收起阅读 »

【开源项目】使用环信SDK搭建在线教学场景(含三端源码下载)

 引言:2021年在线教育行业如火如荼,所谓人人为我,我为人人,为了方便教育行业的小伙伴们更好地使用环信SDK,IMGEEK论坛生态开发者@巍巍 发布了使用环信SDK搭建的在线教学开源项目“环环教育”,一期覆盖1对1互动教学、在线互动小班课...
继续阅读 »

 引言:2021年在线教育行业如火如荼,所谓人人为我,我为人人,为了方便教育行业的小伙伴们更好地使用环信SDK,IMGEEK论坛生态开发者@巍巍 发布了使用环信SDK搭建的在线教学开源项目“环环教育”,一期覆盖1对1互动教学、在线互动小班课两种核心教学场景,实现了iOS、Android和Web三端应用。此开源项目演示了环信IM SDK的部分API使用示例,以帮助开发者更好地理解和运用环信IM SDK。

 


 

开源项目简介:


环环教育demo,覆盖1对1互动教学、在线互动小班课两种核心教学场景,具备即时消息互动、实时音视频互动、互动白板、屏幕共享等丰富功能。Demo生动展示了如何用IM、音视频、互动白板SDK共同搭建在线互动教学场景。同时demo覆盖了教师端和学生端,并开发实现了iOS、Android、Web三端应用。现在正式开源给小伙伴们,详细介绍请往下看。

 

核心界面展示

1对1互动教学主界面



 移动端

 


Web端

 

 

在线互动小班课主界面

 


移动端

 

 



教师web端

 

 

 

核心功能

  • 1对1互动教学
  • 在线互动小班课(1名老师+多位学生互动)
  • 即时消息互动(聊天室)
  • 实时音视频互动(音视频)
  • 互动白板
  • 白板权限控制
  • 屏幕共享
  • 学生列表(小班课老师端特有)

 

资源下载

Github源码下载:(含iOS、Android、Web以及服务器端

https://github.com/easemob/learn-easemob

 

Demo下载体验:

Web端:https://cgame.bjictc.com/#/

iOS端:


识别二维码下载

 

 

Android端:

下载链接:https://download-sdk.oss-cn-beijing.aliyuncs.com/downloads/IMDemo/easemob_education_demo.apk


识别二维码下载

 

tips:同一房间名称+同一身份退出后不要重复多次进入,建议体验时退出后使用不同房间名。 

收起阅读 »

这个自定义键盘能让你欲罢不能!

KingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、身份证、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。Gif 展示引入Maven:<dependency&g...
继续阅读 »

KingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、身份证、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.keyboard</groupId>
<artifactId>kingkeyboard</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>

Gradle:

//AndroidX
implementation 'com.king.keyboard:kingkeyboard:1.0.0'

Lvy:

<dependency org='com.king.keyboard' name='kingkeyboard' rev='1.0.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

自定义按键值

 /*
* 在KingKeyboard的伴生对象中定义了一些核心的按键值,当您需要自定义键盘时,可能需要用到
*/

//------------------------------ 下面是定义的一些公用功能按键值
/**
* Shift键 -> 一般用来切换键盘大小写字母
*/
const val KEYCODE_SHIFT = -1
/**
* 模式改变 -> 切换键盘输入法
*/
const val KEYCODE_MODE_CHANGE = -2
/**
* 取消键 -> 关闭输入法
*/
const val KEYCODE_CANCEL = -3
/**
* 完成键 -> 长出现在右下角蓝色的完成按钮
*/
const val KEYCODE_DONE = -4
/**
* 删除键 -> 删除输入框内容
*/
const val KEYCODE_DELETE = -5
/**
* Alt键 -> 预留,暂时未使用
*/
const val KEYCODE_ALT = -6
/**
* 空格键
*/
const val KEYCODE_SPACE = 32

/**
* 无作用键 -> 一般用来占位或者禁用按键
*/
const val KEYCODE_NONE = 0

//------------------------------

/**
* 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
*/
const val KEYCODE_MODE_BACK = -101

/**
* 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
*/
const val KEYCODE_BACK = -102

/**
* 键盘按键 ->更多
*/
const val KEYCODE_MORE = -103

//------------------------------ 下面是自定义的一些预留按键值,与共用按键功能一致,但会使用默认的背景按键

const val KEYCODE_KING_SHIFT = -201
const val KEYCODE_KING_MODE_CHANGE = -202
const val KEYCODE_KING_CANCEL = -203
const val KEYCODE_KING_DONE = -204
const val KEYCODE_KING_DELETE = -205
const val KEYCODE_KING_ALT = -206

//------------------------------ 下面是自定义的一些功能按键值,与共用按键功能一致,但会使用默认背景颜色

/**
* 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
*/
const val KEYCODE_KING_MODE_BACK = -251

/**
* 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
*/
const val KEYCODE_KING_BACK = -252

/**
* 键盘按键 ->更多
*/
const val KEYCODE_KING_MORE = -253

/*
用户也可自定义按键值,primaryCode范围区间为-999 ~ -300时,表示预留可扩展按键值。
其中-399~-300区间为功能型按键,使用Special背景色,-999~-400自定义按键为默认背景色
*/

示例

代码示例

    //初始化KingKeyboard
kingKeyboard = KingKeyboard(this,keyboardParent)
//然后将EditText注册到KingKeyboard即可
kingKeyboard.register(editText,KingKeyboard.KeyboardType.NUMBER)

/*
* 如果目前所支持的键盘满足不了您的需求,您也可以自定义键盘,KingKeyboard对外提供自定义键盘类型。
* 自定义步骤也非常简单,只需自定义键盘的xml布局,然后将EditText注册到对应的自定义键盘类型即可
*
* 1. 自定义键盘Custom,自定义方法setKeyboardCustom,键盘类型为{@link KeyboardType#CUSTOM}
* 2. 自定义键盘CustomModeChange,自定义方法setKeyboardCustomModeChange,键盘类型为{@link KeyboardType#CUSTOM_MODE_CHANGE}
* 3. 自定义键盘CustomMore,自定义方法setKeyboardCustomMore,键盘类型为{@link KeyboardType#CUSTOM_MORE}
*
* xmlLayoutResId 键盘布局的资源文件,其中包含键盘布局和键值码等相关信息
*/
kingKeyboard.setKeyboardCustom(R.xml.keyboard_custom)
// kingKeyboard.setKeyboardCustomModeChange(xmlLayoutResId)
// kingKeyboard.setKeyboardCustomMore(xmlLayoutResId)
kingKeyboard.register(et12,KingKeyboard.KeyboardType.CUSTOM)
 //获取键盘相关的配置信息
var config = kingKeyboard.getKeyboardViewConfig()

//... 修改一些键盘的配置信息

//重新设置键盘配置信息
kingKeyboard.setKeyboardViewConfig(config)

//按键是否启用震动
kingKeyboard.setVibrationEffectEnabled(isVibrationEffectEnabled)

//... 等等,还有各种监听方法。更多详情,请直接使用。
    //在Activity或Fragment相应的生命周期中调用,如下所示

override fun onResume() {
super.onResume()
kingKeyboard.onResume()
}

override fun onDestroy() {
super.onDestroy()
kingKeyboard.onDestroy()
}

相关说明

  • KingKeyboard主要采用Kotlin编写实现,如果您的项目使用的是Java编写,集成时语法上可能稍微有点不同,除了结尾没有分号以外,对应类伴生对象中的常量,需要通过点伴生对象才能获取。
  //Kotlin 写法
var keyCode = KingKeyboard.KEYCODE_SHIFT
  //Java 写法
int keyCode = KingKeyboard.Companion.KEYCODE_SHIFT;

更多使用详情,请查看app中的源码使用示例

代码下载:KingKeyboard-master.zip

收起阅读 »

多个模块如何管理?用它就对了!

FragmentationA powerful library that manage Fragment for Android!为"单Activity + 多Fragment","多模块Activity + 多Fragment"架构而生,简化开发,轻松解决动...
继续阅读 »

Fragmentation

A powerful library that manage Fragment for Android!

为"单Activity + 多Fragment","多模块Activity + 多Fragment"架构而生,简化开发,轻松解决动画、嵌套、事务相关等问题。

为了更好的使用和了解该库,推荐阅读下面的文章:

Fragment全解析系列(一):那些年踩过的坑

Fragment全解析系列(二):正确的使用姿势

Demo演示:

均为单Activity + 多Fragment,第一个为简单流式demo,第二个为仿微信交互的demo(全页面支持滑动退出),第三个为仿知乎交互的复杂嵌套demo

下载APK

  

特性

1、悬浮球/摇一摇实时查看Fragment的栈视图,降低开发难度

2、内部队列机制 解决Fragment多点触控、事务高频次提交异常等问题

3、增加启动模式、startForResult等类Activity方法

4、类Android事件分发机制的Fragment BACK键机制:onBackPressedSupport()

5、提供onSupportVisible()、懒加载onLazyInitView()等生命周期方法,简化嵌套Fragment的开发过程

6、提供 Fragment转场动画 系列解决方案,动态改变动画

7、提供Activity作用域的EventBus辅助类,Fragment通信更简单、独立(需要使用EventBusActivityScope库)

8、支持SwipeBack滑动边缘退出(需要使用Fragmentation_SwipeBack库)

      

如何使用

1. 项目下app的build.gradle中依赖:

// appcompat-v7包是必须的
compile 'me.yokeyword:fragmentation:1.3.8'

// 如果不想继承SupportActivity/Fragment,自己定制Support,可仅依赖:
// compile 'me.yokeyword:fragmentation-core:1.3.8'

// 如果想使用SwipeBack 滑动边缘退出Fragment/Activity功能,完整的添加规则如下:
compile 'me.yokeyword:fragmentation:1.3.8'
// swipeback基于fragmentation, 如果是自定制SupportActivity/Fragment,则参照SwipeBackActivity/Fragment实现即可
compile 'me.yokeyword:fragmentation-swipeback:1.3.8'

// Activity作用域的EventBus,更安全,可有效避免after onSavenInstanceState()异常
compile 'me.yokeyword:eventbus-activity-scope:1.1.0'
// Your EventBus's version
compile 'org.greenrobot:eventbus:{version}'

2. Activity extends SupportActivity或者 implements ISupportActivity:(实现方式可参考MySupportActivity)

// v1.0.0开始,不强制继承SupportActivity,可使用接口+委托形式来实现自己的SupportActivity
public class MainActivity extends SupportActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(...);
// 建议在Application里初始化
Fragmentation.builder()
// 显示悬浮球 ; 其他Mode:SHAKE: 摇一摇唤出 NONE:隐藏
.stackViewMode(Fragmentation.BUBBLE)
.debug(BuildConfig.DEBUG)
... // 更多查看wiki或demo
.install();

if (findFragment(HomeFragment.class) == null) {
loadRootFragment(R.id.fl_container, HomeFragment.newInstance()); // 加载根Fragment
}
}

3. Fragment extends SupportFragment或者 implements ISupportFragment:(实现方式可参考MySupportFragment):

// v1.0.0开始,不强制继承SupportFragment,可使用接口+委托形式来实现自己的SupportFragment
public class HomeFragment extends SupportFragment {

private void xxx() {
// 启动新的Fragment, 另有start(fragment,SINGTASK)、startForResult、startWithPop等启动方法
        start(DetailFragment.newInstance(HomeBean));
// ... 其他pop, find, 设置动画等等API, 请自行查看WIKI
    }
}


代码下载:Fragmentation-master.zip

收起阅读 »

为什么别人的状态栏那么好看,而你自己却无法实现!

这是一个为Android App 设置状态栏的工具类, 可以在4.4及其以上系统中实现 沉浸式状态栏/状态栏变色,支持设置状态栏透明度,满足你司设计师的各种要求(雾)。在此之前我写过一篇Android App 沉浸式状态栏解决方案,后来我司设计师说默认的透明度...
继续阅读 »

这是一个为Android App 设置状态栏的工具类, 可以在4.4及其以上系统中实现 沉浸式状态栏/状态栏变色,支持设置状态栏透明度,满足你司设计师的各种要求(雾)。

在此之前我写过一篇Android App 沉浸式状态栏解决方案,后来我司设计师说默认的透明度太深了,让我改浅一点,然后在想了一些办法之后给解决了。本着不重复造轮子的原则,索性整理成一个工具类,方便需要的开发者。

项目 GitHub 地址

Sample 下载

下载 StatusBarUtil-Demo

特性

  1. 设置状态栏颜色

    StatusBarUtil.setColor(Activity activity, int color)

  2. 设置状态栏半透明

    StatusBarUtil.setTranslucent(Activity activity, int statusBarAlpha)

  3. 设置状态栏全透明

    StatusBarUtil.setTransparent(Activity activity)

  4. 为包含 DrawerLayout 的界面设置状态栏颜色(也可以设置半透明和全透明)

    StatusBarUtil.setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, int color)

  5. 为使用 ImageView 作为头部的界面设置状态栏透明

    StatusBarUtil.setTranslucentForImageView(Activity activity, int statusBarAlpha, View needOffsetView)

  6. 在 Fragment 中使用

  7. 为滑动返回界面设置状态栏颜色

    推荐配合 bingoogolapple/BGASwipeBackLayout-Android: Android Activity 滑动返回 这个库一起使用。

    StatusBarUtil.setColorForSwipeBack(Activity activity, @ColorInt int color, int statusBarAlpha)

  8. 通过传入 statusBarAlpha 参数,可以改变状态栏的透明度值,默认值是112。

使用

  1. 在 build.gradle 文件中添加依赖, StatusBarUtil 已经发布在 JCenter:

    compile 'com.jaeger.statusbarutil:library:1.4.0'
  2. 在 setContentView() 之后调用你需要的方法,例如:

    setContentView(R.layout.main_activity);
    ...
    StatusBarUtil.setColor(MainActivity.this, mColor);
  3. 如果你在一个包含 DrawerLayout 的界面中使用, 你需要在布局文件中为 DrawerLayout 添加 android:fitsSystemWindows="true" 属性:

    <android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    ...

    </android.support.v4.widget.DrawerLayout>
  4. 滑动返回界面设置状态栏颜色:

    建议配合 bingoogolapple/BGASwipeBackLayout-Android: Android Activity 滑动返回 库一起使用。

    StatusBarUtil.setColorForSwipeBack(Activity activity, @ColorInt int color, int statusBarAlpha)
  5. 当你设置了 statusBarAlpha 值时,该值需要在 0 ~ 255 之间

  6. 在 Fragment 中的使用可以参照 UseInFragmentActivity.java 来实现

收起阅读 »

还在自己写Adapter?再不看你就out了

base-adapterAndroid 万能的Adapter for ListView,RecyclerView,GridView等,支持多种Item类型的情况。引入ForRecyclerViewcompile 'com.zhy:base-rvadapter:...
继续阅读 »

base-adapter

Android 万能的Adapter for ListView,RecyclerView,GridView等,支持多种Item类型的情况。

引入

ForRecyclerView

compile 'com.zhy:base-rvadapter:3.0.3'

ForListView

compile 'com.zhy:base-adapter:3.0.3'

使用

##(1)简单的数据绑定(ListView与其使用方式一致)

首先看我们最常用的单种Item的书写方式:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
@Override
public void convert(ViewHolder holder, String s)
{
holder.setText(R.id.id_item_list_title, s);
}
});

是不是相当方便,在convert方法中完成数据、事件绑定即可。

只需要简单的将Adapter继承CommonAdapter,复写convert方法即可。省去了自己编写ViewHolder等大量的重复的代码。

  • 可以通过holder.getView(id)拿到任何控件。
  • ViewHolder中封装了大量的常用的方法,比如holder.setText(id,text),holder.setOnClickListener(id,listener)等,可以支持使用。

效果图:

##(2)多种ItemViewType(ListView与其使用方式一致)

对于多中itemviewtype的处理参考:https://github.com/sockeqwe/AdapterDelegates ,具有极高的扩展性。

MultiItemTypeAdapter adapter = new MultiItemTypeAdapter(this,mDatas);
adapter.addItemViewDelegate(new MsgSendItemDelagate());
adapter.addItemViewDelegate(new MsgComingItemDelagate());

每种Item类型对应一个ItemViewDelegete,例如:

public class MsgComingItemDelagate implements ItemViewDelegate<ChatMessage>
{

@Override
public int getItemViewLayoutId()
{
return R.layout.main_chat_from_msg;
}

@Override
public boolean isForViewType(ChatMessage item, int position)
{
return item.isComMeg();
}

@Override
public void convert(ViewHolder holder, ChatMessage chatMessage, int position)
{
holder.setText(R.id.chat_from_content, chatMessage.getContent());
holder.setText(R.id.chat_from_name, chatMessage.getName());
holder.setImageResource(R.id.chat_from_icon, chatMessage.getIcon());
}
}

贴个效果图:

##(3) 添加HeaderView、FooterView

mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);

TextView t1 = new TextView(this);
t1.setText("Header 1");
TextView t2 = new TextView(this);
t2.setText("Header 2");
mHeaderAndFooterWrapper.addHeaderView(t1);
mHeaderAndFooterWrapper.addHeaderView(t2);

mRecyclerView.setAdapter(mHeaderAndFooterWrapper);
mHeaderAndFooterWrapper.notifyDataSetChanged();

类似装饰者模式,直接将原本的adapter传入,初始化一个HeaderAndFooterWrapper对象,然后调用相关API添加。

##(4) 添加LoadMore

mLoadMoreWrapper = new LoadMoreWrapper(mOriginAdapter);
mLoadMoreWrapper.setLoadMoreView(R.layout.default_loading);
mLoadMoreWrapper.setOnLoadMoreListener(new LoadMoreWrapper.OnLoadMoreListener()
{
@Override
public void onLoadMoreRequested()
{
}
});

mRecyclerView.setAdapter(mLoadMoreWrapper);

直接将原本的adapter传入,初始化一个LoadMoreWrapper对象,然后调用相关API即可。

##(5)添加EmptyView

mEmptyWrapper = new EmptyWrapper(mAdapter);
mEmptyWrapper.setEmptyView(R.layout.empty_view);

mRecyclerView.setAdapter(mEmptyWrapper );

直接将原本的adapter传入,初始化一个EmptyWrapper对象,然后调用相关API即可。

支持链式添加多种功能,示例代码:

mAdapter = new EmptyViewWrapper(
new LoadMoreWrapper(
new HeaderAndFooterWrapper(mOriginAdapter)));

一些回调

onViewHolderCreated

mListView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
@Override
public void convert(ViewHolder holder, String o, int pos)
{
holder.setText(R.id.id_item_list_title, o);
}

@Override
public void onViewHolderCreated(ViewHolder holder, View itemView)
{
super.onViewHolderCreated(holder, itemView);
//AutoUtil.autoSize(itemView)
}
});

代码下载:
baseAdapter-master.zip
收起阅读 »

为什么别人都在摸鱼就你在加班?用对工具让你事半功倍!

主要包括:缓存(图片缓存、预取缓存、网络缓存)、公共View(下拉及底部加载更多ListView、底部加载更多ScrollView、滑动一页Gallery)及Android常用工具类(网络、下载、Android资源操作、shell、文件、Json、随机数、Co...
继续阅读 »

主要包括缓存(图片缓存、预取缓存、网络缓存)、公共View(下拉及底部加载更多ListView、底部加载更多ScrollView、滑动一页Gallery)及Android常用工具类(网络、下载、Android资源操作、shell、文件、Json、随机数、Collection等等)。
示例源码:TrineaAndroidDemo
使        用:拉取代码导入IDE,右击你的工程->properties->Android,在library中选择TrineaAndroidCommon。
Api Guide:TrineaAndroidCommon API Guide

Dev Tools App

The Dev Tools App is a powerful android development tool that can help you improve efficiency greatly, It can be used to view the latest open source projects, view activity history, view manifest, decompile, color picker, extract apk or so, view app info, open or close the options in the developer options quickly, and more.

You can download it from DevTools@Google Play.

一. 缓存类

主要特性:(1).使用简单 (2).轻松获取及预取取新图片 (3).包含二级缓存 (4).可选择多种缓存算法(FIFO、LIFO、LRU、MRU、LFU、MFU等13种)或自定义缓存算法 (5).可方便的保存及初始化恢复数据 (6).省流量性能佳(有且仅有一个线程获取图片) (7).支持http请求header设置及不同类型网络处理(8).可根据系统配置初始化缓存 (9).扩展性强 (10).支持等待队列 (11)包含map的大多数接口。

1. 图片缓存

使用见:图片缓存的使用
适用:获取图片较多且图片使用频繁的应用,包含二级缓存,如新浪微博、twitter、微信头像、美丽说、蘑菇街、花瓣、淘宝等等。效果图如下:
ImageCahe

2. 图片SD卡缓存

使用见:图片SD卡缓存的使用
适用:应用中获取图片较多且图片较大的情况。需要二级缓存及ListView或GridView图片加载推荐使用上面的ImageCache。效果图如下:
ImageSDCardCache

3. 网络缓存

使用见:Android网络缓存
适用:网络获取内容不大的应用,尤其是api接口数据,如新浪微博、twitter的timeline、微信公众账号发送的内容等等。效果图如下:
HttpCache

4. 预取数据缓存

使用见:预取数据缓存
缓存类关系图如下:其中HttpCache为后续计划的http缓存 Image Cache

二. 公用的view

1. 下拉刷新及滚动到底部加载更多的Listview

使用: 下拉刷新及滚动到底部加载更多listview的使用
实现原理: http://trinea.iteye.com/blog/1562281。效果图如下:
DropDownListView

2. 滑动一页(一个Item)的Gallery

使用及实现原理:滑动一页(一个Item)的Gallery的使用。效果图如下:
ViewPager1 ViewPager2

3. 滑动到底部或顶部响应的ScrollView

使用及实现原理: 滚动到底部或顶部响应的ScrollView使用。效果图如下:
ScrollView

三. 工具类

具体介绍可见:Android常用工具类
目前包括HttpUtils、DownloadManagerProShellUtilsPackageUtils、PreferencesUtils、JSONUtils、FileUtils、ResourceUtils、StringUtils、ParcelUtils、RandomUtils、ArrayUtils、ImageUtils、ListUtils、MapUtils、ObjectUtils、SerializeUtils、SystemUtils、TimeUtils。

1. Android系统下载管理DownloadManager使用

使用示例:Android系统下载管理DownloadManager功能介绍及使用示例
功能扩展:Android下载管理DownloadManager功能扩展和bug修改 效果图如下:
downloadManagerDemo

2. Android APK root权限静默安装

使用示例:Android APK root权限静默安装

3. Android root权限

直接调用ShellUtils.execCommand方法

4. 图片工具类

(1)Drawable、Bitmap、byte数组相互转换; (2)根据url获得InputStream、Drawable、Bitmap
更多工具类介绍见Android常用工具类

Proguard

-keep class cn.trinea.android.** { *; }
-keepclassmembers class cn.trinea.android.** { *; }
-dontwarn cn.trinea.android.**

Download

Gradle:

compile 'cn.trinea.android.common:trinea-android-common:4.2.15'



代码下载:android-common-master.zip

收起阅读 »

Android面试:80%的面试官关于Glide都会问这几个问题!【建议收藏】

Glide的三级缓存有了解过么? 先来了解一下我们常说的图片三级缓存 一般是强引用,软引用和文件系统,Android系统中提供了LruCache,通过维护一个LinkedHashMap来保存我们需要的各种类型数据,例如我们这里需要的Bitmap。Lr...
继续阅读 »



Glide的三级缓存有了解过么?



  • 先来了解一下我们常说的图片三级缓存


一般是强引用,软引用和文件系统,Android系统中提供了LruCache,通过维护一个LinkedHashMap来保存我们需要的各种类型数据,例如我们这里需要的Bitmap。LruCache一般我们会设置为系统最大存储空间的八分之一,而它的机制就是我们常说的最近最少使用原则,如果Lru中的图片大小超过了默认大小,则会把最久使用的图片移除。


当图片被Lru移除时,我们需要手动将图片添加到软引用(SoftRefrence)中。需要维护一个软应用的集合在我们的项目中。



  • 简单概括一下常用的三级缓存的流程:


先去Lru中找,有则直接取。
没有,则去SoftRefrence中找,有则取,同时将图片放回Lru中。
没有的话去文件系统找,有则取,同时将图片添加到Lru中。
没有就走下载图片逻辑,保存到文件系统中,并放到Lru中。

下面介绍一下Glide的缓存结构:


Glide缓存严格意义上说只有内存缓存和磁盘缓存,内存缓存中又分为Lru和弱引用缓存。


所以Glide的三级缓存可以分为:Lru缓存,弱引用缓存,磁盘缓存。


下面我们看一下Glide的读取顺序,这里有一点不同,我用的是Glide4.8版本,跟之前版本的写入顺序稍有不同。


截取部分源码:

@NonNull
Glide build(@NonNull Context context) {

if (memoryCache == null) {
memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}

if (engine == null) {
engine =
new Engine(
memoryCache,
diskCacheFactory,
diskCacheExecutor,
sourceExecutor,
GlideExecutor.newUnlimitedSourceExecutor(),
GlideExecutor.newAnimationExecutor(),
isActiveResourceRetentionAllowed);
}


  • memoryCache就是Glide使用的内存缓存,LruResourceCache类继承了LruCache,这部分可以自行查看一下源码。


通过上面可以看到,GLide#build()方法中实例化memoryCache作为Glide的内存缓存,并将其传给Engine作为构造器的入参。



  • Engine.class 截取部分源码


{
//生成缓存key
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
//从弱应用中读取缓存
EngineResource active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active, DataSource.MEMORY_CACHE);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
//从LruCache中读取缓存
EngineResource cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
EngineJob engineJob =
engineJobFactory.build(
key,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache);
jobs.put(key, engineJob);

engineJob.addCallback(cb);
//开启线程池,加载图片
engineJob.start(decodeJob);
}

从上可知,Glide加载过程中使用loadFromActiveResources方法和loadFromCache方法来获取内存缓存的。


大致总结一下: 首先从弱引用读取缓存,没有的话通过Lru读取,有则取,并且加到弱引用中,如果没有会开启EngineJob进行后面的图片加载逻辑。


下面直接看之后的缓存部分代码:



  • Engine#onEngineJobComplete()


public void onEngineJobComplete(EngineJob engineJob, Key key, EngineResource resource) {
Util.assertMainThread();
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);

if (resource.isCacheable()) {
activeResources.activate(key, resource);
}
}

jobs.removeIfCurrent(key, engineJob);
}
void activate(Key key, EngineResource resource) {
ResourceWeakReference toPut =
new ResourceWeakReference(
key,
resource,
getReferenceQueue(),
isActiveResourceRetentionAllowed);

ResourceWeakReference removed = activeEngineResources.put(key, toPut);
if (removed != null) {
removed.reset();
}
}

这里可以看到activeResources.activate(key, resource)把EngineResource放到了弱引用中,至于lru的放置逻辑如下:



  • EngineResource#release()


void release() {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call release on the main thread");
}
if (--acquired == 0) {
listener.onResourceReleased(key, this);
}
}

当acquired变量大于0的时候,说明图片正在使用中,也就应该放到activeResources弱引用缓存当中。而经过release()之后,如果acquired变量等于0了,说明图片已经不再被使用了,那么此时会调用listener的onResourceReleased()方法来释放资源。



  • Engine#onResourceReleased()


@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
Util.assertMainThread();
activeResources.deactivate(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}

这里首先会将缓存图片从activeResources中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。


接下来就是Glide的磁盘缓存,磁盘缓存简单来说就是根据Key去DiskCache中取缓存,有兴趣可以自行看一下源码。


为什么选择Glide不选择其他的图片加载框架?



  • Glide和Picasso


前者要更加省内存,可以按需加载图片,默认为ARGB_565,后者为ARGB_8888。


前者支持Gif,后者并不支持。



  • Glide和Fresco


Fresco低版本有优势,占用部分native内存,但是高版本一样是java内存。


Fresco加载对图片大小有限制,Glide基本没有。


Fresco推荐使用SimpleDraweeView,涉及到布局文件,这就不得不考虑迁移的成本。


Fresco有很多native的实现,想改源码成本要大的多。


Glide提供对中TransFormation帮助处理图片,Fresco并没有。


Glide版本迭代相对较快。


Glide的几个显著的优点:



  • 生命周期的管理


GLide#with


  @NonNull
public static RequestManager with(@NonNull Context context) {
return getRetriever(context).get(context);
}

@NonNull
public static RequestManager with(@NonNull Activity activity) {
return getRetriever(activity).get(activity);
}

@NonNull
public static RequestManager with(@NonNull FragmentActivity activity) {
return getRetriever(activity).get(activity);
}

@NonNull
public static RequestManager with(@NonNull Fragment fragment) {
return getRetriever(fragment.getActivity()).get(fragment);
}

@Deprecated
@NonNull
public static RequestManager with(@NonNull android.app.Fragment fragment) {
return getRetriever(fragment.getActivity()).get(fragment);
}

可以看到有多个重载方法,主要对两类不同的Context进行不同的处理



  • Application Context 图片加载的生命周期和应用程序一样,肯定是我们不推荐的写法。

  • 其余Context,会像当前Activity创建一个隐藏的Fragment,绑定生命周期。


以Activity为例:


 @NonNull
public RequestManager get(@NonNull Activity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else {
//判断是否是销毁状态
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
//绑定生命周期
return fragmentGet(
activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
}
}

具体看#fragmentGet()


@NonNull
private RequestManager fragmentGet(@NonNull Context context,
@NonNull android.app.FragmentManager fm,
@Nullable android.app.Fragment parentHint,
boolean isParentVisible) {
//这就是绑定的Fragment,RequestManagerFragment
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
RequestManager requestManager = current.getRequestManager();

return requestManager;
}

接着看RequestManagerFragment


public class RequestManagerFragment extends Fragment {
@Override
public void onStart() {
super.onStart();
lifecycle.onStart();
}
@Override
public void onStop() {
super.onStop();
lifecycle.onStop();
}

@Override
public void onDestroy() {
super.onDestroy();
lifecycle.onDestroy();
unregisterFragmentWithRoot();
}

}

关联lifeCycle相应的方法。


简单来说就是通过#with()方法根据穿过来的不同的Context绑定生命周期。



  • Bitmap对象池


Glide提供了一个BitmapPool来保存Bitmap。 简单来说就是当需要加载一个bitmap的时候,会根据图片的参数去池子里找到一个合适的bitmap,如果没有就重新创建。BitMapPool同样是根据Lru算法来工作的。从而提高性能。



  • 高效缓存


缓存相关可以看上文描述,内存和磁盘,磁盘缓存也提供了几种缓存策略。



  1. NONE,表示不缓存任何内容

  2. SOURCE,表示只缓存原始图片

  3. RESULT,表示只缓存转换过后的图片(默认选项)

  4. ALL, 表示既缓存原始图片,也缓存转换过后的图片


文末


好了,今天的文章就到这里,感谢阅读,喜欢的话不要忘了三连。大家的支持和认可,是我分享的最大动力。


对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。


收起阅读 »

做了这么多年开发,TypedArray你该知道的东西

大家好,我是程序员双木L,后续会发专题类的文章,这是自定义控件的第一篇,之后也会陆续更新相关的文章,欢迎关注。 自定义属性在自定义控件过程中属于比较常见的操作,我们可以回想一下这样的场景:自定义view的过程中,我们需要在不同的情况下设置不同的文字大小,那么...
继续阅读 »




大家好,我是程序员双木L,后续会发专题类的文章,这是自定义控件的第一篇,之后也会陆续更新相关的文章,欢迎关注。


自定义属性在自定义控件过程中属于比较常见的操作,我们可以回想一下这样的场景:自定义view的过程中,我们需要在不同的情况下设置不同的文字大小,那么我们是不是就需要提供对外的方法来设置,这样就比较灵活操作。而我们自定义对外的方法,就是我们自定义的属性啦,那我们来分析一下其原理及作用。


下面我们根据例子来进行分析:


1、首先我们需要在res->values目录下新建attrs.xml文件,该文件就是用来声明属性名及其接受的数据格式的,如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="view_int" format="integer" />
<attr name="view_str" format="string" />
<attr name="view_bool" format="boolean" />
<attr name="view_color" format="color" />
<attr name="view_ref" format="reference" />
<attr name="view_float" format="float" />
<attr name="view_dim" format="dimension" />
<attr name="view_frac" format="fraction" />

<attr name="view_enum">
<enum name="num_one" value="1" />
<enum name="num_two" value="2" />
<enum name="num_three" value="3" />
<enum name="num_four" value="4" />
</attr>

<attr name="view_flag">
<flag name="top" value="0x1" />
<flag name="left" value="0x2" />
<flag name="right" value="0x3" />
<flag name="bottom" value="0x4" />
</attr>

</resources>

attr名词解析:


name表示属性名,上面的属性名是我自己定义的。


format表示接受的输入格式,format格式集合如下:


color:颜色值;
boolean:布尔值;
dimension:尺寸值,注意,这里如果是dp那就会做像素转换;
float:浮点值;
integer:整型值;
string:字符串;
fraction:百分数;
enum:枚举值;
flag:是自己定义的,就是里面对应了自己的属性值;
reference:指向其它资源;
reference|color:颜色的资源文件;
reference|boolean:布尔值的资源文件.

2、自定义属性的使用,这里我们使用两种方式进行对比解析


最最最原始的使用方式


(1)、自定义文件如下:


public class TestAttrsView extends View {
private final String TAG = "TestAttrsView:";

public TestAttrsView(Context context) {
this(context, null);
}

public TestAttrsView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public TestAttrsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

//最原始使用方式
for (int i = 0; i < attrs.getAttributeCount(); i++) {
Log.i(TAG, "name:" + attrs.getAttributeName(i) + " value:" + attrs.getAttributeValue(i));
}
}
}

我们可以在TestAttrsView方法的参数AttributeSet是个xml解析工具类,帮助我们从布局的xml里提取属性名和属性值。


(2)、在布局文件xml中的使用


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.example.viewdemo.customView.TestAttrsView
android:layout_width="200dp"
android:layout_height="200dp"
app:view_bool="true"
app:view_color="#e5e5e5"
app:view_dim="10px"
app:view_float="5.0"
app:view_frac="100%"
app:view_int="10"
app:view_ref="@dimen/dp_15"
app:view_str="test attrs view" />

</FrameLayout>

这里使用自定义属性需要声明xml的命名空间,其中app是命名空间,用来加在自定义属性前面。


xmlns:app=“http://schemas.android.com/apk/res-auto”
声明xml命名空间,xmlns意思为“xml namespace”.冒号后面是给这个引用起的别名。
schemas是xml文档的两种约束文件其中的一种,规定了xml中有哪些元素(标签)、
元素有哪些属性及各元素的关系,当然从面向对象的角度理解schemas文件可以
认为它是被约束的xml文档的“类”或称为“模板”。


(3)、将属性名与属性值打印结果如下:


在这里插入图片描述


从打印结果我们可以看出,AttributeSet将布局文件xml下的属性全部打印出来了,细心的童鞋可能已经看出来:


xml文件:
app:view_ref="@dimen/dp_15"

打印结果:
name:view_ref value:@2131034213

这个属性我们设置的是一个整数尺寸,可最后打印出来的是资源编号。


那如果我们想要输出我们设置的整数尺寸,需要怎么操作呢?


这个时候就该我们这篇的主角出场了,使用TypedArray方式。



  • 使用TypedArray方式


(1)、这里我们需要将attrs.xml使用“declare-styleable”标签进行改造,如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="TestStyleable">
<attr name="view_int" format="integer" />
<attr name="view_str" format="string" />
<attr name="view_bool" format="boolean" />
<attr name="view_color" format="color" />
<attr name="view_ref" format="reference" />
<attr name="view_float" format="float" />
<attr name="view_dim" format="dimension" />
<attr name="view_frac" format="fraction" />

<attr name="view_enum">
<enum name="num_one" value="1" />
<enum name="num_two" value="2" />
<enum name="num_three" value="3" />
<enum name="num_four" value="4" />
</attr>

<attr name="view_flag">
<flag name="top" value="0x1" />
<flag name="left" value="0x2" />
<flag name="right" value="0x3" />
<flag name="bottom" value="0x4" />
</attr>
</declare-styleable>

</resources>

从改造后的attrs文件可以看出,我们将属性声明归结到TestStyleable里面,也就意味着这些属性是属于TestStyleable下的。


(2)、属性的解析:


public class TestAttrsView extends View {
private final String TAG = "TestAttrsView:";

public TestAttrsView(Context context) {
this(context, null);
}

public TestAttrsView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public TestAttrsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

//最原始使用方式
/* for (int i = 0; i < attrs.getAttributeCount(); i++) {
Log.i(TAG, "name:" + attrs.getAttributeName(i) + " value:" + attrs.getAttributeValue(i));
}*/


//使用TypeArray方式
//R.styleable.TestStyleable 指的是想要解析的属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TestStyleable);

int integerView = typedArray.getInt(R.styleable.TestStyleable_view_int, 0);
Log.i(TAG, "name:view_int" + " value:" + integerView);

boolean aBooleanView = typedArray.getBoolean(R.styleable.TestStyleable_view_bool, false);
Log.i(TAG, "name:view_bool" + " value:" + aBooleanView);

int colorView = typedArray.getColor(R.styleable.TestStyleable_view_color, Color.WHITE);
Log.i(TAG, "name:view_color" + " value:" + colorView);

String stringView = typedArray.getString(R.styleable.TestStyleable_view_str);
Log.i(TAG, "name:view_str" + " value:" + stringView);

float refView = typedArray.getDimension(R.styleable.TestStyleable_view_ref, 0);
Log.i(TAG, "name:view_ref" + " value:" + refView);

float aFloatView = typedArray.getFloat(R.styleable.TestStyleable_view_float, 0);
Log.i(TAG, "name:view_float" + " value:" + aFloatView);

float dimensionView = typedArray.getDimension(R.styleable.TestStyleable_view_dim, 0);
Log.i(TAG, "name:view_dim" + " value:" + dimensionView);

float fractionView = typedArray.getFraction(R.styleable.TestStyleable_view_frac, 1, 1, 0);
Log.i(TAG, "name:view_frac" + " value:" + fractionView);

//typedArray存放在缓存池,使用完需要释放缓存池
typedArray.recycle();
}
}

这里我直接打印出解析结果,这里可以获取我们想要的自定义属性,而系统有的属性可以忽略。


(3)、运行结果如下


在这里插入图片描述


从解析的结果可以看出,尺寸的结果已经转换为实际值了:


xml文件:
app:view_ref="@dimen/dp_15"

打印结果:
name:view_ref value:41.25

这个时候有童鞋又问了,我设置的是15dp,为啥最后打印是41.25了呢?其实解析出来的值单位是px,所以这里输出的是转换后的值。


解析的过程中用到了这个方法:


context.obtainStyledAttributes(attrs, R.styleable.TestStyleable);

我们来看一下这个方法的源码:


   public final TypedArray obtainStyledAttributes(
@Nullable AttributeSet set, @NonNull @StyleableRes int[] attrs) {
return getTheme().obtainStyledAttributes(set, attrs, 0, 0);
}

源码中我们可以看到这个方法有两个参数:


AttributeSet set:表示当前xml声明的属性集合

int[] attrs:表示你想挑选的属性,你想得到哪些属性,你就可以将其写到这个int数组中


obtainStyledAttributes方法返回值类型为TypedArray。该类型记录了获取到的属性值集合,而通过数组下标索引即可找到对应的属性值。索引下标通过R.styleable.TestStyleable_xx获取,“xx"表示属性名,一般命名为"styleable名” + “_” + “属性名”。


而TypedArray提供了各种Api,如getInteger,getString,getDimension等方法来获取属性值,这些方法都需要传入对应属性名在obtainStyledAttributes中的int数组的位置索引,通过下标获取数组里属性值。


这个TypedArray的作用就是资源的映射作用,把自定义属性在xml设置值映射到class,这样怎么获取都很简单啦。


到这里就分析完啦!

收起阅读 »

想要进阶高级开发?快看画布的基础使用

【Android 自定义控件】2.画布的基础使用 1.设置画布的背景颜色 2.画圆形 基本语法 参数说明 3.画直线 单条直线: 基本语法 ...
继续阅读 »





1.设置画布的背景颜色


void  drawColor(int color)
void drawARGB(int a, int r, int g, int b)
void drawRGB(int r, int g, int b)

2.画圆形


//画笔
Paint paint=new Paint() ;
paint.setColor(OxFFFFOOOO );
paint.setStyle(Paint.Style.FILLANDSTROKE);
paint.setStrokeWidth(50);

//画布(画圆形)
canvas.drawCircle(l90, 200, 150, paint);

基本语法


void drawCircle (float cx, float cy, float radius, Paint paint)


参数说明


cx:圆心的x坐标。
cy:圆心的y坐标。
radius:圆的半径。
paint:绘制时所使用的画笔。


3.画直线


单条直线:


//画笔
Paint paint=new Paint() ;
paint.setColor(OxFFFFOOOO );
paint.setStrokeWidth(50);

//画布(画直线)
canvas.drawLine(100, 100, 200, 200, paint);

基本语法


void drawLine (float startX, float startY, float stopX, float stopY , Paint paint)


参数说明


startX:起始点 坐标。
startY:起始点 坐标
stopX:终点 坐标。
stopY:终点 坐标。
paint:绘制时所使用的画笔。


多条直线:


Paint paint = new Paint();
paint.setColor(color.RED);
paint.setStrokeWidth(5);

float []pts={10,10,100, 100, 200, 200,400,400};
canvas.drawLines(pts, 2,4,paint); //表示从pts 数组中索引为2的数字开始绘图,有4个数值参与绘图,也就是点(100,100)和(200,200),所以效果图就是这两个点的连线。

基本语法


void drawLines(float[] pts,Paint paint)
void drawLines(float [ ] pts,int offset, int count,Paint paint)


参数说明


pts:点的集合,pts的组织方式为{x1,y1,x2,y2,x3,y3,…}。
offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
count:参与绘制的数值个数,指pts数组中数值的个数,而不是点的个数,因为一个点有两个数值。
paint:绘制时所使用的画笔。


4.画点


单个点


//画笔
Paint paint=new Paint() ;
paint.setColor(OxFFFFOOOO );
paint.setStrokeWidth(50);

//画布(画点)
canvas.drawPoint(100, 100, paint);

基本语法


void drawPoint(float x, float y, Paint paint)


参数说明


x:点的X坐标。
y:点的Y坐标。
paint:绘制时所使用的画笔。


多个点


Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(25);

float[] pts = {10,10,100,100,200,200,400,400};
canvas.drawPoints(pts, 2, 4, paint); //4个点:(10,10)、(100,100)、(200,200)和(400,400),在 drawPoints()函数里跳过前两个数值,即第一个点的横、纵坐标,画出后面4个数值代表的点,即第二、三个点,第四个点没画。

基本语法


void drawPoints (float [] pts,Paint paint)
void drawPoints(float[ ] pts,int offset,int count,Paint paint)


参数说明


pts:点的合集,与上面的直线一致,样式为{x1,y1,x2,y2,x3,y3,…}。
offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
count:参与绘制的数值个数,指pts数组中数值的个数,而不是点的个数。
paint:绘制时所使用的画笔。


5.画矩形


区别:


RectF 所保存的数值类型是 float 类型
Rect 所保存的数值类型是 int 类型


构造矩形的两种方法:


//方法一 直接构造
Rect rect = new Rect(10, 10, 100, 100);
//方法二 间接构造
Rect rect = new Rect();
rect.set(10, 10, 100, 100);

绘制矩形:


Paint paint = new Paint(); 
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(15);

//直接构造
canvas.drawRect(10, 10, 100, 100, paint);

//使用 RectF 构造
RectF rect = new RectF(210f, 10f, 300f, 100f);
canvas.drawRect(rect, paint);

6.画圆角矩形


Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Style.FILL);
paint.setStrokeWidth(15);

RectF rect = new RectF(100,10,300,100);
canvas.drawRoundRect(rect,20,10, paint);

基本语法


void drawRoundRect (RectF rect, float rx, float ry,Paint paint)


参数说明


rect:要绘制的矩形。
rx:生成圆角的椭圆的X轴半径。
ry:生成圆角的椭圆的Y轴半径。
paint:绘制时所使用的画笔。


7.画椭圆


Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);

RectF rect = new RectF(100,10,300,100);
canvas.drawRect(rect, paint);

canvas.drawOval(rect, paint);//根据同一个矩形画椭圆

基本语法


void drawOval(RectF oval, Paint paint)


参数说明


oval:用来生成椭圆的矩形。

收起阅读 »

40+场面试,100%通过率,我想分享的14条经验

作者 | 陈同学       责编 | 欧阳姝黎 这里是陈同学,首先来一个简单的自我介绍,和个人的经历分享吧。 我本科和硕士均就读于哈工大,是今年1月毕业。 我经历过3个专业,大一 船舶工程;大二-大四 车辆...
继续阅读 »

作者 | 陈同学       责编 | 欧阳姝黎


这里是陈同学,首先来一个简单的自我介绍,和个人的经历分享吧。


我本科和硕士均就读于哈工大,是今年1月毕业。


我经历过3个专业,大一 船舶工程;大二-大四 车辆工程;研一-研三 机械电子工程。


我拿过5个国家级竞赛的奖项 ,我在研究生期间从0开始,在1年时间,自学操作系统,计算机网络 ,C++,数据结构等等,累计学习30+本书,500+博客文章,100+小时的网课,30w+字的资料。


在实习阶段斩获了腾讯、阿里、华为等大厂的实习offer,在正式秋招阶段收割10+offer,包括但不限于


腾讯sp 

字节sp

阿里云 sp 

BAT大满贯

还有华为15a(应届生最高定级)

中兴蓝剑计划(应届生最高定级)

深信服大牛批(应届生最高定级)

vivo sp等等

均是40w+的总包

最高总包约50w

同时保持了一个在4月份以后

面试通过率100%的记录

今天和大家来分享一下,我从40+场面试中总结出来的14个应该避免的面试大坑。


我认为想面试互联网技术岗, 特别是像我一样的非科班同学,应该特别关注。


这些坑都是从我自身的经历以及从身边的同学的经历中总结出来的,我踩过的坑希望大家不要踩




No1.对简历上的每一个字负责


很多同学,包括我以前会犯的一个误区就是, 追求简历上技能点多多益善。


不论是不是自己真正掌握的 ,只要是接触过某个技术,都罗列在简历上。甚至有些技能点, 还蜜汁自信的写上“精通”但是面试官一深问, 就不会了。这就犯了写简历时候的一个大忌。


我们要对简历上的每一个字负责, 每一个写在简历上的技术点都应该是我们烂熟于心,经得起面试官深入追问的。


具体来说就是要避开下面两个坑



  1. 技术栈不要贪多把写上去的每一个点深入掌握就好。你在简历上写的内容相当于给面试官划定了一个出题范围。面试的时候面试官并不会特意的刁难你。他们主要还是会从你简历上写好的那些技术点去考你。好好对着自己写的简历一行一行看一遍,这都是你挖的坑。同时谨慎的使用熟练精通这些字眼 。


  2. 在描述项目的时候,不要过分夸张,比如把整个团队的活写成你一个人做的。言过其实,很容易会在面试中露馅,简历可以美化包装,但是过分夸张。





No2.技术宽度决定了你是否能够进入一家公司,


技术深度则决定了你offer的等级


对于互联网技术岗的主要问的东西有这样几块计算机学科基础+项目经历+刷题。这3块也就是整个面试的核心了,然后对于不同细分的技术岗位下对于这三块有不同的考察方向比如:



  • 对于开发岗可能考察的就是像操作系统计算机网络等等方面的知识


  • 对于算法岗考察的就是机器学习深度学习等等方面的内容



所以我觉得只有你先对应岗位必问的那些知识掌握,也就是先cover住技术宽度,才是拿到offer的前提。


在此基础上 如果你能在某一方面比较有优势,比如某一些知识领域比较精通或者做的项目比较有优势或者有大厂实习也就是技术深度达到了这样才能有更好的offer等级。


关于怎么提升宽度和深度,其实说真的短期内宽度是好补的,深度确实要看个人,是代价较高。


所以我的建议是,先把宽度提上来,把你能cover的知识点及原理搞懂是第一步。建议对自己之前的项目和技术积累做一个总结和分类。


然后对已经了解的方面尽可能延伸,对盲区或是一些面试重点考察的地方进行针对性的学习和练习。




No3.如果走技术研发岗,学历、成绩、奖学金 


学生组织活动都不会是决定性的因素


因为面试中只考查计算机基础+刷题+项目,只有在最后的HR面的时候才会问一下你的在校的一些经历、奖学金等等。


当然如果你如果前面的技术面都通过的话,最后的HR面其实问题不大,就算没有太多的学生组织经历、太高的绩点、各种奖学金等等。HR面大概率还是会通过的。


只有你的技术水平才是决定性因素像学历、绩点、奖学金等等这些东西只是一个锦上添花。


如果你的技术很拉跨,一个技术问题都回答不上来,我觉得算是清北,面试官也是大概率不会让你通过的。


互联网算是对学历最宽容的行业之一毕竟程序员是一个技术密集型工种。


学校的作用是抬高找工作的下限,很多大厂会认为一个出身名校的同学的基本功是扎实的,因此会很乐于接纳这样的同学。但是指望名校光环提高自己的上限是不切实际的。


有很多同学会因为自己是双非学校,感到自卑,不敢投递大厂会显得有点畏手畏脚。但我觉得我们完全没有必要妄自菲薄


说实话,我自己本科专业也和计算机一点不搭边,在面试的时候也和面试官提到这个问题,但面试官给我的答复是只要有能力,没有人会看你的学校或者专业。




No4.心态作为一个很重要的因子存在,


还是会对最后的结果有挺大影响的


这里给大家列出一个公式 是我在某一个帖子看到的


  offer = 心态 * (实力 + 面试技巧) + 运气

实力就是咱们刚才所说的 计算机基础+刷题+项目


秋招对大部分人来说都是一场难熬的经历,会有各种压力源的存在,真的很容易让我们心态崩溃



  • 可能有的同学开始准备的时间比较晚,快开始秋招了才开始准备,总暗示自己说什么时间不多了,怎么每天过这么快效率怎么这么低。


  • 到笔试了,跟自己说这个算法太难了,肯定做不出来;


  • 面试过程中面试官问的东西好多都不会怎么办?


  • 面完了又收到拒信,这次面试又凉凉了。


  • 周围的XX大佬又收割一个offer了 我还没上岸 太菜了 怎么办



不管是面试前、面试中、面试后的结果 已经周围环境 peer pressure 等等都牵动着我们的神经。


所以这里给大家提供几个调节心态的小建议



  • 要正视自己的能力,不轻视,不高估

    不轻视指的是我们都要对自己有信心,机会那么多,千千万万的初中创公司,各种拥有垂直领域稳定份额的二三线公司甚至有些已经上市,除此之外还有银行,投资,金融的IT岗

    不高估就是要清楚自己的能力范围,过高的期望会让你的心理变得脆弱,稍有不顺心态就有崩掉的趋势。因为面试毕竟有太大的偶然性,就算你达到了一定的水平,相应水平的岗位也不是百发百中的


  • 遭受到各种拒绝时,一定要沉得住气,坚信一切都会是最好的安排

    在确保自身没有问题(学习方法、知识积累或自身定位)的情况下,坚持下去,这个时候你差的就是一点点运气,该来的总会来。面试过程不要紧张,尤其是前几次,建议先从小公司入手锻炼下面试经验


  • 心态实在太差的时候反而要停下你重复而没有效率的工作,去调整一下,可以出去玩一玩,吃吃喝喝


  • 面试过程漫长:适当放松,面试很搞人心态的

    过了简历面等 一面 一面过了等二面 二面过了等三面。互联网面试流程少则三面 多则五六七八面。持续时间少则是、一个礼拜 多则一两个月 。在这个过程中建议大家专注于过程 不要太在意结果


  • 面试准备过程中 和周围同学多交流 不比较
    主要是要找一个能力和你差不多的同学,最好不要找那种比你强太多的,当你看到别人已经收割很多offer了自己还颗粒无收的时候 容易被搞心态,会怀疑人生。当然也不排除有些人拿到offer后在朋友圈装X、散布焦虑情绪,这种我建议屏蔽或拉黑,同时也希望大家拿到offer后能低调一些,以己度人。求职过程中别和身边的人对比 ,别自我怀疑,专注于过程,别在意结果,反思总结,心态别崩





No5.学会平等交流,别把自己身段放的太低


面试是个双选的过程,他可以拒绝你,你也可以拒绝他。回答的时候不用表现的太卑微,反而会影响自己正常的表达和逻辑,不卑不亢就行。


心态也放稳一点,大胆一点,duck不必害怕,互联网技术岗的面试不会像其他行业 其他岗位比如快销,地产等等那样子会在意你的仪表,谈吐等等,他在意的就是面试官问你的技术会不会。


和面试官谈笑风生就行了,而且1面面试官可能只比我们大几岁,如果进去了还是你mentor呢。




No6.回答问题的时候要有层次感 循序渐进


不要一口气把知道的全部说完,然后还毫无条理。学会一个知识点由浅入深讲解给面试官,并且留有余地给他进一步去问。


一个简单的基础问题可以一步一步有条理有层次的回答,每一层表达完抛个引子,让面试官可以继续问下去,这也算是一个引导的技巧,从而让面试官真正了解你的掌握的深度。




No7.如果真的被问到不会的,就直接说你不会


每个程序员都不是全能的大神,总会有知识漏洞,更何况是我们这些应届生所以面试中碰到不会的问题很正常。


不要觉得自己某个问题到不上来,这场面试就注定凉凉了,坦诚的告诉面试官自己不会,或者礼貌地说这方面可能我还要多学习。


对一个拿不准的问题千万不要猜,即使是二选一的那种问题,猜错了直接完蛋,猜对了被人看出来,再往深问还是完蛋。


另外,像可能,大概是,我觉得这种表达最好不要,一听就是对一个点没把握,有可能会让面试官觉得学习太浮躁不喜欢寻求原理。


那对于自己知道原理(确实是理解了的)但是没用过的东西,就讲讲原理,并承认自己实践不足,表现出好学的态度。


面试一定要真诚。不熟直接说不会,更多的展示自己擅长的一面,千万不懂装懂。




No8.手撕代码题的时候主动的和面试官交流


一般每一轮面试的最后一part保留节目是手撕代码。


关于手撕代码部分,不能面试官出完题,就一个人闷头在那里写。


因为面试官是会代入实际工作时的情景的,如果你写题的时侯和他一点交流也没有,那万一把你招进去了以后对需求交接的时侯是不是也是这样的状态?


这个也是我在面试的时侯听面试官提的意见。




No9.思路比答对题目更重要,题不会没关系,


你要体现你的解题思路和能力


当然纯概念不会就是不会,别瞎说。


这里更多的是比如一些开放性的题目,比如说,手撕代码题,项目中的一些优化 一些系统设计题、智力题。


面试官不一定非得要求有一个标准答案呢,主要是想看看你能不能主动的去拆解问题、主动思考,以及和面试官的交流。


这也是面试中考察的很重要的一部分,就是你解决问题的能力。


对于这种问题,还是要多打开思路,多结合自己已经学过的一些技术点进行思考。


自己能够先给出一个简单的方案,再一步一步的优化,到一个相对合理的方案 这样的回答面试官会非常喜欢。




No10.最好把每场面试录音,记录面经,反思总结


在电话面或者视频面的时候 ,最好利用手机的录音功能把每一场面试录下来哦 这样方便自己的复盘 。


发现自己那些模块比较薄弱,查漏补缺, 反思总结, 针对面试中出现的问题下次不要再出现。




No11.在面试中介绍项目的面试时候,


项目的一些描述要提前准备,而不是临场去组织语言


很多同学在面试中描述项目的时候,都是临场发挥,临场去组织语言。这样会往往会导致你在介绍的时候,不流畅不连贯 ,导致面试官抓不住你的重点。


也就会让它认为你的表达有问题,或者你的项目吗没有太核心能吸引他的东西,所以建议大家专门给自己做的项目整理一个类似演讲稿的稿子。


把项目的流程、项目的背景、项目碰到的问题。自己用到的方案,项目的亮点难点改进点,后续的优化方向等等都写在这个稿子上。


在每次面试前过一遍,这样的在面试中直接按照稿子上的描述去说就行。


面试官其实对你的项目业务流程不感兴趣,更感兴趣的是你项目中



  • 自己解决的问题,


  • 所采用的方案,


  • 为什么采用这个方案,


  • 有没有更好的方案,


  • 你的方案和别人的方案的对比,


  • 你的思考在哪里,


  • 你的难点亮点创新点,


  • 以及在项目中所涉到的技术点的一些提问,



这里面最好可以涉及一些数据,比如数据量、响应速度等等来量化的表达。




No12.把握好反问环节


面试官最后一般会问你你有什么想问我的,这个其实就是反问环节。


这个其实是面试官想了解你对公司的一个关注度或者对自身发展的一个关注度。


所以大家可以从这些角度去问新人培养机制?进去以后负责哪些业务?学习建议?


表现出自己的好学求知,以及对公司的关注 这也能看出你对工作的一个诚意,以及对发展的一个预期。


最好不要去问那些比如 “我什么时候会有下一面 ” “我刚刚面的怎么样这种话题”。




No13. HR面的时候 看起来像聊人生 


实际是在考察你的价值观


到HR面的时候就不会在有技术问题了,而是一些看起来无关痛痒的聊生活聊兴趣。


比如,家里人都是干嘛的,有没有女朋友,有没有什么兴趣爱好,有没有拿到别的offer,为什么会来我们呢公司等等。


其实这些问题看起来都很无足轻重,实际上是想看看你的稳定性,是不是适合公司的氛围,是不是接受公司的文化等等。


比如,是不是会因为家里条件好,吃不了苦,加不了班,会不会女朋友异地,过几年就会离职跳槽,稳定性差,会不会有更好的offer放弃这家等等。


所以大家在HR面的时候要摸清楚HR真正想考察你的指标是什么避免跳坑里就行了。


对于互联网技术岗来讲 通过了前面的3、4轮的技术面 一般问题都不大,HR面只要不是回答得得太离谱,offer八成是可以到手的




No14.不要把鸡蛋都放在一个篮子里


这句话的意思是, 尽量多拿几个offer,不要只拿一个offer就躺平了,不要把赌注都压在一个offer上。


因为互联网的秋招一般是面试通过了,先发两方,然后过两个月左右到11月份再谈薪资。


如果你最后只拿了一个offer,然后那个公司又只给你开了一个白菜价你就血亏了,都没有别的选择。


尽量多拿一些offer。事实证明,部分企业会根据你手里offer的情况来定薪资,还有一点,万一后面提前去实习发现不太合适,想违约跑路 没有别的offer在手,根本没有选择。


hr们会养备胎,你也可以多拿几个offer ,算是给自己多养几个备胎,抵抗风险。


收起阅读 »

震惊:从头开发一个RPC是种怎样的体验?

RPC
对于开发人员来说,调用远程服务就像是调用本地服务一样便捷。尤其是在微服务盛行的今天,了解RPC的原理过程是十分有必要的。 作者 | Alex Ellis       译者 | 弯月 出品 | CSDN(ID:CS...
继续阅读 »


对于开发人员来说,调用远程服务就像是调用本地服务一样便捷。尤其是在微服务盛行的今天,了解RPC的原理过程是十分有必要的。


作者 | Alex Ellis       译者 | 弯月


出品 | CSDN(ID:CSDNnews)


以下为译文:


计算机之间的通信方式多种多样,其中最常用的一种方法是远程过程调用(Remote Procedure Call,即RPC)。该协议允许一台计算机调用另一个计算机上的程序,就像调用本地程序一样,并负责所有传输和通信。


假设我们需要在一台计算机上编写一些数学程序,并且有一个判断数字是否为质数的程序或函数。在使用这个函数的时候,我们只需传递数字进去,就可以获得答案。这个函数保存在我们的计算机上。



很多时候,程序保存在本地非常方便调用,而且由于这些程序与我们其余的代码在一起,因此调用的时候几乎不会产生延迟。


但是,在有些情况下,将这些程序保留在本地也不见得是好事。有时,我们需要在拥有大量核心和内存的计算机上运行这些程序,这样它就可以检查非常大的数字。但这也不是什么难事,我们可以将主程序也放到大型计算机上运行,即使其余的程序可能并没有这种需求,质数查找函数也可以自由利用计算机上的资源。如果我们想让其他程序重用质数查找函数,该怎么办?我们可以将其转换成一个库,然后在各个程序之间共享,但是每一台运行质数查找库的计算机,都需要大量的内存资源。


如果我们将质数查找函数单独放在一台计算机上,然后在需要检查数字时与该计算机对话,怎么样呢?如此一来,我们就只需提高质数查找函数所在的计算机的性能,而且其他计算机上程序也可以共享这个函数。



这种方式的缺点是更加复杂。计算机可能会出现故障,网络也有可能出问题,而且我们还需要担心数据的来回传递。如果你只想编写一个简单的数学程序,那么可能无需担心网络状况,也不用考虑如何重新发送丢失的数据包,甚至不用担心如何查找运行质数查找函数的计算机。如果你的工作是编写最佳质数查找程序,那么你可能并不关心如何监听请求或检查已关闭的套接字。


这时就可以考虑远程过程调用。我们可以将计算机间通信的复杂性包装起来,然后在通信的任意一侧建立一个简单的接口(stub)。对于编写数学程序的人来说,看上去就像在调用同一台计算机上的函数;而对于编写质数查找程序的人来说,看上去就像是自己的函数被调用了。如果我们将中间部分抽象化,那么两侧都可以专心做好自己的细节,同时仍然可以享受将计算拆分到多台计算机的优势。



RPC调用的主要工作就是处理中间部分。它的一部分必须存在数学程序的计算机上,负责接受并打包参数,然后发送到另一台计算机。此外,在收到响应后,还需要解析响应,并传递回去。而质数查找函数计算机则必须等待请求,解析参数,然后将其传递给函数,此外,还需要获取结果,将其打包,然后再返回结果。这里的关键之处是数学程序和质数查找程序间,以及它们的stub之间都有一个清晰的接口。



更多详细信息,请参见 Andrew D. Birrell和Bruce Jay Nelson1 于1981年发表的论文《Implementing Remote Procedure Calls》。



从头编写RPC


下面,我们来试试看能不能编写一个RPC。


首先,我们来编写基本的数学程序。为了简单起见,我们编写一个命令行工具,接受输入,然后检查是否为质数。它有一个单独的方法is_prime,处理实际的检查。


// basic_math_program.c
#include <stdio.h>
#include <stdbool.h>


// Basic prime checker. This uses the 6k+-1 optimization
// (see https://en.wikipedia.org/wiki/Primality_test)
bool is_prime(int number) {
// Check first for 2 or 3
if (number == 2 || number == 3) {
return true;
}
// Check for 1 or easy modulos
if (number == 1 || number % 2 == 0 || number % 3 == 0) {
return false;
}
// Now check all the numbers up to sqrt(number)
int i = 5;
while (i * i <= number) {
// If we've found something (or something + 2) that divides it evenly, it's not
// prime.
if (number % i == 0 || number % (i + 2) == 0) {
return false;
}
i += 6;
}
return true;
}


int main(void) {
// Prompt the user to enter a number.
printf("Please enter a number: ");
// Read the user's number. Assume they're entering a valid number.
int input_number;
scanf("%d", &input_number);


// Check if it's prime
if (is_prime(input_number)) {
printf("%d is prime\n", input_number);
} else {
printf("%d is not prime\n", input_number);
}


return 0;
}

这段代码有一些潜在的问题,我们没有处理极端情况。但这里只是为了说明,无伤大雅。



目前一切顺利。下面,我们将代码拆分成多个文件,is_prime 可供同一台计算机上的程序重用。首先,我们为 is_prime 创建一个单独的库:


// is_prime.h
#ifndef IS_PRIME_H
#define IS_PRIME_H


#include <stdbool.h>


bool is_prime(int number);


#endif

// is_prime.c
#include "is_prime.h"


// Basic prime checker. This uses the 6k+-1 optimization
// (see https://en.wikipedia.org/wiki/Primality_test)
bool is_prime(int number) {
// Check first for 2 or 3
if (number == 2 || number == 3) {
return true;
}
// Check for 1 or easy modulos
if (number == 1 || number % 2 == 0 || number % 3 == 0) {
return false;
}
// Now check all the numbers up to sqrt(number)
int i = 5;
while (i * i <= number) {
// If we've found something (or something + 2) that divides it evenly, it's not
// prime.
if (number % i == 0 || number % (i + 2) == 0) {
return false;
}
i += 6;
}
return true;
}

下面,从主程序中调用:


// basic_math_program_refactored.c
#include <stdio.h>
#include <stdbool.h>


#include "is_prime.h"


int main(void) {
// Prompt the user to enter a number.
printf("Please enter a number: ");
// Read the user's number. Assume they're entering a valid number.
int input_number;
scanf("%d", &input_number);


// Check if it's prime
if (is_prime(input_number)) {
printf("%d is prime\n", input_number);
} else {
printf("%d is not prime\n", input_number);
}


return 0;
}

再试试,运行正常!当然,你也可以加一些测试:



下面,我们需要将这个函数放到其他计算机上。我们需要编写的功能包括:



  • 调用程序的 stub:




  • 打包参数


  • 传输参数


  • 接受结果


  • 解析结果




  • 被调用的 stub:




  • 接受参数


  • 解析参数


  • 调用函数


  • 打包结果


  • 传输结果



我们的示例非常简单,因为我们只需要打包并发送一个 int 参数,然后接收一个字节的结果。对于调用程序的库,我们需要打包数据、创建套接字、连接到主机(暂定 localhost)、发送数据、等待结果、解析,然后返回。调用程序库的头文件如下所示:


// client/is_prime_rpc_client.h
#ifndef IS_PRIME_RPC_CLIENT_H
#define IS_PRIME_RPC_CLIENT_H


#include <stdbool.h>


bool is_prime_rpc(int number);


#endif

可能有些读者已经发现了,实际上这个接口与上面的函数库一模一样,但关键就在于此!因为调用程序只需要关注业务逻辑,无需关心其他一切。但实现就稍复杂:


// client/is_prime_rpc_client.c


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>


#define SERVERPORT "5005" // The port the server will be listening on.
#define SERVER "localhost" // Assume localhost for now


#include "is_prime_rpc_client.h"


// Packs an int. We need to convert it from host order to network order.
int pack(int input) {
return htons(input);
}


// Gets the IPv4 or IPv6 sockaddr.
void *get_in_addr(struct sockaddr *sa) {
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
} else {
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
}


// Gets a socket to connect with.
int get_socket() {
int sockfd;
struct addrinfo hints, *server_info, *p;
int number_of_bytes;


memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM; // We want to use TCP to ensure it gets there
int return_value = getaddrinfo(SERVER, SERVERPORT, &hints, &server_info);
if (return_value != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(return_value));
exit(1);
}


// We end up with a linked-list of addresses, and we want to connect to the
// first one we can
for (p = server_info; p != NULL; p = p->ai_next) {
// Try to make a socket with this one.
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
// Something went wrong getting this socket, so we can try the next one.
perror("client: socket");
continue;
}
// Try to connect to that socket.
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
// If something went wrong connecting to this socket, we can close it and
// move on to the next one.
close(sockfd);
perror("client: connect");
continue;
}


// If we've made it this far, we have a valid socket and can stop iterating
// through.
break;
}


// If we haven't gotten a valid sockaddr here, that means we can't connect.
if (p == NULL) {
fprintf(stderr, "client: failed to connect\n");
exit(2);
}


// Otherwise, we're good.
return sockfd;
}


// Client side library for the is_prime RPC.
bool is_prime_rpc(int number) {


// First, we need to pack the data, ensuring that it's sent across the
// network in the right format.
int packed_number = pack(number);


// Now, we can grab a socket we can use to connect see how we can connect
int sockfd = get_socket();


// Send just the packed number.
if (send(sockfd, &packed_number, sizeof packed_number, 0) == -1) {
perror("send");
close(sockfd);
exit(0);
}


// Now, wait to receive the answer.
int buf[1]; // Just receiving a single byte back that represents a boolean.
int bytes_received = recv(sockfd, &buf, 1, 0);
if (bytes_received == -1) {
perror("recv");
exit(1);
}


// Since we just have the one byte, we don't really need to do anything while
// unpacking it, since one byte in reverse order is still just a byte.
bool result = buf[0];


// All done! Close the socket and return the result.
close(sockfd);
return result;
}

如前所述,这段代码需要打包参数、连接到服务器、发送数据、接收数据、解析,并返回结果。我们的示例相对很简单,因为我们只需要确保数字的字节顺序符合网络字节顺序。


接下来,我们需要在服务器上运行被调用的库。它需要调用我们前面编写的 is_prime 库:


// server/is_prime_rpc_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "is_prime.h"
#define SERVERPORT "5005" // The port the server will be listening on.
// Gets the IPv4 or IPv6 sockaddr.
void *get_in_addr(struct sockaddr *sa) {
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
} else {
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
}
// Unpacks an int. We need to convert it from network order to our host order.
int unpack(int packed_input) {
return ntohs(packed_input);
}
// Gets a socket to listen with.
int get_and_bind_socket() {
int sockfd;
struct addrinfo hints, *server_info, *p;
int number_of_bytes;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM; // We want to use TCP to ensure it gets there
hints.ai_flags = AI_PASSIVE; // Just use the server's IP.
int return_value = getaddrinfo(NULL, SERVERPORT, &hints, &server_info);
if (return_value != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(return_value));
exit(1);
}
// We end up with a linked-list of addresses, and we want to connect to the
// first one we can
for (p = server_info; p != NULL; p = p->ai_next) {
// Try to make a socket with this one.
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
// Something went wrong getting this socket, so we can try the next one.
perror("server: socket");
continue;
}
// We want to be able to reuse this, so we can set the socket option.
int yes = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}
// Try to bind that socket.
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
// If something went wrong binding this socket, we can close it and
// move on to the next one.
close(sockfd);
perror("server: bind");
continue;
}
// If we've made it this far, we have a valid socket and can stop iterating
// through.
break;
}
// If we haven't gotten a valid sockaddr here, that means we can't connect.
if (p == NULL) {
fprintf(stderr, "server: failed to bind\n");
exit(2);
}
// Otherwise, we're good.
return sockfd;
}
int main(void) {
int sockfd = get_and_bind_socket();
// We want to listen forever on this socket
if (listen(sockfd, /*backlog=*/1) == -1) {
perror("listen");
exit(1);
}
printf("Server waiting for connections.\n");
struct sockaddr their_addr; // Address information of the client
socklen_t sin_size;
int new_fd;
while(1) {
sin_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
if (new_fd == -1) {
perror("accept");
continue;
}
// Once we've accepted an incoming request, we can read from it into a buffer.
int buffer;
int bytes_received = recv(new_fd, &buffer, sizeof buffer, 0);
if (bytes_received == -1) {
perror("recv");
continue;
}
// We need to unpack the received data.
int number = unpack(buffer);
printf("Received a request: is %d prime?\n", number);
// Now, we can finally call the is_prime library!
bool number_is_prime = is_prime(number);
printf("Sending response: %s\n", number_is_prime ? "true" : "false");
// Note that we don't have to pack a single byte.
// We can now send it back.
if (send(new_fd, &number_is_prime, sizeof number_is_prime, 0) == -1) {
perror("send");
}
close(new_fd);
}
}

最后,我们更新一下我们的主函数,使用新的RPC库调用:


// client/basic_math_program_distributed.c
#include <stdio.h>
#include <stdbool.h>
#include "is_prime_rpc_client.h"
int main(void) {
// Prompt the user to enter a number.
printf("Please enter a number: ");
// Read the user's number. Assume they're entering a valid number.
int input_number;
scanf("%d", &input_number);
// Check if it's prime, but now via the RPC library
if (is_prime_rpc(input_number)) {
printf("%d is prime\n", input_number);
} else {
printf("%d is not prime\n", input_number);
}
return 0;
}

这个 RPC 实际的运行情况如下:


现在运行服务器,就可以运行客户端将质数检查的工作分布到其他计算机上运行!现在,程序调用 is_prime_rpc 时,所有网络业务都在后台进行。我们已经成功分发了计算,客户端实际上是在远程调用程序。



示例有待改进的方面


本文中的实现只是一个示例,虽然实现了一些功能,但只是一个玩具。真正的框架(例如 gRPC3)要复杂得多。我们的实现需要改进的方面包括:



  • 可发现性:在上述示例中,我们我们假定服务器在 localhost 上运行。RPC 库怎么知道将 RPC 发送到哪里呢?我们需要通过某种方式来发现可以处理此 RPC 调用的服务器在哪里。


  • RPC 的类型:我们的的服务器非常简单,只需处理一个 RPC 调用。如果我们希望服务器提供两个不同的RPC服务,比如 is_prime 和get_factors,那么该怎么办?我们需要一种方法来区分发送到服务器的两种请求。


  • 打包:打包整数很容易,打包一个字节更容易。如果我们需要发送一个复杂的数据结构,该怎么办?如果我们需要为了节省带宽而压缩数据,又该怎么办?


  • 自动生成代码:我们肯定不希望每次编写新的 RPC,都需要手动编写所有的打包和网络处理代码。理想情况下,我们只需定义一个接口,然后其余的接口都由计算机自动完成,并自动提供 stub。这里,我们需要考虑协议缓冲区等。


  • 多种语言:按照上面的思路,如果我们能够自动生成 stub,那么就可以考虑支持多种语言,如此一来,跨服务和跨语言的通信也只需调用一个函数。


  • 错误和超时处理:如果 RPC 失败怎么办?如果网络出现故障,服务器停止运行,wifi 掉线,该怎么办?我们需要考虑超时处理。


  • 版本控制:假设上述所有功能已全部实现,但你想修改某个正在多台计算机上运行的 RPC,那么该怎么办?


  • 其他有关服务器的注意事项:线程、阻塞、多路复用、安全性、加密、授权等等。



计算机科学就是要站在巨人的肩膀上,很多库已经为我们完成了大量工作。


原文链接:https://alexanderell.is/posts/rpc-from-scratch/


声明:本文由CSDN翻译,转载请注明来源。



收起阅读 »

Google和腾讯为什么都采用主干开发模式?

作者 | 黄国峰       责编 | 欧阳姝黎 摘要 本文介绍了两种常用的代码分支模式:特性分支开发模式、主干开发模式,分别阐述了其优缺点和适用环境;同时剖析了 Google 和腾讯采用主干开发模式的背景...
继续阅读 »

作者 | 黄国峰       责编 | 欧阳姝黎




摘要


本文介绍了两种常用的代码分支模式:特性分支开发模式、主干开发模式,分别阐述了其优缺点和适用环境;同时剖析了 Google 和腾讯采用主干开发模式的背景和决策因素,捎带分享了这2个巨头的实践,供读者在技术选型中参考。




背景


按之前的写作思路,本文应该叫《Google 工程效能三板斧之三:主干开发》,但我改变了主意,希望能同时提供国内互联网公司的实践,供读者参考,因此文章标题也随之更改。


软件开发过程中,开发人员通过版本管理工具对源码进行存储,追踪目录和文件的修改历史。为了区隔不同状态的源代码,会采用分支进行管理。不同的软件开发模式,对应着不同的分支模式。


软件业界常用的软件分支模式有多种,但本质上可以分为两类:



  • 主干开发模式(Trunk Based Development)


  • 特性分支开发模式(Feature Branch Development)





两种模式的定义及优缺点分析


特性分支开发模式


特性分支开发模式是指为一个或多个特定的需求/缺陷/任务创建代码分支(branch),在其上完成相应的开发(一般经过增量测试)后,把它合并(merge)到主干/集成分支的开发模式。


通常这种分支生命期会持续一段时间,从几天到几周不等,极少数情况甚至以月算。


特性分支开发模式中常用的有 Git-Flow 模式、Github-Flow 模式和 Gitlab-Flow 模式等。这些模式只有细节上的差异,以 Git-Flow为例:



优点:



  • 特性开发周期宽松:因为生命期可以较长,较大的需求特性可以在宽松的时间内完成再合入主干;


  • 分支测试的时间宽松:因为生命期可以较长,可以有较多时间对分支进行测试,甚至手工测试;



缺点:



  • 分支管理复杂:原因在于大量采用代码分支,且来源分支和合入目标分支各异,操作复杂 —— 以上图为例,可以从master(Tag 1.0.0) 拉出 hotfix 1.0.2 分支,然后合入到 develop 分支,开发阶段结束后合入到 release branches,发布后合入 master,非常复杂,很容易出错;


  • 合并冲突多、解决难:分支生命期越长,意味着与主干的代码差异越大,冲突概率越高,冲突的解决难度越大(甚至成为不可能);


  • 迭代速度慢:特性分支生命期长(数天至数周)意味着特性上线速度慢,相应的迭代速度也慢;


  • 需要较多测试环境:每个特性分支都需要分配至少1个测试环境,且长期占用(有状态);



适用环境:



  • 对版本迭代速度要求不高


  • 测试自动化程度低,或说主要靠人工测试的



主干开发模式


主干开发,是指开发人员直接向主干(习惯上主干分支通常为:trunk 或 master)提交/推送代码。通常,开发团队的成员1天至少1次地将代码提交到主干分支。在到达发布条件时,从主干拉出发布分支(通常为 release),用于发布。若发现缺陷,直接在主干上修复,并根据需要 cherry pick 到对应版本的发布分支。


流程:



优点:



  • 分支模型简单高效,开发人员易于掌握不容易出现错误操作


  • 避免了分支合并、冲突解决的困扰


  • 随时拥有可发布的版本


  • 有利于持续集成和持续交付



缺点:



  • 基础架构要求高:合入到主干的代码若质量不过关将直接阻塞整个团队的开发工作,因此需要高效的持续集成平台进行把关;


  • 自动化测试要求高:需有完备单元测试代码,确保在代码合入主干前能在获得快速和可靠的质量反馈;


  • 最好有代码评审:若代码质量要求高,需要配套代码评审(CR)机制,在代码提交到主干时,触发CR,通过 Peer Review 后才能正式合入;


  • 最好有特性开关:主干开发频发合入主干的情况下,特性拆分得很小,可能是半成品特性,需要配套特性开关(Feature Toggle),只有当特性整体开发完才通过灰度发布等手段逐步打开;



适用环境:



  • 对迭代速度要求高,希望需求快速交付上线


  • 基础架构强,持续集成工具高效;


  • 团队成员习惯TDD(测试驱动开发),代码自动化测试覆盖率高(至少增量代码的自动化测试覆盖率高);





为什么 Google 和腾讯采用主干开发模式


互联网巨头 Google 大部分业务开发都采用主干开发模式,国内巨头腾讯也在推行主干开发(试点业务团队大部分已经采用)。


他们采用主干开发的原因在于对主干开发的优点有强烈诉求,而且有能力和资源弥补其缺点:



  • 都是互联网企业,竞争激烈,因此对迭代速度要求高;


  • 基础架构能力强:都能自研强大的持续集成平台,Google 有自研的 Forge,腾讯有自研的蓝盾;


  • 自动化测试能力强:都推行TDD,强调开发负责质量,减少甚至取消手工测试人员(少量必要的手工测试转外包),自动化测试覆盖率高;


  • 都有严格的CR机制确保代码质量:Google 极其严苛的可读性认证(Readability)在业界已经是标杆,腾讯是国内少有正在采用类似实践的互联网企业。严格的代码可读性认证和根据此标准执行的严格代码评审制度,能有效的保证合入主干的代码质量不会降低。



主干开发的最大优点是:效率和质量,而这2者是软件和互联网企业的核心诉求。主干开发的缺点,巨头有能力和资源来填平这些坑。


因此,从ROI(Ratio of Investment)的角度来看,Google 和腾讯采用主干开发实属必然。




美中两巨头的实践


Google 在主干开发的实践


我们在之前的文章提到,Google 的工程效能(也叫研发效能)核心理念只有简单的3条:



  1. 使用单体代码仓库(参考:Google 工程效能三板斧之一:单体代码仓库


  2. 使用 Bazel 构建(参考:Google 工程效能三板斧之二:使用 Bazel 构建


  3. 主干开发;



其中的第3条,就是本文所述内容。


为了保证主干代码的质量,避免出现工程师合入到主干的代码 break 掉主干的情况,Google 采取了以下实践:



  • 代码合入事件触发通过持续集成,确保合入到主干的代码经过充分且必要测试;


  • 通过 Bazel 实现相关代码(指依赖变更代码的代码)的精准测试;


  • 至少 2 个合资格的 reviewer (代码评审人)的 LGTM(Look Good To Me),才允许代码合入主干;


  • 合资格的 reviewer 都是在 Google 内部通过 Readability (代码可读性)认证的员工;



腾讯在主干开发的实践


腾讯某 BG 在2018年开始的“930变革”后,在各试点团队推动主干开发(注:并未全公司普遍采用),具体的举措包括:



  1. 以度量牵引:通过对特性分支)的生命期监控和预警,实现非主干分支的生命期缩短,倒逼开发团队采用主干开发;


  2. 投大力气统一 BG 内的持续集成工具、开发自动化测试平台;


  3. 制定了 7 大编程语言的编码规范,并自研代码静态扫描工具;


  4. 并参考 Google 推行代码可读性(Readability)、可测试性(Testability)认证制度;


  5. 强力推行 CR (代码评审)制度,确保代码的可读性(命名、代码风格、设计、复杂度)。



效果:



  • 质量提升:代码质量从可测量的维度得到明显提升(代码规范率、单元测试覆盖率);


  • 迭代速度提升:试点团队的迭代周期从4周或2周提升至1周;


  • 代码从“私有”变“公有”:通过代码评审制度,提高了代码可读性,使代码从个人拥有(只有写代码的人能看懂),变成团队拥有(整个团队都能看懂);这一点对于企业非常重要,接手过别人代码的程序们都有感受;


  • 代码的自动化测试覆盖率提升明显,为未来的重构构筑了一张安全网;





中小企业能参考什么?


中小企业应该选择特性分支开发模式,还是主干开发模式?根据上文,相信大家已经足以自行判断。


有些中小企业的技术决策者非常认可持续集成/持续交付的理念,从而更希望采用主干开发,但对于主干开发的缺点(或说弥补缺点的成本)存在顾虑。


对此,我有如下建议:



  • 基础架构要求:可以考虑采用开源软件,如持续集成采用 Jenkins、Travis CI、Gitlab CI等,通过简单部署可以投入使用;同时配合代码静态分析工具(如 SonarQube、CheckStyle),确保代码基本质量过关;


  • 自动化测试要求:工具上不存在障碍,现代编程语言(如java、go、c++)都有内建或第三方的单元测试框架;难点只在于成员的开发习惯,可以通过测试覆盖率工具,以增量覆盖率指标保证新增代码都有完备的自动化测试,从而逐步改变团队的研发文化;


  • 代码评审要求:开源的Git服务器(如 Gitlab)基本都支持 push hook,配合开源的 Gerrit 等CR工具,可以实现在代码推送(push)或 pull request(合入请求)时触发1个代码评审请求,实现评审通过后,代码才正式合入的功能;剩下的就是研发文化问题了,需要在团队内部推行代码规范、代码可读性等宣导和教育工作;


  • 发布时的特性开关:如果要求不高,可以通过代码 hard code 一个常量作为特性开关;如果要求高,也有开源的特性开关(比如:unleash、piranha、flipper)工具可供选择。



参考上述建议,并充分认识到主干开发的成本和困难的情况下,中小企业开发团队也并非不可以考虑主干开发的实践。


收起阅读 »

我不是个黑客,但我就喜欢安全。快看如何开拓你的开发价值!!

安卓逆向4-使用AndroidKiller插入广告页文章目录 任务要求 1.安装配置AndroidKiller 2.反编译和拷贝替换 3.反编译 任务要求 利用Androidki...
继续阅读 »




安卓逆向4-使用AndroidKiller插入广告页

文章目录







任务要求


利用Androidkiller重新做一遍添加启动页作业。


1.安装配置AndroidKiller


下载v1.3.1压缩包,解压后运行AndroidKiller.exe,提示“未检测到Java SDK环境”。
此时打开app-debug.apk,软件提示“APK 反编译失败,无法继续下一步源码反编译!”。


配置Java环境:打开AK,选择左上角的“配置”-Java-安装路径,选择Java的bin目录。
再次打开apk文件,仍然提示“反编译失败”。


报错原因是软件自带的apktool版本过旧,点击软件左上角“Android”-APKTOOL管理器,添加本地的apktool。


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述




2.反编译和拷贝替换


??打开apk-debug.apk文件,软件会自动编译,效果如图。
"工程信息"模块包含app名称、包名、程序入口点,"工程管理"模块包含目录结构。
在这里插入图片描述
本次插入的广告页与上次作业相同,回顾一下需要插入哪些文件。
1.拷贝图片hands_make_dream.jpg:把要插入的图片拷贝到AndroidKiller_v1.3.1\projects\app-debug\Project\res\drawable目录下。


2.添加布局文件activity_advert.xml:把要插入的布局文件拷贝到AndroidKiller_v1.3.1\projects\app-debug\Project\res\layout目录下。
修改布局文件的编号:在AndroidKiller_v1.3.1\projects\app-debug\Project\smali\com\example\four目录下的R$layout.smali文件中,添加advert布局文件的自定义编号,如.field public static final text_view_without_line_height:I = 0x7f0b005f


3.添加广告页的smail文件:把广告页的两个文件advert.smaliadvert$1.smali拷贝到AndroidKiller_v1.3.1\projects\app-debug\Project\smali\com\example\four目录下。
修改smail文件编号:打开advert.smali文件,把文件编号0x7f09001c修改为编号0x7f0b005f。


4.修改包名:目标apk的包名是com.example.four,修改广告页的两个smail文件advert.smali和advert$1.smail,把原包名retwo_login全部替换为目标apk的包名信息four


5.修改仓库文件:在AndroidKiller_v1.3.1\projects\app-debug\Project目录下找到AndroidManifest.xml文件,修改activity信息,修改后的activity信息如下。


	<activity android:name="com.example.four.MainActivity"></activity>
<activity android:name="com.example.four.advert">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>



3.反编译


??在AndroidKiller_v1.3.1中选择"Android"-“编译”,即可一键编译和签名。
在这里插入图片描述


安卓手机安装app-debug_killer.apk,运行效果如下。
首页映入眼帘的是插入的广告页文字和图片,停留5秒后跳到正常页面。


在这里插入图片描述
在这里插入图片描述

收起阅读 »

别问我为啥用这个来扫二维码!做开发的都在用

zxing基本使用 官方提供了zxing在Android机子上的使用例子,https://github.com/zxing/zxing/tree/master/android,作为官方的例子,zxing-android考虑了各种各样的情况,包括多种解析格式、...
继续阅读 »

zxing基本使用


官方提供了zxing在Android机子上的使用例子,https://github.com/zxing/zxing/tree/master/android,作为官方的例子,zxing-android考虑了各种各样的情况,包括多种解析格式、解析得到的结果分类、长时间无活动自动销毁机制等。有时候我们需要根据自己的情况定制使用需求,因此会精简官方给的例子。在项目中,我们仅仅用来实现扫描二维码和识别图片二维码两个功能。为了实现高精度的二维码识别,在zxing原有项目的基础上,本文做了大量改进,使得二维码识别的效率有所提升。先来看看工程的项目结构。









1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── QrCodeActivity.java
├── camera
│   ├── AutoFocusCallback.java
│   ├── CameraConfigurationManager.java
│   ├── CameraManager.java
│   └── PreviewCallback.java
├── decode
│   ├── CaptureActivityHandler.java
│   ├── DecodeHandler.java
│   ├── DecodeImageCallback.java
│   ├── DecodeImageThread.java
│   ├── DecodeManager.java
│   ├── DecodeThread.java
│   ├── FinishListener.java
│   └── InactivityTimer.java
├── utils
│   ├── QrUtils.java
│   └── ScreenUtils.java
└── view
└── QrCodeFinderView.java

源码比较简单,这里不做过多地讲解,大部分方法都有注释。主要分为几大块,



  • camera


主要实现相机的配置和管理,相机自动聚焦功能,以及相机成像回调(通过byte[]数组返回实际的数据)。



  • decode


图片解析相关类。通过相机扫描二维码和解析图片使用两套逻辑。前者对实时性要求比较高,后者对解析结果要求较高,因此采用不同的配置。相机扫描主要在DecodeHandler里通过串行的方式解析,图片识别主要通过线程DecodeImageThread异步调用返回回调的结果。FinishListenerInactivityTimer用来控制长时间无活动时自动销毁创建的Activity,避免耗电。



  • utils


图片二维码解析工具类,以及获取屏幕宽高的工具类。



  • view


这个包里只有一个类QrCodeFinderView,官方原本是使用这个类绘制扫描区域框,并且必须在扫描区域里才能识别二维码。我把这个类稍作修改,仅仅用来展示扫描区域,实际在相机扫描二维码的时候,只要在SurfaceView区域范围内,结果都是有效的。



  • QrCodeActivity


启动类,包含相机扫描二维码以及选择图片入口。


zxing源码存在的问题及解决方案


zxing项目源码实现了基本的二维码扫描及图片识别程序,但下载过源码并直接运行的童鞋都知道,例子存在很多的问题,包括基本的识别精准度不高、扫描区域小、部分手机存在预览图形拉伸、默认横向扫描、还有自定义扫描界面困难等问题。


资源下载:zxing

收起阅读 »

不会管理日志,还做什么开发?

Logger 基本用法 简介 Simple, pretty and powerful logger for android 为Android提供的,简单、强大而且格式美观的工具 本质就是封装系统提供的Log类,加上一些分割线易于查找不同的Log;...
继续阅读 »


Logger 基本用法


简介




Simple, pretty and powerful logger for android
为Android提供的,简单、强大而且格式美观的工具


本质就是封装系统提供的Log类,加上一些分割线易于查找不同的Log;logcat中显示的信息可配置。最初的样子如下图



包含线程信息、Log所在的类、方法及所在行数。


这里我忍不住了,就先写了我最喜欢的功能,嘎嘎嘎~~
最最最基本的依赖和简单打印在第二页


我觉得最好的功能是:Logger支持设置日志保存到本地,这样的话就可以想上传上传了。做自己的日志管理系统倍爽!
不过日志保存的位置写死了。找位置的方法是
在Logger包里的 DiskLogAdapter类的构造函数,进入build()方法里。


public DiskLogAdapter() {
formatStrategy = CsvFormatStrategy.newBuilder().build();
}

进入build(),就可以找到相应的路径


@NonNull public CsvFormatStrategy build() {
if (date == null) {
date = new Date();
}
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.UK);
}
if (logStrategy == null) {
//地址在这里
String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String folder = diskPath + File.separatorChar + "logger";
HandlerThread ht = new HandlerThread("AndroidFileLogger." + folder);
ht.start();
Handler handler = new DiskLogStrategy.WriteHandler(ht.getLooper(), folder, MAX_BYTES);
logStrategy = new DiskLogStrategy(handler);
}
return new CsvFormatStrategy(this);
}

具体路径是:/storage/emulated/0/logger
每个文件最大为500K,源码贴出来~~~~
private static final int MAX_BYTES = 500 * 1024; // 500K averages to a 4000 lines per file


生成的文件名称为logs_0.csv 后面的数字会递增 源码贴出来~~~~
newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));




刚用markDown不太会用




我最喜欢的部分写完了,下面写点常规的操作吧。simple


一、依赖Logger


地址:https://github.com/orhanobut/logger
本来不想贴地址呢,github是个好东西,介于我两天前还不会用github,还是贴上吧,啦啦啦,我是莉莉的小叮当
github上介绍很详细了,但是我还是想粘贴一遍。


依赖


dependencies {
implementation 'com.orhanobut:logger:2.2.0'
}

初始化


Logger.addLogAdapter(new AndroidLogAdapter());

到这里Logger已经可以用了
Logger.d(“debug”);
Logger.e(“error”);
Logger.w(“warning”);
Logger.v(“verbose”);
Logger.i(“information”);
Logger.wtf(“What a Terrible Failure”);


下面写点我自己的Logger用法


    val formatStrategy = PrettyFormatStrategy.newBuilder()
.showThreadInfo(true) //(可选)是否显示线程信息。 默认值为true
.methodCount(1) // (可选)要显示的方法行数。 默认2
.methodOffset(5) // (可选)隐藏内部方法调用到偏移量。 默认5
.tag("doShare")//(可选)每个日志的全局标记。 默认PRETTY_LOGGER
.build()
Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))//根据上面的格式设置logger相应的适配器
Logger.addLogAdapter(DiskLogAdapter())//保存到文件


资源下载:logger-master.zip


收起阅读 »

昨天我被开了,技术总监说:不会Arouter做什么架构师

ARouter,A framework for assisting in the renovation of Android componentization (帮助 Android App 进行组件化改造的路由框架) —— 支持模块间的路由、通信、解耦 官...
继续阅读 »


ARouter,A framework for assisting in the renovation of Android componentization (帮助 Android App 进行组件化改造的路由框架) —— 支持模块间的路由、通信、解耦


官方中文介绍:
https://github.com/alibaba/ARouter/blob/master/README_CN.md
(中文比英文文档,详尽得多…,良心文档啊)


基本使用


1.添加依赖

android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
//注解处理器需要的模块名,作为路径映射的前缀
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}

dependencies {
implementation 'com.alibaba:arouter-api:1.4.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2' //注解处理器,会将注解编译成Java类
}

2.添加注解

//注意:路劲至少两级,即xx/xx,前一个xx用于分组
@Route(path = "/test/second")
public class SecondActivity extends AppCompatActivity {
}

3.初始化SDK

一般在Application中初始化


ARouter.init(this);

4.使用

//很简单,一句话完成,可携带参数
ARouter.getInstance().build("/test/second").navigation();

原理浅析


从ARouter.getInstance().build("/test/second").navigation();出发,解释其跳转基本过程。
先上一张时序图:
在这里插入图片描述


1.ARouter.build(path)构建Postcard

ARouter只是对外统一的api接口,实现基本由_ARouter完成,所以构建Postcard也是由_ARouter,构建,build(path, extractGroup(path))中extractGroup方法,就是把/xx/xx中前面的xx转换为默认group的方法,这也是之前必须使用2级以上目录的原因。build到此就完成了,此时还没有完成映射到activity的任务,只是把path浅析了下。


2.Postcard.navigation()实现跳转

最后也是由_ARouter完成,在_ARouter.navigation时,首先调用LogisticsCenter.completion(postcard)完善postcard的信息,而completion方法,则完成了path到activity的转换关系。完善后,再调用_navigation完成最终跳转。


3.LogisticsCenter.completion(postcard)将path映射到activity

核心部分:


public synchronized static void completion(Postcard postcard) {
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) {
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
if (null == groupMeta) {
throw new NoRouteFoundException();
} else {
// Load route and cache it into memory, then delete from metas.
try {
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup());
} catch (Exception e) {
throw new HandlerException();
}

completion(postcard); // Reload
}
} else {
postcard.setDestination(routeMeta.getDestination()); //destination就是需要跳转的Activity.class
......
}
}

首先去Warehouse的routes中寻找RouteMeta(路由元数据)。
Warehouse可以理解为存储路由元数据的容器,包括:路由关系、拦截器、provider的映射关系等。RouteMeta既持有activity等对应跳转类信息。
首次navigation时,RouteMeta == null,故会用postcard build时的group path先找到对一个的IRouteGroup信息[IRouteGroup何时加载到Warehouse的,见下条],然后通过iGroupInstance.loadInto将改分组下的RouteMeta都加载到缓存中,这可以理解为延迟加载,降低初始化时的一些压力。加载后,再重新调用completion(postcard)。


4.IRouteGroup的加载

路由元数据RouteMeta是从实现了IRouteGroup接口的实例中load进来的,这个实现了IRouteGroup接口的类在哪?何时load进Warehouse?在LogisticsCenter.init()方法中,找到了踪迹。


public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
// These class was generated by arouter-compiler.
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}

PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}

for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
}
......
}
}

首次init时,通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)将ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes"下的所有类,都加载进routerMap中。然后通过区分routerMap类名,实例化com.alibaba.android.arouter.routes.ARouter##Root开头的类[由于csdn对$的支持并不友好,故以下$都使用#代替],调用其loadInto将对应的IRouteGroup类都加载到Warehouse.groupsIndex中。
PS:
init方法有一些小细节,针对debug版本或者新版本,才会有一次完整的类find和load的过程,加载完后会将类路径都存入sp,之后启动从sp拿,以增加启动速度。
ClassUtils.getFileNameByPackageName方法并不简单,其中牵扯了在多dex或者特殊rom下加载类的一些处理,有兴趣的同学可以阅读源码了解。


ARouter##Root##app


public class ARouter$$Root$$app implements IRouteRoot {
public ARouter$$Root$$app() {
}

public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", test.class);
}
}

ARouter##Group##test


public class ARouter$$Group$$test implements IRouteGroup {
public ARouter$$Group$$test() {
}

public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/second", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/test/second", "test", (Map)null, -1, -2147483648));
}
}

5.Route类的生成

接着上一步的解析,下面要了解的是,ARouter##Root##app,ARouter##Group##test这些编译好的类,是何时/如何生成的。这里使用的是android的apt(Annotation Processing Tool)技术,即在编译时,将注解生成为Java代码。所以在build.gradle添加依赖时,会添加注解处理器 annotationProcessor ‘com.alibaba:arouter-compiler:1.2.2’ ,具体的处理过程,可以参见arouter-compiler的RouteProcessor,RouteProcessor代码较长,这里就不详述了。感兴趣的童鞋,可以自己写一写注解处理器,com.squareup.javapoet神器了解一下。


资源下载:arouter-develop.zip

收起阅读 »

recycleview适配器,不看后悔到35岁

在我最近开发的一个Android项目当中,用到列表的地方非常多。用RecyclerView+BaseRecyclerViewAdapterHelper(开源框架)可以帮我们节省大量的代码(约节省三分之二),RecyclerView不用多说大家非常熟悉,谷歌推荐...
继续阅读 »


在我最近开发的一个Android项目当中,用到列表的地方非常多。用RecyclerView+BaseRecyclerViewAdapterHelper(开源框架)可以帮我们节省大量的代码(约节省三分之二),RecyclerView不用多说大家非常熟悉,谷歌推荐的列表控件,代替了传统的ListView,更加强大和灵活。BaseRecyclerViewAdapterHelper是一个非常强大的开源框架,它基本上可以解决我们开发中的列表布局。在这里记录一下这个框架。


框架引入


先在项目的 build.gradle中的 repositories 添加


allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

然后在Module的 build.gradle中的 dependencies 添加


	dependencies {
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.47'
}

基本使用


首先我们在Activity中有一个RecyclerView



android:layout_width="match_parent"
android:layout_height="match_parent">

android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"/>



再新建一个item布局,item布局是一个简单的头像和名字



android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginVertical="5dp">

android:id="@+id/iv_head"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_gravity="center_vertical"
android:src="@mipmap/ic_launcher"/>

android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_gravity="center_vertical"
android:text="张三"
android:textSize="18sp"
android:textColor="#000000"/>



再根据item所需数据,编写数据实体类型


public class User {
private String headUrl;
private String name;

public User(String headUrl, String name) {
this.headUrl = headUrl;
this.name = name;
}

public String getHeadUrl() {
return headUrl;
}

public void setHeadUrl(String headUrl) {
this.headUrl = headUrl;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

接下来就要用到我们的BaseRecyclerViewAdapterHelper框架来编写适配器


public class UserAdapter extends BaseQuickAdapter {
public UserAdapter(int layoutResId, @Nullable List data) {
super(layoutResId, data);
}

@Override
protected void convert(BaseViewHolder helper, User item) {
Glide.with(mContext).load(item.getHeadUrl()).into((ImageView)helper.getView(R.id.iv_head));
helper.setText(R.id.tv_name, item.getName());
}
}

在这里我们用到图片加载框架,非常的好用,一行代码就可以加载url图片等,这里就不详细多说,GitHub地址:https://github.com/bumptech/glide


最后一步就是在我们的Activity使用该适配器


public class MainActivity extends AppCompatActivity {
private RecyclerView recycler;

private List userList;

private UserAdapter adapter;

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

recycler = findViewById(R.id.recycler);

//模拟数据,实际开发中一般是从后台获取数据
userList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
userList.add(new User("http://img2.imgtn.bdimg.com/it/u=3749323882,846155126&fm=26&gp=0.jpg",
"第" + i + "条"));
}

//设置布局管理
recycler.setLayoutManager(new LinearLayoutManager(this));
//创建适配器
adapter = new UserAdapter(R.layout.item_recycler, userList);
//给RecyclerView设置适配器
recycler.setAdapter(adapter);
}
}

这样就是RecyclerView+BaseRecyclerViewAdapterHelper的基本使用,效果如下
在这里插入图片描述


点击事件


使用列表那当然也少不了点击事件,不论是整个item的点击事件还是item中的子控件都可以实现。


item的点击事件


		adapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
Toast.makeText(MainActivity.this, "点击了第" + position + "条", Toast.LENGTH_SHORT).show();
}
});

item的长按事件


		adapter.setOnItemLongClickListener(new BaseQuickAdapter.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(BaseQuickAdapter adapter, View view, int position) {
Toast.makeText(MainActivity.this, "长按了第" + position + "条", Toast.LENGTH_SHORT).show();
return false;
}
});

item中子控件的点击事件
首先在适配器中绑定子控件


public class UserAdapter extends BaseQuickAdapter {
public UserAdapter(int layoutResId, @Nullable List data) {
super(layoutResId, data);
}

@Override
protected void convert(BaseViewHolder helper, User item) {
Glide.with(mContext).load(item.getHeadUrl()).into((ImageView)helper.getView(R.id.iv_head));
helper.setText(R.id.tv_name, item.getName());
//绑定点击事件
helper.addOnClickListener(R.id.iv_head);
helper.addOnClickListener(R.id.tv_name);
}
}

接着在activity中就可以监听到子控件的点击事件,根据view.getId()来区分点击了哪一个子控件


		adapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
switch (view.getId()) {
case R.id.iv_head:
Toast.makeText(MainActivity.this, "点击了第" + position + "的头像",
Toast.LENGTH_SHORT).show();
break;
case R.id.tv_name:
Toast.makeText(MainActivity.this, "点击了第" + position + "的名字",
Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
});

子控件的长按事件也是如此。


常用方法


上面介绍了基本的使用方法,还有很多常用的方法这里列举一下:



































方法名 说明
getData() 获取适配器中的数据
addData(data) 向列表中添加数据(可一条,可多条)
setData(position, data) 修改指定位置的数据
setNewData(List) 设置适配器新的数据
notifyDataSetChanged() 刷新适配器
remove(position) 删除指定数据

BaseRecyclerViewAdapterHelper还有很多其他功能,例如列表加载动画、下拉刷新、上拉加载、添加分组、自定义不同的item、拖拽item等,这里就不一一列举出来,有兴趣的可以去官方GitHub了解更多,地址如下:https://github.com/CymChad/BaseRecyclerViewAdapterHelper


资源下载:BaseRecyclerViewAdapterHelper-master (1).zip

收起阅读 »

Android开发者你好~你还在用dialog????

 https://github.com/li-xiaojun/XPopup/ 内置几种了常用的弹窗,十几种良好的动画,将弹窗和动画的自定义设计的极其简单;目前还没有出现XPopup实现不了的弹窗效果。 内置弹窗允许你使用项目已有的布局,同时还能...
继续阅读 »

 https://github.com/li-xiaojun/XPopup/



  • 内置几种了常用的弹窗,十几种良好的动画,将弹窗和动画的自定义设计的极其简单;目前还没有出现XPopup实现不了的弹窗效果。 内置弹窗允许你使用项目已有的布局,同时还能用上XPopup提供的动画,交互和逻辑封装。

  • UI动画简洁,遵循Material Design,在设计动画的时候考虑了很多细节,过渡,层级的变化

  • 交互优雅,实现了优雅的手势交互,智能的嵌套滚动,智能的输入法交互,具体看Demo

  • 适配全面屏,目前适配了小米,华为,谷歌,OPPO,VIVO,三星,魅族,一加全系全面屏手机

  • 自动监听Activity生命周期,自动释放资源。在Activity直接finish的场景也避免了内存泄漏

  • 很好的易用性,所有的自定义弹窗只需继承对应的类,实现你的布局,然后像Activity那样,在onCreate方法写逻辑即可

  • 性能优异,动画流畅;精心优化的动画,让你很难遇到卡顿场景

  • 能在应用后台弹出(需要申请悬浮窗权限,一行代码即可)

  • 支持androidx

  • 完美支持RTL布局

  • 如果你想要时间选择器和城市选择器,可以使用XPopup扩展功能库XPopupExt: https://github.com/li-xiaojun/XPopupExt

  • 设计思路: 综合常见的弹窗场景,我将其分为几类:


  • Center类型,就是在中间弹出的弹窗,比如确认和取消弹窗,Loading弹窗

  • Bottom类型,就是从页面底部弹出,比如从底部弹出的分享窗体,知乎的从底部弹出的评论列表,内部已经处理好手势拖拽和嵌套滚动

  • Attach类型,就是弹窗的位置需要依附于某个View或者某个触摸点,就像系统的PopupMenu效果一样,但PopupMenu的自定义性很差,淘宝的商品列表筛选的下拉弹窗,微信的朋友圈点赞弹窗都是这种。

  • Drawer类型,就是从窗体的坐边或者右边弹出,并支持手势拖拽;好处是与界面解耦,可以在任何界面实现DrawerLayout效果

  • ImageViewer大图浏览类型,就像掘金那样的图片浏览弹窗,带有良好的拖拽交互体验,内部嵌入了改良的PhotoView

  • FullScreen类型,全屏弹窗,看起来和Activity一样,可以设置任意的动画器;适合用来实现登录,选择性的界面效果。

  • Position自由定位弹窗,弹窗是自由的,你可放在屏幕左上角,右下角,或者任意地方,结合强大的动画器,可以实现各种效果。


 


implementation 'com.lxj:xpopup:2.0.0'

底部弹窗,自定义布局


new XPopup.Builder(this)
.asCustom(new RefundPopup(this, new RefundPopup.OnClickListener() {
@Override
public void clickConfirm() {
new XPopup.Builder(GoodsOrderDetailActivity.this)
.asCustom(new RefundReasonPopup(GoodsOrderDetailActivity.this, new RefundReasonPopup.OnClickListener() {
@Override
public void clickConfirm(String tag,String msg) {
getPresenter().applyRefund(goodsOrderId,tag,msg);
}
})).show();
}
})).show();

 



public class RefundPopup extends BottomPopupView {

private Context context;
private OnClickListener mOnClickListener;

public RefundPopup(@NonNull Context context) {
super(context);
this.context = context;
}

public RefundPopup(@NonNull Context context, OnClickListener onClickListener) {
super(context);
this.context = context;
mOnClickListener = onClickListener;
}

@Override
protected int getImplLayoutId() {
return R.layout.popup_refund;
}

protected int getPopupWidth() {
return AutoUtils.getPercentWidthSize(750);
}

@Override
protected void onCreate() {
super.onCreate();
ImageView ivBack = findViewById(R.id.ivBack);
TextView tvConfirm = findViewById(R.id.tvConfirm);
TextView tvCancel = findViewById(R.id.tvCancel);
ivBack.setOnClickListener(view -> {
dismiss();
});
tvCancel.setOnClickListener(view -> dismiss());
tvConfirm.setOnClickListener(view -> {
mOnClickListener.clickConfirm();
dismiss();
});
}

public interface OnClickListener {
void clickConfirm();
}

}

资源下载:xpopup-master.zip


收起阅读 »

快来为你的照片添加个性标签吧!

1. 前言 PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析: 使用用预览布局SurfaceView,在不局上方使用控件的方式来进行设计,最后通过截图的方式将画面进行保存。 使用图片添加水印的方式来完成。 ...
继续阅读 »

1. 前言


需求图.png


PS:最近在项目执行过程中有这样一个需求,要求拍完照的图片必须达到以上的效果。需求分析:




  1. 使用用预览布局SurfaceView,在不局上方使用控件的方式来进行设计,最后通过截图的方式将画面进行保存。




  2. 使用图片添加水印的方式来完成。




2. 方法1 使用SurfaceView


我心想这不简单吗?于是开始一顿balabala的操作,结果到最后一步时发现,SurfaceView居然不能进行截图,截图下来的图片居然是一张黑色的。简单地说这是因为SurfaceView的特性决定的,我们知道安卓中唯一可以在子线程中进行绘制的view就只有Surfaceview了。他可以独立于子线程中绘制,不会导致主线程的卡顿,至于造成surfaceView黑屏的原因,可以移步这里
Android视图SurfaceView的实现原理分析。如果非要使用此方式时还是有三种思路来进行解决:
采用三种思路:


1. 获取源头视频的截图作为SurfaceView的截图
2. 获取SurfaceView的画布canvas,将canvas保存成Bitmap
3. 直接截取整个屏幕,然后在截图SurfaceView位置的图
复制代码

但是我觉得这种方式太过繁琐,所以选择用添加水印的式来完成。


3. 方法2 给拍照下来的图片添加水印


第一步:获取拍照权限


<!--相机权限-->
<uses-permission android:name="android.permission.CAMERA" />
<!--访问外部权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
复制代码

这里使用到郭霖大佬的开源库PermissionX获取权限:


PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
.onExplainRequestReason { scope, deniedList ->
val message = "需要您同意以下权限才能正常使用"
scope.showRequestReasonDialog(deniedList, message, "确定", "取消")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
openCamera()
} else {
Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}
复制代码

第二步:拍照


android 6.0以后,相机权限需要动态申请。


 // 申请相机权限的requestCode
private static final int PERMISSION_CAMERA_REQUEST_CODE = 0x00000012;

/**
* 检查权限并拍照。
* 调用相机前先检查权限。
*/
private void checkPermissionAndCamera() {
int hasCameraPermission = ContextCompat.checkSelfPermission(getApplication(),
Manifest.permission.CAMERA);
if (hasCameraPermission == PackageManager.PERMISSION_GRANTED) {
//有调起相机拍照。
openCamera();
} else {
//没有权限,申请权限。
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CAMERA},
PERMISSION_CAMERA_REQUEST_CODE);
}
}

/**
* 处理权限申请的回调。
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == PERMISSION_CAMERA_REQUEST_CODE) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//允许权限,有调起相机拍照。
openCamera();
} else {
//拒绝权限,弹出提示框。
Toast.makeText(this,"拍照权限被拒绝",Toast.LENGTH_LONG).show();
}
}
}
复制代码

调用相机进行拍照


申请权限后,就可以调起相机拍照了。调用相机只需要调用startActivityForResult传一个Intent就可以了,但是这个Intent需要传递一个uri,用于保存拍出来的图片,创建这个uri时,各个Android版本有所不同,需要进行版本兼容。


  //用于保存拍照图片的uri
private Uri mCameraUri;

// 用于保存图片的文件路径,Android 10以下使用图片路径访问图片
private String mCameraImagePath;

// 是否是Android 10以上手机
private boolean isAndroidQ = Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q;

/**
* 调起相机拍照
*/
private void openCamera() {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 判断是否有相机
if (captureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
Uri photoUri = null;

if (isAndroidQ) {
// 适配android 10
photoUri = createImageUri();
} else {
try {
photoFile = createImageFile();
} catch (IOException e) {
e.printStackTrace();
}

if (photoFile != null) {
mCameraImagePath = photoFile.getAbsolutePath();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri
photoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", photoFile);
} else {
photoUri = Uri.fromFile(photoFile);
}
}
}

mCameraUri = photoUri;
if (photoUri != null) {
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(captureIntent, CAMERA_REQUEST_CODE);
}
}
}

/**
* 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法
*/
private Uri createImageUri() {
String status = Environment.getExternalStorageState();
// 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
if (status.equals(Environment.MEDIA_MOUNTED)) {
return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
} else {
return getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());
}
}

/**
* 创建保存图片的文件
*/
private File createImageFile() throws IOException {
String imageName = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (!storageDir.exists()) {
storageDir.mkdir();
}
File tempFile = new File(storageDir, imageName);
if (!Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(tempFile))) {
return null;
}
return tempFile;
}
复制代码

接收拍照结果


  @Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
if (isAndroidQ) {
// Android 10 使用图片uri加载
ivPhoto.setImageURI(mCameraUri);
} else {
// 使用图片路径加载
ivPhoto.setImageBitmap(BitmapFactory.decodeFile(mCameraImagePath));
}
} else {
Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();
}
}
}
复制代码

注意:


这两需要说明一下,Android 10由于文件权限的关系,显示手机储存卡里的图片不能直接使用图片路径,需要使用图片uri加载。


另外虽然我在这里对Android 10和10以下的手机使用了不同的方式创建uri 和加载图片,但其实Android 10创建uri的方式和使用uri加载图片的方式在10以下的手机是同样适用的。
android 7.0需要配置文件共享。


<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
复制代码

在res目录下创建文件夹xml ,放置一个文件file_paths.xml(文件名可以随便取),配置需要共享的文件目录,也就是拍照图片保存的目录。


<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<!-- 这个是保存拍照图片的路径,必须配置。 -->
<external-files-path
name="images"
path="Pictures" />
</paths>
</resources>
复制代码

第三步:给拍照后得到的图片添加水印


  @Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
Bitmap mp;
if (isAndroidQ) {
// Android 10 使用图片uri加载
mp = MediaStore.Images.Media.getBitmap(this.contentResolver, t.uri);
} else {
// Android 10 以下使用图片路径加载
mp = BitmapFactory.decodeFile(uri);
}
//对图片添加水印 这里添加一张图片为示例:
ImageUtil.drawTextToLeftTop(this,mp,"示例文字",30,R.color.black,20,30)
} else {
Toast.makeText(this,"取消",Toast.LENGTH_LONG).show();
}
}
}
复制代码

这里使用到一个ImageUtil工具类,我在这里贴上。如果需要使用可以直接拿走~


public class ImageUtil {
/**
* 设置水印图片在左上角
*
* @param context 上下文
* @param src
* @param watermark
* @param paddingLeft
* @param paddingTop
* @return
*/
public static Bitmap createWaterMaskLeftTop(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {
return createWaterMaskBitmap(src, watermark,
dp2px(context, paddingLeft), dp2px(context, paddingTop));
}

private static Bitmap createWaterMaskBitmap(Bitmap src, Bitmap watermark, int paddingLeft, int paddingTop) {
if (src == null) {
return null;
}
int width = src.getWidth();
int height = src.getHeight();
//创建一个bitmap
Bitmap newb = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);// 创建一个新的和SRC长度宽度一样的位图
//将该图片作为画布
Canvas canvas = new Canvas(newb);
//在画布 0,0坐标上开始绘制原始图片
canvas.drawBitmap(src, 0, 0, null);
//在画布上绘制水印图片
canvas.drawBitmap(watermark, paddingLeft, paddingTop, null);
// 保存
canvas.save(Canvas.ALL_SAVE_FLAG);
// 存储
canvas.restore();
return newb;
}

/**
* 设置水印图片在右下角
*
* @param context 上下文
* @param src
* @param watermark
* @param paddingRight
* @param paddingBottom
* @return
*/
public static Bitmap createWaterMaskRightBottom(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingBottom) {
return createWaterMaskBitmap(src, watermark,
src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),
src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));
}

/**
* 设置水印图片到右上角
*
* @param context
* @param src
* @param watermark
* @param paddingRight
* @param paddingTop
* @return
*/
public static Bitmap createWaterMaskRightTop(Context context, Bitmap src, Bitmap watermark, int paddingRight, int paddingTop) {
return createWaterMaskBitmap(src, watermark,
src.getWidth() - watermark.getWidth() - dp2px(context, paddingRight),
dp2px(context, paddingTop));
}

/**
* 设置水印图片到左下角
*
* @param context
* @param src
* @param watermark
* @param paddingLeft
* @param paddingBottom
* @return
*/
public static Bitmap createWaterMaskLeftBottom(Context context, Bitmap src, Bitmap watermark, int paddingLeft, int paddingBottom) {
return createWaterMaskBitmap(src, watermark, dp2px(context, paddingLeft),
src.getHeight() - watermark.getHeight() - dp2px(context, paddingBottom));
}

/**
* 设置水印图片到中间
*
* @param src
* @param watermark
* @return
*/
public static Bitmap createWaterMaskCenter(Bitmap src, Bitmap watermark) {
return createWaterMaskBitmap(src, watermark,
(src.getWidth() - watermark.getWidth()) / 2,
(src.getHeight() - watermark.getHeight()) / 2);
}

/**
* 给图片添加文字到左上角
*
* @param context
* @param bitmap
* @param text
* @return
*/
public static Bitmap drawTextToLeftTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingTop) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setTextSize(dp2px(context, size));
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
return drawTextToBitmap(context, bitmap, text, paint, bounds,
dp2px(context, paddingLeft),
dp2px(context, paddingTop) + bounds.height());
}

/**
* 绘制文字到右下角
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @return
*/
public static Bitmap drawTextToRightBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingBottom) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setTextSize(dp2px(context, size));
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
return drawTextToBitmap(context, bitmap, text, paint, bounds,
bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),
bitmap.getHeight() - dp2px(context, paddingBottom));
}

/**
* 绘制文字到右上方
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @param paddingRight
* @param paddingTop
* @return
*/
public static Bitmap drawTextToRightTop(Context context, Bitmap bitmap, String text, int size, int color, int paddingRight, int paddingTop) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setTextSize(dp2px(context, size));
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
return drawTextToBitmap(context, bitmap, text, paint, bounds,
bitmap.getWidth() - bounds.width() - dp2px(context, paddingRight),
dp2px(context, paddingTop) + bounds.height());
}

/**
* 绘制文字到左下方
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @param paddingLeft
* @param paddingBottom
* @return
*/
public static Bitmap drawTextToLeftBottom(Context context, Bitmap bitmap, String text, int size, int color, int paddingLeft, int paddingBottom) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setTextSize(dp2px(context, size));
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
return drawTextToBitmap(context, bitmap, text, paint, bounds,
dp2px(context, paddingLeft),
bitmap.getHeight() - dp2px(context, paddingBottom));
}

/**
* 绘制文字到中间
*
* @param context
* @param bitmap
* @param text
* @param size
* @param color
* @return
*/
public static Bitmap drawTextToCenter(Context context, Bitmap bitmap, String text, int size, int color) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setTextSize(dp2px(context, size));
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
return drawTextToBitmap(context, bitmap, text, paint, bounds,
(bitmap.getWidth() - bounds.width()) / 2,
(bitmap.getHeight() + bounds.height()) / 2);
}

//图片上绘制文字
private static Bitmap drawTextToBitmap(Context context, Bitmap bitmap, String text, Paint paint, Rect bounds, int paddingLeft, int paddingTop) {
android.graphics.Bitmap.Config bitmapConfig = bitmap.getConfig();

paint.setDither(true); // 获取跟清晰的图像采样
paint.setFilterBitmap(true);// 过滤一些
if (bitmapConfig == null) {
bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;
}
bitmap = bitmap.copy(bitmapConfig, true);
Canvas canvas = new Canvas(bitmap);

canvas.drawText(text, paddingLeft, paddingTop, paint);
return bitmap;
}

/**
* 缩放图片
*
* @param src
* @param w
* @param h
* @return
*/
public static Bitmap scaleWithWH(Bitmap src, double w, double h) {
if (w == 0 || h == 0 || src == null) {
return src;
} else {
// 记录src的宽高
int width = src.getWidth();
int height = src.getHeight();
// 创建一个matrix容器
Matrix matrix = new Matrix();
// 计算缩放比例
float scaleWidth = (float) (w / width);
float scaleHeight = (float) (h / height);
// 开始缩放
matrix.postScale(scaleWidth, scaleHeight);
// 创建缩放后的图片
return Bitmap.createBitmap(src, 0, 0, width, height, matrix, true);
}
}

/**
* dip转pix
*
* @param context
* @param dp
* @return
*/
public static int dp2px(Context context, float dp) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
}

复制代码

4. 最终实现的效果如下:


效果.jpg


5.总结


整体来说没有什么太大的问题,添加水印的原理就是通过Canvas绘制的方式将文字/图片添加到图片上。最后再将修改之后的图片呈现给用户。同时也记录下SurfaceView截图黑屏的问题。


作者:LiChengZe_Blog
链接:https://juejin.cn/post/6960579316191068197
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

数据时代之非侵入式埋点方案

在发展日新月异的移动互联网时代,数据扮演着极其重要的角色。埋点作为一种最简单最直接的用户行为统计方式,能够全面精确的采集用户的使用习惯以及各功能点的迭代反馈等等,有了这些数据才能更好的驱动产品的决策设计和新业务场景的规划。本文旨在提出一种轻量级非侵入式的埋点方...
继续阅读 »

在发展日新月异的移动互联网时代,数据扮演着极其重要的角色。埋点作为一种最简单最直接的用户行为统计方式,能够全面精确的采集用户的使用习惯以及各功能点的迭代反馈等等,有了这些数据才能更好的驱动产品的决策设计和新业务场景的规划。本文旨在提出一种轻量级非侵入式的埋点方案,其主要有以下三方面优势

  • 支持动态下发埋点配置

  • 物理隔离埋点代码和业务代码

  • 插件式的埋点功能实现

该方案通过维护一个JSON文件来指定埋点所在的类和方法,继而利用AOP的方式在对应的类和方法执行时动态嵌入埋点代码。对于需要逻辑判断来确定埋点值的场景,提供hook方法的入参,以及所在类的属性值读取,根据相应的状态值设置不同的埋点

埋点配置

埋点配置JSON表中包含需要hook的类名class和具体的事件event信息,event中包括hook的方法和对应的埋点值。如下所示

{
"version": "0.1.0",
"tracking": [
{
"class": "RJMainViewController",
"event": {
"rj_main_tracking": [
"tripTypeViewChangedWithIndex:",
"tripLabClickWithLabKey:"
],
"user_fp_slide_click": "clickNavLeftBtn",
"user_fp_reflocate_click": "clickLocationBtn"
}
},
{
"class": "RJTripHistoryViewModel",
"event": {
"user_mytrip_show": "tableView:didSelectRowAtIndexPath:"
}
},
{
"class": "RJTripViewController",
"event": {
"rj_trip_tracking": "callServiceEvent"
}
}
]
}

简单来说就是本来埋点需要手动在该方法写入埋点代码来记录埋点值,现在通过AOP的方式物理隔离埋点代码和业务代码,避免埋点的逻辑侵入污染业务逻辑。埋点包括固定埋点和需要逻辑判断的场景化埋点,固定埋点如下所示

{
"class": "RJTripHistoryViewModel",
"event": {
"user_mytrip_show": "tableView:didSelectRowAtIndexPath:"
}
}

RJTripHistoryViewModel为类名,tableView:didSelectRowAtIndexPath:为需要hook的该类中的方法,而user_mytrip_show则是具体的埋点值,也就是当RJTripHistoryViewModel中的tableView:didSelectRowAtIndexPath:方法执行的时候记录埋点值user_mytrip_show

{
"class": "RJTripViewController",
"event": {
"rj_trip_tracking": "callServiceEvent"
}
},

对于场景化埋点,则需要提供一个impl类来提供相应的逻辑判断。比如上述配置表中的rj_trip_tracking为场景埋点的实现类,在该类中根据状态量返回对应的埋点值,即当callServiceEvent方法执行时会去找rj_trip_tracking这个埋点impl同名类,取该类返回的埋点值记录埋点。需要注意到是event中的key值既可以作为埋点值也可以作为impl的类名,埋点库会首先判断是否存在对应的类,存在即认为是impl实现类,从该类中取具体的埋点值。反之,则认为是固定埋点值

配置表中的类名和方法名需要对应,在hook的时候会去匹配,如果发现类中不存在对应的方法,则会自动触发断言

固定埋点

对于固定的埋点,只需要在对应的方法执行时直接记录埋点,利用Aspects来hook指定的类和方法,代码如下所示

[class aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
[events enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) {
NSLog(@"<RJEventTracking> - %@", ename);
}];
} error:&error];

为了便于检测无效的埋点,还需对hook的类和方法进行匹配校验,若类中没有对应的方法,则抛出断言

+ (void)checkValidWithClass:(NSString *)class method:(NSString *)method {
SEL sel = NSSelectorFromString(method);
Class c = NSClassFromString(class);
BOOL respond = [c respondsToSelector:sel] || [c instancesRespondToSelector:sel];
NSString *err = [NSString stringWithFormat:@"<RJEventTracking> - no specified method: %@ found on class: %@, please check", method, class];

NSAssert(respond, err);
}

场景埋点

场景化埋点主要为同一事件但是在多种状态或逻辑下不同埋点的情况,比如同是联系客服的操作,在各种订单类型以及订单状态下所设置的埋点是不同的。这个情况下,埋点库通过提供一个protocol由埋点impl类来实现,根据不同的逻辑判断,返回对应的埋点值

@protocol RJEventTracking <NSObject>

- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments;

@end

比如上文的rj_trip_tracking类需要遵循RJEventTracking协议,并根据相关逻辑判断返回对应的埋点值

埋点实现类的类名需要与埋点配置JSON中的event里的key保持一致,因为埋点库会通过检测是否有同名的类来实现插件式的埋点规则。另外,一个impl可以对应多个method方法

状态判断

根据状态量来确定埋点值。还是联系客服埋点的例子,根据订单种类和订单状态来返回对应的埋点值,首先定义JSON表中同名的impl类,并遵循RJEventTracking协议

#import "RJEventTracking.h"

NS_ASSUME_NONNULL_BEGIN

@interface rj_trip_tracking : NSObject <RJEventTracking>

@end

NS_ASSUME_NONNULL_END

在.m文件中实现自定义埋点的协议方法trackingMethod:instance:arguments:

#import "rj_trip_tracking.h"

@implementation rj_trip_tracking

- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
id dataManager = [instance property:@"dataManager"];
NSInteger orderStatus = [[dataManager property:@"orderStatus"] integerValue];
NSInteger orderType = [[dataManager property:@"orderType"] integerValue];

if ([method isEqualToString:@"callServiceEvent"]) {
if (orderType == 1) {
if (orderStatus == 1) {
return @"user_inbook_psgservice_click";
} else if (orderStatus == 2) {
return @"user_finishbook_psgservice_click";
}
} else {
return @"user_psgservice_click";
}
}
return nil;
}

@end

在协议方法中,可以获取当前的实例(在这个示例下为RJTripViewController)和入参数组。订单的类型和状态是存储在RJTripViewController中的dataManager属性中的,所以可以通过埋点库封装好的property:方法来获取属性值,并根据属性值返回对应的埋点名称

@interface NSObject (RJEventTracking)

- (id)property:(NSString *)property;

@end

属性值读取的实现为

- (id)property:(NSString *)property {
return [NSObject runMethodWithObject:self selector:property arguments:nil];
}

其中的原理很简单,就是将getter方法封装到NSInvocation中并invoke读取返回值即可

+ (id)runMethodWithObject:(id)object selector:(NSString *)selector arguments:(NSArray *)arguments {
if (!object) return nil;

if (arguments && [arguments isKindOfClass:NSArray.class] == NO) {
arguments = @[arguments];
}
SEL sel = NSSelectorFromString(selector);

NSMethodSignature *signature = [object methodSignatureForSelector:sel];
if (!signature) {
return nil;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = sel;
invocation.arguments = arguments;
[invocation invokeWithTarget:object];

return invocation.returnValue_obj;
}

入参判断

需要根据JSON中设置的所hook方法的入参来确定埋点名称的情况。比如在订单列表中点击全部,进行中,待支付,待评价,已完成等菜单项时分别埋点。被hook的方法为tripLabClickWithLabKey:其参数为UILabel,原先代码中通过Label的tag判断是点击的哪个子项,同样,我们也可以获取到Label的入参然后据此判断。由于参数只有一个,所以可以直接取arguments第一个值

#import "rj_main_tracking.h"
#import <UIKit/UIKit.h>

static NSString *order_types[5] = { @"user_order_all_click", @"user_order_ongoing_click",
@"user_order_unpay_click", @"user_order_unmark_click",
@"user_order_finish_click" };
@implementation rj_main_tracking

- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
if ([method isEqualToString:@"tripLabClickWithLabKey:"]) {
UILabel *label = arguments[0];
if (!label || label.tag > 4) {
return nil;
}
return order_types[label.tag];
} else if ([method isEqualToString:@"tripTypeViewChangedWithIndex:"]) {
return @"xx_ryan_jin";
}
}

@end

通过AOP来hook方法时,可以获取到当前hook方法所对应的实例对象和入参,在调用协议方法时,直接传给协议实现类

方法调用

和读取属性值类似,也是在不同场景下同一事件不同埋点名称的情况,但获取的状态量不是当前实例对象的,而是某个方法的返回值,这种情况下可以通过埋点库提供的方法调用函数来实现

@interface NSObject (RJEventTracking)

- (id)performSelector:(NSString *)selector arguments:(nullable NSArray *)arguments;

@end

比如获取某个页面的视图类型,而这个视图类型存储于单例对象中

[RJViewTypeModel sharedInstance].viewType

该场景下则根据viewType的类型,来返回相应的埋点名称

- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
NSString *labKey = [instance property:@"labKey"];
id viewTypeModel = [NSClassFromString(@"RJViewTypeModel") performSelector:@"sharedInstance"
arguments:nil];
NSInteger viewType = [[viewTypeModel property:@"viewType"] integerValue];

if (viewType == 0) {
if ([labKey isEqualToString:@"rj_view_begin_add"]) {
return @"user_fp_book_on_click";
}
if ([labKey isEqualToString:@"rj_view_end_add"]) {
return @"user_fp_book_off_click";
}
}
if (viewType == 1) {
if ([labKey isEqualToString:@"rj_view_begin_add"]) {
return @"user_fr_on_click";
}
if ([labKey isEqualToString:@"rj_view_end_add"]) {
return @"user_fr_off_click";
}
}
return nil;
}

逻辑判断

需要额外添加逻辑判断的场景,比如在订单详情页需要统计用户进入页面的查看行为,但是详情页的类型需要在网络请求后才能获取,而且该网络请求会定时触发,所以埋点hook的方法会走多次,该情况下,需要添加一个属性用来标记是否已记录埋点 。故而埋点库需要提供动态添加属性的功能

@interface NSObject (RJEventTracking)

- (id)extraProperty:(NSString *)property;

- (void)addExtraProperty:(NSString *)property defaultValue:(id)value;

@end

在埋点实现impl类里面,添加额外的属性来标记是否已记录过埋点

@implementation user_orderdetail_show

- (NSString *)trackingMethod:(NSString *)method instance:(id)instance arguments:(NSArray *)arguments {
if ([instance extraProperty:@"isRecorded"]) {
return nil;
}
[instance addExtraProperty:@"isRecorded" defaultValue:@(YES)];

return @"user_orderdetail_show";
}

@end

使用addExtraProperty:defaultValue:来给当前实例动态添加属性,而extraProperty:方法则用来获取实例的某个额外属性。如果isRecorded返回YES代表已经记录过该埋点,返回nil值来忽略该次埋点

上面示例中添加的isRecorded属性是因为埋点的需求,和业务逻辑无关,所以比较合理的方式是在埋点的插件impl类中添加,避免影响业务代码

埋点库动态添加属性的原理也很简单,利用runtime的objc_setAssociatedObject和objc_getAssociatedObject方法来绑定属性到实例对象

- (id)extraProperty:(NSString *)property {
return objc_getAssociatedObject(self, NSSelectorFromString(property));
}

- (void)addExtraProperty:(NSString *)property defaultValue:(id)value {
objc_setAssociatedObject(self, NSSelectorFromString(property), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

动态下发

埋点JSON配置表可以由服务器提供接口,客户端在每次启动时通过接口获取最新埋点配置表,从而达到动态下发的目的,客户端拿到JSON后,读取埋点信息并生效

[RJEventTracking loadConfiguration:[[NSBundle mainBundle] pathForResource:@"RJUserTracking" ofType:@"json"]];

读取的代码如下所示,主要逻辑为遍历埋点中的类和hook的方法,并检测是固定埋点还是场景化埋点,对于场景化埋点的情况查询是否有对应的埋点impl实现类。当然,还需检测JSON配置表的合法性,每个类和其中的方法是否匹配

+ (void)loadConfiguration:(NSString *)path {
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
return;
}
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
NSString *version = dict[@"version"];
NSArray *ts = dict[@"tracking"];
[ts enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) {
Class class = NSClassFromString(obj[@"class"]);
NSDictionary *ed = obj[@"event"];
NSMutableDictionary *td = [NSMutableDictionary dictionaryWithCapacity:0];
[ed enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
NSMutableArray *tArr = [NSMutableArray arrayWithCapacity:0];
[tArr addObjectsFromArray:[obj isKindOfClass:[NSArray class]] ? obj : @[obj]];
[tArr enumerateObjectsUsingBlock:^(NSString *m, NSUInteger idx, BOOL *stop) {
if ([td.allKeys containsObject:m]) {
NSMutableArray *ms = [td[m] mutableCopy];
if (![ms containsObject:key]) [ms addObject:key];
td[m] = ms;
} else {
td[m] = @[key];
}
}];
}];
[td enumerateKeysAndObjectsUsingBlock:^(NSString *kmethod, NSArray <NSString *> *tArr, BOOL *stop) {
SEL sel = NSSelectorFromString(kmethod);
NSError *error = nil;
[self checkValidWithClass:obj[@"class"] method:kmethod];
[class aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
[tArr enumerateObjectsUsingBlock:^(NSString *name, NSUInteger idx, BOOL *stop) {
NSString *ename = name;
id<RJEventTracking> t = [NSClassFromString(name) new];
if (t && [t respondsToSelector:@selector(trackingMethod:instance:arguments:)]) {
ename = [t trackingMethod:kmethod instance:info.instance
arguments:info.arguments];
}
if ([ename length]) {
NSLog(@"<RJEventTracking> - %@", ename);
}
}];
} error:&error];
[self checkHookStatusWithClass:obj[@"class"] method:kmethod error:error];
}];
}];
}

最后附上源码地址: https://github.com/rjinxx/RJEventTracking,

pod 'RJEventTracking'

在使用RJEventTracking的过程中中有遇到什么问题或者优化建议欢迎留言PR,谢谢。

转自:https://www.jianshu.com/p/cdf61602316e

收起阅读 »

iOS仿高德路线规划滑动效果

因为项目有个界面要模仿高德地图路径规划滑动效果,因此写了demo,并简单说下分析过程高德地图效果演示:demo效果演示:Demo地址:https://github.com/fangjinfeng/MySampleCode/tree/master/FJFBlog...
继续阅读 »

因为项目有个界面要模仿高德地图路径规划滑动效果,因此写了demo,并简单说下分析过程

高德地图效果演示:


demo效果演示:


Demo地址:https://github.com/fangjinfeng/MySampleCode/tree/master/FJFBlogProjectDemo

一. 分析

  • 首先,我们可以看出这个滚动的视图应该是UIScrollView或者UIScrollView的子类(比如:UITableView);

  • 其次,从高德地图里的视图一开始的滑动,可以看出这个滑动是平稳的滑动,没有加速和减速,因此这里不可能是UIScrollView的滚动效果,因为UIScrollView的滚动效果是由一个加减速的过程,因此一开始滑动,应该是通过滑动手势UIPanGestureRecognizer,来移动UIScrollView的y值来移动

  • 接着滑动到指定位置之后,UIScrollView的y值固定不动,然后UIScrollView的内容进行滚动。这里就涉及到滑动手势UIPanGestureRecognizer的滑动,还有UIScrollView内部的滚动的处理。高德地图的演示效果里面,一开始滑动视图向上移动,移动到指定的点之后,立马就变成视图的滚动,这里可以分析,UIScrollView既支持手势的滑动又支持视图的滚动,只是通过条件来判断限制两者的执行逻辑。

  • 同时我们可以看到,如果一开始向上拉动视图力度大一点,视图会直接滚动到指定位置,如果力度小,就恢复到原来位置,因此这里需要依据手势滑动的加速度来进行判断处理。

  • 而当你滑动到中间位置的时候,也需要依据最后滑动的位置来判断应该动画滚动到上方还是下方。

  • 最后滑动的时候上方的视图和滑动视图本身有背景颜色的渐变效果,这里需要依据滑动距离来判断。

二.代码分析:

首先由于滚动视图(demo里面是UITableView)需要支持手势滑动和内部滚动,因此需要写一个类FJBaseTableView继承自UITableView,然后在FJBaseTableView的实现里面重写如下方法:

// 当有 多个手势 都可以 响应
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {

return YES;
}

来支持响应多个手势。

  • 然后给滚动视图tableView添加滑动手势,当tableView从底部滑动到顶部指定位置时,应该限制tableView内部的视图滚动。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (self.tableView.frame.origin.y > _scrollViewStartPositionY) {
[scrollView setContentOffset:CGPointMake(0, 0)];
}
}

这里的_scrollViewStartPositionY是顶部指定位置。

  • 接着看下手势滑动的处理逻辑:

#pragma mark - 手势处理
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender {

if (sender.state == UIGestureRecognizerStateBegan) {

_beganPoint = [sender locationInView:sender.view.superview];
_curPoint = sender.view.center;
_topTipContainerViewCurrentY = _topContainerView.frame.origin.y;
_previousOffsetY = self.tableView.contentOffset.y;

} else if(sender.state == UIGestureRecognizerStateChanged) {

CGPoint point = [sender locationInView:sender.view.superview];

CGFloat offsetY = _previousOffsetY - self.tableView.contentOffset.y;
NSInteger y_offset = point.y - _beganPoint.y - offsetY;

if (sender.view.frame.origin.y >= _scrollViewStartPositionY || (self.tableView.contentOffset.y == 0 && self.tableView.contentSize.height > self.tableView.frame.size.height)) {
sender.view.center = CGPointMake(_curPoint.x, _curPoint.y + y_offset);
[self updateViewControlsWithSlideOffset:y_offset];
}

if (sender.view.frame.origin.y > _scrollViewLimitMaxY) {
sender.view.y = _scrollViewLimitMaxY;
[self updateViewControlsWithSlideUp:NO];
}
else if(sender.view.frame.origin.y < _scrollViewStartPositionY) {

sender.view.y = _scrollViewStartPositionY;
[self updateViewControlsWithSlideUp:YES];
}
} else if(sender.state == UIGestureRecognizerStateEnded) {

if (sender.view.frame.origin.y <= _scrollViewStartPositionY || sender.view.frame.origin.y > _scrollViewLimitMaxY) {
if (sender.view.frame.origin.y <= _scrollViewStartPositionY) {
[self updateViewControlsWithSlideUp:YES];
}
if (sender.view.frame.origin.y > _scrollViewLimitMaxY) {
[self updateViewControlsWithSlideUp:NO];
}
return;
}
// 滑动速度处理
CGPoint velocity = [sender velocityInView:self.view];
CGFloat speed = 350;
if (velocity.y < - speed) {
// 快速向上
[self tableViewMoveToTop];
return;
} else if (velocity.y > speed) {
// 快速向下
[self tableViewMoveToBottom];
return;
}

// 滑动临界值
CGFloat criticalValue = _scrollViewLimitMaxY/2.0;
if (sender.view.frame.origin.y <= criticalValue) {
[self tableViewMoveToTop];
} else {
[self tableViewMoveToBottom];
}
}
}

这里几个点需要注意:

1. _beganPoint、_curPoint两个参数是用来计算手势滑动距离然后调整scrollView的滑动距离。而_previousOffsetY是用来记录滑动之前tableView的内部视图的偏移距离,因为当tableView滑动到顶部指定位置后,tableView开始滚动,这时候tableView向下滑动是先移动了tableView内部的滚动距离,然后才是滑动距离,因此需要将这部分值先记录,然后去除掉,才是tableView向下真正需要滑动的距离。

CGFloat offsetY = _previousOffsetY - self.tableView.contentOffset.y;
NSInteger y_offset = point.y - _beganPoint.y - offsetY;

2.滑动过程中,顶部视图的移动和渐变处理,这里先依据滑动的距离算出tableView滑动距离与tableView最大滑动距离的比值,然后再算出顶部视图需要移动的距离和背景的透明度。

- (void)updateViewControlsWhenSliding {
if (self.tableView.frame.origin.y > _scrollViewStartPositionY && self.tableView.frame.origin.y < _scrollViewLimitMaxY) {

CGFloat offsetLimitDistance = _scrollViewLimitMaxY - _scrollViewStartPositionY;
CGFloat offsetDistance = self.tableView.frame.origin.y - _scrollViewStartPositionY;
if (offsetDistance > 0 && offsetDistance < offsetLimitDistance) {
CGFloat topViewHeight = [FJFTopContainerView viewHeight];
CGFloat topViewHeightOffset = offsetDistance * (topViewHeight / offsetLimitDistance);
CGFloat viewAlpha = offsetDistance / offsetLimitDistance;
_topContainerView.y = topViewHeightOffset - topViewHeight;
_topContainerView.alpha = viewAlpha;
}
}
}

3.滑动速度处理,依据velocityInView函数获取速度值,然后依据当前速度值大小和正负和设定的速度值比较来判断是否需要向上或向下移动。

// 滑动速度处理
CGPoint velocity = [sender velocityInView:self.view];
CGFloat speed = 350;
if (velocity.y < - speed) {
// 快速向上
[self tableViewMoveToTop];
return;
} else if (velocity.y > speed) {
// 快速向下
[self tableViewMoveToBottom];
return;
}

4.滑动临界值处理,判断最后滑动位置与底部指定位置一半,两个值的大小来判断滑动的方向。

// 滑动临界值
CGFloat criticalValue = _scrollViewLimitMaxY/2.0;
if (sender.view.frame.origin.y <= criticalValue) {
[self tableViewMoveToTop];
} else {
[self tableViewMoveToBottom];
}

三.总结

这里最主要就是介绍了分析的思路,来找出可靠的实现方法,具体逻辑,详见demo

转自:https://www.jianshu.com/p/14dd820393fa

收起阅读 »

Android超级高效换肤框架,让你体验无闪烁换肤

用法1. 在Application中进行初始化public class SkinApplication extends Application { public void onCreate() { super.onCreate(); // Must ...
继续阅读 »

用法

1. 在Application中进行初始化

public class SkinApplication extends Application {
public void onCreate() {
super.onCreate();
// Must call init first
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
}
}

2. 在布局文件中标识需要换肤的View

...
xmlns:skin="http://schemas.android.com/android/skin"
...
<TextView
...
skin:enable="true"
... />

3. 继承BaseActivity或者BaseFragmentActivity作为BaseActivity进行开发

4. 从.skin文件中设置皮肤

String SKIN_NAME = "BlackFantacy.skin";
String SKIN_DIR = Environment.getExternalStorageDirectory() + File.separator + SKIN_NAME;
File skin = new File(SKIN_DIR);
SkinManager.getInstance().load(skin.getAbsolutePath(),
new ILoaderListener() {
@Override
public void onStart() {
}

@Override
public void onSuccess() {
}

@Override
public void onFailed() {
}
});

5. 重设默认皮肤

SkinManager.getInstance().restoreDefaultTheme();

6. 对代码中创建的View的换肤支持

主要由IDynamicNewView接口实现该功能,在BaseActivityBaseFragmentActivityBaseFragment中已经实现该接口.

public interface IDynamicNewView {
void dynamicAddView(View view, List<DynamicAttr> pDAttrs);
}

**用法:**动态创建View后,调用dynamicAddView方法注册该View至皮肤映射表即可(如下).详见sample工程

	private void dynamicAddTitleView() {
TextView textView = new TextView(getActivity());
textView.setText("Small Article (动态new的View)");
RelativeLayout.LayoutParams param = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
param.addRule(RelativeLayout.CENTER_IN_PARENT);
textView.setLayoutParams(param);
textView.setTextColor(getActivity().getResources().getColor(R.color.color_title_bar_text));
textView.setTextSize(20);
titleBarLayout.addView(textView);

List<DynamicAttr> mDynamicAttr = new ArrayList<DynamicAttr>();
mDynamicAttr.add(new DynamicAttr(AttrFactory.TEXT_COLOR, R.color.color_title_bar_text));
dynamicAddView(textView, mDynamicAttr);
}

7. 皮肤包是什么?如何生成?

  • 皮肤包(后缀名为.skin)的本质是一个apk文件,该apk文件不包含代码,只包含资源文件
  • 在皮肤包工程中(示例工程为skin/BlackFantacy)添加需要换肤的同名的资源文件,直接编译生成apk文件,再更改后缀名为.skinj即可(防止用户点击安装)
  • 使用gradle的同学,buildandroid-skin-loader-skin工程后即可在skin-package目录下取皮肤包(修改脚本中def skinName = "BlackFantacy.skin"换成自己想要的皮肤名)

代码下载:Android-Skin-Loader-master.zip

收起阅读 »

探究产生离屏渲染的秘密

一.渲染机制CPU将计算好的需要显示的内容提交给GPU,GPU渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照Vsync(垂直脉冲)信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器进行显示。二.GPU屏幕渲染两种方式1.On-Screen Re...
继续阅读 »

一.渲染机制

CPU将计算好的需要显示的内容提交给GPU,GPU渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照Vsync(垂直脉冲)信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器进行显示。

二.GPU屏幕渲染两种方式

1.On-Screen Rendering:当前屏幕渲染

指GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

2.Off-Screen Rendering:离屏渲染

指GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

三.两种渲染方式比较

相比于当前屏幕渲染,离屏渲染的代价很高,主要体现在以下两个方面:

1.创建新缓冲区

要想进行离屏渲染,首先需要创建一个新的缓冲区。

2.上下文切换

离屏渲染的整个过程,需要多次进行上下文切换:先从当前屏幕(On-Screen)到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换回导致GPU产生空闲,而GPU拥有大量的并行计算的处理单元,这些处理单元都空闲,会产生巨大的浪费。

四.特殊的离屏渲染:CPU渲染

如果重写了drawRect方法,并且使用任何Core Graphics 的技术进行了绘制操作,就涉及到CPU渲染。整个渲染过程由CPU在App内同步完成,渲染得到的bitmap(位图)最后再交由GPU用于显示。

CoreGraphic通常是线程安全的,所以可以进行一步绘制,显示的时候再回主线程,一个简单异步绘制内容如下:

- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}

五.为什么产生离屏渲染

离屏渲染产生的原因主要有两方面:

1.在VSync(垂直脉冲)信号作用下,视频控制器每隔16.67ms就会去帧缓冲区(当前屏幕缓冲区)读取渲染后的数据;但是有些效果被认为不能直接呈现于屏幕前,而需要在别的地方做额外的处理,进行预合成。

比如图层属性的混合体再没有预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前必须在一个屏幕外上下文中被渲染(不论CPU还是GPU)。

举个🌰:

UIView *AView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
AView.backgroundColor = [UIColor redColor];
AView.alpha = 0.5;
[self.view addSubview:AView];

UIView *BView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
BView.backgroundColor = [UIColor blackColor];
BView.alpha = 0.5;
[AView addSubview:BView];

效果图:


如上代码所示:

AView 视图包含BView视图,AView视图是红色,透明度为0.5;BView视图为黑色,透明度也为0.5,那么在渲染阶段,就会对AView和BView图层重叠的部分进行混合操作,但是这个过程并不适合直接显示在屏幕上,因此需要开辟屏幕外的缓存,对这两个图层进行屏幕外的渲染,然后将渲染的结果写回到当前屏幕缓存区。

这里有些人会有疑问,那如果能保证图层在16.67ms里完成渲染,视频控制器去读取的时候能读取到渲染完成的数据,不就可以了。

理论上,确实可以这样理解,但是图层之间的混合、渲染这个过程所耗费的时间是不固定的,跟多个维度相关,比如图层数量、重叠区域、GPU处理器性能等,因此底层设计的时候,应该是将不能够直接呈现在屏幕上的效果,都通过离屏渲染来操作。

2.有些视图渲染后的纹理需要被多次复用,但屏幕内的渲染缓冲区是实时更新的,所以需要通过开辟屏幕外的渲染缓冲区,将视图的内容渲染成纹理并缓存,然后再需要的时候在调入屏幕缓冲区,可以避免多次渲染的开销。

典型的例子就是光栅化。光栅化就是通过把视图的内容渲染成纹理并缓存,等到下次调用的时候直接去缓存的取出纹理,但是更新内容时候,会启用离屏渲染,所以更新的代价比较大,只能用于静态内容;而且如果光栅化的元素100ms没有被使用,也将被移除,故而不常用元素的光栅化并不会优化显示。

注意:光栅化的元素,总大小限制为2.5倍的屏幕。

六.如何检测离屏渲染

1.模拟器

模拟器在工作栏上面的Debug -> Color Off-Screen Rendered


2.真机

真机在工作栏上面的Debug -> View Debugging -> Rendering -> Color Off-Screen Rendered Yellow


七.引起离屏渲染操作和怎样优化

关于这方面的资料,可以参考文章:

离屏渲染优化详解:实例示范+性能测试

iOS-离屏渲染详解

如果想更深入的了解,可以了解下OpenGL、Metal、计算机图形学这方面的知识。

八.延伸阅读

离屏渲染优化详解:实例示范+性能测试

iOS-离屏渲染详解

Metal【1】—— 概述

iOS开发-视图渲染与性能优化

链接:https://www.jianshu.com/p/aa8dc1a61c91

收起阅读 »

Fragment问世这么久,你真的会用吗?

Fragment的简单用法 在一个Activity中添加两个Fragmet,并让这两个Fragment平分屏幕空间 首先新建一个左侧Fragmet布局left_fragment_xml,这里只放置一个按钮 <?xml version="1.0" enco...
继续阅读 »

Fragment的简单用法


在一个Activity中添加两个Fragmet,并让这两个Fragment平分屏幕空间


首先新建一个左侧Fragmet布局left_fragment_xml,这里只放置一个按钮


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="button"/>

</LinearLayout>
复制代码

新建一个右侧Fragment布局叫right_fragment_xml,一个文本框


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#00ff00">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="24sp"
android:text="This is aright Fragment"/>
</LinearLayout>
复制代码

然后分别新建LeftFragmet和RightFragment两个类继承Fragment,并且重写onCreateView()方法


package com.example.fragmenttest

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class LeftFragment:Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.left_fragment,container,false)
}
}
复制代码

package com.example.fragmenttest

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class LeftFragment:Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.right_fragment,container,false)
}
}
复制代码

最后在man.xml标签中引入Fragment布局


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>


<fragment
android:id="@+id/rightFrag"
android:name="com.example.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>




</LinearLayout>
复制代码

最后运行


截屏2021-05-09 下午8.51.49.png


动态添加Fragment


意思就是在运行程序时候动态添加Fragment


首先我们新建一个要添加得Fragment叫anther_rigt_fragment.xml


这里只是把颜色改为黄色背景


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffff00">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="24sp"
android:text="This is anther right fragment"/>
</LinearLayout>
复制代码

下一步也是一样新建一个AotherRightFragment类继承Fragment,重写onCreateView()方法


kage com.example.fragmenttest

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class AntherRightFrogment :Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.anther_right_fragement,container,false)
}
}
复制代码

然后修改man.xml代码,引入FrameLayout布局,把右边的Fragment布局替换


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>

<FrameLayout
android:id="@+id/rightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
/>

</LinearLayout>
复制代码

最后我们修改ManActivity的代码,为button设置监听器,达到点击BUTTON按钮,更换Fragment


class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button:Button=findViewById(R.id.button)
button.setOnClickListener{
repalce(AntherRightFrogment())
}
repalce(RightFragment())
}
private fun repalce(fragment:Fragment){
//就是获取所在fragment的父容器的管理器,
val fragementManager=supportFragmentManager
//开启一个事务
val transaction=fragementManager.beginTransaction()
//添加和替换Fragment
transaction.replace(R.id.rightFragment,fragment)
//返回栈
transaction.addToBackStack(null)
//提交事务
transaction.commit()

}
复制代码

在Fragment中实现返回栈


按下back建返回上一个Fragment


        //返回栈
transaction.addToBackStack(null)
复制代码

Fragment生命周期


onAttach():Fragment和Activity相关联时调用


onCreate():系统创建Fragment时调用


onCreateView():创建Fragment的布局(视图)调用


onActivityCreated():确保与Fragment相关联的Activity调用完时调用


首先我们修改RightFragment代码来看效果


package com.example.fragmenttest

import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class RightFragment :Fragment(){
companion object{
const val TAG="RightFragment"
}
//当Fragment和Activity建立关联时调用
override fun onAttach(context: Context) {
super.onAttach(context)
Log.d(TAG,"onAttach")
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG,"onCteate")
}
//为Fragment创建视图获加载布局时调用
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d(TAG,"onCreateView")
return inflater.inflate(R.layout.right_fragment,container,false)
}
//确保和Fragment相关联的Activity已经创建完毕时候调用
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

Log.d(TAG,"onActivityCreated")
}

override fun onStart() {
super.onStart()
Log.d(TAG,"onStart")
}

override fun onResume() {
super.onResume()
Log.d(TAG,"onResume")
}

override fun onPause() {
super.onPause()
Log.d(TAG,"onPause")
}

override fun onStop() {
super.onStop()
Log.d(TAG,"onStop")
}
//当与Fragment先关联的视图移除时候调用
override fun onDestroyView() {
super.onDestroyView()
Log.d(TAG,"onDestroyView")
}

override fun onDestroy() {
super.onDestroy()
Log.d(TAG,"onDestroy")
}
//当Fragment与Activity解除关联时候调用
override fun onDetach() {
super.onDetach()
Log.d(TAG,"onDetach")
}

}
复制代码

首次加载RightFragment


截屏2021-05-09 下午8.56.39.png


按下Button按钮


截屏2021-05-09 下午8.57.02.png


按下Back键返回


截屏2021-05-09 下午8.57.18.png


再次按下Back键


截屏2021-05-09 下午8.57.34.png


使用限定符


这个作用比较大例如一般平板使用双页符,因为屏幕比较大任性,但是我们手机就不同了屏幕空间小,


只能显示一页,那么怎么才能让运行程序的程序自动判断到底用那页了,这时候就使用我们的限定符(qualifier)实现。


首先我们修改activity.main.xml代码,只留下我们左边的Fragment,就是带一个button按钮的,把它就用做我们手机的单页显示。


修改 android:layout_width="match_parent让它宽度和父布局一样占满空间


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/leftFrag"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"/>



</LinearLayout>
复制代码

接着在res目录下新建一个layout-large文件夹,然后在这个文件夹下添加一个新的activity_main.xml布局,代码就是前面双页布局,这里只修改屏幕占比,这里large就是一个限定符,认为是large的设备就加载这个个layout-large文件夹布局。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<fragment
android:id="@+id/leftFrag"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/rightFrage"
android:name="com.example.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"/>

</LinearLayout>
复制代码

最后运行代码在平板上


截屏2021-05-09 下午8.58.11.png


在手机上


截屏2021-05-09 下午8.58.29.png


最小宽度限定符


large解决了单双页的问题,那么这个large到底多大了,这里我们映入了最小宽度限定符,它允许我们为屏幕指定一个最小值以dp为单位,超过这个最小值,则加载一个布局,那么小于则执行另外一个。


首先我们在res目录下兴建一个layout_sw600dp文件夹,再次兴建activity_main.xml布局


这里的600dp就是一个临界点,代码都一样就是把两个布局加载


运行的结果,当然看你运行的屏幕了,但宽度大于600dp就加载layout_sw600目录下的activity_main.xml布局,小与600dp就加载默认的,就是layout目录下activity_main.xml布局


作者:用户2069379142245
链接:https://juejin.cn/post/6960273662704353294
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS — Swift高级分享:SWIFT协议的替代方案

毫无疑问,协议是SWIFT总体设计的主要部分-并且可以提供一种很好的方法来创建抽象、分离关注点和提高系统或功能的整体灵活性。通过不强烈地将类型绑定在一起,而是通过更抽象的接口连接代码库的各个部分,我们通常会得到一个更加解耦的体系结构,它允许我们孤立地迭代每个单...
继续阅读 »

毫无疑问,协议是SWIFT总体设计的主要部分-并且可以提供一种很好的方法来创建抽象、分离关注点和提高系统或功能的整体灵活性。通过不强烈地将类型绑定在一起,而是通过更抽象的接口连接代码库的各个部分,我们通常会得到一个更加解耦的体系结构,它允许我们孤立地迭代每个单独的特性。

然而,虽然协议在许多不同的情况下都是一个很好的工具,但它们也有各自的缺点和权衡。本周,让我们来看看其中的一些特性,并探索几种在SWIFT中抽象代码的替代方法-看看它们与使用协议相比如何。

使用闭包的单个需求

使用协议抽象代码的优点之一是它允许我们对多个代码进行分组。所需在一起。例如,PersistedValue协议可能需要两个save和一个load方法-这两种方法都使我们能够在所有这些值之间强制执行一定程度的一致性,并编写用于保存和加载数据的共享实用程序。

然而,并不是所有的抽象都涉及多个需求,并且非常常见的协议只有一个方法或属性-比如这个:

protocol ModelProvider {
associatedtype Model: ModelProtocol
func provideModel() -> Model
}

假设上面的ModelProvider协议用于抽象我们在代码库中加载和提供模型的方式。它使用关联类型,以便让每个实现以非常类型安全的方式声明它提供的模型类型,这是很棒的,因为它使我们能够编写通用代码来执行常见任务,例如为给定模型呈现详细视图:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelProvider: AnyModelProvider<Model>

init<T: ModelProvider>(modelProvider: T) where T.Model == Model {
// We wrap the injected provider in an AnyModelProvider
// instance to be able to store a reference to it.
self.modelProvider = AnyModelProvider(modelProvider)
super.init(nibName: nil, bundle: nil)
}

override func viewDidLoad() {
super.viewDidLoad()

let model = modelProvider.provideModel()
...
}

...
}

虽然上面的代码可以工作,但它说明了使用具有关联类型的协议的缺点之一-我们不能将引用存储到ModelProvider直接。相反,我们必须首先执行类型擦除将我们的协议引用转换成一个具体的类型,这两种类型都会使我们的代码混乱,并要求我们实现其他类型,以便能够使用我们的协议。

因为我们所处理的协议只有一个要求,所以问题是-我们真的需要吗?毕竟,我们ModelProvider协议没有添加任何额外的分组或结构,因此让我们取消它的唯一要求,将其转化为闭包-然后可以直接注入,如下所示:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelProvider: () -> Model

init(modelProvider: @escaping () -> Model) {
self.modelProvider = modelProvider
super.init(nibName: nil, bundle: nil)
}

override func viewDidLoad() {
super.viewDidLoad()

let model = modelProvider()
...
}

...
}

通过直接注入我们需要的功能,而不是要求类型符合协议,我们还大大提高了代码的灵活性-因为我们现在可以自由地注入任何东西,从空闲函数到内联定义的闭包,再到实例方法。我们也不再需要执行任何类型删除,留给我们的代码要简单得多。

使用泛型类型

虽然闭包和函数是建模单个需求抽象的好方法,但是如果我们开始添加额外的需求,那么使用它们可能会变得有点混乱。例如,假设我们希望扩展上面的内容DetailViewController也支持书签和删除模型。如果我们坚持基于闭包的方法,我们最终会得到这样的结果:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelProvider: () -> Model
private let modelBookmarker: (Model) -> Void
private let modelDeleter: (Model) -> Void

init(modelProvider: @escaping () -> Model,
modelBookmarker: @escaping (Model) -> Void,
modelDeleter: @escaping (Model) -> Void) {
self.modelProvider = modelProvider
self.modelBookmarker = modelBookmarker
self.modelDeleter = modelDeleter

super.init(nibName: nil, bundle: nil)
}

...
}

上述设置不仅要求我们跟踪多个独立闭包,而且还会出现大量重复的闭包。“模型”前缀-(使用“三人规则”)告诉我们,我们这里有一些结构性问题。而我们能回到将上述所有闭包封装到一个协议中去,这再次要求我们执行类型擦除,并失去我们在开始使用闭包时获得的一些灵活性。

相反,让我们使用泛型类型将我们的需求组合在一起-这两种类型都允许我们保留使用闭包的灵活性,同时在代码中添加一些额外的结构:

struct ModelHandling<Model: ModelProtocol> {
var provide: () -> Model
var bookmark: (Model) -> Void
var delete: (Model) -> Void
}

因为上面是一个具体的类型,所以它不需要任何形式的类型擦除(实际上,它看起来非常类似于我们在使用带关联类型的协议时经常被迫编写的类型擦除包装)。因此,就像闭包一样,它可以直接使用和存储-如下所示:

class DetailViewController<Model: ModelProtocol>: UIViewController {
private let modelHandler: ModelHandling<Model>
private lazy var model = modelHandler.provide()

init(modelHandler: ModelHandling<Model>) {
self.modelHandler = modelHandler
super.init(nibName: nil, bundle: nil)
}

@objc private func bookmarkButtonTapped() {
modelHandler.bookmark(model)
}

@objc private func deleteButtonTapped() {
modelHandler.delete(model)
dismiss(animated: true)
}

...
}

而具有关联类型的协议在定义更高级别的需求时非常有用(就像标准库的Equatable和Collection),当这样的协议需要直接使用时,使用独立闭包或泛型类型通常可以给我们相同的封装级别,但通过一个简单得多的抽象。

使用枚举分离要求

在设计任何类型的抽象时,一个常见的挑战是不要。“过于抽象”通过添加太多的需求。例如,现在假设我们正在开发一个应用程序,它允许用户使用多种媒体-比如文章、播客、视频等等-我们希望为所有这些不同的格式创建一个共享的抽象。如果我们再次从面向协议的方法开始,我们可能会得到这样的结果:

protocol Media {
var id: UUID { get }
var title: String { get }
var description: String { get }
var text: String? { get }
var url: URL? { get }
var duration: TimeInterval? { get }
var resolution: Resolution? { get }
}

由于上面的协议需要与所有不同类型的媒体一起工作,我们最终得到了多个仅与某些格式相关的属性。例如,Article类型没有任何概念持续时间或分辨力-留给我们一些我们必须实现的属性,因为我们的协议要求我们:

struct Article: Media {
let id: UUID
var title: String
var description: String
var text: String?
var url: URL? { return nil }
var duration: TimeInterval? { return nil }
var resolution: Resolution? { return nil }
}

上面的设置不仅要求我们在符合标准的类型中添加不必要的样板,还可能是歧义的来源-因为我们无法强制规定一篇文章实际上包含文本,或者应该支持URL、持续时间或解析的类型实际上携带了该数据-因为所有这些属性都是选项。

我们可以通过多种方法解决上述问题,从将协议拆分为多个协议开始,每个方法都具有提高专业化程度-像这样:

protocol Media {
var id: UUID { get }
var title: String { get }
var description: String { get }
}

protocol ReadableMedia: Media {
var text: String { get }
}

protocol PlayableMedia: Media {
var url: URL { get }
var duration: TimeInterval { get }
var resolution: Resolution? { get }
}

以上所述无疑是一种改进,因为它将使我们能够拥有以下类型Article符合ReadableMedia,和可玩类型(如Audio和Video)符合PlayableMedia-减少歧义和样板,因为每种类型都可以选择哪一种专门版本的Media它想要遵守的。

但是,由于上述协议都是关于数据的,因此使用实际数据类型相反,这既可以减少重复实现的需要,也可以让我们通过单一的具体类型来处理任何媒体格式:

struct Media {
let id: UUID
var title: String
var description: String
var content: Content
}

上面的结构现在只包含我们所有媒体格式之间共享的数据,除了content属性-这就是我们将用于专门化的内容。但这一次,而不是Content一个协议,让我们使用枚举-它将使我们能够通过关联的值为每种格式定义一组量身定做的属性:

extension Media {
enum Content {
case article(text: String)
case audio(Playable)
case video(Playable, resolution: Resolution)
}

struct Playable {
var url: URL
var duration: TimeInterval
}
}

选项已经消失,我们现在已经在共享抽象和启用特定于格式的专门化之间取得了很好的平衡。枚举的美妙之处还在于,它使我们能够表达数据变化,而不必使用泛型或协议-只要我们预先知道变体的数量,一切都可以封装在相同的具体类型中。

类和继承

另一种方法在SWIFT中可能不像在其他语言中那么流行,但仍然值得考虑,那就是使用通过继承专门化的类来创建抽象。例如,而不是使用Content为了实现上述媒体格式,我们可以使用Media基类,然后将其子类化,以添加特定于格式的属性,如下所示:

class Media {
let id: UUID
var title: String
var description: String

init(id: UUID, title: String, description: String) {
self.id = id
self.title = title
self.description = description
}
}

class PlayableMedia: Media {
var url: URL
var duration: TimeInterval

init(id: UUID,
title: String,
description: String,
url: URL,
duration: TimeInterval) {
self.url = url
self.duration = duration
super.init(id: id, title: title, description: description)
}
}

然而,尽管从结构的角度来看,上述方法是完全有意义的-但它也有一些不利之处。首先,由于类还不支持按成员划分的初始化器,所以我们必须自己定义所有初始化器-我们还必须通过调用super.init..但也许更重要的是,课程是参考类型,这意味着在共享时,我们必须小心避免执行任何意外的突变。Media跨代码库的实例。

但这并不意味着SWIFT中没有有效的继承用例。例如,在“在未来的引擎盖下&斯威夫特的承诺”,继承提供了一种公开只读的好方法。Future类型到api用户-同时仍然允许通过Promise子类:

class Future<Value> {
fileprivate var result: Result<Value, Error>? {
didSet { result.map(report) }
}

...
}

class Promise<Value>: Future<Value> {
func resolve(with value: Value) {
result = .success(value)
}

func reject(with error: Error) {
result = .failure(error)
}
}

func loadCachedData() -> Future<Data> {
let promise = Promise<Data>()
cache.load { promise.resolve(with: $0) }
return promise
}

使用上面的设置,我们可以让同一个实例在不同的上下文中公开不同的API集,当我们只允许其中一个上下文对给定的对象进行变异时,这是非常有用的。在使用泛型代码时尤其如此,因为如果我们尝试使用一个协议来实现相同的目标,我们将再次遇到关联类型问题。

结语

在可预见的将来,协议是很棒的,并且很可能仍然是在SWIFT中定义抽象的最常用的方式。然而,这并不意味着使用协议永远是最好的解决方案-有时会超越流行的范围“面向协议的编程”MARRA可以产生更简单、更健壮的代码-特别是当我们想要定义的协议要求我们使用关联类型的时候。

链接:https://www.jianshu.com/p/74d511140089

收起阅读 »

iOS OC开发 BTC、ETH、区块链钱包

ETH钱包部分:功能有:1、创建钱包2、通过助记词导入钱包3、通过KeyStore导入钱包4、通过私钥导入钱包5、查询余额6、查询以太坊系代币余额7、转账BTC钱包部分:功能:1、创建钱包2、通过私钥导入钱包3、通过助记词导入钱包4、查询余额5、查询交易记录6...
继续阅读 »

ETH钱包部分:

功能有:

1、创建钱包

2、通过助记词导入钱包

3、通过KeyStore导入钱包

4、通过私钥导入钱包

5、查询余额

6、查询以太坊系代币余额

7、转账



BTC钱包部分:

功能:

1、创建钱包

2、通过私钥导入钱包

3、通过助记词导入钱包

4、查询余额

5、查询交易记录

6、发起交易



项目连接:


ETH钱包Demo:https://github.com/Ccct/CCTEthereum/

BTC钱包Demo:https://github.com/Ccct/CCTBTC

链接:https://www.jianshu.com/p/1b8c1ed88e69
收起阅读 »

ARC对init方法的处理

前言此文源于前几日工作中遇到的一个问题,并跟同事就init方法进行了相关讨论。相关代码如下:Person *myPerson = [Person alloc];NSMethodSignature *signature = [NSMethodSignature ...
继续阅读 »

前言

此文源于前几日工作中遇到的一个问题,并跟同事就init方法进行了相关讨论。相关代码如下:

Person *myPerson = [Person alloc];
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@16@0:8"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = myPerson;
invocation.selector = @selector(initPerson);
[invocation invoke];
__unsafe_unretained id retValue;
[invocation getReturnValue:&retValue];

正常来说,这段代码运行起来没有任何问题。然而,当Person的initPerson方法返回nil或者返回子类对象时,上述代码就会EXC_BAD_ACCESS。但如果我们把initPerson方法前缀改成其他(比如:createPerson),就不会crash。为了查清原因,便对init方法进行了一次探索(说探索多少有些夸张)。

通过符号断点及反汇编等调试手段,发现在initPerson方法结束的时候,person对象调用了一次release,而上述示例代码执行完,ARC为了抵消[Person alloc]这步操作,会对myPerson进行一次release。也就是说,过渡释放引起了crash。

那么接下来,我们就看下init方法结束的时候,为什么要调用那次看似多余的release?

原因分析

在clang文档中找到这么两个东西:__attribute__((ns_consumes_self))、__attribute((ns_returns_retained))。

据文档描述,前者的作用是将ownership从主调方转移到被调方;而后者的作用是把ownership从被调方转移到主调方。具体原理如下:

0x1. __attribute__((ns_consumes_self))

若某个方法被标记这个特性,调用方会在方法调用前对receiver进行一次retain(也可能会被编译器优化掉),而被调方会在方法结束的时候对self进行一次release。比如下面代码

// 主调方
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
Person *myPerson = [[Person alloc] init];
[myPerson noninitPerson]; // 以非init方法来测试
return YES;
}

// 被调方
@interface Person : NSObject
- (void)noninitPerson __attribute__((ns_consumes_self));
@end

@implementation Person
- (void)noninitPerson {
}
@end

通过Hopper反汇编,伪代码如下:

// 主调方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
rax = [var_28 retain]; // 调用前retain
[rax noninitPerson]; // 开始调用
objc_storeStrong(var_28, 0x0);
return rax;
}

// 被调方
void -[Person noninitPerson](void * self, void * _cmd {
objc_storeStrong(self, 0x0); // 调用完被调方负责release
return;
}

而init开头的方法会被隐式地标记这个特性,文档中有描述:

The implicit self parameter of a method may be marked as consumed by adding __ attribute __((ns_consumes_self)) to the method declaration. Methods in theinitfamily are treated as if they were implicitly marked with this attribute.

0x2. __attribute__((ns_returns_retained))

若方法标记这个特性,表示主调方希望得到一个retainCount+1的对象,即被调方可能会进行一次retain将所有权移交给主调方,主调方会进行一次release(可能会被编译器优化掉)来负责释放。

伪代码如下:

// 主调方
var_28 = [[Person alloc] init];
rax = [var_28 running];
[rax release]; // 主调方负责释放

// 被调方
void * -[Person running](void * self, void * _cmd) {
rax = [self retain]; // 若这里返回一个新分配的对象,则无需retain
return rax;
}

同样地,init开头的方法也会被标记这个特性,文档里亦有体现:

Methods in the alloc, copy, init, mutableCopy, and new families are implicitly marked __ attribute __((ns_returns_retained)).

这么多的retain、release,多少有些凌乱,既然已知init方法会被标记__attribute__((ns_returns_retained))和__attribute__((ns_consumes_self)),那我们干脆看下init方法反汇编后的代码:

// 主调方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
objc_storeStrong(var_28, 0x0);
// 优化掉了一对retain/release
return rax;
}

// 被调方
void * -[Person init](void * self, void * _cmd) {
// 忽略一些无关指令
var_18 = [self retain]; // 对应__attribute__((ns_returns_retained))
objc_storeStrong(self, 0x0); // 对应__attribute__((ns_consumes_self))
rax = var_18;
return rax;
}

到这里,我们基本了解了init方法原理,那么离文章开头那段代码crash又如何解释呢?我们对代码稍作修改,让init方法返回nil,再看下:

// 主调方
bool -[AppDelegate application:didFinishLaunchingWithOptions:](void * self, void * _cmd, void * arg2, void * arg3) {
var_28 = [[Person alloc] init];
objc_storeStrong(var_28, 0x0);
return rax;
}

// 被调方
void * -[Person init](void * self, void * _cmd) {
// 因为返回nil,所以这里的retain不存在了,而下面的self依然要消费掉
objc_storeStrong(self, 0x0); // 对应__attribute__((ns_consumes_self))
return 0x0;
}

至此,过度释放的原因也就清楚了,那么该怎么解决呢?

解决方案

回到文章开头,再看下代码,不难发现,我们只要模仿ARC在init方法调用前插入个retain,并在主调方快结束的时候再插入个release即可。

Person *myPerson = [Person alloc];
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@16@0:8"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = myPerson;
invocation.selector = @selector(initPerson);
CFBridgingRetain(myPerson); // 代替ARC将owneship将传递给被调方
[invocation invoke];

__unsafe_unretained id retValue;
[invocation getReturnValue:&retValue];

CFBridgingRelease((__bridge CFTypeRef)retValue); // 代替ARC来释放ns_returns_retained结果

如果init方法返回nil,即retValue=nil,则CFBridgingRelease不会生效,上面插的那个CFBridgingRetain也就完美抵消掉了init方法结束时的release。

链接:https://www.jianshu.com/p/51adf5b44588

收起阅读 »

视频超过三十秒后再接受 无数据

视频超过三十秒后再接受 无数据

视频超过三十秒后再接受 无数据


Charles使用教程汇总

Charles教程汇总 1.  简介    ●    Charles,一款代理抓包工具,可以分析和排查网络相关的问题●    支持移动(安卓、iOS)...
继续阅读 »

Charles教程汇总

 

1.  简介

    

    Charles,一款代理抓包工具,可以分析和排查网络相关的问题

    支持移动(安卓、iOS)设备

    以下使用文档,以安卓设备为准,iOS设置大同小异

 

2.  Charles安装

    安装包官方下载:http://www.charlesproxy.com/documentation/

    官方版为试用版,启动时有10s等待时间,每隔30min会提示关闭,重新打开后可以继续使用30min

3.  Charles代理配置使用

1)   打开Charles 

2)   查看本地IP

        

3)   手机连接WiFi(此WiFi需要和电脑在同一网段内,公司内直接连接cheetahmobile即可)

4)   设置手机代理

a)   长按cheetahmobile无线网络

b)   选择修改网络

  

c)   勾选高级选项,将代理设置为手动,填入步骤2获取的IP地址,代理服务器端口默认为8888


d)   手机中打开应用,产生网络请求,电脑端同意连接

e)   如在公司外使用,先将电脑未连接VPN之前的IP设置为手机的代理服务器地址,手机连接电脑之后


5)Access
Control

这个功能可以管理目前连接到你电脑上的设备(包括添加,删除,导入导出配置文件等,另外,Charles还提供了能够让所有设备无须询问直接连接电脑的方式:添加一个0.0.0.0/0的IP即可)

有时可能会遇到手机连接到电脑之后,无访问许可请求,此时可以去access
Control中手动进行添加

对Charles有大量使用需求的同学,时间久了之后,access
control里可能会有很多IP,建议大家在每次的测试开始前,清理IP池,将本次要测试的手机添加到自己的访问名单(使用Charles过程中,需要修改网络请求数据和host,如果不维护IP池,可能会干扰你自己或者别人的测试)

4.  修改默认的8888端口号

    测试过程中,默认代理填入的端口号为8888,设备之间经常借调,会导致其他人依旧连接你的代理

    可以手动修改默认的8888端口号

1)   打开设置面板

2)   修改默认的端口号保存即可

 

5.  设置本地(电脑端)不走Charles代理

    本地走代理部分情况下会导致上网慢(如有针对情况下限速),且电脑端产生的请求较多,容易刷屏

1)   Proxy--Proxy
Setting… 

2)  
定位到WindowsMozilla FireFox

3)   将下方的复选框取消勾选后保存

4)   以后再次启动Charles后,电脑端的所有请求就不会被Charles抓包 

 

6.  设置关注的域名

    抓包域名比较多情况下,容易刷屏,可以将需要测试的域名添加到关注列表,方便查看

1)   产生的网络请求中,选中后右键选择Focus

2)   再次产生改请求的情况下就会在前面,而未关注的域名就会被分配到Other
Hosts下


 

7.  域名重定向(A-->B)

    测试过程中客,可能会将户端A域名的请求访问到B域名上

1)   找到需要重定向(A)的域名,右键选择Map Remote...

        

2)   填入对应域名的信息后保存(A-->B)


3)   客户端再次请求,下发的域名变为B

        

       

8.  域名重定向(A-->本地文件)

    测试过程中,需要看客户端对服务端数据的容错,如果服务端没有给脏数据,则需要本地模拟脏数据

    需要对一个字段进行多次校验工作,服务端配置麻烦可以采用本地方式

1)   将正常访问的域名的response数据,复制保存到本地(格式无所谓,txt、json都可以)

2)   选择域名,右键访问的链接,选择Map Local

3)   在Local Path路径中,将本地保存的数据选中

4)   客户端再次请求时,访问的数据即为本地数据


 

9.  查看已配置的重定向设置

    查看和取消Map Local和Map Remote已经设置的重定向

1)   点击菜单中的Tools

2)   需要取消重定向设置,将已配置的数据删除或者选择Enable
Map
Local开关即可


10. 设置限速

    查看网络加载慢情况下,客户端的容错及反馈情况

1)   打开限速设置窗口

        

2)   限速可以针对选定的域名,也可以针对生效

3)   也可以参照Facebook开源的方案进行设置

a)   限速提供了通用的方案,比如Custom,3G等方案

b)   可以手动设置解决方案 


 

海外项目,建议参照Facebook的ATC解决方案来模拟

    Facebook
ATC提供了将近10种类型的网络参数设置,包含了发达国家,发展中国家,郊区、市区等网络情况


 

11. https解密抓包

    安全起见,公司部分域名采用了https的方式,https是加密,常规情况下无法抓到包请求


1)   开启Charles

2)   电脑端安装Charles证书

a)  
Help--SSL
Proxying--install Charles Root Certificate


b)   安装证书


c)   证书安装到“受新人的发布者”下


3)   手机端安装电脑证书

a)   手机访问域名:http://www.charlesproxy.com/getssl/

b)  
部分浏览器如果提示证书下载失败,就换个浏览器,目前QQ浏览器亲测有效

c)   安装证书,证书名字随便写

d)   安装证书需要设置锁屏密码

4)   安装成功后,Charles客户端开启对应的域名SSL


5)   设置对应域名的网络连接,https默认端口为443


6)   设置成功后,访问域名,即可查看解密后的请求状况


12. 重复请求

    对于一些客户端不容易触发的请求,可以通过charles中repeat功能进行重复请求,简单方便

1)   选中请求的URL,右键选择Repeat


2)   即可查看请求的结果

 

13. 对接口进行压力测试

 

1)   选中要进行压测的接口,右键选择Repeat Advanced…


2)   填入重复执行的次数和并发数

a)   Itreations:循环次数

b)   Concurrency:并发次数

c)   Delays,请求与请求之间的间隔时间

    并发代表是统一时间内请求多少次,比如设置循环6次,每次并发3条,则会分2次,每次并发3条去向服务端进行请求(需要注意的是,如果循环次数不是并发次数的整数倍,则不会触发所有的请求,如设置循环次数为10,并发条数为3,那么最终只会发起9次请求)

 


3)   查看测试结果


14. 修改请求参数之Edit

    验证不同请求参数下,接口是否返回对应的数据

    比如发魔方数据,限制了MCC为460以内的生效,那就可以改MCC为非460,看是否还能请求到对应的开关信息

1)   选中对应请求,选择Edit


2)   修改参数请求参数后点Execute


3)   重新请求后,请求参数中就包含了对应的参数


 

15. 修改请求参数之Rewrite 

    客户端的云端开关,大多是通过魔方下发,不同的MCC,语言和aid会下发不同的数据,客户端如果要拉取不同的配置时,需要修改这些参数。修改MCC和aid还需要在root的设备上使用三方工具,随着Android版本的升级,部分参数甚至无法修改

    广告和新闻的数据,会区分国际进行投放,有时甚至只会针对特定的国家(如印度新闻)投放,客户端为了测试这些功能,需要借助VPN或者debug版本

    与MCC和aid修改器说再见

1)   打开Rewrite设置


2)   添加一条配置信息

a)   打开Enable Rewite功能开关


b)   添加一条配置信息

c)   Location中,添加域名的详细信息


3)   对域名添加对应的规则

a)  修改URL中的请求参数,比如MCC,aid等,Type选择:Modify
Query Param

b)  修改URL中的地域,比如添加某个国家的IP,Type选择:Add
Header


   举个栗子:

   例子1:客户端需要请求只针对MCC为310且aid尾号为1的用户下发的魔方云端配置

1.   配置魔方域名


2.   Type选择:Modify Query Param,并填写对应的参数

        


3.   打开开关,客户端再次发生请求



 

   例子2:客户端需求请求只针对印度IP下发的picks广告数据

1.   配置对应域名的数据


2.   新增一个请求参数


3.   再次请求对应链接


 

   例子3:客户端需要请求只针对英国IP下发的新闻数据

1.   配置对应域名的数据


2.   选择Type为Add Header


3.   客户端再去触发请求,抓包查看X-Forwarded-For已修改为2.101.8.8


 

16. session的操作

                    Charles支持同时打开多个session,但新发起的网络请求,只会在最后建议的session中进行记录

                    Charles还支持将session进行保存,在需要的时候可以将session作为Charles的日志提供给其他需要的人进行查看

17. 两种数据查看方式:structur和sequence


18. 复制和保存请求内容

            
1.    
在某个请求上右键选择“copy
URL”,可以将本次请求的完整URL复制出来

            
2.    
在某个请求上右键选择“copy
response”,可以将本次返回数据的完整内容复制出来

            
3.    
在某个请求上右键选择“save
response”,可以将本次返回数据的完整内容以文件的形式保存在本地

19. 选择内容查看方式

            
1.    
在某个请求上右键选择“view
response as”并进一步选择需要的数据查看方式(有时候返回的内容,Charles不能直接提供json的查看方式,可以用这个功能来强行查看josn格式)

            
2.    
同理,可以在某个请求上右键选择“view
request as”并进一步选择需要的数据查看方式

 

     20.对添加ignore的域名取消忽略

            
1.    
忽略对应的域名


            
2.    
进入Proxy--Recording Settings,进入Exclude取消remove即可


收起阅读 »

iOS-TCP网络框架(二)

现在我们已经有了TCP连接, Request, Response和Task, 接下来要做的就是把这一切串起来. 具体来说, 我们需要一个管理方建立并管理TCP连接, 提供接口让调用方通过Request向连接中写入数据, 监听连接中读取到的粘包数据并将数据拆分成...
继续阅读 »

现在我们已经有了TCP连接, Request, Response和Task, 接下来要做的就是把这一切串起来. 具体来说, 我们需要一个管理方建立并管理TCP连接, 提供接口让调用方通过Request向连接中写入数据, 监听连接中读取到的粘包数据并将数据拆分成单个Response返回给调用方.

TCP连接部分比较简单, 这里我们直接跳过, 从发起数据请求部分开始.

发起数据请求

站在调用方的角度, 发起一个TCP请求与发起一个HTTP请求并没有什么区别. 调用方通过Request提供URL和相应参数, 然后通过completionHandler回调处理请求对应的响应数据, 就像这样:


// SomeViewController.m

- (void)fetchData {

HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:aTCPUrl parameters:someParams header:someHeader];
HHTCPSocketTask *task = [[HHTCPSocketClient sharedInstance] dataTaskWithRequest:request completionHandler:^(NSError *error, id result) {
if (error) {
//handle error
} else {
//handle result
}
}
[task resume];
}
站在协议实现方的角度, 发起网络请求做的事情会多一些. 我们需要将调用方提供的Request和completionHandler打包成一个Task并保存起来, 当调用方调用Task.resume时, 我们再将Request.data写入Socket. 这部分的主要代码如下:

//HHTCPSocketClient.m

@interface HHTCPSocketClient()<HHTCPSocketDelegate>

@property (nonatomic, strong) HHTCPSocket *socket;

//任务派发表 以序列号为键保存所有已发出但还未收到响应的Request 待收到响应后再根据序列号一一分发
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, HHTCPSocketTask *> *dispatchTable;

...其他逻辑 略
@end

@implementation HHTCPSocketClient

...其他逻辑 略

#pragma mark - Interface(Public)

//新建数据请求任务 调用方通过此接口定义Request的收到响应后的处理逻辑
- (HHTCPSocketTask *)dataTaskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {

__block NSNumber *taskIdentifier;
//1\. 根据Request新建Task
HHTCPSocketTask *task = [HHTCPSocketTask taskWithRequest:request completionHandler:^(NSError *error, id result) {

//4\. Request已收到响应 从派发表中删除
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self.dispatchTable removeObjectForKey:taskIdentifier];
dispatch_semaphore_signal(lock);

!completionHandler ?: completionHandler(error, result);
}];
//2\. 设置Task.client为HHTCPSocketClient 后续会通过Task.client向Socket中写入数据
task.client = self;
taskIdentifier = task.taskIdentifier;

//3\. 将Task保存到派发表中
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self.dispatchTable setObject:task forKey:taskIdentifier];
dispatch_semaphore_signal(lock);

return task;
}

- (NSNumber *)dispatchTask:(HHTCPSocketTask *)task {
if (task == nil) { return @-1; }

[task resume];// 通过task.resume接口发起请求 task.resume会调用task.client.resumeTask方法 task.client就是HHTCPSocketClient
return task.taskIdentifier;
}

#pragma mark - Interface(Friend)

//最终向Socket中写入Request.data的地方 此接口只提供给HHTCPSocketTask使用 对外不可见
- (void)resumeTask:(HHTCPSocketTask *)task {

// 向Socket中写入Request格式化好的数据
if (self.socket.isConnected) {
[self.socket writeData:task.request.requestData];
} else {

NSError *error;
if (self.isNetworkReachable) {
error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorTimeOut);
} else {
error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorCannotConnectedToInternet);
}
[task completeWithResponseData:nil error:error];
}
}

@end

//HHTCPSocketTask.m

@interface HHTCPSocketTask ()

- (void)setClient:(id)client;//此接口仅提供给上面的HHTCPSocketClient使用 对外不可见

@end

//对外接口 调用方通过通过此接口发起Request
- (void)resume {
...其他逻辑 略

//通知client将task.request的数据写入Socket
[self.client resumeTask:self];
}

简单描述一下代码流程:

  1. 调用方提供Request和completionHandler回调从HHTCPSocketClient获得一个打包好的Task(通过dataTaskWithRequest:completionHandler:接口), HHTCPSocketClient内部会以(Request.serNum: Task)的形式将其保存在dispatchTable中.

  2. 调用方通过Task.resume发起TCP请求, 待收到服务端响应后HHTCPSocketClient会根据Response.serNum从dispatchTable取出Task然后执行调用方提供的completionHandler回调.(这里为了和系统的NSURLSessionTask保持一致的接口, 我给TCPClient和TCPTask加了一些辅助方法, 代码上绕了一个圈, 实际上, Task.resume就是Socket.writeData:Task.Request.Data).

处理请求响应

正常情况下, 请求发出后, 很快就就会收到服务端的响应二进制数据, 我们要做的就是, 从这些二进制数据中切割出单个Response报文, 然后一一进行分发. 代码如下:


//HHTCPSocketClient.m

@interface HHTCPSocketClient()<HHTCPSocketDelegate>

//保存所有收到的服务端数据 等待解析
@property (nonatomic, strong) NSMutableData *buffer;
...其他逻辑 略
@end

#pragma mark - HHTCPSocketDelegate

//从Socket从读取到数据
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
[self.buffer appendData:data]; //1\. 保存读取到的二进制数据

[self readBuffer];//2\. 根据协议解析二进制数据
}

#pragma mark - Parse

//递归截取Response报文 因为读取到的数据可能已经"粘包" 所以需要递归
- (void)readBuffer {
if (self.isReading) { return; }

self.isReading = YES;
NSData *responseData = [self getParsedResponseData];//1\. 从已读取到的二进制中截取单个Response报文数据
[self dispatchResponse:responseData];//2\. 将Response报文派发给对应的Task
self.isReading = NO;

if (responseData.length == 0) { return; }
[self readBuffer]; //3\. 递归解析
}

//根据定义的协议从buffer中截取出单个Response报文
- (NSData *)getParsedResponseData {

NSData *totalReceivedData = self.buffer;
//1\. 每个Response报文必有的16个字节(url+serNum+respCode+contentLen)
uint32_t responseHeaderLength = [HHTCPSocketResponseParser responseHeaderLength];
if (totalReceivedData.length < responseHeaderLength) { return nil; }

//2\. 根据定义的协议读取出Response.content的长度
NSData *responseData;
uint32_t responseContentLength = [HHTCPSocketResponseParser responseContentLengthFromData:totalReceivedData];
//3\. Response.content的长度加上必有的16个字节即为整个Response报文的长度
uint32_t responseLength = responseHeaderLength + responseContentLength;
if (totalReceivedData.length < responseLength) { return nil; }

//4\. 根据上面解析出的responseLength截取出单个Response报文
responseData = [totalReceivedData subdataWithRange:NSMakeRange(0, responseLength)];
self.buffer = [[totalReceivedData subdataWithRange:NSMakeRange(responseLength, totalReceivedData.length - responseLength)] mutableCopy];
return responseData;
}

//将Response报文解析Response 然后交由对应的Task进行派发
- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }

if (response.url > TCP_max_notification) {/** 请求响应 */

HHTCPSocketTask *task = self.dispatchTable[@(response.serNum)];
[task completeWithResponse:response error:nil];
} else {/** 推送或心跳 略 */
...
}
}

简单描述下代码流程:

  1. TCPClient监听Socket读取数据回调方法, 将读取到的服务端二进制数据添加到buffer中.

  2. 根据定义的协议从buffer头部开始, 不停地截取出单个Response报文, 直到buffer数据取无可取.

  3. 从2中截取到的Response报文中解析出Response.serNum, 根据serNum从dispatchTable中取出对应的Task(Response.serNum == Request.serNum), 将Response交付给Task. 至此, TCPClient的工作完成.

  4. Task拿到Response后通过completionHandler交付给调用方. 至此, 一次TCPTask完成.

这里需要注意的是, Socket的回调方法我这边默认都是在串行队列中执行的, 所以对buffer的操作并不没有加锁, 如果是在并行队列中执行Socket的回调, 请记得对buffer操作加锁.

处理后台推送

除了Request对应的Response, 服务端有时也会主动发送一些推送数据给客户端, 我们也需要处理一下:


//HHTCPSocketClient.m

- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }

if (response.url > TCP_max_notification) {/** 请求响应 略*/
//...
} else if (response.url == TCP_heatbeat) {/** 心跳 略 */
//...
} else {/** 推送 */
[self dispatchRemoteNotification:response];
}
}

//各种推送 自行处理
- (void)dispatchRemoteNotification:(HHTCPSocketResponse *)notification {

switch (notification.url) {
case TCP_notification_xxx: ...
case TCP_notification_yyy: ...
case TCP_notification_zzz: ...
default:break;
}
}

请求超时和取消

TCP协议的可靠性规定了数据会完整的, 有序的进行传输, 但并未规定数据传输的最大时长. 这意味着, 从发起Request到收到Response的时间间隔可能比我们能接受的时间间隔要长. 这里我们也简单处理一下, 代码如下:


//HHTCPSocketTask.m

#pragma mark - Interface

- (void)cancel {
if (![self canResponse]) { return; }

self.state = HHTCPSocketTaskStateCanceled;
[self completeWithResult:nil error:[self taskErrorWithResponeCode:HHNetworkTaskErrorCanceled]];
}

- (void)resume {
if (self.state != HHTCPSocketTaskStateSuspended) { return; }

//发起Request的同时也启动一个timer timer超时直接返回错误并忽略后续的Response
self.timer = [NSTimer scheduledTimerWithTimeInterval:self.request.timeoutInterval target:self selector:@selector(requestTimeout) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

self.state = HHTCPSocketTaskStateRunning;
[self.client resumeTask:self];
}

#pragma mark - Action

- (void)requestTimeout {
if (![self canResponse]) { return; }

self.state = HHTCPSocketTaskStateCompleted;
[self completeWithResult:nil error:[self taskErrorWithResponeCode:HHNetworkTaskErrorTimeOut]];
}

#pragma mark - Utils

- (BOOL)canResponse {
return self.state <= HHTCPSocketTaskStateRunning;
}

代码很简单, 只是在写入Task.Request的同时也开启一个timer, timer超时就直接忽略Response并返回错误给调用方而已. 对于类似HTTP的GET请求而言, 忽略和取消几乎是等价的. 但对于POST请求而言, 我们需要的可能就是直接断开连接了

心跳

目前为止, 我们已经有了一个简单的TCP客户端, 它可以发送数据请求, 接收数据响应, 还能处理服务端推送. 最后, 我们做一下收尾工作: 心跳

单向的心跳就不说了, 这里我们给到一张Ping-Pong的简易图:




当发送方为客户端时, Ping-Pong通常用来验证TCP连接的有效性. 具体来说, 如果Ping-Pong正常, 那么证明连接有效, 数据传输没有问题, 反之, 要么连接已断开, 要么连接还在但服务器已经过载无力进行恢复, 此时客户端可以选择断开重连或者切换服务器.

当发送方为服务端时, Ping-Pong通常用来验证数据传输的即时性. 具体来说, 当服务端向客户端发送一条即时性消息时通常还会马上Ping一下客户端, 如果客户端即时进行回应, 那么说明Ping之前的即时性消息已经到达, 反之, 消息不够即时, 服务端可能会走APNS再次发送该消息.

Demo中我简单实现了一下Ping-Pong, 代码如下:


//HHTCPSocketHeartbeat

static NSUInteger maxMissTime = 3;
@implementation HHTCPSocketHeartbeat

+ (instancetype)heartbeatWithClient:(id)client timeoutHandler:(void (^)(void))timeoutHandler {

HHTCPSocketHeartbeat *heartbeat = [HHTCPSocketHeartbeat new];
heartbeat.client = client;
heartbeat.missTime = -1;
heartbeat.timeoutHandler = timeoutHandler;
return heartbeat;
}

- (void)start {

[self stop];
self.timer = [NSTimer timerWithTimeInterval:60 target:self selector:@selector(sendHeatbeat) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)stop {
[self.timer invalidate];
}

- (void)reset {
self.missTime = -1;
[self start];
}

- (void)sendHeatbeat {

self.missTime += 1;
if (self.missTime >= maxMissTime && self.timeoutHandler != nil) {//心跳超时 执行超时回调
self.timeoutHandler();
self.missTime = -1;
}

HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:TCP_heatbeat parameters:@{@"ackNum": @(TCP_heatbeat)} header:nil];
[self.client dispatchDataTaskWithRequest:request completionHandler:nil];
}

- (void)handleServerAckNum:(uint32_t)ackNum {
if (ackNum == TCP_heatbeat) {//服务端返回的心跳回应Pong 不用处理
self.missTime = -1;
return;
}

//服务端发起的Ping 需要回应
HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:TCP_heatbeat parameters:@{@"ackNum": @(ackNum)} header:nil];
[self.client dispatchDataTaskWithRequest:request completionHandler:nil];
}

@end

HHTCPSocketHeartbeat每隔一段时间就会发起一个serNum固定为1的心跳请求Ping一下服务端, 在超时时间间隔内当收到任何服务端回应, 我们认为连接有效, 心跳重置, 否则执行调用方设置的超时回调. 另外, HHTCPSocketHeartbeat还负责回应服务端发起的serNum为随机数的即时性Response(这里的随机数我给的是时间戳).

//HHTCPSocketClient.m

- (void)configuration {

self.heatbeat = [HHTCPSocketHeartbeat heartbeatWithClient:self timeoutHandler:^{//客户端心跳超时回调
// [self reconnect];
SocketLog(@"heartbeat timeout");
}];
}

#pragma mark - HHTCPSocketDelegate

- (void)socket:(HHTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
[self.heatbeat reset];//连接成功 客户端心跳启动
}

- (void)socketDidDisconnect:(HHTCPSocket *)sock error:(NSError *)error {
[self.heatbeat stop];//连接断开 客户端心跳停止
}

- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
[self.heatbeat reset];//收到服务端数据 说明连接有效 重置心跳
//...其他 略
}

//获取到服务端Response
- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }

if (response.url == TCP_heatbeat) {/** 心跳 */
[self.heatbeat handleServerAckNum:response.serNum];//回复服务端心跳请求 如果有必要的话
}
}
文件下载/上传?

到目前为止, 我们讨论的都是类似DataTask的数据请求, 并未涉及到文件下载/上传请求, 事实上, 我也没打算在通讯协议上加上这两种请求的支持. 这部分我是这样考虑的:

如果传输的文件比较小, 那么仿照HTTP直接给协议加上ContentType字段, Content以特殊分隔符进行分隔即可.

如果传输的文件比较大, 那么直接在当前连接进行文件传输可能会阻塞其他的数据传输, 这是我们不希望看到的, 所以一定是另起一条连接专用于大文件传输. 考虑到文件传输不太可能像普通数据传输那样需要即时性和服务端推送, 为了节省服务端开销, 文件传输完成后连接也没有必要继续保持. 这里的"建立连接-文件传输-断开连接"其实已经由HTTP实现得很好了, 而且功能还多, 我们没必要再做重复工作.

基于以上考虑, 文件传输这块我更趋向于直接使用HTTP而不是自行实现.

至此, TCP部分的讨论就结束了.



作者:Cooci
链接:https://www.jianshu.com/p/c0df2690e9d4



收起阅读 »

iOS-TCP网络框架(一)

TCP概述TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC793定义. 在因特网协议族中,TCP属于传输层, 位于网络层之上,应用层之下.需要注意的是, TCP只是协议声明, 仅对外声明协议提供的功能, 但本身并不进行任何实现. ...
继续阅读 »
TCP概述

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC793定义. 在因特网协议族中,TCP属于传输层, 位于网络层之上,应用层之下.

需要注意的是, TCP只是协议声明, 仅对外声明协议提供的功能, 但本身并不进行任何实现. 因此, 在介绍通信协议时, 通常我们还会提及另一个术语: Socket.
Socket并不是一种协议, 而是一组接口(即API). 协议的实现方通过Socket对外提供具体的功能调用. TCP协议的实现方提供的接口就是TCPSocket, UDP协议的实现方提供的接口就是UDPSocket...

通常, 协议的使用方并不直接面对协议的实现方, 而是通过对应的Socket使用协议提供的功能. 因此, 即使以后协议的底层实现进行了任何改动, 但由于对外的接口Socket不变, 使用方也不需要做出任何变更.

TCP协议基于IP协议, 而IP协议属于不可靠协议, 要在一个不可靠协议的的基础上实现一个可靠的数据传输协议是困难且复杂的, TCP的定义者也并不指望所有程序员都能自行实现一遍TCP协议. 所以, 与其说本文是在介绍TCP编程, 倒不如说是介绍TCPSocket编程.

建立通讯连接

通过Socket建立TCP连接是非常简单的, 连接方(客户端)只需要提供被连接方(服务端)的IP地址和端口号去调用连接接口即可, 被连接方接受连接的话, 接口会返回成功, 否则返回失败, 至于底层的握手细节, 双方完全不用关心. 但考虑到网络波动, 前后台切换, 服务器重启等等可能导致的连接主动/被动断开的情况, 客户端这边我会加上必要的重连处理. 主要代码如下:


//HHTCPSocket.h

@class HHTCPSocket;
@protocol HHTCPSocketDelegate <NSObject>

@optional
- (void)socket:(HHTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; //连接成功

- (void)socketCanNotConnectToService:(HHTCPSocket *)sock; //重连失败
- (void)socketDidDisconnect:(HHTCPSocket *)sock error:(NSError *)error; //连接失败并开始重连

@end

@interface HHTCPSocket : NSObject

@property (nonatomic, weak) id<HHTCPSocketDelegate> delegate;
@property (nonatomic, assign) NSUInteger maxRetryTime; //最大重连次数

- (instancetype)initWithService:(HHTCPSocketService *)service; //service提供ip地址和端口号

- (void)close;
- (void)connect; //连接
- (void)reconnect; //重连
- (BOOL)isConnected;

@end

//HHTCPSocket.m

@implementation HHTCPSocket

- (instancetype)initWithService:(HHTCPSocketService *)service {
if (self = [super init]) {
self.service = service ?: [HHTCPSocketService defaultService];

//1\. 初始化Socket
const char *delegateQueueLabel = [[NSString stringWithFormat:@"%p_socketDelegateQueue", self] cStringUsingEncoding:NSUTF8StringEncoding];
self.reconnectTime = self.maxRetryTime;
self.delegateQueue = dispatch_queue_create(delegateQueueLabel, DISPATCH_QUEUE_SERIAL);
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.delegateQueue];

//2\. 初始化Socket连接线程
self.machPort = [NSMachPort port];
self.keepRuning = YES;
self.socket.IPv4PreferredOverIPv6 = NO; //支持ipv6
[NSThread detachNewThreadSelector:@selector(configSocketThread) toTarget:self withObject:nil];

//3\. 处理网络波动/前后台切换
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivedNetworkChangedNotification:) name:kRealReachabilityChangedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivedAppBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
}
return self;
}

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark - Interface

- (void)connect {
if (self.isConnecting || !self.isNetworkReachable) { return; }
self.isConnecting = YES;

[self disconnect];

//去Socket连接线程进行连接 避免阻塞UI
BOOL isFirstTimeConnect = (self.reconnectTime == self.maxRetryTime);
int64_t delayTime = isFirstTimeConnect ? 0 : (arc4random() % 3) + 1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_global_queue(2, 0), ^{
[self performSelector:@selector(connectOnSocketThread) onThread:self.socketThread withObject:nil waitUntilDone:YES];
});
}

- (void)reconnect {

self.reconnectTime = self.maxRetryTime;
[self connect];
}

- (void)disconnect {
if (!self.socket.isConnected) { return; }

[self.socket setDelegate:nil delegateQueue:nil];
[self.socket disconnect];
}

- (BOOL)isConnected {
return self.socket.isConnected;
}

#pragma mark - GCDAsyncSocketDelegate

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
//连接成功 通知代理方
if ([self.delegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) {
[self.delegate socket:self didConnectToHost:host port:port];
}

self.reconnectTime = self.maxRetryTime;
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {

if ([self.delegate respondsToSelector:@selector(socketDidDisconnect:error:)]) {
[self.delegate socketDidDisconnect:self error:error];
}
[self tryToReconnect];//连接失败 尝试重连
}

#pragma mark - Action

- (void)configSocketThread {

if (self.socketThread == nil) {
self.socketThread = [NSThread currentThread];
[[NSRunLoop currentRunLoop] addPort:self.machPort forMode:NSDefaultRunLoopMode];
}
while (self.keepRuning) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

[[NSRunLoop currentRunLoop] removePort:self.machPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]];
[self.socketThread cancel];
self.socket = nil;
self.machPort = nil;
self.socketThread = nil;
self.delegateQueue = nil;
}

- (void)connectOnSocketThread {//实际的调用连接操作在这里

[self.socket setDelegate:self delegateQueue:self.delegateQueue];
[self.socket connectToHost:self.service.host onPort:self.service.port error:nil];
self.isConnecting = NO;
}

#pragma mark - Notification

- (void)didReceivedNetworkChangedNotification:(NSNotification *)notif {
[self reconnectIfNeed];
}

- (void)didReceivedAppBecomeActiveNotification:(NSNotification *)notif {
[self reconnectIfNeed];
}

#pragma mark - Utils

- (void)tryToReconnect {
if (self.isConnecting || !self.isNetworkReachable) { return; }

self.reconnectTime -= 1;
if (self.reconnectTime >= 0) {
[self connect];
} else if ([self.delegate respondsToSelector:@selector(socketCanNotConnectToService:)]) {
[self.delegate socketCanNotConnectToService:self];
}
}

- (NSUInteger)maxRetryTime {
return _maxRetryTime > 0 ? _maxRetryTime : 5;
}

@end

这边因为需要添加重连操作, 所以我在GCDAsyncSocket的基础上又封装了一下, 但总体代码不多, 应该比较好理解. 这里需要注意的是GCDAsyncSocket的连接接口(connectToHost: onPort: error:)是同步调用的, 慢网情况下可能会阻塞线程一段时间, 所以这里我单开了一个线程来做连接操作.

连接建立以后, 就可以读写数据了, 写数据的接口如下:


- (void)writeData:(NSData *)data {
if (!self.isConnected || data.length == 0) { return; }

[self.socket writeData:data withTimeout:-1 tag:socketTag];
}

 //连接成功...
while (1) {

Error *error;
Data *readData = [socket readToLength:1024 error:&error];//同步 读不到数据就阻塞
if (error) { return; }

[self handleData:readData];//同步异步皆可 多为异步
}

具体到我们的代码中, 则是这个样子:
// HHTCPSocket.h

@protocol HHTCPSocketDelegate <NSObject>
//...其他回调方法
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data;//读取到数据回调方法
@end

// HHTCPSocket.m

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
//Socket连接成功 开始读数据
[self.socket readDataWithTimeout:-1 tag:socketTag];
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
//Socket写数据成功 继续读取数据
[self.socket readDataWithTimeout:-1 tag:socketTag];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {

//从Socket中读到数据 交由调用方处理
if ([self.delegate respondsToSelector:@selector(socket:didReadData:)]) {
[self.delegate socket:self didReadData:data];
}
[self.socket readDataWithTimeout:-1 tag:socketTag];//继续读取数据
}

现在我们已经可以通过Socket建立一条会自动重连的TCP连接, 然后还可以通过Socket从连接中读写数据, 接下来要做的就是定义一套自己的通讯协议了.

定义通讯协议
  • 为什么需要定义通讯协议

TCP协议定义了连接双方以字节流而不是报文段的方式进行数据传输, 这意味着任何应用层报文(image/text/html...)想要通过TCP进行传输都必须先转化成二进制数据. 另外, TCP实现出于传输效率考虑, 往往会在连接两端各自开辟一个发送数据缓冲区和一个接收数据缓冲区. 因此, 有时应用层通过Socket向连接中写入数据时, 数据其实并没有立即被发送, 而是被放入缓冲区等待合适的时机才会真正的发送. 理想情况下, TCP进行传输数据的流程可能像这样:




但实际情况中, 因为Nagle算法/网络拥堵/拥塞控制/接收方读取太慢等等各种原因, 数据很有可能会在发送缓冲区/接收缓冲区被累积. 所以, 上面的流程更可能是这样:




或者这样:




上面的图都假设应用层报文不到一个MSS(一个MSS一般为1460字节, 这对大部分非文件请求来说都足够了), 当报文超过一个MSS时, TCP底层实现会对报文进行拆分后多次传输, 这会稍微复杂些(不想画图了), 但最后导致的问题是一致的, 解决方案也是一致的.

从上面的图容易看出, 无论数据在发送缓冲区还是接收缓冲区被累积, 对于接收方程序来说都是一样的: 多个应用层报文不分彼此粘作一串导致数据无法还原(粘包).

得益于TCP协议是可靠的传输协议(可靠意味着TCP实现会保证数据不会丢包, 也不会乱序), 粘包的问题很好处理. 我们只需要在发送方给每段数据都附上一份描述信息(描述信息主要包括数据的长度, 解析格式等等), 接收方就可以根据描述信息从一串数据流中分割出单独的每段应用层报文了.

被传输数据和数据的描述一起构成了一段应用层报文, 这里我们称实际想传输的数据为报文有效载荷, 而数据的描述信息为报文头部. 此时, 数据的传输流程就成了这样:




  • 定义一个简单的通讯协议

自定义通讯协议时, 往往和项目业务直接挂钩, 所以这块其实没什么好写的. 但为了继续接下来的讨论, 这里我会给到一个非常简单的Demo版协议, 它长这样:


因为客户端和服务端都可以发送和接收数据, 为了方便描述, 这里我们对客户端发出的报文统一称为Request, 服务端发出的报文统一称为Response.

这里需要注意的是, 这里的Request和Response并不总是一一对应, 比如客户端单向的心跳请求报文服务端是不会响应的, 而服务端主动发出的推送报文也不是客户端请求的.

Request由4个部分组成:

  1. url: 类似HTTP中的统一资源定位符, 32位无符号整数(4个字节). 用于标识客户端请求的服务端资源或对资源进行的操作. 由服务端定义, 客户端使用.

  2. content(可选): 请求携带的数据, 0~N字节的二进制数据. 用于携带请求传输的内容, 传输的内容目前是请求参数, 也可能什么都没有. 解析格式固定为JSON.

  3. serNum: 请求序列号, 32位无符号整数(4个字节). 用于标示请求本身, 每个请求对应一个唯一的序列号, 即使两个请求的url和content都相同. 由客户端生成并传输, 服务端解析并回传. 客户端通过回传的序列号和请求序列号之间的对应关系进行响应数据分发.

  4. contentLen: 请求携带数据长度, 32位无符号整数(4个字节). 用于标示请求携带的数据的长度. 服务端通过contentLen将粘包的数据进行切割后一一解析并处理.

Response由5个部分组成:

  1. url: 同Request.

  2. respCode: 类似HTTP状态码, 32位无符号整数(4个字节).

  3. content(可选): 响应携带的数据, 0~N字节的二进制数据. 携带的数据可能是某个Request的响应数据, 也可能是服务端主动发出的推送数据, 或者, 什么都没有. 解析格式固定为JSON.

  4. serNum: 该Response所对应的Request序列号, 32位无符号整数(4个字节). 若Response并没有对应的Request(比如推送), Response.serNum==Response.url.

  5. contentLen: Response携带的数据长度, 32位无符号整数(4个字节). 用于标示Response携带的数据的长度. 客户端通过contentLen将粘包的数据进行切割后一一解析并处理.

因为只是Demo用, 这个协议会比较随意. 但在实际开发中, 我们应该尽量参考那些成熟的应用层协议(HTTP/FTP...). 比如考虑到后续的业务变更, 应该加上Version字段. 加上ContentType字段以传输其他类型的数据, 压缩字段字节数以节省流量...等等.


实现通讯协议

有了协议以后, 就可以写代码进行实现了. Request部分主要代码如下:

//HHTCPSocketRequest.h

/** URL类型肯定都是后台定义的 直接copy过来即可 命名用后台的 方便调试时比对 */
typedef enum : NSUInteger {
TCP_heatbeat = 0x00000001,
TCP_notification_xxx = 0x00000002,
TCP_notification_yyy = 0x00000003,
TCP_notification_zzz = 0x00000004,

/* ========== */
TCP_max_notification = 0x00000400,
/* ========== */

TCP_login = 0x00000401,
TCP_weibo_list_public = 0x00000402,
TCP_weibo_list_followed = 0x00000403,
TCP_weibo_like = 0x00000404
} HHTCPSocketRequestURL;

+ (instancetype)requestWithURL:(HHTCPSocketRequestURL)url parameters:(NSDictionary *)parameters header:(NSDictionary *)header;


//HHTCPSocketRequest.m

+ (instancetype)requestWithURL:(HHTCPSocketRequestURL)url parameters:(NSDictionary *)parameters header:(NSDictionary *)header {

NSData *content = [parameters yy_modelToJSONData];
uint32_t requestIdentifier = [self currentRequestIdentifier];

HHTCPSocketRequest *request = [HHTCPSocketRequest new];
request.requestIdentifier = @(requestIdentifier);
[request.formattedData appendData:[HHDataFormatter msgTypeDataFromInteger:url]];/** 请求URL */
[request.formattedData appendData:[HHDataFormatter msgSerialNumberDataFromInteger:requestIdentifier]];/** 请求序列号 */
[request.formattedData appendData:[HHDataFormatter msgContentLengthDataFromInteger:(uint32_t)content.length]];/** 请求内容长度 */

if (content != nil) { [request.formattedData appendData:content]; }/** 请求内容 */
return request;
}

+ (uint32_t)currentRequestIdentifier {

static uint32_t currentRequestIdentifier;
static dispatch_semaphore_t lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

currentRequestIdentifier = TCP_max_notification;
lock = dispatch_semaphore_create(1);
});

dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
if (currentRequestIdentifier + 1 == 0xffffffff) {
currentRequestIdentifier = TCP_max_notification;
}
currentRequestIdentifier += 1;
dispatch_semaphore_signal(lock);

return currentRequestIdentifier;
}


HHTCPSocketRequest主要做两件事: 1.为每个Request生成唯一序列号; 2. 根据协议定义将应用层数据转化为相应的二进制数据.

应用层数据和二进制数据间的转化由HHDataFormatter完成, 它负责统一数据格式化接口和大小端问题

接下来是Response部分的代码:

//HHTCPSocketResponse.h

@interface HHTCPSocketResponse : NSObject

+ (instancetype)responseWithData:(NSData *)data;

- (HHTCPSocketRequestURL)url;

- (NSData *)content;
- (uint32_t)serNum;
- (uint32_t)statusCode;
@end

//HHTCPSocketResponse.m

+ (instancetype)responseWithData:(NSData *)data {
if (data.length < [HHTCPSocketResponseParser responseHeaderLength]) {
return nil;
}

HHTCPSocketResponse *response = [HHTCPSocketResponse new];
response.data = data;
return response;
}

- (HHTCPSocketRequestURL)url {
if (_url == 0) {
_url = [HHTCPSocketResponseParser responseURLFromData:self.data];
}
return _url;
}

- (uint32_t)serNum {
if (_serNum == 0) {
_serNum = [HHTCPSocketResponseParser responseSerialNumberFromData:self.data];
}
return _serNum;
}

- (uint32_t)statusCode {
if (_statusCode == 0) {
_statusCode = [HHTCPSocketResponseParser responseCodeFromData:self.data];
}
return _statusCode;
}

- (NSData *)content {
return [HHTCPSocketResponseParser responseContentFromData:self.data];
}

@end

HHTCPSocketResponse比较简单, 它只做一件事: 根据协议定义将服务端返回的二进制数据解析为应用层数据.

最后, 为了方便管理, 我们再抽象出一个Task. Task将负责请求状态, 请求超时, 请求回调等等的管理. 这部分和协议无关, 但很有必要.
Task部分的代码如下:


//HHTCPSocketTask.h

typedef enum : NSUInteger {
HHTCPSocketTaskStateSuspended = 0,
HHTCPSocketTaskStateRunning = 1,
HHTCPSocketTaskStateCanceled = 2,
HHTCPSocketTaskStateCompleted = 3
} HHTCPSocketTaskState;

@interface HHTCPSocketTask : NSObject

- (void)cancel;
- (void)resume;

- (HHTCPSocketTaskState)state;
- (NSNumber *)taskIdentifier;

@end

//HHTCPSocketTask.m

//保存Request和completionHandler Request用于将调用方数据写入Socket completionHandler用于将Response交付给调用方
+ (instancetype)taskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {

HHTCPSocketTask *task = [HHTCPSocketTask new];
task.request = request;
task.completionHandler = completionHandler;
task.state = HHTCPSocketTaskStateSuspended;
...其他 略
return task;
}

//处理服务端返回的Response Socket读取到相应的Response报文数据后会调用此接口
- (void)completeWithResponse:(HHTCPSocketResponse *)response error:(NSError *)error {
if (![self canResponse]) { return; }

NSDictionary *result;
if (error == nil) {

if (response == nil) {
error = [self taskErrorWithResponeCode:HHTCPSocketResponseCodeUnkonwn];
} else {

error = [self taskErrorWithResponeCode:response.statusCode];
result = [NSJSONSerialization JSONObjectWithData:response.content options:0 error:nil];
}
}

[self completeWithResult:result error:error];
}

//将处理后的数据交付给调用方
- (void)completeWithResult:(id)result error:(NSError *)error {

...其他 略
dispatch_async(dispatch_get_main_queue(), ^{

!self.completionHandler ?: self.completionHandler(error, result);
self.completionHandler = nil;
});
}



作者:Cooci
链接:https://www.jianshu.com/p/c0df2690e9d4





收起阅读 »

前端面试常问的基础(二)

1. 一个程序至少有一个进程,一个进程至少有一个线程2. 线程的划分尺度小于进程,使得多线程程序的并发性高3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率4. 线程在执行过程中与进程还是有区别的。每个独立的线程...
继续阅读 »

1. 一个程序至少有一个进程,一个进程至少有一个线程

2. 线程的划分尺度小于进程,使得多线程程序的并发性高

3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率

4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制 

5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别

------------

1.IE6或更低版本最多20个cookie

2.IE7和之后的版本最后可以有50个cookie。

3.Firefox最多50个cookie

4.chrome和Safari没有做硬性限制

IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie。


优点:极高的扩展性和可用性


1.通过良好的编程,控制保存在cookie中的session对象的大小。

2.通过加密和安全传输技术(SSL),减少cookie被破解的可能性。

3.只在cookie中存放不敏感数据,即使被盗也不会有重大损失。

4.控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。


缺点:

1.`Cookie`数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。


2.安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。


3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。


本文链接:https://blog.csdn.net/kincaid_z/article/details/116530326

待完善

收起阅读 »

前端面试常问的基础(一)

 IE 盒子模型、标准 W3C 盒子模型;IE的content部分包含了 border 和 padding;new操作符具体干了什么呢?1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型2. 属性和方法被加入到 this ...
继续阅读 »

 IE 盒子模型、标准 W3C 盒子模型;IE的content部分包含了 border 和 padding;


new操作符具体干了什么呢?

1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型

2. 属性和方法被加入到 this 引用的对象中

3. 新创建的对象由 this 所引用,并且最后隐式的返回 this


JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它是基于JavaScript的一个子集。数据格式简单, 易于读写, 占用带宽小


内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。

垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。

1. setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。

2. 闭包

3. 控制台日志

4. 循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)


一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

分为4个步骤:

1. 当发送一个 URL 请求时,不管这个 URL 是 Web 页面的 URL 还是 Web 页面上每个资源的 URL,浏览器都会开启一个线程来处理这个请求,同时在远程 DNS 服务器上启动一个 DNS 查询。这能使浏览器获得请求对应的 IP 地址。

2. 浏览器与远程 Web 服务器通过 TCP 三次握手协商来建立一个 TCP/IP 连接。该握手包括一个同步报文,一个同步-应答报文和一个应答报文,这三个报文在 浏览器和服务器之间传递。该握手首先由客户端尝试建立起通信,而后服务器应答并接受客户端的请求,最后由客户端发出该请求已经被接受的报文。

3. 一旦 TCP/IP 连接建立,浏览器会通过该连接向远程服务器发送 HTTP 的 GET 请求。远程服务器找到资源并使用 HTTP 响应返回该资源,值为 200 的 HTTP 响应状态表示一个正确的响应。

4. 此时,Web 服务器提供资源服务,客户端开始下载资源。


GET:一般用于信息获取,使用URL传递参数,对所发送信息的数量也有限制,一般在2000个字符

POST:一般用于修改服务器上的资源,对所发送的信息没有限制


Ajax 同步和异步的区别:

1. 同步:提交请求 -> 等待服务器处理 -> 处理完毕返回,这个期间客户端浏览器不能干任何事

2. 异步:请求通过事件触发 -> 服务器处理(这是浏览器仍然可以作其他事情)-> 处理完毕


js数组去重

[1,1,2,2,3,3,3,3].filter(function(elem, index, self) {

///结果是true的时候返回后面的值

    return index == self.indexOf(elem);

})


1. XSS

2. sql注入

3. CSRF:是跨站请求伪造,很明显根据刚刚的解释,他的核心也就是请求伪造,通过伪造身份提交POST和GET请求来进行跨域的攻击


完成CSRF需要两个步骤:

1. 登陆受信任的网站A,在本地生成 COOKIE

2. 在不登出A的情况下,或者本地 COOKIE 没有过期的情况下,访问危险网站B。



2.HTTP 报文的组成部分

请求报文

1.请求行:http方法、页面地址、协议、版本

2.请求头:key、value告诉服务端需要内容,注意什么类型

3.空行:告诉服务端请求头已经结束

4.请求体

响应报文

1.状态行:协议、版本、状态码

2.响应头

3.空行

4.响应体:文档部分


  • TCP/IP 四层协议: 应用层、传输层、网络互连层和主机到网络层. http对应应用层
  • ISO 七层模型: 物理层, 数据链路层, 网络层, 传输层, 会话层, 表示层, 应用层.  http对应应用



流行的一些东西:

1. Node.js

2. Mongodb

3. npm

4. MVVM

5. MEAN

6. three.js

7. React

本文链接:https://blog.csdn.net/kincaid_z/article/details/116530326


收起阅读 »

解决js精度丢失办法

很简单一个问题,0.1+0.2,我们肉眼可见的算出来等于0.3,但js是一个神奇的语言,我们在控制台输入0.1+0.2等于0.30000000000000004,为什么会这样尼,我百度了了一下,原因如下:JavaScript 中所有数字包括整数和小数都只有一种...
继续阅读 »

很简单一个问题,0.1+0.2,我们肉眼可见的算出来等于0.3,但js是一个神奇的语言,我们在控制台输入0.1+0.2等于0.30000000000000004,为什么会这样尼,我百度了了一下,原因如下:

JavaScript 中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。0.1的二进制表示的是一个无限循环小数,该版本的 JS 采用的是浮点数标准需要对这种无限循环的二进制进行截取,从而导致了精度丢失,造成了0.1不再是0.1,截取之后0.1变成了 0.100…001,0.2变成了0.200…002。所以两者相加的数大于0.3。

原因就是这么个奇葩,做需求的时候涉及到数字计算,那就得解决它老人家这个毛病,解决这个问题,我一般会封装成一个文件,到后面需要的地方可以模块化引入,并使用

1.判断obj是否为一个整数

export const  isInteger = (obj) => {
return Math.floor(obj) === obj //向下取整就是为了让整数部分截取下来不变
}

2.将一个浮点数转成整数,返回整数和倍数

比如:3.14 -->314,倍数是 100 ,floatNum {number} 小数,返回一个对象, {times:100, num: 314}

export const toInteger = (floatNum) => {
var ret = {times: 1, num: 0};
if (isInteger(floatNum)) {
ret.num = floatNum;
return ret
}
//1.//转字符串
var strfi = floatNum + '';
//2.//拿到小数点为
var dotPos = strfi.indexOf('.');
//3. //截取需要的长度
var len = strfi.substr(dotPos + 1).length;
//4.倍数就是长度的幂
var times = Math.pow(10, len);
var intNum = parseInt(floatNum * times , 10);
ret.times = times;
ret.num = intNum;
return ret
}

3.把小数放大为整数(乘),进行算术运算,再缩小为小数(除)

  1. 参数:a {number} 运算数1
  2. b:{number} 运算数2,
  3. op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
export const operation = (a, b, op) => {
var o1 = toInteger(a);
var o2 = toInteger(b);
var n1 = o1.num;
var n2 = o2.num;
var t1 = o1.times;
var t2 = o2.times;
var max = t1 > t2 ? t1 : t2;
var result = null;
switch (op) {
case 'add':
if (t1 === t2) { // 两个小数位数相同
result = n1 + n2
} else if (t1 > t2) { // o1 小数位 大于 o2
result = n1 + n2 * (t1 / t2)
} else { // o1 小数位 小于 o2
result = n1 * (t2 / t1) + n2
}
return result / max;
case 'subtract':
if (t1 === t2) {
result = n1 - n2
} else if (t1 > t2) {
result = n1 - n2 * (t1 / t2)
} else {
result = n1 * (t2 / t1) - n2
}
return result / max;
case 'multiply':
result = (n1 * n2) / (t1 * t2);
return result;
case 'divide':
result = (n1 / n2) * (t2 / t1);
return result
}
}

原文:https://segmentfault.com/a/1190000022730047

收起阅读 »

drawable用Kotlin应该这样写

Kotlin应该这样写系列 SharedPreferences用Kotlin应该这样写 Glide用Kotlin应该这样封装(一) Glide用Kotlin应该这样封装(二) 前言 通常我们在res/drawable下面自定义shape和selector来满足...
继续阅读 »

Kotlin应该这样写系列


SharedPreferences用Kotlin应该这样写


Glide用Kotlin应该这样封装(一)


Glide用Kotlin应该这样封装(二)


前言


通常我们在res/drawable下面自定义shapeselector来满足一些UI的设计,但是由于xml最终转换为drawable需要经过IO或反射创建,会有一些性能损耗,另外随着项目的增大和模块化等,很多通用的样式并不能快速复用,需要合理的项目资源管理规范才能实施。那么通过代码直接创建这些drawable,可以在一定程度上降低这些副作用。本篇介绍用kotlin DSL简洁的语法特性来实现常见的drawable.


代码对应效果预览


shape_line
RECTANGLE
OVAL
LayerList
Selector

集成和使用



  1. 在项目级的build.gradle文件种添加仓库Jitpack:


allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
复制代码


  1. 添加依赖


dependencies {		
implementation 'com.github.forJrking:DrawableDsl:0.0.3’
}
复制代码


  1. 抛弃xml创建方式示例(其他参见demo)


// infix用法用于去掉括号更加简洁,详细后面说明
image src shapeDrawable {
//指定shape样式
shape(ShapeBuilder.Shape.RECTANGLE)
//圆角,支持4个角单独设置
corner(20f)
//solid 颜色
solid("#ABE2E3")
//stroke 颜色,边框dp,虚线设置
stroke(R.color.white, 2f, 5f, 8f)
}
//按钮点击样式
btn.background = selectorDrawable {
//默认样式
normal = shapeDrawable {
corner(20f)
gradient(90, R.color.F97794, R.color.C623AA2)
}
//点击效果
pressed = shapeDrawable {
corner(20f)
solid("#84232323")
}
}
复制代码

实现思路


xml如何转换成drawable


xml变成drawable,通过android.graphics.drawable.DrawableInflater这个类来IO解析标签创建,然后通过解析标签再设置属性:


//标签创建
private Drawable inflateFromTag(@NonNull String name) {
switch (name) {
case "selector":
return new StateListDrawable();
case "level-list":
return new LevelListDrawable();
case "layer-list":
return new LayerDrawable();
....
case "color":
return new ColorDrawable();
case "shape":
return new GradientDrawable();
case "vector":
return new VectorDrawable();
...
}
}
//反射创建
private Drawable inflateFromClass(@NonNull String className) {
try {
Constructor<? extends Drawable> constructor;
synchronized (CONSTRUCTOR_MAP) {
constructor = CONSTRUCTOR_MAP.get(className);
if (constructor == null) {
final Class<? extends Drawable> clazz = mClassLoader.loadClass(className).asSubclass(Drawable.class);
constructor = clazz.getConstructor();
CONSTRUCTOR_MAP.put(className, constructor);
}
}
return constructor.newInstance();
} catch (NoSuchMethodException e) {
...
}
复制代码

代码实现


由于创建shape等需要设置各种属性来构建,比较符合build设计模式,那我们首先封装build模式的shapeBuilder,这样做虽然代码比起直接使用apply{}要多,但是可以让纯java项目用起来很舒服,其他实现请查看源码:


class ShapeBuilder : DrawableBuilder {
private var mRadius = 0f
private var mWidth = 0f
private var mHeight = 0f
...
private var mShape = GradientDrawable.RECTANGLE
private var mSolidColor = 0

/**分别设置四个角的圆角*/
fun corner(leftTop: Float,rightTop: Float,leftBottom: Float,rightBottom: Float): ShapeBuilder {
....if(dp)dp2px(leftTop) else leftTop
return this
}

fun solid(@ColorRes colorId: Int): ShapeBuilder {
mSolidColor = ContextCompat.getColor(context, colorId)
return this
}
// 省略其他参数设置方法 详细代码查看源码
override fun build(): Drawable {
val gradientDrawable = GradientDrawable()
gradientDrawable = GradientDrawable()
gradientDrawable.setColor(mSolidColor)
gradientDrawable.shape = mShape
....其他参数设置
return gradientDrawable
}
}
复制代码

把build模式转换为dsl


理论上所有的build模式都可以轻松转换为dsl写法:


inline fun shapeDrawable(builder: ShapeBuilder.() -> Unit): Drawable {
return ShapeBuilder().also(builder).build()
}
//使用方法
val drawable = shapeDrawable{
...
}
复制代码

备注:dsl用法参见juejin.cn/post/695318… 中dsl小节


函数去括号


通过上面封装已经实现了dsl的写法,通常setBackground可以通过setter简化,但是我发现由于有些api设计还需要加括号,这样不太kotlin:


//容易阅读
iv1.background = shapeDrawable {
shape(ShapeBuilder.Shape.RECTANGLE)
solid("#ABE2E3")
}
//多了括号看起来不舒服
iv2.setImageDrawable(shapeDrawable {
solid("#84232323")
})
复制代码

怎么去掉括号呢?🈶2种方式infix函数(中缀表达)和property setter



  1. infix函数特点和规范:



  • Kotlin允许在不使用括号和点号的情况下调用函数

  • 必须只有一个参数

  • 必须是成员函数或扩展函数

  • 不支持可变参数和带默认值参数


/**为所有ImageView添加扩展infix函数 来去掉括号*/
infix fun ImageView.src(drawable: Drawable?) {
this.setImageDrawable(drawable)
}
//使用如下
iv2 src shapeDrawable {
shape(ShapeBuilder.Shape.OVAL)
solid("#E3ABC2")
}
复制代码

当然了代码是用来阅读的。个人认为如果我们大量使用infix函数,阅读困难会大大增加,所以建议函数命名必须可以直击函数功能,而且函数功能简单且单一。



  1. property setter方式,主要使用kotlin可以简化setter变量 =来去括号:


/**扩展变量*/
var ImageView.src: Drawable
get() = drawable
set(value) {
this.setImageDrawable(value)
}
//使用如下
iv2.src = shapeDrawable {
shape(ShapeBuilder.Shape.OVAL)
solid("#E3ABC2")
}
复制代码

感谢@叮凛凛 指点,欢迎大家讨论一起学习,共同进步。


优缺点


优点:



  • 代码直接创建比起xml方式可以提升性能

  • dsl方式比起build模式和调用方法设置更加简洁符合kotlin风格

  • 通过合适的代码管理可以复用这些代码,比xml管理方便


缺点:



  • 没有as的预览功能,只有通过上机观测

  • api还没有覆盖所有drawable属性(例如shape = ring等)


后语


上面把的DrawableDsl基础用法介绍完了,欢迎大家使用,欢迎提Issues,记得给个star哦。Github链接:github.com/forJrking/D…


作者:forJrking
链接:https://juejin.cn/post/6953472037012635655
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin 高效解析数学表达式(支持函数)

需求 由于项目需求,需要在低性能设备高频率地解析计算数学表达式,所以重量级的比如词法分析,语法分析,抽象语法树🌲三件套就不太合适了。(当然也不是不行,只是有点大材小用,而个人能力又有限,对于ANTLR调优之类不太擅长) 说起数学公式解析,当然离不开老朋友逆波兰...
继续阅读 »

需求


由于项目需求,需要在低性能设备高频率地解析计算数学表达式,所以重量级的比如词法分析,语法分析,抽象语法树🌲三件套就不太合适了。(当然也不是不行,只是有点大材小用,而个人能力又有限,对于ANTLR调优之类不太擅长)


说起数学公式解析,当然离不开老朋友逆波兰表达式,也就是后缀表达式,他能忽略括号等优先级问题,按照顺序一一计算。本次也打算从这里入手,慢慢再加入其他一些特性。


先总结下需求:



  • 支持加减乘除与括号,以及括号嵌套。

  • 支持负数/负表达式以及连续负号。比如 1*-2=-2, 1*--2=2, 1*-(2+3)=-5

  • 支持固定的函数,以及函数与表达式的相互嵌套。比如 1+abs(-2-3)=6

  • 支持常量,例如 pi, e


因为使用场景频率较高,所以解析过程中不要使用临时对象,如果非要使用要建立对象池避免 GC 压力过大。


基础实现


首先是最基本的,中缀转后缀表达式。再捋一下基本思想:


准备一个 OP 栈存放操作符。从头开始扫描字符串,遇到数字则直接输出(暂且称之为输出),否则假设 OP 栈顶元素为 A,扫描到的标识符为 B:



  1. 若 B 是右括号,则将栈中元素依次出栈输出,直到左括号。括号出栈不输出。

  2. 若 A 为空 或 A 是左括号 或 B 是左括号,则 B 入栈。

  3. 若 B 优先级大于 A,则 B 入栈。

  4. 否则 A 出栈输出并循环,直到满足上述条件之一。


扫描结束后将 OP 元素全部出栈输出,得到完整的后缀表达式。


按照上述算法,表达式 2*(3+4) 转换后输出为 234+*。然后只需要按顺序扫描输出,遇到数字入栈,遇到运算符出栈操作数,然后将计算结果再入栈,最后栈中唯一元素就是答案了。


不难看出这种方法相当于遍历了两遍。其实我们可以再准备一个表达式 exp 栈,在输出的过程中直接入 exp,如果遇到操作符,则进行计算再将结果入 exp。这样只需要遍历一遍就能得到答案。



💡 Tips


复盘 B 入栈的条件,可以发现栈中可能存在的元素包含运算符与左括号。右括号永远不会入栈。也就是说 A 可能的值为运算符与左括号。反直觉地,我们只需要将左括号(定义为优先级最低,即可将括号也作为运算符看待一并计入优先级比较,避免一小段特殊逻辑代码。

但请注意 AB 都是左括号时,虽然优先级相等,但是 B 应当入栈而不是 A 出栈。 因此相比于直接定义 int 优先级,我更偏向于定义一个二维 bool 数组,用于快捷查找任意两个符号比较,应当出栈还是入栈。



到此为止已经解决了四则运算与括号的问题。


额外需求


函数


我的基本思想就是将函数作为一个特殊的操作符(和加号一样)处理。那么就会产生几个问题:



  1. 函数的操作数都在操作符的后面。

  2. 函数的优先级如何定义。

  3. 参数的分隔符,怎么办。

  4. 函数什么时候可以出栈?


第一个问题其实不影响什么。我们思考一下常规减号-的处理流程。首先将左边操作数输出,然后将减号入栈,最后输出右边操作数。对比一下函数:首先将函数(特殊操作符)入栈,然后将参数依次输出(当作操作数)。显然,两者最终的输出以及栈是一样的,即:操作数从左至右依次输出,操作符入栈。因此操作数与操作符的相对位置并不重要。


问题二则更简单,显然函数的优先级应该是最高,遇到直接入栈就完事了,但是两个函数之间该如何比较?其实这种情况根本不可能发生,因为函数后必定紧跟着一个左括号(,一旦左括号入栈,则后续的操作符必定直接入栈。因此函数入栈时,栈顶不可能也是函数。


至于参数分隔符,我将其视为一个表达式的结束。因为每一个参数都可能是一个嵌套的表达式,我们知道函数在执行前必须先计算出所有参数的值。如此一来,参数分隔符,和右括号)一样不需要入栈,同时要弹出栈中的其他元素,直到遇左括号(但是不要把左括号出栈,因为左括号和函数是一体的,分隔符只是参数的结束,并不是函数的结束。


最后,既然函数的优先级最高,那么什么时候才能轮到它出栈呢?显而易见肯定不能等最后集中出栈,否则就成了薛定谔的参数了。之前讨论到函数的操作数都在右边,换句话说,当操作数全部输出时,就应该立即让函数出栈。答案呼之欲出:当函数结束的时候函数本身应该出栈,而右括号)就是函数的结束符。这个符号在基础章节已经被定义过了,遇到右括号,我们要循环出栈直到左括号。此时应该加上一条:左括号出栈后如果栈顶元素是函数,那么将其出栈并输出。 这就能保证在后缀表达式中函数可以紧挨着它的所有参数。


负号


负号可以视为一个单目运算符,为了区别于减号(-),这里用波浪线(~)指代负号。那么只需解决两个问题:



  1. 什么情况下减号需要视为负号?

  2. 负号优先级如何?


先来解决简单的,负号优先级应该是最高(除了函数之外)。因为负号总是和后面的数或表达式结合,不受前方运算符的影响。比如 1*~2=2


下面为了解决问题一,我罗列了整个表达式中所有可能的数据类型,然后一一判断此时减号应该如何解释。



























前方符号语义
左括号 (负号
其他运算符 +-x/负号
数字减号
右括号)额外判断

右括号等情况稍微复杂,正常来说前面是个表达式,那么后边应该为减号。但是我们的语法允许空括号 (),这种表达式经过解析后其实什么都没有。所以遇到右括号)得额外判断 exp 栈,如果栈不为空且栈顶是数字则为负号~,否则为减号-


这样我们就需要用一个变量来记录上一个符号的类型,以便判断语义时使用。连续负号情况不需要额外处理。


常量


常量可以说是最简单的额外需求了,简单到甚至不需要额外处理。读取到常量后直接视为数字处理就行了。


解析问题


最后来说一下解析问题。因为一个 Token 可能由多个字符组成,例如数字 12.3 有4个字符,函数 abs 有3个字符。所以本质上我们要手写一个定制的词法解析器,因为语法相对简单,所以难度不算太大。考虑到使用场景,这里的宗旨是整个字符串只扫描一遍,不回退不预读。 容错就不过多考虑了,最外层之间 try 一下。


首先是最简单的常量与函数解析。先来明确下他们的语法定义:以字母开头,只能包含字母数字与下划线_,大小写敏感。我们就可以提炼出下面的特征:(为了便于描述,这里把常量和函数名统称为标识符id)



  • 遇到的第一个字母一定是 id 的开头。

  • 之后遇到的第一个非字母数字下划线一定表示 id 结束。


那么算法就水到渠成了。为了提高 id 拼凑效率,我在外部定义了一个 StringBuilder 用来拼接。当然,还有其他方案,例如记录 id 首末 index 然后一次性提取等。




接下来是识别数字,包括小数。数字可以包含数字和小数点.。类似的,遇到的一个数字或小数点.表示一个数字的开始,之后遇到的第一个非数字且非小数点表示数字的结束。


可以用上面 id 的方法来记录数字字符串,最后一次性地调用 kotlin 标准库函数转换为 double 类型。不过这里扣个字眼:标准库转换的时候必然要扫描字符串,而我们已经扫描一遍了,所以做了重复工作。为此,我定义一个 double 变量 num=0,以及一个表示小数位数的 double fraction=1。扫描到数字后将 num *10 然后加上当前数字。若在小数点之后,则先把 fraction / 10,然后乘以当前数字最后加到 num 上。


如此一来,在扫描的同时我们就实现了字符串到数字类型的转换。


作者:晨鹤
链接:https://juejin.cn/post/6957336568046714917
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Compose Preview 的 UX 设计之旅

本文由来自 Android Developer UX 团队的 Preethi Srinivas (UX 研究员) 和 Paris Hsu (交互设计师) 所撰写。 Jetpack Compose 刚刚进入 测试阶段 啦!🎉 在此激动人心的时刻,Android ...
继续阅读 »

本文由来自 Android Developer UX 团队的 Preethi Srinivas (UX 研究员) 和 Paris Hsu (交互设计师) 所撰写。


Jetpack Compose 刚刚进入 测试阶段 啦!🎉 在此激动人心的时刻,Android Developer UX 团队想邀请您进入我们的世界,走进我们设计 Compose Preview 的设计之旅,旅程将从理解我们面临的挑战、方向的形成,以及原型设计和评估开始。


背景: 理解挑战


Jetpack Compose 是新一代 Android 开发的 UI 工具包,它可更简单高效地构建出精美且性能卓越的 Android 应用。它使用了直观的 Kotlin API,能够做到 UI 随着应用状态的变化而自动更新。当我们的团队第一次听说这个项目时,我们无比期待 Compose 项目的无限可能,它具有将逻辑和数据混合绑定到 UI 的潜力,以及为开发者解锁新的能力。然而,这种新的构建 UI 方式也带来了新的设计挑战。


对于经典的 Android 视图,UI 是静态的,且主要是通过 XML 进行创建。这意味着对 XML 的更改几乎可以立即在 UI 中反映出来,我们可以根据这种特性来构建像 Layout Editor 这样的使用体验,让开发者们通过可视化的拖放操作来编辑他们应用的布局,相应的更改也会自动映射到对 XML 的更改。然而,使用 Compose 的每一次修改,都必须编译 Kotlin 代码才能反映出变化,这就意味着需要花费时间,从而减慢了迭代和创建的过程。


集思构想: 冲刺设计方案


为了探究如何在 Compose 中支持这种开发 UI 代码的新模式,我们团队和我们的软件工程师、开发者关系工程师和产品管理伙伴一起举办了一个研讨会,以解决一个设计挑战: 我们如何利用开发者对现有工具的使用经验来帮助他们创建和掌握 Compose UI?


我们基于 设计思维方法,从理解问题和调整问题的工作场景开始思考。这一过程需要团队在 "我们可以怎样... (How Might We…)" 这一框架下写出自己的想法,然后通过亲和图法 (affinitized) 去识别和提炼这些手头的设计问题。我们以之前的研究为出发点,将自己想象为开发者,结合实际开发过程会遇到的不同阶段,来引导小组进行思考,并绘制出解决方案的工具草图。





Compose 设计研讨会



这一设计研讨会帮助我们总结了几点核心原则,为 Compose Tooling 在 2020 年和之后的发展路线奠定了基础:



  • 基于以往为 XML 构建工具所积累的经验为基础

  • 围绕代码进行界面的绘制

  • 优化迭代和实验


这些原则构成了我们产品设计理念前进的基础。例如,Compose Preview 构建出的使用体验在外观和使用上都会让用户觉得很熟悉,在此之上还补充了 Compose 的范式,通过轻量且可重复利用的 Composable 来构建布局。设计研讨会还鼓励我们更多地以代码为中心构建出 REPL 的编程环境,使得开发者在预览代码时拥有更多的控制权和灵活性 — 这样在本质上就提供了一个支持迭代、实验和学习的交互式编程环境。我们还设想了提供超越 XML 之外的新体验,例如 Interactive Preview (互动预览),它可以支持在 IDE 内部被隔离的沙盒环境下的实时交互;Deploy Preview (部署预览),用于在不重新运行整个应用的前提下,将 Preview Composable 部署到模拟器或者真机上。


原型设计: 早期验证


为了验证我们的假设和设计路径,我们开始对研讨会中的想法进行原型设计,并在用户研究案例中对其进行测试。我们开设了一些研究,以便可以验证当前的方向是否正确,并获取关于未来想法和相关投入的反馈。我们选择了一种迭代方法来获取反馈,从而在涉及其他与 Compose 相关主题的多个研究中,将与 Preview 相关的主题进行了折叠。


例如,为了解 Compose Preview 的使用体验,我们首先列出开发者将会问出的问题:



  • 开发者该如何使用 Compose Preview?

  • 在什么情况下,开发者想要预览动态交互的效果?

  • 在真机或模拟器上部署隔离式 Composable 并与之交互的功能对开发者的帮助程度如何?


我们邀请了开发者来加入我们的 Coding Session,在一个以研究为目的而创建的 Compose 项目中完成一些简单的编程练习。这种方式节省了配置开发环境的时间和精力,尤其是 Compose 仍处于开发者预览版之前的阶段,这一方法还能够帮助我们关注开发者在使用 Preview 和其他 Compose API 时的体验。早期的研究确实需要围绕产品稳定性的问题进行展开,因为 Preview 并非总能按照预期正常工作。研究计划预见到了这些不可避免的问题,同时也能够提供非常早期的洞察。




通过 Coding Session 进行可用性研究



从这些 Session 中我们发现,一些开发者会在区分 Preview 工具栏上的 "Refresh" 图标和横幅中的 "Refresh & Build" 图标时感到困惑。大多数开发者不会意识到 "Refresh" 只更新代码而不需要完整构建,而 "Refresh & Build" 则会通过构建更新全部修改。



"如果 Refresh 和 Refresh & Build 希望保持一致,那么将它们放在一起会更好 — 我最初以为 Refresh 按钮只会刷新 UI 而不会构建项目。"



预览 Refresh & Build (之前和之后)



预览 Refresh & Build (之前和之后)



得到该反馈之后,我们决定将两者统一起来,并改进了体验,当用户点击图标或者横幅时,Preview 会根据代码变化的情况来确定是需要进行刷新还是重新构建。


从早期几轮开发者参与的研究中,产生了一个对于 Compose Preview 的深刻体会是,开发者在 Compose 中进行 UI 原型设计时,会感受到一种掌控感,以及工作效率的提升。



"Refresh 模式让我可以快速完成 UI 的原型设计。加上可以使用功能强大的 Kotlin 创建 UI,以及利用 @Preview 函数展示实例数据,比起老式的 XML 中提供的命名空间助手要好得多。"



我们还感受到了开发者在发现 Preview 中同 Composable 交互时能够导航到对应的代码这一功能时,他们所感到的惊讶和喜悦。



"我才发现这个功能,非常开心,我可以在 Preview 中点击不同的视图,直接跳转到绘制该视图的代码里。我很期待在 Jetpack Compose 中看到更多类似的功能。"



在可用性研究中,我们观察到开发者们会通过在 Preview 中点击不同的 UI 元素来跳转到项目的不同地方 — 这需要人们对 Preview 中的 UI 层次结构有着较为深刻的理解。一些开发者发现,当在 Compose Preview 和代码导航之间进行交互时,会有错位的问题出现。例如,在 Column 中的 Text Composable 区域之外点击,会跳转到代码编辑器中定义 Column 的那一行中去。因而我们通过提供 Composable outline 来增强 Preview 的使用体验,以便在布局中围绕 Composable 提供功能可见性。


Preview 代码跳转功能



Preview 代码跳转功能



沉浸式: 以日记形式进行记录


相对而言,在现场亲自参与可用性研究更容易创造价值,并激发出新的想法。然而由于时间的限制,很难深入地去对主题进行挖掘。因此,我们调整了研究方法,开始更多使用一种远程技术,让开发者自己对某个 Compose 项目进行几周的使用。这段时间内,开发者需要写日记,记录他们在指定项目或者自己项目中关于工作流程上的一些问题。通常我们还会在几周的探索之后,再搭配一次访谈,目的是为了更好了解开发者日记中的具体内容。在几天的探索之后,我们还邀请了一些开发者通过 Google Meet 的 Coding Session,来观察并确定哪些部分的工作是进展顺利的,以及一些可以被改进的地方。


通过提问式的日记来帮助反馈的获取



通过提问式的日记来帮助反馈的获取



在这些研究中出现了一点共性 — 开发者会使用 Preview 来创建工作流程,还会利用它进行一些故障排除/验证的工作。例如,在创建 UI 时,开发者会更倾向于使用 Refresh 模式,而在使用手势/交互时,他们会切换到 Interactive 模式,至于 Deploy 模式,则最常用于故障排除和验证检查。



"当我发现在 Interactive 模式下长按可以显示星星的动画时,我非常的开心。但是,之后的长按操作就不管用了 — 动画再也不出现了。通过在模拟器上部署 Preview 模式,我能确认动画是可以正常工作的。如果 Interactive 模式能够更加稳定的话,它将会成为我测试交互性功能和动画的首选模式。有趣的是,在创建新的 UI 并查看它们的渲染方式时,我大部分时间都不需要使用它。"



此外,我们从一些开发者那里得到反馈,在考虑整个布局之前,能够提取并集中实现一个单独的 Composable 的重要性。



"只部署 Preview 意味着我不需要为了测试一个新的组件,而把 UI 关联到实际的流程中 (包含多个界面和用户输入)。这样使得调试 + 改变复杂 UI 变得更加容易。"



将想法付诸于行动


我们在研究的基础之上确立了要前进的方向,这有助于将开发人员对我们工具的见解和遇到的问题反馈到我们的产品迭代中 — 同时能确保我们也能够捕获到新兴的主题来塑造我们的设计理念。以下是几个示例:


Preview 新用户的使用体验


我们发现开发者在探索如何开始创建 Preview 时会有困难 — 很多人在示例项目中留意到了 Preview,但是在自己的项目中就不能够复刻出类似的使用体验。不直观的设计往往导致在创建 Preview Composable 时,对 Compose 编译器到底支持什么功能而产生误解。例如,我们观察到一些开发者试图预览一个接受参数的 Composable,而这一功能 Compose 是不支持的。在这种情况下,编译器提供的错误信息往往会被忽略或遗漏。



"我无法在 Preview 中显示 Split 视图,即使我是直接从一个示例项目中复制过来的代码,它也无法让 Preview 注解正常工作。"



这一重要的发现使我们引入了默认状态,如果 Kotlin 文件尚未定义 Preview Composable,那么拆分编辑器 (这一概念源于 View/XML 世界中的 Preview) 则始终处于可见状态。我们相信该解决方案不仅可以提高对 Preview 的认知和发现能力,还可以提供创建和操作 Preview 相关的学习经验。


Preview 默认状态



Preview 默认状态



增强编码体验


在调查研究中,开发者问了我们这样几个问题:



  • 如何在浅色和深色主题背景中预览一个布局?

  • 如何利用样本数据预览一个布局?

  • 我如何利用 Preview 来确定我的代码中在哪定义了某个特定的 UI 元素?

  • 有没有一种方法可以让 Compose 模仿 View/XML 世界中的 Preview 使用体验,特别是在 Preview 中如何快速查看因为代码变化产生的视觉变化?


这些问题都指向了一点 — 开发者正在寻找一种快速简单的机制来操作 Preview,并期望它能更快地进行迭代。


我们将继续对开发者反馈的新功能进行原型设计和测试,例如 Preview Configuration Picker (Preview 配置选择器),它允许开发者可视化地配置他们的布局 (例如在不同的主题、设备、语言等),以提高 @Preview API 的可发现性和可学习性。


Preview 配置选择器



Preview 配置选择器



另一个例子是 Live literals (实时显示字面量类型),这是来自工程团队的解决方案,通过在 Preview 面板中对一些 Composable 值 (例如 Boolean、Char、String、Color 等) 引入实时更新,来优化迭代开发的速度。


Live Literal 的实际体验



Live Literal 的实际体验



PreviewParameterProvider 是我们将样本数据纳入 Preview 中来允许真实上下文测试的又一例子。


使用 PreviewParameterProvider



使用 PreviewParameterProvider



旅程仍未结束


我们希望这篇文章能让您了解我们是如何根据您的反馈来改进 Compose Preview 的。当然,我们的旅程并没有就此结束!我们还有很多继续改善 Compose Preview 及其工具使用体验的计划。例如,将 Live Literals 功能扩展到字面量类型之外,以继续优化迭代开发的速度。


如果您在使用 Compose 工具时遇到问题,或者是有任何可以改善使用体验的新功能的想法,请 告诉我们。我们也在寻找开发者参与到用户研究 Session 中,您可以 注册 参与。

作者:Android_开发者
链接:https://juejin.cn/post/6959416526562656292
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS 开发:『Crash 防护系统』(二)KVO 防护

1. KVO Crash 的常见原因KVO(Key Value Observing) 翻译过来就是键值对观察,是 iOS 观察者模式的一种实现。KVO 允许一个对象监听另一个对象特定属性的改变,并在改变时接收到事件。但是 KVO API 的设计,我个人觉得不是...
继续阅读 »

1. KVO Crash 的常见原因

KVO(Key Value Observing) 翻译过来就是键值对观察,是 iOS 观察者模式的一种实现。KVO 允许一个对象监听另一个对象特定属性的改变,并在改变时接收到事件。但是 KVO API 的设计,我个人觉得不是很合理。被观察者需要做的工作太多,日常使用时稍不注意就会导致崩溃。

KVO 日常使用造成崩溃的原因通常有以下几个:

1. KVO 添加次数和移除次数不匹配:

  • 移除了未注册的观察者,导致崩溃。

  • 重复移除多次,移除次数多于添加次数,导致崩溃。

  • 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。

2. 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。 例如:被观察者是局部变量的情况(iOS 10 及之前会崩溃)。

3. 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。

4. 添加或者移除时 keypath == nil,导致崩溃。

2. KVO 防止 Crash 常见方案

为了避免上面提到的使用 KVO 造成崩溃的问题,于是出现了很多关于 KVO 的第三方库,比如最出名的就是 FaceBook 开源的第三方库 facebook / KVOController。

FBKVOController 对 KVO 机制进行了额外的一层封装,框架不但可以自动帮我们移除观察者,还提供了 block 或者 selector 的方式供我们进行观察处理。不可否认的是,FBKVOController 为我们的开发提供了很大的便利性。但是相对而言,这种方式对项目代码的侵入性比较大,必须依靠编码规范来强制约束团队人员使用这种方式。

那么有没有一种对项目代码侵入性小,同时还能有效防护 KVO 崩溃的防护机制呢?

网上有很多类似的方案可以参考一下。

方案一:大白健康系统 -- iOS APP运行时 Crash 自动修复系统

1. 首先为 NSObject 建立一个分类,利用 Method Swizzling,实现自定义的 BMP_addObserver:forKeyPath:options:context:、BMP_removeObserver:forKeyPath:、BMP_removeObserver:forKeyPath:context:、BMPKVO_dealloc 方法,用来替换系统原生的添加移除观察者方法的实现。

2. 然后在观察者和被观察者之间建立一个 KVODelegate 对象,两者之间通过 KVODelegate 对象 建立联系。然后在添加和移除操作时,将 KVO 的相关信息例如 observer、keyPath、options、context 保存为 KVOInfo 对象,并添加到 KVODelegate 对象 中对应 的 关系哈希表 中,对应原有的添加观察者。关系哈希表的数据结构:{keypath : [KVOInfo 对象1, KVOInfo 对象2, ... ]}

3. 在添加和移除操作的时候,利用 KVODelegate 对象 做转发,把真正的观察者变为 KVODelegate 对象,而当被观察者的特定属性发生了改变,再由 KVODelegate 对象 分发到原有的观察者上。

那么,BayMax 系统是如何避免 KVO 崩溃的呢?

1. 添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。

2. 移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。

3. 观察键值改变时:同样通过关系哈希表判断,将改变操作分发到原有的观察者上。

另外,为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO 导致崩溃。BayMax 系统还利用 Method Swizzling 实现了自定义的 dealloc,在系统 dealloc 调用之前,将多余的观察者移除掉。

方案二: ValiantCat / XXShield(第三方框架)

XXShield 实现方案和 BayMax 系统类似。也是利用一个 Proxy 对象用来做转发, 真正的观察者是 Proxy,被观察者出现了通知信息,由 Proxy 做分发。不过不同点是 Proxy 里面保存的内容没有前者多。只保存了 _observed(被观察者) 和关系哈希表,这个关系哈希表中只维护了 keyPath 和 observer 的关系。

关系哈希表的数据结构:{keypath : [observer1, observer2 , ...](NSHashTable)} 。

XXShield 在 dealloc 中也做了类似将多余观察者移除掉的操作,是通过关系数据结构和 _observed ,然后调用原生移除观察者操作实现的。

方案三: JackLee18 / JKCrashProtect(第三方框架)

JKCrashProtect 相对于前两个方案来讲,看上去更加的简洁明了。他的不同点在于没有使用 delegate。而是直接在分类中建立了一个关系哈希表,用来保存 {keypath : [observer1, observer2 , ...](NSHashTable)} 的关系。

添加的时候,如果关系哈希表中与 keyPath 对应的已经有了相关的观察者,就不再进行添加。同样移除观察者的时候,也在哈希表中进行查找,如果存在 observer、keyPath 的信息,就移除掉,否则就不进行移除操作。

不过,这个框架并没有对被观察者在 dealloc 时仍然注册着 KVO ,造成崩溃的情况进行处理。

3. 我的 KVO 防护实现

参考了这几个方法的实现后,分别实现了一下之后,最终还是选择了 方案一、方案二 这两种方案的实现思路。

1. 我使用了 YSCKVOProxy 对象,在 YSCKVOProxy 对象 中使用 {keypath : [observer1, observer2 , ...](NSHashTable)} 结构的 关系哈希表 进行 observer、keyPath 之间的维护。

2. 然后利用 YSCKVOProxy 对象 对添加、移除、观察方法进行分发处理。

3. 在分类中自定义了 dealloc 的实现,移除了多余的观察者。

  • 代码如下所示:

#import "NSObject+KVODefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

// 判断是否是系统类
static inline BOOL IsSystemClass(Class cls){
BOOL isSystem = NO;
NSString *className = NSStringFromClass(cls);
if ([className hasPrefix:@"NS"] || [className hasPrefix:@"__NS"] || [className hasPrefix:@"OS_xpc"]) {
isSystem = YES;
return isSystem;
}
NSBundle *mainBundle = [NSBundle bundleForClass:cls];
if (mainBundle == [NSBundle mainBundle]) {
isSystem = NO;
}else{
isSystem = YES;
}
return isSystem;
}


#pragma mark - YSCKVOProxy 相关

@interface YSCKVOProxy : NSObject

// 获取所有被观察的 keyPaths
- (NSArray *)getAllKeyPaths;

@end

@implementation YSCKVOProxy
{
// 关系数据表结构:{keypath : [observer1, observer2 , ...](NSHashTable)}
@private
NSMutableDictionary<NSString *, NSHashTable<NSObject *> *> *_kvoInfoMap;
}

- (instancetype)init {
self = [super init];
if (self) {
_kvoInfoMap = [NSMutableDictionary dictionary];
}
return self;
}

// 添加 KVO 信息操作, 添加成功返回 YES
- (BOOL)addInfoToMapWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context {

@synchronized (self) {
if (!observer || !keyPath ||
([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
return NO;
}

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];
if (info.count == 0) {
info = [[NSHashTable alloc] initWithOptions:(NSPointerFunctionsWeakMemory) capacity:0];
[info addObject:observer];

_kvoInfoMap[keyPath] = info;

return YES;
}

if (![info containsObject:observer]) {
[info addObject:observer];
}

return NO;
}
}

// 移除 KVO 信息操作, 添加成功返回 YES
- (BOOL)removeInfoInMapWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath {

@synchronized (self) {
if (!observer || !keyPath ||
([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
return NO;
}

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

if (info.count == 0) {
return NO;
}

[info removeObject:observer];

if (info.count == 0) {
[_kvoInfoMap removeObjectForKey:keyPath];

return YES;
}

return NO;
}
}

// 添加 KVO 信息操作, 添加成功返回 YES
- (BOOL)removeInfoInMapWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context {
@synchronized (self) {
if (!observer || !keyPath ||
([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
return NO;
}

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

if (info.count == 0) {
return NO;
}

[info removeObject:observer];

if (info.count == 0) {
[_kvoInfoMap removeObjectForKey:keyPath];

return YES;
}

return NO;
}
}

// 实际观察者 yscKVOProxy 进行监听,并分发
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {

NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

for (NSObject *observer in info) {
@try {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:context];
} @catch (NSException *exception) {
NSString *reason = [NSString stringWithFormat:@"KVO Warning : %@",[exception description]];
NSLog(@"%@",reason);
}
}
}

// 获取所有被观察的 keyPaths
- (NSArray *)getAllKeyPaths {
NSArray <NSString *>*keyPaths = _kvoInfoMap.allKeys;
return keyPaths;
}

@end


#pragma mark - NSObject+KVODefender 分类

@implementation NSObject (KVODefender)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 拦截 `addObserver:forKeyPath:options:context:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: @selector(addObserver:forKeyPath:options:context:)
withMethod: @selector(ysc_addObserver:forKeyPath:options:context:)
withClass: [NSObject class]];

// 拦截 `removeObserver:forKeyPath:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: @selector(removeObserver:forKeyPath:)
withMethod: @selector(ysc_removeObserver:forKeyPath:)
withClass: [NSObject class]];

// 拦截 `removeObserver:forKeyPath:context:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: @selector(removeObserver:forKeyPath:context:)
withMethod: @selector(ysc_removeObserver:forKeyPath:context:)
withClass: [NSObject class]];

// 拦截 `dealloc` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod: NSSelectorFromString(@"dealloc")
withMethod: @selector(ysc_kvodealloc)
withClass: [NSObject class]];
});
}

static void *YSCKVOProxyKey = &YSCKVOProxyKey;
static NSString *const KVODefenderValue = @"YSC_KVODefender";
static void *KVODefenderKey = &KVODefenderKey;

// YSCKVOProxy setter 方法
- (void)setYscKVOProxy:(YSCKVOProxy *)yscKVOProxy {
objc_setAssociatedObject(self, YSCKVOProxyKey, yscKVOProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// YSCKVOProxy getter 方法
- (YSCKVOProxy *)yscKVOProxy {
id yscKVOProxy = objc_getAssociatedObject(self, YSCKVOProxyKey);
if (yscKVOProxy == nil) {
yscKVOProxy = [[YSCKVOProxy alloc] init];
self.yscKVOProxy = yscKVOProxy;
}
return yscKVOProxy;
}

// 自定义 addObserver:forKeyPath:options:context: 实现方法
- (void)ysc_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context {

if (!IsSystemClass(self.class)) {
objc_setAssociatedObject(self, KVODefenderKey, KVODefenderValue, OBJC_ASSOCIATION_RETAIN);
if ([self.yscKVOProxy addInfoToMapWithObserver:observer forKeyPath:keyPath options:options context:context]) {
// 如果添加 KVO 信息操作成功,则调用系统添加方法
[self ysc_addObserver:self.yscKVOProxy forKeyPath:keyPath options:options context:context];
} else {
// 添加 KVO 信息操作失败:重复添加
NSString *className = (NSStringFromClass(self.class) == nil) ? @"" : NSStringFromClass(self.class);
NSString *reason = [NSString stringWithFormat:@"KVO Warning : Repeated additions to the observer:%@ for the key path:'%@' from %@",
observer, keyPath, className];
NSLog(@"%@",reason);
}
} else {
[self ysc_addObserver:observer forKeyPath:keyPath options:options context:context];
}
}

// 自定义 removeObserver:forKeyPath:context: 实现方法
- (void)ysc_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context {

if (!IsSystemClass(self.class)) {
if ([self.yscKVOProxy removeInfoInMapWithObserver:observer forKeyPath:keyPath context:context]) {
// 如果移除 KVO 信息操作成功,则调用系统移除方法
[self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath context:context];
} else {
// 移除 KVO 信息操作失败:移除了未注册的观察者
NSString *className = NSStringFromClass(self.class) == nil ? @"" : NSStringFromClass(self.class);
NSString *reason = [NSString stringWithFormat:@"KVO Warning : Cannot remove an observer %@ for the key path '%@' from %@ , because it is not registered as an observer", observer, keyPath, className];
NSLog(@"%@",reason);
}
} else {
[self ysc_removeObserver:observer forKeyPath:keyPath context:context];
}
}

// 自定义 removeObserver:forKeyPath: 实现方法
- (void)ysc_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath {

if (!IsSystemClass(self.class)) {
if ([self.yscKVOProxy removeInfoInMapWithObserver:observer forKeyPath:keyPath]) {
// 如果移除 KVO 信息操作成功,则调用系统移除方法
[self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath];
} else {
// 移除 KVO 信息操作失败:移除了未注册的观察者
NSString *className = NSStringFromClass(self.class) == nil ? @"" : NSStringFromClass(self.class);
NSString *reason = [NSString stringWithFormat:@"KVO Warning : Cannot remove an observer %@ for the key path '%@' from %@ , because it is not registered as an observer", observer, keyPath, className];
NSLog(@"%@",reason);
}
} else {
[self ysc_removeObserver:observer forKeyPath:keyPath];
}

}

// 自定义 dealloc 实现方法
- (void)ysc_kvodealloc {
@autoreleasepool {
if (!IsSystemClass(self.class)) {
NSString *value = (NSString *)objc_getAssociatedObject(self, KVODefenderKey);
if ([value isEqualToString:KVODefenderValue]) {
NSArray *keyPaths = [self.yscKVOProxy getAllKeyPaths];
// 被观察者在 dealloc 时仍然注册着 KVO
if (keyPaths.count > 0) {
NSString *reason = [NSString stringWithFormat:@"KVO Warning : An instance %@ was deallocated while key value observers were still registered with it. The Keypaths is:'%@'", self, [keyPaths componentsJoinedByString:@","]];
NSLog(@"%@",reason);
}

// 移除多余的观察者
for (NSString *keyPath in keyPaths) {
[self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath];
}
}
}
}


[self ysc_kvodealloc];
}

@end

4. 测试 KVO 防护效果

这里提供一下相关崩溃的测试代码:

/********************* KVOCrashObject.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface KVOCrashObject : NSObject

@property (nonatomic, copy) NSString *name;

@end

/********************* KVOCrashObject.m 文件 *********************/
#import "KVOCrashObject.h"

@implementation KVOCrashObject

@end

/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "KVOCrashObject.h"

@interface ViewController ()

@property (nonatomic, strong) KVOCrashObject *objc;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

self.objc = [[KVOCrashObject alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

// 1.1 移除了未注册的观察者,导致崩溃
[self testKVOCrash11];

// 1.2 重复移除多次,移除次数多于添加次数,导致崩溃
// [self testKVOCrash12];

// 1.3 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
// [self testKVOCrash13];

// 2. 被观察者 dealloc 时仍然注册着 KVO,导致崩溃
// [self testKVOCrash2];

// 3. 观察者没有实现 -observeValueForKeyPath:ofObject:change:context:导致崩溃
// [self testKVOCrash3];

// 4. 添加或者移除时 keypath == nil,导致崩溃。
// [self testKVOCrash4];
}

/**
1.1 移除了未注册的观察者,导致崩溃
*/
- (void)testKVOCrash11 {
// 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc removeObserver:self forKeyPath:@"name"];
}

/**
1.2 重复移除多次,移除次数多于添加次数,导致崩溃
*/
- (void)testKVOCrash12 {
// 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
[self.objc removeObserver:self forKeyPath:@"name"];
[self.objc removeObserver:self forKeyPath:@"name"];
}

/**
1.3 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
*/
- (void)testKVOCrash13 {
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
}

/**
2. 被观察者 dealloc 时仍然注册着 KVO,导致崩溃
*/
- (void)testKVOCrash2 {
// 崩溃日志:An instance xxx of class xxx was deallocated while key value observers were still registered with it.
// iOS 10 及以下会导致崩溃,iOS 11 之后就不会崩溃了
KVOCrashObject *obj = [[KVOCrashObject alloc] init];
[obj addObserver: self
forKeyPath: @"name"
options: NSKeyValueObservingOptionNew
context: nil];
}

/**
3. 观察者没有实现 -observeValueForKeyPath:ofObject:change:context:导致崩溃
*/
- (void)testKVOCrash3 {
// 崩溃日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
KVOCrashObject *obj = [[KVOCrashObject alloc] init];

[self addObserver: obj
forKeyPath: @"title"
options: NSKeyValueObservingOptionNew
context: nil];

self.title = @"111";
}

/**
4. 添加或者移除时 keypath == nil,导致崩溃。
*/
- (void)testKVOCrash4 {
// 崩溃日志: -[__NSCFConstantString characterAtIndex:]: Range or index out of bounds
KVOCrashObject *obj = [[KVOCrashObject alloc] init];

[self addObserver: obj
forKeyPath: @""
options: NSKeyValueObservingOptionNew
context: nil];

// [self removeObserver:obj forKeyPath:@""];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {

NSLog(@"object = %@, keyPath = %@", object, keyPath);
}

@end

可以将示例项目 NSObject+KVODefender.m 中的 + (void)load; 方法注释掉或打开进行防护前后的测试。

经测试可以发现,成功的拦截了这几种因为 KVO 使用不当导致的崩溃。

链接:https://www.jianshu.com/p/e3713d309283

收起阅读 »

Swift高级分享 - 在Swift中构建模型数据

在代码库中建立可靠的结构通常是必不可少的,以便更容易使用。然而,实现一个既足够严格以防止错误和问题的结构 - 以及对现有功能足够灵活的结构以及我们想要的任何未来变化 - 都可能非常棘手。对于模型代码而言尤其如此,模型代码通常由许多不同的功能使用,每个功能都有自...
继续阅读 »

在代码库中建立可靠的结构通常是必不可少的,以便更容易使用。然而,实现一个既足够严格以防止错误和问题的结构 - 以及对现有功能足够灵活的结构以及我们想要的任何未来变化 - 都可能非常棘手。

对于模型代码而言尤其如此,模型代码通常由许多不同的功能使用,每个功能都有自己的一组要求。本周,让我们来看看构建核心模型的数据的几种不同技术,以及如何改进该结构对我们的其余代码库产生重大积极影响。

形成层次结构

在项目开始时,模型通常可以保持非常简单。由于我们尚未实现许多功能,因此我们的模型很可能不需要包含太多数据。然而,随着我们的代码库的增长,我们的模型经常发生变化 - 并且很容易达到一个简单的模型最终成为各种相关数据的“全能”的程度。

例如,假设我们正在构建一个电子邮件客户端,它使用Message模型来跟踪每条消息。最初,该模型可能只包含给定消息的主题行和正文,但此后逐渐增长为包含各种其他数据:

struct Message {
var subject: String
var body: String
let date: Date
var tags: [Tag]
var replySent: Bool
let senderName: String
let senderImage: UIImage?
let senderAddress: String
}

虽然为了呈现消息需要所有上述数据,但是直接将其保留在Message类型本身中会使事情变得有点混乱 - 并且很可能使消息更难以使用,尤其是当我们创建新实例时 - 撰写新邮件时或编写单元测试时。

缓解上述问题的一种方法是将数据分解为多个专用类型 - 然后我们可以使用它们来形成模型层次结构。例如,我们可能会将有关消息发送者的所有数据提取到Person结构中,并将所有元数据(例如消息的标记和日期)提取到Metadata类型中,如下所示:

struct Person {
var name: String
var image: UIImage?
var address: String
}

extension Message {
struct Metadata {
let date: Date
var tags: [Tag]
var replySent: Bool
}
}

现在,有了上述内容,我们可以为我们的Message类型提供一个更清晰的结构 - 因为每个数据不直接作为消息本身的一部分现在包含在更具上下文的专用类型中:

struct Message {
var subject: String
var body: String
var metadata: Metadata
let sender: Person
}

上述方法的另一个好处是,我们现在可以更容易地在不同的上下文中重用部分数据。例如,我们可以使用我们的新Person类型来实现联系人列表等功能,或者允许用户定义组 - 因为该数据不再直接绑定到该Message类型。

减少重复

除了用于更好地组织我们的代码之外,可靠的结构还可以帮助减少项目中的重复。假设我们的电子邮件应用程序使用事件驱动的方法来处理不同的用户操作 - 使用如下所示的Event枚举:

enum Event {
case add(Message)
case update(Message)
case delete(Message)
case move(Message, to: Folder)
}

使用枚举来定义各种代码需要处理的有限事件列表,这是在应用程序中建立更清晰数据流的好方法 - 但是我们当前的实现要求每个案例都包含Message事件所针对的事件 - 领先在Event类型本身内复制,以及在我们想要从事件的消息中提取信息时。

由于每个事件的操作都是对消息执行的,所以让我们将两者分开,并创建一个更简单的枚举类型,它将包含我们的所有操作:

enum Action {
case add
case update
case delete
case move(to: Folder)
}

然后,让我们再次形成一个层次结构 - 这一次通过重构我们的Event类型成为一个包含a Action和Message它将被应用于的包装器- 如下所示:

struct Event {
let message: Message
let action: Action
}

上述方法为我们提供了两全其美 - 处理事件现在只需要切换事件Action,现在可以使用message属性直接从事件的消息中提取数据。

递归结构

到目前为止,我们已经形成了层次结构,其中每个孩子和父母都是完全独立的类型 - 但这并不总是最优雅,或最方便的解决方案。假设我们正在开发一个显示各种内容的应用程序,例如文本和图像,并且我们再次使用枚举来定义每个内容 - 如下所示:

enum Content {
case text(String)
case image(UIImage)
case video(Video)
}

现在让我们说我们希望让用户能够形成一组内容 - 例如,通过创建收藏列表,或使用文件夹来组织内容。最初的想法可能是寻找一个专用Group类型,它包含组的名称和属于它的内容:

struct Group {
var name: String
var content: [Content]
}

然而,尽管上述内容看起来优雅且结构合理,但在这种情况下它有一些缺点。通过引入一种新的专用类型,我们将需要单独处理各个内容组 - 使得构建列表之类的内容变得更加困难 - 而且我们也无法轻松支持嵌套组。

因为在这种情况下,一个组只不过是构造内容的另一种方式,所以让它改为Content枚举本身的第一类成员,只需为它添加一个新的例子 - 就像这样:

enum Content {
case text(String)
case image(UIImage)
case video(Video)
case group(name: String, content: [Content])
}

我们上面基本上做的是创建Content一个递归数据结构。这种方法的优点在于我们现在可以重用我们用于处理内容的大部分相同代码来处理组,并且我们可以自动支持任意数量的嵌套组。

例如,以下是我们如何处理显示内容列表的表视图的单元格选择:

extension ListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let content = contentList[indexPath.row]

switch content {
case .text(let string):
navigator.showText(string)
case .image(let image):
navigator.showImage(image)
case .video(let video):
navigator.openPlayer(for: video)
case .group(let name, let content):
navigator.openList(withTitle: name, content: content)
}
}
}

上面我们使用导航器模式导航到新目的地。您可以在“Swift中的导航”中找到更多相关信息。

由于Content现在是递归的,因此navigator.openList在处理组时调用现在只需创建一个ListViewController包含该组内容列表的新实例,使用户能够轻松地创建和导航任何内容层次结构,而我们只需要很少的努力。

专业模特

虽然能够重用代码通常是件好事,但有时最好创建一个更专业的新版本的模型,而不是尝试在非常不同的上下文中重用它。

回到之前的电子邮件应用程序示例,假设我们希望用户能够保存部分撰写的邮件草稿。而不是让该功能处理完整的Message实例,这需要不能用于草稿的数据 - 例如发件人的姓名或收到邮件的日期 - 让我们创建一个更简单的Draft类型,我们将嵌套在Message其他上下文中:

extension Message {
struct Draft {
var subject: String?
var body: String?
var recipients: [Person]
}
}

这样,我们可以自由地将某些属性作为选项,并减少加载和保存草稿时我们需要处理的数据量 - 而不会影响我们处理正确消息的任何代码。

结论

虽然哪种模型结构最适合每种情况,但在很大程度上取决于所需的数据类型以及数据的使用方式 - 在能够重用代码和不创建模型之间取得平衡太复杂,往往是关键。

形成清晰的层次结构 - 无论是使用专用类型还是通过创建递归数据结构 - 同时仍然偶尔为特定用例创建模型的专用版本,可以在我们的模型代码中形成更清晰的结构 - 并且像往常一样,常量重构和小改进通常是达到目的的方式。

链接:https://www.jianshu.com/p/06e7d171dd99

收起阅读 »

iOS开发性能监控

App 的性能问题虽然不会导致 App不可用,但依然会影响到用户体验。如果这个性能问题不断累积,达到临界点以后,问题就会爆发出来。这时,影响到的就不仅仅是用户了,还有负责App开发的你。线下性能监控其中线下监控使用的还是Instruments,Instrume...
继续阅读 »

App 的性能问题虽然不会导致 App不可用,但依然会影响到用户体验。如果这个性能问题不断累积,达到临界点以后,问题就会爆发出来。这时,影响到的就不仅仅是用户了,还有负责App开发的你。

线下性能监控

其中线下监控使用的还是Instruments,Instruments功能很强大,下图是Instruments的各种性能检测工具。


最新版本的Instruments 10还有以下两大优势:

1.Instruments基于os_signpost 架构,可以支持所有平台。

2.Instruments由于标准界面(Standard UI)和分析核心(Analysis Core)技术,使得我们可以非常方便地进行自定义性能监测工具的开发。当你想要给Instruments内置的工具换个交互界面,或者新创建一个工具的时候,都可以通过自定义工具这个功能来实现。

从整体架构来看,Instruments 包括Standard UI 和 Analysis Core 两个组件,它的所有工具都是基于这两个组件开发的。而且,你如果要开发自定义的性能分析工具的话,完全基于这两个组件就可以实现。

开发一款自定义Instruments工具,主要包括以下这几个步骤:

1.在Xcode中,点击File > New > Project;

2.在弹出的Project模板选择界面,将其设置为macOS;

3.选择 Instruments Package,点击后即可开始自定义工具的开发了。如下图所示。


经过上面的三步之后,会在新创建的工程里面生成一个.instrpkg 文件,接下来的开发过程主要就是对这个文件的配置工作了。这些配置工作中最主要的是要完成Standard UI 和 Analysis Core 的配置。

上面这些内容,就是你在开发一个自定义Instruments工具时,需要完成的编码工作了。可以看到,Instruments 10版本的自定义工具开发还是比较简单的。与此同时,苹果公司还提供了大量的代码片段,帮助你进行个性化的配置。你可以点击这个链接,查看官方指南中的详细教程。

再说一下,线上性能监控

对于线上性能监控,我们需要先明白两个原则:
1、监控代码不要侵入到业务代码中;
2、采用性能消耗最小的监控方案。

接下来我们从CPU使用率、FPS的帧率和内存这三个方面,说一下线上性能监控

CPU使用率的线上监控方法

App作为进程运行起来后会有多个线程,每个线程对CPU 的使用率不同。各个线程对CPU使用率的总和,就是当前App对CPU 的使用率。明白了这一点以后,我们也就摸清楚了对CPU使用率进行线上监控的思路。

在iOS系统中,你可以在 usr/include/mach/thread_info.h 里看到线程基本信息的结构体,其中的cpu_usage 就是 CPU使用率。结构体的完整代码如下所示:

struct thread_basic_info {
time_value_t user_time; // 用户运行时长
time_value_t system_time; // 系统运行时长
integer_t cpu_usage; // CPU 使用率
policy_t policy; // 调度策略
integer_t run_state; // 运行状态
integer_t flags; // 各种标记
integer_t suspend_count; // 暂停线程的计数
integer_t sleep_time; // 休眠的时间
};

因为每个线程都会有这个 thread_basic_info 结构体,所以接下来的事情就好办了,你只需要定时(比如,将定时间隔设置为2s)去遍历每个线程,累加每个线程的 cpu_usage 字段的值,就能够得到当前App所在进程的 CPU 使用率了。实现代码如下:

- (integer_t)cpuUsage {
thread_act_array_t threads; //int 组成的数组比如 thread[1] = 5635
mach_msg_type_number_t threadCount = 0; //mach_msg_type_number_t 是 int 类型
const task_t thisTask = mach_task_self();
//根据当前 task 获取所有线程
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);

if (kr != KERN_SUCCESS) {
return 0;
}

integer_t cpuUsage = 0;
// 遍历所有线程
for (int i = 0; i < threadCount; i++) {

thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;

if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
// 获取 CPU 使用率
threadBaseInfo = (thread_basic_info_t)threadInfo;
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
cpuUsage += threadBaseInfo->cpu_usage;
}
}
}
assert(vm_deallocate(mach_task_self(), (vm_address_t)threads, threadCount * sizeof(thread_t)) == KERN_SUCCESS);
return cpuUsage;
}

在上面这段代码中,task_threads 方法能够取到当前进程中的线程总数 threadCount 和所有线程的数组 threads。

接下来,我们就可以通过遍历这个数组来获取单个线程的基本信息。其中,线程基本信息的结构体是 thread_basic_info_t,这个结构体里就包含了我们需要的 CPU 使用率的字段 cpu_usage。然后,我们累加这个字段就能够获取到当前的整体 CPU 使用率。

接下来我们说说关于FPS的监控

FPS 线上监控方法

FPS 是指图像连续在显示设备上出现的频率。FPS低,表示App不够流畅,还需要进行优化。

但是,和前面对CPU使用率和内存使用量的监控不同,iOS系统中没有一个专门的结构体,用来记录与FPS相关的数据。但是,对FPS的监控也可以比较简单的实现:通过注册 CADisplayLink 得到屏幕的同步刷新率,记录每次刷新时间,然后就可以得到 FPS。具体的实现代码如下:

- (void)startMonitoring {
if (_link) {
[_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_link invalidate];
_link = nil;
}
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}

self.count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
_fps = _count / delta;
NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
self.count = 0;
}


内存使用量的线上监控方法


通常情况下,我们在获取 iOS 应用内存使用量时,都是使用task_basic_info 里的 resident_size 字段信息。但是,我们发现这样获得的内存使用量和 Instruments 里看到的相差很大。后来,在 2018 WWDC Session 416 iOS Memory Deep Dive中,苹果公司介绍说 phys_footprint 才是实际使用的物理内存。


内存信息存在 task_info.h (完整路径 usr/include/mach/task.info.h)文件的 task_vm_info 结构体中,其中phys_footprint 就是物理内存的使用,而不是驻留内存 resident_size。结构体里和内存相关的代码如下:

struct task_vm_info {
mach_vm_size_t virtual_size; // 虚拟内存大小
integer_t region_count; // 内存区域的数量
integer_t page_size;
mach_vm_size_t resident_size; // 驻留内存大小
mach_vm_size_t resident_size_peak; // 驻留内存峰值

...

/* added for rev1 */
mach_vm_size_t phys_footprint; // 物理内存

...

我们只要从这个结构体里取出phys_footprint 字段的值,就能够监控到实际物理内存的使用情况了。具体实现代码如下:

- (unsigned long)memoryUsage {    
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (result != KERN_SUCCESS)
return 0;
return vmInfo.phys_footprint;
}

从以上三个线上性能监控方案可以看出,它们的代码和业务逻辑是完全解耦的,监控时基本都是直接获取系统本身提供的数据,没有额外的计算量,因此对 App 本身的性能影响也非常小,满足了我们要考虑的两个原则。

你可以点击这个链接,查看具体demo,欢迎大家点赞。

链接:https://www.jianshu.com/p/cc02a1e1e019

收起阅读 »

CocoaAsyncSocket源码分析---Connect (八)

Connect流程,用一张图来概括总结一下吧:socketchildSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); 然后调用了newSocketQue...
继续阅读 »

客户端的整个Connect流程,用一张图来概括总结一下吧:



整个客户端连接的流程大致如上图,当然远不及于此,这里我们对地址做了IPV4IPV6的兼容处理,对一些使用socket而产生的网络错误导致进程退出的容错处理。以及在这个过程中,socketQueue、代理queue、全局并发queuestream常驻线程的管理调度等等。

当然其中绝大部分操作都是在socketQueue中进行的。而在socketQueue中,我们也分为两种操作dispatch_syncdispatch_async
因为socketQueue本身就是一个串行queue,所以我们所有的操作都在这个queue中进行保证了线程安全,而需要阻塞后续行为的操作,我们用了sync的方式。其实这样使用sync是及其容易死锁的,但是作者每次在调用sync之前都调用了这么一行判断:

if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))

判断当前队列是否就是这个socketQueue队列,如果是则直接调用,否则就用sync的方式提交到这个queue中去执行。这种防死锁的方式,你学到了么?

接着我们来讲讲服务端Accept流程:

整个流程还是相对Connect来说还是十分简单的,因为这个方法很长,而且大多数是我们直接连接讲到过得内容,所以我省略了一部分的代码,只把重要的展示出来,大家可以参照着源码看。

//监听端口起点
- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr
{
return [self acceptOnInterface:nil port:port error:errPtr];
}

- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr
{
LogTrace();

// Just in-case interface parameter is immutable.
//防止参数被修改
NSString *interface = [inInterface copy];

__block BOOL result = NO;
__block NSError *err = nil;

// CreateSocket Block
// This block will be invoked within the dispatch block below.
//创建socket的Block
int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {

//创建TCP的socket
int socketFD = socket(domain, SOCK_STREAM, 0);

//一系列错误判断
...
// Bind socket
//用本地地址去绑定
status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]);

//监听这个socket
//第二个参数是这个端口下维护的socket请求队列,最多容纳的用户请求数。
status = listen(socketFD, 1024);
return socketFD;
};

// Create dispatch block and run on socketQueue

dispatch_block_t block = ^{ @autoreleasepool {

//一系列错误判断
...

//判断ipv4 ipv6是否支持
...

//得到本机的IPV4 IPV6的地址
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port];
...

//判断可以用IPV4还是6进行请求
...

// Create accept sources
//创建接受连接被触发的source
if (enableIPv4)
{
//接受连接的source
accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);

//事件句柄
dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {

//拿到数据,连接数
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);

LogVerbose(@"numPendingConnections: %lu", numPendingConnections);

//循环去接受这些socket的事件(一次触发可能有多个连接)
while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections));

}});

//取消句柄
dispatch_source_set_cancel_handler(accept4Source, ^{
//...
//关闭socket
close(socketFD);

});

//开启source
dispatch_resume(accept4Source);
}

//ipv6一样
...

//在scoketQueue中同步做这些初始化。
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);

//...错误判断
//返回结果
return result;
}

这个方法省略完仍然有这么长,它主要做了这两件事(篇幅原因,尽量精简):

  1. 创建本机地址、创建socket、绑定端口、监听端口。
  2. 创建了一个GCD Source,来监听这个socket读source,这样连接事件一发生,就会触发我们的事件句柄。接着我们调用了doAccept:方法循环去接受所有的连接。

接着我们来看这个接受连接的方法(同样省略了一部分不那么重要的代码):

//连接接受的方法
- (BOOL)doAccept:(int)parentSocketFD
{
LogTrace();

int socketType;
int childSocketFD;
NSData *childSocketAddress;

//IPV4
if (parentSocketFD == socket4FD)
{
socketType = 0;

struct sockaddr_in addr;
socklen_t addrLen = sizeof(addr);
//调用接受,得到接受的子socket
childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
//NO说明没有连接
if (childSocketFD == -1)
{
LogWarn(@"Accept failed with error: %@", [self errnoError]);
return NO;
}
//子socket的地址数据
childSocketAddress = [NSData dataWithBytes:&addr length:addrLen];
}
//一样
else if (parentSocketFD == socket6FD)
{
...
}
//unix domin socket 一样
else // if (parentSocketFD == socketUN)
{
...
}

//socket 配置项的设置... 和connect一样

//响应代理
if (delegateQueue)
{
__strong id theDelegate = delegate;
//代理队列中调用
dispatch_async(delegateQueue, ^{ @autoreleasepool {

// Query delegate for custom socket queue

dispatch_queue_t childSocketQueue = NULL;

//判断是否实现了为socket 生成一个新的SocketQueue,是的话拿到新queue
if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)])
{
childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress
onSocket:self];
}

// Create GCDAsyncSocket instance for accepted socket
//新创建一个本类实例,给接受的socket
GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate
delegateQueue:delegateQueue
socketQueue:childSocketQueue];
//IPV4 6 un
if (socketType == 0)
acceptedSocket->socket4FD = childSocketFD;
else if (socketType == 1)
acceptedSocket->socket6FD = childSocketFD;
else
acceptedSocket->socketUN = childSocketFD;
//标记开始 并且已经连接
acceptedSocket->flags = (kSocketStarted | kConnected);

// Setup read and write sources for accepted socket
//初始化读写source
dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool {

[acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD];
}});

//判断代理是否实现了didAcceptNewSocket方法,把我们新创建的socket返回出去
if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)])
{
[theDelegate socket:self didAcceptNewSocket:acceptedSocket];
}

}});
}
return YES;
}

  • 这个方法很简单,核心就是调用下面这个函数,去接受连接,并且拿到一个新的socket
childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);

  • 然后调用了newSocketQueueForConnectionFromAddress:onSocket:这个代理,可以为新的socket重新设置一个socketQueue
  • 接着我们用这个Socket重新创建了一个GCDAsyncSocket实例,然后调用我们的代理didAcceptNewSocket方法,把这个实例给传出去了。
  • 这里需要注意的是,我们调用didAcceptNewSocket代理方法传出去的实例我们需要自己保留,不然就会被释放掉,那么这个与客户端的连接也就断开了。
  • 同时我们还初始化了这个新socket的读写source,这一步完全和connect中一样,调用同一个方法,这样如果有读写数据,就会触发这个新的socketsource了。

建立连接之后的无数个新的socket,都是独立的,它们处理读写连接断开的逻辑就和客户端socket完全一样了。
而我们监听本机端口的那个socket始终只有一个,这个用来监听触发socket连接,并返回创建我们这无数个新的socket实例。

作为服务端的Accept流程就这么结束了,因为篇幅原因,所以尽量精简了一些细节的处理,不过这些处理在Connect中也是反复出现的,所以基本无伤大雅。如果大家会感到困惑,建议下载github中的源码注释,对照着再看一遍,相信会有帮助的。


接着我们来讲讲Unix Domin Socket建立本地进程通信流程:

基本上这个流程,比上述任何流程还要简单,简单的到即使不简化代码,也没多少行(当然这是建立在客户端Connect流程已经实现了很多公用方法的基础上)。

接着进入正题,我们来看看它发起连接的方法:


//连接本机的url上,IPC,进程间通信
- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;
{
LogTrace();

__block BOOL result = NO;
__block NSError *err = nil;

dispatch_block_t block = ^{ @autoreleasepool {

//判断长度
if ([url.path length] == 0)
{
NSString *msg = @"Invalid unix domain socket url.";
err = [self badParamError:msg];

return_from_block;
}

// Run through standard pre-connect checks
//前置的检查
if (![self preConnectWithUrl:url error:&err])
{
return_from_block;
}

// We've made it past all the checks.
// It's time to start the connection process.

flags |= kSocketStarted;

// Start the normal connection process

NSError *connectError = nil;
//调用另一个方法去连接
if (![self connectWithAddressUN:connectInterfaceUN error:&connectError])
{
[self closeWithError:connectError];

return_from_block;
}

[self startConnectTimeout:timeout];

result = YES;
}};

//在socketQueue中同步执行
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);

if (result == NO)
{
if (errPtr)
*errPtr = err;
}

return result;
}

连接方法非常简单,就只是做了一些错误的处理,然后调用了其他的方法,包括一个前置检查,这检查中会去判断各种参数是否正常,如果正常会返回YES,并且把url转换成Uinix domin socket地址的结构体,赋值给我们的属性connectInterfaceUN
接着调用了connectWithAddressUN方法去发起连接。

我们接着来看看这个方法:

//连接Unix域服务器
- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr
{
LogTrace();

NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");

// Create the socket

int socketFD;

LogVerbose(@"Creating unix domain socket");

//创建本机socket
socketUN = socket(AF_UNIX, SOCK_STREAM, 0);

socketFD = socketUN;

if (socketFD == SOCKET_NULL)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in socket() function"];

return NO;
}

// Bind the socket to the desired interface (if needed)

LogVerbose(@"Binding socket...");

int reuseOn = 1;
//设置可复用
setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));

// Prevent SIGPIPE signals

int nosigpipe = 1;
//进程终止错误信号禁止
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));

// Start the connection process in a background queue

int aStateIndex = stateIndex;

dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalConcurrentQueue, ^{

const struct sockaddr *addr = (const struct sockaddr *)[address bytes];
//并行队列调用连接
int result = connect(socketFD, addr, addr->sa_len);
if (result == 0)
{
dispatch_async(socketQueue, ^{ @autoreleasepool {
//连接成功的一些状态初始化
[self didConnect:aStateIndex];
}});
}
else
{
// 失败的处理
perror("connect");
NSError *error = [self errnoErrorWithReason:@"Error in connect() function"];

dispatch_async(socketQueue, ^{ @autoreleasepool {

[self didNotConnect:aStateIndex error:error];
}});
}
});

LogVerbose(@"Connecting...");

return YES;
}

主要部分基本和客户端连接相同,并且简化了很多,调用了这一行完成了连接:

int result = connect(socketFD, addr, addr->sa_len);

同样也和客户端一样,在连接成功之后去调用下面这个方法完成了一些资源的初始化:

 [self didConnect:aStateIndex];

基本上连接就这么两个方法了(当然我们省略了一些细节),看完客户端的连接之后,到这就变得非常简单了。

接着我们来看看uinix domin socket作为服务端Accept。

这个Accpet,基本和我们普通Socket服务端的Accept相同。

//接受一个Url,uniex domin socket 做为服务端
- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr;
{
LogTrace();

__block BOOL result = NO;
__block NSError *err = nil;

//基本和正常的socket accept一模一样
// CreateSocket Block
// This block will be invoked within the dispatch block below.
//生成一个创建socket的block,创建、绑定、监听
int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {

//creat socket
...
// Set socket options

...
// Bind socket

...

// Listen
...
};

// Create dispatch block and run on socketQueue
//错误判断
dispatch_block_t block = ^{ @autoreleasepool {

//错误判断
...

//判断是否有这个url路径是否正确
...

//调用上面的Block创建socket,并且绑定监听。
...

//创建接受连接的source
acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, 0, socketQueue);

int socketFD = socketUN;
dispatch_source_t acceptSource = acceptUNSource;
//事件句柄,和accpept一样
dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool {
//循环去接受所有的每一个连接
...
}});

//取消句柄
dispatch_source_set_cancel_handler(acceptUNSource, ^{

//关闭socket
close(socketFD);
});

LogVerbose(@"dispatch_resume(accept4Source)");
dispatch_resume(acceptUNSource);

flags |= kSocketStarted;

result = YES;
}};

if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
//填充错误
if (result == NO)
{
LogInfo(@"Error in accept: %@", err);

if (errPtr)
*errPtr = err;
}

return result;
}

因为代码基本雷同,所以我们省略了大部分代码,大家可以参照着之前的讲解或者源码去理解。这里和普通服务端socket唯一的区别就是,这里服务端绑定的地址是unix domin socket类型的地址,它是一个结构体,里面包含的是我们进行进程通信的纽带-一个本机文件路径。
所以这里服务端简单来说就是绑定的这个文件路径,当这个文件路径有数据可读(即有客户端连接到达)的时候,会触发初始化的source事件句柄,我们会去循环的接受所有的连接,并且新生成一个socket实例,这里和普通的socket完全一样。

就这样我们所有的连接方式已经讲完了,后面这两种方式,为了节省篇幅,确实讲的比较粗略,但是核心的部分都有提到。
另外如果你有理解客户端的Connect流程,那么理解起来应该没有什么问题,这两个流程比前者可简化太多了。


这个框架的Connect篇 全篇结束














收起阅读 »