注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

别再问我 new 字符串创建了几个对象了!我来证明给你看!

我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有...
继续阅读 »

我想所有 Java 程序员都曾被这个 new String 的问题困扰过,这是一道高频的 Java 面试题,但可惜的是网上众说纷纭,竟然找不到标准的答案。有人说创建了 1 个对象,也有人说创建了 2 个对象,还有人说可能创建了 1 个或 2 个对象,但谁都没有拿出干掉对方的证据,这就让我们这帮吃瓜群众们陷入了两难之中,不知道到底该信谁得。


但是今天,老王就斗胆和大家聊聊这个话题,顺便再拿出点证据


以目前的情况来看,关于 new String("xxx") 创建对象个数的答案有 3 种:



  1. 有人说创建了 1 个对象;

  2. 有人说创建了 2 个对象;

  3. 有人说创建了 1 个或 2 个对象。


而出现多个答案的关键争议点在「字符串常量池」上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串。


那我们就先来说说这个「字符串常量池」。


字符串常量池


字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。


字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:


字符串常量池示意图.png


以上说法可以通过如下代码进行证明:


public class StringExample {
public static void main(String[] args) {
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2);
}
}

以上程序的执行结果为:true,说明变量 s1 和变量 s2 指向的是同一个地址。


在这里我们顺便说一下字符串常量池的再不同 JDK 版本的变化。


这里,顺便送大家一份经典学习资料,我把大学和工作中用的经典电子书库(包含数据结构、操作系统、C++/C、网络经典、前端编程经典、Java相关、程序员认知、职场发展)、面试找工作的资料汇总都打包放在这。



戳这里直接获取:


计算机经典必读书单(含下载方式)


Java 入门到精通含面试最全资料包(含下载方式)


常量池的内存布局


JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上


JDK 1.7 内存布局如下图所示:


JDK 1.7 内存布局.png


JDK 1.8 内存布局如下图所示:


JDK 1.8 内存布局.png


JDK 1.8 与 JDK 1.7 最大的区别是 JDK 1.8 将永久代取消,并设立了元空间。官方给的说明是由于永久代内存经常不够用或发生内存泄露,会爆出 java.lang.OutOfMemoryError: PermGen 的异常,所以把将永久区废弃而改用元空间了,改为了使用本地内存空间,官网解释详情:openjdk.java.net/jeps/122


答案解密


认为 new 方式创建了 1 个对象的人认为,new String 只是在堆上创建了一个对象,只有在使用 intern() 时才去常量池中查找并创建字符串。


认为 new 方式创建了 2 个对象的人认为,new String 会在堆上创建一个对象,并且在字符串常量池中也创建一个字符串。


认为 new 方式有可能创建 1 个或 2 个对象的人认为,new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象,如下图所示:


new 字符串常量池.png


老王认为正确的答案:创建 1 个或者 2 个对象


技术论证


解铃还须系铃人,回到问题的那个争议点上,new String 到底会不会在常量池中创建字符呢?我们通过反编译下面这段代码就可以得出正确的结论,代码如下:


public class StringExample {
public static void main(String[] args) {
String s1 = new String("javaer-wang");
String s2 = "wang-javaer";
String s3 = "wang-javaer";
}
}

首先我们使用 javac StringExample.java 编译代码,然后我们再使用 javap -v StringExample 查看编译的结果,相关信息如下:


Classfile /Users/admin/github/blog-example/blog-example/src/main/java/com/example/StringExample.class
Last modified 2020年4月16日; size 401 bytes
SHA-256 checksum 89833a7365ef2930ac1bc3d7b88dcc5162da4b98996eaac397940d8997c94d8e
Compiled from "StringExample.java"
public class com.example.StringExample
minor version: 0
major version: 58
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #16 // com/example/StringExample
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // java/lang/String
#8 = Utf8 java/lang/String
#9 = String #10 // javaer-wang
#10 = Utf8 javaer-wang
#11 = Methodref #7.#12 // java/lang/String."<init>":(Ljava/lang/String;)V
#12 = NameAndType #5:#13 // "<init>":(Ljava/lang/String;)V
#13 = Utf8 (Ljava/lang/String;)V
#14 = String #15 // wang-javaer
#15 = Utf8 wang-javaer
#16 = Class #17 // com/example/StringExample
#17 = Utf8 com/example/StringExample
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 SourceFile
#23 = Utf8 StringExample.java
{
public com.example.StringExample();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: new #7 // class java/lang/String
3: dup
4: ldc #9 // String javaer-wang
6: invokespecial #11 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: ldc #14 // String wang-javaer
12: astore_2
13: ldc #14 // String wang-javaer
15: astore_3
16: return
LineNumberTable:
line 5: 0
line 6: 10
line 7: 13
line 8: 16
}
SourceFile: "StringExample.java"


备注:以上代码的运行也编译环境为 jdk1.8.0_101。



其中 Constant pool 表示字符串常量池,我们在字符串编译期的字符串常量池中找到了我们 String s1 = new String("javaer-wang"); 定义的“javaer-wang”字符,在信息 #10 = Utf8 javaer-wang 可以看出,也就是在编译期 new 方式创建的字符串就会被放入到编译期的字符串常量池中,也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。


那么问题来了,以下这段代码的执行结果为 true 还是 false?


String s1 = new String("javaer-wang");
String s2 = new String("javaer-wang");
System.out.println(s1 == s2);

既然 new String 会在常量池中创建字符串,那么执行的结果就应该是 true 了。其实并不是,这里对比的变量 s1 和 s2 堆上地址,因为堆上的地址是不同的,所以结果一定是 false,如下图所示:


字符串引用.png


从图中可以看出 s1 和 s2 的引用一定是相同的,而 s3 和 s4 的引用是不同的,对应的程序代码如下:


public static void main(String[] args) {
String s1 = "Java";
String s2 = "Java";
String s3 = new String("Java");
String s4 = new String("Java");
System.out.println(s1 == s2);
System.out.println(s3 == s4);
}

程序执行的结果也符合预期:



true false



扩展知识


我们知道 String 是 final 修饰的,也就是说一定被赋值就不能被修改了。但编译器除了有字符串常量池的优化之外,还会对编译期可以确认的字符串进行优化,例如以下代码:


public static void main(String[] args) {
String s1 = "abc";
String s2 = "ab" + "c";
String s3 = "a" + "b" + "c";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
}

按照 String 不能被修改的思想来看,s2 应该会在字符串常量池创建两个字符串“ab”和“c”,s3 会创建三个字符串,他们的引用对比结果也一定是 false,但其实不是,他们的结果都是 true,这是编译器优化的功劳。


同样我们使用 javac StringExample.java 先编译代码,再使用 javap -c StringExample 命令查看编译的代码如下:


警告: 文件 ./StringExample.class 不包含类 StringExample
Compiled from "StringExample.java"
public class com.example.StringExample {
public com.example.StringExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: ldc #7 // String abc
2: astore_1
3: ldc #7 // String abc
5: astore_2
6: ldc #7 // String abc
8: astore_3
9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
13: aload_2
14: if_acmpne 21
17: iconst_1
18: goto 22
21: iconst_0
22: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
25: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_1
29: aload_3
30: if_acmpne 37
33: iconst_1
34: goto 38
37: iconst_0
38: invokevirtual #15 // Method java/io/PrintStream.println:(Z)V
41: return
}

从 Code 3、6 可以看出字符串都被编译器优化成了字符串“abc”了。




总结


本文我们通过 javap -v XXX 的方式查看编译的代码发现 new String 首次会在字符串常量池中创建此字符串,那也就是说,通过 new 创建字符串的方式可能会创建 1 个或 2 个对象,如果常量池中已经存在此字符串只会在堆上创建一个变量,并指向字符串常量池中的值,如果字符串常量池中没有相关的字符,会先创建字符串在返回此字符串的引用给堆空间的变量。我们还将了字符串常量池在 JDK 1.7 和 JDK 1.8 的变化以及编译器对确定字符串的优化,希望能帮你正在的理解字符串的比较。


最后的话 原创不易,本篇近 3000 的文字描述,以及大量精美的图片,耗费了作者大概 5 个多小时的时间,写作是一件很酷,并且能帮助他人的事,作者希望一直能坚持下去。如果觉得有用,请随手点击一个赞吧,谢谢


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

2020-iOS最新面试题解析(原理篇)

runtime怎么添加属性、方法等ivar表示成员变量class_addIvarclass_addMethodclass_addPropertyclass_addProtocolclass_replaceProperty是否可以把比较耗时的操作放在NSNoti...
继续阅读 »

runtime怎么添加属性、方法等


  • ivar表示成员变量
  • class_addIvar
  • class_addMethod
  • class_addProperty
  • class_addProtocol
  • class_replaceProperty


是否可以把比较耗时的操作放在NSNotificationCenter中


  • 首先必须明确通知在哪个线程中发出,那么处理接受到通知的方法也在这个线程中调用
  • 如果在异步线程发的通知,那么可以执行比较耗时的操作;
  • 如果在主线程发的通知,那么就不可以执行比较耗时的操作


runtime 如何实现 weak 属性


首先要搞清楚weak属性的特点


weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。
为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;
然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)


那么runtime如何实现weak变量的自动置nil?


runtime对注册的类,会进行布局,会将 weak 对象放入一个 hash 表中。
用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会调用对象的 dealloc 方法,
假设 weak 指向的对象内存地址是a,那么就会以a为key,在这个 weak hash表中搜索,找到所有以a为key的 weak 对象,从而设置为 nil。


weak属性需要在dealloc中置nil么


  • 在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
  • 即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
  • 在属性所指的对象遭到摧毁时,属性值也会清空


// 模拟下weak的setter方法,大致如下
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}


一个Objective-C对象如何进行内存布局?(考虑有父类的情况)


  • 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
  • 父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中
  • 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的如下信息
    • 对象方法列表
    • 成员变量的列表
    • 属性列表
  • 每个 Objective-C 对象都有相同的结构,如下图所示

Objective-C 对象的结构图

ISA指针

根类(NSObject)的实例变量

倒数第二层父类的实例变量

...

父类的实例变量

类的实例变量


  • 根类对象就是NSObject,它的super class指针指向nil
  • 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类


一个objc对象的isa的指针指向什么?有什么作用?


  • 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型
  • 根据这个指针就能知道将来调用哪个类的方法


下面的代码输出什么?


@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end


  • 答案:都输出 Son
  • 这个题目主要是考察关于objc中对 self 和 super 的理解:
    • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者
    • 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
    • 而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法
    • 调用[self class] 时,会转化成 objc_msgSend函数
id objc_msgSend(id self, SEL op, ...)
    • 调用 [super class]时,会转化成 objc_msgSendSuper函数
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
    • 第一个参数是 objc_super 这样一个结构体,其定义如下
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
    • 第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
    • 第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
    • objc Runtime开源代码对- (Class)class方法的实现


    -(Class)class {
return object_getClass(self);
}



收起阅读 »

iOS 面试策略之算法基础1-3节

1. 基本数据结构数组数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:...
继续阅读 »

1. 基本数据结构


数组


数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种:


  • ContiguousArray:效率最高,元素分配在连续的内存上。如果数组是值类型(栈上操作),则 Swift 会自动调用 Array 的这种实现;如果注重效率,推荐声明这种类型,尤其是在大量元素是类时,这样做效果会很好。
  • Array:会自动桥接到 Objective-C 中的 NSArray 上,如果是值类型,其性能与 ContiguousArray 无差别。
  • ArraySlice:它不是一个新的数组,只是一个片段,在内存上与原数组享用同一区域。


下面是数组最基本的一些运用。


// 声明一个不可修改的数组
let nums = [1, 2, 3]
let nums = [Int](repeating: 0, count: 5)

// 声明一个可以修改的数组
var nums = [3, 1, 2]
// 增加一个元素
nums.append(4)
// 对原数组进行升序排序
nums.sort()
// 对原数组进行降序排序
nums.sort(by: >)
// 将原数组除了最后一个以外的所有元素赋值给另一个数组
// 注意:nums[0..<nums.count - 1] 返回的是 ArraySlice,不是 Array
let anotherNums = Array(nums[0 ..< nums.count - 1])


不要小看这些简单的操作:数组可以依靠它们实现更多的数据结构。Swift 虽然不像 Java 中有现成的队列和栈,但我们完全可以用数组配合最简单的操作实现这些数据结构,下面就是用数组实现栈的示例代码。


// 用数组实现栈
struct Stack<Element> {
private var stack: [Element]
var isEmpty: Bool { return stack.isEmpty }
var peek: AnyObject? { return stack.last }

init() {
stack = [Element]()
}

mutating func push(_ element: Element) {
stack.append(object)
}

mutating func pop() -> Element? {
return stack.popLast()
}
}

// 初始化一个栈
let stack = Stack<String>()


最后特别强调一个操作:reserveCapacity()。它用于为原数组预留空间,防止数组在增加和删除元素时反复申请内存空间或是创建新数组,特别适用于创建和 removeAll() 时候进行调用,为整段代码起到提高性能的作用。


字典和集合


字典和集合(这里专指HashSet)经常被使用的原因在于,查找数据的时间复杂度为 O(1)。

一般字典和集合要求它们的 Key 都必须遵守 Hashable 协议,Cocoa 中的基本数据类型都

满足这一点;自定义的 class 需要实现 Hashable,而又因为 Hashable 是对 Equable 的扩展,

所以还要重载 == 运算符。


下面是关于字典和集合的一些实用操作:


let primeNums: Set = [3, 5, 7, 11, 13]
let oddNums: Set = [1, 3, 5, 7, 9]

// 交集、并集、差集
let primeAndOddNum = primeNums.intersection(oddNums)
let primeOrOddNum = primeNums.union(oddNums)
let oddNotPrimNum = oddNums.subtracting(primeNums)

// 用字典和高阶函数计算字符串中每个字符的出现频率,结果 [“h”:1, “e”:1, “l”:2, “o”:1]
Dictionary("hello".map { ($0, 1) }, uniquingKeysWith: +)


集合和字典在实战中经常与数组配合使用,请看下面这道算法题:


给一个整型数组和一个目标值,判断数组中是否有两个数字之和等于目标值


这道题是传说中经典的 “2Sum”,我们已经有一个数组记为 nums,也有一个目标值记为 target,最后要返回一个 Bool 值。


最粗暴的方法就是每次选中一个数,然后遍历整个数组,判断是否有另一个数使两者之和为 target。这种做法时间复杂度为 O(n^2)。


采用集合可以优化时间复杂度。在遍历数组的过程中,用集合每次保存当前值。假如集合中已经有了目标值减去当前值,则证明在之前的遍历中一定有一个数与当前值之和等于目标值。这种做法时间复杂度为 O(n),代码如下。


func twoSum(nums: [Int], _ target: Int) -> Bool {
var set = Set<Int>()

for num in nums {
if set.contains(target - num) {
return true
}

set.insert(num)
}

return false
}


如果把题目稍微修改下,变为


给定一个整型数组中有且仅有两个数字之和等于目标值,求两个数字在数组中的序号


思路与上题基本类似,但是为了方便拿到序列号,我们采用字典,时间复杂度依然是 O(n)。代码如下。


func twoSum(nums: [Int], _ target: Int) -> [Int] {
var dict = [Int: Int]()

for (i, num) in nums.enumerated() {
if let lastIndex = dict[target - num] {
return [lastIndex, i]
} else {
dict[num] = i
}
}

fatalError("No valid output!")
}


字符串和字符


字符串在算法实战中极其常见。在 Swift 中,字符串不同于其他语言(包括 Objective-C),它是值类型而非引用类型,它是多个字符构成的序列(并非数组)。首先还是列举一下字符串的通常用法。


// 字符串和数字之间的转换
let str = "3"
let num = Int(str)
let number = 3
let string = String(num)

// 字符串长度
let len = str.count

// 访问字符串中的单个字符,时间复杂度为O(1)
let char = str[str.index(str.startIndex, offsetBy: n)]

// 修改字符串
str.remove(at: n)
str.append("c")
str += "hello world"

// 检测字符串是否是由数字构成
func isStrNum(str: String) -> Bool {
return Int(str) != nil
}

// 将字符串按字母排序(不考虑大小写)
func sortStr(str: String) -> String {
return String(str.sorted())
}

// 判断字符是否为字母
char.isLetter

// 判断字符是否为数字
char.isNumber

// 得到字符的 ASCII 数值
char.asciiValue


关于字符串,我们来一起看一道以前的 Google 面试题。


给一个字符串,将其按照单词顺序进行反转。比如说 s 是 "the sky is blue",

那么反转就是 "blue is sky the"。


这道题目一看好简单,不就是反转字符串的翻版吗?这种方法有以下两个问题


  • 每个单词长度不一样
  • 空格需要特殊处理
    这样一来代码写起来会很繁琐而且容易出错。不如我们先实现一个字符串翻转的方法。


fileprivate func reverse<T>(_ chars: inout [T], _ start: Int, _ end: Int) {
var start = start, end = end

while start < end {
swap(&chars, start, end)
start += 1
end -= 1
}
}

fileprivate func swap<T>(_ chars: inout [T], _ p: Int, _ q: Int) {
(chars[p], chars[q]) = (chars[q], chars[p])
}


有了这个方法,我们就可以实行下面两种字符串翻转:


  • 整个字符串翻转,"the sky is blue" -> "eulb si yks eht"
  • 每个单词作为一个字符串单独翻转,"eulb si yks eht" -> "blue is sky the"
    整体思路有了,我们就可以解决这道问题了


func reverseWords(s: String?) -> String? {
guard let s = s else {
return nil
}

var chars = Array(s), start = 0
reverse(&chars, 0, chars.count - 1)

for i in 0 ..< chars.count {
if i == chars.count - 1 || chars[i + 1] == " " {
reverse(&chars, start, i)
start = i + 2
}
}

return String(chars)
}


时间复杂度还是 O(n),整体思路和代码简单很多。


总结


在 Swift 中,数组、字符串、集合以及字典是最基本的数据结构,但是围绕这些数据结构的问题层出不穷。而在日常开发中,它们使用起来也非常高效(栈上运行)和安全(无需顾虑线程问题),因为他们都是值类型。


2. 链表


本节我们一起来探讨用 Swift 如何实现链表以及链表相关的技巧。


基本概念


对于链表的概念,实在是基本概念太多,这里不做赘述。我们直接来实现链表节点。


class ListNode { 
var val: Int
var next: ListNode?

init(_ val: Int) {
self.val = val
}
}


有了节点,就可以实现链表了。


class LinkedList {
var head: ListNode?
var tail: ListNode?

// 头插法
func appendToHead(_ val: Int) {
let node = ListNode(val)

if let _ = head {
node.next = head
} else {
tail = node
}

head = node
}

// 头插法
func appendToTail(_ val: Int) {
let node = ListNode(val)

if let _ = tail {
tail!.next = node
} else {
head = node
}

tail = node
}
}


有了上面的基本操作,我们来看如何解决复杂的问题。


Dummy 节点和尾插法


话不多说,我们直接先来看下面一道题目。


给一个链表和一个值 x,要求将链表中所有小于 x 的值放到左边,所有大于等于 x 的值放到右边。原链表的节点顺序不能变。

例:1->5->3->2->4->2,给定x = 3。则我们要返回1->2->2->5->3->4


直觉告诉我们,这题要先处理左边(比 x 小的节点),然后再处理右边(比 x 大的节点),最后再把左右两边拼起来。


思路有了,再把题目抽象一下,就是要实现这样一个函数:


func partition(_ head: ListNode?, _ x: Int) -> ListNode? {}


即我们有给定链表的头节点,有给定的x值,要求返回新链表的头结点。接下来我们要想:怎么处理左边?怎么处理右边?处理完后怎么拼接?


先来看怎么处理左边。我们不妨把这个题目先变简单一点:


给一个链表和一个值 x,要求只保留链表中所有小于 x 的值,原链表的节点顺序不能变。


例:1->5->3->2->4->2,给定x = 3。则我们要返回 1->2->2


我们只要采用尾插法,遍历链表,将小于 x 值的节点接入新的链表即可。代码如下:


func getLeftList(_ head: ListNode?, _ x: Int) -> ListNode? { 
let dummy = ListNode(0)
var pre = dummy, node = head

while node != nil {
if node!.val < x {
pre.next = node
pre = node!
}
node = node!.next
}

// 防止构成环
pre.next = nil
return dummy.next
}


注意,上面的代码我们引入了 Dummy 节点,它的作用就是作为一个虚拟的头前结点。我们引入它的原因是我们不知道要返回的新链表的头结点是哪一个,它有可能是原链表的第一个节点,可能在原链表的中间,也可能在最后,甚至可能不存在(nil)。而 Dummy 节点的引入可以巧妙的涵盖所有以上情况,我们可以用 dummy.next 方便得返回最终需要的头结点。


现在我们解决了左边,右边也是同样处理。接着只要让左边的尾节点指向右边的头结点即可。全部代码如下:


func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
// 引入Dummy节点
let prevDummy = ListNode(0), postDummy = ListNode(0)
var prev = prevDummy, post = postDummy

var node = head

// 用尾插法处理左边和右边
while node != nil {
if node!.val < x {
prev.next = node
prev = node!
} else {
post.next = node
post = node!
}
node = node!.next
}

// 防止构成环
post.next = nil
// 左右拼接
prev.next = postDummy.next

return prevDummy.next
}


注意这句 post.next = nil,这是为了防止链表循环指向构成环,是必须的但是很容易忽略的一步。

刚才我们提到了环,那么怎么检测链表中是否有环存在呢?



快行指针


笔者理解快行指针,就是两个指针访问链表,一个在前一个在后,或者一个移动快另一个移动慢,这就是快行指针。来看一道简单的面试题:


如何检测一个链表中是否有环?


答案是用两个指针同时访问链表,其中一个的速度是另一个的 2 倍,如果他们相等了,那么这个链表就有环了,这就是快行指针的实际使用。代码如下:


func hasCycle(_ head: ListNode?) -> Bool { 
var slow = head
var fast = head

while fast != nil && fast!.next != nil {
slow = slow!.next
fast = fast!.next!.next

if slow === fast {
return true
}
}

return false
}


再举一个快行指针一前一后的例子,看下面这道题。


删除链表中倒数第 n 个节点。例:1->2->3->4->5,n = 2。返回1->2->3->5。

注意:给定 n 的长度小于等于链表的长度。


解题思路依然是快行指针,这次两个指针移动速度相同。但是一开始,第一个指针(指向头结点之前)就落后第二个指针 n 个节点。接着两者同时移动,当第二个移动到尾节点时,第一个节点的下一个节点就是我们要删除的节点。代码如下:


func removeNthFromEnd(head: ListNode?, _ n: Int) -> ListNode? {
guard let head = head else {
return nil
}

let dummy = ListNode(0)
dummy.next = head
var prev: ListNode? = dummy
var post: ListNode? = dummy

// 设置后一个节点初始位置
for _ in 0 ..< n {
if post == nil {
break
}
post = post!.next
}

// 同时移动前后节点
while post != nil && post!.next != nil {
prev = prev!.next
post = post!.next
}

// 删除节点
prev!.next = prev!.next!.next

return dummy.next
}


这里还用到了 Dummy 节点,因为有可能我们要删除的是头结点。


总结


这次我们用 Swift 实现了链表的基本结构,并且实战了链表的几个技巧。在结尾处,我还想强调一下 Swift 处理链表问题的两个细节问题:


  • 一定要注意头结点可能就是 nil。所以给定链表,我们要看清楚 head 是不是 optional,在判断是不是要处理这种边界条件。
  • 注意每个节点的 next 可能是 nil。如果不为 nil,请用"!"修饰变量。在赋值的时候,也请注意"!"将 optional 节点传给非 optional 节点的情况。


3. 栈和队列


这期我们来讨论一下栈和队列。在 Swift 中,没有内设的栈和队列,很多扩展库中使用 Generic Type 来实现栈或是队列。正规的做法使用链表来实现,这样可以保证加入和删除的时间复杂度是 O(1)。然而笔者觉得最实用的实现方法是使用数组,因为 Swift 没有现成的链表,而数组又有很多的 API 可以直接使用,非常方便。


基本概念


对于栈来说,我们需要了解以下几点:


  • 栈是后进先出的结构。你可以理解成有好几个盘子要垒成一叠,哪个盘子最后叠上去,下次使用的时候它就最先被抽出去。
  • 在 iOS 开发中,如果你要在你的 App 中添加撤销操作(比如删除图片,恢复删除图片),那么栈是首选数据结构
  • 无论在面试还是写 App 中,只关注栈的这几个基本操作:push, pop, isEmpty, peek, size。


protocol Stack {
/// 持有的元素类型
associatedtype Element

/// 是否为空
var isEmpty: Bool { get }
/// 栈的大小
var size: Int { get }
/// 栈顶元素
var peek: Element? { get }

/// 进栈
mutating func push(_ newElement: Element)
/// 出栈
mutating func pop() -> Element?
}

struct IntegerStack: Stack {
typealias Element = Int

var isEmpty: Bool { return stack.isEmpty }
var size: Int { return stack.count }
var peek: Element? { return stack.last }

private var stack = [Element]()

mutating func push(_ newElement: Element) {
stack.append(newElement)
}

mutating func pop() -> Element? {
return stack.popLast()
}
}


对于队列来说,我们需要了解以下几点:


  • 队列是先进先出的结构。这个正好就像现实生活中排队买票,谁先来排队,谁先买到票。
  • iOS 开发中多线程的 GCD 和 NSOperationQueue 就是基于队列实现的。
  • 关于队列我们只关注这几个操作:enqueue, dequeue, isEmpty, peek, size。


protocol Queue {
/// 持有的元素类型
associatedtype Element

/// 是否为空
var isEmpty: Bool { get }
/// 队列的大小
var size: Int { get }
/// 队首元素
var peek: Element? { get }

/// 入队
mutating func enqueue(_ newElement: Element)
/// 出队
mutating func dequeue() -> Element?
}

struct IntegerQueue: Queue {
typealias Element = Int

var isEmpty: Bool { return left.isEmpty && right.isEmpty }
var size: Int { return left.count + right.count }
var peek: Element? { return left.isEmpty ? right.first : left.last }

private var left = [Element]()
private var right = [Element]()

mutating func enqueue(_ newElement: Element) {
right.append(newElement)
}

mutating func dequeue() -> Element? {
if left.isEmpty {
left = right.reversed()
right.removeAll()
}
return left.popLast()
}
}


栈和队列互相转化


处理栈和队列问题,最经典的一个思路就是使用两个栈/队列来解决问题。也就是说在原栈/队列的基础上,我们用一个协助栈/队列来帮助我们简化算法,这是一种空间换时间的思路。下面是示例代码:


// 用栈实现队列
struct MyQueue {
var stackA: Stack
var stackB: Stack

var isEmpty: Bool {
return stackA.isEmpty
}

var peek: Any? {
get {
shift()
return stackB.peek
}
}

var size: Int {
get {
return stackA.size + stackB.size
}
}

init() {
stackA = Stack()
stackB = Stack()
}

func enqueue(object: Any) {
stackA.push(object);
}

func dequeue() -> Any? {
shift()
return stackB.pop();
}

fileprivate func shift() {
if stackB.isEmpty {
while !stackA.isEmpty {
stackB.push(stackA.pop()!);
}
}
}
}

// 用队列实现栈
struct MyStack {
var queueA: Queue
var queueB: Queue

init() {
queueA = Queue()
queueB = Queue()
}

var isEmpty: Bool {
return queueA.isEmpty
}

var peek: Any? {
get {
if isEmpty {
return nil
}

shift()
let peekObj = queueA.peek
queueB.enqueue(queueA.dequeue()!)
swap()
return peekObj
}
}

var size: Int {
return queueA.size
}

func push(object: Any) {
queueA.enqueue(object)
}

func pop() -> Any? {
if isEmpty {
return nil
}

shift()
let popObject = queueA.dequeue()
swap()
return popObject
}

private func shift() {
while queueA.size > 1 {
queueB.enqueue(queueA.dequeue()!)
}
}

private func swap() {
(queueA, queueB) = (queueB, queueA)
}
}


上面两种实现方法都是使用两个相同的数据结构,然后将元素由其中一个转向另一个,从而形成一种完全不同的数据。


面试题实战


给一个文件的绝对路径,将其简化。举个例子,路径是 "/home/",简化后为 "/home";路径是"/a/./b/../../c/",简化后为 "/c"。


这是一道 Facebook 的面试题。这道题目其实就是平常在终端里面敲的 cd、pwd 等基本命令所得到的路径。


根据常识,我们知道以下规则:


  • “. ” 代表当前路径。比如 “ /a/. ” 实际上就是 “/a”,无论输入多少个 “ . ” 都返回当前目录
  • “..”代表上一级目录。比如 “/a/b/.. ” 实际上就是 “ /a”,也就是说先进入 “a” 目录,再进入其下的 “b” 目录,再返回 “b” 目录的上一层,也就是 “a” 目录。


然后针对以上信息,我们可以得出以下思路:


  1. 首先输入是个 String,代表路径。输出要求也是 String, 同样代表路径;
  2. 我们可以把 input 根据 “/” 符号去拆分,比如 "/a/b/./../d/" 就拆成了一个String数组["a", "b", ".", "..", "d"];
  1. 创立一个栈然后遍历拆分后的 String 数组,对于一般 String ,直接加入到栈中,对于 ".." 那我们就对栈做 pop 操作,其他情况不错处理。


思路有了,代码也就有了


func simplifyPath(path: String) -> String {
// 用数组来实现栈的功能
var pathStack = [String]()
// 拆分原路径
let paths = path.split(separatedBy: "/")

for path in paths {
// 对于 "." 我们直接跳过
guard path != "." else {
continue
}
// 对于 ".." 我们使用pop操作
if path == ".." {
if (!pathStack.isEmpty) {
pathStack.removeLast()
}
// 对于太注意空数组的特殊情况
} else if path != "" {
pathStack.append(path)
}
}
// 将栈中的内容转化为优化后的新路径
return "/" + pathStack.joined(separator: "/")
}


上面代码除了完成了基本思路,还考虑了大量的特殊情况、异常情况。这也是硅谷面试考察的一个方面:面试者思路的严谨,对边界条件的充分考虑,以及代码的风格规范。


总结


在 Swift 中,栈和队列是比较特殊的数据结构,笔者认为最实用的实现和运用方法是利用数组。虽然它们本身比较抽象,却是很多复杂数据结构和 iOS 开发中的功能模块的基础。这也是一个工程师进阶之路理应熟练掌握的两种数据结构。

收起阅读 »

iOS 面试简单准备

1.简历的准备在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。1.简洁的艺术互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被...
继续阅读 »

1.简历的准备


在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。


1.简洁的艺术


互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被认为不够专业。


更麻烦的是,多数超过一页的简历很可能在 HR 手中就被过滤掉了。因为 HR 每天会收到大量的简历,一般情况下每份简历在手中的停留时间也就 10 秒钟左右。而超过一页的简历会需要更多的时间去寻找简历中的有价值部分,对于 HR 来说,她更倾向于认为这种人通常是不靠谱的,因为写个简历都不懂行规,为什么还要给他面试机会呢?


那么我们应该如何精简简历呢? 简单说来就是一个字:删!


删掉不必要的自我介绍信息。很多求职者会将自己在学校所学的课程罗列上去,例如:C 语言,数据结构,数学分析⋯⋯好家伙,一写就是几十门,还放在简历的最上面,就怕面试官看不见。对于这类信息,一个字:删!面试官不关心你上了哪些课程,而且在全中国,大家上的课程也都大同小异,所以没必要写出来。


删除不必要的工作或实习、实践经历。如果你找一份程序员的工作,那么你参加了奥运会的志愿者活动,并且拿到了奖励或者你参加学校的辩论队,获得了最佳辩手这些经历通常是不相关的。诸如此类的还有你帮导师代课,讲了和工作不相关的某某专业课,或者你在学生会工作等等。删除不相关的工作、实习或实践内容可以保证你的简历干净。当然,如果你实在没得可写,比如你是应届生,一点实习经历都没有,那可以适当写一两条,保证你能写够一页的简历,但是那两条也要注意是强调你的团队合作能力或者执行力之类的技能,因为这些才是面试官感兴趣的。


删除不必要的证书。最多写个 4、6 级的证书,什么教师资格证,中高级程序员证,还有国内的各种什么认证,都是没有人关心的。


删除不必要的细节。作为 iOS 开发的面试官,很多求职者在介绍自己的 iOS 项目经历的时候,介绍了这个工程用的工作环境是 Mac OS,使用的机器是 Mac Mini,编译器是 Xcode,能够运行在 iOS 什么版本的环境。还有一些人,把这个项目用到的开源库都写上啦,什么 AFNetworking, CocoaPods 啥的。这些其实都不是重点,请删掉。后面我会讲,你应该如何介绍你的 iOS 项目经历。


自我评价,这个部分是应届生最喜欢写的,各种有没有的优点都写上,例如:


性格开朗、稳重、有活力,待人热情、真诚;工作认真负责,积极主动,能吃苦耐劳,勇于承受压力,勇于创新;有很强的组织能力和团队协作精神,具有较强的适应能力;纪律性强,工作积极配合;意志坚强,具有较强的无私奉献精神。对待工作认真负责,善于沟通、协调有较强的组织能力与团队精神;活泼开朗、乐观上进、有爱心并善于施教并行;上进心强、勤于学习能不断提高自身的能力与综合素质。


这些内容在面试的时候不太好考查,都可以删掉。通常如果有 HR 面的话,HR 自然会考查一些你的沟通,抗压,性格等软实力。


我相信,不管你是刚毕业的学生,还是工作十年的老手,你都可以把你的简历精简到一页 A4 纸上。记住,简洁是一种美,一种效率,也是一种艺术。


2.重要的信息写在最前面


将你觉得最吸引人的地方写在最前面。如果你有牛逼公司的实习,那就把实习经历写在最前面,如果你在一个牛逼的实验室里面做科研,就把研究成果和论文写出来,如果你有获得过比较牛逼的比赛名次(例如 Google code jam, ACM 比赛之类),写上绝对吸引眼球。


所以,每个人的简历的介绍顺序应该都是不一样的,不要在网上下载一个模板,然后就一项一项地填:教育经历,实习经历,得奖经历,个人爱好,这样的简历毫无吸引力,也无法突出你的特点。


除了你的个人特点是重要信息外,你的手机号、邮箱、毕业院校、专业以及毕业时间这些也都是非常重要的,一定要写在简历最上面。


3.不要简单地罗列工作经历


不要简单地说你开发了某某 iOS 客户端。这样简单的罗列你的作品集并不能让面试官很好地了解你的能力,当然,真正在面试时面试官可能会仔细询问,但是一份好的简历,应该省去一些面试官额外询问你的工作细节的时间。


具体的做法是:详细的描述你对于某某 iOS 客户端的贡献。主要包括:你参与了多少比例功能的开发? 你解决了哪些开发中的有挑战的问题? 你是不是技术负责人?


而且,通过你反思这些贡献,你也可以达到自我审视,如果你发现这个项目你根本什么有价值的贡献都没做,就打了打酱油,那你最好不要写在简历上,否则当面试官在面试时问起时,你会很难回答,最终让他发现你的这个项目经历根本一文不值时,肯定会给一个负面的印象。


4.不要写任何虚假或夸大的信息


刚刚毕业的学生都喜欢写精通 Java,精通 C/C++,其实代码可能写了不到 1 万行。我觉得你要精通某个语言,至少得写 50 万行这个语言的代码才行,而且要对语言的各种内部机制和原理有了解。那些宣称精通 Java 的同学,连 Java 如何做内存回收,如何做泛型支持,如何做自动 boxing 和 unboxing 的都不知道,真不知道为什么要写精通二字。


任何夸大或虚假的信息,在面试时被发现,会造成极差的面试印象。所以你如果对某个知识一知半解,要么就写 “使用过” 某某,要么就干脆不写。如果你简历实在太单薄,没办法写上了一些自己打酱油的项目,被问起来怎么办? 请看看下面的故事:


我面试过一个同学,他在面试时非常诚实。我问他一些简历上的东西,他如果不会,就会老实说,这个我只是使用了一下,确实不清楚细节。对于一些没有技术含量的项目,他也会老实说,这个项目他做的工作比较少,主要是别人在做。最后他还会补充说,“我自认为自己数据结构和算法还不错,要不你问我这方面的知识吧。”


这倒是一个不错的办法,对于一个没有项目经验,但是聪明并且数据结构和算法基础知识扎实的应届生,其实我们是非常愿意培养的。很多人以为公司面试是看经验,希望招进来就能干活,其实不是的,至少我们现在以及我以前在网易招人,面试的是对方的潜力,潜力越大,可塑性好,未来进步得也更快;一些资质平庸,却经验稍微丰富一点的开发者,相比聪明好学的面试者,后劲是不足的。


总之,不要写任何虚假或夸大的信息,即使你最终骗得过面试官,进了某公司,如果能力不够,在最初的试用期内,也很可能因为能力不足而被开掉。


5.留下更多信息


刚刚说到,简历最好写够一张 A4 纸即可,那么你如果想留下更多可供面试官参考的信息怎么办呢?其实你可以附上更多的参考链接,这样如果面试官对你感兴趣,自然会仔细去查阅这些链接。对于 iOS 面试来说,GitHub 上面的开源项目地址、博客地址都是不错的参考信息。如果你在微博上也频繁讨论技术,也可以附上微博地址。


我特别建议大家如果有精力,可以好好维护一下自己的博客或者 GitHub 上的开源代码。因为如果你打算把这些写到简历上,让面试官去上面仔细评价你的水平,你就应该对上面的内容做到足够认真的准备。否则,本来面试完面试官还挺感兴趣的,结果一看你的博客和开源代码,评价立刻降低,就得不偿失了。


6.不要附加任何可能带来负面印象的信息


任何与招聘工作无关的东西,尽量不要提。有些信息提了可能有加分,也可能有减分,取决于具体的面试官。下面我罗列一下我认为是减分的信息。


1)个人照片


不要在简历中附加个人照片。个人长相属于与工作能力不相关的信息,也许你觉得你长得很帅,那你怎么知道你的样子不和面试官的情敌长得一样? 也许你长得很漂亮,那么你怎么知道 HR 是否被你长得一样的小三把男朋友抢了? 我说得有点极端,那人们对于长相的评价标准确实千差万别,萝卜青菜各有所爱,加上可能有一些潜在的极端情况,所以没必要附加这部分信息。这属于加了可能有加分,也可能有减分的情况。


2)有风险的爱好


不要写各种奇怪的爱好。喜欢打游戏、抽烟、喝酒,这类可能带来负面印象的爱好最好不要写。的确有些公司会有这种一起联机玩游戏或者喝酒的文化,不过除非你明确清楚对于目标公司,写上会是加分项,否则还是不写为妙。


3)使用 PDF 格式


不要使用 Word 格式的简历,要使用 PDF 的格式。我在招 iOS 程序员时,好多人的简历都是 Word 格式的,我都怀疑这些人是否有 Mac 电脑。因为 Mac 下的 office 那么难用,公司好多人机器上都没有 Mac 版 office。我真怀疑这些人真是的想投简历么? PDF 格式的简历通常能展现出简历的专业性。


4)QQ号码邮箱


不要使用 QQ 号开头的 QQ 邮箱,例如 12345@qq.com ,邮箱的事情我之前简单说过,有些人很在乎这个,有些人觉得无所谓,我个人对用数字开头的 QQ 邮箱的求职者不会有加分,但是对使用 Gmail 邮箱的求职者有加分。因为这涉及到个人的工作效率,使用 Gmail 的人通常会使用邮件组,过滤器,IMAP 协议,标签,这些都有助于提高工作效率。如果你非要使用 QQ 邮箱,也应该申请一个有意义的邮箱名,例如 tangqiaoboy@qq.com 。


7.职业培训信息


不要写参加过某某培训公司的 iOS 培训,特别是那种一、两个月的速成培训。这对于我和身边很多面试官来说,绝对是负分。


这个道理似乎有点奇怪,因为我们从小都是由老师教授新知识的。我自己也实验过,掌握同样的高中课本上的知识,自己自学的速度通常比老师讲授的速度要慢一倍的时间。即一个知识点,如果你自己要看 2 小时的书才能理解的话,有好的老师给你讲解的话,只需要一个小时就够了。所以,我一直希望在学习各种东西的时候都能去听一些课程,因为我认为这样节省了我学习的时间。


但是这个道理在程序员这个领域行不通,为什么这么说呢?原因有两点:


  1. 计算机编程相关的知识更新速度很快。同时,国内的 IT 类资料的翻译质量相当差,原创的优秀书籍也很少。所以,我们通常需要靠阅读英文才能掌握最新的资料。拿 iOS 来说,每年 WWDC 的资料都非常重要,而这些内容涉及版权,国内培训机构很难快速整理成教材。
  2. 计算机编程知识需要较多的专业知识积累和实践。学校的老师更多只能做入门性的教学工作。
    如果一个培训机构有一个老师,他强到能够通过自己做一些项目来积累很多专业知识和实践,并且不断地从国外资料上学习最新的技术。那么这个人在企业里面会比在国内的培训机构更有施展自己能力的空间。国内的培训机构因为受众面的原因,基本上还是培养那种初级的程序员新手,所以对老师的新技术学习速度要求不会那么高,自然老师也不会花那么时间在新技术研究上。但是企业就不一样了,企业需要不停地利用新技术来增强自己的产品竞争力,所以对于 IT 企业来说,产品的竞争就是人才的竞争,所以给优秀的人能够开出很高的薪水。
    所以,我们不能期望从 IT 类培训机构中学习到最新的技术,一切只能通过我们自学。当然,自学之后在同行之间相互交流,对于我们的技术成长也是很有用的。小结



上图是本节讨论的总结,在简历准备上,我们需要考虑简历的简洁性等各种注意事项。


2.寻找机会


1.寻找内推机会

其实,最好的面试机会都不是公开渠道的。好的机会都隐藏于各种内部推荐之中。通过内部推荐,你可以更加了解目标工作的团队和内容,另外内部推荐通常也可以跳过简历筛选环节,直接参加笔试或面试。我所在的猿辅导公司为内推设立了非常高的奖金激励,因为我们发现,综合各种渠道来看,内推的渠道的人才的简历质量最高,面试通过率最高的。


所以,如果你是学生,找找你在各大公司的师兄师姐内推;如果你已经工作了,你可以找前同事或者通过一些社交活动认识的技术同行内推。


大部分情况下,如果在目标公司你完全没有认识的人,你也可以找机会来认识几个。比如你可以通过微博、知乎、Twitter、GitHub 来结交新的朋友。然后双方聊天如果愉快的话,我相信内推这种举手之劳的事情对方不会拒绝的。


如果你都工作 5 年以上,还是没有建立足够好的社交圈子帮助你内推,那可能你需要做很多的社交活动交一些朋友。


2.其它常见的渠道

内推之外,其它的公开招聘渠道通常都要差一些。现在也有一些专门针对互联网行业的招聘网站,例如拉勾、100offer 这类,它们也是不错的渠道,可以找到相关的招聘信息。


但因为这类公开渠道简历投放数量巨大,通常 HR 那边就会比较严格地筛选简历,拿我们公司来说,通常在这些渠道看 20 份简历,才会有 1 份愿意约面的简历。而且 HR 会只挑比较好的学校或者公司的候选人,也不排除还有例如笔试这种更多的面试流程。但是面试经验都是慢慢积累的,建议你也可以尝试这些渠道。


3.面试流程


1.流程简述


就我所知,大部分的 iOS 公司的面试流程都大同小异。我们先简述一下大体的流程,然后再详细讨论。


在面试的刚开始,面试官通常会要求你做一个简短的自我介绍。然后面试官可能会和你聊聊你过去的实习项目或者工作内容。接着面试官可能会问你一些具体的技术问题,有经验的面试官会尽量找一些和你过去工作相关的技术问题。最后,有些公司会选择让你当场写写代码,Facebook 会让你在白板上写代码,国内的更多是让你在 A4 纸上写。有一些公司也会问一些系统设计方面的问题,考查你的整体架构能力。在面试快要结束时,通常面试官还会问你有没有别的什么问题。


以上这些流程,不同公司可能会跳过某些环节。比如有一些公司就不会考察当场在白板或 A4 纸上写代码。有些公司可能跳过问简历的环节直接写代码,特别是校园招聘的时候,因为应届生通常项目经验较少。面试流程图如下所示:



2.自我介绍


自我介绍通常是面试中最简单、最好准备的环节。


一个好的自我介绍应该结合公司的招聘职位来做定制。比如公司有硬件的背景,就应该介绍一下在硬件上的经验或者兴趣;公司如果注重算法能力,则介绍自己的算法练习;公司如果注重团队合作,那么你介绍一下自己的社会活动都是可以的。


一个好的自我介绍应该提前写下来,并且背熟。因为候选人通常的紧张感都是来自于面试刚开始的几分钟,如果你刚开始的几分钟讲的结结巴巴,那么这种负面情绪会加剧你面试时的紧张感,从而影响你正常发挥。如果你提前把自我介绍准备得特别流利,那么开始几分钟的紧张感过去之后,你很可能就会很快进入状态,而忘记紧张这个事情了。


即使做到了以上这些仍然是不够的,候选者常见的问题还包括:


  • 太简短
  • 没有重点
  • 太拖沓
  • 不熟练


我们在展开讨论上面这些问题之前,我们可以站在面试官的立场考虑一下:如果你是面试官,你为什么要让候选人做自我介绍?你希望在自我介绍环节考察哪些信息?


在我看来,自我介绍环节相当重要,因为:


  • 首先它考察了候选人的表达能力。大部分的程序员表达能力可能都一般,但是如果连自我介绍都说不清楚,通常就说明表达沟通能力稍微有点问题了。面试官通过这个环节可以初步考察到候选人的表达能力。
  • 它同样也考察了候选人对于面试的重视程度。一般情况下,表达再差的程序员,也可以通过事先拟稿的方式,把自我介绍内容背下来。如果一个人自我介绍太差,说明他不但表达差,而且不重视这次面试。
  • 最后,自我介绍对之后的面试环节起到了支撑作用。因为自我介绍中通常会涉及自己的项目经历,自己擅长的技术等。这些都很可能吸引面试官追问下去。好的候选人会通过自我介绍 “引导” 面试官问到自己擅长的领域知识。如果面试官最后对问题答案很满意,通过面试的几率就会大大增加。


所以我如果是面试官,我希望能得到一个清晰流畅的自我介绍。下面我们来看看候选人在面试中的常见问题。


1)太简短


一个好的自我介绍大概是 3~5 分钟。过短的自我介绍没法让面试官了解你的大致情况,也不足以看出来你的基本表达能力。


如果你发现自己没法说出足够时间的自我介绍。可以考虑在介绍中加入:自己的简单的求学经历,项目经历,项目中有亮点的地方,参与或研究过的一些开源项目,写过的博客,其它兴趣爱好,自己对新工作的期望和目标公司的理解。


我相信每个人经过准备,都可以做到一个 5 分钟长度的自我介绍。


2)没有重点


突破了时间的问题,接下来就需要掌握介绍的重点。通常一个技术面试,技术相关的介绍才是重点。所以求学经历,兴趣爱好之类的内容可以简单提到即可。


对于一个工作过的开发者,你过去做的项目哪个最有挑战,最能展示出你的水平其实自己应该是最清楚的。所以大家可以花时间把这个内容稍微强调一下。


当然你也没必要介绍得太细致,面试官如果感兴趣,自然会在之后的面试过程中和你讨论。


3)太拖沓


有些工作了几年的人,做过的项目差不多有个 3~5 个,面试的时候就忍不住一个一个说。单不说这么多项目在自我介绍环节不够介绍。就是之后的详细讨论环节,面试官也不可能讨论完你的所有项目经历。


所以千万不要做这种 “罗列经历” 的事情,你要做的就是挑一个或者最多两个项目,介绍一下项目大致的背景和你解决的主要问题即可。至于具体的解决过程,可以不必介绍。


4)不熟练


即便你的内容完全合适,时间长度完全合理,你也需要保证一个流利的陈述过程。适当在面试前多排练几次,所有人都可以做到一个流利的自我介绍。


还有一点非常神奇,就是一个人在做一件事情的时候,通常都是开始的前以及刚开始几分钟特别紧张。比如考试,演讲或者面试,通常这几分钟之后,人们进入 “状态” 了,就会忘记紧张了。


将自己的自我介绍背下来,可以保证一个流利顺畅的面试开局,这可以极大舒缓候选人的紧张情绪。相反,一开始自我介绍就结结巴巴,会加剧候选人的紧张情绪,而技术面试如果不能冷静的话,是很难在写代码环节保证逻辑清晰正确的。


所以,请大家务必把这个小环节重视起来,做出一个完美的开局。


3.项目介绍


自我介绍之后,就轮到讨论更加具体的内容环节了,通常面试官都会根据自我介绍或者你的简历,选一个他感兴趣的项目来详细讨论。


这个时候,大家务必需要提前整理出自己参与的项目的具体挑战,以及自己做得比较好的地方。切忌不要说:“这个项目很简单,没什么挑战,那个项目也很简单,没什么好说的”。再简单的事情,都可以做到极致的,就看你有没有一个追求完美的心。


比如你的项目可能在你看来就是摆放几个 iOS 控件。但是,这些控件各自有什么使用上的技巧,有什么优化技巧?其实如果专心研究,还是有很多可以学习的。拿 UITableView 来说,一个人如果能够做到把它的滑动流程度优化好,是非常不容易的。这里面涉及网络资源的异步加载、图片资源的缓存、后台线程的渲染、CALayer 层的优化等等。


这其实也要求我们做工作要精益求精,不求甚解。所以一场成功的面试最最本质上,看得还是自己平时的积累。如果自己平时只是糊弄工作,那么面试时就很容易被看穿。


在这一点上,我奉劝大家在自己的简历上一定要老实。不要在建简历上弄虚作假,把自己没有做过的项目写在里面。


顺便我在这里也教一下大家如何面试别人。如果你是面试官,考察简历的真假最简单的方法就是问细节。一个项目的细节如果问得很深入,候选人有没有做过很容易可以看出来。


举个例子,如果候选人说他在某公司就职期间做了某某项目。你就可以问他:


  • 这个工作具体的产品需求是什么样的?
  • 大概做了多长时间?
  • 整体的软件架构是什么样的?
  • 涉及哪些人合作?几个开发和测试?
  • 项目的时间排期是怎么定的?
  • 你主要负责的部分和合作的人?
  • 项目进行中有没有遇到什么问题?
  • 这个项目最后最大的收获是什么?遗憾是什么?
  • 项目最困难的一个需求是什么?具体什么实现的?


面试官如果做过类似项目,还可以问问通常这个项目常见的坑,看看候选人是什么解决的。


4.写代码


编程能力,说到底还是一个实践的能力,所以说大部分公司都会考察当场写代码。我面试过上百人,见到过很多候选人在自我介绍和项目讨论时都滔滔不绝,侃侃而谈,感觉非常好。但是一到写代码环节就怂了,要么写出来各种逻辑问题和细节问题没处理好,要么就是根本写不出来。


由于人最终招进来就是干活写代码的,所以如果候选人当场写代码表现很差的话,基本上面试就挂掉了。


程序员这个行业,说到底就是一个翻译的工作,把产品经理的产品文档和设计师的 UI 设计,翻译成计算机能够理解的形式,这个形式通常就是一行一行的源码。


当面试官考察你写代码的时候,他其实在考察:


  • 你对语言的熟悉程度。如果候选人连常见的变量定义和系统函数都不熟悉,说明他肯定经验还是非常欠缺。
  • 你对逻辑的处理能力。产品经理关注的是用户场景和核心需求,而程序员关注的是逻辑边界和异常情况。程序的 bug 往往就是边界和特殊情况没有处理好。虽然说写出没有 bug 的程序几乎不可能,但是逻辑清晰的程序员能够把思路理清楚,减少 bug 发生的概率。
  • 设计和架构能力。好的代码需要保证易于维护和修改。这里面涉及很多知识,从简单的 “单一职责” 原则,到复杂的 “好的组合优于继承” 原则,其中设计模式相关的知识最多。写代码的时候多少还是能够看出这方面的能力。另外有些公司,例如 Facebook,会有专门的系统设计(System Design)面试环节,专注于考察设计能力。


5.系统设计


有一些公司喜欢考查一些系统设计的问题,简单来说,就是让你解决一个具体的业务需求,看看你是否能够将业务逻辑梳理清楚,并且拆解成各个模块,设计好模块间的关系。举几个例子,面试官可能让你:


  • 设计一个类似微博的信息流应用。
  • 设计一个本地数据缓存架构。
  • 设计一个埋点分析系统。
  • 设计一个直播答题系统。
  • 设计一个多端的数据同步系统。
  • 设计一个动态补丁的方案。


这些系统的设计都非常考查一个人知识的全面性。通常情况下,如果一个人只知道 iOS 开发的知识,是很难做出相关的设计的。为了能够解决这些系统设计题,我们首先需要有足够的知识面,适度了解一下 Android 端、Web 端以及服务器端的各种技术方案背后的原理。你可以不写别的平台的代码,但是一定要理解它们在技术上能做到什么,不能做到什么。你也不必过于担心,面试官在考查的时候,还是会重点考查 iOS 相关的部分。


我们将在下一小节,展开讨论如何准备和回答系统设计题。


6.提问


提问环节通常在面试结束前,取决于前面的部分是否按时结束,有些时候前面的环节占用了太多时间,也可能没有提问环节了。在后面的章节,我们会展开讨论一下如何提问。



收起阅读 »

腾讯抖音iOS岗位三面面经

1.进程和线程的区别2.死锁的原因3.介绍虚拟内存4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么5.TCP可靠性6.http+https算法Z字遍历二叉树,归并排序后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这...
继续阅读 »

1.进程和线程的区别


2.死锁的原因


3.介绍虚拟内存


4.常见排序算法,排序算法稳定的意思,快排的复杂度什么时候退化,基本有序用什么


5.TCP可靠性


6.http+https


算法


Z字遍历二叉树,归并排序


后面说因为我不会java和安卓,会帮忙转推到iOS的组(面试的这个组是java客户端)


腾讯PCG iOS一面(1h)


1.聊项目,聊了很久,一开始没有意会面试官想知道什么,最后说是想知道我这么做比起从客户端自己去实现的区别(这个项目是小米实习时候的项目,做的浏览器内核,页面翻译功能,


基本每一个客户端应用都会有一个类似于浏览器内核的东西,对页面进行渲染,呈现,也可以叫渲染引擎,学前端的肯定知道这个东西,他主要是解释html,css,js的。


我做的这个页面翻译功能可以不经过内核直接由客户端工程师用安卓客户端实现整套逻辑,所以这么问我了)


2.实现string类,实现构造,析构,里面加一个kmp


3.介绍智能指针,智能指针保存引用计数的变量存在哪里,引用计数是否线程安全


4.算法:两个只有0和1的数字序列,只能0  1互换,每次当前位互换都会使后面的也换掉(比如,011000,换第二位,成了000111),计算从一个变到另一个需要几步操作。


5.https,验证公钥有效的方法,为什么非对称对称并用


腾讯PCG iOS二面 (40min)


1.算法:


合并排序链表


2.static关键字的作用


3.const关键字的作用


4.成员初始化列表的作用


5.指针和引用的区别


6.又是很久的项目,怎么去学习浏览器内核(chromium内核的代码量有几千万行,而且写的很难懂,用了大量的设计模式,作为一个菜鸡真的很痛苦)


,怎么去调试项目中遇到的问题(这里主要是一个ipc接口没用对),你觉得人家google的是怎么去调的,你为什么和人家做法不一样?


腾讯PCG iOS三面(2h)


1.还是聊了很久项目(已经麻了,做过的东西一定要能说出口)


2.浏览器呈现一个页面经过了哪几步(DOM树,layoutobject树,browser进程绘制)


3.C++多态的实现


4.DNS解析,递归与迭代的区别


5.chromium用的渲染引擎是什么,这个渲染引擎对应的js解释引擎是什么(blink和v8,前几个问题表现有些差,这会在问一些1+1的问题了,哭)


6.平时怎么学习技术的,看过哪些书,有过哪些输出(我把实习时写的项目wiki给截了个图)


然后反问,打开牛客让我写了个代码,他不知道去哪了,我自己在这写,写了一个多钟头,你以为这是道很难的题吗?no,是我那会确实很菜,哈哈


题目是,给一个字符串插入最少的字符,让这个字符串变成回文


腾讯hr面 (40min)


1.有哪些缺点


2.投了哪些,为什么不投阿里头条(实习忙的我面你们都要面不过来了)


3.如何选择offer


4.家是哪的,为什么愿意去深圳


每一个问题都不是简单的答完就完事了,他会跟着问很多


然后反问时我问问题给我说了20分钟


抖音 iOS一面 (1h20min)


上来闲聊了一会


1.算法:


字符串大数相加


写完问我有没有需要优化的地方(内存可以优化一下)


2.string类赋值运算符是深拷贝还是浅拷贝


3.算法:


根据前序和中序输出二叉树的后续遍历


4.C++ deque底层,deque有没有重载[]


5.为什么要内存对齐,内存对齐的规则


6.算法:


上台阶,加了个条件,这次上两级,下次就只能上一级


7.反问+闲聊


抖音 iOS二面 (1h)


十分钟不到的项目


1.进程和线程的区别和联系


2.线程共享哪些内存空间


3.进程内存模型


4.进程间通信方式


5. 虚拟内存,为什么要有虚拟内存,虚拟内存如何映射到物理内存


后面还挖了一些操作系统的问题,记不太清了


5.TCP为什么四次挥手


6.https客户端验证公钥的方法


7.描述并写一下LRU


8.说一下怎么学chromium的,怎么上手项目的


9.C++内存分配,写了一段代码,看里面申请了哪部分内存,申请了多少,代码有什么问题


10.代码里面的内存泄漏怎么解决,智能指针的引用计数怎么实现,那些成员函数会影响到引用计数


11.代码里面有无线程安全问题,线程安全问题的是否会导致程序崩溃,为什么


12.C++虚函数的实现原理,纯虚函数


13.C++引用和指针的区别,引用能否为空


抖音 iOS三面 (1h)


1.lambda表达式,它应用表达式外变量的方式和区别


2.decltype的作用,他和auto有什么不同


3.C++的所有智能指针介绍一下


4.C++thread里面的锁,条件变量,讲一下怎么用他们实现生产者消费者模型


5.C++20有什么新东西(我就知道支持了协程,然后他说我就想问你协程,然后我说,其实我具体不了解,丢人了)


6.右值引用是什么,移动构造函数有什么好处


7.操作系统微内核宏内核(懵)


8.进程间通信的共享内存,如何保证安全性(信号量),结合epoll讲讲共享内存


9.TCP协议切片(懵)


10.TCP协议的流量控制机制,滑窗为0时,怎么办


11.算法


合并K个排序链表


12.合并K个排序数组,讲思路,我说归并,他说,传输参数是数组,不是vector,你如何判断数组的大小



收起阅读 »

iOS 整理出一份高级iOS面试题

1、NSArray与NSSet的区别?NSArray内存中存储地址连续,而NSSet不连续NSSet效率高,内部使用hash查找;NSArray查找需要遍历NSSet通过anyObject访问元素,NSArray通过下标访问2、NSHashTable与NSMa...
继续阅读 »

1、NSArray与NSSet的区别?


  • NSArray内存中存储地址连续,而NSSet不连续
  • NSSet效率高,内部使用hash查找;NSArray查找需要遍历
  • NSSet通过anyObject访问元素,NSArray通过下标访问


2、NSHashTable与NSMapTable?


  • NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
  • NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;可以在访问成员时copy


(注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。

NSMapTable与NSDictionary的区别:同上)


3、属性关键字assign、retain、weak、copy


  • assign:用于基本数据类型和结构体。如果修饰对象的话,当销毁时,属性值不会自动置nil,可能造成野指针。
  • weak:对象引用计数为0时,属性值也会自动置nil
  • retain:强引用类型,ARC下相当于strong,但block不能用retain修饰,因为等同于assign不安全。
  • strong:强引用类型,修饰block时相当于copy。


4、weak属性如何自动置nil的?


  • Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。


5、Block的循环引用、内部修改外部变量、三种block


  • block强引用self,self强引用block
  • 内部修改外部变量:block不允许修改外部变量的值,这里的外部变量指的是栈中指针的内存地址。__block的作用是只要观察到变量被block使用,就将外部变量在栈中的内存地址放到堆中。
  • 三种block:NSGlobalBlack(全局)、NSStackBlock(栈block)、NSMallocBlock(堆block)


6、KVO底层实现原理?手动触发KVO?swift如何实现KVO?


  • KVO原理:当观察一个对象时,runtime会动态创建继承自该对象的类,并重写被观察对象的setter方法,重写的setter方法会负责在调用原setter方法前后通知所有观察对象值得更改,最后会把该对象的isa指针指向这个创建的子类,对象就变成子类的实例。
  • 如何手动触发KVO:在setter方法里,手动实现NSObject两个方法:willChangeValueForKey、didChangeValueForKey
  • swift的kvo:继承自NSObject的类,或者直接willset/didset实现。


7、categroy为什么不能添加属性?怎么实现添加?与Extension的区别?category覆盖原类方法?多个category调用顺序


  • Runtime初始化时categroy的内存布局已经确定,没有ivar,所以默认不能添加属性。
  • 使用runtime的关联对象,并重写setter和getter方法。
  • Extenstion编译期创建,可以添加成员变量ivar,一般用作隐藏类的信息。必须要有类的源码才可以添加,如NSString就不能创建Extension。
  • category方法会在runtime初始化的时候copy到原来前面,调用分类方法的时候直接返回,不再调用原类。如何保持原类也调用(https://www.jianshu.com/p/40e28c9f9da5)。
  • 多个category的调用顺序按照:Build Phases ->Complie Source 中的编译顺序。


8、load方法和initialize方法的异同。——主要说一下执行时间,各自用途,没实现子类的方法会不会调用父类的?

load initialize 调用时机 app启动后,runtime初始化的时候 第一个方法调用前调用 调用顺序 父类->本类->分类 父类->本类(如果有分类直接调用分类,本类不会调用) 没实现子类的方法会不会调用父类的 否 是 是否沿用父类实现 否 是




9、对 runtime 的理解。——主要是方法调用时如何查找缓存,如何找到方法,找不到方法时怎么转发,对象的内存布局


OC中向对象发送消息时,runtime会根据对象的isa指针找到对象所属的类,然后在该类的方法列表和父类的方法列表中寻找方法执行。如果在最顶层父类中没找到方法执行,就会进行消息转发:Method resoution(实现方法)、fast forwarding(转发给其他对象)、normal forwarding(完整消息转发。可以转发给多个对象)


10、runtime 中,SEL和IMP的区别?


每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址


11、autoreleasepool的原理和使用场景?


  • 若干个autoreleasepoolpage组成的双向链表的栈结构,objc_autoreleasepoolpush、objc_autoreleasepoolpop、objc_autorelease
  • 使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
  • autoreleasepoolpage的内存结构:4k存储大小



12、Autorelase对象什么时候释放


在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。


13、Runloop与线程的关系?Runloop的mode? Runloop的作用?内部机制?


  • 每一个线程都有一个runloop,主线程的runloop默认启动。
  • mode:主要用来指定事件在运行时循环的优先级
  • 作用:保持程序的持续运行、随时处理各种事件、节省cpu资源(没事件休息释放资源)、渲染屏幕UI


14、iOS中使用的锁、死锁的发生与避免


  • @synchronized、信号量、NSLock等
  • 死锁:多个线程同时访问同一资源,造成循环等待。GCD使用异步线程、并行队列


15、NSOperation和GCD的区别


  • GCD底层使用C语言编写高效、NSOperation是对GCD的面向对象的封装。对于特殊需求,如取消任务、设置任务优先级、任务状态监听,NSOperation使用起来更加方便。
  • NSOperation可以设置依赖关系,而GCD只能通过dispatch_barrier_async实现
  • NSOperation可以通过KVO观察当前operation执行状态(执行/取消)
  • NSOperation可以设置自身优先级(queuePriority)。GCD只能设置队列优先级(DISPATCH_QUEUE_PRIORITY_DEFAULT),无法在执行的block中设置优先级
  • NSOperation可以自定义operation如NSInvationOperation/NSBlockOperation,而GCD执行任务可以自定义封装但没有那么高的代码复用度
  • GCD高效,NSOperation开销相对高


16、oc与js交互


  • 拦截url
  • JavaScriptCore(只适用于UIWebView)
  • WKScriptMessageHandler(只适用于WKWebView)
  • WebViewJavaScriptBridge(第三方框架)


17、swift相比OC有什么优势?


18、struct、Class的区别


  • class可以继承,struct不可以
  • class是引用类型,struct是值类型
  • struct在function里修改property时需要mutating关键字修饰


19、访问控制关键字(public、open、private、filePrivate、internal)


  • public与open:public在module内部中,class和func都可以被访问/重载/继承,外部只能访问;而open都可以
  • private与filePrivate:private修饰class/func,表示只能在当前class源文件/func内部使用,外部不可以被继承和访问;而filePrivate表示只能在当前swift源文件内访问
  • internal:在整个模块或者app内都可以访问,默认访问级别,可写可不写


20、OC与Swift混编


  • OC调用swift:import "工程名-swift.h” @objc 
  • swift调用oc:桥接文件


21、map、filter、reduce?map与flapmap的区别?


  • map:数组中每个元素都经过某个方法转换,最后返回新的数组(xx.map({$0 * $0}))
  • flatmap:同map类似,区别在flatmap返回的数组不存在nil,并且会把optional解包;而且还可以把嵌套的数组打开变成一个([[1,2],[2,3,4],[5,6]] ->[1,2,2,3,4,5,6])
  • filter:用户筛选元素(xxx.filter({$0 > 25}),筛选出大于25的元素组成新数组)
  • reduce:把数组元素组合计算为一个值,并接收初始值()




22、guard与defer


  • guard用于提前处理错误数据,else退出程序,提高代码可读性
  • defer延迟执行,回收资源。多个defer反序执行,嵌套defer先执行外层,后执行内层


23、try、try?与try!


  • try:手动捕捉异常
  • try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
  • try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash


24、@autoclosure:把一个表达式自动封装成闭包


25、throws与rethrows:throws另一个throws时,将前者改为rethrows


26、App启动优化策略?main函数执行前后怎么优化


  • 启动时间 = pre-main耗时+main耗时
  • pre-main阶段优化:
  • 删除无用代码
  • 抽象重复代码
  • +load方法做的事情延迟到initialize中,或者+load的事情不宜花费太多时间
  • 减少不必要的framework,或者优化已有framework
  • Main阶段优化
  • didFinishLauchingwithOptions里代码延后执行
  • 首次启动渲染的页面优化


27、crash防护?


  • unrecognized selector crash
  • KVO crash
  • NSNotification crash
  • NSTimer crash
  • Container crash(数组越界,插nil等)
  • NSString crash (字符串操作的crash)
  • Bad Access crash (野指针)
  • UI not on Main Thread Crash (非主线程刷UI (机制待改善))


28、内存泄露问题?


主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。


29、UI卡顿优化?


30、架构&设计模式


  • MVC设计模式介绍
  • MVVM介绍、MVC与MVVM的区别?
  • ReactiveCocoa的热信号与冷信号
  • 缓存架构设计LRU方案
  • SDWebImage源码,如何实现解码
  • AFNetWorking源码分析
  • 组件化的实施,中间件的设计
  • 哈希表的实现原理?如何解决冲突


31、数据结构&算法


  • 快速排序、归并排序
  • 二维数组查找(每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数)
  • 二叉树的遍历:判断二叉树的层数
  • 单链表判断环


32、计算机基础


  1. http与https?socket编程?tcp、udp?get与post?
  2. tcp三次握手与四次握手
  1. 进程与线程的区别



收起阅读 »

一步一步搭建Flutter开发架子-Tabbar

一点点搭建一个架构,架构对于开发比较重要,有固定的模式,第一不容易产生bug,并且有利于对于项目以及开发架构的理解。 对于一个app,常见的架构一般是底部有Tabbar形式,或者采用抽屉的形式,底部Tabbar大部分app都是平铺的,中间有一块凸出来的形式。普...
继续阅读 »

一点点搭建一个架构,架构对于开发比较重要,有固定的模式,第一不容易产生bug,并且有利于对于项目以及开发架构的理解。 对于一个app,常见的架构一般是底部有Tabbar形式,或者采用抽屉的形式,底部Tabbar大部分app都是平铺的,中间有一块凸出来的形式。

普通Tabbar

 比较简单代码直接贴出来了

 @override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: normalBottomBar(),
);
}
normalBottomBar() {
return BottomNavigationBar(
// 底部导航
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
activeIcon: Icon(Icons.access_alarm)),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'search'),
BottomNavigationBarItem(icon: Icon(Icons.people), label: 'mine'),
],
currentIndex: _selectedIndex,
fixedColor: Colors.blue,
elevation: 10, // default: 8.0
type: BottomNavigationBarType.fixed,
iconSize: 30,
selectedFontSize: 12, // 默认是14,未选择是14
onTap: _onItemTapped,
);
}
_onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}

中间凸出的Tabbar

官方的Material风格库中存在这种效果,不过个人感觉跟现在流行的风格有点不太匹配,所以封装了一个接近于现在流行风格的Tabbar。 刚开始的效果是这样的:  凸出的按钮封装了一下的代码片段:

import 'dart:math';

import 'package:flutter/material.dart';

class CenterNavigationItem extends StatefulWidget {
CenterNavigationItem({Key key, this.onTap}) : super(key: key);

final Function onTap;

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

class _CenterNavigationItemState extends State<CenterNavigationItem> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
// CustomPaint(size: Size(76, 76), painter: MyPainter()),
Container(
width: 76,
height: 76,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Color.fromRGBO(250, 250, 250, 1),
borderRadius: BorderRadius.circular(38)),
child: FloatingActionButton(
child: Icon(Icons.add),
// child: TextField(),
tooltip: '测试', // 长按弹出提示
onPressed: () {
widget.onTap();
}),
),
],
);
}
}
// 主页面引用:
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: CenterNavigationItem(
onTap: () {
setState(() {
_selectedIndex = 1;
});
},
),
bottomNavigationBar: normalBottomBar(),
);
}

感觉还是差点意思, 在于底部线与线直接连接的不够平滑 

带有动画效果的Tabbar

继续改造一下的想法就是通过CustomPainter去画一个半圆。效果如下:  代码片段:

class MyPainter extends CustomPainter {
@override
paint(Canvas canvas, Size size) {
Paint paint = Paint()
..isAntiAlias = false
..color = Colors.green
..strokeCap = StrokeCap.round
..strokeWidth = 0
..style = PaintingStyle.stroke;
print(pi);
canvas.drawArc(
new Rect.fromCircle(center: Offset(38, 38), radius: size.width / 2),
pi,
2 * pi * 0.5,
false,
paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}

在凸出的按钮封装的代码中添加:

class _CenterNavigationItemState extends State<CenterNavigationItem> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
CustomPaint(size: Size(76, 76), painter: MyPainter()),
class _CenterNavigationItemState extends State<CenterNavigationItem> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
// 这个位置加入!!!!!!!!!!
CustomPaint(size: Size(76, 76), painter: MyPainter()),
Container(
width: 76,
height: 76,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Color.fromRGBO(250, 250, 250, 1),
borderRadius: BorderRadius.circular(38)),
child: FloatingActionButton(
child: Icon(Icons.add),
// child: TextField(),
tooltip: '测试', // 长按弹出提示
onPressed: () {
widget.onTap();
}),
),
],
);
}
}
],
);
}
}

最终就是这个效果。还有就是也不能闭门造车,上网搜搜大家都是怎么去构建Tabbar的。其中在github上发现一个加入了动画的开源代码。Motion-Tab-Bar。分析了一波代码,看了一眼效果如下: 

稍加修改了一下,留着以后项目可能用的上。over~欢迎讨论

one more thing...

  • 1, 路由管理,
  • 2, 国际化管理,
  • 3, 数据持久化管理,
  • 4, 响应式管理方法


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

收起阅读 »

一步一步搭建Flutter开发架子-国际化,路由,本地化,响应式

接上一篇文章,这篇文章主要介绍,路由管理,国际化管理,响应式管理方法,数据持久化管理。还是先看看大神么们都是怎么写的,从中学习一下。看到又一个比较好用的库getx,方便简介,基本上都包含今天要封装的内容,那就用起来吧。ps:有人可能会有想法说是应该自己写,总用...
继续阅读 »

接上一篇文章,这篇文章主要介绍,路由管理,国际化管理,响应式管理方法,数据持久化管理。还是先看看大神么们都是怎么写的,从中学习一下。看到又一个比较好用的库getx,方便简介,基本上都包含今天要封装的内容,那就用起来吧。ps:有人可能会有想法说是应该自己写,总用第三方的库,遇到问题不好处理,有点道理,换个想法如果自己没达到那个水平也可以先使用第三方库,好好看看大神的源码,来提升自己。总之所当面看待吧。


引入GetX


在pubspec.yaml文件中加入


dependencies:
get: ^3.24.0

在需要使用的文件中引入


import 'package:get/get.dart'

在main.dart中使用GetMaterialApp替换MaterialApp


 return GetMaterialApp(
enableLog: false,
debugShowCheckedModeBanner: false,
defaultTransition: Transition.rightToLeftWithFade,
theme: ThemeData(
primarySwatch: Colors.orange,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TabBarPage());

路由管理


比较喜欢这个的原因: 不需要获取上下文context直接跳转页面,代码很简洁,并且支持别名路由跳转
效果


不带参数颇通跳转


Get.to(OtherPage())

带参数跳转


Get.to(OtherPage(id:''))

无返回跳转


比如在登录成功之后的跳转,不能够再返回到登录页面


Get.off(OtherPage(id:''))

跳转到Tabbar页面


比如在商品的详情页面直接跳转到购物车页面,一般购物车页面在Tabbar上。


Get.off(TabbarPage(currentIndex: 1));

别名跳转


这种情况大家可以去看下GetX的文档,这里就不介绍了。因为我不打算在项目里面坐这种跳转。ps:纯个人原因


SnackBars,Dialogs,BottomSheets使用


GetX中,我们也可以不获取上下文context进行跳用SnackBars,Dialogs, BottomSheets使用


SnackBars


效果:


 Get.snackbar(
"Hey i'm a Get SnackBar!",
"It's unbelievable! I'm using SnackBar without context, without boilerplate, without Scaffold, it is something truly amazing!",
icon: Icon(Icons.alarm),
shouldIconPulse: true,
barBlur: 20,
isDismissible: true,
duration: Duration(seconds: 3),
backgroundColor: Colors.red);

具体的属性,大家可以点击进去看下源码配置


Dialogs


效果:


 Get.defaultDialog(
onConfirm: () => print("Ok"),
buttonColor: Colors.white,
middleText: "Dialog made in 3 lines of code");


也可以弹出自定义的组件


Get.dialog(YourDialogWidget());

BottomSheets


效果:


Get.bottomSheet(Container(
decoration: BoxDecoration(color: Colors.red),
child: Wrap(
children: <Widget>[
ListTile(
leading: Icon(Icons.music_note),
title: Text('Music'),
onTap: () {}),
ListTile(
leading: Icon(Icons.videocam),
title: Text('Video'),
onTap: () {},
),
],
),
));

以上是简单的用法,我们可以新建哥utils文件夹,封装成一个工具类去调用这个方法。


国际化管理


目前涉及到2中模式,



  • 根据系统语言设置国际化

  • 在应用内设置国际化显示


首先创建一个Languages.dart文件 简单的写了一个hello的中英文含义


import 'package:get/get.dart';
class Languages extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'zh_CN': {
'hello': '你好 世界',
},
'en_US': {
'hello': 'Hallo Welt',
}
};
}

在main.dart中加入代码:


return GetMaterialApp(
translations: Languages(), // 你的翻译
locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
);

显示只需要加入如下就ok。很简单


Text('hello'.tr)


跟随系统语言


ui.window.locale 获取当前系统语言,设置本地语言


GetMaterialApp(
translations: Languages(), // 你的翻译
locale: ui.window.locale, // 将会按照此处指定的语言翻译
fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译 不存在
......
)

在应用内设置国际化显示


更新文字显示为中文如下:


var locale = Locale('zh', 'CN');
Get.updateLocale(locale);

多写两句用RadioListTile实现一下效果


RadioListTile(
value: 'chinese',
groupValue: _selected,
title: Text('中文'),
subtitle: Text('中文'),
selected: _selected == 'chinese',
onChanged: (type) {
var locale = Locale('zh', 'CN');
Get.updateLocale(locale);
setState(() {
_selected = type;
});
}),
RadioListTile(
value: 'english',
groupValue: _selected,
title: Text('英文'),
subtitle: Text('英文'),
selected: _selected == 'english',
onChanged: (type) {
var locale = Locale('en', 'US');
Get.updateLocale(locale);
setState(() {
_selected = type;
});
},
),

这是本人测试使用用的代码。看一下效果


响应式管理方法


GetX举例是一个计数器的例子,已经很容易理解了,作用就是不用在引入过多的状态管理的库,比如provide之类的。用法差不多。更简洁。还是记录一下,方便以后查看用


class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}

lass Home extends StatelessWidget {

@override
Widget build(context) {

// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());

return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: RaisedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}

class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();

@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}

这块还没有在程序中使用,但是状态管理在程序中使用还是很方便的,比如更改用户信息,登录。购物车逻辑中都可以使用


数据持久化管理


这个地方引入了第二个第三方库


  flustars: ^0.3.3
# https://github.com/Sky24n/sp_util
# sp_util分拆成单独的库,可以直接引用
sp_util: ^1.0.1

用起来也很方便感觉不错。
为了之后方便使用,现定义一个Global.dart文件,做初始化操作


import 'package:flustars/flustars.dart';
class Global {
static Future initSqutil() async => await SpUtil.getInstance();
}
在main方法中调用:
Global.initSqutil();

接下来进行存储数据以及获取数据的方法,类型包括:字符串,布尔值,对象,数组
举个例子:


存数据
SpUtil.putString( 'login', '登录了',);
取数据
SpUtil.getString('login',defValue: '');

额外提的一点就是存储对象类型数组,分两种形式,getObjList, getObjectList方法


 类似泛型的结构, 可以进行转换
List<Map> dataList = SpUtil.getObjList('cityMap', (v) => v);
返回一个Map数组
List<Map> dataList = SpUtil.getObjectList('cityMap');

这个地方可以配合国际化语言切换时使用。比如每次改变语言进行存储。然后每次打开app进行,获取初始化。


one more things...



  • 网络请求

  • 页面不同状态展示封装

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

Python操作Redis

Part1前言前面我们都是使用 Redis 客户端对 Redis 进行使用的,但是实际工作中,我们大多数情况下都是通过代码来使用 Redis 的,由于小编对 Python 比较熟悉...
继续阅读 »

Part1前言

前面我们都是使用 Redis 客户端对 Redis 进行使用的,但是实际工作中,我们大多数情况下都是通过代码来使用 Redis 的,由于小编对 Python 比较熟悉,所以我们今天就一起来学习下如何使用 Python 来操作 Redis

Part2环境准备

  • Redis 首先需要安装好。
  • Python 安装好(建议使用 Python3)。
  • Redis 的 Python 库安装好(pip install redis)。

Part3开始实践

1小试牛刀

例:我们计划通过 Python 连接到 Redis。然后写入一个 kv,最后将查询到的 v 打印出来。

直接连接

#!/usr/bin/python3

import redis # 导入redis模块

r = redis.Redis(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,password为认证密码,redis默认端口是6379
r.set('name', 'phyger-from-python-redis') # key是"name" value是"phyger-from-python-redis" 将键值对存入redis缓存
print(r['name']) # 第一种:取出键name对应的值
print(r.get('name')) # 第二种:取出键name对应的值
print(type(r.get('name')))


执行结果
服务端查看客户端列表

其中的 get 为连接池最后一个执行的命令。

连接池

通常情况下,需要连接 redis 时,会创建一个连接,基于这个连接进行 redis 操作,操作完成后去释放。正常情况下,这是没有问题的,但是并发量较高的情况下,频繁的连接创建和释放对性能会有较高的影响,于是连接池发挥作用。

连接池的原理:预先创建多个连接,当进行 redis 操作时,直接获取已经创建好的连接进行操作。完成后,不会释放这个连接,而是让其返回连接池,用于后续 redis 操作!这样避免连续创建和释放,从而提高了性能!

#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-from-python-redis')
print(r['name'])
print(r.get('name')) # 取出键name对应的值
print(type(r.get('name')))


执行结果

你会发现,在实际使用中直连和使用连接池的效果是一样的,只是在高并发的时候会有明显的区别。

2基操实践

对于众多的 Redis 命令,我们在此以 SET 命令为例进行展示。

格式: set(name, value, ex=None, px=None, nx=False, xx=False)

在 redis-py 中 set 命令的参数:

参数名释义
ex过期时间(m)
px过期时间(ms)
nx如果为真,则只有 name 不存在时,当前 set 操作才执行
xx如果为真,则只有 name 存在时,当前 set 操作才执行

ex

我们计划创建一个 kv 并且设置其 ex 为 3,期待 3 秒后此 k 的 v 会变为 None

#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-from-python-redis',ex=3)
print(r['name']) # 应当有v
time.sleep(3)
print(r.get('name')) # 应当无v
print(type(r.get('name')))


3秒过期

nx

由于 px 的单位太短,我们就不做演示,效果和 ex 相同。

我们计划去重复 set 前面已经 set 过的 name,不出意外的话,在 nx 为真时,我们将会 set 失败。但是人如果 set 不存在的 name1,则会成功。

#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-0',nx=3) # set失败
print(r['name']) # 应当不生效
r.set('name1', 'phyger-1',nx=3) # set成功
print(r.get('name1')) # 应当生效
print(type(r.get('name')))


只有不存在的k才会被set

如上,你会发现 name 的 set 未生效,因为 name 已经存在于数据库中。而 name1 的 set 已经生效,因为 name1 是之前在数据库中不存在的。

xx

我们计划去重复 set 前面已经 set 过的 name,不出意外的话,在 nx 为真时,我们将会 set 成功。但是人如果 set 不存在的 name2,则会失败。

#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-0',xx=3) # set失败
print(r['name']) # 应当变了
r.set('name2', 'phyger-1',xx=3) # set成功
print(r.get('name2')) # 应当没有set成功
print(type(r.get('name')))


只有存在的k才会被set

以上,就是今天全部的内容,更多信息建议参考 redis 官方文档。


作者:phyger
来源:https://mp.weixin.qq.com/s/bsv57OPKubD2dz0Wskn6eQ

收起阅读 »

基于echarts 24种数据可视化展示,填充数据就可用,动手能力强的还可以DIY

前言我们先跟随百度百科了解一下什么是“数据可视化 [1]”。   数据可视化,是关于数据视觉表现形式的科学技术研究。   其中,这种数据的视觉表现形式被定义为,一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。   它是一个处于不断演变之中...
继续阅读 »

前言

我们先跟随百度百科了解一下什么是“数据可视化 [1]”。



  数据可视化,是关于数据视觉表现形式的科学技术研究。


  其中,这种数据的视觉表现形式被定义为,一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。


  它是一个处于不断演变之中的概念,其边界在不断地扩大。


主要指的是技术上较为高级的技术方法,而这些技术方法允许利用图形、图像处理计算机视觉以及用户界面,通过表达、建模以及对立体、表面、属性以及动画的显示,对数据加以可视化解释。


与立体建模之类的特殊技术方法相比,数据可视化所涵盖的技术方法要广泛得多。



  大家对展厅显示、客户导流、可视化汇报工作、对数据结果进行图形分析等这些业务场景都不陌生。


很多后端大多都只提供接口数据,并没有去构建前端显示页面,一来是不专业(各种特效、自适应等),二来是公司有前端,用不到后端来写。


  但是暂时用到不代表我们不用,用的时候写不来怎么办?下面介绍24种数据可视化的demo,直接下载下来填充数据就可以跑起来,不满足的还可以DIY(演示地址+下载地址),yyds。


演示地址

注意:演示中如果有加载失败的,是环境问题,下载下来运行就好了。


演示地址:https://www.xiongze.net/viewdata/index.html [2]


现有的24种如下:


大数据展示系统、物流数据概况系统、物流订单系统、物流信息系统、办税渠道监控平台、车辆综合管控平台、


电子商务公共服务中心、各行业程序员中心、简洁大数据统计中心、警务平台大数据统计、农业监测大数据指挥舱、


农业监控数据平台、社会治理运行分析云图、水质监测大数据中心、水质情况实时监测预警系统、


物联网大数据统计平台、消防监控预警、销售数据报表中心、医疗大数据中心、营业数据统中心、


智慧旅游综合服务平台、智慧社区内网比对平台、智慧物流服务中心、政务大数据共享交换平台。



echarts图表库:Echarts提供了常规的折线图、柱状图、散点图、饼图、k线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、treemap、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。



下面demo里面的图标颜色、样式都可以在 echarts官网-文档-配置项手册里面进行查看, 是支持通过修改里面的配置项里面的属性来达到项目需求,我们可以去进行查看修改。


pic_a12bf32a.png

下载地址

Git下载链接:https://gitee.com/xiongze/viewdata.git [3]


百度网盘下载链接:https://pan.baidu.com/s/1jgwK6BvrS2rmbkrtW2MpYA提取码:xion


Demo示例(部分)

1、总览

pic_e6090ae5.png

pic_d4d67ce3.png

pic_f40b7776.png

2、物流信息展示

pic_8757023a.png

3、车辆综合管控平台

pic_cacec7a0.png

4、农业监测大数据指挥舱

pic_72002bf9.png

5、水质情况实时监控预警中心

pic_ab9ecf3a.png

6、消防监控预警中心

pic_94a47e47.png

7、医疗大数据中心

pic_9744b569.png

8、物联网平台数据中心

pic_50685f14.png

更多……

总共24种,这里就不一一展示了,大家下载下来就可以玩了。


可视化应用

  数据可视化的开发和大部分项目开发一样,也是根据需求来根据数据维度或属性进行筛选,根据目的和用户群选用表现方式。同一份数据可以可视化成多种看起来截然不同的形式。



  • 有的可视化目标是为了观测、跟踪数据,所以就要强调实时性、变化、运算能力,可能就会生成一份不停变化、可读性强的图表。
  • 有的为了分析数据,所以要强调数据的呈现度、可能会生成一份可以检索、交互式的图表
  • 有的为了发现数据之间的潜在关联,可能会生成分布式的多维的图表。
  • 有的为了帮助普通用户或商业用户快速理解数据的含义或变化,会利用漂亮的颜色、动画创建生动、明了,具有吸引力的图表。
  • 还有的被用于教育、宣传或政治,被制作成海报、课件,出现在街头、广告手持、杂志和集会上。这类可视化拥有强大的说服力,使用强烈的对比、置换等手段,可以创造出极具冲击力自指人心的图像。在国外许多媒体会根据新闻主题或数据,雇用设计师来创建可视化图表对新闻主题进行辅助。

  数据可视化的应用价值,其多样性和表现力吸引了许多从业者,而其创作过程中的每一环节都有强大的专业背景支持。无论是动态还是静态的可视化图形,都为我们搭建了新的桥梁,让我们能洞察世界的究竟、发现形形色色的关系,感受每时每刻围绕在我们身边的信息变化,还能让我们理解其他形式下不易发掘的事物。


参考文献

[1]百度百科:数据可视化
[2]演示地址:https://www.xiongze.net/viewdata/index.html
[3]下载链接:https://gitee.com/xiongze/viewdata.git
[4]数据可视化概念



作者:熊泽-学习中的苦与乐
来源:https://www.cnblogs.com/xiongze520/p/15588852.html


收起阅读 »

CommonJS和ES6 Module究竟是什么

对于前端模块化总是稀里糊涂,今天深入学习一下前端模块化,彻底弄懂CommonJs和ES6 Module,希望本文可以给你带来帮助。 CommonJS 模块 CommonJS中规定每个文件是一个模块。将一个JS文件通过script标签插入页面与封装成Common...
继续阅读 »

对于前端模块化总是稀里糊涂,今天深入学习一下前端模块化,彻底弄懂CommonJs和ES6 Module,希望本文可以给你带来帮助。


CommonJS


模块


CommonJS中规定每个文件是一个模块。将一个JS文件通过script标签插入页面与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者形成一个属于模块自身的作用域,所有的变量及函数只能自己访问,对外不可见。


导出


导出是一个模块向外暴露自身的唯一方式。在commonJS中,通过modul
e.exports可以导出模块中的内容
。下面的代码导出了一个对象,包含name和add属性。


module.exports = {
name: 'calculater',
add: function(a, b){
return a+b;
}
}

为了书写方便,CommonJS也支持直接使用exports。


exports.name = 'calculater';
exports.add = function(a, b){
return a+b;
}

exports可以理解为


var module = {
exports:{}
};
var exports = module.exports;

注意错误的用法:



  1. 不要给exports直接赋值,否则导出会失效。如下代码,对exports赋值,使其指向新的对象。module.exports却仍然是原来的空对象,因此name属性并不会被导出。


exports = {
name: 'calculater'
}


  1. 不恰当的把module.exports和exports混用。如下代码,先通过exports导出add属性,然后将module.exports重新赋值为另一个对象,将导致add属性丢失,最后导出只有name。


exports.add = function(a,b){
return a+b;
}
module.exports = {
name: 'calculater'
}

导入


在CommonJs中,使用require进行模块导入。


const calculator = require('./calculator.js')
let sum = calculator.add(2,3)

注意:

  1. require的模块是第一次被加载,这时会首先执行该模块,然后导出内容
  2. require的模块曾被加载过,这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
  3. 对于不需要获取导出内容的模块,直接使用require即可。
  4. require函数可以接收表达式,借助这个特性可以动态地指定模块加载路径。

const moduleName = ['a.js', 'b.js'];
moduleNames.forEach(name => {
require('./'+name)
})

ES6 Module


模块


ES6 Module是ES语法的一部分,它也是将每个文件作为一个模块,每个模块拥有自身的作用域。


导出


在ES6 Module中使用export命令来导出模块。export有两种形式:

  • 命名导出
  • 默认导出

一个模块可以有多个命名导出,它有两种不同的写法:


//写法1,将变量的声明和导出写在一行
export const name = 'calculator'
export const add = function(a, b){return a+b}

//写法2,先进行变量的声明,然后在用同一个export语句导出。
const name = 'calculator'
const add = function(a, b){return a+b}
export {name, add}

与命名导出不同,模块的默认导出只能有一个。


export default {
name: 'calculator',
add: function(a, b){
return a+b
}
}

导入


ES6 Module中使用import语法导入模块。


加载带有命名导出的模块

有两种方式



  1. import后面要跟一对大括号,将导入的变量名包裹起来。并且这些变量名要与导出的变量名完全一致。


//calculator.js
const name = 'calculator'
const add = function(a, b){return a+b}
export {name, add}

//index.js
import {name, add} from './calculator.js'
add(2,3)


  1. 采用整体导入的方式, 使用import * as myModule可以把所有导入的变量作为属性值添加到myModule中,从而减少对当前作用域的影响。


import * as calculator from './calculator.js'
console.log(calculator.add(2,3))
console.log(calculator.name)

加载默认导出的模块

import后面直接跟变量名,并且这个名字可以自由指定


//calculator.js
export default {
name: 'calculator',
add: function(a, b){
return a+b
}
}
//index.js
import calculator from './calculator.js'
calculator.add(2,3)

两种导入方式混合起来

import React, {Component} from 'react'

这里的React对应的是该模块的默认导出,Component则是其命名导出中的一个变量。


CommonJS和ES6 Module的区别


动态和静态

  • CommonJS是动态的模块结构,模块依赖关系的建立发生在代码的运行阶段
  • ES Module是静态的模块结构,在编译阶段就可以分析模块的依赖关系。


相比于CommonJS,ES6 Module有如下优势:

  1. 死代码监测和排除
  2. 模块变量和类型检查
  3. 编译器优化

值拷贝和动态映射


在导入一个模块时,对于CommonJs来说,获取的是一份导出值的拷贝。而在ES6 Module中则是值的动态映射,并且这个映射是只读的。


总结

  • CommonJS使用Module.exports或exports导出
  • CommonJS使用require()函数导入,该函数返回一个对象,包含导出的变量。
  • ES6 Module使用export导出,包括命名导出或者默认导出。
  • 命名导出是export后面跟一个大括号,括号里面包含导出的变量
  • 命名导出的另一种方式是export和变量声明在一行。
  • 默认导出是export default,只能有一个默认导出
  • ES6 Module导入使用import
  • 加载带有命名导出的模块,import后面要跟一对大括号,将导入的变量名包裹起来。并且这些变量名要与导出的变量名完全一致。
  • 采用整体导入的方式, 使用import * as 可以把所有导入的变量作为属性值添加到中,从而减少对当前作用域的影响。
  • 加载默认导出的模块,import后面直接跟变量名,并且这个名字可以自由指定。

  • 作者:邓惠子本尊

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

    收起阅读 »

    通过协程简化Activity之间的通信

    假设我们有这样一个常用的场景:有两个Activity,第一个Activity展示一段文本点击“编辑”按钮启动第二个Activity,并把这段文本当做参数传递到第二个Activity在第二个Activity编辑这个字符串编辑完成后点击保存将结果返回到第一个Act...
    继续阅读 »

    假设我们有这样一个常用的场景:

    • 有两个Activity,第一个Activity展示一段文本
    • 点击“编辑”按钮启动第二个Activity,并把这段文本当做参数传递到第二个Activity
    • 在第二个Activity编辑这个字符串
    • 编辑完成后点击保存将结果返回到第一个Activity
    • 第一个Activity展示修改后的字符串

    如下图:

    这是一个非常简单和常见的场景,我们一般通过 startActivityForResult 的方式传递参数,并在 onActivityResult 接收编辑后的结果,代码也很简单,如下:


    //第一个Activity启动编辑Activity
    btnEditByTradition.setOnClickListener {
    val content = tvContent.text.toString().trim()
    val intent = Intent(this, EditActivity::class.java).apply {
    putExtra(EditActivity.ARG_TAG_CONTENT, content)
    }
    startActivityForResult(intent, REQUEST_CODE_EDIT)
    }
    //EditActivity回传编辑后的结果
    btnSave.setOnClickListener {
    val newContent = etContent.text.toString().trim()
    setResult(RESULT_OK, Intent().apply {
    putExtra(RESULT_TAG_NEW_CONTENT, newContent)
    })
    finish()
    }
    //第一个Activity中接受编辑后的结果,并展示
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    when (requestCode) {
    REQUEST_CODE_EDIT -> {
    if (resultCode == RESULT_OK && data != null) {
    val newContent = data.getStringExtra(EditActivity.RESULT_TAG_NEW_CONTENT)
    tvContent.text = newContent
    }
    }
    else -> super.onActivityResult(requestCode, resultCode, data)
    }
    }

    那这种方式有什么缺点呢?

    1. 代码分散,可读性差
    2. 封装不彻底,调用方需要到EditActivity才能知道需要传递什么参数,类型是什么,key是什么
    3. 调用方需要知道EditActivity是如何返回的参数类型和key是什么才能正确解析
    4. 约束性差,各种常量的定义(REQUEST_CODE,PARAM_KEY等),若项目管理不严谨,重复定义,导致后期重构和维护比较麻烦

    那有没有一种方式能解决上面的缺点呢?我们期望的是:

    1. 一个对外提供某些功能的Activity应该有足够的封装性,调用者像调用普通方法一样,一行代码即可完成调用
    2. 方法的参数列表就是调用本服务需要传递的参数(参数数量,参数类型,是否必须)
    3. 方法的返回参数就是本服务的返回结果
    4. 提供服务的Activity像一个组件一样,能对外提供功能都是以一个个方法的形式体现

    通过Kotlin 协程和一个不可见的Fragment来实现。


    btnEditByCoroutine.setOnClickListener {
    GlobalScope.launch {
    val content = tvContent.text.toString().trim()

    // 调用EditActivity的 editContent 方法
    // content为要编辑的内容
    // editContent 即为编辑后的结果
    val newContent = EditActivity.editContent(this@MainActivity, content)

    if (!newContent.isNullOrBlank()) {
    tvContent.text = newContent
    }
    }
    }


    通过上面的代码,我们看到,通过一个方法即可完成调用,基本实现了上文提到的期望。 那 editContent 方法内部是如何实现的呢?看如下代码:


    /**
    * 对指定的文本进行编辑
    * @param content 要编辑的文本
    *
    * @return 可空 不为null 表示编辑后的内容 为null表示用户取消了编辑
    */

    @JvmStatic
    suspend fun editContent(activity: FragmentActivity, content: String): String? =
    suspendCoroutine { continuation ->
    val editFragment = BaseSingleFragment().apply {
    intentGenerator = {
    Intent(it, EditActivity::class.java).apply {
    putExtra(ARG_TAG_CONTENT, content)
    }
    }
    resultParser = { resultCode, data ->
    if (resultCode == RESULT_OK && data != null) {
    val result = data.getStringExtra(RESULT_TAG_NEW_CONTENT)
    continuation.resume(result)
    } else {
    continuation.resume(null)
    }
    removeFromActivity(activity.supportFragmentManager)
    }
    }
    editFragment.addToActivity(activity.supportFragmentManager)
    }

    这里需要借助一个“BaseSingleFragment”来实现,这是因为我不能违背 ActivityManagerService 的规则,依然需要通过 startActivityForResult 和 onActivityResult 来实现,所以我们这里通过一个不可见(没有界面)的 Fragment ,将这个过程封装起来,代码如下:


    class BaseSingleFragment : Fragment() {


    /**
    * 生成启动对应Activity的Intent,因为指定要启动的Activity,如何启动,传递参数,所以由具体的使用位置来实现这个Intent
    *
    * 使用者必须实现这个lambda,否则直接抛出一个异常
    */

    var intentGenerator: ((context: Context) -> Intent) = {
    throw RuntimeException("you should provide a intent here to start activity")
    }

    /**
    * 解析目标Activity返回的结果,有具体实现者解析,并回传
    *
    * 使用者必须实现这个lambda,否则直接抛出一个异常
    */

    var resultParser: (resultCode: Int, data: Intent?) -> Unit = { resultCode, data ->
    throw RuntimeException("you should parse result data yourself")
    }

    companion object {
    const val REQUEST_CODE_GET_RESULT = 100
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val context = requireContext()
    startActivityForResult(intentGenerator.invoke(context), REQUEST_CODE_GET_RESULT)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_GET_RESULT) {
    resultParser.invoke(resultCode, data)
    } else {
    super.onActivityResult(requestCode, resultCode, data)
    }
    }


    /**
    * add current fragment to FragmentManager
    */

    fun addToActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().add(this, this::class.simpleName)
    .commitAllowingStateLoss()
    }

    /**
    * remove current fragment from FragmentManager
    */

    fun removeFromActivity(fragmentManager: FragmentManager) {
    fragmentManager.beginTransaction().remove(this).commitAllowingStateLoss()
    }
    }
    当然,这是一个 suspend 方法,java是不支持协程的,而现实情况是,很多项目都有中途集成Kotlin的,有很多遗留的java代码,对于这种情况,我们需要提供相应的java实现吗?The answer is no. Java 代码同样可以调用 suspend 方法,调用方式如下:
    btnEditByCoroutine.setOnClickListener((view) -> {
    String content = tvContent.getText().toString().trim();
    EditActivity.editContent(MainActivityJava.this, content, new Continuation<String>() {
    @NotNull
    @Override
    public CoroutineContext getContext() {
    return EmptyCoroutineContext.INSTANCE;
    }

    @Override
    public void resumeWith(@NotNull Object o) {
    String newContent = (String) o;
    if (!TextUtils.isEmpty(content)) {
    tvContent.setText(newContent);
    }
    }
    });
    });

    虽然是通过回调的方式,在resumeWith方法中来接受结果,但也是比 startActivityForResult 的方式要好的多。

    Perfect!!!

    这种实现方式的灵感是来源于 RxPermission 对权限申请流程的实现,在此对 RxPermission 表达感谢。 另外 Glide 3.X 版本对图片加载任务的启动,暂停,和取消和Activity的和生命周期绑定也是通过向FragmentManager中添加了一个隐藏的Fragment来实现的。 这个demo的代码在

    CourtineTest GitHub ,

    原文链接:https://juejin.cn/post/7033598140766912549?utm_source=gold_browser_extension

    收起阅读 »

    屏幕旋转切换机制详解

    前言 屏幕旋转的机制; 默认情况下,当用户手机的重力感应器打开后,旋转屏幕方向,会导致当前activity发生onDestroy-> onCreate,这样会重新构造当前activity和界面布局,如果在Camera界面,则表现为卡顿或者黑屏一段时间; 今天...
    继续阅读 »

    前言



    屏幕旋转的机制;


    默认情况下,当用户手机的重力感应器打开后,旋转屏幕方向,会导致当前activity发生onDestroy-> onCreate,这样会重新构造当前activity和界面布局,如果在Camera界面,则表现为卡顿或者黑屏一段时间;


    今天就介绍下平面旋转方面的知识点;



    一、screenOrientation属性说明


    android:screenOrientation属性说明:



    • unspecified,默认值,由系统决定,不同手机可能不一致

    • landscape,强制横屏显示,只有一个方向

    • portrait,强制竖屏显,只有一个方向

    • behind,与前一个activity方向相同

    • sensor,根据物理传感器方向转动,用户90度、180度、270度旋转手机方向,activity都更着变化,会重启activity(无论系统是否设置为自动转屏,activity页面都会跟随传感器而转屏)

    • sensorLandscape,横屏旋转,就是可以上下旋转,有两个方向,不会重启activity

    • sensorPortrait,竖屏旋转,就是可以上下旋转,有两个方向,不会重启activity

    • nosensor,旋转设备时候,界面不会跟着旋转。初始化界面方向由系统控制(无论系统是否设置为自动转屏,activity页面都不会转屏)

    • user,用户当前设置的方向

    • reverseLandscape,与正常的横向方向相反显示(反向横屏)

    • reversePortrait,与正常的纵向方向相反显示(反向竖屏)(我设置没用)


    二、屏幕旋转详解


    1、配置文件设置



    • AndroidManifest.xml设置;

    • 横屏或者竖屏是被直接定死,旋转方向不会变化,只有一个方向(意思是旋转180°也不会改变),当然就不会在手机旋转的时候重启activity;


      
    
                android:name=".test1"

                android:screenOrientation="landscape" />

           
                android:name=".test2"

                android:screenOrientation="portrait" />

    2、代码设置


    调用setRequestedOrientation()函数,其效果就是和在


    AndroidManifest中设置一样的,当前方向和设置的方向不一致的时候,会重启activity,一致的话不会重启;


    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);//横屏设置

    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//竖屏设置

    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);//默认设置

    注意点:


    不想activity被重启,可以在AndroidManifest中加上android:configChanges(orientation|screenSize这两个一定要加上)


     
    
                android:name=".MainActivity"

                android:screenOrientation="sensor"

                android:configChanges="keyboardHidden|orientation|screenSize">

    3、监听屏幕旋转变化


    重写onConfigurationChanged方法


        @Override

        public void onConfigurationChanged(Configuration newConfig) {

            super.onConfigurationChanged(newConfig);

            Log.d(TAG, "onConfigurationChanged");

        }

    这个方法将会在屏幕旋转变化时调用,可以在这里做出我们在屏幕变化时想要的操作,并且不会重启activity。但它只能一次旋转90度,如果一下子旋转180度,onConfigurationChanged函数不会被调用;


    4、自定义旋转监听设置


    想更加完美,更加完全的掌控监听屏幕旋转变化,就的自定义旋转监听


    (1)创建一个类继承OrientationEventListener


    (2)开启和关闭监听


    可以在 activity 中创建MyOrientationDetector 类的对象,注意,监听的开启的关闭,是由该类的父类的 enable() 和 disable() 方法实现的;


    因此,可以在activity的 onResume() 中调用MyOrientationDetector 对象的 enable方法,在 onPause() 中调用MyOrientationDetector 对象的 disable方法来完车功能;


    (3)监测指定的屏幕旋转角度


    MyOrientationDetector类的onOrientationChanged 参数orientation是一个从0~359的变量,如果只希望处理四个方向,加一个判断即可:


     OrientationEventListener mOrientationListener;

        @Override

        public void onCreate(Bundle savedInstanceState) {

            super.onCreate(savedInstanceState);

            setContentView(R.layout.main);

            mOrientationListener = new OrientationEventListener(this,

                SensorManager.SENSOR_DELAY_NORMAL) {

                @Override

                public void onOrientationChanged(int orientation) {

                if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {

        return;  //手机平放时,检测不到有效的角度

    }

    //只检测是否有四个角度的改变

    if (orientation > 350 || orientation < 10) { //0度

        orientation = 0;

    } else if (orientation > 80 && orientation < 100) { //90度

        orientation = 90;

    } else if (orientation > 170 && orientation < 190) { //180度

        orientation = 180;

    } else if (orientation > 260 && orientation < 280) { //270度

        orientation = 270;

    } else {

        return;

    }

    Log.v(DEBUG_TAG,"Orientation changed to " + orientation);

                }

            };

           if (mOrientationListener.canDetectOrientation()) {

               Log.v(DEBUG_TAG, "Can detect orientation");

               mOrientationListener.enable();

           } else {

               Log.v(DEBUG_TAG, "Cannot detect orientation");

               mOrientationListener.disable();

           }

        }

        @Override

        protected void onDestroy() {

            super.onDestroy();

            mOrientationListener.disable();

        }

    总结


    快年底了,很多人都要找工作或者写毕业设计,有不懂就发私信给我,或许可以给你点帮助建议;


    我们一起努力进步;


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

    如何从性能角度选择数组的遍历方式

    前言 本文讲述了JS常用的几种数组遍历方式以及性能分析对比。 如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~ 数组的方法 JavaScript发展到现在已经提供了许多数组的方法,下面这张图涵盖...
    继续阅读 »

    前言


    本文讲述了JS常用的几种数组遍历方式以及性能分析对比。


    如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~


    敖丙.png


    数组的方法


    JavaScript发展到现在已经提供了许多数组的方法,下面这张图涵盖了数组大部分的方法,这篇文章主要说一说数组的遍历方法,以及各自的性能,方法这么多,如何挑选性能最佳的方法对我们的开发有非常大的帮助。


    数组.png


    数组遍历的方法


    for


    标准的for循环语句,也是最传统的循环语句


    var arr = [1,2,3,4,5]
    for(var i=0;i<arr.length;i++){
    console.log(arr[i])
    }

    最简单的一种遍历方式,也是使用频率最高的,性能较好,但还能优化


    优化版for循环语句


    var arr = [1,2,3,4,5]
    for(var i=0,len=arr.length;i<len;i++){
    console.log(arr[i])
    }

    使用临时变量,将长度缓存起来,避免重复获取数组长度,尤其是当数组长度较大时优化效果才会更加明显。


    这种方法基本上是所有循环遍历方法中性能最高的一种


    forEach


    普通forEach


    对数组中的每一元素运行给定的函数,没有返回值,常用来遍历元素


    var arr5 = [10,20,30]
    var result5 = arr5.forEach((item,index,arr)=>{
    console.log(item)
    })
    console.log(result5)
    /*
    10
    20
    30
    undefined 该方法没有返回值
    */

    数组自带的foreach循环,使用频率较高,实际上性能比普通for循环弱


    原型forEach


    由于foreach是Array型自带的,对于一些非这种类型的,无法直接使用(如NodeList),所以才有了这个变种,使用这个变种可以让类似的数组拥有foreach功能。


    const nodes = document.querySelectorAll('div')
    Array.prototype.forEach.call(nodes,(item,index,arr)=>{
    console.log(item)
    })

    实际性能要比普通foreach弱


    for...in


    任意顺序遍历一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。


    一般常用来遍历对象,包括非整数类型的名称和继承的那些原型链上面的属性也能被遍历。像 Array和 Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性就不能遍历了.


    var arr = [1,2,3,4,5]
    for(var i in arr){
    console.log(i,arr[i])
    } //这里的i是对象属性,也就是数组的下标
    /**
    0 1
    1 2
    2 3
    3 4
    4 5 **/

    大部分人都喜欢用这个方法,但它的性能却不怎么好


    for...of(不能遍历对象)



    在可迭代对象(具有 iterator 接口)(Array,Map,Set,String,arguments)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句,不能遍历对象



    let arr=["前端","南玖","ssss"];
    for (let item of arr){
    console.log(item)
    }
    //前端 南玖 ssss

    //遍历对象
    let person={name:"南玖",age:18,city:"上海"}
    for (let item of person){
    console.log(item)
    }
    // 我们发现它是不可以的 我们可以搭配Object.keys使用
    for(let item of Object.keys(person)){
    console.log(person[item])
    }
    // 南玖 18 上海

    这种方式是es6里面用到的,性能要好于forin,但仍然比不上普通for循环


    map



    map: 只能遍历数组,不能中断,返回值是修改后的数组。



    let arr=[1,2,3];
    const res = arr.map(item=>{
    return item+1
    })
    console.log(res) //[2,3,4]
    console.log(arr) // [1,2,3]

    every


    对数组中的每一运行给定的函数,如果该函数对每一项都返回true,则该函数返回true


    var arr = [10,30,25,64,18,3,9]
    var result = arr.every((item,index,arr)=>{
    return item>3
    })
    console.log(result) //false

    some


    对数组中的每一运行给定的函数,如果该函数有一项返回true,就返回true,所有项返回false才返回false


    var arr2 = [10,20,32,45,36,94,75]
    var result2 = arr2.some((item,index,arr)=>{
    return item<10
    })
    console.log(result2) //false

    reduce


    reduce()方法对数组中的每个元素执行一个由你提供的reducer函数(升序执行),将其结果汇总为单个返回值


    const array = [1,2,3,4]
    const reducer = (accumulator, currentValue) => accumulator + currentValue;

    // 1 + 2 + 3 + 4
    console.log(array1.reduce(reducer));

    filter


    对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组


    // filter  返回满足要求的数组项组成的新数组
    var arr3 = [3,6,7,12,20,64,35]
    var result3 = arr3.filter((item,index,arr)=>{
    return item > 3
    })
    console.log(result3) //[6,7,12,20,64,35]

    性能测试


    工具测试


    使用工具测试性能分析结果如下图所示


    性能测试1.png


    手动测试


    我们也可以自己用代码测试:


    //测试函数
    function clecTime(fn,fnName){
    const start = new Date().getTime()
    if(fn) fn()
    const end = new Date().getTime()
    console.log(`${fnName}执行耗时:${end-start}ms`)
    }

    function forfn(){
    let a = []
    for(var i=0;i<arr.length;i++){
    // console.log(i)
    a.push(arr[i])
    }
    }
    clecTime(forfn, 'for') //for执行耗时:106ms

    function forlenfn(){
    let a = []
    for(var i=0,len=arr.length;i<len;i++){
    a.push(arr[i])
    }
    }
    clecTime(forlenfn, 'for len') //for len执行耗时:95ms

    function forEachfn(){
    let a = []
    arr.forEach(item=>{
    a.push[item]
    })
    }
    clecTime(forEachfn, 'forEach') //forEach执行耗时:201ms

    function forinfn(){
    let a = []
    for(var i in arr){
    a.push(arr[i])
    }
    }
    clecTime(forinfn, 'forin') //forin执行耗时:2584ms (离谱)

    function foroffn(){
    let a = []
    for(var i of arr){
    a.push(i)
    }
    }
    clecTime(foroffn, 'forof') //forof执行耗时:221ms

    // ...其余可自行测试

    结果分析


    经过工具与手动测试发现,结果基本一致,数组遍历各个方法的速度:传统的for循环最快,for-in最慢



    for-len > for > for-of > forEach > map > for-in



    javascript原生遍历方法的建议用法:



    • for循环遍历数组

    • for...in遍历对象

    • for...of遍历类数组对象(ES6)

    • Object.keys()获取对象属性名的集合


    为何for… in会慢?


    因为for … in语法是第一个能够迭代对象键的JavaScript语句,循环对象键({})与在数组([])上进行循环不同,引擎会执行一些额外的工作来跟踪已经迭代的属性。因此不建议使用for...in来遍历数组



    作者:南玖
    链接:https://juejin.cn/post/7033578966887694373

    收起阅读 »

    async/await 优雅永不过时

    引言 async/await是非常棒的语法糖,可以说他是解决异步问题的最终解决方案。从字面意思来理解。async 是异步的意思,而 await 是 等待 ,所以理解 async用于申明一个function是异步的,而 await 用于等待一个异步方法执行完成...
    继续阅读 »

    引言



    async/await是非常棒的语法糖,可以说他是解决异步问题的最终解决方案。从字面意思来理解。async 是异步的意思,而 await 是 等待 ,所以理解 async用于申明一个function是异步的,而 await 用于等待一个异步方法执行完成。



    src=http___pic.962.net_up_2019-12_15767448543514326.png&refer=http___pic.962.jpeg


    async作用



    async声明function是一个异步函数,返回一个promise对象,可以使用 then 方法添加回调函数。async函数内部return语句返回的值,会成为then方法回调函数的参数。



    async function test() {
    return 'test';
    }
    console.log(test); // [AsyncFunction: test] async函数是[`AsyncFunction`]构造函数的实例
    console.log(test()); // Promise { 'test' }

    // async返回的是一个promise对象
    test().then(res=>{
    console.log(res); // test
    })

    // 如果async函数没有返回值 async函数返回一个undefined的promise对象
    async function fn() {
    console.log('没有返回');
    }
    console.log(fn()); // Promise { undefined }

    // 可以看到async函数返回值和Promise.resolve()一样,将返回值包装成promise对象,如果没有返回值就返回undefined的promise对象

    await



    await 操作符只能在异步函数 async function 内部使用。如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果。如果等待的不是 Promise 对象,则返回该值本身。



    async function test() {
    return new Promise((resolve)=>{
    setTimeout(() => {
    resolve('test 1000');
    }, 1000);
    })
    }
    function fn() {
    return 'fn';
    }

    async function next() {
    let res0 = await fn(),
    res1 = await test(),
    res2 = await fn();
    console.log(res0);
    console.log(res1);
    console.log(res2);
    }
    next(); // 1s 后才打印出结果 为什么呢 就是因为 res1在等待promise的结果 阻塞了后面代码。

    错误处理



    如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject



    async function test() {
    await Promise.reject('错误了')
    };

    test().then(res=>{
    console.log('success',res);
    },err=>{
    console.log('err ',err);
    })
    // err 错误了


    防止出错的方法,也是将其放在try...catch代码块之中。



    async function test() {
    try {
    await new Promise(function (resolve, reject) {
    throw new Error('错误了');
    });
    } catch(e) {
    console.log('err', e)
    }
    return await('成功了');
    }


    多个await命令后面的异步操作,如果不存在继发关系(即互不依赖),最好让它们同时触发。



    let foo = await getFoo();
    let bar = await getBar();
    // 上面这样写法 getFoo完成以后,才会执行getBar

    // 同时触发写法 ↓

    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;

    async/await优点



    async/await的优势在于处理由多个Promise组成的 then 链,在之前的Promise文章中提过用then处理回调地狱的问题,async/await相当于对promise的进一步优化。
    假设一个业务,分多个步骤,且每个步骤都是异步的,而且依赖上个步骤的执行结果。



    // 假设表单提交前要通过俩个校验接口

    async function check(ms) { // 模仿异步
    return new Promise((resolve)=>{
    setTimeout(() => {
    resolve(`check ${ms}`);
    }, ms);
    })
    }
    function check1() {
    console.log('check1');
    return check(1000);
    }
    function check2() {
    console.log('check2');
    return check(2000);
    }

    // -------------promise------------
    function submit() {
    console.log('submit');
    // 经过俩个校验 多级关联 promise传值嵌套较深
    check1().then(res1=>{
    check2(res1).then(res2=>{
    /*
    * 提交请求
    */
    })
    })
    }
    submit();

    // -------------async/await-----------
    async function asyncAwaitSubmit() {
    let res1 = await check1(),
    res2 = await check2(res1);
    console.log(res1, res2);
    /*
    * 提交请求
    */
    }



    原理



    async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。



    async function fn(args) {
    // ...
    }

    // 等同于

    function fn(args) {
    return spawn(function* () {
    // ...
    });
    }

    /*
    * Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。
    * 异步操作需要暂停的地方,都用 yield 语句注明
    * 调用 Generator 函数,返回的是指针对象(这是它和普通函数的不同之处),。调用指针对象的 next 方法,会移动内部指针。
    * next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
    */

    // 了解generator的用法
    function* Generator() {
    yield '1';
    yield Promise.resolve(2);
    return 'ending';
    }

    var gen = Generator(); // 返回指针对象 Object [Generator] {}

    let res1 = gen.next();
    console.log(res1); // 返回当前阶段的值 { value: '1', done: false }

    let res2 = gen.next();
    console.log(res2); // 返回当前阶段的值 { value: Promise { 2 }, done: false }

    res2.value.then(res=>{
    console.log(res); // 2
    })

    let res3 = gen.next();
    console.log(res3); // { value: 'ending', done: true }

    let res4 = gen.next();
    console.log(res4); // { value: undefined, done: true }



    Generator实现async函数



    // 接受一个Generator函数作为参数
    function spawn(genF) {
    // 返回一个函数
    return function() {
    // 生成指针对象
    const gen = genF.apply(this, arguments);
    // 返回一个promise
    return new Promise((resolve, reject) => {
    // key有next和throw两种取值,分别对应了gen的next和throw方法
    // arg参数则是用来把promise resolve出来的值交给下一个yield
    function step(key, arg) {
    let result;

    // 监控到错误 就把promise给reject掉 外部通过.catch可以获取到错误
    try {
    result = gen[key](arg)
    } catch (error) {
    return reject(error)
    }

    // gen.next() 返回 { value, done } 的结构
    const { value, done } = result;

    if (done) {
    // 如果已经完成了 就直接resolve这个promise
    return resolve(value)
    } else {
    // 除了最后结束的时候外,每次调用gen.next()
    return Promise.resolve(
    // 这个value对应的是yield后面的promise
    value
    ).then((val)=>step("next", val),(err) =>step("throw", err))
    }
    }
    step("next")
    })
    }
    }


    测试



    function fn(nums) {
    return new Promise(resolve => {
    setTimeout(() => {
    resolve(nums)
    }, 1000)
    })
    }
    // async 函数
    async function testAsync() {
    let res1 = await fn(1);
    console.log(res1); // 1
    let res2 = await fn(2);
    console.log(res2); // 2
    return res2;
    }
    let _res = testAsync();
    console.log('testAsync-res',_res); // Promise
    _res.then(v=>console.log('testAsync-res',v)) // 2

    // Generator函数
    function* gen() {
    let res1 = yield fn(3);
    console.log(res1); // 3
    let res2 = yield fn(4);
    console.log(res2); // 4
    // let res3 = yield Promise.reject(5);
    // console.log(res3);
    return res2;
    }

    let _res2 = spawn(gen)();
    console.log('gen-res',_res2); // Promise

    _res2
    .then(v=>console.log('gen-res',v)) // 4
    .catch(err=>{console.log(err)}) // res3 执行会抛出异常



    总结



    async/await语法糖可以让异步代码变得更清晰,可读性更高,所以快快卷起来吧。Generator有兴趣的可以了解一下。


    作者:小撕夜
    链接:https://juejin.cn/post/7033647059378896903

    收起阅读 »

    当老婆又让我下载一个腾讯视频时

    我们结婚了! 是的,这次不是女朋友啦,是老婆了! 时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,...
    继续阅读 »

    我们结婚了!


    是的,这次不是女朋友啦,是老婆了!


    WechatIMG58.jpeg


    时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,上一期很多朋友给我推荐了各种工具,这里我没有一一查看,我可以列举出来,有需要的同学可以尝试看看,不想尝试的也可以看看我下面为了偷懒准备的方法。


    心路历程


    最初,我是想着把我之前的步骤,用无头浏览器加载一遍,然后用代码去下载ts片段,然后在机器上用ffmpeg进行合并,但是仿佛还是有些许麻烦,然后我就去npm搜了一下关键词:m3u8tomp4


    image.png


    m3u8-to-mp4


    于是我点击了第一个包:m3u8-to-mp4


    image.png


    纳尼?这个包就一个版本,用了3年,而且周下载量还不少


    image.png


    于是我想着这个包要么就是很牛逼,一次性解决了m3u8转mp4的问题,一劳永逸,所以3年没更新过了,要么就是作者忘记了自己还有这个包


    于是我就用了这个3年没人维护没人更新的包。


    用法也很简单,就copy example 就好了。代码如下:



    var m3u8ToMp4 = require("m3u8-to-mp4");
    var converter = new m3u8ToMp4();
    (async function() {
    var url = "https://apd-666945ea97106754c57813479384d30c.v.smtcdns.com/omts.tc.qq.com/AofRtrergNwkAhpHs4RrxH2_9DWLWSG8xjDMZDQoFGyY/uwMROfz2r55kIaQXGdGnC2deOm68BrdPrRewQlOzrMAbixNO/svp_50001/cKAgRbCb6Re4BpHkI-IlK_KN1VJ8gQVK2sZtkHEY3vQUIlxVz7AtWmVJRifZrrPfozBS0va-SSJFhQhOFSKVNmqVi165fCQJoPl8V5QZBcGZBDpSIfrpCImJKryoZOdR5C0oGYkzIW77I4his7UkPY9Iwmf1QWjaHwNV2hpKv3aD9ysL_-YByA/szg_9276_50001_0bc3uuaa2aaafmaff4e3ijqvdjodbwsqadka.f304110.ts.m3u8?ver=4"
    await converter
    .setInputFile(url)
    .setOutputFile("dummy.mp4")
    .start();
    console.log("File converted");
    })();

    视频地址是 v.qq.com/x/page/v331…


    然后视频就转换成功了,哇哦!


    so easy ! so beautiful!


    原理


    带着好奇,我想看下这个包是如何进行转换的


    于是我点进去m3u8-to-mp4这个包文件


    包文件内容如下


    image.png


    只有一个文件?


    然后我打开了index.js ,只有64行😂


    全部代码如下


    /**
    * @description M3U8 to MP4 Converter
    * @author Furkan Inanc
    * @version 1.0.0
    */

    let ffmpeg = require("fluent-ffmpeg");

    /**
    * A class to convert M3U8 to MP4
    * @class
    */
    class m3u8ToMp4Converter {
    /**
    * Sets the input file
    * @param {String} filename M3U8 file path. You can use remote URL
    * @returns {Function}
    */
    setInputFile(filename) {
    if (!filename) throw new Error("You must specify the M3U8 file address");
    this.M3U8_FILE = filename;

    return this;
    }

    /**
    * Sets the output file
    * @param {String} filename Output file path. Has to be local :)
    * @returns {Function}
    */
    setOutputFile(filename) {
    if (!filename) throw new Error("You must specify the file path and name");
    this.OUTPUT_FILE = filename;

    return this;
    }

    /**
    * Starts the process
    */
    start() {
    return new Promise((resolve, reject) => {
    if (!this.M3U8_FILE || !this.OUTPUT_FILE) {
    reject(new Error("You must specify the input and the output files"));
    return;
    }

    ffmpeg(this.M3U8_FILE)
    .on("error", error => {
    reject(new Error(error));
    })
    .on("end", () => {
    resolve();
    })
    .outputOptions("-c copy")
    .outputOptions("-bsf:a aac_adtstoasc")
    .output(this.OUTPUT_FILE)
    .run();
    });
    }
    }

    module.exports = m3u8ToMp4Converter;


    大致看了下这个包做的内容,就是检测并设置了输入链接,和输出文件名,然后调用了fluent-ffmpeg这个库


    ???


    站在巨人的肩膀上吗,自己就包了一层😂


    接着看fluent-ffmpeg这个包,是如何实现转换的


    image.png


    然后我们在这个包文件夹下面搜索.run方法,用来定位到具体执行的地方


    image.png


    凭借多年的cv经验,感觉应该是processor.js这个文件里的,然后我们打开这个文件,定位到该方法处


    image.png


    往下看代码,我注意到了这段代码


    image.png


    因为都是基于ffmpeg这个大爹来做的工具,所以最底层也都是去调用ffmpeg的command


    image.png


    这几个if判断都是对结果进行捕获异常,那么我们在这个核心代码的地方打个端点看下


    image.png


    貌似是调用了几个命令行参数


    于是我就有了一个大胆的想法!


    image.png


    是的,我手动在终端将这个命令拼接起来,用我的本地命令去跑应该也没问题的吧,于是我尝试了一下


    image.png


    没想到还成功了,其实成功是必然的,因为都是借助来ffmpeg这个包,只不过我是手动去操作,框架是代码去拼接这个命令而已


    剩余的时间里,我看了看fluent-ffmpeg的其他代码,它做的东西比较多,比如去本查找ffmpeg的绝对路径啊,对ffmpeg的结果进行捕获异常信息等...



    作者:小松同学哦
    链接:https://juejin.cn/post/7033652317958176799

    收起阅读 »

    为什么 MySQL 不推荐使用 join?

     1. 对于 mysql,不推荐使用子查询和 join 是因为本身 join 的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引 单表取数据,然后在程序里面做 join,merge 数据。   2. 子查询就更别用了,效率太差,执行子查询时,M...
    继续阅读 »

     1. 对于 mysql,不推荐使用子查询和 join 是因为本身 join 的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引


    单表取数据,然后在程序里面做 join,merge 数据。


      2. 子查询就更别用了,效率太差,执行子查询时,MYSQL 需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会


    受到一定的影响,这里多了一个创建和销毁临时表的过程。


      3. 如果是 JOIN 的话,它是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是 OK 的。大的话


    业务逻辑中可以控制处理。


      4. 数据库是最底层的,瓶颈往往是数据库。建议数据库只是作为数据 store 的工具,而不要添加业务上去。


      让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。如果关联中的某个表发生了变化,那么就无法使用查


    询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。


      将查询分解后,执行单个查询可以减少锁的竞争。


      在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。


      查询本身效率也可能会有所提升。查询 id 集的时候,使用 IN()代替关联查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的关联要更高效。


      可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需


    要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消艳。


      更进一步,这样做相当于在应用中实现了哈希关联,而不是使用 MySQL 的嵌套循环关联。某些场景哈希关联的效率要高很多。


    当应用能够方便地缓存单个查询的结果的时候


    当可以将数据分布到不同的 MySQL 服务器上的时候


    当能够使用 IN()的方式代替关联查询的时候


    并发场景多,DB 查询频繁,需要分库分表


    1.DB 承担的业务压力大,能减少负担就减少。当表处于百万级别后,join 导致性能下降;


    2. 分布式的分库分表。这种时候是不建议跨库 join 的。目前 mysql 的分布式中间件,跨库 join 表现不良。


    3. 修改表的 schema,单表查询的修改比较容易,join 写的 sql 语句要修改,不容易发现,成本比较大,当系统比较大时,不好维护。


      在业务层,单表查询出数据后,作为条件给下一个单表查询。也就是子查询。 会担心子查询出来的结果集太多。mysql 对 in 的数量没有限制,但是


      mysql 限制整条 sql 语句的大小。通过调整参数 max_allowed_packet ,可以修改一条 sql 的最大值。建议在业务上做好处理,限制一次查询出来的结果集是能接受的。


      关联查询的好处时候可以做分页,可以用副表的字段做查询条件,在查询的时候,将副表匹配到的字段作为结果集,用主表去 in 它,但是问题来了,如果匹配到的数据量太大就不行了,也会导致返回的分页记录跟实际的不一样,解决的方法可以交给前端,一次性查询,让前端分批显示就可以了,这种解决方案的前提是数据量不太,因为 sql 本身长度有限。


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

    我的Android开发之旅(一):BaseActivity的浅入之简单封装 Toolbar

    为什么要写BaseAcivity 我们都知道在做Android应用开发的时候都需要创建一个Activity,但很多时候我们的程序有多个界面并且每个界面都有相似的内容(例如:Toolbar、DrawerLayout)和后台的操作有共同的方法,这个时候我们写一个B...
    继续阅读 »

    为什么要写BaseAcivity


    我们都知道在做Android应用开发的时候都需要创建一个Activity,但很多时候我们的程序有多个界面并且每个界面都有相似的内容(例如:Toolbar、DrawerLayout)和后台的操作有共同的方法,这个时候我们写一个BaseActivity作为每一个Activity的基类,统一管理程序中的每个Activity。


    一行代码实现 Toolbar 效果


    activity_main.xml 的代码


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:background="@android:color/holo_blue_light">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我是MainActivity的界面"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </LinearLayout>

    MainAcitvity.java 的代码


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }
    }

    在这里插入图片描述

    在上面的 activity_main.xml 中可以看出,父布局设置了背景颜色和里面只有一个TextView,并没图中的Toolbar。那到底是为什么呢?其实细心观察的小伙伴们会发现,怎么MainActivity中的代码和平常不一样呢?onCreate()方法呢?别急,我们重头开始!


    “少啰嗦,先看东西”




    • 创建 BaseActivity


      在项目创建后,我们可以看到AndroidStudio自动帮我们生成了MainActivity.java和activity_main.xml文件,然后我们再创建一个新的Activity,命名为BaseActivity。

      在这里插入图片描述




    • 修改 activity_base.xml 文件


      接着打开activity_base.xml文件,把父布局的ConstraintLayout换成垂直的LinearLayout(其实也可以不换的,主要是我喜欢用LinearLayout),并在里面添加两个元素Toolbar和FrameLayout。



      注意:由于Toolbar代替 ActionBar,所以先把 ActionBar 去掉,我们通过设置 Application 的 theme 来隐藏,这样项目中所有的界面的 ActionBar 就都隐藏了。





    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".BaseActivity"
    android:orientation="vertical">

    <androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    </FrameLayout>

    </LinearLayout>


    • 修改 BaseActivity.java 文件


      接下来打开 BaseActivity.java 文件,让 BaseActivity 继承 AppCompatActivity ,修改代码如下



      注意:protected abstract int getContentView(); 是一个抽象方法,所以我们要将 BaseActivity 修改成抽象类。为什么要修改成抽象类呢?原因很简单,因为一个类里是不允许有一个抽象方法的,如要有抽象方法,那这个类就必须是抽象类。那可能你又会问,为什么要用抽象方法呢?(你是十万个为什么吗?哪来的那么多为什么)因为我们想让其他 Activity 的界面显示到 BaseActivity 中,那这个方法是必须要实现的,如果设置成普通的方法的话,我们很有可能在写代码的时候忘记了调用了这个方法,导致界面不显示。所以我们得用抽象方法,这样每个 Activity 继承这个 BaseActivity 的时候就必须覆写 getContentView() 这个方法。





    public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base);
    initView();
    }

    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle("我是BaseActivity的Toolbar");
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    }

    /**
    * 获取要显示内容的布局文件的资源id
    *
    * @return 显示的内容界面的资源id
    */
    protected abstract int getContentView();

    }

    • 修改 activity_main.xml 文件
      打开 activity_main.xml 文件,然后我们将父布局的背景颜色修改一下,方便我们辨别到底是 MainActivity 的布局文件还是 BaseActivity 的布局文件。再添加添加一个 TextView ,原因也是和修改背景颜色是一样的。


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:background="@android:color/holo_blue_light">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我是MainActivity的界面"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </LinearLayout>

    • 修改 MainActivity.java 文件
      让 MainActivity 继承 BaseActivity 并覆写 getContentView() 方法,然后删除onCreate()方法。通过 getContentView() 方法返回当前的布局资源ID给 BaseActivity,让 BaseActivity 加载布局文件。


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }
    }


    • 运行项目


      现在你运行一下项目,我们并没有在 MainActivity 的布局中添加 ToolBar,但是运行出来的效果是 Toolbar 已经存在了。

      在这里插入图片描述

      现在就能做到用一行代码实现 Toolbar 的效果。那你现在可能就会有疑问,如果我像对 Toolbar 修改标题和添加按钮呢?其实也简单,我们继续往下看。




    • 修改标题
      我们在 BaseActivity 中再添加一个抽象方法,并在初始化 Toolbar 那一处调用我们写的这个抽象方法。




    public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_base);
    initView();
    }

    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle(setTitle());
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    }

    /**
    * 获取要显示内容的布局文件的资源id
    *
    * @return 显示的内容界面的资源id
    */
    protected abstract int getContentView();

    /**
    * 设置标题
    *
    * @return 要显示的标题名称
    */
    protected abstract String setTitle();

    }

    • 修改 MainActivity.java
      我们还是像刚才一样覆写 setTitle() 方法,并在返回值输入我们想要显示的标题


    public class MainActivity extends BaseActivity {

    @Override
    protected int getContentView() {
    return R.layout.activity_main;
    }

    @Override
    protected String setTitle() {
    return "我是MainActivity的标题";
    }
    }

    这个时候再运行以下你的程序就会出现你设置的标题了。

    在这里插入图片描述
    那么现在你可能又会问了,如果我想对 Toolbar 添加一个返回按钮,并能对他进行操作应该怎么办?(我不想写了,你也别问了!)其实很简单,在 BaseActivity 里自定义一个接口,在子类中设置这个接口的实例就行。




    • 显示返回按钮
      我们先给 Toolbar 显示返回按钮,通过 getSupportActionBar() 得到 ActionBar 的实例,再调用 ActionBar 的 setDisplayHomeAsUpEnabled(true) 方法让返回按钮显示出来。



      Toolbar 最左侧的按钮是叫做HomeAsUp,默认是隐藏的,并且它的图标是一个返回箭头。还有一种 setNavigationcon() 方法也能设置图标,具体可以查找 Toolbar 的文档说明





    private void initView() {
    // 绑定控件
    Toolbar toolbar = findViewById(R.id.toolbar);
    FrameLayout container = findViewById(R.id.container);
    // 初始化设置Toolbar
    toolbar.setTitle(setTitle());
    setSupportActionBar(toolbar);
    // 将继承了BaseActivity的布局文件解析到 container 中,这样 BaseActivity 就能显示 MainActivity 的布局文件了
    LayoutInflater.from(this).inflate(getContentView(), container);
    // 显示返回按钮
    ActionBar actionBar = getSupportActionBar();
    if (actionBar != null) {
    actionBar.setDisplayHomeAsUpEnabled(true);
    }
    // 初始化
    init();
    }


    • 自己定义一个接口并声明
      这里我就用截图显示代码片段

      在这里插入图片描述




    • 设置监听事件
      打开 MainActivity.java 文件,覆写 init() 方法,并调用父类的 setBackOnClickListener() 方法。



      这里我用了 lambda 表达式,这是 java 8 才支持的,默认项目是不支持的,你在 build.gradle 中需要声明一下。





    	@Override
    protected void init() {
    setBackOnClickListener(() ->
    Toast.makeText(this, "点击了一下返回按钮", Toast.LENGTH_SHORT).show()
    );
    }

    • 运行app
      在这里插入图片描述


    最后


    相信你对 BaseActivity 有了一些简单的了解了,具体如何使用还是得看你的项目,不是项目里就一定要写 BaseActivity 和所有 Activity 都要继承 BaseActivity ,我只是将我所理解的 BaseActivity 和大家分享一下。可能你看完了这一篇文章发现还是没能理解,在这里我想说声抱歉,可能有些地方讲的不够通俗易懂或是讲解有误,还请您多多指教,我会虚心接受并及时改正(就算是讲错了我也不会改)。



    Demo的Github地址:
    github.com/lmx0206/Bas…
    Demo的Gitee地址:
    gitee.com/Leungmx/Bas…


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

    Android 包大小优化实践

    android减少包大小是非常必要的,在性能,转换率等等都有益处,而常用的包大小优化Google已经给出了一些方案,再加上市面上的一些美团方案,微信方案、抖音方案等等,下面就说一下我们在包大小优化做的努力。 1、使用AAB模式 google play现在强制所...
    继续阅读 »

    android减少包大小是非常必要的,在性能,转换率等等都有益处,而常用的包大小优化Google已经给出了一些方案,再加上市面上的一些美团方案,微信方案、抖音方案等等,下面就说一下我们在包大小优化做的努力。


    1、使用AAB模式


    google play现在强制所有上传的应用都使用aab,Google Play 会使用您的 AAB 针对每种设备配置生成并提供经过优化的 APK,因此只会下载特定设备所需的代码和资源来运行您的应用。假如一个AAB是90MB在google play上下载耗费的流量可能也就50MB,但是这种方案对性能上没有任何的影响只是减少了下载流量可能会增加一些转换率。具体文档可以参考官方文档。这里有必要说一下AAB还有更多又去的玩法比如使用AAB实现插件化(对模块拆分还是非常有帮助的),对不同地区实现不同的业务然后使用google play进行分发


    2、使用AGP配置来减少包大小(链接


    使用lint本地检测无用资源或者开启shrinkResources


    使用lint本地检查无用资源


    1、点击AS上的Analyze菜单按钮,选择Run Inspection by Name 如下图


    image.png
    2、会出现一个弹窗, 输入unused resources


    image.png
    3、会弹出“inspaction scope”选择窗口,选择检查的范围,一般选择整个项目或模块。“inspaction scope”窗口下面还可以设置文件过滤,选择好后点ok就开始检查了


    image.png
    4、下面的输出栏会输出没有用的资源文件。


    image.png
    5、删除无用资源


    开启shrinkResources


    android {
        ...
        buildTypes {
            release {
                shrinkResources true
                minifyEnabled true
                proguardFiles
                    getDefaultProguardFile('proguard-android-optimize.txt'),
                    'proguard-rules.pro'
            }
        }
    }
    复制代码

    此配置必须和代码压缩一起使用才有效果,如果说要保留某些资源,假如插件化里面宿主里面放了某个资源需要给很多个插件使用,这个时候就需要保留此资源那么就需要做如下配置:
    在项目的res目录下新建一个创建一个包含 <resources> 标记的 XML 文件,tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。例如:


    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
        tools:discard="@layout/unused2" />
    复制代码

    缩减、混淆处理和代码优化功能


    在AGP 3.4.0以上版本R8是默认编辑器,把代码编译成android所需要的dex文件,但是代码的缩减、混淆和代码优化是默认关闭的,建议在release版本应用将此功能打开可以有效的减少包体积。上面提到的shrinkResources也是必须和此功能一起使用才有效,关于R8更多的配置可以详见官网文档
    如果要保留一些类就要在proguard-rules.pro 配置具体proguard 配置规则可以查看手册
    这里proguard特点说一下他是累加的,假如A moudle 依赖B moudle,那么到最后proguard 规则就是A moudle 的配置+B moudle的配置


    移除无用so库


    现在市面上的手机cpu指令基本上就两种了 armeabi-v7a 和 arm64-v8a,x86可以不考虑了很少有手机在用了,所以就可以通过gradle配置来只依赖v7a和v8a,例如:


    android {
    ...
    defaultConfig {
    ...
    ndk {
    // Specifies the ABI configurations of your native
    // libraries Gradle should build and package with your APK.
    abiFilters 'armeabi-v7a','arm64-v8a'
    }
    }

    }
    复制代码

    但是这样还是会很大,可以减少arm64-v8a指令集,不管你的手机cpu是v7还是v8都可以运行v7的so。
    如果说你的应用有多个变种,比如一个是上线到google play的aab一个是在国内上线的apk这样你可以使用不同变种依赖不同的指令集


    android {
    ...
    defaultConfig {
    ...
    flavorDimensions "market"
    productFlavors {

    //上google play市场
    gp {
    dimension "market"
    ndk.abiFilters "armeabi-v7a", "arm64-v8a"
    }

    //中国版本
    cn {
    dimension "market"
    ndk.abiFilters "armeabi-v7a"}
    }
    }
    复制代码

    3、美团进阶方案


    美团的这种方案主要是提到了zip压缩。dex优化,R Field的优化,我主要是用了R Field的优化。主要说下这个R field的优化这个点。这篇文章写的比较早用的是java代码插桩的方式进行处理的,现在其实AGP就可以完成一样的处理。
    先说下原理为什么R文件会导致包大小变大,假如你的项目结构如下:


    image.png


    lib1_R= lib1_R


    lib2_R= lib2_R


    lib3_R= lib3_R


    biz1_R = lib1_R + lib2_R + lib3_R + (自己的R)


    biz2_R = lib2_R + lib3_R + (自己的R)


    app_R = lib1_R + lib2_R + lib3_R + biz1_R + biz2_R + (自己的R)


    app_R因为是final的所以如果开启java优化也就是混淆会被shrink掉,但是其他moudle的R不是final而且引用也不是直接使用id的值来引用的:


    这是app moudle 下的MainActivity中setcontent对应的字节码:


    WeChata234b76e355e3899a7be1119de8a8880.png


    这是子moudle Activity中同样setContent对应的字节码:


    WeChat71e04d6bc8f6153355d695db68318246.png


    发现什么不同了吗?就是子moudle中是R的变量引用而非常量,在打包过程中aapt会将这个变量统一赋值来防止id冲突,而如果你的项目特别复杂子moudle特别多的话那么各个R类的就会特别大,但是这部分是必须的吗?好像有方法来解决


    R类内联解决方法一:


    就好像美团方案里面这种方法利用插桩在aapt分配了id后将对应的R变量的引用修改成对应的id值这样就可以把原有的R类删除掉(这个方案中的插桩插件是自己写的其实市面上有很多插桩三方库,字节的bytex,滴滴的Booster这些都可以直接使用)


    R类内联解决方法二:


    升级AGP版本到4.1以上
    image.png


    这是AGP 4.1版本的升级说明截图,他帮助咱们做了上面美团方案的插桩替换的一系列动作,R类内联是非常有必要的我们的app做了R类的内联以后apk大小减少了百分之十。这部分收益必须是在R类没有被混淆的时候keep住的前提下才可以,如果keep住了R类这部分收益就没办法了,可以在主moudle的proguard-rules.-printconfiguration "build/outputs/mapping/configuration.txt"(其实此文件的路径随便写都可以)来查看是不是添加了keep R类。如果是自己工程里面添加了keep R类就直接删了就好但是如果是三方的aar怎么办呢?


        tasks.each {
    if (it.name.startsWith("mini") && it.name.endsWith("R8")) {
    def f = it.configurationFiles.filter { File ff ->
    if (ff.exists()) {
    !ff.text.contains("-keep public class **.R\$*")
    } else {
    false
    }
    }
    it.configurationFiles.setFrom(f.files)
    }
    }
    }
    复制代码

    可以在app moudle 下的build.gradle中添加如下代码,会自动的将keep R类排除在外


    4、资源压缩


    在apk中res的资源占了很大的一部分,这部分如果可以被减少那么对减少apk size也有很大的收益,在android中主要的资源是图片,图片有几种格式jpg,png,webp同一个图片应该webp是最小的,所以可以将图片从png转成webp这样apk size不就变小了。如果是一两张图片还好可以在线转然后直接丢到工程里面但是如果所有的图片都要转呢?


    抖音团队给出了一个无入侵的解决方案,但是我并没有完全使用他这种方案因为需要hook如果说android系统版本更新hook点就要非常小心,搞过插件化的都知道这是永远的痛,但是有什么更好的方法吗?其实是有的


    需要先明确个概念就是android中所有的res资源都是会合并的,但是如果在不同的moudle包含了同样的res怎么办呢?答案是合并,合并的规则为参考官网总而言之就是主moudle会优先依赖的moudle,根据这个特点是不是可以将所有的图片都转成webp到主moudle的res目录中呢?


    在我们的项目中使用了zoom通过apk大小分析看到zoom相关的图片是最大的所以这里就拿zoom为例。


    先讲下操作思路,自定义gradle插件,添加png转webp task将png转为webp,我这里用的是webp官方的转换工具libwebp,其实还有其他工具。


    下个问题就是怎么获取当前工程中全部的资源呢?答案是通过AGP 中android 对象下的 ApplicationVariant中的 getAllRawAndroidResources()此方法可以把当前变种所有的资源都获取到,具体代码如下:


    Set<File> getAllResPath(Project project) {
    def extension = project.getExtensions().getByName("android")
    Set<File> allRes = new HashSet<>()
    if (project.getPlugins().hasPlugin("com.android.application")) {
    extension.applicationVariants.each {
    allRes.addAll(it.allRawAndroidResources)
    }
    return allRes
    }
    return null
    }
    复制代码

    获取到所有的资源以后就可以一层层的遍历找到非webp的图片然后使用libwebp转为webp


    5、插件化


    插件化是一个非常好的减少包大小的方式,将一些无关紧要不常用的moudle改成插件,然后发布的时候只发布宿主,到用户使用到对应的模块时候下载对应的插件,市面上有很多插件化方案,大家可以对比选用,这个我们也在进行中ing。


    参考链接:


    美团方案


    网易大前端团队实践


    抖音瘦身实践


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

    Python列表和集合的查找原理

    集合与列表查找对比关于大量数据查找,效率差距到底有多大?先看一组实例:import timeimport randomnums = [random.randint(0, 2000000) for i in range(1000)]list_test = lis...
    继续阅读 »

    集合与列表查找对比

    关于大量数据查找,效率差距到底有多大?

    先看一组实例:

    import time
    import random
    nums = [random.randint(0, 2000000) for i in range(1000)]
    list_test = list(range(1000000))
    set_test = set(list_test)
    count_list, count_set = 0, 0
    t1 = time.time() #测试在列表中进行查找
    for num in nums:
    if num in list_test:
    count_list += 1
    t2 = time.time()
    for num in nums: #测试在集合中进行查找
    if num in set_test:
    count_set += 1
    t3 = time.time() #测试在集合中进行查找
    print('找到个数,列表:{},集合:{}'.format(count_list, count_set))
    print('使用时间,列表:{:.4f}s'.format(t2 - t1))
    print('使用时间,集合:{:.4f}s'.format(t3 - t2))

    输出结果为:

    找到个数,列表:528,集合:528
    使用时间,列表:7.9329s
    使用时间,集合:0.0010s

    对于大数据集量来说,我们清晰地看到,集合的查找效率远远的高于列表,那么本文接下来会从Python底层数据结构的角度分析为何出现如此情况。

    list列表的原理

    Python中的list作为一个常用数据结构,在很多程序中被用来当做数组使用,可能很多人都觉得list无非就是一个动态数组,就像C++中的vector或者Go中的slice一样。但事实真的是这样的吗?

    我们来思考一个简单的问题,Python中的list允许我们存储不同类型的数据,既然类型不同,那内存占用空间就就不同,不同大小的数据对象又是如何存入数组中呢?

    比如下面的代码中,我们分别在数组中存储了一个字符串,一个整形,以及一个字典对象,假如是数组实现,则需要将数据存储在相邻的内存空间中,而索引访问就变成一个相当困难的事情了,毕竟我们无法猜测每个元素的大小,从而无法定位想要的元素位置。

    >>> test = ["hello world", 456, {}]
    >>> test
    ['hello world', 456, {}]

    是通过链表结构实现的吗?毕竟链表支持动态的调整,借助于指针可以引用不同类型的数据。但是这样的话使用下标索引数据的时候,需要依赖于遍历的方式查找,O(n)的时间复杂度访问效率实在是太低。

    同时使用链表的开销也较大,每个数据项除了维护本地数据指针外,还要维护一个next指针,因此还要额外分配8字节数据,同时链表分散性使其无法像数组一样利用CPU的缓存来高效的执行数据读写。

    实现的细节可以从其Python的源码中找到, 定义如下:

    typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
    } PyListObject;

    内部list的实现的是一个C结构体,该结构体中的obitem是一个指针数组,存储了所有对象的指针数据,allocated是已分配内存的数量, PyObjectVAR_HEAD是一个宏扩展包含了更多扩展属性用于管理数组,比如引用计数以及数组大小等内容。

    所以我们可以看出,用动态数组作为第一层数据结构,动态数组里存储的是指针,指向对应的数据。

    既然是一个动态数组,则必然会面临一个问题,如何进行容量的管理,大部分的程序语言对于此类结构使用动态调整策略,也就是当存储容量达到一定阈值的时候,扩展容量,当存储容量低于一定的阈值的时候,缩减容量。

    道理很简单,但实施起来可没那么容易,什么时候扩容,扩多少,什么时候执行回收,每次又要回收多少空闲容量,这些都是在实现过程中需要明确的问题。

    假如我们使用一种最简单的策略:超出容量加倍,低于一半容量减倍。这种策略会有什么问题呢?设想一下当我们在容量已满的时候进行一次插入,随即删除该元素,交替执行多次,那数组数据岂不是会不断地被整体复制和回收,已经无性能可言了。

    对于Python list的动态调整规则程序中定义如下, 当追加数据容量已满的时候,通过下面的方式计算再次分配的空间大小,创建新的数组,并将所有数据复制到新的数组中。这是一种相对数据增速较慢的策略,回收的时候则当容量空闲一半的时候执行策略,获取新的缩减后容量大小。

    具体规则如下:

    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6)
    new_allocated += newsize

    动态数组扩容规则是:当出现数组存满时,扩充容量新加入的长度和额外3个,如果新加入元素大于9时,则扩6额外。

    其实对于Python列表这种数据结构的动态调整,在其他语言中也都存在,只是大家可能在日常使用中并没有意识到,了解了动态调整规则,我们可以通过比如手动分配足够的空间,来减少其动态分配带来的迁移成本,使得程序运行的更高效。

    另外如果事先知道存储在列表中的数据类型都相同,比如都是整形或者字符等类型,可以考虑使用arrays库,或者numpy库,两者都提供更直接的数组内存存储模型,而不是上面的指针引用模型,因此在访问和存储效率上面会更高效一些。

    从上面的数据结构可以得出,Python list的查找时间复杂度为O(n),因为作为一个动态数组,需要遍历每一个元素去找到目标元素,故而是一种较为低效的查找方式。

    set集合的原理

    说到集合,就不得不提到Python中的另一种数据结构,就是字典。字典和集合有异曲同工之妙。

    在Python中,字典是通过散列表或说哈希表实现的。字典也被称为关联数组,还称为哈希数组等。也就是说,字典也是一个数组,但数组的索引是键经过哈希函数处理后得到的散列值。

    哈希函数的目的是使键均匀地分布在数组中,并且可以在内存中以O(1)的时间复杂度进行寻址,从而实现快速查找和修改。哈希表中哈希函数的设计困难在于将数据均匀分布在哈希表中,从而尽量减少哈希碰撞和冲突。由于不同的键可能具有相同的哈希值,即可能出现冲突,高级的哈希函数能够使冲突数目最小化。

    Python中并不包含这样高级的哈希函数,几个重要(用于处理字符串和整数)的哈希函数是常见的几个类型。

    通常情况下建立哈希表的具体过程如下:

    • 数据添加:把key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

    • 数据查询:再次使用哈希函数将key转换为对应的数组下标,并定位到数组的位置获取value。

    哈希函数就是一个映射,因此哈希函数的设定很灵活,只要使得任何关键字由此所得的哈希函数值都落在表长允许的范围之内即可。本质上看哈希函数不可能做成一个一对一的映射关系,其本质是一个多对一的映射,这也就引出了下面一个概念——哈希冲突或者说哈希碰撞。哈希碰撞是不可避免的,但是一个好的哈希函数的设计需要尽量避免哈希碰撞。

    Python中使用开放地址法解决冲突

    CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)作为字典的底层数据结构。由于这个实现细节,只有可哈希的对象才能作为字典的键。字典的三个基本操作(添加元素,获取元素和删除元素)的平均事件复杂度为O(1)。

    Python中所有不可变的内置类型都是可哈希的。可变类型(如列表,字典和集合)就是不可哈希的,因此不能作为字典的键。

    常见的哈希碰撞解决方法:

    1. 开放寻址法(open addressing)
      开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。开放地址的意思是除了哈希函数得出的地址可用,当出现冲突的时候其他的地址也一样可用,常见的开放地址思想的方法有线性探测再散列,二次探测再散列等,这些方法都是在第一选择被占用的情况下的解决方法。
    2. 再哈希法
      这个方法是按顺序规定多个哈希函数,每次查询的时候按顺序调用哈希函数,调用到第一个为空的时候返回不存在,调用到此键的时候返回其值。
    3. 链地址法
      将所有关键字哈希值相同的记录都存在同一线性链表中,这样不需要占用其他的哈希地址,相同的哈希值在一条链表上,按顺序遍历就可以找到。
    4. 公共溢出区
      其基本思想是:所有关键字和基本表中关键字为相同哈希值的记录,不管他们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。
    5. 装填因子α
      一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于哈希表的装填因子。哈希表的装填因子定义为表中填入的记录数和哈希表长度的比值,也就是标志着哈希表的装满程度。直观看来,α越小,发生冲突的可能性就越小,反之越大。一般0.75比较合适,涉及数学推导。

    在Python中一个key-value是一个entry,entry有三种状态:

    1. Unused:me_key == me_value == NULL

    Unused是entry的初始状态,key和value都为NULL。插入元素时,Unused状态转换成Active状态。这是me_key为NULL的唯一情况。

    1. Active:me_key != NULL and me_key != dummy 且 me_value != NULL

    插入元素后,entry就成了Active状态,这是me_value唯一不为NULL的情况,删除元素时Active状态可转换成Dummy状态。

    1. Dummy:me_key == dummy 且 me_value == NULL

    此处的Dummy对象实际上一个PyStringObject对象,仅作为指示标志。Dummy状态的元素可以在插入元素的时候将它变成Active状态,但它不可能再变成Unused状态。

    为什么entry有Dummy状态呢?

    这是因为采用开放寻址法中,遇到哈希冲突时会找到下一个合适的位置,例如某元素经过哈希计算应该插入到A处,但是此时A处有元素的,通过探测函数计算得到下一个位置B,仍然有元素,直到找到位置C为止,此时ABC构成了探测链,查找元素时如果hash值相同,那么也是顺着这条探测链不断往后找,当删除探测链中的某个元素时,比如B,如果直接把B从哈希表中移除,即变成Unused状态,那么C就不可能再找到了,因为AC之间出现了断裂的现象,正是如此才出现了第三种状态-Dummy,Dummy是一种类似的伪删除方式,保证探测链的连续性。

    set集合和dict一样也是基于散列表的,只是他的表元只包含键的引用,而没有对值的引用,其他的和dict基本上是一致的,所以在此就不再多说了。并且dict要求键必须是能被哈希的不可变对象,因此普通的set无法作为dict的键,必须选择被“冻结”的不可变集合类:frozenset。顾名思义,一旦初始化,集合内数据不可修改。

    一般情况下普通的顺序表数组存储结构也可以认为是简单的哈希表,虽然没有采用哈希函数(取余),但同样可以在O(1)时间内进行查找和修改。但是这种方法存在两个问题:

    • 扩展性不强
    • 浪费空间

    dict是用来存储键值对结构的数据的,set其实也是存储的键值对,只是默认键和值是相同的。Python中的dict和set都是通过散列表来实现的。下面来看与dict相关的几个比较重要的问题:

    • dict中的数据是无序存放的。操作的时间复杂度,插入、查找和删除都可以在O(1)的时间复杂度。这是因为查找相当于将查找值通过哈希函数运算之后,直接得到对应的桶位置(不考虑哈希冲突的理想情况下),故而复杂度为O(1)。

    • 由于键的限制,只有可哈希的对象才能作为字典的键和set的值。可hash的对象即Python中的不可变对象和自定义的对象。可变对象(列表、字典、集合)是不能作为字典的键和set的值的。

    与list相比:list的查找和删除的时间复杂度是O(n),添加的时间复杂度是O(1)。但是dict使用hashtable内存的开销更大。为了保证较少的冲突,hashtable的装载因子,一般要小于0.75,在Python中当装载因子达到2/3的时候就会自动进行扩容。

    参考资料:

    Python dict和set的底层原理:https://blog.csdn.net/liuweiyuxiang/article/details/98943272

    python 图解Python List数据结构:https://blog.csdn.net/u014029783/article/details/107992840

    作者:严天宇
    来源:https://mp.weixin.qq.com/s/wvgf7GpbCoeDsLOp1WAFPg
    收起阅读 »

    【前端工程化】- 结合代码实践,全面学习前端工程化

    前言前端工程化,简而言之就是软件工程 前端,以自动化的形式呈现。就个人理解而言:前端工程化,从开发阶段到代码发布生产环境,包含了以下几个内容:开发构建测试部署性能规范 下面我们根据上述几个内容,选择有代表性的几个方面进行深入学习前端工程化。脚手架脚手...
    继续阅读 »

    前言

    前端工程化,简而言之就是软件工程 前端,以自动化的形式呈现。就个人理解而言:前端工程化,从开发阶段到代码发布生产环境,包含了以下几个内容:

    • 开发
    • 构建
    • 测试
    • 部署
    • 性能
    • 规范

    image.png 下面我们根据上述几个内容,选择有代表性的几个方面进行深入学习前端工程化。


    脚手架

    脚手架是什么?(What)

    现在流行的前端脚手架基本上都是基于NodeJs编写,比如我们常用的Vue-CLI,比较火的create-react-app,还有Dva-CLI等。

    脚手架存在的意义?(Why)

    随着前端工程化的概念越来越深入人心,脚手架的出现就是为减少重复性工作而引入的命令行工具,摆脱ctrl cctrl v,此话怎讲? 现在新建一个前端项目,已经不是在html头部引入css,尾部引入js那么简单的事了,css都是采用Sass或则Less编写,在js中引入,然后动态构建注入到html中;除了学习基本的jscss语法和热门框架,还需要学习构建工具webpackbabel这些怎么配置,怎么起前端服务,怎么热更新;为了在编写过程中让编辑器帮我们查错以及更加规范,我们还需要引入ESlint;甚至,有些项目还需要引入单元测试(Jest)。对于一个更入门的人来说,这无疑会让人望而却步。而前端脚手架的出现,就让事情简单化,一键命令,新建一个工程,再执行两个npm命令,跑起一个项目。在入门时,无需关注配置什么的,只需要开心的写代码就好。

    如何实现一个新建项目脚手架(基于koa)?(How)

    先梳理下实现思路

    我们实现脚手架的核心思想就是自动化思维,将重复性的ctrl cctrl v创建项目,用程序来解决。解决步骤如下:

    1. 创建文件夹(项目名)
    2. 创建 index.js
    3. 创建 package.json
    4. 安装依赖

    1. 创建文件夹

    创建文件夹前,需要先删除清空:


    // package.json
    {
    ...
    "scripts": {
    "test": "rm -rf ./haha && node --experimental-modules index.js"
    }
    ...
    }

    创建文件夹:我们通过引入 nodejsfs 模块,使用 mkdirSync API来创建文件夹。


    // index.js
    import fs from 'fs';

    function getRootPath() {
    return "./haha";
    }

    // 生成文件夹
    fs.mkdirSync(getRootPath());

    2. 创建 index.js


    创建 index.js:使用 nodejsfs 模块的 writeFileSync API 创建 index.js 文件:


    // index.js
    fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));

    接着我们来看看,动态模板如何生成?我们最理想的方式是通过配置来动态生成文件模板,那么具体来看看 createIndexTemplate 实现的逻辑吧。


    // index.js
    import fs from 'fs';
    import { createIndexTemplate } from "./indexTemplate.js";

    // input
    // process
    // output
    const inputConfig = {
    middleWare: {
    router: true,
    static: true
    }
    }
    function getRootPath() {
    return "./haha";
    }
    // 生成文件夹
    fs.mkdirSync(getRootPath());
    // 生成 index.js 文件
    fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));

    // indexTemplate.js
    import ejs from "ejs";
    import fs from "fs";
    import prettier from "prettier";// 格式化代码
    // 问题驱动
    // 模板
    // 开发思想 - 小步骤的开发思想
    // 动态生成代码模板
    export function createIndexTemplate(config) {
    // 读取模板
    const template = fs.readFileSync("./template/index.ejs", "utf-8");

    // ejs渲染
    const code = ejs.render(template, {
    router: config.middleware.router,
    static: config.middleware.static,
    port: config.port,
    });

    // 返回模板
    return prettier.format(code, {
    parser: "babel",
    });
    }

    // template/index.ejs
    const Koa = require("koa");
    <% if (router) { %>
    const Router = require("koa-router");
    <% } %>


    <% if (static) { %>
    const serve = require("koa-static");
    <% } %>

    const app = new Koa();

    <% if (router) { %>
    const router = new Router();
    router.get("/", (ctx) => {
    ctx.body = "hello koa-setup-heihei";
    });
    app.use(router.routes());
    <% } %>

    <% if (static) { %>
    app.use(serve(__dirname + "/static"));
    <% } %>

    app.listen(<%= port %>, () => {
    console.log("open server localhost:<%= port %>");
    });

    3. 创建 package.json


    创建 package.json 文件,实质是和创建 index.js 类似,都是采用动态生成模板的思路来实现,我们来看下核心方法 createPackageJsonTemplate 的实现代码:


    // packageJsonTemplate.js
    function createPackageJsonTemplate(config) {
    const template = fs.readFileSync("./template/package.ejs", "utf-8");

    const code = ejs.render(template, {
    packageName: config.packageName,
    router: config.middleware.router,
    static: config.middleware.static,
    });

    return prettier.format(code, {
    parser: "json",
    });
    }

    // template/package.ejs
    {
    "name": "<%= packageName %>",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
    "koa": "^2.13.1"
    <% if (router) { %>
    ,"koa-router": "^10.1.1"
    <% } %>

    <% if (static) { %>
    ,"koa-static": "^5.0.0"
    }
    <% } %>
    }

    4. 安装依赖


    要自动安装依赖,我们可以使用 nodejsexeca 库执行 yarn 安装命令:


    execa("yarn", {
    cwd: getRootPath(),
    stdio: [2, 2, 2],
    });

    至此,我们已经用 nodejs 实现了新建项目的脚手架了。最后我们可以重新梳理下可优化点将其升级完善。比如将程序配置升级成 GUI 用户配置(用户通过手动选择或是输入来传入配置参数,例如项目名)。




    编译构建

    编译构建是什么?


    构建,或者叫作编译,是前端工程化体系中功能最繁琐、最复杂的模块,承担着从源代码转化为宿主浏览器可执行的代码,其核心是资源的管理。前端的产出资源包括JS、CSS、HTML等,分别对应的源代码则是:



    • 领先于浏览器实现的ECMAScript规范编写的JS代码(ES6/7/8...)。

    • LESS/SASS预编译语法编写的CSS代码。

    • Jade/EJS/Mustache等模板语法编写的HTML代码。


    以上源代码是无法在浏览器环境下运行的,构建工作的核心便是将其转化为宿主可执行代码,分别对应:



    • ECMAScript规范的转译。

    • CSS预编译语法转译。

    • HTML模板渲染。


    那么下面我们就一起学习下如今3大主流构建工具:Webpack、Rollup、Vite。


    Webpack


    image.png


    Webpack原理


    想要真正用好 Webpack 编译构建工具,我们需要先来了解下它的工作原理。Webpack 编译项目的工作机制是,递归找出所有依赖模块,转换源码为浏览器可执行代码,并构建输出bundle。具体工作流程步骤如下:



    1. 初始化参数:取配置文件和shell脚本参数并合并

    2. 开始编译:用上一步得到的参数初始化compiler对象,执行run方法开始编译

    3. 确定入口:根据配置中的entry,确定入口文件

    4. 编译模块:从入口文件出发,递归遍历找出所有依赖模块的文件

    5. 完成模块编译:使用loader转译所有模块,得到转译后的最终内容和依赖关系

    6. 输出资源:根据入口和模块依赖关系,组装成一个个chunk,加到输出列表

    7. 输出完成:根据配置中的output,确定输出路径和文件名,把文件内容写入输出目录(默认是dist


    Webpack实践


    1. 基础配置


    【entry】



    入口配置,webpack 编译构建时能找到编译的入口文件,进而构建内部依赖图。



    【output】



    输出配置,告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。



    【loader】



    模块转换器,loader 可以处理浏览器无法直接运行的文件模块,转换为有效模块。比如:css-loader和style-loader处理样式;url-loader和file-loader处理图片。



    【plugin】



    插件,解决 loader 无法实现的问题,在 webpack 整个构建生命周期都可以扩展插件。比如:打包优化,资源管理,注入环境变量等。



    下面是 webpack 基本配置的简单示例:


    const path = require("path");

    module.exports = {
    mode: "development",
    entry: "./src/index.js",
    output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    },
    devServer: {
    static: "./dist",
    },
    module: {
    rules: [
    {
    // 匹配什么样子的文件
    test: /\.css$/i,
    // 使用loader , 从后到前执行
    use: ["style-loader", "css-loader"],
    }
    ],
    },
    };


    参考webpack官网:webpack.docschina.org/concepts/

    (注意:使用不同版本的 webpack 切换对应版本的文档哦)



    2. 性能优化


    • 编译速度优化

    【检测编译速度】


    寻找检测编译速度的工具,比如 speed-measure-webpack-plugin插件 ,用该插件分析每个loader和plugin执行耗时具体情况。


    【优化编译速度该怎么做呢?】




    1. 减少搜索依赖的时间



    • 配置 loader 匹配规则 test/include/exclue,缩小搜索范围,即可减少搜索时间



    1. 减少解析转换的时间



    • noParse配置,精准过滤不用解析的模块

    • loader性能消耗大的,开启多进程



    1. 减少构建输出的时间



    • 压缩代码,开启多进程



    1. 合理使用缓存策略



    • babel-loader开启缓存

    • 中间模块启用缓存,比如使用 hard-source-webpack-plugin


    具体优化措施可参考:webpack性能优化的一段经历|项目复盘



    • 体积优化

    【检测包体积大小】


    寻找检测构建后包体积大小的工具,比如 webpack-bundle-analyzer插件 ,用该插件分析打包后生成Bundle的每个模块体积大小。


    【优化体积该怎么做呢?】




    1. bundle去除第三方依赖

    2. 擦除无用代码 Tree Shaking


    具体优化措施参考:webpack性能优化的一段经历|项目复盘



    Rollup


    Rollup概述


    Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。并且可以对代码模块使用新的标准化格式,比如CommonJSes module


    Rollup原理


    我们先来了解下 Rollup 原理,其主要工作机制是:



    1. 确定入口文件

    2. 使用 Acorn 读取解析文件,获取抽象语法树 AST

    3. 分析代码

    4. 生成代码,输出


    Rollup 相对 Webpack 而言,打包出来的包会更加轻量化,更适用于类库打包,因为内置了 Tree Shaking 机制,在分析代码阶段就知晓哪些文件引入并未调用,打包时就会自动擦除未使用的代码。



    Acorn 是一个 JavaScript 语法解析器,它将 JavaScript 字符串解析成语法抽象树 AST 如果想了解 AST 语法树可以点下这个网址astexplorer.net/



    Rollup实践


    【input】



    入口文件路径



    【output】



    输出文件、输出格式(amd/es6/iife/umd/cjs)、sourcemap启用等。



    【plugin】



    各种插件使用的配置



    【external】



    提取外部依赖



    【global】



    配置全局变量



    下面是 Rollup 基础配置的简单示例:


    import commonjs from "@rollup/plugin-commonjs";
    import resolve from "@rollup/plugin-node-resolve";
    // 解析json
    import json from '@rollup/plugin-json'
    // 压缩代码
    import { terser } from 'rollup-plugin-terser';
    export default {
    input: "src/main.js",
    output: [{
    file: "dist/esmbundle.js",
    format: "esm",
    plugins: [terser()]
    },{
    file: "dist/cjsbundle.js",
    format: "cjs",
    }],
    // commonjs 需要放到 transform 插件之前,
    // 但是又个例外, 是需要放到 babel 之后的
    plugins: [json(), resolve(), commonjs()],
    external: ["vue"]
    };

    Vite

    Vite概述


    Vite,相比 Webpack、Rollup 等工具,极大地改善了前端开发者的开发体验,编译速度极快。


    Vite原理


    为什么 Vite 开发编译速度极快?我们就先来探究下它的原理吧。
    image.png
    由上图可见,Vite 原理是利用现代主流浏览器支持原生的 ESM 规范,配合 server 做拦截,把代码编译成浏览器支持的。
    image.png


    Vite实践体验


    我们可以搭建一个Hello World版的Vite项目来感受下飞快的开发体验:



    注意:Vite 需要 Node.js 版本 >= 12.0.0。



    使用 NPM:


    $ npm init vite@latest

    使用 Yarn:


    $ yarn create vite

    image.png
    上图是Vite项目的编译时间,363ms,开发秒级编译的体验,真的是棒棒哒!


    3种构建工具综合对比





































    WebpackRollupVite
    编译速度一般较快最快
    HMR热更新支持需要额外引入插件支持
    Tree Shaking需要额外配置支持支持
    适用范围项目打包类库打包不考虑兼容性的项目



    测试

    当我们前端项目越来越庞大时,开发迭代维护成本就会越来越高,数十个模块相互调用错综复杂,为了提高代码质量和可维护性,就需要写测试了。下面就给大家具体介绍下前端工程经常做的3类测试。

    单元测试


    单元测试,是对最小可测试单元(一般为单个函数、类或组件)进行检查和验证。

    做单元测试的框架有很多,比如 Mocha断言库ChaiSinonJest等。我们可以先选择 jest 来学习,因为它集成了 Mochachaijsdomsinon 等功能。接下来,我们一起看看 jest 怎么写单元测试吧?



    1. 根据正确性写测试,即正确的输入应该有正常的结果。

    2. 根据错误性写测试,即错误的输入应该是错误的结果。


    以验证求和函数为例:


    // add函数
    module.exports = (a,b) => {
    return a+b;
    }

    // 正确性测试验证
    const add = require('./add.js');

    test('should 1+1 = 2', ()=> {
    // 准备测试数据 -> given
    const a = 1;
    const b = 1;
    // 触发测试动作 -> when
    const r = add(a,b);
    // 验证 -> then
    expect(r).toBe(2);
    })

    image.png


    // 错误性测试验证
    test('should 1+1 = 2', ()=> {
    // 准备测试数据 -> given
    const a = 1;
    const b = 2;
    // 触发测试动作 -> when
    const r = add(a,b)
    // 验证 -> then
    expect(r).toBe(2);
    })

    image.png


    组件测试

    组件测试,主要是针对某个组件功能进行测试,这就相对困难些,因为很多组件涉及了DOM操作。组件测试,我们可以借助组件测试框架来做,比如使用 Cypress(它可以做组件测试,也可以做 e2e 测试)。我们就先来看看组件测试怎么做?


    以 vue3 组件测试为例:



    1. 我们先建好 vue3 + vite 项目,编写测试组件

    2. 再安装 cypress 环境

    3. cypress/component 编写组件测试脚本文件

    4. 执行 cypress open-ct 命令,启动 cypress component testing 的服务运行 xx.spec.js 测试脚本,便能直观看到单个组件自动执行操作逻辑


    // Button.vue 组件

    <template>
    <div>Button测试</div>
    </template>
    <script>
    export default {
    }
    </script>
    <style>
    </style>

    // cypress/plugin/index.js 配置

    const { startDevServer } = require('@cypress/vite-dev-server')
    // eslint-disable-next-line no-unused-vars
    module.exports = (on, config) => {
    // `on` is used to hook into various events Cypress emits
    // `config` is the resolved Cypress config
    on('dev-server:start', (options) => {
    const viteConfig = {
    // import or inline your vite configuration from vite.config.js
    }
    return startDevServer({ options, viteConfig })
    })
    return config;
    }

    // cypress/component/Button.spec.js Button组件测试脚本

    import { mount } from "@cypress/vue";
    import Button from "../../src/components/Button.vue";

    describe("Button", () => {
    it("should show button", () => {
    // 挂载button
    mount(Button);

    cy.contains("Button");
    });
    });

    e2e测试


    e2e 测试,也叫端到端测试,主要是模拟用户对页面进行一系列操作并验证其是否符合预期。我们同样也可以使用 cypress 来做 e2e 测试,具体怎么做呢?


    以 todo list 功能验证为例:



    1. 我们先建好 vue3 + vite 项目,编写测试组件

    2. 再安装 cypress 环境

    3. cypress/integration 编写组件测试脚本文件

    4. 执行 cypress open 命令,启动 cypress 的服务,选择 xx.spec.js 测试脚本,便能直观看到模拟用户的操作流程


    // cypress/integration/todo.spec.js todo功能测试脚本

    describe('example to-do app', () => {
    beforeEach(() => {
    cy.visit('https://example.cypress.io/todo')
    })

    it('displays two todo items by default', () => {
    cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
    cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
    })

    it('can add new todo items', () => {
    const newItem = 'Feed the cat'
    cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)

    cy.get('.todo-list li')
    .should('have.length', 3)
    .last()
    .should('have.text', newItem)
    })

    it('can check off an item as completed', () => {
    cy.contains('Pay electric bill')
    .parent()
    .find('input[type=checkbox]')
    .check()

    cy.contains('Pay electric bill')
    .parents('li')
    .should('have.class', 'completed')
    })

    context('with a checked task', () => {
    beforeEach(() => {
    cy.contains('Pay electric bill')
    .parent()
    .find('input[type=checkbox]')
    .check()
    })

    it('can filter for uncompleted tasks', () => {
    cy.contains('Active').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .first()
    .should('have.text', 'Walk the dog')

    cy.contains('Pay electric bill').should('not.exist')
    })

    it('can filter for completed tasks', () => {
    // We can perform similar steps as the test above to ensure
    // that only completed tasks are shown
    cy.contains('Completed').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .first()
    .should('have.text', 'Pay electric bill')

    cy.contains('Walk the dog').should('not.exist')
    })

    it('can delete all completed tasks', () => {
    cy.contains('Clear completed').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .should('not.have.text', 'Pay electric bill')

    cy.contains('Clear completed').should('not.exist')
    })
    })
    })

    e2e.gif




    总结

    本文前言部分通过开发、构建、性能、测试、部署、规范六个方面,较全面地梳理了前端工程化的知识点,正文则主要介绍了在实践项目中落地使用的前端工程化核心技术点。

    希望本文能够帮助到正在学前端工程化的小伙伴构建完整的知识图谱~


    作者:小铭子
    来源:https://juejin.cn/post/7033355647521554446
    收起阅读 »

    Python运算符优先级及结合性

    当多个运算符出现在一起需要进行运算时,Python 会先比较各个运算符的优先级,按照优先级从高到低的顺序依次执行;当遇到优先级相同的运算符时,再根据结合性决定先执行哪个运算符:如果是左结合性就先执行左边的运算符,如果是右结合性就先执行右边的运算符。运算符的优先...
    继续阅读 »

    当多个运算符出现在一起需要进行运算时,Python 会先比较各个运算符的优先级,按照优先级从高到低的顺序依次执行;

    当遇到优先级相同的运算符时,再根据结合性决定先执行哪个运算符:如果是左结合性就先执行左边的运算符,如果是右结合性就先执行右边的运算符。

    运算符的优先级

    在数学运算2 + 4 * 3中,要先计算乘法,再计算加法,否则结果就是错误的。所谓优先级,就是当多个运算符出现在一起时,需要先执行哪个运算符,那么这个运算符的优先级就更高。 

    Python中运算符优先级如下表所示,括号的优先级是最高的,无论任何时候优先计算括号里面的内容,逻辑运算符的优先级最低,在表中处在同一行运算符的优先级一致,上一层级的优先级高于下一层级的。算术运算符可以分为四种,幂运算最高,其次是正负号,然后是 “* / // %”,最后才是加减“+ -”。

    运算符

    描述

    ()

    括号

    **

    幂运算

    ~

    按位取反

    +、-

    正号、负号

    、/、 %、 //

    乘、除、取模、取整除

    、-

    加、减

    >> 、<<

    右移、左移

    &

    按位“与”

    、|

    按位“异或”,按位“或”

    <=  、< 、>、 >=

    比较运算符

    ==、!=

    等于、不等于

    =、%=、/=、//=、-=、+=、*=、**=

    赋值运算符

    is、is not

    身份运算符

    in、not in

    成员运算符

    and or not

    逻辑运算符

    运算符的结合性

    在多种运算符在一起进行运算时,除了要考虑优先级,有时候还需要考虑结合性。当同时出现多个优先级相同的运算符时,先执行左边的叫左结合性,先执行右边的叫右结合性。如:5 / 2 * 4,由于/*的优先级相同,所以只能参考运算符的结合性了,/*都是左结合性的,所以先计算除法,再计算乘法,结果是10.0。Python中大部分运算符都具有左结合性,其中,幂运算**、正负号、赋值运算符等具有右结合性。

    >>> 5 / 2* 4# 左结合性运算符
    10.0
    >>> 2 ** 2 ** 3# 右结合性,等同于2 ** (2 **3)
    256

    虽然Python运算符存在优先级的关系,但写程序时不建议写很长的表达式,过分依赖运算符的优先级,比如:2 ** -1 % 3 / 5 ** 3 *4,这样的表达式会大大降低程序的可读性。因此,建议写程序时,遵守以下两点原则,保证运算逻辑清晰明了。

    1. 尽量不要把一个表达式写的过长过于复杂,如果计算过程的确需要,可以尝试将它拆分几部分来写。
    2. 尽量多使用()来控制运算符的执行顺序,使用()可以让运算的先后顺序变得十分清楚。



    作者:刘文飞 

    来源:https://mp.weixin.qq.com/s/fXzg2L6emlEVCCT-t4Pk6Q





    收起阅读 »

    【vue自定义组件】实现一个污染日历

    vue
    前言 佛祖保佑, 永无bug。Hello 大家好!我是海的对岸! 实际开发中,碰到一个日历的需求,这个日历的需求中要加入定制的业务,网上没有现成的,手动实现了一下,整理记录下。 动画效果: 实现 实现背景 工作上碰到一个需求,需要有一个可以在日历上能看到每天...
    继续阅读 »

    前言


    佛祖保佑, 永无bug。Hello 大家好!我是海的对岸!


    实际开发中,碰到一个日历的需求,这个日历的需求中要加入定制的业务,网上没有现成的,手动实现了一下,整理记录下。


    动画效果:


    calendar.gif


    实现


    实现背景


    工作上碰到一个需求,需要有一个可以在日历上能看到每天的污染情况的状态,因此,我们梳理下需求:



    1. 要有一个日历组件

    2. 要在这个日历组件中追加自己的业务逻辑


    简单拎一下核心代码的功能


    实现日历模块


    大体上日历就是看某个月有多少多少天,拆分下,如下所示:
    image.png


    再对比这我们的效果图,日历上还要有上个月的末尾几天


    image.png


    实现上个月的末尾几天


    monthFisrtDay() {
    // 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
    // eslint-disable-next-line radix
    const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
    let currWeek = new Date(currDT).getDay();
    return ++currWeek || 7;
    },
    // 刷新日历 获得上个月的结尾天数 <=7
    refreshCalendar() {
    this.nunDays = [];
    const lastDays = [];
    const lastMon = (this.month).replace('月', '') * 1 - 1;
    let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
    for (let i = 1; i < this.monthFisrtDay(); i += 1) {
    lastDays.unshift(lastDay);
    lastDay -= 1;
    }
    this.nunDays = lastDays;
    },

    实现每个月的实际天数


    // 展示 日历数据
    getDatas() {
    if (this.dealDataFinal && this.dealDataFinal.length > 0) {
    // console.log(this.dealDataFinal);
    this.list = [];
    const datas = this.dealDataFinal;
    const dataMap = {};
    if (datas.length > 0) {
    datas.forEach((item) => {
    item.level -= 1;
    item.dateStr = item.tstamp.substr(0, 10);
    item.date = item.tstamp.substr(8, 2);
    dataMap[item.date] = item;
    });
    }

    const curDay = new Date().getDate();
    for (let i = 1; i <= this.monthDays; i += 1) {
    let currColor = this.lvls[6];
    let dateStr = String(i);
    let isCurDay = false;
    if (i == curDay) {
    isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
    }
    dateStr = '0' + dateStr;
    dateStr = dateStr.substr(dateStr.length - 2);
    const dataObj = dataMap[dateStr];
    if (dataObj) {
    if (dataObj.level >= 0 && dataObj.level <= 5) {
    currColor = this.lvls[dataObj.level].color;
    } else {
    currColor = this.lvls[6].color;
    }

    this.list.push({
    date: i,
    curDay: isCurDay,
    color: currColor,
    datas: dataObj,
    checkedColor: undefined, // 选中颜色
    });
    } else {
    this.list.push({
    date: i,
    curDay: isCurDay,
    color: this.lvls[6].color,
    datas: {},
    checkedColor: undefined, // 选中颜色
    });
    }
    }
    // console.log(this.list);
    } else {
    this.clearCalendar();
    }
    },
    // 清除上一次的记录
    clearCalendar() {
    this.list = [];
    for (let i = 1; i <= this.monthDays; i += 1) {
    this.list.push({
    date: i,
    color: this.lvls[6].color,
    datas: {},
    });
    }
    },

    实现日历之后,追加业务


    定义业务上的字段


    data() {
    return {
    ...
    lvls: [
    { title: '优', color: '#00e400' },
    { title: '良', color: '#ffff00' },
    { title: '轻度污染', color: '#ff7e00' },
    { title: '中度污染', color: '#ff0000' },
    { title: '重度污染', color: '#99004c' },
    { title: '严重污染', color: '#7e0023' },
    { title: '未知等级', color: '#cacaca' },
    ],
    list: [], // 当前月的所有天数
    dealDataFinal: [], // 处理接口数据之后获得的最终的数组
    ...
    curYearMonth: '', // 当前时间 年月
    choseYearMonth: '', // 选择的时间 年月
    };
    },

    定义业务上的方法


    // 加载等级
    loadImgType(value) {
    let imgUrl = 0;
    switch (value) {
    case '优':
    imgUrl = 1;
    break;
    case '良':
    imgUrl = 2;
    break;
    case '轻':
    imgUrl = 3;
    break;
    case '中':
    imgUrl = 4;
    break;
    case '重':
    imgUrl = 5;
    break;
    case '严':
    imgUrl = 6;
    break;
    default:
    imgUrl = 0;
    break;
    }
    return imgUrl;
    },

    因为展示效果,用到的是css,css用的比较多,这里就不一段一段的解读了,总而言之,就是日元素不同状态的样式展示,通过前面设置的等级方法,来得到不同的返回参数,进而展示出不同参数对应的不同颜色样式。


    最后会放出日历组件的完整代码。


    完整代码


    <template>
    <div class="right-content">
    <div style="height: 345px;">
    <div class="" style="padding: 0px 15px;">
    <el-select v-model="year" style="width: 119px;" popper-class="EntDate">
    <el-option v-for="item in years" :value="item" :label="item" :key="item"></el-option>
    </el-select>
    <el-select v-model="month" style="width: 119px; margin-left: 10px;" popper-class="EntDate">
    <el-option v-for="item in mons" :value="item" :label="item" :key="item"></el-option>
    </el-select>
    <div class="r-inline">
    <span class="searchBtn" @click="qEQCalendar">查询</span>
    </div>
    </div>
    <div class="calendar" element-loading-spinner="el-icon-loading"
    element-loading-background="rgba(0, 0, 0, 0.6)">
    <div class="day-title clearfix">
    <div class="day-tt" v-for="day in days" :key="day">{{day}}</div>
    </div>
    <div class="clearfix" style="padding-top: 10px;">
    <div :class="{'date-item': true, 'is-last-month': true,}" v-for="(item, index) in nunDays" :key="index + 'num'">
    <div class="day">{{item}}</div>
    </div>
    <div :class="{'date-item': true, 'is-last-month': false, 'isPointer': isPointer}"
    v-for="(item, index) in list" :key="index" @click="queryDeal(item)">
    <div v-if="item.curDay && (curYearMonth === choseYearMonth)" class="day" :style="{border:'2px dashed' + item.color}"
    :class="{'choseDateItemI': item.checkedColor === '#00e400',
    'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
    'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
    >

    </div>
    <div v-else class="day" :style="{border:'2px solid' + item.color}"
    :class="{'choseDateItemI': item.checkedColor === '#00e400',
    'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
    'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
    >
    {{item.date}}
    </div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </template>

    <script>
    const today = new Date();
    const years = [];
    const year = today.getFullYear();
    for (let i = 2018; i <= year; i += 1) {
    years.push(`${i}年`);
    }
    export default {
    props: {
    rightData2: {
    type: Object,
    defaul() {
    return undefined;
    },
    },
    isPointer: {
    type: Boolean,
    default() {
    return false;
    },
    },
    },
    watch: {
    rightData2(val) {
    this.dealData(val);
    },
    calendarData(val) {
    this.dealData(val);
    },
    },
    data() {
    return {
    pointInfo: {
    title: 'xxx污染日历',
    },
    days: ['日', '一', '二', '三', '四', '五', '六'],
    year: year + '年',
    years,
    month: (today.getMonth() + 1) + '月',
    mons: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
    lvls: [
    { title: '优', color: '#00e400' },
    { title: '良', color: '#ffff00' },
    { title: '轻度污染', color: '#ff7e00' },
    { title: '中度污染', color: '#ff0000' },
    { title: '重度污染', color: '#99004c' },
    { title: '严重污染', color: '#7e0023' },
    { title: '未知等级', color: '#cacaca' },
    ],
    list: [], // 当前月的所有天数
    dealDataFinal: [], // 处理接口数据之后获得的最终的数组
    nunDays: [],
    testDays: ['日', '一', '二', '三', '四', '五', '六'],
    calendarData: null,
    curYearMonth: '', // 当前时间 年月
    choseYearMonth: '', // 选择的时间 年月
    };
    },
    computed: {
    // 获取 select框中展示的具体月份应对应的月数
    monthDays() {
    const lastyear = (this.year).replace('年', '') * 1;
    const lastMon = (this.month).replace('月', '') * 1;
    const monNum = new Date(lastyear, lastMon, 0).getDate();
    // return this.$mp.dateFun.GetMonthDays(this.year.substr(0, 4), lastMon);
    return monNum;
    },
    },
    methods: {
    monthFisrtDay() {
    // 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
    // eslint-disable-next-line radix
    const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
    let currWeek = new Date(currDT).getDay();
    return ++currWeek || 7;
    },
    // 刷新日历 获得上个月的结尾天数 <=7
    refreshCalendar() {
    this.nunDays = [];
    const lastDays = [];
    const lastMon = (this.month).replace('月', '') * 1 - 1;
    let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
    for (let i = 1; i < this.monthFisrtDay(); i += 1) {
    lastDays.unshift(lastDay);
    lastDay -= 1;
    }
    this.nunDays = lastDays;
    },
    // 展示 日历数据
    getDatas() {
    if (this.dealDataFinal && this.dealDataFinal.length > 0) {
    // console.log(this.dealDataFinal);
    this.list = [];
    const datas = this.dealDataFinal;
    const dataMap = {};
    if (datas.length > 0) {
    datas.forEach((item) => {
    item.level -= 1;
    item.dateStr = item.tstamp.substr(0, 10);
    item.date = item.tstamp.substr(8, 2);
    dataMap[item.date] = item;
    });
    }

    const curDay = new Date().getDate();
    for (let i = 1; i <= this.monthDays; i += 1) {
    let currColor = this.lvls[6];
    let dateStr = String(i);
    let isCurDay = false;
    if (i == curDay) {
    isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
    }
    dateStr = '0' + dateStr;
    dateStr = dateStr.substr(dateStr.length - 2);
    const dataObj = dataMap[dateStr];
    if (dataObj) {
    if (dataObj.level >= 0 && dataObj.level <= 5) {
    currColor = this.lvls[dataObj.level].color;
    } else {
    currColor = this.lvls[6].color;
    }

    this.list.push({
    date: i,
    curDay: isCurDay,
    color: currColor,
    datas: dataObj,
    checkedColor: undefined, // 选中颜色
    });
    } else {
    this.list.push({
    date: i,
    curDay: isCurDay,
    color: this.lvls[6].color,
    datas: {},
    checkedColor: undefined, // 选中颜色
    });
    }
    }
    // console.log(this.list);
    } else {
    this.clearCalendar();
    }
    },
    clearCalendar() {
    this.list = [];
    for (let i = 1; i <= this.monthDays; i += 1) {
    this.list.push({
    date: i,
    color: this.lvls[6].color,
    datas: {},
    });
    }
    },

    // 处理接口返回的日历数据
    dealData(currDS) {
    const tempData = [];
    if (('dates' in currDS) && ('level' in currDS) && ('levelName' in currDS) && ('values' in currDS)) {
    if (currDS.dates.length > 0 && currDS.level.length > 0 && currDS.levelName.length > 0 && currDS.values.length > 0) {
    for (let i = 0; i < currDS.dates.length; i++) {
    const temp = {
    tstamp: currDS.dates[i],
    level: currDS.level[i],
    levelName: currDS.levelName[i],
    value: currDS.values[i],
    grade: this.loadImgType(currDS.levelName[i]),
    week: this.testDays[new Date(currDS.dates[i]).getDay()], // currDS.dates[i]: '2020-03-31'
    };
    tempData.push(temp);
    }
    // this.dealDataFinal = tempData.filter(item => item.grade>0);
    this.dealDataFinal = tempData;
    this.refreshCalendar();
    this.getDatas();
    } else {
    this.dealDataFinal = null;
    this.getDatas();
    }
    } else {
    this.dealDataFinal = null;
    this.getDatas();
    }
    },
    // 加载等级
    loadImgType(value) {
    let imgUrl = 0;
    switch (value) {
    case '优':
    imgUrl = 1;
    break;
    case '良':
    imgUrl = 2;
    break;
    case '轻':
    imgUrl = 3;
    break;
    case '中':
    imgUrl = 4;
    break;
    case '重':
    imgUrl = 5;
    break;
    case '严':
    imgUrl = 6;
    break;
    default:
    imgUrl = 0;
    break;
    }
    return imgUrl;
    },
    // (右边)区域环境质量日历
    qEQCalendar() {
    this.curYearMonth = new Date().getFullYear() + '-' + (new Date().getMonth() + 1);
    this.choseYearMonth = this.year.substr(0, 4) + '-' + this.month.substr(0, 1);
    this.calendarData = {
    dates: [
    '2020-07-01',
    '2020-07-02',
    '2020-07-03',
    '2020-07-04',
    '2020-07-05',
    '2020-07-06',
    '2020-07-07',
    '2020-07-08',
    '2020-07-09',
    '2020-07-10',
    '2020-07-11',
    '2020-07-12',
    '2020-07-13',
    '2020-07-14',
    '2020-07-15',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    level: [
    1,
    4,
    2,
    3,
    1,
    4,
    4,
    3,
    1,
    4,
    2,
    2,
    4,
    1,
    3,
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    levelName: [
    '优',
    '中度污染',
    '良',
    '轻度污染',
    '优',
    '中度污染',
    '中度污染',
    '轻度污染',
    '优',
    '中度污染',
    '良',
    '良',
    '中度污染',
    '优',
    '轻度污染',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    values: [
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    };
    // this.$axios.get('api/sinoyd-airquality/airquality/gis/calendar?year=' + parseInt(this.year.substr(0, 4)) + '&month=' + parseInt((this.month).replace('月', '')))
    // .then((res) => {
    // if (res.status == 200) {
    // this.calendarData = res.data.data;
    // } else {
    // this.calendarData = null;
    // }
    // }, () => {
    // this.calendarData = null;
    // });
    },
    // 设置选中之后的逻辑
    queryDeal(item) {
    if (this.isPointer) {
    console.log(item);
    // 设置选中之后的效果
    if (this.list && this.list.length) {
    const tempList = [...this.list];
    tempList.forEach((singleObj) => {
    singleObj.checkedColor = undefined;
    if (item.date === singleObj.date) {
    singleObj.checkedColor = singleObj.color;
    }
    });
    this.list = tempList;
    }
    }
    },
    },
    mounted() {
    this.qEQCalendar();
    },
    };
    </script>

    <style>
    .EntDate{
    background-color: rgba(2, 47, 79, 0.8) !important;
    border: 1px solid rgba(2, 47, 79, 0.8) !important;
    }
    .EntDate /deep/ .popper__arrow::after{
    border-bottom-color: rgba(2, 47, 79, 0.8) !important;
    }
    .EntDate /deep/ .el-scrollbar__thumb{
    background-color: rgba(2, 47, 79, 0.8) !important;
    }
    .el-select-dropdown__item.hover, .el-select-dropdown__item:hover{
    background-color: transparent !important;
    }
    </style>

    <style lang="scss" scoped>
    .r-inline{
    display: inline-block;
    }
    .right-content{
    width: 380px;
    margin: 7px;
    border-radius: 9px;
    background-color: rgba(2, 47, 79, 0.8);
    }
    .day-title {
    border-bottom: 2px solid #03596f;
    padding: 1px 0 10px;
    height: 19px;
    .day-tt {
    float: left;
    text-align: center;
    color: #ffffff;
    width: 48px;
    }
    }
    .date-item {
    float: left;
    text-align: center;
    color: #fff;
    width: 34px;
    // padding: 2px 2px;
    padding: 4px 4px;
    margin: 0px 3px;
    &.is-last-month {
    color: #7d8c8c;
    }
    .day {
    border-radius: 17px;
    padding: 3px;
    height: 25px;
    line-height: 25px;
    text-shadow: #000 0.5px 0.5px 0.5px, #000 0 0.5px 0, #000 -0.5px 0 0, #000 0 -0.5px 0;
    background-color: #173953;
    }
    }
    .calendar{
    padding: 0px 6px;
    }
    .lvls {
    padding: 0px 6px 6px 13px;
    }
    .lvl-t-item {
    float: left;
    font-size:10px;
    padding-right: 3px;
    .lvl-t-ico {
    height: 12px;
    width: 12px;
    display: inline-block;
    margin-right: 5px;
    }
    .lvl-tt {
    color: #5b5e5f;
    }
    }
    // ================================================================================================= 日期框样式
    ::v-deep .el-input__inner {
    background-color: transparent;
    border-radius: 4px;
    border: 0px solid #DCDFE6;
    color: #Fcff00;
    font-size: 19px;
    font-weight: bolder;
    }
    ::v-deep .el-select .el-input .el-select__caret {
    color: #fcff00;
    font-weight: bolder;
    }
    // ================================================================================================= 日期框的下拉框样式
    .el-select-dropdown__item{
    background-color: rgba(2, 47, 79, 0.8);
    color: white;
    &:hover{
    background-color: rgba(2, 47, 79, 0.8);
    color: #5de6f8;
    cursor: pointer;
    }
    }
    .searchBtn {
    cursor: pointer;
    width: 60px;
    height: 28px;
    display: inline-block;
    background-color: rgba(2, 47, 79, 0.8);
    color: #a0daff;
    text-align: center;
    border: 1px solid #a0daff;
    border-radius: 5px;
    margin-left: 15px;
    line-height: 28px;
    }

    .isPointer{
    cursor: pointer;
    }
    .choseDateItemI{
    border: 2px solid #00e400 !important;
    box-shadow: #00e400 0px 0px 9px 2px;
    }
    .choseDateItemII{
    border: 2px solid #ffff00 !important;
    box-shadow: #ffff00 0px 0px 9px 2px;
    }
    .choseDateItemIII{
    border: 2px solid #ff7e00 !important;
    box-shadow: #ff7e00 0px 0px 9px 2px;
    }
    .choseDateItemIV{
    border: 2px solid #ff0000 !important;
    box-shadow: #ff0000 0px 0px 9px 2px;
    }
    .choseDateItemV{
    border: 2px solid #99004c !important;
    box-shadow: #99004c 0px 0px 9px 2px;
    }
    .choseDateItemVI{
    border: 2px solid #7e0023 !important;
    box-shadow: #7e0023 0px 0px 9px 2px;
    }
    .choseDateItemVII{
    border: 2px solid #cacaca !important;
    box-shadow: #cacaca 0px 0px 9px 2px;
    }
    </style>
    作者:海的对岸
    链接:https://juejin.cn/post/7033038877485072397

    收起阅读 »

    生成 UUID 的三种方式及测速对比!

    通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个 128 位标识符,通常表现为一串 32 位十六进制数字。 UUID 用于解决 ID 唯一的问题! 然而,如何确保唯一,这本身...
    继续阅读 »

    通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个 128 位标识符,通常表现为一串 32 位十六进制数字。


    image.png


    UUID 用于解决 ID 唯一的问题!


    然而,如何确保唯一,这本身就是一项挑战!


    如何保证所生成 ID 只有一个副本?如何保证两个 ID 之间没有相关性?唯一性和随机性之间怎么取舍......


    (OS:看过本瓜之前写的《理解 P/NP 问题时,我产生了一种已经触碰到人类认知天花板的错觉?!》这篇文章的朋友,应该知道:或许这个世界上没有随机这个东西?任何随机都能被量子计算算清楚,上帝到底掷骰子吗?没人知道......)


    是否有真正的随机,先按下不表,


    基于目前的算力精度,现在各种 UUID 生成器和不同版本的处理方式能最大限度的确保 ID 不重复,重复 UUID 码概率接近零,可以忽略不计。


    本篇带来 3 种 UUID 生成器! 👍👍👍


    UUID


    基于 RFC4122 标准创建的 UUID,它有很多版本:v1,v2..v5;


    uuid v1是使用主机 MAC 地址和当前日期和时间的组合生成的,这种方式意味着 uuid 是匿名的。


    uuid v4 是随机生成的,没有内在逻辑,组合方式非常多(2¹²⁸),除非每秒生成数以万亿计的 ID,否则几乎不可能产生重复,如果你的应用程序是关键型任务,仍然应该添加唯一性约束,以避免 v4 冲突。


    uuid v5与 v1 v4不同,它通过提供两条输入信息(输入字符串和命名空间)生成的,这两条信息被转换为 uuid;


    特性:

    • 完善;
    • 跨平台;
    • 安全:加密、强随机性;
    • 体积小:零依赖,占用空间小;
    • 良好的开源库支持:uuid command line


    上手:


    import { v4 as uuidv4 } from 'uuid';

    let uuid = uuidv4();

    console.log(uuid) // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

    Crypto.randomUUID


    Node.js API Crypto 提供 **randomUUID()** 方法,基于 RFC 4122 V4 生成随机数;


    上手:


    let uuid = crypto.randomUUID();

    console.log(uuid); // ⇨ "36b8f84d-df4e-4d49-b662-bcde71a8764f"

    Nano ID


    Nano ID 有 3 个 api:

    1. normal (blocking); 普通
    2. asynchronous;异步
    3. non-secure;非安全

    默认情况下,Nano ID 使用符号(A-Za-z0-9-),并返回一个包含 21 个字符的 ID(具有类似于UUID v4的重复概率)。


    特性:

    • 体积小:130 bytes (压缩后);
    • 零依赖;
    • 生成更快;
    • 安全:
    • 更短,只要 21 位;
    • 方便移植,支持 20 种编程语言.


    上手:


    import { nanoid } from 'nanoid'

    let uuid = nanoid();

    console.log(uuid) // ⇨ "V1StGXR8_Z5jdHi6B-myT"

    Nano IDnpm 下载趋势:


    image.png


    测速


    我们不妨来对比以上所提 3 种生成 UUID 的方式速度差异:


    // test-uuid-gen.js
    const { v4 as uuidv4 } = require('uuid');

    for (let i = 0; i < 10_000_000; i++) {
    uuidv4();
    }

    // test-crypto-gen.js
    const { randomUUID } = require('crypto');

    for (let i = 0; i < 10_000_000; i++) {
    randomUUID();
    }

    // test-nanoid-gen.js
    const { nanoid } = require('nanoid');

    for (let i = 0; i < 10_000_000; i++) {
    nanoid();
    }

    借助 hyperfine


    调用测试:hyperfine ‘node test-uuid-gen.js’ ‘node test-crypto-gen.js’ ‘node test-nanoid-gen.js’


    运行结果:


    img


    我们可以看到, 第二种 randomUUID() 比第三种 nanoid 快 4 倍左右,比第一种 uuid 快 12 倍左右~



    作者:掘金安东尼
    链接:https://juejin.cn/post/7033221241100042271

    收起阅读 »

    老板:你来弄一个团队代码规范!?

    一、背景 9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口) 小组的技术栈框架有Vue,React,Taro,Nuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我...
    继续阅读 »

    一、背景


    9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口)


    小组的技术栈框架有VueReactTaroNuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范


    到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速,最近得空分享出来~


    ⚠️本篇文章不会讲基础的具体的规范,而是从实践经验讲怎么制定规范以及落地规范


    image.png


    二、为什么要代码规范


    就不说了...大家懂的~
    image.png


    不是很了解的话,指路


    三、确定规范范围


    首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来


    第一步收集团队的技术栈情况,确定规范要包括的范围


    把规范梳理为三部分ESLintStyleLintCommitLint,结合团队实际情况分析如下



    • ESLint:团队统一用的TypeScript,框架用到了VueReactTaro、还有Nuxt

    • StyleLint:团队统一用的Less

    • CommitLint:git代码提交规范


    image.png
    当然,还需考虑团队后续可能会扩展到的技术栈,以保证实现的时候确保可扩展性


    四、调研业内实现方案


    常见以下3种方案




    1. 团队制定文档式代码规范,成员都人为遵守这份规范来编写代码



      靠人来保证代码规范存在不可靠,且需要人为review代码不规范,效率低





    2. 直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等



      a) 开源规范往往不能满足团队需求,可拓展性差; b) 业内提供的规范都是独立的(stylint只提供css代码规范,ESLint只提供JavaScript规范),是零散的,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)





    3. 基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库



      a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在





    五、我们的技术方案


    整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling@jd/eslint-config-selling@jd/commitlint-config-selling分别满足StyleLintESLintCommitLint



    1. @jd/stylelint-config-selling包括css、less、saas(团队暂未使用到)

    2. @jd/eslint-config-selling包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器

    3. @jd/commitlint-config-selling统一使用git


    向上提供一个简单的命令行工具,交互式初始化init、或者更新update规范


    image.png


    几个关键点


    1、用lerna统一管理包


    lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下

    项目结构如下图

    image.png


    2、三个基础包的依赖包都设置为生产依赖dependencies


    如下图,包@jd/eslint-config-selling的依赖包都写在了生产依赖,而不是开发依赖

    image.png
    解释下:
    开发依赖&生产依赖



    • 开发依赖:业务工程用的时候不会下载开发依赖中的包,业内常见的规范如standardairbnb都是写在开发依赖

      • 缺点:业务工程除了安装@jd/eslint-config-selling外,需要自己去安装前置依赖包,如eslint、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue...使用成本、维护升级成本较高

      • 优点:按需安装包,开发时不会安装多余的包(Lint相关的包在业务工程中都是开发依赖,所以只会影响开发时)



    • 生产依赖:业务工程用的时候会下载这些包

      • 优点:安装@jd/eslint-config-selling后,无需关注前置依赖包

      • 缺点:开发时会下载@jd/eslint-config-selling中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue




    3、提供简单的命令行


    这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了


    不会的,指路中高级前端必备:如何设计并实现一个脚手架



    组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去



    六、最重要的一点


    什么是一个好的规范?

    基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范


    所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定,比如几个人去制定styleLint的,几个人制定Vue的...


    然后拉会评审,大家统一通过的规范才敲定
    image.png
    最后以开源的方式维护升级,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范


    写在结尾


    以上就是我们团队在前端规范落地方面的经验~


    作者:jjjona0215
    链接:https://juejin.cn/post/7033210664844066853
    收起阅读 »

    如何优雅的使用枚举功能——Constants

    背景 在项目中,或多或少的会遇到使用枚举/快码/映射/字典,它们一般长这个样子。(PS:我不知道怎么称呼这个玩意) 在一些需要展示的地方,会使用下面的代码来展示定义。 <div>{{ statusList[status] }}</div&g...
    继续阅读 »

    背景


    在项目中,或多或少的会遇到使用枚举/快码/映射/字典,它们一般长这个样子。(PS:我不知道怎么称呼这个玩意)



    在一些需要展示的地方,会使用下面的代码来展示定义。


    <div>{{ statusList[status] }}</div>

    而在代码中,又会使用下面的形式进行判断。这样写会让代码里充斥着许多的 'draft' 字符串,非常不利于管理。


    if (status === 'draft') {
    // do sth...
    }

    基于这种情况,在使用时会先声明一个变量。


    const DRAFT = 'draft'

    if (status === DRAFT) {
    // do sth...
    }

    为了应对整个项目都会使用到的情况,会这样处理。


    export const statusList = {
    draft: '草稿',
    pending: '待处理',
    }

    export const statusKeys = {
    draft: 'draft',
    pending: 'pending',
    }

    看了隔壁后端同事的代码,在 Java 里,枚举的定义及使用一般是如下形式。于是我就有了写这个工具类的想法。


    public enum Status {
    DRAFT('draft', '草稿');

    Status(String code, String name) {
    this.code = code;
    this.name = name;
    }

    public String getCode() {
    return code;
    }

    public String getName() {
    return name;
    }
    }

    public void aFunction() {
    const draftCode = Status.DRAFT.getCode();
    }

    Constants


    直接上代码


    const noop = () => {}

    class Constants {
    constructor(obj) {
    Object.keys(obj).forEach((key) => {
    const initValue = obj[key];
    if (initValue instanceof Object) {
    console.error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
    // throw new Error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
    }
    const newKey = `_${key}`;
    this[newKey] = initValue;
    Object.defineProperty(this, key, {
    configurable : true,
    enumerable : true,
    get: function() {
    const value = this[newKey];
    const constructorOfValue = value.constructor;
    const entry = [key, value];
    ['getKey', 'getValue'].forEach((item, index) => {
    constructorOfValue.prototype[item] = () => {
    constructorOfValue.prototype.getKey = noop;
    constructorOfValue.prototype.getValue = noop;
    return entry[index];
    }
    })
    return value;
    },
    set: function(newValue) {
    this[newKey] = newValue;
    }
    })
    });
    }
    }

    测试


    const testValues = {
    draft: '草稿',
    id: 1,
    money: 1.2,
    isTest: true,
    testObj: {},
    testArray: [],
    }
    const constants = new Constants(testValues)

    const test = (result, expect) => {
    const isExpected = result === expect
    if (isExpected) {
    console.log(`PASS: The result is ${result}`)
    } else {
    console.error(`FAIL: the result is ${result}, should be ${expect}`)
    }
    }

    test(constants.draft, '草稿')
    test(constants.draft.getKey(), 'draft')
    test(constants.draft.getValue(), '草稿')

    test(constants.id, 1)
    test(constants.id.getKey(), 'id')
    test(constants.id.getValue(), 1)

    test(constants.money, 1.2)
    test(constants.money.getKey(), 'money')
    test(constants.money.getValue(), 1.2)

    test(constants.isTest, true)
    test(constants.isTest.getKey(), 'isTest')
    test(constants.isTest.getValue(), true)
    a = 'test'
    test(a.getKey(), undefined)
    test(a.getValue(), undefined)


    作者:Wetoria
    链接:https://juejin.cn/post/7033220309386395679

    收起阅读 »

    CSS mask 实现鼠标跟随镂空效果

    偶然在某思看到这样一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?,最后实现效果是这样的 进一步,还能实现任意形状的镂空效果 鼠标经过的地方清晰可见,其他地方则是模糊的。 可能一开始无从下手,不要急,可以先从简单的、类似的效果开始,一步一步尝试...
    继续阅读 »

    偶然在某思看到这样一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?,最后实现效果是这样的


    237330258-6181fcdb471cf


    进一步,还能实现任意形状的镂空效果


    Kapture 2021-11-20 at 13.44.26


    鼠标经过的地方清晰可见,其他地方则是模糊的。


    可能一开始无从下手,不要急,可以先从简单的、类似的效果开始,一步一步尝试,一起看看吧。


    一、普通半透明的效果


    比如平时开发中碰到更多的可能是一个半透明的效果,有点类似于探照灯(鼠标外面的地方是半透明遮罩,看起来会暗一点)。如下:


    image-20211117200548416


    那先从这种效果开始吧,假设有这样一个布局:


    <div class="wrap" id="img">
    <img class="prew" src="https://tva1.sinaimg.cn/large/008i3skNgy1gubr2sbyqdj60xa0m6tey02.jpg">
    </div>

    那么如何绘制一个镂空的圆呢?先介绍一种方法


    其实很简单,只需要一个足够大的投影就可以了,原理如下


    image-20211117195737723


    这里可以用伪元素::before来绘制,结构更加精简。用代码实现就是


    .wrap::before{
    content:'';
    position: absolute;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%); /*默认居中*/
    box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
    }

    可以得到这样的效果


    image-20211117200548416


    二、借助 CSS 变量传递鼠标位置


    按照以往的经验,可能会在 js 中直接修改元素的 style 属性,类似这样


    img.addEventListener('mousemove', (ev) => {
    img.style.left = '...';
    img.style.top = '...';
    })

    但是这样交互与业务逻辑混杂在一起,不利于后期维护。其实,我们只需要鼠标的坐标,在 CSS 中也能完全实现跟随的效果。


    这里借助 CSS 变量,那一切就好办了!假设鼠标的坐标是 [--x,--y](范围是[0, 1]),那么遮罩的坐标就可以使用 calc计算了


    .wrap::before{
    left: calc(var(--x) * 100%);
    top: calc(var(--y) * 100%);
    }

    然后鼠标坐标的获取可以使用 JS 来计算,也比较容易,如下


    img.addEventListener('mousemove', (ev) => {
    img.style.setProperty('--x', ev.offsetX / ev.target.offsetWidth);
    img.style.setProperty('--y', ev.offsetY / ev.target.offsetHeight);
    })

    这样,半透明效果的镂空效果就完成了


    Kapture 2021-11-17 at 20.26.27


    完整代码可以访问: backdrop-shadow (codepen.io)


    三、渐变也能实现半透明的效果


    除了上述阴影扩展的方式,CSS 径向渐变也能实现这样的效果


    绘制一个从透明到半透明的渐变,如下


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background: radial-gradient( circle at center, transparent 50px, rgba(0,0,0,.5) 51px);
    }

    可以得到这样的效果


    image-20211117200548416


    然后,把鼠标坐标映射上去就可以了。从这里就可以看出 CSS 变量的好处,无需修改 JS,只需要在CSS中修改渐变中心点的位置就可以实现了


    .wrap::before{
    background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
    }

    Kapture 2021-11-18 at 19.51.30


    四、背景模糊的效果尝试


    CSS 中有一个专门针对背景(元素后面区域)的属性:backdrop-filter。使用方式和 filter完全一致!


    backdrop-filter: blur(10px);

    下面是 MDN 中的一个示意效果


    image-20211119191341911


    backdrop-filter是让当前元素所在区域后面的内容模糊,要想看到效果,需要元素本身半透明或者完全透明;而filter是让当前元素自身模糊。有兴趣的可以查看这篇文章: CSS backdrop-filter简介与苹果iOS毛玻璃效果 « 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)


    需要注意的是,这种模糊与背景的半透明度没有任何关系,哪怕元素本身是透明的,仍然会有效果。例如下面是去除背景后的效果 ,整块都是模糊的


    image-20211119193956128


    如果直接运用到上面的例子会怎么样呢?


    1. 阴影实现


    在上面第一个例子中添加 backdrop-filter


    .wrap::before{
    content:'';
    position: absolute;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%); /*默认居中*/
    box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
    backdrop-filter: blur(5px)
    }

    得到效果如下


    Kapture 2021-11-19 at 19.20.57


    可以看到圆形区域是模糊的,正好和希望的效果相反。其实也好理解,只有圆形区域才是真实的结构,外面都是阴影,所以最后作用的范围也只有圆形部分


    2. 渐变实现


    现在在第二个例子中添加 backdrop-filter


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
    backdrop-filter: blur(5px)
    }

    效果如下


    Kapture 2021-11-19 at 19.31.22


    已经全部都模糊了,只是圆形区域外暗一些。由于::before的尺寸占据整个容器,所以整个背后都变模糊了,圆形外部比较暗是因为半透明渐变的影响。


    总之还是不能满足我们的需求,需要寻求新的解决方式。


    五、CSS MASK 实现镂空


    与其说是让圆形区域不模糊,还不如说是把那块区域给镂空了。就好比之前是一整块磨砂玻璃,然后通过 CSS MASK 打了一个圆孔,这样透过圆孔看到后面肯定是清晰的。


    可以对第二个例子稍作修改,通过径向渐变绘制一个透明圆,剩余部分都是纯色的遮罩层,示意如下


    image-20211120113029155


    用代码实现就是


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    -webkit-mask: radial-gradient( circle at calc(var(--x, .5) * 100% ) calc(var(--y, .5) * 100% ), transparent 50px, #000 51px);
    background: rgba(0,0,0,.3);
    backdrop-filter: blur(5px)
    }

    这样就实现了文章开头的效果


    237330258-6181fcdb471cf


    完整代码可以查看:backdrop-mask (codepen.io)


    六、CSS MASK COMPOSITE 实现更丰富的镂空效果


    除了使用径向渐变绘制遮罩层以外,还可以通过 CSS MASK COMPOSITE(遮罩合成)的方式来实现。标准关键值如下(firefox支持):


    /* Keyword values */
    mask-composite: add; /* 叠加(默认) */
    mask-composite: subtract; /* 减去,排除掉上层的区域 */
    mask-composite: intersect; /* 相交,只显示重合的地方 */
    mask-composite: exclude; /* 排除,只显示不重合的地方 */

    遮罩合成是什么意思呢?可以类比 photoshop 中的形状合成,几乎是一一对应的


    image-20211120123004278


    -webkit-mask-composite 与标准下的值有所不同,属性值非常多,如下(chorme 、safari 支持)


    -webkit-mask-composite: clear; /*清除,不显示任何遮罩*/
    -webkit-mask-composite: copy; /*只显示上方遮罩,不显示下方遮罩*/
    -webkit-mask-composite: source-over;
    -webkit-mask-composite: source-in; /*只显示重合的地方*/
    -webkit-mask-composite: source-out; /*只显示上方遮罩,重合的地方不显示*/
    -webkit-mask-composite: source-atop;
    -webkit-mask-composite: destination-over;
    -webkit-mask-composite: destination-in; /*只显示重合的地方*/
    -webkit-mask-composite: destination-out;/*只显示下方遮罩,重合的地方不显示*/
    -webkit-mask-composite: destination-atop;
    -webkit-mask-composite: xor; /*只显示不重合的地方*/

    是不是一脸懵?这里做了一个对应的效果图,如果不太熟练,使用的时候知道有这样一个功能,然后对着找就行了


    image-20211120130421281


    回到这里,可以绘制一整块背景和一个圆形背景,然后通过遮罩合成排除(mask-composite: exclude)打一个孔就行了,实现如下


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='25' cy='25' r='25' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
    -webkit-mask-size: 50px, 100%;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
    -webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
    mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
    background: rgba(0,0,0,.3);
    backdrop-filter: blur(5px)
    }

    需要注意-webkit-mask-position中的计算,这样也能很好的实现这个效果


    237330258-6181fcdb471cf


    完整代码可以查看:backdrop-mask-composite (codepen.io)


    你可能已经发现,上述例子中的圆是通过 svg 绘制的,还用到了遮罩合成,看着好像更加繁琐了。其实呢,这是一种更加万能的解决方式,可以带来无限的可能性。比如我需要一个星星⭐️的镂空效果,很简单,先通过一个绘制软件画一个


    image-20211120131056453


    然后把这段 svg 代码转义一下,这里推荐使用张鑫旭老师的SVG在线压缩合并工具


    image-20211120131335734


    替换到刚才的例子中就可以了


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='96' height='91' viewBox='0 0 96 91' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M48 0l11.226 34.55h36.327l-29.39 21.352L77.39 90.45 48 69.098 18.61 90.451 29.837 55.9.447 34.55h36.327L48 0z' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
    -webkit-mask-size: 50px, 100%;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
    -webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
    mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
    background: rgba(0,0,0,.3);
    backdrop-filter: blur(5px)
    }

    星星镂空实现效果如下


    Kapture 2021-11-20 at 13.35.28


    完整代码可以查看:backdrop-star (codepen.io)


    再比如一个心形❤,实现效果如下


    Kapture 2021-11-20 at 13.44.26


    完整代码可以查看:backdrop-heart (codepen.io)


    只有想不到,没有做不到



    作者:XboxYan
    链接:https://juejin.cn/post/7033188994641100831
    收起阅读 »

    Unable to extract the trust manager on Android10Platform 完美解决

    Unable to extract the trust manager on Android10Platform网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:builder.sslSocketFactory(sslContext.getSoc...
    继续阅读 »

    Unable to extract the trust manager on Android10Platform

    网上有大致有两种解决方案,但都不靠谱。产生这个异常的根本原因是:

    builder.sslSocketFactory(sslContext.getSocketFactory());
    这个方式已经过时了,需要新的方式,如下:



    final X509TrustManager trustManager = new X509TrustManager() {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
    return new X509Certificate[0];
    }
    };
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, new X509TrustManager[]{trustManager}, new SecureRandom());

    OkHttpClient.Builder builder = new OkHttpClient().newBuilder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .readTimeout(15,TimeUnit.SECONDS)
    .addInterceptor(logInterceptor)
    .sslSocketFactory(sslContext.getSocketFactory(),trustManager)
    .hostnameVerifier(new HostnameVerifier() {

    @Override
    public boolean verify(String hostname, SSLSession session) {

    return true;
    }
    });

    收起阅读 »

    面试官:一千万数据,怎么快速查询?

    sql
    前言 面试官: 来说说,一千万的数据,你是怎么查询的?B哥:直接分页查询,使用limit分页。面试官:有实操过吗?B哥:肯定有呀 此刻献上一首《凉凉》 也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。 今天就来带大家实操一下,这次...
    继续阅读 »

    前言



    • 面试官: 来说说,一千万的数据,你是怎么查询的?
    • B哥:直接分页查询,使用limit分页。
    • 面试官:有实操过吗?
    • B哥:肯定有呀

    此刻献上一首《凉凉》


    也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。


    今天就来带大家实操一下,这次是基于MySQL 5.7.26做测试


    准备数据


    没有一千万的数据怎么办?


    创建呗


    代码创建一千万?那是不可能的,太慢了,可能真的要跑一天。可以采用数据库脚本执行速度快很多。


    创建表

    CREATE TABLE `user_operation_log`  (
      `id` int(11NOT NULL AUTO_INCREMENT,
      `user_id` varchar(64CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `ip` varchar(20CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `op_data` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr1` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr2` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr3` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr4` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr5` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr6` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr7` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr8` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr9` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr10` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr11` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr12` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`USING BTREE
    ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

    创建数据脚本

    采用批量插入,效率会快很多,而且每1000条数就commit,数据量太大,也会导致批量插入效率慢


    DELIMITER ;;
    CREATE PROCEDURE batch_insert_log()
    BEGIN
      DECLARE i INT DEFAULT 1;
      DECLARE userId INT DEFAULT 10000000;
     set @execSql = 'INSERT INTO `test`.`user_operation_log`(`user_id`, `ip`, `op_data`, `attr1`, `attr2`, `attr3`, `attr4`, `attr5`, `attr6`, `attr7`, `attr8`, `attr9`, `attr10`, `attr11`, `attr12`) VALUES';
     set @execData = '';
      WHILE i<=10000000 DO
       set @attr = "'测试很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的属性'";
      set @execData = concat(@execData"(", userId + i, ", '10.0.69.175', '用户登录操作'"","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr","@attr")");
      if i % 1000 = 0
      then
         set @stmtSql = concat(@execSql@execData,";");
        prepare stmt from @stmtSql;
        execute stmt;
        DEALLOCATE prepare stmt;
        commit;
        set @execData = "";
       else
         set @execData = concat(@execData",");
       end if;
      SET i=i+1;
      END WHILE;

    END;;
    DELIMITER ;

    开始测试



    哥的电脑配置比较低:win10 标压渣渣i5 读写约500MB的SSD



    由于配置低,本次测试只准备了3148000条数据,占用了磁盘5G(还没建索引的情况下),跑了38min,电脑配置好的同学,可以插入多点数据测试


    SELECT count(1FROM `user_operation_log`

    返回结果:3148000


    三次查询时间分别为:



    • 14060 ms
    • 13755 ms
    • 13447 ms

    普通分页查询


    MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取。


    MySQL分页查询语法如下:


    SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset


    • 第一个参数指定第一个返回记录行的偏移量
    • 第二个参数指定返回记录行的最大数目

    下面我们开始测试查询结果:


    SELECT * FROM `user_operation_log` LIMIT 1000010

    查询3次时间分别为:



    • 59 ms
    • 49 ms
    • 50 ms

    这样看起来速度还行,不过是本地数据库,速度自然快点。


    换个角度来测试


    相同偏移量,不同数据量

    SELECT * FROM `user_operation_log` LIMIT 1000010
    SELECT * FROM `user_operation_log` LIMIT 10000100
    SELECT * FROM `user_operation_log` LIMIT 100001000
    SELECT * FROM `user_operation_log` LIMIT 1000010000
    SELECT * FROM `user_operation_log` LIMIT 10000100000
    SELECT * FROM `user_operation_log` LIMIT 100001000000

    查询时间如下:

















































    数量 第一次 第二次 第三次
    10条 53ms 52ms 47ms
    100条 50ms 60ms 55ms
    1000条 61ms 74ms 60ms
    10000条 164ms 180ms 217ms
    100000条 1609ms 1741ms 1764ms
    1000000条 16219ms 16889ms 17081ms

    从上面结果可以得出结束:数据量越大,花费时间越长


    相同数据量,不同偏移量

    SELECT * FROM `user_operation_log` LIMIT 100100
    SELECT * FROM `user_operation_log` LIMIT 1000100
    SELECT * FROM `user_operation_log` LIMIT 10000100
    SELECT * FROM `user_operation_log` LIMIT 100000100
    SELECT * FROM `user_operation_log` LIMIT 1000000100










































    偏移量 第一次 第二次 第三次
    100 36ms 40ms 36ms
    1000 31ms 38ms 32ms
    10000 53ms 48ms 51ms
    100000 622ms 576ms 627ms
    1000000 4891ms 5076ms 4856ms

    从上面结果可以得出结束:偏移量越大,花费时间越长


    SELECT * FROM `user_operation_log` LIMIT 100100
    SELECT idattr FROM `user_operation_log` LIMIT 100100

    如何优化


    既然我们经过上面一番的折腾,也得出了结论,针对上面两个问题:偏移大、数据量大,我们分别着手优化


    优化偏移量大问题


    采用子查询方式

    我们可以先定位偏移位置的 id,然后再查询数据


    SELECT * FROM `user_operation_log` LIMIT 100000010

    SELECT id FROM `user_operation_log` LIMIT 10000001

    SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 10000001LIMIT 10

    查询结果如下:































    sql 花费时间
    第一条 4818ms
    第二条(无索引情况下) 4329ms
    第二条(有索引情况下) 199ms
    第三条(无索引情况下) 4319ms
    第三条(有索引情况下) 201ms

    从上面结果得出结论:



    • 第一条花费的时间最大,第三条比第一条稍微好点
    • 子查询使用索引速度更快

    缺点:只适用于id递增的情况


    id非递增的情况可以使用以下写法,但这种缺点是分页查询只能放在子查询里面


    注意:某些 mysql 版本不支持在 in 子句中使用 limit,所以采用了多个嵌套select


    SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 100000010AS t)

    采用 id 限定方式

    这种方法要求更高些,id必须是连续递增,而且还得计算id的范围,然后使用 between,sql如下


    SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100

    SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100

    查询结果如下:



















    sql 花费时间
    第一条 22ms
    第二条 21ms

    从结果可以看出这种方式非常快


    注意:这里的 LIMIT 是限制了条数,没有采用偏移量


    优化数据量大问题


    返回结果的数据量也会直接影响速度


    SELECT * FROM `user_operation_log` LIMIT 11000000

    SELECT id FROM `user_operation_log` LIMIT 11000000

    SELECT id, user_id, ip, op_data, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9, attr10, attr11, attr12 FROM `user_operation_log` LIMIT 11000000

    查询结果如下:























    sql 花费时间
    第一条 15676ms
    第二条 7298ms
    第三条 15960ms

    从结果可以看出减少不需要的列,查询效率也可以得到明显提升


    第一条和第三条查询速度差不多,这时候你肯定会吐槽,那我还写那么多字段干啥呢,直接 * 不就完事了


    注意本人的 MySQL 服务器和客户端是在同一台机器上,所以查询数据相差不多,有条件的同学可以测测客户端与MySQL分开


    SELECT * 它不香吗?

    在这里顺便补充一下为什么要禁止 SELECT *。难道简单无脑,它不香吗?


    主要两点:



    1. 用 "SELECT * " 数据库需要解析更多的对象、字段、权限、属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
    2. 增大网络开销,* 有时会误带上如log、IconMD5之类的无用且大文本字段,数据传输size会几何增涨。特别是MySQL和应用程序不在同一台机器,这种开销非常明显。

    结束


    最后还是希望大家自己去实操一下,肯定还可以收获更多,欢迎留言!!


    创建脚本我给你正好了,你还在等什么!!!


    再奉上我之前 MySQL 如何优化


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

    Android 基础架构组面试题 | 面试

    SDK相关面试的时候我觉得哦,这些sdk有任意其实你研究的比较深入就行了,应该能在面试中表现的很好了。还有就是个人建议最好还是在单一方向研究的更深入一点,别的只要大概知道干什么的就行了。配置中心以及灰度测试app必备工具之一,配置中心主要负责的就是动态化的配置...
    继续阅读 »

    SDK相关

    面试的时候我觉得哦,这些sdk有任意其实你研究的比较深入就行了,应该能在面试中表现的很好了。还有就是个人建议最好还是在单一方向研究的更深入一点,别的只要大概知道干什么的就行了。

    1. 配置中心以及灰度测试

    app必备工具之一,配置中心主要负责的就是动态化的配置,比如文本展示类似这些的。sdk提供方需要负责的是提供动态更新能力,这里有个差异化更新,只更新dif部分,还有就是流量优化等等需要开发同学考虑的。然后可以考虑下存储性能方面的提升等。

    而abtest也是app必备工具之一了,动态的下发实验策略,之后开发同学可以切换实验的页面。另外主要需要考虑灰度结果计算,分桶以及版本过滤白名单等等。这里只是一个简单的介绍不展开,因为我只是一个使用方。

    1. 调试组件

    个人还是更推荐滴滴的Dokit,功能点比较多而且接入相对来说比较简单。而且提供了很多给开发同学定制的能力,可以在debug情况下增加很多业务相关的测试功能,方便测试同学,核心还是浮窗太方便了。

    当然很多实验性的预研功能等其实都可以直接接在这里,然后在测试环境下充分展开,之后在进行线上灰度方案。还有一些具有风险的hook操作,个人也比较建议放在debug组件上。

    1. 性能监控框架

    这部分有几个不同的方面,首先是异常崩溃方面的,另外则是性能监控方面的,但是他们整体是划分在一起的,都属于线上性能监控体系的。

    Crash相关的,可以从爱奇艺的xCrash学起。包含了崩溃日志,ANR以及native crash,因为版本适配的问题ANR在高版本上已经不是这么好捞了,还有就是native crash相关的。是一个非常牛逼的库了。

    而线上的性能监控框架可以从腾讯的Matrix学起,以前有两篇文章介绍的内容也都是和Matrix相关的, Matrix首页上也有介绍,比如fps,卡顿,IO,电池,内存等等方面的监控。其中卡顿监控涉及到的就是方法前后插桩,同时要有函数的mapping表,插桩部分整体来说比较简单感觉。

    另外关于线上内存相关的,推荐各位可以学习下快手的koom, 对于hprof的压缩比例听说能达到70%,也能完成线上的数据回捞以及监控等等,是一个非常屌的框架。下面给大家一个抄答案的方式。字节也有一个类似的原理其实也差不多。

    主进程发现内存到达阈值的时候,用leakcanary的方案,通过shark fork进程内存,之后生成hrop。由于hrop文件相对较大,所以我们需要对于我们所要分析的内容进行筛选,可以通过xhook,之后对hrop的写入操作进行hook,当发现写入内容的类型符合我们的需要的情况下才进行写入。

    而当我们要做线上日志回捞的情况,需要对hprof 进行压缩,具体算法可以参考koom/raphel,有提供对应的压缩算法。

    最后线上回捞机制就是基于一个指令,回捞线上符合标准的用户的文件操作,这个自行设计。

    其实上述几个库都还是有一个本质相关的东西,那么就是plthook,这个上面三个库应该都有对其的使用,之前是爱奇艺的xhook,现在是字节的bhook, 这个大佬也是我的偶像之一了,非常离谱了算是。

    Android 性能采集之Fps,Memory,Cpu 和 Android IO监控

    最近已经不咋写这部分相关了,所以也就没有深挖,但是后续可能会有一篇关于phtead hook相关的,也是之前matrix更新的一个新东西,还在测试环境灰度阶段。

    1. 基础网络组件

    虽然核心可能还是三方网络库,但是因为基本所有公司都对网络方面有调整和改动,以及解析器等方面的优化,其实可以挖的东西也还是蛮多的。

    应付面试的同学可以看看Android网络优化方案。当然还是要具体问题具体分析,毕竟头疼医头,脚疼医脚对吧。

    之前和另外一个朋友聊了下,其实很多厂对json解析这部分有优化调整,通过apt之后更换原生成原生的解析方式,加快反序列化速度的都是可以考虑考虑的。

    1. 埋点框架

    其实这个应该要放在更前面一点的,数据上报数据分析啥的其实都还是蛮重要的。

    这部分因为我完全没写过哦,所以我压根不咋会,但是如果你会的话,面试的时候展开说说,可以帮助你不少。

    另外还需要有线上的异常用户数据回捞系统,方便开发同学主动去把线上有异常的用户的日志给收集回来。

    但是有些刁钻的页面曝光监控啦,自动化埋点啥的其实还是写过一点的,有兴趣的可以翻翻历史,还有github 上还有demo。

    AndroidAutoTrack demo工程

    1. 启动相关

    通过DAG(有向无环图)的方式将sdk的初始化拆解成一个个task,之后理顺依赖关系,让他们能按照固定的顺序向下执行。

    核心需要处理的是依赖关系,比如说其实埋点库依赖于网络库初始化,然后APM相关的则依赖于埋点库和配置中心abtest等等,这样的依赖关系需要开发同学去理顺的。

    另外就是把sdk的粒度打的细碎一点,更容易观察每个sdk任务的耗时情况,之后增加task阈值告警,超过某个加载速度就通知到相应的同学改一下。

    多线程是能优化掉一部分,但是也需要避免频繁线程调度。还有就是我个人觉得这些启动相关的东西因为都无法使用sdk级别的灰度,所以改动最好慎重一点。出发点始终都是好的,但是还是结果导向吧。

    启动优化的核心,我个人始终坚持的就是延迟才能优化。开发人员很难做到优化代码执行的复杂度,执行时间之类的。尽人事听天命,玄学代码。

    1. 中间件(图片 日志 存储 基础信息)

    这部分没啥,最好是对第三方库有一层隔离的思维,但是这个隔离也需要对应的同学对于程序设计方面有很好的思维,说起来简单,其实也蛮复杂的。

    这里就不展开了,感觉面试也很少会问的很细。

    1. 第三方sdk大杂烩(偏中台方向)

    基本一个app现在都有啥分享啦,推送啦,支付啦,账号体系啦,webview,jsbridge等等服务于应用内的一些sdk,这些东西就比较偏向于业务。

    有兴趣的可以看看之前写的两篇关于sdk设计相关的。

    活学活用责任链 SDK开发的一点点心得 Android厂商推送Plugin化

    1. 其他方面

    大公司可能都会有些动态化方案的考虑,比如插件化啊动态化之类的。这部分在下确实不行,我就不展开了啊。

    编译相关

    1. 描述下android编译流程

    基架很容易碰到的面试题,以前简单的描述写过。聊聊Android编译流程

    虽然是几年前的知识点了,但是还是要拆开高低版本的agp做比较的。所以这部分可以回答下,基本这题就能简单的拿下了。

    1. Gradle 生命周期

    简单的说下就是buildSrc先编译,之后是根目录的settings.gradle, 根build.gradle,最后才是module build

    网上一堆,你自己翻一番就知道了。

    1. apt是编译中哪个阶段

    APT解析的是java 抽象语法树(AST),属于javac的一部分流程。大概流程:.java -> AST -> .class

    聊聊AbstractProcessor和Java编译流程

    1. Dex和class有什么区别

    链接传送门

    Class与dex的区别

    1)虚拟机: class用jvm执行,dex用dvm执行

    2)文档: class中冗余信息多,dex会去除冗余信息,包含所有类,查找方便,适合手机端

    JVM与DVM

    1)JVM基于栈(使用栈帧,内存),DVM基于寄存器,速度更快,适合手机端

    2)JVM执行Class字节码,DVM执行DEX

    3)JVM只能有一个实例,一个应用启动运行在一个DVM

    DVM与ART

    1)DVM:每次运行应用都需要一次编译,效率降低。JIT

    2)ART:Android5.0以上默认为ART,系统会在进程安装后进行一次预编译,将代码转为机器语言存在本地,这样在每次运行时不用再进行编译,提高启动效率;。 AOP & JIT

    1. Transform是如何被执行的

    Transform 在编译过程中会被封装成Task 依赖其他编译流程的Task执行。

    image.png

    1. Transform和其他系统Transform执行的顺序

    其实这个题目已经是个过期了,后面对这些都合并整合了,而且最新版的api也做了替换,要不然考虑下回怼下面试官?

    Transform和Task之间有关?

    1. 如何监控编译速度变慢问题
    ./gradlew xxxxx -- scan

    之后会生成一个gradle的网页,填写下你的邮箱就好了。

    另外一个相对来说比较简单了。通过gradle原生提供的listener进行就行了。


    // 耗时统计kt化
    class TimingsListener : TaskExecutionListener, BuildListener {
    private var startTime: Long = 0L
    private var timings = linkedMapOf<String, Long>()


    override fun beforeExecute(task: Task) {
    startTime = System.nanoTime()
    }

    override fun afterExecute(task: Task, state: TaskState) {
    val ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
    task.path
    timings[task.path] = ms
    project.logger.warn("${task.path} took ${ms}ms")
    }

    override fun buildFinished(result: BuildResult) {
    project.logger.warn("Task timings:")
    timings.forEach {
    if (it.value >= 50) {
    project.logger.warn("${it.key} cos ms ${it.value}\n")
    }
    }
    }

    override fun buildStarted(gradle: Gradle) {

    }

    override fun settingsEvaluated(settings: Settings) {
    }

    override fun projectsLoaded(gradle: Gradle) {

    }

    override fun projectsEvaluated(gradle: Gradle) {

    }

    }

    gradle.addListener(TimingsListener())

    1. Gradle中如何给一个Task前后插入别的任务

    最简单的可以考虑直接获取到Task实例,之后在after和before插入一些你所需要的代码。

    另外一个就是通过dependOn前置和finalizedBy挂载一个任务 mustAfter

    Gradle 使用指南 -- Gradle Task

    1. ksp APT Transform的区别

    ksp 是kotlin专门独立的ast语法树

    apt 是java 的ast语法树

    transform是 agp 专门修改字节码的一个方法。

    反杀时刻AsmClassVisitorFactory,可以看看我之前写的那篇文章。

    1. Transform上的编译优化能做哪些?

    虽然是个即将过期的api,但是大家对他的改动还是都比较多的。

    首先肯定是需要完成增量编译的,具体的可以参考我的demo工程。记住,所有的transfrom都要全量。

    另外可以考虑多线程优化,将转化操作移动到子线程内,建议使用gradle内部的共享线程。

    参考agp最新做法,抽象出一个新的interface,之后通过spi串联,之后将asm链式调用。我的文章也介绍过,具体的点在哪里自己盘算。

    现在准备好告别Transform了吗

    1. aar 源码切换插件原理

    这个前几天刚介绍过,原理和方案业内都差不多,mulite-repo应该都需要这个东西的。我的版本也比较简陋,大厂内部肯定都会有些魔改的。

    相对来说功能肯定会更丰富,更全面一点。

    aar和源码切换插件Plus

    1. 你们有哪些保证代码质量的手段

    最简单的方式还是通过静态扫描+pipline 处理,之后在合并mr之前进行一次拦截。

    静态扫描方式比较多,下面给大家简单的介绍下

    阿里的sonar 但是对kt的支持很糟糕,因为阿里使用,所以有很多现成的规则可以使用,但是如果从0-1接入,你可能会直接放弃。

    原生的lint,可以基于原生提供的lint api,对其进行开发,支持种类也多,基本上算是一个非常优秀的方案了,但是由于文档资料较少,对于开发的要求可能会较高。

    AndroidLint

    1. 如何对第三方的依赖做静态检查?

    魔高一尺道高一丈。lint还是能解决这个问题的。

    Tree Api+ClassScanner = 识别三方隐私权限调用

    1. R.java code too large 解决方案

    又是一个过期的问题,尽早升级agp版本,让R8帮你解决这个问题,R文件完全可以内联的。

    或者用别的AGP插件的R inline也可以解决这个问题。

    1. R inline 你需要注意些什么?

    预扫描,先收集调用的信息,之后在进行替换。还有javac 的时候可能就因为文件过大,直接挂掉了。

    1. 一个类替换父类 比如所有activity实现类替换baseactivity

    class node 直接替换 superName ,想起了之前另外一个问题,感觉主要是要对构造函数进行修改,否则也会出异常。

    1. R8 D8 以及混淆相关的,还有R8除了混淆还能干些什么? 混淆规则有没有碰到什么奇怪的问题?

    D8Dx的区别,主要涉及到编译速度以及编译产物的体积,包体积大概小11%。

    R8 则是变更了整个编译流程的,其中我觉得最微妙的就是java8 lambda相关的,脱糖前后的差别还是比较大的。同时R8也少了很多之前的Transform。

    R8的混淆部分,混淆除了能增加代码阅读难度意外,更多的是对于代码优化方面的。 比如无效代码优化, 同时也删除代码等等都可以做。

    1. 编译的时候有没有碰到javac的常量优化

    javac会将静态常量直接优化成具体的数值。但是尤其是多模块场景下尤其容易出现异常,看起来是个实际的常量引用,但是产物上却是一个具体的常量值了。

    其他部分

    组件化相关

    不仅仅要聊到路由,还需要聊下业务仓库的设计,如何避免两个模块之间相互相互引用导致的环问题。

    另外就是路由的apt aop的部分都可以深入的聊一下。

    如果只聊路由的话,你就只说了一个字符串匹配规则,非常无聊了。

    路由跳转

    路由跳转只是一小部分,其核心原理就是字符串匹配,之后筛选出符合逻辑的页面进行跳转。

    另外就是拦截器的设计,同步异步拦截器两种完全不同的写法。

    其原理基于apt+transform ,apt负责生成模块德 路由表,而transform则负责将各个模块的路由表进行收集。

    服务发现

    类似路由表,但是维护的是一个基于键值的类构造。ab之间当有相互依赖的情况下,可以通过基于接口编程的方式进行调整,互相只依赖抽象的接口,之后实现类在内部,通过注册的机制。之后在实际的使用地方用服务发现的机制寻找。

    虚拟机部分

    很多人会觉得虚拟机这部分都是硬八股,比较无聊。但是其实有时候我们碰到的一些字节码相关的问题就和这部分基础姿势相关了。

    虽然用的比较少,但是也不是一个硬八股,比hashmap好玩太多了。

    依赖注入

    和服务发现类似,也是拿来解决不同模块间的依赖问题。可以使用hilt,依赖注入的好处就是连构造的这部分工作也有di完成了,而且构造能力更多样。可以多参数构造。

    总结

    其实以当前来说安卓的整个体系相对来说很复杂,第三方库以及源代码量都比较大,并不是要求每个同学都对这些有一个良好的掌握,但是大体上应该了解的还是需要了解的。

    面试造火箭可不是浪得虚名啊,但是鸡架可能还是需要使用到其中一些奇奇怪怪的黑科技的。

    好了胡扯结束了,今天的文章就到此为止了。

    原文链接:https://juejin.cn/post/7032625978023084062?utm_source=gold_browser_extension

    收起阅读 »

    你用过HandlerThread么?

    前言我们都用过Handler,也很熟悉怎么使用Handler,肯定也知道Handler使用过程中的注意事项,那就是内存泄漏,也知道大部分内存泄漏都是因为静态变量引用的问题。Handler是一个内部类,非static内部类或者匿名内部类都会持有外部类的引用。如果...
    继续阅读 »

    前言

    我们都用过Handler,也很熟悉怎么使用Handler,肯定也知道Handler使用过程中的注意事项,那就是内存泄漏,也知道大部分内存泄漏都是因为静态变量引用的问题。Handler是一个内部类,非static内部类或者匿名内部类都会持有外部类的引用。如果此时Activty退出了, handler持有他的引用,则这个Activity 并不会被销毁,其实还是在内存中,所以就造成了内存泄漏 (Memory Leak) 的问题。怎么解决这个问题,网上都有很成熟的文章和技术实现,这里不再累赘,这期主要讲下Handler的另一种使用方式HandlerThread。

    一、HandlerThread的本质

    HandlerThread 本质上就是一个普通Thread。

    Handler完成两个线程通信的代码中,需要调用Looper.prepare() 为一个线程开启一个消息循环,默认情况下Android中新诞生的线程是没有开启消息循环的。(主线程除外,主线程系统会自动为其创建Looper对象,开启消息循环。) Looper对象通过MessageQueue来存放消息和事件。一个线程只能有一个Looper,对应一个MessageQueue。 然后通过Looper.loop() 让Looper开始工作,从消息队列里取消息,处理消息。

    所以要使用Handler完成线程之间的通信,首先需要调用Looper.prepare() 为该线程开启消息循环,然后创建Handle,然后调用 Looper.loop() 开始工作。这都是很常规的流程。

    而HandlerThread 帮我们做好了这些事情,它内部建立了Looper。

    二、HandlerThread 用法

    public class OtherActivity extends AppCompatActivity {
    private static final String TAG = "OtherActivity";
    private Handler handler1;
    private Handler handler2;
    private HandlerThread handlerThread1;
    private HandlerThread handlerThread2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_other);

    // 创建HandlerThread
    handlerThread1 = new HandlerThread("handle-thread-1");
    handlerThread2 = new HandlerThread("handle-thread-2");
    // 开启HandleThread
    handlerThread1.start();
    handlerThread2.start();

    handler1 = new Handler(handlerThread1.getLooper()) {
    @Override
    public void handleMessage(@NonNull Message msg) {
    super.handleMessage(msg);
    Log.i(TAG, "handleMessage: ThreadName = " + Thread.currentThread().getName()
    + " msg.what = " + msg.what);
    }
    };

    handler2 = new Handler(handlerThread2.getLooper()) {
    @Override
    public void handleMessage(@NonNull Message msg) {
    super.handleMessage(msg);
    Log.i(TAG, "handleMessage: ThreadName = " + Thread.currentThread().getName()
    + " msg.what = " + msg.what);
    }
    };

    handler2.sendEmptyMessage(2);
    handler1.sendEmptyMessage(5);
    }

    // 释放资源
    @Override
    protected void onDestroy() {
    super.onDestroy();
    handlerThread1.quit();
    handlerThread2.quitSafely();
    }
    }

    HandlerThread创建 Looper 并执行 loop() 的线程在任务结束的时候,需要手动调用 quit。否则,线程将由于 loop() 的轮询一直处于可运行状态,CPU 资源无法释放。更有可能因为 Thread 作为 GC Root 持有超出生命周期的实例引发内存泄漏。

    官方使用 quitSafely() 去终止 Looper,原因是其只会剔除执行时刻晚于 当前调用时刻 的 Message。这样可以保证 quitSafely 调用的那刻,满足执行时间条件的 Message 继续保留在队列中,在都执行完毕才退出轮询。

    那么主线程需要 quit 吗?其实不需要,在内存不足的时候 App 由 AMS 直接回收进程。因为主线程极为重要,承载着 ContentProvider、Activity、Service 等组件生命周期的管理,即便某个组件结束了,它仍有继续存在去调度其他组件的必要! 换言之,ActivityThread 的作用域超过了这些组件,不该由这些组件去处理它的结束。比如,Activity destroy 了,ActivityThread 仍然要处理其他 Activity 或 Service 等组件的事务,不能结束。

    HandlerThread 的在Android中的用处

    Android本身就是一个巨大的消息处理机,ActivityThread类是Android APP进程的初始类,它的main函数是这个APP进程的入口。APP进程中UI事件的执行代码段都是由ActivityThread提供的。也就是说,主线程实例是存在的,只是创建它的代码我们不可见。ActivityThread的main函数就是在这个主线程里被执行的。

    public final class ActivityThread {

    //...
    private static ActivityThread sCurrentActivityThread;
    public static ActivityThread currentActivityThread() {
    return sCurrentActivityThread;
    }
    private void attach(boolean system) {
    sCurrentActivityThread = this;
    //...
    }
    public static void main(String[] args) {
    //....

    // 创建Looper和MessageQueue对象,用于处理主线程的消息
    Looper.prepareMainLooper();

    // 创建ActivityThread对象
    ActivityThread thread = new ActivityThread();

    // 建立Binder通道 (创建新线程)
    thread.attach(false);

    Looper.loop(); //消息循环运行
    throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    }

    Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施即可。

    总结

    管它呢,用就好了,封装好的东西干嘛不用了,可以减少我们开发过程中bug,提示开发效率,多好的一件事啊,用起来就对了,哈哈

    原文链接:https://juejin.cn/post/7032649435133771807?utm_source=gold_browser_extension

    收起阅读 »

    android 展示PDF文件

    PDF
     注:此方式展示pdf文件会增加apk大小3-4m左右 建议使用x5的webview进行加载pdf文件(可扩展) 1. 加入此依赖 implementation 'com.github.barteksc:android-pdf-viewer:3....
    继续阅读 »
    •  注:此方式展示pdf文件会增加apk大小3-4m左右 建议使用x5的webview进行加载pdf文件(可扩展)


    1. 加入此依赖



    implementation 'com.github.barteksc:android-pdf-viewer:3.2.0-beta.1'



    2. 简单介绍


    此篇文章主要还是将pdf文件进行下载到本sd目录下,之后转为file文件,交给pdfview进行展示,具体的展示pdf文件可进入pdfview源码中进行查看


    https://github.com/barteksc/AndroidPdfViewer

    3. 开始操作



    public class PDF2Activity extends AppCompatActivity {

    private PDFView pdfView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_p_d_f2);
    pdfView = findViewById(R.id.pdfView);

    download("xxx.pdf");

    }

    private void download(String url) {
    DownloadUtil.download(url, getCacheDir() + "/temp.pdf", new DownloadUtil.OnDownloadListener() {
    @Override
    public void onDownloadSuccess(final String path) {
    Log.d("MainActivity", "onDownloadSuccess: " + path);
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    preView(path);
    }
    });
    }

    @Override
    public void onDownloading(int progress) {
    Log.d("MainActivity", "onDownloading: " + progress);
    }

    @Override
    public void onDownloadFailed(String msg) {
    Log.d("MainActivity", "onDownloadFailed: " + msg);
    }
    });
    }

    private void preView(String path) {
    File file = new File(path);
    //这里只是作为一个file文件进行展示 还有其他的办法进行展示
    pdfView.fromFile(file)
    .enableSwipe(true) // allows to block changing pages using swipe
    .swipeHorizontal(false)
    .enableDoubletap(true)
    .defaultPage(0)
    // allows to draw something on the current page, usually visible in the middle of the screen
    .enableAnnotationRendering(false) // render annotations (such as comments, colors or forms)
    .password(null)
    .scrollHandle(null)
    .enableAntialiasing(true) // improve rendering a little bit on low-res screens
    // spacing between pages in dp. To define spacing color, set view background
    .spacing(0)
    .load();
    }


    3.1 okhttp



    implementation("com.squareup.okhttp3:okhttp:4.6.0")



    4.DownLoadUtils


    public class DownloadUtil {

    public static void download(final String url, final String saveFile, final OnDownloadListener listener) {
    Request request = new Request.Builder().url(url).build();
    new OkHttpClient().newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    listener.onDownloadFailed(e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
    InputStream is = null;
    byte[] buf = new byte[2048];
    int len;
    FileOutputStream fos = null;
    try {
    is = response.body().byteStream();
    long total = response.body().contentLength();
    File file = new File(saveFile);
    fos = new FileOutputStream(file);
    long sum = 0;
    while ((len = is.read(buf)) != -1) {
    fos.write(buf, 0, len);
    sum += len;
    int progress = (int) (sum * 1.0f / total * 100);
    listener.onDownloading(progress);
    }
    fos.flush();
    listener.onDownloadSuccess(file.getAbsolutePath());
    } catch (Exception e) {
    listener.onDownloadFailed(e.getMessage());
    } finally {
    try {
    if (is != null)
    is.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    try {
    if (fos != null)
    fos.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    });
    }

    public interface OnDownloadListener {
    void onDownloadSuccess(String path);

    void onDownloading(int progress);

    void onDownloadFailed(String msg);
    }
    }

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

    进来看看是不是你想要的效果,Android吸顶效果,并有着ViewPager左右切换

    老规矩,先上图,看看是不是你想要的 美团: 来一个图形分析 接下来我要写一个简单示例,先分析一下布局,见下图,最外层是NestedScrollView,之后嵌套一个LinearLayout头部,中间TabLayout选择器,底部一个ViewPager Vi...
    继续阅读 »

    老规矩,先上图,看看是不是你想要的


    美团:
    美团




    来一个图形分析


    接下来我要写一个简单示例,先分析一下布局,见下图,最外层是NestedScrollView,之后嵌套一个LinearLayout头部,中间TabLayout选择器,底部一个ViewPager
    ViewPager高度需要动态控制,看自己的需求了,如果是美团那种效果,就是
    ViewPager高度 = NestedScrollView高度 - TabLayout高度
    在这里插入图片描述




    话不多说,代码实现


    接下来我写一个例子,如果按照普通控件的嵌套方式来实现,那么肯定存在滑动冲突,会出现RecyclerView先进行滑动其次才是ScrollView滑动,那么就需要先重写NestedScrollView控件,用于控制最大的滑动距离,当达到最大滑动距离,再分发给RecyclerView滑动!




    NestedScrollView重写


    需要继承自NestedScrollView并重写onStartNestedScroll和onNestedPreScroll方法,如下


    package com.cyn.mt

    import android.content.Context
    import android.util.AttributeSet
    import android.view.View
    import androidx.core.view.NestedScrollingParent2
    import androidx.core.widget.NestedScrollView

    /**
    * @author cyn
    */

    class CoordinatorScrollview : NestedScrollView, NestedScrollingParent2 {
    private var maxScrollY = 0

    constructor(context: Context?) : super(context!!)
    constructor(context: Context?, attrs: AttributeSet?) : super(
    context!!,
    attrs
    )

    constructor(
    context: Context?,
    attrs: AttributeSet?,
    defStyleAttr: Int
    ) : super(context!!, attrs, defStyleAttr)

    override fun onStartNestedScroll(
    child: View,
    target: View,
    axes: Int,
    type: Int
    )
    : Boolean {
    return true
    }

    /**
    * 设置最大滑动距离
    *
    * @param maxScrollY 最大滑动距离
    */

    fun setMaxScrollY(maxScrollY: Int) {
    this.maxScrollY = maxScrollY
    }

    /**
    * @param target 触发嵌套滑动的View
    * @param dx 表示 View 本次 x 方向的滚动的总距离
    * @param dy 表示 View 本次 y 方向的滚动的总距离
    * @param consumed 表示父布局消费的水平和垂直距离
    * @param type 触发滑动事件的类型
    */

    override fun onNestedPreScroll(
    target: View,
    dx: Int,
    dy: Int,
    consumed: IntArray,
    type: Int
    )
    {
    if (dy > 0 && scrollY < maxScrollY) {
    scrollBy(0, dy)
    consumed[1] = dy
    }
    }
    }

    布局文件


    我按照美团的布局大体写出这样的布局

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <!--titleBar-->
    <LinearLayout
    android:id="@+id/titleBar"
    android:layout_width="match_parent"
    android:layout_height="45dp"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingLeft="18dp"
    android:paddingRight="18dp">

    <EditText
    android:layout_width="0dp"
    android:layout_height="35dp"
    android:layout_marginEnd="12dp"
    android:layout_marginRight="12dp"
    android:layout_weight="1"
    android:background="@drawable/edit_style"
    android:paddingLeft="12dp"
    android:paddingRight="12dp" />

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="35dp"
    android:background="@drawable/button_style"
    android:gravity="center"
    android:paddingLeft="15dp"
    android:paddingRight="15dp"
    android:text="搜索"
    android:textColor="#333333"
    android:textStyle="bold" />

    </LinearLayout>

    <!--coordinatorScrollView-->
    <com.cyn.mt.CoordinatorScrollview
    android:id="@+id/coordinatorScrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <!--相当于分析图中头部的LinearLayout,模拟动态添加的情况-->
    <LinearLayout
    android:id="@+id/titleLinerLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" />

    <!--相当于分析图中红色标记处TabLayout-->
    <com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

    <!--相当于分析图中绿色标记处ViewPager,代码中动态设置高度-->
    <androidx.viewpager.widget.ViewPager
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

    </LinearLayout>

    </com.cyn.mt.CoordinatorScrollview>

    </LinearLayout>



    Fragment


    加入,在Fragment中放入RecyclerView,提供给ViewPager使用,这里代码就不贴了,可以直接下源码!源码在文章末尾!




    主要代码(重点来了)


    coordinatorScrollView最大滑动距离即是titleLinerLayout的高度,所以实现titleLinerLayout的post方法,来监听titleLinerLayout的高度,由于这一块布局常常是通过网络请求后加载,所以,网络请求完毕后要再次实现post设置coordinatorScrollView最大滑动距离,如第80行代码和第90行代码,在这里,我并不推荐使用多次回调监听的方法!使用post只用调用一次,如果使用多次监听View变化的方法,应该在最后一次网络请求完毕后将此监听事件remove掉!


    package com.cyn.mt

    import android.content.res.Resources
    import android.os.Bundle
    import android.os.Handler
    import android.util.DisplayMetrics
    import android.view.LayoutInflater.from
    import android.view.View
    import androidx.appcompat.app.AppCompatActivity
    import androidx.fragment.app.Fragment
    import kotlinx.android.synthetic.main.activity_main.*
    import kotlinx.android.synthetic.main.title_layout.view.*


    class MainActivity : AppCompatActivity() {

    //屏幕宽
    var screenWidth = 0

    //屏幕高
    var screenHeight = 0

    //tabLayout的文本和图片
    private val tabTextData = arrayOf("常用药品", "夜间送药", "隐形眼镜", "成人用品", "医疗器械", "全部商家")
    private val tabIconData = arrayOf(
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon,
    R.mipmap.tab_icon
    )
    private var fragmentData = mutableListOf()


    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    initView()
    initData()
    }

    private fun initView() {

    //获取屏幕宽高
    val resources: Resources = this.resources
    val dm: DisplayMetrics = resources.displayMetrics
    screenWidth = dm.widthPixels
    screenHeight = dm.heightPixels

    //状态栏沉浸
    StatusBarUtil.immersive(this)

    //titleBar填充
    StatusBarUtil.setPaddingSmart(this, titleBar)

    //状态栏字体颜色设置为黑色
    StatusBarUtil.darkMode(this)

    //动态设置ViewPager高度
    coordinatorScrollView.post {
    val layoutParams = viewPager.layoutParams
    layoutParams.width = screenWidth
    layoutParams.height = coordinatorScrollView.height - tabLayout.height
    viewPager.layoutParams = layoutParams
    }

    }

    private fun initData() {

    //我模拟在头部动态添加三个布局,就用图片代替了,要设置的图片高度都是我提前算好的,根据屏幕的比例来计算的
    val titleView1 = getTitleView(screenWidth * 0.42F, R.mipmap.title1)
    val titleView2 = getTitleView(screenWidth * 0.262F, R.mipmap.title2)
    titleLinerLayout.addView(titleView1)
    titleLinerLayout.addView(titleView2)

    //设置最大滑动距离
    titleLinerLayout.post {
    coordinatorScrollView.setMaxScrollY(titleLinerLayout.height)
    }

    //用于请求网络后动态添加子布局
    Handler().postDelayed({
    val titleView3 = getTitleView(screenWidth * 0.589F, R.mipmap.title3)
    titleLinerLayout.addView(titleView3)

    //再次设置最大滑动距离
    titleLinerLayout.post {
    coordinatorScrollView.setMaxScrollY(titleLinerLayout.height)
    }

    }, 200)

    //添加TabLayout
    for (i in tabTextData.indices) {
    tabLayout.addTab(tabLayout.newTab())
    tabLayout.getTabAt(i)!!.setText(tabTextData[i]).setIcon(tabIconData[i])

    //添加Fragment
    fragmentData.add(TestFragment.newInstance(tabTextData[i]))
    }

    //Fragment ViewPager
    viewPager.adapter = ViewPagerAdapter(supportFragmentManager, fragmentData)

    //TabLayout关联ViewPager
    tabLayout.setupWithViewPager(viewPager)

    //设置TabLayout数据
    for (i in tabTextData.indices) {
    tabLayout.getTabAt(i)!!.setText(tabTextData[i]).setIcon(tabIconData[i])
    }
    }

    /**
    * 获取一个title布局
    * 我这里就用三张图片模拟的
    *
    * @height 要设置的图片高度
    */

    private fun getTitleView(height: Float, res: Int): View {
    val inflate = from(this).inflate(R.layout.title_layout, null, false)
    val layoutParams = inflate.titleImage.layoutParams
    layoutParams.width = screenWidth
    layoutParams.height = height.toInt()
    inflate.titleImage.setImageResource(res)
    return inflate
    }
    }



    最终效果


    在这里插入图片描述
    至此结束!




    源码资源


    下面3个链接均可下载源码



    源码Github(推荐):github.com/ThirdGoddes…




    源码CodeChina:codechina.csdn.net/qq_40881680…




    源码下载:download.csdn.net/download/qq…


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

    iOS面试基础知识 (五)

    混编技术移动开发已经进入大前端时代。对于混编技术,笔者一般在面试中也会问,通常会问h5混编、rn、weex、flutter等相关方面的问题,以考察面试者对于混编技术的了解程度。H5混编实现相对于rn、weex等混编技术,在App里面内嵌H5实现成本较低,所以目...
    继续阅读 »

    混编技术


    移动开发已经进入大前端时代。对于混编技术,笔者一般在面试中也会问,通常会问h5混编、rn、weex、flutter等相关方面的问题,以考察面试者对于混编技术的了解程度。


    H5混编实现


    相对于rn、weex等混编技术,在App里面内嵌H5实现成本较低,所以目前市面上H5混编仍是主流,笔者在面试中一般会问H5与App怎么通信。概括来说,主要有如下集中方式:


    伪协议实现


    伪协议指的是自己自定义的url协议,通过webview的代理拦截到url的加载,识别出伪协议,然后调用native的方法。伪协议可以这样定义:AKJS://functionName?param1=value1&param2=value2。 其中AKJS代表我们自己定义的协议,functionName代表要调用的App方法,?后面代表传入的参数。

    一、UIWebView通过UIWebViewDelegate的代理方法-webView: shouldStartLoadWithRequest:navigationType:进行伪协议拦截。

    二、WKWebView通过WKNavigationDelegate代理方法实现- webView:decidePolicyForNavigationAction:decisionHandler:进行伪协议拦截。

    此种实现方式优点是简单。

    缺点有:


    • 由于url长度大小有限制,导致传参大小有限制,比如h5如果要传一个图片的base64字符串过来,这种方式就无能为力了。
    • 需要在代理拦截方法里面写一系列if else处理,难以维护。
    • 如果App要兼容UIWebView和WKWebView,需要有两套实现,难以维护。


    JSContext


    为了解决伪协议实现的缺点,我们可以往webview里面注入OC对象,不过这种方案只能用于UIWebView中。此种方式的实现步骤如下:

    一、在webViewDidFinishLoad方法中通过JSContext注入JS对象


    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    self.jsContext[@"AK_JSBridge"] = self.bridgeAdapter; //往JS中注入OC对象


    二、OC对象实现JSExport协议,这样JS就可以调用OC对象的方法了


    @interface AKBridgeAdapter : NSOject< JSExport >
    - (void)getUID; // 获取用户ID


    此种方案的优点是JS可以直接调用对象的方法,通过提供对象这种方式,代码优雅;缺点是只能用于UIWebView、不能用于WKWebView。


    WKScriptMessageHandler


    WKWebView可以通过提供实现了WKScriptMessageHandler协议的类来实现JS调用OC,实现步骤如下:

    一、往webview注入OC对象。


    [self.configuration.userContentController addScriptMessageHandler:self.adapter name:@"AK_JSBridge"]


    二、实现- userContentController:didReceiveScriptMessage:获取方法调用名和参数


    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.body isKindOfClass:[NSDictionary class]]) {
    NSDictionary *dicMessage = message.body;

    NSString *funcName = [dicMessage stringForKey:@"funcName"];
    NSString *parameter = [dicMessage stringForKey:@"parameter"];
    //进行逻辑处理
    }
    }


    此种方案的优点是实现简单,缺点是不支持UIWebView。


    第三方库WKWebViewJavascriptBridge


    该库是iOS使用最广泛的JSBridge库,该库通过伪协议+JS消息队列实现了JS与OC交互,此种方案兼容UIWebView和WKWebView。


    RN、Weex、Flutter混编技术


    RN(React Native)是facebook开发的跨三端(iOS、Android、H5)开源框架,目前在业界使用最广泛;Weex是阿里开源的类似RN的大前端开发框架,国内有些公司在使用;Flutter是Google开发的,作为后旗之秀,目前越来越流行。

    笔者一般在面试中会问一下这类框架是怎么实现页面渲染,怎么实现调用OC的,以考察面试者是否了解框架实现原理。


    组件化


    任何一个对技术有追求的团队,都会做组件化,组件化的目标是模块解耦、代码复用。


    组件代码管理方式


    目前业内一般采用pod私有库的方式来管理自己的组件。


    组件通信方式


    MGJRouter


    MGJRouter通过注册url的方式来实现方法注册和调用


    [MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]);
    // @{@"user_id": @1900}
    }];

    [MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];


    该种方案的缺点有:


    • url定义由于是字符串,有可能造成重复。
    • 参数传入不能直接传model,而是需要传字典,如果方法实现方修改一个字段的类型但没有通知调用方,调用方无法直接知道,有可能导致崩溃。
    • 通过字典传参不直观,调用方需要知道字段的名字才能获取字段值,如果字段名不定义为宏,到处拷贝字段名造成难以维护。


    CTMediator


    CTMediator通过CTMediator的类别来实现方法调用。

    一、组件提供方实现Target、Action。


    @interface Target_A : NSObject

    - (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;

    @end

    - (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
    {
    // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
    return viewController;
    }


    二、组件提供方实现CTMediator类别暴露接口给使用方。


    @interface CTMediator (CTMediatorModuleAActions)

    - (UIViewController *)CTMediator_viewControllerForDetail;

    @end

    - (UIViewController *)CTMediator_viewControllerForDetail
    {
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
    action:kCTMediatorActionNativFetchDetailViewController
    params:@{@"key":@"value"}
    shouldCacheTarget:NO
    ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
    // view controller 交付出去之后,可以由外界选择是push还是present
    return viewController;
    } else {
    // 这里处理异常场景,具体如何处理取决于产品
    return [[UIViewController alloc] init];
    }
    }


    此种方案的优点是通过Targrt-Action实现了组件之间的解耦,通过暴露方法给组件使用方,避免了url直接传递字典带来的问题。

    缺点是:


    • CTMediator类别实现由于需要通过performTarget方式来实现,需要写一堆方法名、方法参数名字字符串,影响阅读;
    • 没有组件管理器概念,组件直接的互相调用都是通过直接引用CTMediator类别来实现,没有实现真正的解耦。


    BeeHive


    BeeHive通过url来实现页面路由,通过Protocol来实现方法调用。

    一、注册service


    [[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];


    二、调用service


    id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

    // use homeVc do invocation


    笔者推荐使用BeeHive这种方式来做组件化,基于Protocol(面向接口)的编程方式能让组件提供方清晰地提供接口声明给使用方;能充分利用编辑器特性,比如如果接口删除了一个参数,能通过编译器编不过来告诉调用方接口发生了变化。

    收起阅读 »

    iOS面试基础知识 (四)

    网络相关做移动开发,除了写UI,大部分的工作就是跟后台做接口联调了,所以网络相关的知识在面试当中是相当重要且必不可少的。Get与Post区别笔者在面试中会经常问这个问题,发现有挺多面试者回答得不好。很多人不知道Get与Post网络请求参数放在哪里。Get请求参...
    继续阅读 »

    网络相关


    做移动开发,除了写UI,大部分的工作就是跟后台做接口联调了,所以网络相关的知识在面试当中是相当重要且必不可少的。


    Get与Post区别


    笔者在面试中会经常问这个问题,发现有挺多面试者回答得不好。很多人不知道Get与Post网络请求参数放在哪里。

    Get请求参数是以kv方式拼在url后面的,虽然http协议对url的长度没有限制,但是浏览器和服务器一般都限制长度;Post请求参数是放在body里面的,对长度没什么限制。


    https原理


    https与http区别


    https是在http的基础上加上ssl形成的协议,http传输数据是明文的,https则是以对称加密的方式传输数据。


    https证书校验过程


    https采用对称加密传输数据,对称加密需要的密钥由客户端生成,通过非对称加密算法加密传输给后台。具体步骤如下:

    1、客户端向服务器发起HTTPS请求,连接到服务器的443端口。

    2、服务器有一个用来做非对称加密的密钥对,即公钥和私钥,服务器端保存着私钥,服务器将自己的公钥发送给客户。

    3、客户端收到服务器的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性,如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了。

    4、客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。

    5、服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。

    6、后续客户端和服务器基于client key进行对称加密传输数据。


    网络参数签名、加密实现方式


    除了用https协议传输数据,有些对数据安全要求比较高的App比如金融类App还会对参数进行签名和加密,这样可以防止网络请求参数被篡改以及敏感业务数据泄露


    网络参数签名


    为了防止网络请求被篡改,一般会对请求参数进行hash,一般会有一个sign字段表示签名。


    假定客户端请求参数dic如下:
    {
    "name":"akon",
    "city":"shenzhen",
    }


    那么如何生成sign字段呢?

    一般通用的做法是把字典按照key的字母升序排序然后拼接起来,然后再进行sha256,再md5。


    • 把字典按照key的字母排序拼接生成字符串str = "city=shenzhen&name=akon"。
    • 对str先进行sha256然后再进行md5生成sign。
      值得注意的是,为了增加破解的难度,我们可以在生成的str前面、后面加入一段我们App特有的字符串,然后对str hash可以采用base64、sha256,md5混合来做。


    网络参数加密方式


    为了效率,我们一般会采用对称加密加密数据,DES,3DES,AES这些方式都可以。既然要用对称加密,那就涉及到对称加密的密钥怎么生成,有如下方式:


    • 最简单的方式,代码写死密钥。密钥可以用base64或者抑或算法进行简单的加密,用的时候再解密,这种方式比裸写密钥更安全。
    • 后台下发密钥。后台可以在登录的时候下发这个密钥,客户端保存这个密钥后续用来做加密。由于客户端要保存这个密钥,所以还是存在泄露的风险。
    • 仿照https证书校验过程,客户端生成对称加密的密钥clientKey,对参数进行加密,然后用非对称加密对clientKey进行加密生成cryptKey传给后台;后台获取到cryptKey解析出clientKey,然后再用clientKey解密出请求参数。这种方式最安全,推荐使用。


    AFNetworking实现原理


    作为iOS使用最广泛的第三方网络库,AFNetworking基本上是面试必问的。笔者面试都会问,通过AF的一些问题,可以了解面试者是否熟练使用AF,以及是否阅读过AF的源代码。


    AF的设计架构图


    如果面试者能把AF的分层架构图清晰地画出来,那至少证明面试者有阅读过AF的源码。


    AF关于证书校验是哪个类实现的?有哪几种证书校验方式?


    AFSecurityPolicy用来做证书校验的。有三种校验方式:


    • AFSSLPinningModeNone 客户端不进行证书校验,完全信任服务端。
    • AFSSLPinningModePublicKey 客户端对证书进行公钥校验。
    • AFSSLPinningModeCertificate 客户端对整个证书进行校验。


    AF请求参数编码、响应参数解码分别是哪两个类实现的?支持什么方式编码,解码?


    • AFHTTPRequestSerializer、AFHTTPResponseSerializer分别用来做编码和解码。
    • 编码方式有url query类型、 json、plist方式。
    • 解码支持NSData、json、xml、image类型。


    关于AF如果再深入点可以问问具体实现细节,可以通过细节进一步考察面试者的内功。


    SDWebImage实现原理


    iOS下载图片基本都用SDWebImage,这个库笔者面试基本都会问。


    下载流程


    一、先去内存缓存找,找到了直接返回UIImage,否则走第二步;

    二、去磁盘缓存里面找,找到了直接返回UIImage,否则走第三步;

    三、网络下载,下载完成后存入本地磁盘和内存缓存,然后返回UIImage给调用方。


    url生成key的算法是什么?


    • 内存缓存key是url
    • 磁盘缓存key是对url进行md5生成的。


    清缓存时机


    • 对于内存缓存,在下载图片加载图片到内存时、内存收到警告时候进行清理。
    • 对于磁盘缓存,在App退出、进后台清理。


    网络防劫持策略


    H5防劫持


    黑客可以通过劫持URL,注入JS代码来劫持H5,可以通过黑名单机制来解决这类问题。


    DNS防劫持


    DNS的过程其实是域名替换成IP的过程,这个过程如果被黑客劫持,黑客可以返回自己的IP给客户端,从而劫持App。可以通过HTTP DNS方案来解决这个问题。


    网络优化


    网络优化的核心点是减少网络请求次数和数据传输量。策略有很多,列举一些常用的手段:


    合并接口


    有些接口可以合并就合并,把几个接口合并成一个接口,可以省去每个接口建立连接的时间以及每个请求传输的http请求头和响应头。


    采用pb等省流量传输协议


    我们可以采用xml、json、pb等格式传输数据。

    这三种方式数据量大小和性能pb>json>xml。


    webp


    采用webp图片可以节省客户端和服务端的带宽。


    采用tcp而不是http


    http是基于tcp的应用层协议,相比tcp,http多出来一个几百字节的请求头和响应头,并且每次通信都要建立连接,效率比不上tcp。


    同运营商、就近接入


    可以根据用户手机的运营商返回相应机房的服务器给客户端,比如联通返回联通的服务器;

    可以根据用户所处区域返回相应的服务器给客户端,比如深圳返回深圳机房的服务器。

    收起阅读 »

    iOS面试基础知识 (三)

    iOS
    多线程多线程创建方式iOS创建多线程方式主要有NSThread、NSOperation、GCD,这三种方式创建多线程的优缺点如下:NSThreadNSThread 封装了一个线程,通过它可以方便的创建一个线程。NSThread 线程之间的并发控制,是需要我们自...
    继续阅读 »

    多线程


    多线程创建方式


    iOS创建多线程方式主要有NSThread、NSOperation、GCD,这三种方式创建多线程的优缺点如下:


    NSThread


    • NSThread 封装了一个线程,通过它可以方便的创建一个线程。NSThread 线程之间的并发控制,是需要我们自己来控制的。它的缺点是需要我们自己维护线程的生命周期、线程之间同步等,优点是轻量,灵活。


    NSOperation


    • NSOperation 是一个抽象类,它封装了线程的实现细节,不需要自己管理线程的生命周期和线程的同步等,需要和 NSOperationQueue 一起使用。使用 NSOperation ,你可以方便地控制线程,比如取消线程、暂停线程、设置线程的优先级、设置线程的依赖。NSOperation常用于下载库的实现,比如SDWebImage的实现就用到了NSOperation。


    GCD


    • GCD(Grand Central Dispatch) 是 Apple 开发的一个多核编程的解决方法。GCD 是一个可以替代 NSThread 的很高效和强大的技术。在平常开发过程中,我们用的最多的就是GCD。哦,对了,NSOperation是基于GCD实现的。


    多线程同步


    多线程情况下访问共享资源需要进行线程同步,线程同步一般都用锁实现。从操作系统层面,锁的实现有临界区、事件、互斥量、信号量等。这里讲一下iOS中多线程同步的方式。


    atomic


    属性加上atomic关键字,编译器会自动给该属性生成代码用以多线程访问同步,它并不能保证使用属性的过程是线程安全的。一般我们在定义属性的时候用nonatomic,避免性能损失。


    @synchronized


    @synchronized指令是一个对象锁,用起来非常简单。使用obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程1和线程2中的@synchronized后面的obj不相同,则不会互斥。@synchronized其实是对pthread_mutex递归锁的封装。

    @synchronized优点是我们不需要在代码中显式的创建锁对象,使用简单; 缺点是@synchronized会隐式的添加一个异常处理程序,该异常处理程序会在异常抛出的时候自动的释放互斥锁,从而带来额外开销。


    NSLock


    最简单的锁,调用lock获取锁,unlock释放锁。如果其它线程已经调用lock获取了锁,当前线程调用lock方法会阻塞当前线程,直到其它线程调用unlock释放锁为止。NSLock使用简单,在项目中用的最多。


    NSRecursiveLock


    递归锁主要用来解决同一个线程频繁获取同一个锁而不造成死锁的问题。注意lock和unlock调用必须配对。


    NSConditionLock


    条件锁,可以设置自定义条件来获取锁。比如生产者消费者模型可以用条件锁来实现。


    NSCondition


    条件,操作系统中信号量的实现,方法- (void)wait和- (BOOL)waitUntilDate:(NSDate *)limit用来等待锁直至锁有信号;方法- (void)signal和- (void)broadcast使condition有信号,通知等待condition的线程,变成非阻塞状态。


    dispatch_semaphore_t


    信号量的实现,可以实现控制GCD队列任务的最大并发量,类似于NSOperationQueue的maxConcurrentOperationCount属性。


    pthread_mutex


    mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。使用pthread_mutex_init创建锁,使用pthread_mutex_lock和pthread_mutex_unlock加锁和解锁。注意:mutex可以通过PTHREAD_MUTEX_RECURSIVE创建递归锁,防止重复获取锁导致死锁


     //创建锁,注意:mutex可以通过PTHREAD_MUTEX_RECURSIVE创建递归锁,防止重复获取锁导致死锁
    pthread_mutexattr_t recursiveAttr;
    pthread_mutexattr_init(&recursiveAttr);
    pthread_mutexattr_settype(&recursiveAttr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(self.mutex, &recursiveAttr);
    pthread_mutexattr_destroy(&recursiveAttr);

    pthread_mutex_lock(&self.mutex)
    //访问共享数据代码
    pthread_mutex_unlock(&self.mutex)


    OSSpinLock


    OSSpinLock 是自旋锁,等待锁的线程会处于忙等状态。一直占用着 CPU。自旋锁就好比写了个 while,whil(被加锁了) ; 不断的忙等,重复这样。OSSpinLock是不安全的锁(会造成优先级反转),什么是优先级反转,举个例子:

    有线程1和线程2,线程1的优先级比较高,那么cpu分配给线程1的时间就比较多,自旋锁可能发生优先级反转问题。如果优先级比较低的线程2先加锁了,紧接着线程1进来了,发现已经被加锁了,那么线程1忙等,while(未解锁); 不断的等待,由于线程1的优先级比较高,CPU就一直分配之间给线程1,就没有时间分配给线程2,就有可能导致线程2的代码就没有办法往下走,就会造成线程2没有办法解锁,所以这个锁就不安全了。

    建议不要使用OSSpinLock,用os_unfair_lock来代替。


    //初始化
    OSSpinLock lock = OS_SPINLOCK_INIT;
    //加锁
    OSSpinLockLock(&lock);
    //解锁
    OSSpinLockUnlock(&lock);


    os_unfair_lock


    os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等


    //初始化
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    //加锁
    os_unfair_lock_lock(&lock);
    //解锁
    os_unfair_lock_unlock(&lock);


    性能


    性能从高到低排序

    1、os_unfair_lock

    2、OSSpinLock

    3、dispatch_semaphore

    4、pthread_mutex

    5、NSLock

    6、NSCondition

    7、pthread_mutex(recursive)

    8、NSRecursiveLock

    9、NSConditionLock

    10、@synchronized


    JSON Model互转


    项目中JSON Model转换方式


    平常开发过程中,经常需要进行JSON与Model互转,尤其是接口数据转换。我们可以手动解析,也可以用MJExtension、YYModel这些第三方库,用第三方库最大的好处他可以自动给你转换并且处理类型不匹配等异常情况,从而避免崩溃。


    MJExtension实现原理


    假定后台返回的字典dic为:
    {
    "name":"akon",
    "address":"shenzhen",
    }

    我们自定义了一个类UserModel
    @interface UserModel : NSObject

    @property (nonatomic, strong)NSString* name;
    @property (nonatomic, strong)NSString* address;

    @end


    • MJExtension是如何做属性映射的?
      MJExtension在遍历dic属性时,比如遍历到name属性时,先去缓存里查找这个类是否有这个属性,有就赋值akon。没有就遍历UserModel的属性列表,把这个类的属性列表加入到缓存中,查看这个类有没有定义name属性,如果有,就把akon赋给这个属性,否则不赋值。
    • MJExtension是如何给属性赋值的?
      利用KVC机制,在查找到UserModel有name的属性,使用[self setValue:@"akon" forKey:@"name"]进行赋值。
    • 如何获取类的属性列表?
      通过class_copyPropertyList方法
    • 如何遍历成员变量列表?
      通过class_copyIvarList方法


    数据存储方式


    iOS常见数据存储方式及使用场景


    iOS中可以采用NSUserDefaults、Archive、plist、数据库等方式等来存储数据,以上存储方式使用的业务场景如下:


    • NSUserDefaults一般用来存储一些简单的App配置。比如存储用户姓名、uid这类轻量的数据。
    • Archive可以用来存储model,如果一个model要用Archive存储,需要实现NSCoding协议。
    • plist存储方式。像NSString、NSDictionary等类都可以直接存调用writeToFile:atomically:方法存储到plist文件中。
      -数据库存储方式。大量的数据存储,比如消息列表、网络数据缓存,需要采用数据库存储。可以用FMDB、CoreData、WCDB、YYCache来进行数据库存储。建议使用WCDB来进行数据库存储,因为WCDB是一个支持orm,支持加密,多线程安全的高性能数据库。


    数据库操作


    笔者在面试中,一般会问下面试者数据库的操作,以此开考察一下面试者对于数据库操作的熟练程度。


    • 考察常用crud语句书写。
      创建表、给表增加字段、插入、删除、更新、查询SQL怎么写。尤其是查询操作,可以考察order by, group by ,distinct, where匹配以及联表查询等技巧。
    • SQL语句优化技巧。如索引、事务等常用优化技巧。
    • 怎么分库、分表?
    • FMDB或者WCDB(orm型)实现原理。
    • 怎么实现数据库版本迁移?
    收起阅读 »

    iOS面试基础知识 (二)

    iOS
    一、类别OC不像C++等高级语言能直接继承多个类,不过OC可以使用类别和协议来实现多继承。1、类别加载时机在App加载时,Runtime会把Category的实例方法、协议以及属性添加到类上;把Category的类方法添加到类的metaclass上。2、类别添...
    继续阅读 »

    一、类别


    OC不像C++等高级语言能直接继承多个类,不过OC可以使用类别和协议来实现多继承。


    1、类别加载时机


    在App加载时,Runtime会把Category的实例方法、协议以及属性添加到类上;把Category的类方法添加到类的metaclass上。


    2、类别添加属性、方法


    1)在类别中不能直接以@property的方式定义属性,OC不会主动给类别属性生成setter和getter方法;需要通过objc_setAssociatedObject来实现。


    @interface TestClass(ak)

    @property(nonatomic,copy) NSString *name;

    @end

    @implementation TestClass (ak)

    - (void)setName:(NSString *)name{

    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY);
    }

    - (NSString*)name{
    NSString *nameObject = objc_getAssociatedObject(self, "name");
    return nameObject;
    }


    2)类别同名方法覆盖问题


    • 如果类别和主类都有名叫funA的方法,那么在类别加载完成之后,类的方法列表里会有两个funA;
    • 类别的方法被放到了新方法列表的前面,而主类的方法被放到了新方法列表的后面,这就造成了类别方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找,殊不知后面可能还有一样名字的方法;
    • 如果多个类别定义了同名方法funA,具体调用哪个类别的实现由编译顺序决定,后编译的类别的实现将被调用。
    • 在日常开发过程中,类别方法重名轻则造成调用不正确,重则造成crash,我们可以通过给类别方法名加前缀避免方法重名。


    关于类别更深入的解析可以参见美团的技术文章深入理解Objective-C:Category


    二、协议


    定义


    iOS中的协议类似于Java、C++中的接口类,协议在OC中可以用来实现多继承和代理。


    方法声明


    协议中的方法可以声明为@required(要求实现,如果没有实现,会发出警告,但编译不报错)或者@optional(不要求实现,不实现也不会有警告)。

    笔者经常会问面试者如下两个问题:

    -怎么判断一个类是否实现了某个协议?很多人不知道可以通过conformsToProtocol来判断。

    -假如你要求业务方实现一个delegate,你怎么判断业务方有没有实现dalegate的某个方法?很多人不知道可以通过respondsToSelector来判断。


    三、通知中心


    iOS中的通知中心实际上是观察者模式的一种实现。


    postNotification是同步调用还是异步调用?


    同步调用。当调用addObserver方法监听通知,然后调用postNotification抛通知,postNotification会在当前线程遍历所有的观察者,然后依次调用观察者的监听方法,调用完成后才会去执行postNotification后面的代码。


    如何实现异步监听通知?


    通过addObserverForName:object:queue:usingBlock来实现异步通知。


    四、KVC


    KVC查找顺序


    1)调用setValue:forKey时候,比如[obj setValue:@"akon" forKey:@"key"]时候,会按照key,iskey,key,iskey的顺序搜索成员并进行赋值操作。如果都没找到,系统会调用该对象的setValue:forUndefinedKey方法,该方法默认是抛出异常。

    2)当调用valueForKey:@"key"的代码时,KVC对key的搜索方式不同于setValue"akon" forKey:@"key",其搜索方式如下:


    • 首先按get, is的顺序查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型,会将其包装成一个NSNumber对象。
    • 如果没有找到,KVC则会查找countOf、objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调
      用这个代理集合的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
      -如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。
    • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按,is,,is的顺序搜索成员变量名。
    • 如果还没找到,直接调用该对象的valueForUndefinedKey:方法,该方法默认是抛出异常。


    KVC防崩溃


    我们经常会使用KVC来设置属性和获取属性,但是如果对象没有按照KVC的规则声明该属性,则会造成crash,怎么全局通用地防止这类崩溃呢?

    可以通过写一个NSObject分类来防崩溃。


    @interface NSObject(AKPreventKVCCrash)

    @end

    @ implementation NSObject(AKPreventKVCCrash)

    - (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    }

    - (id)valueForUndefinedKey:(NSString *)key{

    return nil;
    }

    @end


    五、KVO


    定义


    KVO(Key-Value Observing),键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。


    注册、移除KVO


    通过如下两个方案来注册、移除KVO


    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


    通过observeValueForKeyPath来获取值的变化。


    - (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object
    change:(NSDictionary *)change
    context:(void *)context


    我们可以通过facebook开源库KVOController方便地进行KVO。


    KVO实现


    苹果官方文档对KVO实现介绍如下:


    Key-Value Observing Implementation Details

    Automatic key-value observing is implemented using a technique called isa-swizzling.

    The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

    When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

    You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.


    即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个派生类 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。因此在向ObjectA对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了 override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。


    关于kvc和kvo更深入的详解参考iOS KVC和KVO详解


    六、autorelasepool


    用处


    在 ARC 下,我们不需要手动管理内存,可以完全不知道 autorelease 的存在,就可以正确管理好内存,因为 Runloop 在每个 Runloop Circle 中会自动创建和释放Autorelease Pool。

    当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool 可以有效的避免内存峰值的出现。因为如果不手动创建的话,外层系统创建的 pool 会在整个 Runloop Circle 结束之后才进行 drain,手动创建的话,会在 block 结束之后就进行 drain 操作,比如下面例子:


    for (int i = 0; i < 100000; i++)
    {
    @autoreleasepool
    {
    NSString* string = @"akon";
    NSArray* array = [string componentsSeparatedByString:string];
    }
    }


    比如SDWebImage中这段代码,由于encodedDataWithImage会把image解码成data,可能造成内存暴涨,所以加autoreleasepool避免内存暴涨


     @autoreleasepool {
    NSData *data = imageData;
    if (!data && image) {
    // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
    SDImageFormat format;
    if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
    format = SDImageFormatPNG;
    } else {
    format = SDImageFormatJPEG;
    }
    data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
    }
    [self _storeImageDataToDisk:data forKey:key];
    }


    Runloop中自动释放池创建和释放时机


    • 系统在 Runloop 中创建的 autoreleaspool 会在 Runloop 一个 event 结束时进行释放操作。
    • 我们手动创建的 autoreleasepool 会在 block 执行完成之后进行 drain 操作。需要注意的是:
      当 block 以异常结束时,pool 不会被 drain
      Pool 的 drain 操作会把所有标记为 autorelease 的对象的引用计数减一,但是并不意味着这个对象一定会被释放掉,我们可以在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。


    收起阅读 »

    iOS面试基础知识 (一)

    iOS
    iOS面试基础知识 (一)一、Runtime原理Runtime是iOS核心运行机制之一,iOS App加载库、加载类、执行方法调用,全靠Runtime,这一块的知识个人认为是最基础的,基本面试必问。1、Runtime消息发送机制1)iOS调用一个方法时,实际上...
    继续阅读 »

    iOS面试基础知识 (一)


    一、Runtime原理


    Runtime是iOS核心运行机制之一,iOS App加载库、加载类、执行方法调用,全靠Runtime,这一块的知识个人认为是最基础的,基本面试必问。


    1、Runtime消息发送机制


    1)iOS调用一个方法时,实际上会调用objc_msgSend(receiver, selector, arg1, arg2, ...),该方法第一个参数是消息接收者,第二个参数是方法名,剩下的参数是方法参数;

    2)iOS调用一个方法时,会先去该类的方法缓存列表里面查找是否有该方法,如果有直接调用,否则走第3)步;

    3)去该类的方法列表里面找,找到直接调用,把方法加入缓存列表;否则走第4)步;

    4)沿着该类的继承链继续查找,找到直接调用,把方法加入缓存列表;否则消息转发流程;

    很多面试者大体知道这个流程,但是有关细节不是特别清楚。


    • 问他/她objc_msgSend第一个参数、第二个参数、剩下的参数分别代表什么,不知道;
    • 很多人只知道去方法列表里面查找,不知道还有个方法缓存列表。
      通过这些细节,可以了解一个人是否真正掌握了原理,而不是死记硬背。


    2、Runtime消息转发机制


    如果在消息发送阶段没有找到方法,iOS会走消息转发流程,流程图如下所示:


    1)动态消息解析。检查是否重写了resolveInstanceMethod 方法,如果返回YES则可以通过class_addMethod 动态添加方法来处理消息,否则走第2)步;

    2)消息target转发。forwardingTargetForSelector 用于指定哪个对象来响应消息。如果返回nil 则走第3)步;

    3)消息转发。这步调用 methodSignatureForSelector 进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil 执行第四步;否则返回 methodSignature,则进入 forwardInvocation ,在这里可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。否则执行第4)步;

    4)报错 unrecognized selector sent to instance。

    很多人知道这四步,但是笔者一般会问:


    • 怎么在项目里全局解决"unrecognized selector sent to instance"这类crash?本人发现很多人回答不出来,说明面试者肯定是在死记硬背,你都知道因为消息转发那三步都没处理才会报错,为什么不知道在消息转发里面处理呢?
    • 如果面试者知道可以在消息转发里面处理,防止崩溃,再问下面试者,你项目中是在哪一步处理的,看看其是否有真正实践过?


    二、load与initialize


    1、load与initialize调用时机


    +load在main函数之前被Runtime调用,+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。


    2、load与initialize在分类、继承链的调用顺序


    • load方法的调用顺序为:
      子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。
      如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。
    • initialize的调用顺序为:
      +initialize 方法的调用与普通方法的调用是一样的,走的都是消息发送的流程。如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。
    • 怎么确保在load和initialize的调用只执行一次
      由于load和initialize可能会调用多次,所以在这两个方法里面做的初始化操作需要保证只初始化一次,用dispatch_once来控制


    笔者在面试过程中发现很多人对于load与initialize在分类、继承链的调用顺序不清楚。对怎么保证初始化安全也不清楚


    三、RunLoop原理


    RunLoop苹果原理图



    图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。


    1、RunLoop与线程关系


    • 一个线程是有一个RunLoop还是多个RunLoop? 一个;
    • 怎么启动RunLoop?主线程的RunLoop自动就开启了,子线程的RunLoop通过Run方法启动。


    2、Input Source 和 Timer Source


    两个都是 Runloop 事件的来源,其中 Input Source 又可以分为三类


    • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到;
    • Custom Input Sources,用户手动创建的 Source;
    • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源;
      Timer Source指定时器事件,该事件的优先级是最低的。
      本人一般会问定时器事件的优先级是怎么样的,大部分人回答不出来。


    3、解决NSTimer事件在列表滚动时不执行问题


    因为定时器默认是运行在NSDefaultRunLoopMode,在列表滚动时候,主线程会切换到UITrackingRunLoopMode,导致定时器回调得不到执行。

    有两种解决方案:


    • 指定NSTimer运行于 NSRunLoopCommonModes下。
    • 在子线程创建和处理Timer事件,然后在主线程更新 UI。


    四、事件分发机制及响应者链


    1、事件分发机制


    iOS 检测到手指触摸 (Touch) 操作时会将其打包成一个 UIEvent 对象,并放入当前活动Application的事件队列,UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view。

    hitTest:withEvent:方法的处理流程如下:


    • 首先调用当前视图的 pointInside:withEvent: 方法判断触摸点是否在当前视图内;
    • 若返回 NO, 则 hitTest:withEvent: 返回 nil,若返回 YES, 则向当前视图的所有子视图 (subviews) 发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图(后加入的先遍历),直到有子视图返回非空对象或者全部子视图遍历完毕;
    • 若第一次有子视图返回非空对象,则 hitTest:withEvent: 方法返回此对象,处理结束;
    • 如所有子视图都返回空,则 hitTest:withEvent: 方法返回自身 (self)。
      流程图如下:


    2、响应者链原理


    iOS的事件分发机制是为了找到第一响应者,事件的处理机制叫做响应者链原理。

    所有事件响应的类都是 UIResponder 的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会。当发生事件时,事件首先被发送给第一响应者,第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方。事件将沿着响应者链一直向下传递,直到被接受并做出处理。一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,就传递给它的父视图(superview)对象(如果存在)处理,如果没有父视图,事件就会被传递给它的视图控制器对象 ViewController(如果存在),接下来会沿着顶层视图(top view)到窗口(UIWindow 对象)再到程序(UIApplication 对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。一般情况下,在响应者链中只要由对象处理事件,事件就停止传递。

    一个典型的事件响应路线如下:

    First Responser --> 父视图-->The Window --> The Application --> nil(丢弃)

    我们可以通过 [responder nextResponder] 找到当前 responder 的下一个 responder,持续这个过程到最后会找到 UIApplication 对象。


    五、内存泄露检测与循环引用


    1、造成内存泄露原因


    • 在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;
    • 调用CoreFoundation里面的C方法后创建对对象后不释放。比如调用CGImageCreate不调用CGImageRelease;
    • 循环引用。当对象A和对象B互相持有的时候,就会产生循环引用。常见产生循环引用的场景有在VC的cellForRowAtIndexPath方法中cell block引用self。


    2、常见循环引用及解决方案


    1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。


     cell.clickBlock = ^{
    self.name = @"akon";
    };

    cell.clickBlock = ^{
    _name = @"akon";
    };


    解决方案:把self改成weakSelf;


    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.name = @"akon";
    };


    2)在cell的block中直接引用VC的成员变量造成循环引用。


    //假设 _age为VC的成员变量
    @interface TestVC(){

    int _age;

    }
    cell.clickBlock = ^{
    _age = 18;
    };


    解决方案有两种:


    • 用weak-strong dance


    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    strongSelf->age = 18;
    };


    • 把成员变量改成属性


    //假设 _age为VC的成员变量
    @interface TestVC()

    @property(nonatomic, assign)int age;

    @end

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.age = 18;
    };


    3)delegate属性声明为strong,造成循环引用。


    @interface TestView : UIView

    @property(nonatomic, strong)id<TestViewDelegate> delegate;

    @end

    @interface TestVC()<TestViewDelegate>

    @property (nonatomic, strong)TestView* testView;

    @end

    testView.delegate = self; //造成循环引用


    解决方案:delegate声明为weak


    @interface TestView : UIView

    @property(nonatomic, weak)id<TestViewDelegate> delegate;

    @end


    4)在block里面调用super,造成循环引用。


    cell.clickBlock = ^{
    [super goback]; //造成循环应用
    };


    解决方案,封装goback调用


    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    [weakSelf _callSuperBack];
    };

    - (void) _callSuperBack{
    [self goback];
    }


    5)block声明为strong

    解决方案:声明为copy

    6)NSTimer使用后不invalidate造成循环引用。

    解决方案:


    • NSTimer用完后invalidate;
    • NSTimer分类封装


    + (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
    block:(void(^)(void))block
    repeats:(BOOL)repeats{

    return [self scheduledTimerWithTimeInterval:interval
    target:self
    selector:@selector(ak_blockInvoke:)
    userInfo:[block copy]
    repeats:repeats];
    }

    + (void)ak_blockInvoke:(NSTimer*)timer{

    void (^block)(void) = timer.userInfo;
    if (block) {
    block();
    }
    }

    --



    3、怎么检测循环引用


    • 静态代码分析。 通过Xcode->Product->Anaylze分析结果来处理;
    • 动态分析。用MLeaksFinder(只能检测OC泄露)或者Instrument或者OOMDetector(能检测OC与C++泄露)。


    六、VC生命周期


    考察viewDidLoad、viewWillAppear、ViewDidAppear等方法的执行顺序。

    假设现在有一个 AViewController(简称 Avc) 和 BViewController (简称 Bvc),通过 navigationController 的push 实现 Avc 到 Bvc 的跳转,调用顺序如下:

    1、A viewDidLoad 

    2、A viewWillAppear 

    3、A viewDidAppear 

    4、B viewDidLoad 

    5、A viewWillDisappear 

    6、B viewWillAppear 

    7、A viewDidDisappear 

    8、B viewDidAppear

    如果再从 Bvc 跳回 Avc,调用顺序如下:

    1、B viewWillDisappear 

    2、A viewWillAppear 

    3、B viewDidDisappear 

    4、A viewDidAppear

    收起阅读 »

    微信小程序如何确保每个页面都已经登陆

    现状 一个微信小程序中,有首页,有个人页面,还有一些列表页面,详情页面等等,这些页面大部分是可以分享的。当分享出去的页面被一个另一个用户打开的时候,这个页面怎么确保这个用户已经登陆了呢? 网上有很多方案是在请求封装里面加一道拦截,如果没有token,就先调用登...
    继续阅读 »

    现状


    一个微信小程序中,有首页,有个人页面,还有一些列表页面,详情页面等等,这些页面大部分是可以分享的。当分享出去的页面被一个另一个用户打开的时候,这个页面怎么确保这个用户已经登陆了呢?


    网上有很多方案是在请求封装里面加一道拦截,如果没有token,就先调用登陆请求获取token后,再继续。
    这种方案没毛病,只要注意一点,当一个页面有多个请求同时触发时,当所有请求拦截后,放到一个数组里面,在获取token成功后,遍历数组一个个请求就行。


    但这个需求再复杂一点,比如连锁便利店小程序,大部分页面都需要有一个门店(因为需要根据门店获取当前门店商品的库存、价格等),这个门店是根据当前的定位来调用后台接口获得的,这个时候如果在请求里进行封装就太麻烦了。


    解决方案


    首先,我们注意到,登陆,获取定位与我们的页面请求是异步的,我们需要保证页面请求是在登陆和获取定位之后,但要是我们每个页面都写一个遍,可维护性就太差了。所以我们可以抽离出一个方法来做这件事。
    所以代码就这样了:


    const app = getApp()
    Page({
    data: {
    logs: []
    },
    onLoad() {
    app.commonLogin(()=>{
    // 处理页页面请求
    })
    }
    })

    做到这里好像是解决我们的问题,但再想一想,如果还想做更多的事,比如说每个页面的onShareAppMessage统一处理,但我又不想在每个页面再写一遍,另外,我又想自己对每个页面实现一个watch,怎么做?


    进一步解决方案


    我们可以看到微信小程序,每个页面是一个Page(),那么我们可以给这个Page外面加一层壳子,我们可以有一个MyPage来替换这个Page,废话不多说,上代码:


    tool.js 相关代码


    /**
    * 处理合并参数
    */
    handlePageParamMerge(arg) {
    let numargs = arg.length; // 获取被传递参数的数值。
    let data = {}
    let page = {}
    for (let ix in arg) {
    let item = arg[ix]
    if (item.data && typeof (item.data) === 'object') {
    data = Object.assign(data, item.data)
    }
    if (item.methods && typeof (item.methods) === 'object') {
    page = Object.assign(page, item.methods)
    } else {
    page = Object.assign(page, item)
    }
    }
    page.data = data
    return page
    }

    /***
    * 合并页面方法以及数据, 兼容 {data:{}, methods: {}} 或 {data:{}, a:{}, b:{}}
    */
    mergePage() {
    return this.handlePageParamMerge(arguments)
    }

    /**
    * 处理组件参数合并
    */
    handleCompParamMerge(arg) {
    let numargs = arg.length; // 获取被传递参数的数值。
    let data = {}
    let options = {}
    let properties = {}
    let methods = {}
    let comp = {}
    for (let ix in arg) {
    let item = arg[ix]
    // 合并组件的初始数据
    if (item.data && typeof (item.data) === 'object') {
    data = Object.assign(data, item.data)
    }
    // 合并组件的属性列表
    if (item.properties && typeof (item.properties) === 'object') {
    properties = Object.assign(properties, item.properties)
    }
    // 合组件的方法列表
    if (item.methods && typeof (item.methods) === 'object') {
    methods = Object.assign(methods, item.methods)
    }
    if (item.options && typeof (item.options) === 'object') {
    options = Object.assign(options, item.options)
    }
    comp = Object.assign(comp, item)
    }
    comp.data = data
    comp.options = options
    comp.properties = properties
    comp.methods = methods
    return comp
    }

    /**
    * 组件混合 {properties: {}, options: {}, data:{}, methods: {}}
    */
    mergeComponent() {
    return this.handleCompParamMerge(arguments)
    }

    /***
    * 合成带watch的页面
    */
    newPage() {
    let options = this.handlePageParamMerge(arguments)
    let that = this
    let app = getApp()

    //增加全局点击登录判断
    if (!options.publicCheckLogin){
    options.publicCheckLogin = function (e) {
    let pages = getCurrentPages()
    let page = pages[pages.length - 1]
    let dataset = e.currentTarget.dataset
    let callback = null

    //获取回调方法
    if (dataset.callback && typeof (page[dataset.callback]) === "function"){
    callback = page[dataset.callback]
    }
    // console.log('callback>>', callback, app.isRegister())
    //判断是否登录
    if (callback && app.isRegister()){
    callback(e)
    }
    else{
    wx.navigateTo({
    url: '/pages/login/login'
    })
    }
    }
    }

    const { onLoad } = options
    options.onLoad = function (arg) {
    options.watch && that.setWatcher(this)
    onLoad && onLoad.call(this, arg)
    }

    const { onShow } = options
    options.onShow = function (arg) {
    if (options.data.noAutoLogin || app.isRegister()) {
    onShow && onShow.call(this, arg)
    //页面埋点
    app.ga({})
    }
    else {
    wx.navigateTo({
    url: '/pages/login/login'
    })
    }
    }

    return Page(options)
    }

    /**
    * 合成带watch等的组件
    */
    newComponent() {
    let options = this.handleCompParamMerge(arguments)
    let that = this
    const { ready } = options
    options.ready = function (arg) {
    options.watch && that.setWatcher(this)
    ready && ready.call(this, arg)
    }
    return Component(options)
    }

    /**
    * 设置监听器
    */
    setWatcher(page) {
    let data = page.data;
    let watch = page.watch;
    Object.keys(watch).forEach(v => {
    let key = v.split('.'); // 将watch中的属性以'.'切分成数组
    let nowData = data; // 将data赋值给nowData
    for (let i = 0; i < key.length - 1; i++) { // 遍历key数组的元素,除了最后一个!
    nowData = nowData[key[i]]; // 将nowData指向它的key属性对象
    }

    let lastKey = key[key.length - 1];
    // 假设key==='my.name',此时nowData===data['my']===data.my,lastKey==='name'
    let watchFun = watch[v].handler || watch[v]; // 兼容带handler和不带handler的两种写法
    let deep = watch[v].deep; // 若未设置deep,则为undefine
    this.observe(nowData, lastKey, watchFun, deep, page); // 监听nowData对象的lastKey
    })
    }

    /**
    * 监听属性 并执行监听函数
    */
    observe(obj, key, watchFun, deep, page) {
    var val = obj[key];
    // 判断deep是true 且 val不能为空 且 typeof val==='object'(数组内数值变化也需要深度监听)
    if (deep && val != null && typeof val === 'object') {
    Object.keys(val).forEach(childKey => { // 遍历val对象下的每一个key
    this.observe(val, childKey, watchFun, deep, page); // 递归调用监听函数
    })
    }
    var that = this;
    Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    set: function (value) {
    if (val === value) {
    return
    }
    // 用page对象调用,改变函数内this指向,以便this.data访问data内的属性值
    watchFun.call(page, value, val); // value是新值,val是旧值
    val = value;
    if (deep) { // 若是深度监听,重新监听该对象,以便监听其属性。
    that.observe(obj, key, watchFun, deep, page);
    }
    },
    get: function () {
    return val;
    }
    })
    }

    页面代码:


    app.tool.newPage({
    data: {
    // noAutoLogin: false
    },
    onShow: function () {
    // 在这里写页面请求逻辑
    }
    }

    最后


    代码是在线上跑了很久的,tool里的newPage封装,你可以根据自己的需求进行添加。总之,我这里是提供一种思路,如有更佳,欢迎分享。


    作者:盗道
    链接:https://juejin.cn/post/7026544177844355103

    收起阅读 »

    你写过的所有代码都逃不过这两方面:API 和抽象

    作为前端,你可能开发过 Electron 桌面应用、小程序、浏览器上的 web 应用、基于 React Native 等跨端引擎的 app,基于 Node.js 的工具或者服务等各种应用,这些都是 JS 的不同的 runtime,开发也都是基于前端那套技术。 ...
    继续阅读 »

    作为前端,你可能开发过 Electron 桌面应用、小程序、浏览器上的 web 应用、基于 React Native 等跨端引擎的 app,基于 Node.js 的工具或者服务等各种应用,这些都是 JS 的不同的 runtime,开发也都是基于前端那套技术。


    面对这么多的细分领域,作为前端工程师的你是否曾迷茫过:这么多技术我该学什么?他们中有没有什么本质的东西呢?


    其实所有的这些技术,你写过的所有代码,都可以分为两个方面: api 和 抽象。


    api


    不同平台提供的 api 不同,支持的能力不同:


    浏览器提供了 dom api、支持了 css 的渲染,还提供了音视频、webgl 等相关 api,这些 api 是我们开发前端应用的基础。


    Node.js 提供了操作系统能力的 api,比如进程、线程、网络、文件等,这些 api 是我们开发工具链或后端应用的基础。


    React Native 等跨端引擎支持了 css 的渲染,还提供了设备能力的 api,比如照相机、闪光灯、传感器、GPS 等 api,这是我们开发移动 app 的基础。


    Electron 集成了 Chromium 和 Node.js,同时还提供了桌面相关的 api。


    小程序支持了 css 的渲染之外,还提供了一些宿主 app 能力的 api。


    此外,还有很多的 runtime,比如 vscode 插件、sketch 插件等,都有各自能够使用的 api。


    不同的 JS runtime 提供了不同 api 给上层应用,这是应用开发的基础,也是应用开发的能力边界。


    抽象


    基于 runtime 提供的 api 我们就能完成应用的功能开发,但是复杂场景下往往会做一些抽象。


    比如浏览器上的前端应用主要是把数据通过 dom api 和 css 渲染出来,并做一些交互,那么我们就抽象出了数据驱动的前端框架,抽象出了组件、状态、数据流等概念。之后就可以把不同的需求抽象为不同的组件、状态。


    经过层层抽象之后,开发复杂前端应用的时候代码更容易维护、成本更低。


    比如基于 Node.js 的 fs、net、http 等 api 我们就能实现 web server,但是对于复杂的企业级应用,我们通过后端框架做 MVC 的抽象,抽象出控制器、服务、模型、视图等概念。之后的后端代码就可以把需求抽象为不同的控制器和服务。


    经过 MVC 的抽象之后,后端应用的分层更清晰、更容易维护和扩展。


    复杂的应用需要在 api 的基础上做一些抽象。我们往往会用框架做一层抽象,然后自己再做一层抽象,经过层层抽象之后的代码是更容易维护和扩展的。这也就是所谓的架构。


    如何深入 api 和抽象


    api


    api 是对操作系统能力或不同领域能力的封装。


    比如 Node.js 的进程、线程、文件、网络的 api 是对操作系统能力的封装,想深入它们就要去学习操作系统的一些原理。


    而 webgl、音视频等 api 则分别是对图形学、音视频等领域的能力的封装,想要深入它们就要去学习这些领域的一些原理。


    个人觉得我们知道 api 提供了什么能力就行,没必要过度深入 api 的实现原理。


    抽象


    抽象是基于编程语言的编程范式,针对不同目标做的设计。


    Javascript 提供了面向对象、函数式等编程范式,那么就可以基于对象来做抽象,使用面向对象的各种设计模式,或者基于函数式那一套。这是抽象的基础。


    抽象是根据不同的目标来做的。


    前端领域主要是要分离 dom 操作和数据,把页面按照功能做划分,所以根据这些目标就做了 mvvm 和组件化的抽象。


    后端领域主要是要做分层、解耦等,于是就做了 IOC、MVC 等抽象。


    可以看到,抽象是基于编程语言的范式,根据需求做的设计,好的框架一定是做了满足某种管理代码的需求的抽象。


    想要提升抽象、架构设计能力的话,可以学习下面向对象的设计模式,或者函数式等编程范式。研究各种框架是如何做的抽象。


    总结


    不同平台提供了不同的 api,这是应用开发的基础和边界。复杂应用往往要在 api 基础上做层层抽象,一般会用框架做一层抽象,自己再做一层抽象,目标是为了代码划分更清晰,提升可维护性和可扩展性。


    其实我们写过的所有代码,都可以分为 api 和抽象这两方面。


    深入 API 原理的话要深入操作系统和各领域的知识。提升抽象能力的话,可以学习面向对象的设计模式或者函数式等编程范式。


    不管你现在做哪个平台之上的应用开发,刚开始都是要先学习 api 的,之后就是要理解各种抽象了:框架是怎么抽象的,上层又做了什么抽象。


    API 保证下限,抽象可以提高上限。而且抽象能力或者说架构能力是可以迁移的,是程序员最重要的能力之一。


    作者:zxg_神说要有光
    链接:https://juejin.cn/post/7031931672538906637

    收起阅读 »

    线性表

    由于我是搞前端的为了更友好的描述数据结构,所以全部代码示例都是用TypeScript来编写。 1、线性表类型 1.顺序存储结构(数组) 2.链式存储结构(链表) 1.1、顺序存储 一般指数组,内部数据的存储单元在内存中相邻 优势: 查询很快,时间复杂度为...
    继续阅读 »

    由于我是搞前端的为了更友好的描述数据结构,所以全部代码示例都是用TypeScript来编写。


    1、线性表类型



    • 1.顺序存储结构(数组)

    • 2.链式存储结构(链表)


    1.1、顺序存储


    一般指数组,内部数据的存储单元在内存中相邻



    优势: 查询很快,时间复杂度为O(1)


    劣势:



    1. 元素增、删操作时间复杂度为O(n)

    2. 使用时需要提前确定长度




    1. 需要占据连续内存空间


    1.2、链式存储


    n 个数据元素的有限序列,通常为链式,叫作线性链表或链表。链表中的元素为结点,结点是一块内存空间存储一条数据。结点通常由两个部分组成:



    • 节点存储的数据

    • 指向下一个节点的指针



    来看一下链表的typescript实现


    class ListNode {
    val: number
    next: ListNode | null
    constructor(val?: any, next?: ListNode | null) {
    this.val = val
    this.next = (next===undefined ? null : next)
    }
    }

    2、链表类型


    链表类型大体分为下列:



    • 带头、不带头

    • 单向、双向




    • 循环、非循环


    2.1 带头不带头


    链表都有头指针,带头结点的链表头指针指向的是头结点,不带头结点的头指针直接指向首元结点。


    首元节点:链表中用来存储元素节点的第一个节点


    带头:



    不带头:



    操作差异: 删除和新增操作中,无论操作位置,带头结点的链表不需要修改头指针的值,而不带头结点的有时候需要。清空操作中,带头结点的保留头结点,不带头结点的要销毁。


    结构差异: 带头链表不论链表是否为空,均含有一个头结点,不带头单链表均无结点。


    一般使用链表都为带头链表


    2.2、双向链表


    单项链表中,仅有一个指针指向下一个节点的位置,双向链表中,每个节点有有个指针:



    • pre:指向上一个节点位置

    • next:指向下一个节点位置


    双向链表节点图:



    双向链表节点数据结构:


    class TwoWayListNode {
    val: number
    pre: ListNode | null
    next: ListNode | null
    constructor(val?: any, pre?: ListNode | null, next?: ListNode | null) {
    this.val = val
    this.next = (next===undefined ? null : next)
    }
    }

    双向链表图:



    // 简单的来实现一下上述结构,实际使用时可自行封装统的类
    const p1 = new TwoWayListNode('p1', null, null)
    const p2 = new TwoWayListNode('p2', null, null)
    const p3 = new TwoWayListNode('p3', null, null)

    p1.next = p2
    p2.pre = p1
    p2.next = P3

    2.3、循环链表


    链表中最后一个节点指向头节点



    // 简单的来实现一下上述结构,实际使用时可自行封装统的类
    const p1 = new ListNode('p1', null)
    const p2 = new ListNode('p2', null)
    const p3 = new ListNode('p3', null)

    p1.next = p2
    p2.pre = p1
    p2.next = P3
    p3.next = p1

    以此类推还有双向项循环链表,这里就不展开了


    3、线性表数据处理分析


    3.1、顺序存储操作


    查询:


    由于顺序存储中数据按照逻辑顺序依次放入连续的存储单元中,所以在顺序表结构中很容易实现查询操作,直接通过下标去拿即可。时间复杂度为O(1)


    插入:


    在顺序存储结构中, 插入尾部的时间复杂度为O(1),其他位置时间复杂度为O(n)。


    如下图想要在3位置插入一条数据 "六",需要将"四", "五" 位置的数据依次向后移动一个单位





    删除:


    在顺序存储结构中, 删除尾部的时间复杂度为O(1),其他位置时间复杂度为O(n)。


    如下图想删除数据 "六",先将3位置的数据设置为空,"四", "五" 位置的数据依次向前移动一个单位



    3.2 链式存储


    插入:



    // p1 -> p2 -> p3 -> p4 
    p1.next = p5
    p5.next = p2





    删除:



    // p1 -> p2 -> p3 -> p4 
    p1.next = p3
    p2.next = null

    查询:


    链表中查找只能从链表的头指针出发,顺着连标指针逐个结点查询,直到查到想要的结果为止,时间复杂度O(n)


    作者:siegaii
    链接:https://juejin.cn/post/7031868181203386405

    收起阅读 »

    图解:什么是AVL树?

    引子上一次我给大家介绍了什么是二叉搜索树,但是由于二叉搜索树查询效率的不稳定性,所以很少运用在实际的场景中,所以我们伟大的前人就对二叉搜索树进行了改良,发明了AVL树。AVL树是一种自平衡二叉搜索树,因为AVL树任意节点的左右子树高度差的绝对值不超过1,所以A...
    继续阅读 »

    引子

    上一次我给大家介绍了什么是二叉搜索树,但是由于二叉搜索树查询效率的不稳定性,所以很少运用在实际的场景中,所以我们伟大的前人就对二叉搜索树进行了改良,发明了AVL树。

    AVL树是一种自平衡二叉搜索树,因为AVL树任意节点的左右子树高度差的绝对值不超过1,所以AVL树又被称为高度平衡树。

    AVL树本质上是一棵带有平衡条件的二叉搜索树,它满足二叉搜索树的基本特性,所以本次主要介绍AVL树怎么自平衡,也就是理解它的旋转过程。

    二叉搜索树特性忘了的小伙伴可以看之前的文章:搞定二叉搜索树,9图足矣!同时我也将基本性质给大家再回顾一遍:

    1. 若它的左子树不为空,则左子树上所有节点的值均小于根节点的值。

    2. 若它的右子树不为空,则右子树上所有节点的值均大于根节点的值。

    3. 它的左、右子树也分别为二叉搜索树。

    平衡条件:每个节点的左右子树的高度差的绝对值不超过1。

    我们将每个节点的左右子树的高度差的绝对值又叫做平衡因子。

    AVL树的旋转行为一般是在插入和删除过程中才发生的,因为插入过程中的旋转相比于删除过程的旋转而言更加简单和直观,所以我给大家图解一下AVL树的插入过程。

    插入过程

    最开始的时候为空树,没有任何节点,所以我们直接用数据构造一个节点插入就好了,比如第一个要插入的数据为18。

    第一个节点插入完成,开始插入第二个节点,假如数据为20。

    插入第三个节点数据为14。

    第四个节点数据为16。从根节点位置开始比较并寻找16的对应插入位置。

    第五个要插入的数据为12。还是一样,从树的根节点出发,根据二叉搜索树的特性向下寻找到对应的位置。

    此时插入一个数据11,根据搜索树的性质,我们不难找到它的对应插入位置,但是当我们插入11这个节点之后就不满足AVL树的平衡条件了。

    此时相当于18的左子树高了,右子树矮了,所以我们应该进行一次右单旋,右单旋使左子树被提起来,右子树被拉下去,相当于左子树变矮了,右子树变高了,所以一次旋转之后,又满足平衡条件了。

    简单分析上图的旋转过程:**因为左子树被提上去了,所以14成为了新的根节点,而18被拉到了14右子树的位置,又因为14这个节点原来有右子节点为16,所以18与16旋转之后的位置就冲突了,但是因为16小于18,**所以这个时候根据二叉搜索树的特性,将16调整到18的左子树中去,因为旋转之后的18这个节点的左子树是没有节点的,所以16可以直接挂到18的左边,如果18的左子树有节点,那么还需要根据二叉搜索树的性质去将16与18左子树中的节点比较大小,直到确定新的位置。

    经过上面的分析我们可以知道:如果新插入的节点插入到根节点较高左子树的左侧,则需要进行一次右单旋,我们一般将这种情况简单记为左左情况,第一个左说的是较高左子树的左,第二个左说的是新节点插入到较高左子树的左侧。

    分析完了左左的情况,我想小伙伴们不难推出右右的情况(第一个右说的是较高右子树的右,第二个右说的是新节点插入到较高右子树的右侧),就是一次左单旋,这里就不一步一步地分析右右的情况了,因为它和左左是对称的。给大家画个图,聪明的你一眼就可以学会!

    现在两种单旋的情况已经讲完了,分别是左左和右右,还剩下两种单旋的情况,不过别慌,因为双旋比你想象中的简单,而且同样,双旋也是两种对称的情况,实际上我们只剩下一种情况需要分析了,所以,加油,弄懂了的话,面试的时候就完全不用慌了!

    双旋

    我们假设当前的AVL树为下图。

    这个时候我们新插入一个节点,数据为15,根据搜索树的性质,我们找到15对应的位置并插入,如图

    我们此时再次计算每个节点的平衡因子,发现根节点18的平衡因子为2,超过了1,不满足平衡条件,所以需要对他进行旋转。

    我们将刚才需要进行右单旋的左左情况和现在的这种情况放在一起对比一下,聪明的你一定发现,当前的情况相比于左左的情况只是插入的位置不同而已,左左情况插入的节点在根节点18较高左子树的左侧,而当前这种情况插入节点是在根节点18较高左子树的右侧,我们将它称为左右情况。

    那么可能正看到这里的你可能不禁会想:这不跟刚才左左差不多嘛,直接右单旋不就完事了。真的是这样吗?让我们来一次右单旋看看再说。

    简单分析该右单旋:**节点14上提变成新的根节点,18下拉变成根节点的右子树,又因为当前根节点14原来有右子树16,所以18与16位置冲突,**比较18与16大小之后,发现18大于16,根据搜索树的性质,将以16为根节点的子树调整到18的左子树,因为18的左子树目前为空,所以以16为根的子树直接挂在18的左侧,若18的左子树不为空,则需要根据搜索树的性质继续进行比较,直到找到合适的挂载位置。

    既然一次右单旋不行,那么我们应该怎么办呢?答案就是进行一次双旋,一次双旋可以拆分成两次单旋,对于当前这种不平衡条件,我们可以先进行一次左单旋,再进行一次右单旋,之后就可以将树调整成满足平衡条件的AVL树了,话不多说,图解一下。

    简单分析左右双旋先对虚线框内的子树进行左单旋,则16上提变成子树的新根,以14为根节点的子树下拉,调整到16的左子树,此时发现16的左子树为15,与14这棵子树冲突,所以根据搜索树规则进行调整,将15挂载到以14为根节点子树的右子树,从而完成一次左单旋,之后再对整棵树进行一次右单旋,节点16上提成为新的根节点,18下拉变成根节点的右子树,因为之前16没有右子树,所以以18为根节点的子树直接挂载到16的右子树,从而完成右旋。

    同样,对于左右情况的对称情况右左情况我就不给大家分析了,还是将图解送给大家,相信聪明的你一看就会!

    到此为止,我将AVL树的四种旋转情况都给大家介绍了一遍,仔细想想,其实不止这四种情况需要旋转,严格意义上来说有八种情况需要旋转,比如之前介绍的左左情况吧,我们说左左就是将新的节点插入到了根节点较高左子树的左侧,这个左侧其实细分一下又有两种情况,只不过这两种情况实际可以合成一种情况来看,也就是新的节点插入到左侧的时候可以成为它父亲节点的左孩子,也可以成为它父亲节点的右孩子,那么这样的话就是相当于两种情况了,简单画个图看一下吧。

    就是这样上图这样,每个新插入的节点都可以是它父亲节点的左孩子或者右孩子,这取决于新插入数据的大小,比如11就是12的左孩子,13就是12的右孩子,这两种情况都属于左左情况,也就是说他们本质上是一样的,都插在了节点18较高左子树的左侧。

    那么这样看来这四种旋转情况严格上看都可以多分出一种情况,变成八种情况。

    后话

    emmm…这样看来AVL树确实解决了二叉搜索树可能不平衡的缺陷,补足了性能上不稳定的缺陷,但是细细想来AVL树的效率其实不是很好,这里说的不是查询效率,而是插入与删除效率,上面所说的这四大种八小种情况还是很容易命中的,那么这样的话就需要花费大量的时间去进行旋转调整,我的天,这样也太难搞了!

    不过聪明的前人早就为我们想好了更加利于实际用途的搜索树,在现实场景中AVL树和二叉搜索树一样,基本上用不到,我们接下来要讲的这种二叉类的搜索树才是我们经常应用的,相信见多识广的你一定猜到了它的名字,对,就是它,大名鼎鼎的红黑树!我们下次来盘他!

    鄙人才疏学浅,若有任何差错,还望各位海涵,不吝指教!

    喜欢本文的少侠们,欢迎关注公众号雷子的编程江湖,修炼更多武林秘籍。

    一键三连是中华民族的当代美德!


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

    收起阅读 »

    恕我直言,我怀疑你并不会用 Java 枚举

    开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum。 为了证明这一点,我们来新建一个枚举 PlayerType: public enum PlayerType { TE...
    继续阅读 »

    开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum。


    为了证明这一点,我们来新建一个枚举 PlayerType:


    public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
    }

    两个关键字带一个类名,还有大括号,以及三个大写的单词,但没看到继承 Enum 类啊?别着急,心急吃不了热豆腐啊。使用 JAD 查看一下反编译后的字节码,就一清二楚了。


    public final class PlayerType extends Enum
    {

    public static PlayerType[] values()
    {
    return (PlayerType[])$VALUES.clone();
    }

    public static PlayerType valueOf(String name)
    {
    return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
    }

    private PlayerType(String s, int i)
    {
    super(s, i);
    }

    public static final PlayerType TENNIS;
    public static final PlayerType FOOTBALL;
    public static final PlayerType BASKETBALL;
    private static final PlayerType $VALUES[];

    static
    {
    TENNIS = new PlayerType("TENNIS", 0);
    FOOTBALL = new PlayerType("FOOTBALL", 1);
    BASKETBALL = new PlayerType("BASKETBALL", 2);
    $VALUES = (new PlayerType[] {
    TENNIS, FOOTBALL, BASKETBALL
    });
    }
    }

    看到没?PlayerType 类是 final 的,并且继承自 Enum 类。这些工作我们程序员没做,编译器帮我们悄悄地做了。此外,它还附带几个有用静态方法,比如说 values()valueOf(String name)


    01、内部枚举


    好的,小伙伴们应该已经清楚枚举长什么样子了吧?既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。


    public class Player {
    private PlayerType type;
    public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
    }

    public boolean isBasketballPlayer() {
    return getType() == PlayerType.BASKETBALL;
    }

    public PlayerType getType() {
    return type;
    }

    public void setType(PlayerType type) {
    this.type = type;
    }
    }

    PlayerType 就相当于 Player 的内部类,isBasketballPlayer() 方法用来判断运动员是否是一个篮球运动员。


    由于枚举是 final 的,可以确保在 Java 虚拟机中仅有一个常量对象(可以参照反编译后的静态代码块「static 关键字带大括号的那部分代码」),所以我们可以很安全地使用“==”运算符来比较两个枚举是否相等,参照 isBasketballPlayer() 方法。


    那为什么不使用 equals() 方法判断呢?


    if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
    if(player.getType() == Player.PlayerType.BASKETBALL){};

    “==”运算符比较的时候,如果两个对象都为 null,并不会发生 NullPointerException,而 equals() 方法则会。


    另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 equals() 方法则不会。



    02、枚举可用于 switch 语句


    这个我在之前的一篇我去的文章中详细地说明过了,感兴趣的小伙伴可以点击链接跳转过去看一下。


    switch (playerType) {
    case TENNIS:
    return "网球运动员费德勒";
    case FOOTBALL:
    return "足球运动员C罗";
    case BASKETBALL:
    return "篮球运动员詹姆斯";
    case UNKNOWN:
    throw new IllegalArgumentException("未知");
    default:
    throw new IllegalArgumentException(
    "运动员类型: " + playerType);

    }

    03、枚举可以有构造方法


    如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。


    public enum PlayerType {
    TENNIS("网球"),
    FOOTBALL("足球"),
    BASKETBALL("篮球");

    private String name;

    PlayerType(String name) {
    this.name = name;
    }
    }

    04、EnumSet


    EnumSet 是一个专门针对枚举类型的 Set 接口的实现类,它是处理枚举类型数据的一把利器,非常高效(内部实现是位向量,我也搞不懂)。


    因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法:



    下面的示例中使用 noneOf() 创建了一个空的 PlayerType 的 EnumSet;使用 allOf() 创建了一个包含所有 PlayerType 的 EnumSet。


    public class EnumSetTest {
    public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
    }

    public static void main(String[] args) {
    EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
    System.out.println(enumSetNone);

    EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
    System.out.println(enumSetAll);
    }
    }

    程序输出结果如下所示:


    []
    [TENNIS, FOOTBALL, BASKETBALL]

    有了 EnumSet 后,就可以使用 Set 的一些方法了:



    05、EnumMap


    EnumMap 是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。


    和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字:


    EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);

    有了 EnumMap 对象后就可以使用 Map 的一些方法了:



    和 HashMap 的使用方法大致相同,来看下面的例子:


    EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
    enumMap.put(PlayerType.BASKETBALL,"篮球运动员");
    enumMap.put(PlayerType.FOOTBALL,"足球运动员");
    enumMap.put(PlayerType.TENNIS,"网球运动员");
    System.out.println(enumMap);

    System.out.println(enumMap.get(PlayerType.BASKETBALL));
    System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
    System.out.println(enumMap.remove(PlayerType.BASKETBALL));

    程序输出结果如下所示:


    {TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员}
    篮球运动员
    true
    篮球运动员

    06、单例


    通常情况下,实现一个单例并非易事,不信,来看下面这段代码


    public class Singleton {  
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
    if (singleton == null) {
    synchronized (Singleton.class) {
    if (singleton == null) {
    singleton = new Singleton();
    }
    }
    }
    return singleton;
    }
    }

    但枚举的出现,让代码量减少到极致:


    public enum EasySingleton{
    INSTANCE;
    }

    完事了,真的超级短,有没有?枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。


    07、枚举可与数据库交互


    我们可以配合 Mybatis 将数据库字段转换为枚举类型。现在假设有一个数据库字段 check_type 的类型如下:


    `check_type` int(1) DEFAULT NULL COMMENT '检查类型(1:未通过、2:通过)',

    它对应的枚举类型为 CheckType,代码如下:


    public enum CheckType {
    NO_PASS(0, "未通过"), PASS(1, "通过");
    private int key;

    private String text;

    private CheckType(int key, String text) {
    this.key = key;
    this.text = text;
    }

    public int getKey() {
    return key;
    }

    public String getText() {
    return text;
    }

    private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
    static {
    for(CheckType d : CheckType.values()){
    map.put(d.key, d);
    }
    }

    public static CheckType parse(Integer index) {
    if(map.containsKey(index)){
    return map.get(index);
    }
    return null;
    }
    }

    1)CheckType 添加了构造方法,还有两个字段,key 为 int 型,text 为 String 型。


    2)CheckType 中有一个public static CheckType parse(Integer index)方法,可将一个 Integer 通过 key 的匹配转化为枚举类型。


    那么现在,我们可以在 Mybatis 的配置文件中使用 typeHandler 将数据库字段转化为枚举类型。


    <resultMap id="CheckLog" type="com.entity.CheckLog">
    <id property="id" column="id"/>
    <result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
    </resultMap>

    其中 checkType 字段对应的类如下:


    public class CheckLog implements Serializable {

    private String id;
    private CheckType checkType;

    public String getId() {
    return id;
    }

    public void setId(String id) {
    this.id = id;
    }

    public CheckType getCheckType() {
    return checkType;
    }

    public void setCheckType(CheckType checkType) {
    this.checkType = checkType;
    }
    }

    CheckTypeHandler 转换器的类源码如下:


    public class CheckTypeHandler extends BaseTypeHandler<CheckType> {

    @Override
    public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
    return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
    return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
    return CheckType.parse(cs.getInt(index));
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
    ps.setInt(index, val.getKey());
    }
    }

    CheckTypeHandler 的核心功能就是调用 CheckType 枚举类的 parse() 方法对数据库字段进行转换。



    恕我直言,这篇文章看完后,我觉得小伙伴们肯定会用 Java 枚举了,如果还不会,就过来砍我!


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

    程序员如何优雅的挣零花钱?

    前言虽然程序员有女朋友的不多(误),但是开销往往都不小。 VPS、域名、Mac上那一堆的收费软件、还有Apple每年更新的那些设备,经常都是肾不够用的节奏。 幸好作为程序员,我们有更多挣钱的姿势。 有同学该嚷了:不就是做私单嘛。 ...
    继续阅读 »

    前言

    虽然程序员有女朋友的不多(误),但是开销往往都不小。


    VPS、域名、Mac上那一堆的收费软件、还有Apple每年更新的那些设备,经常都是肾不够用的节奏。


    幸好作为程序员,我们有更多挣钱的姿势。


    有同学该嚷了:不就是做私单嘛。


    对,但是也不太对。做私单的确是一个简单直接方式,但在我看来,私单的投入产出比很差,并不是最优的。


    但既然提到了,就先说说吧。

    关于

    本文作者:easychen 


    GitHub地址:https://github.com/easychen




    私单


    远程外包


    最理想的单子还是直接接海外的项目,比如freelance.com等网站。一方面是因为挣的是美刀比较划算,之前看到像给WordPress写支付+发送注册码这种大家一个周末就能做完的项目,也可以到200~300美刀;另一方面是在国外接单子比较隐蔽。


    常用国外网站:



    (由ahui132同学补充)



    本段由tvvocold同学贡献。国内也有一个软件众包平台 CODING 码市 。 码市基于云计算技术搭建的云端软件开发平台 Coding.net 作为沟通和监管工具,快速连接开发者与需求方,旨在通过云端众包的方式提高软件交付的效率。码市作为第三方监管平台,会对所有项目进行审核以保证项目需求的明确性,并提供付款担保,让开发者只要按时完成项目开发即可获取酬劳。你可以 在这里 看到开发者对码市的评价。



    当然,猪八戒这种站我就不多说了,不太适合专业程序员去自贬身价。


    按需雇用


    按需雇用是近几年新兴的私单方式,开发者在业余时间直接到雇主公司驻场办公,按时薪领取报酬。这种方式省去了网络沟通的低效率,也避免了和雇主的讨价还价,适合怕麻烦的程序员。


    拉勾大鲲





    大鲲 由拉勾网推出,考虑到拉勾上三十多万的招聘方,大鲲不缺雇主,这是其他独立平台相对弱势的地方。


    实现网





    实现网的价格也很不错,但是我强烈建议大家不要在介绍中透漏实名和真实的公司部门信息,因为这实在太高调了。有同学说,这是我的周末时间啊,我爱怎么用就怎么用,公司还能告我怎么的? 虽然很多公司的劳动合同里边并不禁止做兼职,但在网上如此高调的干私活,简直就是在挑衅HR:「我工作不饱和」、「公司加班不够多」… 再想象下你一边和产品经理说这个需求做不完,一边自己却有时间做私单的样子。你自己要是老板也不愿提拔这样的人吧。


    (我这几天重新去看了下,人才页面已经不再显示姓名了,只用使用头像。这样只要在工作经历介绍里边注意一点,就可以避免上述问题了。)


    程序员客栈





    不太熟悉,但国内按需雇用的网站不多,写出来供大家参考。


    Side Project


    比起做私单,做一个Side Project会更划算。


    Side Project的好处是你只需要对特定领域进行持续投入,就可以在很长时间获得收入。这可以让你的知识都在一棵树上分支生长,从而形成良好的知识结构,而不是变成一瓶外包万金油。


    思路有两种:


    一种是做小而美的,针对一个细分领域开发一个功能型应用,然后放到市场上去卖;


    另一种是做大而全的基础应用(想想WordPress),方便别人在上边直接添加代码,定制成自己想要的应用。


    前一种做起来比较快,但需要自己去做一些销售工作;后一种通常是开源/免费+收费模式,推广起来更简单。


    有同学会说,我写的 Side Project 就是卖不掉啊。项目方向的选取的确是比较有技巧的,但简单粗暴的解决方案就是找一个现在卖得非常好、但是产品和技术却不怎样的项目,做一个只要一半价格的竞品。


    比如 Mac 下有一个非常有名的写作软件,叫 Ulysses 。我试用了一下非常不错,但就是贵,283 RMB。后来看到了 Mweb ,光是免费的 Lite 版覆盖了 Ulysses 的主功能,完整版也才98RMB,几乎没有思考就买下来了。


    做咨询


    专家平台


    如果你在技术圈子里边小有名气,或者在某一个业务上特别精通,那么通过做咨询来挣钱是一种更轻松的方式。和人在咖啡厅聊几个小时,几百上千块钱就到手了。


    国内这方面的产品,我知道的有下边几个:




    • 在行: 这个是果壳旗下的,做得比较早,内容是全行业的,所以上边技术向的反而不多。




    • 缘创派: 缘创派的轻合伙栏目,主要面向创业者,适合喜欢感受创业氛围的技术专家们。




    • 极牛: 你可以认为这是一个程序员版本的「在行」,我浏览了下,虽然被约次数比在行要低不少,但专业性比较强,期望他们能尽快的推广开来吧。




    • 知加:这个项目是我参与的,面向程序员,类似「分答」的付费语音问答,刚开始内测,上边有一些硅谷科技公司的同学。感兴趣的可以看看。




    做咨询虽然也是实名的,但和私活是完全不同的。咨询的时间短,不会影响到正常的休息,更不会影响上班;而且大部分公司是鼓励技术交流的,所以大家的接受度都很高。


    付费社群


    除了APP外,我觉得收费群也是可以做的。比如可以搞一个技术创业群,找一些创业成功的同学、做投资的同学、做法务的同学,面向想创业的同学开放,每人收个几百块的年费。然后你在创业过程中遇到的问题,都可以有人解答,不会觉得是孤零零的一个人。如果遇到了问题,群里的人可以解答;如果没遇到问题,那不是更好么。有种卖保险的感觉,哈哈哈。


    比较好用的工具是 知识星球 也就是之前的小密圈。这个工具比较适合交流和讨论,长文比较痛苦。可以发布到其他地方,然后粘贴回来。





    另一个靠谱的工具大概是微博的 V+ 会员。说它靠谱主要是它在微博上,所以等于整合了 「内容分发」→ 「新粉丝获取」 → 「付费用户转化」 的整个流程。


    PS:交流型付费社群的一个比较难处理的事情是,很难平衡免费的粉丝和付费的社群之间的关系,所以我最后的选择是付费类的提供整块的内容,比如整理成册的小书、录制的实战视频等;而日常零碎的资料分享还是放到微博这种公开免费的平台。


    写文章


    投稿


    很多同学喜欢写技术博客,其实把文章投给一些网站是有稿费的。比如InfoQ,他们家喜欢收3000~4000字的深度技术文章;稿费是千字150。虽然不算太多,但一篇长文的稿费也够买个入门级的Cherry键盘了。我喜欢InfoQ的地方是他们的版权要求上比较宽松。文章在他们网站发布后,你可以再发布到自己博客的;而且文章可以用于出书,只要标明原发于InfoQ即可。


    更详细的说明见这里:http://www.infoq.com/cn/article-guidelines



    微博的@rambone同学补充到,文章还可以发到微博、微信、简书等支持打赏的平台。考虑到简书CEO及其官博对程序员的奇葩态度,个人建议是换个咱程序员自己的平台写文章。



    出版


    顺便说一句,比起写文章,其实通过传统发行渠道出书并不怎么挣钱,我之前到手的版税是8%,如果通过网络等渠道销售,数字会更低。出电子书收益会好一些,我之前写过一篇文章专门介绍:《如何通过互联网出版一本小书》


    以前一直写图文为主的书,用Markdown非常不错;但最近开始写技术教程类的书,发现Markdown不太够用了,最主要的问题有 ① 不支持视频标签,尤其是本地视频方案 ② 代码高亮什么的很麻烦 ③ 也没有footer note、文内说明区域的预置。


    这里要向大家严重推荐Asciidoc,你可以把它看成一个增强版的Markdown,预置了非常多的常用格式,而且GitBook直接就支持这个格式(只要把.md 搞成 .adoc 就好),Atom也有实时预览插件。用了一段时间,非常喜欢。


    付费文集


    最近一年有不少的付费文集产品出现,可以把它看成传统出版的一个网络版。一般是写作十篇以内的系列文章,定价为传统计算机书的一半到三分之一。付费文集产品通常是独家授权,所以在选择平台方面一定要慎重,不然一个好作品可能就坑掉了。


    掘金小册





    小册是由掘金推出的付费文集产品。我是小册的第一批作者,一路用下来还是很不错的。文章格式直接采用 Markdown , 发布以后可以实时更新,保证内容的新鲜度,非常的方便。小册的一般定价在19~29,通用内容销量好的能过千,细分内容基本也能过百。挣零花钱的话,是个非常不错的选择。


    达人课





    达人课是 GitChat 旗下的付费文集产品,现在应该已经合并到 CSDN 了。GitChat 的用户群不算大,但付费意愿还可以,大概因为内容就没有免费的🤣。之前我上课的时候是提交完成以后的文档给编辑,由编辑同学手动上架。感觉比较麻烦,尤其是修改错别字什么的。


    小专栏





    这个平台不熟……写到这里仅供参考。


    教学视频



    微博的@瓜瓜射门啦同学给了自己应聘程序教学网站讲师的经验:应聘程序教学网站讲师,出视频+作业教程,平台按小时支付,这个不知道算不算挣零花钱,我算了一下去年,一年大概出 20 个小时视频,拿到手是不到 6 万的样子,平时就是周末花时间弄下。



    在线教育起来以后,录制教学视频也可以赚钱了。关于录制在线课程的收益,一直不为广大程序员所知。但最近和51CTO学院 和 网易云课堂 的同学聊天,才发现一个优秀的40~60节的微专业课程,一年的收益比得上一线城市高级总监的收入。难怪最近做培训的人这么多😂


    渠道和分成


    大部分的平台合同有保密协议,所以不能对外讲。但网易云课堂和Udemy在公开的讲师注册协议中写明了分成,所以这里说一下。


    网易云课堂


    网易的课分三类:




    • 独立上架:等于网易提供平台(视频上传管理、用户管理、支付系统等),由你自己来负责营销。这个分成比例在 9:1 ,平台收取 10% 的技术服务费。我觉得非常划算。




    • 精品课:由网易帮你推广,但需要和他们签订独立的合同,会收取更多的分成。最麻烦的是,通常是独家授权。一旦签署了,就不能在其他平台上架课程了。




    • 微专业:这个是网易自己规划的课程体系,从课程的策划阶段就需要和他们深度沟通。也是网易推广力度最大、收益最大的一类课程。




    方糖全栈课就放在网易平台上,觉得好的地方如下:




    • 支付渠道相对全,还支持花呗,这样对于我这种高价课就非常重要。苹果应用内购买课程会渠道费用会被苹果扣掉30%,好想关掉 🤣




    • 自带推广系统,愿意的话可以用来做课程代理系统。




    Udemy


    相比之下 Udemy 就很贵了,分成是 5:5 ;支付上国内用户只能通过信用卡或者银行卡绑 paypal 支付。但可以把课程推向全球。(但我英文还不能讲课🙃)


    腾讯课堂没用过,欢迎熟悉的同学 PR 。


    小课和大课


    我个人喜欢把视频分成小课和大课两种。因为视频虽然看起来时间短,但实际上要做好的话,背后要消耗的时间、要投入精力还是非常多的。大课动不动就是几十上百个课时,绝大部分上班的程序员都没有时间来录制。所以挣零花钱更适合做小课,这种课一般就几个小时,剪辑成 10 个左右的小课时,价格在几十百来块。如果是自己专业纯熟的领域,一个长假就可以搞定。


    表现形式


    在课程的表现形式上,我个人更喜欢designcode.io这种图文+视频的模式,一方面是学习者可以快速的翻阅迅速跳过自己已经学会的知识;另一方面,会多出来 微博头条文章、微信公众号、知乎和简书专栏这些长文推广渠道。





    当然,内容本身才是最核心的。现在那么多的免费视频,为什么要来买你的收费版?


    其实现在绝大部分教学视频,往往都真的只是教学,和现实世界我们遇到的问题截然不同。里边都是一堆简化后的假项目,为了教学而刻意设计的。


    这里和大家分享一个我之前想操作的想法。


    就是在自己决定开始做一个开源项目后,用录屏软件把整个过程完完整整的录下来。开源的屏幕录制工具OBS,1920*1080的屏幕录成FLV格式,一个小时只需要1G,一个T的移动硬盘可以录制上千小时,对一个中型项目来说也足够了。


    等项目做完,就开源放到GitHub,让大家先用起来。等迭代稳定后,再从录制的全量视频中剪辑出一系列的教程,整理出一系列的文章,放到网站上做收费课程。


    这样做有几个好处:




    • 保证所有遇到的问题都是真实的,不是想象出来的,学习过这个课程的人,可以独立的将整个项目完整的实现。




    • 没有特意的录制过程,所以教程其实是软件开发的副产品,投入产出比更高。




    • 如果你的软件的确写得好,那么用过你软件的人可以成为你的客户或者推荐员。




    后续


    今年我录制方糖全栈课的时候就采用了上边这个思路,效果还不错,不过有几个小问题:




    • 连续性。录着视频写代码总会有一种潜在焦虑,平时经常写一会儿休息一会儿,录像时就会留下大段的空白,有点浪费空间。当然这个主要是心理上的。




    • 录音。录音的问题更大一些。因为一个长期项目很难一直处于一个安静的环境,另外基础课录制可能需要大量的讲解,几个小时写下来嗓子哑了 🤣 。最后的解决方式是剪辑的时候重新配音,不过需要注意音画同步问题。




    软件


    如果是没有太多界面切换的课程,那可以使用keynote自带的录音。在其他环境里边的演示的视频可以直接粘贴到keynote里面来播放。


    但是当你有很多的外部界面的时候,就需要录屏了。mac上可以直接用quicktime来录制。文件,新建里边选 record screen就好。


    我录全栈课的时候,因为会在三个操作系统上录一些界面,所以我选择了obs。虽然这个工具主打的是直播,但实际上它的录制功能也还是挺不错的。


    剪辑的话,用mac的imovie基本就够了,主要用到的功能就是分割片段,然后把不要的删掉。音频去下底噪。部分等待时间过长的片段加速下。当然adobe家的也行,就是贵。


    硬件


    硬件上边,最好买一个用来支撑话筒的支架。不要用手直接握着话筒来录,这样就不会有电流声(或者很小)。外接声卡我用的是 XOX , 在 Mac 下边效果挺好,但不知道为啥在 Windows 上回声比较大(当然也可能是系统设置的原因)。


    内部推荐和猎头推荐


    如果你在BAT等一流互联网公司工作,如果你有一帮志同道合的程序员朋友,那么基本上每隔几个月你应该就会遇到有想换工作的同事和朋友,所以千万别错过你挣推荐费的大好时机。


    一般来讲,公司内部推荐的钱会少一些,我见过的3000~6000的居多。但因为是自己公司,会比较靠谱,所以风险小一些。经常给公司推荐人才,还会提升老大多你的好感度,能优先就优先吧。


    比起内部推荐,猎头推荐的推荐费则会多很多。一个30万年薪的程序员,成功入职后差不多可以拿到1万RMB的推荐费。但猎头渠道的问题在于对简历质量要求非常高,有知名公司背景的才容易成单;回款周期又特别长,一般要入职过了试用期以后才能拿到全部推荐费,得小半年。


    小结


    学会挣钱是一件非常重要的事情,它会让你了解商业是如何运作的,帮助你理解公司的产品逻辑、以及为你可能的技术创业打下坚实的基础。


    所以我鼓励大家多去挣零花钱,最好各种姿势都都试试,会有意想不到的乐趣。如果你有更好的挣零花钱技能,欢迎发PR过来,我会挑不错的合并进去 :)


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

    【小程序实战】- 将图片优化进行到底

    背景 前端的性能优化,图片优化是必不可少的重要环节,大部分网站页面的构成都少不了图片的渲染。尤其在电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,商品列表图等。图片加载数量多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验。 优...
    继续阅读 »

    背景


    前端的性能优化,图片优化是必不可少的重要环节,大部分网站页面的构成都少不了图片的渲染。尤其在电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,商品列表图等。图片加载数量多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验。


    优化方案


    基于上述问题的主要问题是图片数量和图片体积,所以应该怎么提高图片加载速度,提升用户体验。其实图片优化有非常多且优秀的方案,都可以从中借鉴,最后我们对图片进行不同方向的整体优化。


    image-20211021191413342.png


    使用合适的图片格式


    目前广泛应用的 WEB 图片格式有 JPEG/JPG、PNG、GIF、WebP、Base64、SVG 等,这些格式都有各自的特点,以下大概简单总结如下:


    WEB图片格式.png


    使用合适的图片格式通常可以带来更小的图片字节大小,通过合理压缩率,可以减少图片大小,且不影响图片质量。


    降低网络传输


    小程序使用腾讯云图片服务器,提供很多图片处理功能,比如图片缩放、图片降质,格式转换,图片裁剪、图片圆角等功能。这些功能可以通过在图片URL中添加规定参数就能实现,图片服务器会根据参数设置提前将图片处理完成并保存到CDN服务器,这样大大的减少图片传输大小。


    目前后台接口下发返回的图片 URL 都是未设置图片参数预处理,比如一张 800x800 尺寸高清的商品图,体积大概300k 左右,这样就很容易导致图片加载和渲染慢、用户流量消耗大,严重影响了用户体验。所以我们结合腾讯云的图片处理功能,网络图片加载前,先检测是否是腾讯云域名的图片URL,如果域名匹配,对图片URL进行预处理,预处理包括添加缩放参数添加降质参数添加WebP参数的方式减少图片网络传输大小


    我们先看一张通过图片服务器是腾讯云图片处理能力,通过设置图片缩放/降质/WebP,一张尺寸800x800,体积246KB图片,最后输出生成25.6KB,图片体积足足减少了80%,效果显著。


    image-20211021203109404.png


    图片缩放

    目前业务后台都是原图上传,原始图尺寸可能比客户端实际显示的尺寸要大,一方面导致图片加载慢,另一方面导致用户流量的浪费,其中如果是一张很大尺寸图片加载也会影响渲染性能,会让用户感觉卡顿,影响用户体验。通过添加缩放参数的方式,指定图片服务器下发更小和更匹配实际显示size的图片尺寸。


    图片降质

    图片服务器支持图片质量,取值范围 0-100,默认值为原图质量,通过降低图片质量可以减少图片大小,但是质量降低太多也会影响图片的显示效果,网络默认降图片质量参数设置为85,同时通过小程序提供的:wx.getNetworkTypewx.onNetworkStatusChangeoffNetworkStatusChange的接口监听网络状态变化来获取当前用户的网络类型networkType,比如用户当前使用的4G网络,则图片质量会动态设置为80,对于大部分业务情况,一方面可以大幅减少图片下载大小和保证用户使用体验,另一方面节省用户浏览 ,目前添加图片降质参数至少可以减少30-40%的图片大小。


    /**
    * 设置网络情况
    */
    const setNetwork = (res: Record<string, any>) => {
    const { isConnected = true, networkType = 'wifi' } = res;

    this.globalData.isConnected = isConnected;
    this.globalData.networkType = networkType.toLowerCase();
    this.events.emit(EventsEnum.UPDATE_NETWORK, networkType);
    };

    wx.getNetworkType({ success: (res) => setNetwork(res) });
    wx.offNetworkStatusChange((res) => setNetwork(res));
    wx.onNetworkStatusChange((res) => setNetwork(res));

    /**
    * 根据网络环境设置不同质量图片
    */
    const ImageQuality: Record<string, number> = {
    wifi: 85,
    '5g': 85,
    '4g': 80,
    '3g': 60,
    '2g': 60,
    };

    /**
    * 获取图片质量
    */
    export const getImageQuality = () => ImageQuality[getApp().globalData.networkType ?? 'wifi'];

    使用 WebP

    前面简单介绍不同的图片格式都有各自的优缺点和使用场景,其中 WebP 图片格式提供有损压缩与无损压缩的图片格式。按照Google官方的数据,与PNG相比,WebP无损图像的字节数要少26%WebP有损图像比同类JPG图像字节数少25-34%。现如今各大互联网公司的产品都已经使用了,如淘宝、京东和美团等。


    这里放一个 WebP 示例链接(GIF、PNG、JPG 转 Webp),直观感受 WebP 在图片大小上的优势。


    image-20211020191505147.png


    在移动端中 WebP的兼容性,大部分数用户都已经支持了 Can I use... Support tables for HTML5, CSS3, etc


    image-20211020131150424.png


    针对png/jpg图片格式,自动添加WebP参数,转成WebP图片格式。虽然WebP相比png/jpg图片解码可能需要更长时间,但相对网络传输速度提升还是很大。目前 ios 13系统版本有不少用户量的占比,小程序端获取当前系统版本,降级处理不添加WebP参数。


    // 检查是否支持webp格式
    const checkSupportWebp = () => {
    const { system } = wx.getSystemInfoSync();
    const [platform, version] = system.split(' ');

    if (platform.toLocaleUpperCase() === PlatformEnum.IOS) {
    return Number(version.split('.')[0]) > IOS_VERSION_13;
    }

    return true; // 默认支持webp格式
    };


    提示:由于目前图片服务器并不支持、SVG、GIFWebP,并没有做处理



    优化效果


    测试我们小程序首页列表接口加载图片,来对比优化前后的效果


    切片.png


    经过我们通过使用腾讯云图片服务器的图片处理功能,以及动态处理图片格式的方式,减少图片体积,提高图片加载速度,带来的收益比非常可观的


    图片懒加载


    懒加载是一种性能优化的方式,将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载,对于页面加载性能上会有很大的提升,也提高了用户体验。


    实现原理


    使用小程序提供Intersection Observer API,监听某些节点是否可以被用户看见、有多大比例可以被用户看见。这样我们就能判断图片元素是否在可是范围中,进行图片加载。


    我们基于小程序的Intersection Observer API,封装一个监听模块曝光 IntersectionObserver函数工具,提供以下用法


    import IntersectionObserver from 'utils/observer/observer';

    const ob = new IntersectionObserver({
    selector: '.goods-item', // 指定监听的目标节点元素
    observeAll: true, // 是否同时观测多个目标节点
    context: this, // 小程序 this 对象实例
    delay: 200, // 调用 onFinal 方法的间隔时间,默认 200ms
    onEach: ({ dataset }) => {
    // 每一次触发监听调用时,触发 onEach 方法,可以对数据进行一些过滤处理
    const { key } = dataset || {};
    return key;
    },
    onFinal: (data) => {
    // 在触发监听调用一段时间 delay 后,会调用一次 onFinal 方法,可以进行埋点上报
    if (!data) return;
    console.log('module view data', data);
    },
    });

    // 内置函数方法,如下:
    ob.connect(); // 开始监听
    ob.disconnect(); // 停止监听
    ob.reconnect(); // 重置监听

    然后在我们的FreeImage图片组件,添加可视区域加载图片的功能,以下是部分代码


    import IntersectionObserver from 'utils/observer';

    Component({
    properties: {
    src: String,
    /**
    * 是否开启可视区域加载图片
    */
    observer: {
    type: Boolean,
    value: false,
    },
    ....
    },

    data: {
    isObserver: false,
    ...
    },

    lifetimes: {
    attached() {
    // 开启可视区域加载图片
    if (this.data.observer) {
    this.createObserver();
    }
    },
    },
    methods: {
    ...

    /**
    * 监听图片是否进入可视区域
    */
    createObserver() {
    const ob = new IntersectionObserver({
    selector: '.free-image',
    observeAll: true,
    context: this,
    onFinal: (data = []) => {
    data.forEach((item: any) => {
    this.setData({
    isObserver: true,
    });
    ob.disconnect(); // 取消监听
    });
    },
    });

    ob.connect(); // 开始监听
    }
    }
    })

    <free-image observer src="{{ src }}" />

    优化效果


    测试我们小程序首页列表,使用图片懒加载的效果


    27a0b7a88a6e18665fa1ff33b3726b68.gif


    通过使用图片懒加载的功能,减少图片数量的加载,有效提高页面加载性能。在上述我们已经对图片体积进行优化过,所以在我们小程序中,只有在网络情况较差的情况下,才会自动开启图片懒加载功能。


    优化请求数


    我们项目中有很多本地图片资源,比如一些 icon 图标、标签类切图、背景图、图片按钮等。而小程序分包大小是有限制:整个小程序所有分包大小不超过 20M,而单个分包/主包大小不能超过 2M。所以为了减轻小程序体积,本地图片资源需要进行调整,比如图片压缩、上传到 CDN 服务器。这样能减少了小程序主包大小,而大部分图片都在腾讯云 CDN 服务器中,虽然可以加速资源的请求速度,当页面打开需要同时下载大量的图片的话,就会严重影响了用户的使用体验。


    针对此问题,需要找到权衡点来实现来优化请求数,首先我们把图片资源进行分类,以及使用场景,最后确定我们方案如下:



    • 较大体积的图片,选择上传到 CDN 服务器

    • 单色图标使用 iconfont 字体图标,多彩图标则使用svg格式

    • 标签类的图片,则生成雪碧图之后上传到 CDN 服务器

    • 图片体积小于10KB,结合使用场景,则考虑base64 ,比如一张图片体积为3KB的背景图,由于小程序css background不支持本地图片引入,可以使用 base64 方式实现


    其他策略


    大图检测

    实现大图检测机制,及时发现图片不符合规范的问题,当发现图片尺寸太大,不符合商品图尺寸标准时会进行上报。在小程序开发版/体验版中,当我们设置开启Debug模式,图片组件FreeImage会自动检测到大图片时,显示当前图片尺寸、以及设置图片高亮/翻转的方式提醒运营同学和设计同学进行处理



    加载失败处理

    使用腾讯云图片处理功能,URL预处理转换后得新 URL,可能会存在少量图片不存在的异常场景导致加载失败。遇到图片加载失败时,我们还是需要重新加载原始图片 URL, 之后会将错误图片 URL 上报到监控平台,方便之后调整 URL 预处理转换规则,同时也发现一部分错误的图片 URL 推动业务修改。


    这是我们图片组件FreeImage 处理图片加载失败,以下是部分代码


    onError(event: WechatMiniprogram.TouchEvent) {
    const { src, useCosImage } = this.data;

    this.setData({
    loading: false,
    error: true,
    lazy: 'error',
    });

    // 判断是否腾讯云服务的图片
    if (useCosImage) {
    wx.nextTick(() => {
    // 重新加载原生图片
    this.setData({
    formattedSrc: src, // src 是原图地址
    });
    });
    }

    // 上报图片加载失败
    app.aegis.report(AegisEnum.IMAGE_LOAD_FAIL, {
    src,
    errMsg: event?.detail.errMsg,
    });

    this.triggerEvent('error', event.detail);
    }

    图片请求数检查

    使用小程序开发者工具的体验评分功能,体验评分是一项给小程序的体验好坏打分的功能,它会在小程序运行过程中实时检查,分析出一些可能导致体验不好的地方,并且定位出哪里有问题,以及给出一些优化建议。


    image-20211024170719264.png


    通过体验评分的结果,可以分析我们存在短时间内发起太多的图片请求,以及存在图片太大而有效显示区域较小。所以根据分析的结果,开发需要合理控制数量,可考虑使用雪碧图技术、拆分域名或在屏幕外的图片使用懒加载等。


    上传压缩

    图片在上传前在保持可接受的清晰度范围内同时减少文件大小,进行合理压缩。现如今有很多不错的图片压缩插件工具,就不在详情介绍了。


    推荐一个比较优秀的图片压缩网站:TinyPNG使用智能有损压缩技术将您的 WebP, PNG and JPEG 图片的文件大小降低


    作者:稻草叔叔
    链接:https://juejin.cn/post/7031851192481218574

    收起阅读 »

    代码写得好,Reduce 方法少不了,我用这10例子来加深学习!

    数组中的 reduce 犹如一只魔法棒,通过它可以做一些黑科技一样的事情。语法如下: reduce(callback(accumulator, currentValue[, index, array])[,initialValue]) reduce 接受两个参...
    继续阅读 »

    数组中的 reduce 犹如一只魔法棒,通过它可以做一些黑科技一样的事情。语法如下:


    reduce(callback(accumulator, currentValue[, index, array])[,initialValue])

    reduce 接受两个参数,回调函数和初识值,初始值是可选的。回调函数接受4个参数:积累值、当前值、当前下标、当前数组。


    如果 reduce的参数只有一个,那么积累值一开始是数组中第一个值,如果reduce的参数有两个,那么积累值一开始是出入的 initialValue 初始值。然后在每一次迭代时,返回的值作为下一次迭代的 accumulator 积累值。


    今天的这些例子的大多数可能不是问题的理想解决方案,主要的目的是想说介绍如何使用reduce来解决问题。


    求和和乘法


    // 求和
    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a + i);
    // 30

    // 有初始化值
    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a + i, 5 );
    // 35

    // 如果看不懂第一个的代码,那么下面的代码与它等价
    [3, 5, 4, 3, 6, 2, 3, 4].reduce(function(a, i){return (a + i)}, 0 );

    // 乘法
    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a * i);

    查找数组中的最大值


    如果要使用 reduce 查找数组中的最大值,可以这么做:


    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => Math.max(a, i), -Infinity);

    上面,在每一次迭代中,我们返回累加器和当前项之间的最大值,最后我们得到整个数组的最大值。


    如果你真想在数组中找到最大值,不要有上面这个,用下面这个更简洁:


    Math.max(...[3, 5, 4, 3, 6, 2, 3, 4]);

    连接不均匀数组


    let data = [
    ["The","red", "horse"],
    ["Plane","over","the","ocean"],
    ["Chocolate","ice","cream","is","awesome"],
    ["this","is","a","long","sentence"]
    ]
    let dataConcat = data.map(item=>item.reduce((a,i)=>`${a} ${i}`))

    // 结果
    ['The red horse',
    'Plane over the ocean',
    'Chocolate ice cream is awesome',
    'this is a long sentence']

    在这里我们使用 map 来遍历数组中的每一项,我们对所有的数组进行还原,并将数组还原成一个字符串。


    移除数组中的重复项


    let dupes = [1,2,3,'a','a','f',3,4,2,'d','d']
    let withOutDupes = dupes.reduce((noDupes, curVal) => {
    if (noDupes.indexOf(curVal) === -1) { noDupes.push(curVal) }
    return noDupes
    }, [])

    检查当前值是否在累加器数组上存在,如果没有则返回-1,然后添加它。


    当然可以用 Set 的方式来快速删除重复值,有兴趣的可以自己去谷歌一下。


    验证括号


    [..."(())()(()())"].reduce((a,i)=> i==='('?a+1:a-1,0);
    // 0

    [..."((())()(()())"].reduce((a,i)=> i==='('?a+1:a-1,0);
    // 1

    [..."(())()(()()))"].reduce((a,i)=> i==='('?a+1:a-1,0);
    // -1

    这是一个很酷的项目,之前在力扣中有刷到。


    按属性分组


    let obj = [
    {name: 'Alice', job: 'Data Analyst', country: 'AU'},
    {name: 'Bob', job: 'Pilot', country: 'US'},
    {name: 'Lewis', job: 'Pilot', country: 'US'},
    {name: 'Karen', job: 'Software Eng', country: 'CA'},
    {name: 'Jona', job: 'Painter', country: 'CA'},
    {name: 'Jeremy', job: 'Artist', country: 'SP'},
    ]
    let ppl = obj.reduce((group, curP) => {
    let newkey = curP['country']
    if(!group[newkey]){
    group[newkey]=[]
    }
    group[newkey].push(curP)
    return group
    }, [])

    这里,我们根据 country 对第一个对象数组进行分组,在每次迭代中,我们检查键是否存在,如果不存在,我们创建一个数组,然后将当前的对象添加到该数组中,并返回组数组。


    你可以用它做一个函数,用一个指定的键来分组对象。


    扁平数组


    let flattened = [[3, 4, 5], [2, 5, 3], [4, 5, 6]].reduce(
    (singleArr, nextArray) => singleArr.concat(nextArray), [])

    // 结果:[3, 4, 5, 2, 5, 3, 4, 5, 6]

    这只是一层,如果有多层,可以用递归函数来解决,但我不太喜欢在 JS 上做递归的东西😂。


    一个预定的方法是使用.flat方法,它将做同样的事情


    [ [3, 4, 5],
    [2, 5, 3],
    [4, 5, 6]
    ].flat();

    只有幂的正数


    [-3, 4, 7, 2, 4].reduce((acc, cur) => {
    if (cur> 0) {
    let R = cur**2;
    acc.push(R);
    }
    return acc;
    }, []);

    // 结果
    [16, 49, 4, 144]

    反转字符串


    const reverseStr = str=>[...str].reduce((a,v)=>v+a)

    这个方法适用于任何对象,不仅适用于字符串。调用reverseStr("Hola"),输出的结果是aloH


    二进制转十进制


    const bin2dec = str=>[...String(str)].reduce((acc,cur)=>+cur+acc*2,0)

    // 等价于

    const bin2dec = (str) => {
    return [...String(str)].reduce((acc,cur)=>{
    return +cur+acc*2
    },0)
    }

    为了说明这一点,让我们看一个例子:(10111)->1+(1+(1+(0+(1+0*2)*2)*2)*2)*2


    ~完,我是刷碗智,励志等退休后,要回家摆地摊的人,我们下期见!




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


    作者:前端小智
    链接:https://juejin.cn/post/7032061650479874061

    收起阅读 »

    协程调度器详解

    协程和线程的差异目的差异线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)调度差异线程的调度是系统完成的...
    继续阅读 »

    协程和线程的差异

    目的差异

    • 线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.
    • 协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)

    调度差异

    • 线程的调度是系统完成的,一般是抢占式的,根据优先级来分配
    • 协程的调度是开发者根据程序逻辑指定好的,在不同的时期把资源合理的分配给不同的任务.

    协程与线程的关系

    协程并不是取代线程,而且抽象于线程之上,线程是被分割的CPU资源,协程是组织好的代码流程,协程需要线程来承载运行,线程是协程的资源

    协程的核心竞争力

    简化异步并发任务。

    协程上下文 CoroutineContext

    • 协程总是运行在一些以 CoroutineContext 类型为代表的上下文中 ,协程上下文是各种不同元素的集合
    • 集合内部的元素Element是根据key去对应(Map特点),但是不允许重复(Set特点)
    • Element之间可以通过+号进行组合
    • Element有如下四类,共同组成了CoroutineContext
      • Job:协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled)
      • CoroutineDispatcher:指定协程运行的线程(IO、Default、Main、Unconfined)
      • CoroutineName: 指定协程的名称,默认为coroutine
      • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常

    它们的关系如图所示:

    CoroutineDispatcher 作用

    • 用于指定协程的运行线程
    • kotlin已经内置了CoroutineDispatcher的4个实现,分别为 Dispatchers的Default、IO、Main、Unconfined字段

    public actual object Dispatchers {

    @JvmStatic
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()

    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO

    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

    @JvmStatic
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    }

    Dispatchers.Default

    Default根据useCoroutinesScheduler属性(默认为true) 去获取对应的线程池

    • DefaultScheduler :Kotlin内部自己实现的线程池逻辑
    • CommonPool:Java类库中的Executor实现的线程池逻辑
    internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
    if (useCoroutinesScheduler) DefaultScheduler else CommonPool
    internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    .....
    }

    open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
    ) : ExecutorCoroutineDispatcher() {
    constructor(
    corePoolSize: Int = CORE_POOL_SIZE,
    maxPoolSize: Int = MAX_POOL_SIZE,
    schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    ......
    }
    //java类库中的Executor实现线程池逻辑
    internal object CommonPool : ExecutorCoroutineDispatcher() {}

    如果想使用java类库中的线程池该如何使用呢?也就是修改useCoroutinesScheduler属性为false

    internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler"

    internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_NAME).let { value ->
    when (value) {
    null, "", "on" -> true
    "off" -> false
    else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'")
    }
    }

    internal actual fun systemProp(
    propertyName: String
    ): String? =
    try {
    //获取系统属性
    System.getProperty(propertyName)
    } catch (e: SecurityException) {
    null
    }

    从源码中可以看到,使用过获取系统属性拿到的值, 那我们就可以通过修改系统属性 去改变useCoroutinesScheduler的值, 具体修改方法为

     val properties = Properties()
    properties["kotlinx.coroutines.scheduler"] = "off"
    System.setProperties(properties)

    DefaultScheduler的主要实现都在其父类 ExperimentalCoroutineDispatcher 中

    open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
    ) : ExecutorCoroutineDispatcher() {
    public constructor(
    corePoolSize: Int = CORE_POOL_SIZE,
    maxPoolSize: Int = MAX_POOL_SIZE,
    schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    constructor(
    corePoolSize: Int = CORE_POOL_SIZE,
    maxPoolSize: Int = MAX_POOL_SIZE
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS)

    override val executor: Executor
    get() = coroutineScheduler

    private var coroutineScheduler = createScheduler()

    //创建CoroutineScheduler实例
    private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

    override val executor: Executorget() = coroutineScheduler

    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
    try {
    //dispatch方法委托到CoroutineScheduler的dispatch方法
    coroutineScheduler.dispatch(block)
    } catch (e: RejectedExecutionException) {
    ....
    }

    override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit =
    try {
    //dispatchYield方法委托到CoroutineScheduler的dispatchYield方法
    coroutineScheduler.dispatch(block, tailDispatch = true)
    } catch (e: RejectedExecutionException) {
    ...
    }

    internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
    try {
    //dispatchWithContext方法委托到CoroutineScheduler的dispatchWithContext方法
    coroutineScheduler.dispatch(block, context, tailDispatch)
    } catch (e: RejectedExecutionException) {
    ....
    }
    }
    override fun close(): Unit = coroutineScheduler.close()
    //实现请求阻塞
    public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
    require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
    return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING)
    }
    //实现并发数量限制
    public fun limited(parallelism: Int): CoroutineDispatcher {
    require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
    require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
    return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING)
    }

    ....
    }

    实现请求数量限制是调用 LimitingDispatcher 类,其类实现为

    private class LimitingDispatcher(
    private val dispatcher: ExperimentalCoroutineDispatcher,
    private val parallelism: Int,
    private val name: String?,
    override val taskMode: Int
    ) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
    //同步阻塞队列
    private val queue = ConcurrentLinkedQueue<Runnable>()
    //cas计数
    private val inFlightTasks = atomic(0)

    override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)

    private fun dispatch(block: Runnable, tailDispatch: Boolean) {
    var taskToSchedule = block
    while (true) {

    if (inFlight <= parallelism) {
    //LimitingDispatcher的dispatch方法委托给了DefaultScheduler的dispatchWithContext方法
    dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
    return
    }
    ......
    }
    }
    }

    Dispatchers.IO

    先看下 Dispatchers.IO 的定义

        /**
    *This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using
    * `withContext(Dispatchers.IO) { ... }` does not lead to an actual switching to another thread &mdash;
    * typically execution continues in the same thread.
    */

    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO


    Internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)))

    ......

    }

    IO在DefaultScheduler中的实现 是调用blacking()方法,而blacking()方法最终实现是LimitingDispatcher类, 所以 从源码可以看出 Dispatchers.Default和IO 是在同一个线程中运行的,也就是共用相同的线程池。

    而Default和IO 都是共享CoroutineScheduler线程池 ,kotlin内部实现了一套线程池两种调度策略,主要是通过dispatch方法中的Mode区分的

    TypeMode
    DefaultNON_BLOCKING
    IOPROBABLY_BLOCKING
    internal enum class TaskMode {

    //执行CPU密集型任务
    NON_BLOCKING,

    //执行IO密集型任务
    PROBABLY_BLOCKING,
    }
    fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
    ......
    if (task.mode == TaskMode.NON_BLOCKING) {
    signalCpuWork() //Dispatchers.Default
    } else {
    signalBlockingWork() // Dispatchers.IO
    }
    }

    Type处理策略适合场景特点
    Default1、CoroutineScheduler最多有corePoolSize个线程被创建; 2、corePoolSize它的取值为max(2, CPU核心数),即它会尽量的等于CPU核心数复杂计算、视频解码等1、CPU密集型任务特点会消耗大量的CPU资源。2、因为线程本身也有栈等空间,同时线程过多,频繁的线程切换带来的消耗也会影响线程池的性能4.对于CPU密集型任务,线程池并发线程数等于CPU核心数才能让CPU的执行效率最大化
    IO1、创建线程数不能大于maxPoolSize ,公式:max(corePoolSize, min(CPU核心数 * 128, 2^21 - 2))。网络请求、IO操作等1、IO密集型 执行任务时CPU会处于闲置状态,任务不会消耗大量的CPU资源。 2.线程执行IO密集型任务时大多数处于阻塞状态,处于阻塞状态的线程是不占用CPU的执行时间。3.Dispatchers.IO构造时通过LimitingDispatcher默认限制了最大线程并发数parallelism为max(64, CPU核心数),剩余的任务被放进队列中等待。

    Dispatchers.Unconfined

    任务执行在默认的启动线程。之后由调用resume的线程决定恢复协程的线程

    internal object Unconfined : CoroutineDispatcher() {
    //为false为不需要dispatch
    override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

    override fun dispatch(context: CoroutineContext, block: Runnable) {
    // 只有当调用yield方法时,Unconfined的dispatch方法才会被调用
    // yield() 表示当前协程让出自己所在的线程给其他协程运行
    val yieldContext = context[YieldContext]
    if (yieldContext != null) {
    yieldContext.dispatcherWasUnconfined = true
    return
    }
    throw UnsupportedOperationException("Dispatchers.Unconfined.dispatch function can only be used by the yield function. " +
    "If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
    "isDispatchNeeded and dispatch calls.")
    }
    }

    每一个协程都有对应的Continuation实例,其中的resumeWith用于协程的恢复,存在于DispatchedContinuation

    public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    ......

    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

    ......

    }

    重点看resumeWith的实现以及类委托

    internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>//协程suspend挂起方法产生的Continuation
    ) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
    .....
    override fun resumeWith(result: Result<T>) {
    val context = continuation.context
    val state = result.toState()
    if (dispatcher.isDispatchNeeded(context)) {
    _state = state
    resumeMode = MODE_ATOMIC
    dispatcher.dispatch(context, this)
    } else {
    executeUnconfined(state, MODE_ATOMIC) {
    withCoroutineContext(this.context, countOrElement) {
    continuation.resumeWith(result)
    }
    }
    }
    }
    ....
    }

    通过isDispatchNeeded(是否需要dispatch,Unconfined=false,default,IO=true)判断做不同处理

    • true:调用协程的CoroutineDispatcher的dispatch方法
    • false:调用executeUnconfined方法
    private inline fun DispatchedContinuation<*>.executeUnconfined(
    contState: Any?, mode: Int, doYield: Boolean = false,
    block: () -> Unit
    ): Boolean {
    assert { mode != MODE_UNINITIALIZED }
    val eventLoop = ThreadLocalEventLoop.eventLoop
    if (doYield && eventLoop.isUnconfinedQueueEmpty) return false
    return if (eventLoop.isUnconfinedLoopActive) {
    _state = contState
    resumeMode = mode
    eventLoop.dispatchUnconfined(this)
    true
    } else {
    runUnconfinedEventLoop(eventLoop, block = block)
    false
    }
    }

    从threadlocal中取出eventLoop(eventLoop和当前线程相关的),判断是否在执行Unconfined任务

    1. 如果在执行则调用EventLoop的dispatchUnconfined方法把Unconfined任务放进EventLoop中
    2. 如果没有在执行则直接执行
    internal inline fun DispatchedTask<*>.runUnconfinedEventLoop(
    eventLoop: EventLoop,
    block: () -> Unit
    ) {
    eventLoop.incrementUseCount(unconfined = true)
    try {
    block()
    while (true) {
    if (!eventLoop.processUnconfinedEvent()) break
    }
    } catch (e: Throwable) {
    handleFatalException(e, null)
    } finally {
    eventLoop.decrementUseCount(unconfined = true)
    }
    }

    1. 执行block()代码块,即上文提到的resumeWith()
    2. 调用processUnconfinedEvent()方法实现执行剩余的Unconfined任务,知道全部执行完毕跳出循环

    EventLoop是存放与threadlocal,所以是跟当前线程相关联的,而EventLoop也是CoroutineDispatcher的一个子类

    internal abstract class EventLoop : CoroutineDispatcher() {
    .....
    //双端队列实现存放Unconfined任务
    private var unconfinedQueue: ArrayQueue<DispatchedTask<*>>? = null
    //从队列的头部移出Unconfined任务执行
    public fun processUnconfinedEvent(): Boolean {
    val queue = unconfinedQueue ?: return false
    val task = queue.removeFirstOrNull() ?: return false
    task.run()
    return true
    }
    //把Unconfined任务放进队列的尾部
    public fun dispatchUnconfined(task: DispatchedTask<*>) {
    val queue = unconfinedQueue ?:
    ArrayQueue<DispatchedTask<*>>().also { unconfinedQueue = it }
    queue.addLast(task)
    }
    .....
    }

    内部通过双端队列实现存放Unconfined任务

    1. EventLoop的dispatchUnconfined方法用于把Unconfined任务放进队列的尾部
    2. rocessUnconfinedEvent方法用于从队列的头部移出Unconfined任务执行

    Dispatchers.Main

    kotlin在JVM上的实现 Android就需要引入kotlinx-coroutines-android库,它里面有Android对应的Dispatchers.Main实现,

       public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
    return try {
    val factories = if (FAST_SERVICE_LOADER_ENABLED) {
    FastServiceLoader.loadMainDispatcherFactory()
    } else {
    // We are explicitly using the
    // `ServiceLoader.load(MyClass::class.java, MyClass::class.java.classLoader).iterator()`
    // form of the ServiceLoader call to enable R8 optimization when compiled on Android.
    ServiceLoader.load(
    MainDispatcherFactory::class.java,
    MainDispatcherFactory::class.java.classLoader
    ).iterator().asSequence().toList()
    }
    factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
    ?: MissingMainCoroutineDispatcher(null)
    } catch (e: Throwable) {
    // Service loader can throw an exception as well
    MissingMainCoroutineDispatcher(e)
    }
    }

    internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
    val clz = MainDispatcherFactory::class.java
    if (!ANDROID_DETECTED) {
    return load(clz, clz.classLoader)
    }

    return try {
    val result = ArrayList<MainDispatcherFactory>(2)
    createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
    createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
    result
    } catch (e: Throwable) {
    // Fallback to the regular SL in case of any unexpected exception
    load(clz, clz.classLoader)
    }
    }

    通过反射获取AndroidDispatcherFactory 然后根据加载的优先级 去创建Dispatcher

    internal class AndroidDispatcherFactory : MainDispatcherFactory {

    override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
    HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")

    override fun hintOnError(): String? = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"

    override val loadPriority: Int
    get() = Int.MAX_VALUE / 2
    }
    internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
    ) : HandlerDispatcher(), Delay {

    public constructor(
    handler: Handler,
    name: String? = null
    ) : this(handler, name, false)

    ......

    override fun dispatch(context: CoroutineContext, block: Runnable) {
    handler.post(block)
    }

    ......
    }

    而createDispatcher调用HandlerContext 类 通过调用Looper.getMainLooper()获取handler ,最终通过handler来实现在主线程中运行

    Dispatchers.Main 其实就是把任务通过Handler运行在Android的主线程

    收起阅读 »

    Android C++系列:Linux文件IO操作

    1.1 read/writeread函数从打开的设备或文件中读取数据。#include ssize_t read(int fd, void *buf, size_t count); //返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调r...
    继续阅读 »

    1.1 read/write

    read函数从打开的设备或文件中读取数据。

    #include 
    ssize_t read(int fd, void *buf, size_t count);
    //返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0

    参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读 写位置向后移。

    注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写 位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一 个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是 1。

    注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表 示到达文件末尾)也可以返回负值-1(表示出错)。

    read函数返回时,返回值说明了buf中 前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:

    • 读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个 字节而请求读100个字节,则read返回30,下次read将返回0。

    • 从终端设备读,通常以行为单位,读到换行符就返回了。

    • 从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数,后面socket编程部分会详细讲解。

    write函数向打开的设备或文件中写数据。

    #include 
    ssize_t write(int fd, const void *buf, size_t count);
    返回值:成功返回写入的字节数,出错返回-1并设置errno

    写常规文件时,write的返回值通常等于请求写的字节数count,而向终端设备或网络写则不一定。

    1.2 阻塞和非阻塞

    读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

    现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:

    • 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
    • 就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另 一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进 程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程 的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时 要兼顾用户体验,不能让和用户交互的进程响应太慢。

    下面这个小程序从终端读数据再写回终端。

    1.2.1 阻塞读终端

    #include  #include 
    int main(void) {
    char buf[10];
    int n;
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
    perror("read STDIN_FILENO");
    exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    return 0;
    }

    执行结果如下:

    $ ./a.out hello(回车)
    hello
    $ ./a.out
    hello world(回车) hello worl$ d
    bash: d: command not found

    第一次执行a.out的结果很正常,而第二次执行的过程有点特殊,现在分析一下:

    Shell进程创建a.out进程,a.out进程开始执行,而shell进程睡眠等待a.out进程退出。

    a.out调用read时睡眠等待,直到终端设备输入了换行符才从read返回,read只读走10 个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。

    a.out进程打印并退出,这时shell进程恢复运行,Shell继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执 行,结果发现执行不了,没有d这个命令。

    如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例, 如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没 有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询 (Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:

    while(1) { 
    非阻塞read(设备1);
    if(设备1有数据到达)
    处理数据;
    非阻塞read(设备2);
    if(设备2有数据到达)
    处理数据; ...
    }

    如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read 调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处 理。

    非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟 等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行。

    while(1) { 
    非阻塞read(设备1);
    if(设备1有数据到达)
    处理数据;
    非阻塞read(设备2);
    if(设备2有数据到达)
    处理数据;
    ...
    sleep(n);
    }

    这样做的问题是,设备1有数据到达时可能不能及时处理,最长需延迟n秒才能处理,而且反复查询还是做了很多无用功。以后要学习的select(2)函数可以阻塞地同时监视多个设 备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。

    以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。程序开始执行时在0、1、2文件描述符上自动打开的文件就是终 端,但是没有O_NONBLOCK标志。所以就像例 28.2 “阻塞读终端”一样,读标准输入是阻塞 的。我们可以重新打开一遍设备文件/dev/tty(表示当前终端),在打开时指定O_NONBLOCK 标志。

    1.2.2 非阻塞读终端

    #include  
    #include
    #include
    #include
    #include
    #define MSG_TRY "try again\n"
    int main(void) {
    char buf[10];
    int fd, n;
    fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
    if(fd<0) {
    perror("open /dev/tty");
    exit(1);
    }
    tryagain:
    n = read(fd, buf, 10);
    if (n < 0) {
    if (errno == EAGAIN) {
    sleep(1);
    write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    goto tryagain;
    }
    perror("read /dev/tty");
    exit(1);
    }
    write(STDOUT_FILENO, buf, n); close(fd);
    return 0;
    }

    以下是用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。

    1.2.3 非阻塞读终端和等待超时

    #include  
    #include
    #include
    #include
    #include
    #define MSG_TRY "try again\n"
    #define MSG_TIMEOUT "timeout\n"
    int main(void) {
    char buf[10];
    int fd, n, i;
    fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
    if(fd<0) {
    perror("open /dev/tty");
    exit(1);
    }
    for(i=0; i<5; i++) {
    n = read(fd, buf, 10);
    if(n>=0)
    break;
    if(errno!=EAGAIN) {
    perror("read /dev/tty");
    exit(1);
    }
    sleep(1);
    write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    }
    if(i==5)
    write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    else
    write(STDOUT_FILENO, buf, n);
    close(fd);
    return 0;
    }

    1.3 lseek

    每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方 式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek 和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

     #include 
    #include
    off_t lseek(int fd, off_t offset, int whence);

    参数offset和whence的含义和fseek函数完全相同。只不过第一个参数换成了文件描述符。和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长 文件,中间空洞的部分读出来都是0。

    若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏 移量:

    off_t currpos;
    currpos = lseek(fd, 0, SEEK_CUR);

    这种方法也可用来确定文件或设备是否可以设置偏移量,常规文件都可以设置偏移量, 而设备一般是不可以设置偏移量的。如果设备不支持lseek,则lseek返回-1,并将errno 设置为ESPIPE。注意fseek和lseek在返回值上有细微的差别,fseek成功时返回0失败时返 回-1,要返回当前偏移量需调用ftell,而lseek成功时返回当前偏移量失败时返回-1。

    1.4 fcntl

    先前我们以read终端设备为例介绍了非阻塞I/O,为什么我们不直接对STDIN_FILENO做 非阻塞read,而要重新open一遍/dev/tty呢?因为STDIN_FILENO在程序启动时已经被自动 打开了,而我们需要在调用open时指定O_NONBLOCK标志。这里介绍另外一种办法,可以用 fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这 些标志称为File Status Flag),而不必重新open文件。

    #include  #include 
    int fcntl(int fd, int cmd);
    int fcntl(int fd, int cmd, long arg);
    int fcntl(int fd, int cmd, struct flock *lock);

    这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的 cmd参数。下面的例子使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性,加 上O_NONBLOCK选项,实现和例 28.3 “非阻塞读终端”同样的功能。

    1.4.1 用fcntl改变File Status Flag

    #include  
    #include
    #include
    #include
    #include
    #define MSG_TRY "try again\n"
    int main(void) {
    char buf[10];
    int n;
    int flags;
    flags = fcntl(STDIN_FILENO, F_GETFL); flags |= O_NONBLOCK;
    if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
    perror("fcntl");
    exit(1);
    }
    tryagain:
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
    if (errno == EAGAIN) {
    sleep(1);
    write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    goto tryagain;
    }
    perror("read stdin");
    exit(1);
    }
    write(STDOUT_FILENO, buf, n);
    return 0;
    }

    1.5 ioctl

    ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是 不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是 in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数 据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通 过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。

    #include 
    int ioctl(int d, int request, ...);

    d是某个设备的文件描述符。request是ioctl的命令,可变参数取决于request,通常是 一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于 request。

    以下程序使用TIOCGWINSZ命令获得终端设备的窗口大小。

    #include  
    #include
    #include
    #include
    int main(void) {
    struct winsize size;
    if (isatty(STDOUT_FILENO) == 0)
    exit(1);
    if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) {
    perror("ioctl TIOCGWINSZ error");
    exit(1);
    }
    printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
    return 0;
    }

    在图形界面的终端里多次改变终端窗口的大小并运行该程序,观察结果。

    1.6 总结

    本文介绍了read/write的系统调用,以及阻塞、非阻塞相关的概念以及配置方式,等待超时方式。还介绍了lseek、fcntl、ioctl文件操作相关的系统调用。

    原文链接:https://juejin.cn/post/7031846767289499685?utm_source=gold_browser_extension

    收起阅读 »

    iOS 简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存

    iOS
    废话开篇:在日常开发中经常会有上传表单及图片到服务器场景,这里有两种实现方式:一、单独封装一个图片文件格式存储代码,服务器对 Response 返回值里面返回服务器图片路径,再通过其他接口绑定服务器图片路径;二、表单及图片文件直接提交。那么...
    继续阅读 »


    废话开篇:在日常开发中经常会有上传表单及图片到服务器场景,这里有两种实现方式:一、单独封装一个图片文件格式存储代码,服务器对 Response 返回值里面返回服务器图片路径,再通过其他接口绑定服务器图片路径;二、表单及图片文件直接提交。那么,其实说是两种方式,其实归根到底就是一种:数据传输与接收。那么,下面就在 OC 上简单模拟服务器如何解析客户端传来的表单数据及图片格式数据

    以前文章地址:

    # iOS 简单模拟 https 证书信任逻辑

    # iOS 基于 CocoaHTTPServer 搭建手机内部服务器,实现 http 及 https 访问、传输数据

    基于上述文章继续进行本次的 模拟服务器如何解析客户端传来的表单数据及图片格式数据

    效果如下:

    屏幕录制2021-11-18 下午4.17.38.gif

    前言说明:

    这里简单说一下 AFNetwork 下是如何同时进行数据参数提交及文件上传的。这里只是简单的说一下思路:

    先上一段简单的 AF 请求代码

        AFHTTPSessionManager * m = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://10.10.60.20"]];

    NSDictionary * dic = @{@"title":@"中国万岁",@"name":@"中国人"};

        [m POST:@"https://10.10.60.20:12345/doPost" parameters:dic headers:@{} constructingBodyWithBlock:^(**id**<AFMultipartFormData>  _Nonnull formData) {

            NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0]; // 获取当前时间0秒后的时间

            NSTimeInterval time = [date timeIntervalSince1970]*1000;// *1000 是精确到毫秒(13位),不乘就是精确到秒(10位)

            NSString *timeString = [NSString stringWithFormat:@"iOS%.0f", time];

            UIImage * image = [UIImage imageNamed:@"sea"];

            NSData *data = UIImageJPEGRepresentation(image, 0.5f);

            [formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

            } progress:^(NSProgress * _Nonnull uploadProgress) {          

            } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

            } failure:^(NSURLSessionDataTask * **_Nullable** task, NSError * _Nonnull error) {   

            }
         ];

    1、网络请求参数的传入

    这里代码无需过多解释,dic 就是要传输的请求参数,那么,在这个参数完成之后,其实 AFNetworking 就对参数进行了存储,并且在后面的图片上传的时候用拼接的 NSData 的方式进行数据拼接。

    2、图片数据获取及 NSData 拼接

    AF 调用下面的方法进行了请求数据的拼接。

    [formData appendPartWithFileData:data name:@"file" fileName:[NSString stringWithFormat:@"%@.jpg",timeString] mimeType:@"image/jpg"];

    3、基于第二步骤,创建多个数据读取对象,通过 Stream 进行 NSData 的依次读取,因为 AF 下的 POST 请求会跟一个 Stream 进行绑定

    [self.request setHTTPBodyStream:self.bodyStream];

    那么,在开启的发送请求前,AF 又重写了 Stream 下

    - (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)length

    方法。进而可以在 Stream 读取的过程中对多个文件 data 进行拼接,最终将整个数据进行一次传输。

    4、注意事项:(1)AF 会在 header 里面进行数据总长度的标定,这样服务器在最先拿到 header 时便可以知晓此次传输的数据总长度。(2)AF 会随机生成一个 boundary 也放到 header 里面,这个参数的目的就是将请求中不通的 参数文件进行边界划分,这样,服务器在解析的时候就知道了哪些 data 是一个完整的数据。当然,AF 也会标定一下传输类型在 header 里,比如:Content-Type

    好了,上述其实只是一个铺垫,来看一下最终如何总 data 里解析出请求参数及图片文件

    步骤一、基于 CocoaHTTPServer 搭建完的本地 OC 服务器进行数据解析

    对于如何搭建的请参考上面的文章链接

    这里要处理的就是下面的这个方法,客户端传过来的数据都会在这个方法里执行,因为一个系统的 Stream 一次性读取最大数是有限制的,所以,对于大文件上传的过程,此方法会走多次。

    - (void)processBodyData:(NSData *)postDataChunk;

    思路:因为服务器收到所有的 data 里完整的参数数据都是用换行符来分割的,那么通过对 "\r\n" 换行符进行切割,那么,两个换行符之间的数据就是一个完整的参数。

    - (void)parseData:(NSData *)postDataChunk
    {
    //这里记录图片文件 data 在数据接收总 data 里的初始位置索引
        int fileDataStartIndex = 0;
        //换行符\r\n
        UInt16 separatorBytes = 0X0A0D;
        NSData * separatorData = [NSData dataWithBytes:&separatorBytes length:2];
        int l = (int)[separatorData length];
    //遍历接收的数据,找到所有以 0A0D 分割的完整 data 数据
        for (int i = 0; i < [postDataChunk length] - l; i++) {
    //以换行符长度为单位依次排查、寻找
            NSRange searchRange = {i,l};
            //是换行符
            if ([[postDataChunk subdataWithRange:searchRange] isEqualToData:separatorData]) {
                
                //获取换行符之间的data的位置
                NSRange newDataRange = {self.dataStartIndex,i - self.dataStartIndex};
                self.dataStartIndex = i + l;
    //这里先进性请求参数的筛选,文件data保存位置偏后,那么,一开始就需要 self.paramReceiveComplete 标识来标定是否排查到文件 data 了
                if (self.paramReceiveComplete) {
                    fileDataStartIndex = i + l;
                    continue;
                }

                //跳过换行符
                i += (l-1);
    //获取换行符之间的完整数据格式
                NSData * newData = [postDataChunk subdataWithRange:newDataRange];
    //判断是否为空
                if ([newData length]) {
    //获取文本信息
                    NSString *content = [[NSString alloc] initWithData:newData encoding:NSUTF8StringEncoding];
    //替换所有的换行特殊字符
                    content = [content stringByReplacingOccurrencesOfString:@"\r\n" withString:@""];
    //这里注意的是边界信息 Boundary ,也就是 AF 给钉里面的数据不解析
                    if (content.length && ![content containsString:@"--Boundary"]) {
    //如果解析到文件,那么 content 里会包含 name="file" 的标识,用此标识进行数据格式的判断
                        if ([content containsString:@"name=\"file\""]){
    //读到文件了
                            self.currentParserType = @"file";
                        } else {
    //请求参数
                            self.currentParserType = @"text/plain";
                        }

                        //表单数据解析
                        if ([self.currentParserType containsString:@"text/plain"]){
    //content 里面包含 form-data,说明是数据参数说明,里面会包含 key 值
                            if ([content containsString:@"form-data"]) {
                                NSString * key = [content componentsSeparatedByString:@"name="].lastObject;
                                key = [key stringByReplacingOccurrencesOfString:@"\"" withString:@""];
    //这里临时保存了key值,在后面解析到 value 的时候进行数据绑定
                                self.currentParamKey = key;
                            } else {
    //解析到了 value 用 self.currentParamKey 进行绑定
                                if (self.currentParamKey && content) {
                                    [self.receiveParamDic setValue:content forKey:self.currentParamKey];
                                }
                            }
                        } else {
                            //开始文件处理,标定一下,因为由于文件大小的影响,此方法会走多次,那么,在一开始标定后,下一次再进来就直接进行文件数据的拼接
                            self.paramReceiveComplete = YES;
                        }
                    }
                }
            }
        }

    //文件的写入(其实这里不是很严谨,因为请求参数较小的原因,所以,即便是第一次执行此方法,里面也会有文件 data 开始读取的情况)
        NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
        NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
        [self.outputStream write:[fileData bytes] maxLength:fileData.length];

    }

    步骤二、数据写入沙盒

    声明一个 NSOutputStream 对象

    @property (nonatomic,strong) NSOutputStream * outputStream;

    CocoaHTTPServer -> HTTPConnection 类是不进行常规化的 init 的,所以,初始化 outputStream 这里用懒加载的形式。

    - (NSOutputStream *)outputStream

    {

        if (!_outputStream) {
            NSString * cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
            NSString * filePath = [cachePath stringByAppendingPathComponent:@"wsl.png"];
            NSLog(@"filePath = %@",filePath);
            _outputStream = [[NSOutputStream alloc] initToFileAtPath:filePath append:**YES**];
            [_outputStream open];
        }
        return _outputStream;
    }

    进行文件写入沙盒操作:

        NSRange fileDataRange = {fileDataStartIndex,postDataChunk.length - fileDataStartIndex};
        NSData * fileData = [postDataChunk subdataWithRange:fileDataRange];
        [self.outputStream write:[fileData bytes] maxLength:fileData.length];

    在处理完数据后关闭流

    - (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
    {
        [self.outputStream close];
        self.outputStream = nil;
    }

    步骤三、查看运行结果

    先看是否获取了请求的参数:

    image.png

    在看图片是否保存完成,通过打印模拟器的沙盒路径,直接 前往文件夹 即可找到沙盒文件

    image.png

    可以看到,这里保存图片也成功了。

    image.png

    这里说明一下:

    遵循 MultipartFormDataParserDelegate 协议也可以直接获取文件的 data ,直接去读,再去存即可。但是它没有暴露给外界数据请求的 key 而只有 value,但是如果仅作为文件的传输还是很方便的。

    如下:

    遵循代理协议

    image.png

    声明 MultipartFormDataParser 对象

    image.png

    MultipartFormDataParser 对象进行数据解析

    image.png

    进行文件数据解析代理执行

    image.png

    其实 CocoaHTTPServer 封装的解析工具类实现原理亦是如此。

    好了,简单模拟服务器如何解析客户端传来的表单数据及图片格式数据并本地保存 功能就实现完了,代码拙劣,大神勿笑。

    收起阅读 »

    Metal 框架之渲染管线渲染图元

    iOS
    「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」概述在 《 Metal 框架之使用 Metal 来绘制视图内容 》中,介绍了如何设置 MTKView 对象并使用渲染通道更改视图的内容,实现了将背景色渲染为视图的内容。本示...
    继续阅读 »


    「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

    概述

    在 《 Metal 框架之使用 Metal 来绘制视图内容 》中,介绍了如何设置 MTKView 对象并使用渲染通道更改视图的内容,实现了将背景色渲染为视图的内容。本示例将介绍如何配置渲染管道,作为渲染通道的一部分,在视图中绘制一个简单的 2D 彩色三角形。该示例为每个顶点提供位置和颜色,渲染管道使用该数据,在指定的顶点颜色之间插入颜色值来渲染三角形。

    在本示例中,将介绍如何编写顶点和片元函数、如何创建渲染管道状态对象,以及最后对绘图命令进行编码。

    triangle’s vertices.png

    理解 Metal 渲染管线

    渲染管线处理绘图命令并将数据写入渲染通道的目标中。一个完整地渲染管线有许多阶段组成,一些阶段需要使用着色器进行编程,而一些阶段则需要配置固定的功能件。本示例的管线主要包含三个阶段:顶点阶段、光栅化阶段和片元阶段。其中,顶点阶段和片元阶段是可编程的,这可以使用 Metal Shading Language (MSL) 来编写函数,而光栅化阶段则是不可编程的,直接使用固有功能件来配置。

    render piple.png

    渲染从绘图命令开始,其中包括顶点个数和要渲染的图元类型。如下是本例子的绘图命令:


    // Draw the triangle.

    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle

                      vertexStart:0

                      vertexCount:3];


    顶点阶段会处理每个顶点的数据。当顶点经过顶点阶段处理后,渲染管线会对图元光栅化处理,以此来确定渲染目标中的哪些像素位于图元的边界内(即图元可以转化成的像素)。片元阶段是要确定渲染目标的像素值。

    自定义渲染管线

    顶点函数为单个顶点生成数据,片元函数为单个片元生成数据,可以通过编写函数来指定它们的工作方式。我们可以依据希望管道完成什么功能以及如何完成来配置管道的各个阶段。

    决定将哪些数据传递到渲染管道以及将哪些数据传递到管道的后期阶段,通常可以在三个地方执行此操作:

    • 管道的输入,由 App 提供并传递到顶点阶段。

    • 顶点阶段的输出,它被传递到光栅化阶段。

    • 片元阶段的输入,由 App 提供或由光栅化阶段生成。

    在本示例中,管道的输入数据包括顶点的位置及其颜色。为了演示顶点函数中执行的转换类型,输入坐标在自定义坐标空间中定义,以距视图中心的像素为单位进行测量。这些坐标需要转换成 Metal 的坐标系。

    声明一个 AAPLVertex 结构,使用 SIMD 向量类型来保存位置和颜色数据。


    typedef struct

    {

        vector_float2 position;

        vector_float4 color;

    } AAPLVertex;


    SIMD 类型在 Metal Shading Language 中很常见,相应的需要在 App 中使用 simd 库。 SIMD 类型包含特定数据类型的多个通道,因此将位置声明为 vector_float2 意味着它包含两个 32 位浮点值(x 和 y 坐标)。颜色使用 vector_float4 存储,因此它们有四个通道:红色、绿色、蓝色和 alpha。

    在 App 中,输入数据使用常量数组指定:


    static const AAPLVertex triangleVertices[] =

    {

        // 2D positions,    RGBA colors

        { {  250,  -250 }, { 1, 0, 0, 1 } },

        { { -250,  -250 }, { 0, 1, 0, 1 } },

        { {    0,   250 }, { 0, 0, 1, 1 } },

    };


    顶点阶段为顶点生成数据,需要提供颜色和变换的位置。使用 SIMD 类型声明一个包含位置和颜色值的 RasterizerData 结构。


    struct RasterizerData

    {

        // The [[position]] attribute of this member indicates that this value

        // is the clip space position of the vertex when this structure is

        // returned from the vertex function.

        float4 position [[position]];



        // Since this member does not have a special attribute, the rasterizer

        // interpolates its value with the values of the other triangle vertices

        // and then passes the interpolated value to the fragment shader for each

        // fragment in the triangle.

        float4 color;

    };


    输出位置(在下面详细描述)必须定义为 vector_float4 类型。颜色在输入数据结构中声明。

    需要告诉 Metal 光栅化数据中的哪个字段提供位置数据,因为 Metal 不会对结构中的字段强制执行任何特定的命名约定。使用 [[position]] 属性限定符来标记位置字段,使用它来保存该字段输出位置。

    fragment 函数只是将光栅化阶段的数据传递给后面的阶段,因此它不需要任何额外的参数。

    定义顶点函数

    需要使用 vertex 关键字来定义顶点函数,包含入参和出参。


    vertex RasterizerData

    vertexShader(uint vertexID [[vertex_id]],

                 constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],

                 constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])


    第一个参数 vertexID 使用 [[vertex_id]] 属性限定符来修饰,它是 Metal 关键字。当执行渲染命令时,GPU 会多次调用顶点函数,为每个顶点生成一个唯一值。

    第二个参数 vertices 是一个包含顶点数据的数组,使用之前定义的 AAPLVertex 结构。

    要将位置转换为 Metal 的坐标,该函数需要绘制三角形的视口的大小(以像素为单位),因此需要将其存储在 viewportSizePointer 参数中。

    第二个和第三个参数使用 [[buffer(n)]] 属性限定符来修饰。默认情况下,Metal 自动为每个参数分配参数表中的插槽。当使用 [[buffer(n)]] 限定符修饰缓冲区参数时,明确地告诉 Metal 要使用哪个插槽。显式声明插槽可以方便的修改着色器代码,而无需更改 App 代码。

    编写顶点函数

     编写的顶点函数必须生成输出结构的两个字段,使用 vertexID 参数索引顶点数组并读取顶点的输入数据,还需要获取视口尺寸。


    float2 pixelSpacePosition = vertices[vertexID].position.xy;

    // Get the viewport size and cast to float.

    vector_float2 viewportSize = vector_float2(*viewportSizePointer);
    复制代码

    顶点函数必须提供裁剪空间坐标中的位置数据,这些位置数据是 3D 的点,使用四维齐次向量 (x,y,z,w) 来表示。光栅化阶段获取输出位置,并将 x、y 和 z 坐标除以 w 以生成归一化设备坐标中的 3D 点。归一化设备坐标与视口大小无关。

    NDC_ coordinates.png

    归一化设备坐标使用左手坐标系来映射视口中的位置。图元被裁剪到这个坐标系中的一个裁剪框上,然后被光栅化。剪切框的左下角位于 (-1.0,-1.0) 坐标处,右上角位于 (1.0,1.0) 处。正 z 值指向远离相机(指向屏幕)。z 坐标的可见部分在 0.0(近剪裁平面)和 1.0(远剪裁平面)之间。

    下图是将输入坐标系转换为归一化的设备坐标系。

    ndc转换.png

    因为这是一个二维应用,不需要齐次坐标,所以先给输出坐标写一个默认值,w值设置为1.0,其他坐标设置为0.0。这意味顶点函数在该坐标空间中生成的 (x,y) 已经在归一化设备坐标空间中了。将输入位置除以1/2视口大小就生成归一化的设备坐标。由于此计算是使用 SIMD 类型执行的,因此可以使用一行代码同时计算两个通道,执行除法并将结果放在输出位置的 x 和 y 通道中。


    out.position = vector_float4(0.0, 0.0, 0.0, 1.0);

    out.position.xy = pixelSpacePosition / (viewportSize / 2.0);


    最后,将颜色值赋给 out.color 作为返回值。


    out.color = vertices[vertexID].color;


    编写片元函数

    片元阶段对渲染目标可以做修改处理。光栅化器确定渲染目标的哪些像素被图元覆盖,仅处于三角形片元中的那些像素才会被渲染。

    光栅化阶段.png

    片元函数处理光栅化后的位置信息,并计算每个渲染目标的输出值。这些片元值由管道中的后续阶段处理,最终写入渲染目标。

    本示例中的片元着色器接收与顶点着色器的输出中声明的相同参数。使用 fragment 关键字声明片元函数。它只有一个输入参数,与顶点阶段提供的 RasterizerData 结构相同。添加 [[stage_in]] 属性限定符以指示此参数由光栅化器生成。


    fragment float4 fragmentShader(RasterizerData in [[stage_in]])


    如果片元函数写入多个渲染目标,则必须为每个渲染目标声明一个变量。由于此示例只有一个渲染目标,因此可以直接指定一个浮点向量作为函数的输出,此输出是要写入渲染目标的颜色。

    光栅化阶段计算每个片元参数的值并用它们调用片元函数。光栅化阶段将其颜色参数计算为三角形顶点处颜色的混合,片元离顶点越近,顶点对最终颜色的贡献就越大。

    颜色插值.png

    将内插颜色作为函数的输出返回。


    return in.color;


    创建渲染管线状态对象

    完成着色器函数编写后,需要创建一个渲染管道,通过 MTLLibrary 为每个着色器函数指定一个 MTLFunction 对象。


    id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];


    id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];

    id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];


    接下来,创建一个 MTLRenderPipelineState 对象,使用 MTLRenderPipelineDescriptor 来配置管线。


    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];

    pipelineStateDescriptor.label = @"Simple Pipeline";

    pipelineStateDescriptor.vertexFunction = vertexFunction;

    pipelineStateDescriptor.fragmentFunction = fragmentFunction;

    pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;



    _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor

                                                            
    error:&error];




    除了指定顶点和片元函数之外,还可以指定渲染目标的像素格式。像素格式 (MTLPixelFormat) 定义了像素数据的内存布局。对于简单格式,此定义包括每个像素的字节数、存储在像素中的数据通道数以及这些通道的位布局。渲染管线状态必须使用与渲染通道指定的像素格式兼容的像素格式才能够正确渲染,由于此示例只有一个渲染目标并且它由视图提供,因此将视图的像素格式复制到渲染管道描述符中。

    使用 Metal 创建渲染管道状态对象时,渲染管线需要转换片元函数的输出像素格式为渲染目标的像素格式。如果要针对不同的像素格式,则需要创建不同的管道状态对象,可以在不同像素格式的多个管道中使用相同的着色器。

    设置视口

    有了管道的渲染管道状态对象后,就可以使用渲染命令编码器来渲染三角形了。首先,需要设置视口来告诉 Metal 要绘制到渲染目标的哪个部分。


    // Set the region of the drawable to draw into.

    [renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, 0.0, 1.0 }];


    设置渲染管线状态

    为渲染管线指定渲染管线状态对象。


    [renderEncoder setRenderPipelineState:_pipelineState];


    将参数数据发送到顶点函数

    通常使用缓冲区 (MTLBuffer) 将数据传递给着色器。但是,当只需要向顶点函数传递少量数据时,可以将数据直接复制到命令缓冲区中。

    该示例将两个参数的数据复制到命令缓冲区中,顶点数据是从定义的数组复制而来的,视口数据是从设置视口的同一变量中复制的,片元函数仅使用从光栅化器接收的数据,因此没有传递参数。


    [renderEncoder setVertexBytes:triangleVertices

                           length:sizeof(triangleVertices)

                          atIndex:AAPLVertexInputIndexVertices];



    [renderEncoder setVertexBytes:&_viewportSize

                           length:sizeof(_viewportSize)

                          atIndex:AAPLVertexInputIndexViewportSize];


    编码绘图命令

    指定图元的种类、起始索引和顶点数。当三角形被渲染时,vertex 函数被调用,参数 vertexID 的值分别为 0、1 和 2。


    // Draw the triangle.

    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle

                      vertexStart:0

                      vertexCount:3];


    与使用 Metal 绘制到屏幕一样,需要结束编码过程并提交命令缓冲区。不同之处是,可以使用相同的一组步骤对更多渲染命令进行编码。按照指定的顺序来执行命令,生成最终渲染的图像。 (为了性能,GPU 可以并行处理命令甚至部分命令,只要最终结果是按顺序渲染的就行。)

    颜色插值

    在此示例中,颜色值是在三角形内部插值计算出来的。有时希望由一个顶点生成一个值并在整个图元中保持不变,这需要在顶点函数的输出上指定 flat 属性限定符来执行此操作。示例项目中,通过在颜色字段中添加 [[flat]] 限定符来实现此功能。


    float4 color [[flat]];


    渲染管线使用三角形的第一个顶点(称为激发顶点)的颜色值,并忽略其他两个顶点的颜色。还可以混合使用 flat 着色和内插值,只需在顶点函数的输出上添加或删除 flat 限定符即可。

    总结

    本文介绍了如何配置渲染管道,如何编写顶点和片元函数、如何创建渲染管道状态对象,以及最后对绘图命令进行编码,最终在视图中绘制一个简单的 2D 彩色三角形。

    本文示例代码下载

    收起阅读 »