注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

JetPack Compose 主题配色太少怎么办? 来设计自己的颜色系统吧

引言JetPack Compose 正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose 在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。本...
继续阅读 »

引言

JetPack Compose 正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose 在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。

本篇要解决的就是 Compose 默认 Material 主题颜色太少,如何配置自己的业务颜色板,或者说,如何自定义自己的颜色系统,并由点入深,系统的分析相关实现方法与原理。

问题

在开始之前,我们先看看目前创建一个 Compose 项目,默认的 Material 主题为我们提供的颜色有哪些:

图片名称

对于一个普通的应用而言,默认的已基本满足开发使用,基本的主题配色已经足够。但是此时一个问题出现了,如果我存在其他的主题配色呢?

传统做法

在传统的 View 体系中,我们一般都会将颜色定义在 color.xml 文件中,在使用的时候直接读取即可,getColor(R.xx) ,这个大家都已经很熟悉了,那么在 Compose 中呢?

image-20211025151217426

Compose

在 Compose 中,google 将颜色数值统一放在了 theme 下的 color.kt 中,这其实也就是全局静态变量,乍一看好像没什么问题,那我的业务颜色放在那里呢,总不能都全局暴露吧?

image-20211025151546986

但是聪明的你肯定知道,我按照老办法放到 color.xml 里不就行哈,这样也不是不可以,但是随之而来的问题如下:

  • 切换主题时候,颜色怎么统一解决?
  • 在 Google 的 simple 里,color.xml 里往往不会写任何配置,即 Google 本身不建议在 compose 里这样用

那么我该怎么办,我去看看google的simple,看看他们怎么解决:

image-20211025152543420

simple果然是simple 😑 ,Google 完全按照 Material 的标准,即不会出现其他的非主题配色,那实际呢,我们开发怎么办。然后我搜了下目前github上大佬们写的一些开源项目,发现他们也都是按照 Material 去实现,但是很明显这样很不符合实际(国情)。🙃

解决思路

随心所欲写法(不推荐)

形容 没什么标准,直接卷起袖子撸代码,左脑思考,右手开敲,拿起 ⌨️ 就是干,又指新时代埋头苦干的 👷🏻‍♂️

既然官方没写怎么解决,那就自己想办法解决喽。

compose 中,对于数据的改变监听是使用 MutableState ,那么我自己自定义一个单例持有类,持有现有的主题配置,然后定义一个业务颜色类,并且定义相应的主题颜色类对象,最终根据当前单例的主题配置,去判断最终使用什么颜色板即可,更改业务主题时只需要更改这个单例主题配置字段即可。一想到如此简单,我可真是个抖机灵,说干就干 👨‍🔧‍

创建主题枚举
enum class ColorPallet {
// 默认就给两个颜色,根据需求可以定义多个
DARK, LIGHT
}
增加主题配置单例
object ColorsManager {
/** 使用一个变量维护当前主题 */
var pallet by mutableStateOf(ColorPallet.LIGHT)
}
增加颜色板
/** 共用的颜色 */
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

/** 业务颜色配置,如果需要增加其他主题业务色,直接定义相应的下述对象即可,如果某个颜色共用,则增加默认值 */
open class CkColor(val homeBackColor: Color, val homeTitleTvColor: Color = Color.Gray)

/** 提前定义好的业务颜色模板对象 */
private val CkDarkColor = CkColor(
homeBackColor = Color.Black
)

private val CkLightColor = CkColor(
homeBackColor = Color.White
)

/** 默认的颜色配置,即Md的默认配置颜色 */
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
)
增加统一调用入口

为了便于实际使用,我们增加一个 MaterialTheme.ckColor的扩展函数,以便使用我们自定义的颜色组:

/** 增加扩展 */
val MaterialTheme.ckColor: CkColor
get() = when (ColorsManager.pallet) {
ColorPallet.DARK -> CkDarkColor
ColorPallet.LIGHT -> CkLightColor
}
最终的主题如下
@Composable
fun CkTheme(
pallet: ColorPallet = ColorsManager.pallet,
content: @Composable() () -> Unit
) {
val colors = when (pallet) {
ColorPallet.DARK -> DarkColorPalette
ColorPallet.LIGHT -> LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

效果图

 

看效果也还成,简单粗暴,[看着] 也没什么问题,那有没有什么其他方式呢?我还是不相信官方没有写,可能是我疏忽了。

自定义颜色系统(官方)

就在我翻官方文档时,突然看见了这样几个小字,它实现了自定义颜色系统

image-20211025180600191

真是瞎了自己的眼,居然没看到这行字,有了官方示例,于是就赶紧去学习(抄)代码。

增加颜色模板
enum class StylePallet {
// 默认就给两个颜色,根据需求可以定义多个
DARK, LIGHT
}

// 示例,正确做法是放到color.kt下
val Blue50 = Color(0xFFE3F2FD)
val Blue200 = Color(0xFF90CAF9)
val A900 = Color(0xFF0D47A1)

/**
* 实际主题的颜色集,所有颜色都需要添加到其中,并使用相应的子类覆盖颜色。
* 每一次的更改都需要将颜色配置在下方 [CkColors] 中,并同步 [CkDarkColor] 与 [CkLightColor]
* */

@Stable
class CkColors(
homeBackColor: Color,
homeTitleTvColor: Color
) {
var homeBackColor by mutableStateOf(homeBackColor)
private set
var homeTitleTvColor by mutableStateOf(homeTitleTvColor)
private set

fun update(colors: CkColors) {
this.homeBackColor = colors.homeBackColor
this.homeTitleTvColor = colors.homeTitleTvColor
}

fun copy() = CkColors(homeBackColor, homeTitleTvColor)
}

/** 提前定义好的颜色模板对象 */
private val CkDarkColors = CkColors(
homeBackColor = A900,
homeTitleTvColor = Blue50,
)

private val CkLightColors = CkColors(
homeBackColor = Blue200,
homeTitleTvColor = Color.White,
)
增加 xxLocalProvider
@Composable
fun ProvideLcColors(colors: CkColors, content: @Composable () -> Unit) {
val colorPalette = remember {
colors.copy()
}
colorPalette.update(colors)
CompositionLocalProvider(LocalLcColors provides colorPalette, content = content)
}
增加 LocalLcColors 静态变量
// 创建静态 CompositionLocal ,通常情况下主题改动不会很频繁
private val LocalLcColors = staticCompositionLocalOf {
CkLightColors
}
增加主题配置单例
/* 针对当前主题配置颜色板扩展属性 */
private val StylePallet.colors: Pair<Colors, CkColors>
get() = when (this) {
StylePallet.DARK -> DarkColorPalette to CkDarkColors
StylePallet.LIGHT -> LightColorPalette to CkLightColors
}


/** CkX-Compose主题管理者 */
object CkXTheme {
/** 从CompositionLocal中取出相应的Local */
val colors: CkColors
@Composable
get() = LocalLcColors.current

/** 使用一个state维护当前主题配置,这里的写法取决于具体业务,
如果你使用了深色模式默认配置,则无需这个变量,即app只支持深色与亮色,
那么只需要每次读系统配置即可。但是compose本身可以做到快速切换主题,
那么维护一个变量是肯定没法避免的 */

var pallet by mutableStateOf(StylePallet.LIGHT)
}
最终主题代码
@Composable
fun CkXTheme(
pallet: StylePallet = CkXTheme.pallet,
content: @Composable () -> Unit
) {
val (colorPalette, lcColors) = pallet.colors
ProvideLcColors(colors = lcColors) {
MaterialTheme(
colors = colorPalette,
typography = Typography,
shapes = Shapes,
content = content
)
}
}

分析

最终的效果和上述的一致,也就不具体赘述了,我们主要来分析一下,为什么Google要这么写:

我们可以看到上述的示例里主要是使用了 CompositionLocalProvider 去保存当前的主题配置 ,而 CompositionLocalProvider 又继承自 CompositionLocal ,比如我们常用的 MaterialTheme 主题中的 Shapes ,typography 都是由此来管理。

CkColors 这个类上增加了 @Stable ,其代表着对于 Compose 而言,这个类是一个稳定的类,即每次更改不会引发重组,内部的颜色字段使用了 mustbaleStateOf 包装,以便当颜色更改时触发重组,内部还增加了 update() 与 copy() 方法,以便于管理与单向数据的更改。

其实如果我们去看的 Colors 类。就会发现上述示例中的 CkColors 和其是完全一样的设计方式。

image-20211027155601502

所以在Compose中自定义主题颜色,其实就是我们在 Colors 的基础上自己又写了一套自己的配色。😂

既然这样,那为什么我们不直接继承Colors去增加配色呢?使用的时候我强制一下不就行,这样不就不用再自己造什么 CompositionLocal 了?

其实很好理解,因为 Colors 中的 copy() 以及 update() 无法被重写,即没加 open ,而且其内部变量使用了 internal 修饰 set 方法。更重要的原因是这样 不符合Md的设计 ,所以这也就是为什么 需要我们去自定义自己的颜色系统,甚至于可以完全自定义自己的主题系统。前提是你觉得自定义的主题里面再包装一层 MaterialTheme 主题比较丑陋的话,当然相应的,你也需要考虑如何解决其他附带的问题。


收起阅读 »

小程序原理 及 优化

小程序使用的是双线程 在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 > 两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 Native 的 JS...
继续阅读 »

小程序使用的是双线程



在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 >




两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 NativeJSBrigde 做中转



小程序的启动过程



  • 1、小程序初始化: 微信初始化小程序环境:包括 Js 引擎WebView 进行初始化,并注入公共基础库。 这步是微信做的,在用户打开小程序之前就已经准备好了,是小程序运行环境预加载。

  • 2、下载小程序代码包 对小程序业务代码包进行下载:下载的不是小程序的源代码,而是编译、压缩、打包之后的代码。

  • 3、加载小程序代码包 对下载完成对代码包进行注入执行。 此时,app.js、页面所在的 Js 文件和所有其他被require 的 Js 文件会被自动执行一次,小程序基础库会完成所有页面的注册。

  • 4、初始化小程序首页 拉取数据,从逻辑层传递到视图层,进行渲染


setData 的工作原理



  • 1、调用setData方法;

  • 2、逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,将待传输数据转换成字符串并拼接到特定的JS脚本, 并通过 evaluateJavascript 执行脚本将数据传输到渲染层。

  • 3、渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。

  • 4、WebView 线程开始执行渲染时,将 data setData 数据套用在WXML 片段上,得到一个新节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。最后,将 setData 数据合并到 data 中,并用新节点树替换旧节点树,用于下一次重渲染


小程序官方性能指标



  • 1、首屏时间不超过 5 秒

  • 2、渲染时间不超过 500ms

  • 3、每秒调用 setData 的次数不超过 20 次

  • 4、setData 的数据在 JSON.stringify 后不超过 256kb

  • 5、页面 WXML 节点少于 1000 个,节点树深度少于 30 层子节点数不大于 60 个

  • 6、所有网络请求都在 1 秒内返回结果;


小程序优化


1、分包并且使用


分包预加载(通过配置 preloadRule) 将访问率低的页面放入子包里,按需加载;启动时需要访问的页面及其依赖的资源文件应放在主包中。 不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转;可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;


image.png


2、采用独立分包技术(感觉开普勒黄金流程源码可以独立分包)


主包+子包的方式,,如果要跳到子包里,还是会加载主包然后加载子包;采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;


3、异步请求可以在页面onLoad就加载


4、注意利用缓存


利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新


5、及时反馈


及时对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了 先反馈,再请求。比如说,点赞的按钮,可以先改变按钮的样式,再 发起异步请求。


6、可拆分的部分尽量使用自定义组件


自定义组件的更新并不会影响页面上其他元素的更新,各个组件具有独立的逻辑空间、数据、样式环境及 setData 调用


7、避免不当的使用onPageScroll


避免在onPageScroll 中执行复杂的逻辑,避免在onPageScroll中频繁使用setData,避免在onPageScroll中 频繁查询几点信息(selectQuery


8、减少在代码包中直接嵌入的资源文件;图片放在cdn,使用适当的图片格式


9、setData 优化


(1)与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;


this.setData({ 
a: '与渲染有关的字符串',
b: '与渲染无关的字符串'
})
// 可以优化为
this.setData({
a: '与渲染有关的字符串'
})
this.b = '与渲染无关的字符串'

(2)不要过于频繁调用 setData,将多次 setData 合并成一次 setData 调用


(3)数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示数据结构比较复杂包含长字符串,则不应使用setData来设置这些数据


(4)列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新


this.setData({ 
`list[${index}]` = newList[index]
})

(5)切勿在后台页面进行setData(就是不要再页面跳转后使用setData) 页面跳转后,代码逻辑还在执行,此时多个webview是共享一个js进程;后台的setData操作会抢占前台页面的渲染资源;


10、避免过多的页面节点数


页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。


每次执行 setData 更新视图,WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。


11、事件使用不当


(1)去掉不必要的事件绑定(WXML中的bindcatch),从而减少通信的数据量和次数;
(2)事件绑定时需要传输targetcurrentTargetdataset,因而不要在节点的data前缀属性中放置过大的数据


12、逻辑后移,精简业务逻辑


就比如咱们生成分享图片,再比如领取新人券的时候将是否是新人是否符合风控条件和最终领券封装为一个接口


13、数据预拉取(重要


小程序官方为开发者提供了一个在小程序冷启动时提前拉取第三方接口的能力 developers.weixin.qq.com/miniprogram… 预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度


14、跳转时预拉取


可以在发起跳转前(如 wx.navigateTo 调用前),提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可


15、非关键渲染数据延迟请求


小程序会发起一个聚合接口请求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,通过拆分的手段来降低主接口的调用时延,同时减少响应体的数据量,缩减网络传输时间。


16、分屏渲染


在 主体模块 的基础上再度划分出 首屏模块 和 非首屏模块(比如京挑好货的猜你喜欢模块),在所有首屏模块都渲染完成后才会渲染非首屏模块和非主体模块,以此确保首屏内容以最快速度呈现


17、接口聚合,请求合并(主要解决小程序中针对 API 调用次数的限制)


在小程序中针对 API 调用次数的限制: wx.request (HTTP 连接)的最大并发限制是 10 个; wx.connectSocket (WebSocket 连接)的最大并发限制是 5 个;


18、事件总线,替代组件间数据绑定的通信方式


通过事件总线(EventBus),也就是发布/订阅模式,来完成由父向子的数据传递


19、大图裁剪为多块加载


20、长列表优化


(1)不要每次加载更多的时候 都用concat
每获取到新一页数据时,就把它们concatlist上去,这样就会导致每次setData时的list越来越长越来越长,渲染速度也就越来越慢
(2)分批setData,减少一次setData的数量。不要一次性setData list,而是把每一页一批一批地set Data到这个list中去


this.setData({ 
['feedList[' + (page - 1) + ']']: newVal,
})

(3)运用官方的 IntersectionObserver.relativeToViewport 将超出或者没进入可视区的 部分卸载掉(适用于一次加载很多的列表数据,超出了两屏高度所展示的内容)


image.png


this.extData.listItemContainer.relativeToViewport({ top: showNum * windowHeight, bottom: showNum * windowHeight }) 
.observe(`#list-item-${this.data.skeletonId}`, (res) => {
let { intersectionRatio } = res
if (intersectionRatio === 0) {
console.log('【卸载】', this.data.skeletonId, '超过预定范围,从页面卸载')
this.setData({
showSlot: false
})
} else {
console.log('【进入】', this.data.skeletonId, '达到预定范围,渲染进页面')
this.setData({
showSlot: true,
height: res.boundingClientRect.height
})
}
})

21、合理运用函数的防抖与节流,防止出现重复点击及重复请求出现 为避免频繁setData和渲染,做了防抖函数,时间是600ms


作者:甘草倚半夏
链接:https://juejin.cn/post/7023671521075806244

收起阅读 »

Vue 开发规范(下)

提供组件 API 文档 使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。 为什么?良好的文档可以让开发者比较容易的...
继续阅读 »

提供组件 API 文档


使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。


为什么?

良好的文档可以让开发者比较容易的对组件有一个整体的认识,而不用去阅读组件的源码,也更方便开发者使用。

组件配置属性即组件的 API,对于组件的用户来说他们更感兴趣的是 API 而不是实现原理。

正式的文档会告诉开发者组件 API 变更以及向后的兼容性情况

README.md 是标准的我们应该首先阅读的文档文件。代码托管网站(GitHub、Bitbucket、Gitlab 等)会默认在仓库中展示该文件作为仓库的介绍。


怎么做?


在模块目录中添加 README.md 文件:


range-slider/
├── range-slider.vue
├── range-slider.less
└── README.md
在 README 文件中说明模块的功能以及使用场景。对于 vue 组件来说,比较有用的描述是组件的自定义属性即 API 的描述介绍。


提供组件 demo


添加 index.html 文件作为组件的 demo 示例,并提供不同配置情况的效果,说明组件是如何使用的。


为什么?

demo 可以说明组件是独立可使用的。

demo 可以让开发者预览组件的功能效果。

demo 可以展示组件各种配置参数下的功能。


对组件文件进行代码校验


代码校验可以保持代码的统一性以及追踪语法错误。.vue 文件可以通过使用 eslint-plugin-html插件来校验代码。你可以通过 vue-cli 来开始你的项目,vue-cli 默认会开启代码校验功能。


为什么?

保证所有的开发者使用同样的编码规范。

更早的感知到语法错误。


怎么做?


为了校验工具能够校验 *.vue文件,你需要将代码编写在

ESLint
ESLint 需要通过 ESLint HTML 插件来抽取组件中的代码。


通过 .eslintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项:


{
"extends": "eslint:recommended",
"plugins": ["html"],
"env": {
"browser": true
},
"globals": {
"opts": true,
"vue": true
}
}
运行 ESLint


eslint src/**/*.vue


JSHint
JSHint 可以解析 HTML(使用 --extra-ext命令参数)和抽取代码(使用 --extract=auto命令参数)。


通过 .jshintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项。


{
"browser": true,
"predef": ["opts", "vue"]
}
运行 JSHint


jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
注:JSHint 不接受 vue 扩展名的文件,只支持 html。


只在需要时创建组件


为什么?


Vue.js 是一个基于组件的框架。如果你不知道何时创建组件可能会导致以下问题:



  • 如果组件太大, 可能很难重用和维护;

  • 如果组件太小,你的项目就会(因为深层次的嵌套而)被淹没,也更难使组件间通信;


怎么做?



  • 始终记住为你的项目需求构建你的组件,但是你也应该尝试想到它们能够从中脱颖而出(独立于项目之外)。如果它们能够在你项目之外工作,就像一个库那样,就会使得它们更加健壮和一致。

  • 尽可能早地构建你的组件总是更好的,因为这样使得你可以在一个已经存在和稳定的组件上构建你的组件间通信(props & events)。


规则



  • 首先,尽可能早地尝试构建出诸如模态框、提示框、工具条、菜单、头部等这些明显的(通用型)组件。总之,你知道的这些组件以后一定会在当前页面或者是全局范围内需要。

  • 第二,在每一个新的开发项目中,对于一整个页面或者其中的一部分,在进行开发前先尝试思考一下。如果你认为它有一部分应该是一个组件,那么就创建它吧。

  • 最后,如果你不确定,那就不要。避免那些“以后可能会有用”的组件污染你的项目。它们可能会永远的只是(静静地)待在那里,这一点也不聪明。注意,一旦你意识到应该这么做,最好是就把它打破,以避免与项目的其他部分构成兼容性和复杂性。


Vue 组件规范


<!-- iview 等第三方公共组件,推荐大写开头 -->
<Button> from the top</Button>
<Row>
<Col span="24">
</Col>
</Row>


/** * 公共组件 项目内,自己开发的 推荐p开头 * import pLinkpage from 'public/module/linkage' */


<p-linkage v-model="form.pcarea"></p-linkage>


/** * 非公共组件 项目内,自己开发的推荐v开头 * import vSearch from './search' */
<v-search @search="params = $event"></v-search>

自闭合组件


在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。


自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。


不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。


// 反例
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>


<!-- 在 DOM 模板中 -->
<my-component/>
// 好例子
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>


<!-- 在 DOM 模板中 -->
<my-component></my-component>

作者:_Battle
链接:https://juejin.cn/post/7023549490372182052/

收起阅读 »

Vue 开发规范(中)

上一篇:https://www.imgeek.org/article/825358938将 this 赋值给 component 变量 在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 c...
继续阅读 »

上一篇:https://www.imgeek.org/article/825358938

将 this 赋值给 component 变量


在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 component 变量。


换句话说,如果你正在使用 ES6 的话,就不要再编写 var self = this; 这样的代码了,您可以安全地使用 Vue 组件。


为什么?



  • 使用 ES6,就不再需要将 this 保存到一个变量中了。

  • 一般来说,当你使用箭头函数时,会保留 this 的作用域。(译者注:箭头函数没有它自己的 this 值,箭头函数内的 this 值继承自外围作用域。)

  • 如果你没有使用 ES6,当然也就不会使用 箭头函数 啦,那你必须将 “this” 保存到到某个变量中。这是唯一的例外。


怎么做?


<script type="text/javascript"> 
export default { methods: { hello() { return 'hello'; }, printHello() { console.log(this.hello()); }, }, };
</script>
<!-- 避免 -->
<script type="text/javascript">
export default { methods: { hello() { return 'hello'; }, printHello() { const self = this; // 没有必要 console.log(self.hello()); }, }, };
</script>

组件结构化


按照一定的结构组织,使得组件便于理解。


为什么?



  • 导出一个清晰、组织有序的组件,使得代码易于阅读和理解。同时也便于标准化。

  • 按首字母排序 properties、data、computed、watches 和 methods 使得这些对象内的属性便于查找。

  • 合理组织,使得组件易于阅读。(name; extends; props, data 和 computed; components; watch 和 methods; lifecycle methods 等)。

  • 使用 name 属性。借助于 vue devtools 可以让你更方便的测试。

  • 合理的 CSS 结构,如 BEM 或 rscss - 详情?。

  • 使用单文件 .vue 文件格式来组件代码。


怎么做?


组件结构化


<template lang="html">
<div class="Ranger__Wrapper">
<!-- ... -->
</div>
</template>

<script type="text/javascript"> 
export default {
// 不要忘记了 name 属性 name: 'RangeSlider',
// 组合其它组件 extends: {},
// 组件属性、变量 props: { bar: {},
// 按字母顺序 foo: {}, fooBar: {}, },
// 变量 data() {}, computed: {},
// 使用其它组件 components: {},
// 方法 watch: {}, methods: {},
// 生命周期函数 beforeCreate() {}, mounted() {}, };
</script>

<style scoped> .Ranger__Wrapper { /* ... */ } </style>

组件事件命名


Vue.js 提供的处理函数和表达式都是绑定在 ViewModel 上的,组件的每一个事件都应该按照一个好的命名规范来,这样可以避免不少的开发问题,具体可见如下 为什么。


为什么?



  • 开发者可以随意给事件命名,即使是原生事件的名字,这样会带来迷惑性。

  • 过于宽松的事件命名可能与 DOM 模板不兼容。


怎么做?



  • 事件名也使用连字符命名。

  • 一个事件的名字对应组件外的一组意义操作,如:upload-success、upload-error 以及 dropzone-upload-success、dropzone-upload-error (如果需要前缀的话)。

  • 事件命名应该以动词(如 client-api-load) 或是 形容词(如 drive-upload-success)结尾。(出处)


避免 this.$parent


Vue.js 支持组件嵌套,并且子组件可访问父组件的上下文。访问组件之外的上下文违反了基于模块开发的第一原则。因此你应该尽量避免使用 this.$parent。


为什么?



  • 组件必须相互保持独立,Vue 组件也是。如果组件需要访问其父层的上下文就违反了该原则。

  • 如果一个组件需要访问其父组件的上下文,那么该组件将不能在其它上下文中复用。


怎么做?



  • 通过 props 将值传递给子组件。

  • 通过 props 传递回调函数给子组件来达到调用父组件方法的目的。

  • 通过在子组件触发事件来通知父组件。


谨慎使用 this.$refs


Vue.js 支持通过 ref 属性来访问其它组件和 HTML 元素。并通过 this.$refs 可以得到组件或 HTML 元素的上下文。在大多数情况下,通过 this.$refs来访问其它组件的上下文是可以避免的。在使用的的时候你需要注意避免调用了不恰当的组件 API,所以应该尽量避免使用 this.$refs


为什么?



  • 组件必须是保持独立的,如果一个组件的 API 不能够提供所需的功能,那么这个组件在设计、实现上是有问题的。

  • 组件的属性和事件必须足够的给大多数的组件使用。


怎么做?



  • 提供良好的组件 API

  • 总是关注于组件本身的目的。

  • 拒绝定制代码。如果你在一个通用的组件内部编写特定需求的代码,那么代表这个组件的 API 不够通用,或者你可能需要一个新的组件来应对该需求。

  • 检查所有的 props 是否有缺失的,如果有提一个 issue 或是完善这个组件。

  • 检查所有的事件。子组件向父组件通信一般是通过事件来实现的,但是大多数的开发者更多的关注于 props 从忽视了这点。

  • Props向下传递,事件向上传递!。以此为目标升级你的组件,提供良好的 API 和 独立性。

  • 当遇到 propsevents 难以实现的功能时,通过 this.$refs来实现。

  • 当需要操作 DOM 无法通过指令来做的时候可使用 this.$ref 而不是 JQuery、document.getElement*、document.queryElement

  • 基础使用准则是,能不用ParseError: KaTeX parse error: Expected 'EOF', got '就' at position 5: refs就̲尽量不用,如果用,尽量不要通过refs操作状态,可以通过$refs调用methods


<!-- 推荐,并未使用 this.$refs -->
<range :max="max" :min="min" @current-value="currentValue" :step="1"></range>

<!-- 使用 this.$refs 的适用情况-->
<modal ref="basicModal">
<h4>Basic Modal</h4>
<button class="primary" @click="$refs.basicModal.hide()">Close</button>
</modal>
<button @click="$refs.basicModal.open()">Open modal</button>

<!-- Modal component -->
<template>
<div v-show="active">
<!-- ... -->
</div>
</template>

<script> 
export default { // ... data() { return { active: false, }; }, methods: { open() { this.active = true; }, hide() { this.active = false; }, }, // ... };
</script>
<!-- 这里是应该避免的 -->
<!-- 如果可通过 emited 来做则避免通过 this.$refs 直接访问 -->
<template>
<range :max="max" :min="min" ref="range" :step="1"></range>
</template>

<script>
export default { // ... methods: { getRangeCurrentValue() { return this.$refs.range.currentValue; }, }, // ... };
</script>

使用组件名作为样式作用域空间


Vue.js 的组件是自定义元素,这非常适合用来作为样式的根作用域空间。可以将组件名作为 CSS 类的命名空间。


为什么?



  • 给样式加上作用域空间可以避免组件样式影响外部的样式。

  • 保持模块名、目录名、样式根作用域名一样,可以很好的将其关联起来,便于开发者理解。


怎么做?


使用组件名作为样式命名的前缀,可基于 BEM 或 OOCSS 范式。同时给 style 标签加上 scoped 属性。加上 scoped 属性编译后会给组件的 class 自动加上唯一的前缀从而避免样式的冲突。


<style scoped> /* 推荐 */ 
.MyExample { }
.MyExample li { }
.MyExample__item { }
/* 避免 */
.My-Example { }
/* 没有用组件名或模块名限制作用域, 不符合 BEM 规范 */
</style>


作者:_Battle
链接:https://juejin.cn/post/7023548108214648863

收起阅读 »

Vue 开发规范(上)

基于模块开发 始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。 Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。 怎么做? 每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的...
继续阅读 »

基于模块开发


始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。


Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。


怎么做?


每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的、可复用的、微小的 和 可测试的。


如果你的组件做了太多的事或是变得臃肿,请将其拆分成更小的组件并保持单一的原则。一般来说,尽量保证每一个文件的代码行数不要超过 100 行。也请保证组件可独立的运行。比较好的做法是增加一个单独的 demo 示例。


Vue 组件命名


组件的命名需遵从以下原则:



  • 有意义的: 不过于具体,也不过于抽象

  • 简短: 2 到 3 个单词

  • 具有可读性: 以便于沟通交流


同时还需要注意:




  • 必须符合自定义元素规范: 使用连字符分隔单词,切勿使用保留字。




  • app- 前缀作为命名空间: 如果非常通用的话可使用一个单词来命名,这样可以方便于其它项目里复用。




为什么?


组件是通过组件名来调用的。所以组件名必须简短、富有含义并且具有可读性。


如何做?


<!-- 推荐 -->
<app-header></app-header>
<user-list></user-list>
<range-slider></range-slider>

<!-- 避免 -->
<btn-group></btn-group> <!-- 虽然简短但是可读性差. 使用 `button-group` 替代 -->
<ui-slider></ui-slider> <!-- ui 前缀太过于宽泛,在这里意义不明确 -->
<slider></slider> <!-- 与自定义元素规范不兼容 -->

组件表达式简单化


Vue.js 的表达式是 100% 的 Javascript 表达式。这使得其功能性很强大,但也带来潜在的复杂性。因此,你应该尽量保持表达式的简单化。


为什么?




  • 复杂的行内表达式难以阅读。




  • 行内表达式是不能够通用的,这可能会导致重复编码的问题。




  • IDE 基本上不能识别行内表达式语法,所以使用行内表达式 IDE 不能提供自动补全和语法校验功能。




怎么做?


如果你发现写了太多复杂并难以阅读的行内表达式,那么可以使用 method 或是 computed 属性来替代其功能。


<!-- 推荐 -->
<template>
<h1>
{{ `${year}-${month}` }}
</h1>
</template>
<script type="text/javascript"> export default { computed: { month() { return this.twoDigits((new Date()).getUTCMonth() + 1); }, year() { return (new Date()).getUTCFullYear(); } }, methods: { twoDigits(num) { return ('0' + num).slice(-2); } }, }; </script>

<!-- 避免 -->
<template>
<h1>
{{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
</h1>
</template>

组件 props 原子化


虽然 Vue.js 支持传递复杂的 JavaScript 对象通过 props 属性,但是你应该尽可能的使用原始类型的数据。尽量只使用 JavaScript 原始类型(字符串、数字、布尔值)和函数。尽量避免复杂的对象。


为什么?



  • 使得组件 API 清晰直观。

  • 只使用原始类型和函数作为 props 使得组件的 API 更接近于 HTML(5) 原生元素。

  • 其它开发者更好的理解每一个 prop 的含义、作用。

  • 传递过于复杂的对象使得我们不能够清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。


怎么做?


组件的每一个属性单独使用一个 props,并且使用函数或是原始类型的值。







验证组件的 props


在 Vue.js 中,组件的 props 即 API,一个稳定并可预测的 API 会使得你的组件更容易被其他开发者使用。


组件 props 通过自定义标签的属性来传递。属性的值可以是 Vue.js 字符串(:attr="value" 或 v-bind:attr="value")或是不传。你需要保证组件的 props 能应对不同的情况。


为什么?


验证组件 props 可以保证你的组件永远是可用的(防御性编程)。即使其他开发者并未按照你预想的方法使用时也不会出错。


怎么做?



  • 提供默认值。

  • 使用 type 属性校验类型。

  • 使用 props 之前先检查该 prop 是否存在。


<template>
<input type="range" v-model="value" :max="max" :min="min">
</template>
<script type="text/javascript">
export default {
props: {
max: { type: Number, // 这里添加了数字类型的校验 default() { return 10; }, },
min: { type: Number, default() { return 0; }, },
value: { type: Number, default() { return 4; }, },
},
};
</script>

作者:_Battle
链接:https://juejin.cn/post/7023188232368029710

收起阅读 »

带你理解scoped、>>>、/deep/、::v-deep的原理

前言 平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理...
继续阅读 »

前言


平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理解理解理解


1. Scoped CSS的原理


1.1 区别


先带大家看一下无设置Scoped与设置Scoped的区别在哪


无设置Scoped


<div class="login">登录</div>
<style>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码一摸一样的,没有区别。


设置Scoped


<div class="login">登录</div>
<style scoped>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码就有所区别了。如下:


<div data-v-257dda99b class="login">登录</div>
<style scoped>
.login[data-v-257dda99b] {
width: 100px;
height: 100px
}
</style>


我们通过上面的例子,不难发现多了一个data-v-hash属性,也就是说加了scoped,PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom,可以使得组件之间的样式不互相污染。



1.2 原理


Vue的作用域样式 Scoped CSS 的实现思路如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件实例的标识符,我称它为组件实例标识,简称实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个选择器的最后一个选择器单元增加一个属性选择器 原选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id > div,则更改后的选择器为 .cls #id > div[data-v-e0f690c0]


1.3 特点




  1. 将组件的样式的作用范围限制在了组件自身的标签,即:组件内部,包含子组件的根标签,但不包含子组件的除根标签之外的其它标签;所以 组件的css选择器也不能选择到子组件及后代组件的中的元素(子组件的根元素除外);



    因为它给选择器的最后一个选择器单元增加了属性选择器 [data-v-实例标识] ,而该属性选择器只能选中当前组件模板中的标签;而对于子组件,只有根元素 即有 能代表子组件的标签属性 data-v-子实例标识,又有能代表当前组件(父组件)的 签属性 data-v-父实例标识,子组件的其它非根元素,仅有能代表子组件的标签属性 data-v-子实例标识





  2. 如果递归组件有后代选择器,则该选择器会打破特性1中所说的子组件限制,从而选中递归子组件的中元素;



    原因:假设递归组件A的作用域样式中有选择器有后代选择器 div p ,则在每次递归中都会为本次递归创建新的组件实例,同时也会为该实例生成对应的选择器 div p[data-v-当前递归组件实例的实例标识],对于递归组件的除了第一个递归实例之外的所有递归实例来说,虽然 div p[data-v-当前递归组件实例的实例标识] 不会选中子组件实例(递归子组件的实例)中的 p 元素(具体原因已在特性1中讲解),但是它会选中当前组件实例中所有的 p 元素,因为 父组件实例(递归父组件的实例)中有匹配的 div 元素;





2. >>>、/deep/、::v-deep深度选择器的原理


2.1 例子


实际开发中遇到的例子:当我们开发一个页面使用了子组件的时候,如果这时候需要改子组件的样式,但是又不影响其他页面使用这个子组件的样式的时候。比如:


父组件:Parent.vue


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
.child .dyx p {
background-color: red;
}
</style>

子组件:Child.vue


<template>
<div class="child">
<h1>我是子组件</h1>
<div class="dyx">
<p>我是子组件的段落</p>
</div>
</div>
</template>

<style scoped>
.child .dyx p {
background-color: blue;
}
</style>

这时候我们就会发现没有效果。但是如果我们使用>>>/deep/::v-deep三个深度选择器其中一个就能实现了。看代码:


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
::v-deep .child .dyx p {
background-color: red;
}
</style>

2.2 原理


如果你希望 scoped 样式中的一个选择器能够选择到子组 或 后代组件中的元素,我们可以使用 深度作用选择器,它有三种写法:



  • >>>,示例: .gby div >>> #dyx p

  • /deep/,示例: .gby div /deep/ #dyx p.gby div/deep/ #dyx p

  • ::v-deep,示例: .gby div::v-deep #dyx p.gby div::v-deep #dyx p


它的原理与 Scoped CSS 的原理基本一样,只是第3步有些不同(前2步一样),具体如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件的标识符,我称它为实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个深度作用选择器前面的一个选择器单元增加一个属性选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id >>> div,则更改后的选择器为 .cls #id[data-v-e0f690c0] div


因为Vue不会为深度作用选择器后面的选择器单元增加 属性选择器[data-v-实例标识],所以,后面的选择器单元能够选择到子组件及后代组件中的元素;



收起阅读 »

Moshi踩坑之ArrayList

就是这个错 moshi让你写自定义Adapter呢,No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Mo...
继续阅读 »

就是这个错 moshi让你写自定义Adapter呢,

No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.
java.lang.IllegalArgumentException: No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.

解决方法

代码如下自己看吧,这几天就因为这个moshi搞死人哦。

abstract class MoshiArrayListJsonAdapter<C : MutableCollection<T>?, T> private constructor(
private val elementAdapter: JsonAdapter<T>
) :
JsonAdapter<C>() {
abstract fun newCollection(): C

@Throws(IOException::class)
override fun fromJson(reader: JsonReader): C {
val result = newCollection()
reader.beginArray()
while (reader.hasNext()) {
result?.add(elementAdapter.fromJson(reader)!!)
}
reader.endArray()
return result
}

@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: C?) {
writer.beginArray()
for (element in value!!) {
elementAdapter.toJson(writer, element)
}
writer.endArray()
}

override fun toString(): String {
return "$elementAdapter.collection()"
}

companion object {
val FACTORY = Factory { type, annotations, moshi ->
val rawType = Types.getRawType(type)
if (annotations.isNotEmpty()) return@Factory null
if (rawType == ArrayList::class.java) {
return@Factory newArrayListAdapter<Any>(
type,
moshi
).nullSafe()
}
null
}

private fun <T> newArrayListAdapter(
type: Type,
moshi: Moshi
): JsonAdapter<MutableCollection<T>> {
val elementType =
Types.collectionElementType(
type,
MutableCollection::class.java
)

val elementAdapter: JsonAdapter<T> = moshi.adapter(elementType)

return object :
MoshiArrayListJsonAdapter<MutableCollection<T>, T>(elementAdapter) {
override fun newCollection(): MutableCollection<T> {
return ArrayList()
}
}
}
}
}

用法

本来不想写 但怕有人骂我代码不写全

    Moshi.Builder()
.add(MoshiArrayListJsonAdapter.FACTORY)
.build()


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

收起阅读 »

Android 序列化(Serializable和Parcelable)

什么是序列化 由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。 🔥 为什么序列化 永久的保存对象数据(将对象数据保存在文件当中、或者是磁盘中)。 对象在网络中传递。 对象...
继续阅读 »

什么是序列化


由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。


🔥 为什么序列化



  • 永久的保存对象数据(将对象数据保存在文件当中、或者是磁盘中)。

  • 对象在网络中传递。

  • 对象在IPC间传递。


🔥 实现序列化的方式



  • 实现Serializable接口

  • 实现Parcelable接口


🔥 Serializable 和 Parcelable 区别




  • Serializable 是Java本身就支持的接口。




  • Parcelable 是Android特有的接口,效率比实现Serializable接口高效(可用于Intent数据传递,也可以用于进程间通信(IPC))。




  • Serializable的实现,只需要implements Serializable即可。这只是给对象打了一个标记,系统会自动将其序列化。




  • Parcelabel的实现,不仅需要implements Parcelabel,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator接口。




  • Serializable 使用I/O读写存储在硬盘上,而Parcelable是直接在内存中读写。




  • Serializable 会使用反射,序列化和反序列化过程需要大量I/O操作,Parcelable 自己实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在Native内存中,效率要快很多




💥 实现Serializable


import java.io.Serializable;

public class UserSerializable implements Serializable {
public String name;
public int age;
}

然后你会发现没有serialVersionUID


Android Studio 是默认关闭 serialVersionUID 生成提示的,我们需要打开Setting,执行如下操作:



再次回到UserSerializable类,有个提示,就可以添加serialVersionUID了。



效果如下:


public class UserSerializable implements Serializable {
private static final long serialVersionUID = 1522126340746830861L;
public String name;
public int age = 0;

}

💥 实现Parcelable


Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法。如下:


 public class MyParcelable implements Parcelable {
private int mData;

public int describeContents() {
return 0;
}

public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
}

public static final Parcelable.Creator<MyParcelable> CREATOR
= new Parcelable.Creator<MyParcelable>() {
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}

public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};

private MyParcelable(Parcel in) {
mData = in.readInt();
}
}

此时Android Studio 给我们了一个插件可自动生成Parcelable 。


🔥 自动生成 Parcelable


public class User {
String name;
int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

想进行序列化,但是自己写太麻烦了,这里介绍个插件操作简单易上手。


💥先下载



💥使用





💥效果


public class User implements Parcelable {
String name;
int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeInt(this.age);
}

public void readFromParcel(Parcel source) {
this.name = source.readString();
this.age = source.readInt();
}

public User() {
}

protected User(Parcel in) {
this.name = in.readString();
this.age = in.readInt();
}

public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}

@Override
public User[] newArray(int size) {
return new User[size];
}
};
}

搞定。


写完了咱就运行走一波。


🔥 使用实例


💥 Serializable



MainActivity.class
Bundle bundle = new Bundle();
UserSerializable userSerializable=new UserSerializable("SCC",15);
bundle.putSerializable("user",userSerializable);
Intent intent = new Intent(MainActivity.this, BunderActivity.class);
intent.putExtra("user",bundle);
startActivity(intent);

BunderActivity.class
Bundle bundle = getIntent().getBundleExtra("user");
UserSerializable userSerializable= (UserSerializable) bundle.getSerializable("user");
MLog.e("Serializable:"+userSerializable.name+userSerializable.age);

日志:
2021-10-25 E/-SCC-: Serializable:SCC15

💥 Parcelable



MainActivity.class
Bundle bundle = new Bundle();
bundle.putParcelable("user",new UserParcelable("SCC",15));
Intent intent = new Intent(MainActivity.this, BunderActivity.class);
intent.putExtra("user",bundle);
startActivity(intent);

BunderActivity.class
Bundle bundle = getIntent().getBundleExtra("user");
UserParcelable userParcelable= (UserParcelable) bundle.getParcelable("user");
MLog.e("Parcelable:"+userParcelable.getName()+userParcelable.getAge());

日志:
2021-10-25 E/-SCC-: Parcelable:SCC15

作者:Android帅次
链接:https://juejin.cn/post/7023569402519879693
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

看动画学算法之:队列queue

简介 队列Queue是一个非常常见的数据结构,所谓队列就是先进先出的序列结构。 想象一下我们日常的排队买票,只能向队尾插入数据,然后从队头取数据。在大型项目中常用的消息中间件就是一个队列的非常好的实现。 队列的实现 一个队列需要一个enQueue入队列操作和一...
继续阅读 »

简介


队列Queue是一个非常常见的数据结构,所谓队列就是先进先出的序列结构。


想象一下我们日常的排队买票,只能向队尾插入数据,然后从队头取数据。在大型项目中常用的消息中间件就是一个队列的非常好的实现。


队列的实现


一个队列需要一个enQueue入队列操作和一个DeQueue操作,当然还可以有一些辅助操作,比如isEmpty判断队列是否为空,isFull判断队列是否满员等等。



为了实现在队列头和队列尾进行方便的操作,我们需要保存队首和队尾的标记。


先看一下动画,直观的感受一下队列是怎么入队和出队的。


先看入队:



再看出队:



可以看到入队是从队尾入,而出队是从队首出。


队列的数组实现


和栈一样,队列也有很多种实现方式,最基本的可以使用数组或者链表来实现。


先考虑一下使用数组来存储数据的情况。


我们用head表示队首的index,使用rear表示队尾的index。


当队尾不断插入,队首不断取数据的情况下,很有可能出现下面的情况:



上面图中,head的index已经是2了,rear已经到了数组的最后面,再往数组里面插数据应该怎么插入呢?


如果再往rear后面插入数据,head前面的两个空间就浪费了。这时候需要我们使用循环数组。


循环数组怎么实现呢?只需要把数组的最后一个节点和数组的最前面的一个节点连接即可。



有同学又要问了。数组怎么变成循环数组呢?数组又不能像链表那样前后连接。


不急,我们先考虑一个余数的概念,假如我们知道了数组的capacity,当要想数组插入数据的时候,我们还是照常的将rear+1,但是最后除以数组的capacity, 队尾变到了队首,也就间接的实现了循环数组。


看下java代码是怎么实现的:


public class ArrayQueue {

//存储数据的数组
private int[] array;
//head索引
private int head;
//real索引
private int rear;
//数组容量
private int capacity;

public ArrayQueue (int capacity){
this.capacity=capacity;
this.head=-1;
this.rear =-1;
this.array= new int[capacity];
}

public boolean isEmpty(){
return head == -1;
}

public boolean isFull(){
return (rear +1)%capacity==head;
}

public int getQueueSize(){
if(head == -1){
return 0;
}
return (rear +1-head+capacity)%capacity;
}

//从尾部入队列
public void enQueue(int data){
if(isFull()){
System.out.println("Queue is full");
}else{
//从尾部插入
rear = (rear +1)%capacity;
array[rear]= data;
//如果插入之前队列为空,将head指向real
if(head == -1 ){
head = rear;
}
}
}

//从头部取数据
public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data= array[head];
//如果只有一个元素,则重置head和real
if(head == rear){
head= -1;
rear = -1;
}else{
head = (head+1)%capacity;
}
return data;
}
}
}

大家注意我们的enQueue和deQueue中使用的方法:


rear = (rear +1)%capacity
head = (head+1)%capacity

这两个就是循环数组的实现。


队列的动态数组实现


上面的实现其实有一个问题,数组的大小是写死的,不能够动态扩容。我们再实现一个能够动态扩容的动态数组实现。


    //因为是循环数组,这里不能做简单的数组拷贝
private void extendQueue(){
int newCapacity= capacity*2;
int[] newArray= new int[newCapacity];
//先全部拷贝
System.arraycopy(array,0,newArray,0,array.length);
//如果real<head,表示已经进行循环了,需要将0-head之间的数据置空,并将数据拷贝到新数组的相应位置
if(rear< head){
for(int i=0; i< head; i++){
//重置0-head的数据
newArray[i]= -1;
//拷贝到新的位置
newArray[i+capacity]=array[i];
}
//重置real的位置
rear= rear+capacity;
//重置capacity和array
capacity=newCapacity;
array=newArray;
}
}

需要注意的是,在进行数组扩展的时候,我们不能简单的进行拷贝,因为是循环数组,可能出现rear在head后面的情况。这个时候我们需要对数组进行特殊处理。


其他部分是和普通数组实现基本一样的。


队列的链表实现


除了使用数组,我们还可以使用链表来实现队列,只需要在头部删除和尾部添加即可。


看下java代码实现:


public class LinkedListQueue {
//head节点
private Node headNode;
//rear节点
private Node rearNode;

class Node {
int data;
Node next;
//Node的构造函数
Node(int d) {
data = d;
}
}

public boolean isEmpty(){
return headNode==null;
}

public void enQueue(int data){
Node newNode= new Node(data);
//将rearNode的next指向新插入的节点
if(rearNode !=null){
rearNode.next=newNode;
}
rearNode=newNode;
if(headNode == null){
headNode=newNode;
}
}

public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data=headNode.data;
headNode=headNode.next;
}
return data;
}
}

队列的时间复杂度


上面的3种实现的enQueue和deQueue方法,基本上都可以立马定位到要入队列或者出队列的位置,所以他们的时间复杂度是O(1)。


本文的代码地址:


learn-algorithm


作者:程序那些事
链接:https://juejin.cn/post/7023561574669352996
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

来讨论下 Android 面试该问什么?

经历过一些面试,也面过一些同学。 被面试官问到头皮发麻,也把候选人问得面红耳赤。 曾怨恨问题刁钻刻薄,也曾怀疑提问跑题超纲。 经历过攻守的角色转换后,沉下心,回顾过往,不由得发出感叹。如果要将“面试”作类比的话,我愿意将其比作“相亲”。 之所以这样类比,是因为...
继续阅读 »

经历过一些面试,也面过一些同学。


被面试官问到头皮发麻,也把候选人问得面红耳赤。


曾怨恨问题刁钻刻薄,也曾怀疑提问跑题超纲。


经历过攻守的角色转换后,沉下心,回顾过往,不由得发出感叹。如果要将“面试”作类比的话,我愿意将其比作“相亲”。


之所以这样类比,是因为看似客观的技术面试,其实充斥了各种各样的主观判断。“候选人合不合面试官胃口”可能比“候选人有多优秀”更重要一点。


世界这么大,Android 知识体系这么庞杂,我也时不时地怀疑自己,特别是当 pass 一个候选人之后,这种情感愈发强烈。“是不是自己的知识有局限性?”、“我认为关键的问题,真的这么关键吗?”


带着这样的怀疑,我对自己的面试偏好做了一下总结,在此抛砖引玉,欢迎各路大神指点迷津。


ps:本篇仅关注 Android 应用层开发相关面试。


八股文式问题



  1. Activity 有几种 launch mode?每一种有什么特点?

  2. Service 有几种类型?各有什么应用场景?

  3. 广播有几种注册方式?有什么区别?

  4. Activity 有哪些生命周期回调?

  5. Kotlin 中的扩展函数是什么?

  6. JVM 内存模型是怎么样的?

  7. GC 回收算法?

  8. Java 中有几种引用类型?


这类问题的特点是“只需百度即可立马获得答案”。候选人若做过充足的准备,刷过题,就可以倒背如流。但这些问题也是有价值的,可以快速判断候选人是否了解 Android 的基本概念。


上面的第 6,7 问,我不太喜欢问。原因是“掌握了这个问题对应用层开发能起到什么可见的好处?”


计算机的复杂度高,分层是常用的降低复杂度的方法,层与层之间形成了壁垒,也提高了层内的效率。将单独一层的复杂度吃透,都可能要花去毕生的精力。并不是否定深挖底层的价值,学有余力当然可以打通好几层,但作为 Android 应用层的面试,重点还是要关注应用层的技术细节。(个人愚见,欢迎拍砖~)


但如果面试中全都是八股文式问题,则不太公平,太过偏袒死记硬背者,也可能因此 pass 掉能力很强,但基本问题准备不太充分的候选人。


原理性问题


这类问题旨在考察候选人的技术深度,在会用的技术上,知道为什么用它,及其背后的实现原理。比如:



  1. Android 消息机制是怎么实现的?

  2. Android 触摸事件如何传递?

  3. Android 视图是怎么被绘制出来的?

  4. Android 如何在不同组件间通信?(跨进程,跨线程)

  5. Activity 启动流程?

  6. AMS、PMS、WMS 创建过程?

  7. 手写消息入 MessageQueue 的算法。

  8. RecyclerView 缓存机制?


原理性问题也可以被百度出来,但可能得多看几篇博客再消化一番,最后用自己的语言组织一下,才能在面试中对答如流。


这类问题不同于八股文的地方不仅在于考察了技术深度,还顺带便考察了理解分析能力和总结表达能力。把原理性的东西用简单精炼的语言表达出来并让人听懂也是一种能力。


我不太喜欢问 5、6 这样的问题,还是之前提到的那个原因,即“回答出这样的问题对应用层开发能起到什么可见的好处?”。若是 Android 系统开发工程的面试,倒是很有必要问。


第 7 问将原理性和算法结合,不是让默写算法,而是在考察理解原理的基础上的算法实现能力。若死记硬背原理,通常都写不出。


项目经历类问题


这类问题旨在考察候选人项目经历是否真实,技术栈情况。也可就某一个使用过的技术栈追问背后的原理。


这类问题对面试官要求最高,若是没有一定的技术广度和深度,很难就候选人的技术栈问出好问题。


场景类问题


场景类问题是指设计一个“待解决的问题”,让候选人当场解决。


所有前面的问题,都可以提前准备,若准备足够充分,全部拿下不是问题。而场景题是无法提前准备的。



  1. 如图所示:按住View,移到 View 边界外后松手。这个过程中,哪些触摸事件会被传递,它们是如何传递的?


image.png



  1. 要做一个 1MB * 10 的帧动画,有什么办法优化内存?

  2. 如何防止搜索框过度频繁地发起请求?

  3. 如何实现弹幕?

  4. 如何设计直播间礼物队列?

  5. 设计图片异步加载组件需要注意哪些方面?


第 1 问将原理性问题场景化了,对死记硬背不友好。


这些问题都是应用层开发过程中可能遇到的技术问题,场景类问题是开放性的,没有唯一解,考察候选人的思路、技术积累及综合运用能力,甚至是抗压能力。


但场景类问题也有致命的缺点,受到面试官知识及经验的限制,面试官见过多少世面,就能问出多少问题。若面试官经验恰好和候选人有交集则两情相悦,不然则可能话不投机。所以这类问题也不是公平的,就好像相亲,甲之蜜糖乙之砒霜是有可能出现的。


需求拆解估时问题


即把一个真真切切的迭代需求给到面试者,要求把业务需求拆解成技术步骤,然后为每个步骤精确估时。


不要小看“需求拆解”,首先得深入领会需求,能否把产品想表达的理解到位?,能否意会产品想表达而为表达之意?在实际迭代过程中,产品和研发对需求理解的不一致是屡见不鲜的,候选人会不会和产品成为好朋友?


在深入领会需求的基础上,能否将业务故事拆解成技术步骤?考察候选人掌握的技术栈及其综合运用能力,技术选型及实现方案是否合理?是否将扩展性或性能优化考虑在内?


“估时”可以看出候选人对技术实现细节的熟练程度,假设“用 ViewPager + Fragment 实现分页框架”的估时是 1 天,那说明虽然了解改用什么技术,但并未实践过。但此时的估时是理想化的,因为没有将应用的代码现状考虑在内。


这些问题也是候选者入职之后,在每次迭代时真真切切遇到的问题。“拆解合理,估时准确”不是一件容易的事情,即需要深入领会需求、有丰富的技术栈实战经验,还需要对现有代码框架了然于胸,这是一个成熟研发的标志。


没有找到比需求拆解估时问题更务实的面试题了。若相亲的第一感觉不可靠,那就试着约会一次。


总结


我对面试的偏好是按罗列顺序递进的,但水平有限,经验局限,对 Android 应用层的面试也只能停留在这个阶段。还望掘金大神点拨~


作者:唐子玄
链接:https://juejin.cn/post/7023508547832905758
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift 方法

Swift 方法是与某些特定类型相关联的函数在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。实例方法在 Swift 语言中,实例...
继续阅读 »

Swift 方法是与某些特定类型相关联的函数

在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。


实例方法

在 Swift 语言中,实例方法是属于某个特定类、结构体或者枚举类型实例的方法。

实例方法提供以下方法:

  • 可以访问和修改实例属性

  • 提供与实例目的相关的功能

实例方法要写在它所属的类型的前后大括号({})之间。

实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。

实例方法只能被它所属的类的某个特定实例调用。

实例方法不能脱离于现存的实例而被调用。

语法

func funcname(Parameters) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

import Cocoa

class Counter {
var count = 0
func increment() {
count += 1
}
func incrementBy(amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
// 初始计数值是0
let counter = Counter()

// 计数值现在是1
counter.increment()

// 计数值现在是6
counter.incrementBy(amount: 5)
print(counter.count)

// 计数值现在是0
counter.reset()
print(counter.count)

以上程序执行输出结果为:

6
0

Counter类定义了三个实例方法:

  • increment 让计数器按 1 递增;
  • incrementBy(amount: Int) 让计数器按一个指定的整数值递增;
  • reset 将计数器重置为0。

Counter 这个类还声明了一个可变属性 count,用它来保持对当前计数器值的追踪。


方法的局部参数名称和外部参数名称

Swift 函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用

Swift 中的方法和 Objective-C 中的方法极其相似。像在 Objective-C 中一样,Swift 中方法的名称通常用一个介词指向方法的第一个参数,比如:with,for,by等等。

Swift 默认仅给方法的第一个参数名称一个局部参数名称;默认同时给第二个和后续的参数名称为全局参数名称。

以下实例中 'no1' 在swift中声明为局部参数名称。'no2' 用于全局的声明并通过外部程序访问。

import Cocoa

class division {
var count: Int = 0
func incrementBy(no1: Int, no2: Int) {
count = no1 / no2
print(count)
}
}

let counter = division()
counter.incrementBy(no1: 1800, no2: 3)
counter.incrementBy(no1: 1600, no2: 5)
counter.incrementBy(no1: 11000, no2: 3)

以上程序执行输出结果为:

600
320
3666

是否提供外部名称设置

我们强制在第一个参数添加外部名称把这个局部名称当作外部名称使用(Swift 2.0前是使用 # 号)。

相反,我们呢也可以使用下划线(_)设置第二个及后续的参数不提供一个外部名称。

import Cocoa

class multiplication {
var count: Int = 0
func incrementBy(first no1: Int, no2: Int) {
count = no1 * no2
print(count)
}
}

let counter = multiplication()
counter.incrementBy(first: 800, no2: 3)
counter.incrementBy(first: 100, no2: 5)
counter.incrementBy(first: 15000, no2: 3)

以上程序执行输出结果为:

2400
500
45000

self 属性

类型的每一个实例都有一个隐含属性叫做self,self 完全等同于该实例本身。

你可以在一个实例的实例方法中使用这个隐含的self属性来引用当前实例。

import Cocoa

class calculations {
let a: Int
let b: Int
let res: Int

init(a: Int, b: Int) {
self.a = a
self.b = b
res = a + b
print("Self 内: \(res)")
}

func tot(c: Int) -> Int {
return res - c
}

func result() {
print("结果为: \(tot(c: 20))")
print("结果为: \(tot(c: 50))")
}
}

let pri = calculations(a: 600, b: 300)
let sum = calculations(a: 1200, b: 300)

pri.result()
sum.result()

以上程序执行输出结果为:

Self 内: 900
Self 内: 1500
结果为: 880
结果为: 850
结果为: 1480
结果为: 1450

在实例方法中修改值类型

Swift 语言中结构体和枚举是值类型。一般情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个具体的方法中修改结构体或者枚举的属性,你可以选择变异(mutating)这个方法,然后方法就可以从方法内部改变它的属性;并且它做的任何改变在方法结束时还会保留在原始结构中。

方法还可以给它隐含的self属性赋值一个全新的实例,这个新实例在方法结束后将替换原来的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
length *= res
breadth *= res

print(length)
print(breadth)
}
}

var val = area(length: 3, breadth: 5)
val.scaleBy(res: 3)
val.scaleBy(res: 30)
val.scaleBy(res: 300)

以上程序执行输出结果为:

9
15
270
450
81000
135000

在可变方法中给 self 赋值

可变方法能够赋给隐含属性 self 一个全新的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
self.length *= res
self.breadth *= res
print(length)
print(breadth)
}
}
var val = area(length: 3, breadth: 5)
val.scaleBy(res: 13)

以上程序执行输出结果为:

39
65

类型方法

实例方法是被类型的某个实例调用的方法,你也可以定义类型本身调用的方法,这种方法就叫做类型方法。

声明结构体和枚举的类型方法,在方法的func关键字之前加上关键字static。类可能会用关键字class来允许子类重写父类的实现方法。

类型方法和实例方法一样用点号(.)语法调用。

import Cocoa

class Math
{
class func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

struct absno
{
static func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

let no = Math.abs(number: -35)
let num = absno.abs(number: -5)

print(no)
print(num)

以上程序执行输出结果为:

35
5
收起阅读 »

Swift 属性

Swift 属性将值跟特定的类、结构或枚举关联。属性可分为存储属性和计算属性:存储属性计算属性存储常量或变量作为实例的一部分计算(而不是存储)一个值用于类和结构体用于类、结构体和枚举存储属性和计算属性通常用于特定类型的实例。属性也可以直接用于类型本身,这种属性...
继续阅读 »

Swift 属性将值跟特定的类、结构或枚举关联。

属性可分为存储属性和计算属性:

存储属性计算属性
存储常量或变量作为实例的一部分计算(而不是存储)一个值
用于类和结构体用于类、结构体和枚举

存储属性和计算属性通常用于特定类型的实例。

属性也可以直接用于类型本身,这种属性称为类型属性。

另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己写的存储属性上,也可以添加到从父类继承的属性上。


存储属性

简单来说,一个存储属性就是存储在特定类或结构体的实例里的一个常量或变量。

存储属性可以是变量存储属性(用关键字var定义),也可以是常量存储属性(用关键字let定义)。

  • 可以在定义存储属性的时候指定默认值

  • 也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值

import Cocoa

struct Number
{
var digits: Int
let pi = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.pi)")

以上程序执行输出结果为:

67
3.1415

考虑以下代码:

let pi = 3.1415

代码中 pi 在定义存储属性的时候指定默认值(pi = 3.1415),所以不管你什么时候实例化结构体,它都不会改变。

如果你定义的是一个常量存储属性,如果尝试修改它就会报错,如下所示:

import Cocoa

struct Number
{
var digits: Int
let numbers = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.numbers)")
n.numbers = 8.7

以上程序,执行会报错,错误如下所示:

error: cannot assign to property: 'numbers' is a 'let' constant
n.numbers = 8.7

意思为 'numbers' 是一个常量,你不能修改它。


延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。

在属性声明前使用 lazy 来标示一个延迟存储属性。

注意:
必须将延迟存储属性声明成变量(使用var关键字),因为属性的值在实例构造完成之前可能无法得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。

延迟存储属性一般用于:

  • 延迟对象的创建。

  • 当属性的值依赖于其他未知类

import Cocoa

class sample {
lazy var no = number() // `var` 关键字是必须的
}

class number {
var name = "Runoob Swift 教程"
}

var firstsample = sample()
print(firstsample.no.name)

以上程序执行输出结果为:

Runoob Swift 教程

实例化变量

如果您有过 Objective-C 经验,应该知道Objective-C 为类实例存储值和引用提供两种方法。对于属性来说,也可以使用实例变量作为属性值的后端存储。

Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。

一个类型中属性的全部信息——包括命名、类型和内存管理特征——都在唯一一个地方(类型定义中)定义。


计算属性

除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。

import Cocoa

class sample {
var no1 = 0.0, no2 = 0.0
var length = 300.0, breadth = 150.0

var middle: (Double, Double) {
get{
return (length / 2, breadth / 2)
}
set(axis){
no1 = axis.0 - (length / 2)
no2 = axis.1 - (breadth / 2)
}
}
}

var result = sample()
print(result.middle)
result.middle = (0.0, 10.0)

print(result.no1)
print(result.no2)

以上程序执行输出结果为:

(150.0, 75.0)
-150.0
-65.0

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。


只读计算属性

只有 getter 没有 setter 的计算属性就是只读计算属性。

只读计算属性总是返回一个值,可以通过点(.)运算符访问,但不能设置新的值。

import Cocoa

class film {
var head = ""
var duration = 0.0
var metaInfo: [String:String] {
return [
"head": self.head,
"duration":"\(self.duration)"
]
}
}

var movie = film()
movie.head = "Swift 属性"
movie.duration = 3.09

print(movie.metaInfo["head"]!)
print(movie.metaInfo["duration"]!)

以上程序执行输出结果为:

Swift 属性
3.09

注意:

必须使用var关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let关键字只用来声明常量属性,表示初始化后再也无法修改的值。


属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,甚至新的值和现在的值相同的时候也不例外。

可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重载属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。

注意:
不需要为无法重载的计算属性添加属性观察器,因为可以通过 setter 直接监控和响应值的变化。

可以为属性添加如下的一个或全部观察器:

  • willSet在设置新的值之前调用
  • didSet在新的值被设置之后立即调用
  • willSet和didSet观察器在属性初始化过程中不会被调用
import Cocoa

class Samplepgm {
var counter: Int = 0{
willSet(newTotal){
print("计数器: \(newTotal)")
}
didSet{
if counter > oldValue {
print("新增数 \(counter - oldValue)")
}
}
}
}
let NewCounter = Samplepgm()
NewCounter.counter = 100
NewCounter.counter = 800

以上程序执行输出结果为:

计数器: 100
新增数 100
计数器: 800
新增数 700

全局变量和局部变量

计算属性和属性观察器所描述的模式也可以用于全局变量和局部变量。

局部变量全局变量
在函数、方法或闭包内部定义的变量。函数、方法、闭包或任何类型之外定义的变量。
用于存储和检索值。用于存储和检索值。
存储属性用于获取和设置值。存储属性用于获取和设置值。
也用于计算属性。也用于计算属性。

类型属性

类型属性是作为类型定义的一部分写在类型最外层的花括号({})内。

使用关键字 static 来定义值类型的类型属性,关键字 class 来为类定义类型属性。

struct Structname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

enum Enumname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

class Classname {
class var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

注意:
例子中的计算型类型属性是只读的,但也可以定义可读可写的计算型类型属性,跟实例计算属性的语法类似。


获取和设置类型属性的值

类似于实例的属性,类型属性的访问也是通过点运算符(.)来进行。但是,类型属性是通过类型本身来获取和设置,而不是通过实例。实例如下:

import Cocoa

struct StudMarks {
static let markCount = 97
static var totalCount = 0
var InternalMarks: Int = 0 {
didSet {
if InternalMarks > StudMarks.markCount {
InternalMarks = StudMarks.markCount
}
if InternalMarks > StudMarks.totalCount {
StudMarks.totalCount = InternalMarks
}
}
}
}

var stud1Mark1 = StudMarks()
var stud1Mark2 = StudMarks()

stud1Mark1.InternalMarks = 98
print(stud1Mark1.InternalMarks)

stud1Mark2.InternalMarks = 87
print(stud1Mark2.InternalMarks)

以上程序执行输出结果为:

97
87
收起阅读 »

Swift 类

Swift 类是构建代码所用的一种通用且灵活的构造体。我们可以为类定义属性(常量、变量)和方法。与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。...
继续阅读 »

Swift 类是构建代码所用的一种通用且灵活的构造体。

我们可以为类定义属性(常量、变量)和方法。

与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。

类和结构体对比

Swift 中类和结构体有很多共同点。共同处在于:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义附属脚本用于访问值
  • 定义构造器用于生成初始化值
  • 通过扩展以增加默认实现的功能
  • 符合协议以对某类提供标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 解构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

语法:

class classname {
Definition 1
Definition 2
……
Definition N
}

类定义

class student{
var studname: String
var mark: Int
var mark2: Int
}

实例化类:

let studrecord = student()

实例

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark = 300
}
let marks = studentMarks()
print("成绩为 \(marks.mark)")

以上程序执行输出结果为:

成绩为 300

作为引用类型访问类属性

类的属性可以通过 . 来访问。格式为:实例化类名.属性名

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark1 = 300
var mark2 = 400
var mark3 = 900
}
let marks = studentMarks()
print("Mark1 is \(marks.mark1)")
print("Mark2 is \(marks.mark2)")
print("Mark3 is \(marks.mark3)")

以上程序执行输出结果为:

Mark1 is 300
Mark2 is 400
Mark3 is 900

恒等运算符

因为类是引用类型,有可能有多个常量和变量在后台同时引用某一个类实例。

为了能够判定两个常量或者变量是否引用同一个类实例,Swift 内建了两个恒等运算符:

恒等运算符不恒等运算符
运算符为:===运算符为:!==
如果两个常量或者变量引用同一个类实例则返回 true如果两个常量或者变量引用不同一个类实例则返回 true

实例

import Cocoa

class SampleClass: Equatable {
let myProperty: String
init(s: String) {
myProperty = s
}
}
func ==(lhs: SampleClass, rhs: SampleClass) -> Bool {
return lhs.myProperty == rhs.myProperty
}

let spClass1 = SampleClass(s: "Hello")
let spClass2 = SampleClass(s: "Hello")

if spClass1 === spClass2 {// false
print("引用相同的类实例 \(spClass1)")
}

if spClass1 !== spClass2 {// true
print("引用不相同的类实例 \(spClass2)")
}

以上程序执行输出结果为:

引用不相同的类实例 SampleClass
收起阅读 »

Swift 结构体

Swift 结构体是构建代码所用的一种通用且灵活的构造体。我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。与 C 和 Objective C 不同的是:结构体不需要包含实现文件和接口。结构体允许我们创建一个单一文件,且系统会自动生成面向...
继续阅读 »

Swift 结构体是构建代码所用的一种通用且灵活的构造体。

我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。

与 C 和 Objective C 不同的是:

  • 结构体不需要包含实现文件和接口。

  • 结构体允许我们创建一个单一文件,且系统会自动生成面向其它代码的外部接口。

结构体总是通过被复制的方式在代码中传递,因此它的值是不可修改的。

语法

我们通过关键字 struct 来定义结构体:

struct nameStruct { 
Definition 1
Definition 2
……
Definition N
}

实例

我们定义一个名为 MarkStruct 的结构体 ,结构体的属性为学生三个科目的分数,数据类型为 Int:

struct MarkStruct{
var mark1: Int
var mark2: Int
var mark3: Int
}

我们可以通过结构体名来访问结构体成员。

结构体实例化使用 let 关键字:

import Cocoa

struct studentMarks {
var mark1 = 100
var mark2 = 78
var mark3 = 98
}
let marks = studentMarks()
print("Mark1 是 \(marks.mark1)")
print("Mark2 是 \(marks.mark2)")
print("Mark3 是 \(marks.mark3)")

以上程序执行输出结果为:

Mark1  100
Mark2 78
Mark3 98

实例中,我们通过结构体名 'studentMarks' 访问学生的成绩。结构体成员初始化为mark1, mark2, mark3,数据类型为整型。

然后我们通过使用 let 关键字将结构体 studentMarks() 实例化并传递给 marks。

最后我们就通过 . 号来访问结构体成员的值。

以下实例化通过结构体实例化时传值并克隆一个结构体:

import Cocoa

struct MarksStruct {
var mark: Int

init
(mark: Int) {
self.mark = mark
}
}
var aStruct = MarksStruct(mark: 98)
var bStruct = aStruct // aStruct 和 bStruct 是使用相同值的结构体!
bStruct
.mark = 97
print(aStruct.mark) // 98
print(bStruct.mark) // 97

以上程序执行输出结果为:

98
97

结构体应用

在你的代码中,你可以使用结构体来定义你的自定义数据类型。

结构体实例总是通过值传递来定义你的自定义数据类型。

按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:

  • 结构体的主要目的是用来封装少量相关简单数据值。
  • 有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。
  • 任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用。
  • 结构体不需要去继承另一个已存在类型的属性或者行为。

举例来说,以下情境中适合使用结构体:

  • 几何形状的大小,封装一个width属性和height属性,两者均为Double类型。
  • 一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。
  • 三维坐标系内一点,封装xyz属性,三者均为Double类型。

结构体实例是通过值传递而不是通过引用传递。

import Cocoa

struct markStruct{
var mark1: Int
var mark2: Int
var mark3: Int

init
(mark1: Int, mark2: Int, mark3: Int){
self.mark1 = mark1
self.mark2 = mark2
self.mark3 = mark3
}
}

print("优异成绩:")
var marks = markStruct(mark1: 98, mark2: 96, mark3:100)
print(marks.mark1)
print(marks.mark2)
print(marks.mark3)

print("糟糕成绩:")
var fail = markStruct(mark1: 34, mark2: 42, mark3: 13)
print(fail.mark1)
print(fail.mark2)
print(fail.mark3)

以上程序执行输出结果为:

优异成绩:
98
96
100
糟糕成绩:
34
42
13

以上实例中我们定义了结构体 markStruct,三个成员属性:mark1, mark2 和 mark3。结构体内使用成员属性使用 self 关键字。

从实例中我们可以很好的理解到结构体实例是通过值传递的。

收起阅读 »

动画曲线天天用,你能自己整一个吗?看完这篇你就会了!

前言最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟。Curve 类定义查看源码, Curve 类定义如下:abstr...
继续阅读 »

前言

最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟。

曲线

Curve 类定义

查看源码, Curve 类定义如下:

abstract class Curve extends ParametricCurve<double> {
const Curve();

@override
double transform(double t) {
if (t == 0.0 || t == 1.0) {
return t;
}
return super.transform(t);
}

Curve get flipped => FlippedCurve(this);
}

看上去好像没定义什么, 实际这里只是做了两个处理,一个是明确的数据类型为 double,另一个是对 transform 做了重载,也只是对参数 t 做了特殊处理,保证参数 t 的范围在0-1之间,且起点值0.0和终点值1.0不被转换函数转换。主要定义在上一层的ParametricCurve。文档是建议子类重载transformInternal方法,那我们就继续往上看ParametricCurve这个类的实现,代码如下:

abstract class ParametricCurve<T> {
const ParametricCurve();

T transform(double t) {
assert(t != null);
assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
return transformInternal(t);
}

@protected
T transformInternal(double t) {
throw UnimplementedError();
}

@override
String toString() => objectRuntimeType(this, 'ParametricCurve');
}

可以看到,实际上 transform 方法除了做参数合法性验证以外,其实就是调用了transformInternal方法,因此子类必须要实现该方法,否则会抛出UnimplementedError异常。

实例解析

上面的源码可以看到,关键在于参数 t。这个参数 t 代表什么呢?注释里说的是:

Returns the value of the curve at point t. — 返回 t 点的曲线对应的值。

因此 t 可以认为是曲线的横坐标,而为了保证曲线的一致性,做了归一化处理,也就是t的取值都是在0-1之间。这么说可能有点抽象,我们来看2个例子来对比就明白了,先看最简单 Curves.linear 的实现。

class _Linear extends Curve {
const _Linear._();

@override
double transformInternal(double t) => t;
}

超级简单吧,直接返回 t,其实对应我们的数学的函数就是:

y = f(t) = t

对应的曲线就是一条斜线。也就是说在设定的动画时间内,会完成从0-1的线性转变,也就是变化是均匀的。 线性这个很好理解,我们再来看一个减速曲线decelerate的实现。

class _DecelerateCurve extends Curve {
const _DecelerateCurve._();

@override
double transformInternal(double t) {
t = 1.0 - t;
return 1.0 - t * t;
}
}

我们先看一下_DecelerateCurve 的计算表达式是什么。减速公式1

回忆一下我们高中物理学的匀减速运动,加速度为负(即减速)的距离计算公式:减速公式2

上面的减速曲线其实就可以看做是初始速度是2,加速度也是2的减速运动。为什么要是2这个值呢,这是因为 t 的取值范围是0-1,这样计算完的结果的取值范围还是0-1。你肯定会问,为什么要保证曲线的计算结果要是0-1? 我们来假设计算结果不为0-1会发生什么情况,比如我们要在屏幕上移动一个组件为60像素。假设动画曲线初始值不为0。那就意味着一开始的移动距离是跳变的。同样的,如果结束值不为1.0,意味着在最后一个点的距离值不是60.0,那么就意味着结束时需要从最后一个点跳到最终的60像素的位置(动画需要保证最终的移动距离是60像素)这样意味着动画会出现跳变的效果,绘制曲线的话会是下的样子(绿色是正常的,红线是异常的)。这样的动画体验是很糟糕的!因此,这是一个关键点,如果你的自定义曲线的 transformInternal 方法的返回值范围不是0-1,就意味着动画会出现跳变,导致动画缺帧的感觉。

image.png

有了这个基础,我们就可以解释动画曲线的基本机制了,实际上就是在给定的动画时间(Duration)范围内,完成组件的初始状态到结束状态的转变,这个转变是沿着设定的 Curve 类完成的,而其横坐标是0-1.0,曲线的初始值和结束值分别是0和1.0,而至于中间值是可以低于0或超过1的。我们可以想像是我们沿着设定的曲线运动,最终无论如何都会达到设定的目的地,而至于怎么走,拐多少道弯,速度怎么变化都是曲线控制的。但是,如果你的曲线初始值不为0或结束值不为1,就像是跳悬崖的那种感觉!

正弦动画曲线

我们来一个正弦曲线的动画验证一下上面的说法。

class SineCurve extends Curve {
final int count;
const SineCurve({this.count = 1}) : assert(count > 0);

@override
double transformInternal(double t) {
return sin(2 * count* pi * t);
}
}

count 参数用于控制周期,即达到目的地之前可以多来几个来回。这里我们发现,初始值是0,但是一个周期(2π)结束值也是0,这样在动画结束前会出现跳变的结果。来看一下示例代码,这个示例是让圆形向下移动60像素。

AnimatedContainer(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(30.0),
),
transform: Matrix4.identity()..translate(0.0, up ? 60.0 : 0.0, 0.0),
duration: Duration(milliseconds: 3000),
curve: SineCurve(count: 1),
child: ClipOval(
child: Container(
width: 60.0,
height: 60.0,
color: Colors.blue,
),
),
)

运行效果如下,注意看最后一帧从0的位置直接跳到了60的位置。

跳动动画

这个怎么调呢,我们来看一下正弦曲线的样子。

正弦曲线

如果我们要满足0-1范围的要求,那么要往后再移动90度才能够达到。但是,这样还有个问题,这样破坏了周期性,比如设置 count=2的时候结果又不对了。我们来看一下规律,实际上只有第一个周期需要多移动90度(途中箭头指向的点),后面的都是按360度(即2π)为周期了。也就是角度其实是按2.5π,4.5π,6.5π……规律来的,对应的角度公式其实就是:调整后公式

所以调整后的正弦曲线代码为:

class SineCurve extends Curve {
final int count;
const SineCurve({this.count = 1}) : assert(count > 0);

@override
double transformInternal(double t) {
// 需要补偿pi/2个角度,使得起始值是0.终止值是1,避免出现最后突然回到0
return sin(2 * (count + 0.25) * pi * t);
}
}

再看调整后的效果,是不是丝滑般地过渡了?调整后动画

总结

本篇介绍了 Flutter 动画曲线类的原理和控制动画的机制,实际上 Curve 类就是在指定的时间内,沿曲线完成从起点到终点的过渡。但是为了保证动画平滑过渡,应该保证自定义曲线的transformInternal方法返回值的起始值和结束值分别是0和1。

收起阅读 »

Android协程(Coroutines)系列-深入理解suspend(挂起函数)关键字

Kotlin 协程把suspend 修饰符引入到了我们 Android 开发者的日常开发中。您是否好奇它的底层工作原理呢?编译器是如何转换我们的代码,使其能够挂起和恢复协程操作的呢?suspend挂起函数,是指把协程代码挂起不继续执行的函数,也叫协程被函数挂起...
继续阅读 »

Kotlin 协程把suspend 修饰符引入到了我们 Android 开发者的日常开发中。您是否好奇它的底层工作原理呢?编译器是如何转换我们的代码,使其能够挂起和恢复协程操作的呢?

suspend

挂起函数,是指把协程代码挂起不继续执行的函数,也叫协程被函数挂起了。协程中调用挂起函数时,协程所在的线程不会挂起也不会阻塞,但是协程被挂起了。也就是说,协程内挂起函数之后的代码停止执行了,直到挂起函数完成后恢复协程,协程才继续执行后续的代码。所有挂起函数都会通过suspend修饰符修饰。

suspend是协程的关键字,每一个被suspend修饰的方法都必须在另一个suspend函数或者Coroutine协程程序中进行调用。

挂起函数(由suspend关键字修饰)的目的是用来挂起协程的执行等待异步计算的结果,所以一个挂起函数通常有两个要点:挂起异步

这里涉及到一种机制俗称CPS(Continuation-Passing-Style:续体传递风格)。每一个suspend修饰的方法或者lambda表达式都会在代码调用的时候为其额外添加Continuation(续体)类型的参数。

Kotlin协程中使用了状态机,编译器会将协程体编译成一个匿名内部类,每一个挂起函数的调用位置对应一个挂起点。

挂起函数意义解释
join挂起当前协程,直到等待的子协程执行完毕通过当前协程返回的Job接口的join方法,可以单纯的挂起当前协程,等待子协程完成后再恢复继续执行
await挂起当前协程,直到等待的子协程返回结果和join的区别是,它属于Job接口的子接口Deferred的方法,可以等待子协程完成后,带着返回值恢复当前协程
delay挂起当前协程,直到指定时间后恢复当前协程单纯挂起当前协程,指定时长后恢复协程执行
withContext()挂起外部协程,直到自己内部协程全部返回后,才会恢复外部的协程。没有创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成并返回结果。类似async.await的效果

协程挂起流程详解

协程实现异步的核心原理就是通过挂起函数实现协程体的挂起,还不阻塞协程体所在的线程。

fun testInMain() {
Log.d("["+Thread.currentThread().name+"]testInMain start")
var job = CoroutineScope(Dispatchers.Main).launch { //启动协程job
Log.d("[" + Thread.currentThread().name+"]job start")
var job1 = async(Dispatchers.IO) { //启动协程job1
Log.d("["+Thread.currentThread().name+"]job1 start")
delay(3000) //挂起job1协程 3秒
Log.d("["+Thread.currentThread().name+"]job1 end ")
"job1-Return"
} //job1协程 续体执行完毕

var job2 = async(Dispatchers.Default) {
Log.d("["+Thread.currentThread().name+"]job2 start" )
delay(1000) //挂起job2协程 1秒
Log.d("["+Thread.currentThread().name+"]job2 end")
"job2-Return"
} //job2协程 续体执行完毕

Log.d("["+Thread.currentThread().name+"]before job1 return")
Log.d("["+Thread.currentThread().name+"]job1 result = " + job1.await()) //挂起job协程,等待job1返回结果;如果已有结果,不挂起,直接返回

Log.d("["+Thread.currentThread().name+"]before job2 return")
Log.d("["+Thread.currentThread().name+"]job2 result = " + job2.await()) //挂起job协程,等待job2返回结果;如果已有结果,不挂起,直接返回

Log.d("["+Thread.currentThread().name+"]job end ")
} //job协程 续体执行完毕

Log.d("["+Thread.currentThread().name+"]testInMain end")
} //testInMain

示例代码的log输出如下,我们需要重点关注Log输出的次序,和时间间隔:

10:15:04.046 26079-26079/com.example.myapplication D/TC: [main]testInMain start
10:15:04.067 26079-26079/com.example.myapplication D/TC: [main]testInMain end
10:15:04.080 26079-26079/com.example.myapplication D/TC:
[main]job start
10:15:04.083 26079-26079/com.example.myapplication D/TC:
[main]before job1 return
10:15:04.086 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job1 start
10:15:04.087 26079-26684/com.example.myapplication D/TC: [DefaultDispatcher-worker-2]job2 start
10:15:05.090 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job2 end
10:15:05.095 26079-26079/com.example.myapplication D/TC:
[main]button-2 onclick now
10:15:07.090 26079-26685/com.example.myapplication D/TC: [DefaultDispatcher-worker-3]job1 end
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job1 result = job1-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]before job2 return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job2 result = job2-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job end
  • 步骤一:在主线程调用TestInMain,直接打印“[main]testInMain start”的log
  • 步骤二:TestInMain方法继续执行完毕,打印“[main]testInMain end”的log
  • 步骤三:job协程被主线程调度执行,打印“[main]job start”的log
  • 步骤四:job协程继续执行,打印“[main]before job1 return”的log
  • 步骤五:job协程被job1.await挂起函数中断执行,退出main线程,等待job1返回结果后再恢复执行
  • 步骤六:job1协程被异步调度到work-1子线程执行,打印“[DefaultDispatcher-worker-1]job1 start”的log,接着被delay挂起函数中断执行,退出work-1子线程,等待delay 3秒结束后再恢复执行
  • 步骤七:job2协程被异步调度到work-2子线程执行,打印“[DefaultDispatcher-worker-2]job2 start”的log,接着被delay挂起函数中断执行,退出work-2子线程,等待delay 1秒结束后再恢复执行
  • 步骤八:1秒钟后(从04秒-05秒),job2协程被delay挂起函数异步调度到[DefaultDispatcher-worker-1]子线程恢复执行,打印“[DefaultDispatcher-worker-1]job2 end”的log,job2续体结束执行,同时将结果存储到job2协程的result字段中。
  • 步骤九:main线程中button-2点击事件被处理,打印“[main]button-2 onclick now”的log
  • 步骤十:3秒钟后(从04秒-07秒),job1协程被delay挂起函数异步调度到[DefaultDispatcher-worker-3]子线程恢复执行,打印“[DefaultDispatcher-worker-3]job1 end”的log,job1续体结束执行,同时将结果存储到job1协程的result字段中。
  • 步骤十一:job1.await挂起函数得到结果,job协程被await挂起函数异步调度到main线程恢复执行,打印“[main]job1 result = job1-Return”的log
  • 步骤十二:job协程继续执行,打印“[main]before job2 return”的log
  • 步骤十三:job协程继续调用job2.await挂起函数,此时job2协程已经有result结果,所有它不会中断job协程的执行,而是直接返回结果,打印“[main]job2 result = job2-Return”的log
  • 步骤十四:job协程继续执行,打印“[main]job end”的log,job续体结束执行。

微信图片_20211025132142.jpg 从图中,我们可以清晰的得到几点结论:

  1. job协程内部,通过await 阻塞了后续代码的执行。job1和job2协程,通过delay阻塞了后续代码的执行。
  2. 协程job1,job2 启动后,保持并行执行。job2 并没有等待job1执行完才启动执行和恢复,而是在各自线程并行执行。
  3. job的后续代码被await 阻塞后,并没有阻塞main线程,main线程中其它模块的代码能同时被执行,并打印出"[main]button 2 onclick now"。
  4. job1 被delay阻塞后续代码执行时,并没有阻塞所在线程[DefaultDispatcher-worker-1],job2中的后续代码被恢复到此[DefaultDispatcher-worker-1]子线程中执行。
  5. job1 和 job2 协程在恢复执行时,并不能确保在原线程中执行后续代码。如log所示,job2在DefaultDispatcher-worker-2中启动和阻塞后,却在DefaultDispatcher-worker-1中恢复了后续的代码执行。

所以可以看出,协程的挂起,并不会阻塞协程所在的线程,而只是中断了协程后面的代码执行。然后等待挂起函数完成后,恢复协程的后续代码执行。这就是协程挂起最最基本的关键点。

协程挂起的实现原理

上节中的示例代码,经过反编译后的核心代码如下:

//TestCoroutin.decompiled.java
public final void testInMain() {
Log.d("cjf---", var10001.append("testInMain start").toString());

Job job = BuildersKt.launch$default( CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getMain()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
//单独拆分到下面,需要详细讲解
}

public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {/......./}

public final Object invoke(Object var1, Object var2){/......./}

}), 3, (Object)null);

Log.d("cjf---", var10001.append("testInMain end ").toString());
}

//job协程的SuspendLambda续体,其invokeSuspend方法代码
public final Object invokeSuspend(@NotNull Object $result) {
... ...
label17: {
Object var8 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
Log.d(var10001.append("job start ").toString());
Deferred job1 = BuildersKt.async$default(/......./);
job2 = BuildersKt.async$defaultdefault(/......./);
Log.d(var10001.append("before job1 return").toString());
var6 = var10001.append("job1 result =");
this.L$0 = job2;
this.L$1 = var5;
this.L$2 = var6;
this.label = 1;
var10000 = job1.await(this);
if (var10000 == var8) {
return var8;
}
break;
case 1:
var6 = (StringBuilder)this.L$2;
var5 = (String)this.L$1;
job2 = (Deferred)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
case 2:
var6 = (StringBuilder)this.L$1;
var5 = (String)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break label17;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

var7 = var10000;
Log.d(var5, var6.append((String)var7).toString());
Log.d(var10001.append("before wait job2 return").toString());
var6 = var10001.append("job2 result = ");
this.L$0 = var5;
this.L$1 = var6;
this.L$2 = null;
this.label = 2;
var10000 = job2.await(this);
if (var10000 == var8) {
return var8;
}
} //end of label17

Log.d(var5, var6.append((String)var7).toString());
Log.d("cjf---", var10001.append("job end ").toString());
return Unit.INSTANCE;
} //end of invokeSuspend

反编译后的主要区别在job协程,其Lambda代码块转换成了Function2 实现。

我们借助APK反编译工具,可以看到执行代码中,Function2 实际上被SuspendLambda 类继承实现。

微信图片_20211025132929.jpg

SuspendLambda实现类的关键逻辑在invokeSuspend方法中,而invokeSuspend方法中采用了CPS(Continuation-Passing-Style) 续体传递风格

续体传递风格会将job协程的Lambda代码块,通过label标签和switch分割成多个代码块。代码块分割的点,就是协程中调用suspend挂起函数的地方。

分支代码调用到await挂起函数时,如果返回了COROUTINE_SUSPENDED,就退出invokeSuspend,进入挂起状态。

我们用流程图来描述上面示例代码,转换后的续体传递风格代码,如下:

微信图片_20211025132944.jpg

我们可以看到,整个示例代码,被分割成了5个代码块。其中case1 代码块主要负责为label17 代码块进行参数转换;case2 代码块主要负责为最外层代码块进行参数转换;所以相当于2个await挂起函数,将lambda代码块分割成了3个实际执行的代码块。

而且job1.await和job2.await会根据挂起函数的返回值进行不同处理,如果返回挂起,则进行协程挂起,当前协程退出执行;如果返回其它值,则协程继续后续代码块的执行。

编译器在编译期间,会对所有suspend修饰的函数调用处进行续体传递风格变换, Continuation可以称之为协程续体,它提供了协程恢复的基本方法:resumeWith。Continuation续体声明很简单:

public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/

public val context: CoroutineContext

/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/

public fun resumeWith(result: Result<T>)
}

其具体实现在SuspendLambda的父类BaseContinuationImpl中:

//class BaseContinuationImpl 中 fun resumeWith 内部核心代码
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> = //协程返回了结果,说明协程执行完毕
try {
val outcome = invokeSuspend(param)//执行协程的续体代码块
if (outcome === COROUTINE_SUSPENDED) return //挂起函数返回挂起标志,退出后续代码执行
Result.success(outcome) //没有返回挂起标志,将返回值outcome封装为Result返给外层outcome
} catch (exception: Throwable) {
Result.failure(exception)//将异常Result返给外层outcome
}
releaseIntercepted() // 释放当前协程的拦截器
if (completion is BaseContinuationImpl) {//如果上一层续体是一个单纯的续体,则将结果作为上一层续体的恢复参数,进行上一层续体的恢复
current = completion
param = outcome
} else {//上一层续体是一个协程,则调用协程的恢复函数,进行上一层的协程恢复
completion.resumeWith(outcome)
return
}
}
}

如果invokeSuspend函数返回中断标志时,会直接从函数中返回,等待后续继续被恢复执行。

如果invokeSuspend函数返回的是结果,且上一层续体不是单纯的续体而是协程体,它会调用参数completion的resumeWith函数,恢复上一层协程的invokeSuspend代码的执行。

协程被resumeWith恢复后,会继续调用invokeSuspend函数,根据label值执行下一个case分支代码块。按照这个恢复流程,直到所有invokeSuspend代码执行完,返回非COROUTINE_SUSPENDED的结果,协程就执行结束。

我们继续看job续体在invokeSuspend中调用到job1.await函数时,await是怎么实现返回挂起标志,和后续恢复job协程的。核心代码可以在awaitSuspend中查看:

// JobSupport.kt中 awaitSuspend方法
private suspend fun awaitSuspend(): Any? = suspendCoroutineUninterceptedOrReturn { uCont ->
val cont = AwaitContinuation(uCont.intercepted(), this)
cont.disposeOnCancellation(invokeOnCompletion(
ResumeAwaitOnCompletion(this, cont).asHandler))
cont.getResult()
}

// JobSupport.kt中 invokeOnCompletion方法
public final override fun invokeOnCompletion(...):DisposableHandle {
var nodeCache: JobNode<*>? = null
loopOnState { state ->
when (state) {
is Empty -> { // 没有completion handlers,直接创建Node放入state
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
if (_state.compareAndSet(state, node)) return nod
}
is Incomplete -> {// 有completion handlers,加入到node list列表
val list = state.list
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
if (!addLastAtomic(state, list, node)) return@loopOnState /
}
else -> { // 已经完成,不需要加入结果监听Node
if (invokeImmediately) handler.invokeIt((state as? CompletedExceptionally)?.cause) return NonDisposableHandle
}
}
}
}

// AbstractCoroutine.kt 中 resumeWith方法
// 通知state node,进行恢复
public final override fun resumeWith(result: Result<T>) {
// makeCompletingOnce 大致实现是修改协程状态,如果需要的话还会将结果返回给调用者协程,并恢复调用者协程
makeCompletingOnce(result.toState(), defaultResumeMode)
}

可以看出,job1.await()首先会通过getResult()去获取job1的结果,如果有结果则直接返回结果,否则立即返回中断标志,这样就实现了await挂起点挂起job协程了。await()挂起函数恢复job协程的流程是,将job 协程封装为 ResumeAwaitOnCompletion,并将其再次封装成handler 节点,添加job1协程的 state.list。

等job1协程完成后,会通知 handler 节点调用job协程的 resumeWith(result) 方法,从而恢复 job协程await 挂起点之后的代码块的执行。

我们再次结合示例代码, 来梳理这个挂起和恢复流程:

微信图片_20211025145009.jpg

note:绿色底色,表示在主线程执行;红色字体,表示调用挂起函数;

可以看到整个过程:

  • job协程没有阻塞调用者TestInMain,job协程会被post到主线程执行;
  • 子协程job1,job2会同时调度到不同子线程中执行,会并行执行;
  • job协程通过job1,和job2 的 await挂起函数等待异步结果。等待异步结果的时候,job协程也没有阻塞主线程。

通过续体传递风格的invokeSuspend代码,和续体之间形成的resumewith恢复链,协程得以实现挂起和恢复的核心流程。


收起阅读 »

实现一个 Coroutine 版 DialogFragment

Android 对话框有多种实现方法,目前比较推荐的是 DialogFragment,先比较与直接使用 AlertDialog,可以避免屏幕旋转等配置变化造成消失。但是其 API 建立在回调的基础上使用起来并不友好。接入 Coroutine...
继续阅读 »

Android 对话框有多种实现方法,目前比较推荐的是 DialogFragment,先比较与直接使用 AlertDialog,可以避免屏幕旋转等配置变化造成消失。但是其 API 建立在回调的基础上使用起来并不友好。接入 Coroutine 我们可以对其进行一番改造。

1. 使用 Coroutine 进行改造

自定义 AlertDialogFragment 继承自 DialogFragment 如下

class AlertDialogFragment : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
_cont.resume(which)
}
return AlertDialog.Builder(context)
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}

private lateinit var _cont : Continuation<Int>
suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
_cont = cont
}
}

实现很简单,我们是使用 suspendCoroutine 将原本基于 listener 的回调转化为挂起函数。接下来我们可以用同步的方式获取 dialog 的返回值了:

button.setOnClickListener {
GlobalScope.launch {
val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked")
}
}

2. 屏幕旋转时的崩溃

经过测试,发现上述代码存在问题。我们知道 DialogFragment 在屏幕旋转时可以保持不消失,但是此时如果点击 Dialog 的按钮,会出现崩溃:

kotlin.UninitializedPropertyAccessException: lateinit property _cont has not been initialized

如果了解 Fragment 和 Activity 销毁重建的过程就能轻松推理出发生问题的原因:

  1. 旋转屏幕时,Activity 将会重新创建。
  2. Activity 临终前会在 onSaveInstanceState() 中保存 DialogFragment 的状态 FragmentManagerState;
  3. 重建后的 Activity,在 onCreate() 中根据 savedInstanceState 所给予的 FragmentManagerState 自动重建 DialogFragment 并且 show() 出来

总结起来流程如下:

旋转屏幕 --> Activity.onSaveInstanceState() --> Activity.onCreate() --> DialogFragment.show()

重建后的 FragmentDialog 其成员变量 _cont 尚未初始化,此时对其访问自然发生 crash。

那么如果不使用 lateinit 就没问题了呢? 我们尝试引入 RxJava 对其进行改造


3. 二次改造: RxJava + Coroutine

通过 RxJava 的 Subject 避免了 lateinit 的出现,防止 crash :

//build.gradle
implementation "io.reactivex.rxjava2:rxjava:2.2.8"

新的 AlertDialogFragment 代码如下:

class AlertDialogFragment : DialogFragment() {

private val subject = SingleSubject.create<Int>()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}

return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}

suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
}

显示 dialog 时,通过订阅 SingleSubject 响应 listener 的回调。

经过修改,旋转屏幕后点击 Dialog 按钮时没有再发生 crash 的现象,但是仍然存在问题:屏幕旋转后我们无法接收到 Dialog 的返回值,即没有按预期的那样显示下面的日志

Log.d("AlertDialogFragment", "$result Clicked")

当 DialogFragment 重建后, Subject 也跟随重建,但是丢失了之前的 Subscriber ,所以点击按钮后,Rx 的下游无法响应。

有没有办法让 Subject 重建时能够恢复之前的 Subscriber 呢? 此时想到了借助 onSaveInstanceState 。

想要 subject 作为 Fragment 的 arguments 保存到 savedInstanceState,必须是一个 Serializable 或者 Parcelable


4. 三次改造: SerializableSingleSubject

令人高兴的是,查阅 SingleSubject 源码后发现其成员变量全是 Serializable 的子类,也就是只要 SingleSubject 实现 Serializable 接口就可以存入 savedInstanceState 了, 但可惜它不是,而且它是一个 final 类,只好拷贝源码出来,自己实现一个 SerializableSingleSubject :

/**
* 实现 Serializable 接口并增加 serialVersionUID
*/

public final class SerializableSingleSubject<T> extends Single<T> implements SingleObserver<T>, Serializable {
private static final long serialVersionUID = 1L;

final AtomicReference<SerializableSingleSubject.SingleDisposable<T>[]> observers;

@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] EMPTY = new SerializableSingleSubject.SingleDisposable[0];

@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] TERMINATED = new SerializableSingleSubject.SingleDisposable[0];

final AtomicBoolean once;
T value;
Throwable error;

// 以下代码同 SingleSubject,省略

基于 SerializableSingleSubject 重写 AlertDialogFragment 如下:

class AlertDialogFragment : DialogFragment() {

private var subject = SerializableSingleSubject.create<Int>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let {
subject = it["subject"] as SerializableSingleSubject<Int>
}

}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}

return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}


override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable("subject", subject);

}

suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
}

重建后通过 savedInstanceState 恢复之前的 Subscriber ,下游顺利收到消息,日志正常输出。

需要注意的是,此时仍然存在隐患:屏幕旋转后,点击 dialog 虽然可以正常获得返回值,但是此时协程恢复的上下文是前一次 launch { ... } 的闭包

    GlobalScope.launch {
val frag = AlertDialogFragment()
val result = frag.showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked on $frag")
}

如上,此时打印的 frag 是重建之前的 DialogFragment,如果 launch{...} 里引用了外部 Activity(获取成员) ,那也是旧的 Activity,此处需要特别注意避免类似操作。


5. 纯 RxJava 方式

既然引入了 RxJava,最后捎带介绍一下不使用 Coroutine 只依靠 RxJava 的版本:

fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> {
show(fm, tag)
return subject.hide()
}

使用时,由 subscribe() 替代挂起函数的使用。

button.setOnClickListener {
AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe { result ->
Log.d("AlertDialogFragment", "$result Clicked")
}
}


收起阅读 »

LeetCode刷题-合并区间

一、题目描述 难度:中等~ 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。 示例1: 输入:...
继续阅读 »

一、题目描述


难度:中等~

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。


示例1:


输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例2:


输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:
  1 <= intervals.length <= 10^4
  intervals[i].length == 2
  0 <= starti <= endi <= 10^4


作者:力扣 (LeetCode)
链接:leetcode-cn.com/leetbook/re…
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


二、题目解析


思路:
直接代码里注释!


三、代码


1.Python实现



初见的第一思路:
1.按左端点从小到大排序



2.有交集,更新右端点;无交集,则保存当前区间


class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
//将二维数组intervals按照其内每个子数组第一个元素从小到大排序
intervals.sort()
result = list()
for i in intervals:
//如果result中没有子数组或者当前两个数组无交集
//直接保存当前区间
if not result or result[-1][1] < i[0]:
result.append(i)
//否则有交集,取两个数组中第一个元素最大的值作为当前数组的第一个元素(即合并操作)
else:
result[-1][1] = max(result[-1][1], i[1])
return result

复杂度分析




  • 时间复杂度:O(n log n),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(n log n)。




  • 空间复杂度:O(log n),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(log n) 即为排序所需要的空间复杂度。




2.C实现


留空,等变再牛B点再来手写快排加合并!


3.C++实现


class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return {};
}
sort(intervals.begin(), intervals.end());
vector<vector<int>> merge;
for (int i = 0; i < intervals.size(); ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (!merge.size() || merge.back()[1] < L) {
merge.push_back({L, R});
}
else {
merge.back()[1] = max(merge.back()[1], R);
}
}
return merge;
}
};

🔆In The End!


请添加图片描述








从现在做起,坚持下去,一天进步一小点,不久的将来,你会感谢曾经努力的你!

作者:孤寒者
链接:https://juejin.cn/post/7023000958921605127
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Swift 枚举

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:它声明在类中,可以通过实例化类来访问它的值。枚举也可以定义构造函数(ini...
继续阅读 »

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。

Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:

  • 它声明在类中,可以通过实例化类来访问它的值。

  • 枚举也可以定义构造函数(initializers)来提供一个初始成员值;可以在原始的实现基础上扩展它们的功能。

  • 可以遵守协议(protocols)来提供标准的功能。

语法

Swift 中使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

enum enumname {
// 枚举定义放在这里
}

例如我们定义以下表示星期的枚举:

import Cocoa

// 定义枚举
enum DaysofaWeek {
case Sunday
case Monday
case TUESDAY
case WEDNESDAY
case THURSDAY
case FRIDAY
case Saturday
}

var weekDay = DaysofaWeek.THURSDAY
weekDay = .THURSDAY
switch weekDay
{
case .Sunday:
print("星期天")
case .Monday:
print("星期一")
case .TUESDAY:
print("星期二")
case .WEDNESDAY:
print("星期三")
case .THURSDAY:
print("星期四")
case .FRIDAY:
print("星期五")
case .Saturday:
print("星期六")
}

以上程序执行输出结果为:

星期四

枚举中定义的值(如 SundayMonday……Saturday)是这个枚举的成员值(或成员)。case关键词表示一行新的成员值将被定义。

注意: 和 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的DaysofaWeek例子中,SundayMonday……Saturday不会隐式地赋值为01……6。相反,这些枚举成员本身就有完备的值,这些值是已经明确定义好的DaysofaWeek类型。

var weekDay = DaysofaWeek.THURSDAY 

weekDay的类型可以在它被DaysofaWeek的一个可能值初始化时推断出来。一旦weekDay被声明为一个DaysofaWeek,你可以使用一个缩写语法(.)将其设置为另一个DaysofaWeek的值:

var weekDay = .THURSDAY 

weekDay的类型已知时,再次为其赋值可以省略枚举名。使用显式类型的枚举值可以让代码具有更好的可读性。

枚举可分为相关值与原始值。

相关值与原始值的区别

相关值原始值
不同数据类型相同数据类型
实例: enum {10,0.8,"Hello"}实例: enum {10,35,50}
值的创建基于常量或变量预先填充的值
相关值是当你在创建一个基于枚举成员的新常量或变量时才会被设置,并且每次当你这么做得时候,它的值可以是不同的。原始值始终是相同的

相关值

以下实例中我们定义一个名为 Student 的枚举类型,它可以是 Name 的一个字符串(String),或者是 Mark 的一个相关值(Int,Int,Int)。

import Cocoa

enum Student{
case Name(String)
case Mark(Int,Int,Int)
}
var studDetails = Student.Name("Runoob")
var studMarks = Student.Mark(98,97,95)
switch studMarks {
case .Name(let studName):
print("学生的名字是: \(studName)。")
case .Mark(let Mark1, let Mark2, let Mark3):
print("学生的成绩是: \(Mark1),\(Mark2),\(Mark3)。")
}

以上程序执行输出结果为:

学生的成绩是: 98,97,95。

原始值

原始值可以是字符串,字符,或者任何整型值或浮点型值。每个原始值在它的枚举声明中必须是唯一的。

在原始值为整数的枚举时,不需要显式的为每一个成员赋值,Swift会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。

import Cocoa

enum Month: Int {
case January = 1, February, March, April, May, June, July, August, September, October, November, December
}

let yearMonth = Month.May.rawValue
print("数字月份为: \(yearMonth)。")

以上程序执行输出结果为:

数字月份为: 5。
收起阅读 »

Swift 闭包

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。全局函数和嵌套函数其实就是特殊的闭包。闭包的形式有...
继续阅读 »

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。

Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。

全局函数和嵌套函数其实就是特殊的闭包。

闭包的形式有:

全局函数嵌套函数闭包表达式
有名字但不能捕获任何值。有名字,也能捕获封闭函数内的值。无名闭包,使用轻量级语法,可以根据上下文环境捕获值。

Swift中的闭包有很多优化的地方:

  1. 根据上下文推断参数和返回值类型
  2. 从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略return)
  3. 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
  4. 提供了尾随闭包语法(Trailing closure syntax)
  5. 语法

    以下定义了一个接收参数并返回指定类型的闭包语法:

    {(parameters) -> return type in
    statements
    }

    实例

    import Cocoa

    let studname = { print("Swift 闭包实例。") }
    studname
    ()

    以上程序执行输出结果为:

    Swift 闭包实例。

    以下闭包形式接收两个参数并返回布尔值:

    {(Int, Int) -> Bool in
    Statement1
    Statement 2
    ---
    Statement n
    }

    实例

    import Cocoa

    let divide = {(val1: Int, val2: Int) -> Int in
    return val1 / val2
    }
    let result = divide(200, 20)
    print (result)

    以上程序执行输出结果为:

    10

    闭包表达式

    闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。


    sorted 方法

    Swift 标准库提供了名为 sorted(by:) 的方法,会根据您提供的用于排序的闭包函数将已知类型数组中的值进行排序。

    排序完成后,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

    sorted(by:)方法需要传入两个参数:

    • 已知类型的数组
    • 闭包函数,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    // 使用普通函数(或内嵌函数)提供排序功能,闭包函数类型需为(String, String) -> Bool。
    func backwards
    (s1: String, s2: String) -> Bool {
    return s1 > s2
    }
    var reversed = names.sorted(by: backwards)

    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果第一个字符串 (s1) 大于第二个字符串 (s2),backwards函数返回true,表示在新的数组中s1应该出现在s2前。 对于字符串中的字符来说,"大于" 表示 "按照字母顺序较晚出现"。 这意味着字母"B"大于字母"A",字符串"S"大于字符串"D"。 其将进行字母逆序排序,"AT"将会排在"AE"之前。


    参数名称缩写

    Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数。

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted( by: { $0 > $1 } )
    print(reversed)

    $0和$1表示闭包中第一个和第二个String类型的参数。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果你在闭包表达式中使用参数名称缩写, 您可以在闭包参数列表中省略对其定义, 并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字同样也可以被省略.


    运算符函数

    实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。

    Swift 的String类型定义了关于大于号 (>) 的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。 而这正好与sort(_:)方法的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted(by: >)
    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    尾随闭包

    尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。

    func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
    }

    // 以下是不使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    ({
    // 闭包主体部分
    })

    // 以下是使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    () {
    // 闭包主体部分
    }

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    //尾随闭包
    var reversed = names.sorted() { $0 > $1 }
    print(reversed)

    sort() 后的 { $0 > $1} 为尾随闭包。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。

    reversed = names.sorted { $0 > $1 }

    捕获值

    闭包可以在其定义的上下文中捕获常量或变量。

    即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

    Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。

    嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

    看这个例子:

    func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    一个函数makeIncrementor ,它有一个Int型的参数amout, 并且它有一个外部参数名字forIncremet,意味着你调用的时候,必须使用这个外部名字。返回值是一个()-> Int的函数。

    函数体内,声明了变量 runningTotal 和一个函数 incrementor。

    incrementor函数并没有获取任何参数,但是在函数体内访问了runningTotal和amount变量。这是因为其通过捕获在包含它的函数体内已经存在的runningTotal和amount变量而实现。

    由于没有修改amount变量,incrementor实际上捕获并存储了该变量的一个副本,而该副本随着incrementor一同被存储。

    所以我们调用这个函数时会累加:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    print(incrementByTen())

    // 返回的值为20
    print(incrementByTen())

    // 返回的值为30
    print(incrementByTen())

    以上程序执行输出结果为:

    10
    20
    30

    闭包是引用类型

    上面的例子中,incrementByTen是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。

    这是因为函数和闭包都是引用类型。

    无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen指向闭包的引用是一个常量,而并非闭包内容本身。

    这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    incrementByTen
    ()

    // 返回的值为20
    incrementByTen
    ()

    // 返回的值为30
    incrementByTen
    ()

    // 返回的值为40
    incrementByTen
    ()

    let alsoIncrementByTen = incrementByTen

    // 返回的值也为50
    print(alsoIncrementByTen())
收起阅读 »

Swift 函数

Swift 函数用来完成特定任务的独立的代码块。Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。函数声明: 告诉编译器函数的名字,返回类型及参数。函数定义: 提供了函数的实体。Swift 函数包含了参数类型...
继续阅读 »

Swift 函数用来完成特定任务的独立的代码块。

Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。

  • 函数声明: 告诉编译器函数的名字,返回类型及参数。

  • 函数定义: 提供了函数的实体。

Swift 函数包含了参数类型及返回值类型:


函数定义

Swift 定义函数使用关键字 func

定义函数的时候,可以指定一个或多个输入参数和一个返回值类型。

每个函数都有一个函数名来描述它的功能。通过函数名以及对应类型的参数值来调用这个函数。函数的参数传递的顺序必须与参数列表相同。

函数的实参传递的顺序必须与形参列表相同,-> 后定义函数的返回值类型。

语法

func funcname(形参) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

以下我们定义了一个函数名为 runoob 的函数,形参的数据类型为 String,返回值也为 String:

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数调用

我们可以通过函数名以及对应类型的参数值来调用函数,函数的参数传递的顺序必须与参数列表相同。

以下我们定义了一个函数名为 runoob 的函数,形参 site 的数据类型为 String,之后我们调用函数传递的实参也必须 String 类型,实参传入函数体后,将直接返回,返回的数据类型为 String。

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数参数

函数可以接受一个或者多个参数,这些参数被包含在函数的括号之中,以逗号分隔。

以下实例向函数 runoob 传递站点名 name 和站点地址 site:

import Cocoa

func runoob
(name: String, site: String) -> String {
return name + site
}
print(runoob(name: "菜鸟教程:", site: "www.runoob.com"))
print(runoob(name: "Google:", site: "www.google.com"))

以上程序执行输出结果为:

菜鸟教程:www.runoob.com
Googlewww.google.com

不带参数函数

我们可以创建不带参数的函数。

语法:

func funcname() -> datatype {
return datatype
}

实例

import Cocoa

func sitename
() -> String {
return "菜鸟教程"
}
print(sitename())

以上程序执行输出结果为:

菜鸟教程

元组作为函数返回值

函数返回值类型可以是字符串,整型,浮点型等。

元组与数组类似,不同的是,元组中的元素可以是任意类型,使用的是圆括号。

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下面的这个例子中,定义了一个名为minMax(_:)的函数,作用是在一个Int数组中找出最小值与最大值。

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("最小值为 \(bounds.min) ,最大值为 \(bounds.max)")

minMax(_:)函数返回一个包含两个Int值的元组,这些值被标记为min和max,以便查询函数的返回值时可以通过名字访问它们。

以上程序执行输出结果为:

最小值为 -6 ,最大值为 109

如果你不确定返回的元组一定不为nil,那么你可以返回一个可选的元组类型。

你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如(Int, Int)?或(String, Int, Bool)?

注意
可选元组类型如(Int, Int)?与元组包含可选类型如(Int?, Int?)是不同的.可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。

前面的minMax(_:)函数返回了一个包含两个Int值的元组。但是函数不会对传入的数组执行任何安全检查,如果array参数是一个空数组,如上定义的minMax(_:)在试图访问array[0]时会触发一个运行时错误。

为了安全地处理这个"空数组"问题,将minMax(_:)函数改写为使用可选元组返回类型,并且当数组为空时返回nil

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("最小值为 \(bounds.min),最大值为 \(bounds.max)")
}

以上程序执行输出结果为:

最小值为 -6,最大值为 109

没有返回值函数

下面是 runoob(_:) 函数的另一个版本,这个函数接收菜鸟教程官网网址参数,没有指定返回值类型,并直接输出 String 值,而不是返回它:

import Cocoa

func runoob
(site: String) {
print("菜鸟教程官网:\(site)")
}
runoob
(site: "http://www.runoob.com")

以上程序执行输出结果为:

菜鸟教程官网:http://www.runoob.com

函数参数名称

函数参数都有一个外部参数名和一个局部参数名。

局部参数名

局部参数名在函数的实现内部使用。

func sample(number: Int) {
println
(number)
}

以上实例中 number 为局部参数名,只能在函数体内使用。

import Cocoa

func sample
(number: Int) {
print(number)
}
sample
(number: 1)
sample
(number: 2)
sample
(number: 3)

以上程序执行输出结果为:

1
2
3

外部参数名

你可以在局部参数名前指定外部参数名,中间以空格分隔,外部参数名用于在函数调用时传递给函数的参数。

如下你可以定义以下两个函数参数名并调用它:

import Cocoa

func pow
(firstArg a: Int, secondArg b: Int) -> Int {
var res = a
for _ in 1..<b {
res
= res * a
}
print(res)
return res
}
pow
(firstArg:5, secondArg:3)

以上程序执行输出结果为:

125

注意
如果你提供了外部参数名,那么函数在被调用时,必须使用外部参数名。


可变参数

可变参数可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数,其数量是不确定的。

可变参数通过在变量类型名后面加入(...)的方式来定义。

import Cocoa

func vari
<N>(members: N...){
for i in members {
print(i)
}
}
vari
(members: 4,3,5)
vari
(members: 4.5, 3.1, 5.6)
vari
(members: "Google", "Baidu", "Runoob")

以上程序执行输出结果为:

4
3
5
4.5
3.1
5.6
Google
Baidu
Runoob

常量,变量及 I/O 参数

一般默认在函数中定义的参数都是常量参数,也就是这个参数你只可以查询使用,不能改变它的值。

如果想要声明一个变量参数,可以在参数定义前加 inout 关键字,这样就可以改变这个参数的值了。

例如:

func  getName(_ name: inout String).........

此时这个 name 值可以在函数中改变。

一般默认的参数传递都是传值调用的,而不是传引用。所以传入的参数在函数内改变,并不影响原来的那个参数。传入的只是这个参数的副本。

当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。

实例

import Cocoa

func swapTwoInts
(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a
= b
b
= temporaryA
}


var x = 1
var y = 5
swapTwoInts
(&x, &y)
print("x 现在的值 \(x), y 现在的值 \(y)")

swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。

需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀。

以上程序执行输出结果为:

x 现在的值 5, y 现在的值 1

函数类型及使用

每个函数都有种特定的函数类型,由函数的参数类型和返回类型组成。

func inputs(no1: Int, no2: Int) -> Int {
return no1/no2
}

inputs 函数类型有两个 Int 型的参数(no1、no2)并返回一个 Int 型的值。

实例如下:

import Cocoa

func inputs
(no1: Int, no2: Int) -> Int {
return no1/no2
}
print(inputs(no1: 20, no2: 10))
print(inputs(no1: 36, no2: 6))

以上程序执行输出结果为:

2
6

以上函数定义了两个 Int 参数类型,返回值也为 Int 类型。

接下来我们看下如下函数,函数定义了参数为 String 类型,返回值为 String 类型。

func inputstr(name: String) -> String {
return name
}

函数也可以定义一个没有参数,也没有返回值的函数,如下所示:

import Cocoa

func inputstr
() {
print("菜鸟教程")
print("www.runoob.com")
}
inputstr
()

以上程序执行输出结果为:

菜鸟教程
www
.runoob.com

使用函数类型

在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:

var addition: (Int, Int) -> Int = sum

解析:

"定义一个叫做 addition 的变量,参数与返回值类型均是 Int ,并让这个新变量指向 sum 函数"。

sum 和 addition 有同样的类型,所以以上操作是合法的。

现在,你可以用 addition 来调用被赋值的函数了:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

以上程序执行输出结果为:

输出结果: 129

函数类型作为参数类型、函数类型作为返回类型

我们可以将函数作为参数传递给另外一个参数:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

func another
(addition: (Int, Int) -> Int, a: Int, b: Int) {
print("输出结果: \(addition(a, b))")
}
another
(addition: sum, a: 10, b: 20)

以上程序执行输出结果为:

输出结果: 129
输出结果: 30

函数嵌套

函数嵌套指的是函数内定义一个新的函数,外部的函数可以调用函数内定义的函数。

实例如下:

import Cocoa

func calcDecrement
(forDecrement total: Int) -> () -> Int {
var overallDecrement = 0
func decrementer
() -> Int {
overallDecrement
-= total
return overallDecrement
}
return decrementer
}
let decrem = calcDecrement(forDecrement: 30)
print(decrem())

以上程序执行输出结果为:

-30
收起阅读 »

Swift 字典

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我...
继续阅读 »

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。

Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。

和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。

Swift 字典的key没有类型限制可以是整型或字符串,但必须是唯一的。

如果创建一个字典,并赋值给一个变量,则创建的字典就是可以修改的。这意味着在创建字典后,可以通过添加、删除、修改的方式改变字典里的项目。如果将一个字典赋值给常量,字典就不可修改,并且字典的大小和内容都不可以修改。


创建字典

我们可以使用以下语法来创建一个特定类型的空字典:

var someDict =  [KeyType: ValueType]()

以下是创建一个空字典,键的类型为 Int,值的类型为 String 的简单语法:

var someDict = [Int: String]()

以下为创建一个字典的实例:

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

访问字典

我们可以根据字典的索引来访问数组的元素,语法如下:

var someVar = someDict[key]

我们可以通过以下实例来学习如何创建,初始化,访问字典:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var someVar = someDict[1]

print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

修改字典

我们可以使用 updateValue(forKey:) 增加或更新字典的内容。如果 key 不存在,则添加值,如果存在则修改 key 对应的值。updateValue(_:forKey:)方法返回Optional值。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict.updateValue("One 新的值", forKey: 1)

var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

你也可以通过指定的 key 来修改字典的值,如下所示:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict[1]
someDict
[1] = "One 新的值"
var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

移除 Key-Value 对

我们可以使用 removeValueForKey() 方法来移除字典 key-value 对。如果 key 存在该方法返回移除的值,如果不存在返回 nil 。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var removedValue = someDict.removeValue(forKey: 2)

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

你也可以通过指定键的值为 nil 来移除 key-value(键-值)对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

someDict
[2] = nil

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

遍历字典

我们可以使用 for-in 循环来遍历某个字典中的键值对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict {
print("字典 key \(key) - 字典 value \(value)")
}

以上程序执行输出结果为:

字典 key 2 -  字典 value Two
字典 key 3 - 字典 value Three
字典 key 1 - 字典 value One

我们也可以使用enumerate()方法来进行字典遍历,返回的是字典的索引及 (key, value) 对,实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict.enumerated() {
print("字典 key \(key) - 字典 (key, value) 对 \(value)")
}

以上程序执行输出结果为:

字典 key 0 -  字典 (key, value)  (2, "Two")
字典 key 1 - 字典 (key, value) (3, "Three")
字典 key 2 - 字典 (key, value) (1, "One")

字典转换为数组

你可以提取字典的键值(key-value)对,并转换为独立的数组。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

let dictKeys = [Int](someDict.keys)
let dictValues = [String](someDict.values)

print("输出字典的键(key)")

for (key) in dictKeys {
print("\(key)")
}

print("输出字典的值(value)")

for (value) in dictValues {
print("\(value)")
}

以上程序执行输出结果为:

输出字典的键(key)
2
3
1
输出字典的值(value)
Two
Three
One

count 属性

我们可以使用只读的 count 属性来计算字典有多少个键值对:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]

print("someDict1 含有 \(someDict1.count) 个键值对")
print("someDict2 含有 \(someDict2.count) 个键值对")

以上程序执行输出结果为:

someDict1 含有 3 个键值对
someDict2
含有 2 个键值对

isEmpty 属性

Y我们可以通过只读属性 isEmpty 来判断字典是否为空,返回布尔值:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]
var someDict3:[Int:String] = [Int:String]()

print("someDict1 = \(someDict1.isEmpty)")
print("someDict2 = \(someDict2.isEmpty)")
print("someDict3 = \(someDict3.isEmpty)")

以上程序执行输出结果为:

someDict1 = false
someDict2
= false
someDict3
= true
收起阅读 »

使用 Kotlin Flow 优化你的网络请求框架,减少模板代码

一、以前封装的遗憾点 主要集中在如下2点上: Loading的处理 多余的LiveData 总而言之,就是需要写很多模板代码。 不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小. 1.1 Loading的处理 对于封装二,虽然...
继续阅读 »

一、以前封装的遗憾点


主要集中在如下2点上:




  • Loading的处理




  • 多余的LiveData




总而言之,就是需要写很多模板代码。



不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小.



1.1 Loading的处理


对于封装二,虽然解耦比封装一更彻底,但是关于Loading这里我觉得还是有遗憾。


试想一下:如果Activity中业务很多、逻辑复杂,存在很多个网络请求,在需要网络请求的地方都要手动去showLoading() ,然后在 observer() 中手动调用 stopLoading()


假如Activity中代码业务复杂,存在多个api接口,这样Activity中就存在很多个与loading有关的方法。


此外,如果一个网络请求的showLoading()方法和dismissLoading()方法相隔很远。会导致一个顺序流程的割裂。


请求开始前showLoading() ---> 请求网络 ---> 结束后stopLoading(),这是一个完整的流程,代码也应该尽量在一起,一目了然,不应该割裂存在。


如果代码量一多,以后维护起来,万一不小心删除了某个showLoading()或者stopLoading(),也容易导致问题。


还有就是每次都要手动调用这两个方法,麻烦。


1.2 重复的LiveData声明


个人认为常用的网络请求分为两大类:




  • 用完即丢




  • 需要监听数据变化




举个常见的例子,看下面这个页面:


image.png


用户一进入这个页面,绿色框里面内容基本不会变化,(不去纠结微信这个页面是不是webview之类的),这种ui其实是不需要设置一个LiveData去监听的,因为它几乎不会再更新了。


典型的还有:点击登录按钮,成功后就进去了下一个页面。


但是红色的框里面的ui不一样,需要实时刷新数据,也就用到LiveData监听,这种情况下观察者订阅者模式的好处才真正展示出来。并且从其他页面过来,LiveData也会把最新的数据自动更新。


对于用完即丢的网络请求,LoginViewModel会存在这种代码:


// LoginViewModel.kt
val loginLiveData = MutableLiveData<User?>()
val logoutLiveData = MutableLiveData<Any?>()
val forgetPasswordLiveData = MutableLiveData<User?>(

并且对应的Activity中也需要监听这3个LiveData。


这种模板代码让我写的很烦。


用了Flow优化后,完美的解决这2个痛点。



“Talk is cheap. Show me the code.”



二、集成Flow之后的用法


2.1 请求自带Loading&&不需要监听数据变化


需求:




  • 不需要监听数据变化,对应上面的用完即丢




  • 不需要在ViewModel中声明LiveData成员对象




  • 发起请求之前自动showLoading(),请求结束后自动stopLoading()




  • 类似于点击登录按钮,finish 当前页面,跳转到下一个页面




TestActivity 中示例代码:


// TestActivity.kt
private fun login() {
launchWithLoadingAndCollect({mViewModel.login("username", "password")}) {
onSuccess = { data->
showSuccessView(data)
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onError = {e ->
e.printStackTrace()
}
}
}

TestViewModel 中代码:


// TestViewModel中代码
suspend fun login(username: String, password: String): ApiResponse<User?> {
return repository.login(username, password)
}

2.2 请求不带Loading&&不需要声明LiveData


需求:




  • 不需要监听数据变化




  • 不需要在ViewModel中声明LiveData成员对象




  • 不需要Loading的展示




// TestActivity.kt
private fun getArticleDetail() {
launchAndCollect({ mViewModel.getArticleDetail() }) {
onSuccess = {
showSuccessView()
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onDataEmpty = {
showEmptyView()
}
}
}

TestViewModel 中代码和上面一样,这里就不写了。


是不是非常简单,一个方法搞定,将Loading的逻辑都隐藏了,再也不需要手动写 showLoading()stopLoading()


并且请求的结果直接在回调里面接收,直接处理,这样请求网络和结果的处理都在一起,看起来一目了然,再也不需要在 Activity 中到处找在哪监听的 LiveData


同样,它跟 LiveData 一样,也会监听 Activity 的生命周期,不会造成内存泄露。因为它是运行在ActivitylifecycleScope 协程作用域中的。


2.3 需要监听数据变化


需求:




  • 需要监听数据变化,要实时更新数据




  • 需要在 ViewModel 中声明 LiveData 成员对象




  • 例如实时获取最新的配置、最新的用户信息等




TestActivity 中示例代码:


// TestActivity.kt
class TestActivity : AppCompatActivity(R.layout.activity_api) {

private fun initObserver() {
mViewModel.wxArticleLiveData.observeState(this) {

onSuccess = { data: List<WxArticleBean>? ->
showSuccessView(data)
}

onDataEmpty = { showEmptyView() }

onFailed = { code, msg -> showFailedView(code, msg) }

onError = { showErrorView() }
}
}

private fun requestNet() {
// 需要Loading
launchWithLoading {
mViewModel.requestNet()
}
}
}

ViewModel 中示例代码:


class ApiViewModel : ViewModel() {

private val repository by lazy { WxArticleRepository() }

val wxArticleLiveData = StateMutableLiveData<List<WxArticleBean>>()

suspend fun requestNet() {
wxArticleLiveData.value = repository.fetchWxArticleFromNet()
}
}

本质上是通过FLow来调用LiveDatasetValue()方法,还是LiveData的使用。虽然可以完全用 Flow 来实现,但是我觉得这里用 Flow 的方式麻烦,不容易懂,还是怎么简单怎么来。


这种方式其实跟上篇文章中的封装二差不多,区别就是不需要手动调用Loading有关的方法。


三、拆封装


如果不抽取通用方法是这样写的:


// TestActivity.kt
private fun login() {
lifecycleScope.launch {
flow {
emit(mViewModel.login("username", "password"))
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}.collect { response ->
when (response) {
is ApiSuccessResponse -> showSuccessView(response.data)
is ApiEmptyResponse -> showEmptyView()
is ApiFailedResponse -> showFailedView(response.errorCode, response.errorMsg)
is ApiErrorResponse -> showErrorView(response.error)
}
}
}
}

简单介绍下Flow


Flow类似于RxJava,操作符都跟Rxjava差不多,但是比Rxjava简单很多,kotlin通过flow来实现顺序流和链式编程。


flow关键字大括号里面的是方法的执行,结果通过emit发送给下游。


onStart表示最开始调用方法之前执行的操作,这里是展示一个 loading ui


onCompletion表示所有执行完成,不管有没有异常都会执行这个回调。


collect表示执行成功的结果回调,就是emit()方法发送的内容,flow必须执行collect才能有结果。因为是冷流,对应的还有热流。


更多的Flow知识点可以参考其他博客和官方文档。


这里可以看出,通过Flow完美的解决了loading的显示与隐藏。


我这里是在Activity中都调用flow的流程,这样我们扩展BaseActivity即可。


为什么扩展的是BaseActivity?


因为startLoading()stopLoading()BaseActivity中。😂


3.1 解决 flow 的 Loading 模板代码


fun <T> BaseActivity.launchWithLoadingGetFlow(block: suspend () -> ApiResponse<T>): Flow<ApiResponse<T>> {
return flow {
emit(block())
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}
}

这样每次调用launchWithLoadingGetFlow方法,里面就实现了 Loading 的展示与隐藏,并且会返回一个 FLow 对象。


下一步就是处理 flow 结果collect里面的模板代码。


3.2 声明结果回调类


class ResultBuilder<T> {
var onSuccess: (data: T?) -> Unit = {}
var onDataEmpty: () -> Unit = {}
var onFailed: (errorCode: Int?, errorMsg: String?) -> Unit = { _, _ -> }
var onError: (e: Throwable) -> Unit = { e -> }
var onComplete: () -> Unit = {}
}

各种回调按照项目特性删减即可。


3.3 对ApiResponse对象进行解析


private fun <T> parseResultAndCallback(response: ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
val listener = ResultBuilder<T>().also(listenerBuilder)
when (response) {
is ApiSuccessResponse -> listener.onSuccess(response.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(response.errorCode, response.errorMsg)
is ApiErrorResponse -> listener.onError(response.throwable)
}
listener.onComplete()
}

上篇文章这里的处理用的是继承LiveDataObserver,这里就不需要了,毕竟继承能少用就少用。


3.4 最终抽取方法


将上面的步骤连起来如下:


fun <T> BaseActivity.launchWithLoadingAndCollect(block: suspend () -> ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
lifecycleScope.launch {
launchWithLoadingGetFlow(block).collect { response ->
parseResultAndCallback(response, listenerBuilder)
}
}
}

3.5 将Flow转换成LiveData对象


获取到的是Flow对象,如果想要变成LiveDataFlow原生就支持将Flow对象转换成不可变的LiveData对象。


val loginFlow: Flow<ApiResponse<User?>> =
launchAndGetFlow(requestBlock = { mViewModel.login("UserName", "Password") })
val loginLiveData: LiveData<ApiResponse<User?>> = loginFlow.asLiveData()

调用的是 Flow 的asLiveData()方法,原理也很简单,就是用了livedata的扩展函数:


@JvmOverloads
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}

这里返回的是LiveData<ApiResponse<User?>>对象,如果想要跟上篇文章一样用StateLiveData,在observe的回调里面监听不同状态的callback


以前的方式是继承,有如下缺点:



  • 必须要用StateLiveData,不能用原生的LiveData,侵入性很强

  • 不只是继承LiveData,还要继承Observer,麻烦

  • 为了实现这个,写了一堆的代码


这里用 Kotlin 扩展实现,直接扩展 LiveData


@MainThread
inline fun <T> LiveData<ApiResponse<T>>.observeState(
owner: LifecycleOwner,
listenerBuilder: ResultBuilder<T>.() -> Unit
) {
val listener = ResultBuilder<T>().also(listenerBuilder)
observe(owner) { apiResponse ->
when (apiResponse) {
is ApiSuccessResponse -> listener.onSuccess(apiResponse.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(apiResponse.errorCode, apiResponse.errorMsg)
is ApiErrorResponse -> listener.onError(apiResponse.throwable)
}
listener.onComplete()
}
}

感谢Flywith24开源库提供的思路,感觉自己有时候还是在用Java的思路在写Kotlin。


3.6 进一步完善


很多网络请求的相关并不是只有 loading 状态,还需要在请求前和结束后处理一些特定的逻辑。


这里的方式是:直接在封装方法的参数加 callback,默认用是 loading 的实现。


fun <T> BaseActivity.launchAndCollect(
requestBlock: suspend () -> ApiResponse<T>,
startCallback: () -> Unit = { showLoading() },
completeCallback: () -> Unit = { dismissLoading() },
listenerBuilder: ResultBuilder<T>.() -> Unit
)

四、针对多数据来源


虽然项目中大部分都是单一数据来源,但是也偶尔会出现多数据来源,多数据源结合Flow的操作符,也非常的方便。


示例


假如同一份数据可以从数据库获取,可以从网络请求获取,TestRepository的代码如下:


// TestRepository.kt
suspend fun fetchDataFromNet(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = executeHttp { mService.getWxArticle() }
return flow { emit(response) }.flowOn(Dispatchers.IO)
}

suspend fun fetchDataFromDb(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = getDataFromRoom()
return flow { emit(response) }.flowOn(Dispatchers.IO)


Repository中的返回不再直接返回实体类,而是返回flow包裹的实体类对象。


为什么要这么做?


为了用神奇的flow操作符来处理。


flow组合操作符



  • combine、combineTransform


combine操作符可以连接两个不同的Flow。



  • merge


merge操作符用于将多个流合并。



  • zip


zip操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。


关于 Flow 的基础操作符,徐医生大神的这篇文章已经写的很棒了,这里就不多余的写了。


根据操作符的示例可以看出,就算返回的不是同一个对象,也可以用操作符进行处理。


几年前刚开始学RxJava时,好几次都是入门到放弃,操作符太多了,搞的也很懵逼,Flow 真的比它简单太多了。


五、flow的奇淫技巧


flowWithLifecycle


需求:
Activity 的 onSume() 方法中请求最新的地理位置信息。


以前的写法:


// TestActivity.kt
override fun onResume() {
super.onResume()
getLastLocation()
}

override fun onDestory() {
super.onDestory()
// 释放获取定位的代码,防止内存泄露
}

这种写法没问题,也很正常,但是用了 Flow 之后,有一种新的写法。


用了 flow 的写法:


// TestActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getLastLocation()
}

@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private fun getLastLocation() {
if (LocationPermissionUtils.isLocationProviderEnabled() && LocationPermissionUtils.isLocationPermissionGranted()) {
lifecycleScope.launch {
SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}
}
}
}

onCreate中书写该函数,然后 flow 的链式调用中加入:


.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)


flowWithLifecycle能监听 Activity 的生命周期,在 Activity 的onResume开始请求位置信息,onStop 时自动停止,不会导致内存泄露。



flowWithLifecycle 会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。



这个api需要引入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01依赖库。


callbackFlow


有没有发现5.1中调用获取位置信息的代码很简单?


SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}

几行代码解决获取位置信息,并且任何地方都直接调用,不要写一堆代码。


这里就是用到callbackFlow,简而言之,callbackFlow就是将callback回调代码变成同步的方式来写。


这里直接上SharedLocationManager的代码,具体细节自行 Google,因为这就不是网络框架的内容。


这里附上主要的代码:


@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private val _locationUpdates: SharedFlow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
Log.d(TAG, "New location: ${result.lastLocation}")
trySend(result.lastLocation)
}

}
Log.d(TAG, "Starting location updates")

fusedLocationClient.requestLocationUpdates(
locationRequest,callback,Looper.getMainLooper())
.addOnFailureListener { e ->close(e)}

awaitClose {
Log.d(TAG, "Stopping location updates")
fusedLocationClient.removeLocationUpdates(callback)
}
}.shareIn(
externalScope,
replay = 0,
started = SharingStarted.WhileSubscribed()
)

完整代码见:GitHub


总结


上一篇文章# 两种方式封装Retrofit+协程,实现优雅快速的网络请求


加上这篇的 flow 网络请求封装,一共是三种对Retrofit+协程的网络封装方式。


对比下三种封装方式:




  • 封装一 (对应分支oneWay) 传递ui引用,可按照项目进行深度ui定制,方便快速,但是耦合高




  • 封装二 (对应分支master) 耦合低,依赖的东西很少,但是写起来模板代码偏多




  • 封装三 (对应分支dev) 引入了新的flow流式编程(虽然出来很久,但是大部分人应该还没用到),链式调用,loading 和网络请求以及结果处理都在一起,很多时候甚至都不要声明 LiveData 对象。




第二种封装我在公司的商业项目App中用了很长时间了,涉及几十个接口,暂时没遇到什么问题。


第三种是我最近才折腾出来的,在公司的新项目中(还没上线)使用,也暂时没遇到什么问题。


如果某位大神看到这篇文章,有不同意见,或者发现封装三有漏洞,欢迎指出,不甚感谢!


项目地址


FastJetpack


项目持续更新...


作者:零先生
链接:https://juejin.cn/post/7022823222928211975
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

MVVM 进阶版:MVI 架构了解一下~

MVI
前言 Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。 不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而...
继续阅读 »

前言


Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。

不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI可以很好的解决一部分MVVM的痛点。

本文主要包括以下内容



  1. MVC,MVP,MVVM等经典架构介绍

  2. MVI架构到底是什么?

  3. MVI架构实战



需要重点指出的是,标题中说MVI架构是MVVM的进阶版是指MVIMVVM非常相似,并在其基础上做了一定的改良,并不是说MVI架构一定比MVVM适合你的项目

各位同学可以在分析比较各个架构后,选择合适项目场景的架构



经典架构介绍


MVC架构介绍


MVC是个古老的Android开发架构,随着MVPMVVM的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:



MVC架构主要分为以下几部分



  1. 视图层(View):对应于xml布局文件和java代码动态view部分

  2. 控制层(Controller):主要负责业务逻辑,在android中由Activity承担,同时因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。

  3. 模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源


由于androidxml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在androidmvc更像是这种形式:



因此MVC架构在android平台上的主要存在以下问题:



  1. Activity同时负责ViewController层的工作,违背了单一职责原则

  2. Model层与View层存在耦合,存在互相依赖,违背了最小知识原则


MVP架构介绍


由于MVC架构在Android平台上的一些缺陷,MVP也就应运而生了,其架构图如下所示



MVP架构主要分为以下几个部分



  1. View层:对应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦合

  2. Presenter层: 主要负责处理业务逻辑,通过接口回调View

  3. Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化


我们可以看到,MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题


MVP架构同样有自己的问题



  1. Presenter层通过接口与View通信,实际上持有了View的引用

  2. 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。


MVVM架构介绍


MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然

MVVM架构图如下所示:



可以看出MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP


MVVM的双向数据绑定主要通过DataBinding实现,不过相信有很多人跟我一样,是不喜欢用DataBinding的,这样架构就变成了下面这样



  1. View观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性我其实并没有用到

  2. View通过调用ViewModel提供的方法来与ViewMdoel交互


小结



  1. MVC架构的主要问题在于Activity承担了ViewController两层的职责,同时View层与Model层存在耦合

  2. MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter

  3. MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题

  4. MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP

  5. MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定


MVI架构到底是什么?


MVVM架构有什么不足?


要了解MVI架构,我们首先来了解下MVVM架构有什么不足

相信使用MVVM架构的同学都有如下经验,为了保证数据流的单向流动,LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘,如下所示


class TestViewModel : ViewModel() {
//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量
private val _pageState: MutableLiveData<PageState> = MutableLiveData()
val pageState: LiveData<PageState> = _pageState
private val _state1: MutableLiveData<String> = MutableLiveData()
val state1: LiveData<String> = _state1
private val _state2: MutableLiveData<String> = MutableLiveData()
val state2: LiveData<String> = _state2
//...
}

如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。

其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系


小结一下,在我的使用中,MVVM架构主要有以下不足



  1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘

  2. View层与ViewModel层的交互比较分散零乱,不成体系


MVI架构是什么?


MVIMVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示



其主要分为以下几部分



  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态

  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新(注意:这里不是ActivityIntent

  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求


单向数据流


MVI强调数据的单向流动,主要分为以下几步:



  1. 用户操作以Intent的形式通知Model

  2. Model基于Intent更新State

  3. View接收到State变化刷新UI。


数据永远在一个环形结构中单向流动,不能反向流动:


上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的


MVI架构实战


总体架构图




我们使用ViewModel来承载MVIModel层,总体结构也与MVVM类似,主要区别在于ModelView层交互的部分



  1. Model层承载UI状态,并暴露出ViewStateView订阅,ViewState是个data class,包含所有页面状态

  2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式


MVI实例介绍


添加ViewStateViewEvent


ViewState承载页面的所有状态,ViewEvent则是一次性事件,如Toast等,如下所示


data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)  

sealed class MainViewEvent {
data class ShowSnackbar(val message: String) : MainViewEvent()
data class ShowToast(val message: String) : MainViewEvent()
}


  1. 我们这里ViewState只定义了两个,一个是请求状态,一个是页面数据

  2. ViewEvent也很简单,一个简单的密封类,显示ToastSnackbar


ViewState更新


class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()
val viewStates = _viewStates.asLiveData()
private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()
val viewEvents = _viewEvents.asLiveData()

init {
emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))
}

private fun fabClicked() {
count++
emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
}

private fun emit(state: MainViewState?) {
_viewStates.value = state
}

private fun emit(event: MainViewEvent?) {
_viewEvents.value = event
}
}

如上所示



  1. 我们只需定义ViewStateViewEvent两个State,后续增加状态时在data class中添加即可,不需要再写模板代码

  2. ViewEvents是一次性的,通过SingleLiveEvent实现,当然你也可以用Channel当来实现

  3. 当状态更新时,通过emit来更新状态


View监听ViewState


    private fun initViewModel() {
viewModel.viewStates.observe(this) {
renderViewState(it)
}
viewModel.viewEvents.observe(this) {
renderViewEvent(it)
}
}

如上所示,MVI 使用 ViewStateState 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。


View通过Action更新State


class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)

private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}

如上所示,View通过ActionViewModel交互,通过 Action 通信,有利于 ViewViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控


总结


本文主要介绍了MVC,MVP,MVVMMVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点



  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源

  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘

  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护


MVI可以比较好的解决以上痛点,它主要有以下优势



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯

  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。


当然MVI也有一些缺点,比如



  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销


软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。

但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBindingMVVM是一个比较好的选择~


Sample代码


github.com/shenzhen201…


作者:RicardoMJiang
链接:https://juejin.cn/post/7022624191723601928
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android IPC 之 Messenger

绑定服务(Bound Services)概述 绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运...
继续阅读 »

绑定服务(Bound Services)概述


绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运行


💥 基础知识


绑定服务是 Service 类的实现,它允许其他应用程序绑定到它并与之交互。 要为服务提供绑定,你必须实现 onBind() 回调方法。 此方法返回一个 IBinder 对象,该对象定义了客户端可用于与服务交互的编程接口。


🔥 Messenger


💥 概述


一提到IPC 很多人的反应都是 AIDL,其实如果仅仅是多进程单线程,那么你可以使用 Messenger 为你的服务提供接口。


使用 Messenger 比使用 AIDL 更简单,因为 Messenger 会将所有对服务的调用排入队列


对于大多数应用程序,该服务不需要执行多线程,因此使用 Messenger 允许该服务一次处理一个调用。如果你的 服务多线程很重要,那你就要用到ALDL了。


💥 使用 Messenger 步骤




  • 1、该 Service 实现了一个 Handler,该 Handler 接收来自客户端的每次调用的回调。




  • 2、该服务使用 Handler 创建一个 Messenger 对象(它是对 Handler 的引用)。




  • 3、Messenger 创建一个 IBinder,该服务从 onBind() 返回给客户端。




  • 4、客户端使用 IBinder 来实例化 Messenger(引用服务的Handler),客户端使用 Handler 来向服务发送 Message 对象。




  • 5、服务在其 Handler 的 handleMessage() 中接收每个消息。




💥 实例(Client到Server数据传递)


🌀 MessengerService.java


public class MessengerService extends Service {
public static final int MSG_SAY_HELLO = 0;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService:onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
static class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_SAY_HELLO:
Bundle bundle = msg.getData();
String string = bundle.getString("name");
//处理来自客户端的消息
MLog.e("handleMessage:来自Acitvity的"+string);
break;
case 1:

break;
default:
super.handleMessage(msg);
}
}
}
}

🌀 AndroidMainfest.xml


        <service android:name=".ipc.MessengerService"
android:process="com.scc.ipc.messengerservice"
android:exported="true"
android:enabled="true"/>

使用 android:process 属性 创建不同进程。


🌀 MainActivity.class


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
Message message = Message.obtain(null, MessengerService.MSG_SAY_HELLO);
Bundle bundle = new Bundle();
bundle.putString("name","Scc");
message.setData(bundle);
try {
mService.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



两个进程也存在着,也完成了进程间的通信,并把数据传递过去了。


💥 实例(Server将数据传回Client)


我不仅想将消息传递给 Server ,还想让 Server 将数据处理后传会Client。


🌀 MessengerService.java


public class MessengerService extends Service {
/** 用于显示和隐藏我们的通知。 */
ArrayList<Messenger> mClients = new ArrayList<Messenger>();
/** 保存客户端设置的最后一个值。 */
int mValue = 0;

/**
* 数组中添加 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是应该发送回调的客户端的 Messenger。
*/
public static final int MSG_REGISTER_CLIENT = 1;

/**
* 数组中删除 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是之前用 MSG_REGISTER_CLIENT 给出的客户端的 Messenger。
*/
public static final int MSG_UNREGISTER_CLIENT = 2;
/**
* 用于设置新值。
* 这可以发送到服务以提供新值,并将由服务发送给具有新值的任何注册客户端。
*/
public static final int MSG_SET_VALUE = 3;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService-onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_REGISTER_CLIENT:
mClients.add(msg.replyTo);
break;
case MSG_UNREGISTER_CLIENT:
mClients.remove(msg.replyTo);
break;
case MSG_SET_VALUE:
mValue = msg.arg1;
for (int i=mClients.size()-1; i>=0; i--) {
try {
mClients.get(i).send(Message.obtain(null,
MSG_SET_VALUE, mValue, 0));
} catch (RemoteException e) {
// 客户端没了。 从列表中删除它;
//从后往前安全,从前往后遍历数组越界。
mClients.remove(i);
}
}
default:
super.handleMessage(msg);
}
}
}
}

🌀 MainActivity.java


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};
static class ReturnHander extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MessengerService.MSG_SET_VALUE:
//我要起飞:此处处理
MLog.e("Received from service: " + msg.arg1);
break;
default:
super.handleMessage(msg);
}
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
try {
mMessenger = new Messenger(new ReturnHander());
Message msg = Message.obtain(null,
MessengerService.MSG_REGISTER_CLIENT);
msg.replyTo = mMessenger;
//先发一则消息添加Messenger:msg.replyTo = mMessenger;
mService.send(msg);

// Give it some value as an example.
msg = Message.obtain(null,
MessengerService.MSG_SET_VALUE, this.hashCode(), 0);
//传入的arg1值:this.hashCode()
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



我们在MainActivity 的 Handler.sendMessger()中接收到了来自 MesengerService 的消息 。


本次 Messenger 进程间通信齐活,这只是个简单的Demo。最后咱们看一波源码。


🔥 Messenger 源码


Messenger.java


public final class Messenger implements Parcelable {
private final IMessenger mTarget;
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public void send(Message message) throws RemoteException {
mTarget.send(message);
}
public IBinder getBinder() {
return mTarget.asBinder();
}
...
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
}

然后你会发现 只要代码还是在 IMessenger 里面,咱们去找找。


IMessenger.aidl


package android.os;

import android.os.Message;

/** @hide */
oneway interface IMessenger {
void send(in Message msg);
}

new Messenger(Handler handelr)


这里其实是用Handler 调用 getIMessenger() 。咱们去Handler.class里面转转。


    @UnsupportedAppUsage
final IMessenger getIMessenger() {
synchronized (mQueue) {
if (mMessenger != null) {
return mMessenger;
}
mMessenger = new MessengerImpl();
return mMessenger;
}
}
//创建了Messenger实现类
private final class MessengerImpl extends IMessenger.Stub {
public void send(Message msg) {
msg.sendingUid = Binder.getCallingUid();
//Messenger调用send()方法,通过Handler发送消息。
//然后在服务端通过Handler的handleMessge(msg)接收这个消息。
Handler.this.sendMessage(msg);
}
}

new Messenger(IBinder target)


package android.os;
/** @hide */
public interface IMessenger extends android.os.IInterface
{
/** Default implementation for IMessenger. */
public static class Default implements android.os.IMessenger
{
@Override public void send(android.os.Message msg) throws android.os.RemoteException
{
}
@Override
public android.os.IBinder asBinder() {
return null;
}
}
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements android.os.IMessenger
{
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an android.os.IMessenger interface,
* generating a proxy if needed.
*/
public static android.os.IMessenger asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
//判断是否在同一进程。
if (((iin!=null)&&(iin instanceof android.os.IMessenger))) {
//同一进程
return ((android.os.IMessenger)iin);
}
//代理对象
return new android.os.IMessenger.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
...
}
public void send(android.os.Message msg) throws android.os.RemoteException;
}

看了上面代码你会发现这不就是个aidl吗? 什么是aidl,咱们下一篇继续讲到。


作者:Android帅次
链接:https://juejin.cn/post/7022881022438015007
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一天高中的女同桌突然问我是不是程序猿

背景 昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”, 先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人...
继续阅读 »

背景


昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”,


image-20211015101843733.png


先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人生苦短,我用python),即使如此,
为了大家的面子,为了程序猿们的脸,不就简单的小Python嘛,必须答应!


梳理需求


现有excel表格记录着 有效图片的名字,如:


image-20211015103418631.png


要从一个文件夹里把excel表格里记录名字的图片筛选出来;


需求也不是很难,代码思路就有了:



  1. 读取Excel表格第一列的信息并放入A集合

  2. 遍历文件夹下所有的文件,判断文件名字是否存在A集合

  3. 存在A集合则拷贝到目标文件夹


实现(Python 2.7)


读取Excel表格

加载Excel表格的方法有很多种,例如pandasxlrdopenpyxl,我这里选择openpyxl库,
先安装库



pip install openpyxl



代码如下:


from openpyxl import load_workbook

def handler_excel(filename=r'C:/Users/xxx/Desktop/haha.xlsx'):
   # 根据文件路径加载一个excel表格,这里包含所有的sheet
   excel = load_workbook(filename)
   # 根据sheet名称加载对应的table
   table = excel.get_sheet_by_name('Sheet1')
   imgnames = []
   # 读取所有列
   for column in table.columns:
       for cell in column:
           imgnames.append(cell.value+".png")
# 选择图片
   pickImg(imgnames)

遍历文件夹读取文件名,找到target并拷贝

使用os.listdir 方法遍历文件,这里注意windows环境下拿到的unicode编码,需要GBK重新解码


def pickImg(pickImageNames):
   # 遍历所有图片集的文件名
   for image in os.listdir(
           r"C:\Users\xxx\Desktop\work\img"):
       # 使用gbk解码,不然中文乱码
       u_file = image.decode('gbk')
       print(u_file)
       if u_file in pickImageNames:
           oldname = r"C:\Users\xxx\Desktop\work\img/" + image
           newname = r"C:\Users\xxx\Desktop\work\target/" + image
           # 文件拷贝
           shutil.copyfile(oldname, newname)

简单搞定!没有砸程序猿的招牌,豪横的把成果发给女同桌,结果:


image-20211015112550343.png


换来有机会请你吃饭,微信都不带回的,哎 ,xdm,小丑竟是我自己!
小丑竟是我自己什么梗-小丑竟是我自己是什么意思出自什么-55手游网


作者:李诺曹
链接:https://juejin.cn/post/7019167108185456677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

public class testT { public static void main(String [] args){ String A = "hi你是乔戈里"; System.out.println(A.lengt...
继续阅读 »


public class testT {
public static void main(String [] args){
String A = "hi你是乔戈里";
System.out.println(A.length());
}
}复制代码


以上结果输出为7。







小萌边说边在IDEA中的win环境下选中String.length()函数,使用ctrl+B快捷键进入到String.length()的定义。


    /**
* Returns the length of this string.
* The length is equal to the number of <a href="Character.html#unicode">Unicode
* code units</a> in the string.
*
* @return the length of the sequence of characters represented by this
* object.
*/
public int length() {
return value.length;
}复制代码


接着使用google翻译对这段英文进行了翻译,得到了大体意思:返回字符串的长度,这一长度等于字符串中的 Unicode 代码单元的数目。


小萌:乔戈里,那这又是啥意思呢?乔哥:前几天我写的一篇文章:面试官问你编码相关的面试题,把这篇甩给他就完事!)里面对于Java的字符使用的编码有介绍:


Java中 有内码和外码这一区分简单来说



  • 内码:char或String在内存里使用的编码方式。
  • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)



而java内码:unicode(utf-16)中使用的是utf-16.所以上面的那句话再进一步解释就是:返回字符串的长度,这一长度等于字符串中的UTF-16的代码单元的数目。




代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元。UTF-X 中的数字 X 就是各自代码单元的位数。


UTF-16 的 16 指的就是最小为 16 位一个单元,也即两字节为一个单元,UTF-16 可以包含一个单元和两个单元,对应即是两个字节和四个字节。我们操作 UTF-16 时就是以它的一个单元为基本单位的。


你还记得你前几天被面试官说菜的时候学到的Unicode知识吗,在面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜这里面提到,UTF-16编码一个字符对于U+0000-U+FFFF范围内的字符采用2字节进行编码,而对于字符的码点大于U+FFFF的字符采用四字节进行编码,前者是两字节也就是一个代码单元,后者一个字符是四字节也就是两个代码单元!


而上面我的例子中的那个字符的Unicode值就是“U+1D11E”,这个Unicode的值明显大于U+FFFF,所以对于这个字符UTF-16需要使用四个字节进行编码,也就是使用两个代码单元!


所以你才看到我的上面那个示例结果表示一个字符的String.length()长度是2!



来看个例子!


public class testStringLength {
public static void main(String [] args){
String B = "𝄞"; // 这个就是那个音符字符,只不过由于当前的网页没支持这种编码,所以没显示。
String C = "\uD834\uDD1E";// 这个就是音符字符的UTF-16编码
System.out.println(C);
System.out.println(B.length());
System.out.println(B.codePointCount(0,B.length()));
// 想获取这个Java文件自己进行演示的,可以在我的公众号【程序员乔戈里】后台回复 6666 获取
}
}复制代码



可以看到通过codePointCount()函数得知这个音乐字符是一个字符!




几个问题:0.codePointCount是什么意思呢?1.之前不是说音符字符是“U+1D11E”,为什么UTF-16是"uD834uDD1E",这俩之间如何转换?2.前面说了UTF-16的代码单元,UTF-32和UTF-8的代码单元是多少呢?



一个一个解答:


第0个问题:


codePointCount其实就是代码点数的意思,也就是一个字符就对应一个代码点数。


比如刚才音符字符(没办法打出来),它的代码点是U+1D11E,但它的代理单元是U+D834和U+DD1E,如果令字符串str = "u1D11E",机器识别的不是音符字符,而是一个代码点”/u1D11“和字符”E“,所以会得到它的代码点数是2,代码单元数也是2。


但如果令字符str = "uD834uDD1E",那么机器会识别它是2个代码单元代理,但是是1个代码点(那个音符字符),故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.


第1个问题




上图是对应的转换规则:



  • 首先 U+1D11E-U+10000 = U+0D11E
  • 接着将U+0D11E转换为二进制:0000 1101 0001 0001 1110,前10位是0000 1101 00 后10位是01 0001 1110
  • 接着套用模板:110110yyyyyyyyyy 110111xxxxxxxxxx
  • U+0D11E的二进制依次从左到右填入进模板:110110 0000 1101 00 110111 01 0001 1110
  • 然后将得到的二进制转换为16进制:d834dd1e,也就是你看到的utf-16编码了



第2个问题




  • 同理,UTF-32 以 32 位一个单元,它只包含这一种单元就够了,它的一单元自然也就是四字节了。
  • UTF-8 的 8 指的就是最小为 8 位一个单元,也即一字节为一个单元,UTF-8 可以包含一个单元,二个单元,三个单元及四个单元,对应即是一,二,三及四字节。






作者:程序员乔戈里
链接:https://juejin.cn/post/6844904036873814023
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

领导:谁再用定时任务实现关闭订单,立马滚蛋!

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢? 一般的做法有如下几种定时任务关闭订单rocketmq延迟...
继续阅读 »

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?


一般的做法有如下几种

定时任务关闭订单

rocketmq延迟队列

rabbitmq死信队列

时间轮算法

redis过期监听


一、定时任务关闭订单(最low)


一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明


image.png


我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟;通过上图我们看出,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS


二、rocketmq延迟队列方式


延迟消息
生产者把消息发送到消息服务器后,并不希望被立即消费,而是等待指定时间后才可以被消费者消费,这类消息通常被称为延迟消息。
在RocketMQ开源版本中,支持延迟消息,但是不支持任意时间精度的延迟消息,只支持特定级别的延迟消息。
消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别。


发送延迟消息(生产者)


/**
* 推送延迟消息
*
@param topic
*
@param body
*
@param producerGroup
*
@return boolean
*/

public boolean sendMessage(String topic, String body, String producerGroup)
{
try
{
Message recordMsg = new Message(topic, body.getBytes());
producer.setProducerGroup(producerGroup);

//设置消息延迟级别,我这里设置14,对应就是延时10分钟
// "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
recordMsg.setDelayTimeLevel(14);
// 发送消息到一个Broker
SendResult sendResult = producer.send(recordMsg);
// 通过sendResult返回消息是否成功送达
log.info("发送延迟消息结果:======sendResult:{}", sendResult);
DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("发送时间:{}", format.format(new Date()));

return true;
}
catch (Exception e)
{
e.printStackTrace();
log.error("延迟消息队列推送消息异常:{},推送内容:{}", e.getMessage(), body);
}
return false;
}

消费延迟消息(消费者)


/**
* 接收延迟消息
*
* @param topic
* @param consumerGroup
* @param messageHandler
*/

public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler)
{
ThreadPoolUtil.execute(() ->
{
try
{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setConsumerGroup(consumerGroup);
consumer.setVipChannelEnabled(false);
consumer.setNamesrvAddr(address);
//设置消费者拉取消息的策略,*表示消费该topic下的所有消息,也可以指定tag进行消息过滤
consumer.subscribe(topic, "*");
//消费者端启动消息监听,一旦生产者发送消息被监听到,就打印消息,和rabbitmq中的handlerDelivery类似
consumer.registerMessageListener(messageHandler);
consumer.start();
log.info("启动延迟消息队列监听成功:" + topic);
}
catch (MQClientException e)
{
log.error("启动延迟消息队列监听失败:{}", e.getErrorMessage());
System.exit(1);
}
});
}

实现监听类,处理具体逻辑


/**
* 延迟消息监听
*
*/

@Component
public class CourseOrderTimeoutListener implements ApplicationListener
{

@Resource
private MQUtil mqUtil;

@Resource
private CourseOrderTimeoutHandler courseOrderTimeoutHandler;

@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent)
{
// 订单超时监听
mqUtil.messageListener(EnumTopic.ORDER_TIMEOUT, EnumGroup.ORDER_TIMEOUT_GROUP, courseOrderTimeoutHandler);
}
}

/**
* 实现监听
*/

@Slf4j
@Component
public class CourseOrderTimeoutHandler implements MessageListenerConcurrently
{

@Override
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list)
{
// 得到消息体
String body = new String(msg.getBody());
JSONObject userJson = JSONObject.parseObject(body);
TCourseBuy courseBuyDetails = JSON.toJavaObject(userJson, TCourseBuy.class);

// 处理具体的业务逻辑,,,,,

DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("消费时间:{}", format.format(new Date()));

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}

这种方式相比定时任务好了很多,但是有一个致命的缺点,就是延迟等级只有18种(商业版本支持自定义时间),如果我们想把关闭订单时间设置在15分钟该如何处理呢?显然不够灵活。


三、rabbitmq死信队列的方式


Rabbitmq本身是没有延迟队列的,只能通过Rabbitmq本身队列的特性来实现,想要Rabbitmq实现延迟队列,需要使用Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)


死信交换机
一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。


一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
上面的消息的TTL到了,消息过期了。


队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。
死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机,并不是说死信交换机是某种特定的交换机


消息TTL(消息存活时间)
消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取值较小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。


byte[] messageBodyBytes = "Hello, world!".getBytes();  
AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setExpiration("60000");
channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);

可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串:当上面的消息扔到队列中后,过了60秒,如果没有被消费,它就死了。不会被消费者消费到。这个消息后面的,没有“死掉”的消息对顶上来,被消费者消费。死信在队列中并不会被删除和释放,它会被统计到队列的消息数中去


处理流程图


image.png


创建交换机(Exchanges)和队列(Queues)


创建死信交换机


image.png


如图所示,就是创建一个普通的交换机,这里为了方便区分,把交换机的名字取为:delay


创建自动过期消息队列
这个队列的主要作用是让消息定时过期的,比如我们需要2小时候关闭订单,我们就需要把消息放进这个队列里面,把消息过期时间设置为2小时


image.png


创建一个一个名为delay_queue1的自动过期的队列,当然图片上面的参数并不会让消息自动过期,因为我们并没有设置x-message-ttl参数,如果整个队列的消息有消息都是相同的,可以设置,这里为了灵活,所以并没有设置,另外两个参数x-dead-letter-exchange代表消息过期后,消息要进入的交换机,这里配置的是delay,也就是死信交换机,x-dead-letter-routing-key是配置消息过期后,进入死信交换机的routing-key,跟发送消息的routing-key一个道理,根据这个key将消息放入不同的队列


创建消息处理队列
这个队列才是真正处理消息的队列,所有进入这个队列的消息都会被处理


image.png


消息队列的名字为delay_queue2
消息队列绑定到交换机
进入交换机详情页面,将创建的2个队列(delayqueue1和delayqueue2)绑定到交换机上面


image.png
自动过期消息队列的routing key 设置为delay
绑定delayqueue2


image.png


delayqueue2 的key要设置为创建自动过期的队列的x-dead-letter-routing-key参数,这样当消息过期的时候就可以自动把消息放入delay_queue2这个队列中了
绑定后的管理页面如下图:


image.png


当然这个绑定也可以使用代码来实现,只是为了直观表现,所以本文使用的管理平台来操作
发送消息


String msg = "hello word";  
MessageProperties messageProperties = newMessageProperties();
messageProperties.setExpiration("6000");
messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes());
Message message = newMessage(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend("delay", "delay",message);

设置了让消息6秒后过期
注意:因为要让消息自动过期,所以一定不能设置delay_queue1的监听,不能让这个队列里面的消息被接受到,否则消息一旦被消费,就不存在过期了


接收消息
接收消息配置好delay_queue2的监听就好了


package wang.raye.rabbitmq.demo1;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
publicclassDelayQueue{
/** 消息交换机的名字*/
publicstaticfinalString EXCHANGE = "delay";
/** 队列key1*/
publicstaticfinalString ROUTINGKEY1 = "delay";
/** 队列key2*/
publicstaticfinalString ROUTINGKEY2 = "delay_key";
/**
* 配置链接信息
* @return
*/

@Bean
publicConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = newCachingConnectionFactory("120.76.237.8",5672);
connectionFactory.setUsername("kberp");
connectionFactory.setPassword("kberp");
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true); // 必须要设置
return connectionFactory;
}
/**
* 配置消息交换机
* 针对消费者配置
FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
HeadersExchange :通过添加属性key-value匹配
DirectExchange:按照routingkey分发到指定队列
TopicExchange:多关键字匹配
*/

@Bean
publicDirectExchange defaultExchange() {
returnnewDirectExchange(EXCHANGE, true, false);
}
/**
* 配置消息队列2
* 针对消费者配置
* @return
*/

@Bean
publicQueue queue() {
returnnewQueue("delay_queue2", true); //队列持久
}
/**
* 将消息队列2与交换机绑定
* 针对消费者配置
* @return
*/

@Bean
@Autowired
publicBinding binding() {
returnBindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2);
}
/**
* 接受消息的监听,这个监听会接受消息队列1的消息
* 针对消费者配置
* @return
*/

@Bean
@Autowired
publicSimpleMessageListenerContainer messageContainer2(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = newSimpleMessageListenerContainer(connectionFactory());
container.setQueues(queue());
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置确认模式手工确认
container.setMessageListener(newChannelAwareMessageListener() {
publicvoid onMessage(Message message, com.rabbitmq.client.Channel channel) throwsException{
byte[] body = message.getBody();
System.out.println("delay_queue2 收到消息 : "+ newString(body));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //确认消息成功消费
}
});
return container;
}
}

这种方式可以自定义进入死信队列的时间;是不是很完美,但是有的小伙伴的情况是消息中间件就是rocketmq,公司也不可能会用商业版,怎么办?那就进入下一节


四、时间轮算法


image.png


(1)创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)


(2)任务集合,环上每一个slot是一个Set
同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。


Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
(2)订单号,要关闭的订单号(也可以是其他信息,比如:是一个基于某个订单号的任务)


假设当前Current Index指向第0格,例如在3610秒之后,有一个订单需要关闭,只需:
(1)计算这个订单应该放在哪一个slot,当我们计算的时候现在指向1,3610秒之后,应该是第10格,所以这个Task应该放在第10个slot的Set中
(2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1


Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0:
(1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1
(2)如果是0,说明马上要执行这个关单Task了,取出订单号执行关单(可以用单独的线程来执行Task),并把这个订单信息从Set中删除即可。
(1)无需再轮询全部订单,效率高
(2)一个订单,任务只执行一次
(3)时效性好,精确到秒(控制timer移动频率可以控制精度)


五、redis过期监听


1.修改redis.windows.conf配置文件中notify-keyspace-events的值
默认配置notify-keyspace-events的值为 ""
修改为 notify-keyspace-events Ex 这样便开启了过期事件


2. 创建配置类RedisListenerConfig(配置RedisMessageListenerContainer这个Bean)


package com.zjt.shop.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisListenerConfig {

@Autowired
private RedisTemplate redisTemplate;

/**
*
@return
*/

@Bean
public RedisTemplate redisTemplateInit() {

// key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());

//val实例化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

return redisTemplate;
}

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}

}

3.继承KeyExpirationEventMessageListener创建redis过期事件的监听类


package com.zjt.shop.common.util;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.zjt.shop.modules.order.service.OrderInfoService;
import com.zjt.shop.modules.product.entity.OrderInfoEntity;
import com.zjt.shop.modules.product.mapper.OrderInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

@Autowired
private OrderInfoMapper orderInfoMapper;

/**
* 针对redis数据失效事件,进行数据处理
*
@param message
*
@param pattern
*/

@Override
public void onMessage(Message message, byte[] pattern) {
try {
String key = message.toString();
//从失效key中筛选代表订单失效的key
if (key != null && key.startsWith("order_")) {
//截取订单号,查询订单,如果是未支付状态则为-取消订单
String orderNo = key.substring(6);
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfoEntity orderInfo = orderInfoMapper.selectOne(queryWrapper);
if (orderInfo != null) {
if (orderInfo.getOrderState() == 0) { //待支付
orderInfo.setOrderState(4); //已取消
orderInfoMapper.updateById(orderInfo);
log.info("订单号为【" + orderNo + "】超时未支付-自动修改为已取消状态");
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("【修改支付订单过期状态异常】:" + e.getMessage());
}
}
}

4:测试
通过redis客户端存一个有效时间为3s的订单:


image.png


结果:


image.png


总结:
以上方法只是个人对于关单的一些想法,可能有些地方有疏漏,请在公众号直接留言进行指出,当然如果你有更好的关单方式也可以随时沟通交流


作者:程序员阿牛
链接:https://juejin.cn/post/6987233263660040206
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

搜索历史记录的实现-Android

前言最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路主要逻辑搜索后保存当前内容将最新的搜索记录在最前面搜索历史记录可以...
继续阅读 »

前言

最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路

主要逻辑

  1. 搜索后保存当前内容
  2. 将最新的搜索记录在最前面
  3. 搜索历史记录可以点击并执行搜索功能,并将其提到最前面

我里面使用了ObjectBox作为数据存储,因为实际项目用的Java所以没用Room,而且Room好像第一次搜索至少要200ms,不过可以在某个activity随便搜索热启动一下.GreenDao使用有点麻烦,查询条件没有什么太大需求,直接用ObjectBox了,而且使用超级简单

Code

ObjectBox的工具类

public class ObjectBoxUtils {
public static BoxStore init() {
BoxStore boxStore = null;
try {
boxStore = MyApplication.getBoxStore();
if (boxStore == null) {
boxStore = MyObjectBox.builder().androidContext(MyApplication.applicationContext).build();
MyApplication.setBoxStore(boxStore);
}
} catch (Exception e) {
}
return boxStore;
}


public static <T> List<T> getAllData(Class clazz) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<T> box = boxStore.boxFor(clazz);

return box.getAll();
}
} catch (Exception e) {
}
return new ArrayList<>();
}


/**
* 添加数据
*/
public static <T> long addData(T o, Class c) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
return boxStore.boxFor(c).put(o);
}
} catch (Throwable e) {
}
return 0;
}


public static HistoryBean getHistroyBean(String name) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<HistoryBean> box = boxStore.boxFor(HistoryBean.class);
HistoryBean first = box.query().equal(HistoryBean_.name, name).build().findFirst();
return first;
}
} catch (Exception e) {
}
return null;
}
}

其实我在Application就初始化了ObjectBox,但是实际项目中有时候会初始化失败,导致直接空指针,所有每次调用我都会判断一下是否初始化了,没有的话就进行相应操作

Activity

class HistoryActivity : AppCompatActivity() {
private var list: MutableList<HistoryBean>? = null
private var inflate: ActivityHistoryBinding? = null
private var historyAdapter: HistoryAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inflate = ActivityHistoryBinding.inflate(layoutInflater)
setContentView(inflate?.root)
list = ObjectBoxUtils.getAllData(HistoryBean::class.java)
list?.sort()
inflate!!.rv.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
historyAdapter = HistoryAdapter(this, list)
inflate!!.rv.adapter = historyAdapter


inflate!!.et.setOnEditorActionListener(object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
saveHistory(inflate!!.et.text.toString())
return true
}

})
}

/**
* 保存搜索历史
*
*/
fun saveHistory(keyWord: String) {
//查询本地是否有name为参数中的数据
var histroyBean: HistoryBean? = ObjectBoxUtils.getHistroyBean(keyWord)
val currentTimeMillis = System.currentTimeMillis()
//没有就新创建一个
if (histroyBean == null) {
histroyBean = HistoryBean(currentTimeMillis, keyWord, currentTimeMillis)
} else {
//有的话就更新时间,也就说明了两种情况,第一 重复搜索了,搜索肯定要排重嘛,第二就是我们点击历史记录了,因此更新下时间
histroyBean.setTime(currentTimeMillis)
}
//把新/旧数据保存到本地
ObjectBoxUtils.addData(histroyBean, HistoryBean::class.java)
//每一次操作都从数据库拿取数据,性能消耗很低,就这么一个小模块没必要上纲上线
list?.clear()
list?.addAll(ObjectBoxUtils.getAllData(HistoryBean::class.java))
//实体Bean重写了Comparable,排序一下
list?.sort()
historyAdapter?.notifyDataSetChanged()
}
}

相应注释都在代码里,说实话kotlin用的好难受啊,还是自己语法学的不行,一个小东西卡我好久,导致我Application里面直接删除用Java重写了

实体类

@Entity
public class HistoryBean implements Comparable<HistoryBean> {
@Id(assignable = true)
public long id;

public HistoryBean(long id, String name, long time) {
this.id = id;
this.name = name;
this.time = time;
}

public String name;

public long time;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public long getTime() {
return time;
}

public void setTime(long time) {
this.time = time;
}


@Override
public int compareTo(HistoryBean o) {
return (int) (o.time-time);
}
}

实体类重写了CompareTo,因为集合的sort实际上也是调用了ComparteTo,我们直接重写相应逻辑就简化业务层很多代码

效果

历史记录.gif

嗯,效果还不错,继续学习令人脑壳痛的自定义View去了


收起阅读 »

动态代理的使用-功能增强

背景接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserverxxActivit...
继续阅读 »

背景

接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:

切换主线程时序图.png

  1. xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserver
  2. xxActivity 也需要创建observer用于主线程回调, 记为 uiObserver
  3. xxPresenter 在收到 innerObserver 的回调后通过主线程handler进行线程切换, 最终触发 uiObserver 的对应方法
  4. 业务需求回调方法都在 xxActivity 主线程中执行后续操作, innerObserver 几乎仅用于线程切换而已

存在的问题

  1. 如图第2/3步, 对于同一类型的observer, 需要在 activity , presenter中各实现一次, presenter中会产生大量模板代码
  2. 如图第6步, 收到 SDKService 回调后, presenter需要构建Message, 设置各回调实参, 这完全依赖开发人员手动配置, 效率低下且易发生错误, 灵活度低
  3. 对应的, 第11步通过handler线程切换时, 又需要从 message 中依次还原各实参, 这一步同样依赖开发人员手动处理
  4. observer变化时(如形参列表顺序/类型发生变更), 均需要同步更新 prenter 和 handler
  5. 我司项目最多时, 某个SDKService有将近100个observer需要设置, 部分observer的方法数甚至超过45个, 导致单纯在 Presenter 中创建observer的空白匿名内部类时, 代码就超过100行, 模板代码过多
  6. ...

改造思路

根据已知条件:

  1. 各observer均为接口 interface 类型
  2. presenter 中实现的 innerObserver 仅用于进行线程切换,最终触发UI层创建的observer而已 --> 即:有统一的功能增强逻辑

自然联想到 代理模式 中的动态代理:

代理模式-图侵删,来源于C语言中文网

  1. 创建一个 ThreadSwitcher 辅助类, 可根据传入的 observer 的类型Class,自动生成动态代理类对象,即之前的 innerObserver, 然后作用于sdk中 --> 此步骤可节省prsetner中因 new observer(){} 产生的大量模板代码, 且在observer接口发生变更时, 也不需要修改代码,自动完成适配, 伪代码如下:
    Observer innerOb = ThreadSwitcher.generateInnerObserver(Observer.class)

  2. ThreadSwitcher 类同时透出接口供UI层传入用于主线程的observer, 缓存在 Map<Class,IObserver> 中, 供后续切换主线程时使用

  3. 当下层sdk回调动态代理对象时, 最终都会触发 InvocationHandler#invoke 方法, 其方法签名如下, 我们只需要在其方法体中构造runnable, 按需post到主线程中即可:

// package java.lang.reflect.InvocationHandler.java
/**
* @param method 接口中被触发的回调方法
* @param args 方法实参列表
*/

public Object invoke(Object proxy, Method method, Object[] args);
  1. 构造的runnable时, 需查找UI层注入的observer,并触发对应的方法, 而由于 InvocationHandler中已告知我们方法 method 及其实参 args , 因此可直接通过 method.invoke(uiObserver,args) 来触发 uiObserver 的对应方法, 具体代码见下一节

动态代理的使用

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

object ThreadSwitcher {
// ui层注入的observer, 会在主线程中回调
val uiObserverMap = mutableMapOf<Class<*>, Any>()
val targetHandler: Handler = Handler(Looper.mainLooper())

private fun runOnUIThread(runnable: Runnable) {
// 此处省略切换主线程代码,创建一个mainLooper的handler, post Runnable即可
}

// 生成代理类
fun <O> generateInnerObserver(clz: Class<O>): O? {
// 固定写法, 传入classLoader 和 待实现的接口列表, 以及核心的 InvocationHandler 的实现, 在其内部进行功能增强
return Proxy.newProxyInstance(clz.classLoader, arrayOf(clz), object : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {

// 1. 构造runnable, 用于主线程切换
val runnable = Runnable {

// 3. 查找 uiObserver, 若存在则触发
uiObserverMap[clz]?.let { uiObserver ->
val result = method?.invoke(uiObserver, args)
result
}
}

// 2. 将runnable抛主线程
runOnUIThread(runnable)

// 4. 触发method方法得到的返回值, 根据实际类型构造, void时返回null, 此处仅做示意
return null
}
}) as O // 按需强转为实现的接口类型
}
}

具体封装实现可参考如下链接:

改造后的流程如下:

改造后的时序图.png

源码分析

动态代理的实现很简单, 两三行代码就可以搞定, 系统肯定做了很多封装, 把脏活累活给做了, 我们简单看下

从入口方法开始: java.lang.reflect.Proxy#newProxyInstance

// package java.lang.reflect.Proxy.java  基于api 29
private static final Class<?>[] constructorParams = { InvocationHandler.class };

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h){
final Class<?>[] intfs = interfaces.clone();

// 从缓存中查找已生成过的class类型,若不存在则进行生成
Class<?> cl = getProxyClass0(loader, intfs);

// 反射调用构造方法 Proxy(InvocationHandler), 创建并返回实例
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
cons.setAccessible(true);
}
return cons.newInstance(new Object[]{h});
}

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* 创建代理类class
*/

private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
// 接口方法数限制
if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); }

// 优先从缓存中获取已创建过的代理类, 若不存在, 则创建
return proxyClassCache.get(loader, interfaces);
}

关键的 proxyClassCache 是个二级缓存类(WeakCache), 通过调用其 get 方法得到最终的实现类, 其构造方法签名如下:

// java.lang.reflect.WeakCache.java

/**
* Construct an instance of {@code WeakCache}
*
* @param subKeyFactory a function mapping a pair of
* {@code (key, parameter) -> sub-key}
* @param valueFactory a function mapping a pair of
* {@code (key, parameter) -> value}
* @throws NullPointerException if {@code subKeyFactory} or
* {@code valueFactory} is null.
*/

public WeakCache(BiFunction<K, P, ?> subKeyFactory, BiFunction<K, P, V> valueFactory) {

通过参数名也可以猜到最终是通过 valueFactory 生成的, 我们回到 Proxy 类看下:

// java.lang.reflect.Proxy.java

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* A factory function that generates, defines and returns the proxy class given
* the ClassLoader and array of interfaces.
*/

private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 所有动态代理类名的前缀
private static final String proxyClassNamePrefix = "$Proxy";

// 每一个动态代理类类名中唯一的数字,可猜测最终是分层的代理类名就是: $Proxy+数字
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
// 省略部分代码: 对传入的接口数组进行一些校验

String proxyPkg = null; // 最终实现类所在的包路径
int accessFlags = Modifier.PUBLIC | Modifier.FINAL; // 生成的代理类默认访问权限是: public final

// 对接口数组校验: 若待实现的接口是非public的, 则最终实现的代理类也是非public的,并且非public的接口需要在同一个包下
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

// 若待实现的接口均为 public, 则使用默认的包路径
if (proxyPkg == null) { proxyPkg = ""; }

{
List<Method> methods = getMethods(interfaces); // 递归获取所有接口(包括其父接口)的方法,并手动添加了 equals/hashCode/toString 三个方法
Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE); // 对所有接口方法排序
validateReturnTypes(methods); // 校验接口方法: 确保同名方法得返回类型一致
List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods); // 去除重复的方法,并获取每个方法对应的异常值信息

Method[] methodsArray = methods.toArray(new Method[methods.size()]);
Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

long num = nextUniqueNumber.getAndIncrement(); // 生成当前代理实现类的数字信息
String proxyName = proxyPkg + proxyClassNamePrefix + num; // 拼接生成代理类名,默认为: $Proxy+数字

return generateProxy(proxyName, interfaces, loader, methodsArray, exceptionsArray); // 通过native方法生成代理类Class
}
}

@FastNative
private static native Class<?> generateProxy(String name, Class<?>[] interfaces, ClassLoader loader, Method[] methods, Class<?>[][] exceptions);

/**
* 根据传入的接口class信息,获取所有的接口方法,并额外添加 equals/hashCode/toString 三个方法
*/

private static List<Method> getMethods(Class<?>[] interfaces) {
List<Method> result = new ArrayList<Method>();
try {
result.add(Object.class.getMethod("equals", Object.class));
result.add(Object.class.getMethod("hashCode", EmptyArray.CLASS));
result.add(Object.class.getMethod("toString", EmptyArray.CLASS));
} catch (NoSuchMethodException e) {
throw new AssertionError();
}

getMethodsRecursive(interfaces, result); // 通过递归反射的方式一次获取接口所有的方法
return result;
}
}

动态代理生成的类长啥样?

上面我们简单分析了下动态代理的源码, 我们可以知道/推测得到以下信息:

  1. 生成的代理类叫做 $ProxyN 其中 N 是一个数字,随代理类的增加而递增
  2. $ProxyN 实现了所有接口方法,并自动添加了 equals/hashCode/toString 三个方法,因此: --> a. 动态代理生成类应可以强转为任何传入的接口类型 --> b. 额外增加的三个方法通常会影响对象的比较,需要手动赋值区分
  3. 触发动态代理类的方法最终都会回调 InvocationHandler#invoke 方法,而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的,因此: --> 猜测生成 $ProxyN 类应是继承自 Proxy 类

猜测归猜测, 最好能导出生成的 $ProxyN 看下实际代码:

  1. 网上查到的通常是使用 JVM 提供的 sun.misc.ProxyGenerator 类, 但这个类在android中不存在,手动拷贝对应jar包到android中使用也有问题
  2. 尝试使用字节码操作库或者 Class#getResourceAsStream 等方式也失败了, 终究是JVM上的工具, 在android虚拟机上无法直接使用
  3. 最终退而求其次, 先通过反射获取 $ProxyN 的类结构, 至于方法的调用则通过 InvocationHandler#invoke 方法中打印堆栈来查看
// 1. 自定义一个接口如下
package org.lynxz.utils.observer
interface ICallback {
fun onCallback(a: Int, b: Boolean, c: String?)
}

// 2. 通过反射获取类结构
package org.lynxz.utils.reflect.ReflectUtilTest
@Test
fun oriProxyTest() {
val proxyObj = Proxy.newProxyInstance(
javaClass.classLoader,
arrayOf(ICallback::class.java)
) { proxy, method, args -> // InvocationHandler#invoke 方法体
RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace() // 3. 打印调用堆栈信息
args?.forEachIndexed { index, any -> // 4. 打印方法得实参
LoggerUtil.w(TAG, "===> 方法参数: $index - $any")
}
ReflectUtil.generateDefaultTypeValue(method!!.returnType) // 根据方法返回类型生成对应数据
}

// ProxyGeneratorImpl 是自定义的通过反射获取类结构的实现类, 具体代码请查看上面给出的github仓库
LoggerUtil.w(TAG, "===>类结构:\n${ProxyGeneratorImpl(proxyObj.javaClass).generate()}")
if (proxyObj is ICallback) { // 强转生成的动态代理类为自定义的接口
proxyObj.onCallback(1, true, "hello") // 触发接口方法,以便触发 InvocationHandler#invoke 方法, 进而打印堆栈
}
}

最终得到日志如下, 验证了之前的猜测:

// ===>类结构:
public final class $Proxy6 extends java.lang.reflect.Proxy implements ICallback{
public static final Class[] NFC;
public static final Class[][] NFD;
public $Proxy6(Class){...}
public final boolean equals(Object){...} // 方法体的内容不可知, 此处用省略号替代
public final int hashCode(){...}
public final String toString(){...}
public final void onCallback(int,boolean,String){...}
}

// 调用堆栈:
===> 调用堆栈:onCallback
at org.lynxz.utils.reflect.ReflectUtilTest$oriProxyTest$proxyObj$1.invok(ReflectUtilTest.kt:86) // 对应上方代码: RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace()
at java.lang.reflect.Proxy.invoke(Proxy.java:913) // 触发 Proxy#invoke 方法, 其内部直接触发 InvocationHandler#invoke 方法
at $Proxy6.onCallback(Unknown Source) // 对应上方代码: proxyObj.onCallback(1, true, "hello")

// 打印方法实参数据, 序号 - 值, 与我们传入的相同
===> 方法参数: 0 - 1
===> 方法参数: 1 - true
===> 方法参数: 2 - hello

Proxy#invoke 源码, 就是简单的触发 InvocationHandler#invoke 而已

// java.lang.reflect.Proxy.java
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}

// 直接触发 invocationHandler 方法
// 而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的, 最终传到 $Proxy6 的构造方法
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = proxy.h; // 此处的proxy就是上面动态代理生成 `$Proxy6` 类
return h.invoke(proxy, method, args);
}

收起阅读 »

smali语言之locals和registers的区别

介绍对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。作用声明于方法内部(必须).method public getName()V .registers 6 retu...
继续阅读 »

介绍

对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。

作用

声明于方法内部(必须)

.method public getName()V
.registers 6

return-void
.end method

.registers和locals基本区别

在一个方法(method)中有两中方式指定有多少个可用的寄存器。指令.registers指令指定了在这个方法中有多少个可用的寄存器,

指令.locals指明了在这个方法中非参(non-parameter)寄存器的数量。然而寄存器的总数也包括保存方法参数的寄存器。

参数是如何传递的?

1.如果是非静态方法

例如,你写了一个非静态方法LMyObject;->callMe(II)V。这个方法有2个int参数,但在这两个整型参数前面还有一个隐藏的参数LMyObject;也就是当前对象的引用,所以这个方法总共有3个参数。 假如在一个方法中包含了五个寄存器(V0-V4),如下:

.method public callMe(II)V
const-string v0,"1"
const-string v1,"1"

return-void
.end method

那么只需用.register指令指定5个,或者使用.locals指令指定2个(2个local寄存器+3个参数寄存器)。如下:

.method public callMe(II)V
.registers 5
const-string v0,"1"
const-string v1,"1"
v3==>p0
V4==>P1
V5==>P2

return-void
.end method

或者
.method public callMe(II)V
.locals 2
const-string v0,"1"
const-string v1,"1"
return-void
.end method

该方法被调用的时候,调用方法的对象(即this引用)会保存在V2中,第一个参数在V3中,第二个参数在v4中。

2.如果是静态方法

那么参数少了对象引用,除此之外和非静态原理相同,registers为4 locals依然是2

关于寄存器命名规则

v命名法

上面的例子中我们使用的是v命名法,也就是在本地寄存器后面依次添加参数寄存器,

但是这种命名方式存在一种问题:假如我后期想要修改方法体的内容,涉及到增加或者删除寄存器,由于v命名法需要排序的局限性,那么会造成大量代码的改动,有没有一种办法让我们只改动registers或者locals的值就可以了呢, 答案是:有的

v命名法之外,还有一种命名法叫做p命名法

p命名法

p命名法只能给方法参数命名,不能给本地变量命名

假如有一个非静态方法如下:

.method public print(Ljava/lang/String;Ljava/lang/String;I)V

以下是p命名法参数对应表:

p0this
p1第一个参数Ljava/lang/String;
p2第二个参数Ljava/lang/String;
p3第三个参数I

如前面提到的,long和double类型都是64位,需要2个寄存器。当你引用参数的时候一定要记住,例如:你有一个非静态方法

LMyObject;->MyMethod(IJZ)V

方法的参数为int、long、bool。所以这个方法的所有参数需要5个寄存器。

p0this
p1I
p2, p3J
p4Z

另外当你调用方法后,你必须在寄存器列表,调用指令中指明,两个寄存器保存了double-wide宽度的参数。

注意:在默认的baksmali中,参数寄存器将使用P命名方式,如果出于某种原因你要禁用P命名方式,而要强制使用V命名方式,应当使用-p/--no-parameter-registers选项。

总结

  • locals和registers都可以表示寄存器数量,locals指定本地局部变量寄存器个数,registers是locals和参数寄存器数量的总数,两者使用任选其一
  • 同时,寄存器命名一共分两种,一种是v命名法,另一种是p命名法
v0the first local register
v1the second local register
v2p0the first parameter register
v3p1the second parameter register
v4p2the third parameter register

收起阅读 »

Swift 数组

iOS
Swift 数组Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Elem...
继续阅读 »

Swift 数组

Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。

Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Element是这个数组中唯一允许存在的数据类型。

如果创建一个数组,并赋值给一个变量,则创建的集合就是可以修改的。这意味着在创建数组后,可以通过添加、删除、修改的方式改变数组里的项目。如果将一个数组赋值给常量,数组就不可更改,并且数组的大小和内容都不可以修改。


创建数组

我们可以使用构造语法来创建一个由特定数据类型构成的空数组:

var someArray = [SomeType]()

以下是创建一个初始化大小数组的语法:

var someArray = [SomeType](repeating: InitialValue, count: NumbeOfElements)

以下实例创建了一个类型为 Int ,数量为 3,初始值为 0 的空数组:

var someInts = [Int](repeating: 0, count: 3)

以下实例创建了含有三个元素的数组:

var someInts:[Int] = [10, 20, 30]

访问数组

我们可以根据数组的索引来访问数组的元素,语法如下:

var someVar = someArray[index]

index 索引从 0 开始,即索引 0 对应第一个元素,索引 1 对应第二个元素,以此类推。

我们可以通过以下实例来学习如何创建,初始化,访问数组:

import Cocoa

var someInts = [Int](repeating: 10, count: 3)

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 10
第二个元素的值 10
第三个元素的值 10

修改数组

你可以使用 append() 方法或者赋值运算符 += 在数组末尾添加元素,如下所示,我们初始化一个数组,并向其添加元素:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 40

我们也可以通过索引修改数组元素的值:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

// 修改最后一个元素
someInts
[2] = 50

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 50

遍历数组

我们可以使用for-in循环来遍历所有数组中的数据项:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for item in someStrs {
print(item)
}

以上程序执行输出结果为:

Apple
Amazon
Runoob
Google

如果我们同时需要每个数据项的值和索引值,可以使用 String 的 enumerate() 方法来进行数组遍历。实例如下:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for (index, item) in someStrs.enumerated() {
print("在 index = \(index) 位置上的值为 \(item)")
}

以上程序执行输出结果为:

 index = 0 位置上的值为 Apple
index = 1 位置上的值为 Amazon
index = 2 位置上的值为 Runoob
index = 3 位置上的值为 Google

合并数组

我们可以使用加法操作符(+)来合并两种已存在的相同类型数组。新数组的数据类型会从两个数组的数据类型中推断出来:

import Cocoa

var intsA = [Int](repeating: 2, count:2)
var intsB = [Int](repeating: 1, count:3)

var intsC = intsA + intsB

for item in intsC {
print(item)
}

以上程序执行输出结果为:

2
2
1
1
1

count 属性

我们可以使用 count 属性来计算数组元素个数:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)

var intsC = intsA + intsB

print("intsA 元素个数为 \(intsA.count)")
print("intsB 元素个数为 \(intsB.count)")
print("intsC 元素个数为 \(intsC.count)")

以上程序执行输出结果为:

intsA 元素个数为 2
intsB
元素个数为 3
intsC
元素个数为 5

isEmpty 属性

我们可以通过只读属性 isEmpty 来判断数组是否为空,返回布尔值:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)
var intsC = [Int]()

print("intsA.isEmpty = \(intsA.isEmpty)")
print("intsB.isEmpty = \(intsB.isEmpty)")
print("intsC.isEmpty = \(intsC.isEmpty)")

以上程序执行输出结果为:

intsA.isEmpty = false
intsB
.isEmpty = false
intsC
.isEmpty = true
收起阅读 »

Swift 字符(Character)

iOS
Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。以下实例列出了两个字符实例:import Cocoa let char1: Character = "A" let char2: Character = "B" print("...
继续阅读 »

Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。

以下实例列出了两个字符实例:

import Cocoa

let char1: Character = "A"
let char2: Character = "B"

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

char1 的值为 A
char2
的值为 B

如果你想在 Character(字符) 类型的常量中存储更多的字符,则程序执行会报错,如下所示:

import Cocoa

// Swift 中以下赋值会报错
let char: Character = "AB"

print("Value of char \(char)")

以上程序执行输出结果为:

error: cannot convert value of type 'String' to specified type 'Character'
let char: Character = "AB"

空字符变量

Swift 中不能创建空的 Character(字符) 类型变量或常量:

import Cocoa

// Swift 中以下赋值会报错
let char1: Character = ""
var char2: Character = ""

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

 error: cannot convert value of type 'String' to specified type 'Character'
let char1: Character = ""
^~
error
: cannot convert value of type 'String' to specified type 'Character'
var char2: Character = ""

遍历字符串中的字符

Swift 的 String 类型表示特定序列的 Character(字符) 类型值的集合。 每一个字符值代表一个 Unicode 字符。

Swift 3 中的 String 需要通过 characters 去调用的属性方法,在 Swift 4 中可以通过 String 对象本身直接调用,例如:

Swift 3 中:

import Cocoa

for ch in "Runoob".characters {
print(ch)
}

Swift 4 中:

import Cocoa

for ch in "Runoob" {
print(ch)
}

以上程序执行输出结果为:

R
u
n
o
o
b

字符串连接字符

以下实例演示了使用 String 的 append() 方法来实现字符串连接字符:

import Cocoa

var varA:String = "Hello "
let varB:Character = "G"

varA
.append( varB )

print("varC = \(varA)")

以上程序执行输出结果为:

varC  =  Hello G
收起阅读 »

Swift 字符串

iOS
Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String。创建字符串你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:import Cocoa //...
继续阅读 »

Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String


创建字符串

你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:

import Cocoa

// 使用字符串字面量
var stringA = "Hello, World!"
print( stringA )

// String 实例化
var stringB = String("Hello, World!")
print( stringB )

以上程序执行输出结果为:

Hello, World!
Hello, World!

空字符串

你可以使用空的字符串字面量赋值给变量或初始化一个String类的实例来初始值一个空的字符串。 我们可以使用字符串属性 isEmpty 来判断字符串是否为空:

import Cocoa

// 使用字符串字面量创建空字符串
var stringA = ""

if stringA.isEmpty {
print( "stringA 是空的" )
} else {
print( "stringA 不是空的" )
}

// 实例化 String 类来创建空字符串
let stringB = String()

if stringB.isEmpty {
print( "stringB 是空的" )
} else {
print( "stringB 不是空的" )
}

以上程序执行输出结果为:

stringA 是空的
stringB 是空的

字符串常量

你可以将一个字符串赋值给一个变量或常量,变量是可修改的,常量是不可修改的。

import Cocoa

// stringA 可被修改
var stringA = "菜鸟教程:"
stringA += "http://www.runoob.com"
print( stringA )

// stringB 不能修改
let stringB = String("菜鸟教程:")
stringB += "http://www.runoob.com"
print( stringB )

以上程序执行输出结果会报错,因为 stringB 为常量是不能被修改的:

error: left side of mutating operator isn't mutable: 'stringB' is a 'let' constant
stringB += "http://www.runoob.com"

字符串中插入值

字符串插值是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。 您插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:

import Cocoa

var varA = 20
let constA = 100
var varC:Float = 20.0

var stringA = "\(varA) 乘于 \(constA) 等于 \(varC * 100)"
print( stringA )

以上程序执行输出结果为:

20 乘于 100 等于 2000.0

字符串连接

字符串可以通过 + 号来连接,实例如下:

import Cocoa

let constA = "菜鸟教程:"
let constB = "http://www.runoob.com"

var stringA = constA + constB

print( stringA )

以上程序执行输出结果为:

菜鸟教程:http://www.runoob.com

字符串长度

字符串长度使用 String.count 属性来计算,实例如下:

Swift 3 版本使用的是 String.characters.count

import Cocoa

var varA = "www.runoob.com"

print( "\(varA), 长度为 \(varA.count)" )

以上程序执行输出结果为:

http://www.runoob.com, 长度为 14

字符串比较

你可以使用 == 来比较两个字符串是否相等:

import Cocoa

var varA = "Hello, Swift!"
var varB = "Hello, World!"

if varA == varB {
print( "\(varA) 与 \(varB) 是相等的" )
} else {
print( "\(varA) 与 \(varB) 是不相等的" )
}

以上程序执行输出结果为:

Hello, Swift! 与 Hello, World! 是不相等的

Unicode 字符串

Unicode 是一个国际标准,用于文本的编码,Swift 的 String 类型是基于 Unicode建立的。你可以循环迭代出字符串中 UTF-8 与 UTF-16 的编码,实例如下:

import Cocoa

var unicodeString = "菜鸟教程"

print("UTF-8 编码: ")
for code in unicodeString.utf8 {
print("\(code) ")
}

print("\n")

print("UTF-16 编码: ")
for code in unicodeString.utf16 {
print("\(code) ")
}

以上程序执行输出结果为:

UTF-8 编码: 
232
143
156
233
184
159
230
149
153
231
168
139
UTF-16 编码:
33756
40479
25945
31243

字符串函数及运算符

Swift 支持以下几种字符串函数及运算符:

序号函数/运算符 & 描述
1

isEmpty

判断字符串是否为空,返回布尔值

2

hasPrefix(prefix: String)

检查字符串是否拥有特定前缀

3

hasSuffix(suffix: String)

检查字符串是否拥有特定后缀。

4

Int(String)

转换字符串数字为整型。 实例:

let myString: String = "256"
let myInt: Int? = Int(myString)

5

String.count

Swift 3 版本使用的是 String.characters.count

计算字符串的长度

6

utf8

您可以通过遍历 String 的 utf8 属性来访问它的 UTF-8 编码

7

utf16

您可以通过遍历 String 的 utf8 属性来访问它的 utf16 编码

8

unicodeScalars

您可以通过遍历String值的unicodeScalars属性来访问它的 Unicode 标量编码.

9

+

连接两个字符串,并返回一个新的字符串

10

+=

连接操作符两边的字符串并将新字符串赋值给左边的操作符变量

11

==

判断两个字符串是否相等

12

<

比较两个字符串,对两个字符串的字母逐一比较。

13

!=

比较两个字符串是否不相等。

收起阅读 »

Swift 实战技巧

iOS
Swift实战技巧从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助OC调用方法的处理给OC调用的方法需要添加...
继续阅读 »

Swift实战技巧

从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助

  • OC调用方法的处理

给OC调用的方法需要添加@objc标记,一般的action-target的处理方法,通知的处理方法等需要添加@objc标记

@objc func onRefresh(){
self.refreshCallback?()
}

  • 处理SEL选择子

使用方法型如 #selector(方法名称)
eg.

`#selector(self.onRefresh))`  

更加详细的介绍可以看这篇文章: http://swifter.tips/selector/

下面是使用MJRefresh给mj_headermj_footer添加回调处理函数的例子

self.mj_header.setRefreshingTarget(self, refreshingAction: #selector(self.onRefresh))
self.mj_footer.setRefreshingTarget(self, refreshingAction: #selector(self.onLoadMore))

  • try关键字的使用

可能发生异常的方法使用try?方法进行可选捕获异常

let jsonStr=try?String(contentsOfFile: jsonPath!)

  • 类对象参数和类对象的参数值

AnyClass作为方法的形参,类名称.self(modelClass.self)作为实参

    func registerCellNib(nib:UINib,modelClass:AnyClass){
self.register(nib, forCellReuseIdentifier: String(describing:modelClass.self))
}

...
self.tableView?.registerCellNib(nib: R.nib.gameCell(), modelClass: GameModel.self)

  • 线程间调用

主线程使用DispatchQueue.main,全局的子线程使用DispatchQueue.global(),方法可以使用syncasyncasyncAfter等等

下面是网络请求线程间调用的例子

let _  = URLSession.shared.dataTask(with: url, completionHandler: { [weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
if error == nil {
if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) {
let data = json as! [Any]
DispatchQueue.main.async {
weakSelf.suggestions = data[1] as! [String]
if weakSelf.suggestions.count > 0 {
weakSelf.tableView.reloadData()
weakSelf.tableView.isHidden = false
} else {
weakSelf.tableView.isHidden = true
}
}
}
}
}).resume()

  • 闭包中使用weak防止循环引用的语法
URLSession.shared.dataTask(with: requestURL) {[weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
weakSelf.tableView.reloadData()
}

  • 逃逸闭包和非逃逸闭包
    逃逸闭包,在方法执行完成之后才调用的闭包称为逃逸闭包,一般在方法中做异步处理耗时的任务,任务完成之后把结果使用闭包进行回调处理使用的闭包为逃逸闭包,需要显示的使用@escaping关键字修饰
    非逃逸闭包,在方法执行完成之前调用的闭包称为逃逸闭包,比如snapkit框架使用的闭包是在方法执行完成之后就已经处理完毕了
    Swift3之后闭包默认都是非逃逸(@noescape,不能显示声明),并且这种类型是不能显示使用@noescape关键字修饰的
    // 模拟网络请求,completion闭包是异步延迟处理的,所以需要添加`@escaping`进行修饰
class func fetchVideos(completion: @escaping (([Video]) -> Void)) {
DispatchQueue.global().async {
let video1 = Video.init(title: "What Does Jared Kushner Believe", channelName: "Nerdwriter1")
let video2 = Video.init(title: "Moore's Law Is Ending. So, What's Next", channelName: "Seeker")
let video3 = Video.init(title: "What Bill Gates is afraid of", channelName: "Vox")
var items = [video1, video2, video3]
items.shuffle()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: 3000000000), execute: {
completion(items)
})
}
}

  • Notification.Name的封装处理

swift3中Notification的名字是一种特殊的Notification.Name类型,下面使用enum进行封装处理,并且创建一个NotificationCenter的扩展,处理通知消息的发送

// 定义Notification.Name枚举
enum YTNotification: String {
case scrollMenu
case didSelectMenu
case openPage
case hideBar

var stringValue: String {
return "YT" + rawValue
}

// 枚举成员返回对应的Notification.Name类型
var notificationName: NSNotification.Name {
return Notification.Name.init(stringValue)
}
}

extension NotificationCenter {
func yt_post(custom notification: YTNotification, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil) {
self.post(name: notification.notificationName, object: anObject, userInfo: aUserInfo)
}
}

使用方法
添加通知观察者使用的是YTNotification枚举成员的notificationName返回的Notification.Name类型的值
发送消息使用的是YTNotification枚举成员

// 添加通知观察
NotificationCenter.default.addObserver(self, selector: #selector(self.changeTitle(notification:)), name: YTNotification.scrollMenu.notificationName, object: nil)

// 发送消息
NotificationCenter.default.yt_post(custom: YTNotification.scrollMenu, object: nil, userInfo: ["length": scrollIndex])

lazy惰性加载属性,只有在使用的时候才初始化变量

    // 闭包的方式
let menuTitles = ["History", "My Videos", "Notifications", "Watch Later"]
lazy var menuItems : [MenuItem] = {
var tmenuItems = [MenuItem]()
for menuTitle in menuTitles {
let menuItem = MenuItem(iconImage: UIImage.init(named: menuTitle)!, title: menuTitle)
tmenuItems.append(menuItem)
}
return tmenuItems
}()

// 普通方式,
lazy var titles = ["A", "B"]

  • 类型判断

使用is判断类型以及使用if-let和as?判断类型

// MARK:- 类型检查例子
let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]

var chemCount = 0
var mathsCount = 0
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if item is Chemistry {
chemCount += 1
} else if item is Maths {
mathsCount += 1
}
}

// 使用if-let和as?判断类型
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if let _ = item as? Chemistry {
chemCount += 1
} else if let _ = item as? Maths {
mathsCount += 1
}
}

使用switch-case和as判断类型

// Any可以表示任何类型,包括方法类型
var exampleany = [Any]()
exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

// 使用switch-case和as判断类型
for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

  • Swift使用KVC,执行KVC操作的变量需要添加@objc标记
class Feed: NSObject, HandyJSON  {
// 使用KVC添加@objc关键字
@objc var id = 0
var type = ""
var payload: PayLoad?
var user: PostUser?

required override init() {}
}

  • swift中CGRect类型的操作

swift中简化了CGRect类型的操作,比如有一个CGRect的类型实例为frame,以下例举了OC中对应的在swift下的语法

OCSwift
CGRectGetMaxX(frame)frame.maxX
CGRectGetMinY(frame)frame.minY
CGRectGetMidX(frame)frame.midX
CGRectGetWidth(frame)frame.width
CGRectGetHeight(frame)frame.height
CGRectContainsPoint(frame, point)frame.contains(point)
  • Swift中指针的处理

详细的介绍可以查看这篇文章:http://swifter.tips/unsafe/
下面是一个使用OC库RegexKitLite中的一个例子,block中返回值是指针类型的,需要转换为对应的swift对象类型

func composeAttrStr(text: String) -> NSAttributedString {
// 表情的规则
let emotionPattern = "\\[[0-9a-zA-Z\\u4e00-\\u9fa5]+\\]";
// @的规则
let atPattern = "@[0-9a-zA-Z\\u4e00-\\u9fa5-_]+";
// #话题#的规则
let topicPattern = "#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";
// url链接的规则
let urlPattern = "\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^[:punct:]\\s]|/)))";
let pattern = "\(emotionPattern)|\(atPattern)|\(topicPattern)|\(urlPattern)"

var textParts = [TextPart]()

(text as! NSString).enumerateStringsMatched(byRegex: pattern) { (captureCount: Int, capString: UnsafePointer<NSString?>?, capRange: UnsafePointer<NSRange>?, stop: UnsafeMutablePointer<ObjCBool>?) in
let captureString = capString?.pointee as! String
let captureRange = capRange?.pointee as! NSRange

let part = TextPart()
part.text = captureString
part.isSpecial = true
part.range = captureRange
textParts.append(part)
}
// ...
}

  • 只有类才能实现的protocol 有一种场景,protocol作为delegate,需要使用weak关键字修饰的时候,需要指定delegate的类型为ptotocol型,这个ptotocol需要添加class修饰符,比如下面的这个protocol,因为类类型的对象才有引用计数,才有weak的概念,没有引用计数的struct型是没有weak概念的
/// ImageViewer和ImageCell交互使用的协议
protocol YTImageCellProtocol : class {
// Cell的点击事件,处理dismiss
func imageCell(_ imageCell: YTImageCell, didSingleTap: Bool);
}
收起阅读 »

手摸手教你用webpack搭建TS开发环境

前言 最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue...
继续阅读 »

前言


最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue3内部对TS的webpack进行了怎样的配置,废话不多说进入正题。


Node 编译TS


先讲讲如何运行ts文件吧,最传统的方式当然是直接输入命令



tsc xxx.ts



当然你必须得先安装过ts,如果没有请执行以下命令



npm install typescript -g



安装后查看下版本



tsc --version



这样我们就能得到编译后的js文件了,然后我们可以通过node指令



node xxx.js



进行查看,当然也可以新建一个HTML页面引入编译后的js文件


我们从上可以发现有点小复杂,那可不可以直接通过Node直接编译TS呢?接来下就是介绍这种方法

使用ts-node 就可以得到我们想要的效果

安装



npm install ts-node -g



另外ts-node需要依赖 tslib 和 @types/node 两个包,也需要下载



npm install tslib @types/node -g



现在,我们可以直接通过 ts-node 来运行TypeScript的代码



ts-node xxx.ts



如果遇到很多ts文件,那我们用这种方法也会觉得繁琐,所以我们最好是用webpack搭建一个支持TS开发环境,这样才是最好的解决方案。


webpack搭建准备工作


先新建一个文件夹

下载 webpack webpack-cli



npm install webpack webpack-cli -D



下载 ts tsloader(编译ts文件)



npm install typescript ts-loader -D



下载 webpack-dev-server(搭建本地服务器)



npm install webpack-dev-server -D



下载 html模板插件



npm install html-webpack-plugin -D



初始化webpack



npm init



初始化ts



tsc --init



新建配置文件 webpack.config.js


初始化后文件结构如下图所示,当然还有一些测试ts和html需要自己手动创建下
image.png


webpack 配置


配置之前我们先去package.json中添加两个运行和打包指令


image.png


webpack.config.js


代码中有详细说明哦


const path = require('path')//引入内置path方便得到绝对路径
const HtmlWebpackPlugin = require('html-webpack-plugin')//引入模板组件


module.exports = {
mode: 'development',//开发模式
entry: './src/main.ts',//入口文件地址
output: {
path: path.resolve(__dirname, "./dist"),//出口文件,即打包后的文件存放地址
filename: 'bundle.js' //文件名
},
devServer: {

},
resolve: {
extensions:['.ts', '.js', '.cjs', '.json'] //配置文件引入时省略后缀名
},
module: {
rules: [
{
test: /\.ts$/, //匹配规则 以ts结尾的文件
loader: 'ts-loader' //对应文件采用ts-loader进行编译
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html' //使用模板地址
})
]
}

配置完成我们可以进行测试了,执行指令



npm run serve



打包指令



npm run build



End


看完的话点个赞吧~~


QQ图片20200210181218.jpg



收起阅读 »

用 JS 写算法时你应该知道的——数组不能当队列使用!!

在初学 JS 时,发现数组拥有 shift()、unshift()、pop()、push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。 举个例子 - BFS 一般队...
继续阅读 »

在初学 JS 时,发现数组拥有 shift()unshift()pop()push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。


举个例子 - BFS


一般队列的应用是在 BFS 题目中使用到。BFS(Breath First Search)广度优先搜索,作为入门算法,基本原理大家应该都了解,这里不再细说。


LeetCode 1765. 地图中的最高点



给你一个大小为 m x n 的整数矩阵 isWater ,它代表了一个由 陆地 和 水域 单元格组成的地图。


如果 isWater[i][j] == 0 ,格子 (i, j) 是一个 陆地 格子。
如果 isWater[i][j] == 1 ,格子 (i, j) 是一个 水域 格子。
你需要按照如下规则给每个单元格安排高度:



  • 每个格子的高度都必须是非负的。

  • 如果一个格子是是 水域 ,那么它的高度必须为 0 。

  • 任意相邻的格子高度差 至多 为 1 。当两个格子在正东、南、西、北方向上相互紧挨着,就称它们为相邻的格子。(也就是说它们有一条公共边)


找到一种安排高度的方案,使得矩阵中的最高高度值 最大 。


请你返回一个大小为 m x n 的整数矩阵 height ,其中 height[i][j] 是格子 (i, j) 的高度。如果有多种解法,请返回 任意一个 。



常规 BFS 题目,从所有的水域出发进行遍历,找到每个点离水域的最近距离即可。常规写法,三分钟搞定。


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q.shift();
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
q.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
}
return height;
};

然后,超时了……


调整一下,


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
let tmp = [];
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q[i];
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
tmp.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
q = tmp;
}
return height;
};

ok,这回过了,而且打败了 90% 的用户。


image.png


那么问题出在哪里呢?shift()!!!


探究 JavaScript 中 shift() 的实现


在学习 C++ 的时候,队列作为一个先入先出的数据结构,入队和出队肯定都是O(1)的时间复杂度,用链表


让我们查看下 V8 中 shift() 的源码


简单实现就是


function shift(arr) {
let len = arr.length;
if (len === 0) {
return;
}
let first = arr[0];
for (let i = 0; i < len - 1; i++) {
arr[i] = arr[i + 1];
}
arr.length = len - 1;
return first;
}

所以,shift()O(N) 的!!! 吐血 QAQ


同理,unshift() 也是 O(N) 的,不过,pop()push()O(1),也就是说把数组当做栈是没有问题的。


我就是想用队列怎么办!


没想到作为一个 JSer,想好好地用个队列都这么难……QAQ


找到了一个队列实现,详情见注释。


/*

Queue.js

A function to represent a queue

Created by Kate Morley - http://code.iamkate.com/ - and released under the terms
of the CC0 1.0 Universal legal code:

http://creativecommons.org/publicdomain/zero/1.0/legalcode

*/

/* Creates a new queue. A queue is a first-in-first-out (FIFO) data structure -
* items are added to the end of the queue and removed from the front.
*/
function Queue(){

// initialise the queue and offset
var queue = [];
var offset = 0;

// Returns the length of the queue.
this.getLength = function(){
return (queue.length - offset);
}

// Returns true if the queue is empty, and false otherwise.
this.isEmpty = function(){
return (queue.length == 0);
}

/* Enqueues the specified item. The parameter is:
*
* item - the item to enqueue
*/
this.enqueue = function(item){
queue.push(item);
}

/* Dequeues an item and returns it. If the queue is empty, the value
* 'undefined' is returned.
*/
this.dequeue = function(){

// if the queue is empty, return immediately
if (queue.length == 0) return undefined;

// store the item at the front of the queue
var item = queue[offset];

// increment the offset and remove the free space if necessary
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}

// return the dequeued item
return item;

}

/* Returns the item at the front of the queue (without dequeuing it). If the
* queue is empty then undefined is returned.
*/
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}

}

把最初代码中的数组改为 Queue,现在终于可以通过了。:)



收起阅读 »

如何“优雅”地修改 node_modules 下的代码?

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择: 方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。 方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最...
继续阅读 »

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择:


方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。


方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最好祈祷作者发版积极,并且新版本向下兼容。


方法三:把 A 包的源码拖出来自己维护:有点暴力且事后维护成本较高,不过应急时也能勉强接受。


等等,可如果出问题的包是“幽灵依赖”呢,比如项目的依赖链是: A -> B -> C,此时 C 包有 bug。那么上面三个方法的改动需要同时影响到 A、B、C 三个包,修复周期可能就更长了,可是你今晚就要上线啊,这可怎么办?


1


上线要紧,直接手动修改 node_modules 下的代码给缺陷包打个临时补丁吧,可问题又来了,改动只能在本地生效,构建却在云端, 积极的同学开始写起了脚本,然后陷入一个个坑里...


上述场景下即可考虑使用 patch-package 这个包,假设我们现在的源码结构如下所示:


├── node_modules  
│ └── lodash
│ └── toString.js
├── src
│ └── app.js // 依赖 lodash 的 toString 方法
└── package.json

node_modules/lodash/toString.js


var baseToString = require('./_baseToString')

function toString(value) {
return value == null ? '' : baseToString(value);
}

module.exports = toString;

src/app.js


const toString = require('lodash/toString')
console.log(toString(123));

假设现在需要修改 node_modules/lodash/toString.js 文件,只需要遵循以下几步即可“优雅”完成修改:


第一步:安装依赖


yarn add patch-package postinstall-postinstall -D

第二步:修改 node_modules/lodash/toString.js 文件


function toString(value) {
console.log('it works!!!'); // 这里插入一行代码
return value == null ? '' : baseToString(value);
}

module.exports = toString;

第三步:生成修改文件


npx patch-package lodash

这一步运行后会生成 patches/lodash+4.17.21.patch,目录结构变成下面这样:


├── node_modules  
│ └── lodash
│ └── toString.js
├── patches
│ └── lodash+4.17.21.patch
├── src
│ └── app.js
└── package.json

其中 .patch 文件内容如下:


diff --git a/node_modules/lodash/toString.js b/node_modules/lodash/toString.js
index daaf681..8308e76 100644
--- a/node_modules/lodash/toString.js
+++ b/node_modules/lodash/toString.js
@@ -22,6 +22,7 @@ var baseToString = require('./_baseToString');
* // => '1,2,3'
*/
function toString(value) {
+ console.log('it works!!!');
return value == null ? '' : baseToString(value);
}

第四步:修改 package.json 文件


"scripts": {
+ "postinstall": "patch-package"
}

最后重装一下依赖,测试最终效果:


rm -rf node_modules
yarn
node ./src/app.js

// it works!!!
// 123

可以看到,即便重装依赖,我们对 node_modules 下代码的修改还是被 patch-package 还原并最终生效。


至此我们便完成一次临时打补丁的操作,不过这并非真正优雅的长久之计,长期看还是需要彻底修复第三方包缺陷并逐步移除项目中的 .patch 文件。


作者:王力国
链接:https://juejin.cn/post/7022252841116893215

收起阅读 »

封装一个底部导航

前言 在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。 底部导航 BottomNav组件属性 1. value选中值(即选中BottomNavPane的name值)值为字符串类型非必填默认为第一个BottomNavP...
继续阅读 »

前言


在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。

底部导航


BottomNav组件属性


1. value
选中值(即选中BottomNavPane的name值)
值为字符串类型
非必填默认为第一个BottomNavPane的name

2. lazy
未显示的内容面板是否延迟渲染
值为布尔类型
默认为false

样式要求
组件外面需要包裹可以相对定位的元素,增加样式:position: relative

BottomNavPane组件属性


1. name
英文名称
值为字符串类型
必填

2. icon
导航图标名称
值为字符串类型
值需要与src/assets/icon目录下svg文件的名称一致(name值不含“.svg”后缀)
必填

3. label
导航图标下面显示的文字
值为字符串类型
必填

4. scroll
是否有滚动条
值为布尔类型
默认值为:true

示例


<template>
<div class="bottom-nav-wrap">
<BottomNav v-model="curNav" :lazy="true">
<BottomNavPane name="home" label="首页" icon="home">
<h1>首页内容</h1>
</BottomNavPane>
<BottomNavPane name="oa" label="办公" icon="logo">
<h1>办公内容</h1>
</BottomNavPane>
<BottomNavPane name="page2" label="我的" icon="user">
<h1>个人中心</h1>
</BottomNavPane>
</BottomNav>
</div>
</template>

<script>
import { BottomNav, BottomNavPane } from '@/components/m/bottomNav'

export default {
name: 'BottomNavDemo',
components: {
BottomNav,
BottomNavPane
},
data () {
return {
curNav: ''
}
}
}
</script>

<style lang="scss" scoped>
.bottom-nav-wrap {
position: absolute;
top: $app-title-bar-height;
bottom: 0;
left: 0;
right: 0;
}
</style>

BottomNav.vue


<template>
<div class="bottom-nav">
<div class="nav-pane-wrap">
<slot></slot>
</div>
<div class="nav-list">
<div class="nav-item"
v-for="info in navInfos"
:key="info.name"
:class="{active: info.name === curValue}"
@click="handleClickNav(info.name)">
<Icon class="nav-icon" :name="info.icon"></Icon>
<span class="nav-label">{{info.label}}</span>
</div>
</div>
</div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
name: 'BottomNav',
props: {
// 选中导航值(导航的英文名)
value: String,
// 未显示的内容面板是否延迟渲染
lazy: {
type: Boolean,
default: false
}
},
data () {
return {
// 组件实例的唯一ID
id: generateUUID(),
// 当前选中的导航值(导航的英文名)
curValue: this.value,
// 导航信息数组
navInfos: [],
// 导航面板vue实例数组
panes: []
}
},
watch: {
value (val) {
this.curValue = val
},
curValue (val) {
this.$eventBus.$emit('CHANGE_NAV' + this.id, val)
this.$emit('cahnge', val)
}
},
mounted () {
this.calcPaneInstances()
},
beforeDestroy () {
this.$eventBus.$off('CHANGE_NAV' + this.id)
},
methods: {
// 计算导航面板实例信息
calcPaneInstances () {
if (this.$slots.default) {
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'BottomNavPane')
const panes = paneSlots.map(({ componentInstance }) => componentInstance)
const navInfos = paneSlots.map(({ componentInstance }) => {
// console.log(componentInstance.name, componentInstance)
return {
name: componentInstance.name,
label: componentInstance.label,
icon: componentInstance.icon
}
})
this.navInfos = navInfos
this.panes = panes
if (!this.curValue) {
if (navInfos.length > 0) {
this.curValue = navInfos[0].name
}
} else {
this.$eventBus.$emit('CHANGE_NAV' + this.id, this.curValue)
}
}
},
// 导航点击事件处理方法
handleClickNav (val) {
this.curValue = val
}
}
}
</script>
<style lang="scss" scoped>
.bottom-nav {
display: flex;
flex-direction: column;
height: 100%;
.nav-pane-wrap {
flex: 1;
}
.nav-list {
flex: none;
display: flex;
height: 90px;
background-color: #FFF;
align-items: center;
border-top: 1px solid $base-border-color;
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
text-align: center;
color: #666;
.nav-icon {
font-size: 40px;/*yes*/
}
.nav-label {
margin-top: 6px;
font-size: 24px;/*yes*/
}
&.active {
position: relative;
color: $base-color;
}
}
}
}
</style>

BottomNavPane.vue


<template>
<div v-if="canInit" class="bottom-nav-pane" v-show="show">
<Scroll v-if="scroll">
<slot></slot>
</Scroll>
<slot v-else></slot>
</div>
</template>
<script>
import Scroll from '@/components/base/scroll'

export default {
name: 'BottomNavPane',
components: {
Scroll
},
props: {
// 页签英文名称
name: {
type: String,
required: true
},
// 页签显示的标签
label: {
type: String,
required: true
},
// 图标名称
icon: {
type: String,
required: true
},
// 是否有滚动条
scroll: {
type: Boolean,
default: true
}
},
data () {
return {
// 是否显示
show: false,
// 是否已经显示过
hasShowed: false
}
},
computed: {
canInit () {
return (!this.$parent.lazy) || (this.$parent.lazy && this.hasShowed)
}
},
created () {
this.$eventBus.$on('CHANGE_NAV' + this.$parent.id, val => {
if (val === this.name) {
this.show = true
this.hasShowed = true
} else {
this.show = false
}
})
}
}
</script>
<style lang="scss" scoped>
.bottom-nav-pane {
height: 100%;
position: relative;
}
</style>

/**
* 底部图标导航组件
*/
import BaseBottomNav from './BottomNav.vue'
import BaseBottomNavPane from './BottomNavPane.vue'
export const BottomNav = BaseBottomNav
export const BottomNavPane = BaseBottomNavPane


「欢迎在评论区讨论」



收起阅读 »

如何优雅的集成Google pay到你的项目中

官方集成文档 官方集成文档 官方集成文档第一步:javadependencies { def billing_version = "3.0.0" implementation 'com.android.billingcli...
继续阅读 »

官方集成文档 官方集成文档 官方集成文档

第一步:

java

dependencies {
def billing_version = "3.0.0"

implementation 'com.android.billingclient:billing:$billing_version'
}

kotlin

dependencies {
def billing_version = "3.0.0"

implementation 'com.android.billingclient:billing-ktx:$billing_version'
}

第二部:

private PurchasesUpdatedListener purchaseUpdateListener = new PurchasesUpdatedListener() {
@Override
void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
// To be implemented in a later section.
}
};

private BillingClient billingClient = BillingClient.newBuilder(activity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();

第三部:

billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
//链接成功
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
// 链接失败触发,触发重连机制
}
});

第四部: 请求自己服务器,拿到对应的商品列表,这里拿到的商品列表要和Google后台配置的商品列表ID一致。

List<String> skuList = new ArrayList<> ();
skuList.add("premium_upgrade");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
// Process the result.
}
});

第五步: 调起Google 支付界面

// An activity reference from which the billing flow will be launched.
Activity activity = ...;

// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();

// Handle the result.

在这里插入图片描述在这里插入图片描述

此处说明你的支付已经完成,回调到你最开始初始化的onPurchasesUpdated方法里边。

if (billingResult.getResponseCode() == BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}

第六步: Google 支付分为两个部分,购买和验证,比如我们的产品是虚拟货币,是一次性消耗产品,使用下边方法去验证消耗。 最好的处理方法是请求后台服务器,让后台做一个验证,然后我们再去验证消费(保证安全性)

void handlePurchase(Purchase purchase) {
// Purchase retrieved from BillingClient#queryPurchases or your PurchasesUpdatedListener.
Purchase purchase = ...;

// Verify the purchase.
// Ensure entitlement was not already granted for this purchaseToken.
// Grant entitlement to the user.

ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();

ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// Handle the success of the consume operation.
}
}
};

billingClient.consumeAsync(consumeParams, listener);
}

因为网络原因可能会出现,掉单的问题,所以就会出现补单的逻辑。

调起支付之前

BillingResult billingResult = billingClient.launchBillingFlow(this, billingFlowParams);
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
queryHistory();
}
/**
* 查询历史记录,有没校验的开始支付验证流程
*/
public void queryHistory() {
if (billingClient == null) {
return;
}
//google消费失败的补单
List<Purchase> purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP).getPurchasesList();
if (purchases != null && !purchases.isEmpty()) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
return;
}
}
}

收起阅读 »

android 如何优雅的集成 Razorpay

请在您的应用build.gradle文件中添加以下依赖项:repositories { mavenCentral() } dependencies { implementation 'com.razorpay:checkout...
继续阅读 »
  1. 请在您的应用build.gradle文件中添加以下依赖项:
repositories {   
mavenCentral()
}
dependencies {
implementation 'com.razorpay:checkout:1.5.16'
}

在这里插入图片描述

  1. Checkout并将付款详细信息和选项作为传递JSONObject。确保您添加了order_id在步骤1中生成的(一般是后台生成)
 public void startPayment() {
/*
You need to pass current activity in order to let Razorpay create CheckoutActivity
*/
final Activity activity = this;
final Checkout co = new Checkout();

try {
JSONObject options = new JSONObject();
options.put("name", "Razorpay Corp");
options.put("description", "Demoing Charges");
//You can omit the image option to fetch the image from dashboard
options.put("image", "https://s3.amazonaws.com/rzp-mobile/images/rzp.png");
options.put("order_id", "order_DBJOWzybf0sJbb");//这一部很重要,是后台调用Razorpay的接口生成的,否则支付成功的状态不对

options.put("currency", "INR");
options.put("amount", "100");
options.put("payment_capture", "1");

JSONObject preFill = new JSONObject();
preFill.put("email", "test@razorpay.com");
preFill.put("contact", "9876543210");

options.put("prefill", preFill);

co.open(activity, options);
} catch (Exception e) {
Toast.makeText(activity, "Error in payment: " + e.getMessage(), Toast.LENGTH_SHORT)
.show();
e.printStackTrace();
}
}
  1. 回调处理支付状态,处理成功和错误事件
  public class PaymentActivity extends Activity implements PaymentResultListener{
@Override
public void onPaymentSuccess(String s, PaymentData paymentData) {
}

@Override
public void onPaymentError(int i, String s, PaymentData paymentData) {

}
}
  1. 混淆
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}

-keepattributes JavascriptInterface
-keepattributes *Annotation*

-dontwarn com.razorpay.**
-keep class com.razorpay.** {*;}

-optimizations !method/inlining/*

-keepclasseswithmembers class * {
public void onPayment*(...);
}

收起阅读 »

Android-关于设备唯一ID的奇技淫巧

前言最近在二开项目国际版客户的功能,我们项目中默认是有一个游客登录的,一般大家都是取Android设备的唯一ID上传服务器,然后服务器给你分配一个用户信息.但是Google在高版本对于设备唯一Id的获取简直限制到了极点.以前我都是直接获取IMEI来作为设备的唯...
继续阅读 »

前言

最近在二开项目国际版客户的功能,我们项目中默认是有一个游客登录的,一般大家都是取Android设备的唯一ID上传服务器,然后服务器给你分配一个用户信息.但是Google在高版本对于设备唯一Id的获取简直限制到了极点.

以前我都是直接获取IMEI来作为设备的唯一标识

var imei: String = ""
val tm: TelephonyManager =
context.getSystemService(Service.TELEPHONY_SERVICE) as TelephonyManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
imei = tm.imei
} else {
imei = tm.deviceId
}
Log.e("TAG","$imei")

imei和deviceId都有一个重载函数,主要是区别双卡的一个情况

image.png

Android6.0以后我们加一个动态权限即可,但是用户只要拒绝就没办法获取了,不过一般来说我们会有个弹框来引导用户同意

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

Android 10.0 谷歌再一次收紧权限

image.png

<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
如果你把他放到AndroidManifest会报错

image.png

官方也说了,你要是弟弟(9.0 以下)我给你报null,你要是10.0 还敢用我就直接抛异常. 后面在stackoverflow上面找到了一个办法

public class DeviceUuidFactory {
protected static final String PREFS_FILE = "device_id.xml";
protected static final String PREFS_DEVICE_ID = "device_id";
protected static UUID uuid;

public DeviceUuidFactory(Context context) {
if( uuid ==null ) {
synchronized (DeviceUuidFactory.class) {
if( uuid == null) {
final SharedPreferences prefs = context.getSharedPreferences( PREFS_FILE, 0);
final String id = prefs.getString(PREFS_DEVICE_ID, null );
if (id != null) {
// Use the ids previously computed and stored in the prefs file
uuid = UUID.fromString(id);
} else {
final String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
// Use the Android ID unless it's broken, in which case fallback on deviceId,
// unless it's not available, then fallback on a random number which we store
// to a prefs file
try {
if () {
uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
} else {
@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE )).getDeviceId();
uuid = deviceId!=null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")) : UUID.randomUUID();
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
// Write the value out to the prefs file
prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString() ).commit();
}
}
}
}
}
/**
* Returns a unique UUID for the current android device. As with all UUIDs, this unique ID is "very highly likely"
* to be unique across all Android devices. Much more so than ANDROID_ID is.
*
* The UUID is generated by using ANDROID_ID as the base key if appropriate, falling back on
* TelephonyManager.getDeviceID() if ANDROID_ID is known to be incorrect, and finally falling back
* on a random UUID that's persisted to SharedPreferences if getDeviceID() does not return a
* usable value.
*
* In some rare circumstances, this ID may change. In particular, if the device is factory reset a new device ID
* may be generated. In addition, if a user upgrades their phone from certain buggy implementations of Android 2.2
* to a newer, non-buggy version of Android, the device ID may change. Or, if a user uninstalls your app on
* a device that has neither a proper Android ID nor a Device ID, this ID may change on reinstallation.
*
* Note that if the code falls back on using TelephonyManager.getDeviceId(), the resulting ID will NOT
* change after a factory reset. Something to be aware of.
*
* Works around a bug in Android 2.2 for many devices when using ANDROID_ID directly.
*

*
* @return a UUID that may be used to uniquely identify your device for most purposes.
*/
public String getDeviceUuid() {
return uuid.toString();
}
}

这个类的意思是,首先他会去SharedPreferences查询有没有,没有的话再去查询ANDROID_ID,后面判断了是否是9774d56d682e549c,因为有的厂商手机好多ANDROID_ID都是这个,所以判断一下,防止好几万个人用一个账号,不然那就笑嘻嘻了,后面如果真等于9774d56d682e549c了,就通过下面的

@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE )).getDeviceId();

来获取DeviceId,但是这个AndroidId虽然可以是获取了,但是会受限于签名文件,如果在相同设备上运行但是应用签名不一样,获取到的ANDROID_ID就会不一样,比如谷歌商店会二次签名apk,他获取的id可能就是159951,后面我们要测试时,上传到内部测试的包好像会再次签名,这次获取的可能是951159,然后我们用android提供的签名文件可能就是147258,我们自己新建一个签名文件就可能是258369,总之这个ANDROID_ID会受制于签名文件

反正最后我们国际版用到了Mob的推送服务,推送中有一个只推送单个设备,然后我们就设想,直接用Mob的唯一设备Id和我们服务器绑定如何,后面一经测试,效果很好,直接跳过大堆测试和寻找时间

//阿里云唯一设备id
val deviceId = PushServiceFactory.getCloudPushService().deviceId

//Mob
CloudPushService pushService = PushServiceFactory.getCloudPushService();
pushService.register(applicationContext, new CommonCallback() {
@Override
public void onSuccess(String response) {
Log.e("TAG", "onSuccess: "+response);
}

@Override
public void onFailed(String errorCode, String errorMessage) {
}
});

//友盟唯一设备ID
val pushAgent = PushAgent.getInstance(context)
pushAgent.register(object : UPushRegisterCallback {
override fun onSuccess(deviceToken: String) {
//注册成功会返回deviceToken deviceToken是推送消息的唯一标志
Log.i(TAG, "注册成功:deviceToken:--> $deviceToken")
}

override fun onFailure(errCode: String, errDesc: String) {
Log.e(TAG, "注册失败:--> code:$errCode, desc:$errDesc")
}
})

这是常用的第三方服务获取唯一设备ID的方法,其实有的人可能用的跟我不一样,基本上文档里面都有,真找不到可以去问问客服

终于解决一个让人头疼的问题了,下班,回家

收起阅读 »

一条SQL查询语句是如何执行的

sql
背景我们执行一条查询语句时,对客户端是一个很简单的过程,但对服务端(MySQL)内部却涉及到很复杂的组件和逻辑,当出现一些比较复杂的SQL问题时,如果不理解其内部执行的原理,将会很难去定位和解决问题正文先聊聊MySQL的逻辑架构大体来说,MySQL可以分为 S...
继续阅读 »

背景

我们执行一条查询语句时,对客户端是一个很简单的过程,但对服务端(MySQL)内部却涉及到很复杂的组件和逻辑,当出现一些比较复杂的SQL问题时,如果不理解其内部执行的原理,将会很难去定位和解决问题

正文

先聊聊MySQL的逻辑架构

image.png

大体来说,MySQL可以分为 Server层和存储引擎层两部分

Server层

  • 包括连接器、查询缓存、分析器、优化器、执行器
  • 实现了MySQL 的大多数核心服务功能,所有的包括查询解析、分析、优化、缓存以及所有的内置函数,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图

存储引擎层

  • 负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。也就是说,你执行 create table 建表的时候,如果不指定引擎类型,默认使用的就是 InnoDB。不过,你也可以通过指定存储引擎的类型来选择别的引擎,比如在 create table 语句中使用 engine=memory, 来指定使用内存引擎创建表。
  • 不同存储引擎的表数据存取方式不同,支持的功能也不同,不同的存储引擎共用一个 Server 层
  • Server 层通过存储引擎API来与它们交互,这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询尽可能的透明。这些API包含几十个底层函数,用于执行诸如"开始一个事务"或者"根据主键提取一行记录"等操作,存储引擎不能解析SQL,互相之间也不能通信。只是简单地响应上层服务器的请求

SQL查询语句的执行流程

step1:使用连接器与客户端建立连接

首先客户端会先连接到指定数据库上,这时候接待的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接

我们在Linux上会通过以下方式与MySQL建立连接,连接命令中的"mysql"是客户端工具

mysql -h$ip -P$port -u$user -p

在完成经典的TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码:

  • 如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行
  • 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置

连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,可以输入show processlist 命令看到全部的连接,Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接

企业微信截图_16348234821060.png

客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query,这时候如果你要继续,就需要重连,然后再执行请求了

数据库的连接分为有长连接和短连接:

  • 长连接:指连接成功后,如果客户端持续有请求,则一直使用同一个连接
  • 短连接:指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个

建立连接的过程通常是比较复杂的,因此建议在使用中要尽量减少建立连接的动作,也就是尽量使用长连接

但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了

怎么解决这个问题呢?你可以考虑以下两种方案:

  • 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连
  • 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态

step2:查询缓存,有就直接返回查询结果

连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存

MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端

如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高

但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存

好在 MySQL 也提供了这种“按需使用”的方式。你可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:

select SQL_CACHE * from T where ID=10;

注意:MySQL 8.0 版本直接将查询缓存的整块功能删掉了

step3:使用分析器解析你的SQL,知道你要做什么

分析器如果没有命中查询缓存,就要开始真正执行语句了。MySQL首先需要知道你要做什么,因此需要对 SQL 语句做解析

分析器先会做“词法分析”:你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”

做完了这些识别以后,就要做“语法分析”:根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句 select 少打了开头的字母“s”

mysql> elect * from t where ID=1;

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1,

一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容

step4:使用优化器确定语句的执行方案

经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序

比如执行下面这样的语句,这个语句是执行两个表的 join:

select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
  • 既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。
  • 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。

这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了

step5:使用执行器执行语句

MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句

开始执行的时候,要先判断你对这个表 T 有没有执行查询的权限

  • 如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
mysql> elect * from t where ID=1; 

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1
  • 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口

比如我们这个例子中的表 T 中,ID 字段没有索引,那么执行器的执行流程是这样的:

  1. 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
  2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
  3. 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

对于有索引的表,执行的逻辑也差不多:

  1. 第一次调用的是“取满足条件的第一行”这个接口,
  2. 之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。

你会在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。但在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的

总结

  • MySQL的逻辑架构包含两部分,server层存储引擎层

  • SQL查询语句的执行依赖于这些核心组件:先通过连接器与客户端进行连接,随后查询是否可以应用缓存,可以直接返回结果,不可以则使用解析器分析SQL,然后利用优化器确定执行方案,最终利用执行器存储引擎执行SQL获取结果


收起阅读 »

熬夜再战Android之修炼Kotlin-【findView】篇

前提 前面我们学了Kotlin语言,趁热打铁我们试试Kotlin在Android中的应用。 如果是新手,请先学完Android基础。 推荐先看小空之前写的熬夜Android系列,再来尝试。 👉实践过程 😜方式一 使用扩展,如果你第一次创建项目的时候选择的是Ko...
继续阅读 »

前提


前面我们学了Kotlin语言,趁热打铁我们试试Kotlin在Android中的应用。


如果是新手,请先学完Android基础。


推荐先看小空之前写的熬夜Android系列,再来尝试。


👉实践过程


😜方式一


使用扩展,如果你第一次创建项目的时候选择的是Kotlin语言,则默认带有该插件,如果选择的默认是Java语言,则需要手动添加。


该方式的优点就是对编程人员来说可以直接拿到View的id,不需要定义变量和findViewById。


在项目的build文件开头添加【app->build.gradle】


apply plugin: 'kotlin-android-extensions'


image.png
之后在Activity中添加import,固定格式的。


import kotlinx.android.synthetic.main.修改为你的布局名称.*


【*】代表的是该布局下的所有控件,如果只需要指定控件,将【*】改为控件名即可,如下示例


import kotlinx.android.synthetic.main.activity_main.*


import kotlinx.android.synthetic.main.activity_main.mytextview


之后就可以直接在代码中使用控件的id来进行响应操作了。


<TextView
        android:id="@+id/myText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:text="芝麻粒儿和空名先生"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

但是方便的同时,问题的隐患也存在着,在底层仍然回归原始使用的是findViewById,所以会对性能有影响


😜方式二


使用findViewById,这个仍然有两种方式,方式一是【lateinit】关键字,但是存在坑,详情看这(在 Kotlin 代码中慎用 lateinit 属性zhuanlan.zhihu.com/p/31297995
推荐方式二使用【lazy】,如下:



private val myText: TextView by lazy { findViewById<TextView>(R.id.myText) }
private val myImg: ImageView by lazy { findViewById<ImageView>(R.id.myImg) }
private val imageView: ImageView by lazy { findViewById<ImageView>(R.id.imageView) }
private val myBtn: Button by lazy { findViewById<Button>(R.id.myBtn) }

上面是在Activity中的使用,而在Fragment中又怎么用呢?


class LoginFragment : Fragment() {
  private var myText: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //设置布局
        return inflater.inflate(R.layout.login_fragment, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
   myText = view.findViewById(R.id.myText)
        myText?.setText("动态修改文本")
    }
}

除了上面的写法,Android官方给了我们更好的解决方案:


class LoginFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //设置布局
        return inflater.inflate(R.layout.login_fragment, container, false)
    }

    private lateinit var myText: TextView
    private lateinit var myImg: ImageView
    private lateinit var imageView: ImageView
    private lateinit var myBtn: Button

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //查找view
        myText = view.findViewById(R.id.myText)
        myImg = view.findViewById(R.id.myImg)
        imageView = view.findViewById(R.id.imageView)
        myBtn = view.findViewById(R.id.myBtn)
    }
}

还有另外方式就是使用自动化插件,在【File-Setting-Plugins】市场搜索关键字【findview】,看最新的几个,挑选自己用的顺手的使用即可。


作者:芝麻粒儿
链接:https://juejin.cn/post/7021457050496401415
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

翻车了,字节一道 Fragment面试题

一道面试题 前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答 面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么? 所以今天,我们好好了解了解这个用得...
继续阅读 »

一道面试题


前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答


img


面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么?


在这里插入图片描述


所以今天,我们好好了解了解这个用得非常多,但是对底层不是很理解的fragment吧


首先回答面试官的问题,Fragment 的 start与activity 的start 的调用时机



调用顺序:


D/MainActivity: MainActivity:


D/MainActivity: onCreate: start


D/MainFragment: onAttach:


D/MainFragment: onCreate:




D/MainActivity: onCreate: end


D/MainFragment: onCreateView:


D/MainFragment: onViewCreated:


D/MainFragment: onActivityCreated:


D/MainFragment: onViewStateRestored:


D/MainFragment: onCreateAnimation:


D/MainFragment: onCreateAnimator:


D/MainFragment: onStart:




D/MainActivity: onStart:


D/MainActivity: onResume:


D/MainFragment: onResume:



可以看到Activity 在oncreate开始时,Fragment紧接着attach,create,然后activity执行完毕onCreate方法


此后都是Fragment在执行,直到onStart方法结束


然后轮到Activity,执行onStart onResume


也就是,Activity 创建的时候,Fragment一同创建,同时Fragment优先在后台先展示好,最后Activity带着Fragment一起展示到前台。


是什么?


Fragment中文翻译为”碎片“,在手机中,每一个Activity作为一个页面,有时候太大了,尤其是在平板的横屏下,我们希望左半边是一根独立模块,右半边是一个独立模块,比如一个新闻app,左边是标题栏,右边是显示内容


此时就非常适合Fragment


Fragment是内嵌入Activity中的,可以在onCreateView中加载自定义的布局,使用LayoutInflater,然后Activity持有FragmentManager对Fragment进行控制,下图是他的代码框架


img


我们的Activity一般是用AppCompatActivity,而AppCompatActivity继承了FragmentActivity


public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

也就是说Activity之所支持fragment,是因为有FragmentActivity,他内部有一个FragmentController,这个controller持有一个FragmentManager,真正做事的就是这个FragmentManager的实现类FragmentManagerImpl


整体架构


回到我们刚才的面试题,关于生命周期绝对是重中之重,但是实际上,生命周期本质只是被其他地方的方法被动调用而已,关键是Fragment自己的状态变化了,才会回调生命周期方法,所以我们来看看fragment的状态转移


static final int INITIALIZING = 0;     初始状态,Fragment 未创建
static final int CREATED = 1; 已创建状态,Fragment 视图未创建
static final int ACTIVITY_CREATED = 2; 已视图创建状态,Fragment 不可见
static final int STARTED = 3; 可见状态,Fragment 不处于前台
static final int RESUMED = 4; 前台状态,可接受用户交互

fragment有五个状态,


调用过程如下


img


Fragment的状态转移过程主要受到宿主,事务的影响,宿主一般就是Activity,在我们刚刚的题目中,看到了Activity与Fragment的生命周期交替执行,本质上就是,Activity执行完后通知了Fragment进行状态转移,而Fragment执行了状态转移后对应的回调了生命周期方法


下图可以更加清晰


img


宿主改变Fragment状态


那么我们不禁要问,Activity如何改变Fragment的状态?


我们知道Activity继承于FragmentActivity,最终是通过持有的FragmentManager来控制Fragment,我们去看看


FragmentActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);

super.onCreate(savedInstanceState);
...
mFragments.dispatchCreate();
}

可以看到,onCreate方法中执行了mFragments.dispatchCreate();,看起来像是通知Fragment的onCreate执行,这也印证了我们开始时的周期回调顺序


D/MainActivity: MainActivity: 
D/MainActivity: onCreate: start // 进入onCreate
D/MainFragment: onAttach: // 执行mFragments.dispatchCreate();
D/MainFragment: onCreate:
D/MainActivity: onCreate: end // 退出onCreate

类似的FragmentActivity在每一个生命周期方法中都做了相同的事情


@Override
protected void onDestroy() {
super.onDestroy();

if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}

mFragments.dispatchDestroy();
}

我们进入dispatchCreate看看,


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}

//内部修改了两个状态
public void dispatchCreate() {
mStateSaved = false;
mStopped = false;
dispatchStateChange(Fragment.CREATED);

private void dispatchStateChange(int nextState) {
try {
mExecutingActions = true;
moveToState(nextState, false);// 转移到nextState
} finally {
mExecutingActions = false;
}
execPendingActions();
}
//一路下来会执行到
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
// Fragments that are not currently added will sit in the onCreate() state.
if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
newState = Fragment.CREATED;
}
if (f.mRemoving && newState > f.mState) {
if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {
// Allow the fragment to be created so that it can be saved later.
newState = Fragment.CREATED;
} else {
// While removing a fragment, we can't change it to a higher state.
newState = f.mState;
}
}
...
}

可以看到上面的代码,最终执行到 moveToState,通过判断Fragment当前的状态,同时newState > f.mState,避免状态回退,然后进行状态转移


状态转移完成后就会触发对应的生命周期回调方法


事务管理


如果Fragment只能随着Activity的生命周期变化而变化,那就太不灵活了,所以Android给我们提供了一个独立的操作方案,事务


同样由FragManager管理,具体由FragmentTransaction执行,主要是添加删除替换Fragment等,执行操作后,需要提交来保证生效


FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);

transaction.replace(R.id.fragment_container, ExampleFragment.class, null); // 替换Fragment

transaction.commit();// 这里的commit是提交的一种方法

Android给我们的几种提交方式


image-20211020143006614


FragmentTransaction是个挂名抽象类,真正的实现在BackStackState回退栈中,我们看下commit


@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
...
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);//1
} else {
mIndex = -1;
}
// 入队操作
mManager.enqueueAction(this, allowStateLoss);//2
return mIndex;
}

可以看到,commit的本质就是将事务提交到队列中,这里出现了两个数组,注释1处


ArrayList<BackStackRecord> mBackStackIndices;
ArrayList<Integer> mAvailBackStackIndices;
public int allocBackStackIndex(BackStackRecord bse) {
synchronized (this) {
if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
if (mBackStackIndices == null) {
mBackStackIndices = new ArrayList<BackStackRecord>();
}
int index = mBackStackIndices.size();
mBackStackIndices.add(bse);
return ind
} else {
int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
mBackStackIndices.set(index, bse);
return index;
}
}
}

mBackStackIndices数组,每个元素是一个回退栈,用来记录索引。比如说,当有五个BackStackState时,移除掉1,3两个,就是在mBackStackIndices将对应元素置为null,然后mAvailBackStackIndices会添加这两个回退栈,记录被移除的回退栈


当下次commit时,就判定mAvailBackStackIndices中的索引,对应的BackStackState一定是null,直接写到这个索引即可


而一组操作都commit到同一个队列里面,所以要么全部完成,要么全部不做,可以保证原子性


注释二处是一个入队操作


public void enqueueAction(OpGenerator action, boolean allowStateLoss
synchronized (this) {
...
mPendingActions.add(action);
scheduleCommit(); // 真正的提交
}
}
public void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); // 发送请求
}
}

这里最后 mHost.getHandler()是拿到了宿主Activity的handler,使得可以在主线程执行,mExecCommit本身是一个线程


我们继续看下这个mExecCommit


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
public boolean execPendingActions() {
ensureExecReady(true);
...
doPendingDeferredStart();
burpActive();
return didSomething;
}
void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
mHavePendingDeferredStart = false;
startPendingDeferredFragments();
}
}
void startPendingDeferredFragments() {
if (mActive == null) return;
for (int i=0; i<mActive.size(); i++) {
Fragment f = mActive.valueAt(i);
if (f != null) {
performPendingDeferredStart(f);
}
}
}
public void performPendingDeferredStart(Fragment f) {
if (f.mDeferStart) {
f.mDeferStart = false;
moveToState(f, mCurState, 0, 0, false); // 最终到了MoveToState
}
}

还记得我们在宿主改变Fragment状态,里面的最终路径吗?是的,就是这个moveToState,无论是宿主改变Fragment状态,还是事务来改变,最终都会执行到moveToState,然后call对应的生命周期方法来执行,这也是为什么我们要将状态转移作为学习主线,而不是生命周期。


除了commit,可以看到FragmentTransaction有众多对Fragment进行增删改查的方法


image-20211020143442569


都是由BackStackState来执行,最后都会执行到moveToState中


具体是如何改变的,有很多细节,这里不再赘述。


小结


本节我们讲了Fragment在android系统中的状态,那就是通过自身状态转移来回调对应生命周期方法,这块是自动实现的,我们开发时不太需要关注状态转移,只要知道什么时候执行某个生命周期方法,然后再在对应方法中写业务逻辑即可


有两个方法可以让Fragment状态转移,



  • 宿主Activity生命周期内自动修改Fragment状态,回调Fragment的生命周期方法

  • 通过手动提交事务,修改Fragment状态,回调Fragment的生命周期方法

作者:神鹰梦泽
链接:https://juejin.cn/post/7021398731056480269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android模块化设计之组件开发规范

最近一直在做基础建设方面的工作,面对这三十多个完全没有规范可言的组件,气的我直接打了一套闪电五连鞭,但打工还得继续,于是想对这些组件建立一套规范,来降低够用、使用、维护以及扯皮成本,本想在网上白嫖一套,可找到的都是一些基础的代码规范,用处不大,于是乎根据自己的...
继续阅读 »

最近一直在做基础建设方面的工作,面对这三十多个完全没有规范可言的组件,气的我直接打了一套闪电五连鞭,但打工还得继续,于是想对这些组件建立一套规范,来降低够用、使用、维护以及扯皮成本,本想在网上白嫖一套,可找到的都是一些基础的代码规范,用处不大,于是乎根据自己的工作经验,总结出一套规范/约定出来,希望能抛砖引玉,各位大佬多多指点和补充~

规范本身就是开发人员之间的约定,没有最权威的,只有最适合的。

版本规范

为保证组件在各个项目中的兼容性问题,约定组件开发版本如下:

AndroidSdk版本

minSdkVersion:21

targetSdkVersion:28

语言环境

开发语言:Kotlin

Kotlin版本:1.4.x

JDK版本:1.8

其他建议版本

AndroidStuido:4.2

gradle tools:4.1.x

gradle:6.7.x

组件命名规范

根据组件的功能不同,约定组件分为三个类型:

基础组件

为项目提供与业务无关基础支持的组件库,如提供MVVM架构的lib_basic,提供依赖注入的lib_basic_koin,这类组件统一命名方式为lib_basic_xxx。这些基础组件也可以被工具组件和业务组件依赖,而不仅仅是只被项目依赖。

工具组件

对项目中常用的业务无关功能封装的组件库,如Dialog弹窗,相册选择等,这类组件统一命名方式为lib_util_xxx

业务组件

在某个项目的需求中出现的在其他项目中可能也会用到的功能的封装,比如A业务员版中扫描拍照在A商业版中也有用到,于是单独封装为一个业务组件进行管理,这类组件统一命名方式为lib_tool_xxx

GroupId

所有组件统一GroupId为com.company.android,方便在Maven仓库中进行索引和管理。

版本号

对于迭代的版本号,不做强制性的要求,只需合理进行升级即可,但以下情况需特殊注意:

1、对于上传的开发版本,需要在版本中进行体现,如1.1.0-dev02,与正式版本做区分,在项目中验证无风险后,合并到主迭代版本中。

2、对于紧急修复A项目中的问题而临时发版的,需要在版本中进行体现,如1.1.0-hotfix-A,此次修改在其他使用的项目中验证无风险后,合并到主迭代版本中。

开发原则

开闭原则

在组件升级时,应对新增开放,对修改关闭,即做加法不做减法,目的是为了保证对项目中调用老版本API的兼容问题。对于不再建议使用的API,应使用 @Deprecated注解进行标注,并新增建议使用的API进行代替,而不是直接删除旧API。在完成多个稳定版本的迭代之后,可以所有组件使用者讨论删除旧版API的事宜。

向下兼容原则

所有组件的版本迭代必须向下兼容,即1.2.0版本须兼容1.1.0,在使用者升级版本之后,无需修改业务层代码。

如果因组件前期设计不合理导致升级必须修改业务代码时,组件开发者需要与所有组件使用者商讨技术实现方案,看能否以最小的改动完成组件的升级,并在组件功能开发完毕后,以书面形式告知使用者并提供完整的升级文档

三方隔离原则

我们在封装组件的时候,难免会使用到第三方依赖,为了避免更换三方依赖或依赖升级造成的影响,需要对三方依赖库进行二次封装,避免直接使用。比如我们要使用glide框架加载图片,可以创建一个代理类对glide的功能进行代理,在业务代码中使用代理类进行操作而不是直接调用glide,这样我们将来替换glide框架时,能最小的改动业务代码。

当然,并不是所有的三方库都通过代理模式进行隔离,比如retrofit、RxJava等,毕竟我们的隔离原则是为了以后更简单的迭代而不是自寻烦恼。

最少依赖原则

为了减少项目中依赖的类库,在组件封装中遇到如下情况,应对组件进行拆分工作:

现封装一图片加载类库lib_util_imageloader,对GlidePicasso进行了二次封装,由于两个类库提供了类似的功能,在项目中只需要使用一套就能满足业务需求,没有必要把两个图片加载框架都进行依赖,所以应该对lib_util_imageloader进行拆分为如下结构:

lib_util_imageloader_core:图片加载核心库,把加载图片的方法抽象为接口,面向项目,不做具体的实现。

lib_util_imageloader_glide:对glide进行二次封装,实现core中的接口。

lib_util_imageloader_picasso:对picasso进行二次封装,实现core中的接口。

在项目使用图片加载库时,除了必须要依赖的core外,只需要从glide和picasso中挑选一个即可,这样就不会把用不到的类库也打包到项目中去了。

这样的做法还有一个好处,就是容易拓展。如果我想要使用Fresco,只需要再新增一个lib_util_imageloader_fresco,并实现core的接口即可,在切换组件时,只需要改变gradle文件中的依赖,无需变更业务代码,因为业务代码都是基于core组件的。

最少可见原则

组件应该尽可能少的对外暴露类、接口、方法等。可以通过外观模式对使用者统一提供API,降低使用者的理解难度。

支持开发模式

组件需要预留开发者模式,可以让使用者自行选择开启关闭。

打开开发者模式:组件运行的关键节点需进行日志输出,方便使用者进行调试,可以在运行时抛出异常。

关闭开发者模式:组件不再对外输出Error级别以下的日志,禁止在运行时抛出异常、ANR,如发生异常需要在组件内捕获并通过错误回调或打印Error级别日志等方式告知使用者。

可拓展性

部分组件(视具体情况而定,多数为对三方类库的封装组件)应该有一定的拓展性,应支持使用者自定义实现覆盖默认实现。

比如Dialog类库有默认弹窗样式,需要支持使用者自定义弹窗样式而不是只能在默认样式中进行选择,图片加载框架也是相同的逻辑。

开发规范

包名规范

为了避免无意中导致的包名冲突问题,约定组件的包名为4级,除去前两级的com.company为固定写法以外,后两级可根据具体的组件功能进行命名,并要求有良好的可读性。

资源规范

组件中定义资源文件时,要以组件名称为前缀,避免资源冲突导致的打包问题,需要在gradle文件的android节点下新增如下代码强制进行资源名称前缀检查:

android{
 resourcePrefix = "${your_component_name}_"
}
复制代码

代码规范

应当遵循Java开发代码规范,这里不再赘述。

可见域规范

Kotlin中新增关键字internal,可用于修饰类名、方法名和成员变量名,限制所修饰的对象模块内可见,对于无需对外暴露的类、方法和变量,应当降低其可见域。

内联函数

Kotlin中新增关键字inline修饰方法,可以减少方法栈的进栈方法数。在封装组件时善用inline以提高组件的运行效率。

内存泄漏

所有组件在发布之间,必须进行内存泄漏检测,禁止存在内存泄漏的组件上线,应在开发阶段修复所有泄漏问题。

混淆

组件开发者需要确认自己的组件在打包混淆后是否可以正常工作,如有用到运行时注解、反射、Json转换等功能,需要在接入文档中声明避免混淆的规则。

文档

组件的接入、使用、升级和注意事项等需要在开发文档中有明确的体现,没有接入文档或者文档不完善一律不许通过验收。

Demo

组件最好有配套的演示工程,用来最直观的体现出组件所提供的功能,也方便使用者进行参考。

最后

目前只能想起这么多,以后有新增的会继续补充。

规范好定制,落实起来却难,路漫漫其修远兮,加油吧,打工人!


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

收起阅读 »

消失性进度条

效果&使用 图例分别为: 修改读条起点为y轴正方向 消失性读条 正常读条 使用: 1 在xml中添加控件 <com.lloydfinch.ProgressTrackBar android:id="@+id/progress_tr...
继续阅读 »

效果&使用


效果


图例分别为:



  • 修改读条起点为y轴正方向

  • 消失性读条

  • 正常读条


使用:



  • 1 在xml中添加控件


<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_second_color="#E91E63"
app:p_width="3dp" />

<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar2"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_first_color="#18B612"
app:p_second_color="#00000000"
app:p_width="3dp" />

<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar3"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_first_color="#ffd864"
app:p_second_color="#1C3F7C"
app:p_width="3dp" />


  • 2 在代码中启动倒计时


val trackBar = findViewById<ProgressTrackBar>(R.id.progress_track_bar)
trackBar.setStartAngle(-90F) // 从-90度开始读条
trackBar.setOnProgressListener { // 进度回调
Log.d("ProgressTrackBar", "progress is $it")
}
trackBar.startTask(0) { // 开始计时,传入读条结束的回调
Log.d("ProgressTrackBar", "progress run finish")
}

// 从0开始计时
findViewById<ProgressTrackBar>(R.id.progress_track_bar2).startTask(0)

// 从20开始计时
findViewById<ProgressTrackBar>(R.id.progress_track_bar3).startTask(20)

思路&编码


核心思路就一个: 画原环。我们要画两个圆环,一个下层的完整圆环作为底色,一个上层的圆弧作为进度。重点就是计算圆弧弧度的问题了。


假设当前进度是current,最大进度是max,那么当前圆弧进度就是:(current/max)*360,然后我们直接调用:


// oval: 放置圆弧的矩形
// startAngle: 开始绘制的起点角度,方向是顺时针计算的。0就x正半轴,90就是y轴负半轴
// sweepAngle: 要绘制的圆弧的弧度,就是上述: (current/max)x360
// false: 表示不连接到圆心,表示绘制一个圆弧
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);

就能绘制出对应的圆弧。


所以,我们这样:


// 绘制下层: 圆形
mPaint.setColor(firstLayerColor);
canvas.drawCircle(x, y, radius, mPaint);

// 绘制上层: 圆弧
mPaint.setColor(secondLayerColor);
float sweepAngle = (currentProgress / maxProgress) * 360;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);

我们先用下层颜色绘制一个圆形,然后用上层颜色绘制个圆弧,然后不断触发重绘,就能得到想要的效果。


但是,如果我们想要的是: 随着进度变大,圆弧越来越短呢?比如示例图的第二个效果。说白了就是让上层随着时间流逝而变小,直到消失,怎么实现呢?


其实,说白了就是时间越长,弧度越小,我们做减法即可,我们用(max-current)来作为已读进度,这样随着时间流逝,进度就越来越小。


有人说,这样不对啊,这样(max-current)不就越读越小了吗,这样画出来的弧度就越来越短了,最后完全漏出了底层,给人的感觉是倒着读的。没错,所以,我们只绘制一层,我们用下层颜色来绘制圆弧!这样,随着时间流逝,弧度越来越小,因为圆弧是用下层颜色绘制的,所以视觉上就是: 下层越来越少。给人的感觉就是: 上层越来越大以至于盖住了下层。


逻辑如下:


// 用下层颜色 绘制 剩下的弧度
mPaint.setColor(firstLayerColor);
float leaveAngle = ((maxProgress - currentProgress) / maxProgress) * 360;
canvas.drawArc(oval, startAngle, leaveAngle, false, mPaint);

可以看到,这里只绘制一层,随着时间流逝,圆弧越来越短,给人的感觉就是: 圆弧消失。就达到了示例图中 第二个圆弧的效果。


整体代码如下:


public class ProgressTrackBar extends View {


private static final int DEFAULT_FIRST_COLOR = Color.WHITE;
private static final int DEFAULT_SECOND_COLOR = Color.parseColor("#FFA12F");

private static final int PROGRESS_WIDTH = 6;
private static final float MAX_PROGRESS = 360F;
private static final int DEFAULT_SPEED = 1;

private Paint mPaint;
private float startAngle = 0;
private int firstLayerColor = DEFAULT_FIRST_COLOR;
private int secondLayerColor = DEFAULT_SECOND_COLOR;
private final RectF oval = new RectF(); // 圆形轨迹
private float maxProgress = MAX_PROGRESS; // 最大进度:ms
private float currentProgress = 0F; // 当前进度:ms
private int speed = DEFAULT_SPEED; // 速度(多长时间更新一次UI):ms
private int progressWidth = PROGRESS_WIDTH; // 进度条宽度

private OnProgressFinished onProgressFinished;

private Handler taskHandler;
private OnProgress runnable; //进度回调

// 顶层颜色是否是透明
private boolean isSecondColorTransparent = false;

public ProgressTrackBar(Context context) {
super(context);
init();
}

public ProgressTrackBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressTrackBar);
firstLayerColor = typedArray.getColor(R.styleable.ProgressTrackBar_p_first_color, DEFAULT_FIRST_COLOR);
secondLayerColor = typedArray.getColor(R.styleable.ProgressTrackBar_p_second_color, DEFAULT_SECOND_COLOR);
startAngle = typedArray.getFloat(R.styleable.ProgressTrackBar_p_start, 0F);
progressWidth = typedArray.getDimensionPixelSize(R.styleable.ProgressTrackBar_p_width, PROGRESS_WIDTH);
maxProgress = typedArray.getDimension(R.styleable.ProgressTrackBar_p_max_progress, MAX_PROGRESS);

typedArray.recycle();

init();
}

public ProgressTrackBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
refresh();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(progressWidth);
}

public void setFirstLayerColor(int firstLayerColor) {
this.firstLayerColor = firstLayerColor;
}

public void setSecondLayerColor(int secondLayerColor) {
this.secondLayerColor = secondLayerColor;
refresh();
}

public void setMaxProgress(float maxProgress) {
this.maxProgress = maxProgress;
}

public void setSpeed(int speed) {
this.speed = speed;
}

public void setStartAngle(float startAngle) {
this.startAngle = startAngle;
}

public void setProgressWidth(int progressWidth) {
this.progressWidth = progressWidth;
}

public void setOnProgressListener(OnProgress runnable) {
this.runnable = runnable;
}

public void setOnProgressFinished(OnProgressFinished onProgressFinished) {
this.onProgressFinished = onProgressFinished;
}

private void initTask() {
taskHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (currentProgress < maxProgress) {
currentProgress += speed;
postInvalidate();
if (runnable != null) {
runnable.onProgress(currentProgress);
}
taskHandler.sendEmptyMessageDelayed(0, speed);
} else {
stopTask();
}
}
};
}

private void refresh() {
isSecondColorTransparent = (secondLayerColor == Color.parseColor("#00000000"));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int x = getWidth() >> 1;
int y = getHeight() >> 1;
int center = Math.min(x, y);
int radius = center - progressWidth;

int left = x - radius;
int top = y - radius;
int right = x + radius;
int bottom = y + radius;
oval.set(left, top, right, bottom);

// 这里需要处理一下上层是透明的情况
if (isSecondColorTransparent) {
// 用下层颜色 绘制 剩下的弧度
mPaint.setColor(firstLayerColor);
float leaveAngle = ((maxProgress - currentProgress) / maxProgress) * 360;
canvas.drawArc(oval, startAngle, leaveAngle, false, mPaint);
} else {
// 绘制下层
mPaint.setColor(firstLayerColor);
canvas.drawCircle(x, y, radius, mPaint);

// 绘制上层
mPaint.setColor(secondLayerColor);
float sweepAngle = (currentProgress / maxProgress) * 360;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);
}
}

public void startTask(int progress) {
currentProgress = progress;
initTask();
taskHandler.sendEmptyMessage(0);
}

public void startTask(int progress, OnProgressFinished onProgressFinished) {
this.onProgressFinished = onProgressFinished;
currentProgress = progress;
initTask();
taskHandler.sendEmptyMessage(0);
}

public void stopTask() {
if (onProgressFinished != null) {
onProgressFinished.onFinished();
}
if (taskHandler != null) {
taskHandler.removeCallbacksAndMessages(null);
}
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopTask();
}

public interface OnProgressFinished {
void onFinished();
}

public interface OnProgress {
void onProgress(float progress);
}
}

总结


核心思路就一个: 如果上层要用透明盖住下层,这是不可能的,所以不如用上层的相对值去绘制下层


作者:奔波儿灞取经
链接:https://juejin.cn/post/7021448361488154631
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

你知道为何跨域中会发送 options 请求?

同源策略 同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查...
继续阅读 »

同源策略



同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。



简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查。比如防止利用它源的存储信息(Cookies...)做不安全的用途。


跨域 CORS


但凡被浏览器识别为不同源,浏览器都会认为是跨域,默认是不允许的。


比如:试图在 http://127.0.0.1:4000 中,请求 http://127.0.0.1:3000 的资源会出现如下错误:


跨域错误


这也是前端 100% 在接口调试中会遇到的问题。


同源和跨域的判断规则


简单请求和复杂请求



相信都会在浏览器的 Network 中看到两个同样地址的请求,有没有想过这是为什么呢?这是因为在请求中,会分为 简单请求复杂请求


简单请求:满足如下条件的,将不会触发跨域检查:



  • 请求方法为:GETPOSTHEAD

  • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type


其中 Content-Type 限定为 :text/plain、multipart/form-data、application/x-www-form-urlencoded


我们可以更改同源规则,看下如下示例:



http://127.0.0.1:4000/ 下,请求 http://127.0.0.1:3000 不同端口的地址



简单请求


域名不同,这已经跨域了。但由于请求方法为 GET,符合 简单请求,请求将正常工作。


复杂请求:不满足简单请求的都为复杂请求。在发送请求前,会使用 options 方法发起一个 预检请求(Preflight) 到服务器,以获知服务器是否允许该实际请求。


模拟一个跨域请求:


// 端口不同,content-type 也非限定值
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
},
}
);

能看到在请求之前浏览器会事先发起一个 Preflight 预检请求


Preflight


这个 预检请求 的请求方法为 options,同时会包含 Access-Control-xxx 的请求头:


options请求信息


当然,此时服务端没有做跨域处理(示例使用 express 起的服务,预检请求默认响应 200),就会出现浏览器 CORS 的错误警告。


跨域错误


如何解决跨域


对于跨域,前端再熟悉不过,百度搜索能找到一堆解决方法,关键词不是 JSONP,或者添加些 Access-Control-XXX 响应头。


本篇将详细说下后一种方式,姑且称为:服务端解决方案。


为 options 添加响应头


express 举例,首先对 OPTIONS 方法的请求添加这些响应头,它将根据告诉浏览器根据这些属性进行跨域限制:


app.use(function (req, res, next) {
if (req.method == 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.status(200).end();
}
});

如果你不对 预检接口 做正确的设置,那么后续一切都是徒劳。


打个比方:如果 Access-Control-Allow-Methods 只设置了 POST,如果客户端请求方法为 PUT,那么最终会出现跨域异常,并会指出 PUT 没有在预检请求中的 Access-Control-Allow-Methods 出现:


跨域方法错误
所以,以后读懂跨域异常对于正确的添加服务端响应信息非常重要。另外:GET、POST、HEAD 属于简单请求的方法,所以即使不在 Access-Control-Allow-Methods 定义也不碍事(如果不对请指出)


正式的跨域请求


随后对我们代码发出的请求额外添加跨域响应头(这需要和前面的预检接口一致)


if (req.method == 'OPTIONS') {
//...
} else {
// http://127.0.0.1:3000/test/cors
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}

最后能看到我们等请求正常请求到了:


跨域请求


对于跨域请求头的说明


上例出现了我们经常见到的三个:Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers


参考 cors 库,另外还有其他用于预检请求的响应头:



下面将对上面这些头做个说明。


Access-Control-Allow-Origin


预检请求正常请求 告知浏览器被允许的源。支持通配符“*”,但不支持以逗号“,”分割的多源填写方式。


如果尝试些多个域名,则会出现如下错误:



Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'aaa,bbb', but only one is allowed.



多源错误


另外,也不建议 Access-Control-Allow-Origin 以通配符方式定义,这样会增加安全隐患,最好以请求方的 origin 来赋值。


const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin || '*');
// 因为会随着客户端请求的 Origin 变化,所以标识 Vary,让浏览器不要缓存
res.setHeader('Vary', 'Origin');

Access-Control-Allow-Methods


被允许的 Http 方法,按照需要填写,支持多个,例如: GET , HEAD , PUT , PATCH , POST , DELETE


由于判断 简单请求 之一的 HTTP 方法默认为 GETPOSTHEAD ,所以这些即使不在 Access-Control-Allow-Methods 约定,浏览器也是支持的。


比如:如果服务端定义 PUT 方法,而客户端发送的方法为 DELETE,则会出现如下错误:


res.setHeader('Access-Control-Allow-Methods', 'PUT');


Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.



方法错误


Access-Control-Allow-Headers


预检接口 告知客户端允许的请求头。


简单请求 约定的请求头默认支持: AcceptAccept-LanguageContent-LanguageContent-Typetext/plain、multipart/form-data、application/x-www-form-urlencoded


如果客户端的请求头不在定义范围内,则会报错:



Request header field abc is not allowed by Access-Control-Allow-Headers in preflight response.



请求头错误


需要将此头调整为:


res.setHeader('Access-Control-Allow-Headers', 'content-type, abc');

Access-Control-Max-Age


定义 预检接口 告知客户端允许的请求头可以缓存多久。


默认时间规则:



  • 在 Firefox 中,上限是 24 小时 (即 86400 秒)。

  • 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。

  • 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。

  • Chromium 同时规定了一个默认值 5 秒。

  • 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。


比如设置为 5 秒后,客户端在第一次会发送 预检接口 后,5 秒内将不再发送 预检接口


res.setHeader('Access-Control-Max-Age', '5');

缓存示例


Access-Control-Allow-Credentials


跨域的请求,默认浏览器不会将当前地址的 Cookies 信息传给服务器,以确保信息的安全性。如果有需要,服务端需要设置 Access-Control-Allow-Credentials 响应头,另外客户端也需要开启 withCredentials 配置。


// 客户端请求
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
);

// 所有请求
res.setHeader('Access-Control-Allow-Credentials', 'true');

需要注意的是,Access-Control-Allow-Origin 不能设置通配符“*”方式,会出现如下错误:


不支持通配符
这个 Access-Control-Allow-Origin 必须是当前页面源的地址。


Access-Control-Expose-Headers


Access-Control-Allow-Credentials 类似,如果服务端有自定义设置的请求头,跨域的客户端请求在响应信息中是接收不到该请求头的。


// 服务端
res.setHeader('def', '123');

axios
.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
)
.then((data) => {
console.log(data.headers.def); //undefined
});

需要在服务端设置 Access-Control-Expose-Headers 响应头,并标记哪些头是客户端能获取到的:


res.setHeader('Access-Control-Expose-Headers', 'def');
res.setHeader('def', '123');

Access-Control-Request-Headers


我试了半天没找到 Access-Control-Request-Headers 的使用示例,其实它是根据当前请求的头拼接得到的。


如果客户端的请求头为:


{
"content-type": "application/json",
"abc": "123",
"xyz": "123",
},

那么浏览器最后会在 预检接口 添加一个 Access-Control-Request-Headers 的头,其值为:abc,content-type,xyz。然后服务端再根据 Access-Control-Allow-Headers 告诉浏览器服务端的请求头支持说明,最后浏览器判断是否会有跨域错误。


另外,对于服务端也需要针对 Access-Control-Request-HeadersVary 处理:


res.setHeader('Vary', 'Origin' + ', ' + req.headers['access-control-request-headers']);

如此,对于跨域及其怎么处理头信息会有个基本的概念。希望在遇到类似问题能有章法的解决,而非胡乱尝试。


作者:Eminoda
链接:https://juejin.cn/post/7021077647417409550

收起阅读 »

移动端常见问题汇总,拿来吧你!

1px适配方案 某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求, 这时候我们可以利用缩放来达到目的 .border_1px:before{    content: '';  ...
继续阅读 »

1px适配方案


某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求,


这时候我们可以利用缩放来达到目的


.border_1px:before{
   content: '';
   position: absolute;
   top: 0;
   height: 1px;
   width: 100%;
   background-color: #000;
   transform-origin: 0% 0%;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .border_1px:before{
       transform: scaleY(0.5);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .border_1px:before{
       transform: scaleY(0.33);
  }
}


设置一个专门的class来处理1px的问题,利用伪类给其添加



  • -webkit-min-device-pixel-ratio 获取像素比

  • transform: scaleY(0.5) 垂直方向缩放,后面的数字是倍数


图片模糊问题


.avatar{
   background-image: url(conardLi_1x.png);
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .avatar{
       background-image: url(conardLi_2x.png);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .avatar{
       background-image: url(conardLi_3x.png);
  }
}

根据不一样的像素比,准备不一样的图片,正常来说是1px图片像素 对应1px物理像素,图片的显示就不会模糊啦,但是这样的情况不多,不是非常重要,特殊需求的图,我们不这么做。


滚动穿透问题


wt1.gif


移动端的网站,我们是经常会有一些弹出框出现的,这样的弹出框,在上面滑动,会导致我们后面的整个页面发生移动,这个问题怎么解决呢??


body{
   position:fixed;
   width:100%;
}

给body添加position:fixed就可以使滚动条失效,这里弹框的显示和隐藏,我们利用JS进行控制,而且添加上position:fixed的一瞬间,可以看到页面一下回到0,0的位置,因为fixed是根据可视区定位的。


键盘唤起


main{
   padding: 2rem 0;
   /* height: 2000px; */
   position: absolute;
   top: 60px;
   bottom: 60px;
   overflow-y: scroll;
   width: 100%;
   -webkit-overflow-scrolling: touch;
}

当底部根据页面进行fixed定位的时候,键盘弹出一瞬间,fixed会失效,变成类似absoult,让main的内容无滚动,就不会连带fixed一起动了


并且为了保证如丝般顺滑:



  • -webkit-overflow-scrolling: touch;


移动端的神奇操作


IOS下的一些设置 和 安卓下的一些设置


添加到主屏幕后的标题


<meta name="apple-mobile-web-app-title" content="标题"> 

image.png


添加到主屏后的APP图标


<link href="short_cut_114x114.png" rel="apple-touch-icon-precomposed">


  • 一般我们只需要提供一个114*114的图标即可


启用webApp全屏模式


<meta name="apple-mobile-web-app-capable" content="yes" /> 
<meta name="apple-touch-fullscreen" content="yes" />



  • apple-mobile-web-app-capable


    删除默认的苹果工具栏和菜单栏,默认为no




  • apple-touch-fullscreen


    全屏显示




移动端手机号码识别


<meta name="format-detection" content="telephone=no" />


  • safari会对一些可能是手机号码的数字,进行识别,我们可以利用上面的方式,禁止识别


手动开启拨打电话功能


<a href="tel:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以直接拨打电话


手动开启短信功能


<a href="sms:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以跳转去短信页面,给该手机号发送消息


移动端邮箱识别


<meta name="format-detection" content="email=no" /> 

手动开启邮箱发送功能


<a href="mailto:854121000@qq.com">发送邮件</a>


  • 调用邮箱发送功能


优先启用最新版本IE和chrome


<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 

移动端默认样式




  • 移动端默认字体



    1. 数字 和 英文字体 可使用Helvetica字体,IOS 和 Android都有这个字体

    2. 手机系统都有自己默认的字体,直接使用默认的


    body{
       font-family:Helvetica;
    }



  • 字体大小


    如果只是适配手机,可以使用px




  • IOS系统中,链接、按钮等点击会有灰色遮罩


    a,button,input,textarea{-webkit-tap-highlight-color: rgba(0,0,0,0)}



  • 去除圆角


    button,input{
       -webkit-appearance:none;
       border-radius: 0;
    }



  • 禁止文本缩放


    html{
        -webkit-text-size-adjust: 100%;
    }
收起阅读 »

你真的了解border-radius吗?

水平半径和垂直半径 现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写: border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 其中...
继续阅读 »

水平半径和垂直半径


现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写:


border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 

其中,第一个值表示水平半径,第二个值表示圆角垂直半径;


例如:


    <style>
.talk-dialog {
position: relative;
background: deepskyblue;
width: 100px;
height: 100px;
margin: 0 auto;
border-top-left-radius: 30px 80px;
}
</style>

 <div class="talk-dialog"></div>

image.png


那么border-radius的写法应该怎么去写呢??它的水平半径和垂直半径是通过 斜杠 区分。 例如:


border-radius: 30px / 40px;


表示四个角的圆角水平半径都是30px,垂直半径是40px;


border-radius斜杠前后都支持1-4个值,以下多个值得写法为:


border-radius:10px 20px / 5% 20% 3% 10%;(左上+右下,右上+左下, / 左上,右上,右下,左下)


重叠问题


难道你认为这就完了,border-radius你彻底搞懂了??其实不然!


我们来看看下面一个列子:


<!DOCTYPE html>
<html lang="en">
<head>

<style>
.talk-dialog {
position: relative;
background: red;
width: 100px;//重点关注
height: 100px;//重点关注
border-radius: 30px / 80px; //重点关注
margin: 50px auto;
}

.talk-dialog1 {
position: relative;
background: deepskyblue;
width: 100px;//重点关注
height: 100px;//重点关注
border-top-left-radius: 30px 80px; //重点关注
margin: 10px auto;
}
</style>

</head>

<body>
<div class="talk-dialog"></div>
<div class="talk-dialog1"></div>
</body>
</html>

我们的容器大小宽为100px,高为100px, 问大家一个问题!


border-radius: 30px / 80px; 与 border-top-left-radius: 30px 80px; 两个不同的容器的 top-left的圆角大小一样吗???


image.png


大家或许这样看不出来,我们修改为绝对布局,两个元素重叠在一起看看是否左上角可以完美重叠?


image.png


答案揭晓: 圆角效果是不一样的,因为我们容器的垂直高度为100px,我们border-radius:30px / 80px设置以后,我们元素的高度不足以放下两个半轴为80px(80+80=160)的椭圆,如果这种场景不做约束,曲线就会发生一定的重叠,因此css 规范对圆角曲线重叠问题做了额外的渲染设定,具体算法如下:


f=min(L宽度/S宽度,L高度/S高度),L为容器宽高,S为半径之和,


这里计算我们的例子:f=min(100/60,100/160)=0.625 , f的值小于1,则所有的圆角值都要乘以f


因此:border-radius: 30px / 80px;


左上角值等同于:


border-top-left-radius:18.75px 50px;


细节



  • border-radius 不支持负值

  • 圆角以外的区域不可点击

  • border-radius没有继承性,因此父元素设置了border-radius,子元素依旧是直角效果,要想达到圆角效果,需要加overflow:hidden。(重要,工作中常用)

  • border-radius 也支持transition过渡效果


高级用法案例:


image.png


代码:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title></title>
<link rel="icon" href="data:;base64,=" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<meta name=" theme-color" content="#000000" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<style>
.radius {
width: 150px;
height: 150px;
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
object-fit: cover;
object-position: right;
}

.demo {
position: relative;
width: 150px;
height: 150px;
margin: 10px auto;
}

.radius-1 {
width: 150px;
height: 150px;
object-fit: cover;
object-position: right;
background: deepskyblue;
color: #fff;
font-size: 40px;
text-align: center;
line-height: 120px;
border-bottom-right-radius: 100%;
}

.talk {
padding: 10px;
background: deepskyblue;
border-radius: .5em;
color: #fff;
position: relative;
z-index: 0;
}

.talk::before {
content: "";
position: absolute;
width: 15px;
height: 10px;
color: deepskyblue;
border-top: 10px solid;
border-top-left-radius: 80%;
left: 0;
bottom: 0;
margin-left: -12px;
-ms-transform: skewX(-30deg) scaleY(1.3);
transform: skewX(-30deg) scaleY(1.3);
z-index: -1;
}
</style>

</head>

<body>
<div class="demo demo1">
<img class="radius" src="./1.jpg" />
</div>
<div class="demo demo2">
<div class="radius-1">1</div>
</div>
<div class="demo demo3">
<div class="talk">border-radius圆角效果实现。</div>
</div>
</body>

</html>

结语:


欢迎大家多提宝贵意见,一赞一回,如果本文让你get 到知识,请不要吝啬你的star!



收起阅读 »