注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

开发者实践丨盲水印插件:用户端的实时视频溯源保护

本文作者是 RTE 2021 创新编程挑战赛获奖者董章晔团队。在实时音视频领域,视频内容会需要得到版权保护,而盲水印则是保护的措施之一。这支参赛团队基于声网 SDK 开发了一款应用于用户端的实时视频盲水印插件。其他使用声网 SDK 的开发者,也同样可以在自己的...
继续阅读 »

本文作者是 RTE 2021 创新编程挑战赛获奖者董章晔团队。在实时音视频领域,视频内容会需要得到版权保护,而盲水印则是保护的措施之一。这支参赛团队基于声网 SDK 开发了一款应用于用户端的实时视频盲水印插件。其他使用声网 SDK 的开发者,也同样可以在自己的应用中使用该插件。访问『阅读原文』,可以查看该项目的源码。

项目介绍

视频盲水印技术是将标识信息直接嵌入视频 RGB 或 YUV 的频域中,基本不影响原视频的观看质量,也不容易被人觉察或注意。通过这些隐藏在载体中的信息,可确认内容创建者、使用者或者判断视频是否被篡改。该技术通常由专业的版权保护服务商提供,用于广播电视版权保护,商业性较强。

本项目基于声网的 SDK 开发了一款用户端的实时视频盲水印的插件,同时,配套提供了一款基于个人 PC 端的水印识别软件,用于水印验证。降低了使用盲水印服务的专业门槛,为个人用户的隐私保护和作品防盗版提供了便捷的解决方案。

实现原理

盲水印的实现原理是在频域上完成信息叠加,变换的方法包括离散傅立叶变换、小波变换等,比如采用傅里叶变换,在实部和虚部完成文字图像叠加,再通过逆变换显示视频帧。

图片

对视频帧提取水印的方法是对视频帧截图,对截图再进行一次傅里叶变换,得到频域数据,对频域幅度,即能量进行显示,得出频域幅度图,就会显示之前叠加的文字。

图片

快速傅里叶变换复杂度为 O(nlog(n)),原理上可以在视频处理过程中实现盲水印的实时叠加。

设计实现

程序设计包括声网 SDK 对接和盲水印开发两部分,盲水印开发分为 Android 端叠加水印和 Windows 提取水印两部分。分别是灰色、黄色和橙色三块。由于是演示 Demo,所以仅在本地视频预览上完成盲水印的处理,未来可扩展到视频显示上。

图片

该方案设计重点考虑 SDK 衔接和第三方兼容两大方面。主要是少拷贝YUV数据、视频处理串行化、第三方兼容性和场景泛化等方面。

核心代码

叠加水印的主流程:

图片

opencv 的调用函数:

图片

主要是傅立叶变换和叠加文字两个函数,声网 SDK 与 OpenCV 开源库兼容效果良好。

效果展示

图片

第一幅图是原始视频,输入水印文字比如 wm,第二幅图是叠加盲水印的视频,可见视频效果基本不受影响,最后一幅图是将第二幅图上传到 PC 后,用户自己提取水印的图像,可见图像中有明显的 wm 文字。至此完成了验证。

未来展望

下一步计划主要从提高水印的鲁棒性、扩展水印的应用场景、丰富水印的数据维度等方面进行考虑。在水印鲁棒性方面,计划空域上进行网格化分割,针对不同分割区域进行频域水印叠加;采用不同的变换方法,例如 DWT,以求最佳效果;对水印本身进行冗余编码,提升水印辨识度,增加水印的隐蔽性。在扩展水印应用方面,在实时视频显示端,进行水印叠加,达到偷拍溯源的目的。在丰富数据维度方面,在音频处理上,可扩展声纹水印;结合视频内容特征,可扩展特征编码等。


收起阅读 »

Jetpact Compose状态管理简单理解

概览所谓的状态可以简单的理解为应用中的某个值的变化,比如可以是一个布尔值、数组放在业务的场景中,可以是 TextField 中的文字、动画执行的状态、用户收藏的商品都是状态我们知道 compose 是声明式的 ui,每次我们重组页面的时候都会把组件重组,此时就...
继续阅读 »

概览

所谓的状态可以简单的理解为应用中的某个值的变化,比如可以是一个布尔值、数组

放在业务的场景中,可以是 TextField 中的文字、动画执行的状态、用户收藏的商品都是状态

我们知道 compose 是声明式的 ui,每次我们重组页面的时候都会把组件重组,此时就需要引入状态进行管理,例如:

我们在商品的 item 里面点击按钮收藏了商品,此时商品的收藏状态发生了改变,我们需要重组 ui 将商品变为已收藏状态,这个时候就需要用 remember 扩展方法保存重组状态,如果使用 boolean 这个基本类型保存那么就无法在重组 ui 后正常的设置组件的状态。

代码举例(抄官方代码):

@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = "输入的值",
onValueChange = { },
label = { Text("Name") }
)
}
}

运行上面的代码,我们会发现无论我们如何在 TextField 中输入内容,TextFile 的内容都不会变,这就是因为无法保存状态导致的,以下代码示例可以正常的改变 TextField 中的内容


@Composable
fun textFieldStateHasTextShow(){
var value by remember {//这里就是对TextField中展示的文字进行状态保存的操作
mutableStateOf("")
}
Box(modifier = Modifier.fillMaxSize(1f),contentAlignment = Alignment.Center) {
OutlinedTextField(
value = value,
onValueChange = {
value=it//每次输入内容的时候,都回调这个更新状态,从而刷新重组ui
},
label = { Text("Name") }
)
}
}

状态管理的常用方法

remember 重组中保存状态

组合函数可以通过remember记住单个对象,系统会在初始化期间将remember初始的值存储在组合中。重组的时候可以返回对象值,remember既可以用来存储可变对象又可以存储不可变的对象

当可组合项被移除后,会忘记 remember 存储的对象。

mutableStateOf

mutableStateOf 会创建可观察的 MutableState<T>,例如如下代码: data 就是一个MutableState对象

每当data.value值发生改变的时候,系统就会重组ui。

var data = remember {
mutableStateOf("")
}

注:mutableStateOf 必须使用 remember 嵌套才能在数据更改的时候重组界面

rememberSaveable 保存配置

remember可以帮助我们在界面重组的时候保存状态,而rememberSaveable可以帮助我们存储配置更改(重新创建activity或进程)时的状态。

Livedata、Flow、RxJava 转换为状态

这三个框架是安卓常用的三个响应式开发框架,都支持转化为State对象,以 Flow 举例,如下代码可以转化为一个 State:

  val favorites = MutableStateFlow<Set<String>>(setOf())
val state = favorites.collectAsState()

状态管理

有状态和无状态

使用 remember、rememberSaveState 方法保存状态的组合项是有状态组合

反之是无状态组合

状态提升

如下代码是官方关于状态提升的代码:

本例代码中 HelloContent 是无状态的,它的状态被提升到了 HelloScreen 中,HelloContent 有nameonNameChange两个参数,name 是状态,通过 HelloScreen 组合项传给 HelloContent

而 HelloContent 中发生的更改它也不能自己进行处理,必须将更改传给HelloScreen进行处理并重组界面。

以上的逻辑叫做:状态下降,事件上升

@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }

HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}

存储状态的方式

前面的介绍中我们知道使用rememberSaveable方法我们可以通过 Bundle 的方式保存状态,那么如果我们要保存的状态不方便用 Bundle 的情况下该何如处理呢?

以下三种方式,可以实现对非 Bundle 的数据的保存(配置更改后的保存)

Parcelize

代码示例:

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}

MapSaver

data class City(val name: String, val country: String)

val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, countryKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}

@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}

ListSaver

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },//数组中保存的值和City中的属性是顺序对应的
restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}

状态管理源码分析

remember

初次阅读 remember 的源码,可能有理解不对的地方(但总得有人先去看不是),多多见谅,欢迎指正

  • remember 方法调用的主流程

remember方法返回的是一个MutableState对象,MutableState可以在数据更新的时候通知系统重组ui

 rememberedValue 就是数据转换的逻辑

  • rememberedValue 方法解析

inserting:如果我们正在将新的节点插入到视图数中,那么 inserting=true

reusing:意为正在重用,我的理解是当前正在重新使用这个状态,所以避免多次获取

  • reader.next 方法 晒一段源码
  fun next(): Any? {
if (emptyCount > 0 || currentSlot >= currentSlotEnd) return Composer.Empty
return slots[currentSlot++]
}

slots是一个数组,currentSlot表示我们要获取到的状态在数组中的索引,compose 构建页面是单线程的,索引每次我们调用remember方法的时候如果状态已经存在就从slots中获取数据,然后把currentSlot索引加 1,这样当我们调用了最后一个remember方法的时候currentSlot索引刚好等于slots数组.length-1


收起阅读 »

我用index作为key也没啥问题啊,为什么面试还有人diao我???

所有熟悉 Vue 技术栈的小伙伴,都知道在列表渲染的场景下,不能使用 index 或 random 作为 key。 也有很多小伙伴在面试的时候会被面试官比较详细的追问,假如使用 index 作为 key 会有什么问题?假如使用 random 作为 key 会有...
继续阅读 »

所有熟悉 Vue 技术栈的小伙伴,都知道在列表渲染的场景下,不能使用 indexrandom 作为 key


也有很多小伙伴在面试的时候会被面试官比较详细的追问,假如使用 index 作为 key 会有什么问题?假如使用 random 作为 key 会有什么问题?假如使用一个唯一不变的 id 作为 key 有什么好处呢?


这道题目,表面上看起来是考察我们对同级比较过程中 diff 算法的理解,唯一不变的 key 可以帮助我们更快的找到可复用的 VNode,节省性能开销,使用 index 作为 key 有可能造成 VNode 错误的复用,从而产生 bug ,而使用 random 作为 key 会导致VNode 始终无法复用,极大的影响性能。


这么回答有问题么?没有问题。


但是假如这道题目满分100,我只能给你99分。


还有 1分,涉及到 Vue 更新流程中的一点点细节,若不理清,可能在实际的业务场景中给我们造成困扰。


啥困扰呢?


举个栗子


直奔主题,看一段代码,index 作为 key ,假如我们删除某一条,结果会是啥呢?


<template>
 <div id="app">
   <div v-for="(item, index) in data" :key="index">
     <Child />
     <button @click="handleDelete(index)">删除这一行</button>
   </div>
 </div>
</template>

<script>

export default {
 name: "App",
 components: {
   Child: {
     template: '<span>{{name}}{{Math.floor(Math.random() * 1000)}}</span>',
     props: ['name']
  }
},
 data() {
   return {
     data: [
      { name: "小明" },
      { name: "小红" },
      { name: "小蓝" },
      { name: "小紫" },
    ]
  };
},
 methods: {
   handleDelete(index) {
     this.data.splice(index, 1);
  },
}
};
</script>

看结果



可以观察到,虽然我们删除的不是最后一条,但最终却是最后一条被删除了,看起来很奇怪,但是假如你了解过 Vuediff 流程,这个结果应该是可以符合你的预期的。


diff


大段的列源码,会增加我们的理解负担,所以我把 Vue更新流程简化成一张图:



通常来讲,我们说 Vuediff 流程,指的就是 patchVnode ,其中 updateChildren 就是我们说的同层比较,其实就是比较新旧两个 Vnode 数组。


Vue 会声明四个指针变量,分别记录新旧 Vnode 数组的首尾索引,通过首尾索引指针的移动,根据新头旧头、新尾旧尾、旧头新尾、旧尾新头的顺序,依次比较新旧 Vnode ,若不能命中 sameVnode,则将oldVnode.key 维护成一个 map, 继续查询是否包含newVnode.key ,若命中 sameVnode ,则递归执行 patchVnode。若最终无法命中,说明无可复用的 Vnode ,创建新的 dom 节点。


newVnode 的首尾指针先相遇,说明 newVnode 已经遍历完成,直接移除 oldVnode 多余部分,若 oldVnode 的首尾指针先相遇,说明 oldVnode 已经遍历完成,直接新增 newVnode 的多余部分。


这种直接的文字描述会显得比较苍白,所以我给大家准备了个动画


第一步:



第二步:



第三步:



第四步:



第五步:



第六步:



理论上,只要你滑动的足够快,这几张图就可以动起来😊



上面描述updateChildren过程的图片均摘自 Vue技术揭秘 组件更新章节,建议大家翻阅原文


我尝试了半天实在做不出来动画,同时感觉这几张图已经可以带给我们足够直观的感受了,所以直接搬运了


侵删



使用 index 作为 key 会有什么问题


上面我们讲,判断新旧 Vnode 是否可以复用,取决于 sameNode 方法,这个方法非常简单,就是比对 Vnode 的部分属性,其中 key 是最关键的因素


function sameVnode (a, b) {
   return (
     a.key === b.key &&
     a.asyncFactory === b.asyncFactory && (
      (
         a.tag === b.tag &&
         a.isComment === b.isComment &&
         isDef(a.data) === isDef(b.data) &&
         sameInputType(a, b)
      ) || (
         isTrue(a.isAsyncPlaceholder) &&
         isUndef(b.asyncFactory.error)
      )
    )
  )
}

我们再回到上面的栗子,看看是哪里出了问题


上面代码生成的 VNode 大约是这样的:


[
{
   tag: 'div',
key: 0,
children: [
{
tag: VueComponent,
       elm: 408, // 这个Vnode对应的真实dom是408
},
{
tag: 'button'
}
]
},
{
   tag: 'div',
key: 1,
children: [
{
tag: VueComponent,
       elm: 227, // 这个Vnode对应的真实dom是227
},
{
tag: 'button'
}
]
}
 ...
]

我们删除第一条数据,新的 VNode 大约是这样的:


[
{
   tag: 'div',
key: 0,
children: [
{
tag: VueComponent,
       elm: 227, // 这个Vnode对应的真实dom是227
},
{
tag: 'button'
}
]
},
{
   tag: 'div',
key: 1,
children: [
{
tag: VueComponent,
       elm: 324, // 这个Vnode对应的真实dom是324
},
{
tag: 'button'
}
]
}
 ...
]

我们人肉逻辑 一下这两个 Vnode 数组,由于 key 都是0,所以比较第一条的时候,就会命中 sameNode ,导致错误复用,然后 updateChildren ,子节点的 Vnode 依然会命中 sameVnode ,同理,第二、三条均会命中 sameVnode ,而直接错误复用其关联的真实 dom 节点,所以我们明明删除的是第一条,UI表现却是最后一条被删除了。


那么到这里就结束了么?


当然没有,因为很多小伙伴在刚接触 Vue 的时候,也用过 index 作为 key ,部分牛逼的项目甚至已经上线了,似乎也没人来找麻烦


why?


为什么我用 index 作为 key 没出现问题


如果我把代码改成这样,再删除某一条,会是什么结果呢?


<template>
 <div id="app">
   <div v-for="(item, index) in data" :key="index">
     <Child :name="`${item.name}`" />
     <button @click="handleDelete(index)">删除这一行</button>
   </div>
 </div>
</template>

看结果



法克,我们明明把 Vue更新流程捋清楚了,用 index 作为 key 会导致 Vnode 错误复用啊,怎么这里表现却正常了呢?


我们再看一下更新流程简化图:



组件类型的 Vnode ,在 patchVnode 的过程中会执行 prePatch 钩子函数,给组件的 propsData 重新赋值,从而触发 setter ,假如 propsData 的值有变化,则会触发 update ,重新渲染组件


我们可以再人肉逻辑 一下,这次我们删除的是第二条,因为key 一致,新的 Vnode 数组依然会复用旧的 Vnode 数组的前三条,第一条 Vnode 是正确复用,组件的 propsData 未发生变化,不会触发 update ,直接复用其关联的真实 dom 节点,但是第二条 Vnode 是错误复用,但是组件的 propsData 发生变化,由小红变成了小蓝,触发了 update ,组件重新渲染,因此我们看到其实连 random 都发生了变化,第三条同理。


呼~


到这里,总算是搞明白了,我可真是个小机灵鬼


那么到这里就结束了么?


其实还没有,比如我们再改一下代码


<template>
 <div id="app">
   <div v-for="(item, index) in data" :key="index">
     <span>{{item.name}}</span>
     <button @click="handleDelete(index)">删除这一行</button>
   </div>
 </div>
</template>

看结果



这次我们没有组件类型 Vnode ,不会执行 prePatch,为啥表现还是正常的呢?


再观察一下上面的更新流程图,文本类型的 Vnode ,新旧文本不同的时候是会直接覆盖的。


到这里,我们已经完全明白,列表渲染的场景下,为什么推荐使用唯一不变的 id 作为 key了。抛开代码规范不谈,即使某些场景下,问题并未以 bug 的形式暴露出来,但是不能复用、或者错误复用 Vnode ,都会导致组件重新渲染,这部分的性能包袱还是非常沉重的!


最后的1分


纸上得来终觉浅,绝知此事要躬行


我第一次读完 Vue2 源码的时候,以为自己已经清晰的明白了这部分知识,直到团队里的小伙伴拿着一个纯文本类型的列表来质问我


不得已仔细 debug 了一遍更新流程,才算解开了心中疑惑,补上了这 1分 的缺口



链接:https://juejin.cn/post/6999932053466644517

收起阅读 »

Android 10 启动分析之Init篇 (一)

按下电源键时,android做了啥?当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好...
继续阅读 »

按下电源键时,android做了啥?

当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。

bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好软件环境,最后调用操作系统内核”。除此之外,bootloader还有保护功能,部分品牌的手机对bootloader做了加锁操作,防止boot分区和recovery分区被写入。

或许有人会问了,什么是boot分区,什么又是recovery分区?

我们先来认识一下Android系统的常见分区:

/boot

这个分区上有Android的引导程序,包括内核和内存操作程序。没有这个分区设备就不能被引导。恢复系统的时候会擦除这个分区,并且必须重新安装引导程序和ROM才能重启系统。

/recovery

recovery分区被认为是另一个启动分区,你可以启动设备进入recovery控制台去执行高级的系统恢复和管理操作。

/data

这个分区保存着用户数据。通讯录、短信、设置和你安装的apps都在这个分区上。擦除这个分区相当于恢复出厂设置,当你第一次启动设备的时候或者在安装了官方或者客户的ROM之后系统会自动重建这个分区。当你执行恢复出厂设置时,就是在擦除这个分区。

/cache

这个分区是Android系统存储频繁访问的数据和app的地方。擦除这个分区不影响你的个人数据,当你继续使用设备时,被擦除的数据就会自动被创建。

/apex

Android Q新增特性,将系统功能模块化,允许系统按模块来独立升级。此分区用于存放apex 相关的内容。

为什么需要bootloader去拉起linux内核,而不把bootloader这些功能直接内置在linux内核中呢?这个问题不在此做出回答,留给大家自行去思考。

bootloader完成初始化工作后,会载入 /boot 目录下面的 kernel,此时控制权转交给操作系统。操作系统将要完成的存储管理、设备管理、文件管理、进程管理、加载驱动等任务的初始化工作,以便进入用户态。

内核启动完成后,将会寻找init文件(init文件位于/system/bin/init),启动init进程,也就是android的第一个进程。

我们来关注一下内核的common/init/main.c中的kernel_init方法。

static int __ref kernel_init(void *unused)
{
...

if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
}
if (CONFIG_DEFAULT_INIT[0] != '\0') {
ret = run_init_process(CONFIG_DEFAULT_INIT);
if (ret)
pr_err("Default init %s failed (error %d)\n",CONFIG_DEFAULT_INIT, ret);
else
return 0;
}

if (!try_to_run_init_process("/sbin/init") ||!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||!try_to_run_init_process("/bin/sh"))
return 0;
}

可以看到,在init_kernel的最后,会调用run_init_process方法来启动init进程。

static int run_init_process(const char *init_filename){
const char *const *p;
argv_init[0] = init_filename;
return kernel_execve(init_filename, argv_init, envp_init);
}

kernel_execve是内核空间调用用户空间的应用程序的函数。

接下来我们来重点分析init进程。

init进程解析

我们从system/core/init/main.cpp 这个文件开始看起。

int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
__asan_set_error_report_callback(AsanReportCallback);
#endif

if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}

if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap function_map;

return SubcontextMain(argc, argv, &function_map);
}

if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}

if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}

return FirstStageMain(argc, argv);
}

第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数。

main函数有四个参数入口:

  • 一是参数中有ueventd,进入ueventd_main

  • 二是参数中有subcontext,进入InitLogging 和SubcontextMain

  • 三是参数中有selinux_setup,进入SetupSelinux

  • 四是参数中有second_stage,进入SecondStageMain

main的执行顺序如下:

  1.  FirstStageMain  启动第一阶段

  2. SetupSelinux    加载selinux规则,并设置selinux日志,完成SELinux相关工作

  3. SecondStageMain  启动第二阶段

  4.  ueventd_main    init进程创建子进程ueventd,并将创建设备节点文件的工作托付给ueventd。

FirstStageMain

我们来从FirstStageMain的源码看起,源码位于/system/core/init/first_stage_init.cpp

int FirstStageMain(int argc, char** argv) {

boot_clock::time_point start_time = boot_clock::now();

#define CHECKCALL(x) \
if (x != 0) errors.emplace_back(#x " failed", errno);

// Clear the umask.
umask(0);

//初始化系统环境变量
CHECKCALL(clearenv());
CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
// 挂载及创建基本的文件系统,并设置合适的访问权限
CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
CHECKCALL(mkdir("/dev/pts", 0755));
CHECKCALL(mkdir("/dev/socket", 0755));
CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)
CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR
// 不要将原始命令行公开给非特权进程
CHECKCALL(chmod("/proc/cmdline", 0440));
gid_t groups[] = {AID_READPROC};
CHECKCALL(setgroups(arraysize(groups), groups));
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));

CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));

if constexpr (WORLD_WRITABLE_KMSG) {
CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
}

//创建linux随机伪设备文件
CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));

//log wrapper所必须的,需要在ueventd运行之前被调用
CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

...

//将内核的stdin/stdout/stderr 全都重定向/dev/null,关闭默认控制台输出
SetStdioToDevNull(argv);
// tmpfs已经挂载到/dev上,同时我们也挂载了/dev/kmsg,我们能够与外界开始沟通了
//初始化内核log
InitKernelLogging(argv);

//检测上面的操作是否发生了错误
if (!errors.empty()) {
for (const auto& [error_string, error_errno] : errors) {
LOG(ERROR) << error_string << " " << strerror(error_errno);
}
LOG(FATAL) << "Init encountered errors starting first stage, aborting";
}

LOG(INFO) << "init first stage started!";

auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{opendir("/"), closedir};
if (!old_root_dir) {
PLOG(ERROR) << "Could not opendir("/"), not freeing ramdisk";
}

struct stat old_root_info;

...

//挂载 system、cache、data 等系统分区
if (!DoFirstStageMount()) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}

...

//进入下一步,SetupSelinux
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
execv(path, const_cast<char**>(args));

return 1;
}

我们来总结一下,FirstStageMain到底做了哪些重要的事情:

  1. 挂载及创建基本的文件系统,并设置合适的访问权限

  2. 关闭默认控制台输出,并初始化内核级log。

  3. 挂载 system、cache、data 等系统分区

SetupSelinux

这个模块主要的工作是设置SELinux安全策略,本章内容主要聚焦于android的启动流程,selinux的内容在此不做展开。

int SetupSelinux(char** argv) {

...

const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));

return 1;
}

SetupSelinux的最后,进入了init的第二阶段SecondStageMain。

SecondStageMain

不多说,先上代码。

int SecondStageMain(int argc, char** argv) {
// 禁止OOM killer 杀死该进程以及它的子进程
if (auto result = WriteFile("/proc/1/oom_score_adj", "-1000"); !result) {
LOG(ERROR) << "Unable to write -1000 to /proc/1/oom_score_adj: " << result.error();
}

// 启用全局Seccomp,Seccomp是什么请自行查阅资料
GlobalSeccomp();

// 设置所有进程都能访问的会话密钥
keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);

// 创建 /dev/.booting 文件,就是个标记,表示booting进行中
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

//初始化属性服务,并从指定文件读取属性
property_init();

...

// 进行SELinux第二阶段并恢复一些文件安全上下文
SelinuxSetupKernelLogging();
SelabelInitialize();
SelinuxRestoreContext();

//初始化Epoll,android这里对epoll做了一层封装
Epoll epoll;
if (auto result = epoll.Open(); !result) {
PLOG(FATAL) << result.error();
}

//epoll 中注册signalfd,主要是为了创建handler处理子进程终止信号
InstallSignalFdHandler(&epoll);

...

//epoll 中注册property_set_fd,设置其他系统属性并开启系统属性服务
StartPropertyService(&epoll);
MountHandler mount_handler(&epoll);

...

ActionManager& am = ActionManager::GetInstance();
ServiceList& sm = ServiceList::GetInstance();
//解析init.rc等文件,建立rc文件的action 、service,启动其他进程,十分关键的一步
LoadBootScripts(am, sm);

...

am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");

//执行rc文件中触发器为 on early-init 的语句
am.QueueEventTrigger("early-init");

// 等冷插拔设备初始化完成
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");

am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");

// 设备组合键的初始化操作
Keychords keychords;
am.QueueBuiltinAction(
[&epoll, &keychords](const BuiltinArguments& args) -> Result<Success> {
for (const auto& svc : ServiceList::GetInstance()) {
keychords.Register(svc->keycodes());
}
keychords.Start(&epoll, HandleKeychord);
return Success();
},
"KeychordInit");
am.QueueBuiltinAction(console_init_action, "console_init");

// 执行rc文件中触发器为on init的语句
am.QueueEventTrigger("init");

// Starting the BoringSSL self test, for NIAP certification compliance.
am.QueueBuiltinAction(StartBoringSslSelfTest, "StartBoringSslSelfTest");

// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
// wasn't ready immediately after wait_for_coldboot_done
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");


am.QueueBuiltinAction(InitBinder, "InitBinder");

// 当设备处于充电模式时,不需要mount文件系统或者启动系统服务,充电模式下,将charger设为执行队列,否则把late-init设为执行队列
std::string bootmode = GetProperty("ro.bootmode", "");
if (bootmode == "charger") {
am.QueueEventTrigger("charger");
} else {
am.QueueEventTrigger("late-init");
}

// 基于属性当前状态 运行所有的属性触发器.
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");

while (true) {
//开始进入死循环状态
auto epoll_timeout = std::optional<std::chrono::milliseconds>{};

//执行关机重启流程
if (do_shutdown && !shutting_down) {
do_shutdown = false;
if (HandlePowerctlMessage(shutdown_command)) {
shutting_down = true;
}
}

if (!(waiting_for_prop || Service::is_exec_service_running())) {
am.ExecuteOneCommand();
}
if (!(waiting_for_prop || Service::is_exec_service_running())) {
if (!shutting_down) {
auto next_process_action_time = HandleProcessActions();

// If there's a process that needs restarting, wake up in time for that.
if (next_process_action_time) {
epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
*next_process_action_time - boot_clock::now());
if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
}
}

// If there's more work to do, wake up again immediately.
if (am.HasMoreCommands()) epoll_timeout = 0ms;
}

// 循环等待事件发生
if (auto result = epoll.Wait(epoll_timeout); !result) {
LOG(ERROR) << result.error();
}
}

return 0;
}

总结一下,第二阶段做了以下这些比较重要的事情:

  1. 初始化属性服务,并从指定文件读取属性
  2. 初始化epoll,并注册signalfd和property_set_fd,建立和init的子进程以及部分服务的通讯桥梁
  3. 初始化设备组合键,使系统能够对组合键信号做出响应
  4. 解析init.rc文件,并按rc里的定义去启动服务
  5. 开启死循环,用于接收epoll的事件

在第二阶段,我们需要重点关注以下问题:

init进程是如何通过init.rc配置文件去启动其他的进程的呢?

init.rc 解析

我们从 LoadBootScripts(am, sm)这个方法开始看起,一步一部来挖掘init.rc 的解析流程。

static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
//初始化ServiceParse、ActionParser、ImportParser三个解析器
Parser parser = CreateParser(action_manager, service_list);

std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
//bootscript为空,进入此分支
parser.ParseConfig("/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
if (!parser.ParseConfig("/product/etc/init")) {
late_import_paths.emplace_back("/product/etc/init");
}
if (!parser.ParseConfig("/product_services/etc/init")) {
late_import_paths.emplace_back("/product_services/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
} else {
parser.ParseConfig(bootscript);
}
}

我们可以看到这句话,Parse开始解析init.rc文件,在深入下去之前,让我们先来认识一下init.rc。

 parser.ParseConfig("/init.rc")

init.rc是一个可配置的初始化文件,负责系统的初步建立。它的源文件的路径为 /system/core/rootdir/init.rc

init.rc文件有着固定的语法,由于内容过多,限制于篇幅的原因,在此另外单独开了一篇文章进行讲解:

Android 10 启动分析之init语法

了解了init.rc的语法后,我们来看看init.rc文件里的内容。

import /init.environ.rc  //导入全局环境变量
import /init.usb.rc //adb 服务、USB相关内容的定义
import /init.${ro.hardware}.rc //硬件相关的初始化,一般是厂商定制
import /vendor/etc/init/hw/init.${ro.hardware}.rc
import /init.usb.configfs.rc
import /init.${ro.zygote}.rc //定义Zygote服务

我们可以看到,在/system/core/init目录下,存在以下四个zygote相关的文件

image.png

怎样才能知道我们当前的手机用的是哪个配置文件呢?

答案是通过adb shell getprop | findstr ro.zygote命令,看看${ro.zygote}这个环境变量具体的值是什么,笔者所使用的华为手机的ro.zygote值如下所示:

image.png

什么是Zygote,Zygote的启动过程是怎样的,它的启动配置文件里又做了啥,在这里我们不再做进一步探讨, 只需要知道init在一开始在这个文件中对Zygote服务做了定义,而上述的这些问题将留到 启动分析之Zygote篇 再去说明。

on early-init
# Disable sysrq from keyboard
write /proc/sys/kernel/sysrq 0

# Set the security context of /adb_keys if present.
restorecon /adb_keys

# Set the security context of /postinstall if present.
restorecon /postinstall

mkdir /acct/uid

# memory.pressure_level used by lmkd
chown root system /dev/memcg/memory.pressure_level
chmod 0040 /dev/memcg/memory.pressure_level
# app mem cgroups, used by activity manager, lmkd and zygote
mkdir /dev/memcg/apps/ 0755 system system
# cgroup for system_server and surfaceflinger
mkdir /dev/memcg/system 0550 system system

start ueventd

# Run apexd-bootstrap so that APEXes that provide critical libraries
# become available. Note that this is executed as exec_start to ensure that
# the libraries are available to the processes started after this statement.
exec_start apexd-bootstrap

紧接着是一个Action,Action的Trigger 为early-init,在这个 Action中,我们需要关注最后两行,它启动了ueventd服务和apex相关服务。还记得什么是ueventd和apex吗?不记得的读者请往上翻越再自行回顾一下。

ueventd服务的定义也可以在init.rc文件的结尾找到,具体代码及含义如下:

service ueventd    //ueventd服务的可执行文件的路径为 /system/bin/ueventd
class core //ueventd 归属于 core class,同样归属于core class的还有adbd 、console等服务
critical //表明这个Service对设备至关重要,如果Service在四分钟内退出超过4次,则设备将重启进入恢复模式。
seclabel u:r:ueventd:s0 //selinux相关的配置
shutdown critical //ueventd服务关闭行为

然而,early-init 这个Trigger到底什么时候触发呢?

答案是通过init.cpp代码调用触发。

我们可以在init.cpp 代码中找到如下代码片段:

am.QueueEventTrigger("early-init");

QueueEventTrigger这个方法的实现机制我们稍后再进行探讨,目前我们只需要了解, ActionManager 这个类中的 QueueEventTrigger方法,负责触发init.rc中的Action。

我们继续往下看init.rc的内容。

on init

...

# Start logd before any other services run to ensure we capture all of their logs.
start logd

# Start essential services.
start servicemanager
...

在Trigger 为init的Action中,我们只需要关注以上的关键内容。在init的action中启动了一些核心的系统服务,这些服务具体的含义为 :

服务名含义
logdAndroid L加入的服务,用于保存Android运行期间的日志
servicemanagerandroid系统服务管理者,负责查询和注册服务

接下来是late-init Action:

on late-init
//启动vold服务(管理和控制Android平台外部存储设备,包括SD插拨、挂载、卸载、格式化等)
trigger early-fs
trigger fs
trigger post-fs
trigger late-fs

//挂载/data , 启动 apexd 服务
trigger post-fs-data

# 读取持久化属性或者从/data 中读取并覆盖属性
trigger load_persist_props_action

//启动zygote服务!!在启动zygote服务前会先启动netd服务(专门负责网络管理和控制的后台守护进程)
trigger zygote-start

//移除/dev/.booting 文件
trigger firmware_mounts_complete

trigger early-boot
trigger boot //初始化网络环境,设置系统环境和守护进程的权限

最后,我们用流程图来总结一下上述的启动过程:

first_stage
second_stage
设置SetupSelinux安全策略
挂载system、cache、data等系统分区
初始化内核log
初始化基本的文件系统
初始化系统环境变量
开启死循环,用于接收epoll的事件
启动zygote服务
启动netd服务
挂载/data,启动apexd服务
启动vold服务
启动servicemanager服务
启动logd服务
启动ueventd服务
解析init.rc文件
初始化设备组合键
初始化epoll
初始化属性服务
Click Power Button
bootloader
linux kernel
init进程
收起阅读 »

Android 10 启动分析之servicemanager篇 (二)

上一篇文章:Android 10 启动分析之Init篇 (一)在前文提到,init进程会在在Trigger 为init的Action中,启动servicemanager服务,这篇文章我们就来具体分析一下servicemanager服务,它到底做了哪些事情。se...
继续阅读 »

上一篇文章:

Android 10 启动分析之Init篇 (一)

在前文提到,init进程会在在Trigger 为init的Action中,启动servicemanager服务,这篇文章我们就来具体分析一下servicemanager服务,它到底做了哪些事情。

servicemanager服务的源码位于/frameworks/native/cmds/servicemanager/service_manager.c,我们将从这个类的入口开始看起。

int main(int argc, char** argv)
{
struct binder_state *bs;
union selinux_callback cb;
char *driver;

if (argc > 1) {
driver = argv[1];
} else {
//启动时默认无参数,走这个分支
driver = "/dev/binder";
}

//打开binder驱动,并设置mmap的内存大小为128k
bs = binder_open(driver, 128*1024);

...

if (binder_become_context_manager(bs)) {
ALOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}

cb.func_audit = audit_callback;
selinux_set_callback(SELINUX_CB_AUDIT, cb);
#ifdef VENDORSERVICEMANAGER
cb.func_log = selinux_vendor_log_callback;
#else
cb.func_log = selinux_log_callback;
#endif
selinux_set_callback(SELINUX_CB_LOG, cb);

#ifdef VENDORSERVICEMANAGER
sehandle = selinux_android_vendor_service_context_handle();
#else
sehandle = selinux_android_service_context_handle();
#endif
selinux_status_open(true);

if (sehandle == NULL) {
ALOGE("SELinux: Failed to acquire sehandle. Aborting.\n");
abort();
}

if (getcon(&service_manager_context) != 0) {
ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n");
abort();
}


/* binder_loop已封装如下步骤:
while (1)
{
/* read data */
/* parse data, and process */
/* reply */
}
*/
binder_loop(bs, svcmgr_handler);

return 0;
}

从main函数中可以看出,它主要做了三件事情:

  1. 打开/dev/binder设备,并在内存中映射128K的空间。
  2. 通知Binder设备,把自己变成context_manager,其他用户进程都通过0号句柄访问ServiceManager。
  3. 进入循环,不停的去读Binder设备,看是否有对service的请求,如果有的话,就去调用svcmgr_handler函数回调处理请求。

我们再来看看svcmgr_handler函数的实现:

int svcmgr_handler(struct binder_state *bs,
struct binder_transaction_data_secctx *txn_secctx,
struct binder_io *msg,
struct binder_io *reply)
{
struct svcinfo *si;
uint16_t *s;
size_t len;
uint32_t handle;
uint32_t strict_policy;
int allow_isolated;
uint32_t dumpsys_priority;

struct binder_transaction_data *txn = &txn_secctx->transaction_data;

if (txn->target.ptr != BINDER_SERVICE_MANAGER)
return -1;

if (txn->code == PING_TRANSACTION)
return 0;

...

switch(txn->code) {
case SVC_MGR_GET_SERVICE:
case SVC_MGR_CHECK_SERVICE:
s = bio_get_string16(msg, &len);
if (s == NULL) {
return -1;
}
handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid,
(const char*) txn_secctx->secctx);
if (!handle)
break;
bio_put_ref(reply, handle);
return 0;

case SVC_MGR_ADD_SERVICE:
s = bio_get_string16(msg, &len);
if (s == NULL) {
return -1;
}
handle = bio_get_ref(msg);
allow_isolated = bio_get_uint32(msg) ? 1 : 0;
dumpsys_priority = bio_get_uint32(msg);
if (do_add_service(bs, s, len, handle, txn->sender_euid, allow_isolated, dumpsys_priority,
txn->sender_pid, (const char*) txn_secctx->secctx))
return -1;
break;

case SVC_MGR_LIST_SERVICES: {
uint32_t n = bio_get_uint32(msg);
uint32_t req_dumpsys_priority = bio_get_uint32(msg);

if (!svc_can_list(txn->sender_pid, (const char*) txn_secctx->secctx, txn->sender_euid)) {
ALOGE("list_service() uid=%d - PERMISSION DENIED\n",
txn->sender_euid);
return -1;
}
si = svclist;
// walk through the list of services n times skipping services that
// do not support the requested priority
while (si) {
if (si->dumpsys_priority & req_dumpsys_priority) {
if (n == 0) break;
n--;
}
si = si->next;
}
if (si) {
bio_put_string16(reply, si->name);
return 0;
}
return -1;
}
default:
ALOGE("unknown code %d\n", txn->code);
return -1;
}

bio_put_uint32(reply, 0);
return 0;
}

我们先来认识一下binder的数据传输载体binder_transaction_data:

struct binder_transaction_data {
union {
/* 当binder_transaction_data是由用户空间的进程发送给Binder驱动时,
handle是该事务的发送目标在Binder驱动中的信息,即该事务会交给handle来处理;
handle的值是目标在Binder驱动中的Binder引用。*/
__u32 handle;

/* 当binder_transaction_data是有Binder驱动反馈给用户空间进程时,
ptr是该事务的发送目标在用户空间中的信息,即该事务会交给ptr对应的服务来处理;
ptr是处理该事务的服务的服务在用户空间的本地Binder对象。*/
binder_uintptr_t ptr;

} target; // 该事务的目标对象(即,该事务数据包是给该target来处理的)

// 只有当事务是由Binder驱动传递给用户空间时,cookie才有意思,它的值是处理该事务的ServerC++层的本地Binder对象
binder_uintptr_t cookie;
// 事务编码。如果是请求,则以BC_开头;如果是回复,则以BR_开头。
__u32 code;

/* General information about the transaction. */
__u32 flags;

//表示事务发起者的pid和uid。
pid_t sender_pid;
uid_t sender_euid;

// 数据大小
binder_size_t data_size;

//数据偏移量
binder_size_t offsets_size;

//data是一个共用体,当通讯数据很小的时,可以直接使用buf[8]来保存数据。当够大时,只能用指针buffer来描述一个申请的数据缓冲区。
union {
struct {
/* transaction data */
binder_uintptr_t buffer;

binder_uintptr_t offsets;
} ptr;
__u8 buf[8];
} data;
};

可以看到,svcmgr_handler函数中对binder data的事务编码进行了判断,并分别对SVC_MGR_GET_SERVICE(SVC_MGR_CHECK_SERVICE)SVC_MGR_ADD_SERVICESVC_MGR_LIST_SERVICES三种类型的事务编码做了业务处理。

获取服务

  case SVC_MGR_CHECK_SERVICE:  
        s = bio_get_string16(msg, &len);  
        ptr = do_find_service(bs, s, len);  
        if (!ptr)  
            break;  
        bio_put_ref(reply, ptr);  
        return 0;

do_find_service函数中主要执行service的查找,并把找到的服务句柄写入reply,返回给客户端。

uint32_t do_find_service(const uint16_t *s, size_t len, uid_t uid, pid_t spid, const char* sid)
{
struct svcinfo *si = find_svc(s, len);

...

return si->handle;
}

我们继续看find_svc函数:

struct svcinfo *find_svc(const uint16_t *s16, size_t len)
{
struct svcinfo *si;

for (si = svclist; si; si = si->next) {
if ((len == si->len) &&
!memcmp(s16, si->name, len * sizeof(uint16_t))) {
return si;
}
}
return NULL;
}

svclist 是一个单向链表,储存了所有向servicemanager注册的服务信息。find_svc遍历svclist链表,通过服务名称作为索引条件,最终找到符合条件的服务。

注册服务

    case SVC_MGR_ADD_SERVICE:
s = bio_get_string16(msg, &len);
if (s == NULL) {
return -1;
}
handle = bio_get_ref(msg);
allow_isolated = bio_get_uint32(msg) ? 1 : 0;
dumpsys_priority = bio_get_uint32(msg);
if (do_add_service(bs, s, len, handle, txn->sender_euid, allow_isolated, dumpsys_priority,
txn->sender_pid, (const char*) txn_secctx->secctx))
return -1;

我们继续看do_add_service函数中做了哪些事情。

在该函数中,首先会去检查客户端是否有权限注册service,如果没有权限就直接返回,不能注册。

 if (!svc_can_register(s, len, spid, sid, uid)) {
ALOGE("add_service('%s',%x) uid=%d - PERMISSION DENIED\n",
str8(s, len), handle, uid);
return -1;
}

然后会去检查该service是否已经注册过了,如果已经注册过,那么就不能再注册了。

 si = find_svc(s, len);
if (si) {
if (si->handle) {
ALOGE("add_service('%s',%x) uid=%d - ALREADY REGISTERED, OVERRIDE\n",
str8(s, len), handle, uid);
svcinfo_death(bs, si);
}
si->handle = handle;
}

再判断内存是否足够。

 si = malloc(sizeof(*si) + (len + 1) * sizeof(uint16_t));
if (!si) {
ALOGE("add_service('%s',%x) uid=%d - OUT OF MEMORY\n",
str8(s, len), handle, uid);
return -1;
}

如果都没什么问题,会注册该service,并加入到svcList链表中。

综上所述,servicemanager主要负责查询和注册其他的系统服务,是系统服务的管理者。

文章的最后,留给大家一个问题进行思考:

为什么Android需要设计servicemanager做中转来添加和获取系统服务,而不直接让客户端去获取服务端句柄?
收起阅读 »

Android 10 启动分析之Zygote篇 (三)

上一篇文章:# Android 10 启动分析之servicemanager篇 (二)app_main在init篇中有提到,init进程会在在Trigger 为late-init的Action中,启动Zygote服务,这篇文章我们就来具体分析一下Zygote服...
继续阅读 »

上一篇文章:

# Android 10 启动分析之servicemanager篇 (二)

app_main

在init篇中有提到,init进程会在在Trigger 为late-init的Action中,启动Zygote服务,这篇文章我们就来具体分析一下Zygote服务,去挖掘一下Zygote负责的工作。

Zygote服务的启动入口源码位于 /frameworks/base/cmds/app_process/app_main.cpp,我们将从这个文件的main方法开始解析。

int main(int argc, char* const argv[])
{

//声明AppRuntime类的实例runtime,在AppRuntime类的构造方法中初始化的skia图形引擎
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));

...

bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;

++i; // Skip unused "parent dir" argument.
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
//对于64位系统nice_name为zygote64; 32位系统为zygote
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
//是否需要启动system server
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
//启动进入独立的程序模式
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
//niceName 为当前进程别名,区别abi型号
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}

...

}

可以看到,app_main根据启动时传入参数的区别,分为zygote 模式和application模式。

我们可以从init.zygote64_32.rc文件中看到zygote的启动参数为:

-Xzygote /system/bin --zygote --start-system-server --socket-name=zygote

我们接着往下看:

Vector<String8> args;
if (!className.isEmpty()) {
// We're not in zygote mode, the only argument we need to pass
// to RuntimeInit is the application argument.
//
// The Remainder of args get passed to startup class main(). Make
// copies of them before we overwrite them with the process name.
args.add(application ? String8("application") : String8("tool"));
runtime.setClassNameAndArgs(className, argc - i, argv + i);

if (!LOG_NDEBUG) {
String8 restOfArgs;
char* const* argv_new = argv + i;
int argc_new = argc - i;
for (int k = 0; k < argc_new; ++k) {
restOfArgs.append(""");
restOfArgs.append(argv_new[k]);
restOfArgs.append("" ");
}
ALOGV("Class name = %s, args = %s", className.string(), restOfArgs.string());
}
} else {
// We're in zygote mode.
//初始化Dalvik虚拟机Cache目录和权限
maybeCreateDalvikCache();

if (startSystemServer) {
//附加上start-system-serve 的arg
args.add(String8("start-system-serve 的argr"));
}

char prop[PROP_VALUE_MAX];
if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
ABI_LIST_PROPERTY);
return 11;
}

String8 abiFlag("--abi-list=");
abiFlag.append(prop);
args.add(abiFlag);

// In zygote mode, pass all remaining arguments to the zygote
// main() method.
for (; i < argc; ++i) {
args.add(String8(argv[i]));
}
}

if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string(), true /* setProcName */);
}

if (zygote) {
//进入此分支
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
}

结合传入的启动参数来看,代码将从if语句的else分支继续往下执行,进入zygote模式。至于application模式我们暂时先忽略它,等我们分析app的启动过程时再来说明。

上述代码最后将通过 runtime.start("com.android.internal.os.ZygoteInit", args, zygote);语句,将控制权限转交给AppRuntime类去继续执行。

继续从AppRuntime的start函数看起:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{

...

// 虚拟机创建及启动,主要是关于虚拟机参数的设置
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);

//注册JNI方法
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}

/*
* We want to call main() with a String array with arguments in it.
* At present we have two arguments, the class name and an option string.
* Create an array to hold them.
*/
jclass stringClass;
jobjectArray strArray;
jstring classNameStr;

//等价于strArray[0] = "com.android.internal.os.ZygoteInit"
stringClass = env->FindClass("java/lang/String");
assert(stringClass != NULL);
strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
assert(strArray != NULL);
classNameStr = env->NewStringUTF(className);
assert(classNameStr != NULL);
env->SetObjectArrayElement(strArray, 0, classNameStr);

//strArray[1] = "start-system-server";
//strArray[2] = "--abi-list=xxx";
//其中xxx为系统响应的cpu架构类型,比如arm64-v8a.
for (size_t i = 0; i < options.size(); ++i) {
jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
assert(optionsStr != NULL);
env->SetObjectArrayElement(strArray, i + 1, optionsStr);
}

/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
//将"com.android.internal.os.ZygoteInit"转换为"com/android/internal/os/ZygoteInit
char* slashClassName = toSlashClassName(className != NULL ? className : "");
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
/* keep going */
} else {
//找到这个类后就继续找成员函数main方法的Mehtod ID
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
/* keep going */
} else {
// 通过Jni调用ZygoteInit.main()方法
env->CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
if (env->ExceptionCheck())
threadExitUncaughtException(env);
#endif
}
}
free(slashClassName);

ALOGD("Shutting down VM\n");
if (mJavaVM->DetachCurrentThread() != JNI_OK)
ALOGW("Warning: unable to detach main thread\n");
if (mJavaVM->DestroyJavaVM() != 0)
ALOGW("Warning: VM did not shut down cleanly\n");
}

start()函数主要做了三件事情,一调用startVm开启虚拟机,二调用startReg注册JNI方法,三就是使用JNI把Zygote进程启动起来。

ZygoteInit

通过上述分析,代码进入了ZygoteInit.java中的main方法继续执行。从这里开始,就真正的启动了Zygote进程。我们从/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java这个文件继续往下看。

public static void main(String argv[]) {
//ZygoteServer 是Zygote进程的Socket通讯服务端的管理类
ZygoteServer zygoteServer = null;

// 标记zygote启动开始,调用ZygoteHooks的Jni方法,确保当前没有其它线程在运行
ZygoteHooks.startZygoteNoThreadCreation();

//设置pid为0,Zygote进入自己的进程组
try {
Os.setpgid(0, 0);
} catch (ErrnoException ex) {
throw new RuntimeException("Failed to setpgid(0,0)", ex);
}

Runnable caller;
try {

...

//开启DDMS(Dalvik Debug Monitor Service)功能
RuntimeInit.enableDdms();

//解析app_main.cpp - start()传入的参数
boolean startSystemServer = false;
String zygoteSocketName = "zygote";
String abiList = null;
boolean enableLazyPreload = false;
for (int i = 1; i < argv.length; i++) {
if ("start-system-server".equals(argv[i])) {
//启动zygote时,传入了参数:start-system-server,会进入此分支
startSystemServer = true;
} else if ("--enable-lazy-preload".equals(argv[i])) {
//启动zygote_secondary时,才会传入参数:enable-lazy-preload
enableLazyPreload = true;
} else if (argv[i].startsWith(ABI_LIST_ARG)) {
abiList = argv[i].substring(ABI_LIST_ARG.length());
} else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
//SOCKET_NAME_ARG 为 zygote 或zygote_secondary,具体请参考 init.zyoget64_32.rc文件
zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length());
} else {
throw new RuntimeException("Unknown command line argument: " + argv[i]);
}
}

// 根据传入socket name来决定是创建socket还是zygote_secondary
final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME);

if (abiList == null) {
throw new RuntimeException("No ABI list supplied.");
}

// In some configurations, we avoid preloading resources and classes eagerly.
// In such cases, we will preload things prior to our first fork.
// 在第一次zygote启动时,enableLazyPreload为false,执行preload
if (!enableLazyPreload) {
bootTimingsTraceLog.traceBegin("ZygotePreload");
EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
SystemClock.uptimeMillis());
// 加载进程的资源和类
preload(bootTimingsTraceLog);
EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
SystemClock.uptimeMillis());
bootTimingsTraceLog.traceEnd(); // ZygotePreload
} else {
Zygote.resetNicePriority();
}

// Do an initial gc to clean up after startup
bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
gcAndFinalize();
bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC

bootTimingsTraceLog.traceEnd(); // ZygoteInit
// Disable tracing so that forked processes do not inherit stale tracing tags from
// Zygote.
Trace.setTracingEnabled(false, 0);


Zygote.initNativeState(isPrimaryZygote);

ZygoteHooks.stopZygoteNoThreadCreation();

// 调用ZygoteServer 构造函数,创建socket Server端,会根据传入的参数,
// 创建两个socket:/dev/socket/zygote 和 /dev/socket/zygote_secondary
zygoteServer = new ZygoteServer(isPrimaryZygote);

if (startSystemServer) {
//fork出system server进程
Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);

// {@code r == null} in the parent (zygote) process, and {@code r != null} in the
// child (system_server) process.
if (r != null) {
// 启动SystemServer
r.run();
return;
}
}

Log.i(TAG, "Accepting command socket connections");

// ZygoteServer进入无限循环,处理请求
caller = zygoteServer.runSelectLoop(abiList);
} catch (Throwable ex) {
Log.e(TAG, "System zygote died with exception", ex);
throw ex;
} finally {
if (zygoteServer != null) {4
zygoteServer.closeServerSocket();
}
}

// We're in the child process and have exited the select loop. Proceed to execute the
// command.
if (caller != null) {
caller.run();
}
}

main方法中主要做了以下几件事:

  1. 加载进程的资源和类。
  2. 根据传入socket name来创建socket server。
  3. fork SystemServer 进程。

preload

既然preload方法是负责加载进程的资源和类,那么它究竟加载了哪些资源和哪些类呢,这些资源又位于什么位置呢?

我们先来看看preload方法里具体做了什么:

static void preload(TimingsTraceLog bootTimingsTraceLog) {

beginPreload();
//预加载类
preloadClasses();

cacheNonBootClasspathClassLoaders();
//加载图片、颜色等资源文件
preloadResources();
//加载HAL相关内容
nativePreloadAppProcessHALs();
//加载图形驱动
maybePreloadGraphicsDriver();
// 加载 android、compiler_rt、jnigraphics等library
preloadSharedLibraries();
//用于初始化文字资源
preloadTextResources();
//用于初始化webview;
WebViewFactory.prepareWebViewInZygote();
endPreload();
warmUpJcaProviders();


sPreloadComplete = true;
}

preloadClasses

 private static void preloadClasses() {
final VMRuntime runtime = VMRuntime.getRuntime();


} catch (IOException e) {
Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
} finally {
...
}
}

可以看到,preloadClasses方法读取/system/etc/preloaded-classes文件的内容,并通过Class.forName初始化类。那么在/system/etc/preloaded-classes文件具体有哪些类呢?

由于内容过多,我这里只截取部分截图让大家看看具体装载是什么类。

image.png

image.png

image.png

从装载列表中,我们可以看到很多熟悉的类,实际上,装载的类都是我们应用程序运行时可能用到的java类。

preloadResources

private static void preloadResources() {
final VMRuntime runtime = VMRuntime.getRuntime();


mResources.finishPreloading();
} catch (RuntimeException e) {
Log.w(TAG, "Failure preloading resources", e);
}
}

从上述代码可以看到,preloadResources加载了特定的图片资源和颜色资源。这些资源的路径又具体在哪里呢?

com.android.internal.R.array.preloaded_drawables的路径位于/frameworks/base/core/res/res/values/arrays.xml中,其他的资源路径也可以类似找到。各位读者可以自行去该路径下去看看所包含的资源文件到底是什么样的。

preloadSharedLibraries

private static void preloadSharedLibraries() {
Log.i(TAG, "Preloading shared libraries...");
System.loadLibrary("android");
System.loadLibrary("compiler_rt");
System.loadLibrary("jnigraphics");
}

preloadSharedLibraries里的内容很简单,主要是加载位于/system/lib目录下的libandroid.so、libcompiler_rt.so、libjnigraphics.so三个so库。


我们不妨想一下,为什么android要在Zygote中将资源先进行预加载,这么做有什么好处?

这个问题留给各位读者去自行思考,在这里便不再回答了。

forkSystemServer

 private static Runnable forkSystemServer(String abiList, String socketName,
ZygoteServer zygoteServer) {
...


return null;
}

forkSystemServer方法只是fork了一个Zygote的子进程,而handleSystemServerProcess方法构造了一个Runnable对象,创建一个子线程用于启动SystemServer的逻辑。

private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) {
Os.umask(S_IRWXG | S_IRWXO);

if (parsedArgs.mNiceName != null) {
//nicename 为 system_server
Process.setArgV0(parsedArgs.mNiceName);
}

...

if (parsedArgs.mInvokeWith != null) {
String[] args = parsedArgs.mRemainingArgs;
// If we have a non-null system server class path, we'll have to duplicate the
// existing arguments and append the classpath to it. ART will handle the classpath
// correctly when we exec a new process.
if (systemServerClasspath != null) {
String[] amendedArgs = new String[args.length + 2];
amendedArgs[0] = "-cp";
amendedArgs[1] = systemServerClasspath;
System.arraycopy(args, 0, amendedArgs, 2, args.length);
args = amendedArgs;
}

WrapperInit.execApplication(parsedArgs.mInvokeWith,
parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
VMRuntime.getCurrentInstructionSet(), null, args);

throw new IllegalStateException("Unexpected return from WrapperInit.execApplication");
} else {
//parsedArgs.mInvokeWith 为null,会进入此分支
createSystemServerClassLoader();
ClassLoader cl = sCachedSystemServerClassLoader;
if (cl != null) {
Thread.currentThread().setContextClassLoader(cl);
}

/*
* Pass the remaining arguments to SystemServer.
*/
return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
parsedArgs.mRemainingArgs, cl);
}

/* should never reach here */
}

继续从ZygoteInit.zygoteInit看起:

public static final Runnable zygoteInit(int targetSdkVersion, String[] argv,
ClassLoader classLoader) {
...

RuntimeInit.commonInit();
//注册两个jni函数
//android_internal_os_ZygoteInit_nativePreloadAppProcessHALs
//android_internal_os_ZygoteInit_nativePreloadGraphicsDriver
ZygoteInit.nativeZygoteInit();
return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
}

RuntimeInit.applicationInit

protected static Runnable applicationInit(int targetSdkVersion, String[] argv,
ClassLoader classLoader) {
//true代表应用程序退出时不调用AppRuntime.onExit(),否则会在退出前调用
nativeSetExitWithoutCleanup(true);

//设置虚拟机的内存利用率参数值为0.75
VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);

final Arguments args = new Arguments(argv);

// Remaining arguments are passed to the start class's static main
return findStaticMain(args.startClass, args.startArgs, classLoader);
}

继续看findStaticMain:

 protected static Runnable findStaticMain(String className, String[] argv,
ClassLoader classLoader) {
Class<?> cl;

}

这里通过反射获得了 com.android.server.SystemServer 类中的main方法,并传递给MethodAndArgsCaller用于构造一个Runnable。只要执行此Runnable,就会开始调用com.android.server.SystemServer 类中的main方法。

到此,Zygote的逻辑已经全部执行完毕,android启动进入了SystemServer的阶段。

最后,我们再用一个流程图来总结一下Zygote的业务逻辑:

app_mainAppRuntimeZygoteInit进入Zygote模式创建及启动Dalvik注册Jni方法预加载进程的资源和类Zygote创建socket Server端fork SystemServer子进程载入SystemServer逻辑进入无限循环,处理请求app_mainAppRuntimeZygoteInit

收起阅读 »

iOS 图层时间 一

图层时间时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMedia...
继续阅读 »

图层时间

时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克

在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMediaTiming,看看Core Animation是如何跟踪时间的。

9.1 CAMediaTiming协议

CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayerCAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。

持续和重复

我们在第八章“显式动画”中简单提到过durationCAMediaTiming的属性之一),duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。

这里的一次迭代是什么意思呢?CAMediaTiming另外还有一个属性叫做repeatCount,代表动画重复的迭代次数。如果duration是2,repeatCount设为3.5(三个半迭代),那么完整的动画时长将是7秒。

durationrepeatCount默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了“默认”,也就是0.25秒和1次,你可以用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展示了程序的结果。

清单9.1 测试durationrepeatCount

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UITextField *durationField;
@property (nonatomic, weak) IBOutlet UITextField *repeatField;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@property (nonatomic, strong) CALayer *shipLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
self.shipLayer.position = CGPointMake(150, 150);
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
}

- (void)setControlsEnabled:(BOOL)enabled
{
for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {
control.enabled = enabled;
control.alpha = enabled? 1.0f: 0.25f;
}
}

- (IBAction)hideKeyboard
{
[self.durationField resignFirstResponder];
[self.repeatField resignFirstResponder];
}

- (IBAction)start
{
CFTimeInterval duration = [self.durationField.text doubleValue];
float repeatCount = [self.repeatField.text floatValue];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = duration;
animation.repeatCount = repeatCount;
animation.byValue = @(M_PI * 2);
animation.delegate = self;
[self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
//disable controls
[self setControlsEnabled:NO];
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
//reenable controls
[self setControlsEnabled:YES];
}

@end

图9.1

图9.1 演示durationrepeatCount的测试程序

创建重复动画的另一种方式是使用repeatDuration属性,它让动画重复一个指定的时间,而不是指定次数。你甚至设置一个叫做autoreverses的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它(图9.2)。

图9.2

图9.2 摆动门的动画

对门进行摆动的代码见清单9.2。我们用了autoreverses来使门在打开后自动关闭,在这里我们把repeatDuration设置为INFINITY,于是动画无限循环播放,设置repeatCountINFINITY也有同样的效果。注意repeatCountrepeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。

清单9.2 使用autoreverses属性实现门的摇摆

@interface ViewController ()

@property (nonatomic, weak) UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//add the door
CALayer *doorLayer = [CALayer layer];
doorLayer.frame = CGRectMake(0, 0, 128, 256);
doorLayer.position = CGPointMake(150 - 64, 150);
doorLayer.anchorPoint = CGPointMake(0, 0.5);
doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
[self.containerView.layer addSublayer:doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//apply swinging animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 2.0;
animation.repeatDuration = INFINITY;
animation.autoreverses = YES;
[doorLayer addAnimation:animation forKey:nil];
}

@end

相对时间

每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。

beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。

speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。

timeOffsetbeginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。

beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。

可以用清单9.3的测试程序验证一下,设置speedtimeOffset滑块到随意的值,然后点击播放来观察效果(见图9.3)

清单9.3 测试timeOffsetspeed属性

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UILabel *speedLabel;
@property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel;
@property (nonatomic, weak) IBOutlet UISlider *speedSlider;
@property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
@property (nonatomic, strong) UIBezierPath *bezierPath;
@property (nonatomic, strong) CALayer *shipLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create a path
self.bezierPath = [[UIBezierPath alloc] init];
[self.bezierPath moveToPoint:CGPointMake(0, 150)];
[self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = self.bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
self.shipLayer.position = CGPointMake(0, 150);
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
//set initial values
[self updateSliders];
}

- (IBAction)updateSliders
{
CFTimeInterval timeOffset = self.timeOffsetSlider.value;
self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
float speed = self.speedSlider.value;
self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
}

- (IBAction)play
{
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.timeOffset = self.timeOffsetSlider.value;
animation.speed = self.speedSlider.value;
animation.duration = 1.0;
animation.path = self.bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
animation.removedOnCompletion = NO;
[self.shipLayer addAnimation:animation forKey:@"slide"];
}

@end

图9.3

图9.3 测试时间偏移和速度的简单的应用程序

fillMode

对于beginTime非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion被设置为NO的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢?

一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章“隐式动画”,模型图层和呈现图层的解释)。

另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充,因为动画开始和结束的值用来填充开始之前和结束之后的时间。

这种行为就交给开发者了,它可以被CAMediaTimingfillMode来控制。fillMode是一个NSString类型,可以接受如下四种常量:

kCAFillModeForwards 
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved

默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。

这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。

收起阅读 »

iOS 显示动画 二

8.3 过渡有时候对于iOS应用程序来说,希望能通过属性动画来对比较难做动画的布局进行一些改变。比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移...
继续阅读 »

8.3 过渡

有时候对于iOS应用程序来说,希望能通过属性动画来对比较难做动画的布局进行一些改变。比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移除图层,属性动画将不起作用。

于是就有了过渡的概念。过渡并不像属性动画那样平滑地在两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。

为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个typesubtype来标识变换效果。type属性是一个NSString类型,可以被设置成如下类型:

kCATransitionFade 
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal

到目前为止你只能使用上述四种类型,但你可以通过一些别的方法来自定义过渡效果,后续会详细介绍。

默认的过渡类型是kCATransitionFade,当你在改变图层属性之后,就创建了一个平滑的淡入淡出效果。

我们在第七章的例子中就已经用到过kCATransitionPush,它创建了一个新的图层,从边缘的一侧滑动进来,把旧图层从另一侧推出去的效果。

kCATransitionMoveInkCATransitionRevealkCATransitionPush类似,都实现了一个定向滑动的动画,但是有一些细微的不同,kCATransitionMoveIn从顶部滑动进入,但不像推送动画那样把老土层推走,然而kCATransitionReveal把原始的图层滑动出去来显示新的外观,而不是把新的图层滑动进入。

后面三种过渡类型都有一个默认的动画方向,它们都从左侧滑入,但是你可以通过subtype来控制它们的方向,提供了如下四种类型:

kCATransitionFromRight 
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom

一个简单的用CATransition来对非动画属性做动画的例子如清单8.11所示,这里我们对UIImageimage属性做修改,但是隐式动画或者CAPropertyAnimation都不能对它做动画,因为Core Animation不知道如何在插图图片。通过对图层应用一个淡入淡出的过渡,我们可以忽略它的内容来做平滑动画(图8.4),我们来尝试修改过渡的type常量来观察其它效果。

清单8.11 使用CATransition来对UIImageView做动画

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//set up images
self.images = @[[UIImage imageNamed:@"Anchor.png"],
[UIImage imageNamed:@"Cone.png"],
[UIImage imageNamed:@"Igloo.png"],
[UIImage imageNamed:@"Spaceship.png"]];
}


- (IBAction)switchImage
{
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to imageview backing layer
[self.imageView.layer addAnimation:transition forKey:nil];
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
}

@end

你可以从代码中看出,过渡动画和之前的属性动画或者动画组添加到图层上的方式一致,都是通过-addAnimation:forKey:方法。但是和属性动画不同的是,对指定的图层一次只能使用一次CATransition,因此,无论你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”,也就是常量kCATransition

图8.4

图8.4 使用CATransition对图像平滑淡入淡出

隐式过渡

CATransision可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。苹果当然意识到了这点,并且当设置了CALayercontent属性的时候,CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。

我们在第七章使用CATransition作为一个图层行为来改变图层的背景色,当然backgroundColor属性可以通过正常的CAPropertyAnimation来实现,但这不是说不可以用CATransition来实行。

对图层树的动画

CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。

这些例子和我们之前所讨论的情况完全不同,因为它们不仅涉及到图层的属性,而且是整个图层树的改变--我们在这种动画的过程中手动在层级关系中添加或者移除图层。

这里用到了一个小诡计,要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer

在清单8.2中,我们展示了如何在UITabBarController切换标签的时候添加淡入淡出的动画。这里我们建立了默认的标签应用程序模板,然后用UITabBarControllerDelegate-tabBarController:didSelectViewController:方法来应用过渡动画。我们把动画添加到UITabBarController的视图图层上,于是在标签被替换的时候动画不会被移除。

清单8.12 对UITabBarController做动画

#import "AppDelegate.h"
#import "FirstViewController.h"
#import "SecondViewController.h"
#import
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
UIViewController *viewController1 = [[FirstViewController alloc] init];
UIViewController *viewController2 = [[SecondViewController alloc] init];
self.tabBarController = [[UITabBarController alloc] init];
self.tabBarController.viewControllers = @[viewController1, viewController2];
self.tabBarController.delegate = self;
self.window.rootViewController = self.tabBarController;
[self.window makeKeyAndVisible];
return YES;
}
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to tab bar controller's view
[self.tabBarController.view.layer addAnimation:transition forKey:nil];
}
@end

自定义动画

我们证实了过渡是一种对那些不太好做平滑动画属性的强大工具,但是CATransition的提供的动画类型太少了。

更奇怪的是苹果通过UIView +transitionFromView:toView:duration:options:completion:+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransitiontype属性提供的常量完全不同UIView过渡方法中options参数可以由如下常量指定:

UIViewAnimationOptionTransitionFlipFromLeft 

UIViewAnimationOptionTransitionFlipFromRight UIViewAnimationOptionTransitionCurlUp UIViewAnimationOptionTransitionCurlDown UIViewAnimationOptionTransitionCrossDissolve UIViewAnimationOptionTransitionFlipFromTop UIViewAnimationOptionTransitionFlipFromBottom

除了UIViewAnimationOptionTransitionCrossDissolve之外,剩下的值和CATransition类型完全没关系。你可以用之前例子修改过的版本来测试一下(见清单8.13)。

清单8.13 使用UIKit提供的方法来做过渡动画

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad]; //set up images
self.images = @[[UIImage imageNamed:@"Anchor.png"],
[UIImage imageNamed:@"Cone.png"],
[UIImage imageNamed:@"Igloo.png"],
[UIImage imageNamed:@"Spaceship.png"]];
- (IBAction)switchImage
{
[UIView transitionWithView:self.imageView duration:1.0
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
}
completion:NULL];
}

@end

文档暗示过在iOS5(带来了Core Image框架)之后,可以通过CATransitionfilter属性,用CIFilter来创建其它的过渡效果。然是直到iOS6都做不到这点。试图对CATransition使用Core Image的滤镜完全没效果(但是在Mac OS中是可行的,也许文档是想表达这个意思)。

因此,根据要实现的效果,你只用关心是用CATransition还是用UIView的过渡方法就可以了。希望下个版本的iOS系统可以通过CATransition很好的支持Core Image的过渡滤镜效果(或许甚至会有新的方法)。

但这并不意味着在iOS上就不能实现自定义的过渡效果了。这只是意味着你需要做一些额外的工作。就像之前提到的那样,过渡动画做基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。

事实证明,对图层做截图还是很简单的。CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。

清单8.14演示了一个基本的实现。我们对当前视图状态截图,然后在我们改变原始视图的背景色的时候对截图快速转动并且淡出,图8.5展示了我们自定义的过渡效果。

为了让事情更简单,我们用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation可以达到同样的效果,但是那样的话我们就需要对图层的变换和不透明属性创建单独的动画,然后当动画结束的是哦户在CAAnimationDelegate中把coverView从屏幕中移除。

清单8.14 用renderInContext:创建自定义过渡效果

@implementation ViewController
- (IBAction)performTransition
{
//preserve the current view snapshot
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
//insert snapshot view in front of this one
UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
coverView.frame = self.view.bounds;
[self.view addSubview:coverView];
//update the view (we'll simply randomize the layer background color)
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
//perform animation (anything you like)
[UIView animateWithDuration:1.0 animations:^{
//scale, rotate and fade the view
CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
transform = CGAffineTransformRotate(transform, M_PI_2);
coverView.transform = transform;
coverView.alpha = 0.0;
} completion:^(BOOL finished) {
//remove the cover view now we're finished with it
[coverView removeFromSuperview];
}];
}
@end

图8.5

图8.5 使用renderInContext:创建自定义过渡效果

这里有个警告:-renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。但是用CATransition,或者用私有的截屏方式就没有这个限制了。


8.4 在动画过程中取消动画

之前提到过,你可以用-addAnimation:forKey:方法中的key参数来在添加动画之后检索一个动画,使用如下方法:

- (CAAnimation *)animationForKey:(NSString *)key;

但并不支持在动画运行过程中修改动画,所以这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。

为了终止一个指定的动画,你可以用如下方法把它从图层移除掉:

- (void)removeAnimationForKey:(NSString *)key;

或者移除所有动画:

- (void)removeAllAnimations;

动画一旦被移除,图层的外观就立刻更新到当前的模型图层的值。一般说来,动画在结束之后被自动移除,除非设置removedOnCompletionNO,如果你设置动画在结束之后不被自动移除,那么当它不需要的时候你要手动移除它;否则它会一直存在于内存中,直到图层被销毁。

我们来扩展之前旋转飞船的示例,这里添加一个按钮来停止或者启动动画。这一次我们用一个非nil的值作为动画的键,以便之后可以移除它。-animationDidStop:finished:方法中的flag参数表明了动画是自然结束还是被打断,我们可以在控制台打印出来。如果你用停止按钮来终止动画,它会打印NO,如果允许它完成,它会打印YES

清单8.15是更新后的示例代码,图8.6显示了结果。

清单8.15 开始和停止一个动画

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) CALayer *shipLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
self.shipLayer.position = CGPointMake(150, 150);
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
}

- (IBAction)start
{
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
animation.delegate = self;
[self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
}

- (IBAction)stop
{
[self.shipLayer removeAnimationForKey:@"rotateAnimation"];
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
//log that the animation stopped
NSLog(@"The animation stopped (finished: %@)", flag? @"YES": @"NO");
}

@end

图8.6

图8.6 通过开始和停止按钮控制的旋转动画

总结

这一章中,我们涉及了属性动画(你可以对单独的图层属性动画有更加具体的控制),动画组(把多个属性动画组合成一个独立单元)以及过度(影响整个图层,可以用来对图层的任何内容做任何类型的动画,包括子图层的添加和移除)。

在第九章中,我们继续学习CAMediaTiming协议,来看一看Core Animation是怎样处理逝去的时间。

收起阅读 »

call, call.call, call.call.call, 你也许还不懂这疯狂的call

Function.prototype.call 我想大家都觉得自己很熟悉了,手写也没问题!! 你确认这个问题之前, 首先看看 三千文字,也没写好 Function.prototype.call, 看完,你感觉还OK,那么再看一道题: 请问如下的输出结果 fun...
继续阅读 »

Function.prototype.call 我想大家都觉得自己很熟悉了,手写也没问题!!

你确认这个问题之前, 首先看看 三千文字,也没写好 Function.prototype.call,


看完,你感觉还OK,那么再看一道题:

请问如下的输出结果


function a(){ 
console.log(this,'a')
};
function b(){
console.log(this,'b')
}
a.call.call(b,'b')

如果,你也清晰的知道,结果,对不起,大佬, 打扰了,我错了!


本文起源:

一个掘友加我微信,私聊问我这个问题,研究后,又请教了 阿宝哥

觉得甚有意思,遂与大家分享!


结果


结果如下: 惊喜还是意外,还是淡定呢?


String {"b"} "b"

再看看如下的代码:2个,3个,4个,更多个的call,输出都会是String {"b"} "b"


function a(){ 
console.log(this,'a')
};
function b(){
console.log(this,'b')
}
a.call.call(b,'b') // String {"b"} "b"
a.call.call.call(b,'b') // String {"b"} "b"
a.call.call.call.call(b,'b') // String {"b"} "b"

看完上面,应该有三个疑问?



  1. 为什么被调用的是b函数

  2. 为什么thisString {"b"}

  3. 为什么 2, 3, 4个call的结果一样


结论:

两个以上的call,比如call.call(b, 'b'),你就简单理解为用 b.call('b')


分析


为什么 2, 3, 4个call的结果一样


a.call(b) 最终被调用的是a,

a.call.call(b), 最终被调用的 a.call

a.call.call.call(b), 最终被执行的 a.call.call


看一下引用关系


a.call === Function.protype.call  // true
a.call === a.call.call // true
a.call === a.call.call.call // true

基于上述执行分析:

a.call 被调用的是a

a.call.calla.call.call.call 本质没啥区别, 被调用的都是Function.prototype.call


为什么 2, 3, 4个call的结果一样,到此已经真相


为什么被调用的是b函数


看本质就要返璞归真,ES 标准对 Funtion.prototye.call 的描述



Function.prototype.call (thisArg , ...args)


When the call method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:



  1. If IsCallable(func) is false, throw a TypeError exception.

  2. Let argList be an empty List.

  3. If this method was called with more than one argument then in left to right order, starting with the second argument, append each argument as the last element of argList.

  4. Perform PrepareForTailCall().

  5. Return Call(functhisArgargList).



中文翻译一下



  1. 如果不可调用,抛出异常

  2. 准备一个argList空数组变量

  3. 把第一个之后的变量按照顺序添加到argList

  4. 返回 Call(functhisArgargList)的结果


这里的Call只不是是一个抽象的定义, 实际上是调用函数内部 [[Call]] 的方法, 其也没有暴露更多的有用的信息。


实际上在这里,我已经停止了思考:


a is a function, then what a.call.call really do? 一文的解释,有提到 Bound Function Exotic Objects , MDN的 Function.prototype.bind 也有提到:



The bind() function creates a new bound function, which is an exotic function object (a term from ECMAScript 2015) that wraps the original function object. Calling the bound function generally results in the execution of its wrapped function.



Function.prototype.call 相反,并没有提及!!! 但不排查在调用过程中有生成。


Difference between Function.call, Function.prototype.call, Function.prototype.call.call and Function.prototype.call.call.call 一文的解释,我觉得是比较合理的


function my(p) { console.log(p) }
Function.prototype.call.call(my, this, "Hello"); // output 'Hello'


Function.prototype.call.call(my, this, "Hello"); means:


Use my as this argument (the function context) for the function that was called. In this case Function.prototype.call was called.


So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.


It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string.



重点标出:

So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.


It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string


翻译一下:

Function.prototype.call.call(my, this, "Hello")表示: 用my作为上下文调用Function.prototype.call,也就是说my是最终被调用的函数。


my带着这些 (this, "Hello") 被调用, this 作为被调用函数的上下文,此处是作为my函数的上下文, 唯一被传递的参数是 "hello"字符串。


基于这个理解, 我们简单验证一下, 确实是这样的表象


// case 1:
function my(p) { console.log(p) }
Function.prototype.call.call(my, this, "Hello"); // output 'Hello'

// case 2:
function a(){
console.log(this,'a')
};
function b(){
console.log(this,'b')
}
a.call.call(b,'b') // String {"b"} "b"

为什么被调用的是b函数, 到此也真相了。


其实我依旧不能太释怀, 但是这个解释可以接受,表象也是正确的, 期望掘友们有更合理,更详细的解答。


为什么thisString {"b"}


在上一节的分析中,我故意遗漏了Function.prototype.call的两个note



NOTE 1: The thisArg value is passed without modification as the this value. This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value. Even though the thisArg is passed without modification, non-strict functions still perform these transformations upon entry to the function.




NOTE 2: If func is an arrow function or a bound function then the thisArg will be ignored by the function [[Call]] in step 5.



注意这一句:



This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value



两点:



  1. 如果thisArgundefined 或者null, 会用global object替换


这里的前提是 非严格模式


"use strict"

function a(m){
console.log(this, m); // undefined, 1
}

a.call(undefined, 1)


  1. 其他的所有类型,都会调用 ToObject进行转换


所以非严格模式下, this肯定是个对象, 看下面的代码:


Object('b') // String {"b"}

note2的 ToObject 就是答案


到此, 为什么thisSting(b) 这个也真相了


万能的函数调用方法


基于Function.prototype.call.call的特性,我们可以封装一个万能函数调用方法


var call = Function.prototype.call.call.bind(Function.prototype.call);

示例


var person = {
hello() {
console.log('hello', this.name)
}
}

call(person.hello, {"name": "tom"}) // hello tom

写在最后


如果你觉得不错,你的一赞一评就是我前行的最大动力。




作者:云的世界
链接:https://juejin.cn/post/6999781802923524132

收起阅读 »

iOS 显式动画 一

显式动画如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做...
继续阅读 »

显式动画

如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆

上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动。


8.1 属性动画

CAAnimationDelegate在任何头文件中都找不到,但是可以在CAAnimation头文件或者苹果开发者文档中找到相关函数。在这个例子中,我们用-animationDidStop:finished:方法在动画结束之后来更新图层的backgroundColor

当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为显式的CABasicAnimation,另一次是因为隐式动画,具体实现见订单8.3。

清单8.3 动画完成之后修改图层的背景色

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:self.colorLayer];
}

- (IBAction)changeColor
{
//create a new random color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
//create a basic animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)color.CGColor;
animation.delegate = self;
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
}

- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
//set the backgroundColor property to match animation toValue
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue;
[CATransaction commit];
}

@end

CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,就是当你有多个动画的时候,无法在在回调方法中区分。在一个视图控制器中创建动画的时候,通常会用控制器本身作为一个委托(如清单8.3所示),但是所有的动画都会调用同一个回调方法,所以你就需要判断到底是那个图层的调用。

考虑一下第三章的闹钟,“图层几何学”,我们通过简单地每秒更新指针的角度来实现一个钟,但如果指针动态地转向新的位置会更加真实。

我们不能通过隐式动画来实现因为这些指针都是UIView的实例,所以图层的隐式动画都被禁用了。我们可以简单地通过UIView的动画方法来实现。但如果想更好地控制动画时间,使用显式动画会更好(更多内容见第十章)。使用CABasicAnimation来做动画可能会更加复杂,因为我们需要在-animationDidStop:finished:中检测指针状态(用于设置结束的位置)。

动画本身会作为一个参数传入委托的方法,也许你会认为可以控制器中把动画存储为一个属性,然后在回调用比较,但实际上并不起作用,因为委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。

当使用-addAnimation:forKey:把动画添加到图层,这里有一个到目前为止我们都设置为nilkey参数。这里的键是-animationForKey:方法找到对应动画的唯一标识符,而当前动画的所有键都可以用animationKeys获取。如果我们对每个动画都关联一个唯一的键,就可以对每个图层循环所有键,然后调用-animationForKey:来比对结果。尽管这不是一个优雅的实现。

幸运的是,还有一种更加简单的方法。像所有的NSObject子类一样,CAAnimation实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:-valueForKey:方法来存取属性。但是CAAnimation有一个不同的性能:它更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。

这意味着你可以对动画用任意类型打标签。在这里,我们给UIView类型的指针添加的动画,所以可以简单地判断动画到底属于哪个视图,然后在委托方法中用这个信息正确地更新钟的指针(清单8.4)。

清单8.4 使用KVC对动画打标签

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIImageView *hourHand;
@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
@property (nonatomic, weak) IBOutlet UIImageView *secondHand;
@property (nonatomic, weak) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//adjust anchor points
self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
//set initial hand positions
[self updateHandsAnimated:NO];
}

- (void)tick
{
[self updateHandsAnimated:YES];
}

- (void)updateHandsAnimated:(BOOL)animated
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;
//calculate hour hand angle //calculate minute hand angle
CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
//calculate second hand angle
CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;
//rotate hands
[self setAngle:hourAngle forHand:self.hourHand animated:animated];
[self setAngle:minuteAngle forHand:self.minuteHand animated:animated];
[self setAngle:secondAngle forHand:self.secondHand animated:animated];
}

- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
{
//generate transform
CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
if (animated) {
//create transform animation
CABasicAnimation *animation = [CABasicAnimation animation];
[self updateHandsAnimated:NO];
animation.keyPath = @"transform";
animation.toValue = [NSValue valueWithCATransform3D:transform];
animation.duration = 0.5;
animation.delegate = self;
[animation setValue:handView forKey:@"handView"];
[handView.layer addAnimation:animation forKey:nil];
} else {
//set transform directly
handView.layer.transform = transform;
}
}

- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
//set final position for hand view
UIView *handView = [anim valueForKey:@"handView"];
handView.layer.transform = [anim.toValue CATransform3DValue];
}

我们成功的识别出每个图层停止动画的时间,然后更新它的变换到一个新值,很好。

不幸的是,即使做了这些,还是有个问题,清单8.4在模拟器上运行的很好,但当真正跑在iOS设备上时,我们发现在-animationDidStop:finished:委托方法调用之前,指针会迅速返回到原始值,这个清单8.3图层颜色发生的情况一样。

问题在于回调方法在动画完成之前已经被调用了,但不能保证这发生在属性动画返回初始状态之前。这同时也很好地说明了为什么要在真实的设备上测试动画代码,而不仅仅是模拟器。

我们可以用一个fillMode属性来解决这个问题,下一章会详细说明,这里知道在动画之前设置它比在动画结束之后更新属性更加方便。

关键帧动画

CABasicAnimation揭示了大多数隐式动画背后依赖的机制,这的确很有趣,但是显式地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。

CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation类似,CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。

关键帧起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过关键帧推算出)将由熟练的艺术家来完成。CAKeyframeAnimation也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进行插入。

我们可以用之前使用颜色图层的例子来演示,设置一个颜色的数组,然后通过关键帧动画播放出来(清单8.5)

清单8.5 使用CAKeyframeAnimation应用一系列颜色的变化

- (IBAction)changeColor
{
//create a keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor ];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
}

注意到序列中开始和结束的颜色都是蓝色,这是因为CAKeyframeAnimation并不能自动把当前值作为第一帧(就像CABasicAnimation那样把fromValue设为nil)。动画会在开始的时候突然跳转到第一帧的值,然后在动画结束的时候突然恢复到原始的值。所以为了动画的平滑特性,我们需要开始和结束的关键帧来匹配当前属性的值。

当然可以创建一个结束和开始值不同的动画,那样的话就需要在动画启动之前手动更新属性和最后一帧的值保持一致,就和之前讨论的一样。

我们用duration属性把动画时间从默认的0.25秒增加到2秒,以便于动画做的不那么快。运行它,你会发现动画通过颜色不断循环,但效果看起来有些奇怪。原因在于动画以一个恒定的步调在运行。当在每个动画之间过渡的时候并没有减速,这就产生了一个略微奇怪的效果,为了让动画看起来更自然,我们需要调整一下缓冲,第十章将会详细说明。

提供一个数组的值就可以按照颜色变化做动画,但一般来说用数组来描述动画运动并不直观。CAKeyframeAnimation有另一种方式去指定动画,就是使用CGPathpath属性可以用一种直观的方式,使用Core Graphics函数定义运动序列来绘制动画。

我们来用一个宇宙飞船沿着一个简单曲线的实例演示一下。为了创建路径,我们需要使用一个三次贝塞尔曲线,它是一种使用开始点,结束点和另外两个控制点来定义形状的曲线,可以通过使用一个基于C的Core Graphics绘图指令来创建,不过用UIKit提供的UIBezierPath类会更简单。

我们这次用CAShapeLayer来在屏幕上绘制曲线,尽管对动画来说并不是必须的,但这会让我们的动画更加形象。绘制完CGPath之后,我们用它来创建一个CAKeyframeAnimation,然后用它来应用到我们的宇宙飞船。代码见清单8.6,结果见图8.1。

清单8.6 沿着一个贝塞尔曲线对图层做动画

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create a path
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 64, 64);
shipLayer.position = CGPointMake(0, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
animation.path = bezierPath.CGPath;
[shipLayer addAnimation:animation forKey:nil];
}

@end

图8.1

图8.1 沿着一个贝塞尔曲线移动的宇宙飞船图片

运行示例,你会发现飞船的动画有些不太真实,这是因为当它运动的时候永远指向右边,而不是指向曲线切线的方向。你可以调整它的affineTransform来对运动方向做动画,但很可能和其它的动画冲突。

幸运的是,苹果预见到了这点,并且给CAKeyFrameAnimation添加了一个rotationMode的属性。设置它为常量kCAAnimationRotateAuto(清单8.7),图层将会根据曲线的切线自动旋转(图8.2)。

清单8.7 通过rotationMode自动对齐图层到曲线

- (void)viewDidLoad
{
[super viewDidLoad];
//create a path
...
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
animation.path = bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
[shipLayer addAnimation:animation forKey:nil];
}

图8.2

图8.2 匹配曲线切线方向的飞船图层

虚拟属性

之前提到过属性动画实际上是针对于关键路径而不是一个键,这就意味着可以对子属性甚至是虚拟属性做动画。但是虚拟属性到底是什么呢?

考虑一个旋转的动画:如果想要对一个物体做旋转的动画,那就需要作用于transform属性,因为CALayer没有显式提供角度或者方向之类的属性,代码如清单8.8所示

清单8.8 用transform属性对图层做动画

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform";
animation.duration = 2.0;
animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI, 0, 0, 1)];
[shipLayer addAnimation:animation forKey:nil];
}

@end

这么做是可行的,但看起来更因为是运气而不是设计的原因,如果我们把旋转的值从M_PI(180度)调整到2 * M_PI(360度),然后运行程序,会发现这时候飞船完全不动了。这是因为这里的矩阵做了一次360度的旋转,和做了0度是一样的,所以最后的值根本没变。

现在继续使用M_PI,但这次用byValue而不是toValue。也许你会认为这和设置toValue结果一样,因为0 + 90度 == 90度,但实际上飞船的图片变大了,并没有做任何旋转,这是因为变换矩阵不能像角度值那样叠加。

那么如果需要独立于角度之外单独对平移或者缩放做动画呢?由于都需要我们来修改transform属性,实时地重新计算每个时间点的每个变换效果,然后根据这些创建一个复杂的关键帧动画,这一切都是为了对图层的一个独立做一个简单的动画。

幸运的是,有一个更好的解决方案:为了旋转图层,我们可以对transform.rotation关键路径应用动画,而不是transform本身(清单8.9)。

清单8.9 对虚拟的transform.rotation属性做动画

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
[shipLayer addAnimation:animation forKey:nil];
}

@end

结果运行的特别好,用transform.rotation而不是transform做动画的好处如下:

  • 我们可以不通过关键帧一步旋转多于180度的动画。
  • 可以用相对值而不是绝对值旋转(设置byValue而不是toValue)。
  • 可以不用创建CATransform3D,而是使用一个简单的数值来指定角度。
  • 不会和transform.position或者transform.scale冲突(同样是使用关键路径来做独立的动画属性)。

transform.rotation属性有一个奇怪的问题是它其实并不存在。这是因为CATransform3D并不是一个对象,它实际上是一个结构体,也没有符合KVC相关属性,transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性。

你不可以直接设置transform.rotation或者transform.scale,他们不能被直接使用。当你对他们做动画时,Core Animation自动地根据通过CAValueFunction来计算的值来更新transform属性。

CAValueFunction用于把我们赋给虚拟的transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。你可以通过设置CAPropertyAnimationvalueFunction属性来改变,于是你设置的函数将会覆盖默认的函数。

CAValueFunction看起来似乎是对那些不能简单相加的属性(例如变换矩阵)做动画的非常有用的机制,但由于CAValueFunction的实现细节是私有的,所以目前不能通过继承它来自定义。你可以通过使用苹果目前已经提供的常量(目前都是和变换矩阵的虚拟属性相关,所以没太多使用场景了,因为这些属性都有了默认的实现方式)。


8.2 动画组

动画组

CABasicAnimationCAKeyframeAnimation仅仅作用于单独的属性,而CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。我们把清单8.6那种关键帧动画和调整图层背景色的基础动画组合起来(清单8.10),结果如图8.3所示。

清单8.10 组合关键帧动画和基础动画

- (void)viewDidLoad
{
[super viewDidLoad];
//create a path
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add a colored layer
CALayer *colorLayer = [CALayer layer];
colorLayer.frame = CGRectMake(0, 0, 64, 64);
colorLayer.position = CGPointMake(0, 150);
colorLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.containerView.layer addSublayer:colorLayer];
//create the position animation
CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
animation1.keyPath = @"position";
animation1.path = bezierPath.CGPath;
animation1.rotationMode = kCAAnimationRotateAuto;
//create the color animation
CABasicAnimation *animation2 = [CABasicAnimation animation];
animation2.keyPath = @"backgroundColor";
animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
//create group animation
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = @[animation1, animation2];
groupAnimation.duration = 4.0;
//add the animation to the color layer
[colorLayer addAnimation:groupAnimation forKey:nil];
}

图8.3

图8.3 关键帧路径和基础动画的组合

收起阅读 »

Vue3的7种和Vue2的12种组件通信,年轻人?还不收藏在等什么!!!

Vue2.x组件通信12种方式写在后面了,先来 Vue3 的 奥力给! Vue3 组件通信方式 props $emit expose / ref $attrs v-model provide / inject Vuex Vue3 通信使用写法 props ...
继续阅读 »

Vue2.x组件通信12种方式写在后面了,先来 Vue3 的


奥力给!


Vue3 组件通信方式



  • props

  • $emit

  • expose / ref

  • $attrs

  • v-model

  • provide / inject

  • Vuex


Vue3 通信使用写法


props


用 props 传数据给子组件有两种方法,如下


方法一,混合写法


// Parent.vue 传送
<child :msg1="msg1" :msg2="msg2"></child>
<script>
import child from "./child.vue"
import { ref, reactive } from "vue"
export default {
data(){
return {
msg1:"这是传级子组件的信息1"
}
},
setup(){
// 创建一个响应式数据

// 写法一 适用于基础类型 ref 还有其他用处,下面章节有介绍
const msg2 = ref("这是传级子组件的信息2")

// 写法二 适用于复杂类型,如数组、对象
const msg2 = reactive(["这是传级子组件的信息2"])

return {
msg2
}
}
}
</script>

// Child.vue 接收
<script>
export default {
props: ["msg1", "msg2"],// 如果这行不写,下面就接收不到
setup(props) {
console.log(props) // { msg1:"这是传给子组件的信息1", msg2:"这是传给子组件的信息2" }
},
}
</script>

方法二,纯 Vue3 写法


// Parent.vue 传送
<child :msg2="msg2"></child>
<script setup>
import child from "./child.vue"
import { ref, reactive } from "vue"
const msg2 = ref("这是传给子组件的信息2")
// 或者复杂类型
const msg2 = reactive(["这是传级子组件的信息2"])
</script>

// Child.vue 接收
<script setup>
// 不需要引入 直接使用
// import { defineProps } from "vue"
const props = defineProps({
// 写法一
msg2: String
// 写法二
msg2:{
type:String,
default:""
}
})
console.log(props) // { msg2:"这是传级子组件的信息2" }
</script>

注意:


如果父组件是混合写法,子组件纯 Vue3 写法的话,是接收不到父组件里 data 的属性,只能接收到父组件里 setup 函数里传的属性


如果父组件是纯 Vue3 写法,子组件混合写法,可以通过 props 接收到 data 和 setup 函数里的属性,但是子组件要是在 setup 里接收,同样只能接收到父组件中 setup 函数里的属性,接收不到 data 里的属性


官方也说了,既然用了 3,就不要写 2 了,所以不推荐混合写法。下面的例子,一律只用纯 Vue3 的写法,就不写混合写法了


$emit


// Child.vue 派发
<template>
// 写法一
<button @click="emit('myClick')">按钮</buttom>
// 写法二
<button @click="handleClick">按钮</buttom>
</template>
<script setup>

// 方法一 适用于Vue3.2版本 不需要引入
// import { defineEmits } from "vue"
// 对应写法一
const emit = defineEmits(["myClick","myClick2"])
// 对应写法二
const handleClick = ()=>{
emit("myClick", "这是发送给父组件的信息")
}

// 方法二 不适用于 Vue3.2版本,该版本 useContext()已废弃
import { useContext } from "vue"
const { emit } = useContext()
const handleClick = ()=>{
emit("myClick", "这是发送给父组件的信息")
}
</script>

// Parent.vue 响应
<template>
<child @myClick="onMyClick"></child>
</template>
<script setup>
import child from "./child.vue"
const onMyClick = (msg) => {
console.log(msg) // 这是父组件收到的信息
}
</script>

expose / ref


父组件获取子组件的属性或者调用子组件方法


// Child.vue
<script setup>
// 方法一 不适用于Vue3.2版本,该版本 useContext()已废弃
import { useContext } from "vue"
const ctx = useContext()
// 对外暴露属性方法等都可以
ctx.expose({
childName: "这是子组件的属性",
someMethod(){
console.log("这是子组件的方法")
}
})

// 方法二 适用于Vue3.2版本, 不需要引入
// import { defineExpose } from "vue"
defineExpose({
childName: "这是子组件的属性",
someMethod(){
console.log("这是子组件的方法")
}
})
</script>

// Parent.vue 注意 ref="comp"
<template>
<child ref="comp"></child>
<button @click="handlerClick">按钮</button>
</template>
<script setup>
import child from "./child.vue"
import { ref } from "vue"
const comp = ref(null)
const handlerClick = () => {
console.log(comp.value.childName) // 获取子组件对外暴露的属性
comp.value.someMethod() // 调用子组件对外暴露的方法
}
</script>

attrs


attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合


// Parent.vue 传送
<child :msg1="msg1" :msg2="msg2" title="3333"></child>
<script setup>
import child from "./child.vue"
import { ref, reactive } from "vue"
const msg1 = ref("1111")
const msg2 = ref("2222")
</script>

// Child.vue 接收
<script setup>
import { defineProps, useContext, useAttrs } from "vue"
// 3.2版本不需要引入 defineProps,直接用
const props = defineProps({
msg1: String
})
// 方法一 不适用于 Vue3.2版本,该版本 useContext()已废弃
const ctx = useContext()
// 如果没有用 props 接收 msg1 的话就是 { msg1: "1111", msg2:"2222", title: "3333" }
console.log(ctx.attrs) // { msg2:"2222", title: "3333" }

// 方法二 适用于 Vue3.2版本
const attrs = useAttrs()
console.log(attrs) // { msg2:"2222", title: "3333" }
</script>

v-model


可以支持多个数据双向绑定


// Parent.vue
<child v-model:key="key" v-model:value="value"></child>
<script setup>
import child from "./child.vue"
import { ref, reactive } from "vue"
const key = ref("1111")
const value = ref("2222")
</script>

// Child.vue
<template>
<button @click="handlerClick">按钮</button>
</template>
<script setup>

// 方法一 不适用于 Vue3.2版本,该版本 useContext()已废弃
import { useContext } from "vue"
const { emit } = useContext()

// 方法二 适用于 Vue3.2版本,不需要引入
// import { defineEmits } from "vue"
const emit = defineEmits(["key","value"])

// 用法
const handlerClick = () => {
emit("update:key", "新的key")
emit("update:value", "新的value")
}
</script>

provide / inject


provide / inject 为依赖注入


provide:可以让我们指定想要提供给后代组件的数据或


inject:在任何后代组件中接收想要添加在这个组件上的数据,不管组件嵌套多深都可以直接拿来用


// Parent.vue
<script setup>
import { provide } from "vue"
provide("name", "沐华")
</script>

// Child.vue
<script setup>
import { inject } from "vue"
const name = inject("name")
console.log(name) // 沐华
</script>

Vuex


// store/index.js
import { createStore } from "vuex"
export default createStore({
state:{ count: 1 },
getters:{
getCount: state => state.count
},
mutations:{
add(state){
state.count++
}
}
})

// main.js
import { createApp } from "vue"
import App from "./App.vue"
import store from "./store"
createApp(App).use(store).mount("#app")

// Page.vue
// 方法一 直接使用
<template>
<div>{{ $store.state.count }}</div>
<button @click="$store.commit('add')">按钮</button>
</template>

// 方法二 获取
<script setup>
import { useStore, computed } from "vuex"
const store = useStore()
console.log(store.state.count) // 1

const count = computed(()=>store.state.count) // 响应式,会随着vuex数据改变而改变
console.log(count) // 1
</script>

Vue2.x 组件通信方式


Vue2.x 组件通信共有12种



  1. props

  2. $emit / v-on

  3. .sync

  4. v-model

  5. ref

  6. $children / $parent

  7. $attrs / $listeners

  8. provide / inject

  9. EventBus

  10. Vuex

  11. $root

  12. slot


父子组件通信可以用:



  • props

  • $emit / v-on

  • $attrs / $listeners

  • ref

  • .sync

  • v-model

  • $children / $parent


兄弟组件通信可以用:



  • EventBus

  • Vuex

  • $parent


跨层级组件通信可以用:



  • provide/inject

  • EventBus

  • Vuex

  • $attrs / $listeners

  • $root


Vue2.x 通信使用写法


下面把每一种组件通信方式的写法一一列出


1. props


父组件向子组件传送数据,这应该是最常用的方式了


子组件接收到数据之后,不能直接修改父组件的数据。会报错,所以当父组件重新渲染时,数据会被覆盖。如果子组件内要修改的话推荐使用 computed


// Parent.vue 传送
<template>
<child :msg="msg"></child>
</template>

// Child.vue 接收
export default {
// 写法一 用数组接收
props:['msg'],
// 写法二 用对象接收,可以限定接收的数据类型、设置默认值、验证等
props:{
msg:{
type:String,
default:'这是默认数据'
}
},
mounted(){
console.log(this.msg)
},
}

2. .sync


可以帮我们实现父组件向子组件传递的数据 的双向绑定,所以子组件接收到数据后可以直接修改,并且会同时修改父组件的数据


// Parent.vue
<template>
<child :page.sync="page"></child>
</template>
<script>
export default {
data(){
return {
page:1
}
}
}

// Child.vue
export default {
props:["page"],
computed(){
// 当我们在子组件里修改 currentPage 时,父组件的 page 也会随之改变
currentPage {
get(){
return this.page
},
set(newVal){
this.$emit("update:page", newVal)
}
}
}
}
</script>

3. v-model


和 .sync 类似,可以实现将父组件传给子组件的数据为双向绑定,子组件通过 $emit 修改父组件的数据


// Parent.vue
<template>
<child v-model="value"></child>
</template>
<script>
export default {
data(){
return {
value:1
}
}
}

// Child.vue
<template>
<input :value="value" @input="handlerChange">
</template>
export default {
props:["value"],
// 可以修改事件名,默认为 input
model:{
event:"updateValue"
},
methods:{
handlerChange(e){
this.$emit("input", e.target.value)
// 如果有上面的重命名就是这样
this.$emit("updateValue", e.target.value)
}
}
}
</script>

4. ref


ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;


如果在子组件上,引用的指向就是子组件实例,然后父组件就可以通过 ref 主动获取子组件的属性或者调用子组件的方法


// Child.vue
export default {
data(){
return {
name:"沐华"
}
},
methods:{
someMethod(msg){
console.log(msg)
}
}
}

// Parent.vue
<template>
<child ref="child"></child>
</template>
<script>
export default {
mounted(){
const child = this.$refs.child
console.log(child.name) // 沐华
child.someMethod("调用了子组件的方法")
}
}
</script>

5. $emit / v-on


子组件通过派发事件的方式给父组件数据,或者触发父组件更新等操作


// Child.vue 派发
export default {
data(){
return { msg: "这是发给父组件的信息" }
},
methods: {
handleClick(){
this.$emit("sendMsg",this.msg)
}
},
}
// Parent.vue 响应
<template>
<child v-on:sendMsg="getChildMsg"></child>
// 或 简写
<child @sendMsg="getChildMsg"></child>
</template>

export default {
methods:{
getChildMsg(msg){
console.log(msg) // 这是父组件接收到的消息
}
}
}

6. $attrs / $listeners


多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时


$attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合。通过 this.$attrs 获取父作用域中所有符合条件的属性集合,然后还要继续传给子组件内部的其他组件,就可以通过 v-bind="$attrs"


$listeners:包含父作用域里 .native 除外的监听事件集合。如果还要继续传给子组件内部的其他组件,就可以通过 v-on="$linteners"


使用方式是相同的


// Parent.vue
<template>
<child :name="name" title="1111" ></child>
</template
export default{
data(){
return {
name:"沐华"
}
}
}

// Child.vue
<template>
// 继续传给孙子组件
<sun-child v-bind="$attrs"></sun-child>
</template>
export default{
props:["name"], // 这里可以接收,也可以不接收
mounted(){
// 如果props接收了name 就是 { title:1111 },否则就是{ name:"沐华", title:1111 }
console.log(this.$attrs)
}
}

7. $children / $parent


$children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等


$parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等


// Parent.vue
export default{
mounted(){
this.$children[0].someMethod() // 调用第一个子组件的方法
this.$children[0].name // 获取第一个子组件中的属性
}
}

// Child.vue
export default{
mounted(){
this.$parent.someMethod() // 调用父组件的方法
this.$parent.name // 获取父组件中的属性
}
}

8. provide / inject


provide / inject 为依赖注入,说是不推荐直接用于应用程序代码中,但是在一些插件或组件库里却是被常用,所以我觉得用也没啥,还挺好用的


provide:可以让我们指定想要提供给后代组件的数据或方法


inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用


要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象


所以建议还是传递一些常量或者方法


// 父组件
export default{
// 方法一 不能获取 methods 中的方法
provide:{
name:"沐华",
age: this.data中的属性
},
// 方法二 不能获取 data 中的属性
provide(){
return {
name:"沐华",
someMethod:this.someMethod // methods 中的方法
}
},
methods:{
someMethod(){
console.log("这是注入的方法")
}
}
}

// 后代组件
export default{
inject:["name","someMethod"],
mounted(){
console.log(this.name)
this.someMethod()
}
}

9. EventBus


EventBus 是中央事件总线,不管是父子组件,兄弟组件,跨层级组件等都可以使用它完成通信操作


定义方式有三种


// 方法一
// 抽离成一个单独的 js 文件 Bus.js ,然后在需要的地方引入
// Bus.js
import Vue from "vue"
export default new Vue()

// 方法二 直接挂载到全局
// main.js
import Vue from "vue"
Vue.prototype.$bus = new Vue()

// 方法三 注入到 Vue 根对象上
// main.js
import Vue from "vue"
new Vue({
el:"#app",
data:{
Bus: new Vue()
}
})

使用如下,以方法一按需引入为例


// 在需要向外部发送自定义事件的组件内
<template>
<button @click="handlerClick">按钮</button>
</template>
import Bus from "./Bus.js"
export default{
methods:{
handlerClick(){
// 自定义事件名 sendMsg
Bus.$emit("sendMsg", "这是要向外部发送的数据")
}
}
}

// 在需要接收外部事件的组件内
import Bus from "./Bus.js"
export default{
mounted(){
// 监听事件的触发
Bus.$on("sendMsg", data => {
console.log("这是接收到的数据:", data)
})
},
beforeDestroy(){
// 取消监听
Bus.$off("sendMsg")
}
}

10. Vuex


Vuex 是状态管理器,集中式存储管理所有组件的状态。这一块内容过长,如果基础不熟的话可以看这个Vuex,然后大致用法如下


比如创建这样的文件结构


微信图片_20210824003500.jpg


index.js 里内容如下


import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
import state from './state'
import user from './modules/user'

Vue.use(Vuex)

const store = new Vuex.Store({
modules: {
user
},
getters,
actions,
mutations,
state
})
export default store

然后在 main.js 引入


import Vue from "vue"
import store from "./store"
new Vue({
el:"#app",
store,
render: h => h(App)
})

然后在需要的使用组件里


import { mapGetters, mapMutations } from "vuex"
export default{
computed:{
// 方式一 然后通过 this.属性名就可以用了
...mapGetters(["引入getters.js里属性1","属性2"])
// 方式二
...mapGetters("user", ["user模块里的属性1","属性2"])
},
methods:{
// 方式一 然后通过 this.属性名就可以用了
...mapMutations(["引入mutations.js里的方法1","方法2"])
// 方式二
...mapMutations("user",["引入user模块里的方法1","方法2"])
}
}

// 或者也可以这样获取
this.$store.state.xxx
this.$store.state.user.xxx

11. $root


$root 可以拿到 App.vue 里的数据和方法


12. slot


就是把子组件的数据通过插槽的方式传给父组件使用,然后再插回来


// Child.vue
<template>
<div>
<slot :user="user"></slot>
</div>
</template>
export default{
data(){
return {
user:{ name:"沐华" }
}
}
}

// Parent.vue
<template>
<div>
<child v-slot="slotProps">
{{ slotProps.user.name }}
</child>
</div>
</template>

结语


写作不易,你的一赞一评,就是我前行的最大动力。


链接:https://juejin.cn/post/6999687348120190983

收起阅读 »

前端9种图片格式基础知识, 你应该知道的

彩色深度 彩色深度标准通常有以下几种: 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约...
继续阅读 »

彩色深度


彩色深度标准通常有以下几种:



  • 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。

  • 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。

  • 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约1680万种颜色。

  • 32位真彩色,即在24位真彩色图像的基础上再增加一个表示图像透明度信息的Alpha通道。

    32位真彩色并非是2的32次方的色数,它其实也是1677万多色,不过它增加了256阶颜色的灰度,为了方便称呼,就规定它为32位色


图的分类


光栅图和矢量图


对于图片,一般分光栅图和矢量图。



  • 光栅图:是基于 pixel像素构成的图像。JPEG、PNG,webp等都属于此类

  • 矢量图:使用点,线和多边形等几何形状来构图,具有高分辨率和缩放功能. SVG就是一种矢量图。


无压缩, 无损压缩, 有损压缩


另一种分类




  • 无压缩。无压缩的图片格式不对图片数据进行压缩处理,能准确地呈现原图片。BMP格式就是其中之一。




  • 无损压缩。压缩算法对图片的所有的数据进行编码压缩,能在保证图片的质量的同时降低图片的尺寸。png是其中的代表。




  • 有损压缩。压缩算法不会对图片所有的数据进行编码压缩,而是在压缩的时候,去除了人眼无法识别的图片细节。因此有损压缩可以在同等图片质量的情况下大幅降低图片的尺寸。其中的代表是jpg。




前端9种图片格式


诞生时间


对于超过30岁的程序员来说,她们都很年轻,真的是遇到好时光!


85年前,人们都在干嘛呢?



  1. GIF - 1987

  2. Base64- 1987

  3. JPEG - 1992

  4. PNG - 1996

  5. SVG - 1999

  6. JPEG2000 - 1997 to 2000

  7. APNG - 2004

  8. WebP - 2010


ico: 1985年??

查阅文档说ico文件格式是伴随着 Windows 1.0 发行诞生的。


GIF


GIF是一种索引色模式图片,所以GIF每帧图所表现的颜色最多为256种。GIF能够支持动画,也能支持背景透明,这点连古老的IE6都支持,所以在以前想要在项目中使用背景透明图片,其中一种方案就是生成GIF图片。


优点



  • 支持动画和透明背景

  • 兼容性好

  • 灰度图像表现佳

  • 支持交错

    部分接收到的文件可以以较低的质量显示。这在网络连接缓慢时特别有用。


缺点



  • 最多支持 8 位 256 色,色阶过渡糟糕,图片具有颗粒感

  • 支持透明,但不支持半透明,边缘有杂边


适用场景



  • 色彩简单的logo、icon、线框图适合采用gif格

  • 动画


JPG/JPEG


这里提个问题: jpg和jpeg有啥区别


平常我们大部分见到的静态图基本都是这种图片格式。这种格式的图片能比较好的表现各种色彩,主要在压缩的时候会有所失真,也正因为如此,造就了这种图片格式体积的轻量。


优点



  • 压缩率高

  • 兼容性好

  • 色彩丰富


缺点



  • JPEG不适合用来存储企业Logo、线框类的这种高清图

  • 不支持动画、背景透明


JPEG 2000 (了解即可)


JPEG 2000是基于小波变换的图像压缩标准,由Joint Photographic Experts Group组织创建和维护。JPEG 2000通常被认为是未来取代JPEG(基于离散余弦变换)的下一代图像压缩标准。JPEG 2000文件的副档名通常为.jp2,MIME类型是image/jp2。


JPEG2000的压缩比更高,而且不会产生原先的基于离散余弦变换的JPEG标准产生的块状模糊瑕疵。JPEG2000同时支持有损压缩无损压缩


目前就safari支持,can is use-png2000支持18%。


优点



  • 支持有损和无损压缩


缺点



  • 支持率太低了


ICO


ICO (Microsoft Windows 图标)文件格式是微软为 Windows 系统的桌面图标设计的。网站可以在网站的根目录中提供一个名为 favicon.ICO, 在收藏夹菜单中显示的图标,以及其他一些有用的标志性网站表示形式。

一个 ICO 文件可以包含多个图标,并以列出每个图标详细信息的目录开始。


其主要用来做网站图标,现在png也是可以用来做网站图标的。


PNG


PNG格式是有三种版本的,分别为PNG-8,PNG-24,PNG-32,所有这些版本都不支持动画的。PNG-8跟GIF类似的属性是相似的,都是索引色模式,而且都支持背景透明。相对比GIF格式好的特点在与背景透明时,图像边缘没有什么噪点,颜色表现更优秀。PNG-24其实就是无损压缩的JPEG。而PNG-32就是在PNG-24的基础上,增加了透明度的支持。


如果没有动画需求推荐使用png-8来替代gif


优点



  1. 不失真的情况下尽可能压缩图像文件的大小

  2. 像素丰富

  3. 支持透明(alpha通道)


缺点



  1. 文件大


这里额外提一下,gif和jpg有渐进,png有交错,都是在没有完全下载图片的时候,能看到图片全貌。


具体可以看在线示例: png正常,png交错,jpg渐进


APNG:Animated PNG


APNG(Animated Portable Network Graphics)顾名思义是基于 PNG 格式扩展的一种动画格式,增加了对动画图像的支持,同时加入了 24 位图像和 8 位 Alpha 透明度的支持,这意味着动画将拥有更好的质量,其诞生的目的是为了替代老旧的 GIF 格式,但它目前并没有获得 PNG 组织官方的认可。


从Can I Use上查看,除了IE系列, chrome, firefox, safari均已支持。2021-08月的时候支持达到94%。


相对GIF来说



  • 色彩丰富

  • 支持透明

  • 向下兼容 PNG

  • 支持动画


缺点



  • 生成比较繁琐

  • 未标准化


webP


有损 WebP 图像平均比视觉上类似压缩级别的 JPEG 图像小25-35% 。无损耗的 WebP 图像通常比 PNG 格式的相同图像小26% 。WebP 还支持动画: 在有损的 WebP 文件中,图像数据由 VP8位流表示,该位流可能包含多个帧。


包括体积小、色彩表现足够、支持动画。 简直了就是心中的完美女神!!


can i use - webp上看,支持率95%。 主要是Safari低版本和IE低版本不兼容。


优点



  • 同等质量更小

  • 压缩之后质量无明显变化

  • 支持无损图像

  • 支持动画


缺点



  • 兼容性吧,相对jpg,png,gif来说


SVG


SVG 是一种基于 xml 的矢量图形格式,它将图像的内容指定为一组绘图命令,这些命令创建形状、线条、应用颜色、过滤器等等。SVG 文件是理想的图表,图标和其他图像,可以准确地绘制在任何大小。因此,SVG 是现代 Web 设计中用户界面元素的流行选择。


优点



  • 可伸缩性

    你可以随心所欲地把它们做大或者做小,而不用牺牲质量



  • Svg 平均比 GIF、 JPEG、 PNG 小得多,甚至在极高的分辨率下也是如此

  • 支持动画

    更灵活,质量无与伦比

  • 与DOM无缝衔接

    Svg 可以直接使用 HTML、 CSS 和 JavaScript (例如动画)来操作


缺点



  • SVG复杂度高会减慢渲染速度

  • 不适合游戏类等高互动动画


base64


图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址,图片随着 HTML 的下载同时下载到本地,不再单独消耗一个http来请求图片。


优点



  • 无额外请求

  • 对于极小或者极简单图片

  • 可像单独图片一样使用,比如背景图片重复使用等

  • 没有跨域问题,无需考虑缓存、文件头或者cookies问题  


缺点



  • 相比其他格式,体积会至少大1/3

  • 编码解码有额外消耗


一些对比


PNG, GIF, JPG 比较


大小比较:通常地,PNG ≈ JPG > GIF 8位的PNG完全可以替代掉GIF

透明性:PNG > GIF > JPG

色彩丰富程度:JPG > PNG >GIF

兼容程度:GIF ≈ JPG > PNG

gif, jpg, png, web优缺点和使用场景



链接:https://juejin.cn/post/7000154907156152327

收起阅读 »

Why | 为什么需要虚拟内存?

冯-诺依曼老爷子告诉过我们,算术逻辑单元和控制器单元组成的 CPU 负责进行运算以及程序流程的控制。运算所需要的指令和数据由 内存 来提供。 那么,如果让你作为操作系统的顶层设计者,你会提供一种什么机制,让 CPU 可以从内存中获取指令和数据呢? 用 ...
继续阅读 »

冯-诺依曼老爷子告诉过我们,算术逻辑单元和控制器单元组成的 CPU 负责进行运算以及程序流程的控制。运算所需要的指令和数据由 内存 来提供。


Von_Neumann_Architecture.png


那么,如果让你作为操作系统的顶层设计者,你会提供一种什么机制,让 CPU 可以从内存中获取指令和数据呢?


用 C 语言写一个 Hello World,通过 objdump 查看汇编代码。我随便截取一行。


mov    0x200aed(%rip),%rax        # 200fe8 <__gmon_start__>

这一行汇编代码中包含了一个内存地址。这个内存地址是物理内存中的真实地址吗?


我们假设它就是真实的物理地址,但是程序员在编程时是无法得知要运行的设备的内存信息的,所以针对不同的操作系统,得在编译期将程序中的地址转换为真实物理地址。这在单道编程的情况下可行,对于多道编程呢?不同的程序之间如何确定各自在内存中的位置?


从单道编程到多道编程是计算机发展前进的一大步。CPU 通过轮询时间片的方式让多个程序仿佛在同时运行。显然,在程序中使用真实的物理地址会打破这一幻像,不同的程序之间不得而知对方用的是哪一块物理内存,各自的内存完全无法得到保护。


所以,程序中的地址不能是真实的物理地址,但又要与真实的物理地址存在一定的映射关系 。我们把程序中的地址称为 虚拟地址 ,它至少应该具备以下特性:



  • 能通过一定的机制映射到真实的物理地址

  • 保证不同的程序(进程) 映射的真实物理地址之间互相独立

  • 它应该是自动工作的,对于程序开发者来说是透明的


基于这三个特性,我们一起来探究一下 虚拟内存 的工作方式。


一个萝卜一个坑,分段


最直观的解决方案,给每个程序分配一块独立的内存空间,如下图所示。


动态定位.png


对于每个程序来说,它的虚拟内存空间都从 0 开始,基址寄存器 中存储其在物理内存空间的起始地址。所以,物理地址和虚拟地址之间就存在这样的关系:


物理地址 = 虚拟地址 + 基址

这样的地址转换由叫做 内存管理单元(Memory Management Unit,MMU) 的硬件负责完成。


界限寄存器 可以存储程序占用内存的大小,也可以存储界限的物理地址,它提供基本的内存访问保护。如果 MMU 转换出的物理地址超过了界限,将会触发异常。每个 CPU 都有一对基址寄存器和界限寄存器,当发生进程切换时,更新寄存器的值,这样就做到了进程间内存独立。


乍一看,基本满足了虚拟内存的三个特性,但事实上基本没有操作系统会这么干。由于它需要在虚拟内存和物理内存中分别分配一块连续的内存空间,再进行内存映射。这样的缺点很明显。


第一,容易造成内存碎片。假设内存经过一段时间的使用,还剩下两块 128 MB 的小块,但此时用户需要运行一个内存占用 129 MB 的程序,在此机制下就无法成功分配内存。虽然可以通过内存交换,将内存拾掇拾掇,和磁盘换来换去,把空余内存拼接起来,但是这么大一块数据,磁盘读写的速度实在太慢了,性能上根本无法接受。


第二,浪费了很多内存空间。如果把二八法则搬到计算机上,一个程序最经常运行的代码可能两成都占不到。而上面的方案在一开始就要分配好整个程序需要的内存空间,堆和栈之间有一大块的内存是空闲的。


上面的方案暂且可以看成一种特殊的 “分段”。我们可以试着把段分的更细一些。


典型的 Linux 进程用户空间内存包含栈、共享库、堆、数据、代码等。我们可以按这些基本类型来分段,为了方便演示,下图中仅分为 栈、堆、代码 三个段。


分段.png


将程序按逻辑分为一段一段,放入内存中对应的段区域内,这样避免了之前的方案中堆和栈之间的空间浪费,真正需要内存的时候才会去申请。同时顺带实现了共享。对于一些可以公用的系统基本库,在之前的方案中仍然需要拷贝到各个进程独立的空间中。而分段的方案中,只需要一份拷贝就行,不同进程间的虚拟地址映射到这一份物理拷贝就可以了。


但是由于各个段的大小不一致,内存碎片的问题可能并不比上一个方案好到哪里去。


另外,上面提到的所有方案都没有考虑到程序大小的问题。如果程序大小大于物理内存,你再怎么分段也没有办法解决问题。


把段再分细一点,分页


为了解决分段产生的内存碎片问题,我们把段分的再细一些,细成一个一个固定大小的页面,虚拟内存和固定内存都是如此。这个固定大小在当前主流操作系统中一般是 4 KB ,部分系统也支持 8 KB、16 KB、64 KB。


将虚拟页和物理页一一对应起来,虚拟地址到物理地址的转换就不是难事了。


不论是虚拟内存还是物理内存,在分页之后,给每页拟定一个 页号,再根据 页内偏移量 就可以取到数据了。由于虚拟页和物理页的页大小是一致的,所以页内偏移量无需转换,只需要把虚拟页号转换为物理页号就可以了。


而虚拟地址正是由 虚拟页号页内偏移量 组成。


操作系统将虚拟页号到物理页号的映射关系保存在 页表 中,页表是一个 页表项(PTE) 的数组,页表项包含了有效位,物理地址等数据。页表直接使用虚拟页号作为索引,找到对应的页表项。


分页.png


上图中的第 3 个虚拟页被映射到了第 2 个物理页。其实 虚拟页可以被映射到任意物理页,连续的虚拟页也不需要对应连续的物理页,这给了操作系统很大的自由。不仅相对减少了内存碎片的产生,也能更方便的实现进程间的数据共享,只要将不同进程的虚拟页映射到同样的物理页就行了。


为了能直接使用虚拟页号作为索引检索到页表项,页表中的所有页表项必须连续的,并且要提前创建好。那么问题来了,页表有多大?


以 32 位操作系统为例,最大寻址空间为 2 ^ 32 = 4 GB,页的大小为 4 KB,所以共需要 1 M 个页表项。每个页表项大小为 4 个字节,所以一个页表的大小为 1 M * 4 B = 4 MB 。为实现进程隔离,每个进程又必须有自己独立的页表。顺手看一下你的操作系统,至少都得有上百个进程在同时运行,光页表就得占用几百兆内存,这显然是不合适的。


实际上,对大多数程序来说,并不需要占用全部的 4 GB 虚拟内存,所以没有必要在一开始就分配完整个页表。使用多级页表可以解决这个问题。


时间换空间,多级页表


还是以 32 位操作系统为例,来看个简单的二级页表。


二级页表.png


第一级叫 页目录项 ,共有 1 K 项。每一个页目录项又对应着 1 K 个 页表项,总共 1 K * 1 K = 1 M 个页表项,正好对应着 4 GB 的寻址空间。


对于 32 位的虚拟地址来说,正好 10 位对应着 1 K 个页目录项索引,10 位对应着指定页目录项下的 1 K 个页表项索引,剩下 12 位正好对应页大小 4 KB 的页内偏移量。


算一下二级页表的大小。1 K 个一级页目录项一共 4 KB,1 M 个二级页表项一共 4 MB ,加起来一共 4.004 MB


所以,二级页表比普通页表占用的内存还要大?其实并不然。


首先得明确一点,不管是几级页表,都必须要能覆盖整个虚拟空间。对于只有一级的普通页表来说,一上来就得初始化所有页表项,才能覆盖到整个虚拟空间地址。而对于二级页表来说,1 K 个一级的页目录项就可以足以覆盖,二级页表项只有在需要的时候才被创建。这样就可以节省相当一部分内存。


另外,二级页表可以不存储在内存中,而是存在磁盘中。这倒并不是专门为多级页表而设计的,这是虚拟内存分页的特性,也正因如此,程序的大小可以大于实际物理内存的大小。


页命中和缺页


回想一下之前描述的寻址过程。虚拟地址经过内存管理单元 MMU 的处理,找到对应的页表项 PTE ,转换为物理地址,然后在物理内存中定位到对应的数据。这种理想的情况叫做 页命中 ,根据虚拟地址直接就可以在内存中获取到数据。


但是,并不是任何时候都可以直接根据 PTE 在内存中拿到数据的。最典型的情况,程序的大小大于物理内存,必然会有数据不存在内存中。另外,由于多级页表并不是开始就创建,所以 PTE 对应的数据可能也不在内存中。


在任意时刻,虚拟内存页都可以分为三个状态:



  • 未分配的:还未分配(或创建)的页。没有任何数据与其关联,不占用任何磁盘空间

  • 已缓存的:当前已缓存在物理内存中的已分配页

  • 未缓存的:未缓存在物理内存中的已分配页


只有已缓存的虚拟页可以发生页命中,实际上 PTE 会有一个有效位来表示页表是否有效,假设 0 表示有效,1 表示无效。


有效位为 0,表示 PTE 可用,直接读数据即可。有效位为 1,在不考虑非法内存地址的情况下,可以认为是未分配或者未缓存,无法直接从内存中读取数据,这种情况称为 缺页


一旦发生缺页,将由系统的缺页异常处理程序来接管,它会根据特定算法从内存中寻找一个 牺牲页,如果该牺牲页数据被修改过,要先写回磁盘,然后将需要的页换到该牺牲页的位置,并更新 PTE。当异常处理程序返回时,它会重新执行之前导致缺页的命令,也就是之前的寻址操作,这次就直接页命中了。


看到这,你会发现缺页是一个非常昂贵的操作,操作系统必须尽量减少缺页的发生,所以如何寻找合适的牺牲页是个大问题。如果你替换了一个即将要访问的页,那么一会又得把它换回来,这样频繁的换来换去是无法接受的。关于具体的替换算法,可以阅读 《操作系统导论》第22章 超越物理内存:策略


缺页.png


给页表加一层缓存,TLB


再说回到页表,将虚拟地址转换为物理地址,如果使用未分级的普通页表只需要一次内存访问,但占用内存较大。大多数操作系统使用的是多级页表,例如目前的 64 位 Linux 操作系统,使用的是 四级页表,内存占用小了很多,但付出的代价是要访问四次内存。其实这就是一个 时间换空间 的策略。


另外,程序执行时的一连串指令的虚拟地址是连续的,相连几个虚拟地址通常是在一个虚拟页中,自然而然它们都对应着同一个物理页。但是无论页表如何设计,访问相邻的虚拟地址,每次仍然都要去访问页表。这里是一个可以优化的点。


计算机科学领域里的任何问题,都可以通过引入一个中间层来解决。


既要保留多级页表的低内存特性,又要避免多余的内存访问,那就再加一层 缓存 吧。


TLB(Translation Lookaside Buffer) ,有的资料翻译成 翻译后备缓冲器,有的翻译成 地址变换高速缓存,且不纠结这个名字。TLB 是封装在 CPU 里的一块缓存芯片,它就是页表的缓存,存储了虚拟地址和页表项的映射关系。


当进行地址转换时,第一步就是根据虚拟地址从 TLB 中查询是否存在对应的页表项 PTE 。如果 TLB 命中,就不用访问页表了,直接根据 TLB 中缓存的物理地址去 CPU Cache 或者内存取数据。如果 TLB 未命中,和缺页的处理流程类似,通过抛出一个异常,让 TLB 的异常处理程序来接手,它会去访问页表,找到对应的页表项,然后更新 TLB 。当异常处理程序执行完后,会再次执行原来的指令,这时候会命中 TLB 。可想而知, TLB 的命中率直接影响了操作系统运行的效率。


TLB.png


总结


先说说为什么写了这么一篇文章。


最近在读 《深入理解 Android 内核设计思想》Binder 相关章节的时候,发现对 mmap 没有一个深度认识的话,就很难理解 Binder 只复制一次的底层逻辑。而如果对虚拟内存机制又没有一个很好的理解的话,也很难去理解 mmap 的实现原理。算是一环扣一环,倒逼学习了一波。


其实编程领域的很多问题,归根到底都是针对计算机操作系统的特性,做出的解决方案和妥协。打好操作系统的扎实基础,对学习任何编程相关的知识,都是大有裨益的。但另一方面,操作系统的知识也多且杂,我也不敢保证我这篇文章没有任何错误。如果你对上面的内容有不同意见,欢迎评论区和我探讨。


最后,一张图总结虚拟内存的工作机制。



虚拟内存.png

收起阅读 »

【开源项目】Compose仿豆瓣榜单客户端,了解一下~

前言 Compose正式发布也有一段时间了,感觉要上手还是得实战一波。 所以借着空闲时间,参照豆瓣榜单页面的设计,开发了几个Compose版的豆瓣榜单页面 UI效果还是挺好看的,有兴趣的同学可以点个Star:Compose仿豆瓣榜单客户端 效果图 首先看...
继续阅读 »

前言


Compose正式发布也有一段时间了,感觉要上手还是得实战一波。
所以借着空闲时间,参照豆瓣榜单页面的设计,开发了几个Compose版的豆瓣榜单页面
UI效果还是挺好看的,有兴趣的同学可以点个Star:Compose仿豆瓣榜单客户端


效果图


首先看下最终的效果图


douban_compress.gif


特性


在项目中主要用到了以下几个特性,以美化UI及体验



  1. 支持设置沉浸式状态栏及状态栏颜色

  2. 支持水平方向滚动,竖直方向滚动等多种UI效果

  3. 支持给Image设置渐变滤镜,以美化显示效果

  4. 支持标题与列表页联动

  5. 通过Paging支持了分页加载


主要实现


具体源码可以直接查看,这里主要介绍一些主要功能的实现


沉浸式状态栏设置


状态栏主要是通过accompanist-insetsaccompanist-systemuicontroller库设置的
accompanist上提供了一系列常用的,如状态栏,权限,FlowLayout,ViewPagerCompose
如果有时你发现基础库里没有相应的内容,可以去这里查找下


设置状态栏主要分为以下几步



  1. 设置沉浸时状态栏

  2. 获取状态栏高度

  3. 设置状态栏颜色


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 设置状态栏沉浸式
WindowCompat.setDecorFitsSystemWindows(window, false)

setContent {
BD_ToolTheme {
// 加入ProvideWindowInsets
ProvideWindowInsets {
// 2. 设置状态栏颜色
rememberSystemUiController().setStatusBarColor(
Color.Transparent, darkIcons = MaterialTheme.colors.isLight)
Column {
// 3. 获取状态栏高度并设置占位
Spacer(modifier = Modifier
.statusBarsHeight()
.fillMaxWidth())
Text(text = "首页\r\n首页1\r\n首页2\r\n首页3")
}
}
}
}
}

通过以上方法,就可以比较简单的实现沉浸状态栏的设置


Image设置渐变滤镜


豆瓣榜单页面都给Image设置了渐变滤镜,以美化UI效果
其实实现起来也比较简单,给Image前添加一层渐变的蒙层即可


@Composable
fun TopRankItem(item: HomeTopRank) {
Box(
modifier = Modifier
.size(180.dp, 220.dp)
.padding(8.dp)
.clip(RoundedCornerShape(10.dp))
) {
// 1. 图片
Image(
painter = rememberCoilPainter(request = item.imgUrl),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Column(
modifier = Modifier
.fillMaxSize()
// 渐变滤镜
.background(
brush = Brush.linearGradient(
colors = listOf(Color(item.startColor), Color(item.endColor)),
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
)
.padding(8.dp)

) {
//内容
}
}
}

如上所示,使用Box布局,给前景设置一个从左下到右上渐变的背景即可


标题与列表联动


具体效果可见上面的动图,即在列表滚动时标题会有一个渐现渐隐效果
这个效果其实我们在Android View体系中也很常见,主要思路也很简单:



  1. 监听列表滚动,获取列表滚动offset

  2. 根据列表滚动offset设置Header效果,如背景或者高度变化等


@Composable
fun RankScreen(viewModel: RankViewModel = RankViewModel()) {
val scrollState = rememberLazyListState()
Box {
// 1. 监听列表
LazyColumn(state = scrollState) {
//列表内容
}
RankHeader(scrollState)
}
}

@Composable
fun RankHeader(scrollState: LazyListState) {
val target = LocalDensity.current.run {
200.dp.toPx()
}
// 2. 根据列表偏移量计算比例
val scrollPercent: Float = if (scrollState.firstVisibleItemIndex > 0) {
1f
} else {
scrollState.firstVisibleItemScrollOffset / target
}
val activity = LocalContext.current as Activity
val backgroundColor = Color(0xFF7F6351)
Column() {
Spacer(
modifier = Modifier
.fillMaxWidth()
.statusBarsHeight()
// 3. 根据比例设置Header的alpha,以实现渐变效果
.alpha(scrollPercent)
.background(backgroundColor)
)
//....
}
}

如上所示,主要有三步:



  1. 监听列表

  2. 根据列表偏移量计算比例

  3. 根据比例设置Headeralpha,以实现渐变效果


利用Paging实现分页


目前Pagin3已经支持了Compose,我们可以利用Paging轻松实现分页效果
主要分为以下几步:



  1. ViewModel中设置数据源

  2. 在页面中监听Paging数据

  3. 根据加载状态设置加载更多footr状态


//1. 设置数据源
class RankViewModel : ViewModel() {
val rankItems: Flow<PagingData<RankDetail>> =
Pager(PagingConfig(pageSize = 10, prefetchDistance = 1)) {
MovieSource()
}.flow
}

@Composable
fun RankScreen(viewModel: RankViewModel = RankViewModel()) {
val lazyMovieItems = viewModel.rankItems.collectAsLazyPagingItems()
Box {
LazyColumn(state = scrollState) {
// 2. 在页面中监听paging
items(lazyMovieItems) {
it?.let {
RankListItem(it)
}
}
// 3. 根据paging状态设置加载更多footer状态等
lazyMovieItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item { LoadingItem() }
}
}
}
}
}
}

通过以上步骤,就可以比较简单方便地实现分页了


总结


项目地址


ComposeDouban
开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~

收起阅读 »

如何优雅的在业务中使用设计模式(代码如诗)

前言 有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。 当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&...
继续阅读 »

前言



有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。


当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&、*、as_mut、as_ref。。。让人头秃。。。


之前看到过一句话,觉得很不错:学习Rust并不会给你带来智商上的优越感,但或许会让你重新爱上编程。



大家如果阅读过一些开源框架的源码,可能会发现其中数不尽的抽象类,设计模式拈手而来,在功能框架中,可以使用设计模式随心所欲的解耦;在实际的复杂业务中,当然也可以应用合适的设计模式。


这篇文章,我会结合较为常见的实际业务场景,探讨如何使用合适的设计模式将业务解耦



  • 此处的应用绝不是生搬硬套,是我经过深思熟虑,并将较为复杂的业务进行全面重构后,得出的一套行之有效的思路历程

  • 任何一个设计模式都是一个伟大的经验及其思想总结,千人千面,如果对文章中内容,有不同的意见,希望你能在评论中提出,我们共同探讨,共同进步


本文章是一篇弱代码类型文章,我会画大量的图片向大家展示,引用设计模式后,会对原有的业务流程,产生什么样的影响。


前置知识


这里,需要了解下基础知识,什么是责任链模式和策略模式


责任链模式,在很多开源框架中都是有所应用,你如果听到啥啥拦截器,基本就是责任链模式,责任链模式的思想很简单,但是有很多种实现方式



  • 最简单的链表实现就和OkHttp的拦截器实现大相径庭

  • OkHttp的拦截器实现和Dio拦截器实现结构相同,但遍历方式不一样

  • 很多骚操作:我喜欢OkHttp的实现方式,喜欢dio的Api设计,结尾会给出一个结合这俩者思想的通用拦截器


策略模式,或是天生适合业务,同一模块不同类型业务,如果行为相同,或许就可以考虑使用策略模式去解耦了


责任链模式


这边用Dart写一个简单的拦截器,dart和java非常像



  • 为了减少语言差异,我就不使用箭头语法了

  • 下划线表示私有


用啥语言不重要,这边只是用代码简单演示下思想


此处实现就用链表了;如果,使用数组的形式,需要多写很多逻辑,数组的优化写法在结尾给出,此处暂且不表


结构



  • 责任链的结构,通常有俩种结构

    • 链表结构:链表构建责任链,十分便捷的就能和下一节点建立联系

    • 数组结构:数组,用通用的List即可,方便增删,不固定长度(别费劲的用固定长度Array了,例如:int[]、String[])



责任链结构



  • 实现一个链表实体很简单


abstract class InterceptChain<T> {
InterceptChain? next;

void intercept(T data) {
next?.intercept(data);
}
}

实现



  • 拦截器实现


/// 该拦截器以最简单的链表实现
abstract class InterceptChain<T> {
InterceptChain? next;

void intercept(T data) {
next?.intercept(data);
}
}

class InterceptChainHandler<T> {
InterceptChain? _interceptFirst;

void add(InterceptChain interceptChain) {
if (_interceptFirst == null) {
_interceptFirst = interceptChain;
return;
}

var node = _interceptFirst!;
while (true) {
if (node.next == null) {
node.next = interceptChain;
break;
}
node = node.next!;
}
}

void intercept(T data) {
_interceptFirst?.intercept(data);
}
}


  • 使用

    • 调整add顺序,就调整了对应逻辑的节点,在整个责任链中的顺序

    • 去掉intercept重写方法中的super.intercept(data),就能实现拦截后续节点逻辑



void main() {
var intercepts = InterceptChainHandler<String>();
intercepts.add(OneIntercept());
intercepts.add(TwoIntercept());
intercepts.intercept("测试拦截器");
}

class OneIntercept extends InterceptChain<String> {
@override
void intercept(String data) {
data = "$data:OneIntercept";
print(data);
super.intercept(data);
}
}

class TwoIntercept extends InterceptChain<String> {
@override
void intercept(String data) {
data = "$data:TwoIntercept";
print(data);
super.intercept(data);
}
}


  • 打印结果


测试拦截器:OneIntercept
测试拦截器:OneIntercept:TwoIntercept

策略模式


结构



  • 策略模式最重要的:应该就是对抽象类的设计,对行为的抽象


策略模式应用


实现



  • 定义抽象类,抽象行为


/// 结合适配器模式的接口适配:抽象必须实现行为,和可选实现行为
abstract class BusinessAction {
///创建相应资源:该行为必须实现
void create();

///可选实现
void dealIO() {}

///可选实现
void dealNet() {}

///可选实现
void dealSystem() {}

///释放资源:该行为必须实现
void dispose();
}


  • 实现策略类


//Net策略
class NetStrategy extends BusinessAction {
@override
void create() {
print("创建Net资源");
}

@override
void dealNet() {
print("处理Net逻辑");
}

@override
void dispose() {
print("释放Net资源");
}
}

///IO策略
class IOStrategy extends BusinessAction {
@override
void create() {
print("创建IO资源");
}

@override
void dealIO() {
print("处理IO逻辑");
}

@override
void dispose() {
print("释放IO资源");
}
}


  • 使用


void main() {
var type = 1;
BusinessAction strategy;

//不同业务使用不同策略
if (type == 0) {
strategy = NetStrategy();
} else {
strategy = IOStrategy();
}

//开始创建资源
strategy.create();
//......... 省略N多逻辑(其中某些场景,会有用到Net业务,和上面type是关联的)
//IO业务:开始处理业务
strategy.dealIO();
//......... 省略N多逻辑
//释放资源
strategy.dispose();
}


  • 结果


创建IO资源
处理IO逻辑
释放IO资源

适合的业务场景


这边举一些适合上述设计模式的业务场景,这些场景是真实存在的!


这些真实的业务,使用设计模式解耦和纯靠if else怼,完全是俩种体验!


代码如诗,这并不是一句玩笑话。


连环弹窗业务


业务描述


连环弹窗夺命call来袭。。。



  • A弹窗弹出:有确定和取消按钮

    • 确定按钮:B弹窗弹出(有查看详情和取消按钮)

      • 查看详情按钮:C弹窗弹出(有同意和拒绝按钮)

        • 同意按钮:D弹窗弹出(有查看和下一步按钮)

          • 查看按钮:E弹窗弹出(只有下一步按钮)

            • 下一步按钮:F弹窗弹出(结束)


          • 下一步按钮:F弹窗弹出(结束)


        • 拒绝按钮:流程结束


      • 取消按钮:流程结束


    • 取消按钮:流程结束



好家伙,套娃真是无所不在,真不是我们代码套娃,实在是业务套娃,手动滑稽.png


img



  • 图示弹窗业务


连环弹窗业务1


直接开搞


看到这个业务,大家会去怎么做呢?



  • 有人可能会想,这么简单的业务还需要想吗?直接写啊!

    • A:在确定回调里面,跳转B弹窗

    • B:查看详情按钮跳转C弹窗

    • 。。。


  • 好一通套后,终于写完了



产品来了,加需求


B和C弹窗之间要加个预览G弹窗,点击B的查看详情按钮,跳转预览G弹窗;预览G弹窗只有一个确定按钮,点击后跳转C弹窗



img



  • 你心里可能要想了,这特么不是坑爹?

    • 业务本来就超吉尔套,我B弹窗里面写的跳转代码要改,传参要改,而且还要加弹窗!


  • 先要去找产品撕比,撕完后

    • 然后继续在屎山上,小心翼翼的再拉了坨shit

    • 这座克苏鲁山初成规模



连环弹窗业务2



产品又来了,第一稿需求不合理,需要调整需求


交换C和D弹窗位置,D弹窗点击下一步的时候,需要加一个校验请求,通过后才能跳转到C弹窗



img



  • 你眉头一皱,发现事情没有表面这么简单

    • 由于初期图简单,几乎都写在一个文件里,眼花缭乱弹窗回调太多,而且弹窗样式也不一样

    • 现在改整个流程,导致你整个人脑子嗡嗡响


  • 心中怒气翻涌,找到产品说


img



  • 回来,坐在椅子上,心里想:

    • 老夫写的代码天衣无缝,这什么几把需求

    • 可恶,这次测试,起码要给我多提十几个BUG



image-20210822215435299



  • 克苏鲁山开始狰狞


连环弹窗业务3



产品飘来,加改需求:如此,如此,,,这般,这般,,,




  • 你....


img



产品:改下,,,然后,扔给你几十页的PRD



你看了看这改了几十版的克苏鲁山,这几十个弹窗逻辑居然都写在一个文件里,快一万行的代码。。。



  • 心里不禁想:

    • 本帅比写的代码果然牛批,或许这就是艺术!艺术总是曲高和寡,难被人理解!而我的代码更牛批,连我自己都看不懂了!

    • 这代码行数!这代码结构!不得拍个照留念下,传给以后的孩子当传家宝供着!


  • 心里不禁嘚瑟:

    • 这块业务,除了我,还有谁敢动,成为头儿的心腹,指日可待!



16c3-ikhvemy5945899



  • 但,转念深思后:事了拂衣去,深藏功与名


img


重构



随着业务的逐渐复杂,最初的设计缺点会逐渐暴露;重构有缺陷的代码流程,变得势在必行,这会极大的降低维护成本



如果心中对责任链模式有一些概念的话,会发现上面的业务,极其适合责任链模式!


对上面的业务进行分析,可以明确一些事



  • 这个业务是一个链式的,有着明确的方向性:单向,从头到尾指向

  • 业务拆分开,可以将一个弹窗作为单颗粒度,一个弹窗作为节点

  • 上级的业务节点可以对下级节点拦截(点击取消,拒绝按钮,不再进行后续业务)


重构上面的代码,只要明确思想和流程就行了



第一稿业务




  • 业务流程


连环弹窗业务1



  • 责任链


责任链业务1



  • 代码:简写


void main() {
var intercepts = InterceptChainHandler<String>();
intercepts.add(AIntercept());
intercepts.add(BIntercept());
intercepts.add(CIntercept());
intercepts.add(DIntercept());
intercepts.add(EIntercept());
intercepts.add(FIntercept());
intercepts.intercept("测试拦截器");
}


第二稿业务




  • 业务流程


连环弹窗业务2



  • 责任链


责任链业务2



  • 代码:简写


void main() {
var intercepts = InterceptChainHandler<String>();
intercepts.add(AIntercept());
intercepts.add(BIntercept());
intercepts.add(GIntercept());
intercepts.add(CIntercept());
intercepts.add(DIntercept());
intercepts.add(EIntercept());
intercepts.add(FIntercept());
intercepts.intercept("测试拦截器");
}


第三稿业务




  • 业务流程


连环弹窗业务3



  • 责任链


责任链业务3



  • 代码:简写


void main() {
var intercepts = InterceptChainHandler<String>();
intercepts.add(AIntercept());
intercepts.add(BIntercept());
intercepts.add(GIntercept());
intercepts.add(DIntercept());
intercepts.add(CIntercept());
intercepts.add(EIntercept());
intercepts.add(FIntercept());
intercepts.intercept("测试拦截器");
}


总结



经过责任链模式重构后,业务节点被明确的区分开,整个流程从代码上看,都相当的清楚,维护将变的异常轻松;或许,此时能感受到一些,编程的乐趣了


img


花样弹窗业务


业务描述


来描述一个新的业务:这个业务场景真实存在某办公软件



  • 进入APP首页后,和后台建立一个长连接

  • 后台某些工单处理后,会通知APP处理,此时app会弹出处理工单的弹窗(app顶部)

  • 弹窗类型很多:工单处理弹窗,流程审批弹窗,邀请类型弹窗,查看工单详情弹窗,提交信息弹窗。。。

  • 弹窗弹出类型,是根据后台给的Type进行判断:从而弹出不同类型弹窗、点击其按钮,跳转不同业务,传递不同参数。


花样弹窗业务


分析



确定设计



这个业务,是一种渐变性的引导你搭建克苏鲁代码山



  • 在前期开发的时候,一般只有俩三种类型弹窗,前期十分好做;根本不用考虑如何设计,抬手一行代码,反手一行代码,就能搞定

  • 但是后来整个业务会渐渐的鬼畜,不同类型会慢慢加到几十种之多!!!


首先这个业务,使用责任链模式,肯定是不合适的,因为弹窗之间的耦合性很低,并没有什么明确的上下游关系


但是,这个业务使用策略模式非常的合适!



  • type明确:不同类型弹出不同弹窗,按钮执行不同逻辑

  • 抽象行为明确:一个按钮就是一种行为,不同行为的实现逻辑大相径庭



抽象行为



多样弹窗的行为抽象,对应其按钮就行了


确定、取消、同意、拒绝、查看详情、我知道了、提交


直接画图来表示吧


花样弹窗业务-抽象行为


实现


来看下简要的代码实现,代码不重要,重要的是思想,这边简要的看下代码实现流程



  • 抽象基类


/// 默认实现抛异常,可提醒未实现方法被误用
abstract class DialogAction {
///确定
void onConfirm() {
throw 'DialogAction:not implement onConfirm()';
}

///取消
void onCancel() {
throw 'DialogAction:not implement onCancel()';
}

///同意
void onAgree() {
throw 'DialogAction:not implement onAgree()';
}

///拒绝
void onRefuse() {
throw 'DialogAction:not implement onRefuse()';
}

///查看详情
void onDetail() {
throw 'DialogAction:not implement onDetail()';
}

///我知道了
void onKnow() {
throw 'DialogAction:not implement onKnow()';
}

///提交
void onSubmit() {
throw 'DialogAction:not implement onSubmit()';
}
}


  • 实现逻辑类


class OneStrategy extends DialogAction {
@override
void onConfirm() {
print("确定");
}

@override
void onCancel() {
print("取消");
}
}

class TwoStrategy extends DialogAction{
@override
void onAgree() {
print("同意");
}

@override
void onRefuse() {
print("拒绝");
}
}

//........省略其他实现


  • 使用


void main() {
//根据接口获取
var type = 1;
DialogAction strategy;
switch (type) {
case 0:
strategy = DefaultStrategy();
break;
case 1:
strategy = OneStrategy();
break;
case 2:
strategy = TwoStrategy();
break;
case 3:
strategy = ThreeStrategy();
break;
case 4:
strategy = FourStrategy();
break;
case 5:
strategy = FiveStrategy();
break;
default:
strategy = DefaultStrategy();
break;
}

//聚合弹窗按钮触发事件
BusinessDialog(
//确定按钮
onConfirm: () {
strategy.onConfirm();
},
//取消按钮
onCancel: () {
strategy.onCancel();
},
//同意按钮
onAgree: () {
strategy.onAgree();
},
//拒绝按钮
onRefuse: () {
strategy.onRefuse();
},
//查看详情按钮
onDetail: () {
strategy.onDetail();
},
//我知道了按钮
onKnow: () {
strategy.onKnow();
},
//提交按钮
onSubmit: () {
strategy.onSubmit();
},
);
}


  • 图示


花样弹窗业务-业务流程


一个复杂业务场景的演变


我们看下,一个简单的提交业务流,怎么逐渐变的狰狞


我会逐渐给出一个合适的解决方案,如果大家有更好的想法,务必在评论区告诉鄙人


业务描述:我们的车子因不可抗原因坏了,要去维修厂修车,工作人员开始登记这个损坏车辆。。。


业务的演变



第一稿


初始业务



登记一个维修车辆的流程,实际上还是满麻烦的



  • 登记一个新车,需要将车辆详细信息登记清楚:车牌、车架、车型号、车辆类型、进出场时间、油量、里程。。。

  • 还需要登记一下用户信息:姓名、手机号、是否隶属公司。。。

  • 登记车损程度:车顶、车底、方向盘、玻璃、离合器、刹车。。。

  • 车内物品:车座皮套、工具。。。

  • 以及其他我没想到的。。。

  • 最后:提交所有登记好的信息


第一稿,业务流程十分清晰,细节复杂,但是做起来不难


车辆登记-第一稿



第二稿(实际是多稿聚合):增加下述几个流程


外部登记:外部登记了一个维修车辆部分信息(后台,微信小程序,H5等等),需要在app上完善信息,提交接口不同(必带车牌号)


快捷洗车:洗车业务极其常见,快捷生成对应信息,提交接口不同


预约订单登记:预约好了车辆一部分一些信息,可快捷登记,提交接口不同(必带车牌号)



因为登记维修车辆流程,登记车辆信息流程极其细致繁琐,我们决定复用登记新车模块



  • 因为此处逻辑大多涉及开头和结尾,中间登记车辆信息操作几乎未改动,复用想法是可行的

  • 如果增加车辆登记项,新的三个流程也必须提交这些信息;所以,复用势在必行


因为这一稿需求,业务也变得愈加复杂


车辆登记-第二稿



第三稿


现在要针对不同的车辆类型,做不同的处理;车类型分:个人车,集团车


不同类型的登记,在提交的时候,需要校验不同的信息;校验不通过,需要提示用户,并且不能进行提交流程


提交后,需要处理下通用业务,然后跳转到某个页面



第三稿的描述不多,但是,大大的增加了复杂度



  • 尤其是不同类型校验过程还不同,还能中断后续提交流程

  • 提交流程后,还需要跳转通用页面


车辆登记-第三稿


开发探讨


第一稿



  • 业务流程


车辆登记-第一稿



  • 开发


正常流程开发、、、


第二稿



  • 业务流程


车辆登记-第二稿



  • 思考


对于第二稿业务,可以好好考虑下,怎么去设计?


开头和结尾需要单独写判断,去处理不同流程的业务,这至少要写俩个大的判断模块,接受数据的入口模块可能还要写判断


这样就非常适合策略模式去做了


开头根据执行的流程,选择相应的策略对象,后续将逻辑块替换抽象的策略方法就OK了,大致流程如下


车辆登记-第二稿(策略模式)


第三稿



业务流程



车辆登记-第三稿



探讨




  • 第三稿的需求,实际上,已经比较复杂了



    • 整个流程中掺杂着不同业务流程处理,不同流程逻辑又拥有阻断下游机制(绿色模块)

    • 下游逻辑又会合流(结尾)的多种变换


  • 在这一稿的需求



    • 使用策略模式肯定是可以的

    • 阻断那块(绿色模块)需要单独处理下:抽象方法应该拥有返回值,外层根据返回值,判断是否进行后续流程

    • 但!这!也太不优雅了!


  • 思考上面业务一些特性



    • 拦截下游机制

    • 上游到下游、方向明确

    • 随时可能插入新的业务流程。。。



可以用责任链模式!但,需要做一些小改动!这地方,我们可以将频繁变动的模块用责任链模式全都隔离出来



  • 看下,使用责任链模式改造后流程图


车辆登记-第三稿(责任链模式)



浏览上述流程图可发现,本来是极度杂乱糅合的业务,可以被设计相对更加平行的结构




  • 对于上述流程,可以进一步分析,并进一步简化:对整体业务分析,我们需要去关注其变或不变的部分



    • 不变:整体业务变动很小的是,登记信息流程(主体逻辑这块),此处的相关变动是很小的,对所有流程也是共用的部分

    • 变:可以发现,开头和结尾是变动更加频繁的部分,我们可以对此处逻辑进行整体的抽象


  • 抽象多变的开头和结尾



车辆登记-第三稿(责任链模式——简化)



  • 所以我们抽象拦截类,可以做一些调整


abstract class InterceptChainTwice<T> {
InterceptChainTwice? next;

void onInit(T data) {
next?.onInit(data);
}

void onSubmit(T data) {
next?.onSubmit(data);
}
}


来看下简要的代码实现,代码不重要,主要看看实现流程和思想




  • 抽象拦截器


abstract class InterceptChainTwice<T> {
InterceptChainTwice? next;

void onInit(T data) {
next?.onInit(data);
}

void onSubmit(T data) {
next?.onSubmit(data);
}
}

class InterceptChainTwiceHandler<T> {
InterceptChainTwice? _interceptFirst;

void add(InterceptChainTwice interceptChain) {
if (_interceptFirst == null) {
_interceptFirst = interceptChain;
return;
}

var node = _interceptFirst!;
while (true) {
if (node.next == null) {
node.next = interceptChain;
break;
}
node = node.next!;
}
}

void onInit(T data) {
_interceptFirst?.onInit(data);
}

void onSubmit(T data) {
_interceptFirst?.onSubmit(data);
}
}


  • 实现拦截器


/// 开头通用拦截器
class CommonIntercept extends InterceptChainTwice<String> {
@override
void onInit(String data) {
//如果有车牌,请求接口,获取数据
//.................
//填充页面
super.onInit(data);
}
}

/// 登记新车拦截器
class RegisterNewIntercept extends InterceptChainTwice<String> {
@override
void onInit(String data) {
//处理开头针对登记新车的单独逻辑
super.onInit(data);
}

@override
void onSubmit(String data) {
var isPass = false;
//如果校验不过,拦截下游逻辑
if (!isPass) {
return;
}
// ......
super.onSubmit(data);
}
}

/// 省略其他实现


  • 使用


void main() {
var type = 0;
var intercepts = InterceptChainTwiceHandler();

intercepts.add(CommonIntercept());
intercepts.add(CarTypeDealIntercept());
if (type == 0) {
//登记新车
intercepts.add(CommonIntercept());
} else if (type == 1) {
//外部登记
intercepts.add(OutRegisterIntercept());
} else if (type == 2) {
//快捷洗车
intercepts.add(FastWashIntercept());
} else {
//预约订单登记
intercepts.add(OrderRegisterIntercept());
}
intercepts.add(TailIntercept());

//业务开始
intercepts.onInit("传入数据源");

//开始处理N多逻辑
//............................................................
//经历了N多逻辑

//提交按钮触发事件
SubmitBtn(
//提交按钮
onSubmit: () {
intercepts.onSubmit("传入提交数据");
},
);
}

总结


关于代码部分,关键的代码,我都写出来,用心看看,肯定能明白我写的意思


也不用找我要完整代码了,这些业务demo代码写完后,就删了


本栏目这个业务,实际上是非常常见的的一个业务,一个提交流程与很多其它的流程耦合,整个业务就会慢慢的变的鬼畜,充满各种判断,很容易让人陷入泥泞,或许,此时可以对已有业务进行思考,如何进行合理的优化


该业务的演变历程,和开发改造是本人的一次思路历程,如大家有更好的思路,还请不吝赐教。


通用拦截器


我结合OkHttp的思想和Dio的API,封装了俩个通用拦截器,这边贴下代码,如果哪里有什么不足,请及时告知本人


说明下:这是Dart版本的


抽象单方法


///一层通用拦截器,T的类型必须一致
abstract class InterceptSingle<T> {
void intercept(T data, SingleHandler handler) => handler.next(data);
}

///添加拦截器,触发拦截器方法入口
class InterceptSingleHandler<T> {
_InterceptSingleHandler _handler = _InterceptSingleHandler(
index: 0,
intercepts: [],
);

void add(InterceptSingle intercept) {
//一种类型的拦截器只能添加一次
for (var item in _handler.intercepts) {
if (item.runtimeType == intercept.runtimeType) {
return;
}
}

_handler.intercepts.add(intercept);
}

void delete(InterceptSingle intercept) {
_handler.intercepts.remove(intercept);
}

void intercept(T data) {
_handler.next(data);
}
}

///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
abstract class SingleHandler {
next(dynamic data);
}

///实现init处理器
class _InterceptSingleHandler extends SingleHandler {
List<InterceptSingle> intercepts;

int index;

_InterceptSingleHandler({
required this.index,
required this.intercepts,
});

@override
next(dynamic data) {
if (index >= intercepts.length) {
return;
}

var intercept = intercepts[index];
var handler =
_InterceptSingleHandler(index: index + 1, intercepts: intercepts);

intercept.intercept(data, handler);
}
}

抽象双方法


///俩层通用拦截器,T的类型必须一致
abstract class InterceptTwice<T> {
void onInit(T data, TwiceHandler handler) => handler.next(data);

void onSubmit(T data, TwiceHandler handler) => handler.next(data);
}

///添加拦截器,触发拦截器方法入口
class InterceptTwiceHandler<T> {
_TwiceInitHandler _init = _TwiceInitHandler(index: 0, intercepts: []);
_TwiceSubmitHandler _submit = _TwiceSubmitHandler(index: 0, intercepts: []);

void add(InterceptTwice intercept) {
//一种类型的拦截器只能添加一次
for (var item in _init.intercepts) {
if (item.runtimeType == intercept.runtimeType) {
return;
}
}

_init.intercepts.add(intercept);
_submit.intercepts.add(intercept);
}

void delete(InterceptTwice intercept) {
_init.intercepts.remove(intercept);
_submit.intercepts.remove(intercept);
}

void onInit(T data) {
_init.next(data);
}

void onSubmit(T data) {
_submit.next(data);
}
}

///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
abstract class TwiceHandler {
next(dynamic data);
}

///实现init处理器
class _TwiceInitHandler extends TwiceHandler {
List<InterceptTwice> intercepts;

int index;

_TwiceInitHandler({
required this.index,
required this.intercepts,
});

@override
next(dynamic data) {
if (index >= intercepts.length) {
return;
}

var intercept = intercepts[index];
var handler = _TwiceInitHandler(index: index + 1, intercepts: intercepts);

intercept.onInit(data, handler);
}
}

///实现submit处理器
class _TwiceSubmitHandler extends TwiceHandler {
List<InterceptTwice> intercepts;

int index;

_TwiceSubmitHandler({
required this.index,
required this.intercepts,
});

@override
next(dynamic data) {
if (index >= intercepts.length) {
return;
}

var intercept = intercepts[index];
var handler = _TwiceSubmitHandler(index: index + 1, intercepts: intercepts);

intercept.onSubmit(data, handler);
}
}

最后


第一次,写这种结合业务的文章


如有收获,还请点个赞,让我感受一下,各位是否读有所获~~


img



感谢阅读,下次再会~~


img

收起阅读 »

PermissionX 1.5发布,支持申请Android特殊权限啦

前言 Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。 不过之...
继续阅读 »

前言


Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。


不过之前一直有朋友在反映,对于 Android 中的一些特殊权限申请,PermissionX 并不支持。是的,PermissionX 本质上只是对 Android 运行时权限 API 进行了一层封装,用于简化运行时权限申请的。而这些特殊权限并不属于 Android 运行时权限的一部分,所以 PermissionX 自然也是不支持的。


但是特殊权限却是我们这些开发者们可能经常要与之打交道的一部分,它们并不难写,但是每次去写都感觉很繁琐。因此经慎重考虑之后,我决定将几个比较常用的特殊权限纳入 PermissionX 的支持范围。那么本篇文章我们就来看一看,对于这几个常见的特殊权限,使用 PermissionX 和不使用 PermissionX 的写法有什么不同之处。


事实上,Android 的权限机制也是经历过长久的迭代的。在 6.0 系统之前,Google 将权限机制设计的比较简单,你的应用程序需要用到什么权限,只需要在 AndroidManifest.xml 文件中声明一下就可以了。


但是从 6.0 系统开始,Android 引入了运行时权限机制。Android 将常用的权限大致归成了几类,一类是普通权限,一类是危险权限,一类是特殊权限。


普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,这种权限和过去一样,只需要在 AndroidManifest.xml 文件中声明一下就可以了,不需要做任何特殊处理。


危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等。这部分权限需要通过代码进行申请,并要用户手动同意才可获得授权。PermissionX 库主要就是处理的这种权限的申请。


而特殊权限则更加少见,Google 认为这种权限比危险权限还要敏感,因此不能仅仅让用户手动同意就可以获得授权,而是需要让用户到专门的设置页面去手动对某一个应用程序授权,该程序才能使用这个权限。


不过相比于危险权限,特殊权限没有非常固定的申请方式,每个特殊权限可能都要使用不同的写法才行,这也导致申请特殊权限比申请危险权限还要繁琐。


从 1.5.0 版本开始,PermissionX 对最常用的几个特殊权限进行了支持。正如刚才所说,特殊权限没有固定的申请方式,因此 PermissionX 也是针对于这几个特殊权限一个一个去适配并支持的。如果你发现你需要申请的某个特殊权限还没有被 PermissionX 支持,也可以向我提出需求,我会考虑在接下来的版本中加入。


在过去,我们发布开源库通常都是发布到 jcenter 上的,但是相信大家现在都已经知道了,jcenter 即将停止服务,具体可以参考我的这篇文章 浅谈 JCenter 即将被停止服务的事件


目前的 jcenter 处在一个半废弃的边缘,虽然还可以正常从 jcenter 下载开源库,但是已经不能再向 jcenter 发布新的开源库了。而在明年 2 月 1 号之后,下载服务也会被关停。


所以,以后要想再发布开源库我们只能选择发布到其他仓库,比如现在 Google 推荐我们使用 Maven Central。


于是,从 1.5.0 版本开始,PermissionX 也会将库发布到 Maven Center 上,之前的老版本由于迁移价值并不大,所以我也不想再耗费经历做迁移了。1.5.0 之前的版本仍然保留在 jcenter 上,提供下载服务直到明年的 2 月 1 号。


而关于如何将库发布到 Maven Central,请参考 再见 JCenter,将你的开源库发布到 MavenCentral 上吧


Android的特殊权限


Android 里具体有哪些特殊权限呢?


说实话,这个我也不太清楚。我所了解的特殊权限基本都是因为需要用到了,然后发现这个权限即不属于普通权限,也不属于危险权限,要用一种更加特殊的方式去申请,才知道原来这是一个特殊权限。


因此,PermissionX 1.5.0 版本中对特殊权限的支持,也就仅限于我知道的,以及从网友反馈得来的几个最为常用的特殊权限。


一共是以下 3 个:



  1. 悬浮窗

  2. 修改设置

  3. 管理外部存储


接下来我就分别针对这 3 个特殊权限做一下更加详细的介绍。


悬浮窗


悬浮窗功能在不少应用程序中使用得非常频繁,因为你可能总有一些内容是要置顶于其他内容之上显示的,这个时候用悬浮窗来实现就会非常方便。


当然,如果你只是在自己的应用内部实现悬浮窗功能是不需要申请权限的,但如果你的悬浮窗希望也能置顶于其他应用程序的上方,这就必须得要申请权限了。


悬浮窗的权限名叫做 SYSTEM_ALERT_WINDOW,如果你去查一下这个权限的文档,会发现这个权限的申请方式比较特殊:



按照文档上的说法,从 Android 6.0 系统开始,我们在使用 SYSTEM_ALERT_WINDOW 权限前需要发出一个 action 为 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 的 Intent,引导用户手动授权。另外我们还可以通过 Settings.canDrawOverlays() 这个 API 来判断用户是否已经授权。


因此,想要申请悬浮窗权限,自然而然就可以写出以下代码:


if (Build.VERSION.SDK_INT >= 23) {
if (Settings.canDrawOverlays(context)) {
showFloatView()
} else {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
} else {
showFloatView()
}


看上去也不复杂嘛。


确实,但是它麻烦的点主要在于,它的请求方式是脱离于一般运行时权限的请求方式的,因此得要为它额外编写独立的权限请求逻辑才行。


而 PermissionX 的目标就是要弱化这种独立的权限请求逻辑,减少差异化代码编写,争取使用同一套 API 来实现对特殊权限的请求。


如果你已经比较熟悉 PermissionX 的用法了,那么以下代码你一定不会陌生:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW)
.onExplainRequestReason { scope, deniedList ->
val message = "PermissionX需要您同意以下权限才能正常使用"
scope.showRequestReasonDialog(deniedList, message, "Allow", "Deny")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(activity, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}


可以看到,这就是最标准的 PermissionX 的正常用法,但是我们在这里却用来请求了悬浮窗权限。也就是说,即使是特殊权限,在 PermissionX 中也可以用普通的方式去处理。


另外不要忘记,所有申请的权限都必须在 AndroidManifest.xml 进行注册才行:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>


那么运行效果是什么样的呢?我们来看看吧:



可以看到,PermissionX 还自带了一个权限提示框,友好地告知用户我们需要悬浮窗权限,引导用户去手动开启。


修改设置


了解了悬浮窗权限的请求方式之后,接下来我们就可以快速过一下修改设置权限的请求方式了,因为它们的用法是完全一样的。


修改设置的权限名叫 WRITE_SETTINGS,如果我们去查看一下它的文档,你会发现它和刚才悬浮窗权限的文档简直如出一辙:



同样是从 Android 6.0 系统开始,在使用 WRITE_SETTINGS 权限前需要先发出一个 action 为 Settings.ACTION_MANAGE_WRITE_SETTINGS 的 Intent,引导用户手动授权。然后我们还可以通过 Settings.System.canWrite() 这个 API 来判断用户是否已经授权。


所以,如果是自己手动申请这个权限,相信你已经知道要怎么写了。


那么用 PermissionX 申请的话应该要怎么写呢?这个当然就更简单了,只需要把要申请的权限替换一下即可,其他部分都不用作修改:


PermissionX.init(activity)
.permissions(Manifest.permission.WRITE_SETTINGS)
...


当然,不要忘记在 AndroidManifest.xml 中注册权限:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>


运行一下,效果如下图所示:



管理外部存储


管理外部存储权限也是一种特殊权限,它可以允许你的 App 拥有对整个 SD 卡进行读写的权限。


有些朋友可能会问,SD 卡本来不就是可以全局读写的吗?为什么还要再申请这个权限?


那你一定是没有了解 Android 11 上的 Scoped Storage 功能。从 Android 11 开始,Android 系统强制启用了 Scoped Storage,所有 App 都不再拥有对 SD 卡进行全局读写的权限了。


关于 Scoped Storage 的更多内容,可以参考我的这篇文章 Android 11 新特性,Scoped Storage 又有了新花样


但是如果有的应用就是要对 SD 卡进行全局读写该怎么办呢(比如说文件浏览器)?


不用担心,Google 仍然还是给了我们一种解决方案,那就是请求管理外部存储权限。


这个权限是 Android 11 中新增的,为的就是应对这种特殊场景。


那么这个权限要怎么申请呢?我们还是先来看一看文档:



大致可以分为几步吧:


第一,在 AndroidManifest.xml 中声明 MANAGE_EXTERNAL_STORAGE 权限。


第二,发出一个 action 为 Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 的 Intent,引导用户手动授权。


第三,调用 Environment.isExternalStorageManager() 来判断用户是否已授权。


传统请求权限的写法我就不再演示了,使用 PermissionX 来请求的写法仍然也还是差不多的。只不过要注意,因为 MANAGE_EXTERNAL_STORAGE 权限是 Android 11 系统新加入的,所以我们也只应该在 Android 11 以上系统去请求这个权限,代码如下所示:


if (Build.VERSION.SDK_INT >= 30) {
PermissionX.init(this)
.permissions(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
...
}


AndroidManifest.xml 中的权限如下:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>


运行一下程序,效果如下图所示:



这样我们就拥有全局读写 SD 卡的权限了。


另外 PermissionX 还有一个特别方便的地方,就是它可以一次性申请多个权限。假如我们想要同时申请悬浮窗权限和修改设置权限,只需要这样写就可以了:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW, Manifest.permission.WRITE_SETTINGS)
...


运行效果如下图所示:



当然你也可以将特殊权限与普通运行时权限放在一起申请,PermissionX 对此也是支持的。只有当所有权限都请求结束时,PermissionX 才会将所有权限的请求结果一次性回调给开发者。


关于 PermissionX 新版本的内容变化就介绍到这里,升级的方式非常简单,修改一下 dependencies 当中的版本号即可:


repositories {
google()
mavenCentral()
}


dependencies {
implementation 'com.guolindev.permissionx:permissionx:1.5.0'
}


注意现在一定要使用 mavenCentral 仓库,而不能再使用 jcenter 了。



如果你对 PermissionX 的源码感兴趣,可以访问 PermissionX 的项目主页:


github.com/guolindev/P…

收起阅读 »

iOS逆向必学-logos语法

一、概述Logos语法其实是CydiaSubstruct框架提供的一组宏定义。便于开发者使用宏进行HOOK操作。语法简单,功能强大且稳定,它是跨平台的。[logos] (http://iphonedevwiki.net/index.php/Logos)二、lo...
继续阅读 »

一、概述

Logos语法其实是CydiaSubstruct框架提供的一组宏定义。便于开发者使用宏进行HOOK操作。语法简单,功能强大且稳定,它是跨平台的。[logos] (http://iphonedevwiki.net/index.php/Logos)

二、logos语法

logos语法分为3类。

2.1、Block level

这一类型的指令会开辟一个代码块,以%end结束。

%group

用来将代码分组。开发中hook代码会很多,这样方便管理Logos代码。所有的group都必须初始化,否则编译报错。


#import <UIKit/UIKit.h>

%group group1

%hook RichTextView

- (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
//hook后要处理的方式1
return %orig;
}

%end

%end


%group group2

%hook RichTextView

- (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
//hook后要处理的方式2
return %orig;
}

%end

%end

%group group3

%hook RichTextView

- (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
//hook后要处理的方式3
return %orig;
}

%end

%end

//使用group要配合ctor
%ctor {
//[[UIDevice currentDevice] systemVersion].doubleValue 可以用来判断版本或其它逻辑。
if ([[UIDevice currentDevice] systemVersion].doubleValue >= 11.0) {
//这里group3会覆盖group1,不会执行group1逻辑。
%init(group1)%init(group3);
} else {
%init(group2);
}
}



  • group初始化在%ctor中,需要%init初始化。
  • 所有group必须初始化,否则编译报错。
  • 在一个逻辑中同时初始化多个group,后面的会覆盖前面的。
  • 在不添加group的情况下,默认有个_ungrouped组,会自动初始化。

Begin a hook group with the name Groupname. Groups cannot be inside another [%group](https://iphonedev.wiki/index.php/Logos#.25group "Logos") block. All ungrouped hooks are in the implicit "_ungrouped" group. The _ungrouped group is initialized for you if there are no other groups. You can use the %init directive to initialize it manually. Other groups must be initialized with the %init(Groupname) directive

%hook

HOOK某个类里面的某个方法。

%hook RichTextView

- (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
//hook后要处理的方式1
return %orig;
}

%end

%hook后面需要跟需要hook的类名。

%new
为某个类添加新方法,在%hook 和 %end 中使用。

%hook RichTextView

%new
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

}

%end

%subclass

%subclass Classname: Superclass <Protocol list>

运行时创建子类,只能包含方法或者关联属性,不能包含属性。可以通过%c创建类实例。

#import <UIKit/UIKit.h>

@interface MyObject

- (void)setSomeValue:(id)value;

@end

%subclass MyObject : NSObject

- (id)init {
self = %orig;
[self setSomeValue:@"value"];
return self;
}

%new
- (id)someValue {
return objc_getAssociatedObject(self, @selector(someValue));
}

%new
- (void)setSomeValue:(id)value {
objc_setAssociatedObject(self, @selector(someValue), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

%end

%property

%property (nonatomic|assign|retain|copy|weak|strong|getter|setter) Type name;

subclass或者hook的类添加属性。必须在 %subclass 或%hook中。

%property(nonatomic,assign) NSInteger age;

%end

与其它命令配对出现。

2.2、Top level

TopLevel指令不放在BlockLevel中。

%config

%config(Key=Value);

logos设置标记。

Configuration Flags

keyvaluesnotes
generatorMobileSubstrate生成的代码使用MobileSubstrate hook
generatorinternal生成的代码只使用OC runtime方法hook
warningsnone忽略所有警告
warningsdefault没有致命的警告
warningserror使所有警告报错
dumpyamlYAML格式转储内部解析树

%config(generator=internal);
%config(warnings=error);
%config(dump=yaml);

%hookf

hook函数,类似fishhook
语法

%hookf(rtype, symbolName, args...) { … }
  • rtype:返回值。
  • symbolName:原函数地址。
  • args...:参数。
    示例
FILE *fopen(const char *path, const char *mode);
%hookf(FILE *, fopen, const char *path, const char *mode) {
NSLog(@"Hey, we're hooking fopen to deny relative paths!");
if (path[0] != '/') {
return NULL;
}
return %orig;
}

%ctor

构造函数,用于确定加载那个组。和%init结合用。

%dtor

析构,做一些收尾工作。比如应用挂起的时候。

2.3、Function level

这一块的指令就放在方法中

%init

用来初始化某个组。

%class

%class Class;

%class已经废弃了,不建议使用。

%c

类似getClass函数,获得一个类对象。一般用于调用类方法。

//只是为了声明编译通过
@interface MainViewController

+ (void)HP_classMethod;

@end


%hook MainViewController

%new
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//方式一
// [self.class HP_classMethod];
//方式二
// [NSClassFromString(@"MainViewController") HP_classMethod];
//方式三
[%c(MainViewController) HP_classMethod];
}

%new
+ (void)HP_classMethod {
NSLog(@"HP_classMethod");
}

%end
  • %c 中没有引号。

%orig

保持原有的方法实现,如果原来的方法有返回值和参数,那么可以传递参数和接收返回值。

%hook RichTextView

- (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
//传递参数&接收返回值。
BOOL result1 = %orig(arg1,arg2,arg3,arg4);
BOOL result2 = %orig;
return %orig;
}

%end

  • %orig
    可以接收返回值。
  • 可以传递参数,不传就是传递该方法的默认参数。

%log

能够输出日志,输出方法调用的详细信息 。

%hook RichTextView

- (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
%log;
return %orig;
}

%end

输出:

 WeChat[11309:6708938] -[<RichTextView: 0x15c4c9720> setPrefixContent:(null) TargetContent:钱已经借给你了。 TargetParserString:<contentMD5>0399062cd62208dad884224feae2aa30</contentMD5><fontsize>20.287109</fontsize><fwidth>240.000000</fwidth><parser><type>1</type><range>{0, 8}</range><info><![CDATA[<style><range>{0, 8}</range><rect>{{0, 0}, {135, 21}}</rect></style>]]></info></parser> SuffixContent:(null)]

能够输出详细的日志信息,包含类、方法、参数、以及控件信息等详细信息。

总结

  • logos语法其实是CydiaSubstruct框架提供的一组宏定义。
  • 语法
    • %hook%end勾住某个类,在一个代码块中直接写需要勾住的方法。
    • %group%end用于分组。
      • 每一组都需要%ctor()函数构造。
      • 通过%init(组名称)进行初始化。
    • %log输出方法的详细信息(调用者、方法名、方法参数)
    • %orig调用原始方法。可以传递参数,接收返回值。
    • %c类似getClass函数,获取一个类对象。
    • %new添加某个方法。
  • .xm文件代表该文件支持OCC/C++语法。
  • 编译该文件时需要导入头文件以便编译通过。.xm文件不参与代码的执行,编译后生成的.mm文件参与代码的执行。


作者:HotPotCat
链接:https://www.jianshu.com/p/70151c602886


收起阅读 »

lookUpImpOrForward 消息慢速查找(下)

3.1.2 search_method_list_inlineALWAYS_INLINE static method_t * search_method_list_inline(const method_list_t *mlist, SEL sel) { ...
继续阅读 »

3.1.2 search_method_list_inline

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
//methodlist是否已经修复
int methodListIsFixedUp = mlist->isFixedUp();
//是否有序
int methodListHasExpectedSize = mlist->isExpectedSize();

if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
//二分查找
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
//无序,循环查找
if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
return m;
}
return nil;
}
  • 首先判断有序无序。
  • 有序进入二分查找findMethodInSortedMethodList
  • 无序进入循环查找findMethodInUnsortedMethodList

⚠️ 那么就有个问题,排序是什么时候完成的?
既然是method_t相关类型那就进去搜一下sort相关的关键字。发现了如下方法:

 struct SortBySELAddress :
public std::binary_function<const struct method_t::big&,
const struct method_t::big&, bool>
{
bool operator() (const struct method_t::big& lhs,
const struct method_t::big& rhs)
{ return lhs.name < rhs.name; }
};



是在_read_images类加载映射的时候注册调用的。又见到了_read_images,这个方法将在后面继续研究。

结论:类在加载实例化的时候进行的排序,是按照sel address进行排序的。

3.1.3 findMethodInSortedMethodList 二分查找

findMethodInSortedMethodList会根据架构最终进入各自的findMethodInSortedMethodList方法,这里以x86为例进行分析:

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);

auto first = list->begin();
auto base = first;
decltype(first) probe;

uintptr_t keyValue = (uintptr_t)key;
//method list count
uint32_t count;
//count >>= 1 相当于除以2。加入count为8
for (count = list->count; count != 0; count >>= 1) {//7 >> 1 = 3,前一个已经比较了4,这里就完美的避开了4。
//base是为了配合少查找
//probe中间元素,第一次 probe = 0 + 8 >> 1 = 4
probe = base + (count >> 1);
//sel
uintptr_t probeValue = (uintptr_t)getName(probe);
//与要查找的sel是否匹配
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
//查找分类同名sel。如果匹配了就找分类中的。因为分类是在前面的,所以一直找到最开始的位置。
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
//匹配则返回。
return &*probe;
}
//没有匹配
if (keyValue > probeValue) {//大于的情况下,在后半部分
//没有匹配的情况下,如果sel在后半部分,这里base指向了上次查找的后面一个位置。
base = probe + 1;//5
//count对应减1
count--;//7 -- 操作为了少做比较,因为已经比较过了
}
//在前半部分不进行额外操作。
}

return nil;
}
  • 首先是一个循环比较,条件是count >>= 1,这里是对count进行减半,相当于二分。
  • base是为了少做比较,相当于是一个基线,当要继续往后查找的时候base为当前查找元素的下一个元素。
  • 当要继续往后查找的时候count进行了--操作,这一步是为了count >>= 1不包含已经比较过的范围。
  • 找到值的时候会循环继续往前查找,因为存在分类与类中方法同名的情况(分类方法放在类中同名方法前面),一直找到不同名为止。

⚠️根据源码可以得到以下结论:
1.分类方法调用先找类中方法,再逐次找到分类方法,直到找到第一个。
2.因为判断条件是当前命中元素与前一个元素比较,sel相同的情况下才继续查找,那就说明分类的方法是插入类中方法列表中的,都在对应类中方法的前面。

  • 查找完毕后会返回lookUpImpOrForward

这里以有8个方法为类来分析查找流程,过程如下:

比较开始值:count = 8 base = 0 probe = 4
- 第一次:比较 probe = 4
- keyValue > probeValue count = 3(先--,再>>1) base = 5 probe = 6
第二次: 比较 probe = 6
- keyValue > probeValue count = 1(先--,再>>1) base = 7 probe = 7
第三次:比较 probe = 7
- keyValue > probeValue count = 0(先--,再>>1) base = 8 probe = 8count == 0
- keyValue < probeValue count = 0>>1) base = 7 probe = 7count == 0
- keyValue < probeValue count = 1>>1) base = 5 probe = 5
第三次:比较 probe = 5
- keyValue > probeValue count = 0(先--,再>>1) base = 6 probe = 6count == 0
- keyValue < probeValue count = 0>>1) base = 5 probe = 5count == 0
- keyValue < probeValue count = 4>>1) base = 0 probe = 2
第二次:比较 probe = 2
- keyValue > probeValue count = 1(先--,再>>1) base = 3 probe = 3
第三次:比较 probe = 3
- keyValue > probeValue count = 0(先--,再>>1) base = 4 --count == 0
- keyValue < probeValue count = 0>>1) base = 3 --count == 0
- keyValue < probeValue count = 2>>1) base = 1 probe = 1
第三次:比较 probe = 1
- keyValue > probeValue count = 0(先--,再>>1) base = 0 --count == 0
- keyValue < probeValue count = 1>>1) base = 0 probe = 0
第四次:比较 probe = 0
- keyValue > probeValue count = 0(先--,再>>1) base = 1 --count == 0
- keyValue < probeValue count = 0>>1) base = 0 --count == 0

代码模拟:

int testFindSortedMethods(int methodCount,int findKey) {
int base = 0;
int probe = 0;
int round = 0;
printf("查找key:%d\n",findKey);
for (int count = methodCount; count != 0; count >>= 1) {
round++;
probe = base + (count >> 1);
printf("\t第%d轮 scan count :%d, base:%d,probe:%d\n",round,count,base,probe);
if (findKey == probe) {
printf("\tfound prode:%d\n",probe);
return probe;
}
if (findKey > probe) {
base = probe + 1;
count--;
}
}
printf("\tnot found -1\n");
return -1;
}

调用:

testFindSortedMethods(8, 0);
testFindSortedMethods(8, 1);
testFindSortedMethods(8, 2);
testFindSortedMethods(8, 3);
testFindSortedMethods(8, 4);
testFindSortedMethods(8, 5);
testFindSortedMethods(8, 6);
testFindSortedMethods(8, 7);
testFindSortedMethods(8, 8);
testFindSortedMethods(8, 9);

输出:

查找key:0
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :4, base:0,probe:2
第3轮 scan count :2, base:0,probe:1
第4轮 scan count :1, base:0,probe:0
found prode:0
查找key:1
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :4, base:0,probe:2
第3轮 scan count :2, base:0,probe:1
found prode:1
查找key:2
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :4, base:0,probe:2
found prode:2
查找key:3
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :4, base:0,probe:2
第3轮 scan count :1, base:3,probe:3
found prode:3
查找key:4
第1轮 scan count :8, base:0,probe:4
found prode:4
查找key:5
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :3, base:5,probe:6
第3轮 scan count :1, base:5,probe:5
found prode:5
查找key:6
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :3, base:5,probe:6
found prode:6
查找key:7
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :3, base:5,probe:6
第3轮 scan count :1, base:7,probe:7
found prode:7
查找key:8
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :3, base:5,probe:6
第3轮 scan count :1, base:7,probe:7
not found -1
查找key:9
第1轮 scan count :8, base:0,probe:4
第2轮 scan count :3, base:5,probe:6
第3轮 scan count :1, base:7,probe:7
not found -1

可以看到输出与验证的结论一致。

流程图:




四、案例分析慢速查找流程

定义一个HPObject以及它的子类HPSubObject
HPObject定义和实现如下:

@interface HPObject : NSObject

- (void)instanceMethod1;

- (void)instanceMethod2;

+ (void)classMethod;

@end

@implementation HPObject

- (void)instanceMethod1 {
NSLog(@"%s",__func__);
}

+ (void)classMethod {
NSLog(@"%s",__func__);
}

@end

HPSubObject定义和实现如下:

@interface HPSubObject : HPObject

- (void)subInstanceMethod;

@end

@implementation HPSubObject

- (void)subInstanceMethod {
NSLog(@"%s",__func__);
}

@end

根据前面分析的方法查找逻辑测试代码:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
HPSubObject *subObject = [HPSubObject new];
//对象方法 根据慢速查找分析是能找到的
[subObject subInstanceMethod];
[subObject instanceMethod1];
[subObject instanceMethod2];
#pragma clang diagnostic pop

输出:

-[HPSubObject subInstanceMethod]
-[HPObject instanceMethod1]
-[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0'
  • subInstanceMethodinstanceMethod1符合预期。
  • instanceMethod2找不到报错unrecognized selector sent to instance,为什么报这个错误呢?
    查看调用堆栈如下:



    • 那么去源码中搜索错误信息找到以下内容:
    // Replaced by CF (throws an NSException)
    + (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
    class_getName(self), sel_getName(sel), self);
    }

    // Replaced by CF (throws an NSException)
    - (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
    object_getClassName(self), sel_getName(sel), self);
    }

    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
    "(no message forward handler is installed)",
    class_isMetaClass(object_getClass(self)) ? '+' : '-',
    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    那么调用的是哪个呢?断点后并没有进入。根据上面的分析imp找不到的时候会有两个选项resolveMethod_locked或者_objc_msgForward_impcache
    _objc_msgForward_impcache的汇编实现如下:

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    内部直接调用了__objc_msgForward

    ENTRY __objc_msgForward

    adrp x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgForward

    可以看到__objc_msgForward的实现是调用__objc_forward_handler,也就是:

    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
    "(no message forward handler is installed)",
    class_isMetaClass(object_getClass(self)) ? '+' : '-',
    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    这也就是报错信息的原因,里面进行了格式化的错误信息打印。

    接着增加一个NSObject的分类:

    @interface NSObject (Additions)

    - (void)categoryInstanceMethod;

    @end

    @implementation NSObject (Additions)

    - (void)categoryInstanceMethod {
    NSLog(@"%s",__func__);
    }

    @end
    调用:

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
    [HPSubObject classMethod];
    [HPSubObject performSelector:@selector(categoryInstanceMethod)];
    #pragma clang diagnostic pop
    输出:

    +[HPObject classMethod]
    -[NSObject(Additions) categoryInstanceMethod]
    • classMethod类方法能找到符合预期。
    • 为什么HPSubObject能调用categoryInstanceMethod实例方法?
      这就涉及到了类的继承链,NSObject元类的父类是NSObject类,所以能找到。

    再次说明OC的底层没有实例和类方法的区分,类方法和实例方法是人为加上去的。我们只是为了配合OC的演出视而不见。

    五、 总结

    慢速查找流程:

    • checkIsKnownClass检查注册类。
    • realizeAndInitializeIfNeeded_locked初始化类,为方法查找做好准备。
    • 递归查找imp,会涉及到动态缓存库的二次确认以及父类的快速慢速查找。
      • 查找过程会进行二分查找/递归查找。
      • 是否二分要看方法列表是否已经排序,排序操作是在类加载实例化的时候完成的。
      • 二分查找算法很经典,充分利用>>1以及--不多浪费一次机会。
    • 找到imp直接跳转返回。根据LOOKUP_NOCACHE判断是否插入缓存。
    • 没有找到则判断是否进行动态方法决议。
    • 不进行动态方法决议则判断是否要forward


    作者:HotPotCat
    链接:https://www.jianshu.com/p/db43c28e0e11


    收起阅读 »

    lookUpImpOrForward 消息慢速查找(上)

    上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。一、汇编中找不到缓存在汇编代码中只有_lookUpImpOrForward的调用而没有实现...
    继续阅读 »

    上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。

    一、汇编中找不到缓存

    在汇编代码中只有_lookUpImpOrForward的调用而没有实现,代码中直接搜这个也是搜不到的。因为实现在c/c++代码中,需要搜索lookUpImpOrForward。声明如下:

    extern IMP lookUpImpOrForward(id obj, SEL, Class cls, int behavior);

    那么参数肯定也就是汇编中传过来的,汇编中调用如下:

    .macro MethodTableLookup

    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    //x2 = cls
    mov x2, x16
    //x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver
    //_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    mov x3, #3
    bl _lookUpImpOrForward

    // IMP in x0
    mov x17, x0

    RESTORE_REGS MSGSEND

    .endmacro

    • 3个参数没有什么好说的,behaviorLOOKUP_INITIALIZE | LOOKUP_RESOLVER。那就证明lookUpImpOrForward是有查找模式的。
    • 调用完_lookUpImpOrForward后有mov x17, x0说明是有返回值的,与c/c++lookUpImpOrForward的声明对应上了。

    那么就有一个问题了,为什么cache查找要使用汇编?
    1.汇编更接近机器语言,执行速度快。为了快速找到方法,优化方法查找时间。
    2.消息发送参数是未知参数(比如可变参数),c参数必须明确,汇编相对能够更加动态化。
    3.更安全。


    二、 慢速查找流程

    慢速查找就是不断遍历methodlist的过程,遍历是一个耗时的过程,所以是使用c/c++来实现的。

    2.1 lookUpImpOrForward

    首先明确慢速查找流程的目标是找到sel对应的imp。所以核心就是lookUpImpOrForward中返回imp的逻辑,精简后源码如下:


    NEVER_INLINE
    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
    //forward_imp赋值
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    //要返回的imp
    IMP imp = nil;
    //当前查找的cls
    Class curClass;

    //初始化的一些处理,如果类没有初始化behavior会增加 LOOKUP_NOCACHE,判断是否初始化取的是data()->flags的第29位。
    if (slowpath(!cls->isInitialized())) {
    behavior |= LOOKUP_NOCACHE;
    }

    //类是否已经注册,注册后会加入allocatedClasses表中
    checkIsKnownClass(cls);
    //初始化需要的类。由于要去类中查找方法,如果rw,ro没有准备好那就没有办法查了。也就是为后面的查找代码做好准备。LOOKUP_INITIALIZE用在了这里
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    //赋值要查找的类
    curClass = cls;
    //死循环,除非return/break
    for (unsigned attempts = unreasonableClassCount();;) {//……}

    //参数LOOKUP_RESOLVER用在了这里,动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    return resolveMethod_locked(inst, sel, cls, behavior);
    }

    done:
    //没有初始化LOOKUP_NOCACHE就有值了,也就是查完后不要插入缓存。在这个流程中是插入
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    #if CONFIG_USE_PREOPT_CACHES
    //共享缓存
    while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
    cls = cls->cache.preoptFallbackClass();
    }
    #endif
    //填充缓存,这里填充的是`cls`。也就是父类如果有缓存也会被加进子类。
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
    done_unlock:
    runtimeLock.unlock();
    //forward_imp 并且有 LOOKUP_NIL 的时候直接返回nil。也就是不进行forward_imp
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
    return nil;
    }
    return imp;
    }
    • 先给forward_imp赋值_objc_msgForward_impcache,这个函数的实现是在汇编中。
    • impcurClass定义。
    • cls->isInitialized()类没有初始化则behavior增加LOOKUP_NOCACHE,类有没有初始化时由data()->flags的第29位决定的。
        bool isInitialized() {
    //#define uint32_t RW_INITIALIZED (1<<29)
    return getMeta()->data()->flags & RW_INITIALIZED;
    }

    • checkIsKnownClass判断类是否已经注册,注册后会加入allocatedClasses表中。
    • realizeAndInitializeIfNeeded_locked初始化需要的类,由于要去类中查找方法,如果rw ro没有准备好那就没有办法查了(methods就存在其中)。也就是为后面的查找代码做好准备。汇编中调用的时候传递的behaviorLOOKUP_INITIALIZE用在了这里。它的流程会在后面介绍。
    • 进入for死循环查找imp,核心肯定就是找imp赋值的地方了。那么就只有breakreturngoto才能停止循环,否则一直查找。
    • 如果上面imp没有找到,LOOKUP_RESOLVER是有值的,会进入动态方法决议。
    • 如果找到imp会跳转到done,判断是否需要插入缓存会调用log_and_fill_cache最终调用到cache.insert。父类如果有缓存找到也会加入到子类,这里是因为写入的时候参数是cls
    • 根据LOOKUP_NIL判断是否需要forward,不需要直接返回nil,需要返回imp

    2.1.1 behavior 说明

    在从汇编调入lookUpImpOrForward的时候传入的behavior参数是LOOKUP_INITIALIZELOOKUP_RESOLVER
    behavior类型如下:

    /* method lookup */
    enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
    };

    根据上面的分析可以得到大致结论:

    • LOOKUP_INITIALIZE: 控制是否去进行类的初始化。有值初始化,没有不初始化。
    • LOOKUP_RESOLVER:是否进行动态方法决议。有值决议,没有值不决议。
    • LOOKUP_NIL:是否进行forward。有值不进行,没有值进行。
    • LOOKUP_NOCACHE:是否插入缓存。有值不插入缓存,没有值插入。

    2.2 realizeAndInitializeIfNeeded_locked

    在这里主要进行类的实例化和初始化,有两个分支:RealizeInitialize

    2.2.1 Realize

    (这个分支一般在_read_images的时候就处理好了)
    在进行类的实例化的时候调用流程是这样的realizeAndInitializeIfNeeded_locked->realizeClassMaybeSwiftAndLeaveLocked->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift,最终会调用realizeClassWithoutSwiftswift会调用realizeSwiftClass。这个不是这篇文章的重点,分析下主要代码如下:


    static Class realizeClassWithoutSwift(Class cls, Class previously)
    {
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
    // This was a future class. rw data is already allocated.
    rw = cls->data();
    ro = cls->data()->ro();
    ASSERT(!isMeta);
    cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
    // Normal class. Allocate writeable class data.
    rw = objc::zalloc<class_rw_t>();
    rw->set_ro(ro);
    rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
    cls->setData(rw);
    }
    //赋类和元类的操作
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

    //关联类
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);
    return cls;
    }
    • 对类的ro以及rw进行处理。
    • 循环调用了父类和元类的realizeClassWithoutSwift
    • 关联了父类和元类。

    当对象调用方法的时候判断类是否初始化,如果初始化了再判断类的父类以及元类,相当于是递归操作了,一直到NSObject->nil为止。也就是说只要有一个类进行初始化它的上层(也就是父类和元类)都会进行初始化,是一个连锁反应。

    ⚠️为什么这么操作?
    就是为了查找方法。类没有实例方法的话会找父类,类没有类方法会找元类,所以需要这么操作。

    2.2.2 Initialized

    realizeAndInitializeIfNeeded_locked->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass。在initializeNonMetaClass中调用了callInitialize(cls)


    void callInitialize(Class cls)
    {
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
    }


    系统直接objc_msgSend发送了initialize消息。所以initialize是在类第一个方法被调用的时候进行调用的。也就是发送第一个消息的时候:
    消息慢速查找开始前进行类初始化的时候发送的initialize消息

    三、循环查找

    对于慢速查找流程,我们想到的就是先查自己然后再查父类一直找到NSObject->nil
    慢速查找流程应该是这样:
    1.查自己methodlist->(sel,imp)。
    2.查父类->NSObject->nil ->跳出来

    查看源码:

    //死循环,除非return/break
    for (unsigned attempts = unreasonableClassCount();;) {
    //先去共享缓存查找,防止这个时候共享缓存中已经写入了该方法。
    if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    #if CONFIG_USE_PREOPT_CACHES
    //这里也是调用到了`_cache_getImp`汇编代码,最终调用了`CacheLookup`查找共享缓存。
    imp = cache_getImp(curClass, sel);
    //找到后直接跳转done_unlock
    if (imp) goto done_unlock;
    curClass = curClass->cache.preoptFallbackClass();
    #endif
    } else {
    // curClass method list.进行循环查找
    Method meth = getMethodNoSuper_nolock(curClass, sel);
    //找到method
    if (meth) {
    //返回imp
    imp = meth->imp(false);
    //跳转done
    goto done;
    }
    //这里curClass 会赋值,直到找到 NSObject->nil就会返回forward_imp
    if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    imp = forward_imp;
    break;
    }
    }

    // Superclass cache.
    imp = cache_getImp(curClass, sel);
    if (slowpath(imp == forward_imp)) {

    break;
    }
    if (fastpath(imp)) {
    goto done;
    }
    }
    • 可以看到这是一个死循环。
    • 如果有共享缓存,先查找共享缓存,因为前面做了很多准备工作,防止这个时候共享缓存中已经写入了该方法(在汇编中已经查过一次了)。
    • 否则就进行二分查找流程,核心逻辑是在getMethodNoSuper_nolock中调用的,查找完成返回。
    • 如果找到method则获取imp跳转done,如果没有找到将父类赋值给curClass,父类不存在则imp = forward_imp;
      • 找到则进入找到imp done的逻辑。
        • log_and_fill_cache插入缓存,也就是调用cls->cache.insert与分析cache的时候逻辑对上了。
        • 返回imp
      • 没有找到则curClass赋值superclass,没有superclass也就是找到了NSObject->nil的情况下imp = forward_imp
      • 没有找到并且有父类的情况下通过cache_getImp去父类的cache中查找。这里与共享缓存的cache_getImp是一个逻辑,最终都是调用汇编_cache_getImp->CacheLookup

      父类也有快速和慢速查找。

    • 如果父类中也没有找到,则进入递归。直到imp找到或者变为forward_imp才结束循环。

    _cache_getImp 说明
    源码:

        STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0, 0
    CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

    LGetImpMissDynamic:
    mov p0, #0
    ret

    LGetImpMissConstant:
    mov p0, p2
    ret

    END_ENTRY _cache_getImp

    最终也是调用CacheLookup进行缓存查找。但是第三个参数是LGetImpMissDynamic实现是mov p0, #0 ret也就是找不到就返回了。不会去走__objc_msgSend_uncached逻辑。

    ⚠️ 找到父类缓存会插入自己的缓存

    3.1 二分查找流程

    3.1.1 getMethodNoSuper_nolock

    首先进入的是getMethodNoSuper_nolock,实现如下:


    static method_t *
    getMethodNoSuper_nolock(Class cls, SEL sel)
    {
    //获取methods
    auto const methods = cls->data()->methods();
    //循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
    for (auto mlists = methods.beginLists(),
    end = methods.endLists();
    mlists != end;
    ++mlists)
    {
    method_t *m = search_method_list_inline(*mlists, sel);
    if (m) return m;
    }
    return nil;
    }



    • 这里只是普通的循环,因为methods获取的数据类型是method_array_t,它存储的是method_list_t。这里的数据结构有可能是二维数据,因为动态加载方法和类导致。
    • 核心逻辑是调用search_method_list_inline实现的


    收起阅读 »

    为数不多的人知道的 Kotlin 技巧以及 原理解析

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁...
    继续阅读 »

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。

    结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是呢简洁的背后是有代价的,使用不当对性能可能会有损耗,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,关于 Kotlin 性能损失那些事,可以看一下我另外两篇文章。

    这两篇文章都分析了 Kotlin 使用不当对性能的影响,不仅如此 Kotlin 当中还有很多让人傻傻分不清楚的语法糖例如 run, with, let, also, apply 等等,这篇文章将介绍一种简单的方法来区分它们以及如何选择使用。

    通过这篇文章你将学习到以下内容,文中会给出相应的答案

    • 如何使用 plus 操作符对集合进行操作?
    • 当获取 Map 值为空时,如何设置默认值?
    • require 或者 check 函数做什么用的?
    • 如何区分 run, with, let, also and apply 以及如何使用?
    • 如何巧妙的使用 in 和 when 关键字?
    • Kotlin 的单例有几种形式?
    • 为什么 by lazy 声明的变量只能用 val?

    plus 操作符

    在 Java 中算术运算符只能用于基本数据类型,+ 运算符可以与 String 值一起使用,但是不能在集合中使用,在 Kotlin 中可以应用在任何类型,我们来看一个例子,利用 plus (+) 和 minus (-) 对 Map 集合做运算,如下所示。

    fun main() {
    val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)

    // plus (+)
    println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
    println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
    println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}

    // minus (-)
    println(numbersMap - "one") // {two=2, three=3}
    println(numbersMap - listOf("two", "four")) // {one=1, three=3}
    }

    其实这里用到了运算符重载,Kotlin 在 Maps.kt 文件里面,定义了一系列用关键字 operator 声明的 Map 的扩展函数。

    用 operator 关键字声明 plus 函数,可以直接使用 + 号来做运算,使用 operator 修饰符声明 minus 函数,可以直接使用 - 号来做运算,其实我们也可以在自定义类里面实现 plus (+) 和 minus (-) 做运算。


    data class Salary(var base: Int = 100){
    override fun toString(): String = base.toString()
    }

    operator fun Salary.plus(other: Salary): Salary = Salary(base + other.base)
    operator fun Salary.minus(other: Salary): Salary = Salary(base - other.base)

    val s1 = Salary(10)
    val s2 = Salary(20)
    println(s1 + s2) // 30
    println(s1 - s2) // -10

    Map 集合的默认值

    在 Map 集合中,可以使用 withDefault 设置一个默认值,当键不在 Map 集合中,通过 getValue 返回默认值。

    val map = mapOf(
    "java" to 1,
    "kotlin" to 2,
    "python" to 3
    ).withDefault { "?" }

    println(map.getValue("java")) // 1
    println(map.getValue("kotlin")) // 2
    println(map.getValue("c++")) // ?

    源码实现也非常简单,当返回值为 null 时,返回设置的默认值。

    internal inline fun <K, V> Map<K, V>.getOrElseNullable(key: K, defaultValue: () -> V): V {
    val value = get(key)
    if (value == null && !containsKey(key)) {
    return defaultValue()
    } else {
    @Suppress("UNCHECKED_CAST")
    return value as V
    }
    }

    但是这种写法和 plus 操作符在一起用,有一个 bug ,看一下下面这个例子。

    val newMap = map + mapOf("python" to 3)
    println(newMap.getValue("c++")) // 调用 getValue 时抛出异常,异常信息:Key c++ is missing in the map.

    这段代码的意思就是,通过 plus(+) 操作符合并两个 map,返回一个新的 map, 但是忽略了默认值,所以看到上面的错误信息,我们在开发的时候需要注意这点。

    使用 require 或者 check 函数作为条件检查

    // 传统的做法
    val age = -1;
    if (age <= 0) {
    throw IllegalArgumentException("age must not be negative")
    }

    // 使用 require 去检查
    require(age > 0) { "age must be negative" }

    // 使用 checkNotNull 检查
    val name: String? = null
    checkNotNull(name){
    "name must not be null"
    }

    那么我们如何在项目中使用呢,具体的用法可以查看我 GitHub 上的项目 DataBindingDialog.kt 当中的用法。

    如何区分和使用 run, with, let, also, apply

    感谢大神 Elye 的这篇文章提供的思路 Mastering Kotlin standard functions

    run, with, let, also, apply 都是作用域函数,这些作用域函数如何使用,以及如何区分呢,我们将从以下三个方面来区分它们。

    • 是否是扩展函数。
    • 作用域函数的参数(this、it)。
    • 作用域函数的返回值(调用本身、其他类型即最后一行)。

    是否是扩展函数

    首先我们来看一下 with 和 T.run,这两个函数非常的相似,他们的区别在于 with 是个普通函数,T.run 是个扩展函数,来看一下下面的例子。

    val name: String? = null
    with(name){
    val subName = name!!.substring(1,2)
    }

    // 使用之前可以检查它的可空性
    name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")

    在这个例子当中,name?.run 会更好一些,因为在使用之前可以检查它的可空性。

    作用域函数的参数(this、it)

    我们在来看一下 T.run 和 T.let,它们都是扩展函数,但是他们的参数不一样 T.run 的参数是 this, T.let 的参数是 it。

    val name: String? = "hi-dhl.com"

    // 参数是 this,可以省略不写
    name?.run {
    println("The length is ${this.length} this 是可以省略的 ${length}")
    }

    // 参数 it
    name?.let {
    println("The length is ${it.length}")
    }

    // 自定义参数名字
    name?.let { str ->
    println("The length is ${str.length}")
    }

    在上面的例子中看似 T.run 会更好,因为 this 可以省略,调用更加的简洁,但是 T.let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let。

    作用域函数的返回值(调用本身、其他类型)

    接下里我们来看一下 T.let 和 T.also 它们接受的参数都是 it, 但是它们的返回值是不同的 T.let 返回最后一行,T.also 返回调用本身。


    var name = "hi-dhl"

    // 返回调用本身
    name = name.also {
    val result = 1 * 1
    "juejin"
    }
    println("name = ${name}") // name = hi-dhl

    // 返回的最后一行
    name = name.let {
    val result = 1 * 1
    "hi-dhl.com"
    }
    println("name = ${name}") // name = hi-dhl.com

    从上面的例子来看 T.also 似乎没有什么意义,细想一下其实是非常有意义的,在使用之前可以进行自我操作,结合其他的函数,功能会更强大。

    fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

    当然 T.also 还可以做其他事情,比如利用 T.also 在使用之前可以进行自我操作特点,可以实现一行代码交换两个变量,在后面会有详细介绍

    T.apply 函数

    通过上面三个方面,大致了解函数的行为,接下来看一下 T.apply 函数,T.apply 函数是一个扩展函数,返回值是它本身,并且接受的参数是 this。

    // 普通方法
    fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
    }
    // 改进方法
    fun createInstance(args: Bundle)
    = MyFragment().apply { arguments = args }


    // 普通方法
    fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
    }
    // 改进方法,链式调用
    fun createIntent(intentData: String, intentAction: String) =
    Intent().apply { action = intentAction }
    .apply { data = Uri.parse(intentData) }

    汇总

    以表格的形式汇总,更方便去理解

    函数是否是扩展函数函数参数(this、it)返回值(调用本身、最后一行)
    with不是this最后一行
    T.runthis最后一行
    T.letit最后一行
    T.alsoit调用本身
    T.applythis调用本身

    使用 T.also 函数交换两个变量

    接下来演示的是使用 T.also 函数,实现一行代码交换两个变量?我们先来回顾一下 Java 的做法。

    int a = 1;
    int b = 2;

    // Java - 中间变量
    int temp = a;
    a = b;
    b = temp;
    System.out.println("a = "+a +" b = "+b); // a = 2 b = 1

    // Java - 加减运算
    a = a + b;
    b = a - b;
    a = a - b;
    System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

    // Java - 位运算
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

    // Kotlin
    a = b.also { b = a }
    println("a = ${a} b = ${b}") // a = 2 b = 1

    来一起分析 T.also 是如何做到的,其实这里用到了 T.also 函数的两个特点。

    • 调用 T.also 函数返回的是调用者本身。
    • 在使用之前可以进行自我操作。

    也就是说 b.also { b = a } 会先将 a 的值 (1) 赋值给 b,此时 b 的值为 1,然后将 b 原始的值(2)赋值给 a,此时 a 的值为 2,实现交换两个变量的目的。

    in 和 when 关键字

    使用 in 和 when 关键字结合正则表达式,验证用户的输入,这是一个很酷的技巧。

    // 使用扩展函数重写 contains 操作符
    operator fun Regex.contains(text: CharSequence) : Boolean {
    return this.containsMatchIn(text)
    }

    // 结合着 in 和 when 一起使用
    when (input) {
    in Regex("[0–9]") -> println("contains a number")
    in Regex("[a-zA-Z]") -> println("contains a letter")
    }

    in 关键字其实是 contains 操作符的简写,它不是一个接口,也不是一个类型,仅仅是一个操作符,也就是说任意一个类只要重写了 contains 操作符,都可以使用 in 关键字,如果我们想要在自定义类型中检查一个值是否在列表中,只需要重写 contains() 方法即可,Collections 集合也重写了 contains 操作符。

    val input = "kotlin"

    when (input) {
    in listOf("java", "kotlin") -> println("found ${input}")
    in setOf("python", "c++") -> println("found ${input}")
    else -> println(" not found ${input}")
    }

    Kotlin 的单例三种写法

    我汇总了一下目前 Kotlin 单例总共有三种写法:

    • 使用 Object 实现单例。
    • 使用 by lazy 实现单例。
    • 可接受参数的单例(来自大神 Christophe Beyls)。

    使用 Object 实现单例

    代码:

    object WorkSingleton

    Kotlin 当中 Object 关键字就是一个单例,比 Java 的一坨代码看起来舒服了很多,来看一下编译后的 Java 文件。

    public final class WorkSingleton {
    public static final WorkSingleton INSTANCE;

    static {
    WorkSingleton var0 = new WorkSingleton();
    INSTANCE = var0;
    }
    }

    通过 static 代码块实现的单例,优点:饿汉式且是线程安全的,缺点:类加载时就初始化,浪费内存。

    使用 by lazy 实现单例

    利用伴生对象 和 by lazy 也可以实现单例,代码如下所示。

    class WorkSingleton private constructor() {

    companion object {

    // 方式一
    val INSTANCE1 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WorkSingleton() }

    // 方式二 默认就是 LazyThreadSafetyMode.SYNCHRONIZED,可以省略不写,如下所示
    val INSTANCE2 by lazy { WorkSingleton() }
    }
    }

    lazy 的延迟模式有三种:

    • 上面代码所示 mode = LazyThreadSafetyMode.SYNCHRONIZED,lazy 默认的模式,可以省掉,这个模式的意思是:如果有多个线程访问,只有一条线程可以去初始化 lazy 对象。

    • 当 mode = LazyThreadSafetyMode.PUBLICATION 表达的意思是:对于还没有被初始化的 lazy 对象,可以被不同的线程调用,如果 lazy 对象初始化完成,其他的线程使用的是初始化完成的值。

    • mode = LazyThreadSafetyMode.NONE 表达的意思是:只能在单线程下使用,不能在多线程下使用,不会有锁的限制,也就是说它不会有任何线程安全的保证以及相关的开销。

    通过上面三种模式,这就可以理解为什么 by lazy 声明的变量只能用 val,因为初始化完成之后它的值是不会变的。

    可接受参数的单例

    但是有的时候,希望在单例实例化的时候传递参数,例如:

    Singleton.getInstance(context).doSome()

    上面这两种形式都不能满足,来看看大神 Christophe Beyls 在这篇文章给出的方法 Kotlin singletons with argument 代码如下。

    class WorkSingleton private constructor(context: Context) {
    init {
    // Init using context argument
    }

    companion object : SingletonHolder<WorkSingleton, Context>(::WorkSingleton)
    }


    open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile
    private var instance: T? = null

    fun getInstance(arg: A): T {
    val i = instance
    if (i != null) {
    return i
    }

    return synchronized(this) {
    val i2 = instance
    if (i2 != null) {
    i2
    } else {
    val created = creator!!(arg)
    instance = created
    creator = null
    created
    }
    }
    }
    }

    有没有感觉这和 Java 中双重校验锁的机制很像,在 SingletonHolder 类中如果已经初始化了直接返回,如果没有初始化进入 synchronized 代码块创建对象,利用了 Kotlin 伴生对象提供的非常强大功能,它能够像其他任何对象一样从基类继承,从而实现了与静态继承相当的功能。 所以我们将 SingletonHolder 作为单例类伴随对象的基类,在单例类上重用并公开 getInstance()函数。

    参数传递给 SingletonHolder 构造函数的 creator,creator 是一个 lambda 表达式,将 WorkSingleton 传递给 SingletonHolder 类构造函数。

    并且不限制传入参数的类型,凡是需要传递参数的单例模式,只需将单例类的伴随对象继承于 SingletonHolder,然后传入当前的单例类和参数类型即可,例如:

    class FileSingleton private constructor(path: String) {

    companion object : SingletonHolder<FileSingleton, String>(::FileSingleton)

    }
    收起阅读 »

    解析android匿名共享内存几个关键函数

    基础知识当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和...
    继续阅读 »

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }


    内存几个关键函数

    基础原理

    android系统在应用程序框架层中提供了两个C++类MemoryHeapBase和MemoryBase来创建和管理匿名共享内存。 如果一个进程需要与其他进程共享一块完整的匿名共享内存,那么就可以通过使用MemoryHeapBase类类创建这块匿名共享内存。如果一个进程创建一块匿名共享内存后,只希望与其他进程共享其中的一部分,那么就可以通过MemoryBase类来创建这块匿名共享内存。

    IMemory.h:定义内存相关类的接口,表示堆内存的类IMemoryHeap和BnMemoryHeap,表示一般内存的类IMemory和BnMemory。 MemoryHeapBase.h:定义类MemoryHeapBase,继承并实现BnMemoryHeap MemoryBase.h:定义类MemoryBase,继承并实现BnMemory。

    android系统在应用程序框架层中提供了java类MemoryFile来创建和管理匿名共享内存。使用java类MemoryFile创建的匿名共享内存可以在不同的Android应用程序之间进行共享。

    java代码解析

    匿名共享内存java类MemoryFile在系统中的source\frameworks\base\core\java\android\os\MemoryFile.java文件中实现。

    //匿名共享内存的构造函数,参数1表示创建匿名共享内存的名称,参数2表示创建匿名共享内存大小
    public MemoryFile(String name int length) throws IOException {
    mLength = length;
    if (length >= 0) {
    //通过调用jni的接口去打开匿名共享内存
    mFD = native_open(name length);
    } else {
    throw new IOException("Invalid length: " + length);
    }

    if (length > 0) {
    //进行映射
    mAddress = native_mmap(mFD length PROT_READ | PROT_WRITE);
    } else {
    mAddress = 0;
    }
    }

    C++关键函数解析

    //MemoryHeapBase构造函数的实现
    MemoryHeapBase::MemoryHeapBase(const char* device size_t size uint32_t flags)
    : mFD(-1) mSize(0) mBase(MAP_FAILED) mFlags(flags)
    mDevice(0) mNeedUnmap(false) mOffset(0)
    {
    int open_flags = O_RDWR;
    if (flags & NO_CACHING)
    open_flags |= O_SYNC;
    //通过调用open打开匿名共享内存设备文件
    int fd = open(device open_flags);
    ALOGE_IF(fd<0 "error opening %s: %s" device strerror(errno));
    if (fd >= 0) {
    //指定的匿名共享内存大小按页对齐
    const size_t pagesize = getpagesize();
    size = ((size + pagesize-1) & ~(pagesize-1));
    //匿名共享内存映射到当前进程地址空间
    if (mapfd(fd size) == NO_ERROR) {
    mDevice = device;
    }
    }
    }
    //MemoryHeapBase构造函数
    MemoryHeapBase::MemoryHeapBase(size_t size uint32_t flags char const * name)
    : mFD(-1) mSize(0) mBase(MAP_FAILED) mFlags(flags)
    mDevice(0) mNeedUnmap(false) mOffset(0)
    {
    //获得系统中页大小的内存
    const size_t pagesize = getpagesize();
    //内存页对齐
    size = ((size + pagesize-1) & ~(pagesize-1));
    //创建一块匿名共享内存
    int fd = ashmem_create_region(name == NULL ? "MemoryHeapBase" : name size);
    ALOGE_IF(fd<0 "error creating ashmem region: %s" strerror(errno));
    if (fd >= 0) {
    //创建的匿名共享内存映射到当前进程地址空间中
    if (mapfd(fd size) == NO_ERROR) {
    if (flags & READ_ONLY) {//如果地址映射成功,修改匿名共享内存的访问属性
    ashmem_set_prot_region(fd PROT_READ);
    }
    }
    }
    }

    初探android系统中input的java层实现

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }

    初探android系统中input的java层实现

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }


    收起阅读 »

    iOS 隐式动画 二

    图层行为现在来做个实验,试着直接对UIView关联的图层做动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了colorLayer,并且直接设置layerView关联图层的背景色。清单7.4 直接设置图层的属性@interface View...
    继续阅读 »

    图层行为

    现在来做个实验,试着直接对UIView关联的图层做动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了colorLayer,并且直接设置layerView关联图层的背景色。

    清单7.4 直接设置图层的属性

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //set the color of our layerView backing layer directly
    self.layerView.layer.backgroundColor = [UIColor blueColor].CGColor;
    }

    - (IBAction)changeColor
    {
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.layerView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
    }

    运行程序,你会发现当按下按钮,图层颜色瞬间切换到新的值,而不是之前平滑过渡的动画。发生了什么呢?隐式动画好像被UIView关联图层给禁用了。

    试想一下,如果UIView的属性都有动画特性的话,那么无论在什么时候修改它,我们都应该能注意到的。所以,如果说UIKit建立在Core Animation(默认对所有东西都做动画)之上,那么隐式动画是如何被UIKit禁用掉呢?

    我们知道Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把它关联的图层的这个特性关闭了。为了更好说明这一点,我们需要知道隐式动画是如何实现的。

    我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:

    • 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
    • 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
    • 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
    • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。

    所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。

    于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。我们可以用一个demo做个简单的实验(清单7.5)

    清单7.5 测试UIView的actionForLayer:forKey:实现

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //test layer action when outside of animation block
    NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //begin animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block
    NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //end animation block
    [UIView commitAnimations];
    }

    @end

    运行程序,控制台显示结果如下:

    $ LayerTest[21215:c07] Outside: <null>
    $ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>

    于是我们可以预言,当属性在动画块之外发生改变,UIView直接通过返回nil来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性,在这个例子就是CABasicAnimation(第八章“显式动画”将会提到)。

    当然返回nil并不是禁用隐式动画唯一的办法,CATransacition有个方法叫做+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的[CATransaction begin]之后添加下面的代码,同样也会阻止动画的发生:

    [CATransaction setDisableActions:YES];

    总结一下,我们知道了如下几点

    • UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画(具体细节见第八章)。
    • 对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。

    我们来对颜色渐变的例子使用一个不同的行为,通过给colorLayer设置一个自定义的actions字典。我们也可以使用委托来实现,但是actions字典可以写更少的代码。那么到底改如何创建一个合适的行为对象呢?

    行为通常是一个被Core Animation隐式调用的显式动画对象。这里我们使用的是一个实现了CATransaction的实例,叫做推进过渡

    第八章中将会详细解释过渡,不过对于现在,知道CATransition响应CAAction协议,并且可以当做一个图层行为就足够了。结果很赞,不论在什么时候改变背景颜色,新的色块都是从左侧滑入,而不是默认的渐变效果。

    清单7.6 实现自定义行为

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;
    @property (nonatomic, weak) IBOutlet CALayer *colorLayer;/*热心人发现这里应该改为@property (nonatomic, strong) CALayer *colorLayer;否则运行结果不正确。
    */

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];

    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add a custom action
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromLeft;
    self.colorLayer.actions = @{@"backgroundColor": transition};
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
    }

    - (IBAction)changeColor
    {
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    }

    @end

    图7.3

    图7.3 使用推进过渡的色值动画

    7.4 呈现与模型

    CALayer的属性行为其实很不正常,因为改变一个图层的属性并没有立刻生效,而是通过一段时间渐变更新。这是怎么做到的呢?

    当你改变一个图层的属性,属性值的确是立刻更新的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。

    当设置CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。

    我们讨论的就是一个典型的微型MVC模式CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。

    在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着CALayer除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。

    每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值(图7.4)。

    我们在第一章中提到除了图层树,另外还有呈现树。呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil

    你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。

    图7.4

    图7.4 一个移动的图层是如何通过数据模型呈现的

    大多数情况下,你不需要直接访问呈现图层,你可以通过和模型图层的交互,来让Core Animation更新显示。两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。

    • 如果你在实现一个基于定时器的动画(见第11章“基于定时器的动画”),而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。
    • 如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法(见第三章“图层几何学”)来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。

    我们可以用一个简单的案例来证明后者(见清单7.7)。在这个例子中,点击屏幕上的任意位置将会让图层平移到那里。点击图层本身可以随机改变它的颜色。我们通过对呈现图层调用-hitTest:来判断是否被点击。

    如果修改代码让-hitTest:直接作用于colorLayer而不是呈现图层,你会发现当图层移动的时候它并不能正确显示。这时候你就需要点击图层将要移动到的位置而不是图层本身来响应点击(这就是为什么用呈现图层来响应交互的原因)。

    清单7.7 使用presentationLayer图层来判断当前图层位置

    @interface ViewController ()

    @property (nonatomic, strong) CALayer *colorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a red layer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
    self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.colorLayer];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //get the touch point
    CGPoint point = [[touches anyObject] locationInView:self.view];
    //check if we've tapped the moving layer
    if ([self.colorLayer.presentationLayer hitTest:point]) {
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    } else {
    //otherwise (slowly) move the layer to new position
    [CATransaction begin];
    [CATransaction setAnimationDuration:4.0];
    self.colorLayer.position = point;
    [CATransaction commit];
    }
    }

    @end


    总结

    这一章讨论了隐式动画,还有Core Animation对指定属性选择合适的动画行为的机制。同时你知道了UIKit是如何充分利用Core Animation的隐式动画机制来强化它的显式系统,以及动画是如何被默认禁用并且当需要的时候启用的。最后,你了解了呈现和模型图层,以及Core Animation是如何通过它们来判断出图层当前位置以及将要到达的位置。

    在下一章中,我们将研究Core Animation提供的显式动画类型,既可以直接对图层属性做动画,也可以覆盖默认的图层行为。

    收起阅读 »

    iOS 隐式动画 一

    隐式动画按照我的意思去做,而不是我说的。 -- 埃德娜,辛普森我们在第一部分讨论了Core Animation除了动画之外可以做到的任何事情。但是动画是Core Animation库一个非常显著的特性。这一章我们来看看它是怎么做到的。具体来说,我们先...
    继续阅读 »

    隐式动画

    按照我的意思去做,而不是我说的。 -- 埃德娜,辛普森

    我们在第一部分讨论了Core Animation除了动画之外可以做到的任何事情。但是动画是Core Animation库一个非常显著的特性。这一章我们来看看它是怎么做到的。具体来说,我们先来讨论框架自动完成的隐式动画(除非你明确禁用了这个功能)。

    7.1 事务

    Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。

    当你改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。

    这看起来这太棒了,似乎不太真实,我们来用一个demo解释一下:首先和第一章“图层树”一样创建一个蓝色的方块,然后添加一个按钮,随机改变它的颜色。代码见清单7.1。点击按钮,你会发现图层的颜色平滑过渡到一个新值,而不是跳变(图7.1)。

    清单7.1 随机改变图层颜色

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;
    @property (nonatomic, weak) IBOutlet CALayer *colorLayer;/*热心人发现这里应该改为@property (nonatomic, strong) CALayer *colorLayer;否则运行结果不正确。
    */
    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
    }

    - (IBAction)changeColor
    {
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 
    }

    @end

    图7.1

    图7.1 添加一个按钮来控制图层颜色

    这其实就是所谓的隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。Core Animaiton同样支持显式动画,下章详细说明。

    但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为

    事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。

    事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc-init方法创建它。但是可以用+begin+commit分别来入栈或者出栈。

    任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。

    Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。

    明白这些之后,我们就可以轻松修改变色动画的时间了。我们当然可以用当前事务的+setAnimationDuration:方法来修改动画时间,但在这里我们首先起一个新的事务,于是修改时间就不会有别的副作用。因为修改当前事务的时间可能会导致同一时刻别的动画(如屏幕旋转),所以最好还是在调整动画之前压入一个新的事务。

    修改后的代码见清单7.2。运行程序,你会发现色块颜色比之前变得更慢了。

    清单7.2 使用CATransaction控制动画时间

    - (IBAction)changeColor
    {
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
    }

    如果你用过UIView的动画方法做过一些动画效果,那么应该对这个模式不陌生。UIView有两个方法,+beginAnimations:context:+commitAnimations,和CATransaction+begin+commit方法类似。实际上在+beginAnimations:context:+commitAnimations之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction的原因。

    在iOS4中,苹果对UIView添加了一种基于block的动画方法:+animateWithDuration:animations:。这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。

    CATransaction+begin+commit方法在+animateWithDuration:animations:内部自动调用,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对+begin+commit匹配的失误造成的风险。

    7.2 完成块

    基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。我们来调整上个例子,在颜色变化结束之后执行一些操作。我们来添加一个完成之后的block,用来在每次颜色变化结束之后切换到另一个旋转90的动画。代码见清单7.3,运行结果见图7.2。

    清单7.3 在颜色动画完成之后添加一个回调

    - (IBAction)changeColor
    {
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //add the spin animation on completion
    [CATransaction setCompletionBlock:^{
    //rotate the layer 90 degrees
    CGAffineTransform transform = self.colorLayer.affineTransform;
    transform = CGAffineTransformRotate(transform, M_PI_2);
    self.colorLayer.affineTransform = transform;
    }];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
    }

    图7.2

    图7.2 颜色渐变之完成之后再做一次旋转

    注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。


    收起阅读 »

    iOS 专用图层 八

    6.10 AVPlayerLayer最后一个图层类型是AVPlayerLayer。尽管它不是Core Animation框架的一部分(AV前缀看上去像),AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation...
    继续阅读 »

    6.10 AVPlayerLayer

    最后一个图层类型是AVPlayerLayer。尽管它不是Core Animation框架的一部分(AV前缀看上去像),AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation紧密地结合在一起,提供了一个CALayer子类来显示自定义的内容类型。

    AVPlayerLayer是用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。AVPlayerLayer的使用相当简单:你可以用+playerLayerWithPlayer:方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后用player属性绑定一个AVPlayer实例。

    在我们开始之前,我们需要添加AVFoundation到我们的项目中。然后,清单6.15创建了一个简单的电影播放器,图6.16是代码运行结果。

    清单6.15 用AVPlayerLayer播放视频

    #import "ViewController.h"
    #import
    #import

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView; @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //get video URL
    NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];

    //create player and player layer
    AVPlayer *player = [AVPlayer playerWithURL:URL];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];

    //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];

    //play the video
    [player play];
    }
    @end

    图6.16 用AVPlayerLayer图层播放视频的截图

    我们用代码创建了一个AVPlayerLayer,但是我们仍然把它添加到了一个容器视图中,而不是直接在controller中的主视图上添加。这样其实是为了可以使用自动布局限制使得图层在最中间;否则,一旦设备被旋转了我们就要手动重新放置位置,因为Core Animation并不支持自动大小和自动布局(见第三章『图层几何学』)。

    当然,因为AVPlayerLayerCALayer的子类,它继承了父类的所有特性。我们并不会受限于要在一个矩形中播放视频;清单6.16演示了在3D,圆角,有色边框,蒙板,阴影等效果(见图6.17).

    清单6.16 给视频增加变换,边框和圆角

    - (void)viewDidLoad
    {
    ...
    //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];

    //transform layer
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / 500.0;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
    playerLayer.transform = transform;

    //add rounded corners and border
    playerLayer.masksToBounds = YES;
    playerLayer.cornerRadius = 20.0;
    playerLayer.borderColor = [UIColor redColor].CGColor;
    playerLayer.borderWidth = 5.0;

    //play the video
    [player play];
    }

    图6.17 3D视角下的边框和圆角AVPlayerLayer

    总结
    这一章我们简要概述了一些专用图层以及用他们实现的一些效果,我们只是了解到这些图层的皮毛,像CATiledLayer和CAEMitterLayer这些类可以单独写一章的。但是,重点是记住CALayer是用处很大的,而且它并没有为所有可能的场景进行优化。为了获得Core Animation最好的性能,你需要为你的工作选对正确的工具,希望你能够挖掘这些不同的CALayer子类的功能。 这一章我们通过CAEmitterLayer和AVPlayerLayer类简单地接触到了一些动画,在第二章,我们将继续深入研究动画,就从隐式动画开始。

    收起阅读 »

    iOS 专用图层 七

    6.9 CAEAGLLayer当iOS要处理高性能图形绘制,必要时就是OpenGL。应该说它应该是最后的杀手锏,至少对于非游戏的应用来说是的。因为相比Core Animation和UIkit框架,它不可思议地复杂。OpenGL提供了Core Animation...
    继续阅读 »

    6.9 CAEAGLLayer

    当iOS要处理高性能图形绘制,必要时就是OpenGL。应该说它应该是最后的杀手锏,至少对于非游戏的应用来说是的。因为相比Core Animation和UIkit框架,它不可思议地复杂。

    OpenGL提供了Core Animation的基础,它是底层的C接口,直接和iPhone,iPad的硬件通信,极少地抽象出来的方法。OpenGL没有对象或是图层的继承概念。它只是简单地处理三角形。OpenGL中所有东西都是3D空间中有颜色和纹理的三角形。用起来非常复杂和强大,但是用OpenGL绘制iOS用户界面就需要很多很多的工作了。

    为了能够以高性能使用Core Animation,你需要判断你需要绘制哪种内容(矢量图形,例子,文本,等等),但后选择合适的图层去呈现这些内容,Core Animation中只有一些类型的内容是被高度优化的;所以如果你想绘制的东西并不能找到标准的图层类,想要得到高性能就比较费事情了。

    因为OpenGL根本不会对你的内容进行假设,它能够绘制得相当快。利用OpenGL,你可以绘制任何你知道必要的集合信息和形状逻辑的内容。所以很多游戏都喜欢用OpenGL(这些情况下,Core Animation的限制就明显了:它优化过的内容类型并不一定能满足需求),但是这样依赖,方便的高度抽象接口就没了。

    在iOS 5中,苹果引入了一个新的框架叫做GLKit,它去掉了一些设置OpenGL的复杂性,提供了一个叫做CLKViewUIView的子类,帮你处理大部分的设置和绘制工作。前提是各种各样的OpenGL绘图缓冲的底层可配置项仍然需要你用CAEAGLLayer完成,它是CALayer的一个子类,用来显示任意的OpenGL图形。

    大部分情况下你都不需要手动设置CAEAGLLayer(假设用GLKView),过去的日子就不要再提了。特别的,我们将设置一个OpenGL ES 2.0的上下文,它是现代的iOS设备的标准做法。

    尽管不需要GLKit也可以做到这一切,但是GLKit囊括了很多额外的工作,比如设置顶点和片段着色器,这些都以类C语言叫做GLSL自包含在程序中,同时在运行时载入到图形硬件中。编写GLSL代码和设置EAGLayer没有什么关系,所以我们将用GLKBaseEffect类将着色逻辑抽象出来。其他的事情,我们还是会有以往的方式。

    在开始之前,你需要将GLKit和OpenGLES框架加入到你的项目中,然后就可以实现清单6.14中的代码,里面是设置一个GAEAGLLayer的最少工作,它使用了OpenGL ES 2.0 的绘图上下文,并渲染了一个有色三角(见图6.15).

    清单6.14 用CAEAGLLayer绘制一个三角形

    #import "ViewController.h"
    #import
    #import

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *glView;
    @property (nonatomic, strong) EAGLContext *glContext;
    @property (nonatomic, strong) CAEAGLLayer *glLayer;
    @property (nonatomic, assign) GLuint framebuffer;
    @property (nonatomic, assign) GLuint colorRenderbuffer;
    @property (nonatomic, assign) GLint framebufferWidth;
    @property (nonatomic, assign) GLint framebufferHeight;
    @property (nonatomic, strong) GLKBaseEffect *effect;

    @end

    @implementation ViewController

    - (void)setUpBuffers
    {
    //set up frame buffer
    glGenFramebuffers(1, &_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);

    //set up color render buffer
    glGenRenderbuffers(1, &_colorRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);

    //check success
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
    }

    - (void)tearDownBuffers
    {
    if (_framebuffer) {
    //delete framebuffer
    glDeleteFramebuffers(1, &_framebuffer);
    _framebuffer = 0;
    }

    if (_colorRenderbuffer) {
    //delete color render buffer
    glDeleteRenderbuffers(1, &_colorRenderbuffer);
    _colorRenderbuffer = 0;
    }
    }

    - (void)drawFrame {
    //bind framebuffer & set viewport
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    glViewport(0, 0, _framebufferWidth, _framebufferHeight);

    //bind shader program
    [self.effect prepareToDraw];

    //clear the screen
    glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);

    //set up vertices
    GLfloat vertices[] = {
    -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
    };

    //set up colors
    GLfloat colors[] = {
    0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
    };

    //draw triangle
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    //present render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
    }

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //set up context
    self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.glContext];

    //set up layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = self.glView.bounds;
    [self.glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};

    //set up base effect
    self.effect = [[GLKBaseEffect alloc] init];

    //set up buffers
    [self setUpBuffers];

    //draw frame
    [self drawFrame];
    }

    - (void)viewDidUnload
    {
    [self tearDownBuffers];
    [super viewDidUnload];
    }

    - (void)dealloc
    {
    [self tearDownBuffers];
    [EAGLContext setCurrentContext:nil];
    }
    @end

    图6.15

    图6.15 用OpenGL渲染的CAEAGLLayer图层

    在一个真正的OpenGL应用中,我们可能会用NSTimerCADisplayLink周期性地每秒钟调用-drawRrame方法60次,同时会将几何图形生成和绘制分开以便不会每次都重新生成三角形的顶点(这样也可以让我们绘制其他的一些东西而不是一个三角形而已),不过上面这个例子已经足够演示了绘图原则了。

    收起阅读 »

    【插件&热修系列】ClassLoader方案设计

    引言 上一个阶段我们开始进入插件/热修的领域,了解了热修的前世今生,下面我们来学习下热修中的ClassLoader方案设计; ClassLoader主要是用来加载插件用的,在启动插件前首先要把插件加载进来,下面我们通过不同方案分析,了解加载的不同姿势~ ...
    继续阅读 »

    引言


    上一个阶段我们开始进入插件/热修的领域,了解了热修的前世今生,下面我们来学习下热修中的ClassLoader方案设计;


    ClassLoader主要是用来加载插件用的,在启动插件前首先要把插件加载进来,下面我们通过不同方案分析,了解加载的不同姿势~


    方案1:合并Dex(hook方式)


    谁用了这个方案?


    QQ团队的空间换肤功能


    原理


    将我们插件dex和宿主apk的class.dex合并,都放到宿主dexElements数组中。App每次启动从该数组中加载。


    实战流程


    1)获取宿主,dexElements


    2)获取插件,dexElements


    3)合并两个dexElements


    4)将新的dexElements 赋值到 宿主dexElements


    代码


    Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
    Field pathListField = clazz.getDeclaredField("pathList");
    pathListField.setAccessible(true);

    Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
    Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
    dexElementsField.setAccessible(true);

    // 宿主的 类加载器
    ClassLoader pathClassLoader = context.getClassLoader();
    // DexPathList类的对象
    Object hostPathList = pathListField.get(pathClassLoader);
    // 宿主的 dexElements
    Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

    // 插件的 类加载器
    ClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(),null, pathClassLoader);
    // DexPathList类的对象
    Object pluginPathList = pathListField.get(dexClassLoader);
    // 插件的 dexElements
    Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

    // 宿主dexElements = 宿主dexElements + 插件dexElements
    // 创建一个新数组
    Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),hostDexElements.length + pluginDexElements.length);
    // 拷贝
    System.arraycopy(hostDexElements, 0, newDexElements,0, hostDexElements.length);
    System.arraycopy(pluginDexElements, 0,newDexElements,hostDexElements.length, pluginDexElements.length);

    // 赋值
    dexElementsField.set(hostPathList, newDexElements);


    特点


    此乃单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,存在的短板为“插件之间或者插件与宿主之间使用的类库有相同的时候,那么就会加载乱序等问题”


    方案2:替换 PathClassloader 的 parent


    谁用了这个方案?


    微店、Instant-Run


    知识基础


    安装在手机里的apk(宿主)的ClassLoader链路关系


    1)代码:


    ClassLoader classLoader = getClassLoader();
    ClassLoader parentClassLoader = classLoader.getParent();
    ClassLoader pParentClassLoader = parentClassLoader.getParent();

    2)关系:


    ==classLoader==:dalvik.system.PathClassLoader


    ==parentClassLoader==:java.lang.BootClassLoader


    ==pParentClassLoader==:null


    可以看出,当前的classLoader是PathClassLoader,parent的ClassLoader是BootClassLoader,而BootClassLoader没有parent的ClassLoader


    实现思想


    如何利用上面的宿主链路基础原理设计?


    ClassLoader的构造方法中有一个参数是parent; 如果把PathClassLoader的parent替换成我们==插件的classLoader==; 再把==插件的classLoader的parent==设置成BootClassLoader; 加上父委托的机制,查找插件类的过程就变成:BootClassLoader->==插件的classLoader==->PathClassLoader


    代码实现


    public static void loadApk(Context context, String apkPath) {
    File dexFile = context.getDir("dex", Context.MODE_PRIVATE);
    File apkFile = new File(apkPath);
    //找到 PathClassLoader
    ClassLoader classLoader = context.getClassLoader();
    //构建插件的 ClassLoader
    //PathClassLoader 的父亲 传递给 插件的ClassLoader
    //到这里,顺序为:BootClassLoader->插件的classLoader
    DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(),dexFile.getAbsolutePath(), null,classLoader.getParent());
    try {
    //PathClassLoader 的父亲设置为 插件的ClassLoader
    //顺序为:BootClassLoader->插件的classLoader->PathClassLoader
    Field fieldClassLoader = ClassLoader.class.getDeclaredField("parent");
    if (fieldClassLoader != null) {
    fieldClassLoader.setAccessible(true);
    fieldClassLoader.set(classLoader, dexClassLoader);
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    特点


    此乃单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,存在的短板为“插件之间或者插件与宿主之间使用的类库有相同的时候,那么就会加载乱序等问题”


    方案3:利用LoadedApk的缓存机制


    谁用了这个方案?


    360的DroidPlugin


    实现原理


    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    StrictMode.incrementExpectedActivityCount(activity.getClass());
    r.intent.setExtrasClassLoader(cl);

    上面代码做了两件事:


    1)系统用packageInfo.getClassLoader()来加载已安装app的Activity


    2)实例化的Activity


    其中packageInfo为LoadedApk类型,是APK文件在内存中的表示,Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。


    packageInfo怎么生成的?通过阅读源码得出:


    1)先在ActivityThread中的mPackages缓存(Map,key为包名,value为LoadedApk)中获取


    2)如果缓存没有,new LoadedApk 生成一个,然后放到缓存mPackages中


    基于上面系统的原理,实现的关键点步骤:


    1)构建插件 ApplicationInfo 信息


    ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,packageObj, 0, defaultPackageUserState);
    String apkPath = apkFile.getPath();
    applicationInfo.sourceDir = apkPath;
    applicationInfo.publicSourceDir = apkPath;

    2)构建 CompatibilityInfo


    Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
    Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
    defaultCompatibilityInfoField.setAccessible(true);
    Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);

    3)根据 ApplicationInfo 和 CompatibilityInfo,构建插件的 loadedApk


    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);
    Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

    4)构建插件的ClassLoader,然后把它替换到插件loadedApk的ClassLoader中


    String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
    String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
    ClassLoader classLoader = new DexClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
    Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
    mClassLoaderField.setAccessible(true);
    mClassLoaderField.set(loadedApk, classLoader);

    5)把插件loadedApk添加进ActivityThread的mPackages中


    // 先获取到当前的ActivityThread对象
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    // 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
    Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
    mPackagesField.setAccessible(true);
    Map mPackages = (Map) mPackagesField.get(currentActivityThread);

    // 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC; 那么就前功尽弃了.
    sLoadedApk.put(applicationInfo.packageName, loadedApk);
    WeakReference weakReference = new WeakReference(loadedApk);
    mPackages.put(applicationInfo.packageName, weakReference);

    6)绕过系统检查,让系统觉得插件已经安装在系统上了


    private static void hookPackageManager() throws Exception {
    // 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
    // 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);

    // 获取ActivityThread里面原始的 sPackageManager
    Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
    sPackageManagerField.setAccessible(true);
    Object sPackageManager = sPackageManagerField.get(currentActivityThread);

    // 准备好代理对象, 用来替换原始的对象
    Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
    Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
    new Class<?>[] { iPackageManagerInterface },
    new IPackageManagerHookHandler(sPackageManager));

    // 1. 替换掉ActivityThread里面的 sPackageManager 字段
    sPackageManagerField.set(currentActivityThread, proxy);
    }


    特点


    1)自定义了插件的ClassLoader,并且绕开了Framework的检测


    2)Hook的地方也有点多:不仅需要Hook AMS和H,还需要Hook ActivityThread的mPackages和PackageManager!


    3)多ClassLoader构架,每一个插件都有一个自己的ClassLoader,隔离性好,如果不同的插件使用了同一个库的不同版本,它们相安无事


    4)真正完成代码的热加载!


    插件需要升级,直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可(Java中,不同ClassLoader加载的同一个类被认为是不同的类)


    单ClassLoader的话实现非常麻烦,有可能需要重启进程。


    方案4:自定义ClassLoader逻辑


    谁用了?


    腾讯视频等事业群中的Shadow热修框架


    实现原理


    1)先了解下宿主(已经安装App)的ClassLoader链路: BootClassLoader -> PathClassLoader


    2)插件可以加载宿主的类实现:


    构建插件的ClassLoader,名字为ApkClassLoader,其中父加载器传的是宿主的ClassLoader,代码片段为:


    class ApkClassLoader extends DexClassLoader {

    static final String TAG = "daviAndroid";
    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    @Deprecated
    ApkClassLoader(InstalledApk installedApk,
    ClassLoader parent,////parent = 宿主ClassLoader
    String[] mInterfacePackageNames,
    int grandTimes) {

    super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);

    在这个流程下,插件查找的流程变为: BootClassLoader -> PathClassLoader -> ApkClassLoader(其实就是双亲委托)


    3)插件不需要加载宿主的类实现:


    class ApkClassLoader extends DexClassLoader {

    ............
    //1)系统里面找
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
    //2)从自己的dexPath中查找
    clazz = findClass(className);
    if (clazz == null) {
    //3)从parent的parent找(BootClassLoader)ClassLoader中查找。
    clazz = mGrandParent.loadClass(className);
    }
    }

    ............
    }

    这个逻辑插件不需要加载宿主的类,所以加载逻辑中不会去加载宿主的类(也就是会经过PathClassLoader),这种情况下,即使插件和宿主用到了同一个类,那么插件加载的时候不会因为委托加载机制而去加载了宿主的,导致插件的加载错了;


    代码实现


    class ApkClassLoader extends DexClassLoader {
    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    ApkClassLoader(InstalledApk installedApk,
    ClassLoader parent, String[] mInterfacePackageNames, int grandTimes) {
    super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);
    ClassLoader grand = parent;
    for (int i = 0; i < grandTimes; i++) {
    grand = grand.getParent();
    }
    mGrandParent = grand;
    this.mInterfacePackageNames = mInterfacePackageNames;
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    String packageName;
    int dot = className.lastIndexOf('.');
    if (dot != -1) {
    packageName = className.substring(0, dot);
    } else {
    packageName = "";
    }

    boolean isInterface = false;
    for (String interfacePackageName : mInterfacePackageNames) {
    if (packageName.equals(interfacePackageName)) {
    isInterface = true;
    break;
    }
    }

    if (isInterface) {
    return super.loadClass(className, resolve);
    } else {
    Class<?> clazz = findLoadedClass(className);

    if (clazz == null) {
    ClassNotFoundException suppressed = null;
    try {
    clazz = findClass(className);
    } catch (ClassNotFoundException e) {
    suppressed = e;
    }

    if (clazz == null) {
    try {
    clazz = mGrandParent.loadClass(className);
    } catch (ClassNotFoundException e) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    e.addSuppressed(suppressed);
    }
    throw e;
    }
    }
    }

    return clazz;
    }
    }

    /**
    * 从apk中读取接口的实现
    *
    * @param clazz 接口类
    * @param className 实现类的类名
    * @param <T> 接口类型
    * @return 所需接口
    * @throws Exception
    */
    <T> T getInterface(Class<T> clazz, String className) throws Exception {
    try {
    Class<?> interfaceImplementClass = loadClass(className);
    Object interfaceImplement = interfaceImplementClass.newInstance();
    return clazz.cast(interfaceImplement);
    } catch (ClassNotFoundException | InstantiationException
    | ClassCastException | IllegalAccessException e) {
    throw new Exception(e);
    }
    }

    }

    该代码实现不正常的双亲委派逻辑,既能和parent隔离类加载(和宿主),也能通过白名单复用一些宿主的类


    特点


    1)属于多ClassLoader方案


    2)插件可以选择加载宿主的类和绕过宿主加载,选择性强


    结尾


    哈哈,该篇就写到这里(一起体系化学习,一起成长)


    收起阅读 »

    Suspension(挂起/暂停) 在Kotlin coroutines里面到底是如何工作的?

    前言 挂起函数是Kotlin协程的标志。挂起功能也是其中最重要的功能,所有其他功能都建立在此基础上。这也是为什么在这篇文章中,我们目标是深入了解它的工作原理。 挂起一个协程(suspending a coroutine)意味着在其(代码块)执行过程中中断(挂起...
    继续阅读 »

    前言


    挂起函数是Kotlin协程的标志。挂起功能也是其中最重要的功能,所有其他功能都建立在此基础上。这也是为什么在这篇文章中,我们目标是深入了解它的工作原理。


    挂起一个协程(suspending a coroutine)意味着在其(代码块)执行过程中中断(挂起)它。和咱们停止玩电脑单机游戏很类似: 你保存并关闭了游戏,紧接着你和你的电脑又去干其他不同的事儿去了。然后,过了一段时间,你想继续玩游戏。所以你重新打开游戏,恢复之前保存的位置,继续从你之前玩的地方开始玩起了游戏。


    上面所讲的场景是协程的一个形象比喻。他们(任务/一段代码)可以被中断(挂起去执行去他任务),当他们要回来(任务执行完成)的时候,他们通过返回一个Continuation(指定了我们恢复到的位置)。我们可以用它(Continuation)来继续我们的任务从之前我们中断的地方。


    Resume(恢复)


    那么我们来看一下它(Resume)的实际效果。首先,我们需要一个协程代码块。创建协程的最简单方式是直接写一个suspend函数,下面这段代码是我们的起始点:


    suspend fun testCoroutine() {
    println("Before")

    println("After")
    }
    //依次输出
    //Before
    //After


    上面代码很简单:会依次输出“Before”和“After”。这个时候如果我们在两行代码中间挂起的话会发生什么?为了到达挂起的效果,我们可以使用kotlin标准库提供的suspendCoroutine方法:


    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> {

    }

    println("After")
    }

    //依次输出
    //Before

    如果你调用上面的代码,你将不会看到”After“,而且这个代码将会一直运行下去(也就是说我们的testCoroutine方法不会结束)。这个协程在打印完”Before“后就被挂起了。我们的代码快被中断了,而且不会被恢复。所以?我们该怎么做呢?哪里有提到Continuation(可以主动恢复)吗?


    再看一下suspendCoroutine的调用, 而且注意它是以一个lambda表达式结尾。这个方法在挂起前给我们传递了一个参数,它的类型是Continuation


    uspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    println("Before too")
    }

    println("After")
    }

    //依次输出
    //Before
    //Before too

    上面的代码添加了: 在lambda表达式里面调用了另外一个方法, 好吧,这不是啥新鲜事儿。这个就和letapply等类似。suspendCoroutine方法需要这样子设计以便在协程挂起之前就拿到了continuation。如果suspendCoroutine执行了,那就晚了,所以lambda表达式将会在挂起前被调用。这样子设计的好处就是可以在某些时机可以恢复或者存储continuation。so 我们可以让continuation立即恢复


    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    continuation.resume(Unit)
    }

    println("After")
    }

    //依次输出
    //Before
    //After

    我们也可以用它来开启一个新的线程,而且还延迟了一会儿才恢复它:


    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    thread {
    Thread.sleep(1000)
    continuation.resume(Unit)
    }
    }

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After

    这是一个重要的发现。注意,新启动一个线程的代码可以提到一个方法里面,而且恢复可以通过回调来触发。在这种情况下,continuation将被lambda表达式捕获:


    fun invokeAfterSecond(operation: () -> Unit) {
    thread {
    Thread.sleep(1000)
    operation.invoke()
    }
    }

    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    invokeAfterSecond {
    continuation.resume(Unit)
    }
    }

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After


    这种机制是有效的,但是上面的代码我们没必要通过创建线程来做。线程是昂贵的,所以为啥子要浪费它们?一种更好的方式是设置一个闹钟。在JVM上面,我们可以使用ScheduledExecutorService。我们可以使用它来触发*continuation.resume(Unit)*在一定时间后:


    private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply { isDaemon = true }
    }

    suspend fun testCoroutine() {
    println("Before")


    suspendCoroutine<Unit> { continuation ->
    executor.schedule({
    continuation.resume(Unit)
    }, 1000, TimeUnit.MILLISECONDS)
    }

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After

    “挂起一定时间后恢复” 看起来像是一个很常用的功能。那我们就把它提到一个方法内,并且我们将这个方法命名为delay


    private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply { isDaemon = true }
    }

    suspend fun delay(time: Long) = suspendCoroutine<Unit> { cont ->
    executor.schedule({
    cont.resume(Unit)
    }, time, TimeUnit.MILLISECONDS)
    }

    suspend fun testCoroutine() {
    println("Before")

    delay(1000)

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After

    实际上上面的代码就是kotlin协程库delay的具体实现。我们的实现比较复杂,主要是为了支持测试,但是本质思想是一样的。


    Resuming with value(带值恢复)


    有件事可能一直让你感到疑惑:为啥我们调用resume方法的时候传递的是Unit?也有可能你会问为啥子我写suspendCoroutine方法的时候前面也带了Unit类型。实际上这两个是同一类型不是巧合:一个作为continuation恢复的时候入参类型,一个作为suspendCoroutine方法的返回值类型(指定我们要返回什么类型的值),这两个类型要保持一致:


    val ret: Unit =
    suspendCoroutine<Unit> { continuation ->
    continuation.resume(Unit)
    }

    当我们调用suspendCoroutine,我们决定了continuation恢复时候的数据类型,当然这个恢复时候返回的数据也作为了suspendCoroutine方法的返回值:


    suspend fun testCoroutine() {

    val i: Int = suspendCoroutine<Int> { continuation ->
    continuation.resume(42)
    }
    println(i)//42

    val str: String = suspendCoroutine<String> { continuation ->
    continuation.resume("Some text")
    }
    println(str)//Some text

    val b: Boolean = suspendCoroutine<Boolean> { continuation ->
    continuation.resume(true)
    }
    println(b)//true
    }

    上面这些代码好像和咱们之前聊得游戏有点不一样,没有任何一款游戏可以在恢复进度得时候你可以携带一些东西(除非你作弊或者谷歌了下知道下一个挑战是什么)。但是上面代码有返回值的设计方式对于协程来说却意义非凡。我们经常挂起是因为我们需要等待一些数据。比如,我们需要通过API网络请求获取数据,这是一个很常见的场景。一个线程正在处理业务逻辑,处理到某个点的时候,我们需要一些数据才能继续往下执行,这个时候我们通过网络库去请求数据并返回给我们。如果没有协程,这个线程则需要停下来等待。这是一个巨大的浪费---线程资源是非常昂贵的。尤其当这个线程是很重要的线程的时候,就像Android里面的Main Thread。但是有了协程就不一样了,这个网络请求只需要挂起,然后我们给网络请求库传递一个带有自我介绍的continuation:”一旦你获取到数据了,就将他们扔到我的resume方法里面“。然后这个线程就可以去做其他事儿了。一旦数据返回了,当前或其他方法(依赖于我们设置的dispatcher)就会从之前协程挂起的地方继续执行了。


    紧着我们实践一波,通过回调函数来模拟一下我们的网络库:


    data class User(val name: String)

    fun requestUser(callback: (User) -> Unit) {
    thread {
    Thread.sleep(1000)
    callback.invoke(User("hyy"))
    }
    }
    suspend fun testCoroutine() {
    println("Before")

    val user: User =
    suspendCoroutine<User> { continuation ->
    requestUser {
    continuation.resume(it)
    }
    }

    println(user)
    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //User(name=hyy)
    //After

    直接调用suspendCoroutine不是很方便,我们可以抽取一个挂起函数来替代:


    suspend fun requestUser(): User {
    return suspendCoroutine<User> { continuation ->
    requestUser {
    continuation.resume(it)
    }
    }
    }
    suspend fun testCoroutine() {
    println("Before")

    val user = requestUser()

    println(user)
    println("After")
    }

    现在,你很少需要包装回调函数以使其成为挂起函数,因为很多流行库(RetrofitRoom等)都已经支持挂起函数了。但从另方面来讲,我们已经对那些函数的底层实现有了一些了解。它就和我们刚才写的类似。不一样的是,底层使用的是suspendCancellableCoroutine函数(支持取消)。后面我们会讲到。


    suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { continuation ->
    requestUser {
    continuation.resume(it)
    }
    }
    }

    你可能想知道如果API接口没给我们返回数据而是抛出了异常,比如服务死机或者返回一些错误。这种情况下,我们不能返回数据,相反我们需要在协程挂起的地方抛出异常。这是我们在异常情况下恢复地方。


    Resume with exception(异常恢复)


    我们调用的每个函数可能返回一些值也可能抛异常。就像suspendCoroutine: 当resume调用的时候返回正常值, 当resumeWithException调用的时候,则会在挂起点抛出异常:


    class MyException : Throwable("Just an exception")

    suspend fun testCoroutine() {

    try {
    suspendCoroutine<Unit> { continuation ->
    continuation.resumeWithException(MyException())
    }
    } catch (e: MyException) {
    println("Caught!")
    }
    }

    //Caught

    这种机制是为了处理各种不同的问题。比如,标识网络异常:


    suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { cont ->
    requestUser { resp ->
    if (resp.isSuccessful) {
    cont.resume(resp.data)
    } else {
    val e = ApiException(
    resp.code,
    resp.message
    )
    cont.resumeWithException(e)
    }
    }
    }
    }

    翻译不动了。。。😂, 就差不多到这吧。。


    结尾


    我希望现在您可以从用户的角度清楚的了解挂起(暂停)是如何工作的。Best wishes!


    原文地址:kt.academy/article/cc-…


    作者:老炮儿丶狗二
    链接:https://juejin.cn/post/6999461797140889614
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    ConstraintLayout2.0一篇写不完之极坐标布局与动画

    相对于一般布局方式的笛卡尔坐标系,MotionLayout还拓展了ConstraintLayout中的相对中心布局方式,我们暂且称之为「极坐标布局」方式。 极坐标布局方式在某些场景下,比笛卡尔坐标系的建立更加方便,特别是涉及到一些圆周运动和相对中心点运动的场...
    继续阅读 »

    相对于一般布局方式的笛卡尔坐标系,MotionLayout还拓展了ConstraintLayout中的相对中心布局方式,我们暂且称之为「极坐标布局」方式。


    极坐标布局方式在某些场景下,比笛卡尔坐标系的建立更加方便,特别是涉及到一些圆周运动和相对中心点运动的场景。


    Rotational OnSwipe


    在OnSwipe的基础上,极坐标方式拓展了运动的方向,给dragDirection增加了dragClockwise和dragAnticlockwise参数,用于设置OnSwipe的顺时针滑动和逆时针滑动,这两个属性,在设置rotationCenterId后才会生效。那么借助这个,就可以很方便的实现一些圆形路径的滑动效果和动画。


    通过下面这个例子,我们来看下Rotational OnSwipe的使用方法。


    首先,极坐标的布局还是借助ConstraintLayout,代码如下所示。


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#cfc"
    app:layoutDescription="@xml/motion_01_dial_scene"
    app:motionDebug="SHOW_ALL">

    <TextView
    android:id="@+id/number1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="1"
    android:textSize="24sp"
    app:layout_constraintCircle="@id/dial"
    app:layout_constraintCircleAngle="73"
    app:layout_constraintCircleRadius="112dp"
    app:layout_constraintTag="hop" />

    ......

    <TextView
    android:id="@+id/number0"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="0"
    android:textSize="24sp"
    app:layout_constraintCircle="@id/dial"
    app:layout_constraintCircleAngle="172"
    app:layout_constraintCircleRadius="112dp"
    app:layout_constraintTag="hop" />

    <ImageView
    android:id="@+id/dial"
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:src="@drawable/dial"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.6"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTag="center"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.8" />

    <ImageView
    android:id="@+id/dialhook"
    android:layout_width="70dp"
    android:layout_height="70dp"
    android:src="@drawable/dial_hook"
    app:layout_constraintCircle="@id/dial"
    app:layout_constraintCircleAngle="122"
    app:layout_constraintCircleRadius="112dp"
    app:layout_constraintTag="hop" />

    </androidx.constraintlayout.motion.widget.MotionLayout>


    极坐标布局就是借助layout_constraintCircle、layout_constraintCircleAngle、layout_constraintCircleRadius来确定圆心、角度和半径,从而实现极坐标的布局,接下来,再通过OnSwipe来实现圆形滑动效果。


    <?xml version="1.0" encoding="utf-8"?>
    <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto"
    motion:defaultDuration="2000">

    <ConstraintSet android:id="@+id/start">
    <Constraint android:id="@+id/dial">
    <Transform android:rotation="0" />
    </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
    <Constraint android:id="@+id/dial">
    <Transform android:rotation="300" />
    </Constraint>
    </ConstraintSet>

    <Transition
    motion:autoTransition="animateToStart"
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@+id/start"
    motion:duration="1000"
    motion:motionInterpolator="easeIn">

    <OnSwipe
    motion:dragDirection="dragClockwise"
    motion:dragScale=".9"
    motion:maxAcceleration="10"
    motion:maxVelocity="50"
    motion:onTouchUp="autoCompleteToStart"
    motion:rotationCenterId="@id/dial" />
    <KeyFrameSet>

    </KeyFrameSet>
    </Transition>
    </MotionScene>

    核心就在OnSwipe中,设置rotationCenterId后,再设置滑动的方向为顺时针即可,展示如下所示。


    image-20302


    Relative Animation


    在MotionLayout中,它进一步加强了在动画中对极坐标运动的支持,特别是一些极坐标的相对运动动画,可以通过MotionLayout,以非常简单的方式表现出来。我们举个简单的例子,一个行星环绕的动画,如下所示。


    image-208867


    我们可以发现,这个动画的轨迹是非常复杂的,太阳以自己为中心自传,地球绕着太阳旋转的同时还在自传,月球绕着地球旋转,卫星绕着地球旋转的同时,逐渐远离地球,靠近月球。


    这样一个复杂的极坐标动画效果,虽然借助ConstraintLayout可以很方便的实现定位布局,但是运动时,却无法继续保持极坐标的依赖关系,所以,这里需要使用MotionLayout来维持运动时的极坐标约束关系。


    首先,使用ConstraintLayout来完成起始布局的建立,代码如下所示。


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FF003b60"
    app:layoutDescription="@xml/motion_01_motion_scene"
    app:motionDebug="SHOW_ALL">

    <ImageView
    android:id="@+id/sun"
    android:layout_width="180dp"
    android:layout_height="180dp"
    android:src="@drawable/sun"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="parent" />

    <TextView
    android:id="@+id/rocket"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="?"
    android:textSize="28sp"
    app:layout_constraintCircle="@id/earth"
    app:layout_constraintCircleAngle="0"
    app:layout_constraintCircleRadius="60dp" />

    <androidx.constraintlayout.utils.widget.ImageFilterView
    android:id="@+id/moon"
    android:layout_width="16dp"
    android:layout_height="16dp"
    android:rotation="-240"
    android:src="@drawable/moon"
    app:layout_constraintCircle="@id/earth"
    app:layout_constraintCircleAngle="0"
    app:layout_constraintCircleRadius="180dp" />

    <androidx.constraintlayout.utils.widget.ImageFilterView
    android:id="@+id/earth"
    android:layout_width="160dp"
    android:layout_height="160dp"
    android:src="@drawable/earth"
    app:layout_constraintCircle="@id/sun"
    app:layout_constraintCircleAngle="315"
    app:layout_constraintCircleRadius="200dp" />

    </androidx.constraintlayout.motion.widget.MotionLayout>

    接下来,在Scene文件中,设置相对运动关系,代码如下所示。


    <ConstraintSet android:id="@+id/start">

    <Constraint android:id="@id/earth">
    <Motion motion:animateRelativeTo="@+id/sun" />
    </Constraint>

    <Constraint android:id="@id/moon">
    <Motion motion:animateRelativeTo="@+id/earth" />
    </Constraint>

    <Constraint android:id="@+id/rocket">
    <Motion
    motion:animateRelativeTo="@+id/earth"
    motion:motionPathRotate="45" />
    </Constraint>
    </ConstraintSet>

    借助animateRelativeTo来实现Motion中的相对中心点,使用motionPathRotate来设置旋转的角度。



    Motion标签中的motionPathRotate和Constraint标签中的transitionPathRotate的作用,都是让其相对于Path旋转一定角度。



    MotionLayout中新增的属性非常多,大家可以参考我的这些文章,从各个方面,逐个击破MotionLayout的各个难点。

    收起阅读 »

    Android分区存储常见问题解答

    要在 Google Play 上发布,开发者需要将应用的 目标 API 级别 (targetSdkVersion) 更新到 API 级别 30 (Android 11) 或者更高版本。针对新上架的应用,这个政策自 8 月开始生效;现有应用更新新的版本,这个政策...
    继续阅读 »

    要在 Google Play 上发布,开发者需要将应用的 目标 API 级别 (targetSdkVersion) 更新到 API 级别 30 (Android 11) 或者更高版本。针对新上架的应用,这个政策自 8 月开始生效;现有应用更新新的版本,这个政策的要求将自 11 月开始生效。


    API 30 所带来的一个巨大变更是,应用需要使用分区存储 (Scoped Storage)。


    变更之大,对于大型应用来说堪称恐怖。更糟糕的是,我们在网上看到的有关如何适配分区存储的建议,有一些建议十分令人迷惑,甚至会误导我们。


    为了帮您排忧解难,我们收集了一些有关分区存储的常见问题,同时也为如何适配您的应用提供了一些建议和可能的替代方案。


    Q: android:requestLegacyStorage 会被移除吗?


    A: 部分移除。


    如果您的应用当前已经设置了 android:requestLegacyStorage="true",就应该在 targetSdkVersion 设置为 30 后保持现状。该标记在 Android 11 设备中没有任何效果,但是可以继续让应用在 Android 10 设备上以旧的方式访问存储。


    如果您需要针对 Android 10 设备在 AndroidManifest.xml 中设置 android:requestLegacyStorage="true",那在应用的目标版本改为 Android 11 后应当保留此设置。它仍会在 Android 10 设备上生效。


    Q: android:preserveLegacyStorage 是如何工作的?


    A: 如果您的应用安装在 Android 10 设备上,并设置了 android:requestLegacyStorage="true",那在设备升级至 Android 11 后,此设置会继续保持旧的存储访问方式。


    ?? 如果应用被卸载,或者是第一次在 Android 11 上安装,那么就无法使用旧的存储访问方式。此标记仅适用于进一步帮助设备从传统存储升级到分区存储。


    Q: 如果我的应用没有访问照片、视频或音频文件,是否仍然需要请求 READ_EXTERNAL_STORAGE 权限?


    A: 不需要,从 Android 11 开始,仅在访问其他应用所属的媒体文件时才需要请求 READ_EXTERNAL_STORAGE 权限。如果您的应用仅使用自身创建的非媒体文件 (或自身创建的媒体文件),那么就不再需要请求该权限。


    如需在 Android 11 后停止请求该权限,仅需修改应用 AndroidManifest.xml 文件中的 <uses-permission> 标签,添加 android:maxSdkVersion="29" 即可:


    <uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

    Q: 我想要访问不属于我应用的照片、视频或一段音频,我必须使用系统文件选择器吗?


    A: 不。但如果您想用就可以用,ACTION_OPEN_DOCUMENT 最早可支持至 Android KitKat (API 19),而 ACTION_GET_CONTENT 则支持至 API 1,二者使用的都是系统文件选择器。由于不需要任何权限,这仍然是首选的解决方案。


    如果您不想使用系统文件选择器,您仍然可以请求 READ_EXTERNAL_STORAGE 权限,它会使您的应用可以访问所有的照片、视频以及音频文件,同时也包含访问 File API 的权限!


    如果您需要使用 File API 访问媒体内容,记得设置 android:requestLegacyStorage="true",否则 File API 在 Android 10 中将无法工作。


    Q: 我想保存非媒体文件,但我不想在卸载我的应用时删除它们。我需要使用 SAF 吗?


    A: 也许需要。


    如果这些文件允许在应用外打开而无需通过您的应用,那么系统文件选择器是较好的选择。您可以使用 ACTION_CREATE_DOCUMENT 创建文件。当然也可以使用 ACTION_OPEN_DOCUMENT 来打开一个现有文件。


    如果应用曾经创建了一个目录用于存储所有这些文件,那最好的选择就是使用系统文件选择器和 ACTION_OPEN_DOCUMENT_TREE,以便用户可以选择要使用的特定文件夹。


    如果这些文件只对您的应用有意义,可以考虑在应用 AndroidManifest.xml 文件的 <application> 标签中设置 android:hasFragileUserData="true"。这将使用户可以保留这些数据,即使在卸载应用时亦是如此。


    上图为拥有 "脆弱用户数据" 应用的卸载对话框。对话框中包含了一个复选框,用于指示系统是否应该保留应用数据。


    △ 上图为拥有 "脆弱用户数据" 应用的卸载对话框。对话框中包含了一个复选框,用于指示系统是否应该保留应用数据。


    设置了该标记后,存储文件的最佳位置将取决于其内容。包含敏感或私人信息的文件应当存储在 Context#getFilesDir() 所返回的目录中;而不敏感的数据则应存储于 Context#getExternalFilesDir() 所返回的目录中。


    Q: 我可以将非媒体文件放置于其他文件夹中 (例如 Downloads 文件夹),而无需任何权限。这是一个 Bug 吗?


    A: 不是。应用可能会向这类集合提供文件,而且最好的方式是对非媒体文件同时使用 Downloads 和 Documents 集合。不过请记得,默认情况下只有创建该文件的应用才可以访问它们。其他应用需要通过系统文件选择器获得访问权限或者拥有对外部存储的广泛访问权限 (即: MANAGE_EXTERNAL_STORAGE 权限) 才行。


    ?? 对 MANAGE_EXTERNAL_STORAGE 权限的访问受到 Play 政策 监管。


    Q: 如果我需要保存一个文档,是否需要使用 SAF?


    A: 不用。应用可以向 Documents 与 Downloads 集合提供非媒体文件,而无需任何特殊权限。只要没被卸载,那么向这些集合提供文档的应用拥有这些文档的完全访问权限。


    ? 如果您的应用为了上面提到的方式保存文档而请求 READ_EXTERNAL_STORAGE 权限的话,在 Android 11 及更高版本中将不必再请求该权限。您可以参考下面的示例修改对该权限的请求 (设定 maxSdkVersion 为 API 版本 29):


    <uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

    如要访问其他应用添加的文档,或者在您的应用卸载重装后访问其卸载前添加的文档,就需要通过 ACTION_OPEN_DOCUMENT Intent 来使用系统文件选择器。


    Q: 我想要与其他应用共享文件,是否需要使用 SAF?


    A: 不需要。如下是一些与其他应用共享文件的方式:



    • 直接分享: 使用 Intent.ACTION_SEND 可以让您的用户通过各种格式与设备上的其他应用共享数据。如果您使用这种方式,使用 AndroidX 的 FileProvider 来将 file:// Uri 自动转换为 content:// Uri 可能会对您有所帮助。

    • 创建您自己的 DocumentProvider: 这可以让您的应用继续处理应用的私有目录 ( Context#getFilesDirs() 或 Context#getExternalFilesDirs()) 中内容的同时,仍可以向使用系统文件选择器的其他应用提供访问权限。(请注意,可以在卸载应用后继续保存这些文件——参阅上文中的 android:hasFragileUserData="true" 设置来了解其使用方式。)


    最后的思考


    Scoped Storage 是一项旨在改善用户隐私保护的重大变更。不过仍然有很多方法可以处理不依赖使用存储访问框架 (Storage Access Framework) 的内容。


    如果要存储的数据仅适用于您的应用,那么我们强烈建议使用 应用特定目录


    如果数据是媒体文件,例如照片、视频或者音频,那么可以 使用 MediaStore。注意,从 Android 10 开始,提供内容 不再需要请求权限


    也别忘了可以通过 ACTION_SEND 来与 其他应用共享数据 (或允许它们 与您的应用共享数据)!


    收起阅读 »

    一文读懂 Android 主流屏幕适配方案

    公众号:字节数组,希望对你有所帮助 ?? 关于 Android 的屏幕适配现在已经有很多成熟的方案了,已经不是一个热门话题了。印象中 2018 年是讨论适配方案最火热的一段时间,那时候字节跳动技术团队发文介绍了其适配方案,之后就带动起了很多位大佬陆续发表...
    继续阅读 »

    公众号:字节数组,希望对你有所帮助 ??



    关于 Android 的屏幕适配现在已经有很多成熟的方案了,已经不是一个热门话题了。印象中 2018 年是讨论适配方案最火热的一段时间,那时候字节跳动技术团队发文介绍了其适配方案,之后就带动起了很多位大佬陆续发表文章进行讨论。当时我刚工作不久,在面试时也有被问到过关于屏幕适配的问题,因为对于一些概念认识不清导致回答得并不好 ?? 所以本篇文章就想要从头到尾讲清楚关于屏幕适配的主要知识点,希望对你有所帮助,有错误也希望读者能够指出来 ??


    一、ppi & dpi


    关于屏幕适配有两个绕不开的概念:ppi 和 dpi,两者在含义上很类似,很容易混淆,但其实是属于不同领域上的概念


    ppi


    ppi(Pixels Per Inch)即像素密度,指每英寸包含的物理像素的数量。ppi 是设备在物理上的属性值,取决于屏幕自身,计算公式如下所示。被除数和除数都属于客观不可改变的值,所以 ppi 也是无法修改的,是硬件上一个客观存在无法改变的值



    dpi


    dpi(Dots Per Inch)原先用于在印刷行业中描述每英寸包含有多少个点,在 Android 开发中则用来描述屏幕像素密度。屏幕像素密度决定了在软件概念上单位距离对应的像素总数,是手机在出厂时就会被写入系统配置文件中的一个属性值,一般情况下用户是无法修改该值的,但在开发者模式中有修改该值的入口,是软件上一个可以修改的值


    我们知道,在不同手机屏幕上 1 dp 所对应的 px 值可能是会有很大差异的。例如,在小屏幕手机上 1 dp 可能对应 1 px,在大屏幕手机上对应的可能是 3 px,这也是我们实现屏幕适配的基础原理。决定了在特定一台手机上 1 dp 对应多少 px 的正是该设备的 dpi 值,这可以通过 DisplayMetrics 来获取


    val displayMetrics = applicationContext.resources.displayMetrics
    Log.e("TAG", "densityDpi: " + displayMetrics.densityDpi)
    Log.e("TAG", "density: " + displayMetrics.density)
    Log.e("TAG", "widthPixels: " + displayMetrics.widthPixels)
    Log.e("TAG", "heightPixels: " + displayMetrics.heightPixels)

    TAG: densityDpi: 480
    TAG: density: 3.0
    TAG: widthPixels: 1080
    TAG: heightPixels: 2259

    从中就可以提取出几点信息:



    1. 屏幕像素密度为 480 dpi

    2. density 等于 3,说明在该设备上 1 dp 等于 3 px

    3. 屏幕宽高大小为 2259 x 1080 px,即 753 x 360 dp


    Android 系统定义的屏幕像素密度基准值是 160 dpi,该基准值下 1 dp 就等于 1 px,依此类推 480 dpi 下 1 dp 就等于 3 px,计算公式:


    px = dp * (dpi / 160)

    不同屏幕像素密度的设备就对应了不同的配置限定符。例如,在 320 到 480 dpi 之间的设备就对应 xxhdpi,该类型设备在取图片时就会优先从 drawable-xxhdpi 文件夹下取



    二、为什么要适配


    不管我们在布局文件中使用的是什么单位,最终系统在使用时都需要将其转换为 px,由于不同手机的屏幕像素尺寸会相差很大,我们自然不能在布局文件中直接使用 px 进行硬编码。因此 Google 官方也推荐开发者尽量使用 dp 作为单位值,因为系统会根据屏幕的实际情况自动完成 dp 与 px 之间的对应换算


    举个例子。假设设计师给出来的设计稿是按照 1080 x 1920 px,420 dpi 的标准来进行设计的,那么设计稿的宽高即 411 x 731 dp,那对于一个希望占据屏幕一半宽度的 ImageView 来说,在设计稿中的宽即 205.5 dp


    那么,对于一台 1440 x 2880 px,560 dpi 的真机来说,其宽高即 411 x 822 dp,此时我们在布局文件中就可以直接使用设计稿中给出来的宽度值,使得 ImageView 在这台真机上也占据了屏幕一半宽度。虽然设计稿和真机的屏幕像素并不相同,但由于屏幕像素密度的存在,使得两者的 dp 宽度是一样的,从而令开发者可以只使用同一套 dp 尺寸值就完成设计要求了


    既然有了 dp,那我们为什么还需要进行屏幕适配呢?当然也是因为 dp 只适用于大部分正常情况了。以上情况之所以能够完美适配,那也是因为举的例子刚好也是完美的:1440 / 1080 = 560 / 420 = 1.3333,设计稿和真机的 px 宽度和 dp 宽度刚好具有相同比例,此时使用 dp 才能刚好适用


    再来看一个不怎么完美的例子。以两台真机为例:



    • 华为 nova5:1080 x 2259 px,480 dpi,屏幕宽度为 1080 / (480 / 160) = 360 dp

    • 三星 Galaxy S10:1080 x 2137 px,420 dpi,屏幕宽度为 1080 / (420 / 160) = 411 dp


    可以看到,在像素宽度相同的情况下,不同手机的像素密度是有可能不一样的。手机厂家有可能是根据屏幕像素和屏幕尺寸来共同决定该值的大小,但不管怎样,这就造成了应用的实际效果与设计稿之间无法对应的情况:对于一个 180 dp 宽度的 View 来说,在华为 nova5 上能占据一半的屏幕宽度,但在三星 Galaxy S10 上却只有 180 / 411 = 0.43,这就造成了一定偏差


    以上情况就是直接使用 dp 值无法解决的问题,使用 dp 只能适配大部分宽高比例比较常规的机型,对于特殊机型就无能为力了……


    屏幕适配就是要来解决上述问题。对于屏幕适配,开发者希望实现的效果主要有两个:



    • 在声明宽高值时,能够直接套用设计稿上给出来的尺寸值,这个尺寸值映射到项目中可能是对应一个具体的值,也可能是对应多套 dimens 文件中的值,但不管是哪一种,在开发阶段都希望能够直接套用而无需再来进行手动计算。这关乎进行屏幕适配的效率

    • 适配后的界面最终在不同屏幕上的空间比例都能保持一致。这关乎进行屏幕适配的最终成效


    下面就来介绍三种当前比较主流或曾经是主流的的适配方案 ~~


    三、今日头条方案


    字节跳动技术团队曾经发布过一篇文章介绍了其适配方案:一种极低成本的Android屏幕适配方式


    其适配思路基于以下几条换算公式:



    • px = density * dp

    • density = dpi / 160

    • px = dp * (dpi / 160)


    在布局文件中声明的 dp 值,最终都需要通过 TypedValue 的 applyDimension 方法来转换为 px,转换公式即:density * dp


        public static float applyDimension(int unit, float value, DisplayMetrics metrics) {
    switch (unit) {
    case COMPLEX_UNIT_PX:
    return value;
    case COMPLEX_UNIT_DIP:
    return value * metrics.density;
    case COMPLEX_UNIT_SP:
    return value * metrics.scaledDensity;
    case COMPLEX_UNIT_PT:
    return value * metrics.xdpi * (1.0f/72);
    case COMPLEX_UNIT_IN:
    return value * metrics.xdpi;
    case COMPLEX_UNIT_MM:
    return value * metrics.xdpi * (1.0f/25.4f);
    }
    return 0;
    }

    那么,如果我们能够动态修改 density 值的大小,要求修改后计算出的屏幕宽度就等于设计稿的宽度,不就可以在布局文件中直接使用设计稿给出的各个 dp 宽高值,且使得 View 在不同手机屏幕上都能占据同样的比例吗?


    举个例子,假设设计师给出来的设计稿是按照 **1080 x 1920 px,density 2.625,420 dpi ** 的标准来进行设计的,设计稿的宽高即 411 x 731 dp。那么对于一个宽度为 100 dp 的 View,占据设计稿的宽度比例是:100 * 2.625 / 1080 = 0.2430


    用以下两台真机的数据为例,在适配前:



    • 华为 nova5:1080 x 2259 px,480 dpi。正常情况下其 density 为 3,View 占据的屏幕宽度比例是:100 x 3 / 1080 = 0.2777

    • Pixel 2 XL:1440 x 2800 px,560 dpi。正常情况下其 density 为 3.5,View 占据的屏幕宽度比例是:100 x 3.5 / 1440 = 0.2430


    采用字节跳动技术团队的方案动态改变 density 进行适配,适配后的 density = 设备真实宽度(单位 px) / 设计稿的宽度(单位 dp):



    • 华为 nova5:适配后 density 变成 1080 / 411 = 2.6277,View 占据的屏幕宽度比例是:100 x 2.6277 / 1080 = 0.2433

    • Pixel 2 XL:适配后 density 变成 1440 / 411 = 3.5036,View 占据的屏幕宽度比例是:100 x 3.5036 / 1440 = 0.2433


    可以看出来,虽然由于除法运算会导致一点点精度丢失,但完全可以忽略不计,只要我们能动态改变手机的 density,最终 View 在宽度上就都能保持和设计稿完全相同的比例了


    实际上 density 只是 DisplayMetrics 类中的一个 public 变量,不涉及任何私有 API,修改后理论上也不会影响到应用的稳定性。因此,只要我们在 Activity 的 onCreate 方法中完成对 density 和 densityDpi 的修改,我们就可以在布局文件中直接使用设计稿给出的 dp 值,不用准备多套 dimens 就能完成适配,十分简洁


        fun setCustomDensity(activity: Activity, application: Application, designWidthDp: Int) {
    val appDisplayMetrics = application.resources.displayMetrics
    val targetDensity = 1.0f * appDisplayMetrics.widthPixels / designWidthDp
    val targetDensityDpi = (targetDensity * 160).toInt()
    appDisplayMetrics.density = targetDensity
    appDisplayMetrics.densityDpi = targetDensityDpi
    val activityDisplayMetrics = activity.resources.displayMetrics
    activityDisplayMetrics.density = targetDensity
    activityDisplayMetrics.densityDpi = targetDensityDpi
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    setCustomDensity(this, application, 420)
    super.onCreate(savedInstanceState)
    }


    字节跳动技术团队的文章只给出了示例代码,并没有给出最终落地可用的代码,但在 GitHub 上有一个挺出名的落地实践库,读者值得一看:AndroidAutoSize



    四、宽高限定符


    宽高限定符是系统原生支持的一种适配方案,通过穷举市面上所有 Android 手机的屏幕像素尺寸来实现适配。实现思路很简单,就是通过比例换算来为不同分辨率的屏幕分别生成一套 dimens 文件


    首先,以设计稿的尺寸作为基准分辨率,假设设计稿是 1920 x 1080 px,那么就可以先生成默认的 dimens 文件,生成规则:



    • 将屏幕宽度均分为 1080 份,每份 1 px,声明 1080 个 key 值,值从 1 px 开始递增,每次递增 1 px

    • 将屏幕高度均分为 1920 份,每份 1 px,声明 1920 个 key 值,值从 1 px 开始递增,每次递增 1 px


    最终 dimens 文件就像以下这样:


    <resources>
    <dimen name="x1">1px</dimen>
    <dimen name="x2">2px</dimen>
    ···
    <dimen name="x1080">1080px</dimen>

    <dimen name="y1">1px</dimen>
    <dimen name="y2">2px</dimen>
    ···
    <dimen name="y1920">1920px</dimen>
    </resources>

    类似地,再来为屏幕尺寸为 1440 x 720 px 的手机生成专属的 dimens 文件,生成规则:



    • 将屏幕宽度均分为 1080 份,每份 720 / 1080 = 0.666 px,声明 1080 个 key 值,值从 0.666 px 开始递增,每次递增 0.666 px

    • 将屏幕高度均分为 1920 份,每份 1440 / 1920 = 0.75 px,声明 1920 个 key 值,值从 0.75 px 开始递增,每次递增 0.75 px


    最终 dimens 文件就像以下这样:


    <resources>
    <dimen name="x1">0.666px</dimen>
    <dimen name="x2">1.332px</dimen>
    ···
    <dimen name="x1080">720px</dimen>

    <dimen name="y1">0.75px</dimen>
    <dimen name="y2">1.5px</dimen>
    ···
    <dimen name="y1920">1440px</dimen>
    </resources>

    最终,为市面上主流的屏幕尺寸均按照如上规则生成一套专属的 dimens 文件,每套文件均放到以像素尺寸进行命名的 value 文件夹下,就像以下这样:


    values
    values-1440x720
    values-1920x1080
    values-2400x1080
    values-2408x1080
    values-2560x1440

    之后,我们就可以直接套用设计稿中的像素尺寸进行开发了,设计稿写的是 100 x 200 px,那么我们在布局文件中就可以直接引用 x100 和 y200。当应用运行在不同分辨率的手机中时,应用会自动去引用相同分辨率的 dimens 文件,此时引用到的实际 px 值具有和设计稿相同的比例大小,这样就实现了适配需求了


    需要注意,宽高限定符方案有一个致命缺陷:需要精准命中分辨率才能实现适配。比如 1920 x 1080 px 的手机就一定要引用到 values-1920x1080文件夹内的 dimens 文件,否则就只能去引用默认的 values 文件夹,此时引用到的尺寸值就有可能和实际需求有很大出入,从而导致界面变形。而对于市面上层出不穷的各种分辨率,开发者想穷举完其实很麻烦,所以说,宽高限定符方案的容错率很低


    五、smallestWidth


    smallestWidth 也是系统原生支持的一种适配方案。smallestWidth 即最小宽度,指的是最短的那一个边长,而不考虑屏幕的方向,适配原理和宽高限定符方案一样,本质上都是通过比例换算来为不同尺寸的屏幕分别准备一套 dimens 文件,应用在运行时再去引用相匹配的 dimens 文件,以此来实现屏幕适配


    首先,我们要以设计稿的尺寸作为基准分辨率,假设设计师给出来的设计稿是按照 **1080 x 1920 px **的标准来进行设计的,那么基准分辨率就是设计稿的宽度 1080 px


    先为宽度为 360 dp 的设备生成 dimens 文件,生成规则:



    • 将 360 dp 均分为 1080 份,每份 360 / 1080 dp,声明 1080 个 key 值,值从 360 / 1080 dp 开始递增,每次递增 360 / 1080 dp


    最终 dimens 文件就像以下这样:


    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <dimen name="DIMEN_1PX">0.33dp</dimen>
    <dimen name="DIMEN_2PX">0.67dp</dimen>
    ···
    <dimen name="DIMEN_1078PX">359.33dp</dimen>
    <dimen name="DIMEN_1079PX">359.67dp</dimen>
    <dimen name="DIMEN_1080PX">360.00dp</dimen>
    </resources>

    类似地,我们再按照上述规则为宽度为 380 dp 的设备生成 dimens 文件:


    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <dimen name="DIMEN_1PX">0.35dp</dimen>
    <dimen name="DIMEN_2PX">0.70dp</dimen>
    ···
    <dimen name="DIMEN_1078PX">379.30dp</dimen>
    <dimen name="DIMEN_1079PX">379.65dp</dimen>
    <dimen name="DIMEN_1080PX">380.00dp</dimen>
    </resources>

    最终,为市面上主流的屏幕宽度均按照如上规则生成一套专属的 dimens 文件,每套文件均放到以宽度进行命名的 value 文件夹内,就像以下这样:


    values
    values-sw360dp
    values-sw380dp
    values-sw400dp
    values-sw420dp

    这样,我们就可以直接在布局文件中套用设计稿的 px 值了,应用在运行时就会自动去匹配最符合当前屏幕宽度的资源文件。例如,如果我们引用了 DIMEN_1080PX,那么不管是在宽度为 360 dp 还是 380 dp 的设备中,该引用对应的 dp 值都是刚好占满屏幕宽度,这样就实现了适配需求了


    smallestWidth 方案和宽高限定符方案最大的差别就在于容错率,smallestWidth 方案具有很高的容错率,即使应用中没有找到符合当前屏幕宽度的 dimens 文件,应用也会向下寻找并采用最接近当前屏幕宽度的 dimens 文件,只有都找不到时才会去引用默认的 dimens 文件。只要我们准备的 dimens 足够多,且每套 dimens 文件以 5 ~ 10 dp 作为步长递增,那么就能够很好地满足市面上的绝大部分手机了。此外,我们不仅可以使用设计稿的 px 宽度作为基准分辨率,也可以改为使用 dp 宽度,计算规则还是保持一致


    六、总结


    以上介绍的三种方案各有特点,这里来做个总结



    • 今日头条方案。优点:可以直接使用设计稿中的 dp 值,无需生成多套 dimens 文件进行映射,因此不会增大 apk 体积。此外,此方案的 UI 还原度在三种方案中应该是最高的了,其它两种方案都需要精准命中屏幕尺寸后才能达到此方案的还原度。缺点:由于此方案会影响到应用全局,因此如果我们引入了一些第三方库的话,三方库中的界面也会随之被影响到,可能会造成效果变形,此时就需要进行额外处理了

    • 宽高限定符方案。容错率太低,且需要准备很多套 dimens 文件,在 Android 刚兴起,屏幕类型还比较少的时候比较吃香,目前应该已经很少有项目采用此方案了,读者可以直接忽略

    • smallestWidth 方案。优点:容错率高,在 320 ~ 460 dp 之间每 10 dp 就提供一套 dimens 文件就足够使用了,想要囊括更多设备的话也可以再缩短步长,基本不用担心最终效果会与设计稿相差太多,且此方案不会影响到三方库。缺点:需要生成多套 dimens 文件,增大了 apk 体积


    需要强调下,以上三种方案其实都存在一个问题:我们只能实现对单个方向的适配,无法同时兼顾宽高。之所以只能单个方向,是因为当前手机屏幕的宽高比并不是按照一个固定的比例进行递增的,4 : 3、16 : 9、甚至其它宽高比都有,这种背景下我们要达到百分百还原设计稿是不现实的,我们只能选择一个维度来进行适配。幸运的是大部分情况下我们也只需要根据屏幕宽度来进行适配,以上方案已经能够满足我们绝大多数时候的开发需求了。对于少部分需要根据高度进行适配的页面,今日头条方案可以很灵活的进行切换,smallestWidth 方案就比较麻烦了,此时可以通过 ConstraintLayout 来精准按比例控制控件的宽高大小或者是位置,同样也能达到适配要求 ~


    此外,我看到网络上很多开发者都在说 dpi 的存在就是为了让大屏幕手机能够显示更多内容,屏幕适配导致 dpi 失去了其原有的意义,但我其实并不理解这和屏幕适配有什么关系。现在的现实背景就是存在某些屏幕像素宽度相同的手机,其 dpi 却不一样,如果单纯直接使用 dp 而不进行额外适配的话,那在这类机型下控件就会相比设计稿多出一些空白或者是超出屏幕范围,这是开发者不得不解决的问题。如果说显示更多内容指的是让控件在大屏幕手机上能够占据更多的物理空间,那么前提也是要让各个控件的大小和位置都符合设计稿的要求,屏幕适配要做到的就是这一点,同等比例下控件在大屏幕手机上自然就会有更多物理空间。而如果说显示更多内容指的是当在大屏幕手机上有剩余空间时就相比小屏幕多显示其它控件,那么我觉得不仅开发要疯,设计师都要疯……


    最后,这里再提供一份用于生成 dimens 文件的代码,基于 smallestWidth 方案,代码总的不到一百行,实现思路在前文讲的很清楚了。仅需要填入设计稿的宽高像素大小就可以,默认基于 1080 x 1920 px 的设计稿,生成范围从 320 到 460 dp 之间,步长 10 dp,读者可以按需调整



    有需要的同学自取:SmallestWidthGenerator

    收起阅读 »

    基于环信MQTT消息云,Android端快速实现消息收发

    本文介绍Android端如何连接环信MQTT消息云快速实现消息的自收自发。一、前提条件1、部署Android开发环境下载安装Android studio,配置好开发环境2、导入项目依赖在项目根目录build.gradle文件里配置repositories { ...
    继续阅读 »

    本文介绍Android端如何连接环信MQTT消息云快速实现消息的自收自发。

    一、前提条件

    1、部署Android开发环境

    下载安装Android studio,配置好开发环境

    2、导入项目依赖

    在项目根目录build.gradle文件里配置
    repositories {
    maven {
    url "https://repo.eclipse.org/content/repositories/paho-snapshots/"
    }
    }
    另需要在app的build.gradle里添加依赖
    dependencies {
    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
    implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
    }

    二、实现流程

    1、获取鉴权

    首先需要登录环信云console控制台,获取到AppID、连接地址、连接端口,然后代码实现获取token

    客户端获取token代码实例如下:

    //使用okhttp实现的获取token
    JSONObject reqBody = new JSONObject();
    reqBody.put("grant_type", "password");
    reqBody.put("username", "hxtest");
    reqBody.put("password", "1");
    OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
    MediaType mediaType = MediaType.parse("application/json");
    RequestBody requestBody = RequestBody.create(mediaType, reqBody.toString());
    Request request = new Request.Builder()
    .url("https://{token域名}/{org_name}/{app_name}/token")
    .post(requestBody)
    .build();
    Call call = okHttpClient.newCall(request);
    call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    Log.e(TAG, "okhttp_onFailure:" + e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
    String responseBody = response.body().string();
    if (response.code() == 200) {
    try {
    JSONObject result = new JSONObject(responseBody);
    String token = result.getString("access_token");
    } catch (JSONException e) {
    e.printStackTrace();
    }
    }
    }
    });



    2、初始化
    在项目中创建MQTT客户端,客户端初始配置包括创建clientID,topic名称,QoS质量,连接地址等信息。

    //连接时使用的clientId, 必须唯一
    String clientId = String.format("%s@%s", userName, appId);
    MqttAndroidClient mMqttClient = new MqttAndroidClient(context, String.format("tcp://%s:%s", mqttUri, mqttPort), clientId);


    3、连接服务器

    调用connect()函数连接至环信MQTT消息云

    //连接参数
    MqttConnectOptions options;
    options = new MqttConnectOptions();
    //设置自动重连
    options.setAutomaticReconnect(true);
    // 缓存
    options.setCleanSession(true);
    // 设置超时时间,单位:秒
    options.setConnectionTimeout(15);
    // 心跳包发送间隔,单位:秒
    options.setKeepAliveInterval(15);
    // 用户名
    options.setUserName(userName);
    // 密码
    options.setPassword(token.toCharArray());
    options.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1);
    //进行连接
    mMqttClient.connect(options, null, new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {

    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {

    }
    });


    4、订阅

    【订阅主题】当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。

    try {
    //连接成功后订阅主题
    mMqttClient.subscribe(topic, qos, null, new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {

    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {

    }
    });
    } catch (MqttException e) {
    e.printStackTrace();
    }

    【取消订阅】

    try {
    mMqttClient.unsubscribe(topic);
    } catch (MqttException e) {
    e.printStackTrace();
    }


    5、收发消息

    【发送消息】配置发送消息回调方法,向环信MQTT消息云中指定topic发送消息。

    MqttMessage msg=new MqttMessage();
    msg.setPayload(content.getBytes());//设置消息内容
    msg.setQos(qos);//设置消息发送质量,可为0,1,2.
    //设置消息的topic,并发送。
    mMqttClient.publish(topic, msg, null, new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {
    Log.d(TAG, "onSuccess: 发送成功");
    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
    Log.d(TAG, "onFailure: 发送失败="+ exception.getMessage());
    }
    });

    【接收消息】配置接收消息回调方法,从环信MQTT消息云接收订阅消息。

    // 设置MQTT监听
    mMqttClient.setCallback(new MqttCallback() {
    @Override
    public void connectionLost(Throwable cause) {
    Log.d(TAG, "connectionLost: 连接断开");
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
    Log.d(TAG, "收到消息:"+message.toString());
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {

    }
    });

    三、更多信息

    github地址: https://github.com/wangxinjeff/mqttdemo-android




    收起阅读 »

    (算法入门)人人都能看懂的时间复杂度和空间复杂度

    你是怎么理解算法的呢? 简单说就是,同一个功能 别人写的代码跑起来占内存 100M,耗时 100 毫秒 你写的代码跑起来占内存 500M,耗时 1000 毫秒,甚至更多 所以 衡量代码好坏有两个非常重要的标准就是:运行时间和占用空间,就是我们后面要说到的...
    继续阅读 »

    你是怎么理解算法的呢?


    简单说就是,同一个功能



    • 别人写的代码跑起来占内存 100M,耗时 100 毫秒

    • 你写的代码跑起来占内存 500M,耗时 1000 毫秒,甚至更多


    所以



    1. 衡量代码好坏有两个非常重要的标准就是:运行时间占用空间,就是我们后面要说到的时间复杂度空间复杂度也是学好算法的重要基石

    2. 这也是会算法和不会算法的攻城狮的区别、更是薪资的区别,因为待遇好的大厂面试基本都有算法


    可能有人会问:别人是怎么做到的?代码没开发完 运行起来之前怎么知道占多少内存和运行时间呢?


    确切的占内用存或运行时间确实算不出来,而且同一段代码在不同性能的机器上执行的时间也不一样,可是代码的基本执行次数,我们是可以算得出来的,这就要说到时间复杂度了


    什么是时间复杂度


    看个栗子


    function foo1(){
    console.log("我吃了一颗糖")
    console.log("我又吃了一颗糖")
    return "再吃一颗糖"
    }

    调用这个函数,里面总执行次数就是3次,这个没毛病,都不用算


    那么下面这个栗子呢


    function foo2(n){
    for( let i = 0; i < n; i++){
    console.log("我吃了一颗糖")
    }
    return "一颗糖"
    }

    那这个函数里面总执行次数呢?根据我们传进去的值不一样,执行次数也就不一样,但是大概次数我们总能知道


    let = 0               :执行 1 次
    i < n : 执行 n+1 次
    i++ : 执行 n+1 次
    console.log("执行了") : 执行 n 次
    return 1 : 执行 1 次

    这个函数的总执行次数就是 3n + 4 次,对吧


    可是我们开发不可能都这样去数,所以根据代码执行时间的推导过程就有一个规律,也就是所有代码执行时间 T(n)和代码的执行次数 f(n) ,这个是成正比的,而这个规律有一个公式



    T(n) = O( f(n) )



    n 是输入数据的大小或者输入数据的数量  
    T(n) 表示一段代码的总执行时间
    f(n) 表示一段代码的总执行次数
    O 表示代码的执行时间 T(n) 和 执行次数f(n) 成正比

    完整的公式看着就很麻烦,别着急,这个公式只要了解一下就可以了,为的就是让你知道我们表示算法复杂度的 O() 是怎么来的,我们平时表示算法复杂度主要就是用 O(),读作大欧表示法,是字母O不是零


    只用一个 O() 表示,这样看起来立马就容易理解多了


    回到刚才的两个例子,就是上面的两个函数



    • 第一个函数执行了3次,用复杂度表示就是 O(3)

    • 第二个函数执行了3n + 4次,复杂度就是 O(3n+4)


    这样有没有觉得还是很麻烦,因为如果函数逻辑一样的,只是执行次数差个几次,像O(3) 和 O(4),有什么差别?还要写成两种就有点多此一举了,所以复杂度里有统一的简化的表示法,这个执行时间简化的估算值就是我们最终的时间复杂度


    简化的过程如下



    • 如果只是常数直接估算为1,O(3) 的时间复杂度就是 O(1),不是说只执行了1次,而是对常量级时间复杂度的一种表示法。一般情况下,只要算法里没有循环和递归,就算有上万行代码,时间复杂度也是O(1)

    • O(3n+4) 里常数4对于总执行次数的几乎没有影响,直接忽略不计,系数 3 影响也不大,因为3n和n都是一个量级的,所以作为系数的常数3也估算为1或者可以理解为去掉系数,所以 O(3n+4) 的时间复杂度为 O(n)

    • 如果是多项式,只需要保留n的最高次项O( 666n³ + 666n² + n ),这个复杂度里面的最高次项是n的3次方。因为随着n的增大,后面的项的增长远远不及n的最高次项大,所以低于这个次项的直接忽略不计,常数也忽略不计,简化后的时间复杂度为 O(n³)


    这里如果没有理解的话,暂停理解一下


    接下来结合栗子,看一下常见的时间复杂度


    常用时间复杂度


    O(1)


    上面说了,一般情况下,只要算法里没有循环和递归,就算有上万行代码,时间复杂度也是 O(1),因为它的执行次数不会随着任何一个变量的增大而变长,比如下面这样


    function foo(){
    let n = 1
    let b = n * 100
    if(b === 100){
    console.log("开始吃糖")
    }
    console.log("我吃了1颗糖")
    console.log("我吃了2颗糖")
    ......
    console.log("我吃了10000颗糖")
    }

    O(n)


    上面也介绍了 O(n),总的来说 只有一层循环或者递归等,时间复杂度就是 O(n),比如下面这样


    function foo1(n){
    for( let i = 0; i < n; i++){
    console.log("我吃了一颗糖")
    }
    }
    function foo2(n){
    while( --n > 0){
    console.log("我吃了一颗糖")
    }
    }
    function foo3(n){
    console.log("我吃了一颗糖")
    --n > 0 && foo3(n)
    }

    O(n²)


    比如嵌套循环,如下面这样的,里层循环执行 n 次,外层循环也执行 n 次,总执行次数就是 n x n,时间复杂度就是 n 的平方,也就是 O(n²)。假设 n 是 10,那么里面的就会打印 10 x 10 = 100 次


    function foo1(n){
    for( let i = 0; i < n; i++){
    for( let j = 0; j < n; j++){
    console.log("我吃了一颗糖")
    }
    }
    }

    还有这样的,总执行次数为 n + n²,上面说了,如果是多项式,取最高次项,所以这个时间复杂度也是 O(n²)


    function foo2(n){
    for( let k = 0; k < n; k++){
    console.log("我吃了一颗糖")
    }
    for( let i = 0; i < n; i++){
    for( let j = 0; j < n; j++){
    console.log("我吃了一颗糖")
    }
    }
    }

    //或者下面这样,以运行时间最长的,作为时间复杂度的依据,所以下面的时间复杂度就是 O(n²)
    function foo3(n){
    if( n > 100){
    for( let k = 0; k < n; k++){
    console.log("我吃了一颗糖")
    }
    }else{
    for( let i = 0; i < n; i++){
    for( let j = 0; j < n; j++){
    console.log("我吃了一颗糖")
    }
    }
    }
    }

    O(logn)


    举个栗子,这里有一包糖


    asdf.jpeg


    这包糖里有16颗,沐华每天吃这一包糖的一半,请问多少天吃完?


    意思就是16不断除以2,除几次之后等于1?用代码表示


    function foo1(n){
    let day = 0
    while(n > 1){
    n = n/2
    day++
    }
    return day
    }
    console.log( foo1(16) ) // 4

    循环次数的影响主要来源于 n/2 ,这个时间复杂度就是 O(logn) ,这个复杂度是怎么来的呢,别着急,继续看


    再比如下面这样


    function foo2(n){
    for(let i = 0; i < n; i *= 2){
    console.log("一天")
    }
    }
    foo2( 16 )

    里面的打印执行了 4 次,循环次数主要影响来源于 i *= 2 ,这个时间复杂度也是 O(logn)


    这个 O(logn) 是怎么来的,这里补充一个小学三年级数学的知识点,对数,我们看一张图


    未标题-1.jpg


    没有理解的话再看一下,理解一下规律



    • 真数:就是真数,这道题里就是16

    • 底数:就是值变化的规律,比如每次循环都是i*=2,这个乘以2就是规律。比如1,2,3,4,5...这样的值的话,底就是1,每个数变化的规律是+1嘛

    • 对数:在这道题里可以理解成x2乘了多少次,这个次数


    仔细观察规律就会发现这道题里底数是 2,而我们要求的天数就是这个对数4,在对数里有一个表达公式



    ab = n  读作以a为底,b的对数=n,在这道题里我们知道a和n的值,也就是  2b = 16 然后求 b



    把这个公式转换一下的写法如下



    logan = b    在这道题里就是   log216 = ?  答案就是 4



    公式是固定的,这个16不是固定的,是我们传进去的 n,所以可以理解为这道题就是求 log2n = ?


    用时间复杂度表示就是 O(log2n),由于时间复杂度需要去掉常数和系数,而log的底数跟系数是一样的,所以也需要去掉,所以最后这个正确的时间复杂度就是 O(logn)


    emmmmm.....


    没有理解的话,可以暂停理解一下


    其他还有一些时间复杂度,我由快到慢排列了一下,如下表顺序



    这些时间复杂度有什么区别呢,看张图


    未标题-3.jpg


    随着数据量或者 n 的增大,时间复杂度也随之增加,也就是执行时间的增加,会越来越慢,越来越卡


    总的来说时间复杂度就是执行时间增长的趋势,那么空间复杂度就是存储空间增长的趋势


    什么是空间复杂度


    空间复杂度就是算法需要多少内存,占用了多少空间


    常用的空间复杂度有 O(1)O(n)O(n²)


    O(1)


    只要不会因为算法里的执行,导致额外的空间增长,就算是一万行,空间复杂度也是 O(1),比如下面这样,时间复杂度也是 O(1)


    function foo(){
    let n = 1
    let b = n * 100
    if(b === 100){
    console.log("开始吃糖")
    }
    console.log("我吃了1颗糖")
    console.log("我吃了2颗糖")
    ......
    console.log("我吃了10000颗糖")
    }

    O(n)


    比如下面这样,n 的数值越大,算法需要分配的空间就需要越多,来存储数组里的值,所以它的空间复杂度就是 O(n),时间复杂度也是 O(n)


    function foo(n){
    let arr = []
    for( let i = 1; i < n; i++ ) {
    arr[i] = i
    }
    }

    O(n²)


    O(n²) 这种空间复杂度一般出现在比如二维数组,或是矩阵的情况下


    不用说,你肯定明白是啥情况啦


    就是遍历生成类似这样格式的


    let arr = [
    [1,2,3,4,5],
    [1,2,3,4,5],
    [1,2,3,4,5]
    ]

    结语


    希望本文对你有一点点帮助,另外,求个赞,谢谢! ^_^


    想要学好算法,就必须要理解复杂度这个重要基石


    复杂度分析不难,关键还是在于多练。每次看到代码的时候,简单的一眼就能看出复杂度,难的稍微分析一下也能得出答案。推荐去 leetCode 刷题哦,App或者PC端都可以


    链接:https://juejin.cn/post/6999307229752983582

    收起阅读 »

    什么?数学不好人都不配写CSS?

    前言 大家好,这里是 CSS 兼 WebGL 魔法使——alphardex。 之前一直在玩 three.js ,接触了很多数学函数,用它们创造过很多特效。于是我思考:能否在 CSS 中也用上这些数学函数,但发现 CSS 目前还没有,据说以后的新规范会纳入,估计...
    继续阅读 »

    前言


    大家好,这里是 CSS 兼 WebGL 魔法使——alphardex。


    之前一直在玩 three.js ,接触了很多数学函数,用它们创造过很多特效。于是我思考:能否在 CSS 中也用上这些数学函数,但发现 CSS 目前还没有,据说以后的新规范会纳入,估计也要等很久。


    然而,我们可以通过一些小技巧,来创作出一些属于自己的 CSS 数学函数,从而实现一些有趣的动画效果。


    让我们开始吧!



    CSS 数学函数


    注意:以下的函数用原生 CSS 也都能实现,这里用 SCSS 函数只是为了方便封装,封装起来的话更方便调用


    绝对值


    绝对值就是正的还是正的,负的变为正的


    可以创造 2 个数,其中一个数是另一个数的相反数,比较它们的最大值,即可获得这个数的绝对值


    @function abs($v) {
    @return max(#{$v}, calc(-1 * #{$v}));
    }

    中位数


    原数减 1 并乘以一半即可


    @function middle($v) {
    @return calc(0.5 * (#{$v} - 1));
    }

    数轴上两点距离


    数轴上两点距离就是两点所表示数字之差的绝对值,有了上面的绝对值公式就可以直接写出来


    @function dist-1d($v1, $v2) {
    $v-delta: calc(#{$v1} - #{$v2});
    @return #{abs($v-delta)};
    }

    三角函数


    其实这个笔者也不会实现~不过之前看到过好友 chokcoco 的一篇文章写到了如何在 CSS 中实现三角函数,在此表示感谢


    @function fact($number) {
    $value: 1;
    @if $number>0 {
    @for $i from 1 through $number {
    $value: $value * $i;
    }
    }
    @return $value;
    }

    @function pow($number, $exp) {
    $value: 1;
    @if $exp>0 {
    @for $i from 1 through $exp {
    $value: $value * $number;
    }
    } @else if $exp < 0 {
    @for $i from 1 through -$exp {
    $value: $value / $number;
    }
    }
    @return $value;
    }

    @function rad($angle) {
    $unit: unit($angle);
    $unitless: $angle / ($angle * 0 + 1);
    @if $unit==deg {
    $unitless: $unitless / 180 * pi();
    }
    @return $unitless;
    }

    @function pi() {
    @return 3.14159265359;
    }

    @function sin($angle) {
    $sin: 0;
    $angle: rad($angle);
    // Iterate a bunch of times.
    @for $i from 0 through 20 {
    $sin: $sin + pow(-1, $i) * pow($angle, (2 * $i + 1)) / fact(2 * $i + 1);
    }
    @return $sin;
    }

    @function cos($angle) {
    $cos: 0;
    $angle: rad($angle);
    // Iterate a bunch of times.
    @for $i from 0 through 20 {
    $cos: $cos + pow(-1, $i) * pow($angle, 2 * $i) / fact(2 * $i);
    }
    @return $cos;
    }

    @function tan($angle) {
    @return sin($angle) / cos($angle);
    }

    例子


    以下的几个动画特效演示了上面数学函数的作用


    一维交错动画


    初始状态


    创建一排元素,用内部阴影填充,准备好我们的数学函数


    <div class="list">
    <div class="list-item"></div>
    ...(此处省略14个 list-item)
    <div class="list-item"></div>
    </div>

    body {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
    background: #222;
    }

    :root {
    --blue-color-1: #6ee1f5;
    }

    (这里复制粘贴上文所有的数学公式)

    .list {
    --n: 16;

    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;

    &-item {
    --p: 2vw;
    --gap: 1vw;
    --bg: var(--blue-color-1);

    @for $i from 1 through 16 {
    &:nth-child(#{$i}) {
    --i: #{$i};
    }
    }

    padding: var(--p);
    margin: var(--gap);
    box-shadow: inset 0 0 0 var(--p) var(--bg);
    }
    }

    fb7wZV.png


    应用动画


    这里用了 2 个动画:grow 负责将元素缩放出来;melt 负责“融化”元素(即消除阴影的扩散半径)


    <div class="list grow-melt">
    <div class="list-item"></div>
    ...(此处省略14个 list-item)
    <div class="list-item"></div>
    </div>

    .list {
    &.grow-melt {
    .list-item {
    --t: 2s;

    animation-name: grow, melt;
    animation-duration: var(--t);
    animation-iteration-count: infinite;
    }
    }
    }

    @keyframes grow {
    0% {
    transform: scale(0);
    }

    50%,
    100% {
    transform: scale(1);
    }
    }

    @keyframes melt {
    0%,
    50% {
    box-shadow: inset 0 0 0 var(--p) var(--bg);
    }

    100% {
    box-shadow: inset 0 0 0 0 var(--bg);
    }
    }

    fqkIkF.gif


    交错动画



    1. 计算出元素下标的中位数

    2. 计算每个元素 id 到这个中位数的距离

    3. 根据距离算出比例

    4. 根据比例算出 delay


    <div class="list grow-melt middle-stagger">
    <div class="list-item"></div>
    ...(此处省略14个 list-item)
    <div class="list-item"></div>
    </div>

    .list {
    &.middle-stagger {
    .list-item {
    --m: #{middle(var(--n))}; // 中位数,这里是7.5
    --i-m-dist: #{dist-1d(var(--i), var(--m))}; // 计算每个id到中位数之间的距离
    --ratio: calc(var(--i-m-dist) / var(--m)); // 根据距离算出比例
    --delay: calc(var(--ratio) * var(--t)); // 根据比例算出delay
    --n-delay: calc((var(--ratio) - 2) * var(--t)); // 负delay表示动画提前开始

    animation-delay: var(--n-delay);
    }
    }
    }

    fqkzkD.gif


    地址:Symmetric Line Animation


    二维交错动画


    初始状态


    如何将一维的升成二维?应用网格系统即可


    <div class="grid">
    <div class="grid-item"></div>
    ...(此处省略62个 grid-item)
    <div class="grid-item"></div>
    </div>

    .grid {
    $row: 8;
    $col: 8;
    --row: #{$row};
    --col: #{$col};
    --gap: 0.25vw;

    display: grid;
    gap: var(--gap);
    grid-template-rows: repeat(var(--row), 1fr);
    grid-template-columns: repeat(var(--col), 1fr);

    &-item {
    --p: 2vw;
    --bg: var(--blue-color-1);

    @for $y from 1 through $row {
    @for $x from 1 through $col {
    $k: $col * ($y - 1) + $x;
    &:nth-child(#{$k}) {
    --x: #{$x};
    --y: #{$y};
    }
    }
    }

    padding: var(--p);
    box-shadow: inset 0 0 0 var(--p) var(--bg);
    }
    }

    fLsvPx.png


    应用动画


    跟上面的动画一模一样


    <div class="grid grow-melt">
    <div class="grid-item"></div>
    ...(此处省略62个 grid-item)
    <div class="grid-item"></div>
    </div>

    .grid {
    &.grow-melt {
    .grid-item {
    --t: 2s;

    animation-name: grow, melt;
    animation-duration: var(--t);
    animation-iteration-count: infinite;
    }
    }
    }

    fLsGvD.gif


    交错动画



    1. 计算出网格行列的中位数

    2. 计算网格 xy 坐标到中位数的距离并求和

    3. 根据距离算出比例

    4. 根据比例算出 delay


    <div class="grid grow-melt middle-stagger">
    <div class="grid-item"></div>
    ...(此处省略62个 grid-item)
    <div class="grid-item"></div>
    </div>

    .grid {
    &.middle-stagger {
    .grid-item {
    --m: #{middle(var(--col))}; // 中位数,这里是7.5
    --x-m-dist: #{dist-1d(var(--x), var(--m))}; // 计算x坐标到中位数之间的距离
    --y-m-dist: #{dist-1d(var(--y), var(--m))}; // 计算y坐标到中位数之间的距离
    --dist-sum: calc(var(--x-m-dist) + var(--y-m-dist)); // 距离之和
    --ratio: calc(var(--dist-sum) / var(--m)); // 根据距离和计算比例
    --delay: calc(var(--ratio) * var(--t) * 0.5); // 根据比例算出delay
    --n-delay: calc(
    (var(--ratio) - 2) * var(--t) * 0.5
    ); // 负delay表示动画提前开始

    animation-delay: var(--n-delay);
    }
    }
    }

    fL2Ppt.gif


    地址:Symmetric Grid Animation


    另一种动画


    可以换一种动画 shuffle(穿梭),会产生另一种奇特的效果


    <div class="grid shuffle middle-stagger">
    <div class="grid-item"></div>
    ...(此处省略254个 grid-item )
    <div class="grid-item"></div>
    </div>

    .grid {
    $row: 16;
    $col: 16;
    --row: #{$row};
    --col: #{$col};
    --gap: 0.25vw;

    &-item {
    --p: 1vw;

    transform-origin: bottom;
    transform: scaleY(0.1);
    }

    &.shuffle {
    .grid-item {
    --t: 2s;

    animation: shuffle var(--t) infinite ease-in-out alternate;
    }
    }
    }

    @keyframes shuffle {
    0% {
    transform: scaleY(0.1);
    }

    50% {
    transform: scaleY(1);
    transform-origin: bottom;
    }

    50.01% {
    transform-origin: top;
    }

    100% {
    transform-origin: top;
    transform: scaleY(0.1);
    }
    }

    fOJSZ8.gif


    地址:Shuffle Grid Animation


    余弦波动动画


    初始状态


    创建 7 个不同颜色的(这里直接选了彩虹色)列表,每个列表有 40 个子元素,每个子元素是一个小圆点


    让这 7 个列表排列在一条线上,且 z 轴上距离错开,设置好基本的 delay


    <div class="lists">
    <div class="list">
    <div class="list-item"></div>
    ...(此处省略39个 list-item)
    </div>
    ...(此处省略6个 list)
    </div>

    .lists {
    $list-count: 7;
    $colors: red, orange, yellow, green, cyan, blue, purple;

    position: relative;
    width: 34vw;
    height: 2vw;
    transform-style: preserve-3d;
    perspective: 800px;

    .list {
    position: absolute;
    top: 0;
    left: 0;
    display: flex;
    transform: translateZ(var(--z));

    @for $i from 1 through $list-count {
    &:nth-child(#{$i}) {
    --bg: #{nth($colors, $i)};
    --z: #{$i * -1vw};
    --basic-delay-ratio: #{$i / $list-count};
    }
    }

    &-item {
    --w: 0.6vw;
    --gap: 0.15vw;

    width: var(--w);
    height: var(--w);
    margin: var(--gap);
    background: var(--bg);
    border-radius: 50%;
    }
    }
    }

    hSdtfI.png


    余弦排列


    运用上文的三角函数公式,让这些小圆点以余弦的一部分形状进行排列


    .lists {
    .list {
    &-item {
    $item-count: 40;
    $offset: pi() * 0.5;
    --wave-length: 21vw;

    @for $i from 1 through $item-count {
    &:nth-child(#{$i}) {
    --i: #{$i};
    $ratio: ($i - 1) / ($item-count - 1);
    $angle-unit: pi() * $ratio;
    $wave: cos($angle-unit + $offset);
    --single-wave-length: calc(#{$wave} * var(--wave-length));
    --n-single-wave-length: calc(var(--single-wave-length) * -1);
    }
    }

    transform: translateY(var(--n-single-wave-length));
    }
    }
    }

    hSwuNj.png


    波动动画


    对每个小圆点应用上下平移动画,平移的距离就是余弦的波动距离


    .lists {
    .list {
    &-item {
    --t: 2s;

    animation: wave var(--t) infinite ease-in-out alternate;
    }
    }
    }

    @keyframes wave {
    from {
    transform: translateY(var(--n-single-wave-length));
    }

    to {
    transform: translateY(var(--single-wave-length));
    }
    }

    hSwfPA.gif


    交错动画


    跟上面一个套路,计算从中间开始的 delay,再应用到动画上即可


    .lists {
    .list {
    &-item {
    --n: #{$item-count + 1};
    --m: #{middle(var(--n))};
    --i-m-dist: #{dist-1d(var(--i), var(--m))};
    --ratio: calc(var(--i-m-dist) / var(--m));
    --square: calc(var(--ratio) * var(--ratio));
    --delay: calc(
    calc(var(--square) + var(--basic-delay-ratio) + 1) * var(--t)
    );
    --n-delay: calc(var(--delay) * -1);

    animation-delay: var(--n-delay);
    }
    }
    }

    hSwqaQ.gif


    地址:Rainbow Sine


    最后


    CSS 数学函数能实现的特效远不止于此,希望通过本文能激起大家创作特效的灵感~


    作者:alphardex
    链接:https://juejin.cn/post/6999416290997698596

    收起阅读 »

    聊一聊移动端适配

    一、引言 用户选择大屏幕有两个几个出发点,有些人想要更大的字体,更大的图片;有些人想要更多的内容,并不想要更大的图标;有些人想要个镜子…. 充分了解各种设备,我们会知道不同尺寸的屏幕本身就有各自的定位,像ipad类的大屏设备本身相比较iphone5就应该具...
    继续阅读 »

    一、引言



    用户选择大屏幕有两个几个出发点,有些人想要更大的字体,更大的图片;有些人想要更多的内容,并不想要更大的图标;有些人想要个镜子….



    充分了解各种设备,我们会知道不同尺寸的屏幕本身就有各自的定位,像ipad类的大屏设备本身相比较iphone5就应该具有更大的视野,而不是粗暴的让用户去感受老人机的体验。


    但由于设计及开发资源的紧张,现阶段只能将一套设计稿应用在多尺寸设备上,因此我们需要考虑在保持一套设计稿的方案下如何使展示更加合理。


    二、基本单位


    对于移动端开发而言,为了做到页面高清的效果,视觉稿的规范往往会遵循以下两点:



    1. 首先,选取一款手机的屏幕宽高作为基准(以前是iphone4的320×480,现在更多的是iphone6的375×667)。

    2. 对于retina屏幕(如: dpr=2),为了达到高清效果,视觉稿的画布大小会是基准的2倍,也就是说像素点个数是原来的4倍(对iphone6而言:原先的375×667,就会变成750×1334)。


    物理像素(physical pixel)


    一个物理像素是显示器(手机屏幕)上最小的物理显示单元,在操作系统的调度下,每一个设备像素都有自己的颜色值和亮度值。


    设备独立像素(density-independent pixel)


    设备独立像素(也叫密度无关像素),可以认为是计算机坐标系统中得一个点,这个点代表一个可以由程序使用的虚拟像素(比如: css像素),然后由相关系统转换为物理像素。


    所以说,物理像素和设备独立像素之间存在着一定的对应关系,这就是接下来要说的设备像素比。


    DPR 设备像素比(device pixel ratio )


    设备像素比 = 物理像素 / 设备独立像素; // 在某一方向上,x方向或者y方向
    可以在JS中 window.devicePixelRatio获取到当前设备的dpr


    三、常见的布局类型


    rem 布局


    原理: 根据手机的屏幕尺寸 和dpr,动态修改html的基准值(font-size)


    公式: rem = document.documentElement.clientWidth * dpr / 100


    注释: 乘以dpr,是因为页面有可能为了实现1px border页面会缩放(scale) 1/dpr 倍(如果没有,dpr=1)


    假设我们将屏幕宽度平均分成100份,每一份的宽度用per表示,per = 屏幕宽度 / 100,如果将per作为单位,per前面的数值就代表屏幕宽度的百分比


    p {width: 50per;} /* 屏幕宽度的50% */

    如果想要页面元素随着屏幕宽度等比变化,我们需要上面的per单位,如果子元素设置rem单位的属性,通过更改html元素的字体大小,就可以让子元素实际大小发生变化


    html {font-size: 16px}
    p {width: 2rem} /* 32px*/

    html {font-size: 32px}
    p {width: 2rem} /*64px*/

    如果让html元素字体的大小,恒等于屏幕宽度的1/100,那1rem和1per就等价了


    html {fons-size: 元素宽度 / 100}
    p {width: 50rem} /* 50rem = 50per = 屏幕宽度的50% */

    实际应用



    rem作用于非根元素时,相对于根元素字体大小;rem作用于根元素字体大小时,相对于其初始字体大小



    可以看出 rem 取值分为两种情况,设置在根元素时和非根元素时,举个例子:


    /* 作用于根元素,相对于原始大小(16px),所以html的font-size为32px*/
    html {font-size: 2rem}

    /* 作用于非根元素,相对于根元素字体大小,所以为64px */
    p {font-size: 2rem}

    举个例子:
























    vw


    vw/vh是基于 Viewport 视窗的长度单位window.innerWidth/window.innerHeight
    在CSS Values and Units Module Level 3中和Viewport相关的单位有四个,分别为vwvhvminvmax



    • vw:是Viewport’s width的简写, 1vw等于window.innerWidth的1%

    • vh:和vw类似,是Viewport’s height的简写,1vh等于window.innerHeihgt的1%\

    • vmin:vmin的值是当前vw和vh中较小的值

    • vmax:vmax的值是当前vw和vh中较大的值


    image.png
    可以看到vw其实是实现了1vw = 1per,比起rem需要计算html的基准值,vw无疑更加方便。


    /* rem方案 */
    html {fons-size: width / 100}
    p {width: 15.625rem}

    /* vw方案 */
    p {width: 15.625vw}

    Q:vw如此方便,是不是就比rem更好,可以完全取代rem了呢?


    A:当然不是。


    vw也有缺点。



    • vw换算有时并不精确,较小的像素不好适配,就像我们可以用较小值精确地表示较大值,用较大值表示较小值就可能存在数位换算等问题而无法精确表示。

    • vw的兼容性不如rem

    • 使用弹性布局时,vw无法限制最大宽度。rem可以通过控制HTML基准值,来实现最大宽度的限制。


    Q:rem就如此完美吗?


    A:rem也并不是万能的



    • rem的制作成本更大,需要使用额外的插件去实现。

    • 字体不能用rem,字体大小和字体宽度不成线性关系,所有字体大小不能使用rem,由于设置了根元素字体的大小,会影响所有没有设置字体的元素,因此需要设置所有需要字体控制的元素。

    • 从用户体验上来看,文字阅读的舒适度跟媒体介质大小是没关系的。


    四、适配方案


    方案一: rem/vw


    适用场景:



    • 对视觉组件种类较多,视觉设计对元素位置的相对关系依赖较强的移动端页面:vw/rem


    示例:



    • 饿了么(h5.ele.me/msite/)

    • 对viewport进行了缩放

    • html元素的font-size依然由px指定

    • 具体元素的布局上使用vw + rem fallbak的形式

    • 没有限制布局宽度

    • css构建过程需要插件支持


    方案二: flex + px + 百分比


    适用场景:



    • 追求阅读体验的场景,如列表页。


    示例:





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

    收起阅读 »

    8个工程必备的JavaScript代码片段(建议添加到项目中)

    1. 获取文件后缀名 使用场景:上传文件判断后缀名 /** * 获取文件后缀名 * @param {String} filename */ export function getExt(filename) { if (typeof filena...
    继续阅读 »

    1. 获取文件后缀名


    使用场景:上传文件判断后缀名


    /**
    * 获取文件后缀名
    * @param {String} filename
    */
    export function getExt(filename) {
    if (typeof filename == 'string') {
    return filename
    .split('.')
    .pop()
    .toLowerCase()
    } else {
    throw new Error('filename must be a string type')
    }
    }

    使用方式


    getExt("1.mp4") //->mp4

    2. 复制内容到剪贴板


    export function copyToBoard(value) {
    const element = document.createElement('textarea')
    document.body.appendChild(element)
    element.value = value
    element.select()
    if (document.execCommand('copy')) {
    document.execCommand('copy')
    document.body.removeChild(element)
    return true
    }
    document.body.removeChild(element)
    return false
    }


    使用方式:


    //如果复制成功返回true
    copyToBoard('lalallala')

    原理:



    1. 创建一个textare元素并调用select()方法选中

    2. document.execCommand('copy')方法,拷贝当前选中内容到剪贴板。


    3. 休眠多少毫秒


    /**
    * 休眠xxxms
    * @param {Number} milliseconds
    */
    export function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
    }

    //使用方式
    const fetchData=async()=>{
    await sleep(1000)
    }

    4. 生成随机字符串


    /**
    * 生成随机id
    * @param {*} length
    * @param {*} chars
    */
    export function uuid(length, chars) {
    chars =
    chars ||
    '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    length = length || 8
    var result = ''
    for (var i = length; i > 0; --i)
    result += chars[Math.floor(Math.random() * chars.length)]
    return result
    }

    使用方式


    //第一个参数指定位数,第二个字符串指定字符,都是可选参数,如果都不传,默认生成8位
    uuid()

    使用场景:用于前端生成随机的ID,毕竟现在的Vue和React都需要绑定key


    5. 简单的深拷贝


    /**
    *深拷贝
    * @export
    * @param {*} obj
    * @returns
    */
    export function deepCopy(obj) {
    if (typeof obj != 'object') {
    return obj
    }
    if (obj == null) {
    return obj
    }
    return JSON.parse(JSON.stringify(obj))
    }

    缺陷:只拷贝对象、数组以及对象数组,对于大部分场景已经足够


    const person={name:'xiaoming',child:{name:'Jack'}}
    deepCopy(person) //new person

    6. 数组去重


    /**
    * 数组去重
    * @param {*} arr
    */
    export function uniqueArray(arr) {
    if (!Array.isArray(arr)) {
    throw new Error('The first parameter must be an array')
    }
    if (arr.length == 1) {
    return arr
    }
    return [...new Set(arr)]
    }

    原理是利用Set中不能出现重复元素的特性


    uniqueArray([1,1,1,1,1])//[1]

    7. 对象转化为FormData对象


    /**
    * 对象转化为formdata
    * @param {Object} object
    */

    export function getFormData(object) {
    const formData = new FormData()
    Object.keys(object).forEach(key => {
    const value = object[key]
    if (Array.isArray(value)) {
    value.forEach((subValue, i) =>
    formData.append(key + `[${i}]`, subValue)
    )
    } else {
    formData.append(key, object[key])
    }
    })
    return formData
    }

    使用场景:上传文件时我们要新建一个FormData对象,然后有多少个参数就append多少次,使用该函数可以简化逻辑


    使用方式:


    let req={
    file:xxx,
    userId:1,
    phone:'15198763636',
    //...
    }
    fetch(getFormData(req))

    8.保留到小数点以后n位


    // 保留小数点以后几位,默认2位
    export function cutNumber(number, no = 2) {
    if (typeof number != 'number') {
    number = Number(number)
    }
    return Number(number.toFixed(no))
    }

    使用场景:JS的浮点数超长,有时候页面显示时需要保留2位小数


    作者:_红领巾
    链接:https://juejin.cn/post/6999391770672889893

    收起阅读 »

    前端工程化实战 - 可配置的模板管理

    功能设计 如果每一次新脚手架的开发或者模板的更新都需要重新更新一次 CLI 的话,虽然成本不高,但是开发模板的同学需要通知 CLI 开发的同学去升级,使用模板的同学又需要在等 CLI 开发完毕才能使用,中间交流沟通的成本就增加了。 其次,对于业务开发同学来说,...
    继续阅读 »

    功能设计


    如果每一次新脚手架的开发或者模板的更新都需要重新更新一次 CLI 的话,虽然成本不高,但是开发模板的同学需要通知 CLI 开发的同学去升级,使用模板的同学又需要在等 CLI 开发完毕才能使用,中间交流沟通的成本就增加了。


    其次,对于业务开发同学来说,可能只需要一类或者几类的模板,那么如果 CLI 是一个大而全的模板集合,对这些同学来说,快速选择模板创建项目反而也是一个负担,因为要在很多模板中选择自己想要的也是很花费时间。


    所以我们的目的是设计一款拥有自定义配置与可升级模板功能的 CLI 工具。


    未命名文件.png


    既然是自定义配置,那么就需要用户可以在本地手动添加、删除、更新自己常用的模板信息,同时需要可以动态的拉取这些模板而不是一直下载下来就是本地的旧版本。


    根据需求,可简单设计一下我们 CLI 的模板功能概要:



    1. 需要保存模板来源的地址

    2. 根据用户的选择拉取不同的模板代码

    3. 将模板保存在本地


    实战开发


    那么根据上面的设计思路,我们可以一步步开发所需要的功能


    本地保存模板地址功能


    第一步,如果需要将模板的一些信息保存在本地的话,我们需要一个对话型的交互,引导用户输入我们需要的信息,所以可以选择 inquirer 这个工具库。



    Inquirerjs 是一个用来实现命令行交互式界面的工具集合。它帮助我们实现与用户的交互式交流,比如给用户提一个问题,用户给我们一个答案,我们根据用户的答案来做一些事情,典型应用如 plop等生成器工具。



    一般拉取代码的话,我们需要知道用户输入的模板地址(通过 URL 拉取对应模板的必须条件)、模板别名(方便用户做搜索)、模板描述(方便用户了解模板信息)


    这样需要保存的模板信息有地址、别名与描述,后续可以方便我们去管理对应的模板。示例代码如下:


    import inquirer from 'inquirer';
    import { addTpl } from '@/tpl'

    const promptList = [
    {
    type: 'input',
    message: '请输入仓库地址:',
    name: 'tplUrl',
    default: 'https://github.com/boty-design/react-tpl'
    },
    {
    type: 'input',
    message: '模板标题(默认为 Git 名作为标题):',
    name: 'name',
    default({ tplUrl }: { tplUrl: string }) {
    return tplUrl.substring(tplUrl.lastIndexOf('/') + 1)
    }
    },
    {
    type: 'input',
    message: '描述:',
    name: 'desc',
    }
    ];

    export default () => {
    inquirer.prompt(promptList).then((answers: any) => {
    const { tplUrl, name, desc } = answers
    addTpl(tplUrl, name, desc)
    })
    }
    复制代码

    通过 inquirer 已经拿到了对应的信息,但由于会有电脑重启等各种情况发生,所以数据存在缓存中是不方便的,这种 CLI 工具如果使用数据库来存储也是大材小用,所以可以将信息直接已经以 json 文件的方式存储在本地。


    示例代码如下:


    import { loggerError, loggerSuccess, getDirPath } from '@/util'
    import { loadFile, writeFile } from '@/util/file'

    interface ITpl {
    tplUrl: string
    name: string
    desc: string
    }

    const addTpl = async (tplUrl: string, name: string, desc: string) => {
    const cacheTpl = getDirPath('../cacheTpl')
    try {
    const tplConfig = loadFile<ITpl[]>(`${cacheTpl}/.tpl.json`)
    let file = [{
    tplUrl,
    name,
    desc
    }]
    if (tplConfig) {
    const isExist = tplConfig.some(tpl => tpl.name === name)
    if (isExist) {
    file = tplConfig.map(tpl => {
    if (tpl.name === name) {
    return {
    tplUrl,
    name,
    desc
    }
    }
    return tpl
    })
    } else {
    file = [
    ...tplConfig,
    ...file
    ]
    }
    }
    writeFile(cacheTpl, '.tpl.json', JSON.stringify(file, null, "\t"))
    loggerSuccess('Add Template Successful!')
    } catch (error) {
    loggerError(error)
    }
    }

    export {
    addTpl,
    }

    这里我们需要对是否保存还是更新模板做一个简单的流程判断:



    1. 判断当前是否存在 tpl 的缓存文件,如果已存在缓存文件,那么需要跟当前的模板信息合并,如果不存在的话则需要创建文件,将获取的信息保存进去。

    2. 如果当前已存在缓存文件,需要根据 name 判断是已经被缓存了,如果被缓存了的话,则根据 name 来更新对应的模板信息。


    接下来,我们来演示一下,使用的效果。


    根据之前的操作,构建完 CLI 之后,运行 fe-cil add tpl 可以得到如下的结果:


    image.png


    那么在对应的路径可以看到已经将这条模板信息缓存下来了。


    image.png


    如上,我们已经完成一个简单的本地对模板信息添加与修改功能,同样删除也是类似的操作,根据自己的实际需求开发即可。


    下载模板


    在保存了模板之后,我们需要选择对应的模板下载了。


    下载可以使用 download-git-repo 作为 CLI 下载的插件,这是一款非常好用的插件,支持无 clone 去下载对应的模板,非常适合我们的项目。



    download-git-repo 是一款下载 git repository 的工具库,它提供了简写与 direct:url 直接下载两种方式,同时也提供直接下载代码与 git clone 的功能,非常使用与方便。



    同样在下载模板的时候,我们需要给用户展示当前的保存好的模板列表,这里同样需要使用到 inquirer 工具。



    1. 使用 inquirer 创建 list 选择交互模式,读取本地模板列表,让用户选择需要的模板


    export const selectTpl = () => {
    const tplList = getTplList()
    const promptList = [
    {
    type: 'list',
    message: '请选择模板下载:',
    name: 'name',
    choices: tplList && tplList.map((tpl: ITpl) => tpl.name)
    },
    {
    type: 'input',
    message: '下载路径:',
    name: 'path',
    default({ name }: { name: string }) {
    return name.substring(name.lastIndexOf('/') + 1)
    }
    }
    ];

    inquirer.prompt(promptList).then((answers: any) => {
    const { name, path } = answers
    const select = tplList && tplList.filter((tpl: ITpl) => tpl.name)
    const tplUrl = select && select[0].tplUrl || ''
    loadTpl(name, tplUrl, path)
    })
    }


    1. 使用 download-git-repo 下载对应的模板


    export const loadTpl = (name: string, tplUrl: string, path: string) => {
    download(`direct:${tplUrl}`, getCwdPath(`./${path}`), (err: string) => {
    if (err) {
    loggerError(err)
    } else {
    loggerSuccess(`Download ${name} Template Successful!`)
    }
    })
    }

    但是问题来了,如果选择 direct 的模式,那么下载的是一个 zip 的地址,而不是正常的 git 地址,那么我们上述的地址就无效了,所以在正式下载代码之前需要对地址做一层转换。


    首先看拉取规则,正常的 git 地址是 https://github.com/boty-design/react-tpl,而实际在 github 中下载的地址则是 https://codeload.github.com/boty-design/react-tpl/zip/refs/heads/main,可以看到对比正常的 github 链接的话,域名跟链接都有所改变,但是一定有项目名跟团队名,所以我们在存储的时候可以将 boty-design/react-tpl 拆出来,后期方便我们组装。


    const { pathname } = new URL(tplUrl)
    if (tplUrl.includes('github.com')) {
    reTpl.org = pathname.substring(1)
    reTpl.downLoadUrl = 'https://codeload.github.com'
    }

    如上述代码,解析 tplUrl 拿到的 pathname 就是我们需要的信息,再 dowload 模板的时候,重新组装下载链接即可。


    image.png


    image.png


    如上图所示,我们可以将公共的模板下载到本地,方便同学正常开发了,但是此时还有一个问题,那就是上面的分支是 main 分支,不是每一个模板都有这个分支,可控性太差,那么我们怎么拿到项目所有的分支来选择性下载呢。


    Github Api


    在 Github 中对于开源、不是私有的项目,可以省去授权 token 的步骤,直接使用 Github Api 获取到对应的信息。


    所以针对上面提到问题,我们可以借助 Github Api 提供的能力来解决。


    获取分支的链接是 https://api.github.com/repos/boty-design/react-tpl/branches,在开发之前我们可以使用 PostMan 来测试一下是否正常返回我们需要的结果。


    image.png


    如上可以看到,已经能通过 Github Api 拿到我们想要的分支信息了。



    如果出现了下述错误的话,没关系,只是 github 限制访问的频率罢了



    image.png


    针对上述的问题,我们需要的是控制频率、使用带条件的请求或者使用 token 请求 Github Api 的方式来规避,但是鉴于模板来说,一般请求频率也不会很高,只是我在开发的时候需要不断的请求来测试,才会出现这种问题,各位同学有兴趣的话可以自己试试其他的解决方案。


    分支代码优化


    未命名文件 (1).png


    在预研完 Github Api 之后,接下来就需要对拿到的信息做一层封装,例如只有一条分支的时候用户可以直接下载模板,如果请求到多条分支的时候,则需要显示分支让用户自由选择对应的分支下载模板,整体的业务流程图如上所示。


    主要逻辑代码如下:


    export const selectTpl = async () => {
    const prompts: any = new Subject();
    let select: ITpl
    let githubName: string
    let path: string
    let loadUrl: string

    try {
    const onEachAnswer = async (result: any) => {
    const { name, answer } = result
    if (name === 'name') {
    githubName = answer
    select = tplList.filter((tpl: ITpl) => tpl.name === answer)[0]
    const { downloadUrl, org } = select
    const branches = await getGithubBranch(select) as IBranch[]
    loadUrl = `${downloadUrl}/${org}/zip/refs/heads`
    if (branches.length === 1) {
    loadUrl = `${loadUrl}/${branches[0].name}`
    prompts.next({
    type: 'input',
    message: '下载路径:',
    name: 'path',
    default: githubName
    });
    } else {
    prompts.next({
    type: 'list',
    message: '请选择分支:',
    name: 'branch',
    choices: branches.map((branch: IBranch) => branch.name)
    });
    }
    }
    if (name === 'branch') {
    loadUrl = `${loadUrl}/${answer}`
    prompts.next({
    type: 'input',
    message: '下载路径:',
    name: 'path',
    default: githubName
    });
    }
    if (name === 'path') {
    path = answer
    prompts.complete();
    }
    }

    const onError = (error: string) => {
    loggerError(error)
    }

    const onCompleted = () => {
    loadTpl(githubName, loadUrl, path)
    }

    inquirer.prompt(prompts).ui.process.subscribe(onEachAnswer, onError, onCompleted);

    const tplList = getTplList() as ITpl[]

    prompts.next({
    type: 'list',
    message: '请选择模板:',
    name: 'name',
    choices: tplList.map((tpl: ITpl) => tpl.name)
    });
    } catch (error) {
    loggerError(error)
    }
    }

    上述代码,我们可以看到使用了 RXJS 来动态的渲染交互问题,因为存在一些模板的项目分支只有一个的情况。如果我们每次都需要用户都去选择分支是有多余累赘的,所以固定的问题式交互已经不适用了,我们需要借助 RXJS 动态添加 inquirer 问题,通过获取的分支数量来判断是否出现选择分支这个选项,提高用户体验。



    链接:https://juejin.cn/post/6999397309180182564

    收起阅读 »

    CSS为什么这么难学?方法很重要!

    大家好,我是零一。前段时间我在知乎刷到这样一个提问:为什么CSS这么难学? 看到这个问题以后,我仔细一想,CSS学习起来好像是挺困难的,它似乎没有像JavaScript那样非常系统的学习大纲,大家平时也不会用到所有的CSS,基本上用来用去就是那么几个常用的属...
    继续阅读 »

    大家好,我是零一。前段时间我在知乎刷到这样一个提问:为什么CSS这么难学?


    知乎某用户提问


    看到这个问题以后,我仔细一想,CSS学习起来好像是挺困难的,它似乎没有像JavaScript那样非常系统的学习大纲,大家平时也不会用到所有的CSS,基本上用来用去就是那么几个常用的属性,甚至就连很多培训机构的入门教学视频都也只会教你一些常用的CSS(不然你以为一个几小时的教学视频怎么能让你快速入门CSS的呢?)


    一般别人回答你CSS很好学也是因为它只用那些常用的属性,他很有可能并没有深入去了解。要夸张一点说,CSS应该也能算作一门小小的语言了吧,深入研究进去,知识点也不少。我们如果不是专门研究CSS的,也没必要做到了解CSS的所有属性的使用以及所有后续新特性的语法,可以根据工作场景按需学习,但要保证你学习的属性足够深入~


    那么我们到底该如何学习CSS呢? 为此我列了一个简单的大纲,想围绕这几点大概讲一讲


    CSS学习大纲


    一、书籍、社区文章


    这应该是大家学习CSS最常见的方式了(我亦如此)。有以下几个场景:


    场景一:开发中遇到「文本字数超出后以省略号(...)展示」的需求,打开百度搜索:css字数过多用省略号展示,诶~搜到了!ctrl+c、ctrl+v,学废了,完工!


    搜索引擎学习法


    场景二:某天早晨逛技术社区,看到一篇关于CSS的文章,看到标题中有个CSS属性叫resizeresize属性是啥,我咋没用过?点进去阅读得津津有味~ two minutes later ~ 奥,原来还有这个属性,是这么用的呀,涨姿势了!


    社区博客学习法


    场景三:我决定了,我要好好学CSS,打开购物网站搜索:CSS书籍,迅速下单!等书到了,开始每天翻阅学习。当然了此时又有好几种情况了,分别是:



    • 就只有刚拿到书的第一天翻阅了一下,往后一直落灰

    • 看了一部分,但又懒得动手敲代码,最终感到无趣放弃了阅读

    • 认认真真看完了书,也跟着书上的代码敲了,做了很多笔记,最终学到了很多



    无论是上面哪几种方式,我觉得都是挺不错的,顺便再给大家推荐几个不错的学习资源



    毕竟站在巨人的肩膀上,才是最高效的,你们可以花1个小时学习到大佬们花1天才总结出来的知识


    二、记住CSS的数据类型


    CSS比较难学的另一个点,可能多半是因为CSS的属性太多了,而且每个属性的值又支持很多种写法,所以想要轻易记住每个属性的所有写法几乎是不太可能的。最近在逛博客时发现原来CSS也有自己的数据类型,这里引用一下张鑫旭大佬的CSS值类型文档大全,方便大家后续查阅


    简单介绍一下CSS的数据类型就是这样的:


    CSS数据类型


    图中用<>括起来的表示一种CSS数据类型,介绍一下图中几个类型:



    • :表示值可以是数字

    • :表示元素的尺寸长度,例如3px33em34rem

    • :表示基于父元素的百分比,例如33%

    • :表示值既可以是 ,也可以是

    • :表示元素的位置。值可以是 left/right/top/bottom


    来看两个CSS属性:



    • 第一个是width,文档会告诉你该属性支持的数据类型有 ,那么我们就知道该属性有以下几种写法:width: 1pxwidth: 3remwidth: 33emwidth: 33%

    • 第二个属性是background-position,文档会告诉你该属性支持的数据类型有 ,那么我们就知道该属性有以下几种写法:background-position: leftbackground-position: right background-position: topbackground-position: bottombackground-position: 30%background-position: 3rem


    从这个例子中我们可以看出,想要尽可能得记住更多的CSS属性的使用,可以从记住CSS数据类型(现在差不多有40+种数据类型)开始,这样你每次学习新的CSS属性时,思路就会有所转变,如下图


    没记住CSS数据类型的我:


    之前的思想


    记住CSS数据类型的我:


    现在的思想


    不知道你有没有发现,如果文档只告诉你background-position支持 数据类型,你确定你能知道该属性的全部用法吗?你确实知道该属性支持background-position: 3rem这样的写法,因为你知道 数据类型包含了 数据类型,但你知道它还支持background-position: bottom 50px right 100px;这样的写法吗?为什么可以写四个值并且用空格隔开?这是谁告诉你的?


    这就需要我们了解CSS的语法了,请认真看下一节


    三、读懂CSS的语法


    我之前某个样式中需要用到裁剪的效果,所以准备了解一下CSS中的clip-path属性怎么使用,于是就查询了比较权威的clip-path MDN,看着看着,我就发现了这个


    clip-path 语法


    我这才意识到我竟然连CSS的语法都看不懂。说实话,以前无论是初学CSS还是临时找一下某个CSS属性的用法,都是直接百度,瞬间就能找到自己想要的答案(例如菜鸟教程),而这次,我是真的傻了! 因为本身clip-path这个属性就比较复杂,支持的语法也比较多,光看MDN给你的示例代码根本无法Get到这个属性所有的用法和含义(菜鸟教程就更没法全面地教你了)


    于是我就顺着网线去了解了一下CSS的语法中的一些符号的含义,帮助我更好得理解语法


    因为关于CSS语法符号相关的知识在CSS属性值定义语法 MDN上都有一篇超级详细的介绍了(建议大家一定要先看看MDN这篇文章!!非常通俗易懂),所以我就不多做解释了,这里只放几个汇总表格


    属性组合符号


    解读CSS语法


    以本节clip-path的语法为例,我们来简单对其中某一个属性来进行解读(只会解读部分哦,因为解读全部的话篇幅会很长很长)


    先看看整体的结构


    clip-path的语法


    一共分为四部分,顺序是从上到下的,每两个部分之间都以where来连接,表示的是where下面的部分是对上面那个部分的补充解释


    :表示的是clip-path这个属性支持的写法为:要不只写 数据类型的值,要不就最起码从 这两者之间选一种类型的值来写,要不就为none


    :我们得知①中的 数据类型支持的写法为:inset()circle()ellipse()polygon()path()这5个函数


    :因为我们想了解circle()这个函数的具体使用,所以就先只看这个了。我们得知circle()函数的参数支持 两种数据结构,且两者都是可写可不写,但如果要写 ,那前面必须加一个at


    :首先看到 支持的属性是 (这个顾名思义就是)、closest-sidefarthest-side。而 数据类型的语法看起来就比较复杂了,我们单独来分析,因为真的非常非常长,我将 格式化并美化好给你展现出来,便于你们阅读(我也建议你们如果在学习某个属性的语法时遇到这么长的语法介绍,也像我一下把它格式化一下,这样方便你们阅读和理解)


    <position>数据类型的语法


    如图可得,整体分为三大部分,且这三部分是互斥关系,即这三部分只能出现一个,再根据我们前面学习的CSS语法的符号,就可以知道怎么使用了,因为这里支持的写法太多了,我直接列个表格吧(其实就是排列组合)!如果还有不懂的,你们可以仔细阅读一下MDN的语法介绍或者也可以评论区留言问我,我看到会第一时间回复!


    类型支持的写法


    嚯!累死我了,这支持的写法也太多太多了吧!


    四、多动手尝试


    上一节,我们在学习clip-path属性的语法以后,知道了我们想要的圆圈裁剪(circle())的语法怎么写,那么你就真的会了吗?可能你看了MDN给你举的例子,知道了circle(40%)大致实现的效果是咋样的,如下图


    MDN clip-path的简单案例


    如我前文说的一样,MDN只给你列举了circle()这个函数最简单的写法,但我们刚刚学习了其语法,得知还有别的写法(例如circle(40% at left)),而且MDN文档也只是告诉你支持哪些语法,它也并没有明确告诉你,哪个语法的作用是怎么样的,能实现什么样的效果。


    此时就需要我们自己上手尝试了






    <span class="scss">尝试<span class="hljs-attribute">clip-path</span>的circle()的使用</span>







    看一下效果,嗯,跟MDN展示的是一样的


    clip-path: circle(40%)


    再修改一下值clip-path: circle(60%),看看效果


    clip-path: circle(60%)


    我似乎摸出了规律,看样子是以元素的中心为基准点,60%的意思就是从中心到边缘长度的60%为半径画一个圆,裁剪掉该圆之外的内容。这些都是MDN文档里没有讲到的,靠我亲手实践验证出来的。


    接下来我们来试试其它的语法~


    试试将值改成clip-path: circle(40% at top)


    clip-path: circle(40% at top)


    诶?很神奇!为什么会变成这个样子,我似乎还没找到什么规律,再把值改一下试试clip-path: circle(80% at top)


    clip-path: circle(80% at top)


    看样子圆心挪到了元素最上方的中间,然后以圆心到最下面边缘长度的80%为半径画了个圆进行了裁剪。至此我们似乎明白了circle()语法中at 后面的数据类型是干什么的了,大概就是用来控制裁剪时画的圆的圆心位置


    剩下的时间就交给你自己来一个一个试验所有的语法了,再举个简单的例子,比如你再试一下clip-path: circle(40% at 30px),你一定好奇这是啥意思,来看看效果


    clip-path: circle(40% at 30px)


    直观上看,整个圆向左移动了一些距离,在我们没设置at 30px时,圆心是在元素的中心的,而现在似乎向右偏移了,大胆猜测at 30px的意思是圆心的横坐标距离元素的最左侧30px


    接下来验证一下我们的猜测,继续修改其值clip-path: circle(40% at 0)


    clip-path: circle(40% at 0)


    很明显此时的圆心是在最左侧的中间部分,应该可以说是证明了我们刚才的猜测了,那么不妨再来验证一下纵坐标的?继续修改值clip-path: circle(40% at 0 0)


    clip-path: circle(40% at 0 0)


    不错,非常顺利,at 0 0中第二个0的意思就是圆心纵坐标离最上方的距离为0的意思。那么我们此时就可以放心得得出一个结论了,对于像30px33em这样的 数据类型的值,其对应的坐标是如图所示的


    坐标情况


    好了,本文篇幅也已经很长了,我就不继续介绍其它语法的使用了,刚才纯粹是用来举个例子,因为本文我们本来就不是在介绍circle()的使用教程,感兴趣的读者可以下去自己动手实践哦~


    所以实践真的很重要很重要!! MDN文档没有给你列举每种语法对应的效果,因为每种都列出来,文档看着就很杂乱了,所以这只能靠你自己。记得张鑫旭大佬在一次直播中讲到,他所掌握的CSS的特性,也都是用大量的时间去动手试出来的,也不是看看啥文档就能理解的,所以你在大佬们的一篇文章中了解到的某个CSS属性的使用,可能是他们花费几小时甚至十几个小时研究出来的。


    CSS很多特性会有兼容性问题,因为市面上有很多家浏览器厂商,它们支持的程度各不相同,而我们平常了解CSS某个属性的兼容性,是这样的


    查看MDN的某个属性的浏览器兼容性


    clip-path的浏览器兼容性


    通过Can I Use来查找某个属性的浏览器兼容性


    can i use


    这些都是正确的,但有时候可能某些CSS属性的浏览器兼容性都无法通过这两个渠道获取到,那么该怎么办呢?手动试试每个浏览器上该属性的效果是否支持呗(鑫旭大佬说他以前也会这么干),这点我就不举例子了,大家应该能体会到


    ☀️ 最后


    其实每个CSS大佬都不是因为某些快捷的学习路径而成功的,他们都是靠着不断地动手尝试、记录、总结各种CSS的知识,也会经常用学到的CSS知识去做一个小demo用于巩固,前几个月加了大漠老师的好友,我就经常看到他朋友圈有一些CSS新特性的demo演示代码和文章(真心佩服),coco大佬也是,也经常会发一些单纯用CSS实现的炫酷特效(据说没有他实现不了的特效哦~)


    另外,如果想要更加深入,你们还可以关注一下CSS的规范,这个比较权威的就是W3C的CSS Working Group了,里面有很多CSS的规范文档


    w3c css规范


    好了,再推荐几本业界公认的还算不错的书籍吧~例如《CSS权威指南》、《CSS揭秘》、《CSS世界》、《CSS新世界》等等...


    最后对于「如何学习CSS?」这个话题,你还有什么问题或者你觉得还不错的学习方法吗?欢迎在评论区留言讨论~



    链接:https://juejin.cn/post/6999418363239727111

    收起阅读 »

    WMS在Activity启动中的职责 添加窗体(三)

    Context 获取系统服务在正式聊WMS之前,我们先来看看context.getSystemService其核心原理,才能找到WindowManager的实现类: @Override public Object getSystemService...
    继续阅读 »

    Context 获取系统服务

    在正式聊WMS之前,我们先来看看context.getSystemService其核心原理,才能找到WindowManager的实现类:

        @Override
    public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
    }
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
    new HashMap<String, ServiceFetcher<?>>();

    public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
    }

    能看到是实际上所有的我们通过Context获取系统服务,是通过SYSTEM_SERVICE_FETCHERS这个提前存放在HashMap的服务集合中。这个服务是在静态代码域中提前注册。

            registerService(Context.WINDOW_SERVICE, WindowManager.class,
    new CachedServiceFetcher<WindowManager>() {
    @Override
    public WindowManager createService(ContextImpl ctx) {
    return new WindowManagerImpl(ctx);
    }});

    能看到此时实际上WindowManager的interface是由WindowManagerImpl实现的。

    这里先上一个WindowManager的UML类图。

    image.png

    我们能够从这个UML图能够看到,其实所有的事情都委托给WindowManagerGlobal工作。因此我们只需要看WindowManagerGlobal中做了什么。

    因此我们要寻求WindowManager的addView的方法,实际上就是看WindowManagerGlobal的addView方法。

    public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
    ...
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
    parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
    // If there's no parent, then hardware acceleration for this view is
    // set from the application's hardware acceleration setting.
    final Context context = view.getContext();
    if (context != null
    && (context.getApplicationInfo().flags
    & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
    wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
    }
    }

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
    // Start watching for system property changes.
    ...
    int index = findViewLocked(view, false);
    if (index >= 0) {
    if (mDyingViews.contains(view)) {
    // Don't wait for MSG_DIE to make it's way through root's queue.
    mRoots.get(index).doDie();
    } else {
    throw new IllegalStateException("View " + view
    + " has already been added to the window manager.");
    }
    // The previous removeView() had not completed executing. Now it has.
    }

    // If this is a panel window, then find the window it is being
    // attached to for future reference.
    if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
    final int count = mViews.size();
    for (int i = 0; i < count; i++) {
    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
    panelParentView = mViews.get(i);
    }
    }
    }

    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    try {
    root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
    // BadTokenException or InvalidDisplayException, clean up.
    if (index >= 0) {
    removeViewLocked(index, true);
    }
    throw e;
    }
    }
    }

    这里能够看到一个新的addView的时候,会找到是否有父Window。没有则继续往后走,判断新建窗体的type是否是子窗口类型,是则查找传进来的Binder对象和存储在缓存中的Binder对象又没有对应的Window。有则作为本次新建窗口的复窗口。

    最后能够看到我们熟悉的类ViewRootImpl。这个类可以说是所有View绘制的根部核心,这个类会在后面View绘制流程聊聊。最后会调用ViewRootImpl的setView进一步的沟通系统应用端。

    这里涉及到了几个有趣的宏,如WindowManager.LayoutParams.FIRST_SUB_WINDOW 。它们象征这当前Window处于什么层级。

    Window的层级

    Window的层级,我们大致可以分为3大类:System Window(系统窗口),Application Window(应用窗口),Sub Window(子窗口)

    Application Window(应用窗口)

    Application值得注意的有这么几个宏:

    type描述
    FIRST_APPLICATION_WINDOW = 1应用程序窗口初始值
    TYPE_BASE_APPLICATION = 1应用窗口类型初始值,其他窗口以此为基准
    TYPE_APPLICATION = 2普通应用程序窗口类型
    TYPE_APPLICATION_STARTING = 3应用程序的启动窗口类型,不是应用进程支配,当第一个应用进程诞生了启动窗口就会销毁
    TYPE_DRAWN_APPLICATION = 4应用显示前WindowManager会等待这种窗口类型绘制完毕,一般在多用户使用
    LAST_APPLICATION_WINDOW = 99应用窗口类型最大值

    因此此时我们能够清楚,应用窗口的范围在1~99之间。

    Sub Window(子窗口)

    type描述
    FIRST_SUB_WINDOW = 1000子窗口初始值
    TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW应用的panel窗口,在父窗口上显示
    TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1多媒体内容子窗口,在父窗口之下
    TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2也是一种panel子窗口,位于所有TYPE_APPLICATION_PANEL之上
    TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3dialog弹窗
    TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4多媒体内容窗口的覆盖层
    TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5位于子panel之上窗口
    LAST_SUB_WINDOW = 1999子窗口类型最大值

    能够看到子窗口的范围从1000~1999

    System Window(系统窗口)

    type描述
    FIRST_SYSTEM_WINDOW = 2000系统窗口初始值
    TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW系统状态栏
    TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1搜索条窗口
    TYPE_PHONE = FIRST_SYSTEM_WINDOW+2通话窗口
    TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3alert窗口,电量不足时警告
    TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4屏保窗口
    TYPE_TOAST = FIRST_SYSTEM_WINDOW+5Toast提示窗口
    TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6系统覆盖层窗口,这个层不会响应点击事件
    TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7电话优先层,在屏保状态下显示通话
    TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8系统层级的dialog,比如RecentAppDialog
    TYPE_KEYGUARD_DIALOG= FIRST_SYSTEM_WINDOW+9屏保时候对话框(如qq屏保时候的聊天框)
    TYPE_SYSTEM_ERROR= FIRST_SYSTEM_WINDOW+10系统错误窗口
    TYPE_INPUT_METHOD= FIRST_SYSTEM_WINDOW+11输入法窗口
    TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12输入法窗口上的对话框
    TYPE_WALLPAPER= FIRST_SYSTEM_WINDOW+13壁纸窗口
    TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14滑动状态栏窗口
    LAST_SYSTEM_WINDOW = 2999系统窗口最大值

    常见的系统级别窗口主要是这几个。能够注意到系统窗口层级是从2000~2999。

    这些层级有什么用的?这些层级会作为参考,将会插入到显示栈的位置,层级值越高,越靠近用户。这个逻辑之后会聊到。

    ViewRootImpl setView

    ViewRootImpl里面包含了许多事情,主要是包含了我们熟悉的View的绘制流程,以及添加Window实例的流程。

    本文是关于WMS,因此我们只需要看下面这个核心函数

    这个方法有两个核心requestLayout以及addToDisplay。

    • 1.requestLayout实际上就是指View的绘制流程,并且最终会把像素数据发送到Surface底层。
    • 2.mWindowSession.addToDisplay 添加Window实例到WMS中。

    WindowManager的Session设计思想

    先来看看Session类:

    class Session extends IWindowSession.Stub implements IBinder.DeathRecipient

    得知此时Session实现了一个IWindowSession的Binder对象。并且实现了Binder的死亡监听。

    那么这个Session是从哪里来的呢?实际上是通过WMS通过跨进程通信把数据这个Binder对象传递过来的:

        @Override
    public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
    IInputContext inputContext) {
    if (client == null) throw new IllegalArgumentException("null client");
    if (inputContext == null) throw new IllegalArgumentException("null inputContext");
    Session session = new Session(this, callback, client, inputContext);
    return session;
    }

    通着这种方式,就能把一个Session带上WMS相关的环境送给客户端操作。这种方式和什么很相似,实际上和servicemanager查询服务Binder的思路几乎一模一样。

    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
    int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
    Rect outStableInsets, Rect outOutsets,
    DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
    outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel);
    }

    很有趣的是,我们能够看到,按照道理我们需要添加窗体实例到WMS中。从逻辑上来讲,我们只需要做一次跨进程通信即可。但是为什么需要一个Session作为中转站呢?

    image.png

    能够看到实际上Session(会话)做的事情不仅仅只有沟通WMS这么简单。实际上它还同时处理了窗口上的拖拽,输入法等逻辑,更加重要的是Session面对着系统多个服务,但是通过这个封装,应用程序只需要面对这个Sesion接口,真的是名副其实的"会话"。

    这种设计想什么?实际上就是我们常说的门面设计模式。

    IWindow对象

    注意,这里面除了IWindowSession之外,当我们调用addWindow添加Window到WMS中的时候,其实还存在一个IWindow接口.这个IWindow是指PhoneWindow吗?

    很遗憾。并不是。PhoneWindow基础的接口只有Window接口。它并不是一个IBinder对象。我们转过头看看ViewRootImpl.

    public ViewRootImpl(Context context, Display display) {
    mContext = context;
    mWindowSession = WindowManagerGlobal.getWindowSession();
    mDisplay = display;
    mBasePackageName = context.getBasePackageName();
    mThread = Thread.currentThread();
    mLocation = new WindowLeaked(null);
    mLocation.fillInStackTrace();
    mWidth = -1;
    mHeight = -1;
    mDirty = new Rect();
    mTempRect = new Rect();
    mVisRect = new Rect();
    mWinFrame = new Rect();
    mWindow = new W(this);
    mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
    mViewVisibility = View.GONE;
    mTransparentRegion = new Region();
    mPreviousTransparentRegion = new Region();
    mFirst = true; // true for the first time the view is added
    mAdded = false;
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
    context);
    ...
    mViewConfiguration = ViewConfiguration.get(context);
    mDensity = context.getResources().getDisplayMetrics().densityDpi;
    mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
    mFallbackEventHandler = new PhoneFallbackEventHandler(context);
    mChoreographer = Choreographer.getInstance();
    mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);

    if (!sCompatibilityDone) {
    sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P;

    sCompatibilityDone = true;
    }

    loadSystemProperties();
    }

    能看到此时,实际上在ViewRootImpl的构造函数会对应当前生成一个W的内部类。这个内部类:

    static class W extends IWindow.Stub

    这个内部类实际上就是一个Binder类,里面回调了很多方法来操作当前的ViewRootImpl。换句话说,就是把当前的ViewRootImpl的代理W交给WMS去管理。

    那么我们可以总结,IWindow是WMS用来间接操作ViewRootImpl中的View,IWindowSession是App用来间接操作WMS。

    WMS.addWindow

    WMS的addWindow很长,因此我这边拆开成3部分聊

    添加窗体的准备步骤

    我们抛开大部分的校验逻辑。实际上可以把这个过程总结为以下几点:

    • 1.判断又没有相关的权限
    • 2.尝试着获取当前displayId对应的DisplayContent,没有则创建。其逻辑实际上和我上一篇说的创建DisplayContent一摸一样
    • 3.通过mWindowMap,判断当前IWindow是否被添加过,是的话说明已经存在这个Window,不需要继续添加
    • 4.如果当前窗口类型是子窗口,则会通过WindowToken.attrs参数中的token去查找当前窗口的父窗口是什么。
    • 5.如果有父窗口,则从DisplayContent中以父窗口的IWindow获取父窗口WindowToken的对象,否则尝试的获取当前窗口对应的WindowToken对象。

    我们稍微探索一下其中的几个核心:

    通过windowForClientLocked查找父窗口的WindowState

    final WindowState windowForClientLocked(Session session, IBinder client, boolean throwOnError) {
    WindowState win = mWindowMap.get(client);
    if (localLOGV) Slog.v(TAG_WM, "Looking up client " + client + ": " + win);
    if (win == null) {
    if (throwOnError) {
    throw new IllegalArgumentException(
    "Requested window " + client + " does not exist");
    }
    Slog.w(TAG_WM, "Failed looking up window callers=" + Debug.getCallers(3));
    return null;
    }
    if (session != null && win.mSession != session) {
    if (throwOnError) {
    throw new IllegalArgumentException("Requested window " + client + " is in session "
    + win.mSession + ", not " + session);
    }
    Slog.w(TAG_WM, "Failed looking up window callers=" + Debug.getCallers(3));
    return null;
    }

    return win;
    }

    实际上可以看到这里面是从mWindowMap通过IWindow获取WindowState对象。还记得我上篇说过很重要的数据结构吗?mWindowMap实际上是保存着WMS中IWindow对应WindowState对象。IWindow本质上是WMS控制ViewRootImpl的Binder接口。因此我们可以把WindowState看成应用进程的对应的对象也未尝不可。

    获取对应的WindowToken

                AppWindowToken atoken = null;
    final boolean hasParent = parentWindow != null;
    //从DisplayContent找到对应的WIndowToken
    WindowToken token = displayContent.getWindowToken(
    hasParent ? parentWindow.mAttrs.token : attrs.token);

    从这里面我们能够看到WindowToken,是通过DisplayContent获取到的。

    WindowToken getWindowToken(IBinder binder) {
    return mTokenMap.get(binder);
    }

    这样就能看到我前两篇提到过的很重要的数据结构:mTokenMap以及mWindowMap。这两者要稍微区分一下:
    mWindowMap是以IWindow为key,WindowState为value。
    mTokenMap是以WindowState的IBinder(一般为IApplicationToken)为key,WindowToken为value

    还记得mTokenMap在Activity的启动流程中做的事情吗?在创建AppWIndowContainer的时候,会同时创建AppWindowToken,AppWIndowToken的构造会把当前的IBinder作为key,AppWindowToken作为value添加到mTokenMap中。

    也就是说,如果系统想要通过应用进程给的IWindow找到真正位于WMS中Window的句柄,必须通过这两层变换才能真正找到。

    拆分情况获取对应的WindowToken和AppWindowToken

    这个时候就分为两种情况,一种是存在WindowToken,一种是不存在WindowToken。

                boolean addToastWindowRequiresToken = false;

    if (token == null) {
    //校验窗口参数是否合法
    ...

    final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
    final boolean isRoundedCornerOverlay =
    (attrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0;
    token = new WindowToken(this, binder, type, false, displayContent,
    session.mCanAddInternalSystemWindow, isRoundedCornerOverlay);
    } else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
    atoken = token.asAppWindowToken();
    if (atoken == null) {
    return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
    }
    ...
    } else if (atoken.removed) {
    ...
    } else if (type == TYPE_APPLICATION_STARTING && atoken.startingWindow != null) {
    ...

    }
    } else if (rootType == TYPE_INPUT_METHOD) {
    ...

    } else if (rootType == TYPE_VOICE_INTERACTION) {
    ...
    } else if (rootType == TYPE_WALLPAPER) {
    ...
    } else if (rootType == TYPE_DREAM) {
    ...
    } else if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
    ...
    } else if (type == TYPE_TOAST) {
    ....
    } else if (type == TYPE_QS_DIALOG) {
    ...
    } else if (token.asAppWindowToken() != null) {

    attrs.token = null;
    token = new WindowToken(this, client.asBinder(), type, false, displayContent,
    session.mCanAddInternalSystemWindow);
    }

    当我们通过mTokenMap获取WindowToken的时候,大致分为四种情况。WindowToken会尝试的获取父窗口对应的Token,找不到则使用WindowManager.LayoutParams中的WindowToken。一般来说我们找到的都有父亲的WindowToken。

    • 1.无关应用的找不到WindowToken
    • 2.有关应用找不到WindowToken。
    • 3.无关应用找到WindowToken
    • 4.有关应用找到WindowToken

    前两种情况解析

    实际上前两种情况,一旦发现找不到WindowToken,如果当前的窗口和应用相关的,就一定爆错误。如Toast,输入法,应用窗口等等。

    因此在Android 8.0开始,当我们想要显示Toast的时候,加入传入的Context是Application而不是Activity,此时一旦发现mTokenMap中找不到IApplicationToken对应的WindowToken就爆出了错误。正确的做法应该是需要获取Activity当前的Context。

    在上面的情况应用启动窗口,此时并没有启动Activity。因此不可能会被校验拦下,因此并没有异常抛出。就会自己创建一个WindowToken。

    后两种的解析

    当找到WindowToken,一般是指Activity启动之后,在AppWindowToken初始化后,自动加入了mTokenMap中。此时的情况稍微复杂了点。

    当是子窗口的时候,则会判断当前的WindowToken是不是AppWindowToken。不是,或者被移除等异常情况则报错。

    如果是壁纸,输入法,系统弹窗,toast等窗口模式,子窗口和父窗口的模式必须一致。

    当此时的AppWindowToken不为空的时候,说明在New的时候已经生成,且没有移除,将会生成一个新的WindowToken。

    为什么要生成一个新的windowToken?可以翻阅之前我写的文章,只要每一次调用一次构造函数将会把当前的WindowToken添加到mTokenMap中,实际上也是担心,对应的AppWindowToken出现的重新绑定的问题。

    添加WindowState实例到数据结构

    但是别忘了,我们这个时候还需要把相关的数据结构存储到全局。

                final WindowState win = new WindowState(this, session, client, token, parentWindow,
    appOp[0], seq, attrs, viewVisibility, session.mUid,
    session.mCanAddInternalSystemWindow);
    if (win.mDeathRecipient == null) {
    ...
    return WindowManagerGlobal.ADD_APP_EXITING;
    }

    if (win.getDisplayContent() == null) {
    ...
    return WindowManagerGlobal.ADD_INVALID_DISPLAY;
    }

    final boolean hasStatusBarServicePermission =
    mContext.checkCallingOrSelfPermission(permission.STATUS_BAR_SERVICE)
    == PackageManager.PERMISSION_GRANTED;
    mPolicy.adjustWindowParamsLw(win, win.mAttrs, hasStatusBarServicePermission);
    win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));

    res = mPolicy.prepareAddWindowLw(win, attrs);
    if (res != WindowManagerGlobal.ADD_OKAY) {
    return res;
    }
    // From now on, no exceptions or errors allowed!

    res = WindowManagerGlobal.ADD_OKAY;
    if (mCurrentFocus == null) {
    mWinAddedSinceNullFocus.add(win);
    }

    if (excludeWindowTypeFromTapOutTask(type)) {
    displayContent.mTapExcludedWindows.add(win);
    }

    origId = Binder.clearCallingIdentity();

    win.attach();
    //以IWindow为key,WindowState为value存放到WindowMap中
    mWindowMap.put(client.asBinder(), win);

    win.initAppOpsState();

    ....
    win.mToken.addWindow(win);

    因为完全可能出现新的WindowToken,因此干脆会创建一个新的WindowState。此时会对调用WindowState.attach方法

        void attach() {
    mSession.windowAddedLocked(mAttrs.packageName);
    }

    这方法挺重要的,Session做了一次添加锁定。

    void windowAddedLocked(String packageName) {
    mPackageName = packageName;
    mRelayoutTag = "relayoutWindow: " + mPackageName;
    if (mSurfaceSession == null) {
    if (WindowManagerService.localLOGV) Slog.v(
    TAG_WM, "First window added to " + this + ", creating SurfaceSession");
    mSurfaceSession = new SurfaceSession();
    if (SHOW_TRANSACTIONS) Slog.i(
    TAG_WM, " NEW SURFACE SESSION " + mSurfaceSession);
    mService.mSessions.add(this);
    if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
    mService.dispatchNewAnimatorScaleLocked(this);
    }
    }
    mNumWindow++;
    }

    此时的工作是什么?联系上下文,当我们新增了PhoneWindow,就会一个ViewRootImpl,也因此新增了Session。此时说明诞生一个新界面,此时已经诞生了相关的容器对象,但是相关的绘制到底层对象还没有创建出来。

    命名逻辑和Session很相似。Session是WMS给应用App的会话对象,SurfaceSession是SurfaceFlinger面向上层每一个WIndow需要绘制内容对象。

    这个SurfaceSession和SurfaceControl都是重点,联通到SurfaceFlinger很重要的对象。

    最后再添加到mWindowMap中。并且把WindowState添加到WindowToken中,让每一个WindowToken赋予状态的信息。我们稍微探索一下addWindow的方法。


    收起阅读 »

    Android自定义view之3D正方体

    前言在之前写了一篇关于3D效果的文章,借助传感器展示,有小伙伴问可不可以改成手势滑动操作(事件分发),所以出一篇文章传感器相关文章链接:Android 3D效果的实现一、小提相对于常见的自定义view而言,继承的GLSurfaceView只有两个构造函数。可以...
    继续阅读 »

    前言

    在之前写了一篇关于3D效果的文章,借助传感器展示,有小伙伴问可不可以改成手势滑动操作(事件分发),所以出一篇文章


    传感器相关文章链接:Android 3D效果的实现

    一、小提

    相对于常见的自定义view而言,继承的GLSurfaceView只有两个构造函数。可以理解为没有提供获取自定义属性的方法。

        public TouchSurfaceView(Context context) {
    super(context);
    }

    public TouchSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    二、将传感器改成事件分发机制

        @Override
    public boolean onTouchEvent(MotionEvent e) {
    float x = e.getX();
    float y = e.getY();
    switch (e.getAction()) {
    case MotionEvent.ACTION_MOVE:
    float dx = x - mPreviousX;
    float dy = y - mPreviousY;
    mRenderer.mAngleX += dx * TOUCH_SCALE_FACTOR;
    mRenderer.mAngleY += dy * TOUCH_SCALE_FACTOR;
    requestRender();
    }
    mPreviousX = x;
    mPreviousY = y;
    return true;
    }

    要注意还有一个滚动球事件

        @Override
    public boolean onTrackballEvent(MotionEvent e) {
    mRenderer.mAngleX += e.getX() * TRACKBALL_SCALE_FACTOR;
    mRenderer.mAngleY += e.getY() * TRACKBALL_SCALE_FACTOR;
    requestRender();
    return true;
    }

    三、在Activity中使用

      mGLSurfaceView = new TouchSurfaceView(this);
    setContentView(mGLSurfaceView);
    mGLSurfaceView.requestFocus();
    mGLSurfaceView.setFocusableInTouchMode(true);

    注意要在对应生命周期中处理

        @Override
    protected void onResume() {
    super.onResume();
    mGLSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
    super.onPause();
    mGLSurfaceView.onPause();
    }

    四、源码

    TouchSurfaceView.java

    除去前面的修改部分,其他大多与链接文章相同,仅将传感器改成了事件分发。(代码中难点有注释)

    public class TouchSurfaceView extends GLSurfaceView {
    private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
    private final float TRACKBALL_SCALE_FACTOR = 36.0f;
    private CubeRenderer mRenderer;
    private float mPreviousX;
    private float mPreviousY;

    public TouchSurfaceView(Context context) {
    super(context);
    mRenderer = new CubeRenderer();
    setRenderer(mRenderer);
    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }

    public TouchSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }


    @Override
    public boolean onTrackballEvent(MotionEvent e) {
    mRenderer.mAngleX += e.getX() * TRACKBALL_SCALE_FACTOR;
    mRenderer.mAngleY += e.getY() * TRACKBALL_SCALE_FACTOR;
    requestRender();
    return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
    float x = e.getX();
    float y = e.getY();
    switch (e.getAction()) {
    case MotionEvent.ACTION_MOVE:
    float dx = x - mPreviousX;
    float dy = y - mPreviousY;
    mRenderer.mAngleX += dx * TOUCH_SCALE_FACTOR;
    mRenderer.mAngleY += dy * TOUCH_SCALE_FACTOR;
    requestRender();
    }
    mPreviousX = x;
    mPreviousY = y;
    return true;
    }


    private class CubeRenderer implements GLSurfaceView.Renderer {

    private Cube mCube;
    public float mAngleX;
    public float mAngleY;
    public CubeRenderer() {
    mCube =new Cube();
    }

    public void onDrawFrame(GL10 gl) {
    // | GL10.GL_DEPTH_BUFFER_BIT
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glTranslatef(0, 0, -3.0f);
    gl.glRotatef(mAngleX, 0, 1, 0);
    gl.glRotatef(mAngleY, 1, 0, 0);
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
    mCube.draw(gl);
    }


    @Override
    public void onSurfaceCreated(GL10 gl, javax.microedition.khronos.egl.EGLConfig config) {
    gl.glDisable(GL10.GL_DITHER);
    gl.glClearColor(1,1,1,1);
    }

    public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);
    //设置投影矩阵。但并不需要在每次绘制时都做,通常情况下,当视图调整大小时,需要设置一个新的投影。
    float ratio = (float) width / height;
    gl.glMatrixMode(GL10.GL_PROJECTION);
    gl.glLoadIdentity();
    gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
    }

    }



    public class Cube {
    //opengl坐标系中采用的是3维坐标:
    private FloatBuffer mVertexBuffer;
    private FloatBuffer mColorBuffer;
    private ByteBuffer mIndexBuffer;

    public Cube() {
    final float vertices[] = {
    -1, -1, -1, 1, -1, -1,
    1, 1, -1, -1, 1, -1,
    -1, -1, 1, 1, -1, 1,
    1, 1, 1, -1, 1, 1,
    };

    final float colors[] = {
    0, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 0, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 0, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 0, 1,
    };

    final byte indices[] = {
    0, 4, 5, 0, 5, 1,
    1, 5, 6, 1, 6, 2,
    2, 6, 7, 2, 7, 3,
    3, 7, 4, 3, 4, 0,
    4, 7, 6, 4, 6, 5,
    3, 0, 1, 3, 1, 2
    };

    ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
    vbb.order(ByteOrder.nativeOrder());
    mVertexBuffer = vbb.asFloatBuffer();
    mVertexBuffer.put(vertices);
    mVertexBuffer.position(0);

    ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4);
    cbb.order(ByteOrder.nativeOrder());
    mColorBuffer = cbb.asFloatBuffer();
    mColorBuffer.put(colors);
    mColorBuffer.position(0);

    mIndexBuffer = ByteBuffer.allocateDirect(indices.length);
    mIndexBuffer.put(indices);
    mIndexBuffer.position(0);
    }

    public void draw(GL10 gl) {
    //启用服务器端GL功能。
    gl.glEnable(GL10.GL_CULL_FACE);
    //定义多边形的正面和背面。
    //参数:
    //mode——多边形正面的方向。GL_CW和GL_CCW被允许,初始值为GL_CCW。
    gl.glFrontFace(GL10.GL_CW);
    //选择恒定或光滑着色模式。
    //GL图元可以采用恒定或者光滑着色模式,默认值为光滑着色模式。当图元进行光栅化的时候,将引起插入顶点颜色计算,不同颜色将被均匀分布到各个像素片段。
    //参数:
    //mode——指明一个符号常量来代表要使用的着色技术。允许的值有GL_FLAT 和GL_SMOOTH,初始值为GL_SMOOTH。
    gl.glShadeModel(GL10.GL_SMOOTH);
    //定义一个顶点坐标矩阵。
    //参数:
    //
    //size——每个顶点的坐标维数,必须是2, 3或者4,初始值是4。
    //
    //type——指明每个顶点坐标的数据类型,允许的符号常量有GL_BYTE, GL_SHORT, GL_FIXED和GL_FLOAT,初始值为GL_FLOAT。
    //
    //stride——指明连续顶点间的位偏移,如果为0,顶点被认为是紧密压入矩阵,初始值为0。
    //
    //pointer——指明顶点坐标的缓冲区,如果为null,则没有设置缓冲区。
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
    //定义一个颜色矩阵。
    //size指明每个颜色的元素数量,必须为4。type指明每个颜色元素的数据类型,stride指明从一个颜色到下一个允许的顶点的字节增幅,并且属性值被挤入简单矩阵或存储在单独的矩阵中(简单矩阵存储可能在一些版本中更有效率)。
    gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
    //由矩阵数据渲染图元
    //可以事先指明独立的顶点、法线、颜色和纹理坐标矩阵并且可以通过调用glDrawElements方法来使用它们创建序列图元。
    gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE, mIndexBuffer);
    }
    }
    }

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
    private GLSurfaceView mGLSurfaceView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mGLSurfaceView = new TouchSurfaceView(this);
    setContentView(mGLSurfaceView);
    mGLSurfaceView.requestFocus();
    mGLSurfaceView.setFocusableInTouchMode(true);
    }

    @Override
    protected void onResume() {
    super.onResume();
    mGLSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
    super.onPause();
    mGLSurfaceView.onPause();
    }


    }

    总结

    收起阅读 »

    内存管理(MRC、ARC)

    一、 什么是内存管理程序在运行的过程中通常通过以下行为,来增加程序的的内存占用创建一个OC对象定义一个变量调用一个函数或者方法而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再...
    继续阅读 »

    一、 什么是内存管理

    • 程序在运行的过程中通常通过以下行为,来增加程序的的内存占用
      • 创建一个OC对象
      • 定义一个变量
      • 调用一个函数或者方法
    • 而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的
    • 当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要使用的对象、变量等
    • 如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验

    所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。

    那么,那些对象才需要我们进行内存管理呢?

    • 任何继承了NSObject的对象需要进行内存管理
    • 而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理

    这是因为

    • 继承了NSObject的对象的存储在操作系统的里边。
    • 操作系统的:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
    • 非OC对象一般放在操作系统的里面
    • 操作系统的:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)
    • 示例:
    int main(int argc, const char * argv[])
    {
    @autoreleasepool {
    int a = 10; // 栈
    int b = 20; // 栈
    // p : 栈
    // Person对象(计数器==1) : 堆
    Person *p = [[Person alloc] init];
    }
    // 经过上面代码后, 栈里面的变量a、b、p 都会被回收
    // 但是堆里面的Person对象还会留在内存中,因为它是计数器依然是1
    return 0;
    }




    二、 内存管理模型

    提供给Objective-C程序员的基本内存管理模型有以下3种:

    • 自动垃圾收集(iOS运行环境不支持)
    • 手工引用计数和自动释放池(MRC)
    • 自动引用计数(ARC)

    三、MRC 手动管理内存(Manual Reference Counting)

    1. 引用计数器

    系统是根据对象的引用计数器来判断什么时候需要回收一个对象所占用的内存

    • 引用计数器是一个整数
    • 从字面上, 可以理解为”对象被引用的次数”
    • 也可以理解为: 它表示有多少人正在用这个对象
    • 每个OC对象都有自己的引用计数器
    • 任何一个对象,刚创建的时候,初始的引用计数为1
      • 当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1
    • 当没有任何人使用这个对象时,系统才会回收这个对象, 也就是说
      • 当对象的引用计数器为0时,对象占用的内存就会被系统回收
      • 如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出 )

    2. 引用计数器操作

    • 为保证对象的存在,每当创建引用到对象需要给对象发送一条retain消息,可以使引用计数器值+1 ( retain 方法返回对象本身)
    • 当不再需要对象时,通过给对象发送一条release消息,可以使引用计数器值-1
    • 给对象发送retainCount消息,可以获得当前的引用计数器值
    • 当对象的引用计数为0时,系统就知道这个对象不再需要使用了,所以可以释放它的内存,通过给对象发送dealloc消息发起这个过程。
    • 需要注意的是:release并不代表销毁\回收对象,仅仅是计数器-1

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 只要创建一个对象默认引用计数器的值就是1
    Person *p = [[Person alloc] init];
    NSLog(@"retainCount = %lu", [p retainCount]); // 1

    // 只要给对象发送一个retain消息, 对象的引用计数器就会+1
    [p retain];

    NSLog(@"retainCount = %lu", [p retainCount]); // 2
    // 通过指针变量p,给p指向的对象发送一条release消息
    // 只要对象接收到release消息, 引用计数器就会-1
    // 只要一个对象的引用计数器为0, 系统就会释放对象

    [p release];
    // 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数器-1
    NSLog(@"retainCount = %lu", [p retainCount]); // 1

    [p release]; // 0
    NSLog(@"--------");
    }
    // [p setAge:20]; // 此时对象已经被释放
    return 0;
    }

    3. dealloc方法

    • 当一个对象的引用计数器值为0时,这个对象即将被销毁,其占用的内存被系统回收
    • 对象即将被销毁时系统会自动给对象发送一条dealloc消息(因此,从dealloc方法有没有被调用,就可以判断出对象是否被销毁)
    • dealloc方法的重写
      • 一般会重写dealloc方法,在这里释放相关资源,dealloc就是对象的遗言
      • 一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用

    - (void)dealloc
    {
    NSLog(@"Person dealloc");
    // 注意:super dealloc一定要写到所有代码的最后
    // 一定要写在dealloc方法的最后面
    [super dealloc];
    }

    • 使用注意
      • 不能直接调用dealloc方法
      • 一旦对象被回收了, 它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)

    4. 野指针和空指针

    • 只要一个对象被释放了,我们就称这个对象为 “僵尸对象(不能再使用的对象)”
    • 当一个指针指向一个僵尸对象(不可用内存),我们就称这个指针为野指针
    • 只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS错误)
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init]; // 执行完引用计数为1

    [p release]; // 执行完引用计数为0,实例对象被释放
    [p release]; // 此时,p就变成了野指针,再给野指针p发送消息就会报错
    [p release];
    }
    return 0;
    }
    • 为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针
    • 空指针
      • 没有指向存储空间的指针(里面存的是nil, 也就是0)
      • 给空指针发消息是没有任何反应的

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init]; // 执行完引用计数为1

    [p release]; // 执行完引用计数为0,实例对象被释放
    p = nil; // 此时,p变为了空指针
    [p release]; // 再给空指针p发送消息就不会报错了
    [p release];
    }
    return 0;
    }

    5. 内存管理规律

    单个对象内存管理规律
    • 谁创建谁release :
      • 如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么你必须调用release或autorelease
    • 谁retain谁release:
      • 只要你调用了retain,就必须调用一次release
    • 总结一下就是
      • 有加就有减
      • 曾经让对象的计数器+1,就必须在最后让对象计数器-1
    多个对象内存管理规律

    因为多个对象之间往往是联系的,所以管理起来比较复杂。这里用一个玩游戏例子来类比一下。

    游戏可以提供给玩家(A类对象) 游戏房间(B类对象)来玩游戏。

    • 只要一个玩家想使用房间(进入房间),就需要对这个房间的引用计数器+1
    • 只要一个玩家不想再使用房间(离开房间),就需要对这个房间的引用计数器-1
    • 只要还有至少一个玩家在用某个房间,那么这个房间就不会被回收,引用计数至少为1




    下面来定义两个类 玩家类:Person 和 房间类:Room

    房间类:Room,房间类中有房间号

    #import <Foundation/Foundation.h>

    @interface Room : NSObject
    @property int no; // 房间号
    @end

    玩家类:Person

    #import <Foundation/Foundation.h>
    #import "Room.h"

    @interface Person : NSObject
    {
    Room *_room;
    }

    - (void)setRoom:(Room *)room;

    - (Room *)room;
    @end

    现在我们通过几个玩家使用房间的不同应用场景来逐步深入理解内存管理。

    1. 玩家没有使用房间,玩家和房间之间没有联系的情况
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    [r release]; // 释放房间
    [p release]; // 释放玩家
    }
    return 0;
    }

    上述代码执行完前3行

    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    之后在内存中的表现如下图所示:




    可见,Room实例对象和Person实例对象之间没有相互联系,所以各自释放不会报错。执行完4、5行代码

    [r release];    // 释放房间      
    [p release]; // 释放玩家

    后,将房间对象和玩家对象各自释放掉,在内存中的表现如下图所示:



    最后各自实例对象的内存就会被系统回收

    2. 一个玩家使用一个游戏房间,玩家和房间之间相关联的情况
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    // 将房间赋值给玩家,表示玩家在使用房间
    // 玩家需要使用这间房,只要玩家在,房间就一定要在
    p.room = r; // [p setRoom:r]

    [r release]; // 释放房间

    // 在这行代码之前,玩家都没有被释放,但是因为玩家还在,那么房间就不能销毁
    NSLog(@"-----");

    [p release]; // 释放玩家
    }
    return 0;
    }

    上边代码执行完前3行的时候和之前在内存中的表现一样,如图


    当执行完第4行代码p.room = r;时,因为调用了setter方法,将Room实例对象赋值给了Person的成员变量,不做其他设置的话,在内存中的表现如下图(做法不对):



    在调用setter方法的时候,因为Room实例对象多了一个Person对象引用,所以应将Room实例对象的引用计数+1才对,即setter方法应该像下边一样,对room进行一次retain操作。

    - (void)setRoom:(Room *)room // room = r
    {
    // 对房间的引用计数器+1
    [room retain];
    _room = room;
    }

    那么执行完第4行代码p.room = r;,在内存中的表现为:




    继续执行第5行代码[r release];,释放房间,Room实例对象引用计数-1,在内存中的表现如下图所示:



    然后执行第6行代码[p release];,释放玩家。这时候因为玩家不在房间里了,房间也没有用了,所以在释放玩家的时候,要把房间也释放掉,也就是在delloc里边对房间再进行一次release操作。

    这样对房间对象来说,每一次retain/alloc操作都对应一次release操作。

    - (void)dealloc
    {
    // 人释放了, 那么房间也需要释放
    [_room release];
    NSLog(@"%s", __func__);

    [super dealloc];
    }

    那么在内存中的表现最终如下图所示:



    最后实例对象的内存就会被系统回收

    3. 一个玩家使用一个游戏房间R后,换到另一个游戏房间R2,玩家和房间相关联的情况
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    // 2.将房间赋值给玩家,表示玩家在使用房间
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    // 3. 换房
    Room *r2 = [[Room alloc] init];
    r2.no = 444;
    p.room = r2;
    [r2 release]; // 释放房间 r2

    [p release]; // 释放玩家 p
    }
    return 0;
    }

    执行下边几行代码

    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    // 2.将房间赋值给玩家,表示玩家在使用房间
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    之后的内存表现为:



    接着执行换房操作而不进行其他操作的话,

    // 3. 换房
    Room *r2 = [[Room alloc] init];
    r2.no = 444;
    p.room = r2;

    内存的表现为:



    最后执行完

    [r2 release];    // 释放房间 r2
    [p release]; // 释放玩家 p

    内存的表现为:




    可以看出房间 r 并没有被释放,这是因为在进行换房的时候,并没有对房间 r 进行释放。所以应在调用setter方法的时候,对之前的变量进行一次release操作。具体setter方法代码如下:

    - (void)setRoom:(Room *)room // room = r
    {
    // 将以前的房间释放掉 -1
    [_room release];

    // 对房间的引用计数器+1
    [room retain];

    _room = room;
    }
    }

    这样在执行完p.room = r2;之后就会将 房间 r 释放掉,最终内存表现为:




    4. 一个玩家使用一个游戏房间,不再使用游戏房间,将游戏房间释放掉之后,再次使用该游戏房间的情况

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init];
    Room *r = [[Room alloc] init];
    r.no = 888;

    // 2.将房间赋值给人
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    // 3.再次使用房间 r
    p.room = r;
    [r release]; // 释放房间 r
    [p release]; // 释放玩家 p
    }
    return 0;
    }

    执行下面代码

    // 1.创建两个对象
    Person *p = [[Person alloc] init];
    Room *r = [[Room alloc] init];
    r.no = 888;

    // 2.将房间赋值给人
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    之后的内存表现为:



    然后再执行p.room = r;,因为setter方法会将之前的Room实例对象先release掉,此时内存表现为:




    此时_room、r 已经变成了一个野指针。之后再对野指针 r 发出retain消息,程序就会崩溃。所以我们在进行setter方法的时候,要先判断一下是否是重复赋值,如果是同一个实例对象,就不需要重复进行release和retain。换句话说,如果我们使用的还是之前的房间,那换房的时候就不需要对这个房间再进行release和retain。则setter方法具体代码如下:

    - (void)setRoom:(Room *)room // room = r
    {
    // 只有房间不同才需用release和retain
    if (_room != room) { // 0ffe1 != 0ffe1
    // 将以前的房间释放掉 -1
    [_room release];

    // 对房间的引用计数器+1
    [room retain];

    _room = room;
    }
    }

    因为retain不仅仅会对引用计数器+1, 而且还会返回当前对象,所以上述代码可最终简化成:

    - (void)setRoom:(Room *)room // room = r
    {
    // 只有房间不同才需用release和retain
    if (_room != room) { // 0ffe1 != 0ffe1
    // 将以前的房间释放掉 -1
    [_room release];

    _room = [room retain];
    }
    }

    以上就是setter方法的最终形式。

    6. @property参数

    • 在成员变量前加上@property,系统就会自动帮我们生成基本的setter/getter方法
    @property (nonatomic) int val;
    • 如果在property后边加上retain,系统就会自动帮我们生成getter/setter方法内存管理的代码,但是仍需要我们自己重写dealloc方法
    @property(nonatomic, retain) Room *room;
    • 如果在property后边加上assign,系统就不会帮我们生成set方法内存管理的代码,仅仅只会生成普通的getter/setter方法,默认什么都不写就是assign
    @property(nonatomic, retain) int val;

    7. 自动释放池

    当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C提供了autorelease方法。

    • autorelease是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release操作

      注意,这里只是发送release消息,如果当时的引用计数(reference-counted)依然不为0,则该对象依然不会被释放。

    • autorelease方法会返回对象本身,且调用完autorelease方法后,对象的计数器不变

    Person *p = [Person new];
    p = [p autorelease];
    NSLog(@"count = %lu", [p retainCount]); // 计数还为1
    1. 使用AUTORELEASE有什么好处呢
    • 不用再关心对象释放的时间
    • 不用再关心什么时候调用release
    2. AUTORELEASE的原理实质上是什么?

    autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release。

    3. AUTORELEASE的创建方法
    1. 使用NSAutoreleasePool来创建
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
    [pool release]; // [pool drain]; 销毁自动释放池
    1. 使用@autoreleasepool创建
    @autoreleasepool
    { //开始代表创建自动释放池

    } //结束代表销毁自动释放池
    4. AUTORELEASE的使用方法

    NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
    Person *p = [[[Person alloc] init] autorelease];
    [autoreleasePool drain];
    @autoreleasepool
    { // 创建一个自动释放池
    Person *p = [[Person new] autorelease];
    // 将代码写到这里就放入了自动释放池
    } // 销毁自动释放池(会给池子中所有对象发送一条release消息)
    5. AUTORELEASE的注意事项
    • 并不是放到自动释放池代码中,都会自动加入到自动释放池
    @autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
    }
    • 在自动释放池的外部发送autorelease 不会被加入到自动释放池中
      • autorelease是一个方法,只有在自动释 放池中调用才有效。
    @autoreleasepool {
    }
    // 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
    Person *p = [[[Person alloc] init] autorelease];
    [p run];

    // 正确写法
    @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    }

    // 正确写法
    Person *p = [[Person alloc] init];
    @autoreleasepool {
    [p autorelease];
    }

    6. 自动释放池的嵌套使用
    • 自动释放池是以栈的形式存在

    • 由于栈只有一个入口, 所以调用autorelease会将对象放到栈顶的自动释放池

      栈顶就是离调用autorelease方法最近的自动释放池


    @autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
    @autoreleasepool { // 栈顶自动释放池
    Person *p = [[[Person alloc] init] autorelease];
    }
    Person *p = [[[Person alloc] init] autorelease];
    }
    }
    • 自动释放池中不适宜放占用内存比较大的对象
      • 尽量避免对大内存使用该方法,对于这种延迟释放机制,还是尽量少用
      • 不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升
    // 内存暴涨
    @autoreleasepool {
    for (int i = 0; i < 99999; ++i) {
    Person *p = [[[Person alloc] init] autorelease];
    }
    }

    // 内存不会暴涨
    for (int i = 0; i < 99999; ++i) {
    @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    }
    }

    7. AUTORELEASE错误用法
    • 不要连续调用autorelease
    @autoreleasepool {
    // 错误写法, 过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
    }

    • 调用autorelease后又调用release(错误)
    @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 错误写法, 过度释放
    }

    8. MRC中避免循环retain

    定义两个类Person类和Dog类

    • Person类:
    #import <Foundation/Foundation.h>
    @class Dog;

    @interface Person : NSObject
    @property(nonatomic, retain)Dog *dog;
    @end
    • Dog类:
    #import <Foundation/Foundation.h>
    @class Person;

    @interface Dog : NSObject
    @property(nonatomic, retain)Person *owner;
    @end

    执行以下代码:

    int main(int argc, const char * argv[]) {
    Person *p = [Person new];
    Dog *d = [Dog new];

    p.dog = d; // retain
    d.owner = p; // retain assign

    [p release];
    [d release];

    return 0;
    }

    就会出现A对象要拥有B对象,而B对应又要拥有A对象,此时会形成循环retain,导致A对象和B对象永远无法释放

    那么如何解决这个问题呢?

    • 不要让A retain B,B retain A
    • 让其中一方不要做retain操作即可
    • 当两端互相引用时,应该一端用retain,一端用assign

    四、ARC 自动管理内存(Automatic Reference Counting)

    • Automatic Reference Counting,自动引用计数,即ARC,WWDC2011和iOS5所引入的最大的变革和最激动人心的变化。ARC是新的LLVM 3.0编译器的一项特性,使用ARC,可以说一 举解决了广大iOS开发者所憎恨的手动内存管理的麻烦。
    • 使用ARC后,系统会检测出何时需要保持对象,何时需要自动释放对象,何时需要释放对象,编译器会管理好对象的内存,会在何时的地方插入retain, release和autorelease,通过生成正确的代码去自动释放或者保持对象。我们完全不用担心编译器会出错

    1\ ARC的判断原则

    ARC判断一个对象是否需要释放不是通过引用计数来进行判断的,而是通过强指针来进行判断的。那么什么是强指针?

    • 强指针
      • 默认所有对象的指针变量都是强指针
      • 被__strong修饰的指针

    Person *p1 = [[Person alloc] init];
    __strong Person *p2 = [[Person alloc] init];
    • 弱指针
      • 被__weak修饰的指针
    __weak  Person *p = [[Person alloc] init];

    ARC如何通过强指针来判断?

    • 只要还有一个强指针变量指向对象,对象就会保持在内存中

    2. ARC的使用

    int main(int argc, const char * argv[]) {
    // 不用写release, main函数执行完毕后p会被自动释放
    Person *p = [[Person alloc] init];

    return 0;
    }

    3. ARC的注意点

    • 不允许调用对象的 release方法
    • 不允许调用 autorelease方法
    • 重写父类的dealloc方法时,不能再调用 [super dealloc];

    4. ARC下单对象内存管理

    • 局部变量释放对象随之被释放
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init];
    } // 执行到这一行局部变量p释放
    // 由于没有强指针指向对象, 所以对象也释放
    return 0;
    }

    • 清空指针对象随之被释放
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init];
    p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放
    }
    return 0;
    }
    • 默认清空所有指针都是强指针
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // p1和p2都是强指针
    Person *p1 = [[Person alloc] init];
    __strong Person *p2 = [[Person alloc] init];
    }
    return 0;
    }

    • 弱指针需要明确说明
      • 注意: 千万不要使用弱指针保存新创建的对象
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // p是弱指针, 对象会被立即释放
    __weak Person *p1 = [[Person alloc] init];
    }
    return 0;
    }

    5. ARC下多对象内存管理

    • ARC和MRC一样, 想拥有某个对象必须用强指针保存对象, 但是不需要在dealloc方法中release
    @interface Person : NSObject
    // MRC写法
    //@property (nonatomic, retain) Dog *dog;

    // ARC写法
    @property (nonatomic, strong) Dog *dog;
    @end

    6. ARC下@property参数

    • strong : 用于OC对象,相当于MRC中的retain
    • weak : 用于OC对象,相当于MRC中的assign
    • assign : 用于基本数据类型,跟MRC中的assign一样

    7. ARC下循环引用问题

    • ARC和MRC一样,如果A拥有B,B也拥有A,那么必须一方使用弱指针

    @interface Person : NSObject
    @property (nonatomic, strong) Dog *dog;
    @end

    @interface Dog : NSObject
    // 错误写法, 循环引用会导致内存泄露
    //@property (nonatomic, strong) Person *owner;

    // 正确写法, 当如果保存对象建议使用weak
    @property (nonatomic, weak) Person *owner;
    @end




    作者:NJKNJK
    链接:https://www.jianshu.com/p/af3d7700f280


    收起阅读 »

    『Blocks』基本使用

    本文用来介绍 iOS开发中 『Blocks』的基本使用。通过本文您将了解到:什么是 BlocksBlocks 变量语法Blocks 变量的声明与赋值Blocks 变量截获局部变量值特性使用 __block 说明符Blocks 变量的循环引用以及如何避...
    继续阅读 »

    本文用来介绍 iOS开发中 『Blocks』的基本使用。通过本文您将了解到:

    1. 什么是 Blocks
    2. Blocks 变量语法
    3. Blocks 变量的声明与赋值
    4. Blocks 变量截获局部变量值特性
    5. 使用 __block 说明符
    6. Blocks 变量的循环引用以及如何避免

    1. 什么是 Blocks ?

    一句话总结:Blocks 是带有 局部变量 的 匿名函数(不带名称的函数)。

    Blocks 也被称作 闭包代码块。展开来讲,Blocks 就是一个代码块,把你想要执行的代码封装在这个代码块里,等到需要的时候再去调用。

    下边我们先来理解 局部变量匿名函数 的含义。

    1.1 局部变量

    在 C 语言中,定义在函数内部的变量称为 局部变量。它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。

    int x, y; // x,y 为全局变量

    int fun(int a) {
    int b, c; //a,b,c 为局部变量
    return a+b+c;
    }

    int main() {
    int m, n; // m,n 为局部变量
    return 0;
    }

    从上边的代码中,我们可以看出:

    1. 我们在开始位置定义了变量 x 和 变量 y。 x 和 y 都是全局变量。它们的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。
    2. 而我们在 fun() 函数中定义了变量 a、变量 b、变量 c。它们的作用域是 fun() 函数。只能在 fun() 函数内部使用,离开 fun() 函数就是无效的。
    3. 同理,main() 函数中的变量 m、变量 n 也只能在 main() 函数内部使用。

    1.2 匿名函数

    匿名函数指的是不带有名称的函数。但是 C 语言中不允许存在这样的函数。

    在 C 语言中,一个普通的函数长这样子:


    int fun(int a);

    fun 就是这个函数的名称,在调用的时候必须要使用该函数的名称 fun 来调用。

    int result = fun(10);
    在 C 语言中,我们还可以通过函数指针来直接调用函数。但是在给函数指针赋值的时候,同样也是需要知道函数的名称。

    int (*funPtr)(int) = &fun;
    int result = (*funPtr)(10);

    而我们通过 Blocks,可以直接使用函数,不用给函数命名。


    2. Blocks 变量语法

    我们使用 ^ 运算符来声明 Blocks 变量,并将 Blocks 对象主体部分包含在 {} 中,同时,句尾加 ; 表示结尾。

    下边来看一个官方的示例:

    int multiplier = 7;
    int (^ myBlock)(int)= ^(int num) {
    return num * multiplier;
    };
    这个 Blocks 示例中,myBlock 是声明的块对象,返回类型是 整型值,myBlock 块对象有一个 参数,参数类型为整型值,参数名称为 num。myBlock 块对象的 主体部分 为 return num * multiplier;,包含在 {} 中。

    参考上面的示例,我们可以将 Blocks 表达式语法表述为:

    ^ 返回值类型 (参数列表) { 表达式 };

    例如,我们可以写出这样的 Block 语法:

    ^ int (int count) { return count + 1; };

    Blocks 规定可以省略好多项目。例如:返回值类型参数列表。如果用不到,都可以省略。

    2.1 省略返回值类型:^ (参数列表) { 表达式 };

    上边的 Blocks 语法就可以写为:

    ^ (int count) { return count + 1; };

    表达式中,return 语句使用的是 count + 1 语句的返回类型。如果表达式中有多个 return 语句,则所有 return 语句的返回值类型必须一致。

    如果表达式中没有 return 语句,则可以用 void 表示,或者也省略不写。代码如下:。

    ^ void (int count)  { printf("%d\n", count); };    // 返回值类型使用 void
    ^ (int count) { printf("%d\n", count); }; // 省略返回值类型

    2.2 省略参数列表 ^ 返回值类型 (void) { 表达式 };

    如果表达式中,没有使用参数,则用 void 表示,也可以省略 void。


    ^ int (void) { return 1; };    // 参数列表使用 void
    ^ int { return 1; }; // 省略参数列表类型

    2.3 省略返回值类型、参数列表:^ { 表达式 };

    从上边 2.1 中可以看出,无论有无返回值,都可以省略返回值类型。并且,从 2.2 中可以看出,如果不需要参数列表的话,也可以省略参数列表。则代码可以简化为:

    ^ { printf("Blocks"); };

    3. Blocks 变量的声明与赋值

    3.1 Blocks 变量的声明与赋值语法

    Blocks 变量的声明与赋值语法可以总结为:

    返回值类型 (^变量名) (参数列表) = Blocks 表达式

    注意:此处返回值类型不可以省略,若无返回值,则使用 void 作为返回值类型。

    例如,定义一个变量名为 blk 的 Blocks 变量:


    int (^blk) (int)  = ^(int count) { return count + 1; };
    int (^blk1) (int); // 声明变量名为 blk1 的 Blocks 变量
    blk1 = blk; // 将 blk 赋值给 blk1

    Blocks 变量的声明语法有点复杂,其实我们可以和 C 语言函数指针的声明类比着来记。

    Blocks 变量的声明就是把声明函数指针类型的变量 * 变为 ^

    //  C 语言函数指针声明与赋值
    int func (int count) {
    return count + 1;
    }
    int (*funcptr)(int) = &func;

    // Blocks 变量声明与赋值
    int (^blk) (int) = ^(int count) { return count + 1; };

    3.2 Blocks 变量的声明与赋值的使用

    3.2.1 作为局部变量:返回值类型 (^变量名) (参数列表) = 返回值类型 (参数列表) { 表达式 };

    我们可以把 Blocks 变量作为局部变量,在一定范围内(函数、方法内部)使用。

    // Blocks 变量作为本地变量
    - (void)useBlockAsLocalVariable {
    void (^myLocalBlock)(void) = ^{
    NSLog(@"useBlockAsLocalVariable");
    };

    myLocalBlock();
    }
    3.2.2 作为带有 property 声明的成员变量:@property (nonatomic, copy) 返回值类型 (^变量名) (参数列表);

    作用类似于 delegate,实现 Blocks 回调。

    /* Blocks 变量作为带有 property 声明的成员变量 */
    @property (nonatomic, copy) void (^myPropertyBlock) (void);

    // Blocks 变量作为带有 property 声明的成员变量
    - (void)useBlockAsProperty {
    self.myPropertyBlock = ^{
    NSLog(@"useBlockAsProperty");
    };

    self.myPropertyBlock();
    }

    3.2.3 作为 OC 方法参数:- (void)someMethodThatTaksesABlock:(返回值类型 (^)(参数列表)) 变量名;

    可以把 Blocks 变量作为 OC 方法中的一个参数来使用,通常 blocks 变量写在方法名的最后。

    // Blocks 变量作为 OC 方法参数
    - (void)someMethodThatTakesABlock:(void (^)(NSString *)) block {
    block(@"someMethodThatTakesABlock:");
    }
    3.2.4 调用含有 Block 参数的 OC方法:[someObject someMethodThatTakesABlock:^返回值类型 (参数列表) { 表达式}];
    // 调用含有 Block 参数的 OC方法
    - (void)useBlockAsMethodParameter {
    [self someMethodThatTakesABlock:^(NSString *str) {
    NSLog(@"%@",str);
    }];
    }

    通过 3.2.3 和 3.2.4 中,Blocks 变量作为 OC 方法参数的调用,我们同样可以实现类似于 delegate 的作用,即 Blocks 回调(后边应用场景中会讲)。

    3.2.5 作为 typedef 声明类型:
    typedef 返回值类型 (^声明名称)(参数列表);
    声明名称 变量名 = ^返回值类型(参数列表) { 表达式 };
    // Blocks 变量作为 typedef 声明类型
    - (void)useBlockAsATypedef {
    typedef void (^TypeName)(void);

    // 之后就可以使用 TypeName 来定义无返回类型、无参数列表的 block 了。
    TypeName myTypedefBlock = ^{
    NSLog(@"useBlockAsATypedef");
    };

    myTypedefBlock();
    }

    4. Blocks 变量截获局部变量值特性

    先来看一个例子。

    // 使用 Blocks 截获局部变量值
    - (void)useBlockInterceptLocalVariables {
    int a = 10, b = 20;

    void (^myLocalBlock)(void) = ^{
    printf("a = %d, b = %d\n",a, b);
    };

    myLocalBlock(); // 打印结果:a = 10, b = 20

    a = 20;
    b = 30;

    myLocalBlock(); // 打印结果:a = 10, b = 20
    }

    为什么两次打印结果都是 a = 10, b = 20

    明明在第一次调用 myLocalBlock(); 之后已经重新给变量 a、变量 b 赋值了,为什么第二次调用 myLocalBlock(); 的时候,使用的还是之前对应变量的值?

    因为 Block 语法的表达式使用的是它之前声明的局部变量 a、变量 b。Blocks 中,Block 表达式截获所使用的局部变量的值,保存了该变量的瞬时值。所以在第二次执行 Block 表达式时,即使已经改变了局部变量 a 和 b 的值,也不会影响 Block 表达式在执行时所保存的局部变量的瞬时值。

    这就是 Blocks 变量截获局部变量值的特性。

    5. 使用 __block 说明符

    实际上,在使用 Block 表达式的时候,只能使用保存的局部变量的瞬时值,并不能直接对其进行改写。直接修改编译器会直接报错,如下图所示。



    那么如果,我们想要该写 Block 表达式中截获的局部变量的值,该怎么办呢?

    如果,我们想在 Block 表达式中,改写 Block 表达式之外声明的局部变量,需要在该局部变量前加上 __block 的修饰符。

    这样我们就能实现:在 Block 表达式中,为表达式外的局部变量赋值。


    // 使用 __block 说明符修饰,更改局部变量值
    - (void)useBlockQualifierChangeLocalVariables {
    __block int a = 10, b = 20;
    void (^myLocalBlock)(void) = ^{
    a = 20;
    b = 30;

    printf("a = %d, b = %d\n",a, b); // 打印结果:a = 20, b = 30
    };

    myLocalBlock();
    }

    可以看到,使用 __block 说明符修饰之后,我们在 Block表达式中,成功的修改了局部变量值。

    6. Blocks 变量的循环引用以及如何避免

    从上文中我们知道 Block 会对引用的局部变量进行持有。同样,如果 Block 也会对引用的对象进行持有,从而会导致相互持有,引起循环引用。


    /* —————— retainCycleBlcok.m —————— */   
    #import <Foundation/Foundation.h>
    #import "Person.h"
    int main() {
    Person *person = [[Person alloc] init];
    person.blk = ^{
    NSLog(@"%@",person);
    };

    return 0;
    }


    /* —————— Person.h —————— */
    #import <Foundation/Foundation.h>

    typedef void(^myBlock)(void);

    @interface Person : NSObject
    @property (nonatomic, copy) myBlock blk;
    @end


    /* —————— Person.m —————— */
    #import "Person.h"

    @implementation Person

    @end

    上面 retainCycleBlcok.m 中 main() 函数的代码会导致一个问题:person 持有成员变量 myBlock blk,而 blk 也同时持有成员变量 person,两者互相引用,永远无法释放。就造成了循环引用问题。

    那么,如何来解决这个问题呢?

    6.1 ARC 下,通过 __weak 修饰符来消除循环引用

    在 ARC 下,可声明附有 __weak 修饰符的变量,并将对象赋值使用。

    int main() {
    Person *person = [[Person alloc] init];
    __weak typeof(person) weakPerson = person;

    person.blk = ^{
    NSLog(@"%@",weakPerson);
    };

    return 0;
    }

    这样,通过 __weak,person 持有成员变量 myBlock blk,而 blk 对 person 进行弱引用,从而就消除了循环引用。

    6.2 MRC 下,通过 __block 修饰符来消除循环引用

    MRC 下,是不支持 weak 修饰符的。但是我们可以通过 block 来消除循环引用。

    int main() {
    Person *person = [[Person alloc] init];
    __block typeof(person) blockPerson = person;

    person.blk = ^{
    NSLog(@"%@", blockPerson);
    };

    return 0;
    }

    通过 __block 引用的 blockPerson,是通过指针的方式来访问 person,而没有对 person 进行强引用,所以不会造成循环引用。




    作者:NJKNJK
    链接:https://www.jianshu.com/p/c5561abe9dd8


    收起阅读 »

    2021 提升Android开发效率的实战技巧

    一 泛型 + 反射 我们创建Activity的时候 需要先设置布局setContentView(R.layout..) 如果使用了ViewModel,还得给每个Activity创建ViewModel. 如果项目中Activity过多,无疑是...
    继续阅读 »

    一 泛型 + 反射


    我们创建Activity的时候



    1. 需要先设置布局setContentView(R.layout..)

    2. 如果使用了ViewModel,还得给每个Activity创建ViewModel.


    如果项目中Activity过多,无疑是写很多模板代码的,借助Java的泛型机制,我们可以在BaseAct,封装上述逻辑。


    1.1 示例


    先创建BaseAct


    abstract class BaseAct<B : ViewDataBinding, VM : ViewModel> : AppCompatActivity() {
    private var mBinding: B? = null
    private lateinit var mModel: VM
    abstract val layoutId: Int
    abstract fun doBusiness(savedInstanceState: Bundle?)
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 创建View
    setContentView(createViewBinding().root)
    // 创建ViewModel
    createViewModel()
    doBusiness(savedInstanceState)
    }

    fun getB(): B {
    return mBinding!!
    }

    fun getVM(): VM {
    return mModel
    }

    private fun createViewBinding(): B {
    mBinding = DataBindingUtil.inflate(LayoutInflater.from(this), layoutId, null, false)
    mBinding!!.lifecycleOwner = this
    return mBinding!!
    }

    private fun createViewModel() {
    val type = javaClass.genericSuperclass!! as ParameterizedType
    val argsType = type.actualTypeArguments
    val modelClass: Class<VM> = argsType[1] as Class<VM>
    val model = ViewModelProvider(this).get(modelClass)
    mModel = model
    }

    override fun onDestroy() {
    super.onDestroy()
    mBinding?.unbind()
    mBinding = null
    }
    }

    创建一个LoginAct的时候,可以这样写


    // 声明泛型类
    class LoginAct : BaseAct<LoginActBinding, LoginActViewModel>() {
    override val layoutId: Int = R.layout.login_act

    override fun doBusiness(savedInstanceState: Bundle?) {
    // 逻辑处理
    getVm() // 获取到的是 LoginActViewModel
    }
    }
    class LoginActViewModel : ViewModel() {

    }

    二 一次生成多个文件


    上面LoginAct的创建。我们一般得做以下几个步骤



    1. 创建一个xml布局

    2. new 一个 Kotlin Class/File创建LoginViewModel

    3. new 一个 Kotlin Class/File创建LoginAct

    4. LoginAct 继承 BaseAct,重写方法


    通过 templates模板,可以把上面步骤简化。


    2.1 as 版本4.1之前


    使用的是FreeMarker模板引擎


    2.1.1 把模板放到对应目录



    1. 新建文件夹mvvm_templates,放到目录**android Studio\plugins\android\lib\templates\activities **

    2. 把以下文件放到mvvm_templates文件夹里


    image.png


    2.1.2 模板文件介绍


    mvvm_templates
    |-- root // 文件
    |-- src
    |-- app_package
    |-- xx.kt // 期望生成的kt文件
    |-- xx.java // 期望生成的java文件
    |-- ...
    |-- res // 资源模板
    |-- xx.xml.ftl // 期望生成的xml
    |-- ...
    |-- globals.xml.ftl
    |-- recipe.xml.ftl // 管理所有的文件声明
    |-- template.xml // 模板控制台

    2.1.3 使用方法



    • 上面的ftl 描述执行模板的参数和指令

    • as启动后,Android Studio 会解析“  /templates ”文件夹的内容,向“ **New -> **”菜单界面添加模板名,当点击对应模板名,Android Studio会读取“ template.xml ”的内容,构建UI等。


    image.png


    image.png 我 as 升级了,无法截我自己的配置页面图,原理是一样的,你的模板配置了哪些选项,在上图中就可以选择。



    1. 我的mvvm_templates 模板下载地址


    这是我自己的配置,大家可以拿去参考修改。


    2.2 as 版本4.1后


    从 Android Studio 4.1 开始,Google 停止了对自定义 FreeMarker 模板的支持。 该功能对于我来讲是非常实用的,所以我在github上找到了另外一种解决方案1解决方案2


    很多人在谷歌的问题追踪里进行反馈,但到目前还在等待官方支持。


    三 一次生成一个文件


    Edit File Templates,创建单个xml、单个文件、文件头等模板


    3.1 创建xml布局


    image.png 步骤还是挺繁琐的,也需要点几下,创建出来的布局文件只有1个根布局。


    通过下面模板布局,可以简化上面步骤,并且可以设置一些常用的脚手架布局。


    3.2 创建xml模板布局


    3.2.1 配置模板



    1. 编辑模板

    2. 创建一个file

    3. 定义模板名字

    4. 定义文件后缀

    5. 把你的模板布局copy进去

    6. 完成


    image.png


    3.2.2 使用模板


    刚才配置的模板就会在这里显示,点击后就会生成对应的布局。 image.png


    配置布局会自动填充进来,可以根据不同场景,定义多种不同的模板。 image.png


    3.3 创建kt文件模板


    image.png 步骤和上面创建xml模板是一样的,只是该下文件后缀名。这里多了个File Header,创建步骤如下。


    3.4 创建File Header


    image.png


    四 单个文件快捷输出


    在AS 设置里 Live Templates


    4.1 示例


    如果我想让红色图片居中显示,必须得添加4行约束属性,这些属性对于咱们开发来讲是经常要写的。如果在xml里输入 cc 按下回车,就能生成这4行代码,是不是能节约点时间? image.png


    4.2 配置


    建议分组管理。



    • 在xml里的快捷键单独创建一个组。

    • 在kotlin的快捷键单独创建一个组。


    image.png


    image.png


    4.3 使用


    我设置了



    • cc显示4个约束属性

    • tt显示app:layout_constraintTop_toTopOf="parent"

    • 同样,在kotlin中,在java中,比如日志打印、if判断、初始化变量、更多使用场景等你挖掘。


    布局快捷键.gif


    4.4 我自己的模板


    image.png



    最终as会在该路径下生成上面我们的配置模板: C:\Users\userName\AppData\Roaming\Google\AndroidStudio4.1\templates



    五 AS 常用插件


    5.1 WiFi连接手机调试


    image.png


    5.2 Translation 翻译英文


    image.png


    5.3 其他



    • Alibaba Java Coding Guidelines 阿里Java代码规范

    • CodeGlance 在右边可以预览代码结构,实现快速定位

    • Database Navigator 数据库调试

    收起阅读 »

    Flutter 入门与实战:让模拟器和和邮递员(Postman)聊聊天

    前言 上一篇Flutter 入门与实战(五十五):和 Provider 一起玩 WebSocket我们讲了使用 socket_client_io 和 StreamProvider实现 WebSocket 通讯。本篇延续上一篇,来讲一下如何实现与其他用户进行即...
    继续阅读 »

    前言


    上一篇Flutter 入门与实战(五十五):和 Provider 一起玩 WebSocket我们讲了使用 socket_client_io 和 StreamProvider实现 WebSocket 通讯。本篇延续上一篇,来讲一下如何实现与其他用户进行即时聊天。


    Socket 消息推送


    在 与服务端Socket 通讯中,调用 socket.emit 方法时默认发送消息都是给当前连接的 socket的,如果要实现发送消息给其他用户,服务端需要做一下改造。具体的做法如下:



    • 在建立连接后,客户多发送消息将用户唯一标识符(例如用户名或 userId)与连接的 socket 对象进行绑定。

    • 当其他用户发送消息给该用户时,找到该用户绑定的 socket 对象,再通过该 socketemit 方法发送消息就可以搞定了。


    因此客户端需要发送一个注册消息到服务端以便与用户绑定,同时还应该有一个注销消息,以解除绑定(可选的,也可以通过断开连接来自动解除绑定)。整个聊天过程的时序图如下:


    时序图.png


    服务端代码已经好了,采用了一个简单的对象来存储用户相关的未发送消息和 socket 对象。可以到后端代码仓库拉取最新代码,


    消息格式约定


    Socket 可以发送字符串或Json 对象,这里我们约定消息聊天为 Json 对象,字段如下:



    • fromUserId:消息来源用户 id

    • toUserId:接收消息用户 id

    • contentType:消息类型,方便发送文本、图片、语音、视频等消息。目前只做了文本消息,其他消息其实可以在 content 中传对应的资源 id 后由App 自己处理就好了。

    • content:消息内容。


    StreamSocket 改造


    上一篇的 StreamSocket 改造我们只能发送字符串,为了扩大适用范围,将该类改造成泛型。这里需要注意,Socketemit 的数据会调用对象的 toJson 将对象转为 Json 对象发送,因此泛型的类需要实现 Map<String dynamic> toJson 方法。同时增加了如下属性和方法:



    • recvEvent:接收事件的名称

    • regsiter:注册方法,将用户 id发送到服务端与 socket 绑定,可以理解为上线通知;

    • unregister:注销方法,将用户 id 发送到服务端与 socket解绑,可以理解为下线通知。


    class StreamSocket<T> {
    final _socketResponse = StreamController<T>();

    Stream<T> get getResponse => _socketResponse.stream;

    final String host;
    final int port;
    late final Socket _socket;
    final String recvEvent;

    StreamSocket(
    {required this.host, required this.port, required this.recvEvent}) {
    _socket = SocketIO.io('ws://$host:$port', <String, dynamic>{
    'transports': ['websocket'],
    'autoConnect': true,
    'forceNew': true
    });
    }

    void connectAndListen() {
    _socket.onConnect((_) {
    debugPrint('connected');
    });

    _socket.onConnectTimeout((data) => debugPrint('timeout'));
    _socket.onConnectError((error) => debugPrint(error.toString()));
    _socket.onError((error) => debugPrint(error.toString()));
    _socket.on(recvEvent, (data) {
    _socketResponse.sink.add(data);
    });
    _socket.onDisconnect((_) => debugPrint('disconnect'));
    }

    void regsiter(String userId) {
    _socket.emit('register', userId);
    }

    void unregsiter(String userId) {
    _socket.emit('unregister', userId);
    }

    void sendMessage(String event, T message) {
    _socket.emit(event, message);
    }

    void close() {
    _socketResponse.close();
    _socket.disconnect().close();
    }
    }

    聊天页面


    新建一个 chat_with_user.dart 文件,实现聊天相关的代码,其中ChatWithUserPageStatefulWidget,以便在State 的生命周期管理 Socket的连接,注册和注销等操作。目前我们写死了 App 端的用户是 user1,发送消息给 user2


    class _ChatWithUserPageState extends State<ChatWithUserPage> {
    late final StreamSocket<Map<String, dynamic>> streamSocket;

    @override
    void initState() {
    super.initState();
    streamSocket =
    StreamSocket(host: '127.0.0.1', port: 3001, recvEvent: 'chat');
    streamSocket.connectAndListen();
    streamSocket.regsiter('user1');
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('即时聊天'),
    ),
    body: Stack(
    alignment: Alignment.bottomCenter,
    children: [
    StreamProvider<Map<String, dynamic>?>(
    create: (context) => streamSocket.getResponse,
    initialData: null,
    child: StreamDemo(),
    ),
    ChangeNotifierProvider<MessageModel>(
    child: MessageReplyBar(messageSendHandler: (message) {
    Map<String, String> json = {
    'fromUserId': 'user1',
    'toUserId': 'user2',
    'contentType': 'text',
    'content': message
    };
    streamSocket.sendMessage('chat', json);
    }),
    create: (context) => MessageModel(),
    ),
    ],
    ),
    );
    }

    @override
    void dispose() {
    streamSocket.unregsiter('user1');
    streamSocket.close();
    super.dispose();
    }
    }

    其他的和上一篇基本类似,只是消息对象由 String换成了 Map<String, dynamic>


    调试


    消息的对话界面本篇先不涉及,下一篇我们再来介绍。现在来看一下如何进行调试。目前 PostMan 的8.x 版本已经支持 WebSocket 调试了,我们拿PostMan 和手机模拟器进行联调。Postman 的 WebSocket 调试界面如下: image.png 使用起来比较简单,这里我们已经完成了如下操作:



    • 注册:使用 user2注册

    • 设置发送消息为 json,消息事件(event)为 chat,以便和 app、服务端 保持一致。


    现在来看看调试效果怎么样(PostMan 调起来有点手忙脚乱?)?


    屏幕录制2021-08-19 下午9.35.45.gif


    可以看到模拟器和 PostMan 直接的通讯是正常的。




    收起阅读 »

    iOS 专用图层 六

    6.8 CAEmitterLayer在iOS 5中,苹果引入了一个新的CALayer子类叫做CAEmitterLayer。CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时例子动画如:烟雾,火,雨等等这些效果。CAEmitterLayer看上去...
    继续阅读 »

    6.8 CAEmitterLayer

    在iOS 5中,苹果引入了一个新的CALayer子类叫做CAEmitterLayerCAEmitterLayer是一个高性能的粒子引擎,被用来创建实时例子动画如:烟雾,火,雨等等这些效果。

    CAEmitterLayer看上去像是许多CAEmitterCell的容器,这些CAEmitierCell定义了一个例子效果。你将会为不同的例子效果定义一个或多个CAEmitterCell作为模版,同时CAEmitterLayer负责基于这些模版实例化一个粒子流。一个CAEmitterCell类似于一个CALayer:它有一个contents属性可以定义为一个CGImage,另外还有一些可设置属性控制着表现和行为。我们不会对这些属性逐一进行详细的描述,你们可以在CAEmitterCell类的头文件中找到。

    我们来举个例子。我们将利用在一圆中发射不同速度和透明度的粒子创建一个火爆炸的效果。清单6.13包含了生成爆炸的代码。图6.13是运行结果

    清单6.13 用CAEmitterLayer创建爆炸效果

    #import "ViewController.h"
    #import

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end


    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];

    //create particle emitter layer
    CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:emitter];

    //configure emitter
    emitter.renderMode = kCAEmitterLayerAdditive;
    emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0);

    //create a particle template
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    cell.contents = (__bridge id)[UIImage imageNamed:@"Spark.png"].CGImage;
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
    cell.alphaSpeed = -0.4;
    cell.velocity = 50;
    cell.velocityRange = 50;
    cell.emissionRange = M_PI * 2.0;

    //add particle template to emitter
    emitter.emitterCells = @[cell];
    }
    @end

    图6.13 火焰爆炸效果

    CAEMitterCell的属性基本上可以分为三种:

    • 这种粒子的某一属性的初始值。比如,color属性指定了一个可以混合图片内容颜色的混合色。在示例中,我们将它设置为桔色。
    • 例子某一属性的变化范围。比如emissionRange属性的值是2π,这意味着例子可以从360度任意位置反射出来。如果指定一个小一些的值,就可以创造出一个圆锥形
    • 指定值在时间线上的变化。比如,在示例中,我们将alphaSpeed设置为-0.4,就是说例子的透明度每过一秒就是减少0.4,这样就有发射出去之后逐渐小时的效果。

    CAEmitterLayer的属性它自己控制着整个例子系统的位置和形状。一些属性比如birthRatelifetimecelocity,这些属性在CAEmitterCell中也有。这些属性会以相乘的方式作用在一起,这样你就可以用一个值来加速或者扩大整个例子系统。其他值得提到的属性有以下这些:

    • preservesDepth,是否将3D例子系统平面化到一个图层(默认值)或者可以在3D空间中混合其他的图层
    • renderMode,控制着在视觉上粒子图片是如何混合的。你可能已经注意到了示例中我们把它设置为kCAEmitterLayerAdditive,它实现了这样一个效果:合并例子重叠部分的亮度使得看上去更亮。如果我们把它设置为默认的kCAEmitterLayerUnordered,效果就没那么好看了(见图6.14).

    图6.14

    图6.14 禁止混色之后的火焰粒子

    6.9 CAEAGLLayer

    当iOS要处理高性能图形绘制,必要时就是OpenGL。应该说它应该是最后的杀手锏,至少对于非游戏的应用来说是的。因为相比Core Animation和UIkit框架,它不可思议地复杂。

    OpenGL提供了Core Animation的基础,它是底层的C接口,直接和iPhone,iPad的硬件通信,极少地抽象出来的方法。OpenGL没有对象或是图层的继承概念。它只是简单地处理三角形。OpenGL中所有东西都是3D空间中有颜色和纹理的三角形。用起来非常复杂和强大,但是用OpenGL绘制iOS用户界面就需要很多很多的工作了。

    为了能够以高性能使用Core Animation,你需要判断你需要绘制哪种内容(矢量图形,例子,文本,等等),但后选择合适的图层去呈现这些内容,Core Animation中只有一些类型的内容是被高度优化的;所以如果你想绘制的东西并不能找到标准的图层类,想要得到高性能就比较费事情了。

    因为OpenGL根本不会对你的内容进行假设,它能够绘制得相当快。利用OpenGL,你可以绘制任何你知道必要的集合信息和形状逻辑的内容。所以很多游戏都喜欢用OpenGL(这些情况下,Core Animation的限制就明显了:它优化过的内容类型并不一定能满足需求),但是这样依赖,方便的高度抽象接口就没了。

    在iOS 5中,苹果引入了一个新的框架叫做GLKit,它去掉了一些设置OpenGL的复杂性,提供了一个叫做CLKViewUIView的子类,帮你处理大部分的设置和绘制工作。前提是各种各样的OpenGL绘图缓冲的底层可配置项仍然需要你用CAEAGLLayer完成,它是CALayer的一个子类,用来显示任意的OpenGL图形。

    大部分情况下你都不需要手动设置CAEAGLLayer(假设用GLKView),过去的日子就不要再提了。特别的,我们将设置一个OpenGL ES 2.0的上下文,它是现代的iOS设备的标准做法。

    尽管不需要GLKit也可以做到这一切,但是GLKit囊括了很多额外的工作,比如设置顶点和片段着色器,这些都以类C语言叫做GLSL自包含在程序中,同时在运行时载入到图形硬件中。编写GLSL代码和设置EAGLayer没有什么关系,所以我们将用GLKBaseEffect类将着色逻辑抽象出来。其他的事情,我们还是会有以往的方式。

    在开始之前,你需要将GLKit和OpenGLES框架加入到你的项目中,然后就可以实现清单6.14中的代码,里面是设置一个GAEAGLLayer的最少工作,它使用了OpenGL ES 2.0 的绘图上下文,并渲染了一个有色三角(见图6.15).

    清单6.14 用CAEAGLLayer绘制一个三角形

    #import "ViewController.h"
    #import
    #import

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *glView;
    @property (nonatomic, strong) EAGLContext *glContext;
    @property (nonatomic, strong) CAEAGLLayer *glLayer;
    @property (nonatomic, assign) GLuint framebuffer;
    @property (nonatomic, assign) GLuint colorRenderbuffer;
    @property (nonatomic, assign) GLint framebufferWidth;
    @property (nonatomic, assign) GLint framebufferHeight;
    @property (nonatomic, strong) GLKBaseEffect *effect;

    @end

    @implementation ViewController

    - (void)setUpBuffers
    {
    //set up frame buffer
    glGenFramebuffers(1, &_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);

    //set up color render buffer
    glGenRenderbuffers(1, &_colorRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);

    //check success
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
    }

    - (void)tearDownBuffers
    {
    if (_framebuffer) {
    //delete framebuffer
    glDeleteFramebuffers(1, &_framebuffer);
    _framebuffer = 0;
    }

    if (_colorRenderbuffer) {
    //delete color render buffer
    glDeleteRenderbuffers(1, &_colorRenderbuffer);
    _colorRenderbuffer = 0;
    }
    }

    - (void)drawFrame {
    //bind framebuffer & set viewport
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    glViewport(0, 0, _framebufferWidth, _framebufferHeight);

    //bind shader program
    [self.effect prepareToDraw];

    //clear the screen
    glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);

    //set up vertices
    GLfloat vertices[] = {
    -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
    };

    //set up colors
    GLfloat colors[] = {
    0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
    };

    //draw triangle
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    //present render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
    }

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //set up context
    self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.glContext];

    //set up layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = self.glView.bounds;
    [self.glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};

    //set up base effect
    self.effect = [[GLKBaseEffect alloc] init];

    //set up buffers
    [self setUpBuffers];

    //draw frame
    [self drawFrame];
    }

    - (void)viewDidUnload
    {
    [self tearDownBuffers];
    [super viewDidUnload];
    }

    - (void)dealloc
    {
    [self tearDownBuffers];
    [EAGLContext setCurrentContext:nil];
    }
    @end

    图6.15

    图6.15 用OpenGL渲染的CAEAGLLayer图层

    在一个真正的OpenGL应用中,我们可能会用NSTimerCADisplayLink周期性地每秒钟调用-drawRrame方法60次,同时会将几何图形生成和绘制分开以便不会每次都重新生成三角形的顶点(这样也可以让我们绘制其他的一些东西而不是一个三角形而已),不过上面这个例子已经足够演示了绘图原则了。


    收起阅读 »

    iOS 专用图层 五

    6.6 CAScrollLayer对于一个未转换的图层,它的bounds和它的frame是一样的,frame属性是由bounds属性自动计算而出的,所以更改任意一个值都会更新其他值。但是如果你只想显示一个大图层里面的一小部分呢。比如说,你可能有一个很大的图片,...
    继续阅读 »

    6.6 CAScrollLayer

    对于一个未转换的图层,它的bounds和它的frame是一样的,frame属性是由bounds属性自动计算而出的,所以更改任意一个值都会更新其他值。

    但是如果你只想显示一个大图层里面的一小部分呢。比如说,你可能有一个很大的图片,你希望用户能够随意滑动,或者是一个数据或文本的长列表。在一个典型的iOS应用中,你可能会用到UITableView或是UIScrollView,但是对于独立的图层来说,什么会等价于刚刚提到的UITableViewUIScrollView呢?

    在第二章中,我们探索了图层的contentsRect属性的用法,它的确是能够解决在图层中小地方显示大图片的解决方法。但是如果你的图层包含子图层那它就不是一个非常好的解决方案,因为,这样做的话每次你想『滑动』可视区域的时候,你就需要手工重新计算并更新所有的子图层位置。

    这个时候就需要CAScrollLayer了。CAScrollLayer有一个-scrollToPoint:方法,它自动适应bounds的原点以便图层内容出现在滑动的地方。注意,这就是它做的所有事情。前面提到过,Core Animation并不处理用户输入,所以CAScrollLayer并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何iOS指定行为例如滑动反弹(当视图滑动超多了它的边界的将会反弹回正确的地方)。

    让我们来用CAScrollLayer来常见一个基本的UIScrollView替代品。我们将会用CAScrollLayer作为视图的宿主图层,并创建一个自定义的UIView,然后用UIPanGestureRecognizer实现触摸事件响应。这段代码见清单6.10. 图6.11是运行效果:ScrollView显示了一个大于它的frameUIImageView

    清单6.10 用CAScrollLayer实现滑动视图

    #import "ScrollView.h"
    #import @implementation ScrollView
    + (Class)layerClass
    {
    return [CAScrollLayer class];
    }

    - (void)setUp
    {
    //enable clipping
    self.layer.masksToBounds = YES;

    //attach pan gesture recognizer
    UIPanGestureRecognizer *recognizer = nil;
    recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [self addGestureRecognizer:recognizer];
    }

    - (id)initWithFrame:(CGRect)frame
    {
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
    [self setUp];
    }
    return self;
    }

    - (void)awakeFromNib {
    //this is called when view is created from a nib
    [self setUp];
    }

    - (void)pan:(UIPanGestureRecognizer *)recognizer
    {
    //get the offset by subtracting the pan gesture
    //translation from the current bounds origin
    CGPoint offset = self.bounds.origin;
    offset.x -= [recognizer translationInView:self].x;
    offset.y -= [recognizer translationInView:self].y;

    //scroll the layer
    [(CAScrollLayer *)self.layer scrollToPoint:offset];

    //reset the pan gesture translation
    [recognizer setTranslation:CGPointZero inView:self];
    }
    @end

    图6.11 用UIScrollView创建一个凑合的滑动视图

    不同于UIScrollView,我们定制的滑动视图类并没有实现任何形式的边界检查(bounds checking)。图层内容极有可能滑出视图的边界并无限滑下去。CAScrollLayer并没有等同于UIScrollViewcontentSize的属性,所以当CAScrollLayer滑动的时候完全没有一个全局的可滑动区域的概念,也无法自适应它的边界原点至你指定的值。它之所以不能自适应边界大小是因为它不需要,内容完全可以超过边界。

    那你一定会奇怪用CAScrollLayer的意义到底何在,因为你可以简单地用一个普通的CALayer然后手动适应边界原点啊。真相其实并不复杂,UIScrollView并没有用CAScrollLayer,事实上,就是简单的通过直接操作图层边界来实现滑动。

    CAScrollLayer有一个潜在的有用特性。如果你查看CAScrollLayer的头文件,你就会注意到有一个扩展分类实现了一些方法和属性:

    - (void)scrollPoint:(CGPoint)p;
    - (void)scrollRectToVisible:(CGRect)r;
    @property(readonly) CGRect visibleRect;

    看到这些方法和属性名,你也许会以为这些方法给每个CALayer实例增加了滑动功能。但是事实上他们只是放置在CAScrollLayer中的图层的实用方法。scrollPoint:方法从图层树中查找并找到第一个可用的CAScrollLayer,然后滑动它使得指定点成为可视的。scrollRectToVisible:方法实现了同样的事情只不过是作用在一个矩形上的。visibleRect属性决定图层(如果存在的话)的哪部分是当前的可视区域。如果你自己实现这些方法就会相对容易明白一点,但是CAScrollLayer帮你省了这些麻烦,所以当涉及到实现图层滑动的时候就可以用上了。

    6.7 CATiledLayer

    有些时候你可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者是地球表面的详细地图。iOS应用通畅运行在内存受限的设备上,所以读取整个图片到内存中是不明智的。载入大图可能会相当地慢,那些对你看上去比较方便的做法(在主线程调用UIImage-imageNamed:方法或者-imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。

    能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048*2048,或4096*4096,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU(见第12章『速度的曲调』,和第13章『高效绘图』,它更加详细地解释了软件绘制和硬件绘制)。

    CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。让我们用实验来证明一下。

    小片裁剪

    这个示例中,我们将会从一个2048*2048分辨率的雪人图片入手。为了能够从CATiledLayer中获益,我们需要把这个图片裁切成许多小一些的图片。你可以通过代码来完成这件事情,但是如果你在运行时读入整个图片并裁切,那CATiledLayer这些所有的性能优点就损失殆尽了。理想情况下来说,最好能够逐个步骤来实现。

    清单6.11 演示了一个简单的Mac OS命令行程序,它用CATiledLayer将一个图片裁剪成小图并存储到不同的文件中。

    清单6.11 裁剪图片成小图的终端程序

    #import 

    int main(int argc, const char * argv[])
    {
    @autoreleasepool{
    //handle incorrect arguments
    if (argc < 2) {
    NSLog(@"TileCutter arguments: inputfile");
    return 0;
    }

    //input file
    NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];

    //tile size
    CGFloat tileSize = 256; //output path
    NSString *outputPath = [inputFile stringByDeletingPathExtension];

    //load image
    NSImage *image = [[NSImage alloc] initWithContentsOfFile:inputFile];
    NSSize size = [image size];
    NSArray *representations = [image representations];
    if ([representations count]){
    NSBitmapImageRep *representation = representations[0];
    size.width = [representation pixelsWide];
    size.height = [representation pixelsHigh];
    }
    NSRect rect = NSMakeRect(0.0, 0.0, size.width, size.height);
    CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil];

    //calculate rows and columns
    NSInteger rows = ceil(size.height / tileSize);
    NSInteger cols = ceil(size.width / tileSize);

    //generate tiles
    for (int y = 0; y < rows; ++y) {
    for (int x = 0; x < cols; ++x) {
    //extract tile image
    CGRect tileRect = CGRectMake(x*tileSize, y*tileSize, tileSize, tileSize);
    CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect);

    //convert to jpeg data
    NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage];
    NSData *data = [imageRep representationUsingType: NSJPEGFileType properties:nil];
    CGImageRelease(tileImage);

    //save file
    NSString *path = [outputPath stringByAppendingFormat: @"_i_i.jpg", x, y];
    [data writeToFile:path atomically:NO];
    }
    }
    }
    return 0;
    }

    这个程序将2048*2048分辨率的雪人图案裁剪成了64个不同的256*256的小图。(256*256是CATiledLayer的默认小图大小,默认大小可以通过tileSize属性更改)。程序接受一个图片路径作为命令行的第一个参数。我们可以在编译的scheme将路径参数硬编码然后就可以在Xcode中运行了,但是以后作用在另一个图片上就不方便了。所以,我们编译了这个程序并把它保存到敏感的地方,然后从终端调用,如下面所示:

    > path/to/TileCutterApp path/to/Snowman.jpg

    The app is very basic, but could easily be extended to support additional arguments such as tile size, or to export images in formats other than JPEG. The result of running it is a sequence of 64 new images, named as follows:

    这个程序相当基础,但是能够轻易地扩展支持额外的参数比如小图大小,或者导出格式等等。运行结果是64个新图的序列,如下面命名:

    Snowman_00_00.jpg
    Snowman_00_01.jpg
    Snowman_00_02.jpg
    ...
    Snowman_07_07.jpg

    既然我们有了裁切后的小图,我们就要让iOS程序用到他们。CATiledLayer很好地和UIScrollView集成在一起。除了设置图层和滑动视图边界以适配整个图片大小,我们真正要做的就是实现-drawLayer:inContext:方法,当需要载入新的小图时,CATiledLayer就会调用到这个方法。

    清单6.12演示了代码。图6.12是代码运行结果。

    清单6.12 一个简单的滚动CATiledLayer实现

    #import "ViewController.h"
    #import

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIScrollView *scrollView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the tiled layer
    CATiledLayer *tileLayer = [CATiledLayer layer];
    tileLayer.frame = CGRectMake(0, 0, 2048, 2048);
    tileLayer.delegate = self; [self.scrollView.layer addSublayer:tileLayer];

    //configure the scroll view
    self.scrollView.contentSize = tileLayer.frame.size;

    //draw layer
    [tileLayer setNeedsDisplay];
    }

    - (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
    {
    //determine tile coordinate
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height);

    //load tile image
    NSString *imageName = [NSString stringWithFormat: @"Snowman_i_i", x, y];
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
    UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];

    //draw tile
    UIGraphicsPushContext(ctx);
    [tileImage drawInRect:bounds];
    UIGraphicsPopContext();
    }
    @end

    图6.12

    图6.12 用UIScrollView滚动CATiledLayer

    当你滑动这个图片,你会发现当CATiledLayer载入小图的时候,他们会淡入到界面中。这是CATiledLayer的默认行为。(你可能已经在iOS 6之前的苹果地图程序中见过这个效果)你可以用fadeDuration属性改变淡入时长或直接禁用掉。CATiledLayer(不同于大部分的UIKit和Core Animation方法)支持多线程绘制,-drawLayer:inContext:方法可以在多个线程中同时地并发调用,所以请小心谨慎地确保你在这个方法中实现的绘制代码是线程安全的。

    Retina小图

    你也许已经注意到了这些小图并不是以Retina的分辨率显示的。为了以屏幕的原生分辨率来渲染CATiledLayer,我们需要设置图层的contentsScale来匹配UIScreenscale属性:

    tileLayer.contentsScale = [UIScreen mainScreen].scale;

    有趣的是,tileSize是以像素为单位,而不是点,所以增大了contentsScale就自动有了默认的小图尺寸(现在它是128*128的点而不是256*256).所以,我们不需要手工更新小图的尺寸或是在Retina分辨率下指定一个不同的小图。我们需要做的是适应小图渲染代码以对应安排scale的变化,然而:

    //determine tile coordinate
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    CGFloat scale = [UIScreen mainScreen].scale;
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);

    通过这个方法纠正scale也意味着我们的雪人图将以一半的大小渲染在Retina设备上(总尺寸是1024*1024,而不是2048*2048)。这个通常都不会影响到用CATiledLayer正常显示的图片类型(比如照片和地图,他们在设计上就是要支持放大缩小,能够在不同的缩放条件下显示),但是也需要在心里明白。

    收起阅读 »

    Android修炼系列,图解抓包和弱网测试

    本节主要介绍下,如何使用 Charles 进行抓包和模拟弱网环境测试。Charles 能够帮助我们查看设备和 Internet 之间的所有 HTTP 和 SSL/HTTPS 通信,这包括请求、响应和 HTTP 头。 HTTP代理 我们要保证手机设备和电脑在...
    继续阅读 »

    本节主要介绍下,如何使用 Charles 进行抓包和模拟弱网环境测试。Charles 能够帮助我们查看设备和 Internet 之间的所有 HTTP 和 SSL/HTTPS 通信,这包括请求、响应和 HTTP 头。


    HTTP代理


    我们要保证手机设备和电脑在一个局域网下,这点要注意。


    安装完 Charles 软件后,首先要进行 HTTP 代理设置,默认端口号:8888


    抓包1.png


    接着查看电脑 IP,将手机 WIFI 网络选项也进行代理设置。通常步骤为 WIFI 高级选项 -> 代理手动 -> 输入IP与端口 -> 保存。如本例中将安卓设备的 WIFI 代理设置为 192.168.0.110


    抓包2.jpg


    随后会有连接成功的提示,点击允许。


    抓包3.jpg


    Allow 之后,我们就进入抓包界面了。请求信息会在界面的左侧展示。但是通过下图也能发现,https 的请求抓包乱码。


    抓包4.png


    针对HTTPS乱码的问题,我们需要设置 HTTPS 代理。


    HTTPS 代理


    要抓取 https 的接口的请求信息,那么 Charles 需要在电脑端安装证书。


    抓包5.png


    在电脑如下目录下,我们双击安装证书,并信任。


    抓包6.png


    证书安装完毕,Charles 还需要进行 SSL 代理配置。


    抓包7.png


    其中 Charles 的 Location 配置是支持通配符的,如不需要抓取特定域名,我们可直接填写 * 。Host的配置,ssl port 常规为 443。


    抓包8.png


    配置好SSL代理之后,我们同样需要给待测试手机安装证书,下证书载地址可通过如下方式查看。


    抓包9.png


    通过下图,我们知下载地址:chls.pro/ssl 我们打开手机浏览器,输入该地址下载手机证书。随后安装,并信任


    抓包10.png


    当我们操作完毕之后,我们就能抓取部分 HTTPS 的请求了。我实际测试有些 HTTPS 请求还是没办法脱码的。


    网速配置


    弱网环境测试就简单多了,在说之前,我们先来看下 Charles 工具栏中提供的快捷按钮:


    抓包11.jpg



    • 清除捕获到的所有请求


    • 红点状态说明正在捕获请求,灰色状态说明目前没有捕获请求。


    • 停止SSL代理


    • 灰色状态说明是没有开启网速节流,绿色状态说明开启了网速节流。


    • 灰色状态说明是没有开启断点,红色状态说明开启了断点。


    • 编辑修改请求,点击之后可以修改请求的内容。


    • 重复发送请求,点击之后选中的请求会被再次发送。


    • 验证选中的请求的响应。


    • 常用功能,包含了 Tools 菜单中的常用功能。


    • 常用设置,包含了 Proxy 菜单中的常用设置。



    这里的小乌龟图标就是我们所需要的啦。


    当然我们也可通过 Throttle Setting 来进行节流控制。其包括 Bandwidth:带宽、Utilistation:利用百分比、Round-trip:往返延迟、MTU:字节。这里选择 BandWidth(带宽)复选框来开启限速。


    抓包12.png



    好了,本文到此就结束了。知识无涯,勿焦虑。



    收起阅读 »

    iOS 专用图层 四

    6.5 CAReplicatorLayerCAReplicatorLayer的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。看上去演示能够更加解释这些,我们来写个例子吧。重复图层(Repeating Laye...
    继续阅读 »

    6.5 CAReplicatorLayer

    CAReplicatorLayer的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。看上去演示能够更加解释这些,我们来写个例子吧。

    重复图层(Repeating Layers)

    清单6.8中,我们在屏幕的中间创建了一个小白色方块图层,然后用CAReplicatorLayer生成十个图层组成一个圆圈。instanceCount属性指定了图层需要重复多少次。instanceTransform指定了一个CATransform3D3D变换(这种情况下,下一图层的位移和旋转将会移动到圆圈的下一个点)。

    变换是逐步增加的,每个实例都是相对于前一实例布局。这就是为什么这些复制体最终不会出现在同意位置上,图6.8是代码运行结果。

    清单6.8 用CAReplicatorLayer重复图层

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController
    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a replicator layer and add it to our view
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:replicator];

    //configure the replicator
    replicator.instanceCount = 10;

    //apply a transform for each instance
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0, 200, 0);
    transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
    transform = CATransform3DTranslate(transform, 0, -200, 0);
    replicator.instanceTransform = transform;

    //apply a color shift for each instance
    replicator.instanceBlueOffset = -0.1;
    replicator.instanceGreenOffset = -0.1;

    //create a sublayer and place it inside the replicator
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    [replicator addSublayer:layer];
    }
    @end

    图6.8

    图6.8 用CAReplicatorLayer创建一圈图层

    注意到当图层在重复的时候,他们的颜色也在变化:这是用instanceBlueOffsetinstanceGreenOffset属性实现的。通过逐步减少蓝色和绿色通道,我们逐渐将图层颜色转换成了红色。这个复制效果看起来很酷,但是CAReplicatorLayer真正应用到实际程序上的场景比如:一个游戏中导弹的轨迹云,或者粒子爆炸(尽管iOS 5已经引入了CAEmitterLayer,它更适合创建任意的粒子效果)。除此之外,还有一个实际应用是:反射。

    反射

    使用CAReplicatorLayer并应用一个负比例变换于一个复制图层,你就可以创建指定视图(或整个视图层次)内容的镜像图片,这样就创建了一个实时的『反射』效果。让我们来尝试实现这个创意:指定一个继承于UIViewReflectionView,它会自动产生内容的反射效果。实现这个效果的代码很简单(见清单6.9),实际上用ReflectionView实现这个效果会更简单,我们只需要把ReflectionView的实例放置于Interface Builder(见图6.9),它就会实时生成子视图的反射,而不需要别的代码(见图6.10).

    清单6.9 用CAReplicatorLayer自动绘制反射

    #import "ReflectionView.h"
    #import

    @implementation ReflectionView

    + (Class)layerClass
    {
    return [CAReplicatorLayer class];
    }

    - (void)setUp
    {
    //configure replicator
    CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
    layer.instanceCount = 2;

    //move reflection instance below original and flip vertically
    CATransform3D transform = CATransform3DIdentity;
    CGFloat verticalOffset = self.bounds.size.height + 2;
    transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    layer.instanceTransform = transform;

    //reduce alpha of reflection layer
    layer.instanceAlphaOffset = -0.6;
    }

    - (id)initWithFrame:(CGRect)frame
    {
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
    [self setUp];
    }
    return self;
    }

    - (void)awakeFromNib
    {
    //this is called when view is created from a nib
    [self setUp];
    }
    @end

    图6.9

    图6.9 在Interface Builder中使用ReflectionView

    图6.10

    图6.10 ReflectionView自动实时产生反射效果。

    开源代码ReflectionView完成了一个自适应的渐变淡出效果(用CAGradientLayer和图层蒙板实现),代码见 https://github.com/nicklockwood/ReflectionView

    收起阅读 »

    iOS 专用图层 三

    6.3 CATransformLayer当我们在构造复杂的3D事物的时候,如果能够组织独立元素就太方便了。比如说,你想创造一个孩子的手臂:你就需要确定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等...
    继续阅读 »

    6.3 CATransformLayer

    当我们在构造复杂的3D事物的时候,如果能够组织独立元素就太方便了。比如说,你想创造一个孩子的手臂:你就需要确定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。

    当然是允许独立地移动每个区域的啦。以肘为指点会移动前臂和手,而不是肩膀。Core Animation图层很容易就可以让你在2D环境下做出这样的层级体系下的变换,但是3D情况下就不太可能,因为所有的图层都把他的孩子都平面化到一个场景中(第五章『变换』有提到)。

    CATransformLayer解决了这个问题,CATransformLayer不同于普通的CALayer,因为它不能显示它自己的内容。只有当存在了一个能作用域子图层的变换它才真正存在。CATransformLayer并不平面化它的子图层,所以它能够用于构造一个层级的3D结构,比如我的手臂示例。

    用代码创建一个手臂需要相当多的代码,所以我就演示得更简单一些吧:在第五章的立方体示例,我们将通过旋转camara来解决图层平面化问题而不是像立方体示例代码中用的sublayerTransform。这是一个非常不错的技巧,但是只能作用域单个对象上,如果你的场景包含两个立方体,那我们就不能用这个技巧单独旋转他们了。

    那么,就让我们来试一试CATransformLayer吧,第一个问题就来了:在第五章,我们是用多个视图来构造了我们的立方体,而不是单独的图层。我们不能在不打乱已有的视图层次的前提下在一个本身不是有寄宿图的图层中放置一个寄宿图图层。我们可以创建一个新的UIView子类寄宿在CATransformLayer(用+layerClass方法)之上。但是,为了简化案例,我们仅仅重建了一个单独的图层,而不是使用视图。这意味着我们不能像第五章一样在立方体表面显示按钮和标签,不过我们现在也用不到这个特性。

    清单6.5就是代码。我们以我们在第五章使用过的相同基本逻辑放置立方体。但是并不像以前那样直接将立方面添加到容器视图的宿主图层,我们将他们放置到一个CATransformLayer中创建一个独立的立方体对象,然后将两个这样的立方体放进容器中。我们随机地给立方面染色以将他们区分开来,这样就不用靠标签或是光亮来区分他们。图6.5是运行结果。

    清单6.5 用CATransformLayer装配一个3D图层体系

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (CALayer *)faceWithTransform:(CATransform3D)transform
    {
    //create cube face layer
    CALayer *face = [CALayer layer];
    face.frame = CGRectMake(-50, -50, 100, 100);

    //apply a random color
    CGFloat red = (rand() / (double)INT_MAX);
    CGFloat green = (rand() / (double)INT_MAX);
    CGFloat blue = (rand() / (double)INT_MAX);
    face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

    //apply the transform and return
    face.transform = transform;
    return face;
    }

    - (CALayer *)cubeWithTransform:(CATransform3D)transform
    {
    //create cube layer
    CATransformLayer *cube = [CATransformLayer layer];

    //add cube face 1
    CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
    [cube addSublayer:[self faceWithTransform:ct]];

    //add cube face 2
    ct = CATransform3DMakeTranslation(50, 0, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];

    //add cube face 3
    ct = CATransform3DMakeTranslation(0, -50, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];

    //add cube face 4
    ct = CATransform3DMakeTranslation(0, 50, 0);
    ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];

    //add cube face 5
    ct = CATransform3DMakeTranslation(-50, 0, 0);
    ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];

    //add cube face 6
    ct = CATransform3DMakeTranslation(0, 0, -50);
    ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];

    //center the cube layer within the container
    CGSize containerSize = self.containerView.bounds.size;
    cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);

    //apply the transform and return
    cube.transform = transform;
    return cube;
    }

    - (void)viewDidLoad
    {
    [super viewDidLoad];

    //set up the perspective transform
    CATransform3D pt = CATransform3DIdentity;
    pt.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = pt;

    //set up the transform for cube 1 and add it
    CATransform3D c1t = CATransform3DIdentity;
    c1t = CATransform3DTranslate(c1t, -100, 0, 0);
    CALayer *cube1 = [self cubeWithTransform:c1t];
    [self.containerView.layer addSublayer:cube1];

    //set up the transform for cube 2 and add it
    CATransform3D c2t = CATransform3DIdentity;
    c2t = CATransform3DTranslate(c2t, 100, 0, 0);
    c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);
    c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);
    CALayer *cube2 = [self cubeWithTransform:c2t];
    [self.containerView.layer addSublayer:cube2];
    }
    @end

    图6.5

    图6.5 同一视角下的俩不同变换的立方体

    6.4 CAGradientLayer

    CAGradientLayer是用来生成两种或更多颜色平滑渐变的。用Core Graphics复制一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处在于绘制使用了硬件加速。

    基础渐变

    我们将从一个简单的红变蓝的对角线渐变开始(见清单6.6).这些渐变色彩放在一个数组中,并赋给colors属性。这个数组成员接受CGColorRef类型的值(并不是从NSObject派生而来),所以我们要用通过bridge转换以确保编译正常。

    CAGradientLayer也有startPointendPoint属性,他们决定了渐变的方向。这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标是{1, 1}。代码运行结果如图6.6

    清单6.6 简单的两种颜色的对角线渐变

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create gradient layer and add it to our container view
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:gradientLayer];

    //set gradient colors
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor];

    //set gradient start and end points
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
    }
    @end

    图6.6

    图6.6 用CAGradientLayer实现简单的两种颜色的对角线渐变

    多重渐变

    如果你愿意,colors属性可以包含很多颜色,所以创建一个彩虹一样的多重渐变也是很简单的。默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用locations属性来调整空间。locations属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了colors属性中每个不同颜色的位置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。

    locations数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations的数组大小和colors数组大小一定要相同,否则你将会得到一个空白的渐变。

    清单6.7展示了一个基于清单6.6的对角线渐变的代码改造。现在变成了从红到黄最后到绿色的渐变。locations数组指定了0.0,0.25和0.5三个数值,这样这三个渐变就有点像挤在了左上角。(如图6.7).

    清单6.7 在渐变上使用locations

    - (void)viewDidLoad {
    [super viewDidLoad];

    //create gradient layer and add it to our container view
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:gradientLayer];

    //set gradient colors
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id) [UIColor yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];

    //set locations
    gradientLayer.locations = @[@0.0, @0.25, @0.5];

    //set gradient start and end points
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
    }

    图6.7

    图6.7 用locations构造偏移至左上角的三色渐变

    收起阅读 »

    字节跳动开源AndroidPLThook方案bhook

    字节 bhook 开源 github.com/bytedance/b… 字节的 Android PLT hook 方案 bhook 开源了。bhook 支持 Android 4.1 - 12 (API level 16 - 31),支持 armeabi-v...
    继续阅读 »

    字节 bhook 开源


    github.com/bytedance/b…


    字节的 Android PLT hook 方案 bhook 开源了。bhook 支持 Android 4.1 - 12 (API level 16 - 31),支持 armeabi-v7a, arm64-v8a, x86 和 x86_64,使用 MIT 许可证授权。


    字节的大多数 Android app 都在线上使用了 bhook 作为 PLT hook 方案。字节内部有 20 多个不同技术纬度的 SDK 使用了 bhook。bhook 在线上稳定性,功能性,性能等多个方面都达到了预期。


    Android native hook


    随着 Android app 开发的技术栈不断向 native 层扩展,native hook 已经被用于越来越多的技术场景中。Android native hook 的实现方式有很多种,其中使用最广泛,并且通用性最强的是 inline hook 和 PLT hook。


    inline hook 的功能无疑是最强大的,它受到的限制很少,几乎可以 hook 任何地方。inline hook 在线下场景中使用的比较多,业内现有的通用的 inline hook 开源方案或多或少都存在一些稳定性问题,而且基本都缺乏大规模的线上验证。


    PLT hook 的优点是稳定性可控,可以真正的在线上全量使用。但 PLT hook 只能 hook 通过 PLT 表跳转的函数调用,这在一定程度上限制了它的使用场景。


    在真实的线上环境中,经常是 PLT hook 和 inline hook 并存的,这样它们可以各自扬长避短,在不同的场景中发挥作用。


    ELF


    要弄清 Android PLT hook 的原理,需要了解 ELF 文件格式,以及 linker(动态连接器)加载 ELF 文件的过程。


    app_process 和 so 库(动态链接库)都是 ELF(Executable and Linkable Format)格式的文件。对于运行时 native hook 来说,我们主要关心最终的产物,即 ELF 文件。


    ELF 文件的起始处,有一个固定格式的定长的文件头。ELF 文件头中包含了 SHT(section header table)和 PHT(program header table)在当前 ELF 文件中的起始位置和长度。SHT 和 PHT 分别描述了 ELF 的“连接视图”和“执行视图”的基本信息。



    Execution View(执行视图)


    ELF 分为连接视图(Linking View)和执行视图(Execution View)。



    • 连接视图:ELF 未被加载到内存执行前,以 section 为单位的数据组织形式。

    • 执行视图:ELF 被加载到内存后,以 segment 为单位的数据组织形式。


    PLT hook 并不是修改磁盘上的 ELF 文件,而是在运行时修改内存中的数据,因此我们主要关心的是执行视图,即 ELF 被加载到内存后,ELF 中的数据是如何组织和存放的。


    linker 依据 ELF 文件执行视图中的信息,用 mmap 将 ELF 加载到内存中,执行 relocation(重定位)把外部引用的绝对地址填入 GOT 表和 DATA 中,然后设置内存页的权限,最后调用 init_array 中的各个初始化函数。


    PLT hook 执行的时机是在 linker 完全加载完 ELF 之后,我们需要解析内存中的 ELF 数据,然后修改 relocation 的结果。


    ELF 中可以包含很多类型的 section(节),下面介绍一些比较重要的,以及和 PLT hook 相关的 section。


    Dynamic section


    .dynamic 是专门为 linker 设计的,其中包含了 linker 解析和加载 ELF 时会用到的各项数据的索引。linker 在解析完 ELF 头和执行视图的内容后,就会开始解析 .dynamic


    Data(数据)



    • .bss:未初始化的数据。比如:没有赋初值的全局变量和静态变量。(.bss 不占用 ELF 文件体积)

    • .data:已初始化的非只读数据。比如:int g_value = 1;,或者 size_t (*strlen_ptr)(const char *) = strlen;(初始化过程需要 linker relocation 参与才能知道外部 strlen 函数的绝对地址)

    • .rodata:已初始化的只读数据,加载完成后所属内存页会被 linker 设置为只读。比如:const int g_value = 1;

    • .data.rel.ro:已初始化的只读数据,初始化过程需要 linker relocation 参与,加载完成后所属内存页会被 linker 设置为只读。比如:const size_t (*strlen)(const char *) = strlen;


    Code(代码)



    • .text:大多数函数被编译成二进制机器指令后,会存放在这里。

    • .init_array:有时候我们需要在 ELF 被加载后立刻自动执行一些逻辑,比如定义一个全局的 C++ 类的实例,这时候就需要在 .init_array 中调用这个类的构造函数。另外,也可以用 __attribute__((constructor)) 定义单独的 init 函数。

    • .plt:对外部或内部的符号的调用跳板,.plt 会从 .got.data.data.rel.ro 中查询符号的绝对地址,然后执行跳转。


    Symbol(符号)


    符号可以分为两类:“动态链接符号”和“内部符号(调试符号)”,这两个符号集合并不存在严格的相互包含关系,调试器一般会同时加载这两种符号。linker 只关心动态链接符号,内部符号并不会被 linker 加载到内存中。执行 PLT hook 时也只关心动态链接符号。



    • .dynstr:动态链接符号的字符串池,保存了动态链接过程中用到的所有字符串信息,比如:函数名,全局变量名。

    • .dynsym:动态链接符号的索引信息表,起到“关联”和“描述”的作用。


    动态链接符号分为“导入符号”和“导出符号”:



    • 导出符号:指当前 ELF 提供给外部使用的符号。比如:libc.so 中的 open 就是 libc.so 的导出符号。

    • 导入符号:指当前 ELF 需要使用的外部符号。比如:你自己的 libtest.so 如果用到了 open,那么 open 就会被定义为 libtest.so 的导入符号。


    顺便提一下,内部符号的信息包含在 .symtab.strtab.gnu_debugdata 中。


    hash table(哈希表)


    为了加速“动态链接符号的字符串”的查找过程,ELF 中包含了这些字符串的哈希表,通过查哈希表,可以快速确认 ELF 中是否存在某个动态链接符号,以及这个符号对应的信息项在 .dynsym 中的偏移位置。


    历史原因,Android ELF 中会存在两种格式的哈希表:



    • .hash:SYSV hash。其中包含了所有的动态链接符号。

    • .gnu.hash:GNU hash。只包含动态链接符号中的导出符号。


    ELF 中可能同时包含 .hash.gnu.hash,也可能只包含其中一个。具体看 ELF 编译时的静态链接参数 -Wl,--hash-style,可以设置为 sysvgnuboth。从 Android 6.0 开始,linker 支持了 .gnu.hash 的解析。


    linker(动态链接器)



    linker 在加载 ELF 时的最主要工作是 relocation(重定位),这个过程的目的是为当前 ELF 的每个“导入符号”找到对应的外部符号(函数或数据)的绝对地址。最终,这些地址会被写入以下几个地方:



    • .got.plt:保存外部函数的绝对地址。这就是我们经常会听到的 “GOT 表”。

    • .data.data.rel.ro:保存外部数据(包括函数指针)的绝对地址。


    要完成 relocation 过程,需要依赖于 ELF 中的以下信息:



    • .rel.plt.rela.plt:用于关联 .dynsym.got.plt。这就是我们经常会听到的 “PLT 表”。

    • .rel.dyn.rela.dyn.rel.dyn.aps2.rela.dyn.aps2:用于关联 .dynsym.data.data.rel.ro


    Android 只在 64 位实现中使用 RELA 格式,它比 REL 格式多了附加的 r_addend 字段。另外,Android 从 6.0 开始支持 aps2 格式的 .rel.dyn.rela.dyn 数据,这是一种 sleb128 编码格式的数据,读取时需要特别的解码逻辑。


    relocation 完成之后的函数调用关系如下:



    relocation 完成之后的数据引用关系如下:



    Android PLT hook


    PLT hook 基本原理


    了解了 ELF 格式和 linker 的 relocation 过程之后,PLT hook 的过程就不言自明了。它做了和 relocation 类似的事情。即:通过符号名,先在 hash table 中找到对应的符号信息(在 .dynsym 中),再找到对应的 PLT 信息(在 .rel.plt.rela.plt.rel.dyn.rela.dyn.rel.dyn.aps2.rela.dyn.aps2 中),最后找到绝对地址信息(在 .got.plt.data.data.rel.ro 中)。最后要做的就是修改这个绝对地址的值,改为我们需要的自己的“代理函数”的地址。


    要注意的是,在修改这个绝对地址之前,需要先用 mprotect 设置当前地址位置所在内存页为“可写”的,因为 linker 在做完 relocation 后会把 .got.plt.data.rel.ro 设置为只读的。修改完之后,需要用 __builtin___clear_cache 来清除该内存位置的 CPU cache,以使修改能立刻生效。


    xHook 的不足之处


    xHook 是一个开源较早的 Android PLT hook 方案,受到了很多的关注。xHook 比较好的实现了 ELF 解析和绝对地址替换的工作。但是作为一个工程化的 PLT hook 方案,xHook 存在很多不足之处,主要有:



    • native 崩溃兜底机制有缺陷,导致线上崩溃无法完全避免。

    • 无法自动对新加载的 ELF 执行 hook。(需要外部反复调用 refresh 来“发现”新加载的 ELF。但是在什么时机调用 refresh 呢?频率太高会影响性能,频率太低会导致 hook 不及时)

    • 由于依赖于链式调用的机制。如果一个调用点被多次 hook,在对某个 proxy 函数执行 unhook 后,链中后续的 proxy 函数就会丢失。

    • 只使用了读 maps 的方式来遍历 ELF。在高版本 Android 系统和部分机型中兼容性不好,经常会发生 hook 不到的情况。

    • API 设计中使用了正则来指定 hook 哪些目标 ELF,运行效率不佳。

    • 需要在真正执行 hook 前,注册完所有的 hook 点,一旦开始执行 hook(调用 refresh 后),不能再添加 hook 点。这种设计是很不友好的。

    • 无法适配 Android 8.0 引入 Linker Namespace 机制(同一个函数符号,在进程中可能存在多个实现)。


    由于存在上述这些稳定性、有效性、功能性上的问题,使 xHook 难以真正大规模的用于线上环境中。


    更完善的 Android PLT hook 方案


    我们迫切需要一个新的更完善的 Android PLT hook 方案,它应该是什么样子的呢?我认为它应该满足这些条件:



    • 要有一套真正可靠的 native 崩溃兜底机制,来避免可控范围内的 native 崩溃。

    • 可以随时 hook 和 unhook 单个、部分、全部的调用者 ELF。

    • 当新的 ELF 被加载到内存后,它应该自动的被执行所有预定的 hook 操作。

    • 多个使用方如果 hook 了同一个调用点,它们应该可以彼此独立的执行 unhook,相互不干扰。

    • 为了适配 Android linker namespace,应该可以指定 hook 函数的被调用者 ELF。

    • 能自动避免由于 hook 引起的意外的“递归调用”和“环形调用”。比如:open 的 proxy 函数中调用了 read,然后 read 的 proxy 函数中又调用了 open。如果这两个 proxy 存在于两个独立的 SDK 中,此时形成的环形调用将很难在 SDK 开发阶段被发现。如果在更多的 SDK 之间形成了一个更大的 proxy 函数调用环,情况将会失去控制。

    • proxy 函数中要能以正常的方式获取 backtrace(libunwind、libunwindstack、llvm libunwind、FP unwind 等)。有大量的业务场景是需要 hook 后在 proxy 函数中抓取和保存 backtrace,然后在特定的时机 dump 和聚合这些 backtrace,符号化后再将数据投递到服务端,从而监控和发现业务问题。

    • hook 管理机制本身带来的额外性能损耗要足够低。


    我们带着上面的这些目标设计和开发了 bhook。


    字节 bhook 介绍


    ELF 和 linker 前面已经介绍过了,下面介绍 bhook 中另外几个关键模块。



    DL monitor


    在 Android 系统中,动态加载 so 库最终是通过 dlopenandroid_dlopen_ext 完成的,通过 dlclose 则可以卸载 so 库。


    bhook 在内部 hook 了这三个函数调用。因此,当有新的 so 被加载到内存后,bhook 能立刻感知到,于是可以立刻对它执行预定的 hook 任务。当有 so 正在被卸载时,bhook 也能立刻感知到,并且会通过内部的读写锁机制与“ELF cache 和 hook 执行模块”同步,以此保证“正在被 hook 的 so 不会正在被卸载”。


    Android 从 7.0 开始不再允许 app 中 dlopen 系统库;从 8.0 开始引入了 linker namespace 机制,并且 libdl.so 不再是 linker 的虚拟入口,而成为了一个真实的 so 文件。对于 linker 来说,Android 7.0 和 8.0 是两个重要的版本。


    我们需要设法绕过系统对 app dlopen 系统库的限制,否则 hook dlopenandroid_dlopen_ext 之后,在代理函数中是无法直接调用原始的 dlopenandroid_dlopen_ext 函数的。


    这里我们参考了 ByteDance Raphael(github.com/bytedance/m… Android 7.0 开始,hook dlopenandroid_dlopen_ext 后不再调用原函数,而是通过调用 linker 和 libdl.so 内部函数的方式绕过了限制。主要用到了以下几个符号对应的内部函数:


    Android 7.x linker:


    __dl__ZL10dlopen_extPKciPK17android_dlextinfoPv
    __dl__Z9do_dlopenPKciPK17android_dlextinfoPv
    __dl__Z23linker_get_error_bufferv
    __dl__ZL23__bionic_format_dlerrorPKcS0_

    Android 8.0+ libdl.so:


    __loader_dlopen
    __loader_android_dlopen_ext

    trampoline


    简单的 PLT hook 方案(比如 xHook)是不需要 trampoline 的,只需要替换 .got.plt(和 .data.data.rel.ro)中的绝对地址就可以了。但是这种方式会导致“同一个 hook 点的多个 proxy 函数形成链式调用”(类似于 Linux 通过 sigaction 注册的 signal handler),如果其中一个 proxy 被 unhook 了,那么“链” 中后续的 proxy 也会丢失。xHook 就存在这个问题:



    当 proxy 1 被 unhook 后,proxy 2 也从调用链上消失了,因为 proxy 1 根本不知道 proxy 2 的存在,在 unhook proxy 1 时,会试图恢复最初的初始值,即 callee 的地址。


    为了解决这个问题,对于每个被 hook 的函数调用点,我们都需要一个对应的管理入口函数,我们改为在 GOT 表中写入这个管理入口函数的地址。同时,对于每个被 hook 的函数调用点,我们还需要维护一个 proxy 函数列表,在管理入口函数中,需要遍历和调用 proxy 函数列表中的每一个具体 proxy 函数。


    为了在运行时达到指定跳转的效果,我们需要用 mmapmprotect 来创建 shellcode。按照术语惯例,我们把这里创建的跳转逻辑称为 trampoline(蹦床):



    另外,为了检测和避免“环形调用”,每次 trampoline 开始执行时,都会开始记录 proxy 函数的执行栈,在 proxy 函数链中遍历执行时,会检测当前待执行的 proxy 函数是否已经在执行栈中出现过,如果出现过,就说明发生了“环形调用”,此时会忽略 proxy 函数链中后续所有的 proxy 函数,直接执行最后的“原函数”。


    trampoline 实现的难点在于性能。trampoline 给执行流程注入了额外的逻辑,在多线程环境中,proxy 调用链会被高频的遍历,其中保存的 proxy 函数可能随时会增加和减少,我们还需要保存 proxy 函数的执行栈。所有这些逻辑都不能加锁,否则 hook 高频函数时,性能损耗会比较明显。


    native 崩溃兜底


    执行 hook 操作时,需要直接计算很多的内存绝对地址,然后对这些内存位置进行读写,但这样做并不总是安全的,我们可能会遇到这些情况:



    • 在 DL monitor 初始化的过程中,对 dlclose 的 hook 尚未完成时,此时 linker 执行了 dlclose,恰恰 dlclose 了我们正在执行 dlclose hook 操作的 ELF。

    • ELF 文件可能意外损坏,导致 linker 加载了格式不正确的 ELF。


    这时候,对指定内存位置的读写可能会发生 sigsegv 或 sigbus,导致 native 崩溃。我们需要一种类似 Java / C++ try-catch 的机制来保护这种危险的操作,避免发生崩溃:


    int *p = NULL;

    TRY(SIGSEGV, SIGBUS) {
    *p = 1;
    } CATCH() {
    LOG("There was a problem, but it's okay.");
    } EXIT

    当崩溃发生时,因为我们明白在保护的代码区间中只有“内存读”或“单个内存写”操作,因此忽略这种崩溃并不会带来任何副作用。在 Java 虚拟机中,也有类似的机制用于检测 native 崩溃,并且创建合适的 Java 异常。


    bhook 通过注册 sigsegv 和 sigbus 信号处理函数来进行 native 崩溃兜底,在 try 块开头用 sigsetjmp 保存寄存器和 sigmask,当发生崩溃时,在信号处理函数中用 siglongjmp 跳转到 catch 块中并恢复 sigmask。


    值得注意的几个问题:



    • ART sigchain 代理了 sigactionsigprocmask 等函数,我们需要用 dlsym 在 libc.so 中找到原始的函数再调用它们。

    • bionic 和 ART sigchain 在某些 AOSP 版本上存在 bug,所以我们需要优先使用 sigaction64sigprocmask64,而不是 sigactionsigprocmask

    • 在正确的地方用正确的方式设置 sigmask 很重要。

    • 我们的 try-catch 机制运行于多线程环境中,所以需要以某种线程独立的方式来保存 sigjmp_buf

    • 考虑到性能和更多使用场景,整个机制需要无锁、无堆内存分配、无 TLS 操作、线程安全,异步信号安全。


    bhook 的 native 崩溃兜底模块经过了比较严格的压力测试和线上测试,如果正确的使用,可以达到预期的效果。如你在 bhook 的源码中所见,我们故意把这个模块设计成现在的样子(只有一个 .c 和 一个 .h 文件,并且没有任何外部依赖),这样做的好处是容易移植和复用。如果你想把这个模块用在自己的工程中,请注意以下几点:



    • native 崩溃兜底属于“高危”操作,可能引起不确定的难以排查的问题。所以能不用尽量不要用。

    • 纯业务类型的 native 库请不要使用 native 崩溃兜底。而是应该让崩溃暴露出来,然后修复问题。

    • try 块中的逻辑越少越好。比如兜底 sigsegv 和 sigbus 时,最好 try 块中只有一些内存地址的读操作和单个写操作,尽量不要调用外部函数(包括 mallocfreenewdelete 等)。

    • try 块中尽量不要使用 C++。某些 C++ 的语法封装,编译器会为它生成一些意外的逻辑(比如读写 C++ TLS 变量,编译器会生成 _emutls_get_address 调用,其中可能会调用 malloc)。

    • 在当前的设计中:try 块中请不要调用 return,否则会跳过 catch 或 exit 块中的回收逻辑,引起难以排查的问题。另外,在 try 块中不可以嵌套使用另一个“相同信号的 try”。
    收起阅读 »

    RecyclerView 添加分割线,ItemDecoration 的实用技巧

    官网解释: An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter'...
    继续阅读 »

    官网解释: An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.


    我的理解:ItemDecoration 允许我们给 recyclerview 中的 item 添加专门的绘制和布局;比如分割线、强调和装饰等等。


    默认 recyclerview 的表现像下面这样


    image.png


    其实我想要的是这样


    image.png


    如果我们不使用这个的话,那么我们在编写 xml 文件的时候只能添加 layout_margin 这样的值,而且即便这样在有些场景下也是不好用的。其实也没关系我们可以使用代码控制,比如在 onBindViewHolder 中根据数据的位置写对应的逻辑,像我上面那种我需要把最后一个数据多对应的 layout_margin 给去掉,这样也是完全没问题的,只不过如果采用了这样的方式,首先如果我们把 layout_margin 设置到每一项上,那么将来要复用这个 xml 文件,由于间距不同,我们就没法复用,或者复用也需要在代码中控制。如果使用这个,就会非常的简单,并且不会在 adapter 中再使用代码控制了。


    使用这个需要进行两步:



    1. 实现自己的 ItemDecoration 子类;

    2. 添加到 recyclerView


    1. 实现自己的 ItemDecoration 子类


    这个类在 androidx.recyclerview.widget.RecyclerView.ItemDecoration 下:


    class ItemSeparatorDecoration: RecyclerView.ItemDecoration()

    这样就实现了,下面我们看看 ItemDecoration 的源代码,我把将要废弃的 API 都删掉:


    abstract class ItemDecoration {
    public void onDraw(Canvas c, RecyclerView parent, State state) {}
    public void onDrawOver(Canvas c, RecyclerView parent, State state) {}
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {}
    }

    发现我们可以重写这三个函数,下面说一下这三个的含义:


    1)void onDraw(Canvas c, RecyclerView parent, State state)


    参数的含义:



    • Canvas c 》 canvas 绘制对象

    • RecyclerView 》 parent RecyclerView 对象本身

    • State state 》 当前 RecyclerView 的状态


    作用就是绘制,可以在任何位置绘制,如果只是想绘制到每一项里面,那么就需要计算出对应的位置。


    2)void onDrawOver(Canvas c, RecyclerView parent, State state)


    跟上面一样,不同的地方在于绘制的总是在最上面,也就是绘制出来的不会被遮挡。


    3)void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)


    参数的含义:



    • Rect outRect 》item 四周的距离对象

    • View view 》 当前 view

    • RecyclerView 》 parent RecyclerView 本身

    • State state 》 RecyclerView 状态


    这里可以设置 itemRecyclerView 各边的距离。这里需要说明一下,我这里说的到各边的距离指的是啥?


    image.png


    2. 实现上面的间隔


    实现间隔是最简单的,因为我们只需要重写 getItemOffsets 函数,这个函数会在绘制每一项的时候调用,所以在这里我们只需要处理每一项的间隔,下面是重写代码,注意这里的单位并不是 dp ,而是 px ,所以如果需要使用 dp 的话,那么就需要自己转换一下,如果你不知道转换可以定义 dpdimen.xml 中,然后直接在代码中获取:


    context.resources.getDimensionPixelSize(R.dimen.test_16dp)

    其中 R.dimen.test_16dp 就是你定义好的值。


    下面看重写的 getItemOffsets 函数:


    override fun getItemOffsets(
    outRect: Rect,
    view: View,
    parent: RecyclerView,
    state: RecyclerView.State
    )
    {
    super.getItemOffsets(outRect, view, parent, state)
    if (parent.getChildLayoutPosition(view) != 0) {
    outRect.top = context.resources.getDimensionPixelSize(R.dimen.test_10dp)
    }
    }

    有没有发现很简单,这样就可以实现上边的效果,只不过最常见的应该还是分割线了。


    3. 实现分割线


    看代码:


    class MyItemDivider(val context: Context, orientation: Int) : RecyclerView.ItemDecoration() {
    companion object {
    // 分割线的 attr
    private val ATTRS = intArrayOf(android.R.attr.listDivider)
    const val HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL
    const val VERTICAL_LIST = LinearLayoutManager.VERTICAL
    }

    // 分割线绘制所需要的 Drawable ,当然也可以直接使用 Canvas 绘制,只不过我这里使用 Drawable
    private var mDivider: Drawable? = null
    private var mOrientation: Int? = null

    init {
    val a = context.obtainStyledAttributes(ATTRS)
    mDivider = a.getDrawable(0)
    a.recycle()
    setOrientation(orientation)
    }

    /**
    * 设置方向,如果是 RecyclerView 是上下方向,那么这里设置 VERTICAL_LIST ,否则设置 HORIZONTAL_LIST
    * @param orientation 方向
    */

    private fun setOrientation(orientation: Int) {
    // 传入的值必须是预先定义好的
    if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
    throw IllegalArgumentException("invalid orientation")
    }
    mOrientation = orientation
    }

    /**
    * 开始绘制,这个函数只会执行一次,
    * 所以我们在绘制的时候需要在这里把所有项的都绘制,
    * 而不是只处理某一项
    */

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDraw(c, parent, state)
    if (mOrientation == VERTICAL_LIST) {
    drawVertical(c, parent)
    } else {
    drawHorizontal(c, parent)
    }
    }

    private fun drawHorizontal(c: Canvas, parent: RecyclerView) {
    val top = parent.paddingTop
    val bottom = parent.height - parent.paddingBottom
    val childCount = parent.childCount
    for (i in 0 until childCount) {
    val child = parent.getChildAt(i)
    val params = child.layoutParams as RecyclerView.LayoutParams
    val left = child.right + params.rightMargin
    val right = left + (mDivider?.intrinsicWidth ?: 0)
    mDivider?.setBounds(left, top, right, bottom)
    mDivider?.draw(c)
    }
    }

    private fun drawVertical(c: Canvas, parent: RecyclerView) {
    // 左边的距离,
    // 意思是左边从哪儿开始绘制,
    // 对于每一项来说,
    // 肯定需要将 RecyclerView 的左边的 paddingLeft 给去掉
    val left = parent.paddingLeft
    // 右边就是 RecyclerView 的宽度减去 RecyclerView 右边设置的 paddingRight 值
    val right = parent.width - parent.paddingRight
    // 获取当前 RecyclerView 下总共有多少 Item
    val childCount = parent.childCount
    // 循环把每一项的都绘制完成,如果最后一项不需要,那么这里的循环就少循环一次
    for (i in 0 until childCount) {
    val child = parent.getChildAt(i)
    val params = child.layoutParams as RecyclerView.LayoutParams
    // 上边的距离就是当前 Item 下边再加上本身设置的 marginBottom
    val top = child.bottom + params.bottomMargin
    // 下边就简单了,就是上边 + 分割线的高度
    val bottom = top + (mDivider?.intrinsicHeight ?: 0)
    mDivider?.setBounds(left, top, right, bottom)
    mDivider?.draw(c)
    }
    }

    // 这个函数会被反复执行,执行的次数跟 Item 的个数相同
    override fun getItemOffsets(
    outRect: Rect,
    view: View,
    parent: RecyclerView,
    state: RecyclerView.State
    )
    {
    super.getItemOffsets(outRect, view, parent, state)
    // 由于在上面的距离绘制,但是实际上那里不会主动为我们绘制腾出空间,
    // 需要重写这个函数来手动调整空间,给上面的绘制不会被覆盖
    if (mOrientation == VERTICAL_LIST) {
    outRect.set(0, 0, 0, mDivider?.intrinsicHeight ?: 0)
    } else {
    outRect.set(0, 0, mDivider?.intrinsicWidth ?: 0, 0)
    }
    }
    }

    代码来源于刘望舒的三部曲,我对代码进行了解释和说明。大家可能在代码中的距离那一块不是很明白,直接看下面的图就很明白的。


    1629513919(1).png 注意 top 我只标注了距离当前 Item 的距离,其实不是,其实是距离最上面的距离,这里这样标注是跟代码保持统一;假如上面的红色方框是我们要画的分割线,那么我们要获取的值对应上面的标注。一般 onDrawgetItemOffsets 要配合使用,如果不的话,那么你绘制的也看不见,即便看见了也是不正常的。原因我在上面讲到了, onDraw 绘制会绘制到 Item 的下面,所以如果没有留足空间的话,那么结果就是看不见绘制的内容。


    内容还会补充,同时关于 RecyclerView 的将来陆续推出,真正做到完全攻略,从使用到问题解决再到源码分析。

    收起阅读 »