注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(3)

四、文件处理 1、常规文件处理 1)、读文件 eachLine 方法 我们可以使用 eachLine 方法读该文件中的每一行,它唯一的参数是一个 Closure,Closure 的参数是文件每一行的内容。示例代码如下所示: def file = new Fil...
继续阅读 »

四、文件处理


1、常规文件处理


1)、读文件


eachLine 方法


我们可以使用 eachLine 方法读该文件中的每一行,它唯一的参数是一个 Closure,Closure 的参数是文件每一行的内容。示例代码如下所示:


def file = new File(文件名)
file.eachLine{ String oneLine ->
println oneLine
}

def text = file.getText()
def text2 = file.readLines()

file.eachLine { oneLine, lineNo ->
println "${lineNo} ${oneLine}"
}
复制代码

然后,我们可以使用 'targetFile.bytes' 直接得到文件的内容。


使用 InputStream


此外,我们也可以通过流的方式进行文件操作,如下代码所示:


//操作 ism,最后记得关掉
def ism = targetFile.newInputStream()
// do sth
ism.close
复制代码

使用闭包操作 inputStream


利用闭包来操作 inputStream,其功能更加强大,推荐使用这种写法,如下所示:


targetFile.withInputStream{ ism ->
// 操作 ism,不用 close。Groovy 会自动替你 close
}
复制代码

2)、写文件


关于写文件有两种常用的操作形式,即通过 withOutputStream/withInputStream 或 withReader/withWriter 的写法。示例代码如下所示:


通过 withOutputStream/、withInputStream copy 文件


def srcFile = new File(源文件名)
def targetFile = new File(目标文件名) targetFile.withOutputStream{ os->
srcFile.withInputStream{ ins->
os << ins //利用 OutputStream 的<<操作符重载,完成从 inputstream 到 OutputStream //的输出
}
}
复制代码

通过 withReader、withWriter copy 文件


def copy(String sourcePath, String destationPath) {
try {
//首先创建目标文件
def desFile = new File(destationPath)
if (!desFile.exists()) {
desFile.createNewFile()
}

//开始copy
new File(sourcePath).withReader { reader ->
def lines = reader.readLines()
desFile.withWriter { writer ->
lines.each { line ->
writer.append(line + "\r\n")
}
}
}
return true
} catch (Exception e) {
e.printStackTrace()
}
return false
}
复制代码

此外,我们也可以通过 withObjectOutputStream/withObjectInputStream 来保存与读取 Object 对象。示例代码如下所示:


保存对应的 Object 对象到文件中


def saveObject(Object object, String path) {
try {
//首先创建目标文件
def desFile = new File(path)
if (!desFile.exists()) {
desFile.createNewFile()
}
desFile.withObjectOutputStream { out ->
out.writeObject(object)
}
return true
} catch (Exception e) {
}
return false
}
复制代码

从文件中读取 Object 对象


def readObject(String path) {
def obj = null
try {
def file = new File(path)
if (file == null || !file.exists()) return null
//从文件中读取对象
file.withObjectInputStream { input ->
obj = input.readObject()
}
} catch (Exception e) {

}
return obj
}
复制代码

2、XML 文件操作


1)、获取 XML 数据


首先,我们定义一个包含 XML 数据的字符串,如下所示:


final String xml = '''
<response version-api="2.0">
<value>
<books id="1" classification="android">
<book available="20" id="1">
<title>疯狂Android讲义</title>
<author id="1">李刚</author>
</book>
<book available="14" id="2">
<title>第一行代码</title>
<author id="2">郭林</author>
</book>
<book available="13" id="3">
<title>Android开发艺术探索</title>
<author id="3">任玉刚</author>
</book>
<book available="5" id="4">
<title>Android源码设计模式</title>
<author id="4">何红辉</author>
</book>
</books>
<books id="2" classification="web">
<book available="10" id="1">
<title>Vue从入门到精通</title>
<author id="4">李刚</author>
</book>
</books>
</value>
</response>
'''
复制代码

然后,我们可以 使用 XmlSlurper 来解析此 xml 数据,代码如下所示:


def xmlSluper = new XmlSlurper()
def response = xmlSluper.parseText(xml)

// 通过指定标签获取特定的属性值
println response.value.books[0].book[0].title.text()
println response.value.books[0].book[0].author.text()
println response.value.books[1].book[0].@available

def list = []
response.value.books.each { books ->
//下面开始对书结点进行遍历
books.book.each { book ->
def author = book.author.text()
if (author.equals('李刚')) {
list.add(book.title.text())
}
}
}
println list.toListString()
复制代码

2)、获取 XML 数据的两种遍历方式


获取 XML 数据有两种遍历方式:深度遍历 XML 数据 与 广度遍历 XML 数据,下面我们看看它们各自的用法,如下所示:


深度遍历 XML 数据


def titles = response.depthFirst().findAll { book ->
return book.author.text() == '李刚' ? true : false
}
println titles.toListString()
复制代码

广度遍历 XML 数据


def name = response.value.books.children().findAll { node ->
node.name() == 'book' && node.@id == '2'
}.collect { node ->
return node.title.text()
}
复制代码

在实际使用中,我们可以 利用 XmlSlurper 求获取 AndroidManifest.xml 的版本号(versionName),代码如下所示:


def androidManifest = new XmlSlurper().parse("AndroidManifest.xml") println androidManifest['@android:versionName']
或者
println androidManifest.@'android:versionName'
复制代码

3)、生成 XML 数据


除了使用 XmlSlurper 解析 XML 数据之外,我们也可以 使用 xmlBuilder 来创建 XML 文件,如下代码所示:


/**
* 生成 xml 格式数据
* <langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
*/
def sw = new StringWriter()
// 用来生成 xml 数据的核心类
def xmlBuilder = new MarkupBuilder(sw)
// 根结点 langs 创建成功
xmlBuilder.langs(type: 'current', count: '3',
mainstream: 'true') {
//第一个 language 结点
language(flavor: 'static', version: '1.5') {
age('16')
}
language(flavor: 'dynamic', version: '1.6') {
age('10')
}
language(flavor: 'dynamic', version: '1.9', 'JavaScript')
}

// println sw

def langs = new Langs()
xmlBuilder.langs(type: langs.type, count: langs.count,
mainstream: langs.mainstream) {
//遍历所有的子结点
langs.languages.each { lang ->
language(flavor: lang.flavor,
version: lang.version, lang.value)
}
}

println sw

// 对应 xml 中的 langs 结点
class Langs {
String type = 'current'
int count = 3
boolean mainstream = true
def languages = [
new Language(flavor: 'static',
version: '1.5', value: 'Java'),
new Language(flavor: 'dynamic',
version: '1.3', value: 'Groovy'),
new Language(flavor: 'dynamic',
version: '1.6', value: 'JavaScript')
]
}
//对应xml中的languang结点
class Language {
String flavor
String version
String value
}
复制代码

4)、Groovy 中的 json


我们可以 使用 Groovy 中提供的 JsonSlurper 类去替代 Gson 解析网络响应,这样我们在写插件的时候可以避免引入 Gson 库,其示例代码如下所示:


def reponse =
getNetworkData(
'http://yuexibo.top/yxbApp/course_detail.json')

println reponse.data.head.name

def getNetworkData(String url) {
//发送http请求
def connection = new URL(url).openConnection()
connection.setRequestMethod('GET')
connection.connect()
def response = connection.content.text
//将 json 转化为实体对象
def jsonSluper = new JsonSlurper()
return jsonSluper.parseText(response)
}
复制代码

五、总结


在这篇文章中,我们从以下 四个方面 学习了 Groovy 中的必备核心语法:



  • 1)、groovy 中的变量、字符串、循环等基本语法。

  • 2)、groovy 中的数据结构:数组、列表、映射、范围。

  • 3)、groovy 中的方法、类等面向对象、强大的运行时机制。

  • 4)、groovy 中对普通文件、XML、json 文件的处理。


在后面我们自定义 Gradle 插件的时候需要使用到这些技巧,因此,掌握好 Groovy 的重要性不言而喻,只有扎实基础才能让我们走的更远。


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

深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(2)

三、Groovy 基础语法Groovy 的基础语法主要可以分为以下 四个部分:1)、Groovy 核心基础语法。2)、Groovy 闭包。3)、Groovy 数据结构。4)、Groovy 面向对象1、Groovy 核心基础语法Groovy 中的变量变...
继续阅读 »

三、Groovy 基础语法

Groovy 的基础语法主要可以分为以下 四个部分:

  • 1)、Groovy 核心基础语法。
  • 2)、Groovy 闭包。
  • 3)、Groovy 数据结构。
  • 4)、Groovy 面向对象

1、Groovy 核心基础语法

Groovy 中的变量

变量类型

Groovy 中的类型同 Java 一样,也是分为如下 两种:

  • 1)、基本类型。
  • 2)、对象类型。

但是,其实 Groovy 中并没有基本类型,Groovy 作为动态语言, 在它的世界中,所有事物都是对象,就如 Python、Kotlin 一样:所有的基本类型都是属于对象类型。为了验证这个 Case,我们可以新建一个 groovy 文件,创建一个 int 类型的变量并输出它,会得到输出结果为 'class java.lang.Integer',因此可以验证我们的想法是正确的。实际上,Groovy 的编译器会将所有的基本类型都包装成对象类型

变量定义

groovy 变量的定义与 Java 中的方式有比较大的差异,对于 groovy 来说,它有 两种定义方式,如下所示:

  • 1)、强类型定义方式:groovy 像 Java 一样,可以进行强类型的定义,比如上面直接定义的 int 类型的 x,这种方式就称为强类型定义方式,即在声明变量的时候定义它的类型。
  • 2)、弱类型定义方式:不需要像强类型定义方式一样需要提前指定类型,而是通过 def 关键字来定义我们任何的变量,因为编译器会根据值的类型来为它进行自动的赋值。

那么,这两种方式应该分别在什么样的场景中使用呢?

如果这个变量就是用于当前类或文件,而不会用于其它类或应用模块,那么,建议使用 def 类型,因为在这种场景下弱类型就足够了

但是,如果你这个类或变量要用于其它模块的,建议不要使用 def,还是应该使用 Java 中的那种强类型定义方式,因为使用强类型的定义方式,它不能动态转换为其它类型,它能够保证外界传递进来的值一定是正确的。如果你这个变量要被外界使用,而你却使用了 def 类型来定义它,那外界需要传递给你什么才是正确的呢?这样会使调用方很疑惑。

如果此时我们在后面的代码中改变上图中 x1 的值为 String 类型,那么 x1 又会被编译器推断为 String 类型,于是我们可以猜测到,其实使用 def 关键字定义出来的变量就是 Obejct 类型。

Groovy 中的字符串

Groovy 中的字符串与 Java 中的字符串有比较大的不同,所以这里我们需要着重了解一下。

Groovy 中的字符串除了继承了 Java 中传统 String 的使用方式之前,还 新增 了一个 GString 类型,它的使用方式至少有七、八种,但是常用的有三种定义方式。此外,在 GString 中新增了一系列的操作符,这能够让我们对 String 类型的变量有 更便捷的操作。最后,在 GString 中还 新增 了一系列好用的 API,我们也需要着重学习一下。

Groovy 中常用的三种字符串定义方式

在 Groovy 中有 三种常用 的字符串定义方式,如下所示:

  • 1)、单引号 '' 定义的字符串
  • 2)、双引号 "" 定义的字符串
  • 3)、三引号 '""' 定义的字符串

首先,需要说明的是,'不管是单引号、双引号还是三引号,它们的类型都是 java.lang.String'。

那么,单引号与三引号的区别是什么呢?

既生瑜何生亮,其实不然。当我们编写的单引号字符串中有转义字符的时候,需要添加 '',并且,当字符串需要具备多行格式的时候,强行将单引号字符串分成多行格式会变成由 '+' 号组成的字符串拼接格式

那么,双引号定义的变量又与单引号、三引号有什么区别呢?

双引号不同与单、三引号,它定义的是一个可扩展的变量。这里我们先看看两种双引号的使用方式,如下图所示:

在上图中,第一个定义的 name 字符串就是常规的 String 类型的字符串,而下面定义的 sayHello 字符串就是可扩展的字符串,因为它里面使用了 '${name}' 的方式引用了 name 变量的内容。而且,从其最后的类型输出可以看到,可扩展的类型就是 'org.codehaus.groovy.runtime.GStringImpl' 类型的。

需要注意的是,可扩展的字符串是可以扩展成为任意的表达式,例如数学运算,如上图中的 sum 变量。

有了 Groovy 的这种可扩展的字符串,我们就可以 避免 Java 中字符串的拼接操作,提升 Java 程序运行时的性能

那么,既然有 String 和 GString 两种类型的字符串,它们在相互赋值的场景下需要不需要先强转再赋值呢?

不需要,编译器可以帮我们自动在 String 和 GString 之间相互转换,我们在编写的时候并不需要太过关注它们的区别

2、Groovy 闭包(Closure)

闭包的本质其实就是一个代码块,闭包的核心内容可以归结为如下三点:

  • 1)、闭包概念
    • 定义
    • 闭包的调用
  • 2)、闭包参数
    • 普通参数
    • 隐式参数
  • 3)、闭包返回值
    • 总是有返回值

闭包的调用

clouser.call()
clouser()
def xxx = { paramters -> code }
def xxx = { 纯 code }
复制

从 C/C++ 语言的角度看,闭包和函数指针很像,闭包可以通过 .call 方法来调用,也可以直接调用其构造函数,代码如下所示:

闭包对象.call(参数)
闭包对象(参数)
复制代码

如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it,和 this 的作用类似。it 代表闭包的参数。表示闭包中没有参数的示例代码:

def noParamClosure = { -> true }
复制代

注意点:省略圆括号

函数最后一个参数都是一个闭包,类似于回调函数的用法,代码如下所示:

task JsonChao {
doLast ({
println "love is peace~"
}
})

// 似乎好像doLast会立即执行一样
task JsonChao {
doLast {
println "love is peace~"
}
}
复制代码

闭包的用法

闭包的常见用法有如下 四种:

  • 1)、与基本类型的结合使用。
  • 2)、与 String 类的结合使用。
  • 3)、与数据结构的结合使用。
  • 4)、与文件等结合使用。

闭包进阶

  • 1)、闭包的关键变量
    • this
    • owner
    • delegate
  • 2)、闭包委托策略

闭包的关键变量

this 与 owner、delegate

其差异代码如下代码所示:

def scrpitClouser = {
// 代表闭包定义处的类
printlin "scriptClouser this:" + this
// 代表闭包定义处的类或者对象
printlin "scriptClouser this:" + owner
// 代表任意对象,默认与 ownner 一致
printlin "scriptClouser this:" + delegate
}

// 输出都是 scrpitClouse 对象
scrpitClouser.call()

def nestClouser = {
def innnerClouser = {
// 代表闭包定义处的类
printlin "scriptClouser this:" + this
// 代表闭包定义处的类或者对象
printlin "scriptClouser this:" + owner
// 代表任意对象,默认与 ownner 一直
printlin "scriptClouser this:" + delegate
}
innnerClouser.call()
}

// this 输出的是 nestClouser 对象,而 owner 与 delegate 输出的都是 innnerClouser 对象
nestClouser.call()
复制

可以看到,如果我们直接在类、方法、变量中定义一个闭包,那么这三种关键变量的值都是一样的,但是,如果我们在闭包中又嵌套了一个闭包,那么,this 与 owner、delegate 的值就不再一样了。换言之,this 还会指向我们闭包定义处的类或者实例本身,而 owner、delegate 则会指向离它最近的那个闭包对象

delegate 与 this、owner 的差异

其差异代码如下代码所示:

def nestClouser = {
def innnerClouser = {
// 代表闭包定义处的类
printlin "scriptClouser this:" + this
// 代表闭包定义处的类或者对象
printlin "scriptClouser this:" + owner
// 代表任意对象,默认与 ownner 一致
printlin "scriptClouser this:" + delegate
}

// 修改默认的 delegate
innnerClouser.delegate = p
innnerClouser.call()
}

nestClouser.call()
复制代

可以看到,delegate 的值是可以修改的,并且仅仅当我们修改 delegate 的值时,delegate 的值才会与 ownner 的值不一样

闭包的委托策略

其示例代码如下所示:

def stu = new Student()
def tea = new Teacher()
stu.pretty.delegate = tea
// 要想使 pretty 闭包的 delegate 修改生效,必须选择其委托策略为 Closure.DELEGATE_ONLY,默认是 Closure.OWNER_FIRST。
stu.pretty.resolveStrategy = Closure.DELEGATE_ONLY
println stu.toString()
复制

需要注意的是,要想使上述 pretty 闭包的 delegate 修改生效,必须选择其委托策略为 Closure.DELEGATE_ONLY,默认是 Closure.OWNER_FIRST 的。

3、Groovy 数据结构

Groovy 常用的数据结构有如下 四种:

  • 1)、数组
  • 2)、List
  • 3)、Map
  • 4)、Range

数组的使用和 Java 语言类似,最大的区别可能就是定义方式的扩展,如下代码所示:

// 数组定义
def array = [1, 2, 3, 4, 5] as int[]
int[] array2 = [1, 2, 3, 4, 5]
复制代

下面,我们看看其它三种数据结构。

1、List

即链表,其底层对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类,List 变量由[]定义,其元素可以是任何对象

链表中的元素可以通过索引存取,而且 不用担心索引越界。如果索引超过当前链表长度,List 会自动往该索引添加元素。下面,我们看看 List 最常使用的几个操作。

1)、排序

def test = [100, "hello", true]
// 左移位表示向List中添加新元素
test << 200
// list 定义
def list = [1, 2, 3, 4, 5]
// 排序
list.sort()
// 使用自己的排序规则
sortList.sort { a, b ->
a == b ?0 :
Math.abs(a) < Math.abs(b) ? 1 : -1
}
复制

2)、添加

// 添加
list.add(6)
list.leftShift(7)
list << 8
复制代码

3)、删除

// 删除
list.remove(7)
list.removeAt(7)
list.removeElement(6)
list.removeAll { return it % 2 == 0 }
复制代码

4)、查找

// 查找
int result = findList.find { return it % 2 == 0 }
def result2 = findList.findAll { return it % 2 != 0 }
def result3 = findList.any { return it % 2 != 0 }
def result4 = findList.every { return it % 2 == 0 }

5)、获取最小值、最大值

// 最小值、最大值
list.min()
list.max(return Math.abs(it))
复制代码

6)、统计满足条件的数量

// 统计满足条件的数量
def num = findList.count { return it >= 2 }
复制代

Map

表示键-值表,其 底层对应 Java 中的 LinkedHashMap

Map 变量由[:]定义,冒号左边是 key,右边是 Value。key 必须是字符串,value 可以是任何对象。另外,key 可以用 '' 或 "" 包起来,也可以不用引号包起来。下面,我们看看 Map 最常使用的几个操作。

1)、存取

其示例代码如下所示:

aMap.keyName
aMap['keyName']
aMap.anotherkey = "i am map"
aMap.anotherkey = [a: 1, b: 2]
复制代码

2)、each 方法

如果我们传递的闭包是一个参数,那么它就把 entry 作为参数。如果我们传递的闭包是 2 个参数,那么它就把 key 和 value 作为参数。

def result = ""
[a:1, b:2].each { key, value ->
result += "$key$value"
}

assert result == "a1b2"

def socre = ""
[a:1, b:2].each { entry ->
result += entry
}

assert result == "a=1b=2"

3)、eachWithIndex 方法

如果闭包采用两个参数,则将传递 Map.Entry 和项目的索引(从零开始的计数器);否则,如果闭包采用三个参数,则将传递键,值和索引。

def result = ""
[a:1, b:3].eachWithIndex { key, value, index -> result += "$index($key$value)" }
assert result == "0(a1)1(b3)"

def result = ""
[a:1, b:3].eachWithIndex { entry, index -> result += "$index($entry)" }
assert result == "0(a=1)1(b=3)"

4)、groupBy 方法

按照闭包的条件进行分组,代码如下所示:

def group = students.groupBy { def student ->
return student.value.score >= 60 ? '及格' : '不及格'
}
复制代

5)、findAll 方法

它有两个参数,findAll 会将 Key 和 Value 分别传进 去。并且,如果 Closure 返回 true,表示该元素是自己想要的,如果返回 false 则表示该元素不是自己要找的。

Range

表示范围,它其实是 List 的一种拓展。其由 begin 值 + 两个点 + end 值表示。如果不想包含最后一个元素,则 begin 值 + 两个点 + < + end 表示。我们可以通过 aRange.from 与 aRange.to 来获对应的边界元素

如果需要了解更多的数据结构操作方法,我们可以直接查 Groovy API 详细文档 即可。

4、Groovy 面向对象

如果不声明 public/private 等访问权限的话,Groovy 中类及其变量默认都是 public 的

1)、元编程(Groovy 运行时)

Groovy 运行时的逻辑处理流程图如下所示:

为了更好的讲解元编程的用法,我们先创建一个 Person 类并调用它的 cry 方法,代码如下所示:

// 第一个 groovy 文件中
def person = new Person(name: 'Qndroid', age: 26)
println person.cry()

// 第二个 groovy 文件中
class Person implements Serializable {

String name

Integer age

def increaseAge(Integer years) {
this.age += years
}

/**
* 一个方法找不到时,调用它代替
* @param name
* @param args
* @return
*/
def invokeMethod(String name, Object args) {

return "the method is ${name}, the params is ${args}"
}


def methodMissing(String name, Object args) {

return "the method ${name} is missing"
}
}
复制

为了实现元编程,我们需要使用 metaClass,具体的使用示例如下所示:

ExpandoMetaClass.enableGlobally()
//为类动态的添加一个属性
Person.metaClass.sex = 'male'
def person = new Person(name: 'Qndroid', age: 26)
println person.sex
person.sex = 'female'
println "the new sex is:" + person.sex
//为类动态的添加方法
Person.metaClass.sexUpperCase = { -> sex.toUpperCase() }
def person2 = new Person(name: 'Qndroid', age: 26)
println person2.sexUpperCase()
//为类动态的添加静态方法
Person.metaClass.static.createPerson = {
String name, int age -> new Person(name: name, age: age)
}
def person3 = Person.createPerson('renzhiqiang', 26)
println person3.name + " and " + person3.age

需要注意的是通过类的 metaClass 来添加元素的这种方式每次使用时都需要重新添加,幸运的是,我们可以在注入前调用全局生效的处理,代码如下所示:

ExpandoMetaClass.enableGlobally()
// 在应用程序初始化的时候我们可以为第三方类添加方法
Person.metaClass.static.createPerson = { String name,
int age ->
new Person(name: name, age: age)
}
复制代码

2)、脚本中的变量和作用域

对于每一个 Groovy 脚本来说,它都会生成一个 static void main 函数,main 函数中会调用一个 run 函数,脚本中的所有代码则包含在 run 函数之中。我们可以通过如下的 groovyc 命令用于将编译得到的 class 文件拷贝到 classes 文件夹下:

// groovyc 是 groovy 的编译命令,-d classes 用于将编译得到的 class 文件拷贝到 classes 文件夹 下
groovyc -d classes test.groovy
复制代码

当我们在 Groovy 脚本中定义一个变量时,由于它实际上是在 run 函数中创建的,所以脚本中的其它方法或其他脚本是无法访问它的。这个时候,我们需要使用 @Field 将当前变量标记为成员变量,其示例代码如下所示:

import groovy.transform.Field; 

@Field author = JsonCh


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

收起阅读 »

【iOS】Keychain 钥匙串

钥匙串,实际上是一个加密后的数据库,如下图所示。即使吧App删除,钥匙串里面的数据也不会丢失。数据都是以 Item 的形式来存储的,每个 Item 由一个加密后的 Data 数据,还有一系列用来描述该 Item 属性的 Attributes 组成。由于是数据库...
继续阅读 »

钥匙串,实际上是一个加密后的数据库,如下图所示。
即使吧App删除,钥匙串里面的数据也不会丢失。


数据都是以 Item 的形式来存储的,每个 Item 由一个加密后的 Data 数据,还有一系列用来描述该 Item 属性的 Attributes 组成。
由于是数据库,关键方法只有四种,增删改查,对应的是

SecItemAdd
SecItemDelete
SecItemUpdate
SecItemCopyMatching
下面简单讲述一下使用方法

SecItemAdd

CFTypeRef result;
NSDictionary *query = @{
// 一个典型的新增方法的参数,包含三个部分
// 1.kSecClass key,它用来指定新增对象的类型
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2.若干项属性 key,例如 kSecAttrAccount,kSecAttrLabel 等,用来描述新增对象的属性
(NSString *)kSecAttrAccount: @"uniqueID",
// 3.kSecValueData key,用来设置新增对象保存的数据
(NSString *)kSecValueData: [@"token" dataUsingEncoding:NSUTF8StringEncoding],
// 可选
// 如果需要获取新增的 Item 对象的属性,需要如下属性,
(NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
(NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
};
OSStatus status = SecItemAdd((CFDictionaryRef)query, &result);
if (result == errSecSuccess) {
// 新增成功
NSDictionary *itemInfo = (__bridge NSDictionary *)result;
NSLog(@"info: %@", itemInfo);
} else {
// 其他错误
}

result类型判断方式


SecItemDelete

NSDictionary *query = @{
// 一个典型的删除方法的参数,包含两个部分
// 1、kSecClass key,它用来指定删除对象的类型,必填。
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2、若干项属性 key,可选。
// 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
// 默认情况下,符合条件的全部 Item 都会被删除
(NSString *)kSecAttrAccount: @"uniqueID",
};
OSStatus status = SecItemDelete((CFDictionaryRef)query);
if (result == errSecSuccess) {
// 删除成功
} else {
// 其他错误
}

SecItemUpdate

// 1、找出需要更新属性的 Item
// 参数格式与 SecItemCopyMatching 方法中的参数格式相同
NSDictionary *query = @{
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
(NSString *)kSecAttrAccount: @"uniqueID",
};

// 2、需要更新的属性
// 若干项属性 key
NSDictionary *update = @{
(NSString *)kSecAttrAccount: @"another uniqueID",
(NSString *)kSecValueData: @"another value",
};

OSStatus status = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)update);

if (result == errSecSuccess) {
// 更新成功
} else {
// 其他错误
}

SecItemDelete

NSDictionary *query = @{
// 一个典型的删除方法的参数,包含两个部分
// 1、kSecClass key,它用来指定删除对象的类型,必填。
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2、若干项属性 key,可选。
// 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
// 默认情况下,符合条件的全部 Item 都会被删除
(NSString *)kSecAttrAccount: @"uniqueID",
};
OSStatus status = SecItemDelete((CFDictionaryRef)query);
if (result == errSecSuccess) {
// 删除成功
} else {
// 其他错误
}

SecItemCopyMatching

CFTypeRef result;
NSDictionary *query = @{
// 一个典型的搜索方法的参数,包含三个部分
// 1、kSecClass key(必填),它用来指定搜索对象的类型
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2、若干项属性 key(可选),例如 kSecAttrAccount,kSecAttrLabel 等,用来描述搜索对象的属性
(NSString *)kSecAttrAccount: @"uniqueID",
// 3、搜索属性(可选)
// 例如 kSecMatchLimit(搜索一个还是多个,影响返回结果类型)
// kSecMatchCaseInsensitive 是否大小写敏感等
(NSString *)kSecMatchLimit: (NSString *)kSecMatchLimitAll,
(NSString *) kSecMatchCaseInsensitive: (NSNumber *) kCFBooleanTrue,
// (可选)如果需要获取新增的 Item 对象的属性,需要如下属性,
(NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
(NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
};
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, &result);
if (result == errSecSuccess) {
// 新增成功
NSDictionary *itemInfo = (__bridge NSDictionary *)result;
NSLog(@"info: %@", itemInfo);
} else {
// 其他错误
}

result类型判断方式


链接:https://www.jianshu.com/p/8f8db1ff024d


收起阅读 »

iOS 网页和原生列表混合布局开发(文章+评论)

我们总会遇见特别不适合使用原生开发的页面,比如一个文章详情页,上面是文章下面是评论,就比如现在用的简书的手机版这样,那么这种需求应该怎么做呢?最好的方法当然是整个页面都是用H5开发,哈哈哈;当然下面评论有时候会有很多交互导致得用原生控件开发,那这里就面临着严峻...
继续阅读 »

我们总会遇见特别不适合使用原生开发的页面,比如一个文章详情页,上面是文章下面是评论,就比如现在用的简书的手机版这样,那么这种需求应该怎么做呢?
最好的方法当然是整个页面都是用H5开发,哈哈哈;当然下面评论有时候会有很多交互导致得用原生控件开发,那这里就面临着严峻的问题了,上面是网页可以滑动,下面是评论最好是用列表做,具体怎么组合起来就值得我们说道说道了,当然方法有很多种,我这里讲解一种我觉得各方面都不错的。

ps:问题总结起来还是两个滑动视图上下滑动问题所以用我之前讲解的多个滑动视图冲突解决https://www.jianshu.com/p/cfe517ce437b 也可以解决不过这样使用H5那面配合的地方比较多。这个不多说,下面介绍我们今天要说的。

这个方案的整体思路:把web和table同时加在一个底层ScrollView上面,滑动底层ScrollView同时不断控制web和table的偏移量位置,使页面看起来是两个滑动视图连在一起的。
整体结构如图


一、视图介绍

黄色的是底层ScrollView,青色的一个加在底层ScrollView上的view(这里我们叫它contentView),然后正加载简书网页的是web,红色部分是table。web和table再加contentView上,这样我们控制整体位置的时候使用contentView就行;

二、视图之间的高度关系:

web和table的最大高度都是底层ScrollView的高度,这样做可以正好让其中一个充满整个底层ScrollView。
contentView的高度是web和table高度的和(毕竟就是为了放他们两)。
底层ScrollView的可滑动高度这里设定成web和table可滑动高度的总和,方便滑动处理。
ps:具体代码在后面。

三、滑动处理思路

滑动都靠底层ScrollView,禁用web和table的滑动,上面说了底层ScrollView的可滑动高度是web和table的总和所以进度条是正常的。
然后在滑动的同时不断调整contentView的位置,web和table的偏移量,使页面效果看起来符合预期。

四、滑动处理具体操作,整个滑动可以分成五阶段。ps:offsety 底层ScrollView的偏移量
1.offsety<=0,不用过多操作正常滑动
2.web内部可以滑动。控制contentView悬浮,使web在屏幕可视区域。同时修改web的偏移量。
3.web滑动到头。保持contentView的位置和web的偏移量,使table滑动到屏幕可视区域
4.table内部可以滑动。控制contentView悬浮,使table在屏幕可视区域。同时修改table的偏移量。
5.table滑动到头。保持contentView的位置和table的偏移量,使页面滑动到底部
五、具体代码
1.因为web和table都是随内容变高的,这里选择通过监听两者高度变化,同时刷新各个控件的高度,对应第二步骤

//添加监听
[self.webView addObserver:self forKeyPath:@"scrollView.contentSize" options:NSKeyValueObservingOptionNew context:nil];
[self.collectionView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
//刷新各个控件高度
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (object == _webView) {
if ([keyPath isEqualToString:@"scrollView.contentSize"]) {
[self updateContainerScrollViewHeight];
}
}else if(object == _collectionView) {
if ([keyPath isEqualToString:@"contentSize"]) {
[self updateContainerScrollViewHeight];
}
}
}

- (void)updateContainerScrollViewHeight{
CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
CGFloat collectionContentHeight = self.collectionView.contentSize.height;

if (webViewContentHeight == _lastWebViewContentHeight && collectionContentHeight == _lastCollectionContentHeight) {
return;
}

_lastWebViewContentHeight = webViewContentHeight;
_lastCollectionContentHeight = collectionContentHeight;

self.containerScrollView.contentSize = CGSizeMake(self.view.width, webViewContentHeight + collectionContentHeight);

CGFloat webViewHeight = (webViewContentHeight < _contentHeight) ?webViewContentHeight :_contentHeight;
CGFloat collectionHeight = collectionContentHeight < _contentHeight ?collectionContentHeight :_contentHeight;
self.webView.height = webViewHeight <= 0.1 ?0.1 :webViewHeight;
self.contentView.height = webViewHeight + collectionHeight;
self.collectionView.height = collectionHeight;
self.collectionView.top = self.webView.bottom;

[self scrollViewDidScroll:self.containerScrollView];
}

2.具体滑动处理代码:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (_containerScrollView != scrollView) {
return;
}

CGFloat offsetY = scrollView.contentOffset.y;

CGFloat webViewHeight = self.webView.height;
CGFloat collectionHeight = self.collectionView.height;

CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
CGFloat collectionContentHeight = self.collectionView.contentSize.height;
if (offsetY <= 0) {
self.contentView.top = 0;
self.webView.scrollView.contentOffset = CGPointZero;
self.collectionView.contentOffset = CGPointZero;
}else if(offsetY < webViewContentHeight - webViewHeight){
self.contentView.top = offsetY;
self.webView.scrollView.contentOffset = CGPointMake(0, offsetY);
self.collectionView.contentOffset = CGPointZero;
}else if(offsetY < webViewContentHeight){
self.contentView.top = webViewContentHeight - webViewHeight;
self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
self.collectionView.contentOffset = CGPointZero;
}else if(offsetY < webViewContentHeight + collectionContentHeight - collectionHeight){
self.contentView.top = offsetY - webViewHeight;
self.collectionView.contentOffset = CGPointMake(0, offsetY - webViewContentHeight);
self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
}else if(offsetY <= webViewContentHeight + collectionContentHeight ){
self.contentView.top = self.containerScrollView.contentSize.height - self.contentView.height;
self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
self.collectionView.contentOffset = CGPointMake(0, collectionContentHeight - collectionHeight);
}else {
//do nothing
NSLog(@"do nothing");
}
}


链接:https://www.jianshu.com/p/ca7f826fd39b

收起阅读 »

深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(1)

前言 成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。 Groovy 作为 Gradle 这一强大构建工具的核心语言,其重要性不言而喻,但是 Groovy 本身是十分复杂的,要想全面地掌握它,我想几十篇万字...
继续阅读 »

前言


成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。


Groovy 作为 Gradle 这一强大构建工具的核心语言,其重要性不言而喻,但是 Groovy 本身是十分复杂的,要想全面地掌握它,我想几十篇万字长文也无法将其彻底描述。所幸的是,在 Gradle 领域中涉及的 Groovy 知识都是非常基础的,因此,本篇文章的目的是为了在后续深入探索 Gradle 时做好一定的基础储备。


一、DSL 初识


DSL(domain specific language),即领域特定语言,例如:Matliba、UML、HTML、XML 等等 DSL 语言。可以这样理解,Groovy 就是 DSL 的一个分支。


特点



  • 1)、解决特定领域的专有问题。

  • 2)、它与系统编程语言走的是两个极端,系统编程语言是希望解决所有的问题,比如 Java 语言希望能做 Android 开发,又希望能做服务器开发,它具有横向扩展的特性。而 DSL 具有纵向深入解决特定领域专有问题的特性。


总的来说,DSL 的 核心思想 就是:“求专不求全,解决特定领域的问题”。


二、Groovy 初识


1、Groovy 的特点


Groovy 的特点具有如下 三点:



  • 1)、Groovy 是一种基于 JVM 的敏捷开发语言。

  • 2)、Groovy 结合了 Python、Ruby 和 Smalltalk 众多脚本语言的许多强大的特性。

  • 3)、Groovy 可以与 Java 完美结合,而且可以使用 Java 所有的库。


那么,在已经有了其它脚本语言的前提下,为什么还要制造出 Grvooy 语言呢?


因为 Groovy 语言相较其它编程语言而言,其 入门的学习成本是非常低的,因为它的语法就是对 Java 的扩展,所以,我们可以用学习 Java 的方式去学习 Groovy。


2、Groovy 语言本身的特性


其特性主要有如下 三种:



  • 1)、语法上支持动态类型,闭包等新一代语言特性。并且,Groovy 语言的闭包比其它所有语言类型的闭包都要强大。

  • 2)、它可以无缝集成所有已经存在的 Java 类库,因为它是基于 JVM 的。

  • 3)、它即可以支持面向对象编程(基于 Java 的扩展),也可以支持面向过程编程(基于众多脚本语言的结合)。


需要注意的是,在我们使用 Groovy 进行 Gradle 脚本编写的时候,都是使用的面向过程进行编程的


3、Groovy 的优势


Groovy 的优势有如下 四种:



  • 1)、它是一种更加敏捷的编程语言:在语法上构建除了非常多的语法糖,许多在 Java 层需要写的代码,在 Groovy 中是可以省略的。因此,我们可以用更少的代码实现更多的功能。

  • 2)、入门简单,但功能非常强大。

  • 3)、既可以作为编程语言也可以作为脚本语言

  • 4)、熟悉掌握 Java 的同学会非常容易掌握 Groovy。


4、Groovy 包的结构



Groovy 官方网址



从官网下载好 Groovy 文件之后,我们就可以看到 Groovy 的目录结构,其中我们需要 重点关注 bin 和 doc 这个两个文件夹


bin 文件夹


bin 文件夹的中我们需要了解下三个重要的可执行命令文件,如下所示:



  • 1)、groovy 命令类似于 Java 中的 java 命令,用于执行 groovy Class 字节码文件。

  • 2)、groovyc 命令类似于 Java 中的 javac 命令,用于将 groovy 源文件编译成 groovy 字节码文件。

  • 3)、groovysh 命令是用来解释执行 groovy 脚本文件的。


doc 文件夹


doc 文件夹的下面有一个 html 文件,其中的 api 和 documentation 是我们需要重点关注的,其作用分别如下所示:



  • api:groovy 中为我们提供的一系列 API 及其 说明文档。

  • documentation:groovy 官方为我们提供的一些教程。


5、Groovy 中的关键字


下面是 Groovy 中所有的关键字,命名时尤其需要注意,如下所示:


as、assert、break、case、catch、class、const、continue、def、default、
do、else、enum、extends、false、finally、for、goto、if、implements、
import、in、instanceof、interface、new、null、package、return、super、
switch、this、throw、throws、trait、true、try、while
复制代码

6、Groovy && Java 差异学习


1)、getter / setter


对于每一个 field,Groovy 都会⾃动创建其与之对应的 getter 与 setter 方法,从外部可以直接调用它,并且 在使⽤ object.fieldA 来获取值或者使用 object.fieldA = value 来赋值的时候,实际上会自动转而调⽤ object.getFieldA() 和 object.setFieldA(value) 方法


如果我们不想调用这个特殊的 getter 方法时则可以使用 .@ 直接域访问操作符


2)、除了每行代码不用加分号外,Groovy 中函数调用的时候还可以不加括号。


需要注意的是,我们在使用的时候,如果当前这个函数是 Groovy API 或者 Gradle
API 中比较常用的,比如 println,就可以不带括号。否则还是带括号。不然,Groovy 可能会把属性和函数调用混淆


3)、Groovy 语句可以不用分号结尾。


4)、函数定义时,参数的类型也可以不指定。


5)、Groovy 中函数的返回值也可以是无类型的,并且无返回类型的函数,其内部都是按返回 Object 类型来处理的。


6)、当前函数如果没有使用 return 关键字返回值,则会默认返回 null,但此时必须使用 def 关键字。


7)、在 Groovy 中,所有的 Class 类型,都可以省略 .class。


8)、在 Groovy 中,== 相当于 Java 的 equals,,如果需要比较两个对象是否是同一个,需要使用 .is()。


9)、Groovy 非运算符如下:


assert (!"android") == false                      
复制代码

10)、Groovy 支持 ** 次方运算符,代码如下所示:


assert  2 ** 3 == 8
复制代码

11)、判断是否为真可以更简洁:


    if (android) {}
复制代码

12)、三元表达式可以更加简洁:


// 省略了name
def result = name ?: "Unknown"
复制代码

13)、简洁的非空判断


println order?.customer?.address
复制代码

14)、使用 assert 来设置断言,当断言的条件为 false 时,程序将会抛出异常。


15)、可以使用 Number 类去替代 float、double 等类型,省去考虑精度的麻烦。


16)、switch 方法可以同时支持更多的参数类型。


注意,swctch 可以匹配列表当中任一元素,示例代码如下所示:


// 输出 ok
def num = 5.21
switch (num) {
case [5.21, 4, "list"]:
return "ok"
break
default:
break
}

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

iOS你需要知道的事--Crash分析

Crash ,,CrashlyticsHockeyapp友盟Bugly 等等但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课首先我们来了解一下Crash的底层原理...
继续阅读 »
大家平时在开发过程中,经常会遇到Crash,那也是在正常不过的事,但是作为一个优秀的iOS开发人员,必将这些用户不良体验降到最低。

线下Crash,我们直接可以调试,结合stack信息,不难定位!
线上Crash当然也有一些信息,毕竟苹果爸爸的产品还是做得非常不错的!


通过iPhone的Crash log也可以分析一些,但是这个是需要用户配合的,因为需要用户在手机 中 设置-> 诊断与用量->勾选 自动发送 ,然后在xcode中 Window->Organizer->Crashes 对应的app,就是当前app最新一版本的crash log ,并且是解析过的,可以根据crash 栈 等相关信息 ,尤其是程序代码级别的 有超链接,一键可以直接跳转到程序崩溃的相关代码,这样更容易定位bug出处.

为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如  KSCrashplcrashreporterCrashKit 等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙Crash统计产品,如 CrashlyticsHockeyapp ,友盟Bugly 等等

但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课

首先我们来了解一下Crash的底层原理

iOS系统自带的 Apple’s Crash Reporter记录在设备中的Crash日志,Exception Type项通常会包含两个元素:Mach异常和 Unix信号。

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)    
Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3

Mach异常是什么?它又是如何与Unix信号建立联系的?

Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常,被定义在下 。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。

所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API就是通过Mach之上的 BSD层实现的。


因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach层的EXC_BAD_ACCESS异常,在host层被转换成SIGSEGV信号投递到出错的线程。

iOS的异常Crash
* KVO问题
* NSNotification线程问题
* 数组越界
* 野指针
* 后台任务超时
* 内存爆出
* 主线程卡顿超阀值
* 死锁
....

下面我就拿出最常见的两种Crash分析一下



Crash分析处理

上面我们也知道:既然最终以信号的方式投递到出错的线程,那么就可以通过注册相应函数来捕获信号.达到Hook的效果

+ (void)installUncaughtSignalExceptionHandler{
NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
signal(SIGABRT, LGSignalHandler);
}

我们从上面的函数可以Hook到信息,下面我们开始进行包装处理.这里还是面向统一封装,因为等会我们还需要考虑Signal

void LGExceptionHandlers(NSException *exception) {
NSLog(@"%s",__func__);

NSArray *callStack = [LGUncaughtExceptionHandle lg_backtrace];
NSMutableDictionary *mDict = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
[mDict setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
[mDict setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];
[mDict setObject:@"LGException" forKey:LGUncaughtExceptionHandlerFileKey];

// exception - myException

[[[LGUncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:mDict] waitUntilDone:YES];
}

下面针对封装好的myException进行处理,在这里要做两件事

1.存储,上传:方便开发人员检查修复

2.处理Crash奔溃,我们也不能眼睁睁看着BUG闪退在用户的手机上面,希望“起死回生,回光返照”

- (void)lg_handleException:(NSException *)exception{
// crash 处理
// 存
NSDictionary *userInfo = [exception userInfo];
[self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]];
}

下面是一些封装的一些辅助函数

保存奔溃信息或者上传:针对封装数据本地存储,和相应上传服务器

- (void)saveCrash:(NSException *)exception file:(NSString *)file{

NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 异常的堆栈信息
NSString *reason = [exception reason];// 出现异常的原因
NSString *name = [exception name];// 异常名称

// 或者直接用代码,输入这个崩溃信息,以便在console中进一步分析错误原因
// NSLog(@"crash: %@", exception);

NSString * _libPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file];

if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
[[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
}

NSDate *dat = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval a=[dat timeIntervalSince1970];
NSString *timeString = [NSString stringWithFormat:@"%f", a];

NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];

NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];

BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];

NSLog(@"保存崩溃日志 sucess:%d,%@",sucess,savePath);
}

获取函数堆栈信息,这里可以获取响应调用堆栈的符号信息,通过数组回传

+ (NSArray *)lg_backtrace{

void* callstack[128];
int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (i = LGUncaughtExceptionHandlerSkipAddressCount;
i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount;
i++)
{
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}

获取应用信息,这个函数提供给Siganl数据封装

NSString *getAppInfo(){
NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n",
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
[UIDevice currentDevice].model,
[UIDevice currentDevice].systemName,
[UIDevice currentDevice].systemVersion];
// [UIDevice currentDevice].uniqueIdentifier];
NSLog(@"Crash!!!! %@", appInfo);
return appInfo;
}

做完这些准备,你可以非常清晰的看到程序奔溃,哈哈哈!(好像以前奔溃还不清晰似的),这里说一下:我的意思你非常清晰的知道奔溃之前做了一些什么!
下面是检测我们奔溃之前的沙盒存储的信息:error.log


下面我们来一个骚操作:在监听的信息的时候来了一个Runloop,我们监听所有的mode,开启循环

(一个相对于我们应用程序自启的Runloop的平行空间).

SCLAlertView *alert = [[SCLAlertView alloc] initWithNewWindowWidth:300.0f];
[alert addButton:@"奔溃" actionBlock:^{
self.dismissed = YES;
}];
[alert showSuccess:exception.name subTitle:exception.reason closeButtonTitle:nil duration:0];
// 本次异常处理
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allMode = CFRunLoopCopyAllModes(runloop);
while (!self.dismissed) {
// machO
// 后台更新 - log
// kill
//
for (NSString *mode in (__bridge NSArray *)allMode) {
CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false);
}
}

CFRelease(allMode);

在这个平行空间我们开启一个弹框,这个弹框,跟着我们的应用程序保活,并且具备相应的响应能力,到目前为止:此时此刻还有谁!这不就是回光返照?只要我们的条件成立,那么在相应的这个平行空间继续做一些我们的工作,程序不死:what is dead may never die,but rises again harder and stronger


signal 函数拦截不到的解决方式

在debug模式下,如果你触发了崩溃,那么应用会直接崩溃到主函数,断点都没用,此时没有任何log信息显示出来,如果你想看log信息的话,你需要在lldb中,拿SIGABRT来说吧,敲入pro hand -p true -s false SIGABRT命令,不然你啥也看不到。


然后断开断点,程序进入监听,下面剩下的操作就是包装异常,操作类似Exception


最后我们需要注意的针对我们的监听回收相应内存:

NSSetUncaughtExceptionHandler(NULL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);

if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
{
kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
}
else
{
[exception raise];
}

到目前为止,我们响应的Crash处理已经入门,如果你还想继续探索也是有很多地方比如:

我们能否hook系统奔溃,异常的方法NSSetUncaughtExceptionHandler,已达到拒绝传递 UncaughtExceptionHandler的效果

我们在处理异常的时候,利用Runloop回光返照,有没有更加合适的方法

Runloop回光返照我们怎么继续保证应用程序稳定执行


摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/56f96167a6e9

收起阅读 »

iOS-UIView常用的setNeedsDisplay和setNeedsLayout

UIView的setNeedsDisplay和setNeedsLayout方法      首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphics...
继续阅读 »
  • UIView的setNeedsDisplay和setNeedsLayout方法
      首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,就可以 处理子视图中的一些数据。综上所诉,setNeedsDisplay方便绘图,而    layoutSubViews方便出来数据。
  • layoutSubviews在以下情况下会被调用:

1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。
  • drawRect在以下情况下会被调用:

1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡
  • drawRect方法使用注意点:

1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用CAlayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕

链接:https://www.jianshu.com/p/33a28bb14749

收起阅读 »

高度封装的 WebView-AgentWeb

AgentWeb 介绍AgentWeb 是一个基于的 Android WebView ,极度容易使用以及功能强大的库,提供了 Android WebView 一系列的问题解决方案 ,并且轻量和极度灵活,体验请下载的agentweb.apk,或者你也可以到 Go...
继续阅读 »

AgentWeb 介绍

AgentWeb 是一个基于的 Android WebView ,极度容易使用以及功能强大的库,提供了 Android WebView 一系列的问题解决方案 ,并且轻量和极度灵活,体验请下载的
agentweb.apk
或者你也可以到 Google Play 里面下载 AgentWeb
详细使用请参照上面的 Sample 。

引入

  • Gradle

     implementation 'com.just.agentweb4.1.4' // (必选)
    implementation 'com.just.agentweb4.1.4'// (可选)
    implementation 'com.download.library4.1.4'// (可选)
  • androidx

     implementation 'com.just.agentweb4.1.4' // (必选)
    implementation 'com.just.agentweb4.1.4'// (可选)
    implementation 'com.download.library4.1.4'// (可选


  • 调用 Javascript 方法拼接太麻烦 ? 请看 。

function callByAndroid(){
console.log("callByAndroid")
}
mAgentWeb.getJsAccessEntrace().quickCallJs("callByAndroid");
  • Javascript 调 Java ?

mAgentWeb.getJsInterfaceHolder().addJavaObject("android",new AndroidInterface(mAgentWeb,this));
window.android.callAndroid();
  • 事件处理

    @Override
public boolean onKeyDown(int keyCode, KeyEvent event) {

if (mAgentWeb.handleKeyEvent(keyCode, event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
  • 跟随 Activity Or Fragment 生命周期 , 释放 CPU 更省电 。

    @Override
protected void onPause() {
mAgentWeb.getWebLifeCycle().onPause();
super.onPause();

}

@Override
protected void onResume() {
mAgentWeb.getWebLifeCycle().onResume();
super.onResume();
}
@Override
public void onDestroyView() {
mAgentWeb.getWebLifeCycle().onDestroy();
super.onDestroyView();
}
  • 全屏视频播放


android:hardwareAccelerated="true"
android:configChanges="orientation|screenSize"
  • 定位


<!--AgentWeb 是默认允许定位的 ,如果你需要该功能 , 请在你的 AndroidManifest 文件里面加入如下权限 。-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  • WebChromeClient 与 WebViewClient

AgentWeb.with(this)
.setAgentWebParent(mLinearLayout,new LinearLayout.LayoutParams(-1,-1) )
.useDefaultIndicator()
.setReceivedTitleCallback(mCallback)
.setWebChromeClient(mWebChromeClient)
.setWebViewClient(mWebViewClient)
.setSecutityType(AgentWeb.SecurityType.strict)
.createAgentWeb()
.ready()
.go(getUrl());
private WebViewClient mWebViewClient=new WebViewClient(){
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
//do you work
}
};
private WebChromeClient mWebChromeClient=new WebChromeClient(){
@Override
public void onProgressChanged(WebView view, int newProgress) {
//do you work
}
};
  • 返回上一页

if (!mAgentWeb.back()){
AgentWebFragment.this.getActivity().finish();
}
  • 获取 WebView

	mAgentWeb.getWebCreator().getWebView();
  • 查看 Cookies

String cookies=AgentWebConfig.getCookiesByUrl(targetUrl);
  • 同步 Cookie

AgentWebConfig.syncCookie("http://www.jd.com","ID=XXXX");
  • MiddlewareWebChromeBase 支持多个 WebChromeClient

//略,请查看 Sample
  • MiddlewareWebClientBase 支持多个 WebViewClient

//略,请查看 Sample
  • 清空缓存

AgentWebConfig.clearDiskCache(this.getContext());
  • 权限拦截

protected PermissionInterceptor mPermissionInterceptor = new PermissionInterceptor() {

@Override
public boolean intercept(String url, String[] permissions, String action) {
Log.i(TAG, "url:" + url + " permission:" + permissions + " action:" + action);
return false;
}
};
  • AgentWeb 完整用法

 //略,请查看 Sample
  • AgentWeb 所需要的权限(在你工程中根据需求选择加入权限)

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

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
<uses-permission android:name="android.permission.CAMERA"></uses-permission>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"></uses-permission>


  • AgentWeb 所依赖的库

    compile "com.android.support:design:${SUPPORT_LIB_VERSION}" // (3.0.0开始该库可选)
compile "com.android.support:support-v4:${SUPPORT_LIB_VERSION}"
SUPPORT_LIB_VERSION=27.0.2(该值会更新)

混淆

如果你的项目需要加入混淆 , 请加入如下配置

-keep class com.just.agentweb.** {
*;
}
-dontwarn com.just.agentweb.**

Java 注入类不要混淆 , 例如 sample 里面的 AndroidInterface 类 , 需要 Keep 。

-keepclassmembers class com.just.agentweb.sample.common.AndroidInterface{ *; }

注意事项

  • 支付宝使用需要引入支付宝SDK ,并在项目中依赖 , 微信支付不需要做任何操作。
  • AgentWeb 内部使用了 AlertDialog 需要依赖 AppCompat 主题 。
  • setAgentWebParent 不支持 ConstraintLayout 。
  • mAgentWeb.getWebLifeCycle().onPause();会暂停应用内所有WebView 。
  • minSdkVersion 低于等于16以下自定义WebView请注意与 JS 之间通信安全。
  • AgentWeb v3.0.0以上版本更新了包名,混淆的朋友们,请更新你的混淆配置。
  • 多进程无法取消下载,解决方案

代码下载:AgentWeb-master.zip

原文链接:https://github.com/Justson/AgentWeb



收起阅读 »

iOS Crash分析中的Signal

下面是一些信号说明1.SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运...
继续阅读 »

下面是一些信号说明

1.SIGHUP

本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

2.SIGINT

程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

3.SIGQUIT

和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

4.SIGILL

执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

5.SIGTRAP

由断点指令或其它trap指令产生. 由debugger使用。

6.SIGABRT

调用abort函数生成的信号。

7.SIGBUS

非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

8.SIGFPE

在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

9.SIGKILL

用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

10.SIGUSR1

留给用户使用

11.SIGSEGV

试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

12.SIGUSR2

留给用户使用

13.SIGPIPE

管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

14.SIGALRM

时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

15.SIGTERM

程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

16.SIGCHLD

子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

17.SIGCONT

让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

18.SIGSTOP

停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

19.SIGTSTP

停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

20.SIGTTIN

当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

21.SIGTTOU

类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

22.SIGURG

有”紧急”数据或out-of-band数据到达socket时产生.

23.SIGXCPU

超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

24.SIGXFSZ

当进程企图扩大文件以至于超过文件大小资源限制。

25.SIGVTALRM

虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

26.SIGPROF

类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

27.SIGWINCH

窗口大小改变时发出.

28.SIGIO

文件描述符准备就绪, 可以开始进行输入/输出操作.

SIGPWR
Power failure
SIGSYS

非法的系统调用。


关键点注意

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:

SIGKILL,SIGSTOP

不能恢复至默认动作的信号有:

SIGILL,SIGTRAP

默认会导致进程流产的信号有:

SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

默认会导致进程退出的信号有:

SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM

默认会导致进程停止的信号有:

SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

默认进程忽略的信号有:

SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。


摘自作者:Cooci_和谐学习_不急不躁

原贴链接:https://www.jianshu.com/p/3a9dc6bd5e58

收起阅读 »

iOS——SDWebImage加载WebP图片

1.确定第三方库首先直接去SDWebImage的仓库,里面直接就有关于WebP的仓库地址也就是SDWebImageWebPCoder,直接pod 'SDWebImageWebPCoder'就行。(如果项目里没有SDWebImage,需要pod 'SDWebIm...
继续阅读 »

1.确定第三方库

首先直接去SDWebImage的仓库,里面直接就有关于WebP的仓库地址



也就是SDWebImageWebPCoder,直接pod 'SDWebImageWebPCoder'就行。(如果项目里没有SDWebImage,需要pod 'SDWebImage')

这里要注意!!!是pod 'SDWebImageWebPCoder'

我搜索SDWebImage加载WebP,权重高的答案都是pod 'SDWebImage/WebP',但是这个仓库我在SDWebImage的repositories里搜索不到,也就是说没有这个仓库,结果如图。


猜测可能之前的旧仓库是这个名字,那些文章也一直没更新,但是权重又高,不免误人子弟了一番。

2.导入SDWebImageWebPCoder

大概率会在pod install时报错,因为libwebp这个仓库的地址连接不上。

1、在终端输入pod repo 查看 cocoapods 在本机的PATH,每个人的路径都可能不一样


2、复制trunk的path,command + shift + G 输入上一步的地址,依次点击Specs-->1-->9-->2-->libwebp。(这里要注意有可能你的路径是cocoapods的path)

3、选择报错的版本打开,将source下git地址更改为

https://github.com/webmproject/libwebp.git


4、pod install(如果还报一样的错,那么是第2步出了问题,去另一个路径改source-git的地址即可)

3.使用SDWebImageWebPCoder

SDImageWebPCoder *webPCoder = [SDImageWebPCoder sharedCoder];
[[SDImageCodersManager sharedManager] addCoder:webPCoder];

NSData *webpData;
UIImage *wimage = [[SDImageWebPCoder sharedCoder] decodedImageWithData:webpData options:nil];
NSData *webpData;
[UIImage sd_imageWithWebPData:webpData];

经测试以上两种写法都能成功加载webp图片

转自:https://www.jianshu.com/p/74fab9c7de77

收起阅读 »

iOS dispatch_semaphore信号量的使用(for循环请求网络时,使用信号量导致死锁)

有的时候我们会遇到这样的需求:循环请求网络,但是在循环的过程中,必须上一个网络回调完成后才能请求下一个网络即进行下一个循环,也就是所谓的多个异步网络做同步请求,首先想到的就是用信号量拦截,但是发现AFNetWorking配合信号量使用时,网络不回调了,是什么原...
继续阅读 »

有的时候我们会遇到这样的需求:
循环请求网络,但是在循环的过程中,必须上一个网络回调完成后才能请求下一个网络即进行下一个循环,也就是所谓的多个异步网络做同步请求,首先想到的就是用信号量拦截,但是发现AFNetWorking配合信号量使用时,网络不回调了,是什么原因引起的网络无法回调。下面我们模拟下正常使用过程并分析,如下:

-(void)semaphoreTest{

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

for (int i = 0; i<10; i++) {
[self semaphoreTestBlock:^(NSString *TNT) {
NSLog(@"任务完成 %d",i);
dispatch_semaphore_signal(semaphore);
}];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"信号量限制 %d",i);
}
}

//这里用延迟模拟异步网络请求
-(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{
/*
queue 的类型无论是串行队列还是并行队列并不影响最终结果
如果 queue = dispatch_get_main_queue() 将会堵塞组线程,造成死锁
*/
dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
block(@"完成");
});
}

这段代码的输出结果为:

2019-10-11 14:40:23.961328+0800 LJC[9013:1358198] 任务完成 0
2019-10-11 14:40:23.961751+0800 LJC[9013:1356826] 信号量限制 0
2019-10-11 14:40:25.061312+0800 LJC[9013:1358198] 任务完成 1
2019-10-11 14:40:25.061673+0800 LJC[9013:1356826] 信号量限制 1
2019-10-11 14:40:26.062082+0800 LJC[9013:1356931] 任务完成 2
2019-10-11 14:40:26.062381+0800 LJC[9013:1356826] 信号量限制 2
2019-10-11 14:40:27.062883+0800 LJC[9013:1356931] 任务完成 3
2019-10-11 14:40:27.063275+0800 LJC[9013:1356826] 信号量限制 3
2019-10-11 14:40:28.160535+0800 LJC[9013:1356931] 任务完成 4
2019-10-11 14:40:28.160988+0800 LJC[9013:1356826] 信号量限制 4
2019-10-11 14:40:29.161327+0800 LJC[9013:1356931] 任务完成 5
2019-10-11 14:40:29.161512+0800 LJC[9013:1356826] 信号量限制 5
2019-10-11 14:40:30.161756+0800 LJC[9013:1356931] 任务完成 6
2019-10-11 14:40:30.161989+0800 LJC[9013:1356826] 信号量限制 6
2019-10-11 14:40:31.261507+0800 LJC[9013:1356931] 任务完成 7
2019-10-11 14:40:31.261912+0800 LJC[9013:1356826] 信号量限制 7
2019-10-11 14:40:32.361503+0800 LJC[9013:1356931] 任务完成 8
2019-10-11 14:40:32.361870+0800 LJC[9013:1356826] 信号量限制 8
2019-10-11 14:40:33.461544+0800 LJC[9013:1358198] 任务完成 9
2019-10-11 14:40:33.461953+0800 LJC[9013:1356826] 信号量限制 9

如果我们把
dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
替换成
dispatch_queue_t queue = dispatch_get_main_queue()
发现输出结果为空

为什么呢?
首先我们要知道
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
他怎么才能实现锁的功能,他的锁其实是针对线程的,我们当前任务是在主线程执行的,我们就需要在主线程上锁。
完成任务我们去将信号量+1,即执行
dispatch_semaphore_signal(semaphore)
这个时候发现你的回调也是在主线程触发的,但是此时主线程上锁,已经卡住了,是不能让你在主线程做任务的,这就形成了相互等待,卡死了,所以我们需要将回调任务放在非主线程中(以目前这个例子来说,就是非主线程,其实我们最终调整的目的是让执行任务和回调任务不在同一线程即可)。

那我们如果将任务(for循环)在子线程中执行,回调在主线程中是否可以呢?下面我们修改代码

-(void)semaphoreTest{

dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

for (int i = 0; i<10; i++) {
[self semaphoreTestBlock:^(NSString *TNT) {
NSLog(@"任务完成 %d",i);
dispatch_semaphore_signal(semaphore);
}];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"信号量限制 %d",i);
}
});
}

-(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{

// dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
block(@"完成");
});
}

输出结果:

2019-10-11 14:51:00.224109+0800 LJC[9063:1362953] 任务完成 0
2019-10-11 14:51:00.224486+0800 LJC[9063:1363099] 信号量限制 0
2019-10-11 14:51:01.325117+0800 LJC[9063:1362953] 任务完成 1
2019-10-11 14:51:01.325493+0800 LJC[9063:1363099] 信号量限制 1
2019-10-11 14:51:02.425129+0800 LJC[9063:1362953] 任务完成 2
2019-10-11 14:51:02.425491+0800 LJC[9063:1363099] 信号量限制 2
2019-10-11 14:51:03.524266+0800 LJC[9063:1362953] 任务完成 3
2019-10-11 14:51:03.524715+0800 LJC[9063:1363099] 信号量限制 3
2019-10-11 14:51:04.625254+0800 LJC[9063:1362953] 任务完成 4
2019-10-11 14:51:04.625659+0800 LJC[9063:1363099] 信号量限制 4
2019-10-11 14:51:05.725228+0800 LJC[9063:1362953] 任务完成 5
2019-10-11 14:51:05.725573+0800 LJC[9063:1363099] 信号量限制 5
2019-10-11 14:51:06.726094+0800 LJC[9063:1362953] 任务完成 6
2019-10-11 14:51:06.726442+0800 LJC[9063:1363099] 信号量限制 6
2019-10-11 14:51:07.825270+0800 LJC[9063:1362953] 任务完成 7
2019-10-11 14:51:07.825613+0800 LJC[9063:1363099] 信号量限制 7
2019-10-11 14:51:08.925323+0800 LJC[9063:1362953] 任务完成 8
2019-10-11 14:51:08.925674+0800 LJC[9063:1363099] 信号量限制 8
2019-10-11 14:51:10.025359+0800 LJC[9063:1362953] 任务完成 9
2019-10-11 14:51:10.025722+0800 LJC[9063:1363099] 信号量限制 9

这就验证了我们的想法, 执行任务和任务回调是不能在一个线程中的

整理

在使用信号量的时候,需要注意 dispatch_semaphore_wait 需要和 任务 放在同一线程,在任务执行异步回调的时候,需要将回调放在与执行任务不同的线程中,因为如果在同一线程中 dispatch_semaphore_wait 操作会造成相互等待导致死锁问题,我们在使用 AFNetWorking 的时候,他默认的回调是在 主线程中,所以我们在配合 AFNetWorking 使用信号量的时候可以指定 AFNetWorking 的回调线程,或者我们在执行任务的时候,将任务放在其他线程

注释:
写这篇文章是因为我在用信号量配合AFNetWorking做网路任务的时候发现一只卡死,在网上找的都说指定AFNetWorking 的 completionQueue ,然后我更改了代码,request是我们网络对AFNetWorking的封装对象实例,按理来说是没问题的,但是不知道为什么还是会造成死锁。目前原因没找到。所以我将for循环再放了子线程中

request.sessionManager.completionQueue = dispatch_get_global_queue(0, 0);

如发现理解错误,望指出 ^_^ THANKS

转自:https://www.jianshu.com/p/91e9e38e3f51

收起阅读 »

iOS 登录接口封装实践

登录。。。基本所有APP都少不了,开始写APP,可能首先就是从登录开始我也一样,我手上有一个封装了所有账户体系相关接口的SDK,运行良好但也遇到一些烦心事,就拿登录来说说吧。首先有如下相关封装,很常见,也无需太多解释:import Foundationpubl...
继续阅读 »

登录。。。基本所有APP都少不了,开始写APP,可能首先就是从登录开始
我也一样,我手上有一个封装了所有账户体系相关接口的SDK,运行良好但也遇到一些烦心事,就拿登录来说说吧。

首先有如下相关封装,很常见,也无需太多解释:

import Foundation

public typealias Response = (_ json: String?, _ error: Error?) -> Void

// 账户体系管理器
public class AccountMgr: NSObject {
private override init() {}
public static let shared = AccountMgr()
}

public extension AccountMgr {
/// 登录
/// - Parameters:
/// - accountType: 账户类型 see `AccountType`
/// - password: 密码
/// - res: 请求结果
func login(by accountType: AccountType, password: String, res: Response?) {
var params = [String: Any]()
switch accountType {
case let .email(email):
params["type"] = "email"
params["email"] = email
case let .mobile(mobile, mobileArea):
params["type"] = "mobile"
params["mobile"] = mobile
params["mobileArea"] = mobileArea
}

params["password"] = password
//网络请求,并回调
//request(type: .post, api: .login, params: params, res: res)
}
}

/// 账号类型
public enum AccountType {
/// 手机号
/// - mobile: 手机号
/// - mobileArea: 国家区号(中国 86)
case mobile(_ phoneNumber: String, mobileArea: String = "86")
/// 邮箱
case email(_ email: String)
}

使用也很方便:

// 分开使用
AccountMgr.shared.login(by: .email(""), password: "", res: nil)
AccountMgr.shared.login(by: .mobile("", mobileArea: ""), password: "", res: nil)

// 合并使用
var loginType: AccountType
if inputEmail {
loginType = .email("test@weixian.com")
} else {
loginType = .mobile("18000000000", mobileArea: "86")
}
AccountMgr.shared.login(by: loginType, password: "xxxxx", res: nil)

无论是邮箱,手机号登录分开逻辑登录,还是统一的登录管理器登录都能胜任,并且只有两种登录,分开写也不会多很多代码。

有一天,这个SDK需要在OC项目中使用

感觉没爱了,懒得想太多,直接废弃了Swift 枚举的便利性,写成了两个方法:

public class AccountMgr: NSObject {
private override init() {}
@objc(shareInstance)
public static let shared = AccountMgr()
}

@objc func loginBy(email: String, password: String, res: Response?)

@objc func loginBy(mobile: String, mobilArea: String, password: String, res: Response?)

之所以写成loginBy(email:)而不是login(by email:),主要是为了swift 转 OC 后使用的时候能直接看懂,也不需要去查看定义,看如下截图就能明白了:


第一个方法不看定义,应该没办法了解参数应该填什么了。

就这样,我的SDK又运行了一段时间,看起来也没什么大问题,无非是手机登录和邮箱登录一定要分开调用罢了

又有一天,这个登录方法要增加用户账号登录

依样画葫芦,我又增加了一个接口~~~,只是这样,那故事就结束了。

可惜,我还有第三方绑定接口,即微信登录后绑定手机,邮箱,或账号、、、、这里又三个接口,还有查询账号信息又三个,还有。。。又三个。。。,还有。。。又三个。。。

这个时候我又开始怀念第一版的接口了,其实这很容易解决,只要一个整型枚举,然后把多出来的参数设置为可选,虽然使用的时候会有点奇怪,但是很好的解决了问题。并且最终我也是这么做的,可我还是想在Swift中能够更好的使用Swfit特性,写出更简洁的代码。。所以我写了两套接口。。。。,一套OC使用,一套Swfit使用,因为我总觉得在不久的将来,我就不需要支持OC了:

首先增加了一个OC的类型枚举:

@objc public enum AccountType_OC: Int {
case mobile
case email
case userId
}

然后增加了一个只有OC可用的方法:

@available(swift 10.0)
@objc func loginBy(accountType: AccountType_OC, account: String, password: String, mobileArea: String?, res: Response?) {
let type = getSwiftAccountType(accountType: accountType, account: account, mobileArea: mobileArea)
login(by: type, password: password, res: res)
}

private func getSwiftAccountType(accountType: AccountType_OC, account: String, mobileArea: String?) -> AccountType {
var type: AccountType
switch accountType {
case .mobile:
guard let mobileArea = mobileArea else { fatalError("need mobile area") }
type = .mobile(account, mobileArea: mobileArea)
case .email:
type = .email(account)
case .userId:
type = .userId(account)
}
return type
}

OC中没办法给参数赋默认值,即类似mobileArea: String = "86" 这种,完全没有用。。。

私有类型转换的方法的封装,使得所有其他方法可以快速转换,关于@available(swift 10.0) 意思就是说只有Swift 版本10.0只后才可以使用。。即变相达到了,在Swift 代码中不会出现这个方法,只有下面方法可以使用:

func login(by accountType: AccountType, password: String, res: Response?)

基本就是这样了,看起来很麻烦,也确实挺麻烦,其实完全可以只保留OC使用的方法,这完全归于我的代码洁癖,以及我自己在使用Swift和对于日后去掉OC支持时我可以快乐的删代码的白日幻想。

当然,如果你只是在自己的混编APP内部封装一些接口,那一套接口应该是比较好的,如果你的是SDK,同时你也不是很怕麻烦,像我这样写也许会有一些意外的收获。

链接:https://www.jianshu.com/p/247c1e923c5c

收起阅读 »

iOS自定义键盘-简单版

为什么说是简单版,因为这里只说一个数字键盘。一,怎么自定义键盘随便一个view都可以作为键盘,主要代码是为你的输入框指定inputView,这个inputView就是键盘,键盘具体什么样子都可以。kfZNumberKeyBoard * mkb = [kfZNu...
继续阅读 »

为什么说是简单版,因为这里只说一个数字键盘。
一,怎么自定义键盘
随便一个view都可以作为键盘,主要代码是为你的输入框指定inputView,这个inputView就是键盘,键盘具体什么样子都可以。

kfZNumberKeyBoard * mkb = [kfZNumberKeyBoard moneyKeyBoardBuyer];

UITextField * field = [[UITextField alloc]init];
field.backgroundColor = [UIColor cyanColor];
field.inputView = mkb;
[self.view addSubview:field];
field.frame = CGRectMake(20, NavBottom + 50, DEF_SCREEN_WIDTH - 40, 40);

二,自定义键盘怎么实现各种输入
这里千万不要自己拼接字符串太容易出问题了,用系统自带的方法。我们发现不管UITextField还是UITextView都遵循UITextInput协议,这个协议又遵循UIKeyInput协议,我们用的就是UIKeyInput协议中的方法。

- (void)insertText:(NSString *)text;//插入文字,不用处理光标位置
- (void)deleteBackward;//删除,不用处理光标位置

用这两个方法是不是事情就特别简单了,其实说到这里已经可以了,怎么做都说完了。不过我还是推销一下我写的数字键盘吧。最后面我会贴出代码用的可以拷贝改一下。

三,数字键盘
先看效果图:


a.UI布局上,删除和确定是单独的按键,其他部分我用了collectionView,想着之后做的乱序加密效果好做,打乱数据源刷新一下就行(当然现在没有,不是懒,过渡开发是病)
b.获取当前输入框,这里为了不在外面传,直接在内部监听了输入框开始输入和结束输入。
c.加了几个输入限制:
1.有小数点不能在输入小数点
2.内容为空输入小数点时,前面自动补0
3.最大小数位数限制(测试不多可能有bug哦)
4.移除焦点时小数点前面没东西自动补0
5.输入框有内容确定可以点击,输入框没内容确定不能点击。

下面是代码了:

@interface kfZNumberKeyBoard : UIView

/** 确认按键 */
@property (nonatomic, strong) UIButton * returnButton;
/** 有没有小数点 */
@property (nonatomic, assign) BOOL hiddenPoint;
/** 小数位数,为0不限制,不需要小数时请使用hiddenPoint隐藏点 默认是2 */
@property (nonatomic, assign) NSUInteger decimalCount;
/** 整体高度 */
@property (nonatomic, assign, readonly) CGFloat KFZNumberKeyBoardHeight;

+(instancetype)moneyKeyBoardBuyer;
+(instancetype)moneyKeyBoardSeller;

-(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint;

@end
#import "kfZNumberKeyBoard.h"
#import "KFZKeyBoardCell.h"
@interface kfZNumberKeyBoard ()

@property(nonatomic, weak) UIView * textInputView;

/** 删除按键 */
@property (nonatomic, strong) UIButton * deleteButton;

@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) NSArray *dataSource;

/** 间隔 */
@property (nonatomic, assign) CGFloat KFZNumberKeyBoardSpace;
/** 数字按键高度 */
@property (nonatomic, assign) CGFloat KFZNumberKeyBoardItemHeight;

@end

@implementation kfZNumberKeyBoard

+(instancetype)moneyKeyBoardBuyer{
kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
return keyBoard;
}

+(instancetype)moneyKeyBoardSeller{
kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
keyBoard.returnButton.backgroundColor = [UIColor maintonal_sellerMain];
return keyBoard;
}

-(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint{
self = [super init];
if (self) {
_hiddenPoint = hiddenPoint;
_KFZNumberKeyBoardItemHeight = 50.f;
_KFZNumberKeyBoardSpace = 0.5;
_KFZNumberKeyBoardHeight = _KFZNumberKeyBoardItemHeight * 4 + _KFZNumberKeyBoardSpace * 5 + HOMEINDICATOR_HEIGHT;
_decimalCount = 2;

self.frame = CGRectMake(0, 0, DEF_SCREEN_WIDTH, _KFZNumberKeyBoardHeight);

_deleteButton = [[UIButton alloc]init];
_deleteButton.backgroundColor = [UIColor color_FAFAFA];
[_deleteButton setImage:[UIImage imageNamed:@"keyboard_icon_backspace"] forState:UIControlStateNormal];
[_deleteButton addTarget:self action:@selector(deleteEvent) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_deleteButton];
[_deleteButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(_KFZNumberKeyBoardSpace);
make.right.mas_equalTo(0.f);
make.width.equalTo(self).multipliedBy(0.25);
}];

_returnButton = [[UIButton alloc]init];
[_returnButton setTitle:@"确定" forState:UIControlStateNormal];
[_returnButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_returnButton.titleLabel.font = [UIFont custemFontOfSize:20 weight:UIFontWeightRegular];
_returnButton.backgroundColor = [UIColor mainTonal_main];
[_returnButton addTarget:self action:@selector(returnEvent) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_returnButton];
[_returnButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_deleteButton.mas_bottom);
make.right.equalTo(_deleteButton);
make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
make.height.equalTo(_deleteButton);
make.width.equalTo(_deleteButton).offset(_KFZNumberKeyBoardSpace);
}];

//101对应小数点 102对应收起键盘 修改的话其他的判断逻辑也要修改
_dataSource = @[@(1), @(2), @(3), @(4), @(5), @(6), @(7), @(8), @(9), @(101), @(0), @(102)];

UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init];
layout.itemSize = CGSizeMake((DEF_SCREEN_WIDTH * 3.f/4.f - _KFZNumberKeyBoardSpace*3)/3.f, (_KFZNumberKeyBoardHeight - HOMEINDICATOR_HEIGHT - _KFZNumberKeyBoardSpace*5)/4.f);
layout.sectionInset = UIEdgeInsetsMake(_KFZNumberKeyBoardSpace, 0, _KFZNumberKeyBoardSpace, _KFZNumberKeyBoardSpace);
layout.minimumLineSpacing = _KFZNumberKeyBoardSpace;
layout.minimumInteritemSpacing = _KFZNumberKeyBoardSpace;

_collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView.dataSource = self;
_collectionView.delegate = self;
[_collectionView registerClass:[KFZKeyBoardCell class] forCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class])];
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.scrollEnabled = NO;
[self addSubview:_collectionView];
[_collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.mas_equalTo(0.f);
make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
make.right.equalTo(_deleteButton.mas_left);
}];


[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
}
return self;
}

-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidEndEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
}

#pragma mark - response

-(void)textInputWithNumber:(NSNumber *)number{
NSString *strValue = [self inputViewString];

if ([number isEqualToNumber:@(101)]) {
if ([strValue containsString:@"."]){
return;
}else{
if ([strValue length] <= 0)
[self.textInputView insertText:@"0."];
else
[self.textInputView insertText:@"."];
}
}else{
if ([strValue containsString:@"."] && _decimalCount > 0) {
NSInteger pointLocation = [strValue rangeOfString:@"."].location;
NSInteger curDecimalCount = strValue.length - pointLocation - 1;
if (curDecimalCount >= _decimalCount) {
NSInteger cursorLocation = [self inputViewSelectRangeLocation];
if (cursorLocation <= pointLocation) {
[_textInputView insertText:number.stringValue];
}
}else{
[_textInputView insertText:number.stringValue];
}
}else{
[_textInputView insertText:number.stringValue];
}
}
[self freshReturnButtonEnabled];
}

-(void)deleteEvent{
[_textInputView deleteBackward];
[self freshReturnButtonEnabled];
}

-(void)returnEvent{
[_textInputView resignFirstResponder];
}

-(void)textInputViewDidBeginEditing:(NSNotification*)notification{
_textInputView = notification.object;
[self freshReturnButtonEnabled];
}

-(void)textInputViewDidEndEditing:(NSNotification*)notification{
NSString *strValue = [self inputViewString];
if ([strValue startsWithString:@"."]) {
strValue = [NSString stringWithFormat:@"0%@", strValue];
[self setInputViewString:strValue];
}
_textInputView = nil;

}

-(NSString *)inputViewString{
NSString *strValue = @"";
if ([self.textInputView isKindOfClass:[UITextView class]]){
strValue = ((UITextView *)self.textInputView).text;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
strValue = ((UITextField *)self.textInputView).text;
}
return strValue;
}

-(void)setInputViewString:(NSString *)string{
if ([self.textInputView isKindOfClass:[UITextView class]]){
((UITextView *)self.textInputView).text = string;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
((UITextField *)self.textInputView).text = string;
}
}

-(NSInteger)inputViewSelectRangeLocation{
NSInteger location = 0;
if ([self.textInputView isKindOfClass:[UITextView class]]){
UITextView * textView = (UITextView *)self.textInputView;
location = textView.selectedRange.location;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
UITextField *textField = (UITextField *)self.textInputView;
UITextPosition* beginning = textField.beginningOfDocument;
UITextRange* selectedRange = textField.selectedTextRange;
UITextPosition* selectionStart = selectedRange.start;
location = [textField offsetFromPosition:beginning toPosition:selectionStart];
}
return location;
}

-(void)freshReturnButtonEnabled{
NSString *strValue = [self inputViewString];
if (strValue.length == 0) {
_returnButton.enabled = NO;
_returnButton.alpha = 0.6;
}else{
_returnButton.enabled = YES;
_returnButton.alpha = 1.f;
}
}

#pragma mark -- Delegate
#pragma mark - UICollectionViewDataSource

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

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
KFZKeyBoardCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class]) forIndexPath:indexPath];
NSNumber * number = self.dataSource[indexPath.row];
if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
cell.textLabel.text = @"";
}else{
cell.textNumber = number;
}
return cell;
}

#pragma mark - UICollectionViewDelegate

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
NSNumber * number = self.dataSource[indexPath.row];
if ([number isEqualToNumber:@(102)]) {
[_textInputView resignFirstResponder];
return;
}
if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
return;
}
[self textInputWithNumber:number];
}

#pragma mark - init

-(void)setHiddenPoint:(BOOL)hiddenPoint{
_hiddenPoint = hiddenPoint;
[_collectionView reloadData];
}

@end

这个是里面cell的:

@interface KFZKeyBoardCell : UICollectionViewCell
/** 文字 */
@property (nonatomic, strong) UILabel * textLabel;
/** 图片 */
@property (nonatomic, strong) UIImageView * imageIcon;

/** 设置值 */
@property (nonatomic, strong) NSNumber * textNumber;
@end
#import "KFZKeyBoardCell.h"

@implementation KFZKeyBoardCell

- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor color_FAFAFA];

[self.contentView addSubview:self.textLabel];
[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.left.right.mas_equalTo(0.f);
}];

self.imageIcon.hidden = YES;
[self.contentView addSubview:self.imageIcon];
[self.imageIcon mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(CGPointZero);
make.size.mas_equalTo(CGSizeMake(24.f, 22.f));
}];
}
return self;
}
-(void)prepareForReuse{
self.textLabel.hidden = NO;
self.imageIcon.hidden = YES;
}
- (void)setTextNumber:(NSNumber *)textNumber{
_textNumber = textNumber;
if ([textNumber isEqualToNumber:@(101)]) {
self.textLabel.text = @"·";
}
else if ([textNumber isEqualToNumber:@(102)]){
self.textLabel.hidden = YES;
self.imageIcon.hidden = NO;
self.imageIcon.image = [UIImage imageNamed:@"keyboard_icon_smallkb"];
}
else{
self.textLabel.text = textNumber.stringValue;
}
}

- (UILabel *)textLabel{
if (!_textLabel) {
_textLabel = [[UILabel alloc]init];
_textLabel.font = [UIFont KFZSpecial_DINAlternateBoldWithFontSize:24.f];
_textLabel.textAlignment = NSTextAlignmentCenter;
_textLabel.userInteractionEnabled = NO;
_textLabel.backgroundColor = UIColor.clearColor;
}
return _textLabel;
}

-(UIImageView *)imageIcon{
if (!_imageIcon) {
_imageIcon = [[UIImageView alloc]init];
}
return _imageIcon;
}

@end

转自:https://www.jianshu.com/p/226f67166770

收起阅读 »

iOS 设备信息获取

1.获取电池电量(一般用百分数表示,大家自行处理就好)-(CGFloat)getBatteryQuantity{ return [[UIDevice currentDevice] batteryLevel];}2.获取电池状态(UIDeviceBatte...
继续阅读 »

1.获取电池电量(一般用百分数表示,大家自行处理就好)

-(CGFloat)getBatteryQuantity
{
return [[UIDevice currentDevice] batteryLevel];
}

2.获取电池状态(UIDeviceBatteryState为枚举类型)

-(UIDeviceBatteryState)getBatteryStauts
{
return [UIDevice currentDevice].batteryState;
}

3.获取总内存大小

-(long long)getTotalMemorySize
{
return [NSProcessInfo processInfo].physicalMemory;
}

4.获取当前可用内存

-(long long)getAvailableMemorySize
{
vm_statistics_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
if (kernReturn != KERN_SUCCESS)
{
return NSNotFound;
}
return ((vm_page_size * vmStats.free_count + vm_page_size * vmStats.inactive_count));
}

5.获取已使用内存

- (double)getUsedMemory
{
task_basic_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(),
TASK_BASIC_INFO,
(task_info_t)&taskInfo,
&infoCount);

if (kernReturn != KERN_SUCCESS
) {
return NSNotFound;
}

return taskInfo.resident_size;
}

6.获取总磁盘容量

include 
-(long long)getTotalDiskSize
{
struct statfs buf;
unsigned long long freeSpace = -1;
if (statfs("/var", &buf) >= 0)
{
freeSpace = (unsigned long long)(buf.f_bsize * buf.f_blocks);
}
return freeSpace;
}

7.获取可用磁盘容量

-(long long)getAvailableDiskSize
{
struct statfs buf;
unsigned long long freeSpace = -1;
if (statfs("/var", &buf) >= 0)
{
freeSpace = (unsigned long long)(buf.f_bsize * buf.f_bavail);
}
return freeSpace;
}

8.容量转换

-(NSString *)fileSizeToString:(unsigned long long)fileSize
{
NSInteger KB = 1024;
NSInteger MB = KB*KB;
NSInteger GB = MB*KB;

if (fileSize < 10) {
return @"0 B";
}else if (fileSize < KB) {
return @"< 1 KB";
}else if (fileSize < MB) {
return [NSString stringWithFormat:@"%.1f KB",((CGFloat)fileSize)/KB];
}else if (fileSize < GB) {
return [NSString stringWithFormat:@"%.1f MB",((CGFloat)fileSize)/MB];
}else {
return [NSString stringWithFormat:@"%.1f GB",((CGFloat)fileSize)/GB];
}
}

9.型号

#import 

+ (NSString *)getCurrentDeviceModel:(UIViewController *)controller
{
int mib[2];
size_t len;
char *machine;

mib[0] = CTL_HW;
mib[1] = HW_MACHINE;
sysctl(mib, 2, NULL, &len, NULL, 0);
machine = malloc(len);
sysctl(mib, 2, machine, &len, NULL, 0);

NSString *platform = [NSString stringWithCString:machine encoding:NSASCIIStringEncoding];
free(machine);

if ([platform isEqualToString:@"iPhone3,1"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,2"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,3"]) return @"iPhone 4 (A1349)";
if ([platform isEqualToString:@"iPhone4,1"]) return @"iPhone 4s (A1387/A1431)";
if ([platform isEqualToString:@"iPhone5,1"]) return @"iPhone 5 (A1428)";
if ([platform isEqualToString:@"iPhone5,2"]) return @"iPhone 5 (A1429/A1442)";
if ([platform isEqualToString:@"iPhone5,3"]) return @"iPhone 5c (A1456/A1532)";
if ([platform isEqualToString:@"iPhone5,4"]) return @"iPhone 5c (A1507/A1516/A1526/A1529)";
if ([platform isEqualToString:@"iPhone6,1"]) return @"iPhone 5s (A1453/A1533)";
if ([platform isEqualToString:@"iPhone6,2"]) return @"iPhone 5s (A1457/A1518/A1528/A1530)";
if ([platform isEqualToString:@"iPhone7,1"]) return @"iPhone 6 Plus (A1522/A1524)";
if ([platform isEqualToString:@"iPhone7,2"]) return @"iPhone 6 (A1549/A1586)";
if ([platform isEqualToString:@"iPhone8,1"]) return @"iPhone 6s";
if ([platform isEqualToString:@"iPhone8,2"]) return @"iPhone 6s Plus";
if ([platform isEqualToString:@"iPhone8,4"]) return @"iPhone SE";
if ([platform isEqualToString:@"iPhone9,1"]) return @"国行、日版、港行iPhone 7";
if ([platform isEqualToString:@"iPhone9,2"]) return @"港行、国行iPhone 7 Plus";
if ([platform isEqualToString:@"iPhone9,3"]) return @"美版、台版iPhone 7";
if ([platform isEqualToString:@"iPhone9,4"]) return @"美版、台版iPhone 7 Plus";
if ([platform isEqualToString:@"iPhone10,1"]) return @"国行(A1863)、日行(A1906)iPhone 8";
if ([platform isEqualToString:@"iPhone10,4"]) return @"美版(Global/A1905)iPhone 8";
if ([platform isEqualToString:@"iPhone10,2"]) return @"国行(A1864)、日行(A1898)iPhone 8 Plus";
if ([platform isEqualToString:@"iPhone10,5"]) return @"美版(Global/A1897)iPhone 8 Plus";
if ([platform isEqualToString:@"iPhone10,3"]) return @"国行(A1865)、日行(A1902)iPhone X";
if ([platform isEqualToString:@"iPhone10,6"]) return @"美版(Global/A1901)iPhone X";

if ([platform isEqualToString:@"iPod1,1"]) return @"iPod Touch 1G (A1213)";
if ([platform isEqualToString:@"iPod2,1"]) return @"iPod Touch 2G (A1288)";
if ([platform isEqualToString:@"iPod3,1"]) return @"iPod Touch 3G (A1318)";
if ([platform isEqualToString:@"iPod4,1"]) return @"iPod Touch 4G (A1367)";
if ([platform isEqualToString:@"iPod5,1"]) return @"iPod Touch 5G (A1421/A1509)";

if ([platform isEqualToString:@"iPad1,1"]) return @"iPad 1G (A1219/A1337)";
if ([platform isEqualToString:@"iPad2,1"]) return @"iPad 2 (A1395)";
if ([platform isEqualToString:@"iPad2,2"]) return @"iPad 2 (A1396)";
if ([platform isEqualToString:@"iPad2,3"]) return @"iPad 2 (A1397)";
if ([platform isEqualToString:@"iPad2,4"]) return @"iPad 2 (A1395+New Chip)";
if ([platform isEqualToString:@"iPad2,5"]) return @"iPad Mini 1G (A1432)";
if ([platform isEqualToString:@"iPad2,6"]) return @"iPad Mini 1G (A1454)";
if ([platform isEqualToString:@"iPad2,7"]) return @"iPad Mini 1G (A1455)";

if ([platform isEqualToString:@"iPad3,1"]) return @"iPad 3 (A1416)";
if ([platform isEqualToString:@"iPad3,2"]) return @"iPad 3 (A1403)";
if ([platform isEqualToString:@"iPad3,3"]) return @"iPad 3 (A1430)";
if ([platform isEqualToString:@"iPad3,4"]) return @"iPad 4 (A1458)";
if ([platform isEqualToString:@"iPad3,5"]) return @"iPad 4 (A1459)";
if ([platform isEqualToString:@"iPad3,6"]) return @"iPad 4 (A1460)";

if ([platform isEqualToString:@"iPad4,1"]) return @"iPad Air (A1474)";
if ([platform isEqualToString:@"iPad4,2"]) return @"iPad Air (A1475)";
if ([platform isEqualToString:@"iPad4,3"]) return @"iPad Air (A1476)";
if ([platform isEqualToString:@"iPad4,4"]) return @"iPad Mini 2G (A1489)";
if ([platform isEqualToString:@"iPad4,5"]) return @"iPad Mini 2G (A1490)";
if ([platform isEqualToString:@"iPad4,6"]) return @"iPad Mini 2G (A1491)";
if ([platform isEqualToString:@"iPad4,7"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad4,8"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad4,9"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad5,1"]) return @"iPad Mini 4 (WiFi)";
if ([platform isEqualToString:@"iPad5,2"]) return @"iPad Mini 4 (LTE)";
if ([platform isEqualToString:@"iPad5,3"]) return @"iPad Air 2";
if ([platform isEqualToString:@"iPad5,4"]) return @"iPad Air 2";
if ([platform isEqualToString:@"iPad6,3"]) return @"iPad Pro 9.7";
if ([platform isEqualToString:@"iPad6,4"]) return @"iPad Pro 9.7";
if ([platform isEqualToString:@"iPad6,7"]) return @"iPad Pro 12.9";
if ([platform isEqualToString:@"iPad6,8"]) return @"iPad Pro 12.9";
if ([platform isEqualToString:@"iPad6,11"]) return @"iPad 5 (WiFi)";
if ([platform isEqualToString:@"iPad6,12"]) return @"iPad 5 (Cellular)";
if ([platform isEqualToString:@"iPad7,1"]) return @"iPad Pro 12.9 inch 2nd gen (WiFi)";
if ([platform isEqualToString:@"iPad7,2"]) return @"iPad Pro 12.9 inch 2nd gen (Cellular)";
if ([platform isEqualToString:@"iPad7,3"]) return @"iPad Pro 10.5 inch (WiFi)";
if ([platform isEqualToString:@"iPad7,4"]) return @"iPad Pro 10.5 inch (Cellular)";

if ([platform isEqualToString:@"AppleTV2,1"]) return @"Apple TV 2";
if ([platform isEqualToString:@"AppleTV3,1"]) return @"Apple TV 3";
if ([platform isEqualToString:@"AppleTV3,2"]) return @"Apple TV 3";
if ([platform isEqualToString:@"AppleTV5,3"]) return @"Apple TV 4";

if ([platform isEqualToString:@"i386"]) return @"iPhone Simulator";
if ([platform isEqualToString:@"x86_64"]) return @"iPhone Simulator";
return platform;
}

10.IP地址

#import 和#import 

- (NSString *)deviceIPAdress {
NSString *address = @"an error occurred when obtaining ip address";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;

success = getifaddrs(&interfaces);

if (success == 0) { // 0 表示获取成功

temp_addr = interfaces;
while (temp_addr != NULL) {
if( temp_addr->ifa_addr->sa_family == AF_INET) {
// Check if interface is en0 which is the wifi connection on the iPhone
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// Get NSString from C String
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}

temp_addr = temp_addr->ifa_next;
}
}

freeifaddrs(interfaces);
return address;
}

11.当前手机连接的WIFI名称(SSID)

需要#import 

- (NSString *)getWifiName
{
NSString *wifiName = nil;

CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
if (!wifiInterfaces) {
return nil;
}

NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;

for (NSString *interfaceName in interfaces) {
CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));

if (dictRef) {
NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;

wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];

CFRelease(dictRef);
}
}

CFRelease(wifiInterfaces);
return wifiName;
}

12.当前手机系統版本

[[[UIDevice currentDevice] systemVersion] floatValue] ;


摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/b25cdf09ece2

收起阅读 »

WKWebView的特性及原理

WKWebView是在Apple的WWDC 2014随iOS 8和OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。使用UIWebView加载网页的时候,我们会发现内存会无限增长,还有内存泄漏的问题存在。WebKit中更新的...
继续阅读 »

WKWebView是在Apple的WWDC 2014随iOS 8和OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。

使用UIWebView加载网页的时候,我们会发现内存会无限增长,还有内存泄漏的问题存在。

WebKit中更新的WKWebView控件的新特性与使用方法,它很好的解决了UIWebView存在的内存、加载速度等诸多问题。

一、WKWebView新特性

在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存);

允许JavaScript的Nitro库加载并使用(UIWebView中限制);

支持了更多的HTML5特性;

高达60fps的滚动刷新率以及内置手势;

将UIWebViewDelegate与UIWebView重构成了14类与3个协议查看苹果官方文档

二、WebKit框架概览


如上图所示,WebKit框架中最核心的类应该属于WKWebView了,这个类专门用来渲染网页视图,其他类和协议都将基于它和服务于它。

WKWebView:网页的渲染与展示,通过WKWebViewConfiguration可以进行自定义配置

WKWebViewConfiguration:这个类专门用来配置WKWebView。

WKPreference:这个类用来进行相关webView设置。

WKProcessPool:这个类用来配置进程池,与网页视图的资源共享有关。

WKUserContentController:这个类主要用来做native与JavaScript的交互管理。

WKUserScript:用于进行JavaScript注入。

WKScriptMessageHandler:这个类专门用来处理JavaScript调用native的方法。

WKNavigationDelegate:网页跳转间的导航管理协议,这个协议可以监听网页的活动

WKNavigationAction:网页某个活动的示例化对象。

WKUIDelegate:用于交互处理JavaScript中的一些弹出框。

WKBackForwardList:堆栈管理的网页列表。

WKBackForwardListItem:每个网页节点对象。

三、WKWebView的属性

/// webView的自定义配置
@property (nonatomic,readonly, copy) WKWebViewConfiguration *configuration;
/// 导航代理
@property (nullable, nonatomic, weak)id navigationDelegate;
/// UI代理
@property (nullable, nonatomic, weak)id UIDelegate;
/// 访问过网页历史列表
@property (nonatomic,readonly, strong) WKBackForwardList *backForwardList;

/// 自定义初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
/// url加载webView视图
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
/// 文件加载webView视图
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));
/// HTMLString字符串加载webView视图
- (nullable WKNavigation *)loadHTMLString:(NSString *)stringbaseURL:(nullable NSURL *)baseURL;
/// NSData数据加载webView视图
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0));
/// 返回上一个网页节点
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

/// 网页的标题
@property (nullable, nonatomic,readonly, copy) NSString *title;
/// 网页的URL地址
@property (nullable, nonatomic,readonly, copy) NSURL *URL;
/// 网页是否正在加载
@property (nonatomic,readonly, getter=isLoading) BOOL loading;
/// 加载的进度 范围为[0, 1]
@property (nonatomic,readonly)double estimatedProgress;
/// 网页链接是否安全
@property (nonatomic,readonly) BOOL hasOnlySecureContent;
/// 证书服务
@property (nonatomic,readonly, nullable) SecTrustRef serverTrust API_AVAILABLE(macosx(10.12), ios(10.0));
/// 是否可以返回
@property (nonatomic,readonly) BOOL canGoBack;
/// 是否可以前进
@property (nonatomic,readonly) BOOL canGoForward;

/// 返回到上一个网页
- (nullable WKNavigation *)goBack;
/// 前进到下一个网页
- (nullable WKNavigation *)goForward;
/// 重新加载
- (nullable WKNavigation *)reload;
/// 忽略缓存 重新加载
- (nullable WKNavigation *)reloadFromOrigin;
/// 停止加载
- (void)stopLoading;
/// 执行JavaScript
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^ _Nullable)(_Nullableid, NSError * _Nullable error))completionHandler;

/// 是否允许左右滑动,返回-前进操作 默认是NO
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
/// 自定义代理字符串
@property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));
/// 在iOS上默认为NO,标识不允许链接预览
@property (nonatomic) BOOL allowsLinkPreview API_AVAILABLE(macosx(10.11), ios(9.0));
/// 滚动视图
@property (nonatomic,readonly, strong) UIScrollView *scrollView;
/// 是否支持放大手势,默认为NO
@property (nonatomic) BOOL allowsMagnification;
/// 放大因子,默认为1
@property (nonatomic) CGFloat magnification;
/// 据设置的缩放因子来缩放页面,并居中显示结果在指定的点

- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;/// 证书列表@property (nonatomic,readonly, copy) NSArray *certificateChain API_DEPRECATED_WITH_REPLACEMENT("serverTrust", macosx(10.11,10.12), ios(9.0,10.0));

四、WKWebView的使用
简单使用,直接加载url地址

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://developer.apple.com/reference/webkit"]]];
[self.view addSubview:webView];

自定义配置
再WKWebView里面注册供JS调用的方法,是通过WKUserContentController类下面的方法:

- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;

// 创建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];

// 创建UserContentController(提供JavaScript向webView发送消息的方法)
WKUserContentController* userContent = [[WKUserContentController alloc] init];

// 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
[userContent addScriptMessageHandler:self name:@"NativeMethod"];

// 将UserConttentController设置到配置文件
config.userContentController = userContent;

// 高端的自定义配置创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
// 设置访问的
URLNSURL *url = [NSURL URLWithString:@"https://developer.apple.com/reference/webkit"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
[self.view addSubview:webView];

// 实现WKScriptMessageHandler协议方法

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

  // 判断是否是调用原生的
if([@"NativeMethod" isEqualToString:message.name]) {
// 判断message的内容,然后做相应的操作
if([@"close" isEqualToString:message.body]) {
}
}
}

注意:上面将当前ViewController设置为MessageHandler之后需要在当前ViewController销毁前将其移除,否则会造成内存泄漏。

[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];

五、WKNavigationDelegate代理方法
如果实现了代理方法,一定要在decidePolicyForNavigationAction和decidePolicyForNavigationResponse方法中的回调设置允许跳转。

typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {

WKNavigationActionPolicyCancel, // 取消跳转

WKNavigationActionPolicyAllow, // 允许跳转

} API_AVAILABLE(macosx(10.10), ios(8.0));

1.在发送请求之前,决定是否跳转

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler {

NSLog(@"1-------在发送请求之前,决定是否跳转 -->%@",navigationAction.request);

decisionHandler(WKNavigationActionPolicyAllow);
}

2. 页面开始加载时调用

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@"2-------页面开始加载时调用");
}

3.在收到响应后,决定是否跳转

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisionHandler {
/// 在收到服务器的响应头,根据response相关信息,决定是否跳转。decisionHandler必须调用,来决定是否跳转,参数WKNavigationActionPolicyCancel取消跳转,WKNavigationActionPolicyAllow允许跳转    NSLog(@"3-------在收到响应后,决定是否跳转");

decisionHandler(WKNavigationResponsePolicyAllow);

4. 当内容开始返回时调用

- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {

NSLog(@"4-------当内容开始返回时调用");
}

5 页面加载完成之后调用

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

NSLog(@"5-------页面加载完成之后调用");
}

6 页面加载失败时调用

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@"6-------页面加载失败时调用");
}

7.接收到服务器跳转请求之后调用

- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@"-------接收到服务器跳转请求之后调用");
}

8.数据加载发生错误时调用

- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {

NSLog(@"----数据加载发生错误时调用");
}

9.需要响应身份验证时调用 同样在block中需要传入用户身份凭证

- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {

//用户身份信息 NSLog(@"----需要响应身份验证时调用 同样在block中需要传入用户身份凭证");

NSURLCredential *newCred = [NSURLCredential credentialWithUser:@"" password:@"" persistence:NSURLCredentialPersistenceNone];

// 为 challenge 的发送方提供 credential [[challenge sender] useCredential:newCred forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);

}

10.进程被终止时调用

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {

NSLog(@"----------进程被终止时调用");
}

六、WKUIDelegate代理方法

/**
* web界面中有弹出警告框时调用
*
* @param webView 实现该代理的webview
* @param message 警告框中的内容
* @param completionHandler 警告框消失调用
*/

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(void(^)())completionHandler {

NSLog(@"-------web界面中有弹出警告框时调用");
}


* 创建新的webView时调用的方法

- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {

NSLog(@"-----创建新的webView时调用的方法");

return webView;

}

// 关闭webView时调用的方法

- (void)webViewDidClose:(WKWebView *)webView {

NSLog(@"----关闭webView时调用的方法");

}

// 下面这些方法是交互JavaScript的方法

// JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去

-(void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(BOOL))completionHandler {

NSLog(@"%@",message);

completionHandler(YES);

}

// JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入

-(void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSString * _Nullable))completionHandler{

NSLog(@"%@",prompt);

completionHandler(@"123");

}

// 默认预览元素调用

- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {

NSLog(@"-----默认预览元素调用");

return YES;

}

// 返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。

- (nullable UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray> *)previewActions {

NSLog(@"----返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。");

return self;

}

// 允许应用程序向它创建的视图控制器弹出

- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController {

NSLog(@"----允许应用程序向它创建的视图控制器弹出");

}

// 显示一个文件上传面板。completionhandler完成处理程序调用后打开面板已被撤销。通过选择的网址,如果用户选择确定,否则为零。如果不实现此方法,Web视图将表现为如果用户选择了取消按钮。

- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSArray * _Nullable URLs))completionHandler {

NSLog(@"----显示一个文件上传面板");

}


摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/1fd78ec144bb

收起阅读 »

taro-ui实现省市区三级联动

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

        <Picker

  mode="multiSelector" // 多列选择
onChange={this.onChange} // change事件
onColumnChange={this.onColumnChange} // 某列改变的事件
range={rangeData} //需要展示的数据
value={rangeKey} // 选择的下标
>
<View className="picker">
<Text className="label">所在地址:</Text>
{formData.province && (
<Text>
{formData.province}
{formData.city}
{formData.country}
</Text>
)} // 主要是数据回显加的代码,
{!formData.province && (
<Text className="placeholder">请选择省/市/区</Text>
)}
</View>
</Picker>


上述代码其实taro-ui官方文档都有具体的事例,这里就不多解释了。

相信每个的省市区结构都不一样,现在贴一部分自己项目的省市区结构

[{
provinceName: '北京市',
provinceCode: '11',
cities: [
{
cityName: '市辖区',
cityCode: '1101',
countries: [
{
countryCode: "110101"
countryName: "东城区"
}
]
}
]
}]

现在开始处理数据,因为rangeData是所有数据,省市区,我们需要把数据转换成[‘省’, ‘市’, ‘区’]。

handleCityData = key => {
// 处理数据。
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange, // 省市区所有的数据
rangeKey: key // key是多列选择器需要展示的下标,因为是初始化,所以我们传入[0,0,0]
});
};

数据处理代码有点丑,欢迎大家提意见。因babel没升级到7版本,所以if判断有点繁琐。

数据处理完了之后,我们需要开始处理每列的值改变,数据联动了,那么我们需要列联动事件。

onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) { // 根据改变不同的列,来显示不同的数据
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};

到这里的话,就基本实现了省市区三级联动。

下面说一哈,省市区数据回显的代码,不需要的朋友也可以了解一哈。
数据回显,其实很简单,只要找到对应的省市区的下标,就可以回显了。下面是具体实现代码:

getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
};

通过上面的循环找出对应省市区的下标,就可以实现省市区的数据回显。

噢,还忘了多列选择器的change事件,这个的话,根据自己项目需要返回的是code还是name,这块就自己处理了,我这边讲的主要是省市区的三级联动。
我是把省市区写成一个组件,然后在父节点传入对应的数据以及事件就可以在一个项目中多次用到了。

下面是该组件的所有代码

import Taro, { Component } from "@tarojs/taro";
import { View, Text, Image, ScrollView, Picker } from "@tarojs/components";
import { connect } from "@tarojs/redux";
import * as actions from "@actions/address";
// import { dispatchCartNum } from '@actions/cart';
import "./index.scss";

@connect(state => state.address, { ...actions })
class ChangeCity extends Component {
static defaultProps = {
detailAddress: {}
};
constructor(props) {
super(props);
this.state = {
addressData: [],
rangeKey: [0, 0, 0],
rangeData: [[], [], []],
formData: {
province: "",
city: "",
country: ""
}
};
}

componentDidMount() {
this.getAddress();
}
getAddress = () => {
this.props.dispatchAddressChina().then(res => {
let addressData = [...res.data];
this.setState(
{
addressData: addressData
},
() => {
let { detailAddress } = this.props;
if (!detailAddress.province) {
this.handleCityData([0, 0, 0]);
} else {
this.getRangeKey(detailAddress);
}
}
);
});
};
getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
this.setState({
rangeKey: rangeKey
});
};
handleCityData = key => {
// 处理数据
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange,
rangeKey: key
});
};
onChange = e => {
let { value } = e.detail;
this.getAddressName(value);
};
getAddressName = value => {
// 这里是转化用户选择的地址数据
let { addressData } = this.state;
let formData = {
province: "",
city: "",
country: ""
};
let payload = {
province: "",
city: "",
country: ""
};
if (addressData[value[0]]) {
formData.province = addressData[value[0]].provinceName; // 省名称
payload.province = addressData[value[0]].provinceCode; // 省code
if (
addressData[value[0]].cities &&
addressData[value[0]].cities[value[1]]
) {
formData.city = addressData[value[0]].cities[value[1]].cityName;
payload.city = addressData[value[0]].cities[value[1]].cityCode;
if (
addressData[value[0]].cities[value[1]].countries &&
addressData[value[0]].cities[value[1]].countries[value[2]]
) {
formData.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryName;
payload.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryCode;
}
}
}
// console.log(formData, "formData");
this.setState({
formData: formData
});
this.props.onChangeAddress(payload, formData);
};
onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) {
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};
render() {
const { formData, rangeData, rangeKey } = this.state;
return (


mode="multiSelector"
onChange={this.onChange}
onColumnChange={this.onColumnChange}
range={rangeData}
value={rangeKey}
>

所在地址:
{formData.province && (

{formData.province}
{formData.city}
{formData.country}

)}
{!formData.province && (
请选择省/市/区
)}




);
}
}
export default ChangeCity;

样式自己处理一下子就好了

本文链接:https://blog.csdn.net/weixin_42381896/article/details/106854708


EaseIMKit如何设置昵称、头像

参考截图:
1、聊天页面




2、会话列表




"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(4)

五、Groovy数据结构 通过这个模块的学习,我会结合具体的例子来说明如何查阅文档来确定闭包中的参数,在讲 Map 的时候我会讲到 Groovy 常用的数据结构有如下 四种: 1)、数组 2)、List 3)、Map 4)、Range 1、数组 在 Gro...
继续阅读 »

五、Groovy数据结构


通过这个模块的学习,我会结合具体的例子来说明如何查阅文档来确定闭包中的参数,在讲 Map 的时候我会讲到


Groovy 常用的数据结构有如下 四种:



  • 1)、数组

  • 2)、List

  • 3)、Map

  • 4)、Range


1、数组


在 Groovy 中使用 [ ] 表示的是一个 List 集合,如果要定义 Array 数组,我们就必须强制指定为一个数组的类型

//在 Java 中,我们一般会这样去定义一个数组
String[] javaArray = ["Java", "Groovy", "Android"]

//在 Groovy 中,我们一般会使用 as 关键字定义数组
def groovyArray = ["Java", "Groovy", "Android"] as String[]

2、List


1)、列表集合定义


1、List 即列表集合,对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类


2、定义一个列表集合的方式有点像 Java 中定义数组一样


3、集合元素可以接收任意的数据类型

//在 Groovy 中定义的集合默认就是对应于 Java 中 ArrayList 集合
def list1 = [1,2,3]
//打印 list 类型
print list1.class
//打印结果
class java.util.ArrayList

//集合元素可以接收任意的数据类型
def list2 = ['erdai666', 1, true]

那么问题来了,如果我想定义一个 LinkedList 集合,要怎么做呢?有两种方式:


1、通过 Java 的强类型方式去定义


2、通过 as 关键字来指定

//方式1:通过 Java 的强类型方式去定义
LinkedList list3 = [4, 5, 6]

//方式2:通过 as 关键字来指定
def list4 = [1, 2, 3] as LinkedList

2)、列表集合增删改查

def list = [1,2,3]
//-------------------------- 增加元素 ---------------------------------
//有以下几种方式
list.add(20)
list.leftShift(20)
list << 20

//-------------------------- 删除元素 ---------------------------------
//根据下标移除元素
list.remove(0)

//-------------------------- 修改元素 ---------------------------------
//根据下标修改元素
list[0] = 100

//-------------------------- 查询元素 ---------------------------------
//调用闭包的 find 方法,方法中接收一个闭包,闭包的参数就是 list 中的元素
list.find {
println it
}

列表集合 Api 挺多的,对于一些其他Api,使用到的时候自行查阅文档就好了,我会在下面讲 Map 的时候演示查阅 Api 文档确定闭包的参数


3、Map


1)、定义


1、Map 表示键-值表,其底层对应 Java 中的 LinkedHashMap


2、Map 变量由[:]定义,冒号左边是 key,右边是 Value。key 必须是字符串,value 可以是任何对象


3、Map 的 key 可以用 '' 或 "" 或 ''' '''包起来,也可以不用引号包起来

def map = [a: 1, 'b': true, "c" : "Groovy", '''d''' : '''ddd''']

2)、Map 常用操作


这里列举一些 Map 的常用操作,一些其他的 Api 使用到的时候自行查阅文档就好了

//---------------------------- Map 中元素访问操作 ----------------
/**
* 有如下三种方式:
* 1、map.key
* 2、map[key]
* 3、map.get(ket)
*/
println map.a
println map['b']
println map.get('c')
//打印结果
1
true
Groovy

//---------------------------- Map 中添加和修改元素 -------------------
//如果当前 key 在 map 中不存在,则添加该元素,如果存在则修改该元素
map.put('key','value')
map['key'] = "value"

3)、Map 遍历,演示查阅官方文档


现在我要去遍历 map 中的元素,但是我不知道它的 Api 是啥,那这个时候就要去查官方 Api 文档了:


http://docs.groovy-lang.org/latest/html/groovy-jdk/java/util/Map.html



通过官方文档我们可以发现: each 和 eachWithIndex 的闭包参数还是不确定的,如果我们使用 each 方法,如果传递给闭包是一个参数,那么它就把 entry 作为参数,如果我们传递给闭包是两个参数,那么它就把 key 和 value 作为参数,eachWithIndex 比 each 多了个 index 下标而已.


那么我们现在就使用以下这两个 Api :

//下面为了打印输出的格式清晰,做了一些额外的操作
def map = [a: 1, 'b': true, "c" : "Groovy", '''d''' : '''ddd''']

map.each {
print "$it.key $it.value \t"
}
println()

map.each {key,value ->
print "$key $value \t"
}
println()

map.eachWithIndex {entry,index ->
print "$entry.key $entry.value $index \t"
}
println()

map.eachWithIndex { key,value,index ->
print "$key $value $index \t"
}
//打印结果
a 1 b true c Groovy d ddd
a 1 b true c Groovy d ddd
a 1 0 b true 1 c Groovy 2 d ddd 3
a 1 0 b true 1 c Groovy 2 d ddd 3

4、Range


Range 表示范围,它其实是 List 的一种拓展。其由 begin 值 + 两个点 + end 值表示。如果不想包含最后一个元素,则 begin 值 + 两个点 + < + end 表示。我们可以通过 aRange.from 与 aRange.to 来获对应的边界元素,实际操作感受一下:

//定义一个两端都是闭区间的范围
def range = 1..10
range.each {
print it + " "
}
//打印值
1 2 3 4 5 6 7 8 9 10

//如果不想包含最后一个元素
def range1 = 1..<10
range1.each {
print it + " "
}
//打印结果
1 2 3 4 5 6 7 8 9

//打印头尾边界元素
println "$range1.from $range1.to"
//打印结果
1 9

六、Groovy 文件处理


1、IO


下面我们开始来操作这个文件,为了闭包的可读性,我会在闭包上加上类型和参数:

//-------------------------------1、文件定位 --------------------------------
def file = new File('testFile.txt')

//-----------------------2、使用 eachLine Api 每次读取一行, 闭包参数是每一行的字符串------------
file.eachLine { String line ->
println line
}
//打印结果
erdai666
erdai777
erdai888

//------------------------3、获取输入流,输出流读文件和写文件---------------------------------
//获取输入流读取文件的每一行
//1
file.withInputStream { InputStream inputStream ->
inputStream.eachLine { String it ->
println it
}
}

//2
file.withReader { BufferedReader it ->
it.readLines().each { String it ->
println it
}
}

//打印结果
erdai666
erdai777
erdai888

//获取输出流将字符串写入文件 下面这两种方式写入的文件内容会把之前的内容给覆盖
//1
file.withOutputStream { OutputStream outputStream ->
outputStream.write("erdai999".getBytes())
}

//2
file.withWriter { BufferedWriter it ->
it.write('erdai999')
}

//------------------------4、通过输入输出流实现文件拷贝功能---------------------------------
//1、通过 withOutputStream withInputStream 实现文件拷贝
def targetFile = new File('testFile1.txt')
targetFile.withOutputStream { OutputStream outputStream ->
file.withInputStream { InputStream inputStream ->
outputStream << inputStream
}
}

//2、通过 withReader、withWriter 实现文件拷贝
targetFile.withWriter {BufferedWriter bufferedWriter ->
file.withReader {BufferedReader bufferedReader ->
bufferedReader.eachLine {String line ->
bufferedWriter.write(line + "\r\n")
}
}
}

2、XML 文件操作


1)、解析 XML 文件

//定义一个带格式的 xml 字符串
def xml = '''
<response>
<value>
<books id="1" classification="android">
<book available="14" id="2">
<title>第一行代码</title>
<author id="2">郭霖</author>
</book>
<book available="13" id="3">
<title>Android开发艺术探索</title>
<author id="3">任玉刚</author>
</book>
</books>
</value>
</response>
'''
//创建 XmlSlurper 类对象,解析 XML 文件主要借助 XmlSlurper 这个类
def xmlSlurper = new XmlSlurper()
//解析 mxl 返回 response 根结点对象
def response = xmlSlurper.parseText(xml)
//打印一些结果
println response.value.books[0].book[0].title.text()
println response.value.books[0].book[0].author.text()
//打印结果
第一行代码
郭霖

//1、使用迭代器解析
response.value.books.each{ books ->
books.book.each{ book ->
println book.title
println book.author
}
}
//打印结果
第一行代码
郭霖
Android开发艺术探索
任玉刚

//2、深度遍历 XML 数据
def str1 = response.depthFirst().findAll { book ->
return book.author == '郭霖'
}
println str1
//打印结果
[第一行代码郭霖]

//3、广度遍历 XML 数据
def str2 = response.value.books.children().findAll{ node ->
node.name() == 'book' && node.@id == '2'
}.collect { node ->
"$node.title $node.author"
}
println str2
//打印结果
[第一行代码 郭霖]

2)、生成 XML 文件


上面我们使用 XmlSlurper 这个类解析了 XML,现在我们借助 MarkupBuilder 来生成 XML ,代码如下:

/**
* <response>
* <value>
* <books id="1" classification="android">
* <book available="14" id="2">
* <title>第一行代码</title>
* <author id="2">郭霖</author>
* </book>
* <book available="13" id="3">
* <title>Android开发艺术探索</title>
* <author id="3">任玉刚</author>
* </book>
* </books>
* </value>
* </response>
*/
//方式1:通过下面这种方式 就可以实现上面的效果,但是这种方式有个弊端,数据都是写死的
def sw = new StringWriter()
def xmlBuilder = new MarkupBuilder(sw)
xmlBuilder.response{
value{
books(id: '1',classification: 'android'){
book(available: '14',id: '2'){
title('第一行代码')
author(id: '2' ,'郭霖')
}
book(available: '13',id: '3'){
title('Android开发艺术探索')
author(id: '3' ,'任玉刚')
}
}
}
}
println sw

//方式2:将 XML 数据对应创建相应的数据模型,就像我们解析 Json 创建相应的数据模型是一样的
//创建 XML 对应数据模型
class Response {

def value = new Value()

class Value {

def books = new Books(id: '1', classification: 'android')

class Books {
def id
def classification
def book = [new Book(available: '14', id: '2', title: '第一行代码', authorId: 2, author: '郭霖'),
new Book(available: '13', id: '3', title: 'Android开发艺术探索', authorId: 3, author: '任玉刚')]

class Book {
def available
def id
def title
def authorId
def author
}
}
}
}

//创建 response 对象
def response = new Response()
//构建 XML
xmlBuilder.response{
value{
books(id: response.value.books.id,classification: response.value.books.classification){
response.value.books.book.each{
def book1 = it
book(available: it.available,id: it.id){
title(book1.title)
author(authorId: book1.authorId,book1.author)
}
}
}
}
}
println sw

3、Json 解析


Json解析主要是通过 JsonSlurper 这个类实现的,这样我们在写插件的时候就不需要额外引入第三方的 Json 解析库了,其示例代码如下所示:

//发送请求获取服务器响应的数据
def response = getNetWorkData("https://www.wanandroid.com/banner/json")
println response.data[0].desc
println response.data[0].imagePath

def getNetWorkData(String url){
def connect = new URL(url).openConnection()
connect.setRequestMethod("GET")
//这个会阻塞线程 在Android中不能这样操作 但是在桌面程序是可以的
connect.connect()
def response = connect.content.text

//json转实体对象
def jsonSlurper = new JsonSlurper()
jsonSlurper.parseText(response)
}
//打印结果
扔物线
https://wanandroid.com/blogimgs/8a0131ac-05b7-4b6c-a8d0-f438678834ba.png

7、总结


在本篇文章中,我们主要介绍了以下几个部分:


1、一些关于 Gradle ,Groovy 的问题


2、搭建 Groovy 开发环境,创建一个 Groovy 工程


3、讲解了 Groovy 的一些基础语法


4、对闭包进行了深入的讲解


5、讲解了 Groovy 中的数据结构和常用 Api 使用,并以 Map 举例,查阅官方文档去确定 Api 的使用和闭包的参数


6、讲解了 Groovy 文件相关的处理


学习了 Groovy ,对于我们后续自定义 Gradle 插件迈出了关键的一步。其次如果你学习过 Kotlin ,你会发现,它们的语法非常的类似,因此对于后续学习 Kotlin 我们也可以快速去上手。


作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(3)

四、Groovy 闭包 在 Groovy 中,闭包非常的重要,因此单独用一个模块来讲 1、闭包定义 引用 Groovy 官方对闭包的定义:A closure in Groovy is an open, anonymous, block of code that...
继续阅读 »

四、Groovy 闭包


在 Groovy 中,闭包非常的重要,因此单独用一个模块来讲


1、闭包定义


引用 Groovy 官方对闭包的定义:A closure in Groovy is an open, anonymous, block of code that can take arguments, return a value and be assigned to a variable. 翻译过来就是:Groovy 中的闭包是一个开放的、匿名的代码块,它可以接受参数、返回值并将值赋给变量。 通俗的讲,闭包可以作为方法的参数和返回值,也可以作为一个变量而存在,闭包本质上就是一段代码块,下面我们就由浅入深的来学习闭包


2、闭包声明


1、闭包基本的语法结构:外面一对大括号,接着是申明参数,参数类型可省略,在是一个 -> 箭头号,最后就是闭包体里面的内容


2、闭包也可以不定义参数,如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it

//1
{ params ->
//do something
}

//2
{
//do something
}

3、闭包调用


1、闭包可以通过 .call 方法来调用


2、闭包可以直接用括号+参数来调用

//定义一个闭包赋值给 closure 变量
def closure = { params1,params2 ->
params1 + params2
}

//闭包调用方式1: 闭包可以通过 .call 方法来调用
def result1 = closure('erdai ','666')
//闭包调用方式2: 闭包可以直接用括号+参数来调用
def result2 = closure.call('erdai ','777')
//打印值
println result1
println result2
//打印结果
erdai 666
erdai 777

//定义一个无参闭包
def closure1 = {
println('无定义参数闭包')
}
closure1() //或者调用 closure1.call()
//打印结果
无定义参数闭包

4、闭包进阶


1)、闭包中的关键变量


每个闭包中都含有 this、owner 和 delegate 这三个内置对象,那么这三个三个内置对象有啥区别呢?我们用代码去验证一下


注意


1、getThisObject() 方法 和 thisObject 属性等同于 this


2、getOwner() 方法 等同于 owner


3、getDelegate() 方法 等同于 delegate


这些去看闭包的源码你就会有深刻的体会


1、我们在 GroovyGrammar.groovy 这个脚本类中定义一个闭包打印这三者的值看一下:

//定义一个闭包
def outerClosure = {
println "this: " + this
println "owner: " + owner
println "delegate: " + delegate
}
//调用闭包
outerClosure.call()
//打印结果
this: variable.GroovyGrammar@39dcf4b0
owner: variable.GroovyGrammar@39dcf4b0
delegate: variable.GroovyGrammar@39dcf4b0
//证明当前三者都指向了GroovyGrammar这个脚本类对象

2、我们在这个 GroovyGrammar.groovy 这个脚本类中定义一个类,类中定义一个闭包,打印看下结果:

//定义一个 OuterClass 类
class OuterClass {
//定义一个闭包
def outerClosure = {
println "this: " + this
println "owner: " + owner
println "delegate: " + delegate
}
}

def outerClass = new OuterClass()
outerClass.outerClosure.call()

//打印结果如下:
this: variable.OuterClass@1992eaf4
owner: variable.OuterClass@1992eaf4
delegate: variable.OuterClass@1992eaf4
//结果证明这三者都指向了当前 OuterClass 类对象

3、我们在 GroovyGrammar.groovy 这个脚本类中,定义一个闭包,闭包中在定义一个闭包,打印看下结果:

def outerClosure = {
def innerClosure = {
println "this: " + this
println "owner: " + owner
println "delegate: " + delegate
}
innerClosure.call()

}
println outerClosure
outerClosure.call()

//打印结果如下
variable.GroovyGrammar$_run_closure4@64beebb7
this: variable.GroovyGrammar@5b58ed3c
owner: variable.GroovyGrammar$_run_closure4@64beebb7
delegate: variable.GroovyGrammar$_run_closure4@64beebb7
//结果证明 this 指向了当前GroovyGrammar这个脚本类对象 owner 和 delegate 都指向了 outerClosure 闭包对象

我们梳理一下上面的三种情况:


1、闭包定义在GroovyGrammar.groovy 这个脚本类中 this owner delegate 就指向这个脚本类对象


2、我在这个脚本类中创建了一个 OuterClass 类,并在他里面定义了一个闭包,那么此时 this owner delegate 就指向了 OuterClass 这个类对象


3、我在 GroovyGrammar.groovy 这个脚本类中定义了一个闭包,闭包中又定义了一个闭包,this 指向了当前GroovyGrammar这个脚本类对象, owner 和 delegate 都指向了 outerClosure 闭包对象


因此我们可以得到结论:


1、this 永远指向定义该闭包最近的类对象,就近原则,定义闭包时,哪个类离的最近就指向哪个,我这里的离得近是指定义闭包的这个类,包含内部类


2、owner 永远指向定义该闭包的类对象或者闭包对象,顾名思义,闭包只能定义在类中或者闭包中


3、delegate 和 owner 是一样的,我们在闭包的源码中可以看到,owner 会把自己的值赋给 delegate,但同时 delegate 也可以赋其他值


注意:在我们使用 this , owner , 和 delegate 的时候, this 和 owner 默认是只读的,我们外部修改不了它,这点在源码中也有体现,但是可以对 delegate 进行操作


2)、闭包委托策略


下面我们就来对修改闭包的 delegate 进行实操:

//创建一个香蕉类
class Banana{
def name
}

//创建一个橘子类
class Orange{
def name
}

//定义一个香蕉对象
def banana = new Orange(name: '香蕉')
//定义一个橘子对象
def orange = new Orange(name: '橘子')
//定义一个闭包对象
def closure = {
//打印值
println delegate.name
}
//调用闭包
closure.call()

//运行一下,发现结果报错了,如下
Caught: groovy.lang.MissingPropertyException: No such property: name for class: variable.GroovyGrammar
//大致意思就是GroovyGrammar这个脚本类对象没有这个 name 对象

我们来分析下报错的原因原因,分析之前我们要明白一个知识点:


闭包的默认委托策略是 OWNER_FIRST,也就是闭包会先从 owner 上寻找属性或方法,找不到则在 delegate 上寻找


1、closure 这个闭包是生明在 GroovyGrammar 这个脚本类当中


2、根据我们之前学的知识,在不改变 delegate 的情况下 delegate 和 owner 是一样的,都会指向 GroovyGrammar 这个脚本类对象


3、GroovyGrammar 这个脚本类对象,根据闭包默认委托策略,找不到 name 这个属性


因此报错了,知道了报错原因,那我们就修改一下闭包的 delegate , 还是上面那段代码,添加如下这句代码:

//修改闭包的delegate
closure.delegate = orange
//我们在运行一下,打印结果:
橘子

此时闭包的 delegate 指向了 orange ,因此会打印 orange 这个对象的 name ,那么我们把 closure 的 delegate 改为 banana,肯定就会打印香蕉了

//修改闭包的delegate
closure.delegate = banana
//我们在运行一下,打印结果:
香蕉

3)、深入闭包委托策略

//定义一个 ClosureDepth 类
class ClosureDepth{
//定义一个变量 str1 赋值为 erdai666
def str1 = 'erdai666'
//定义一个闭包
def outerClosure = {
//定义一个变量 str2 赋值为 erdai777
def str2 = 'erdai777'
//打印str1 分析1
println str1

//闭包中在定义一个闭包
def innerClosure = {
//分析2
println str1
println str2
}
//调用内部这个闭包
innerClosure.call()
}
}

//创建 ClosureDepth 对象
def closureDepth = new ClosureDepth()
//调用外部闭包
closureDepth.outerClosure.call()
//运行程序,打印结果如下
erdai666
erdai666
erdai777

上面代码注释写的很清楚,现在我们来重点分析下分析1和分析2处的打印值:


分析1:


分析1处打印了 str1 , 它处于 outerClosure 这个闭包中,此时 outerClosure 这个闭包的 owner , delegate 都指向了 ClosureDepth 这个类对象,因此 ClosureDepth 这个类对象的属性和方法我们就都能调用到,因此分析1处会打印 erdai666


分析2:


分析2处打印了 str1和 str2,它处于 innerClosure 这个闭包中,此时 innerClosure 这个闭包的 owner 和 delegate 会指向 outerClosure 这个闭包对象,我们会发现 outerClosure 有 str2 这个属性,但是并没有 str1 这个属性,因此 outerClosure 这个闭包会向它的 owner 去寻找,因此会找到 ClosureDepth 这个类对象的 str1 属性,因此打印的 str1 是ClosureDepth 这个类对象中的属性,打印的 str2 是outerClosure 这个闭包中的属性,所以分析2处的打印结果分别是 erdai666 erdai777


上面的例子中没有显式的给 delegate 设置一个接收者,但是无论哪层闭包都能成功访问到 str1、str2 值,这是因为默认的解析委托策略在发挥作用,Groovy 闭包的委托策略有如下几种:




  1. OWNER_FIRST:默认策略,首先从 owner 上寻找属性或方法,找不到则在 delegate 上寻找


  2. DELEGATE_FIRST:和上面相反,首先从 delegate 上寻找属性或者方法,找不到则在 owner 上寻找


  3. OWNER_ONLY:只在 owner 上寻找,delegate 被忽略


  4. DELEGATE_ONLY:和上面相反,只在 delegate 上寻找,owner 被忽略


  5. TO_SELF:高级选项,让开发者自定义策略,必须要自定义实现一个 Closure 类,一般我们这种玩家用不到


下面我们就来修改一下闭包的委托策略,加深理解:

class People1{
def name = '我是People1'

def action(){
println '吃饭'
}

def closure = {
println name
action()
}
}

class People2{
def name = '我是People2'

def action(){
println '睡觉'
}
}

def people1 = new People1()
def people2 = new People2()
people1.closure.delegate = people2
people1.closure.call()
//运行下程序,打印结果如下:
我是People1
吃饭

what? 这是啥情况,我不是修改了 delegate 为 people2 了,怎么打印结果还是 people1 的?那是因为我们忽略了一个点,没有修改闭包委托策略,他默认是 OWNER_FIRST ,因此我们修改一下就好了,还是上面这段代码,添加一句代码如下:

people1.closure.resolveStrategy = Closure.DELEGATE_FIRST
//运行下程序,打印结果如下:
我是People2
睡觉

到这里,相信你对闭包了解的差不多了,下面我们在看下闭包的源码就完美了


4)、闭包 Closure 类源码


仅贴出关键源码

public abstract class Closure<V> extends GroovyObjectSupport implements Cloneable, Runnable, GroovyCallable<V>, Serializable {
/**
* 熟悉的一堆闭包委托代理策略
*/
public static final int OWNER_FIRST = 0;
public static final int DELEGATE_FIRST = 1;
public static final int OWNER_ONLY = 2;
public static final int DELEGATE_ONLY = 3;
public static final int TO_SELF = 4;
/**
* 闭包对应的三个委托对象 thisObject 对应的就是 this 属性,都是用 private 修饰的,外界访问不到
*/
private Object delegate;
private Object owner;
private Object thisObject;
/**
* 闭包委托策略
*/
private int resolveStrategy;

/**
* 在闭包的构造方法中:
* 1、将 resolveStrategy 赋值为0,也是就默认委托策略OWNER_FIRST
* 2、thisObject ,owner ,delegate都会被赋值,delegate 赋的是 owner的值
*/
public Closure(Object owner, Object thisObject) {
this.resolveStrategy = 0;
this.owner = owner;
this.delegate = owner;
this.thisObject = thisObject;
CachedClosureClass cachedClass = (CachedClosureClass)ReflectionCache.getCachedClass(this.getClass());
this.parameterTypes = cachedClass.getParameterTypes();
this.maximumNumberOfParameters = cachedClass.getMaximumNumberOfParameters();
}

/**
* thisObject 只提供了 get 方法,且 thisObject 是用 private 修饰的,因此 thisObject 即 this 只读
*/
public Object getThisObject() {
return this.thisObject;
}

/**
* owner 只提供了 get 方法,且 owner 是用 private 修饰的,因此 owner 只读
*/
public Object getOwner() {
return this.owner;
}

/**
* delegate 提供了 get 和 set 方法,因此 delegate 可读写
*/
public Object getDelegate() {
return this.delegate;
}

public void setDelegate(Object delegate) {
this.delegate = delegate;
}

/**
* 熟悉的委托策略设置
*/
public void setResolveStrategy(int resolveStrategy) {
this.resolveStrategy = resolveStrategy;
}
public int getResolveStrategy() {
return resolveStrategy;
}
}

到这里闭包相关的知识点就都讲完了,但是,但是,但是,重要的事情说三遍:我们使用闭包的时候,如何去确定闭包的参数呢?,这个真的很蛋疼,作为 Android 开发者,在使用 AndroidStudio 进行 Gradle 脚本编写的时候,真的是非常不友好,上面我讲了可以使用一个小技巧去解决这个问题,但是这种情况是在你知道要使用一个 Api 的情况下,比如你知道 Map 的 each 方法可以遍历,但是你不知道参数,这个时候就可以去使用。那如果你连 Api 都不知道使用,那就更加不知道闭包的参数了,因此要解决这种情况,我们就必须去查阅 Groovy 官方文档:


http://www.groovy-lang.org/api.html


http://docs.groovy-lang.org/latest/html/groovy-jdk/index-all.html



作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(2)

三、Groovy 基础语法 再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的,既然如此,Groovy 的优势在哪里呢? 在于 Groovy 提供了更加灵活简单的语法,大量的语法糖以及闭包特性可以让你用更少的代码来实现和...
继续阅读 »

三、Groovy 基础语法


再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的,既然如此,Groovy 的优势在哪里呢?


在于 Groovy 提供了更加灵活简单的语法,大量的语法糖以及闭包特性可以让你用更少的代码来实现和Java同样的功能。比如解析xml文件,Groovy 就非常方便,只需要几行代码就能搞定,而如果用 Java 则需要几十行代码。


1、支持动态类型,使用 def 关键字来定义一个变量


在 Groovy 中可以使用 def 关键字定义一个变量,当然 Java 里面定义数据类型的方式,在 Groovy 中都能用

//Java 中,我们一般会这么定义
int age = 16
String name = "erdai"

//Groovy 中,我们可以这样定义,在变量赋值后,Groovy 编译器会推断出变量的实际类型
def age = 16
def name = 'erdai'

2、不用写 ; 号


现在比较新的语言都不用写,如 Kotlin

def age = 16
def name = 'erdai'

3、没有基本数据类型了,全是引用类型


上面说到,定义一个变量使用 def 关键字,但是 Groovy 是基于 Java 扩展的,因此我们也可以使用 Java 里面的类型,如 Java 中8大基本类型:byte , short , int , long , float , double ,char,boolean

//定义8大基本类型
byte mByte = 1
short mShort = 2
int mInt = 3
long mLong = 4
float mFloat = 5
double mDouble = 6
char mChar = 'a'
boolean mBoolean = true
//对类型进行打印
println(mByte.class)
println(mShort.class)
println(mInt.class)
println(mLong.class)
println(mFloat.class)
println(mDouble.class)
println(mChar.class)
println(mBoolean.class)

//打印结果如下:
class java.lang.Byte
class java.lang.Short
class java.lang.Integer
class java.lang.Long
class java.lang.Float
class java.lang.Double
class java.lang.Character
class java.lang.Boolean

因此我们可以得出结论:Groovy中没有基本数据类型,全是引用类型,即使定义了基础类型,也会被转换成对应的包装类


4、方法变化


1、使用 def 关键字定义一个方法,方法不需要指定返回值类型,参数类型,方法体内的最后一行会自动作为返回值,而不需要return关键字


2、方法调用可以不写 () ,最好还是加上 () 的好,不然可读性不好


3、定义方法时,如果参数没有返回值类型,我们可以省略 def,使用 void 即可


4、实际上不管有没有返回值,Groovy 中返回的都是 Object 类型


5、类的构造方法,避免添加 def 关键字

def sum(a,b){
a + b
}
def sum = sum(1,2) //还可以写成这样,但是可读性不好 def sum = sum 1,2
println(sum)

//打印结果
3

//如果方法没有返回值,我们可以这样写:
void doSomething(param1, param2) {

}

//类的构造方法,避免添加 def 关键字
class MyClass {
MyClass() {

}
}

5、字符串变化


在 Groovy 中有三种常用的字符串定义方式,如下所示:


这里先解释一下可扩展字符串的含义,可扩展字符串就是字符串里面可以引用变量,表达式等等


1 、单引号 '' 定义的字符串为不可扩展字符串


2 、双引号 "" 定义的字符串为可扩展字符串,可扩展字符串里面可以使用 ${} 引用变量值,当 {} 里面只有一个变量,非表达式时,{}也可以去掉


3 、三引号 ''' ''' 定义的字符串为输出带格式的不可扩展字符串

def age = 16
def name = 'erdai'
//定义一个不可扩展字符串,和我门在Java中使用差不多
def str1 = 'hello ' + name
//定义可扩展字符串,字符串里面可以引用变量值,当 {} 里面只有一个变量时,{}也可以去掉
def str2 = "hello $name ${name + age}"
//定义带输出格式的不可扩展字符串 使用 \ 字符来分行
def str3 = '''
\
hello
name
'''
//打印类型和值 下面代码我省略了 println 方法的(),上面有讲到这种语法也是允许的
println 'str1类型: ' + str1.class
println 'str1输出值: ' + str1
println 'str2类型: ' + str2.class
println 'str2输出值: ' + str2
println 'str3类型: ' + str3.class
println 'str3输出值: ' + str3

//打印结果
str1类型: class java.lang.String
str1输出值: hello erdai
str2类型: class org.codehaus.groovy.runtime.GStringImpl
str2输出值: hello erdai erdai16
str3类型: class java.lang.String
str3输出值:
hello
name

从上面代码我们可以看到,str2 是 GStringImpl 类型的,而 str1 和 str3 是 String 类型的,那么这里我就会有个疑问,这两种类型在相互赋值的情况下是否需要强转呢?我们做个实验在测试下:

//定义一个 String 类型的变量接收 GStringImpl 类型的变量,并没有强转
String str4 = str2
println 'str4类型: ' + str4.class
println 'str4输出值: ' + str4

//打印类型和值
str4类型: class java.lang.String
str4输出值: hello erdai erdai16

因此我们可以得出结论:编码的过程中,不需要特别关注 String 和 GString 的区别,编译器会帮助我们自动转换类型


6. 不用写 get 和 set 方法


1、在我们创建属性的时候,Groovy会帮我们自动创建 get 和 set 方法


2、当我们只定义了一个属性的 get 方法,而没有定义这个属性,默认这个属性只读


3、我们在使⽤对象 object.field 来获取值或者使用 object.field = value 来赋值的时候,实际上会自动转而调⽤ object.getField() 和 object.setField(value) 方法,如果我们不想调用这个特殊的 get 方法时则可以使用 .@ 直接域访问操作符访问属性本身


我们来模拟1,2,3这三种情况

//情况1:在我们创建属性的时候,Groovy会帮我们自动创建 get 和 set 方法
class People{
def name
def age
}

def people = new People()
people.name = 'erdai'
people.age = 19
println "姓名: $people.name 年龄: $people.age"
//打印结果
姓名: erdai 年龄: 19

//情况2 当我们定义了一个属性的 get 方法,而没有定义这个属性,默认这个属性只读
//我们修改一下People类
class People{
def name
def getAge(){
12
}
}

def people = new People()
people.name = 'erdai'
people.age = 19
println "姓名: $people.name 年龄: $people.age"
//运行一下代码 打印结果报错了,如下:
Caught: groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: age for class: variable.People
//大概错误意思就是我们不能修改一个只读的属性

//情况3: 如果我们不想调用这个特殊的 get 方法时则可以使用 .@ 直接域访问操作符访问属性本身
class People{
def name
def age

def getName(){
"My name is $name"
}
}
//这里使用了命名的参数初始化和默认的构造器创建people对象,后面会讲到
def people = new People(name: 'erdai666')
people.age = 19
def myName = people.@name

//打印值
println myName
println "姓名: $people.name 年龄: $people.age"

//打印结果
erdai666
姓名: My name is erdai666 年龄: 19
//看到区别了吗?使用 people.name 则会去调用这个属性的get方法,而 people.@name 则会访问这个属性本身

7、Class 是一等公民,所有的 Class 类型可以省略 .Class

//定义一个Test类
class Test{

}

//定义一个测试class的方法,从前面的语法我们知道,方法的参数类型是可以省略的
def testClass(myClass){

}

//测试
testClass(Test.class)
testClass(Test)

8、== 和 equals


在 Groovy 中,== 就相当于 Java 的 equals,如果需要比较两个对象是否是同一个,需要使用 .is()

class People{
def name
def age
}

def people1 = new People(name: 'erdai666')
def people2 = new People(name: 'erdai666')

println("people1.name == people2.name is: " + (people1.name == people2.name))
println("people1 is people2 is: " + people1.is(people2))

//打印结果
people1.name == people2.name is: true
people1 is people2 is: false

9、使用 assert 来设置断言,当断言的条件为 false 时,程序将会抛出异常

assert  2 ** 4 == 15
//运行程序,报错了,结果如下:
Caught: Assertion failed:
assert 2 ** 4 == 15
| |
16 false

10、支持 ** 次方运算符

assert  2 ** 4 == 16

11、简洁的三元表达式

//在java中,我们会这么写
String str = obj != null ? obj : ""

//在Groovy中,我们可以这样写,?: 操作符表示如果左边结果不为空则取左边的值,否则取右边的值
String str = obj ?: ""

12、简洁的非空判断

//在java中,我们可能会这么写
if(obj != null){
if(obj.group != null){
if(obj.group.artifact != null){
//do something
}
}
}

//在Groovy中,我们可以这样写 ?. 操作符表示如果当前调用对象为空就不执行了
obj?.group?.artifact


13、强大的 Switch


在 Groovy 中,switch 方法变得更加灵活,强大,可以同时支持更多的参数类型,比在 Java 中增强了很多

def result = 'erdai666'
switch (result){
case [1,2,'erdai666']:
println "匹配到了result"
break
default:
println 'default'
break
}
//打印结果
匹配到了result

14、判断是否为 null 和 非运算符


在 Groovy 中,所有类型都能转成布尔值,比如 null 就相当于0或者相当于false,其他则相当于true

//在 Java 中,我们会这么用
if (name != null && name.length > 0) {

}

//在 Groovy 中,可以这么用,如果name为 null 或 0 则返回 false,否则返回true
if(name){

}

//非运算符 erdai 这个字符串为非 null ,因此为true,而 !erdai 则为false
assert (!'erdai') = false

15、可以使用 Number 类去替代 float、double 等类型,省去考虑精度的麻烦


16、默认是 public 权限


默认情况下,Groovy 的 class 和 方法都是 public 权限,所以我们可以省略 public 关键字,除非我们想使用 private 修饰符

class Server { 
String toString() { "a server" }
}

17、使用命名的参数初始化和默认的构造器


Groovy中,我们在创建一个对象实例的时候,可以直接在构造方法中通过 key value 的形式给属性赋值,而不需要去写构造方法,说的有点抽象,上代码感受一下:

//定义一个people
class People{
def name
def age
}

//我们可以通过以下几种方式去实例化一个对象,注意我们People类里面没有写任何一个构造方法哦
def people1 = new People()
def people1 = new People(age: 15)
def people2 = new People(name: 'erdai')
def people3 = new People(age: 15,name: 'erdai')

18、使用 with 函数操作同一个对象的多个属性和方法


with 函数接收一个闭包,闭包下面会讲,闭包的参数就是当前调用的对象

class People{
def name
def age

void running(){
println '跑步'
}
}
//定义一个 people 对象
def people = new People()
//调用 with 函数 闭包参数即为peopeo 如果闭包不写参数,默认会有一个 it 参数
people.with{
name = "erdai"
age = 19
println "$name $age"
running()
}
//打印结果
erdai 19
跑步

19、异常捕获


如果你实在不想关心 try 块里抛出何种异常,你可以简单的捕获所有异常,并且可以省略异常类型:

//在 java 中我们会这样写
try {
// ...
} catch (Exception e) {
// do something
}

//在 Groovy 中,我们可以这样写
try {
// ...
} catch (any) {
// do something
}


上面 Groovy 的写法其实就是省略了参数类型,实际上 any 的参数类型也是 Exception, 并不包括 Throwable ,如果你想捕获所有的异常,你可以明确捕获异常的参数类型



作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(1)

前言 引用 Gradle 官方一段对Gradle的介绍:Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build sc...
继续阅读 »

前言


引用 Gradle 官方一段对Gradle的介绍:Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL.翻译过来就是:Gradle 是一个开源的自动化构建工具,专注于灵活性和性能。Gradle 构建脚本是使用 Groovy 或 Kotlin DSL 编写的。 之前官网的介绍是说 Gradle 是基于 Groovy 的 DSL,为啥现在又多了个 Kotlin 呢?因为 Gradle 从5.0开始,开始支持了 Kotlin DSL,现在已经发展到了6.8.3,因此我们可以使用 Groovy 或者 Kotlin 来编写 Gradle脚本。Kotlin 现作为 Android 第一开发语言,重要性不言而喻,作为一个 Android开发者,Kotlin 是必学的,后续我也会出个 Kotlin 系列文章。今天我们的重点是介绍一些 Gradle 的相关概念,以及对 Groovy 语言的学习


一、问题


我学习知识喜欢以问题为导向,这样可以让我明确学习的目的,提高学习效率,下面也是我在学习 Gradle 的过程中,由浅入深所产生的一些疑问,我们都知道,Android 应用是用 Gradle 构建的,在刚开发 Android 的时候我会想:


1、什么是自动化构建工具?


2、Gradle 是什么?


3、什么是 DSL?


4、什么是 Groovy?


5、Gradle 和 Groovy 有什么区别?


6、静态编程语言和动态编程语言有什么区别?


带着这些疑问,我们继续学习


1、自动化构建工具


在 Android 上的体现,简单的说就是自动化的编译、打包程序


在上大学学习Java那会,老师为了让我们深刻的体验撸码的魅力,都是通过文本直接敲代码的,敲完之后把扩展名改成.java后缀,然后通过javac命令编译,编译通过后,在执行java命令去运行,那么这种文件一多,我们每次都得手动去操作,效率会大大的降低,这个时候就出现了自动化编译工具,我们只需要在编译工具中,点击编译按钮,编译完成后,无需其他手动操作,程序就可以直接运行了,自动化编译工具就是最早的自动化构建工具。那么随着业务功能的不断扩展,我们的产品需要加入多媒体资源,需要打不同的渠道包发布到不同的渠道,那就必须依靠自动化构建工具,要能支持平台、需求等方面的差异、能添加自定义任务、专门的用来打包生成最终产品的一个程序、工具,这个就是自动化构建工具。自动化构建工具本质上还是一段代码程序。这就是自动化构建工具的一个发展历程,自动化构建工具在这个过程中不断的发展和优化


2、Gradle 是什么?


理解了自动化构建工具,那么理解 Gradle 就比较简单了,还是引用官方的那一段话:


Gradle 是一个开源的自动化构建工具,专注于灵活性和性能。Gradle 构建脚本是使用 Groovy 或 Kotlin DSL 编写的。


Gradle 是 Android 的默认构建工具,Android 项目这么多东西,既有我们自己写的 java、kotlin、C++、Dart 代码,也有系统自己的 java、C,C++ 代码,还有引入的第三方代码,还有多媒体资源,这么多代码、资源打包成 APK 文件肯定要有一个规范,干这个活的就是我们熟悉的 gradle 了,总而言之,Gradle就是一个帮我们打包 APK 的工具


3、什么是DSL?


DSL英文全称:domain specific language,中文翻译即领域特定语言,例如:HTML,XML等 DSL 语言


特点



  • 解决特定领域的专有问题

  • 它与系统编程语言走的是两个极端,系统编程语言是希望解决所有的问题,比如 Java 语言希望能做 Android 开发,又希望能做后台开发,它具有横向扩展的特性。而 DSL 具有纵向深入解决特定领域专有问题的特性。


总的来说,DSL 的核心思想就是:“求专不求全,解决特定领域的问题”。


4、什么是 Groovy?


Groovy 是基于 JVM 的脚本语言,它是基于Java扩展的动态语言


基于 JVM 的语言有很多种,如:Groovy,Kotlin,Java,Scala等等,他们都拥有一个共同的特性:最终都会编译生成 Java 字节码文件并在 JVM 上运行。


因为 Groovy 就是对 Java 的扩展,所以,我们可以用学习 Java 的方式去学习 Groovy 。 学习成本相对来说还是比较低的,即使开发过程中忘记 Groovy 语法,也可以用 Java 语法继续编码


5、Gradle 和 Groovy 有什么区别?


Gradle是基于 Groovy 的一种自动化构建工具,是运行在JVM上的一个程序,Groovy是基于JVM的一种语言,Gradle 和 Groovy 的关系就像 Android 和 Java 的关系一样


6、静态编程语言和动态编程语言有什么区别?


静态编程语言是在编译期就要确定变量的数据类型,而动态编程语言则是在运行期确定变量的数据类型。就像静态代理和动态代理一样,一个强调的是编译期,一个强调的是运行期,常见的静态编程语言有Java,Kotlin等等,动态编程语言有Groovy,Python等语言。

二、Groovy 开发环境搭建与工程创建

1、到官网下载JDK安装,并配置好 JDK 环境



2、到官网下载好 Groovy SDK,并解压到合适的位置



3、配置 Groovy 环境变量



4、到官网下载 IntelliJ IDEA 开发工具并安装



5、创建 Groovy 工程即可


小技巧: 作为 Android 开发者,我们一般都是使用 AndroidStudio 进行开发的,但是 AndroidStudio 对于 Groovy 支持不是很友好,各种没有提示,涉及到闭包,你也不知道闭包的参数是啥?因此这个时候,你就可以使用 IntelliJ IDEA 先弄好,在复制过去,IntelliJ IDEA 对Groovy 的支持还是很友好的


作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Fastlane 自动打包技术

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布AppGithub官网文档我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们...
继续阅读 »

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App

Github

官网

文档

我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:

1.一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。

2.避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。

3.通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect

4.证书的同步更新,管理,在新电脑能够迅速具备项目打包环境。

如果你也有上述需求,那我相信Fastlane是一个好的选择。

多说无益,开始上手

一、安装xcode命令行工具
xcode-select --install,如果没有安装,会弹出对话框,点击安装。

如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates表示已经安装

二、安装Fastlane
sudo gem install fastlane -NV或是brew cask install fastlane我这里使用gem安装的

安装完了执行fastlane --version,确认下是否安装完成和当前使用的版本号。

三、初始化Fastlane
cd到你的项目目录执行

fastlane init

这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。

如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。


接着会提示你输入开发者账号和密码。

[20:48:55]: Please enter your Apple ID developer credentials
[20:48:55]: Apple ID Username:

登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。

如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。

四、文件系统

初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。

其中metadata和screenshots分别对应App元数据和商店应用截图。

Appfile主要存放App的apple_id team_id app_identifier等信息

Deliverfile中为发布的配置信息,一般情况用不到。

Fastfile是我们最应该关注的文件,也是我们的工作文件。

Fastfile


之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。

打包到蒲公英
这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。

蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。

打开终端输入fastlane add_plugin pgyer

更多信息查看蒲公英文档

新建一个lane

desc "打包到pgy"
lane :test do |options|
gym(
clean:true, #打包前clean项目
export_method: "ad-hoc", #导出方式
scheme:"shangshaban", #scheme
configuration: "Debug",#环境
output_directory:"./app",#ipa的存放目录
output_name:get_build_number()#输出ipa的文件名为当前的build号
)
#蒲公英的配置 替换为自己的api_key和user_key
pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
end

这样一个打包到蒲公英的lane就完成了。

option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台

执行

在工作目录的终端执行

fastlane test desc:测试打包


然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置。

其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解

其他的一些小提示

1.可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来。

2.如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。

3.如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。

4.如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据 skip_screenshots:true,#不上传屏幕截图

关于fastlane的一些想法
其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!

摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/59725c52e0fa

收起阅读 »

iOS 常见面试题总结及答案(4)

一.OC对象的内存管理机制?在iOS中,使用引用计数来管理OC对象的内存一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1内存管...
继续阅读 »

一.OC对象的内存管理机制?

在iOS中,使用引用计数来管理OC对象的内存

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结

当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况

extern void _objc_autoreleasePoolPrint(void);

二.内存区域分布

在iOS开发过程中,为了合理的分配有限的内存空间,将内存区域分为五个区,由低地址向高地址分类分别是:代码区、常量区、全局静态区、堆、栈。

代码段 -- 程序编译产生的二进制的数据
常量区 -- 存储常量数据,通常程序结束后由系统自动释放
全局静态区 -- 全局区又可分为未初始化全局区:.bss段和初始化全局区:data段。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,在程序结束后有系统释放。
堆(heap) -- 程序运行过程中,动态分配的内存
栈(stack) -- 存放局部变量,临时变量

三.堆区和栈取的区别

按管理方式分

对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
按分配方式分

堆是动态分配和回收内存的,没有静态分配的堆
栈有两种分配方式:静态分配和动态分配
静态分配是系统编译器完成的,比如局部变量的分配
动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动 态分配也由系统编译器进行释放,不需要程序员手动管理

四.怎么保证多人开发进行内存泄露的检查

1.使用Analyze进行代码的静态分析
2.使用leaks 进行内存泄漏检测
3.使用一些三方工具(DoraemonKit/WithMLeaksFinder)

五.内存泄漏可能会出现的几种原因?

第一种可能:第三方框架不当使用;
第二种可能:block循环引用;
第三种可能:delegate循环引用;
第四种可能:NSTimer循环引用
第五种可能:非OC对象内存处理
第六种可能:地图类处理
第七种可能:大次数循环内存暴涨

六.什么是Tagged Pointer?

1.从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
2.使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
3.当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

七.copy和mutableCopy区别


八.AutoreleasePoolPage的结构?以及如何 push 和 pop 的

AutoreleasePool(自动释放池)其实并没有自身的结构,他是基于多个AutoreleasePoolPage(一个C++类)以双向链表组合起来的结构; 可以通过 push操作添加对象,pod 操作弹出对象,以及通过 release 操作释放对象;


调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

id *next指向了下一个能存放autorelease对象地址的区域

九.Autoreleasepool 与 Runloop 的关系

主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理
iOS在主线程的Runloop中注册了2个Observer

第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

十.什么是多线程?

多线程是指实现多个线程并发执行的技术,进而提升整体处理性能。

同一时间,CPU 只能处理一条线程,多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换)如果 CPU 调度线程的时间足够快, 就造成了多线程并发执行的假象

主线程的栈区 空间大小为1M,非常非常宝贵

子线程的栈区 空间大小为512K内存空间

优势
充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“并行计算”状态

弊端
新线程会消耗内存控件和cpu时间,线程太多会降低系统运行性能。

十一.iOS的多线程方案有哪几种?


十二,讲一下GCD

GCD(Grand Central Dispatch), 又叫做大中央调度, 它对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口, 使用更加高效,也是苹果推荐的使用方式.

GCD 的队列

1.并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效

2.串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务),按照FIFO顺序执行.

同步和异步任务

GCD多线程经常会使用 dispatch_sync和dispatch_async函数向指定队列添加任务,分别是同步和异步

同步指阻塞当前线程,既要等待添加的耗时任务块Block完成后,函数才能返回,后面的代码才能继续执行

异步指将任务添加到队列后,函数立即返回,后面的代码不用等待添加的任务完成后即可执行,异步提交无法确定任务执行顺序

相关常用函数使用:

1.dispatch_after使用 (通过该函数可以让提交的任务在指定时间后开始执行,也就是延迟执行;)

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"10秒后开始执行")
});

2.dispatch_group_t (组调度)的使用 (组调度可以实现等待一组操都作完成后执行后续任务.)

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求3
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});

3.dispatch_semaphore (信号量)如何使用?

用于控制最大并发数     可以防止资源抢夺

与他相关的共有三个函数,分别是:

dispatch_semaphore_create,  // 创建最大并发数
dispatch_semaphore_wait。 // -1 开始执行 (0则等待)
dispatch_semaphore_signal, // +1

4.dispatch_barrier_(a)sync使用?

一个dispatch barrier 允许在一个并发队列中创建一个同步点。当在并发队列中遇到一个barrier, 他会延迟执行barrier的block,等待所有在barrier之前提交的blocks执行结束。 这时,barrier block自己开始执行。 之后, 队列继续正常的执行操作。

十三.什么是NSOperation?

1.NSOperation是基于GCD的上封装,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强

例如:
可以加入操作依赖控制执行顺序,设置操作队列最大并发数,取消操作等

2.NSOperation如何实现操作依赖

通过任务间添加依赖,可以为任务设置执行的先后顺序。接下来通过一个案例来展示设置依赖的效果。

NSOperationQueue *queue=[[NSOperationQueue alloc] init];
//创建操作
NSBlockOperation *operation1=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第1次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation2=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第2次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation3=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第3次操作,线程:%@",[NSThread currentThread]);
}];
//添加依赖
[operation1 addDependency:operation2];
[operation2 addDependency:operation3];
//将操作添加到队列中去
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];

十四.在项目什么时候选择使用 GCD,什么时候选 择 NSOperation

项目中使用 NSOperation 的优点是 NSOperation 是对线程的高度抽象,在项目中使 用它,会使项目的程序结构更好,子类化 NSOperation 的设计思路,是具有面向对 象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中 使用。

项目中使用 GCD 的优点是 GCD 本身非常简单、易用,对于不复杂的多线程操 作,会节省代码量,而 Block 参数的使用,会是代码更为易读,建议在简单项目中 使用。

区别,以及各自的优势

GCD是纯C语⾔言的API,NSOperationQueue是基于GCD的OC版本封装

GCD只⽀支持FIFO的队列列,NSOperationQueue可以很⽅方便便地调整执⾏行行顺 序、设 置最⼤大并发数量量

NSOperationQueue可以在轻松在Operation间设置依赖关系,⽽而GCD 需要写很 多的代码才能实现

NSOperationQueue⽀支持KVO,可以监测operation是否正在执⾏行行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld)

GCD的执⾏行行速度⽐比NSOperationQueue快 任务之间不不太互相依赖:GCD 任务之间 有依赖\或者要监听任务的执⾏行行情况:NSOperationQueue

十五.线程安全的处理手段有哪些,线程锁都有哪些?

1.加锁

2.同步执行

线程锁 (我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁 应运而生。)

1.OSSpinLock (自旋锁)

注:苹果爸爸已经在iOS10.0以后废弃了这种锁机制,使用os_unfair_lock 替换,顾名思义能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题.

2.os_unfair_lock(自旋锁)

3.dispatch_semaphore (信号量)

4.pthread_mutex(互斥锁)

5.NSLock(互斥锁、对象锁)

6.NSCondition(条件锁、对象锁)

7.NSConditionLock(条件锁、对象锁)

8.NSRecursiveLock(递归锁、对象锁)

9.@synchronized(条件锁)

10.pthread_mutex(recursive)(递归锁) 

注.递归锁可以被同一线程多次请求,而不会引起死锁。即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。这主要是用在循环或递归操作中

性能图


十六.HTTPS连接过程简述

1.客户端向服务端发起 https 请求

2.服务器(需要申请 ca 证书),返回证书(包含公钥)给客户端

3.客户端使用根证书验证 服务器证书的有效性,进行身份确认

4.客户端生成对称密钥,通过公钥进行密码,发送给服务器

5.服务器使用私钥进行 解密,获取对称密钥

6.双方使用对称加密的数据进行通信

十七.http 与https区别

HTTPS和HTTP的区别主要为以下四点:

1.https协议需要到ca申请证书,一般免费证书很少,需要交费。

2.http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。

3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4.http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全

十八.什么是DNS?DNS劫持问题?

域名系统(DomainNameSystem,缩写:DNS)是[互联网]的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问[互联网]

DNS劫持又称(域名劫持), 是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。

解决办法: 使用HTTPDNS

十九.网络七层是什么?

OSI模型有7层结构,每层都可以有几个子层。 OSI的7层从上到下分别是 7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层 ;其中高层(即7、6、5、4层)定义了应用程序的功能,下面3层(即3、2、1层)主要面向通过网络的端到端的数据流。

1.应用层
网络服务与最终用户的一个接口。
协议有:HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP

2.表示层
数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层)
格式有,JPEG、ASCll、DECOIC、加密格式等

3 .会话层
建立、管理、终止会话。(在五层模型里面已经合并到了应用层)
对应主机进程,指本地主机与远程主机正在进行的会话

4.传输层
定义传输数据的协议端口号,以及流控和差错校验。
协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层

5.网络层
进行逻辑地址寻址,实现不同网络之间的路径选择。
协议有:ICMP IGMP IP(IPV4 IPV6) ARP RARP

6.数据链路层
建立逻辑连接、进行硬件地址寻址、差错校验 [2] 等功能。(由底层网络定义协议)
将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。

7.物理层
建立、维护、断开物理连接。(由底层网络定义协议)

二十.项目中网络层如何做安全处理

1.尽量使用https

2.不要传输明文密码

3.Post并不比Get安全

4.不要使用301跳转

5.http请求都带上MAC

6.http请求使用临时密钥

7.AES使用CBC模式







收起阅读 »

ios加固,ios代码混淆,ios代码混淆工具, iOS源码混淆使用说明详解

ios加固,ios代码混淆,ios代码混淆工具,iOS源码混淆产品是一款纯离线的源码加密工具,主要用于保护iOS项目中的核心代码,避免因逆向工程或破解,造成核心技术被泄漏、代码执行流程被分析等安全问题。该加密工具和普通编译器相似,基于项目源代码可将Object...
继续阅读 »

ios加固,ios代码混淆,ios代码混淆工具,iOS源码混淆产品是一款纯离线的源码加密工具,主要用于保护iOS项目中的核心代码,避免因逆向工程或破解,造成核心技术被泄漏、代码执行流程被分析等安全问题。该加密工具和普通编译器相似,基于项目源代码可将Objective-C、Swift、C、C++代码编译成二进制代码,不同之处在于,加密工具在编译时,能够对代码采取混淆、字符串加密等安全措施。从而避免攻击者通过IDA Pro等逆向工具反编译二进制代码,分析业务代码执行流程,进一步篡改或窃取核心技术。

概述

本文主要介绍iOS源码混淆产品之Xcode插件的使用方式,阅读者需具备iOS开发经验,否则使用可能存在困难。

安装插件

v13.0.2-20190703及其之前的版本为替换clang编译器的模式,之后版本为切换Xcode -> Toolchains的模式,后者可以在Xcode中快速切换编译器。

Xcode插件通过执行python install.py 命令安装编译器,使用完成后执行 python uninstal.py 即可卸载编译器。如下图:

(备注:如果有多个Xcode版本,请修改configuration.txt文件中Xcode默认的路径。)


执行安装会提示输入密码,输入电脑开机密码即可,Xcode插件安装成功后会有Install Success提示,如下图:


引入头文件

将include目录下的KiwiOBF.h头文件拷贝到iOS项目中,并在需的地方进行引用即可。

添加KIWIOBF标签

对需要进行混淆保护的函数,添加KIWIOBF标签,以告知编译器该函数需要进行混淆编译。如下图:


设置参数

全编译器有默认混淆参数,如不能满足需求,可以自定义配置参数
加密参数说明


iOS项目的混淆参数在 Other C Flags,Other C++ Flags,Other Swift Flags中设置,如下图:


卸载插件

Xcode插件:执行 python uninstall.py 即可卸载编译器。

友情告知地址,ios代码混淆,ios加固:https://www.kiwisec.com/product/compiler-ios.html

转自:https://www.jianshu.com/p/7fdb4544c916

收起阅读 »

iOS 常见面试题总结及答案(3)

一.列举出延迟调用的几种方法?1.performSelector方法 [self performSelector:@selector(Delay) withObject:nil afterDelay:3.0f];2.NSTimer定时器  [NSTimer s...
继续阅读 »

一.列举出延迟调用的几种方法?

1.performSelector方法 

[self performSelector:@selector(Delay) withObject:nil afterDelay:3.0f];

2.NSTimer定时器  

[NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:@selector(Delay) userInfo:nil repeats:NO];

3.sleepForTimeInterval

[NSThread sleepForTimeInterval:3.0f];

4.GCD方式

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self Delay];
});
- (void)Delay {
NSLog(@"执行");
}

二.NSCache 和NSDictionary 区别?

NSCache可以提供自动删减缓存功能,而且保证线程安全,与字典不同,不会拷贝键。
NSCache可以设置缓存上限,限制对象个数和总缓存开销。定义了删除缓存对象的时机。这个机制只对NSCache起到指导作用,不会一定执行。
NSPurgeableData搭配NSCache使用,可以自动清除数据。
只有那种“重新计算很费劲”的数据才值得放入缓存。

三.NSArray 和 NSSet区别

NSSet和NSArray功能性质一样,用于存储对象,属于集合。
NSSet属于 “无序集合”,在内存中存储方式是不连续
NSArray是 “有序集合” 它内存中存储位置是连续的。
NSSet,NSArray都是类,只能添加对象,如果需要加入基本数据类型(int,float,BOOL,double等),需要将数据封装成NSNumber类型。
由于NSSet是用hash实现的所以就造就了它查询速度比较快,但是我们不能把某某对象存在第几个元素后面之类的有关下标的操作。

四.什么是分类?

分类: 在不修改原有类代码的情况下,可以给类添加方法
Categroy 给类扩展方法,或者关联属性, Categroy底层结构也是一个结构体:内部存储这结构体的名字,那个类的分类,以及对象和类方法列表,协议,属性信息
通过Runtime加载某个类的所有Category数据
把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面
将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

五.为什么说OC是一门动态语言?

动态语言:是指程序在运行时可以改变其结构,新的函数可以被引进,已有的函数可以被删除等在结构上的变化
动态类型语言: 就是类型的检查是在运行时做的。
OC的动态特性可从三方面:

动态类型(Dynamic typing):最终判定该类的实例类型是在运行期间
动态绑定(Dynamic binding):在运行时确定调用的方法
动态加载(Dynamic loading):在运行期间加载需要的资源或可执行代码

六.什么是动态绑定?

动态绑定 将调用方法的确定也推迟到运行时。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去,这就是动态绑定。
在编译时,方法的 调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,

七.什么是谓词?

谓词(NSPredicate)是OC针对数据集合的一种逻辑帅选条件,类似一个过滤器,简单实实用代码如下:

Person * p1 = [Person personWithName:@"alex" Age:20];
Person * p2 = [Person personWithName:@"alex1" Age:30];
Person * p3 = [Person personWithName:@"alex2" Age:10];
Person * p4 = [Person personWithName:@"alex3" Age:40];
Person * p5 = [Person personWithName:@"alex4" Age:80];

NSArray * persons = @[p1, p2, p3, p4, p5];
//定义谓词对象,谓词对象中包含了过滤条件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age < 30"];
//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];

八.什么是类工厂方法?

类工厂方法就是用来快速创建对象的类方法, 他可以直接返回一个初始化好的对象,具备以下特征:

一定是类方法
返回值需要是 id/instancetype 类型
规范的方法名说说明类工厂方法返回的是一个什么对象,一般以类名首字母小写开始;
比如系统 UIButton 的buttonWithType 就是一个类工厂方法:

// 类工厂方法
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
// 使用
+ UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];

九.简要说明const,宏,static,extern区分以及使用?

1.const

const常量修饰符,经常使用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量。

- const 作用:限制类型
- 使用const修饰基本变量, 两种写法效果一致 , b都是只读变量
const int b = 5;
int const b = 5;
- 使用const修饰指针变量的变量
第一种: const int *p = &a 和 int const *q = &a; 效果一致,*p 的值不能改,p 的指向可以改;
第二种: int * const p = &a; 表示 p 的指向不能改,*p 的值可以改
第三种:
const int * const p = &a; *p 值和 p 的指向都不能改

const 在*左边, 指向可变, 值不可变
const 在*的右边, 指向不可变, 值可变
const 在*的两边, 都不可变

2.

* 基本概念:宏是一种批量处理的称谓。一般说来,宏是一种规则或模式,或称语法替换 ,用于说明某一特定输入(通常是字符串)如何根据预定义的规则转换成对应的输出(通常也是字符串)。这种替换在预编译时进行,称作宏展开。编译器会在编译前扫描代码,如果遇到我们已经定义好的宏那么就会进行代码替换,宏只会在内存中copy一份,然后全局替换,宏一般分为对象宏和函数宏。 宏的弊端:如果代码中大量的使用宏会使预编译时间变长。

const与宏的区别?

* 编译检查 宏没有编译检查,const有编译检查;
* 宏的好处 定义函数,方法 const不可以;
* 宏的坏处 大量使用宏,会导致预编译时间过长

3.static

* 修饰局部变量: 被static修饰局部变量,延长生命周期,跟整个应用程序有关,程序结束才会销毁,被 static 修饰局部变量,只会分配一次内存
* 修饰全局变量: 被static修饰全局变量,作用域会修改,也就是只能在当前文件下使用

4.extern

声明外部全局变量(只能用于声明,不能用于定义)

常用用法(.h结合extern联合使用)
如果在.h文件中声明了extern全局变量,那么在同一个类中的.m文件对全局变量的赋值必须是:数据类型+变量名(与声明一致)=XXXX结构。并且在调用的时候,必须导入.h文件。代码如下:

.h
@interface ExternModel : NSObject
extern NSString *lhString;
@end
.m
@implementation ExternModel
NSString *lhString=@"hello";
@end

调用的时候:例如:在viewController.m中调用,则可以引入:ExternModel.h,否则无法识别全局变量。当然也可以通过不导入头文件的方式进行调用(通过extern调用)。

十.id类型, nil , Nil ,NULL和NSNULL的区别?

id类型: 是一个独特的数据类型,可以转换为任何数据类型,id类型的变量可以存放任何数据类型的对象,在内部处理上,这种类型被定义为指向对象的指针,实际上是一个指向这种对象的实例变量的指针; id 声明的对象具有运行时特性,既可以指向任意类型的对象
nil 是一个实例对象值;如果我们要把一个对象设置为空的时候,就用nil
Nil 是一个类对象的值,如果我们要把一个class的对象设置为空的时候,就用Nil
NULL 指向基本数据类型的空指针(C语言的变量的指针为空)
NSNull 是一个对象,它用在不能使用nil的场合

十一.C和 OC 如何混编&&Swift 和OC 如何调用?

1.xcode可以识别一下几种扩展名文件:

.m文件,可以编写 OC语言 和 C 语言代码
.cpp: 只能识别C++ 或者C语言(C++兼容C)
.mm: 主要用于混编 C++和OC代码,可以同时识别OC,C,C++代码

2.Swift 调用 OC代码

需要创建一个 Target-BriBridging-Header.h 的桥文件,在乔文件导入需要调用的OC代码头文件即可

3.OC 调用 Swift代码
直接导入 Target-Swift.h文件即可, Swift如果需要被OC调用,需要使用@objc 对方法或者属性进行修饰

十二.OC与 JS交互方式有哪些?

1.通过拦截URL

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = request.URL.absoluteString;
if ([url rangeOfString:@"需要跳转源生界面的URL判断"].location != NSNotFound) {
//跳转原生界面
return NO;
}
return YES;
}

2.使用MessageHandler(WKWebView)

当JS端想传一些数据给iOS.那它们会调用下方方法来发送.
window.webkit.messageHandlers.<方法名>.postMessage(<数据>)上方代码在JS端写会报错,导致网页后面业务不执行.可使用try-catch执行.
那么在OC中的处理方法如下.它是WKScriptMessageHandler的代理方法.name和上方JS中的方法名相对应.

- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;

3.JavaScriptCore (UIWebView)
使用三方库WebViewJavascriptBridge,可提供 js 调OC,以及OC掉JS

1. 设置 webViewBridge
_bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.webView];
[_bridge setWebViewDelegate:self];
2. 注册handler方法,需要和 前段协商好 方法名字,是供 JS调用Native 使用的。
[_bridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
// OC调用
NSString *scanResult = @"http://www.baidu.com";
// js 回调传参
responseCallback(scanResult);
}];
3. OC掉用JS
[_bridge callHandler:@"testJSFunction" data:@"一个字符串" responseCallback:^(id responseData) {
NSLog(@"调用完JS后的回调:%@",responseData);
}];

4.OC调用JS代码

// 直接运行 使用 
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];

// 使用JavaScriptCore框架
#import
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//获取webview中的JS内容
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSString *runJS = @"执行的JS代码";
//准备执行的JS代码
[context evaluateScript:runJS];
}

十三.编译过程做了哪些事情

Objective,Swift都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。Objective,Swift二者的编译都是依赖于Clang + LLVM. OC和Swift因为原理上大同小异,知道一个即可!
1.iOS编译 不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。
2.编译器前端 :编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行
3.编译器后端 :编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。LVVM优化器会进行BitCode的生成,链接期优化等等,LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码。

十四.Category的实现原理&&使用场合&&Class Extension的区别

1.Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
2.在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

使用场合:

在不修改原有类代码的情况下,为类添对象方法或者类方法
或者为类关联新的属性
分解庞大的类文件

添加实例方法
添加类方法
添加协议
添加属性
关联成员变量

区别

Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中。

十五.Category能否添加成员变量?如果可以,如何给Category添加成员变量?

不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构)
可以通过 runtime 动态的关联属性

十六.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

十七.initialize方法如何调用,以及调用时机

当类第一次收到消息的时候会调用类的initialize方法
是通过 runtime 的消息机制 objc_msgSend(obj,@selector()) 进行调用的
优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的

十八.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

load 是类加载到内存时候调用, 优先父类->子类->分类
initialize 是类第一次收到消息时候调用,优先分类->子类->父类
同级别和编译顺序有关系
load 方法是在 main 函数之前调用的

十九.什么是Runtime?平时项目中有用过么?

Objective-C runtime是一个运行时库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码,类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用。通过了解runtime以及源码,可以更加深入的了解OC其特性和原理

OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行

OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数

平时编写的OC代码,底层都是转换成了Runtime API进行调用

具体应用

利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题

二十.讲一下 OC 的消息机制

1.OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
2.objc_msgSend底层有3大阶段   消息发送(当前类、父类中查找)、动态方法解析、消息转发

消息发送流程

当我们的一个 receiver(实例对象)收到消息的时候, 会通过 isa 指针找到 他的类对象, 然后在类对象方法列表中查找 对应的方法实现,如果 未找到,则会通过 superClass 指针找到其父类的类对象, 找到则返回,未找打则会一级一级往上查到,最终到NSObject 对象, 如果还是未找到就会进行动态方法解析
类方法调用同上,只不过 isa 指针找到元类对象;

动态方法解析机制&&消息转发机制流程

当我们发送消息未找到方法实现,就会进入第二步,动态方法解析: 代码实现如下

//  动态方法绑定- 实例法法调用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
Method method = class_getInstanceMethod(self, @selector(test));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 类方法调用
+(BOOL) resolveClassMethod:(SEL)sel....

未找到动态方法绑定,就会进行消息转发阶段

// 快速消息转发- 指定消息处理对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [Student new];
}
return [super forwardingTargetForSelector:aSelector];
}

// 标准消息转发-消息签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(run))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//内部逻辑自己处理
}

答案摘自作者:iOS猿_员

原贴链接:https://www.jianshu.com/p/4aaf45c11082

收起阅读 »

TCP、UDP协议和IP协议

一、TCP定义TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。面向连接意味着两个使用TCP的进程(一个客户和一个服务器)在交换数据之前必须先建立好连接,然后才能开始传输数据。建立连接时采用客户服务器模式,其中主动发起连接建立的进程叫做客户(Clie...
继续阅读 »
一、TCP
  1. 定义

    TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。面向连接意味着两个使用TCP的进程(一个客户和一个服务器)在交换数据之前必须先建立好连接,然后才能开始传输数据。建立连接时采用客户服务器模式,其中主动发起连接建立的进程叫做客户(Client),被动等待连接建立的进程叫做服务器(Server)。

  2. 端对端

    TCP提供全双工的数据传输服务,这意味着建立了TCP连接的主机双方可以同时发送和接收数据。这样,接收方收到发送方消息后的确认可以在反方向的数据流中进行捎带。“端到端”的TCP通信意味着TCP连接发生在两个进程之间,一个进程发送数据,只有一个接收方,因此TCP不支持广播和组播。

  3. 面向字节

    TCP连接面向字节流,字节流意味着用户数据没有边界,例如,发送进程在TCP连接上发送了2个512字节的数据,接收方接收到的可能是2个512字节的数据,也可能是1个1024字节的数据。因此,接收方若要正确检测数据的边界,必须由发送方和接收方共同约定,并且在用户进程中按这些约定来实现。

  4. 位于传输层
    TCP接收到数据包后,将信息送到更高层的应用程序,如FTP的服务程序和客户程序。应用程序处理后,再轮流将信息送回传输层,传输层再将它们向下传送到网际层,最后到接收方。


二、UDP

UDP与TCP位于同一层,但与TCP不同

  • UDP协议提供的是一种无连接的、不可靠的传输层协议,只提供有限的差错检验功能。

  • 它在IP层上附加了简单的多路复用功能,提供端到端的数据传输服务。

  • 设计UDP的目的是为了以最小的开销在可靠的或者是对数据可靠性要求不高的环境中进行通信,

  • 由于无连接,UDP支持广播和组播,这在多媒体应用中是非常有用的。


三、IP协议

  1. 定义

    IP(网际)协议是TCP/IP模型的核心,也是网络层最重要的协议。

  2. 功能

    网际层接收来自网络接口层的数据包,并将数据包发送到传输层;相反,也将传输层的数据包传送到网络接口层。
    IP协议主要包括无连接数据报传送,数据报路由器选择以及差错处理等功能。

  3. 局限及对策

    由于网络拥挤、网络故障等问题可能导致数据报无法顺利通过传输层。IP协议具有有限的报错功能,不能有效处理数据报延迟,不按顺序到达和数据报出错,所以IP协议需要与另外的协议配套使用,包括地址解析协议ARP、逆地址解析协议RARP、因特网控制报文协议ICMP、因特网组管理协议IGMP等。
    IP数据包中含有源地址(发送它的主机地址)和目的地址(接收它的主机地址)。

  4. 意义

    IP协议对于网络通信而言有着重要的意义。由于网络中的所有计算机都安装了IP软件,使得许许多多的局域网构成了庞大而严密的通信系统,才形成了如今的Internet。其实,Internet并非一个真实存在的网络,而是一个虚拟网络,只不过是利用IP协议把世界上所有愿意接入Internet的计算机局域网络连接起来,使之能够相互通信。

    链接:https://www.jianshu.com/p/b8b2220a8bd0

收起阅读 »

iOS 一键返回首页

在APP的开发中,我们难免会遇到这种情况,一层层的打开下一级控制,这时,我们再想回到原始控制器时,一级级返回不太现实,所以我们需要一种方法,来一次性返回首页从App的rootViewController开始,找到所有presentedController,然后...
继续阅读 »

在APP的开发中,我们难免会遇到这种情况,一层层的打开下一级控制,这时,我们再想回到原始控制器时,一级级返回不太现实,所以我们需要一种方法,来一次性返回首页

从App的rootViewController开始,找到所有presentedController,然后逆序dismiss这些Controller,最后pop to rootViewController就可以了。

- (void)backToHomePage
{
UIWindow *window = [(AppDelegate *)[UIApplication sharedApplication].delegate window];
UIViewController *presentedController = nil;

UIViewController *rootController = [window rootViewController];
if ([rootController isKindOfClass:[UITabBarController class]]) {
rootController = [(UITabBarController *)rootController selectedViewController];
}
presentedController = rootController;
//找到所有presented的controller,包括UIViewController和UINavigationController
NSMutableArray *presentedControllerArray = [[NSMutableArray alloc] init];
while (presentedController.presentedViewController) {
[presentedControllerArray addObject:presentedController.presentedViewController];
presentedController = presentedController.presentedViewController;
}
if (presentedControllerArray.count > 0) {
//把所有presented的controller都dismiss掉
[self dismissControllers:presentedControllerArray topIndex:presentedControllerArray.count - 1 completion:^{
[self popToRootViewControllerFrom:rootController];
}];
} else {
[self popToRootViewControllerFrom:rootController];
}
}
- (void)dismissControllers:(NSArray *)presentedControllerArray topIndex:(NSInteger)index completion:(void(^)(void))completion
{
if (index < 0) {
completion();
} else {
[presentedControllerArray[index] dismissViewControllerAnimated:NO completion:^{
[self dismissControllers:presentedControllerArray topIndex:index - 1 completion:completion];
}];
}
}
- (void)popToRootViewControllerFrom:(UIViewController *)fromViewController
{
//pop to root
if ([fromViewController isKindOfClass:[UINavigationController class]]) {
[(UINavigationController *)fromViewController popToRootViewControllerAnimated:YES];
}
if (fromViewController.navigationController) {
[fromViewController.navigationController popToRootViewControllerAnimated:YES];
}
}

参考这个思路可以做一些其他非常规页面跳转,跳转到我们想要跳转的指定界面去

原文链接:https://blog.csdn.net/yinyignfenlei/article/details/86167245

收起阅读 »

Node交互式命令行工具开发——自动化文档工具

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。  npm上命令行开发相关包很多,例如minimist、optimist、nopt、commander.js...
继续阅读 »

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。
  npm上命令行开发相关包很多,例如minimistoptimistnoptcommander.jsyargs等等,使用方法和效果类似。其中用得比较多的是TJ大神的commanderyargs,本文以commander为基础讲述,可以参考这篇教程,yargs教程可以参考阮大神的或者这一篇
  另外,一个完整的命令行工具开发,还需要了解processshelljspathlinebyline等模块,这些都是node基础模块或一些简单模块,非常简单,就不多说了,另外如果你不想用回调函数处理异步还需要了解一下PromiseGenerator函数。这是教程:i5ting大神的《深入浅出js(Node.js)异步流程控制》和阮大神的异步编程教程以及promise小人书,另外想尝试ES7 stage3阶段的async/await异步解决方案,可参考这篇教程async/await解决方案需要babel转码,这是教程。本人喜欢async/await(哪个node开发者不喜欢呢?)但不喜欢倒腾,况且async/await本身就是Promise的语法糖,所以没选择使用,据江湖消息,nodejs将在今年晚些时候(10月份?)支持async/await,很是期待。
  以下是文章末尾实例用到的一些依赖。

"dependencies": {
"bluebird": "^3.4.1",
"co": "^4.6.0",
"colors": "^1.1.2",
"commander": "^2.9.0",
"dox": "^0.9.0",
"handlebars": "^4.0.5",
"linebyline": "^1.3.0",
"mkdirp": "^0.5.1"
}

 其中bluebird用于Promise化,TJ大神的co用于执行Generator函数,handlebars是一种模板,linebyline用于分行读取文件,colors用于美化输出,mkdirp用于创建目录,另外教程中的示例是一款工具,可以自动化生成数据库和API接口的markdown文档,并通过修改git hooks,使项目的每次commit都会自动更新文档,借助了TJ大神的dox模块。
  <span style="color:rgb(0, 136, 204)">所有推荐教程/教材,仅供参考,自行甄选阅读。</span>

安装Node

  各操作系统下安装见Nodejs官网,安装完成之后用node -v或者which node等命令测试安装是否成功。which在命令行开发中是一个非常有用的命令,使用which命令确保你的系统中不存在名字相同的命令行工具,例如which commandName,例如which testdev命令返回空白那么说明testdev命令名称还没有被使用。

初始化

  1. 新建一个.js文件,即是你的命令要执行的主程序入口文件,例如testdev.js。在文件第一行加入#!/usr/bin/env node指明系统在运行这个文件的时候使用node作为解释器,等价于node testdev.js命令。
  2. 初始化package.json文件,使用npm init命令根据提示信息创建,也可以是使用npm init -y使用默认设置创建。创建完成之后需要修改package.json文件内容加入"bin": {"testdev": "./testdev.js"}这条信息用于告诉npm你的命令(testdev)要执行的脚本文件的路径和名字,这里我们指定testdev命令的执行文件为当前目录下的testdev.js文件。
  3. 为了方便测试在testdev.js文件中加入代码console.log('hello world');,这里只是用于测试环境是否搭建成功,更加复杂的程序逻辑和过程需要按照实际情况进行编写

测试

  使用npm link命令,可以在本地安装刚刚创建的包,然后就可以用testdev来运行命令了,如果正常的话在控制台会打印出hello world

commander

  TJ的commander非常简洁,README.md已经把使用方法写的非常清晰。下面是例子中的代码:

const program = require('commander'),
co = require('co');

const appInfo = require('./../package.json'),
asyncFunc = require('./../common/asyncfunc.js');

program.allowUnknownOption();
program.version(appInfo.version);

program
.command('init')
.description('初始化当前目录doc.json文件')
.action(() => co(asyncFunc.initAction));

program
.command('show')
.description('显示配置文件状态')
.action(() => co(asyncFunc.showAction));

program
.command('run')
.description('启动程序')
.action(() => co(asyncFunc.runAction));

program
.command('modifyhook')
.description('修改项目下的hook文件')
.action(() => co(asyncFunc.modifyhookAction));

program
.command('*')
.action((env) => {
console.error('不存在命令 "%s"', env);
});

program.on('--help', () => {
console.log(' Examples:');
console.log('');
console.log(' $ createDOC --help');
console.log(' $ createDOC -h');
console.log(' $ createDOC show');
console.log('');
});

program.parse(process.argv);

 定义了四个命令和个性化帮助说明。

交互式命令行process

  commander只是实现了命令行参数与回复一对一的固定功能,也就是一个命令必然对应一个回复,那如何实现人机交互式的命令行呢,类似npm init或者eslint --init这样的与用户交互,交互之后根据用户的不同需求反馈不同的结果呢。这里就需要node内置的process模块。
  这是我实现的一个init命令功能代码:

exports.initAction = function* () {
try {
var docPath = yield exists(process.cwd() + '/doc.json');
if (docPath) {
func.initRepl(config.coverInit, arr => {
co(newDoc(arr));
})
} else {
func.initRepl(config.newInit, arr => {
co(newDoc(arr));
})
}
} catch (err) {
console.warn(err);
}

首先检查doc.json文件是否存在,如果存在执行覆盖交互,如果不存在执行生成交互,try...catch捕获错误。
  交互内容配置如下:

newInit:
[
{
title:'initConfirm',
description:'初始化createDOC,生成doc.json.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],
coverInit:[
{
title:'modifyConfirm',
description:'doc.json已存在,初始化将覆盖文件.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],

人机交互部分代码也就是initRepl函数内容如下:

//初始化命令,人机交互控制
exports.initRepl = function (init, func) {
var i = 1;
var inputArr = [];
var len = init.length;
process.stdout.write(init[0].description);
process.stdin.resume();
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk) => {
chunk = chunk.replace(/[\s\n]/, '');
if (chunk !== 'y' && chunk !== 'Y' && chunk !== 'n' && chunk !== 'N') {
console.log(config.colors.red('您输入的命令是: ' + chunk));
console.warn(config.colors.red('请输入正确指令:y/n'));
process.exit();
}
if (
(init[i - 1].title === 'modifyConfirm' || init[i - 1].title === 'initConfirm') &&
(chunk === 'n' || chunk === 'N')
) {
process.exit();
}
var inputJson = {
title: init[i - 1].title,
value: chunk,
};
inputArr.push(inputJson);
if ((len--) > 1) {
process.stdout.write(init[i++].description)
} else {
process.stdin.pause();
func(inputArr);
}
});
}

人机交互才用向用户提问根据用户不同输入产生不同结果的形式进行,顺序读取提问列表并记录用户输入结果,如果用户输入n/N则终止交互,用户输入非法字符(除y/Y/n/N以外)提示输入命令错误。

文档自动化

  文档自动化,其中数据库文档自动化,才用依赖sequelize的方法手写(根据需求不同自行编写逻辑),API文档才用TJ的dox也很简单。由于此处代码与命令行功能相关度不大,请读者自行去示例地址查看代码。

示例地址

github地址
npm地址

原文链接:https://segmentfault.com/a/1190000039749423

收起阅读 »

JS前端面试总结

ES5的继承和ES6的继承有什么区别ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制完全不同,实质上是先创建父类的实...
继续阅读 »

ES5的继承和ES6的继承有什么区别

ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。
ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。

如何实现一个闭包?闭包的作用有哪些

在一个函数里面嵌套另一个函数,被嵌套的那个函数的作用域是一个闭包。
作用:创建私有变量,减少全局变量,防止变量名污染。可以操作外部作用域的变量,变量不会被浏览器回收,保存变量的值。

介绍一下 JS 有哪些内置对象

Object 是 JavaScript 中所有对象的父对象
数据封装类对象:Object、Array、Boolean、Number、String
其他对象:Function、Argument、Math、Date、RegExp、Error

new 操作符具体干了什么呢

(1)创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
(2)属性和方法被加入到 this 引用的对象中。
(3)新创建的对象由 this 所引用,并且最后隐式的返回 this 。

同步和异步的区别

同步的概念应该是来自于操作系统中关于同步的概念:不同进程为协同完成某项工作而在先后次序上调整(通过阻塞,唤醒等方式)。
同步强调的是顺序性,谁先谁后;异步则不存在这种顺序性。

同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作。

异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容。

异步解决方式优缺点

回调函数(callback)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return
优点:解决了同步的问题

Promise

Promise就是为了解决callback的问题而产生的。
回调地狱的根本问题在于:

缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
嵌套函数过多的多话,很难处理错误

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题
缺点:无法取消 Promise ,错误需要通过回调函数来捕获

Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

Async/await

async、await 是异步的终极解决方案

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

null 和 undefined 的区别

null: null表示空值,转为数值时为0;
undefined:undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

• 变量被声明了,但没有赋值时,就等于undefined。
• 对象没有赋值的属性,该属性的值为undefined。
• 函数没有返回值时,默认返回undefined。

JavaScript 原型,原型链 ? 有什么特点?

JavaScript 原型: 每创建一个函数,函数上都有一个属性为 prototype,它的值是一个对象。 这个对象的作用在于当使用函数创建实例的时候,那么这些实例都会共享原型上的属性和方法。

原型链: 在 JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接(proto)。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向)。这种一级一级的链结构就称为原型链(prototype chain)。 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止;到查找到达原型链的顶部(Object.prototype),仍然没有找到指定的属性,就会返回 undefined

如何获取一个大于等于0且小于等于9的随机整数

function randomNum(){
return Math.floor(Math.random()*10)
}

想要去除一个字符串的第一个字符,有哪些方法可以实现str.slice(1)

 str.substr(1)
str.substring(1)
str.replace(/./,'')
str.replace(str.charAt(0),'')

JavaScript的组成

JavaScript 由以下三部分组成:

ECMAScript(核心):JavaScript 语言基础
DOM(文档对象模型):规定了访问HTML和XML的接口
BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法

到底什么是前端工程化、模块化、组件化

前端工程化就是用做工程的思维看待和开发自己的项目,
而模块化和组件化是为工程化思想下相对较具体的开发方式,因此可以简单的认为模块化和组件化是工程化的表现形式。
模块化和组件化一个最直接的好处就是复用,同时我们也应该有一个理念,模块化和组件化除了复用之外还有就是分治,我们能够在不影响其他代码的情况下按需修改某一独立的模块或是组件,因此很多地方我们及时没有很强烈的复用需要也可以根据分治需求进行模块化或组件化开发。
模块化开发的4点好处:

  1 避免变量污染,命名冲突
  2 提高代码复用率
  3 提高维护性
4 依赖关系的管理

前端模块化实现的过程如下:
一 函数封装
我们在讲到函数逻辑的时候提到过,函数一个功能就是实现特定逻辑的一组语句打包,在一个文件里面编写几个相关函数就是最开始的模块了

function m1(){
    //...
  }

  function m2(){
    //...
  }

这样做的缺点很明显,污染了全局变量,并且不能保证和其他模块起冲突,模块成员看起来似乎没啥关系
二 对象
为了解决这个问题,有了新方法,将所有模块成员封装在一个对象中

var module = new Object({

_count:0,

m1:function (){ ``` },

m2:function (){ ``` }

})

这样 两个函数就被包在这个对象中, 嘿嘿 看起来没毛病是吗 继续往下:
当我们要使用的时候,就是调用这个对象的属性
module.m1()
诶嘿 那么问题来了 这样写法会暴露全部的成员,内部状态可以被外部改变,比如外部代码可直接改变计数器的值
//坏人的操作

module._count = 10;

最后的最后,聪明的人类找到了究极新的方法——立即执行函数,这样就可以达到不暴露私有成员的目的

var module = (function (){

var _count = 5;

var m1 = function (){ ``` };

var m2 = function (){ ``` };

return{
m1:m1,
m2:m2
}

})()

面向对象与面向过程

  1. 什么是面向过程与面向对象?

• 面向过程就是做围墙的时候,由你本身操作,叠第一层的时候:放砖头,糊水泥,放砖头,糊水泥;然后第二层的时候,继续放砖头,糊水泥,放砖头,糊水泥……
• 面向对象就是做围墙的时候,由他人帮你完成,将做第一层的做法抽取出来,就是放砖头是第一个动作,糊水泥是第二个动作,然后给这两个动作加上步数,最后告诉机器人有 n 层,交给机器人帮你工作就行了。

  1. 为什么需要面向对象写法?

• 更方便
• 可以复用,减少代码冗余度
• 高内聚低耦合
简单来说,就是增加代码的可复用性,减少咱们的工作,使代码更加流畅。

事件绑定和普通事件有什么区别

普通添加事件的方法:

var btn = document.getElementById("hello");
btn.onclick = function(){
alert(1);
}
btn.onclick = function(){
alert(2);
}

执行上面的代码只会alert 2

事件绑定方式添加事件:

var btn = document.getElementById("hello");
btn.addEventListener("click",function(){
alert(1);
},false);
btn.addEventListener("click",function(){
alert(2);
},false);

垃圾回收

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
  现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
1、标记清除
  这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
  垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
关于这一块,建议读读Tom大叔的几篇文章,关于作用域链的一些知识详解,读完差不多就知道了,哪些变量会被做标记。

2、引用计数
  另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。


原文链接:https://segmentfault.com/a/1190000018077712


收起阅读 »

面向面试编程,面向掘金面试

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣关于前端,后端,移动端的面试,这里统...
继续阅读 »

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣

关于前端,后端,移动端的面试,这里统统都有,希望可以在面试的过程中帮助到你。另外我也有一个仓库 日问 来记录前后端以及 devops 一些有意思的问题,欢迎交流

前端

后端

Android/IOS

原文:https://segmentfault.com/a/1190000021037487

收起阅读 »

通用的广告栏控件-ConvenientBanner

demo:ConvenientBanner通用的广告栏控件,让你轻松实现广告头效果。支持无限循环,可以设置自动翻页和时间(而且非常智能,手指触碰则暂停翻页,离开自动开始翻页。你也可以设置在界面onPause的时候不进行自动翻页,onResume之后继续自动翻页...
继续阅读 »

demo:



ConvenientBanner

通用的广告栏控件,让你轻松实现广告头效果。支持无限循环,可以设置自动翻页和时间(而且非常智能,手指触碰则暂停翻页,离开自动开始翻页。你也可以设置在界面onPause的时候不进行自动翻页,onResume之后继续自动翻页),并且提供多种翻页特效。 对比其他广告栏控件,大多都需要对源码进行改动才能加载网络图片,或者帮你集成不是你所需要的图片缓存库。而这个库能让有代码洁癖的你欢喜,不需要对库源码进行修改你就可以使用任何你喜欢的网络图片库进行配合。

demo是用Module方式依赖,你也可以使用gradle 依赖:

    implementation 'com.bigkoo:convenientbanner:2.1.5'//地址变小写了,额。。。
implementation 'androidx.recyclerview:recyclerview:1.0.0+'

// compile 'com.bigkoo:ConvenientBanner:2.1.4'//地址变ConvenientBanner 大写了,额。。。
//compile 'com.bigkoo:convenientbanner:2.0.5'旧版
Config in xml
<com.bigkoo.convenientbanner.ConvenientBanner
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/convenientBanner"
android:layout_width="match_parent"
android:layout_height="200dp"
app:canLoop="true" //控制循环与否
/>

config in java code

//自定义你的Holder,实现更多复杂的界面,不一定是图片翻页,其他任何控件翻页亦可。
convenientBanner.setPages(
new CBViewHolderCreator() {
@Override
public LocalImageHolderView createHolder(View itemView) {
return new LocalImageHolderView(itemView);
}

@Override
public int getLayoutId() {
return R.layout.item_localimage;
}
}, localImages)
//设置两个点图片作为翻页指示器,不设置则没有指示器,可以根据自己需求自行配合自己的指示器,不需要圆点指示器可用不设
// .setPageIndicator(new int[]{R.drawable.ic_page_indicator, R.drawable.ic_page_indicator_focused})
.setOnItemClickListener(this);
//设置指示器的方向
// .setPageIndicatorAlign(ConvenientBanner.PageIndicatorAlign.ALIGN_PARENT_RIGHT)
// .setOnPageChangeListener(this)//监听翻页事件
;

public class LocalImageHolderView implements Holder<Integer>{
private ImageView imageView;
@Override
public View createView(Context context) {
imageView = new ImageView(context);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
return imageView;
}

@Override
public void UpdateUI(Context context, final int position, Integer data) {
imageView.setImageResource(data);
}
}


原文链接:https://github.com/saiwu-bigkoo/Android-ConvenientBanner

代码下载:Android-ConvenientBanner-master.zip

收起阅读 »

iOS面试题(二)

数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t 对象,类对象,元类对象 消息传递 消息转发 一、数据结构:objc_object,objc_class,isa,class...
继续阅读 »
  • 数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t

  • 对象,类对象,元类对象

  • 消息传递

  • 消息转发



一、数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t







  • objc_object(id)
    isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关

  • objc_class (class) 继承自objc_object

  • isa指针,共用体isa_t


  • isa指向

    关于对象,其指向类对象。

    关于类对象,其指向元类对象。

    实例--(isa)-->class--(isa)-->MetaClass

  • cache_t

    用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用


 struct cache_t {
struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
mask_t _mask;//分配用来缓存bucket的总数
mask_t _occupied;//表明目前实际占用的缓存bucket的个数

struct bucket_t {
private:
cache_key_t _key;
IMP _imp;

复制代码


  • class_data_bits_t:对class_rw_t的封装


struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

复制代码

Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议


struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};
复制代码


  • method_t

    函数四要素:名称,返回值,参数,函数体


struct method_t {
SEL name; //名称
const char *types;//返回值和参数
IMP imp; //函数体

复制代码

二、 对象,类对象,元类对象



  • 类对象存储实例方法列表等信息。

  • 元类对象存储类方法列表等信息。


  • superClass是一层层集成的,到最后NSObject的superClass是nil.而NSObject的isa指向根元类,这个根元类的isa指向它自己,而它的superClass是NSObject,也就是最后形成一个环,

    三、消息传递


    void objc_msgSend(void /* id self, SEL op, ... */ )

    void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

    struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message.
    #if !defined(__cplusplus) && !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
    #else
    __unsafe_unretained _Nonnull Class super_class;
    #endif
    /* super_class is the first class to search */
    };
    复制代码



消息传递的流程:缓存查找-->当前类查找-->父类逐级查找



  • 调用方法之前,先去查找缓存,看看缓存中是否有对应选择器的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)

  • 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法 ,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)

  • 如果当前类对象的方法列表没有,就会逐级父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程)

  • 如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递


四、消息转发


+ (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
+ (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
复制代码





那么最后消息未能处理的时候,还会调用到

- (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来

方法交换(Method-Swizzling)
+ (void)load
{
Method test = class_getInstanceMethod(self, @selector(test));

Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

method_exchangeImplementations(test, otherTest);
}

应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息

动态添加方法

class_addMethod(self, sel, testImp, "v@:");

void testImp (void)
{
NSLog(@"testImp");
}

  • @dynamic 动态方法解析

    动态运行时语言将函数决议推迟到运行时

    编译时语言在编译期进行函数决议


  • [obj foo]和objc_msgSend()函数之间有什么关系?

    objc_msgSend()是[obj foo]的具体实现。在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。


  • runtime是如何通过selector找到对应的IMP地址的?

    缓存查找-->当前类查找-->父类逐级查找


  • 能否向编译后的类中增加实例变量?

    不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。

    但可以向动态添加的类中增加实例变量。


链接:https://juejin.cn/post/6844904039004504072 收起阅读 »

iOS面试题(一)

字符串反转链表反转有序数组合并Hash算法查找两个子视图的共同父视图求无序数组当中的中位数一、字符串反转给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh- (void)charReverse { NSString ...
继续阅读 »
  • 字符串反转
  • 链表反转
  • 有序数组合并
  • Hash算法
  • 查找两个子视图的共同父视图
  • 求无序数组当中的中位数

一、字符串反转
给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh

- (void)charReverse
{
NSString * string = @"hello,world";

NSLog(@"%@",string);

NSMutableString * reverString = [NSMutableString stringWithString:string];

for (NSInteger i = 0; i < (string.length + 1)/2; i++) {

[reverString replaceCharactersInRange:NSMakeRange(i, 1) withString:[string substringWithRange:NSMakeRange(string.length - i - 1, 1)]];

[reverString replaceCharactersInRange:NSMakeRange(string.length - i - 1, 1) withString:[string substringWithRange:NSMakeRange(i, 1)]];
}

NSLog(@"reverString:%@",reverString);

//C
char ch[100];

memcpy(ch, [string cStringUsingEncoding:NSUTF8StringEncoding], [string length]);

//设置两个指针,一个指向字符串开头,一个指向字符串末尾
char * begin = ch;

char * end = ch + strlen(ch) - 1;

//遍历字符数组,逐步交换两个指针所指向的内容,同时移动指针到对应的下个位置,直至begin>=end
while (begin < end) {

char temp = *begin;

*(begin++) = *end;

*(end--) = temp;
}

NSLog(@"reverseChar[]:%s",ch);
}
复制代码

二、链表反转
反转前:1->2->3->4->NULL
反转后:4->3->2->1->NULL

/**  定义一个链表  */
struct Node {

NSInteger data;

struct Node * next;
};

- (void)listReverse
{
struct Node * p = [self constructList];

[self printList:p];

//反转后的链表头部
struct Node * newH = NULL;
//头插法
while (p != NULL) {

//记录下一个结点
struct Node * temp = p->next;
//当前结点的next指向新链表的头部
p->next = newH;
//更改新链表头部为当前结点
newH = p;
//移动p到下一个结点
p = temp;
}

[self printList:newH];
}
/**
打印链表

@param head 给定链表
*/

- (void)printList:(struct Node *)head
{
struct Node * temp = head;

printf("list is : ");

while (temp != NULL) {

printf("%zd ",temp->data);

temp = temp->next;
}

printf("\n");
}


/** 构造链表 */
- (struct Node *)constructList
{
//头结点
struct Node *head = NULL;
//尾结点
struct Node *cur = NULL;

for (NSInteger i = 0; i < 10; i++) {

struct Node *node = malloc(sizeof(struct Node));

node->data = i;

//头结点为空,新结点即为头结点
if (head == NULL) {

head = node;

}else{
//当前结点的next为尾结点
cur->next = node;
}

//设置当前结点为新结点
cur = node;
}

return head;
}

复制代码

三、有序数组合并
将有序数组 {1,4,6,7,9} 和 {2,3,5,6,8,9,10,11,12} 合并为
{1,2,3,4,5,6,6,7,8,9,9,10,11,12}

- (void)orderListMerge
{
int aLen = 5,bLen = 9;

int a[] = {1,4,6,7,9};

int b[] = {2,3,5,6,8,9,10,11,12};

[self printList:a length:aLen];

[self printList:b length:bLen];

int result[14];

int p = 0,q = 0,i = 0;//p和q分别为a和b的下标,i为合并结果数组的下标

//任一数组没有达到s边界则进行遍历
while (p < aLen && q < bLen) {

//如果a数组对应位置的值小于b数组对应位置的值,则存储a数组的值,并移动a数组的下标与合并结果数组的下标
if (a[p] < b[q]) result[i++] = a[p++];

//否则存储b数组的值,并移动b数组的下标与合并结果数组的下标
else result[i++] = b[q++];
}

//如果a数组有剩余,将a数组剩余部分拼接到合并结果数组的后面
while (++p < aLen) {

result[i++] = a[p];
}

//如果b数组有剩余,将b数组剩余部分拼接到合并结果数组的后面
while (q < bLen) {

result[i++] = b[q++];
}

[self printList:result length:aLen + bLen];
}
- (void)printList:(int [])list length:(int)length
{
for (int i = 0; i < length; i++) {

printf("%d ",list[i]);
}

printf("\n");
}
复制代码

四、HASH算法

  • 哈希表
    例:给定值是字母a,对应ASCII码值是97,数组索引下标为97。
    这里的ASCII码,就算是一种哈希函数,存储和查找都通过该函数,有效地提高查找效率。
  • 在一个字符串中找到第一个只出现一次的字符。如输入"abaccdeff",输出'b'

    字符(char)是一个长度为8的数据类型,因此总共有256种可能。每个字母根据其ASCII码值作为数组下标对应数组种的一个数字。数组中存储的是每个字符出现的次数。
- (void)hashTest
{
NSString * testString = @"hhaabccdeef";

char testCh[100];

memcpy(testCh, [testString cStringUsingEncoding:NSUTF8StringEncoding], [testString length]);

int list[256];

for (int i = 0; i < 256; i++) {

list[i] = 0;
}

char *p = testCh;

char result = '\0';

while (*p != result) {

list[*(p++)]++;
}

p = testCh;

while (*p != result) {

if (list[*p] == 1) {

result = *p;

break;
}

p++;
}

printf("result:%c",result);
}
复制代码

五、查找两个子视图的共同父视图
思路:分别记录两个子视图的所有父视图并保存到数组中,然后倒序寻找,直至找到第一个不一样的父视图。

- (void)findCommonSuperViews:(UIView *)view1 view2:(UIView *)view2
{
NSArray * superViews1 = [self findSuperViews:view1];

NSArray * superViews2 = [self findSuperViews:view2];

NSMutableArray * resultArray = [NSMutableArray array];

int i = 0;

while (i < MIN(superViews1.count, superViews2.count)) {

UIView *super1 = superViews1[superViews1.count - i - 1];

UIView *super2 = superViews2[superViews2.count - i - 1];

if (super1 == super2) {

[resultArray addObject:super1];

i++;

}else{

break;
}
}

NSLog(@"resultArray:%@",resultArray);

}
- (NSArray *)findSuperViews:(UIView *)view
{
UIView * temp = view.superview;

NSMutableArray * result = [NSMutableArray array];

while (temp) {

[result addObject:temp];

temp = temp.superview;
}

return result;
}
复制代码

六、求无序数组中的中位数
中位数:当数组个数n为奇数时,为(n + 1)/2,即是最中间那个数字;当n为偶数时,为(n/2 + (n/2 + 1))/2,即是中间两个数字的平均数。
首先要先去了解一些几种排序算法:iOS排序算法
思路:

  • 1.排序算法+中位数
    首先用冒泡排序、快速排序、堆排序、希尔排序等排序算法将所给数组排序,然后取出其中位数即可。
  • 2.利用快排思想
链接:https://juejin.cn/post/6844904038996279309
收起阅读 »

vue 自动化路由实现

1、需求描述在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我...
继续阅读 »

1、需求描述

在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示


那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我们几乎都是一个文件夹下有很多子文件,子文件中一般包含index.vue, detail.vue, edit.vue分别对应的事列表页,详情页和编辑页。


 上图是我们的文件目录,views文件夹中存放的是所有的页面,goodsPlanning是一级目录,onNewComplete和thirdGoods是二级目录,二级目录中存放的是具体的页面,indexComponents中存放的是index.vue的文件,editComponents也是同样的道理。index.vue对应的路由是/goodsPlanning/onNewComplete, edit.vue对应的路由是/goodsPlanning/onNewComplete/edit,detail.vue也是同样的道理。所以我们的文件夹和路由是完全能够对应上的,只要知道路由,就能很快的找到对应的文件。那么有没有办法能够读取我们二级目录下的所有文件,然后根据文件名来生成路由呢?答案是有的


2 、require.context介绍

简单说就是:有了require.context,我们可以得到指定文件夹下的所有文件

require.context(directory, useSubdirectories = false, regExp = /^\.\//);

require.context有三个参数:

  • directory:说明需要检索的目录
  • useSubdirectories:是否检索子目录
  • regExp: 匹配文件的正则表达式

require.context()的返回值,有一个keys方法,返回的是个数组

let routers = require.context('VIEWS', true).keys()
console.log(routers)



 通过上面的代码,我们打印出了所有的views文件夹下的所有文件和文件夹,我们只要写好正则就能找到我们所需要的文件


3、 直接上代码

import Layout from 'VIEWS/layout/index'

/**
* 正则 首先匹配./ ,然后一级目录,不包含components的二级目录,以.vue结尾的三级目录
*/
let routers = require.context('VIEWS', true, /\.\/[a-z]+\/(?!components)[a-z]+\/[a-z]+\.vue$/i).keys()
let indexRouterMap = {} // 用来存储以index.vue结尾的文件,因为index.vue是列表文件,需要加入layout(我们的菜单),需要keepAlive,需要做权限判断
let detailRouterArr = [] // 用来存储以非index.vue结尾的vue文件,此类目前不需要layout
routers.forEach(item => {
const paths = item.match(/[a-zA-Z]+/g) //paths中存储了一个目录,二级目录,文件名
const routerChild = { //定义路由对象
path: paths[1],
name: `${paths[0]}${_.upperFirst(paths[1])}`, //upperFirst,lodash 首字母大写方法
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
}
if (/index\.vue$/.test(item)) { //判断是否以indexvue结尾
if (indexRouterMap[paths[0]]) { //判断一级路由是否存在,存在push二级路由,不存在则新建
indexRouterMap[paths[0]].children.push(routerChild)
} else {
indexRouterMap[paths[0]] = {
path: '/' + paths[0],
component: Layout,
children: [routerChild]
}
}
} else { //不以index.vue结尾的,直接添加到路由中
detailRouterArr.push({
path: item.slice(1, -4), //渠道最前面的 . 和最后的.vue
name: `${paths[0]}${_.upperFirst(paths[1])}${_.upperFirst(paths[2])}`,
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
meta: {
noCache: true, //不keepAlive
noVerify: true //不做权限验证
}
})
}
})

export default [
...Object.values(indexRouterMap),
...detailRouterArr,
/**
* dashboard单独处理下
*/
{
path: '',
component: Layout,
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: () => import('VIEWS/dashboard/index'),
name: 'dashboard',
meta: { title: '首页', noCache: true, noVerify: true }
}
]
},
]

简简单单的几十行代码就实现了所有的路由功能,再也不用一行一行的写路由文件了。可能你的文件管理方式和我的不一样,但是只要稍微改改正则就行了。


4、 注意

  1. 不能用import引入路由因为用import引入不支持变量
  2. 不能用别名找了半天问题,才知道用变量时也不能用别名,所以我用的都是相对路径


5、 好处

  • 不用在添加路由了,这个就不说了,明眼人都看得出来
  • 知道了路由,一个能找到对应的文件,以前我们团队就出现过,乱写path的情况
  • 更好的控制验证和keepAlive

原文链接:https://www.cnblogs.com/mianbaodaxia/p/11452123.html

收起阅读 »

iOS基础之Category(一)

一、简介 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要...
继续阅读 »

一、简介


  1. 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要的 category,比如 SDWebImage 中 UIImageView+WebCache 和 UIButton+WebCache,根据不同需求加载不同的 category。


二、Extension 和 Category 对比



  • extension 是在编译器决定的,它就是类的一部分,在编译期和头文件里的 @interface 和 实现文件里的 @implementation形成一个完整的类,它伴随类的的产生而产生,随着类的消亡而消亡。extension 一般用来隐藏类的私有信息,必须有类的源码才可以为一个类添加 extension。所以无法为系统的类添加 extension

  • category 是在运行期决定的,category 是无法添加实例变量的,extension 是可以添加的。


三、Category 的本质

3.1 Category的基本使用



我们首先来看以下 category的基本使用:


// Person+Eat.h

#import "Person.h"

@interface Person (Eat)

- (void)eatBread;

+ (void)eatFruit;

@property (nonatomic, assign) int count;

@end

// Person+Eat.m

#import "Person+Eat.h"

@implementation Person (Eat)

- (void)eatBread {
NSLog(@"eatBread");
}

+ (void)eatFruit {
NSLog(@"eatFruit");
}

@end
复制代码


  • 创建了一个 Person 的分类,专门实现吃这个功能

  • 这个分类遵守了2个协议,分别为 NSCopyingNSCoding

  • 声明了2个方法,一个实例方法,一个类方法

  • 定义一个 count 属性


3.2 编译期的 Category


我们通过 clang 编译器来观察一下在编译期这些代码的本质是什么?


xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MyClass.m -o MyClass-arm64.cpp
复制代码

编译之后,我们可以发现 category 的本质是结构体 category_t,无论我们创建了多少个 category 最终都会生成 category_t 这个结构体,并且 category 中的方法、属性、协议都是存储在这个结构体里的。也就是说在编译期,分类中成员是不会和类合并在一起的


struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
复制代码


  • name:类的名字

  • cls:类

  • instanceMethodscategory 中所有给类添加的实例方法的列表

  • classMethodscategory 中所有给类添加的类方法的列表

  • protocolscategory 中实现的所有协议的列表

  • instancePropertiescategory 中添加的所有属性


category 的定义中可以看到我们可以 添加实例方法,添加类方法,可以实现协议,可以添加属性。


不可以添加实例变量


我们继续研究下面的编译后的代码:


static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatBread", "v16@0:8", (void *)_I_Person_Eat_eatBread}}
};

static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatFruit", "v16@0:8", (void *)_C_Person_Eat_eatFruit}}
};

static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};

static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"count","Ti,N"}}
};

static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{

"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat,
};
复制代码


  • 首先看一下 _OBJC_$_CATEGORY_Person_$_Eat 结构体变量中的值,就是分别对应 category_t 的成员,第1个成员就是类名,因为我们声明了实例方法,类方法,遵守了协议,定义了属性,所以我们的结构体变量中这些都会有值。

  • _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat 结构体表示实例方法列表,里面包含了 eatBread 实例方法

  • _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat 结构体包含了 eatFruit 类方法

  • _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat 结构体包含了 NSCopingNSCoding 协议

  • _OBJC_$_PROP_LIST_Person_$_Eat 结构体包含了 count 属性


3.3 运行期的 Category


在研究完编译时期的 category 后,我们进而研究运行时期的 category


objc-runtime-new.mm 的源码中,我们可以最终找到如何将 category 中的方法列表,属性列表,协议列表添加到类中。


static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)

{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}

/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/

constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];

uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();

for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if ( ) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}

protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}

if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}

rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
复制代码


  • rwe->methods.attachLists(mlists, mcount);

  • rwe->protocols.attachLists(protolists, protocount);

  • rwe->properties.attachLists(proplists, propcount);


以上三个函数就是把 category 中的方法、属性和协议列表添加到类中的函数。


继续查看 attchLists 函数的实现:


void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
复制代码


  • 在这段源码中,主要关注2个函数 memmovememcpy

  • memmove 函数的作用是移动内存,将之前的内存向后移动,将原来的方法列表往后移

  • memcpy 函数的作用是内存的拷贝,将 category 中的方法列表复制到上一步移出来的位置。


从上述源码中,可以发现 category 的方法并没有替换原来类已有的方法,如果 category 和原来类中都有某个同名方法,只不过 category 中的方法被放到了新方法列表的前面,在运行时查找方法的时候是按照顺序查找的,一旦找到该方法,就不会向下继续查找了,产生了 category 会覆盖原类方法的假象。



所以我们在 category 定义方法的时候都要加上前缀,以避免意外的重名把类本身的方法”覆盖“掉。




  • 如果多个 category 中存在同名的方法,运行时最终调用哪个方法是由编译器决定的,最后一个参与编译的方法将会先被调用

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

收起阅读 »

iOS 常见面试题总结及答案(2)

一.App启动过慢,你可能想到的因素有哪些?1.解析Info.plist   加载相关信息,例如如闪屏 沙箱建立、权限检查2.Mach-O加载 如果是胖二进制文件,寻找合适当前CPU类别的部分加载所有依赖的Mach-O...
继续阅读 »

一.App启动过慢,你可能想到的因素有哪些?

1.解析Info.plist  

 加载相关信息,例如如闪屏 沙箱建立、权限检查

2.Mach-O加载 

如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为attribute((constructor))的C函数
加载类扩展(Category)中的方法

3.程序执行

调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching

影响启动性能的因素

main()函数之前耗时的影响因素
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load越多,启动越慢
main()函数之后耗时的影响因素
执行main()函数的耗时
执行applicationWillFinishLaunching的耗时
rootViewController及其childViewController的加载、view及其subviews的加载

优化

纯代码方式而不是storyboard加载首页UI。
对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载。
对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。


二.单例的利弊

优点:
1:一个类只被实例化一次,提供了对唯一实例的受控访问。
2:节省系统资源
3:允许可变数目的实例。

缺点:
1:一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”。
2:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
3:滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。


三.TCP和UDP的区别于联系

TCP为传输控制层协议,为面向连接、可靠的、点到点的通信;
UDP为用户数据报协议,非连接的不可靠的点到多点的通信;
TCP侧重可靠传输,UDP侧重快速传输


四.TCP连接的三次握手

第一次握手:客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;

第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开 TCP 连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)



五.假如Controller太臃肿,如何优化?

1.将网络请求抽象到单独的类中

方便在基类中处理公共逻辑;
方便在基类中处理缓存逻辑,以及其它一些公共逻辑;
方便做对象的持久化。

2.将界面的封装抽象到专门的类中
构造专门的 UIView 的子类,来负责这些控件的拼装。这是最彻底和优雅的方式,不过稍微麻烦一些的是,你需要把这些控件的事件回调先接管,再都一一暴露回 Controller。

3.构造 ViewModel
借鉴MVVM。具体做法就是将 ViewController 给 View 传递数据这个过程,抽象成构造 ViewModel 的过程

4.专门构造存储类
专门来处理本地数据的存取。

5.整合常量


六.对程序性能的优化你有什么建议?

1.使用复用机制

2.尽可能设置 View 为不透明

3.避免臃肿的 XIB 文件

4.不要阻塞主线程

5.图片尺寸匹配 UIImageView

6.选择合适的容器

7.启用 GZIP 数据压缩

8.View 的复用和懒加载机制

9、缓存

服务器的响应信息(response)。图片。计算值。比如:UITableView 的 row heights。

10.关于图形绘制

11.处理 Memory Warnings

在 AppDelegate 中实现 - [AppDelegate applicationDidReceiveMemoryWarning:] 代理方法。
在 UIViewController 中重载 didReceiveMemoryWarning 方法。
监听 UIApplicationDidReceiveMemoryWarningNotification 通知。

12.复用高开销的对象

13.减少离屏渲染(设置圆角和阴影的时候可以选用绘制的方法)

14.优化 UITableView

通过正确的设置 reuseIdentifier 来重用 Cell。
尽量减少不必要的透明 View。
尽量避免渐变效果、图片拉伸和离屏渲染。
当不同的行的高度不一样时,尽量缓存它们的高度值。
如果 Cell 展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的 response。
使用 shadowPath 来设置阴影效果。
尽量减少 subview 的数量,对于 subview 较多并且样式多变的 Cell,可以考虑用异步绘制或重写 drawRect。
尽量优化 - [UITableView tableView:cellForRowAtIndexPath:] 方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。
选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
对于 rowHeight、sectionFooterHeight、sectionHeaderHeight 尽量使用常量。

15.选择合适的数据存储方式

在 iOS 中可以用来进行数据持有化的方案包括:
NSUserDefaults。只适合用来存小数据。
XML、JSON、Plist 等文件。JSON 和 XML 文件的差异在「选择正确的数据格式」已经说过了。
使用 NSCoding 来存档。NSCoding 同样是对文件进行读写,所以它也会面临必须加载整个文件才能继续的问题。
使用 SQLite 数据库。可以配合 FMDB 使用。数据的相对文件来说还是好处很多的,比如可以按需取数据、不用暴力查找等等。
使用 CoreData。也是数据库技术,跟 SQLite 的性能差异比较小。但是 CoreData 是一个对象图谱模型,显得更面向对象;SQLite 就是常规的 DBMS。

16.减少应用启动时间

快速启动应用对于用户来说可以留下很好的印象。尤其是第一次使用时。
保证应用快速启动的指导原则:
尽量将启动过程中的处理分拆成各个异步处理流,比如:网络请求、数据库访问、数据解析等等。
避免臃肿的 XIB 文件,因为它们会在你的主线程中进行加载。重申:Storyboard 没这个问题,放心使用。
注意:在测试程序启动性能的时候,最好用与 Xcode 断开连接的设备进行测试。因为 watchdog 在使用 Xcode 进行调试的时候是不会启动的。

17.使用 Autorelease Pool (内存释放池)

18.imageNamed 和 imageWithContentsOfFile


七.使用drawRect有什么影响?

drawRect方法依赖Core Graphics框架来进行自定义的绘制

缺点:它处理touch事件时每次按钮被点击后,都会用setNeedsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了

这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制


八.基于CTMediator的组件化方案,有哪些核心组成?

假如主APP调用某业务A,那么需要以下组成部分:

CTMediator类,该类提供了函数 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
这个函数可以根据targetName生成对象,根据actionName构造selector,然后可以利用performSelector:withObject:方法,在目标上执行动作。

业务A的实现代码,另外要加一个专门的类,用于执行Target Action
类的名字的格式:Target_%@,这里就是Target_A。
这个类里面的方法,名字都以Action_开头,需要传参数时,都统一以NSDictionary*的形式传入。
CTMediator类会创建Target类的对象,并在对象上执行方法。

业务A的CTMediator扩展
扩展里声明了所有A业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。
在扩展的实现里,对Target, Action需要通过硬编码进行指定。由于扩展的负责方和业务的负责方是相同的,所以这个不是问题。


九.为什么CTMediator方案优于基于Router的方案?

Router的缺点:

在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。

在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。

为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的

CTMediator的优点:

调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。

组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。

方便传递各种类型的参数。


十.内存的使用和优化的注意事项

重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用;

尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;

不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多;

选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。

延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。

数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。

处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。

避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要;

使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;

正确选择图片加载方式:UIImage加载方式


摘自作者:iOS猿_员
原贴链接:https://www.jianshu.com/p/4b4bd4e3feff

收起阅读 »

前端自测清单(前端八股文)

缘起这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。先从性能优化开始吧。性能优化大体可以分为两个,运行时优化加载时优化加载时优化网络优化dns寻址过程tcp的三次握手和四次挥手,以及为何要三次和为何要四次https的握手过程,以及对称加密和非...
继续阅读 »

/zi-ce-qing-dan/featured-image.jpg

缘起

这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。

先从性能优化开始吧。性能优化大体可以分为两个,

  • 运行时优化
  • 加载时优化

加载时优化

网络优化

  • dns寻址过程
  • tcp的三次握手和四次挥手,以及为何要三次和为何要四次
  • https的握手过程,以及对称加密和非对称加密的区别,什么是中间人劫持,ca证书包括哪些内容
  • http1.0,http1.1以及http2.0的区别,多路复用具体指的是什么,keep-alive具体如何体现
  • cdn的原理,cdn什么情况下会回源,cdn的适用场景
  • 浏览器缓存有哪几种,它们的区别是什么,什么时候发生缓存,如何决定缓存哪些文件
  • 了解过websocket么,解释一下websocket的作用

渲染优化

  • 关键渲染路径优化,什么是关键渲染路径,分别如何优化
  • 优化体积,webpack的分包策略,如何配置优化,如何提高构建速度,tree-shaking是什么
  • cssom 的优化,以及html解析过程中,遇到哪些tag会阻塞渲染
  • 雅虎军规说,css尽量放到head里,js放到下方,那么移动端适配的flexiblejs为何要放到css上方呢
  • 影响回流重绘的因素有哪些,如何避免回流,以及bfc是什么,bfc有什么特性,清除浮动的原理是什么

场景:如何优化首屏

除了上以及下面说到的,这里也是分两个层面,

  • 加载时优化
  • 运行时优化

加载

  • 首屏请求和非首屏请求拆分
  • 图片都应该使用懒加载的形式加载
  • 使用preload预加载技术,以及prefetch的dns预解析
  • 与首屏无关的代码可以加async甚至是defer等待网页加载完成后运行

运行

这里跟加载的异常耦合,另作分析吧。

运行时优化

  • 虚拟长列表渲染
  • 图片懒加载
  • 使用事件委托
  • react memo以及pureComponent
  • 使用SSR
  • 。。。

以及一些比较骚的操作,只能特定场景使用,

  • serviceWorker劫持页面
  • 利用worker

更新一波,性能优化之外的面试题,

底层

  • V8是如何实现GC的
  • JS的let,const,call stack,function context,global context。。。的区别
  • this的指向,箭头函数中this和function里的this有什么区别
  • 原型链是什么,继承呢,有几种继承方式,如何实现es6的class
  • eventloop是什么,浏览器的eventloop和nodejs的eventloop有什么区别,nexttick是什么
  • commonjs和AMD,CMD的区别,以及跟ES MODULE的区别
  • 说说require.cache
  • 了解过,洋葱模型没有,它是如何实现的
  • 说说nodejs中的流,也就是stream
  • 你用过ts,说说你常用的ts中的数据类型
  • js的数据类型,weakMap,weakSet和Map以及Set的区别是什么
  • 为何0.1+0.2 不等于0.3,如何解决这个问题
  • js的类型转换
  • 正则表达式
  • 对象循环引用会发生什么问题
  • 如何捕获异步的异常,你能说出几种方案

CSS相关

  • position有哪几种属性,它们的区别是什么
  • 如何实现垂直居中,移动端的呢
  • margin设置百分比,是依据谁的百分比,padding呢
  • 怪异盒模型和一般盒模型有什么区别
  • flex:1代表什么,flex-shrink和flex-grow有什么区别
  • background-size origin基准点在哪里
  • 移动端1px解决方案,以及为何会产生这个问题
  • 移动端高清屏图片的解决方案
  • 说说GPU加速

跨端

  • RN 实现原理
  • 小程序实现原理
  • webview跟h5有什么区别
  • RPC 是什么
  • JSBridge 原理是什么
  • 网页唤起app的原理是什么

服务端

  • oauth2了解过没有,sso呢
  • JWT 如何实现的

网络

除了之前提到的网络问题,当然还有很多,比如

  • 为何使用1x1的gif进行请求埋点
  • TCP 如何进行拥塞控制

安全

  • csrf是什么,防范措施是什么
  • xss如何防范

浏览器相关

  • 跨域是如何产生的,如何解决
  • 如何检查性能瓶颈
  • 打开页面白屏,如何定位问题,或者打开页面CPU100%,如何定位问题
  • jsonp是什么,为何能解决跨域,使用它可能会产生什么问题
  • base64会产生什么问题
  • event.target和event.currTarget有什么区别

框架相关

  • react和vue的区别
  • react的调度原理
  • setstate为何异步
  • key的作用是什么,为何说要使用唯一key,react的diff算法是如何实现的,vue的呢
  • react的事件系统是如何实现的
  • react hook是如何实现的
  • react的通信方式,hoc的使用场景
  • 听过闭包陷阱么,为何会出现这种现象,如何避免
  • vue的响应式原理
  • 为何vue3.x用的是proxy而不是object.defineProperties
  • vue是如何实现对数据的监听的,对数组呢
  • vue中的nexttick是如何实现的
  • fiber是什么,简单说说时间切片如何实现,为何vue不需要时间切片
  • webpack是如何实现的,HMR是如何实现的,可以写个简单的webpack么,webpack的执行流程是怎样的
  • koa源码实现,洋葱模型原理,get/post等这些方法如何set入koa里的,ctx.body为何能直接改变response的body
  • 你简历上写的了解过webpack源码,到哪种程度了(实话说没写koa简单。。

算法相关

  • js大整数加法
  • 双指针
  • 经典排序
  • 动态规划
  • 贪心算法
  • 回溯法
  • DFS
  • BFS
  • 链表操作
  • 线性求值
  • 预处理,前缀和

项目相关

  • 项目中遇到的最大问题是什么,如何解决的
  • nodejs作为中间层的作用是什么

场景题(机试)

  • 如何实现直播上的弹幕组件,要求不能重叠,仿照b站上的弹幕
  • 如何实现动态表单,仿照antd上的form组件
  • 实现一个promise(一般不会这样问)
  • 实现一个限制请求数量的方法
  • 如何实现一个大文件的上传
  • 实现一个eventEmitter
  • 实现一个new,call,bind,apply
  • 实现一个throttle,debound
  • 实现promise.then,finally,all
  • 实现继承,寄生组合继承,instanceof
  • 实现Generator,Aynsc

20.11.20 更新来了

  • React 生命周期,(分三个阶段进行回答,挂载阶段,更新阶段以及卸载阶段)
  • Vue 生命周期 以及其父子组件的生命周期调度顺序
  • 如果让你用强缓存或者协商缓存来缓存资源的话,你会如何使用
  • 作用域是什么,作用域链呢?(这题我想了下,不会利用语言去表达这个东西。。)

目前暂时想到这些,后来有想到的会补充上去。

原文链接:https://steinw.cc/zi-ce-qing-dan/


收起阅读 »

iOS 常见面试题总结及答案(1)

一.    Runloop和线程的关系?1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建2.runloop在第一次获取时创建,在线程结束时销毁.1.NSTimer在子线程开启一个定时器;控制定...
继续阅读 »

一.    Runloop和线程的关系?

1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建

2.runloop在第一次获取时创建,在线程结束时销毁.

runloop 的运行逻辑就是 do-while 循环下运用观察者模式(或者说是消息发送),根据7种状态的变化,处理事件输入源和定时器。如下图


runloop的应用:

1.NSTimer在子线程开启一个定时器;控制定时器在特定模式下执行

2.imageView的显示

3.performSelector

4.常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)

5.自动释放池

二.自动释放池什么时候释放?

第一次创建:启动runloop时候

最后一次销毁:runloop退出的时候

其他时候的创建和销毁:当runloop即将睡眠时销毁之前的释放池,重建一个新的

三.什么时候使用weak关键字,和assign的区别?

1.arc中有可能出现循环引用的地方,比如delegate属性

2.自定义IBOutlet空间属性一般也是使用weak

区别:weak表明一种非持有关系,必须用于oc对象;assign用于基本数据类型

weak修饰的指针默认是nil,如果用assign修饰对象,在对象被销毁时,会产生野指针,容易发生崩溃

四.objc中向一个nil对象发送消息将会发生什么?

在oc中向nil发送消息是完全有效的,只是在运行时不会有任何作用,如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil),如果向一个nil对象发送消息,首先寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误.

五.runtime如何实现weak变量的自动置nil

runtime对注册的类,会进行布局,对于weak对象会放入一个hash表中,用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil

六.runtime如何通过selector找到对应的IMP地址?

每一个类对象都有一个方法列表,方法列表中记录着方法名称.方法实现.参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现

七.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

1.不能向编译后得到的类中增加实例变量,可以向运行时创建的类中添加实例变量

原因: (1)因为编译后的类已经注册在runtime中,类结构中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同事runtime会调用class_setIvarLayout 或者class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量

(2)运行时创建的的类可以添加实例变量,调用class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上

八.kvo的实现原理?

当你观察一个对象时,一个新的类会被动态创建,这个类继承自该对象的原本的类,并重写了被观察属性的setter方法,重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值得更改,最后通过isa混写,把这个对象的isa指针(isa指针告诉runtime系统这个对象的类是什么)指向这个新创建的子类,对象就神奇的变成了新创建子类的实例

实现原理如下图:


九.谈谈你对kvc的理解

kvc可以通过key直接访问对象属性,或者给对象的属性赋值,这样可以在运行时动态访问或者改变对象的属性值

当调用setValue:属性值forKey:@"name"的代码时,底层执行机制如下:

1.程序优先调用set:属性值方法,代码通过setter方法完成设置,注意,这里的是指成员变量名,首字母大小写要符合kvc的命名规则

2.如果没有找到setName:方法,kvc机制会检查+(Bool)accessInstanceVariablesDiretly方法,有没有返回yes,默认是返回yes,如果重写了该方法切返回NO的话,那么这一步会直接执行setValue:forUnderfindKey:方法, 如果返回Yes,那么kvc机制会搜索该类里面有没有名为的成员变量,不论该变量是在类接口处定义,还是在类实现处定义,也无论用什么样的访问修饰符只要存在以命名的变量,kvc都可以对该变量赋值

3.如果该类即没有set方法,也没有成员变量,kvc机制会搜索_is的成员变量

4.和上面一样,如果该类即没有set:方法,也没有_和_is成员变量,kvc机制会继续搜索和is的成员变量,再给它赋值

5.如果以上列出的方法和成员变量都不存在,系统将执行setValue:forUnderfindKey:方法,默认是抛出异常.

十.Notification和KVO的区别

1.KVO提供一种机制,当指定的被观察对象属性被修改后,kvo会自动通知响应的观察者

2.通知:是一种广播机制,在事件发生的时候,通过通知中心对象,一个对象能够为所关心这个事件发生的对象发送消息,两者都是观察者模式,不同在于kvo是被观察者直接发送消息给观察者,是对象间的直接交互.通知则是两者都和通知中心对象交互,对象之间不知道彼此

3.本质区别,底层原理不一样,kvo基于runtime,通知则有个通知中心来进行通知

十一.如果让你设计一个通知中心,设计思路

1.创建通知中心单例类,并在里面有一个保存全局NSDictionary

2.对于注册通知的类,将注册通知名作为key,执行的方法和类,以及一些参数作为一个数组为值

3.发送通知可以调用通知中心,通过字典key(通知名)找到对应的类和方法进行调用传值

十二.atomic和nonatomic区别,以及作用?

atomic与nonatom的主要区别就是系统自动生成的getter/setter方法不一样 ,atomic系统自动生成的getter/setter方法会进行加锁操作,nonatomic系统自动生成的getter/setter方法不会进行加锁操作

atomic不是线程安全的

系统生成的getter/setter方法会进行加锁操作,注意:这个锁仅仅保证了getter和setter存取方法的线程安全.因为getter/setter方法有加锁的缘故,故在别的线程来读写这个属性之前,会先执行完当前操作

atomic可以保证多线程访问时候,对象是未被其他线程销毁(比如:如果当一个线程正在get或set时,又有另一个线程同时在进行release操作,可能会直接crash)

十三.说一下静态库和动态库之间的区别

静态库:以.a 和 .framework为文件后缀名。链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。

动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存 

静态库.a 和 framework区别.a 主要是二进制文件,不包含资源,需要自己添加头文件.framework 可以包含头文件+资源信息

十四.遇到过BAD_ACCESS的错误吗?你是怎样调试的?

BAD_ACCESS 报错属于内存访问错误,会导致程序崩溃,错误的原因是访问了野指针(悬挂指针)。

常规操作如下:

设置全局断点快速定位问题代码所在行。

开启僵尸对象诊断
Analyze分析
重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object。
Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer

十五.说一下iOS 中的APNS,远程推送原理?

Apple push Notification Service,简称 APNS,是苹果的远程消息推送,原理如下:
iOS 系统向APNS服务器请求手机端的deviceToken
App 接收到手机端的 deviceToken,然后传给 App 对应的服务器.
App 服务端需要发送推送消息时, 需要先通过 APNS 服务器
然后根据对应的 deviceToken 发送给对应的手机

十六.UITableView的优化

1.重用cell

2.缓存行高(在请求到数据的时候提前计算好行高,用字典缓存好高度)

3.加载网络图片,使用异步加载,并缓存,下载的图片根据显示大小切成合适大小的图,查看大图时再显示大图,服务端最好处理好大图和小图,延时加载,当滚动很快时避免频繁请求,可通过runloop设置defultMode状态下渲染请求

4.局部刷新,减少全局刷新

5.渲染,尽量不要使用透明图层,将cell的opaque值设为Yes,背景色和子View不要使用透明色,减少阴影渐变,圆角等

6.少用addSubview给cell动态添加子View,初始化时直接设置好,通过hidden控制显示隐藏,布局在初始化直接布局好,避免cell的重新布局

7.按需加载cell,滚动很快时,只加载范围内的cell,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后定制n行加载,按需加载,提高流畅性方法如下


8.遇到复杂界面,需要异步绘制,给自定义的cell添加draw方法,在方法中利用GCD异步绘制,或者直接重写drawRect方法,此外,绘制cell不建议使用UIView,建议使用CALayer,UIView的绘制是建立在CoreGraphic上的,使用的是cpu,CALayer使用的是core Animation,CPU.GPU通吃.由系统决定使用哪一个,view的绘制使用的自下向上的一层层的绘制,然后渲染layer处理的是Texure,利用GPU的Texure Cache和独立的浮点数计算单元加速纹理的处理,GPU不喜欢透明,所以绘图一定要弄成不透明,对于圆角和阴影截一个伪透明的小图绘制上去,在layer回调里一定只做绘图,不做计算

cell被重用时,内部绘制的内容并不会自动清除,因此需要调用setNeedsDisplay或者setNeedsDisplayLayInRect:方法

十七.离屏渲染

下面的情况或操作会引发离屏渲染:

1.为图层设置遮罩(layer.mask)
2.将图层的layer.masksToBounds/view.clipsToBounds属性设置为ture
3.将图层layer,allowsGroupOpacity属性设置为Yes和layer.opacity小于1.0
4.给图层设置阴影(layer.shadow)
5.为图层设置layer.shouldRasterize=Yes(光栅化)
6.具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层(圆角,抗锯齿)
7.使用CGContext在drawRect:方法中绘制大部分情况会导致离屏渲染,甚至是一个空的实现

优化方案

圆角优化:使用CAShapeLayer和UIBezierPath设置圆角;直接中间覆盖一张为圆形的透明图片
shadow优化:使用shadowPath指定layer阴影效果路径,优化性能
使用异步进行layer渲染(Facebook开源异步绘制框架AsncDisplayKit)
设置layer的opaque值为Yes,减少复杂图层合成
尽量使用不包含透明(alpha)通道的图片资源
尽量设置layer的大小为整型值

十八.UIView和CALayer区别

1.UIView可以响应事件,CALayer不可以,UIView继承自UIResponder,在UIResponder中定义了处理各种事件的事件传递接口。而CALayer直接继承NSObject,并没有相应的处理事件接口。
2.一个CALayer的frame是由它的anchorPoint(锚点),position,bounds,和transform共同决定,而一个view的frame只是简单的返回layer的frame,同样view的center和bounds也是返回layer的一些属性
3..UIView主要是对显示内容的管理,而CALayer主要是侧重显示内容的绘制。UIView是CALayer的CALayerDelegate。
4.每一个view内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的CALayer提供,两者都有树状层级结构,layer内部有subLayers,view内部有subviews。
5.两者最明显的区别是view可以接受并处理事件,而Layer不可以。View是Layer的代理Delegate。

十九.iOS应用程序生命周期

ios程序启动原理(过程):如下:


二十.view视图生命周期



























收起阅读 »

协程+Retrofit 让你的代码足够优雅

目标 简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。 如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。 一、引入依赖 协程和 Retrofit 的版本:dependen...
继续阅读 »

目标


简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。


如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。


一、引入依赖


协程和 Retrofit 的版本:

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.0.0'
}

二、使用Retrofit


创建一个 interface

interface GithubApi {
@GET("search/repositories?sort=stars")
suspend fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): RepoSearchResponse
}

和我们平时使用 Retrofit 有两点不一样:



  1. 需要在函数前加上 suspend 修饰符

  2. 直接使用我们需要返回的类型,不需要包上 Call<T> 或者 Observable<T>


RepoSearchResponse 是返回的数据:

data class RepoSearchResponse(
@SerializedName("total_count") val total: Int = 0,
@SerializedName("items") val items: List<Repo> = emptyList()
)

data class Repo(
@SerializedName("id") val id: Long,
@SerializedName("name") val name: String,
@SerializedName("full_name") val fullName: String,
@SerializedName("description") val description: String?,
@SerializedName("html_url") val url: String,
@SerializedName("stargazers_count") val stars: Int,
@SerializedName("forks_count") val forks: Int,
@SerializedName("language") val language: String?
)

之后的步骤就和我们平常使用 Retrofit 一致:



  1. 创建一个 OkHttpClient

  2. 创建一个 Retrofit

  3. 返回上面创建的接口


代码:

interface GithubApi {
//... 代码省略

companion object {
private const val BASE_URL = "https://api.github.com/"

fun createGithubApi(): GithubApi {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC

val client = OkHttpClient.Builder()
.addInterceptor(logger)
.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(),
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String
) {}
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String
) {}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
})
.hostnameVerifier(SSLSocketClient.getHostnameVerifier())
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GithubApi::class.java)
}
}
}

因为接口是 Https 请求,所以需要加上忽略 SSL 的验证,其他都一样了。


三、使用协程去请求


初始化 RecyclerView 的代码就不放了,比较简单:

class MainActivity : AppCompatActivity() {
val scope = MainScope()
private val mAdapter by lazy {
MainAdapter()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// ... 初始化RecyclerView
fetchData()
}

private fun fetchData(){
scope.launch {
try {
val result = GithubApi.createGithubApi().searchRepos("Android", 0, 20)
if(result != null && !result.items.isNullOrEmpty()){
mAdapter.submitList(result.items)
}
}catch (e: Exception){
e.printStackTrace()
}
}
}

override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}

协程中最方便的还是省去切线程的步骤,用同步代码处理耗时的异步网络请求


需要注意的是,由于没有使用 LifecycleKTX 的扩展库,所以协程作用域的生命周期得我们自己去释放,在上面的代码中,我是在 onCreate 方法中启动了一个协程,然后在 onDestroy 方法中去取消了正在执行的任务,以防内存泄漏。


总结


协程 + Retrofit 的方便之处在于:使用同步代码处理异步的网络请求,减去 Retrofit 中网络回调或者 RxJava + Retrofit 的请求回调


作者:九心_
链接:https://www.jianshu.com/p/dd3a9323b81a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Jetpack新成员,Paging3从吐槽到真香

各位小伙伴们大家早上好。 随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。 现在,关于App Start和Hilt的文章我都...
继续阅读 »

各位小伙伴们大家早上好。


随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。


现在,关于App Start和Hilt的文章我都已经写完了,请参考 Jetpack新成员,App Startup一篇就懂 和 Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 。


那么本篇文章,我们要学习的自然就是Paging 3了。


Paging 3简介


Paging是Google推出的一个应用于Android平台的分页加载库。


事实上,Paging并不是现在才刚刚推出的,而是之前就已经推出过两个版本了。


但Paging 3和前面两个版本的变化非常大,甚至可以说是完全不同的东西了。所以即使你之前没有学习过Paging的用法也没有关系,把Paging 3当成是一个全新的库去学习就可以了。


我相信一定会有很多朋友在学习Paging 3的时候会产生和我相同的想法:本身Android上的分页功能并不难实现,即使没有Paging库我们也完全做得出来,但为什么Paging 3要把一个本来还算简单的功能设计得如此复杂呢?


是的,Paging 3很复杂,至少在你还不了解它的情况下就是如此。我在第一次学习Paging 3的时候就直接被劝退了,心想着何必用这玩意委屈自己呢,自己写分页功能又不是做不出来。


后来本着拥抱新技术的态度,我又去学习了一次Paging 3,这次算是把它基本掌握了,并且还在我的新开源项目 Glance 当中应用了Paging 3的技术。


如果现在再让我来评价一下Paging 3,那么我大概是经历了一个由吐槽到真香的过程。理解了Paging 3之后,你会发现它提供了一套非常合理的分页架构,我们只需要按照它提供的架构去编写业务逻辑,就可以轻松实现分页功能。我希望大家在看完这篇文章之后,也能觉得Paging 3香起来。


不过,本篇文章我不能保证它的易懂性。虽然很多朋友都觉得我写的文章简单易懂,但Paging 3的复杂性在于它关联了太多其他的知识,如协程、Flow、MVVM、RecyclerView、DiffUtil等等,如果你不能将相关联的这些知识都有所了解,那么想要掌握Paging 3就会更有难度。


另外,由于Paging 3是Google基于Kotlin协程全新重写的一个库,所以它主要是应用于Kotlin语言(Java也能用,但是会更加复杂),并且以后这样的库会越来越多,比如Jetpack Compose等等。如果你对于Kotlin还不太了解的话,可以去参郭霖的新书《第一行代码 Android 第3版》。


上手Paging 3


经过我自己的总结,我发现如果零散去介绍一些Paging 3的知识点是很难能掌握得了这个库的。最好的学习方式就是直接上手,用Paging 3去做一个项目,项目做完了,你也基本就掌握了。本篇文章中我们就会采用这种方式来学习。


另外,我相信大家之前应该都做过分页功能,正如我所说,这个功能并不难实现。但是现在,请你完全忘掉过去你所熟知的分页方案,因为它不仅对理解Paging 3没有帮助,反而在很大程度上会影响你对Paging 3的理解。


是的,不要想着去监听列表滑动事件,滑动到底部的时候发起一个网络请求加载下一页数据。Paging 3完全不是这么用的,如果你还保留着这种过去的实现思路,在学习Paging 3的时候会很受阻。


那么现在就让我们开始吧。


首先新建一个Android项目,这里我给它起名为Paging3Sample。


接下来,我们在build.gradle的dependencies当中添加必要的依赖库:

dependencies {
...
implementation 'androidx.paging:paging-runtime:3.0.0-beta01'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

注意虽然我刚才说,Paging 3是要和很多其他关联库结合到一起工作的,但是我们并不需要将这些关联库一一手动引入,引入了Paging 3之后,所有的关联库都会被自动下载下来。


另外这里还引入了Retrofit的库,因为待会我们会从网络上请求数据,并通过Paging 3进行分页展示。


那么在正式开始涉及Paging 3的用法之前,让我们先来把网络相关的代码搭建好,方便为Paging 3提供分页数据。


这里我准备采用GitHub的公开API来作为我们这个项目的数据源,请注意GitHub在国内虽然一般都是可以访问的,但有时接口并不稳定,如果你无法正常请求到数据的话,请自行科学上网。


我们可以尝试在浏览器中请求如下接口地址:

https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1

这个接口表示,会返回GitHub上所有Android相关的开源库,以Star数量排序,每页返回5条数据,当前请求的是第一页。


服务器响应的数据如下,为了方便阅读,我对响应数据进行了简化:

{
"items": [
{
"id": 31792824,
"name": "flutter",
"description": "Flutter makes it easy and fast to build beautiful apps for mobile and beyond.",
"stargazers_count": 112819,
},
{
"id": 14098069,
"name": "free-programming-books-zh_CN",
"description": ":books: 免费的计算机编程类中文书籍,欢迎投稿",
"stargazers_count": 76056,
},
{
"id": 111583593,
"name": "scrcpy",
"description": "Display and control your Android device",
"stargazers_count": 44713,
},
{
"id": 12256376,
"name": "ionic-framework",
"description": "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.",
"stargazers_count": 43041,
},
{
"id": 55076063,
"name": "Awesome-Hacking",
"description": "A collection of various awesome lists for hackers, pentesters and security researchers",
"stargazers_count": 42876,
}
]
}

简化后的数据格式还是非常好理解的,items数组中记录了第一页包含了哪些库,其中name表示该库的名字,description表示该库的描述,stargazers_count表示该库的Star数量。


那么下面我们就根据这个接口来编写网络相关的代码吧,由于这部分都是属于Retrofit的用法,我会介绍的比较简略。


首先根据服务器响应的Json格式定义对应的实体类,新建一个Repo类,代码如下所示:

data class Repo(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("description") val description: String?,
@SerializedName("stargazers_count") val starCount: Int
)

然后定义一个RepoResponse类,以集合的形式包裹Repo类:

class RepoResponse(
@SerializedName("items") val items: List<Repo> = emptyList()
)

接下来定义一个GitHubService用于提供网络请求接口,如下所示:

interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse

companion object {
private const val BASE_URL = "https://api.github.com/"

fun create(): GitHubService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GitHubService::class.java)
}
}

}

这些都是Retrofit的标准用法,现在当调用searchRepos()函数时,Retrofit就会自动帮我们向GitHub的服务器接口发起一条网络请求,并将响应的数据解析到RepoResponse对象当中。


好了,现在网络相关的代码都已经准备好了,下面我们就开始使用Paging 3来实现分页加载功能。


Paging 3有几个非常关键的核心组件,我们需要分别在这几个核心组件中按部就班地实现分页逻辑。


首先最重要的组件就是PagingSource,我们需要自定义一个子类去继承PagingSource,然后重写load()函数,并在这里提供对应当前页数的数据。


新建一个RepoPagingSource继承自PagingSource,代码如下所示:

class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
return try {
val page = params.key ?: 1 // set page 1 as default
val pageSize = params.loadSize
val repoResponse = gitHubService.searchRepos(page, pageSize)
val repoItems = repoResponse.items
val prevKey = if (page > 1) page - 1 else null
val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
LoadResult.Page(repoItems, prevKey, nextKey)
} catch (e: Exception) {
LoadResult.Error(e)
}
}

override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null

}

这段代码并不长,但却需要好好解释一下。


在继承PagingSource时需要声明两个泛型类型,第一个类型表示页数的数据类型,我们没有特殊需求,所以直接用整型就可以了。第二个类型表示每一项数据(注意不是每一页)所对应的对象类型,这里使用刚才定义的Repo。


然后在load()函数当中,先通过params参数得到key,这个key就是代表着当前的页数。注意key是可能为null的,如果为null的话,我们就默认将当前页数设置为第一页。另外还可以通过params参数得到loadSize,表示每一页包含多少条数据,这个数据的大小我们可以在稍后设置。


接下来调用刚才在GitHubService中定义的searchRepos()接口,并把page和pageSize传入,从服务器获取当前页所对应的数据。


最后需要调用LoadResult.Page()函数,构建一个LoadResult对象并返回。注意LoadResult.Page()函数接收3个参数,第一个参数传入从响应数据解析出来的Repo列表即可,第二和第三个参数分别对应着上一页和下一页的页数。针对于上一页和下一页,我们还额外做了个判断,如果当前页已经是第一页或最后一页,那么它的上一页或下一页就为null。


这样load()函数的作用就已经解释完了,可能你会发现,上述代码还重写了一个getRefreshKey()函数。这个函数是Paging 3.0.0-beta01版本新增的,以前的alpha版中并没有。它是属于Paging 3比较高级的用法,我们本篇文章涉及不到,所以直接返回null就可以了。


PagingSource相关的逻辑编写完成之后,接下来需要创建一个Repository类。这是MVVM架构的一个重要组件,还不了解的朋友可以去参考《第一行代码 Android 第3版》第15章的内容。

object Repository {

private const val PAGE_SIZE = 50

private val gitHubService = GitHubService.create()

fun getPagingData(): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(PAGE_SIZE),
pagingSourceFactory = { RepoPagingSource(gitHubService) }
).flow
}

}

这段代码虽然很短,但是却不易理解,因为用到了协程的Flow。我无法在这里展开解释Flow是什么,你可以简单将它理解成协程中对标RxJava的一项技术。


当然这里也没有用到什么复杂的Flow技术,正如你所见,上面的代码很简短,相比于理解,这更多是一种固定的写法。


我们定义了一个getPagingData()函数,这个函数的返回值是Flow<PagingData<Repo>>,注意除了Repo部分是可以改的,其他部分都是固定的。


在getPagingData()函数当中,这里创建了一个Pager对象,并调用.flow将它转换成一个Flow对象。在创建Pager对象的时候,我们指定了PAGE_SIZE,也就是每页所包含的数据量。又指定了pagingSourceFactory,并将我们自定义的RepoPagingSource传入,这样Paging 3就会用它来作为用于分页的数据源了。


将Repository编写完成之后,我们还需要再定义一个ViewModel,因为Activity是不可以直接和Repository交互的,要借助ViewModel才可以。新建一个MainViewModel类,代码如下所示:

class MainViewModel : ViewModel() {

fun getPagingData(): Flow<PagingData<Repo>> {
return Repository.getPagingData().cachedIn(viewModelScope)
}

}

代码很简单,就是调用了Repository中定义的getPagingData()函数而已。但是这里又额外调用了一个cachedIn()函数,这是用于将服务器返回的数据在viewModelScope这个作用域内进行缓存,假如手机横竖屏发生了旋转导致Activity重新创建,Paging 3就可以直接读取缓存中的数据,而不用重新发起网络请求了。


写到这里,我们的这个项目已经完成了一大半了,接下来开始进行界面展示相关的工作。


由于Paging 3是必须和RecyclerView结合使用的,下面我们定义一个RecyclerView的子项布局。新建repo_item.xml,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:orientation="vertical">

<TextView
android:id="@+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#5194fd"
android:textSize="20sp"
android:textStyle="bold" />

<TextView
android:id="@+id/description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:maxLines="10"
android:ellipsize="end" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="end"
tools:ignore="UseCompoundDrawables">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="5dp"
android:src="@drawable/ic_star"
tools:ignore="ContentDescription" />

<TextView
android:id="@+id/star_count_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />

</LinearLayout>

</LinearLayout>

这个布局中使用到了一个图片资源,可以到本项目的源码中去获取,源码地址见文章最底部。


接下来定义RecyclerView的适配器,但是注意,这个适配器也比较特殊,必须继承自PagingDataAdapter,代码如下所示:

class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.ViewHolder>(COMPARATOR) {

companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem == newItem
}
}
}

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name_text)
val description: TextView = itemView.findViewById(R.id.description_text)
val starCount: TextView = itemView.findViewById(R.id.star_count_text)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val repo = getItem(position)
if (repo != null) {
holder.name.text = repo.name
holder.description.text = repo.description
holder.starCount.text = repo.starCount.toString()
}
}

}

相比于一个传统的RecyclerView Adapter,这里最特殊的地方就是要提供一个COMPARATOR。因为Paging 3在内部会使用DiffUtil来管理数据变化,所以这个COMPARATOR是必须的。如果你以前用过DiffUtil的话,对此应该不会陌生。


除此之外,我们并不需要传递数据源给到父类,因为数据源是由Paging 3在内部自己管理的。同时也不需要重写getItemCount()函数了,原因也是相同的,有多少条数据Paging 3自己就能够知道。


其他部分就和普通的RecyclerView Adapter没什么两样了,相信大家都能够看得明白。


接下来就差最后一步了,让我们把所有的一切都集成到Activity当中。


修改activity_main.xml布局,在里面定义一个RecyclerView和一个ProgressBar:

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

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

</FrameLayout>

然后修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {

private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }

private val repoAdapter = RepoAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = repoAdapter
lifecycleScope.launch {
viewModel.getPagingData().collect { pagingData ->
repoAdapter.submitData(pagingData)
}
}
repoAdapter.addLoadStateListener {
when (it.refresh) {
is LoadState.NotLoading -> {
progressBar.visibility = View.INVISIBLE
recyclerView.visibility = View.VISIBLE
}
is LoadState.Loading -> {
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.INVISIBLE
}
is LoadState.Error -> {
val state = it.refresh as LoadState.Error
progressBar.visibility = View.INVISIBLE
Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
}
}
}
}

}

这里最重要的一段代码就是调用了RepoAdapter的submitData()函数。这个函数是触发Paging 3分页功能的核心,调用这个函数之后,Paging 3就开始工作了。


submitData()接收一个PagingData参数,这个参数我们需要调用ViewModel中返回的Flow对象的collect()函数才能获取到,collect()函数有点类似于Rxjava中的subscribe()函数,总之就是订阅了之后,消息就会源源不断往这里传。


不过由于collect()函数是一个挂起函数,只有在协程作用域中才能调用它,因此这里又调用了lifecycleScope.launch()函数来启动一个协程。


其他地方应该就没什么需要解释的了,都是一些传统RecyclerView的用法,相信大家都能看得懂。


好了,这样我们就把整个项目完成了,在正式运行项目之前,别忘了在你的AndroidManifest.xml文件中添加网络权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.paging3sample">

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

</manifest>



可以看到,GitHub上Android相关的开源库已经成功显示出来了。并且你可以不断往下滑,Paging 3会自动加载更多的数据,仿佛让你永远也滑不到头一样。


如次一来,使用Paging 3来进行分页加载的效果也就成功完成了。


总结一下,相比于传统的分页实现方案,Paging 3将一些琐碎的细节进行了隐藏,比如你不需要监听列表的滑动事件,也不需要知道知道何时应该加载下一页的数据,这些都被Paging 3封装掉了。我们只需要按照Paging 3搭建好的框架去编写逻辑实现,告诉Paging 3如何去加载数据,其他的事情Paging 3都会帮我们自动完成。


在底部显示加载状态


根据Paging 3的设计,其实我们理论上是不应该在底部看到加载状态的。因为Paging 3会在列表还远没有滑动到底部的时候就提前加载更多的数据(这是默认属性,可配置),从而产生一种好像永远滑不到头的感觉。


然而凡事总有意外,比如说当前的网速不太好,虽然Paging 3会提前加载下一页的数据,但是当滑动到列表底部的时候,服务器响应的数据可能还没有返回,这个时候就应该在底部显示一个正在加载的状态。


另外,如果网络条件非常糟糕,还可能会出现加载失败的情况,此时应该在列表底部显示一个重试按钮。


那么接下来我们就来实现这个功能,从而让项目变得更加完善。


创建一个footer_item.xml布局,用于显示加载进度条和重试按钮:

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

<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

<Button
android:id="@+id/retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Retry" />

</FrameLayout>

然后创建一个FooterAdapter来作为RecyclerView的底部适配器,注意它必须继承自LoadStateAdapter,如下所示:

class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)
val retryButton: Button = itemView.findViewById(R.id.retry_button)
}

override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_item, parent, false)
val holder = ViewHolder(view)
holder.retryButton.setOnClickListener {
retry()
}
return holder
}

override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
holder.progressBar.isVisible = loadState is LoadState.Loading
holder.retryButton.isVisible = loadState is LoadState.Error
}

}

这仍然是一个非常简单的Adapter,需要注意的地方大概只有两点。


第一点,我们使用Kotlin的高阶函数来给重试按钮注册点击事件,这样当点击重试按钮时,构造函数中传入的函数类型参数就会被回调,我们待会将在那里加入重试逻辑。


第二点,在onBindViewHolder()中会根据LoadState的状态来决定如何显示底部界面,如果是正在加载中那么就显示加载进度条,如果是加载失败那么就显示重试按钮。


最后,修改MainActivity中的代码,将FooterAdapter集成到RepoAdapter当中:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })
...
}

}

代码非常简单,只需要改动一行,调用RepoAdapter的withLoadStateFooter()函数即可将FooterAdapter集成到RepoAdapter当中。


另外注意这里使用Lambda表达式来作为传递给FooterAdapter的函数类型参数,在Lambda表示式中,调用RepoAdapter的retry()函数即可重新加载。


这样我们就把底部显示加载状态的功能完成了,现在来测试一下吧,效果如下图所示。




可以看到,首先我在设备上开启了飞行模式,这样当滑动到列表底部时就会显示重试按钮。


然后把飞行模式关闭,并点击重试按钮,这样加载进度条就会显示出来,并且成功加载出新的数据了。


最后


本文到这里就结束了。


不得不说,我在文章中讲解的这些知识点仍然只是Paging 3的基本用法,还有许多高级用法文中并没有涵盖。当然,这些基本用法也是最最常用的用法,所以如果你并不打算成为Paging 3大师,掌握文中的这些知识点就已经足够应对日常的开发工作了。


如果你还想要进一步进阶学习Paging 3,可以参考Google官方的Codelab项目,地址是:

https://developer.android.com/codelabs/android-paging


我们刚才一起编写的Paging3Sample项目其实就是从Google官方的Codelab项目演化而来的,我根据自己的理解重写了这个项目并进行了一定的简化。直接学习原版项目,你将能学到更多的知识。


最后,如果你需要获取Paging3Sample项目的源码,请访问以下地址:

https://github.com/guolindev/Paging3Sample

作者:飞鱼_9d08
链接:https://www.jianshu.com/p/588562fbd19d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 常见的错误日志及相应的解决方案总结

之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步) android.view.WindowMana...
继续阅读 »

之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步)


android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

at android.view.ViewRootImpl.setView(ViewRootImpl.java:635)
at android.view.ColorViewRootImpl.setView(ColorViewRootImpl.java:60)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:321)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:86)
at android.widget.PopupWindow.invokePopup(PopupWindow.java:1262)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1110)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1069)

以上bug出现的原因是因为PopupWindow需要依附在一个创建好的Activity上,那么出现这个异常就说明此时你的Activity还没有创建好,出现这种情况,很可能是在onCreate()或者是onStart()中调用导致的。

下面有两种方法可以解决这个问题:



方法一:重载Activity的onWindowFocusChanged方法,然后在里面实现相应的逻辑如下:

public void onWindowFocusChanged(boolean hasFocus) {  
super.onWindowFocusChanged(hasFocus);
if(hasFocus) {
//执行PopupWindow相应的操作
}
}

下面给大家看下这个方法的源码,有兴趣的小伙伴可以看看

 /**
* Called when the current {@link Window} of the activity gains or loses
* focus. This is the best indicator of whether this activity is visible
* to the user. The default implementation clears the key tracking
* state, so should always be called.
*
* <p>Note that this provides information about global focus state, which
* is managed independently of activity lifecycles. As such, while focus
* changes will generally have some relation to lifecycle changes (an
* activity that is stopped will not generally get window focus), you
* should not rely on any particular order between the callbacks here and
* those in the other lifecycle methods such as {@link #onResume}.
*
* <p>As a general rule, however, a resumed activity will have window
* focus... unless it has displayed other dialogs or popups that take
* input focus, in which case the activity itself will not have focus
* when the other windows have it. Likewise, the system may display
* system-level windows (such as the status bar notification panel or
* a system alert) which will temporarily take window input focus without
* pausing the foreground activity.
*
* @param hasFocus Whether the window of this activity has focus.
*
* @see #hasWindowFocus()
* @see #onResume
* @see View#onWindowFocusChanged(boolean)
*/
public void onWindowFocusChanged(boolean hasFocus) {
}


方法二:上面的那种方法是需要实现Activity的一个方法并在方法中做操作,一般我们在项目中会在一些逻辑里面showPopupWindow或者其他的,那这样就会影响一些,然后我们就针对这个源码,追溯一下会发现另外一个方法:hasWindowFocus

/**
* Returns true if this activity's <em>main</em> window currently has window focus.
* Note that this is not the same as the view itself having focus.
*
* @return True if this activity's main window currently has window focus.
*
* @see #onWindowAttributesChanged(android.view.WindowManager.LayoutParams)
*/
public boolean hasWindowFocus() {
Window w = getWindow();
if (w != null) {
View d = w.getDecorView();
if (d != null) {
return d.hasWindowFocus();
}
}
return false;
}

查看上面的源码,我们会发现,我们可以直接使用hasWindowFocus来判断当前的Activity有没有创建好,再去做其他操作;以上就是这个错误日志相应的解决方案,如果还有其他的希望大家补充。


java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity


这个问题是在使用Glide的时候生产上面爆出来的,如果遇到其他相似的错误也可以试一下,以下有两种解决方案:



方法一:参考博文

在使用Glide的地方加上这个判断;Util是系统自带的;

if(Util.isOnMainThread()) {
Glide.with(AppUtil.getContext()).load``(R.mipmap.iclunch).error(R.mipmap.cuowu).into(imageView);
}

在使用的Glide的界面的生命周期onDestroy中添加如下代码:

@Override
protected void onDestroy() {
super.onDestroy();
if(Util.isOnMainThread()) {
Glide.with(this).pauseRequests();
}
}

上面Destroy中with(this),改成with(AppUtil.getContext());

不然会报: java.lang.IllegalStateException: Activity has been destroyed

扩展:

Glide.with(AppUtil.getContext()).resumeRequests()和 Glide.with(AppUtil.getContext()).pauseRequests()的区别:

1.当列表在滑动的时候,调用pauseRequests()取消请求;

2.滑动停止时,调用resumeRequests()恢复请求;

另外Glide.clear():当你想清除掉所有的图片加载请求时,这个方法可以用到。

ListPreloader:如果你想让列表预加载的话,可以试试这个类。

请记住一句话:不要再非主线程里面使用Glide加载图片,如果真的使用了,请把context参数换成getApplicationContext;



方法二:使用Activity提供的isFinishing和isDestroyed方法,来判断当前的Activity是不是已经销毁了或者说正在finishing,下面贴出来相应的源码:

/**
* Check to see whether this activity is in the process of finishing,
* either because you called {@link #finish} on it or someone else
* has requested that it finished. This is often used in
* {@link #onPause} to determine whether the activity is simply pausing or
* completely finishing.
*
* @return If the activity is finishing, returns true; else returns false.
*
* @see #finish
*/
public boolean isFinishing() {
return mFinished;
}

/**
* Returns true if the final {@link #onDestroy()} call has been made
* on the Activity, so this instance is now dead.
*/
public boolean isDestroyed() {
return mDestroyed;
}

附上项目相应的源码,希望有所帮助:

final WeakReference<ImageView> imgBankLogoWeakReference = new WeakReference<>(imgBankLogo);
final WeakReference<ImageView> imgBankBgWeakReference = new WeakReference<>(imgBankBg);
ImageView imgBankLogoTarget = imgBankLogoWeakReference.get();
ImageView imgBankBgTarget = imgBankBgWeakReference.get();
if (imgBankLogoTarget != null && imgBankBgTarget != null) {
if (!(isFinishing() || isDestroyed())) {
Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankLogo())
.centerCrop().into(imgBankLogoTarget);
Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankBg())
.centerCrop().into(imgBankBgTarget);
}
}

java.lang.IllegalStateException: Could not find a method OnButtonClick(View) in the activity class android.view.ContextThemeWrapper for onClick handler on view class android.widget.ImageView with id 'img_apply_result'
at android.view.View$1.onClick(View.java:4061)
at android.view.View.performClick(View.java:4848)
at android.view.View$PerformClick.run(View.java:20262)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5714)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:984)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
Caused by: java.lang.NoSuchMethodException: OnButtonClick [class android.view.View]
at java.lang.Class.getMethod(Class.java:664)
at java.lang.Class.getMethod(Class.java:643)
at android.view.View$1.onClick(View.java:4054)


以上的错误日志出现的有点low,但是呢有时候部分人还是容易忽略:我们一般在Activity或fragment等Java代码中使用资源文件时,例如:在Java代码中对一个Imageview附一张图片,我们不能img.setImageResource(图片相应的资源ID);需要img.setImageResource(context.getResources().getDrawable(图片相应的资源ID));需要先获取文件资源,再去拿图片,但是刚刚写的那个方法现在已经过时了,下面我贴出Google官方给出的最新的方法img.setImageDrawable(ContextCompat.getDrawable(context, imgResId));其实setImageDrawable是最省内存高效的,如果担心图片过大或者图片过多影响内存和加载效率,可以自己解析图片然后通过调用setImageDrawable方法进行设置


java.lang.NoClassDefFoundError: android.app.AppOpsManager


Appops是Application Operations的简称,是关于应用权限管理的一套方案,但这里的应用指的是系统应用,这些API不对第三方应用开放。Appops的两个重要组成部分是AppOpsManager和AppOpsService,它们是典型的客户端和服务端设计,通过Binder跨进程调用。AppOpsManager提供标准的API供APP调用,但google有明确说明,大部分只针对系统应用。AppOpsService是做最终检查的系统服务,它的注册名字是appops, 应用可以类似于

mAppOps=(AppOpsManager)getContext().getSystemService(Context.APP_OPS_SERVICE);的方式来获取这个服务。


解决方法:这个api是在19新加入的,所以要注意加个判断,参考项目代码如下:



判断是否开启通知权限(解释比较好的博客推荐

private boolean isNotificationEnabled(Context context) {

String CHECK_OP_NO_THROW = "checkOpNoThrow";
String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
if (Build.VERSION.SDK_INT < 19) {
return true;
}
try {
AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
ApplicationInfo appInfo = context.getApplicationInfo();
String pkg = context.getApplicationContext().getPackageName();
int uid = appInfo.uid;
Class appOpsClass = null;
/* Context.APP_OPS_MANAGER */
appOpsClass = Class.forName(AppOpsManager.class.getName());
Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE,
String.class);
Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);

int value = (Integer) opPostNotificationValue.get(Integer.class);
return ((Integer) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED);

} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoClassDefFoundError e) {
e.printStackTrace();
}
return false;
}

java.lang.SecurityException: getDeviceId: Neither user 10185 nor current process has android.permission.READ_PHONE_STATE.


这里的getDeviceId可能是获取系统状态或内容的操作,需要授予android.permission.READ_PHONE_STATE 权限,首先我们来看一下危险权限组



我们会发现android.permission.READ_PHONE_STATE 这个权限在PHONE组里面,在Android M版本及以后,当你的应用运行在Android6.0系统上如果设置targetSdkVersion小于23的时候,它也会默认采用以前的权限管理机制,当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,它会采用新的这套权限管理机制。相关动态权限爬坑这块可以看一下之前的博文(传送门)

,当你配置了targetSdkVersion>=23时,默认第一次安装会打开android.permission.READ_PHONE_STATE这个权限,部分手机亲测,那样依旧可以获取getDeviceId,但这个权限是可见的,用户在后续是可以关闭的。当用户关闭了这个权限,下次进来会动态弹出授权页面提醒用户授权,如果用户依旧关闭权限将获取不到DeviceId。但是国产手机的各种自定义导致部分手机会出现动态权限返回0,(PS:当用户禁止了权限,返回回调还是为已授权,例如:OPPO meizu等兼容),这样就尴尬了,如果我们拿到用户已经授权(但实际上是禁止的)就去调用

TelephonyManager tm = (TelephonyManager) getApplicationContext()
.getSystemService(Context.TELEPHONY_SERVICE);
_clientInfo.setDeviceId(tm.getDeviceId());

就会闪退,目前这边处理的思路为:第一次如果拿到就放在SharedPreferences里面存起来,当下次用户再次关闭权限也不用担心报错;


java.util.ConcurrentModificationExceptionat java.util.ArrayList$ArrayListIterator.next(ArrayList.java:578)
at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:637)
at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:624)
at com.google.gson.JsonSerializationVisitor.findAndInvokeCustomSerializer(JsonSerializationVisitor.java:184)
at com.google.gson.JsonSerializationVisitor.visitUsingCustomHandler(JsonSerializationVisitor.java:160)
at com.google.gson.ObjectNavigator.accept(ObjectNavigator.java:101)
at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:62)
at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:53)
at com.google.gson.Gson.toJsonTree(Gson.java:220)
at com.google.gson.Gson.toJson(Gson.java:260)
at com.google.gson.Gson.toJson(Gson.java:240)



在ArrayList.addAll()中对传进来的参数没有做null判断,于是,在调用collection.toArray()函数的时候就抛异常了,activity就崩溃了


在使用ArrayList.addAll()的时候一定要注意传入的参数会不会出现为null的情况,如果有,那么我们可以做以下判断

if (collection!= null)
mInfoList.addAll(Collection<? extends E> collection);

如果为null,就不执行下面的了,我们也不能确保是不是存在null的情况,所以为了确保不会出错,在前面加个判断是一个有经验的程序员该做的。以上错误日志的原因,可以看下源码大家就可以理解了:(这个问题虽小但容易忽略,希望各位注意)

/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the
* specified collection's Iterator. The behavior of this operation is
* undefined if the specified collection is modified while the operation
* is in progress. (This implies that the behavior of this call is
* undefined if the specified collection is this list, and this
* list is nonempty.)
*
* @param c collection containing elements to be added to this list
* @return <tt>true</tt> if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

android.content.ActivityNotFoundException: Unable to find explicit activity class {com.android.browser/com.android.browser.BrowserActivity}; have you declared this activity in your AndroidManifest.xml?


以上错误日志出现的背景是调用系统自带的浏览器出现的,原因是因为部分手机设备商更改Android原生自带的com.android.browser/com.android.browser.BrowserActivity自己弄了一个其他的,例如,生产就出现一款手机 HTC 802t,这款手机自带浏览器的代码包名为:com.htc.sense.browser,看到这块是不是想吐槽一下,所以说如果直接写以下代码,就会出现以上错误日志:

 Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
intent.setData(contentUri);
intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));


解决方案(PS:获取系统安装的所有的浏览器应用 过滤):

Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
intent.setData(contentUri);
// HTC com.htc.sense.browser
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);//通过查询,获得所有ResolveInfo对象.
for (ResolveInfo resolveInfo : resolveInfos) {
browsers.add(resolveInfo.activityInfo.packageName);
System.out.println(resolveInfo.activityInfo.packageName);
}
if (browsers.contains("com.android.browser")) {
intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));
}
context.startActivity(intent);

android.os.FileUriExposedException/NullPointerException: Attempt to invoke virtual method 'java.lang.String android.net.Uri.getPath()' on a null object reference:


上述错误日志是Android 7.0应用间共享文件(FileProvider)兼容的问题,后续会出一篇博文来讲解:

下面提供代码:

/**
* 适配7.0及以上
*
* @param context
* @param file
* @return
*/
private static Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(context.getApplicationContext(), "xxx.fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}

AndroidManifest中配置provider:

<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.crfchina.market.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>

下面是很久之前的备忘的,也贴出来给大家分享一下。可能涉及到其他博文的内容,如有发现,麻烦私信,我后续加上 ……


android java.net.UnknownHostException: Unable to resolve host "...": No address associated 错误



解决方法:




  • (1)手机3G或者WIFI没有开启



  • (2).Manifest文件没有标明网络访问权限

    如果确认网络已经正常连接并且还是出这种错误的话,那么请看下你的Manifest文件是否标明应用需要网络访问权限,如果没标明的话,也访问不了网络,也会造成这种情况的.


    //网络访问权限


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




java.lang.NullPointerException: missing IConnectivityManager

at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:52)
at android.net.ConnectivityManager.<init>(ConnectivityManager.java:919)
at android.app.ContextImpl$11.createService(ContextImpl.java:387)
at android.app.ContextImpl$ServiceFetcher.getService(ContextImpl.java:278)
at android.app.ContextImpl.getSystemService(ContextImpl.java:1676)
at android.content.ContextWrapper.getSystemService(ContextWrapper.java:540)
at com.crfchina.market.util.NetUtil.getNetworkState(NetUtil.java:28)


错误日志产生原因:



Android里面内存泄漏问题最突出的就是Activity的泄漏,而泄漏的根源大多在于单例的使用,也就是一个静态实例持有了Activity的引用。静态变量的生命周期与应用(Application)是相同的,而Activity生命周期通常比它短,也就会造成在Activity生命周期结束后,还被引用导致无法被系统回收释放。

生成静态引用内存泄漏可能有两种情况:

1. 应用级:应用程序代码实现的单例没有很好的管理其生命周期,导致Activity退出后仍然被引用。
2. 系统级:Android系统级的实现的单例,被应用不小心错误调用(当然你也可以认为是系统层实现地不太友好)。

这个主要讲下系统级的情况,这样的情况可能也有很多,举个最近发现的问题ConnectivityManager。

通常我们获取系统服务时采用如下方式:

context.getSystemService()

在Android6.0系统上,如果这里的Context如果是Activity的实例,那么即使你什么也不干也会造成内存泄漏。

这个Context在ConnectivityManager 创建时传入,这个Context在StaticOuterContextServiceFetcher中由ContextImpl对象转换为OuterContext,与就是Activity对象,所以最终ConnectivityManager的单实例持有了Activity的实例引用。这样即使Activity退出后仍然无法释放,导致内存泄漏。

这个问题仅在6.0上出现,在5.1上ConnectivityManager实现为单例但不持有Context的引用,在5.0有以下版本ConnectivityManager既不为单例,也不持有Context的引用。

其他服务没认真研究,不确定有没有这个问题。不过为了避免类似的情况发生,

最好的解决办法就是:



解决方案:



获取系统服务getSystemService时使用ApplicationContext

context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);


java.lang.IllegalArgumentException: View not attached to window manager



错误日志产生原因:



在延时线程里调用了ProgressDialog.dismiss,但此时主Activity已经destroy了。于是应用崩溃,我写了一个 SafeProgressDialog 来避免这个问题,主要原理是覆写dismiss方法,在ProgressDialog.dismiss之前判断Activity是否存在。



解决方案:

class SafeProgressDialog extends ProgressDialog
{
Activity mParentActivity;
public SafeProgressDialog(Context context)
{
super(context);
mParentActivity = (Activity) context;
}

@Override
public void dismiss()
{
if (mParentActivity != null && !mParentActivity.isFinishing())
{
super.dismiss(); //调用超类对应方法
}
}
}

Android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.test.testFragment: make sure class name exists, is public, and has an empty constructor that is public



错误日志产生原因及解决方案:



根据报错提示 “make sure class name exists, is public, and has an empty constructor that is public” ,若Fragement定义有带参构造函数,则一定要定义public的默认的构造函数。即可解决此问题。

除了他说的public.还有一个就是弄个空的构造函数。

例如我是这样定义了我的fragment。带有了构造函数

public TestFragment(int index){
mIndex = index;
}

然后我添加了一个这样的构造函数

public TestFragment(){
}

java.lang.IllegalStateException: Unable to get package info for com.crfchina.market; is package not installed?



错误日志产生原因:



简单的卸载app 没有卸载干净,然后再次运行,当dalvik重新安装。apk文件并试图重用以前的活动从同一个包




好了目前就总结这么多,后续还会继续更新补充!毕竟太长也没有人愿意耐下心去看,以上也是曾经遇到过坑,希望有遇到的兄弟能从中受益!欢迎大家贴一些内容作为补充,相互学习共同进步……



作者:大荣言午
链接:https://www.jianshu.com/p/dd9714beb7ea
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

错过了金三银四,还不赶紧准备金九银十?这份Android大厂面试大纲静下心应对,九月就是你的战场!

感悟这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。而面试官也一样,他也可能只掌握了20%的知识,而且一个面...
继续阅读 »

感悟

这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。

而面试官也一样,他也可能只掌握了20%的知识,而且一个面试也不会有足够多的时间给你展示你全部的知识,而往往只会注意开发中最常遇到的20%。

这时候,你对这些问题理解的深度就显得尤为重要。回答了10个问题,而每个问题都只是浅显分析,还没有你将一个问题讲得透彻、全面更能让面试官加分。

当然这并不意味着当你要准备跳槽,要做面试准备的时候,你就只盯着几个自己感兴趣的课题,使劲背,使劲学,而其他的知识点就完全不学了。

想要面试的时候完胜面试官,最简便的,最稳妥的办法就是将一套完整系统的面试题全部刷完,然后再进行自我总结。

我知道有很多人最近都在为跳槽换工作面试做准备,所以在这里,我把我所收集到的面试大纲,分享给大家。

  • 阿里巴巴

  • LRUCache原理
  • 图片加载原理
  • 模块化实现(好处,原因)
  • JVM
  • 视频加密传输
  • 统计启动时长,标准
  • 如何保持应用的稳定性
  • ThreadLocal 原理
  • 谈谈classloader
  • 动态布局
  • 热修复,插件化
  • HashMap源码,SpareArray原理
  • 性能优化,怎么保证应用启动不卡顿
  • 怎么去除重复代码
  • SP是进程同步的吗?有什么方法做到同步
  • 介绍下SurfView
  • HashMap实现原理,ConcurrentHashMap 的实现原理
  • BroadcastReceiver,LocalBroadcastReceiver 区别
  • Bundle 机制
  • Handler 机制
  • android 事件传递机制
  • 线程间 操作 List
  • App启动流程,从点击桌面开始
  • 动态加载
  • 类加载器
  • OSGI
  • Https请求慢的解决办法,DNS,携带数据,直接访问IP
  • GC回收策略
  • 画出 Android 的大体架构图
  • 描述清点击 Android Studio 的 build 按钮后发生了什么
  • 大体说清一个应用程序安装到手机上时发生了什么;
  • 对 Dalvik、ART 虚拟机有基本的了解;
  • Android 上的 Inter-Process-Communication 跨进程通信时如何工作的;
  • App 是如何沙箱化,为什么要这么做;
  • 权限管理系统(底层的权限是如何进行 grant 的)
  • 进程和 Application 的生命周期;
  • 系统启动流程 Zygote进程 –> SystemServer进程 –> 各种系统服务 –> 应用进程
  • recycleview listview 的区别,性能
  • 排序,快速排序的实现
  • 树:B+树的介绍
  • 图:有向无环图的解释
  • TCP/UDP的区别
  • synchronized与Lock的区别
  • volatile
  • Java线程池
  • Java中对象的生命周期
  • 类加载机制
  • 双亲委派模型
  • Android事件分发机制
  • MVP模式
  • RxJava
  • 抽象类和接口的区别
  • 集合 Set实现 Hash 怎么防止碰撞
  • JVM 内存区域 开线程影响哪块内存
  • 垃圾收集机制 对象创建,新生代与老年代
  • 二叉树 深度遍历与广度遍历
  • B树、B+树
  • 消息机制
  • 进程调度
  • 进程与线程
  • 死锁
  • 进程状态
  • JVM内存模型
  • 并发集合了解哪些
  • ConCurrentHashMap实现
  • CAS介绍
  • 开启线程的三种方式,run()和start()方法区别
  • 线程池
  • 常用数据结构简介
  • 判断环(猜测应该是链表环)
  • 排序,堆排序实现
  • 链表反转

  • 腾讯

  • synchronized用法
  • volatile用法
  • 动态权限适配方案,权限组的概念
  • 网络请求缓存处理,okhttp如何处理网络缓存的
  • 图片加载库相关,bitmap如何处理大图,如一张30M的大图,如何预防OOM
  • 进程保活
  • listview图片加载错乱的原理和解决方案
  • https相关,如何验证证书的合法性,https中哪里用了对称加密,哪里用了非对称加密,对加密算法(如RSA)等是否有了解

  • 滴滴

  • MVP
  • 广播(动态注册和静态注册区别,有序广播和标准广播)
  • service生命周期
  • handler实现机制(很多细节需要关注:如线程如何建立和退出消息循环等等)
  • 多线程(关于AsyncTask缺陷引发的思考)
  • 数据库数据迁移问题
  • 设计模式相关(例如Android中哪里使用了观察者模式,单例模式相关)
  • x个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完
  • TCP与UDP区别与应用(三次握手和四次挥手)涉及到部分细节(如client如何确定自己发送的消息被server收到) HTTP相关 提到过Websocket 问了WebSocket相关以及与socket的区别
  • 是否熟悉Android jni开发,jni如何调用java层代码
  • 进程间通信的方式
  • java注解
  • 计算一个view的嵌套层级
  • 项目组件化的理解
  • 多线程断点续传原理
  • Android系统为什么会设计ContentProvider,进程共享和线程安全问题
  • jvm相关
  • Android相关优化(如内存优化、网络优化、布局优化、电量优化、业务优化)
  • EventBus实现原理

  • 美团

  • static synchronized 方法的多线程访问和作用,同一个类里面两个synchronized方法,两个线程同时访问的问题
  • 内部类和静态内部类和匿名内部类,以及项目中的应用
  • handler发消息给子线程,looper怎么启动
  • View事件传递
  • activity栈
  • 封装view的时候怎么知道view的大小
  • arraylist和linkedlist的区别,以及应用场景
  • 怎么启动service,service和activity怎么进行数据交互
  • 下拉状态栏是不是影响activity的生命周期,如果在onStop的时候做了网络请求,onResume的时候怎么恢复
  • view渲染

  • 今日头条

  • 数据结构中堆的概念,堆排序
  • 死锁的概念,怎么避免死锁
  • ReentrantLock 、synchronized和volatile(n面)
  • HashMap
  • singleTask启动模式
  • 用到的一些开源框架,介绍一个看过源码的,内部实现过程。
  • 消息机制实现
  • ReentrantLock的内部实现
  • App启动崩溃异常捕捉
  • 事件传递机制的介绍
  • ListView的优化
  • 二叉树,给出根节点和目标节点,找出从根节点到目标节点的路径
  • 模式MVP,MVC介绍
  • 断点续传的实现
  • 集合的接口和具体实现类,介绍
  • TreeMap具体实现
  • synchronized与ReentrantLock
  • 手写生产者/消费者模式
  • 逻辑地址与物理地址,为什么使用逻辑地址
  • 一个无序,不重复数组,输出N个元素,使得N个元素的和相加为M,给出时间复杂度、空间复杂度。手写算法
  • Android进程分类
  • 前台切换到后台,然后再回到前台,Activity生命周期回调方法。弹出Dialog,生命值周期回调方法。
  • Activity的启动模式

  • 爱奇艺

  • RxJava的功能与原理实现
  • RecycleView的使用,原理,RecycleView优化
  • ANR的原因
  • 四大组件
  • Service的开启方式
  • Activity与Service通信的方式
  • Activity之间的通信方式
  • HashMap的实现,与HashSet的区别
  • JVM内存模型,内存区域
  • Java中同步使用的关键字,死锁
  • MVP模式
  • Java设计模式,观察者模式
  • Activity与Fragment之间生命周期比较
  • 广播的使用场景

  • 百度

  • Bitmap 使用时候注意什么?
  • Oom 是否可以try catch ?
  • 内存泄露如何产生?
  • 适配器模式,装饰者模式,外观模式的异同?
  • ANR 如何产生?
  • String buffer 与string builder 的区别?
  • 如何保证线程安全?
  • java四中引用
  • Jni 用过么?
  • 多进程场景遇见过么?
  • 关于handler,在任何地方new handler 都是什么线程下
  • sqlite升级,增加字段的语句
  • bitmap recycler 相关
  • 强引用置为null,会不会被回收?
  • glide 使用什么缓存?
  • Glide 内存缓存如何控制大小?
  • 如何保证多线程读写文件的安全?

  • 携程

  • Activity启动模式
  • 广播的使用方式,场景
  • App中唤醒其他进程的实现方式
  • AndroidManifest的作用与理解
  • List,Set,Map的区别
  • HashSet与HashMap怎么判断集合元素重复
  • Java中内存区域与垃圾回收机制
  • EventBus作用,实现方式,代替EventBus的方式
  • Android中开启摄像头的主要步骤

  • 网易

  • 集合
  • concurrenthashmap
  • volatile
  • synchronized与Lock
  • Java线程池
  • wait/notify
  • NIO
  • 垃圾收集器
  • Activity生命周期
  • AlertDialog,popupWindow,Activity区别

  • 小米

  • String 为什么要设计成不可变的?
  • fragment 各种情况下的生命周期
  • Activity 上有 Dialog 的时候按 home 键时的生命周期
  • 横竖屏切换的时候,Activity 各种情况下的生命周期
  • Application 和 Activity 的 context 对象的区别
  • 序列化的作用,以及 Android 两种序列化的区别。
  • List 和 Map 的实现方式以及存储方式。
  • 静态内部类的设计意图。
  • 线程如何关闭,以及如何防止线程的内存泄漏

  • 360

  • 软引用、弱引用区别
  • 垃圾回收
  • 多线程:怎么用、有什么问题要注意;Android线程有没有上限,然后提到线程池的上限
  • JVM
  • OOM,内存泄漏
  • ANR怎么分析解决
  • LinearLayout、RelativeLayout、FrameLayout的特性、使用场景
  • 如何实现Fragment的滑动
  • ViewPager使用细节,如何设置成每次只初始化当前的Fragment,其他的不初始化
  • ListView重用的是什么
  • 进程间通信的机制
  • AIDL机制
  • AsyncTask机制
  • 如何取消AsyncTask
  • 序列化
  • Android为什么引入Parcelable
  • 有没有尝试简化Parcelable的使用
  • AIDL机制
  • 项目:拉活怎么做的
  • 应用安装过程
  • 某海外直播公司
  • 线程和进程的区别?
  • 为什么要有线程,而不是仅仅用进程?
  • 算法判断单链表成环与否?
  • 如何实现线程同步?
  • hashmap数据结构?
  • arraylist 与 linkedlist 异同?
  • object类的equal 和hashcode 方法重写,为什么?
  • hashmap如何put数据(从hashmap源码角度讲解)?
  • 简述IPC?
  • fragment之间传递数据的方式?
  • 简述tcp四次挥手?
  • threadlocal原理
  • 内存泄漏的可能原因?
  • 用IDE如何分析内存泄漏?
  • OOM的可能原因?
  • 线程死锁的4个条件?
  • 差值器&估值器
  • 简述消息机制相关
  • 进程间通信方式?
  • Binder相关?
  • 触摸事件的分发?
  • 简述Activity启动全部过程?
  • okhttp源码?
  • RxJava简介及其源码解读?
  • 性能优化如何分析systrace?
  • 广播的分类?
  • 点击事件被拦截,但是相传到下面的view,如何操作?
  • Glide源码?
  • ActicityThread相关?
  • volatile的原理
  • synchronize的原理
  • lock原理
  • 翻转一个单项链表
  • string to integer
  • 合并多个单有序链表(假设都是递增的)
  • 其他公司
  • 四大组件
  • Android中数据存储方式
  • 微信主页面的实现方式
  • 微信上消息小红点的原理
  • 两个不重复的数组集合中,求共同的元素。
  • 上一问扩展,海量数据,内存中放不下,怎么求出。
  • Java中String的了解。
  • ArrayList与LinkedList区别
  • 堆排序过程,时间复杂度,空间复杂度
  • 快速排序的时间复杂度,空间复杂度
  • RxJava的作用,与平时使用的异步操作来比,优势
  • Android消息机制原理
  • Binder机制介绍
  • 为什么不能在子线程更新UI
  • JVM内存模型
  • Android中进程内存的分配,能不能自己分配定额内存
  • 垃圾回收机制与调用System.gc()区别
  • Android事件分发机制
  • 断点续传的实现
  • RxJava的作用,优缺点

版权声明:本文为CSDN博主「Young-G2333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/YoungOne2333/article/details/115679291

收起阅读 »

Flutter踩坑:Android sdkmanager tool not found

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误: Android...
继续阅读 »

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误:



Android sdkmanager tool not found

(/Users/xx/android-sdk/tools/bin/sdkmanager).

Try re-installing or updating your Android SDK,

visit https://flutter.io/setup/#android-setup for detailed instructions.



解决步骤:

看字面意思问题应该是在“/Users/xx/android-sdk/tools/bin/sdkmanager”,但是我尝试了一下发现根本SDK文件夹下根本没有Tools文件夹
百度了一圈,网上给的解决方案,都是将emulator目录下的sdkmanager移动到 tools目录下,可是我根本就没有这个文件夹。
后来在Stack Overflow上找到了原因:Android Studio最新版本中,默认情况下是不会安装Android SDK Tools的,我的版本是3.6。



找到了原因就好解决了:




  • 在窗口左上角andriod studio-偏好设置中找到SDKTools






按图操作就好.png

继续在终端执行

flutter doctor --android-licenses (之后一路选Y就行了)






PS:VScode和AS都要记得装flutter插件,AS还要另外装dart插件


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

UITableViewCell嵌套WKWebView

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在找到了解决方案,废话不多说,直接看解决方案。正文1. 构建WKWebViewself.webView = [[WKWeb...
继续阅读 »
前言

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在stackoverflow找到了解决方案,废话不多说,直接看解决方案。

正文

1. 构建WKWebView

self.webView = [[WKWebView alloc] init];
// 创建请求
NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.jianshu.com"]];
// 加载网页
[self.webView loadRequest:request];

self.webView.scrollView.scrollEnabled = NO;
self.webView.scrollView.bounces = NO;
self.webView.scrollView.showsVerticalScrollIndicator = NO;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

// 将webView添加到界面
[self.contentView addSubview:self.webView];

2. cell高度适应WKWebView的内容

cell.webView.navigationDelegate = self;

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

[webView evaluateJavaScript:@"document.body.offsetHeight" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 计算webView高度
self.webViewCellHeight = [result doubleValue];
// 刷新tableView
[self.tableView reloadData];
}];
}

3. 解决加载空白问题
原因:由于WKWebView采用的lazy加载模式,所在的scrollView的滚动被禁用,导致被嵌套的WKCompositingView不进行数据加载。
详细细节请参考:WKWebView刷新机制小探

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 判断webView所在的cell是否可见,如果可见就layout
NSArray *cells = self.tableView.visibleCells;
for (UITableViewCell *cell in cells) {
if ([cell isKindOfClass:[TraitWebViewCell class]]) {
TraitWebViewCell *webCell = (TraitWebViewCell *)cell;

[webCell.webView setNeedsLayout];
}
}

}


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

Material Design实战之可折叠式标题栏

CollapsingToolbarLayout1.基本介绍CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果注意:Collapsin...
继续阅读 »

CollapsingToolbarLayout

1.基本介绍

CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果

注意:
CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout的子布局。。

2.具体使用

①我们需要一个具体展示水果详情的页面

在这里就新建一个FruitActivity。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FruitActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mCollapsingToolbarLayout"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/purple_500"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fruitImageView"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>



app:contentScrim用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色。
app:layout_scrollFlags的exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
里面的ImageView和Toolbar就是标题栏的具体内容。
app:layout_collapseMode表示设置折叠过程中的折叠样式。
然后我们在加一个NestedScrollView,它不仅允许使用滚动来查看屏幕以外的数据,而且还增加了嵌套响应滚动事件的功能。
布局文件全部代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FruitActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mCollapsingToolbarLayout"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/purple_500"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fruitImageView"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginTop="35dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
app:cardCornerRadius="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruitContentText"
android:layout_margin="10dp"
/>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>



很好理解,就不解释了

②编写功能逻辑

public class FruitActivity extends AppCompatActivity {
static String FRUIT_NAME = "fruit_name";
static String FRUIT_IMAGE_ID = "fruit_image_id";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fruit);
String fruitName = getIntent ().getStringExtra(FRUIT_NAME);
String fruitImageId = getIntent ().getStringExtra(FRUIT_IMAGE_ID);

Toolbar toolbar = findViewById (R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

CollapsingToolbarLayout collapsingToolbarLayout = findViewById (R.id.mCollapsingToolbarLayout);
collapsingToolbarLayout.setTitle(fruitName);
Glide.with(this).load(fruitImageId).into((ImageView) findViewById (R.id.fruitImageView));

TextView textView = findViewById (R.id.fruitContentText);
}
}


textView.setText("声卡的那句阿奎那飞机咔叽脑筋那就是可能安东尼上级领导那就ask的年纪ask" +
"打卡时间开机卡死的你课件撒的就看撒贷记卡十多年按实际困难贷记卡大卡司你可记得是" +
"多久啊是当年就卡死的你叫阿三的");

toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();

}
});



没啥难度,不说了

③在RecyclerView的适配器中增加点击事件

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.MyViewHolder> {
List<Fruit> fruits = new ArrayList<>();
Context context;
FruitAdapter(Context context){
this.context = context;
for (int i = 0; i < 30; i++) {
fruits.add(new Fruit("香蕉",R.mipmap.banana));
}
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
MyViewHolder holder = new MyViewHolder(view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit = fruits.get(position);
Intent intent = new Intent(context, FruitActivity.class);
intent.putExtra(FruitActivity.FRUIT_NAME,fruit.name);
intent.putExtra(FruitActivity.FRUIT_IMAGE_ID,fruit.imageId);
context.startActivity(intent);
}
});
return holder;
}
}


————————————————
版权声明:本文为CSDN博主「独饮敌敌畏丶」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/afdafvdaa/article/details/115583226

收起阅读 »

前端如何进行用户权限管理

【前端如何进行用户权限管理】1:问题:假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?2:要不要给老师和学生各自设计一...
继续阅读 »

【前端如何进行用户权限管理】

1:问题:
假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。


现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?

2:要不要给老师和学生各自设计一套页面?这样工作量是不是太大了,并且如果还要加入其它角色的话,难道每个角色对应一套代码?

所以我们需要用一套页面适应各种用户角色,并根据身份赋予他们不同权限

3:权限设计与管理是一个很复杂的问题,涉及的东西很多,相比前端,更偏向于后端,在搜集相关资料的过程中,发现掺杂了许多数据库之类的知识,以及几个用于权限管理的java框架,比如spring,比如shiro等等,都属于后端的工作

4:那我们前端能做什么呢?

权限的设计中比较常见的就是RBAC基于角色的访问控制,基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。

一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

在Angular构建的单页面应用中,要实现这样的架构我们需要额外多做一些事.从整体项目上来讲,大约有3处地方,前端工程师需要进行处理.

1. UI处理(根据用户拥有的权限,判断页面上的一些内容是否显示)

2. 路由处理(当用户访问一个它没有权限访问的url时,跳转到一个错误提示的页面)

3. HTTP请求处理(当我们发送一个数据请求,如果返回的status是401或者401,则通常重定向到一个错误提示的页面)

如何实现?
首先需要在Angular启动之前就获取到当前用户的所有的permissions,然后比较优雅的方式是通过一个service存放这个映射关系.对于UI处理一个页面上的内容是否根据权限进行显示,我们应该通过一个directive来实现.当处理完这些,我们还需要在添加一个路由时额外为其添加一个"permission"属性,并为其赋值表明拥有哪些权限的角色可以跳转这个URL,然后通过Angular监听routeChangeStart事件来进行当前用户是否拥有此URL访问权限的校验.最后还需要一个HTTP拦截器监控当一个请求返回的status是401或者403时,跳转页面到一个错误提示页面.

大致上的工作就是这些,看起来有些多,其实一个个来还是挺好处理的.

在Angular运行之前获取到permission的映射关系



Angular项目通过ng-app启动,但是一些情况下我们是希望Angular项目的启动在我们的控制之中.比如现在这种情况下,我就希望能获取到当前登录用户的所有permission映射关系后,再启动Angular的App.幸运的是Angular本身提供了这种方式,也就是angular.bootstrap().看的仔细的人可能会注意到,这里使用的是$.get(),没有错用的是jQuery而不是Angular的$resource或者$http,因为在这个时候Angular还没有启动,它的function我们还无法使用.

进一步使用上面的代码可以将获取到的映射关系放入一个service作为全局变量来使用.


在取得当前用户的权限集合后,我们将这个集合存档到对应的一个service中,然后又做了2件事:

(1) 将permissions存放到factory变量中,使之一直处于内存中,实现全局变量的作用,但却没有污染命名空间.

(2) 通过$broadcast广播事件,当权限发生变更的时候.

如何确定UI组件的依据权限进行显隐




这里我们需要自己编写一个directive,它会依据权限关系来进行显示或者隐藏元素.

这里看到了比较理想的情况是通关一个has-permission属性校验permission的name,如果当前用户有则显示,没有则隐藏.




扩展一下之前的factory:




路由上的依权限访问
这一部分的实现的思路是这样: 当我们定义一个路由的时候增加一个permission的属性,属性的值就是有哪些权限才能访问当前url.然后通过routeChangeStart事件一直监听url变化.每次变化url的时候,去校验当前要跳转的url是否符合条件,然后决定是跳转成功还是跳转到错误的提示页面.

router.js:






mainController.js 或者 indexController.js (总之是父层Controller)





这里依然用到了之前写的hasPermission,这些东西都是高度可复用的.这样就搞定了,在每次view的route跳转前,在父容器的Controller中判断一些它到底有没有跳转的权限即可.



HTTP请求处理
这个应该相对来说好处理一点,思想的思路也很简单.因为Angular应用推荐的是RESTful风格的接口,所以对于HTTP协议的使用很清晰.对于请求返回的status code如果是401或者403则表示没有权限,就跳转到对应的错误提示页面即可.





当然我们不可能每个请求都去手动校验转发一次,所以肯定需要一个总的filter.代码如下:

写到这里我们就基本实现了在这种前后端分离模式下,前端部分的权限管理和控制。

原文链接:https://blog.csdn.net/jnshu_it/article/details/77511588


收起阅读 »

Android图片轮播-banner

使用步骤以下提供的是最简单的步骤,需要复杂的样式自己可以自定义Step 1.依赖bannerGradledependencies{ compile 'com.youth.banner:banner:2.1.0' }Step 2.添加权限到你的 An...
继续阅读 »

使用步骤

以下提供的是最简单的步骤,需要复杂的样式自己可以自定义

Step 1.依赖banner

Gradle

dependencies{
compile 'com.youth.banner:banner:2.1.0'
}

Step 2.添加权限到你的 AndroidManifest.xml


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

Step 3.在布局文件中添加Banner,可以设置自定义属性

!!!此步骤可以省略,可以直接在Activity或者Fragment中new Banner();

<com.youth.banner.Banner
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="高度自己设置" />

Step 4.继承BannerAdapter,和RecyclerView的Adapter一样(如果你只是图片轮播也可以使用默认的)

!!!此步骤可以省略,图片轮播提供有默认适配器,其他的没有提供是因为大家的可变性要求不确定,所以直接自定义的比较好。

/**
* 自定义布局,下面是常见的图片样式,更多实现可以看demo,可以自己随意发挥
*/
public class ImageAdapter extends BannerAdapter<DataBean, ImageAdapter.BannerViewHolder> {

public ImageAdapter(List<DataBean> mDatas) {
//设置数据,也可以调用banner提供的方法,或者自己在adapter中实现
super(mDatas);
}

//创建ViewHolder,可以用viewType这个字段来区分不同的ViewHolder
@Override
public BannerViewHolder onCreateHolder(ViewGroup parent, int viewType) {
ImageView imageView = new ImageView(parent.getContext());
//注意,必须设置为match_parent,这个是viewpager2强制要求的
imageView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
return new BannerViewHolder(imageView);
}

@Override
public void onBindView(BannerViewHolder holder, DataBean data, int position, int size) {
holder.imageView.setImageResource(data.imageRes);
}

class BannerViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;

public BannerViewHolder(@NonNull ImageView view) {
super(view);
this.imageView = view;
}
}
}

Step 5.Banner具体方法调用

public class BannerActivity extends AppCompatActivity {
public void useBanner() {
//--------------------------简单使用-------------------------------
banner.addBannerLifecycleObserver(this)//添加生命周期观察者
.setAdapter(new BannerExampleAdapter(DataBean.getTestData()))
.setIndicator(new CircleIndicator(this));

//—————————————————————————如果你想偷懒,而又只是图片轮播————————————————————————
banner.setAdapter(new BannerImageAdapter<DataBean>(DataBean.getTestData3()) {
@Override
public void onBindView(BannerImageHolder holder, DataBean data, int position, int size) {
//图片加载自己实现
Glide.with(holder.itemView)
.load(data.imageUrl)
.apply(RequestOptions.bitmapTransform(new RoundedCorners(30)))
.into(holder.imageView);
}
})
.addBannerLifecycleObserver(this)//添加生命周期观察者
.setIndicator(new CircleIndicator(this));
//更多使用方法仔细阅读文档,或者查看demo
}
}

Banner使用中优化体验

如果你需要考虑更好的体验,可以看看下面的代码

Step 1.(可选)生命周期改变时

public class BannerActivity {

//方法一:自己控制banner的生命周期

@Override
protected void onStart() {
super.onStart();
//开始轮播
banner.start();
}

@Override
protected void onStop() {
super.onStop();
//停止轮播
banner.stop();
}

@Override
protected void onDestroy() {
super.onDestroy();
//销毁
banner.destroy();
}

//方法二:调用banner的addBannerLifecycleObserver()方法,让banner自己控制

protected void onCreate(Bundle savedInstanceState) {
//添加生命周期观察者
banner.addBannerLifecycleObserver(this);
}
}

常见问题(收录被反复询问的问题)

  • 网络图片加载不出来?

    banner本身不提供图片加载功能,首先确认banner本身使用是否正确,具体参考demo, 然后请检查你的图片加载框架或者网络请求框架,服务端也可能加了https安全认证,是看下是否报有证书相关错误

  • 怎么实现视频轮播?

    demo中有实现类似淘宝商品详情的效果,第一个放视频,后面的放的是图片,并且可以设置首尾不能滑动。 因为大家使用的播放器不一样业务环境也不同,具体情况自己把握,demo就是给一个思路哈!可以参考和修改

  • 我想指定轮播开始的位置?

    现在提供了setStartPosition()方法,在sheAdapter和setDatas直接调用一次就行了,当然setAdapter后通过setCurrentItem设置也行

  • 父控件滑动时,banner切换会获取焦点,然后自动全部显示。不想让banner获取焦点可以给父控件加上:

        //banner也一定要用最新版哦!
    android:focusable="true"
    android:focusableInTouchMode="true"


代码下载:banner-master.zip

原文链接:https://github.com/SenhLinsh/Android-Hot-Libraries

收起阅读 »

彻底解决小程序无法触发SESSION问题

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来wx.request({ ...... success: function(res) { console.log(res.header); //set-co...
继续阅读 »

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来

wx.request({
......
success: function(res) {
console.log(res.header);
//set-cookie:PHPSESSID=ic4vj84aaavqgb800k82etisu0; path=/; domain=.fengkui.net

// 登录成功,获取第一次的sessionid,存储起来
// 注意:Set-Cookie(开发者工具中调试全部小写)(远程调试和线上首字母大写)
wx.setStorageSync("sessionid", res.header["Set-Cookie"]);
}
})

二、请求时带上将sessionid放入request的header头中传到服务器,服务器端可直接在cookie中获取

wx.request({
......
header: {
'content-type': 'application/json', // 默认值
'cookie': wx.getStorageSync("sessionid")
//读取sessionid,当作cookie传入后台将PHPSESSID做session_id使用
},
success: function(res) {
console.log(res)
}
})

三、后台获取cookie中的PHPSESSID,将PHPSESSID当作session_id使用

<?php
// 判断$_COOKIE['PHPSESSID']是否存在,存在则作session_id使用
if ($_COOKIE['PHPSESSID']) {
session_id($_COOKIE['PHPSESSID']);
}

session_start();
echo session_id();


原文链接:https://blog.csdn.net/qq_41654694/article/details/85991846

收起阅读 »

iOS 一个OC对象在内存中的布局&&占用多少内存

一.先来看看我们平时接触的NSObject NSObject *objc = [[NSObject alloc]init]的本质 在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为: struct NSObject_I...
继续阅读 »

一.先来看看我们平时接触的NSObject



  • NSObject *objc = [[NSObject alloc]init]的本质

    在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为:


struct NSObject_IMPL {
Class isa;
};

在64位机中,一个isa占8个字节,在32位机中,一个isa占4个字节(当然苹果后面的机型都是64位的,这里我们着重讲解64位机)

  • 我们先来看看这个创建好的objc占多少个字节


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

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
//定义一个objc
NSObject *objc = [[NSObject alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([NSObject class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));
}

}




其打印结果为:



objc打印结果





  • 为什么一个是8一个是16
    • 我们先来认识一下class_getInstanceSize、malloc_size的区别

      1.class_getInstanceSize:是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)即实例对象的大小->结构体内存对齐

      2.创建的对象【至少】需要的内存大小不考虑malloc函数的话,内存对齐一般是以【8】对齐

      3.#import <objc/runtime.h>使用这个函数时倒入runtime运行时



    • malloc_size:堆空间【实际】分配给对象的内存大小 -系统内存对齐



      1. 在Mac、iOS中的malloc函数分配的内存大小总是【16】的倍数 即指针指向的内存大小

      2. import <malloc/malloc.h>使用时倒入这个框架





  • sizeof:是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等),这些数值在程序编译时就转成常数,程序运行时是直接获取的
  • 看到上面对两个函数的认识,应该知道为什么输出的一个是8,一个是16了吧,当内存申请<16时,在底层分配的时候,系统会默认最低16个字节,系统给objc16个字节,而objc用到的是8个字节(没添加任何成员变量之前)

二.内存对齐



  • 在上面的基础上我们新建一个类Student继承NSObject,那么对于student的底层C++编译实现就变成了:


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
};


也就是说,继承关系,子类直接将父类的isa引用进来




  • 对于class_getInstanceSize(也就是类本质的内存对其)

    1.在student中创建成员变量:
@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;
}
@end

其底层C++编译结构体就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
};



  • 打印结果:


 //定义一个objc
Student *objc = [[Student alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([Student class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));

2020-09-08 12:35:27.158568+0800 OC底层[1549:79836] tu-24

2020-09-08 12:35:27.159046+0800 OC底层[1549:79836] tu-32




  • 先来说说24的由来





由于创建对象的时候,内存是以8对齐,上面我们讲到一个对象里面包含了一个isa占8个字节,对于student来说它有四个成员变量,isa,age,no,tc,共占8+4+4+4=20字节,但是由于内存以8对齐的原因,我们看到的输出是24,

所以class_getInstanceSize在计算实例大小的时候就是24,其白色区域表示空出了四个字节

再来看看32的由来
上面我们说到malloc_size指的是实际堆分配的空间,它以16字节对齐
可以看到,空白的区域为空出了12个字节,总共为32个字节

三.添加属性


  • 添加属性


@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;

}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSArray *array;
@end

其在底层C++编译就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
NSString _name;
NSArray _array;
};


默认的会将属性生成的_name添加进结构体中,计算相应的大小



总结:所以在实际计算类的占用空间大小的时候,根据添加的成员变量就可以计算出一个实例占用的内存大小(即计算出结构体的大小24,然后告诉系统,系统调用calloc分配内存的时候按照16对齐原则分配)

收起阅读 »

vue 重复点击菜单,路由重复报错

报错信息vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。报错原因vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果...
继续阅读 »

报错信息

vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。


报错原因
vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果没有捕获到错误,控制台始终会出现如上图的报错
node_module/vue-router/dist/vue-router.js 搜VueRouter.prototype.push

解决方法

1.降低vue-router的版本

npm i vue-router@3.0 -S

2.在vue.use(Router)使用路由插件之前插入如下代码

//获取原型对象上的push函数
const originalPush = Router.prototype.push
//修改原型对象中的push方法
Router.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => err)
}

3.捕获异常

// 捕获router.push异常
this.$router.push(route).catch(err => {
console.log('输出报错',err)

4.补齐router第三个参数

// 补齐router.push()的第三个参数
this.$router.push(route, () => {}, (e) => {
console.log('输出报错',e)
})

本文链接:https://blog.csdn.net/pinbolei/article/details/115620529


收起阅读 »