【Android】SharedPreferences源码分析,2024年最新体系面试问题

SharedPreferencesImpl.java中的相关代码:


private void loadFromDisk() {

    // 同步锁,锁对象为mLock

        synchronized (mLock) {

            // 双重锁,若已经加载过,则返回

            if (mLoaded) {

                return;

            }

            // 若备份文件存在,说明不是第一次创建该SharedPreferences

            if (mBackupFile.exists()) {

                // 删除原文件

                mFile.delete();

                // 将备份文件的内容复制到原文件,若操作成功,会删除备份文件

                // 即使用备份文件作为原文件,但名字为原文件的名字

                mBackupFile.renameTo(mFile);

            }

        }

        // 用于临时保存磁盘中加载解析后的键值对数据

        Map<String, Object> map = null;

        // 用于保存文件的信息

        StructStat stat = null;

        // 用于保存加载数据中产生的异常

        Throwable thrown = null;

        try {

            // 获取文件的信息

            stat = Os.stat(mFile.getPath());

            // 若文件可读

            if (mFile.canRead()) 

                BufferedInputStream str = null;

                try {

                    // 创建缓存输入流,缓存大小为16 * 1024KB

                    str = new BufferedInputStream(

                            new FileInputStream(mFile), 16 * 1024);

                    // 将文件中的数据解析成键值对,保存在map中

                    map = (Map<String, Object>) XmlUtils.readMapXml(str);

                } catch (Exception e) {

                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);

                } finally {

                    // 关闭缓存输入流

                    IoUtils.closeQuietly(str);

                }

            }

        } catch (ErrnoException e) {

            // An errno exception means the stat failed. Treat as empty/non-existing by

            // ignoring.

        } catch (Throwable t) {

            thrown = t;

        }

        // 同步锁,锁对象为mLock

        synchronized (mLock) {

            // 设置状态为加载完成

            mLoaded = true;

            mThrowable = thrown;



            try {

                // 若加载解析数据中没有发生异常

                if (thrown == null) {

                    // 解析的数据不为空

                    if (map != null) {

                        // 将结果保存在全局变量中

                        mMap = map;

                        mStatTimestamp = stat.st_mtim;

                        mStatSize = stat.st_size;

                    } else { // 若解析的数据为空,即文件中没有数据

                        // 对全局变量进行初始化

                        mMap = new HashMap<>();

                    }

                }

            } catch (Throwable t) {

                mThrowable = t;

            } finally {

                // 释放锁,随机唤醒一个等待mLock锁的线程

                mLock.notifyAll();

            }

        }

}



3.从SharedPreferences中获取数据


SharedPreferencesImpl类中提供很多不同的get方法,用来获取不同类型的数据。这些方法内部实现的原理是类似的。以getString方法为例。

SharedPreferencesImpl.java中的相关代码:


    @Override

    @Nullable

public String getString(String key, @Nullable String defValue) {

   		// 同步锁 锁对象为mLock

        synchronized (mLock) {

            // 让当前的线程等待从磁盘加载数据的线程加载完数据。

            // 详解在1)处

            awaitLoadedLocked();

            // 从全局变量mMap中获取数据,并进行强制转换

            String v = (String)mMap.get(key);

            // 若获取数据不存在,则返回默认值,否则返回获取的数据

            return v != null ? v : defValue;

        }

}



1)awaitLoadedLocked方法

SharedPreferencesImpl.java中的相关代码:


    @GuardedBy("mLock")

private void awaitLoadedLocked() {

   		// 若从硬盘加载数据没有完成

        if (!mLoaded) {

            // 调用线程策略处理

            BlockGuard.getThreadPolicy().onReadFromDisk();

        }

 	    // 若从硬盘加载数据没有完成,则循环执行

        while (!mLoaded) {

            try {

                // 释放锁,等待

                mLock.wait();

            } catch (InterruptedException unused) {

            }

        }

        // 若等待期间发生异常,则抛出异常

        if (mThrowable != null) {

            throw new IllegalStateException(mThrowable);

        }

}



4.获取Editor对象


调用SharedPreferences对象的edit方法获取Editor对象,然后对Editor对象进行操作。

SharedPreferencesImpl.java中的相关代码:


@Override

public Editor edit() {

    // 同步锁,锁对象为mLock

    synchronized (mLock) {

        // 让当前的线程等待从磁盘加载数据的线程加载完数据

        awaitLoadedLocked();

    }

    // 创建EditorImpl对象并返回

    return new EditorImpl();

}



三.EditorImpl类

===============================================================================

Editor是一个接口,它的具体实现是EditorImpl类。因此最终获取的为EditorImpl对象。

EditorImpl类是SharedPreferencesImpl类的内部类。SharedPreferences中对数据的增、删、改都是通过调用Editor的相关方法来实现的。

1.重要的全局变量


SharedPreferencesImpl.java中的相关代码:


	private final Object mEditorLock = new Object();// 锁



    @GuardedBy("mEditorLock")

    private final Map<String, Object> mModified = new HashMap<>();// 用于临时存储需要写入磁盘的数据或需要移除的数据,之后统一处理



    @GuardedBy("mEditorLock")

	private boolean mClear = false;// 用于表示是否清空SharedPreferences



2.添加数据


EditorImpl类中提供很多不同的put方法来添加不同类型的数据。这些方法内部实现的原理是类似的。以putString方法为例。

SharedPreferencesImpl.java中的相关代码:


    @Override

	public Editor putString(String key, @Nullable String value) {

   		// 同步锁,锁对象为mEditorLock

        synchronized (mEditorLock) {

            // 向全局变量mModified添加数据

            mModified.put(key, value);

            // 返回

            return this;

        }

	}



3.清空数据


SharedPreferencesImpl.java中的相关代码:


    @Override

	public Editor clear() {

    	// 同步锁,锁对象为mEditorLock

        synchronized (mEditorLock) {

            // 清除标志位为true

            mClear = true;

            // 返回

            return this;

        }

	}



4.删除数据


SharedPreferencesImpl.java中的相关代码:


    @Override

	public Editor remove(String key) {

  	   // 同步锁,锁对象为mEditorLock

       synchronized (mEditorLock) {

            // 将全局变量mModified对应的value改为自身,表示这个键值对需要删除

            mModified.put(key, this);

            // 返回

            return this;

        }

	}



5.提交数据到磁盘


EditorImpl中有apply和commit两种方法来实现将数据写入磁盘。apply为异步方法,commit方法为同步方法。

同步提交数据:commit

SharedPreferencesImpl.java中的相关代码:


    @Override

	public boolean commit() {

   		// 对EditorImpl对象的操作(put、remove、clear等)进行整合处理

  		// 详解在1)处

        MemoryCommitResult mcr = commitToMemory();

        // 将数据写入磁盘

        // 详解在3)处

        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);

        try {

            // 阻塞等待写入过程完成

            mcr.writtenToDiskLatch.await();

        } catch (InterruptedException e) {

            return false;

        } finally {

            }

        }

        // 对注册当前SharedPreferences对象的监听器进行回调

        notifyListeners(mcr);

        // 返回

        return mcr.writeToDiskResult;

	}



1)commitToMemory方法

SharedPreferencesImpl.java中的相关代码:


private MemoryCommitResult commitToMemory() {

        long memoryStateGeneration; // 用于记录

        List<String> keysModified = null; // 用于存储发生变化的键

        Set<OnSharedPreferenceChangeListener> listeners = null; // 用于存储监听器

        Map<String, Object> mapToWriteToDisk; // 用于记录需要写入磁盘的所有的数据

        

		// 同步锁,锁对象为SharedPreferencesImpl.this.mLock

        synchronized (SharedPreferencesImpl.this.mLock) {

            // 若当前有线程在写入    

            if (mDiskWritesInFlight > 0) {

                // 复制mMap,对新数据进行操作    

                mMap = new HashMap<String, Object>(mMap);

            }

            // 获取全局变量

            mapToWriteToDisk = mMap;

            // 当前进行写入操作的线程数加一

            mDiskWritesInFlight++;



            // SharedPreferences是否有监听器

            boolean hasListeners = mListeners.size() > 0;

            // 若有监听器

            if (hasListeners) {

                // 进行初始化

                keysModified = new ArrayList<String>();

                listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());

            }

             

            // 同步锁,锁对象为mEditorLock

            synchronized (mEditorLock) {

                // 表示内存数据是否发生变化

                boolean changesMade = false;



                // 若用户调用clear方法清空数据

                if (mClear) {

                    // 若写入的数据不为空,即有需要清除的数据

                    if (!mapToWriteToDisk.isEmpty()) {

                        // 表示内存数据发生变化

                        changesMade = true;

                        // 清空内存中的数据

                        mapToWriteToDisk.clear();

                    }

                    // 重置清除标志

                    mClear = false;

                }

                 

                // 对提交到Editor中的数据进行遍历

                for (Map.Entry<String, Object> e : mModified.entrySet()) {

                    // 获取键

                    String k = e.getKey();

                    // 获取值

                    Object v = e.getValue();

                    // 若值为空或自身,表示该键值对需要删除

                    if (v == this || v == null) {

                        // 若写入磁盘中的数据没有k这个键

                        // 说明之前没有添加k和其对应的v数据到磁盘中

                        // 本次删除是无效的

                        if (!mapToWriteToDisk.containsKey(k)) {

                            // 跳过本次操作

                            continue;

                        }

                        // 从打算写入磁盘的数据中移除

                        mapToWriteToDisk.remove(k);

                    } else { //若值不为空,也不为自身

						//说明添加了新键值对或修改了键值对的值

                        // 若打算写入磁盘的数据包含k这个键,说明对值进行了修改

                        if (mapToWriteToDisk.containsKey(k)) {

                            // 获取k键之前的值

                            Object existingValue = mapToWriteToDisk.get(k);

                            // 若之前的值不为空,同时和现在的值相同

                            // 说明实际没有修改

                            if (existingValue != null && existingValue.equals(v)) {

                                // 跳过本次操作

                                continue;

                            }

                        }

                        // 将新添加的键值对添加到打算写入磁盘的数据中

                        mapToWriteToDisk.put(k, v);

                    }



                    // 表示内存数据发生变化

                    changesMade = true;

                    // 若有监听器

                    if (hasListeners) {

                        // 则对变换的键进行保存

                        keysModified.add(k);

                    }

                }

                // 内存整理结束,所有需要写入磁盘的数据保存在mapToWriteToDisk中

                // 清空EditorImpl中临时存储的数据

                mModified.clear();



                // 若内存数据发生变换

                if (changesMade) {

                    // 内存变换次数加一

                    mCurrentMemoryStateGeneration++;

                }



                // 赋值到方法内的局部变量

                memoryStateGeneration = mCurrentMemoryStateGeneration;

            }

        }

        // 创建MemoryCommitResult对象并返回

        // 详解在2)处

        return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,

                    mapToWriteToDisk);

}



2)MemoryCommitResult类

MemoryCommitResult类是SharedPreferencesImpl类的静态内部类。用来保存对内存整理的结果。

SharedPreferencesImpl.java中MemoryCommitResult类的全部代码:


    private static class MemoryCommitResult {

        final long memoryStateGeneration; // 用于保存当前为第几次内存变换

        @Nullable final List<String> keysModified; // 用于保存发生变化的键

        @Nullable final Set<OnSharedPreferenceChangeListener> listeners; // 用于保存监听器

        final Map<String, Object> mapToWriteToDisk; // 用于保存整理后的需要写入磁盘的全部数据

        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); // 用于同步阻塞



        @GuardedBy("mWritingToDiskLock")

        volatile boolean writeToDiskResult = false; // 表示写入磁盘是否成功

        boolean wasWritten = false; // 表示是否执行写入磁盘的操作



        // 构造方法

        private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,@Nullable Set<OnSharedPreferenceChangeListener> listeners,

                Map<String, Object> mapToWriteToDisk) {

            // 保存传入的参数

            this.memoryStateGeneration = memoryStateGeneration;

            this.keysModified = keysModified;

            this.listeners = listeners;

            this.mapToWriteToDisk = mapToWriteToDisk;

        }



        // 该方法用于设置硬盘写入的结果

        void setDiskWriteResult(boolean wasWritten, boolean result) {

            // 保存参数

            this.wasWritten = wasWritten;

            writeToDiskResult = result;

            // writtenToDiskLatch减一,由于减一后为0,因此解除阻塞状态

            writtenToDiskLatch.countDown();

        }

	}



3)enqueueDiskWrite方法

SharedPreferencesImpl.java中MemoryCommitResult类的全部代码:


    private void enqueueDiskWrite(final MemoryCommitResult mcr,

                                  final Runnable postWriteRunnable) {

        // 通过判断参数postWriteRunnable是否为空

		// 表示需要同步写入磁盘还是异步写入磁盘

        final boolean isFromSyncCommit = (postWriteRunnable == null);

        // 创建writeToDiskRunnable对象,封装写入磁盘的核心操作

        final Runnable writeToDiskRunnable = new Runnable() {

                @Override

                public void run() {

                    // 同步锁,锁对象为mWritingToDiskLock

                    synchronized (mWritingToDiskLock) {

                        // 写入磁盘

                        writeToFile(mcr, isFromSyncCommit);

                    }

                    // 同步锁,锁对象为mLock

                    synchronized (mLock) {

                        // 当前写入磁盘的线程数量减一

                        mDiskWritesInFlight--;

                    }

                    // 若postWriteRunnable不为空

                    if (postWriteRunnable != null) {

                        // 调用run方法

                        postWriteRunnable.run();

                    }

                }

            };



        // 若为同步写入磁盘

        if (isFromSyncCommit) {

            // 表示是否有其它线程正在写入

            boolean wasEmpty = false;

            // 同步锁,锁对象为mLock

            synchronized (mLock) {

                // 若当前写入磁盘的线程数量为1

                // 说明除了本线程,没有其它线程写入,wasEmpty为true

                wasEmpty = mDiskWritesInFlight == 1;

            }

            // 若没有其它线程正在写入数据

            if (wasEmpty) {

                // 调用上面writeToDiskRunnable对象封装的run方法

				// 同步写入数据

                writeToDiskRunnable.run();

                // 返回

                return;

            }

        }



        // 若为异步写入,或同步写入时有其它线程正在写入,则调用本方法异步写入

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

	}



从enqueueDiskWrite方法的代码可以知道,若为同步方法,会调用writeToFile方法。

4)writeToFile方法

SharedPreferencesImpl.java中的相关代码:


@GuardedBy("mWritingToDiskLock")

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {

        // 判断要写入的文件是否存在

        boolean fileExists = mFile.exists();



        // 若文件存在

        if (fileExists) {

            // 表示是否要写入

            boolean needsWrite = false;



            // 若写入磁盘的次数小于内存变化的次数

            // 因为当写入磁盘次数大于等于磁盘变换次数,说明写入操作是重复的。

            // 即写入的数据和磁盘的数据相同

            if (mDiskStateGeneration < mcr.memoryStateGeneration) {

                // 若为同步写入

                if (isFromSyncCommit) {

                    // 表示需要写入

                    needsWrite = true;

                } else {// 若为异步写入

                    // 同步锁, 锁对象为mLock

                    synchronized (mLock) {

                        // 若内存变化的次数和当前内存整合的次数相同

                        // 说明当前要写入的数据为最新的数据

                        // 防止中间数据也写入,造成IO资源浪费

                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {

                            // 表示需要写入

                            needsWrite = true;

                        }

                    }

                }

            }



            若不需要写入

            if (!needsWrite) {

                // 设置结果

                // 写入结果为true,因为当前数据不是最终要写入的数据,之后会写入

                mcr.setDiskWriteResult(false, true);

                // 返回

                return;

            }



            // 表示备份文件是否存在

            boolean backupFileExists = mBackupFile.exists();



            // 若备份文件不存在

            if (!backupFileExists) {

                // 将原文件的内容复制到备份文件,文件名为备份文件的名

                // 若操作失败

                if (!mFile.renameTo(mBackupFile)) {

                    Log.e(TAG, "Couldn't rename file " + mFile

                          + " to backup file " + mBackupFile);

                    // 设置结果,写入失败

                    mcr.setDiskWriteResult(false, false);

                    // 返回

                    return;

                }

            } else {//若备份文件存在

                // 删除原文件

                mFile.delete();

            }

        }



        try {

            // 创建文件输出流

            // 详解在5)处

            FileOutputStream str = createFileOutputStream(mFile);



            // 若文件输出流创建失败

            if (str == null) {

                // 设置结果,写入失败

                mcr.setDiskWriteResult(false, false);

                // 返回

                return;

            }

            // 核心方法,将内存中的数据按Xml文件格式写入到文件

            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);



            // 等待磁盘写入文件完成

            FileUtils.sync(str);

            // 关闭文件输出流

            str.close();

            // 设置文件的权限

            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);



            try {

                // 获取文件信息

                final StructStat stat = Os.stat(mFile.getPath());

                // 同步锁,锁对象为mLock

                synchronized (mLock) {

                    // 保存文件修改时间

                    mStatTimestamp = stat.st_mtim;

                    // 保存文件大小

                    mStatSize = stat.st_size;

                }

            } catch (ErrnoException e) {

                // Do nothing

            }



            // 写入成功后,删除备份文件

            mBackupFile.delete();

            // 更新硬盘写入次数

            mDiskStateGeneration = mcr.memoryStateGeneration;

            // 设置结果,写入成功

            mcr.setDiskWriteResult(true, true);

            // 返回

            return;



**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
![img](https://img-blog.csdnimg.cn/img_convert/152e1a554a58a00c107f492d043b1d15.png)
![img](https://img-blog.csdnimg.cn/img_convert/248205cfbe2a30534ae44de128ddff50.png)
![img](https://img-blog.csdnimg.cn/img_convert/c2e05e70f1bdb6bd49f9ac5b9cf58af1.png)
![img](https://img-blog.csdnimg.cn/img_convert/574dffb882f93f288072292120e78f49.png)
![img](https://img-blog.csdnimg.cn/img_convert/06990093392f01eb1b130318902f6493.png)
![img](https://img-blog.csdnimg.cn/img_convert/27b0d5709bc92fe652c6e637e368f1c7.png)
![img](https://img-blog.csdnimg.cn/13f2cb2e05a14868a3f0fd6ac81d625c.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)**
![img](https://img-blog.csdnimg.cn/img_convert/b5d07763fce99a03f9d52966d96849c5.png)



### 结语

看到这篇文章的人不知道有多少是和我一样的Android程序员。

35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。
![](https://img-blog.csdnimg.cn/img_convert/0884789b0c4c1467580f779d00a69c0a.webp?x-oss-process=image/format,png)



**一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
![img](https://img-blog.csdnimg.cn/img_convert/0c7aa6c76287b8421f3e53f0f2adcaf7.png)
移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中...(img-kpEWtuoz-1712683519320)]
[外链图片转存中...(img-IsSmptOq-1712683519321)]
[外链图片转存中...(img-dsDx1xgl-1712683519321)]
[外链图片转存中...(img-q4259509-1712683519322)]
[外链图片转存中...(img-03g1P1yi-1712683519322)]
[外链图片转存中...(img-rrxetwtV-1712683519322)]
![img](https://img-blog.csdnimg.cn/13f2cb2e05a14868a3f0fd6ac81d625c.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)**
[外链图片转存中...(img-3XGexpOi-1712683519323)]



### 结语

看到这篇文章的人不知道有多少是和我一样的Android程序员。

35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。
[外链图片转存中...(img-vJKACdZh-1712683519323)]



**一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
[外链图片转存中...(img-eWkL85gX-1712683519324)]
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值