设计模式之状态模式与备忘录模式详解和应用


1.状态模式

1.1 目标

1、 掌握状态模式和备忘录模式的应用场景。

2、 了解状态机实现订单状态流转控制的过程

3、 掌握状态模式和策略模式的区别。

4、 掌握状态模式和责任链模式的区别。

5、 掌握备忘录模式在落地实战中的压栈管理。

1.2 内容定位

1、如果参与电商订单业务开发的人群,可以重点关注状态模式。

2、如果参与富文本编辑器开发的人群,可以重点关注备忘录模式。

1.3.迭代器模式

状态模式( State Pattern ) 也称为状态机模式(State Machine Pattern), 是允许对象在内 部状态发生改变时改变它的行为,对象看起来好像修改了它的类,属于行为型模式。

原文:Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
解释:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

状态模式中类的行为是由状态决定的,不同的状态下有不同的行为。其意图是让一个对象在 其内部改变的时候,其行为也随之改变。状态模式核心是状态与行为绑定,不同的状态对应不 同的行为。

1.4 应用场景

状态模式在生活场景中也还比较常见。例如:我们平时网购的订单状态变化。另外,我们平 时坐电梯,电梯的状态变化。

在软件开发过程中,对于某一项操作,可能存在不同的情况。通常处理多情况问题最直接的 方式就是使用if…else或 switch…case条件语句进行枚举。但是这种做法对于复杂状态的判断 天然存在弊端:条件判断语句过于臃肿,可读性差,且不具备扩展性,维护难度也大。而如果 转换思维,将这些不同状态独立起来用各个不同的类进行表示,系统处于哪种情况,直接使用 相应的状态类对象进行处理,消除了 if-else , switch…case等冗余语句,代码更有层次性且具 备良好扩展力。

状态模式主要解决的就是当控制一个对象状态的条件表达式过于复杂时的情况。通过把状态 的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。对象的行为依赖 于它的状态(属性),并且会根据它的状态改变而改变它的相关行为。状态模式适用于以下场景 :

1、 行为随状态改变而改变的场景;

2、 一个操作中含有庞大的多分支结构,并且这些分支取决于对象的状态。

首先来看下状态模式的通用UML类图:

在这里插入图片描述

从 UML类图中,我们可以看到,状态模式主要包含三种角色:

1、环境类角色(Context):定义客户端需要的接口,内部维护一个当前状态实例,并负责 具体状态的切换

2、抽象状态角色(State ):定义该状态下的行为,可以有一个或多个行为;

3、具体状态角色(ConcreteState ):具体实现该状态对应的行为,并且在需要的情况下进行状态切换。

1.5 状态模式在业务场景中的应用

我们在GPer社区阅读文章时,如果觉得文章写的很好,我们就会评论、收藏两连发。如果 处于登录情况下,我们就可以直接做评论,收藏这些行为。否则,跳转到登录界面,登录后再 继续执行先前的动作。这里涉及的状态有两种:登录与未登录,行为有两种:评论,收满。下 面我们使状态模式来实现一下这个逻辑,代码如下。

首先创建抽象状态角色UserState类:

 public abstract class UserState {
     protected AppContext context;
 
     public void setContext(AppContext context) {
         this.context = context;
     }
 
     public abstract void favorite();
 
     public abstract void comment(String comment);
 }

然 后 ,创建登录状态LogInState类 :

 public class LoginState extends UserState {
     @Override
     public void favorite() {
         System.out.println("收藏成功!");
     }
 
     @Override
     public void comment(String comment) {
         System.out.println(comment);
     }
 }

创建未登录状态UnloginState类 :

 public class UnLoginState extends UserState {
 
     @Override
     public void favorite() {
         this.switch2login();
         this.context.getState().favorite();
     }
 
     @Override
     public void comment(String comment) {
         this.switch2login();
         this.context.getState().comment(comment);
     }
 
     private void switch2login(){
         System.out.println("跳转到登录页!");
         this.context.setState(this.context.STATE_LOGIN);
     }
 }

创建上下文角色AppContext类 :

 public class AppContext {
 
     public static final UserState STATE_LOGIN = new LoginState();
     public static final UserState STATE_UNLOGIN = new UnLoginState();
 
     private UserState currentState = STATE_UNLOGIN;
 
     {
         STATE_LOGIN.setContext(this);
         STATE_UNLOGIN.setContext(this);
     }
 
     public void setState(UserState state){
         this.currentState = state;
     }
 
     public UserState getState(){
         return this.currentState;
     }
 
     public void favorite(){
         this.currentState.favorite();
     }
 
     public void comment(String comment){
         this.currentState.comment(comment);
     }
 }

编写客户端测试代码:

 public class Test {
     public static void main(String[] args) {
         AppContext context = new AppContext();
         context.favorite();
         context.comment("评论:好文章,360个赞");
     }
 }

运行结果如下:

在这里插入图片描述

1.6 利用状态机实现订单状态流转控制

状态机是状态模式的一种应用,相当于上下文角色的一个升级版。在工作流或游戏等各种系 统中有大量使用,如各种工作流引擎,它几乎是状态机的子集和实现,封装状态的变化规则。 Spring也提供给了我们一个很好的解决方案。Spring中的组件名称就叫StateMachine(状态机)。状态机帮助开发者简化状态控制的开发过程,让状态机结构更加层次化。下面,我们用 Spring状态机模拟一个订单状态流转的过程。

 <dependency>
     <groupId>org.springframework.statemachine</groupId>
     <artifactId>spring-statemachine-core</artifactId>
     <version>2.0.1.RELEASE</version>
 </dependency>

1、创建订单实体Order类

 @Data
 public class Order {
     private int id;
     private OrderStatus status;
 
     @Override
     public String toString() {
         return "订单号:" + id + ", 订单状态:" + status;
     }
 }

2、创建订单状态枚举类和状态转换枚举类

 public enum OrderStatus {
     // 待支付,待发货,待收货,订单结束
     WAIT_PAYMENT, WAIT_DELIVER, WAIT_RECEIVE, FINISH;
 }
public enum OrderStatusChangeEvent {
    // 支付,发货,确认收货
    PAYED, DELIVERY, RECEIVED;
}

3、添加状态流转配置

/**
 * 订单状态机配置
 */
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatus, OrderStatusChangeEvent> {
 
    /**
     * 配置状态
     * @param states
     * @throws Exception
     */
    public void configure(StateMachineStateConfigurer<OrderStatus, OrderStatusChangeEvent> states) throws Exception {
        states
                .withStates()
                .initial(OrderStatus.WAIT_PAYMENT)
                .states(EnumSet.allOf(OrderStatus.class));
    }
 
    /**
     * 配置状态转换事件关系
     * @param transitions
     * @throws Exception
     */
    public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderStatusChangeEvent> transitions) throws Exception {
        transitions
                .withExternal().source(OrderStatus.WAIT_PAYMENT).target(OrderStatus.WAIT_DELIVER).event(OrderStatusChangeEvent.PAYED)
                .and()
                .withExternal().source(OrderStatus.WAIT_DELIVER).target(OrderStatus.WAIT_RECEIVE).event(OrderStatusChangeEvent.DELIVERY)
                .and()
                .withExternal().source(OrderStatus.WAIT_RECEIVE).target(OrderStatus.FINISH).event(OrderStatusChangeEvent.RECEIVED);
    }
 
    /**
     * 持久化配置
     * 实际使用中,可以配合redis等,进行持久化操作
     * @return
     */
    @Bean
    public DefaultStateMachinePersister persister(){
        return new DefaultStateMachinePersister<>(new StateMachinePersist<Object, Object, Order>() {
            @Override
            public void write(StateMachineContext<Object, Object> context, Order order) throws Exception {
                //此处并没有进行持久化操作
            }
 
            @Override
            public StateMachineContext<Object, Object> read(Order order) throws Exception {
                //此处直接获取order中的状态,其实并没有进行持久化读取操作
                return new DefaultStateMachineContext(order.getStatus(), null, null, null);
            }
        });
    }
}

4、添加订单状态监听器

@Component("orderStateListener")
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListenerImpl{
 
    @OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
    public boolean payTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.WAIT_DELIVER);
        System.out.println("支付,状态机反馈信息:" + message.getHeaders().toString());
        return true;
    }
 
    @OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
    public boolean deliverTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.WAIT_RECEIVE);
        System.out.println("发货,状态机反馈信息:" + message.getHeaders().toString());
        return true;
    }
 
    @OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
    public boolean receiveTransition(Message<OrderStatusChangeEvent> message){
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.FINISH);
        System.out.println("收货,状态机反馈信息:" + message.getHeaders().toString());
        return true;
    }
}

5、创建 lOrderService 接口

public interface IOrderService {
    //创建新订单
    Order create();
    //发起支付
    Order pay(int id);
    //订单发货
    Order deliver(int id);
    //订单收货
    Order receive(int id);
    //获取所有订单信息
    Map<Integer, Order> getOrders();
}

6、在 Service业务逻辑中应用

@Service("orderService")
public class OrderServiceImpl implements IOrderService {

    @Autowired
    private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;
 
    @Autowired
    private StateMachinePersister<OrderStatus, OrderStatusChangeEvent, Order> persister;
 
    private int id = 1;
    private Map<Integer, Order> orders = new HashMap<>();

    public Order create() {
        Order order = new Order();
        order.setStatus(OrderStatus.WAIT_PAYMENT);
        order.setId(id++);
        orders.put(order.getId(), order);
        return order;
    }

    public Order pay(int id) {
        Order order = orders.get(id);
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试支付,订单号:" + id);
        Message message = MessageBuilder.withPayload(OrderStatusChangeEvent.PAYED).setHeader("order", order).build();
        if (!sendEvent(message, order)) {
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 支付失败, 状态异常,订单号:" + id);
        }
        return orders.get(id);
    }

    public Order deliver(int id) {
        Order order = orders.get(id);
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试发货,订单号:" + id);
        if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.DELIVERY).setHeader("order", order).build(), orders.get(id))) {
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 发货失败,状态异常,订单号:" + id);
        }
        return orders.get(id);
    }

    public Order receive(int id) {
        Order order = orders.get(id);
        System.out.println("线程名称:" + Thread.currentThread().getName() + " 尝试收货,订单号:" + id);
        if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.RECEIVED).setHeader("order", order).build(), orders.get(id))) {
            System.out.println("线程名称:" + Thread.currentThread().getName() + " 收货失败,状态异常,订单号:" + id);
        }
        return orders.get(id);
    } 

    public Map<Integer, Order> getOrders() {
        return orders;
    } 
 
    /**
     * 发送订单状态转换事件
     *
     * @param message
     * @param order
     * @return
     */
    private synchronized boolean sendEvent(Message<OrderStatusChangeEvent> message, Order order) {
        boolean result = false;
        try {
            orderStateMachine.start();
            //尝试恢复状态机状态
            persister.restore(orderStateMachine, order);
            //添加延迟用于线程安全测试
            Thread.sleep(1000);
            result = orderStateMachine.sendEvent(message);
            //持久化状态机状态
            persister.persist(orderStateMachine, order);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            orderStateMachine.stop();
        }
        return result;
    }
}

7、编写客户端测试代码

@SpringBootApplication
public class Test {
    public static void main(String[] args) {

        Thread.currentThread().setName("主线程");

        ConfigurableApplicationContext context = SpringApplication.run(Test.class,args);

        IOrderService orderService = (IOrderService)context.getBean("orderService");

        orderService.create();
        orderService.create();

        orderService.pay(1);

        new Thread("客户线程"){
            @Override
            public void run() {
                orderService.deliver(1);
                orderService.receive(1);
            }
        }.start();

        orderService.pay(2);
        orderService.deliver(2);
        orderService.receive(2);

        System.out.println("全部订单状态:" + orderService.getOrders());
    }
}

相信小伙伴们,通过这个真实的业务案例,对状态模式已经有了一个非常深刻的理解。

1.7 状态模式在源码中的体现

状态模式的具体应用在源码中非常少见,在源码中一般只是提供一种通用的解决方案。如果 一定要找,当然也是能找到的。经历千辛万苦,持续烧脑,下面我们来看一个在JSF源码中的 Lifecycle类。JSF也算是一个比较经典的前端框架,那么没用过的小伙伴也没关系,我们这是 只是分析一下其设计思想。在 JSF中它所有页面的处理分为6 个阶段,被定义在了 Phaseld类 中 ,用不同的常量来表示生命周期阶段,源码如下:

public class PhaseId implements Comparable {
    private static final PhaseId[] values =
    { ANY_PHASE, RESTORE_VIEW, APPLY_REQUEST_VALUES,
      PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES, INVOKE_APPLICATION, RENDER_RESPONSE }; 
}

那 么 这 些 状 态 的 切 换 都 在 Lifecycle的 execute()方 法 中 进 行 。其中会传一个参数 FacesContext对象,最终所有的状态都被FacesContext保存。在此呢,我们就不做继续深入 的分析。

1.8 状态模式相关的设计模式

1、状态模式与责任链模式

状态模式和责任链模式都能消除if分支过多的问题。但某些情况下,状态模式中的状态可以理解为责任,那么这种情况下,两种模式都可以使用。

从定义来看,状态模式强调的是一个对象内在状态的改变,而责任链模式强调的是外部节点对象间的改变。

从其代码实现上来看,他们间最大的区别就是状态模式各个状态对象知道自己下一个要进入的状态对象;而责任链模式并不清楚其下一个节点处理对象,因为链式组装由客户端负责。

2 .状态模式与策略模式

状态模式和策略模式的UML类图架构几乎完全一样,但他们的应用场景是不一样的。策略模式 多种算法行为择其一都能满足,彼此之间是独立的,用户可自行更换策略算法;而状态模式各个状态间是存在相互关系的,彼此之间在一定条件下存在自动切换状态效果,且用户无法指定 状态,只能设置初始状态。

状态模式责任链模式相同都能消除if分支过多的问题都能消除if分支过多的问题定义强调的是一个对象内在状态的改变强调的是外部节点对象间的改变代码实现状态模式各个状态对象知道自己下一个要进入的状态对象并不清楚其下一个节点处理对象,因为链式组装由客户端负责。状态模式策略模式应用场景多种算法行为择其一都能满足,彼此之间是独立的,用户可自行更换策略算法各个状态间是存在相互关系的,彼此之间在一定条件下存在自动切换状态效果,且用户无法指定状态,只能设置初始状态。

1.9 状态模式的优缺点

优点:

1、结构清晰:将状态独立为类,消除了冗余的if…else或 switch…case语 句 ,使代码更加简洁, 提高系统可维护性;

2、将状态转换显示化:通常的对象内部都是使用数值类型来定义状态,状态的切换是通过赋值 进行表现,不够直观;而使用状态类,在切换状态时,是以不同的类进行表示,转换目的更加 明确;

3、状态类职责明确且具备扩展性。

缺点:

1、类膨胀:如果一个事物具备很多状态,则会造成状态类太多;

2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱;

3、状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

2 备忘录模式

2.1 定义

备忘录模式( Memento Pattern )又称为快照模式(Snapshot Pattern )或令牌模式(Token Pattern ) , 是指在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态,属于行为型模式。

原文:Without violating encapsulation, capture and externalize an object * s internal state so that the object can be restored to this state later.

在软件系统中,备忘录模式可以为我们提供一种"后悔药”的机制,它通过存储系统各个历 史状态的快照,使得我们可以在任一时刻将系统回滚到某一个历史状态。

备忘录模式本质是从发起人实体类(Originator) 隔离存储功能,降低实体类的职责。同时 由于存储信息( Memento ) 独 立 ,且存储信息的实体交由管理类(Caretaker) 管 理 ,则可以 通过为管理类扩展额外的功能对存储信息进行扩展操作(比如增加历史快照功能…) 。

2.2 备忘录模式的应用场景

对于我们程序员来说,可能天天都在使用备忘录模式,比如我们每天使用的Git、SVN都可 以提供一种代码版本撤回的功能。还有一个比较贴切的现实场景应该是游戏的存档功能,通过 将游戏当前进度存储到本地文件系统或数据库中,使得下次继续游戏时,玩家可以从之前的位 置继续进行。

备忘录模式适用于以下应用场景:

1、需要保存历史快照的场景;

2、希望在对象之外保存状态,且除了自己其他类对象无法访问状态保存具体内容。

首先来看下备忘录模式的通用UML类图:

在这里插入图片描述

从 UML类图中,我们可以看到,备忘录模式主要包含三种角色:

发起人角色( Originator ) : 负责创建一个备忘录,记录自身需要保存的状态;具备状态回滚功能;

备忘录角色( Memento ) : 用于存储Originator的内部状态,且可以防止Originator以外的对象进行访问;

备忘录管理员角色( Caretaker) : 负责存储,提供管理备忘录( Memento ) , 无法对备忘录内容进行操作和访问。

2.3 利用压栈管理落地备忘录模式

我们肯定都用过网页中的富文本编辑器,编辑器中的通常会附带草稿箱、撤销等这样的操作。 下面我们用一段带代码来实现一个这样的功能。假设 ,我们在GPer社区中发布一篇文章,文章编辑的过程需要花很长时间,中间也会不停地撤销、修改。甚至可能要花好几天才能写出一篇 精品文章,因此可能会将已经编辑好的内容实时保存到草稿箱。

首先创建发起人角色编辑器Editor类 :

@Data
public class Editor {

    private String title;
    private String content;
    private String imgs;

    public Editor(String title, String content, String imgs) {
        this.title = title;
        this.content = content;
        this.imgs = imgs;
    }

    public ArticleMemento saveOldluemento(){
        ArticleMemento articleMemento = new ArticleMemento(this.title,this.content,this.imgs);
        return articleMemento;
    }

    public void undoFromMemento(ArticleMemento articleMemento){
        this.title = articleMemento.getTitle();
        this.content = articleMemento.getContent();
        this.imgs = articleMemento.getImgs();
    }
}

然后创建备忘录角色ArticleMemento类 :

@Data
public class ArticleMemento {
    private String title;
    private String content;
    private String imgs;

    public ArticleMemento(String title, String content, String imgs) {
        this.title = title;
        this.content = content;
        this.imgs = imgs;
    }
}

最后创建备忘录管理角色草稿箱DraftsBox类:

public class DraftsBox {
    private final Stack<ArticleMemento> STACK = new Stack<ArticleMemento>();

    public ArticleMemento getMemento(){
        ArticleMemento articleMemento = STACK.pop();
        return articleMemento;
    }

    public void addMemento(ArticleMemento articleMemento){
        STACK.push(articleMemento);
    }
}

草稿箱中定义的Stack类是Vector的一个子类,它实现了一个标准的后进先出的栈。主要 定义了以下方法:

方法定义方法描述boolean empty()测试堆栈是否为空。Object peek()查看堆栈顶部的对象,但不从堆栈中移除它。Object pop()移除堆栈顶部的对象,并作为此函数的值返回该对象。Object push(Object element)把对象压入堆栈顶部。int search(Object element)返回对象在堆栈中的位置,以 1 为基数。

最后,编写客户端测试代码:

public class Test {
    public static void main(String[] args) {
        DraftsBox draftsBox = new DraftsBox();

        Editor editor = new Editor("测试文章"。",
                "35576a9ef6fc407aa088eb8280fb1d9d.png");

        ArticleMemento articleMemento = editor.saveOldluemento();
        draftsBox.addMemento(articleMemento);

        System.out.println("标题:" + editor.getTitle() + "\n" +
                            "内容:" + editor.getContent() + "\n" +
                            "插图:" + editor.getImgs() + "\n暂存成功");

        System.out.println("完整的信息" + editor);


        System.out.println("==========首次修改文章===========");
        editor.setTitle("test");
        editor.setContent("Oldlu");

        System.out.println("==========首次修改文章完成===========");

        System.out.println("完整的信息" + editor);

        articleMemento = editor.saveOldluemento();

        draftsBox.addMemento(articleMemento);

        System.out.println("==========保存到草稿箱===========");


        System.out.println("==========第2次修改文章===========");
        editor.setTitle("test111");
        editor.setContent("Oldlu111");
        System.out.println("完整的信息" + editor);
        System.out.println("==========第2次修改文章完成===========");

        System.out.println("==========第1次撤销===========");
        articleMemento = draftsBox.getMemento();
        editor.undoFromMemento(articleMemento);
        System.out.println("完整的信息" + editor);
        System.out.println("==========第1次撤销完成===========");


        System.out.println("==========第2次撤销===========");
        articleMemento = draftsBox.getMemento();
        editor.undoFromMemento(articleMemento);
        System.out.println("完整的信息" + editor);
        System.out.println("==========第2次撤销完成===========");

    }
}

2.4 备忘录模式在源码中的体现

备忘录模式在框架源码中的应用也是比较少的,主要还是结合具体的应用场景来使用。我在JDK源码一顿找,目前为止还是没找到具体的应用,包括在MyBatis中也没有找到对应的源码。 如 果 有 小 伙 伴 找 到 可 以 联 系 我 。 在 Spring的 webflow源 码 中 还 是 找 到 一 个 StateManageableMessageContext 接口 ,我们来看它的源代码:

public interface StateManageableMessageContext extends MessageContext {
    Serializable createMessagesMemento();

    void restoreMessages(Serializable var1);

    void setMessageSource(MessageSource var1);
}

我们看到有一个createMessagesMementoQ方法,创建一个消息备忘录。可以打开它的实 现类:

public class DefaultMessageContext implements StateManageableMessageContext {

   private static final Log logger = LogFactory.getLog(DefaultMessageContext.class);

   private MessageSource messageSource;

   @SuppressWarnings("serial")
   private Map<Object, List<Message>> sourceMessages = new AbstractCachingMapDecorator<Object, List<Message>>(
         new LinkedHashMap<Object, List<Message>>()) {

      protected List<Message> create(Object source) {
         return new ArrayList<Message>();
      }
   };
	public void clearMessages() {
		sourceMessages.clear();
	}

	// implementing state manageable message context
	public Serializable createMessagesMemento() {
		return new LinkedHashMap<Object, List<Message>>(sourceMessages);
	}

	@SuppressWarnings("unchecked")
	public void restoreMessages(Serializable messagesMemento) {
		sourceMessages.putAll((Map<Object, List<Message>>) messagesMemento);
	}

	public void setMessageSource(MessageSource messageSource) {
		if (messageSource == null) {
			messageSource = new DefaultTextFallbackMessageSource();
		}
		this.messageSource = messageSource;
	}        
}       

我们看到其主要逻辑就相当于是给Message留一个备份,以备恢复之用。

2.5 命令模式的优缺点

优点:

1、简化发起人实体类(Originator ) 职 责 ,隔离状态存储与获取,实现了信息的封装,客户端 无需关心状态的保存细节;

2、提供状态回滚功能;

缺点:

1、消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赵广陆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值