注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

元宇宙讨论

元宇宙讨论

元宇宙到底是什么?来畅所欲言
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

代码review,瑞出事来了!

不久之前,部门进行了一次代码评审。 代码整体比较简单,该吹B的地方都已经吹过了,无非是些if else的老问题而已。当翻到一段定时任务的一步执行代码时,我的双眼一亮,觉得该BB两句了。 谁知这群家伙,评审的时候满满的认同感,但评审结束不久,就给我冠了个事B的称...
继续阅读 »

不久之前,部门进行了一次代码评审。


代码整体比较简单,该吹B的地方都已经吹过了,无非是些if else的老问题而已。当翻到一段定时任务的一步执行代码时,我的双眼一亮,觉得该BB两句了。


谁知这群家伙,评审的时候满满的认同感,但评审结束不久,就给我冠了个事B的称号。


今天我就把当时的这些话儿整理整理,让大家说道说道,我到底是不是个事B。淦!


一个任务处理例子


代码的结构大体是这样的。


通过定时,这段代码每天晚上凌晨都要对数据库的记录进行一遍对账。主要的逻辑,就是使用独立的线程,渐进式的读取数据库中的相关记录,然后把这些记录,放在循环中逐条进行处理。


ExecutorService service = Executors.newFixedThreadPool(10);
...
service.submit(()->{
    while(true){
        if(CollectionUtils.isEmpty(items)){
            break;
        }
        List<Data> items = queryPageData(start, end); // 分页逻辑
        for(Data item : items){
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                //noop 
            }
            processItem(item);
        }
    }
});


等一下。在代码马上被翻过去的时候,我叫停了,这里的processItem没有捕获异常


通常情况下,这不会有什么问题。但静好的岁月,总是偶尔会被一些随机的事故打断。如果这是你任务的完整代码,那它就有一种非常隐晦的故障处理方式。即使你的单元测试写的再好,这段代码我们依然可以通过远程投毒的方式,通过问题记录来让它产生问题。


是的。以上代码的根本原因,就是没有捕捉processItem函数可能产生的异常。如果在记录处理的时候,有任何一条抛出了异常,不管是checked异常还是unchecked异常,整个任务的执行都会终止!


不要觉得简单哦,踩过这个坑的同学,请记得扣个666。或者翻一下你的任务执行代码,看看是不是也有这个问题。


Java编译器在很多情况下都会提示你把异常给捕捉了,但总有些异常会逃出去,比如空指针异常。如下图,RuntimeException和Error都属于unchecked异常。


图片


RuntimeException可以不用try...catch进行处理,但是如果一旦出现异常,则会导致程序中断执行,JVM将统一处理这些异常。


你捕捉不到它,它自然会让你的任务完蛋。


如果你想要异步的执行一些任务,最好多花一点功夫到异常设计上面。在这上面翻车的同学比比皆是,这辆车并不介意再带上你一个。


评审的小伙很谦虚,马上就现场修改了代码。


不要生吞异常


且看修改后的代码。


ExecutorService service = Executors.newFixedThreadPool(10);
...
service.submit(()->{
    while(true){
        if(CollectionUtils.isEmpty(items)){
            break;
        }
        List<Data> items = queryPageData(start, end); // 分页逻辑
        for(Data item : items){
            try {
                Thread.sleep(10L);
            } catch (InterruptedException e) {
                //noop 
            }
            try{
                processItem(item);
            }catch(Exception ex){
                LOG.error(...,ex);
            }
        }
    }
});
...
service.shutdownNow();


为了控制任务执行的频率,sleep大法是个有效的方法。


代码里考虑的很周到,按照我们上述的方式捕捉了异常。同时,还很贴心的把sleep相关的异常也给捕捉了。这里不贴心也没办法,因为不补齐这部分代码的话,编译无法通过,我们姑且认为是开发人员的水平够屌。


由于sleep抛出的是InterruptedException,所以代码什么也没处理。这也是我们代码里常见的操作。不信打开你的项目,忽略InterruptedException的代码肯定多如牛毛。


此时,你去执行这段代码,虽然线程池使用了暴力的shutdownNow函数,但你的代码依然无法终止,它将一直run下去。因为你忽略了InterruptedException异常。


当然,我们可以在捕捉到InterruptedException的时候,终止循环。


try {
    Thread.sleep(10L);
} catch (InterruptedException e) {
    break;
}


虽然这样能够完成预期,但一般InterruptedException却不是这么处理的。正确的处理方式是这样的:


while (true) {
    Thread currentThread = Thread.currentThread();
    if(currentThread.isInterrupted()){
        break;
    }
    try {
        Thread.sleep(1L);
    } catch (InterruptedException e) {
        currentThread.interrupt();
    }
}


除了捕捉它,我们还要再次把interrupt状态给复位,否则它就随着捕捉给清除了。InterruptedException在很多场景非常的重要。当有些方法一直阻塞着线程,比如耗时的计算,会让整个线程卡在那里什么都干不了,InterruptedException可以中断任务的执行,是非常有用的。


但是对我们现在代码的逻辑来说,并没有什么影响。被评审的小伙伴不满意的说。


还有问题!


有没有影响是一回事,是不是好的习惯是另一回事 。我尽量的装了一下B,其实,你的异常处理代码里还有另外隐藏的问题。


还有什么问题?,大家都一改常日慵懒的表情,你倒是说说


图片


我们来看一下小伙伴现场改的问题。他直接使用catch捕获了这里的异常,然后记录了相应的日志。我要说的问题是,这里的Exception粒度是不对的,太粗鲁。


try{
    processItem(item);
}catch(Exception ex){
    LOG.error(...,ex);
}


processItem函数抛出了IOException,同时也抛出了InterruptedException,但我们都一致对待为普通的Exception,这样就无法体现上层函数抛出异常的意图。


比如processItem函数抛出了一个TimeoutExcepiton,期望我们能够基于它做一些重试;或者抛出了SystemBusyExcption,期望我们能够多sleep一会,给服务器一点时间。这种粗粒度的异常一股脑的将它们捕捉,在新异常添加的时候根本无法发现这些代码,会发生风险。


一时间会议室里寂静无比。


我觉得你说的很对 ,一位比较资深的老鸟说, 你的意思是把所有的异常情况都分别捕捉,进行精细化处理。但最后你还是要使用Exception来捕捉RuntimeException,异常还是捕捉不到啊


果然是不同凡响的发问。


优秀的、标准的代码写法,其中无法实施的一个重要因素,就是项目中的其他代码根本不按规矩来。如果我们下层的代码,进行了正确的空指针判断、数组越界操作,或者使用类似guava的Preconditions这类API进行了前置的异常翻译,上面的这种问题根本不用回答。


但上面这种代码的情况,我们就需要手动的捕捉RuntimeException,进行单独的处理。


你们这个项目,烂代码太多了,所以不好改。我虽然有情商,但我更有脾气。


大家不欢而散。


End


我实在是想不通,代码review就是用来发现问题的。结果这review会一开下来,大家都在背后讽刺我。这到底是我的问题呢?还是这个团队的问题呢?让人搞不懂。


你们在纠结使用Integer还是int的时候,我也没说什么呀,现在就谈点异常处理的问题,就那么玻璃心受不了了。这B不能全都让你们装了啊。


什么?你要review一下我的代码?看看我到底有没有像我说的一样写代码,有没有以身作则?是在不好意思,我可是架构师哎,我已经很多年没写代码了。


你的这个愿望让你落空了!


图片


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

领导:谁再用定时任务实现关闭订单,立马滚蛋!

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢? 一般的做法有如下几种 定时任务关闭订单 rock...
继续阅读 »

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?


一般的做法有如下几种




  • 定时任务关闭订单




  • rocketmq延迟队列




  • rabbitmq死信队列




  • 时间轮算法




  • redis过期监听




一、定时任务关闭订单(最low)


一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明


image.png


我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟;通过上图我们看出,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS


二、rocketmq延迟队列方式


延迟消息
生产者把消息发送到消息服务器后,并不希望被立即消费,而是等待指定时间后才可以被消费者消费,这类消息通常被称为延迟消息。
在RocketMQ开源版本中,支持延迟消息,但是不支持任意时间精度的延迟消息,只支持特定级别的延迟消息。
消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别。


发送延迟消息(生产者)


/**
* 推送延迟消息
* @param topic
* @param body
* @param producerGroup
* @return boolean
*/
public boolean sendMessage(String topic, String body, String producerGroup)
{
try
{
Message recordMsg = new Message(topic, body.getBytes());
producer.setProducerGroup(producerGroup);

//设置消息延迟级别,我这里设置14,对应就是延时10分钟
// "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
recordMsg.setDelayTimeLevel(14);
// 发送消息到一个Broker
SendResult sendResult = producer.send(recordMsg);
// 通过sendResult返回消息是否成功送达
log.info("发送延迟消息结果:======sendResult:{}", sendResult);
DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("发送时间:{}", format.format(new Date()));

return true;
}
catch (Exception e)
{
e.printStackTrace();
log.error("延迟消息队列推送消息异常:{},推送内容:{}", e.getMessage(), body);
}
return false;
}


消费延迟消息(消费者)


/**
* 接收延迟消息
*
* @param topic
* @param consumerGroup
* @param messageHandler
*/
public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler)
{
ThreadPoolUtil.execute(() ->
{
try
{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setConsumerGroup(consumerGroup);
consumer.setVipChannelEnabled(false);
consumer.setNamesrvAddr(address);
//设置消费者拉取消息的策略,*表示消费该topic下的所有消息,也可以指定tag进行消息过滤
consumer.subscribe(topic, "*");
//消费者端启动消息监听,一旦生产者发送消息被监听到,就打印消息,和rabbitmq中的handlerDelivery类似
consumer.registerMessageListener(messageHandler);
consumer.start();
log.info("启动延迟消息队列监听成功:" + topic);
}
catch (MQClientException e)
{
log.error("启动延迟消息队列监听失败:{}", e.getErrorMessage());
System.exit(1);
}
});
}


实现监听类,处理具体逻辑


/**
* 延迟消息监听
*
*/
@Component
public class CourseOrderTimeoutListener implements ApplicationListener<ApplicationReadyEvent>
{

@Resource
private MQUtil mqUtil;

@Resource
private CourseOrderTimeoutHandler courseOrderTimeoutHandler;

@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent)
{
// 订单超时监听
mqUtil.messageListener(EnumTopic.ORDER_TIMEOUT, EnumGroup.ORDER_TIMEOUT_GROUP, courseOrderTimeoutHandler);
}
}



/**
* 实现监听
*/
@Slf4j
@Component
public class CourseOrderTimeoutHandler implements MessageListenerConcurrently
{

@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list)
{
// 得到消息体
String body = new String(msg.getBody());
JSONObject userJson = JSONObject.parseObject(body);
TCourseBuy courseBuyDetails = JSON.toJavaObject(userJson, TCourseBuy.class);

// 处理具体的业务逻辑,,,,,

DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("消费时间:{}", format.format(new Date()));

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}


这种方式相比定时任务好了很多,但是有一个致命的缺点,就是延迟等级只有18种(商业版本支持自定义时间),如果我们想把关闭订单时间设置在15分钟该如何处理呢?显然不够灵活。


三、rabbitmq死信队列的方式


Rabbitmq本身是没有延迟队列的,只能通过Rabbitmq本身队列的特性来实现,想要Rabbitmq实现延迟队列,需要使用Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)


死信交换机
一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。


一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
上面的消息的TTL到了,消息过期了。


队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。
死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机,并不是说死信交换机是某种特定的交换机


消息TTL(消息存活时间)
消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取值较小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。


byte[] messageBodyBytes = "Hello, world!".getBytes();  
AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setExpiration("60000");
channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);


可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串:当上面的消息扔到队列中后,过了60秒,如果没有被消费,它就死了。不会被消费者消费到。这个消息后面的,没有“死掉”的消息对顶上来,被消费者消费。死信在队列中并不会被删除和释放,它会被统计到队列的消息数中去


处理流程图


image.png


创建交换机(Exchanges)和队列(Queues)


创建死信交换机


image.png


如图所示,就是创建一个普通的交换机,这里为了方便区分,把交换机的名字取为:delay


创建自动过期消息队列
这个队列的主要作用是让消息定时过期的,比如我们需要2小时候关闭订单,我们就需要把消息放进这个队列里面,把消息过期时间设置为2小时


image.png


创建一个一个名为delay_queue1的自动过期的队列,当然图片上面的参数并不会让消息自动过期,因为我们并没有设置x-message-ttl参数,如果整个队列的消息有消息都是相同的,可以设置,这里为了灵活,所以并没有设置,另外两个参数x-dead-letter-exchange代表消息过期后,消息要进入的交换机,这里配置的是delay,也就是死信交换机,x-dead-letter-routing-key是配置消息过期后,进入死信交换机的routing-key,跟发送消息的routing-key一个道理,根据这个key将消息放入不同的队列


创建消息处理队列
这个队列才是真正处理消息的队列,所有进入这个队列的消息都会被处理


image.png


消息队列的名字为delay_queue2
消息队列绑定到交换机
进入交换机详情页面,将创建的2个队列(delayqueue1和delayqueue2)绑定到交换机上面


image.png
自动过期消息队列的routing key 设置为delay
绑定delayqueue2


image.png


delayqueue2 的key要设置为创建自动过期的队列的x-dead-letter-routing-key参数,这样当消息过期的时候就可以自动把消息放入delay_queue2这个队列中了
绑定后的管理页面如下图:


image.png


当然这个绑定也可以使用代码来实现,只是为了直观表现,所以本文使用的管理平台来操作
发送消息


String msg = "hello word";  
MessageProperties messageProperties = newMessageProperties();
messageProperties.setExpiration("6000");
messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes());
Message message = newMessage(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend("delay", "delay",message);


设置了让消息6秒后过期
注意:因为要让消息自动过期,所以一定不能设置delay_queue1的监听,不能让这个队列里面的消息被接受到,否则消息一旦被消费,就不存在过期了


接收消息
接收消息配置好delay_queue2的监听就好了


package wang.raye.rabbitmq.demo1;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
publicclassDelayQueue{
/** 消息交换机的名字*/
publicstaticfinalString EXCHANGE = "delay";
/** 队列key1*/
publicstaticfinalString ROUTINGKEY1 = "delay";
/** 队列key2*/
publicstaticfinalString ROUTINGKEY2 = "delay_key";
/**
* 配置链接信息
* @return
*/
@Bean
publicConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = newCachingConnectionFactory("120.76.237.8",5672);
connectionFactory.setUsername("kberp");
connectionFactory.setPassword("kberp");
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true); // 必须要设置
return connectionFactory;
}
/**
* 配置消息交换机
* 针对消费者配置
FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
HeadersExchange :通过添加属性key-value匹配
DirectExchange:按照routingkey分发到指定队列
TopicExchange:多关键字匹配
*/
@Bean
publicDirectExchange defaultExchange() {
returnnewDirectExchange(EXCHANGE, true, false);
}
/**
* 配置消息队列2
* 针对消费者配置
* @return
*/
@Bean
publicQueue queue() {
returnnewQueue("delay_queue2", true); //队列持久
}
/**
* 将消息队列2与交换机绑定
* 针对消费者配置
* @return
*/
@Bean
@Autowired
publicBinding binding() {
returnBindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2);
}
/**
* 接受消息的监听,这个监听会接受消息队列1的消息
* 针对消费者配置
* @return
*/
@Bean
@Autowired
publicSimpleMessageListenerContainer messageContainer2(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = newSimpleMessageListenerContainer(connectionFactory());
container.setQueues(queue());
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置确认模式手工确认
container.setMessageListener(newChannelAwareMessageListener() {
publicvoid onMessage(Message message, com.rabbitmq.client.Channel channel) throwsException{
byte[] body = message.getBody();
System.out.println("delay_queue2 收到消息 : "+ newString(body));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //确认消息成功消费
}
});
return container;
}
}


这种方式可以自定义进入死信队列的时间;是不是很完美,但是有的小伙伴的情况是消息中间件就是rocketmq,公司也不可能会用商业版,怎么办?那就进入下一节


四、时间轮算法


image.png


(1)创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)


(2)任务集合,环上每一个slot是一个Set
同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。


Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
(2)订单号,要关闭的订单号(也可以是其他信息,比如:是一个基于某个订单号的任务)


假设当前Current Index指向第0格,例如在3610秒之后,有一个订单需要关闭,只需:
(1)计算这个订单应该放在哪一个slot,当我们计算的时候现在指向1,3610秒之后,应该是第10格,所以这个Task应该放在第10个slot的Set中
(2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1


Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0:
(1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1
(2)如果是0,说明马上要执行这个关单Task了,取出订单号执行关单(可以用单独的线程来执行Task),并把这个订单信息从Set中删除即可。
(1)无需再轮询全部订单,效率高
(2)一个订单,任务只执行一次
(3)时效性好,精确到秒(控制timer移动频率可以控制精度)


五、redis过期监听


1.修改redis.windows.conf配置文件中notify-keyspace-events的值
默认配置notify-keyspace-events的值为 ""
修改为 notify-keyspace-events Ex 这样便开启了过期事件


2. 创建配置类RedisListenerConfig(配置RedisMessageListenerContainer这个Bean)


package com.zjt.shop.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisListenerConfig {

@Autowired
private RedisTemplate redisTemplate;

/**
* @return
*/
@Bean
public RedisTemplate redisTemplateInit() {

// key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());

//val实例化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

return redisTemplate;
}

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}

}


3.继承KeyExpirationEventMessageListener创建redis过期事件的监听类


package com.zjt.shop.common.util;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.zjt.shop.modules.order.service.OrderInfoService;
import com.zjt.shop.modules.product.entity.OrderInfoEntity;
import com.zjt.shop.modules.product.mapper.OrderInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

@Autowired
private OrderInfoMapper orderInfoMapper;

/**
* 针对redis数据失效事件,进行数据处理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
try {
String key = message.toString();
//从失效key中筛选代表订单失效的key
if (key != null && key.startsWith("order_")) {
//截取订单号,查询订单,如果是未支付状态则为-取消订单
String orderNo = key.substring(6);
QueryWrapper<OrderInfoEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfoEntity orderInfo = orderInfoMapper.selectOne(queryWrapper);
if (orderInfo != null) {
if (orderInfo.getOrderState() == 0) { //待支付
orderInfo.setOrderState(4); //已取消
orderInfoMapper.updateById(orderInfo);
log.info("订单号为【" + orderNo + "】超时未支付-自动修改为已取消状态");
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("【修改支付订单过期状态异常】:" + e.getMessage());
}
}
}


4:测试
通过redis客户端存一个有效时间为3s的订单:


image.png


结果:


image.png


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

什么是响应式编程:以RxJava为例

RxJava思想 文章概述: 本文围绕Rx编程思想(响应式编程)进行深入细致探讨;以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;借助卡片式编程思想,对Rx编程方式进行第一次优化;借助 Java泛型对Rx编程进一步优化; ...
继续阅读 »

RxJava思想




  • 文章概述:



    • 本文围绕Rx编程思想(响应式编程)进行深入细致探讨;以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;借助卡片式编程思想,对Rx编程方式进行第一次优化;借助 Java泛型对Rx编程进一步优化;




Rx编程出现背景:改变思维来提升效率




  • 通过事件流动,推进业务执行




    • 从起点到终点,逻辑严密



      • 下一层依赖上一层:体现在函数参数




    • 链式调用只是里面的一环




    • 样例:每一层逻辑上关联



      • 起点(分发事件:点击登录)----------登录API-------请求服务器--------获取响应码----------> 终点(更新UI登录成功 消费事件)






RxJava 配合 Retrofit




  • 业务逻辑:



    • Retrofit通过OKHHTTP请求服务器拿到响应码,交给RxJava由RxJava处理数据




  • 防抖:



    • 一秒钟点击了20次,只响应一次




  • 网络嵌套:拿到主数据再拿到item数据




  • doNext运用:异步与主线之间频繁切换



    • 异步线程A拿到数据,切换至UI线程更新,再次切换到异步线程B,再拿到UI线程




对比说明Rx 编程优势:统一业务代码逻辑



  • 主要内容:以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;


传统模式获取图片




  • 实现效果:


    image-20220621192036883




  • 传统编写思路:




    • 弹出加载框




    • 开启异步线程:此时有多种途径



      • 封装方法....

      • 全部写在一起

      • new Thread

      • 使用 线程池




    • 将从服务器获取的图片转成Bitmap




    • 从异步线程切换至UI线程更新UI




    • 代码实现:


       public void downloadImageAction(View view) {
           progressDialog = new ProgressDialog(this);
           progressDialog.setTitle("下载图片中...");
           progressDialog.show();
       
           //       异步线程处理耗时任务
           new Thread(new Runnable() {
               @Override
               public void run() {
                   try {
                       URL url = new URL(PATH);
                       HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                       httpURLConnection.setConnectTimeout(5000);
                       int responseCode = httpURLConnection.getResponseCode(); // 才开始 request
                       if (responseCode == HttpURLConnection.HTTP_OK) {
                           InputStream inputStream = httpURLConnection.getInputStream();
                           //                       图片丢给bitmap
                           Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                           //                       使用Handler 进行切换
                           Message message = handler.obtainMessage();
                           message.obj = bitmap;
                           handler.sendMessage(message);
                      }
                  } catch (Exception e) {
                       e.printStackTrace();
                  }
              }
          }).start();
       }
       
       //   使用Handler处理问题
       private final Handler handler = new Handler(new Handler.Callback() {
       
           @Override
           public boolean handleMessage(@NonNull Message msg) {
               Bitmap bitmap = (Bitmap) msg.obj;
               image.setImageBitmap(bitmap);
       
               if (progressDialog != null) progressDialog.dismiss();
               return false;
          }
       });








  • 传统方式弊端:




    • 在具体实现(切换线程)时,因为思维不统一,导致实现方式不同






RxJava思路:采用观察者设计模式,实现响应式(Rx)编程




  • 以事件流动推进业务执行




  • 角色:




    • 起点:被观察者(为其分配异步线程--->请求服务器)


       // 起点
       Observable.just(PATH)  // 内部会分发 PATH Stirng // TODO 第二步



    • 终点:观察者(为其分配UI线程--->更新UI)


       //终点
       .subscribe(
           new Observer<Bitmap>() {
               //订阅
               @Override
               public void onSubscribe(Disposable d) {
                 
              }
               //拿到事件:因为上一层是一个String类型的Path事件
               @Override
               public void onNext(@NonNull Bitmap bitmap) {
                   image.setImageBitmap(bitmap);
              }
       
               // 错误事件
               @Override
               public void onError(Throwable e) {
       
              }
       
               // 完成事件
               @Override
               public void onComplete() {
               
              }
          });





  • 编写思路:框架在实际使用中是U型逻辑(终点--->起点--->终点--->……)




    • 第一步:处理终点中拿到事件后的业务逻辑


       //拿到事件:因为上一层是一个String类型的Path事件
       @Override
       public void onNext(@NonNull String s) {
           image.setImageBitmap(bitmap);
       }



      • 细节:onNext的参数问题




        • Rx 整体是以事件流动推进业务逻辑,如果上一层是String类型的事件(Path)那么它的下一层应该也是String类型的事件(参数为String类型)




        • 但Rx 中根据业务进行事件的拦截



          • A层(String事件),B层(Bitmap事件),逻辑为A层--->B层

          • 那么就需要在A层到B层之间添加一个拦截器,进行事件转换








    • 第二步:在起点与终点之间添加拦截器




      • 为什么要添加拦截器:业务需求是拿到一个Bitmap而起点提供的是String类型的事件




      • 拦截器为map(K,V):K为上层事件,V为下层事件


         //上层事件为String类型,由系统自动推断;但此时拦截器并不知道下一层是什么事件,因此为Object
         .map(new Function<String, Object>() {
         })






      • 终点要求Bitmap事件


         //根据业务将map 中的value改为 Bitmap类型
         .map(new Function<String, Object>() {
         })



        • 终点完成事件(onNext报错,联动变化):注意由Rx思想决定,那么终点处的完成事件参数因为Bitmap


           //由Rx思想决定,那么终点处的完成事件参数因为Bitmap
           @Override
           public void onNext(@NonNull Bitmap bitmap) {
           image.setImageBitmap(bitmap);
           }



        • 整体事件流向:


          image-20220622223332983








    • 第三步:在拦截器内添加网络请求


       @Override
       public Bitmap apply(@NonNull String s) throws Exception {
       
           //处理网络请求:将String类型的Path事件处理为Bitmap实例
           URL url = new URL(PATH);
           HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
           int responseCode = httpURLConnection.getResponseCode();
           if(responseCode == httpURLConnection.HTTP_OK){
               InputStream inputStream = httpURLConnection.getInputStream();
               Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
               return bitmap;
          }
           return null;
       }


      • 此时不要使用Handler,因为拦截器已经将String类型事件转为Bitmap类型了,将Bitmap流向终点进行显示




    • 第四步:分配线程




      • 起点到此时拦截器结束,应当分配异步线程(因为需要请求服务器)


         //给上边代码分配异步线程,用于请求服务器
         .subscribeOn(Schedulers.io())



      • 拦截器结束位置到终点处,应当分配UI主线程(因为需要更新UI)


         //给下边的代码分配主线程,用于更新UI
         .observeOn(AndroidSchedulers.mainThread())



        • 分配的主线程跟下面这个是一样的


           // Thread.currentThread().getName(); == Android的主线程,这个跟RxJava切的android主线程是一样的







    • 到此基础功能已经实现,为了使得用户友好,需要添加下列步骤






Rx 代码优化(一):卡片式编程




  • 什么是卡片式编程:



    • 因为Rx 响应式编程是依靠事件流动推进业务执行,那么我们可以在起点与终点之间添加卡片(拦截器)实现具体的业务功能




  • 代码扩展:点击按钮后立即加载对话框,拿到图片并更新,随后关闭对话框




    • 整体流程:




      • 预处理:点击按钮后,立即加载对话框,开始准备事件分发


         //在终点订阅开始处加载对话框(预处理操作)
         // 订阅开始:一订阅就要显示对话框
         @Override
         public void onSubscribe(Disposable d) {
             //                               第一步:事件分发前预准备
             progressDialog = new ProgressDialog(Test.this);
             progressDialog.setTitle("开始下载");
             progressDialog.show();
         }



      • 第一步:回到起点,开始分发事件


         Observable.just(PATH)



      • 第二步:拦截器工作将String事件转为Bitmap事件(附带网络请求,从服务器拿到数据)




      • 第三步:抵达终点拿到事件处,更新UI


         //拿到事件:因为上一层是一个String类型的Path事件
         @Override
         public void onNext(@NonNull Bitmap bitmap) {
             image.setImageBitmap(bitmap);
         }






      • 第四步:抵达终点完成事件完成处,此时事件整体结束(Rx 编程结束尾巴)


         // 完成事件
         @Override
         public void onComplete() {
             //如果不为空那么就隐藏起来
             if (progressDialog != null)
             progressDialog.dismiss();
         }










  • 这种编程方式成为卡片式编程




    • 好处:后期若需要添加功能,仅需在起点与重点之间添加对应的拦截器,在其中进行处理即可




    • 图片示例:一开始的



      • 事件流动顺序


      image-20220622223332983




      • 运行结果:


        image-20220622231216269









    • 图片示例:此时需要添加个需求,将下载下来的图片添加水印后再展示




      • 事件流动顺序


        image-20220622225848759




      • 添加代码:图片上绘制文字 加水印


         // 图片上绘制文字 加水印
         private final Bitmap drawTextToBitmap(Bitmap bitmap, String text, Paint paint, int paddingLeft, int paddingTop) {
             Bitmap.Config bitmapConfig = bitmap.getConfig();
         
             paint.setDither(true); // 获取跟清晰的图像采样
             paint.setFilterBitmap(true);// 过滤一些
             if (bitmapConfig == null) {
                 bitmapConfig = Bitmap.Config.ARGB_8888;
            }
             bitmap = bitmap.copy(bitmapConfig, true);
             Canvas canvas = new Canvas(bitmap);
         
             canvas.drawText(text, paddingLeft, paddingTop, paint);
             return bitmap;
         }



      • 添加代码:在前面一个拦截器后添加


         .map(new Function<Bitmap, Bitmap>() {
             @Override
             public Bitmap apply(@NonNull Bitmap bitmap) throws Exception {
                 //开始添加水印
                 Paint paint = new Paint();
                 paint.setTextSize(88);
                 paint.setColor(Color.GREEN);
                 return drawTextToBitmap(bitmap,"水印",paint,88,88);
            }
         })



      • 运行结果:从服务器获取图片并添加水印


        image-20220622231334525






    • 还可以添加:及时记录日志等功能






Rx 代码优化(二):封装代码部分功能提升程序结构



  • 封装线程分配


 //为上游(起点到拦截器结束)分配异步线程,为下游(拦截器结束位置到终点结束)分配android主线程
 private final static <UD> ObservableTransformer<UD,UD> opMixed(){
     return new ObservableTransformer<UD, UD>() {
         @NonNull
         @Override
         //分配线程
         public ObservableSource<UD> apply(@NonNull Observable<UD> upstream) {
             return upstream.subscribeOn(Schedulers.io()).
             observeOn(AndroidSchedulers.mainThread())
             //继续链式调用
            .map(new Function<UD, UD>() {
                 @Override
                 public UD apply(@NonNull UD ud) throws Exception {
                     Log.d(TAG,"日志记录")
                     return ud;
                }
            })
 
             //还可以加卡片(拦截器)
            ;
 
        }
 
    };
 }


  • 仅需在终点前调用封装好的库就行了


 ……
 //是需要在终点前调用封装好的东西就行了
 .compose(opMixed())
     //终点
    .subscribe(

Rx 编程完整代码:


 package com.xiangxue.rxjavademo.downloadimg;
 
 import android.app.ProgressDialog;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.util.Log;
 import android.view.View;
 import android.widget.ImageView;
 
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AppCompatActivity;
 
 import com.xiangxue.rxjavademo.R;
 
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
 
 import io.reactivex.Observable;
 import io.reactivex.ObservableSource;
 import io.reactivex.ObservableTransformer;
 import io.reactivex.Observer;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.functions.Function;
 import io.reactivex.schedulers.Schedulers;
 
 public class Test extends AppCompatActivity {
 
     // 网络图片的链接地址,String类型的Path事件
     private final static String PATH = "http://pic1.win4000.com/wallpaper/c/53cdd1f7c1f21.jpg";
 
     // 弹出加载框
     private ProgressDialog progressDialog;
 
     // ImageView控件,用来显示结果图像
     private ImageView image;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_download);
         image = findViewById(R.id.image);
 
         // Thread.currentThread().getName(); == Android的主线程,这个跟RxJava切的android主线程是一样的
    }
 
     // 通过订阅将 起点 和 终点 关联起来
     public void rxJavaDownloadImageAction(View view) {
 
         // 起点
         Observable.just(PATH)  // 内部会分发 PATH Stirng // TODO 第二步
         //流程中的卡片
        .map(new Function<String, Bitmap>() {
             @Override
             public Bitmap apply(@NonNull String s) throws Exception {
 
                 //处理网络请求:将String类型的Path事件处理为Bitmap实例
                 URL url = new URL(PATH);
                 HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                 int responseCode = httpURLConnection.getResponseCode();
                 if(responseCode == httpURLConnection.HTTP_OK){
                     InputStream inputStream = httpURLConnection.getInputStream();
                     Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                     return bitmap;
                }
                 return null;
            }
        })
         //给上边代码分配异步线程,用于请求服务器
        .subscribeOn(Schedulers.io())
         //给下边的代码分配主线程,用于更新UI
        .observeOn(AndroidSchedulers.mainThread())
 
         //终点
        .subscribe(
             new Observer<Bitmap>() {
 
                 // 订阅开始:一订阅就要显示对话框
                 @Override
                 public void onSubscribe(Disposable d) {
                     //                               第一步:事件分发前预准备
                     progressDialog = new ProgressDialog(Test.this);
                     progressDialog.setTitle("开始下载");
                     progressDialog.show();
                }
                 //拿到事件:因为上一层是一个String类型的Path事件
                 @Override
                 public void onNext(@NonNull Bitmap bitmap) {
                     image.setImageBitmap(bitmap);
                }
 
                 // 错误事件
                 @Override
                 public void onError(Throwable e) {
 
                }
 
                 // 完成事件
                 @Override
                 public void onComplete() {
                     //如果不为空那么就隐藏起来
                     if (progressDialog != null)
                     progressDialog.dismiss();
                }
            });
 
    }
 
 
     // 图片上绘制文字 加水印
     private final Bitmap drawTextToBitmap(Bitmap bitmap, String text, Paint paint, int paddingLeft, int paddingTop) {
         Bitmap.Config bitmapConfig = bitmap.getConfig();
 
         paint.setDither(true); // 获取跟清晰的图像采样
         paint.setFilterBitmap(true);// 过滤一些
         if (bitmapConfig == null) {
             bitmapConfig = Bitmap.Config.ARGB_8888;
        }
         bitmap = bitmap.copy(bitmapConfig, true);
         Canvas canvas = new Canvas(bitmap);
 
         canvas.drawText(text, paddingLeft, paddingTop, paint);
         return bitmap;
    }
 }

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

【面试黑洞】Android 的键值对存储有没有最优解?

正文 这是我在网上找到的一份 Android 键值对存储方案的性能测试对比(数越小越好): 可以看出,DataStore 的性能比 MMKV 差了一大截。MMKV 是腾讯在 2018 年推出的,而 DataStore 是 Android 官方在 2020 年...
继续阅读 »

正文


这是我在网上找到的一份 Android 键值对存储方案的性能测试对比(数越小越好):



可以看出,DataStore 的性能比 MMKV 差了一大截。MMKV 是腾讯在 2018 年推出的,而 DataStore 是 Android 官方在 2020 年推出的,并且它的正式版在 2021 年 8 月才发布。一个官方发布的、更(gèng)新的库,性能竟然比不过比它早两年发布的、第三方的库。而且我们能看到,更离谱的是,它甚至还比不过 SharedPreferences 。Android 官方当初之所以推出 DataStore,就是要替代掉 SharedPreferences,并且主要原因之一就是 SharedPreferences 有性能问题,可是测试结果却是它的性能不如 SharedPreferences。


所以,这到底是为什么?


啊,我知道了——因为 Google 是傻逼!


SharedPreferences:不知不觉被嫌弃


大家好,我是扔物线朱凯。


键值对的存储在移动开发里非常常见。比如深色模式的开关、软件语言、字体大小,这些用户偏好设置,很适合用键值对来存。而键值对的存储方案,最传统也最广为人知的就是 Android 自带的 SharedPreferences。它里面的 -Preferences,就是偏好设置的意思,从名字也能看出它最初的定位。


SharedPreferences 使用起来很简单,也没什么问题,大家就这么用了很多年。——但!渐渐地,有人发现它有一个问题:卡顿,甚至有时候会出现 ANR。


MMKV:好快!


怎么办?换!2018 年 9 月,腾讯开源了一个叫做 MMKV 的项目。它和 SharedPreferences 一样,都是做键值对存储的,可是它的性能比 SharedPreferences 强很多。真的是强,很,多。在 MMKV 推出之后,很多团队就把键值对存储方案从 SharedPreferences 换到了 MMKV。


DataStore:官方造垃圾?


再然后,就是又过了两年,Google 自己也表示受不了 SharedPreferences 了,Android 团队公布了 Jetpack 的新库:DataStore,目标直指 SharedPreferences,声称它就是 Android 官方给出的 SharedPreferences 的替代品。


替代的理由,Android 团队列了好几条,但不出大家意料地,「性能」是其中之一:


也就是说,Android 团队直接抛弃了 SharedPreferences,换了个新东西来提供更优的性能。


但是,问题随之就出现了:大家一测试,发现这 DataStore 的性能并不强啊?跟 MMKV 比起来差远了啊?要知道,MMKV 的发布是比 DataStore 早两年的。DataStore 比人家晚两年发布,可是性能却比人家差一大截?甚至,从测试数据来看,它连要被它替代掉的 SharedPreferences 都比不过。这么弱?那它搞个毛啊!


Android 团队吭哧吭哧搞个新东西出来,竟然还没有市场上两年前就出现的东西强?这是为啥?


首先,肯定得排除「DataStore 是垃圾」这个可能性。虽然这猛一看、粗一想,明显就是 DataStore 垃圾、Google 傻逼,但是你仔细想想,这可能吗?


那如果不是的话,又是因为什么?——因为你被骗了。


MMKV 的一二三四


被谁骗了?不是被 MMKV 骗了,也不是具体的某个人。事情其实是这样的:


大家知道 MMKV 当初为什么会被创造出来吗?其实不是为了取代 SharedPreferences。


最早是因为微信的一个需求(来源:MMKV 组件现在开源了):


微信作为一个全民的聊天 App,对话内容中的特殊字符所导致的程序崩溃是一类很常见、也很需要快速解决的问题;而哪些字符会导致程序崩溃,是无法预知的,只能等用户手机上的微信崩溃之后,再利用类似时光倒流的回溯行为,看看上次软件崩溃的最后一瞬间,用户收到或者发出了什么消息,再用这些消息中的文字去尝试复现发生过的崩溃,最终试出有问题的字符,然后针对性解决。
1.gif
那么这个「时光倒流」应该怎么做,就成了问题的关键。我们要知道,程序中的所有变量都是存活在内存里的,一旦程序崩溃,所有变量全都灰飞烟灭。
2.gif
所以要想实现「时光倒流」,就需要把想回溯的时光预先记录下来。说人话就是,我们需要把界面里显示的文字写到手机磁盘里,才能在程序崩溃、重新启动之后,通过读取文件的方式来查看。
3.gif
更麻烦的是,这种记录的目标是用来回溯查找「导致程序崩溃的那段文字」,而同时,正是因为没有人知道哪段文字会导致程序崩溃才去做的记录,这就要求每一段文字都需要先写入磁盘、然后再去显示,这样才能保证程序崩溃的时候那段导致崩溃的文字一定已经被记录到了磁盘。
4.gif
对吧?


这就有点难了。


我们来想象一下实际场景:



  • 如果用户的微信现在处于一个对话界面中,这时候来了一条新的消息,这条消息里可能会包含微信处理不了的字符,导致微信的崩溃。


5.gif



  • 而微信为了及时地找出导致崩溃的字符或者字符串,所以给程序增加了逻辑:所有的对话内容在显示之前,先保存到磁盘再显示:


val bubble: WxTextView = ...
recordTextToDisk(text) // 显示之前,先保存到磁盘
bubble.setText(text)


  • 那么你想一下,这个「保存到磁盘」的行为,我应该做成同步的还是异步的?
    6.gif

    • 为了不卡主线程,我显然应该做成异步的;

    • 但这是马上就要显示的文字,如果做成异步的,就极有可能在程序崩溃的时候,后台线程还没来得及把文字存到磁盘。这样的话,就无法进行回溯,从而这种记录也就失去了价值。


    7.gif

    • 所以从可用性的角度来看,我只能选择放弃性能,把它做成同步的,也就是在主线程进行磁盘的写操作。


    8.gif

    • 一次磁盘的写操作,花个一两毫秒是很正常的,三五毫秒甚至超过 10 毫秒也都是有可能的。具体的方案可以选择 SharedPreferences,也可以选择数据库,但不管选哪个,只要在主线程去完成这个写操作,这种耗时就绝对无法避免。一帧的时间也就 16 毫秒而已——那时候还没有高刷,我们就先不谈高刷了,一帧就是 16 毫秒——16 毫秒里来个写磁盘的操作,用户很可能就会感受到一次卡顿。



  • 这还是相对比较好的情况。我们再想一下,如果用户点开了一个活跃的群,这个群里有几百条没看过的消息:

    • 那么在他点开的一瞬间,是不是界面中会显示出好几条消息气泡?这几条消息的内容,哪些需要记录到磁盘?全都要记录的,因为谁也知道哪一条会导致微信的崩溃,任何一条都是可能的。

    • 而如果把这几条消息都记录下来,是不是每条消息的记录都会涉及一次写磁盘的操作?这几次写磁盘行为,是发生在同一帧里的,所以在这一帧里因为记录文字而导致的主线程耗时,也会相比起刚才的例子翻上好几倍,卡顿时间就同样也会翻上好几倍。



  • 还有更差的情况。如果用户看完这一页之后,决定翻翻聊天记录,看看大家之前都聊了什么:

    • 这时候,是不是上方每一个新的聊天气泡的出现,都会涉及一次主线程上的写磁盘行为?

    • 而如果用户把手猛地往下一滑,让上面的几十条消息依次滑动显示出来,这是不是就会导致一次爆发性的、集中式的对磁盘的写入?

    • 用户的手机,一定会卡爆。




所以这种「高频、同步写入磁盘」的需求,让所有的现有方案都变得不可行了:不管你是用 SharedPreferences 还是用数据库还是别的什么,只要你在主线程同步写入磁盘,就一定会卡,而且是很卡。


但是微信还是有高手,还是有能想办法的人,最终微信找到了解决方案。他们没有用任何的现成方案,而是使用了一种叫做内存映射(mmap())的底层方法。
CleanShot 2022-05-31 at <a href=15.18.23@2x.png" loading="lazy">
它可以让系统为你指定的文件开辟一块专用的内存,这块内存和文件之间是自动映射、自动同步的关系,你对文件的改动会自动写到这块内存里,对这块内存的改动也会自动写到文件里。
9.gif
更多更深的原理,说实话我也不是看得很懂,就不跟大家装了。但关键是,有了这一层内存作为中间人,我们就可以用「写入内存」的方式来实现「写入磁盘」的目标了。内存的速度多快呀,耗时几乎可以忽略,这样一下子就把写磁盘造成卡顿的问题解决了。
11.gif
而且这个内存映射还有一点很方便的是,虽然这块映射的内存不是实时向对应的文件写入新数据,但是它在程序崩溃的时候,并不会随着进程一起被销毁掉,而是会继续有条不紊地把它里面还没同步完的内容同步到它所映射的文件里面去。
12.gif
至于更下层的原理,我也说了,没看懂,你也别问我。


总之,有了这些特性,内存映射就可以让程序用往内存里写数据的速度实现往磁盘里写数据的实际效果,这样的话,「高频、同步写入磁盘」的需求就完美满足了。不管是用户打开新的聊天页面,还是滑动聊天记录来查看聊天历史,用内存映射的方式都可以既实时写入所有即将被渲染的文字,又不会造成界面的卡顿。这种性能,是 SharedPreferences 和数据库都做不到的——顺便提一句,虽然我总在提 SharedPreferences,但其实这种做法本来是先在 iOS 版的微信里应用的,后来才移植到了 Android 版微信。这也是我刚才说的,MMKV 的诞生并不是为了取代 SharedPreferences。


再后来,就是 2018 年,微信把这个叫做 MMKV 的项目开源了。它的名字,我猜就是直白的「Memory-Map based Key-Value(方案)」,基于内存映射的键值对。不过没有找作者求证,如果说错了欢迎指正。


在 MMKV 开源之后,很多团队就把键值对存储方案从 SharedPreferences 迁移到了 MMKV。为什么?因为它快呀。


MMKV 并不总是快如闪电


不过……事情其实没那么简单。MMKV 虽然大的定位方向和 SharedPreferences 一样,都是对于键值对的存储,但它并不是一个全方位更优的方案。


比如性能。我前面一直在说 MMKV 的性能更强,对吧?但事实上,它并不是任何时候都更强。由于内存映射这种方案是自行管理一块独立的内存,所以它在尺寸的伸缩上面就比较受限,这就导致它在写大一点的数据的时候,速度会慢,而且可能会很慢。我做了一份测试:
13.gif
在连续 1000 次写入 Int 值的场景中,SharedPreferences 的耗时是 1034 毫秒,也就是 1 秒多一点;而 MMKV 只有 2 毫秒,简直快得离谱;而且最离谱的是,Android 官方最新推出的 DataStore 是 1215 毫秒,竟然比 SharedPreferences 还慢。这个前面我也提过,别人的测试也是这样的结果。


可是,SharedPreferences 是有异步 API 的,而 DataStore 是基于协程的。这就意味着,它们实际占用主线程的时间是可以低于这份测试出的时间的,而界面的流畅在意的正是主线程的时间消耗。所以如果我统计的不是全部的耗时,而是主线程的耗时,那么统计出的 SharedPreferencesDataStore 的耗时将会大幅缩减:
14.gif
还是比 MMKV 慢很多,是吧?但是这是对于 Int类型的高频写入,Int 数据是很小的。而如果我把写入的内容换成长字符串,再做一次测试:
15.gif
MMKV 就不具备优势了,反而成了耗时最久的;而这时候的冠军就成了 DataStore,并且是遥遥领先。这也就是我在开头说的:你可能被骗了。被谁骗了?被「耗时」这个词:我们关注性能,考量的当然是耗时,但要明确:是主线程的耗时。所以视频开头的那张图,是不具备任何参考意义的。
CleanShot 2022-06-22 at <a href=20.52.01@2x.png" loading="lazy">


但其实,它们都够快了


不过在换成了这种只看主线程的耗时的对比方案之后,我们会发现谁是冠军其实并不是很重要,因为从最终的数据来看,三种方案都不是很慢。虽然这半秒左右的主线程耗时看起来很可怕,但是要知道这是 1000 次连续写入的耗时,而我们在真正写程序的时候,怎么会一次性做 1000 次的长字符串的写入?所以真正在项目中的键值对写入的耗时,不管你选哪个方案,都会比这份测试结果的耗时少得多的,都少到了可以忽略的程度,这是关键。


各自的优势和弱点


那……既然它们的耗时都少到了可以忽略,不就是选谁都行?那倒不是。


MMKV 优势:写速度极快


我们来看一个 MMKV 官方给出的数据对比图:
image.png
从这张图看来,SharedPreferences 的耗时是 MMKV 的接近 60 倍。很明显,如果 SharedPreferences 用异步的 API 也就是 apply() 来保存的话,是不可能有这么差的性能的,这个一定是使用同步的 commit() 的性能来做的对比。那么为什么 MMKV 官方会这样做对比呢?这个又要说到它的诞生场景了:MMKV 最初的功能是在文字显示之前先把它记录到磁盘,然后如果接下来这个文字显示失败导致程序崩溃,稍后就可以从磁盘里把这段文字恢复出来,进行分析。而刚才我也说过,这种场景的特殊性在于,导致程序崩溃的文字往往是刚刚被记录下来,程序就崩溃了,所以如果采用异步处理的方案,就很有可能在文字还没来得及真正存储到磁盘的时候程序就发生了崩溃,那就没办法把它恢复出来进行分析了。因此这样的场景,是不能接受异步处理的方案的,只能同步进行。所以 MMKV 在意的,就是同步处理机制下的耗时,它不在意异步,因为它不接受异步。


而在同步处理的机制下,MMKV 的性能优势就太明显了。原因上面说过了,它写入内存就几乎等于写入了磁盘,所以速度巨快无比。这就是 MMKV 的优势之一:极高的同步写入磁盘的性能。


另外 MMKV 还有个特点是,它的更新并不像 SharedPreferences 那样全量重新写入磁盘,而是只把要更新的键值对写入,也就是所谓的增量式更新。这也会给它带来一些性能优势,不过这个优势并不算太核心,因为 SharedPreferences 虽然是全量更新的模式,但只要把保存的数据用合适的逻辑拆分到多个不同的文件里,全量更新并不会对性能造成太大的拖累。所以这个性能优势虽然有,但并不是关键。


还有刚才提到的,对于大字符串的场景,MMKV 的写入性能并不算快,甚至在我们的测试结果里是最慢的,对吧?这一点算是劣势。但是实事求是地说,我们在开发里不太可能连续不断地去写入大字符串吧?所以这个性能劣势虽然有,但也并不是关键。


整体来说,MMKV 比起 SharedPreferences 和 DataStore 来说,在写入小数据的情况下,具有很高的写入性能,这就让高频写入的场景非常适合使用 MMKV 来处理。因此如果你的项目里也有像微信的崩溃回溯的这种高频写入的需求,MMKV 就很可能是你的最佳方案。而如果你除了「高频写入」,还和微信一样要求「同步写入」,那 MMKV 就可能是你的唯一选择了。不过,如果你真的主要是存储大字符串的——例如你写的是一个文本编辑软件,需要保存的总是大块的文本——那么用 MMKV 不一定会更快了,甚至可能会比较慢。


MMKV 优势:支持多进程


另外,MMKV 还有一个巨大的优势:它支持多进程。


行业内也有很多公司选用 MMKV 并不是因为它快,而是因为它支持多进程。SharedPreferences 是不支持多进程的,DataStore 也不支持——从 DataStore 提交的代码来看,它已经在加入多进程的支持了,但目前还没有实现。所以如果你们公司的 App 是需要在多个进程里访问键值对数据,那么 MMKV 是你唯一的选择。


MMKV 劣势:丢数据


除了速度快和支持多进程这两个优势之外,MMKV 也有一个弱点:它会丢数据。


任何的操作系统、任何的软件,在往磁盘写数据的过程中如果发生了意外——例如程序崩溃,或者断电关机——磁盘里的文件就会以这种写了一半的、不完整的形式被保留。写了一半的数据怎么用啊?没法用,这就是文件的损坏。这种问题是不可能避免的,MMKV 虽然由于底层机制的原因,在程序崩溃的时候不会影响数据往磁盘的写入,但断电关机之类的操作系统级别的崩溃,MMKV 就没办法了,文件照样会损坏。对于这种文件损坏,SharedPreferences 和 DataStore 的应对方式是在每次写入新数据之前都对现有文件做一次自动备份,这样在发生了意外出现了文件损坏之后,它们就会把备份的数据恢复过来;而 MMKV,没有这种自动的备份和恢复,那么当文件发生了损坏,数据就丢了,之前保存的各种信息只能被重置。也就是说,MMKV 是唯一会丢数据的方案。


可能会有人好奇,为什么 MMKV 不做全自动的备份和恢复。我的猜测是这样的:MMKV 底层的原理是内存映射,而内存映射这种方式,它从内存往磁盘里同步写入的过程并不是实时的,也就是说并不是每次我们写入到映射的内存里就会立即从这块内存写入到磁盘,而是会有一些滞后。而如果我们要做全自动的备份,那就需要每次往内存里写入之后,立即手动把内存里最新的数据同步到磁盘。但这就和 MMKV 的定位不符了:因为这种「同步」本质上就是一次从内存到磁盘的写入,并且是同步的写入;而 MMKV 是要高频写入的,如果在高频写入内存的同时,还要实时地把数据从内存同步到磁盘,就会一下子把写入速度从内存级别下降到磁盘级别,MMKV 的性能优势也就荡然无存了。所以从原理上,自动备份是个很难实现的需求,因为它和 MMKV 的定位是矛盾的。不过正好 MMKV 所要记录的这些要显示的文字,也并不是不能丢失的内容——真要是丢了就丢了呗,反正是崩溃日志,丢了就不要了,我下次启动程序之后继续记录就是了——所以既然要求必须高频写入而导致很难实现自动备份,并且也确实能接受因为不做自动备份而导致的数据损坏,那就干脆不做自动备份了。不过这也是我猜的啊,大家如果有不同意见欢迎留言评论指正。


所以如果你要用 MMKV,一定要记得只能用它来存可以接受丢失、不那么重要的数据。或者你也可以选择对数据进行定期的手动备份——全自动的实时备份应该是会严重影响性能的,不过我没试过,你如果有兴趣可以试试。另外据我所知,国内在使用 MMKV 的团队里,几乎没有对 MMKV 数据做了备份和恢复的处理的。


那么说到这里,很容易引出一个问题:微信自己就不怕丢数据吗?(大字:微信就不怕丢数据?)关于这一点,我相信,微信绝对不会把用户登录状态相关的信息用 MMKV 保存并且不做任何的备份,因为这一定会导致每天都会有一些用户在新一次打开微信的时候发现自己登出了。这会是非常差的用户体验,所以微信一定不会让这种事发生。至于一些简单的用户设置,那我就不清楚了。比如深色主题重要吗?这是个不好说的事情:某个用户在打开软件的时候,发现自己之前设置的深色主题失效了,软件突然变回了亮色方案,这肯定是不舒服的事;但我们要知道,MMKV 的文件损坏终归是个概率极低的事件,所以偶尔地发生一次这样的事件在产品的角度是否可以接受,那可能是需要产品团队自身做一个综合考量的事了。对于不同的产品和团队,也许不可接受,也许无伤大雅。而对于你所开发的产品应该是怎样的判断,就得各位自己和团队去商量了。所以像深色主题这种「可以重要也可以不重要」的信息,用不用 MMKV 保存、用的时候做不做备份,大家需要自己去判断。


总之,大家要知道这件事:MMKV 是有数据损坏的概率的,这个在 MMKV 的官方文档就有说明:MMKV 的 GitHub wiki 页面显示,微信的 iOS 版平均每天有 70 万次的数据校验不通过(即数据损坏)。这还是 2020 年的数据,现在可能会更多。
CleanShot 2022-05-31 at 15.46.20.png
所以我们在使用 MMKV 的时候,一定要考虑到这个问题,你要知道这件事。至于具体的应对,是接受它、坏就坏了,还是要认真应对、做好备份和恢复,这就是大家自己的决策了。


SharedPreferences 的优势:不丢数据


好,那么说完了 MMKV,我来说一下 SharedPreferences,这个最传统的方案。


它有什么优势呢?——它没有优势。跟 MMKV 比起来,它不会丢数据,这个倒是它比 MMKV 强的地方,但是我觉得更应该归为 MMKV 的劣势,而不是 SharedPreferences 的优势,因为只有 MMKV 会丢数据嘛,是吧?


不过不管是这个的优势还是那个的劣势,如果你不希望丢数据,并且也不想花时间去做手动的备份和恢复,同时对于 MMKV 的超高写入性能以及多进程支持都没有需求,那你其实更应该选择 SharedPreferences,而不是 MMKV。对吧?


SharedPreferences 的劣势:卡顿


但更进一步地说:如果你选择了 SharedPreferences,那么你更应该考虑 DataStore。因为 DataStore 是一个完全超越了 SharedPreferences 的存在。你看 SharedPreferences 和 MMKV 它俩是各有优劣对吧?虽然 MMKV 几乎完胜,但是毕竟 SharedPreferences 不会丢数据呀,所以它俩是各有优劣的。但当 DataStore 和 SharedPreferences 比起来,那就是 DataStore 完胜了。这其实也很合理,因为 DataStore 被创造出来,就是用于替代掉 SharedPreferences 的;而 MMKV 不一样,它的诞生有它独特的使命,它是为了「高频同步写入」而诞生的,所以不能全角度胜过 SharedPreferences 也很正常。


我们还说回 DataStore。DataStore 被创造出来的目标就是替代 SharedPreferences,而它解决的 SharedPreferences 最大的问题有两点:一是性能问题,二是回调问题。


先说性能问题:SharedPreferences 虽然可以用异步的方式来保存更改,以此来避免 I/O 操作所导致的主线程的耗时;但在 Activity 启动和关闭的时候,Activity 会等待这些异步提交完成保存之后再继续,这就相当于把异步操作转换成同步操作了,从而会导致卡顿甚至 ANR(程序未响应)。这是为了保证数据的一致性而不得不做的决定,但它也确实成为了 SharedPreferences 的一个弱点。而 MMKV 和 DataStore 用不同的方式各自都解决了这个问题——事实上,当初 MMKV 被公布的时候之所以在业界有相当大的反应,就是因为它解决了 SharedPreferences 的卡顿和 ANR 的问题。


不过有一点我的观点可能和一些人不同:SharedPreferences 所导致的卡顿和 ANR,其实并不是个很大的问题。它和 MMKV 的数据损坏一样,都是非常低概率的事件。它俩最大的区别在于其实是政治上的:SharedPreferences 的卡顿很容易被大公司的性能分析后台监测到,所以不解决的话会扣绩效,而解决掉它会提升绩效;而 MMKV 的数据损坏是无法被监测到的,所以……哈?事实上,大家想一下:卡顿和数据损坏,哪个更严重?当然是数据损坏了,对吧。


其实除了写数据时的卡顿,SharedPreferences 在读取数据的时候也会卡顿。虽然它的文件加载过程是在后台进行的,但如果代码在它加载完成之前就去尝试读取键值对,线程就会被卡住,直到文件加载完成,而如果这个读取的过程发生在主线程,就会造成界面卡顿,并且数据文件越大就会越卡。这种卡顿,不是 SharedPreferences 独有的,MMKV 也是存在的,因为它初始化的过程同样也是从磁盘里读取文件,而且是一股脑把整个文件读完,所以耗时并不会比 SharedPreferences 少。而 DataStore,就没有这种问题。DataStore 不管是读文件还是写文件,都是用的协程在后台进行读写,所有的 I/O 操作都是在后台线程发生的,所以不论读还是写,都不会卡主线程。


简单来说,SharedPreferences 会有卡顿的问题,这个问题 MMKV 解决了一部分(写时的卡顿),而 DataStore 完全解决了。所以如果你的目标在于全方位的性能,那么你应该考虑的是 DataStore,因为它是唯一完全不会卡顿的。


SharedPreferences 的劣势:回调


DataStore 解决的 SharedPreferences 的另一个问题就是回调。SharedPreferences 如果使用同步方式来保存更改(commit()),会导致主线程的耗时;但如果使用异步的方式,给它加回调又很不方便,也就是如果你想做一些「等这个异步提交完成之后再怎么怎么样」的工作,会很麻烦。


而 DataStore 由于是用协程来做的,线程的切换是非常简单的,你就把「保存完成之后做什么」直接写在保存代码的下方就可以了,很直观、很简单。


对比来说,MMKV 虽然没有使用协程,但是它太快了,所以大多数时候并不需要切线程也不会卡顿。总之,在这件事上,只有 SharedPreferences 最弱。


总结


区别大概就是这么些区别了,大致总结一下就是:


如果你有多进程支持的需求,MMKV 是你唯一的选择;如果你有高频写入的需求,你也应该优先考虑 MMKV。但如果你使用 MMKV,一定要知道它是可能丢失数据的,不过概率很低就是了,所以你要在权衡之后做好决定:是自行实现数据的备份和恢复方案,还是直接接受丢数据的事实,在每次丢失数据之后帮用户把相应的数据进行初始化。当然了,一个最鸡贼的做法是:反正数据监测不会监测到 MMKV 的数据丢失,又不影响绩效,那就不管它呗!不过我个人是不太赞同这种策略的,有点不负责哈。


另外,如果你没有多进程的需求,也没有高频写入的需求,DataStore 作为性能最完美的方案,应该优先被考虑。因为它在任何时候都不会卡顿,而 MMKV 在写大字符串和初次加载文件的时候是可能会卡顿的,而且初次加载文件的卡顿不是概率性的,只要文件大到了引起卡顿的程度,就是 100% 的卡顿。不过如果你的团队没有在用协程,甚至没有在用 Kotlin,那 DataStore 也暂时不适合你们,因为它是完全依赖 Kotlin 协程来实现和使用的。


哦对了,其实我今天说的 DataStore 只是面向简单键值对存储的 DataStore 方案,它的全称叫 Preferences DataStore,而 DataStore 还有用于保存结构化数据的方案,叫做 Proto DataStore,它内部用的是 Protocol Buffer 作为数据结构的支持。但是这个有点跑题,我就不展开了。


至于 SharedPreferences 嘛,在这个时代,它真的可以被放弃了。除非——像我刚说的——如果你们还没在用协程,那 SharedPreferences 可能还能苟延残喘一下。


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

面试了十几个高级前端,竟然连(扁平数据结构转Tree)都写不出来

前言 招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。 我们看下题目:打平的数据内容如下: let arr = [ {id: 1, name: '部门1...
继续阅读 »

前言


招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。


我们看下题目:打平的数据内容如下:


let arr = [
{id: 1, name: '部门1', pid: 0},
{id: 2, name: '部门2', pid: 1},
{id: 3, name: '部门3', pid: 1},
{id: 4, name: '部门4', pid: 3},
{id: 5, name: '部门5', pid: 4},
]

输出结果


[
{
"id": 1,
"name": "部门1",
"pid": 0,
"children": [
{
"id": 2,
"name": "部门2",
"pid": 1,
"children": []
},
{
"id": 3,
"name": "部门3",
"pid": 1,
"children": [
// 结果 ,,,
]
}
]
}
]

我们的要求很简单,可以先不用考虑性能问题。实现功能即可,回头分析了面试的情况,结果使我大吃一惊。


10%的人没思路,没碰到过这种结构


60%的人说用过递归,有思路,给他个笔记本,但就是写不出来


20%的人在引导下,磕磕绊绊能写出来


剩下10%的人能写出来,但性能不是最佳


感觉不是在招聘季节遇到一个合适的人真的很难。


接下来,我们用几种方法来实现这个小算法


什么是好算法,什么是坏算法


判断一个算法的好坏,一般从执行时间占用空间来看,执行时间越短,占用的内存空间越小,那么它就是好的算法。对应的,我们常常用时间复杂度代表执行时间,空间复杂度代表占用的内存空间。


时间复杂度



时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。



随着n的不断增大,时间复杂度不断增大,算法花费时间越多。 常见的时间复杂度有



  • 常数阶O(1)

  • 对数阶O(log2 n)

  • 线性阶O(n)

  • 线性对数阶O(n log2 n)

  • 平方阶O(n^2)

  • 立方阶O(n^3)

  • k次方阶O(n^K)

  • 指数阶O(2^n)


计算方法



  1. 选取相对增长最高的项

  2. 最高项系数是都化为1

  3. 若是常数的话用O(1)表示


举个例子:如f(n)=3*n^4+3n+300 则 O(n)=n^4


通常我们计算时间复杂度都是计算最坏情况。计算时间复杂度的要注意的几个点



  • 如果算法的执行时间不随n增加增长,假如算法中有上千条语句,执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。 举例如下:代码执行100次,是一个常数,复杂度也是O(1)


    let x = 1;
while (x <100) {
x++;
}


  • 多个循环语句时候,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的方法决定的。举例如下:在下面for循环当中,外层循环每执行一次内层循环要执行n次,执行次数是根据n所决定的,时间复杂度是O(n^2)


  for (i = 0; i < n; i++){
for (j = 0; j < n; j++) {
// ...code
}
}


  • 循环不仅与n有关,还与执行循环判断条件有关。举例如下:在代码中,如果arr[i]不等于1的话,时间复杂度是O(n)。如果arr[i]等于1的话,循环不执行,时间复杂度是O(0)


    for(var i = 0; i<n && arr[i] !=1; i++) {
// ...code
}


空间复杂度



空间复杂度是对一个算法在运行过程中临时占用存储空间的大小。



计算方法:



  1. 忽略常数,用O(1)表示

  2. 递归算法的空间复杂度=(递归深度n)*(每次递归所要的辅助空间)


计算空间复杂度的简单几点



  • 仅仅只复制单个变量,空间复杂度为O(1)。举例如下:空间复杂度为O(n) = O(1)。


   let a = 1;
let b = 2;
let c = 3;
console.log('输出a,b,c', a, b, c);


  • 递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1) = O(n)。


    function fun(n) {
let k = 10;
if (n == k) {
return n;
} else {
return fun(++n)
}
}

不考虑性能实现,递归遍历查找


主要思路是提供一个递getChildren的方法,该方法递归去查找子集。
就这样,不用考虑性能,无脑去查,大多数人只知道递归,就是写不出来。。。


/**
* 递归查找,获取children
*/
const getChildren = (data, result, pid) => {
for (const item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []};
result.push(newItem);
getChildren(data, newItem.children, item.id);
}
}
}

/**
* 转换方法
*/
const arrayToTree = (data, pid) => {
const result = [];
getChildren(data, result, pid)
return result;
}


从上面的代码我们分析,该实现的时间复杂度为O(2^n)


不用递归,也能搞定


主要思路是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //

// 先转成map存储
for (const item of items) {
itemMap[item.id] = {...item, children: []}
}

for (const item of items) {
const id = item.id;
const pid = item.pid;
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n),需要一个Map把数据存储起来,空间复杂度O(n)


最优性能


主要思路也是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储。不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //
for (const item of items) {
const id = item.id;
const pid = item.pid;

if (!itemMap[id]) {
itemMap[id] = {
children: [],
}
}

itemMap[id] = {
...item,
children: itemMap[id]['children']
}

const treeItem = itemMap[id];

if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为O(n),需要一个Map把数据存储起来,空间复杂度O(n)


小试牛刀



































方法1000(条)10000(条)20000(条)50000(条)
递归实现154.596ms1.678s7.152s75.412s
不用递归,两次遍历0.793ms16.499ms45.581ms97.373ms
不用递归,一次遍历0.639ms6.397ms25.436ms44.719ms

从我们的测试结果来看,随着数量的增大,递归的实现会越来越慢,基本成指数的增长方式。


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

腾讯员工晒出薪资:真实985毕业薪资,大家看我还有救吗?网友:日薪?

敢晒薪资的程序员大多都是还不错的,虽然互联网薪资比其他行业稍微高一些,但也存在普通薪资的情况。近日,有认证为腾讯员工的网友发帖称:真实985毕业薪资,大家看我还有救吗?原贴如下:这个帖子一经发出。引起了不少网友的“舒适”。不少网友质疑的同时怀疑是日薪,单位写错...
继续阅读 »
敢晒薪资的程序员大多都是还不错的,虽然互联网薪资比其他行业稍微高一些,但也存在普通薪资的情况。近日,有认证为腾讯员工的网友发帖称:真实985毕业薪资,大家看我还有救吗?

原贴如下:


这个帖子一经发出。引起了不少网友的“舒适”。不少网友质疑的同时怀疑是日薪,单位写错了。


但也有网友说自己也是这样。


还有人调侃楼主的职业。


楼主从11年到21年,已经工作了10年,薪资从1.5K到12K,在评论区有网友表示:十年了,兄嘚,你这薪资确实有点太低了吧,我现在毕业第一年还没完,就顶你工作四年了?我咋这不信。

但实际上工作10年并不一定能拿到高薪,相反,很多时候努力并不一定会成功。

在《2019国人工资报告》中,工作十年以上的人群中,月薪过万的只占22.44%,也就是说接近八成的人薪资处于10000元以下。所以说,每月能领一万元工资的人已经超越了很多人了!


不同的人有着不同的人生。何况,公司给你发工资与你工作的年限关系不大,除了你所能创造的价值外,还和时下薪酬标准有关系。

们觉得呢?

来源:mp.weixin.qq.com/s/j21xz0PXHEkJ9Q5q7xDGdA

收起阅读 »

不同年龄段人群上班时的自我感觉

不同年龄段人群上班时的自我感觉↓↓↓完美男人的样子↓↓↓一天上一天班↓↓↓最后的赢家还是我↓↓↓敷衍的等级↓↓↓这个企业文化太佛系了↓↓↓成熟的大人↓↓↓身体不舒服就请假吧↓↓↓多个职业规划同步进行↓↓↓恭喜你离职了↓↓↓来源:mp.weixin.qq.com...
继续阅读 »

不同年龄段人群上班时的自我感觉

↓↓↓


完美男人的样子

↓↓↓



一天上一天班

↓↓↓



最后的赢家还是我

↓↓↓



敷衍的等级

↓↓↓



这个企业文化太佛系了

↓↓↓



成熟的大人

↓↓↓



身体不舒服就请假吧

↓↓↓



多个职业规划同步进行

↓↓↓



恭喜你离职了

↓↓↓


来源:mp.weixin.qq.com/s/f3KmoXG7nCBsdMafxmQfNw

收起阅读 »

Kotlin中Channel的使用

什么是Channel Channel API是用来在多个协程之间进行通信的,并且它是并发安全的。它的概念有点与BlockQueue相似,都遵循先进先出的规则,差别就在于Channel使用挂起的概念替代了BlockQueque中的阻塞。使用它我们可以很轻易的构建...
继续阅读 »

什么是Channel


Channel API是用来在多个协程之间进行通信的,并且它是并发安全的。它的概念有点与BlockQueue相似,都遵循先进先出的规则,差别就在于Channel使用挂起的概念替代了BlockQueque中的阻塞。使用它我们可以很轻易的构建一个生产者消费者模型。并且Channel支持任意数量的生产者和消费者


channel_mimo.webp


从源码我们可以看出Channel主要实现了两个接口


public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {}

interface SendChannel<in E> {
suspend fun send(element: E)
public fun trySend(element: E): ChannelResult<Unit>
fun close(): Boolean
//...
}

interface ReceiveChannel<out E> {
suspend fun receive(): E
public fun tryReceive(): ChannelResult<E>
fun cancel(cause: CancellationException? = null)
// ...
}


  • SendChannel: 用于添加元素到通道中和关闭通道;

  • ReceiveChannel:主要用于接收通道中的元素


你会发现SendChannel中的send()和ReceiveChannel中的receive方法都是挂起函数,为什么会怎么设计,在通道中如果存储元素的数量达到了我们设置的通道存储大小的时候,再通过send()方法往通道中发送数据,就会挂起,直至通道有空闲空间是才会将挂起的发送动作恢复。同理,如果我们的通道中没有可用的元素时,这个时候我们通过receive方法去接收数据,就会发现此操作将会被挂起,直到通道中存在可用元素为止。


如果我们需要在非挂起函数中去接收和发送数据,我们可以使用trySendtryReceive,这两个操作都会立即返回一个ChannelResult,结果中会包含此次操作的的结果以及数据,但是这两个操作只能使用在容量有限的通道上。


Channel的使用


下面我们通过构建一个简单的消费者和生产者模型了解以下Channel的使用


suspend fun main(): Unit = runBlocking {
val channel = Channel<String>()
//生产者协程
launch {
channel.send("Hello World!")
}
//消费者协程
launch {
val received = channel.receive()
println(received)
}
}
}

上面这种创建Channel的方式,在我们使用完通道之后很容易忘记一个close操作,特别是如果其中一个生产者协程应为某些情况发生异常,停止了生产,那么消费者协程会一直挂起等待生产者生产完成进行消费。所以我们可以使用协程的一个扩展方法produce,当协程发生异常,或者是协程完成时,它会自动去调用close方法,并且它会返回一个ReceiveChannel,下面我们就来看看怎么使用


runBlocking {
val channel = produce {
listOf("apple","banana","orange").forEach {
send(it)
}
}
for (element in channel){
print(element)
}
}

Channel有哪些


我们在使用Channel函数在创建通道时,我们会指定通道的容量大小,然后会根据容量创建不同类型的通道


public fun <E> Channel(
capacity: Int = RENDEZVOUS,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
when (capacity) {
RENDEZVOUS -> {
if (onBufferOverflow == BufferOverflow.SUSPEND)
RendezvousChannel(onUndeliveredElement)
else
ArrayChannel(1, onBufferOverflow, onUndeliveredElement)
}
CONFLATED -> {
require(onBufferOverflow == BufferOverflow.SUSPEND) {
"CONFLATED capacity cannot be used with non-default onBufferOverflow"
}
ConflatedChannel(onUndeliveredElement)
}
UNLIMITED -> LinkedListChannel(onUndeliveredElement)
BUFFERED -> ArrayChannel(
if (onBufferOverflow == BufferOverflow.SUSPEND)
CHANNEL_DEFAULT_CAPACITY
else 1,
onBufferOverflow, onUndeliveredElement
)
else -> {
if (capacity == 1 && onBufferOverflow == BufferOverflow.DROP_OLDEST)
ConflatedChannel(onUndeliveredElement)
else
ArrayChannel(capacity, onBufferOverflow, onUndeliveredElement)
}
}

可以从以上源码看出我们的通道主要分为4种类型



  • RENDEZVOUS :默认容量为0,且生产者和消费者只有在相遇时才能进行数据的交换

  • CONFLATED :容量大小为1,且每个新元素会替换前一个元素

  • UNLIMITED: 无限容量缓冲区且send永不挂起的通道。

  • BUFFERED : 默认容量为64,且在溢出时挂起的通道,可以通过设置JVM的 DEFAULT_BUFFER_PROPERTY_NAME来覆盖它


我们从Channel源码看出,Channel在创建时还会指定缓冲区溢出时的策略


public enum class BufferOverflow {
//缓冲区满时,将操作进行挂起,等待缓冲区有空间
SUSPEND,
//删除旧值
DROP_OLDEST,
//将即将要添加进缓冲区的值删除
DROP_LATEST
}

Channel函数还有一个可选参数onUndeliveredElement,接收一个Lambda在元素被发送且未被消费时调用,我们通常使用它来关闭一些该通道发送的资源。


在Channel内部结构种维护的缓冲区结构除了ArrayChannel内部自己维护了一个数组作为缓冲区,其余的都是使用AbstractSendChannel的链表作为缓冲区


那么我们将两个管道的内容合并成一个呢


fun <T> CoroutineScope.fanIn(
channels: List<ReceiveChannel<T>>
): ReceiveChannel<T> = produce {
for (channel in channels) {
launch {
for (elem in channel) {
send(elem)
}
}
}
}

扇出:多个协程从单个通道接收数据。为了正确地接收数据,我们应该使用for循环(使用consumeEach是不安全的)。


扇入:多个协程对单个通道发送数据


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

Flutter布局指南之谁动了我的Key

Key
Key用来干嘛 Flutter中的Key,一直都是作为一个可选参数在很多Widget中出现,那么它到底有什么用,它到底怎么用,本篇文章将带你从头到尾,好好理解下,Flutter中的Key。 我们首先来看下面这个Demo: Column( mainAxisA...
继续阅读 »

Key用来干嘛


Flutter中的Key,一直都是作为一个可选参数在很多Widget中出现,那么它到底有什么用,它到底怎么用,本篇文章将带你从头到尾,好好理解下,Flutter中的Key。


我们首先来看下面这个Demo:


Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
color: Colors.red,
),
Container(
width: 100,
height: 100,
color: Colors.blue,
),
],
)

image-20220227203558343


展示为两个不同颜色的方块。


问题1


这时候,如果我们在代码中交换两个Container的位置,Hot reload之后,它们的位置会发生改变吗?


下面我们把Demo修改一下,将Container抽取出来,并在中间放一个Text用来做计时器,并改为StatefulWidget,代码如下。


class KeyBox extends StatefulWidget {
final Color color;

KeyBox(this.color);

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

class _KeyBoxState extends State<KeyBox> {
var counter = 0;

@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: widget.color,
child: Center(
child: TextButton(
onPressed: () {
setState(() => counter++);
},
child: Text(
counter.toString(),
style: const TextStyle(fontSize: 60),
),
),
),
);
}
}

Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
KeyBox(Colors.yellow),
KeyBox(Colors.green),
],
)

这样当我们点击计时器工作之后,展示如下。


image-20220227203642652


问题2


这时候,如果我们在代码中交换两个Container的位置,Hot reload之后,它们的数字会发生改变吗?


问题3


如果我们删掉第一个Widget,Hot reload之后,显示的是数字几?


问题4


如果我们再重新把删掉的Widget加回来,Hot reload之后,又会如何显示?


问题5


如果在问题2的基础上,给第一个Widget外新增一个Center,那么又会如何显示呢?


如果你能完全回答上面的这几个问题并知道为什么,那么恭喜你,看完这篇文章,你会浪费十几分钟,当然,如果你不清楚,那么这十几分钟的时间,将给你带来不小的收益。


Key是什么


Flutter通过Widget来渲染UI,那么它是如何区分上面的两个不同颜色的Container的呢?通过颜色吗?当然不是,如果Container的颜色相同,那岂不是无法区分了?


所以,Key就成了Flutter区分不同Widget的依据,这就好比是Android中布局的ViewID。


知道Key是什么还不够,我们还得知道,我们为什么需要Key,首先,我们来看下上面的三个问题。


对于问题1,这个应该很简单了,Container是StatelessWidget,所以每次Hot reload都会重新build,因此颜色肯定会发生互换,这个很好理解。


那么对于问题2呢?StatelessWidget改成了StatefulWidget,这次再交换两个Widget的位置,你可以发现,虽然颜色互换了,但是数字没变。


要怎么解决这个问题呢?这就需要用到Key了,我们给KeyBox增加一个Key的参数。



新的Flutter Lint已经会提示你构造函数需要增加key的可选参数了。



const KeyBox(this.color, {Key? key}) : super(key: key);

在使用的地方,传入ValueKey即可。


KeyBox(Colors.yellow, key: ValueKey(2)),
SizedBox(height: 20),
KeyBox(Colors.cyan, key: ValueKey(1)),

这时候你再切换两个Container的位置,数字就会跟着变换了。


Key的原理


Key实际上是Flutter用来标记Widget的唯一标识,但是为什么需要Key,就要从Flutter的渲染流程上说起了。


Widget作为Flutter中的不可变数据,是作为渲染的数据类而存在的,它实际上就是内容的配置表,根据View的树形结构,自然而然模拟出了一个Widget Tree的概念。


Widget在运行时会创建Element实例,这些Element和Widget也组成了一一对应的关系,对于StatefulWidget来说,Widget中包含了组件的外观、位置等信息,而Element中,包含了State信息,这也是Flutter的核心原理。所以,在上面的Demo中,Counter作为State,被保存在Element中,而颜色,被保存在Widget中。


Widget和Element分离之后,如果修改颜色等Widget属性,那么可以直接创建新的Widget替换旧的Widget,同时还可以保留Element中的数据,因为创建Widget的成本是很低的,而Element则会高很多,所以Element会持续尽可能长的时间。


那么在Widget被改变之后,Element是如何和Widget进行关联的呢?这就需要两个东西了:



  • runtimeType

  • Key


所以Element会先对比当前新的Widget Tree中的新元素,是否跟当前Element的类型一致,如果不一致,那么说明Element已经无效了,只能重新创建,如果类型一致,那么就需要进一步判断Key了。


问题2的原因


所以,在问题2中,由于两个Widget的类型并没有发生变化,而又没有Key,所以,Widget被重新创建后,与原来的Element又关联起来了,看上去就是只修改了颜色。


那么在问题2的解法中,我们给Widget增加了Key,当我们调换两个Widget的位置时,虽然类型没有改变,但是Key发生了改变,Element在原来的位置找不到对应的Widget,那么这时候,它会选择在当前层级下,继续搜索这个Key。


这里要注意,Element只会在当前层级下搜索,如果这个Key的Widget被移入了其它层级,那么也是无法找到的,在问题2的场景下,由于只是交换了两个Widget的顺序,所以Element会在后面找到之前Key的Widget,同理,下一个Element也会找到,所以,两个Widget都被关联起来了,所以State也显示正确了。


问题3的原因


那么在问题3中,我们删除了第一个Widget,当没有Key时,Element会在Widget Tree中搜索,当它发现第二个Key类型是一样的时,它就以为它找到了,而第二个Element,因为找不到Widget,就销毁了。最终的效果就是剩下第二个Box的颜色和第一个Box的数字。


那么如果有Key呢?有Key的话,就不会找错了啊,所以自然能够对应上,与我们预想的也就是一样的了。


问题4的原因


理解了问题3,那么问题4就好理解了。当我们在开头创建同一个类型的Widget时,Element会把这个新增的Widget当作是以前的Widget,因为它们类型相同,所以Element被关联到了这个新的Widget,而另一个Widget发现已经没有Element了,所以会选择新建一个Element,这时候,数字就是默认值0了。


问题5的原因


对于问题5来说,实际上就是Element的搜寻机制,前面解释了,Element只会在当前层级进行搜索,所以Center的加入,改变了Widget的层级,Element无法对应了,所以它也选择了消耗重建,所以第一个Widget会显示默认值0。



但是要注意的是,如果类型不一致,那么Flutter会直接判断不相同,从而直接消耗重建,所以,在这些问题里,如果在KeyBox之间插上一些不同类型的Widget,那么就瞬间破防了,演示的效果就完全不同了。



Key有哪些Key


Key从整体上来说,分为两种,即:



  • Local Key:分为Value Key、Object Key和Unique Key

  • Global Key


Local Key顾名思义,指的是在当前Widget层级下,有唯一的Key属性,而Global Key,则是在全局APP中,具有唯一性。Global Key的性能会比Local Key差很多。


Value Key


在前面的Demo中,我们给KeyBox增加了Key之后,Widget在修改、移动之后,Element就可以正确的找到对应的Widget了,这里我们使用的是Value Key。


Value Key,顾名思义,就是使用Value来对Key做标识的Key,例如我们在Demo中使用的,ValueKey(1),value可以是任意类型,这里是1,其实更符合的场景,应该是用Color,或者是更加具有语义性的value来作为Key的value。


Value Key在同一层级下需要具有唯一性,所以当两个KeyBox都设置成ValueKey(1)时,程序就会报错,告诉你Key重复了。


Object Key


Object Key与Value Key类似,但是又不完全一样,Value Key对比的是Value,Value相等,就是相等,而Object Key,对比的是实例,实例相同,才是相等,就好比一个Java中的equals,一个是「==」。我们看下Object Key的源码就一目了然了。


@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ObjectKey
&& identical(other.value, value);
}

假如我们有一个自定义的Class,重写了它的==函数,那么用Value Key,new两个同样的对象,它们就是相等的,而Object Key,则不相等,原因就是一个比较的是值,一个比较的是引用。


Unique Key


Unique Key自己都说了,它是独一无二的,也就是说,Unique Key只和自己相等,任意创建多个Unique Key,都是不相等的,相当于唯一标识了。


如果在Build函数中创建Unique Key,那么这个Key在大部分场景下就没有意义,因为Hot reload时,Build函数会重建,所以Unique Key被重建,而且和之前也不相等。


这就很奇怪了,这玩意有什么用呢?


用处确实不多,但一旦用到,就必须得用,例如下面这个例子。


假如我们要用AnimatedSwitcher来实现切换时的动画效果,这时候,我们需要让每次改变都要执行动画,那么这里就可以使用Unique Key,强制每一次都是新的Widget,这样才能有动画效果。


那么另一种使用场景,就是在无法使用Value Key和Object Key的时候使用,但是这时候,需要将Unique Key定义在Build函数之外,这样Unique Key只会创建一次,从而保证唯一性的同时,不用去创建value和Object。


Global Key


Global Key全局唯一且只和自己相等,还记得之前Element在关联新变化的Widget时是怎么比较Key的吗——Element为了效率问题,只会在当前层级下进行寻找,所以,在问题5中,一旦我们修改了某个Widget的层级,那么Element就会消耗重建,那么如果使用了Global Key呢?当Key的类型是Global Key时,Element会不惜代价在全局寻找这个Key,这也是为什么Global Key的效率会比较低的原因。


那么有了Global Key,即使Widget Tree发生了改变,也依然可以找到这个Widget进行关联,但是要注意的是,Global Key需要定义在Build函数之外,否则每次都会重新创建Global Key,那就没有意义了。


除此之外,Global Key还有一个作用,那就是给一个Widget增加一个全局标识,这样有点像命令式编程的意思,类似Android中的FindViewByID,通过Global Key就可以找到当前标记的这个Widget,从而获取它的一些相关信息。


final count = (globalKey.currentState as _KeyBoxState).counter;
print('count: $count');
final color = (globalKey.currentWidget as KeyBox).color;
print('color: $color');
final size = (globalKey.currentContext?.findRenderObject() as RenderBox).size;
print('size: $size');
final position = (globalKey.currentContext?.findRenderObject() as RenderBox).localToGlobal(Offset.zero);
print('position: $position');

// output
flutter: count: 0
flutter: color: MaterialColor(primary value: Color(0xff4caf50))
flutter: size: Size(100.0, 100.0)
flutter: position: Offset(145.0, 473.5)

由此可见,通过Global Key,我们可以拿到State、Widget、Element(Context)以及通过Element关联的RenderObject,这样就可以获取Widget中的一些配置参数,State中的数据变量,以及RenderObject中的绘制信息,例如尺寸、位置、约束等等。


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

FlutterWeb开发进出坑总结

一、启动运行乱码 没错,启动一个demo,遇到坑了,如图所示 点击Android Studio上方运行按钮,程序启动之后汉字文字显示乱码,这是由于flutter web有三种渲染模式,auto 、html 和 canvaskit,点击运行按钮(flutter...
继续阅读 »

一、启动运行乱码


没错,启动一个demo,遇到坑了,如图所示


image


点击Android Studio上方运行按钮,程序启动之后汉字文字显示乱码,这是由于flutter web有三种渲染模式,auto 、html 和 canvaskit,点击运行按钮(flutter build web命令)默认的渲染模式为auto,这种模式在移动端使用html渲染,在pc端使用canvaskit渲染。


解决办法 1: 用命令行运行,并指定渲染模式,就能解决问题。


// 指定渲染模式为html
flutter build web --web-renderer html

解决办法 2: 上面虽然能解决问题,但我习惯用按钮运行程序怎么办?当然也找到了其他办法。在程序包下web/index.html文件中body标签下copy如下代码。


 <!--指定web运行模式-->
<!-- window.flutterWebRenderer = "canvaskit";-->
<script type="text/javascript">
window.flutterWebRenderer = "html";

</script>
<script src="main.dart.js" type="application/javascript"></script>

二、Debug启动运行断点失败


web开发和APP端开发一样,也可以断点。项目之初断点是可以的,但是不知道怎么的,debug可以运行,但断不到,很奇怪,花了一上午,发现同事因为发版改了下web/index.htmlhead->base标签下 href="***"的值。


解决办法 :


// 之前,断点可用
<base href="$FLUTTER_BASE_HREF">

// 同事改动,断点不可用
<base href="git/******">

// 修复后,断点可用
<base href="/">

不能断点开发实在是麻烦。


三、Hot Reload热重载、点击浏览器刷新,都会重启整个程序


在APP端开发时,在某个页面点hot reload按钮,只会重新运行当前页面,但是在web中,点热重载会重启,这只是开发中的不方便。已经上线的程序,用户只要点击浏览器刷新就会重启整个程序,无论在哪个页面,都会回到第一个页面,这与我浏览网页的习惯明显是不符的。


查找原因,发现是flutter底层问题,仔细观察web页面是通过不同的url来确定的,而Flutter从始至终都是一个url,只是flutter在一个网页中绘制了不同的页面(与APP端原理一致),所以想解决问题就是要每个页面都有自己的url。


解决办法 : 用静态路由的方式跳转页面和传参,具体代码如下。


  // 跳转与传参
static Future toName(String pageName, Map<String, dynamic> params) {
var uri = Uri(scheme: RoutePath.scheme, host: pageName, queryParameters: params);
return Navigator.of(currentContext).pushNamed(uri.toString());
}

// 取参方式
static Route<dynamic> generateRoute(RouteSettings settings) {
return PageRouteBuilder(
settings: settings,
pageBuilder: (BuildContext c, Animation<double> a,Animation<double> sa) {
var uri = Uri.parse(settings.name ?? ''); //解析页面名
switch (uri.host) {
case RoutePath.name:
return NamePage(uri.queryParameters); 、、传参
default:
return Scaffold(
body: Center(
child: Text('没有找到对应的页面:${settings.name}'),
),
);
}
});
}

通过以上方式,跳转时每个页面都会有自己的url和拼接的参数,这样刷新的时候就不会重启整个程序,会停留在当前页面。


四、用静态路由的方式跳转,全局变量,单例对象丢失,页面栈记录丢失。


没错,坑是连着的,我也是服了。当在某页面热重载或点击浏览器刷新,会停留在当前页面,但是无法返回,就算点击跳转至其他页面,也会报错,因为全局变量都已经丢失,比如:登录信息,用户信息,已经初始化的工具类对象等。


已经有人提了Issues,国内也有大神分析了原因和不完全结局方案



目前flutter web对于浏览器还是没有适配完全,无论Navigator1.0还是Navigator2.0,都存在不可解决的严重问题。目前来看google的对flutter web的意图,还是开发移动web并在App中通过webkit这种内核使用,并没有想开发者使用flutter web来开发真正的web应用,或者后续会完善这部分。



我的解决方案



  1. 除了登录页和首页,其他页面不用静态路由的方式跳转,这样做即使用户刷新,也不会回到登录页,而是回首页。

  2. 在有刷新需求的页面上提供刷新图标,可触发刷新,避免用户点击浏览器的刷新。

  3. 全局变量持久化,用html.window.localStorage并配合工厂模式持久化数据,当被触发刷新,会从本地重新赋值,比如:登录信息等。

  4. 弱化全局成员变量,非必要不使用全局类的变量,数据尽量放云端,页面间不耦合。


五、检测浏览器/标签页关闭还是刷新


解决办法 : 可以使用函数onBeforeUnload来检查选项卡是否正在关闭。它也可能检测到页面刷新。


import 'dart:html' as html;
html.window.onBeforeUnload.listen((event) async{
// do something
});

或者


import 'dart:html' as html;
html.window.onUnload.listen((event) async{
// do something
});

六、引用 import 'dart:html' 运行提示报错


多端运行,如果引用了html会提示报错。


解决办法 : 可以引用第三方universal_html 2.0.8,帮封装了一层,支持多端。



universal_html :适用于所有平台的“dart:html”,包括 Flutter 和服务器端。简化跨平台开发和 HTML / XML 处理。



七、可点击提示


在平常浏览网页时,鼠标滑动到可点击的文字或按钮上,鼠标“箭头”会变成一个“小手”,或背景出现颜色变化提示。
Flutter中常用的GestureDetector()手势工具,虽然可以实现点击等回调,但是鼠标滑动到可点击区域,鼠标“箭头”并不会变成“小手”,在交互上不符合大众使用网页的习惯。


解决办法 : 使用InkWell替换GestureDetector,用InkWell包住的按钮或文字,鼠标悬停,就会出现小手。


Ink(
width: width,
height: height,
color: color,
child: InkWell(
focusColor: Colors.transparent,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
hoverColor: const Color(0x0818a7fb),
onTap: onTap,
child: Center(child: this)))

ezgif.comvideotogif.gif
分析源码可知,内部用MouseRegion监听了鼠标位置,那什么是MouseRegion呢?


八、鼠标监听控件 MouseRegion


相对于APP端,web端多了个鼠标,可以实现app实现不了的交互效果,比如悬停,划过,进入退出某区域等,都可以用MouseRegion实现。


n_v29940d762539d4d1788cd31924e19fcaf.gif


MouseRegion的属性和说明










































字段属性Col3
onEnterPointerEnterEventListener鼠标进入区域时的回调
onExitPointerHoverEventListener鼠标退出区域时的回调
onHoverPointerExitEventListener鼠标在区域内移动时的回调
cursorMouseCursor鼠标悬停区域时的光标样式
opaquebool是否阻止检测鼠标
childWidget子组件

最后


这是目前遇到有价值的坑,后面遇到新的也会持续更新。


Flutter开发web上,路由和全局变量上的坑还是挺严重的,但只要没有复杂的页面间逻辑,普通展示完全没问题。


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

“精准红码”事件,查清楚了!史上最惨程序员干的

从6月13日起,一些从外地赴河南郑州的人,一到郑州扫码填报个人信息后,健康码立马变红码,行动被限制。这些“被精准红码”的人怎么了?是被病毒跨时空盯上了?他们的共同身份是:河南的村镇银行储户。甚至有储户称,派出所人员说,只要返程,健康码就“应该可由红变绿”。如果...
继续阅读 »

导读:储户何罪之有?健康码到底是防疫用的,还是用来控制人身自由的电子脚铐?


河南“天降红码”,让我们认识到现实往往比小说更魔幻。

从6月13日起,一些从外地赴河南郑州的人,一到郑州扫码填报个人信息后,健康码立马变红码,行动被限制。

有的人还在家中,只不过扫了微信群中有人分享的郑州登记二维码,健康码同样变红。

这些“被精准红码”的人怎么了?是被病毒跨时空盯上了?

都不是!

他们的共同身份是:河南的村镇银行储户。

前不久,河南发生多家村镇银行暴雷事件,出现提款难的问题,涉金额高达数百亿。据媒体报道,这些外地储户原本准备在6月13日从各地赶到郑州了解情况。


根据财新网的调查,涉及线上系统被关闭的四家河南村镇银行、两家安徽村镇银行,线上储户共约41.3万人,其中很大一部分是外地储户。

“被红码”的精准度高得惊人。例如,有一位6月12日从成都飞往郑州的张女士说,他们一行三人,有两人为村镇银行储户,另一人是用其丈夫名字开的账户。也正因如此,那位用丈夫名字开户的女士至今仍为绿码。不过,也有人称,全家都被赋红码。

有意思的是,一些储户抱怨进入郑州后健康码突然变红,但他们一离开郑州后就变回绿。

甚至有储户称,派出所人员说,只要返程,健康码就“应该可由红变绿”。如果这样的说法属实,那显然是让人“进难出易”。

令人吃惊的是,滥用健康码的范围可能不仅局限在限制外地储户入郑州维权。据第一财经报道,郑州多个在建楼盘的业主在6月15日反映称,自己的健康码也曾在6月12日、6月13日先后被“赋红码”。

被“赋红码”业主反映的信息后发现一个共同点:他们都曾向有关方面反映过购房中遇到的问题,同时,他们都被有关方面询问过是否为村镇银行储户。

由于“天降红码”的对象针对性很强,均为河南村镇银行储户,网民有理由怀疑这背后藏有猫腻。

实际上,5月下旬,郑州有数百名人走上街头,聚集到河南银保监局门外要求拿回存款。


目前部分已“复绿”,当地工作人员建议离豫

记者了解到,部分储户的健康码已于6月14日下午“复绿”,但当地工作人员建议他们尽快离豫。而一份由储户提供的录音显示,对于暂时还是红码的储户,当地负责隔离的工作人员称他们只要离开河南后就可以变回绿码。也有储户反映,其健康码变绿后又多次变回红码,需要刷新、重新填写资料才能再次“复绿”。

6月13日下午,在郑州财贸学校图书馆隔离期间,孙先生无意中打开健康码发现,其健康码已经变绿。孙先生随即提出离开隔离点的诉求,但工作人员建议其尽快离豫,孙先生买了回山东的火车票后,由工作人员开车将其带到了郑州站。


郑州财贸学校图书馆被作为红码人员隔离点。

孙先生称,上火车后直至6月14日中午,他多次打开健康码,发现还会出现变回红码的情况,在经过刷新和重新填写个人信息后,健康码才能再度变回绿码。

记者联系多位储户了解到,6月14日中午过后,储户们的红码开始批量“复绿”。

一位来自湖南娄底、在青龙山庄隔离的储户称,其健康码于14日中午13时“复绿”,但“复绿”后并不能立马就畅通出行,须经过核酸检测并出结果后才能离开,不过确认买车票离豫的人员可以马上离开。

省卫健委:相关部门已在调查核实

14日最新消息,6月14日16时许,河南省卫健委值班室一名工作人员告诉澎湃新闻记者,接到多个“外地来豫储户被赋红码”投诉后,省卫健委昨日已将投诉反馈至相关部门,目前正在调查核实中。

至于这些储户为何被赋红码以及调查核实的进展等,该工作人员表示暂不了解。

郑州市12345:大数据信息库出现问题

另据南方都市报,针对储户所疑问的“红码”情况,郑州市12345热线13日晚曾回应南都记者称:目前没有接到“外地来郑州一律赋红码”通知,如果被赋“红码”,建议先联系信息排查专班了解赋码原因。目前低风险地区来郑州只需要48小时核酸检测阴性证明,出示行程码和健康码。

天目新闻此前在郑州市12345工作人员处得到的回复则是,今天已接到多个来电反映未出行或者无缘由被河南省赋红码的情况。对于“红码”是否仅针对储户,工作人员表示具体情况并不了解,但经过查证是因大数据信息库出现了一些问题,现在已将该情况上报政府,正在积极改进中,“建议进行后续的关注”。

牛逼啊!接私活必备的 N 个开源项目!赶快收藏吧

此外,12345工作人员告诉记者,“河南省赋红码的情况,是省级单位进行处理的。建议可以尝试向社区咨询,但据其接到的通知是目前社区也无法处理。如果是郑州市的红码,我们可以进行受理。”

河南部分村镇银行取款难

再说回河南村镇银行,据中国基金报,4月中旬,河南个别银行取款难的问题逐渐引发关注,彼时多家银行先后发布公告,称因系统升级维护,网上银行、手机银行将暂停服务;亦有媒体报道,一些储户在线下营业网点排队等待取款。

5月18日,银保监会与人民银行持续关注河南4家村镇银行线上服务渠道关闭问题,已责成河南银保监局和人民银行郑州中心支行切实履行属地监管职责,密切配合地方党委政府和相关部门稳妥处置。

据了解,4家村镇银行股东——河南新财富集团通过内外勾结、利用第三方平台以及资金掮客等吸收公众资金,涉嫌违法犯罪,公安机关已立案调查。目前4家村镇银行营业网点存取款业务正常开展,凡依法合规办理的业务均受到国家法律保护。银保监会与人民银行将密切配合地方党委政府和相关部门,严惩金融犯罪,依法保护广大金融消费者合法权益。

银保监会与人民银行提醒广大金融消费者,办理金融业务选择正规渠道,不被“高息”“高收益”等虚假宣传误导,不轻易将资金委托给第三方代办,防止上当受骗。

侠客岛评论:一码归一码,随意给人赋红码应被严肃追责!

给维护正当权益的储户赋红码,不知是哪个“天才”想出来的主意,更不知这种明显有违常识、法治、公理的操作,怎么就能堂而皇之地施行!不客气地说,疫情防控措施被随意用于“社会治理”或“维稳”目的,不管是哪个部门、哪些人授意干的,都应被严肃追究责任。因为一个基本的道理是:“一码归一码”。

储户维权的事,应当交由相关职能部门处置,金融专业案件涉及利益大,更需详细审慎调查处理。你要维权、上访?好,我给你赋个红码,让你“动弹不得”。这当然“方便”、省事,来“找麻烦”的人没有了嘛!照这逻辑,所有棘手的社会矛盾、久拖不决的纠纷、不想处理的麻烦,一个红码了事,多么省心又潇洒!问题解不解决不知道,反正你们老实待着,哪儿都甭去。这不是解决问题,而是在激化矛盾。这不是“聪明能干”,而是典型的懒政塞责。想出这些主意的人挺会抖机灵,可惜脑筋动歪了。

中国能取得疫情防控的巨大成果,基础是高效精准的科学手段和对政策规则的普遍遵守。健康码称得上疫情防控的信息基础设施,一些人基于自己的治理“小目标”,耍小聪明出昏招,不仅于事无补,更会失信于民。“民无信不立”。因为一点小算计而损失了公信力,这种代价绝非金钱可以衡量,亦非“抖机灵”的人能够承担。给储户“精准”赋红码这出闹剧,不仅荒唐,更应警惕!

半月谈:给讨说法的储户赋红码?健康码不能被滥用

给讨说法的储户赋红码,突破了公众对健康码适用范围的共识,当然激起了人们的强烈不满与深深担忧。这种做法,一方面消解了健康码的本真功能,一方面也扰乱了疫情防控大局。我们不禁要追问,这一做法是经过了什么样的流程?又是由谁来决策使用的?相关地方应该查清楚来龙去脉,及时回应社会关切。

河南这一事件所暴露出的“权力任性”的危险倾向,警醒我们必须加强健康码的规范使用。事实上,国家卫健委等三部门2020年12月联合发布的《关于深入推进“互联网+医疗健康”“五个一”服务行动的通知》曾明确规定,加强防疫健康码数据规范使用,强化数据安全管理,切实保护个人隐私。从目前出现的问题看,还需进一步制定详细的规范,明确规定滥用的惩处举措。

健康码是为人们出行和流动提供便利的,绝不能成为任何人任何地方压制社会矛盾的手段。唯有完善健康码规范使用制度,建立起防范滥用的追责机制,才能防止乱动的手滥用健康码,让健康码回归其本真功能。

网友:健康码到底是防疫用的,还是用来控制人身自由的电子脚铐?

针对此事引起网友热议,有网友表示,储户何罪之有?必须还储户一个公道;还有网友表示,应该严肃追责,严肃处理;也有网友发出感慨,健康码到底是防疫用的,还是用来控制人身自由的电子脚铐?

记者从河南卫健委了解到,河南卫健委已经就“河南南村镇银行入豫健康码变红事件进行了调查。经疫情防控指挥部和技术服务等部门的调查,初步确定负责豫康码开发运维的公司,在更新外省人员入豫风险判定规则时, 由于程序员违规操作,错误使用了过期的规则,导致部分外省入豫人员被错误赋予红码。目前程序bug已经修复,被误判的红码也已经变为绿色,相关涉事人员将会被进行行下一步追责。


你还有什么想要补充的吗?

来源:mp.weixin.qq.com/s/VIN8EgWF00l4lfbwvoLKNQ

收起阅读 »

Kotlin知识点的深入思考

Kotlin是基于JVM的一个语言,也是很时髦的语言。Java语言这几年的发展,借鉴了Kotlin的很多特性。Google把Kotlin作为Android的优先使用语言之后,更是应者影从。本文整理了在Kotlin学习和使用中总结整理出来的几个有意思的知识点,和...
继续阅读 »

Kotlin是基于JVM的一个语言,也是很时髦的语言。Java语言这几年的发展,借鉴了Kotlin的很多特性。Google把Kotlin作为Android的优先使用语言之后,更是应者影从。本文整理了在Kotlin学习和使用中总结整理出来的几个有意思的知识点,和大家学习和交流。


Coroutines ARE light-weight


fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}

以上代码在学习Kotlin协程的时候应该都见过了,是为了说明协程很轻量。原因也很简单,在一般的操作系统中,用户线程和内核线程是一对一的关系,操作用户线程的就是操作内核线程,每一个内核线程,都有专门的内核数据结构来管理,在Linux里面使用数据结构task_struct来管理的,这是一个很复杂的数据结构,内核线程的挂起和执行,都会涉及到变量和寄存器数据的保存和恢复,甚至内核线程本身的调度就需要消耗CPU的时间。但是协程完全是用户空间里面的事情,说的简单点,就是几个任务的执行队列的管理,当然协程也是运行中线程之上的。


有个疑问产生了?那为什么现在操作系统用户线程和内核线程是一对一的关系呢?


因为在早期的Java版本中,在单核CPU的时代,用户线程和内核线程的关系是多对一。在多核时代,也有多对多的模型。即多路复用多个用户级线程到同样数量或者更少数量的内核线程。在Solaris早几年的版本也是支持类似的多对多的模型,大家想过没有,为什么现在几乎所有的操作系统都使用一对一的模型了呢?


以下是一家之言,和大家探讨。OS本身越来越复杂,参与方也越来越多。之前线程这块分两层,内核层和用户线程库。用户线程库为程序员提供创建和管理线程的API。随着互联网的发展,有一些需求产生了,如高并发支持,OS这一层比较笨重的,很难快速满足越来越快的需求的变化。这个时候,一些语言,在设计之初就考虑来解决这些新产生的问题,一些时髦的语言,也非常快速的来响应这些需求。所以就有了在线程库之上,在语言的层面来解决这些问题,所以协程产生了,并且越来越多的语言支持了这些特性。


哈哈,为什么线程库为什么没有演进来支持协程呢?原因也很简单,线程库基本被定位成管理内核线程的接口,而且线程库的作者的主要精力也不在这个方向。线程库做好自己的事情(管理内核线程),然后把其他的交给别人。这也是自然形成的分工和分层。


想想这几年的Android应用开发的发展,AndroidX里的东西越来越多,演进也越来越快。这是因为Android系统的体量限制,不可能跑地很快,一年一次算得上是OS升级的极限了。所以必须把需要跑得快的东西剥离出来。这个道理和协程的发展也有异曲同工之处。


Lambda表达式捕获变量


Lambda表达式应该是一个历史比较悠久的东西了,由于函数式编程风行,Lambda表达式也是被非常广泛地使用。Java对Lambda的支持比较后知后觉,应该是在Java8才开始支持的吧,不过在JDK7的时候,JVM字节码引入了InvokeDynamic,后续应该会成为各个基于JVM语言解析Lambda表达式的统一的标准方法。后面会有单独一段来讨论这个InvokeDynamic。


Lambda本质上是一个函数,一块可以被执行的代码段。在函数式编程环境下,Lambda表达式可以被传递,保存和返回。在类似的C/C++的语言环境中,函数指针应该可以非常方便和高效的来实现Lambda,但是在Java和Kotlin这样的语言中,没有函数指针这样的东西,所以用对象来存储Lambda,这当然是在没有InvokeDynamic之前(Java7)。


说到Lambda表达式,大家还记不记得在Java中,如果Lambda要引入外部的一些变量时,这个变量一定要被声明为final。


public Runnable test() {
int i = 1000;
Runnable r = () -> System.out.println(i);
return r;
}

上面这段代码,变量i为实际上的final,编译器会把这个i变量自动加上final。


如下的代码端编译就会出错了,因为变量i不是final的。


public Runnable test() {
int i = 1000;
i++;
// Variable used in lambda expression should be final or effectively final
Runnable r = () -> System.out.println(i);
return r;
}

会什么会有这个限制呢?呵呵,你看上面test函数,如果调用test函数,然后把返回的对象保存下来后再执行,这个时候i这个变量已经在内存中销毁掉了,这个lambda也就没法执行了。因为i这个变量是栈变量,生命周期只在test函数执行期间存在。那么为什么声明称final就没事情了呢,因为在这种情况下,变量是final的,lambda可以把这个变量Copy过来。换句话说,lambda执行的是这个变量的Copy,而不是原始值。


讲到这里,如果你熟悉Kotlin的话,你知道Kotlin没有这个限制,引用的变量不是非得被声明为final啊。难道Kotlin就没有Java遇到的问题吗?


Kotlin一样会遇到同样的问题,只是Kotlin的编译器比较聪明能干啊,它把Lambda引用到的变量都变成final了啊。哈哈,可能你发现了,如果变量本身不是final的,强制变成final这就不会有问题吗?


fun test(): () -> Unit {
var i = 0
i++
return {
println("i = $i")
}
}

以上的代码是可以正常编译被执行的。原因就是编译器干了一些事情,它把i变成堆变量(IntRef)了,并且声明了一个final的栈变量来指向堆变量。


public static final Function0 test() {
final IntRef i = new IntRef();
i.element = 0;
int var10001 = i.element++;
return (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}

public final void invoke() {
String var1 = "i = " + i.element;
System.out.println(var1);
}
});
}

呵呵,其实吧,这是违背函数式编程的原则的。函数只需要依赖函数的输入,如果引用外部变量,会导致函数输出的不确定性。可能会导致一些偶现的很难解决的bug。
尤其如果在函数里面修改这些变量的话,如在final的List对象里面进行add/remove,这样还会有并发的安全隐患。


Invokedynamic避免为Lambda创建匿名对象


先稍微介绍一下字节码InvokeDynamic指令,需要更详细可以查看官方文档。这个指令最开始是在JDK7引入的,为了支持运行在JVM上面的动态类型语言。


先看如下代码,一个简单的Lambda表达式。


public Consumer<Integer> test() {
Consumer<Integer> r = (Integer i) -> {
StringBuilder sb = new StringBuilder();
sb.append("hello world").append(i);
System.out.println(sb.toString());
};
return r;
}

查看编译之后的字节码如下:


  public java.util.function.Consumer<java.lang.Integer> test();
descriptor: ()Ljava/util/function/Consumer;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
5: astore_1
6: aload_1
7: areturn
LineNumberTable:
line 7: 0
line 12: 6
Signature: #20 // ()Ljava/util/function/Consumer<Ljava/lang/Integer;>;

可以看到,具体Lambda表达式被invokedynamic取代,可以将实现Lambda表达式的这部分的字节码生成推迟的运行时。这样避免了匿名对象的创建,而且没有额外的开销,因为原本也是从Java字节码进行函数对象的创建。而且如果这个Lambda没有被使用到的话,这个过程也不会被创建。如果这个Lambda被调用多次的话,只会在第一次进行这样的转换,其后所有的Lambda调用直接调用之前的链接的实现。


Kotlin因为需要兼容Java6,没法使用invokedynamic,所以编译器会为每个Lambda生成一个.class文件。这些文件通常为XXX$1的形式出现。生成大量的类文件是对性能有负面影响的,因为每个类文件在使用之前都要进行加载和验证,这会影响应用的启动时间,尤其是Lambda被大量使用之后。不过虽然Kotlin现在不支持,但是应该会在不久的将来就支持了。


可以在一些合适的场景下,使用inline来避免匿名对象的创建,Kotlin内置的很多方法都是inline的。也要注意,如果inline关键字使用不当,也会造成字节码膨胀,并影响性能。


Callback转协程


现在很多库函数都使用回调来进行异步处理,但是回调会有一些问题。主要有两方面吧



  1. 错误处理比较麻烦。

  2. 在一些循环中处理回调也会是麻烦的事情。


所以如果我们在工程中遇到回调API的话,一般的做法会把这些回调转换成协程,这样就可以用协程进行统一处理了。
回调大致分两类:



  1. 一次性事件回调,用suspendCancellableCoroutine处理。

  2. 多次事件回调,用callbackFlow处理。


我们先来看下用suspendCancellableCoroutine,以下是模板代码,使用这段模板代码可以方便的把任意回调方便地转换成协程。


suspend fun awaitCallback(): T = suspendCancellableCoroutine { continuation ->
val callback = object : Callback { // Implementation of some callback interface
override fun onCompleted(value: T) {
// Resume coroutine with a value provided by the callback
continuation.resume(value)
}
override fun onApiError(cause: Throwable) {
// Resume coroutine with an exception provided by the callback
continuation.resumeWithException(cause)
}
}
// Register callback with an API
api.register(callback)
// Remove callback on cancellation
continuation.invokeOnCancellation { api.unregister(callback) }
// At this point the coroutine is suspended by suspendCancellableCoroutine until callback fires
}

接下来,我们来看看suspendCancellableCoroutine这个函数到底干了什么,在注释里面有相关的代码的解释。


public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
// 从最开始调用suspend函数的地方获取Continuation对象,并对把对象转换成CancellableContinuation对象。

val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)

cancellable.initCancellability()
// 调用block进行回调的注册
block(cancellable)

// 这个函数有个逻辑,如果回调已经结束,直接返回,调用者不进行挂起
// 如果回调还没有结束,返回COROUTINE_SUSPENDED,调用者挂起。
cancellable.getResult()
}

这里有一个关键的函数suspendCoroutineUninterceptedOrReturn,第一次看到这个函数的时候,就感到困惑,uCont这个变量是从哪里来的,这个地方光看代码是看不出从哪里来的。原因是这个uCont变量最终是编译器来处理的。每个suspend函数在编译的时候都会在参数列表最后增加一个Continuation的变量,在调用suspendCoroutineUninterceptedOrReturn的时候,会把调用者的Continuation的对象赋值给uCont。


所有这个函数给了我们一个机会,手动来处理suspend关键字给我们增加的那个参数对象。为什么我们要手动来处理呢,因为我们要把Continuation的对象转换成CancellableContinuation对象,这样我们就可以在被取消的时候来把回调给取消掉了。


如果要完全看懂以上代码,需要知道suspend关键字后面的逻辑,后面会有专门一节来说明。


关于多次事件的回调处理callbackFlow,基本逻辑与关键知识点和上面说的一致,所以这里不对callbackFlow进行说明了。


在suspend关键字后面


Kotlin协程是一个用户空间(相对于内核空间)实现的异步编程的框架。suspend关键字的处理是其中比较关键的一部分,要理解Kotlin的协程如何实现挂起和恢复,就必须要了解suspend关键字的后面的故事。


在讲suspend之前,我们先来了解一下Continuation-passing style(CPS)。
先来一道开胃菜,已知直角三角形的两条直角边长度分别为a和b,求斜边的长度 ?


define (a, b) {
return sqrt(a * a + b * b)
}

用勾股定理,可以用上面的代码可以轻松解决。上面的写法是典型的命令式编程,通过命令的组合方式解决问题。


现在我们来看看CPS的方案的代码应该如何来写?


define (a, b, continuation: (x, y) -> sqrt(x + y)) {
return continuation(a * a, b * b)
}

这里的CPS写法,把勾股定理分成两部分,第一部分计算直角边的平方和,第二部分进行开方操作。开方作为函数的参数传入,当第一部分计算完成之后,进行第二部分的操作。


哈哈,这不就是Callback的吗?没错CPS的本质就是Callback,或者说CPS就是通过Callback来实现的。当然如果仅仅把CPS理解Callback也是不完全准确。CPS要求每个函数,都需要指定这个函数执行完成之后的接下来的操作,所以这个名词应该是continuation,而不是callback,一般情况下,代码里面不会返回callback的执行结果,因为callback的语义上不是继续要干的事情,Continuation才是继续要干的事情,然后把最终的结果返回。


Kotlin编译器就是把suspend函数变成CPS的函数,来实现函数的挂起和恢复的。我们先来看最简单的例子,这个函数没有参数,也没有返回。先打印hello,1s之后再打印world。


suspend fun test() {
println("hello")
delay(1000L)
println("world")
}

对于这个函数,编译器做了两个主要的事情:



  1. 通过状态机的机制,把这个函数逻辑上分为两个函数,第一个函数是第一次被调用的,另一个函数是在这个函数从挂起状态恢复的时候调用,也就是在delay 1s之后执行的。

  2. 给suspend函数的参数添加continuation的参数,并在函数体里面对这个参数进行处理。


下面来看下,这个函数再被编译器处理之后的代码,代码以伪代码的形式给出。


fun test(con: Continuation): Any? {

class MyContinuation(
continuation: Continuation<*>): ContinuationImpl(continuation) {
var result: Any? = null
var label = 0
override fun invokeSuspend(res: Any?): Any? {
this.result = res
return test(this);
}
}
val continuation = con as? MyContinuation
?: MyContinuation(con)
if (continuation.label == 0) {
println("hello")
continuation.label = 1
if (delay(1000, continuation) == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
}
if (continuation.label == 1) {
println("world")
return Unit
}

error("error")
}

每一个suspend函数,都有一个continuation参数传入,自己也有一个continuation,包含传入的continuation,自己的continuation对象的类是自己独有的,一般会是一个匿名内部类,这里为了好理解,我把这个匿名的内部来变成普通的类,便于说明问题。在第一次调用这个函数的时候,会实例化自己的continuation对象。实例化的逻辑是


val continuation = con as? MyContinuation
?: MyContinuation(con)

这里有个特别关键的MyContinuation对象的变量label,初始化为0,所以函数第一次执行的是代码里面label等于0的分支,通过这样状态机的机制,把函数从逻辑上可以分成多个函数。


再来看下上面函数体里面的COROUTINE_SUSPENDED,当delay函数返回COROUTINE_SUSPENDED,这个函数也返回COROUTINE_SUSPENDED,同样,如果有函数调用这个函数的时候,也返回COROUTINE_SUSPENDED。这个标识就是用来指示函数进入了挂起状态,等着被回调了。所以函数挂起的实质是,这个函数在当前的label分支下返回了


如果suspend函数没有返回COROUTINE_SUSPENDED呢,那就接着执行,执行函数下一个状态的逻辑。所以函数在进入当前的状态的时候,就要马上把下个状态设置好。
continuation.label = 1。如果当前函数进入挂起状态,就会把当前的continuation对象传入到调用的函数中,当函数需要恢复的时候,会调用continuation的invokeSuspend的方法,就会重新执行这个函数,这里就是一个Callback了。当然会进入label等于1的分支。所以函数恢复的实质是,这个函数在新Label状态下被重新调用了。


注意了suspend函数不一定返回COROUTINE_SUSPENDED的,也可能返回具体的值。如以下的函数:


suspend fun test(): String {
return "hello"
}

这个函数就没必要进入挂起了,没有返回COROUTINE_SUSPENDED,在这种情况下,函数会执行下一个label分支。
这也是为什么每个suspend函数在编译器处理之后的函数返回值是Any?。这其实是一个union的结构体,只是现在Kotlin还不支持union这样的概念,不过Kotlin变化这么快,之后没准也会支持。


这里的MyContinuation继承了ContinuationImpl,所以看起来MyContinuation实现的比较简单,因为很多的复杂的逻辑都封装在ContinuationImpl中了。下面我们尝试用一个更复杂的例子,然后自己实现ContinuationImpl,更完整来看下背后的逻辑。


在下面的例子中,suspend会更复杂,有参数,有返回。


suspend fun test(token: String) {
println("hello")
val userId = getUserId(token) // suspending
println("userId: $userId")
val userName = getUserName(userId) // suspending
println("id: $userId, name: $userName")
println("world")
}

编译器处理过的代码大致如下:


fun test(
token: String,
con: Continuation
): Any? {
val continuation = con as? MyContinuation
?: MyContinuation(con)

var result: Result<Any>? = continuation.result
var userId: String? = continuation.userId
val userName: String

if (continuation.label == 0) {
println("hello")
continuation.label = 1
val res = getUserId(token, continuation)
if (res == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
result = Result.success(res)
}
if (continuation.label == 1) {
userId = result.getOrThrow() as String
println("userId: $userId")
continuation.label = 2
continuation.userId = userId
val res = getUserName(userId, continuation)
if (res == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
result = Result.success(res)
}
if (continuation.label == 2) {
userName = result.getOrThrow() as String
println("id: $userId, name: $userName")
println("world")
return Unit
}
error("error")
}

MyContinuation的代码如下:


class MyContinuation(
val completion: Continuation<Unit>,
val token: String
) : Continuation<String> {
override val context: CoroutineContext
get() = completion.context

var label = 0
var result: Result<Any>? = null
var userId: String? = null

override fun resumeWith(result: Result<String>) {
this.result = result
val res = try {
val r = test(token, this)
if (r == COROUTINE_SUSPENDED) return
Result.success(r as Unit)
} catch (e: Throwable) {
Result.failure(e)
}
completion.resumeWith(res)
}
}

还记得函数调用,一般都是通过Stack来处理,局部变量和函数的返回之后继续执行的地址都存在stack frame中,这里Continuation的作用就相当于这个Stack了。


还有一个小小的知识点,suspend函数可以调用suspend,所以总有一个最初始suspend函数的吧,不会不就没止境了啊。最初始的那个suspend函数一定是从Callback转换而来的,这里具体可以查看上一节关于Callback转suspend函数的介绍。


以上较多参考了Coroutines under the hood这篇文章,并加入了一些自己的思考,很多都是自己的理解,肯定有错误和不足之处,也请指正。


Corountine Job.join()的一个大坑


这问题起源于我的另外一篇文章,应用程序启动优化新思路 - Kotlin协程,文章讲的是应用启动时,通过Kotlin协程的方案,简化多任务并行初始化的代码逻辑。其实这类问题具有普遍性,我现在举另外一个例子来说明。


做过Android系统开发的工程师一定知道,编译整个Android系统是耗时的,因为里面有至少有数百个模块,模块和模块之间也可能存在依赖关系。这里一般系统都是支持多线程并行编译的,那如何来使用多线程来组织这些模块的编译呢?


考虑一个最简单的例子,现在有5个build tasks,依赖关系如下:


WechatIMG37.jpeg


这个图是一个典型的有向无环图,按照拓扑排序的顺利来执行即可,下面考虑使用协程来多任务并行。首先,任务1和任务2没有被依赖,可以被启动,这里可以并行的执行。


suspend fun build() = coroutineScope {
// 调度协程

val job1 = launch(Dispatchers.Default) {
// start build task 1.
}

val job2 = launch(Dispatchers.Default) {
// start build task 2.
}
}

接下来有些难办,任务3,任务4,任务5都是有依赖的,但是我们没法知道任务1和任务2什么时候可以执行完成,所以我们使用了Kotlin协程系统的Join来进行等待。但是这里要注意,我们不能在调度协程里面进行对Job的Join操作。如以下的代码就会存在问题:


suspend fun build() = coroutineScope {
val job1 = launch(Dispatchers.Default) {
// start build task 1.
}

val job2 = launch(Dispatchers.Default) {
// start build task 2.
}

job1.join()
val job3 = launch(Dispatchers.Default) {
// start build task 3.
}
}

如果Task1很耗时,Task3需要等Task1完成之后执行,但是Task2很快就执行完了,可以安排Task5进行执行,如果在调度协程中进行Join,就会一直处于等待Task1执行完成,所以Join的等待不能在调度协程中,那怎么办呢? 我们可以在Task任务协程中进行等待,就可以解决这个问题了。如下面的代码。


suspend fun build() = coroutineScope {
val job1 = launch(Dispatchers.Default) {
// start build task 1.
}

val job2 = launch(Dispatchers.Default) {
// start build task 2.
}

val job3 = launch(Dispatchers.Default) {
job1.join()
// start build task 3.
}

val job4 = launch(Dispatchers.Default) {
job1.join()
job2.join()
// start build task 4.
}

val job5 = launch(Dispatchers.Default) {
job2.join()
// start build task 5.
}
}

以上代码运行良好,我们在测试的时候,所有的逻辑都按照我们所预想的方式执行。但是有一天,我们发现,有非常低的概率发生,这些任务会无法结束,也就是以上的build方法没办法返回,这是一个概率极低的事件,但是确实存在,哈哈,我们掉坑里去了。所以我们就去看了Join的源码,想看看到底发生了什么事情?首先,如果你看懂了上面关于Suspend的那节的话,你会清楚的知道Join是如何进行挂起的,重新恢复必然会走Continuation的resume方法。


以上是我们的大致的想法,然后我们来看一下Join函数到底干了什么?


public final override suspend fun join() {
if (!joinInternal()) { // fast-path no wait
coroutineContext.ensureActive()
return // do not suspend
}
return joinSuspend() // slow-path wait
}

上面Join函数两个分支,第一个分支的意思是,依赖的Job已经结束了,不需要等待了,可以执行返回了。第二个意思是依赖的任务还没有结束,我们需要等待。毫无疑问,我们出问题的代码是走的是第二个分支,那我们来看看第二个分支到底做了些什么?


private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
// We have to invoke join() handler only on cancellation, on completion we will be resumed regularly without handlers
cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(cont).asHandler))
}

哈哈,这不就是我们之前讨论的Callback转Suspend函数的代码吗?代码里面cont变量就是代表调用Join函数编译器加入的最后一个参数。我们可以看到,cont变量给了一个叫ResumeOnCompletion的类,那我们接着来看ResumeOnCompletion这个类的实现的吧。


private class ResumeOnCompletion(
private val continuation: Continuation<Unit>
) : JobNode() {
override fun invoke(cause: Throwable?) = continuation.resume(Unit)
}

我们找到了那个关键的代码了,continuation.resume(Unit),这个是Join函数返回最关键的代码了,所以我在这里这个函数上面下了断点,当函数执行到这里的时候,所有的调用栈清晰可见。原来是被依赖的Job里面有个list,里面放着所有这个Job的Join函数的ResumeOnCompletion,然后在Job结束的时候,会遍历这个list,然后执行resume函数,然后Join函数就会返回了。这里的返回只是感觉上的返回,如果你看了上面关于suspend的介绍的话,就会知道所谓的返回就是在新状态下从新执行了那个函数了。那这个ResumeOnCompletion是如何放到这个list的呢? 就是通过上面的invokeOnCompletion方法。如果需要更加细致的了解,可以自己调试一下这个代码。


2.png


说到这里,不知道大家是否意识到之前代码的问题所在了?


问题出现在,因为Join的代码有可能运行在另外的线程,所以当判断所依赖的任务没有结束,需要等待的时候,把自己的放到list的过程中,还没有放在list里面的那一刹那,Job刚好结束,然后通知list里面的任务可以重新开始了,但是那个任务刚好没有被放到list里面,所以一旦错过,就成了永远了。


所以吧,Kotlin的官方代码里面,所有的Join函数的执行,都是在launch这个Job的协程中执行的。一个协程,不同的时候,可能会运行在不同的线程上,但是一个协程本身是顺序执行的。


好吧,正确的代码如下:


suspend fun build() = coroutineScope {

val context = coroutineContext

val job1 = launch(Dispatchers.Default) {
// start build task 1.
}

val job2 = launch(Dispatchers.Default) {
// start build task 2.
}

val job3 = launch(Dispatchers.Default) {
withContext(context) {
job1.join()
}

// start build task 3.
}

val job4 = launch(Dispatchers.Default) {
withContext(context) {
job1.join()
job2.join()
}
// start build task 4.
}

val job5 = launch(Dispatchers.Default) {
withContext(context) {
job2.join()
}
// start build task 5.
}
}

以上的代码经过长时间的测试和验证,证明是可靠的。另外,如果想知道这个问题更详细的背景,请参看 应用程序启动优化新思路 - Kotlin协程


CoroutineContext vs CoroutineScope


这一节聊下在Kotlin协程中一些基本概念,这些知识点本身不难,但是对于初学者来说,比较容易搞混。下面尝试来试着说明。


首先,先来看一下CoroutineContext,这个比较好理解,就是协程的context。什么叫context,中文一般翻译成上下文,表示一些基本信息。对于Android Application的context,包含包名,版本信息,应用安装路径,应用的工作目录和缓存目录等等基本信息,是描述应用的一些基本信息的。同理协程的context当然就是协程的基本信息。CoroutineContext包含4类信息,如下:



  1. coroutineContext[Job],Job的作用是管理协程的生命周期,和父子协程的关系,可以通过获取。

  2. coroutineContext[ContinuationInterceptor],协程工作线程的管理。

  3. coroutineContext[CoroutineExceptionHandler],错误处理。

  4. coroutineContext[CoroutineName],协程的名字,一般用作调试。


CoroutineScope这个概念,最开始看的时候和CoroutineContext有点分不清楚。其实你看CoroutineScope的接口代码,里面就包含且仅包含CoroutineContext,本质上,他们其实是一个东西。为什么要设计CoroutineScope呢?虽然这两个本质上是同一个东西,但是他们有不同的设计目的。Context是管理来协程的基本信息,而Scope是用来管理协程的启动。


一般的协程通过launch来启动,launch设计成CoroutineScope的扩展函数,非常有意思的设计是,launch的最后一个参数,新协程的执行体也是一个CoroutineScope的扩展函数。launch函数的第一个参数是一个Context,launch会把第一个参数的Context和本身的Context合成一个新的Context,这个新的Context会用来生成新协程的Context,注意这里不是作为新协程的Context。为什么呢,因为新协程为生成一个Job,这个Job和这个Context合成之后,才是作为新协程的Context。


这里两个知识点要注意,所有的Context都是Immutable的,如果要修改一个Context,就会新生成一个新的Context。另外,launch的第一个参数,一般没有指定Job的,一旦指定Job的话,会破会两个协程的父子关系。除非你很确定你要这么做。


所有这些概念的东西,本质上不难,但是对于初学者来说,会感到一头雾水,要深入了解这些概念,需要先去了解一下设计者的设计思路,这样才可以做到事半功倍。


结尾


以上希望可以给大家一些帮助。另外文章免不了一些疏忽和错误,不吝指正。


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

AsyncTask源码分析

AsyncTask,Android 实现异步方式之一,即可以在子线程进行数据操作,然后在主线程进行 UI 操作 AsyncTask的简单使用 示例 同样的,我们先看看 AsyncTask 如何进行简单使用: AsyncTask<Boole...
继续阅读 »

AsyncTask,Android 实现异步方式之一,即可以在子线程进行数据操作,然后在主线程进行 UI 操作


AsyncTask的简单使用


示例


同样的,我们先看看 AsyncTask 如何进行简单使用:


        AsyncTask<Boolean, Integer, String> asyncTask = new AsyncTask<Boolean, Integer, String>() {

@Override
protected void onPreExecute() {
super.onPreExecute();
Log.i("TAG", "onPreExecute:正在执行前的准备操作");
}

@Override
protected String doInBackground(Boolean... booleans) {
Log.i("TAG", "doInBackground:获得参数" + booleans[0]);
for (int i = 0; i < 5; i++) {
publishProgress(i);
}
return "任务完成";
}

@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
Log.i("TAG", "onProgressUpdate:进度更新" + values[0]);
}

@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);

Log.i("TAG", "onPostExecute:" + s);
}
};
Log.i("TAG", "开始调用execute");
asyncTask.execute(true);

输出结果:


2020-04-02 17:14:42.029 4995-4995 I/TAG: 开始调用execute
2020-04-02 17:14:42.029 4995-4995 I/TAG: onPreExecute:正在执行前的准备操作
2020-04-02 17:14:42.030 4995-5118 I/TAG: doInBackground:获得参数true
2020-04-02 17:14:45.774 4995-4995 I/TAG: onProgressUpdate:进度更新0
2020-04-02 17:14:45.774 4995-4995 I/TAG: onProgressUpdate:进度更新1
2020-04-02 17:14:45.774 4995-4995 I/TAG: onProgressUpdate:进度更新2
2020-04-02 17:14:45.774 4995-4995 I/TAG: onProgressUpdate:进度更新3
2020-04-02 17:14:45.774 4995-4995 I/TAG: onProgressUpdate:进度更新4
2020-04-02 17:14:45.775 4995-4995 I/TAG: onPostExecute:任务完成

创建说明


首先,在创建 AsyncTask 的时候,需要传入三个泛型数据AsyncTask<Params, Progress, Result>,其分别对应着


   protected abstract Result doInBackground(Params... params);

    @MainThread
protected void onProgressUpdate(Progress... values) {
}

    @MainThread
protected void onPostExecute(Result result) {
}

从注解可以看出,onProgressUpdateonPostExecute是在主线程执行。


执行流程



  • AsyncTask 调用execute(Params... params)方法

  • onPreExecute() 被调用,该方法用于在执行后台操作前进行一些操作,例如:弹出个加载框等

  • doInBackground(Params... params) 被调用,该方法用于进行一些复杂的数据处理,例如数据库操作等

  • doInBackground进行操作的过程中,可以通过publishProgress(Progress... values)进行进度更新,从而自动调用onProgressUpdate(Progress... values)

  • doInBackground执行完毕后,返回数据,将会调用onPostExecute(Result result)


源码分析(源码只保留关键部分,并非全部源码)


AsyncTask创建


    public AsyncTask() {
this((Looper) null);
}

   public AsyncTask(@Nullable Looper callbackLooper) {
//创建Handler,默认使用主线程的 Looper
mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
? getMainHandler()
: new Handler(callbackLooper);

//后面这段代码看起来有点长,其实就是使用了 Future 模式,
//先建立一个类继承 Callable 接口,再将该类赋值到 FutureTask 中,
//至于 call() 和 done() 方法里面具体内容可以先不用理会
//等 mFuture 被线程调用的时候,就会调用 call()
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
···
return result;
}
};

mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
···
}
};
}

InternalHandler解析


着重看下getMainHandler()


    private static Handler getMainHandler() {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler(Looper.getMainLooper());
}
return sHandler;
}
}

private static class InternalHandler extends Handler {
public InternalHandler(Looper looper) {
super(looper);
}

@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}

在创建 InternalHandler 的时候,传入了Looper.getMainLooper(),说明了 InternalHandler 的handleMessage方法可以执行 UI 操作。


我们在仔细看看handleMessage里面有两种处理:


result.mTask.finish(result.mData[0]);


result.mTask.onProgressUpdate(result.mData);

其中,result 即为 AsyncTaskResult<?>


    private static class AsyncTaskResult<Data> {
final AsyncTask mTask;
final Data[] mData;

AsyncTaskResult(AsyncTask task, Data... data) {
mTask = task;
mData = data;
}
}

所以result.mTask其实就是 AsyncTask,result.mTask.finish就是调用 AsyncTask 的 finish 方法:


    private void finish(Result result) {
//判断当前 AsyncTask 是否已被取消,已取消则调用 onCancelled,未取消则调用 onPostExecute
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}

由此,可以证明onPostExecute在 UI 线程执行


result.mTask.onProgressUpdate(result.mData);就更容易理解了,直接调用onProgressUpdate


    @MainThread
protected void onProgressUpdate(Progress... values) {
}

由此,可以证明onProgressUpdate也在 UI 线程执行


AsyncTask执行


asyncTask.execute(true);


    @MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}

// sDefaultExecutor 为线程池
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    @MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
//判断当前 AsyncTask 的运行状态,假如运行状态为 RUNNING 或者 FINISHED,则直接报错
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}

mStatus = Status.RUNNING;

onPreExecute();

mWorker.mParams = params;
exec.execute(mFuture);

return this;
}

从以上代码可以得出:



  • AsyncTask 内部使用了线程池进行线程操作

  • 每个 AsyncTask 对象只能执行一次!!!


???


使用线程池,却一个对象只能执行一次?这两个不是互相矛盾的吗?众所周知,线程池都是为了管理多线程而存在的。


我们再来仔细看下


 private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

static volatilestatic final,说明默认实现 AsyncTask 的对象都是共用该线程池,也就是说,所有使用 AsyncTask 默认生成方式,以及继承 AsyncTask 的类使用 AsyncTask 默认生成方式,他们的线程执行都是共用一个线程池,这就为什么 AsyncTask 里面使用线程池的原因。


好了,我们重新回来看看executeOnExecutor(Executor exec, Params... params)


      onPreExecute();

mWorker.mParams = params;
exec.execute(mFuture);

首先,先执行onPreExecute(),其次使用线程池调用mFuture


关于mFuture,重点为call()


  mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
//调用doInBackground
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
//调用postResult
postResult(result);
}
return result;
}
};

从这里可以看出,doInBackground是被线程池调用的时候执行的,也就是说,doInBackground在子线程中执行。而外,我们看看postResult(result)


    private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}

发送了一个MESSAGE_POST_RESULT消息,也就是执行刚刚我们分析过的


        case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;

至此,onPreExecutedoInBackgroundonPostExecute的一个流程我们已经分析完了,大概流程如下:


execute(AsyncTask被调用的线程)-->onPreExecute(AsyncTask被调用的线程)-->doInBackground(子线程)-->onPostExecute(UI线程)


onProgressUpdate


剩下,我们来看遗漏的onProgressUpdate


首先,onProgressUpdate要被调用的话,需要先调用publishProgress


    @WorkerThread
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}

其实就是使用InternalHandler发送MESSAGE_POST_PROGRESS


             case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;

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

Android与JavaScript交互上(获取Html内容)

在Android开发中,一般通过WebView实现与JavaScript的交互(还有其他更高级的方法!)。WebView用于加载网页内容,如果需要对该网页进行交互操作,可以通过添加一个 JavascriptInterface 交互对象,在恰当的时候调用Js语句...
继续阅读 »

在Android开发中,一般通过WebView实现与JavaScript的交互(还有其他更高级的方法!)。WebView用于加载网页内容,如果需要对该网页进行交互操作,可以通过添加一个
JavascriptInterface
交互对象,在恰当的时候调用Js语句运行接口中对应的方法,进行交互操作。


以交互掘金页面为例子。



  • 开启JavaScript:拿到webView后设置javaScriptEnabled=true,开启JavaScript。

  • 设置UserAgent:userAgentString非必需,如果需要对网页的某个元素进行解析,最好进行设置。因为一般网页会对不同浏览器进行适配,使得浏览器之间的Html代码会有差异,而导致解析失败。设置方法:电脑浏览器打开目标网页,F12开启后台模式,左上角切换到手机模式,找到user-agent(具体见下图)复制粘贴。

  • 设置交互对象:webView.addJavascriptInterface(JavaOBjectJsInterface(),"testJs")添加一个用于交互的对象,交互方法在类JavaOBjectJsInterface()中,testJs是一个自定义的名字。Js语句通过testJs调用到JavaOBjectJsInterface()内的方法。

  • 设置webClient开启交互:重写onPageFinished(view, url)方法。该方法在网页加载完成时调用,一般这个时候可以拿到完整的网页Html代码。Js语句document.getElementsByTagName('body')[0].innerHTML可以拿到网页<body>块代码,将该代码作为参数传入交互方法test(html)中。webView运行javascript:window.testJs.test(document.getElementsByTagName('body')[0].innerHTML,通过上一步设置交互对象传入的testJs调用到接口对象中的test(html)方法,实现交互。

  • 实现交互:test(html)中,通过Jsoup(一个网页解析框架)对<body>块代码进行解析,通过在网页端获得的按钮class名获取到目标元素。


val link="https://juejin.cn/?utm_source=gold_browser_extension"
val webView=findViewById<WebView>(R.id.webview)
webView.settings.run {
javaScriptEnabled=true //开启Js
userAgentString="Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Mobile Safari/537.36"
useWideViewPort=true
loadWithOverviewMode=true
setSupportZoom(true)
javaScriptCanOpenWindowsAutomatically=true
loadsImagesAutomatically=true
defaultTextEncodingName="utf - 8"
}
webView.webViewClient=object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
webView.loadUrl("javascript:window.testJs.test(document.getElementsByTagName('body')[0].innerHTML);")
}
}
webView.addJavascriptInterface(JavaOBjectJsInterface(),"testJs")
webView.loadUrl(link)

class JavaOBjectJsInterface {

private val TAG = JavaOBjectJsInterface::class.java.simpleName

@JavascriptInterface
fun test(html: String) {
val document = Jsoup.parse(html)
val btn=document.getElementsByClass("seach-icon-container")
Log.d(TAG, "btn:" + btn);
}
}

截屏2022-05-27 下午11.49.08.png
获取目标网页UserAgent


截屏2022-05-27 下午11.55.24.png
运行结果


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

90%的人都不懂的泛型,泛型的缺陷和应用场景

Hi 大家好,我是 DHL。公众号:ByteCode ,专注分享有趣硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经 全文分为 视频版 和 文字版, 文字版: 文字侧重细节和深度,有些知识点,视频不好表达,文字...
继续阅读 »

Hi 大家好,我是 DHL。公众号:ByteCode ,专注分享有趣硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经



全文分为 视频版文字版



  • 文字版: 文字侧重细节和深度,有些知识点,视频不好表达,文字描述的更加准确

  • 视频版: 视频会更加的直观,看完文字版,在看视频,知识点会更加清楚


视频版 bilibili 地址:https://b23.tv/AdLtUGf


泛型对于每个开发者而言并不陌生,平时在项目中会经常见到,但是有很多小伙伴们,每次见到通配符 ? extends? superoutin 都傻傻分不清楚它们的区别,以及在什么情况下使用。


通过这篇文章将会学习的到以下内容。



  • 为什么要有泛型

  • Kotlin 和 Java 的协变

  • Kotlin 和 Java 的逆变

  • 通配符 ? extends? superoutin 的区别和应用场景

  • Kotlin 和 Java 数组协变的不同之处

  • 数组协变的缺陷

  • 协变和逆变的应用场景


为什么要有泛型


在 Java 和 Kotlin 中我们常用集合( ListSetMap 等等)来存储数据,而在集合中可能存储各种类型的数据,现在我们有四种数据类型 IntFloatDoubleNumber,假设没有泛型,我们需要创建四个集合类来存储对应的数据。


class IntList{ }
class Floatlist{}
class DoubleList{}
class NumberList{}
......
更多

如果有更多的类型,就需要创建更多的集合类来保存对应的数据,这显示是不可能的,而泛型是一个 "万能的类型匹配器",同时有能让编译器保证类型安全。


泛型将具体的类型( IntFloatDouble 等等)声明的时候使用符号来代替,使用的时候,才指定具体的类型。


// 声明的时候使用符号来代替
class List<E>{
}

// 在 Kotlin 中使用,指定具体的类型
val data1: List<Int> = List()
val data2: List<Float> = List()

// 在 Java 中使用,指定具体的类型
List<Integer> data1 = new List();
List<Float> data2 = new List();

泛型很好的帮我们解决了上面的问题,但是随之而来出现了新的问题,我们都知道 IntFloatDoubleNumber 子类型, 因此下面的代码是可以正常运行的。


// Kotlin
val number: Number = 1

// Java
Number number = 1;

我们花三秒钟思考一下,下面的代码是否可以正常编译。


List<Number> numbers = new ArrayList<Integer>();

答案是不可以,正如下图所示,编译会出错。



这也就说明了泛型是不可变的,IDE 认为 ArrayList<Integer> 不是 List<Number> 子类型,不允许这么赋值,那么如何解决这个问题呢,这就需要用到协变了,协变允许上面的赋值是合法的。


Kotlin 和 Java 的协变



  • 在 Java 中用通配符 ? extends T 表示协变,extends 限制了父类型 T,其中 ? 表示未知类型,比如 ? extends Number,只要声明时传入的类型是 Number 或者 Number 的子类型都可以

  • 在 Kotlin 中关键字 out T 表示协变,含义和 Java 一样


现在我们将上面的代码修改一下,在花三秒钟思考一下,下面的代码是否可以正常编译。


// kotlin
val numbers: MutableList<out Number> = ArrayList<Int>()

// Java
List<? extends Number> numbers = new ArrayList<Integer>();

答案是可以正常编译,协变通配符 ? extends Number 或者 out Number 表示接受 Number 或者 Number 子类型为对象的集合,协变放宽了对数据类型的约束,但是放宽是有代价的,我们在花三秒钟思考一下,下面的代码是否可以正常编译。


// Koltin
val numbers: MutableList<out Number> = ArrayList<Int>()
numbers.add(1)

// Java
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(1)

调用 add() 方法会编译失败,虽然协变放宽了对数据类型的约束,可以接受 Number 或者 Number 子类型为对象的集合,但是代价是 无法添加元素,只能获取元素,因此协变只能作为生产者,向外提供数据。


为什么无法添加元素


因为 ? 表示未知类型,所以编译器也不知道会往集合中添加什么类型的数据,因此索性不允许往集合中添加元素。


但是如果想让上面的代码编译通过,想往集合中添加元素,这就需要用到逆变了。


Kotlin 和 Java 的逆变


逆变其实是把继承关系颠倒过来,比如 IntegerNumber 的子类型,但是 Integer 加逆变通配符之后,Number? super Integer 的子类,如下图所示。




  • 在 Java 中用通配符 ? super T 表示逆变,其中 ? 表示未知类型,super 主要用来限制未知类型的子类型 T,比如 ? super Number,只要声明时传入是 Number 或者 Number 的父类型都可以

  • 在 Kotlin 中关键字 in T 表示逆变,含义和 Java 一样


现在我们将上面的代码简单修改一下,在花三秒钟思考一下是否可以正常编译。


// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)

// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);

答案可以正常编译,逆变通配符 ? super Number 或者关键字 in 将继承关系颠倒过来,主要用来限制未知类型的子类型,在上面的例子中,编译器知道子类型是 Number,因此只要是 Number 的子类都可以添加。


逆变可以往集合中添加元素,那么可以获取元素吗?我们花三秒钟时间思考一下,下面的代码是否可以正常编译。


// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
numbers.get(0)

// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
numbers.get(0);

无论调用 add() 方法还是调用 get() 方法,都可以正常编译通过,现在将上面的代码修改一下,思考一下是否可以正常编译通过。


// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
val item: Int = numbers.get(0)

// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
int item = numbers.get(0);

调用 get() 方法会编译失败,因为 numbers.get(0) 获取的的值是 Object 的类型,因此它不能直接赋值给 int 类型,逆变和协变一样,放宽了对数据类型的约束,但是代价是 不能按照泛型类型读取元素,也就是说往集合中添加 int 类型的数据,调用 get() 方法获取到的不是 int 类型的数据。


对这一小节内容,我们简单的总结一下。

























关键字(Java/Kotlin)添加读取
协变? extends / out
逆变? super / in

Kotlin 和 Java 数组协变的不同之处


无论是 Kotlin 还是 Java 它们协变和逆变的含义的都是一样的,只不过通配符不一样,但是他们也有不同之处。


Java 是支持数组协变,代码如下所示:


Number[] numbers = new Integer[10];

但是 Java 中的数组协变有缺陷,将上面的代码修改一下,如下所示。


Number[] numbers = new Integer[10];
numbers[0] = 1.0;

可以正常编译,但是运行的时候会崩溃。



因为最开始我将 Number[] 协变成 Integer[],接着往数组里添加了 Double 类型的数据,所以运行会崩溃。


而 Kotlin 的解决方案非常的干脆,不支持数组协变,编译的时候就会出错,对于数组逆变 Koltin 和 Java 都不支持。


协变和逆变的应用场景


协变和逆变应用的时候需要遵循 PECS(Producer-Extends, Consumer-Super)原则,即 ? extends 或者 out 作为生产者,? super 或者 in 作为消费者。遵循这个原则的好处是,可以在编译阶段保证代码安全,减少未知错误的发生。



协变应用



  • 在 Java 中用通配符 ? extends 表示协变

  • 在 Kotlin 中关键字 out 表示协变


协变只能读取数据,不能添加数据,所以只能作为生产者,向外提供数据,因此只能用来输出,不用用来输入。


在 Koltin 中一个协变类,参数前面加上 out 修饰后,这个参数在当前类中 只能作为函数的返回值,或者修饰只读属性 ,代码如下所示。


// 正常编译
interface ProduceExtends<out T> {
val num: T // 用于只读属性
fun getItem(): T // 用于函数的返回值
}

// 编译失败
interface ProduceExtends<out T> {
var num : T // 用于可变属性
fun addItem(t: T) // 用于函数的参数
}

当我们确定某个对象只作为生产者时,向外提供数据,或者作为方法的返回值时,我们可以使用 ? extends 或者 out



  • 以 Kotlin 为例,例如 Iterator#next() 方法,使用了关键字 out,返回集合中每一个元素




  • 以 Java 为例,例如 ArrayList#addAll() 方法,使用了通配符 ? extends



传入参数 Collection<? extends E> c 作为生产者给 ArrayList 提供数据。


逆变应用



  • 在 Java 中使用通配符 ? super 表示逆变

  • 在 Kotlin 中使用关键字 in 表示逆变


逆变只能添加数据,不能按照泛型读取数据,所以只能作为消费者,因此只能用来输入,不能用来输出。


在 Koltin 中一个逆变类,参数前面加上 in 修饰后,这个参数在当前类中 只能作为函数的参数,或者修饰可变属性


// 正常编译,用于函数的参数
interface ConsumerSupper<in T> {
fun addItem(t: T)
}

// 编译失败,用于函数的返回值
interface ConsumerSupper<in T> {
fun getItem(): T
}

当我们确定某个对象只作为消费者,当做参数传入时,只用来添加数据,我们使用通配符 ? super 或者关键字 in



  • 以 Kotlin 为例,例如扩展方法 Iterable#filterTo(),使用了关键字 in,在内部只用来添加数据




  • 以 Java 为例,例如 ArrayList#forEach() 方法,使用了通配符 ? super



不知道小伙伴们有没有注意到,在上面的源码中,分别使用了不同的泛型标记符 TE,其实我们稍微注意一下,在源码中有几个高频的泛型标记符 TEKV 等等,它们分别应用在不同的场景。



























标记符应用场景
T(Type)
E(Element)集合
K(Key)
V(Value)



全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!


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

网传互联网公司加班表,排名第一的没有悬念!

【0】网传互联网公司加班表【1】互联网工作生存指南【2】据说,互联网公司员工上课座位如下【3】当代互联网企业真实写照【4】互联网公司排位,你同意吗?【5】网传当代互联网公司内部结构【6】互联网公司各岗位口头禅(图自:微博@刘兴亮 )【7】最真实的现代互联网商业...
继续阅读 »

【0】

网传互联网公司加班表

4209aebdf503d80ac07808f2d06054d5.png

【1】

互联网工作生存指南

8ed5a3bfe414e047074088754de041a8.png

【2】

据说,互联网公司员工上课座位如下

2863a5064f00fddd237522ffde6d979d.png

【3】

当代互联网企业真实写照

e9d55f607cc12e3cd428d3c52333e690.png

【4】

互联网公司排位,你同意吗?

c6a383d49e10a13871233e7777be4fee.png

【5】

网传当代互联网公司内部结构

a27e05bd4b1fc35d97797b21259cf5bb.png

【6】

互联网公司各岗位口头禅

46cb51aef84d88099f803ca858181dbf.png

(图自:微博@刘兴亮 )

【7】

最真实的现代互联网商业模式

9c527815f0172d2e36219e8f4c36588c.png

【8】

听说这是996互联网公司员工的标配?

bdd22a9052781b3783c3a52d41b04c8a.png

【9】

互联网公司各部门眼中的对方

f9dbbdd79e3e72e7348b4cb9d9e309b8.png

【10】

说不出三人以上姓名,

别说你是混互联网的。

9e97c7c1904cb241eff94e5cc85f73d3.png

来源:humor1024

收起阅读 »

vue-cli3 一直运行 /sockjs-node/info?t= 解决方案

首先 sockjs-node 是一个JavaScript库,提供跨浏览器JavaScript的API,创建了一个低延迟、全双工的浏览器和web服务器之间通信通道。服务端:sockjs-node(https://github.com/sock...
继续阅读 »

首先 sockjs-node 是一个JavaScript库,提供跨浏览器JavaScript的API,创建了一个低延迟、全双工的浏览器和web服务器之间通信通道。

服务端:sockjs-node(https://github.com/sockjs/sockjs-node)
客户端:sockjs-clien(https://github.com/sockjs/sockjs-client)

如果你的项目没有用到 sockjs,vuecli3 运行 npm run serve 之后 network 里面一直调研一个接口:http://localhost:8080/sockjs-node/info?t=1462183700002

作为一个有节操的程序猿,实在不能忍受,特意自己研究了下源码,从根源上关闭这个调用

1. 找到/node_modules/sockjs-client/dist/sockjs.js 

2.找到代码的 1605行  

try {
// self.xhr.send(payload); 把这里注掉
} catch (e) {
self.emit('finish', 0, '');
self._cleanup(false);
}

3.刷新,搞定。

原文:https://www.cnblogs.com/sichaoyun/p/10178080.html


收起阅读 »

外卖小哥帮程序员改代码,是正能量还是资本盘剥下的悲剧?

近日,青岛一外卖小哥大晚上主动帮助一个崩溃程序员修改代码的新闻上了热搜。 据了解,一男子和朋友来酒吧看欧洲杯球赛,途中多次离开接电话,最后不得已抱着电脑坐在酒吧门口加班而情绪崩溃,路过送餐的外卖小哥见状,主动上前帮他改代码。看了这条新闻后,很多善良单...
继续阅读 »

近日,青岛一外卖小哥大晚上主动帮助一个崩溃程序员修改代码的新闻上了热搜。 


据了解,一男子和朋友来酒吧看欧洲杯球赛,途中多次离开接电话,最后不得已抱着电脑坐在酒吧门口加班而情绪崩溃,路过送餐的外卖小哥见状,主动上前帮他改代码。

看了这条新闻后,很多善良单纯的网友纷纷评论说:“外卖小哥太暖心了,外卖小哥好赞,好温暖哦!中国的外卖小哥太牛了......”。

但也有很多网友与我一样,十分清醒,并不会被这个新闻的假象所欺骗。

首先,这条新闻的真实性是很存疑的:这个外卖小哥恰巧就是最后一单了吗,可以不管后续的单子了?即使外卖小哥是个高手,他就不用和程序员小哥先沟通一下项目吗,直接拿过电脑就上手敲代码了?


因此,我们并不能排除这个视频本身就是摆拍的,以此来博取大众眼球,给“美团外卖”做热点营销、打广告。

再者,即使这条新闻是真的,我们是否值得为这暖心的一幕而感动和开心呢?

很多网友会说:“一个外卖小哥本身工作很辛苦,但他还在工作之余很热情的帮助他人,这样的行为难道不暖心吗?!”

从这个新闻里,我们当然能够看到作为普通劳动者一员的外卖小哥身上的朴实、热情与善良,我们固然要为他的行为点赞。


但我无论如何也开心不起来,因为这件事本身恰恰说明了劳动者的地位低下,而其成为热点事件的背后更隐含着资本对于劳动者反抗意识的消解!

现在,互联网公司都推行“加班文化”,程序员加班早已成为了常态,程序员因为压力大而自杀、猝死的新闻更是屡见不鲜。

在这件事中,我们首先应该看到的是,这位程序员崩溃的事实以及崩溃背后所代表的“劳动者被资本无情盘剥的事实”。

这位程序员上班的时候,每天面对着电脑高强度地思考着程序实现的算法,耗费巨大精力敲着一行又一行的代码,很累很疲惫。他有一个看足球赛的爱好,就想着下班后能去释放压力,看他想看的比赛。

结果,好不容易下班了,领导一而再、再而三的发消息、打电话:说项目有很多问题,让赶紧加班搞项目。于是,他不得不放弃自己最喜爱的球赛,

面对这样的状况,他内心必然是非常难受和痛苦的。因为工作正在侵蚀着他全部的生活,除了无聊的、单调的工作外,他几乎没有任何自己的时间。 

这位程序员面临的情况不仅是万千程序员的真实写照,更是无数普通劳动者被压榨的缩影!

现在,我们再来看这个新闻中的另一个人物,也是让许多人感动、赞叹不已的人——外卖小哥。

在假设此新闻真实可靠的前提下,我从这位外卖小哥身上看到的绝不是社会正能量,而恰恰是资本社会下的一个悲剧人生。

外卖小哥到底是如何学会程序算法的呢?亦或者说在进入外卖行业前他也是一名程序员?但不管他是如何学会程序语言和程序算法的,他既然有这么强的程序语言能力,却又为何会成为一名外卖小哥呢?这难道不是对人才的埋没和浪费吗? 

在资本家的眼里,只有利润是最可靠、最值得追求的东西,他所做的一切皆为此服务,人的全面发展与生活的尊严从来就不是他们需要考虑的内容。

如果外卖小哥是自学成才,那首先他就会在学历上被资本公司所淘汰,而即使是资本家将他录用,也会在在用完“年轻的他”之后果断将其抛弃。

总而言之,“编程高手”送外卖不是一件我们值得赞扬和惊奇的事情,“编程高手”为什么会成为外卖小哥才更应该值得我们关注。 


某些媒体将这样一条“温暖”的新闻曝光并大肆宣传,强调着外卖小哥的贴心与善良,既使得一大波不明真相的吃瓜群众沉醉在“社会真美好”的臆想中,同时又借此收割一大波流量。

现如今,资本一边利用人工智能、物联网技术、大数据手段对劳动者进行严格的监管、残酷的剥削;另一方面又善于利用媒体宣传“正能量”,试图通过一些温情脉脉的内容来掩盖jj剥削的实质;此外,也善于利用慈善手段给予穷苦劳动者一些小恩小惠,以此来宣扬社会的美好与富人的善心,达到欺骗穷人、忽悠劳动者的目的!

 

虽然,上一轮的社会主义革命高潮早已远去,但也倒逼着资本家们变得更加“文明”,这“文明”中依然是赤裸裸的剥削,这“文明”中充满着铜臭味儿与虚伪。

今天,作为劳动者,我们应打起十二分的警惕,不要被资本家们的伪善所欺骗。

请记得,“我们走后,他们会给你们修学校和医院,会提高你们的工资,这不是因为他们良心发现,也不是因为他们变成了好人,而是因为我们来过。“——切格瓦拉! 



作者:python学习小帮手 https://www.bilibili.com/read/cv12732001 出处:bilibili


收起阅读 »

Java并发系列:详解Synchronized关键字

一、简介 为了提高效率,出现了多线程并发执行,并发执行处理共享变量就会带来安全性问题。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对...
继续阅读 »

一、简介


为了提高效率,出现了多线程并发执行,并发执行处理共享变量就会带来安全性问题。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉。


二、synchronized实现原理


2.1 monitor机制


2.1.1 Java对象头


Java中每个对象都有他的对象头,并且synchronized用的锁是存在对象头中的。
在这里插入图片描述



  • Klass Word 指明对象的类型

  • Mark Word 存储hashCode或锁信息


在这里插入图片描述
在这里插入图片描述



  • 对于数组对象,还需要32位存储数组长度(32位虚拟机);

  • 所以在java中,int类型和Integer类型所占的大小是不同的,int占4个字节,Integer是对象,本身8个字节和存储的值4个字节,总共占12个字节;


2.1.2 monitor


在这里插入图片描述



  • 线程去执行临界区代码;

  • Thread-2先获得锁,成为monitor的主人,其他线程都要到blocked队列中等待;


在这里插入图片描述



  • 不加synchronized关键字,对象不会关联monitor,也就不会有上述情况。也就是说,被加锁的对象,才会关联monitor,那么多个线程操作这个对象,就会有上述情况


2.2 synchronized内存语义


synchronized内存语义其实可以认为是锁的内存语义,也即锁的释放-获取的内存语义(个人理解,如有错误请指正)
在锁释放后,会将共享变量刷新到主内存中,保证其可见性,这里和volatile类似。


同理 synchronized的happens-before关系也就是JMM中happens-before规则中的监视器锁规则:对同一个锁的解锁,happens-before于对该锁的加锁。 同时其又与volatile的happens-before规则有相同的内存语义。
详细解释不再赘述。


三、synchronized拓展优化


3.1 CAS操作


链接: Java并发系列:CAS操作


3.2 锁升级


Java线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的介入,需要在用户态和内核态转换。在Java早期版本中(Java5前),synchronized属于重量级锁,因为监视器锁是依赖于底层的操作系统的互斥量来实现的,挂起线程和恢复线程都要转到内核态去完成,非常消耗资源,因此引入偏向锁、轻量级锁,尽量避免用户态到内核态的切换
在这里插入图片描述
在这里插入图片描述


==线程的访问方式==:



  • 只有一个线程来访问,有且唯一Only One

  • 有两个线程(两个线程交替访问)

  • 多个线程访问,竞争激烈


锁指向:在这里插入图片描述


3.2.1 偏向锁


==偏向锁:单线程竞争下==
在这里插入图片描述
当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问时便会自动获得锁。


在这里插入图片描述
在这里插入图片描述


线程并不会主动放弃偏向锁


在这里插入图片描述
偏向锁开启:
在这里插入图片描述



  • 延迟时间为4秒;


偏向锁的撤销
当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为欸轻量级锁。竞争线程尝试CAS更新对象头但是失败了,那么会等待到==全局安全点==撤销偏向锁。全局安全的也即该时间点上没有字节码执行。
在这里插入图片描述



  • 1属于上面所述的竞争失败,2属于竞争成功

  • 升级为轻量级锁后,另一个线程在外面自旋,如果成功则进入,不成功则继续自旋。如果自旋次数太多,可能升级为重量级锁。


注:Java15后逐步废弃偏向锁,原因是维护成本过高,(JVM也在不断更新)


3.2.2 轻量级锁


两个线程,交替运行(基本上是轮流执行),锁的竞争不激烈,不必升级到重量级锁去阻塞线程。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Java6之后使用自适应自旋锁
在这里插入图片描述
轻量级锁和偏向锁区别:
在这里插入图片描述


3.2.3 重量级锁


在这里插入图片描述


四、参考资料


1、Java并发编程的艺术 方腾飞等著;
2、黑马JUC编程;
3、大厂学苑JUC
4、深入理解Java虚拟机


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

Java异常体系和分类

🥞异常概念 异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是: 异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。 在Java等面向对象的编...
继续阅读 »

🥞异常概念


异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是:



  • 异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。


在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。



异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.



🥪异常体系


异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception
异常体系.png
Throwable体系:



  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。

  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。


Throwable中的常用方法:



  • public void printStackTrace():打印异常的详细信息。


包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。



  • public String getMessage():获取发生异常的原因。


提示给用户的时候,就提示错误原因。



  • public String toString():获取异常的类型和异常描述信息(不用)。


出现异常,不要紧张,把异常的简单类名,拷贝到API中去查。


🍿异常分类


我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。
异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?



  • 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)

  • 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)


异常的分类.png


🍝异常的产生过程解析


先运行下面的程序,程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生的过程。
工具类


public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}

测试类


public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
intnum = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}

上述程序执行过程图解:
异常产生过程.png


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

北京某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开发。