Giter VIP home page Giter VIP logo

blog's People

Contributors

yytmzys avatar

Stargazers

 avatar

Watchers

 avatar

Forkers

sicofied

blog's Issues

类和结构体

Swift 结构体是构建代码所用的一种通用且灵活的构造体。

我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。
与 C 和 Objective C 不同的是:
1 结构体不需要包含实现文件和接口。
2 结构体允许我们创建一个单一文件,且系统会自动生成面向其它代码的外部接口。
结构体总是通过被复制的方式在代码中传递,因此它的值是不可修改的。

语法
我们通过关键字 struct 来定义结构体:

使用github issue 写博客-记录, 技术如此优雅

使用github issue 写博客 记录, 感谢大佬开源 yihong0618/gitblog

1 创建工程

1

2 新建/上传文件 标红的两个文件, 文件内容不改

2

3 新建文件, 输入框输入 BACKUP/.gitkeep, 这样就会出现 文件夹 和空白文件 .gitkeep

3

4 新建文件, 输入框输入 .github/workflows/generate_readme.yml

复制文件内容, 修改三项
master改为main, 因为新建的工程仓库默认是主分支名字main
env:
GITHUB_NAME: qq
GITHUB_EMAIL: [email protected]
4

这一项 secrets.G_T 改或不改,
5

5 添加token, 全部权限 https://github.com/settings/tokens,

完成复制token, 保存起来
6

6 将token添加到 Actions secrets and variables, 我们创建 编辑 issue, 会触发 generate_readme 这里的方法,

7

7 为 Actions 添加读写权限,

8 9 ### 8 添加一条issue, 标题 内容, 触发 Action, 自动将issue添加到BACKUP文件夹, 创建/编辑issue会自动同步到 README.md, 形成目录 10

9 添加/编辑标签 Label, 这样 README.md 会按照标签分类

11 12

GCD

GCD

  • GCD---同步/异步 ,串行/并发
  • 死锁
  • GCD任务执行顺序
  • dispatch_barrier_async
  • dispatch_group_async
  • Dispatch Semaphore
  • 延时函数(dispatch_after)
  • 使用dispatch_once实现单例

一、GCD---队列

iOS中,有GCD、NSOperation、NSThread等几种多线程技术方案。

而GCD共有三种队列类型:
main queue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列。

global queue:全局队列是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列。调用dispath_get_global_queue并传入优先级来访问队列。

自定义队列:通过函数dispatch_queue_create创建的队列。

二、 死锁

死锁就是队列引起的循环等待

1、一个比较常见的死锁例子:主队列同步
- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_sync(dispatch_get_main_queue(), ^{

        NSLog(@"deallock");
    });
    // Do any additional setup after loading the view, typically from a nib.
}

在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。
同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。
而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。
想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。

2、同样,下边的代码也会造成死锁:
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{

        dispatch_sync(serialQueue, ^{

            NSLog(@"deadlock");
        });
    });

外面的函数无论是同步还是异步都会造成死锁。
这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁
解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    dispatch_async(serialQueue, ^{

        dispatch_sync(serialQueue2, ^{

            NSLog(@"deadlock");
        });
    });

这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。

三、GCD任务执行顺序

1、串行队列先异步后同步
    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    NSLog(@"1");

    dispatch_async(serialQueue, ^{

         NSLog(@"2");
    });

    NSLog(@"3");

    dispatch_sync(serialQueue, ^{

        NSLog(@"4");
    });

    NSLog(@"5");

打印顺序是13245
原因是:
首先先打印1
接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3
然后是任务4,将任务4添加至串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5
所以最终顺序就是13245。
这里的任务4在主线程中执行,而任务2在子线程中执行。
如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行(可以添加多个任务看效果)

2、performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{

        [self performSelector:@selector(test:) withObject:nil afterDelay:0];
    });

这里的test方法是不会去执行的,原因在于

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

这个方法要创建提交任务到runloop上的,而gcd底层创建的线程是默认没有开启对应runloop的,所有这个方法就会失效。
而如果将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行(将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也是会去执行的)。

四、dispatch_barrier_async

1、问:怎么用GCD实现多读单写?

多读单写的意思就是:可以多个读者同时读取数据,而在读的时候,不能去写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的。

image

这里的写处理就是通过栅栏的形式去写。
就可以用**dispatch_barrier_sync(栅栏函数)**去实现

2、dispatch_barrier_sync的用法:
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    for (NSInteger i = 0; i < 10; i++) {

        dispatch_sync(concurrentQueue, ^{

            NSLog(@"%zd",i);
        });
    }

    dispatch_barrier_sync(concurrentQueue, ^{

        NSLog(@"barrier");
    });

    for (NSInteger i = 10; i < 20; i++) {

        dispatch_sync(concurrentQueue, ^{

            NSLog(@"%zd",i);
        });
    }

这里的dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。
从打印上看,任务0-9和任务任务10-19因为是异步并发的原因,彼此是无序的。而由于栅栏函数的存在,导致顺序必然是先执行任务0-9,再执行栅栏函数,再去执行任务10-19。

  • dispatch_barrier_sync: Submits a barrier block object for execution and waits until that block completes.(提交一个栅栏函数在执行中,它会等待栅栏函数执行完)
  • dispatch_barrier_async: Submits a barrier block for asynchronous execution and returns immediately.(提交一个栅栏函数在异步执行中,它会立马返回)
    而dispatch_barrier_sync和dispatch_barrier_async的区别也就在于会不会阻塞当前线程
    比如,上述代码如果在dispatch_barrier_async后随便加一条打印,则会先去执行该打印,再去执行任务0-9和栅栏函数;而如果是dispatch_barrier_sync,则会在任务0-9和栅栏函数后去执行这条打印。
3、则可以这样设计多读单写:
- (id)readDataForKey:(NSString *)key
{
    __block id result;

    dispatch_sync(_concurrentQueue, ^{

        result = [self valueForKey:key];
    });

    return result;
}

- (void)writeData:(id)data forKey:(NSString *)key
{
    dispatch_barrier_async(_concurrentQueue, ^{

        [self setValue:data forKey:key];
    });
}

五、dispatch_group_async

场景:在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面。

dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT);

    dispatch_group_t group = dispatch_group_create();

    for (NSInteger i = 0; i < 10; i++) {

        dispatch_group_async(group, concurrentQueue, ^{

            sleep(1);

            NSLog(@"%zd:网络请求",i);
        });
    }

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{

        NSLog(@"刷新页面");
    });
深入理解GCD之dispatch_group
六、Dispatch Semaphore

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。

Dispatch Semaphore 提供了三个函数

1.dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
2.dispatch_semaphore_signal:发送一个信号,让信号总量加1
3.dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁
1、保持线程同步:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block NSInteger number = 0;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        number = 100;

        dispatch_semaphore_signal(semaphore);
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"semaphore---end,number = %zd",number);

dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后当前线程继续执行

2、保证线程安全,为线程加锁:

在线程安全中可以将dispatch_semaphore_wait看作加锁,而dispatch_semaphore_signal看作解锁
首先创建全局变量

 _semaphore = dispatch_semaphore_create(1);

注意到这里的初始化信号量是1。

- (void)asyncTask
{

    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);

    count++;

    sleep(1);

    NSLog(@"执行任务:%zd",count);

    dispatch_semaphore_signal(_semaphore);
}

异步并发调用asyncTask

  for (NSInteger i = 0; i < 100; i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [self asyncTask];
        });
    }

然后发现打印是从任务1顺序执行到100,没有发生两个任务同时执行的情况。

原因如下:
在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。

六、延时函数(dispatch_after)

dispatch_after能让我们添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue

//第一个参数是time,第二个参数是dispatch_queue,第三个参数是要执行的block
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"dispatch_after");
    });

由于其内部使用的是dispatch_time_t管理时间,而不是NSTimer。
所以如果在子线程中调用,相比performSelector:afterDelay,不用关心runloop是否开启

七、使用dispatch_once实现单例

+ (instancetype)shareInstance {

    static dispatch_once_t onceToken;

    static id instance = nil;

    dispatch_once(&onceToken, ^{

        instance = [[self alloc] init];
    });

    return instance;
}

AutoLayout

Autolayout 是 iOS 开发中的自动布局系统,用于对视图进行灵活的布局。

Autolayout 使用约束(Constraints)来描述视图之间的关系,包括视图的位置、大小和间距等。通过添加约束,实现视图的自适应布局。

Autolayout 在设备旋转、屏幕尺寸发生变化变化时,会自动重新计算视图的布局,调整位置和大小,确保视图正确呈现。

Swift语法和基础

Swift 知识分类

1 语法和基础:

变量、常量和数据类型 #3 (comment)
运算符 #3 (comment)
表达式 #3 (comment)
控制流程:if、switch、循环 #3 (comment)
函数 #3 (comment)
闭包 #3 (comment)
类、结构体和枚举
属性和方法
协议和扩展

2 可选项和错误处理:

可选项(Optionals)
强制解包和可选项绑定
错误处理(Error Handling)

3 集合类型:

数组(Array)
字典(Dictionary)
集合(Set)

1 函数式编程:

高阶函数
map、filter、reduce
尾随闭包

1 并发和多线程:

Grand Central Dispatch(GCD)
Operation 和 OperationQueue
异步编程

1 内存管理:

引用类型和值类型
自动引用计数(ARC)
内存管理模式面向对象编程:
继承和多态
初始化器和反初始化器
访问控制

1 模式设计:

单例模式
工厂模式
观察者模式等

1 界面开发:

SwiftUI 和 UIKit
视图控制器和视图
自定义视图和界面布局

1 网络和数据:

网络请求和 URLSession
JSON 解析和编码
数据持久化:Core Data、UserDefaults

1 测试和调试:

单元测试和 UI 测试
断言和调试技巧
Instruments 工具

1 设计模式:

MVC、MVVM、VIPER
依赖注入、装饰器等

1 工具和开发环境:

Xcode 使用技巧
Swift Package Manager
调试器和性能优化

1 Swift 版本更新:

Swift 语言的版本更新和变化

NSOperation 和 NSOperationQueue

NSOperation 和 NSOperationQueue

  • NSOperationQueue的优点
  • NSOperation和NSOperationQueue
  • NSThread+runloop实现常驻线程
  • 自旋锁与互斥锁

一、NSOperationQueue的优点

NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

  • 1、可以添加任务依赖,方便控制执行顺序
  • 2、可以设定操作执行的优先级
  • 3、任务执行状态控制:isReady,isExecuting,isFinished,isCancelled

如果只是重写NSOperation的main方法,由底层控制变更任务执行及完成状态,以及任务退出
如果重写了NSOperation的start方法,自行控制任务状态
系统通过KVO的方式移除isFinished==YES的NSOperation

  • 4、可以设置最大并发量

二、NSOperation和NSOperationQueue

  • 操作(Operation):

执行操作的意思,换句话说就是你在线程中执行的那段代码。
在 GCD 中是放在 block 中的。在 NSOperation 中,使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。

  • 操作队列(Operation Queues):

这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。

iOS 多线程:『NSOperationNSOperationQueue』详尽总结 - 简书

三、NSThread+runloop实现常驻线程

NSThread在实际开发中比较常用到的场景就是去实现常驻线程。

  • 由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用,那么如何创建一个线程可以让它可以再次工作呢?也就是创建一个常驻线程。

首先常驻线程既然是常驻,那么我们可以用GCD实现一个单例来保存NSThread

+ (NSThread *)shareThread {

    static NSThread *shareThread = nil;

    static dispatch_once_t oncePredicate;

    dispatch_once(&oncePredicate, ^{

        shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];

        [shareThread setName:@"threadTest"];

        [shareThread start];
    });

    return shareThread;
}

这样创建的thread就不会销毁了吗?

[self performSelector:@selector(test) onThread:[ViewController shareThread] withObject:nil waitUntilDone:NO];

- (void)test
{
    NSLog(@"test:%@", [NSThread currentThread]);
}

并没有打印,说明test方法没有被调用。
那么可以用runloop来让线程常驻

+ (NSThread *)shareThread {

    static NSThread *shareThread = nil;

    static dispatch_once_t oncePredicate;

    dispatch_once(&oncePredicate, ^{

        shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest2) object:nil];

        [shareThread setName:@"threadTest"];

        [shareThread start];
    });

    return shareThread;
}

+ (void)threadTest
{
    @autoreleasepool {

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

        [runLoop run];
    }
}

这时候再去调用performSelector就有打印了。

四、自旋锁与互斥锁

image

自旋锁:

是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

互斥锁:

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。

总结:

自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
  互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。

优缺点:

自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
  缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。

自旋锁:atomic、OSSpinLock、dispatch_semaphore_t
互斥锁:pthread_mutex、@ synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock
深入理解 iOS 开发中的锁

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

HTTP协议常见错误码

iOS开发中HTTP协议常见错误码

使用ASP.NET/PHP/JSP 或者javascript都会用到http的不同状态,一些常见的状态码为:

status code 状态描述
200 服务器成功返回网页
404 请求的网页不存在
503 服务不可用

status code :1xx(表示临时响应,需要继续处理)

status code 状态 描述
100 继续 请求者应当继续提出请求。 服务器返回此代码表示已收到请求的第一部分,正在等待其余部分
101 切换协议 请求者已要求服务器切换协议,服务器已确认并准备切换。
102 扩展码 处理将被继续执行。

status code :2xx(表示成功处理了请求的状态代码)

status code 状态 描述
200 成功 服务器已成功处理了请求。
201 已创建 请求成功并且服务器创建了新的资源。
202 已接受 服务器已接受请求,但尚未处理。
203 非授权信息 服务器已成功处理了请求,但返回的信息可能来自另一来源。
204 无内容 服务器成功处理了请求,但没有返回任何内容。
205 重置内容 服务器成功处理了请求,但没有返回任何内容。
206 部分内容 服务器成功处理了部分 GET 请求。
207 状态码 代表之后的消息体将是一个XML消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码

status code :3xx(重定向这类状态码代表需要客户端采取进一步的操作才能完成请求)

status code 状态 描述
300 多种选择 针对请求,服务器可执行多种操作。
301 永久移动 请求的网页已永久移动到新位置。
302 临时移动 请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。。
303 查看其他位置 对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源。
304 未修改 304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。
305 使用代理 被请求的资源必须通过指定的代理才能被访问。
306 在最新版的规范中,306状态码已经不再被使用。
307 临时重定向 请求的资源现在临时从不同的URI响应请求

status code :4xx(客户端错误

这类的状态码代表了客户端看起来可能发生了错误,妨碍了服务器的处理。)

status code 状态 描述
400 语法错误 由于包含语法错误,当前请求无法被服务器理解。
401 验证错误 当前请求需要用户验证。
402 该状态码是为了将来可能的需求而预留的。
403 拒绝 服务器已经理解请求,但是拒绝执行它。。
404 未发现 请求失败,请求所希望得到的资源未被在服务器上发现。
405 请求行中指定的请求方法不能被用于请求相应的资源。
406 请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。
407 验证 与401响应类似,只不过客户端必须在代理服务器上进行身份验证。
408 超时 请求超时。
409 无法完成 由于和被请求的资源的当前状态之间存在冲突,请求无法完成。
410 响应的目的主要是帮助网站管理员维护网站,通知用户该资源已经不再可用,并且服务器拥有者希望所有指向这个资源的远端连接也被删除。
411 无定义 服务器拒绝在没有定义Content-Length头的情况下接受请求。
412 不满足 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。
413 拒绝 服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。
414 URI 过长 请求的URI长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。
415 不支持的媒体类型 请求的格式不受请求页面的支持。
416 请求范围不符合要求 如果页面无法提供请求的范围,则服务器会返回此状态代码。
417 未满足期望值 服务器未满足”期望”请求标头字段的要求。

详细:https://blog.csdn.net/qqqqzxg/article/details/50715332

iOS 知识分类

-1 Objective-C语言基础, 逝去的OC

0 SwiftUI

code url
SwiftUI基础 #85
文本视图 Text, Label, TextField, TextEditor, SecureField #86
图片 Image AsyncImage #87
按钮-Button
导航 Link, NavigationLink https://www.jianshu.com/p/74ed753feb40
TabView #90
选择器 Picker https://www.jianshu.com/p/4b9365d5bed5
开关 指示器 计数器-Toggle ProgressView Stepper #92
弹窗, Alert, ActionSheet https://www.jianshu.com/p/c4edd212f59d
地图 Map https://www.jianshu.com/p/bc25595e59f1?v=1690899147140
视图和布局
HStack VStack ZStack #99
List, ScrollView, LazyVGrid, Form, Spacer, Divider, NavigationView, TabView https://www.jianshu.com/p/9f6383144133
数据绑定和状态管理
自定义视图和组件
SwiftUI与UIKit混编

1 Swift语言基础

code url
Swift语法和基础 #3
函数和闭包 #4
类和结构体 #5
枚举 #6
协议和扩展 #7
泛型 #8
错误处理 #9

2 UIKit框架

code 链接
视图控制器(UIViewController)
视图(UIView)
控件(UIButton、UILabel、UITextField等)
表格视图(UITableView)
集合视图(UICollectionView)
导航控制器(UINavigationController)
标签栏控制器(UITabBarController)
页面视图控制器(UIPageViewController)
弹出视图(UIAlertController)
图片选择器(UIImagePickerController)
等等...

3 自动布局和界面设计

code 链接
AutoLayout https://www.jianshu.com/p/e5ca657d6495?v=1690895203958
Size Classes
Size Classes
Interface Builder (IB)
Storyboards 和 XIBs

4 多线程和异步编程

code 链接url
进程 线程 多线程 任务 队列 https://www.jianshu.com/p/afef02f0e4cc
GCD https://www.jianshu.com/p/5a2b1ae59b5e
NSOperation 和 NSOperationQueue https://www.jianshu.com/p/b77ddeb5e1eb?v=1690973440713
后台任务和异步操作

5 数据存储和持久化

code 链接
Core Data
SQLite数据库
UserDefaults
文件系统操作

6 网络通信和数据交互

code 链接
URLSession
Alamofire 或其他网络库
HTTPS、对称加密、非对称加密 https://www.jianshu.com/p/2d024ca4baf2?v=1690981970791
JSON解析和序列化
JSON解析和序列化
HTTP协议常见错误码 https://www.jianshu.com/p/959318890a4e

7 用户界面交互和手势识别

code 链接
用户交互和事件处理
手势识别器(Gesture Recognizers)

8 图像处理和多媒体

code 链接
Core Graphics
Core Image
图片和视频处理
音频播放和录制

9 地图和位置服务

code 链接
Core Location
MapKit

10 推送通知和本地通知

code 链接
远程推送通知 #61
本地通知 #62

11 常用框架和库

code 链接
AVFoundation(多媒体处理) #63
Core Animation(动画)
Core Bluetooth(蓝牙通信)
Core Motion(传感器数据) #66
Core NFC(近场通信) #67
Core Spotlight(搜索功能) #68
Core Telephony(电话和网络信息) #69

12 测试和调试

code 链接
单元测试(Unit Testing) #76
UI测试(UI Testing) #77
调试技巧和性能优化 #78

13 应用发布和分发

code 链接
应用打包和签名 #79
App Store上架和审核 #80
企业分发 企业分发

14 设备特定功能和权限

code 链接
相机和相册访问 #82
地理位置权限
通知权限
蓝牙和NFC权限

HTTPS、对称加密、非对称加密


一、HTTPS和HTTP的区别
HTTPS协议 = HTTP协议 + SSL/TLS协议
SSL的全称是Secure Sockets Layer,即安全套接层协议,是为网络通信提供安全及数据完整性的一种安全协议。TLS的全称是Transport Layer Security,即安全传输层协议。
即HTTPS是安全的HTTP。

二、HTTPS的连接建立流程
HTTPS为了兼顾安全与效率,同时使用了对称加密和非对称加密。在传输的过程中会涉及到三个密钥:

服务器端的公钥和私钥,用来进行非对称加密

客户端生成的随机密钥,用来进行对称加密

如上图,HTTPS连接过程大致可分为八步:

1、客户端访问HTTPS连接。
客户端会把安全协议版本号、客户端支持的加密算法列表、随机数C发给服务端。

2、服务端发送证书给客户端
服务端接收密钥算法配件后,会和自己支持的加密算法列表进行比对,如果不符合,则断开连接。否则,服务端会在该算法列表中,选择一种对称算法(如AES)、一种公钥算法(如具有特定秘钥长度的RSA)和一种MAC算法发给客户端。
服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。
在发送加密算法的同时还会把数字证书和随机数S发送给客户端

3、客户端验证server证书
会对server公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。

4、客户端组装会话秘钥
如果公钥合格,那么客户端会用服务器公钥来生成一个前主秘钥(Pre-Master Secret,PMS),并通过该前主秘钥和随机数C、S来组装成会话秘钥

5、客户端将前主秘钥加密发送给服务端
是通过服务端的公钥来对前主秘钥进行非对称加密,发送给服务端

6、服务端通过私钥解密得到前主秘钥
服务端接收到加密信息后,用私钥解密得到主秘钥。

7、服务端组装会话秘钥
服务端通过前主秘钥和随机数C、S来组装会话秘钥。
至此,服务端和客户端都已经知道了用于此次会话的主秘钥。

8、数据传输
客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。
同理,服务端收到客户端发送来的密文,用服务端密钥对其进行对称解密,得到客户端发送的数据。

总结:
会话秘钥 = random S + random C + 前主秘钥

HTTPS连接建立过程使用非对称加密,而非对称加密是很耗时的一种加密方式
后续通信过程使用对称加密,减少耗时所带来的性能损耗
其中,对称加密加密的是实际的数据,非对称加密加密的是对称加密所需要的客户端的密钥。
三、对称加密和非对称加密
1、对称加密
用同一套密钥来进行加密解密。
对称加密通常有 DES,IDEA,3DES 加密算法。

2、非对称加密
用公钥和私钥来加解密的算法。
公钥(Public Key)与私钥(Private Key)是通过一种算法得到的一个密钥对(即一个公钥和一个私钥),公钥是密钥对中公开的部分,私钥则是非公开的部分,私钥通常是保存在本地。

用公钥进行加密,就要用私钥进行解密;反之,用私钥加密,就要用公钥进行解密(数字签名)。

由于私钥是保存在本地的,所以非对称加密相对与对称加密是安全的。
但非对称加密比对称加密耗时(100倍以上),所以通常要结合对称加密来使用。

常见的非对称加密算法有:RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)

而为了确保客户端能够确认公钥就是想要访问的网站的公钥,引入了数字证书的概念,由于证书存在一级一级的签发过程,所以就出现了证书链,在证书链中的顶端的就是根CA

函数和闭包

函数

        // 函数的定义与调用
        func greet(person: String) -> String {
            let greeting = "Hello, " + person + "!"
            return greeting
        }
        print(greet(person: "Anna"))
        
        // 为了简化这个函数的定义, 可以将问候消息的创建和返回写成一句
        func greetAgain(person: String) -> String {
            return "Hello, again, " + person + "!"
        }
        print(greetAgain(person: "Anna"))
        
        // 函数参数与返回值
        // 无参数函数
        func sayHelloWorld() -> String {
            return "Hello, World!"
        }
        print(sayHelloWorld())
        
        // 多参数函数, 函数可以有多种输入参数, 包含在函数括号中, 以逗号分隔
        func greet2(person: String, alreadyGreeted: Bool) -> String {
            if alreadyGreeted {
                return greetAgain(person: person)
            } else {
                return greet(person: person)
            }
        }
        print(greet2(person: "Tim", alreadyGreeted: true))
        
        // 无返回值函数, 函数可以没有返回值, 不需要返回箭头 -> 和返回类型
        // 严格上来说, 虽然没有返回值被定义, greet2(person:) 函数依然返回了值. 没有定义返回值类型的函数会返回一个特殊的Void值. 它其实是一个空的元祖 touple, 没意义任何元素, 可以写成 ()
        func greet3(person: String) {
            print("Hello, \(person)!")
        }
        greet3(person: "Dave")
        
        // 被调用时, 一个函数的返回值可以被忽略
        func printAndCount(string: String) -> Int {
            print(string)
            return string.count
        }
        
        func pringWithoutCounting(string: String) {
            let _ = printAndCount(string: string)
        }
        printAndCount(string: "Hello, World!")    // 打印, 并返回值
        pringWithoutCounting(string: "Hello, World!")
        
        // 多重返回值函数
        func minMax(array: [Int]) -> (min: Int, max: Int) {
            var currentMin = array[0]
            var currentMax = array[0]
            for value in array[1..<array.count] {
                if value < currentMin {
                    currentMin = value
                } else if value > currentMax {
                    currentMax = value
                }
            }
            
            return(currentMin, currentMax)
        }
        
        let bounds = minMax(array: [8, -9, 2, 109, 2, 76])
        print("min is \(bounds.min) and max is \(bounds.max)")
        
        // 可选元祖返回类型,
        func minMax2(array: [Int]) -> (min: Int, max: Int)? {
            if array .isEmpty { return nil }
            var currentMin = array[0]
            var currentMax = array[0]
            for value in array[1..<array.count] {
                if value < currentMin {
                    currentMin = value
                } else if value > currentMax {
                    currentMax = value
                }
            }
            
            return(currentMin, currentMax)
        }
        let bounds2 = minMax2(array: [8, -9, 2, 109, 2, 76])
        print("min is \(bounds2?.min) and max is \(bounds2?.max)")
        
        // 函数参数标签和参数名称, 你可以在参数名称前指定它的参数标签, 中间以空格分割,
        //    参数标签的使用能够让一个函数在调用时更有表达力, 更类似自然语言, 并且扔保持了函数内部的可读性以及清晰的意图
        func greet4(person: String, from hometown: String) -> String {
            return "Hello \(person)! Glad you could visit from \(hometown)."
        }
        print(greet4(person: "Bill", from: "Cupertino"))
        // Hello Bill! Glad you could visit from Cupertino.
        
        // 忽略函数标签,  如果你不希望为某个参数添加一个标签, 可以使用一个下划线 _ 来代替一个明确的参数标签,
        //           如果一个参数有一个标签, 那么在调用的时候必须用标签来标记这个参数
        func greet5(_ person: String, from hometown:String) -> String {
            return "Hello \(person)! Glad you could visit from \(hometown)."
        }
        print(greet5("Bill", from: "Cupertino"))
        
        // 默认参数值 , 你可以在函数体中通过给参数值来为任意一个参数定义默认值, 当默认值被定以后, 调用这个函数时就可以忽略这个参数
        func greet6(person: String, hometown: String = "earth") -> (String){
            return "Hello \(person)! Glad you could visit from \(hometown)."
        }
        print(greet6(person: "Bill"))
        // Hello Bill! Glad you could visit from earth.
        
        // 可变参数, 可以接受零个或多个值. 函数调用时, 你可以用可变参数来指定函数参数可以被传入不确定数量的输入值. 通过在变量类型名后面加入 ... 的方式来定义可变参数
        // 可变参数的传入值在函数体重变为此类型的一个数组. 例如, 一个叫做numbers 的 Double... 型可变参数, 在函数体内可以当做一个叫numbers 的 [Double]型的数组常量
        func arithmeticMean(_ numbers: Double...) -> Double {
            var total: Double = 0
            for number in numbers {
                total += number
            }
            
            return total / Double( numbers.count )
        }
        
        print(arithmeticMean(1, 2, 3, 4, 5))    // 3.0
        print(arithmeticMean(1, 2, 3, 4, 5, 7)) // 3.66666666666667
        
        // 输入输出参数
        func swapTwoInts(_ a: inout Int, _ b: inout Int ) -> (Int, Int) {
            let temp = a
            a = b
            b = temp
            return (a, b)
        }
        
        var a = 3
        var b = 5
        print(swapTwoInts(&a, &b))  // (5, 3)
        
        // 函数类型. 每个函数都有种特定的函数类型, 函数的类型都由函数的参数类型和返回值类型组成
        func addTwoInts(_ a: Int, _ b: Int) -> Int{
            return a + b
        }
        func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
            return a * b
        }
        // 这两个函数的类型是 (Int, Int) -> Int, 可以解读为" 这个函数类型有两个 Int 型的参数并返回一个 Int 型的值".
        
        // 使用函数类型, 在Swift中, 使用函数类型就像使用其他类型一样.
        var mathFunction: (Int, Int) -> Int = addTwoInts
        // 这段代码可以理解为:  定义一个叫做 mathFunction的常量, 类型是'一个有两个Int型的参数并返回一个Int型的值得函数', 并让这个新变量指向 addTwoInts 函数, addTwoInts 和 mathFunction 有同样的类型, 所以这个赋值 在 Swi 类型检查(type-check) 中是允许的
        print(mathFunction( 2, 3)) // 5
        // 有相同匹配类型的不同函数可以被赋值给同一个变量,
        mathFunction = multiplyTwoInts
        print(mathFunction( 2, 3)) // 6
        
        
        // 函数类型作为参数类型,  你可以用 (Int, Int) -> Int 这样的函数类型最为另一个函数的参数类型. 这样可以将函数的一部分实现留给函数的调用者来提供
        func printMathResult(_ mathFunction: (Int, Int) -> Int, a: Int, _ b: Int) {
            print("Result: \(mathFunction(a, b))")
        }
        printMathResult(addTwoInts, a: 2, 3) // Result: 8
        
        // 函数类型作为返回类型, 你可以用函数类型作为另一个函数的返回类型, 你需要做的就是在返回箭头 -> 后写一个完整的函数类型
        func stepForward(_ input: Int) -> Int{
            return input + 1
        }
        
        func stepBackward(_ input: Int) -> Int {
            return input - 1
        }
        // 如下名为chooseStepFunction的函数, 它的返回值类型是 (Int) -> int 类型的函数
        func chooseStepFunction(backward: Bool) -> (Int) -> Int {
            return backward ? stepForward : stepBackward
        }
        
        var currentValue = 3
        var moveNearerToZero = chooseStepFunction(backward: currentValue > 5)
        // moveNearerToZero 现在指向 stepBackward
        while currentValue != 0 {
            print("\(currentValue)")
            currentValue = moveNearerToZero(currentValue)
        }
        print("zero!")
        
        // 嵌套函数, 可以把函数定义在别的函数体中.    默认情况下, 嵌套函数对外界是不可见的, 但是可以被它们的外围函数(enclosing function) 调用.    一个外围函数也可以返回它的某一个嵌套函数, 使得这个函数可以在其他领域中被使用
        func chooseStepFunction2(backward: Bool) -> (Int) -> Int {
            func stepForward(input: Int)  -> Int { return input + 1}
            func stepBackward(input: Int) -> Int { return input - 1}
            return backward ? stepForward : stepBackward
        }
        
        currentValue = -4
        moveNearerToZero = chooseStepFunction2(backward: currentValue < 0)
        // moveNearerToZero 指向 stepForward
        while currentValue != 0 {
            print("\(currentValue)")
            currentValue = moveNearerToZero(currentValue)
        }
        print("zero!")

闭包, 在 Swift 中,Block 被称为闭包(Closures),示例代码如下:

1 非逃逸闭包(Non-Escaping Closure):

func performOperation(completion: () -> Void) {
    print("Performing operation...")
    completion()
}

performOperation {
    print("Operation completed.")
}

2 逃逸闭包(Escaping Closure):

var completionHandlers: [() -> Void] = []

func appendCompletionHandler(handler: @escaping () -> Void) {
    completionHandlers.append(handler)
}

func performOperation(completion: () -> Void) {
    print("Performing operation...")
    completionHandlers.append(completion)
}

performOperation {
    print("Operation completed.")
}

completionHandlers.forEach { $0() }

3 自动闭包(Autoclosure):

func printAndRemove(element: @autoclosure () -> Int) {
    print("Element:", element())
}

printAndRemove(element: 10)

这些示例展示了不同类型的闭包在 Objective-C 和 Swift 中的用法。全局块类似于 Swift 中的全局函数,而栈块和堆块用于捕获局部变量并在不同的作用域中使用。在 Swift 中,根据闭包是否允许逃逸,以及是否自动封装表达式,有更多的灵活性和功能。

进程 线程 多线程 任务 队列

一、 进程:

  • 1.进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.
  • 2.进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.
  • 3.每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源

二、 线程

  • 1.程序执行流的最小单元,线程是进程中的一个实体.
  • 2.一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程

三、 进程和线程的关系

  • 1.线程是进程的执行单元,进程的所有任务都在线程中执行
  • 2.线程是 CPU 分配资源和调度的最小单位
  • 3.一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
  • 4.同一个进程内的线程共享进程资源

四、 多进程

打开mac的活动监视器,可以看到很多个进程同时运行

image

  • 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。
  • 进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身;所有由用户启动的进程都是用户进程。进程是操作系统进行资源分配的单位。
  • 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。

五、 多线程

  • 1.同一时间,CPU只能处理1条线程,只有1条线程在执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象

  • 2.如果线程非常非常多,CPU会在N多线程之间调度,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)

  • 3.多线程的优点:
    能适当提高程序的执行效率
    能适当提高资源利用率(CPU、内存利用率)

  • 4.多线程的缺点:
    开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
    线程越多,CPU在调度线程上的开销就越大
    程序设计更加复杂:比如线程之间的通信、多线程的数据共享

六、任务

就是执行操作的意思,也就是在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)

  • 同步(Sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。
  • 异步(Async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务

七、队列

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

  • 串行队列(Serial Dispatch Queue)
    同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的

  • 并发队列(Concurrent Dispatch Queue)
    同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有在异步(dispatch_async)函数下才有效

    image

八、iOS中的多线程

主要有三种:NSThread、NSoperationQueue、GCD
1. NSThread:轻量级别的多线程技术

是我们自己手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造器方式它就会自动启动。只要是我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收

    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是参数"];
    // 当使用初始化方法出来的主线程需要start启动
    [thread start];
    // 可以为开辟的子线程起名字
    thread.name = @"NSThread线程";
    // 调整Thread的权限 线程权限的范围值为0 ~ 1 。越大权限越高,先执行的概率就会越高,由于是概率,所以并不能很准确的的实现我们想要的执行顺序,默认值是0.5
    thread.threadPriority = 1;
    // 取消当前已经启动的线程
    [thread cancel];
    // 通过遍历构造器开辟子线程
    [NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方式"];
  • performSelector...只要是NSObject的子类或者对象都可以通过调用方法进入子线程和主线程,其实这些方法所开辟的子线程也是NSThread的另一种体现方式。
    在编译阶段并不会去检查方法是否有效存在,如果不存在只会给出警告
      //在当前线程。延迟1s执行。响应了OC语言的动态性:延迟到运行时才绑定方法
        [self performSelector:@selector(aaa) withObject:nil afterDelay:1];
      // 回到主线程。waitUntilDone:是否将该回调方法执行完在执行后面的代码,如果为YES:就必须等回调方法执行完成之后才能执行后面的代码,说白了就是阻塞当前的线程;如果是NO:就是不等回调方法结束,不会阻塞当前线程
        [self performSelectorOnMainThread:@selector(aaa) withObject:nil waitUntilDone:YES];
      //开辟子线程
        [self performSelectorInBackground:@selector(aaa) withObject:nil];
      //在指定线程执行
        [self performSelector:@selector(aaa) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES]

需要注意的是:如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的Runloop中。也就是如果当前线程没有开启runloop,该方法会失效。在子线程中,需要启动runloop(注意调用顺序)

[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];

而performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行

2、GCD 对比 NSOprationQueue

我们要明确NSOperationQueue与GCD之间的关系
GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象。

1、GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便
2、GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
3、NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂

4、NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)

  • 实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选

  • 如果考虑异步操作之间的事务性,顺序行,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持

  • 不论是GCD还是NSOperationQueue,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销

  • 造成一些性能上的开销

  • 更多关于GCD:2019 iOS面试题-----多线程相关之GCD、死锁、dispatch_barrier_async、dispatch_group_async、Dispatch Semaphore

1782258-8a5f34dd1b510f52

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.