RxJava——FlowableProcessor详解

FlowableProcessor详解

一、概述

核心概念:

FlowableProcessor​ 是支持背压的 Subject,它同时实现了:

  • Processor<T, R> 接口(Reactive Streams 规范)
  • Flowable 接口(可以被订阅)
  • FlowableSubscriber 接口(可以订阅其他Flowable)

继承关系:

java 复制代码
// 核心继承关系
public abstract class FlowableProcessor<T> 
    extends Flowable<T> 
    implements Processor<T, T>, FlowableSubscriber<T> {
    
    // 核心方法
    public abstract boolean hasSubscribers();
    public abstract Throwable getThrowable();
    public abstract boolean hasThrowable();
    public abstract boolean hasComplete();
    public abstract boolean hasSubscribers();
}

与Subject对比:

特性 FlowableProcessor Subject
背压支持 ✅ 完全支持 ❌ 不支持(Observable版本)
Reactive Streams兼容 ✅ Processor接口 ❌ 不兼容
订阅者数量限制 可配置 无限制
缓冲区策略 多种策略 无缓冲区
性能开销 较高 较低

二、PublishProcessor(实时发布处理器)

2.1、核心特性

  • 类似 PublishSubject,但支持背压
  • 只向当前订阅者发射数据
  • 不缓存任何数据
  • 支持背压策略
java 复制代码
@Test
public void testPublishProcessor() {
    //基本用法
    PublishProcessor<Integer> processor = PublishProcessor.create();

    //生产者
    Flowable.range(1, 1000)
            .subscribe(processor);
    //消费者1
    processor.onBackpressureBuffer(100)//缓冲区大小
            .subscribe(
                    data -> System.out.println("Consumer 1:" + data),
                    error -> System.out.println("Error 1:" + error),
                    () -> System.out.println("Completed 1")
            );

    //消费者2(稍后订阅,收不到之前的数据)
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    processor.subscribe(
            data -> System.out.println("Consumer 2:" + data),
            error -> System.out.println("Error 2:" + error),
            () -> System.out.println("Completed 2")
    );
}

背压处理示例:

java 复制代码
public class PublishProcessorBackpressure {
    
    public static void demoBackpressure() {
        PublishProcessor<Integer> processor = PublishProcessor.create();
        
        // 快速生产者
        Flowable.interval(10, TimeUnit.MILLISECONDS)
            .take(1000)
            .map(Long::intValue)
            .subscribe(processor);
        
        // 慢速消费者
        processor
            .onBackpressureBuffer(
                50,                     // 缓冲区大小
                () -> System.out.println("Buffer overflow"),
                BackpressureOverflowStrategy.DROP_OLDEST
            )
            .observeOn(Schedulers.io(), false, 16)  // 缓冲区16
            .subscribe(
                data -> {
                    // 慢速处理
                    Thread.sleep(100);
                    System.out.println("Processed: " + data);
                },
                error -> System.err.println("Error: " + error)
            );
    }
    
    // 使用不同的背压策略
    public static void differentStrategies() {
        PublishProcessor<Integer> processor = PublishProcessor.create();
        
        // 策略1: BUFFER - 无限缓冲(可能OOM)
        processor.onBackpressureBuffer();
        
        // 策略2: DROP - 丢弃溢出数据
        processor.onBackpressureDrop(dropped -> 
            System.out.println("Dropped: " + dropped)
        );
        
        // 策略3: LATEST - 保留最新数据
        processor.onBackpressureLatest();
        
        // 策略4: ERROR - 缓冲区满时抛异常
        processor.onBackpressureBuffer(100, 
            () -> {}, 
            BackpressureOverflowStrategy.ERROR
        );
    }
}

2.2、使用场景

java 复制代码
// 1. 实时数据流处理
public class RealTimeDataProcessor {
    private final PublishProcessor<SensorData> dataProcessor = PublishProcessor.create();
    private final CompositeDisposable disposables = new CompositeDisposable();
    
    public RealTimeDataProcessor() {
        // 设置数据处理管道
        disposables.add(
            dataProcessor
                .onBackpressureBuffer(1000)
                .observeOn(Schedulers.computation())
                .subscribe(this::processData)
        );
    }
    
    public void feedData(SensorData data) {
        dataProcessor.onNext(data);
    }
    
    public void addConsumer(Consumer<SensorData> consumer) {
        disposables.add(
            dataProcessor
                .onBackpressureBuffer(100)
                .subscribe(consumer::accept)
        );
    }
    
    private void processData(SensorData data) {
        // 复杂的数据处理逻辑
    }
}

// 2. 事件总线(支持背压)
public class BackpressureEventBus {
    private final PublishProcessor<Event> eventProcessor = PublishProcessor.create();
    
    public void postEvent(Event event) {
        eventProcessor.onNext(event);
    }
    
    public Flowable<Event> getEventStream() {
        return eventProcessor
            .onBackpressureBuffer(1000)
            .filter(event -> event != null)
            .share();  // 多播
    }
    
    public Flowable<Event> getEventStream(String eventType) {
        return getEventStream()
            .filter(event -> event.getType().equals(eventType));
    }
}

// 3. 消息队列桥接
public class MessageQueueBridge {
    private final PublishProcessor<Message> messageProcessor = PublishProcessor.create();
    private final MessageQueue queue;
    
    public MessageQueueBridge(MessageQueue queue) {
        this.queue = queue;
        
        // 从队列消费
        Flowable.generate(
            () -> queue.createConsumer(),
            (consumer, emitter) -> {
                Message msg = consumer.receive(1000, TimeUnit.MILLISECONDS);
                if (msg != null) {
                    emitter.onNext(msg);
                } else {
                    emitter.onComplete();
                }
            },
            Consumer::close
        )
        .subscribe(messageProcessor);
    }
    
    public void sendMessage(Message message) {
        messageProcessor.onNext(message);
    }
    
    public Flowable<Message> getMessageStream() {
        return messageProcessor
            .onBackpressureBuffer(100)
            .doOnNext(msg -> queue.acknowledge(msg.getId()));
    }
}

三、BehaviorProcessor(行为处理器)

3.1、核心特性

  • 类似 BehaviorSubject,但支持背压
  • 发射最近的一个值和后续值
  • 需要初始值(或默认值)
  • 支持背压策略
java 复制代码
@Test
public void testBehaviorProcessor() {
    //基本用法
    BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("Initial");

    //订阅者1
    processor.subscribe(data -> System.out.println("Subscriber 1:" + data));

    processor.onNext("Update 1");
    processor.onNext("Update 2");

    //订阅者2(立即受到最近的值update 2)
    processor.subscribe(data -> System.out.println("Subscriber 2:" + data));

    processor.onNext("Update 3");
    processor.onComplete();

    //获取当前值
    String currentValue = processor.getValue();
}

Subscriber 1:Initial
Subscriber 1:Update 1
Subscriber 1:Update 2
Subscriber 2:Update 2
Subscriber 1:Update 3
Subscriber 2:Update 3

背压处理示例:

java 复制代码
public class BehaviorProcessorBackpressure {
    
    public static void demoWithBackpressure() {
        BehaviorProcessor<Integer> processor = BehaviorProcessor.create();
        
        // 快速生产者
        Flowable.interval(1, TimeUnit.MILLISECONDS)
            .take(10000)
            .map(Long::intValue)
            .subscribe(processor);
        
        // 慢速消费者
        processor
            .onBackpressureBuffer(
                100,
                () -> System.out.println("Buffer overflow"),
                BackpressureOverflowStrategy.DROP_OLDEST
            )
            .observeOn(Schedulers.io(), false, 16)
            .subscribe(
                value -> {
                    Thread.sleep(10);  // 慢速处理
                    System.out.println("Processed: " + value);
                }
            );
    }
    
    // 状态管理示例
    public static class StateManager<T> {
        private final BehaviorProcessor<T> stateProcessor;
        private final Flowable<T> stateStream;
        
        public StateManager(T initialState) {
            this.stateProcessor = BehaviorProcessor.createDefault(initialState);
            this.stateStream = stateProcessor
                .onBackpressureLatest()  // 状态只需要最新值
                .distinctUntilChanged()
                .share();
        }
        
        public void setState(T newState) {
            stateProcessor.onNext(newState);
        }
        
        public Flowable<T> getStateStream() {
            return stateStream;
        }
        
        public T getCurrentState() {
            return stateProcessor.getValue();
        }
        
        public boolean hasState() {
            return stateProcessor.hasValue();
        }
    }
}

3.2、使用场景

java 复制代码
// 1. 应用状态管理
public class AppStateManager {
    private final BehaviorProcessor<AppState> stateProcessor = 
        BehaviorProcessor.createDefault(AppState.IDLE);
    private final Flowable<AppState> stateStream;
    
    public AppStateManager() {
        this.stateStream = stateProcessor
            .onBackpressureLatest()
            .distinctUntilChanged()
            .share();
    }
    
    public void transitionTo(AppState newState) {
        if (isValidTransition(stateProcessor.getValue(), newState)) {
            stateProcessor.onNext(newState);
        } else {
            throw new IllegalStateException("Invalid state transition");
        }
    }
    
    public Flowable<AppState> getStateStream() {
        return stateStream;
    }
    
    public AppState getCurrentState() {
        return stateProcessor.getValue();
    }
    
    public Flowable<Boolean> isState(AppState targetState) {
        return stateStream.map(state -> state == targetState);
    }
}

// 2. 配置管理
public class ConfigManager {
    private final BehaviorProcessor<Config> configProcessor = 
        BehaviorProcessor.create();
    private final Flowable<Config> configStream;
    
    public ConfigManager(Config initialConfig) {
        configProcessor.onNext(initialConfig);
        this.configStream = configProcessor
            .onBackpressureLatest()
            .share();
    }
    
    public void updateConfig(Config newConfig) {
        configProcessor.onNext(newConfig);
    }
    
    public Flowable<Config> getConfigStream() {
        return configStream;
    }
    
    public Config getCurrentConfig() {
        return configProcessor.getValue();
    }
    
    public <T> Flowable<T> observeConfigValue(
        Function<Config, T> extractor, 
        Class<T> type
    ) {
        return configStream
            .map(extractor)
            .distinctUntilChanged()
            .filter(Objects::nonNull);
    }
}

// 3. 实时价格更新
public class StockPriceFeed {
    private final BehaviorProcessor<StockPrice> priceProcessor = 
        BehaviorProcessor.create();
    private final Map<String, Flowable<Double>> symbolStreams = new ConcurrentHashMap<>();
    
    public void updatePrice(String symbol, double price) {
        priceProcessor.onNext(new StockPrice(symbol, price, System.currentTimeMillis()));
    }
    
    public Flowable<Double> getPriceStream(String symbol) {
        return symbolStreams.computeIfAbsent(symbol, sym -> 
            priceProcessor
                .onBackpressureLatest()
                .filter(p -> p.getSymbol().equals(sym))
                .map(StockPrice::getPrice)
                .distinctUntilChanged()
                .share()
        );
    }
    
    public Double getCurrentPrice(String symbol) {
        StockPrice current = priceProcessor.getValue();
        if (current != null && current.getSymbol().equals(symbol)) {
            return current.getPrice();
        }
        return null;
    }
}

四、ReplayProcessor(重放处理器)

4.1、核心特性

  • 类似 ReplaySubject,但支持背压
  • 缓存所有发射的数据
  • 可配置缓冲区大小和时间窗口
  • 新订阅者立即收到所有缓存数据
java 复制代码
@Test
public void testReplayProcessor() throws InterruptedException {
    // 1. 无限缓存
    ReplayProcessor<String> unlimited = ReplayProcessor.create();
    unlimited.onNext("Data 1");
    unlimited.onNext("Data 2");
    unlimited.subscribe(data -> System.out.println("Subscriber: " + data));
    // 输出: Data 1, Data 2

    // 2. 限制缓存大小
    ReplayProcessor<Integer> sizeLimited = ReplayProcessor.create(2);
    sizeLimited.onNext(1);
    sizeLimited.onNext(2);
    sizeLimited.onNext(3);  // 1被丢弃
    sizeLimited.subscribe(System.out::println);
    // 输出: 2, 3

    // 3. 限制时间窗口
    ReplayProcessor<String> timeLimited = ReplayProcessor.createWithTime(
            1, TimeUnit.SECONDS, Schedulers.computation()
    );
    timeLimited.onNext("Data 1");
    Thread.sleep(500);
    timeLimited.onNext("Data 2");
    Thread.sleep(600);  // Data 1过期
    timeLimited.subscribe(System.out::println);
    // 输出: Data 2

    // 4. 限制大小和时间
    ReplayProcessor<String> bounded = ReplayProcessor.createWithTimeAndSize(
            1, TimeUnit.SECONDS,  // 时间窗口
            Schedulers.computation(),                  // 最大缓存数量
            100
    );
}


Subscriber: Data 1
Subscriber: Data 2
1
2
3
Data 2

背压处理示例:

java 复制代码
public class ReplayProcessorBackpressure {
    
    public static void demoWithBackpressure() {
        // 创建有界重放处理器
        ReplayProcessor<Integer> processor = ReplayProcessor.createWithSize(1000);
        
        // 快速生产者
        Flowable.interval(1, TimeUnit.MILLISECONDS)
            .take(10000)
            .map(Long::intValue)
            .subscribe(processor);
        
        // 慢速消费者
        processor
            .onBackpressureBuffer(100)
            .observeOn(Schedulers.io(), false, 16)
            .subscribe(
                value -> {
                    Thread.sleep(10);
                    System.out.println("Processed: " + value);
                }
            );
        
        // 新订阅者会收到最近1000个值
        Thread.sleep(1000);
        processor.subscribe(value -> 
            System.out.println("New subscriber: " + value)
        );
    }
    
    // 历史数据查询
    public static class HistoricalDataStore<T> {
        private final ReplayProcessor<T> dataProcessor;
        private final Flowable<T> liveStream;
        
        public HistoricalDataStore(int historySize) {
            this.dataProcessor = ReplayProcessor.createWithSize(historySize);
            this.liveStream = dataProcessor
                .onBackpressureBuffer(100)
                .share();
        }
        
        public void addData(T data) {
            dataProcessor.onNext(data);
        }
        
        public Flowable<T> getLiveStream() {
            return liveStream;
        }
        
        public Flowable<T> getHistory() {
            return dataProcessor.take(dataProcessor.getBufferSize());
        }
        
        public Flowable<T> getRecentData(int count) {
            return dataProcessor.takeLast(count);
        }
        
        public int getStoredCount() {
            return dataProcessor.getBufferSize();
        }
    }
}

4.2、使用场景

java 复制代码
// 1. 日志记录系统
public class LoggingSystem {
    private final ReplayProcessor<LogEntry> logProcessor = 
        ReplayProcessor.createWithTime(1, TimeUnit.HOURS, Schedulers.io());
    private final Flowable<LogEntry> logStream;
    
    public LoggingSystem() {
        this.logStream = logProcessor
            .onBackpressureBuffer(10000)
            .share();
    }
    
    public void log(Level level, String message, Throwable throwable) {
        LogEntry entry = new LogEntry(
            level, 
            message, 
            System.currentTimeMillis(), 
            Thread.currentThread().getName(),
            throwable
        );
        logProcessor.onNext(entry);
    }
    
    public Flowable<LogEntry> getLogStream() {
        return logStream;
    }
    
    public Flowable<LogEntry> getLogsByLevel(Level level) {
        return logStream.filter(entry -> entry.getLevel() == level);
    }
    
    public Flowable<LogEntry> getRecentLogs(int count) {
        return logProcessor.takeLast(count);
    }
    
    public Flowable<LogEntry> getLogsSince(long timestamp) {
        return logStream.filter(entry -> entry.getTimestamp() >= timestamp);
    }
}

// 2. 传感器数据历史
public class SensorDataRecorder {
    private final ReplayProcessor<SensorData> dataProcessor = 
        ReplayProcessor.createWithTimeAndSize(
            10, TimeUnit.MINUTES,
            10000,
            Schedulers.computation()
        );
    private final Flowable<SensorData> dataStream;
    
    public SensorDataRecorder() {
        this.dataStream = dataProcessor
            .onBackpressureBuffer(1000)
            .share();
    }
    
    public void recordData(SensorData data) {
        dataProcessor.onNext(data);
    }
    
    public Flowable<SensorData> getDataStream() {
        return dataStream;
    }
    
    public Flowable<SensorData> getDataBySensor(String sensorId) {
        return dataStream.filter(data -> data.getSensorId().equals(sensorId));
    }
    
    public Flowable<Double> getAverageValue(String sensorId, long duration, TimeUnit unit) {
        long window = System.currentTimeMillis() - unit.toMillis(duration);
        return dataStream
            .filter(data -> 
                data.getSensorId().equals(sensorId) && 
                data.getTimestamp() >= window
            )
            .map(SensorData::getValue)
            .reduce(0.0, (sum, val) -> sum + val)
            .toFlowable()
            .flatMap(sum -> 
                dataStream
                    .filter(data -> 
                        data.getSensorId().equals(sensorId) && 
                        data.getTimestamp() >= window
                    )
                    .count()
                    .map(count -> count > 0 ? sum / count : 0.0)
            );
    }
}

// 3. 聊天消息历史
public class ChatRoom {
    private final ReplayProcessor<Message> messageProcessor = 
        ReplayProcessor.createWithSize(1000);
    private final Flowable<Message> messageStream;
    
    public ChatRoom() {
        this.messageStream = messageProcessor
            .onBackpressureBuffer(100)
            .share();
    }
    
    public void sendMessage(User sender, String content) {
        Message message = new Message(
            sender,
            content,
            System.currentTimeMillis()
        );
        messageProcessor.onNext(message);
    }
    
    public Flowable<Message> getMessageStream() {
        return messageStream;
    }
    
    public Flowable<Message> getMessagesByUser(User user) {
        return messageStream.filter(msg -> msg.getSender().equals(user));
    }
    
    public void replayHistoryFor(User user, int messageCount) {
        messageProcessor
            .takeLast(messageCount)
            .subscribe(msg -> sendToUser(user, msg));
    }
}

五、UnicastProcessor(单播处理器)

5.1、核心特性

  • 类似 UnicastSubject,但支持背压
  • 只允许一个订阅者
  • 支持缓冲区
  • 如果没有订阅者,会缓存数据直到有订阅者
java 复制代码
// 基本用法
UnicastProcessor<String> processor = UnicastProcessor.create();

// 发射数据(还没有订阅者,数据被缓存)
processor.onNext("Cached 1");
processor.onNext("Cached 2");

// 订阅者(立即收到缓存的数据)
processor.subscribe(
    data -> System.out.println("Received: " + data),
    error -> System.err.println("Error: " + error),
    () -> System.out.println("Completed")
);

processor.onNext("Live 1");
processor.onComplete();

// 输出:
// Received: Cached 1
// Received: Cached 2
// Received: Live 1
// Completed

// 尝试第二个订阅者会抛异常
try {
    processor.subscribe(data -> System.out.println("Second: " + data));
} catch (IllegalStateException e) {
    System.out.println("只能有一个订阅者: " + e.getMessage());
}

背压处理示例:

java 复制代码
public class UnicastProcessorBackpressure {
    
    public static void demoWithBackpressure() {
        // 创建带缓冲区的单播处理器
        UnicastProcessor<Integer> processor = UnicastProcessor.create(100);
        
        // 快速生产者
        Flowable.interval(1, TimeUnit.MILLISECONDS)
            .take(1000)
            .map(Long::intValue)
            .subscribe(processor);
        
        // 慢速消费者
        processor
            .onBackpressureBuffer(50)
            .observeOn(Schedulers.io(), false, 16)
            .subscribe(
                value -> {
                    Thread.sleep(10);
                    System.out.println("Processed: " + value);
                }
            );
    }
    
    // 队列式处理器
    public static class QueueProcessor<T> {
        private final UnicastProcessor<T> processor;
        private final Flowable<T> outputStream;
        
        public QueueProcessor(int bufferSize) {
            this.processor = UnicastProcessor.create(bufferSize);
            this.outputStream = processor
                .onBackpressureBuffer(bufferSize)
                .share();
        }
        
        public void enqueue(T item) {
            processor.onNext(item);
        }
        
        public void complete() {
            processor.onComplete();
        }
        
        public void error(Throwable throwable) {
            processor.onError(throwable);
        }
        
        public Flowable<T> getOutputStream() {
            return outputStream;
        }
        
        public boolean hasSubscriber() {
            return processor.hasSubscribers();
        }
        
        public int getBufferSize() {
            return processor.getBufferSize();
        }
    }
}

5.2、使用场景

java 复制代码
// 1. 单消费者任务队列
public class TaskQueue {
    private final UnicastProcessor<Task> taskProcessor = UnicastProcessor.create(1000);
    private final Flowable<Task> taskStream;
    private Disposable workerDisposable;
    
    public TaskQueue() {
        this.taskStream = taskProcessor
            .onBackpressureBuffer(100)
            .observeOn(Schedulers.io(), false, 16);
        
        // 启动工作线程
        this.workerDisposable = taskStream.subscribe(this::processTask);
    }
    
    public void submitTask(Task task) {
        taskProcessor.onNext(task);
    }
    
    public void submitTasks(List<Task> tasks) {
        tasks.forEach(taskProcessor::onNext);
    }
    
    public void shutdown() {
        taskProcessor.onComplete();
        if (workerDisposable != null && !workerDisposable.isDisposed()) {
            workerDisposable.dispose();
        }
    }
    
    public boolean isActive() {
        return workerDisposable != null && !workerDisposable.isDisposed();
    }
    
    private void processTask(Task task) {
        try {
            task.execute();
        } catch (Exception e) {
            System.err.println("Task failed: " + e.getMessage());
        }
    }
}

// 2. 串行写入器
public class SerialWriter {
    private final UnicastProcessor<byte[]> writeProcessor = UnicastProcessor.create(100);
    private final OutputStream outputStream;
    private Disposable writeDisposable;
    
    public SerialWriter(OutputStream outputStream) {
        this.outputStream = outputStream;
        
        this.writeDisposable = writeProcessor
            .onBackpressureBuffer(50)
            .observeOn(Schedulers.io())
            .subscribe(
                data -> {
                    synchronized (outputStream) {
                        outputStream.write(data);
                        outputStream.flush();
                    }
                },
                error -> {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        error.addSuppressed(e);
                    }
                },
                () -> {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            );
    }
    
    public void write(byte[] data) {
        writeProcessor.onNext(data);
    }
    
    public void close() {
        writeProcessor.onComplete();
        if (writeDisposable != null && !writeDisposable.isDisposed()) {
            writeDisposable.dispose();
        }
    }
    
    public boolean isClosed() {
        return writeProcessor.hasComplete() || writeProcessor.hasThrowable();
    }
}

// 3. 命令处理器
public class CommandProcessor {
    private final UnicastProcessor<Command> commandProcessor = UnicastProcessor.create(100);
    private final Map<Class<? extends Command>, CommandHandler> handlers = new ConcurrentHashMap<>();
    private Disposable processingDisposable;
    
    public CommandProcessor() {
        this.processingDisposable = commandProcessor
            .onBackpressureBuffer(50)
            .observeOn(Schedulers.io())
            .subscribe(this::processCommand);
    }
    
    public void registerHandler(Class<? extends Command> commandType, CommandHandler handler) {
        handlers.put(commandType, handler);
    }
    
    public void execute(Command command) {
        commandProcessor.onNext(command);
    }
    
    public void shutdown() {
        commandProcessor.onComplete();
        if (processingDisposable != null && !processingDisposable.isDisposed()) {
            processingDisposable.dispose();
        }
    }
    
    private void processCommand(Command command) {
        CommandHandler handler = handlers.get(command.getClass());
        if (handler != null) {
            try {
                handler.handle(command);
            } catch (Exception e) {
                System.err.println("Command execution failed: " + e.getMessage());
            }
        } else {
            System.err.println("No handler for command: " + command.getClass());
        }
    }
}
相关推荐
恋猫de小郭28 分钟前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker6 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴6 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭16 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab17 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
这个实现不了1 天前
echarts实例:可堆叠的立体柱形图+特殊symbol的折线图
echarts
这个实现不了1 天前
echarts实例:进度条加描述
echarts
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
这个实现不了1 天前
echarts实例:最高最低标识-并列立体柱形图
echarts
这个实现不了1 天前
echarts实例:双轴水平条形图(菱形和三角形的symbol)
echarts