WCDB源码解析

article/2025/10/9 11:09:52

源文链接:http://xiangwangfeng.com/2018/01/08/WCDB-源码解析

起因

最近开了个新项目,项目的主程童鞋引入了 WCDB 代替原先自制的 KeyValueStore 和 FMDB。问为何,答曰:好用,线程安全又高效。又问具体实现细节,答曰:不懂,就是好用。所以作为一个负责任的 前 iOS 开发 决定花点时间扒一扒 WCDB 的实现。

WCDB 的 Wiki 介绍了它的三大特性:易用,高效和完整。通过 ORM 和 WINQWCDB 能提供非常简洁的数据访问接口,减少调用者错误使用的可能性。而通过整个框架的设计及局部代码优化则使得整体较为高效。下面主要扒一扒这两个特性如何达到。至于完整性,尤其是损坏修复由于涉及过多 SQLite 文件格式,不在本文讨论范围内,有兴趣的可以参考 《微信 SQLite 数据库修复实践》 和 《Database File Format》。

ORM 和 WINQ

在 iOS 上实现 ORM 并不是什么新鲜事。一般的流程是:给定一个类,继承基类(非必须),通过 runtime 对其属性进行读写,并使用协议或基类方法进行约束。以 Realm 为例,所有模型对象需要继承自 RLMObject 并通过重写基类方法指定主键,忽略属性等。但市面上的 ORM 实现往往存在如下问题 (也是 WCDB 尝试解决的问题)

  • 部分流程仍需要使用硬编码,有出错概率
  • 约束表达力有限,无法覆盖复杂应用场景

同样以 Realm 为例,指定数据模型主键时,需要重写 + (NSString *)primaryKey 方法,此时不可避免使用硬编码字符串。而在约束表达力上,Realm 也存在无法使用联合索引的问题。

回到 WCDB,它使用内建宏实现 ORM 功能:将一个已有对象进行 ORM 绑定时,我们需要指定其遵守 WCTTableCoding 协议并通过各种内建宏完成绑定和约束 —- 凭借宏强大的表达力能够有效避免上述问题。

一个完整的 WCTTableCoding 协议如下


@protocol WCTTableCoding
@required
+ (const WCTBinding *)objectRelationalMappingForWCDB;
+ (const WCTPropertyList &)AllProperties;
+ (const WCTAnyProperty &)AnyProperty;
+ (WCTPropertyNamed)PropertyNamed; //className.PropertyNamed(propertyName)
@optional
@property(nonatomic, assign) long long lastInsertedRowID;
@property(nonatomic, assign) BOOL isAutoIncrement;
@end

其中和 ORM 关联最密切的自然是 objectRelationalMappingForWCDB 这个方法,通过它返回类和数据库表的绑定关系,即 WCTBinding。而每一个 WCTBinding 又包含字段绑定关系(WCTColumnBinding),约束绑定关系(WCTConstraintBindingBase),索引绑定关系(WCTIndexBinding),分别通过对应的宏实现:字段宏,约束宏和索引宏。关于几种宏的定义和使用可以参考这里。

一个完整 ORM 对应关系如下

下面仅以 WCTSampleORM 中的例子来解释各个类型宏的实现原理,类定义和实现如下:


@interface WCTSampleORM : NSObject@property(nonatomic, assign) int identifier;
@property(nonatomic, retain) NSString *desc;
@property(nonatomic, assign) float value;
@property(nonatomic, retain) NSString *timestamp;
@property(nonatomic, assign) WCTSampleORMType type;@end@implementation WCTSampleORMWCDB_IMPLEMENTATION(WCTSampleORM)
WCDB_SYNTHESIZE(WCTSampleORM, identifier)
WCDB_SYNTHESIZE_COLUMN(WCTSampleORM, desc, "description") //use "description" as column name in Database
WCDB_SYNTHESIZE_DEFAULT(WCTSampleORM, value, 1.0f)
WCDB_SYNTHESIZE_DEFAULT(WCTSampleORM, timestamp, WCTDefaultTypeCurrentTimestamp)
WCDB_SYNTHESIZE(WCTSampleORM, type)WCDB_PRIMARY(WCTSampleORM, identifier)@end

首先是 WCDB_IMPLEMENTATION(WCTSampleORM) 这个宏,展开后是:


static WCTBinding _s_WCTSampleORM_binding(WCTSampleORM.class);
static WCTPropertyList _s_WCTSampleORM_properties;
+(const WCTBinding *) objectRelationalMappingForWCDB 
{ if (self.class != WCTSampleORM.class) { WCDB::Error::Abort("Inheritance is not supported for ORM"); } return &_s_WCTSampleORM_binding; 
} 
+(const WCTPropertyList &)AllProperties 
{ return _s_WCTSampleORM_properties; 
}+(const WCTAnyProperty &)AnyProperty
{static const WCTAnyProperty s_anyProperty(WCTSampleORM.class);return s_anyProperty; 
}
+(WCTPropertyNamed) PropertyNamed 
{return WCTProperty::PropertyNamed; 
}

这里我们只关注 objectRelationalMappingForWCDB 这个方法,会发现这个宏只做了一件事:初始化名为 _s_WCTSampleORM_binding 的静态变量,并通过 objectRelationalMappingForWCDB 方法返回。

(这里有个小贴士,由于 WCDB 的宏定义较为复杂,推荐通过 Xcode 的 [Product -> Perform Action -> Preprocess] 菜单进行代码的预处理)

通过 WCDB_IMPLEMENTATION 宏,我们已经可以从类定义中获取对应的 WCTBinding 信息,唯一的问题是它里面空空如也,需要通过其他宏进行填充,这里主要依靠 字段宏:它提供对象属性和表字段的对应关系。

字段宏

同样以

WCDB_SYNTHESIZE(WCTSampleORM, identifier)

为例,我们将这个宏展开,就得到了如下代码


+(const WCTProperty &)identifier 
{ static const WCTProperty s_property( "identifier", WCTSampleORM.class, _s_WCTSampleORM_binding .addColumnBinding<decltype([WCTSampleORM new].identifier)>("identifier", "identifier"));return s_property; 
} 
static const auto _unused0 = [](WCTPropertyList &propertyList) 
{ propertyList.push_back(WCTSampleORM.identifier); return nullptr; 
}(_s_WCTSampleORM_properties);

这个宏做了两件事情:

  • 生成和属性同名的静态方法,返回值为 WCTProperty,同时将字段绑定关系 WCTColumnBinding添加至 _s_WCTSampleORM_binding
  • 通过匿名函数调用,将上一步返回值加入属性列表 _s_WCTSampleORM_properties

一个完整的字段绑定关系往往包括如下字段

  • 数据模型类名
  • 绑定属性名
  • 数据库字段名字
  • 属性类型

这些都可以通过字段宏自动生成。而 WCDB 在实现字段宏时使用了两个比较 tricky 的写法:

  • addColumnBinding 时使用了 decltype,传入的表达式为 [WCTSampleORM new].identifier。这样做一方面可以使用这个特性在编译期间检查属性拼写,如不慎将 identifier 误拼成 identifer 则会产生编译错误,这就规避了前面提到的硬编码问题,后续的一些宏处理也是同理,不赘述。另一方面也可以通过获取的属性类型动态选择后续使用的 Accessor 类型。

  • _unused0 实际是通过 _unused 和 __COUNTER__ 这个宏拼接而成,通过 __COUNTER__ 可以保证当前文件中每个静态变量的唯一性,同时也可以在该静态方法初始化时调用对应的匿名函数,完成属性绑定关系的添加。

上面的例子是一个简单的字段宏展开结果分析,WCDB 中还内置了几种较为复杂的的字段宏形式,如绑定时指定数据库字段名,或绑定时指定默认值,这些宏实现原理大同小异,无非是宏展开时使用默认值还是上层传入字符串的区别。

这里我们仅以指定默认值这个字段宏实现为例。我们将 WCDB_SYNTHESIZE_DEFAULT(WCTSampleORM, value, 1.0f) 展开后,结果为


static const auto _unused3 = [](WCTBinding *binding) 
{ binding->getColumnBinding(WCTSampleORM.value)->makeDefault<decltype([WCTSampleORM new].value)>( 1.0f);return nullptr; 
}(&_s_WCTSampleORM_binding);

为了设定字段默认值,这里又额外添加了一个匿名函数,通过当前绑定关系 WCTBinding 查询属性对应的 字段绑定关系 WCTColumnBinding,并添加约束 (makeDefault)。

通过上面的分析,我们会发现通过字段宏已经可以完成一个 ORM 的雏形,但为了满足更加复杂的场景需求,我们还需要对绑定关系添加额外的约束和索引。

约束宏和索引宏

WCDB 中的约束宏作用与 SQLite 中的约束基本是一一对应的关系,数据库表内每一种单字段约束,如主键,唯一,非空等都对应 WCDB 中的一种约束宏。下面仅以 WCTSampleORM 中的主键宏为例进行说明。我们将 WCDB_PRIMARY(WCTSampleORM, identifier) 展开后得到

static const auto _unused7 = [](WCTBinding *binding)
{ binding->getColumnBinding(WCTSampleORM.identifier)->makePrimary(WCTOrderedNotSet, false, WCTConflictNotSet);return nullptr; }
(&_s_WCTSampleORM_binding);

我们会发现在单字段约束的实现上和字段宏的默认值实现完全一致:通过 _s_WCTSampleORM_binding 查找当前属性对应的字段绑定关系 (WCTColumnBinding),并设置相应的约束。

而多字段约束和索引则需要被记录在额外的约束列表(WCTConstraintBindingBase List/Map) 和索引列表(WCTIndexBinding List/Map)中。上文提到联合索引的实现则是通过同名索引合并的逻辑:每个索引宏都可以指定当前索引的后缀,相同后缀的索引以 WCTIndex 的形式被记录存储于同一项索引绑定关系中(WCTIndexBinding),并在生成索引命令时进行合并(详将 WCTIndexBinding::generateCreateIndexStatement)。

而一旦通过对象获取绑定关系后,后续的流程就非常简单了,无非是 CRUD 而已。

  • C 核心代码参考 WCTInterface 的 createTableAndIndexesOfName:withClass:andError: 方法,通过 WCTColumnBinding 和 WCTConstraintBinding 列表生成建表命令,再通过 WCTIndexBinding列表添加对应索引。如果已存在表,建表过程则变成更新 column 操作,并忽略约束信息(SQLite只实现 Alert Table 的有效子集)。

  • R 核心代码参考 WCTSelect 的 extractPropertyToObject:atIndex:withColumnBinding: 方法,通过 WINQ 的链式调用获得最终查询结果,并调用上述方法将数据设置给对象属性。

  • U 核心代码参考 WCTInsert 和 WCTUpdate 的初始化方法,通过 WCTTableCoding 获取 AllProperties 信息并配合当前实例属性进行链式调用,最后输出 SQL 语句执行。

  • D 核心代码参考 WCTDelete 的 excute 方法,首先通过 WCTDelete 的链式调用生成最终的 WCTDelete 对象,最后输出 SQL 语句并执行。

上面就个 WCDB 实现 ORM 大致流程。虽然并不一定是最优解,但不可否认的确是非常细致和考虑周到的实现,基本涵盖了日常开发的 99% 的需求。

而 WINQ 则相对更加简单,其核心思路无非是用具体的类调用代替硬编码的 SQL 语句,同时提供足够丰富的排列组合方式。基本可以将 WINQ 的实现分为两层:

  • C++ 实现层,位于 abstract 目录,主要继承自 Describable ,包括表达式 expr 和执行语句 statement ,用于简化拼装 SQL 语句过程和提供更多的组合可能。
  • Objective-C 接口层,胶水代码,主要起到简化 Objective-C 调用的作用,内部大多持有 C++ 实现的 statement

线程安全和性能

线程安全

在讲线程安全和性能前,必须要了解 SQLite 是怎么实现线程安全和达到高性能,具体可以参考《SQLite 线程安全和并发》。使用 SQLite 时常规的优化方案无非是

  • 缓存 sqlite3_prepare 编译结果
  • 使用 WAL 模式
  • 采用多线程模式,单写多读
  • 合理安排事务

接下来就来扒一扒 WCDB 线程安全的实现细节。这部分代码位于 core 目录中,即所谓的核心层。主要围绕 HandleHandlePool 和 Database 三个类完成。其中 Handle 主要负责持有 sqlite3 指针,即平时说的数据库连接,和 FMDB 对比的话,基本可以认为它是一个 C++ 版本的 FMDatabase。而 HandlePool 的作用基本等同于 FMDatabasePool,起到管理连接的作用。当进行数据库访问时,通过 HandlePool 返回当前可用的连接 (Handle) 进行操作,使用完毕后则回收。详细可以参考 HandlePool 的 flowOut 和 flowBack 方法


RecyclableHandle HandlePool::flowOut(Error &error)
{m_rwlock.lockRead();std::shared_ptr<HandleWrap> handleWrap = m_handles.popBack();if (handleWrap == nullptr) {if (m_aliveHandleCount < s_maxConcurrency) {handleWrap = generate(error);if (handleWrap) {++m_aliveHandleCount;if (m_aliveHandleCount > s_hardwareConcurrency) {WCDB::Error::Warning(("The concurrency of database:" +std::to_string(tag.load()) + " with " +std::to_string(m_aliveHandleCount) +" exceeds the concurrency of hardware:" +std::to_string(s_hardwareConcurrency)).c_str());}}} else {Error::ReportCore(tag.load(), path, Error::CoreOperation::FlowOut,Error::CoreCode::Exceed,"The concurrency of database exceeds the max concurrency",&error);}}if (handleWrap) {handleWrap->handle->setTag(tag.load());if (invoke(handleWrap, error)) {return RecyclableHandle(handleWrap, [this](std::shared_ptr<HandleWrap> &handleWrap) {flowBack(handleWrap);});}}handleWrap = nullptr;m_rwlock.unlockRead();return RecyclableHandle(nullptr, nullptr);
}void HandlePool::flowBack(const std::shared_ptr<HandleWrap> &handleWrap)
{if (handleWrap) {bool inserted = m_handles.pushBack(handleWrap);m_rwlock.unlockRead();if (!inserted) {--m_aliveHandleCount;}}
}

简单来说,WCDB 的连接池通过读写锁保证线程安全,和 FMDatabasePool 使用 gcd queue 并没有太多差异,一些肉眼可见的区别在于

  • WCDB 并不对外暴露数据库连接对象,以减少外面错误使用的几率。
  • WCDB 在连接池之外还提供基于 ThreadLocal 的缓存机制,保证当前事务操作下永远只使用同一个连接。 (详见 Database::flowOut)
  • 内部自动约束并发数,并对不合理的并发做出提示。比如连接数超过 std::thread::hardware_concurrency() 就会有警告。 (详见 HandlePool::flowOut)
  • 连接回收基于 C++ 变量作用域。这一点上在我看倒没有明显的优劣点,反倒有点炫技的成分,为了实现这一点还需要额外引入 RecylceHandle
  • 支持内存不足时的数据库连接自动回收。 (详见 Database::purgeFreeHandles)

这些细微的差别能够使得 WCDB 在保证线程安全和合理并发的前提下,使用起来更加方便安心。

性能

除了上面说的合理设计框架,合理提供并发外,WCDB 还做了一些额外性能有点。下面仅列出一些我读代码和 wiki 的发现。

  • checkpointing 优化

在使用 WAL 模式时,默认情况下,当 WAL 文件 大小超过 1000 个页大小时,SQLite 就会尝试将 WAL 文件 写回数据库文件,这就是所谓的 checkpointing。(详见 wal) 那么在大量数据批量写入的场景下,可能会不停的产生提交文件到数据库的事务。而 WCDB 的做法则是在触发 checkpointing 时,通过延时队列进行,避免大量写入时不停的触发 WalCheckpoint 调用。

代码如下


[](std::shared_ptr<Handle> &handle, Error &error) -> bool {handle->registerCommittedHook([](Handle *handle, int pages, void *) {static TimedQueue<std::string> s_timedQueue(2);if (pages > 1000) {s_timedQueue.reQueue(handle->path);}static std::thread s_checkpointThread([]() {pthread_setname_np(("WCDB-" + Database::defaultCheckpointConfigName).c_str());while (true) {s_timedQueue.waitUntilExpired([](const std::string &path) {Database database(path);WCDB::Error innerError;database.exec(StatementPragma().pragma(Pragma::WalCheckpoint),innerError);});}});static std::once_flag s_flag;std::call_once(s_flag,[]() { s_checkpointThread.detach(); });},nullptr);return true;},

通过 TimedQueue 将同个数据库的 WalCheckpoint 合并延迟到 2 秒后统一进行。

  • SQLITE_BUSY 优化

SQLite 的机制并不允许进行多线程同时进行写操作,当发生多个线程进行写操作时未得到锁的那一方将直接返回 SQLITE_BUSY。从 FMDB 的提交记录我们可以看出,ccgus 对怎么处理 SQLITE_BUSY 也是相当头疼,具体可以参考 FMDB 中关于 SQLITE_BUSY 的 issues。目前 FMDB 的做法是默认重试 2 秒,在此期间调用 sqlite3_sleep 随机休眠几十毫秒,等待另外一个线程释放锁。这种处理方式可以较大程度上缓解 SQLITE_BUSY 的问题,但仍不可避免。这也是 WCDB Benchmark 认为 FMDB 无法支持 Multi-Thread WriteWrite 的原因。

而 WCDB 的处理方式则相当粗暴:通过修改 sqlcipher 源码,如果当前未进入事务状态而产生 SQLITE_BUSY 则会挂起等待,超时时间为 10 秒。详细代码可以参见 btree.c 中的 sqlite3BtreeBeginTrans 方法。


do {//一堆判断sqlite3PagerBegin(pBt->pPager,wrflag>1,sqlite3TempInMemory(p->db));//一堆判断
}while( (rc&0xFF)==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE &&btreeInvokeBusyHandler(pBt) );
  • 编译选项优化

SQLite 有大量预编译宏选项可以配置,具体可以参见 sqliteLimit.h 和 sqliteInt.hWCDB 也对此作了较多配置,具体可以参考 sqlchiper-preprocessed.xcodeproj 中的宏定义。像我在 《SQLite 分表》 提到的 SQLITE_MALLOC_SOFT_LIMIT 就是偷师自微信,通过设置它为 0,可以加快在大量表情况下的初始化过程。从微信分享给出的资料还有相当多的优化项,如 开启 mmap,禁用文件锁(针对 iOS单进程的场景)等,具体可以参考 《微信iOS SQLite源码优化实践》 并查找对应源码进行对照。




http://chatgpt.dhexx.cn/article/G4FqhIqT.shtml

相关文章

IOS数据存储 之WCDB (二)WCDB.swift使用篇

IOS数据存储 之WCDB &#xff08;二&#xff09;WCDB.swift使用篇 1.WCDB.Swfit基础使用1.1 WCDB.Swfit 简介1.1.1 模型绑定1.1.2 创建数据库与表1.1.3 操作数据1.1.3.1 插入操作1.1.3.2 查找操作1.1.3.3 更新操作1.1.3.4 删除操作 1.2. 模型绑定1.2.1 Swift 模型绑定1.2.2 字段…

Android使用WCDB+Room 总结

最近项目有需要用到wcdb数据库&#xff0c;并且保证和IOS互通数据&#xff0c;在网上找很多相关资料&#xff0c;最后还是靠自己一点点摸索成功&#xff0c;现在做个总结。 一、在gradle 里加上 WCDB 相关的 room 组件 def room_version "2.3.0"// wcdb数据库和roo…

IOS数据存储 之WCDB (一)

IOS数据存储 之WCDB &#xff08;一&#xff09; 1. WCDB 简介1.1 使用WCDB框架3大优势1.2 WCDB 的一些基础概念1.2.1 类字段绑定&#xff08;ORM&#xff09;1.2.2 WINQ&#xff08;WCDB语言集成查询&#xff09;1.2.2.1 字段映射与运算符1.2.2.2 字段组合1.2.2.3 AllProperti…

iOS开发 数据存储之WCDB的介绍

一.介绍 WCDB是一个高效、完整、易用的移动数据库框架,基于SQLCipher,支持iOS,macOS和Android 二.基本特性 易用,WCDB支持一句代码即可将数据取出并组合为object WINQ(WCDB语言集成查询):通过WINQ,开发者无须为了拼接SQL的字符串而写一大坨胶水代码ORM(Object Relational Ma…

开源微信小程序自助建站系统源码 含精美的多行业模板和搭建教程

分享一个微信小程序自助建站系统源码&#xff0c;含各行各业的小程序模板和搭建教程&#xff0c;可一键切换模板&#xff0c;自由DIY&#xff0c;搭建属于你自己的小程序。 特色功能一览&#xff1a; 11、支持创建多个小程序&#xff01;&#xff08;没有数量限制&#xff0c;后…

强大易用的开源建站工具Halo

最近无意间看到别人的博客外观非常美观&#xff0c;便萌生了偷师学艺的想法…所以就看到看了Halo这个开源的建站项目,其实使用起来非常简单&#xff0c;但是想要做一个类似的开源建站工具&#xff0c;谈何容易 访问官网 https://halo.run/ 使用docker部署 搜索镜像halo do…

14个免费好用的建站工具

有时候&#xff0c;我们想建立一个独立网站的时候&#xff0c;苦于自己技能不够&#xff0c;而迟迟没有行动&#xff0c;其实&#xff0c;我们真正的去构建一个独立网站的时候&#xff0c;我们并不需要多复杂的技术。我们也不一定要成为非常专业的程序员&#xff0c;因为现在&a…

推荐一款免费开源的建站系统 - AnqiCMS

安企内容管理系统(AnqiCMS)&#xff0c;是一款使用 GoLang 开发的企业站内容管理系统&#xff0c;它部署简单&#xff0c;软件安全&#xff0c;界面优雅&#xff0c;小巧&#xff0c;执行速度飞快&#xff0c;使用 AnqiCMS 搭建的网站可以防止众多安全问题发生。AnqiCMS 的设计…

介绍一款开源、高性价比的在线教育建站系统

今天给大家介绍一款开源在线教育建站系统——edusoho&#xff0c;项目是用PHP开发&#xff0c;所以基本上会搭建php站点就可以完成本次的搭建。 先看看安装之后的登录界面。 去官网下载源码 笔者下载企培开源版&#xff1a;edusoho-ct-21.4.5.zip 系统说明 1.系统&#xff1…

国内好用的五款开源建站系统

推荐5款优秀的开源建站系统,都有免费版本,有需要可以去试试。蝉知 蝉知系统是一款开源的的企业营销自助建站系统。它专为企业营销设计,伪静态网址、关键词、语义化结构,内置流量统计。 蝉知功能全面,文章发布、会员管理、论坛评论、产品展示等,并内置商城系统,商品、订…

免费开源的建站程序大全,不会编程也可以自助搭建网站了哦

想建网站又不会编程的小伙伴有福啦&#xff0c;本期推荐一些开源的cms建站程序&#xff0c;不需要写后端的任何逻辑代码&#xff0c;轻轻松松就可以建立自己的网站了&#xff0c;当然&#xff0c;要想网站有自己的个性&#xff0c;模版还是需要自己写的&#xff0c;只需要会简单…

绝了!小说建站项目完整开源

超棒的开源小说文学建站 CMS 系统&#xff0c;作为面试项目有牌面儿&#xff01; 编程导航开源仓库&#xff1a;https://github.com/liyupi/code-nav 大家好&#xff0c;我是鱼皮&#xff0c;今天给大家推荐一个优秀的开源 Java 全栈项目。 小说精品屋&#xff0c;是一套非常完…

最新首发自助建站系统源码,傻瓜式一键建站系统源码,高度开源支持专业在线自助建站服务平台软件

一佰互联,巅云门户自助建站系统v8建站平台版&#xff0c;历经3年不断打磨终于上线了。专业PS级大师级高端响应式智能建站平台软件&#xff0c;只为网络公司而生&#xff0c;采用国内知名开源php框架,Thinkphp6vue.js前端数据响应系统,实现了在线自助开通网站&#xff0c;企业站…

四大免费开源建站系统

原文&#xff1a;四大免费开源建站系统 - 知乎 第一&#xff1a; WordPress WordPress的主流客户是企业/个人的官网。一家公司不一定会在网上卖东西&#xff0c;但一定会需要一个官网。用WordPress做官网可谓是性价比最优选择。如果没有预算&#xff0c;你可以自己买几十美金的…

有哪些免费好用的开源建站程序/系统,推荐下?

我推荐WordPress建站程序&#xff0c;学习入门门槛很低&#xff0c;全世界近三分之一的网站都是采用wordpress,所以没有理由不去学习它。 我从一名小白变为wordpress建站老手&#xff0c;对比过很多建站程序&#xff0c;还是觉得wordpress能帮我节省时间和精力&#xff0c;让我…

开源自助建站系统源码完整源码+搭建教程 傻瓜式一键建站系统源码

一键傻瓜式自助建站系统源码&#xff0c;目前包含七百多套完整网站模板&#xff0c;全部都是响应式网站模板&#xff0c;傻瓜一键自助建站。开发组合PHPmysql&#xff0c;功能强大。 一键自助建站系统源码带安装教程&#xff0c;源码下载&#xff1a;春哥技术博客获取。自助建站…

五款开源免费的建站系统推荐

最近研究了下开源的建站系统&#xff0c;推荐5款国内的吧&#xff0c;都有免费版本&#xff0c;有需要可以去试试。 ECTouch ECTouch是一款开源免费的移动商城网店系统。能够帮助企业和个人快速构建手机移动商城并减少二次开发带来的成本。 ECTouch采用PHPMYSQL方式运行&…

Excel如何按照指定顺序排列

需求&#xff1a;要求按照指定的code出相应的value值&#xff0c;后台可能查出来的code对应的值无法与给定的顺序匹配上&#xff0c;当然可以用代码解决&#xff0c;但是如果想偷懒可以直接用Excel强大的自定义排序解决。 Step1&#xff1a;在Excel中自定义排序&#xff1a;点击…

(19)写一个函数,用”起泡法“对输入的10数字符按由小到大顺序排列

用”起泡法“对输入的10数字符按由小到大顺序排列 起泡法&#xff1a;即每一次将相邻两个数进行比较&#xff0c;若符合条件则交换两个数的值。每进行一趟排序&#xff0c;最大的一个数变成最后一个数。以此类推&#xff0c;直至整个数组按照由小到大排列。 举例&#xff1a;…

G2Plot 折线图表错乱,不按顺序排列

问题描述&#xff1a; 问题&#xff1a;G2Plot 折线图表错乱&#xff0c;不按顺序排列 BUG如图&#xff1a; 原因分析&#xff1a; 问题分析&#xff1a; 在仔细检查了参数值时&#xff0c;分析没有问题的图表的 value 值类型为Number&#xff0c;出问题的是String类型。 有…