注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

初识 Jetpack Compose(五) :组件-Text

一、Text Compose中的Text的作用与 xml 中的TextView无二,作用于最基本的文本显示。 1.属性 @Composable fun Text( // text: String, text: AnnotatedString, ...
继续阅读 »

一、Text


Compose中的Text的作用与 xml 中的TextView无二,作用于最基本的文本显示。


1.属性


@Composable
fun Text(
// text: String,
text: AnnotatedString,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
inlineContent: Map<String, InlineTextContent> = mapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
...
}



























































































属性名类型作用
textString要显示的文本
textAnnotatedString要显示的文本,可设置文本中指定字符的颜色、字体、大小等属性
modifierModifier修饰符
fontSizeTextUnit文本字体大小
fontStyleFontStyle?文本字体变体
fontWeightfontWeight?文本字体字重
fontFamilyFontFamily?文本字体
letterSpacingTextUnit文本字符之间的间距
textDecorationTextDecoration?用于为文本绘制装饰(例如:下划线、删除线)
textAlignTextAlign?文本段落的对齐方式
lineHeightTextUnit文本段落之间的行高
overflowTextOverflow文本字符溢出的显示方式 ,同xml ellipsize
softWrapBoolean文本是否应在换行符处中断。如果为false,则文本的宽度会在水平方向上无限延伸,且textAlign属性失效,可能会出现异常情况
maxLinesInt文本可跨越的可选最大行数,必要时可以换行。如果文本超过给定的行数,则会根据textAlignsoftWrap属性截断文本。它的值必须大于零。
onTextLayout(TextLayoutResult) -> Unit计算新的文本布局时执行的回调
styleTextStyle文本的样式配置,例如颜色,字体,行高等。可参考主题-排版 使用

2.使用示例


2.1 示例一


@Composable
fun TextExampleMoney(value:Float){
var str = value.toString()
if(!str.contains("."))
str+=".00"
var strSplit = str.split(".")

val annotatedStringBuilder = AnnotatedString.Builder()
annotatedStringBuilder.apply {
pushStyle(
SpanStyle(
color = Color.Gray,
fontSize = 16.sp,
)
)
append("¥")
pop()
pushStyle(
SpanStyle(
color = Color.DarkGray,
fontSize = 26.sp,
fontWeight = FontWeight.Bold
)
)
append(strSplit[0])
pop()
pushStyle(
SpanStyle(
color = Color.Gray,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
)
append(".${strSplit[1]}")
pop()
}

Text(
text = annotatedStringBuilder.toAnnotatedString(),
)

}

//调用
TextExampleMoney(98.99f)

1630916637(1).jpg


2.2 示例二


@Composable
fun TextExample(){
val annotatedStringBuilder = AnnotatedString.Builder()
annotatedStringBuilder.apply {
append("Jetpack Compose ")
pushStyle(
SpanStyle(
color = Color.Blue,
fontSize = 16.sp,
)
)
append("widget")
pop()
append(" [ ")
pushStyle(
SpanStyle(
color = Color.Red,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
)
append("Text")
pop()
append(" ] usage example")
}

Text(
text = annotatedStringBuilder.toAnnotatedString(),
fontSize = 16.sp,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)

}

//调用
TextExample()

1630916734(1).jpg


2.3 示例三


Text(
text = "High level element that displays text and provides semantics / accessibility information.\n" +
"\n" +
"The default style uses the LocalTextStyle provided by the MaterialTheme / components. If you are setting your own style, you may want to consider first retrieving LocalTextStyle, and using TextStyle.copy to keep any theme defined attributes, only modifying the specific attributes you want to override.\n" +
"\n" +
"For ease of use, commonly used parameters from TextStyle are also present here. The order of precedence is as follows:\n" +
"\n" +
"If a parameter is explicitly set here (i.e, it is notnull or TextUnit.Unspecified), then this parameter will always be used.\n" +
"\n" +
"If a parameter is not set, (null or TextUnit.Unspecified), then the corresponding value from style will be used instead.\n" +
"\n" +
"Additionally, for color, if color is not set, and style does not have a color, then LocalContentColor will be used with an alpha of LocalContentAlpha- this allows this Text or element containing this Text to adapt to different background colors and still maintain contrast and accessibility.",
fontSize = 16.sp,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
style = MaterialTheme.typography.Description,
maxLines = 20,
lineHeight = 20.sp,
textDecoration = TextDecoration.Underline
)

image.png


二、ClickableText


在一些开发需求中,我们需要监听一个Text的某一段区域的Touch事件,比如:

请阅读并同意《xxx用户使用协议》,我们需要监听书名号内的文字的点击事件并进行跳转,ClickableText可以很轻松的帮我们做到这一点。


@Composable
fun ClickableText(
text: AnnotatedString,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
//点击事件监听,参数为触发事件时点击的字符在文本中的位置
onClick: (Int) -> Unit
) {
...
}

通过源码得知,ClickableText区别于Text外,额外提供了onClick回调,会返回我们点击文本时,手指所点击的字符在文本中的位置。
比如:


ClickableText(
text = annotatedStringBuilder.toAnnotatedString(),
overflow = TextOverflow.Ellipsis
){
Log.d("ClickableText", "$it -> character is clicked.")
}

image.png


预计在未来还会提供LongClickableText:文本的长按事件,不过目前暂未支持。


三、最后


好记性不如烂笔头,初识 Jetpack Compose 系列是我自己的学习笔记,在加深知识巩固的同时,也可以锻炼一下写作技能。文章中的内容仅作参考,如有问题请留言指正。


1. 参考



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

【开源项目】简单易用的Compose版骨架屏,了解一下~

前言 骨架屏是页面的一个空白版本,通常会在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。骨架屏加载中效果,比起传统的加载中效果可以提供更多信息,用户体验更好,因此也变得越来越流行 本文主要介绍如何使用Compose实...
继续阅读 »

前言


骨架屏是页面的一个空白版本,通常会在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。骨架屏加载中效果,比起传统的加载中效果可以提供更多信息,用户体验更好,因此也变得越来越流行

本文主要介绍如何使用Compose实现一个简单易用的骨架屏效果,有兴趣的同学可以点个StarCompose版骨架屏


效果图


首先看下最终的效果图



特性



  1. 简单易用,可复用页面UI,不需要针对骨架屏定制UI

  2. 支持设置骨架屏是否显示,一般结合加载状态使用

  3. 支持设置骨架屏背景与高亮颜色

  4. 支持设置骨架屏高度部分宽度,渐变部分宽度

  5. 支持设置骨架屏动画的角度与方向

  6. 支持设置骨架屏动画的时间与两次动画间隔


使用


接入


第 1 步:在工程的build.gradle中添加:


allprojects {
repositories {
...
mavenCentral()
}
}

第2步:在应用的build.gradle中添加:


dependencies {
implementation 'io.github.shenzhen2017:shimmer:1.0.0'
}

简单使用


@Composable
fun ShimmerSample() {
var loading: Boolean by remember {
mutableStateOf(true)
}
Column(
modifier = Modifier
.fillMaxWidth()
.shimmer(loading,config = ShimmerConfig())
) {
repeat(3) {
PlaceHolderItem()
Spacer(modifier = Modifier.height(10.dp))
}
}
}

如上所示:



  1. 只需要在ColumnModifier中加上shimmerColumn下的所有组件即可实现骨架屏效果

  2. 可通过loading参数,控制骨架屏效果是否显示

  3. 如果需要定制骨架屏动画效果,也可通过一些参数配置


具体主要有以下这些参数


data class ShimmerConfig(
// 未高亮部分颜色
val contentColor: Color = Color.LightGray.copy(alpha = 0.3f),
// 高亮部分颜色
val higLightColor: Color = Color.LightGray.copy(alpha = 0.9f),
// 渐变部分宽度
@FloatRange(from = 0.0, to = 1.0)
val dropOff: Float = 0.5f,
// 高亮部分宽度
@FloatRange(from = 0.0, to = 1.0)
val intensity: Float = 0.2f,
//骨架屏动画方向
val direction: ShimmerDirection = ShimmerDirection.LeftToRight,
//动画旋转角度
val angle: Float = 20f,
//动画时长
val duration: Float = 1000f,
//两次动画间隔
val delay: Float = 200f
)

主要原理


通过图像混合模式复用页面UI


如果我们要实现骨架屏效果,首先想到的是需要按照页面的结构再写一套UI,然后在加载中的时候,显示这套UI,否则隐藏

一般的加载中效果都是这样实现的,但这样会带来一个问题,不同的页面结构不同,那我们岂不是要一个页面就重写一套UI?这显然是不可接受的


我们可以想到,页面的结构其实我们已经写过一遍了,如果我们能复用我们写的页面结构不就好了吗?

我们可以通过图像混合模式来实现这一点


图像混合模式定义的是,当两个图像合成时,图像最终的展示方式。在Androd中,有相应的API接口来支持图像混合模式,即Xfermode.

图像混合模式主要有以下16种,以下这张图片从一定程度上形象地说明了图像混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,具体如下



我们介绍几个常用的,其它的感兴趣的同学可自行查阅



  • SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】

  • DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响

  • SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤

  • DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤


如果我们把页面的UI结构作为目标图像,骨架屏效果作为源图像,然后使用SRC_IN混合模式

就可以实现只在页面的结构上显示骨架屏,在空白部分不显示,这样就可以避免重复写UI


通过平移实现动画效果


上面我们已经实现了在页面结构上显示骨架屏,但是骨架屏效果还有一个动画效果

其实也很简单,给骨架屏设置一个渐变效果,然后做一个平移动画,然后看起来就是现在的骨架屏闪光动画了


fun Modifier.shimmer(): Modifier = composed {
var progress: Float by remember { mutableStateOf(0f) }
val infiniteTransition = rememberInfiniteTransition()
progress = infiniteTransition.animateFloat().value // 动画效果,计算百分比
ShimmerModifier(visible = visible, progress = progress, config = config)
}

internal class ShimmerModifier(progress:Float) : DrawModifier, LayoutModifier {
private val paint = Paint().apply {
blendMode = BlendMode.SrcIn //设置混合模式
shader = LinearGradientShader(Offset(0f, 0f),toOffset,colors,colorStops)//设置渐变色
}

override fun ContentDrawScope.draw() {
drawContent()
val (dx, dy) = getOffset(progress) //根据progress,设置平移的位置
paint.shader?.postTranslate(dx, dy) // 平移操作
it.drawRect(Rect(0f, 0f, size.width, size.height), paint = paint)//绘制骨架屏效果
}
}

如上所示,主要是几步:



  1. 启动动画,获得当前进度progress,并根据progress获得当前平移的位置

  2. 设置骨架屏的背景渐变颜色与混合模式

  3. 绘制骨架屏效果


自定义骨架屏效果


上面介绍了我们提供了一些参数,可以自定义骨架屏的效果,其它参数都比较好理解,主要是以下两个参数有点难理解



  1. dropOff:渐变部分宽度

  2. intensity: 高亮部分宽度


我们知道,可以通过contentColor自定义普通部分颜色,higLightColor自定义高亮部分颜色

但是这两种颜色是如何分布的呢?渐变的比例是怎样的呢?可以看下下面的代码:


    private val paint = Paint().apply {
shader = LinearGradientShader(Offset(0f, 0f),toOffset,colors,colorStops)//设置渐变色
}

private val colors = listOf(
config.contentColor,
config.higLightColor,
config.higLightColor,
config.contentColor
)

private val colorStops: List<Float> = listOf(
((1f - intensity - dropOff) / 2f).coerceIn(0f, 1f),
((1f - intensity - 0.001f) / 2f).coerceIn(0f, 1f),
((1f + intensity + 0.001f) / 2f).coerceIn(0f, 1f),
((1f + intensity + dropOff) / 2f).coerceIn(0f, 1f)
)

可以看出,我们的颜色渐变有以下特点:



  1. 渐变颜色分布为:contentColor->higLightColor->higLightColor->contentColor

  2. LinearGradientShader使用colors定义颜色,colorStops定义颜色渐变的分布,colorStopsintensitydropoff计算得来

  3. intensity决定了高亮部分的宽度,即intensity越大,高亮部分越大

  4. dropOff决定了渐变部分的宽度,即dropOff越大,渐变部分越大


总结


特别鸣谢


在实现Compose版本骨架屏的过程中,主要借鉴了以下开源框架的思想,有兴趣的同学也可以了解下

Facebook开源的shimmer-android

Habib Kazemi开源的compose-shimmer


项目地址


简单易用的Compose版骨架屏

开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~


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

iOS - swift常用的关键词解释和用法

deinit: 当一个类的实例即将被销毁时,会调用这个方法。class Person { var name:String var age:Int var gender:String deinit {...
继续阅读 »

deinit: 当一个类的实例即将被销毁时,会调用这个方法。

class Person  
{
var name:String
var age:Int
var gender:String

deinit
{
//从堆中释放,并释放的资源
}
}

extension:允许给已有的类、结构体、枚举、协议类型,添加新功能。

class Person  
{
var name:String = ""
var age:Int = 0
var gender:String = ""
}

extension Person
{
func printInfo()
{
print("My name is \(name), I'm \(age) years old and I'm a \(gender).")
}
}

inout:将一个值传入函数,并可以被函数修改,然后将值传回到调用处,来替换初始值。适用于引用类型和值类型。 其实就是声明参数为指针

func dangerousOp(_ error:inout NSError?)  
{
error = NSError(domain: "", code: 0, userInfo: ["":""])
}

var potentialError:NSError?
dangerousOp(&potentialError)

//代码运行到这里,potentialError 不再是 nil,而是已经被初始化

internal:访问控制权限,允许同一个模块下的所有源文件访问,如果在不同模块下则不允许访问。


public:可以被任何人访问。但其他 module 中不可以被 override 和继承,而在 module 内可以被 override 和继承。


open:访问控制权限,允许在定义的模块外也可以访问源文件里的所有类,并进行子类化。对于类成员,允许在定义的模块之外访问和重写。

open var foo:String? //这个属性允许在 app 内或 app 外重写和访问。在开发框架的时候,会应用到这个访问修饰符。

private:访问控制权限,只允许实体在定义的类以及相同源文件内的 extension 中访问。

class Person  
{
private var jobTitle:String = ""
}

// 当 extension 和 class 不在同一个源文件时
extension Person
{
// 无法编译通过,只有在同一个源文件下才可以访问
func printJobTitle()
{
print("My job is \(jobTitle)")
}
}

fileprivate:访问控制权限,只允许在定义源文件中访问。// 同文件中访问

class Person  
{
fileprivate var jobTitle:String = ""
}

extension Person
{
//当 extension 和 class 在同一个文件中时,允许访问
func printJobTitle()
{
print("My job is (jobTitle)")
}
}


从高到低排序如下:

open > public > interal > fileprivate > private



static:用于定义类方法,在类型本身进行调用。此外还可以定义静态成员。

class Person  
{
var jobTitle:String?

static func assignRandomName(_ aPerson:Person)
{
aPerson.jobTitle = "Some random job"
}
}

let somePerson = Person()
Person.assignRandomName(somePerson)
//somePerson.jobTitle 的值是 "Some random job"


在方法的 func 关键字之前加上关键字 static 或者 class 都可以用于指定类方法.不同的是用class关键字指定的类方法可以被子类重写, 如下:

override class func work() { print("Teacher: University Teacher")}

但是用 static 关键字指定的类方法是不能被子类重写的, 根据报错信息: Class method overrides a 'final' class method.



我们可以知道被 static 指定的类方法包含 final 关键字的特性--防止被重写.


struct:通用、灵活的结构体,是程序的基础组成部分,并提供了默认初始化方法。与 class 不同,当 struct 在代码中被传递时,是被拷贝的,并不使用引用计数。除此之外,struct 没有下面的这些功能:



  • 使用继承。

  • 运行时的类型转换。

  • 使用析构方法。



数据类型:struct是值类型,class是引用类型。

值类型变量直接包含数据,赋值时也是值拷贝,或者叫深拷贝,所以多个变量的操作不会相互影响。

引用类型变量存储的是对数据的引用地址,后者称为对象,赋值时,是将对象的引用地址复制过去,也叫浅拷贝,因此若多个变量指向同一个对象时,操作会相互影响。

值类型数据没有引用计数,也就不会因为循环引用导致内存泄漏,而引用类型存在引用计数,需要小心循环引用导致的内存泄漏

拷贝时,struct是深拷贝,拷贝的是内容,class则需要选用正确的深浅拷贝类型。



因为值类型数据是深拷贝,所以是线程安全的,而引用类型数据则不是



  • property的初始化:初始化属性时,class 需要创建一个带形参的constructor;struct可以把属性放在默认的constructor 的参数里。

  • immutable变量:swift用var和let区分可变数据和不可变数据,struct遵循这个特性;对class则不适用。

  • mutating function:struct 的 function 改变 property 时,需加上 mutating,而 class 不用。

  • 速度:struct分配在栈中,class分配在堆中,也就意味着struct更迅速。

  • NSUserDefaults:struct 不能被序列化成 NSData 对象,class可以。

  • 继承: struct不可以继承,class可以继承。

  • swift与oc混合开发时,oc调用swift需要继承NSObject,这就导致了class可以继承,所以可以调用class,但struct不能继承,所以不能调用struct


typealias:给代码中已经存在的类,取别名。

typealias JSONDictionary = [String: AnyObject]

func parseJSON(_ deserializedData:JSONDictionary){}

defer:用于在程序离开当前作用域之前,执行一段代码。 // dafer 是倒叙 先加入后执行




  • 关闭文件

    func foo() {
    let fileDescriptor = open(url.path, O_EVTONLY)
    defer {
    close(fileDescriptor)
    }
    // use fileDescriptor...
    }



  • 加/解锁:下面是 swift 里类似 Objective-C 的 synchronized block 的一种写法,可以使用任何一个 NSObject 作 lock

    func foo() {
    objc_sync_enter(lock)
    defer {
    print("003")
    objc_sync_exit(lock)
    }
    defer {
    print("002")
    }
    print("001")
    // do something...
    }



defer 的执行时机:

defer 的执行时机紧接在离开作用域之后,但是是在其他语句之前。这个特性为 defer 带来了一些很“微妙”的使用方式。比如从 0 开始的自增:

class Foo {
var num = 0
func foo() -> Int {
defer { num += 1 }
return num
}

// 没有 `defer` 的话我们可能要这么写
// func foo() -> Int {
// num += 1
// return num - 1
// }
}

let f = Foo()
f.foo() // 0
f.foo() // 1
f.num // 2

fallthrough:显式地允许从当前 case 跳转到下一个相邻 case 继续执行代码。

let box = 1

switch box
{
case 0:
print("Box equals 0")
fallthrough
case 1:
print("Box equals 0 or 1")
default:
print("Box doesn't equal 0 or 1")
// Box equals 0 和 Box equals 0 or 1 都执行了
}

guard:当有一个以上的条件不满足要求时,将离开当前作用域。同时还提供解包可选类型的功能。

private func printRecordFromLastName(userLastName: String?)
{
guard let name = userLastName, name != "Null" else
{
//userLastName = "Null",需要提前退出
return
}
//继续执行代码
print(dataStore.findByLastName(name))
}


1.guard关键字必须使用在函数中。

2.guard关键字必须和else同时出现。

3.guard关键字只有条件为false的时候才能走else语句 相反执行后边语句。



repeat:在使用循环的判断条件之前,先执行一次循环中的代码。类似于 do while 循环


repeat

{

print("Always executes at least once before the condition is considered")

}

while 1 > 2


where:要求关联类型必须遵守特定协议,或者类型参数和关联类型必须保持一致。也可以用于在 case 中提供额外条件,用于满足控制表达式。



  • 增加判断条件
for i in 0…3 where i % 2 == 0  
{
print(i) //打印 0 和 2
}


  • 协议使用where, 只有基类实现了当前协议才能添加扩展。 换个说法, 多个类实现了同一个协议,该语法根据类名分别为这些类添加扩展, 注意是分别(以类名区分)!!!
protocol SomeProtocol {
func someMethod()
}
class A: SomeProtocol {
let a = 1
func someMethod() {
print("call someMethod")
}
}
class B {
let a = 2
}
//基类A继承了SomeProtocol协议才能添加扩展
extension SomeProtocol where Self: A {
func showParamA() {
print(self.a)
}
}
//反例,不符合where条件
extension SomeProtocol where Self: B {
func showParamA() {
print(self.a)
}
}
let objA = A()
let objB = B() //类B没实现SomeProtocol, 所有没有协议方法
objA.showParamA() //输出1

as:类型转换运算符,用于尝试将值转成其它类型。




  • as : 数值类型转换

    let age = 28 as Int
    let money = 20 as CGFloat
    let cost = (50 / 2) as Double
    switch person1 { 
    case let person1 as Student:
    print("是Student类型,打印学生成绩单...")
    case let person1 as Teacher:
    print("是Teacher类型,打印老师工资单...")
    default: break
    }



  • as!:向下转型(Downcasting)时使用。由于是强制类型转换,如果转换失败会报 runtime 运行错误。

    let a = 13 as! String
    print(a)
    //会crash
    let a = 13 as? String
    print(a)
    //输出为nil



is:类型检查运算符,用于确定实例是否为某个子类类型。

class Person {}  
class Programmer : Person {}
class Nurse : Person {}

let people = [Programmer(), Nurse()]

for aPerson in people
{
if aPerson is Programmer
{
print("This person is a dev")
}
else if aPerson is Nurse
{
print("This person is a nurse")
}
}

nil:在 Swift 中表示任意类型的无状态值。



Swift的nil和OC中的nil不一样.在OC中,nil是一个指向不存在对象的指针.而在Swift中,nil不是指针,它是一个不确定的值.用来表示值缺失.任何类型的optional都可以被设置为nil. 而在OC中,基本数据类型和结构体是不能被设置为nil的. 给optional的常量或者变量赋值为nil.来表示他们的值缺失情况.一个optional常量或者变量如果在初始化的时候没有被赋值,他们自动会设置成nil.

class Person{}  
struct Place{}

//任何 Swift 类型或实例可以为 nil
var statelessPerson:Person? = nil
var statelessPlace:Place? = nil
var statelessInt:Int? = nil
var statelessString:String? = nil

super:在子类中,暴露父类的方法、属性、下标。

class Person  
{
func printName()
{
print("Printing a name. ")
}
}

class Programmer : Person
{
override func printName()
{
super.printName()
print("Hello World!")
}
}

let aDev = Programmer()
aDev.printName() //打印 Printing a name. Hello World!

self:任何类型的实例都拥有的隐式属性,等同于实例本身。此外还可以用于区分函数参数和成员属性名称相同的情况。

class Person  
{
func printSelf()
{
print("This is me: \(self)")
}
}

let aPerson = Person()
aPerson.printSelf() //打印 "This is me: Person"

Self:在协议中,表示遵守当前协议的实体类型。

protocol Printable  
{
func printTypeTwice(otherMe:Self)
}

struct Foo : Printable
{
func printTypeTwice(otherMe: Foo)
{
print("I am me plus \(otherMe)")
}
}

let aFoo = Foo()
let anotherFoo = Foo()

aFoo.printTypeTwice(otherMe: anotherFoo) //打印 I am me plus Foo()

_:用于匹配或省略任意值的通配符。

for _ in 0..<3  
{
print("Just loop 3 times, index has no meaning")
}

另外一种用法:

let _ = Singleton() //忽略不使用的变量

convenience:


在 Swift 中,为保证安全性,init 方法只能调用一次,且在 init 完成后,保证所有非 Optional 的属性都已经被初始化。


每个类都有指定的初始化方法:designated initializer,这些初始化方法是子类必须调用的,为的就是保证父类的属性都初始化完成了。


而如果不想实现父类的 designated initializer,可以添加 convenience 关键字,自己实现初始化逻辑。

convenience 初始化不能调用父类的初始化方法,只能调用同一个类中的 designated initializer。

由于 convenience 初始化不安全,所以 Swift 不允许 convenience initializer 被子类重写,限制其作用范围。

class People {
var name: String
init(name: String) {
self.name = name
}
}

通过extension给原有的People类增加init方法:

// 使用convenience增加init方法
extension People {
convenience init(smallName: String) {
self.init(name: smallName)
}
}

接下来,Student类继承父类People

class Student: People {
var grade: Int

init(name: String, grade: Int) {
self.grade = grade
super.init(name: name)
// 无法调用
// super.init(smallName: name)
}

// 可以被重写
override init(name: String) {
grade = 1
super.init(name: name)
}

// 无法重写,编译不通过
override init(smallName: String) {
grade = 1
super.init(smallName: smallName)
}
}

子类对象调用父类的convenience的init方法:只要在子类中实现重写了父类convenience方法所需要的init方法的话,我们在子类中就可以使用父类的convenience初始化方法了

class People {

var name: String

init(name: String) {
self.name = name
}
}
// 使用convenience增加init方法
extension People {
convenience init(smallName: String) {
self.init(name: smallName)
}
}


// 子类
class Teacher: People {

var course: String

init(name: String, course: String) {
self.course = course
super.init(name: name)
}

override init(name: String) {
self.course = "math"
super.init(name: name)
}
}

// 调用convenience的init方法
let xiaoming = Teacher(smallName: "xiaoming")


  • 总结:子类的designated初始化方法必须调用父类的designated方法,以保证父类也完成初始化。


required


对于某些我们希望子类中一定实现的designated初始化方法,我们可以通过添加required关键字进行限制,强制子类对这个方法重写。

required修饰符的使用规则:



  • required修饰符只能用于修饰类初始化方法。

  • 当子类含有异于父类的初始化方法时(初始化方法参数类型和数量异于父类),子类必须要实现父类的required初始化方法,并且也要使用required修饰符而不是override。

  • 当子类没有初始化方法时,可以不用实现父类的required初始化方法。
    class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}
class MySubClass:MyClass
{
init(i:Int) {
super.init(str:String(i))
}

}
// 编译错误
MySubClass(i: 123)

会报错,因为你没有实现父类中必须实现的方法。正确的写法:


    class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}
class MySubClass:MyClass
{
init(i:Int) {
super.init(str:String(i))
}
required init(str: String) {
fatalError("init(str:) has not been implemented")
}
}
// 编译错误
MySubClass(i: 123)

从上面的代码中,不难看出子类需要添加异于父类的初始化方法,必须要重写有required的修饰符的初始化方法,并且也要使用required修饰符而不是override,请千万注意!


如果子类中并没有不同于父类的初始化方法,Swift会默认使用父类的初始化方法:

class MyClass{
var str: String?
required init(str: String?) {
self.str = str
}
}
class MySubClass: MyClass{
}
var MySubClass(str: "hello swift")

在这种情况下,编译器不会报错,因为如果子类没有任何初始化方法时,Swift会默认使用父类的初始化方法。


以#开头的关键字


#available:基于平台参数,通过 if,while,guard 语句的条件,在运行时检查 API 的可用性。

if #available(iOS 10, *)  
{
print("iOS 10 APIs are available")
}

#colorLiteral:在 playground 中使用的字面表达式,用于创建颜色选取器,选取后赋值给变量。

let aColor = #colorLiteral //创建颜色选取器

#column:一种特殊的字面量表达式,用于获取字面量表示式的起始列数。

class Person  
{
func printInfo()
{
print("Some person info - on column \(#column)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on column 47

#function:特殊字面量表达式,返回函数名称。在方法中,返回方法名。在属性的 getter 或者 setter 中,返回属性名。在特殊的成员中,比如 init 或 subscript 中,返回关键字名称。在文件的最顶层时,返回当前所在模块名称。

class Person
{
func printInfo()
{
print("Some person info - inside function \(#function)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - inside function printInfo()

#line:特殊字面量表达式,用于获取当前代码的行数。

class Person  
{
func printInfo()
{
print("Some person info - on line number \(#line)")
}
}

let aPerson = Person()
aPerson.printInfo() //Some person info - on line number 5

#selector:用于创建 Objective-C selector 的表达式,可以静态检查方法是否存在,并暴露给 Objective-C。

//静态检查,确保 doAnObjCMethod 方法存在  
control.sendAction(#selector(doAnObjCMethod), to: target, forEvent: event)

dynamic && @objc


@objc


OC 是基于运行时,遵循了 KVC 和动态派发,而 Swift 为了追求性能,在编译时就已经确定,而不需要在运行时的,在 Swift 类型文件中,为了解决这个问题,需要暴露给 OC 使用的任何地方(类,属性,方法等)的生命前面加上 @objc 修饰符

如果用 Swift 写的 class 是继承 NSObject 的话, Swift 会默认自动为所有非 private 的类和成员加上@objc


在Swift中,我们在给button添加点击事件时,对应的点击事件的触发方法就需要用@objc来修饰


dynamic


Swift 中的函数可以是静态调用,静态调用会更快。当函数是静态调用的时候,就不能从字符串查找到对应的方法地址了。这样 Swift 跟 Objective-C 交互时,Objective-C 动态查找方法地址,就有可能找不到 Swift 中定义的方法。


这样就需要在 Swift 中添加一个提示关键字,告诉编译器这个方法是可能被动态调用的,需要将其添加到查找表中。这个就是关键字 dynamic 的作用。


didSet


属性观察者,当值存储到属性后马上调用。

var data = [1,2,3]  
{
didSet
{
tableView.reloadData()
}
}

final


防止方法、属性、下标被重写。

final class Person {}  
class Programmer : Person {} //编译错误


get


返回成员的值。还可以用在计算型属性上,间接获取其它属性的值。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}


infix


指明一个用于两个值之间的运算符。如果一个全新的全局运算符被定义为 infix,还需要指定优先级。

let twoIntsAdded = 2 + 3


indirect


指明在枚举类型中,存在成员使用相同枚举类型的实例作为关联值的情况。

indirect enum Entertainment  
{
case eventType(String)
case oneEvent(Entertainment)
case twoEvents(Entertainment, Entertainment)
}

let dinner = Entertainment.eventType("Dinner")
let movie = Entertainment.eventType("Movie")

let dateNight = Entertainment.twoEvents(dinner, movie)


lazy


指明属性的初始值,直到第一次被使用时,才进行初始化。

class Person  
{
lazy var personalityTraits = {
//昂贵的数据库开销
return ["Nice", "Funny"]
}()
}
let aPerson = Person()
aPerson.personalityTraits //当 personalityTraits 首次被访问时,数据库才开始工作

left


指明运算符的结合性是从左到右。在没有使用大括号时,可以用于正确判断同一优先级运算符的执行顺序。

//"-" 运算符的结合性是从左到右
10-2-4 //根据结合性,可以看做 (10-2) - 4


mutating


允许在方法中修改结构体或者枚举实例的属性值。

struct Person  
{
var job = ""

mutating func assignJob(newJob:String)
{
self = Person(job: newJob)
}
}

var aPerson = Person()
aPerson.job //""

aPerson.assignJob(newJob: "iOS Engineer at Buffer")
aPerson.job //iOS Engineer at Buffer


none


是一个没有结合性的运算符。不允许这样的运算符相邻出现。

//"<" 是非结合性的运算符
1 < 2 < 3 //编译失败


nonmutating


指明成员的 setter 方法不会修改实例的值,但可能会有其它后果。

enum Paygrade  
{
case Junior, Middle, Senior, Master

var experiencePay:String?
{
get
{
database.payForGrade(String(describing:self))
}

nonmutating set
{
if let newPay = newValue
{
database.editPayForGrade(String(describing:self), newSalary:newPay)
}
}
}
}

let currentPay = Paygrade.Middle

//将 Middle pay 更新为 45k, 但不会修改 experiencePay 值
currentPay.experiencePay = "$45,000"

optional


用于指明协议中的可选方法。遵守该协议的实体类可以不实现这个方法。

@objc protocol Foo  
{
func requiredFunction()
@objc optional func optionalFunction()
}

class Person : Foo
{
func requiredFunction()
{
print("Conformance is now valid")
}
}

override


指明子类会提供自定义实现,覆盖父类的实例方法、类型方法、实例属性、类型属性、下标。如果没有实现,则会直接继承自父类。

class Person  
{
func printInfo()
{
print("I'm just a person!")
}
}

class Programmer : Person
{
override func printInfo()
{
print("I'm a person who is a dev!")
}
}

let aPerson = Person()
let aDev = Programmer()

aPerson.printInfo() //打印 I'm just a person!
aDev.printInfo() //打印 I'm a person who is a dev!

postfix


位于值后面的运算符。

var optionalStr:String? = "Optional"  
print(optionalStr!)

precedence


指明某个运算符的优先级高于别的运算符,从而被优先使用。

infix operator ~ { associativity right precedence 140 }  
4 ~ 8

prefix


位于值前面的运算符。


var anInt = 2  
anInt = -anInt //anInt 等于 -2


required


确保编译器会检查该类的所有子类,全部实现了指定的构造器方法。

class Person  
{
var name:String?

required init(_ name:String)
{
self.name = name
}
}

class Programmer : Person
{
//如果不实现这个方法,编译不会通过
required init(_ name: String)
{
super.init(name)
}
}

right


指明运算符的结合性是从右到左的。在没有使用大括号时,可以用于正确判断同一优先级运算符的顺序。

//"??" 运算符结合性是从右到左
var box:Int?
var sol:Int? = 2

let foo:Int = box ?? sol ?? 0 //Foo 等于 2


set


通过获取的新值来设置成员的值。同样可以用于计算型属性来间接设置其它属性。如果计算型属性的 setter 没有定义新值的名称,可以使用默认的 newValue。

class Person  
{
var name:String
{
get { return self.name }
set { self.name = newValue}
}

var indirectSetName:String
{
get
{
if let aFullTitle = self.fullTitle
{
return aFullTitle
}
return ""
}

set (newTitle)
{
//如果没有定义 newTitle,可以使用 newValue
self.fullTitle = "(self.name) :(newTitle)"
}
}
}


Type


表示任意类型的类型,包括类类型、结构类型、枚举类型、协议类型。

class Person {}  
class Programmer : Person {}

let aDev:Programmer.Type = Programmer.self

unowned


让循环引用中的实例 A 不要强引用实例 B。前提条件是实例 B 的生命周期要长于 A 实例。

class Person  
{
var occupation:Job?
}

//当 Person 实例不存在时,job 也不会存在。job 的生命周期取决于持有它的 Person。
class Job
{
unowned let employee:Person

init(with employee:Person)
{
self.employee = employee
}
}

weak


允许循环引用中的实例 A 弱引用实例 B ,而不是强引用。实例 B 的生命周期更短,并会被先释放。

class Person  
{
var residence:House?
}

class House
{
weak var occupant:Person?
}

var me:Person? = Person()
var myHome:House? = House()

me!.residence = myHome
myHome!.occupant = me

me = nil
myHome!.occupant // myHome 等于 nil


willSet


属性观察者,在值存储到属性之前调用。

class Person  
{
var name:String?
{
willSet(newValue) {print("I've got a new name, it's (newValue)!")}
}
}

let aPerson = Person()
aPerson.name = "Jordan" //在赋值之前,打印 "I've got a new name, it's Jordan!"



链接:https://www.jianshu.com/p/46cf5c77dee7
收起阅读 »

iOS 常用技巧

1、递归查看 view 的子视图(私有方法,没有代码提示)[self.view recursiveDescription] 2、// 定义一个特殊字符的集合 NSCharacterSet *set = [NSCharacterSet characterSet...
继续阅读 »

1、递归查看 view 的子视图(私有方法,没有代码提示)

[self.view recursiveDescription]

2、

// 定义一个特殊字符的集合
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:
@"@/:;()¥「」"、[]{}#%-*+=_\\|~<>$€^•'@#$%^&*()_+'\""];
// 过滤字符串的特殊字符
NSString *newString = [trimString stringByTrimmingCharactersInSet:set];

3、Transform 属性

//平移按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformTranslate(transForm, 10, 0);

//旋转按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformRotate(transForm, M_PI_4);

//缩放按钮
self.buttonView.transform = CGAffineTransformScale(transForm, 1.2, 1.2);

//初始化复位
self.buttonView.transform = CGAffineTransformIdentity;

4、去掉分割线多余15pt

首先在viewDidLoad方法加入以下代码:
if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
if ([self.tableView respondsToSelector:@selector(setLayoutMargins:)]) {
[self.tableView setLayoutMargins:UIEdgeInsetsZero];
}
然后在重写willDisplayCell方法
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath{
if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
[cell setSeparatorInset:UIEdgeInsetsZero];
}
if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}
}

5、计算耗时方法时间间隔

// 获取时间间隔
#define TICK CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
#define TOCK NSLog(@"Time: %f", CFAbsoluteTimeGetCurrent() - start)

6、Color颜色宏定义

// 随机颜色
#define RANDOM_COLOR [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1]
// 颜色(RGB)
#define RGBCOLOR(r, g, b) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1]
// 利用这种方法设置颜色和透明值,可不影响子视图背景色
#define RGBACOLOR(r, g, b, a) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:(a)]

7、退出iOS应用

- (void)exitApplication {
AppDelegate *app = [UIApplication sharedApplication].delegate;
UIWindow *window = app.window;

[UIView animateWithDuration:1.0f animations:^{
window.alpha = 0;
} completion:^(BOOL finished) {
exit(0);
}];
}

8、NSArray 快速求总和 最大值 最小值 和 平均值

NSArray *array = [NSArray arrayWithObjects:@"2.0", @"2.3", @"3.0", @"4.0", @"10", nil];
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
NSLog(@"%f\n%f\n%f\n%f",sum,avg,max,min);

9、Debug栏打印时自动把Unicode编码转化成汉字


 DXXcodeConsoleUnicodePlugin 插件

10、自动生成模型代码的插件

ESJsonFormat-for-Xcode

11、设置滑动的时候隐藏navigationbar

self.navigationController.hidesBarsOnSwipe = YES

12、隐藏导航栏上的返回字体

//Swift
UIBarButtonItem.appearance().setBackButtonTitlePositionAdjustment(UIOffsetMake(0, -60), forBarMetrics: .Default)
//OC
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];

13、设置导航栏透明

//方法一:设置透明度
[[[self.navigationController.navigationBar subviews]objectAtIndex:0] setAlpha:0.1];
//方法二:设置背景图片
/**
* 设置导航栏,使其透明
*
*/
- (void)setNavigationBarColor:(UIColor *)color targetController:(UIViewController *)targetViewController{
//导航条的颜色 以及隐藏导航条的颜色targetViewController.navigationController.navigationBar.shadowImage = [[UIImage alloc]init];
CGRect rect=CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, [color CGColor]); CGContextFillRect(context, rect);
UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [targetViewController.navigationController.navigationBar setBackgroundImage:theImage forBarMetrics:UIBarMetricsDefault];
}

14、解决同时按两个按钮进两个view的问题

[button setExclusiveTouch:YES];

15、修改 textFieldplaceholder 字体颜色和大小

  textField.placeholder = @"请输入手机号码";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:13] forKeyPath:@"_placeholderLabel.font"];

16、UIImage 与字符串互转

//图片转字符串  
-(NSString *)UIImageToBase64Str:(UIImage *) image
{
NSData *data = UIImageJPEGRepresentation(image, 1.0f);
NSString *encodedImageStr = [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
return encodedImageStr;
}

//字符串转图片
-(UIImage *)Base64StrToUIImage:(NSString *)_encodedImageStr
{
NSData *_decodedImageData = [[NSData alloc] initWithBase64Encoding:_encodedImageStr];
UIImage *_decodedImage = [UIImage imageWithData:_decodedImageData];
return _decodedImage;
}


链接:https://www.jianshu.com/p/adb3ebf5354c 收起阅读 »

iOS - 图层性能 二

混合和过度绘制    在第12章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可...
继续阅读 »

混合和过度绘制

    在第12章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可能发生了。

    GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:

  • 给视图的backgroundColor属性设置一个固定的,不透明的颜色
  • 设置opaque属性为YES

    这样做减少了混合行为(因为编译器知道在图层之后的东西都不会对最终的像素颜色产生影响)并且计算得到了加速,避免了过度绘制行为因为Core Animation可以舍弃所有被完全遮盖住的图层,而不用每个像素都去计算一遍。

    如果用到了图像,尽量避免透明除非非常必要。如果图像要显示在一个固定的背景颜色或是固定的背景图之前,你没必要相对前景移动,你只需要预填充背景图片就可以避免运行时混色了。

    如果是文本的话,一个白色背景的UILabel(或者其他颜色)会比透明背景要更高效。

    最后,明智地使用shouldRasterize属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了。

减少图层数量

    初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大致资源开销。事实上,一次性能够在屏幕上显示的最大图层数量也是有限的。

    确切的限制数量取决于iOS设备,图层类型,图层内容和属性等。但是总得说来可以容纳上百或上千个,下面我们将演示即使图层本身并没有做什么也会遇到的性能问题。

裁切

    在对图层做任何优化之前,你需要确定你不是在创建一些不可见的图层,图层在以下几种情况下回事不可见的:

  • 图层在屏幕边界之外,或是在父图层边界之外。
  • 完全在一个不透明图层之后。
  • 完全透明

    Core Animation非常擅长处理对视觉效果无意义的图层。但是经常性地,你自己的代码会比Core Animation更早地想知道一个图层是否是有用的。理想状况下,在图层对象在创建之前就想知道,以避免创建和配置不必要图层的额外工作。

    举个例子。清单15.3 的代码展示了一个简单的滚动3D图层矩阵。这看上去很酷,尤其是图层在移动的时候(见图15.1),但是绘制他们并不是很麻烦,因为这些图层就是一些简单的矩形色块。

清单15.3 绘制3D图层矩阵

#import "ViewController.h"
#import

#define WIDTH 10
#define HEIGHT 10
#define DEPTH 10
#define SIZE 100
#define SPACING 150
#define CAMERA_DISTANCE 500

@interface ViewController ()

@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);

//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;

//create layers
for (int z = DEPTH - 1; z >= 0; z--) {
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
//create layer
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE);
layer.position = CGPointMake(x*SPACING, y*SPACING);
layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[self.scrollView.layer addSublayer:layer];
}
}
}

//log
NSLog(@"displayed: %i", DEPTH*HEIGHT*WIDTH);
}
@end

图15.1 滚动的3D图层矩阵

    WIDTHHEIGHTDEPTH常量控制着图层的生成。在这个情况下,我们得到的是10*10*10个图层,总量为1000个,不过一次性显示在屏幕上的大约就几百个。

    如果把WIDTHHEIGHT常量增加到100,我们的程序就会慢得像龟爬了。这样我们有了100000个图层,性能下降一点儿也不奇怪。

    但是显示在屏幕上的图层数量并没有增加,那么根本没有额外的东西需要绘制。程序慢下来的原因其实是因为在管理这些图层上花掉了不少功夫。他们大部分对渲染的最终结果没有贡献,但是在丢弃这么图层之前,Core Animation要强制计算每个图层的位置,就这样,我们的帧率就慢了下来。

    我们的图层是被安排在一个均匀的栅格中,我们可以计算出哪些图层会被最终显示在屏幕上,根本不需要对每个图层的位置进行计算。这个计算并不简单,因为我们还要考虑到透视的问题。如果我们直接这样做了,Core Animation就不用费神了。

    既然这样,让我们来重构我们的代码吧。改造后,随着视图的滚动动态地实例化图层而不是事先都分配好。这样,在创造他们之前,我们就可以计算出是否需要他。接着,我们增加一些代码去计算可视区域这样就可以排除区域之外的图层了。清单15.4是改造后的结果。

清单15.4 排除可视区域之外的图层

#import "ViewController.h"
#import

#define WIDTH 100
#define HEIGHT 100
#define DEPTH 10
#define SIZE 100
#define SPACING 150
#define CAMERA_DISTANCE 500
#define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE)

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;
}

- (void)viewDidLayoutSubviews
{
[self updateLayers];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateLayers];
}

- (void)updateLayers
{
//calculate clipping bounds
CGRect bounds = self.scrollView.bounds;
bounds.origin = self.scrollView.contentOffset;
bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
//create layers
NSMutableArray *visibleLayers = [NSMutableArray array];
for (int z = DEPTH - 1; z >= 0; z--)
{
//increase bounds size to compensate for perspective
CGRect adjusted = bounds;
adjusted.size.width /= PERSPECTIVE(z*SPACING);
adjusted.size.height /= PERSPECTIVE(z*SPACING);
adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2;
adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
for (int y = 0; y < HEIGHT; y++) {
//check if vertically outside visible rect
if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height)
{
continue;
}
for (int x = 0; x < WIDTH; x++) {
//check if horizontally outside visible rect
if (x*SPACING < adjusted.origin.x ||x*SPACING >= adjusted.origin.x + adjusted.size.width)
{
continue;
}

//create layer
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE);
layer.position = CGPointMake(x*SPACING, y*SPACING);
layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[visibleLayers addObject:layer];
}
}
}
//update layers
self.scrollView.layer.sublayers = visibleLayers;
//log
NSLog(@"displayed: %i/%i", [visibleLayers count], DEPTH*HEIGHT*WIDTH);
}
@end

    这个计算机制并不具有普适性,但是原则上是一样。(当你用一个UITableView或者UICollectionView时,系统做了类似的事情)。这样做的结果?我们的程序可以处理成百上千个『虚拟』图层而且完全没有性能问题!因为它不需要一次性实例化几百个图层。

对象回收

    处理巨大数量的相似视图或图层时还有一个技巧就是回收他们。对象回收在iOS颇为常见;UITableViewUICollectionView都有用到,MKMapView中的动画pin码也有用到,还有其他很多例子。

    对象回收的基础原则就是你需要创建一个相似对象池。当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。每次当你需要一个实例时,你就从池中取出一个。当且仅当池中为空时再创建一个新的。

    这样做的好处在于避免了不断创建和释放对象(相当消耗资源,因为涉及到内存的分配和销毁)而且也不必给相似实例重复赋值。

    好了,让我们再次更新代码吧(见清单15.5)

清单15.5 通过回收减少不必要的分配

@interface ViewController () 

@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
@property (nonatomic, strong) NSMutableSet *recyclePool;


@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad]; //create recycle pool
self.recyclePool = [NSMutableSet set];
//set content size
self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
//set up perspective transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / CAMERA_DISTANCE;
self.scrollView.layer.sublayerTransform = transform;
}

- (void)viewDidLayoutSubviews
{
[self updateLayers];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateLayers];
}

- (void)updateLayers {

//calculate clipping bounds
CGRect bounds = self.scrollView.bounds;
bounds.origin = self.scrollView.contentOffset;
bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
//add existing layers to pool
[self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers];
//disable animation
[CATransaction begin];
[CATransaction setDisableActions:YES];
//create layers
NSInteger recycled = 0;
NSMutableArray *visibleLayers = [NSMutableArray array];
for (int z = DEPTH - 1; z >= 0; z--)
{
//increase bounds size to compensate for perspective
CGRect adjusted = bounds;
adjusted.size.width /= PERSPECTIVE(z*SPACING);
adjusted.size.height /= PERSPECTIVE(z*SPACING);
adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
for (int y = 0; y < HEIGHT; y++) {
//check if vertically outside visible rect
if (y*SPACING < adjusted.origin.y ||
y*SPACING >= adjusted.origin.y + adjusted.size.height)
{
continue;
}
for (int x = 0; x < WIDTH; x++) {
//check if horizontally outside visible rect
if (x*SPACING < adjusted.origin.x ||
x*SPACING >= adjusted.origin.x + adjusted.size.width)
{
continue;
}
//recycle layer if available
CALayer *layer = [self.recyclePool anyObject]; if (layer)
{

recycled ++;
[self.recyclePool removeObject:layer]; }
else
{
layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, SIZE, SIZE); }
//set position
layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING;
//set background color
layer.backgroundColor =
[UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
//attach to scroll view
[visibleLayers addObject:layer]; }
} }
[CATransaction commit]; //update layers
self.scrollView.layer.sublayers = visibleLayers;
//log
NSLog(@"displayed: %i/%i recycled: %i",
[visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled);
}
@end

    本例中,我们只有图层对象这一种类型,但是UIKit有时候用一个标识符字符串来区分存储在不同对象池中的不同的可回收对象类型。

    你可能注意到当设置图层属性时我们用了一个CATransaction来抑制动画效果。在之前并不需要这样做,因为在显示之前我们给所有图层设置一次属性。但是既然图层正在被回收,禁止隐式动画就有必要了,不然当属性值改变时,图层的隐式动画就会被触发。

Core Graphics绘制

    当排除掉对屏幕显示没有任何贡献的图层或者视图之后,长远看来,你可能仍然需要减少图层的数量。例如,如果你正在使用多个UILabel或者UIImageView实例去显示固定内容,你可以把他们全部替换成一个单独的视图,然后用-drawRect:方法绘制出那些复杂的视图层级。

    这个提议看上去并不合理因为大家都知道软件绘制行为要比GPU合成要慢而且还需要更多的内存空间,但是在因为图层数量而使得性能受限的情况下,软件绘制很可能提高性能呢,因为它避免了图层分配和操作问题。

    你可以自己实验一下这个情况,它包含了性能和栅格化的权衡,但是意味着你可以从图层树上去掉子图层(用shouldRasterize,与完全遮挡图层相反)。

-renderInContext: 方法

    用Core Graphics去绘制一个静态布局有时候会比用层级的UIView实例来得快,但是使用UIView实例要简单得多而且比用手写代码写出相同效果要可靠得多,更边说Interface Builder来得直接明了。为了性能而舍弃这些便利实在是不应该。

    幸好,你不必这样,如果大量的视图或者图层真的关联到了屏幕上将会是一个大问题。没有与图层树相关联的图层不会被送到渲染引擎,也没有性能问题(在他们被创建和配置之后)。

    使用CALayer-renderInContext:方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在UIImageView中,或者作为另一个图层的contents。不同于shouldRasterize —— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。

    当图层内容改变时,刷新这张图片的机会取决于你(不同于shouldRasterize,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。

总结

    本章学习了使用Core Animation图层可能遇到的性能瓶颈,并讨论了如何避免或减小压力。你学习了如何管理包含上千虚拟图层的场景(事实上只创建了几百个)。同时也学习了一些有用的技巧,选择性地选取光栅化或者绘制图层内容在合适的时候重新分配给CPU和GPU。这些就是我们要讲的关于Core Animation的全部了(至少可以等到苹果发明什么新的玩意儿)。


收起阅读 »

巧用CSS filter,让你的网站更加酷炫!

前言 我们在处理图片时,经常使用的一个功能就是滤镜,它能使一张图像呈现各种不同的视觉效果。 在 CSS 中,也有一个filter属性,让我们能用 CSS 代码为元素指定各种滤镜效果,比如模糊、灰度、明暗度、颜色偏移等。 CSS filter的基础使用非常简单...
继续阅读 »

前言


我们在处理图片时,经常使用的一个功能就是滤镜,它能使一张图像呈现各种不同的视觉效果。


image.png


在 CSS 中,也有一个filter属性,让我们能用 CSS 代码为元素指定各种滤镜效果,比如模糊、灰度、明暗度、颜色偏移等。


CSS filter的基础使用非常简单,CSS 标准里包含了一些已实现预定义效果的函数(下面blur、brightness、contrast等),我们可以通过指定这些函数的值来实现想要的效果:


/* 使用单个滤镜 (如果传入的参数是百分数,那么也可以传入对应的小数:40% --> 0.4)*/
filter: blur(5px);
filter: brightness(40%);
filter: contrast(200%);
filter: drop-shadow(16px 16px 20px blue);
filter: grayscale(50%);
filter: hue-rotate(90deg);
filter: invert(75%);
filter: opacity(25%);
filter: saturate(30%);
filter: sepia(60%);

/* 使用多个滤镜 */
filter: contrast(175%) brightness(3%);

/* 不使用任何滤镜 */
filter: none;

官方demo:MDN


filter-demo.gif


滤镜在日常开发中是很常见的,比如使用drop-shadow给不规则形状添加阴影;使用blur来实现背景模糊,以及毛玻璃效果等。


下面我们将进一步使用CSS filter实现一些动画效果,让网站交互更加酷炫,同时也加深对CSS filter的理解。一起开始吧!


( 下面要使用到的 动画 和 伪类 知识,在 CSS的N个编码技巧 中都有详细的介绍,这里就不重复了,有需要的朋友可以前往查看哦。 )


电影效果


滤镜中的brightness用于调整图像的明暗度。默认值是1;小于1时图像变暗,为0时显示为全黑图像;大于1时图像显示比原图更明亮。


我们可以通过调整 背景图的明暗度文字的透明度 ,来模拟电影谢幕的效果。


movie.gif


<div>
<div></div>
<div>
<p>如果生活中有什么使你感到快乐,那就去做吧</p>
<br>
<p>不要管别人说什么</p>
</div>
</div>

.pic{
height: 100%;
width: 100%;
position: absolute;
background: url('./images/movie.webp') no-repeat;
background-size: cover;
animation: fade-away 2.5s linear forwards; //forwards当动画完成后,保持最后一帧的状态
}
.text{
position: absolute;
line-height: 55px;
color: #fff;
font-size: 36px;
text-align: center;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
opacity: 0;
animation: show 2s cubic-bezier(.74,-0.1,.86,.83) forwards;
}

@keyframes fade-away { //背景图的明暗度动画
30%{
filter: brightness(1);
}
100%{
filter: brightness(0);
}
}
@keyframes show{ //文字的透明度动画
20%{
opacity: 0;
}
100%{
opacity: 1;
}
}

模糊效果


在下面的单词卡片中,当鼠标hover到某一张卡片上时,其他卡片背景模糊,使用户焦点集中到当前卡片。


card-blur.gif


html结构:


<ul>
<li>
<p>Flower</p>
<p>The flowers mingle to form a blaze of color.</p>
</li>
<li>
<p>Sunset</p>
<p>The sunset glow tinted the sky red.</p>
</li>
<li>
<p>Plain</p>
<p>The winds came from the north, across the plains, funnelling down the valley. </p>
</li>
</ul>

实现的方式,是将背景加在.card元素的伪类上,当元素不是焦点时,为该元素的伪类加上滤镜。


.card:before{
z-index: -1;
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: 20px;
filter: blur(0px) opacity(1);
transition: filter 200ms linear, transform 200ms linear;
}
/*
这里不能将滤镜直接加在.card元素,而是将背景和滤镜都加在伪类上。
因为,父元素加了滤镜,它的子元素都会一起由该滤镜改变。
如果滤镜直接加在.card元素上,会导致上面的文字也变模糊。
*/

//通过css选择器选出非hover的.card元素,给其伪类添加模糊、透明度和明暗度的滤镜 

.cards:hover > .card:not(:hover):before{
filter: blur(5px) opacity(0.8) brightness(0.8);
}

//对于hover的元素,其伪类增强饱和度,尺寸放大

.card:hover:before{
filter: saturate(1.2);
transform: scale(1.05);
}

褪色效果


褪色效果可以打造出一种怀旧的风格。下面这组照片墙,我们通过sepia滤镜将图像基调转换为深褐色,再通过降低 饱和度saturate 和 色相旋转hue-rotate 微调,模拟老照片的效果。


old-photo-s.gif


.pic{
border: 3px solid #fff;
box-shadow: 0 10px 50px #5f2f1182;
filter: sepia(30%) saturate(40%) hue-rotate(5deg);
transition: transform 1s;
}
.pic:hover{
filter: none;
transform: scale(1.2) translateX(10px);
z-index: 1;
}

灰度效果


怎样让网站变成灰色?在html元素上加上filter: grayscale(100%)即可。


grayscale(amount)函数将改变输入图像灰度。amount 的值定义了灰度转换的比例。值为 100% 则完全转为灰度图像,值为 0% 图像无变化。若未设置值,默认值是 0


gray-scale.gif


融合效果


要使两个相交的元素产生下面这种融合的效果,需要用到的滤镜是blurcontrast


merge.gif


<div>
<div></div>
<div></div>
</div>

.container{
margin: 50px auto;
height: 140px;
width: 400px;
background: #fff; //给融合元素的父元素设置背景色
display: flex;
align-items: center;
justify-content: center;
filter: contrast(30); //给融合元素的父元素设置contrast
}
.circle{
border-radius: 50%;
position: absolute;
filter: blur(10px); //给融合元素设置blur
}
.circle-1{
height: 90px;
width: 90px;
background: #03a9f4;
transform: translate(-50px);
animation: 2s moving linear infinite alternate-reverse;
}
.circle-2{
height: 60px;
width: 60px;
background: #0000ff;
transform: translate(50px);
animation: 2s moving linear infinite alternate;
}
@keyframes moving { //两个元素的移动
0%{
transform: translate(50px)
}
100%{
transform: translate(-50px)
}
}

实现融合效果的技术要点:



  1. contrast滤镜应用在融合元素的父元素(.container)上,且父元素必须设置background

  2. blur滤镜应用在融合元素(.circle)上。


blur设置图像的模糊程度,contrast设置图像的对比度。当两者像上面那样组合时,就会产生神奇的融合效果,你可以像使用公式一样使用这种写法。


在这种融合效果的基础上,我们可以做一些有趣的交互设计。



  • 加载动画:


loading-l.gif
htmlcss如下所示,这个动画主要通过控制子元素.circle的尺寸和位移来实现,但是由于父元素和子元素都满足 “融合公式” ,所以当子元素相交时,就出现了融合的效果。


<div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>

.container {
margin: 10px auto;
height: 140px;
width: 300px;
background: #fff; //父元素设置背景色
display: flex;
align-items: center;
filter: contrast(30); //父元素设置contrast
}
.circle {
height: 50px;
width: 60px;
background: #1aa7ff;
border-radius: 50%;
position: absolute;
filter: blur(20px); //子元素设置blur
transform: scale(0.1);
transform-origin: left top;
}
.circle{
animation: move 4s cubic-bezier(.44,.79,.83,.96) infinite;
}
.circle:nth-child(2) {
animation-delay: .4s;
}
.circle:nth-child(3) {
animation-delay: .8s;
}
.circle:nth-child(4) {
animation-delay: 1.2s;
}
.circle:nth-child(5) {
animation-delay: 1.6s;
}
@keyframes move{ //子元素的位移和尺寸动画
0%{
transform: translateX(10px) scale(0.3);
}
45%{
transform: translateX(135px) scale(0.8);
}
85%{
transform: translateX(270px) scale(0.1);
}
}


  • 酷炫的文字出场方式:


gooey-text.gif
主要通过不断改变letter-spacingblur的值,使文字从融合到分开:


<div>
<span>fantastic</span>
</div>

.container{
margin-top: 50px;
text-align: center;
background-color: #000;
filter: contrast(30);
}
.text{
font-size: 100px;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
letter-spacing: -40px;
color: #fff;
animation: move-letter 4s linear forwards; //forwards当动画完成后,保持最后一帧的状态
}
@keyframes move-letter{
0% {
opacity: 0;
letter-spacing: -40px;
filter: blur(10px);
}
25% {
opacity: 1;
}
50% {
filter: blur(5px);
}
100% {
letter-spacing: 20px;
filter: blur(2px);
}
}

水波效果


filter还可以通过 URL 链接到 SVG 滤镜元素,SVG滤镜元素MDN 。


下面的水波纹效果就是基于 SVG 的feTurbulence滤镜实现的,原理参考了 说说SVG的feTurbulence滤镜

SVG feTurbulence滤镜深入介绍,有兴趣的朋友可以深入阅读。



feTurbulence滤镜借助Perlin噪声算法模拟自然界真实事物那样的随机样式。它接收下面5个属性:



  • baseFrequency表示噪声的基本频率参数,频率越高,噪声越密集。

  • numOctaves就表示倍频的数量,倍频的数量越多,噪声看起来越自然。

  • seed属性表示feTurbulence滤镜效果中伪随机数生成的起始值,不同数量的seed不会改变噪声的频率和密度,改变的是噪声的形状和位置。

  • stitchTiles定义了Perlin噪声在边框处的行为表现。

  • type属性值有fractalNoiseturbulence,模拟随机样式使用turbulence



wave.gif


在这个例子,两个img标签使用同一张图片,将第二个img标签使用scaleY(-1)实现垂直方向的镜像翻转,模拟倒影。


并且,对倒影图片使用feTurbulence滤镜,通过动画不断改变feTurbulence滤镜的baseFrequency值实现水纹波动的效果。


<div>
<img src="images/moon.jpg">
<img src="images/moon.jpg">
</div>

<!--定义svg滤镜,这里使用的是feTurbulence滤镜-->
<svg width="0" height="0">
<filter id="displacement-wave-filter">

<!--baseFrequency设置0.01 0.09两个值,代表x轴和y轴的噪声频率-->
<feTurbulence baseFrequency="0.01 0.09">

<!--这是svg动画的定义方式,通过动画不断改变baseFrequency的值,从而形成波动效果-->
<animate attributeName="baseFrequency"
dur="20s" keyTimes="0;0.5;1" values="0.01 0.09;0.02 0.13;0.01 0.09"
repeatCount="indefinite" ></animate>

</feTurbulence>
<feDisplacementMap in="SourceGraphic" scale="10" />
</filter>
</svg>

.container{
height: 520px;
width: 400px;
display: flex;
clip-path: inset(10px);
flex-direction: column;
}
img{
height: 50%;
width: 100%;
}
.reflect {
transform: translateY(-2px) scaleY(-1);
//对模拟倒影的元素应用svg filter
//url中对应的是上面svg filter的id
filter: url(#displacement-wave-filter);
}

抖动效果


在上面的水波动画中改变的是baseFrequency值,我们也通过改变seed的值,实现文字的抖动效果。
text-shaking.gif


<div>
<p>Such a joyful night!</p>
</div>
<svg width="0" height="0">
<filter id="displacement-text-filter">

<!--定义feTurbulence滤镜-->
<feTurbulence baseFrequency="0.02" seed="0">

<!--这是svg动画的定义方式,通过动画不断改变seed的值,形成抖动效果-->
<animate attributeName="seed"
dur="1s" keyTimes="0;0.5;1" values="1;2;3"
repeatCount="indefinite" ></animate>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" scale="10" />
</filter>
</svg>

.shaky{
font-size: 60px;
filter: url(#displacement-text-filter); //url中对应的是上面svg filter的id
}

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

收起阅读 »

iOS - 图层性能 一

隐式绘制    寄宿图可以通过Core Graphics直接绘制,也可以直接载入一个图片文件并赋值给contents属性,或事先绘制一个屏幕之外的CGContext上下文。在之前的两章中我们讨论了这些场景下的优化。但是除...
继续阅读 »

隐式绘制

    寄宿图可以通过Core Graphics直接绘制,也可以直接载入一个图片文件并赋值给contents属性,或事先绘制一个屏幕之外的CGContext上下文。在之前的两章中我们讨论了这些场景下的优化。但是除了常见的显式创建寄宿图,你也可以通过以下三种方式创建隐式的:1,使用特性的图层属性。2,特定的视图。3,特定的图层子类。

    了解这个情况为什么发生何时发生是很重要的,它能够让你避免引入不必要的软件绘制行为。

文本

    CATextLayerUILabel都是直接将文本绘制在图层的寄宿图中。事实上这两种方式用了完全不同的渲染方式:在iOS 6及之前,UILabel用WebKit的HTML渲染引擎来绘制文本,而CATextLayer用的是Core Text.后者渲染更迅速,所以在所有需要绘制大量文本的情形下都优先使用它吧。但是这两种方法都用了软件的方式绘制,因此他们实际上要比硬件加速合成方式要慢。

    不论如何,尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。例如,如果你想在图层的角落里显示一段静态的文本,但是这个图层经常改动,你就应该把文本放在一个子图层中。

光栅化

    在第四章『视觉效果』中我们提到了CALayershouldRasterize属性,它可以解决重叠透明图层的混合失灵问题。同样在第12章『速度的曲调』中,它也是作为绘制复杂图层树结构的优化方法。

    启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。

    当我们使用得当时,光栅化可以提供很大的性能优势(如你在第12章所见),但是一定要避免作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。

    为了检测你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新(这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。

离屏渲染

    当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制:

  • 圆角(当和maskToBounds一起使用时)
  • 图层蒙板
  • 阴影

    屏幕外渲染和我们启用光栅化时相似,除了它并没有像光栅化图层那么消耗大,子图层并没有被影响到,而且结果也没有被缓存,所以不会有长期的内存占用。但是,如果太多图层在屏幕外渲染依然会影响到性能。

    有时候我们可以把那些需要屏幕外绘制的图层开启光栅化以作为一个优化方式,前提是这些图层并不会被频繁地重绘。

    对于那些需要动画而且要在屏幕外渲染的图层来说,你可以用CAShapeLayercontentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。

CAShapeLayer

    cornerRadiusmaskToBounds独立作用的时候都不会有太大的性能问题,但是当他俩结合在一起,就触发了屏幕外渲染。有时候你想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不需要沿着圆角裁切,这个情况下用CAShapeLayer就可以避免这个问题了。

    你想要的只是圆角且沿着矩形边界裁切,同时还不希望引起性能问题。其实你可以用现成的UIBezierPath的构造器+bezierPathWithRoundedRect:cornerRadius:(见清单15.1).这样做并不会比直接用cornerRadius更快,但是它避免了性能问题。

清单15.1 用CAShapeLayer画一个圆角矩形

#import "ViewController.h"
#import

@interface ViewController ()

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

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//create shape layer
CAShapeLayer *blueLayer = [CAShapeLayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.fillColor = [UIColor blueColor].CGColor;
blueLayer.path = [UIBezierPath bezierPathWithRoundedRect:
CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath;

//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}
@end

可伸缩图片

    另一个创建圆角矩形的方法就是用一个圆形内容图片并结合第二章『寄宿图』提到的contensCenter属性去创建一个可伸缩图片(见清单15.2).理论上来说,这个应该比用CAShapeLayer要快,因为一个可拉伸图片只需要18个三角形(一个图片是由一个3*3网格渲染而成),然而,许多都需要渲染成一个顺滑的曲线。在实际应用上,二者并没有太大的区别。

清单15.2 用可伸缩图片绘制圆角矩形

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

//create layer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0);
blueLayer.contentsScale = [UIScreen mainScreen].scale;
blueLayer.contents = (__bridge id)[UIImage imageNamed:@"Circle.png"].CGImage;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}
@end

    使用可伸缩图片的优势在于它可以绘制成任意边框效果而不需要额外的性能消耗。举个例子,可伸缩图片甚至还可以显示出矩形阴影的效果。

shadowPath

    在第2章我们有提到shadowPath属性。如果图层是一个简单几何图形如矩形或者圆角矩形(假设不包含任何透明部分或者子图层),创建出一个对应形状的阴影路径就比较容易,而且Core Animation绘制这个阴影也相当简单,避免了屏幕外的图层部分的预排版需求。这对性能来说很有帮助。

    如果你的图层是一个更复杂的图形,生成正确的阴影路径可能就比较难了,这样子的话你可以考虑用绘图软件预先生成一个阴影背景图。

收起阅读 »

iOS - 图像IO 三

文件格式    图片加载性能取决于加载大图的时间和解压小图时间的权衡。很多苹果的文档都说PNG是iOS所有图片加载的最好格式。但这是极度误导的过时信息了。    PNG图片使用的无...
继续阅读 »

文件格式

    图片加载性能取决于加载大图的时间和解压小图时间的权衡。很多苹果的文档都说PNG是iOS所有图片加载的最好格式。但这是极度误导的过时信息了。

    PNG图片使用的无损压缩算法可以比使用JPEG的图片做到更快地解压,但是由于闪存访问的原因,这些加载的时间并没有什么区别。

    清单14.6展示了标准的应用程序加载不同尺寸图片所需要时间的一些代码。为了保证实验的准确性,我们会测量每张图片的加载和绘制时间来确保考虑到解压性能的因素。另外每隔一秒重复加载和绘制图片,这样就可以取到平均时间,使得结果更加准确。

清单14.6

#import "ViewController.h"

static NSString *const ImageFolder = @"Coast Photos";

@interface ViewController ()

@property (nonatomic, copy) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//set up image names
self.items = @[@"2048x1536", @"1024x768", @"512x384", @"256x192", @"128x96", @"64x48", @"32x24"];
}

- (CFTimeInterval)loadImageForOneSec:(NSString *)path
{
//create drawing context to use for decompression
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
//start timing
NSInteger imagesLoaded = 0;
CFTimeInterval endTime = 0;
CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
while (endTime - startTime < 1) {
//load image
UIImage *image = [UIImage imageWithContentsOfFile:path];
//decompress image by drawing it
[image drawAtPoint:CGPointZero];
//update totals
imagesLoaded ++;
endTime = CFAbsoluteTimeGetCurrent();
}
//close context
UIGraphicsEndImageContext();
//calculate time per image
return (endTime - startTime) / imagesLoaded;
}

- (void)loadImageAtIndex:(NSUInteger)index
{
//load on background thread so as not to
//prevent the UI from updating between runs dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//setup
NSString *fileName = self.items[index];
NSString *pngPath = [[NSBundle mainBundle] pathForResource:filename
ofType:@"png"
inDirectory:ImageFolder];
NSString *jpgPath = [[NSBundle mainBundle] pathForResource:filename
ofType:@"jpg"
inDirectory:ImageFolder];
//load
NSInteger pngTime = [self loadImageForOneSec:pngPath] * 1000;
NSInteger jpgTime = [self loadImageForOneSec:jpgPath] * 1000;
//updated UI on main thread
dispatch_async(dispatch_get_main_queue(), ^{
//find table cell and update
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.detailTextLabel.text = [NSString stringWithFormat:@"PNG: ims JPG: ims", pngTime, jpgTime];
});
});
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleValue1 reuseIdentifier:@"Cell"];
}
//set up cell
NSString *imageName = self.items[indexPath.row];
cell.textLabel.text = imageName;
cell.detailTextLabel.text = @"Loading...";
//load image
[self loadImageAtIndex:indexPath.row];
return cell;
}

@end

    PNG和JPEG压缩算法作用于两种不同的图片类型:JPEG对于噪点大的图片效果很好;但是PNG更适合于扁平颜色,锋利的线条或者一些渐变色的图片。为了让测评的基准更加公平,我们用一些不同的图片来做实验:一张照片和一张彩虹色的渐变。JPEG版本的图片都用默认的Photoshop60%“高质量”设置编码。结果见图片14.5。

图14.5

图14.5 不同类型图片的相对加载性能

    如结果所示,相对于不友好的PNG图片,相同像素的JPEG图片总是比PNG加载更快,除非一些非常小的图片、但对于友好的PNG图片,一些中大尺寸的图效果还是很好的。

    所以对于之前的图片传送器程序来说,JPEG会是个不错的选择。如果用JPEG的话,一些多线程和缓存策略都没必要了。

    但JPEG图片并不是所有情况都适用。如果图片需要一些透明效果,或者压缩之后细节损耗很多,那就该考虑用别的格式了。苹果在iOS系统中对PNG和JPEG都做了一些优化,所以普通情况下都应该用这种格式。也就是说在一些特殊的情况下才应该使用别的格式。

混合图片

    对于包含透明的图片来说,最好是使用压缩透明通道的PNG图片和压缩RGB部分的JPEG图片混合起来加载。这就对任何格式都适用了,而且无论从质量还是文件尺寸还是加载性能来说都和PNG和JPEG的图片相近。相关分别加载颜色和遮罩图片并在运行时合成的代码见14.7。

清单14.7 从PNG遮罩和JPEG创建的混合图片

#import "ViewController.h"

@interface ViewController ()

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

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//load color image
UIImage *image = [UIImage imageNamed:@"Snowman.jpg"];
//load mask image
UIImage *mask = [UIImage imageNamed:@"SnowmanMask.png"];
//convert mask to correct format
CGColorSpaceRef graySpace = CGColorSpaceCreateDeviceGray();
CGImageRef maskRef = CGImageCreateCopyWithColorSpace(mask.CGImage, graySpace);
CGColorSpaceRelease(graySpace);
//combine images
CGImageRef resultRef = CGImageCreateWithMask(image.CGImage, maskRef);
UIImage *result = [UIImage imageWithCGImage:resultRef];
CGImageRelease(resultRef);
CGImageRelease(maskRef);
//display result
self.imageView.image = result;
}

@end

    对每张图片都使用两个独立的文件确实有些累赘。JPNG的库(https://github.com/nicklockwood/JPNG)对这个技术提供了一个开源的可以复用的实现,并且添加了直接使用+imageNamed:+imageWithContentsOfFile:方法的支持。

JPEG 2000

    除了JPEG和PNG之外iOS还支持别的一些格式,例如TIFF和GIF,但是由于他们质量压缩得更厉害,性能比JPEG和PNG糟糕的多,所以大多数情况并不用考虑。

    但是iOS之后,苹果低调添加了对JPEG 2000图片格式的支持,所以大多数人并不知道。它甚至并不被Xcode很好的支持 - JPEG 2000图片都没在Interface Builder中显示。

    但是JPEG 2000图片在(设备和模拟器)运行时会有效,而且比JPEG质量更好,同样也对透明通道有很好的支持。但是JPEG 2000图片在加载和显示图片方面明显要比PNG和JPEG慢得多,所以对图片大小比运行效率更敏感的时候,使用它是一个不错的选择。

    但仍然要对JPEG 2000保持关注,因为在后续iOS版本说不定就对它的性能做提升,但是在现阶段,混合图片对更小尺寸和质量的文件性能会更好。

PVRTC

    当前市场的每个iOS设备都使用了Imagination Technologies PowerVR图像芯片作为GPU。PowerVR芯片支持一种叫做PVRTC(PowerVR Texture Compression)的标准图片压缩。

    和iOS上可用的大多数图片格式不同,PVRTC不用提前解压就可以被直接绘制到屏幕上。这意味着在加载图片之后不需要有解压操作,所以内存中的图片比其他图片格式大大减少了(这取决于压缩设置,大概只有1/60那么大)。

    但是PVRTC仍然有一些弊端:

  • 尽管加载的时候消耗了更少的RAM,PVRTC文件比JPEG要大,有时候甚至比PNG还要大(这取决于具体内容),因为压缩算法是针对于性能,而不是文件尺寸。

  • PVRTC必须要是二维正方形,如果源图片不满足这些要求,那必须要在转换成PVRTC的时候强制拉伸或者填充空白空间。

  • 质量并不是很好,尤其是透明图片。通常看起来更像严重压缩的JPEG文件。

  • PVRTC不能用Core Graphics绘制,也不能在普通的UIImageView显示,也不能直接用作图层的内容。你必须要用作OpenGL纹理加载PVRTC图片,然后映射到一对三角板来在CAEAGLLayer或者GLKView中显示。

  • 创建一个OpenGL纹理来绘制PVRTC图片的开销相当昂贵。除非你想把所有图片绘制到一个相同的上下文,不然这完全不能发挥PVRTC的优势。

  • PVRTC使用了一个不对称的压缩算法。尽管它几乎立即解压,但是压缩过程相当漫长。在一个现代快速的桌面Mac电脑上,它甚至要消耗一分钟甚至更多来生成一个PVRTC大图。因此在iOS设备上最好不要实时生成。

    如果你愿意使用OpehGL,而且即使提前生成图片也能忍受得了,那么PVRTC将会提供相对于别的可用格式来说非常高效的加载性能。比如,可以在主线程1/60秒之内加载并显示一张2048×2048的PVRTC图片(这已经足够大来填充一个视网膜屏幕的iPad了),这就避免了很多使用线程或者缓存等等复杂的技术难度。

    Xcode包含了一些命令行工具例如texturetool来生成PVRTC图片,但是用起来很不方便(它存在于Xcode应用程序束中),而且很受限制。一个更好的方案就是使用Imagination Technologies PVRTexTool,可以从http://www.imgtec.com/powervr/insider/sdkdownloads免费获得。

    安装了PVRTexTool之后,就可以使用如下命令在终端中把一个合适大小的PNG图片转换成PVRTC文件:

/Applications/Imagination/PowerVR/GraphicsSDK/PVRTexTool/CL/OSX_x86/PVRTexToolCL -i {input_file_name}.png -o {output_file_name}.pvr -legacypvr -p -f PVRTC1_4 -q pvrtcbest

    清单14.8的代码展示了加载和显示PVRTC图片的步骤(第6章CAEAGLLayer例子代码改动而来)。

清单14.8 加载和显示PVRTC图片

#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;
@property (nonatomic, strong) GLKTextureInfo *textureInfo;

@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, 0.0);
//set up vertices
GLfloat vertices[] = {
-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f
};
//set up colors
GLfloat texCoords[] = {
0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f
};
//draw triangle
glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, vertices);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, 0, texCoords);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
//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.glLayer.opaque = NO;
[self.glView.layer addSublayer:self.glLayer];
self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking: @NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
//load texture
glActiveTexture(GL_TEXTURE0);
NSString *imageFile = [[NSBundle mainBundle] pathForResource:@"Snowman" ofType:@"pvr"];
self.textureInfo = [GLKTextureLoader textureWithContentsOfFile:imageFile options:nil error:NULL];
//create texture
GLKEffectPropertyTexture *texture = [[GLKEffectPropertyTexture alloc] init];
texture.enabled = YES;
texture.envMode = GLKTextureEnvModeDecal;
texture.name = self.textureInfo.name;
//set up base effect
self.effect = [[GLKBaseEffect alloc] init];
self.effect.texture2d0.name = texture.name;
//set up buffers
[self setUpBuffers];
//draw frame
[self drawFrame];
}

- (void)viewDidUnload
{
[self tearDownBuffers];
[super viewDidUnload];
}

- (void)dealloc
{
[self tearDownBuffers];
[EAGLContext setCurrentContext:nil];
}

@end

    如你所见,非常不容易,如果你对在常规应用中使用PVRTC图片很感兴趣的话(例如基于OpenGL的游戏),可以参考一下GLView的库(https://github.com/nicklockwood/GLView),它提供了一个简单的GLImageView类,重新实现了UIImageView的各种功能,但同时提供了PVRTC图片,而不需要你写任何OpenGL代码。


总结

    在这章中,我们研究了和图片加载解压相关的性能问题,并延展了一系列解决方案。

    在第15章“图层性能”中,我们将讨论和图层渲染和组合相关的性能问题。

收起阅读 »

iOS - 图像IO 二

缓存    如果有很多张图片要显示,最好不要提前把所有都加载进来,而是应该当移出屏幕之后立刻销毁。通过选择性的缓存,你就可以避免来回滚动时图片重复性的加载了。    缓存其实很简单...
继续阅读 »

缓存

    如果有很多张图片要显示,最好不要提前把所有都加载进来,而是应该当移出屏幕之后立刻销毁。通过选择性的缓存,你就可以避免来回滚动时图片重复性的加载了。

    缓存其实很简单:就是存储昂贵计算后的结果(或者是从闪存或者网络加载的文件)在内存中,以便后续使用,这样访问起来很快。问题在于缓存本质上是一个权衡过程 - 为了提升性能而消耗了内存,但是由于内存是一个非常宝贵的资源,所以不能把所有东西都做缓存。

    何时将何物做缓存(做多久)并不总是很明显。幸运的是,大多情况下,iOS都为我们做好了图片的缓存。

+imageNamed:方法

    之前我们提到使用[UIImage imageNamed:]加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。但是[UIImage imageNamed:]方法有另一个非常显著的好处:它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。

    对于iOS应用那些主要的图片(例如图标,按钮和背景图片),使用[UIImage imageNamed:]加载图片是最简单最有效的方式。在nib文件中引用的图片同样也是这个机制,所以你很多时候都在隐式的使用它。

    但是[UIImage imageNamed:]并不适用任何情况。它为用户界面做了优化,但是并不是对应用程序需要显示的所有类型的图片都适用。有些时候你还是要实现自己的缓存机制,原因如下:

  • [UIImage imageNamed:]方法仅仅适用于在应用程序资源束目录下的图片,但是大多数应用的许多图片都要从网络或者是用户的相机中获取,所以[UIImage imageNamed:]就没法用了。

  • [UIImage imageNamed:]缓存用来存储应用界面的图片(按钮,背景等等)。如果对照片这种大图也用这种缓存,那么iOS系统就很可能会移除这些图片来节省内存。那么在切换页面时性能就会下降,因为这些图片都需要重新加载。对传送器的图片使用一个单独的缓存机制就可以把它和应用图片的生命周期解耦。

  • [UIImage imageNamed:]缓存机制并不是公开的,所以你不能很好地控制它。例如,你没法做到检测图片是否在加载之前就做了缓存,不能够设置缓存大小,当图片没用的时候也不能把它从缓存中移除。

自定义缓存

    构建一个所谓的缓存系统非常困难。菲尔 卡尔顿曾经说过:“在计算机科学中只有两件难事:缓存和命名”。

    如果要写自己的图片缓存的话,那该如何实现呢?让我们来看看要涉及哪些方面:

  • 选择一个合适的缓存键 - 缓存键用来做图片的唯一标识。如果实时创建图片,通常不太好生成一个字符串来区分别的图片。在我们的图片传送带例子中就很简单,我们可以用图片的文件名或者表格索引。

  • 提前缓存 - 如果生成和加载数据的代价很大,你可能想当第一次需要用到的时候再去加载和缓存。提前加载的逻辑是应用内在就有的,但是在我们的例子中,这也非常好实现,因为对于一个给定的位置和滚动方向,我们就可以精确地判断出哪一张图片将会出现。

  • 缓存失效 - 如果图片文件发生了变化,怎样才能通知到缓存更新呢?这是个非常困难的问题(就像菲尔 卡尔顿提到的),但是幸运的是当从程序资源加载静态图片的时候并不需要考虑这些。对用户提供的图片来说(可能会被修改或者覆盖),一个比较好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比较。

  • 缓存回收 - 当内存不够的时候,如何判断哪些缓存需要清空呢?这就需要到你写一个合适的算法了。幸运的是,对缓存回收的问题,苹果提供了一个叫做NSCache通用的解决方案

NSCache

    NSCacheNSDictionary类似。你可以通过-setObject:forKey:-object:forKey:方法分别来插入,检索。和字典不同的是,NSCache在系统低内存的时候自动丢弃存储的对象。

    NSCache用来判断何时丢弃对象的算法并没有在文档中给出,但是你可以使用-setCountLimit:方法设置缓存大小,以及-setObject:forKey:cost:来对每个存储的对象指定消耗的值来提供一些暗示。

    指定消耗数值可以用来指定相对的重建成本。如果对大图指定一个大的消耗值,那么缓存就知道这些物体的存储更加昂贵,于是当有大的性能问题的时候才会丢弃这些物体。你也可以用-setTotalCostLimit:方法来指定全体缓存的尺寸。

    NSCache是一个普遍的缓存解决方案,我们创建一个比传送器案例更好的自定义的缓存类。(例如,我们可以基于不同的缓存图片索引和当前中间索引来判断哪些图片需要首先被释放)。但是NSCache对我们当前的缓存需求来说已经足够了;没必要过早做优化。

    使用图片缓存和提前加载的实现来扩展之前的传送器案例,然后来看看是否效果更好(见清单14.5)。

清单14.5 添加缓存

#import "ViewController.h"

@interface ViewController()

@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
//set up data
self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
//register cell class
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}

- (UIImage *)loadImageAtIndex:(NSUInteger)index
{
//set up cache
static NSCache *cache = nil;
if (!cache) {
cache = [[NSCache alloc] init];
}
//if already cached, return immediately
UIImage *image = [cache objectForKey:@(index)];
if (image) {
return [image isKindOfClass:[NSNull class]]? nil: image;
}
//set placeholder to avoid reloading image multiple times
[cache setObject:[NSNull null] forKey:@(index)];
//switch to background thread
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
[image drawAtPoint:CGPointZero];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image for correct image view
dispatch_async(dispatch_get_main_queue(), ^{ //cache the image
[cache setObject:image forKey:@(index)];
//display the image
NSIndexPath *indexPath = [NSIndexPath indexPathForItem: index inSection:0]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
UIImageView *imageView = [cell.contentView.subviews lastObject];
imageView.image = image;
});
});
//not loaded yet
return nil;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
//add image view
UIImageView *imageView = [cell.contentView.subviews lastObject];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds];
imageView.contentMode = UIViewContentModeScaleAspectFit;
[cell.contentView addSubview:imageView];
}
//set or load image for this index
imageView.image = [self loadImageAtIndex:indexPath.item];
//preload image for previous and next index
if (indexPath.item < [self.imagePaths count] - 1) {
[self loadImageAtIndex:indexPath.item + 1]; }
if (indexPath.item > 0) {
[self loadImageAtIndex:indexPath.item - 1]; }
return cell;
}

@end

    果然效果更好了!当滚动的时候虽然还有一些图片进入的延迟,但是已经非常罕见了。缓存意味着我们做了更少的加载。这里提前加载逻辑非常粗暴,其实可以把滑动速度和方向也考虑进来,但这已经比之前没做缓存的版本好很多了。

收起阅读 »

用 JavaScript 做数独

最近看到老婆天天在手机上玩数独,突然想起 N 年前刷 LeetCode 的时候,有个类似的算法题(37.解数独),是不是可以把这个算法进行可视化。 说干就干,经过一个小时的实践,最终效果如下: 怎么解数独 解数独之前,我们先了解一下数独的规则: 数字 1-...
继续阅读 »

最近看到老婆天天在手机上玩数独,突然想起 N 年前刷 LeetCode 的时候,有个类似的算法题(37.解数独),是不是可以把这个算法进行可视化。


说干就干,经过一个小时的实践,最终效果如下:



怎么解数独


解数独之前,我们先了解一下数独的规则:



  1. 数字 1-9 在每一行只能出现一次。

  2. 数字 1-9 在每一列只能出现一次。

  3. 数字 1-9 在每一个以粗实线分隔的九宫格( 3x3 )内只能出现一次。



接下来,我们要做的就是在每个格子里面填一个数字,然后判断这个数字是否违反规定。


填第一个格子


首先,在第一个格子填 1,发现在第一列里面已经存在一个 1,此时就需要擦掉前面填的数字 1,然后在格子里填上 2,发现数字在行、列、九宫格内均无重复。那么这个格子就填成功了。



填第二个格子


下面看第二个格子,和前面一样,先试试填 1,发现在行、列、九宫格内的数字均无重复,那这个格子也填成功了。



填第三个格子


下面看看第三个格子,由于前面两个格子,我们已经填过数字 12,所以,我们直接从数字 3 开始填。填 3 后,发现在第一行里面已经存在一个 3,然后在格子里填上 4,发现数字 4 在行和九宫格内均出现重复,依旧不成功,然后尝试填上数字 5,终于没有了重复数字,表示填充成功。



一直填,直到填到第九个格子


照这个思路,一直填到第九个格子,这个时候,会发现,最后一个数字 9 在九宫格内冲突了。而 9 已经是最后一个数字了,这里没办法填其他数字了,只能返回上一个格子,把第七个格子的数字从 8 换到 9,发现在九宫格内依然冲突。


此时需要替换上上个格子的数字(第六个格子)。直到没有冲突为止,所以在这个过程中,不仅要往后填数字,还要回过头看看前面的数字有没有问题,不停地尝试。



综上所述


解数独就是一个不断尝试的过程,每个格子把数字 1-9 都尝试一遍,如果出现冲突就擦掉这个数字,直到所有的格子都填完。



通过代码来实现


把上面的解法反映到代码上,就需要通过 递归 + 回溯 的思路来实现。


在写代码之前,先看看怎么把数独表示出来,这里参考 leetcode 上的题目:37. 解数独



前面的这个题目,可以使用一个二维数组来表示。最外层数组内一共有 9 个数组,表示数独的 9 行,内部的每个数组内 9 字符分别对应数组的列,未填充的空格通过字符('.' )来表示。


const sudoku = [
['.', '.', '.', '4', '.', '.', '.', '3', '.'],
['7', '.', '4', '8', '.', '.', '1', '.', '2'],
['.', '.', '.', '2', '3', '.', '4', '.', '9'],
['.', '4', '.', '5', '.', '9', '.', '8', '.'],
['5', '.', '.', '.', '.', '.', '9', '1', '3'],
['1', '.', '.', '.', '8', '.', '2', '.', '4'],
['.', '.', '.', '.', '.', '.', '3', '4', '5'],
['.', '5', '1', '9', '4', '.', '7', '2', '.'],
['4', '7', '3', '.', '5', '.', '.', '9', '1'],
]

知道如何表示数组后,我们再来写代码。


const sudoku = [……]
// 方法接受行、列两个参数,用于定位数独的格子
function solve(row, col) {
if (col >= 9) {
// 超过第九列,表示这一行已经结束了,需要另起一行
col = 0
row += 1
if (row >= 9) {
// 另起一行后,超过第九行,则整个数独已经做完
return true
}
}
if (sudoku[row][col] !== '.') {
// 如果该格子已经填过了,填后面的格子
return solve(row, col + 1)
}
// 尝试在该格子中填入数字 1-9
for (let num = 1; num <= 9; num++) {
if (!isValid(row, col, num)) {
// 如果是无效数字,跳过该数字
continue
}
// 填入数字
sudoku[row][col] = num.toString()
// 继续填后面的格子
if (solve(row, col + 1)) {
// 如果一直到最后都没问题,则这个格子的数字没问题
return true
}
// 如果出现了问题,solve 返回了 false
// 说明这个地方要重填
sudoku[row][col] = '.' // 擦除数字
}
// 数字 1-9 都填失败了,说明前面的数字有问题
// 返回 FALSE,进行回溯,前面数字要进行重填
return false
}

上面的代码只是实现了递归、回溯的部分,还有一个 isValid 方法没有实现。该方法主要就是按照数独的规则进行一次校验。


const sudoku = [……]
function isValid(row, col, num) {
// 判断行里是否重复
for (let i = 0; i < 9; i++) {
if (sudoku[row][i] === num) {
return false
}
}
// 判断列里是否重复
for (let i = 0; i < 9; i++) {
if (sudoku[i][col] === num) {
return false
}
}
// 判断九宫格里是否重复
const startRow = parseInt(row / 3) * 3
const startCol = parseInt(col / 3) * 3
for (let i = startRow; i < startRow + 3; i++) {
for (let j = startCol; j < startCol + 3; j++) {
if (sudoku[i][j] === num) {
return false
}
}
}
return true
}

通过上面的代码,我们就能解出一个数独了。


const sudoku = [
['.', '.', '.', '4', '.', '.', '.', '3', '.'],
['7', '.', '4', '8', '.', '.', '1', '.', '2'],
['.', '.', '.', '2', '3', '.', '4', '.', '9'],
['.', '4', '.', '5', '.', '9', '.', '8', '.'],
['5', '.', '.', '.', '.', '.', '9', '1', '3'],
['1', '.', '.', '.', '8', '.', '2', '.', '4'],
['.', '.', '.', '.', '.', '.', '3', '4', '5'],
['.', '5', '1', '9', '4', '.', '7', '2', '.'],
['4', '7', '3', '.', '5', '.', '.', '9', '1']
]
function isValid(row, col, num) {……}
function solve(row, col) {……}
solve(0, 0) // 从第一个格子开始解
console.log(sudoku) // 输出结果

输出结果


动态展示做题过程


有了上面的理论知识,我们就可以把这个做题的过程套到 react 中,动态的展示做题的过程,也就是文章最开始的 Gif 中的那个样子。


这里直接使用 create-react-app 脚手架快速启动一个项目。


npx create-react-app sudoku
cd sudoku

打开 App.jsx ,开始写代码。


import React from 'react';
import './App.css';

class App extends React.Component {
state = {
// 在 state 中配置一个数独二维数组
sudoku: [
['.', '.', '.', '4', '.', '.', '.', '3', '.'],
['7', '.', '4', '8', '.', '.', '1', '.', '2'],
['.', '.', '.', '2', '3', '.', '4', '.', '9'],
['.', '4', '.', '5', '.', '9', '.', '8', '.'],
['5', '.', '.', '.', '.', '.', '9', '1', '3'],
['1', '.', '.', '.', '8', '.', '2', '.', '4'],
['.', '.', '.', '.', '.', '.', '3', '4', '5'],
['.', '5', '1', '9', '4', '.', '7', '2', '.'],
['4', '7', '3', '.', '5', '.', '.', '9', '1']
]
}

// TODO:解数独
solveSudoku = async () => {
const { sudoku } = this.state
}

render() {
const { sudoku } = this.state
return (
<div className="container">
<div className="wrapper">
{/* 遍历二维数组,生成九宫格 */}
{sudoku.map((list, row) => (
{/* div.row 对应数独的行 */}
<div className="row" key={`row-${row}`}>
{list.map((item, col) => (
{/* span 对应数独的每个格子 */}
<span key={`box-${col}`}>{ item !== '.' && item }</span>
))}
</div>
))}
<button onClick={this.solveSudoku}>开始做题</button>
</div>
</div>
);
}
}

九宫格样式


给每个格子加上一个虚线的边框,先让它有一点九宫格的样子。


.row {
display: flex;
direction: row;
/* 行内元素居中 */
justify-content: center;
align-content: center;
}
.row span {
/* 每个格子宽高一致 */
width: 30px;
min-height: 30px;
line-height: 30px;
text-align: center;
/* 设置虚线边框 */
border: 1px dashed #999;
}

可以得到一个这样的图形:



接下来,需要给外边框和每个九宫格加上实线的边框,具体代码如下:


/* 第 1 行顶部加上实现边框 */
.row:nth-child(1) span {
border-top: 3px solid #333;
}
/* 第 3、6、9 行底部加上实现边框 */
.row:nth-child(3n) span {
border-bottom: 3px solid #333;
}
/* 第 1 列左边加上实现边框 */
.row span:first-child {
border-left: 3px solid #333;
}

/* 第 3、6、9 列右边加上实现边框 */
.row span:nth-child(3n) {
border-right: 3px solid #333;
}

这里会发现第三、六列的右边边框和第四、七列的左边边框会有点重叠,第三、六行的底部边框和第四、七行的顶部边框也会有这个问题,所以,我们还需要将第四、七列的左边边框和第三、六行的底部边框进行隐藏。



.row:nth-child(3n + 1) span {
border-top: none;
}
.row span:nth-child(3n + 1) {
border-left: none;
}

做题逻辑


样式写好后,就可以继续完善做题的逻辑了。


class App extends React.Component {
state = {
// 在 state 中配置一个数独二维数组
sudoku: [……]
}

solveSudoku = async () => {
const { sudoku } = this.state
// 判断填入的数字是否有效,参考上面的代码,这里不再重复
const isValid = (row, col, num) => {
……
}
// 递归+回溯的方式进行解题
const solve = async (row, col) => {
if (col >= 9) {
col = 0
row += 1
if (row >= 9) return true
}
if (sudoku[row][col] !== '.') {
return solve(row, col + 1)
}
for (let num = 1; num <= 9; num++) {
if (!isValid(row, col, num)) {
continue
}

sudoku[row][col] = num.toString()
this.setState({ sudoku }) // 填了格子之后,需要同步到 state

if (solve(row, col + 1)) {
return true
}

sudoku[row][col] = '.'
this.setState({ sudoku }) // 填了格子之后,需要同步到 state
}
return false
}
// 进行解题
solve(0, 0)
}

render() {
const { sudoku } = this.state
return (……)
}
}

对比之前的逻辑,这里只是在对数独的二维数组填空后,调用了 this.setStatesudoku 同步到了 state 中。


function solve(row, col) {   ……   sudoku[row][col] = num.toString()+  this.setState({ sudoku })	 ……   sudoku[row][col] = '.'+  this.setState({ sudoku }) // 填了格子之后,需要同步到 state}

在调用 solveSudoku 后,发现并没有出现动态的效果,而是直接一步到位的将结果同步到了视图中。



这是因为 setState 是一个伪异步调用,在一个事件任务中,所有的 setState 都会被合并成一次,需要看到动态的做题过程,我们需要将每一次 setState 操作放到该事件流之外,也就是放到 setTimeout 中。更多关于 setState 异步的问题,可以参考我之前的文章:React 中 setState 是一个宏任务还是微任务?


solveSudoku = async () => {
const { sudoku } = this.state
// 判断填入的数字是否有效,参考上面的代码,这里不再重复
const isValid = (row, col, num) => {
……
}
// 脱离事件流,调用 setState
const setSudoku = async (row, col, value) => {
sudoku[row][col] = value
return new Promise(resolve => {
setTimeout(() => {
this.setState({
sudoku
}, () => resolve())
})
})
}
// 递归+回溯的方式进行解题
const solve = async (row, col) => {
……
for (let num = 1; num <= 9; num++) {
if (!isValid(row, col, num)) {
continue
}

await setSudoku(row, col, num.toString())

if (await solve(row, col + 1)) {
return true
}

await setSudoku(row, col, '.')
}
return false
}
// 进行解题
solve(0, 0)
}

最后效果如下:



作者:Shenfq
链接:https://juejin.cn/post/7004616375591239711

收起阅读 »

Retorfit + 协程机制 + MVVM

协程是一种解决方案,是一种解决嵌套,并发、弱化线程概念的方案。能让多个任务之间更好的协作,能够以同步的方式编排代码完成异步工作,将异步代码写的像同步代码一样直观。 重点 协程的本质是方法的挂起与恢复:return + callback 协程是什么: ...
继续阅读 »

协程是一种解决方案,是一种解决嵌套,并发、弱化线程概念的方案。能让多个任务之间更好的协作,能够以同步的方式编排代码完成异步工作,将异步代码写的像同步代码一样直观。




重点
协程的本质是方法的挂起恢复:return + callback





  • 协程是什么:


协程是可以由程序自行控制挂起、恢复的程序
协程可以实现多任务的协作执行
协程可以用来解决异步任务控制流的灵活转移



  • 协程的作用:


协程可以让异步代码同步化
协程可以降低异步程序的设计复杂度
协程本身不能让代码异步,只是让异步代码更简单控制流程




总结一句话:协程就是将程序进行挂起和恢复,解决异步回调让异步代码同步化。



什么是协程


Kotlin 协程的标准库:kotlinx-coroutines-core 它是协程的框架层的,而kotlin本身带有协程的基础层,要是用协程框架层封装需要引入依赖库,才能使用协程的框架层


 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
复制代码

常见的异步回调: 在enqueue中创建回调实例接受回调


retrofit.create(GitHubService::class.java).listRepos("octocat")
.enqueue(object : Callback<List<Repo>> {
override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
println("retrofit:" + response.body()?.size)
}

override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
TODO("Not yet implemented")
}
})

经过协程改造的异步程序:



  1. 在函数前面加上suspend 定义为挂起函数,在retrofit 2.6以上的版本就已经支持协程了


    /**
* kotlin 协程
*/
@GET("users/{user}/repos")
suspend fun listReposKx(@Path("user") user: String?): List<Repo>


  1. 协程的方式请求网络,将异步代码写成同步代码更加简单化


code.png
执行结果:



协程函数的挂起需要有一个CoroutineScope空间,函数挂起并不会影响CoroutineScope独立空间之外的代码执行,非阻塞式的挂起
注意:通过suspend修饰的挂起函数只能被挂起函数协程调用。



image.png


协程运行:遇到挂起函数listReposKx在线程2中执行,执行完毕后返回结果继续执行。挂起恢复
image.png



协程和线程的区别:
线程Thread:指操作系统的线程,内核线程
协程Coroutines: 指语言实现的协程,运行在内核线程之上的



协程的基本要素



Kotlin的协程分为两层:



  • 基础设施层:标准库的协程API,主要对协程提供了概念和语义上最基本的支持

  • 业务框架层: 协程的上层框架支持



在上述代码中GlobalScope.launch其实是业务框架层的实现。如果抛去框架层的封装,kotlin提供的基础层是如何实现协程的?这就需要了解协程的基本要素
code.png


如下代码:就是kotlin协程的基础层API包括:suspend挂起函数ContinuationcreateCoroutine创建协程startCoroutine启动协程CoroutinContext协程上下文。这五个就是协程的基本要素
code.png


image.png


挂起函数suspend



挂起点: suspend修饰的函数,只能在其他挂起函数或者协程中调用。
挂起函数调用时包含了协程的“挂起”的语义,而挂起函数返回时则包含了协程的“恢复”语义。



挂起和恢复主要通过:Continuation来实现,如下代码在service中添加的suspend函数反编译后的结果,在挂起函数中多了一个参数Continuation
code.png
其实在上述的createCoroutine中传递了一个Continuation实例,而Continuation主要的作用就是执行挂起函数“恢复”后的代码并且得到挂起函数的返回值作为参数返回


如何将异步回调通过挂起函数的改造呢?其实就是retrofit中是如何处理suspend函数的,通过kotlin的基础层API代码如下:通过suspendCoroutine来实现挂起函数,在通过Continuation返回异步回调请求的结果
code.png
Retrofit中的实现其实是一样的原理,Retrofit通过对Call进行了扩展方法处理,在Service API 的suspend方法的基础上又调用了Retrofit自己写的suspend函数
code.png
其实对于常见的框架层的封装例如xxxScope.launch:就是基于kotlin的基础层的API进行实现
code.png


协程上下文



在协程上下文(CoroutineContext)中存在着拦截器ContinuationInterceptor是一类协程上下文元素,可以对协程上下文所在协程的Continuation进行拦截,可以实现切换线程。
协程上下文包含一个 协程调度器 (参见 CoroutineDispatcher)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
所有的协程构建器诸如 launchasync 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。



image.png


协程框架在Android上的应用



Kotlin 的框架层基于基础层进行的封装:kotlinx-coroutines



Kotlin 提供了协程框架的基础库以及协程Android库


//kotlin 标准库-提供了协程的基础层
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
def coroutines_version = "1.4.2"
//kotlin协程依赖库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
//协程Android库,提供AndroidUI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

和协程相关的一些扩展库
image.png


image.png



官方文档中都有讲解这里不在复述了,重点讲解协程在Android中的应用。




  • Job: 协程的启动模式

  • 调度器:协程的线程调度(将协程在特有的线程或者线程池中执行)

  • 作用域:协程的作用域(在Android中常见的:lifecycleScope viewmodelScope)

  • Channel:“热”数据流,并发安全的通信机制(不去订阅也会发数据)

  • Flow:“冷”数据流,协程的响应式API(和RxJava类似,只有订阅了才会发送数据)

  • Select:可以对多个挂起事件进行等待


协程简化Dialog


创建Dialog的挂起函数给Context扩展alert方法如下代码:



通过suspendCancellableCoroutine 创建挂起函数,将异步流程同步化



code.png
通过协程来调用挂起函数:



lifecycleScope 和Activity/Fragment的生命周期绑定,可以直接拿到弹窗点击的结果进行处理



        val show_dialog = findViewById<Button>(R.id.show_dialog)
show_dialog.setOnClickListener {
lifecycleScope.launch {
//Activity生命周期的协程
val myCheck = alert("警告", "Do You Want is?")
Log.e("TAG", "onCreate: $myCheck")
}
}

简化Handler的调用



Handler.post的异步调用



code.png


调用方式如下:


        lifecycleScope.launch {
val handler = Handler(Looper.getMainLooper())
val h1 = handler.run { "test"}
Log.e("TAG", "onCreate: $h1") // test
val h2 = handler.runDelay(1000) { "delay" }
Log.e("TAG", "onCreate: $h2") // delay
}


协程:可以将所有的异步调用简化为同步调用的解决方案



Job 启动模式


code.png
image.pngimage.png


image.pngimage.png


调度器


image.pngimage.png


Retrofit+协程配合LiveData的封装原理



协程只是为了解决异步调用问题而存在的,Retrofit中内部已经将异步问题通过协程解决了,协程更多的是通过和LiveData和ViewModel结合再次简化封装部分业务逻辑。



需要引入的依赖库:



在解析josn的converter,kotlin推荐的moshi,可以调研调研。



    //retrofit
api rootProject.depsLibs.retrofit
api rootProject.depsLibs.logging_interceptor
api rootProject.depsLibs.converter_gson

//协程相关
def coroutines_version = "1.4.2"
//kotlin协程依赖库
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
//协程Android库,提供AndroidUI调度器
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
api "androidx.core:core-ktx:1.3.2"
api "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
api "androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.1"

网络请求的生命周期管理由下面依赖库进行管理



lifecycleScope 来定义Activity/Fragment的生命周期管理的协程运行网络请求的挂起函数或者通过viewModelScope 的生命周期协程来管理网络请求的挂起函数(推荐)。
注意:Lifecycle 和 ViewModel的是不同的,ViewModel可以当屏幕发生旋转后继续存在。
关于Lifecycle和ViewModel的
本质
讲解看这里:
ViewModel 如何对视图状态管理
LiveData 如何安全观察数据



    api "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

框架层设计


NetWorkManager 是网络框架的入口类,API设计:
code.png


网络请求返回数据结构设计:



**ResultData **作为最终返回的数据结构类.**ResponseBean**作为JSON的基础数据结构



code.png


通过Kotlin的密封类,用于区分网络状态,具体设计ApiResonse 主要包括网络异常状态空数据状态成功状态、业务错误状态。(对于code的业务错误状态.需要根据上述的错误处理器进行设置)
code.png
ApiResponse 通过静态方法进行构建不同状态的返回数据



有两种create构建方法,第一个是捕捉到的网络请求系统异常,第二个create首先判断是否是成功的状态码,如果不是返回ApiAppErrorResponse业务状态异常,data 为空表示返回空数据返回空数据下的状态,否则返回成功下的状态



code.png


核心类:RequestAction 通过kotlin的DSL包装网络请求动作




  • api: 传递的就是Retrofit的Service中定义的suspend 挂起函数

  • loadCache : 传递的就是加载缓存数据的函数

  • saveCache:传递的是实现缓存数据的函数



注意:这几个关键的方法都是传递DSL包装的函数


code.png
例如这样的调用方式:代码看起来会非常舒服


    @GET("users/{user}/repos")
suspend fun getJson2(@Path("user") user: String): ResponseBean<List<Repo>>

private val netApi = NetWorkManager.instance.create(Service::class.java)

val requestLiveData = viewModelScope.requestLiveData<List<Repo>> {
//请求网络
api { netApi.getJson2(name) }

//加载数据库缓存
loadCache {
//数据库请求,将结果返回给liveData中
_databaseLiveData
}

//将数据保存到数据库
saveCache {
//向数据库中保存数据
Log.e("TAG", "getRepo: ${it.size}")
}
}

下面就是通过协程来进行整合Retrofit+LiveData



CoroutineScope 是所有协程作用域的父类,对其进行扩展一个函数requestLiveData,liveData 是扩展依赖库提供的使其可以在LiveDataScope中运行挂起函数 emit 以及api.invoke都是挂起函数



code.png


异常处理设计


统一异常类:统一使用ResponseThrowable作为网络请求的所有异常类
code.png


IExceptionHandler 作为将异常转换为ResponseThrowable的实现,appSuccessCode 用来定义业务层的成功码,非成功码下的都是业务异常,其他情况为系统异常


code.png


IAdapterHandler 用来处理 IExceptionHandler 转换的ResponseThrowable异常


code.png


NetManager 的API



  1. 初始化设置


code.png



  1. 全局下异常处理的实现类如下,通过转换后的ResponseThrowable中的code判断处于哪个异常情况


code.png



  1. 将异常转换为ResponseThrowable



框架层默认实现了:DefaultExceptionHandler 如果不设置addExceptionHandler会使用默认的异常转换类



code.png



  1. MVVM 架构模式下在ViewModel中实现网络请求,外部通过repoLiveData监听数据,这里_repoLiveData 使用MediaorLiveData 只能监听数据变化不能改变数据,为了防止外部改变数据导致不可预期的错误


code.png



  1. View层实现监听网络数据



监听到的数据都是在框架层定义好的ResultData直接拿到状态等信息,注意在Error状态下所有的异常在框架层已经全部转换为了ResponseThrowable通过自定义异常中的code来判断具体的异常情况



code.png


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

Android正确的保活方案,不要掉进保活需求死循环陷进

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技: 大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。 最近2年Github上...
继续阅读 »

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技:


大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。


最近2年Github上面出来一个Leoric 感兴趣的可以去看一下源码,谁敢用在生产环境呢,也就自己玩玩的才会用吧(不能因为保活而导致手机卡巴斯基),我没有试过这个,我想说的是:黑科技能黑的了一时,能黑的了一世吗?


没有规矩,不成方圆,要提升产品的存活率,最终还是要落到产品本身上面来,尊重用户,提升用户体验才是正道。


以前我也是深受保活需求的压迫,最近发现QQ群里有人又提到了如何保活,那么我们就来说一说,如何来正确保活App?




Android 8.0之后: 加强了应用后台限制,当时测试过一组数据:



应用处于前台,启动一个前台Service,里面使用JobScheduler启动定时任务(30秒触发一次),
此时手机锁屏,前10分钟内,定时任务都是正常执行;

大概在12分钟左右,发现应用进程就被kill掉了,解锁屏幕,app也不在前台了;



各大国产手机厂商底层都经过自己魔改,自家都有自己的一套自启动管理,小米手机更乱(当时有个神隐模式的概念,那也是杀后台高手),只能说当时Android手机各种性能方面都不足,各家都会有自己的一套省电模式,以此来达到省电和提高手机性能,Android 系统变得越来越完善,但是厂商定制的自启动、省电模式还在,所以我们要做保活。


1.Android 8.0之前-常用的保活方案



1.开启一个前台Service

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)





2.Android 8.0之后-常用的保活方案



1.开启一个前台Service(可以加上,单独启用的话无法满足保活需求)

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)

4.应用自启动权限(最简单的方案是针对不同系统提供教程图片-让用户自己去打开)

5.多任务列表窗口加锁(提供GIF教程图片-让用户自己去打开)

6.多任务列表窗口隐藏App(仅针对有这方面需求的App)

7.应用后台高耗电(仅针对Vivo手机)



3.保活方案实现步骤


(1). 前台Service


//前台服务
class ForegroundCoreService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
private var mForegroundNF:ForegroundNF by lazy {
ForegroundNF(this)
}
override fun onCreate() {
super.onCreate()
mForegroundNF.startForegroundNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if(null == intent){
//服务被系统kill掉之后重启进来的
return START_NOT_STICKY
}
mForegroundNF.startForegroundNotification()
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
mForegroundNF.stopForegroundNotification()
super.onDestroy()
}
}

//初始化前台通知,停止前台通知
class ForegroundNF(private val service: ForegroundCoreService) : ContextWrapper(service) {
companion object {
private const val START_ID = 101
private const val CHANNEL_ID = "app_foreground_service"
private const val CHANNEL_NAME = "前台保活服务"
}
private var mNotificationManager: NotificationManager? = null

private var mCompatBuilder:NotificationCompat.Builder?=null

private val compatBuilder: NotificationCompat.Builder?
get() {
if (mCompatBuilder == null) {
val notificationIntent = Intent(this, MainActivity::class.java)
notificationIntent.action = Intent.ACTION_MAIN
notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER)
notificationIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
//动作意图
val pendingIntent = PendingIntent.getActivity(
this, (Math.random() * 10 + 10).toInt(),
notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this,CHANNEL_ID)
//标题
notificationBuilder.setContentTitle(getString(R.string.notification_content))
//通知内容
notificationBuilder.setContentText(getString(R.string.notification_sub_content))
//状态栏显示的小图标
notificationBuilder.setSmallIcon(R.mipmap.ic_coolback_launcher)
//通知内容打开的意图
notificationBuilder.setContentIntent(pendingIntent)
mCompatBuilder = notificationBuilder
}
return mCompatBuilder
}

init {
createNotificationChannel()
}

//创建通知渠道
private fun createNotificationChannel() {
mNotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//针对8.0+系统
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(false)
mNotificationManager?.createNotificationChannel(channel)
}
}

//开启前台通知
fun startForegroundNotification() {
service.startForeground(START_ID, compatBuilder?.build())
}

//停止前台服务并清除通知
fun stopForegroundNotification() {
mNotificationManager?.cancelAll()
service.stopForeground(true)
}
}

(2).忽略电池优化(Android 6.0+)


1.我们需要在AndroidManifest.xml中声明一下权限


<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

2.通过Intent来请求忽略电池优化的权限(需要引导用户点击)


//在Activity的onCreate中注册ActivityResult,一定要在onCreate中注册
//监听onActivityForResult回调
mIgnoreBatteryResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
//查询是否开启成功
if(queryBatteryOptimizeStatus()){
//忽略电池优化开启成功
}else{
//开启失败
}
}

通过Intent打开忽略电池优化弹框:


val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
//启动忽略电池优化,会弹出一个系统的弹框,我们在上面的
launchActivityResult(intent)

查询是否成功开启忽略电池优化开关:


fun Context.queryBatteryOptimizeStatus():Boolean{
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager?
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
powerManager?.isIgnoringBatteryOptimizations(packageName)?:false
} else {
true
}
}

(3).无障碍服务


看官方文档:创建自己的无障碍服务

它也是一个Service,它的优先级比较高,提供界面增强功能,初衷是帮助视觉障碍的用户或者是可能暂时无法与设备进行全面互动的用户完成操作。

可以做很多事情,使用了此Service,在6.0+不需要申请悬浮窗权限,直接使用WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY 挺方便的

(仅针对有需要此服务的app,可以开启增强后台保活)


(4).自启动权限(即:白名单管理列表页面)


是系统给用户自己去打开“自启动权限”开关的入口,我们需要针对不同的手机厂商和系统版本,弹出提示引导用户是否前去打开“自启动权限”

有的手机厂商叫:白名单管理,有的叫:自启动权限,两个是一个概念;

点击查看跳转到『手机自启动设置页面』完整代码


(需要注意:如果是代码控制跳转,无法保证永远可以调整,系统升级可能就给你屏蔽了,
最简单的方法是:显示一个如何找到自启动页面的引导图,下面以华为手机为例:)



华为手机-自启动管理

(5).多任务列表窗口加锁


可以针对不同手机厂商,显示引导用户,开启App窗口加锁之后,点击清理加速不会导致应用被kill



华为手机窗口加锁-教程图

(6).多任务列表窗口隐藏App窗口


刚刚上面多任务窗口加锁完,再提示用户去App里面把隐藏App窗口开关打开,这样用户就不会多任务列表里面把App窗口给手抖划掉


多任务窗口中『隐藏App窗口』,可以用如下代码控制:

(这个也只是针对有这方面需求App提供的一种增强方案罢了:因为隐藏了窗口,用户就不会去想他,不会去手痒去划掉它)


//在多任务列表页面隐藏App窗口
fun hideAppWindow(context: Context,isHide:Boolean){
try {
val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
//控制App的窗口是否在多任务列表显示
activityManager.appTasks[0].setExcludeFromRecents(isHide)
}catch (e:Exception){
.....
}
}

(7).应用后台高耗电(Vivo手机独有)


开启的入口:“设置”>“电池”>“后台高耗电”>“找到xxxApp打开开关”



vivo允许后台高耗电



最后还是奉劝那些,仍然执着于找寻黑科技的开发者,醒醒吧,太阳晒屁股了。


如果说你的App用户群体不是普通用户,是专门给一些玩机大神们用的,都可以root手机的话,那么直接 move 到系统目录 priv/system/app 即可, 即使被用户强杀也会自动重新拉起。



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

Flutter实现"剑气"加载?️

效果图知识点Animation【动效】Clipper/Canvas【路径裁剪/画布】Matrix4【矩阵转化】剑气形状我们仔细看一道剑气,它的形状是一轮非常细小的弯弯的月牙;在Flutter中,我们可以通过Clipper路径来裁剪出来,或者也可以通过canva...
继续阅读 »

效果图

剑气加载.gif

知识点

  • Animation【动效】
  • Clipper/Canvas【路径裁剪/画布】
  • Matrix4【矩阵转化】

剑气形状

我们仔细看一道剑气,它的形状是一轮非常细小的弯弯的月牙;在Flutter中,我们可以通过Clipper路径来裁剪出来,或者也可以通过canvas绘制出来。

  1. 先看canvas如何进行绘制的
class MyPainter extends CustomPainter {
Color paintColor;

MyPainter(this.paintColor);

Paint _paint = Paint()
..strokeCap = StrokeCap.round
..isAntiAlias = true
..strokeJoin = StrokeJoin.bevel
..strokeWidth = 1.0;

@override
void paint(Canvas canvas, Size size) {
_paint..color = this.paintColor;
Path path = new Path();
// 获取视图的大小
double w = size.width;
double h = size.height;
// 月牙上边界的高度
double topH = h * 0.92;
// 以区域中点开始绘制
path.moveTo(0, h / 2);
// 贝塞尔曲线连接path
path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);

canvas.drawPath(path, _paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; // 一次性画好,不需要更新,返回false
}
  1. Clipper也上代码,跟canvas两种选其一即可,我用的是canvas
class SwordPath extends CustomClipper<Path> {
@override
getClip(Size size) {
print(size);
// 获取视图的大小
double w = size.width;
double h = size.height;
// 月牙上边界的高度
double topH = h * 0.92;
Path path = new Path();
// 以区域中点开始绘制
path.moveTo(0, h / 2);
// 贝塞尔曲线连接path
path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);
return path;
}

@override
bool shouldReclip(covariant CustomClipper oldClipper) => false;
}
  1. 生成月牙控件
CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(200, 200),
),

让剑气旋转起来

我们需要剑气一直不停的循环转动,所以需要用到动画,让剑气围绕中心的转动起来。注意这里只是单纯的平面旋转,也就是我们说的2D变换。这里我们用到的是Transform.rotate控件,通过animation.value传入旋转的角度,从而实现360度的旋转。

class _SwordLoadingState extends State<SwordLoading>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
double angle = 0;

@override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 800));
// pi * 2:360°旋转
_animation = Tween(begin: 0.0, end: pi * 2).animate(_controller);
_controller.repeat(); // 循环播放动画
super.initState();
}

@override
Widget build(BuildContext context) {
return Transform.rotate(
alignment: Alignment.center,
angle: _animation.value,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
);
}
}

转起来啦!

让剑气有角度的、更犀利的转动

  • 我们仔细看单独一条剑气,其实是在一个三维的模型中,把与Z轴垂直的剑气 向Y轴、X轴进行了一定角度的偏移。
  • 相当于在这个3D空间内,剑气不在某一个平面了,而是斜在这个空间内,然后 再绕着圆心去旋转。
  • 而观者的视图,永远与Z轴垂直【或者说:X轴和Y轴共同组成的平面上】,所以就会产生剑气 从外到里进行旋转 的感觉。

下图纯手工绘制,不要笑我~~~

纯手工绘制

不要笑我

综上,可以确定这个过程是一个3D的变换,很明显我们Transform.rotate这种2D的widget已经不满足需求了,这个时候Matrix4大佬上场了,我们通过Matrix4.identity()..rotate的方法,传入我们的3D转化,在通过rotateZ进行旋转,简直完美。代码如下

 AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(0, -8, 12), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),

这里多说一句,要完成矩阵变换,Matrix4必不可少,可以着重学习下。

让剑气一起动起来

完成一个剑气的旋转之后,我们回到预览效果,无非就是3个剑气堆叠在一起,通过偏移角度去区分。Flutter堆叠效果直接用Stack实现,完整代码如下:

import 'package:flutter/material.dart';
import 'dart:math';
import 'package:vector_math/vector_math_64.dart' as v;

class SwordLoading extends StatefulWidget {
const SwordLoading({Key? key, this.loadColor = Colors.black, this.size = 88})
: super(key: key);

final Color loadColor;
final double size;

@override
_SwordLoadingState createState() => _SwordLoadingState();
}

class _SwordLoadingState extends State<SwordLoading>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
double angle = 0;

@override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 800));
_animation = Tween(begin: 0.0, end: pi * 2).animate(_controller);
_controller.repeat();
super.initState();
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(0, -8, 12), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),
AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(-12, 8, 8), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),
AnimatedBuilder(
animation: _animation,
builder: (context, _) => Transform(
transform: Matrix4.identity()
..rotate(v.Vector3(-8, -8, 6), pi)
..rotateZ(_animation.value),
alignment: Alignment.center,
child: CustomPaint(
painter: MyPainter(widget.loadColor),
size: Size(widget.size, widget.size),
),
),
),
],
);
}
}

class MyPainter extends CustomPainter {
Color paintColor;

MyPainter(this.paintColor);

Paint _paint = Paint()
..strokeCap = StrokeCap.round
..isAntiAlias = true
..strokeJoin = StrokeJoin.bevel
..strokeWidth = 1.0;

@override
void paint(Canvas canvas, Size size) {
_paint..color = this.paintColor;
Path path = new Path();
// 获取视图的大小
double w = size.width;
double h = size.height;
// 月牙上边界的高度
double topH = h * 0.92;
// 以区域中点开始绘制
path.moveTo(0, h / 2);
// 贝塞尔曲线连接path
path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);

canvas.drawPath(path, _paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
false; // 一次性画好,不需要更新,返回false
}

业务端调用

SwordLoading(loadColor: Colors.black,size: 128),

写在最后

花了我整个周六下午的时间,很开心用Flutter实现了加载动画,说说感受吧。

  1. 在编写的过程中,对比html+css的方式,Flutter的实现难度其实更大,而且剑气必须使用canvas绘制出来。
  2. 如果你也懂前端,你可以深刻体会声明式和命令式UI在编写布局和动画所带来的强烈差异,从而加深Flutter万物皆对象的思想。*【因为万物皆对象,所以所有控件和动画,都是可以显示声明的对象,而不是像前端那样通过解析xml命令来显示】
  3. 2D/3D变换,我建议Flutter学者们,一定要深入学习,这种空间思维对我们实现特效是不可获取的能力。

好了,小弟班门弄斧,希望能一起学习进步!!!


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

收起阅读 »

常见问题之webView 内存泄露

WebView内存泄露的原因:webView内部的一些线程持有activity对象,导致activity无法释放。继而内存泄漏。现象正常使用都会有内存泄露反复进出多次后:内存没有下降。 网上查了一些解决办法,主要有两种:解决方法一1.在Activit...
继续阅读 »

WebView内存泄露的原因:

webView内部的一些线程持有activity对象,导致activity无法释放。继而内存泄漏。

现象

正常使用都会有内存泄露

image.png

反复进出多次后:
内存没有下降。 image.png

网上查了一些解决办法,主要有两种:

解决方法一

1.在Activity中手动new WebView,然后将Activity通过弱引用的方式传进去。并且在onDestroy里将webview先从父容器中移除webview,然后再销毁webview(调用webView的各种clear)

image.png

image.png

页面反复进出以后:

屁用没有

image.png

解决方法二

2.新启一个进程,单独进程跑webView。AIDL通信。
简化的代码以后:

AIDL接口:
image.png

AIDL的服务: image.png

设置服务隐士启动:
image.png

客户端显示webView的Activity:
image.png

将Activity设置成单独进程:
image.png

在Activity的onDestroy,解绑Service和结束进程
image.png

页面反复进出以后:
没有占用内存,完美。 image.png

补充
如果想要webView跟Activity有更多的数据交互,可以用AIDL->Service(数据交互)->Activity(主进程,Handler、回调等方式)

收起阅读 »

ConstraintLayout2.0一篇写不完之KeyCycles的秘密

KeyCycle与KeyFrame类似,但是又比KeyFrame复杂,复杂在于KeyFrame只是单帧,而KeyCycle则是在KeyFrame的基础上,增加了周期性的处理,所以,KeyCycle的核心就是周期,KeyCycle决定了在Scene中所有需要重复...
继续阅读 »

KeyCycle与KeyFrame类似,但是又比KeyFrame复杂,复杂在于KeyFrame只是单帧,而KeyCycle则是在KeyFrame的基础上,增加了周期性的处理,所以,KeyCycle的核心就是周期,KeyCycle决定了在Scene中所有需要重复处理的部分操作,它的核心API如下所示。

  • framePosition:作为一个KeyFrame,KeyCycle必须知道在场景的哪一点上进行操作
  • motionTarget:指定的View ID
  • wavePeriod:周期数量
  • waveOffset:起始位置的偏移
  • waveShape:Cycle的波形

image-20210827101705951

MotionLayout提供了CycleEditor来帮助开发者编辑KeyCycle,下载地址如下:

github.com/googlesampl…

直接执行即可,点击file中的parse,就可以将编辑区域的xml转换为波形。

java -jar CycleEditor.jar

分割Scene

在创建KeyCycle之前的第一件事,就是通过使用不同的framePositions把你的Scene分成多部分组合的Partial Scene,接下来就可以通过使用wavePeriod来指定你想要的每个部分的周期数,以及waveShape来指定具体的波形。

1*cdtzWO2JKu6VvmN9Ew2kvw

wavePeriod是KeyCycle最难理解的一部分,要掌握wavePeriod的定义,就必须先了解Partial Scene,Partial Scene指的是当前指定点的前一个点和后一个点,总共三个点构成的区域,这点非常重要。

在某个framePosition的KeyCycle中指定wavePeriod,其实就是指在这个Partial Scene中,有几个周期的波形来填满这个区域。

但是这里问题又来了,每个framePosition都被周围的framePosition有关,那么wavePeriod不是被重复计算了吗?

没错。。。所以在整个Partial Scene中的wavePeriod是由Partial Scene中所有framePosition的的wavePeriod之和确定的。很绕是不是,是就对了。

这也是为什么KeyCycle有个单独的生成工具的原因,结合KeyCycleEditor,还是比较能理解的。

wavePeriod已经很绕了,但是绕的还在后面。

我们再来看看KeyCycle中指定的具体属性值的含义。

例如,我们在KeyCycle中指定rotation为20,代码如下所示。

<KeyCycle 
motion:framePosition="0"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="20"/>

这个rotation为20是什么意思?你以为是当然framePosition的属性值为20吗?太年轻了。。。

其实这个属性值与View在当前framePosition的属性值,并没有直接联系。。。

是不是很奇怪,的确如此,那么这玩意儿到底是干嘛的呢???

这里我们需要转换一下思路,那就是KeyCycle里面设置的一切东西,都是为了画出「波形图」,所以,这些参数的设置,就是为了修改波形图的具体形状。

<KeyFrameSet>

<KeyCycle
motion:framePosition="0"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="0"/>

<KeyCycle
motion:framePosition="50"
motion:target="@+id/button"
motion:wavePeriod="1"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="10"/>

<KeyCycle
motion:framePosition="100"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="0"
motion:waveShape="sin"
android:rotation="30"/>

</KeyFrameSet>

这样一个KeyCycle最后形成的波形图就是这样。

image-20210827151332911

由此可以发现,每个framePosition的属性值,就是为了画出波形图的波峰。

在这个的基础上,waveOffset就好理解了,它的作用就是给framePosition的当前value增加一个初始值,这个初始值同样是为了修改波形。

要干嘛

你说KeyCycle这玩意儿整这么复杂,到底有什么用呢??

我们有了KeyFrame,可以用来添加中间态关键帧,那么还要KeyCycle干嘛呢?

说到这来,就不能不提下Monotonic Spline(单调采样)了,通常的关键帧插值算法都是使用的单调采样,但是这样无法做到曲线的圆滑过渡,就像下图中的绿色曲线,这样四个点使用单调采样,就变成了下面这样的曲线,过渡会非常生硬。

image-20210827154425111

那么为了让曲线圆滑过渡,KeyCycle使用的是Typical Spline(特征采样),就如上图中的紫色曲线,四个点被圆滑的连接了起来。

如果仅仅是为了让曲线能圆滑过渡,那么你就太小看KeyCycle了,不得不说老外做的这些东西,总能在一些你觉得无关紧要的地方,做的非常深入。

KeyCycle的核心在于波形,而波是什么呢?

image-20210827155302534

上面这张图表达了sin和cos的几何含义,也是sin和cos的来源。

说句不像傅里叶变换的话,我们可以将一个View的曲线运动,拆解成多个不同波形运动的叠加。

例如我们对一个View的translationX同时设置sin和cos的KeyCycle,最终形成的运动轨迹,就是一个圆形!

所以,由此及彼,我们可以复合多个属性的同时,通过不同的波形叠加,实现任何你想要的运动轨迹!这TM就牛逼了啊,简直就是傅里叶变换在Android动画中的实现了。

在CycleEditor中,有一些自带的Demo,可以让你充分的了解这个思想,例如下面这个例子。

image-20210827160218065

太复杂了是吗?

CustomWave shape in keyCycle

CL2.1之后,motion:waveShape除了之前定义的sin、cos、bounce这些预设曲线外,你还可以设置自定义的波形曲线,定义方式如下所示。

<KeyCycle motion:waveShape=”spline(0.0, 1.0, -1.0, 0)” />

这就有点牛逼了,本来就很复杂了,这下还来了自定义曲线,再见。

KeyCycle确实比较强大,但是也非常复杂,强烈建议大家使用CycleEditor来学习,KeyCycle这种东西,就像核武器一样,可以不用,但是不能没有。

收起阅读 »

Jetpack App Startup如何使用及原理分析

1.App Startup是什么?来自 Google官方App Startup文档: App Startup 库提供了一种在应用程序启动时初始化组件的简单、高效的方法。Libary开发人员和App开发人员都可以使用App Startup来简化启动顺序...
继续阅读 »

1.App Startup是什么?

来自 Google官方App Startup文档: App Startup 库提供了一种在应用程序启动时初始化组件的简单、高效的方法。
Libary开发人员和App开发人员都可以使用App Startup来简化启动顺序并明确设置初始化顺序。

2.简单回顾一下ContentProvider

内容提供者ContentProvider 玩法有很多种:可以进程间通信(数据共享)、ContentResolver#registerContentObserver观察uri变化(可以解析uri来处理各种命令) 等等
ContentProvider通过Binder实现进程间通信,使用起来比AIDL要简单,因为系统已经帮我们进行了封装(这里就不扩散介绍Binder,东西太多)

(1).注册

我们需要在AndroidManifest.xml中注册我们的ContentProvider

//AndroidManifest.xml
<provider
android:authorities="..."
android:name="..."/>

上面有两个属性,我们来看一下是干什么的?

  • android:authorities: 一个或多个URI授权方的列表,多个授权方需要使用分号( ; )分隔开。
  • android:name: 完整的ContentProvider类名,如:(com.xxx.xxx.xxxProvider)

(2).实现一个CustomProvider

class CustomProvider: ContentProvider() {
override fun onCreate(): Boolean {
// 返回true表示初始化成功
// 返回false表示初始化失败
}
override fun query(uri: Uri, projection: Array<out String>?,selection: String?,selectionArgs: Array<out String>?,sortOrder: String?): Cursor? {
//查询数据返回Cursor,如果是别的玩法,返回null也可以
}
override fun getType(uri: Uri): String? {
//没有类型的话可以直接返回null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
//插入数据,返回uri,如果是别的玩法,返回null也可以
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
//删除数据,返回行数,如果是别的玩法,返回-1也可以
}
override fun update(uri: Uri,values: ContentValues?,selection: String?,selectionArgs: Array<out String>?): Int {
//更新数据,返回行数,如果是别的玩法,返回-1也可以
}
}

(3).ContentProvider在哪初始化的?

我们从ActivityThread的main函数开始跟踪:

//frameworks/base/core/java/android/app/ActivityThread.java
public static void main(String[] args) {
....
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
....
}

我们来看一下ActivityThread的attach方法

private void attach(boolean system, long startSeq) {
....
final IActivityManager mgr = ActivityManager.getService();
try {
//通过IBinder进程间通讯调用AMS
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
.....
}
.....
}

打开ActivityManagerService查看attachApplication方法里面会执行什么

//com.android.server.am.ActivityManagerService
public final void attachApplication(IApplicationThread thread, long startSeq) {
....
synchronized (this) {
....
attachApplicationLocked(thread, callingPid, callingUid, startSeq);
....
}
}

private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
int pid, int callingUid, long startSeq) {
......
try {
......
if (instr2 != null) {
thread.bindApplication(processName, appInfo, providerList,
instr2.mClass,
profilerInfo, instr2.mArguments,
instr2.mWatcher,....);
} else {
thread.bindApplication(processName, appInfo, providerList, null, profilerInfo,
null, null, null, testMode,....);
}
......
} catch (Exception e) {
......
}
......
}

看到这里我们发现,调用了ActivityThread里面的bindApplication:

//android.app.ActivityThread.ApplicationThread#bindApplication

public final void bindApplication(String processName, ApplicationInfo appInfo,........){
.....
AppBindData data = new AppBindData();
data.processName = processName;
data.appInfo = appInfo;
//ContentProvider列表
data.providers = providerList.getList();
.....
sendMessage(H.BIND_APPLICATION, data);
}

看到这里,仿佛此刻看到了光,BIND_APPLICATION这条消息内部会执行到ActivityThread#handleBindApplication

//android.app.ActivityThread.ApplicationThread#handleBindApplication

private void handleBindApplication(AppBindData data) {
.....
Application app;
.....
try {
//触发LoadedApk调用makeApplication
app = data.info.makeApplication(data.restrictedBackupMode, null);
.....
mInitialApplication = app;
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
//安装ContentProvider入口
installContentProviders(app, data.providers);
}
}
....
try {
//调用Application#onCreate方法
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
....
}
} finally {
.....
}
.....
}

handleBindApplication步骤如下:
(1). 执行LoadedApk调用makeApplication
(2). 执行installContentProviders遍历providers列表,实例化ContentProvider
(3). mInstrumentation.callApplicationOnCreate触发Application#onCreate回调

//android.app.ActivityThread#installContentProviders
private void installContentProviders(
Context context, List<ProviderInfo> providers) {
final ArrayList<ContentProviderHolder> results = new ArrayList<>();
for (ProviderInfo cpi : providers) {
....
//真正初始化ContentProvider的地方
ContentProviderHolder cph = installProvider(context, null, cpi,
false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
if (cph != null) {
cph.noReleaseNeeded = true;
results.add(cph);
}
}
try {
//AMS内部会执行mHandler.removeMessages(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG, r)
ActivityManager.getService().publishContentProviders(
getApplicationThread(), results);
} catch (RemoteException ex) {
....
}
}

installContentProviders步骤如下:
(1). 遍历providers列表,执行installProvider来实例化ContentProvider
(2). AMS调用publishContentProviders清除掉超时的消息,否则会导致应用被kill

//android.app.ActivityThread#installProvider
private ContentProviderHolder installProvider(Context context,
ContentProviderHolder holder, ProviderInfo info,
boolean noisy, boolean noReleaseNeeded, boolean stable) {
ContentProvider localProvider = null;
IContentProvider provider;
if (holder == null || holder.provider == null) {
....
Context c = null;
ApplicationInfo ai = info.applicationInfo;
//初始化Context
if (context.getPackageName().equals(ai.packageName)) {
c = context;
} else if (mInitialApplication != null &&
mInitialApplication.getPackageName().equals(ai.packageName)) {
c = mInitialApplication;
} else {
try {
c = context.createPackageContext(ai.packageName,
Context.CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
}
....
try {
final java.lang.ClassLoader cl = c.getClassLoader();
//ContentProvider和Application的加载器是同一个
LoadedApk packageInfo = peekPackageInfo(ai.packageName, true);
if (packageInfo == null) {
// System startup case.
packageInfo = getSystemContext().mPackageInfo;
}
//通过ClassLoader初始化ContentProvider
localProvider = packageInfo.getAppFactory()
.instantiateProvider(cl, info.name);
provider = localProvider.getIContentProvider();
if (provider == null) {
return null;
}
//ContentProvider实例化成功之后,回调ContentProvider#onCreate
localProvider.attachInfo(c, info);
} catch (java.lang.Exception e) {
return null;
}
} else {
provider = holder.provider;
}
....
return retHolder;
}

installProvider步骤如下:
(1). 获取Context
(2). 通过peekPackageInfo获取LoadedApk,然后通过ClassLoader来实例化ContentProvider
(3). ContentProvider实例化成功之后,attachInfo会触发ContentProvider#onCreate回调

从上面的代码分析之后得出结论:ContentProvider的onCreate会比Application的onCreate先执行

(4).ContentResolver

如果我们需要访问内容提供者里面数据的话,可以使用App的Context中的ContentResolver对象与ContentProvider进行通信。
ContentProvider对象从客户端接收数据请求、执行请求的操作并返回结果。ContentResolver方法可以提供基本的“CRUD”功能。

A.在哪初始化

//frameworks/base/core/java/android/app/LoadedApk.java

public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
....
Application app = null;
....
try {
....
//初始化ContextImpl,并初始化一个ApplicationContentResolver「即ContentResolver」
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
....
//执行进程中application对象的实例化,并执行Application#attach方法同时触发attachBaseContext回调
//更多细节,感兴趣的,可以自己看源码查看更多内容
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
//设置applicationContext
appContext.setOuterContext(app);
} catch (Exception e) {
...
}
....
mApplication = app;
if (instrumentation != null) {
try {
//触发application的onCreate方法执行
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
....
}
}
....
return app;
}

makeApplication简单逻辑如下:
(1).初始化ContextImpl同时初始化ContentResolver
(2).使用ActivityThread中的Instrumentation执行里面的newApplication:
初始化Application并调用attach方法并触发attachBaseContext回调
(3).调用Application的onCreate方法

B.监听数据变化

我们可以使用ContentResolver#registerContentObserver来观察数据是否发生变化。
我们来看一下简单的demo:

//注册内容观察者
contentResolver.registerContentObserver(Uri.parse("....."),true,xxxxCallback)

//不是和数据库进行操作交互的话,自己写的一些功能玩法的uri可以像下面这样
//uri => content://xxxx.service.status
//我们registerContentObserver观察这个原始的uri
//别的进程或者app通过:contentResolver.notifyChange(uri+"/lastPathSegment")
//可以通过下面的方式去解析uri来做一些操作,可玩性也比较好
private val xxxxCallback: ContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
val lastPathSegment = uri?.lastPathSegment
//有些玩法可以自己定义,比如我们可以拿这个lastPathSegment来实现一些命令操作
.....
}
}

C.如何CURD

//查询数据
ContentResolver#query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String)

//插入一条数据
ContentResolver#insert(android.net.Uri, android.content.ContentValues)

//删除数据
ContentResolver#delete(android.net.Uri, java.lang.String, java.lang.String[])

//更新数据
ContentResolver#update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[])

3.App Startup使用

(1).依赖

dependencies {
implementation("androidx.startup:startup-runtime:<新版本>")
}

(2).AndroidManifest.xml配置

App Startup是用InitializationProvider来发现和调用你的组件,所以这里我们需要定义<provider/>

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="xxx.xxx.xxxInitializer"
android:value="androidx.startup" />
</provider>

使用tools:node="merge"合并属性,解决任何冲突的条目,合并到一起。
使用<meta-data/>,这使得我们自定义的Initializer可以被发现。
如果<provider/>里面增加tools:node="remove"禁用所有组件的自动初始化。
如果需要禁用单个组件自动初始化,可以在<meta-data/>里面增加:tools:node="remove"

(3).自定义Initializer

class MyInitializer :Initializer<Boolean>{
override fun create(context: Context): Boolean {
//可以在此处进行SDK初始化
....
return true
}
override fun dependencies(): List<Class<out Initializer<*>>> {
//没有依赖直接返回emptyList()即可
//如果有依赖,是针对实现了Initializer的类
return emptyList()
}
}

举个例子:
一、Leakcanary目前还没有集成App Startup,那么我们怎么使用呢?
首先我们去AndroidManifest.xml找到Leakcanary的<provider/>

WX20210902-105232@2x.png

然后我们在AndroidManifest.xml找到这两个<provider/>,添加tools:node="remove"

<provider
android:name="leakcanary.internal.PlumberInstaller"
android:authorities="写你自己的包名.plumber-installer"
android:enabled="@bool/leak_canary_plumber_auto_install"
android:exported="false"
tools:node="remove"/>
<provider
android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
android:authorities="写你自己的包名.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false"
tools:node="remove"/>

这个时候我们就可以在我们的App Startup中来控制初始化,愉快的玩耍了

class MyInitializer :Initializer<Boolean>{
override fun create(context: Context): Boolean {
val application = context.applicationContext as Application
AndroidLeakFixes.applyFixes(application)
AppWatcher.manualInstall(application)
....
return true
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}

二、WorkManager目前也没有集成App Startup,那么我们自己来扩展一下它实现Initializer,然后再看一下dependencies()的用处

class MyWorkManagerInitializer:Initializer<WorkManager> {
override fun create(context: Context): WorkManager {
//集成了WorkManager之后,可以在AndroidManifest.xml中找到默认的provider,可以看到内部实现
WorkManager.initialize(context, Configuration.Builder().build())
return WorkManager.getInstance(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}

我们还需要把WorkManager默认的provider删除掉

<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="你自己的包名.workmanager-init"
android:directBootAware="false"
android:exported="false"
android:multiprocess="true"
tools:targetApi="n"
tools:node="remove"/>

在我们的MyInitializer中依赖MyWorkManagerInitializer

class MyInitializer :Initializer<Boolean>{
override fun create(context: Context): Boolean {
//先触发依赖的执行,执行完,我们这个create才会被执行到,表示依赖已经完成,可以正常使用了
return true
}
//先执行
override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(MyWorkManagerInitializer::class.java)
}
}

玩法使用如下: (1). 我们可以自己定义扩展的XXXInitializer来替换第三方Libary的ContentProvider,然后在自定义的Initializer的dependencies()中依赖扩展的XXXInitializer
(2). 不自定义扩展,我们知道第三方Libary的ContentProvider内部初始化的代码,可以写在自定义的Initializer的onCreate()中也可以。

4.原理分析

我们在上面提到AndroidManifest.xml中使用了InitializationProvider,它就是一个ContentProvider

//androidx.startup.InitializationProvider

public class InitializationProvider extends ContentProvider {
@Override
public final boolean onCreate() {
Context context = getContext();
if (context != null) {
//初始化解析
AppInitializer.getInstance(context).discoverAndInitialize();
} else {
throw new StartupException("Context cannot be null");
}
return true;
}
....
}

InitializationProvider#onCreate
获取内容提供者信息,解析meta-data数据,初始化Initializer并执行Initializer#create调用

//androidx.startup.AppInitializer

void discoverAndInitialize() {
try {
Trace.beginSection(SECTION_NAME);
ComponentName provider = new ComponentName(mContext.getPackageName(),
InitializationProvider.class.getName());
//通过PackageManager.getProviderInfo获取内容提供者信息
ProviderInfo providerInfo = mContext.getPackageManager()
.getProviderInfo(provider, GET_META_DATA);
//获取元数据
Bundle metadata = providerInfo.metaData;
//用于比较元数据里面的数据是不是androidx.startup
String startup = mContext.getString(R.string.androidx_startup);
if (metadata != null) {
Set<Class<?>> initializing = new HashSet<>();
Set<String> keys = metadata.keySet();
//遍历元数据
for (String key : keys) {
String value = metadata.getString(key, null);
if (startup.equals(value)) { //只初始化匹配成功的
Class<?> clazz = Class.forName(key);
if (Initializer.class.isAssignableFrom(clazz)) {
Class<? extends Initializer<?>> component =
(Class<? extends Initializer<?>>) clazz;
mDiscovered.add(component);
if (StartupLogger.DEBUG) {
StartupLogger.i(String.format("Discovered %s", key));
}
//初始化Initializer
doInitialize(component, initializing);
}
}
}
}
} catch (PackageManager.NameNotFoundException | ClassNotFoundException exception) {
throw new StartupException(exception);
} finally {
Trace.endSection();
}
}

discoverAndInitialize()逻辑如下:
(1). 通过PackageManager.getProviderInfo获取内容提供者信息 (2). 遍历元数据key列表,比较value是否等于androidx.startup (3). 条件匹配成功,执行doInitialize(component, initializing)

//androidx.startup.AppInitializer#doInitialize

<T> T doInitialize(
@NonNull Class<? extends Initializer<?>> component,
@NonNull Set<Class<?>> initializing) {
synchronized (sLock) {
try {
....
if (initializing.contains(component)) {
//没有初始化完成的抛异常
throw new IllegalStateException(....);
}
Object result;
//缓存里面是否已经有初始化过的数据
if (!mInitialized.containsKey(component)) {
//记录正在进行初始化
initializing.add(component);
try {
//实例化Initializer
Object instance = component.getDeclaredConstructor().newInstance();
Initializer<?> initializer = (Initializer<?>) instance;
//调用initializer.dependencies()
List<Class<? extends Initializer<?>>> dependencies =
initializer.dependencies();

//如果dependencies不为空
if (!dependencies.isEmpty()) {
for (Class<? extends Initializer<?>> clazz : dependencies) {
if (!mInitialized.containsKey(clazz)) {
//递归调用
doInitialize(clazz, initializing);
}
}
}
//回调Initializer#create
result = initializer.create(mContext);
//Initializer初始化完,移除临时添加的component
initializing.remove(component);
//添加到缓存中
mInitialized.put(component, result);
} catch (Throwable throwable) {
throw new StartupException(throwable);
}
} else {
//从缓存中获取
result = mInitialized.get(component);
}
return (T) result;
} finally {
...
}
}
}

doInitialize步骤如下:
(1). 正在初始化的HashSet集合(initializing)中如果包含component,需要抛出异常。
(2). 缓存的Map集合mInitialized中没有component: 先把component记录到正在初始化的HashSet集合(initializing),实例化Initializer。
检查initializer.dependencies()集合是否有数据,如果有数据,需要递归调用doInitialize
如果没有数据直接执行Initializer#create方法的调用,然后从正在初始化的HashSet集合(initializing)移除component,再把component添加到缓存的Map集合mInitialized中。
(3). 缓存的Map集合mInitialized中有component,直接从Map集合中get返回,不用重复初始化。
(4). 从上面代码分析可以看出来:Initializer中dependencies()优先执行,最后再执行Initializer#create

5.总结 + 耗时对比

App Startup VS 多个ContentProvider 耗时对比,默认对比数值是参考什么都不集成的原始apk

我们用三款设备进行测试冷启动比较(大概的一个数据值,仅供参考):

测试设备1: Huawei Mate20-HarmonyOS 2.0.0 001.png


测试设备2: Nexus 6-Android 7.1 002.png


测试设备3: Nokia N1平板-Android 5.1.1 003.png


结论如下:
App Startup使用的好处有:统一管理,简化启动顺序并明确设置初始化顺序。
如果每个Libary库开发者都自己搞一个ContentProvider来初始化,不方便管理。
设计初衷应该是为了收拢ContentProvider,实际上对启动优化的帮助不是特别大。
抛开ContentProvider中初始化数据,如果大家在项目中大多数的时候会使用线程池异步加载初始化一些数据,这个时候App Startup就没有必要使用了。

我们从测试的数据对比可以看出来:
针对少量的SDK使用少量ContentProvider的时候,如果使用App Startup不一定能缩短应用启动时间。
但是在多个不同性能的设备上(尤其是低端机),App Startup启动时间会比使用多个ContentProvider有些许缩短时间。

看完上面的内容,我想大家应该知道App Startup什么时候用,怎么用,为什么要用。
这样我写这篇文章的目的就达到了。

收起阅读 »

Jetpack生命周期管理 -Lifecycle实战及源码分析

上次我们聊了 Android 触摸事件传递机制,这次我们来聊聊 Jetpack。具体地说是聊聊他的生命周期管理组件 LifeCycle,因为JetPack这个官方库还蛮大。这里不会再讲 Jetpack的前世今生,以及他的作用什么的。这里我们主要讲讲 LifeC...
继续阅读 »

上次我们聊了 Android 触摸事件传递机制,这次我们来聊聊 Jetpack。具体地说是聊聊他的生命周期管理组件 LifeCycle,因为JetPack这个官方库还蛮大。这里不会再讲 Jetpack的前世今生,以及他的作用什么的。这里我们主要讲讲 LifeCycle的基本使用,以及用LifeCycle改进一下上次我们讲到的 MVP 的例子。

然后从源码角度分析一下 LifeCycle是如何帮助 Activity 或 Fragment管理生命周期的。后续会继续推出分析 Jetpack其他组件的文章。

我们知道,我们在用某些模块进行数据加载的时候,往往需要去监听 Activity或 Fragment的生命周期。再根据生命周期的变化去调整数据加载或回调的策略。

不使用组件来管理的话,一般我们可以在 Activity或 Fragment的生命周期回调方法里手动去调用模块的生命周期方法。这样页面的生命周期回调方法里可能就会出现大量这样的刻板代码,也不好管理。LifeCycle就用来解决这样的一些问题。

1、使用

使用 LifeCycle不用添加依赖了,因为已经内置了。如果要使用 Viewmodel和 Livedata则需要加依赖,这两个后续会有文章分析。

首先,既然是生命周期的监听,那就会有观察者和被观察者。被观察者需要实现的接口是 LifecycleOwner,也就是生命周期拥有者(Activity 或 Fragment)。这两者都实现了 LifecycleOwner接口,我们可以看一下 Activity 的父类 ComponentActivity:

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
...
...
{

生命周期观察者需要实现的接口是 LifecycleObserver。下面我们就结合上次 MVP架构的例子,给 Presenter添加 LifeCycle生命周期监听方法。

首先让 BasePresenter实现观察者接口:

// 实现 LifecycleObserver
public class BasePresenter implements LifecycleObserver {
private V mView;
public void attach(V iView) {
this.mView = iView;
}
public void detach() {
this.mView = null;
}
public V getView() {
return mView;
}
}

然后我们给 BasePresenter的实现类 Presenter添加几个生命周期的方法,并加上**@OnLifecycleEvent**注解:

//  Presenter.java

// onCreate
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void onCreate(){
Log.d(TAG, "-----------LifecycleObserver -- onCreate");
}
// onStart
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onStart(){
Log.d(TAG, "-----------LifecycleObserver -- onStart");
}
// onPause
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void onPause(){
Log.d(TAG, "-----------LifecycleObserver -- onPause");
}
// onStop
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onStop(){
Log.d(TAG, "-----------LifecycleObserver -- onStop");
}
// onDestroy
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy(){
Log.d(TAG, "-----------LifecycleObserver -- onDestroy");
}

然后在 BaseMvpActivity初始化时添加观察者:

// BaseMvpActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView();
initView();
initData();
mPresenter = createP();
//mPresenter.attach(this);
// 注释 1, 添加观察者
getLifecycle().addObserver(mPresenter);
}
@Override
protected void onDestroy() {
super.onDestroy();
// mPresenter.detach();
// 移除观察者
getLifecycle().removeObserver(mPresenter);
}

上次例子使用了模板设计模式,所以现在生命周期观察者的添加和移除也放在模板里了。 然后打开 Activity后退出,看打印结果:

com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onCreate
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onStart
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onPause
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onStop
com.ethan.mvpapplication D/Presenter: -----------LifecycleObserver -- onDestroy

Demo: MVP

2、源码分析

下面我们来分析一下 Lifecycle生命周期监听的原理。简单粗暴,直接点进上面注释 1的getLifecycle()方法,看看干了啥:

//  ComponentActivity.java

private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);

@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}

返回的是一个 LifecycleRegistry 对象,我们可以点进去看。LifecycleRegistry 是抽象类 Lifecycle的实现类:

public abstract class Lifecycle {
@MainThread
public abstract void addObserver(@NonNull LifecycleObserver observer);
@MainThread
public abstract void removeObserver(@NonNull LifecycleObserver observer);
@MainThread
@NonNull
public abstract androidx.lifecycle.Lifecycle.State getCurrentState();
public enum Event {
ON_CREATE,
ON_START,
ON_RESUME,
ON_PAUSE,
ON_STOP,
ON_DESTROY,
ON_ANY
}
public enum State {
DESTROYED,
INITIALIZED,
CREATED,
STARTED,
RESUMED;
public boolean isAtLeast(@NonNull androidx.lifecycle.Lifecycle.State state) {
return compareTo(state) >= 0;
}
}
}

上面抽象类 Lifecycle 不仅包含了添加和移除观察者的方法,还包含了 Event 和 State 两个枚举。我们可以看到,Event 这个枚举包含了 Activity最主要的几个生命周期的方法。

下面我们继续看 LifeCycle是怎么监听生命周期的:

  public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
......{

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......
// 注释 2, ComponentActivity注入 ReportFragment
ReportFragment.injectIfNeededIn(this);
......
}
......

@Override
public androidx.lifecycle.Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
}

按理说可以在上面的 ComponentActivity的各个生命周期回调中调用观察者的对应方法,但我们可以看到 ComponentActivity 这个类里并没有这样做。而是在上面注释 2处将当前Activity对象注入 ReportFragment中,我们看看ReportFragment干了啥:

 public class ReportFragment extends Fragment {
private static final String REPORT_FRAGMENT_TAG = "androidx.lifecycle"
+ ".LifecycleDispatcher.report_fragment_tag";

public static void injectIfNeededIn(Activity activity) {
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
// 注释 3, 创建ReportFragment 添加到 Activity,使得生命周期与之同步
manager.beginTransaction().add(new androidx.lifecycle.ReportFragment(), REPORT_FRAGMENT_TAG).commit();
manager.executePendingTransactions();
}
}
static androidx.lifecycle.ReportFragment get(Activity activity) {
return (androidx.lifecycle.ReportFragment) activity.getFragmentManager().findFragmentByTag(
REPORT_FRAGMENT_TAG);
}
// 分发生命周期回调事件
private void dispatchCreate(androidx.lifecycle.ReportFragment.ActivityInitializationListener listener) {
if (listener != null) {
listener.onCreate();
}
}
......
......
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
dispatchCreate(mProcessListener);
dispatch(androidx.lifecycle.Lifecycle.Event.ON_CREATE);
}

@Override
public void onStart() {
super.onStart();
// 注释 4, 开始调用观察者的生命周期
dispatchStart(mProcessListener);
dispatch(androidx.lifecycle.Lifecycle.Event.ON_START);
}
......
......
private void dispatch(androidx.lifecycle.Lifecycle.Event event) {
Activity activity = getActivity();
if (activity instanceof LifecycleRegistryOwner) {
((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
return;
}

if (activity instanceof LifecycleOwner) {
androidx.lifecycle.Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
if (lifecycle instanceof LifecycleRegistry) {
// 注释 5,生命周期事件分发
((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
}
}
}
}

上面注释 3处我们可以看到,这里创建了一个 Fragment,然后将 Fragment添加到 Activity中。这样的话,新创建的这个 Fragment和 Activity就可以同步生命周期。之后,在上面注释 4的地方,Fragment的各种生命周期的方法里就可以调用观察者(LifecycleObserver)的相关的生命周期方法了。

也就是说,ComponentActivity 创建了一个 ReportFragment ,并把生命周期回调的事务交给了Fragment。这sao操作是不是很熟悉?没错!我们之前分析过,Glide 也是这么管理生命周期的。

上面注释 5,我们再看看生命周期事件分发,看看观察者方法最终调用的地方:

  // LifecycleRegistry.java
static class ObserverWithState {
androidx.lifecycle.Lifecycle.State mState;
LifecycleEventObserver mLifecycleObserver;
ObserverWithState(LifecycleObserver observer, androidx.lifecycle.Lifecycle.State initialState) {
// 获取 ReflectiveGenericLifecycleObserver对象
mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
mState = initialState;
}

void dispatchEvent(LifecycleOwner owner, androidx.lifecycle.Lifecycle.Event event) {
......
// 生命周期回调事件分发
mLifecycleObserver.onStateChanged(owner, event);
}
}

// Lifecycling.java
static LifecycleEventObserver lifecycleEventObserver(Object object) {
.....
// 返回 ReflectiveGenericLifecycleObserver对象
return new androidx.lifecycle.ReflectiveGenericLifecycleObserver(object);
}

// ReflectiveGenericLifecycleObserver.java
class ReflectiveGenericLifecycleObserver implements LifecycleEventObserver {
private final Object mWrapped;
private final ClassesInfoCache.CallbackInfo mInfo;
ReflectiveGenericLifecycleObserver(Object wrapped) {
mWrapped = wrapped;
mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
}

@Override
public void onStateChanged(LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event) {
// 生命周期回调
mInfo.invokeCallbacks(source, event, mWrapped);
}
}

// ClassesInfoCache.java
private ClassesInfoCache.CallbackInfo createInfo(Class klass, @Nullable Method[] declaredMethods) {
Class superclass = klass.getSuperclass();
for (Method method : methods) {
// 反射遍历观察者的各个方法,将带 @OnLifecycleEvent注解的方法保存在
// Map对象中,方便生命周期变化时调用
OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
if (annotation == null) { continue; }
Class[] params = method.getParameterTypes();
......
androidx.lifecycle.Lifecycle.Event event = annotation.value();
......
ClassesInfoCache.MethodReference methodReference = new ClassesInfoCache.MethodReference(callType, method);
verifyAndPutHandler(handlerToEvent, methodReference, event, klass);
mCallbackMap.put(klass, info);
mHasLifecycleMethods.put(klass, hasLifecycleMethods);
}
......
return info;
}
static class CallbackInfo {
final Map> mEventToHandlers;
final Map mHandlerToEvent;

CallbackInfo(Map handlerToEvent) {
mHandlerToEvent = handlerToEvent;
mEventToHandlers = new HashMap<>();
for (Map.Entry entry : handlerToEvent.entrySet()) {
androidx.lifecycle.Lifecycle.Event event = entry.getValue();
List methodReferences = mEventToHandlers.get(event);
if (methodReferences == null) {
methodReferences = new ArrayList<>();
mEventToHandlers.put(event, methodReferences);
}
methodReferences.add(entry.getKey());
}
}

@SuppressWarnings("ConstantConditions")
void invokeCallbacks(LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event, Object target) {
invokeMethodsForEvent(mEventToHandlers.get(event), source, event, target);
invokeMethodsForEvent(mEventToHandlers.get(androidx.lifecycle.Lifecycle.Event.ON_ANY), source, event,
target);
}

private static void invokeMethodsForEvent(List handlers,
LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event, Object mWrapped) {
if (handlers != null) {
for (int i = handlers.size() - 1; i >= 0; i--) {
// 调用 MethodReference的 invokeCallback方法
handlers.get(i).invokeCallback(source, event, mWrapped);
}
}
}
}

// ClassesInfoCache.MethodReference
static class MethodReference {
final int mCallType;
final Method mMethod;
.......
void invokeCallback(LifecycleOwner source, androidx.lifecycle.Lifecycle.Event event, Object target) {
//noinspection TryWithIdenticalCatches
try {
switch (mCallType) {
case CALL_TYPE_NO_ARG:
mMethod.invoke(target);
break;
case CALL_TYPE_PROVIDER:
mMethod.invoke(target, source);
break;
case CALL_TYPE_PROVIDER_WITH_EVENT:
mMethod.invoke(target, source, event);
break;
}
} catch (InvocationTargetException e) {
throw new RuntimeException("Failed to call observer method", e.getCause());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

时序图就不画了,大概写一下调用流程吧:

LifecycleRegistry.java

  • --> handleLifecycleEvent();
  • --> moveToState(next);
  • --> sync();
  • --> forwardPass(lifecycleOwner);
  • --> LifecycleRegistry.ObserverWithState --> dispatchEvent(owner, event);

ReflectiveGenericLifecycleObserver .java

  • --> onStateChanged()

ClassesInfoCache.java

  • --> CallbackInfo createInfo(); (反射将观察者带注解的方法保存)

MethodReference.java

  • -->invokeCallback(source, event, mWrapped);
  • --> mMethod.invoke(target); (反射调用观察者的生命周期的方法)

经过层层调用和包装,注册时最终会用反射将观察者带**@OnLifecycleEvent注解的方法保存在MethodReference中,并放入HashMap。当 Activity生命周期改变时,随着层层调用,最终保存在集合里的观察者LifecycleObserver**生命周期相关方法会被调用。

这波sao操作是不是又似曾相识?没错!EventBus也是这么管理订阅和发送事件的。


收起阅读 »

View的绘制流程 硬件渲染

负责硬件渲染的主体对象ThreadedRenderer在整个绘制流程中做了哪几个步骤。1.enableHardwareAcceleration 实例化ThreadedRenderer2.initialize 初始化3.updateSurface 更新Surfa...
继续阅读 »

负责硬件渲染的主体对象ThreadedRenderer在整个绘制流程中做了哪几个步骤。

  • 1.enableHardwareAcceleration 实例化ThreadedRenderer
  • 2.initialize 初始化
  • 3.updateSurface 更新Surface
  • 4.setup 启动ThreadedRenderer设置阴影等参数
  • 5.如果需要 执行invalidateRoot 判断是否需要从根部开始遍历查找无效的元素
  • 6.draw 开始硬件渲染进行View层级绘制
  • 7.updateDisplayListIfDirty 更新硬件渲染中的脏区
  • 8.destroy 销毁硬件渲染对象

在硬件渲染的过程中,有一个很核心的对象RenderNode,作为每一个View绘制的节点对象。

当每一次进行准备进行绘制的时候,都会雷打不动执行如下三个步骤:

  • 1.RenderNode.start 生成一个新的DisplayListCanvas
  • 2.DisplayListCanvas 上进行绘制 如调用Drawable的draw方法,把DisplayListCanvas作为参数
  • 3.RenderNode.end 完成RenderNode的操作

重要对象

  • 1.ThreadedRenderer 管理所有的硬件渲染对象,也是ViewRootImpl进行硬件渲染的入口对象。
  • 2.RenderNode 每一个View都会携带的对象,当打开了硬件渲染的时候,将会根据判断,把相关的渲染逻辑移动到RenderNode中。
  • 3.DisplayListCanvas 每一个RenderNode真正开始绘制自己的内容之前,需要通过RenderNode生成一个DisplayListCanvas,所有的绘制的行为都会在DisplayListCanvas中绘制,最后DisplayListCanvas会保存会RenderNode中。

在Java层中面向Framework中,只有这么多,下面是一一映射的简图。

image.png

能看到实际上RenderNode也会跟着View 树的构建同时一起构建整个显示层级。也是因此ThreadedRender也能以RenderNode为线索构建出一套和软件渲染一样的渲染流程。

让我继续介绍一下,在硬件渲染中native层的核心对象。

  • 1.RootRenderNode 所有RenderNode的根部RenderNode,一切的View层级结构遍历都从这个RenderNode开始。类似View中DecorView的职责。但是DecorView并非和RootRenderNode对应,而是拥有自己的RenderNode。
  • 2.RenderNode 对应于Java层的native对象
  • 3.RenderThread 硬件渲染线程,所有的渲染任务都会在该线程中使用硬件渲染线程的Looper进行。
  • 4.CanvasContext 是所有的渲染的上下文,它将持用PipeLine渲染管道
  • 5.PipeLine 如OpenGLPipeLine,SkiaOpenGLPipeLine,VulkanPipeLine渲染管道。而这个渲染管道将会根据Android系统的配置,执行真正的渲染行为
  • 6.DrawFrameTask 是整个ThreadedRender中真正开始执行渲染的对象
  • 7.RenderNodeProxy ThreadedRender的对应native层的入口。它将全局的作为RootRenderNode,CanvasContext,以及RenderThread门面(门面设计模式)。

如下是一个思维导图:

image.png

ThreadedRenderer 实例化

当发现mSurfaceHolder为空的时候会调用如下函数:

                if (mSurfaceHolder == null) {
enableHardwareAcceleration(attrs);
....
}

而这个方法则调用如下的方法对ThreadedRenderer进行创建:

 mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent,
attrs.getTitle().toString());
    public static boolean isAvailable() {
if (sSupportsOpenGL != null) {
return sSupportsOpenGL.booleanValue();
}
if (SystemProperties.getInt("ro.kernel.qemu", 0) == 0) {
sSupportsOpenGL = true;
return true;
}
int qemu_gles = SystemProperties.getInt("qemu.gles", -1);
if (qemu_gles == -1) {
return false;
}
sSupportsOpenGL = qemu_gles > 0;
return sSupportsOpenGL.booleanValue();
}


public static ThreadedRenderer create(Context context, boolean translucent, String name) {
ThreadedRenderer renderer = null;
if (isAvailable()) {
renderer = new ThreadedRenderer(context, translucent, name);
}
return renderer;
}

能不能创建的了ThreadedRenderer则决定于全局配置。如果ro.kernel.qemu的配置为0,说明支持OpenGL 则可以直接返回true。如果qemu.gles为-1说明不支持OpenGL es返回false,只能使用软件渲染。如果设置了qemu.gles并大于0,才能打开硬件渲染。

ThreadedRenderer构造函数

    ThreadedRenderer(Context context, boolean translucent, String name) {
...

long rootNodePtr = nCreateRootRenderNode();
mRootNode = RenderNode.adopt(rootNodePtr);
mRootNode.setClipToBounds(false);
mIsOpaque = !translucent;
mNativeProxy = nCreateProxy(translucent, rootNodePtr);
nSetName(mNativeProxy, name);

ProcessInitializer.sInstance.init(context, mNativeProxy);

loadSystemProperties();
}

我们能看到ThreadedRenderer在初始化,做了三件事情:

  • 1.nCreateRootRenderNode 创建native层的RootRenderNode,也就是所有RenderNode的根。类似DecorView的角色,是所有View的父布局,我们把整个View层次看成一个树,那么这里是根节点。
  • 2.RenderNode.adopt 根据native的 RootRenderNode创建Java层的根部RenderNode。
  • 3.nCreateProxy 创建RenderNode的代理者,nSetName给该代理者赋予名字。
  • 4.ProcessInitializer的初始化graphicsstats服务
  • 5.loadSystemProperties 读取系统给硬件渲染器设置的属性。

关键是看1-3点中ThreadRenderer都做了什么。

nCreateRootRenderNode

static jlong android_view_ThreadedRenderer_createRootRenderNode(JNIEnv* env, jobject clazz) {
RootRenderNode* node = new RootRenderNode(env);
node->incStrong(0);
node->setName("RootRenderNode");
return reinterpret_cast<jlong>(node);
}

能看到这里是直接实例化一个RootRenderNode对象,并把指针的地址直接返回。

class RootRenderNode : public RenderNode, ErrorHandler {
public:
explicit RootRenderNode(JNIEnv* env) : RenderNode() {
mLooper = Looper::getForThread();
env->GetJavaVM(&mVm);
}
}

能看到RootRenderNode继承了RenderNode对象,并且保存一个JavaVM也就是我们所说的Java虚拟机对象,一个java进程全局只有一个。同时通过getForThread方法,获取ThreadLocal中的Looper对象。这里实际上拿的就是UI线程的Looper。

native层RenderNode 的实例化
RenderNode::RenderNode()
: mDirtyPropertyFields(0)
, mNeedsDisplayListSync(false)
, mDisplayList(nullptr)
, mStagingDisplayList(nullptr)
, mAnimatorManager(*this)
, mParentCount(0) {}

在这个构造函数有一个mDisplayList十分重要,记住之后会频繁出现。接着来看看RenderNode的头文件:

class RenderNode : public VirtualLightRefBase {
friend class TestUtils; // allow TestUtils to access syncDisplayList / syncProperties
friend class FrameBuilder;


...

private:
...
} /* namespace uirenderer */
} /* namespace android */

实际上我把几个重要的对象留下来:

  • 1.mDisplayList 实际上就是RenderNode中持有的所有的子RenderNode对象
  • 2.mStagingDisplayList 这个一般是一个View遍历完后保存下来的DisplayList,之后会在绘制行为之前转化为mDisplayList
  • 3.RenderProperties mProperties 是指RenderNode的宽高等信息的存储对象
  • 4.OffscreenBuffer mProperties RenderNode真正的渲染内存对象。

RenderNode.adopt

    public static RenderNode adopt(long nativePtr) {
return new RenderNode(nativePtr);
}

能看到很简单,就是包裹一个native层的RenderNode返回一个Java层对应的对象开放Java层的操作API。

nCreateProxy

static jlong android_view_ThreadedRenderer_createProxy(JNIEnv* env, jobject clazz,
jboolean translucent, jlong rootRenderNodePtr) {
RootRenderNode* rootRenderNode = reinterpret_cast<RootRenderNode*>(rootRenderNodePtr);
ContextFactoryImpl factory(rootRenderNode);
return (jlong) new RenderProxy(translucent, rootRenderNode, &factory);
}

能看到这个过程生成了两个对象:

  • 1.ContextFactoryImpl 动画上下文工厂
class ContextFactoryImpl : public IContextFactory {
public:
explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {}

virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) {
return new AnimationContextBridge(clock, mRootNode);
}

private:
RootRenderNode* mRootNode;
};

这个对象实际上让RenderProxy持有一个创建动画上下文的工厂。RenderProxy可以通过ContextFactoryImpl为每一个RenderNode创建一个动画执行对象的上下文AnimationContextBridge。

  • 2.RenderProxy 一个 根RenderNode的代理对象。这个代理对象将作为所有绘制开始遍历入口。

RenderProxy 根RenderNode的代理对象的创建

RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode,
IContextFactory* contextFactory)
: mRenderThread(RenderThread::getInstance()), mContext(nullptr) {
mContext = mRenderThread.queue().runSync([&]() -> CanvasContext* {
return CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
});
mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode);
}
  • 1.RenderThread 硬件渲染线程,所有的硬件渲染命令都需要经过这个线程排队执行。初始化方法如下:
RenderThread::getInstance()
  • 2.CanvasContext 一个硬件Canvas的上下文,一般来说就在这个上下文决定了使用OpenGL es还是其他的渲染管道。初始化方法如下:
CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory);
  • 2.DrawFrameTask 每一帧绘制的任务对象。

我们依次看看他们初始化都做了什么。

RenderThread的初始化和运行机制

RenderThread& RenderThread::getInstance() {
static RenderThread* sInstance = new RenderThread();
gHasRenderThreadInstance = true;
return *sInstance;
}

能看到其实就是简单的调用RenderThread的构造函数进行实例化,并且返回对象的指针。

RenderThread是一个线程对象。先来看看其头文件继承的对象:

class RenderThread : private ThreadBase {
PREVENT_COPY_AND_ASSIGN(RenderThread);

public:
// Sets a callback that fires before any RenderThread setup has occured.
ANDROID_API static void setOnStartHook(void (*onStartHook)());

WorkQueue& queue() { return ThreadBase::queue(); }
...
}

其中RenderThread的中进行排队处理的任务队列实际上是来自ThreadBase的WorkQueue对象。

class ThreadBase : protected Thread {
PREVENT_COPY_AND_ASSIGN(ThreadBase);

public:
ThreadBase()
: Thread(false)
, mLooper(new Looper(false))
, mQueue([this]() { mLooper->wake(); }, mLock) {}

WorkQueue& queue() { return mQueue; }

void requestExit() {
Thread::requestExit();
mLooper->wake();
}

void start(const char* name = "ThreadBase") { Thread::run(name); }
...
}

ThreadBase则是继承于Thread对象。当调用start方法时候其实就是调用Thread的run方法启动线程。

另一个更加关键的对象,就是实例化一个Looper对象到WorkQueue中。而直接实例化Looper实际上就是新建一个Looper。但是这个Looper并没有获取当先线程的Looper,这个Looper做什么的呢?下文就会揭晓。

WorkQueue把一个Looper的方法指针设置到其中,其作用可能是完成了某一件任务后唤醒Looper继续工作。

RenderThread::RenderThread()
: ThreadBase()
, mVsyncSource(nullptr)
, mVsyncRequested(false)
, mFrameCallbackTaskPending(false)
, mRenderState(nullptr)
, mEglManager(nullptr)
, mVkManager(nullptr) {
Properties::load();
start("RenderThread");
}
  • 1.先从Properties读取一些全局配置,进行一些如debug的配置。
  • 2.start启动当前的线程

而start方法会启动Thread的run方法。而run方法最终会走到threadLoop方法中,至于是怎么走进来的,之后有机会会解剖虚拟机的源码线程篇章进行讲解。

RenderThread::threadLoop

bool RenderThread::threadLoop() {
setpriority(PRIO_PROCESS, 0, PRIORITY_DISPLAY);
if (gOnStartHook) {
gOnStartHook();
}
initThreadLocals();

while (true) {
waitForWork();
processQueue();

if (mPendingRegistrationFrameCallbacks.size() && !mFrameCallbackTaskPending) {
drainDisplayEventQueue();
mFrameCallbacks.insert(mPendingRegistrationFrameCallbacks.begin(),
mPendingRegistrationFrameCallbacks.end());
mPendingRegistrationFrameCallbacks.clear();
requestVsync();
}

if (!mFrameCallbackTaskPending && !mVsyncRequested && mFrameCallbacks.size()) {
requestVsync();
}
}

return false;
}

在threadloop中关键的步骤有如下四个:

  • 1.initThreadLocals 初始化线程本地变量
  • 2.waitForWork 等待RenderThread的渲染工作
  • 3.processQueue 执行保存在WorkQueue的渲染工作
  • 4.mPendingRegistrationFrameCallbacks大于0或者mFrameCallbacks大于0;并且mFrameCallbackTaskPending为false,则会调用requestVsync,打开SF进程的EventThread的阻塞让监听返回。mFrameCallbackTaskPending这个方法代表Vsync信号来了并且执行则mFrameCallbackTaskPending为true。
initThreadLocals
void RenderThread::initThreadLocals() {
mDisplayInfo = DeviceInfo::queryDisplayInfo();
nsecs_t frameIntervalNanos = static_cast<nsecs_t>(1000000000 / mDisplayInfo.fps);
mTimeLord.setFrameInterval(frameIntervalNanos);
initializeDisplayEventReceiver();
mEglManager = new EglManager(*this);
mRenderState = new RenderState(*this);
mVkManager = new VulkanManager(*this);
mCacheManager = new CacheManager(mDisplayInfo);
}

在这个过程中创建了几个核心对象:

  • 1.EglManager 当使用OpenGL 相关的管道的时候,将会通过EglManager对OpenGL进行上下文等操作。
  • 2.VulkanManager 当使用Vulkan 的渲染管道,将会使用VulkanManager进行操作(Vulkan 是新一代的3d硬件显卡渲染api,比起OpenGL更加轻量化,性能更佳)
  • 3.RenderState 渲染状态,内有OpenGL和Vulkan的管道,需要渲染的Layer等。

另一个核心的方法就是initializeDisplayEventReceiver,这个方法为WorkQueue的Looper注册了监听:

void RenderThread::initializeDisplayEventReceiver() {
LOG_ALWAYS_FATAL_IF(mVsyncSource, "Initializing a second DisplayEventReceiver?");

if (!Properties::isolatedProcess) {
auto receiver = std::make_unique<DisplayEventReceiver>();
status_t status = receiver->initCheck();

mLooper->addFd(receiver->getFd(), 0, Looper::EVENT_INPUT,
RenderThread::displayEventReceiverCallback, this);
mVsyncSource = new DisplayEventReceiverWrapper(std::move(receiver));
} else {
mVsyncSource = new DummyVsyncSource(this);
}
}

能看到在这个Looper中注册了对DisplayEventReceiver的监听,也就是Vsync信号的监听,回调方法为displayEventReceiverCallback。

我们暂时先对RenderThread的initializeDisplayEventReceiver方法探索到这里,我们稍后继续看看回调后的逻辑。

waitForWork 对Looper监听的对象进行阻塞等待
    void waitForWork() {
nsecs_t nextWakeup;
{
std::unique_lock lock{mLock};
nextWakeup = mQueue.nextWakeup(lock);
}
int timeout = -1;
if (nextWakeup < std::numeric_limits<nsecs_t>::max()) {
timeout = ns2ms(nextWakeup - WorkQueue::clock::now());
if (timeout < 0) timeout = 0;
}
int result = mLooper->pollOnce(timeout);
}

能看到这里的逻辑很简单实际上就是调用Looper的pollOnce方法,阻塞Looper中的循环,直到Vsync的信号到来才会继续往下执行。

processQueue
void processQueue() { mQueue.process(); }

实际上调用的是WorkQueue的process方法。

WorkQueue的process
    void process() {
auto now = clock::now();
std::vector<WorkItem> toProcess;
{
std::unique_lock _lock{mLock};
if (mWorkQueue.empty()) return;
toProcess = std::move(mWorkQueue);
auto moveBack = find_if(std::begin(toProcess), std::end(toProcess),
[&now](WorkItem& item) { return item.runAt > now; });
if (moveBack != std::end(toProcess)) {
mWorkQueue.reserve(std::distance(moveBack, std::end(toProcess)) + 5);
std::move(moveBack, std::end(toProcess), std::back_inserter(mWorkQueue));
toProcess.erase(moveBack, std::end(toProcess));
}
}
for (auto& item : toProcess) {
item.work();
}
}

能看到这个过程中很简单,几乎和Message的loop的逻辑一致。如果Looper的阻塞打开了,则首先找到预计执行时间比当前时刻都大的WorkItem。并且从mWorkQueue移除,最后添加到toProcess中,并且执行每一个WorkItem的work方法。而每一个WorkItem其实就是通过从某一个压入方法添加到mWorkQueue中。

到这里,我们就明白了RenderThread中是如何消费渲染任务的。那么这些渲染任务又是哪里诞生呢?

RenderThread 相应Vsync信号的回调

在RenderThread中的Looper会监听Vsync信号,当信号回调后将会执行下面的回调。

displayEventReceiverCallback

int RenderThread::displayEventReceiverCallback(int fd, int events, void* data) {
if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) {
return 0; // remove the callback
}

if (!(events & Looper::EVENT_INPUT)) {
return 1; // keep the callback
}

reinterpret_cast<RenderThread*>(data)->drainDisplayEventQueue();

return 1; // keep the callback
}

能看到这个方法的核心实际上就是调用drainDisplayEventQueue方法,对ui渲染任务队列进行处理。

RenderThread::drainDisplayEventQueue
void RenderThread::drainDisplayEventQueue() {
ATRACE_CALL();
nsecs_t vsyncEvent = mVsyncSource->latestVsyncEvent();
if (vsyncEvent > 0) {
mVsyncRequested = false;
if (mTimeLord.vsyncReceived(vsyncEvent) && !mFrameCallbackTaskPending) {
mFrameCallbackTaskPending = true;
nsecs_t runAt = (vsyncEvent + DISPATCH_FRAME_CALLBACKS_DELAY);
queue().postAt(runAt, [this]() { dispatchFrameCallbacks(); });
}
}
}

能到在这里mVsyncRequested设置为false,且mFrameCallbackTaskPending将会设置为true,并且调用queue的postAt的方法执行ui渲染方法。

实际上就是保存这三对象RenderThread;CanvasContext;RenderNode。

    template <class F>
auto runSync(F&& func) -> decltype(func()) {
std::packaged_task<decltype(func())()> task{std::forward<F>(func)};
post([&task]() { std::invoke(task); });
return task.get_future().get();
};

能看到这个方法实际上也是调用post执行排队执行任务,不同的是,这里使用了线程的Future方式,阻塞了执行,等待CanvasContext的setName工作完毕。


收起阅读 »

iOS - 图像IO 一

图像IO潜伏期值得思考 - 凯文 帕萨特    在第13章“高效绘图”中,我们研究了和Core Graphics绘图相关的性能问题,以及如何修复。和绘图性能相关紧密相关的是图像性能。在这一章中,我们将研究如何优...
继续阅读 »

图像IO

潜伏期值得思考 - 凯文 帕萨特

    在第13章“高效绘图”中,我们研究了和Core Graphics绘图相关的性能问题,以及如何修复。和绘图性能相关紧密相关的是图像性能。在这一章中,我们将研究如何优化从闪存驱动器或者网络中加载和显示图片。

加载和潜伏

    绘图实际消耗的时间通常并不是影响性能的因素。图片消耗很大一部分内存,而且不太可能把需要显示的图片都保留在内存中,所以需要在应用运行的时候周期性地加载和卸载图片。

    图片文件加载的速度被CPU和IO(输入/输出)同时影响。iOS设备中的闪存已经比传统硬盘快很多了,但仍然比RAM慢将近200倍左右,这就需要很小心地管理加载,来避免延迟。

    只要有可能,试着在程序生命周期不易察觉的时候来加载图片,例如启动,或者在屏幕切换的过程中。按下按钮和按钮响应事件之间最大的延迟大概是200ms,这比动画每一帧切换的16ms小得多。你可以在程序首次启动的时候加载图片,但是如果20秒内无法启动程序的话,iOS检测计时器就会终止你的应用(而且如果启动大于2,3秒的话用户就会抱怨了)。

    有些时候,提前加载所有的东西并不明智。比如说包含上千张图片的图片传送带:用户希望能够能够平滑快速翻动图片,所以就不可能提前预加载所有图片;那样会消耗太多的时间和内存。

    有时候图片也需要从远程网络连接中下载,这将会比从磁盘加载要消耗更多的时间,甚至可能由于连接问题而加载失败(在几秒钟尝试之后)。你不能够在主线程中加载网络造成等待,所以需要后台线程。

线程加载

    在第12章“性能调优”我们的联系人列表例子中,图片都非常小,所以可以在主线程同步加载。但是对于大图来说,这样做就不太合适了,因为加载会消耗很长时间,造成滑动的不流畅。滑动动画会在主线程的run loop中更新,所以会有更多运行在渲染服务进程中CPU相关的性能问题。

    清单14.1显示了一个通过UICollectionView实现的基础的图片传送器。图片在主线程中-collectionView:cellForItemAtIndexPath:方法中同步加载(见图14.1)。

清单14.1 使用UICollectionView实现的图片传送器


#import "ViewController.h"

@interface ViewController()

@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
//set up data
self.imagePaths =
[[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
//register cell class
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

//add image view
const NSInteger imageTag = 99;
UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame: cell.contentView.bounds];
imageView.tag = imageTag;
[cell.contentView addSubview:imageView];
}
//set image
NSString *imagePath = self.imagePaths[indexPath.row];
imageView.image = [UIImage imageWithContentsOfFile:imagePath];
return cell;
}

@end

图14.1

图14.1 运行中的图片传送器

    传送器中的图片尺寸为800x600像素的PNG,对iPhone5来说,1/60秒要加载大概700KB左右的图片。当传送器滚动的时候,图片也在实时加载,于是(预期中的)卡动就发生了。时间分析工具(图14.2)显示了很多时间都消耗在了UIImage+imageWithContentsOfFile:方法中了。很明显,图片加载造成了瓶颈。

图14.2

图14.2 时间分析工具展示了CPU瓶颈

    这里提升性能唯一的方式就是在另一个线程中加载图片。这并不能够降低实际的加载时间(可能情况会更糟,因为系统可能要消耗CPU时间来处理加载的图片数据),但是主线程能够有时间做一些别的事情,比如响应用户输入,以及滑动动画。

    为了在后台线程加载图片,我们可以使用GCD或者NSOperationQueue创建自定义线程,或者使用CATiledLayer。为了从远程网络加载图片,我们可以使用异步的NSURLConnection,但是对本地存储的图片,并不十分有效。

GCD和NSOperationQueue

    GCD(Grand Central Dispatch)和NSOperationQueue很类似,都给我们提供了队列闭包块来在线程中按一定顺序来执行。NSOperationQueue有一个Objecive-C接口(而不是使用GCD的全局C函数),同样在操作优先级和依赖关系上提供了很好的粒度控制,但是需要更多地设置代码。

    清单14.2显示了在低优先级的后台队列而不是主线程使用GCD加载图片的-collectionView:cellForItemAtIndexPath:方法,然后当需要加载图片到视图的时候切换到主线程,因为在后台线程访问视图会有安全隐患。

    由于视图在UICollectionView会被循环利用,我们加载图片的时候不能确定是否被不同的索引重新复用。为了避免图片加载到错误的视图中,我们在加载前把单元格打上索引的标签,然后在设置图片的时候检测标签是否发生了改变。

清单14.2 使用GCD加载传送图片

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell"
forIndexPath:indexPath];
//add image view
const NSInteger imageTag = 99;
UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
if (!imageView) {
imageView = [[UIImageView alloc] initWithFrame: cell.contentView.bounds];
imageView.tag = imageTag;
[cell.contentView addSubview:imageView];
}
//tag cell with index and clear current image
cell.tag = indexPath.row;
imageView.image = nil;
//switch to background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image; }
});
});
return cell;
}

    当运行更新后的版本,性能比之前不用线程的版本好多了,但仍然并不完美(图14.3)。

    我们可以看到+imageWithContentsOfFile:方法并不在CPU时间轨迹的最顶部,所以我们的确修复了延迟加载的问题。问题在于我们假设传送器的性能瓶颈在于图片文件的加载,但实际上并不是这样。加载图片数据到内存中只是问题的第一部分。

图14.3

图14.3 使用后台线程加载图片来提升性能

延迟解压

    一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。

    用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。

    当加载图片的时候,iOS通常会延迟解压图片的时间,直到加载到内存之后。这就会在准备绘制图片的时候影响性能,因为需要在绘制之前进行解压(通常是消耗时间的问题所在)。

    最简单的方法就是使用UIImage+imageNamed:方法避免延时加载。不像+imageWithContentsOfFile:(和其他别的UIImage加载方法),这个方法会在加载图片之后立刻进行解压(就和本章之前我们谈到的好处一样)。问题在于+imageNamed:只对从应用资源束中的图片有效,所以对用户生成的图片内容或者是下载的图片就没法使用了。

    另一种立刻加载图片的方法就是把它设置成图层内容,或者是UIImageViewimage属性。不幸的是,这又需要在主线程执行,所以不会对性能有所提升。

    第三种方式就是绕过UIKit,像下面这样使用ImageIO框架:

NSInteger index = indexPath.row;
NSURL *imageURL = [NSURL fileURLWithPath:self.imagePaths[index]];
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CFRelease(source);

    这样就可以使用kCGImageSourceShouldCache来创建图片,强制图片立刻解压,然后在图片的生命周期保留解压后的版本。

    最后一种方式就是使用UIKit加载图片,但是立刻会知道CGContext中去。图片必须要在绘制之前解压,所以就强制了解压的及时性。这样的好处在于绘制图片可以再后台线程(例如加载本身)执行,而不会阻塞UI。

    有两种方式可以为强制解压提前渲染图片:

  • 将图片的一个像素绘制成一个像素大小的CGContext。这样仍然会解压整张图片,但是绘制本身并没有消耗任何时间。这样的好处在于加载的图片并不会在特定的设备上为绘制做优化,所以可以在任何时间点绘制出来。同样iOS也就可以丢弃解压后的图片来节省内存了。

  • 将整张图片绘制到CGContext中,丢弃原始的图片,并且用一个从上下文内容中新的图片来代替。这样比绘制单一像素那样需要更加复杂的计算,但是因此产生的图片将会为绘制做优化,而且由于原始压缩图片被抛弃了,iOS就不能够随时丢弃任何解压后的图片来节省内存了。

    需要注意的是苹果特别推荐了不要使用这些诡计来绕过标准图片解压逻辑(所以也是他们选择用默认处理方式的原因),但是如果你使用很多大图来构建应用,那如果想提升性能,就只能和系统博弈了。

    如果不使用+imageNamed:,那么把整张图片绘制到CGContext可能是最佳的方式了。尽管你可能认为多余的绘制相较别的解压技术而言性能不是很高,但是新创建的图片(在特定的设备上做过优化)可能比原始图片绘制的更快。

    同样,如果想显示图片到比原始尺寸小的容器中,那么一次性在后台线程重新绘制到正确的尺寸会比每次显示的时候都做缩放会更有效(尽管在这个例子中我们加载的图片呈现正确的尺寸,所以不需要多余的优化)。

        如果修改了-collectionView:cellForItemAtIndexPath:方法来重绘图片(清单14.3),你会发现滑动更加平滑。

清单14.3 强制图片解压显示

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
...
//switch to background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect:imageView.bounds];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image;
}
});
});
return cell;
}

CATiledLayer

    如第6章“专用图层”中的例子所示,CATiledLayer可以用来异步加载和显示大型图片,而不阻塞用户输入。但是我们同样可以使用CATiledLayerUICollectionView中为每个表格创建分离的CATiledLayer实例加载传动器图片,每个表格仅使用一个图层。

    这样使用CATiledLayer有几个潜在的弊端:

  • CATiledLayer的队列和缓存算法没有暴露出来,所以我们只能祈祷它能匹配我们的需求

  • CATiledLayer需要我们每次重绘图片到CGContext中,即使它已经解压缩,而且和我们单元格尺寸一样(因此可以直接用作图层内容,而不需要重绘)。

    我们来看看这些弊端有没有造成不同:清单14.4显示了使用CATiledLayer对图片传送器的重新实现。

清单14.4 使用CATiledLayer的图片传送器

#import "ViewController.h"
#import

@interface ViewController()

@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
//set up data
self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"jpg" inDirectory:@"Vacation Photos"];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagePaths count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
//add the tiled layer
CATiledLayer *tileLayer = [cell.contentView.layer.sublayers lastObject];
if (!tileLayer) {
tileLayer = [CATiledLayer layer];
tileLayer.frame = cell.bounds;
tileLayer.contentsScale = [UIScreen mainScreen].scale;
tileLayer.tileSize = CGSizeMake(cell.bounds.size.width * [UIScreen mainScreen].scale, cell.bounds.size.height * [UIScreen mainScreen].scale);
tileLayer.delegate = self;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[cell.contentView.layer addSublayer:tileLayer];
}
//tag the layer with the correct index and reload
tileLayer.contents = nil;
[tileLayer setValue:@(indexPath.row) forKey:@"index"];
[tileLayer setNeedsDisplay];
return cell;
}

- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
//get image index
NSInteger index = [[layer valueForKey:@"index"] integerValue];
//load tile image
NSString *imagePath = self.imagePaths[index];
UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
//calculate image rect
CGFloat aspectRatio = tileImage.size.height / tileImage.size.width;
CGRect imageRect = CGRectZero;
imageRect.size.width = layer.bounds.size.width;
imageRect.size.height = layer.bounds.size.height * aspectRatio;
imageRect.origin.y = (layer.bounds.size.height - imageRect.size.height)/2;
//draw tile
UIGraphicsPushContext(ctx);
[tileImage drawInRect:imageRect];
UIGraphicsPopContext();
}

@end

    需要解释几点:

  • CATiledLayertileSize属性单位是像素,而不是点,所以为了保证瓦片和表格尺寸一致,需要乘以屏幕比例因子。

  • -drawLayer:inContext:方法中,我们需要知道图层属于哪一个indexPath以加载正确的图片。这里我们利用了CALayer的KVC来存储和检索任意的值,将图层和索引打标签。

    结果CATiledLayer工作的很好,性能问题解决了,而且和用GCD实现的代码量差不多。仅有一个问题在于图片加载到屏幕上后有一个明显的淡入(图14.4)。

图14.4

图14.4 加载图片之后的淡入

    我们可以调整CATiledLayerfadeDuration属性来调整淡入的速度,或者直接将整个渐变移除,但是这并没有根本性地去除问题:在图片加载到准备绘制的时候总会有一个延迟,这将会导致滑动时候新图片的跳入。这并不是CATiledLayer的问题,使用GCD的版本也有这个问题。

    即使使用上述我们讨论的所有加载图片和缓存的技术,有时候仍然会发现实时加载大图还是有问题。就和13章中提到的那样,iPad上一整个视网膜屏图片分辨率达到了2048x1536,而且会消耗12MB的RAM(未压缩)。第三代iPad的硬件并不能支持1/60秒的帧率加载,解压和显示这种图片。即使用后台线程加载来避免动画卡顿,仍然解决不了问题。

    我们可以在加载的同时显示一个占位图片,但这并没有根本解决问题,我们可以做到更好。

分辨率交换

    视网膜分辨率(根据苹果市场定义)代表了人的肉眼在正常视角距离能够分辨的最小像素尺寸。但是这只能应用于静态像素。当观察一个移动图片时,你的眼睛就会对细节不敏感,于是一个低分辨率的图片和视网膜质量的图片没什么区别了。

    如果需要快速加载和显示移动大图,简单的办法就是欺骗人眼,在移动传送器的时候显示一个小图(或者低分辨率),然后当停止的时候再换成大图。这意味着我们需要对每张图片存储两份不同分辨率的副本,但是幸运的是,由于需要同时支持Retina和非Retina设备,本来这就是普遍要做到的。

    如果从远程源或者用户的相册加载没有可用的低分辨率版本图片,那就可以动态将大图绘制到较小的CGContext,然后存储到某处以备复用。

    为了做到图片交换,我们需要利用UIScrollView的一些实现UIScrollViewDelegate协议的委托方法(和其他类似于UITableViewUICollectionView基于滚动视图的控件一样):

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

    你可以使用这几个方法来检测传送器是否停止滚动,然后加载高分辨率的图片。只要高分辨率图片和低分辨率图片尺寸颜色保持一致,你会很难察觉到替换的过程(确保在同一台机器使用相同的图像程序或者脚本生成这些图片)。

收起阅读 »

iOS - 高效绘图四

异步绘制    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。  ...
继续阅读 »

异步绘制

    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。

    针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选择:CATiledLayerdrawsAsynchronously属性。

CATiledLayer

    我们在第六章简单探索了一下CATiledLayer。除了将图层再次分割成独立更新的小块(类似于脏矩形自动更新的概念),CATiledLayer还有一个有趣的特性:在多个线程中为每个小块同时调用-drawLayer:inContext:方法。这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的CATiledLayer是实现异步更新图片视图的简单方法。

drawsAsynchronously

    iOS 6中,苹果为CALayer引入了这个令人好奇的属性,drawsAsynchronously属性对传入-drawLayer:inContext:的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。

    它与CATiledLayer使用的异步绘制并不相同。它自己的-drawLayer:inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。

    根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如UITableViewCell之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。

总结

本章我们主要围绕用Core Graphics软件绘制讨论了一些性能挑战,然后探索了一些改进方法:比如提高绘制性能或者减少需要绘制的数量。

第14章,『图像IO』,我们将讨论图片的载入性能。

收起阅读 »

iOS - 高效绘图三

脏矩形    有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像...
继续阅读 »

脏矩形

    有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用CAShapeLayer没办法实现。

    我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。

    我们的『黑板』应用的最初实现见清单13.3,我们更改了之前版本的DrawingView,用一个画刷位置的数组代替UIBezierPath。图13.2是运行结果

清单13.3 简单的类似黑板的应用

#import "DrawingView.h"
#import
#define BRUSH_SIZE 32

@interface DrawingView ()

@property (nonatomic, strong) NSMutableArray *strokes;

@end

@implementation DrawingView

- (void)awakeFromNib
{
//create array
self.strokes = [NSMutableArray array];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//needs redraw
[self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);

//draw brush stroke 
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
@end

图13.2

图13.2 用程序绘制一个简单的『素描』

    这个实现在模拟器上表现还不错,但是在真实设备上就没那么好了。问题在于每次手指移动的时候我们就会重绘之前的线刷,即使场景的大部分并没有改变。我们绘制地越多,就会越慢。随着时间的增加每次重绘需要更多的时间,帧数也会下降(见图13.3),如何提高性能呢?

图13.3

图13.3 帧率和线条质量会随时间下降。

    为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。

    当一个视图被改动过了,TA可能需要重绘。但是很多情况下,只是这个视图的一部分被改变了,所以重绘整个寄宿图就太浪费了。但是Core Animation通常并不了解你的自定义绘图代码,它也不能自己计算出脏区域的位置。然而,你的确可以提供这些信息。

    当你检测到指定视图或图层的指定部分需要被重绘,你直接调用-setNeedsDisplayInRect:来标记它,然后将影响到的矩形作为参数传入。这样就会在一次视图刷新时调用视图的-drawRect:(或图层代理的-drawLayer:inContext:方法)。

    传入-drawLayer:inContext:CGContext参数会自动被裁切以适应对应的矩形。为了确定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法来从上下文获得大小。调用-drawRect()会更简单,因为CGRect会作为参数直接传入。

    你应该将你的绘制工作限制在这个矩形中。任何在此区域之外的绘制都将被自动无视,但是这样CPU花在计算和抛弃上的时间就浪费了,实在是太不值得了。

    相比依赖于Core Graphics为你重绘,裁剪出自己的绘制区域可能会让你避免不必要的操作。那就是说,如果你的裁剪逻辑相当复杂,那还是让Core Graphics来代劳吧,记住:当你能高效完成的时候才这样做。

    清单13.4 展示了一个-addBrushStrokeAtPoint:方法的升级版,它只重绘当前线刷的附近区域。另外也会刷新之前线刷的附近区域,我们也可以用CGRectIntersectsRect()来避免重绘任何旧的线刷以不至于覆盖已更新过的区域。这样做会显著地提高绘制效率(见图13.4)

    清单13.4 用-setNeedsDisplayInRect:来减少不必要的绘制

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//set dirty rect
[self setNeedsDisplayInRect:[self brushRectForPoint:point]];
}

- (CGRect)brushRectForPoint:(CGPoint)point
{
return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = [self brushRectForPoint:point];

//only draw brush stroke if it intersects dirty rect
if (CGRectIntersectsRect(rect, brushRect)) {
//draw brush stroke
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
}

图13.4

图13.4 更好的帧率和顺滑线条

收起阅读 »

Android 中java多线程编程及注意事项

开启线程方式://方式1 public class MyThread extends Thread{ @Override public void run() { super.run(); //do my work...
继续阅读 »

开启线程方式:

//方式1
public class MyThread extends Thread{
@Override
public void run() {
super.run();
//do my work
}
}
new MyThread().start();

//方式2:
public class MyRunnable implements Runnable{
@Override
public void run() {
//do my work
}
}
new Thread(new MyRunnable()).start();

//方式3
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "result";
}
}
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();

当然还有一些 HandlerThread、ThreadPool、AsyncTask 等也可以开启新线程,都是基于Thread的封装


线程状态:

image-20210829004506607.png

等待和阻塞的区别:简单理解,等待是线程判断条件不满足,主动调用指令进入的一种状态;而阻塞是被动进入,而且只有synchronized关键字修饰时可能触发。

还一种说法:将IO、sleep、synchronized等进入的线程block状态称为阻塞,他们的共同点是让出cpu,但不释放已经拿到的锁,参考:blog.csdn.net/weixin_3104…


线程安全

不共享数据:ThreadLocal

        ThreadLocal<String> threadLocal = new ThreadLocal<>();

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("线程1数据");
//do some work ...

String data = threadLocal.get();
}
});

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("线程2数据");
//do some work ...

String data = threadLocal.get();
}
});

原理:

没个线程里都有一个threadlocalmap数组, 调用threadlocalset方法时,以threadlocal变量hash值做key,将对于value存入数组中。 image-20210829011508921.png

共享数据:

1.jvm关键字 synchronized, 注:非公平、可重入

    private List<Integer> shareData = new ArrayList<>();

public void trySync(int data) {
synchronized (this) {//锁TestSyncData对象, thread1, thread2 ...
//操作共享数据
shareData.add(data);
}
//do something later ...
}

注意坑点:锁范围,存活周期不一样

    public void trySync1() {
synchronized (this) {//锁TestSyncData对象
}
}
//锁TestSyncData对象
public synchronized void trySync2() {
}

//锁TestSyncData类
public synchronized static void trySync3() {
}

//锁TestSyncData类
public void trySync4() {
synchronized (TestSyncData.class){
}
}

有耗时任务获取锁,后续收尾处理一定要考虑锁释放耗时问题;比如单例对象在主线程释放时,一定要注意锁能否及时拿到。

如果不能确定,考虑将锁降级存活时长,比如用栈内锁,线程安全型bean

synchronized原理详见:blog.csdn.net/weixin_3960…

2.wait-notify 函数,java祖先类Object的成员函数

    public void testWait() {
//1.创建同步对象
Object lock = new Object();

new Thread(new Runnable() {
@Override
public void run() {
//2. do something hard ...
shareData = new ShareData();

//3. 唤醒原线程
synchronized (lock){
lock.notify();
}
//5.some other logic
}
}).start();

//4.原线程等待
synchronized (lock){
try {
lock.wait(45000);
} catch (InterruptedException e) {
//6.线程中断触发
e.printStackTrace();
}
}

if (shareData != null) {
//do something happy ...
}
}

坑点多多:

  1. notify不能定向唤醒,只能随机唤醒一个wait的线程(保证notify的数量>=wait数量),使用的时候一定要保证没有多个线程处于wait状态;如果想定向唤醒,考虑使用 ReentrantLock的Condition

  2. 标记5的地方最好不要做其他逻辑,可能不会执行到,尽量保证notify是耗时任务里的最后逻辑

  3. 标记6的地方注意wait会被线程中断,而跳出同步逻辑,如果需要可以使用抗扰动写法:

            while (shareData == null) {//4 抗线程扰动写法
    synchronized (lock) {
    try {
    lock.wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
  4. 业务销毁的时候,如果不需要等数据返回,可以直接notifyAll,提前结束线程任务,释放对象

3.基于aqs(队列同步器)接口的一系列同步锁或组件 ReentrantLock、Semaphore、CountDownLantch;通过队列+计数+CAS

    private ReentrantLock lock = new ReentrantLock();
private List<Integer> shareData = new ArrayList<>();

public void testLock() {//非标准
lock.lock();
shareData.add(1);
lock.unlock();
}

坑点:注意手动释放,以免死锁; 同步逻辑会有exception,标准最好用try-catch-finally


线程安全的数据类型:StringBuffer、CopyOnWriteArrayList、concurrentxxx、BlockingQueue、Atomicxxx(CAS)

有些基于锁,有些基于无锁化的CAS(compare and swap),有些两者混合

cas原理参考:cloud.tencent.com/developer/a…

image-20210829040630653.png

坑点:

  1. CopyOnWriteArrayList:先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加删除元素,添加删除完元素之后,再将原容器的引用指向新的容器,整个过程加锁,保证了写的线程安全。然而我们还是出现了多线程的数组越界异常
        //不安全写法
for (int i = 0; i < copyOnWriteArrayList.size(); i++) {
copyOnWriteArrayList.get(i);
}

//安全写法
Iterator iterator = copyOnWriteArrayList.iterator();
while (iterator.hasNext()){
iterator.next();
}
  1. Atomicxxx 注意使用场景

    ABA问题   AtomicStampedReference,AtomicMarkableReference
    开销问题
    只能保证一个共享变量的原子操作 AtomicReference

阻塞队列(BlockingQueue): 生产者与消费者模式里,生产者与消费者解耦,生产者与消费者性能均衡问题

BlockingQueue
ArrayBlockingQueue
数组结构的有界阻塞队列
LinkedBlockingQueue
链表结构的有界阻塞队列
PriorityBlockingQueue
优先级排序无界阻塞队列
DelayQueue
优先级队列无界阻塞队列
应用--过期缓存
SynchronousQueue
不存储元素的阻塞队列
应用--newCachedThreadPool
LinkedTransferQueue
链表结构无界阻塞队列
LinkedBlockingDeque
链表结构双向阻塞队列

有界:定义最大容量 无界:不定义


线程池:线程管理与统一调度

创建:

ThreadPoolExecutor(int corePoolSize,    //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //空闲线程存活时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程池工厂
RejectedExecutionHandler handler) //拒绝策略

线程比较稀缺,需合理配置,根据任务特性

cpu密集型:内存中取数据计算 (最大线程数 <=Runtime.getRuntime().availableProcessors()+1) 注:+1 虚拟内存-页缺失

IO密集型:网络通信、读写磁盘 (最大线程数 <= cpu核心*2), 高阻塞低占用

混合型:以上两者,如果两者执行时间相当,拆分线程池;否则视权重大的分配。具体时长可以本地测试或者根据下面的经验表预估后选择 image-20210828234846390-0165731.png

执行:注意任务存放位置顺序 1、2、3、4(重点)

image-20210828225601263.png

拒绝策略(饱和策略):

RejectedExecutionHandler
AbortPolicy
直接抛出异常-默认
CallerRunsPolicy
调用者线程执行
DiscardOldestPolicy
丢弃旧的
DiscardPolicy
直接丢弃

关闭:

awaitTermination(long timeout, TimeUnit unit)//阻塞
shutDown()//中断没有执行任务的线程
shutdownNow()//中断所有线程,协作式处理,只是发出中断信号
isShutdown()//是否关闭
收起阅读 »

自定义View

判断自己有没有掌握这个知识点,就模拟面试,看看你能不能给对方讲清楚1. 坐标系在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。2...
继续阅读 »

判断自己有没有掌握这个知识点,就模拟面试,看看你能不能给对方讲清楚

1. 坐标系

在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:

image.png

除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。

image.png

2. 自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:

  1. 自定义一个View
  2. 编写values/attrs.xml,在其中编写styleable和item等标签元素
  3. 在布局文件中View使用自定义的属性(注意namespace)
  4. 在View的构造方法中通过TypedArray获取

自定义View属性很重要,但是并不复杂,需要的话再查一下就好了

3. View绘制流程

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

3.1 MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpecmodesize

MeasureSpec当中一共存在三种modeUNSPECIFIEDEXACTLY
AT_MOST

对于View来说,MeasureSpec的mode和Size有如下意义

模式意义对应
EXACTLY精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Sizematch_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,View对尺寸没有任何限制,View设置为多大就应当为多大一般系统内部使用

3.2 Layout()

layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。

3.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在Viewdraw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

  1. 如果需要,绘制背景。
  2. 有过有必要,保存当前canvas。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果有必要,绘制边缘、阴影等效果。
  6. 绘制装饰,如滚动条等等。

使用下方的流程图表示:

image.png

布局过程的自定义:

方式: 重写布局过程的相关方法\

1. 测量过程: onMeasure()
2. 布局过程: onLayout()
复制代码

具体:

1. 重写onMeasure()来修改已有的View的尺寸
2. 重写onMeasure()来全新计算自定义View的尺寸
3. 重写onMeasure()和onLayout()来全新计算自定义 ViewGroup 的内部布局
复制代码
public class SquareImageView extends AppCompatImageView {
private static final String TAG = "SquareImageView";

public SquareImageView(Context context) {
super(context);
}

public SquareImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}



@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先执行原测量算法
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 获取原先的测量结果
int measureWidth = getMeasuredWidth();
int measureHeight = getMeasuredHeight();
Log.d(TAG, "onMeasure11" +
", measureWidth = " + measureWidth +
", measureHeight = " + measureHeight +
"");

// 利用原先的测量结果计算出新尺寸
if (measureWidth > measureHeight) {
measureWidth = measureHeight;
} else {
measureHeight = measureWidth;
}
Log.d(TAG, "onMeasure22" +
", measureWidth = " + measureWidth +
", measureHeight = " + measureHeight +
"");
// 保存计算后的结果
setMeasuredDimension(measureWidth, measureHeight);
}
}
复制代码

重写onMeasure() 修改尺寸

1. 重写 onMeasure() 修改尺寸,并调用super.onMeasure触发原先的测量
2. 用getMeasuredWidth() 和 getMeasuredHeight() 取到之前测得的尺寸,利用这两个尺寸来计算出最终尺寸。
3. 使用 setMeasuredDimension() 保存尺寸
复制代码

收起阅读 »

你真的了解Handler吗?

Handler,一个面试中常问的高频词汇。大家想想这个知识点一般是怎么考察的?请解释一下Handler的原理?不不不,这个问题已经烂大街了,我要是面试官,我会这么问。我们知道在Handler中,存在一个方法叫 sendMessageDelay , 作用是延时发...
继续阅读 »

Handler,一个面试中常问的高频词汇。

大家想想这个知识点一般是怎么考察的?请解释一下Handler的原理?

不不不,这个问题已经烂大街了,我要是面试官,我会这么问。

我们知道在Handler中,存在一个方法叫 sendMessageDelay , 作用是延时发送消息,请解释一下Handler是如何实现延时发送消息的?

Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?

也请各位读者先自己思考一下这两个问题,换做是你该怎么回答。

Handler

我们先从Handler的定义来认识它,先上谷歌原文:

/**
* A Handler allows you to send and process {@link Message} and Runnable
*/

下面由我这枚英语渣上线,强行翻译一波。

  1. Handler是用来结合线程的消息队列来发送、处理Message对象Runnable对象的工具。每一个Handler实例化之后会关联一个线程和该线程的消息队列。当你创建一个Handler的时候,它就会自动绑定到到所在的线程或线程的消息队列,并陆续把Message/Runnable分发到消息队列,然后在它们出队的时候去执行。

  2. Handler主要有两个用途: (1) 调度在将来某个时候执行的MessageRunnable。(2)把需要在另一个线程执行的操作加入到消息队列中去。

  3. post runnablesend messagehandler时,您可以在消息队列准备就绪后立即处理该事务。也可以延迟一段时间执行,或者指定某个特定时间去执行。


我们先从Handler的构造方法来认识一下它:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) 

Handler的构造方法有很多个,但最终调用的就是上述构造方法。

老规矩,先上官方解释,再上学渣翻译。

* Use the provided {@link Looper} instead of the default one and take a callback
* interface in which to handle messages. Also set whether the handler
* should be asynchronous.
*
* Handlers are synchronous by default unless this constructor is used to make
* one that is strictly asynchronous.
*
* Asynchronous messages represent interrupts or events that do not require global ordering
* with respect to synchronous messages. Asynchronous messages are not subject to
* the synchronization barriers introduced by conditions such as display vsync.
  1. 使用提供的Looper而不是默认的Looper,并使用回调接口来处理消息。还设置处理程序是否应该是异步的。

  2. 默认情况下,Handler是同步的,除非此构造函数用于生成严格异步的Handler

  3. 异步消息指的是不需要进行全局排序的中断或事件。异步消息不受同步障碍(比如display vsync)的影响。


Handler中的方法主要分为以下两类:

  1. 获取及查询消息,比如 obtainMessage(int what),hasMessages(int what)

  2. 将message或runnable添加/移出消息队列,比如 postAtTime(@NonNull Runnable r, long uptimeMillis),sendEmptyMessageDelayed(int what, long delayMillis)

在这些方法中,我们重点需要关注一下enqueueMessage这个方法。

为什么呢?

无论是 postAtTimesendMessageDelayed还是其他的post、send方法,它们最终都会调到enqueueMessage这个方法里去。

比如:

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

可以看到,sendMessageDelayed方法里将延迟时间转换为消息触发的绝对时间,最终调用的是sendMessageAtTime方法。

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

而sendMessageAtTime方法调用了enqueueMessage方法。

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();

if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

enqueueMessage方法直接将message交给了MessageQueue去执行。

Message

在分析MessageQueue之前,我们应该先来认识一下Message这个消息载体类。

老规矩,先从定义看起:

* Defines a message containing a description and arbitrary data object that can be
* sent to a {@link Handler}. This object contains two extra int fields and an
* extra object field that allow you to not do allocations in many cases.
*
* <p>While the constructor of Message is public, the best way to get
* one of these is to call {@link #obtain Message.obtain()} or one of the
* {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
* them from a pool of recycled objects.</p>

下面是渣翻译:

  1. 定义一条包含描述和任意数据对象的消息,该对象可以发送到Handler。此对象包含两个额外的int字段和一个额外的object字段。

  2. 尽管Message的构造方法是public,但获取一个Message的最好的方法是调用Message.obtain或者Handler.obtainMessage方法,这些方法会从可回收的线程池中获取Message对象。


我们来认识一下Message里的字段:

public final class Message implements Parcelable {

}

在Message中,我们需要关注一下Message的回收机制。

先来看下recyclerUnchecked方法:

void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;

synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}

在这个方法中,有三个关键变量。

  1. sPoolSync :主要是给Message加一个对象锁,不允许多个线程同时访问Message类和recycleUnchecked方法。
  2. sPool:存储我们循环利用Message的单链表。这里sPool只是链表的头节点。
  3. sPoolSize:单链表的链表的长度,即存储的Message对象的个数。

当我们调用recycleUnchecked方法时,首先会将当前Message对象的属性清空。然后判断Message是否已到达缓存的上限(50个),如果没有,将当前的Message对象置于链表的头部。

那么取缓存的操作呢?

我们来看下obtain方法:

public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}

可以看出,Message会尝试取出sPool链表的第一个元素,并将sPool的头元素往后移动一位。如果sPool链表为空,将会返回一个新的Message对象。

Message里提供obtain方法获取Message对象,使得Message到了重复的利用,减少了每次获取Message时去申请空间的时间。同时,这样也不会永无止境的去创建新对象,减小了Jvm垃圾回收的压力,提高了效率。

MessageQueue

MessageQueue用于保存由Looper发送的消息的列表。消息不会直接添加到消息队列,而是通过Handler对象中关联的Looper里的MessageQueue完成添加的动作。

您可以使用Looper.myQueue()检索当前线程的MessageQueue。

我们先来看看MessageQueue如何实现添加一个Message的操作。

boolean enqueueMessage(Message msg, long when) {
//判断msg是否有target属性以及是否正在使用中
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}


// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
//唤醒消息
nativeWake(mPtr);
}
}
return true;
}

mMessages是一个按照消息实际触发时间msg.when排序的链表,越往后的越晚触发。enqueueMessage方法根据新插入消息的when,将msg插入到链表中合适的位置。如果是及时消息,还需要唤醒MessageQueue

我们接着来看看nativeWake方法,nativeWake方法的源码位于\frameworks\base\core\jni\android_os_MessageQueue.cpp

static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}

继续看NativeMessageQueue里的wake函数。

void NativeMessageQueue::wake() {
mLooper->wake();
}

它又转交给了Looper(源码位置/system/core/libutils/Looper.cpp)去处理。

void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ wake", this);
#endif

uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",
mWakeEventFd.get(), nWrite, strerror(errno));
}
}
}

Looper里的wake函数很简单,它只是向mWakeEventFd里写入了一个 1 值。

上述的mWakeEventFd又是什么呢?

Looper::Looper(bool allowNonCallbacks)
: mAllowNonCallbacks(allowNonCallbacks),
mSendingMessage(false),
mPolling(false),
mEpollRebuildRequired(false),
mNextRequestSeq(0),
mResponseIndex(0),
mNextMessageUptime(LLONG_MAX) {

mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));

...
}

从Looper的构造函数里可以找到答案,mWakeEventFd本质上是一个eventfd。至于什么是eventfd,这里只能说是eventfd是Linux 2.6提供的一种系统调用,它可以用来实现事件通知,更具体的内容需要各位读者自行查阅资料了。

既然有发送端,那么必然有接收端。接收端在哪呢?

void Looper::awoken() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ awoken", this);
#endif

uint64_t counter;
TEMP_FAILURE_RETRY(read(mWakeEventFd.get(), &counter, sizeof(uint64_t)));
}

可以看到,awoken函数里的内容很简单,只是做了一个读取的动作,它并不关系读到的具体值是啥。为什么要这样设计呢,我们得结合awoken函数在哪里调用去分析。

awoken函数在LooperpollInner函数里调用。pollInner函数里有一条语句

int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

它在这里起到阻塞的作用,如果没有调用nativeWake函数,epoll_wait将一直等待写入事件,直到超时为止。

如此,便回到我们文章一开始提出的问题了。

Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?

首先,我们需要明确一点,Handler中到底有没有阻塞?

答案是有!!!那它为什么不会导致ANR呢?

这得从ANR产生的原理说起。

ANR的本质也是一个Message,这一点很关键。我们拿前台服务的创建来举例,前台服务创建时,会发送一个 what值为ActivityManagerService.SERVICE_TIMEOUT_MSG的延时20s的Message,如果Service的创建 工作在上述消息的延时时间内完成,则会移除该消息,否则,在Handler正常收到这个消息后,就会进行服务超时处理,即弹出ANR对话框。

为什么不会ANR,现在各位读者清楚了吗?ANR消息本身就是通过Handler去派发的,Handler阻塞与否与ANR并没有必然关系。


我们看了MessageQueue是如何加入一条消息的,接下来,我们来看看它是如何取出一条消息的。

Message next() {
//如果消息循环已退出并已被释放,则return
//如果应用程序在退出后尝试重新启动looper,则可能发生这种情况
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
//将当前线程中挂起的所有Binder命令刷新到内核驱动程序。
//在执行可能会阻塞很长时间的操作之前调用此函数非常有用,以确保已释放任何挂起的对象引用,
//从而防止进程保留对象的时间超过需要的时间。
Binder.flushPendingCommands();
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

next方法里主要做了三件事,(1)使用nativePollOnce阻塞指定时间,等待下一条消息的执行。 (2)获取下一条消息,并返回此消息。 (3)如果消息队列为空,则执行IdleHandler。

这里有个新名词IdleHandlerIdleHandler是可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。 MessageQueue中提供了addIdleHandlerremoveIdleHandler去添加删除IdleHandler


next方法的第一行有个ptr变量,这个ptr变量是什么含义呢?

MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}

mPtr是一个long型变量,它是在MessageQueue的构造方法中,通过nativeInit方法初始化的。

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}

nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
}

可以看到,ptr的本质是对 jni层的NativeMessageQueue对象的指针的引用。


我们重点来看下nativePollOnce方法,探寻一下Handler中的阻塞机制。nativePollOnce方法最终调用的是Looper.cpp中的pollOnce函数。

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) { //一个死循环
while (mResponseIndex < mResponses.size()) {
const Response& response = mResponses.itemAt(mResponseIndex++);
int ident = response.request.ident;
if (ident >= 0) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - returning signalled identifier %d: "
"fd=%d, events=0x%x, data=%p",
this, ident, fd, events, data);
#endif
if (outFd != nullptr) *outFd = fd;
if (outEvents != nullptr) *outEvents = events;
if (outData != nullptr) *outData = data;
return ident;
}
}

if (result != 0) {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - returning result %d", this, result);
#endif
if (outFd != nullptr) *outFd = 0;
if (outEvents != nullptr) *outEvents = 0;
if (outData != nullptr) *outData = nullptr;
return result;
}

result = pollInner(timeoutMillis);
}
}

函数里有个关于mResponses的while循环,我们从java层调用的暂时不用管它,它是ndk的handler处理逻辑。我们重点来看pollInner函数。

int Looper::pollInner(int timeoutMillis) {
// 根据下一条消息的到期时间调整超时。
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
if (messageTimeoutMillis >= 0
&& (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
timeoutMillis = messageTimeoutMillis;
}
}

// 默认触发唤醒事件,POLL_WAKE == -1
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;

// We are about to idle.
mPolling = true;

struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//等待写入事件,写入事件由awoken函数触发。timeoutMillis为超时时间,0立即返回,-1一直等待
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

// No longer idling.
mPolling = false;

// Acquire lock.
mLock.lock();

...

// Check for poll error.
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
ALOGW("Poll failed with an unexpected error: %s", strerror(errno));
//POLL_ERROR == -4
result = POLL_ERROR;
goto Done;
}

// Check for poll timeout.
if (eventCount == 0) {
//POLL_TIMEOUT == -3,epoll超时会走此分支
result = POLL_TIMEOUT;
goto Done;
}

// Handle all events.
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd.get()) {
if (epollEvents & EPOLLIN) {
//将eventfd里的数值取出,无实际含义,只是为了清空epoll事件和eventfd里的数据
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
//不会走到此分支,忽略它
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
"no longer registered.", epollEvents, fd);
}
}
}
Done: ;

// 中间省略的代码不做探究,和ndk的handler实现有关
...
return result;
}

可以看到,pollInner函数主要的逻辑是使用epoll_wait去读取唤醒事件,它有一个最大的等待时长,其最大等待时长和下一条消息的触发时间有关。

需要注意一下pollInner的返回值result,它有三种状态。进入方法默认为POLL_WAKE,表示触发唤醒事件。 接下来通过对epoll_wait返回值的判断,它可能会变更为另两种状态。epoll_wait返回值为0,表示epoll_wait因超时而结束等待,result值设为POLL_TIMEOUT;epoll_wait返回值为-1,表示epoll_wait因系统中断等原因而结束等待,result值设为POLL_ERROR。但不管result值设为哪一个,都会导致pollOnce退出死循环,然代码流程回到java层的next方法中,去取得下一个Message对象。

因此,nativePollOnce简单意义上的理解,它就是一个阻断器,可以将当前线程阻塞,直到超时或者因需立即执行的新消息入队才结束阻塞。

各位读者,看到这里,大家再回过头去想想文章的第一个问题该怎么回答吧。

Looper

Handler 机制中,我们还剩最后一个一个模块没有分析———— Looper。我们先从官方定义来看起:

* Class used to run a message loop for a thread.  Threads by default do
* not have a message loop associated with them; to create one, call
* {@link #prepare} in the thread that is to run the loop, and then
* {@link #loop} to have it process messages until the loop is stopped.

概括一下:

Looper是一个用于在线程中循环遍历消息的类。默认情况下,线程没有与之关联的消息循环;如果要创建一个,请在运行Looper的线程中调用Looper.prepare(),然后使用Looper.loop()让它处理消息直到循环停止。

上面的定义提到了两个比较关键的方法,我们一个一个来看。

Looper.prepare()

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

prepare的方法内容非常简单,创建一个Looper对象,并把它放到sThreadLocal里,其中sThreadLocal是一个ThreadLocal类。

ThreadLocal类又是什么呢?

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量,这样就不会存在线程不安全问题。

因此,使用ThreadLocal能够保证不同线程的Looper对象都有一个独立的副本,它们彼此独立,互不干扰。


Looper.looper()

public static void loop() {
//获取当前线程的Looper对象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//获取与Looper关联的messagequeue
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);

boolean slowDeliveryDetected = false;

for (;;) {
//进入死循环,不断去从MessageQueue中去拉取Message
Message msg = queue.next(); // next方法我们已经在MessageQueue中做了分析
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}


// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;

...

final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
//注意这里,msg.target是一个handler对象,这个方法最终调用了handler的dispatchMessage
//去做消息分发
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}

//回收Message,上文中有做过分析
msg.recycleUnchecked();
}
}

loop方法主要的工作是:建立一个死循环,不断的通过调用MessageQueue中的next方法获取下一个消息,并最终通过取得的消息关联的handler去完成消息的分发。

总结

最后,我们再来理一理 HandlerMessageMessageQueueLooper四者的关系和职责。

  • Handler : 消息分发的管理者。负责获取消息、封装消息、派发消息以及处理消息。
  • Message :消息的载体类。
  • MessageQueue :消息的容器。负责按消息的触发时间对消息入队出队,以及在合适的时间唤醒或休眠消息队列。
  • Looper : 消息分发的执行者。负责从消息队列中拉去消息并交给handler去执行。

为了更好的理解它们的关系,拿现实生活中的场景来举个例子:

Handler是快递员,负责收快递,取快递,查快递以及退回快递。

Message是快递包裹,message的target属性就是收件地址,而延时消息就是收件人预约了派送时间,
希望在指定的时间上门派送。

MessageQueue是菜鸟驿站,要对快递进行整理并摆放在合适的位置。

Looper是一个24小时不休息的资本家,他总是不停的在看菜鸟驿站有没有需要派送的快递,一有快递就立马取
出然后压榨快递员去派送。

最后,我们用一张四者之间的流程图来结束整篇文章:

image.png

收起阅读 »

View的绘制流程 onDraw

performTravel的方法走完onMeasure和onLayout流程后会走到下面这段代码段。 if (mFirst) { if (sAlwaysAssignFocus || !isInTouchMode()) { ...
继续阅读 »


performTravel的方法走完onMeasure和onLayout流程后会走到下面这段代码段。

        if (mFirst) {
if (sAlwaysAssignFocus || !isInTouchMode()) {
if (mView != null) {
if (!mView.hasFocus()) {
mView.restoreDefaultFocus();
} else {
...
}
}
} else {

View focused = mView.findFocus();
if (focused instanceof ViewGroup
&& ((ViewGroup) focused).getDescendantFocusability()
== ViewGroup.FOCUS_AFTER_DESCENDANTS) {
focused.restoreDefaultFocus();
}
}
}

final boolean changedVisibility = (viewVisibilityChanged || mFirst) && isViewVisible;
final boolean hasWindowFocus = mAttachInfo.mHasWindowFocus && isViewVisible;
final boolean regainedFocus = hasWindowFocus && mLostWindowFocus;
if (regainedFocus) {
mLostWindowFocus = false;
} else if (!hasWindowFocus && mHadWindowFocus) {
mLostWindowFocus = true;
}

if (changedVisibility || regainedFocus) {

boolean isToast = (mWindowAttributes == null) ? false
: (mWindowAttributes.type == WindowManager.LayoutParams.TYPE_TOAST);
...
}

mFirst = false;
mWillDrawSoon = false;
mNewSurfaceNeeded = false;
mActivityRelaunched = false;
mViewVisibility = viewVisibility;
mHadWindowFocus = hasWindowFocus;

if (hasWindowFocus && !isInLocalFocusMode()) {
final boolean imTarget = WindowManager.LayoutParams
.mayUseInputMethod(mWindowAttributes.flags);
if (imTarget != mLastWasImTarget) {
mLastWasImTarget = imTarget;
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && imTarget) {
imm.onPreWindowFocus(mView, hasWindowFocus);
imm.onPostWindowFocus(mView, mView.findFocus(),
mWindowAttributes.softInputMode,
!mHasHadWindowFocus, mWindowAttributes.flags);
}
}
}

在进入onDraw的流程之前,会先处理焦点。这个过程中可以分为2大步骤:

  • 1.如果是第一次渲染,则说明之前的宽高都是都为0.在requestFocus方法中会有这个判断把整个焦点集中拦截下来:
    private boolean canTakeFocus() {
return ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
&& ((mViewFlags & FOCUSABLE) == FOCUSABLE)
&& ((mViewFlags & ENABLED_MASK) == ENABLED)
&& (sCanFocusZeroSized || !isLayoutValid() || hasSize());
}

而在每一次onMeasure之前,都会尝试集中一次焦点的遍历。其中requestFocusNoSearch方法中,如果没有测量过就会直接返回false。因为每一次更换焦点或者集中焦点都可能伴随着如背景drawable,statelistDrawable等切换。没有测量过就没有必要做这无用功。

因此此时为了弥补之前拒绝焦点的行为,会重新进行一次restoreDefaultFocus的行为进行requestFocus处理。

  • 2.如果存在窗体焦点,同时不是打开了FLAG_LOCAL_FOCUS_MODE标志(这是一种特殊情况,一般打上这个标志位只有在startingWindow的快照中才会有。

则会调用InputMethodManager的onPostWindowFocus方法启动带了android.view.InputMethod这个action的软键盘服务。

onDraw流程

        if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
reportNextDraw();
}

boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}

performDraw();
} else {
if (isViewVisible) {
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}

mIsInTraversal = false;
  • 1.判断到如果是第一次调用draw方法,则会调用reportNextDraw。
    private void reportNextDraw() {
if (mReportNextDraw == false) {
drawPending();
}
mReportNextDraw = true;
}

void drawPending() {
mDrawsNeededToReport++;
}

能看到实际上就是设置mReportNextDraw为true。我们回顾一下前两个流程mReportNextDraw参与了标志位的判断。在执行onMeasure和onLayout有两个大前提,一个是mStop为false,一个是mReportNextDraw为true。只要满足其一就会执行。

这么做的目的只有一个,保证调用一次onDraw方法。为什么会这样呢?performDraw是整个Draw流程的入口。然而在这个入口,必须要保证cancelDraw为false以及newSurface为false。

注意,如果是第一次渲染因为会添加进新的Surface,此时newSurface为true。所以会走到下面的分之,如果串口可见则调用scheduleTraversals执行下一次Loop的绘制流程。否则判断是否有需要执行的LayoutTransitions layout动画就执行了。

因此第一次是不会走到onDraw,是从第二次Looper之后View的绘制流程才会执行onDraw。

ViewRootImpl performDraw

    private void performDraw() {
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
return;
}

final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
mFullRedrawNeeded = false;

mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");

boolean usingAsyncReport = false;
if (mReportNextDraw && mAttachInfo.mThreadedRenderer != null
&& mAttachInfo.mThreadedRenderer.isEnabled()) {
usingAsyncReport = true;
mAttachInfo.mThreadedRenderer.setFrameCompleteCallback((long frameNr) -> {
pendingDrawFinished();
});
}

try {
boolean canUseAsync = draw(fullRedrawNeeded);
if (usingAsyncReport && !canUseAsync) {
mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
usingAsyncReport = false;
}
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

if (mAttachInfo.mPendingAnimatingRenderNodes != null) {
final int count = mAttachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators();
}
mAttachInfo.mPendingAnimatingRenderNodes.clear();
}

if (mReportNextDraw) {
mReportNextDraw = false;

if (mWindowDrawCountDown != null) {
try {
mWindowDrawCountDown.await();
} catch (InterruptedException e) {
Log.e(mTag, "Window redraw count down interrupted!");
}
mWindowDrawCountDown = null;
}

if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.setStopped(mStopped);
}

if (mSurfaceHolder != null && mSurface.isValid()) {
SurfaceCallbackHelper sch = new SurfaceCallbackHelper(this::postDrawFinished);
SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();

sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
} else if (!usingAsyncReport) {
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.fence();
}
pendingDrawFinished();
}
}
}

我们把整个流程抽象出来实际上就是可以分为如下几个步骤:
对于软件渲染:

  • 1.调用draw方法,遍历View的层级。
  • 2.如果Surface是生效的,则在SurfaceHolder.Callback的surfaceRedrawNeededAsync回调中调用pendingDrawFinished。
  • 3.如果是强制同步渲染,则会直接调用pendingDrawFinished。

对于硬件渲染:

  • 1.调用draw方法,遍历View的层级。
  • 2.通过监听mThreadedRenderer的setFrameCompleteCallback回调执行pendingDrawFinished方法。

我们先关注软件渲染的流程。也就是draw和pendingDrawFinished。

ViewRootImpl draw

    private boolean draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return false;
}

if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
final int count = sFirstDrawHandlers.size();
for (int i = 0; i< count; i++) {
mHandler.post(sFirstDrawHandlers.get(i));
}
}
}

scrollToRectOrFocus(null, false);

if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}

boolean animating = mScroller != null && mScroller.computeScrollOffset();
final int curScrollY;
if (animating) {
curScrollY = mScroller.getCurrY();
} else {
curScrollY = mScrollY;
}
if (mCurScrollY != curScrollY) {
mCurScrollY = curScrollY;
fullRedrawNeeded = true;
if (mView instanceof RootViewSurfaceTaker) {
((RootViewSurfaceTaker) mView).onRootViewScrollYChanged(mCurScrollY);
}
}

final float appScale = mAttachInfo.mApplicationScale;
final boolean scalingRequired = mAttachInfo.mScalingRequired;

final Rect dirty = mDirty;
if (mSurfaceHolder != null) {
dirty.setEmpty();
if (animating && mScroller != null) {
mScroller.abortAnimation();
}
return false;
}

if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}


mAttachInfo.mTreeObserver.dispatchOnDraw();

int xOffset = -mCanvasOffsetX;
int yOffset = -mCanvasOffsetY + curScrollY;
final WindowManager.LayoutParams params = mWindowAttributes;
final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
if (surfaceInsets != null) {
xOffset -= surfaceInsets.left;
yOffset -= surfaceInsets.top;

dirty.offset(surfaceInsets.left, surfaceInsets.right);
}

...

mAttachInfo.mDrawingTime =
mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;

boolean useAsyncReport = false;
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested;
mInvalidateRootRequested = false;

mIsAnimating = false;

if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
mHardwareYOffset = yOffset;
mHardwareXOffset = xOffset;
invalidateRoot = true;
}

if (invalidateRoot) {
mAttachInfo.mThreadedRenderer.invalidateRoot();
}

dirty.setEmpty();

final boolean updated = updateContentDrawBounds();

if (mReportNextDraw) {
mAttachInfo.mThreadedRenderer.setStopped(false);
}

if (updated) {
requestDrawWindow();
}

useAsyncReport = true;

final FrameDrawingCallback callback = mNextRtFrameCallback;
mNextRtFrameCallback = null;
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
} else {

if (mAttachInfo.mThreadedRenderer != null &&
!mAttachInfo.mThreadedRenderer.isEnabled() &&
mAttachInfo.mThreadedRenderer.isRequested() &&
mSurface.isValid()) {

try {
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
} catch (OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
}

mFullRedrawNeeded = true;
scheduleTraversals();
return false;
}

if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}

if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return useAsyncReport;
}

大致上完成了如下流程:

  • 1.如果surface无效则直接返回
    1. sFirstDrawHandlers这个存储着runnable静态对象。实际上是在ActivityThread启动后调用attach方法通过addFirstDrawHandler添加进来的目的只是为了启动jit模式。
  • 3.scrollToRectOrFocus 处理滑动区域或者焦点区域。如果发生了滑动则回调TreeObserver.dispatchOnScrollChanged。接下来则通过全局的mScroller通过computeScrollOffset判断是否需要滑动动画。如果需要执行动画,则调用DeocView的onRootViewScrollYChanged,进行Y轴上的动画执行。
  • 4.通过ViewTreeObserver的dispatchOnDraw开始分发draw开始绘制的监听者。
  • 5.判断是否存在surface面上偏移量,有就矫正一次脏区,把偏移量添加上去。

接下来则会进入到硬件渲染和软件渲染的分支。但是进一步进行调用draw的流程有几个前提条件:脏区不为空,需要执行动画,辅助服务发生了焦点变化

  • 6.如果ThreadedRenderer不为空且可用。ThreadedRenderer通过onPreDraw回调到ViewRootImpl,更新mHardwareYOffset,mHardwareXOffset。如果这两个参数发生了变化,则说明整个发生了硬件绘制的区域变化,需要从头遍历一次所有的区域设置为无效区域,mThreadedRenderer.invalidateRoot。

最后调用ThreadedRenderer.draw方法执行硬件渲染绘制。并且设置通过registerRtFrameCallback设置进来的callback设置到ThreadedRenderer中。

  • 7.如果此时ThreadedRenderer不可用但是不为空,说明此时需要对ThreadedRenderer进行初始化,调用scheduleTraversals在下一轮的绘制流程中才进行硬件渲染。
  • 8.如果以上情况都不满足,说明是软件渲染,则调用drawSoftware进行软件渲染。
  • 9.如果不许要draw方法遍历全局的View树,则判断是否需要执行滑动动画,需要则调用scheduleTraversals进入下一轮的绘制。

本文先抛开硬件渲染,来看看软件渲染drawSoftware中做了什么。还有scrollToRectOrFocus滑动中做了什么?

ViewRootImpl scrollToRectOrFocus

    boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {
final Rect ci = mAttachInfo.mContentInsets;
final Rect vi = mAttachInfo.mVisibleInsets;
int scrollY = 0;
boolean handled = false;

if (vi.left > ci.left || vi.top > ci.top
|| vi.right > ci.right || vi.bottom > ci.bottom) {

final View focus = mView.findFocus();
if (focus == null) {
return false;
}
View lastScrolledFocus = (mLastScrolledFocus != null) ? mLastScrolledFocus.get() : null;
if (focus != lastScrolledFocus) {

rectangle = null;
}

if (focus == lastScrolledFocus && !mScrollMayChange && rectangle == null) {

} else {
mLastScrolledFocus = new WeakReference<View>(focus);
mScrollMayChange = false;
if (focus.getGlobalVisibleRect(mVisRect, null)) {
if (rectangle == null) {
focus.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focus, mTempRect);
}
} else {
mTempRect.set(rectangle);
}
if (mTempRect.intersect(mVisRect)) {
if (mTempRect.height() >
(mView.getHeight()-vi.top-vi.bottom)) {
}
else if (mTempRect.top < vi.top) {
scrollY = mTempRect.top - vi.top;
} else if (mTempRect.bottom > (mView.getHeight()-vi.bottom)) {
scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);
} else {
scrollY = 0;
}
handled = true;
}
}
}
}

if (scrollY != mScrollY) {
if (!immediate) {
if (mScroller == null) {
mScroller = new Scroller(mView.getContext());
}
mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY);
} else if (mScroller != null) {
mScroller.abortAnimation();
}
mScrollY = scrollY;
}

return handled;
}

能看到在这个过程中实际上就是处理两个区域mVisibleInsets可见区域以及mContentInsets内容区域。

实际上这个过程就是从根部节点开始寻找焦点,然后整个画面定格在焦点处。因为mVisibleInsets一般是屏幕中出去过扫描区的大小,但是内容区域就不一定了,可能内容会超出屏幕大小,因此会通过mScroller滑动定位。

计算原理如下,分为2个情况:

  • 1.可视区域的顶部比起获得了焦点的view的顶部要低,说明这个view在屏幕外了,需要向下滑动:

scrollY = mTempRect.top - vi.top;

  • 2.如果焦点view的底部比起可视区域要比可视区域的低,说明需要向上滑动,注意滑动之后需要展示view,因此滑动的距离要减去view的高度:

scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);
稍微变一下如下:
scrollY = mTempRect.bottom +vi.bottom - mView.getHeight();


收起阅读 »

如何用Rust做AndroidUI渲染

大力智能客户端团队 西豢沝尔 背景 Rust优秀的安全性、媲美C++的性能以及对跨平台编译和外部语言(ffi)的支持使得其成为高性能跨平台库的上佳实现语言。然而,Rust是否可以在逻辑层之上进一步服务于一些通用性的UI渲染?我们大力智能客户端团队针对开...
继续阅读 »

大力智能客户端团队 西豢沝尔



背景


Rust优秀的安全性、媲美C++的性能以及对跨平台编译和外部语言(ffi)的支持使得其成为高性能跨平台库的上佳实现语言。然而,Rust是否可以在逻辑层之上进一步服务于一些通用性的UI渲染?我们大力智能客户端团队针对开源项目rust-windowing( github.com/rust-window… )中几个核心工程进行剖析,并结合在Android系统的接入对此进行探索。


未命名.gif


Rust UI渲染:


Android系统上使用Rust渲染核心围绕ANativeWindow类展开,ANativeWindow位于android ndk中,是egl跨平台EGLNativeWindowType窗口类型在Android架构下的特定实现,因而基于ANativeWindow就可以创建一个EglSurface并通过GLES进行绘制和渲染。另一方面,ANativeWindow可以简单地与Java层的Surface相对应,因而将Android层需要绘制的目标转换为ANativeWindow是使用Rust渲染的关键,这一部分可以通过JNI完成。首先,我们先看一下rust-windowing对UI渲染的支持。


1 软件绘制:


在rust-windowing项目中,android-ndk-rs提供了rust与android ndk之间的胶水层,其中与UI渲染最相关的就是NativeWindow类,NativeWindow在Rust上下文实现了对ANativeWindow的封装,支持通过ffi对ANativeWindow进行操作,达到与在java层使用lockCanvas()和unlockCanvasAndPost()进行绘制相同的效果,基于这些api,我们可以实现在(A)NativeWindow上的指定区域绘制一个长方形:


unsafe fn draw_rect_on_window(nativewindow: &NativeWindow, colors: Vec<u8>, rect: ndk_glue::Rect) {
let height = nativewindow.height();
let width = nativewindow.width();
let color_format = get_color_format();
let format = color_format.0;
let bbp = color_format.1;
nativewindow.set_buffers_geometry(width, height, format);
nativewindow.acquire();
let mut buffer = NativeWindow::generate_epmty_buffer(width, height, width, format);
let locked = nativewindow.lock(&mut buffer, &mut NativeWindow::generate_empty_rect(0, 0, width, height));
if locked < 0 {
nativewindow.release();
return;
}

draw_rect_into_buffer(buffer.bits, colors, rect, width, height);
let result = nativewindow.unlock_and_post();
nativewindow.release();
}

unsafe fn draw_rect_into_buffer(bits: *mut ::std::os::raw::c_void, colors: Vec<u8>, rect: ndk_glue::Rect, window_width: i32, window_height: i32) {
let bbp = colors.len() as u32;
let window_width = window_width as u32;
for i in rect.top+1..=rect.bottom {
for j in rect.left+1..=rect.right {
let cur = (j + (i-1) * window_width - 1) * bbp;
for k in 0..bbp {
*(bits.offset((cur + (k as u32)) as isize) as *mut u8) = colors[k as usize];
}
}
}
}

这样就通过提交一个纯色像素填充的Buffer在指定的位置成功渲染出了一个长方形,不过这种方式本质上是软件绘制,性能欠佳,更好的方式是通过在Rust层封装GL在ANativeWindow上使能硬件绘制。


2 硬件绘制:


2.1 跨平台窗口系统:winit


2.1.1 Window:窗口

窗口系统最主要的目的是提供平台无关的Window抽象,提供一系列通用的基础方法、属性方法、游标相关方法、监控方法。winit以Window类抽象窗口类型并持有平台相关的window实现,通过WindowId唯一识别一个Window用于匹配后续产生的所有窗口事件WindowEvent,最后通过建造者模式对外暴露实例化的能力,支持在Rust侧设置一些平台无关的参数(大小、位置、标题、是否可见等)以及平台相关的特定参数,基本结构如下:


// src/window.rs
pub struct Window {
pub(crate) window: platform_impl::Window,
}

impl Window {
#[inline]
pub fn request_redraw(&self) {
self.window.request_redraw()
}

pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
self.window.inner_position()
}

pub fn current_monitor(&self) -> Option<MonitorHandle> {
self.window.current_monitor()
}
}

pub struct WindowId(pub(crate) platform_impl::WindowId);

pub struct WindowBuilder {
/// The attributes to use to create the window.
pub window: WindowAttributes,

// Platform-specific configuration.
pub(crate) platform_specific: platform_impl::PlatformSpecificWindowBuilderAttributes,
}

impl WindowBuilder {
#[inline]
pub fn build<T: 'static>(
self,
window_target: &EventLoopWindowTarget<T>,
) -> Result<Window, OsError> {
platform_impl::Window::new(&window_target.p, self.window, self.platform_specific).map(
|window| {
window.request_redraw();
Window { window }
},
)
}
}

在Android平台,winit暂时不支持使用给定的属性构建一个“Window”,大部分方法给出了空实现或者直接panic,仅保留了一些事件循环相关的能力,真正的窗口实现仍然从android-ndk-rs胶水层获得:当前的android-ndk-rs仅针对ANativeActivity进行了适配,通过属性宏代理了unsafe extern "C" fn ANativeActivity_onCreate(...)方法,在获得ANativeActivity指针*activity后,注入自定义的生命周期回调,在onNativeWindowCreated回调中获得ANativeWindow(封装为NativeWindow)作为当前上下文活跃的窗口。当然,android-ndk-rs的能力也支持我们在任意一个ANativeWindow上生成对应的上层窗口。


2.1.2 EventLoop:事件循环 - 上层

事件循环是整个窗口系统行为的驱动,统一响应抛出的系统任务和用户交互并将反馈渲染到窗口上形成闭环,当你需要合理地触发渲染时,最好的方式就是将指令发送给事件循环。winit中,将事件循环封装为EventLoop,使用ControlFlow控制EventLoop如何获取、消费循环中的事件,并对外提供一个EventLoopProxy代理用于作为与用户交互的媒介,支持用户通过proxy向EventLoop发送用户自定义的事件:



Android平台的事件循环建立在ALooper之上,通过android-ndk-rs提供的胶水层注入的回调处理生命周期行为和窗口行为,通过代理InputQueue处理用户手势,同时支持响应用户自定义事件和内部事件。一次典型的循环根据当前first_event的类型分发处理,一次处理一个主要事件;当first_event处理完成后,触发一次MainEventsCleared事件回调给业务方,并判断是否需要触发Resized和RedrawRequested,最后触发RedrawEventsCleared事件标识所有事件处理完毕。


单次循环处理完所有事件后进入控制流,决定下一次处理事件的行为,控制流支持Android epoll多路复用,在必要时唤醒循环处理后续事件,此外,控制流提供了强制执行、强制退出的能力。事实上,android-ndk-rs就是通过添加fd的方式将窗口行为抛到EventLoop中包装成Callback事件处理:



  • 首先,新建一对fdPIPE: [RawFd; 2],并把读端加到ALooper中,指定标识符为NDK_GLUE_LOOPER_EVENT_PIPE_IDENT;

  • 然后,在适当的时机调用向fd写端写入事件;

  • 最后,fd写入后触发ALooper在poll时被唤醒,且得到被唤醒fd的ident为NDK_GLUE_LOOPER_EVENT_PIPE_IDENT,便可以从fd读端读出此前wake()写入的事件并进行相应的处理;


// <--1--> 挂载fd
// ndk-glue/src/lib.rs
lazy_static! {
static ref PIPE: [RawFd; 2] = {
let mut pipe: [RawFd; 2] = Default::default();
unsafe { libc::pipe(pipe.as_mut_ptr()) };
pipe
};
}

{
...
thread::spawn(move || {
let looper = ThreadLooper::prepare();
let foreign = looper.into_foreign();
foreign
.add_fd(
PIPE[0],
NDK_GLUE_LOOPER_EVENT_PIPE_IDENT,
FdEvent::INPUT,
std::ptr::null_mut(),
)
.unwrap();
});
...
}

// <--2--> 向fd写入数据
// ndk-glue/src/lib.rs
unsafe fn wake(_activity: *mut ANativeActivity, event: Event) {
log::trace!("{:?}", event);
let size = std::mem::size_of::<Event>();
let res = libc::write(PIPE[1], &event as *const _ as *const _, size);
assert_eq!(res, size as _);
}

// <--3--> 唤醒事件循环读出事件
// src/platform_impl/android/mod.rs
fn poll(poll: Poll) -> Option<EventSource> {
match poll {
Poll::Event { ident, .. } => match ident {
ndk_glue::NDK_GLUE_LOOPER_EVENT_PIPE_IDENT => Some(EventSource::Callback),
...
},
...
}
}

// ndk-glue/src/lib.rs
pub fn poll_events() -> Option<Event> {
unsafe {
let size = std::mem::size_of::<Event>();
let mut event = Event::Start;
if libc::read(PIPE[0], &mut event as *mut _ as *mut _, size) == size as _ {
Some(event)
} else {
None
}
}
}

2.2 跨平台egl上下文:glutin


我们有了跨平台的OpenGL(ES)用于描述图形对象,也有了跨平台的窗口系统winit封装窗口行为,但是如何理解图形语言并将其渲染到各个平台的窗口上?这就是egl发挥的作用,它实现了OpenGL(ES)和底层窗口系统之间的接口层。在rust-windowing项目中,glutin工程承接了这个职责,以上下文的形式把窗口系统winit和gl关联了起来。



Context是gl的上下文环境,全局可以有多个gl上下文,但是一个线程同时只能有一个活跃的上下文,使用ContextCurrentState区分这一状态。glutin中Context可关联零个或多个Window,当Context与Window相关联时,使用ContextWrapper类,ContextWrapper使得可以方便地在上下文中同时操作gl绘制以及Window渲染。在其上衍生出两个类型:(1)RawContext:Context与Window虽然关联但是分开存储;(2)WindowedContext:同时存放相互关联的一组Context和Window。常见的场景下WindowedContext更加适用,通过ContextBuilder指定所需的gl属性和像素格式就可以构造一个WindowedContext,内部会初始化egl上下文,并基于持有的EglSurfaceType类型的window创建一个eglsurface作为后续gl指令绘制(draw)、回读(read)的作用目标(指定使用该surface上的缓冲)。


2.3 硬件绘制的例子:


基于winit和glutin提供的能力,使用Rust进行渲染的准备工作只需基于特定业务需求去创建一个glutin的Context,通过Context中创建的egl上下文可以调用gl api进行绘制,而window让我们可以掌控渲染流程,在需要的时候(比如基于EventLoop重绘指令或者一个简单的无限循环)下发绘制指令。简单地实现文章开头的三角形demo动画效果如下:


fn render(&mut self, gl: &Gl) {
let time_elapsed = self.startTime.elapsed().as_millis();
let percent = (time_elapsed % 5000) as f32 / 5000f32;
let angle = percent * 2f32 * std::f32::consts::PI;

unsafe {
let vs = gl.CreateShader(gl::VERTEX_SHADER);
gl.ShaderSource(vs, 1, [VS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
gl.CompileShader(vs);
let fs = gl.CreateShader(gl::FRAGMENT_SHADER);
gl.ShaderSource(fs, 1, [FS_SRC.as_ptr() as *const _].as_ptr(), std::ptr::null());
gl.CompileShader(fs);
let program = gl.CreateProgram();
gl.AttachShader(program, vs);
gl.AttachShader(program, fs);
gl.LinkProgram(program);
gl.UseProgram(program);
gl.DeleteShader(vs);
gl.DeleteShader(fs);
let mut vb = std::mem::zeroed();
gl.GenBuffers(1, &mut vb);
gl.BindBuffer(gl::ARRAY_BUFFER, vb);
let vertex = [
SIDE_LEN * (BASE_V_LEFT+angle).cos(), SIDE_LEN * (BASE_V_LEFT+angle).sin(), 0.0, 0.4, 0.0,
SIDE_LEN * (BASE_V_TOP+angle).cos(), SIDE_LEN * (BASE_V_TOP+angle).sin(), 0.0, 0.4, 0.0,
SIDE_LEN * (BASE_V_RIGHT+angle).cos(), SIDE_LEN * (BASE_V_RIGHT+angle).sin(), 0.0, 0.4, 0.0,
];

gl.BufferData(
gl::ARRAY_BUFFER,
(vertex.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr,
vertex.as_ptr() as *const _,
gl::STATIC_DRAW,
);

if gl.BindVertexArray.is_loaded() {
let mut vao = std::mem::zeroed();
gl.GenVertexArrays(1, &mut vao);
gl.BindVertexArray(vao);
}

let pos_attrib = gl.GetAttribLocation(program, b"position\0".as_ptr() as *const _);
let color_attrib = gl.GetAttribLocation(program, b"color\0".as_ptr() as *const _);
gl.VertexAttribPointer(
pos_attrib as gl::types::GLuint,
2,
gl::FLOAT,
0,
5 * std::mem::size_of::<f32>() as gl::types::GLsizei,
std::ptr::null(),
);

gl.VertexAttribPointer(
color_attrib as gl::types::GLuint,
3,
gl::FLOAT,
0,
5 * std::mem::size_of::<f32>() as gl::types::GLsizei,
(2 * std::mem::size_of::<f32>()) as *const () as *const _,
);

gl.EnableVertexAttribArray(pos_attrib as gl::types::GLuint);
gl.EnableVertexAttribArray(color_attrib as gl::types::GLuint);
gl.ClearColor(1.3 * (percent-0.5).abs(), 0., 1.3 * (0.5 - percent).abs(), 1.0);
gl.Clear(gl::COLOR_BUFFER_BIT);
gl.DrawArrays(gl::TRIANGLES, 0, 3);
}
}

3 Android - Rust JNI开发


以上Rust UI渲染部分完全运行在Rust上下文中(包括对c++的封装),而实际渲染场景下很难完全脱离Android层进行UI的渲染或不与Activity等容器进行交互。所幸Rust UI渲染主要基于(A)NativeWindow,而Android Surface在c++的对应类实现了ANativeWindow,ndk也提供了ANativeWindow_fromSurface方法从一个surface获得ANativeWindow对象,因而我们可以通过JNI的方式使用Rust在Android层的Surface上进行UI渲染:


// Android

surface_view.holder.addCallback(object : SurfaceHolder.Callback2 {

override fun surfaceCreated(p0: SurfaceHolder) {
RustUtils.drawColorTriangle(surface, Color.RED)
}

override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {}

override fun surfaceDestroyed(p0: SurfaceHolder) {}

override fun surfaceRedrawNeeded(p0: SurfaceHolder) {}

})

// Rust
pub unsafe extern fn Java_com_example_rust_1demo_RustUtils_drawColorTriangle__Landroid_view_Surface_2I(env: *mut JNIEnv, _: JClass, surface: jobject, color: jint) -> jboolean {
println!("call Java_com_example_rust_1demo_RustUtils_drawColor__Landroid_view_Surface_2I");
ndk_glue::set_native_window(NativeWindow::from_surface(env, surface));
runner::start();
0
}

需要注意,由于EventLoop是基于ALooper的封装,调用Rust实现渲染时需要确保调用在有Looper的线程(比如HandlerThread中),或者在Rust渲染前初始化时为当前线程准备ALooper。


总结



使用Rust在Android上进行UI渲染的可行性已经得证,但是它的性能表现究竟如何?未来又将在哪些业务上落地?这些仍待进一步探索。

收起阅读 »

带倒计时RecyclerView的设计心路历程

需求 目前有这样一个需求: 1 需要一个页面,展示多个条目 2 每个条目有独立的倒计时,倒计时结束后就删除此条目 3 每个条目上有删除按钮,点击可以删除该条目 4 列表上的条目类型是多样的 可行性分析 首先肯定是可以做的: ...
继续阅读 »

需求



目前有这样一个需求:



  • 1 需要一个页面,展示多个条目

  • 2 每个条目有独立的倒计时,倒计时结束后就删除此条目

  • 3 每个条目上有删除按钮,点击可以删除该条目

  • 4 列表上的条目类型是多样的


可行性分析


首先肯定是可以做的:



  • 1 用一个RecyclerView来实现

  • 2 每个item里面添加一个倒计时控件,注意倒计时是在item对应的数据里面,不是UI里面

  • 3 添加删除按钮,点击就删除对应的数据,并且停止数据对应的倒计时,同时更新适配器

  • 4 使用getViewType()来实现多个item类型


三流程序员看到这里已经去写代码了...


二流以上程序员接着往下看。


需求分析


首先,第1条没问题。


第2条,需要在item对应的数据里面添加一个倒计时组件,这听着就不对,倒计时组件明明是用来更新UI的,应该是UI持有,现在让数据持有,那不就等价于数据间接持有了UI吗,长生命周期持有短生命周期了,不行。而且,数据多的时候,比如10w条数据,就有10w个倒计时组件,cpu不吃不喝也忙不过来(cpu:wqnmlgb)!这明显属于量变引起质变的问题,为了避免这个问题,我们需要将倒计时组件常量化,也就是: 只有常数个倒计时,从而让倒计时组件的个数,不受数据数量的影响。


那么,我们怎么定义这个常量呢?


我们考虑到倒计时是用来更新UI的,那么屏幕内可见的item有多少个,就创建多少个 倒计时组件 不就行了吗,反正屏幕外的,别人也看不见,所以我们可以让ViewHolder持有倒计时组件,而且正好可以利用RecyclerView对ViewHolder的复用机制。


但是,如果让ViewHolder持有,当ViewHolder滑出屏幕外,就会回收,那么倒计时就终止了,此时就无法触发倒计时结束的删除操作,因为即使在屏幕外,只要触发了倒计时的删除数据,我们屏幕内的数据就会向上滑动一个位置,是可以感知的,所以,如果滑出屏幕后,倒计时终止了,就无法触发删除,那么我们可能等了很久,也没发现屏幕内的数据向上滑动,明显是不对的。


程序是为用户服务的,根据以上分析,我们只站在用户角度来考虑:



  • case1 如果倒计时放在数据内,用户可以感知到删除操作,因为有滑动,但是数据多了明显会感觉到卡顿,因为有很多倒计时

  • case2 如果倒计时放在ViewHolder内,用户无法感知到删除操作,因为滑出屏幕倒计时就终止了,但是数据多了不会感觉到卡顿


此乃死锁,无法解决!那么就需要退一步来改下需求了。既然无法完美解决用户的问题,我们就来改变用户的习惯,我们让:倒计时结束后,不再删除item,只是置灰


为什么这么改呢?因为针对case1,我们没法解决,只能从case2入手,而case2的问题就是: 用户无法感知到删除操作,那我就不删除了,这样你也不用感知了,只置灰即可。


好,第二条解决。


第3条,没啥问题,直接remove(index),然后调用adapter.notifyItemRemoved()完事。


第4条,也没啥问题,可以if-else/switch-case,根据不同的type返回不同的ViewHolder。但是可以写的更好点,就是使用工厂模式


设计


可行性分析和需求分析完了后,我们就开始进行概要设计了



  • 1 我们需要创建个RecyclerView。

  • 2 我们需要在ViewHolder里面添加一个倒计时组件,这里我们使用Handler就足够,并且我们需要在进入屏幕时,开启倒计时,在滑出屏幕后,停止倒计时来省cpu。

  • 3 删除数据就不废话了,这都不会的话,回炉重造吧。

  • 4 使用工厂模式,来根据不同的ViewType创建不同的ViewHolder。


这里面有几点需要注意:



  • 1 ViewHolder进入屏幕会触发onBindViewHolder(),滑出屏幕会触发onViewRecycled()。

  • 2 工厂模式要使用多工厂,这样可以降低耦合,有新的ViewType时,只添加就行,可以做到OCP原则。

  • 3 我们可以提前加载工厂,使用map缓存,就跟工厂模式的实现思想里面最后的源码类似,Android源码也是提前加载工厂的。


好,分析结束,开始撸码。


编码


首先,我们先定义数据实体:


// 注意这里的terminalTime,指的指终止时间,不是时间差,是一个时间值。
// type可以理解为ViewType,当然中间有对应关系的
data class BaseItemBean(val id: Long, var terminalTime: Long, val type: Int)

很简单的一行代码,是个Bean对象,直接上data class即可。


然后,我们来定义两个ViewHolder,因为有相同布局,我们可以直接用继承关系:


// 基础ViewHolder
open inner class BaseVH(itemView: View) : RecyclerView.ViewHolder(itemView) {

// 展示倒计时
private val tvTimer = itemView.findViewById<TextView>(R.id.tv_time)
// 删除按钮
private val btnDelete = itemView.findViewById<TextView>(R.id.btn_delete)

init {
btnDelete.setOnClickListener {
onItemDeleteClick?.invoke(adapterPosition)
}
}

/**
* 剩余倒计时
*/
private var delay = 0L

private val timerRunnable = Runnable {
// 这里打印日志,来印证我们只跑了 "屏幕内可展示item数量的 倒计时"
Log.d(TAG, "run: ${hashCode()}")
delay -= 1000
updateTimerState()
}

// 开始倒计时
private fun startTimer() {
timerHandler.postDelayed(timerRunnable, 1000)
}

// 结束倒计时
private fun endTimer() {
timerHandler.removeCallbacks(timerRunnable)
}

// 检测倒计时 并 更新状态
private fun updateTimerState() {
if (delay <= 0) {
// 倒计时结束了
tvTimer.text = "已结束"
itemView.setBackgroundColor(Color.GRAY)
endTimer()
} else {
// 继续倒计时
tvTimer.text = "${delay / 1000}S"
itemView.setBackgroundColor(Color.parseColor("#FFBB86FC"))
startTimer()
}
}

/**
* 进入屏幕时: 填充数据,这里声明为open,让子类重写
*/
open fun display(bean: BaseItemBean) {
Log.d(TAG, "display: $adapterPosition")

// 使用 终止时间 - 当前时间,计算倒计时还有多少秒
delay = bean.terminalTime - System.currentTimeMillis()

// 检测并更新timer状态
updateTimerState()
}

/**
* 滑出屏幕时: 移除倒计时
*/
fun onRecycled() {
Log.d(TAG, "onRecycled: $adapterPosition")

// 终止计时
endTimer()
}
}

在基础ViewHolder里,我们添加了倒计时套件,并且在进入屏幕时,计算并开始倒计时,滑出屏幕后,就终止倒计时,下次滑入屏幕,重新计算delay时间差,再倒计时。


然后看另一个ViewHolder:


// 继承自BaseViewHolder,因为有公共的倒计时套件
inner class OnSaleVH(itemView: View) : BaseVH(itemView) {
// 添加了一个名字
private val tvName = itemView.findViewById<TextView>(R.id.tv_name)

override fun display(bean: BaseItemBean) {
super.display(bean)
// 添加名字
tvName.text = "${bean.id} 在售"
}
}

接下来我们来看创建ViewHolder的工厂:


/**
* 定义抽象工厂
*/
abstract class VHFactory {
abstract fun createVH(context: Context, parent: ViewGroup): BaseVH
}

/**
* BaseViewHolder工厂
*/
inner class BaseVHFactory : VHFactory() {
override fun createVH(context: Context, parent: ViewGroup): BaseVH {
return BaseVH(LayoutInflater.from(context).inflate(R.layout.item_base, parent, false))
}
}

/**
* OnSaleVH工厂
*/
inner class OnSaleVHFactory : VHFactory() {
override fun createVH(context: Context, parent: ViewGroup): BaseVH {
return OnSaleVH(LayoutInflater.from(context).inflate(R.layout.item_on_sale, parent, false))
}
}

很简单,接下来,我们来看Adapter:


class Adapter(private val datas: List<BaseItemBean>) : RecyclerView.Adapter<Adapter.BaseVH>() {

private val TAG = "Adapter"

/**
* 点击item的事件
*/
var onItemDeleteClick: ((position: Int) -> Unit)? = null

/**
* ViewHolder的工厂
*/
private val vhs = SparseArray<VHFactory>()

/**
* 用来执行倒计时
*/
private val timerHandler = Handler(Looper.getMainLooper())

/**
* 初始化工厂
*/
init {
vhs.put(ItemType.ITEM_BASE, BaseVHFactory())
vhs.put(ItemType.ITEM_ON_SALE, OnSaleVHFactory())
}

// 直接从工厂map中获取对应的工厂调用createVH()方法即可
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVH = vhs.get(viewType).createVH(parent.context, parent)

// 滑入屏幕内调用,直接使用hoder.display()展示数据
override fun onBindViewHolder(holder: BaseVH, position: Int) = holder.display(datas[position])

override fun getItemCount(): Int = datas.size

// ViewHolder滑出屏幕调用,进行回收
override fun onViewRecycled(holder: BaseVH) = holder.onRecycled()

/**
* 根据数据类型返回ViewType
*/
override fun getItemViewType(position: Int): Int = datas[position].type
}

代码也很easy,就是使用工厂模式来返回不同的ViewHolder。


写代码的心路历程:



  • 1 因为有多个ViewType,肯定有多个ViewHolder,ViewType和ViewHolder是映射关系

  • 2 可以用if-else,可以用switch-case,但是这样扩展性差

  • 3 所以用多工厂来实现

  • 4 这样需要创建工厂,每次onCreateViewHolder()都要创建吗?不行,那就缓存起来。

  • 5 缓存需要知道哪个工厂创建哪个ViewHolder,而ViewHolder和ViewType对应,所以可以让工厂和ViewType对应,那就创建一个Map。

  • 6 ViewType是Integer类型的,那就可以用更加省内存的SparseArray(),原因可以看这里

  • 7 于是,我们就有了上述代码。


我们定义的ViewType(都是int类型的,因为int的匹配速度快):


object ItemType {
const val ITEM_BASE = 0x001
const val ITEM_ON_SALE = 0x002
}

接下来我们就可以在Activity中使用了:


class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.recyclerView.layoutManager = LinearLayoutManager(this)

// 添加测试数据
val beans = ArrayList<BaseItemBean>()
for (i in 0..100) {
// 计算终止时间,这里都是当前时间 + i乘以10s
val terminalTime = System.currentTimeMillis() + i * 10_000
// 这里手动计算了ViewType (i%2)+1
beans.add(BaseItemBean(i.toLong(), terminalTime, (i % 2) + 1))
}

val adapter = Adapter(beans)
adapter.onItemDeleteClick = { position ->
// 点击就删除
beans.removeAt(position)
adapter.notifyItemRemoved(position)
}
binding.recyclerView.adapter = adapter

}
}

效果如下:


收起阅读 »

在 Flutter 中探索 StreamBuilder

原文 medium.com/flutterdevs… 正文 异步交互可能需要一个理想的机会来进行总结。偶尔,在周期结束之前可能会发出一些值。在 Dart 中,您可以创建一个返回 Stream 的容量,该容量可以在异步进程处于活动状态时发射一些值...
继续阅读 »


原文



medium.com/flutterdevs…



正文


异步交互可能需要一个理想的机会来进行总结。偶尔,在周期结束之前可能会发出一些值。在 Dart 中,您可以创建一个返回 Stream 的容量,该容量可以在异步进程处于活动状态时发射一些值。假设您需要根据一个 Stream 的快照在 Flutter 中构造一个小部件,那么有一个名为 StreamBuilder 的小部件。


在这个博客中,我们将探索 Flutter 中的 StreamBuilder。我们还将实现一个演示程序,并向您展示如何在您的 Flutter 应用程序中使用 StreamBuilder。


介绍:


StreamBuilder 可以监听公开的流,并返回小部件和捕获获得的流信息的快照。造溪者提出了两个论点。



A stream


构建器,它可以将流中的多个组件更改为小部件



Stream 像一条线。当您从一端输入值而从另一端输入侦听器时,侦听器将获得该值。一个流可以有多个侦听器,这些侦听器的负载可以获得流水线,流水线将获得等价值。如何在流上放置值是通过使用流控制器实现的。流构建器是一个小部件,它可以将用户定义的对象更改为流。


建造者:



要使用 StreamBuilder,需要调用下面的构造函数:



const StreamBuilder({
Key? key,
Stream<T>? stream,
T? initialData,
required AsyncWidgetBuilder<T> builder,
})

实际上,您需要创建一个 Stream 并将其作为流争用传递。然后,在这一点上,您需要传递一个 AsyncWidgetBuilder,该 AsyncWidgetBuilder 可用于构造依赖于 Stream 快照的小部件。


参数:



下面是 StreamBuilderare 的一些参数:




  • Key? key: 小部件的键,用于控制小部件如何被另一个小部件取代

  • Stream<T>? stream: 一个流,其快照可以通过生成器函数获得

  • T? initialData: 将利用这些数据制作初始快照

  • required AsyncWidgetBuilder<T> builder: 生成过程由此生成器使用


如何实现 dart 文件中的代码:


你需要分别在你的代码中实现它:



让我们创建一个流:



下面的函数返回一个每秒生成一个数字的 Stream。你需要使用 async * 关键字来创建一个流。若要发出值,可以使用 yield 关键字后跟要发出的值。


Stream<int> generateNumbers = (() async* {
await Future<void>.delayed(Duration(seconds: 2));

for (int i = 1; i <= 10; i++) {
await Future<void>.delayed(Duration(seconds: 1));
yield i;
}
})();


From that point onward, pass it as the stream argument


从那一点开始,把它作为流参数传递下去



StreamBuilder<int>(
stream: generateNumbers,
// other arguments
)


让我们创建一个 AsyncWidgetBuilder



构造函数期望您传递一个类型为 AsyncWidgetBuilder 的命名争用构建器。这是一个有两个参数的函数,它们的类型都是 BuildContext 和 AsyncSnapshot < t > 。后续的边界(包含当前快照)可以用来确定应该呈现的内容。


要创建这个函数,首先需要了解 AsyncSnapshot。AsyncSnapshot 是使用异步计算的最新通信的不变描述。在这种独特的情况下,它解决了与 Stream 的最新通信。可以通过 AsyncSnapshot 属性获取流的最新快照。您可能需要使用的属性之一是 connectionState,这个枚举将当前关联状态转换为异步计算,在这种特殊情况下,这种异步计算就是 Steam。


StreamBuilder<int>(
stream: generateNumbers,
builder: (
BuildContext context,
AsyncSnapshot<int> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._red_, fontSize: 40)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),

AsyncSnapshot 还有一个名为 hasError 的属性,可用于检查快照是否包含非空错误值。如果异步活动的最新结果失败,hasError 值将有效。为了获取信息,首先,您可以通过获取其 hasData 属性来检查快照是否包含信息,如果 Stream 有效地释放了任何非空值,那么 hasData 属性将是有效的。然后,在这一点上,您可以从 AsyncSnapshot 的数据属性获取信息。


由于上面属性的值,您可以计算出应该在屏幕上呈现什么。在下面的代码中,当 connectionState 值正在等待时,将显示一个 CircularProgressIndicator。当 connectionState 更改为 active 或 done 时,可以检查快照是否有错误或信息。建造函数称为 Flutter 管道的检测。因此,它将获得一个与时间相关的快照子组。这意味着,如果在实际上相似的时间里,Stream 发出了一些值,那么一部分值可能没有传递给构建器。



枚举有一些可能的值:




  • > none: 无: 不与任何异步计算关联。如果流为空,则可能发生

  • > waiting: 等待: 与异步计算关联并等待协作。在这个上下文中,它暗示流还没有完成

  • > active: 活跃的: 与活动的异步计算相关联。例如,如果一个 Stream 已经返回了任何值,但此时还没有结束

  • > done: > 完成: 与结束的异步计算相关联。在这个上下文中,它暗示流已经完成



设置初始数据:



您可以选择传递一个 worth 作为 initialData 参数,这个参数将被利用,直到 Stream 发出 a。如果传递的值不为空,那么当 connectionState 在等待时,hasData 属性在任何事件中首先都将为 true


StreamBuilder<int>(
initialData: 0,
// other arguments
)

要在 connectionState 等待时显示初始数据,应该调整 if snapshot.connectionState = = connectionState.waiting,然后调整上面代码中的块。


if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Visibility(
visible: snapshot.hasData,
child: Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._black_, fontSize: 24),
),
),
],
);
}

当我们运行应用程序,我们应该得到屏幕的输出像下面的屏幕视频。



Code File:


密码档案:


import 'package:flutter/material.dart';
import 'package:flutter_steambuilder_demo/splash_screen.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Splash(),
debugShowCheckedModeBanner: false,
);
}
}

Stream<int> generateNumbers = (() async* {
await Future<void>.delayed(Duration(seconds: 2));

for (int i = 1; i <= 10; i++) {
await Future<void>.delayed(Duration(seconds: 1));
yield i;
}
})();

class StreamBuilderDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _StreamBuilderDemoState ();
}
}

class _StreamBuilderDemoState extends State<StreamBuilderDemo> {

@override
initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Flutter StreamBuilder Demo'),
),
body: SizedBox(
width: double._infinity_,
child: Center(
child: StreamBuilder<int>(
stream: generateNumbers,
initialData: 0,
builder: (
BuildContext context,
AsyncSnapshot<int> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Visibility(
visible: snapshot.hasData,
child: Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._black_, fontSize: 24),
),
),
],
);
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors._red_, fontSize: 40)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),
),
),
);
}
}

结语:


在本文中,我已经简单介绍了 StreamBuilder 的基本结构; 您可以根据自己的选择修改这段代码。这是我对 StreamBuilder On User Interaction 的一个小小介绍,它正在使用 Flutter 工作。

收起阅读 »

iOS 高效绘图 二

异步绘制    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。  ...
继续阅读 »

异步绘制

    UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。

    针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选择:CATiledLayerdrawsAsynchronously属性。

CATiledLayer

    我们在第六章简单探索了一下CATiledLayer。除了将图层再次分割成独立更新的小块(类似于脏矩形自动更新的概念),CATiledLayer还有一个有趣的特性:在多个线程中为每个小块同时调用-drawLayer:inContext:方法。这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的CATiledLayer是实现异步更新图片视图的简单方法。

drawsAsynchronously

    iOS 6中,苹果为CALayer引入了这个令人好奇的属性,drawsAsynchronously属性对传入-drawLayer:inContext:的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。

    它与CATiledLayer使用的异步绘制并不相同。它自己的-drawLayer:inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。

    根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如UITableViewCell之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。

总结

本章我们主要围绕用Core Graphics软件绘制讨论了一些性能挑战,然后探索了一些改进方法:比如提高绘制性能或者减少需要绘制的数量。

第14章,『图像IO』,我们将讨论图片的载入性能。

收起阅读 »

iOS 高效绘图 一

高效绘图不必要的效率考虑往往是性能问题的万恶之源。 ——William Allan Wulf    在第12章『速度的曲率』我们学习如何用Instruments来诊断Core Animation性能问题。在构建一个iOS...
继续阅读 »

高效绘图

不必要的效率考虑往往是性能问题的万恶之源。 ——William Allan Wulf

    在第12章『速度的曲率』我们学习如何用Instruments来诊断Core Animation性能问题。在构建一个iOS app的时候会遇到很多潜在的性能陷阱,但是在本章我们将着眼于有关绘制的性能问题。

软件绘图

    术语绘图通常在Core Animation的上下文中指代软件绘图(意即:不由GPU协助的绘图)。在iOS中,软件绘图通常是由Core Graphics框架完成来完成。但是,在一些必要的情况下,相比Core Animation和OpenGL,Core Graphics要慢了不少。

    软件绘图不仅效率低,还会消耗可观的内存。CALayer只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。

    但是一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽*图层高*4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048*1526*4字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。

    软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。


矢量图形

    我们用Core Graphics来绘图的一个通常原因就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些:

  • 任意多边形(不仅仅是一个矩形)
  • 斜线或曲线
  • 文本
  • 渐变

    举个例子,清单13.1 展示了一个基本的画线应用。这个应用将用户的触摸手势转换成一个UIBezierPath上的点,然后绘制成视图。我们在一个UIView子类DrawingView中实现了所有的绘制逻辑,这个情况下我们没有用上view controller。但是如果你喜欢你可以在view controller中实现触摸事件处理。图13.1是代码运行结果。

清单13.1 用Core Graphics实现一个简单的绘图应用

#import "DrawingView.h"

@interface DrawingView ()

@property (nonatomic, strong) UIBezierPath *path;

@end

@implementation DrawingView

- (void)awakeFromNib
{
//create a mutable path
self.path = [[UIBezierPath alloc] init];
self.path.lineJoinStyle = kCGLineJoinRound;
self.path.lineCapStyle = kCGLineCapRound;

self.path.lineWidth = 5;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];

//add a new line segment to our path
[self.path addLineToPoint:point];

//redraw the view
[self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
//draw path
[[UIColor clearColor] setFill];
[[UIColor redColor] setStroke];
[self.path stroke];
}
@end

图13.1

图13.1 用Core Graphics做一个简单的『素描』

    这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径(UIBezierPath),随着路径越来越复杂,每次重绘的工作就会增加,直接导致了帧数的下降。看来我们需要一个更好的方法了。

    Core Animation为这些图形类型的绘制提供了专门的类,并给他们提供硬件支持(第六章『专有图层』有详细提到)。CAShapeLayer可以绘制多边形,直线和曲线。CATextLayer可以绘制文本。CAGradientLayer用来绘制渐变。这些总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图。

    如果稍微将之前的代码变动一下,用CAShapeLayer替代Core Graphics,性能就会得到提高(见清单13.2).虽然随着路径复杂性的增加,绘制性能依然会下降,但是只有当非常非常浮躁的绘制时才会感到明显的帧率差异。

清单13.2 用CAShapeLayer重新实现绘图应用

#import "DrawingView.h"
#import

@interface DrawingView ()

@property (nonatomic, strong) UIBezierPath *path;

@end

@implementation DrawingView

+ (Class)layerClass
{
//this makes our view create a CAShapeLayer
//instead of a CALayer for its backing layer
return [CAShapeLayer class];
}

- (void)awakeFromNib
{
//create a mutable path
self.path = [[UIBezierPath alloc] init];

//configure the layer
CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.lineWidth = 5;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];

//add a new line segment to our path
[self.path addLineToPoint:point];

//update the layer with a copy of the path
((CAShapeLayer *)self.layer).path = self.path.CGPath;
}
@end

脏矩形

    有时候用CAShapeLayer或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用CAShapeLayer没办法实现。

    我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。

    我们的『黑板』应用的最初实现见清单13.3,我们更改了之前版本的DrawingView,用一个画刷位置的数组代替UIBezierPath。图13.2是运行结果

清单13.3 简单的类似黑板的应用

#import "DrawingView.h"
#import
#define BRUSH_SIZE 32

@interface DrawingView ()

@property (nonatomic, strong) NSMutableArray *strokes;

@end

@implementation DrawingView

- (void)awakeFromNib
{
//create array
self.strokes = [NSMutableArray array];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self];

//add brush stroke
[self addBrushStrokeAtPoint:point];
}

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//needs redraw
[self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);

//draw brush stroke 
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
@end

图13.2

图13.2 用程序绘制一个简单的『素描』

    这个实现在模拟器上表现还不错,但是在真实设备上就没那么好了。问题在于每次手指移动的时候我们就会重绘之前的线刷,即使场景的大部分并没有改变。我们绘制地越多,就会越慢。随着时间的增加每次重绘需要更多的时间,帧数也会下降(见图13.3),如何提高性能呢?

图13.3

图13.3 帧率和线条质量会随时间下降。

    为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。

    当一个视图被改动过了,TA可能需要重绘。但是很多情况下,只是这个视图的一部分被改变了,所以重绘整个寄宿图就太浪费了。但是Core Animation通常并不了解你的自定义绘图代码,它也不能自己计算出脏区域的位置。然而,你的确可以提供这些信息。

    当你检测到指定视图或图层的指定部分需要被重绘,你直接调用-setNeedsDisplayInRect:来标记它,然后将影响到的矩形作为参数传入。这样就会在一次视图刷新时调用视图的-drawRect:(或图层代理的-drawLayer:inContext:方法)。

    传入-drawLayer:inContext:CGContext参数会自动被裁切以适应对应的矩形。为了确定矩形的尺寸大小,你可以用CGContextGetClipBoundingBox()方法来从上下文获得大小。调用-drawRect()会更简单,因为CGRect会作为参数直接传入。

    你应该将你的绘制工作限制在这个矩形中。任何在此区域之外的绘制都将被自动无视,但是这样CPU花在计算和抛弃上的时间就浪费了,实在是太不值得了。

    相比依赖于Core Graphics为你重绘,裁剪出自己的绘制区域可能会让你避免不必要的操作。那就是说,如果你的裁剪逻辑相当复杂,那还是让Core Graphics来代劳吧,记住:当你能高效完成的时候才这样做。

    清单13.4 展示了一个-addBrushStrokeAtPoint:方法的升级版,它只重绘当前线刷的附近区域。另外也会刷新之前线刷的附近区域,我们也可以用CGRectIntersectsRect()来避免重绘任何旧的线刷以不至于覆盖已更新过的区域。这样做会显著地提高绘制效率(见图13.4)

    清单13.4 用-setNeedsDisplayInRect:来减少不必要的绘制

- (void)addBrushStrokeAtPoint:(CGPoint)point
{
//add brush stroke to array
[self.strokes addObject:[NSValue valueWithCGPoint:point]];

//set dirty rect
[self setNeedsDisplayInRect:[self brushRectForPoint:point]];
}

- (CGRect)brushRectForPoint:(CGPoint)point
{
return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE);
}

- (void)drawRect:(CGRect)rect
{
//redraw strokes
for (NSValue *value in self.strokes) {
//get point
CGPoint point = [value CGPointValue];

//get brush rect
CGRect brushRect = [self brushRectForPoint:point];

//only draw brush stroke if it intersects dirty rect
if (CGRectIntersectsRect(rect, brushRect)) {
//draw brush stroke
[[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect];
}
}
}

图13.4

图13.4 更好的帧率和顺滑线条

收起阅读 »

iOS 性能调优 三

Instruments    Instruments是Xcode套件中没有被充分利用的一个工具。很多iOS开发者从没用过Instruments,或者只是用Leaks工具检测循环引用。实际上有很多Instruments工具...
继续阅读 »

Instruments

    Instruments是Xcode套件中没有被充分利用的一个工具。很多iOS开发者从没用过Instruments,或者只是用Leaks工具检测循环引用。实际上有很多Instruments工具,包括为动画性能调优的东西。

    你可以通过在菜单中选择Profile选项来打开Instruments(在这之前,记住要把目标设置成iOS设备,而不是模拟器)。然后将会显示出图12.1(如果没有看到所有选项,你可能设置成了模拟器选项)。

图12.1

图12.1 Instruments工具选项窗口

    就像之前提到的那样,你应该始终将程序设置成发布选项。幸运的是,配置文件默认就是发布选项,所以你不需要在分析的时候调整编译策略。

我们将讨论如下几个工具:

  • 时间分析器 - 用来测量被方法/函数打断的CPU使用情况。

  • Core Animation - 用来调试各种Core Animation性能问题。

  • OpenGL ES驱动 - 用来调试GPU性能问题。这个工具在编写Open GL代码的时候很有用,但有时也用来处理Core Animation的工作。

    Instruments的一个很棒的功能在于它可以创建我们自定义的工具集。除了你初始选择的工具之外,如果在Instruments中打开Library窗口,你可以拖拽别的工具到左侧边栏。我们将创建以上我们提到的三个工具,然后就可以并行使用了(见图12.2)。

图12.2

图12.2 添加额外的工具到Instruments侧边栏

时间分析器

    时间分析器工具用来检测CPU的使用情况。它可以告诉我们程序中的哪个方法正在消耗大量的CPU时间。使用大量的CPU并不一定是个问题 - 你可能期望动画路径对CPU非常依赖,因为动画往往是iOS设备中最苛刻的任务。

    但是如果你有性能问题,查看CPU时间对于判断性能是不是和CPU相关,以及定位到函数都很有帮助(见图12.3)。

图12.3

图12.3 时间分析器工具

    时间分析器有一些选项来帮助我们定位到我们关心的的方法。可以使用左侧的复选框来打开。其中最有用的是如下几点:

  • 通过线程分离 - 这可以通过执行的线程进行分组。如果代码被多线程分离的话,那么就可以判断到底是哪个线程造成了问题。

  • 隐藏系统库 - 可以隐藏所有苹果的框架代码,来帮助我们寻找哪一段代码造成了性能瓶颈。由于我们不能优化框架方法,所以这对定位到我们能实际修复的代码很有用。

  • 只显示Obj-C代码 - 隐藏除了Objective-C之外的所有代码。大多数内部的Core Animation代码都是用C或者C++函数,所以这对我们集中精力到我们代码中显式调用的方法就很有用。

Core Animation

    Core Animation工具用来监测Core Animation性能。它给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画(见图12.4)。

图12.4

图12.4 使用可视化调试选项的Core Animation工具

    Core Animation工具也提供了一系列复选框选项来帮助调试渲染瓶颈:

  • Color Blended Layers - 这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。

  • ColorHitsGreenandMissesRed - 当使用shouldRasterizep属性的时候,耗时的图层绘制会被缓存,然后当做一个简单的扁平图片呈现。当缓存再生的时候这个选项就用红色对栅格化图层进行了高亮。如果缓存频繁再生的话,就意味着栅格化可能会有负面的性能影响了(更多关于使用shouldRasterize的细节见第15章“图层性能”)。

  • Color Copied Images - 有时候寄宿图片的生成意味着Core Animation被强制生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针。这个选项把这些图片渲染成蓝色。复制图片对内存和CPU使用来说都是一项非常昂贵的操作,所以应该尽可能的避免。

  • Color Immediately - 通常Core Animation Instruments以每毫秒10次的频率更新图层调试颜色。对某些效果来说,这显然太慢了。这个选项就可以用来设置每帧都更新(可能会影响到渲染性能,而且会导致帧率测量不准,所以不要一直都设置它)。

  • Color Misaligned Images - 这里会高亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图片(也就是非整型坐标)。这些中的大多数通常都会导致图片的不正常缩放,如果把一张大图当缩略图显示,或者不正确地模糊图像,那么这个选项将会帮你识别出问题所在。

  • Color Offscreen-Rendered Yellow - 这里会把那些需要离屏渲染的图层高亮成黄色。这些图层很可能需要用shadowPath或者shouldRasterize来优化。

  • Color OpenGL Fast Path Blue - 这个选项会对任何直接使用OpenGL绘制的图层进行高亮。如果仅仅使用UIKit或者Core Animation的API,那么不会有任何效果。如果使用GLKView或者CAEAGLLayer,那如果不显示蓝色块的话就意味着你正在强制CPU渲染额外的纹理,而不是绘制到屏幕。

  • Flash Updated Regions - 这个选项会对重绘的内容高亮成黄色(也就是任何在软件层面使用Core Graphics绘制的图层)。这种绘图的速度很慢。如果频繁发生这种情况的话,这意味着有一个隐藏的bug或者说通过增加缓存或者使用替代方案会有提升性能的空间。

    这些高亮图层的选项同样在iOS模拟器的调试菜单也可用(图12.5)。我们之前说过用模拟器测试性能并不好,但如果你能通过这些高亮选项识别出性能问题出在什么地方的话,那么使用iOS模拟器来验证问题是否解决也是比真机测试更有效的。

图12.5

图12.5 iOS模拟器中Core Animation可视化调试选项

OpenGL ES驱动

    OpenGL ES驱动工具可以帮你测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animation那样显示FPS的工具(图12.6)。

图12.6

图12.6 OpenGL ES驱动工具

侧栏的邮编是一系列有用的工具。其中和Core Animation性能最相关的是如下几点:

  • Renderer Utilization - 如果这个值超过了~50%,就意味着你的动画可能对帧率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。

  • Tiler Utilization - 如果这个值超过了~50%,就意味着你的动画可能限制于几何结构方面,也就是在屏幕上有太多的图层占用了。

一个可用的案例

    现在我们已经对Instruments中动画性能工具非常熟悉了,那么可以用它在现实中解决一些实际问题。

    我们创建一个简单的显示模拟联系人姓名和头像列表的应用。注意即使把头像图片存在应用本地,为了使应用看起来更真实,我们分别实时加载图片,而不是用–imageNamed:预加载。同样添加一些图层阴影来使得列表显示得更真实。清单12.1展示了最初版本的实现。

清单12.1 使用假数据的一个简单联系人列表

#import "ViewController.h"
#import

@interface ViewController ()

@property (nonatomic, strong) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (NSString *)randomName
{
NSArray *first = @[@"Alice", @"Bob", @"Bill", @"Charles", @"Dan", @"Dave", @"Ethan", @"Frank"];
NSArray *last = @[@"Appleseed", @"Bandicoot", @"Caravan", @"Dabble", @"Ernest", @"Fortune"];
NSUInteger index1 = (rand()/(double)INT_MAX) * [first count];
NSUInteger index2 = (rand()/(double)INT_MAX) * [last count];
return [NSString stringWithFormat:@"%@ %@", first[index1], last[index2]];
}

- (NSString *)randomAvatar
{
NSArray *images = @[@"Snowman", @"Igloo", @"Cone", @"Spaceship", @"Anchor", @"Key"];
NSUInteger index = (rand()/(double)INT_MAX) * [images count];
return images[index];
}

- (void)viewDidLoad
{
[super viewDidLoad];
//set up data
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {
//add name
[array addObject:@{@"name": [self randomName], @"image": [self randomAvatar]}];
}
self.items = array;
//register cell class
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
//load image
NSDictionary *item = self.items[indexPath.row];
NSString *filePath = [[NSBundle mainBundle] pathForResource:item[@"image"] ofType:@"png"];
//set image and text
cell.imageView.image = [UIImage imageWithContentsOfFile:filePath];
cell.textLabel.text = item[@"name"];
//set image shadow
cell.imageView.layer.shadowOffset = CGSizeMake(0, 5);
cell.imageView.layer.shadowOpacity = 0.75;
cell.clipsToBounds = YES;
//set text shadow
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
cell.textLabel.layer.shadowOpacity = 0.5;
return cell;
}

@end

    当快速滑动的时候就会非常卡(见图12.7的FPS计数器)。

图12.7

图12.7 滑动帧率降到15FPS

    仅凭直觉,我们猜测性能瓶颈应该在图片加载。我们实时从闪存加载图片,而且没有缓存,所以很可能是这个原因。我们可以用一些很赞的代码修复,然后使用GCD异步加载图片,然后缓存。。。等一下,在开始编码之前,测试一下假设是否成立。首先用我们的三个Instruments工具分析一下程序来定位问题。我们推测问题可能和图片加载相关,所以用Time Profiler工具来试试(图12.8)。

图12.8

图12.8 用The timing profile分析联系人列表

    -tableView:cellForRowAtIndexPath:中的CPU时间总利用率只有~28%(也就是加载头像图片的地方),非常低。于是建议是CPU/IO并不是真正的限制因素。然后看看是不是GPU的问题:在OpenGL ES Driver工具中检测GPU利用率(图12.9)。

图12.9

图12.9 OpenGL ES Driver工具显示的GPU利用率

    渲染服务利用率的值达到51%和63%。看起来GPU需要做很多工作来渲染联系人列表。

    为什么GPU利用率这么高呢?我们来用Core Animation调试工具选项来检查屏幕。首先打开Color Blended Layers(图12.10)。

图12.10

图12.10 使用Color Blended Layers选项调试程序

    屏幕中所有红色的部分都意味着字符标签视图的高级别混合,这很正常,因为我们把背景设置成了透明色来显示阴影效果。这就解释了为什么渲染利用率这么高了。

    那么离屏绘制呢?打开Core Animation工具的Color Offscreen - Rendered Yellow选项(图12.11)。

图12.11

图12.11 Color Offscreen–Rendered Yellow选项

    所有的表格单元内容都在离屏绘制。这一定是因为我们给图片和标签视图添加的阴影效果。在代码中禁用阴影,然后看下性能是否有提高(图12.12)。

图12.12

图12.12 禁用阴影之后运行程序接近60FPS

    问题解决了。干掉阴影之后,滑动很流畅。但是我们的联系人列表看起来没有之前好了。那如何保持阴影效果而且不会影响性能呢?

    好吧,每一行的字符和头像在每一帧刷新的时候并不需要变,所以看起来UITableViewCell的图层非常适合做缓存。我们可以使用shouldRasterize来缓存图层内容。这将会让图层离屏之后渲染一次然后把结果保存起来,直到下次利用的时候去更新(见清单12.2)。

清单12.2 使用shouldRasterize提高性能

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"
forIndexPath:indexPath];
...
//set text shadow
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
cell.textLabel.layer.shadowOpacity = 0.5;
//rasterize
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}

    我们仍然离屏绘制图层内容,但是由于显式地禁用了栅格化,Core Animation就对绘图缓存了结果,于是对提高了性能。我们可以验证缓存是否有效,在Core Animation工具中点击Color Hits Green and Misses Red选项(图12.13)。

图12.13

图12.13 Color Hits Green and Misses Red验证了缓存有效

    结果和预期一致 - 大部分都是绿色,只有当滑动到屏幕上的时候会闪烁成红色。因此,现在帧率更加平滑了。

    所以我们最初的设想是错的。图片的加载并不是真正的瓶颈所在,而且试图把它置于一个复杂的多线程加载和缓存的实现都将是徒劳。所以在动手修复之前验证问题所在是个很好的习惯!

总结

    在这章中,我们学习了Core Animation是如何渲染,以及我们可能出现的瓶颈所在。你同样学习了如何使用Instruments来检测和修复性能问题。

    在下三章中,我们将对每个普通程序的性能陷阱进行详细讨论,然后学习如何修复。


收起阅读 »

带着问题学习Android中View的measure测量

在进行研究measure原理之前,我们先带着这三个问题来想想。因为我是遇到这三个问题才开始研究measure的源码,所以我也把下面的三个问题当做引子。调用measure(int widthMeasureSpec, int heightMeasureSpec)方...
继续阅读 »

在进行研究measure原理之前,我们先带着这三个问题来想想。因为我是遇到这三个问题才开始研究measure的源码,所以我也把下面的三个问题当做引子。

调用measure(int widthMeasureSpec, int heightMeasureSpec)方法传递的参数是什么? 为什么调用measure方法View控件没有进行测量? 如何强制view进行测量? 在进行研究之前,我们先来看一个简单的布局,

        <Button
android:id="@+id/btn_click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"
android:onClick="start"
/>

<LinearLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FF0000">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"/>

</LinearLayout>

看效果图:

image.png

根据布局文件,我们并没有设置边距属性,为什么显示的效果的Button跟下面的没有对齐。这就是。在实际开发中,我们细心点会发现,对于Button控件,我们选中它的时候显示的区域比它展现的区域大。

如果我们给Button控件添加背景色:

        <Button
android:id="@+id/btn_click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"
android:background="#FF0000"
android:onClick="start"
/>

可以看到Button的背景色和LinearLayout的背景色无缝连接在一起,同时我们观察下面的那个点击的Button,发现它的周围区域实际是存在的,是白色与我们的背景色重叠起来了。这就引入了我们的一个重要概念:控件边界布局和视觉编辑布局。我们在真机上打开【显示布局边界】,在设置——>开发者选项——>显示布局边界。 我们看下效果图。

注:蓝色 为控件的布局边界;粉红色为视觉边界 这就涉及到我们的一个ViewGroup属性:android:layoutMode

说的通俗一点,clipBounds就是默认值,不处理一些控件之间的“留白”,opticalBounds消除控件之间的留白。

我们抽出LinearLayout的布局来说:

        <LinearLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff000"
android:layoutMode="clipBounds">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试的"
/>

</LinearLayout>

我们修改属性android:layoutMode=”opticalBounds”,效果图:

image.png

通过对比发现就是一个清除的效果。

MeasureSpec 我们分析第一个问题,onMeasure()方法里传的是什么?传的就是MeasureSpec变量。它是View的一个内部类。源码设计非常简单精悍。

一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由大小和模式组成。由32位组成,头8位为模式,后24位封装大小。它有三种模式:UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;AT_MOST(至多),子元素至多达到指定大小的值。它常用的三个函数:

static int getMode(int measureSpec):根据提供的测量值(格式)提取模式(上述三个模式之一) static int getSize(int measureSpec):根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小) static int makeMeasureSpec(int size,int mode):根据提供的大小值和模式创建一个测量值(格式) Mode的取值:

MeasureSpec.AT_MOST,即十进制2,该值表示View最大可以取其父ViewGroup给其指定的尺寸,例如现在有个Int值widthMeasureSpec,ViewGroup将其传递给了View的measure方法,如果widthMeasureSpec中的mode值是AT_MOST,size是300,那么表示View能取的最大的宽度是300。

MeasureSpec.EXACTLY,即十进制1,该值表示View必须使用其父ViewGroup指定的尺寸,还是以widthMeasureSpec为例,如果其mode值是EXACTLY,控件大小就是它老子的大小

MeasureSpec.UNSPECIFIED,即十进制0,该值表示View的父ViewGroup没有给View在尺寸上设置限制条件,这种情况下View可以忽略measureSpec中的size,View可以取自己想要的值作为量算的尺寸。

我们常看到measure(0,0)或者measure(1,1)之类的,这就是传入的测量模式。

measure()方法 下面就开始分析measure方法。

       public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//判断当前view的LayoutMode是否为opticalbounds
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {//判断当前view的ParentView的LayoutMode是否为opticalbounds
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}

// 根据我们传入的widthMeasureSpec和heightMeasureSpec计算key值,我们在mMeasureCache中存储我们view的信息
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
//如果mMeasureCache为null,则进行new一个对象
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

//mOldWidthMeasureSpec和mOldHeightMeasureSpec分别表示上次对View进行量算时的widthMeasureSpec和heightMeasureSpec
//执行View的measure方法时,View总是先检查一下是不是真的有必要费很大力气去做真正的量算工作
//mPrivateFlags是一个Int类型的值,其记录了View的各种状态位
//如果(mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT,
//那么表示当前View需要强制进行layout(比如执行了View的forceLayout方法),所以这种情况下要尝试进行量算
//如果新传入的widthMeasureSpec/heightMeasureSpec与上次量算时的mOldWidthMeasureSpec/mOldHeightMeasureSpec不等,
//那么也就是说该View的父ViewGroup对该View的尺寸的限制情况有变化,这种情况下要尝试进行量算
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {

//通过运算,重置mPrivateFlags值,即View的测量状态
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
//解决布局中的Rtl问题
resolveRtlPropertiesIfNeeded();
//判断当前View是否是强制进行测量,如果是则将cacheIndex=-1,反之从mMeasureCache中获取
//对应的index,即从缓存中读取存储的大小。
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
//根据cacheIndex的大小判断是否需要重新测量,或者根据布尔变量sIgnoreMeasureCache进行判断。
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 重新测量,则调用我们重写的onMeasure()方法进行测量,然后重置View的状态
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
// 通过我们计算的cacheIndex值,从缓存中读取我们的测量值。
long value = mMeasureCache.valueAt(cacheIndex);
// 通过setMeasuredDimension()方法设置我们的测量值,然后重置View的状态
setMeasuredDimension((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

// 如果View的状态没有改变,则会抛出异常“我们没有调用”setMeasuredDimension()“方法,一般出现在我们重写onMeasure方法,
//但是没有调用setMeasuredDimension方法导致的。
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}

mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//将最新的widthMeasureSpec和heightMeasureSpec进行存储到mMeasureCache
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

在上面的代码中,注释还算详细,仔细看应该能知道测量的流程。 (1)、测量首先判断控件的模式,通过调用isLayoutModeOptical方法进行判断。

        public static boolean isLayoutModeOptical(Object o) {
return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
}

//ViewGroup的isLayoutModeOptical方法
boolean isLayoutModeOptical() {
return mLayoutMode == LAYOUT_MODE_OPTICAL_BOUNDS;
}

这个方法就是判断view是否为ViewGroup类型,然后判断layoutMode设定是否为opticalBounds。如果是,则对传入的widthMeasureSpec、heightMeasureSpec进行重新计算封装,通过上面的试验,我们看到了设定的区别,所以需要重新计算封装。

(2)、判断当前view是否强制重新计算,或者传入进来的MeasureSpec是否和上次不同。这两种情况满足一种则进行测量运算。 (3)、系统还不满足,又判断是否为强制测量,如果为强制测量或者忽略缓存,则调用我们重写的onMeasure()方法进行测量,反之,从mMeasureCache缓存中读取上次的测量数据。

为什么调用measure()方法控件没有进行重新测量?

通过前面的源码分析,是不是对结果知道一二,View也不是因为我们调用了measure方法就进行真真切切的重新测量,首先,它会判断我们是否是强制测量或者测量模式发生了改变没有,这个是必要条件,如果这里都不满足就不会进入执行到我们的onMeasure方法,之后还要判断我们是否强制重新测量,不然取缓存的值,只样实际上还没有达到我们的测量。 注:Android不同版本对应的measure方法源码可能有所不同。

说到这里,measure的源码是分析了,我们在往深入的想,我们如果在我们的自定义View时没有对onMeasure()方法进行重写,那么系统调用的onMeasure()方法是怎么实现的呢?不错,我们就瞧一瞧View中默认的onMeasure()方法是怎么实现的。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

这里面涉及到三个方法:

  • getDefaultSize
  • getSuggestedMinimumWidth
  • getSuggestedMinimumHeight

稍微思考下,我们也知道肯定是设置一个默认值的,我们看下后两个函数的源码:

    protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

都是进行判断backgroud是否为空,如果为空,返回view最小的高度或宽度,如果不为空,返回与backgroud的最小宽高中的最大值。可能你会疑惑,view的最小宽度或高度是怎么来的?这个就要回归到我们的View构造函数。

    case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
case R.styleable.View_minHeight:
mMinHeight = a.getDimensionPixelSize(attr, 0);
break;

可以从这里获取,当然我们也可以进行设定:

    public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}


public void setMinimumHeight(int minHeight) {
mMinHeight = minHeight;
requestLayout();
}

我们接着看看看getDefaultSize()的源码:

    public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

在getDefaultSize中,传入进来我们获取的最小值,然后根据我们设定的MeasureSpec获取对应size和mode,然后判断mode,如果为MeasureSpec.UNSPECIFIED就将size赋值我们获取的最小大小。模式为MeasureSpec.AT_MOST、MeasureSpec.EXACTLY时,赋值为我们从MeasureSpec获取的大小。这也证实了自定义控件时,我们没有重写onMeasure方法,同时给控件设置wrap_content属性,控件显示的效果是match_parent的效果。

说到这里measure流程的大概也基本搞明白了。 我们来看第三个问题,如何强制一个view进行重绘? 根据上面的分析,我们强制重绘就得清除缓存mMeasureCache缓存中的数据。这里就得提及forceLayout()方法,看下这个方法的源码:

    public void forceLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
}

这个方法中就是清除缓存mMeasureCache中的缓存数据,然后改变View的mPrivateFlags属性值。这里又得说起requestLayout()函数,用于请求重新布局。


public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

这样就可以完成View的强制测量。在实际的开发中,我们在对自定义View进行测量的时候,只需要重写onMeasure()方法即可,在onMeasure()方法中指定我们要求的控件大小,除非我们在刷新控件的时候需要我们去考虑一些方法的实现,探究源码让我们知道了为什么是这样,不至于迷惘。

收起阅读 »

kotlin协程最佳实践-android官网

协程最佳实践 android官网地址 这些实践可以让你的程序在使用协程的时候更加的易扩展和易测试 1.注入调度器 不要在创建一个协程的时候或者调用withContext,硬编码来指定调度器 比如这样的 class NewsRepository { ...
继续阅读 »

协程最佳实践 android官网地址


这些实践可以让你的程序在使用协程的时候更加的易扩展和易测试


1.注入调度器


不要在创建一个协程的时候或者调用withContext,硬编码来指定调度器 比如这样的


class NewsRepository {
// DO NOT use Dispatchers.Default directly, inject it instead
suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

而应该进行注入


class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

原因:依赖注入的模式可以让你在测试的时候容易更换调度器 详细参考Android中简易的协程


2. 挂起函数在实现的时候,应该保证对主线程是安全的


比如这样的:


suspend fun fetchLatesNews():ListArtical{
withContext(Dispatchers.IO){
}
}

主线程调用的时候


suspend operator fun updateContent(){
val news = fetchLatesNews()
}

这样可以保证你的App是易扩展的,类挂起方法调用的时候,不需要担心线程是在哪个环境调度的,由具体实现类中的方法来确保线程调度的安全


3. viewModle 应该去创建一个协程


viewModle更应该去创建一个协程,而不是去暴露一个suspend方法。 比如应该是这样:


//示例代码,viewModle内去创建一个协程
class LastestNewsViewModel{
//内部维护了一个可观察的带状态的数据
private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
val uiState:StateFlow<LatestNewsUiState> = _uiState

//重点来了,这里不是一个suspend方法
fun loadNews(){
viewModleScope.lanuch{
val lastestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.valule = LastestNewUiState.Success(lastestNewsWithAuthors)
}
}

}


而不是这样的


class LastestNewsViewModel():ViewModel{
//这种是直接返回了一个suspend方法
suspend fun loadNews() = getLatestNewsWithAuthors()
}


除非不需要调用知道数据流的状态,而只需要发射一个单独的数据。(个人理解,是保持viewModle中的定义,维护一个可观察的带状态的数据,而不是直接扔原始数据出来)


4.不要暴露可修改的参数类型


应该对其他类暴露不可修改的的类型,这样所有可变类型数据的变更都集中在一个类里,如果有问题的时候,更容易调试(也是迪米特原则) 比如应该是这样的


class LastestNewsViewModel : ViewModel{
//可修改类型
private val _uiState = _MutalbeStateFlwow(LastestNewsViewModel.Loading)
//对外暴露不可修改类型数据(对外不提供修改功能)
val uiState : StateFlow<LatestNewsUiState> = _uiState
}

5. 数据和业务层应该暴露挂起函数 或 Flow


数据层和业务层通常需要暴露方法,去执行一次性的调用或者需要持续接收数据的变化,这时候应该提供为一次性调用提供挂起函数 或者 提供Flow来帮忙观察数据的变化操作 比如这样的:


class ExampleRepository{
//为一次性的调用提供 suspend方法
suspend fun makeNetworkRequest(){}

//为一需要观察的数据提供Flow对象
fun getExamples():Flow<Example>{}
}

最佳的实践可以使调用者通常是业务层,能够控制业务的执行和生命周期的运转,并且在需要的时候可以取消任务


6. 在业务和数据层创建协程


在数据和业务层需要创建协程的原因可能有不几的原因,下边是一些可能的选项



  • 如果协程的任务是相关的,且只在用户在当前界面时才显示,那么它需要关联调用者的生命周期,这个调用者通常就是ViewModel,在这种 情况下, 应该使用coroutineScope 和 supervisorScope


示例代码:


class GetAllBooksAndAuthorsUseCase(
private val booksRepository:BooksRepository,
private val authorsRepository:AuthorsRepository,
private val defaultDispatcher:CoroutineDispatcher = Dispatchers.Default
){
suspend fun getBookdAndAuthors():BookAndAuthors{
//平行的情况需要等待结果,书籍列表和作者列表需要同时准备好之后再返回
return coroutineScope{
val books = async(defaultDispatcher){
booksRepository.getAllBooks()
}
val authors = async(defaultDispatcher){
authorsRepository.getallAuthors()
}
//准备好数据之后再返回
BookAndAuthors(books.await(),authors.await())
}
}
}


  • 如果这个任务是在App开启期间需要执行,这个任务也不绑定到某一个具体的界面,这时候任务是需要在超出调用者的生命周期的,这种场景下,需要用到


external 的 CoroutineScope ,详细可参考 协程设计模式之任务不应该被取消


参考示例代码:


class ArticalesRepository(
private val articlesDataSource: ArticlesDataSource,
private val externalScope:CoroutineScope,
private val defaultDispatcher:CoroutineDispatcher = Dispatchers.Default
){
//这个场景是这样的,即使我们离开的屏幕,也希望这个预订操作是能够被完整执行的,那么这任务斋要在外部域开启一个新的协程里来完成这wh
suspend fun bookmarkArtical(artical:Article){
externalScope.lanuch(defaultDispatcher){
articlesDataSource.bookmarkArticle(article)
}.join() //等待协程执行完毕
}
}


说明: 外部域需要被一个比当前界面的生命周期更长的一个类来创建,比如说 Application或者是一个navigatin grah的ViewModel


7. 避免使用GlobalScope全局作用域


就像最佳实践里边的注入调度器,如果用了GlobalScope,那就是在类里边使用硬编码,可能会有以下几个负面影响



  • 硬编码。

  • 难以测试


8. 协程需要可以被取消


取消操作也是一种协程的操作,意思是说当协程被取消的时候,协程并没有直接被取消,除非它在 挂起 或者 有取消操作,如果你的协程是在操作一个阻塞的操作,需要确保协程是中途可以被取消的。 举个例子,如果你正在读取多个文件,需要在读取每个文件之前,检查下协程是否已经被取消了,一个检查协程是否被取消的方法就是 调用 ensureActivite方法,(或者还有isActive可用) 参考示例代码:


    someScope.lanuch{
ensureActive()//检查协程是否已经被取消
readFile(file)
}

更多详细的描述信息可以参考 取消协程


9. 协程的异常处理


如果协程抛出的异常处理不当,可能会导致你的App崩溃。如果异常出现了,就在协程里就捕获好异常并进行处理


参考示例代码:


class LoginViewModel(
private val loginRepository:LoginRepository
):ViewModel(){
fun login(username:String,token:String){
viewModleScope.lanuch{
try{
loginRepository.login(username,token)
//通知界面登录成功
}catch(error:Throwable){
//通知view 登录操作失败
}
}
}
}

更多协程异常的处理,或者其他场景需要用到CoroutineExceptionHandler,可以参考 协程异常处理



																			        	收起阅读 »
								        											

影响性能的 Kotlin 代码(一)

Kotlin 高级函数的特性不仅让代码可读性更强,更加简洁,而且还提高了生产效率,但是简洁的背后是有代价的,隐藏着不能被忽视的成本,特别是在低端机上,这种成本会被放大,因此我们需要去研究 kotlin 语法糖背后的魔法,选择合适的语法糖,尽量避免这些坑。 L...
继续阅读 »

Kotlin 高级函数的特性不仅让代码可读性更强,更加简洁,而且还提高了生产效率,但是简洁的背后是有代价的,隐藏着不能被忽视的成本,特别是在低端机上,这种成本会被放大,因此我们需要去研究 kotlin 语法糖背后的魔法,选择合适的语法糖,尽量避免这些坑。


Lambda 表达式


Lambda 表达式语法简洁,避免了冗长的函数声明,代码如下。


fun requestData(type: Int, call: (code: Int, type: Int) -> Unit) {
call(200, type)
}

Lambda 表达式语法虽然简洁,但是隐藏着两个性能问题。



  • 每次调用 Lambda 表达式,都会创建一个对象



图中标记 1 所示的地方,涉及一个字节码类型的知识点。



















标识符 含义
I 基本类型 int
L 对象类型,以分号结尾,如 Lkotlin/jvm/functions/Function2;

Lambda 表达式 call: (code: Int, type: Int) -> Unit 作为函数参数,传递到函数中,Lambda 表达式会继承 kotlin/jvm/functions/Function2 , 每次调用都会创建一个 Function2 对象,如图中标记 2 所示的地方。



  • Lambda 表达式隐含自动装箱和拆箱过程



正如你所见 lambda 表达式存在装箱和拆箱的开销,会将 int 转成 Integer,之后进行一系列操作,最后会将 Integer 转成 int


如果想要避免 Lambda 表达式函数对象的创建及装箱拆箱开销,可以使用 inline 内联函数,直接执行 lambda 表达式函数体。


Inline 修饰符


Inline 内联函数的作用:提升运行效率,调用被 inline 修饰符标记的函数,会把函数内的代码放到调用的地方。


如果阅读过 Koin 源码的朋友,应该会发现 inline 都是和 lambda 表达式和 reified 修饰符配套在一起使用的,如果只使用 inline 修饰符标记普通函数,Android Studio 也会给一个大大大的警告。



编译器建议我们在含有 lambda 表达式作为形参的函数中使用内联,既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告? 这是为了防止 inline 操作符滥用而带来的性能损失。


inline 修饰符适用于以下情况



  • inline 修饰符适用于把函数作为另一个函数的参数,例如高阶函数? filter、map、joinToString 或者一些独立的函数 repeat

  • inline 操作符适合和 reified 操作符结合在一起使用

  • 如果函数体很短,使用 inline 操作符可以提高效率


Kotlin 遍历数组


这一小节主要介绍 Kotlin 数组,一起来看一下遍历数组都有几种方式。



  • 通过 forEach 遍历数组

  • 通过区间表达式遍历数组(..downTountil)

  • 通过 indices 遍历数组

  • 通过 withIndex 遍历数组


通过 forEach 遍历数组


先来看看通过 forEach 遍历数组,和其他的遍历数组的方式,有什么不同。


array.forEach { value ->

}

反编译后:

Integer[] var5 = array;
int var6 = array.length;
for(int var7 = 0; var7 < var6; ++var7) {
Object element$iv = var5[var7];
int value = ((Number)element$iv).intValue();
boolean var10 = false;
}

正如你所见通过 forEach 遍历数组的方式,会创建额外的对象,并且存在装箱/拆箱开销,会占用更多的内存。


通过区间表达式遍历数组


在 Kotlin 中区间表达式有三种 ..downTountil



  • .. 关键字,表示左闭右闭区间

  • downTo 关键字,实现降序循环

  • until 关键字,表示左闭右开区间


.. 、downTo 、until


for (value in 0..size - 1) {
// case 1
}

for (value in size downTo 0) {
// case 2
}

for (value in 0 until size) {
// case 3
}

反编译后

// case 1
if (value <= var4) {
while(value != var4) {
++value;
}
}

// case 2
for(boolean var5 = false; value >= 0; --value) {
}

// case 3
for(var4 = size; value < var4; ++value) {
}

如上所示 区间表达式 ( ..downTountil) 除了创建一些临时变量之外,不会创建额外的对象,但是区间表达式 和 step 关键字结合起来一起使用,就会存在内存问题。


区间表达式 和 step 关键字


step 操作的 ..downTountil, 编译之后如下所示。


for (value in 0..size - 1 step 2) {
// case 1
}

for (value in 0 downTo size step 2) {
// case 2
}

反编译后:

// case 1
var10000 = RangesKt.step((IntProgression)(new IntRange(var6, size - 1)), 2);
while(value != var4) {
value += var5;
}

// case 2
var10000 = RangesKt.step(RangesKt.downTo(0, size), 2);
while(value != var4) {
value += var5;
}

step 操作的 ..downTountil 除了创建一些临时变量之外,还会创建 IntRangeIntProgression 对象,会占用更多的内存。


通过 indices 遍历数组


indices 通过索引的方式遍历数组,每次遍历的时候通过索引获取数组里面的元素,如下所示。


for (index in array.indices) {
}

反编译后:

for(int var4 = array.length; var3 < var4; ++var3) {
}

通过 indices 遍历数组, 编译之后的代码 ,除了创建了一些临时变量,并没有创建额外的对象。


通过 withIndex 遍历数组


withIndexindices 遍历数组的方式相似,通过 withIndex 遍历数组,不仅可以获取的数组索引,同时还可以获取到每一个元素。


for ((index, value) in array.withIndex()) {

}

反编译后:

Integer[] var5 = array;
int var6 = array.length;
for(int var3 = 0; var3 < var6; ++var3) {
int value = var5[var3];
}

正如你所看到的,通过 withIndex 方式遍历数组,虽然不会创建额外的对象,但是存在装箱/拆箱的开销


总结:



  • 通过 forEach 遍历数组的方式,会创建额外的对象,占用内存,并且存在装箱 / 拆箱开销

  • 通过 indices 和区间表达式 ( ..downTountil) 都不会创建额外的对象

  • 区间表达式 和 step 关键字结合一起使用, 会有创建额外的对象的开销,占用更多的内存

  • 通过 withIndex 方式遍历数组,不会创建额外的对象,但是存在装箱/拆箱的开销


尽量少使用 toLowerCase 和 toUpperCase 方法



这一小节内容,在我之前的文章中分享过,但是这也是很多小伙伴,遇到最多的问题,所以单独拿出来在分析一次



当我们比较两个字符串,需要忽略大小写的时候,通常的写法是调用 toLowerCase() 方法或者 toUpperCase() 方法转换成大写或者小写,然后在进行比较,但是这样的话有一个不好的地方,每次调用 toLowerCase() 方法或者 toUpperCase() 方法会创建一个新的字符串,然后在进行比较。


调用 toLowerCase() 方法


fun main(args: Array<String>) {
// use toLowerCase()
val oldName = "Hi dHL"
val newName = "hi Dhl"
val result = oldName.toLowerCase() == newName.toLowerCase()

// or use toUpperCase()
// val result = oldName.toUpperCase() == newName.toUpperCase()
}

toLowerCase() 编译之后的 Java 代码



如上图所示首先会生成一个新的字符串,然后在进行字符串比较,那么 toUpperCase() 方法也是一样的如下图所示。


toUpperCase() 编译之后的 Java 代码



这里有一个更好的解决方案,使用 equals 方法来比较两个字符串,添加可选参数 ignoreCase 来忽略大小写,这样就不需要分配任何新的字符串来进行比较了。


fun main(args: Array<String>) {
val oldName = "hi DHL"
val newName = "hi dhl"
val result = oldName.equals(newName, ignoreCase = true)
}

equals 编译之后的 Java 代码



使用 equals 方法并没有创建额外的对象,如果遇到需要比较字符串的时候,可以使用这种方法,减少额外的对象创建。


by lazy


by lazy 作用是懒加载,保证首次访问的时候才初始化 lambda 表达式中的代码, by lazy 有三种模式。



  • LazyThreadSafetyMode.NONE 仅仅在单线程

  • LazyThreadSafetyMode.SYNCHRONIZED 在多线程中使用

  • LazyThreadSafetyMode.PUBLICATION 不常用


LazyThreadSafetyMode.SYNCHRONIZED 是默认的模式,多线程中使用,可以保证线程安全,但是会有 double check + lock 性能开销,代码如下图所示。



如果是在主线程中使用,和初始化相关的逻辑,建议使用 LazyThreadSafetyMode.NONE 模式,减少不必要的开销。


仓库 KtKit 是用 Kotlin 语言编写的小巧而实用的工具库,包含了项目中常用的一系列工具, 正在逐渐完善中,如果你有兴趣,想邀请你和我一起来完善这个库。


收起阅读 »

从android系统源码看java层的so加载。

理论基础我们在android开发项目过程中都必然会更so加载打交道,那么so加载在系统中的顺序和流程是怎样的,我们就有必要对这个加载过程进行熟悉了解掌握。so的加载是一种解析式装载,这与dex有一定区别,dex是先加载进行优化验证生成odex,再去解析odex...
继续阅读 »

理论基础

我们在android开发项目过程中都必然会更so加载打交道,那么so加载在系统中的顺序和流程是怎样的,我们就有必要对这个加载过程进行熟悉了解掌握。
so的加载是一种解析式装载,这与dex有一定区别,dex是先加载进行优化验证生成odex,再去解析odex文件,而so更像边解析边装载,在加载过程中主要解析是load段。
下面主要是以java层的so加载进行从源码上进行解析加载流程。

java层的so加载流程分析

System.loadLibrary入口点

在java层我们知道加载so文件是通过System.loadLibrary函数其实现的,下面就以其作为入口点进行分析它的调用关系和实现。 System.loadLibrary在的函数定义系统source\libcore\luni\src\main\java\java\lang\system.java的文件中。

下面是其函数定义实现。

//参数就是要加载的so文件名称
public static void loadLibrary(String libName) {
//通过调用Runtime下面的loadLibrary函数实现
//函数有两个参数,参数1是加载的so文件名,参数2 类加载器。
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

Runtime的loadLibray

通过上面的System.java的loadLibrary函数我们需要继续分析Runtime.java文件中的loadLibray函数的定义实现。 Runtime的loadLibrary函数在android系统中的位置是 source\libcore\luni\src\main\java\java\lang\Runtime.java文件。

下面是Runtime的 loadLibrary函数的定义实现源码。

    /*
* Searches for and loads the given shared library using the given ClassLoader.
*/

void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
//通过加载器去查找要加载的so文件名
String filename = loader.findLibrary(libraryName);
//查找失败
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//加载so文件名
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}

String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
//循环遍历文件路径
for (String directory : mLibPaths) {
//文件路径和文件名进行拼接
String candidate = directory + filename;
candidates.add(candidate);

if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}

if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

Runtime的doLoad

通过上面的Runtime的loadLibrary函数,我们看到加载so的函数是走到doLoad函数,那么我们就需要继续分析Runtime下的doload函数的定义实现。 Rutime下的doload函数在系统中的 source\libcore\luni\src\main\java\java\lang\Runtime.java文件中。

下面的代码是Runtime的doload函数的定义实现。

 private String doLoad(String name, ClassLoader loader) {
// Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
// which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.

// The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
// libraries with no dependencies just fine, but an app that has multiple libraries that
// depend on each other needed to load them in most-dependent-first order.

// We added API to Android's dynamic linker so we can update the library path used for
// the currently-running process. We pull the desired path out of the ClassLoader here
// and pass it to nativeLoad so that it can call the private dynamic linker API.

// We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
// beginning because multiple apks can run in the same process and third party code can
// use its own BaseDexClassLoader.

// We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
// dlopen(3) calls made from a .so's JNI_OnLoad to work too.

// So, find out what the native library search path is for the ClassLoader in question...
String ldLibraryPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();
}
// nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
// of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}

总结

从以上的源码实现流程分析,我们可以看出Android在java层加载so的接口是System.loadLibrary(),通过层层递进关系从而实现java层的加载so。

下图是详细的java层加载so函数的调用关系。

收起阅读 »

View的绘制流程 onLayout

onLayout的原理 final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); boolean triggerGlo...
继续阅读 »

onLayout的原理

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);

if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
host.getLocationInWindow(mTmpLocation);
mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
mTmpLocation[0] + host.mRight - host.mLeft,
mTmpLocation[1] + host.mBottom - host.mTop);

host.gatherTransparentRegion(mTransparentRegion);
if (mTranslator != null) {
mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
}

if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
mPreviousTransparentRegion.set(mTransparentRegion);
mFullRedrawNeeded = true;
try {
mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
} catch (RemoteException e) {
}
}
}

}

if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
//分发内部的insets,这里我们暂时不去关心省略的逻辑
...
}

接下来performTraversals后续事情分为如下几个方面:

  • 1.就是判断当前的View是否需要重新摆放位置。如果通过requestLayout执行performTraversals方法,则layoutRequested为true;此时需要调用performLayout进行重新的摆放。
  • 2.判断到调用了requestTransparentRegion方法,需要重新计算透明区域,则会调用gatherTransparentRegion方法重新计算透明区域。如果发现当前的和之前的透明区域发生了变化,则通过WindowSession更新WMS那边的区域。

这种情况通常是指存在SurfaceView的情况。因为SurfaceView本身就拥有自己的一套体系沟通到SF体系中进行渲染。Android没有必要把SurfaceView纳入到层级中处理,需要把这部分当作透明,当作不必要的层级进行优化。

整个核心我还是回头关注performLayout究竟做了什么?

ViewRootImpl performLayout

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;

final View host = mView;
if (host == null) {
return;
}


try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {

ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {

mHandlingLayoutInLayoutRequest = true;

int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

mHandlingLayoutInLayoutRequest = false;

}

}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}

其实整个核心还是这一段代码:

            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

这一段代码将会开启遍历View树的layout的流程,也就是View的摆放的流程。

当处理完layout流程之后,就会继续检查是否有View在测量,摆放的流程请求
中是否有别的View请求进行刷新,如果请求则把这个View保存在mLayoutRequesters对象中。此时取出重新进行测量和摆放。

记住此时的根布局是DecorView是layout方法.由于DecorView和FrameLayout都有重写layout,我们来看看ViewGroup的layout.

ViewGroup layout

    public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}

能看到,layout走到View的layout方法的条件有2:

  • 1.mSuppressLayout为false,也就是不设置抑制Layout方法
  • 2.mTransition LayoutTransition 布局动画为空或者没有改变才可以。

在Android中动画api中提供了LayoutTransition,用于对子View的加入和移除添加自定义的属性动画。

记住此时从DecorView传进来的layout四个参数,分别代表该View可以摆放的左部,顶部,右部,底部四个位置。但是不代表该View就是摆放到这个位置。

View layout

    public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b)

final boolean wasLayoutValid = isLayoutValid();

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {

clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {

clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}

} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}

if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}

在这里可以分为如下几个步骤:

  • 1.判断PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT是否开启了。这个标志位打开的时机是在onMeasure步骤发现原来父容器传递下来的大小不变,就会设置老的测量结果在View中。在layout的步骤会先调用一次onMeasure继续遍历测量底层的子View的大小。
  • 2.判断isLayoutModeOptical是否开启了光学边缘模式。打开了则setOpticalFrame进行四个方向的边缘设置,否则则setFrame处理。用于判断是否需要更新四个方向的数值。
  • 3.如果发生了大小或者摆放的位置变化,则进行onLayout的回调。一般子类都会重写这个方法,进行进一步的摆放设置。
  • 4.如果需要显示滑动块,则初始化RoundScrollbarRenderer对象。这个对象实际上就一个封装好如何绘制绘制一个滑动块的自定义View。
  • 5.回调已经进行了Layout变化监听的OnLayoutChangeListener回调。
  • 6.当前View的layout的行为进行的同时没有另一个layout进行,说明当前的Layout行为是有效的。如果layout的行为是无效的,此时的View又获取了焦点则清除。如果此时是想要请求焦点,则清空焦点。
  • 7.通知AllFillManager进行相关的处理。

这里我们着重看看setFrame方法做了什么。

setFrame

    protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;

if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

invalidate(sizeChanged);

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}

if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {

mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);

invalidateParentCaches();
}

mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}

notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}

就以setFrame为例子来看看其核心思想。实际上很简单:

  • 1.比较左上右下四个方向的数值是否发生了变化。如果发生了变化,则更新四个方向的大小,并判断整个需要绘制的区域是否发生了变化,把sizechange作为参数调用invalidate进行onDraw的刷新。
  • 2.获取mRenderNode这个硬件渲染的对象,并且设置这个渲染点的位置。
  • 3.调用sizeChange方法进行onSizeChange的回调:
    private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
if (mOverlay != null) {
mOverlay.getOverlayView().setRight(newWidth);
mOverlay.getOverlayView().setBottom(newHeight);
}

if (!sCanFocusZeroSized && isLayoutValid()
// Don't touch focus if animating
&& !(mParent instanceof ViewGroup && ((ViewGroup) mParent).isLayoutSuppressed())) {
if (newWidth <= 0 || newHeight <= 0) {
if (hasFocus()) {
clearFocus();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).clearFocusedInCluster();
}
}
clearAccessibilityFocus();
} else if (oldWidth <= 0 || oldHeight <= 0) {
if (mParent != null && canTakeFocus()) {
mParent.focusableViewAvailable(this);
}
}
}
rebuildOutline();
}

如果当前不是ViewGroup且新的宽高小于0焦点则清除焦点,并且通知AccessibilityService。如果新的宽高大于0,则通知父容器焦点可集中。最后重新构建外框。

  • 4.如果判断到mGhostView不为空,且当前的View可见。则对mGhostView发出draw的刷新命令。并通知父容器也刷新。这里mGhostView实际上是一层覆盖层,作用和ViewOverLay相似。

到这里View和ViewGroup的onLayout似乎就看完了。但是还没有完。记得我们现在分析的是DecorView。因此我们看看DecorView在onLayout中做了什么。

DecorView onLayout

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
getOutsets(mOutsets);
if (mOutsets.left > 0) {
offsetLeftAndRight(-mOutsets.left);
}
if (mOutsets.top > 0) {
offsetTopAndBottom(-mOutsets.top);
}
if (mApplyFloatingVerticalInsets) {
offsetTopAndBottom(mFloatingInsets.top);
}
if (mApplyFloatingHorizontalInsets) {
offsetLeftAndRight(mFloatingInsets.left);
}

updateElevation();
mAllowUpdateElevation = true;

if (changed && mResizeMode == RESIZE_MODE_DOCKED_DIVIDER) {
getViewRootImpl().requestInvalidateRootRenderNode();
}
}
  • 1.先调用了FrameLayout的onLayout的方法后,确定每一个子View的摆放位置。

  • 2.getOutsets方法获取mAttachInfo中的mOutSet区域。从上一篇文章的打印看来,mOutsets的区域实际上就是指当前屏幕最外层的四个padding大小。如果左右都大于0,则调用offsetLeftAndRight和offsetTopAndBottom进行设置。

  • 3.如果mApplyFloatingVerticalInsets或者mApplyFloatingHorizontalInsets为true,说明DecorView自己需要处理一次onApplyWindowInsets的回调。如果关闭FLAG_LAYOUT_IN_SCREEN标志位也就是非全屏模式,且宽或高是WRAP_CONTENT模式,则给横轴或者纵轴两端增加systemwindowInset的padding数值。

    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
final WindowManager.LayoutParams attrs = mWindow.getAttributes();
mFloatingInsets.setEmpty();
if ((attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0) {

if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
mFloatingInsets.top = insets.getSystemWindowInsetTop();
mFloatingInsets.bottom = insets.getSystemWindowInsetBottom();
insets = insets.inset(0, insets.getSystemWindowInsetTop(),
0, insets.getSystemWindowInsetBottom());
}
if (mWindow.getAttributes().width == WindowManager.LayoutParams.WRAP_CONTENT) {
mFloatingInsets.left = insets.getSystemWindowInsetTop();
mFloatingInsets.right = insets.getSystemWindowInsetBottom();
insets = insets.inset(insets.getSystemWindowInsetLeft(), 0,
insets.getSystemWindowInsetRight(), 0);
}
}
mFrameOffsets.set(insets.getSystemWindowInsets());
insets = updateColorViews(insets, true /* animate */);
insets = updateStatusGuard(insets);
if (getForeground() != null) {
drawableChanged();
}
return insets;
}
  • 4.当所有都Layout好之后,则调用updateElevation更新窗体的阴影面积。
    // The height of a window which has focus in DIP.
private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
// The height of a window which has not in DIP.
private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;

private void updateElevation() {
float elevation = 0;
final boolean wasAdjustedForStack = mElevationAdjustedForStack;
final int windowingMode =
getResources().getConfiguration().windowConfiguration.getWindowingMode();
if ((windowingMode == WINDOWING_MODE_FREEFORM) && !isResizing()) {
elevation = hasWindowFocus() ?
DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP : DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP;

if (!mAllowUpdateElevation) {
elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
}
elevation = dipToPx(elevation);
mElevationAdjustedForStack = true;
} else if (windowingMode == WINDOWING_MODE_PINNED) {
elevation = dipToPx(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP);
mElevationAdjustedForStack = true;
} else {
mElevationAdjustedForStack = false;
}

if ((wasAdjustedForStack || mElevationAdjustedForStack)
&& getElevation() != elevation) {
mWindow.setElevation(elevation);
}
}

能看到这个过程中,如果窗体模式是freedom模式(也就是更像电脑中可以拖动的窗体一样)且不是正在拖拽变化大小,则会根据是否窗体聚焦了来决定阴影的的四个方向的大小。注意如果是没有焦点则为5,有焦点则为20.这里面的距离并非是measure的时候增加当前测量的大小,而是在测量好的大小中继续占用内容空间,也就是相当于设置了padding数值。

如果窗体是WINDOWING_MODE_PINNED模式或者WINDOWING_MODE_FREEFORM模式,且elevation发生了变化则通过PhoneWindow.setElevation设置Surface的Insets数值。

  • 5.最后调用requestInvalidateRootRenderNode,通知ViweRootImpl中的硬件渲染对象ThreadRenderer进行刷新绘制。

整个过程中,有一系列函数用于更新摆放的偏移量,就以offsetLeftAndRight比较重要,看看是如何计算的。

offsetLeftAndRight

    public void offsetLeftAndRight(int offset) {
if (offset != 0) {
final boolean matrixIsIdentity = hasIdentityMatrix();
if (matrixIsIdentity) {
if (isHardwareAccelerated()) {

if (!matrixIsIdentity) {
invalidateViewProperty(false, true);
}
invalidateParentIfNeeded();
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}

这个过程会判断offset如果不等于0才会进行计算。如果从RenderNode判断到存在单位变化矩阵(关于这个矩阵我们暂时不去聊,涉及到了硬件渲染的机制)。

  • 1.判断到如果有硬件加速,则直接调用invalidateViewProperty方法刷新。
  • 2.没有硬件加速,软件渲染的逻辑本质上也是一样的。

在这里能看到如果offset小于0,则把minLeft的大小增加。如果offset大于0,则增加maxRight的数值。计算出刷新的区域:

offset>0 maxRight = maxRight + mRight
offset<0 minLeft = minLeft + mLeft
刷新横向范围:maxRight - minLeft

计算出需要刷新的区域通过获取父布局的invalidateChild发送刷新命令。换算成图就是如下原理:

image.png

  • 3.如果没有单位变换矩阵,则调用invalidateViewProperty发送刷新命令
  • 4.最后mLeft和mRight增加offset。并把left和right同步数据到mRenderNode中。
  • 5.如果打开了硬件加速,又一次调用了invalidateViewProperty,并且调用invalidateParentIfNeededAndWasQuickRejected拒绝遍历刷新。
  • 6.关闭硬件加速,如果没有单位变换矩阵,则调用invalidateViewProperty。接着调用invalidateParentIfNeeded。

能看到这个过程中有几个方法被频繁的调用:

  • 1.invalidate
  • 2.invalidateChild
  • 3.invalidateViewProperty
  • 4.invalidateParentIfNeededAndWasQuickRejected
  • 5.invalidateParentIfNeeded

这几个方法决定了绘制需要更新的区域。

FrameLayout onLayout

 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();

final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();

final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();

int childLeft;
int childTop;

int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}

final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}

switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}

child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}

FrameLayout的onLayout方法很简单。实际上就是遍历每一个可见的子View处理其gravity。
可以分为横轴和竖轴两个方向进行处理:
横轴的处理方向:

  • 1.是判断到gravity是CENTER_HORIZONTAL,说明要横向居中:

每一个孩子左侧 = 父View的左侧位置 - (父亲的宽度 - 孩子的宽度)/ 2 + 孩子的marginLeft - 孩子的marginRight

保证孩子位置的居中。

  • 2.Gravity.RIGHT:

孩子的左侧= 父View的右侧 - 孩子宽度 - 孩子的marginRight

保证了孩子是从右边还是摆放位置。

  • 3.Gravity.LEFT

孩子的左侧= 父View的左侧 + 孩子的marginLeft

竖直方向上同理。

最后把每一个摆放好的孩子位置通过child.layout进行迭代执行子View的layout流程。


收起阅读 »

Java “优雅”地中断线程(原理篇)

前言之前有分析过如何优雅地中断线程,秉着"既要知其然,也要知其所以然"精神,本篇将从底层源码分析中断是如何工作的。 通过本篇文章,你将了解到:1、线程底层源码入口2、中断的作用3、Thread.sleep/Object.join/Object.wait 对中断...
继续阅读 »

前言

之前有分析过如何优雅地中断线程,秉着"既要知其然,也要知其所以然"精神,本篇将从底层源码分析中断是如何工作的。 通过本篇文章,你将了解到:

1、线程底层源码入口
2、中断的作用
3、Thread.sleep/Object.join/Object.wait 对中断的处理
4、究竟该如何停止线程
5、疑难点分析

1、线程底层源码入口

Java 层的Thread

以Thread.java里的方法Thread.interrupt()为例,最终调用了interrupt0()方法:

    private native void interrupt0();

可以看出,是native方法,接下来看看怎么找到其JNI实现。

JNI 入口

在Thread.c里定义了JNI 方法: image.png interrupt0()方法对应JVM_Interrupt()函数。 在jvm.cpp里:

#jvm.cpp
JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_Interrupt");

oop java_thread = JNIHandles::resolve_non_null(jthread);
MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
if (thr != NULL) {
//调用Thread.cpp里的函数
Thread::interrupt(thr);
}
JVM_END

继续跟进Thread.cpp:

#Thread.cpp
void Thread::interrupt(Thread* thread) {
//调用os里的函数
os::interrupt(thread);
}

最终调用了os里的函数。

2、中断的作用

中断源码分析

入口找到了,接着继续深入分析。上面分析到了os::interrupt(thread),os是区分系统的,此处以Linux系统为例:

#os_linux.cpp
void os::interrupt(Thread* thread) {
...
OSThread* osthread = thread->osthread();

//中断标记位没有设置
if (!osthread->interrupted()) {
//则设置中断标记位为true
osthread->set_interrupted(true);
OrderAccess::fence();
ParkEvent * const slp = thread->_SleepEvent ;
//唤醒线程,对应sleep挂起
if (slp != NULL) slp->unpark() ;
}

//唤醒线程,对应wait/join 操作挂起等
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();

//唤醒线程,对应synchronized 获取锁挂起
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}

显然,在Java 层调用Thread.interrupt()方法,最终底层完成了两件事:

1、将中断标记位设置为true。
2、将挂起的线程唤醒。

image.png

中断状态查询

Java 层的Thread.java里提供了两个方法来查询中断标记位的值,分别是:

#Thread.java
//成员方法
public boolean isInterrupted() {
return isInterrupted(false);
}

//静态方法
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}

无论是成员方法还是静态方法,最终都调用了Thread.isInterrupted(xx)方法:

#Thread.java
//ClearInterrupted 表示是否清空中断标记位
private native boolean isInterrupted(boolean ClearInterrupted);

可以看出:

1、成员方法isInterrupted()没有清空中断标记位。
2、静态方法interrupted()清空了中断标记位。

继续跟进isInterrupted(xx)方法,由上面跟踪的入口经验最终有如下代码:

#Thread.cpp
bool Thread::is_interrupted(Thread* thread, bool clear_interrupted) {
return os::is_interrupted(thread, clear_interrupted);
}

#os_linux.cpp
bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
OSThread* osthread = thread->osthread();
//查询当前中断值
bool interrupted = osthread->interrupted();

if (interrupted && clear_interrupted) {
//如果参数clear_interrupted 为true,表示要清空标记位
//则设置标记位为false
osthread->set_interrupted(false);
}

//返回查询到的中断标记位的值
return interrupted;
}

因此,Thread.isInterrupted(xx)方法的作用是:

1、查询当前线程的中断标记位的值。
2、根据参数决定是否重置中断标记。

而Thread.isInterrupted(xx) 是私有方法,因此Thread.java 对外提供了两种方法:Thread.isInterrupted()(成员方法)和Thread.interrupted(静态方法)。

3、Thread.sleep/Object.join/Object.wait 对中断的处理

中断线程Demo

既然知道了中断的作用,接着来看看如何使用Thread.interrupt()来中断线程,先来看看个Demo。

public class TestThread {
public static void main(String args[]) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("t1 is alive " + System.currentTimeMillis());
}
}
});

t1.start();

try {
//保证t1运行一会
Thread.sleep(2000);
//中断t1
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

while (true) {
System.out.println("t1 interrupt status:" + t1.isInterrupted() + " t1 isAlive:" + t1.isAlive() + " " + System.currentTimeMillis());
}
}
}

该Demo的目的是:

1、先开启线程t1,让t1不断循环打印时间。
2、在另一个线程(主线程)中断t1,并不断查询t1中断标记位的值。

如果没有分析上面的中断原理,你可能会认为t1应该被中断退出了循环。实际上从打印结果可知,t1的中断标记位被设置为true,然而t1并没有退出循环。这结果符合我们的原理分析,因为中断线程的操作底层只做了两件事:设置中断标记位和唤醒线程。 在上面的例子里,t1并没有被挂起,因此唤醒线程没啥实际意义。 总而言之:

"中断线程"这个词听起来比较霸气,让人误以为就是让一个线程停止运行。实际上线程的停止与否并不是它控制的,而是线程执行过程中主动退出或是有异常抛出。

将上面的Demo稍加修改如下:

public class TestThread {
public static void main(String args[]) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
System.out.println("t1 is alive " + System.currentTimeMillis());
//睡眠一会再执行
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("t1 catch exception:" + e.getMessage());
}
}
});

t1.start();

try {
//保证t1运行一会
Thread.sleep(2000);
//中断t1
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

while (true) {
System.out.println("t1 interrupt status:" + t1.isInterrupted() + " t1 isAlive:" + t1.isAlive() + " " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

相比上一个Demo,仅仅是在t1里增加1s的睡眠时间,打印结果如下: image.png 可以看出,t1被中断后因为抛出了异常而退出了循环,其中断标记位为false,线程已经停止了运行。

Thread.sleep 源码解析

通过比对上面两个Demo可知,引起结果不同是因为增加了Thread.sleep(xx)方法,因此来看看它内部到底做了什么。

#jvm.cpp
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
JVMWrapper("JVM_Sleep");
//在睡眠之前先检查是否已经发生了中断,若是则抛出中断异常
if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
}
...

if (millis == 0) {
...
} else {
ThreadState old_state = thread->osthread()->get_state();
thread->osthread()->set_state(SLEEPING);
if (os::sleep(thread, millis, true) == OS_INTRPT) {
//睡眠结束后,判断是否已经发生了中断,若是则抛出中断异常
THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
}
}
thread->osthread()->set_state(old_state);
}
...
JVM_END

#os_linux.cpp
int os::sleep(Thread* thread, jlong millis, bool interruptible) {
assert(thread == Thread::current(), "thread consistency check");

//_SleepEvent 是ParkEvent类型
ParkEvent * const slp = thread->_SleepEvent ;
...
//interruptible 表示是否支持中断,默认支持
if (interruptible) {
...
for (;;) {
//此处是死循环,退出依靠return或者break
if (os::is_interrupted(thread, true)) {
//再次判断是否已经发生中断,若是则返回OS_INTRPT
//该值在外层判断
return OS_INTRPT;
}

//时间耗尽,则退出
if(millis <= 0) {
return OS_OK;
}
...
{
//挂起线程
slp->park(millis);
//线程被唤醒后继续循环
}
}
} else {
for (;;) {
...
//时间耗尽,则退出
if(millis <= 0) break ;

prevtime = newtime;
slp->park(millis);
}
return OS_OK ;
}
}

可以看出,Thread.sleep(xx)方法作用如下:

1、线程挂起一定的时间,时间到达后继续执行循环。
2、interruptible==true场景下,在循环里判断是否发生了中断,若是,则抛出中断异常。

再来分析上面的Demo:

1、线程调用Thread.sleep(xx)进行睡眠,此时线程被挂起。
2、外部调用Thread.interrupt()中断该线程,此时中断标记位值为true。
3、线程被唤醒,唤醒后判断是否发生了中断(通过中断标记位),若是则抛出异常。

虽然Thread.interrupt()没有直接停止线程,但是可以利用中断标记位来查看是否发生过中断的动作,根据这个动作来决定是否停止线程的执行。 而对于Thread.sleep(xx)来说,作为一个公共方法,当检测到中断时,抛出中断异常让外部处理。

以上分析了Thread.sleep(xx)原理,还有个小细节: 当外界调用Thread.interrupt()并捕获了中断异常的时候,此时线程的中断标记位的值位false,这个在哪修改的呢? 判断中断标记时使用了如下代码:

os::is_interrupted(thread, true)

第二个参数表示重置当前中断标记位的值,该函数上面已经分析过。

调用Object.join/Object.wait 挂起线程,此时线程若被中断,表现与Thread.sleep差不多,此处就不再展开分析了。

4、究竟该如何停止线程

还是来看Demo:

public class TestThread {
static volatile boolean isCancelThread = false;
public static void main(String args[]) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(!isCancelThread || !Thread.currentThread().isInterrupted()) {
try {
doSomething1();
doSomething2();
} catch (Exception e) {
if (e instanceof InterruptedException) {
isCancelThread = true;
}
}
}
}
});

t1.start();

//另一个线程两种方式停止线程
//1、设置isCancelThread = true
//2、调用t1.interrupt()
}

private static void doSomething1() {
//具体逻辑
}

private static void doSomething2() {
//具体逻辑
}
}

来分析Demo逻辑: doSomething1() 、doSomething2() 可能是自己写的方法,也可能是其它人提供的方法,它们内部可能会使得线程阻塞。

第一: 外部通过设置isCancelThread 来标记是否让线程停止运行。这种写法有个缺陷,若是线程在调用doSomething1() 或doSomething2()时阻塞,那么将不会立即检测isCancelThread的值,也即是不能立即停止线程。

第二 针对doSomething1() 或doSomething2()可能阻塞的问题,外部通过使用 Thread.interrupt()中断线程,此时需要捕获异常,捕获到了中断异常意味着可以停止线程运行了。 当然,如果你不想额外使用isCancelThread标记,可以直接判断中断标记位:Thread.currentThread().isInterrupted(),此时Demo再改改:

    while(!Thread.currentThread().isInterrupted()) {
try {
doSomething1();
doSomething2();
} catch (Exception e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}

为什么要额外在catch里增加中断动作呢?原因是中断时可能会遇到sleep/wait/join 等方法将中断标记位的值置为false,此处再次中断是为了让while感知到中断已经发生过了,从而退出循环。

5、疑难点分析

网上有些文章将线程能不能中断总结为:

1、若是线程处在RUNNABLE状态,则interrupt不能中断线程。 2、若是线程处在WAITING/WAITING 则interrupt能中断线程。

通过上面的分析,相信你已经知道了这种说法并不严谨。 来看个Demo,Thread处在RUNNABLE时调用interrupt()中断:

public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
doSomething();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();


Thread.sleep(2000);

t1.interrupt();
System.out.println("main thread interrupt Thread t1");

}

private static void doSomething() throws InterruptedException{
while(true) {
System.out.println("thread state:" + Thread.currentThread().getState() + " " + System.currentTimeMillis());
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
}
}
}

显然Thread1 一直处在RUNNABLE 状态,但是调用interrupt()后Thread1停止了。 再来看来看个Demo,Thread处在WAITING时调用interrupt()中断:

public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
doSomething();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();

long count = 100000;
while(count >= 0) {
System.out.println("thread state:" + t1.getState() + " " + System.currentTimeMillis());
count--;
}

t1.interrupt();
System.out.println("main thread interrupt Thread t1");
while (true) {
Thread.sleep(1000);
System.out.println("after interrupt thread state:" + t1.getState() + " " + System.currentTimeMillis());
}

}

private static void doSomething() throws InterruptedException{
while(true) {
LockSupport.park();
}
}
}

显然Thread1 一直处在WAITING 状态,但是调用interrupt()后Thread1却没停止。 问题的根源是:

线程停止与否要看线程执行体里的代码(方法/代码块)有没有检测中断,并且检测之后是否有处理中断(抛出异常/return 出正常流程)。

因此更严谨的说法是:线程是否能被中断取决于它是否检测并处理了中断状态。 这在AQS里实现可中断锁/不可中断锁时会充分体现。

本文基于JDK 1.8,运行环境也是jdk1.8。

收起阅读 »

讲讲ViewGroup的setPersistentDrawingCache方法

这是一篇采坑文章,灵感来源于博主某篇文章中与大佬的聊天前言记得在三年前,还在上一个学校,学生时代的时候,接过一个外包的Android单,里面有个需求是在一个Activity中根据用户的点击反复的执行两个动画以达到比较好的交互效果。当时在网上没有找到类似的效果,...
继续阅读 »

这是一篇采坑文章,灵感来源于博主某篇文章中与大佬的聊天


前言

记得在三年前,还在上一个学校,学生时代的时候,接过一个外包的Android单,里面有个需求是在一个Activity中根据用户的点击反复的执行两个动画以达到比较好的交互效果。当时在网上没有找到类似的效果,最后还是在Android官方Demo里找到的。

而本人对其案例中setPersistentDrawingCache方法的理解一直是在对其进行优化。

mContainer.setPersistentDrawingCache(ViewGroup.PERSISTENT_ANIMATION_CACHE);

并且在后续的自己开发APP中,甚至是拿了软著权准备上架的那几款APP相同情景下一直使用此方法。今天和大佬在另一篇文章中聊到性能消耗,于是测试了下setPersistentDrawingCache加与不加的性能消耗,结果发现貌似不加效果还好一点。


一、看看Demo中给出的注释

// Since we are caching large views, we want to keep their cache
//由于我们缓存的是大视图,我们希望保留它们的缓存

开始想着是动画的强度不够,加大了动画的强度,结果不加setPersistentDrawingCache还是好一点,于是去Android官方文档查看了下,才明白。

二、Android官方文档给出的介绍

此方法在APl级别28中已弃用,随着APl 11中硬件加速渲染的引入,视图绘图缓存在很大程度上已经过时了。在使用硬件加速时,中间缓存层基本上是不必要的,并且由于创建和更新该层的成本,很容易导致性能的净损失。在极少数情况下,缓存层是有用的,比如alpha动画。View.setLaverTvpe (int。android.araphics.Paint)通过硬件渲染处理这个问题。对于视图层次结构或单个视图的一小部分的软件渲染快照,建议从位图或图片创建一个Canvas,并在视图上调用View. draw(android.graphics.Canvas)。然而,这些软件渲染的用法是不鼓励的,并且与硬件渲染特性(如Config)存在兼容性问题。硬件位图,实时阴影,轮廓剪辑。对于Ul反馈报告或单元测试的屏幕截图,建议使用PixelCopy APl。

三、丢一个案例源码(此案例为官方Demo源码,根据个人修改了点效果)

代码比较简单就不解释了

1.3d动画类

public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;


public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}

@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);

final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;

final Matrix matrix = t.getMatrix();

camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();

matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}

2.Activity类

public class Transition3d extends Activity implements
AdapterView.OnItemClickListener, View.OnClickListener {
private ListView mPhotosList;
private ViewGroup mContainer;
private ImageView mImageView;

//item-name
private static final String[] PHOTOS_NAMES = new String[] {
"百度关键词",
"微信公众号",
"微信小程序",
"个人网站",
"掘金",
"同id:计蒙不吃鱼"
};
//item-img
private static final int[] PHOTOS_RESOURCES = new int[] {
R.drawable.aaaa,
R.drawable.aaaa1,
R.drawable.aaaa1,
R.drawable.aaaa,
R.drawable.aaaa,
R.drawable.aaaa

};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.animations_main_screen);

mPhotosList = (ListView) findViewById(android.R.id.list);
mImageView = (ImageView) findViewById(R.id.picture);
mContainer = (ViewGroup) findViewById(R.id.container);

// 准备列表视图
final ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_list_item_1, PHOTOS_NAMES);
mPhotosList.setAdapter(adapter);
mPhotosList.setOnItemClickListener(this);
// 准备图片
mImageView.setClickable(true);
mImageView.setFocusable(true);
mImageView.setOnClickListener(this);


//由于我们缓存的是大视图,我们希望在每个动画之间保持它们的缓存
mContainer.setPersistentDrawingCache(ViewGroup.PERSISTENT_ANIMATION_CACHE);
}

/**
* 在容器视图上设置一个新的3D旋转。
*
* @param position 单击该项以显示图片,或单击-1以显示列表
* @param start 旋转必须开始的起始角度
* @param end •旋转的结束角度
*/

private void applyRotation(int position, float start, float end) {
//找到容器的中心
final float centerX = mContainer.getWidth() / 2.0f;
final float centerY = mContainer.getHeight() / 2.0f;

//使用提供的参数创建一个新的3D旋转
// 动画监听器用于触发网络动画
final Rotate3dAnimation rotation =
new Rotate3dAnimation(start, end, centerX, centerY, 310.0f, true);
rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new AccelerateInterpolator());
rotation.setAnimationListener(new DisplayNextView(position));

mContainer.startAnimation(rotation);
}

public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
//预加载图像然后开始动画
mImageView.setImageResource(PHOTOS_RESOURCES[position]);
applyRotation(position, 0, 90);
}

public void onClick(View v) {
applyRotation(-1, 180, 90);
}

/**
这class收听动画前半部分的结尾部分。•当容器旋转90度,因此不可见时,它会发布一个新的动作,有效地交换视图。
*/

private final class DisplayNextView implements Animation.AnimationListener {
private final int mPosition;

private DisplayNextView(int position) {
mPosition = position;
}

public void onAnimationStart(Animation animation) {
}

public void onAnimationEnd(Animation animation) {
mContainer.post(new SwapViews(mPosition));
}

public void onAnimationRepeat(Animation animation) {
}
}

/**
这个类负责交换视图并启动第二个视图
动画的一半。
*/

private final class SwapViews implements Runnable {
private final int mPosition;

public SwapViews(int position) {
mPosition = position;
}

public void run() {
final float centerX = mContainer.getWidth() / 2.0f;
final float centerY = mContainer.getHeight() / 2.0f;
Rotate3dAnimation rotation;

if (mPosition > -1) {
mPhotosList.setVisibility(View.GONE);
mImageView.setVisibility(View.VISIBLE);
mImageView.requestFocus();

rotation = new Rotate3dAnimation(90, 180, centerX, centerY, 310.0f, false);
} else {
mImageView.setVisibility(View.GONE);
mPhotosList.setVisibility(View.VISIBLE);
mPhotosList.requestFocus();

rotation = new Rotate3dAnimation(90, 0, centerX, centerY, 310.0f, false);
}

rotation.setDuration(500);
rotation.setFillAfter(true);
rotation.setInterpolator(new DecelerateInterpolator());

mContainer.startAnimation(rotation);
}
}

}

3.布局文件

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">


<ListView
android:id="@android:id/list"
android:persistentDrawingCache="animation|scrolling"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/layout_bottom_to_top_slide" />


<ImageView
android:id="@+id/picture"
android:scaleType="fitCenter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />


</FrameLayout>

4.Listview的载入动画

  • layout_bottom_to_top_slide.xml
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="30%"
android:animationOrder="reverse"
android:animation="@anim/slide_right" />

  • slide_right.xml
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
<translate android:fromXDelta="-100%p" android:toXDelta="0"
android:duration="@android:integer/config_shortAnimTime" />

</set>

5.效果图

Video_20210831_035149_726.gif

收起阅读 »

Android 开发小总结

1、Java 用FileReader 和 FileWriter 进行文件读写FileReader 和FileWriter是对文件进行读取和写入的;具体流程://文件路径 String path = Environment.getExternalStorageD...
继续阅读 »

1、Java 用FileReader 和 FileWriter 进行文件读写

FileReader 和FileWriter是对文件进行读取和写入的;

具体流程:

//文件路径
String path = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"bz_Contact"+File.separator+"bz_contact.txt";

FileWriter.java

//根据给定的 File 对象构造一个 FileWriter 对象。如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。

FileWriter writer = new FileWriter(file,true);
//写入类
BufferWriter bw = new BufferWriter(writer);
//写入方法
bw.writer("XXXXXXXXXXXXXXXXX");
//清空管道
bw.flush();
//关闭管道
bw.close();

读取 FileReader.java

//创建 读取器
FileReader fr= new FileReader(file);

//字符串结果
StringBuffer sb = new StringBuffer();

//读取工具
BufferReader reader = new BufferReader(fr);

//按行读取
String line = reader.readerLine();

//遍历读取
while(line != null){
//追加
sb.appen(line);
//读取
line = reader.readerLine();
}

//清空管道
reader.flush();

//关闭管道
reader.close();

//或者使用
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));
String str = buffer.readLine();
System.out.println(str);
buffer.close();

如果要清空文件夹中所有数据

/**
* 清除之前的所有数据
*/
FileWriter writer = new FileWriter(file);
writer.write("");
writer.flush();
writer.close();

//判断此文件是否还有数据
FileInputStream inputStream = new FileInputStream(file);
//根据这个size来判断,如果是0,表示没有数据
int size = inputStream.available();
Log.e("清除缓存","清除缓存"+size);

这个是判断文件里面是否有数据,如有数据就先清除,之后在新增

public void saveHtml(String html){
try {
if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
String tag = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"news.html";
File file = new File(tag);
if(!file.exists()){
file.createNewFile();
}
FileInputStream inputStream = new FileInputStream(file);
if(inputStream.available()>0){
FileWriter writer = new FileWriter(file); BufferedWriter bufferedWriter = new BufferedWriter(writer);
bufferedWriter.write("");
bufferedWriter.flush();
bufferedWriter.close();
writer.close();
writer = null;
}
FileOutputStream outputStream = new FileOutputStream(file);
FileWriter writer = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(writer);
bufferedWriter.write(html);
bufferedWriter.flush();
bufferedWriter.close();
writer.close();
writer = null;
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}

2、获取手机屏幕的几种方法

1、
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
2、
WindowManager wm = this.getWindowManager();
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
3、
DisplayMetrics  dm = new DisplayMetrics();     
getWindowManager().getDefaultDisplay().getMetrics(dm);     
intscreenWidth = dm.widthPixels;    
intscreenHeight = dm.heightPixels;

3、LinkedHashMap与Map的区别以及遍历MAP的几种中方式

  • public class LinkedHashMap<K,V>extends HashMap<K,V>implements Map<K,V>

  • Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。注意,如果在映射中重新插入 键,则插入顺序不受影响。(如果在调用 m.put(k, v) 前 m.containsKey(k) 返回了 true,则调用时会将键 k 重新插入到映射 m 中。)

  • 遍历Map的四种方法

public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");

//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}

//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
 }
收起阅读 »

JS中this的指向原理

前言 在JS中,每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。与声明的位置无关。 调用位置 理解调用位置:调用位置就是函数在执行时被调用的位置(而不是声明的位置)。 要找到函数的调用位置,最重要是找到函数的调用...
继续阅读 »

前言


在JS中,每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。与声明的位置无关。


调用位置



理解调用位置:调用位置就是函数在执行时被调用的位置(而不是声明的位置)。



要找到函数的调用位置,最重要是找到函数的调用栈(就是为了到达当前执行位置所调用的所有函数),而函数的调用位置就是当前所在栈顶的前一个位置。


举个栗子


function baz() { 
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域,浏览器下位window,node下为global
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置

this绑定规则


函数的this在js引擎执行时,会根据一些规则去绑定到上下文中。


默认绑定


默认绑定应用在最常用的函数调用类型:独立函数调用上。可以把这条规则看作是无法应用其他规则时的默认规则。


function foo() {
//默认规则下,this指向全局对象,即顶层作用域
console.log( this.a );
}

var a = 2;
foo()//2

怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看 foo()是如何调用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用
默认绑定,无法应用其他规则。



严格模式下,不能将全局对象用于默认绑定,因此 this 会绑定到undefined,在浏览器和node中是一样的。



这里有一个微妙但是非常重要的细节,虽然this的绑定规则完全取决于调用位置,但是只有 foo() 运行在非严格模式下时,默认绑定才能绑定到全局对象;在严格模式下调用
foo() 则不影响默认绑定:


function foo() { 
//在非严格模式下运行
console.log( this.a );
}
var a = 2;
(function(){
//在严格模式下调用
"use strict";
foo(); // 2
})();

以上代码混合使用了严格模式和非严格模式,因此foothis不受严格模式影响,但混合使用严格模式是不提倡的,幸运的是es6默认是严格模式


隐式绑定


当一个函数的引用被一个对象持有时(作为该对象的方法),那么该函数的this就绑定在了这个对象上。通常这在声明一个对象,并将一个已声明的函数作为该对象属性时触发。


function foo() { 
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

obj对象声明时,foo作为obj的一个属性,因此其this被隐式绑定到了obj上,因为obj持有对foo的引用。



在对象属性引用链中,只有上一层或者说最后一层在调用位置中起作用。



举个栗子


function foo() { 
console.log( this.a );
}

// obj2.foo引用了foo函数
var obj2 = {
a: 42,
foo: foo
};

//obj1.obj2 引用了obj1对象
var obj1 = {
a: 2,
obj2: obj2
};

//但是foo中的this永远指向直接持有它的引用的那个对象,即obj2
obj1.obj2.foo(); // 42

一个函数的引用被一个对象持有,而这个对象的引用又被另一个对象持有,另一个对象的引用再被另一个对象持有...,这就像一条项链,但是不管层次有多深,这个函数的this永远指向直接持有它的引用的那个对象。


隐式丢失



一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。



function foo() { 
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!

var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

虽然barobj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此==此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定==。


再看一个栗子,发生在传入回调函数时


function foo() { 
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!,很明显,这是个默认绑定
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一
个例子一样。


如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一
样的,没有区别:


function foo() { 
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

以上的栗子再次向我们证明了,函数this是在运行时绑定的,与声明位置无关。



除此之外,还有一种情



况 this 的行为会出乎我们意料:调用回调函数的函数可能会修改 this。在一些流行的
JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。如onclick,addEventListener,会将this绑定在dom元素 上。


显式绑定


显示绑定就是利用js提供的一些内置函数,将this绑定到指定的上下文中。



具体点说,可以使用函数的 call(..) 和



apply(..) 方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们
并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自
己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。


function foo() { 
console.log( this.a );
}
var obj = {
a:2
};
//执行时,foo的this就是obj了
foo.call( obj ); // 2


如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者



new Number(..))。这通常被称为“装箱”。



显式绑定仍然无法解决我们之前提出的丢失绑定问题。



硬绑定


硬绑定是显式绑定的一个变种。


function foo() { 
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2

很好理解,就是在函数运行时再把这个函数绑定到我们制定的this上。



硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值



function foo(something) { 
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5


另一种使用方法是创建一个可以重复使用的辅助函数



function foo(something) { 
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind,bind(..) 会返回一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文并调用原始函数


function foo(something) { 
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

API中可选的调用“上下文”



第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一



个可选的参数,通常被称为"上下文"(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this。这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定。


function foo(el) { 
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

new 绑定


使用new来调用函数时(函数也是对象),或者说发生构造函数调用时,会自动执行下面的操作:



  1. 创建(或者说构造)一个全新的对象。

  2. 这个新对象会被执行原型链[[Prototype]] 连接。

  3. 这个新对象会绑定到函数调用的 this

  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。


function foo(a) { 
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2


使用 new 来调用foo(..)时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this上。ne是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。



规则的优先级


实际判断时,一个场景可能存在多个规则,因此判定时需要由高优先级往下判定。


可以按照下面的顺序来进行判断:




  1. 函数是否在new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。




  2. 函数是否通过callapply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是




指定的对象。



  1. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上


下文对象。



  1. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定


到全局对象。


规则例外



在某些场景下this的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。



1. 将null或undefined作为this进行显式绑定


2. 赋值表达式的返回值


function foo() { 
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是
p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。


3.软绑定


硬绑定很好地解决了隐式绑定可能会无意间将this绑定在顶级作用对象(严格模式下,为undefined)上的问题,但降低了其灵活性,我们要的结果是,保留其灵活性,既能绑定到指定的this上,但又不想让它默认绑定到全局对象上,解决方法就是软绑定。


通俗的说,就是有一个默认值,指定了绑定对象的话就绑定到指定的对象上,否则就绑定到默认对象。


//实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}

总结


如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。


找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。



  1. new 调用?绑定到新创建的对象。

  2. call 或者 apply(或者 bind)调用?绑定到指定的对象。

  3. 由上下文对象调用?绑定到那个上下文对象。

  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。


箭头函数不会以上四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论this绑定到什么)。这和我们创建一个变量来保存当前的this的效果是一样的。


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

收起阅读 »

一道看似简单的阿里前端算法题

题目描述 题目分析 我们以下面这个数组为例,我们首先要明白题目中的第2大的元素指的是4,第3大的元素指的是3,也就是说指的是去重后的数组中的排序。我们之所以要建立一个哈希表是因为我们需要知道第k大和第m大的元素总共出现了几次,因为最后需要进行求和。 [1...
继续阅读 »

题目描述


image.png


题目分析



我们以下面这个数组为例,我们首先要明白题目中的第2大的元素指的是4,第3大的元素指的是3,也就是说指的是去重后的数组中的排序。我们之所以要建立一个哈希表是因为我们需要知道第k大和第m大的元素总共出现了几次,因为最后需要进行求和。



[1, 2, 4, 4, 3, 5]

解题思路



本题博主采用的是哈希表 + 堆排序的方式来求解。



第一步:构建哈希表,键为目标元素,值为目标元素出现的次数


const map = new Map();
for (let v of arr) {
if (!map.get(v)) {
map.set(v,1);
} else {
map.set(v,map.get(v) + 1)
}
}

第二步:对数组去重


const singleNums = [...new Set(arr)]

第三步:构建大顶堆


// 堆的尺寸指的是去重后的数组
let heapSize = singleNums.length;
buildMaxHeap(singleNums, heapSize);
function buildMaxHeap(arr, heapSize) {
// 从最后一个叶子节点开始进行堆化
for (let i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
// 进行堆化
maxHeapify(arr, i, heapSize);
}
}
function maxHeapify(arr, i, heapSize) {
// 首先假定第i个是最大的
let max = i;
let leftChild = 2 * i + 1;
let rightChild = 2 * i + 2;
// 如果下标不越界,并且左孩子的比最大值大则更新最大值
if (leftChild < heapSize && arr[leftChild] > arr[max]) {
max = leftChild;
}
if (rightChild < heapSize && arr[rightChild] > arr[max]) {
max = rightChild;
}
if (max !== i) {
swap(arr, i, max);
// 上来的元素的位置往下要接着堆化
maxHeapify(arr, max, heapSize);
}
}
// 交换数组中两个元素
function swap(nums, a, b) {
let temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}

第四步:求第k大的元素和第m大元素


function target(arr, x) {
for (let i = 0; i < x - 1; i++) {
// 交换不需要进行堆化的元素
if (i === min - 1) result.push(arr[0]);
swap(arr, 0, arr.length - 1 - i);
arr
heapSize--;
maxHeapify(arr, 0, heapSize)
}
}
target(singleNums, max)
result.push(singleNums[0]);

第五步:根据哈希表出现的次数计算并返回结果


return result.reduce((pre,cur) => pre + cur * map.get(cur),0)

AC代码


/*
* @Author: FaithPassion
* @Date: 2021-07-09 10:06:00
* @LastEditTime: 2021-08-28 11:09:30
* @Description: 找出数组中第k大和第m大的数字相加之和
* let arr = [1,2,4,4,3,5], k = 2, m = 4
* findTopSum(arr, k, m); // 第2大的数是4,出现2次,第4大的是2,出现1次,所以结果为10
*/

/**
* @description: 采用堆排序求解
* @param {*} arr 接收一个未排序的数组
* @param {*} k 数组中第k大的元素
* @param {*} m 数组中第m大的元素
* @return {*} 返回数组中第k大和第m大的数字相加之和
*/
function findTopSum(arr, k, m) {


function buildMaxHeap(arr, heapSize) {
// 从最后一个叶子节点开始进行堆化
for (let i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
// 进行堆化
maxHeapify(arr, i, heapSize);
}
}
// 最大堆化函数
function maxHeapify(arr, i, heapSize) {
// 首先假定第i个是最大的
let max = i;
let leftChild = 2 * i + 1;
let rightChild = 2 * i + 2;
// 如果下标不越界,并且左孩子的比最大值大则更新最大值
if (leftChild < heapSize && arr[leftChild] > arr[max]) {
max = leftChild;
}
if (rightChild < heapSize && arr[rightChild] > arr[max]) {
max = rightChild;
}
if (max !== i) {
swap(arr, i, max);
// 上来的元素的位置往下要接着堆化
maxHeapify(arr, max, heapSize);
}
}

// 交换数组中两个元素
function swap(nums, a, b) {
let temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
let result = []
// k和m中较大的
let max = Math.max(k, m);
// k和m中较小的
let min = Math.min(k, m);
const map = new Map();

for (let v of arr) {
if (!map.get(v)) {
map.set(v,1);
} else {
map.set(v,map.get(v) + 1)
}
}
// 求第x大的元素
function target(arr, x) {
for (let i = 0; i < x - 1; i++) {
// 交换不需要进行堆化的元素
if (i === min - 1) result.push(arr[0]);
swap(arr, 0, arr.length - 1 - i);
arr
heapSize--;
maxHeapify(arr, 0, heapSize)
}
}
const singleNums = [...new Set(arr)]
// 堆的大小
let heapSize = singleNums.length;
// 构建大顶堆
buildMaxHeap(singleNums, heapSize);

target(singleNums, max)
result.push(singleNums[0]);
return result.reduce((pre,cur) => pre + cur * map.get(cur),0)

}

findTopSum([1, 2, 4, 4, 3, 5], 2, 4)

题目反思



  • 学会通过堆排序的方式来求解Top K问题。

  • 学会对数组进行去重。

  • 学会使用reduce Api。


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

收起阅读 »

cookie和session、localStorage和sessionStorage、IndexedDB、JWT汇总

cookie和session HTTP协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;Session和Cookie的主要目的就是为了弥补HTTP的无状态特性。 cookie是什么? cookie是...
继续阅读 »

cookie和session


HTTP协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;SessionCookie的主要目的就是为了弥补HTTP的无状态特性。


cookie是什么?


cookie是服务器发送到Web浏览器的一小块数据,服务器发送到浏览器的Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器,用于判断请求是否来自同一个浏览器,例如用户保持登录状态。


cookie的属性




  • name:表示cookie的名称




  • valuecookie对应的值。




  • domain:该字段为可以访问此cookie的域名,即cookie在哪个域有效




  • path: cookie的有效路径。DomainPath标识共同定义了Cookie的作用域:即 Cookie应该发送给哪些URL




  • sizecookie的大小(不超过4kb)




  • expires/Max-Age:有效期。expirescookie被删除的时间戳;Max-Age有效期的时间戳(服务器返回的时间,和客户端可能存在误差),默认为-1,页面关闭立即失效。




  • HttpOnly: 设置为true时不允许通过脚本document.cookie去更改cookie值,也不可获取,能有效的防止xss攻击。但发送请求仍会携带cookie。




  • secure: 标记为SecureCookie只应通过被HTTPS协议加密过的请求发送给服务端,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。




  • SameSite: 该属性可以让Cookie在跨站请求时不会被发送,用来防止CSRF攻击和用户追踪




    • Strict:完全禁止第三方cookie,跨站点时,任何情况下都不会发送cookie。也就是说,只有当前网页的URL与请求目标一致,才会带上cookie




    • Lax: 大多数情况不发送第三方cookie,但导航到目标网址的get请求(链接,预加载请求,GET表单)除外。




    • None: 网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。


      浏览器查看cookie






cookie安全


安全问题可以看我总结的这篇:前端安全—常见的攻击方式及防御方法


session是什么?


Session是保存在服务器记录客户状态的机制。客户端浏览器访问服务器的时候,服务器会为这次请求开辟一块内存空间,这个对象便是Session 对象,存储结构为 ConcurrentHashMap。Session 弥补了 HTTP 无状态特性,服务器可以利用 Session 存储客户端在同一个会话期间的一些操作记录。


session的创建



  • 用户向服务器发送用户名和密码

  • 服务器通过验证后,在当前对话(session)里面保存相关数据(比如用户角色,登录时间等)

  • 服务器向用户返回一个session_id,写入要不干湖的cookie

  • 用户随后的每一次请求都会通过cookie,将session_id传回服务器

  • 服务器收到session_id,找到前期保存的数据,由此得知用户的身份。


cookie和session的区别



  • 储存方式:cookie是服务端产生,储存在客户端;session储存在服务端

  • 储存大小:单个cookie不超过4kb;session没有大小限制

  • 安全性:session更安全

  • 储存内容:cookie只能保存字符串,以文本的方式;session通过类似hashtable的数据结构来储存,能支持任何类型的对象

  • 使用方式

    • cookie机制:如果不在浏览器设置过期时间,cookie被保存在内存中,cookie生命周期随浏览器的关闭而结束。如果在浏览器中设置了cookie的过期时间,cookie被保存在硬盘中,关闭浏览器后,cookie数据仍然存在,知道过期时间才消失。

    • session机制:当服务器收到请求需要创建session对象时,首先会检查客户端请求中是否包含session_id,如果有,服务器将根据id返回对应的session对象。如果没有session_id,服务器会创建新的session对象,并把session_id在本次响应中返回给客户端。




cookie、localStorage和sessionStorage


HTML5提供了两种在客户端存储数据的新方法:localStorage和sessionStorage,挂载在window对象下。


webStorage是本地存储,数据不是由服务器请求传递的。从而它可以存储大量的数据,而不影响网站的性能。


Web Storage的目的是为了克服由cookie带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器。比如客户端需要保存的一些用户行为或数据,或从接口获取的一些短期内不会更新的数据,我们就可以利用Web Storage来存储。


localStorage


生命周期是永久性的。localStorage存储的数据,以“键值对”的形式存在。即使关闭浏览器,也不会让数据消失,除非主动的去删除数据。如果想设置失效时间,需自行封装。localStorage 在所有同源窗口中都是共享的。


sessionStorage


sessionStorage保存的数据用于浏览器的一次会话,当会话结束(关闭浏览器或者页面),数据被清空;SessionStorage的属性和方法与LocalStorage完全一样。


sessionStorage特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 sessionStorage 内容便无法共享
localStorage 在所有同源窗口中都是共享的; cookie也是在所有同源窗口中都是共享的。除了保存期限的长短不同,


cookie、localStorage和sessionStorage的区别



  • 共同点:都是保存在浏览器端,且都遵循同源策略。

  • 不同点:在于生命周期与作用域等不同


image.png


IndexedDB


IndexedDB是一个运行在浏览器上的非关系型数据库,储存空间大,用于客户端存储大量结构化数据(包括文件和blobs) 。可以存字符串,也可以存二进制数据,数据以"键值对"的形式保存,不能有重复,否则会报错。除非被清理,否则一直存在。



  • 键值对储存

  • 异步

  • 支持事务

  • 同源策略

  • 支持二进制储存


JWT(JSON Web Token)


互联网服务离不开用户认证。一般流程看上面session的创建


什么是Token?




  • Token的定义


    Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。token其实说的更通俗点可以叫暗号,在一些数据传输之前,要先进行暗号的核对,不同的暗号被授权不同的数据操作。




  • 简单 token 的组成



    • uid(用户唯一的身份标识)

    • time(当前时间戳)

    • sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)




  • token 的身份验证流程



    • 客户端使用用户名跟密码请求登录

    • 服务端收到请求,去验证用户名与密码

    • 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage

    • 客户端每次向服务端请求资源的时候需要带着服务端签发的 token

    • 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
      image.png




  • 使用Token的目的


    Token的目的是为了减少频繁的查询数据库,减轻服务器的压力。基于Token用户认证是一种服务器无状态的认证方式,服务器不存放数据,所有数据都保存在客户端,每次请求都发回服务器,用解析token的时间来换取session的储存空间,从而减轻服务器的压力,减少频繁的查询数据库。token 完全由应用管理,所以它可以避开同源策略。




什么是 JWT?


JWT的原理


JWT的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。


JWT的数据结构




  • Header(头部)


    Header部分是一个JSON对象,描述JWT的元数据,使用Base64编码转成字符串。




  • Payload(负载)


    Payload是一个JSON对象,用来存放实际需要传递的数据,使用Base64编码转成字符串。



    • iss (issuer):签发人

    • exp (expiration time):过期时间

    • sub (subject):主题

    • aud (audience):受众

    • nbf (Not Before):生效时间

    • iat (Issued At):签发时间

    • jti (JWT ID):编号




  • Signature(签名)


    Signature是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是 HMAC SHA256)产生签名。用"点"(.)分隔拼接成字符串后返回给用户。




JWT的特点



  • 默认不加密,也可以加密

  • 可以用于认证,也可以用于交换信息。降低服务器查询数据库的次数,减小服务器压力

  • 服务器无状态,因此无法在使用过程中废除某个Token,或者更改Token的权限。即一旦JWT签发了,在到期之前始终有效,除非服务器部署额外的逻辑

  • JWT本身包含了认证信息,为保证安全性,有效期应设置得比较短

  • 为了减少盗用,JWT应使用HTTPS协议传输

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

iOS 优雅的处理网络数据,你真的会吗?不如看看这篇.

相信大家平时在用 App 的时候, 往往有过这样的体验,那就是加载网络数据等待的时间过于漫长,滚动浏览时伴随着卡顿,甚至在没有网络的情况下,整个应用处于不可用状态。那么我们该怎么去提高用户体验,保证用户没有漫长的等待感,还可以轻松自在的享受等待,对加载后的内容...
继续阅读 »
相信大家平时在用 App 的时候, 往往有过这样的体验,那就是加载网络数据等待的时间过于漫长,滚动浏览时伴随着卡顿,甚至在没有网络的情况下,整个应用处于不可用状态。那么我们该怎么去提高用户体验,保证用户没有漫长的等待感,还可以轻松自在的享受等待,对加载后的内容有明确的预期呢?

案例分享

在现代的工作生活中,手机早已不是单纯的通信工具了,它更像是一个集办公,娱乐,消费的终端,潜移默化的成为了我们生活的一部分。所以作为 iOS 开发者的我们,在日常的开发中,也早已不是处理显示零星的数据这么简单,为了流量往往我们需要在 App 里显示大量有价值的信息来吸引用户,如何优雅的显示这些海量的数据,考量的就是你的个人经验了。

正如大多数 iOS 开发人员所知,显示滚动数据是构建移动应用中常见的任务,Apple 的 SDK 提供了 UITableView 和 UICollectionVIew 这俩大组件来帮助执行这样的任务。但是,当需要显示大量数据时,确保平滑如丝的滚动可能会非常的棘手。所以今天正好趁这个机会,和大家分享一下处理大量可滚动数据方面的个人经验。

在这篇文章中,你将会学到以下内容:

1.让你的 App 可以无限滚动(infinite scrolling),并且滚动数据无缝加载

2.让你的 App 数据滚动时避免卡顿,实现平滑如丝的滚动

3.异步存储(Cache)和获取图像,来使你的 App 具有更高的响应速度

无限滚动,无缝加载

提到列表分页,相信大家第一个想到的就是 MJRefresh,用于上拉下拉来刷新数据,当滚动数据到达底部的时候向服务器发送请求,然后在控件底部显示一个 Loading 动画,待请求数据返回后,Loading 动画消失,由 UITableView 或者 UICollectionView 控件继续加载这些数据并显示给用户,效果如下图所示:




在这种情况下就造成了一种现象,那就是 App 向服务器请求数据到数据返回这段时间留下了一个空白,如果在网络差的情况下,这段空白的时间将会持续,这给人的体验会很不好。那该如何去避免这种现象呢!或者说我们能否去提前获取到其余的数据,在用户毫无感知的情况下把数据请求过来,看上去就像无缝加载一样呢!

答案当然是肯定的!

为了改善应用程序体验,在 iOS 10 上,Apple 对 UICollectionView 和 UITableView 引入了 Prefetching API,它提供了一种在需要显示数据之前预先准备数据的机制,旨在提高数据的滚动性能。

首先,我先和大家介绍一个概念:无限滚动,无限滚动是可以让用户连续的加载内容,而无需分页。在 UI 初始化的时候 App 会加载一些初始数据,然后当用户滚动快要到达显示内容的底部时加载更多的数据。

多年来,像 Instagram, Twitter 和 Facebook 这样的社交媒体公司都使这种技术。如果查看他们的 App ,你就可以看到无限滚动的实际效果,这里我就给大伙展示下 Instagram 的效果吧!


如何实现

由于 Instagram 的 UI 过于复杂,在这我就不去模仿实现了,但是我模仿了它的加载机制,同样的实现了一个简单的数据无限滚动和无缝加载的效果。

简单的说下我的思路:

先自定义一个 Cell 视图,这个视图由一个 UILabel 和 一个 UIImageView 构成,用于显示文本和网络图片;然后模拟网络请求来获取数据,注意该步骤一定是异步执行的;最后用 UITableView 来显示返回的数据,在 viewDidLoad 中先请求网络数据来获取一些初始化数据,然后再利用 UITableView 的 Prefetching API 来对数据进行预加载,从而来实现数据的无缝加载。

那关于无限滚动该如何实现呢!其实这个无限滚动并不是真正意义上的永无止尽,严格意义上来讲它是有尽头的,只不过这个功能背后的数据是不可估量的,只有大量的数据做支持才能让应用一直不断的从服务端获取数据。

正常情况下,我们在构建 UITableView 这个控件的时候,需要对它的行数(numsOfRow)做一个初始化,这个行数对我们实现无限加载和无缝加载是一个很关键的因素,假设我们每次根据服务端返回的数据量去更新 UITableView 的行数并 Reload,那我之前说的 Prefetching API 在这种情况下就失去作用了,因为它起作用的前提是要保证预加载数据时 UITableView 当前的行数要小于它的总行数。当然前者也可以实现数据加载,但它的效果就不是无缝加载,它在每次加载数据的时候都会有一个 Loading 等待的时间。

回到我上面所说的无限滚动, 其实实现起来并不难,正常情况下,我们向服务端请求大量相同类型的数据的时候,都会提供一个接口,我称之为分页请求接口,该接口在每次数据返回的时候,都会告诉客户端总共有多少页数据,每页的数据量是多少,当前是第几页,这样我们就能计算出来总共的数据有多少,而这就是 UITableView 的总行数。

响应数据的示范如下(为清楚起见,它仅显示与分页有关的字段):

{

"has_more": true,
"page": 1,
"total": 84,
"items": [

...
...
]
}

下面,我就用代码来一步步的实现它吧!

模拟分页请求

由于没有找到合适的分页测试接口,于是我自己模拟一了一个分页请求接口,每次调用该接口的时候都延时 2s 来模拟网络请求的状态,代码如下:

 func fetchImages() {
guard !isFetchInProcess else {
return
}

isFetchInProcess = true
// 延时 2s 模拟网络环境
print("+++++++++++ 模拟网络数据请求 +++++++++++")
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 2) {
print("+++++++++++ 模拟网络数据请求返回成功 +++++++++++")
DispatchQueue.main.async {
self.total = 1000
self.currentPage += 1
self.isFetchInProcess = false
// 初始化 30个 图片
let imagesData = (1...30).map {
ImageModel(url: baseURL+"\($0).png", order: $0)
}
self.images.append(contentsOf: imagesData)

if self.currentPage > 1 {
let newIndexPaths = self.calculateIndexPathsToReload(from: imagesData)
self.delegate?.onFetchCompleted(with: newIndexPaths)
} else {
self.delegate?.onFetchCompleted(with: .none)
}
}
}
}

数据回调处理:

extension ViewController: PreloadCellViewModelDelegate {

func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
guard let newIndexPathsToReload = newIndexPathsToReload else {
tableView.tableFooterView = nil
tableView.reloadData()
return
}

let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
indicatorView.stopAnimating()
tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}

func onFetchFailed(with reason: String) {
indicatorView.stopAnimating()
tableView.reloadData()
}
}

预加载数据

首先如果你想要 UITableView 预加载数据,你需要在 viewDidLoad() 函数中插入如下代码,并且请求第一页的数据:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
...
tableView.prefetchDataSource = self
...
// 模拟请求图片
viewModel.fetchImages()
}

然后,你需要实现 UITableViewDataSourcePrefetching 的协议,它的协议里包含俩个函数:


// this protocol can provide information about cells before they are displayed on screen.

@protocol UITableViewDataSourcePrefetching <NSObject>

@required

// indexPaths are ordered ascending by geometric distance from the table view
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

@optional

// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths:
- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

@end

第一个函数会基于当前滚动的方向和速度对接下来的 IndexPaths 进行 Prefetch,通常我们会在这里实现预加载数据的逻辑。

第二个函数是一个可选的方法,当用户快速滚动导致一些 Cell 不可见的时候,你可以通过这个方法来取消任何挂起的数据加载操作,有利于提高滚动性能, 在下面我会讲到。

实现这俩个函数的逻辑代码为:

extension ViewController: UITableViewDataSourcePrefetching {
// 翻页请求
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
if needFetch {
// 1.满足条件进行翻页请求
indicatorView.startAnimating()
viewModel.fetchImages()
}

for indexPath in indexPaths {
if let _ = viewModel.loadingOperations[indexPath] {
return
}

if let dataloader = viewModel.loadImage(at: indexPath.row) {
print("在 \(indexPath.row) 行 对图片进行 prefetch ")
// 2 对需要下载的图片进行预热
viewModel.loadingQueue.addOperation(dataloader)
// 3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}

func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
indexPaths.forEach {
if let dataLoader = viewModel.loadingOperations[$0] {
print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
dataLoader.cancel()
viewModel.loadingOperations.removeValue(forKey: $0)
}
}
}
}

最后,再加上俩个有用的方法该功能就大功告成了:

    // 用于计算 tableview 加载新数据时需要 reload 的 cell
func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {
let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []
let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)
return Array(indexPathsIntersection)
}

// 用于确定该索引的行是否超出了目前收到数据的最大数量
func isLoadingCell(for indexPath: IndexPath) -> Bool {
return indexPath.row >= (viewModel.currentCount)
}

见证时刻的奇迹到了,请看效果:




通过日志,我们也可以清楚的看到,在滚动的过程中是有 Prefetch 和 CancelPrefetch 操作的:




好了,到这里我就简单的实现了 UITableView 无尽滚动和对数据无缝加载的效果,你学会了吗?

如何避免滚动时的卡顿

当你遇到滚动卡顿的应用程序时,通常是由于任务长时间运行阻碍了 UI 在主线程上的更新,想让主线程有空来响应这类更新事件,第一步就是要将消耗时间的任务交给子线程去执行,避免在获取数据时阻塞主线程。

苹果提供了很多为应用程序实现并发的方式,例如 GCD,我在这里对 Cell 上的图片进行异步加载使用的就是它。

代码如下:

class DataLoadOperation: Operation {
var image: UIImage?
var loadingCompleteHandle: ((UIImage?) -> ())?
private var _image: ImageModel
private let cachedImages = NSCache<NSURL, UIImage>()

init(_ image: ImageModel) {
_image = image
}

public final func image(url: NSURL) -> UIImage? {
return cachedImages.object(forKey: url)
}

override func main() {
if isCancelled {
return
}

guard let url = _image.url else {
return
}
downloadImageFrom(url) { (image) in
DispatchQueue.main.async { [weak self] in
guard let ss = self else { return }
if ss.isCancelled { return }
ss.image = image
ss.loadingCompleteHandle?(ss.image)
}
}

}

// Returns the cached image if available, otherwise asynchronously loads and caches it.
func downloadImageFrom(_ url: NSURL, completeHandler: @escaping (UIImage?) -> ()) {
// Check for a cached image.
if let cachedImage = image(url: url) {
DispatchQueue.main.async {
print("命中缓存")
completeHandler(cachedImage)
}
return
}

URLSession.shared.dataTask(with: url as URL) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let _image = UIImage(data: data)
else { return }
// Cache the image.
self.cachedImages.setObject(_image, forKey: url, cost: data.count)
completeHandler(_image)
}.resume()
}
}

那具体如何使用呢!别急,听我娓娓道来,这里我再给大家一个小建议,大家都知道 UITableView 实例化 Cell 的方法是:tableView:cellForRowAtIndexPath: ,相信很多人都会在这个方法里面去进行数据绑定然后更新 UI,其实这样做是一种比较低效的行为,因为这个方法需要为每个 Cell 调用一次,它应该快速的执行并返回重用 Cell 的实例,不要在这里去执行数据绑定,因为目前在屏幕上还没有 Cell。我们可以在 tableView:willDisplayCell:forRowAtIndexPath: 这个方法中进行数据绑定,这个方法在显示cell之前会被调用。

为每个 Cell 执行下载任务的实现代码如下:

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "PreloadCellID") as? ProloadTableViewCell else {
fatalError("Sorry, could not load cell")
}

if isLoadingCell(for: indexPath) {
cell.updateUI(.none, orderNo: "\(indexPath.row)")
}

return cell
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// preheat image ,处理将要显示的图像
guard let cell = cell as? ProloadTableViewCell else {
return
}

// 图片下载完毕后更新 cell
let updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in
cell.updateUI(image, orderNo: "\(indexPath.row)")
viewModel.loadingOperations.removeValue(forKey: indexPath)
}

// 1\. 首先判断是否已经存在创建好的下载线程
if let dataLoader = viewModel.loadingOperations[indexPath] {
if let image = dataLoader.image {
// 1.1 若图片已经下载好,直接更新
cell.updateUI(image, orderNo: "\(indexPath.row)")
} else {
// 1.2 若图片还未下载好,则等待图片下载完后更新 cell
dataLoader.loadingCompleteHandle = updateCellClosure
}
} else {
// 2\. 没找到,则为指定的 url 创建一个新的下载线程
print("在 \(indexPath.row) 行创建一个新的图片下载线程")
if let dataloader = viewModel.loadImage(at: indexPath.row) {
// 2.1 添加图片下载完毕后的回调
dataloader.loadingCompleteHandle = updateCellClosure
// 2.2 启动下载
viewModel.loadingQueue.addOperation(dataloader)
// 2.3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}

对预加载的图片进行异步下载(预热):

func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
if needFetch {
// 1.满足条件进行翻页请求
indicatorView.startAnimating()
viewModel.fetchImages()
}

for indexPath in indexPaths {
if let _ = viewModel.loadingOperations[indexPath] {
return
}

if let dataloader = viewModel.loadImage(at: indexPath.row) {
print("在 \(indexPath.row) 行 对图片进行 prefetch ")
// 2 对需要下载的图片进行预热
viewModel.loadingQueue.addOperation(dataloader)
// 3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}

取消 Prefetch 时,cancel 任务,避免造成资源浪费

func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
indexPaths.forEach {
if let dataLoader = viewModel.loadingOperations[$0] {
print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
dataLoader.cancel()
viewModel.loadingOperations.removeValue(forKey: $0)
}
}
}

经过这般处理,我们的 UITableView 滚动起来肯定是如丝般顺滑,小伙伴们还等什么,还不赶紧试一试。

图片缓存

虽然我在上面对我的应用增加了并发操作,但是我一看 Xcode 的性能分析,我不禁陷入了沉思,我的应用程序太吃内存了,假如我不停的刷,那我的手机应该迟早会把我的应用给终止掉,下图是我刷到 200 行的时候的性能分析图:

内存



磁盘



可以看到我的应用的性能分析很不理想,究其原因在于我的应用里显示了大量的图片资源,每次来回滚动的时候,都会重新去下载新的图片,而没有对图片做缓存处理。

所以,针对这个问题,我为我的应用加入了缓存 NSCache 对象,来对图片做一个缓存,具体代码实现如下:

class ImageCache: NSObject {

private var cache = NSCache<AnyObject, UIImage>()
public static let shared = ImageCache()
private override init() {}

func getCache() -> NSCache<AnyObject, UIImage> {
return cache
}
}

在下载开始的时候,检查有没有命中缓存,如果命中则直接返回图片,否则重新下载图片,并添加到缓存中:

func downloadImageFrom(_ url: URL, completeHandler: @escaping (UIImage?) -> ()) {
// Check for a cached image.
if let cachedImage = getCacheImage(url: url as NSURL) {
print("命中缓存")
DispatchQueue.main.async {
completeHandler(cachedImage)
}
return
}

URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let _image = UIImage(data: data)
else { return }

// Cache the image.
ImageCache.shared.getCache().setObject(_image, forKey: url as NSURL)

completeHandler(_image)
}.resume()
}

有了缓存的加持后,再用 Xcode 来查看我的应用的性能,就会发现内存和磁盘的占用已经下降了很多:

内存


磁盘



关于图片缓存的技术,这里只是用了最简单的一种,外面很多开源的图片库都有不同的缓存策略,感兴趣的可以去 GitHub 上学习一下它们的源码,我在这里就不做赘述了。

最后

终于写完了,长舒了一口气,这篇文章的篇幅有点长,主要是我花了一点时间做调研,然后看到这个知识点想给大家讲一下,看到那个知识点也想给大家讲一下,最终引经据典(东拼西凑)完成了这篇文章,希望大家能够喜欢 :)。




作者:iOS鑫
链接:https://www.jianshu.com/p/f00f43e401da

收起阅读 »

Flutter 系列 - 环境搭建

Flutter 作为火热的跨端工具包,在 github 上超过 120k 的关注量,可见一斑。 基于目前本人正在学习 Flutter 的路上,会将整个学习的过程记录下来。 本博文主要讲解环境的搭建,先把项目搭建好,跑通 demo 才有玩下去的必要和成就感,你说...
继续阅读 »

Flutter 作为火热的跨端工具包,在 github 上超过 120k 的关注量,可见一斑。


基于目前本人正在学习 Flutter 的路上,会将整个学习的过程记录下来。


本博文主要讲解环境的搭建,先把项目搭建好,跑通 demo 才有玩下去的必要和成就感,你说是吧?


本人开发环境




  • macOS Big Sur 版本 11.2 芯片 Apple M1




  • 磁盘空间:> 2.8 GB (要求的最小的空间)




  • $SHELL




echo $SHELL
/bin/bash


⚠️ 之后出现并解决的问题都是基于本人的环境



安装 Flutter


通过官网下载安装包。


将安装包放到自己想存放的地方。这里,我放在 文稿 -> sdk 方便管理,然后解压下载包。


配置 flutterPATH 环境变量,格式如下:


export PATH=$PATH:${pwd}/flutter/bin

export PATH=${pwd}/flutter/bin:$PATH

这里我需要编辑 ~/.bash_profile 文件,添加下面这行内容:


export PATH=/Users/jimmy/Documents/sdk/flutter/bin:$PATH

安装 IDE


作为一个前端开发者,比较偏向 VS code,直接安装其稳定版即可。


因为需要调试安卓平台,还需要安装编辑器 Android StudioAndroid StudioFlutter 提供了一个完整的集成开发环境。


不管 VS code 还是 Android Studio 都需要安装 Flutter 插件。



Android Studio 我还是安装在 文稿 -> sdk



注意安装android studio的路径,也许会报sdk的错误。类似错误 ❌


# [Flutter-Unable to find bundled Java version(flutter doctor), after updated android studio Arctic Fox(2020.3.1) on M1 Apple Silicon](https://stackoverflow.com/questions/68569430/flutter-unable-to-find-bundled-java-versionflutter-doctor-after-updated-andro)

对应的解决方法:flutter-unable-to-find-bundled-java-versionflutter-doctor-after-updated-andro


验证


之后,运行 flutter doctor 或者 flutter doctor -v 来检查是否安装了必要的安装包。


下面是自己搭建环境的情况flutter doctor -v


[✓] Flutter (Channel stable, 2.2.3, on macOS 11.2 20D64 darwin-arm, locale

    zh-Hans-CN)

    • Flutter version 2.2.3 at /Users/jimmy/Documents/sdk/flutter

    • Framework revision f4abaa0735 (9 weeks ago), 2021-07-01 12:46:11 -0700

    • Engine revision 241c87ad80

    • Dart version 2.13.4

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)

    • Android SDK at /Users/jimmy/Library/Android/sdk

    • Platform android-31, build-tools 31.0.0

    • Java binary at: /Users/jimmy/Documents/sdk/Android

      Studio.app/Contents/jre/jdk/Contents/Home/bin/java

    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)

    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS

    • Xcode at /Applications/Xcode.app/Contents/Developer

    • Xcode 12.5.1, Build version 12E507

    • CocoaPods version 1.10.2

[✓] Chrome - develop for the web

    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2020.3)

    • Android Studio at /Users/jimmy/Documents/sdk/Android Studio.app/Contents # 留意 Android Studio 路径

    • Flutter plugin can be installed from:

      🔨 https://plugins.jetbrains.com/plugin/9212-flutter

    • Dart plugin can be installed from:

      🔨 https://plugins.jetbrains.com/plugin/6351-dart

    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)

[✓] VS Code (version 1.59.1)

    • VS Code at /Applications/Visual Studio Code.app/Contents

    • Flutter extension version 3.25.0

[✓] Connected device (1 available)

    • Chrome (web) • chrome • web-javascript • Google Chrome 92.0.4515.159

• No issues found!

出现 No issues found! 的提示,说明你捣鼓成功了~


运行 Demo


我们在 VS code 上新建一个项目:


查看 -> 命令面板 -> Flutter: New Application Project

初始化项目之后,运行 -> 启动调试,然后按照下图运行应用:


vscode_demo.png


如果选中 Chrome web 会直接调起你安装好的谷歌浏览器。


如果选中 Start iOS Simulator 会调起 xCode 的模拟器。


如果选中 Start Pixel 2 API 31 会调起 Android Studio 的模拟器。



当然你得在 Android Studio 上预设手机型号是哪个,不然初次在 VS code 上调不起来。



effect_result.png


【完】~ 下次可以更加愉快玩耍了


作者:Jimmy
链接:https://juejin.cn/post/7002401225270362143

收起阅读 »

Swift声明参考-超详细(下)

枚举声明在你的程序里使用枚举声明来引入一个枚举类型。枚举声明有两种基本的形式,使用关键字enum来声明。枚举声明体使用从零开始的变量——叫做枚举事件,和任意数量的 声明,包括计算型属性,实例方法,静态方法,构造器,类型别名,甚至其他枚举,结构体,和类。枚举声明...
继续阅读 »


枚举声明

在你的程序里使用枚举声明来引入一个枚举类型。

枚举声明有两种基本的形式,使用关键字enum来声明。枚举声明体使用从零开始的变量——叫做枚举事件,和任意数量的 声明,包括计算型属性,实例方法,静态方法,构造器,类型别名,甚至其他枚举,结构体,和类。枚举声明不能 包含析构器或者协议声明。

不像类或者结构体。枚举类型并不提供隐式的初始构造器,所有构造器必须显式的声明。构造器可以委托枚举中的其他 构造器,但是构造过程仅当构造器将一个枚举时间完成后才全部完成。

和结构体类似但是和类不同,枚举是值类型:枚举实例在赋予变量或常量时,或者被函数调用时被复制。 更多关于值类型的信息,参见结构体和枚举都是值类型(Structures and Enumerations Are Value Types)一节。

你可以扩展枚举类型,正如在扩展名声明(Extension Declaration)中讨论的一样。

任意事件类型的枚举

如下的形式声明了一个包含任意类型枚举时间的枚举变量


1.
enum enumeration name { case enumeration case 1 case enumeration case 2(associated value types) }

这种形式的枚举声明在其他语言中有时被叫做可识别联合(discrinminated)。

这种形式中,每一个事件块由关键字case开始,后面紧接着一个或多个以逗号分隔的枚举事件。每一个事件名必须是 独一无二的。每一个事件也可以指定它所存储的指定类型的值,这些类型在关联值类型的元祖里被指定,立即书写在事件 名后。获得更多关于关联值类型的信息和例子,请查看关联值(associated values)一节。

使用原始事件值的枚举

以下的形式声明了一个包含相同基础类型的枚举事件的枚举:


1.
enum enumeration name: raw value type { case enumeration case 1 = raw value 1 case enumeration case 2 = raw value 2 }

在这种形式中,每一个事件块由case关键字开始,后面紧接着一个或多个以逗号分隔的枚举事件。和第一种形式的枚举 事件不同,这种形式的枚举事件包含一个同类型的基础值,叫做原始值(raw value)。这些值的类型在原始值类型(raw value type) 中被指定,必须是字面上的整数,浮点数,字符或者字符串。

每一个事件必须有唯一的名字,必须有一个唯一的初始值。如果初始值类型被指定为int,则不必为事件显式的指定值, 它们会隐式的被标为值0,1,2等。每一个没有被赋值的Int类型时间会隐式的赋予一个初始值,它们是自动递增的。


1.
num ExampleEnum: Int { case A, B, C = 5, D }

在上面的例子中,ExampleEnum.A的值是0,ExampleEnum.B的值是。因为ExampleEnum.C的值被显式的设定为5,因此 ExampleEnum.D的值会自动增长为6.

枚举事件的初始值可以调用方法roRaw获得,如ExampleEnum.B.toRaw()。你也可以通过调用fromRaw方法来使用初始值找到 其对应的事件,并返回一个可选的事件。查看更多信息和获取初始值类型事件的信息,参阅初始值(raw values)。

获得枚举事件

使用点(.)来引用枚举类型的事件,如 EnumerationType.EnumerationCase。当枚举类型可以上下文推断出时,你可以 省略它(.仍然需要),参照枚举语法(Enumeration Syntax)和显式成员表达(Implicit Member Expression).

使用switch语句来检验枚举事件的值,正如使用switch语句匹配枚举值(Matching Enumeration Values with a Switch Statement)一节描述的那样。

枚举类型是模式匹配(pattern-matched)的,和其相反的是switch语句case块中枚举事件匹配,在枚举事件类型(Enumeration Case Pattern)中有描述。

GRAMMAR OF AN ENUMERATION DECLARATION
enum-declaration → attributes­opt­union-style-enum­ attributes­opt­raw-value-style-enum­ union-style-enum → enum-name­generic-parameter-clause­opt­{­union-style-enum-members­opt­}­ union-style-enum-members → union-style-enum-member­union-style-enum-members­opt­ union-style-enum-member → declaration­ union-style-enum-case-clause­ union-style-enum-case-clause → attributes­opt­case­union-style-enum-case-list­ union-style-enum-case-list → union-style-enum-case­ union-style-enum-case­,­union-style-enum-case-list­ union-style-enum-case → enum-case-name­tuple-type­opt­ enum-name → identifier­ enum-case-name → identifier­ raw-value-style-enum → enum-name­generic-parameter-clause­opt­:­type-identifier­{­raw-value-style-enum-members­opt­}­ raw-value-style-enum-members → raw-value-style-enum-member­raw-value-style-enum-members­opt­ raw-value-style-enum-member → declaration­ raw-value-style-enum-case-clause­ raw-value-style-enum-case-clause → attributes­opt­case­raw-value-style-enum-case-list­ raw-value-style-enum-case-list → raw-value-style-enum-case­ raw-value-style-enum-case­,­raw-value-style-enum-case-list­ raw-value-style-enum-case → enum-case-name­raw-value-assignment­opt­ raw-value-assignment → =­literal­

结构体声明

使用结构体声明可以在你的程序里引入一个结构体类型。结构体声明使用struct关键字,遵循如下的形式:


1.
struct structure name: adopted protocols { declarations }

结构体内包含零或多个声明。这些声明可以包括存储型和计算型属性,静态属性,实例方法,静态方法,构造器, 类型别名,甚至其他结构体,类,和枚举声明。结构体声明不能包含析构器或者协议声明。详细讨论和包含多种结构体 声明的实例,参见类和结构体一节。

结构体可以包含任意数量的协议,但是不能继承自类,枚举或者其他结构体。

有三种方法可以创建一个声明过的结构体实例:
-调用结构体内声明的构造器,参照构造器(initializers)一节。
—如果没有声明构造器,调用结构体的逐个构造器,详情参见Memberwise Initializers for Structure Types.
—如果没有声明析构器,结构体的所有属性都有初始值,调用结构体的默认构造器,详情参见默认构造器(Default Initializers).
结构体的构造过程参见初始化(initiaization)一节。
结构体实例属性可以用点(.)来获得,详情参见获得属性(Accessing Properties)一节。
结构体是值类型;结构体的实例在被赋予变量或常量,被函数调用时被复制。获得关于值类型更多信息,参见 结构体和枚举都是值类型(Structures and Enumerations Are Value Types)一节。
你可以使用扩展声明来扩展结构体类型的行为,参见扩展声明(Extension Declaration).

GRAMMAR OF A STRUCTURE DECLARATION
struct-declaration → attributes­opt­struct­struct-name­generic-parameter-clause­opt­type-inheritance-clause­opt­struct-body­ struct-name → identifier­ struct-body → {­declarations­opt­}

类声明

你可以在你的程序中使用类声明来引入一个类。类声明使用关键字class,遵循如下的形式:


1
. class class name: superclass, adopted protocols { declarations }

一个类内包含零或多个声明。这些声明可以包括存储型和计算型属性,实例方法,类方法,构造器,单独的析构器方法, 类型别名,甚至其他结构体,类,和枚举声明。类声明不能包含协议声明。详细讨论和包含多种类声明的实例,参见类和 结构体一节。

一个类只能继承一个父类,超类,但是可以包含任意数量的协议。这些超类第一次在type-inheritance-clause出现,遵循任意协议。

正如在初始化声明(Initializer Declaration)谈及的那样,类可以有指定和方便的构造器。当你声明任一中构造器时, 你可以使用requierd变量来标记构造器,要求任意子类来重写它。指定类的构造器必须初始化类所有的已声明的属性, 它必须在子类构造器调用前被执行。

类可以重写属性,方法和它的超类的构造器。重写的方法和属性必须以override标注。

虽然超类的属性和方法声明可以被当前类继承,但是超类声明的指定构造器却不能。这意味着,如果当前类重写了超类 的所有指定构造器,它就继承了超类的方便构造器。Swift的类并不是继承自一个全局基础类。

有两种方法来创建已声明的类的实例:

  • 调用类的一个构造器,参见构造器(initializers)。
  • 如果没有声明构造器,而且类的所有属性都被赋予了初始值,调用类的默认构造器,参见默认构造器(default initializers).

类实例属性可以用点(.)来获得,详情参见获得属性(Accessing Properties)一节。

类是引用类型;当被赋予常量或变量,函数调用时,类的实例是被引用,而不是复制。获得更多关于引用类型的信息, 结构体和枚举都是值类型(Structures and Enumerations Are Value Types)一节。

你可以使用扩展声明来扩展类的行为,参见扩展声明(Extension Declaration).

GRAMMAR OF A CLASS DECLARATION
class-declaration → attributes­opt­class­class-name­generic-parameter-clause­opt­type-inheritance-clause­opt­class-body­ class-name → identifier­ class-body → {­declarations­opt­}

协议声明(translated by 小一)

一个协议声明为你的程序引入一个命名了的协议类型。协议声明使用 protocol 关键词来进行声明并有下面这样的形式:


1
. protocol protocol name: inherited protocols {
2. protocol member declarations
3. }

协议的主体包含零或多个协议成员声明,这些成员描述了任何采用该协议必须满足的一致性要求。特别的,一个协议可以声明必须实现某些属性、方法、初始化程序及附属脚本的一致性类型。协议也可以声明专用种类的类型别名,叫做关联类型,它可以指定协议的不同声明之间的关系。协议成员声明会在下面的详情里进行讨论。

协议类型可以从很多其它协议那继承。当一个协议类型从其它协议那继承的时候,来自其它协议的所有要求就集合了,而且从当前协议继承的任何类型必须符合所有的这些要求。对于如何使用协议继承的例子,查看协议继承

注意:你也可以使用协议合成类型集合多个协议的一致性要求,详情参见协议合成类型协议合成

你可以通过采用在类型的扩展声明中的协议来为之前声明的类型添加协议一致性。在扩展中你必须实现所有采用协议的要求。如果该类型已经实现了所有的要求,你可以让这个扩展声明的主题留空。

默认地,符合某一个协议的类型必须实现所有声明在协议中的属性、方法和附属脚本。也就是说,你可以用optional属性标注这些协议成员声明以指定它们的一致性类型实现是可选的。optional属性仅仅可以用于使用objc属性标记过的协议。这样的结果就是仅仅类类型可以采用并符合包含可选成员要求的协议。更多关于如何使用optional属性的信息及如何访问可选协议成员的指导——比如当你不能肯定是否一致性的类型实现了它们——参见可选协议要求

为了限制协议的采用仅仅针对类类型,需要使用class_protocol属性标记整个协议声明。任意继承自标记有class_protocol属性协议的协议都可以智能地仅能被类类型采用。

注意:如果协议已经用object属性标记了,class_protocol属性就隐性地应用于该协议;没有必要再明确地使用class_protocol属性来标记该协议了。

协议是命名的类型,因此它们可以以另一个命名类型出现在你代码的所有地方,就像协议类型里讨论的那样。然而你不能构造一个协议的实例,因为协议实际上不提供它们指定的要求的实现。

你可以使用协议来声明一个类的代理的方法或者应该实现的结构,就像委托(代理)模式描述的那样。

协议声明的语法 protocol-declaration → attributes­opt­protocol­protocol-name­type-inheritance-clause­opt­protocol-body­ protocol-name → identifier­ protocol-body → {­protocol-member-declarations­opt­}­ protocol-member-declaration → protocol-property-declaration­ protocol-member-declaration → protocol-method-declaration­ protocol-member-declaration → protocol-initializer-declaration­ protocol-member-declaration → protocol-subscript-declaration­ protocol-member-declaration → protocol-associated-type-declaration­ protocol-member-declarations → protocol-member-declaration­protocol-member-declarations­opt­

协议属性声明

协议声明了一致性类型必须在协议声明的主体里通过引入一个协议属性声明来实现一个属性。协议属性声明有一种特殊的类型声明形式:


1.
var property name: type { get set }

同其它协议成员声明一样,这些属性声明仅仅针对符合该协议的类型声明了getter和setter要求。结果就是你不需要在协议里它被声明的地方实现getter和setter。

getter和setter要求可以通过一致性类型以各种方式满足。如果属性声明包含get和set关键词,一致性类型就可以用可读写(实现了getter和setter)的存储型变量属性或计算型属性,但是属性不能以常量属性或只读计算型属性实现。如果属性声明仅仅包含get关键词的话,它可以作为任意类型的属性被实现。比如说实现了协议的属性要求的一致性类型,参见属性要求

更多参见变量声明

协议属性声明语法 protocol-property-declaration → variable-declaration-head­variable-name­type-annotation­getter-setter-keyword-block­

协议方法声明

协议声明了一致性类型必须在协议声明的主体里通过引入一个协议方法声明来实现一个方法. 协议方法声明和函数方法声明有着相同的形式,包含如下两条规则:他们不包括函数体,你不能在类的声明内为他们的 参数提供初始值.举例来说,符合的类型执行协议必需的方法。参见必需方法一节。

使用关键字class可以在协议声明中声明一个类或必需的静态方法。执行这些方法的类也用关键字class声明。 相反的,执行这些方法的结构体必须以关键字static声明。如果你想使用扩展方法,在扩展类时使用class关键字, 在扩展结构体时使用static关键字。

更多请参阅函数声明。

GRAMMAR OF A PROTOCOL METHOD DECLARATION
protocol-method-declaration → function-head­function-name­generic-parameter-clause­opt­function-signature­

协议构造器声明

协议声明了一致性类型必须在协议声明的主体里通过引入一个协议构造器声明来实现一个构造器。协议构造器声明 除了不包含构造器体外,和构造器声明有着相同的形式,
更多请参阅构造器声明。

GRAMMAR OF A PROTOCOL INITIALIZER DECLARATION
protocol-initializer-declaration → initializer-head­generic-parameter-clause­opt­parameter-clause­

协议附属脚本声明

协议声明了一致性类型必须在协议声明的主体里通过引入一个协议附属脚本声明来实现一个附属脚本。协议属性声明 对附属脚本声明有一个特殊的形式:

subscript (parameters) -> return type { get set }

附属脚本声明只为和协议一致的类型声明了必需的最小数量的的getter和setter。如果附属脚本申明包含get和set关键字, 一致的类型也必须有一个getter和setter语句。如果附属脚本声明值包含get关键字,一致的类型必须至少包含一个 getter语句,可以选择是否包含setter语句。

更多参阅附属脚本声明。

GRAMMAR OF A PROTOCOL SUBSCRIPT DECLARATION
protocol-subscript-declaration → subscript-head­subscript-result­getter-setter-keyword-block­

协议相关类型声明

协议声明相关类型使用关键字typealias。相关类型为作为协议声明的一部分的类型提供了一个别名。相关类型和参数 语句中的类型参数很相似,但是它们在声明的协议中包含self关键字。在这些语句中,self指代和协议一致的可能的类型。 获得更多信息和例子,查看相关类型或类型别名声明。

GRAMMAR OF A PROTOCOL ASSOCIATED TYPE DECLARATION
protocol-associated-type-declaration → typealias-head­type-inheritance-clause­opt­typealias-assignment­opt­

构造器声明

构造器声明会为程序内的类,结构体或枚举引入构造器。构造器使用关键字Init来声明,遵循两条基本形式。

结构体,枚举,类可以有任意数量的构造器,但是类的构造器的规则和行为是不一样的。不像结构体和枚举那样,类 有两种结构体,designed initializers 和convenience initializers,参见构造器一节。

如下的形式声明了结构体,枚举和类的指定构造器:


1
. init(parameters) { statements }

类的指定构造器将类的所有属性直接初始化。如果类有超类,它不能调用该类的其他构造器,它只能调用超类的一个 指定构造器。如果该类从它的超类处继承了任何属性,这些属性在当前类内被赋值或修饰时,必须带哦用一个超类的 指定构造器。

指定构造器可以在类声明的上下文中声明,因此它不能用扩展声明的方法加入一个类中。

结构体和枚举的构造器可以带哦用其他的已声明的构造器,来委托其中一个火全部进行初始化过程。

以关键字convenience来声明一个类的便利构造器:


1
. convenience init(parameters) { statements }

便利构造器可以将初始化过程委托给另一个便利构造器或类的一个指定构造器。这意味着,类的初始化过程必须 以一个将所有类属性完全初始化的指定构造器的调用作为结束。便利构造器不能调用超类的构造器。

你可以使用requierd关键字,将便利构造器和指定构造器标记为每个子类的构造器都必须拥有的。因为指定构造器 不被子类继承,他们必须被立即执行。当子类直接执行所有超类的指定构造器(或使用便利构造器重写指定构造器)时, 必需的便利构造器可以被隐式的执行,亦可以被继承。不像方法,附属脚本那样,你不需要为这些重写的构造器标注 overrride关键字。

查看更多关于不同声明方法的构造器的例子,参阅构造过程一节。

GRAMMAR OF AN INITIALIZER DECLARATION
initializer-declaration → initializer-head­generic-parameter-clause­opt­parameter-clause­initializer-body­ initializer-head → attributes­opt­convenience­opt­init­ initializer-body → code-block­

析构声明

析构声明为类声明了一个析构器。析构器没有参数,遵循如下的格式:


1
. deinit { statements }

当类没有任何语句时将要被释放时,析构器会自动的被调用。析构器在类的声明体内只能被声明一次——但是不能在 类的扩展声明内,每个类最多只能有一个。

子类继承了它的超类的析构器,在子类将要被释放时隐式的调用。子类在所有析构器被执行完毕前不会被释放。

析构器不会被直接调用。

查看例子和如何在类的声明中使用析构器,参见析构过程一节 。

GRAMMAR OF A DEINITIALIZER DECLARATION
deinitializer-declaration → attributes­opt­deinit­code-block

扩展声明

扩展声明用于扩展一个现存的类,结构体,枚举的行为。扩展声明以关键字extension开始,遵循如下的规则:



1
. extension type: adopted protocols { declarations }

一个扩展声明体包括零个或多个声明。这些声明可以包括计算型属性,计算型静态属性,实例方法,静态和类方法,构造器, 附属脚本声明,甚至其他结构体,类,和枚举声明。扩展声明不能包含析构器,协议声明,存储型属性,属性监测器或其他 的扩展属性。详细讨论和查看包含多种扩展声明的实例,参见扩展一节。

扩展声明可以向现存的类,结构体,枚举内添加一致的协议。扩展声明不能向一个类中添加继承的类,因此 type-inheritance-clause是一个只包含协议列表的扩展声明。

属性,方法,现存类型的构造器不能被它们类型的扩展所重写。

扩展声明可以包含构造器声明,这意味着,如果你扩展的类型在其他模块中定义,构造器声明必须委托另一个在 那个模块里声明的构造器来恰当的初始化。

GRAMMAR OF AN EXTENSION DECLARATION
extension-declaration → extension­type-identifier­type-inheritance-clause­opt­extension-body­ extension-body → {­declarations­opt­}­

附属脚本声明(translated by 林)

附属脚本用于向特定类型添加附属脚本支持,通常为访问集合,列表和序列的元素时提供语法便利。附属脚本声明使用关键字subscript,声明形式如下:

subscript (parameter) -> (return type){ get{ statements } set(setter name){ statements } } 附属脚本声明只能在类,结构体,枚举,扩展和协议声明的上下文进行声明。

变量(parameters)指定一个或多个用于在相关类型的附属脚本中访问元素的索引(例如,表达式object[i]中的i)。尽管用于元素访问的索引可以是任意类型的,但是每个变量必须包含一个用于指定每种索引类型的类型标注。返回类型(return type)指定被访问的元素的类型。

和计算性属性一样,附属脚本声明支持对访问元素的读写操作。getter用于读取值,setter用于写入值。setter子句是可选的,当仅需要一个getter子句时,可以将二者都忽略且直接返回请求的值即可。也就是说,如果使用了setter子句,就必须使用getter子句。

setter的名字和封闭的括号是可选的。如果使用了setter名称,它会被当做传给setter的变量的名称。如果不使用setter名称,那么传给setter的变量的名称默认是value。setter名称的类型必须与返回类型(return type)的类型相同。

可以在附属脚本声明的类型中,可以重载附属脚本,只要变量(parameters)或返回类型(return type)与先前的不同即可。此时,必须使用override关键字声明那个被覆盖的附属脚本。(注:好乱啊!到底是重载还是覆盖?!)

同样可以在协议声明的上下文中声明附属脚本,Protocol Subscript Declaration中有所描述。

更多关于附属脚本和附属脚本声明的例子,请参考Subscripts

GRAMMAR OF A SUBSCRIPT DECLARATION
subscript-declaration → subscript-head­subscript-result­code-block­ subscript-declaration → subscript-head­subscript-result­getter-setter-block­ subscript-declaration → subscript-head­subscript-result­getter-setter-keyword-block­ subscript-head → attributes­opt­subscript­parameter-clause­ subscript-result → ->­attributes­opt­type­

运算符声明(translated by 林)

运算符声明会向程序中引入中缀、前缀或后缀运算符,它使用上下文关键字operator声明。 可以声明三种不同的缀性:中缀、前缀和后缀。操作符的缀性描述了操作符与它的操作数的相对位置。 运算符声明有三种基本形式,每种缀性各一种。运算符的缀性通过在operator和运算符之间添加上下文关键字infix,prefix或postfix来指定。每种形式中,运算符的名字只能包含Operators中定义的运算符字符。

下面的这种形式声明了一个新的中缀运算符:

operator infix operator name{ precedence precedence level associativity associativity }

中缀运算符是二元运算符,它可以被置于两个操作数之间,比如表达式1 + 2 中的加法运算符(+)。

中缀运算符可以可选地指定优先级,结合性,或两者同时指定。

运算符的优先级可以指定在没有括号包围的情况下,运算符与它的操作数如何紧密绑定的。可以使用上下文关键字precedence并优先级(precedence level)一起来指定一个运算符的优先级。优先级可以是0到255之间的任何一个数字(十进制整数);与十进制整数字面量不同的是,它不可以包含任何下划线字符。尽管优先级是一个特定的数字,但它仅用作与另一个运算符比较(大小)。也就是说,一个操作数可以同时被两个运算符使用时,例如2 + 3 * 5,优先级更高的运算符将优先与操作数绑定。

运算符的结合性可以指定在没有括号包围的情况下,优先级相同的运算符以何种顺序被分组的。可以使用上下文关键字associativity并结合性(associativity)一起来指定一个运算符的结合性,其中结合性可以说是上下文关键字left,right或none中的任何一个。左结合运算符以从左到右的形式分组。例如,减法运算符(-)具有左结合性,因此4 - 5 - 6被以(4 - 5) - 6的形式分组,其结果为-7。 右结合运算符以从右到左的形式分组,对于设置为none的非结合运算符,它们不以任何形式分组。具有相同优先级的非结合运算符,不可以互相邻接。例如,表达式1 < 2 < 3非法的。

声明时不指定任何优先级或结合性的中缀运算符,它们的优先级会被初始化为100,结合性被初始化为none。

下面的这种形式声明了一个新的前缀运算符:

operator prefix operator name{}

紧跟在操作数前边的前缀运算符(prefix operator)是一元运算符,例如表达式++i中的前缀递增运算符(++)。

前缀运算符的声明中不指定优先级。前缀运算符是非结合的。

下面的这种形式声明了一个新的后缀运算符:

operator postfix operator name{}

紧跟在操作数后边的后缀运算符(postfix operator)是一元运算符,例如表达式i++中的前缀递增运算符(++)。

和前缀运算符一样,后缀运算符的声明中不指定优先级。后缀运算符是非结合的。

声明了一个新的运算符以后,需要声明一个跟这个运算符同名的函数来实现这个运算符。如何实现一个新的运算符,请参考Custom Operators

GRAMMAR OF AN OPERATOR DECLARATION
operator-declaration → prefix-operator-declaration­ postfix-operator-declaration­ >infix-operator-declaration­ prefix-operator-declaration → operator ­prefix­ operator­{­}­ postfix-operator-declaration → operator ­postfix­ operator­{­}­ infix-operator-declaration → operator­infix­operator­{­infix-operator-attributes­opt­}­ infix-operator-attributes → precedence-clause­opt­associativity-clause­opt­ precedence-clause → precedence­precedence-level­ precedence-level → Digit 0 through 255 associativity-clause → associativity­associativity­ associativity → left­ right­ none



作者:iOS鑫
链接:https://www.jianshu.com/p/6638bbb73c14


收起阅读 »

Swift声明参考-超详细(上)

一条声明可以在你的程序里引入新的名字和构造。举例来说,你可以使用声明来引入函数和方法,变量和常量,或者来定义 新的命名好的枚举,结构,类和协议类型。你也可以使用一条声明来延长一个已经存在的命名好的类型的行为。或者在你的 程序里引入在其他地方声明的符号。在swi...
继续阅读 »
一条声明可以在你的程序里引入新的名字和构造。举例来说,你可以使用声明来引入函数和方法,变量和常量,或者来定义 新的命名好的枚举,结构,类和协议类型。你也可以使用一条声明来延长一个已经存在的命名好的类型的行为。或者在你的 程序里引入在其他地方声明的符号。

在swift中,大多数声明在某种意义上讲也是执行或同事声明它们的初始化定义。这意味着,因为协议和他们的成员不匹配, 大多数协议成员需要单独的声明。为了方便起见,也因为这些区别在swift里不是很重要,声明语句同时包含了声明和定义。

GRAMMAR OF A DECLARATION
declaration → import-declaration­
declaration → constant-declaration­
declaration → variable-declaration­
declaration → typealias-declaration­
declaration → function-declaration­
declaration → enum-declaration­
declaration → struct-declaration­
declaration → class-declaration­
declaration → protocol-declaration­
declaration → initializer-declaration­
declaration → deinitializer-declaration­
declaration → extension-declaration­
declaration → subscript-declaration­
declaration → operator-declaration­
declarations → declaration­declarations­opt­
declaration-specifiers → declaration-specifier­declaration-specifiers­opt­
declaration-specifier → class­ | mutating ­| nonmutating­ | override­ | static­ | unowned |

模块范围

模块范围定义了对模块中其他源文件可见的代码。(注:待改进)在swift的源文件中,最高级别的代码由零个或多个语句, 声明和表达组成。变量,常量和其他的声明语句在一个源文件的最顶级被声明,使得他们对同一模块中的每个源文件都是可见的。

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

GRAMMAR OF A TOP-LEVEL DECLARATION
top-level-declaration → statements ­opt

代码块

代码块用来将一些声明和控制结构的语句组织在一起。它有如下的形式:


1.
{ `statements` }

代码块中的语句包括声明,表达式和各种其他类型的语句,它们按照在源码中的出现顺序被依次执行。

GRAMMAR OF A CODE BLOCK
code-block → ­statements ­opt­

引入声明

引入声明使你可以使用在其他文件中声明的内容。引入语句的基本形式是引入整个代码模块;它由import关键字开始,后面 紧跟一个模块名:


1.
import module


你可以提供更多的细节来限制引入的符号,如声明一个特殊的子模块或者在一个模块或子模块中做特殊的声明。(待改进) 当你使用了这些细节后,在当前的程序汇总只有引入的符号是可用的(并不是声明的整个模块)。


1.
import import kind module.symbol name import module.submodule
GRAMMAR OF AN IMPORT DECLARATION
import-declaration → attributes ­opt ­import­ import-kind­ opt import-path­ import-kind → typealias­ | struct­ | class­ | enum­ | protocol­ | var­ | func­ import-path → import-path-identifier­ import-path-identifier­.­import-path­ import-path-identifier → identifier­ operator

常量声明

常量声明可以在你的程序里命名一个常量。常量以关键词let来声明,遵循如下的格式:



1. let constant name: type = expression

当常量的值被给定后,常量就将常量名称和表达式初始值不变的结合在了一起,而且不能更改。 这意味着如果常量以类的形式被初始化,类本身的内容是可以改变的,但是常量和类之间的结合关系是不能改变的。 当一个常量被声明为全局变量,它必须被给定一个初始值。当一个常量在类或者结构体中被声明时,他被认为是一个常量 属性。常量并不是可计算的属性,因此不包含getters和setters。(译者注:getters和setters不知道怎么翻译,待改进)

如果常量名是一个元祖形式,元祖中的每一项初始化表达式中都要有对应的值



1. let (firstNumber, secondNumber) = (10, 42)
在上例中,firstNumber是一个值为10的常量,secnodeName是一个值为42的常量。所有常量都可以独立的使用:


1.
1 println("The first number is \(firstNumber).")
2 // prints "The first number is 10."
3 println("The second number is \(secondNumber).")
4 // prints "The second number is 42."

类型注释(:type)在常量声明中是一个可选项,它可以用来描述在类型接口(type inference)中找到的类型。

声明一个静态常量要使用关键字static。静态属性在类型属性(type propetries)中有介绍。

如果还想获得更多关于常量的信息或者想在使用中获得帮助,请查看常量和变量(constants and variables), 存储属性(stored properties)等节。

GRAMMAR OF A CONSTANT DECLARATION
constant-declaration → attributes­ opt ­declaration-specifiers­ opt ­let­pattern-initializer-list­ pattern-initializer-list → pattern-initializer­ | pattern-initializer­ , pattern-initializer-list­ pattern-initializer → pattern ­initializer ­opt­ initializer → =­expression

变量声明

变量声明可以在你的程序里声明一个变量,它以关键字var来声明。根据声明变量类型和值的不同,如存储和计算 变量和属性,存储变量和属性监视,和静态变量属性,有着不同的声明形式。(待改进) 所使用的声明形式取决于变量所声明的范围和你打算声明的变量类型。

注意:你也可以在协议声明的上下文声明属性,详情参见类型属性声明。

存储型变量和存储型属性

下面的形式声明了一个存储型变量或存储型变量属性


1.
var variable name: type = expression

你可以在全局,函数内,或者在类和结构体的声明(context)中使用这种形式来声明一个变量。当变量以这种形式 在全局或者一个函数内被声明时,它代表一个存储型变量。当他在类或者结构体中被声明时,他代表一个存储型变量属性。

构造器表达式可以和常量声明相比,如果变量名是一个元祖类型,元祖的每一项的名字都要和初始化表达式一致。

正如名字一样,存储型变量的值或存储型变量属性存储在内存中。

计算型变量和计算型属性

如下形式声明一个一个存储型变量或存储型属性:


1.
var variable name: type { get { statements } set(setter name) { statements } }

你可以在全局,函数体内或者类,结构体,枚举,扩展声明的上下文中使用这种形式的声明。 当变量以这种形式在全局或者一个函数内被声明时,它代表一个计算型变量。当他在类,结构体,枚举,扩展声明的上下文 中中被声明时,他代表一个计算型变量属性。

getter用来读取变量值,setter用来写入变量值。setter子句是可选择的,只有getter是必需的,你可以将这些语句 都省略,只是简单的直接返回请求值,正如在只读计算属性(read-only computed properites)中描述的那样。 但是如果你提供了一个setter语句,你也必需提供一个getter语句。

setter的名字和圆括号内的语句是可选的。如果你写了一个setter名,它就会作为setter的参数被使用。如果你不写setter名, setter的初始名为newValue,正如在seter声明速记(shorthand setter declaration)中提到的那样。

不像存储型变量和存储型属性那样,计算型属性和计算型变量的值不存储在内存中。

获得更多信息,查看更多关于计算型属性的例子,请查看计算型属性(computed properties)一节。

存储型变量监视器和属性监视器

你可以用willset和didset监视器来声明一个存储型变量或属性。一个包含监视器的存储型变量或属性按如下的形式声明:


1
. var variable name: type = expression { willSet(setter name) { statements } didSet(setter name { statements } }

你可以在全局,函数体内或者类,结构体,枚举,扩展声明的上下文中使用这种形式的声明。 当变量以这种形式在全局或者一个函数内被声明时,监视器代表一个存储型变量监视器; 当他在类,结构体,枚举,扩展声明的上下文中被声明时,监视器代表属性监视器。

你可以为适合的监视器添加任何存储型属性。你也可以通过重写子类属性的方式为适合的监视器添加任何继承的属性 (无论是存储型还是计算型的),参见重写属性监视器(overriding properyt observers)。

初始化表达式在类或者结构体的声明中是可选的,但是在其他地方是必需的。无论在什么地方声明, 所有包含监视器的变量声明都必须有类型注释(type annotation)。

当变量或属性的值被改变时,willset和didset监视器提供了一个监视方法(适当的回应)。 监视器不会在变量或属性第一次初始化时不会被运行,他们只有在值被外部初始化语句改变时才会被运行。

willset监视器只有在变量或属性值被改变之前运行。新的值作为一个常量经过过willset监视器,因此不可以在 willset语句中改变它。didset监视器在变量或属性值被改变后立即运行。和willset监视器相反,为了以防止你仍然 需要获得旧的数据,旧变量值或者属性会经过didset监视器。这意味着,如果你在变量或属性自身的didiset监视器语句 中设置了一个值,你设置的新值会取代刚刚在willset监视器中经过的那个值。

在willset和didset语句中,setter名和圆括号的语句是可选的。如果你写了一个setter名,它就会作为willset和didset的参数被使用。如果你不写setter名, willset监视器初始名为newvalue,didset监视器初始名为oldvalue。

当你提供一个willset语句时,didset语句是可选的。同样的,在你提供了一个didset语句时,willset语句是可选的。

获得更多信息,查看如何使用属性监视器的例子,请查看属性监视器(prpperty observers)一节。

类和静态变量属性

class关键字用来声明类的计算型属性。static关键字用来声明类的静态变量属性。类和静态变量在类型属性(type properties)中有详细讨论。

GRAMMAR OF A VARIABLE DECLARATION
variable-declaration → variable-declaration-head­pattern-initializer-list­
variable-declaration → variable-declaration-head ­variable-name ­type-annotation ­code-block­
variable-declaration → variable-declaration-head ­variable-name ­type-annotation ­getter-setter-block­
variable-declaration → variable-declaration-head ­variable-name­ type-annotation ­getter-setter-keyword-block­
variable-declaration → variable-declaration-head­ variable-name ­type-annotation­initializer­ opt ­willSet-didSet-block­
variable-declaration-head → attributes ­opt­ declaration-specifiers ­opt ­var ­ variable-name → identifier­
getter-setter-block → {­getter-clause ­setter-clause­ opt­}­
getter-setter-block → {­setter-clause ­getter-clause­}­
getter-clause → attributes ­opt­get­code-block­
setter-clause → attributes ­opt ­set­ setter-name­ opt­ code-block­
setter-name → (­identifier­)­
getter-setter-keyword-block → {­getter-keyword-clause ­setter-keyword-clause­ opt­} ­ getter-setter-keyword-block → {­setter-keyword-clause ­getter-keyword-clause­}
getter-keyword-clause → attributes­ opt­ get­
setter-keyword-clause → attributes ­opt­ set­
willSet-didSet-block → {­willSet-clause ­didSet-clause ­opt­}­
willSet-didSet-block → {­didSet-clause ­willSet-clause­}­
willSet-clause → attributes ­opt ­willSet ­setter-name­ opt ­code-block­
didSet-clause → attributes ­opt ­didSet ­setter-name ­opt­ code-block­

类型的别名声明

类型别名的声明可以在你的程序里为一个已存在的类型声明一个别名。类型的别名声明以关键字typealias开始,遵循如下的 形式:



1. typealias name = existing type

当一个类型被别名被声明后,你可以在你程序的任何地方使用别名来代替已存在的类型。已存在的类型可以是已经被命名的 类型或者是混合类型。类型的别名不产生新的类型,它只是简单的和已存在的类型做名称替换。

查看更多Protocol Associated Type Declaration.

GRAMMAR OF A TYPE ALIAS DECLARATION
typealias-declaration → typealias-head­ typealias-assignment typealias-head → typealias­ typealias-name typealias-name → identifier typealias-assignment → =type

函数声明

你可以使用函数声明在你的程序里引入新的函数。函数可以在类的上下文,结构体,枚举,或者作为方法的协议中被声明。 函数声明使用关键字func,遵循如下的形式:


1
. func function name(parameters) -> return type { statements }

如果函数不返回任何值,返回类型可以被忽略,如下所示:


1.
func function name(parameters) { statements }

每个参数的类型都要标明,它们不能被推断出来。初始时函数的参数是常值。在这些参数前面添加var使它们成为变量, 作用域内任何对变量的改变只在函数体内有效,或者用inout使的这些改变可以在调用域内生效。 更多关于in-out参数的讨论,参见in-out参数(in-out parameters)

函数可以使用元组类型作为返回值来返回多个变量。

函数定义可以出现在另一个函数声明内。这种函数被称作nested函数。更多关于nested函数的讨论,参见nestde functions。

参数名

函数的参数是一个以逗号分隔的列表 。函数调用是的变量顺序必须和函数声明时的参数顺序一致。 最简单的参数列表有着如下的形式:



1. parameter name: parameter type
对于函数参数来讲,参数名在函数体内被使用,而不是在函数调用时使用。对于方法参数,参数名在函数体内被使用, 同时也在方法被调用时作为标签被使用。该方法的第一个参数名仅仅在函数体内被使用,就像函数的参数一样,举例来讲:


1
. func f(x: Int, y: String) -> String { return y + String(x) } f(7, "hello") // x and y have no name class C { func f(x: Int, y: String) -> String { return y + String(x) } } let c = C() c.f(7, y: "hello") // x没有名称,y有名称

你可以按如下的形式,重写参数名被使用的过程:



1. external parameter name local parameter name: parameter type #parameter name: parameter type _ local parameter name: parameter type

在本地参数前命名的第二名称(second name)使得参数有一个扩展名。且不同于本地的参数名。 扩展参数名在函数被调用时必须被使用。对应的参数在方法或函数被调用时必须有扩展名 。

在参数名前所写的哈希符号(#)代表着这个参数名可以同时作为外部或本体参数名来使用。等同于书写两次本地参数名。 在函数或方法调用时,与其对应的语句必须包含这个名字。

本地参数名前的强调字符(_)使参数在函数被调用时没有名称。在函数或方法调用时,与其对应的语句必须没有名字。

特殊类型的参数

参数可以被忽略,值可以是变化的,并且提供一个初始值,这种方法有着如下的形式:


1.
_ : <#parameter type#. parameter name: parameter type... parameter name: parameter type = default argument value

以强调符(_)命名的参数明确的在函数体内不能被访问。

一个以基础类型名的参数,如果紧跟着三个点(...),被理解为是可变参数。一个函数至多可以拥有一个可变参数, 且必须是最后一个参数。可变参数被作为该基本类型名的数组来看待。举例来讲,可变参数int...被看做是int[]。 查看可变参数的使用例子,详见可变参数(variadic parameters)一节。

在参数的类型后面有一个以等号(=)连接的表达式,这样的参数被看做有着给定表达式的初试值。如果参数在函数 调用时被省略了,就会使用初始值。如果参数没有胜率,那么它在函数调用是必须有自己的名字.举例来讲, f()和f(x:7)都是只有一个变量x的函数的有效调用,但是f(7)是非法的,因为它提供了一个值而不是名称。

特殊方法

以self修饰的枚举或结构体方法必须以mutating关键字作为函数声明头。

子类重写的方法必须以override关键字作为函数声明头。不用override关键字重写的方法,使用了override关键字 却并没有重写父类方法都会报错。

和类型相关而不是和类型实例相关的方法必须在static声明的结构以或枚举内,亦或是以class关键字定义的类内。

柯里化函数和方法

柯里化函数或方法有着如下的形式:


1.
func function name(parameters)(parameters) -> return type { statements }

以这种形式定义的函数的返回值是另一个函数。举例来说,下面的两个声明时等价的:


1
. func addTwoNumbers(a: Int)(b: Int) -> Int { return a + b } func addTwoNumbers(a: Int) -> (Int -> Int) { func addTheSecondNumber(b: Int) -> Int { return a + b } return addTheSecondNumber } addTwoNumbers(4)(5) // Returns 9

多级柯里化应用如下

GRAMMAR OF A FUNCTION DECLARATION
function-declaration → function-head­ function-name­ generic-parameter-clause ­opt­function-signature­ function-body­ function-head → attributes ­opt ­declaration-specifiers ­opt ­func­ function-name → identifier­ operator­ function-signature → parameter-clauses ­function-result ­opt­ function-result → ->­attributes ­opt ­type­ function-body → code-block­ parameter-clauses → parameter-clause ­parameter-clauses ­opt­ parameter-clause → (­)­ (­parameter-list­...­opt­)­ parameter-list → parameter­ parameter­,­parameter-list­ parameter → inout ­opt ­let ­opt­#­opt­parameter-name local-parameter-name ­opt­ type-annotation ­default-argument-clause ­opt­ parameter → inout­opt­var­#­opt­parameter-name­local-parameter-name ­opt­ type-annotation­default-argument-clause ­opt­ parameter → attributes ­opt ­type­ parameter-name → identifier­ _­ local-parameter-name → identifier­ _­ default-argument-clause → =­expression­:




收起阅读 »

XCode 使用 PMD 扫描重复代码

使用  HomeBrew 安装 PMDbrew install pmd在 Xcode 的 Build Phases 中,我们增加一个新的 Run Script#检测swi...
继续阅读 »

使用  HomeBrew 安装 PMD

brew install pmd


在 Xcode 的 Build Phases 中,我们增加一个新的 Run Script

#检测swift代码
#pmd cpd --files ${EXECUTABLE_NAME} --minimum-tokens 50 --language swift --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer > cpd-output.xml --failOnViolation true

#检测objective-c代码
pmd cpd --files ${EXECUTABLE_NAME} --minimum-tokens 20 --language objectivec --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer > cpd-output.xml --failOnViolation true

# Running script
php ./cpd_script.php -cpd-xml cpd-output.xml






上面的脚本中使用 pmd 进行复制粘贴检查, 检查结果会存放在 cpd-output.xml 文件中。
--minimum-tokens 的值是一个经验值,这个需要根据具体情况确定。

另外脚本中需要 php 对输出脚本进行分析并显示到 Xcode 中,如果没有安装 php,则需要安装。安装PHP参考文章

MAC HomeBrew 安装 php

还有需要的 cpd_script.php 文件, 需要放到工程根目录,它的作用是利用之前生成的检查结果文件,将检查的结果显示到 Xcode 中,在工程主目录下,创建 cpd_script.php 文件

<?php
foreach (simplexml_load_file('cpd-output.xml')->duplication as $duplication) {
$files = $duplication->xpath('file');
foreach ($files as $file) {
echo $file['path'].':'.$file['line'].':1: warning: '.$duplication['lines'].' copy-pasted lines from: '
.implode(', ', array_map(function ($otherFile) { return $otherFile['path'].':'.$otherFile['line']; },
array_filter($files, function ($f) use (&$file) { return $f != $file; }))).PHP_EOL;
}
}
?>

php 脚本可以参考: https://link.jianshu.com/?t=https://krakendev.io/blog/generating-warnings-in-xcode

测试

新建空的 demo 工程,command + b 编译项目,工程目录下会出现 cpd-output.xml 文件,此时文件内容为空




在项目中添加相同代码



再次编译项目,cpd-output.xml 文件则会出现重复代码提示信息,项目中代码也同样会给出警告提示信息








作者:gaookey
链接:https://www.jianshu.com/p/04a2a93d7b52
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。




收起阅读 »

ConstraintLayout2.0一篇写不完之嵌套滚动怎么滚

在ConstraintLayout1.x阶段,它主要提供的能力是对静态布局的支撑,那么到2.x之后,MotionLayout的拓展,让它对动态布局的支持有了进一步的优化,在1.x阶段不能实现的嵌套滚动布局布局方式,现在也就非常简单了。在没有Constraint...
继续阅读 »

在ConstraintLayout1.x阶段,它主要提供的能力是对静态布局的支撑,那么到2.x之后,MotionLayout的拓展,让它对动态布局的支持有了进一步的优化,在1.x阶段不能实现的嵌套滚动布局布局方式,现在也就非常简单了。

在没有ConstraintLayout的时候,要实现嵌套滚动布局,通常都是使用CoordinatorLayout来实现,但是这个东西的使用局限性比较大,能非常简单的实现的嵌套布局,就那么几种,如果要实现一些特别的滚动效果,就需要自定义behavior来实现,这样一来,嵌套滚动布局就成了一个比较复杂的布局方式了,而MotionLayout的出现,就可以完美的解决这样一个布局难题。

在ConstraintLayout2.x中,有两种方式来实现嵌套滚动布局。

CoordinatorLayout配合MotionLayout

这种方式实际上还是借助CoordinatorLayout,是一种比较早期的实现方案,如果是对CoordinatorLayout比较熟悉的开发者,可以很快改造现有代码来适配MotionLayout的嵌套滚动。

这种方案的布局结构如下:

CoordinatorLayout

--------AppBarLayout

----------------MotionLayout

--------NestedScrollView

可以发现,这种方式,实际上就是利用MotionLayout来替代之前在AppBarLayout里面的CollapsingToolbarLayout,借助MotionLayout来实现之前CollapsingToolbarLayout的一些折叠效果。

这种方式的一般套路结构如下。

image-20210223105619990

在AppBarLayout中,我们通过MotionLayout控制动画效果。

那么在这里,一般又有两个套路,一是直接使用MotionLayout,然后在代码里面通过AppBarLayout.OnOffsetChangedListener的回调,设置MotionLayout的progress,另一种是直接自定义MotionLayout,实现AppBarLayout.OnOffsetChangedListener,这样通用性比较强,示例如下。

class CollapsibleToolbar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {

override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
progress = -verticalOffset / appBarLayout?.totalScrollRange?.toFloat()!!
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
(parent as? AppBarLayout)?.addOnOffsetChangedListener(this)
}
}

这两种方式没有本质上的不同,但是对于MotionEditor来说,如果使用自定义的MotionLayout,在非根布局下创建约束的时候会有一些问题(修改属性也会存在一些问题),所以,如果使用自定义MotionLayout的话,建议通过include的方式,引用新的根布局为自定义MotionLayout的方式来使用,而直接使用MotionLayout的方式,则没有这个限制,希望MotionEditor能早日改善这个问题。PS:好消息,Android Studio Arctic Fox已经修复了这个问题。

单纯MotionLayout实现

MotionLayout的出现,就是为了能替代动态的布局模型,所以,如果还使用CoordinatorLayout,那么就违背了开发者的初心了,所以,我们的目的就是去除CoordinatorLayout,而仅使用MotionLayout来实现嵌套滚动效果,实现滚动布局的大一统。

这种套路的一般结构如下所示。

MotionLayout

--------MotionLayout

--------NestedScrollView

我们可以发现,这里有两层MotionLayout,外层的MotionLayout,用于控制头部的伸缩布局,而内部的MotionLayout,则用于控制头部的滚动时效果。

这样一来,整个嵌套滚动的格局一下子就打开了,再也没了之前使用CoordinatorLayout的高度限制,效果限制,所有的内容,都可以通过约束来进行设置,再通过MotionLayout来进行动态约束,从而实现嵌套滚动布局。

对于外层的MotionLayout,它的Scene提供两个能力,一个是控制头部从200dp,变为56dp,即提供一个伸缩的功能,另一个重要的而且很容易被忽视的作用,就是给内层MotionLayout提供progress数据,有了这个progress,内部MotionLayout才能联动,这个和使用CoordinatorLayout配合MotionLayout使用要设置progress是一个道理。

我们来看下最外层的Scene,代码如下所示。

<?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">

<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@+id/start">

<OnSwipe
motion:dragDirection="dragUp"
motion:touchAnchorId="@+id/motionLayout"
motion:touchAnchorSide="bottom" />
</Transition>

<ConstraintSet android:id="@+id/start">
<ConstraintOverride
android:id="@id/motionLayout"
android:layout_height="200dp"
motion:motionProgress="0" />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<ConstraintOverride
android:id="@id/motionLayout"
android:layout_height="56dp"
motion:motionProgress="1" />
</ConstraintSet>
</MotionScene>

对于非layout_constraintXXX_toXXXOf的约束,可以使用ConstraintOverride来直接覆写,这样可以少写很多重复的约束,这里的约束改变实际上只有两个,即layout_height从200变为56,而另一个重要的点,就是motionProgress的指定,motionProgress的作用就是设置motionProgress,如果不设置这个,那么progress数据是没办法传递到内部MotionLayout的,从而会导致内部无法联动。

解决完外部的MotionLayout之后,内部的MotionLayout就迎刃而解了,因为它真的就是一个平平常常的MotionLayout,你想要对它内部的元素做任何的改动,都和之前直接使用MotionLayout没有任何区别。

我们来看这个简单的例子,如图所示。

image-20210817161849272

头部伸缩配上文字的移动,一个简简单单的类CoordinatorLayout布局,外部的Scene我们已经解决了,再来看看内部的Scene,算了不看了,没什么必要,就是简单的体力劳动。

整个套路的布局结构如下所示。

image-20210817162156160

总体看来,MotionLayout是不是实现了大一统,它将滚动的布局效果,转化为了多层MotionLayout的Scene分解,利用progress串联起来,设计思路不可谓不精,一旦你熟练掌握了MotionLayout的各种基础布局,那么即使再复杂的布局,也能分而治之。

收起阅读 »

面试官问:我们聊聊原型和继承?我:这里边水深,我把握不住。。。

前言 原型和继承一直是js中非常基础和重要的部分,我们来看看日常开发中经常会用到的原型和继承。 class Person extends React.Component { componentDidMount() {} render() {...
继续阅读 »

前言


原型和继承一直是js中非常基础和重要的部分,我们来看看日常开发中经常会用到的原型和继承。


  class Person extends React.Component {
componentDidMount() {}
render() {}
}

这行代码代码大家都很熟悉,Person通过extends关键字继承了React的特性,componentDidMount和render在class类中的是一个普通定义好的函数。特殊的是,它们也是在Component中提前定义好的钩子函数,用于在某个固定的时机触发。


看完了基本的使用,下面我们一起来深入探索下class和extends。


class只是一个语法糖


class是ES6中引入的概念,我们也称它为类。class的用途是作为对象模版,用来创建对象。但需要明确的是,class只是一个语法糖,它内部实现上还是和ES5创建对象是相同的。由于class的写法更加符合面向对象编程的习惯,所以被推广使用,逐步替代了ES5中的对象创建。


   console.log(typeof React.Component); // function

ES5是通过构造函数函数来创建对象,React.Component的类型同样是一个function,所以想要完全搞清楚对象和原型,还是要去学习下ES5中对象的创建。后面有一篇文章是关于ES5中对象的创建和继承,有需要的大家可以自己去看,这里就不展开说了。


class与构造函数的对比


class的本质还是构造函数,但是与构造函数又有些许使用上的不同。


相同点


定义方式


class与构造函数都有两种定义方式,声明和表达式,这两种写法完全等价。且名称都必须大写,以区别于它创建的实例.


  // 函数声明
function Person() {};
// 函数表达式
let Person = function () {};

// 类声明
class Person {}
// 类表达式
let Person = class {};

// 创建实例(函数和类)
let person = new Person();

通过name访问原表达式。


表达式赋值时,可通过name访问原表达式。


  let Student = function Person() {};
// 通过name属性获取类表达式的名称
console.log(Student.name); // Person

let Student = class Person {};
// 通过name属性获取类表达式的名称
console.log(Student.name); // Person

表达式外部,无法访问原表达式


  let Student = function Person() {};
// 外部无法访问类表达式
console.log(Person); // Person is not defined

let Student = class Person {};
// 外部无法访问类表达式
console.log(Person); // Person is not defined

不同点


类不可以变量提升


函数可以变量提升,而类不可以。


  // 声明
console.log(Person); // 报错
console.log(Student); // ƒ Student() {}

class Person {}
function Student() {}

console.log(Person); // class Person {}
console.log(Student); // ƒ Student() {}

// 表达式定义
console.log(Person); // undefined
console.log(Student); // undefined

var Person = class {};
var Student = function () {};

console.log(Person); // class {}
console.log(Student); // ƒ () {}

类受块级作用域限制


  {
class Person {}
function Student() {}
}
console.log(Person); // 报错,Person is not defined
console.log(Student); // ƒ Student() {}

类必须通过new来调用


类必须通过new来调用,否则会报错。构造函数不使用new调用也可以,就会把全局的this作为内部对象。


  function Person() {}
class Animal {}

let p = Person(); // Person内部this指向window
let a = Animal(); // TypeError: class constructor Animal cannot be invoked without 'new'

class的实例化


class实例化的时候,会调用class中的constructor函数。constructor是类的默认方法,如果没有定义,constructor方法会被默认添加。


  class Bar {}
等同于
class Bar {
constructor() {}
}

constructor方法会默认返回一个实例对象(即this),也可以完全返回另一个对象。但返回另一个对象,会导致返回的对象不是Bar的实例(因为它的原型指针没有被更改,具体的原因后面分析)。


  // 返回一个对象
class Bar {
constructor() {
return {
name: 1,
};
}
}

let bar = new Bar();
console.log(bar); // {name: 1}
console.log(bar instanceof Bar); // false

// 返回默认对象
class Bar {
constructor() {}
}

let bar = new Bar();
console.log(bar); // Bar {}
console.log(bar instanceof Bar); // true

前面说到了,如果手动返回了一个对象,会导致返回的对象不是class的实例。那么我们看看生成一个对象的过程是什么样的,为什么手动返回一个对象,这个对象就不是类的实例了。


实例化的过程:



  1. 在内存中创建一个对象

  2. 新对象的__proto__赋值为构造函数的prototype

  3. 构造函数内部的this指向新对象

  4. 执行构造函数内部代码(给新对象添加属性)

  5. 如果构造函数返回非空对象,则返回该对象。否则,则返回新创建的对象。


通过上面的第二步可以看到,原型的赋值作用在新对象上,只有新对象与原型有关系,人为的在constructor返回的对象,与原型毫无关联,自然不是class的实例。


数据共享


定义在constructor中的属性,是每个实例独有的,不会在原型上共享。


  class Person {
constructor() {
this.name = new String("Jack");
// 定义在constructor中的函数是不被原型共享的
this.sayName = () => console.log(this.name);
this.nicknames = ["Jake", "J-Dog"];
}
}
let p1 = new Person();
let p2 = new Person();
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); //false

实例化的时候相当于复制了一个新函数


  class Person {
constructor() {
this.name = new String("Jack");
this.sayName = new Function();
this.nicknames = new Array(["Jake", "J-Dog"]);
}
}

如果想在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。


  class Person {
constructor() {
// 定义在constructor中的方法是属于每个实例的
this.locate = () => console.log("instance");
}
// 定义在类块中的方法是所有实例共享的
test() {
console.log("test");
}
}

let person1 = new Person();
let person2 = new Person();
console.log(person1.locate === person2.locate); // false
console.log(person1.test === person2.test); // true
// 实例中有该属性
console.log(person1.hasOwnProperty("locate")); // true
// 实例中没有该属性
console.log(person1.hasOwnProperty("test")); // false

类的静态方法


类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。


作用


在日常开发中,我会通过类的静态方法去处理一些名称管理和接口,如下面所示:


  class Home {
static getData() {
return [];
}
}
console.log(Home.getData());

静态方法中的this至与类有关


需要注意的是,静态方法不要求存在类的实例,所以this引用类自身,而不是实例。


  class Bar {
static test() {
console.log(this);
}
}
// 类可以直接调用静态方法
Bar.test(); // class Bar {}

var bar = new Bar();
// 实例与静态方法无关
bar.test(); // 报错,bar.test is not a function

静态方法也可被继承


  class Bar {
static test() {
console.log(this);
}
}

class Foo extends Bar {}
Foo.test(); // class Foo extends Bar {}

静态方法也是可以从super对象上调用的


  class Bar {
static test() {
return "test1";
}
}

class Foo extends Bar {
static test2() {
return super.test() + " test2";
}
}
console.log(Foo.test2()); // test1 test2

类中this指向



  1. this存在于类的构造函数中,this指向实例

  2. this存在于类的原型对象上,this指向类的原型

  3. this存在于类的静态方法中,this指向当前类


类的继承


类的继承使用的是新语法,但它的本质依旧是原型链。


ES6中,使用extends关键字,就可以继承任何拥有constructor和原型的对象。所以它不仅可以继承一个类,还可以继承普通的构造函数。


  class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true

super


派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,在类构造函数中使用 super 可以调用父类构造函数。


提炼几个要点:



  1. super关键字只能在派生类的构造函数和静态方法上使用,如下所示,Vehicle不是派生类


  class Vehicle {
constructor() {
// SyntaxError: 'super' keyword unexpected
super();
}
}


  1. 在类构造函数中,不能在调用 super()之前引用 this。


  class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this);
}
}
new Bus();
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor


  1. 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象。


  class Vehicle {}

class Car extends Vehicle {}
console.log(new Car()); // Car {}

class Bus extends Vehicle {
constructor() {
super();
}
}
console.log(new Bus()); // Bus {}

class Van extends Vehicle {
constructor() {
return {};
}
}
console.log(new Van()); // {}

class Test extends Vehicle {
constructor() {}
}
console.log(new Test());
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

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

收起阅读 »

二进制都不了解?也配做什么程序员???

最近在学习一些计算机专业课,学习的过程中二进制的基础是必须要有的,不管是计算机网络,还是组成原理,还是操作系统,都是以二进制为基础的,所以本文总结一些二进制基础。今天,2021年8月30日,写下第一版,后面会陆续增加一些内容,增加一些应用便于更多人理解。 本文...
继续阅读 »

最近在学习一些计算机专业课,学习的过程中二进制的基础是必须要有的,不管是计算机网络,还是组成原理,还是操作系统,都是以二进制为基础的,所以本文总结一些二进制基础。今天,2021年8月30日,写下第一版,后面会陆续增加一些内容,增加一些应用便于更多人理解。


本文目标:



  • 理解的概念

  • 熟记常见的2的次幂,例如128是2的几次幂(2的几次幂就需要多少个二进制位)

  • 理解字节,对于1个字节能存储多少数据做到理性认知

  • 熟记16进制0-16,对应的2进制


带着问题阅读:



  1. 一个ip地址 192.168.1.1共有几位

  2. CSS中的颜色表示 #ffaaff,需要占用多大的存储空间存储

  3. 为什么计算机专业书籍中,表示内存地址大部分都是用16进制表示的,而不是10进制或者2进制

  4. javascirpt中的数字类型在计算机内存储为多少Byte

  5. 宽带的带宽是200M,为什么下载的时候怎么都达不到200M呢


如果所有的问题,你都会,就不用读了,直接退出。


进制


10进制,一位数可以是0-9,共10种可能,如果要表示第11种可能,就要进位。


类比一下,2进制,一位数只能是0或1,有2种可能。


16进制,一位数可以是0-15,有16种可能


10进制的进位规则如下:满10进一位


0  10  20
1 11
2 12
3 13
4 14
5 15
6 16
7 17
8 18
9 19

2进制的进位规则如下:满两位进一位,10进制的0是2进制0,10进制的1是2进制的1,如果要表示10进制的2,就要用两位2进制数,10


0  10  100  1000
1 11 101 1001
110 1010
111 1011
1100
1101
1110
1111


16进制的规则,满16进一位(a表示10进制的10,b:11,c:12...)


0  10(10进制的16)
1 11(10进制的17)
2 12
3
4
5
6
7
8
9
a
b
c
d
e
f

2进制与16进制


一位二进制数,称为1bit。


image.png


1位二进制数,也就是1bit,有2种可能,可以表示数0,1


2位二进制数,2bit,有4种可能(2x2),可以表示数0,1,2,3


3位二进制数,3bit,有8种可能(2x2x2),可以表示数0,1,2,3,4,5,6,7


...


n位二进制数,有 2^n -1 种可能。


有一些常用的2的次幂需要记住,必须记在脑子里,例如看到10进制的128,就想起来是2的7次方,就想起来有7位,0000000


image.png


2进制是计算机用的,人用起来写起来并不方便,所以就有了16进制。


一个16进制,可以表示16种可能性,也就是2的4次方,就是4位2进制数,就是4bit


举个栗子,


16进制是f,表示为2进制就是1111


16进制的ff,表示为2进制就是1111 1111


规律就是,一位16进制,可以用4位2进制来表示。2位16进制,用8位2进制数来表示。


那么16进制的ffffff表示为2进制是多少位呢


字节



字节(英语:Byte),通常用作计算机信息计量单位,不分数据类型。是通信和数据存储的概念。



一个字节能存储8位2进制数据(这个是规范,需要刻在DNA里面)


1Byte =8bit

2^8是256,1个字节能表示的数就是0-255,共256种可能性。


1位16进制数能表示为4位2进制,所以一个字节能表示2个16进制。


总结如下:


1Byte
8bit 1111 1111
2个16进制位 f f

KB,MB,GB,Kb,Mb,Gb


KB(Kilobyte) 千字节,国际单位法一般以1000来定义千,例如1千米=1000米,但是在信息领域,尤其是表示主存储容量时,千字节一般表示1024(2^10)个字节


1KB = 1024 B   2^10 Byte
1MB = 1024 KB 2^20 Byte
1GB = 1024 MB 2^30 Byte

Kb与KB是不同的,Kb是 Kilobit,


1Kb = 1024bit

我们的宽带的带宽是200M每秒,其实是200Mb/s,但是文件是以Byte为单位的,而不是bit,所以需要换算一下


200Mb / 8 = 25 MB

其实能够达到的最高下载速度是25MB/s


简单应用


一个ip地址 192.168.1.1,共32位,why?


因为ip地址是10进制表示的,ip地址用.分开,每一段的范围是0-255,就是2^8,共8位,4*8=32,一共32位。


CSS中的颜色表示 #ffaaff,需要占用多大的存储空间存储


1个Byte存储8位2进制,


1个16进制相当于4位2进制,


所以1个Byte存储2位16进制


#ffaaff存储需要 3Byte


本文就先到这里,后续要有一些内容需要补充,比如按位&``|``!左移右移以及更多的应用(在内存层面的应用,在计算机网络中的应用,在字符编码中的应用等)等我学会了,整理了,补充在这篇文章的后面。


有问题请在评论区提出。


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

收起阅读 »