注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

北京某3平米出租屋内最“难堪”的一幕,戳破当下社会悲哀的真相

01前段时间,在网上看到这样一则帖子,心里顿时涌上一股酸楚。有人在朋友圈发出了一则出租信息,配文称:“国贸CBD附近,独卫1000/月。随时看房,这哥们随时搬走。”配图是这间出租屋的照片,准确来说,这就是一个厕所改造的房间。用来如厕的蹲坑旁边,就是一张简陋的单...
继续阅读 »

01

前段时间,在网上看到这样一则帖子,心里顿时涌上一股酸楚。

有人在朋友圈发出了一则出租信息,配文称:

“国贸CBD附近,独卫1000/月。随时看房,这哥们随时搬走。”


配图是这间出租屋的照片,准确来说,这就是一个厕所改造的房间。

用来如厕的蹲坑旁边,就是一张简陋的单人床,上面杂乱地铺着被褥和衣物。

床铺前面是一张小小的四方桌,上面放着电磁炉和锅具,还有几瓶调味品。

一男子坐在凳子上吃泡面,仿佛丝毫不在意这恶劣的就餐环境。

他身后的墙上,还贴着一副劣质贴纸,上面有一行字:“生活嘛,笑笑就好了。”

这则帖子的评论区里,有人觉得不可思议、大开眼界:


“这房子还要一千一月啊?”

有人感慨人间疾苦、活着不容易:


“有人在生活,有人只是活着。”

有人分享了自己身边发生的真实经历:


“我们学校有同学租不起房,扎帐篷住在图书馆里很久。”

是啊,在你看不到的地方,有多少人正蜗居在10平米不到的“纳米房”里,度过每一天。

他们做饭用不了明火,只能用电磁炉;

晒衣服没有阳台,只能悬挂在床的正上方;

一张床铺狭窄得连腿都伸不开,只能蜷缩着身子睡觉......


但即使这样,他们依旧在每天奋力生活着,就像那副贴在墙上的贴纸,懂得苦中作乐,把生活过得充满烟火气。

02

之前有一档导演竞技类的综艺,有位女导演因为执导了一部短片而被群嘲了。

短片讲的是这样一个故事:外卖员丁小北,独自一个人在上海打拼。

每天忙着送外卖,疏忽了与家人的联络。

直到有一天接到爷爷去世的电话,他才悔悟:最该给爷爷送的那一单,却永远无法送达了。

看得出来,导演很想通过这类底层小人物的温情故事,来达到催泪、震撼人心的效果。

然而,拍出来的效果却是,观众们丝毫无法与短片中的外卖员共情,只觉得尴尬。

为什么?

在寸土寸金的上海,租住着带有独卫的大单间,床上用品和窗帘,都是简约高级的北欧风。

旁边的书桌和书架,都是材质极好的原木复古风,上面堆满了书籍,架子旁还放着一把吉他。

你猜猜这样的房子在上海要租多少钱一个月?至少也要6000打底吧。

而短片中的外卖小哥,平均每天只能送30单外卖,也就是说,日薪最高也就只有150元,每个月的工资都不够来交房租的。

悬浮、割裂、强行煽情,让所有人都面无表情地看完了这部短片,唯独女导演自己哭得梨花带雨,感动了自己。

放眼当下的国产影视剧里,也很难再看到真正的“穷人”了。

某穿越电视剧中,女主角是一位一没钱二没势的小北漂,普通上班族。


然而,你看看这位小北漂居住的房子,就知道导演和编剧有多离谱:

精装修的豪华复式房,超大落地窗,各种家电家具一应俱全;


当屋外阳光正好,就会恰到好处地照进屋里的梦幻大浴缸中。


我实在是无法理解,能住在这样的房子里,普通小北漂真的能承受得起?

还有某爱情偶像剧里,男主的设定是:穷苦的大学生,读书之余还要同时兼职几份工,供养年级还小的弟弟和妹妹。

听起来是不是很惨?然而,看看他们在剧中租住的房子吧:


在大城市深圳,租的是带花园的独栋复式,屋子里充斥着各种糖果色的家具。

剧中的白富美女主,在这样的房子里居然还发出了感慨:


“我以为自己知道穷是什么意思,可到了今天,我才真的明白。”

你明白了啥?恐怕你对穷人的理解还是存在着很大的误区......

还有某都市情感剧里,女主的设定是刚毕业的房产客服,欠着信用卡5000多,身上也只剩下2000多。

而她住的房子,则是温馨别致的一房一厅。


在国产剧编辑的眼里,也许穿着运动裤、坐在沙发上吃着自嗨锅,就已经代表着贫穷了......

傲慢,太傲慢了。明星对普通人生活的苦,原来就只有这点想象力。

你无法从他们那里找到任何对普通人的体恤、尊重和共情,只看得出优越感,和对底层的俯视。

03

昨天,有这样一个话题火上了热搜第一:“真的建议明星别卖穷了”。

事情起因,是内娱的某位选秀明星,在访谈中说自己接不到通告,已经9个月没有收入了。

但她的哭穷,是很难得到网友们的共情的。因为明星眼里的“穷”,可能是指卡里只剩一百万收入了。


他们拍个戏轻轻松松都能拿个几百上千万的片酬,上个综艺节目露个脸,几万几十万的通告费就轻松到手。


而在很多普通人眼中,这个水平的收入,是穷尽一生的努力都无法达到的。

评论区里,密密麻麻几百上千条留言,都是普通人真实的生活:


有人为了省钱,中午饭都是早餐店里的饼子加榨菜;

有人好几年没买新衣服了,看中了一件白色短袖,39元,嫌贵,没舍得买;

有人不小心拿错了一支高价雪糕,付款的时候看到要十几块钱,只能含泪吃完,之后心疼、难过了半天......

还记得在知乎上之前有个很火的提问:“因为穷,你做过什么卑微的事情?”

一位叫“King”的答主,诉说了自己的故事:

高中时住校,中午点一份凉皮外卖,加上红包满减实际消费3元多,备注多加辣。

吃完凉皮,汤料留到晚上,泡份白水面条,挑到中午的凉皮汤里拌着吃。

还得估算当天的学习状态,不满意就少吃点面条,作为惩罚。

周五晚上放学,走16里路回家,跟朋友说想散散步,实际上是为了省下2块钱的公交车费,买包肉类零食,给家里晚饭添荤。

读大学送外卖挣钱,暴雨夜赶时间飙车,摔断了膝关节韧带,还伤了腰。

每年寒暑假回家,单趟48小时,只买硬座。

好在,他通过用功读书,毕业后找到了月薪8000的工作,终于不用再让贫穷成为困扰他日日夜夜的梦。

但在你看不到的角落,还有多少普通人,因生活所迫,把自己低到了尘埃里。

04

心理学上有这样一个词,叫“认知盲区”,指的是:

你注意不到的地方,你不知道自己不知道的,就是盲区。

就像《笑林广记》中的聋人,看到别人放鞭炮,感到很困惑,好好的纸筒怎么突然四分五裂。

聋人不能理解,是因为他缺少了听觉的维度。

当我们观察事物,如果丢失了一个维度,你可能就永远无法知道发生了什么。

见识不光是往上走的,还应该是往下走的。

丧失了对普通小人物的共情能力,并不是什么值得骄傲的事。

国产剧编辑和明星们的优越感,恰恰只能证明了他们的没见识和无知。

很喜欢村上春树的一句话:“我们只是落向广袤大地的众多雨滴中那无名的一滴。

人世百态,纵使现实不如意,但你要相信依旧会有光亮照进来,让你重拾对生活的信心。

愿你在生活的万般刁难下,也能留住那半分的温柔与可爱。

共勉。

作者:小椰子

链接:https://zhuanlan.zhihu.com/p/531219466?utm_source=qq&utm_medium=social&utm_oi=34442248716288

收起阅读 »

线程池7个参数拿捏死死的,完爆面试官

线程池 上一章节我们介绍的四种创建线程的方式算是热身运动了。线程池才是我们的重点介绍对象。 这个是JDK对线程池的介绍。 但是你会问为什么上面我们创建线程池的方式是通过Executors.newCachedThreadPool(); 关...
继续阅读 »

线程池



  • 上一章节我们介绍的四种创建线程的方式算是热身运动了。线程池才是我们的重点介绍对象。


image-20211214192828938.png



  • 这个是JDK对线程池的介绍。


image-20211214193012582.png



  • 但是你会问为什么上面我们创建线程池的方式是通过Executors.newCachedThreadPool();


image-20211214193421625.png


image-20211214193626746.png




  • 关于Exectors内部提供了很多快捷创建线程的方式。这些方法内部都是依赖ThreadPoolExecutor。所以线程池的学习就是ThreadPoolExecutor




  • 线程池ThreadPoolExecutor正常情况下最好用线程工厂来创建线程。他的作用是用来处理每一次提交过来的任务;ThreadPoolExecutor可以解决两个问题



    • 在很大并发量的情况下线程池不仅可以提供稳定的处理还可以减少线程之间的调度开销。

    • 并且线程池提供了对线程和资源的回收及管理。




  • 另外在内部ThreadPoolExecutor提供了很多参数及可扩展的地方。同时他也内置了很多工厂执行器方法供我们快速使用,比如说Executors.newCacheThreadPool():无限制处理任务。 还有Executors.newFixedThreadPool():固定线程数量;这些内置的线程工厂基本上能满足我们日常的需求。如果内置的不满足我们还可以针对内部的属性进行个性化设置




image-20211215135754716.png



  • 通过跟踪源码我们不难发现,内置的线程池构建都是基于上面提到的7个参数进行设置的。下面我画了一张图来解释这7个参数的作用。


image-20211215141012974.png



  • 上面这张图可以解释corePoolSizemaxiumPoolSizekeepAliveTimeTimeUnitworkQueue 这五个参数。关于threadFactoryhandler是形容过程中的两个参数。

  • 关于ThreadPoolExecutor我们还得知道他虽然是线程池但是也并不是一开始就初始化好线程的。而是根据任务实际需求中不断的构建符合自身的线程池。那么构建线程依赖谁呢?上面也提到了官方推荐使用线程工厂。就是我们这里的ThreadFactory类。

  • 比如Executors.newFixedThreadPool是设置了固定的线程数量。那么当任务超过线程数和队列长度总和时,该怎么办?如果真的发生那种情况我们只能拒绝提供线程给任务使用。那么该如何拒绝这里就涉及到我们的RejectExecutionHandler

  • 点进源码我们可以看到默认的队列好像是LinkedBlockingQueue ; 这个队列是链表结构的怎么会有长度呢? 的确是但是Executors还给我们提供了很多扩展性。如果我们自定义的话我们能够发现还有其他的


image-20211215142858176.png


核心数与总线程数



  • 这里对应corePoolSizemaxiumPoolSize


 final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
         executorService.execute(new Runnable() {
             @Override
             public void run() {
                 System.out.println("我是线程1做的事情");
            }
        });


  • 我们已newFixedThreadPool来分析下。首先它需要一个整数型参数。


 public static ExecutorService newFixedThreadPool(int nThreads) {
         return new ThreadPoolExecutor(nThreads, nThreads,
                                       0L, TimeUnit.MILLISECONDS,
                                       new LinkedBlockingQueue<Runnable>());
    }


  • 而实际上内部是构建一个最大线程数量为10,且10个线程都是核心线程(公司核心员工);这10个线程是不会有过期时间一说的。过期时间针对非核心线程存活时间(公司外包员工)。

  • 当我们执行execute方法时。点进去看看我们发现


image-20211215145232205.png



  • 首先会判断当前任务数是否超过核心线程数,如果没有超过则会添加值核心线程队列中。注意这里并没有去获取是否有空闲线程。而是只要满足小于核心线程数,进来的任务都会优先分配线程。


image-20211215145509226.png



  • 但是当任务数处于(corePoolSize,maxiumPoolSize】之间时,线程池并没有立马创建非核心线程,这点我们从源码中可以求证。


image-20211215145810633.png



  • 这段代码时上面if 判断小于核心线程之后的if , 也就是如果任务数大于核心线程数。优先执行该if 分支。意思就是会将核心线程来不及处理的放在队列中,等待核心线程缓过来执行。像我们上面所说如果这个时候我们用的时有边界的队列的话,那么队列总有放满的时候。这个时候执行来到我们第三个if分支


image-20211215150107158.png



  • 这里还是先将任务添加到非核心队列中。false表示非核心。如果能添加进去说明还没有溢出非核心数。如果溢出了正好if添加就是false . 就会执行了拒绝策略。

  • 下面时executor执行源码


 int c = ctl.get();
         if (workerCountOf(c) < corePoolSize) {
             if (addWorker(command, true))
                 return;
             c = ctl.get();
        }
         if (isRunning(c) && workQueue.offer(command)) {
             int recheck = ctl.get();
             if (! isRunning(recheck) && remove(command))
                 reject(command);
             else if (workerCountOf(recheck) == 0)
                 addWorker(null, false);
        }
         else if (!addWorker(command, false))
             reject(command);
    }

思考



  • 基于上面我们对核心数和总数的讲述,我们来看看下面这段代码是否能够正确执行吧。


 public static void main(String[] args) throws InterruptedException {
         ThreadPoolExecutor executorService = new ThreadPoolExecutor(10,20,0,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
         for (int i = 0; i < 100; i++) {
             int finalI = i;
             executorService.execute(new Runnable() {
                 @SneakyThrows
                 @Override
                 public void run() {
                     System.out.println(finalI);
                     TimeUnit.SECONDS.sleep(1000);
                }
            });
        }
    }

image-20211215150903146.png



  • 很不幸,我们的执行报错了。而且出发了ThreadPoolExecutor中的拒绝策略。而且分析日志我们能够发现成功执行的有20个任务。分别是【0,9】+【20,29】这20个任务。

  • 拒绝我们很容易理解。因为我们设置了最大20个线程数加上长度为10的队列。所以该线程城同时最多只能支持30个任务的并发。另外因为我们每一个任务执行时间至少在1000秒以上,所以程序执行到第31个的时候其他都没有释放线程。没有空闲的线程给第31个任务所以直接拒绝了。

  • 那么为什么是是【0,9】+【20,29】呢?上面源码分析我们也提到了,进来的任务优先分配核心线程数,然后是缓存到队列中。当队列满了之后才会分配非核心数。当第31个来临直接出发拒绝策略,所以不管是核心线程还是非核心线程都没有时间处理队列中的10个线程。所以打印是跳着的。

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

奇怪,为什么ArrayList初始化容量大小为10?

背景 看ArrayList源码时,无意中看到ArrayList的初始化容量大小为10,这就奇怪了!我们都知道ArrayList和HashMap底层都是基于数组的,但为什么ArrayList不像用HashMap那样用16作为初始容量大小,而是采用10呢? 于是各...
继续阅读 »

背景


看ArrayList源码时,无意中看到ArrayList的初始化容量大小为10,这就奇怪了!我们都知道ArrayList和HashMap底层都是基于数组的,但为什么ArrayList不像用HashMap那样用16作为初始容量大小,而是采用10呢?


于是各方查找资料,求证了这个问题,这篇文章就给大家讲讲。


为什么HashMap的初始化容量为16?


在聊ArrayList的初始化容量时,要先来回顾一下HashMap的初始化容量。这里以Java 8源码为例,HashMap中的相关因素有两个:初始化容量及装载因子:


/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

在HashMap当中,数组的默认初始化容量为16,当数据填充到默认容量的0.75时,就会进行2倍扩容。当然,使用者也可以在初始化时传入指定大小。但需要注意的是,最好是2的n次方的数值,如果未设置为2的n次方,HashMap也会将其转化,反而多了一步操作。


关于HashMap的实现原理的内容,这里就不再赘述,网络上已经有太多文章讲这个了。有一点我们需要知道的是HashMap计算Key值坐标的算法,也就是通过对Key值进行Hash,进而映射到数组中的坐标。


此时,保证HashMap的容量是2的n次方,那么在hash运算时就可以采用位运行直接对内存进行操作,无需转换成十进制,效率会更高。


通常,可以认为,HashMap之所以采用2的n次方,同时默认值为16,有以下方面的考量:



  • 减少hash碰撞;

  • 提高Map查询效率;

  • 分配过小防止频繁扩容;

  • 分配过大浪费资源;


总之,HashMap之所以采用16作为默认值,是为了减少hash碰撞,同时提升效率。


ArrayList的初始化容量是10吗?


下面,先来确认一下ArrayList的初始化容量是不是10,然后在讨论为什么是这个值。


先来看看Java 8中,ArrayList初始化容量的源码:


/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;

很明显,默认的容器初始化值为10。而且从JDK1.2到JDK1.6,这个值也始终都为10。


从JDK1.7开始,在初始化ArrayList的时候,默认值初始化为空数组:


    /**
    * Shared empty array instance used for default sized empty instances. We
    * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
    * first element is added.
    */
  private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
   
  /**
    * Constructs an empty list with an initial capacity of ten.
    */
  public ArrayList() {
      this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  }

此处肯定有朋友说,Java 8中ArrayList默认初始化大小为0,不是10。而且还会发现构造方法上的注释有一些奇怪:构造一个初始容量10的空列表。什么鬼?明明是空的啊!


保留疑问,先来看一下ArrayList的add方法:


    public boolean add(E e) {
      ensureCapacityInternal(size + 1); // Increments modCount!!
      elementData[size++] = e;
      return true;
  }

在add方法中调用了ensureCapacityInternal方法,进入该方法一开始是一个空容器所以size=0传入的minCapacity=1


    private void ensureCapacityInternal(int minCapacity) {
      ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
  }
复制代码

上述方法中先通过calculateCapacity来计算容量:


    private static int calculateCapacity(Object[] elementData, int minCapacity) {
      if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
          return Math.max(DEFAULT_CAPACITY, minCapacity);
      }
      return minCapacity;
  }

会发现minCapacity被重新赋值为10 (DEFAULT_CAPACITY=10),传入ensureExplicitCapacity(minCapacity);minCapacity=10,下面是方法体:


    private void ensureExplicitCapacity(int minCapacity) {
      modCount++;

      // overflow-conscious code
      if (minCapacity - elementData.length > 0)
          grow(minCapacity);
  }
   
  private void grow(int minCapacity) {
      // overflow-conscious code
      int oldCapacity = elementData.length;
      int newCapacity = oldCapacity + (oldCapacity >> 1);
      if (newCapacity - minCapacity < 0)
          newCapacity = minCapacity;
      if (newCapacity - MAX_ARRAY_SIZE > 0)
          newCapacity = hugeCapacity(minCapacity);
      // minCapacity is usually close to size, so this is a win:
      elementData = Arrays.copyOf(elementData, newCapacity);
  }

上述代码中grow方法是用来处理扩容的,将容量扩容为原来的1.5倍。


了解上面的处理流程,我们会发现,本质上ArrayList的初始化容量还是10,只不过使用懒加载而已,这是Java 8为了节省内存而进行的优化而已。所以,自始至终,ArrayList的初始化容量都是10。


这里再多提一下懒加载的好处,当有成千上万的ArrayList存在程序当中,10个对象的默认大小意味着在创建时为底层数组分配10个指针(40 或80字节)并用空值填充它们,一个空数组(用空值填充)占用大量内存。如果能够延迟初始化数组,那么就能够节省大量的内存空间。Java 8的改动就是出于上述目的。


为什么ArrayList的初始化容量为10?


最后,我们来探讨一下为什么ArrayList的初始化容量为10。其实,可以说没有为什么,就是“感觉”10挺好的,不大不小,刚刚好,眼缘!


首先,在讨论HashMap的时候,我们说到HashMap之所以选择2的n次方,更多的是考虑到hash算法的性能与碰撞等问题。这个问题对于ArrayList的来说并不存在。ArrayList只是一个简单的增长阵列,不用考虑算法层面的优化。只要超过一定的值,进行增长即可。所以,理论上来讲ArrayList的容量是任何正值即可。


ArrayList的文档中并没有说明为什么选择10,但很大的可能是出于性能损失与空间损失之间的最佳匹配考量。10,不是很大,也不是很小,不会浪费太多的内存空间,也不会折损太多性能。


如果非要问当初到底为什么选择10,可能只有问问这段代码的作者“Josh Bloch”了吧。


如果你仔细观察,还会发现一些其他有意思的初始化容量数字:


ArrayList-10
Vector-10
HashSet-16
HashMap-16
HashTable-11

ArrayList与Vector初始化容量一样,为10;HashSet、HashMap初始化容量一样,为16;而HashTable独独使用11,又是一个很有意思的问题。


小结


有很多问题是没有明确原因、明确的答案的。就好像一个女孩儿对你没感觉,可能是因为你不够好,也可能是她已经爱上别人了,但也有很大可能你是不会知道答案。但在寻找原因和答案的过程中,还是能够学到很多,成长很多的。没有对比就没有伤害,比如HashMap与ArrayList的对比,没有对比就不知道是否适合,还比如HashMap与ArrayList。当然,你还可以试试特立独行的HashTable,或许适合你呢。


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

假如高考考编程

2046年的春天,编程全面纳入高考的第四年, 河北某三线城市,星期五下午5点半。王子明同学拿着手机,一脸沮丧的走在街上,时不时的有电动汽车和外卖机器人在他身边驶过,偶尔天空中划过几架直升机。“都是有钱人,跟我不是一个世界的”。王子明想着,他经过一家充电站,来到...
继续阅读 »

2046年的春天,编程全面纳入高考的第四年, 河北某三线城市,星期五下午5点半。

王子明同学拿着手机,一脸沮丧的走在街上,时不时的有电动汽车和外卖机器人在他身边驶过,偶尔天空中划过几架直升机。“都是有钱人,跟我不是一个世界的”。王子明想着,他经过一家充电站,来到杂货店,里面的美女机器人微笑着递给他一瓶可口可乐,还有一块口香糖,他拿起手机在美女的胸前一比划,“总价30元,谢谢光临,欢迎您下次再来。”机器人微笑着走了。“也就喝快乐水能让我稍微快乐一点。“


王子明为什么不高兴呢,因为一模成绩出来了,他的成绩非常不理想,其他科目还好,但是信息技术考砸了,150分的题目只拿到了85分,连及格线都没过。

“高考为什么要考计算机,为什么要考编程,我真羡慕我爸,他们高考根本不考算法!我真想回到10年代,那个时候我早就能上双一流了。“王子明恨恨的说。

王子明的确不擅长编程,Dijkstra,KMP怎么背都背不过,BFS和DFS经常写错边界条件,至于动态规划压轴题,他就从来就没做出来过。他也就只能靠前面那60分选择题拿点分,选择题考的都是计算机基础知识和基本操作,不涉及编程。“现在是高度信息化社会,计算机普及率99%,不会编程的人就是文盲!“ 他脑海里又浮现出计算机老师在课堂上的话。计算机老师是一个又高又瘦的中年妇女,非常严厉,他很怕她,每次走廊里见到她都绕着走。

王子明回到家里,只见家门口站着一个快递机器人,正在给妈妈搬东西,“子明快点,快帮我把这些菜放在冰箱里,肉放冷冻,菜放冷藏。”子明连忙过去帮忙。帮完忙,妈妈问道:“这次一模成绩怎么样,多少名啊?”王子明有点支支吾吾,不愿意说。“20名?”妈妈脸色沉了下来,王子明摇了摇头,“30名?”妈妈脸色更难看了。王子明断断续续说道:“4....2名”

"什么?!42名,你之前不都一直是前20的吗,这次怎么回事?"

“我计算机没考好,只有85分。”

“呵呵!天天都在家里打游戏,我就从来都没看你主动的刷过leetcode ! 你看隔壁那小崔,不仅刷leetcode,每天晚上还要打codeforces,打完还要补题,人家说了,我不把所有题目AC,就不睡觉。你呢,我就从来没见你说过这个话。”

“我刷leetcode,昨天刷了8道呢。”

“别骗我了,你把别人的题解复制粘贴也叫刷题?骗谁呢,自己骗自己有意思吗?高考能让你看别人的题解?你要是真的刷了,好,你现在就给我打开你昨天刷的题,当着我的面给我AC。AC不了,你就别吃饭!“

“妈妈我错了!“王子明快要哭出来了。

妈妈看到王子明这个样子,也有点心软,说道:“孩子,不是我逼你,我也希望你能高高兴兴的去玩耍,你现在还有三个月就要高考了,我们家没钱,你爸爸天天在公司加班拼死拼活一年连100万都挣不了,我们也不能让你去国外读书。你现在这个样子,怎么考好大学?考不上好大学,你就找不到好工作,找不到好工作,就没有女孩子愿意嫁给你。现在中国每3个男人就得有一个在打光棍,你愿意做那三分之一吗?“

“妈妈这些我都知道,但是编程太痛苦了,我讨厌算法,什么dijkstra,什么二分图,什么KMP,现实生活又用不到!“

“妈妈也知道你不喜欢,但是高考它就考啊,妈妈上学的时候也很讨厌数学,也要学一些根本用不到的圆锥曲线方程,三角函数什么的,你咬咬牙,背过它们,高考完就让它们滚一边去不就得了?“

“妈妈我......“

"别说了,要不我给你报个辅导班吧,计算机突击辅导班,周日下午两点去上课。"

“周日我要和小洋去踢球。“

“踢什么球!现在是踢球的时候吗?高考完了你踢到天黑我也不管,现在不行!“


饭后,子明闷闷不乐的回到屋里,打开leetcode,开始完成今天老师布置的题目。说起leetcode,据王子明认识的一个程序员爷爷说,这leetcode在他们年轻的时候就有了,当时的目的是总结一些程序员算法面试的题目。后来因为各大公司都在面试算法,leetcode越做越大,再后来,听闻中国高考要考算法,leetcode立刻推出了中国高考专用版,把总部迁到了北京,迅速统治了中国计算机教育市场,甚至还高价收购了《五年高考,三年模拟》,进军其他学科。目前是全国的中学生都在刷leetcode,老师们也在上面布置作业。

另外,每周的周赛也是全国乃至全球的一场盛会,几十万名用户在上面比赛,小明他们学校有个学长因为某次周赛拿了全球第7,被大家称为“七神”,全校闻名。

......

子明还在刷题,Wrong answer,Wrong answer,数不清的Wrong answer.......好不容易解决了这个wrong answer,又在下一个test case挂了,好不容易把这几个出错的case都解决,结果又变成了Time limit exceeded。“为什么就不能出现Accepted这个词?很难吗?”子明怒吼道。

当子明东拼西凑把最后一道题AC,已经凌晨三点了,他发现桌子旁边有一杯奶,不知道是什么时候妈妈给送过来的。奶旁边还有一张面膜和一个字条,字条写着:“喝完奶早点睡觉,不管你考多少名,你都是妈妈最爱的子明。”

子明躺在床上睡的很香,梦里他变成了一个天才,所有的算法题,只要他随便写点,交上去就通过。不知不觉就到了上午10点,他猛的一下子起来:“糟了,今天还要去补课呢,都迟到两个小时了。”这时候妈妈进来:“孩子不用补了,妈妈已经替你请假了。你昨晚睡的太晚,要好好休息一下。“

“谢谢妈妈!“子明松了一口气。

“你现在就是要全心全力把算法搞好,其他科目都可以放放,我昨天刚给你报了那个计算机辅导班,最后一个名额呢,被我抢到了。辅导班的杨老师非常厉害,而且很擅长一对一辅导。“

“哦!“

“那个辅导班的老师要求你这几天打一场编程比赛,然后把你的比赛记录以及代码发给他,他帮你分析一下。一会10点半是leetcode周赛,你吃完早饭去打一下。能进前1万名我请你吃火锅。“

“好的!“

然而子明并没有进前1万,甚至连前10万都没进,他只做出了一道easy难度的签到题。望着满眼的红色wrong answer,他非常沮丧。有一道BFS的题明明会做,但就是不对,也不知道错在哪里。毕竟,为了提升自己比赛成绩的含金量,防止有人hard code,leetcode平台这几年在比赛的时候不再告诉选手具体哪个test case错了,只会告诉错误类型。

妈妈看在眼里,也没说什么,去卧室给那个辅导老师打电话,“我们家子明可能让您费心了,他在编程方面完全不开窍。”他听到妈妈的声音,电话那边则是 “没问题没问题,这样的学生我见多了,你家孩子算不错的了,起码还做出来一道,没有我教不好的学生。”

周日的下午,他跟妈妈去了辅导班,进了教室,辅导老师正在给大家演示匈牙利算法的实现过程。“我们现在是月老,撮合的越多越好......" 子明听着听着,发现这个老师真的不一般,匈牙利算法讲的栩栩如生,要知道他学校的老师只会念ppt,而且ppt做的还不咋地,但这个老师讲完,他听懂了,而且有种想找一道题练练手的冲动。

“妈妈这个老师讲的真好!”妈妈说道:“那是必须的,这个老师可是知名教练,大学时候是ICPC全国金牌,我们小区那个全市冠军吴刚就是他教出来的。衡水中学开了1200万年薪挖他,他不去。要不是妈妈凌晨一点守在电脑前抢课,根本抢不到呢。

”下课他和妈妈来办公室找到老师,发现老师已经拿着一堆纸在等着他。子明一看,是他参加比赛提交的所有代码,老师已经打印出来了,上面有不少勾圈,显然是认真读过。老师第一句话就是:“这个比赛题目做不出来没关系,赛后分析才是关键。我看了你比赛的代码,比我想象的要好,你如果注意一下细节,这次比赛你就不是一题,而是三题了。

”这句话让子明不敢相信:“三题,要是三题的话我就是前1万名了。“

"你还记得你第二题死活都做不对,一直都是wrong answer吗?"

“是的“

“你把第37行i和j两个变量调过来试试看。

“子明拿出自己的电脑,一调换,提交,发现直接变成漂亮的Accepted了。

“哈哈,看来这道题我还是会做的,这就是低级错误“

老师一脸严肃:“住嘴,什么低级错误,低级错误也是错误!高考犯低级错误,照样没有分数。”

子明沉默了,老师继续说道:“你的代码命名非常不规范,i,j,k横行,而且整体局部不分,前面刚给一个全局变量赋值为 i,后面又把 i 赋值给另一个局部变量。这样的话很容易出错。“

“可是老师,我每一行都仔细检查的,出错的概率很低的。“

“那好,假设你每行出错的概率是1%,很低了吗,那30行出错的概率是多少?“

“26%。“子明拿出手机快速计算了一下。“

对啊,你每个程序写30行,就有26%的概率出错,100分就变成74分了。你高考丢26分,足以让你跟重点大学失之交臂。马虎是不可避免的,你如果能把出错率降到0.1%,那一道题出错的概率就是3%,你就能提高23分,如果你能把你的代码写规范,那足以降到0.1%。注意,所有的马虎都是有因可循,一定要找深层原因,而不是把它仅仅归结于马虎!

“子明默不作声,这些他老师从来都没跟他说过,老师只会说:“马虎啊,那下次注意不要马虎不就行了吗?”但他还是会马虎

“代码规范真的很重要,你知道衡水中学的学生怎么练的吗?他们全校写代码用的都是一个规范,学校自己搞了一个做题系统,学生们在这个系统上做题的时候,如果代码不符合规范,会被直接判错,就算是写对了也没用,因此所有学生写的代码都一个样子。当然我不能要求你这样,但是你一定要留心。

“杨老师喝了一口水,又看了一下子明的代码,继续说道:“然后第三题,我看你已经判断出这道题需要用dijkstra算法了,但是你为什么只写了一个def dijkstra然后就没了?“

“我忘了怎么写了。“

“这个方法其实不难记的,就是建造一个堆,然后每次把权重最小的边抽出来更新,我有一个专门讲这个方法的视频,你把接收器给我,我传给你,你回去好好看看,里面有专门的口诀。“

“好的,谢谢老师!“

“你这两道题都是经过努力可以做出来的,只要做出来,你的成绩就会有飞跃。这些是我一些辅导材料,你回去好好研究一下,以后每周要来定时上课哦。“

子明说完,他妈妈马上跟老师说:“杨老师你看,能不能给我孩子单独开个小灶啊。”“可以,不过这个是另收钱的,每小时5000。““没问题,这个老师您看您什么时候有时间啊。”“这是我的日程表,子明你来看一下,从这个白色区域选两个小时”.......

回家的路上,子明埋怨妈妈:“妈你这是干什么啊,花这么多钱,两个小时都顶我一个月生活费了,咱家又没那么多钱。”“再穷也不能穷教育啊,你考上好大学,能给我省好几千万呢,再说你打网游充的钱也有好几万了,你少充点不就补回来了?”

子明不说话,回去之后开始拿出老师给的资料慢慢看,妈妈则在厨房忙活,给儿子做他最爱吃的红烧牛肉。爸爸每天在公司加班,平时就很晚回家,最近为了不影响儿子备战高考,干脆就住在公司了。“儿子,考上双一流,爸爸带你去你最想去的环球影城。”爸爸和儿子视频聊天,“爸爸,我们班明明她爸爸要带她去月球呢。”“去月球要20亿呢,我一辈子也挣不到这个数啊。”“爸爸我开玩笑呢,明明她爸爸是大公司老总,咱家能跟她家比嘛。”“哈哈哈哈,吓死爸爸了!等你有了出息,还想让你带我去呢。”......

过了一周,子明准时出现在杨老师家里。杨老师很热情招待了他,然后对他进行了辅导。

“这个信息技术高考在我上学的时候并没有,是10年前刚刚加上的。一开始满分是100,而且只在部分发达省份考,后来因为人口老龄化加剧,青年信息人才越来越紧缺,2043年开始上升到了150分,并推广到了全国,统一上机考试,时间两个半小时。

首先是60分的选择题,这个考的就是计算机基础知识,什么windows操作啊,什么进制转换呢,我相信你应该没什么问题,这些题目一定要快,必须1分钟就要一道,30分钟内解决战斗。然后就是6道编程题了,每道15分。

但是高考和比赛的区别是,高考在中间不会告诉你做的对不对,只有考试结束之后才会判分,所以你120分钟做完,和150分钟做完,结果是一样的。这就要求你必须非常仔细,一遍就要做对。不要指望着让OJ帮你调BUG。不过呢,就算你写的不对也没关系,test case和test case之间是相互独立的,你通过了这个test case就给这个分,全通过了就是满分。

另外,高考是不限语言的,但是所有语言的time limit都一样,你用python当然OK,因为运算时间很充裕,复杂度只要正确就行,但是呢,如果你用C++可能会有额外的好处,衡水中学在入学时就强制所有学生都用C++。我记得2044年上海的题目,出题者本来是想要大家用O(n log n)算法的,但是限时给的太长了,如果你用C++再加上一些优化,O(n^2)也能拿满分,最后好多人暴力过了。判卷组本来想缩短时限重判的,结果因为大家已经知道自己成绩,社会反对声浪太大,只好作罢,你看,用c++白捡了10分,还省了大量时间。所以如果你现在上高二,我会建议你改C++,但是你是高三,所以你可以选择不改。

前两道题,就是一些数组,字符串的题目,考察最基本的前缀,后缀,二分,双指针,哈希表之类的,这些题目,是送分题,一分都不能丢的,而且要10分钟一道。

中间两道题,一般是二维数组或者图之类的,需要用到各种搜索,BFS,DFS,或者一些图论基础知识,比如union find,dijkstra之类的,这些题目,如果你想要上双一流,也是必须要拿下的,要做到20分钟一道。

最后两道题要花一个小时完成,第五题容易出奇葩的题目,或者是几个知识点综合起来的综合题,或者需要用到线段树进行优化,甚至可能会出几何和数论。而第六题,就是臭名昭著的动态规划了。这两道题,以你目前的水平,是拿不下的,但是你可以从里面抢分,千万不要空着。注意,你哪怕用最暴力的方法,也能拿到大约1/3的分数,前面如果再不扣分。你就有130了,足够你上双一流的。不过你要是想上清北华五,那就要至少140分,也就是选择题全对,然后最多有一道题只会做small test case,剩下的都要满分。

而且,动态规划,千万不要畏惧,虽然千变万化,其实也可以分成几大类的,背包问题,树形DP,博弈论.....你现在水平不高,你只要记住,动态规划,就是记忆化的递归,你只要往这方面想,绝大部分题目你都是可以解决的,至少可以拿到大部分分数。

虽然题型分布一般是这样,但是高考也有不按套路出牌的时候,比如去年最后一题是贪心+最小堆,而动态规划放到了第三题的位置,难倒了一大片,还有我记得前年某个省,最后一题是概率题,需要用到排列组合,而排列组合需要存储中间结果,取模需要用到数论知识,否则大数据会超时...... 虽然中国剩余定理还有欧拉定理什么的是超纲的,会在考试的时候给你写出来,但是掌握了没亏吃,毕竟时间是最宝贵的.....

最后想说,学习算法,最关键的还是多练习,尤其是练习自己的薄弱环节,刷自己的强项题固然很爽,但是没什么效果。衡水中学的学生,三年下来要做好几千道题,他们每周要搞两次编程比赛,全校大排名,每个班的最高名次和平均名次都会算到教师绩效里。而且,他们搞出来的那个系统,不仅强制学生把代码写规范,还能通过每个学生的答题情况来分析他们的弱点,专门给每个学生出他们大概率做错的题目,比如一个学生binary search已经炉火纯青,但是DFS经常写错,那么系统就会大概率给他出DFS相关题目,很少出binary search。要不说那边学生平时用那个系统做题非常痛苦,但最后高考成绩都很高呢!虽然这些他们老师在课上也说过,但是子明还是听的津津有味。后来老师又给他辅导了几道错题,扩展了很多知识点,两个小时很快过去了,老师把把子明送走,说:“现在努力绝对来得及,千万不要对自己失去信心!只要听我的,把我给你布置的题目刷透,你高考上130没问题。”

在回家的路上,子明充满了斗志,说道:“不就是区区算法嘛,有什么好怕的,来啊高考!看我6月把你打的落花流水!”

这个时候子明的手机响了,是他的朋友小洋:“小明啊,下周日上午我们一起踢球怎么样,咱们初中同学长庚也加入呢。”

子明又有点心痒痒了,想偷偷去,不告诉妈妈,后来觉得有点不好,就在电话里跟妈妈说了,没想到妈妈爽快答应了“你既然跟我说了,说明你现在还是知道分寸的,踢球没什么不好,可以放松你的大脑,你这次可以去,但是你要用一次周赛前1万名的成绩来回报我。”“好!”子明高兴的说道。

夕阳下,一个少年坚定地前行着。

来源:www.zhihu.com/question/50360847/answer/1894183447
作者:Super Mario

收起阅读 »

Java中BufferedReader、BufferedWriter用法

FileWriter/FileReader 介绍 FileWriter 类从 OutputStreamWriter 类继承而来。该类按字符向流中写入数据。 构造 参数为 File 对象 FileWriter(File file) 参数是文件的路径及文件名(默认...
继续阅读 »

FileWriter/FileReader


介绍

FileWriter 类从 OutputStreamWriter 类继承而来。该类按字符向流中写入数据。


构造

参数为 File 对象


FileWriter(File file)

参数是文件的路径及文件名(默认是当前执行文件的路径)


FileWrite(String filename)

等价于


OutputStreamWriter out = new OutputStreamWriter(
new FileOutputStream(File file))

方法























序号方法描述
1public void write(int c) throws IOException 写入单个字符c。
2public void write(char [] c, int offset, int len) 写入字符数组中开始为offset长度为len的某一部分。
3public void write(String s, int offset, int len) 写入字符串中开始为offset长度为len的某一部分。

栗子


public class Main {
public static void main(String[] args) throws Exception {
File file = new File("d:/abc/f10");
// 创建文件
file.createNewFile();
// creates a FileWriter Object
FileWriter writer = new FileWriter(file);
// 向文件写入内容
writer.write("This\n is\n an\n example\n");
writer.flush();
writer.close();
// 创建 FileReader 对象
FileReader fr = new FileReader(file);
char[] a = new char[50];
fr.read(a); // 从数组中读取内容
for (char c : a)
System.out.print(c); // 一个个打印字符
fr.close();
}
}

运行程序会在 D 盘 abc 文件夹下创建 f10,同时打印内容如下

在这里插入图片描述


BufferedReader/BufferedWriter


介绍

BufferedReader 类从字符输入流中读取文本并缓冲字符,以便有效地读取字符,数组和行。


可以通过构造函数指定缓冲区大小也可以使用默认大小。对于大多数用途,默认值足够大。


由 Reader 构成的每个读取请求都会导致相应的读取请求由基础字符或字节流构成,建议通过 BufferedReader 包装 Reader 的实例类以提高效率。(Reader 构成的对象是字符对象,每次的读取请求都会涉及到字节读取解码字符的过程,而 BufferedReader 类中有设计减少这样的解码次数的方法,进而提高转换效率)


创建对象


BufferedReader in  = new BufferedReader(new FileReader(“foo.in”));

方法

BufferedReader 由 Reader 类扩展而来,提供通用的缓冲方式文本读取,而且提供了很实用的readLine(),读取一个文本行,从字符输入流中读取文本,缓冲各个字符,从而提供字符、数组和行的高效读取。


readLine()读取一行字符串,不含末尾换行符,读取结束再读取返回 null。


栗子1:写入


BufferedWriter bufw = new BufferedWriter(new FileWriter("d:/abc/f11"));
bufw.write("This");
bufw.newLine();
bufw.newLine();
bufw.write("is");
bufw.write("an");
bufw.write("example");
//使用缓冲区中的方法,将数据刷新到目的地文件中去。
bufw.flush();
//关闭缓冲区,同时关闭了fw流对象
bufw.close();

运行结果会在 D 盘 abc 文件夹下新建 f11 文件


栗子2:读取


//相接的字符流,只要读取字符,都要做编码转换
//只要使用字符流,必须要有转换流
BufferedReader in = new BufferedReader(
new InputStreamReader(
new FileInputStream("d:/abc/f11")));

String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
in.close();

运行结果

在这里插入图片描述


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

如何优雅地消除复杂条件表达式

在复杂的实际业务中,往往会出现各种嵌套的条件判断逻辑。我们需要考虑所有可能的情况。随着需求的增加,条件逻辑会变得越来越复杂,判断函数会变的相当长,而且也不能轻易修改这些代码。每次改需求的时候,都要保证所有分支逻辑判断的情况都改了。 面对这种情况,简化判断逻辑就...
继续阅读 »

在复杂的实际业务中,往往会出现各种嵌套的条件判断逻辑。我们需要考虑所有可能的情况。随着需求的增加,条件逻辑会变得越来越复杂,判断函数会变的相当长,而且也不能轻易修改这些代码。每次改需求的时候,都要保证所有分支逻辑判断的情况都改了。


面对这种情况,简化判断逻辑就是不得不做的事情,下面介绍几种方法。


一个实际例子


@GetMapping("/exportOrderRecords")
public void downloadFile(User user, HttpServletResponse response) {
if (user != null) {
if (!StringUtils.isBlank(user.role) && authenticate(user.role)) {
String fileType = user.getFileType(); // 获得文件类型
if (!StringUtils.isBlank(fileType)) {
if (fileType.equalsIgnoreCase("csv")) {
doDownloadCsv(); // 不同类型文件的下载策略
} else if (fileType.equalsIgnoreCase("excel")) {
doDownloadExcel(); // 不同类型文件的下载策略
} else {
doDownloadTxt(); // 不同类型文件的下载策略
}
} else {
doDownloadCsv();
}
}
}
}

public class User {
private String username;
private String role;
private String fileType;
}

上面的例子是一个文件下载功能。我们根据用户需要下载Excel、CSV或TXT文件。下载之前需要做一些合法性判断,比如验证用户权限,验证请求文件的格式。


使用断言


在上面的例子中,有四层嵌套。但是最外层的两层嵌套是为了验证参数的有效性。只有条件为真时,代码才能正常运行。可以使用断言Assert.isTrue()。如果断言不为真的时候抛出RuntimeException。(注意要注明会抛出异常,kotlin中也一样)


@GetMapping("/exportOrderRecords")
public void downloadFile(User user, HttpServletResponse response) throws Exception {
Assert.isTrue(user != null, "the request body is required!");
Assert.isTrue(StringUtils.isNotBlank(user.getRole()), "download file is for");
Assert.isTrue(authenticate(user.getRole()), "you do not have permission to download files");

String fileType = user.getFileType();
if (!StringUtils.isBlank(fileType)) {
if (fileType.equalsIgnoreCase("csv")) {
doDownloadCsv();
} else if (fileType.equalsIgnoreCase("excel")) {
doDownloadExcel();
} else {
doDownloadTxt();
}
} else {
doDownloadCsv();
}
}

可以看出在使用断言之后,代码的可读性更高了。代码可以分成两部分,一部分是参数校验逻辑,另一部分是文件下载功能。


表驱动


断言可以优化一些条件表达式,但还不够好。我们仍然需要通过判断filetype属性来确定要下载的文件格式。假设现在需求有变化,需要支持word格式文件的下载,那我们就需要直接改这块的代码,实际上违反了开闭原则。


表驱动可以解决这个问题。


private HashMap<String, Consumer> map = new HashMap<>();

public Demo() {
map.put("csv", response -> doDownloadCsv());
map.put("excel", response -> doDownloadExcel());
map.put("txt", response -> doDownloadTxt());
}

@GetMapping("/exportOrderRecords")
public void downloadFile(User user, HttpServletResponse response) {
Assert.isTrue(user != null, "the request body is required!");
Assert.isTrue(StringUtils.isNotBlank(user.getRole()), "download file is for");
Assert.isTrue(authenticate(user.getRole()), "you do not have permission to download files");

String fileType = user.getFileType();
Consumer consumer = map.get(fileType);
if (consumer != null) {
consumer.accept(response);
} else {
doDownloadCsv();
}
}

可以看出在使用了表驱动之后,如果想要新增类型,只需要在map中新增一个key-value就可以了。


使用枚举


除了表驱动,我们还可以使用枚举来优化条件表达式,将各种逻辑封装在具体的枚举实例中。这同样可以提高代码的可扩展性。其实Enum本质上就是一种表驱动的实现。(kotlin中可以使用sealed class处理这个问题,只不过具实现方法不太一样)


public enum FileType {
EXCEL(".xlsx") {
@Override
public void download() {
}
},

CSV(".csv") {
@Override
public void download() {
}
},

TXT(".txt") {
@Override
public void download() {
}
};

private String suffix;

FileType(String suffix) {
this.suffix = suffix;
}

public String getSuffix() {
return suffix;
}

public abstract void download();
}

@GetMapping("/exportOrderRecords")
public void downloadFile(User user, HttpServletResponse response) {
Assert.isTrue(user != null, "the request body is required!");
Assert.isTrue(StringUtils.isNotBlank(user.getRole()), "download file is for");
Assert.isTrue(authenticate(user.getRole()), "you do not have permission to download files");

String fileType = user.getFileType();
FileType type = FileType.valueOf(fileType);
if (type!=null) {
type.download();
} else {
FileType.CSV.download();
}
}

策略模式


我们还可以使用策略模式来简化条件表达式,将不同文件格式的下载处理抽象成不同的策略类。


public interface FileDownload{
boolean support(String fileType);
void download(String fileType);
}

public class CsvFileDownload implements FileDownload{

@Override
public boolean support(String fileType) {
return "CSV".equalsIgnoreCase(fileType);
}

@Override
public void download(String fileType) {
if (!support(fileType)) return;
// do something
}
}

public class ExcelFileDownload implements FileDownload {

@Override
public boolean support(String fileType) {
return "EXCEL".equalsIgnoreCase(fileType);
}

@Override
public void download(String fileType) {
if (!support(fileType)) return;
//do something
}
}

@Autowired
private List<FileDownload> fileDownloads;

@GetMapping("/exportOrderRecords")
public void downloadFile(User user, HttpServletResponse response) {
Assert.isTrue(user != null, "the request body is required!");
Assert.isTrue(StringUtils.isNotBlank(user.getRole()), "download file is for");
Assert.isTrue(authenticate(user.getRole()), "you do not have permission to download files");

String fileType = user.getFileType();
for (FileDownload fileDownload : fileDownloads) {
fileDownload.download(fileType);
}
}

策略模式对提高代码可扩展性很有帮助。扩展新的类型只需要添加一个策略类


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

由浅入深 Android 混淆实战

许久没有做混淆相关的工作, 以前存储的知识遗忘得差不多了。停留在很多人的记忆里面混淆还不简单吗?不就是 -keep。这样说也没错,但是要把混淆做得精细精准还是不简单的,今天就一文带你全而透。 混淆的作用 我们为什么要做这个工作,有什么好处? 代码缩减(摇树...
继续阅读 »

许久没有做混淆相关的工作, 以前存储的知识遗忘得差不多了。停留在很多人的记忆里面混淆还不简单吗?不就是 -keep。这样说也没错,但是要把混淆做得精细精准还是不简单的,今天就一文带你全而透。


混淆的作用


我们为什么要做这个工作,有什么好处?




  • 代码缩减(摇树优化):使用静态代码分析来查找和删除无法访问的代码和未实例化的类型,对规避 64k 引用限制非常有用;




  • 资源缩减:移除不使用的资源,包括应用库依赖项中不使用的资源。




  • 混淆代码:缩短类和成员的名称,从而减小 DEX 文件的大小,增加反编译成本。




  • 优化代码:检查并重写代码,选择性内联,移除未使用的参数和类合并来优化代码大小。




  • 减少调试信息 : 规范化调试信息并压缩行号信息。




混淆的情况


混淆的情况是指你接手混淆时候的状况,大致分两种。



  • 一种是项目刚刚立项,这个时候你跟进混淆,随着你的代码量增多,和引入的第三方库&SDK 增多逐渐增加混淆规则,这是一个应该有的良性的状态,做到精准混淆也容易。

  • 第二种情况是以前的维护者完全没有混淆,有海量的代码和第三方库,里面的反射注解和各种存在混淆风险的问题存在,这样想做到精准混淆并不容易


上文多次提到精准混淆,我理解的精准混淆是最细粒度的白名单,而不是如下反例:


-keep public class * extends java.lang.Object{*;}

混淆基础知识储备


开启和关闭混淆


开启混淆比较简单,一般来讲为了方便开发调试只混淆 release 版本:


buildTypes {
release {
shrinkResources true //开启资源压缩
minifyEnabled true //开启混淆
zipAlignEnabled true //k对齐
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

minifyEnabled 和 proguardFiles 是必选项其他为可选,关闭混淆的话就比较容易了直接 minifyEnabled 修饰为 false 即可。


proguard-android.txt 和 proguard-android-optimize.txt


我们经常在代码里面看到这样的语句:


image.png
proguard-rules.pro 我们知道就在 app/ 目录下,但是这个 getDefaultProguardFile 是什么?在哪里?有什么用?


getDefaultProguardFile 是 Android SDK 为我们提供的一些 Android 内置的混淆规则,一般来将这些是通用的,你要做到精通混淆必选知道它的位置以及他里面包含的内容和含义。


位置:android/sdk/tools/proguard/


image.png


# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# This file is no longer maintained and is not used by new (2.2+) versions of the
# Android plugin for Gradle. Instead, the Android plugin for Gradle generates the
# default rules at build time and stores them in the build directory.

-dontusemixedcaseclassnames #混淆时不会产生形形色色的类名
-dontskipnonpubliclibraryclasses #指定不去忽略非公共类库
-verbose #输出生成信息

# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
#-dontoptimize #不优化指定文件
-dontpreverify #不预校验
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.

-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}

# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}

-keepclassmembers class **.R$* {
public static <fields>;
}

# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**

# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep

-keep @android.support.annotation.Keep class * {*;}

-keepclasseswithmembers class * {
@android.support.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
@android.support.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
@android.support.annotation.Keep <init>(...);
}

mapping 文件


image.png


Mapping 非常重要,在 app/build/mapping 中生成的 mapping 文件是我们分析混淆是否生效,混淆后的崩溃寻因的重要依据,通过这个文件的映射我们能够在一堆杂乱无章的 a、 b、 c 中回溯到原始代码。例:


image.png


工具集


工欲善其事必先利其器,两款对混淆有着很大帮助的工具介绍


Android Studio APK Analysis


AS自带简单好用,对比包体积的占比分析也是相当不错,并且随着 AS 的迭代有着官方的支持相信功能会越来越强大,我们只需要简单的将 apk 包拖拽到 AS 中就会自动触发 AS 的 apk 解析功能:


image.png


Jadx


Jadx 的强大之处在于相较于 AS 自带的分析器,它还能直接查看源代码,AS 只能看到类名和方法名,简直是逆向神器。


image.png


更多介绍请移步 github.com/skylot/jadx


混淆实战


通过 demo 样例的混淆实战深刻理解每个混淆规则的含义,我们要做到的不是仅仅开启 minifyEnabled 然后应用通过,而是需要知到得更细更透彻,理解每个混淆语句规则的作用范围。


先定义一下基准包以及子包,还有类、内部类、接口、注解、方法、成员,然后我们分部对其进行混淆和 -keep 保持,记住下图的 proguard 开始的包类目录关系,我们后面一直要使用它。


image.png


后续的文章都会以这几个类做样例,所以我们把它罗列出来再加深一下印象:



  • User.java

  • Animal.java

  • MethodLevel.java

  • Student.java

  • Teacher.java

  • Company.java

  • IBehaviour.java


部分样例类:


public class Teacher extends User implements IBehaviour {

@Override
public void doSomething() {
System.out.println("teaching ...");
}

@MethodLevel(value = 1)
private void waking(){

}
}

混淆中代码调用关系


先开启混淆,不添加任何规则。我们通过 jadx 看下混淆情况


image.png
proguard 包和类一个都找不到应该都是被混淆了,进一步印证一下我们的想法,我们去 mapping 文件里面找下映射关系,结果出乎意料,我没有在 mapping 中找到映射关系,只在 usage.txt 中找到了对应的全路径名


image.png


是不是我们的类申明了没有引用导致的呢?我们去 activity 做一下调用


image.png


果然和我们的预想的一样,如果类创建了没有使用,mapping 不会生成映射关系,甚至可能在打包的过程中给优化掉,再看加上调用关系后我们查询 mapping 文件:


image.png


image.png


上图可以得知,我们的 proguard 包和下面的所有内容全部都混淆了。


keep 探寻


网络上的大部分文章都千篇一律,简单的给出了一个 Keep 语句,实际用的时候都是 copy 对其作用范围不是很明确,接下来我们就一个一个来探寻


keep *


-keep class com.zxmdly.proguard.*

我们先加上这句看看打包后的变化


对比之前的结果,我们看到的是 proguard 的包和下面的类名被保留下来了,注意仅仅是包合类名被保留,类中的字段和成员是没有找到的,这是为什么呢?难道是字段没有被使用


image.png


image.png


我们去印证下


image.png


image.png


好了,到现在我们已经能够透彻的知道了 -keep * 的作用,总结作用范围:




  • 能够保持该包和该包下的类、和静态内部类的类名保持,对字段和方法不额外起作用,子包不起作用,字段或者方法没有被调用则直接忽略。




keep **


-keep class com.zxmdly.proguard.**

image.png


通过查看上图和上面 keep * 的经验,我们可以得出结论:



  • keep ** 能够保持该包和其子包的子类的所有类名(含注解、枚举)不被混淆,但是方法和字段不在其作用范围,字段或者方法没有被调用则直接忽略。


值得注意的是, keep ** 对于 keep * 是包含关系,声明了 keep ** 混淆规则就无需再声明 keep * 了。


keep ** {*;}


-keep class com.zxmdly.proguard.* {}

image.png
有了之前的经验,我们可以得出结论:



  • keep ** {*;} 的作用范围特别大,能够保持该包及其子包、子类和其字段方法都不被混淆,相对来讲我们需要慎用这样的语句,因为可能导致混淆不够精准。


单个类名保持


-keep class com.zxmdly.proguard.Company

image.png



  • 仅保持类名,字段和成员被混淆


保持方法


-keep class com.zxmdly.proguard.Company{
<methods>;
}

image.png


保持字段


-keep class com.zxmdly.proguard.Company{
<fields>;
}

image.png


实现关系保持


-keep public class * implements com.zxmdly.proguard.IBehaviour

image.png


-keep public class * implements com.zxmdly.proguard.IBehaviour {*;}

image.png


继承关系保持


-keep public class * extends com.zxmdly.proguard.base.User {*;}

image.png


指定保持具体方法或者字段


-keep class com.zxmdly.proguard.Company{
public java.lang.String address;
public void printlnAddress();
}

image.png


Tips 小技巧


在 gralde 中,我们可以通过下面配置将我们的混淆规则分门别类,指定多个混淆配置文件。


image.png


例如给第三方的 SDK 专门放到一个 Third 混淆配置文件,使用了这个小技巧加上注释,我们的混淆规则是不是更清晰了呢


结语


通过本文由浅入深的带大家进行混淆实战,相信 99% 的精准混淆工作已经难不倒你,当然混淆还有更深入和更细节的用法,篇幅关系我们下次再探。


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

Android动态加载so!这一篇就够了!

背景 对于一个普通的android应用来说,so库的占比通常都是巨高不下的,因为我们无可避免的在开发中遇到各种各样需要用到native的需求,所以so库的动态化可以减少极大的包体积,自从2020腾讯的bugly团队发部关于动态化so的相关文章后,已经过去两年了...
继续阅读 »

背景


对于一个普通的android应用来说,so库的占比通常都是巨高不下的,因为我们无可避免的在开发中遇到各种各样需要用到native的需求,所以so库的动态化可以减少极大的包体积,自从2020腾讯的bugly团队发部关于动态化so的相关文章后,已经过去两年了,相关文章,经过两年的考验,实际上so动态加载也是非常成熟的一项技术了,但是很遗憾,许多公司都还没有这方面的涉略又或者说不知道从哪里开始进行,因为so动态其实涉及到下载,so版本管理,动态加载实现等多方面,我们不妨抛开这些额外的东西,从最本质的so动态加载出发吧!这里是本次的例子,我把它命名为sillyboy,欢迎pr还有后续点赞呀!


so动态加载介绍


动态加载,其实就是把我们的so库在打包成apk的时候剔除,在合适的时候通过网络包下载的方式,通过一些手段,在运行的时候进行分离加载的过程。这里涉及到下载器,还有下载后的版本管理等等确保一个so库被正确的加载等过程,在这里,我们不讨论这些辅助的流程,我们看下怎么实现一个最简单的加载流程。


image.png


从一个例子出发


我们构建一个native工程,然后在里面编入如下内容,下面是cmake


# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.18.1)

# Declares and names the project.

project("nativecpp")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
nativecpp

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
native-lib.cpp)

add_library(
nativecpptwo
SHARED
test.cpp

)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
nativecpp

# Links the target library to the log library
# included in the NDK.
${log-lib})


target_link_libraries( # Specifies the target library.
nativecpptwo

# Links the target library to the log library
# included in the NDK.
nativecpp
${log-lib})

可以看到,我们生成了两个so库一个是nativecpp,还有一个是nativecpptwo(为什么要两个呢?我们可以继续看下文)
这里也给出最关键的test.cpp代码




#include <jni.h>
#include <string>
#include<android/log.h>


extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativecpp_MainActivity_clickTest(JNIEnv *env, jobject thiz) {
// 在这里打印一句话
__android_log_print(ANDROID_LOG_INFO,"hello"," native 层方法");

}

很简单,就一个native方法,打印一个log即可,我们就可以在java/kotin层进行方法调用了,即


public native void clickTest();

so库检索与删除


要实现so的动态加载,那最起码是要知道本项目过程中涉及到哪些so吧!不用担心,我们gradle构建的时候,就已经提供了相应的构建过程,即构建的task【
mergeDebugNativeLibs】,在这个过程中,会把一个project里面的所有native库进行一个收集的过程,紧接着task【stripDebugDebugSymbols】是一个符号表清除过程,如果了解native开发的朋友很容易就知道,这就是一个减少so体积的一个过程,我们不在这里详述。所以我们很容易想到,我们只要在这两个task中插入一个自定义的task,用于遍历和删除就可以实现so的删除化了,所以就很容易写出这样的代码



ext {
deleteSoName = ["libnativecpptwo.so","libnativecpp.so"]
}
// 这个是初始化 -配置 -执行阶段中,配置阶段执行的任务之一,完成afterEvaluate就可以得到所有的tasks,从而可以在里面插入我们定制化的数据
task(dynamicSo) {
}.doLast {
println("dynamicSo insert!!!! ")
//projectDir 在哪个project下面,projectDir就是哪个路径
print(getRootProject().findAll())

def file = new File("${projectDir}/build/intermediates/merged_native_libs/debug/out/lib")
//默认删除所有的so库
if (file.exists()) {
file.listFiles().each {
if (it.isDirectory()) {
it.listFiles().each {
target ->
print("file ${target.name}")
def compareName = target.name
deleteSoName.each {
if (compareName.contains(it)) {
target.delete()
}
}
}
}
}
} else {
print("nil")
}
}
afterEvaluate {
print("dynamicSo task start")
def customer = tasks.findByName("dynamicSo")
def merge = tasks.findByName("mergeDebugNativeLibs")
def strip = tasks.findByName("stripDebugDebugSymbols")
if (merge != null || strip != null) {
customer.mustRunAfter(merge)
strip.dependsOn(customer)
}

}

可以看到,我们定义了一个自定义task dynamicSo,它的执行是在afterEvaluate中定义的,并且依赖于mergeDebugNativeLibs,而stripDebugDebugSymbols就依赖于我们生成的dynamicSo,达到了一个插入操作。那么为什么要在afterEvaluate中执行呢?那是因为android插件是在配置阶段中才生成的mergeDebugNativeLibs等任务,原本的gradle构建是不存在这样一个任务的,所以我们才需要在配置完所有task之后,才进行的插入,我们可以看一下gradle的生命周期


image.png


通过对条件检索,我们就删除掉了我们想要的so,即ibnativecpptwo.so与libnativecpp.so。


动态加载so


根据上文检索出来的两个so,我们就可以在项目中上传到自己的后端中,然后通过网络下载到用户的手机上,这里我们就演示一下即可,我们就直接放在data目录下面吧


image.png
真实的项目过程中,应该要有校验操作,比如md5校验或者可以解压等等操作,这里不是重点,我们就直接略过啦!


那么,怎么把一个so库加载到我们本来的apk中呢?这里是so原本的加载过程,可以看到,系统是通过classloader检索native目录是否存在so库进行加载的,那我们反射一下,把我们自定义的path加入进行不就可以了吗?这里采用tinker一样的思路,在我们的classloader中加入so的检索路径即可,比如


private static final class V25 {
private static void install(ClassLoader classLoader, File folder) throws Throwable {
final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);

final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");

List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
if (origLibDirs == null) {
origLibDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = origLibDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
break;
}
}
origLibDirs.add(0, folder);

final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
if (origSystemLibDirs == null) {
origSystemLibDirs = new ArrayList<>(2);
}

final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);

final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);

final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);

final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
}
}

我们在原本的检索路径中,在最前面,即数组为0的位置加入了我们的检索路径,这样一来claaloader在查找我们已经动态化的so库的时候,就能够找到!


结束了吗?


一般的so库,比如不依赖其他的so的时候,直接这样加载就没问题了,但是如果存在着依赖的so库的话,就不行了!相信大家在看其他的博客的时候就能看到,是因为Namespace的问题。具体是我们动态库加载的过程中,如果需要依赖其他的动态库,那么就需要一个链接的过程对吧!这里的实现就是Linker,Linker 里检索的路径在创建 ClassLoader 实例后就被系统通过 Namespace 机制绑定了,当我们注入新的路径之后,虽然 ClassLoader 里的路径增加了,但是 Linker 里 Namespace 已经绑定的路径集合并没有同步更新,所以出现了 libxxx.so 文件(当前的so)能找到,而依赖的so 找不到的情况。bugly文章


很多实现都采用了Tinker的实现,既然我们系统的classloader是这样,那么我们在合适的时候把这个替换掉不就可以了嘛!当然bugly团队就是这样做的,但是笔者认为,替换一个classloader显然对于一个普通应用来说,成本还是太大了,而且兼容性风险也挺高的,当然,还有很多方式,比如采用Relinker这个库自定义我们加载的逻辑。


为了不冷饭热炒,嘿嘿,虽然我也喜欢吃炒饭(手动狗头),这里我们就不采用替换classloader的方式,而是采用跟relinker的思想,去进行加载!具体的可以看到sillyboy的实现,其实就不依赖relinker跟tinker,因为我把关键的拷贝过来了,哈哈哈,好啦,我们看下怎么实现吧!不过在此这前,我们需要了解一些前置知识


ELF文件


我们的so库,本质就是一个elf文件,那么so库也符合elf文件的格式,ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。


image.png


那么我们so中,如果依赖于其他的so,那么这个信息存在哪里呢!?没错,它其实也存在elf文件中,不然链接器怎么找嘛,它其实就存在.dynamic段中,所以我们只要找打dynamic段的偏移,就能到dynamic中,而被依赖的so的信息,其实就存在里面啦
我们可以用readelf(ndk中就有toolchains目录后) 查看,readelf -d nativecpptwo.so 这里的 -d 就是查看dynamic段的意思


image.png
这里面涉及到动态加载so的知识,可以推荐大家一本书,叫做程序员的自我修养-链接装载与库这里就画个初略图
image.png
我们再看下本质,dynamic结构体如下,定义在elf.h中


typedef struct{
Elf32_Sword d_tag;
union{
Elf32_Addr d_ptr;
....
}
}

当d_tag的数值为DT_NEEDED的时候,就代表着依赖的共享对象文件,d_ptr表示所依赖的共享对象的文件名。看到这里读者们已经知道了如果我们知道了文件名,不就可以再用System.load去加载这个不就可以了嘛!不用替换classloader就能够保证被依赖的库先加载!我们可以再总结一下这个方案的原理,如图


image.png
比如我们要加载so3,我们就需要先加载so2,如果so2存在依赖,那我们就先加载so1,这个时候so1就不存在依赖项了,就不需要再调用Linker去查找其他so库了。我们最终方案就是,只要能够解析对应的elf文件,然后找偏移,找到需要的目标项(DT_NEED)就可以了


public List<String> parseNeededDependencies() throws IOException {
channel.position(0);
final List<String> dependencies = new ArrayList<String>();
final Header header = parseHeader();
final ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.order(header.bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);

long numProgramHeaderEntries = header.phnum;
if (numProgramHeaderEntries == 0xFFFF) {
/**
* Extended Numbering
*
* If the real number of program header table entries is larger than
* or equal to PN_XNUM(0xffff), it is set to sh_info field of the
* section header at index 0, and PN_XNUM is set to e_phnum
* field. Otherwise, the section header at index 0 is zero
* initialized, if it exists.
**/
final SectionHeader sectionHeader = header.getSectionHeader(0);
numProgramHeaderEntries = sectionHeader.info;
}

long dynamicSectionOff = 0;
for (long i = 0; i < numProgramHeaderEntries; ++i) {
final ProgramHeader programHeader = header.getProgramHeader(i);
if (programHeader.type == ProgramHeader.PT_DYNAMIC) {
dynamicSectionOff = programHeader.offset;
break;
}
}

if (dynamicSectionOff == 0) {
// No dynamic linking info, nothing to load
return Collections.unmodifiableList(dependencies);
}

int i = 0;
final List<Long> neededOffsets = new ArrayList<Long>();
long vStringTableOff = 0;
DynamicStructure dynStructure;
do {
dynStructure = header.getDynamicStructure(dynamicSectionOff, i);
if (dynStructure.tag == DynamicStructure.DT_NEEDED) {
neededOffsets.add(dynStructure.val);
} else if (dynStructure.tag == DynamicStructure.DT_STRTAB) {
vStringTableOff = dynStructure.val; // d_ptr union
}
++i;
} while (dynStructure.tag != DynamicStructure.DT_NULL);

if (vStringTableOff == 0) {
throw new IllegalStateException("String table offset not found!");
}

// Map to file offset
final long stringTableOff = offsetFromVma(header, numProgramHeaderEntries, vStringTableOff);
for (final Long strOff : neededOffsets) {
dependencies.add(readString(buffer, stringTableOff + strOff));
}

return dependencies;
}

扩展


我们到这里,就能够解决so库的动态加载的相关问题了,那么还有人可能会问,项目中是会存在多处System.load方式的,如果加载的so还不存在怎么办?比如还在下载当中,其实很简单,这个时候我们字节码插桩就派上用场了,只要我们把System.load替换为我们自定义的加载so逻辑,进行一定的逻辑处理就可以了,嘿嘿,因为笔者之前就有写一个字节码插桩的库的介绍,所以在本次就不重复了,可以看Sipder,同时也可以用其他的字节码插桩框架实现,相信这不是一个问题。


总结


看到这里的读者,相信也能够明白动态加载so的步骤了,最后源代码可以在SillyBoy,当然也希望各位点赞呀!当然,有更好的实现也欢迎评论!!


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

程序员坐牢了,会被安排去写代码吗?

今天给大家分享一篇有意思的爽文,但也是根据多年之前一个真实报道改编而来的。本文字数较多,建议先收藏,上下班路上、带薪上厕所、浑水摸鱼时再慢慢看~本故事纯属虚构请大家不要随意模仿,后果自负!— — — — — — — —因为删库跑路,我坐牢了。公司老板经营不善,...
继续阅读 »

今天给大家分享一篇有意思的爽文,但也是根据多年之前一个真实报道改编而来的。

本文字数较多,建议先收藏,上下班路上、带薪上厕所、浑水摸鱼时再慢慢看~

本故事纯属虚构

请大家不要随意模仿,后果自负!

— — — — — — — —

因为删库跑路,我坐牢了。

公司老板经营不善,拖欠工资半年,我终于忍无可忍,提出离职。

而老板居然说:爱走就走,一毛没有。滚吧!

我气愤的直接设置了全盘删除的自动任务,明天凌晨定时执行。然后直接走了。

收拾自己的东西离开了公司。

隔天老板发现这事,报了警。

老板以我的行为对公司造成了几百万损失的名义把我告上法院。

最后,我进了牢房。

狱友们问我:你怎么进来的?

我答:我写代码写进来的。

狱友们:你牛逼啊!

我只好惭愧的接受了赞扬。

进了监狱,其他人都是劳动改造,做些低端工作,而我作为技术人才,那就不一样了。我接着996写代码。

首先是管理监狱数据的小哥,要清点监狱人员的资料,居然用的是Excel表格。

他整理起来太累了,向领导抱怨。

领导就说:xx不是写代码的吗,让他来帮帮你。

然后我就被提了出来,替小哥整资料。整着整着觉得不对劲,我一个程序员,我凭什么手动整理资料?

然后我就打开了Excel函数,快速写了几个方法,把所有数据分门别类的处理完毕。

就在我靠着电脑椅打哈欠的时候,小哥回来了。发现我已经完成了。

小哥惊了,大声道:这是我平常一个月的工作量,你这就完了?

我不屑一顾的笑了笑,点头道:没错。

小哥举起了大拇指,赞道:不愧是程序员。牛逼!

然后我以为我会被放回牢房了。

结果,领导又找上了我……

领导:小x啊,我这边有点事情,你帮忙做一下。要是做好了呢,我可以给你申请减刑。

我略微激动,居然能减刑,当场拍胸脯:没问题,有什么事就让我来吧。

然后领导把我带到了办公室,告诉我:这系统莫名其妙就坏了,帮忙看看?

我心里开始发毛,又不是我写的系统,让我看问题,我勒个去,我有可能看不出来啊。

但是来都来了,牛也吹了,就只能硬着头皮看下去了。

捣鼓了一会儿,大概明白了,这是个管理数据的系统。现在数据查询完全废了。报错还挺明显,就直接弹出了具体的失败原因。

看了下详细报错,我恍然大悟:这**谁干的,往纯数字的id信息里面插了中文!

领导:那依你看,怎么修?

我拉过键盘,迅速操作,把记录调出来,将相关的几条记录修正。然后系统恢复了顺利运行。

领导看了看,夸赞道:小x你可真厉害啊!

我得意的笑了笑。然后越看那几行数据越眼熟。

等下,这不是我刚刚造的数据吗?那小哥整理数据就是为了导入到这个系统?

我去,是我插的中文!写函数的时候手抖了啊!

隐隐冒着冷汗,我昂然挺立,推翻了刚才的结论:这其实还是这系统的稳定性不够高,做系统的人没有做好防护啊。

越说越顺嘴,我大声到:要是我来做,这系统肯定不会这样崩!

但是,心里想了想,可能是换个方式崩。

领导看了我一眼,似乎发现了我前后不一致的说辞。但是并没有再说话。

我呆了一会儿,有点虚,主动咨询领导:我现在可以回去了嘛?

领导:可以,可以。

领导叫来小哥,把我送回去。还嘱咐着,给我换个好点的牢房。

但我心里寻思,这牢房还能有什么好的。

走出办公室前,还听领导在那边跟其他人聊什么,监狱改造,技术创收,充分发挥技术人才的价值……

我就知道,这事儿,还没完。

等下,我的减刑呢?领导不会忘了吧!啊!!!

小哥带着我回牢房,我看着这路不对啊。

我问道:这是去哪?

小哥答:带你去高级间。

我沉默了,还真换牢房了。原本的狱友们远离了,颇有点怀念呢。

进了新牢房,开局第一问:你咋进来的?

我:写代码写进来的。

狱友:哦豁,牛啊!

我:你呢?

狱友:我也是。

我:……合着你刚才是夸自己呢。

狱友:那可不。

看着狱友昂扬的头,我有些困惑。

于是我详细说道:我是被欠薪了,所以删库跑路,被告了。你呢?

狱友:我是产品经理要我写个五彩斑斓的黑色,我把他狗头打爆了!

我瞬间躲远了。好家伙,暴力狂。

但是多想想,我点头道:打得好!胡乱提需求的产品经理确实该打。

狱友猛的靠过来,握住我的手:同道中人啊。

我尴尬的笑着,不敢反驳。我才不是怕被打呢。

过了两天,小哥又来找我了。

小哥在门口招呼着:张工、x工,跟我来,领导有事找你们。

张工就是我狱友,而我就是x工。我们听到招呼,也就服从指挥的跟着走了。

在办公室见到了领导,领导笑呵呵的说到:小x,小张,你们来了啊。

领导接着说道:我这次找你们来,是想你们给他们做做培训,学习一下编程技术。让他们在里面能学技术,出去能融入社会。

张工瞬间不屑之色冲上脸庞,喊到:就他们那群没文化的,大字不识几个,怎么教的来!

我沉默着,我心里赞同狱友的想法。教好学生都要教很久的内容,更何况教一群可能没基础,而且也没向学之心的人。

领导被张工怼了,脸色青一阵紫一阵,沉默良久,最后对着旁边的狱警说到:把小张架回去,这个月的晚饭减半!

张工气的脸色一红,从旁抓起椅子,就想丢过去。

旁边狱警冲上来,摁住了张工。最后张工被架走了。

领导补了一句:不服管教,扣分!加刑期!

我沉默着,不发一言。

领导这时,缓缓转过头来,和善的笑道:小x,你怎么看?

我当即义正严词的回道:领导出的主意极好!教他们编程,能够做项目创收,出去也能找到工作,利于融入社会。我极力支持!

领导:那行,后面就你负责教他们了。

我:好!

接下来,我就多了一项任务:对着黑板教狱友们编程。

是的,只有黑板。

因为监狱配的电脑数量是有限的,就那么三四台办公电脑,满足不了广大狱友的需要。

我请小哥帮我从外面带了点编程教材,然后我把代码写在黑板上。狱友们看着黑板学编程。

我一边在黑板上写代码,一边在黑板上写输出。

狱友们都是一副看着汉奸的样子盯着我。

还好我是和张工一个牢房,不然我怀疑我会被他们在下课后暴打。

在上了三次课后,有狱友忍不住了。

在座位上举手,发言问我:x工,你说我们这样学,真的有用吗?

我:额……

有狱友搭腔:你这就像在问,用自己的右手能不能学会完整的调情技巧一样。

狱友们:哈哈哈!

我尴尬了,熬到上课时间结束,落荒而逃。

回去就打了报告,想找领导谈一谈。

我心里构思好了,就说我能力还是不足,带不了这么多优秀的狱友。而且这边也没足够的电脑,无法实际操作。所以请辞。

被带到了领导办公室。

领导:哦,小x啊,你来的正好,我这边接了个项目。

我满脸震惊,刚才构思的一切都忘光了。什么鬼?我进了监狱还得继续编程?

领导自顾自的接着说:这不,我们监狱有了你这样的人才,就得充分发挥价值。所以我找朋友问了下,拿了个商家的小项目,来试试手。

我:……

在震惊中缓不过神。监狱真的能接项目吗?合规吗?天呐。

领导:你别这样盯着我,我们监狱是可以组织服刑人员劳动创收的。

领导仿佛看出了我的意思。

我斟酌了一下,想了想张工的情况,便严肃点头:全凭领导安排!

(补充个相关报道,网上可查:

据凤凰网报道,2006年,讷河监狱进购了250台电脑,布置了两层电脑室。

监狱组织服刑人员打“魔兽世界”和“完美”等游戏。“他们每天打出多少游戏币是有要求的。”服刑人员需要升级挣装备卖钱,“这是监狱的创收方式”。)

然后领导就大概说了一下,要做的是个xx麻将的项目。说白了就是打麻将的APP,但是麻将的规则根据地区特色进行特殊化处理。

我听完有点疑问:那盈利点呢?是氪金给辅助工具吗?还是弹广告?

领导自信一笑:点卡模式,一张卡五块钱,一张卡打一局麻将。

我大吃一惊,不可思议道:现在是免费游戏的时代,道具付费才是常态。点卡模式已经被淘汰了啊。

领导神秘一笑:会有人买的。你尽管做项目吧。

我一时语塞。但也不想深究,反正又不是我做推广。

于是我提出了新的请求:项目可以做,但是我需要性能比较好的电脑,以及能够连到外网,找相关资料。

领导轻松的点了点头,说道:电脑过几天就来,到时候给你在办公室隔个位置,你就在那做项目。外网,我想想办法。

果不其然,过了两天,电脑就到了。

连上网,我就先上知乎,看看网友们又整了什么新活儿。

然后就看到居然有网友提问:

程序员坐牢了会被安排去写代码吗?

这就怒敲回答:不仅要写代码,还要996!

不行,不能多说了,领导来问我项目进度了。

一边写着代码,还寻思着领导刚刚来说的:

就这么个小游戏,今天做出来没问题吧?

我:……

我寻思领导不是业内人士,只好面露难色,想着怎么解释,不可能那么快。

领导看到了我的神色,皱起眉头,试探问道:一周?这总行了吧!

我:……

蛋疼感加剧。这如果有现成的案例去抄,有可能可以一周出货,但是我不能打包票。我保持沉默,皱着眉头。

领导一拍桌子:一个月!一个月我要见到项目出来,不能再多了!

我知道领导耐心可能到头了,便只好咬牙说道:那再加一台电脑,把张工派给我,我试试。

领导眉头放宽,说道:小张可以派给你。就一个月,我一定要见到成果。

然后领导就走了。

我赶在后面喊到:领导,记得我的减刑!

领导随意摇了摇手,表示听到了。

然后我就坐在这,想着怎么把xx麻将一个月做出来。

这肯定是996了,说不定还得007。

还要去网上搜索,有没有合适的参考项目,如果有的话,一周可能就能出货。

想着,我就下载了聊天软件,登陆了我的账号,找上了我的朋友。

我:在吗?有没有xx麻将的项目经验或者案例,我这急要!

我朋友:咦?xx你不是进去了吗?被盗号了吧,骗子!别想骗我。

我:我在监狱入了个创收项目,要做xx麻将。现在来求你帮忙了!我不是骗子。

我朋友:你怎么证明你不是骗子?

我:我知道你喜欢男的,够不够!

我朋友:……你进去了还能上网,牛啊!不愧是x哥。

我朋友:听说你删库跑路,我还为你叹息,现在一看,你进去以后过得还挺好。

我朋友:不愧是牛人,到哪都过得潇洒。

我朋友:你这是打算在监狱里接着干下去了?

我:……少哔哔,有没有资料。监狱领导给的活儿,我就等着干出点成绩来,求领导减刑了。

我朋友:我寻思在监狱里有电脑,有饭吃,可以打游戏,其实蹲里面不比外面差。

我朋友:你一直蹲里面,也没什么不好。这不,反正还能上网吹牛。

我:……

我心里真想撕了这小子,逼逼赖赖个不停。

跟我朋友掰扯了半天,他一直劝我狱里挺好的,不用急着出去。

我烦的骂了他一顿,把话题拉回来,到底有没有资料?最后这货说他也没资料,回头帮我问问做这方面的熟人。

蛋疼的结束了对话。

然后自己上网检索。这类项目还真不少。但是源码又拿不到,还是得自己做。

暂时没什么思路,张工的电脑也没到位。我只好一边紧张的牙疼,一边上知乎摸鱼。

看到网友们的评论,笑得我合不拢嘴。

网友都是人才啊!

随后下了几个游戏,电脑设置静音,然后打了起来。

打的痛快了,然后想起项目还没做……战战兢兢的……继续打游戏。

就这样,在紧张的摸鱼划水中,张工的电脑也配齐了。张工也给我派过来了。

然后……我们就开始在游戏中双排。

当然,中间还是有讨论一下项目的。

张工表示不难,他来搭一下总体架构。

那我就放心了,然后我们继续双排冲分。

此刻回想起我朋友的话,似乎也没什么毛病。

监狱里挺好的,网友,哦不,是狱友们个个都是人才,说话又好听,我超喜欢这里的。

到监狱里就跟到家一样。

打了几天游戏,不对,是做了几天项目,进度不咋地。

我开始有些头皮发麻的时候,我朋友回信了。

我朋友:x哥,在不?

我:不在。

我朋友:前两天你让我问的项目,我问到了。

我:说说看?

我朋友:你那个xx麻将有雷啊,表面上是点卡收费,实际上……是灰产。

我:你可闭嘴吧你,就说有没有资料。

我朋友:你不关心风险?后面加刑了怎么办?

我:我不做项目,立马就加刑。

我朋友:……

沉默良久,我朋友接着发了句:看来你确实在里面呆的很舒服,想接着呆里面。

我:呸呸呸!你可少哔哔,赶紧把资料给我。

经过我的一顿催促,我朋友总算把资料发给我了。

还给我絮絮叨叨说什么风险,我只回了句:

技术是无罪的。[滑稽]

翻开资料,按步骤,架设后台服务器,安装手机模拟器,打开xx麻将APP。

完美!

就是贴图不太对,是yy地区的,我要改成xx地区。

用P图调整一下,大功告成!

然后我和张工讲了一下这事,我们击掌相庆。

项目初步完成,继续打开游戏,双排。

当领导走进我们项目组的时候,差不多是一周左右。

那时候张工正站了起来,怒视着我,呵斥道:你怎么这么菜,刚才那波你不应该上的!你就不会先拉扯一下吗!

我尴尬的笑着:我觉得我可以打赢。没想到我不行。

张工立刻举起了椅子,喊道:你再说一遍!

我:不敢不敢。

领导:咳咳,你们在干嘛?

瞅见领导来了,我的脑筋立刻转了一百八十度,回答道:我们对于项目的实现有点分歧,正在沟通。

一边说着,一边把游戏退了,切到了程序页面。

领导狐疑的看了我一眼,但是没有深究。这时候张工也把游戏界面切掉了,我们完美过关。

领导接着说道:有分歧不是问题,要好好沟通嘛。

我:是是是。

张工没说话,保持沉默。

领导:我现在过来,就是看看进度的。怎么样了?

我不敢报太快,就是模糊说到:只是做了个初步的模型,还有待完善。

领导:能一个月完成吗?

我想到那个完整的资料,即刻拍起了胸脯,说道:没问题,保证完成任务!

领导:那让我看看你们做的模型吧。

我:好!

然后我在一通手忙脚乱之后,打开了模拟器,启动xx麻将。

领导:这个xx公司是什么意思?

我冷汗直冒,糟糕,原有xx公司的水印还没去掉。

然而冒冷汗并不能解决问题。

经过短暂的思考,我解释道:这是我和张工打算为了这个项目的运营成立的公司,先写上了名字。

领导:是吗?为什么我感觉好像听过这公司名字?

我舔了舔发干的嘴唇,故作疑惑道:什么?名字已经被占了?那看来不小心重名了,这个公司名不能用了。

领导沉默了一会儿,没再纠结这个问题。

然后领导接着看项目,时不时一句这里不对,那里不对,提了一堆修改意见。

艰难的应付完,送走了领导,我和张工面面相觑。

我:接下来可真得干活了。

张工:别说了,赶紧下一把。

我:走走走!

然后在紧张激烈的打游戏过程中,我们抽空改了改项目。

做着做着,开始了闲聊。

张工:你知道吗,减刑窗口期就在下个月了。

我:咋了?你的意思是尽快完成,争取奖励?

张工:不,我的意思是卡住时间,不减刑绝不完成!

我:emmmmm……可你没戏吧,你这不是要被加刑了。

张工:不会,那领导现在有求我们的地方,把柄在手还怕他不减刑?

我奇怪的看了他一眼,赞道:不愧是张哥,牛。就看你发挥了。

又过了几天,领导果然再次来检查进度。

我们故意提供未改完的版本给领导看。

我故作艰难的说道:这个改造比较复杂,正在努力完成。

然后我使了使眼色,张工跟着开口:听说减刑窗口期要到了。这次能给我们减多少刑期?

我领导先呵斥了我,说道:工期就一个月,必须按时完成。做不到就加班加点的干。

然后撇了一眼张工,说道:你们放心,我都安排好了。

领导似乎说了什么,又似乎什么都没说。

局面一时尴尬,集体沉默了几秒。

而后领导又抚慰道:小x,小张,你们放心,好好给我做事,我不会亏待你们的。

张工听完,脸色渐渐变红,大声喊道:你给我说清楚,什么叫不会亏待?你安排好了什么?

在张工咆哮的时候,旁边的狱警一下子窜了上来,一个擒拿,先制服了他。

领导撇了张工一眼,脸上略显无奈。

随后领导示意狱警放开张工,劝道:你好好按时完成,我尽力申请减刑。这总行了吧?

张工冷哼了一下,拍了拍衣服,说道:还凑合吧。

我悄悄比了个大拇指。

等领导和狱警走后,我赞道:还是张哥牛逼啊。这下子稳了!

张工脸色慢慢恢复平静,然后说道:不能信这种人的鬼话,依然要拖工期。他求着我们,才会给我们办事,等我们完成了,没有利用价值,那就不可能了。

我一时有点诧异,但是刚刚一幕还在眼前,于是点头道:张哥靠谱,就按张哥说的办。

接下来的几天,我们接着双排,冲分。项目干脆先不写了。

游戏打着打着,我忽然想起个事。

我说道:张哥你先单排,我去写个后门程序。

张工看了我一眼,点了点头,然后继续埋头打游戏去了。

除了拖工期之外,要时刻拿捏把柄,那自然是后门程序无疑了。

首先,我先写个加密,然后设置了有效期一个月。每过一个月,必须给一个新的密令,要不然程序直接罢工。

其次,我写了罢工后的操作,对关键程序文件进行自我删除。反正我这边有完整的文件备份,删了就删了。关键就是让他们无法恢复。

最后我写了个程序罢工后的常规提示:请找系统管理员解决。

接下来,把密令和加密程序上传我的云盘,删除本地文件。万事大吉。

我跟张哥透了个气,共享一下后门程序。张哥表示不需要,他一定要在上线前解决问题,不拖到上线后。

想法不一样,不要就不要,我也乐得如此。

独掌后门程序,想让项目走下去,还得回来找我。

监狱里实在太无聊了!

能见到的就那么几个人。

狱警小哥,狱友,领导,就这么些人。

天天打游戏也很烦啊。

我想出去,换换口味,吃点鸡排,汉堡,烧烤。

我想出去玩点别的,不是天天打游戏,还可以去爬山,去打球,去玩桌游。

我想看点美女,穿汉服的,穿jk的,穿洛丽塔的。不像这里面,衣服就特么清一色,还连个女的都没有!

张工:来来来,下一把。

张工招呼了一下,不说了,继续打游戏了。

但是,我想出狱的心思愈发浓厚了。

仅仅打游戏,只是满足了低层次的需要。

我还要吃美食,看美女。

我要站在山巅,俯望大地。

然后我又操作失误,屏幕灰了。

切出去一看,我朋友又找我了。

我朋友:x哥,咋样,项目做完可以出来了吗?

我:没呢,拖着。逼他减刑再交项目。

我朋友:666,x哥牛批!

我愧不敢当,这不,复活了。继续冲杀。

我朋友:但是你那个项目有问题啊。这种xx麻将实际上是给别人提供网络赌博的渠道,点卡等于赌场的抽水。

我朋友:你这种间接提供网赌,被抓到就又进去了。

我朋友:x哥,人呢?你这样不行啊。

然后我屏幕又灰了。再切出游戏。

我:去去去,别乌鸦嘴。

我:技术无罪,你懂吧。这又不是我想搞的项目。

我朋友:要不,你举报吧。说不定还能拿个戴罪立功?

我:……这,不太好吧……

我有点意动,又有点犹豫。

我还没给我朋友回消息,一旁的张工先叫了起来。

张工:又要输了。你怎么就不能专心点打游戏呢!

张工:连打游戏都不专心!

我只好尴尬的关闭了聊天窗口。

经过一场奋战,果然还是输了。

张工握紧拳头盯着我。

我立刻认怂:我错了,是我太菜了。

领导:你们在说什么呢?

没注意间,领导又来了,手上还拿着一叠材料。

我瞎编了几句项目遇到困难,正在讨论,糊弄了过去。

领导:来看一下,这是减刑申请书。已经给你们写好了。

我稍微翻了翻,减刑申请书包含:

  • 申请人的信息。

  • 犯案情节,服刑期间的积极行为。

  • 说明减刑条款,就是减刑原因。

看了看我的减刑原因,态度积极,确有悔改。

看看张工的减刑原因,态度积极,确有悔改。

我偷偷看了眼张工刚刚还捏紧的拳头。真可谓:

说你没悔改,你就没悔改,有悔改也没悔改。

说你有悔改,你就有悔改,没悔改也有悔改。

看完申请书,我非常满意的点了点头。

但是张工却在一旁低声说道:申请了之后还要评审,评审了还要公示,有人提异议还要复核。这只是第一步。

领导不管我们窃窃私语,继续问项目进度。

那还用说……我们都忙着写(da)代(you)码(xi),当然没什么进度啦。

领导呵斥道:减刑申请书都给你们搞了,你这进度行吗?我下周必须要见到成果!

领导沉默了两秒,补充道:做不完就给我加班加点的干!

我和张工对了对眼色,张工微微摇头。

我心中有数,当即答道:我们会努力的。

我似乎说了什么,但其实我什么也没说。

领导:下周如果没完成,减刑申请书不会通过审批的。

领导呵斥完,就走了。

我和张工面面相觑。

我:要不,还是下周提交完整版?

张工保持沉默,皱着眉头,没说话。

这一招,给个蜜枣再敲一棒子,令我和张工都踌躇了。

我犹豫了一下,说道:要不,这周少玩点游戏,推点进度意思一下?

张工犹豫了一会儿,微微点头。

我叹息道:再这样磨洋工不合适,但是完全做完也等于主动丢弃谈判资格,所以推动一些,意思意思,只能如此了。

张工诧异的看了我一眼,赞道:说的不错。

暂时也无心游戏了,我切到聊天界面一看。

好家伙,我朋友快给我刷了99+了。

就看最后几句……

我朋友:x哥,你还在吗?

我朋友:你是不是被监狱领导抓到了?

我朋友:我现在报警来得及吗?

我朋友:呸,不是,我现在举报来得及救你吗?

我朋友:x哥你说话啊……

我踌躇了,我开始思考一个人生的终极问题,我朋友会不会是喜欢我?

这不就是,我拿你当兄弟,你居然想上我?

烦恼了挠了挠头,我还是回了消息。

我:闭上你的乌鸦嘴。

我:我刚才在打游戏。

我:刚才领导来了下。

我:给了减刑申请书。

我:暂时不考虑举报。

我:就看后面减刑顺不顺利了。

隔了会儿,我朋友回信了。

我朋友:我差点就在想报警了。

我朋友:不过想了想,你在里面,人家民警也不管啊。

我:……废话,狱警也只会一起对付我……

这可能就是入狱的困扰了吧,警察不会保护你了。相反,警察遇到你,得抓你。

在紧张的写(da)代(you)码(xi)中,一周很快就过去了。

当领导来检查的时候,我们的修改,当然……并没有完成。

领导对我们拍桌子瞪眼,怎么这么久了还没完。

我赶紧解释:你看这个这个,这几个关键点,我们这一周加班加点的赶出来了。

然后我跟上一句:减刑还是需要您大大的……

领导直接打断了我,说道:行,就这样上线吧!

我懵逼了。我看向张工,张工也懵逼。

领导:客户等得不耐烦了,先上线。这些问题看他们反馈再考虑改不改,不反馈就不管了。

我和张工无以言对,最后我只能竖起拇指,夸道:您真是高!高明!

领导接着发话:小张先送回去劳改,小x你负责给客户上线。

然后狱警就把张工制服带走了。

张工走前留了这么一句:小x,要注意保证质量啊。

张工强调了“质量”,我自然明白这意思。

在项目中质量和速度近乎是反义词,做得快就容易粗制滥造,赶工做出垃圾。而要提高质量,那么速度上就快不起来。所以,张工是提醒我,切记别忘了拖时间,把握好把柄。

看着领导那不屑的笑容,我想他没明白这个提醒的含义。

接下来领导给了个联系方式,让我去联系。好家伙,居然是让我上线。那我岂不是……具备了再次删库的条件。

等下,我为什么要说再?算了,先再来一把游戏吧。

和客户的联系人沟通之后,确认了是他们提供主机,我远程登录上去部署。

然后,我要配合他们联调测试,直到彻底确认能可以使用。

了解到这些,我立刻又写了个后门。

既然能够得到具体的部署地址,那么,我就在服务器上面留了个入口。

只要我发送特定的加密字符串到特定入口,立刻启动核心代码删除程序。

这样,主动删库和被动删库的能力就齐活了。

(被动就是指那个一个月没有更新密钥就自动删除代码的程序。)

然后就是枯燥的上线过程。

先部署数据库,然后部署服务器,然后测试网络情况。

自己先用电脑的手机模拟器下载APP,进行测试。

然后指导对方联系人用手机下载APP,进行测试。

中间略有波折,最终顺利通过。

我就基本完成了上线任务。

闲下来之后,我开始慌了。我忽然意识到了一个问题:

领导在决定上线之后,立刻拖走了张工。

那现在上线完成了,是不是也会拖走我?

虽然我留了后门,但是也不能低估领导的凶狠啊。

我立刻把本地代码上传云端,然后对本地代码进行清空。保证我独一份的数据。张工那台我也给删干净。

然后通知我朋友:如果一个月,不对,如果两个月联系不到我,就举报领导参加灰产。

我就一边上传文件,一边写举报信。

当然是实名举报,举报人是谁?是我自己。

这多劲爆啊,狱里的犯人还能举报狱里的领导。

匆匆忙忙,传完文件,本地清空也搞定。然后举报信发给了我朋友。

好了,我安心了,继续打游戏。

我正要开下一把,领导倒是没来,但是狱警来了。

我:额……有什么事吗?

狱警:领导说项目结束了,从哪来回哪去。

狱警颠了颠警棍,问道:你自己走,还是我带你走?

我:……我自己走,我自己走。

在狱警的监督下,我回到了和张工一起的牢房。

张工诧异的发问:你怎么回来了?

张工下一秒醒悟:你怎么没拖住呢!

我当场尴尬,回道:这也不是写代码,只是部署个项目,一不小心就全弄完了。

张工气的抬起了手,犹豫了一会儿,又放下。

张工叹息:唉,这下子完蛋了。给这老小子得意了。

我尴尬的不知道说什么。但是觉得沉默也不好。

于是我顺着张工的话头说道:是啊,这下子完蛋了。

但是我想了想,又回过味来。之前就是坐牢,现在还是坐牢,有什么区别呢?

可能区别就是不能打游戏了吧。

于是我和张工一起,原地坐牢。

我:好无聊哦,现在没有游戏可以玩了。

张工:何止,刑期还变长了。

我:有吗?没变长吧。

张工:本来可以减刑,现在有可能减不了,那就是变长了。

我:……似乎很有道理的样子。

然后我们继续参加劳改。

大体内容就是,简单重复的工作,钉扣子和绣花等等。

熬了两三天,从难熬到逐渐习惯。

我和张工都开始麻木了。

这时候年轻的狱警小哥找来了。

小哥:领导正找你呢,赶紧跟我来。

我懵逼:我这儿活还没干完……

小哥:别干了别干了,你程序出bug了。领导喊你回去项目组修bug呢。

我缓缓回过神来,问道:出bug了?

小哥:是啊。

我猛地意识到,我可以回去了。

我笑了起来:哈哈哈,我的程序出bug了,出bug了啊!

强烈的喜悦冲刷着我的内心。

意料之外,而又情理之中,代码出bug了。

往常的我,出bug愤怒至极;而这次的我,出bug特别开心!

然后我就被狱警小哥送回去了。

我那个开心啊,又能回去打游戏了,又能跟网友们吹牛了。

乐颠颠地走着。

直到我坐在电脑面前,我才开始思索。为什么出bug了?

我明明是拿了个现成的项目改的,凭什么出bug啊?

难道又是历史的代码,屎山带来的问题?

想着想着开始头大了,我就想先打把游戏解解压。

刚刚打开游戏界面,我就瞧见领导正走进来。

我赶紧把游戏关了,切到代码界面,假装在看问题。

领导:小x啊,你怎么回事,项目出bug了,赶紧看看吧。

我:在看呢在看呢。

一边假装严阵以待,一边想着等会儿游戏用什么英雄。

领导:什么时候能查出来啊?

我灵机一动,答道:这个,我也没把握啊,可能是张工写的部分有问题。需要张工帮忙看看。

领导陷入了深思。

我感觉我真特么贼机灵,赶紧借着这个机会,把张工拉回来,正面肛领导。

领导沉默了一会儿,脸都黑了,最后用手一拍桌子,说道:我把小张给你派来,但是你今天必须查出来问题是什么。

然后领导威胁道:不然的话,不仅不能减刑,晚饭也别吃了!

我立即点头:好。

然后领导走了。

得了,这游戏看来暂时不能玩了。

我得研究研究,到底是为啥啊?

我远程登陆了服务器,然后通过工具,获取了服务器上面的报错内容。

报错内容挺简单的,内存溢出了。

就是内存不足,项目需要的内存超过了分配的内存。

这真是个经典错误,然后我开始探究是不是服务器太垃圾了,或者配置有问题,内存不够用?一看,好家伙,服务器没问题,内存给的很大,应该不是这方面的问题。

然后我换了工具,尝试提取了内存分布情况。就是看看到底什么占了大部分内存。

这时候张工就来了。

我:张哥,你可算来了。兄弟我够义气吧?见到机会,就把你拉回来了。

张工:小x不错啊,好兄弟!

张工赞了我一句,然后问我,是什么情况,为什么能把他拉回来?

我就开始介绍,大概出了什么问题,我跟踪到了哪里。还提了领导威胁的话。

然后我们初步达成了共识,先把问题查出来,然后以此为理由,跟领导讨价还价。

接下来,我就打开了内存分布的日志,好家伙,内存里占满的是基本类型。

这种基本类型到处都在用,根本看不出问题。

我和张工相互对视,两脸懵逼。

然后,我们讨论了一下,原有项目没这个问题,所以我们一起检查修改的代码部分,尽快找出问题。

查了一阵子,张工大叫一声,找到了。

我赶忙跟过去看,是哪里出了问题。

张工:就是这个函数,没有释放内存资源。

我:这一块啊……我记得我专门优化过这一系列的内存释放啊。

我:之前这一块乱七八糟的,用一下释放一下,没有规律。代码跟屎一样。

我:当时我看到了,就把内存释放合并到特定模块。优化结构,挺高可阅读性和可用性。

张工指着屏幕上的特定部分,说道:你的优化我看到了,思路不错。但是,这一块没有引用到你的释放模块。

我:……

核对一下代码,是的,几个优化的模块都有引用到了,但是这一个,没有。

我再看了一遍,是的。唯独这个,它没有就是没有。

我:这,咋说……哎,它怎么就没引用到呢。

张工:所以说,就是改的时候漏了。

张工:哎,你不知道程序员界的那句谚语吗?就是“bug能跑,就别改。”懂?

我寻思这是哪儿来的谚语,说道:可这也不是bug,就是设计混乱,代码稀烂。我才做的优化。

张工:一样。这种写的烂的,不管他再烂,只要能跑,就别改!你改了一个bug,就可能因此衍生出一千个bug。

我无奈点头,答道:是是是,明白了。bug只要能跑,就别动它。

接下来我们讨论了一下,有两个方案:

1,直接恢复原始代码,恢复这个模块的逻辑;

2,检索所有涉及部分,都改成新的,确认无遗漏。

讨论了一下,还是新的更合理。优化是有必要的。

我们采用方案2,全部改成新的。

于是我进行了全局检索,确保全部修改到位。

改完了。接下来?当然是来一把游戏啊!

打了两把游戏之后,领导来催了。

领导:小x啊,问题找到了吗?

我:找到了找到了。

领导:是为什么?

我:这个,程序在我们电脑上都是好好的,我查了下,是服务器的问题。

领导有点担忧的问道:那怎么弄一下,修复一下?是不是要换服务器?

我:不用不用。我调整一下程序和服务器的配置,兼容一下就好了。

领导脸色欣慰,说道:那赶紧弄一下吧。

(说句闲话。这个bug是真的出现过的,我们这边来了个新人,把c的内存释放的部分优化了一下……然后就出现了严重的生产事故。)

领导让我赶紧修复,而我却闭上了嘴。

这时,张工面无表情的看着墙壁说道:我们的减刑,安排的怎么样了?

领导皱起了眉头。

场面一时间沉默了。

不知过了多久,领导神情平缓了,说道:现在修复吧,我会为你们争取减刑的。放心,亏待不了你们。

张工看向了我,微微点头。

我心里有了底,然后手指如飞的操作起来。

其实也没什么内容,就是编译一个新版本,然后丢上去覆盖,重启,完事了。

看着项目启动完成的提示出来,我就对领导说,启动好了,可以试试了。

随后领导播了个电话,确认了运行正常。

这次紧急bug就到这里了。

接下来,我们不需要回去劳改了,因为领导终于意识到了项目可能出问题。

所以,我们转成项目的维护工程师了,接下来就是在这边维护项目。顺便把几个可能要做的修改点,先做一下。

领导走前还强调了,会给我们“加薪”。

劳动改造产生经济效益,会给犯人发点补贴,就是零花钱。

一个月,少的40或者60元,多的100元,可以买点烟抽一下,或者买点榨菜改善一下伙食。

而我们从事技术类工作,领导许诺,会给我们一个月发300元。能多买好几包榨菜呢。

然后,接下来我们就放心的继续打游戏了。

过了几天,减刑的审查结束了,开始公示减刑名单。

张工果然在减刑名单上面,稳得很。

我看到名单就夸张工:张哥稳啊!稳得一批!

但是,减刑名单上没有我的名字。

我翻来覆去的看了好几遍,确实,真的没有我的名字。

我:我要见领导!我要见你们领导!

狱警:领导说了,他不在。

狱警:……就是领导不在,现在见不了你。

我:那你告诉他,不来的话,我现在就删库。

狱警:删库?啥意思?

我:就是让项目死掉的意思。你转告领导吧。

然后狱警就走了。

隔了没一会儿,领导来了。

领导:哎,我说小x啊,别激动嘛。我不会亏待你的。

我:……

无语了一阵子,我直接问领导:为什么减刑名单上面没有我?

领导:我可是给你换了高级牢房呢,我不会亏待你的。

我再问领导:为什么减刑名单上面没有我?

领导:我给你提供了优越的办公条件,你们是少有的能够碰电脑的犯人啊,别不知足。

我三问领导:为什么减刑名单上面没有我?

领导:你看,别的犯人一个月才几十块,我现在可是给你开了300块工资呢。

我快要疯了,嘶吼着问道:你xx的,到底为什么减刑名单上面没有我!

领导脸也黑了,沉默良久,吐出了四个字:下次一定!

随后领导安抚了我,说了一套什么我审查资格不达标,所以没通过的话。

我持怀疑态度,没说话。

然后,领导强调下次会再为我申请。

我没说话,但也没办法。

随后领导离开了。我只能叹息,等待。

审查资格不达标什么的,我是不信的。毕竟张工的减刑原因可是“态度积极,确有悔改”,这他都能通过,我凭什么不能通过?

但是胳膊掰不过大腿,领导说你不达标,那你就是不达标。

我等这个“下次一定”,着实等了好久。

接下来,我和张工继续一起打了一段时间游戏。

张工在减刑后刑期缩短,不久后申请了假释,他就出狱了。

然后我就失去了双排的小伙伴,开始了孤独的单排之旅。

中间我朋友有找我聊天。

我朋友:x哥近来可好?

我:不好,差极了。

我朋友:咋了,监狱里过的不是美滋滋吗?

我朋友:上次还以为你要出事,结果也没多久又跟我说没事了。

我:刑期变长了,能好吗。

我朋友:啥?刑期还能变长?你是在里面斗殴了?

我就跟我朋友解释了一下整个事情,我本来好好地能减刑的,结果减刑飞了。

我:减刑没减成,可不就是变长了嘛。

我:另个一起的狱友可是减刑成功了,他都假释出狱了。

我朋友:……真惨啊。但,也有可能就是你的狱友够狠,才成功的。

我朋友:而你太好拿捏了,就被剩下了。

我朋友:有些东西只能自己去争取,而不能假手于人。

我:是是是,你说的都对。

我微微叹息,事已至此,徒呼奈何。

过不久,领导又来找我了。

领导:小x啊,我这边又接了个项目。

我:这个,是什么项目?

领导给我讲了讲,我越听越耳熟。

我:等下,这不就是我入狱前做的项目?

领导微微点头:没错,就是它。

我瞬间开始蛋疼,脸上不知什么表情是好,应该是一抽一抽的。

有了上次的教训,我也不迂回了,直接问道:那我做了的话,有什么好处,给我几十万?帮我减刑假释?

领导神秘一笑,然后大手一挥,大气的说道:做成的话,工资再给你加两百!

我气得开始笑:哈哈哈。

蛋疼的抽抽,笑了会儿也就罢了。

我慢慢回过气,说道:这项目可是据说价值几百万,还把我送进来了。你确定,只是给我加两百?

领导低沉的说道:也是,听说这个项目必须靠你。那,再加两百!不能再多了!

我摇了摇头,直接不理领导了。

狱里拿个千八百有什么用,多吃几包榨菜吗?

出去了之后,以我的能力每个月至少一万以上的,在这里面加薪,有什么用呢。

领导劝了几句,发现我完全不理它。

然后咬牙切齿的走了,说着,以后有你好看的。类似这种威胁的话,然后往外走。

我完全没听进去,还能咋滴啊。我的情况难道还能更差。

等下,我下次减刑咋办?领导可是承诺了“下次一定”的。

于是我喊住了领导:等下!

领导立刻转怒为喜,说道:怎么,改主意了?我说嘛,就没有加两百搞定不了的,不行就再加两百!

我开始怀疑自己是不是做错了,我应该继续保持沉默。

领导:咋了,又不说话了?

我缓了缓神,慢慢的说道:这项目,我做了,能立刻减刑吗?

领导:这当然不可能……

领导看我就要变脸了,转而说道:不可能不行。

讨价还价了一番,大概是要求我把删库的数据完整恢复,并且再做一些相关的改造调整。

领导给出的价码是:“尽快”帮我申请减刑。

那我的回应自然是:“尽快”完成工作。

商量完之后,领导走了。

我接着打游戏。

打了一天游戏,回到牢房,躺着。

有点无聊啊。张工走了,单排着实没劲。

闲的开始数绵羊。

然后狱警小哥带个人来了。

狱警小哥:这位是x工。这位是陈工。

狱警小哥:领导安排陈工过来,后续协助你开发项目。

我无可无不可的点了点头。

随后狱警小哥完成了引导,就走人了。

陈工立刻贴了上来,媚笑着说道:x哥,你好!叫我小陈就行。

我随意的点点头,问道:你咋进来的?

陈工略带不好意思的笑了笑:就是急用钱,顺手从公司账上划了点。

我大吃一惊,说道:你是黑了公司财务系统,还是盗了账号?

陈工保持不好意思的笑容,说道:修改了公司的收款路径,转到了我的账户上。不小心搞多了,被发现了。

我:搞多了是多少?

陈工:也就几百万吧。

我寻思这也太明显了,公司一段时间内流水变少,肯定是会探究的。这人略蠢啊,被抓的不冤枉。

陈工看我不说话了,就问我:x哥,你是怎么进来的?

我就把我的苦逼故事复述了一遍。

隔天,我去项目组继续打游戏,陈工就也跟过来了。

我约陈工双排,陈工没答应。

陈工说:监狱里是难得的清净时光,要好好的修炼,出去才能赚大钱。

得了,人各有志,各玩各的吧。

玩着玩着,领导又来催进度了。

领导刚刚进门,陈工就站了起来,迎到门口。

陈工媚笑着说道:哎哟,领导来了,真是蓬荜生辉啊!

在迎接领导的同时,陈工朝我使眼色,让我快点关闭游戏。

虽然陈工的姿态我有点不爽,但,也是给我打掩护,对吧。

我马马虎虎关闭了游戏,看着电脑屏幕开始思考人生。

领导:小陈啊,在这里感觉如何啊。

陈工:太舒服了,没想到监狱里能有这么好的条件。我简直想在这里永远住下去。

领导微微点头,随口说着:不错,不错。

然后领导过来问我:小x,进度怎么样了?

我表示:工作比较困难,正在努力推进。

领导皱了皱眉。随后若有所指的说了句:小陈你要好好学啊,x工回头减刑出狱了,项目就靠你扛大梁了。

陈工点头如捣蒜:是是是,一定努力学习。

随后领导走了。

陈工找我要项目源代码,用于他学习。

我有点不想给,这独占的源码放出去,可能就收不回来了。

但是,不给的话,他跟领导告一状,那我怎么办?现在又没和领导撕破脸,减刑还要仰仗领导。

最后考虑到,毕竟是狱友,在减刑这块,我们是统一战线的。还是给吧。

于是,我把之前领导给我和张工减刑的故事,跟他说了一遍。告诉他,要长教训,拿住把柄。

陈工赞同道:x哥说得对!确实是这样。

我看他应该明白了,然后把源码也就共享给他了。

隔天领导就又来了,检视了一下。

领导问我进度,我的回应和昨天一样。反正我都在打游戏,怎么可能有进度。

然后领导问陈工,项目熟悉的怎么样?

陈工就拿出了好几个我一直拖着没改的功能。陈工居然全都做完了。

我当场惊呆,这陈工,咋这样?

一来是做得快,不愧是不打游戏的人;二来是怎么没拖呢,昨天的话都白讲了啊!

领导大大的夸赞了陈工。随后就提出了给陈工加薪。

陈工千恩万谢的说着:感谢领导栽培,领导恩重如山,领导就是再生父母!

我在一旁冷眼旁观,感觉事情不太对劲。

看来我要早做打算了。

我打开了聊天框,我朋友的头像暗着。

我:我可能要出事了。

我:如果连续两天联系不上我,就把举报材料帮我发出去吧。

我:鱼死网破而已,我不惧!

随后在服务器上设置了自动任务,48小时后自动开始批量删除文件。

只要我连续两天无法接触电脑,就直接炸了吧。

这自动任务之前就写过,现在基本上就是复制黏贴,轻车熟路。

领导和陈工一顿亲切交流。这时才结束。

然后领导看我的眼神就变了。

领导似笑非笑的说道:x工,你可真能拖啊。

我一边退出软件,一边删除文件,随意的应道:不敢拖延,只是陈工能力比我强罢了。

领导点了点头:x工说的,有几分道理。

随后领导侧头斜着眼睛看过来:那么,x工就回去劳改吧。

而后领导拍了拍陈工的肩膀,说道:小陈,好好干,我不会亏待你的。

而后领导打了个手势,旁边的狱警冲上来,制服了我。

这一幕可真嘲讽,想当初被按住的可是另一位呢。

我挣了一下,挣不脱。也就放弃了挣扎。

我走前,大喊道:过河拆桥,不得好死!

随后我度过了战战兢兢的两天。

中间陈工有和我一起吃饭,我沉默了许久,还是忍不住问了一嘴。

我看似不在乎的说道:小陈,你害我,又有什么好处呢?

陈工先是一脸迷茫,而后恍然,说道:我没想这样啊,我只是努力展现价值,希望得到认可与奖励。

陈工随后一脸歉意的说道:非常不好意思,我没想到我这样,会导致你被叉出去。

我微微叹息,也不知道该如何说话。原来,有些人只是在努力,他根本不知道他的努力把你卷死了。

随后我不再说话,静静吃饭。

但是,陈工却是打开了话匣子。

陈工左右看了看,压低声音对我说:其实我也没指望这领导能给我多少好处,这领导一看就是个抠门精,只会画饼,不干实事。

我诧异的看了陈工一眼,这小伙子似乎挺清醒的。

我:那你还……

陈工继续压低音量说道:我已经把我的收款账户插进项目里了。

陈工说到这里,就阴恻恻的笑了笑。而我,哈哈哈,笑声没收住,赶紧捂住嘴。

接下来,饭都感觉更香了。

不过我的处境也没什么本质变化,过一会儿也就恢复了平静。

熬到两天后,我静静的待着。我心里想着,系统该炸了。估摸着是这个时间了。

又过了小半天,领导果然来了。

领导倒提着警棍,他来了,他来了。

但是脸色和我预料的不同,领导居然一点也不愤怒。

领导讽刺的笑着:好小子,居然在项目里埋雷。

我微微皱眉,应道:不敢,可能是系统出bug了,是不是需要我去看看?

领导摇了摇头,说道:不用了,小陈修好了。

我恍然大悟,却不知该哭还是该笑。

该哭的是,陈工居然能解决我删库的定时程序;该笑的是,项目的钱已经不再属于你了,而你毫不知情。

看我不接话,也不绝望,领导不满意了。

领导皱着眉说:虽然你没给我造成很大的损失,但是,我不教训你一顿,我心里不痛快!

领导指使手下控制住了我,随后给我一顿乱抽。

给我抽断了几根骨头,躺在地上只能喘息,痛到无法动弹。

不知时间过了多久,检察院的人来了。

指着我说了些什么,开设网络赌场,殴打犯人,之类的罪行。

然后,我松了一口气,就昏了过去。

我再次醒来时,人已经在医院了。

后来,和朋友聊了聊才知道,我朋友把我的举报信发给了检察院,纪委,市长热线,等举报渠道,并且通过记者朋友进行了宣传,煽动舆论。

举报了领导非法经营灰产,利用xx麻将APP开设网络赌场,肆意殴打犯人,等几个罪行。

然后,我提供的材料就是证据,我本人也成了证据。

我还问我朋友:你怎么知道殴打犯人?我没写这个啊。

我朋友:我猜的。你都失联了,大概率会挨打,随便写上的。

我:……

过了一段时间,慢慢的知道。

领导因为开设赌场等罪,也进去了,不过这类人的关押的监狱不一样,换了个监狱,

而我也因为举报有功,符合了重大立功表现的条例,成功得到了减刑。

而后许久听说,陈工又加刑了,也不知是真是假。

好了好了,不说了,医院的领导叫我帮他们看看系统问题了。

(全文结束)

转载类似新闻:

2020年11月17日消息,随着网络的普及,一些不法分子动起了利用网络赌博非法牟利的歪脑筋。海南省琼海市嘉积镇的黄某某就是其中之一,他利用网上打麻将软件,开设赌场聚众赌博,两年多内赚取利润20万余元,参赌人员达到180人,总流水金额达到541万余元。11月17日,记者从海口市秀英区人民检察院获悉,上述案件经检察院提起公诉,黄某某因犯开设赌场罪被判处有期徒刑三年,缓刑四年,并处罚金一万二千元。

——写完了,没了,出狱了。

来源:http://www.zhihu.com/question/483752248/answer/2127520344

收起阅读 »

00后整顿职场?网传一公司反手成立“专管00后部门”

随着大批00后涌入职场,作为职场新人的他们会有什么样的表现呢?近期,“00后整顿职场”的火热话题给了我们答案。图源:网络一人顶嘴领导,全体00后被统一管理该部门除了一位周姓主管外,其余均是00后新时代同事。以后公司在职及入职的00后,不管隶属哪个部门,都由此部...
继续阅读 »

据教育部统计,2022届高校应届毕业生人数高达1076万。同时,今年也是00后的第一个毕业季。

随着大批00后涌入职场,作为职场新人的他们会有什么样的表现呢?近期,“00后整顿职场”的火热话题给了我们答案

在网传的截图中,00后反对职场PUA、拒绝加班、捍卫法定休假权。他们面对不合理的职场文化直接回怼,整顿职场违法规定,甚至一言不合就申请仲裁,即使罚款离职也无所谓。



图源:网络

然而,近日有网友向上游新闻爆料,称自己所在的公司专门设立了一个名为“新一代”的新部门,以便对所有00后员工进行统一管理。

一人顶嘴领导,全体00后被统一管理

一位IP位于广东,名为“奥莱斯”的网友爆出疑似公司发布的通知截图显示,他们公司为了方便管理,规范职场,现设立“新一代”部门。

该部门除了一位周姓主管外,其余均是00后新时代同事。以后公司在职及入职的00后,不管隶属哪个部门,都由此部门统一管理,其他部门不得插手。新部门所有规章制度均按照职场管理进行,如有违反,一律按照相关规定处罚。如果不适应该部门,可申请调离。


图源:微博

该网友进一步解释,事情的起因是一个00后同事工作效率比较低,别的员工8小时完成的工作他需要16个小时。上级领导质问他为什么别人可以完成,他完成不了。他顶嘴说:“那你叫别人做。”此后,公司以不符合公司要求将他辞退。这个事情最终经过劳动仲裁,判00后员工输了仲裁。之后,公司高层便开会讨论成立了这样一个部门。

该网友表示,成立这样的部门并不是因为00后表现不佳,而是00后和公司目前管理制度的大框架有不太契合的地方。

据了解,这个“新一代”部门将对上班迟到、早退、旷工、上班偷懒、没在规定时间完成工作等情况设置相应的处罚制度。对迟到几次算旷工,转岗甚至辞退等情况也做了说明。新部门还特别强调,请假不提前、不服从工作安排都有相应的处罚。如果员工与公司解除合同后,诋毁公司、泄露公司机密都会被起诉。


图源:微博

总而言之,新部门的宗旨是要给00后员工强调制度,所有事都按制度来办,没有弹性了。

网友:整顿职场反被整顿?

此消息一出,不少网友戏称,00后刚开始整顿职场,就被职场给整顿了。还有网友表示,公司管不住00后,才会设立这种“奇葩”部门。

支持00后的网友认为,00后站出来反抗,勇敢表示异议的做法让人敬佩

  • “又想要应届生的低薪酬,又想要他们拥有老员工的高经验高能力,想得真美,给不了高工资就不要嫌人家干事慢”

  • “我还是相信00后,有没有一种可能,那八小时的工作正常情况下本就该16小时才能完成的”

  • “为00后鼓掌,你们是内卷滚蛋的希望”

还有部分支持公司的网友则认为,没完成工作是原罪,整顿职场并不是要变成“杠精”,基本的规章制度还是要遵守的

  • “尽力做好了自己该做的,我爱横着竖着老板管不着我。但是份内工作完成得这么差劲,这人拽什么拽啊”

  • “这难道不是没做完工作还厚脸皮顶撞上级的员工的错?”

  • “拿一份工资就要干好自己的工作,这是责任。”

最后,你对此次网传事件有什么看法?欢迎留言交流~

参考链接:

来源:程序人生

收起阅读 »

IE 正式入土!网友祭出实体版墓碑...

尽管你可能早八百年就只用IE来下Chrome了,不过作为“童年回忆”,网友们对于这位老同志,感情还是非常深的。有人觉得IE靠着魂器仍存于世(手动狗头)。但也有人迫不及待给IE P起了墓碑。而据韩国网友透露,在韩国庆州,还有实体版……他曾经是个下载其他浏览器的好...
继续阅读 »

嗨胖友们,今儿IE浏览器就正式退出历史舞台了。

尽管你可能早八百年就只用IE来下Chrome了,不过作为“童年回忆”,网友们对于这位老同志,感情还是非常深的。

这不,梗图排着队就来了。


死神:IE,是时候上路了。

IE:Internet Explorer已停止工作。

有人觉得IE靠着魂器仍存于世(手动狗头)。


但也有人迫不及待给IE P起了墓碑。


而据韩国网友透露,在韩国庆州,还有实体版……

墓志铭写的是:

他曾经是个下载其他浏览器的好工具。


从96%到0.64%

值此送别之际,我们还是来回顾一下IE这位曾经的浏览器老大哥波澜起伏的一生。

IE的第一个版本Internet Explorer 1诞生于1995年8月。

第一轮网页浏览器“大战”,也就此拉开序幕:

当时,网景 (Netscape) 作为浏览器界一哥,市场占有率超过70%。

值得注意的是,那时候苹果的默认浏览器就是网景,而作为竞争对手,在IE之前微软并没有自己的默认浏览器。


通过与Windows系统捆绑的方式,IE很快就给网景造成了冲击。

特别是在1996年Internet Explorer 3——首款支持编程语言及CSS的商用浏览器推出之后,IE的市场占有率开始紧追网景。

而随着两者竞争的白热化,当时的网页设计者们还会把“用网景可获得最佳效果”、“用IE可获得最佳效果”的标志放在主页上,甚至由此触发了名为 Viewable With Any Brower (可用任何浏览器浏览)的运动。

1998年,背靠财大气粗的微软,IE正式斩网景于马下。后者在这一年年底被美国网络公司美国在线 (AOL) 收购。

此后,IE一路高歌。到了2002年,其市场份额达到了惊人的96%,可以说占据了浏览器领域的绝对统治地位。


只是IE成功了,却也懈怠了。

IE 6.0版本2001年推出,而其下一代版本却直到2005年才与用户见面,IE6也成为该系列产品中生命周期最长的一个版本。

并且在这长达5年的时间内,IE6不断受到用户诟病——运行速度慢不说,安全漏洞还层出不穷……

而网景虽败,却仍留下了一点星星之火。

就在败退的1998年,在网景资助下,Mozilla组织成立。

没错,就是火狐浏览器(Mozilla Firefox)名字里的那个Mozilla。

这也是为什么,火狐被认为是网景的“精神续作”。

2004年,火狐推出1.0版本。到了2005年,IE市占率就在火狐的冲击之下,跌至85%。

如此竞争压力之下,微软也终于开始重拾第一轮浏览器之战时的创新动力,加快IE 7.0版本的研发。

但更强大的竞争对手很快就出现了——

2008年,谷歌推出Google Chrome浏览器,同时推出对应开源版本Chromium。

Chromium对现今浏览器的影响不消多说:

如今替代了IE Windows系统默认浏览器之位的Microsoft Edge,在2020年也已经改为基于Chromium开发。


来自StarCounter

Chrome的强势登场,成为了压死骆驼的最后一根稻草。

2015年,IE的市场占有率已经跌破20%。这一年的微软Build开发者大会上,微软也开始舍弃IE,宣布用Microsoft Edge替代IE,成为新的Windows系统默认浏览器。

2016年,微软宣布将会停止发布Internet Explorer 11之前版本的安全更新。

2020年,微软宣布将陆续停止对IE的支持。

2021年,微软预告了IE的“寿终正寝”。

而现在,告别时刻已至。

数据显示,在上个月,IE全球市场份额仅余0.64%。

最后问一句: 你们的应用现在还要求支持IE吗? 留言区说说呗!

来源:量子位

收起阅读 »

解决mpvue小程序分享到朋友圈无效问题

手动修改一下mpvue这个包,在node_modules里面找到mpvue在index里面搜索下onShareAppMessage找到// 用户点击右上角分享onShareAppMessage: rootVueVM.$options.onShareAppMes...
继续阅读 »

手动修改一下mpvue这个包,在node_modules里面找到mpvue在index里面
搜索下onShareAppMessage找到

// 用户点击右上角分享
onShareAppMessage: rootVueVM.$options.onShareAppMessage
? function (options) { return callHook$1(rootVueVM, 'onShareAppMessage', options); } : null,

在这一段代码下面添加一个处理就可以了

// 分享朋友圈
onShareTimeline: rootVueVM.$options.onShareTimeline
? function (options) { return callHook$1(rootVueVM, 'onShareTimeline', options); } : null,

最好也在LIFECYCLE_HOOKS这个数组中把onShareTimeline这个添加进去

var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated', 'onLaunch',
'onLoad',
'onShow',
'onReady',
'onHide',
'onUnload',
'onPullDownRefresh',
'onReachBottom',
'onShareAppMessage',
'onShareTimeline',
'onPageScroll',
'onTabItemTap',
'attached',
'ready',
'moved',
'detached'
];

然后打包,完美解决

如果项目中因为页面问题引入了例如mpvue-factory这种插件的还需要处理一下,用下面这个文件去处理吧,两个问题一起处理。

再高级一点的话,可以写一个fix命令,复制我下面的,放到build文件夹,检查下你们的相对路径是不是对的,不对的话改一下你们的文件目录指向,然后自己去package里面加命令执行这个文件,直接命令跑一下就可以

var chalk = require('chalk')
var path = require('path')
var fs = require('fs')
var data = ''
var dataFactory = ''

const hookConfig = '\'onShareAppMessage\','
const hookFn = '// 用户点击右上角分享\n' +
' onShareAppMessage: rootVueVM.$options.onShareAppMessage\n' +
' ? function (options) { return callHook$1(rootVueVM, \'onShareAppMessage\', options); } : null,'
const mpVueSrc = '../node_modules/mpvue/index.js'
const mpVueFactorySrc = '../node_modules/mpvue-page-factory/index.js'

const factoryHook = 'onShareAppMessage: App.onShareAppMessage ?\n' +
' function (options) {\n' +
' var rootVueVM = getRootVueVm(this);\n' +
' return callHook$1(rootVueVM, \'onShareAppMessage\', options);\n' +
' } : null,'
try {
data = fs.readFileSync(path.join(__dirname, mpVueSrc), 'utf-8')
if (data.indexOf('onShareTimeline') === -1) {
data = replaceHook(data)
}
fs.writeFileSync(path.join(__dirname, mpVueSrc), data)
} catch (e) {
console.error(e)
}

try {
dataFactory = fs.readFileSync(path.join(__dirname, mpVueFactorySrc), 'utf-8')
if (dataFactory.indexOf('onShareTimeline') === -1) {
dataFactory = replaceFactoryHook(dataFactory)
}
fs.writeFileSync(path.join(__dirname, mpVueFactorySrc), dataFactory)
} catch (e) {
console.error(e)
}

// 处理mpvue框架中没有处理onShareTimeline方法的问题
function replaceHook(str) {
let res = str.replace(hookConfig, '\'onShareAppMessage\',\n' +
' \'onShareTimeline\',')
res = res.replace(hookFn, '// 用户点击右上角分享\n' +
' onShareAppMessage: rootVueVM.$options.onShareAppMessage\n' +
' ? function (options) { return callHook$1(rootVueVM, \'onShareAppMessage\', options); } : null,\n' +
'\n' +
' // 分享朋友圈\n' +
' onShareTimeline: rootVueVM.$options.onShareTimeline\n' +
' ? function (options) { return callHook$1(rootVueVM, \'onShareTimeline\', options); } : null,')

return res
}

// 处理mpvue-factory插件中没有处理onShareTimeline方法的问题
function replaceFactoryHook(str) {
let res = str.replace(factoryHook, 'onShareAppMessage: App.onShareAppMessage ?\n' +
' function (options) {\n' +
' var rootVueVM = getRootVueVm(this);\n' +
' return callHook$1(rootVueVM, \'onShareAppMessage\', options);\n' +
' } : null,\n' +
'\n' +
' // 用户点击右上角分享\n' +
' onShareTimeline: App.onShareTimeline ?\n' +
' function (options) {\n' +
' var rootVueVM = getRootVueVm(this);\n' +
' return callHook$1(rootVueVM, \'onShareTimeline\', options);\n' +
' } : null,')
return res
}
console.log(chalk.green(
' Tip: fix mpvue_share Success!'
))


原文链接:https://blog.csdn.net/weixin_41961749/article/details/107402802


收起阅读 »

内卷的天花板,新东方的直播间

现在的直播间充斥着大量低俗、搞笑、无脑的行为,而当一群训练有素的老师出现时,他们充分发挥了聊天扯淡的本事,扯哲学、扯到苏格拉底;聊理想,能聊到阿拉斯加鳕鱼。不一定非常深刻,但气氛一定要烘托到位,而这对其他主播简直是降维打击。01 直播间的一股清流人们印象中的直...
继续阅读 »


现在的直播间充斥着大量低俗、搞笑、无脑的行为,而当一群训练有素的老师出现时,他们充分发挥了聊天扯淡的本事,扯哲学、扯到苏格拉底;聊理想,能聊到阿拉斯加鳕鱼。不一定非常深刻,但气氛一定要烘托到位,而这对其他主播简直是降维打击。

01 直播间的一股清流

人们印象中的直播间卖货是怎样的?

“家人们,买不了吃亏,买不了上当。今天最后一百单,全网最低价”。

“三二一,上链接”。

“OMG,买它买它”。

但在新东方的东方甄选直播间,看到的却是完全不一样的画风。

一位长相酷似陕西兵马俑的老师,苦口婆心地劝说直播间的网友:听了一早上了,买一单吧。答应我,不要空手走好吗?


结果,有网友说,不说英语不买。

于是,这位自称是中关村吴彦祖的兵马俑老师马上切换到了英语模式。

“I can speak English all the time.But please remember to buy something,OK?I am now introduce to this steak……”

接着,他拿起一块与他的脸一样大的牛排,开始用英语讲解,尽管评论区很多人未必听的懂,但看起来津津有味,估计平时上课也没有这么认真过。

一款冰冻的虾也被他讲出了高级感。“因为是急速冷冻,所以包裹着薄薄的冰衣,叫coated in thin ice。所以the taste、flavor and nutrition are well preserved。”

这个时候就开始讲解flavor与taste的不同,preserved与protected的不同。

“一时之间,我不知道该下单,还是该记笔记。”一位网友说。

别人的直播间都是“家人们,宝宝们”,这里都是ladies and gentlemen。


当你以为卖货直播间里只能学英语?

你会发现远远低估了这些转型老师的才能。他们开始讲起了一堆人生哲理。

“今天是6月10日,再过11天,太阳将会到达地球的最北端,那个时候整个北半球会享受最慷慨的阳光,普照的阳光就如爱一样慷慨与自由,让万物竞相自由生长。北极会达到极昼,太阳不会落下,正如你对你家人的爱,你对你朋友的爱,落落大方。右下角一号链接,晒透了阳光的水蜜桃,500单。”


光听前半段还以为在深情地诗歌朗诵,谁能想到带货主播都卷成这样了。

即便是公认很难在直播间销售的书籍,也被这帮老师吹得天花乱坠。当时卖一本7斤重的百科全书,售价一百多元。

主播讲到了巴塞罗那的圣家堂,讲到了高迪,讲到了高迪的名言“直线属于人类,曲线属于上帝”。然后告诉你,你的孩子需要这么一本重达7斤的百科全书,这是打开自然大门的钥匙。


“生命之美就在于理解和欣赏,不要把人类的狂妄定义为征服者,那些是愚蠢的,愚蠢的人终将付出代价。”

“人生很短暂,你花了太多时间回答不应该回答的问题,花了太多时间去关注不应该关注的情绪,花了太多时间去忧虑不应该忧虑的痛苦。但你却很少花一个下午,泡一杯柠檬红茶,翻一本书,看着石头、看着花草,讲述你小时候成长的故事。”

上来就是跨跨一顿高能输出,结果,话还没讲完,700本书已经卖空了。就是这么一本在其他直播间很难卖掉的百科全书,新东方累计卖了2.2万本。

除了卖货,也还能听到相声。

那位被网友亲切称呼为兵马俑老师说:“12 pieces of steak”

俞敏洪就在旁边抬杠:为什么牛排不能加“S”?

兵马俑老师又说:“24 bags of seasonings”

俞敏洪仿佛安装了ETC:为什么调料要加“S”?

早已经厌倦了大吼大叫卖货的网友,仿佛发现了一个宝藏,弹幕也变得异常活跃和欢乐。


“说英语,说英语,我是来学英语的。”

“直播都这么卷了吗,让其他主播怎么活。”

“新东方转型了,好像又没有完全转。”

“网购的尽头是学英语。”

“全网唯一一个中英文双语直播卖货的主播。”


在直播道路上摸索了半年后,新东方貌似找到了开启直播大门的钥匙。而这把钥匙好像一般人还学不来。

02 老师们的降维打击

现在的直播间充斥着大量低俗、搞笑、无脑的行为,而当这些训练有素的老师出现在直播间时,对其他人简直是降维打击。

他们在充分发挥聊天扯淡的本事,扯哲学、扯到苏格拉底、尼采,扯到柴可夫斯基,天南海北,不一定非常深刻,但气氛一定要烘托到位。

“当你背单词的时候,阿拉斯加的鳕鱼正跃出水面;当你算数学的时候,南太平洋的海鸥正掠过海岸;当你晚自习的时候,地球的极圈正五彩斑斓。但少年,梦要你亲自实现,世界你要亲自去看,未来可期,拼尽全力。当你为未来付出踏踏实实努力的时候,那些你觉得看不到的人和遇不到的风景,都终将在你的生命里出现。”


是不是很熟悉,好像在哪里听过。这些话其实是这些老师们平时演讲上课的一部分,只不过当他们出现在直播间卖货时,让人耳目一新。

而讲段子、说鸡汤是新东方老师的基本功。

上一位在抖音直播上大火的是罗永浩也曾经是新东方的老师。尽管老罗早已不在新东方,但他在新东方上课的语录流传至今。

“女生就这点不好,你吵不过可以打嘛,打不过可以不打嘛!干什么去打小报告呢?”

“我走来走去,为中国的命运苦苦思索。”

“不被嘲笑的梦想,是不值得去实现的。”

那个年代还没有短视频,但很多学生把声音录下来,到处传播。

很多人可能还看过罗永浩写给俞敏洪的万字求职信。老罗把自己的幽默和段子手的天赋展现得淋漓尽致。

“我到新东方来应聘不是来做教师的,是来做优秀教师的,所以不适合以常理判断。”

而那位兵马俑老师本名叫董宇辉,据我们了解,大学没毕业就去了新东方,工作至今,教过的学生超过50万,浑身上下都散发着新东方的味道。

当这些风趣幽默、口才极佳的新东方老师被解放出来时,与直播卖货很好地结合到了一起,以上课的方式卖货,不拼低价,不用大喊大叫,对其他主播简直是一场降维打击。

老罗在直播间证明了自己,俞敏洪用一个更加差异化的方式也证明了自己。

03 俞敏洪最难的日子要过去了

半年前,俞老师刚开始转型直播带货时,其实一直不温不火。

当时新东方成立“东方甄选”农产品直播电商平台。俞敏洪也在抖音进行了首场农产品带货直播。

但除了首秀表现不错外,东方甄选每天的销售额确实少得可怜。数据显示,从2021年12月29日到2022年2月20日的29场直播中,俞敏洪和东方甄选的销售额,普遍停留在30万元以内。

东方甄选卖的东西并不便宜,选的都不是能走量的东西,包括南美大虾、阿根廷牛排、五常大米、进口橄榄油,与那些网红产品或者9.9元包邮的商品完全不是一个路数。

这也导致了直播间一开始并不热闹,热度维持在几百左右。货也卖得很少,新东方的财报披露,试水了两个月的直播新事业,只卖出450万元。当时还有网友建议说俞老师去上一下罗永浩的直播课。

新东方转型农产品直播卖货,是一个从开始就不被看好的事业。当时一家央媒还刊发了评论文章,指出客观条件决定了农货直播从市场到监管挑战极大,新东方要从校外培训跨界到直播电商并不轻松。

但里面有一句话争议很大,认为新东方是在“从一个挣快钱的行业跳到另一个挣快钱的行业”。俞敏洪虚心接受了批评,但专门针对这句话提出了不同意见。

“商业模式不存在快钱和不快钱,教育领域做起来也挺艰难的,很多教育公司原来都是赔钱的。直播也是不容易的。我知道大主播一直播就是十几个小时甚至20个小时,没日没夜地选品,没日没夜地努力,所以其实没有一项行业是好做的。”

那是新东方最艰难的时刻。2021年7月,教育“双减”政策落地,K9业务全军覆没。新东方从11万人减少到5万人,俞敏洪把新东方学校的7万多套新桌椅捐赠给了当地的农村学校。在退场时保留了难得的体面。

那些日子,俞敏洪和新东方的传闻很多。有人在微信群里造谣说新东方要去公海搞游轮游学,把新东方放在火上烤,急得俞敏洪赶紧出来辟谣。后来又传出俞老师在开会的时候哭了。

但从今年6月开始,东方甄选直播间的在线观看人数就过万了。那位有着70后长相、实际是90后的主播感慨:以前直播间也就几百人,一早上只能卖出去20单大米。但在6月10日,在线观看人数突破了十万人,还没开始介绍产品,直接就卖光了。


蝉妈妈数据也显示,东方甄选直播间近一个月销售额高达3084.8万元,其中仅6月10日的销售额就达735.6万元。

本周,俞敏洪又组建了“新东方直播间”队伍,销售的是新东方的教育产品,包括图书、学习硬件、课程等。

老罗在直播间完成了《真还传》,被无数人质疑的俞敏洪也终于在尝试半年后,直播事业步入正轨。接下来,就像刘耕宏带火了健身直播一样,新东方这种双语直播卖货方式,估计也会成为竞相模仿的对象。看来,主播们要抓紧时间学点英语了。

而截至6月10日收盘,新东方在线收盘价6.23港元,暴涨近40%,股价创了年内新高的同时,单日涨幅也居港股主板第一。


不过,现在说俞敏洪的直播事业成功了,或许为时尚早,但新东方老师的直播至少说明了一点:

这个世界,不仅属于好看的皮囊,有趣的灵魂永远都有市场。

来源:数智前线

收起阅读 »

使用正则表达式解析短信内容

使用正则表达式解析短信内容 通常,Android手机自带的短信软件都可以将解析内容并且提取出里面的关键信息展示成卡片的样式或者提供让用户进一步操作的按钮。例如在坚果手机上验证码短信会展示成这样: 信用卡的消费短信会展示成这样: 这篇文章将讨论如何基于正则表...
继续阅读 »

使用正则表达式解析短信内容


通常,Android手机自带的短信软件都可以将解析内容并且提取出里面的关键信息展示成卡片的样式或者提供让用户进一步操作的按钮。例如在坚果手机上验证码短信会展示成这样:


WechatIMG215.jpeg


信用卡的消费短信会展示成这样:


WechatIMG216.jpeg
这篇文章将讨论如何基于正则表达式实现类似上述的功能。


一、需求分析




  1. 需要在卡片上展示标题 标题可能是固定的比如像图上的信用卡消费或者是验证码,也可能是不固定,比如说来自短信的内容




  2. 卡片上将会醒目的展示出最核心的内容和类型,例如图上验证码信息和金额信息




  3. 其余的次重要信息按照键值对的形式分别展示出来,例如消费短信中的 账号、短信、时间等信息。




  4. 是否需要展示短信的原文内容,这个部分考虑支持可配。让使用者去决定当前的短信卡片是否要展示原文。




  5. 短信卡片展示的下一步动作,我们暂时仅考虑支持复制卡片上最醒目的信息,例如验证码,或者是消费金额(尽管好像没什么卵用)




二、关于正则


在整个短信解析的正则中最重要的就是捕获组的概念。


我们首先来看一下京东快递柜的短信提醒


【京东快递柜】凭取件码26558117一个小区京东快递柜取件,电话12345678910,关注京东快递公众号扫码取件。


在这个短信中除了加粗的部分之外,其余的内容都是固定的,所以我们很容易的可以写出下面这样的正则表达式


【京东快递柜】凭取件码\d{8}到.*?京东快递柜取件,电话\d{11},关注京东快递公众号扫码取件。

我们需要在卡片上展示短信中的内容,我们要用到捕获命名组,我们加上命名捕获组后正则表达式会是下面这样


【京东快递柜】凭取件码(?<code>\d{8})到(?<location>.*?)京东快递柜取件,电话(?<phone>\d{11}),关注京东快递公众号扫码取件。

我们就可以在代码中通过名称组名来获取对应的内容。


三、规则数据存储


基于以上的需求我们就可以设计出如下的结构来存储一个短信内容的解析规则,毕竟一个正则是没有办法解析所有的短信的。


    filter       '过滤器,如果短信的内容符合过滤的规则,则使用 regex记录的正则来提取数据',
regex '用于数据提取的正则表达式',
group_names '内容分组的名称 使用 , 分割 ,例如记录上述京东快递柜短信提醒组的名称: code|取件码,location|位置,phone|联系电话'
sort '排序,规则的先后顺序,满足一个则后面的规则默认不在参与处理',
title '标题,标题可以写规定的内容,也可以引用捕获组的内容 ,例如 #P#code 使用捕获到组名为code的文本作为短信卡片的标题'
show_content '是否显示原文'
copy_main '是否可以复制名为 mian 组捕获到的内容到剪贴板'

在group_names 字段中,默认显示在卡片最醒目位置的文本的捕获组的名称命名为 main,例如快递柜通知短信要显示的最醒目的内容是取件码,那么取件码的组名就是 main


四、代码实现


有了上面的结构,对短信的解析就可以简单处理为让短信按照定义的sort字段的顺序,逐个匹配 filter记录的正则表达式,如果通过,则使用regex记录的正则提取数据,并且group_name将内容与记录的标题对应,给页面去渲染就可以了。


代码示例如下:


function resolveSMSContent(content, extractRules) {
for (let i = 0; i < extractRules.length; i++) {
let extractRule = extractRules[i];
let filter = eval(extractRule.filter);
let exec;
if (filter.test(content)) {
let patternStr = eval(extractRule.regex)
let mainContent;
let title = extractRule.title;
let groupNames = extractRule.groupNames.split(",");
let values = [];
patternStr = eval(patternStr)
exec = patternStr.exec(content);
for (let i = 0; i < groupNames.length; i++) {
let groupName = groupNames[i].split("|")[0];
let param = {
key: groupNames[i].split("|")[1],
text: exec.groups[groupName]
}
if (extractRule.title.startsWith("#P#") && extractRule.title.replace("#P#", "") === groupName) {
title = exec.groups[groupName];
continue;
}
if (groupNames[i].split("|")[0] === 'main') {
mainContent = param;
continue;
}
values.push(param)
}
let card = {
title: title,
mainContent: mainContent,
content: content,
param: values,
copyMain: extractRule.copyMain,
showContent: extractRule.showContent
}
if (card.copyMain) {
writeContentToClipBoard(card.mainContent.text);
}

console.log(card);
return card;
}

return '';
}

解析之后的数据:


{
"title": "京东快递柜", //卡片标题
"mainContent": {
"key": "取件码", //展示关键信息的名称
"text": "26558117" 关键信息的内容
},
"content": "【京东快递柜】凭取件码26558117到一个小区京东快递柜取件,电话12345678910,关注京东快递公众号扫码取件。", //短信原文
"param": [ //次重要参数
{
"key": "联系电话", //名称
"text": "12345678910" //内容
},
{
"key": "位置",
"text": "一个小区"
}
],
"copyMain": false //是否可以复制关键信息
}

有了上面的数据就可以很容易的展示成下面这样:


Xnip2022-06-05_00-27-45.jpg


源代码:Util.js#resolveSMSContent


代码来自于Blue bird 这是一个将Android手机电量,短信,手机通知等数据发送给电脑端的项目,有兴趣可以自己打包部署使用一下,欢迎star


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

多线程原理和常用方法以及Thread和Runnable的区别

多线程原理 随机性打印 CPU有了两条执行的路径,CPU就有了选择 ,一会执行main方法 一会执行run方法。 也可以说两个线程,一个main线程 一个run线程 一起请求CPU的执行权(执行时间)谁抢到了就执行对应的代码 多线程内存图解 main...
继续阅读 »

多线程原理


随机性打印


CPU有了两条执行的路径,CPU就有了选择 ,一会执行main方法 一会执行run方法。
也可以说两个线程,一个main线程 一个run线程 一起请求CPU的执行权(执行时间)谁抢到了就执行对应的代码
01_多线程随机性打印结果.bmp


多线程内存图解



  1. main方法的第一步创建对象,创建对象开辟堆内存存储在堆内存中(地址值赋值给变量名0x11)

  2. mt.run()调用时 run方法被压栈进来 其实是一个单线程的程序(main线程,会先执行完run方法再执行主线程中的去其他方法)

  3. mt.start()调用时会开辟一个新的栈空间。执行run方法(run方法就不是在main线程执行,而是在新的栈空间执行,如果再start会再开辟一个栈空间再多一个线程)


对cpu而言,cpu就有了选择的权利 可以执行main方法、也可以执行两个run方法。
多线程好处:多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间,多个线程互不影响 进行方法的压栈和弹栈。
02_多线程内存图解.bmp


Thread类的常用方法


获取线程名称 getName()


public static void main(String[] args) {
//创建Thread类的子类对象
MyThread mt = new MyThread();
//调用start方法,开启新线程,执行run方法
mt.start();

new MyThread().start();
new MyThread().start();

//链式编程
System.out.println(Thread.currentThread().getName());
}

/**
获取线程的名称:
1.使用Thread类中的方法getName()
String getName() 返回该线程的名称。
2.可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
static Thread currentThread() 返回对当前正在执行的线程对象的引用。
* @author zjq
*/
// 定义一个Thread类的子类
public class MyThread extends Thread{
//重写Thread类中的run方法,设置线程任务
@Override
public void run() {
//获取线程名称
//String name = getName();
//System.out.println(name);

//链式编程
System.out.println(Thread.currentThread().getName());
}
}

输出如下:


main
Thread-2
Thread-0
Thread-1

设置线程名称 setName() 或者 new Thread(“线程名字”)




  1. 使用Thread类中的方法setName(名字)
    void setName(String name) 改变线程名称,使之与参数 name 相同。




  2. 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字


     Thread(String name) 分配新的 Thread 对象。



代码案例:


//开启多线程
MyThread mt = new MyThread();
mt.setName("小强");
mt.start();

//开启多线程
new MyThread("旺财").start();

使当前正在执行的线程以指定的毫秒数暂停 sleep(long millis)


代码案例:


public static void main(String[] args) {
//模拟秒表
for (int i = 1; i <=60 ; i++) {
System.out.println(i);

//使用Thread类的sleep方法让程序睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

创建多线程程序的第二种方式-实现Runnable接口


实现Runnable接口实现多线程的步骤:



  1. 创建一个Runnable接口的实现类

  2. 在实现类中重写Runnable接口的run方法,设置线程任务

  3. 创建一个Runnable接口的实现类对象

  4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象

  5. 调用Thread类中的start方法,开启新的线程执行run方法


代码案例如下:


/**
* 1.创建一个Runnable接口的实现类
* @author zjq
*/
public class RunnableImpl implements Runnable{
//2.在实现类中重写Runnable接口的run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}


public static void main(String[] args) {
//3.创建一个Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t = new Thread(run);//打印线程名称
//5.调用Thread类中的start方法,开启新的线程执行run方法
t.start();

for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}

Thread和Runnable的区别


实现Runnable接口创建多线程程序的好处:



  1. 避免了单继承的局限性


一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类。
实现了Runnable接口,还可以继承其他的类,实现其他的接口。



  1. 增强了程序的扩展性,降低了程序的耦合性(解耦)


实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)。
实现类中,重写了run方法:用来设置线程任务。
创建Thread类对象,调用start方法:用来开启新线程。


使用匿名内部类开启线程


匿名内部类开启线程可以简化代码的编码。
代码案例如下:


/**
匿名内部类方式实现线程的创建

匿名:没有名字
内部类:写在其他类内部的类

匿名内部类作用:简化代码
把子类继承父类,重写父类的方法,创建子类对象合一步完成
把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

格式:
new 父类/接口(){
重复父类/接口中的方法
};
* @author zjq

*/
public class Demo01InnerClassThread {
public static void main(String[] args) {
//线程的父类是Thread
// new MyThread().start();
new Thread(){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"詹");
}
}
}.start();

//线程的接口Runnable
//Runnable r = new RunnableImpl();//多态
Runnable r = new Runnable(){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"线程");
}
}
};
new Thread(r).start();

//简化接口的方式
new Thread(new Runnable(){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"zjq");
}
}
}).start();
}
}

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

第一批00后,遇到“史上最难就业季”

梦碎“实在对不起,我们部门没有HC(Headcount,坑位)了。”面对肖迪,某大厂运营部的主管露出抱歉的表情,“希望你可以继续努力,如果以后有机会的话,HC还会给到你。”这顿午餐吃的是新疆炒米粉,又麻又辣,是主管请的,“像是一种补偿”,但肖迪觉得滋味全无。她...
继续阅读 »

外部世界称呼肖迪和她的同龄人为“千禧年出生的一代”。知乎上“回忆千禧年”的话题下,有人写下那一年中国加入了世界贸易组织,县城广场上有人举着国旗庆祝。那一年也有亚洲杯,半决赛中国队碰上了当时最强的日本队,一度遥遥领先。更重要的是,前有腾讯、阿里巴巴、百度成立,后有搜狐、新浪、网易在美国上市,中国的互联网进入1.0时代,一切看起来欣欣向荣,充满希望。那为什么到了自己毕业这一年,会如此艰难?找工作的00后们都想知道答案。

梦碎

2022年夏天,00后们求职梦碎,只需要一句话。

“实在对不起,我们部门没有HC(Headcount,坑位)了。”面对肖迪,某大厂运营部的主管露出抱歉的表情,“希望你可以继续努力,如果以后有机会的话,HC还会给到你。”

肖迪是00后,就读于西北某大学的汉语言文学专业,去年12月份来到这家大厂实习。今年4月25日,午餐时间,她再次询问主管关于HC的消息,对方以最直接的方式浇灭了她的希望。

这顿午餐吃的是新疆炒米粉,又麻又辣,是主管请的,“像是一种补偿”,但肖迪觉得滋味全无。她浑浑噩噩回到工位,隔壁的员工们正在聊天,她无心参与——在平时,她一定是加入话题最积极的那个。她不断暗示自己,不要在办公室哭,不要被情绪绑架,要体面,“我也是有自尊的”。

因为手头上还有两三个项目,那天,肖迪还是加班到晚上11点,离开的时候,大厦顶端的logo十分耀眼,从前,她看到这个logo会不自觉地笑,但那一刻,她哭了。她想打电话给远在陕西的父母倾诉一场,可这个念头很快就打消了,“之前我对他们把话说得太满了,现在不想让他们担心”。

一个人从陕西来北京实习时,肖迪的父母就一直放心不下,觉得她单靠个人,无法在北京立足。但肖迪觉得,从公司接收到的一直是积极的信号。“今年部门有一个HC,很大几率就是你的。”来实习后,肖迪就多次从直属领导口中听到这样的话。4月初,她还拿着和直属领导的聊天截图给父母看:“你们看,没有意外,我是可以留下来的。”父母这才放下心来。但没想到,这份工作最后还是出了意外。

就业艰难,同为00后的程露对此并非毫无察觉。

交通是对疫情最为敏感的行业,2021年初,正在找实习岗位的程露发现,几乎所有的航空公司都停止了招聘。昆明航空一个只招10人的空乘岗,就有超过1500人来竞争。程露听朋友聊起那场面试,顺序全凭先来后到,早上八九点才开始面试,凌晨三四点就有应聘者在打地铺等候。

程露是听着飞机的轰鸣声长大的。2000年,她出生在昆明机场航站楼附近的一个村庄里,父母都在机场国内出发层做保洁员,大她十多岁的哥哥也在机场附近开出租车,整个家庭的生存都与机场联系紧密,在考虑未来出路时,程露的设想也是:找一份和航空有关的工作。


▲ 图 / 视觉中国

高中毕业后,程露进入昆明的一所大专,就读于服务专业。在这里,许多学生曾靠着分配,或是参与学校与机场的一些就业合作项目,留在航空公司,成为一名乘务员。今年也是程露的毕业年,若不出意外,她也会找一个在昆明机场的实习机会。

疫情让程露脱离了原定的轨迹。往年,她所在专业的三四十人,总有四五个能顺利进入航空公司,但行业萧条,去年6月,学校原定的机场实习转正项目取消了,程露没有料到,她和同学们被分配到了湖南的一个平板电脑制造厂里,干起了拧螺丝的活。发通知那天,学校领导劝她们:“现在能找份谋生的工作不错了,放低要求,沉住气。”

厂里又闷又热,防护服将程露浑身上下包得严严实实。早上9点到晚上10点,她站到几乎要晕厥。这样强度的工作,一个月的实习工资只有两千多元。十多个同学受不了这苦,第一天就整理好行李,偷偷逃走了。

程露原本想再熬一熬,再向学校老师打听进航空公司的门道,但那时,找中介成了唯一的通道。想进东航、南航这样的头部航空公司,少说得花上四五万的中介费。中介只是第一关,要想留在航空公司,还要交三个月的培训费,程露打听了一下,昆明航空的培训费要一两万元,就算中途淘汰也一概不退还。她不好再张口向父母要钱,彻底打消了留在航空公司的念头。

实习三天后,因为长期拿着钻头,程露的双手疼到无法握住筷子。她不想回家,老家的女性们大都在20岁左右相亲、结婚,“我不想这么早就嫁人”。那晚,程露用半个小时收拾好行李,再花了100元,买了一张前往深圳的最便宜的夜间车票,投奔朋友,“先去找份能干下去的工作”。

有多拼?

1076万,是今年教育部统计的毕业生人数,创下历史新高。去年,这个数字是909万。这1076万里,就包括肖迪和程露这样的本科应届生,她们恰好也是第一批“00后”。

00后们没有料到,踏入真实社会的第一年,会面临这么多的变故和打击:经济增长有压力,疫情在延续,竞争在增加,HC却在收紧,大家的起点似乎被拉平,无论是高职大专生,还是985的毕业生们,都要闯一闯“就业难”这一关。

HC什么时候开始收紧,谁也无法说出一个准确的时间点,但能被感知到的震荡,是从2021年底开始的,各家大厂们相继裁撤不少非营利性部门,先是“毕业式裁员”,再后来,“毁约校招生”的狼狈和不光彩也摆到了台面上。

尽管裁员风暴离自己这样近,但那时,在大厂实习的肖迪并不相信达摩克利斯剑会落到自己头上,她曾天真地想:“既然裁员,那大厂怎么还在招人呢?”

在大厂,肖迪主动揽下了许多用户运营实习本职外的活,写自媒体文案、跟进流程、做直播……原本晚上七八点的下班时间,一度延迟到午夜十二点。那几个月,她一度压力大到经期紊乱,但她觉得,只要能留下来,“累也很值”。

肖迪珍惜这次实习机会,因为在这之前,她经历过一次“得而复失”。

那是去年12月,肖迪给字节跳动和快手的新媒体运营岗投递了实习简历。不到一周,两家企业的HR都给了肯定的答复。“现在去实习,半年后可能转正。”字节的面试官曾和肖迪提过,留下来的机会很大。实习确定的那天,肖迪还特意去学校食堂的网红窗口排了40分钟的队,买了室友们想吃的菜一起庆祝。

但没想到,一场疫情在12月扼住了西安的咽喉。不到一个月,西安累计出现上千例感染者,为控制疫情进一步蔓延,12月23日起,西安实行全市封闭管理,封城也让肖迪的入职变得异常困难。出于保密的需求,字节规定,员工即便远程办公也需要使用公司的电脑,得知肖迪被困在西安,HR主动和她商量,将公司的电脑从北京快递过来。但此时,西安已掐断了物流,快递无法进城。

肖迪不甘心,问HR自己能否延迟入职。对方发了个无奈的哭脸:“不能再拖了,公司有规定。不然只能等下次的招聘了。”那晚,肖迪哭着放弃了字节的实习offer,“就感觉自己的精神垮了,曾经多么高兴,现在就有多么绝望”。到了2022年1月,收到另一家大厂的实习offer时,肖迪立刻订了去北京的机票,“不能再让意外把我困住”。


▲ 图 / 视觉中国

这样的状况下,这一届00后“卷”起来是不可避免的。上海一所985大学的夏悦,就敏锐感受到了今时不同往日。

夏悦的男朋友是上海人,一开始,她一心想留在上海。聚集着金融、快消、广告就业机会的上海,也是许多毕业生梦寐以求的城市。据上海市政府统计,2021年,上海新增就业岗位63.51万个,正规就业规模创历史新高,达1084.5万人。去年6月,58同城发布的《2021年高校毕业生就业报告》显示,上海是2021届毕业生工作首选的城市,支持率达8.7%,高于同为一线城市的北京(5.4%)、深圳(7.9%)和广州(7.3%)。

但夏悦发现,今年,上海的岗位都在缩减。她向在腾讯上海公司工作的学姐打听岗位的情况,发现一些条线的HC缩了一半,商业分析岗也不例外。不少往年顺利进入头部互联网公司的学长、学姐们在校友群中透露一些风声:“今年的机会可能没有去年多,不要把鸡蛋放在一个篮子里。”

连学校招聘经验分享会的主题也在透露一种讯息,去年还是“教你拿到某厂offer”,今年就变成了“教你offer大满贯”,这意味着,只拿到一家大厂的offer,也不意味着顺利“靠岸”。她还记得秋招一次线上宣讲会上,几名热心的直系前辈分享了自己的面试经验,会议结束后,他们的微信立刻出现了几十条好友申请,都是来要内推名额的。

无奈之下,除了腾讯,夏悦还瞄准了三四家上海公司,投简历和准备面试就填满了她封控在家的时间。即便无法准确得知互联网到底发生了什么,她还是早早给自己的求职顺位排了序:互联网商业分析岗、咨询业、快消,每个大类又细分到了具体的部门和岗位。根据每个岗位要求的不同,她把自己的简历改了侧重点各异的好几份,“要给自己留条后路”。

没有找到心仪的工作,夏悦只能把目光从上海投向别的城市。那段时间,所有陌生来电都让她紧张,“以往我是很少接陌生来电的”。她还把自己的邮件设置为有声提醒,生怕错过邮件。


▲ 图 / 视觉中国

她也为自己上海毕业生的身份感到担忧。上海被封控后,有企业甚至直接放弃了上海的求职者。一名互联网公司的HR告诉每日人物,当时公司要求给求职者发通知时,一定要询问所在地点,“一看你现在在上海,立马就pass了,毕竟也没办法立刻入职”。

4月28日18:00左右,一个显示“广东深圳”的来电让夏悦赶紧放下了手中的筷子。进行了约莫半个小时的面试后,她回到电脑前,不断刷新着招聘网站,“我有种预感,很快就会出结果”。没过多久,系统上申报流程的红点就从“HR面”跳到了“录用评估中”——“按往年的经验,这基本是有戏了。”

但6月1日这天,HR给夏悦来了电话:“不好意思,因为业务调整,我们无法给你offer了。”这一天是上海全面复工复产的日子,路上全是期待解封已久的行人,不少孩子拿着气球奔跑在路上,这天也是属于他们的节日。夏悦走在许久没有走过的熟悉街道上,只觉得“欢乐不是属于自己的”。

求职继续

肖迪曾被家人寄予厚望。她家在毗邻西安的一个小城市,去一线城市,或是至少留在省会工作,是父母对她的期待。她自己心气更高,社交平台常常给她推大厂的实习帖,对她来说,那是另一个世界:光鲜,有吸引力。她想留在北京,去探一探潮水的方向。

外部世界称呼肖迪和她的同龄人为“千禧年出生的一代”。知乎上“回忆千禧年”的话题下,有人写下那一年中国加入了世界贸易组织,县城广场上有人举着国旗庆祝。那一年也有亚洲杯,半决赛中国队碰上了当时最强的日本队,一度遥遥领先。更重要的是,前有腾讯、阿里巴巴、百度成立,后有搜狐、新浪、网易在美国上市,中国的互联网进入1.0时代,一切看起来欣欣向荣,充满希望。

那为什么到了自己毕业这一年,会如此艰难?找工作的00后们都想知道答案。

肖迪本想归咎于疫情,但同样在西安的朋友拿到了美团的offer后,她感觉失去了借口,一度只能从自己身上寻找原因:“我不该浪费秋招的时间去考研。”

她就读的西北某大学以地质学和自然科学见长,学汉语言文学的她,在同校学生的择业竞争里,并不具备优势,大三秋招期间,班上2/3的同学都在准备考研。她跟许多考研的同学聊过,大家的判断都是,以自己的学历和专业,是无法找到满意的工作的,既然就业严峻,不如再等一年。但一个意外是,今年“考研也特别卷”,400高分的落榜者比比皆是,考研失利那一刻,肖迪才意识到,“秋招失去的时间和机会是回不来了”。

可即便是拥有985的“原生学历”,夏悦也并不觉得轻松。她从大二就开始实习,密度高达每半年一次,一张A4纸已经放不下所有的经历。在985高校,“社会时钟”是自然被建立的,大家都遵循着“什么时候该干什么事”的准则。大二那年,夏悦就记得许多学长、学姐开始在朋友圈发“人贩贴”——一些头部公司的实习生招聘。学院也将实习算进必修的学分,实习的公司越好越头部,拿最高档绩点的可能性就越大。

她周围,不管是毕业还是继续读研,几乎人人都在朝着top努力:打算出国的同学,大三就把英语成绩刷完了,proposal(注:陈述书)都请院里最好的教授修改了好几遍;如果是国内升学,那就要保研到更好的985;至于那些和她一样,计划本科毕业后就业的同学,在大三第一个学期就几乎修完了所有的学分,剩下的时间,都留给了实习和求职。她自己也是如此,做好了应对就业的万全准备,她想不通,怎么还是这么难。


▲ 图 / 视觉中国

至于程露,她的处境更艰难一些。某种意义上,在深圳,不缺赚钱的门道。914条公交线路和12条地铁线路连接着华为、腾讯、富士康等世界500强,还有222万家带着淘金梦的创业公司、服装厂、电子厂。但好机会对程露又是吝啬的,拿着专科的学历,她只接到过酒店前台和网站客服岗的面试机会。最后,不看学历、服务员月薪能到六七千的海底捞,成了她最好的去处。

她和朋友在靠近东莞的郊区租了个一室一厅,客厅还能勉强塞下一张床,平摊后,每人每个月只需要500元。只是,她怕家里人失望,还没有告诉他们自己已经逃离流水线的事实。

不论怎样,求职还得继续。

夏悦又投了两家快消公司,顺利进入了最后一面。不过,上一份offer的毁约依然让她心有余悸。6月3日,她花了800元的高价,找一个求职机构做了咨询。看了简历后,对方肯定了她的选择。夏悦放心了一些:“这种时候,我就需要一个人来鼓励自己。”

5月初,大厂备战“618”,用户运营部更忙了,肖迪也接了不少项目,但她不知道该如何面对手头的工作,“我知道要好好交接,但是我失去了工作的动力”。5月末,她对主管领导提了离职,赶着春招的尾声,去参加了几场招聘。

留给应届生们找工作的时间不多了。5月5日,中国传媒大学学生工作部发了致用人单位的一封信,请求“通过线上方式,给学生们提供实习岗位或面试机会”;在教育部发了《关于高等学校做好2022年开发科研助理岗位吸纳毕业生就业工作的通知》后,复旦大学开始向毕业生开放校内派遣制岗位,针对“受疫情影响暂未落实去向的应届毕业生”。大学辅导员李嘉很希望自己的学生能找到工作:“应届毕业生的身份还是很有分量的,于公于私都希望学生们抓住机会。”

2021年8月,程露从深圳的海底捞离了职。她觉得自己不能一直甘于同一个地方,浪费自己学到的专业技能。她先是进了一家苹果专卖店,干着月薪只有4000元的销售工作。两个月后,她又跳槽到了一家服装公司。年末是服装销售的旺季,她每天在店铺叫卖到12点,理货到凌晨2点,工资终于涨到了六七千。但2022年3月,疫情又来了,深圳的夜晚不再热闹,再怎么熬夜,程露也达不到一天卖出2000元的KPI。

她也听哥哥说起空姐近期的遭遇。空姐的薪酬,是在飞机离地那一刻才开始计算的。一个在疫情前常年飞行的空姐,每个月能拿到一万多。而现在,昆明机场只留下了自助检票通道,成都双流机场的安检口从30个变成了3个。哥哥告诉程露,一个待业家中的空姐,每个月只能拿到两三千的保底工资。

程露索性想开了,没有进入航空业,似乎也没有多可惜。她打算在深圳休息一段时间,逛一逛这座因为工作而无法好好看看的城市后,再去寻找新的机会。

(文中肖迪、程露、夏悦、李嘉均为化名;导语图来源:泱波(江苏分社)/中新社/视觉中国)

作者:周鑫雨

收起阅读 »

还不知道npm私服?一篇教会你搭建私服并发布vue3组件库到nexus

日常工作时,出于保密性、开发便捷性等需求,或者是还在内部测试阶段,我们可能需要将vue3组件库部署到公司的nexus中。我们可能希望部署vue3组件库的操作是CI/CD中的一环。节点:npm发布依赖包安装建木CI,参考建木快速开始安装nexus搭建npm私服,...
继续阅读 »

介绍

日常工作时,出于保密性、开发便捷性等需求,或者是还在内部测试阶段,我们可能需要将vue3组件库部署到公司的nexus中。我们可能希望部署vue3组件库的操作是CI/CD中的一环。

现在建木CI有了自动发布构件的官方npm节点,这一切都将变得非常简单。

节点:npm发布依赖包

准备工作

  • 安装建木CI,参考建木快速开始

  • 安装nexus搭建npm私服,创建用户、开启token验证、生成token

1. 安装sonatype nexus

# docker search nexus 搜索nexus 下载量最高的那个sonatype/nexus3
docker search nexus

# 从docker hub中将sonatype nexus拉取下来
docker pull sonatype/nexus3

# 启动sonatype nexus并使其监听8081端口
docker run -d -p 8081:8081 --name nexus sonatype/nexus3
复制代码

访问搭建的nexus,可以看到如下界面,那么nexus搭建成功


接下来,需要登录管理员(第一次登录会提供密码,然后要求改密码),创建Blob Stores的数据池地址,供后面仓库选择


创建我们的私有npm库,需要注意的是我们要创建三个仓库(仓库名有辨识即可)

  • group见名知意,是一个仓库组,包含多个具体的仓库(proxy、hosted)

  • hosted本地仓库,就是我们内部发布包的地址

  • proxy代理仓库,会去同步代理仓库的npm包(即下载本地仓库没有的包时,会去代理仓库下载,代理仓库可设置为官方仓库)


创建proxy仓库,需要设置一些值


创建hosted仓库,需要设置一些值


创建group仓库,选择我们之前创建的两个仓库


大功告成!查看这个hosted类型的地址,建木CI流程编排需要这个地址作为参数


还需要私服的token,需要先创建一个账户,用于本地生成token


开启nexus的用户token权限验证


需要本地设置hosted类型仓库地址,npm config set registry http://xxx:8081/xxx/npm_hosted 然后npm login获取token


添加token到建木的密钥,先创建命名空间npm,在该空间下创建账户的密钥wllgogogo_token


2. 挑选节点

建木CI是一个节点编排工具,那么我们需要挑选合适的节点完成一系列的业务操作

git clone节点

使用git clone节点,将我们需要部署的前端npm包项目从远程仓库上拉取下来。git clone节点的版本,我们选择1.2.3版本

如下图:访问建木Hub可以查看节点详细信息,比如,git clone节点的参数,源码,版本说明等信息


nodejs构建节点

使用nodejs构建节点,会将我们clone下来的项目进行build构建,本文我们将用到1.4.0-16.13.0版本

如下图查看此节点的详细信息:


发布npm依赖包节点

使用发布npm依赖包节点,会将我们build后的项目发布到公服或私服,从1.1.0-16.15.0版本开始支持私服的发布

如下图查看此节点的详细信息:


3. 编排流程

节点选好了,得把它们编排在一起,目前建木CI提供了两种方式来编排节点:

  1. 使用建木CI的DSL来编排节点

  2. 使用建木CI图形化编排功能来编排节点

此次我们使用图形化编排功能编辑此测试流程(ps:图形化编排是建木CI 2.4.0推出的重磅级功能,详见「v2.4」千呼万唤的图形化编排,来了!

首先编辑项目信息


从左边抽屉中将所需的三个节点拖拽出来


填充节点参数

填充参数之前,将三个节点连起来,如图:这个箭头可以完成的功能有:

  • 定义流程运行先后顺序

  • 将上游节点的输出参数输出到下游节点,这里的git clone节点输出参数将被输出到后续所有节点


点击节点图标开始填充参数

  • git clone节点

    这里我们配置一个需要部署的 npm包 项目的 git 地址,选择1.2.3版本,改名git_clone


  • nodejs构建 节点

    同样配置此节点的必需参数

    1.节点版本:nodejs构建节点的版本选择 1.4.0-16.13.0
    2.工作目录:需要build的项目路径
    3.registry url:给包管理工具设置镜像,一般设置淘宝镜像registry.npmmirror.com/
    4.包管理器类型:根据具体项目情况来选择包管理器,这个项目构建用的是pnpm
    5.项目package.json文件目录相对路径:package.json目录相对地址,读取name和version


    nodejs构建节点的工作目录参数引用了git_clone节点的输出参数(git_clone作为上游节点将它的输出参数作为nodejs构建的输入参数传递给nodejs构建节点),下图演示了下游节点如何选择上游节点的输出参数作为自己的输入参数


  • 发布npm依赖包 节点

    1.节点版本:选择 1.1.0-16.15.0
    2.工作目录:发布包目录
    3.镜像仓库:前面准备工作nexus创建的npm本地仓库地址
    4.token令牌:前面准备工作nexus创建的用户,在本地设置hosted地址后,执行npm login生成的token


发布 npm包 构件到 nexus

启动流程

如下图启动流程


流程运行中


流程运行成功


查看每个节点的运行日志

git clone节点:


nodejs构建节点


发布npm依赖包节点


在nexus中查看部署的npm依赖包


至此,我们已经使用建木CI成功将npm依赖包部署到了nexus上!


作者:Jianmu
来源:juejin.cn/post/7109026865259479076

收起阅读 »

ASM 插桩采集方法入参,出参及耗时信息

ASM
前言 ASM字节码插桩技术在Android开发中有着广泛的应用,但相信很多人会不知道怎么上手,不知道该拿ASM来做点什么。 学习一门技术最好的方法就是动手实践,本文主要通过ASM插桩采集方法的入参,出参及耗时信息并打印,通过一个不大不小的例子快速上手ASM插桩...
继续阅读 »

前言


ASM字节码插桩技术在Android开发中有着广泛的应用,但相信很多人会不知道怎么上手,不知道该拿ASM来做点什么。


学习一门技术最好的方法就是动手实践,本文主要通过ASM插桩采集方法的入参,出参及耗时信息并打印,通过一个不大不小的例子快速上手ASM插桩开发。


技术目标


我们先看下最终的效果


插桩前代码


首先来看下插桩前代码,就是一个sum方法


    private fun sum(i: Int, j: Int): Int {
return i + j
}

插桩后代码


接下来看下插桩后的代码


    private final int sum(int i, int j) {
ArrayList arrayList = new ArrayList();
arrayList.add(Integer.valueOf(i));
arrayList.add(Integer.valueOf(j));
MethodRecorder.onMethodEnter("com.zj.android_asm.MainActivity", "sum", arrayList);
int i2 = i + j;
MethodRecorder.onMethodExit(Integer.valueOf(i2), "com.zj.android_asm.MainActivity", "sum", "I,I", "I");
return i2;
}

可以看出,方法所有参数都被添加到了一个arrayList中,并且调用了MethodRecorder.onMethodEnter方法

而在结果返回之前,则会调用MethodRecorder.onMethodExit方法,并将返回值,参数类型,返回值类型等作为参数传递过支。


日志输出


在调用了onMethodExit之后,会计算出方法耗时并输出日志,如下所示


类名:com.zj.android_asm.MainActivity 
方法名:sum
参数类型:[I,I]
入参:[1,2]
返回类型:I
返回值:3
耗时:0 ms

技术实现


上面我们介绍了最后要实现的效果,下面就来看下怎么一步一步实现,主要分为以下3步:



  1. 在方法开始时采集方法参数

  2. 在方法结束时采集返回值

  3. 调用帮助类计算耗时及打印结果


ASM采集方法参数


采集方法参数的方法也很简单,主要就是读取出所有参数的值并存储在一个List中,主要问题在于我们需要用字节码来实现这些逻辑.


override fun onMethodEnter() {
// 方法开始
if (isNeedVisiMethod() && descriptor != null) {
val parametersIdentifier = MethodRecordUtil.newParameterArrayList(mv, this) //1. new一个List
MethodRecordUtil.fillParameterArray(methodDesc, mv, parametersIdentifier, access) //2. 填充列表
MethodRecordUtil.onMethodEnter(mv, className, name, parametersIdentifier) //3. 调用帮助类
}
super.onMethodEnter()
}

如上所示,采集方法参数也分为3步,接下来我来一步步看下代码


ASM创建列表


    fun newParameterArrayList(mv: MethodVisitor, localVariablesSorter: LocalVariablesSorter): Int {
// new一个ArrayList
mv.visitTypeInsn(AdviceAdapter.NEW, "java/util/ArrayList")
mv.visitInsn(AdviceAdapter.DUP)
mv.visitMethodInsn(
AdviceAdapter.INVOKESPECIAL,
"java/util/ArrayList",
"<init>",
"()V",
false
)
// 存储new出来的List
val parametersIdentifier = localVariablesSorter.newLocal(Type.getType(List::class.java))
mv.visitVarInsn(AdviceAdapter.ASTORE, parametersIdentifier)
// 返回parametersIdentifier,方便后续访问这个列表
return parametersIdentifier
}

逻辑其实很简单,主要问题在于需要用ASM代码写,需要掌握一些字节码指令相关知识。不过我们也可以用asm-bytecode-outline来自动生成这段代码,这样难度可以降低不少。关于代码中各个指令的具体含义,可查阅Java虚拟机(JVM)字节码指令表


ASM填充列表


接下来要做的就是读出所有的参数并填充到上面创建的列表中


    fun fillParameterArray(
methodDesc: String,
mv: MethodVisitor,
parametersIdentifier: Int,
access: Int
) {
// 判断是不是静态函数
val isStatic = (access and Opcodes.ACC_STATIC) != 0
// 静态函数与普通函数的cursor不同
var cursor = if (isStatic) 0 else 1
val methodType = Type.getMethodType(methodDesc)
// 获取参数列表
methodType.argumentTypes.forEach {
// 读取列表
mv.visitVarInsn(AdviceAdapter.ALOAD, parametersIdentifier)
// 根据不同类型获取不同的指令,比如int是iload, long是lload
val opcode = it.getOpcode(Opcodes.ILOAD)
// 通过指令与cursor读取参数的值
mv.visitVarInsn(opcode, cursor)
if (it.sort >= Type.BOOLEAN && it.sort <= Type.DOUBLE) {
// 基本类型转换为包装类型
typeCastToObject(mv, it)
}
// 更新cursor
cursor += it.size
// 添加到列表中
mv.visitMethodInsn(
AdviceAdapter.INVOKEINTERFACE,
"java/util/List",
"add",
"(Ljava/lang/Object;)Z",
true
)
mv.visitInsn(AdviceAdapter.POP)
}
}

主要代码如上所示,代码中都有注释,主要需要注意以下几点:



  1. 静态函数与普通函数的初始cursor不同,因此需要区分开来

  2. 不同类型的参数加载的指令也不同,因此需要通过Type.getOpcode获取具体指令

  3. 为了将参数放在一个列表中,需要将基本类型转换为包装类型,比如int转换为Integer


ASM调用帮助类


    fun onMethodEnter(
mv: MethodVisitor,
className: String,
name: String?,
parametersIdentifier: Int
) {
mv.visitLdcInsn(className)
mv.visitLdcInsn(name)
mv.visitVarInsn(AdviceAdapter.ALOAD, parametersIdentifier)
mv.visitMethodInsn(
AdviceAdapter.INVOKESTATIC, "com/zj/android_asm/MethodRecorder", "onMethodEnter",
"(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V", false
)
}

这个比较简单,主要就是通过ASM调用MethodRecorder.onMethodEnter方法


ASM采集返回值


override fun onMethodExit(opcode: Int) {
// 方法结束
if (isNeedVisiMethod()) {
if ((opcode in IRETURN..RETURN) || opcode == ATHROW) {
when (opcode) {
// 基本类型返回
in IRETURN..DRETURN -> {
// 读取返回值
MethodRecordUtil.loadReturnData(mv, methodDesc)
MethodRecordUtil.onMethodExit(mv, className, name, methodDesc)
}
// 对象返回
ARETURN -> {
// 读取返回值
mv.visitInsn(DUP)
MethodRecordUtil.onMethodExit(mv, className, name, methodDesc)
}
// 空返回
RETURN -> {
mv.visitLdcInsn("void")
MethodRecordUtil.onMethodExit(mv, className, name, methodDesc)
}
}
}
}
super.onMethodExit(opcode);
}

采集返回值的逻辑也很简单,主要分为以下几步



  1. 判断当前指令,并且根据不同类型的返回添加不同的逻辑

  2. 通过DUP指令复制栈顶数值并将复制值压入栈顶,以读取返回值

  3. 读取方法参数类型与返回值类型,并调用MethodRecorder.onMexthodExit方法


帮助类实现


由于ASM需要直接操作字节码,写起来终究不太方便,因此我们尽可能把代码转移到帮助类中,然后通过在ASM中调用帮助类来简化开发,帮助类的代码如下所示:


object MethodRecorder {
private val mMethodRecordMap = HashMap<String, MethodRecordItem>()

@JvmStatic
fun onMethodEnter(className: String, methodName: String, parameterList: List<Any?>?) {
val key = "${className},${methodName}"
val startTime = System.currentTimeMillis()
val list = parameterList?.filterNotNull() ?: emptyList()
mMethodRecordMap[key] = MethodRecordItem(startTime, list)
}

@JvmStatic
fun onMethodExit(
response: Any? = null,
className: String,
methodName: String,
parameterTypes: String,
returnType: String
) {
val key = "${className},${methodName}"
mMethodRecordMap[key]?.let {
val parameters = it.parameterList.joinToString(",")
val duration = System.currentTimeMillis() - it.startTime
val result = "类名:$className \n方法名:$methodName \n参数类型:[$parameterTypes] \n入参:[$parameters] \n返回类型:$returnType \n返回值:$response \n耗时:$duration ms \n"
Log.i("methodRecord", result)
}
}
}

代码其实也很简单,主要逻辑如下:



  1. 方法开始时调用onMethodEnter方法,传入参数列表,并记录下方法开始时间

  2. 方法结束时调用onMethodExit方法,传入返回值,计算方法耗时并打印结果


总结


通过上述步骤,我们就把ASM插桩实现记录方法入参,返回值以及方法耗时的功能完成了,通过插桩可以在方法执行的时候输出我们需要的信息。而这些信息的价值就是可以很好的让我们做一些程序的全链路监控以及工程质量验证。


总得来说,逻辑上其实并不复杂,主要问题可能在于需要熟悉如何直接操作字节码,我们可以通过asm-bytecode-outline等工具自动生成代码来简化开发,同时也可以通过尽量把逻辑迁移到帮助类中的方式来减少直接操作字节码的工作。


示例代码


本文所有源码可见:github.com/shenzhen201…


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

Activity 感知 Fragment 中的触摸事件

前言 Fragment 在 Activity 上,发现 Fragment 上的触摸事件会被 Activity 所接收。这在一些业务场景上很不适用,很多时候业务逻辑不想让我们Fragment中的触摸事件被Activity所感知,那应该怎么做呢? 举个例子吧 我们...
继续阅读 »

前言


FragmentActivity 上,发现 Fragment 上的触摸事件会被 Activity 所接收。这在一些业务场景上很不适用,很多时候业务逻辑不想让我们Fragment中的触摸事件被Activity所感知,那应该怎么做呢?


举个例子吧


我们先建一个Activity,然后在Activity上放一个FragmentFragment位于整个屏幕的下半部分,然后尝试在Fragment上点击,滑动,这时Activity可以接收到这些触摸事件吗?


先将这个Demo的代码写出来:


先为 MainActivity 布局,将我们的Fragment位于整个屏幕的下半部分。


MainActivity.java

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="<http://schemas.android.com/apk/res/android>"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="@color/color_text_gray_light"
android:textSize="16sp"
android:layout_weight="1" />

<androidx.fragment.app.FragmentContainerView
android:id="@+id/liTestFcv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:name="com.jmkj.VirtualCurrency.Home.Fragment.LiTestFragment"/>

</LinearLayout>

然后为我们的Fragment布局,这里取名为 LiTestFragment,放两个Button,其余空间都空着,方便后续的点击、滑动等触摸事件处理。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="<http://schemas.android.com/apk/res/android>"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="button 1" />

<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="button 2" />

</LinearLayout>

并且为这两个按钮添加点击事件,点击跳出Toast提示。


btn1.setOnClickListener {
Toast.makeText(context, "btn 1 click", Toast.LENGTH_SHORT).show()
}
btn2.setOnClickListener {
Toast.makeText(context, "btn 2 click", Toast.LENGTH_SHORT).show()
}

接着,我们在 MainActivity.java 中重写 onTouchEvent 方法,进行对触摸事件的拦截。


@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent: ACTION_UP");
break;

}
return true;
}

猜想一下:


当我们在LiTestFragment的空白区域进行点击、滑动时,MainActivity可以接收到这些触摸事件吗?


答案:可以收到。


当我们在 LiTestFragment 空白区域滑动时,输出日志如下:


E/MainActivity: onTouchEvent: ACTION_DOWN
E/MainActivity: onTouchEvent: ACTION_MOVE
E/MainActivity: onTouchEvent: ACTION_MOVE
E/MainActivity: onTouchEvent: ACTION_MOVE
E/MainActivity: onTouchEvent: ACTION_MOVE
E/MainActivity: onTouchEvent: ACTION_MOVE
E/MainActivity: onTouchEvent: ACTION_MOVE
E/MainActivity: onTouchEvent: ACTION_UP

那再猜想一下:


当我点击LiTestFragment 中的两个按钮时,MainActivity可以接收到这两个点击事件吗?


答案:收不到。


这是为什么呢?思考一下。


那如果我们想让在Fragment中的触摸事件不被Activity接收到,那又该怎么做呢?


Fragment中先行一步拦截掉,然后将触摸事件消费掉,这样就可以避免该触摸事件被Activity所接收到。


override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_li_test, container, false)
view?.setOnTouchListener { v, event ->
v.performClick()
true
}
...省略代码...

return view
}

这里 v.performClick() 为调用该视图定义的 OnClickListener 方法,返回true就是代表消耗该触摸事件。这样子,触摸事件就将在Fragment中被消耗,所以MainActivity也就收不到该触摸事件了。


这里我们回到刚刚的问题,为什么我在LiTestFragment中点击两个按钮时,MainActivity收不到该触摸事件?


因为这里我们是为整个Fragment添加触摸事件监听,而我们的两个按钮就是该Fragment的子view


当我们点击按钮时,触发其onTouchEvent,然后执行 performClick(),执行mOnClickListener.onClick(this),也就是我们为按钮添加的点击监听事件。


所以,如果我们不想让按钮的点击监听事件工作的话,我们只需要为按钮设置OnTouchEvent,然后将事件消费掉就可以了。


findViewById<Button>(R.id.btn1).apply {
setOnTouchListener { v, event ->
Log.e(TAG, "operation: btn1 onTouchListener")
true
}

setOnClickListener {
Toast.makeText(context, "btn 1 click", Toast.LENGTH_SHORT).show()
}
}

总结


其实本文所述的内容都是属于Android事件分发的知识点,想要更好的理解本文,更好的理解ActivityFragment以及子View之间的触摸事件传递,就需要进一步学习一下Android事件分发知识点,我会在下一篇文章中做进一步分享。


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

我在国外写代码,35岁焦虑消失了

随着中国互联网行业红利逐渐消逝,竞争压力越来越大,很多互联网人把目光瞄向了国外。在技术前沿的美国硅谷,在消费互联网方兴未艾的东南亚,在平静安宁的北欧,都能见到中国互联网人的身影。他们有人为了平衡工作和生活,试图逃离内卷,来到国外后,危机感和焦虑感减轻了一些。还...
继续阅读 »

随着中国互联网行业红利逐渐消逝,竞争压力越来越大,很多互联网人把目光瞄向了国外。在技术前沿的美国硅谷,在消费互联网方兴未艾的东南亚,在平静安宁的北欧,都能见到中国互联网人的身影。

他们有人为了平衡工作和生活,试图逃离内卷,来到国外后,危机感和焦虑感减轻了一些。还有一些错过国内互联网浪潮的人,试图在新的环境中寻找职业生涯的第二曲线。

这仿佛是一部互联网人的《出埃及记》。

挣脱魔咒

一年之内,李双悦连续遭遇了两次裁员。

第一次裁员的原因很简单,她已经34岁了,作为一名已婚未育的女性,拿着接近40万元的年薪,公司打算趁她还没怀孕,先一步把她裁掉,不然产假一休,活干不了,白白损失一笔巨大的开支。

李双悦的老东家是武汉的一家港企,她在那里做数据分析师,已经做到了经理级别。2021年春节刚过,一个和她关系很好的HR突然问她打算什么时候备孕、生孩子,她才意识到不对劲,原来公司的一份裁员名单上,已经有了她的名字。

在此之前,她本来已经打算转成程序员,去瑞典工作了,但裁员依然让她措手不及。“我老公知道我被裁员,安慰我可以安心在家做家庭主妇,照顾老人、带带孩子。”但她不甘心,认真读书、认真工作这么多年,自己才30多岁,在各方面能力更成熟,可以挑战更难工作的时候,却突然什么都没有了。

李双悦拒绝了丈夫的建议,开始从领英和一些瑞典公司的官网上找工作机会。也有一些猎头来找她,但都“把丑话说在了前面”——工资起码会腰斩,税前月薪1.5万都很困难,有1万就不错了。武汉虽然也有好的企业和好的职位,但这些职位要么是核心,要么工作强度很大。“不论哪一点都不会允许HR招聘一个34岁、已婚未育、可能马上准备生孩子的女性去做。这是机制矛盾,我没办法解决。”

在中国互联网行业,流传着35岁失业的魔咒,不少从业者都会陷入到年龄焦虑中。《2021年中国程序员薪资和生活现状调查报告》显示,互联网企业对程序员的需求正在减少,与2020年相比,刚刚就业的程序员数量显著减少,工作两年以上的程序员占比则从73%上升到88%。35岁及以上的程序员占比仅为9.4%,不足十分之一。

经历了二十年高速发展,中国互联网行业增速下滑,用人需求少了,自然更愿意用低成本的、有一定经验的年轻人替换掉那些高薪的老人。

李双悦还是寄希望于国外的公司,她一边查找职位信息,一边开始每天在B站上学习代码知识,有空的时候就在力扣(leetcode,IT技术职业化提升平台)上面做题。经过两个月的面试,今年3月份,她成功入职了瑞典一家处于Pre-IPO阶段的互联网金融公司。


▲ 李双悦在瑞典工作的公司。图 / 受访者提供

不幸的是,公司主营金融业务受到俄乌战争的影响,欧洲市场不断萎缩,公司上市在即,也需要压缩成本。5月份的时候,她发现自己又出现在700人的裁员名单中。

但这次她只慌了一个晚上。

被裁员后的4天内,李双悦连续收到超过240封面试邮件,还有很多猎头打来电话询问她的工作意愿。原来是公司成立了一个内部互助小组,把所有被裁员工的姓名、职位、联络方式等信息整理到公开文档,在领英上开放权限给所有猎头、各大公司的HR查看。一夜之间,几乎整个瑞典的互联网圈都知道她们公司要裁员。

在得知李双悦被裁员后,她的直属领导非常生气,立刻去跟COO沟通这件事情,还在公司大群里据理力争。“第一,他认为我在他团队里的位置很重要;第二,我刚来两个月,所有的行李都还卡在上海港口没有到瑞典,他认为公司就这样把新人裁掉很不人道。但最终,裁员的决策没有改变,他一怒之下交了辞呈。”

李双悦的同事们也都很好。当得知她被裁员但是行李还在路上时,有别的部门领导说自己夏天要出国度假2个月,房子可以免费给她住。还有女生打电话问她需不需要衣服,可以免费送给她穿。

在此之前,她从来没想过自己喜不喜欢这份工作、喜不喜欢瑞典,但经过这第二次裁员,她发现原来自己的上司也会为她鸣不平,会重视她未来的发展,尊重她的意愿,并为她提供一切尽可能的帮助。

现在,李双悦还在找工作,但她不急了。她觉得自己挣脱了35岁的魔咒,找到一个更好的工作,只是时间问题。


▲ 图 / 视觉中国

“亲密关系”

“你上个周末是怎么过的?”Grab新加坡总部的走廊上,迎面走来的男同事叫住了李米,她听到这句寒暄吓了一跳。此前她在国内一家大厂做了4年产品经理,从来没有同事关心过她的私生活,现在听到男同事这样问,她甚至有种“这个人是不是对我有意思,想要追我”的错觉。

在这家上市不久的出行公司里,李米感受到一种奇怪的氛围,上级最常问自己的问题不是日活、月活、产品进度,而是“你最近好吗?”“工作做得开心吗?”“组里现在做的事情还是太多,你认为有什么(功能)是不太重要可以先推掉的吗?”

在上一家公司,从毕业那会儿以校招生的身份进去直到离开,她都没有产生跟同事交朋友的念头,也很少跟同事聊工作之外的事情。

“361”(30%优秀,60%及格,10%淘汰)绩效强排制度是一个重要原因。“你和同事更多是竞争关系,不是合作。”有一次她和同事一起写晋升PPT,一部分产品功能明明是一起做的,但大家都默契地不问对方的准备进度,更不会给对方看自己的PPT,“生怕有什么亮点和成果被别人抄去”。

李米没在别的公司工作过,她天然认为同事是输入的条件句不同,就会执行不同程序的一群人——如果项目做出成绩,同事是和自己抢功的人;如果项目出了问题,同事就是把锅甩给自己的人。简而言之,是下班就想装作不认识的人。

在公司里,每个人的一言一行都在打造拼搏进取的人设。“今天好累,不想上班”这种牢骚话绝不能告诉任何同事,而且“本来也没有人在乎你是怎么想的”。

直到进入Grab,她突然面临另一种环境。在新加坡职场,好员工是美剧里那种成绩又好、又会打扮的社交女王,善于在一些工作之外的社交场合展现自己的谈吐。

李米一开始并不适应。她总是紧守边界,告诉同事自己不喝酒,但发现这个回答让所有人扫兴之后,李米决定退一步。“每次我就只喝一杯”,为获得集体的认同,她愿意妥协。

以前面对那种“你周末做了什么”的问题,李米会老老实实说在家待着,但后来发现这个答案太呆了,别人会认为她很无聊。“所以我宁可搬出上周末做的事情,也要给出一个有意思的回答。”

在国内,直属领导很大程度上决定了她能不能晋升。但在新加坡,起码在Grab,直属领导对下属晋升没有一锤定音的话语权。除了需要直接领导的打分之外,还要请10个与自己工作关系密切的同事打分,相加之后得出总分。如果谁想升职,就得自己主动发邮件抄送大家、知会大家自己正在完成的工作,刷出存在感很重要。

进入到另一种价值体系里,过往的经验都失效了。

好的一点是,李米不再需要讨好领导,同事之间也多了一些信任。在之前的公司里,程序员不太信任产品经理,他们会觉得产品经理什么都不懂,总是问她“为什么要做这个功能?你之前也做过类似的,屁用都没有,这回你想清楚了吗?”“产品经理怎么能没想清楚就要求程序员写代码?”

来了Grab,李米发现程序员们都很相信她。“我说希望能上线某个功能,他们就会积极地反馈我什么时间可以开始做,有时候在系统里能看见他们真的是加班加点写代码。”

不好的地方在于,除了需要付出更多努力得到所有人认可,工作效率也因为平等的氛围被拖累了,任何大大小小的决策,需要每一个同事一致同意,最起码也是原则上同意,谁都能找她开会,也不好拒绝,真正的工作只能见缝插针地推进。


▲ 图 / 电视剧《硅谷》剧照

逃离无效内卷

爱尔兰每天早上9点,雅虎数据科学家金蕴从床边走几步到办公桌前,就可以开始一天的工作。在这里,效率不是唯一的追求,秒回工作信息也不是铁律,直到10点,金蕴很多同事才会把系统状态调成在线,他在9点发给同事的消息,通常能在11点左右得到回复。

下午5点半前,他合上电脑,就再也不会收到任何工作消息。每天属于工作和自己的时间,被两个数字精准分割开。这是一种一视同仁的分寸感,即便在金蕴做实习生的时候,美国团队的代教员工都会算准时差,不占用他的休息时间。

按照金蕴自己制定的OKR,日常工作通常在每天下午2~3点完成,剩下的时间由自己支配。“闲得长草”之后,他甚至在爱尔兰国立梅努斯大学读起了兼职博士。

每周不足38个小时的工作时长还有可能继续降低,雅虎总部准备尝试在维持员工年薪不变的前提下逐步推行每周4天工作制。这一政策在2015年~2019年在冰岛率先实验,取得很好的成效。

同一时间的中国,996工作制在互联网行业逐步推行开来,大厂们喜欢“雇两个人,给三个人的钱,干四个人的活儿”,永远干不完的活儿,让工作和生活的界限变得模糊。

李米的老东家打卡时间是每天10~19点,但她承认,绝大多数工作日“19点是走不了的”,“就算工作做完了也不行”。直系领导的作息时间成为了更可靠的参考系。

有一天晚上9点,直属领导拍下空空荡荡的工区,在工作大群里问所有人:“这才几点办公室就没人了,你们最近回家都这么早吗?”李米很快就明白了其中的利害关系。从此以后,临近下班,她会先确认一下领导的动态,熬到领导先走,她才敢和其他同事一同站起身离开办公室。

对在另一家大厂工作的程序员王滔来说,在裁员招聘并行成为部门常态之后,她无意间得知绩效考核要和个人加班时长挂钩,于是她的最新目标是成为最后一个离开办公室的人。

研发需求一个压着一个,为了拿出工作结果,每个产品经理都说自己“是新需求、优先级很高、尽量把功能都实现”,而她应急的方法只能是自己加班。除了王滔不善拒绝的性格,最重要的原因是,她需要用又快又准的代码证明自己符合组织对人才的要求。

“Talk is cheap,show me the code(废话少说,放码过来)。”王滔原本很喜欢Linux创始人的这句话,但现在如果有机会,她想问问这位52岁的前辈,写多少行代码才能避开裁员的漩涡?

李双悦也经历过这种荒谬。武汉疫情复工之后,整个行业就“卷”得很厉害,不仅是自己的同事,从供应商到整个行业上下游公司都在“卷”。那会儿夜里11:30下班是常态,同事们为了让老板看到自己的勤奋,下午去商场排奶茶2个小时,直到6点再缓缓打开电脑。

她不一样,总是尽量在工作时间内把所有事情高效做完,7点钟准时打卡回家。但这样的工作态度,公司并不接受。去年4月,另一个部门的领导把她拉到小屋里问:“作为一个经理级别的人,你为什么每天按时下班?”

她好言好语地解释:“我的KPI都已经完成了,而且我和老公需要自己开火做饭,不可能每天都11点回家。”他立刻反问:“那你们两家老人是都死掉了吗?”听到这句话,李双悦整个人处在惊吓的状态里,一句话都说不出来。


▲ 图 / 电视剧《我,到点下班》

但来到海外,她又显得太过勤奋。在瑞典,李双悦还维持着自己在国内工作的惯性——实时回复、事事同步。

哪怕她正在吃饭,只要听到办公软件的提示音就会扔下饭碗,立刻去回复工作信息。但显然,海外的同事都没有患上这种“强迫症”。“我发出的消息,隔两天才有人回复是常事。”她说,“每个人都会优先自己手头有时间表的工作,中途进来的需求就是得等。”直到大半个月之后,李双悦才习惯这种等待。

她的上司们也感受到这种勤奋所带来的压力。

李双悦每天早晨都会用Google Keep记下自己当天要完成的工作,在每一项任务后面打勾、同步给同事和上级是规定动作。来了瑞典,她依然如此。李双悦每完成一项工作,对方都会立刻收到更新推送。

没过多久,直属上级就不胜其扰地告诉她:“不要再把任务表单同步给我,否则打开手机尽是你的工作信息,我压力太大。”领导委婉地说,工作总结每周更新就可以,她又拿出自己在国内写周报的劲头发了个文件夹过去,对方只好直说:“不不不,我没有想看。”

超时工作更会被严肃劝告。为了树立优秀新人的形象,第一次独立交报表之前,李双悦连续加班了一个星期。直到周五,上司发现她竟然晚上10点还在工作,立刻严肃地给她发了消息:这个时间,你应该关掉电脑,和家人待在一起。她只能尴尬地回复:“我这就去”。这件“耸人听闻”的事甚至直接传到了老板那里,对方认真地告诉她:每周工作时长绝对不要超过45小时。

“他们都嫌弃我太卷了。”李双悦说。

而对张森来说,出国之后的工作强度反而更大了。他在半年前加入爱尔兰亚马逊云总部,负责IT支持业务,帮助大客户寻找服务器问题。

PIP制度(绩效改进计划)是每个亚马逊人的达摩克利斯之剑。作为新人,他一度在危险边缘徘徊。亚马逊云的考核标准只有两个维度——服务客户数量、客户满意度。这要求他必须又快又准地解决问题,每天6~8个小时的工作排得很满,“不可能像之前的工作还有摸鱼的时间”。

他从来没有想到,大学毕业14年之后,自己竟然回到了都柏林大学的图书馆自学长达900页的《ECS开发者手册》。“而这只是其中一个服务,亚马逊云可以提供超过200个服务,每个服务的文档几乎都是这么长。”

他的妻子夏晓似乎也感受到亚马逊云内部竞争的压力,“公司里(技术人员)都是高手,他也在追赶别人吧”。


▲ 图 / 视觉中国

更高的追求

最近李米、金蕴、徐坤总是遇到国内亲朋好友和陌生人的提问:“怎么才能拿到海外大厂的offer?”“有没有内推的可能?”还有一些人纯粹把他们当作许愿池,“希望我也能在新加坡找到工作”“学文科的也想留在爱尔兰”。

越来越多的中国互联网人选择出海,这股风潮从两三年前就开始了,除了技术更前沿的硅谷、高福利的北欧,东南亚是更多人的选择,语言相近,文化相通,市场广阔,到处都是机会,特别像十几年前的中国。

李米当时去新加坡,就是为了陪男朋友,当时他在互联网金融领域创业,在新加坡更容易融资。现在她已经跟男朋友分手了,但不后悔来到新加坡。

成为下一个李彦宏的念头深深诱惑着徐坤。李彦宏从纽约州立大学硕士毕业之后进入硅谷infoseek公司工作。1999年,李彦宏带着彼时最先进的搜索引擎技术和120万美元融资回国,创立百度。至此,一个颇具统治力的互联网巨头拔地而起。

这是早被证明可复制的成功人生模板,但前提是学到一流的技术。为此,徐坤大学毕业就来到美国,先后转换几次赛道,最终选择在VR/AR领域研究三维模型重建方面的算法。今年3月,他在瑞士苏黎世参加了Google线上面试,但却失败了,接下来打算继续申请Magic Leap的职位。

徐坤很着急,想要快点找到最顶尖的技术人才合作,掌握最顶尖的技术,5年内回国创业。“2011年出国到现在,我错过了整个国内的移动互联网浪潮、房地产浪潮、创业浪潮,我肯定不甘心。但我觉得自己也没有完全准备好,不只是运气不够,而是我还没有掌握到超一流的技术,没办法形成壁垒,创立一家公司。”

更多人选择离开,只是因为不想当一颗螺丝钉,不想做一台全年无休写代码、做产品的机器。


▲ 图 / 电视剧《硅谷》剧照

在国内,没有刷过力扣(leetcode)200道题的算法工程师不足以面大厂。把整个程序一句一句写在白纸上被称为“手撕代码”。在金蕴眼里,“就是考察候选人的背诵能力”。但进入到实际工作中,往往并不需要解决那么复杂、高难度的问题,而是对已经构建好的代码大厦修修补补,这让很多程序员觉得没意思。

在雅虎、亚马逊云的面试里,刷题没有用。

雅虎的技术面试只给了3道题,金蕴除了第一题写完了程序,第二题和第三题都只写了代码逻辑。要做什么、得出什么结果、提升什么功能,“这要是在国内,我肯定过不了”。

有时候国外企业也会拿一些还在研究阶段的问题来考察候选人解决问题的能力,而不是考察背诵Leetcode题库的能力。加入雅虎后,金蕴每天有一半的时间用来开技术研讨会,真正写代码的时间很少,他还申请了兼职博士,雅虎也非常鼓励员工一边学习、一边研究。

国内企业的HR倾向于拿着题库问张森,“怎么在服务器调取某个文件,文件权限怎样显示”。但在亚马逊云6个小时的面试里,问题变成“谈谈你如何面对一些棘手客户?”

一个人过往的工作经验、思考问题的逻辑,甚至有无种族、年龄、性别歧视都变得格外重要。“这些企业不是想随便招一个人,而是想招到我这样的人。”张森确信。

但选择了诗和远方,就一定能躲得过生活的苟且吗?

来到爱尔兰都柏林,张森一家彻底变成月光族。妻子夏晓全职带孩子,张森的“收入一半交给政府、一半交给房东,剩下的钱只够一家人吃饭”。

按照张森的收入水平,每个月有接近40%的工资要交税,“税后的薪水应该不会比都柏林普通的蓝领高很多”。

《爱尔兰时报》去年调查显示,在全球首都城市的租房成本排名中,都柏林位列第六,租金平均每月1643欧,每月净工资为2960欧。张森居住的地段算是中产社区,不到80平米的房子每个月租金高达2350欧,并且按照合同约定,租金将以每年5%的比例递增。

“从前两个人工作还能攒钱,现在真的是一分钱都剩不下。”夏晓说。

父母虽然对李米出国工作的决定不置可否,但她知道,他们心里盼着自己唯一的女儿回国。

新加坡靠近赤道,全年潮湿多雨。在某一个雨天,他看到马来西亚华裔作家黄锦树的《雨》。身在异乡的她记下了这句:

“你还不懂得时间的微妙。它不是只会流逝,还会回卷,像涨潮时的浪。”


▲ 图 / 视觉中国

(文中受访者均为化名)

来源:mp.weixin.qq.com/s/DHzv6uoB6jHOZioNE0ln_w

收起阅读 »

2022 年的 React 生态

今天的文章,我们将从状态管理、样式和动画、路由、代码风格等多个方面来看看 React 最新的生态,希望你以后在做技术选型的时候能够有所帮助。Next.js 可以支持你生成静态站点,而 Gatsby.js 也支持了服务端渲染。不过就我个人的使用体验而言,我会觉得...
继续阅读 »

今天我们来聊 ReactReact 已经风靡前端届很长一段时间了,在这段时间里它发展了一个非常全面而强大的生态系统。大厂喜欢在大型的前端项目中选择 React,它的生态功不可没。

今天的文章,我们将从状态管理、样式和动画、路由、代码风格等多个方面来看看 React 最新的生态,希望你以后在做技术选型的时候能够有所帮助。

创建 React 项目


对于大多数 React 初学者来说,在刚刚开始学习 React 时如何配置一个 React 项目往往都会感到迷惑,可以选择的框架有很多。React 社区中大多数会给推荐 Facebookcreate-react-app (CRA)。它基本上零配置,为你提供开箱即用的简约启动和运行 React 应用程序。


但现在来看,CRA 使用的工具过时了 — 从而导致我们的开发体验变慢。Vite 是近期最受欢迎的打包库之一,它具有令人难以置信的开发和生产速度,而且也提供了一些模板(例如 React、React + TypeScript)可以选择。


如果你已很经熟悉 React 了,你可以选择它最流行的框架之一作为替代:Next.jsGatsby.js。这两个框架都基于 React 建立,因此你应该至少熟悉了 React 的基础知识再去使用。这个领域另一个流行的新兴框架是 Remix,它在 2022 年绝对值得一试。


虽然 Next.js 最初是用来做服务端渲染的,而 Gatsby.js 主要用来做静态站点生成(例如博客和登录页面等静态网站)。然而,在过去几年里,这两个框架之间一直在互相卷...

Next.js 可以支持你生成静态站点,而 Gatsby.js 也支持了服务端渲染。不过就我个人的使用体验而言,我会觉得 Next.js 更好用一点。

如果你只想了解一下 create-react-app 这些工具在后台的工作原理,建议尝试一下自己从头开始配置一个 React 项目。从一个简单的 HTML JavaScript 项目开始,并自己添加 React 及其支持工具(例如 Webpack、Babel)。这并不是你在日常工作中必须要做的事情,但这是了解底层工具实现原理的一个很好的方式。

建议:

  • 优先使用 Vite创建 React客户端应用

    • CRA 备选

  • 优先使用 Next.js 创建 React服务端渲染应用

    • 最新技术:Remix

    • 仅创建静态站点备选 Gatsby.js

  • 可选的学习经验:从0自己搭建一个 React 应用。

链接:

阅读:


状态管理


React 带有两个内置的 Hooks 来管理本地状态:useStateuseReducer。如果需要全局状态管理,可以选择加入 React 内置的 useContext Hook 来将 props 从顶层组件传递到底层组件,从而避免 props 多层透传的问题。这三个 Hooks 足以让你实现一个强大的状态管理系统了。

如果你发现自己过于频繁地使用 ReactContext 来处理共享/全局状态,你一定要看看 Redux,它是现在最流行的状态管理库。它允许你管理应用程序的全局状态,任何连接到其全局存储的 React 组件都可以读取和修改这些状态。


如果你碰巧在用 Redux,你一定也应该查看 Redux Toolkit。它是基于 Redux 的一个很棒的 API,极大地改善了开发者使用 Redux 的体验。

作为替代方案,如果你喜欢用全局存储的思想管理状态,但不喜欢 Redux 的处理方式,可以看看其他流行的本地状态管理解决方案,例如 Zusand、Jotai、XStateRecoil

另外,如果你想拥有和 Vue.js 一样的开发体验,建议看看 Mobx

建议:

  • useState/useReducer 处理共享状态

  • 选择性使用 useContext 管理某些全局状态

  • Redux(或另一种选择) 管理全局状态

链接:

阅读:


远程数据请求


React 的内置 Hooks 非常适合 UI 状态管理,但当涉及到远程数据的状态管理(也包括数据获取)时,我建议使用一个专门的数据获取库,例如 React Query,它自带内置的状态管理功能。虽然 React Query 本身的定位并不是一个状态管理库,它主要用于从 API 获取远程数据,但它会为你处理这些远程数据的所有状态管理(例如缓存,批量更新)。


React Query 最初是为使用 REST API 而设计的,但是现在它也支持了 GraphQL。然而如果你正在为你的 React 项目寻找专门的 GraphQL 库,我还是推荐你去看看 Apollo Client(当前最流行的)、urql(轻量级)或 RelayFacebook 维护)。

如果你已经在使用 Redux,并且想要在 Redux 中添加集成状态管理的数据请求功能,建议你看看 RTK Query,它将数据请求的功能更巧妙的集成到 Redux 中。

建议:

  • React Query(REST API、GraphQL API 都有)

  • Apollo Client(只有 GraphQL API)

  • 可选的学习经验:了解 React Query 的工作原理

链接:

阅读:


路由


如果你使用的是像 Next.jsGatsby.js 这样的 React 框架,那么路由已经为你处理好了。但是,如果你在没有框架的情况下使用 React 并且仅用于客户端渲染(例如 CRA),那么现在最强大和流行的路由库是 React Router

链接:

阅读:


样式/CSS

React 中有很多关于 样式/CSS 的选项和意见,作为一个 React 初学者,可以使用一个带有所有 CSS 属性的样式对象作为 HTML 样式属性的键/值对,从内联样式和基本的 CSS 开始就可以。

const ConardLi = ({ title }) =>
 <h1 style={{ color: 'blue' }}>
  {title}
 h1>

内联样式可以在 React 中通过 JavaScript 动态添加样式,而外部 CSS 文件可以包含 React 应用的所有剩余样式:

import './Headline.css';

const ConardLi = ({ title }) =>
 <h1 className="ConardLi" style={{ color: 'blue' }}>
  {title}
 h1>

如果你的应用越来越大了,建议再看看其他选项。首先,我建议你将 CSS Module 作为众多 CSS-in-CSS 解决方案的首选。CRA 支持 CSS Module ,并为提供了一种将 CSS 封装到组件范围内的模块的方法。这样,它就不会意外泄露到其他 React 组件的样式中。你的应用的某些部分仍然可以共享样式,但其他部分不必访问它。在 React 中, CSS Module 通常是将 CSS 文件放在 React 组件文件中:

import styles from './style.module.css';

const ConardLi = ({ title }) =>
 <h1 className={styles.headline}>
  {title}
 h1>


其次,我想向你推荐所谓的 styled components ,作为 React 的众多 CSS-in-JS 解决方案之一。它通过一个名为 styles-components(或者其他例如 emotion 、stitches)的库来实现的,它一般将样式放在 React 组件的旁边:

import styled from 'styled-components';

const BlueHeadline = styled.h1`
 color: blue;
`;

const ConardLi = ({ title }) =>
 <BlueHeadline>
  {title}
 BlueHeadline>


第三,我想推荐 Tailwind CSS 作为最流行的 Utility-First-CSS 解决方案。它提供了预定义的 CSS 类,你可以在 React 组件中使用它们,而不用自己定义。这可以提升一些效率,并与你的 React 程序的设计系统保持一致,但同时也需要了解所有的类:

const ConardLi = ({ title }) =>
 <h1 className="text-blue-700">
  {title}
 h1>

使用 CSS-in-CSS、CSS-in-js 还是函数式 CSS 由你自己决定。所有的方案在大型 React 应用中都适用。最后一点提示:如果你想在 React 中有条件地应用一个 className,可以使用像 clsx 这样的工具。

建议:

  • CSS-in-CSS 方案: CSS Modules

  • CSS-in-JS

    方案: Styled Components(目前最受欢迎)

    • 备选: EmotionStitches

  • 函数式 CSS:Tailwind CSS

  • 备选:CSS 类的条件渲染:clsx

链接:

阅读:


组件库

对于初学者来说,从零开始构建可复用的组件是一个很好的学习经验,值得推荐。无论它是 dropdown、radio button 还是 checkbox ,你最终都应该知道如何创建这些UI组件组件。


然而,在某些时候,你想要使用一个UI组件库,它可以让你访问许多共享一套设计系统的预构建组件。以下所有的UI组件库都带有基本组件,如 Buttons、Dropdowns、DialogsLists

尽管所有这些UI组件库都带有许多内部组件,但它们不能让每个组件都像只专注于一个UI组件的库那样强大。例如 react-table-library 提供了非常强大的表格组件,同时提供了主题(例如 Material UI),可以很好的和流行的UI组件库兼容。

阅读:


动画库


Web 应用中的大多数动画都是从 CSS 开始的。最终你会发现 CSS 动画不能满足你所有的需求。通常开发者会选择 React Transition Group,这样他们就可以使用 React组件来执行动画了,React 的其他知名动画库有:


可视化图表


如果你真的想要自己从头开始开发一些图表,那么就没有办法绕过 D3 。这是一个很底层的可视化库,可以为你提供开发一些炫酷的图表所需的一切。然而,学习 D3 是很有难度的,因此许多开发者只是选择一个 React 图表库,这些库默认封装了很多能力,但是缺失了一些灵活性。以下是一些流行的解决方案:


表单


React 现在最受欢迎的表单库是 React Hook Form 。它提供了从验证(一般会集成 yupzod)到提交到表单状态管理所需的一切。之前流行的另一种方式是 Formik。两者都是不错的解决方案。这个领域的另一个选择是 React Final Form 。毕竟,如果你已经在使用 React UI组件库了,你还可以查看他们的内置表单解决方案。

建议:

  • React Hook Form

    • 集成 yupzod 进行表单验证

  • 如果已经在使用组件库了,看看内置的表单能不能满足需求

链接:

阅读:


类型检查

React 带有一个名为 PropTypes 的内部类型检查。通过使用 PropTypes,你可以为你的 React 组件定义 props。每当将类型错误的 prop 传递给组件时,你可以在运行时收到错误消息:

import PropTypes from 'prop-types';

const List = ({ list }) =>
 <div>
  {list.map(item => <div key={item.id}>{item.title}div>)}
 div>

List.propTypes = {
 list: PropTypes.array.isRequired,
};


在过去的几年里,PropTypes 已经不那么流行了,PropTypes 也已经不再包含在 React 核心库中了,现在 TypeScript 才是最佳的选择:

type Item = {
 id: string;
 title: string;
};

type ListProps = {
 list: Item[];
};

const List: React.FC<ListProps> = ({ list }) =>
 <div>
  {list.map(item => <div key={item.id}>{item.title}div>)}
 div>

阅读:


代码风格


对于代码风格,基本上有两种方案可以选择:

如果你想要一种统一的、通用的代码风格,在你的 React 项目中使用 ESLint 。像 ESLint 这样的 linter 会在你的 React 项目中强制执行特定的代码风格。例如,你可以在 ESLint 中要求遵循一个流行的风格指南(如 Airbnb 风格指南)。之后,将 ESLint 与你的IDE/编辑器集成,它会指出你的每一个错误。

如果你想采用统一的代码格式,可以在 React 项目中使用 Prettier。它是一个比较固执的代码格式化器,可选择的配置很少。你也可以将它集成到编辑器或IDE中,以便在每次保存文件的时候自动对代码进行格式化。虽然 Prettier 不能取代 ESLint,但它可以很好地与 ESLint 集成。

建议:

阅读:


身份认证


React 应用程序中,你可能希望引入带有注册、登录和退出等功能的身份验证。通常还需要一些其他功能,例如密码重置和密码更改功能。这些能力远远超出了 React 的范畴,我们通常会把它们交给服务端去管理。

最好的学习经验是自己实现一个带有身份验证的服务端应用(例如 GraphQL 后端)。然而,由于身份验证有很多安全风险,而且并不是所有人都了解其中的细节,我建议使用现有的众多身份验证解决方案中的一种:

阅读:


测试

现在最常见的 React 测试方案还是 Jest,它基本上提供了一个全面的测试框架所需要的一切。

你可以使用 react-test-renderer 在你的 Jest 测试中渲染 React 组件。这已经足以使用 Jest 执行所谓的 Snapshot Tests 了:一旦运行测试,就会创建 React 组件中渲染的 DOM 元素的快照。当你在某个时间点再次运行测试时,将创建另一个快照,这个快照会和前一个快照进行 diff。如果存在差异,Jest 将发出警告,你要么接受这个快照,要么更改一下组件的实现。

最近 React Testing Library (RTL) 也比较流行(在 Jest 测试环境中使用),它可以为 React 提供更精细的测试。RTL 支持让渲染组件模拟 HTML 元素上的事件成,配合 Jest 进行 DOM 节点的断言。

如果你正在寻找用于 React 端到端 (E2E) 测试的测试工具,Cypress 是现在最受欢迎的选择。

阅读:


数据结构


Vanilla JavaScript 为你提供了大量内置工具来处理数据结构,就好像它们是不可变的一样。但是,如果你觉得需要强制执行不可变数据结构,那么最受欢迎的选择之一是 Immer 。我个人没用过它,因为 JavaScript 本身就可以用于管理不可变的数据结构,但是如果有人专门问到 JS 的不可变性,有人会推荐它。

链接:

阅读:


国际化


当涉及到 React 应用程序的国际化 i18n 时,你不仅需要考虑翻译,还需要考虑复数、日期和货币的格式以及其他一些事情。这些是处理国际化的最流行的库:


富文本编辑


React 中的富文本编辑器,就简单推荐下面几个,我也没太多用过:


时间处理


近年来,JavaScript 本身在处理日期和时间方面做了很多优化和努力,所以一般没必要使用专门的库来处理它们。但是,如果你的 React 应用程序需要大量处理日期、时间和时区,你可以引入一个库来为你管理这些事情:


客户端


Electron 是现在跨平台桌面应用程序的首选框架。但是,也存在一些替代方案:

阅读:


移动端


ReactWeb 带到移动设备的首选解决方案仍然是 React Native

阅读:


VR/AR


通过 React,我们也可以深入研究虚拟现实或增强现实。老实说,这些库我还都没用过,但它们是我在 React 中所熟悉的 AR/VR 库:


原型设计


如果你是一名 UI/UX 设计师,你可能希望使用一种工具来为新的 React 组件、布局或 UI/UX 概念进行快速原型设计。我之前用的是 Sketch ,现在改用了 Figma 。尽管我两者都喜欢,但我还是更喜欢 FigmaZeplin 是另一种选择。对于一些简单的草图,我喜欢使用 Excalidraw。如果你正在寻找交互式 UI/UX 设计,可以看看 InVision


文档


我在很多项目里都在使用 Storybook 作为文档工具,不过也有一些其他好的方案:

最后

参考:http://www.robinwieruch.de/react-libra…

本文完,欢迎大家补充。


作者:ConardLi
来源:juejin.cn/post/7085542534943883301

收起阅读 »

真刑啊!蔚来员工用公司服务器挖矿,已供认不讳

蔚来员工,用公司服务器挖矿。就在刚刚,一位微博博主曝出了这么条消息。据称,涉事人张某是蔚来汽车员工,此前担任某集群服务器管理员。而他在在职期间,利用职务上的便利,用公司服务器挖虚拟货币。△图源:微博事件一出,立即登上了微博热搜:对此,不少网友纷纷发出感慨:可真...
继续阅读 »

蔚来员工,用公司服务器挖矿。


图片


就在刚刚,一位微博博主曝出了这么条消息。


据称,涉事人张某是蔚来汽车员工,此前担任某集群服务器管理员。


而他在在职期间,利用职务上的便利,用公司服务器挖虚拟货币。


图片

△图源:微博


事件一出,立即登上了微博热搜:


图片


对此,不少网友纷纷发出感慨:

可真刑啊,越来越有判头了呢。

图片


# 用公司服务器挖矿


虽然对于这件事情,蔚来汽车官方并没有出面做回应。


但是从流露出来的内部消息图片中,可以获取到些许事件详情。


事情还要追溯到2021年9月1日,蔚来汽车合规和风险管理部收到投诉称:

研发部门员工张某疑似利用其服务器管理的便利,不当利用公司服务器资源进行虚拟货币数字挖掘操作。


而后,蔚来内部经授权,对此事展开了调查。


调查结果显示,张某从2021年2月开始,便开始了这样的违规操作。


其所挖的虚拟货币,从爆料中的图片中可以看到,是以太币。


图片


并且消息还称,张某在调查期间对于自己的违规行为供认不讳。


而张某的这一行为,触犯了我国刑法第285条第二款非法控制计算机信息系统罪。


据了解,犯此项罪的:

处三年以下有期徒刑或者拘役,并处或者单处罚金;

情节特别严重的,处三年以上七年以下有期徒刑,并处罚金。


# 此前还有百度员工


蔚来员工的事情一经曝出,很多网友纷纷联想到了之前的那位百度工程师。


早在2020年,一位百度员工在短短7个月内便走完了从“挖矿”、“变现”到“被判3年”的三部曲。


其挖矿所用的,便是百度搜索服务器。


在判决书中,也对这位百度员工“薅羊毛”的细节做了公布:


从2018年1月底到5月底,安某薅了155台服务器的羊毛,用来挖比特币、门罗币,卖掉一部分之后获利10万元。


事发之后,不仅这笔钱被没收,还额外被罚了11000元,另外还有3年的有期徒刑。


图片


……


而从国内外来看,近年来公然用公司服务器挖矿的事件时有发生。


虽然截至发稿,蔚来汽车方面并未做出更详细的回应。


但还需从此事中了解一点:


道路千万条,守法第一条。


为了牟利而赌上未来和自由,不值得!

来源:公众号 QbitAI

收起阅读 »

程序员定级1-5:你是哪一级?

1、代码写得好,bug少,看起来像个闲人2、注释多,代码清晰,任何人接手都很方便,看起来谁都可以替代3、代码写的烂,每天风风火火写bug,解决各种线上重大问题,顺理成章成为公司亮眼人才4、代码乱的只有自己能看懂,公司不可替代人才5、多写bug,好的程序员带动两...
继续阅读 »

1、代码写得好,bug少,看起来像个闲人

2、注释多,代码清晰,任何人接手都很方便,看起来谁都可以替代

3、代码写的烂,每天风风火火写bug,解决各种线上重大问题,顺理成章成为公司亮眼人才

4、代码乱的只有自己能看懂,公司不可替代人才

5、多写bug,好的程序员带动两个以上的兄弟就业,整个代码行业就会繁荣发展

收起阅读 »

Java多线程案例之线程池

⭐️前面的话⭐️本篇文章将介绍多线程案例,线程池,线程在Linux中也叫做轻量级线程,尽管线程比进程较轻,但是如果线程的创建和销毁频率高了,开销也还是有的,为了进一步提高效率,引入了线程池,和字符串常量池类似,把线程提前创建好,放到一个“池子”里面,后面使用的...
继续阅读 »

⭐️前面的话⭐️

本篇文章将介绍多线程案例,线程池,线程在Linux中也叫做轻量级线程,尽管线程比进程较轻,但是如果线程的创建和销毁频率高了,开销也还是有的,为了进一步提高效率,引入了线程池,和字符串常量池类似,把线程提前创建好,放到一个“池子”里面,后面使用的时候,速度就快了,但是代价就是空间,线程池本质上也还是空间换时间。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆掘金首发时间:🌴2022年6月4日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《Java核心技术》,📚《Java编程思想》,📚《Effective Java》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


🎵1.线程池概述

🎶1.1什么是线程池

线程池和字符串常量池一样,都是为了提高程序运行效率而提出的效率,程序中每创建一个线程就会把该线程加载到一个“池子”中去,其实这个池子就是List,当程序下次需要调用该线程的时候,可以直接从线程池中去取,而不用花费更大的力气去重新创建和销毁线程,从而使程序的运行效率提高,线程池也是管理线程的方式之一。

🌳那为什么从线程池中“拿”线程会比直接创建线程要更加高效呢?

因为使用线程池调度线程是在用户态实现的,而线程的创建是基于内核态实现的。那为什么说用户态比内核态更加高效呢?因为你将任务交给内核态时,内核态不仅仅只去完成你交给它的任务,大概率还会伴随完成其他的任务,而你将任务交给用户态时,用户态只去完成你所交代的任务,所以综上所述,用户态效率更高。

🎶1.2Java线程池标准类

java也提供了相关行程池的标准类ThreadPoolExecutor,也被称作多线程执行器,该类里面的线程包括两类,一类是核心线程,另一类是非核心线程,当核心线程全部跑满了还不能满足程序运行的需求,就会启用非核心线程,直到任务量少了,慢慢地,非核心线程也就退役了,通俗一点核心线程就相当于公司里面的正式工,非核心线程相当于临时工,当公司人手不够的时候就会请临时工来助力工作,当员工富余了,公司就会将临时工辞退。

jdk8中,提供了4个构造方法,我主要介绍参数最多的那一个构造方法,其他3个构造方法都是基于此构造方法减少了参数,所以搞懂最多参数的构造方法,其他构造方法也就明白了。

//参数最多的一个构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
  • corePoolSize表示核心线程数。
  • maximumPoolSize表示最大线程数,就是核心线程数与非核心线程数之和。
  • keepAliveTime非核心线程最长等待新任务的时,就是非核心线程的最长摸鱼时间,超过此时间,该线程就会被停用。
  • unit 时间单位。
  • workQueue任务队列,通过submit方法将任务注册到该队列中。
  • threadFactory线程工厂,线程创建的方案。
  • handler拒绝策略,由于达到线程边界和队列容量而阻止执行时使用的处理策略。

线程池构造方法参数

其他几个构造方法:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)

🌳那核心线程数最合适值是多少呢?假设CPU有N核心,最适核心线程数是N?是2N?是1.5N?只要你能够说出一个具体的数,那就错了,最适的核心线程数要视情况而定,没有一个绝对的标准的值。

在具体使用线程池时,往往使用的是Executor,因为 Executor是 ThreadPoolExecutor所实现的一个接口,由于标准库中的线程池使用较复杂,对于ThreadPoolExecutor类中的方法我们就不介绍了,最重要的一个方法是submit方法,这个方法能够将任务交给线程池去执行,接下来我们来理一理线程池最基本的工作原理,我们来尝试实现一个简单的线程池。 线程池标准类的族谱

🎵2.线程池的实现

🎶2.1线程池的基本工作原理

线程池是通过管理一系列的线程来执行程序员所传入的任务,这些任务被放在线程池对象中的一个阻塞队列中,然后线程池会调度线程来执行这些任务,优先调度核心线程(核心线程会在线程池对象构造时全部创建),如果核心线程不够用了,就会创建非核心线程来帮忙处理任务,当非核心线程一定的时间没有收到新任务时,非核心线程就会被销毁,我们实现线程池的目的是加深对线程池的理解,所以实现的过程中就不去实现非核心线程了,线程池里面的线程全部以核心线程的形式实现。

🌳我们需要实现一个线程池,根据以上的原理需要准备:

  • 任务,可以使用Runnable。
  • 组织任务的数据结构,可以使用阻塞对列。
  • 工作线程(核心线程)的实现。
  • 组织线程的数据结构,可以使用List。
  • 新增任务的方法submit

🎶2.2线程池的简单实现

关于任务和任务的组织就不用多说了,直接使用Runnable和阻塞队列BlockingQueue<Runnable>就可以了,重点说一下工作线程如何描述的,工作线程中需要有一个阻塞队列引用来获取我们存任务的那一个阻塞队列对象,然后重写run方法通过循环不断的获取任务执行任务。

然后根据传入的核心线程数来创建并启动工作线程,将这些线程放入顺序表或链表中,便于管理。

最后就是创建一个submit方法用来给用户或程序员派发任务到阻塞队列,这样线程池中的线程就会去执行我们所传入的任务了。 线程池基本实现逻辑

🌳实现代码:

class MyThreadPool {
//1.需要一个类来描述具体的任务,直接使用Runnable即可
//2.有了任务,我们需要将多个任务组织起来,可以使用阻塞队列
private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

//3.组织好任务,就可以分配线程池中的线程来执行任务了,所以我们需要描述线程,专门来执行任务
static class Worker extends Thread {
//获取任务队列
private final BlockingQueue<Runnable> queue;
//构造线程时需要将任务队列初始化
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
//重写线程中的run方法,用来执行阻塞队列中的任务
@Override
public void run() {
while (true) {
try {
//获取任务
Runnable runnable = queue.take();
//执行任务
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 4.线程池中肯定存在不止一个线程,所以我们需要对线程进行组织,这里我们可以使用顺序表,使用链表也可以
private final List<Worker> workers = new ArrayList<>();

//根据构造方法指定的线程数将线程存入workers中
public MyThreadPool(int threadNums) {
for (int i = 0; i < threadNums; i++) {
Worker worker = new Worker(this.queue);
worker.start();
this.workers.add(worker);
}
}
// 5.创建一个方法,用来将任务存放到线程池中
public void submit(Runnable runnable) {
try {
this.queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

🌳我们来测试一下我们所实现的线程池:

import java.util.ArrayList;
import java.util.List;

public class ThreadPoolProgram {
private static int NUMS = 1;
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);

for (int i = 0; i < 20; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("第" + NUMS + "个任务!" );
}
});
Thread.sleep(200);
NUMS++;
}
}

🌳运行结果: 线程池运行结果

好了,你知道线程池的工作原理了吗?


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

收起阅读 »

Flutter 小技巧之玩转字体渲染和问题修复

这次的 Flutter 小技巧是字体渲染,虽然是小技巧但是内容略长,可能大家在日常开发中不会特别关心字体相关的部分,而这将是一篇你平时可能用不到 ,但是遇到问题就会翻出来的文章。 本篇将快速普及一些字体渲染相关的基础,解决一些因为字体而导致的异常问题,并穿插...
继续阅读 »

这次的 Flutter 小技巧是字体渲染,虽然是小技巧但是内容略长,可能大家在日常开发中不会特别关心字体相关的部分,而这将是一篇你平时可能用不到 ,但是遇到问题就会翻出来的文章



本篇将快速普及一些字体渲染相关的基础,解决一些因为字体而导致的异常问题,并穿插一些实用小技巧,内容篇幅可能略长,建议先 Mark 后看。



一、字体库


首先,问一个我经常问的面试题:Flutter 在 Android 和 iOS 上使用了哪些字体


如果你恰好看过 typography.dart 的源码和解释,你可以会有初步结论:



  • Android 上使用的是 Roboto 字体;

  • iOS 上使用的是 .SF UI Display 或者 .SF UI Text 字体;


image-20220601135913731


但是,如果你再进一步去了解就会发现,在加上中文显示之后,结论应该是:



  • 默认在 iOS 上:

    • 中文字体:PingFang SC (繁体还有 PingFang TCPingFang HK )

    • 英文字体:.SF UI Text / .SF UI Display



  • 默认在 Android 上:

    • 中文字体:Source Han Sans / Noto

    • 英文字体:Roboto




那这时候你可能会问:.SF 没有中文,那可以使用 PingFang 显示英文吗? 答案是可以的,但是字形和字重会有微妙区别, 例如下图里的 G 就有很明显的不同。


image-20220601141145552


那如果加上韩文呢?这时候 iOS 上的 PingFang.SF 就不够用了,需要调用如 Apple SD Gothic Neo 这样的超集字体库,而说到这里就需要介绍一个 Flutter 上你可能会遇到的 Bug。


如下图所示,当在使用 Apple SD Gothic Neo 字体出现中文和韩文同时显示时,你可能会察觉一些字形很奇怪,比如【推广】这两个字,其中【广】这个字符在超集上是不存在的,所以会变成了中文的【广】,但是【推】字用的还是超集里的字形。


image-20220601141720525


这种情况下,最终渲染的结果会如下图所示,解决的思路也很简单,小技巧就是给 TextStyle 或者 ThemefontFamilyFallback 配置上 ["PingFang SC" , "Heiti SC"]


image-20220601142805434


另外,如果你还对英文下 .SF UI Display 和 ``SF UI Text` 之间的关系困惑的话,那其实你不用太过纠结,因为从 SF 设计上大概意思上理解的话:



.SF Text 适用于更小的字体;.SF Display 则适用于偏大的字体,分水岭大概是 20pt 左右,不过 SF(San Francisco) 属于动态字体,系统会动态匹配。



二、Flutter Text


虽然上面介绍字体的一些相关内容,但是在 Flutter 上和原生还是有一些差异,在 Flutter 中的文本呈现逻辑是有分层的,其中:



  • 衍生自 Minikin 的 libtxt 库用于字体选择,分隔行等;

  • HartBuzz 用于字形选择和成型;

  • Skia作为 渲染 / GPU后端;

  • 在 Android / Fuchsia 上使用 FreeType 渲染,在 iOS 上使用CoreGraphics 来渲染字体


Text Height


那如果这时候我问你一个问题: 一个 fontSize: 100 的 H 字母需要占据多大的高度 ?你会回答多少?


首先,我们用一个 100 的红色 Container fontSize: 100 的 H 文本做个对比,可以看到 H 文本所在的蓝色区域其实是需要大于 100 的红色区域的。


image-20220601145346189


事实上,前面的蓝色区域是字体的行高,也就是 line height,关于这个行高,首先需要解释的就是 TextStyle 中的 height 参数。


默认情况下 height 参数是 null,当我们把它设置为 1 之后,如下图所示,可以看到蓝色区域的高度和红色小方块对齐,变成了 100 的高度,也就是行高变成了 100 ,而 H 字母完整地显示在了蓝色区域内。


image-20220601145634196


height 是什么呢?首先 TextStyle 中的 height 参数值在设置后,其效果值是 fontSize 的倍数:



  • height 为空时,行高默认是使用字体的量度(这个量度后面会有解释);

  • height 不是空时,行高为 height * fontSize 的大小;


如下图所示,蓝色区域和红色区域的对比就是 heightnull1 的对比高度。


image-20220601145710275


所以,看到这里你又知道了一个小技巧:当文字在 Container “有限高度” 内容内无法居中时,可以考虑调整 TextStyle 中的 height 来实现


image-20220601151621858



当然,这时候如果你把 Containerheight:50 去掉,又会是另外一个效果。



所以 height 参数和文本渲染的高度之间是成倍数关系,具体如下图所示,同时最需要注意的点就是:文本内容在 height 里并不是居中,这里的 height 可以类比于调整行高。


image-20220601151923432


另外,文本中的除了 TextStyle 下的 height 之外,还是有 StrutStyle 参数下的 height ,它影响的是字体的整体量度,也就是如下图所示,影响的是 ascent - descent 的高度。


image-20220601152843273


那你说它和 TextStyle 下的 height 有什么区别? 如下图所示例子:



  • StrutStylefroceStrutHeight 开启后,TextStyleheight 不会生效;

  • StrutStyle 设置 fontSize:50 影响的内容和 TextStylefontSize:100 影响的内容不一样;



另外在 StrutStyle 里还有一个叫 leading 的 参数,加上了 leading 后才是 Flutter 中对字体行高完全的控制组合,leading 默认为 null ,同时它的效果也是 fontSize 的倍数,并且分布是上下均分。


所以,看到这里你又知道了一个小技巧:设置 leading 可以均分高度,所以如下图所示,也可以用于调整行间距。


image-20220601154712338



更多行高相关可见 :《深入理解 Flutter 中的字体“冷”知识》



FontWeight


另外一个关于字体的知识点就是 FontWeight ,相信大家对 FontWeight 不会陌生,比如我们默认的 normal 是 w400,而常用的 bold 是 w700 ,整个 FontWeight 列表覆盖 100-900 的数值。


image-20220601155236983


那么这里又有个问题:这些 Weight 在字体里都能找到对应的粗细吗


答案是不行的,因为正常情况下如下图所示 ,有些字体库在某些 Weight 下是没有对应支持,例如



  • Roboto 没有 w600

  • PingFang 没有高于 w600


image-20220601162130629


那你可能好奇,为什么这里要特意介绍 FontWeight ?因为在 Flutter 3.0 目前它对中文有 Bug


从下面这张图你可以看到,在 Flutter 3.0 上中文从 100-500 的字重显示是不正常的,肉眼可以看出在 100 - 500 都显示同一个字重。


image-20220601162935325



这个 Bug 来自于当 SkParagraph 调用 onMatchFamilyStyleCharacter 时,onMatchFamilyStyleCharacter 的实现没有选择最接近 TextStyle 的字体,所以在 CTFontCreateWithFontDescriptor 时会带上 weight 参数但是却没有 familyName ,所以 CTFontCreateWithFontDescriptor` 函数就会返回 Helvetica 字体的默认 weight。



临时解决小技巧也很简单:全局设置 fontFamilyFallback: ["PingFang SC"] 或者 fontFamily: 'PingFang SC' 就可以解决,又是 Fallback , 这时候你就会发现,前面介绍的字体常识,可以在这里快速被利用起来


image-20220601163255325



因为 iOS 上中文就是 PingFang SC ,只要 Fallback 回 PingFang 就可以正常渲染,而这个问题在 Android 模拟器、iOS 真机、Mac 上等会出现,但是 Android 真机上却不会,该问题我也提交在 #105014 下开始跟进。



添加的 Fallback 之后效果如上图左侧所示, 那 Fallback 的作用是什么?


前面我们介绍过,系统在多语言中渲染是需要多种字体库来支持,而当找不到字形时,就要依赖提供的 Fallback 里的有序列表,例如:



如果在 fontFamily 中找不到字形,则在 fontFamilyFallback 中搜索,如果没有找到,则会在返回默认字体。



另外关于 FontWeight 还有一个“小彩蛋”,在 iOS 上,当用户在辅助设置里开启 Bold Text 之后,如果你使用的是 Text 控件,那么默认情况下所有的字体都会变成 w700 的粗体。


image-20220601164236038


因为在 Text 内使用了 MediaQuery.boldTextOverride 判断,Flutter 会接收到 iOS 上用户开启了 Bold Text ,从而强行将 fontWeight 设置为 FontWeight.bold ,当然如果你直接使用 RichText 就 没有这一行为。



这时候小技巧就又来了:如果你不希望这些系统行为干扰到你,那么你可以通过嵌套 MediaQuery 来全局关闭,而类似的行为还有 textScaleFactorplatformBrightness


return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);

image-20220531082324707


FontFeature


最后再介绍一个冷门参数 FontFeature 。


什么是 FontFeature简单来说就是影响字体形状的一个属性 ,在前端的对应领域里应该是 font-feature-settings,它有别于 FontFamily ,是用于指定字体内字的形状参数。



如下图所示是 frac 分数和 tnum 表格数字的对比渲染效果,这种效果可以在不增加字体库时实现特殊的渲染,另外 Feature 也有特征的意思,所以也可以理解为字体特征。



image-20220601165224593


那 FontFeature 有什么用呢? 这里又有一个使用小技巧了:当出现数字和文本同时出现,导致排列不对齐时,可以通过给 Text 设置 fontFeatures: [FontFeature("tnum")] 来对齐


例如下图左边是没有设置 fontFeatures 的情况,右边是设置了 FontFeature("tnum") 的情况,对比之下还是很明显的。


image-20220601165855711



更多关于 FontFeature 的内容可见 《Flutter 上字体的另类玩法:FontFeature 》



三、最后


总结一下,本篇内容信息量相对比较密集,主要涉及:



  • 字体基础

  • Text Height

  • FontWeight

  • FontFeature


从以上四个方面介绍了 Flutter 开发里关于字体渲染的“冷知识”和小技巧,包括:解决多语言下的字体错误、如何正确调整行高、如何对其数字内容等相关小技巧。


如果你还有什么关于字体的疑问,欢迎留言讨论~


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

怎么才能不焦虑?

什么是焦虑?引用百度百科里的解释:焦虑是人对现实或未来事物的价值特性出现严重恶化趋势所产生的情感反映 。与之相反的情感形式是企盼,即企盼是人对现实或未来事物的价值特性出现明显利好趋势所产生的情感反映。焦虑是指个人对即将来临的、可能会造成的危险或威胁所产生的紧张...
继续阅读 »

什么是焦虑?

引用百度百科里的解释:

焦虑是人对现实或未来事物的价值特性出现严重恶化趋势所产生的情感反映 。与之相反的情感形式是企盼,即企盼是人对现实或未来事物的价值特性出现明显利好趋势所产生的情感反映。

焦虑是指个人对即将来临的、可能会造成的危险或威胁所产生的紧张、不安、忧虑、烦恼等不愉快的复杂情绪状态。

所以焦虑的客观目的是有益的,作用在于引导人迅速地采取各种措施,紧急调动各种价值资源,以有效地阻止现实或未来事物的价值特性出现严重恶化的这种趋势,使之朝着利好的方向发展。

试想在远古时期,正是因为人类在残酷的大自然环境下,对食物、居住、猛兽等东西的焦虑,才能让人类及时采取行动,保证人类延续下去。

所以焦虑是一种正常的人类反应,但如果你经常焦虑,很容易因为一些小事感到不安,并伴随着极度紧张、头痛、心悸、心悸、烦躁不安、胃部有坠胀感等症状,很有可能,你患了焦虑症,我更建议你去医院就医。

我在查阅资料的时候,看到很多人推荐这本《精神焦虑症的自救(病理分析卷)》,豆瓣评分 8.2,你也可以参考。

不过大部分人应该还没到这么严重,面对焦虑,我有以下几个建议:

  1. 接受,而非逃避或者抗争

面对焦虑,我们常有两种回应方式。

第一种是逃避,用所谓的“忙碌”来填补自己的内心,只不过不同的人选择“忙碌”的方式不一样。有的人用工作逃避,这种方式最大的好处在于“有理有据”,面对别人的劝说或指责,可以给自己找到看似合理的借口,比如“我这是在工作,在干重要的事情呢”,或者“工作那么忙,哪有时间”,其实内心深处比谁都知道,这不过是自欺欺人的借口。跟工作相似的,还有阅读、家务等,这些都可以给自己找出合理且正向的借口。

有的人则是用奶头乐逃避,听音乐看动漫打游戏,看新闻刷抖音再刷朋友圈,各个软件都点开一遍去除小红点,明明没有动态还总觉得下次点开的时候会有消息,里面的内容自己也知道,但还是会再看一遍,实在没事干了,平时懒得干的事情也会捡起来,整理图片文件夹,换皮肤改字体,疯狂的寻找各种干起来不费力又有一点价值和快乐的事情。

但无论是哪种,一停下来,焦虑的情绪就会立刻涌现,让自己坐立不安,然后又继续逃避,陷入恶性循环之中。

第二种方式是抗争,为了摆脱焦虑的情绪,于是反复思考这些想法和情绪,试图找到解决方案。但是当你焦虑的时候,你越思考,你越觉得这些其实不怎么靠谱的想法像是真的,最后反而让这些情绪和所谓更加牢固,又形成了一个恶性循环。

难道我们就没有其他回应方式了吗?当然是有了,这就是很多书中提到的最核心的方法 —— 接受。然而怎样才算是接受呢?

我觉得这步最重要的就是放弃。放弃抗争,放弃试图控制控制恐惧的努力以及通过不断的自我分析“应该做什么”的想法。放弃逃避,允许意识中焦虑的存在,并愿意与它一起共存,毕竟它的存在客观目的还是帮助我们。

如果你只是有焦虑的情绪,那你可以告诉自己“这些情绪只是暂时的,很快就会消失,我允许它的存在,现在我可以尝试感受下这种情绪,因为很快它就会流走”。

而如果你还焦虑到胃痉挛,那你就做好准备,随便胃痉挛,然后干你要做的事情。

那你接受之后,胃痉挛就会立刻好吗?那当然不可能,通常你不做挣扎时候,情绪会慢慢的平静下来,症状也会慢慢的减弱,但你的身体已经习惯性的对这种焦虑做出了应激反应,消除这种应激则需要更长的时间。

  1. 多读书

如果你直接去百度搜索“焦虑如何治疗”,你会看到很多这样的论述,“我看了大量的书籍,尝试了无数的方法,都是治标不治本……”然而这种鬼话你可千万不要相信,就以为读书无用。

实际上,关于焦虑,人们早已做了多年研究和探索,不能说成熟,但至少也有比较有效的理论、方法和实践,多找些大家普遍推荐的、评分高评价好的书籍去看,尤其是那些关于焦虑原理的书籍,对焦虑有更加清晰的认知,会帮助你面对焦虑,毕竟“知彼知己,百战不殆”。

对抗焦虑这种事情,最重要的是还是靠自己,佛不渡人人自渡,从书中探索方法,搞不好哪个方法就对你有效果。比如我就在书中看到这样一个方法,每次焦虑的时候,就把事情写下来:某年某月某日,我很焦虑,我焦虑的内容是什么,我担心的事情具体是什么,然后等事情过去了,再写上真实的结果。

这样的事情多记上几件,很快你就会发现,那些你担心的事情往往一件也没有发生。所以再当焦虑的事情发生时,我想起过往的那些焦虑经历,就觉得,害,这次又不过是个纸老虎而已。焦虑的情绪就化解了许多。

当然除了直接讲焦虑的书籍,我认为更应该看些其他书籍,比如人文社科,天文地理等,多思考宇宙、人生、国家、社会、自然这种宏大尺度的事物,不能说是对当下的苦难的降维打击,但在这些宏大事物的衬托下,生活中的那些看似紧急的事情也变得无所谓了一些,情绪自然就缓和了。

除了这种好处之外,长期以来的读书、思考和实践,会帮助你突破认知限制,在面对很多事情时,你可以站在更高的思维层面上,对事情的结果有清晰的认知,自然不会再对这些事情感到焦虑。

  1. 冥想

冥想之所以有用,在于其中的正念训练可以让我们不被想法和情绪劫持。冥想不是努力把阴云赶走,而是搬把椅子到花园里,坐看云卷云舒,面对脑海中的那些思绪,无论是否讨厌,无论是否喜欢,不强求、不抗拒、不挣扎,坦然的接受自己。

运用心理学中的术语就是“认知解离”,将自我从思维内容、记忆感觉、语言规则中分离,客观地注视思想活动如同观察外在事物,将想法看作是语言和文字本身,而不是它所代表的事物。

冥想就是最好的练习认知解离的方法。

然而面对脑海中的情绪,人们尝尝无法控制自己,很容易就深陷其中。所以冥想需要从日常就开始练习起来,如果只有当焦虑来袭的时候,你才想到要冥想,往往会因为自己水平不到家,反而被焦虑折磨,最后陷入逃避或者抗争的恶性循环之中。

  1. 行动

有一个人十分崇拜杨绛。高中快毕业的时候,他给杨绛写了一封长信,表达了自己对他的仰慕之情以及自己的一些人生困惑。

杨绛回信了,淡黄色的竖排红格信纸,毛笔字。除了寒暄和一些鼓励晚辈的句子外,杨绛的信里其实只写了一句话,诚恳而不客气:

“你的问题主要在于读书不多而想得太多”。

这句话用来形容焦虑也不为过。焦虑本质上是对未来的不确定性而产生的情绪,行动之所以能让我们克服焦虑,是因为我们可以通过行动让我们对未来的不确定性有更加清楚的认知,甚至可以让我们扭转这种不确定性为确定性。

所以如果自己对未来的某件事情感到焦虑,那就开始准备,准备到自己满意为止,把注意力放在自己能改变的事情,做到尽善尽美,虽然结构可能依然有不确定性,但万一实现了呢,就算没有实现,但你要相信,人生是一个很长的旅途,我们所做的这些事情,也许当下没有取得应有的结果,但很可能会在未来回馈到你。

“吾尝终日不食,终夜不寝,以思,无益,不如学也”,与其焦虑,不如从日常练习冥想,多阅读好书开始做起,当面对焦虑的时候,不要逃避,不要抗争,尝试着去感受它,接受它,然后伴随着行动,逐渐的将焦虑化解。

来源:冴羽答读者问

收起阅读 »

Flutter极简状态管理库Creator

我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。先看一下它的co...
继续阅读 »

我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。

这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。

先看一下它的counter例子:

// 定义状态
final counter = Creator.value(0);
Widget build(BuildContext context) {
return Column(
  children: [
    // 响应状态
    Watcher((context, ref, _) => Text('${ref.watch(counter)}')),
    TextButton(
      // 更新状态
      onPressed: () => context.ref.update<int>(counter, (count) => count + 1),
      child: const Text('+1'),
    ),
  ],
);
}

它的核心概念极其简单,只提供两种creator:

  • Creator 产生一系列的 T

  • Emitter 产生一系列的 Future<T>

这里T可以是任何类型,甚至可以是Widget。然后它把所有的creator都组织成一个有向图(叫做Ref)。

还是举一个官网的例子吧。可以在DartPad上跑,显示摄氏温度或者华氏温度:



// repo.dart

// 假装调用一个后端API。
Future<int> getFahrenheit(String city) async {
await Future.delayed(const Duration(milliseconds: 100));
return 60 + city.hashCode % 20;
}
// logic.dart

// 简单的creator
final cityCreator = Creator.value('London');
final unitCreator = Creator.value('Fahrenheit');

// 可以像Iterable/Stream那样使用 map, where, reduce 之类的.
final fahrenheitCreator = cityCreator.asyncMap(getFahrenheit);

// 组合不同的creator,产生新的业务逻辑。
final temperatureCreator = Emitter<String>((ref, emit) async {
final f = await ref.watch(fahrenheitCreator);
final unit = ref.watch(unitCreator);
emit(unit == 'Fahrenheit' ? '$f F' : '${f2c(f)} C');
});

// 摄氏华氏温度转换
int f2c(int f) => ((f - 32) * 5 / 9).round();
// main.dart

Widget build(BuildContext context) {
return Watcher((context, ref, _) =>
    Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading'));
}
... context.ref.set(cityCreator, 'Pairs'); // 会调用后端API
... context.ref.set(unitCreator, 'Celsius'); // 不会调用后端API

可以看出,当用户改变所选城市之后, 状态会沿着图中的箭头传导,一直传到最后的Creator<Widget>,从而更新UI。

我觉得这个有向图的设计还是非常独特的,很好理解,也很简单。组织比较复杂的业务逻辑的时候非常方便。

这个库的核心代码才500行,感兴趣的同学可以去看官方文档和代码。

欢迎讨论!

作者:Jay_Guo
来源:juejin.cn/post/7107433326054473736

收起阅读 »

关于 async/await 你应该认真对待下

web
深入理解 async/await一个语法糖 是异步操作更简单返回值 返回值是一个 promise 对象return 的值是 promise resolved 时候的 valueThrow 的值是 Promise rejected 时候的 reasonasync...
继续阅读 »

无论是在项目还是在面试过程中,总还是会有那么一小部分同学,没有学会使用 async/await ,今天就特地整理了几个代码段,并以此文进行提醒大家常用的技术点还是要会的,不单单只是应对面试需要,在日常工作中使用,也会提升你的效率及代码质量的,不必每次都使用 .then 进行处理,错误输出可以写个公共方法,统一处理。⛽️ 加油,共勉!!! 无论是在项目还是在面试过程中,总还是会有那么一小部分同学,没有学会使用 async/await ,今天就特地整理下并提醒大家常用的技术点还是要会的,不单是为了应对面试需要,日常工作中也是有利无害的

深入理解 async/await

async 函数

  • 一个语法糖 是异步操作更简单

  • 返回值 返回值是一个 promise 对象

    • return 的值是 promise resolved 时候的 value

    • Throw 的值是 Promise rejected 时候的 reason

async function test() {
 return true
}
const p = test()
console.log(p) // 打印出一个promise,状态是resolved,value是true

// Promise {: true}
//   [[Prototype]]: Promise
//   [[PromiseState]]: "fulfilled"
//   [[PromiseResult]]: true

p.then((data) => {
 console.log(data) // true
})
async function test() {
 throw new Error('error')
}
const p = test()
console.log(p) // 打印出一个promise,状态是rejected,value是error
p.then((data) => {
 console.log(data) //打印出的promise的reason 是error
})

可以看出 async 函数的返回值是一个 promise

await 函数

  • 只能出现在 async 函数内部或最外层

  • 等待一个 promise 对象的值

  • await 的 promise 的状态为 rejected,后续执行中断

await 可以 await promise 和非 promsie,如果非 primse,例如:await 1 就返回 1


await 为等待 promise 的状态是 resolved 的情况

async function async1() {
 console.log('async1 start')
 await async2() // await为等待promise的状态,然后把值拿到
 console.log('async1 end')
}
async function async2() {
 return Promsie.resolve().then(_ => {
   console.log('async2 promise')
})
}
async1()
/*
打印结果
async1 start
async2 promise
async1 end
*/

await 为等待 promise 的状态是 rejected 的情况

async function f() {
 await Promise.reject('error')
 //后续代码不会执行
 console.log(1)
 await 100
}

// 解决方案1
async function f() {
 await Promise.reject('error').catch(err => {
   // 异常处理
})
 console.log(1)
 await 100
}

// 解决方案2
async function f() {
 try {
   await Promise.reject('error')
} catch (e) {
   // 异常处理
} finally {
}
 console.log(1)
 await 100
}

async 函数实现原理

实现原理:Generator+自动执行器

async 函数是 Generator 和 Promise 的语法糖

应用

用 async 函数方案读取文件

const fs = require('fs')

async function readFilesByAsync() {
 const files = [
   '/Users/xxx/Desktop/Web/1.json',
   '/Users/xxx/Desktop/Web/2.json',
   '/Users/xxx/Desktop/Web/3.json'
]
 const readFile = function(src) {
   return new Promise((resolve, reject) => {
     fs.readFile(src, (err, data) => {
       if (err) reject(err)
       resolve(data)
    })
  })
}

 const str0 = await readFile(files[0])
 console.log(str0.toString())
 const str1 = await readFile(files[1])
 console.log(str1.toString())
 const str2 = await readFile(files[2])
 console.log(str2.toString())
}

作者:Gaby
来源:juejin.cn/post/7108362437706907685

收起阅读 »

大一女生废话编程爆火!懂不懂编程的看完都拴Q了

她的日更作业,竟让网友直呼: 中国计算机界的神!短短两个星期的时间里,这个女大学生实力吸引了40万+粉丝,超260万的点赞。而平日里底下的评论画风都是这样:这是量子计算机的计算演示吗,好强。顶级黑客诶,好厉害。……甚至还有网友改变了对计算机的认知:&...
继续阅读 »

她的日更作业,竟让网友直呼: 中国计算机界的神!

短短两个星期的时间里,这个女大学生实力吸引了40万+粉丝,超260万的点赞。

而平日里底下的评论画风都是这样:

这是量子计算机的计算演示吗,好强。

顶级黑客诶,好厉害。

……

甚至还有网友改变了对计算机的认知: 学计算机原来可以这么有趣 。

这 究竟是人性的扭曲还是道德的沦丧,到底是什么样的作业,能让网友赞叹至此?

她的代表作 《身高计算器》,被评价是“市面上最准的计算器”、“完全0误差”!

这究竟是如何做到?带着这样的好奇,我们研究了下。

软件还可以这么开发?只有你想不到

按照她本人的说法,因为学校老师的规定,需要每天发布一款“精心制作”的软件作业。

于是从上个月25开始,陈同学几乎就没怎么断更。

先来说说她的代表作《身高计算器》。

只需 输入身高就能立马测身高,计算过程都已经快到飞起,而且有在准的。

还有另一个被网友“捧上天”代表作:《分秒转换器》,还会出现2分钟= 1分20秒这类的大智若愚。

有网友表示:“说实话,看到这个结果的时候我犹豫了一下。”“黑客,这绝对是黑客!”

如果说,前面提到的这两大代表作,有些人可能还会觉得也不过如此。

那么,你就着实小看陈同学了。因为她会用实际行动告诉你:软件已经不再是软件,已经升级成高情商了属于是。

网友提到的这个软件叫做《体重计算器》,顾名思义跟前面两个类似,但不同的是,它多少带点人情世故。

比如,如果你输入90,它会告诉你只有80多斤;你输入199,它告诉你只有100多斤……

太会了有木有?哪个女朋友不会想拥有一个这么“准”的体重计算器!

还有当你在纠结买什么水果的时候,那么陈同学《水果选择器》也不能错过。

这个选择器不仅可以帮你确定买的水果,而且还考虑到 怕你再次选择,就直接关机

不得不说,很贴心了。

除开这些“高情商”的软件,还有能真正展现陈同学技术流的东西。

比如,调戏操作系统一下~

就像这个《猫猫回收站》。

只需轻轻一点,回收站就可以变成猫咪,日常可以投递文件来喂食。

底下网友还给出改进的建议,让垃圾移到猫猫身上时再让它张嘴,或者做成动图咀嚼一会,或许会更意思。

还有帮你系统优化的,你可以选择你的需求:清理垃圾、清理内存、系统修复、加速优化。

在经过一顿严密的计算分析之后,这个软件直接霸气给出解决方式——

帮你点开系统安装的360

这个操作可是再次把大家都给震住了:360都能命令,绝对是顶级黑客啊。

你以为这就完了吗?

NONONO,毕竟顶级黑客这个称呼可是靠眼见为实的!

来看这个 《123木头人》

表面上看,点任何按钮都会关机。但实际并没有那么简单。

它先是给你虚晃几枪,可就当你卸下心防,以为没啥事儿了的时候,电脑真的被点关机了!

对此,陈同学本人则轻描淡写地说一句,这是用来测试人的灵敏度和反应力的~

各位看官,是不是下巴都惊掉了?

值得一提的是,她还建了一个qq群,专门分享这些软件包供人下载玩耍,结果加群的人还真不少。

最新群已经是第6个了,然后不到几个小时的功夫,就又上千人了。

ps. 不过有一点蹊跷的是,她没有给源码,要想自己改程序玩一下,还得去找她 定制

另外也问了一些计算机的同学,陈同学这样一天一个软件的作业量,着实是有点大的。况且还是大一的学生。

当然要是都提交类似这样式儿的作业的话,其实也还行。

废话软件真的栓Q

而在抖音上出现的废话软件,还远不止陈依涵的日更。

在“计算机神”出现之后,“凡间”纷纷出现了效仿者,编程水平有过之而不及。

比如,这个神和形都相似的性格测试器,网友就直接叫陈依涵来pk。

甚至有人还帮着陈依涵同学专门做起了优化。嗯,跟他们此前日常画风截然不同。

随便点进去一看,真的会栓Q~

那么就以那个著名的(120秒=1分20秒)时分转换器为例,这位小哥贴心修复了bug,还增加了嘲讽buff。

数据小于120秒,你就会被计算机嘲讽: 太拉了,这么简单还需要算?

怎么样,是不是高级了不少。

同样高级不少的还有这个免费起名器,任意输入老公or老婆姓名,就可以跳出孩子姓名,还附赠了隔壁老王的惊喜。

除了优化党,还有智慧的网友开始举一反三,另辟蹊径。

有实用的测试工具,比如屏幕亮灭检测、联网检测、开机状态检测。

还做成手机APP了呢~

不得不说,这波技术水平直接拉升~

还有人做了个随机点名系统,然后成功吸引到了陈依涵的注意。

网友:为无聊的生活增添了无聊

对于这波废话软件在抖音上风行,不少网友真心表示: 为无聊的生活增添了无聊

虽然无聊,还是忍不住再看亿遍。于是不少网友开始各种玩梗。

失去体:“中国计算机界不能失去陈依涵,就像西方不能失去耶路撒冷。”“开发届不能失去你,就像人不能失去阑尾”,“就像鱼不能没有自行车”

反对体:“当年你退出软件开发我是极力反对的。”

养成对比体:“今天又是一整个大进步”“比之前的身高计算器强多了”

夸夸体:“太神奇了吧,它是怎么知道的。”“天哪好准,可以帮我计算一下吗”“顶级黑客诶,好厉害”

除了增加一些“无聊”的乐趣外,也有不少网友表示,看了陈依涵的视频之后,学计算机竟然可以这么有趣。

好了,对于这波废话软件,你怎么看?

来源:量子位公众号

收起阅读 »

Py大蟒蛇搞机器人

itchat使用教程itchat是一个开源的微信个人号接口,使用python调用微信从未如此简单。使用不到三十行的代码,你就可以完成一个能够处理所有信息的微信机器人。首先,在终端安装一下itchat:#pip是pyth的包管理工具也就是pyth的应用商店专门用...
继续阅读 »

itchat使用教程

itchat是一个开源的微信个人号接口,使用python调用微信从未如此简单。使用不到三十行的代码,你就可以完成一个能够处理所有信息的微信机器人。

首先,在终端安装一下itchat:

#pip是pyth的包管理工具也就是pyth的应用商店专门用来安装和卸载库
pip install itchat

1.登录

  1. login() - 每次运行程序都需要扫二维码

  2. login(hotReload==True) - 下次登录不用再扫二维码

  3. auto_login(loginCallback=登录成功回调函数, exitCallback=退出登录回调函数)

2.退出登录

  1. logout() - 强制退出登录

3.获取好友信息

  1. get_friends(update=True) - 获取所有的好友信息

  2. get_chatrooms() - 获取群组

  3. get_mps() - 获取公众号

  4. get_msg() - 获取消息列表

  5. get_head_img() - 获取个人头像

4.发送消息

send(msg=消息内容, toUserName=用户名)

1).msg的值会因为消息类型不同而不同:

  • 文本消息 - 引号中直接写要发送的文字内容

  • 发送文件 - @fil@文件路径

  • 发送图片 - @img@图片路径

  • 发送视频 - @vid@视频路径

2).toUserName: 发送对象,如果不填就发送给自己

5.接收消息

想要自动接收消息,需要先对不同类型的消息进行注册,如果没有注册,对应类型的消息将不会被接收.

注册的方式如下:

@itchat.msg_register(消息类型,isFriendChat=True, isGroupChat=True,isMpChat=True)

def 函数名(msg):
#接收到对应的消息会自动执行的代码段
  #msg.download(msg['FileName'])   #这个同样是下载文件的方式
  #msg['Text'](msg['FileName'])     #下载文件

1)消息类型:

参数类型Text键值
TEXT文本文本内容(文字消息)
CARD名片推荐人字典(推荐人的名片)
SHARING分享分享名称(分享的音乐或者文章等)
RECORDING语音下载方法
ATTACHMENT附件下载方法
VIDEO小视频下载方法
FRIENDS好友邀请添加好友所需参数
SYSTEM系统消息更新内容的用户或群聊的UserName组成的列表
MAP地图位置文本(位置分享)
NOTE通知通知文本(消息撤回等)
PICTURE图片/表情下载方法

来源:blog.csdn.net/weixin_46014553/article/details/110200748

收起阅读 »

Github最炫酷编辑器Atom即将退休:挺了11年,终究败给VS Code

拥抱技术,将技术扩展到其他产品中,最后消灭技术。2022年12月15日,微软旗下的GitHub计划关停Atom,这款知名的开源文本编辑器启发并影响了众多广泛使用的商业应用软件,比如Microsoft Visual Studio Code、Slack和GitHu...
继续阅读 »

拥抱技术,将技术扩展到其他产品中,最后消灭技术。

2022年12月15日,微软旗下的GitHub计划关停Atom,这款知名的开源文本编辑器启发并影响了众多广泛使用的商业应用软件,比如Microsoft Visual Studio Code、Slack和GitHub Desktop。

这家社交代码公司表示,这么做是为了把精力集中到基于云的软件上。

GitHub近日解释道:“虽然发展壮大软件创建者社区的这个目标依然不变,但我们决定弃用Atom,以便进一步兑现我们的承诺,即通过Microsoft Visual Studio Code和GitHub Codespaces,将快速可靠的软件开发引入到云端。”

GitHub Codespaces是一种集成了Visual Studio Code的云托管开发环境。

2018年6月微软收购GitHub,时任首席执行官的Nat Friedman向GitHub社区保证,Atom还活着,并且很好。

Friedman在Reddit的“随便提问题”讨论中说道:“Atom是一款出色的编辑器,拥有健康的社区、狂热的粉丝、出众的设计,涉足实时协作且初见成效。在微软,我们平常已经在使用从Atom、VS Code、Sublime到Vim的各种编辑器,我们希望开发人员可以使用偏爱与GitHub结合使用的任何编辑器。”

“因此将来,我们将继续开发并支持Atom和VS Code。”

经过这四年的发展后,Atom却停滞不前。据GitHub声称,除了维护和安全更新外,该项目已好几年没有重大的功能开发。在此期间,社区参与度有所下降,本地安装的软件这块业务现在看起来不如基于云的应用软件那么诱人,后者带来了潜在的经常性收入、客户黏度和信息收集。

Atom可以追溯到2011年的GitHub;2015年,Atom shell(用于与Chromium、Node.js和原生API集成的单独组件)更名为Electron(一种基于Web技术的跨平台应用程序框架),此后微软开始致力于在Atom、Electron以及后来成为Visual Studio Code的技术上的GitHub。

这种关系现在遵循因微软而家喻户晓的模式:拥抱、扩展再熄灭(embrace, extend, extinguish),不过弃用Atom似乎更像是摆脱向云转型的包袱,而不是战略上有利的出击。

GitHub发言人在电子邮件中告诉IT外媒The Register:“我们想在未来几年致力于我们的核心项目,这意味着专注于增强云端开发者体验。还有许多功能强大的Atom替代工具可以满足各种需求,VS Code已经获得了巨大的市场份额,我们对这种变化感到很放心。”

“这对GitHub的开发者生态系统应该没有什么影响。GitHub的API将继续得到支持,使开发者能够面对成千上万的其他产品与GitHub进行集成。我们还维护自己的一套应用软件,包括GitHub Desktop、GitHub Mobile和GitHub CLI。”

Atom的影响力通过Electron框架应该可以继续感受得到。Electron.js仍然充当Discord、Skype、Slack、Trello和Visual Studio Code等其他应用软件的基础。但是技术在发生变化。微软此前曾表示,它打算远离Teams中的Electron。而其他跨平台框架可能受到追捧,比如Flutter、Tauri或微软最近宣布的.NET Multi-platform App UI(.NET MAUI)。

不过,Atom看起来在2022年12月15日关停之后继续存在。虽然GitHub有意将Atom存储库归档,但代码是开源的,任何想要支持该项目的人都可以使用。

来源:云头条

收起阅读 »

flutter简单优秀的开源dialog使用free_dialog

前言 今天我来介绍一款简单,易用的dialog,该dialog比较简洁,可以适应很多app(主要没有其他动画及以外的图片等,黑白风格可以适配多种样式的app)。如果你的app急需一款不错的dialog,并且你懒得开发,那么用这款就对了。 开始 集成dialog...
继续阅读 »

前言


今天我来介绍一款简单,易用的dialog,该dialog比较简洁,可以适应很多app(主要没有其他动画及以外的图片等,黑白风格可以适配多种样式的app)。如果你的app急需一款不错的dialog,并且你懒得开发,那么用这款就对了。


开始


集成dialog


dependencies:
free_dialog: ^0.0.1

git地址:github.com/smartbackme…


简单使用


例1(输入文字框):


FreeDialog(context: context,title: "请输入文字",
iWidget: EditWidget(_controller!),
btnOkOnPress: (a){
print(a);

},
btnCancelOnPress: (){

},
onDissmissCallback: (DismissType type){
print(type);

}
).show();
}, child: const Text("输入文字框")),


说明:free_dialog 提供了多种Widget 配置(目前有,list&edit两种),可以快速集成。



展示效果如下图:
在这里插入图片描述


例2(选择框):


FreeDialog(context: context,title: "请选择",
onDissmissCallback: (DismissType type){
print(type);

},
iWidget: ListWidget(["123","1233","12312","12312","12312","12312","12312","12312","12312","12312","12312","12312","12312"],(a){
print(a);

},)
).show();
}, child: const Text("选择框")),

展示效果如下图:
在这里插入图片描述


例3(内容多dialog):


FreeDialog(context: context,title: "提示",
desc: "123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321",
btnOkOnPress: (a){
print(a);

},
btnCancelOnPress: (){

},
onDissmissCallback: (DismissType type){
print(type);

}
).show();

展示效果如下图:


在这里插入图片描述


例4(内容多dialog,单按钮):


FreeDialog(context: context,title: "提示",
desc: "123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321123123211231232112312321",
btnOkOnPress: (a){
print(a);

},

onDissmissCallback: (DismissType type){
print(type);

}
).show();

展示效果如下图:


在这里插入图片描述


例5(内容少dialog):


FreeDialog(context: context,title: "提示",
desc: "111",
btnOkOnPress: (a){
print(a);

},

onDissmissCallback: (DismissType type){
print(type);

}
).show();

展示效果如图:
在这里插入图片描述


例6(单提示):


FreeDialog(context: context,title: "提示",
desc: "12312",
onDissmissCallback: (DismissType type){
print(type);

}
).show();

展示效果如图所示:
在这里插入图片描述


例7(禁止退出 dialog):


FreeDialog(context: context,title: "提示",
desc: "1111",
dismissOnTouchOutside: false,
dismissOnBackKeyPress: false,
btnCancelOnPress: (){

},

onDissmissCallback: (DismissType type){
print(type);

}
).show();

支持的定制



属性类型描述默认属性
widthdoubledialog宽度屏幕窄边的80%
titleString设置title不传的话默认是没有title的
descString设置普通 框的文字内容没有的话不展示,如果有设置body和iwidget的话也不展示
bodyWidget自定义widgetNull
contextBuildContext@requiredNull
btnOkTextStringok文字'Ok'
btnOkOnPressFunction点击okNull (如果传了则会展示ok)
btnOkColorColorok颜色Color(0xFF00CA71)
btnOkWidget传一个 ok组件null
btnCancelTextString取消'Cancel'
btnCancelOnPressFunction点击取消Null (如果传了则会展示cancle)
btnCancelColorColor颜色 取消Colors.red
btnCancelWidget传一个cancle组件null
dismissOnTouchOutsidebool点外部关闭true
onDismissCallbackFunction退出弹框回调Null
animTypeAnimType动画类型AnimType.SCALE
alignmentAlignmentGeometry排版Alignment.center
useRootNavigatorbool是否用 useRootNavigatorfalse
autoHideDuration自动消失null
keyboardAwarebool是否随着键盘移动(填充键盘区域)true
dismissOnBackKeyPressbool返回键退出true
buttonsBorderRadiusBorderRadiusGeometry按钮 RadiusBorderRadius.all(Radius.circular(100)
dialogBackgroundColorColordialog背景Theme.of(context).cardColor
borderSideBorderSide整个弹窗形状null
iWidgetIWidget通用定义widget(源码带有edit和list)null


收起阅读 »

一文搞明白协程的挂起和恢复

协程是使用非阻塞式挂起的方式来实现线程运行的。那协程又是如何挂起和恢复的,这里面的概念又是什么,带着这些问题就让我们重新探究下协程的挂起和恢复。我们先创建个协程:override fun initView() { lifecycleScope....
继续阅读 »

协程是使用非阻塞式挂起的方式来实现线程运行的。那协程又是如何挂起和恢复的,这里面的概念又是什么,带着这些问题就让我们重新探究下协程的挂起和恢复。

我们先创建个协程:

override fun initView() {
lifecycleScope.launch {
val num = dealA()
dealB(num)
}
}

private suspend fun dealA():Int {
withContext(Dispatchers.IO) {
delay(3000)
}
return 1
}

private suspend fun dealB(num:Int) {
withContext(Dispatchers.IO) {
delay(1000)
}
}

可以看到写协程的时候要在函数前面加上suspend修饰,这也是常说的挂起函数,那挂起函数又是什么?

挂起函数

了解之前,我们先将上面的挂起函数dealA()反编译成 Java,简单的看看编译后是什么样的?(省略了后面会着重解释的一些代码,主要先看挂起函数的方法)

private final Object dealA(Continuation var1) {
......

Object $result = ((<undefinedtype>)$continuation).result;
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
CoroutineContext var10000 = (CoroutineContext)Dispatchers.getIO();
Function2 var10001 = (Function2)(new Function2((Continuation)null) {
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
.......

return Unit.INSTANCE;
}
......
if (BuildersKt.withContext(var10000, var10001, (Continuation)$continuation) == var4) {
return var4;
}
break;
......
}

return Boxing.boxInt(1);
}

可以看到suspend经过反编译后,会出现Continuation类型的参数传进去,并且返回的是Object对象。

是不是对Continuation是什么很好奇,这也是协程的核心部分`:

public interface Continuation<in T> {
//对应于这个延续的协程的上下文
public val context: CoroutineContext

//继续执行相应的协程,传递一个成功或失败的 [result] 作为最后一个暂停点的返回值。
public fun resumeWith(result: Result<T>)
}

从定义上可以看出Continuation其实就是一个带有泛型参数的callback,而resumeWith也就相当于onSuccess的成功回调,来恢复执行后面的代码,除这个之外,还有一个ContineContext,它就是协程的上下文。

回到dealA方法中,当执行到withContext方法的时候,会返回CoroutineSingletons.COROUTINE_SUSPENDED,表示函数被挂起了,到这里你是不是觉得就结束了,其实还没有。

在查看过程中是不是看到有个invokeSuspend的回调方法还没有被调用,这又是在什么时候会被触发的?

那我们就从刚才执行到的withContext那里进一步查看,写过协程的都知道这就是用来切换线程:

public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// compute new context
val oldContext = uCont.context
val newContext = oldContext + context
// always check for cancellation of new context
newContext.ensureActive()
// FAST PATH #1 -- 新上下文与旧上下文相同
if (newContext === oldContext) {
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
// FAST PATH #2 新的调度程序与旧的调度程序相同
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
val coroutine = UndispatchedCoroutine(newContext, uCont)
// 上下文有变化,所以这个线程需要更新
withCoroutineContext(newContext, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
// SLOW PATH -- 使用新的调度程序
val coroutine = DispatchedCoroutine(newContext, uCont)
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
}
}

withContext方法中,传入了两个参数,一个是协程的上下文,另一个就是协程里的代码。可以看到不管新的调度和旧的调度一样最后都是会调用startCoroutineCancellable方法:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
receiver: R, completion: Continuation<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
) =
runSafely(completion) {
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
}

而在startCoroutineCancellable方法中,创建了Coroutination,之后会调用resumeCancelableWith方法:

public fun <T> Continuation<T>.resumeCancellableWith(
result: Result<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
): Unit = when (this) {
is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
else -> resumeWith(result)
}

在这里是不是看到了我们之前提到过的resumeWith方法,之前也解释了下它就相当于一个回调。然后我们再来看下它的具体实现,是在ContinuationImpl类中:

public final override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {

probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
.......
}
}
}

resumeWith方法中执行到了我们一直在找的invokeSuspend,通过这个方法将result回调了出去,并判断当前是不是COROUTINE_SUSPENDED(挂起),是挂起直接退出,去执行上面说到的invokeSuspend里面的内容。

在这里我们了解到invokeSuspend是由resumeWith所触发的,那接下来我们看看真正的挂起和恢复如何被执行的。

协程的启动

了解挂起和恢复的过程,要从协程的启动执行开始,我们还是跟刚才一样反编译启动协程的代码:

 BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
......
}

@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}

public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);

在协程启动的反编译代码我们又看到了ininvokeSuspend方法,这个方法又是在最下面创建了Continuation,之后在invoke中被调用,更多的信息是看不出来了。我们还是回到launch源码内部里面去寻找答案。

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

//coroutine.start
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
start(block, receiver, this)
}

当查看到start这里的时候,你会发现跟进不下去了。那我们就换种方法,还是将这个类反编译下,你会看到变成了这样:

 public final void start(@NotNull CoroutineStart start, Object receiver, @NotNull Function2 block) {
Intrinsics.checkNotNullParameter(start, "start");
Intrinsics.checkNotNullParameter(block, "block");
start.invoke(block, receiver, (Continuation)this);
}

//使用此协程启动策略将带有接收器的相应块作为协程启动
@InternalCoroutinesApi
public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit // will start lazily
}

在这里又看到了我们熟悉的startCoroutineCancellable,由于默认值为CoroutineStart.DEFAULT,所以该方法会被调用。后面会怎么调用,应该很清楚了,最后会一路调用到invokeSuspend方法,所以这时候就会执行到suspend{}代码块里面,协程启动!

协程的挂起

调用到invokeinvokeSuspend函数里面的代码的时候,我们单拎出出来看下:

//launch
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
ButtonTextActivity var4;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
var4 = ButtonTextActivity.this;
this.label = 1;
var10000 = var4.dealA(this);
if (var10000 == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
case 2:
ResultKt.throwOnFailure($result);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

int num = ((Number)var10000).intValue();
var4 = ButtonTextActivity.this;
this.label = 2;
if (var4.dealB(num, this) == var3) {
return var3;
} else {
return Unit.INSTANCE;
}
}

这里涉及到了label状态机的分析,当label为0时,会调用case为0下面的代码。在里面label被设置为了1,又调用了var4.dealA(this)这个挂起函数,从前面挂起函数的分析知道其会返回COROUTINE_SUSPENDED标志,所以var10000也就会得到COROUTINE_SUSPENDED标志,此时会被判断相等,协程会被挂起。

协程的恢复

挂起后就要恢复了。在前面执行到的dealA方法中,在withContext的时候会触发dealA中的invokeSuspend方法。此时label被设置为1,所以会被调用到case为1的代码:

//dealA 
public final Object invokeSuspend(@NotNull Object $result) {
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
this.label = 1;
if (DelayKt.delay(3000L, this) == var2) {
return var2;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

return Unit.INSTANCE;
}

return Boxing.boxInt(1);

执行了ResultKt.throwOnFailure($result),最后返回int的值。同时launch中的invokeSuspend也被执行,上面已经将label设置为1,这里就会执行到case 1下的代码:

//launch  invokeSuspend
switch(this.label) {
......
case 1:
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
}

int num = ((Number)var10000).intValue();
var4 = ButtonTextActivity.this;
this.label = 2;
if (var4.dealB(num, this) == var3) {
return var3;
} else {
return Unit.INSTANCE;
}

对结果进行了失败处理,此时var10000也就是刚刚得到的int值,接着执行suspend剩余的代码,在下面将lable设置为了2,开始执行dealB的方法。

dealB方法中,跟之前分析的步骤一样,也会回到invokeSuspend中:

private final Object dealB(int num, Continuation $completion) {
Object var10000 = BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
......

return Unit.INSTANCE;
}

......
return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}

最后当没有挂起函数的时候,会返回Unit.INSTANCE,结束协程执行。

小结

协程通过suspend来标识挂起点,但真正的挂起点还需要通过是否返回COROUTINE_SUSPENDED来判断,而代码体现是通过状态机来处理协程的挂起与恢复。

在挂起和恢复的过程中,当判断挂起函数到返回值是COROUTINE_SUSPENDED标志时,会挂起,在需要挂起的时候,状态机会把之前的结果以成员变量的方式保存在 continuation 中。在挂起函数恢复的时候,会调用Continuation的resumeWith方法,继而触发invokeSuspend。根据保存在Continuation中的label,进入不同的 分支恢复之前保存的状态,进入下一个状态。

在挂起的时候并不会阻塞当前的线程,是因为挂起是在invokeSuspend方法中return出去的,而invokeSuspend之外的函数当然还是会继续执行。


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

收起阅读 »

OkHttp源码分析

 大家好,我是小黑,一个还没秃头的程序员~~~ 路是走出来的,而不是空想出来的。 相信大家找工作的时候都会被问及到Okhttp的原理以及源码分析,好记性不如烂笔头,所以这次我打算把它记录下来方便日后复习查看,也和大家分享一下,如果有什么不对之处还请大...
继续阅读 »

 大家好,我是小黑,一个还没秃头的程序员~~~


路是走出来的,而不是空想出来的。


相信大家找工作的时候都会被问及到Okhttp的原理以及源码分析,好记性不如烂笔头,所以这次我打算把它记录下来方便日后复习查看,也和大家分享一下,如果有什么不对之处还请大家多多指教!


这次的分析分为三个步骤:



  1. 网络请求发出去后到了哪里

  2. 请求是怎么被处理的

  3. 请求结束后又会做什么


(一)网络请求发出去后到了哪里


OkHttp发送请求有两种方式:enqueue/execute,在enqueue异步请求之前我们需要调用newCall()newCall() 返回的是RealCall对象


 /**
* Prepares the {@code request} to be executed at some point in the future.
*/
@Override public Call newCall(Request request) {
return new RealCall(this, request, false /* for web socket */);
}

所以我们找到RealCall类中的enqueue()


  @Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

可以看到最终是执行了dispatcher().enqueue() 来完成的,我们进入Dispatcher类中,Dispatcher类是用来实现任务调度的,主要有以下变量


  //最大并发请求数
private int maxRequests = 64;
//每个主机的最大请求数
private int maxRequestsPerHost = 5;

//消费者线程池
private ExecutorService executorService;

//等待中的请求队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

//正在运行的异步请求队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

//正在运行的同步请求队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

Dispatcher类中,有两个构造函数,一个有传入线程池,一个没有传入线程池,没有传入线程池的话会在异步请求之前创建一个默认的线程池


 public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}

public Dispatcher() {
}

//创建线程池,SynchronousQueue为一个没有容量的队列
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}

前面讲到执行RealCallenqueue() 便会最终到Dispatcherenqueue() ,它的代码如下


 synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}

当正在运行的异步请求队列中的数量小于64并且正在运行的请求主机数小于5时,把请求加载到runningAsyncCalls队列中中并在线程池中执行,否则就加入到readyAsyncCalls队列中进行等待。到此为止,第一个问题就解决了,请求最终会被发送到两个队列中,要么被执行要么等待。


(二)请求是怎么被处理的


任务被放进队列中后,任务是AsyncCall类,是继承于NamedRunnable的一个类,执行AsyncCallexecute() 方法,代码如下:


    @Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}

上面的代码中,getResponseWithInterceptorChain() 返回了response,并在相应的回调中返回,这是处理请求的地方,这里会添加一个拦截器链,如是否重定向,缓存拦截器,自定义拦截器等,代码如下:


  Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//自定义拦截器
interceptors.addAll(client.interceptors());
//重定向拦截器
interceptors.add(retryAndFollowUpInterceptor);
//桥接拦截器,设置请求头中的属性的
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//缓存拦截器,有缓存就会取缓存,没有缓存再去连接服务器
interceptors.add(new CacheInterceptor(client.internalCache()));
//连接拦截器
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}

//最后一个拦截器,会对服务器进行网络调用,在intercept()方法中构建头部以及body,并获取返回
interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}

接下来我们看代码最后面的proceed() 方法,这个方法是RealInterceptorChain这个类实现的,代码如下:


  @Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}

我们接着点进去 ,在这里面会去做关于response的返回,在这里从拦截器列表中取出拦截器,并使用通过各个拦截器通过intercept()方法的实现来获取拦截器的调用返回,拦截器是按顺序取出来处理的,代码如下:


  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
Connection connection) throws IOException {
...

// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(
interceptors, streamAllocation, httpCodec, connection, index + 1, request);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);

...
return response;
}

到这里,问题二已解决了,请求是在getResponseWithInterceptorChain() 获取返回的。


(三)请求结束后又会做什么


上面提到任务执行的时候会调用到AsyncCallexecute() 方法,方法最后会调用client.dispatcher().finished(this) ,我们点进finished() 方法看一下,代码如下:


  void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}

  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}

if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}

关键在于promoteCalls() 方法,代码如下:


  private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // 运行中的请求以及到了最大的请求数
if (readyAsyncCalls.isEmpty()) return; // 没有等待中的请求了
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();

if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}

if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}

从代码中可以看出,在一个请求结束后,会去对运行中的请求以及等待中的请求数进行判断,并将等待队列中的请求移除一个并添加到线程池中进行处理,即进入运行队列中处理,这就是请求结束后的内容了,到此为止,OkHttp源码分析的三个步骤就介绍完毕,日后我会接着分享自己在阅读源码的体会与总结,最后,希望喜欢我文章的朋友们可以帮忙点赞、收藏、评论,也可以关注一下,如果有问题可以在评论区提出,谢谢大家的支持与阅读!


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

Kotlin浅析之Contract

在进行kotlin的项目开发中,我们依赖kotlin语法糖相比java可以更高效地产出,kotlin的彩蛋众多,这篇文章着重跟大家聊一聊Contract,其实Contract在官方函数中其实也有被多次使用,比如我们常用的let、apply、also、isNul...
继续阅读 »

在进行kotlin的项目开发中,我们依赖kotlin语法糖相比java可以更高效地产出,kotlin的彩蛋众多,这篇文章着重跟大家聊一聊Contract,其实Contract在官方函数中其实也有被多次使用,比如我们常用的let、apply、also、isNullOrEmpty等函数:


@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}

return this == null || this.length == 0
}

接下来,我们来了解一下contract到底是什么以及怎么用?


一、Contract是什么?


contract翻译过来意思是"契约",那么既然是"契约",约定的双方又是谁?


“我”和"你",心连心,同住地球村?搞叉了,再来!


契约的双方实际上是"开发者'和"编译器" ,我们都知道,kotlin编译器有着智能推断自动类型转换的功能。但实际上,它的智能推断有时候并不那么智能,下面会讲到,而官方为开发者预留了一个通道去与编译器沟通,这就是contract存在的意义。


二、Contact怎么用?


首先,我们定义一个String常规的判空扩展函数


/**
* 字符串扩展函数判空,常规方式
* @receiver String? 接收类型
* @return Boolean 是否为空
*/
fun String?.isNullOrEmptyWithoutContract(): Boolean {
return this == null || this.isEmpty()
}

然后,我们来调用看看


/**
* 问题示例1 使用自定义函数判空,编译器无感知
* @param name String? 传入的姓名字符串
*/
private fun problemNull(name: String?) {
// 用常规方式的自定义扩展函数对局部变量判空
if (!name.isNullOrEmptyWithoutContract()) {
//name.length报错,自定义扩展函数中的判空逻辑未同步到编译器 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
Log.d(TAG, "name:$name,length:${name.length}")
}
}

结果,在函数内部调用外部自定义字符串判空函数,不起作用,这是因为编译器并不知道这种间接的判空是不是有效的,而这时候,我们请出contract来表演看看:


判空扩展函数contract returns改造


/**
* 字符串扩展函数判空,contract方式
* @receiver String? 接收类型
* @return Boolean 是否为空
*/
@ExperimentalContracts
fun String?.isNullOrEmptyWithContract(): Boolean {
contract {
returns(false) implies (this@isNullOrEmptyWithContract != null)
}
return this == null || this.isEmpty()
}

/**
* 解决问题1 自定义函数判空后结果同步编译器
* @param name String? 传入的姓名字符串
*/
@ExperimentalContracts
fun fixProblemNull(name: String?) {
// 用contract方式的自定义扩展函数对局部变量判空
if (!name.isNullOrEmptyWithContract()) {
//运行正常
Log.d(TAG, "name:$name,length:${name.length}")
}
}

可以看到,判空扩展函数加入了contract之后,编译器就懂事了,但编译器是如何懂事的呢?contract内部到底跟编译器说了什么悄悄话?咱们先分析下判空扩展函数的代码


contract {
returns(false) implies (this@isNullOrEmptyWithContract != null)
}

contract所包裹的语句,实际上就是我们要告诉编译器的逻辑,这里的returns(false) 代表当前函数isNullOrEmptyWithContract()的返回值也就是 return this == null || this.isEmpty()如果是false,那么会告知编译器implies后面的表达式也就是this@isNullOrEmptyWithContract != null成立,也就是调用者对象String不为空,那么后面在打印name.length的时候编译器就知道name不为空拉,这就是开发者与编译器的契约!


其次,我们发现除了resturns的用法外,常用的apply扩展函数里面的contract是callsInPlace形式,那么callsInPlace又是什么意思?


/**
* 定义apply函数,常规方式
* @receiver T 接收类型
* @param block [@kotlin.ExtensionFunctionType] Function1<T, Unit> 函数入参
* @return T 返回类型
*/
fun <T> T.applyWithoutContract(block: T.() -> Unit): T {
block()
return this
}

/**
* 问题示例2 函数执行变量初始化,编译器无感知
*/
fun problemInit() {
var name: String
// 用常规方式的自定义扩展函数对局部变量赋值
applyWithoutContract {
// 编译器实际上不知道这个函数入参有没有被调用
name = "WenChangJi"
}
// 报错 'Variable 'name' must be initialized'
Log.d(TAG, "name:${name}")
}

这里我们给间接给局部变量name去赋值,但是后续使用时编译器报错声称name没有初始化,采取以往经验,我们加入contract去改造试试:


/**
*
* 定义apply函数,contract方式
* @receiver T 接收类型
* @param block [@kotlin.ExtensionFunctionType] Function1<T, Unit> 函数入参
* @return T 返回类型
*/
@ExperimentalContracts
fun <T> T.applyWithContract(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

/**
* 解决问题2 函数执行变量初始化后同步编译器
*/
@ExperimentalContracts
fun fixProblemInit() {
var name: String
// 用contract方式的自定义扩展函数对局部变量赋值
applyWithContract {
// applyWithContract内部契约告知编译器,这里绝对会调用一次的,也就一定会初始化
name = "WenChangJi"
}
// 运行正常
Log.d(TAG, "name:${name}")

}

这里我们并没有采用returns告知编译器在满足什么条件下什么表达式成立,而是采用callsInPlace方式告知编译器入参函数block的调用规则,callsInPlace(block, InvocationKind.EXACTLY_ONCE)即是告诉编译器block在内部会被调用一次,也就是后续调用时的语句name = "WenChangJi"会被调用一次进行赋值,那么在使用name时编译器就不会说没有初始化之类的问题拉!


callsInPlace内部次数的常量值由以下几种:



























常量值含义
- InvocationKind.AT_MOST_ONCE最多调用一次
InvocationKind.AT_LEAST_ONCE最少调用一次
InvocationKind.EXACTLY_ONCE调用一次
InvocationKind.UNKNOWN未知

最后,咱们这边文章只是讲解了Contract是什么和怎么用的部分场景,还有更多的场景以及具体的原理有兴趣的同学可以深挖~


感谢大家的观看!!!


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

如果有机会,你会选择脱产学习深造吗?

因为有同学让我帮忙写封情书,所以我最近在看朱生豪写给他爱人宋清如的情书,其中有这样一句:要是有人问你,你愿意做快乐的猪呢,还是愿意做苦恼的哲学家?你就回答:我愿意做快乐的哲学家,这样可以显出你的聪明。还有一个提问,当你手上拿着一杯水,接下来你要做什么?答案很简...
继续阅读 »

因为有同学让我帮忙写封情书,所以我最近在看朱生豪写给他爱人宋清如的情书,其中有这样一句:

要是有人问你,你愿意做快乐的猪呢,还是愿意做苦恼的哲学家?你就回答:我愿意做快乐的哲学家,这样可以显出你的聪明。

还有一个提问,当你手上拿着一杯水,接下来你要做什么?

答案很简单,做你自己该做的事情,和水没关。

对于这个提问的回答,其实我也是一样的。“如果有机会,你会选择脱产学习吗?”,脱离现实的题目,也不用进入这个设定进行回答,反正也没有什么参考意义,这个机会真出现的时候,情况往往是复杂的,你之前想的再齐全,真到了选择的时候,你还是可能做出截然不同的选择,最后喊上一句,真香。

在我看来,这个机会,就是我手里的这杯水,我如果不渴,为什么要喝水?该干什么你就干什么。

我相信很多同学面对这个问题,都会选择脱产学习。如果是一个穿越的故事背景,比如你回到了你大一刚入学的时候,说真的,都已经预知未来了,我还学习干什么,无限种可能,光是想想,都能写成万字爽文。如果是父母支持,读研深造的故事背景,那我不一定愿意,尽管大学生活让人向往,但我要考试呀,我还是要学习一堆我觉得可能没什么用的东西,然后准备考试,可能还要给导师打工,我不是很想做。我真正想做的是,自由的学习我想学习的内容,然后结束后,一鸣惊人,从此走上人生巅峰之类的。

你看,在脱产这个选择下,我想过的都是爽番的生活。

而站在不选择脱产的角度来看的话,多是出于现实的角度考虑。我也有些朋友天马行空说是要花钱考研,说真的,在互联网行业中,其实学历的影响并不大,从本科提到硕士,收益并不高,主要是图个自由散漫的生活状态、贪恋校园年轻帅气的师兄学长,本质上还是逃避心理。

真想对工作有用,就下班回到家里,充电学习工作上用到的技能,但很不巧,这些技能在大学往往并不教授,还是要靠自学。至于找对象,这个理由我觉得是成立的,你可以因此去脱产学习,我支持。

可是人生真的会有这样的机会,让你能够脱产学习吗?

大概率是不会主动出现的,但也许我们可以主动创造出来。如果成家了,抛家弃子,一心搞梦想,《月亮与六便士》的翻版。没成家的,工作攒几年积蓄,然后离职过个自由的生活。想学习?想学什么学什么,想运动,办了卡可以天天去。可是我们真的会这么自律吗?

其实想一想我们过往的寒暑假,有几次是真的好好学习度过的呢?那不就是我们自由生活的缩影吗?也许前几天还能自律,后几天就开始散漫,散漫久了,又开始想学习,就这样反复横跳,很可能也做不出什么。所以才感叹,有些人真的很厉害,为了梦想,拼尽全力。你以为你差的是一个梦想,但你的思想觉悟却可能已经差了一大截。

其实我们人生中还会遇到很多这种两难的选择,是考研还是工作?是学后端还是前端?是接受还是拒绝?是分手还是继续?等等

我认为两难选择中,真正难的并不是问题本身,而是有问题的这个人,当然很多时候,这个人就是我自己。人并不是一个绝对理性的生物,人是很容易被各种情绪压的不能动弹。我们很可能遇到过这种情况,当有好友向我们咨询问题的时候,你发现,不管你给他什么建议,他都能找到很好的理由来解释为什么这个方法不行。

有的时候我觉得这种人就是在作,左右都不行,然后还逼逼赖赖,就在这里不做决定,耽搁生命,让事情越来越糟糕。不过转念一想,我们很多时候不也是这样的吗?

真正让我们无法做出正确决定的是我们的恐惧。基本上所有的恐惧都是害怕会失去某些东西。

要突破这种左右为难的困境其实也很简单,那就是遵循内心的声音,还有就是坦然接受两个选择的任何一个,或者两个都不选。这一点之所以重要,是因为处在两难的境地中,你往往会相信你就只有两个选择,再没有第三条路可以选。什么样的选择是最好的,答案就在心中,可我们听不到,是因为恐惧、焦虑、压力等已经扰乱了我们的思想。如果我们愿意两个选择中选择一个,或都不选的话,我们就能得到平静、平息恐惧,并听到自己内心的声音。两难的境地会让人既排斥现有的选择,同时又不肯放弃它们。这也就是让我们陷入困境的原因。

无论做出什么样的选择,有一件事是确定的,那就是跟随着自己的心去做的事,不管造成多大的骚动,都将会为每个人带来好的影响。所以每一次左右为难的处境,你都可以理解为考验你追寻自我的决心。

但还要记得一件事情,那就是:拖延做决定是最差的决定!

每个人都害怕受伤,为了避免让自己受伤,也为了避免让他人受伤(当然往往这是个借口),人总是拖到不得不做决定的时候才做决定,最后到了不得不决定的时候,于人于己都是更大的伤害,于是又懊恼过往的时光怎么不早早做出决定,但下次同样的问题出现时又是同样的行为模式,人总是这样不长教训,可笑而又无奈。

有的时候我们即便不断地听到那个内心的声音,我们还是会把它掩盖,用其他事物转移自己的注意力,我们还会寄希望于寻找所谓的真理,希望能够降维解决当下的难题,然后自己就会有做出决定的勇气,怀着这样一种不切实际的幻想投入所谓的行动,虽然在其他方面真的有所长进,实际上也只是不断地缓解自己拖延决定的痛苦而已。

你说有什么解决方法吗?没有。你不张开嘴,说出那句话,你不做出那个小小的行动,一切都是于事无补,一切都不会有大的改变,一切的想法都只是镜花水月。

我们常说人生只有一次,我们常说开心快乐就好,我们希望这样的想法能给自己勇气,人们常以为勇气就是有了它你就不恐惧的东西,但勇气从来都不是无知者无畏,而是当你还未开始就已知道自己会输,可你依然要去做,而且无论如何都要把它坚持到底。引用《存在的勇气》:

什么是勇气?概言之,就是不顾非存在的威胁而对存在进行自我肯定。

有了勇气问题就能解决吗?问题会尘埃落地,但不一定就能如我们期望那样解决,“你很少能赢,但有时也会”,而你所做的,不过是对无力的自己的一点抗争,我们能掌控的从来不是事情的结果,而是那个能够选择做出抗争的自己,听起来有些无奈,但仅是如此,我们也拥有了自由。

不为他人,不为自己,仅是为了与被推着自己往前走的命运抗争,仅为了证明你有掌控命运一角的可能,仅为了守住一份自由,你也值得向前迈出一步。

作者:冴羽

来源:juejin.cn/post/7103474675455361037

收起阅读 »

在uni-app中使用微软的文字转语音服务

前言尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格.....
继续阅读 »

前言

尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格...


但就是不能下载成mp3文件,所以有一些小伙伴逼不得已只好通过转录电脑的声音来获得音频文件,但这样太麻烦了。其实,能在网页里看到听到的所有资源,都是解密后的结果。也就是说,只要这个声音从网页里播放出来了,我们必然可以找到方法提取到音频文件。

本文就是记录了这整个探索实现的过程,请尽情享用~

本文大部分内容写于今年年初一直按在手里未发布,我深知这个方法一旦公之于众,可能很快会迎来微软的封堵,甚至直接取消网页体验的入口和相关接口。

解析Azure官网的演示功能

使用Chrome浏览器打开调试面板,当我们在Azure官网中点击播放功能时,可以从network标签中监控到一个wss://的请求,这是一个websocket的请求。


两个参数

在请求的URL中,我们可以看到有两个参数分别是AuthorizationX-ConnectionId


有意思的是,第一个参数就在网页的源码里,使用axios对这个Azure文本转语音的网址发起get请求就可以直接提取到


const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){
  const token = RegExp.$1;
}

通过查看发起请求的JS调用栈,加入断点后再次点击播放



可以发现第二个参数X-ConnectionId来自一个createNoDashGuid的函数

this.privConnectionId = void 0 !== t ? t : s.createNoDashGuid(),

这就是一个uuid v4格式的字符串,nodash就是没有-的意思。

三次发送

请求时URL里的两个参数已经搞定了,我们继续分析这个webscoket请求,从Message标签中可以看到


每次点击播放时,都向服务器上报了三次数据,明显可以看出来三次上报数据各自的作用

第一次的数据:SDK版本,系统信息,UserAgent

Path: speech.config
X-RequestId: 818A1E398D8D4303956D180A3761864B
X-Timestamp: 2022-05-27T16:45:02.799Z
Content-Type: application/json

{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/MacIntel","name":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","version":"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36"}}}

第二次的数据:转语音输出配置,从outputFormat可以看出来,最终的音频格式为audio-24khz-160kbitrate-mono-mp3,这不就是我们想要的mp3文件吗?!

Path: synthesis.context
X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
X-Timestamp: 2022-05-27T16:48:43.340Z
Content-Type: application/json

{"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-160kbitrate-mono-mp3"},"language":{"autoDetection":false}}}

第三次的数据:要转语音的文本信息和角色voice name,语速rate,语调pitch,情感等配置

Path: ssml
X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
X-Timestamp: 2022-05-27T16:48:49.594Z
Content-Type: application/ssml+xml

<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US"><voice name="zh-CN-XiaoxiaoNeural"><prosody rate="0%" pitch="0%">我叫大帅,一个热爱编程的老程序猿</prosody></voice></speak>

接收的二进制消息

既然从前三次上报的信息已经看出来返回的格式就是mp3文件了,那么我们是不是把所有返回的二进制数据合并就可以拼接成完整的mp3文件了呢?答案是肯定的!

每次点击播放后接收的所有来自websocket的消息的最后一条,都有明确的结束标识符



turn.end代表转换结束!

用Node.js实现它

既然都解析出来了,剩下的就是在Node.js中重新实现这个过程。

两个参数

  1. Authorization,直接通过axios的get请求抓取网页内容后通过正则表达式提取

const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){
  const Authorization = RegExp.$1;
}
  1. X-ConnectionId,直接使用uuid库即可

//npm install uuid
const { v4: uuidv4 } = require('uuid');

const XConnectionId = uuidv4().toUpperCase();

创建WebSocket连接

//npm install nodejs-websocket
const ws = require("nodejs-websocket");

const url = `wss://eastus.tts.speech.microsoft.com/cognitiveservices/websocket/v1?Authorization=${Authorization}&X-ConnectionId=${XConnectionId}`;
const connect = ws.connect(url);

三次发送

第一次发送

function getXTime(){
  return new Date().toISOString();
}

const message_1 = `Path: speech.config\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"Browser/Linux x86_64","name":"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0","version":"5.0 (X11)"}}}}`;

connect.send(message_1);

第二次发送

const message_2 = `Path: synthesis.context\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-16khz-32kbitrate-mono-mp3"}}}`;

connect.send(message_2);

第三次发送

const SSML = `
  <speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US">
      <voice name="zh-CN-XiaoxiaoNeural">
          <mstts:express-as style="general">
              <prosody rate="0%" pitch="0%">
              我叫大帅,一个热爱编程的老程序猿
              </prosody>
          </mstts:express-as>
      </voice>
  </speak>
  `

const message_3 = `Path: ssml\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/ssml+xml\r\n\r\n${SSML}`

connect.send(message_3);

接收二进制消息拼接mp3

当三次发送结束后我们通过connect.on('binary')监听websocket接收的二进制消息。

创建一个空的Buffer对象final_data,然后将每一次接收到的二进制内容拼接到final_data里,一旦监听到普通文本消息中包含Path:turn.end标识时则将final_data写入创建一个mp3文件中。

let final_data=Buffer.alloc(0);
connect.on("text", (data) => {
  if(data.indexOf("Path:turn.end")>=0){
      fs.writeFileSync("test.mp3",final_data);
      connect.close();
  }
})
connect.on("binary", function (response) {
  let data = Buffer.alloc(0);
  response.on("readable", function () {
      const newData = response.read()
      if (newData)data = Buffer.concat([data, newData], data.length+newData.length);
  })
  response.on("end", function () {
      const index = data.toString().indexOf("Path:audio")+12;
      final_data = Buffer.concat([final_data,data.slice(index)]);
  })
});

这样我们就成功的保存出了mp3音频文件,连Azure官网都不用打开!

命令行工具

我已经将整个代码打包成一个命令行工具,使用非常简单

npm install -g mstts-js
mstts -i 文本转语音 -o ./test.mp3

已全部开源: github.com/ezshine/mst…

在uni-app中使用

新建一个云函数

新建一个云函数,命名为mstts


由于mstss-js已经封装好了,只需要在云函数中npm install mstts-js然后require即可,代码如下

'use strict';
const mstts = require('mstts-js')

exports.main = async (event, context) => {
  const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
 
  //res为buffer格式
});

下载播放mp3文件

要在uniapp中播放这个mp3格式的文件,有两种方法

方法1. 先上传到云存储,通过云存储地址访问

exports.main = async (event, context) => {
  const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
 
  //res为buffer格式
  var uploadRes = await uniCloud.uploadFile({
      cloudPath: "xxxxx.mp3",
      fileContent: res
  })
   
  return uploadRes.fileID;
});

前端用法:

uniCloud.callFunction({
  name:"mstts",
  success:(res)=>{
      const aud = uni.createInnerAudioContext();
      aud.autoplay = true;
      aud.src = res;
      aud.play();
  }
})
  • 优点:云函数安全

  • 缺点:文件上传到云存储不做清理机制的话会浪费空间

方法2. 利用云函数的URL化+集成响应来访问

这种方法就是直接将云函数的响应体变成一个mp3文件,直接通过audio.src赋值即可访问`

exports.main = async (event, context) => {
const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');

return {
mpserverlessComposedResponse: true,
isBase64Encoded: true,
statusCode: 200,
headers: {
'Content-Type': 'audio/mp3',
'Content-Disposition':'attachment;filename=\"temp.mp3\"'
},
body: res.toString('base64')
}
};

前端用法:

const aud = uni.createInnerAudioContext();
aud.autoplay = true;
aud.src = 'https://ezshine-274162.service.tcloudbase.com/mstts';
aud.play();
  • 优点:用起来很简单,无需保存文件到云存储

  • 缺点:URL化后的云函数如果没有安全机制,被抓包后可被其他人肆意使用

作者:大帅老猿
来源:juejin.cn/post/7103720862221598757

收起阅读 »

百度95后程序员删库跑路被判刑,原因竟是工作内容变动及对领导不满

删库一时爽,后果很严重!近日,记者自北京裁判文书网上获悉,百度某“95后”校招员工金某某在任职期间,私自建立隧道进入数据库“删表”。最终因犯破坏计算机信息系统罪,被判处有期徒刑九个月。而究其动机,居然是出于对工作内容变动及领导的不满。为了显示自己在项目中的重要...
继续阅读 »
删库一时爽,后果很严重!

近日,记者自北京裁判文书网上获悉,百度某“95后”校招员工金某某在任职期间,私自建立隧道进入数据库“删表”。最终因犯破坏计算机信息系统罪,被判处有期徒刑九个月。



而究其动机,居然是出于对工作内容变动及领导的不满。为了显示自己在项目中的重要性,遂对平台数据进行破坏。事后,百度职业道德建设部很快接到了内部安全部门的举报,开始进行数据恢复及调查。同月,民警在百度将该金某某抓获。

来看详情——

“95后”员工愤然删库

“删库跑路”往往只是程序员们之间排解压力的调侃之语,但当更注重实现自我价值的“Z世代”们走上工作岗位,一时头脑发热删库泄愤,最终导致严重后果。

据裁定书显示,金某某出生于1996年,大学文化,在“删库”被抓时还不满25岁。据金某某供述,他毕业后就在百度网讯科技有限公司工作,负责测试开发,工作内容是测试公司的平台与写程序。2020年8月,公司派其他员工来接手项目,金某某对该安排感到不满意。为了显示自己在这个项目里还有作用,就对平台的数据进行了破坏。

具体而言,金某某使用链接内网的工具,打通外部与公司服务器之间的链接,然后在家中使用手机登录隧道进入到公司内网服务器,用内网服务器做跳板去访问可视化项目服务器,分次将可视化项目程序数据库内的项目表进行了删除、锁定、修改。

每对数据库删除、锁定、修改一次,公司就需要修复一遍。在公司修好后,金某某再次进行删改。在多次“折腾”后,金某某主动申请更换部门,再未对数据库进行删改。

金某某的工作实际情况如何?对此,他同部门的同事赵某介绍,金某某通过校招入职百度,到公司后大部分时间在商业质量效能部部门工作,2020年10月23日被调到ACG部门。

商业质量效能部门自己开发了一套用于测试服务的系统,金某某当时参与了这套系统的开发,他将这套系统数据库的数据部分篡改、删除,导致系统的算法无法正常运行,得不到要的结论。赵某证实,金某某的行为在一段时间内影响了部门工作的正常开展。

删库造成严重后果

一时泄愤“删库”,还没等到“跑路”,金某某的行为就已被发现。

据安全工程师李某证实,2020年8月,百度商业质量效能部向其部门反馈ff.baidu-int.com平台数据库近期频繁被篡改,且数据库管理软件Adminer被不明人员连接,安全部门遂按照公司要求配合开展核查工作。经调查发现,事发时间段相关IP地址由金某某使用。

百度在线出具的数据库删除数据操作日志情况说明显示:结合业务反馈与安全排查共发现16次疑似恶意操作,其中10次操作关联到内部员工。

证人艾某则证实,2021年3月,其公司职业道德建设部接内部安全部门举报称,员工金某某在商业质量效能部任职期间,由于对部门领导不满,使用隧道违规从外网接入百度IDC并对商业质量效能部ff.baidu-int.com平台数据库内表进行清空、篡改、锁定,造成严重后果。

艾某介绍,金某某的行为一方面导致平台数据不一致或丢失,无法使用快捷操作功能,数据通过脚本回溯快速恢复,共计影响50个项目使用平台快捷操作能力;另一方面数据库的异常变化会带来用户对百度产品使用体验的误解,严重影响公司形象及经济效益。

“删库一时爽”,但恢复被删除数据则需要大量的人力和时间成本。经北京中海义信信息技术有限公司司法鉴定所鉴定,并对删除的表数据进行恢复,共计花费16300元。

2021年3月,北京市公安局海淀分局警务支援大队对涉案被破坏计算机信息系统服务器日志信息等数据进行远程提取。同日,民警在百度公司将金某某抓获。

被判有期徒刑9个月

年轻人犯错,总是很容易被原谅。在家属的帮助下,金某某赔偿百度7万元并获得了谅解,但仍将面临法律的惩处。

一审海淀区法院认为,被告人金某某违反国家规定,对计算机信息系统中存储的数据进行删除、修改操作,后果严重,其行为已构成破坏计算机信息系统罪,应予惩处。鉴于被告人金某某能够如实供述自己的主要犯罪事实,并在家属的帮助下赔偿百度公司经济损失并获得谅解,依法对其从轻处罚并适用缓刑。

一审法院判决:金某某犯破坏计算机信息系统罪,判处有期徒刑九个月,缓刑一年。

对此,金某某提出上诉称,自己做的的确不对,但其行为没有造成这么大的损失,修复数据16300元不是必要费用,故不构成犯罪。

其辩护人亦要求改判金某某无罪或裁定发回重审,认为百度委托鉴定无必要,且并非由公安机关委托,费用不应被认定为经济损失。金某某具有自首情节,涉案行为情节轻微显著,且社会危害性不大。

对此,二审法院指出,破坏计算机信息系统犯罪中“经济损失”的计算范围,具体包括危害计算机信息系统犯罪行为给用户直接造成的经济损失,以及用户为恢复数据、功能而支出的必要费用。本案中,百度公司在其相关数据库被金某某破坏后,委托北京中海义信信息技术有限公司司法鉴定所进行恢复相关数据,并无不妥。

而对于金某某是否具有自首情节,法院指出,金某某系被公安民警在百度公司抓获到案,其行为不符合自首的法律规定。金某某对计算机信息系统中存储的数据进行删除、修改操作,后果严重,不属于情节显著轻微的情况。考虑到金某某如实供述并获得谅解,已对其从轻量刑。最终,二审法院裁定驳回上诉,维持原判。

“删库跑路”者屡现

“别惹程序员!”业内删库报复的案例不断增加。

最有名的案例,莫过于微盟集团程序员的“删库”。2020年2月,有商户发现微盟的SaaS业务服务突然宕机,微盟旗下300万商户的线上业务全部停止,商铺后台的所有数据被清零。此后,微盟集团发布公告解释这次事故,称数据库遭遇“人为破坏”。

据事后了解,该员工一直深陷网络贷,还曾有过轻生举动。最终,该员工被判有期徒刑6年,自称系因生活不如意、无力偿还网贷等个人原因导致作出“删库”行为。

据中国裁判文书网公布的案例,2018年6月,链家数据库管理员韩某利用其担任并掌握该公司财务系统“root”权限的便利,登录该公司财务系统,并将系统内的财务数据及相关应用程序删除,致使该公司财务系统彻底无法访问。

对于韩某的删库行为,同样是因积怨所致。韩冰于2018年2月开始在公司负责财务系统维护,但5月被调整至技术保障部,工作地点也产生变动。韩冰对组织调整有意见,觉得自己不受重视,这也是他后来删库的重要原因之一。最终,韩某被判有期徒刑七年。

另外,据上海市杨浦区法院近期披露的一则刑事判决书显示,92年出生的程序员录某负责京东到家平台的代码研发工作。在2021年6月离职后,录某未经许可用本人账户登录服务器的代码控制平台,将其在职期间所写京东到家平台优惠券、预算系统以及补贴规则等代码删除,导致原定按期上线项目延后。与金某某情况类似,录某在积极赔偿后取得了公司谅解,最终被判有期徒刑十个月。

除了知名大厂外,小公司程序员“删库泄愤”的情况发生更加频繁。例如,2021年10月公布的一份裁判文书显示,某集团邯郸客运总站售票系统计算机编程人员因薪酬等问题离职心生不满,遂利用自己的苹果笔记本电脑远程接入网上自助售票系统的接口地址,删除了售票员表、网络售票表、结算单表、售票数据表、手持机表等,造成该站当日约5小时所有售票渠道全部无法正常使用,当日部分售票数据丢失。

放眼海外,今年年初,知名开源库Faker.js和colors.js的作者MarakSquires主动恶意破坏了自己的项目,不仅“删库跑路”,还注入了导致程序死循环的恶意代码,影响甚众。

对于企业来说,程序员删库跑路带来的不止是经济上的损失,还有顾客信任度的丧失以及对企业形象的负面影响。因此,公司在平时就应完善相应的安全机制和管理制度,做好备份恢复和权限管理,防患于未然。而对于程序员来说,“删库一时爽”,但短暂的宣泄情绪后,将面临的是法律的惩处。

来源:中国基金报记者 颜颖

收起阅读 »

Mac修改hosts,域名与ip绑定,vue Invalid Host header

在移动开发过程中,有时候需要使用域名进行访问(如微信网页开发)本地ip地址服务,或者使用域名访问本地ip地址服务等。这时候可以修改host进行实现。1. 修改host文件在命令终端,使用root用户修改host文件。域名使用root用户打开/etc/hosts...
继续阅读 »

在移动开发过程中,有时候需要使用域名进行访问(如微信网页开发)本地ip地址服务,或者使用域名访问本地ip地址服务等。

这时候可以修改host进行实现。

1. 修改host文件

在命令终端,使用root用户修改host文件。域名使用root用户打开/etc/hosts host文件进行修改。添加
ip及对应的域名

$ sudo vi /etc/hosts
127.0.0.1       localhost
127.0.0.1 zhangguoyedeMacBook-Pro.local
255.255.255.255 broadcasthost
::1 localhost
::1 zhangguoyedeMacBook-Pro.local

# 在这里添加上ip及对应的域名并保存退出
#(这里假设你设置的是本机ip是 127.0.0.1 访问域名是 guoye.com)
127.0.0.1 guoye.com

2. 通过域名访问项目

现在可以在浏览器上访问你设置的域名guoye.com,跟直接通过ip访问127.0.0.1的内容是一致的。
通常你的项目会加上端口号,域名也需要加上端口号,如http://guoye.com:4201

3. vue (Invalid Host header)

在vue项目开发时,直接通过ip地址访问正常,但通过上面host域名方式访问,浏览器会显示一段文字:Invalid Host header
这是由于新版webpack-dev-server出于安全考虑,默认检查hostname,如果hostname 没有配置在内的,将中断访问。

解决方法:
vue.config.jsdevServer配置文件加上 disableHostCheck: true

devServer: {
port: 4201, // 端口配置
proxy: {
// 代理配置
},
disableHostCheck: true, // 这是由于新版的webpack-dev-server出于安全考虑,默认检查hostname,如果hostname 不是配置内的,将中断访问。
}

4. 手机端也通过域名进行访问

移动开发时,可以使用Charles软件进行代理。
此时手机端也能通过域名访问本机电脑的应用。

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

收起阅读 »

iOS-底层原理 02:alloc & init & new 源码分析

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:分别输出3个对象的内容、内存地址、指针地址,下图是打印结果结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址是相同的,但是对象的指针...
继续阅读 »

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:


分别输出3个对象的内容、内存地址、指针地址,下图是打印结果


结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址相同的,但是对象的指针地址是不同的

%p -> &p1:是对象的指针地址,
%p -> p1: 是对象指针指向的的内存地址

这就是本文需要探索的内容,alloc做了什么?init做了什么?

准备工作

alloc 源码探索

alloc + init 整体源码的探索流程如下


  • 【第一步】首先根据main函数中的LGPerson类的alloc方法进入alloc方法的源码实现(即源码分析开始),

  • 【第二步】跳转至_objc_rootAlloc的源码实现

  • 【第三步】跳转至callAlloc的源码实现

如上所示,在calloc方法中,当我们无法确定实现走到哪步时,可以通过断点调试,判断执行走哪部分逻辑。这里是执行到_objc_rootAllocWithZone

slowpath & fastpath

其中关于slowpathfastpath这里需要简要说明下,这两个都是objc源码中定义的,其定义如下



其中的__builtin_expect指令是由gcc引入的,
1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
2、作用:允许程序员将最有可能执行的分支告诉编译器。
3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。
4、fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大
5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大
6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest

cls->ISA()->hasCustomAWZ()

其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone


【第四步】跳转至_objc_rootAllocWithZone的源码实现


【第五步】跳转至_class_createInstanceFromZone的源码实现,这部分是alloc源码的核心操作,由下面的流程图及源码可知,该方法的实现主要分为三部分
cls->instanceSize:计算需要开辟的内存空间大小
calloc:申请内存,返回地址指针
obj->initInstanceIsa:将 类 与 isa 关联


根据源码分析,得出其实现流程图如下所示:


alloc 核心操作

核心操作都位于calloc方法中

cls->instanceSize:计算所需内存大小

计算需要开辟内存的大小的执行流程如下所示


  • 1、跳转至instanceSize的源码实现

通过断点调试,会执行到cache.fastInstanceSize方法,快速计算内存大小

  • 2、跳转至fastInstanceSize的源码实现,通过断点调试,会执行到align16

  • 3、跳转至align16的源码实现,这个方法是16字节对齐算法

内存字节对齐原则

在解释为什么需要16字节对齐之前,首先需要了解内存字节对齐的原则,主要有以下三点

数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐
为什么需要16字节对齐

需要字节对齐的原因,有以下几点:

通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况
字节对齐-总结

在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体,
结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转。
苹果早期是8字节对齐,现在是16字节对齐
下面以align(8) 为例,图解16字节对齐算法的计算过程,如下所示

首先将原始的内存 8 与 size_t(15)相加,得到 8 + 15 = 23
将 size_t(15) 即 15进行~(取反)操作,~(取反)的规则是:1变为0,0变为1
最后将 23 与 15的取反结果 进行 &(与)操作,&(与)的规则是:都是1为1,反之为0,最后的结果为 16,即内存的大小是以16的倍数增加的

calloc:申请内存,返回地址指针

通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针


这里我们可以通过断点来印证上述的说法,在未执行calloc时,po objnil,执行后,再po obj法线,返回了一个16进制的地址


在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x01111111f>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc 地址 还没有与传入 的 cls进行关联,
  • 同时印证了 alloc的根本作用就是 开辟内存
obj->initInstanceIsa:类与isa关联

经过calloc可知,内存已经申请好了,类也已经出入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示


主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联

同样也可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针


总结

  • 通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍
  • 开辟内存的核心步骤有3步:计算 -- 申请 -- 关联

init 源码探索

alloc源码探索完了,接下来探索init源码,通过源码可知,inti的源码实现有以下两种

类方法 init


这里的init是一个构造方法 ,是通过工厂设计(工厂方法模式),主要是用于给用户提供构造方法入口。这里能使用id强转的原因,主要还是因为 内存字节对齐后,可以使用类型强转为你所需的类型

实例方法 init

  • 通过以下代码进行探索实例方法 init

  • 通过main中的init跳转至init的源码实现

  • 跳转至_objc_rootInit的源码实现

有上述代码可以,返回的是传入的self本身。

new 源码探索

一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实现,通过源码可以得知,new函数中直接调用了callAlloc函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [alloc init]的结论


但是一般开发中并不建议使用new,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX,会在这个方法中调用[super init],用new初始化可能会无法走到自定义的initWithXXX部分。

例如,在CJLPerson中有两个初始化方法,一个是重写的父类的init,一个是自定义的initWithXXX方法,如下图所示


使用 alloc + init 初始化时,打印的情况如下


使用new 初始化时,打印的情况如下

总结

如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。

补充

【问题】为什么无法断点到obj->initInstanceIsa(cls, hasCxxDtor);

主要是因为断点断住的不是 自定义类的流程,而是系统级别的


作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108427260

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS底层原理01:源码探索的三种方式

iOS
本文主要介绍下源码探索的三种方法1、符号断点直接跟流程2、通过按住control+step into3、汇编跟流程下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例1、符号断点直接跟流程通过下alloc的符号断点选择断点Symbolic Br...
继续阅读 »

本文主要介绍下源码探索的三种方法

  • 1、符号断点直接跟流程
  • 2、通过按住control+step into
  • 3、汇编跟流程

下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例

1、符号断点直接跟流程

  • 通过下alloc的符号断点

    • 选择断点Symbolic Breakpoint


符号断点中输入 alloc


main中的CJLPerson处 加一个断点
在走到这部分断点之前,需要关闭上面新增的符号断点,原因是因为alloc的调用有很多,如果开启了就不能准确的定位到CJLPerson的alloc方法


以下为符号断点的关闭状态


运行程序, 断在CJLPerson部分

  • 打开 alloc符号断点 ,断点状态为


    继续执行


    以下为alloc符号断点断住的堆栈调用情况,从下图可以看出 alloc 的源码位于libobjc.A.dylib库(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


    2、通过按住control+step into

    • main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


  • 按住 control键,选择 step into ⬇️键


进去后,显示为以下内容


再下一个objc_alloc符号断点,符号断点后显示了 objc_alloc所在的源码库
(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


3、汇编跟流程

main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


xcode 工具栏 选择 Debug --> Debug Workflow --> Always Show Disassembly,这个 选项表示 始终显示反汇编 ,即 通过汇编 跟流程


按住control,点击 step into ⬇️键,执行到下图的callq ,对应 objc_alloc


  • 按住control,点击 step into ⬇️键进入,看到断点断在objc_alloc部分


  • 同样通过objc_alloc的符号断点,得知源码所在库
    (需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)



作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108425742
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

web网页基础知识

浮动元素重叠1、行内元素与浮动元素发生重叠,边框、背景、内容都会显示在浮动元素之上2、块级元素与浮动元素发生重叠,边框、背景会显示在浮动元素之下,内容会显示在浮动元素之上3、若不浮动的是块级元素,那么浮动的元素将显示在其上方4、若不浮动的是行内元素或者行内块元...
继续阅读 »

浮动元素重叠
1、行内元素与浮动元素发生重叠,边框、背景、内容都会显示在浮动元素之上
2、块级元素与浮动元素发生重叠,边框、背景会显示在浮动元素之下,内容会显示在浮动元素之上
3、若不浮动的是块级元素,那么浮动的元素将显示在其上方
4、若不浮动的是行内元素或者行内块元素,那么浮动的元素不会覆盖它,而是将其挤往左方、、

表单里面enctype 属性的默认值是“application/x-www-form-urlencoded”,



Boolean.FALSE与new Boolean(false)的区别

因为Boolean的 构造函数Boolean(String s) 参数只有为" true "(忽略大小写,比如TRUE,tRue都行)时候才是创建为真的Boolean值。其他情况都为假

 JavaScript的其他数据类型都可以转换成Boolean类型,注意!!!只有这几种类型会转换为false

undefined
null
0
-0
NaN
"" (空字符串)

  其他的都会转换为true。空对象{},空数组[] , 负数 ,false的对象包装等

  重点,new Boolean(false)是布尔值的包装对象 typeof (new Boolean(false)) // 'object' ,所以 转换为boolean是true,而不是false

内联元素是不可以控制宽和高、margin等;并且在同一行显示,不换行。
块级元素时可以控制宽和高、margin等,并且会换行。
行内元素不可以设置宽高,但是可以设置 左右padding、左右margin
1. inline : 使用此属性后,元素会被显示为内联元素,元素则不会换行
inline是行内元素,同行可以显示,像span、font、em、b这些默认都是行内元素,不会换行,无法设置宽度、高度、margin、border
2. block : 使用此属性后,元素会被现实为块级元素,元素会进行换行。
block,块元素,div、p、ul、li等这些默认都是块元素,会换行,除非设置float
3. inline-block : 是使元素以块级元素的形式呈现在行内。意思就是说,让这个元素显示在同一行不换行,但是又可以控制高度和宽度,这相当于内敛元素的增强。(IE6不支持)
inline-block,可以同行显示的block,想input、img这些默认就是inline-block,出了可以同行显示,其他基本block一样
一.h1~h6标签:有默认margin(top,bottom且相同)值,没有默认padding值。

在chrome中:16,15,14,16,17,19;

在firefox中:16,15,14,16,17,20;

在safari中:16,15,14,16,17,19;

在opera中:16,15,14,14,17,21;

在maxthon中:16,14,14,15,16,18;

在IE6.0中:都是19;

在IE7.0中:都是19;

在IE8.0中:16,15,14,16,17,19;
二.dl标签:有默认margin(top,bottom且相同)值,没有默认padding值。

在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

在IE6.0,7.0中:margin:19px 0px;

dd标签有默认margin-left:40px;(在所有上述浏览器中)。
三.ol,ul标签:有默认margin-(top,bottom且相同)值,有默认padding-left值

在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

在IE6.0,7.0中:margin:19px 0px;

默认padding-left值:在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中都是padding-left:40px;在IE6.0,7.0中没有默认padding值,因为ol,ul标签的边框不包含序号。
四.table标签没有默认的margin,padding值;th,td标签没有默认的margin值,有默认的padding值。

在Chrome,Firefox,Safari,Opera,Maxthon中:padding:1px;

在IE8.0中:padding:0px 1px 1px;

在IE7.0中:padding:0px 1px;

相同内容th的宽度要比td宽,因为th字体有加粗效果。
五.form标签在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中没有默认的margin,padding值,但在IE6.0,7.0中有默认的margin:19px 0px;
六.p标签有默认margin(top,bottom)值,没有默认padding值。

在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

在IE6.0,7.0中:margin:19px 0px;
七.textarea标签在上述所有浏览器中:margin:2px;padding:2px;
八.select标签在Chrome,Safari,Maxthon中有默认的margin:2px;在Opera,Firefox,IE6.0,7.0,8.0没有默认的margin值。

option标签只有在firefox中有默认的padding-left:3px;

###属性继承

1. 不可继承的:display、margin、border、padding、background、height、min-height、max-height、width、min-width、max-width、overflow、position、left、right、top、bottom、z-index、float、clear、table-layout、vertical-align、page-break-after、page-bread-before和unicode-bidi。
2. 所有元素可继承:visibility和cursor。
3. 内联元素可继承:letter-spacing、word-spacing、white-space、line-height、color、font、font-family、font-size、font-style、font-variant、font-weight、text-decoration、text-transform、direction。
4. 终端块状元素可继承:text-indent和text-align。
5. 列表元素可继承:list-style、list-style-type、list-style-position、list-style-image。

收起阅读 »

uniapp里面可以使用的单利定时器

主要代码 var HashMap = require('../tools/HashMap') /** * 使用说明: * 1、引入 var timeTool=require("../utils/timeTool.js") ...
继续阅读 »


主要代码
var HashMap = require('../tools/HashMap')
/**
* 使用说明:
* 1、引入 var timeTool=require("../utils/timeTool.js")
* 2、onload 里面实例化并调用start方法:
* mtimeTool = new timeTool( this); mtimeTool.start();
添加监听函数:keykey随意写不要重复就好
mPKGame.addCallBack("keykey", () => {})
*/
//

class PKGame {
constructor(handler) {
this.mNetTool = netTool;
this.mHandler = handler;
this.mHandler;
this.commonTimer;
this.callBackListener = new HashMap();
this.instance;
// uni.setStorageSync("token",handler.appInfo.token)
}

destroy() {
this.clearInterval(commonTimer)
this.commonTimer = null;
}
start() {
if (!this.commonTimer) {
this.commonTimer = setInterval(() => {
var values = this.callBackListener.values();
for (var i in values) {
typeof values[i] == "function" && values[i]();
}
}, 1000);
}
// this.createRoom();
}

addCallBack(listenerKey, listener) {
this.callBackListener.put(listenerKey, listener)
}
removeCallBack(listenerKey) {
this.callBackListener.remove(listenerKey)
}

static getInstance = function (handler) { //静态方法
return this.instance || (this.instance = new PKGame(handler))
}
}

module.exports = ( handler) => {
return PKGame.getInstance( handler)
};

hashmap工具类

/**
* ********* 操作实例 **************
* var map = new HashMap();
* map.put("key1","Value1");
* map.put("key2","Value2");
* map.put("key3","Value3");
* map.put("key4","Value4");
* map.put("key5","Value5");
* alert("size:"+map.size()+" key1:"+map.get("key1"));
* map.remove("key1");
* map.put("key3","newValue");
* var values = map.values();
* for(var i in values){
* document.write(i+":"+values[i]+" ");
* }
* document.write("<br>");
* var keySet = map.keySet();
* for(var i in keySet){
* document.write(i+":"+keySet[i]+" ");
* }
* alert(map.isEmpty());
*/

function HashMap(){
//定义长度
var length = 0;
//创建一个对象
var obj = new Object();

/**
* 判断Map是否为空
*/
this.isEmpty = function(){
return length == 0;
};

/**
* 判断对象中是否包含给定Key
*/
this.containsKey=function(key){
return (key in obj);
};

/**
* 判断对象中是否包含给定的Value
*/
this.containsValue=function(value){
for(var key in obj){
if(obj[key] == value){
return true;
}
}
return false;
};

/**
*向map中添加数据
*/
this.put=function(key,value){
if(!this.containsKey(key)){
length++;
}
obj[key] = value;
};

/**
* 根据给定的Key获得Value
*/
this.get=function(key){
return this.containsKey(key)?obj[key]:null;
};

/**
* 根据给定的Key删除一个值
*/
this.remove=function(key){
if(this.containsKey(key)&&(delete obj[key])){
length--;
}
};

/**
* 获得Map中的所有Value
*/
this.values=function(){
var _values= new Array();
for(var key in obj){
_values.push(obj[key]);
}
return _values;
};

/**
* 获得Map中的所有Key
*/
this.keySet=function(){
var _keys = new Array();
for(var key in obj){
_keys.push(key);
}
return _keys;
};

/**
* 获得Map的长度
*/
this.size = function(){
return length;
};

/**
* 清空Map
*/
this.clear = function(){
length = 0;
obj = new Object();
};
}
module.exports = HashMap;

收起阅读 »

uniapp开发px和rpx

开发中难免出现单位问题,就像获取系统信息,里面的屏幕宽度什么的都是px作为单位的,因此这里说明一下uniapp的转换使用rpx转pxuni.upx2px(rpx的值)px转rpxpx的值/(uni.upx2px(10)/10)使用的时候可以 let px = ...
继续阅读 »

开发中难免出现单位问题,就像获取系统信息,里面的屏幕宽度什么的都是px作为单位的,因此这里说明一下uniapp的转换使用
rpx转px

uni.upx2px(rpx的值)

px转rpx

px的值/(uni.upx2px(10)/10)

使用的时候可以 let px = uni.upx2px(rpx的值)什么的 返回值就是计算好了的

收起阅读 »

潜力APP新品推荐:Vibetoon——人人都可以是音乐视频创作人

1、Vibetoon——人人都可以是音乐视频创作人不同于在大多数人都在布局 3D Avatar 和虚拟数字人,Vibetoon 另辟蹊径选择用“2D Avatar+音乐+短视频”的模式为自己开路。并成功吸引了 Will Smith、Martin Lawrenc...
继续阅读 »

1、Vibetoon——人人都可以是音乐视频创作人

image.gif

不同于在大多数人都在布局 3D Avatar 和虚拟数字人,Vibetoon 另辟蹊径选择用“2D Avatar+音乐+短视频”的模式为自己开路。并成功吸引了 Will Smith、Martin Lawrence、Dj Jazzy Jeff、BLXST、Dave East、Macaulay Culkin 等多名音乐人、唱作人、明星们使用和宣传,同时也同华纳、Redbull等知名唱片公司达成合作。

而根据 Vibetoon 联合创始人 Veronica 表示,“Vibetoon 设立的初衷是希望每一个音乐人都可以不受资金和时间的限制,拥有自己专属的MV”。

而 Vibetoon 的使用方法也非常简单,用户只需要完成“Avatar 头像创作、场景选择和上传音乐”三步就可以创作一个专属自己的音乐视频了。

关于Avatar,用户可以自由选择选择 Avatar 的性别、体型、肤色、发色、眉毛、眼睛形状、虹膜颜色、眉毛、鼻子、嘴唇、睫毛、妆容、胡子、帽子、眼镜、耳饰、项链、手链、服装等内容。尽管每个类目提供的选项不是很丰富,但是也给出了一定的差异化空间,至少可以帮助创作者保留一定特点。

在场景上,Vibetoon 提供了沙发一角、跑车1(自然风光)、跑车2(城市风光)、录音房、泳池、阳台、地铁、沙滩等 8 个场景选择。

而除了场景本身,用户还可以加上如吸烟、弹吉他、敲电子键盘、打游戏、喝东西、写东西等不同的动画效果,选择不同的动画场景即可触发不同的动画效果。

从笔者个人的直观感受来看,Vibetoon 提供的金链子、编发、夸张耳饰、公路跑车等个性化选项,似乎更贴近“说唱”风格,这可能也是为何会多次在 Twitter 上被说唱歌手翻牌的原因。

Vibetoon 支持用户以不同比例的格式导出音乐视频并上传至 Instagram、YouTube 等不同社交媒体平台。

image.gif

目前 Vibetoon 正处于努力众筹阶段,会将筹措资金用于更多场景、动作、服装和新功能的研发和探索中。

全球越来越多的用户习惯用短视频的方法来进行表达,而 Vibetoon 似乎在短视频生态链路中找到了一条垂直,且被公司、创作者和普通用户同时需要的路线。

收起阅读 »

一句话总结工程师的辛酸

当产品出现问题时,锅已经甩到了工程师的头上,他们最喜欢说的一些话是: 1.明明在我的电脑上运行正常,为何就……2.不可能出现这种情况的啊,绝对是玄学!3.快了,已经完成了90%~4.这个很简单的,我三天就能搞定~然而……5.昨天程序运行明明是正常的,但不知道为...
继续阅读 »

当产品出现问题时,锅已经甩到了工程师的头上,他们最喜欢说的一些话是:

1.明明在我的电脑上运行正常,为何就……

2.不可能出现这种情况的啊,绝对是玄学!

3.快了,已经完成了90%~

4.这个很简单的,我三天就能搞定~然而……

5.昨天程序运行明明是正常的,但不知道为啥今天就不行了,奇了怪了~

6.只是改一行代码,不会对整个程序造成影响的,放心吧~

7.如果有问题,一定不会是我程序的原因,要不考虑一下硬件问题?

8.审查代码时:当时写这个程序的时候只有上帝和我知道我为啥这样写,现在只有上帝知道了。(我也不记得当时是什么原因了~~)

9.这个功能我会在下个版本修正……到下个版本的时候,再重复上面那句话。

10.已经做好了,但还有一些细节要调一下。

11.我会在代码更替的时候添加单元测试。

12.这只是暂时的解决方案,在正式版我会修改方案的,然后……

13.我觉得这文档写的很清楚啊,我就不明白为啥你说看不懂,这也太难了~

14.


15.


16.我正在调试这个bug,但程序是没问题的啊,是不是你硬件出错了?

17.这是字符编码的问题。

18.不用担心,这次肯定不会有问题了。上~~

19.这不可能,肯定是芯片坏了,或者是编译器出错了。

20.这个变量怎么可能被修改了,奇怪了~

21.我需要重构代码,因为上一个人写得太烂了。辣鸡代码~

22.我检查过一遍了,没问题的,版本可以发布上线了!

23.没办法,这是一个公认的bug,没有办法解决~

24.再给我两天,保证能做好。

25.之前一直都没有出现过这种情况啊~

26.我又不能测试所有的功能。

27.这不是bug,这肯定是配置问题,或者网络问题。

28.程序肯定是没问题了,你是不是改了什么,你重演一下我看看。

29.这些代码是上一个开发者写的,不是我写的。这锅我不背~

30.运行那么久,第一次出现这样的问题啊,我之前都没见过。还得瞧瞧~

看完是不是感觉自己躺着也中枪了?

声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

收起阅读 »

用80%的工时拿100%的薪水,英国正式开启“四天工作制”试验!

据外媒报道,英国从6日开始一项一周工作四天的试验,有来自70家企业的3000多名员工参与其中。这是迄今为止全球类似试验中规模最大的一次,也再次引发各界对“做四休三”工作模式的热议。全球最大型试验据报道,参与这项试验的有来自70家英国企业的3300多名员工。试验...
继续阅读 »

据外媒报道,英国从6日开始一项一周工作四天的试验,有来自70家企业的3000多名员工参与其中。这是迄今为止全球类似试验中规模最大的一次,也再次引发各界对“做四休三”工作模式的热议。

全球最大型试验

据报道,参与这项试验的有来自70家英国企业的3300多名员工。试验从本周一开始,为期6个月。参与试验的员工每周将额外获得1天带薪休假的机会,而收入不会减少。

参与企业表示,新冠疫情推动了企业管理人员对工作方式进行反思,希望通过这项试验测试是否可以在不造成生产力损失的情况下缩短工时,同时提升员工的心理健康和福利。

英国慈善银行(Charity Bank)首席执行官埃德·西格尔(Ed Siegel)说:“20世纪的五天工作制概念不再是21世纪企业的最佳选择。我们坚信,在工资和福利不变的情况下,四天工作制将创造更快乐的员工团队,并将对企业的生产率、客户体验和我们的社会使命产生同样积极的影响。

据悉,此次试验是全球同类试验中规模最大的一次,涉及金融、酒店、护理、餐饮、动画制作等多个行业的约70家英国公司,并将由牛津大学、剑桥大学和波士顿学院的研究人员对试验结果进行监测。

研究人员将记录员工对多休息一天的反应,包括压力、工作和生活满意度、健康、睡眠、能源使用和旅行等因素。

试验结果将在2023年公布,而后将由研究机构向英国政府提交正式审议,以建立每周工作32小时的制度。

我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。

“100-80-100”模式

消息一出,迅速占据各大英媒头条。英国广播公司(BBC)的标题十分吸引眼球:“员工们拿100%的薪水,工作时长只有80%”。

尽管上班时间减少了,但实际上,新模式鼓励的是企业更多关注工作效率,而不是工作时长。

据悉,这项试验的发起方是一家名为“4 Day Week Global”的非营利性组织。该组织自2021年开始倡议试行每周四天工作制,目前已在西班牙、爱尔兰、美国和英国的苏格兰地区进行试点。

该倡议采用的是“100-80-100”模式,即企业用100%的工资支付员工80%的工作时间,以换取100%的产出。具体操作上,不同公司可根据各自情况来决定哪一天休息,以及每周的工时。

“4 Day Week Global”首席执行官乔·奥康纳(Joe O’connor)表示,“越来越多的公司已经认识到,需要竞争的新领域是如何保证员工的生活质量,而缩短工时、聚焦产出将赋予它们竞争优势。”

该组织称,大量研究表明,每周四天工作制可以提高工作效率,降低公司开支,提升员工幸福感。78%的员工表示,每周工作四天会感到更快乐,压力更小。63%的企业发现,每周工作四天更容易吸引和留住人才。

此外,缩短工时也能产生一定的经济效益。有数据显示,引入四天工作制将使英国商业街销售额增加约580亿英镑,这是因为每周三天的假期会让消费者多出20%的购物时间,在培养兴趣爱好、园艺等方面的支出也可能增加。

如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取

一种新趋势?

英国的做法让不少网友表示羡慕,希望自己有朝一日也能实现工作生活相平衡。更多人关注,多国尝试的“做四休三”会不会成为未来一种新趋势。

事实上,全球范围内已有不少国家和企业对此进行探索,并取得了一些成绩。

早在2015至2019年期间,冰岛就进行过一次缩短工时的试验,主要涉及幼儿园、医院等公共服务部门。当时,有2500名冰岛员工(相当于冰岛劳动力的1%)进行了每周工作四天的尝试。

试验结束后,参与者们普遍反映生活压力降低,幸福感提升,且工作效率也提高了25%-40%,员工士气亦有大幅提振。

如今,冰岛已有超过80%的员工每周工作35小时,并且正在酝酿迈向下一步——每周工作32小时。

而在加班文化盛行的日本,近年来也在探索这一新模式。2019年8月,微软日本办公室试行每周四天工作制且不减薪,约有2300名员工连续5个周五放假。

微软表示,新模式下生产率提高了40%,会议效率更高,员工也更快乐,还节省了用电量和用纸量。

尽管多数案例都获得成功,但缩短工时也可能带来意想不到的副作用。

批评人士指出,四天工作制可能给一些员工带来更大压力,因为他们必须在更短的时间内完成更多的工作。此外,缩短工时在以客户为导向,或者7天24小时运营的紧急服务类工作中是不现实的。

还有人指出,虽然规定的工作时间减少了,但由于工作量不变,因此真正的工作时间并未改变,由此导致的加班工资还会给雇主带来额外成本。

例如,法国进行的一项类似试验发现,员工们依然需要五天时间完成既定任务,这也导致公司的劳务成本上升。

相关人士表示,尽管“四天工作制”是对传统工作模式的颠覆,但要推进到政府层面并落地,还有很长的路要走。那么你觉得“四天工作制”怎么样呢?可能成为将来的主流吗?

来源:上观新闻

收起阅读 »

如何快速在团队内做一次技术分享?

前言相信很多小伙伴跟我一样,是一位奋斗在一线的业务开发,每天有做不完的任务,还有项目经理在你耳边催你,“这个功能今天能完成吗?”其实作为一名前端工程师,任务就是完成 Leader 的任务, 但公司实行 OKR 以来,你就不得不在完成任务的基础上加上几条,“提示...
继续阅读 »

前言

相信很多小伙伴跟我一样,是一位奋斗在一线的业务开发,每天有做不完的任务,还有项目经理在你耳边催你,“这个功能今天能完成吗?”其实作为一名前端工程师,任务就是完成 Leader 的任务, 但公司实行 OKR 以来,你就不得不在完成任务的基础上加上几条,“提示个人能力”是我任务之外一个长期目标。


为了能完成这个目标,团队内部分享就成了这个目标的关键结果,那么如何在短时间内完成这项任务呢?下面分享下我的技巧。

明确主题

首先我们要明确公司需要什么?我们不能随便搞一个知识点去分享,这样没有人愿意去听,比如公司接下来可能会上前端监控系统,那么我们可以在先做一个技术调研,出一个《前端监控体系搭建要点》,比如公司接下来需要做小程序,那么我们可以出一个《小程序跨端实现方案探索》等,如果没有什么新的功能要开发,那么我们也可以谈一谈《前端性能优化》、《Typescript 快速上手》,总之要明确一个切合实际的目标。

巧用搜索引擎

确定好主题后,我们可以在技术社区搜索相关的技术文章,比如掘金、知乎、思否、微信公众号等, 比如直接在掘金搜索“性能优化” 然后按热度排序,就可以找到不错的文章。


接下来我们需要根据这些文章中的内容制作 PPT

使用 markdown 来制作 PPT

程序员做 PPT 可能会浪费不少时间,所以我选择是 markdown 来制作 PPT,这里我分享 2 个工具

Marp for VS Code vscode 插件

只用关注内容,简单分隔一下,就可以制作 PPT,看下 marp 官方文档可以很快学会用法,看看 jeremyxu 写的效果,项目地址:kubernetes 分享 PPT 源文件


二: Slidev 也可以让我们用 Markdown 写 PPT 的工具库

官网地址:sli.dev, 基于 Node.js、Vue.js 开发,而且它可以支持各种好看的主题、代码高亮、公式、流程图、自定义的网页交互组件,还可以方便地导出成 PDF 或者直接部署成一个网页使用。

  • 演讲者头像

当然还有很多酷炫的功能,比如说,我们在讲 PPT 的时候,可能想同时自己也出镜,Slidev 也可以支持。


  • 演讲录制

Slidev 还支持演讲录制功能,因为它背后集成了 WebRTC 和 RecordRTC 的 API,


文章转 markdown


这里推荐下我写的油猴扩展

  • 第一步: 安装 chrome 油猴扩展

  • 第二步: 安装文章拷贝助手

可以直接将文章转为 markdown 格式,目前已经支持掘金、知乎、思否、简书、微信公众号文章。

接下来就根据 H2 分页组织PPT内容即可。

---
layout: cover
---

# 第 1 页

This is the cover page.

<!-- 这是一条备注 -->

较长的内容可以将内容改为幻灯片编写备注。它们将展示在演讲者模式中,供你在演示时参考。

小结

本文讲述了我在准备团队内容分享的小技巧,我认为最重要的就是结合公司实际来做分享修改,无论主题也好文章内容也罢,虽然文章是别人写的,但要经过自己的思考和消化,变成自己的知识,这样我们才可以快速成长!在此,祝各位小伙伴在能够获知识的同时得较高的 OKR 考核。

以上就是本文全部内容,希望这篇文章对大家有所帮助,也可以参考我往期的文章或者在评论区交流你的想法和心得,欢迎一起探索前端。

作者:狂奔滴小马
来源:juejin.cn/post/7106810693910790152

收起阅读 »

抖音 Android 包体积优化探索:基于 ReDex 的 DEX 优化落地实践

抖音是字节跳动规模最大、运行环境复杂度最高的应用之一。在 ReDex 落地初期,由于对复杂度估计不足,在独立灰度和全量灰度期间引起了一些问题,在解决问题的过程中,我们也逐步形成了一套迭代流程以保证优化的稳定性。前言应用安装包的体积会显著影响应用的下载速度和安装...
继续阅读 »

抖音是字节跳动规模最大、运行环境复杂度最高的应用之一。在 ReDex 落地初期,由于对复杂度估计不足,在独立灰度和全量灰度期间引起了一些问题,在解决问题的过程中,我们也逐步形成了一套迭代流程以保证优化的稳定性。

前言

应用安装包的体积会显著影响应用的下载速度和安装速度,按照 Google 的经验数据,包体积每增加 1M 会造成 0.17%的新增折损。抖音的一些实验也证明了包体积会显著影响下载激活的转化率。

Android 的安装包是 APK 格式的,在抖音的安装包中 DEX 的体积占比达到了 40%以上,所以针对 DEX 的体积优化是一种行之有效的包体积优化手段。

DEX 本质上是由 Java/Kotlin 代码编译而成的字节码,因此,针对字节码进行业务无感的通用优化成为我们的一个探索方向。

优化结果

终端基础技术团队和抖音基础技术团队在过去的一年里,利用 ReDex 在抖音包体积优化方面取得了一些明显的收益,这些优化也被同步到了其他各大 App 上。

在抖音、头条和其他应用上,我们的优化对 APK 体积的缩减普遍达到了 4%以上,对 DEX 体积的缩减则可以达到 8% ~ 10%

优化思路

在 android 应用的构建过程中,Java/Kotlin 代码会先被编译成 Class 字节码,在这个阶段 gradle 提供了 Transformer 可以进行字节码的自定义处理,很多插件都是在这个阶段处理字节码的。然后,Class 文件经过 dexBuilder/mergeDex 等任务的处理会生成 DEX 文件,并最终被打进安装包中。整个过程如下所示:


所以,针对字节码的优化是有 2 个时机可以进行的:

  • 在 transformer 阶段对 Class 字节码进行优化

  • 在 DEX 阶段对 DEX 文件进行优化

显然,对 DEX 进行优化是更理想的一种方式,因为在 DEX 文件中,除了字节码指令外,还存在跨 DEX 引用、字符串池这样的结构,针对这些 DEX 格式的优化是无法在 transformer 阶段进行的。

在确定了针对 DEX 文件进行优化的思路后,我们选择了 facebook 的开源框架 ReDex 作为优化工具,并对其进行了定制开发。

选择 ReDex 的原因是它提供了丰富的基础能力,ReDex 的基础能力包括:

  1. 读写及解析 DEX 的能力,同时可以在一定程度上读取并解析 xml 和 so 文件

  2. 解析简单的 proguard keep 规则并匹配类/方法/成员变量的能力

  3. 对字节码进行数据流分析的能力,提供了常用的数据流分析算法

  4. 对字节码进行合法性校验的能力,包括寄存器检查、类型检查等

  5. 一系列的字节码优化项,每项优化称为一个 pass,多个 pass 组成 pipeline 对 DEX 进行优化


我们基于这些能力进行了定制和扩展,并期望最终建立完善的优化体系。

优化项

在抖音落地的优化项,包括 facebook 开源的优化和我们自研的优化,从其出发点来看,可以大致分为下面几种:

  • 通用字节码优化:通常意义下的编译优化,如常量传播、内联等,一般也可在 Transformer 阶段实现

  • DEX 格式优化:DEX 中除了字节码指令外,还包括字符串池、类/方法引用、debug 信息等等,针对这些方面的优化归类为 DEX 格式优化

  • 针对编程语言的优化:Java/Kotlin 的一些语法糖会生成大量字节码,可以对这些字节码进行针对性的分析和优化

  • 提升压缩率的优化:将 DEX 打包成 APK 实质上是个压缩的过程,对 DEX 内容进行针对性的优化可以提升压缩率,从而产生体积更小的 APK

这几种优化没有明确的标准和界线,有时一个 Pass 会涉及到多种,下面详细介绍一下各项优化。

通用字节码优化

ConstantPropagationPass

该 Pass 实际上包含了常量折叠和常量传播。

常量折叠是在编译期简化常量的过程,比如

复制

y = 7 - 14 / 2
--->
y = 0

常量传播是在编译期替代指令中已知常量的过程,比如

int x = 14;
int y = 7 - x / 2;
return y * (28 / x + 2);
--->
int x = 14;
int y = 7 - 14 / 2;
return (7 - 14 / 2) * (28 / 14 + 2);

上面的例子经过 常量折叠 + 常量传播优化后就会简化为

int x = 14;
int y = 0;
return 0;

再经过死代码删除就可以最终变为return 0。

具体的优化过程是:

  1. 对方法进行数据流分析,主要针对 const/move 等指令,得出一个寄存器在某个位置可能的取值

  2. 根据分析的结果,进行指令替换或指令删除,包括:

  • 如果值肯定是非空的,可以将对应的判空去掉,比如 kotlin 生成的 null check 调用

  • 如果值肯定为空,可以将指令替换为抛空异常

  • 如果值肯定让某 if 分支走不到,可以删除对应的分支

  • 如果值是固定的,可以用 const 指令替换对应的赋值或计算指令

一个方法经过 ConstantPropagationPass 优化后,可能会产生一些死代码,比如例子中的int y = 0,这也为后续的死代码删除创造了条件。

AnnoKillPass

该 Pass 是用来移除无用注解的。注解主要分为三种类型:

  • SOURCE:java 源码编译为 class 字节码就不可见,此类注解一般不用过于关注

  • CLASS:字节码通过 dx 工具转成 DEX 就不可见,代码运行时不需要获取信息,所以一般来说也不需要关注,实测发现部分注解仍然存在于 DEX 中,这部分注解可以进行优化

  • RUNTIME:DEX 中仍然可见,代码运行中可以通过 getAnnotations 等接口获取注解信息,但是随着业务的迭代,可能获取注解信息的代码已经去掉,注解却没有下掉,这部分注解会被 ReDex 安全的移除

除此之外,实际上为了支持某些系统特性,编译器会自动生成系统注解,虽然注解本身是 RUNTIME 类型,但是可见性是VISIBILITY_SYSTEM

  • AnnotationDefault : 默认注解,不能删除

  • EnclosingClass : 当前内部类申明时所在的类

  • EnclosingMethod : 当前内部类申明时所在的方法

  • InnerClass : 当前内部类名称

  • MemberClasses : 当前类的所有内部类列表

  • MethodParameters : 方法参数

  • Signature : 泛型相关

  • Throws : 异常相关

举例说明


编译器生成 1MainApplication$1这个匿名内部类,带有 EnclosingMethod 和 InnerClass 注解


系统提供以下接口获取类相关的信息,就是通过分析相关的系统注解来实现的

  • Class.getEnclosingMethod

  • Class.getSimpleName

  • Class.isAnonymousClass

  • ....

如果代码中不存在使用这些接口获取类信息的逻辑,就可以安全的移除这部分注解,从而达到缩减包大小的目的。

RenameClassesPass

该 Pass 通过缩减类名的字符串长度来减小包体积

比如把类名从La/b/c/d/e;改为LX/a;,可以类名字符串的长度,从而达到包大小缩减的目的。实际上 Proguard 本身已经提供类似的功能: -repackageclasses 'X',效果如下:


但是-repackageclasses 'X'的处理会影响 ReDex 的 InterDexPass 的算法逻辑(InterDexPass 可以参考下文),导致收益缩减

  • 收益测试

  • Proguard-repackageclasses 'X' 收益: 600K+

  • RedexInterDexPass 收益: 400K+

  • 同时应用 Proguard-repackageclasses 'X' 和 RedexInterDexPass 收益: 40K+

本质原因在于 Proguard 重命名后,影响了 InterDexPass 函数引用权重分配,导致 InterDex 收益被回收

  • 解决方案

  • InterDexPass 深入分析原理,优化权重算法

  • 先执行 InterDexPass,后执行类似 Proguard 的-repackageclasses 'X'

权重算法优化相对来说比较复杂,同时存在众多不可确定性,比如潜在的跟其他优化的冲突,所以我们采取了第二种解决方案。

这里需要解决的一个关键点在于如何确定一个类名是否可以被安全的重命名,我们采取了一个比较取巧的方式,ReDex 会分析 Proguard 传递上来 mapping.txt 文件,只要我们保持跟 Proguard 类重命名优化一样的处理策略,就不会引发反射/native 调用/序列化等一系列问题。


但是执行起来还是碰到各种千奇百怪的问题,比如 Signature 系统注解失效问题。Signature 注解的内容是非标准的类名格式,所以类重命名后简单回写字符串或者更新 Type 类型会导致 Signature 注解失效,最后通过深入解析 Signature 格式规避了这个问题。

StringBuilderOutlinerPass

该 Pass 是针对 StringBuilder 的 CallSites 进行分析缩略的优化,与死代码删除搭配使用可以有不错的优化效果。

为何要优化 StringBuilder 呢?在 Java 的代码开发过程中,字符串操作几乎是我们最经常做的一件事情,无论是实际处理字符串拼接还是各种不同数据类型之间的拼接操作。而这些拼接操作都会被 Java 的 de-sugar 优化为 StringBuilder 操作。比如:var log = "A" + 1 + "B" + 1.0f + other_var; 会被优化为:

StringBuilder builder = new StringBuilder();
builder.append("A"); builder.append(1);
builder.append("B"); builder.append(1.0f);
builder.append(other_var);
builder.toString();

因此我们对 StringBuilder 的所有 Callsites 进行分析,在最好情况下多个方法调用可以被优化为一个调用,这个方法是一个 outline (外联)方法,具体的参数拼接和 toString 被隐藏在函数内部:

invoke-static {v1, v2, v3} Outline;.bind:([Ljava/lang/Object)Ljava/lang/String;

优化步骤可以被简单的分为如下几个步骤:

  1. 生成一个泛型的外联方法、以及数个特定参数的方法:我们可以认为生成的方法大概是这样的

@Keep
public static String bind(Object... args) {
  StringBuilder builder = new StringBuilder();
  for (int i = 0; i < args.length ; i++) {
      builder.append(args[i]);
  }
  return builder.toString();
}
  1. 收集StringBuilder 的 CallSites :通过抽象解释和不动点分析,分析所有的 StringBuilder 操作,对 append、new-instance、和 init 方法分类。判断每次 append 的参数是不是 immutable 操作,如果增加的 insn 少于减少的 insn 即会减少代码,就对这里进行处理。

  2. 生成外联方法调用:由于我们使用了泛型方法来接受参数,因此我们要对基础类型生成 ValueOf 的转换操作、并且删除append 方法前为了防止被错误优化我们还需要插入 move 指令来 copy 原有参数(这些 move 指令会被后续优化正确删除)、如果参数个数还在我们生成的特定 outline 方法范围内我们就可以使用特定方法来生成外联函数,其余的将使用泛化的外联来接受。

DEX 格式优化

InterDexPass

该 Pass 是针对跨 DEX 引用的优化。

跨 DEX 引用是指当一个 DEX 需要“使用”到另一个 DEX 中的类/方法/变量时,需要在本 DEX 中保存一份对应的类/方法/变量的 id,如果 2 个 DEX 用到了相同的字符串,那么这个字符串在 2 个 DEX 都需要进行定义。所以,改变类/方法/变量和字符串在 DEX 中的分布,可以减小引用的数量,从而减小 DEX 的体积。从原理中也可以看出,该优化对单 DEX 的应用是无效的。


从上图可以看到,进行类重排后,DEX0 的类引用和方法引用数量都减少了,DEX 的体积也会因此减小。

具体的优化过程是:

  1. 收集每个类涉及的所有引用,按照引用数量和类型计算出类的权重

  2. 根据权重计算出每个类的优先级

  3. 根据优先级选取一个类放入 DEX 中,然后调整剩余类的优先级,重复此步骤直到所有类都被处理

ReBindRefsPass

该 Pass 是针对方法引用的优化,其原理同 InterDexPass。

在字节码中,invoke-virtual/interface指令需要一个方法引用,在很多情况下,这个引用指向的是子类或者实现类的引用,把这个引用替换成父类和接口的方法引用不会影响运行时逻辑,同时会减少 DEX 中方法引用的数量。在生成 DEX 的时候,方法引用的 65536 限制通常是最先遇到的瓶颈,该优化也可以缓解这种情况。


如上图所示,优化前 caller 方法的 invoke 指令使用的是子类引用,其伪指令如下所示,需要用到 2 个引用

new-instance v0, Sub1
invoke-virtual v0, Sub1.a()
new-instance v1, Sub2
invoke-virtual v1, Sub2.a()

优化后,invoke 指令都指向其父类应用,2 个引用可以合并为 1 个,减少了 DEX 中的引用数量

new-instance v0, Sub1
invoke-virtual v0, Base.a()
new-instance v1, Sub2
invoke-virtual v1, Base.a()

针对编程语言的优化

KotlinDataClassPass

该 Pass 是对 Kotlin data class 的优化,基本思路是对 data class 的生成代码进行精简。

解构声明优化

Kotlin 中存在解构声明这种语法,可以更方便的创建多个变量,基本用法如下

data class Person(val name: String,val age: Int)
val (name,age) = person("John",20)

kotlinc 会为Person类生成 get 方法和 componentN 方法,如下是伪代码表示

Person {
    String name;
    Int age;

    getName(): String { return name; }
  getAge(): Int { return age; }
    component1(): String { return name; }
    component2(): Int { return age; }
}

// 解构声明编译为
val name = person.component12 1()
val age = person.component2()

可以看到,get 和 component 的逻辑是一样的,所以在编译期,可以进行全局的匹配,用 get 替换掉 component,然后再删除 component。

toString 等生成方法优化

kotlin compiler 为 data class 生成的 toString 具有相似的代码结构,因此可以生成一个辅助方法,然后在所有 data class 的 toString 方法中调用这个辅助方法,即外联,从而减少指令数量。

equals 和 hashCode 也可以进行类似优化,但是风险相对较高,因此单独为这些优化配置了开关,业务方可以视情况开启。

提升压缩率的优化

RegAllocPass

DEX 及其他文件经过压缩打成 APK,如果能通过改变 DEX 的内容来提升压缩率,那么也会减小最终的包体积。RegAllocPass 就是通过重新分配寄存器来提升压缩率的。

dx 生成 DEX 时使用的是线性寄存器分配算法,其基本步骤是进行存活变量分析,然后计算出每个变量的活跃区间,再根据活跃区间依次为变量分配寄存器,超出活跃区间的寄存器可以进行再分配,其优点是运行速度快,但结果往往不是最优的。

比如下面的代码,dx 分配了 6 个寄存器,v0 ~ v5

public static double calculateLuminance(@ColorInt int color) {
  final double[] result = getTempDouble3Array();
    colorToXYZ(color,result);
  return result[1] / 100;
}


相对的,ReDex 使用了图着色算法进行寄存器分配,基本步骤是进行存活变量分析,并构建冲突图,冲突图的每个节点是一个变量,如果 2 个变量可以同时存活,就在两个节点之间建立边,最后为冲突图着色,每个颜色代表一个寄存器,着色完成即寄存器分配完成。着色法相对更慢,结果一般更优。对上面同样的代码,着色法使用了 4 个寄存器,v0 ~ v3。


DEX 中的方法使用的寄存器越少,其内容重复率就越高,压缩率也会更大,从而减小了包体积。

抖音落地

抖音是字节跳动规模最大、运行环境复杂度最高的应用之一。在 ReDex 落地初期,由于对复杂度估计不足,在独立灰度和全量灰度期间引起了一些问题,在解决问题的过程中,我们也逐步形成了一套迭代流程以保证优化的稳定性。下面介绍一下我们遇到过的典型问题及当前的迭代流程。

遇到的问题

兼容性问题

一般来说,只要按照字节码规范进行优化,就不会有兼容性问题,因为 dalvik/art 也是按照规范去校验和运行字节码的,即使进行了错误的优化,引起的问题也应该是共性问题。但很多事都有例外,ReDex 就在某品牌手机的部分 Android 5.x 的机型上遇到了问题。

从 log 和一些 hook 来看,某品牌手机对 5.x 的 art 做了大量的魔改,可以推断其魔改存在一些问题,导致对正确的字节码的校验和运行也可能出现问题。一个可能的原因是:在 ReDex 进行优化时,会对一些方法体的指令顺序进行重排,这种重排是不影响方法的逻辑的,但是可能会改变一部分指令,魔改后的 art 在校验这样的方法时可能会报 verify error,引起 crash。

最终通过黑名单配置跳过了这些方法的优化规避了问题,在后续的优化过程中,没有再遇到类似的问题。

复杂场景优化问题

抖音业务复杂,代码写法多样,给静态分析和优化增加了一些难度,也更容易遇到问题。下面是 2 个典型问题:

1.空方法优化问题 代码中可能存在一些空方法,排除掉反射和 natvie 调用等场景后,剩下的空方法应该是可以删除的。但是在做优化时,却遇到了 crash,如以下代码

object XXXSDKHelper {
  init {
      initXXXSDK()
    }
    fun fakeInit() {
    }
}

// 初始化任务
public class XXInitTask implements Runnable {
    @Override
  public void run() {
        XXXSDKHelper.INSTANCE.fakeInit();
    }
}

在初始化代码中调用fakeInit,它是一个空方法,调用它的目的是触发XXSDKHelper类加载从而执行init语句块,如果删除了这个空方法,就会导致初始化未执行,在后续的流程中抛空指针。

2.复杂反射问题

对于 Class.forname(...)等简单的反射用法,静态分析是可以分析出来的,但是对一些经过字符串拼接或者嵌套之后的反射,静态分析很难分析到。因此,对可能会被反射的代码进行优化需要非常小心,通常来说,匿名内部类是不会通过反射调用的,基于此前提,我们进行了匿名内部类的重命名优化,但是在灰度后,发现某些第三方 SDK 会通过复杂的运行时逻辑对匿名内部类进行了反射调用,最终导致了 ClassNotFoundError。

复杂场景的优化问题有些是业务代码不规范造成的,但更多的是优化前提(空方法可以删除/匿名内部类不会被反射)不成立所导致,所以在进行优化时首先需要对假设进行谨慎的验证。

迭代流程

为了减少稳定性问题,我们总结了 ReDex Pass 的迭代流程。

在对一项 Pass 有了初步构思后,组内会进行可行性讨论,如果理论上可行就进入开发和验证阶段,之后同步进行至少 2 轮的独立灰度验证和业务方 Pass 评审,最后进行全量灰度验证。其中任意一个环节发现问题,都会重新进行整个流程。


通过这个流程,我们大大减少了稳定性问题遗留到灰度阶段的可能,在不断完善迭代流程的同时我们也在探索通过加强单元测试、自动化测试等方式来提升质量。

后续规划

ReDex 仍然在持续迭代中,未来我们会在以下几个方向继续进行深入探索:

  1. 更多包体积优化的探索和迭代,同时探索字节码优化在性能提升方面的可能性

  2. 提升字节码质量

  • 更加严格的合法性校验;ReDex 之前已经检测出若干自定义插件和 proguard 的问题,将问题拦截在了编译期,后续会继续提升该能力

  • 建立更加完善的质量验证体系;ReDex 作为编译期的全局字节码优化方案,如果保证优化后的字节码质量一直是个痛点,我们会继续在单元测试、自动化测试等方向探索质量提升的手段

  1. 增加编译期监控,更加快速便捷的解决编译期字节码问题,提升接入体验

  2. 其他应用方向探索;如方法插桩、某些条件下的死代码扫描等。

作者 | 冯瑞;廖斌斌;刘丰恺

来源:https://www.51cto.com/article/710484.html

收起阅读 »

瞄准Web3:互联网巨头捍卫流量“王座”之争

web
日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。Web3 是什么?有人对它寄予厚望,认为这是真正可实现的下一代互联网;有人表示悲观,觉得这是一个“去中心化”的乌托邦,“就像是一场梦,醒来还是很感动”。对...
继续阅读 »
日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。

Web3 是什么?

有人对它寄予厚望,认为这是真正可实现的下一代互联网;有人表示悲观,觉得这是一个“去中心化”的乌托邦,“就像是一场梦,醒来还是很感动”。对大多数人来说,Web3

的定义是什么并不重要。重要的是,在可见的未来,Web3 能给我们带来什么。

在普遍认知中,Web3 是一个基于区块链技术的去中心化互联网。其中,“去中心化”是Web3 的精神内核。围绕这一内核,Web3 的理想愿景是,将互联网及其生产内容的控制权从少数几家科技巨头手中返还到个人,从而让用户能对自己的身份和数据有更多控制权。

换句话说,Web3 就像是曾经的“占领华尔街运动”在当今互联网世界的复刻,针对的恰恰是 Web2 时代的既得利益者,即 Meta、亚马逊、谷歌,乃至BAT 这类巨头。面对这一可能的威胁,巨头们也陆续有了动作,纷纷落子,希冀在 Web3 的棋盘上继续巩固各自的生态帝国,继续成为互联网世界中隐形的规则制定者和秩序维护者。

日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。

谷歌的布局

谷歌对于 Web3 的横空出世有其自身的判断。

在谷歌云的官方博客中,如此描述:“区块链和数字资产正在改变世界存储和传递信息以及价值的方式。如今的 Web3 热潮就如同10-15年前开源和互联网的兴起。正如开源开发是互联网早期不可或缺的一部分一样,区块链正在为用户和企业带来创新的推动力。”

在今年1月,谷歌云曾对外披露,他们正在研究怎么使用加密货币支付。当时,谷歌云金融业务副总裁 Yolande Piazza 表示,已经成立了一个谷歌云数字资产团队,来协助客户在基于区块链的平台上创建新产品。彼时已经有人猜测,谷歌云未来会接受数字货币作为支付方式。

而此次谷歌云组建的Web3团队目标指向则更为清晰。它旨在构建 Web3 世界的基础设施,主要为有兴趣编写Web3软件的开发人员提供后端服务。

在发给团队的电子邮件中,谷歌云副总裁 Amit Zavery 写道,虽然世界仍处于拥抱 Web3的早期阶段,但 Web3 是一个已经显示出巨大潜力的市场,许多客户要求谷歌增加对 Web3 和 Crypto 相关技术的支持。

在外媒的公开采访中,Zavery明确表示,对于谷歌来说,参与这一趋势的方式不是直接成为加密货币浪潮的一部分,而是为Web3开发者提供基础设施服务。谷歌不参与也不干涉具体业务,而是计划成为基础设施提供商,降低开发者基于区块链设计去中心化系统的门槛,推动企业在业务中使用和利用Web3的分布式特性。

尽管今年以来,资本对于比特币这一市场的投资热情大为减弱,但Zavery表示,区块链应用不断进入主流,并在金融服务和零售业等行业中有越来越高的参与度。未来,谷歌或许会设计相关系统,使人们更容易探索链上链下数据,同时简化构建和运行区块链节点进行验证和记录交易的过程。

而在团队构成上,Zavery透露,新组建的Web3团队主要是将内部参与过 Web3 项目的员工合并到一起,然后从外部招募一些区块链开发工程师和其他相关人才。2019年加入谷歌的前花旗集团高管 James Tromans 将领导产品和工程小组,并向Zavery汇报。

谷歌的动机

谷歌入场Web3 ,除了未雨绸缪布局 Web3 基础设施之外,是否有其他考量?

有人注意到,这支 Web3 团队的主导部门是谷歌云,因此猜测,云服务市场的博弈或许也是个中关键。

谷歌母公司 Alphabet 2022 年一季度财报显示,谷歌云营收同比增长 44% 至 58.2 亿美元。Alphabet 首席财务官 Ruth Porat 表示,谷歌云服务的增长速度已经超过了其核心的广告部门,且员工人数增长最快的就是云部门。

尽管谷歌云表现不俗,但目前来说,仍旧无法与微软 Azure 抗衡,更不用说在云计算市场一骑绝尘的 AWS 。更值得一提的是,早在2018年,就有大量以太坊、比特币还有其他区块链的节点部署在 AWS 上。而到了2021 年,AWS Marketplace 总监 Marta Whiteaker 曾透露:“目前以太坊全球 25% 的工作负载都运行在 AWS 上。”

可以说,亚马逊无论是在 Web2 还是 Web3 时代,在云服务领域都占得了先机。而在Web3赛道策略相对保守的谷歌之所以选择在这个时间入场,竞争对手带来的压力可能也是一大诱因。

在一定程度上可预见的赛道内,Web3 的发展极有可能会冲击到谷歌的云服务市场份额,甚至波及广告业务,进而在更大范围内降低谷歌对全球数字生态的影响力,为了捍卫其庞大的生态版图,适时入场至少不至于在真正交锋时完全陷入被动。

局中人

面对 Web3,科技巨头、开发者、用户表现出了泾渭分明的态度。

除了谷歌之外,其他互联网巨擘也在选择拥抱Web3。

亚马逊在2018 年便推出了自己的区块链支持服务 Amazon Managed Blockchain ,主要面向在 Hyperledger Fabric 或以太坊中搭建项目的开发人员提供托管和硬件服务,提高客户为 DeFi、供应链、金融服务等业务用例创建和利用可扩展区块链技术的能力。而不久前,亚马逊CEO Andy Jassy也公开表态,亚马逊在数字资产行业和 NFT领域看到了巨大的潜力。

微软从2015 起就开始为区块链开发商提供支持。今年3月,微软投资区块链初创公司ConsenSys也被视为微软在加密相关领域的一次罕见押注。因为ConsenSys被投资者认为是为 Web3 提供动力的公司之一。有分析人士认为,这一举动展示了微软对Web3日益增长的兴趣。

在科技巨头们纷纷下注之际,开发者们对 Web3 的反映要冷淡得多。

对于谷歌入场Web3 ,美国著名软件工程师 Grady Booch在推特上表达了他的失望,并直言这种投入是对资源的浪费。

调查机构Stack Overflow在今年4月出具的报告也揭示了类似的态度。在接受调查的595名开发人员中,37%的人不知道Web3 是什么;在知道的人群中,25%的人认为 Web3

是互联网的未来;15%的人认为这是一堆炒作;14%的人认为它对加密货币领域相关应用程序很重要;9%的人认为这是一个骗局。

对于 Web3 的潜在用户群体或者吃瓜群众来说,Web3更像是一个仍旧遥远的概念。虽然“去中心化”的愿景很美,但他们使用的产品和服务是否完全去中心化在现实角度看或许并不是关注焦点。

加密通讯应用 Signal 创始人 Moxie Marlinspike 指出,即使是很多极客,也不想运行自己的服务器。即使一家大的软件企业,运行自己的服务器也是很大的负担。基于此,云厂商才会取得成功。Web3 同样如此。“如果谷歌开发出更容易使用的服务,填补市场空白,那么即使该服务没有达到去中心化的程度,人们也会去那里。”

比如以太坊最大的节点服务提供商 Infura,其运行的节点分散在各地甚至是用户家中,但不断发生的 Infura 宕机事件向人们证明了,在“去中心化”的服务名目下,要真正实现大规模推广,还是要依赖中心化的基础设施。

用户期望的并不是一个单纯取代 Web2 的Web3 时代,而是一个边界不断拓宽、新场景不断涌现的数字世界,偶有惊喜又值得期待。

结语

在Web3 领域,关于“中心化”与“去中心化”之争一直存在。

矛盾的是,“去中心化”虽然是 Web3 信徒们奉为圭臬的理想,但真正主导 Web3 发展进度的其实是一群 Web2 时代“中心化”规则下的受益者。

根据网络监控公司Sandvine发布的2021年全球互联网现象报告显示,谷歌、Meta、Netflix、亚马逊、微软和苹果这六家企业产生了超过56%的全球网络流量,他们在2021年产生的流量占比超过了所有其他互联网公司的总和。

在如此可观的流量背后,这些科技巨头在事实上控制了用户的账号、交互、产出内容甚至是隐私,这也构成了其生态帝国的权力基石。由此来看,谷歌布局 Web3 这件事,依旧是 Web2 时代巨头博弈的续篇。

但 Web3 的可贵之处在于它仍是一片待开发的荒原,没有人能预判其爆发的时机。它提供了基于区块链的新价值模型,为市场带来了创新和颠覆的可能。不确定性让 Web3 危险,也让它浪漫,因为这块土地无限自由,不拒绝任何人的踏入。巨头的主动不见得是优势,小透明的崛起也不一定荒诞。前景不明,也意味着前景有无数可能。

参考资料:

  https://www.cnbc.com/2022/05/06/googles-cloud-group-forms-web3-product-and-engineering-team.html

  https://cointelegraph.com/news/amid-crypto-hype-google-s-cloud-unit-creates-web3-team

  https://www.itpro.co.uk/cloud/367612/google-cloud-is-reportedly-building-a-dedicated-team-to-support-web3-developers

  https://stackoverflow.blog/2022/04/20/new-data-developers-web3/

来源:www.51cto.com/article/710198.html

收起阅读 »