注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS 线程安全和锁机制

iOS
一、线程安全场景 多个线程中同时访问同一块资源,也就是资源共享。多牵扯到对同一块数据的读写操作,可能引发数据错乱问题。 比较经典的线程安全问题有购票和存钱取钱问题,为了说明读写操作引发的数据混乱问题,以存钱取钱问题来做个说明。 1. 购票案例 用代码示例如下...
继续阅读 »

一、线程安全场景


多个线程中同时访问同一块资源,也就是资源共享。多牵扯到对同一块数据的读写操作,可能引发数据错乱问题。


比较经典的线程安全问题有购票和存钱取钱问题,为了说明读写操作引发的数据混乱问题,以存钱取钱问题来做个说明。


1. 购票案例




用代码示例如下:

@IBAction func ticketSale() {

        tickets = 30

        let queue = DispatchQueue.global()

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

    }

    //卖票

    func sellTicket() {

        var oldTicket = tickets

        sleep(UInt32(0.2))

         oldTicket -= 1

         tickets = oldTicket

        print("还剩\(tickets)张票 ---- \(Thread.current)")

    }

同时有三条线程进行卖票,对余票进行减操作,三条线程每条卖10次,原定票数30,应该剩余票数为0,下面看下打印结果:




可以看到打印票数不为0


2. 存钱取钱案例


先用个图说明




上图可以看出,存钱和取钱之后的余额理论上应该是500,但由于存钱取钱同时访问并修改了余额,导致数据错乱,最终余额可能变成了400,下面用代码做一下验证说明:

//存钱取钱

    @IBAction func remainTest() {

        remain = 500

        let queue = DispatchQueue.global()

        queue.async {

            for _ in 0..<5 {

                self.saveMoney()

            }

        }

        queue.async {

            for _ in 0..<5 {

                self.drawMoney()

            }

        }

    }

    //存钱

    func saveMoney() {

       var oldRemain = remain

        sleep(2)

        oldRemain += 100

        remain = oldRemain

        print("存款100元,账户余额还剩\(remain)元 ----\(Thread.current)")

    }

    

    //取钱

    func drawMoney() {

        var oldRemain = remain

         sleep(2)

         oldRemain -= 50

         remain = oldRemain

        print("取款50元,账户余额还剩\(remain)元 ---- \(Thread.current)")

    }

上述代码存款5次100,取款5次50,最终的余额应该是 500 + 5 * 100 - 5 * 50 = 750




如图所示,可以看到在存款取款之间已经出现错乱了



上述两个案例之所以出现数据错乱问题,就是因为有多个线程同时操作了同一资源,导致数据不安全而出现的。



那么遇到这个问题该怎么解决呢?自然而然的,我们想到了对资源进行加锁处理,以此来保证线程安全,在同一时间,只允许一条线程访问资源。


加锁的方式大概有以下几种:

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock

1. OSSpinLock 自旋锁




OSSpinLock 是自旋锁,在系统框架 libkern/OSAtomic




如图,系统提供了以下几个API

  • 定义lock let osspinlock = OSSpinLock()
  • OSSpinLockTry

官方给定的解释如下

Locks a spinlock if it would not block
return false, if the lock was already held by another thread,
return true, if it took the lock successfully.


尝试加锁,加锁成功则继续,加锁失败则直接返回,不会阻塞线程

  • OSSpinLockLock
Although the lock operation spins, it employs various strategies to back

off if the lock is held.

加锁成功则继续,加锁失败,则会阻塞线程,处于忙等状态

  • OSSpinLockUnlock: 解锁

使用
@IBAction func ticketSale() {

        osspinlock = OSSpinLock()

        tickets = 30

        let queue = DispatchQueue.global()

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

        queue.async {

            for _ in 0..<10 {

                self.sellTicket()

            }

        }

    }

    //卖票

    func sellTicket() {

        OSSpinLockLock(&osspinlock)

        var oldTicket = tickets

        sleep(UInt32(0.2))

         oldTicket -= 1

         tickets = oldTicket

        print("还剩\(tickets)张票 ---- \(Thread.current)")

        OSSpinLockUnlock(&osspinlock)

    }



可以看到,最终的余票数量已经是正确的了,这里要注意的是osspinlock需要做成全局变量或者属性,多个线程要用这同一把锁去加锁和解锁,如果每个线程各自生成锁,则达不到要加锁的目的了


那么自旋锁是怎么样做到加锁保证线程安全的呢?
先来介绍下让线程阻塞的两种方法:

  • 忙等:也就是自旋锁的原理,它本质上就是个while循环,不停地去判断加锁条件,自旋锁没有让线程真正的阻塞,只是将线程处在while循环中,系统CPU还是会不停地分配资源来处理while循环指令。
  • 真正阻塞线程: 这是让线程休眠,类似于Runloop里的match_msg() 实现的效果,它借助系统内核指令,让线程真正停下来处于休眠状态,系统的CPU不再分配资源给线程,也不会再执行任何指令。系统内核用的是symcall指令来让线程进入休眠

它的原理就是,自旋锁在加锁失败时,让线程处于忙等状态,让线程停留在临界区之外,一旦加锁成功,就可以进入临界区对资源进行操作。




通过这个可以看到,苹果在iOS10之后就弃用了OSSpinLock,官方建议用 os_unfair_lock来代替,那么为什么要弃用呢?因为在iOS10之后线程可以设置优先级,在优先级配置下,可以产生优先级反转,使自旋锁卡住,自旋锁本身已经不再安全。


2. os_unfair_lock


os_unfair_lock 是苹果官方推荐的,自iOS10之后用来替代 OSSpinLock 的一种锁

  • os_unfair_lock_trylock: 尝试加锁,加锁成功返回true,继续执行。加锁失败,则返回false,不会阻塞线程。
  • os_unfair_lock_lock: 加锁,加锁失败,阻塞线程继续等待。加锁成功,继续执行。
  • os_unfair_lock_unlock : 解锁

使用:

//卖票

    func sellTicket() {

        os_unfair_lock_lock(&unfairlock)

        var oldTicket = tickets

        sleep(UInt32(0.2))

         oldTicket -= 1

         tickets = oldTicket

        print("还剩\(tickets)张票 ---- \(Thread.current)")

        os_unfair_lock_unlock(&unfairlock)

    }

打印结果和OSSpinLock一样,os_unfair_lock摒弃了OSSpinLock的while循环实现的忙等状态,而是采用了真正让线程休眠,从而避免了优先级反转问题。


3. pthread_mutex


pthread_mutexpthread跨平台的一种解决方案,mutex 为互斥锁,等待锁的线程会处于休眠状态。
互斥锁的初始化比较麻烦,主要为以下方式:

  1. var ticketMutexLock = pthread_mutex_t()
  2. 初始化属性:
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)

3. 初始化锁:pthread_mutex_init(&ticketMutexLock, &attr)

关于互斥锁的使用,主要提供了以下方法:

  1. 尝试加锁:pthread_mutex_trylock(&ticketMutexLock)
  2. 加锁:pthread_mutex_lock(&ticketMutexLock)
  3. 解锁:pthread_mutex_unlock(&ticketMutexLock)
  4. 销毁相关资源:pthread_mutexattr_destory(&attr)pthread_mutex_destory(&ticketMutexLock)

使用方式如下:




要注意,在析构函数中要将锁进行销毁释放掉
在初始化属性中,第二个参数有以下几种方式:




PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL,代表普通的互斥锁
PTHREAD_MUTEX_ERRORCHECK 代表检查错误锁
PTHREAD_MUTEX_RECURSIVE 代表递归互斥锁


互斥锁的底层原理实现也是通过阻塞线程,等待锁的线程处于休眠状态,CPU不再给等待的线程分配资源,和上面讲到的os_unfair_lock原理类似,都是通过内核调用symcall方法来休眠线程,通过这个对比也能推测出,os_unfair_lock实际上也可以归属于互斥锁


3.1 递归互斥锁



如图所示,如果是上述场景,方法1里面嵌套方法2,正常调用时,输出应该为:




若要对上述场景保证线程安全,先用普通互斥锁添加锁试下




结果打印如下:




和预想中的不一样,如果懂得锁机制便会明白,图中所示的rsmText2中加锁失败,需要等待rsmText1中的锁释放后才可加锁,所以rsmText2方法开始等待并阻塞线程,程序无法再执行下去,那么rsmText1中锁释放的逻辑就无法执行,就这样造成了死锁,所以只能打印rsmText1中的输出内容。
解决这个问题,只需要给两个方法用两个不同的锁对象进行加锁就可以了,但是如果是针对于同一个方法递归调用,那么就无法通过不同的对象去加锁,这时候应该怎么办呢?递归互斥锁就该用上了。








如上,已经可以正常调用并加锁
那么递归锁是如何避免死锁的呢?简而言之就是允许对同一个对象进行重复加锁,重复解锁,加锁和解锁的次数相等,调用结束时所有的锁都会被解开


3.2 互斥锁条件 pthread_cond_t

互斥锁条件所用到的常见方法如下:

  1. 定义一个锁: var condMutexLock = pthread_mutex_t()
  2. 初始化锁对象:pthread_mutex_init(&condMutexLock)
  3. 定义条件对象:var condMutex = pthread_cond_t()
  4. 初始化条件对象:pthread_cond_init(&condMutex, nil)
  5. 等待条件:pthread_cond_wait(&condMutex, &condMutexLock) 等待过程中会阻塞线程,知道有激活条件的信号发出,才会继续执行
  6. 激活一个等待该条件的线程:pthread_cond_signal(&condMutex)
  7. 激活所有等待该条件的线程pthread_cond_broadcast(&condMutex)
  8. 解锁:pthread_mutex_unlock(&condMutexLock)
  9. 销毁锁对象和销毁条件对象:pthread_mutex_destroy(&condMutexLock) pthread_cond_destroy(&condMutex)

下面设计一个场景:

  • 在一个线程里对dataArr做remove操作,另一个线程里做add操作
  • dataArr为0时,不能进行删除操作
@IBAction func mutexCondTest(_ sender: Any) {

        initMutextCond()

    }

    func initMutextCond() {

        //初始化属性

        var attr = pthread_mutexattr_t()

        pthread_mutexattr_init(&attr)

        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)

        //初始化锁

        pthread_mutex_init(&condMutexLock, &attr)

        //释放属性

        pthread_mutexattr_destroy(&attr)

        //初始化cond

        pthread_cond_init(&condMutex, nil)

        _testDataArr()

        

    }

    func _testDataArr() {

        let threadRemove = Thread(target: self, selector: #selector(_remove), object: nil)

        threadRemove.name = "remove 线程"

        threadRemove.start()

        

        sleep(UInt32(1))

        let threadAdd = Thread(target: self, selector: #selector(_add), object: nil)

        threadAdd.name = "add 线程"

        threadAdd.start()

        

    }

    @objc func _add() {

        //加锁

        pthread_mutex_lock(&condMutexLock)

        print("add 加锁成功---->\(Thread.current.name!)开始")

        sleep(UInt32(2))

        dataArr.append("test")

        print("add成功,发送条件信号 ------ 数组元素个数为\(dataArr.count)")

        pthread_cond_signal(&condMutex)

        //解锁

        pthread_mutex_unlock(&condMutexLock)

        print("解锁成功,\(Thread.current.name!)线程结束")

    }

    @objc func _remove() {

        //加锁

        pthread_mutex_lock(&condMutexLock)

        print("remove 加锁成功,\(Thread.current.name!)线程开启")

        if(dataArr.count == 0) {

            print("数组内没有元素,开始等待,数组元素为\(dataArr.count)")

            pthread_cond_wait(&condMutex, &condMutexLock)

            print("接收到条件更新信号,dataArr元素个数为\(dataArr.count),继续向下执行")

        }

        dataArr.removeLast()

        print("remove成功,dataArr数组元素个数为\(dataArr.count)")

        

        //解锁

        pthread_mutex_unlock(&condMutexLock)

        print("remove解锁成功,\(Thread.current.name!)线程结束")

    }

    

    deinit {

//        pthread_mutex_destroy(&ticketMutexLock)

        pthread_mutex_destroy(&condMutexLock)

        pthread_cond_destroy(&condMutex)

    }

输出结果为:




从打印结果来看,如果不满足条件时进行条件等待 pthread_cond_wati,remove 线程是解锁,此时线程是休眠状态,然后等待的add 线程进行加锁成功,处理add的逻辑。


当add 操作完毕时,通过 pthread_cond_signal发出信号,remove线程收到信号后被唤醒,然后remove线程会等待add线程解锁后,再进行加锁处理后续的逻辑.


整个过程中一共用到了三次加锁,三次解锁,这种锁可以处理线程依赖的场景.


4. NSLock, NSRecursiveLock, NSCondition


上文中提到了mutex普通互斥锁 mutex递归互斥锁mutext条件互斥锁,这几种锁都是基于C语言的API,苹果在此基础上做了面向对象的封装,分别对应如下:

  • NSLock 封装了 pthread_mutex_t的 attr类型为 PTHREAD_MUTEX_DEFAULT 或者 PTHREAD_MUTEX_NORMAL 普通锁
  • NSRecursiveLock 封装了 pthread_mutex 的 attr类型为PTHREAD_MUTEX_RECURSIVE递归锁
  • NSCondition 封装了 pthread_mutex_t 和 pthread_cond_t

底层实现和 pthread_mutex_t一样,这里只看下使用方式即可:


4.1 NSLock
//普通锁 
let lock = NSLock()
lock.lock()
lock.unlock()

4.2 NSRecursiveLock
let lock = NSRecursiveLock()
lock.lock()
lock.unlock()

4.3 NSCondition
let condition = NSCondition()
condition.lock()
condition.wait()
condition.signal()//condition.broadcast()
condition.unlock()

4.4 NSConditionLock

这个是NSCondition 的进一步封装,该锁允许我们在锁中设定条件具体条件值,有了这个功能,我们可以更加方便的多条线程的依赖关系和前后执行顺序


下面用一个场景来模拟下顺序控制的功能,有三条线程执行A,B,C三个方法,要求按A,C,B的顺序执行

@IBAction func conditionLockTest(_ sender: Any) {

       let threadA = Thread(target: self, selector: #selector(A), object: nil)

        threadA.name = "ThreadA"

        threadA.start()

       let threadB = Thread(target: self, selector: #selector(B), object: nil)

        threadB.name = "ThreadB"

        threadB.start()

       let threadC = Thread(target: self, selector: #selector(C), object: nil)

        threadC.name = "ThreadC"

        threadC.start()

    }

    @objc func A() {

        conditionLock.lock()

        print("A")

        sleep(UInt32(1))

        conditionLock.unlock(withCondition: 3)

    }

    @objc func B() {

        conditionLock.lock(whenCondition: 2)

        print("B")

        sleep(UInt32(1))

        conditionLock.unlock()

    }

    @objc func C() {

        conditionLock.lock(whenCondition: 3)

        print("C")

        conditionLock.unlock(withCondition: 2)

    }

输出结果为:

A

C

B

5. dispatch_semaphore


信号量 的初始值可以用来控制线程并发访问的最大数量,初始值为1,表示同时允许一条线程访问资源,这样可以达到线程同步的目的

  • 创建信号量:dispatch_semaphore_create(value)

  • 等待:dispatch_semaphore_wait(semaphore, 等待时间) 信号量的值 <= 0,线程就休眠等待,直到信号量 > 0,如果信号量的值 > 0,则就将信号量的值递减1,继续执行下面的程序

  • 信号量值+1: dispatch_semaphore_signal(semaphore)


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

【iOS】高效调试 iOS APP 的 UI

iOS
调试是程序是开发过程中必不可少的环节,每当我们完成一段代码或者发现一些问题都需要对程序进行调试。高效的调试能帮我们节省大量的开发时间。这篇文章我将分享一些提高调试效率的工具和它们的使用方法。 在开发iOS APP的时候我们最频繁进行调试的莫过于UI了。 一、U...
继续阅读 »

调试是程序是开发过程中必不可少的环节,每当我们完成一段代码或者发现一些问题都需要对程序进行调试。高效的调试能帮我们节省大量的开发时间。这篇文章我将分享一些提高调试效率的工具和它们的使用方法。


在开发iOS APP的时候我们最频繁进行调试的莫过于UI了。


一、UI的调试


开发中我们经常需要多次修改UI元素的样式进行微调,查看效果并确定正确的数值。

Xcode

如下图所示,Xcode 提供了完备的UI调试工具。




在左边,我们可以看到完整对视图树,中间有各个视图对3D拆分展示,右边,可以看到当前选中的视图的一些信息。


Xcode在进行UI调试的时候,会暂停APP,视图的信息也只能查看不能方便的修改。在UI调试的时候需要修改代码然后重新编译运行才能看到最终的效果。


在频繁调试UI样式的时候是很耗费时间的(如果电脑性能非常好可能会耗费的时间可能会短一些)所以这不是最佳的选择。

LookIn

在这里向大家介绍一款视图调试工具Lookin,它是由腾讯的QMUI团队开发并开源的一款免费的UI调试工具。


有了它,我们就能进行高效的UI调试。


使用方法也非常简单,具体可以查看官方的集成指导


接下来我将分几点简单的介绍一下这个工具的强大功能。

查看与修改UI

Lookin 可以查看与修改 iOS App 里的 UI 对象,类似于 Xcode 自带的 UI Inspector 工具,不需要重新编译运行。而且借助于“控制台”和“方法监听”功能,Lookin 还可以进行 UI 之外的调试。



独立运行
此外,虽然 Lookin 主体是一款 macOS 程序,它亦可嵌入你的 iOS App 而单独运行在 iPhone 或 iPad 上。



显示变量名
Lookin 会显示变量名,以及 indexPath 等各种提示。



显示手势
添加了手势的 UIView,或添加了 Target-Action 的 UIControl,左侧都会被加上一个小蓝条,点击即可查看信息或调试手势



测距
按住 Option 键,即可测量任意两个 view 之间的距离



导出文件

通过手机或电脑将当前 iOS App 的 UI 结构导出为 Lookin 文件以备之后查看,或直接转发给别人。
当测试发现BUG时可以完美对固定现场,并可以将文件发送给开发者查看当时的视图结构。


二、热重载


💉Injection III


Lookin已经帮我们解决了很多问题,但当我们修改了代码的业务逻辑,或者修改了UI的加载逻辑,或者对代码进行了比较大的改动,此时还是需要重新编译运行才能使新的代码生效。同样会耗费许多时间编译、重新运行、点击屏幕到达刚才修改的页面的时间。


这个时候就是我们的第二款高效开发的得力助手登场的时候了。


它就是 💉 Injection III,一款开源免费的热重载工具。


Injection III 是一款能在iOS开发时实现类似Web前端那样热重载的工具。他会监听代码文件的变化,当代码发生改变,他会将改变的部分自动编译成一个动态链接库,然后动态的加载到程序中,达到不重启APP直接热重载的目的。


下面我简单介绍一下如何使用它。


我们可以在 Mac App Store 上下载InjectionIII。打开后会在状态栏有一个蓝色的注射器图标,选择Open Project 打开工程所在目录开始监听我们的文件更改。




接下来在工程中进行一些配置,


Xcodebuild settingOther Linker Flags 中添加-Xlinker -interposable


AppDelegateapplicationDidFinishLaunching方法中加入如下代码:

#if DEBUG
//for iOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
//for tvOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load()
//Or for macOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
#endif

接下来,编译运行你的APP,此时控制台会打印Injection的相关信息




同时状态栏的图标也会变为红色。此时说明Injection启动成功。


接下来你就可以修改一下代码,并保存,Injection会自动编译并自动将其注入到模拟器中运行的APP。控制台也会打印相关的信息。




同时,它会为被注入的类提供一个回调@objc func injected() ,当某个类被注入时,会调用该方法。


我们可以在这里刷新UI,就能做到实时更新UI了。


注意事项


虽然Injection很强大,但它也有很多限制:

  • 你可以修改class、enum、struct 的成员方法的实现,但如果是inline函数则不行,如果有这种情况需要重新编译运行。

  • 它只支持模拟器,不支持真机。

  • 你不能修改class、enum、struct的布局(即成员变量和方法的顺序),如果有这种情况需要重新编译运行。

  • 你不能增加或删除存储类型的属性和方法,如果有这种情况需要重新编译运行。


更多详情可以参见官方的说明:InjectionIII/README.md at main · johnno1962/InjectionIII (github.com)


虽然 Injection III 有很多限制,但它依然能为我们带来非常大的效率提升。


另一个热重载神器: krzysztofzablocki/Inject


krzysztofzablocki/Inject: Hot Reloading for Swift applications! (github.com)


它配合 Injection III 可以更方便的实现热重载和界面自动刷新,实现类似Swift UI的自动刷新效果,但是,它只支持Swift,并且通过Swift Package Manager进行安装。


三、写在最后


实用的工具很多,找到一款既强大又好用的工具,并且把它用好能够很大的提升我们开发的效率。


希望大家能喜欢我分享的这两款工具,希望它们能为大家带来效率的提升。


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

iOS UITableView 图片刷新闪烁问题记录

iOS
一. 问题背景 项目中遇到一个问题,就是当App不在首页的时候,切换到其他App比如微信,然后返回App当前页面,然后从当前页面返回首页,会在首页viewWillAppear这里去拉取是否有未完成订单的接口,刷新UITableView,这时会出现广告位闪烁问题...
继续阅读 »

一. 问题背景


项目中遇到一个问题,就是当App不在首页的时候,切换到其他App比如微信,然后返回App当前页面,然后从当前页面返回首页,会在首页viewWillAppear这里去拉取是否有未完成订单的接口,刷新UITableView,这时会出现广告位闪烁问题。




二. 问题排查


1.原因分析


这个问题经过断点调试和排除法,发现只要当App进入后台后,回来刷新首页的UITableView都有可能出现闪烁现象。


因此首先我们对图片的加载做延迟操作,并在Cell生成方法调用里面添加相关打印:






可以看到如下打印日志:




从打印日志我们可以看出来,调用reloadData方法后,原来UITableViewcell位置会调整。


但是如果我们App没有进入后台,而是直接调用UITableViewreloadData方法,并不会出现闪烁现象。


因此可以这里可以推测应该是进入后台做了什么操作导致,回到App刷新才会导致闪烁。


因为使用的是SDWebImage加载框架加载,我们合理的怀疑是加载图片的SDWebImage框架,进入后台的处理逻辑导致的,因此我们先使用imageCacheDict字典写下图片加载和缓存逻辑:




经测试,进入后台,再返回App刷新不会出现闪烁现象。


因此可以肯定UITableView调用reloadData方法闪烁原因是SDWebImage,在进入后台的时候对内存缓存做了相关操作导致。


我们都知道SDWebImage,默认是使用NSCache来做内存缓存,而NSCache在进入后台的时候,默认会清空缓存操作,导致返回App调用UITableView调用reloadData方法时候,SDWebImage需要根据图片地址重新去磁盘获取图像数据,然后解压解码渲染,因为是从缓存磁盘直接获取图像数据,没有渲染流程,因此会造成闪烁。


为了验证这个猜想,我们使用YYWebImage加载框架来做对比实验:

首先注释掉YYWebImage进入后台清空内存缓存的逻辑: 


然后进入后台,返回App调用UITableView调用reloadData刷新,发现一切正常。

原因总结:

  • 第一个原因是UITableView调用reloadData方法,由于UITableViewCell的复用,会出现Cell位置调整现象

  • 由于SDWebImage使用了NSCache做内存缓存,当App进入后台,NSCache会清空内存缓存,导致返回App后调用UITableView调用reloadData,刷新去加载图片的时候,需要从SDWebImage的磁盘中重新获取图片数据,然后重新解压解码渲染,因为从磁盘中读取速度快,两者原因导致了闪烁。


三. 解决方案


因为该现象是由如上两个原因导致,因此针对这两个原因,有如下两种解决方案:

1. 解决UITableViewCell复用问题


可以通过设置ReusableCellWithIdentifier不同,保证广告cell不进行复用。

 NSString *cellId = [NSString stringWithFormat:@"%ld-%ld-FJFAdTableViewCell", indexPath.section, indexPath.row];

2. 从后台返回后,提早进行刷新操作

当从后台返回App前台的时候或者视图添加到父视图的时候,先执行下UITableView调用reloadData方法,提前通过SDWebImage去从磁盘中加载图片。


从后台返回前台:

[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(willEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
- (void)willEnterForeground {
[self.tableView reloadData];
NSLog(@"--------------------------willEnterForeground");
}

视图添加到父视图:

- (void)willMoveToParentViewController:(UIViewController *)parent {
[self.tableView reloadData];
}

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

RunLoop:iOS开发中的神器,你真的了解它吗?

iOS
在iOS开发中,RunLoop是一个非常重要的概念,它提供了一个事件循环机制,用于处理各种事件,例如用户交互、网络请求、定时器等等。RunLoop不仅是iOS开发中的核心之一,而且在其他平台的开发中也有广泛的应用。本文将为您介绍Swift中RunLoop的基本...
继续阅读 »

iOS开发中,RunLoop是一个非常重要的概念,它提供了一个事件循环机制,用于处理各种事件,例如用户交互、网络请求、定时器等等。RunLoop不仅是iOS开发中的核心之一,而且在其他平台的开发中也有广泛的应用。本文将为您介绍SwiftRunLoop的基本概念和使用方法。


什么是RunLoop?


RunLoop是一个事件循环机制,它用于在iOS应用程序中处理各种事件。RunLoop在应用程序的主线程中运行,它负责管理该线程中的事件,并确保UI更新等重要任务能够顺利执行。RunLoop还负责处理其他线程发送的事件,例如网络请求等等。


RunLoop的基本思想是循环地处理事件。当RunLoop启动时,它会进入一个无限循环,等待事件的发生。当有事件发生时,RunLoop会调用相应的处理方法来处理该事件,并继续等待下一个事件的发生。RunLoop会一直运行,直到被手动停止或应用程序退出。


RunLoop与线程


iOS中,每个线程都有一个RunLoop,但默认情况下,RunLoop是被禁用的。要使用RunLoop,必须手动启动它,并将其添加到线程的运行循环中。


例如,要在主线程中使用RunLoop,可以使用如下代码:

RunLoop.current.run()

这将启动主线程的RunLoop,并进入一个无限循环,等待事件的发生。


RunLoop模式


RunLoop模式是RunLoop的一个重要概念,它定义了RunLoop在运行过程中需要处理的事件类型。一个RunLoop可以有多个模式,但在任何时刻只能处理一个模式。每个模式都可以包含多个输入源(input source)和定时器(timer)RunLoop会根据当前模式中的输入源和定时器来决定下一个事件的处理方式。


RunLoop提供了几个内置模式,例如:

  1. NSDefaultRunLoopMode:默认模式,处理所有UI事件、定时器和PerformSelector方法。
  2. UITrackingRunLoopMode:跟踪模式,只处理与界面跟踪相关的事件,例如UIScrollView的滚动事件。
  3. NSRunLoopCommonModes:公共模式,同时包含NSDefaultRunLoopModeUITrackingRunLoopMode。 RunLoop还允许开发者自定义模式,以满足特定需求。

定时器


iOS开发中,定时器是一种常见的事件,例如每隔一段时间刷新UI、执行后台任务等等。RunLoop提供了定时器(timer)机制,用于在指定时间间隔内执行某个操作。


例如,要在主线程中创建一个定时器并启动它,可以使用如下代码:

let timer = Timer(timeInterval: 1.0, repeats: true) { timer in // 定时器触发时执行的操作 } RunLoop.current.add(timer, forMode: .common)

这将创建一个每隔1秒钟触发一次的定时器,并在公共模式下添加到主线程的RunLoop中。


在添加定时器时,需要指定它所属的RunLoop模式。如果不指定模式,则默认为NSDefaultRunLoopMode。如果需要在多个模式下都能响应定时器事件,可以使用NSRunLoopCommonModes


输入源


输入源(input source)是一种与RunLoop一起使用的机制,用于处理异步事件,例如网络请求、文件读写等等。RunLoop在运行过程中,会检查当前模式下是否有输入源需要处理,如果有则会立即处理。


输入源可以是一个Port、Socket、CFFileDescriptor等等。要使用输入源,必须将其添加到RunLoop中,并设置回调函数来处理输入事件。


例如,要在主线程中使用输入源,可以使用如下代码:

let inputSource = InputSource()
inputSource.setEventHandler {
// 输入源触发时执行的操作
}
RunLoop.current.add(inputSource, forMode: .common)

这将创建一个输入源,并在公共模式下添加到主线程的RunLoop中。


Perform Selector


Perform Selector是一种调用方法的方式,可以在RunLoop中异步执行某个方法。在调用方法时,可以设置延迟执行时间和RunLoop模式。该方法会在指定的时间间隔内执行,直到被取消。


例如,要在主线程中使用Perform Selector,可以使用如下代码:

RunLoop.current.perform(#selector(doSomething), target: self, argument: nil, order: 0, modes: [.default])

这将在默认模式下异步执行doSomething方法。


RunLoop的常用操作


除了上述基本操作之外,RunLoop还提供了其他常用操作,例如:

  1. stop:停止RunLoop的运行。
  2. runUntilDate:运行RunLoop直到指定日期。
  3. runMode:运行RunLoop指定模式下的事件处理循环。
  4. currentMode:获取当前RunLoop的运行模式。

RunLoop与线程安全


iOS开发中,多线程是一个常见的问题。RunLoop在处理异步事件时,可能会导致线程不安全的问题。为了保证RunLoop的线程安全,可以使用以下方法:

  1. 使用RunLoopQueue,在队列中使用RunLoop来执行异步操作。
  2. 在主线程中使用RunLoop来处理异步事件,避免跨线程操作。

结论


RunLoopiOS开发中非常重要的一个概念,它提供了一种事件循环机制,用于处理各种事件。RunLoop的基本思想是循环地处理事件,当有事件发生时,RunLoop会调用相应的处理函数来处理事件。RunLoop还提供了定时器、输入源、Perform Selector等机制来处理异步事件。了解RunLoop的工作原理,可以帮助我们更好地理解iOS应用的运行机制,避免出现一些奇怪的问题。


最后,我们再来看一下RunLoop的一些注意事项:

  1. 不要在主线程中阻塞RunLoop,否则会导致UI卡顿。
  2. 避免使用RunLoopNSDefaultRunLoopMode模式,因为这个模式下会处理大量UI事件,可能会导致其他事件无法及时处理。
  3. 在使用RunLoop的过程中,需要注意线程安全问题。

RunLoop是一种事件循环机制,通过它,我们可以很方便地处理各种事件,避免出现一些奇怪的问题。在日常开发中,我们需要经常使用RunLoop,所以建议大家多多练习,掌握RunLoop的各种用法。


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

作为一个前端为什么要学习 Rust ?

这里抛出一个问题 作为一个前端为什么要去学习 Rust ? 这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言? 那么今天...
继续阅读 »

这里抛出一个问题


作为一个前端为什么要去学习 Rust ?


这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言?


那么今天我就谈谈我自己对于这个问题的看法~,主要是分为 5 点:

  • 性能
  • 跨平台特性
  • 安全性
  • 职业视野
  • 职业竞争力

性能


Rust 可以给 Node.js 提供一个性能逃生通道,当我们使用 Node.js 遇到性能瓶颈或 CPU 密集计算场景的时候,便可以使用 Rust 编写 Native Addon 解决这个问题了,Native Addon 就是一个二进制文件,也就是 xxx.node 文件,比如 swc(对应 babel)、Rspack(对应Webpack)、Rome(对应 eslint、prettier、babel、webpack 等,目标是代替我们所熟悉的所有前端工具链...),上面提到的工具链就都是使用 Rust 编写的,性能比 Node.js 对应功能的包都有了极大的提高,同时 Rust 也是支持多线程的,你编写的多线程代码在 Node.js 中一样可以跑,这就可以解决了 Node.js 不擅长 CPU 密集型的问题。在前端架构领域目前 Rust 已经差不多是标配了,阿里、字节内部的前端基建目前都开始基于 Rust 去重构了。


跨平台


可以编写高性能且支持跨平台的 WebAssembly 扩展,可以在浏览器、IOT 嵌入式设备、服务端环境等地方使用,并且也拥有很不错的性能;和上面提到的 Native Addon 不一样, Native Addon 在不同的平台上都需要单独的进行编译,不支持跨平台;但是 WebAssembly 不一样,虽然它的性能没 Native Addon 好,但是跨平台成本很低,我编写的一份代码在 Node.js 中执行没问题,在 Deno 中跑也没问题,在 Java 或者 Go 中跑也都没问题,甚至在单片机也可以运行,只要引入对应的 Wasm 运行时即可。现在 Docker 也已经有 WebAssembly 版本了;同时 Rust 也是目前编写 WebAssembly 最热门的语言,因为它没有垃圾回收,性能高,并且有一个超好用的包管理器 cargo。


安全


Rust 编译器真的是事无巨细,它保证你编写的代码不会出低级错误,比如一些类型上的错误和内存分配上的错误,基本上只要 Rust 代码能够编译通过,就可以安心上线,在服务端、操作系统等领域来说这也是个很好的特性,Linux 系统和安卓系统内核都已经开始使用 Rust ,这还信不过嘛?


视野


Rust 可以提升自己在服务端领域的视野,Rust 不同于 Node.js 这个使用动态 JS 语言的运行时,它是一门正儿八经的静态编译型编程语言,并且没有垃圾回收,可以让我们掌握和理解计算机的一些底层工作机制,比如内存是如何分配和释放的,Rust 中使用所有权、生命周期等概念来保证内存安全,这对我们对于编程的理解也可以进一步提升,很多人说学习了 Rust 之后对自己编写其它语言的代码也有了更深的理解,毕竟计算机底层的概念都是相通的,开阔自己的编程思维。


职业竞争力


这个问题简单,你比别人多一门技能,比如 WebAssembly 和 Native Addon 都可以作为 Node.js 性能优化的一种手段,面试的时候说你会使用 Rust 解决 Node.js 性能问题,这不是比别人多一些竞争力吗?面试官那肯定也会觉得你顶呱呱~ 另外虽然目前 Rust 的工作机会比较少,但是也不代表没有,阿里和字节目前都有关于前端基建的岗位,会 Rust 是加分项,另外 Rust 在 TIOBE 编程语言榜排名中已经冲进了前 20,今年 6 月份是第 20 名,7 月份是第 17 名,流行度开始慢慢上来了,我相信以后工作机会也会越来越多的。


总结


不过,总的来说,这还是得看自己个人的学习能力,学有余力的时候可以学习一下 Rust,我自己不是 Rust 吹啊,我学习 Rust 的过程中真的觉得很有趣,因为里面的很多概念在前端领域中都是接触不到的,学了之后真的像是打开了新世界的大门,包括可以去看 Deno 的源码了,可以了解到一个 Js 运行时是怎么进行工作的,这些都是与我们前端息息相关的东西,即使哪天不做前端了,可以去转服务端或嵌入式方向,起码编程语言这一关不需要费多大力气了,Rust 是目前唯一一门从计算机底层到应用层都有落地应用的语言。不多说了,学就完事了,技多不压身嘛


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

只改了五行代码接口吞吐量提升了10多倍

背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想,500/s吞吐量还不简单。Tomcat按照100个线...
继续阅读 »

背景


公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。


当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。


然而压测一开,100 的并发,吞吐量居然只有 50 ...


image.png


而且再一查,100的并发,CPU使用率居然接近 80% ...




从上图可以看到几个重要的信息。


最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!


最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!


再一看百分位,大部分的请求响应时间都在4s。无语了!!!


所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!


分析过程


定位“慢”原因



这里暂时先忽略 CPU 占用率高的问题



首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:



  • 锁 (同步锁、分布式锁、数据库锁)

  • 耗时操作 (链接耗时、SQL耗时)


结合这些先配置耗时埋点。



  1. 接口响应时长统计。超过500ms打印告警日志。

  2. 接口内部远程调用耗时统计。200ms打印告警日志。

  3. Redis访问耗时。超过10ms打印告警日志。

  4. SQL执行耗时。超过100ms打印告警日志。


上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:


<!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
<!-- 压测时可以认为 type = 1 是写死的 -->
update table set field = field - 1 where type = 1 and filed > 1;

上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。


二话不说先改为敬。因为是压测环境,直接先改为异步执行,确认一下效果。实际解决方案,感兴趣的可以参考另外一篇文章:大量请求同时修改数据库表一条记录时应该如何设计


PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...


优化后的效果:


image.png


嗯...


emm...


好! 这个优化还是很明显的,提升提升了近2倍。




此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。


最大值: 已经从 5s -> 2s


百分位值: 4s -> 1s


这已经是很大的提升了。


继续定位“慢”的原因


通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。


我们继续看日志,此时日志出现类似下边这种情况:


2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?



  1. 发生了线程切换,换其他线程执行其他任务了。(线程太多了)

  2. 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)

  3. STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)


按照这三个思路做了以下操作:


首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。


然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。


最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。


唉,一顿操作猛如虎。


PS:其实中间还对数据库参数一通瞎搞,这里不多说了。




其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。


此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。


定位CPU使用率高的原因


CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:



  1. 有额外的线程存在。

  2. 代码有部分CPU密集操作。


然后继续一顿操作:



  1. 观察服务活跃线程数。

  2. 观察有无CPU占用率较高线程。


在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。


image.png


没有很高就证明大家都很正常,只是多而已...


此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~


在看的过程中发现这段日志:


"http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
java.lang.Thread.State: RUNNABLE
at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
......
......

上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。


而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。


通过堆栈信息很快定位到执行位置:


<!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

而RedisMaster类


@Component
@Scope("prototype")
public class RedisMaster implements IRedisTool {
// ......
}

没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...


叹气!!!


赶紧改代码,直接使用万能的 new 。


在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:


long start = System.currentTimeMillis();
// ......
long end = System.currentTimeMillis();
long runTime = start - end;


或者Hutool提供的StopWatch:


这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。


StopWatch watch = new StopWatch();
watch.start();
// ......
watch.stop();
System.out.println(watch.getTotalTimeMillis());

而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。





最终结果:



image.png





排查涉及的命令如下:



查询服务进程CPU情况: top –Hp pid


查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)


打印当前堆栈信息: jstack -l pid >> stack.log


总结


结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是事后臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。



  • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...

  • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...

  • JVM : 内存大小,分配,垃圾收集器都想换...


总归一通瞎搞,能想到的都试试。


后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。




最后5行代码有哪些:



  1. new Redis实例:1

  2. 耗时统计:3

  3. SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)


TODO


问题虽然解决了。但是原理还不清楚,需要继续深挖。



为什么createBean对性能影响这么大?



如果影响这么大,Spring为什么还要有多例?


首先非并发场景速度还是很快的。这个毋庸置疑。毕竟接口响应时间不足50ms。


所以问题一定出在,并发createBean同一对象的锁等待场景。根据堆栈日志,翻了一下Spring源码,果然发现这里出现了同步锁。相信锁肯定不止一处。


image.png


org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


image.png



System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?



很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。



继续学习性能优化知识




  • 吞吐量与什么有关?


首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。


其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。


最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。



  • CPU使用率的高低与哪些因素有关?


CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。


假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。


此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。



  • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20
    作者:FishBones
    来源:juejin.cn/post/7185479136599769125
    左右。

收起阅读 »

前段时间面试了一些人,有这些槽点跟大家说说

大家好,我是拭心。 前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。 简历书写和自我介绍 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备 去年工...
继续阅读 »

大家好,我是拭心。


前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。


image.png


简历书写和自我介绍



  1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备


image.png




  1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了




  2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求




  3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色




  4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多




  5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!




  6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)




  7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点




  8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说




  9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到




  10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点




  11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略




  12. 你可以这样审视自己的简历和自我介绍:


    a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点


    b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考


    c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点




面试问题


image.png




  1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质




  2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任




  3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质




项目经历


项目经历就是我们过往做过的项目。


项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。


有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?


大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。


image.png


在项目经历上,面试者常见的问题有这些:



  1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

  2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

  3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)


出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。


在看面试者的项目经历时,面试官主要关注这三点:


1. 之前做的项目有没有难度


2. 项目经验和当前岗位需要的是否匹配


3. 经过这些项目,这个人的能力有哪些成长


因此,我们在日常工作和准备面试时,可以这样做:



  1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

  2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

  3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

  4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

  5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

  6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

  7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验


技能知识点


技能知识点就是我们掌握的编程语言、技术框架和工具。


相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。


image.png


在技能知识点方面,面试者常见的问题有这些:



  1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

  2. 技术不对口:没有岗位需要的领域技术

  3. 技术过剩:能力远远超出岗位要求


第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。


第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。


第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。


在我面试的人里,通过面试的都有这些特点:



  1. 技术扎实:不仅仅基础好,还有深度

  2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事


有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?


软素质


这里的「软素质」指面试时考察的、技术以外的点。


程序员的日常工作里,除了写代码还需要做这些事:



  1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

  2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

  3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


image.png


因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:



  1. 理解能力和沟通表达能力

  2. 业务能力

  3. 稳定性


第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。


第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。


业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。


遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。


第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。


针对以上这三点,我们可以这样做:



  1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

  2. 回答问题时有逻辑条理,可以采用类似总分总的策略

  3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

  4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标


总结


好了,这就是我前段时间面试的感悟和吐槽。


总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。

作者:张拭心
来源:juejin.cn/post/7261604248319918136

收起阅读 »

揭秘:Android屏幕中你不知道的刷新机制

前言 之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了: 16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时...
继续阅读 »

前言


之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了:


16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时机调用是不同的,如果操作是在16.6ms快结束的时候去绘制的,那么岂不是就是时间少于16.6ms,也会产生丢帧的问题?再者熟悉绘制的朋友都知道请求绘制是一个Message对象,那这个Message是会放进主线程Looper的队列中吗,那怎么能保证在16.6ms之内会执行到这个Message呢?


View ## invalidate()


既然是绘制,那么就从这个方法看起吧


public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate
) {
......
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
.....
}
}

主要关注这个p,最终调用的是它的invalidateChild()方法,那么这个p到底是个啥,ViewParent是一个接口,那很明显p是一个实现类,答案是ViewRootImpl,我们知道View树的根节点是DecorView,那DecorView的Parent是不是ViewRootImpl呢


熟悉Activity启动流程的朋友都知道,Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView()最终其实是调用了 WindowManagerGlobal 的 addView() 方法,我们就从这里开始看,因为是隐藏类,所以这里借助Source Insight查看WindowManagerGlobal


public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow
) {
synchronized (mLock) {
.....
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
....
view.assignParent(this);
...
}
}
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
}
}

参数是ViewParent,所以在这里就直接将DecorView和ViewRootImpl给绑定起来了,所以也验证了上述的结论,子 View 里执行 invalidate() 之类的操作,最后都会走到 ViewRootImpl 里来


ViewRootImpl##scheduleTraversals


根据上面的链路最终是会执行到scheduleTraversals方法


void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
复制代码方法不长,首先如果mTraversalScheduled为false,进入判断,同时将此标志位置位true,第二句暂时不管,后续会讲到,主要看postCallback方法,传递进去了一个mTraversalRunnable对象,可以看到这里是一个请求绘制的Runnable对象
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

doTraversal方法里面,又将mTraversalScheduled置位了false,对应上面的scheduleTraversals方法,可以看到一个是postSyncBarrier(),而在这里又是removeSyncBarrier(),这里其实涉及到一个很有意思的东西,叫同步屏障,等会会拉出来单独讲解,然后调用了performTraversals(),这个方法应该都知道了,View 的测量、布局、绘制都是在这个方法里面发起的,代码逻辑太多了,就不贴出来了,暂时只需要知道这个方法是发起测量的开始。


这里我们暂时总结一下,当子View调用invalidate的时候,最终是调用到ViewRootImpl的performTraversals()方法的,performTraversals()方法又是在doTraversal里面调用的,doTraversal又是封装在mTraversalRunnable之中的,那么这个Runnable的执行时机又在哪呢


Choreographer##postCallback


回到上面的scheduleTraversals方法中,mTraversalRunnable是传递进了Choreographer的postCallback方法之中


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis
) {
if (DEBUG_FRAMES) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

可以看到内部好像有一个类似MessageQueue的东西,将Runnable通过delay时间给存储起来的,因为我们这里传递进来的delay是0,所以执行scheduleFrameLocked(now)方法


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
private boolean isRunningOnLooperThreadLocked() {
return Looper.myLooper() == mLooper;
}

这里有一个判断isRunningOnLooperThreadLocked,看着像是判断当前线程是否是主线程,如果是的话,调用scheduleVsyncLocked()方法,不是的话会发送一个MSG_DO_SCHEDULE_VSYNC消息,但是最终都会调用这个方法


public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}

如果mReceiverPtr不等于0的话,会去调用nativeScheduleVsync(mReceiverPtr),这是个native方法,暂不跟踪到C++里面去了,看着英文方法像是一个安排信号的意思 之前是把CallBack存储在一个Queue之中了,那么必然有执行的方法


void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
if (DEBUG_FRAMES) {
Log.d(TAG, "RunCallback: type=" + callbackType
+ ", action=" + c.action + ", token=" + c.token
+ ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
}
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

看一下这个方法在哪里调用的,走到了doFrame方法里面


void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
try {
.....
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
.....
}

那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl 封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals() 开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements
Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}

@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
scheduleVsync();
return;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

可以看到,是在onVsync回调中,Message msg = Message.obtain(mHandler, this),传入了this,然后会执行到run方法里面,自然也就执行了doFrame方法,所以最终问题也就来了,这个onVsync()这个是什么时候回调来的,这里查询了网上的一些资料,是这么解释的



FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。



那也就是说,onVsync是底层回调回来的,那也就是每16.6ms,底层会发出一个屏幕刷新的信号,然后会回调到onVsync方法之中,但是有一点很奇怪,底层怎么知道上层是哪个app需要这个信号来刷新呢,结合日常开发,要实现这种一般使用观察者模式,将app本身注册下去,但是好像也没有看到哪里有往底层注册的方法,对了,再次回到上面的那个native方法,nativeScheduleVsync(mReceiverPtr),那么这个方法大体的作用也就是注册监听了,


同步屏障


总结下上面的知识,我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过 performTraversals() 遍历绘制 View 树来执行这些刷新操作。


那么这样是不是产生一个问题,因为我们知道,平常Handler发送的消息都是同步消息的,也就是Looper会从MessageQueue中不断去取Message对象,一个Message处理完了之后,再去取下一个Message,那么绘制的这个Message如何尽量保证能够在16.6ms之内执行到呢,


这里就使用到了一个同步屏障的东西,再次回到scheduleTraversals代码


void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

mHandler.getLooper().getQueue().postSyncBarrier(),这一句上面没有分析,进入到方法里


private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

可以看到,就是生成Message对象,然后进一些赋值,但是细心的朋友可能会发现,这个msg为什么没有设置target,熟悉Handler流程的朋友应该清楚,最终消息是通过msg.target发送出去的,一般是指Handler对象


那我们再次回到MessageQueue的next方法中看看


Message next() {
for (;;) {
....
synchronized (this) {
...
//对,就是这里了,target==null
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}
}
}

可以看到有一个Message.target==null的判断, do while循环遍历消息链表,跳出循环时,msg指向离表头最近的一个消息,此时返回msg对象


可以看到,当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息


这样的话,能够尽快的将绘制的Message给取出来执行,嗯,这里为什么说是尽快呢,因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行

作者:花海blog
来源:juejin.cn/post/7267528065809907727

收起阅读 »

毕业两年,我的生活发生了翻天覆地的变化

去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。 我学会接受了自己的平凡 大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减...
继续阅读 »

去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。


c129b212a0f596a998f904adaf8772c.jpg
我学会接受了自己的平凡


大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减少,于是校招、找工作的时候就认清现实,好像是个offer就去。


毕业后,我从中选了最满意的一个offer,前往深圳。


在公司的一年,我浑浑噩噩,每天感觉时间像不够用似的。每天升级打怪,学到了很多,后面因为公司业务原因,跳槽了。但是很开心的是在这里认识到了很多小伙伴,大家现在也有联系,时不时出来喝个酒。也和公司技术很牛的大佬成了朋友。也学会接受了自己的平凡,原来真的有那种写代码很轻松,把写代码当游戏,把工作当乐趣的人呀,真的是降维打击我这个小菜鸡。


78840c794394308c40b286f3321073b.jpg


人生就是不断的坍塌,然后自我重建。


一年过去了,我好像没了刚出社会的冲劲,偶尔下班也会学习,但是没有像刚毕业一样有很多的学习热情,闲暇时间就会多刷技术贴,技术文章。


跳槽跑路了,结果我发现我从刀山跑到了火海。入职后我才知道我所在部门的前端再过几天就要跑路了,相当于就我一个啥也不熟悉的来接锅了。组长带人和我交接时用了两小时,然后留下一脸蒙蔽的我。总之,后面度过了艰难的两个月,好歹算是背着锅缓慢前行。后面公司又出了不少幺蛾子,挺了7个月,忍不了又跑路了。但是这几个月吃的苦也让我的工作能力上涨,技能增多,抗压能力增强。于是我发现“人生就是不断的坍塌,然后自我重建,最后堆成了现在的我”。


相亲也不是不行


话说我年纪也不大,但是不知道为什么毕业两年,时间飞逝,居然就开始有点年龄焦虑。工作后也没什么渠道去认识女孩子。办公室一屋子男生,问他们对象哪儿来的,都说自己的对象是new来的。


也不是没被家里人拉去相亲过,第一年我觉得自己还小,也考虑到在家乡相亲的岂不是要异地啊,无比抗拒。第二年我成熟了,(不是,被毒打了)发现工作后是真难找对象啊。


转折点在某个风和日丽的下午,大数据都知道我单身了。我刷脉脉看到了相亲贴,然后知道了大厂相亲角公众号这个平台,这个公众号标榜用户都是阿里、字节、百度、腾讯、华为等大厂的单身同学。因为注册需要企业认证,最开始不太信任平台,就没注册。先进了他们的单身群观望,后面群里面每天都发一些嘉宾推荐,然后想到这种有门槛的,用户都是经过审核的,岂不是更可靠。感觉确实还蛮靠谱的,于是就注册了。被拉到上墙群后发现上墙群里的群友们都好优秀,小厂的我夹缝求生。


我算是发现了,人的观念是会一直变的,想当初我怎么也想不到自己会去相亲吧。


66b61a33c9903edc332893e26b27945.jpg


总结


毕业两年,我的生活好像变了很多,又好像没啥变化,曾经我不能接受的,现在又行了。曾经觉得自己可以了,现在也认清

作者:苍苍尔
来源:juejin.cn/post/7158708534471819278
现实了。哈哈哈哈哈。

收起阅读 »

卸下if-else 侠的皮衣!- 状态模式

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


之前文章学习了**策略模式适配器模式,有兴趣可以过去看看,下面我们来学习适配器模式**


场景:做四种咖啡的咖啡机


- 美式咖啡(american):只吐黑咖啡
- 普通拿铁(latte):黑咖啡加点奶
- 香草拿铁(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡(mocha):黑咖啡加点奶再加点巧克力


用if-else来写,如下


class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/

// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
}

// 关注咖啡机状态切换函数
changeState(state) {
// 记录当前状态
this.state = state;
if(state === 'american') {
// 这里用 console 代指咖啡制作流程的业务逻辑
console.log('我只吐黑咖啡');
} else if(state === 'latte') {
console.log(`给黑咖啡加点奶`);
} else if(state === 'vanillaLatte') {
console.log('黑咖啡加点奶再加香草糖浆');
} else if(state === 'mocha') {
console.log('黑咖啡加点奶再加点巧克力');
}
}
}


分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


class CoffeeMaker {
constructor() {
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
// 初始化牛奶的存储量
this.leftMilk = '500';
}
stateToProcessor = {
that: this,
american() {
this.that.leftMilk = this.that.leftMilk - 100
console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
console.log('吐黑咖啡');
},
latte() {
this.american()
console.log('加点奶');
},
vanillaLatte() {
this.latte();
console.log('再加香草糖浆');
},
mocha() {
this.latte();
console.log('再加巧克力');
}
}

changeState(state) {
this.state = state;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}

const mk = new CoffeeMaker();
mk.changeState('latte');
mk.changeState('mocha');



这个状态模式实际上跟策略模式很像,但是状态模式会关注里面的状态变化,就像上诉代码能检测咖啡牛奶量,去除了if-else,能很好的扩展维护



结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加油,骚年们!给我点点赞,关注下!

作者:向乾看
来源:juejin.cn/post/7267207014382829579

收起阅读 »

老板搞这些事情降低我写码生产力,我把他开除了

web
Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Ki...
继续阅读 »

Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Kill Developer Productivity,不敢私藏干货,赶紧来分享给大家,希望能共同避免



简介


几周前,我突然发现:在工作 4 小时内,我的工作时间和有效的编码时间差了整整 2 小时。为了重回正轨,我决定主动减少阻碍,来缩小这个差距,争取能写更多代码,把无关的事情抛开。这个时间差越大,我的效率就越低。


和其他行业的人相比,程序员在这方面遇到的困境更甚。这些障碍往往会导致码农信心下降、写代码和优化的时间变少,职业倦怠率更高。影响创造力和热情。


根据本周 CodeTime 的全球平均编码时间,约 45%的总编码时间都是消极编码。时间和资金都在被浪费。


低效的开发流程是罪魁祸首。


1. 会议


会议


低效的会议是导致开发人员生产力下降的最不必要的因素之一。编程需要心流。平均而言,进入心流状态大约需要 30 分钟。但是由于乱七八糟会议,专注力就被打断了,码农必须重复这个过程。


有时 10 分钟的会议硬拖到一个小时,这也很浪费时间。减少了用于实际编程和解决问题的时间。有些会议还需要码农无用的出席。如果这次会议和码农的专业知识无关,根本没必要让他们参会。


2. 技术债(Fix it Later)


技术债


技术债,简单来说就是“以后再修”的心态。先采用捷径实现,妄图后面有空再修改成更优的方式。


最开始,先让功能可用就行,而优化留到以后。短期来看这可能行得通,因为它可以加快项目进度,你可能在 deadline 前完成。但是反复这么做就会留下大量待完成的工作。会使维护、扩展和优化软件变得更加困难。


技术债会以多种方式阻碍码农的生产力。列举一些:




  • Code Review 的瓶颈:当技术债增加时,会增加 Code Review 所花费的时间。




  • 更多 Bug:由于关注点都在速度而不是优化上,会导致引入隐藏的 Bug。




  • 代码质量降低:只为了让它可以跑,会导致糟糕的代码质量、随意的 Code Review,甚至没有代码注释,随意乱写复杂的代码等。




上述所有点都需要额外的时间来处理。因此,这会拖长项目的时间线。


3. Code Review


Code Review


Code Review 需要时间,如果 Review 时间过长,会延迟新代码的集成并放缓整个开发过程。有时候码农提出 PR 但 Code Reviewer 没有时间进行审查。会码农处理下一任务的时间。在进行下一个任务的同时,再回头 Code Review 时会有上下文切换。会影响码农的专注力和生产力。


对于 Code Review,码农可能不得不参加多个会议,减少了码农的生产力。代码反馈往往不明确或过于复杂,需要进一步讨论来加深理解,解决问题通常需要更长时间。Code Review 对一个组织来说必不可少且至关重要,但是需要注意方式和效率。


4. 微观管理 (Micromanagement)(缺乏自治)


微观管理


微观管理是一种管理方式,主管密切观察和管理下属的工作。在码农的语境下,当管理者想要控制码农的所有编码细节时就发生了。这可能导致码农对他们的代码、流程、决策和创造力的降低。


举例来说:




  • 缺乏动力:微观管理可能表明组织对码农能力的信任不足。这样,码农很容易感到失去动力。




  • 缺乏创造力:开发软件是一项需要专注以探索创造性解决方案的创作性任务。但是微观管理会导致码农对代码的控制较少,阻碍码农的创造力。




  • 决策缓慢:码农必须就简单的决定向管理层寻求确认,在这个过程中大量时间被浪费。




在所有这些情况下,码农的生产力都会下降。


5. 职业倦怠


职业倦怠


职业倦怠是码农面临的主要问题之一。面对复杂具有挑战性的项目和紧迫的 deadline,以及不断提高代码质量的压力都可能导致职业倦怠。这最终会导致码农的生产力下降,会显著减弱码农的注意力和写代码的能力。


这也会导致码农的创造力和解决问题的能力下降。这最终会导致开发周期变慢。


6. 垃圾文档


垃圾文档


文档对码农至关重要,因为它传达有关代码、项目和流程的关键信息。垃圾文档可能会导致开发周期被延迟,因为码农需要花更多时间试图理解代码库、项目和流程。这会导致码农生产力降低。


在码农入职期间,提供垃圾文档会导致码农在设置项目、管理环境、理解代码等任务上花费更多时间。在缺乏清晰文档的情况下,维护和修改现有代码变得困难。由于担心破坏功能,码农可能会犹豫重构或进行更改。因此,码农的生产力将浪费在非核心任务上。


7. 痴心妄想的 Deadline


痴心妄想的Deadline


Deadline 是使码农发疯的原因之一。你必须在较短的时间窗口内完成大量工作时,你会很容易感到沮丧。这可能导致职业倦怠、代码质量差、疏忽 Code Review 等。这将导致技术债的积累。因此,码农的生产力会下降。


Deadline 对计划开发周期是必要的,但是通过设置不切实际的 Deadline 来向码农施加压力,会让他们承受压力。在压力下,整体生产力和代码质量都会下降。


总结


上文提到的会议、技术债积累、拖沓的 Code Review、微观管理、导致职业倦怠的压力、垃圾代码文档以及为项目设置不切实际的 Deadline 等因素会阻碍码农的生产力。我试图阐明软件开发人员在追求效率和创造性解决方案的过程中面临的挑战。


其中的重点是,这些挑战是可以通过在码农的健康和高生产力之间建立平衡来克服的。你可以使用一些码农工具来帮助管理你的生产力、深度专注和工作效率。


下面是一些可以帮助提高生产力的工具:




  • FocusGuard:这是一个 Chrome 扩展,可以通过屏蔽网站帮助你保持专注。




  • Code Time:这是一个 VSCode 扩展,用于跟踪你的编码时间和活动编码时间。




  • JavaScript Booster:这个 VSCode 扩展可以为代码重构提供建议。你也给其他编程语言找找这种扩展。




  • Hatica:虽然上述工具局限于一个任务:专注于编码,但 Hatica 可以处理更多工作。它通过改进工作流程、识别瓶颈和跟踪进度来帮助工程团队提高码农生产力。通过这种方式,它可以给码农节约大量的时间。在这里了解更多关于这个工程管理平台的信息。





作者:ssh_晨曦时梦见兮

来源:juejin.cn/post/7267578376050114614


收起阅读 »

拿到优秀员工了

大家好呀,我是楼仔。 上周拿了个优秀员工,又收获了一枚证书,刚好抽屉里也塞了几个,发现装不下了,准备打包一并带回家。 收拾的时候,旁边的同事看到了,“楼哥,你咋这么多证书,是准备集齐后,召唤神龙么?” 尼玛,他这句话,差点让我笑出声。 后来发现,来小米已经 ...
继续阅读 »

大家好呀,我是楼仔。


上周拿了个优秀员工,又收获了一枚证书,刚好抽屉里也塞了几个,发现装不下了,准备打包一并带回家。


收拾的时候,旁边的同事看到了,“楼哥,你咋这么多证书,是准备集齐后,召唤神龙么?”


尼玛,他这句话,差点让我笑出声。



后来发现,来小米已经 4 年了,刚好一年一个,中间也换了几个部门,但是发现每个部门都能做得还不错,这个其实挺难的,下面就给大家简单分享一下个人经验。


可以再好一点


我们有个小群,除了我是菜鸡以外,其余基本都是大佬,有一次有个大佬在群里问 “大家觉得,在公司工作,什么是你觉得最重要的呢?”


有同学回答是技术,也有回答情绪管理,但是有一位同学的回答,让我印象深刻,就三个字“超预期”。


是的,很多同学都能把事情做到 70 分,甚至 80 分,但是如何才能做到让同事、老板对你印象深刻呢,答案就是超预期。


比如你是一个核心研发,写的代码质量高,甚至可以提前完成,完全不用老板操心,那么就能到 80 分,但是如果你还能带领大家一起把事情做好,慢慢成为项目的领头羊,那么这就是“超预期”。


当然,我们不可能所有事情都做到超预期,甚至我们也不知道有些事情是否能超预期,但是只要我们对自己高要求,反复琢磨,如何才能做得更好一点点,首先让自己满意,然后重要事情能超预期,就非常棒了。


打破边界,持续学习


偶尔让别人觉得你做得好,会比较容易,如果一直让别人觉得你做得好,就很难了。


我举个例子,比如今年你是核心研发,但是别人发现,你居然能把项目管理也做得很好,别人对你投来赞许的目光。


但是如果到了明年,你还是能把项目管理做得很好,可能也还好,但是如果过了 2-3 年呢,别人感觉你项目管理还不错,但是还会投来赞许的目光么?


这个就叫边际递减效应,就好比吃苹果,吃第一个苹果,感觉很好吃,但是当你一次性吃到第 6 个苹果时,你可能就觉得没那么好吃的。


所以工作也一样,你需要突破自己的边界,还是拿项目管理举例,比如我今年把项目管理做好,但是明年,我沉淀了一些项目管理的方法论,然后对部门进行培训,并指导他人如何进行项目管理,是不是又更进一步了?


那如何才能突破自己的边界呢?答案就是持续学习,不断突破自己。


当然,这个学习,肯定不是漫无目的地学习,你需要知道自己的短板,以及你岗位上限需要具备的技能,试探自己的边界,然后有针对地学习。


年龄焦虑


其实做计算机这一行,无论你是研发,还是产品、测试,很多同学都有年龄焦虑,这个很正常。


当你在公司,如果一直原地踏步,哪怕你现在表现得很好,但是当你年纪更大时,你具备的技能和你的年龄不匹配,当公司进行人员优化时,你可能就很危险了。


比如小王在公司里面,每天老老实实敲代码,兢兢业业,平时连假都不会请,这工作劲,是不是连他自己都感动?


当小王到了 40 岁,如果还是只会写代码,哪怕他代码水平一流,每天还是兢兢业业,公司如果要进行人员优化,你觉得他安全么?


所以当你的能力,能匹配上你的年龄,我想就不会那么焦虑了。


可能有同学会和我杠,那我到 40 多岁,然后被裁了呢?如果你到 40 多岁,都还没完成自己的财富积累,还没有提前准备好自己的退路,那真不能怪别人了。


........................................................


二哥上周六来武汉了,我和小灰给二哥接风,带着二哥的老婆和孩子,溜达了一整天。


小朋友没看过企鹅,就去了海洋世界,二哥不喜欢露脸,那就狗头伺候(二哥看到会不会打我),年龄虽然大了,但是精气神还是要有的。


大家猜猜,最右边的小伙伴是谁呢?


作者:楼仔
来源:juejin.cn/post/7266265543412793398

收起阅读 »

24岁 我裸辞啦😀

21年-22年间快速发展 23年-至今停滞不前 1.引言👺     交了辞呈估计有半个月了,自从我把钉钉状态改成last date 08-24之后清静了许多,现在坐在工位上突然回想起刚入职那会,罢了罢了,索性记录一下2年开发的心路历程,以纪念我咋从腼腆小白到相...
继续阅读 »

21年-22年间快速发展

23年-至今停滞不前


1.引言👺


    交了辞呈估计有半个月了,自从我把钉钉状态改成last date 08-24之后清静了许多,现在坐在工位上突然回想起刚入职那会,罢了罢了,索性记录一下2年开发的心路历程,以纪念我咋从腼腆小白相由薪生 怨气深重的打工🐶


image.png


2.工作背景💻︎


    事情还得从今年年初说起,那时候刚接手负责省厅的项目的研发,以前都是做自研或者市级的项目,作为一个小菜鸡内心还是有些(很大的)雀跃。参与需求调研=>了解用户以前的核心业务=>技术选型分析=>项目从零搭建到一期部署上线,都给我带来了很大的成就感。

    成就感的buff加持坚持不了多久,日渐疲惫,😇加上一直带我的领导跳槽之后暴露的问题越来越多由于需求改动很大且频繁,加上非gis专业,边自学边开发,从三月份开始了每日加班生活。

     简单来说就是:



  • 一个星期重做1-2版设计(没办法预测,省厅主任一句话不喜欢就得全部推翻)

  • 由于需要用到数广提供的平台部署生产环境(更新一次最少一个小时),实际开发时间为1-1.5天(包含推送、部署上线、测试)

  • 一周演示2次,通常是:演示(一天)=>修改/开发(1-2天)=>演示(1天)=>反复修改(1-2天)=>演示

  • 原型跟UI跟开发几乎是同步修改的

  • 开发替实施背锅


01b6054e850b3282dc1d12e3fea39f1.jpg


衍生出一系列问题可想而知,实施人员与产品没办法把握甲方要的效果,加上能拒绝不合理需求的领导走了,实施人员没有开发经验,实施运维的活推给我们开发做,我与她沟通也较为困难(当前业务如果这样做的话一定会被要求重做的、时间赶不及等等),我能做的只有反馈2个人给我这边一起开发,捋清楚需求、接口再分模块派任务等等。

    本来是我一个人骂骂咧咧😶‍,后来参与进来的全部人都在骂骂咧咧。。。

    第一次跟同事(实施项目经理)吵架:(因为她是女孩子,另一个经理让我们多理解包容一下。。。后面我实在忍不住了。。。🧐)


63641c20d9110e8b78474d6520a35c4.jpg


周五园区发通知说周六停电一天,我本以为可以好好休息,周末刷刷剧之类的,因为周四刚演示(前几天都加班到很晚)真的很累,下午的时候实施人员开完会说出新需求,周一前上线,领导就一句话让我们几个今晚扛电脑去另一个园区把项目赶出来(我们加班是没有加班费的,只有周末加班能调休,调休假期很难批)


昨天加班到一两点的同事们受不了了。。。开始抗议🤯


661f841e113699854488b618d8e8614.jpg

然后公司大领导出面开导我们了。。。


image.png

省流版:



  • 大家都在加班 研发突然不想加班 有点不负责任

  • 无加班费,无餐补无打车费,但是加班超过晚上八点半 早上可以晚半个钟打卡

  • 不涨工资,收益好的话(我们公司规模蛮大的,1000人+,绩效都是按项目统一定好的)绩效应该能多个几十块


3.摆脱内耗👋


看着领导发的话,有点内疚,我知道我能力不足,很多时候没能统筹好,理解业务起来也很慢,效率不高,还拉着同组同事一起加班,我下班回到家躺在床上望着天花板发呆,房间到是很安静,脑子很吵


是不是我不够认真 想的不够周全


是不是我太矫情了 加点班就不舒服


是不是我太敏感了 被人说两句就不得劲


想了很多下次我要安排好时间,动态设计参数等等等


后来躲在被窝偷偷哭了很久 给我妈打电话 我妈说:人家多少工资 你多少工资啊一起担责


嗯???啊???对啊 。。。我突然脑子短路。。。害得我哭饿了吃了一份烤鸡 两个冰淇淋 一份红豆糖水才缓过来,啊。。。就这样结束了我的内耗


image.png


想通了,然后我就在公司里开启了“一分钱一分货”的上班原则


PS:我是来赚钱的,两年没涨薪了,我的能力值得最少工资有百分之20%的涨幅,加班有加班费也是合理的(这句话听起来怪怪的),加上gis行业就业面窄,好领导还走了,加上还要出差,那也是时候换个更好的平台了


总之很心累


至于我。。。那晚没有在群里说话,而是第二天直接交了辞呈,表示周六要去复诊,以后也不加班了

然后周一被各个领导约去谈谈心,以前部门的同事也问我是不是受啥委屈了


和领导谈的时候,他们都只是表示会争取涨薪跟绩效。我没有聊太多,提出需要涨基本工资而不是绩效后没有明确的答复,还是决定裸辞。
613827a4b1e752a43404146dd4c914c.jpg


4.初入职场回忆


     21年入职这里,对于刚毕业的想蹲在广州的小菜鸡来说还是一份很体面的工作,面试的时候有些题没答上来,面试官(也是一直带我的前辈/领导)鼓励我按照自己的思路解答(我觉得当时的回答不沾边),感觉这个领导温柔又靠谱就选择了这里。


     该说不说,新哥就像老父亲一样💙


30755cdfe988e8ec4a4506175fdc7e8.jpg


团队里的各位靓仔美女前辈一直以来也对我很包容


3bb53172ffb7eecca3022c9c8b7b3a9.png
2a19a340e763803ba1ca303d084ca37.png
76ddce2a0bfba9a92356979338cd35c.png


那时候我的猪猪好像跟我一样 也在闪闪发光


4a0b8489318c5021c6f7ef127d305a0.jpg


055b0561e9ccbb74b9130d7df66360c.jpg


5.入行契机


    大学念的软件工程专业,虽然读书的时候java学不会C#看不懂但还是莫名觉得会留在互联网行业,
写写代码或者做剪辑。

    由于我的人设是个社恐话痨,虽是个吹水佬但不想做售前或营销之类(每天需要接触很多客户的)岗位,加上做前端有种“QQ秀”的吸引力,OK~决定往前端发展。

    其实真正开始学习前端是从毕业前的六个月(培训班太贵了,就在B站自学,主要还是我这个人比较抠),一直到现在仍然觉得女生做技术也很酷,环境也相对包容,与同事间的病情交流也十分和睦,头发还十分茂盛😎。


687b65bbb8e136be27fc8cd82044220.jpg


6.最后(关于未来)


目前即将成为无业游民,有合适的岗位积极争取,也做好了几个月待业的准备,嘎嘎刷题不焦虑。




  • 先出去旅游散散心,或者逛逛广州的博物馆 尝尝美食,吃好喝好睡好 给自己充充电




  • 做更详细的职业规划:



    • 继续做前端

    • 转岗产品 or 数据分析岗

    • 前端=>逐渐转产品 / 管理岗




  • 坐标广州,或许各位大佬可以给我一点建议(听劝!!🤟)或者内推~感激不尽😁




  • 最后的最后,躺平计划尚未成功,还需继

    作者:慌张的葡萄
    来源:juejin.cn/post/7267496335845163068
    续努力~



收起阅读 »

大佬都在用的 IDE 主题,21k Star!

大家好,我是 Zhero! 作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采...
继续阅读 »

大家好,我是 Zhero!


作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采用深色调和高对比度的设计风格,成为黑暗系编程主题的杰出代表。Dracula 主题的界面清晰简洁,代码高亮显示明确鲜明,使得代码结构更加清晰易读。使用 Dracula 主题不仅能减少眼睛的疲劳,还能让我们更专注于代码的编写和理解。如果你正在寻找一个优秀的代码编辑器主题,不妨给 Dracula 一试,相信它会给你带来全新的编程体验。


来源



Dracula 主题源自于一种热门的色彩风格,也被称为“Dracula”。它最初由 Zeno Rocha 在 TextMate 编辑器上设计和实现。随着其受欢迎程度的不断增加,Dracula Color Scheme 成为一个跨平台的开源项目,并得到了许多编辑器和 IDE 的支持。


JetBrains 公司注意到了 Dracula 这种深色调和高对比度的设计,并将其引入了他们的 IDE 产品线。现在,IntelliJ IDEA、PyCharm、WebStorm 等 JetBrains 的 IDE 都提供了官方支持的 Dracula 主题插件。这款黑暗炫彩的主题受到了广大程序员的喜爱,成为了他们工作中常用的选择之一。无论是在日常编码还是阅读代码时,Dracula 主题都能为程序员带来舒适的使用体验。


设计风格



Dracula 主题的设计具有以下魅力:

  • 深邃紫罗兰色基调: Dracula 的主题采用深邃的紫罗兰色作为基调,给人一种神秘而吸引人的感觉。
    1. 高对比度的前景和背景: Dracula 主题使用高对比度的前景和背景色,使得代码内容的层次分明,易于阅读和理解。
    2. 强调重要内容的语法高亮: Dracula 主题使用明亮的绿色进行语法高亮,能够清晰地强调代码中的重要部分,帮助程序员更好地理解代码逻辑。
    3. FLAT 扁平化设计风格: Dracula 主题采用简洁大方的 FLAT 扁平化设计风格,界面整洁清晰,让代码更加突出。
    4. 黑客文化与美学融合: Dracula 主题融合了黑客文化中的深色基调和对于对比度和视觉冲击的美学追求。它既展现了黑客式的科技感,又兼具艺术家般的美学气质。

    通过这些设计特征,Dracula 主题确保了代码的可读性,提供了令人愉悦的编程体验,并为开发者们带来了独特的视觉享受。


    优点


    Dracula 主题在技术上具有以下优势:

  • 中性深色背景的精心调配: Dracula 主题采用中性深色背景,软化了强光对眼睛的刺激,减轻了长时间工作导致的眼睛疲劳问题。
    • 明暗分明的前景和背景: Dracula 主题使用明暗分明的前景和背景色,使得代码的视觉层次感强,识别度高,提高了代码的可读性和理解效率。
    • 温暖色菜单栏和标识色边框: Dracula 主题在菜单栏和标识色边框上采用温暖色,增加了页面元素的识别度,帮助用户更好地找到所需功能。
    • 强调重要内容的明亮色彩: Dracula 主题使用明亮的色彩来突出重要的内容,提高了可关注点的辨识度,使开发者能够更快速地定位和理解关键代码部分。
    • 条件颜色支持: Dracula 主题提供了适应不同环境光照条件的条件颜色支持,确保在不同的工作环境中都能有良好的显示效果。

    Dracula 主题带来的用户体验提升包括:

  • 减轻眼睛疲劳问题: 通过精心调配的色彩和对比度,Dracula 主题可以减轻长时间工作导致的眼睛疲劳问题。
    • 提高代码可读性和理解效率: 明暗分明的视觉层次感和明亮色彩的使用使得代码更易于阅读和理解,提高了开发者的工作效率。
    • 丰富的语法色彩增强趣味性: Dracula 主题提供丰富的语法色彩,使得编程过程更具趣味性和乐趣,激发开发者的工作热情。
    • 酷炫的外观满足个性化追求: Dracula 主题具有独特的外观设计,满足技术宅对个性化的追求,让开发环境更具魅力和个性。
    • 对色弱用户友好: Dracula 主题经过精心设计,在保证美观的同时也考虑到了色弱用户的需求,不会造成视觉障碍。

    正因为这些优势,Dracula 主题备受码农的青睐。它极大地提升了 JetBrains IDE 的美观性和可用性,无论是初学者还是老手,都能够享受到 Dracula 主题带来的舒适的用户体验。


    支持产品


    到目前为止,Dracula 主题已经广泛支持341+款应用程序,涵盖了各个平台和工具。除了 JetBrains IDE,Dracula 还适用于许多其他知名的应用程序,其中包括但不限于以下几个:

    • Notepad++: Dracula 主题为 Notepad++ 提供了独特的外观,使得文本编辑器更加美观和舒适。
    • iTerm: Dracula 主题为 iTerm 终端应用程序带来了独特的配色方案,提升了终端界面的可视性和使用体验。
    • Visual Studio Code: Dracula 主题为 Visual Studio Code 提供了一套酷炫且易于辨识的代码颜色方案,让开发者能够更好地编写和调试代码。
    • Vim: Dracula 主题为 Vim 编辑器提供了一种简洁而又优雅的配色方案,使得代码在终端中的显示更加清晰明了。
    • Terminal.app: Dracula 主题为 macOS 上的 Terminal.app 终端应用程序提供了一种时尚和易于识别的配色方案,提升了终端的可用性和美观性。
    • Zsh: Dracula 主题兼容 Zsh 终端的配色方案,使得命令行界面更加美观和个性化。

    这些应用程序只是 Dracula 主题所支持的众多应用程序中的一部分,它们的加入使得 Dracula 主题在各个平台和工具上都能够提供一致的视觉体验,满足开发者对于美观和可用性的追求。



    查看更多支持产品:



    draculatheme.com



    使用


    下面我用 IDEA 实例给大家展示一下如何使用吧!



    1. 前往插件市场,搜索Dracula,点击安装




    1. 前往 Preferences > Appearance & Behavior > Appearance,从下拉菜单中选择Dracula



    1. 前往 Preferences > Editor > Color Scheme,从下拉菜单中选择Dracula



    通过上述步骤,您可以启用Dracula主题啦!


    总结


    Dracula 主题为 JetBrains IDE 带来了卓越的高对比度黑暗风格,本文我为大家介绍一下它的优点。如果你还没有尝试过这款插件,快去试试吧!


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

    🔥搞钱必看🔥 盘点那些靠谱的程序员副业,狠狠提升财富值💯

    这是一个职业生涯三叶草模型,它分为兴趣、价值、能力三个维度,完美的主职业最好同时满足这三项。但事情往往未必那么如意,如果主职业没能同时满足,那么剩下的部分,完全可以用副业填充。 或者,通俗点说,做副业的第一目标一般是赚钱,想大幅度增加“价值”尤其是物质价值这个...
    继续阅读 »


    这是一个职业生涯三叶草模型,它分为兴趣、价值、能力三个维度,完美的主职业最好同时满足这三项。但事情往往未必那么如意,如果主职业没能同时满足,那么剩下的部分,完全可以用副业填充。


    或者,通俗点说,做副业的第一目标一般是赚钱,想大幅度增加“价值”尤其是物质价值这个维度,那就让我们进入正题,看看程序员可以做的副业都有哪些。


    内容变现


    特点:高投入、门槛中等、长期主义


    公众号


    现在短视频更为吸引流量的时代,做公众号可能已经不是最优解,但做公众号的思路可以推广到一切内容副业上。认真写的内容也终将被看到,现在获取的粉丝反而更为核心、粘性更高。

    公众号的赚钱途径主要有三个:流量主、接受打赏、接广告。

    到达一定流量后,官方会邀请开通“流量主”功能,每篇推文末尾会有广告卡片,被点击即可获得收益。也就是说,开通该功能后,不需要做额外动作,发送推文就有收入。



    第二种是接受打赏,是很直观的读者对作者的物质激励。不过引导比较少的情况下,很少有读者自行去做打赏这个动作的,这部分收入就当做运营公众号的额外小激励吧。



    公众号积累粉丝,长期下来阅读量和粉丝比较好会有广告商找来做广告,俗称“恰饭”,这种收入相比前面两者来说就是“大单子”了,一单一般来说根据公众号质量和流量,会上百、上千、甚至上万、几十万不等。那这种收入就比较可观了,而且有广告主找到,也往往意味着,这个公众号质量还不错。


    为什么说公众号的思路可以推广到一切内容副业呢?因为内容可复用,就可以搭建自己的技术博客网站、分发到其他技术社区,而获利思路也是一致的,无非是流量和知名度的获益。具体的平台运营方式可能不会完全相同,还需要自己提前学习,不断在使用中进行探索。


    知识专栏


    如果你的写作水平还可以,也有可以成体系、系统化的知识输出,那可以尝试整理一个系列课程,以付费知识专栏的形式推出。



    极客时间、掘金小册、慕课网……非常多技术社区都推出了付费专栏,除了内容创作者自发上传的零散内容,一般也有这种成体系的、可以系统性掌握一部分技术的课程,大多数价格不高,一般在百元以内,正因为如此付费门槛也很低,用户乐意为此支付。这种付费专栏如果是线上分成形式,相当于是一个一劳永逸持续入账的项目。


    视频博主


    同样地,如果内容足够好的话,也可以考虑做视频博主,这分两个方向,个人博主和视频教程。不过相比于图文内容的专栏,视频的门槛会更高,对表达能力、授课能力、互动能力都有要求,还可能需要学习相应的录制、剪辑、运营知识。


    做个人视频博主,有平台本身给到内容创作者的流量收益,也有接广告恰饭的收益,一个细水长流,一个来得快但未必持久。长期下来还是需要持续更新、长久维护,小流量不断的同时,打广告有规律。出视频教程,则是与对应平台签约,看自己的版权要求、价格要求、付费机制,自行选择即可。


    技术变现


    特点:技术门槛低、项目导向、投入一般


    接外包


    说起程序员赚外快、副业来,一般最容易想到的就是接私活接外包,比如有很多外包服务平台如猪八戒、一品威客、解放号、码客帮、码市、程序员客栈等等,不过看起来价格并不很美好,大家可以自行筛选。比较靠谱的是熟人介绍,这样需求确认、尾款交付时都会比单纯陌生人更顺利。所以重要的还是积累技术、积累资源、积累人脉,逐渐打造自己的渠道和核心竞争力。


    自主开发项目


    如果技术水平不错,有想法有新意,但不想写文章、不愿录视频,依然只想跟代码打交道,那就可以自主开发项目来变现。根据自己的技术栈还有创意想法,可以是不同的产品,如桌面应用程序、手机APP、小程序、各种插件等。当然,产品要做到通过付费来盈利,门槛确实比较高了,而且除了技术,对运营能力也有一定的要求。做得好的完全可以做独立开发者来养活自己。


    比如这样在禅道插件市场上传插件的用户,已经获得了很高的持续性收益。


    其他方向


    特点:门槛高低不一、收入不一、形式灵活


     除了前面的内容导向和技术导向,其实还有很多跟技术、跟主业完全没有任何关系的方向。比如培训师,往往是按天收费,费用很高,不过当然也需要极高的知识壁垒;比如经常调侃的跑滴滴、送外卖,也能见证一些人生百态;比如有的人做手绘涂鸦、游戏陪玩这种兴趣驱动的副业,满足兴趣爱好的同时还能获得收入;比如很多脑力劳动者跑去做的轻体力活,如便利店收银、酒店前台、快餐店兼职,在机械操作的体力活中解放大脑。


    总之一句话:副业形式千千万,还是得靠实力干。短期的在线刷单、杀猪盘投资等等都被翻来覆去地反诈骗讲烂了,但任何宣称短期的、高回报率,都可能是带着引号的“副业”,需要擦亮眼睛加以鉴别。



    大多数副业还是有门槛的,需要投入精力和时间,找好方向,坚持长期主义,去做吧!


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

    为什么开发者应该多关注海外市场

    在上一篇《单枪匹马程序员,月营收从 0 到 1 万刀,近 90% 的毛利》里有掘友评论,这样的产品只有国外有,国内缺乏生态。 这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。 早在 2000 年(点 com 泡沫...
    继续阅读 »

    在上一篇《单枪匹马程序员,月营收从 0 到 1 万刀,近 90% 的毛利》里有掘友评论,这样的产品只有国外有,国内缺乏生态。


    这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。




    早在 2000 年(点 com 泡沫那阵),国外就有了 indie hacker / indie maker 这个名词来称呼做“独立”产品的人。这里的 indie 是 independent 的意思,意在独立(解脱)于各种束缚,比如:朝九晚五的工作时间、固定的办公室、领导、或者是投资人。


    而国内在最近几年也涌现了一拨独立开发者,多数以工程师为主,当然做的产品也是面向国内的市场。有做地不错的,像 Baye 的熊猫吃短信、vulgur 的极简时钟、Kenshin 的简阅等;但综合我这两年来对海外一些独立产品的研究,海外市场或许是更好的选择。


    当然凡是都有个前提,就是你没有一个豪华的创始团队或者是顶级投资人的背书,就是个人或者两三人的小团队。这个条件我觉得可以覆盖 90% 的中国的开发者;对于另外 10% 的拥有资源或者金主爸爸靠山的个人或者团队,不仅“可以”还“应该”去磕中国市场。但这不是今天要讨论的主题。


    在 BAT TMD 等巨头和背靠资源的精英创业者们的夹缝里,我觉得只有做面向海外市场的小产品是更有胜率一点;做国内市场面临的四个问题:


    第一、不存在足够的空间给个人/小团队做独立产品存活。


    Slack 大家应该都知道,在美国已经上市了,市值 200 亿刀。Slack 一直是被 qiang 的,但是为什么国内没有出现 Slack 这样的产品作为一个信息中心来连接各个办公工具?


    其实有,还不少,但都没活太久。一部分原因是腾讯阿里都非常重视这个“商业流量入口”,不想有可能被对方占有了。另外是国内互联网生态,从 BAT TMD 巨头到小软件公司,都太封闭;不仅不开放,还相互制约,都想把自己流量的守住,所以就同时出现了三个 Slack:

    • 微信出个企业微信(还封杀了 wetools)
    • 阿里出个钉钉
    • 字节出个飞书

    在这种巨头虎视眈眈且相互对抗的格局里,作为三缺(缺钱、缺资源、缺核心门槛)的个人或者团队是无法存活的。或许在 2010 年至 2016 年间还有草根产品团队依靠“热钱”注入有爆发的可能性,时至今日,特别是这个蜜汁 2020 的局势,是不太可能的了。


    即使,你找到了一个空白的利基市场(niche),你接下来面对三个问题:需求验证(试错)、推广、和商业化。


    第二点、需求验证或者叫“试错”成本高。


    由于国情不同,咱们需要经过一些不可避免的审核流程来保证互联网的干净。这个没话说,在哪做事就守哪的规矩。但这“需求验证”的门槛可就提高了不少。


    比如要做个网站吧,备案最快也两周。做游戏?有没有版号?做 app ?有没有软著?小程序(从用户端来讲)是个不错的创新,但是你最烦看到的是不是“审核不通过,类目不符合”?稍微做点有用户互动的功能都需要公司主体。公司注册、银行开户、做帐、以及各种实名制等;这些虽然都不是不可达到的门槛,但是去完成这些要耗费大量的精力,对于本身就单打独斗的开发者来说 - 太累了。


    再看看海外,简直不要太爽。做 app 还是需要经过苹果和谷歌的审核,但几乎不会对程序本身以外的东西设置门槛。网站注册个域名,30 秒改个 DNS 指到你的 IP,Netlify 或 Vercel 代码一推,就自动构建、部署、上线了。哪怕你不会写代码或者会写代码但是想先验证一下需求,看看潜在用户的响应如何,国外有不少非常值得一样的 no code 或 low code 平台。这个以后可以单独写一篇。


    OK,国内你也通过重重难关项目终于上线了,你面临剩下的两个问题:推广和商业化。


    第三点、推广渠道少 && 门槛高。


    海外市场的推广渠道更多元,比如 ProductHunt, IndieHackers, BetaList 等。这些平台不仅国内没有,我想表达的更重要一点是,这些平台用户都比较真诚和热心会实在地给你提建议,给你写反馈。国内也有几个论坛/平台,但是用户氛围和友好度就和上述几个没法比了。


    在付费推广方面,国内门槛(资质、资金)都挺高,感觉只有大品牌才能投得起广告,这对于缺资金的团队来讲,又是闭门羹。而国外,facebook 和 google 拿个 50、100 刀去做个推广计划都没问题。


    可以以非常低的成本来获取种子用户或者验证需求。


    行吧,推广也做得不错,得到了批种子用户并且涨势还不错;就最后一步了,商业化。


    第四点、商业化选择少。


    说商业化选择少可能说过了。国内是由于巨头间的竞争太激烈,出现各种补贴手段,导致互联网用户习惯于免费的互联网产品,甚至觉得应该倒贴给他来使用;伸手党、白 piao 党挺多;付费以及版权意识都还有改善空间。


    想想你都给哪些浏览器插件付费过?“插件还需要付钱?!”


    而海外用户的付费意愿足够强烈,在以后的案例分享中就能体会到。一个小小的浏览器插件,做得精美,触碰到了用户的购买欲望,解决了他一个痛点,他就愿意购买。


    下一篇就分享国外浏览器插件的产品案例。


    顺便再说一个「免费 vs 付费」的问题,这个不针对哪个国家,全世界都一样。免费(不愿意付费)的用户是最难伺候的,因为他们不 value 你的产品,觉得免费的就是创造者轻易做出来的、廉价的。如果依着这部分不愿意付费的客户来做需求,产品只会越做越难盈利。




    掘友们,下一篇文章见了


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

    iOS时钟翻转动画

    iOS
    最近项目有个时间倒计时的功能,网上也有很多优秀的第三方。这个功能应用还是比较广泛的,就稍微研究了一下。有好几种方法实现,笔者选取较简单一种。 原理其实很简单,就是一个的绕X轴的翻转动画,只是在翻转过程中针对特殊情况做 特殊处理就行,下面也会讲到。 效果图 思...
    继续阅读 »

    最近项目有个时间倒计时的功能,网上也有很多优秀的第三方。这个功能应用还是比较广泛的,就稍微研究了一下。有好几种方法实现,笔者选取较简单一种。


    原理其实很简单,就是一个的绕X轴的翻转动画,只是在翻转过程中针对特殊情况做 特殊处理就行,下面也会讲到。


    效果图




    思路


    以一次完整动画为例,分步骤解析:


    第一步:


    新建3个UILable,分别是正在显示(currentLabel)、下一个显示(nextLabel)、做动画的(animationLabel)。


    第二步:


    首先在每次动画前给nextLabel设置默认的X轴起始角度翻转,这样处理是为了能够只显示上半部分,下半部分被隐藏(zPosition不改动的情况下),如下图,红色的是nextLabel,绿色的是currentLabel,灰色的是animationLabel




    代码:

    // 设置默认的X轴起始角度翻转,为了能够只显示上半部分,下半部分被隐藏(zPosition不改动的情况下)
    func setupStartRotate() -> CATransform3D {
    var transform = CATransform3DIdentity
    transform.m34 = CGFLOAT_MIN
    transform = CATransform3DRotate(transform, .pi*kStartRotate, -1, 0, 0)
    return transform
    }

    第三步:


    使用CADisplayLink做动画,笔者这里设置固定的刷新帧率为60(因为存在不同的刷新帧率设备),且动画执行时间0.5s,即每次刷新帧率时动画执行了2/60进度。


    接下来使用CATransform3DRotateanimationLabel沿着X轴进行翻转动画,这时候我们会发现动画的进度超过一半时,会存在如下问题:




    上图这个是倒计时 2 变为 1 的过程,且动画进度超过一半时的显示画面。我们换个角度看看:




    可知在当前情况下,灰色的标签显示的是 2 的上部分的背面,但是应该显示的是 1 的下部分,这显示是有问题的。这么说有点拗口,简单来说就是一个物体在3D空间中沿X轴翻转大于90度时,我们看到的实际是物体的上下和前后均颠倒的二维平面,所以才会出现如此的不和谐。


    所以解决这个问题,使动画更和谐流畅,我们需要物体翻转的动画在临界点翻转到90度时,即与屏幕垂直的时候,为了正确显示,即需要将动画的animationLabel同时沿着Y和Z轴翻转,并切换文字,将2切换成1。即:

    if animateProgress >= 0.5 {
    t = CATransform3DRotate(t, .pi, 0, 0, 1);
    t = CATransform3DRotate(t, .pi, 0, 1, 0);
    animationLabel.text = nextLabel.text
    }else{
    animationLabel.text = currentLabel.text
    }

    此时的过程就是 2 在翻转超过90时,将之沿着Y和Z轴翻转,并切换为1,看到的就是动图显示的过程了。


    到这里一个完整的翻转动画就结束了,后面使用CADisplayLink定时重复上述动画就可以了。


    后续也使用这个动画写了一个时间显示的和倒计时的demo,具体的代码在下面的链接,感兴趣的可以查阅指导下。


    RCFoldAnimation


    若存在什么不对的地方,欢迎指正!


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

    从 Mac 远程登录到 iPhone

    iOS
    简介 平时在使用 Mac 的过程中,经常会使用终端输入命令来执行一些操作。在越狱开发的过程中,同样需要在 iOS 系统上输入一些命令来执行一些任务。那么如何才能在 iOS 系统上输入命令呢,在 iOS 上安装一个终端命令行工具,然后在 iPhone 那小小的屏...
    继续阅读 »

    简介


    平时在使用 Mac 的过程中,经常会使用终端输入命令来执行一些操作。在越狱开发的过程中,同样需要在 iOS 系统上输入一些命令来执行一些任务。那么如何才能在 iOS 系统上输入命令呢,在 iOS 上安装一个终端命令行工具,然后在 iPhone 那小小的屏幕上用触摸屏输入命令吗?虽然说理论上和实际上都是可行的,但是通过手指触摸屏幕来输入命令的方式效率比较低,也不是很方便。这里还是推荐在 Mac 上远程登录到 iOS 系统,这样就可以使用 Mac 的键盘输入命令到 iOS 上去执行,更加方便,快捷。


    SSL、openSSL、SSH、openSSH


    SSL(Secure Sockets Layer)是一种用于在计算机网络上进行安全通信的协议。SSL 最初由 Netscape 开发,后来发展为 TLS(Transport Layer Security)。SSL/TLS 用于在客户端和服务器之间建立安全的加密连接,以保护敏感数据的传输,例如在网页浏览器和服务器之间的数据传输。


    OpenSSL 是一个强大的、商业级的、功能齐全的开源工具包,它提供了一组库和命令行工具,用于处理 SSL/TLS 协议和加密算法,是 SSL 协议的一款开源实现工具。OpenSSL 可以用于创建和管理数字证书、实现安全传输和通信,以及进行加密和解密等操作。它不仅支持 SSL/TLS 协议,还支持多种加密算法和密码学功能。


    SSH(Secure Shell)是一种用于安全远程登录和数据传输的网络协议。它为计算机之间的通信提供了加密和身份验证,以确保通信的机密性和完整性。SSH 使用公钥密码体制进行身份验证,并使用加密算法来保护数据的传输。


    OpenSSH 是一个开源的 SSH 实现,它提供了 SSH 客户端和服务器的功能,用于安全远程登录、命令执行和文件传输。它包括客户端 ssh 和服务器 sshd、文件传输实用程序 scp 和 sftp 以及密钥生成工具 (ssh-keygen)、运行时密钥存储 (ssh-agent) 和许多支持程序。它是 Linux 和其他类 Unix 系统中最常见的 SSH 实现,也支持其他操作系统。


    SSL 最早出现于 1994 年,用于 Web 浏览器和服务器之间的安全通信。OpenSSL 和 SSH 都起源于 1995 年,OpenSSL 是一个加密工具包,而 SSH 是用于安全远程登录和数据传输的协议。OpenSSH 是 SSH 协议的开源实现,起源于 1999 年,为 SSH 提供了广泛使用的实现。


    OpenSSH 通常依赖于 OpenSSL。OpenSSH 使用 OpenSSL 库来实现加密和安全功能,包括加密通信、密钥生成、数字证书处理等。OpenSSL 提供了各种加密算法和密码学功能,使 OpenSSH 能够建立安全的 SSH 连接,并保护通信数据的机密性和完整性。在大多数情况下,安装 OpenSSH 时,系统会自动安装或链接到已经安装的 OpenSSL 库。这样,OpenSSH 就能够使用 OpenSSL 的功能来实现加密和安全性,而不必重新实现这些复杂的加密算法和协议。


    因此,可以说 OpenSSH 依赖于 OpenSSL,OpenSSL 提供了 OpenSSH 所需的加密和安全功能,使得 OpenSSH 成为一种安全、可靠的远程登录和数据传输工具。这些安全协议和工具对于保护通信和数据安全至关重要。


    实践


    对以上名词概念有了基本的了解之后,我们可以进行实践操作。如果感觉还是迷迷糊糊也不要紧,实践起来就会感觉简单多了。主要是对 OpenSSH 这个开源库提供的常用命令的使用。Mac 系统自带了这个工具所以不需要进行配置,而 iOS 系统上默认是没有安装这个工具的,包括越狱之后的 iOS 也没有,所以需要先下载安装这个工具。


    安装过程很简单,如下图所示,在 Cydia 上搜索 OpenSSH 下载并按照提示进行安装就好了。



    安装好之后,就可以在 Mac 上远程登录到越狱 iOS 了。iOS 系统默认提供了两个用户,一个是 root 用户,是 iOS 中最高权限的用户,我们在逆向开发过程中基本都是使用这个用户。还有一个是 mobile 用户,是普通权限用户,iOS 平时进行 APP 安装,卸载基本都是使用这个用户,但是我们在逆向开发中很少或者基本不会使用到这个用户,这里有个了解就够了。


    Cydia 首页有 OpenSSH 访问教程,这个文档详细的记载了如何从 Mac 远程登录到 iOS 设备上,并且也提供了修改默认密码的方法。建议英文不错的同学直接阅读这篇文档,不想看的就看我后面的介绍也可以。文档位置如下图所示



    通过默认账号密码登录到 iPhone


    ssh 提供了两种登录到服务器的方式,第一种是使用账号和密码。第二种是免密码登录。下面先介绍第一种

    1. 越狱 iPhone 在 Cydia 上安装 OpenSSH
    2. 确认 iPhone 和 Mac 电脑在同一个局域网下,在 Mac 打开终端,输入以下命令
      ssh root@iPhone的IP地址
      第一次连接会出现 Are you sure you want to continue connecting (yes/no/[fingerprint])? 提示,输入 yes 确认进行连接
    3. 输入默认的初始密码 alpine ,这里终端为了安全并不会显示密码的明文
    4. 之后就会看到终端切换到了 iPhone:~ root# 用户,代表成功登录到远程 iPhone 手机的 root 用户上了。这个时候,你在 Mac 终端输入的指令都会被发送到 iPhone 上,如下图 

       如果你觉得还不过瘾,可以输入 reboot 命令,体会一下远程操纵手机的快乐(重启之后,你可能需要重新越狱一下 iPhone 了😶)
    5. 输入 exit 退出登录

    刚刚我们登录的是 root 用户。在 iOS 中,除了 root 用户,还有一个 mobile 用户。其中 root 用户是 iOS 中最高权限的用户。mobile 是普通权限用户,其实平时越狱调试过程中,很少会使用这个 mobile 用户,这里只是介绍一下。


    能够成功登录 iPhone 之后,建议修改一下用户的默认密码,既然做逆向开发了,当然对安全也要注意一点。在登录 root 用户之后,输入:passwd 可以修改 root 用户的密码,输入 passwd mobile 可以修改 mobile 用户的密码。


    通过免密码方式登录到 iPhone


    OpenSSH 除了默认的账号密码登录的方式,还提供了免密码登录的方式。需要进一步完成一些配置才可以实现。服务器(在当前情况下,iPhone是服务器,Mac是客户端)的 ~/.ssh 目录下需要添加一个 authorized_keys 文件,里面记录可以免密登录的设备的公钥信息。当有客户端(Mac)登录的时候,服务器会查看 ~/.ssh/authorized_keys 文件中是否记录了当前登录的客户端的公钥信息,如果有就直接登录成功,没有就要求输入密码。所以我们要做的就是将 Mac 设备的公钥信息追加到 iPhone 的 authorized_keys 文件内容的最后面。追加是为了不影响其他的设备。完成这个操作需要先确保我们的 Mac 设备上已经有 ssh 生成的公钥文件。


    打开 Mac 终端,输入 ls ~/.ssh 查看是否已经存在 id_rsa.pub 公钥文件,.pub就是公钥文件的后缀




    如果没有看到公钥文件,需要使用 ssh-keygen 命令生成该文件。按回车键接受默认选项,或者根据需要输入新的文件名和密码。这将生成一个公钥文件(id_rsa.pub)和一个私钥文件(id_rsa)。


    使用 SSH 复制公钥到远程服务器。使用以下命令将本地计算机(Mac)上的公钥复制到远程服务器(iPhone)。请将user替换为您的远程服务器用户名,以及remote_server替换为服务器的域名或IP地址。

    ssh-copy-id user@remote_server



    在远程服务器(iPhone)上设置正确的权限。确保远程服务器上的~/.ssh文件夹权限设置为 700,并将~/.ssh/authorized_keys文件的权限设置为 600。这样可以确保SSH可以正确识别公钥并允许免密码登录。如下图所示:




    .ssh 文件夹前面的 drwx------ 是 Linux 和类 Unix 系统中表示文件或目录权限的一种格式。在这个格式中,每一组由10个字符组成,代表文件或目录的不同权限。让我们逐个解释这些字符的含义:




    所以,drwx------ 表示这是一个目录,并且具有以下权限:

    • 文件所有者具有读、写和执行权限。
    • 文件所有者所在组没有任何权限。
    • 其他用户没有任何权限。

    后面 9 个字符分为三组,每组从左至右如果有对应的权限就是421相加起来就是 7 后面都是0。所以 .ssh 文件夹的权限是正确的值 700,如果不是 700 的使用 chmod 700 .ssh 进行提权。authorized_keys 文件的权限是 rw 就是 420 相加起来就是 6 。后面都是 0,所以 authorized_keys 的权限也是正确的值 600。同样如果不是 600,使用 chmod 600 authorized_keys 命令修改权限。


    配置完成后,您现在可以使用 SSH 免密码登录到远程服务器(iPhone)。在 Mac 上,使用以下命令连接到远程服务器:

    ssh root@10.10.20.155

    这将直接连接到远程服务器,而无需输入密码。




    通过 USB 有线的方式登录到 iPhone


    配置为免密码登录之后,还可以进一步使用 USB 有线连接的方式登录到手机。如果你经常使用 WiFi 这种方式远程登录调试就会发现偶尔会碰到指令输入,响应卡顿,反应慢的情况,这样的体验显然让人感到不爽。所以,在大部分情况下,更推荐使用 USB 有线连接登录到 iPhone 上,这样使用的过程中,就像在本地输入命令操作一样流畅。


    iproxy 是一个用于端口转发的命令行工具。它通常用于在 iOS 设备和计算机之间建立端口映射,从而将 iOS 设备上运行的服务暴露到计算机上。这对于开发者来说非常有用,因为可以通过本地计算机访问 iOS 设备上运行的服务,而无需将服务部署到公共网络上。


    iproxyusbmuxd 的一部分,后者是一个用于连接和管理 iOS 设备的 USB 通信的守护进程。usbmuxd 允许通过 USB 连接与 iOS 设备进行通信,并且iproxy 则负责在本地计算机和iOS设备之间建立端口转发。


    通常,您可以在命令行中使用 iproxy 命令来建立端口转发,例如:

    iproxy local_port device_port

    其中,local_port 是本地计算机上的端口号,device_port 是 iOS 设备上的端口号。执行此命令后,iOS 设备上的服务将通过 device_port 映射到本地计算机上的 local_port


    请注意,使用 iproxy 需要先安装 libusbmuxd 包。在 macOS 上,您可以使用 Homebrew 来安装 libusbmuxd

    brew install libusbmuxd

    安装好之后,就可以使用 iproxy 命令了,使用 iproxy 将本机 10010 端口和 USB 设备的 22 端口进行映射的命令如下:

    iproxy 10010 22



    这里本机的端口 10010 可以设置为你想要的其他端口,但是不能是系统保留的端口(系统保留的端口有哪些,可以看百度的介绍)。端口转发设置完成之后,这个终端就不要关闭,也不要管它了,新建另一个端口进行 ssh 登录。此时,需要给 ssh 加上指定端口参数,命令如下:

    ssh -p 10010 root@localhost

    同样第一次使用这种方式建立连接会给出提示,输入 yes 确认




    之后,在 iPhone 设备上输入命令调试时,再也不会遇到卡顿,慢,延迟的现象啦。玩得开心~


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

    我只是用了个“笨”方法,一个月后不再惧怕英文文档

    在日常工作中,尤其是程序员时时刻刻都会与英文打交道,虽然我们尽可能的在互联网和中文书籍中寻找我们需要的信息,但是,有时候总是不尽人意。对待翻译过来的文档或者书本可能有些定义依然无法明确理解,回到它原有的场景中才能明白究竟是什么意思?阅读英文文档应该是我们的基本...
    继续阅读 »

    在日常工作中,尤其是程序员时时刻刻都会与英文打交道,虽然我们尽可能的在互联网和中文书籍中寻找我们需要的信息,但是,有时候总是不尽人意。对待翻译过来的文档或者书本可能有些定义依然无法明确理解,回到它原有的场景中才能明白究竟是什么意思?阅读英文文档应该是我们的基本技能。


    本文笔者将会从以下几个方面来分享一个月的时间,笨方法跨越英文障碍的落地方法:



    • 遇到的 Bug 百度没有结果怎么办

    • 中文翻译文献模棱两可怎么办

    • 寻找外文文献的发现过程是什么样的

    • 如何解决英文文档中的复杂句子

    • 如何结合文档学习语法

    • 结合实例带你操作“笨”方法找到适合自己的路


    在本文内,笔者想在这儿分享的一点工作体会。在标题中重点引号标注的一个字——“笨”。既然是“笨”方法,就说明我们都很容易理解,很容易上手,很容易操作,很容易见效。


    之所以没有说,两天速成,七天见效,因为这些似乎是赤脚医生行走江湖的招数。工作或者学习,还是务实一点比较能走长远。


    那么笔者的方法究竟“笨”在哪儿呢?展示几张图就很快明白了。



    1. 如果在出差的路上,或者咖啡馆,或者自习室,可以拿出英文资料,或者是需要查看的技术文档,或者需要学习的产品手册。将不能够准确理解的段落、句子、单词摘抄到随身携带的抄写本上,这样便于针对细节深入处理。


    9b292760-6c15-11ea-a6ec-45ee4806df26.jpeg


    (图片来自手机拍摄,不太美观)


    上图是笔者曾经在了解 JWT 的涉及到的实现机制的过程中,对于 Auth0 提供的 JWT 指南的阅读过程中留下的笔记。


    当再次看到笔记的时候对 JWT-handbook 第 16 页的部分内容就能够了然于心。


    那么我们在来看它对应的原始的手册中的内容如下:


    b0126510-6c15-11ea-848f-1ff50c7c2559.png


    (图片来自:jwt-handbook)


    通过这种方式,相信一定程度上就能感受到对于英文手册,技术指南的文档也能够看上一看,并获取到一些有意义的信息。



    1. 接着上面,如下的内容是笔者对于原文流程图下方的注解的学习过程。


    c00f90a0-6c15-11ea-8b2e-a93d3912f08f.jpeg


    通过剖析一张图和图对应的注解,就能够发现能够很轻松的干掉一篇 122 页技术手册中的其中一页,前前后后可能也不到几分钟的时间,如果基础比较好的情况下,也许几秒钟就解决了无法在各式各样的解读后的博客中看到的原始概念和流程。进而对关键的技术点有更加清晰的认识。



    1. 如果在家的情况下,有时候会觉得使用电脑打字的速度要比用手抄本写笔记的速度要快,你就可以像下图这样,在电脑主屏幕上用文本编辑器来记下笔记,用扩展显示器显示 pdf,这样不用切来切去,影响阅读速度。


    d5c4d0e0-6c15-11ea-98a4-2b16cffe68b2.jpeg



    1. 要说上面是笔者使用的“笨”方法, 那下面就可以说明,它“笨”的程度。笔者大概记下了这么多页的笔记,看完一遍 JWT handbookThe OAuth 2.0 Authorization Framework。这个过程大概花了一个月的空闲时间。所以如果你的英语基础更好的话,那就更不用担心了,连一个月的时间都用不了。


    1ff4d200-6c16-11ea-937f-590540467001.jpeg


    曾经多次看到全英文的技术指南,都只能看几句话就放弃,在阅读两篇英文技术文档之后,对于其他的文档来说,都会尝试拿来读一读,而且随着阅读量的增加速度和理解能力都会逐渐增强。


    上面分享完了笔者曾经的经历,便能直观的认识到,对于技术人员或者产品经理,应对工作中的英语非要先背诵大量的词汇,并不像想象的那样:买两本厚厚的语法书,啃上半年一年才能够掌握它。


    知识点来源于生活和工作,同样它又应用于生活和工作。就像我们的母语汉语一样,从咿呀学语到长篇大论,也并不是长篇大论之前一直在背诵汉字,学习句法。这需要一个过程,但是在这个过程中,随着知识的积累,在积累的过程中一定不耽误成长和认识新的事物。


    所以,我们不必恐惧我们遇到的其他语种的文献,它像我们学习汉语一样,只是有一个过程。相对于曾经考试过程中遇到的令人头疼的“完形填空”,“阅读理解”这些,兴许比我们看到的技术文档要更难,因为技术文档对于技术人来说,它所涉及的内容与我们工作的范畴是高度结合的。


    如果要学习 Spring Security,那么我们自然不会去看关于发表的“新冠肺炎”的最新文献。随着技术词汇的总结,我们便能够对于相关的技术文档更容易理解,更快速的阅读掌握。因为它首先在行业上就存在一定的专一性,这样就为我们学习圈定了范围。


    接下来,从以下几个方面来分享。


    遇到的 Bug 百度没有结果怎么办


    我们在工作中如果遇到的问题,经常会去百度一些问题,尤其是具体的 IDE 中的一些异常。例如,在 Eclipse 中有如下的错误提示:


    MongoDB shell version v3.4.5 connecting to: mongodb://127.0.0.1:27017 2020-0

    如果第一次遇见这个问题,无从下手的时候,可能首先会去百度,查找结果,如下:


    5dd80560-6c16-11ea-86a9-9df84f90745d.png


    如果得到的答案,没有我想要的,或者是解决不了我的问题的情况下怎么办呢?


    Google


    74a76830-6c16-11ea-a6ec-45ee4806df26.png


    不同的搜索引擎,由于它的搜索机制不一样,自然呈现给我们的检索结果就不一样,所以当无路可走的时候,常常会使用这几种方法综合起来,来搜寻符合需要的方案。


    必应


    有时候,我们无法访问谷歌的情况下,可以尝试使用必应的国际版。
    8581d460-6c16-11ea-a8b7-1d883493075c.png
    (图片来自:cn.bing.com


    输入我们之前的问题,得到如下结果:


    985662e0-6c16-11ea-8b2e-a93d3912f08f.png


    上面几种检索结果的区别对于我们来说有时候未必不是一件好事,因为它能够展现出更多的推荐方案供我们选择。


    Stack Overflow


    仔细的从谷歌和必应国际版的检索结果中会发现它得到的结果都有 Stack Overflow 提供的方案。那么 Stack Overflow 是何方神圣呢?可以把它看成是中国的 CSDN、博客园这一类的技术问答网站。


    如果无法登陆 Google,而且不习惯于 biying 的搜索引擎的情况下,那么可以直接去 Stack Overflow 查找或者询问你自己的问题。


    a8684ef0-6c16-11ea-98a4-2b16cffe68b2.png


    (图片来自:stackoverflow.com/


    MSDN


    如果是微软技术栈的工程师或者是产品经理,可以考虑使用 MSDN。


    bb1928d0-6c16-11ea-a3c8-9bd79d9bd149.png


    (图片来自:docs.microsoft.com


    cd755300-6c16-11ea-bd54-bd0372f44fbe.png


    (图片来自:docs.microsoft.com


    在使用 MSDN 的时候有一个优点,就是它支持中文,也支持英文。这样可能能够为我们阅读英文文档提供一个参照。如果英文的版本的确无法看下去的情况下,便可以参考中文的说明。


    其他


    当然不乏一些其他优秀的网站、论坛,比如日本的 Fresheye、俄罗斯的 Yandex、韩国的 NAVER,如下图:


    dd0bcd80-6c16-11ea-a3c8-9bd79d9bd149.png


    (图片来自:search.fresheye.com


    edcb37a0-6c16-11ea-98a4-2b16cffe68b2.png
    (图片来自:yandex.com


    fe7a9c30-6c16-11ea-a3c8-9bd79d9bd149.png


    (图片来自:search.naver.com/


    从这也能看出来,尽管我们是学习技术的,或者是产品部门的一个产品专员,但我们一样能够通过我们的努力打开世界上每一扇我们感兴趣的大门。


    接下来,说一说文章起始位置的第二个问题。


    中文翻译文献模棱两可怎么办


    对于这一点,我们引用 DAN OLSEN 的一本书的封面的内容来说明。如图:


    0f2322f0-6c17-11ea-86a9-9df84f90745d.png


    (图片来自:THE LEAN PRODUCT PLAYBOOK


    封面上能够得到的信息是:



    DAN OLSEN


    THE LEAN PRODUCT PLAYBOOK


    HOW TO INNOVATE WITH MINIMUM VIABLE PRODUCTS AND RAPID CUSTOMER FEEDBACK


    WILEY



    其中 DAN OLSEN 是作者、WILEY 是指这本书是由 WILEY 出版的。从领英上的介绍来看,这本书是 DAN OLSEN 是他很得意的一本畅销书。


    25d69950-6c17-11ea-96fb-af457604317b.png


    (图片来自:http://www.linkedin.com


    这也是笔者接触到的第一本专业的关于产品的书。通过它来印证我们遇到模棱两可的解释的时候,该如何处理。



    THE LEAN PRODUCT PLAYBOOK


    HOW TO INNOVATE WITH MINIMUM VIABLE PRODUCTS AND RAPID CUSTOMER FEEDBACK


    WILEY



    假设你不知道它的意思的情况下,分别在 Google 翻译和百度翻译上翻译的结果如下:


    3833fc00-6c17-11ea-98a4-2b16cffe68b2.png


    (图片来自:translate.google.cn


    465a6bc0-6c17-11ea-a8b7-1d883493075c.png


    (图片来自:fanyi.baidu.com


    通过对比就能够知道它的含义就是精益产品手册,对于 PLAYBOOK 的翻译结果稍有不同,如果深究的情况下, 我们可以去看一下 PLAYBOOK 的英文解释。


    5818a840-6c17-11ea-86a9-9df84f90745d.png


    (图片来自:dictionary.cambridge.org


    68a9af60-6c17-11ea-9561-3fe89f4fa56e.png


    (图片来自:http://www.merriam-webster.com


    774d1750-6c17-11ea-a3c8-9bd79d9bd149.png


    (图片来自:http://www.oxfordlearnersdictionaries.com


    相信对于你的感到模糊的词汇或者句子,在谷歌、百度、韦氏、剑桥、牛津五家线上词典的围攻下将你的疑问降低到零。


    这就是笔者在日常工作和学习中遇到的模棱两可的问题的解决方案。


    当时引起笔者注意的是如下这一段话:(引自:THE LEAN PRODUCT PLAYBOOK



    The Lean Product Process will guide you through the critical thinking steps required to achieve product-market fit. In the next chapter, I begin describing the details of the process, but before I do, I want to share an important hight-level concept: seperating problem space from solution space. I have been discussing this concept in my talks for years and am glad to see those terms used more frequently these days.


    Any product that you actually build exists in solution space,as do any product designs that you create-such as mockups,wireframes,or prototypes. Solution space includes any product or representation of a product that is used by or intended for use by a customer It is the opposite of a blank slate.When you build a product,you have chosen a specific implementation.Whether you've done so explicitly or not, you've determined how the product looks,what it does,and how it works.


    In contrast,there is no product or design that exists in problem space. Instead, problem space is where all the customer needs that you'd like your product to deliver live. You shouldn't interpret the word "needs" too narrowly: Whether it's a customer pain point, a desire, a job to be done, or a user story, it lives in problem space.



    如果我是一名产品经理或者产品专员,我想在这里我便应该仔细的去推敲 DAN OLSEN 说的这一句话中包含了些什么信息?


    什么是 problem space,什么是 solution space,这两个概念是它的范畴中包含了哪些元素,它们之间有什么关系,它们又是如何促进产品创新的。可以带着这些问题去别的章节中去寻找答案。


    另外,这里想要提醒的一点是,在国内我们是可以使用谷歌翻译的,它对应的域名是 translate.google.cn,而不是 translate.google.com,笔者曾经由于一直输入 .com 所以导致困惑了好多天。


    寻找外文文献的发现过程是什么样的


    白岩松老师在一个《对白》中曾经说过一句这样的话,笔者很赞同。“人找书是很难的,但是书找书是容易的,越读书,越知道该读什么书”。


    在工作过程中,我们如果需要找到一个问题的解决方案,可能需要在搜索过程中不断调整检索词,希望能够通过检索词来搜索出自己需要的有价值的内容。




    1. 在工作中,常常为了对项目结构有清晰的认知,需要我们能够画出“软件架构图” ,如果你搜索,“软件架构图”,检索后可以看到几乎第一屏展现出来的有一多半的都是实现软件架构的工具的广告。这往往是一件令人很头疼的事情。




    2. 调整检索词,改为“software architecture”,在百度中的结果会得到一些相对有用的信息。




    8d276bc0-6c17-11ea-9561-3fe89f4fa56e.png


    这时候标注出来的 PDF、读书、图文的字样便出现了。




    1. 再次细化检索词,使用“Software Architecture Patterns”。




    2. 通过查看能够进一步得到某一本书的信息:




    9c5c8c60-6c17-11ea-a8b7-1d883493075c.png


    (图片来自:http://www.oreilly.com/library


    这种情况下,就能够对这本书中的部分内容,查看是否是存在自己想要调研的相关内容,而后便能进一步去学习。



    1. 如果你需要查看这本书,继续在搜索引擎中搜索,比如“software architecture patterns pdf”,能够看到很多好心人分享出来的书,这种方法往往都比较有效,只要耐心的搜索检索词。


    aaf5e230-6c17-11ea-86a9-9df84f90745d.png



    1. 找到后打开对应的文件,浏览目录,这时候看到了它对架构师经常要打交道的”层图“的介绍,这就为我们平时的工作中的画图能够提供参考和建议。


    bde328d0-6c17-11ea-937f-590540467001.png


    (图片来自:Software Architecture Patterns


    这一部分,基本就介绍了,在我们对于英文文献的寻找过程是怎样的,输入中文检索词,如果中文检索词无法满足要求情况下,可以考虑将中文检索词转换为英文检索词,用转换的英文检索词,一步步优化,结合不同搜索引擎的检索结果去找到符合自己目标的内容。


    另外上面的例子中提到的 Software Architecture Patterns 篇幅比较小,也比较适合查看。同时架构图在软件设计过程中又比较重要,我们可以尝试通过这种方式的查找和学习、阅读,逐渐地对于架构图不会敬而远之,不会感觉遥不可及。


    有些情况下在某些书中,会对于一些概念在脚注或者引用文献都会进行说明,这样便可以在阅读过程中对于相关的概念,去其他的书中寻找,这就形成了一个书找书的过程,它是实现起来还是比较便捷有价值的。但是在阅读过程中想要提醒的一点是,我们不要过度发散,导致忘记了最初需要解决的问题,导致花费大量的时间,需要解决的问题没有解决。


    如何解决英文文档中的复杂句子


    在阅读过程中,我们可能会发现长句理解起来相对麻烦一点,这里我们拿上文中提到的 Software Architecture Patterns 的一个段落来解剖:



    Each layer in the architecture forms an abstraction around the work that needs to be done to satisfy a particular business request. For example, the presentation layer doesn’t need to know or worry about how to get customer data; it only needs to display that information on a screen in particular format. Similarly, the business layer doesn’t need to be concerned about how to format customer data for display on a screen or even where the customer data is coming from; it only needs to get the data from the persistence layer, perform business logic against the data (e.g., calculate values or aggregate data), and pass that information up to the presentation layer.



    文章无非就是通过这种类似的长短句形成的段落堆砌出来的,所以解决了词,解决了句子,自然没什么可怕的。看到这种段落的时候,可以试试去拆分:


    d0655190-6c17-11ea-8f43-edb1924172bb.png


    将一个段落拆分,按照标点符号拆分成四个部分。再对每一个部分进行分解,再把其中一句单独分离出来。


    比如第一句:


    de4346a0-6c17-11ea-a8b7-1d883493075c.png


    只要能够找到句子中的主语、谓语、宾语,基本上就能确定大概的意思。这类比于汉语中实际上,也比较容易理解,“谁”“干了”“什么”。其余的内容只是对这几个元素的修饰和补充说明。


    架构中的每一层,形成一个围绕工作的抽象,什么工作?需要被实现用来满足业务需求的工作。


    那么上一句就能够基本理解它的意思,架构中的每一层实际上是对于满足特定业务需要的抽象,再简化就是架构图中的层,是对于特定业务的抽象。


    回到原文中结合上下文,便能够对第一句进一步理解。


    而这时候,如果你不加分析,直接去 Google 翻译或者百度翻译上看它的翻译结果,可能会让你更加迷糊。


    ec0faf80-6c17-11ea-937f-590540467001.png


    f457bde0-6c17-11ea-a3c8-9bd79d9bd149.png


    从这个结果来看,我们在阅读过程中,所有的工具只能是参考,需要我们结合自己实际情况,不断补充,不断调查,不断积累和完善。


    如何结合文档学习语法


    从上文中的复杂句子的分解过程中,实际上就能看到语法的影子,如果只是在文档的学习中遇到的问题解决的情况下,如果无法正确理解的时候,从词性、时态、句型、从句等方面去针对性的查找。这儿就不再深入扩展了。


    可以查找关于高中、托福、雅思一类的专业英语语法书籍,比如在世界上颇有影响力的 Practical English Usage,它的中译本是《牛津英语用法指南》。


    笔者从书中摘抄一段内容,兴许这一小段就会纠正原本的错误认识:



    动词形式(”时态“)和时间


    动词形式和时间没有直接关系。例如,像 went 这样的动词过去式不但可以表示过去的事情(如 We went to Morocco last January 我们去年 1 月份去了摩洛哥),而且可以表示发生在现在或将来的不真实的或不确定的事情(如 It would be better if we went home how 我们现在回家更好)。动词现在式可以表示将来(如 I'm seeing Peter tomorrow 我明天要见彼得)。另外,进行式和完成式不单单表示时间有关的概念,还可表示动作的持续、完成或目前的重要性。


    (摘自:《牛津英语用法指南》)



    这就熟悉又陌生的语法,可能从高中毕业后再也没有系统的学习过过于英语语法的内容,但是没关系,只要用到就有机会学习。


    结合实例带你操作“笨”方法找到适合自己的路


    前面的内容,笔者分享了关于在工作中遇到的问题,查找、分解的方法。总的来说大概几个方面,包括了:



    • 搜索

    • 翻译

    • 找文献

    • 分解复杂句子

    • 查找语法知识


    希望这些能够对你的工作中遇到的头疼的英语问题有所帮助。


    下面推荐了相关的文档,你可以使用它,也可以按照前面介绍的方法结合自己遇到的实际问题去寻找对应的文献。


    研发的同学可以尝试去读一下,Windows 上 ZIP 版本 Redis 的三个文档:


    0407dae0-6c18-11ea-8b2e-a93d3912f08f.png


    (图片来自:github.com/



    • Redis on Windows Release Notes

    • Redis on Windows

    • Windows Service Documentation


    产品的同学,笔者目前只发现了上文提到的 DAN OLSEN 的 THE LEAN PRODUCT PLAYBOOK,有好资源的同学可以分享,共同学习。


    另外,闲暇时有兴趣的同学可以去浏览、了解日常的消息(国内可访问)



    13ff6120-6c18-11ea-86a9-9df84f90745d.png


    (图片来自:apnews.com/



    242a5370-6c18-11ea-937f-590540467001.png


    (图片来自: http://www.afp.com



    3db41b50-6c18-11ea-9561-3fe89f4fa56e.png


    (图片来自:globalnews.ca/


    这次的分享到这儿基本结束了,希望你可以用更快的,更简单的方法,去克服工作中遇到的问题。欢迎共同学习,携手共进。


    作者:问问计算机
    来源:juejin.cn/post/7149197829477662728
    收起阅读 »

    怎么去选择一个公司?

    一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。 那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。 企业文化和价值观 行业势头 工资待遇 公司规模 人才水平 企业文化和价值观 无...
    继续阅读 »

    一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。


    那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。



    • 企业文化和价值观

    • 行业势头

    • 工资待遇

    • 公司规模

    • 人才水平


    企业文化和价值观


    无法适应企业文化和价值观的员工,注定会被边缘化,获取不到资源,直到被淘汰。而适应企业文化和价值观的员工,在公司做事情则更能够得心应手。


    如何选择适合自己的企业文化和价值观


    如果你打算在一个公司长期发展,可以试着找找里面的熟人,聊聊公司内部的做事风格,比如晋升、奖金、淘汰、组内合作、跨部门合作以及如何处理各种意外情况等,这样就能实际感受到企业的文化和价值观了,然后再根据自己的标准,判断是否适合自己。


    行业势头


    行业一般会有风口期、黄金发展期和下降期三个阶段。



    • 处于下降趋势的行业要慎重考虑。

    • 处于风口期的行业发展趋势还不是很明显,如果你之前从事的行业和新的风口相关,那么不妨试试;如果你对这些风口背后的行业不是很熟悉,那不妨等风口的势头明朗了,再做打算。

    • 处于黄金发展期的行业发展已经稳定,有成熟的盈利模式,在这样的行业中积累经验,会在行业的发展上升期变得越来越值钱。如果你对这些行业感兴趣,不妨考虑相关的公司。


    工资待遇


    工资待遇不仅仅包括固定工资,还有一次性收入、奖金、股票以及各种福利等。


    很多新入职的员工会有一些的奖金,例如签字费、安家费等,这些是一次性的,有时还会附加”规定时间内不能离职”等约束条件。这部分钱的性价比比较低,但一般金额还不错。


    奖金主要看公司,操作空间很大,它和公司的经营状况关联紧密,谈Offer时约定的数额到后面不一定能够兑现,尤其是这两年整个互联网行业都不景气,很多公司的奖金都“打骨折”甚至直接取消了。


    其他福利一般包括商业医疗保险、年假、体检、补贴等,它和公司所在行业有关联,具有公司特色。


    股票也是待遇中很重要的一部分,很多公司在签Offer时会约定一定数量的股票,但是会分四年左右结清,这需要考虑你能坚持四年吗?四年之后没有股票要怎么办?


    公司规模


    如果待遇和岗位差不多,建议优先选择头部大公司,这样你可以学到更多的经验,接触更有挑战的业务场景,让自己成长的更快。


    如果你看好一个行业,那么需要努力进入这个行业的头部公司。


    人才水平


    一个公司的人才水平,决定了公司对人才的态度和公司内部合作与管理的风格。


    举个例子,如果一个公司里程序员的水平都很一般,那么这个公司就更倾向于不相信员工的技术能力,并制定非常细致和严格的管理规范和流程,避免员工犯错。如果你的水平高,就会被各种管理规范和流程束缚住。同时,如果你发现与你合作的人的水平都很“感人”,你也需要调整自己的风格,让自己的工作成果能够适应公司普遍的水平。




    此文章为极客时间3月份Day26学习笔记,内容来自《职场生存

    手册》课程。

    收起阅读 »

    iOS - 人脸识别

    iOS
    前言 最近公司提出了一个有趣的新需求,需要开发一个功能来自动识别用户前置摄像头中的人脸,并且能够对其进行截图。 话不多说,直接开整...技术点:AVCaptureSession:访问和控制设备的摄像头,并捕获实时的视频流。Vision:提供了强大的人脸识别和分...
    继续阅读 »

    前言


    最近公司提出了一个有趣的新需求,需要开发一个功能来自动识别用户前置摄像头中的人脸,并且能够对其进行截图。


    话不多说,直接开整...

    • 技术点:
    • AVCaptureSession:访问和控制设备的摄像头,并捕获实时的视频流。
    • Vision:提供了强大的人脸识别和分析功能,能够快速准确地检测和识别人脸。

    效果




    开始


    首先,工程中引入两个框架

    import Vision
    import AVFoundation

    接下来,我们需要确保应用程序具有访问设备摄像头的权限。先判断是否拥有权限,如果没有权限我们通知用户去获取

    let videoStatus = AVCaptureDevice.authorizationStatus(for: .video)
    switch videoStatus {
    case .authorized, .notDetermined:
    print("有权限、开始我们的业务")
    case .denied, .restricted:
    print("没有权限、提醒用户去开启权限")
    default:
    break
    }

    然后,我们需要对摄像头进行配置,包括确认前后置摄像头、处理视频分辨率、设置视频稳定模式、输出图像方向以及设置视频数据输出


    配置


    确认前后置摄像头:


    使用AVCaptureDevice类可以获取设备上的所有摄像头,并判断它们是前置摄像头还是后置摄像头

    // 获取所有视频设备
    let videoDevices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .unspecified).devices

    // 筛选前置摄像头和后置摄像头
    var frontCamera: AVCaptureDevice?
    var backCamera: AVCaptureDevice?

    for device in videoDevices {
    if device.position == .front {
    frontCamera = device
    } else if device.position == .back {
    backCamera = device
    }
    }

    // 根据需要选择前置或后置摄像头
    let cameraDevice = frontCamera ?? backCamera

    处理视频分辨率:


    可以通过设置AVCaptureSession的sessionPreset属性来选择适合的视频分辨率。常见的分辨率选项包括.high、.medium、.low等。

    let captureSession = AVCaptureSession()
    captureSession.sessionPreset = .high


    输出图像方向:


    可以通过设置AVCaptureVideoOrientation来指定输出图像的方向。通常,我们需要根据设备方向和界面方向进行调整。

    if let videoConnection = videoOutput.connection(with: .video) {
    if videoConnection.isVideoOrientationSupported {
    let currentDeviceOrientation = UIDevice.current.orientation
    var videoOrientation: AVCaptureVideoOrientation

    switch currentDeviceOrientation {
    case .portrait:
    videoOrientation = .portrait
    case .landscapeRight:
    videoOrientation = .landscapeLeft
    case .landscapeLeft:
    videoOrientation = .landscapeRight
    case .portraitUpsideDown:
    videoOrientation = .portraitUpsideDown
    default:
    videoOrientation = .portrait
    }

    videoConnection.videoOrientation = videoOrientation
    }
    }

    视频数据输出:


    可以使用AVCaptureVideoDataOutput来获取摄像头捕捉到的实时视频数据。首先,创建一个AVCaptureVideoDataOutput对象,并将其添加到AVCaptureSession中。然后,设置代理对象来接收视频数据回调。

    let videoOutput = AVCaptureVideoDataOutput()
    captureSession.addOutput(videoOutput)

    let videoOutputQueue = DispatchQueue(label: "VideoOutputQueue")
    videoOutput.setSampleBufferDelegate(self, queue: videoOutputQueue)

    视频处理、人脸验证


    接下来,我们将对视频进行处理,包括人脸验证和圈出人脸区域。我们将在AVCaptureVideoDataOutputSampleBufferDelegate 的代理方法中来实现这些功能

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {

    guard let bufferRef = CMSampleBufferGetImageBuffer(sampleBuffer) else {
    return
    }

    let detectFaceRequest = VNDetectFaceRectanglesRequest()
    let detectFaceRequestHandler = VNImageRequestHandler(cvPixelBuffer: bufferRef, options: [:])

    do {
    try detectFaceRequestHandler.perform([detectFaceRequest])
    guard let results = detectFaceRequest.results else {
    return
    }

    DispatchQueue.main.async { [weak self] in
    guard let self = self else {
    return
    }

    // 移除先前的人脸矩形
    for layer in self.layers {
    layer.removeFromSuperlayer()
    }
    self.layers.removeAll()

    for observation in results {
    let oldRect = observation.boundingBox
    let w = oldRect.size.width * self.view.frame.size.width
    let h = oldRect.size.height * self.view.frame.size.height
    let x = oldRect.origin.x * self.view.bounds.size.width
    let y = self.view.frame.size.height - (oldRect.origin.y * self.view.frame.size.height) - h

    // 添加矩形图层
    let layer = CALayer()
    layer.borderWidth = 2
    layer.cornerRadius = 3
    layer.borderColor = UIColor.orange.cgColor
    layer.frame = CGRect(x: x, y: y, width: w, height: h)

    self.layers.append(layer)
    }

    // 将矩形图层添加到视图的图层上
    for layer in self.layers {
    self.view.layer.addSublayer(layer)
    }
    }
    } catch {
    print("错误: \(error)")
    }
    }

    结尾


    识别单个人脸的时候没有太大问题,但是多个人脸位置不是很准确,有知道原因的小伙伴告知一下


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

    高斯模糊

    前言 通常,图像处理软件会提供"模糊"(blur)滤镜,使图片产生模糊的效果。 “模糊”的算法不只一种,高斯模糊只是其中一种,甚至它只是其中效率很差的一种。 在Android中使用高斯模糊,需要使用到 JNI 技术,Android Studio开发之 JNI...
    继续阅读 »

    前言


    通常,图像处理软件会提供"模糊"(blur)滤镜,使图片产生模糊的效果。



    “模糊”的算法不只一种,高斯模糊只是其中一种,甚至它只是其中效率很差的一种。


    在Android中使用高斯模糊,需要使用到 JNI 技术,Android Studio开发之 JNI 篇已具体讨论JNI的用法等。本文主要讲述高斯模糊原理及编码等。


    高斯模糊原理


    所谓"模糊",可以理解成每一个像素都取周边像素的平均值。



    如图所示,2是中间点,周围点都是1。中间点取周围点平均值,就会变成1。在数值上,这是一种"平滑化"。在图形上,就相当于产生"模糊"效果,"中间点"失去细节。


    显然,计算平均值时,取值范围越大,"模糊效果"越强烈。


    如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。


    高斯模糊根据正态分布,决定周围点的权重值。



    正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。


    计算平均值的时候,我们只需要将"中心点"作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。


    正态分布的一维公式为:



    由于每次计算都是以中间点为原点,所以u为标准差,即为0。所以公式进一步进化为:



    由于图像是二维的,需要根据二维正态分布函数来计算权重值,它的公式以及曲线如下:



    不过为了代码效率问题,不会采用二维正态分布的计算方式,而是分别对 X 轴和 Y 轴进行两次高斯模糊,也能达到效果(即通过一维正态分布计算权重)。


    高斯模糊代码


    先分别计算正态分布各参数,sigma与高斯模糊半径有关系,2.57既是1除以根号2 PI得来。

    float sigma = 1.0 * radius / 2.57;
    float deno = 1.0 / sigma * sqrt(2.0 * PI);
    float nume = -1.0 / (2.0 * sigma * sigma);

    因为对于每一个像素点来说,周围点在正态分布中所占的权重值都是一样的,所以正态分布计算一次即可。

    float *gaussMatrix = (float *) malloc(sizeof(float) * (radius + radius + 1));
    float gaussSum = 0.0;
    for (int i = 0, x = -radius; x <= radius; ++x, ++i) {
    float g = deno * exp(1.0 * nume * x * x);
    gaussMatrix[i] = g;
    gaussSum += g;
    }

    因为是以中间点自身为原点,所以 x 的取值范围是从 -radius 到 radius,计算结果存储的数组中。请注意周围点权重值与数组的对应关系,x 等于 -radius 时,而 i 等于0,后文会用到。


    由于并没有计算所有的周围点,所以权重总合必然不为1,所以需要归一化,设法使权重值为一。

    int len = radius + radius + 1;
    for (int i = 0; i < len; ++i) {
    gaussMatrix[i] /= gaussSum;
    }

    先进行 x 轴的模糊。

      for (int y = 0; y < h; ++y) {
    //取一行像素数据,注意像素总数组的访问方式是 x + y * w
    memcpy(rowData, pix + y * w, sizeof(int) * w);
    for (int x = 0; x < w; ++x) {
    float r = 0, g = 0, b = 0;
    gaussSum = 0;
    //以当前坐标点 x、y 为中心,查看前后一个模糊半径的周围点,根据正态分布
    //重新计算像素点的颜色值
    for (int i = -radius; i <= radius; ++i) {
    // k 表示周围点的真实坐标
    int k = x + i;
    // 边界上的像素点,它的周围点只有正常的一半,所以要保证 k 的取值范围
    if (k >= 0 && k <= w) {
    // 取到周围点的像素,并根据 argb 的排列方式,计算 r、g、b分量
    int color = rowData[k];
    int cr = (color & 0x00ff0000) >> 16;
    int cg = (color & 0x0000ff00) >> 8;
    int cb = (color & 0x000000ff);
    //真实点坐标为 k,与它对应的权重数组下标是 i + radius
    //前文中计算正态分布权重时已经说明相关的对应关系。
    //根据正态分布的权重关系,计算中心点的 r g b各分量
    int index = i + radius;
    r += cr * gaussMatrix[index];
    g += cg * gaussMatrix[index];
    b += cb * gaussMatrix[index];
    gaussSum += gaussMatrix[index];
    }
    }
    //因为边界点的存在,gaussSum值不一定为1,所以需要除以gaussSum,归一化。
    int cr = (int) (r / gaussSum);
    int cg = (int) (g / gaussSum);
    int cb = (int) (b / gaussSum);
    //根据权重值与各周围点像素相乘之和,得到新的中间点像素。
    pix[y * w + x] = cr << 16 | cg << 8 | cb | 0xff000000;
    }
    }

    y轴的模糊原理和x轴基本一样,这里就不再重复说明了。


    JNI图片接口


    JNI中处理图片,需要引用 bitmap.h,头文件中主要定义三个方法。

      int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
    AndroidBitmapInfo* info);
    int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
    int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);

    AndroidBitmap_getInfo:获取图片信息,比如宽、高、图片格式等
    AndroidBitmap_lockPixels:顾名思义,锁定像素
    AndroidBitmap_unlockPixels:解锁。


    AndroidBitmap_lockPixels 和 AndroidBitmap_unlockPixels 成对调用,在两个方法之间可对图片像素进行相应处理,解锁像素以后,对图片的调整效果可以立即看到,并不需要再重新生成图片了。


    ps:有时并不知道 JNI 有哪些接口可以调用,最好的方式就是看源码,有哪些接口,一目了然。


    其它模糊方法


    除了高斯模糊之外,还有其它模糊方法,比如说 fastblur,不过这个算法还没看明白,此处不再详述,具体代码本人的github上都有,欢迎访问。


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

    当遇到需要在Activity间传递大量的数据怎么办?

    在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
    继续阅读 »

    在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


    Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


    Activity之间传递大量数据主要有如下几种方式实现:
    • LruCache
    • 持久化(sqlite、file等)
    • 匿名共享内存

    使用LruCache

    LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


    首先我们定义好写入读出规则:

    public interface IOHandler {
       //保存数据
       void put(String key, String value);
       void put(String key, int value);
       void put(String key, double value);
       void put(String key, float value);
       void put(String key, boolean value);
       void put(String key, Object value);

       //读取数据
       String getString(String key);
       double getDouble(String key);
       boolean getBoolean(String key);
       float getFloat(String key);
       int getInt(String key);
       Object getObject(String key);
    }

    我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.

    LruCache<String, Object> mCache = new LruCache<>( 10 * 1024*1024);

    写入数据我们使用比较简单:

    @Override
    public void put(String key, String value) {
       mCache.put(key, value);
    }

    好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


    读取数据也是比较简单方便:

    @Override
    public String getString(String key) {
       return String.valueOf(mCache.get(key));
    }

    持久化数据

    那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


    优势:


    (1)应用中全部地方均可以访问


    (2)即便应用被强杀也不是问题了


    缺点:


    (1)操做麻烦


    (2)效率低下


    匿名共享内存

    在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


    Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


    Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。



    今日分享到此结束,对你有帮助的话,点个赞再走呗,每日一个面试小技巧




    关注公众号:Android老皮

    解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版



    内容如下



    1.Android车载应用开发系统学习指南(附项目实战)

    2.Android Framework学习指南,助力成为系统级开发高手

    3.2023最新Android中高级面试题汇总+解析,告别零offer

    4.企业级Android音视频开发学习路线+项目实战(附源码)

    5.Android Jetpack从入门到精通,构建高质量UI界面

    6.Flutter技术解析与实战,跨平台首要之选

    7.Kotlin从入门到实战,全方面提升架构基础

    8.高级Android插件化与组件化(含实战教程和源码)

    9.Android 性能优化实战+360°全方面性能调优

    10.Android零基础入门到精通,高手进阶之路


    作者:派大星不吃蟹
    链接:https://juejin.cn/post/7264503091116965940
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    三分钟教会你微信炸一炸,满屏粑粑也太可爱了!

    相信这个特效你和你的朋友(或对象)一定玩过 当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。 不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢的...
    继续阅读 »

    相信这个特效你和你的朋友(或对象)一定玩过


    当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。


    不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢的不行,拉着朋友们就开始“炸”得不亦乐乎。


    同样被虏获芳心的设计小哥哥在玩到尽兴后,突然灵感大发,连夜绘制出了设计稿,第二天就拉上产品和研发开始脑暴。


    “微信炸💩打动我的一点是他满屏的设计,能够将用户强烈的情绪抒发出来;同时他能够与上一条表情进行捆绑,加强双方的互动性。”设计小哥哥声情并茂道。


    “所以,让我们的表情也‘互动’起来吧!”


    这不,需求文档就来了:



    改掉常见的emoji表情发送方式,替换为动态表情交互方式。即,

    当用户发送或接收互动表情,表情会在屏幕上随机分布,展示一段时间后会消失。

    用户可以频繁点击并不停发送表情,因此屏幕上的表情是可以非常多且重叠的,能够将用户感情强烈抒发。


    (暂用微信的聊天界面进行解释说明,图1为原样式,图2是需求样式)



    这需求一出,动态表情在屏幕上的分布方案便引起了研发内部热烈讨论:当用户点击表情时,到底应该将表情放置在屏幕哪个位置比较好呢?


    最直接的做法就是完全随机方式:取0到屏幕宽度和高度中随机值,放置表情贴纸。但这么做的不确定因素太多,比如存在一定几率所有表情都集中在一个区域,布局边缘化以及最差的重叠问题。因此简单的随机算法对于用户的体验是无法接受的。




    我们开始探索新的方案:

    因为目前点的选择依赖于较多元素,比如与屏幕已有点的间距,与中心点距离以及屏幕已有点的数目。因此最终决定采用点权随机的方案,根据上述元素决策出屏幕上可用点的优度,选取优度最高的插入表情。


    基本思路


    维护对应屏幕像素的二维数组,数组元素代指新增图形时,图形中心取该点的优度。

    采用懒加载的方式,即每次每次新增图形后,仅记录现有方块的位置,当需要一个点的优度时再计算。


    遍历所有方块的位置,将图形内部的点优度全部减去 A ,将图形外部的点按到图形的曼哈顿距离从 0 到 max (W,H),映射,减 0 到 A * K2。

    每次决策插入位置时,随机取 K + n * K1 个点,取这些点中优度最高的点为插入中心

    A, K, K1, K2 四个常数可调整



    一次选择的复杂度是 n * randT,n 是场上方块数, randT 是本次决策需要随机取多少个点。 从效率和 badcase来说,这个方案目前最优。





    代码展示


    ```cpp
    #include <iostream>
    #include <vector>
    using namespace std;

    const int screenW = 600;
    const int screenH = 800;

    const int kInnerCost = 1e5;
    const double kOuterCof = .1;

    const int kOutterCost = kInnerCost * kOuterCof;

    class square
    int x1;
    int x2;
    int y1;
    int y2;
    };

    int lineDist(int x, int y, int p){
    if (p < x) {
    return x - p;
    } else if (p > y) {
    return p - y;
    } else {
    return 0;
    }
    }

    int getVal(const square &elm, int px, int py){
    int dx = lineDist(elm.x1, elm.x2, px);
    int dy = lineDist(elm.y1, elm.y2, py);
    int dist = dx + dy;
    constexpr int maxDist = screenW + screenH;
    return dist ? ( (maxDist - dist) * kOutterCost / maxDist ) : kInnerCost;
    }

    int getVal(const vector<square> &elmArr, int px, int py){
    int rtn = 0;
    for (auto elm:elmArr) {
    rtn += getVal(elm, px, py);
    }
    return rtn;
    }

    int main(void){

    int n;
    cin >> n;

    vector<square> elmArr;
    for (int i=0; i<n; i++) {
    square cur;
    cin >> cur.x1 >> cur.x2 >> cur.y1 >> cur.y2;
    elmArr.push_back(cur);
    }


    for (;;) {
    int px,py;
    cin >> px >> py;
    cout << getVal(elmArr, px, py) << endl;
    }

    }

    优化点

    1. 该算法最优解偏向边缘。因此随着随机值设置越多,得出来的点越偏向边缘,因此随机值不能设置过多。
    2. 为了解决偏向边缘的问题,每一个点在计算优度getVal需要加上与屏幕中心的距离 * n * k3

    效果演示


    最后就是给大家演示一下最后的效果啦!


    圆满完成任务,收工,下班!


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

    为什么我在公司里访问不了家里的电脑?

    本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! 上篇文章「为什么我们家里的IP都是192.168开头的?」提到,因为IPv4地址有限,最大42亿个。为了更好的利用这有限的IP数量,网络分为局域网和广域网,将IP分为了私有I...
    继续阅读 »

    本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!



    上篇文章「为什么我们家里的IP都是192.168开头的?」提到,因为IPv4地址有限,最大42亿个。为了更好的利用这有限的IP数量,网络分为局域网和广域网,将IP分为了私有IP和公网IP,一个局域网里的N多台机器都可以共用一个公网IP,从而大大增加了"可用IP数量"。


    收发数据就像收发快递


    当我们需要发送网络包的时候,在IP层,需要填入源IP地址,和目的IP地址,也就是对应快递的发货地址和收货地址。


    IP报头里含有发送和接收IP地址


    但是我们家里的局域网内,基本上都用192.168.xx.xx这样的私有IP


    如果我们在发送网络包的时候,这么填。对方在回数据包的时候该怎么回?毕竟千家万户人用的都是192.168.0.1,网络怎么知道该发给谁?


    所以肯定需要将这个192.168.xx私有IP转换成公有IP


    因此在上篇文章最后,留了这么个问题。局域网内用的是私有IP,公网用的都是公有IP。一个局域网里的私有IP想访问局域网外的公有IP,必然要做个IP转换,这是在哪里做的转换呢?


    私有IP和公有IP在哪进行转换


    答案是NAT设备,全称Network Address Translation,网络地址转换。基本上家用路由器都支持这功能。


    我们来聊下它是怎么工作的。


    NAT的工作原理


    为了简单,我们假设你很富,你家里分到了一个公网IP地址 20.20.20.20,对应配到了你家自带NAT功能的家用路由器上,你家里需要上网的设备有很多,比如你的手机,电脑都需要上网,他们构成了一个局域网,用的都是私有IP,比如192.168.xx。其中你在电脑上执行ifconfig命令,发现家里的电脑IP是192.168.30.5。 你要访问的公网IP地址是30.30.30.30


    于是就有下面这样一张图


    内网IP访问公网IP


    当你准备发送数据包的时候,你的电脑内核协议栈就会构造一个IP数据包。这个IP数据包报头里的发送端IP地址填的就是192.168.30.5接收端IP地址就是30.30.30.30。将数据包发到NAT路由器中。


    此时NAT路由器会将IP数据包里的源IP地址修改一下,私有IP地址192.168.30.5改写为公网IP地址20.20.20.20,这叫SNATSource Network Address Translation,源地址转换)。并且还会在NAT路由器内部留下一条 192.168.30.5 -> 20.20.20.20的映射记录,这个信息会在后面用到。之后IP数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30,到这里发送流程结束。


    SNAT


    如果接收端处理完数据了,需要发一个响应给你的电脑,那就需要将发送端IP地址填上自己的30.30.30.30,将接收端地址填为你的公网IP地址20.20.20.20,发往NAT路由器。NAT路由器收到公网来的消息之后,会检查下自己之前留下的映射信息,发现之前留下了这么一条 192.168.30.5 -> 20.20.20.20记录,就会将这个数据包的目的IP地址修改一下,变成内网IP地址192.168.30.5, 这也叫DNATDestination Network Address Translation,目的地址转换)。 之后将其转发给你的电脑上。


    DNAT


    整个过程下来,NAT悄悄的改了IP数据包的发送和接收端IP地址,但对真正的发送方和接收方来说,他们却对这件事情,一无所知


    这就是NAT的工作原理。




    NAPT的原理


    到这里,相信大家都有一个很大的疑问。


    局域网里并不只有一台机器,局域网内 每台机器都在NAT下留下的映射信息都会是 192.168.xx.xx -> 20.20.20.20,发送消息是没啥事,但接收消息的时候就不知道该回给谁了。


    NAT的问题


    这问题相当致命,因此实际上大部分时候不会使用普通的NAT


    那怎么办呢?


    问题出在我们没办法区分内网里的多个网络连接。


    于是乎。


    我们可以加入其他信息去区分内网里的各个网络连接,很自然就能想到端口。


    但IP数据包(网络层)本身是没有端口信息的。常见的传输层协议TCP和UDP数据报文里才有端口的信息。


    TCP报头有端口号


    UDP报头也有端口号


    于是流程就变成了下面这样子。


    当你准备发送数据包的时候,你的电脑内核协议栈就会先构造一个TCP或者UDP数据报头,里面写入端口号,比如发送端口是5000,接收端口是3000,然后在这个基础上,加入IP数据报头,填入发送端和接收端的IP地址。


    那数据包长这样。


    数据包的构成


    假设,发送端IP地址填的就是192.168.30.5接收端IP地址就是30.30.30.30


    将数据包发到NAT路由器中。


    此时NAT路由器会将IP数据包里的源IP地址和端口号修改一下,从192.168.30.5:5000改写成20.20.20.20:6000。并且还会在NAT路由器内部留下一条 192.168.30.5:5000 -> 20.20.20.20:6000的映射记录。之后数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30:3000,到这里发送流程结束。


    NAPT发送数据


    接收端响应时,就会在数据包里填入发送端地址是30.30.30.30:3000,将接收端20.20.20.20:6000,发往NAT路由器。NAT路由器发现下自己之前留下过这么一条 192.168.30.5:5000 -> 20.20.20.20:6000的记录,就会将这个数据包的目的IP地址和端口修改一下,变回原来的192.168.30.5:5000。 之后将其转发给你的电脑上。


    NAPT接收数据


    如果局域网内有多个设备,他们就会映射到不同的公网端口上,毕竟端口最大可达65535,完全够用。这样大家都可以相安无事。


    像这种同时转换IP和端口的技术,就是NAPT(Network Address Port Transfer , 网络地址端口转换 )。


    看到这里,问题就来了。


    那这么说只有用到端口的网络协议才能被NAT识别出来并转发?


    但这怎么解释ping命令?ping基于ICMP协议,而ICMP协议报文里并不带端口信息。我依然可以正常的ping通公网机器并收到回包。


    ping报头


    事实上针对ICMP协议,NAT路由器做了特殊处理。ping报文头里有个Identifier的信息,它其实指的是放出ping命令的进程id


    对NAT路由器来说,这个Identifier的作用就跟端口一样。


    另外,当我们去抓包的时候,就会发现有两个Identifier,一个后面带个BE(Big Endian),另一个带个LE(Little Endian)


    其实他们都是同一个数值,只不过大小端不同,读出来的值不一样。就好像同样的数字345,反着读就成了543。这是为了兼容不同操作系统(比如linux和Windows)下大小端不同的情况。


    1667783441963


    内网穿透是什么


    看到这里,我们大概也发现了。使用了NAT上网的话,前提得内网机器主动请求公网IP,这样NAT才能将内网的IP端口转成外网IP端口


    反过来公网的机器想主动请求内网机器,就会被拦在NAT路由器上,此时由于NAT路由器并没有任何相关的IP端口的映射记录,因此也就不会转发数据给内网里的任何一台机器。


    举个现实中的场景就是,你在你家里的电脑上启动了一个HTTP服务,地址是192.168.30.5:5000,此时你在公司办公室里想通过手机去访问一下,却发现访问不了。


    那问题就来了,有没有办法让外网机器访问到内网的服务?


    有。


    大家应该听过一句话叫,"没有什么是加中间层不能解决的,如果有,那就再加一层"。


    放在这里,依然适用。


    说到底,因为NAT的存在,我们只能从内网主动发起连接,否则NAT设备不会记录相应的映射关系,没有映射关系也就不能转发数据。


    所以我们就在公网上加一台服务器x,并暴露一个访问域名,再让内网的服务主动连接服务器x,这样NAT路由器上就有对应的映射关系。接着,所有人都去访问服务器x,服务器x将数据转发给内网机器,再原路返回响应,这样数据就都通了。这就是所谓的内网穿透


    像上面提到的服务器x,你也不需要自己去搭,已经有很多现成的方案,花钱就完事了,比如花某壳。


    内网穿透


    到这里,我们就可以回答文章标题的问题。


    为什么我在公司里访问不了家里的电脑?


    那是因为家里的电脑在局域网内,局域网和广域网之间有个NAT路由器。由于NAT路由器的存在,外网服务无法主动连通局域网内的电脑。


    两个内网的聊天软件如何建立通讯


    好了,问题就叒来了。


    我家机子是在我们小区的局域网里,班花家的机子也是在她们小区的局域网里。都在局域网里,且NAT只能从内网连到外网,那我电脑上登录的QQ是怎么和班花电脑里的QQ连上的呢?


    两个局域网内的服务无法直接连通


    上面这个问法其实是存在个误解,误以为两个qq客户端应用是直接建立连接的。


    然而实际上并不是,两个qq客户端之间还隔了一个服务器。


    聊天软件会主动与公网服务器建立连接


    也就是说,两个在内网的客户端登录qq时都会主动向公网的聊天服务器建立连接,这时两方的NAT路由器中都会记录有相应的映射关系。当在其中一个qq上发送消息时,数据会先到服务器,再通过服务器转发到另外一个客户端上。反过来也一样,通过这个方式让两台内网的机子进行数据传输。


    两个内网的应用如何直接建立连接


    上面的情况,是两个客户端通过第三方服务器进行通讯,但有些场景就是要抛开第三端,直接进行两端通信,比如P2P下载,这种该怎么办呢?


    这种情况下,其实也还是离不开第三方服务器的帮助。


    假设还是A和B两个局域网内的机子,A内网对应的NAT设备叫NAT_A,B内网里的NAT设备叫NAT_B,和一个第三方服务器server


    流程如下。


    step1和2: A主动去连server,此时A对应的NAT_A就会留下A的内网地址和外网地址的映射关系,server也拿到了A对应的外网IP地址和端口。


    step3和4: B的操作和A一样,主动连第三方server,NAT_B内留下B的内网地址和外网地址的映射关系,然后server也拿到了B对应的外网IP地址和端口。


    step5和step6以及step7: 重点来了。此时server发消息给A,让A主动发UDP消息到B的外网IP地址和端口。此时NAT_B收到这个A的UDP数据包时,这时候根据NAT_B的设置不同,导致这时候有可能NAT_B能直接转发数据到B,那此时A和B就通了。但也有可能不通,直接丢包,不过丢包没关系,这个操作的目的是给NAT_A上留下有关B的映射关系


    step8和step9以及step10: 跟step5一样熟悉的配方,此时server再发消息给B,让B主动发UDP消息到A的外网IP地址和端口。NAT_B上也留下了关于A到映射关系,这时候由于之前NAT_A上有过关于B的映射关系,此时NAT_A就能正常接受B的数据包,并将其转发给A。到这里A和B就能正常进行数据通信了。这就是所谓的NAT打洞


    step11: 注意,之前我们都是用的UDP数据包,目的只是为了在两个局域网的NAT上打个洞出来,实际上大部分应用用的都是TCP连接,所以,这时候我们还需要在A主动向B发起TCP连接。到此,我们就完成了两端之间的通信。


    NAT打洞


    这里估计大家会有疑惑。


    端口已经被udp用过了,TCP再用,那岂不是端口重复占用(address already in use)?

    其实并不会,端口重复占用的报错常见于两个TCP连接在不使用SO_REUSEADDR的情况下,重复使用了某个IP端口。而UDP和TCP之间却不会报这个错。之所以会有这个错,主要是因为在一个linux内核中,内核收到网络数据时,会通过五元组(传输协议,源IP,目的IP,源端口,目的端口)去唯一确定数据接受者。当五元组都一模一样的时候,内核就不知道该把数据发给谁。而UDP和TCP之间"传输协议"不同,因此五元组也不同,所以也就不会有上面的问题。五元组


    NAPT还分为好多种类型,上面的nat打洞方案,都能成功吗?

    关于NAPT,确实还细分为好几种类型,比如完全锥形NAT和限制型NAT啥的,但这并不是本文的重点。所以我就略过了。我们现在常见的都是锥形NAT。上面的打洞方案适用于大部分场景,这其中包括限制最多的端口受限锥形NAT


    1668247032737


    总结



    • IPV4地址有限,但通过NAT路由器,可以使得整个内网N多台机器,对外只使用一个公网IP,大大节省了IP资源。

    • 内网机子主动连接公网IP,中间的NAT会将内网机子的内网IP转换为公网IP,从而实现内网和外网的数据交互。

    • 普通的NAT技术,只会修改网络包中的发送端和接收端IP地址,当内网设备较多时,将有可能导致冲突。因此一般都会使用NAPT技术,同时修改发送端和接收端的IP地址和端口

    • 由于NAT的存在,公网IP是无法访问内网服务的,但通过内网穿透技术,就可以让公网IP访问内网服务。一波操作下来,就可以在公司的网络里访问家里的电脑。


    最后留个问题,有了NAT之后,原本并不富裕的IPv4地址突然就变得非常够用了。


    那我们为什么还需要IPv6?


    另外IPv6号称地址多到每粒沙子都能拥有自己的IP地址,那我们还需要NAT吗?


    最后


    最近原创更文的阅读量稳步下跌,思前想后,夜里辗转反侧。


    我有个不成熟的请求。





    离开广东好长时间了,好久没人叫我靓仔了。


    大家可以在评论区里,叫我一靓仔吗?


    最近评论区里叫我diao毛的兄弟越来越多了。


    so emo. 哪有什么diao毛,在你面前的,不过是一个漂泊在外,思念故乡的可怜打工人而已。


    所以。


    我这么善良质朴的愿望,能被满足吗?


    别说了,一起在知识的海洋里呛水吧


    作者:小白debug
    来源:juejin.cn/post/7170850066473680927
    收起阅读 »

    一个月后,我们又从 MySQL 双主切换成了主 - 从

    一、遇到的坑 一个月前,我们在测试环境部署了一套 MySQL 高可用架构,也就是 MySQL 双主 + Keepalived 的模式。 在这一个月遇到了很多坑: 因为两个 MySQL 节点都可以写入,极其容易造成主键重复,进而导致主从同步失败。 同步失败后,...
    继续阅读 »

    一、遇到的坑


    一个月前,我们在测试环境部署了一套 MySQL 高可用架构,也就是 MySQL 双主 + Keepalived 的模式。


    在这一个月遇到了很多坑



    • 因为两个 MySQL 节点都可以写入,极其容易造成主键重复,进而导致主从同步失败。

    • 同步失败后,Slave_SQL_Thread 线程就停了,除非解决了同步的错误,才能继续进行同步。

    • 同步失败的错误,不会只有一条记录有问题,往往是一大片的同步问题。

    • 两个节点互相缺少对方的数据。

    • 主从的同步延迟,切换到新主库后,数据不是最新。

    • 当出现不一致时,无法确定以哪个库为准。


    造成上面问题的主要原因就是因为两个节点都支持写入 + 双主可以随时切换。


    解决这种问题的方案有 改进自增主键的步长(影响未评估),使用 GTID 方案(未验证)。即使这样,双主同步的风险还是有,而且不同步后,如何处理是个大难题。


    那么回到我们最初的想法:为什么会选择双主?


    最开始的目的就是为了高可用。双主就是说有一台 MySQL 节点挂了,另外一台能够顶上,对于用户来说是无感的,给运维人员一定的缓冲时间来排查 MySQL 故障。另外老的主节点恢复后,不用改配置就能立即成为从节点。


    经过这一个月的 MySQL 双主模式的试运行,最后我们还是决定切换到 MySQL 主 - 从模式。


    双主模式就是两个节点即是主节点也是从节点,那我们现在切换到一主一从模式,就可以认为是降级。接下来我们聊聊双主换成主从的思路和步骤。


    二、双主降为主从


    双主模式


    双主模式的原理图如下:



    两个主节点,都安装了 KeepAlived 高可用组件,对外提供了一个 VIP,只有一个节点接管 VIP,客户端访问的请求都是到这个 VIP,另外一个节点处于待机状态。


    主从模式


    和双主不一样的地方如下,从节点是只读的。



    一主一从是主从模式中的一种,具有以下特点:



    • 一个主节点,一个从节点,主节点提供给客户端访问,从节点只通过主节点的 binlog 进行数据同步。

    • 从节点是只读的。从节点可以作为只读节点提供类似报表查询等耗时读操作。

    • 主节点宕机后,从节点成为主节点,也是高可用的一种方案。


    相对于双主的高可用方案,不同之处如下:



    • 主从切换需要用脚本将从库设置为可读可写。

    • 主从切换后,需要将从库设置为不同步老主库。

    • 主从切换后,老的主库恢复后,需要人工设置为只读,且开启同步新主库的功能。


    这样来看,主从模式在异常情况下,多了些人工操作。


    在异常情况下,主从切换一般是这样处理的:通过脚本监测主节点是否宕机,如果主库宕机了,则从库自动切换为新的主库,待老主库恢复后,就作为从库同步新主库数据,新主库上的 Keepalived 接管 VIP。


    目前改为主从模式有两种方式:



    • 简单方式:人工切换模式,主节点故障后需要人工切换主从。

    • 复杂方式:高可用方式,主节点故障后,主从自动切换,读写分离自动切换。


    本篇只涉及简单方式,复杂方式的原理和配置步骤放到下篇专门讲解。


    三、改为主从的简单方式


    简单方式的主从切换流程如下:



    和双主模式的主从切换的区别是,从节点是只读的,Keepalived 没有启动,需要人工操作主从切换和启动 Keepalived。


    修改配置的步骤如下


    ① 为了避免从节点上的 Keepalived 自动接管 VIP 的情况出现,将从节点的 Keepalived 停止,如果遇到主节点故障,则需要人工干预来进行主从切换。从节点切换为主节点后,重新启动从节点 Keepalived。


    systemctl status keepalived

    ② 保留主节点的 Keepalived,保证 MySQL 的连接信息都不需要变。


    ③ 主节点 node1 停用 MySQL 的同步线程。


    STOP SLAVE

    ④ 从节点 node2 设置 MySQL 为只读模式。


    # 修改 my.cnf 文件read_only = 1

    ⑤ 移除主节点 node1 同步 node2 MySQL 的权限。


    ⑥ 从节点 node1 的开机启动项中移除 keepalived 服务自启动。


    # 修改启动项配置sudo vim /etc/rc.local# 移除以下脚本systemctl start keepalived

    四、总结


    双主高可用的坑确实比较多,没有 MySQL 的硬核知识真的很难搞定。笔者在这一个月的实践中,深刻体会到了双主同步的难点所在,最后还是选择了一主一从的模式。


    另外因为最开始的配置都是双主模式下的,所以要修改一些配置,来改为主从模式。因项目时间比较紧,目前采取的是非高可用的主从模式。


    作者:优雅程序员阿鑫
    来源:juejin.cn/post/7136841690802814989
    收起阅读 »

    所有开发者注意,苹果审核策略有变

    iOS
    访问敏感数据的 App 新规 苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。 这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过信息指纹收集有关用户设备的信息。 早在今年 6 月...
    继续阅读 »


    访问敏感数据的 App 新规


    苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。


    这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过信息指纹收集有关用户设备的信息。


    早在今年 6 月的 WWDC23 上苹果就宣布,开发人员需要在其应用程序的隐私清单中声明使用某些 API 的原因,目前正式放出了这份需要声明的 API 列表。


    新规详情


    从今年(2023年)秋天开始,大概是 9 月中旬左右,如果你将你的 App 上传到 App Store Connect,你的应用程序使用到了需要声明原因的 API(也包括你引入的第三方 SDK),但是你没有在隐私清单文件中添加原因,那么 Apple 会给你发送一封警告性的邮件。


    从 2024 年春季开始,大概是 3 月左右,没有在隐私清单文件中说明使用原因的 App 将会被拒审核。


    需要声明原因的 API 有哪些?


    1、NSUserdefaults 相关 API


    这个 API 是被讨论最多争议最大的,因为几乎每个 App 都会用到,而且因为有沙盒保护,每个 app 的存储空间是隔离的,这都要申报理由,的确十分荒谬。


    2、获取文件时间戳相关的 API

    • creationDate
    • modificationDate
    • fileModificationDate
    • contentModificationDateKey
    • creationDateKey
    • getattrlist(::::_:)
    • getattrlistbulk(::::_:)
    • fgetattrlist(::::_:)
    • stat
    • fstat(::)
    • fstatat(::::)
    • lstat(::)
    • getattrlistat(::::::)

    3、获取系统启动时间的 API


    大多数衡量 App 启动时间的 APM 库会用到这个 API。

    • systemUptime
    • mach_absolute_time()

    4、磁盘空间 API

    • volumeAvailableCapacityKey
    • volumeAvailableCapacityForImportantUsageKey
    • volumeAvailableCapacityForOpportunisticUsageKey
    • volumeTotalCapacityKey
    • systemFreeSize
    • systemSize
    • statfs(::)
    • statvfs(::)
    • fstatfs(::)
    • fstatvfs(::)
    • getattrlist(::::_:)
    • fgetattrlist(::::_:)
    • getattrlistat(::::::)

    5、活动键盘 API


    这个 API 可以来确定当前用户文本输入的主要语言,有些 App 可能会用来标记用户。

    • activeInputModes


    如何在 Xcode 中配置


    由于目前 Xcode 15 正式版还没有发布,下边的操作是在 Beta 版本进行的。


    在 Xcode 15 中隐私部分全部归类到了一个后缀为 .xcprivacy 的文件中,创建项目时默认没有生成这个文件,我们先来创建一下。


    打开项目后,按快捷键 Command + N 新建文件,在面板中搜索 privacy,选择 App Pirvacy 点击下一步创建这个文件。



    这个文件是个 plist 格式的面板,默认情况下长这样:



    然后点击加号,创建一个 Privacy Accessed API TypesKey,这是一个数组,用来包含所有你 App 使用到需要申明原因的 API。



    在这个数组下继续点击加号,创建一个 Item,会看到两个选项:

    • Privacy Accessed API Type:用到的 API 类型
    • Privacy Accessed API Reasons:使用这个 API 的原因(也是个数组,因为可能包含多个原因)



    这两个 Key 都创建出来,然后在 Privacy Accessed API Type 一栏点击右侧的菜单,菜单中会列出上边提到的所有 API,选择你需要申报的 API,我这里就拿 UserDefault 来举例:



    然后在 Privacy Accessed API Reasons 一览中点击加号,在右侧的选项框中选择对应的原因,每个 API 对应的原因都会列出来,可以到苹果的官方文档上查看这个 API 的原因对应的是哪个,比如 UserDefault 对应的是 CA92.1,我这里就选择这个:



    到此,申报原因就完成了,原因不需要自己填写,直接使用苹果给出的选项就可以了,还是蛮简单的。


    参考资料


    [1]公告原文: developer.apple.com/news/?id=z6…


    [2]需要在 App 内声明的 API 列表: developer.apple.com/documentati…


    [3]API 列表对应的原因: developer.apple.com/documentati…


    作者:杂雾无尘
    来源:juejin.cn/post/7267091810379759676
    收起阅读 »

    懂点心理学 - 踢猫效应

    懂点心理学,生活工作两不误~ 什么是踢猫效应 某公司董事长为了重整公司事务,许诺自己将早到晚归。有一次,他在家看报太入迷以至于忘记了时间,为了不迟到,他在公路上超速驾驶,结果被警察开了罚单,最后还耽误了时间。这位老董愤怒之极,回到办公室,为了转移他人的注意,...
    继续阅读 »

    pexels-yana-kangal-17591717.jpg


    懂点心理学,生活工作两不误~


    什么是踢猫效应



    某公司董事长为了重整公司事务,许诺自己将早到晚归。有一次,他在家看报太入迷以至于忘记了时间,为了不迟到,他在公路上超速驾驶,结果被警察开了罚单,最后还耽误了时间。这位老董愤怒之极,回到办公室,为了转移他人的注意,他将销售经理叫到办公室训斥了一顿。


    销售经理挨训之后,气急败坏地走出办公室,将秘书叫到自己的办公室并对他挑剔一顿。


    秘书无言无故被人挑剔,自然是一肚子气,故意找接线员的茬儿。


    接线员无可奈何垂头丧气地回到家,对着自己的儿子大发雷霆。


    儿子莫名其妙地被父亲痛斥之后,也很恼火,便将自己家里的猫狠狠地踢了一脚。



    可怜的猫🥺。


    提猫效应指的是坏情绪的传达,并且承受最多最痛的人,是弱小的群体。


    其他案例


    笔者在工作中也会遇到这样的人:



    **主管,因为任务的延期完成或者任务出现问题,并没能完成上面的 OKR。受到经理的警告,然后 TA 会找茬自己手下的人,对其一顿 PUA。有时候,TA 还直接跟经理火拼,美其名曰为公司着想,吵着吵着就哭起来,然后任性连休几天假...



    这是一种病态的社会表现。也是个人情绪无法抑制的无良的宣泄渠道。更是一种伤害集体利益和他人利益的一种行为。


    当然,现实生活中还有很多这种鲜活的案例,比如:


    所谓的专家,建议老百姓城市买房去库存,买车带动经济,再开车回农村耕田。升斗小民们可真是惨啊~


    如何应对



    1. 认清自己的角色:自己的角色是相对,有时候你是弱者,有时候你是施暴者。我们要正确认识到自己当前所处的角色,尊重善待无辜的人和动物。

    2. 情绪管理:人不是神,会有喜怒哀乐愁,适当给自己放松一下,拥抱下大自然,请别压抑情绪,更不能为了释放情绪而转嫁情绪给他人。「亲人最容易受伤」

    3. 规范和法律:当自己的权益受到了侵害,需要我们拿起法律的武器。比如,公司无缘无故裁了你,但是又没有给赔偿你,甚至在推荐信上恶意差评你。


    参考


    收起阅读 »

    都用HTTPS了,还能被查出浏览记录?

    大家好,我卡颂。 最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥? 实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如: DNS查询:通常DN...
    继续阅读 »

    大家好,我卡颂。


    最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥?



    实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如:




    • DNS查询:通常DNS查询是不会加密的,所以,能看到你DNS查询的观察者(比如运营商)是可以推断出访问的网站




    • IP地址:如果一个网站的IP地址是独一无二的,那么只需看到目标 IP地址,就能推断出用户正在访问哪个网站。当然,这种方式对于多网站共享同一个IP地址(比如CDN)的情况不好使




    • 流量分析:当访问一些网站的特定页面,可能导致特定大小和顺序的数据包,这种模式可能被用来识别访问的网站




    • cookies或其他存储:如果你的浏览器有某个网站的cookies,显然这代表你曾访问过该网站,其他存储信息(比如localStorage)同理




    除此之外,还有很多方式可以直接、间接知道你的网站访问情况。


    本文将聚焦在HTTPS协议本身,聊聊只考虑HTTPS协议的情况下,你的隐私是如何泄露的。


    欢迎围观朋友圈、加入人类高质量前端交流群,带飞


    HTTPS简介


    我们每天访问的网站大部分是基于HTTPS协议的,简单来说,HTTPS = HTTP + TLS,其中:




    • HTTP是一种应用层协议,用于在互联网上传输超文本(比如网页内容)。由于HTTP是明文传递,所以并不安全




    • TLS是一种安全协议。TLS在传输层对数据进行加密,确保任何敏感信息在两端(比如客户端和服务器)之间安全传输,不被第三方窃取或篡改




    所以理论上,结合了HTTPTLS特性的HTTPS,在数据传输过程是被加密的。但是,TLS建立连接的过程却不一定是加密的。


    TLS的握手机制


    当我们通过TLS传递加密的HTTP信息之前,需要先建立TLS连接,比如:




    • 当用户首次访问一个HTTPS网站,浏览器开始查询网站服务器时,会发生TLS连接




    • 当页面请求API时,会发生TLS连接




    建立连接的过程被称为TLS握手,根据TLS版本不同,握手的步骤会有所区别。



    但总体来说,TLS握手是为了达到三个目的:




    1. 协商协议和加密套件:通信的两端确认接下来使用的TLS版本及加密套件




    2. 验证省份:为了防止“中间人”攻击,握手过程中,服务器会向客户端发送其证书,包含服务器公钥和证书授权中心(即CA)签名的身份信息。客户端可以使用这些信息验证服务器的身份




    3. 生成会话密钥:生成用于加密接下来数据传输的密钥




    TLS握手机制的缺点


    虽然TLS握手机制会建立安全的通信,但在握手初期,数据却是明文发送的,这就造成隐私泄漏的风险。


    在握手初期,客户端、服务端会依次发送、接收对方的打招呼信息。首先,客户端会向服务端打招呼(发送client hello信息),该消息包含:




    • 客户端支持的TLS版本




    • 支持的加密套件




    • 一串称为客户端随机数client random)的随机字节




    • SNI等一些服务器信息




    服务端接收到上述消息后,会向客户端打招呼(发送server hello消息),再回传一些信息。


    其中,SNIServer Name Indication,服务器名称指示)就包含了用户访问的网站域名。


    那么,握手过程为什么要包含SNI呢?


    这是因为,当多个网站托管在一台服务器上并共享一个IP地址,且每个网站都有自己的SSL证书时,那就没法通过IP地址判断客户端是想和哪个网站建立TLS连接,此时就需要域名信息辅助判断。


    打个比方,快递员送货上门时,如果快递单只有收货的小区地址(IP地址),没有具体的门牌号(域名),那就没法将快递送到正确的客户手上(与正确的网站建立TLS连接)。


    所以,SNI作为TLS的扩展,会在TLS握手时附带上域名信息。由于打招呼的过程是明文发送的,所以在建立HTTPS连接的过程中,中间人就能知道你访问的域名信息。


    企业内部防火墙的访问控制和安全策略,就是通过分析SNI信息完成的。



    虽然防火墙可能已经有授信的证书,但可以先分析SNI,根据域名情况再判断要不要进行深度检查,而不是对所有流量都进行深度检查



    那么,这种情况下该如何保护个人隐私呢?


    Encrypted ClientHello


    Encrypted ClientHelloECH)是TLS1.3的一个扩展,用于加密Client Hello消息中的SNI等信息。


    当用户访问一个启用ECH的服务器时,网管无法通过观察SNI来窥探域名信息。只有目标服务器才能解密ECH中的SNI,从而保护了用户的隐私。



    当然,对于授信的防火墙还是不行,但可以增加检查的成本



    开启ECH需要同时满足:




    • 服务器支持TLSECH扩展




    • 客户端支持ECH




    比如,cloudflare SNI测试页支持ECH扩展,当你的浏览器不支持ECH时,访问该网站sni会返回plaintext



    对于chrome,在chrome://flags/#encrypted-client-hello中,配置ECH支持:



    再访问上述网站,sni如果返回encrypted则代表支持ECH


    总结


    虽然HTTPS连接本身是加密的,但在建立HTTPS的过程中(TLS握手),是有数据明文传输的,其中SNI中包含了服务器的域名信息。


    虽然SNI信息的本意是解决同一IP下部署多个网站,每个网站对应不同的SSL证书,但也会泄漏访问的网站地址


    ECH通过对TLS握手过程中的敏感信息(主要是SNI)进行加密,为用户提供了更强的隐私保护。


    作者:魔术师卡颂
    来源:juejin.cn/post/7264753569834958908
    收起阅读 »

    被一个问题卡了近两天,下班后我哭了。。。

    写在前面 好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。 近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。 好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。 整个过程 没经过...
    继续阅读 »

    写在前面


    好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。


    近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。


    好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。


    整个过程


    没经过深思熟虑的计划制定


    两周前,组内同事想让我帮忙做冒烟测试脚本,原因是因为每次发版测试的时间耗时特别长,所以在结束批量测试工具的开发工作后,我便主动和领导请缨做冒烟测试脚本的开发工作。



    和领导说,脚本开发需要5天,整个冒烟测试每次需要大约5分钟!



    领导听完很吃惊,我自己说应该差不多吧。


    迷之自信?


    可能很多同学也会和我的领导一样吃惊,为什么?


    系统发版后的回归测试,就测试场景和流程来看,工作量肯定不小,姑且不说技术问题,就业务流程的梳理就很费时间了。


    而我却说整个过程只需要五天,可见我是多想证明自己了


    其实不然,我自己还是有一些考量的,才说出五天,原因有两个:



    • 因为信任,所以备受期待,同事信任我,真的感觉自己被需要,并且想为团队贡献出一份自己的力量;

    • 因为之前做过测试环境的性能测试脚本,以为很多接口可以直接拿来就用(我天真了,因为改了不少,需要重做)。


    理性永远在给感性收拾烂摊子


    整个系统总共6个测试流程,也就是说我每天要完成1.2个流程的脚本开发。


    我特别喜欢现在团队的氛围,第一天到下班点时,差一个模块就完成了一个流程。


    所以在责任心的驱使下,心想加个班吧,今天能赶出来这个模块,明天其他的流程就能复用了。


    一切看似很好,也正是这个模块把我彻底卡住了,我遇到了一个让我很抓狂的问题:



    打个类比,比如发起申请接口,申请成功了,到领导审批,点击同意的时候报错,而发起申请这个接口却不报错,你在页面同样的操作,领导同意却是正常好用的。



    被问题卡住,心态开始崩盘


    这个问题,我反复查了近两天......


    这期间我积极的找开发同事帮忙排查问题,并确认是否是我的入参不对导致节点数据不正确。


    由于开发同事比较忙,能帮我排查问题的时间有限,所以只有在开发稍微有点时间,才能帮忙排查联调。


    也正因为开发同事的尽心尽力帮忙,几次下来,让我感觉离问题根源好像又进了一步。


    也知道为什么不能审批了,因为虽然请求成功了,但是没走业务逻辑,导致部分数据还是默认值,所以审批报错。


    关于入参的排查,暂时告一段落了,因为数据状态不对,无法进行审批,意味着还是没有解决问题。


    到这已经是第三天了,一个流程都没整完,感觉整个人都不好了,心态有点崩了......


    于是向领导说明原因,领导了解后,并说先把耗时最长的做完,虽然没那么大压力,但是心里还是有些深深地自责。


    我还是没忍住,终于哭了出来......


    距离周五晚上发版测试还有两天,这个问题不解决,怎么也说不过去,心里一直憋着这个劲特别难受。


    当时的想法,真的是谁能帮帮我,帮帮我行么?


    但是我也不知道该找谁帮忙,谁又能帮助我?


    为什么?说是业务问题吧?还不算?技术问题吧,入参还查不出来啥问题?真的就是进退两难!


    因为开发太忙,实在没时间,暂时也没想到什么好的解决办法,我就先下班回了家。


    把车停好后,习惯性地给女友打了电话,那天还是我的生日,再加上那阶段烦心事特别多,说着说着我哭了出来,突然感觉好无助而且很没用,最后彻底哭了出来,为什么就那么难?


    我以为我很颓废,今天我才知道,原来我早废了。


    因为烦心事特别多,导致整个人都不好了,哭出来后,感觉真的很舒服,而且整个人平和了许多。


    没人能教你,只有自己能拯救自己


    回到家后,搭建好环境,改用工具进行测试,使用jmeter+fiddler抓包开始,重新调接口来模拟测试,结果居然成功了,真的很意外,难道是我代码写的有问题?


    第二天上班,我把自己代码接口调用及入参与昨天做好的jmeter脚本一一对照,发现入参一模一样,这让我产生了怀疑,是我封装的工具类有问题?


    我代码走的HTTP协议,而jmeter脚本是HTTPS协议才成功的。


    这让我想到,可能我的httpclient需要走HTTPS协议请求会让接口调用后,数据应该会正常显示吧。


    有了思路,就开始找httpclient如何进行HTTPS请求的相关文章。


    经过一番搜索,找到的重点都是围绕使用ssl和根证书的使用的代码片段,我又对httpclient底层封装进行改造,改造完再次使用封装工具类调用接口,结果还是数据状态不对,我真的彻底绝望了。


    于是,我又去找到了强哥(我北京的同事),强哥说你干嘛自己封装,用hutool呀。


    我照着强哥的思路,又去照着hutool中的工具类,开始写demo,逐一调用接口,结果竟然成功了,这让我欣喜若狂,真的好用。


    于是,我对写好的demo,再次进行封装,也就是hutool中的工具类封装,封装好后,再次使用封装好的工具类调用,结果数据状态又不对了。


    我真的服了,这是玩我吗?分开就好使,封装就不行。


    有的同学说了,应该是你封装的有问题,那为什么其他模块都好用,就这个模块不行?


    后来,我灵机一动,那就都对分开可用这部分代码进行简单封装,保证流程跑通就行,算是退而求其次的解决方法,虽然,它很low,但是能用。


    也正因为这个临时解决方案,助力我在周五发版前成功的让同事用上了,一个流程的冒烟测试,跑完这一个流程仅需113秒,比手动回归快了近10倍的时间。


    写在最后


    整个过程让我记忆深刻,在此特别记录一下,真的是头一次被问题卡的这么难受,那种既生气,又干不掉难题的感觉,太难受了!


    你有被难题阻塞,一直无法继续下去的情况吗?

    作者:软件测试君
    来源:juejin.cn/post/7135473631559811080
    欢迎文末给我留言哦!

    收起阅读 »

    鹅厂组长,北漂 10 年,有房有车,做了一个违背祖宗的决定

    前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。 抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」...
    继续阅读 »

    前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。


    抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」时腾讯做的「艰难的决定」来的轻松。如今距离这个决定过去了快 3 个月,我也还在适应着这个决定带来的变化。


    按照工作汇报的习惯,先说结论:



    在北漂整整 10 年后,我回老家合肥上班了



    做出这个决定的唯一原因:



    没有北京户口,积分落户陪跑了三年,目测 45 岁之前落不上



    户口搞不定,意味着孩子将来在北京只能考高职,这断然是不能接受的;所以一开始是打算在北京读几年小学后再回老家,我也能多赚点钱,两全其美。


    因为我是一个人在北京,如果在北京上小学,就得让我老婆或者让我父母过来。可是我老婆的职业在北京很难就业,我父母年龄大了,北京人生地不熟的,而且那 P 点大的房子,住的也憋屈。而将来一定是要回去读书的,这相当于他们陪着我在北京折腾了。


    或者我继续在北京打工赚钱,老婆孩子仍然在老家?之前的 6 年基本都是我老婆在教育和陪伴孩子,我除了逢年过节,每个月回去一到两趟。孩子天生过敏体质,经常要往医院跑,生病时我也帮不上忙,所以时常被抱怨”丧偶式育儿“,我也只能跟渣男一样说些”多喝热水“之类的废话。今年由于那啥,有整整 4 个多月没回家了,孩子都差点”笑问客从何处来“了。。。


    5月中旬,积分落户截止,看到贴吧上网友晒出的分数和排名,预计今年的分数线是 105.4,而实际分数线是 105.42,比去年的 100.88 多了 4.54 分。而一般人的年自然增长分数是 4 分,这意味着如果没有特殊加分,永远赶不上分数线的增长。我今年的分数是 90.8,排名 60000 左右,每年 6000 个名额,即使没有人弯道超车,落户也得 10 年后了,孩子都上高一了,不能在初二之前搞到户口,就表示和大学说拜拜了。


    经过我的一番仔细的测算,甚至用了杠杆原理和人品守恒定理等复杂公式,最终得到了如下结论:



    我这辈子与北京户口无缘了



    所以,思前想后,在没有户口的前提下,无论是老婆孩子来北京,还是继续之前的异地,都不是好的解决方案。既然将来孩子一定是在合肥高考,为了减少不必要的折腾,那就只剩唯一的选择了,我回合肥上班,兼顾下家里。


    看上去是个挺自然的选择,但是:



    我在腾讯是组长,团队 20 余人;回去是普通工程师,工资比腾讯打骨折



    不得不说,合肥真的是互联网洼地,就没几个公司招人,更别说薪资匹配和管理岗位了。因此,回合肥意味着我要放弃”高薪“和来之不易的”管理“职位,从头开始,加上合肥这互联网环境,基本是给我的职业生涯判了死刑。所以在 5 月底之前就没考虑过这个选项,甚至 3 月份时还买了个显示器和 1.6m * 0.8m 的大桌子,在北京继续大干一场,而在之前的 10 年里,我都是用笔记本干活的,从未用过外接显示器。


    5 月初,脉脉开始频繁传出毕业的事,我所在的部门因为是盈利的,没有毕业的风险。但是营收压力巨大,作为底层的管理者,每天需要处理非常非常多的来自上级、下级以及甲方的繁杂事务,上半年几乎都是凌晨 1 点之后才能睡觉。所以,回去当个普通工程师,每天干完手里的活就跑路,貌似也不是那么不能接受。毕竟自己也当过几年 leader 了,leader 对自己而言也没那么神秘,况且我这还是主动激流勇退,又不是被撸下来的。好吧,也只能这样安慰自己了,中年人,要学会跟自己和解。后面有空时,我分享下作为 leader 和普通工程师所看到的不一样的东西。


    在艰难地说服自己接受之后,剩下的就是走各种流程了:

    1. 5月底,联系在合肥工作的同学帮忙内推;6月初,通过面试。我就找了一家,其他家估计性价比不行,也不想继续面了
    2. 6月底告诉总监,7月中旬告诉团队,陆续约或被约吃散伙饭
    3. 7月29日,下午办完离职手续,晚上坐卧铺离开北京
    4. 8月1日,到新公司报道

    7 月份时,我还干了一件大事,耗时两整天,历经 1200 公里,不惧烈日与暴雨,把我的本田 125 踏板摩托车从北京骑到了合肥,没有拍视频,只能用高德的导航记录作为证据了:




    这是导航中断的地方,晚上能见度不行,在山东花了 70 大洋,随便找了个宾馆住下了,第二天早上出发时拍的,发现居然是水泊梁山附近,差点落草为寇:




    骑车这两天,路上发生了挺多有意思的事,以后有时间再分享。到家那天,是我的结婚 10 周年纪念日,我没有提前说我要回来,更没说骑着摩托车回来,当我告诉孩子他妈时,问她我是不是很牛逼,得到的答复是:



    我觉得你是傻逼



    言归正传,在离开北京前几天,我找团队里的同学都聊了聊,对我的选择,非常鲜明的形成了两个派系:

    1. 未婚 || 工作 5 年以内的:不理解,为啥放弃管理岗位,未来本可以有更好的发展的,太可惜了,打骨折的降薪更不能接受

    2. 已婚 || 工作 5 年以上的:理解,支持,甚至羡慕;既然迟早都要回去,那就早点回,多陪陪家人,年龄大了更不好回;降薪很正常,跟房价也同步,不能既要又要

    确实,不同的人生阶段有着不同的想法,我现在是第 2 阶段,需要兼顾家庭和工作了,不能像之前那样把工作当成唯一爱好了。


    在家上班的日子挺好的,现在加班不多,就是稍微有点远,单趟得 1 个小时左右。晚上和周末可以陪孩子玩玩,虽然他不喜欢跟我玩🐶。哦,对了,我还有个重要任务 - 做饭和洗碗。真的是悔不当初啊,我就不应该说会做饭的,更不应该把饭做的那么好吃,现在变成我工作以外的最重要的业务了。。。


    比较难受的是,现在公司的机器配置一般,M1 的 MBP,16G 内存,512G 硬盘,2K 显示器。除了 CPU 还行,内存和硬盘,都是快 10 年前的配置了,就这还得用上 3 年,想想就头疼,省钱省在刀刃上了,属于是。作为对比,腾讯的机器配置是:



    M1 Pro MBP,32G 内存 + 1T SSD + 4K 显示器


    客户端开发,再额外配置一台 27寸的 iMac(i9 + 32G内存 + 1T SSD)



    由奢入俭难,在习惯了高配置机器后,现在的机器总觉得速度不行,即使很多时候,它和高配机没有区别。作为开发,尤其是客户端开发,AndroidStudio/Xcode 都是内存大户,16G 实在是捉襟见肘,非常影响搬砖效率。公司不允许用自己的电脑,否则我就自己买台 64G 内存的 MBP 干活用了。不过,换个角度,编译时间变长,公司提供了带薪摸鱼的机会,也可以算是个福利🐶


    另外,比较失落的就是每个月发工资的日子了,比之前少了太多了,说没感觉是不可能的,还在努力适应中。不过这都是小事,毕竟年底发年终奖时,会更加失落,hhhh😭😭😭😭


    先写这么多吧,后面有时间的话,再分享一些有意思的事吧,工作上的或生活上的。


    遥想去年码农节时,我还在考虑把房子从昌平换到海淀,好让孩子能有个“海淀学籍”,当时还做了点笔记:




    没想到,一年后的我回合肥了,更想不到一年后的腾讯,股价竟然从 500 跌到 206 了(10月28日,200.8 了)。真的是世事难料,大家保重身体,好好活着,多陪陪家人,一起静待春暖花开💪🏻💪🏻


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

    程序员崩溃的40个瞬间!!!太形象了,你遇到几个?

    说到程序员,在外界眼里他们是掌控代码的大神他们是改变世界的王者其实程序员的工作不容易不信就来看看程序员崩溃的各种瞬间——1.公司实习生找bug2.在调试时,将断点设置在错误的位置3.当我有一个很棒的调试想法4.偶然间看到自己多年前写的代码5.当我第一次启动我的...
    继续阅读 »

    说到程序员,在外界眼里

    他们是掌控代码的大神

    他们是改变世界的王者

    其实程序员的工作不容易

    不信

    就来看看程序员崩溃的各种瞬间——

    1.公司实习生找bug


    2.在调试时,将断点设置在错误的位置


    3.当我有一个很棒的调试想法


    4.偶然间看到自己多年前写的代码


    5.当我第一次启动我的单元测试


    6.数据库的delete语句忘了使用限定词where


    7.明明是个小bug,但就是死活修不好


    8.当我尝试调整生产数据库中的一些东西时

    动图封面

    9.好像真的没人发现我产品里的bug


    10.下班前我还有一项任务没有完成


    11.产品还没测试直接投入生产时


    12.调试过多线程的都会懂


    13.当我以为已捕获了所有可能的异常...的时候


    14.当我试图清理几行所谓的旧代码


    15.当有人让我帮他调试代码时


    16.程序员第一次向老板演示项目


    17.当你看到你几个月没碰过的代码


    18.接到产品经理电话的我睡意全无


    19.测试的时候一切ok,真正上线的时候……


    20.作为一个程序员,拷问灵魂的时刻到了


    21.当年学C语言的过程


    22.当前端程序员想改后台代码时,后台程序员的样子


    23.调试bug


    24.正在调试,突然内存溢出了


    25.需求文档又改了


    26.苦逼的后端工程师


    27.后端工程师做UI的活


    28.在生产环境做hotfix


    29.刚调稳定的系统,公司叕空降了一位架构师,又双叕要重构现有系统……


    30.当程序员听客户说还在用IE时


    31.功能先上了再说


    32.新手程序员第一次做项目的过程


    33.零错误零警告一次编译通过


    34.春节前后上班写代码状态是这样的


    35.被老板委派接手刚刚离职同事的项目...


    36.准备下班的时候,测试又提bug过來了…


    37.测试刚写完的代码


    38.当我以为我修复了一个bug


    39.程序员新手尝试新框架的时候


    40.当我第一次测试我的代码时


    41.我设计的接口和别人调用我的接口(好疼)


    42.高级开发人员作为一个团队进行编程时


    43.不小心碰到了遗留代码,真惨


    本文转自知乎专栏 互联网视界https:/

    收起阅读 »

    Hook神器—Frida安装

    什么是Frida Frida is Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It ...
    继续阅读 »

    什么是Frida



    Frida is Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript into native apps that run on Windows, Mac, Linux, iOS and Android. Frida is an open source software.



    frida 是平台原生 appGreasemonkey ,说的专业一点,就是一种动态插桩工具,可以插入一些代码到原生 app 的内存空间去,(动态地监视和修改其行为),这些原生平台可以是WinMacLinuxAndroid 或者 iOS 。而且 frida 还是开源的。


    Frida安装


    Python3安装


    安装Frida之前需要电脑有Python环境的,Python3的安装可以参考这篇文章。Python安装之后,还要检查是否安装了pip3,如果没有安装的话,需要自行查找安装的方法。


    安装Frida

    1.安装frida

    pip3 install frida

    2.安装frida-tools

    pip3 install frida-tools

    3.安装objection

    pip3 install objection

    执行完以上命令就完成了frida的安装,上面的命令安装的都是最新版本的


    安装frida-server


    安装frida-server之前需要知道Android手机的cpu架构,命令如下

    adb shell getprop ro.product.cpu.abi 

    还要知道电脑安装的frida的版本,frida-server的版本要与电脑端的frida版本相同,查看电脑端的frida版本的命令如下,

    frida --version

    知道了Android手机的cpu架构和frida的版本,到github下载相应版本的frida-server,github地址点击这里




    测试是否安装成功


    启动frida-server

    1. 将下载的frida-server压缩包解压
    2. 解压后的文件push到手机
    3. 启动frida-server服务

    上面步骤的对应命令如下

    $ adb root 
    $ adb push frida-server-16.0.8-android-arm /data/local/tmp
    $ adb shell
    $ su
    $ cd /data/local/tmp
    $ chmod 777 /data/local/tmp/frida-server-16.0.8-android-arm
    $ ./frida-server-16.0.8-android-arm

    端口映射


    启动frida-server之后还要进行端口映射,否则电脑无法连接到手机

    adb forward tcp:27042 tcp:27042
    adb forward tcp:27043 tcp:27043

    查看进程


    上面的步骤都完成后,就可以执行下面的命令,获取手机当前的进程信息

    frida-ps -U

    如果能看到你手机的进程信息,如图




    则说明你的frida和frida-server安装配置成功。


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

    教你如何实现接口防刷

    教你如何实现接口防刷 前言 我们在浏览网站后台的时候,假如我们频繁请求,那么网站会提示 “请勿重复提交” 的字样,那么这个功能究竟有什么用呢,又是如何实现的呢? 其实这就是接口防刷的一种处理方式,通过在一定时间内限制同一用户对同一个接口的请求次数,其目的是为了...
    继续阅读 »

    教你如何实现接口防刷


    前言


    我们在浏览网站后台的时候,假如我们频繁请求,那么网站会提示 “请勿重复提交” 的字样,那么这个功能究竟有什么用呢,又是如何实现的呢?


    其实这就是接口防刷的一种处理方式,通过在一定时间内限制同一用户对同一个接口的请求次数,其目的是为了防止恶意访问导致服务器和数据库的压力增大,也可以防止用户重复提交。


    思路分析


    接口防刷有很多种实现思路,例如:拦截器/AOP+Redis、拦截器/AOP+本地缓存、前端限制等等很多种实现思路,在这里我们来讲一下 拦截器+Redis 的实现方式。


    其原理就是 在接口请求前由拦截器拦截下来,然后去 redis 中查询是否已经存在请求了,如果不存在则将请求缓存,若已经存在则返回异常。具体可以参考下图




    具体实现



    注:以下代码中的 AjaxResult 为统一返回对象,这里就不贴出代码了,大家可以根据自己的业务场景来编写。



    编写 RedisUtils

    import com.apply.core.exception.MyRedidsException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;

    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;

    /**
    * Redis工具类
    */
    @Component
    public class RedisUtils {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /****************** common start ****************/
    /**
    * 指定缓存失效时间
    *
    * @param key 键
    * @param time 时间(秒)
    * @return
    */
    public boolean expire(String key, long time) {
    try {
    if (time > 0) {
    redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 根据key 获取过期时间
    *
    * @param key 键 不能为null
    * @return 时间(秒) 返回0代表为永久有效
    */
    public long getExpire(String key) {
    return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
    * 判断key是否存在
    *
    * @param key 键
    * @return true 存在 false不存在
    */
    public boolean hasKey(String key) {
    try {
    return redisTemplate.hasKey(key);
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 删除缓存
    *
    * @param key 可以传一个值 或多个
    */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
    if (key != null && key.length > 0) {
    if (key.length == 1) {
    redisTemplate.delete(key[0]);
    } else {
    redisTemplate.delete(CollectionUtils.arrayToList(key));
    }
    }
    }
    /****************** common end ****************/


    /****************** String start ****************/

    /**
    * 普通缓存获取
    *
    * @param key 键
    * @return 值
    */
    public Object get(String key) {
    return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
    * 普通缓存放入
    *
    * @param key 键
    * @param value 值
    * @return true成功 false失败
    */
    public boolean set(String key, Object value) {
    try {
    redisTemplate.opsForValue().set(key, value);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 普通缓存放入并设置时间
    *
    * @param key 键
    * @param value 值
    * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
    * @return true成功 false 失败
    */
    public boolean set(String key, Object value, long time) {
    try {
    if (time > 0) {
    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    } else {
    set(key, value);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 递增
    *
    * @param key 键
    * @param delta 要增加几(大于0)
    * @return
    */
    public long incr(String key, long delta) {
    if (delta < 0) {
    throw new MyRedidsException("递增因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
    * 递减
    *
    * @param key 键
    * @param delta 要减少几(小于0)
    * @return
    */
    public long decr(String key, long delta) {
    if (delta < 0) {
    throw new MyRedidsException("递减因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, -delta);
    }
    /****************** String end ****************/
    }

    定义Interceptor

    import com.alibaba.fastjson.JSON;
    import com.apply.common.utils.redis.RedisUtils;
    import com.apply.common.validator.annotation.AccessLimit;
    import com.apply.core.http.AjaxResult;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Objects;

    /**
    * @author Bummon
    * @description 重复请求拦截
    * @date 2023-08-10 14:14
    */
    @Component
    public class RepeatRequestIntercept extends HandlerInterceptorAdapter {

    @Autowired
    private RedisUtils redisUtils;

    /**
    * 限定时间 单位:秒
    */
    private final int seconds = 1;

    /**
    * 限定请求次数
    */
    private final int max = 1;

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //判断请求是否为方法的请求
    if (handler instanceof HandlerMethod) {
    String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
    Object requestCountObj = redisUtils.get(key);
    if (Objects.isNull(requestCountObj)) {
    //若为空则为第一次请求
    redisUtils.set(key, 1, seconds);
    } else {
    response.setContentType("application/json;charset=utf-8");
    ServletOutputStream os = response.getOutputStream();
    AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
    String jsonString = JSON.toJSONString(result);
    os.write(jsonString.getBytes());
    os.flush();
    os.close();
    return false;
    }
    }
    return true;
    }

    }

    然后我们 将拦截器注册到容器中

    import com.apply.common.validator.intercept.RepeatRequestIntercept;
    import com.apply.core.base.entity.Constants;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    /**
    * @author Bummon
    * @description
    * @date 2023-08-10 14:17
    */
    @Configuration
    public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private RepeatRequestIntercept repeatRequestIntercept;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(repeatRequestIntercept);
    }
    }

    我们再来编写一个接口用于测试

    import com.apply.common.validator.annotation.AccessLimit;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    /**
    * @author Bummon
    * @description
    * @date 2023-08-10 14:35
    */
    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test(){
    return "SUCCESS";
    }

    }

    最后我们来看一下结果是否符合我们的预期:


    1秒内的第一次请求:




    1秒内的第二次请求:




    确实已经达到了我们的预期,但是如果我们对特定接口进行拦截,或对不同接口的限定拦截时间和次数不同的话,这种实现方式无法满足我们的需求,所以我们要提出改进。


    改进


    我们可以去写一个自定义的注解,并将 secondsmax 设置为该注解的属性,再在拦截器中判断请求的方法是否包含该注解,如果包含则执行拦截方法,如果不包含则直接返回。


    自定义注解 RequestLimit

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    /**
    * @author Bummon
    * @description 幂等性注解
    * @date 2023-08-10 15:10
    */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RequestLimit {

    /**
    * 限定时间
    */
    int seconds() default 1;

    /**
    * 限定请求次数
    */
    int max() default 1;

    }

    改进 RepeatRequestIntercept

    /**
    * @author Bummon
    * @description 重复请求拦截
    * @date 2023-08-10 15:14
    */
    @Component
    public class RepeatRequestIntercept extends HandlerInterceptorAdapter {

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //判断请求是否为方法的请求
    if (handler instanceof HandlerMethod) {
    HandlerMethod hm = (HandlerMethod) handler;
    //获取方法中是否有幂等性注解
    RequestLimit anno = hm.getMethodAnnotation(RequestLimit.class);
    //若注解为空则直接返回
    if (Objects.isNull(anno)) {
    return true;
    }
    int seconds = anno.seconds();
    int max = anno.max();
    String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
    Object requestCountObj = redisUtils.get(key);
    if (Objects.isNull(requestCountObj)) {
    //若为空则为第一次请求
    redisUtils.set(key, 1, seconds);
    } else {
    //限定时间内的第n次请求
    int requestCount = Integer.parseInt(requestCountObj.toString());
    //判断是否超过最大限定请求次数
    if (requestCount < max) {
    //未超过则请求次数+1
    redisUtils.incr(key, 1);
    } else {
    //否则拒绝请求并返回信息
    refuse(response);
    return false;
    }
    }
    }
    return true;
    }

    /**
    * @param response
    * @date 2023-08-10 15:25
    * @author Bummon
    * @description 拒绝请求并返回结果
    */
    private void refuse(HttpServletResponse response) throws IOException {
    response.setContentType("application/json;charset=utf-8");
    ServletOutputStream os = response.getOutputStream();
    AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
    String jsonString = JSON.toJSONString(result);
    os.write(jsonString.getBytes());
    os.flush();
    os.close();
    }

    }

    这样我们就可以实现我们的需求了。


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

    Mac开发环境配置看这一篇就够了

    iOS
    前言 从 macOS Catalina 开始,Mac 使用 zsh 作为默认登录 Shell 和交互式 Shell。当然你也可以修改默认Shell,但一般没这个必要。而实际开发中经常会遇到一些环境问题导致的报错,下面我们就讲一下一些常用库的环境配置以及原理。 ...
    继续阅读 »

    前言


    macOS Catalina 开始,Mac 使用 zsh 作为默认登录 Shell 和交互式 Shell。当然你也可以修改默认Shell,但一般没这个必要。而实际开发中经常会遇到一些环境问题导致的报错,下面我们就讲一下一些常用库的环境配置以及原理。


    一、Homebrew


    作为Mac上最常用的包管理器,Homebrew可以称为神器,用它来管理Mac上的依赖环境便捷又省心。


    1. 安装


    这里我们直接在终端执行国人写的一键安装脚本,换源(官方源的速度你懂的)啥的都直接安排上了。

    /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"



    这里我们选择1、中科大下载源就好了,按照提示输入并耐心等待安装完成。






    最后一步重载配置文件我们执行source ~/.zshrc,重载用户目录下的.zshrc


    到这里我们可以执行brew -v测试一下Homebrew的安装结果:

    ~:~$brew -v
    Homebrew 3.6.21-26-gb0a74e5
    Homebrew/homebrew-core (git revision 4fbf6930104; last commit 2023-02-08)
    Homebrew/homebrew-cask (git revision cbce859534; last commit 2023-02-09)

    有版本号输出说明已经安装完成了。


    2. 卸载


    直接在终端执行一键脚本即可

    复制代码
    /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/HomebrewUninstall.sh)"

    3. 常用命令

    /// 安装
    brew install FORMULA|CASK...

    /// 搜索
    brew search TEXT|/REGEX/

    /// 卸载包
    brew uninstall FORMULA|CASK...

    /// 查看安装列表
    brew list [FORMULA|CASK...]

    /// 查看包信息
    brew info [FORMULA|CASK...]

    /// 查看哪些包可以更新
    brew outdated

    /// 更新指定包(安装新包,但旧包依旧保留)
    brew upgrade [FORMULA|CASK...]

    /// 更新Homebrew
    brew update

    /// 清理旧版本和缓存
    brew cleanup # 清理所有包的旧版本
    brew cleanup [FORMULA ...] # 清理指定包的旧版本
    brew cleanup -n # 查看可清理的旧版本包,不执行实际操作

    /// 锁定不想更新的包(因为update会一次更新所有的包的,当我们想忽略的时候可以使用这个命令)
    brew pin [FORMULA ...] # 锁定某个包
    brew unpin [FORMULA ...] # 取消锁定

    /// 软件服务管理
    brew services list # 查看使用brew安装的服务列表
    brew services run formula|--all # 启动服务(仅启动不注册)
    brew services start formula|--all # 启动服务,并注册
    brew services stop formula|--all # 停止服务,并取消注册
    brew services restart formula|--all # 重启服务,并注册

    二、Ruby

    1. 安装



    其实Mac系统默认已经有Ruby的环境了,在终端中执行ruby -v查看版本号。

    ~:~$ruby -v
    ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]

    本地ruby版本有点低了,这里我们使用Homebrew来更新,

    brew install ruby

    执行结束后默认会将最新版本的ruby安装到/usr/local/Cellar/目录下。


    我们查看一下当前的ruby版本:

    ~:~$ruby -v
    ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]

    好像版本并未发生变化,why? 这里主要是因为Shell环境中并没有读到最新的ruby路径,我们可以再编辑一下用户目录下的环境配置文件~/.zshrc,新增ruby的路径并写入环境变量:

    # 环境变量配置
    export RUBY=/usr/local/Cellar/ruby/3.2.0/bin
    export GEMS=/usr/local/lib/ruby/gems/3.2.0/bin

    # 写入环境变量
    export PATH=$RUBY:$GEMS:$PATH

    这里先添加上面的内容然后执行source ~/.zshrc,后面会讲到Shell环境配置相关的内容。


    再次查看ruby版本:

    ~:~$ruby -v
    ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-darwin20]

    此时可以看到ruby已经升级到最新的3.2.0版本。


    当然我们还可以执行which ruby查看当前的ruby的具体路径:

    ~:~$which ruby
    /usr/local/Cellar/ruby/3.2.0/bin/ruby

    从结果可以看出当前使用的ruby正是我们在.zshrc中配置的路径。


    2. Gem换源


    Gemruby的包管理器,一些ruby库我们需要使用Gem来安装,但Gem官方源速度拉胯,这里我们需要替换为国内源。

    /// 添加国内源并删除官方源
    gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/

    /// 查看当前源地址
    gem sources -l

    查看当前源,确认已替换为国内源即可。

    ~:~$gem sources -l
    *** CURRENT SOURCES ***

    https://gems.ruby-china.com/

    3. 常用包安装

    /// cocoapods安装
    gem install cocoapods

    /// fastlane安装
    gem install fastlane

    耐心等待安装完成后我们可以测试一下:

    ~:~$pod --version
    1.11.3

    ~:~$fastlane --version
    fastlane installation at path:
    /usr/local/lib/ruby/gems/3.2.0/gems/fastlane-2.211.0/bin/fastlane
    -----------------------------
    [✔] 🚀
    fastlane 2.211.0

    从结果可以看出cocoapodsfastlane都安装完成了。


    三、Python

    1. 使用Xcode自带Python库(推荐)



    其实Xcode命令行工具自带了python库,项目中需要执行python脚本的优先使用这个会更合适,因为Xcode编译项目时会优先使用这个python库,Mac中仅使用这一个版本可以避免一些多python版本环境问题导致的报错。


    根据当前Xcode命令行工具中的python版本,这里我们需要在~/.zshrc中添加相关配置并执行source ~/.zshrc重载配置:

    # 环境变量配置
    export PYTHON=/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.8/Python3/bin

    # 写入环境变量
    export PATH=$PYTHON:$PATH

    # 别名
    alias python=python3
    alias pip=pip3

    这里使用别名以便于执行python命令时使用的是python3, 查看一下版本,结果也符合预期。

    ~:~$python --version
    Python 3.8.9

    2. 使用Homebrew安装


    这里我们直接执行:

    brew install python

    耐心等待安装完成,其实Homebrew会将Python安装到/usr/local/Cellar/目录下,并在/usr/local/bin目录创建了链接文件。这里我们需要在~/.zshrc中添加相关配置并执行source ~/.zshrc重载配置:

    # 环境变量配置
    export SBIN=/usr/local/bin:/usr/local/sbin

    # 写入环境变量
    export PATH=$SBIN:$PATH

    # 别名
    alias python=python3
    alias pip=pip3

    查看一下版本,已经升级到最新版:

    ~:~$python --version
    Python 3.10.10

    3. pip换源


    pippython的包管理器,我们可以使用它来安装一些python库。我们可以更换一个国内源来提升下载速度:

    /// 查看当前源
    pip config list

    /// 替换为清华大学源
    pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

    /// 还原为默认源
    pip config unset global.index-url


    4. 常用包安装

    /// openpyxl安装
    pip install openpyxl

    安装速度非常快:

    ~:~$pip install openpyxl
    Collecting openpyxl
    Using cached openpyxl-3.1.0-py2.py3-none-any.whl (250 kB)
    Requirement already satisfied: et-xmlfile in /usr/local/lib/python3.10/site-packages (from openpyxl) (1.1.0)
    Installing collected packages: openpyxl
    Successfully installed openpyxl-3.1.0

    四、Shell环境配置

    1. zsh的配置文件.zshrc



    macOS Catalina 开始,Mac 使用 zsh 作为默认shell,而它的配置文件是用户目录下的.zshrc文件,所以我们之前在定义环境变量时都会编辑这个文件。每次打开终端时都会读取这个配置文件,如果需要在当前的shell窗口读取最新的环境配置则需要执行source ~/.zshrc,这也是之前我们编辑该文件后重载配置的原因(为了让最新的配置生效😁)。


    2. 定义环境变量(全局变量)

    export RUBY=/usr/local/Cellar/ruby/3.2.0/bin

    其实我们之前在讲Ruby的安装时已经在~/.zshrc文件中定义过全局变量,语法就是在一个变量名前面加上export关键字。这里我们可以在终端输出一下这个变量:

    ~:~$echo $RUBY
    /usr/local/Cellar/ruby/3.2.0/bin

    变量的值可以正常输出,这也意味着这样的变量在当前shell程序中全局可读。


    3. 写入环境变量


    常见的环境变量:

    • CDPATH:冒号分隔的目录列表,作为cd命令的搜索路径
    • HOME:当前用户的主目录
    • PATHshell查找命令的目录列表,由冒号分隔
    • BASH:当前shell实例的全路径名
    • PWD:当前工作目录

    这里重点关注一下PATH变量,当我们在shell命令行界面中输入一个外部命令时,shell必须搜索系统来找到对应的程序。PATH环境变量定义了用于进行命令和程序查找的目录:

    echo $PATH

    某些时候我们执行命令会遇到command not found这样的报错,比如:

    ~:~$hi
    zsh: command not found: hi

    这是因为PATH中的目录并没有包含hi命令,所以我们执行hi就报错。同理,当我们在配置环境时,某些库的目录需要被写入到PATH中,比如:

    # 环境变量配置
    export SBIN=/usr/local/bin:/usr/local/sbin
    export HOMEBREW=/usr/local/Homebrew/bin
    export RUBY=/usr/local/Cellar/ruby/3.2.0/bin
    export GEMS=/usr/local/lib/ruby/gems/3.2.0/bin

    # 写入环境变量
    export PATH=$SBIN:$HOMEBREW:$RUBY:$GEMS:$PATH

    这样当我们执行具体的命令时,shell才能够正确的访问。




    • 附.zshrc常见配置

      # 环境变量配置
      export SBIN=/usr/local/bin:/usr/local/sbin
      export HOMEBREW=/usr/local/Homebrew/bin
      export RUBY=/usr/local/Cellar/ruby/3.2.0/bin
      export GEMS=/usr/local/lib/ruby/gems/3.2.0/bin

      # 写入环境变量
      export PATH=$SBIN:$HOMEBREW:$RUBY:$GEMS:$PATH

      # 别名
      alias python=python3
      alias pip=pip3

      # 编码
      export LC_ALL=en_US.UTF-8
      export LANG=en_US.UTF-8

      # 控制PS1信息
      PROMPT='%U%F{51}%1~%f%u:~$'

      # 镜像源
      export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles



    五、参考文档


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

    收起阅读 »

    类似chat-gpt的打字机效果

    类似chat-gpt的打字机效果 展示效果: 实现思路:只要控制显示内容的长度就行了,每次加一点显示内容,然后一直播放闪烁动画,加载完了就停掉动画。 结论:单个字逐渐加载 + 闪烁动画 = 打字机效果 闪烁动画实现 通过css实现.cursor { ...
    继续阅读 »

    类似chat-gpt的打字机效果


    展示效果:




    实现思路:只要控制显示内容的长度就行了,每次加一点显示内容,然后一直播放闪烁动画,加载完了就停掉动画。


    结论:单个字逐渐加载 + 闪烁动画 = 打字机效果


    闪烁动画实现


    通过css实现

    .cursor {
    position: absolute;
    display: inline-block;
    width: 2px;
    height: 16px;
    background-color: #000;
    animation: blink 1s infinite;
    transform: translate(2px, 3px);
    }

    @keyframes blink {
    0%, 100% {
    opacity: 1;
    }
    50% {
    opacity: 0;
    }
    }

    展现效果:




    完整代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>打字机效果</title>
    <style>
    .cursor {
    position: absolute;
    display: inline-block;
    width: 2px;
    height: 16px;
    background-color: #000;
    animation: blink 1s infinite;
    transform: translate(2px, 3px);
    }

    @keyframes blink {
    0%, 100% {
    opacity: 1;
    }
    50% {
    opacity: 0;
    }
    }
    .box1 {
    line-height: 22px;
    width: 300px;
    font-size: 16px;
    padding: 10px;
    border: 1px solid pink;
    margin-bottom: 10px;
    min-height: 100px;
    }
    </style>
    </head>
    <body>
    <div class="box1">
    <span class="cursor"></span>
    </div>
    <button class="btn">添加文字</button>
    <script>
    const randomTextArr = ["萨嘎", '三', "agas", '大厦', '阿萨法施工', 'saf', '啊', '收到', '三个哈哈哈', '阿事实上事实上事实上', '事实上事实上少时诵诗书', '叫哦大家搜狗号度搜化工三打哈干撒的很尬山东干红手打很尬搜哈', '时间几节课MVvvvvvvvvvv啪啪啪啪啪啪PPT科技我IQ和瓦暖气,你', '撒啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊就你跟贵公司懂法守法收入与武器我先把发发花洒就你跟贵公司懂法守法收入与武器我先把发发花洒'];
    const box1 = document.querySelector('.box1');
    const btn = document.querySelector('.btn');
    let showText = '';
    let addTextArr = [];
    let timer = null;

    btn.onclick = () => {
    getRandomText();
    updateText();
    }

    function getRandomText() {
    const randomTextArrLength = randomTextArr.length;
    let randomNum = Math.random();
    let addText = randomTextArr[Math.floor(Math.random() * randomTextArrLength)];
    addTextArr.push(addText);
    console.log(addText)
    }

    function updateText() {
    let index = 0;
    if (!timer) {
    timer = setInterval(() => {
    if (addTextArr.length > 0) {
    if (index < addTextArr[0].length) {
    box1.innerHTML = showText + addTextArr[0][index] + `<span></span>`;
    showText += addTextArr[0][index];
    index ++;
    } else {
    index = 0;
    box1.innerHTML = showText;
    addTextArr.shift();
    }
    } else {
    clearInterval(timer);
    timer = null;
    }
    }, 50)
    }
    }
    </script>
    </body>
    </html>

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

    让你兴奋不已的13个CSS技巧🤯

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器已上线 cube.waixingyun.cn/home 1.使用边框绘制一个三角形 在某些情况下,例如...
    继续阅读 »

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器已上线 cube.waixingyun.cn/home


    1.使用边框绘制一个三角形


    在某些情况下,例如在工具提示中添加箭头指针时,如果你只需要简单的三角形,那么加载图片可能会过度。


    仅使用CSS,您就可以通过边框创建一个三角形。


    这是一个相当老的技巧。理想情况下,你会在一个宽度和高度都为零的元素上设置边框。所有的边框颜色都是透明的,除了那个将形成箭头的边框。例如,要创建一个向上指的箭头,底部边框是有颜色的,而左边和右边是透明的。无需包括顶部边框。边框的宽度决定了箭头的大小

    .upwards-arrow {
    width: 0;
    height: 0;
    border-left: 20px solid transparent;
    border-right: 20px solid transparent;

    border-bottom: 20px solid crimson;
    }

    这将创建一个像下面所示的向上指的箭头:




    事例地址:codepen.io/chriscoyier…


    2.交换元素的背景


    z-index 属性规定了元素如何堆叠在其他定位元素上。有时,你可能会设置一个 z-index 属性让子元素的层级较低,结果却发现它隐藏在其父元素的背景之后。为了防止这种情况,你可以在父元素上创建一个新的堆叠上下文,防止子元素隐藏在其后面。创建堆叠上下文的一种方法是使用 isolation: isolate CSS样式声明。


    我们可以利用这种堆叠上下文技术来创建悬停效果,该效果可以交换按钮的背景。例如:

    button.join-now {
    cursor: pointer;
    border: none;
    outline: none;
    padding: 10px 15px;

    position: relative;
    background-color: #5dbea3;
    isolation: isolate; /* If ommitted, child pseudo element will be stacked behind */
    }

    button.join-now::before {
    content: "";
    position: absolute;
    background-color: #33b249;
    top: 0;
    left: 100%;
    right: 0;
    bottom: 0;
    transition: left 500ms ease-out;

    z-index: -1;
    }

    button.join-now:hover::before {
    left: 0;
    }

    上述代码在鼠标悬停时交换了 button 的背景。背景的变化不会干扰前景的文本,如下面的gif所示:




    3.将元素居中


    可能,你已经知道如何使用 display: flex;display: grid; 来居中元素。然而,另一种不太受欢迎的在x轴上居中元素的方法是使用 text-align CSS属性。这个属性在居中文本时就能直接使用。要想在DOM中也居中其他元素,子元素需要有一个 inline 的显示。它可以是 inline-block 或任何其他内联...

    div.parent {
    text-align: center;
    }

    div.child {
    display: inline-block;
    }

    4.药丸💊形状按钮


    可以通过将按钮的边框半径设置为非常高的值来制作药丸形状的按钮。当然,边框半径应该高于按钮的高度。

    button.btn {
    border-radius: 80px; /* value higher than height of the button */
    padding: 20px 30px;
    background-color: #fdd835;
    border: none;
    color: black;
    font-size: 20px;
    }



    按钮的高度可能会随着设计的改变而增加。因此,你会发现将 border-radius 设置为非常高的值是很方便的,这样无论按钮是否增大,你的css都能继续工作。


    5.轻松为你的网站添加美观的加载指示器


    对于开发者来说,将注意力转移到为你的网站创建一个美观的加载指示器上往往是一项乏味的任务。这种关注力更好地用于构建项目的其他重要部分,这些部分值得我们去关注。


    当你在阅读时,很可能你也觉得这是个令人烦恼的难题。这就是为什么我花时间为你消除这个障碍,并精心准备了一个装有加载指示器的库,让你可以在你的梦想项目中“即插即用”。这是一个完整的集合,你只需要挑选出那个能点燃你心中火花💖的。只需看看这个库的简单用法,源代码在Github上可用。别忘了给个星星⭐


    地址:http://www.npmjs.com/package/rea…




    6.简易暗色或亮色模式


    您只需要几行CSS代码,就可以在我们的网站上启用深色/浅色模式。您只需让浏览器知道,您的网站可以在系统的深色/浅色模式下正确显示。

    html {
    color-scheme: light dark;
    }

    注意: color-scheme 属性可以设置在除 html 之外的任何DOM元素上。


    然后通过我们的网站设置控制背景颜色和文字颜色的变量,通过检查浏览器支持使其更加防弹:

    html {
    --bg-color: #ffffff;
    --txt-color: #000000;
    }

    @supports (background-color: Canvas) and (color: CanvasText) {
    :root {
    --bg-color: Canvas;
    --txt-color: CanvasText;
    }
    }

    注意:如果你不在元素上设置 background-color ,它将继承浏览器定义的与深色/浅色主题匹配的系统颜色。这些系统颜色在不同的浏览器之间可能会有所不同。


    明确设置 background-color 可以与 prefers-color-scheme 结合使用,以提供与浏览器默认设置不同的颜色阴影。


    以下是暗/亮模式的实际应用。用户的偏好在暗模式和亮模式之间进行模拟。




    7.使用省略号( ... )截断溢出的文本


    这个技巧已经存在一段时间,用于美观地修剪长文本。但你可能仍然错过了它。你只需要以下的CSS:

    p.intro {
    width: 300px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    }

    只需实施以下规则:



    • 明确的宽度,因此剪裁的边界将永远被达到。

    • 浏览器会将超出元素宽度的长文本进行换行。所以你需要阻止这种情况: white-space: nowrap; 。

    • 溢出的内容应被剪裁: overflow: hidden; 。

    • 当文本即将被剪切时,用省略号( ... )填充字符串: text-overflow: ellipsis; 。


    结果看起来像这样:




    8.将长文本截断为若干行


    这与上述技巧略有不同。这次,文本被剪裁,将内容限制为一定的行数。

    p.intro {
    width: 300px;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3; /* Truncate when no. of lines exceed 3 */
    overflow: hidden;
    }

    输出看起来像这样:




    9. 停止过度劳累自己写作 toprightbottomleft


    在处理定位元素时,你通常会编写如下代码:

    .some-element {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    }

    这可以通过使用 inset 属性来简化:

    .some-element {
    position: absolute;
    inset: 0;
    }

    或者,如果你对 toprightbottomleft 有不同的值,你可以按照如下的顺序分别设置它们: inset: -10px 0px -10px 0px这种简写方式与margin 的工作方式相同。


    10.提供优化过的图片


    请尝试在浏览器的开发者工具中将网络速度调整到较慢,然后访问一个由高清图片组成的网站,比如 unsplash。这就是你的网站访客在网络速度较慢的地理区域尝试欣赏你的高清内容时所经历的痛苦。


    但你可以通过 image-set CSS 技巧提供一种解救方法。


    可以为浏览器提供选项,让它决定最适合用户设备的图片。例如:

    .banner {
    background-image: url("elephant.png"),
    background-image: -webkit-image-set(
    url("elephant.webp") type("image/webp") 1x,
    url("elephantHD.webp") type("image/webp") 2x,
    url("elephant.png") type("image/png") 1x,
    url("elephantHD.png") type("image/png") 2x
    );
    }

    上述代码将设置元素的背景图像。


    如果支持 -webkit-image-set ,那么背景图像将会是一种优化的图像,也就是说,这将是一种支持的MIME类型的图像,且更适合用户设备的分辨率能力。


    例如:由于更高质量的图像直接与更大的尺寸成正比,所以在网络状况差的情况下使用高分辨率设备的用户,会促使浏览器决定提供支持的低分辨率图像。让用户等待高清图像加载是不合逻辑的。


    11. 计数器


    你不必纠结于浏览器如何渲染编号列表。你可以利用 counters() 实现你自己的设计。以下是操作方法:

    ul {
    margin: 0;
    font-family: sans-serif;

    /* Define & Initialize Counter */
    counter-reset: list 0;
    }

    ul li {
    list-style: none;
    }

    ul li:before {
    padding: 5px;
    margin: 0 8px 5px 0px;
    display: inline-block;
    background: skyblue;
    border-radius: 50%;
    font-weight: 100;
    font-size: 0.75rem;

    /* Increment counter by 1 */
    counter-increment: list 1;
    /* Show incremented count padded with `.` */
    content: counter(list) ".";
    }



    12.表单验证视觉提示


    仅使用CSS,您就可以向用户显示有关表单输入有效性的视觉提示。我们可以在表单元素上使用 :valid:invalid CSS伪类,当其内容验证成功或失败时,应用适当的样式。


    请考虑以下HTML页面结构:

    <!-- Regex in pattern attribute means input can accept `firstName Lastname` (whitespace sepearated names) -->
    <!-- And invalidates any other symbols like `*` -->
    <input
    type="text"
    pattern="([a-zA-Z0-9]\s?)+"
    placeholder="Enter full name"
    required
    />
    <span></span>

    <span> 将用于显示验证结果。以下的CSS根据其验证结果来设置输入框的样式:

    input + span {
    position: relative;
    }

    input + span::before {
    position: absolute;
    right: -20px;
    bottom: 0;
    }

    input:not(:placeholder-shown):invalid {
    border: 2px solid red;
    }

    input:not(:placeholder-shown):invalid + span::before {
    content: "✖";
    color: red;
    }

    input:not(:placeholder-shown):valid + span::before {
    content: "✓";
    color: green;
    }

    地址:codepen.io/hane-smitte…


    13. 一键选择文本


    这个技巧主要是为了提升网站用户的复制和粘贴体验。使用 user-select: all ,可以通过一键实现简单的文本选择。所有位于该元素下方的文本节点都会被选中。


    另一方面,可以使用 user-select: none; 来禁用文本选择。禁用文本选择的另一种方法是将文本放在 ::before::after CSS伪元素的 content: ''; 属性中。


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

    你大脑中的画面,现在可以高清还原了

    前言 AI 直接把你脑中的创意画出来的时刻,已经到来了。 本文转载自机器之心 仅用于学术分享,若侵权请联系删除 欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。 CV各大方向专栏与各个部署框架最全教程整理 【...
    继续阅读 »

    前言 AI 直接把你脑中的创意画出来的时刻,已经到来了。



    本文转载自机器之心


    仅用于学术分享,若侵权请联系删除


    欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。


    CV各大方向专栏与各个部署框架最全教程整理


    【CV技术指南】CV全栈指导班、基础入门班、论文指导班 全面上线!!


    近几年,图像生成领域取得了巨大的进步,尤其是文本到图像生成方面取得了重大突破:只要我们用文本描述自己的想法,AI 就能生成新奇又逼真的图像。


    但其实我们可以更进一步 —— 将头脑中的想法转化为文本这一步可以省去,直接通过脑活动(如 EEG(脑电图)记录)来控制图像的生成创作。


    这种「思维到图像」的生成方式有着广阔的应用前景。例如,它能极大提高艺术创作的效率,并帮助人们捕捉稍纵即逝的灵感;它也有可能将人们夜晚的梦境进行可视化;它甚至可能用于心理治疗,帮助自闭症儿童和语言障碍患者。


    最近,来自清华大学深圳国际研究生院、腾讯 AI Lab 和鹏城实验室的研究者们联合发表了一篇「思维到图像」的研究论文,利用预训练的文本到图像模型(比如 Stable Diffusion)强大的生成能力,直接从脑电图信号生成了高质量的图像。



    论文地址:arxiv.org/pdf/2306.16…


    项目地址:github.com/bbaaii/Drea…


    方法概述


    近期一些相关研究(例如 MinD-Vis)尝试基于 fMRI(功能性磁共振成像信号)来重建视觉信息。他们已经证明了利用脑活动重建高质量结果的可行性。然而,这些方法与理想中使用脑信号进行快捷、高效的创作还差得太远,这主要有两点原因:


    首先,fMRI 设备不便携,并且需要专业人员操作,因此捕捉 fMRI 信号很困难;


    其次,fMRI 数据采集的成本较高,这在实际的艺术创作中会很大程度地阻碍该方法的使用。


    相比之下,EEG 是一种无创、低成本的脑电活动记录方法,并且现在市面上已经有获得 EEG 信号的便携商用产品。


    但实现「思维到图像」的生成还面临两个主要挑战:


    1)EEG 信号通过非侵入式的方法来捕捉,因此它本质上是有噪声的。此外,EEG 数据有限,个体差异不容忽视。那么,如何从如此多的约束条件下的脑电信号中获得有效且稳健的语义表征呢?


    2)由于使用了 CLIP 并在大量文本 - 图像对上进行训练,Stable Diffusion 中的文本和图像空间对齐良好。然而,EEG 信号具有其自身的特点,其空间与文本和图像大不相同。如何在有限且带有噪声的 EEG - 图像对上对齐 EEG、文本和图像空间?


    为了解决第一个挑战,该研究提出,使用大量的 EEG 数据来训练 EEG 表征,而不是仅用罕见的 EEG 图像对。该研究采用掩码信号建模的方法,根据上下文线索预测缺失的 token。


    不同于将输入视为二维图像并屏蔽空间信息的 MAE 和 MinD-Vis,该研究考虑了 EEG 信号的时间特性,并深入挖掘人类大脑时序变化背后的语义。该研究随机屏蔽了一部分 token,然后在时间域内重建这些被屏蔽的 token。通过这种方式,预训练的编码器能够对不同个体和不同脑活动的 EEG 数据进行深入理解。


    对于第二个挑战,先前的解决方法通常直接对 Stable Diffusion 模型进行微调,使用少量噪声数据对进行训练。然而,仅通过最终的图像重构损失对 SD 进行端到端微调,很难学习到脑信号(例如 EEG 和 fMRI)与文本空间之间的准确对齐。因此,研究团队提出采用额外的 CLIP 监督,帮助实现 EEG、文本和图像空间的对齐。


    具体而言,SD 本身使用 CLIP 的文本编码器来生成文本嵌入,这与之前阶段的掩码预训练 EEG 嵌入非常不同。利用 CLIP 的图像编码器提取丰富的图像嵌入,这些嵌入与 CLIP 的文本嵌入很好地对齐。然后,这些 CLIP 图像嵌入被用于进一步优化 EEG 嵌入表征。因此,经过改进的 EEG 特征嵌入可以与 CLIP 的图像和文本嵌入很好地对齐,并更适合于 SD 图像生成,从而提高生成图像的质量。


    基于以上两个精心设计的方案,该研究提出了新方法 DreamDiffusion。DreamDiffusion 能够从脑电图(EEG)信号中生成高质量且逼真的图像。



    具体来说,DreamDiffusion 主要由三个部分组成:


    1)掩码信号预训练,以实现有效和稳健的 EEG 编码器;


    2)使用预训练的 Stable Diffusion 和有限的 EEG 图像对进行微调;


    3)使用 CLIP 编码器,对齐 EEG、文本和图像空间。


    首先,研究人员利用带有大量噪声的 EEG 数据,采用掩码信号建模,训练 EEG 编码器,提取上下文知识。然后,得到的 EEG 编码器通过交叉注意力机制被用来为 Stable Diffusion 提供条件特征。



    为了增强 EEG 特征与 Stable Diffusion 的兼容性,研究人员进一步通过在微调过程中减少 EEG 嵌入与 CLIP 图像嵌入之间的距离,进一步对齐了 EEG、文本和图像的嵌入空间。


    实验与分析


    与 Brain2Image 对比


    研究人员将本文方法与 Brain2Image 进行比较。Brain2Image 采用传统的生成模型,即变分自编码器(VAE)和生成对抗网络(GAN),用于实现从 EEG 到图像的转换。然而,Brain2Image 仅提供了少数类别的结果,并没有提供参考实现。


    鉴于此,该研究对 Brain2Image 论文中展示的几个类别(即飞机、南瓜灯和熊猫)进行了定性比较。为确保比较公平,研究人员采用了与 Brain2Image 论文中所述相同的评估策略,并在下图 5 中展示了不同方法生成的结果。


    下图第一行展示了 Brain2Image 生成的结果,最后一行是研究人员提出的方法 DreamDiffusion 生成的。可以看到 DreamDiffusion 生成的图像质量明显高于 Brain2Image 生成的图像,这也验证了本文方法的有效性。



    消融实验


    预训练的作用:为了证明大规模 EEG 数据预训练的有效性,该研究使用未经训练的编码器来训练多个模型进行验证。其中一个模型与完整模型相同,而另一个模型只有两层的 EEG 编码层,以避免数据过拟合。在训练过程中,这两个模型分别进行了有 / 无 CLIP 监督的训练,结果如表 1 中 Model 列的 1 到 4 所示。可以看到,没有经过预训练的模型准确性有所降低。



    mask ratio:本文还研究了用 EEG 数据确定 MSM 预训练的最佳掩码比。如表 1 中的 Model 列的 5 到 7 所示,过高或过低的掩码比会对模型性能都会产生不利影响。当掩码比为 0.75 达到最高的整体准确率。这一发现至关重要,因为这表明,与通常使用低掩码比的自然语言处理不同,在对 EEG 进行 MSM 时,高掩码比是一个较好的选择。


    CLIP 对齐:该方法的关键之一是通过 CLIP 编码器将 EEG 表征与图像对齐。该研究进行实验验证了这种方法的有效性,结果如表 1 所示。可以观察到,当没有使用 CLIP 监督时,模型的性能明显下降。实际上,如图 6 右下角所示,即使在没有预训练的情况下,使用 CLIP 对齐 EEG 特征仍然可以得到合理的结果,这凸显了 CLIP 监督在该方法中的重要性。



    欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。


    计算机视觉入门1v3辅导班


    【技术文档】《从零搭建pytorch模型教程》122页PDF下载


    QQ交流群:470899183。群内有大佬负责解答大家的日常学习、科研、代码问题。


    其它文章


    中科院自动化所发布FastSAM | 精度相当,速度提升50倍!!!


    大核卷积网络是比 Transformer 更好的教师吗?ConvNets 对 ConvNets 蒸馏奇效


    MaskFormer:将语义分割和实例分割作为同一任务进行训练


    CVPR 2023 VAND Workshop Challenge零样本异常检测冠军方案


    视觉魔法师:开启语义分割的奇幻之旅


    沈春华团队最新 | SegViTv2对SegViT进行全面升级,让基于ViT的分割模型更轻更强


    刷新20项代码任务SOTA,Salesforce提出新型基础LLM系列编码器-解码器Code T5+


    可能95%的人还在犯的PyTorch错误


    从DDPM到GLIDE:基于扩散模型的图像生成算法进展


    CVPR最佳论文颁给自动驾驶大模型!中国团队第一单位,近10年三大视觉顶会首例


    最新轻量化Backbone | FalconNet汇聚所有轻量化模块的优点,成就最强最轻Backbone


    ReID专栏(二)多尺度设计与应用


    ReID专栏(一) 任务与数据集概述


    libtorch教程(三)简单模型搭建


    libtorch教程(二)张量的常规操作


    libtorch教程(一)开发环境搭建:VS+libtorch和Qt+libtorch


    NeRF与三维重建专栏(三)nerf_pl源码部分解读与colmap、cuda算子使用


    NeRF与三维重建专栏(二)NeRF原文解读与体渲染物理模型


    NeRF与三维重建专栏(一)领域背景、难点与数据集介绍


    异常检测专栏(三)传统的异常检测算法——上


    异常检测专栏(二):评价指标及常用数据集


    异常检测专栏(一)异常检测概述


    BEV专栏(二)从BEVFormer看BEV流程(下篇)


    BEV专栏(一)从BEVFormer深入探究BEV流程(上篇)


    可见光遥感图像目标检测(三)文字场景检测之Arbitrary


    可见光遥感目标检测(二)主要难点与研究方法概述


    可见光遥感目标检测(一)任务概要介绍


    TensorRT教程(三)TensorRT的安装教程


    TensorRT教程(二)TensorRT进阶介绍


    TensorRT教程(一)初次介绍TensorRT


    AI最全资料汇总 | 基础入门、技术前沿、工业应用、部署框架、实战教程学习


    计算机视觉入门1v3辅导班


    计算机视觉交流群


    聊聊计算机视觉入门


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

    阿里又又发布了一个“AI神器”

    历史回顾 上一次阿里发布通义千问,犹在昨天,结果,阿里又发布了一件AI神器,该神器实用性极强,据说背后依然采用阿里的通义千问大模型。 不了解的可以看下我的历史文章 阿里的通义千问,震惊到我了 最近一直在整理AIGC方面的东西分享给大家,当然编程也不会落下,欢迎...
    继续阅读 »

    历史回顾


    上一次阿里发布通义千问,犹在昨天,结果,阿里又发布了一件AI神器,该神器实用性极强,据说背后依然采用阿里的通义千问大模型。


    不了解的可以看下我的历史文章 阿里的通义千问,震惊到我了


    最近一直在整理AIGC方面的东西分享给大家,当然编程也不会落下,欢迎关注,让我们在AI的道路上越走越远


    他来了,讯飞星火迈着矫健的步伐向我们走来了


    免费搭建个人stable-diffusion绘画(非本地、干货教程)


    阿里给“打工”朋友送上“节日礼物”




    六一儿童节当天,阿里就给所有“打工”的大朋友送上了一份“节日礼物”


    6月1日上午,阿里云发布了面向音视频内容的AI新品“通义听悟”,并正式公测



    【通义听悟】 推荐给你~ tingwu.aliyun.com/u/14xZ00303… 工作学习AI助手,依托大模型,为每一个人提供全新的音视频体验。点击链接立即注册,公测期免费体验。



    通义千问、通义听悟 这哥俩现在所处环境不同,定位不同,功能不同 但依赖大模型是相同的


    这是阿里通义大模型最新的应用进展,也是国内首个开放公测的大模型应用产品。


    根据阿里云智能CTO周靖人介绍,“通义听悟”是依托通义千问大模型和音视频AI模型的AI助手,可进行实时语音识别,实现对话的实时记录、多语言翻译、发言总结、提取PPT、内容规整等。


    对我们打工人有什么用


    会议神器




    当领导在上面夸夸其谈的时候,你的会议纪要可谓是错乱无章,这会儿通义听悟就上线了,你只需要录音




    或者我们本地上传




    支持区分多人对话,然后开始转写


    值得一提的是, “听悟”可以根据AI转写,提取这场说话内容的关键词,给出全文摘要。




    视频总结神器




    不同于传统的实时会议速记转写,如今面向C端提供视频转写服务的应用尚在少数。而如今的通义听悟,则从纯粹的音频转写,延伸到了音视频领域,融合了十多项AI新功能。


    “通义听悟”我个人认为最大的实用功能是:可以设置插件,无论看视频、看直播,还是开会,点开听悟插件,就能实现音视频的实时转录和翻译。




    其实看到这里,可以感受到,这不只是说对打工人的福利,也是对于学生党的福利,比如我们上课,课后复盘总结




    最后再提一点阿里的生态,他们将数据存储和阿里云盘打通 这点是值得表扬的,在阿里云盘中,用户可以一键转写云盘中的文件,在云盘内在线播放视频时,能够实时生成字幕。


    还能帮我们什么


    通义听悟未来还有更多基于大模型的功能上线。比如,对视频中出现的PPT,AI能够基于通义千问大模型做到一键提取,而用户也能向AI助手针对多个音视频内容进行提问、让听悟概括特定段落等等。


    值得注意的是,听悟目前针对一些细分场景中提供了不同的部署形态,如浏览器插件。在Chrome安装听悟插件后,听悟在无字幕视频中就可以实时生成双语悬浮字幕。二转写结果可下载为字幕文件,方便新媒体从业者视频后期制作




    通义千问Chrome插件示意图,近期该功能将上线,可以持续关注 我后续给大家做详细介绍,不过我们可以先感受下




    钉钉的在线会议模块“钉闪记”,同样集成了听悟。在会议结束后,钉闪记所能够输出的也不再是纯粹的速记,而是包含重点摘要的完整文档,可以有效地提升公司内部工作效率。甚至,在开会时,AI可以代为记录会议、整理要点。


    未来一段时间还将在夸克APP、阿里云盘等端口提供服务


    总结


    这一番体验下来总体的效果还是可以的




    从通义听悟中可以看出,国内大模型厂商除了在底层大模型搭建上快马加鞭外,AI应用也已经成为他们必须抓住的机遇——AI音视频转写、内容理解等功能,背后意味着通用能力,厂商们可以覆盖包括开会、上课、培训、面试、直播、看视频、听播客等音视频场景,嵌入到不同的应用软件当中。


    今天的分享就到这里,我们的AI绘画系列正在慢慢搭建,对AI有兴趣的可以关注公众号(微信公众号搜索 1点东西) ,我们会持续输出AIGC类好玩的工具和想法,立志让每个人都能感受AI,利用AI找寻更多可能性


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

    App 备案的复杂情绪:某些海外的独立 app 要和我们告别了

    最近国内 App 备案的消息引发了大家热烈的讨论,其实对于国内的正规开发者而言其实影响没那么大,只是多了一道行政程序。有媒体报道说这样做是为了打击网络欺诈。对于作为独立开发者的我而言,app 要求备案我是完全理解的。只是希望能让备案维持一个低门槛,不要出现地方...
    继续阅读 »

    最近国内 App 备案的消息引发了大家热烈的讨论,其实对于国内的正规开发者而言其实影响没那么大,只是多了一道行政程序。有媒体报道说这样做是为了打击网络欺诈。对于作为独立开发者的我而言,app 要求备案我是完全理解的。只是希望能让备案维持一个低门槛,不要出现地方部门在执行中为了图省事增加额外的制度成本。


    关于备案一个焦点问题是海外的 app 怎么办。参照历史经验,海外的一些独立 app 可能要跟国区告别了。备案对于海外的独立开发者而言还是不太好操作的,除非 AppStore 可以提供足够的帮助。但是我个人的观点,AppStore 只是一个发行商。替开发者备案是一个重运营的体力活,从商业角度 apple 实在是没动力做这个事情。何况如果海外 app 能赚钱,他们自然有能力和动力去完成备案。总的来说对海外的独立开发者而言,增加了不少门槛。




    还有一个坏消息:如果一个 app 提供的是订阅服务,在订阅期间停止服务苹果会给用户退款。所以海外的 app 在国区下架以后,如果提供的是订阅服务,就不只是损失国内市场。苹果给用户退款,开发者可能还要贴钱给苹果。不过我个人觉得一个 app 如果能在中国赚到钱,似乎完全有动力找一个本地代理解决一些备案的事情。也有不少海外 app 接入的是支付集成方案,也许支付的解决方案提供商会有兴趣提供国内的备案服务(stripe?)。


    但是也许这个对国内的一些开发者而言有一个小小的利好。如果国区大量海外 app 下架,国内的 app 市场就空出了不少市场。虽然这个并不是我所期待的,但是在商言商这个就是事实。也许 AppStore 会再现 copy to china 的情况,做一个高仿的海外 app 上架国区。道德上这样当然是要被人谴责的,但是国内现状就有不少安卓的开发者 copy 优质的 iOS 独立 app 到安卓市场。真很难评。


    再说监管的执行问题。Apple 因为对自己的 app 分发一直有严格的管理,前几年就开始收紧了企业证书,很容易满足监管要求。加上 apple 又是一家守法的外企,相信在要求 app 在信息里填上备案号就可以了。但是安卓因为可以比较自由的安装 apk 的包,主流安卓手机厂商又都是中国的企业,我觉得未来如果要收紧监管,要求国内安卓手机接入 apk 安装认证,只有有备案号的 app 才能安装在手机上也不是不可能。到时候如果海外 app 不仅不能从应用商店下,自己下载的 apk 也不能安装恐怕会是一个沉重的打击。


    更加严格的监管,对于会被电信诈骗骗到的小白用户是有好处的。但是我想对于另外一头对 app 有自主辨别能力的自由派用户而言就相当不友好了。我觉得简单的抱怨有点肤浅,而且伤身体。还是找到一个和现实世界妥协的方式吧。


    作者:独立开花卓富贵
    链接:https://juejin.cn/post/7266802662049267772
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如今的网络投票还有意义吗?完全就是比哪家预算高吧

    最近老板的老婆在参加一项什么“指尖博物馆”的评选,因此老板每天在群里给我们发链接让我们投票。我们兢兢业业每天一投,甚至还相互提醒,完全把这个当成事业在努力。结果,对面直接不按常理出牌,一夜就刷了300票,我们一早来,发现这个数据,面面相觑:这绝对是刷票了吧! ...
    继续阅读 »

    最近老板的老婆在参加一项什么“指尖博物馆”的评选,因此老板每天在群里给我们发链接让我们投票。我们兢兢业业每天一投,甚至还相互提醒,完全把这个当成事业在努力。结果,对面直接不按常理出牌,一夜就刷了300票,我们一早来,发现这个数据,面面相觑:这绝对是刷票了吧!


    这就是现在网络投票的现状,可能一开始开启网络刷票的人并没有想到会变成这样。本以为这将是民意的体现,具有公平、公正、透明的特质。而且它不需要场地、设备、人力,更不需要大量资金,极大地节省了人力成本。最重要的是,它能通过网络迅速传播活动信息、扩大影响规模,最后的数据一导出,妥妥一大亮点。


    事与愿违,网络投票中的刷票行为,让这原本可以最大体现民意的设置,成了黑灰产牟利的“利器”。“某某评选开始啦,请动动你的手指为我们投上宝贵的一票”“本次评选活动对我们很重要,我们需要您的支持,鞠躬感谢”……每次朋友圈看到这样的话,我都心有戚戚:发了也没用呀,对面可能直接机器刷票了!


    网络刷票的两种形式


    网络刷票背后有一群分工明确的黑灰产业。所谓黑灰产是指利用计算机、网络等手段,基于各类漏洞,通过恶意程序、木马病毒、网络、电信等形式,以非法盈利为目的规模化、组织化、分工明确的群体组织。
    图片


    网络刷票形式,主要有人工刷票与机器刷票两种。


    人工刷票:就是一些空闲时间多的人士,以帮助参赛选手投票来获得“佣金”为主要工作。这些人多数为兼职投票手,外界称之为投票水军。


    **技术刷票:**通过抓包工具分析提交投票时所产生的数据,然后使用脚本程序批量提交数据的刷票方式。


    通过分析可以发现,网络刷票黑灰产揽客主要分为三步:第一步,在网上搜索这类评选活动,通过冒充主办方或媒体等方式,拨打参选对象单位公开的办公电话,索要参选者私人联系方式。第二步,抛诱饵强卖。会以“专业低价、保证安全”、“先刷后付”等话术来诱骗参选人交钱刷票,不管参选人是否同意,团伙都会假意进行少量刷票,为下一步诈骗进行铺垫。第三步,威胁+诈骗。即便参选者交了一部分的预付金,诈骗团伙也不会再帮你刷票了。而是以此为要挟不断加价,等到活动快结束时,再以冲击排名为由诈骗一笔。


    此外,为了吸引参与者自发的去拉票刷票,商家往往会将投票配合奖励去吸引投票用户。由此经引起“羊毛党”注意,出现组队集团化刷票薅礼品的现象。


    如何防止网络刷票


    毫无疑问,网络刷票对于发起者(商家)、参与者、投票用户这三方来说,都是会造成损失的。我们需要采取一定的措施来防止黑灰产进行刷票。


    比较简单的方式有以下集中:


    1. 验证码: 在投票页面中加入验证码功能,通过输入验证码来确认投票者是真实的人类用户,而不是机器人或其他自动程序。这里可以使用顶象的免费验证码,因此就不展开叙述了。


    2. IP地址限制: 通过限制相同IP地址的投票次数,可以防止同一IP地址的用户对同一选项进行多次投票。P地址限制可以通过服务器端脚本语言(如PHP、Python等)或者Web服务器(如Apache、Nginx等)来实现。


    <?php
    // 获取投票者的IP地址
    $ip = $_SERVER['REMOTE_ADDR'];

    // 设置IP地址的投票次数上限为10次
    $vote_limit = 10;

    // 判断IP地址的投票次数是否已经超过上限
    if(get_votes_count($ip) >= $vote_limit){
    // 如果超过了上限,提示投票失败
    echo "投票失败,您已经超过投票次数限制!";
    }else{
    // 如果没有超过上限,进行投票操作
    do_vote();
    // 记录投票记录
    record_vote($ip);
    // 提示投票成功
    echo "投票成功,谢谢您的支持!";
    }

    // 获取指定IP地址的投票次数
    function get_votes_count($ip){
    // 连接数据库
    $conn = mysqli_connect('localhost', 'username', 'password', 'database');
    // 查询指定IP地址的投票次数
    $result = mysqli_query($conn, "SELECT COUNT(*) AS count FROM votes WHERE ip='$ip'");
    // 获取投票次数
    $row = mysqli_fetch_assoc($result);
    $count = $row['count'];
    // 关闭数据库连接
    mysqli_close($conn);
    return $count;
    }

    // 记录投票记录
    function record_vote($ip){
    // 连接数据库
    $conn = mysqli_connect('localhost', 'username', 'password', 'database');
    // 插入投票记录
    mysqli_query($conn, "INSERT INTO votes (ip) VALUES ('$ip')");
    // 关闭数据库连接
    mysqli_close($conn);
    }

    // 进行投票操作
    function do_vote(){
    // TODO:进行投票操作
    }
    ?>


    3. Cookie限制: 通过在用户浏览器中设置Cookie,可以限制同一浏览器的用户对同一选项进行多次投票。


    <?php
    // 获取投票者的Cookie
    $cookie_name = "voted";
    $voted = isset($_COOKIE[$cookie_name]) ? $_COOKIE[$cookie_name] : 0;

    // 设置Cookie的过期时间为1天
    $expire_time = time() + 86400;

    // 判断投票者是否已经投过票
    if($voted){
    // 如果已经投过票,提示投票失败
    echo "投票失败,您已经投过票!";
    }else{
    // 如果还没有投票,进行投票操作
    do_vote();
    // 设置投票者的Cookie
    setcookie($cookie_name, 1, $expire_time);
    // 提示投票成功
    echo "投票成功,谢谢您的支持!";
    }

    // 进行投票操作
    function do_vote(){
    // TODO:进行投票操作
    }
    ?>


    另外,在用户打开页面时,Cookie已经配置在用户浏览器中了,因为示例代码中的setcookie()函数会在服务器响应中设置Cookie并发送给客户端浏览器,当用户打开页面时,浏览器会检查本地是否存在该网站的Cookie,并将其附加在该请求中一同发送给服务器,以便服务器识别该用户的身份,或者保存一些用户相关的数据。因此,在示例代码中,当用户访问该网站时,Cookie已经生效并配置在用户浏览器中了。


    4. 人工审核: 对投票结果进行人工审核,通过人工审核来确认投票者的身份和投票行为的真实性。但是,该方法需要投入较大的人力和时间成本,不适合大规模的投票活动。


    结语


    在2021年1月8日,国家互联网信息办公室公布《互联网信息服务管理办法(修订草案征求意见稿)》。对互联网信息发布、保存及个人信息安全保护等方面作出规定,并对日益泛滥的刷票、刷量、刷评论及制作虚假账号给出了处罚细则。其中特别第二十五条特提到,任何组织和个人不得以营利为目的或为获取其他非法利益,实施下列行为,扰乱网络秩序:
    (一)明知是虚假信息而发布或者有偿提供信息发布服务的;
    (二)为他人有偿提供删除、屏蔽、替换、下沉信息服务的;
    (三)大量倒卖、注册并提供互联网信息服务账号,被用于违法犯罪的;
    (四)从事虚假点击、投票、评价、交易等活动,破坏互联网诚信体系的。


    希望在此基础上,网络刷票情况能够得到一定的缓解。<

    作者:昀和
    来源:juejin.cn/post/7225875600644735037
    /p>

    以上。

    收起阅读 »

    职场上有什么谎言?

    努力干活就能赚多点钱 职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的...
    继续阅读 »

    努力干活就能赚多点钱


    职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的报酬,而是更倾向于平衡员工的工作与生活,提高员工的幸福感和满意度。因此,新进职场人士应该认识到,在职场中坚持适度的工作量、良好的工作习惯和优秀的职业素养才是取得成功的重要因素。


    我都是为你好


    “我都是为你好”可能是一种常见的谎言,在不同情境下被使用。在某些情况下,这可能真诚地表达出对他人的关心和照顾,但在其他情况下,这也可能成为掩盖自己私人动机或者行为错误的借口。因此,在职场和日常生活中,我们需要学会审视这句话所蕴含的背后意图,并判断其是否真实可信。同时,我们也应该秉持着开放、坦诚、尊重和理解的态度,与他人进行良好的沟通和相处,以建立健康、和谐的人际关系。


    他做得比你好,向他好好学习


    “他做得比你好,向他好好学习”是一句非常有益的建议,可以让人们从成功的经验中汲取营养,不断提高自己的能力和水平。在职场中,人们面对不同的工作任务和挑战,而且每个人的工作方式、思维模式和经验都不同,因此,我们应该善于借鉴他人的优点和长处,吸取别人的经验和教训,不断完善自己的职业素养和技能。然而,这并不意味着要完全依赖和模仿别人,而是应该在合适的时机,根据自身实际情况和需要,加以改进和创新,开拓自己的专业视野和发展空间。


    在职场中,有些人可能会通过拍马屁、拉关系等不正当手段来获取自己的利益或者提高自己的地位。然而,这种做法可能会导致负面后果和损失,例如破坏工作团队的合作氛围、损害自己的职业形象和信誉等。因此,我们应该始终保持清醒和冷静的头脑,不受拍马屁等诱惑,专注于自己的工作和职责,努力提高自己的专业水平和职业素养。同时,我们也应该与他人建立良好的人际关系,以合理、公正、透明的方式展示自己的才华和成果,赢得别人的尊重和信任,并在适当的时刻借助他人的力量来实现共同的目标。


    公司不怎么赚钱,理解一下,行情好了加工资


    如果公司在过去设定了一些目标和承诺,但无法兑现或者没有达到预期的结果,那么这就是一种失信行为。画饼充当推销手段,可能会对员工、客户和利益相关方造成误导和不良影响,并破坏公司的商誉和形象。因此,公司应该根据市场实际情况和自身能力水平,制定合理、可行的计划和策略,避免过于浮夸和虚幻的承诺,注重落实和执行,加强与员工、客户和社会各方的沟通和互动,建立坦诚、透明的企业文化和价值观念。同时,员工也应该保持客观、谨慎、理性的态度,不盲目追求高回报或者虚假宣传,始终以个人职业道德和职责为先,为公司和自

    作者:象骑士
    来源:juejin.cn/post/7213636024102469693
    己的未来发展负责任。

    收起阅读 »

    糟了,生产环境数据竟然不一致,人麻了!

    大家好,我是冰河~~ 今天发现Mysql的主从数据库没有同步 先上Master库: mysql>show processlist; 查看下进程是否Sleep太多。发现很正常。 show master status; 也正常。 mysql> sh...
    继续阅读 »

    大家好,我是冰河~~


    今天发现Mysql的主从数据库没有同步


    先上Master库:


    mysql>show processlist;

    查看下进程是否Sleep太多。发现很正常。


    show master status;

    也正常。


    mysql> show master status;
    +-------------------+----------+--------------+-------------------------------+
    | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +-------------------+----------+--------------+-------------------------------+
    | mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
    +-------------------+----------+--------------+-------------------------------+
    1 row in set (0.00 sec)

    再到Slave上查看


    mysql> show slave status\G                                                

    Slave_IO_Running: Yes
    Slave_SQL_Running: No

    可见是Slave不同步


    解决方案


    下面介绍两种解决方法


    方法一:忽略错误后,继续同步


    该方法适用于主从库数据相差不大,或者要求数据可以不完全统一的情况,数据要求不严格的情况


    解决:


    stop slave;

    #表示跳过一步错误,后面的数字可变
    set global sql_slave_skip_counter =1;
    start slave;

    之后再用mysql> show slave status\G 查看


    mysql> show slave status\G
    Slave_IO_Running: Yes
    Slave_SQL_Running: Yes

    ok,现在主从同步状态正常了。。。


    方式二:重新做主从,完全同步


    该方法适用于主从库数据相差较大,或者要求数据完全统一的情况


    解决步骤如下:


    1.先进入主库,进行锁表,防止数据写入


    使用命令:


    mysql> flush tables with read lock;

    注意:该处是锁定为只读状态,语句不区分大小写


    2.进行数据备份


    #把数据备份到mysql.bak.sql文件


    mysqldump -uroot -p -hlocalhost > mysql.bak.sql

    这里注意一点:数据库备份一定要定期进行,可以用shell脚本或者python脚本,都比较方便,确保数据万无一失。


    3.查看master 状态


    mysql> show master status;
    +-------------------+----------+--------------+-------------------------------+
    | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +-------------------+----------+--------------+-------------------------------+
    | mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
    +-------------------+----------+--------------+-------------------------------+
    1 row in set (0.00 sec)

    4.把mysql备份文件传到从库机器,进行数据恢复


    scp mysql.bak.sql root@192.168.128.101:/tmp/

    5.停止从库的状态


    mysql> stop slave;

    6.然后到从库执行mysql命令,导入数据备份


    mysql> source /tmp/mysql.bak.sql

    7.设置从库同步,注意该处的同步点,就是主库show master status信息里的| File| Position两项


    change master to master_host = '192.168.128.100', master_user = 'rsync',  master_port=3306, master_password='', master_log_file =  'mysqld-bin.000001', master_log_pos=3260;

    8.重新开启从同步


    mysql> start slave;

    9.查看同步状态


    mysql> show slave status\G  

    Slave_IO_Running: Yes
    Slave_SQL_Running: Yes

    10.回到主库并执行如下命令解除表锁定。


    UNLOCK TABLES;

    好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~

    作者:冰_河
    来源:juejin.cn/post/7221858081495203897
    ~

    收起阅读 »

    某用户说他付钱了订单状态未修改

    背景 某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查...
    继续阅读 »



    背景


    某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查该问题。


    涉及服务:



    • server1 主服务业务,提供所有业务的服务;

    • m-server:提供移动端 H5 视图和部分业务功能,部分功能业务都直接请求 server1;


    总体架构


    2022-09-02-10-11-16-image.png


    总体流程:



    • 用户打开移动端应用时,由 m-server 提供页面视图;

    • 移动端相关业务数据由 server1 提供,即用户请求时,会到 m- server 再由其转发到 server1 服务上;

    • 相关支付业务由 m-server 服务与微信支付交互,支付完成后再由 m-server 与 server1 交互,同步订单的状态;


    详细支付时序图:


    支付流程的时序图,可以参考微信的官网:pay.weixin.qq.com/wiki/doc/ap…


    支付流程:



    • 用户点击支付时,向 m-server 发起支付,然后生成订单

    • 向微信支付发起生成预付单

    • 点击微信支付的支付,此时会与微信支付进行验证支付授权权限

    • 微信返回支付授权,然后用户输入密码,确认支付,向微信支付服务提交授权

    • 微信支付返回支付结果给用户,并发微信消息提醒,同时会向 m-server 异步通知支付结果

    • m-server 接收到支付结果将同步给 server ,然后server 变更订单状态结果

    • m-server 显示最后结果给用户,如支付成功的订单详情


    订单状态同步设计:


    2022-09-02-10-34-24-image.png


    订单状态流程:



    • server1 生成订单并记录到 db 中

    • m-server 从 server 中获取到订单的列表

    • m-server 接收到微信支付成功时,就会告知 server 支付成功,然后由 server 将订单状态修改为已支付


    问题分析


    已支付成功了,但订单状态却还是未支付成功?


    首先,订单的状态由待支付到支付成功,必须是由微信支付服务异步通知 m-server 支付成功,然后再由 m-server 通知 server1 去修改订单状态。


    所以,无论 m-server 还是 server1 服务在支付期间发生抖动都可能导致支付成功的信息成功通知给 server1 ,从而导致订单状态修改失败。还有一种可能性,微信支付服务可能没有异步通知。毕竟是第三方发起通知,所以也可能发生未通知情况。


    优化方案


    为了保证订单状态最终结果状态一致性,需要增加服务高可用,且可以支持自动重新发送订单状态变更的请求,及时重发重试。


    详细设计


    2022-09-02-10-45-00-image.png



    • m-server 确认支付时也将订单信息进行存储,状态为待支付

    • m-server 接收到微信支付成功通知后,就转发告知server1 服务

    • server1 修改订单状态进行响应,m-server 接收到响应进行删除或者修改订单状态(可按需进行),m-server 这里订单信息没有用了就也可以删除

    • 同时开启一个异步轮询 m-server 存储的订单信息,对于订单状态是待支付的,进行重发重试。这个过程需要先和微信支付服务确认确实是已支付,然后再将信息重新发送 server1,告知将订单状态调整为已支付


    // 定时轮询订单信息状态
    func notifyAuto() {
    // 异步定时监控订单状态的变化
    var changeOrder = func() {
    result := orderService.getUnPayOrder(0) // 获取当前未支付状态的订单
    if len(result) > 0 {
    for v := range result {
    status := wechat.getOrderStatus(v.orderId)
    if status == 1 { // 订单在微信上时已支付的,需重新调用server 修改订单状态
    orderService.sendOrderFinish(v.orderId)
    }
    }
    }
    }

    go func() {
    time.AfterFunc(time.Minute*10, changeOrder)
    }()
    }
    复制代码
    作者:小雄Ya
    来源:juejin.cn/post/7138609603356901413
    >
    收起阅读 »

    1.0 除 0 没抛出错误,我差点被输送社会

    简言 在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。 先让我们来试一试 public class TestDouble { public static v...
    继续阅读 »

    简言


    在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。


    先让我们来试一试


    public class TestDouble {   
    public static void main(String[] args) {
    System.out.println(1.0 / 0);
    }
    }

    你认为的我认为的它应该会抛出 ArithmeticException 异常


    但是它现在输出了 Infinity



    为什么呢?


    Double 数据类型支持无穷大


    还有其他类型支持吗?


    有,还有 Float


    下面我们来查看 Double 源码,可以看到


    /** 
    * 一个常数,保持类型的正无穷大
    */

    public static final double POSITIVE_INFINITY = 1.0 / 0.0;
    /**
    * 一个常数,保持类型的负无穷大
    */

    public static final double NEGATIVE_INFINITY = -1.0 / 0.0;
    /**
    * 一个常数,非数值类型
    */

    public static final double NaN = 0.0d / 0.0;

    下面来试验下 0.0/0 与 -1.0/0


    Double 正无穷 = 1.0 / 0;
    Double 负无穷 = -1.0 / 0;
    System.out.println("正无穷:" + 正无穷);
    System.out.println("负无穷:" + 负无穷);
    Double 非数值 = 0.0 / 0;
    System.out.println("非数值 0.0/0 ->" + 非数值);

    输出:


    正无穷:Infinity
    负无穷:-Infinity
    非数值 0.0/0 ->NaN

    对无穷大进行运算


    下面来测试对 Float 类型与 Doubloe 类型无穷大进行运算


    public static void testFloatInfinity() {
    Float infFloat = Float.POSITIVE_INFINITY;
    Double infDouble = Double.POSITIVE_INFINITY;
    System.out.println("infFloat + 5 = " + (infFloat + 5));
    System.out.println("infFloat - infDouble = " + (infFloat - infDouble));
    System.out.println("infFloat * -1 = " + (infFloat * -1));
    }

    输出:


    infFloat + 5 = InfinityinfFloat - infDouble = NaNinfFloat * -1 = -Infinity

    可以注意到 1,3 行运算符合我们的预计结果


    ps: Infinity- Infinity 的结果不是数字类型


    对这些值进行判断


    public static void checkFloatInfinity() {   
    Double 正无穷 = 1.0 / 0;
    Double 负无穷 = -1.0 / 0;
    Double 非数值 = 0.0 / 0;
    System.out.println("判断正无穷: " + Double.isInfinite(正无穷));
    System.out.println("判断负无穷: " + (Double.NEGATIVE_INFINITY == 负无穷));
    System.out.println("判断非数值(==): " + (Double.NaN == 非数值));
    System.out.println("判断非数值(isNaN): " + Double.isNaN(非数值));
    }

    输出:


    判断正无穷: true
    判断负无穷: true
    判断非数值(==): false
    判断非数值(isNaN): true


    ps: 判断 NaN 不要使用 ==


    作者:程序员鱼丸
    来源:juejin.cn/post/7135621128818524174

    收起阅读 »

    谈谈国内前端的三大怪啖

    web
    因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。 今天聊三个事情: 小程序 微前端 模块加载 小程序 每个行业都有一把银座,当坐上那把银座时,做什么...
    继续阅读 »

    因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。


    今天聊三个事情:



    • 小程序

    • 微前端

    • 模块加载


    小程序



    每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。



    “我们为什么需要小程序?”


    第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。


    于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?


    说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。


    即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:





    看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。


    但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。


    所以从某种程度上来看,这更像是一场截胡的商业案例:


    应用市场

    全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。


    只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。


    反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。


    另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。


    在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?


    毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)


    那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。


    那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?


    于是,在 19 年夏天,深圳滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...


    全新体验心智

    小程序用起来挺方便的。


    你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?



    1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。

    2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂

    3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。


    H5小程序


    1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。


    我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。


    而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。


    心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。


    打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。




    我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。


    很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。


    管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。


    不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。



    当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。



    小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。


    但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。


    不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。


    小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。


    微前端


    qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?


    我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。


    先说下我的看法:



    1. 微前端,重在解决项目管理而不在用户体验。

    2. 微前端,解决不了该优化和需要规范的问题。

    3. 微前端,在挽救没想清楚 MPA 的 SPA 项目。


    没有万能银弹



    银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。



    所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。


    当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。


    不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。


    不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。


    不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。


    上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。


    B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。


    微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。


    SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。



    ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。



    质疑 “墨守成规”,打开视野,深度把玩,理性消费。


    分而治之


    分治法,一个很基本的工程思维。


    在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。


    你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)


    我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。


    比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。


    而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。


    当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。


    当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?


    只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。


    体验差异


    从 SPA 再回 MPA,说了半天不又回去了么。


    所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?


    流畅的用户体验:

    这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏


    但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。



    以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。


    因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。



    这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。



    所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。


    离线访问 (PWA)

    SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。


    但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。


    也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。


    项目协同、代码复用

    有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。


    这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。


    但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。


    这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。



    也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...



    这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”


    如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。


    项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。


    这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。


    模块加载


    模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。



    实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。


    import * from * 我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。


    模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。


    比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。


    比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。


    在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。


    当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。


    有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。



    题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、ASP 直接返回带有数据的 HTML Ajax 一样的事情么。




    传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。


    但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...


    到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,<script type="module"></script>,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。


    “但我们用不了,有兼容性问题。”


    哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。



    import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…


    试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。


    模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史



    历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。



    结语


    文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?


    因为我们的智慧需要有开花的土壤。如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。


    不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。



    希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...



    作者:YeeWang
    来源:juejin.cn/post/7267091810366488632
    收起阅读 »

    刚来公司就接了一个不发版直接改代码的需求

    前言 前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。 适用场景:代码逻辑需要经常变动的业务。 核...
    继续阅读 »

    前言


    前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。



    适用场景:代码逻辑需要经常变动的业务。


    核心思想



    • 页面改动 java 代码字符串

    • java 代码字符串编译成 class

    • 动态加载到 jvm



    实现重点


    JDK 提供了一个工具包 javax.tools 让使用者可以用简易的 API 进行编译。



    这些工具包的使用步骤:



    1. 获取一个 javax.tools.JavaCompiler 实例。

    2. 基于 Java 文件对象初始化一个编译任务 CompilationTask 实例。

    3. 因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后可以通过自定义的类加载器加载对应的类实例

    4. 使用反射 API 进行实例化和后续的调用。


    1. 代码编译


    这一步需要将 java 文件编译成 class,其实平常的开发过程中,我们的代码编译都是由 IDEA、Maven 等工具完成。


    内置的 SimpleJavaFileObject 是面向源码文件的,而我们的是源码字符串,所以需要实现 JavaFileObject 接口自定义一个 JavaFileObject。


    public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

    public static final String CLASS_EXTENSION = ".class";

    public static final String JAVA_EXTENSION = ".java";

    private static URI fromClassName(String className) {
    try {
    return new URI(className);
    } catch (URISyntaxException e) {
    throw new IllegalArgumentException(className, e);
    }
    }

    private ByteArrayOutputStream byteCode;
    private final CharSequence sourceCode;

    public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
    super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
    this.sourceCode = sourceCode;
    }

    public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
    super(fromClassName(fullClassName), kind);
    this.sourceCode = null;
    }

    public CharSequenceJavaFileObject(URI uri, Kind kind) {
    super(uri, kind);
    this.sourceCode = null;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
    return sourceCode;
    }

    @Override
    public InputStream openInputStream() {
    return new ByteArrayInputStream(getByteCode());
    }

    // 注意这个方法是编译结果回调的OutputStream,回调成功后就能通过下面的getByteCode()方法获取目标类编译后的字节码字节数组
    @Override
    public OutputStream openOutputStream() {
    return byteCode = new ByteArrayOutputStream();
    }

    public byte[] getByteCode() {
    return byteCode.toByteArray();
    }
    }

    如果编译成功之后,直接通过 CharSequenceJavaFileObject#getByteCode()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)


    2. 实现 ClassLoader


    因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后得通过自定义的类加载器加载对应的类实例,否则是加载不了的,因为同一个类只会加载一次。


    主要关注 findClass 方法


    public class JdkDynamicCompileClassLoader extends ClassLoader {

    public static final String CLASS_EXTENSION = ".class";

    private final static Map<String, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

    public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
    super(parentClassLoader);
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    JavaFileObject javaFileObject = javaFileObjectMap.get(name);
    if (null != javaFileObject) {
    CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
    byte[] byteCode = charSequenceJavaFileObject.getByteCode();
    return defineClass(name, byteCode, 0, byteCode.length);
    }
    return super.findClass(name);
    }

    @Override
    public InputStream getResourceAsStream(String name) {
    if (name.endsWith(CLASS_EXTENSION)) {
    String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
    CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
    if (null != javaFileObject && null != javaFileObject.getByteCode()) {
    return new ByteArrayInputStream(javaFileObject.getByteCode());
    }
    }
    return super.getResourceAsStream(name);
    }

    /**
    * 暂时存放编译的源文件对象,key为全类名的别名(非URI模式),如club.throwable.compile.HelloService
    */

    void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
    javaFileObjectMap.put(qualifiedClassName, javaFileObject);
    }

    Collection<JavaFileObject> listJavaFileObject() {
    return Collections.unmodifiableCollection(javaFileObjectMap.values());
    }
    }

    3. 封装了上面的 ClassLoader 和 JavaFileObject


    public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private final JdkDynamicCompileClassLoader classLoader;
    private final Map<URI, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

    public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
    super(fileManager);
    this.classLoader = classLoader;
    }

    private static URI fromLocation(Location location, String packageName, String relativeName) {
    try {
    return new URI(location.getName() + '/' + packageName + '/' + relativeName);
    } catch (URISyntaxException e) {
    throw new IllegalArgumentException(e);
    }
    }

    @Override
    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
    JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
    if (null != javaFileObject) {
    return javaFileObject;
    }
    return super.getFileForInput(location, packageName, relativeName);
    }

    /**
    * 这里是编译器返回的同(源)Java文件对象,替换为CharSequenceJavaFileObject实现
    */

    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
    JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
    classLoader.addJavaFileObject(className, javaFileObject);
    return javaFileObject;
    }

    /**
    * 这里覆盖原来的类加载器
    */

    @Override
    public ClassLoader getClassLoader(Location location) {
    return classLoader;
    }

    @Override
    public String inferBinaryName(Location location, JavaFileObject file) {
    if (file instanceof CharSequenceJavaFileObject) {
    return file.getName();
    }
    return super.inferBinaryName(location, file);
    }

    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
    Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
    List<JavaFileObject> result = new ArrayList<>();
    // 这里要区分编译的Location以及编译的Kind
    if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
    // .class文件以及classPath下
    for (JavaFileObject file : javaFileObjectMap.values()) {
    if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
    result.add(file);
    }
    }
    // 这里需要额外添加类加载器加载的所有Java文件对象
    result.addAll(classLoader.listJavaFileObject());
    } else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
    // .java文件以及编译路径下
    for (JavaFileObject file : javaFileObjectMap.values()) {
    if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
    result.add(file);
    }
    }
    }
    for (JavaFileObject javaFileObject : superResult) {
    result.add(javaFileObject);
    }
    return result;
    }

    /**
    * 自定义方法,用于添加和缓存待编译的源文件对象
    */

    public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
    javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
    }
    }

    4. 使用 JavaCompiler 编译并反射生成实例对象


    public final class JdkCompiler {

    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

    @SuppressWarnings("unchecked")
    public static <T> T compile(String packageName,
    String className,
    String sourceCode) throws Exception {
    // 获取系统编译器实例
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    // 设置编译参数
    List<String> options = new ArrayList<>();
    options.add("-source");
    options.add("1.8");
    options.add("-target");
    options.add("1.8");
    // 获取标准的Java文件管理器实例
    StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
    // 初始化自定义类加载器
    JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());

    // 初始化自定义Java文件管理器实例
    JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
    String qualifiedName = packageName + "." + className;
    // 构建Java源文件实例
    CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
    // 添加Java源文件实例到自定义Java文件管理器实例中
    fileManager.addJavaFileObject(
    StandardLocation.SOURCE_PATH,
    packageName,
    className + CharSequenceJavaFileObject.JAVA_EXTENSION,
    javaFileObject
    );
    // 初始化一个编译任务实例
    JavaCompiler.CompilationTask compilationTask = compiler.getTask(
    null,
    fileManager,
    DIAGNOSTIC_COLLECTOR,
    options,
    null,
    Collections.singletonList(javaFileObject)
    );
    Boolean result = compilationTask.call();
    System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result));
    Class<?> klass = classLoader.loadClass(qualifiedName);
    return (T) klass.getDeclaredConstructor().newInstance();
    }
    }

    完成上面工具的搭建之后。我们可以接入数据库的操作了。数据库层面省略,只展示 service 层


    service 层:


    public class JavaService {

    public Object saveAndGetObject(String packageName,String className,String javaContent) throws Exception {
    Object object = JdkCompiler.compile(packageName, className, javaContent);
    return object;
    }

    }

    测试:


    public class TestService {

    public static void main(String[] args) throws Exception {
    test();
    }

    static String content="package cn.mmc;\n" +
    "\n" +
    "public class SayHello {\n" +
    " \n" +
    " public void say(){\n" +
    " System.out.println(\"11111111111\");\n" +
    " }\n" +
    "}";

    static String content2="package cn.mmc;\n" +
    "\n" +
    "public class SayHello {\n" +
    " \n" +
    " public void say(){\n" +
    " System.out.println(\"22222222222222\");\n" +
    " }\n" +
    "}";

    public static void test() throws Exception {
    JavaService javaService = new JavaService();
    Object sayHello = javaService.saveAndGetObject("cn.mmc", "SayHello", content);
    sayHello.getClass().getMethod("say").invoke(sayHello);

    Object sayHello2 = javaService.saveAndGetObject("cn.mmc", "SayHello", content2);
    sayHello2.getClass().getMethod("say").invoke(sayHello2);
    }
    }

    我们在启动应用时,更换了代码文件内存,然后直接反射调用对象的方法。执行结果:


    可以看到,新的代码已经生效!!!



    注意,直接开放修改代码虽然方便,但是一定要做好安全防护


    作者:女友在高考
    来源:juejin.cn/post/7134155429147312141

    收起阅读 »

    你身边的那些'加班文化'

    说起来加班,别管什么80后,90后,00后,内心肯定都是非常抵制的,朝九晚六,平衡工作生活,想必是大多数人的梦想。 为什么突然想写一篇这样的文章,这两天确实有几件事情触动了我。 1.同事猝死 曾任职的一家公司,一名同事于4月1日猝死。以前总是看网上那些猝死的案...
    继续阅读 »

    说起来加班,别管什么80后,90后,00后,内心肯定都是非常抵制的,朝九晚六,平衡工作生活,想必是大多数人的梦想。

    为什么突然想写一篇这样的文章,这两天确实有几件事情触动了我。


    1.同事猝死


    曾任职的一家公司,一名同事于4月1日猝死。以前总是看网上那些猝死的案例,感慨加班猝死,不曾想这件事生生的发生在自己身边,这名同事是一名销售,具体猝死缘由(前)公司并没有多谈,但我想一定是和熬夜加班息息相关。


    image.png


    这家公司的加班文化,我深有体会,去年11月初,各个项目组长在工作群先后通知强制要求加班,并不给你机会以及理由请假,反正加班给出的理由就是冲刺冲刺。


    1.png


    当时有同事用dingding和其他同事讨论加班的各种不情愿,聊天记录居然被公司扒出来并认为其散播负面言论,邮件全体对其进行警告,我大为震惊,当然这名同事后来也进入了裁员名单。


    4.png


    警告信发出之后越来越多的同事周末有一天不能来公司办公,其实他们在家根本也没闲着,这时候大老板急了,又发了这样的一封邮件。


    5.png


    (我看到这封邮件的时候,心里的想法就是作为公司的领头人,这格局不是一般的小,仅代表个人意见)

    显然这封邮件是给那些不坚持10 10 7的人看的,对于那些不加班的人等待着的自然是优化。就这样还是有些同事继续坚持着10 10 7,在元旦之前大家终于把所有项目全部成功交付,本以为等待着大家的是老板分享喜悦的成果,结果等来的却是3/2的裁员,后来大家想明白了,就是催促着大家感觉把项目做完,裁掉你们。其中有位同事,前一天通宵加完班,第二天就被裁了,那时候整个办公室一片哗然。


    2.持续加班住进icu


    image.png


    image.png


    朋友的同事,据说刚刚升为一名奶爸,每天十点前不曾下班,大家问他为何这么拼,给出的理由是我刚刚生了娃,买了房子,工作一点不敢懈怠,怕惹得领导不高兴随时让我滚蛋。

    是啊,谁想加班,谁愿意加班天天守着个破电脑,可是生活让我们不得不加班,不得不向一些不合理的规定屈服。


    3.中国电科龙哥


    这两天龙哥火遍了各个社交网络,龙哥痛批强制加班员工事情为什么能火,为什么传播速度这么快,我猜测是龙哥一人之言说出了很多打工人的心声,引起网友一片共鸣。


    不过央视网的这文字怕是惹得大家一顿p喽。


    720635d7223ec0b82485f989ae38034.jpg


    职场人偶尔加下班,没有任何问题,毕竟工作中肯定会遇到一些紧急事件需要及时处理,作为公司员工,加班赶下进度也是应该的,可是长期被自愿加班就不合时宜了,毕竟身体是革命的本钱,一旦因为长期加班,出现问题就得不偿失了,所以加班文化不可取,尤其是超

    作者:zhouzhouya
    来源:juejin.cn/post/7218735340043092028
    长加班更不应该存在。

    收起阅读 »

    项目中前端如何实现无感刷新 token!

    前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下! 环境请求采用的 Axios V1.3...
    继续阅读 »

    前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下!


    环境

    1. 请求采用的 Axios V1.3.2。
    2. 平台的采用的 JWT(JSON Web Tokens) 进行用户登录鉴权。
      (拓展:JWT 是一种认证机制,让后台知道该请求是来自于受信的客户端;更详细的可以自行查询相关资料)

    问题现象


    线上用户在使用的时候,偶尔会出现突然跳转到登录页面,需要重新登录的现象。


    原因

    1. 突然跳转到登录页面,是由于当前的 token 过期,导致请求失败;在 axios 的响应拦截axiosInstance.interceptors.response.use中处理失败请求返回的状态码 401,此时得知token失效,因此跳转到登录页面,让用户重新进行登录。
    2. 平台目前的逻辑是在 token 未过期内,用户登录平台可直接进入首页,无需进行登录操作;因此就存在该现象:用户打开平台,由于此时 token 未过期,用户直接进入到了首页,进行其他操作。但是在用户操作的过程中,token 突然失效了,此时就会出现突然跳转到登录页面,严重影响用户的体验感!
      注:目前线上项目中存在数据大屏,一些实时数据的显示;因此存在用户长时间停留在大屏页面,不进行操作,查看实时数据的情况

    切入点

    1. 怎样及时的、在用户感知不到的情况下更新token
    2. 当 token 失效的情况下,出错的请求可能不仅只有一个;当失效的 token 更新后,怎样将多个失败的请求,重新发送?

    操作流程


    好了!经过了一番分析后,我们找到了问题的所在,并且确定了切入点;那么接下来让我们实操,将问题解决掉。

    前要:

    1、我们仅从前端的角度去处理。

    2、后端提供了两个重要的参数:accessToken(用于请求头中,进行鉴权,存在有效期);refreshToken(刷新令牌,用于更新过期的 accessToken,相对于 accessToken 而言,它的有效期更长)。


    1、处理 axios 响应拦截


    注:在我实际的项目中,accessToken 过期后端返回的 statusCode 值为 401,需要在axiosInstance.interceptors.response.useerror回调中进行逻辑处理

    // 响应拦截
    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    (error) => {
    let {
    data, config
    } = error.response;
    return new Promise((resolve, reject) => {
    /**
    * 判断当前请求失败
    * 是否由 toekn 失效导致的
    */
    if (data.statusCode === 401) {
    /**
    * refreshToken 为封装的有关更新 token 的相关操作
    */
    refreshToken(() => {
    resolve(axiosInstance(config));
    });
    } else {
    reject(error.response);
    }
    })
    }
    )


    1. 我们通过判断statusCode来确定,是否当前请求失败是由token过期导致的;

    2. 使用 Promise 处理将失败的请求,将由于 token 过期导致的失败请求存储起来(存储的是请求回调函数,resolve 状态)。理由:后续我们更新了 token 后,可以将存储的失败请求重新发起,以此来达到用户无感的体验


    补充:


    现象:在我过了几天登录平台的时候发现,refreshToken过期了,但是没有跳转到登录界面
    原因

    1、当refreshToken过期失效后,后端返回的状态码也是 401

    2、发起的更新token的请求采用的也是处理后的axios,因此响应失败的拦截,对更新请求同样适用

    问题:

    这样会造成,当refreshToken过期后,会出现停留在首页,无法跳转到登录页面。

    解决方法

    针对这种现象,我们需要完善一下axios中响应拦截的逻辑

    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    (error) => {
    let {
    data, config
    } = error.response;
    return new Promise((resolve, reject) => {
    /**
    * 判断当前请求失败
    * 是否由 toekn 失效导致的
    */
    if (
    data.statusCode === 401 &&
    config.url !== '/api/token/refreshToken'
    ) {
    refreshToken(() => {
    resolve(axiosInstance(config));
    });
    } else if (
    data.statusCode === 401 &&
    config.url === '/api/token/refreshToken'
    ) {
    /**
    * 后端 更新 refreshToken 失效后
    * 返回的状态码, 401
    */
    window.location.href = `${HOME_PAGE}/login`;
    } else {
    reject(error.response);
    }
    })
    }
    )

    2、封装 refreshToken 逻辑


    要点:



    1. 存储由于token过期导致的失败的请求。
    2. 更新本地以及axios中头部的token
    3. 当 refreshToken 刷新令牌也过期后,让用户重新登录
    // 存储由于 token 过期导致 失败的请求
    let expiredRequestArr: any[] = [];

    /**
    * 存储当前因为 token 失效导致发送失败的请求
    */
    const saveErrorRequest = (expiredRequest: () => any) => {
    expiredRequestArr.push(expiredRequest);
    }

    // 避免频繁发送更新
    let firstRequre = true;
    /**
    * 利用 refreshToken 更新当前使用的 token
    */
    const updateTokenByRefreshToken = () => {
    firstRequre = false;
    axiosInstance.post(
    '更新 token 的请求',
    ).then(res => {
    let {
    refreshToken, accessToken
    } = res.data;
    // 更新本地的token
    localStorage.setItem('accessToken', accessToken);
    // 更新请求头中的 token
    setAxiosHeader(accessToken);
    localStorage.setItem('refreshToken', refreshToken);

    /**
    * 当获取了最新的 refreshToken, accessToken 后
    * 重新发起之前失败的请求
    */
    expiredRequestArr.forEach(request => {
    request();
    })
    expiredRequestArr = [];
    }).catch(err => {
    console.log('刷新 token 失败err', err);
    /**
    * 此时 refreshToken 也已经失效了
    * 返回登录页,让用户重新进行登录操作
    */
    window.location.href = `${HOME_PAGE}/login`;
    })
    }

    /**
    * 更新当前已过期的 token
    * @param expiredRequest 回调函数,返回由token过期导致失败的请求
    */
    export const refreshToken = (expiredRequest: () => any) => {
    saveErrorRequest(expiredRequest);
    if (firstRequre) {
    updateTokenByRefreshToken();
    }
    }

    补充:


    问题:

    1、怎么能保证当更新token后,在处理存储的过期请求时,此时没有过期请求还在存呢?;万一此时还在expiredRequestArr推失败的请求呢?

    解决方法
    我们需要调整一下更新 token的逻辑,确保当前由于过期失败的请求都接收到了,再更新token然后重新发起请求。


    最终结果:

    // refreshToken.ts

    /**
    * 功能:
    * 用于实现无感刷新 token
    */
    import { axiosInstance, setAxiosHeader } from "@/axios"
    import { CLIENT_ID, HOME_PAGE } from "@/systemInfo"

    // 存储由于 token 过期导致 失败的请求
    let expiredRequestArr: any[] = [];

    /**
    * 存储当前因为 token 失效导致发送失败的请求
    */
    const saveErrorRequest = (expiredRequest: () => any) => {
    expiredRequestArr.push(expiredRequest);
    }

    /**
    * 执行当前存储的由于过期导致失败的请求
    */
    const againRequest = () => {
    expiredRequestArr.forEach(request => {
    request();
    })
    clearExpiredRequest();
    }

    /**
    * 清空当前存储的过期请求
    */
    export const clearExpiredRequest = () => {
    expiredRequestArr = [];
    }

    /**
    * 利用 refreshToken 更新当前使用的 token
    */
    const updateTokenByRefreshToken = () => {
    axiosInstance.post(
    '更新请求url',
    {
    clientId: CLIENT_ID,
    userName: localStorage.getItem('userName')
    },
    {
    headers: {
    'Content-Type': 'application/json;charset=utf-8',
    'Authorization': 'bearer ' + localStorage.getItem("refreshToken")
    }
    }
    ).then(res => {
    let {
    refreshToken, accessToken
    } = res.data;
    // 更新本地的token
    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', refreshToken);
    setAxiosHeader(accessToken);
    /**
    * 当获取了最新的 refreshToken, accessToken 后
    * 重新发起之前失败的请求
    */
    againRequest();
    }).catch(err => {
    /**
    * 此时 refreshToken 也已经失效了
    * 返回登录页,让用户重新进行登录操作
    */
    window.location.href = `${HOME_PAGE}/login`;
    })
    }

    let timer: any = null;
    /**
    * 更新当前已过期的 token
    * @param expiredRequest 回调函数,返回过期的请求
    */
    export const refreshToken = (expiredRequest: () => any) => {
    saveErrorRequest(expiredRequest);
    // 保证再发起更新时,已经没有了过期请求要进行存储
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
    updateTokenByRefreshToken();
    }, 500);
    }
    // 响应拦截 区分登录前
    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    (error) => {
    let {
    data, config
    } = error.response;
    return new Promise((resolve, reject) => {
    /**
    * 判断当前请求失败
    * 是否由 toekn 失效导致的
    */
    if (
    data.statusCode === 401 &&
    config.url !== '/api/token/refreshToken'
    ) {
    refreshToken(() => {
    resolve(axiosInstance(config));
    });
    } else if (
    data.statusCode === 401 &&
    config.url === '/api/token/refreshToken'
    ) {
    /**
    * 后端 更新 refreshToken 失效后
    * 返回的状态码, 401
    */
    clearExpiredRequest();
    window.location.href = `${HOME_PAGE}/login`;
    } else {
    reject(error.response);
    }
    })
    }
    )

    补充


    感谢很多朋友提出了很多更好的方法;我写这篇文章主要是为了分享一下,恰好这种问题推到了我(前端工程师)身上,我是怎样处理的;虽然有可能在一些朋友看来很低级,但它确是我实际工作中碰到的问题,每一个问题的出现解决后都对自身是一种成长,通过分享的方式来巩固自己,也希望能对他人有一些帮助!


    总结


    经过一波分析以及操作,我们最终实现了实际项目中的无感刷新token,最主要的是有效避免了:用户在平台操作过程中突然要退出登录的现象(尤其是当用户进行信息填写,突然要重新登录,之前填写的信息全部作废,是很容易让人发狂的)。

    其实回顾一下,技术上并没有什么难点,只是思路上自己是否能够想通、自洽。人是一棵会思想的芦苇,我们要有自己的思想,面对问题,有自己的思考。

    希望我们能在技术的路上走的越来越远,与君共勉!!!


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

    iOS 组件间通信,另一种与众不同的实现方式

    iOS
    本文已参与「新人创作礼」活动,一起开启掘金创作之路。 组件间通信,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通信问题。 那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:URL 路由target-actionprotoco...
    继续阅读 »

    本文已参与「新人创作礼」活动,一起开启掘金创作之路。


    组件间通信,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通信问题。


    那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:

    1. URL 路由
    2. target-action
    3. protocol


    iOS:组件化的三种通讯方案 这篇写的挺不错,没了解的同学可以看一下



    也有很多第三方组件代表,MGJRouterCTMediatorBeeHiveZIKRouter 等(排名不分前后[手动狗头])。


    但他们或多或少都有各自的优缺点,这里也不展开说,但基本上的有这么几种问题:

    1. 使用起来比较繁琐,需要理解成本,开发起来也需要写很多冗余代码。
    2. 基本都需要先注册,再实现。那就无法保证代码一定存在实现,也无法保证实现是否跟注册出现不一致(当然你可以增加一些校验手段,比如静态检测之类的)。这一点在比较大型的项目里都是很痛的,要不就不敢删除历史代码来积债,要不就是莽过去,测试或者线上出现问题[手动狗头]。
    3. 如果存在 Model 需要传递,要不下沉到公共模块,要不就是转 NSDictionary。还是公共层积债或者模型变更导致运行时出问题。

    那有没有银弹呢?这就是本次要讲的实现方式,换个角度解决问题。


    与众不同的方案


    通过上述的问题,想一下我们想要的实现是什么样:

    1. 不需要增加开发成本,也不需要理解整体的实现原理。
    2. 由组件提供方提供,先有实现再有定义,保证 API 是完全可用的,如果实现发生变更,调用方会编译时报错(问题暴露前置)。且其他模块不依赖但又可以准确调用到这个方法。
    3. 各类模型在模块内是正常使用的,且对外暴露也是可以正常使用的,但又不用去下沉在公共模块。

    是不是感觉要求很过分?就像一个渣男既不想跟你结婚,又想跟你生孩子[手动狗头] 。


    但能不能实现呢,确实是可以的。但解决办法不在 iOS 本身,而在 codegen。铺垫到这里,我们来看看具体实现。


    GDAPI 原理


    在笔者所在的稿定,之前用的是 CTMediator 方案做组件间通信,当然也就有上面的那些问题,甚至线上也出现过因为 Protocol 找不到 Mediator 导致的线上 crash。


    为了解决定义和实现不匹配的问题,我们希望定义一定要有实现,实现一定要跟定义一致。


    那是否就可以换个思路,先有实现,再有定义,从实现生成定义。


    这点参考了 JAVA 的注解机制,我们定义了一个宏 GDM_EXPORT_MODULE(),用于说明哪些方法是需要开发给其他模块使用的。

    // XXLoginManager.h

    /// 判断是否登陆
    - (BOOL)isLogin GDM_EXPORT_MODULE();

    这样在组件开发方就完成了 API 开放,剩下的工作就是如何生成一个调用层代码。


    调用层代码其实也就是 CTMediator 的翻版,通过 iOS 的运行时反射机制去寻找实现类

    // XXService.m

    static id<GDXXXAPI> _mXXXService = nil;
    + (id<GDXXXAPI>)XXXService {
    if (_mXXXService == nil) {
    _mXXXService = [self implementorOfName:@"GDXXXManager"];
    }
    return _mXXXService;
    }

    我们把这些生成的方法调用,生成到一个 GDAPI 模块统一存储,当然这个模块除了上述模块的 Service 层是要有具体的 .m 来做落地,其他都是 .h 的头文件。


    那调用侧只需要 pod 增加依赖 s.dependency 'GDAPI/XXXXService' 即可调用到具体实现了

    @import GDAPI;

    ...

    bool isLogin = [GDAPI.XXService isLogin];


    这里肯定有同学会问,生成过程呢???


    笔者是用 Ruby 代码实现了整个 codegen 过程,当时没选择 Python 主要是为了跟 cocoapods 使用相同的开发语言,易于做侵入设计,但其实用其他语言都没问题,通过 shell 脚本做中转即可。




    这里源码有些定制化实现,放出来现在也是徒增大家烦恼,所以讲一下生成关键过程:

    1. 遍历组件所在目录,取出所有的 .h 文件,缓存在 Map<文件路径,文件内容>(一级缓存)
    2. 解析存在 GDM_EXPORT_MODULE() 的方法,将方法的名称、参数、注释通过正则手段分解成相应的属性,存储到 Map<模块名,API 模型列表> (二级缓存)
    3. 对于每一个 API 模型进行进一步解析,解析入参和出参,判断参数类型是否为自定义类型(模型、代理、枚举、包括复杂的 NSArray<CustomModel *> * 等),如果有存在,则遍历一级缓存,找到自定义类型的定义,生成对应的 Model -> Procotol 等,且存储在多个 Map 中 Map<类名/代理名/枚举名,具体解析后的模型>(三级缓存)
    4. 有了 AST 生成就变得很简单,模版代码 + 模版输出即可


    有了上述各种模型,就差不多完成了 AST (抽象语法树) 的生成过程,至于为什么是用的正则而不是 iOS 的 AST 工具,主要原因是想做的很轻,尽量减少大家的构建时长,不要通过编译来实现。0



    可以看到已经有大量模块生成了相应的 GDAPI




    执行时长在 2S 左右,因为有一个预执行的过程,来做组件项目化,这个也算是特殊实现了。
    实质上执行也就 1S 即可。


    还有一点要说的是执行时机是在 pod install / update 之前,这个是通过 hooks cocoapods 的执行过程做到的。


    一些难点


    嵌套模型


    上面虽然粗略的讲了下 Model / Procotol 会生成 Protocol,但其实这一部分确实是最困难的,也是因为历史积债问题,下沉在公共模块的庞大的模型在各个组件里传输。


    那要把它完全的 API 化,就需要对它的属性进行递归解析,生成完全符合的 protocol


    例如:

    ... 举例为伪代码,OC 代码确实很啰嗦

    class A extends B {
    C c;

    NSArray<D> d;
    }

    /// 测试
    - (void)test:(A *)a GDM_EXPORT_MODULE();

    生成结果就如下图(伪代码):


    @protocol GDAPI_A {
    NSObject<GDAPI_C> c;

    NSArray<NSObject<GDAPI_D>> d;
    }

    @protocol GDAPI_B {
    }

    @protocol GDAPI_C {
    }

    @protocol GDAPI_D {
    }

    以及调用服务

    @protocol GDXXXAPI <NSObject>
    /// 测试
    - (void)test:(NSObject<GDAPI_A, GDAPI_B>)a;


    这个在落地过程中坑确实非常多。


    B 模块想创建 A 模块的模型


    当然这个是很不合理的,但现实中确实很多这样的历史问题。


    当然也不能用模型下沉开倒车,那解决上用了一个巧劲

    /// 创建 XX
    - (XXXModel *)createXXX GDM_EXPORT_MODULE();

    提供一个创建模型的 API 给外部使用,这样对于 Model 的管理还是在模块内,外部模块使用上从 new XXX() 改为 [GDAPI.XXService createXX]; 即可。


    零零碎碎


    用正则判断抓取 AST,在一些二三方库中也是很常见的,但来处理 OC 确实挺痛苦的,再加上历史代码很多没什么规范,空格、注释各式各样,写个通用的适配算是比较耗时的。


    还有就是一些个性化的兼容,也存在一些硬编码的情况,比如有些组件去关联到的 Model 在 framework 中,维护一个对应表,用 @class 来兼容解决。


    后续


    篇(jing)幅(li)有限,就不再展开说明,这个实现思路影响了笔者后续的很多开发过程,有兴趣可以看下笔者 Flutter 的文章,里面也是 codegen 的广泛运用。


    如果有任何问题,都可以评论区一起讨论。


    手敲不易,如果对你学习工作上有所启发,请留个赞, 感谢阅读 ~~


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