Android-Flutter面试真经

关于Android Flutter 相关的面试知识点,我通过这次面试期间问道的问题进行了总结,将被问的问题第一反应就是去网上查找大家优秀的回答。希望通过这篇文章能帮助到大家。

面试节奏

3月26号开始了第一家公司的第一面,期间没考虑过去投其他公司的简历,主要目的是锻炼一下自己的面试,毕竟4年没面试过了,还是很虚。磕磕绊绊历时两个星期五轮面试最终拿到了第一家offer,同时公司人力那边也知道我有离职的打算就间接催促我提交OA离职,自己很被动,一冲动直接提交OA了,预留了两个星期的交接时间,也就是有两个星期的时间找工作,开始变的焦虑,就直接在boss上打开简历,并投了五家,然后一天过去没有任何反应,很是着急,直到第三天才接到4家的面试邀请,悬着的心终于松口气。

4月15号正式开始面试,4月16号请假在家安心面试,一天三家公司共经历了五轮面试,每轮面试都是一个小时左右,因为我表达了时间紧迫所以面试公司的HR跟进的很给力第三天有两个公司就都进行到了第五轮面试。4月20号基本算是拿到了3个offer也有个比较满意的公司,后续几天另外2家的复试也开始了,就没约新的面试公司,也过了。目前还有3家到了最终面,因为周期太长就直接拒绝了。

一共是面了7家公司,共经历了27轮面试,拿到了4个offer(有一家是flutter开发),3家hr面试直接拒绝没参加。7家公司有BBA大厂也有D轮中型公司。

参加的公司面试流程都很标准,会提前发邮件预约1个小时的面试时间。

也有一面很满意之后一次性解决的,3个小时的面试

说了这么多废话,进入正题吧,面试知识点,我只大概总结下,其实每个知识点都可以深入拓展,分了五个模块java、Android、网络、dart、flutter

java

GC机制

垃圾回收需要完成两件事:找到垃圾,回收垃圾。 找到垃圾一般的话有两种方法:

  • 引用计数法: 当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
  • 可达性分析法: 我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中的静态对象和常量池中的常量。 回收垃圾的话有这么四种方法:
  • 标记清除算法: 顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的碎片化。
  • 复制算法: 复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要两倍的内存。
  • 标记整理算法: 标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会将存活的对象压缩,避免了内存的碎片化。
  • 分代算法: 分代算法将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率。

新生代对象分为三个区域:Eden 区和两个 Survivor 区。新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。

这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。 老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法。

JVM内存区域的划分,哪些区域会发生 OOM

JVM 的内存区域可以分为两类:线程私有和区域和线程共有的区域。 线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域:堆、方法区、运行时常量池

  • 程序计数器。 每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址。
  • JVM虚拟机栈。 创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出站。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。
  • 本地方法栈。 跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。
  • 堆。 堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代。
  • 方法区。方法区主要存放类的结构信息,比如静态属性和方法等等。
  • 运行时常量池。运行时常量池位于方法区中,主要存放各种常量信息。

其实除了程序计数器,其他的部分都会发生 OOM。

  • 堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏。
  • JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的。
  • 方法区。方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的。

java的内存模型

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

原子性

在Java中,为了保证原子性,提供了两个高级的字节码指令monitorentermonitorexit。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized

因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

可见性

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

除了volatile,Java中的synchronizedfinal两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。

有序性

在Java中,可以使用synchronizedvolatile来保证多线程之间操作的有序性。实现方式有所区别:

volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。

但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用

类加载过程

Java 中类加载分为 3 个步骤:加载、链接、初始化。

  • 加载。 加载是将字节码数据从不同的数据源读取到JVM内存,并映射为 JVM 认可的数据结构,也就是 Class 对象的过程。数据源可以是 Jar 文件、Class 文件等等。如果数据的格式并不是 ClassFile 的结构,则会报 ClassFormatError。

  • **链接。**链接是类加载的核心部分,这一步分为 3 个步骤:验证、准备、解析。

  • 验证。 验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如果验证出错,则会报VerifyError。

  • 准备。 这一步会创建静态变量,并为静态变量开辟内存空间。

  • 解析。 这一步会将符号引用替换为直接引用。

  • 初始化。 初始化会为静态变量赋值,并执行静态代码块中的逻辑。

双亲委派模型

类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。

  • 启动类加载器主要加载 jre/lib下的jar文件。
  • 扩展类加载器主要加载 jre/lib/ext 下的jar文件。
  • 应用程序类加载器主要加载 classpath 下的文件。

所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。

HashMap 的原理

HashMap 的内部可以看做数组+链表的复合结构。数组被分为一个个的桶(bucket)。哈希值决定了键值对在数组中的寻址。具有相同哈希值的键值对会组成链表。需要注意的是当链表长度超过阈值(默认是8)的时候会触发树化,链表会变成树形结构。

把握HashMap的原理需要关注4个方法:hash、put、get、resize。

  • hash方法。 将 key 的 hashCode 值的高位数据移位到低位进行异或运算。这么做的原因是有些 key 的 hashCode 值的差异集中在高位,而哈希寻址是忽略容量以上高位的,这种做法可以有效避免哈希冲突。

  • **put 方法。**put 方法主要有以下几个步骤:

  • 通过 hash 方法获取 hash 值,根据 hash 值寻址。

  • 如果未发生碰撞,直接放到桶中。

  • 如果发生碰撞,则以链表形式放在桶后。

  • 当链表长度大于阈值后会触发树化,将链表转换为红黑树。

  • 如果数组长度达到阈值,会调用 resize 方法扩展容量。

  • **get方法。**get 方法主要有以下几个步骤:

  • 通过 hash 方法获取 hash 值,根据 hash 值寻址。

  • 如果与寻址到桶的 key 相等,直接返回对应的 value。

  • 如果发生冲突,分两种情况。如果是树,则调用 getTreeNode 获取 value;如果是链表则通过循环遍历查找对应的 value。

  • **resize 方法。**resize 做了两件事:

  • 将原数组扩展为原来的 2 倍

  • 重新计算 index 索引值,将原节点重新放到新的数组中。这一步可以将原先冲突的节点分散到新的桶中。

sleep 和 wait 的区别

  • sleep 方法是 Thread 类中的静态方法,wait 是 Object 类中的方法
  • sleep 并不会释放同步锁,而 wait 会释放同步锁
  • sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步代码块中使用
  • sleep 中必须传入时间,而 wait 可以传,也可以不传,不传时间的话只有 notify 或者 notifyAll 才能唤醒,传时间的话在时间之后会自动唤醒

join 的用法

join 方法通常是保证线程间顺序调度的一个方法,它是 Thread 类中的方法。比方说在线程 A 中执行线程 B.join(),这时线程 A 会进入等待状态,直到线程 B 执行完毕之后才会唤醒,继续执行A线程中的后续方法。

join 方法可以传时间参数,也可以不传参数,不传参数实际上调用的是 join(0)。它的原理其实是使用了 wait 方法,join 的原理如下:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

volatile

一般提到 volatile,就不得不提到内存模型相关的概念。我们都知道,在程序运行中,每条指令都是由 CPU 执行的,而指令的执行过程中,势必涉及到数据的读取和写入。程序运行中的数据都存放在主存中,这样会有一个问题,由于 CPU 的执行速度是要远高于主存的读写速度,所以直接从主存中读写数据会降低 CPU 的效率。为了解决这个问题,就有了高速缓存的概念,在每个 CPU 中都有高速缓存,它会事先从主存中读取数据,在 CPU 运算之后在合适的时候刷新到主存中。

这样的运行模式在单线程中是没有任何问题的,但在多线程中,会导致缓存一致性的问题。举个简单的例子:i=i+1 ,在两个线程中执行这句代码,假设i的初始值为0。我们期望两个线程运行后得到2,那么有这样的一种情况,两个线程都从主存中读取i到各自的高速缓存中,这时候两个线程中的i都为0。在线程1执行完毕得到i=1,将之刷新到主存后,线程2开始执行,由于线程2中的i是高速缓存中的0,所以在执行完线程2之后刷新到主存的i仍旧是1。

所以这就导致了对共享变量的缓存一致性的问题,那么为了解决这个问题,提出了缓存一致性协议:当 CPU 在写数据时,如果发现操作的是共享变量,它会通知其他 CPU 将它们内部的这个共享变量置为无效状态,当其他 CPU 读取缓存中的共享变量时,发现这个变量是无效的,它会从新从主存中读取最新的值。

在Java的多线程开发中,有三个重要概念:原子性、可见性、有序性。

  • **原子性:**一个或多个操作要么都不执行,要么都执行。
  • 可见性: 一个线程中对共享变量(类中的成员变量或静态变量)的修改,在其他线程立即可见。
  • 有序性: 程序执行的顺序按照代码的顺序执行。 把一个变量声明为volatile,其实就是保证了可见性和有序性。 可见性我上面已经说过了,在多线程开发中是很有必要的。这个有序性还是得说一下,为了执行的效率,有时候会发生指令重排,这在单线程中指令重排之后的输出与我们的代码逻辑输出还是一致的。但在多线程中就可能发生问题,volatile在一定程度上可以避免指令重排。

volatile的原理是在生成的汇编代码中多了一个lock前缀指令,这个前缀指令相当于一个内存屏障,这个内存屏障有3个作用:

  • 确保指令重排的时候不会把屏障后的指令排在屏障前,确保不会把屏障前的指令排在屏障后。
  • 修改缓存中的共享变量后立即刷新到主存中。
  • 当执行写操作时会导致其他CPU中的缓存无效。

volatile和synchronize的区别

volatile

它所修饰的变量不保留拷贝,直接访问主内存中的。

在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。

使用场景

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

1)对变量的写操作不依赖于当前值。

2)该变量没有包含在具有其他变量的不变式中。

volatile最适用一个线程写,多个线程读的场合。

如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。

synchronized

当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
  3. 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
  4. 当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

区别

  1. volatile是变量修饰符,而synchronized则作用于一段代码或方法。
  2. volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。
  3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  4. volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

关键字volatile主要使用的场合是在多个线程中可以感知实例变量被修改,并且可以获得最新的值使用,也就是多线程读取共享变量时可以获得最新值使用。

关键字volatile提示线程每次从共享内存中读取变量,而不是私有内存中读取,这样就保证了同步数据的可见性。但是要注意的是:如果修改实例变量中的数据

ThreadLocal的作用

ThreadLocal的作用是提供线程内的局部变量,说白了,就是在各线程内部创建一个变量的副本,相比于使用各种锁机制访问变量,ThreadLocal的思想就是用空间换时间,使各线程都能访问属于自己这一份的变量副本,变量值不互相干扰,减少同一个线程内的多个函数或者组件之间一些公共变量传递的复杂度。

get函数用来获取与当前线程关联的ThreadLocal的值,如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回,initialValue是protected类型的,所以一般我们使用时需要继承该函数,给出初始值。而set函数是用来设置当前线程的该ThreadLocal的值,remove函数用来删除ThreadLocal绑定的值,在某些情况下需要手动调用,防止内存泄露。

Java中生产者与消费者模式

生产者消费者模式要保证的是当缓冲区满的时候生产者不再生产对象,当缓冲区空时,消费者不再消费对象。实现机制就是当缓冲区满时让生产者处于等待状态,当缓冲区为空时让消费者处于等待状态。当生产者生产了一个对象后会唤醒消费者,当消费者消费一个对象后会唤醒生产者。

三种种实现方式:wait 和 notify、await 和 signal、BlockingQueue。

  • wait 和 notify
//wait和notify
import java.util.LinkedList;
public class StorageWithWaitAndNotify {
    private final int                MAX_SIZE = 10;
    private       LinkedList<Object> list     = new LinkedList<Object>();
    public void produce() {
        synchronized (list) {
            while (list.size() == MAX_SIZE) {
                System.out.println("仓库已满:生产暂停");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(new Object());
            System.out.println("生产了一个新产品,现库存为:" + list.size());
            list.notifyAll();
        }
    }
    public void consume() {
        synchronized (list) {
            while (list.size() == 0) {
                System.out.println("库存为0:消费暂停");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("消费了一个产品,现库存为:" + list.size());
            list.notifyAll();
        }
    }
}
  • await 和 signal
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class StorageWithAwaitAndSignal {
    private final int                MAX_SIZE = 10;
    private       ReentrantLock      mLock    = new ReentrantLock();
    private       Condition          mEmpty   = mLock.newCondition();
    private       Condition          mFull    = mLock.newCondition();
    private       LinkedList<Object> mList    = new LinkedList<Object>();
    public void produce() {
        mLock.lock();
        while (mList.size() == MAX_SIZE) {
            System.out.println("缓冲区满,暂停生产");
            try {
                mFull.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        mList.add(new Object());
        System.out.println("生产了一个新产品,现容量为:" + mList.size());
        mEmpty.signalAll();
        mLock.unlock();
    }
    public void consume() {
        mLock.lock();
        while (mList.size() == 0) {
            System.out.println("缓冲区为空,暂停消费");
            try {
                mEmpty.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        mList.remove();
        System.out.println("消费了一个产品,现容量为:" + mList.size());
        mFull.signalAll();
        mLock.unlock();
    }
}
  • BlockingQueue
import java.util.concurrent.LinkedBlockingQueue;
public class StorageWithBlockingQueue {
    private final int                         MAX_SIZE = 10;
    private       LinkedBlockingQueue<Object> list     = new LinkedBlockingQueue<Object>(MAX_SIZE);
    public void produce() {
        if (list.size() == MAX_SIZE) {
            System.out.println("缓冲区已满,暂停生产");
        }
        try {
            list.put(new Object());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产了一个产品,现容量为:" + list.size());
    }
    public void consume() {
        if (list.size() == 0) {
            System.out.println("缓冲区为空,暂停消费");
        }
        try {
            list.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("消费了一个产品,现容量为:" + list.size());
    }
}

final、finally、finalize区别

final 可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写 (override)。

finally 是保证重点代码一定会执行的一种机制。通常是使用 try-finally 或者 try-catch-finally 来进行文件流的关闭等操作。

finalize 是 Object 类中的一个方法,它的设计目的是保证对象在垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9已经被标记为 deprecated。

Java 中单例模式

Java 中常见的单例模式实现有这么几种:饿汉式、双重判断的懒汉式、静态内部类实现的单例、枚举实现的单例。 这里着重讲一下双重判断的懒汉式和静态内部类实现的单例。

双重判断的懒汉式:

public class SingleTon {
    //需要注意的是volatile
    private static volatile SingleTon mInstance;
    private SingleTon() {
    }
    public static SingleTon getInstance() {
        if (mInstance == null) { 
            synchronized (SingleTon.class) {
                if (mInstance == null) {
                    mInstance=new SingleTon();
                }
            }
        }
        return mInstance;
    }
}

双重判断的懒汉式单例既满足了延迟初始化,又满足了线程安全。通过 synchronized 包裹代码来实现线程安全,通过双重判断来提高程序执行的效率。这里需要注意的是单例对象实例需要有 volatile 修饰,如果没有 volatile 修饰,在多线程情况下可能会出现问题。原因是这样的,mInstance=new SingleTon()这一句代码并不是一个原子操作,它包含三个操作:

  1. 给 mInstance 分配内存
  2. 调用 SingleTon 的构造方法初始化成员变量
  3. 将 mInstance 指向分配的内存空间(在这一步 mInstance 已经不为 null 了)

我们知道 JVM 会发生指令重排,正常的执行顺序是1-2-3,但发生指令重排后可能会导致1-3-2。我们考虑这样一种情况,当线程 A 执行到1-3-2的3步骤暂停了,这时候线程 B 调用了 getInstance,走到了最外层的if判断上,由于最外层的 if 判断并没有 synchronized 包裹,所以可以执行到这一句,这时候由于线程 A 已经执行了步骤3,此时 mInstance 已经不为 null 了,所以线

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值