iOS系统崩溃的捕获
iOS系统崩溃的捕获
相信大家在开发iOS程序的时候肯定写过各种Bug,而其中最为严重的Bug就是会导致崩溃的Bug(一般来说妥妥的P1级)。在应用软件大大小小的各种异常中,崩溃确实是最让人难以接受的行为。毕竟崩溃意味着用户将丢失应用程序运行中的所有上下文环境,丢失其所有未保存的数据,会带给用户最糟糕的使用体验。
所以在应用的开发阶段,我们一定要杜绝此类可能造成应用程序无法使用的崩溃。但是很多崩溃并不是自己在开发阶段就能预料到的,此时就需要一种能够线上获取崩溃日志并且上报的机制,这就是所谓的崩溃捕获和上报体系。
今天我们不研究SuperApp中的崩溃上报,主要研究一下崩溃捕获是如何实现的。
iOS系统中如何捕获崩溃
首先,iOS系统中,并没有通用的能够捕获所有崩溃的处理函数。捕获崩溃主要有以下三种方式:
- NSSetUncaughtExceptionHandler
- Unix Signal捕获函数
- Mach(读音为[mʌk])异常捕获函数
关于如何用上述的方式捕获崩溃,不是本次分享的重点,大家可以自行查阅博客中的代码。我们主要需要理解的是这三者各自的原理和应用场景。
NSSetUncaughtExceptionHandler
首先我们写一个会导致崩溃的Objective-C代码片段:
NSDictionary *userinfo = @{
@"username": @"TP-LINK",
@"email": @"admin@tp-link.com.cn",
@"tel": @"15015001500"
};
NSMutableArray<NSDictionary *> *memberarray = [NSMutableArray arrayWithArray:@[userinfo]];
for (NSDictionary *dic in memberarray) {
if ([[dic valueForKey:@"username"] isEqualToString:@"TP-LINK"]) {
[memberarray removeObject:dic];
}
}
运行程序,不出意外的话,程序在执行到片段的时候就会立刻崩溃,然后我们会在控制台里面看到如下打印:
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0xb550c30> was mutated while being enumerated.'
相信不少同学都会这串提示很熟悉,从字面上来看,程序崩溃是因为有个异常没有被捕获到,异常的类型是NSGenericException
,导致异常的原因则是因为在遍历集合的时候尝试去修改里面的元素。
NSGenericException
是一个继承自NSException
的类,表示触发的是一种通用的异常,除了这个类,还有很多其他的子类,像是NSRangException
、NSInvalidArgumentException
等,基本上只要看到名称就知道异常大致是什么原因导致的。
NSException
是可以被我们手动捕获的,例如:
@try {
for (NSDictionary *dic in memberarray) {
if ([[dic valueForKey:@"username"] isEqualToString:@"TP-LINK"]) {
[memberarray removeObject:dic];
}
}
@catch (NSException *exception) {
NSLog(@"Caught %@: %@", [exception name], [exception reason]);
}
但是在写实际项目的时候,我们通常不会手动写这类处理异常的代码,Objective-C也并没有强制要求我们写此类的异常处理程序。
其实,这主要是因为异常代表的通常是我们编写的程序存在逻辑错误,通常不可恢复,需要我们在发布给用户使用之前由开发者进行处理,所以NSException又被称为应用级异常。NSSetUncaughtExceptionHandler实际上是给我们提供了一个手段,对这些我们未捕获的异常进行一个最终的处理,但如果这些错误是在用户使用的时候发生的,我们也无法立刻进行处理。
或许也是因为这个原因,Swift语言抛弃了NSException,而只保留了Error。
由于NSSetUncaughtExceptionHandler不是万能的,比如我们写一段Swift的强制解包代码:
var userName= fetchUserName()
printUserName(userName!)
上述代码假设fetchUserName()函数返回nil,并且printUserName()函数只接受非空参数,那么在程序运行时,由于强制解包失败,应用程序会崩溃并且NSSetUncaughtExceptionHandler也无法捕获此类崩溃,这时候就需要其他的机制来捕获此类异常。
Mach异常
要了解Mach异常,首先要了解什么是Mach!首先上一张mac OS X的架构图:
mac OS X的核心操作系统被称为“Darwin”,其由系统组件和内核构成。其中内核被称为"xnu",他是一个混合型的内核,包括了Mach和BSD两个部分,其中BSD实现了文件系统、网络、NKE(Network Kernel Extension,实现注入通信加密、虚拟网络接口等网络方面的扩展功能)、POSIX接口等功能,而Mach则实现了I/O组件和驱动程序。xnu内核是开源的。
从图里面可以看到,内核的下面就是硬件,所以由Mach内核抛出的异常也被称为是最底层的异常,造成异常的原因通常是硬件导致的异常,比如:
- 试图访问不存在的内存
- 试图访问违反地址空间保护的内存
- 由于非法或未定义的操作代码或操作数而无法执行指令
- 产生算术错误,例如被零除、上溢、或者下溢
- ……
关于Mach抛出异常的流程,我们可以结合以下图来理解:
如果出错的线程触发了一个硬件级别的错误,处于内核的陷阱处理程序就会调用exception_deliver()函数依次尝试将异常投递到thread、task和host。
这里插入一个小话题,在Mach内核中,为了和thread、task和host打交道,或者他们互相之间打交道,提供了一种基于端口的IPC手段,这个手段在Cocoa上层也有对应的抽象,就是NSMachPort。这个mach port大家可能听说过,不知大家是否有印象?
当异常发生的时候,一条包含异常的mach message,例如异常类型、发生异常的线程等等,都会被发送到一个异常端口。而thread、task、host都会维护一组异常端口,当Mach Exception机制传递异常消息的时候,它会按照thread → task → host
的顺序传递异常消息。这是通过上面的mach_exc_raise()类函数来实现的。
如果thread、task都没有处理异常,那么就会由host也就是操作系统内核来处理异常,操作系统处理异常的方式就是上图Exception Handler中的流程,可以看到,handler是一个循环处理消息的机制,mach_msg_receive()函数负责接受消息;mach_exc_server()函数内有catch_mach_exception_raise()函数,这个函数通过ux_exception()将mach异常转换为Unix的Signal,并通过threadsignal()将其发送到对应的线程上去。
这一系列过程中,我们可控的部分是thread,我们可以新建一条thread并且通过mach port监听异常端口来实现崩溃的捕获。
有时候,Debugger会在程序崩溃的时候,给出Mach异常的类型:
上述代码试图给一个assign类型的property赋值,由于引用计数为0,对象在赋值之后就被立刻释放了,所以这行代码就崩溃了
Debugger给出的标红信息,可以这么理解:
一些其他常见的Mach异常类型及其对应的原因如下表:
Exception | Notes |
---|---|
EXC_BAD_ACCESS | 访问了不该访问的内存 |
EXC_BAD_INSTRUCTION | 线程执行非法指令 |
EXC_ARITHMETIC | 算术异常 |
EXC_SOFTWARE | 软件生成的异常 |
EXC_BREAKPOINT | 跟踪或者断点 |
关于code大家可能会存在疑惑,它代表的其实是内核函数的返回值,其中,code=1代表的是地址不可用,其定义如下:
#define KERN_INVALID_ADDRESS 1
由于code的种类有很多,其他code对应的含义,可以翻阅kern_return.h
头文件进行查阅
以下为苹果的崩溃日志,里面也包含类似信息:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00000000000000b8
将两者进行结合,一般就可以判断崩溃的原因究竟是什么。了解以上知识相信会对大家日后解决Bug带来一定的帮助。
Unix Signal
Signal是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。信号的作用有很多,比如可以用来进程间通信(IPC)、用于Debugger调试等,当然也可以用来报告异常。
既然Mach已经实现了硬件导致的异常,为什么还需要将其转化为Unix Signal,继续报告一次呢?
原因很简单,因为xnu包含了BSD和Mach,为了实现POSIX兼容,让用户可以使用BSD提供的POSIX API,就需要做这样一层转换。
Mach异常和Unix Signal两者的转换关系如下表:
Mach 异常 | Unix Signal | 原因 |
---|---|---|
EXC_BAD_INSTRUCTION | SIGILL | 非法指令,比如数组越界,强制解包可选形等等 |
EXC_BAD_ACCESS | SIGSEVG、SIGBUS | SIGSEVG、SIGBUS两者都是错误内存访问,但是两者之间是有区别的:SIGBUS(总线错误)是内存映射有效,但是不允许被访问,比如访问一个结构体但是起始地址有误; SIGSEVG(段地址错误)是内存地址映射都失效,比如野指针 |
EXC_ARIHMETIC | SIGFPE | 运算错误,比如浮点数运算异常 |
EXC_BREAKPOINT | SIGTRAP | trace、breakpoint等等,比如说使用Xcode的断点 |
EXC_SOFTWARE | SIGABRT、SIGPIPE、SIGSYS、SIGKILL | 软件错误,其中SIGABRT最为常见。 |
问1:既然Mach异常可以转换为unix异常,而signal也是可以由我们自由处理的,那是否可以不处理Mach异常,只处理unix的signal就可以了?
答案是不行,因为某些异常,比如EXC_GUARD 异常(这是一种违反了受保护资源的防护而导致的异常,比如访问SQLite文件的时候关闭了它的文件描述符),是没有映射到Unix Signal的,这种异常就没法通过signal处理。
问2:那是不是处理了Mach异常,就不需要处理signal异常了呢?
答案是也不行,因为如果底层有些异常类型只能通过signal处理,比如直接调用了 __pthread_kill
函数直接向某条线程发送了SIGABRT
这个signal,这类异常不能被Mach所捕获
为什么没有通用的异常处理函数
现在我们可以回答这个问题了。总结一下,iOS系统中,崩溃有可能是以下两种方式产生的:
- 应用级异常,比如NSException
- 硬件级异常,比如野指针访问
对于前者,我们只能使用NSSetUncaughtExceptionHandler进行捕获,对于后者,我们需要使用以下机制:
- Mach异常处理机制
- Unix Signal异常处理机制
因为以上两者作用域也无法互相覆盖,所以以上两者也需要结合使用。
正是因为这三种处理机制覆盖了不同的领域,并且处理机制也不尽相同,因此iOS中没有通用的异常处理函数。
然而,事情没有那么简单
上述三个函数的功能十分强大,但是实际上设计一个崩溃捕获系统没有那么容易。一般来说,捕获系统除了捕获崩溃,还需要记录崩溃时的现场信息,比如崩溃时的iOS系统版本、应用版本、崩溃时间、异常信息、程序堆栈等等:
{"app_name":"TP-LINK物联","timestamp":"2023-02-16 15:40:40.00 +0800","app_version":"4.12.1","slice_uuid":"d146125f-f904-3e39-940a-0f7dd32d6071","adam_id":0,"build_version":"41201","platform":2,"bundleID":"net.tplink.surveillancesystem","share_with_app_devs":0,"is_first_party":0,"bug_type":"109","os_version":"iPhone OS 14.0.1 (18A393)","incident_id":"70E8ABFF-6F0F-4094-BF31-EE929EFA78DD","name":"TP-LINK物联"}
Incident Identifier: 70E8ABFF-6F0F-4094-BF31-EE929EFA78DD
CrashReporter Key: 8c905de38d4cd4ff6ad692cc4ca4f6b1f41a50af
Hardware Model: iPhone12,8
Process: TP-LINK物联 [2002]
Path: /private/var/containers/Bundle/Application/F17C1188-8ED4-4C72-8E46-FE7ABE28DDA1/TP-LINK物联.app/TP-LINK物联
Identifier: net.tplink.surveillancesystem
Version: 41201 (4.12.1)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: net.tplink.surveillancesystem [564]
Date/Time: 2023-02-16 15:40:39.7011 +0800
Launch Time: 2023-02-16 15:37:15.6262 +0800
OS Version: iPhone OS 14.0.1 (18A393)
Release Type: User
Baseband Version: 2.00.01
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00000000000000b8
VM Region Info: 0xb8 is not in any region. Bytes before following region: 4375183176
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
UNUSED SPACE AT START
--->
__TEXT 104c80000-1077ac000 [ 43.2M] r-x/r-x SM=COW ...app/TP-LINK物联
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [2002]
Triggered by Thread: 15
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0:
0 libsystem_kernel.dylib 0x00000001d0bbfdd0 0x1d0bbc000 + 15824
1 libsystem_kernel.dylib 0x00000001d0bbf184 0x1d0bbc000 + 12676
2 CoreFoundation 0x00000001a4bb6cf8 0x1a4b19000 + 646392
3 CoreFoundation 0x00000001a4bb0ea8 0x1a4b19000 + 622248
4 CoreFoundation 0x00000001a4bb04bc 0x1a4b19000 + 619708
5 GraphicsServices 0x00000001bb635820 0x1bb632000 + 14368
6 UIKitCore 0x00000001a7554734 0x1a69d7000 + 12048180
7 UIKitCore 0x00000001a7559e10 0x1a69d7000 + 12070416
8 TP-LINK物联 0x0000000104c89ff0 0x104c80000 + 40944
9 libdyld.dylib 0x00000001a4877e60 0x1a4877000 + 3680
……
在iOS系统中,如果直接在上述的崩溃处理函数中进行这些信息的记录,并不安全,这主要是因为iOS中App被限制在一个进程中运行,如果应用崩溃,那崩溃的线程将会立刻暂停执行,那就会导致如下问题:
- 内存可能被破坏(比如某些数值溢出导致的崩溃,内存会被溢出的数据覆盖)
- 锁可能正在被暂停执行的线程持有着
- 数据结构可能只更新一半
这样的不稳定环境,大部分函数都不能保证能够正确运行,导致崩溃处理程序能够调用的库函数非常有限,你将无法做到:
- 通过malloc等函数分配堆内存
- 通过backtrace函数获取调用栈信息
如果破解这些限制?我们不妨研究下SuperApp中集成的Breakpad是怎么操作的。
Breakpad的整体构成
如上图所示,Breakpad主要由三部分构成:
- symbol dumper:符号提取器。应用程序在构建的时候会包含debug相关的信息,它能够提取这些信息并生成专属的符号文件。
- client:客户端是一种包含在你应用程序里面的第三方库,它能够捕获当前各线程的状态以及当前加载的共享库等信息,将其写入minidump文件中。
- processor:处理器主要用来读取minidump文件和符号文件,将其翻译为人类可读的格式
符号文件是程序编译的产物,里面会包含函数或数据的名称、地址、大小、类型等。由于Breakpad是一个跨平台的方案,因此没有采用XCode编译产生的符号表文件,而是使用了自定义的格式。minidump则是一种微软开发的文件格式,它被用在微软的崩溃上传体系中,包含了可执行文件和共享库的列表、进程中的各线程列表信息、调用栈信息等。
Breakpad如何解决上述问题
1.如何安全分配内存
以下为Breakpad启动代码:
ProtectedMemoryAllocator* gMasterAllocator = NULL;
ProtectedMemoryAllocator* gKeyValueAllocator = NULL;
ProtectedMemoryAllocator* gBreakpadAllocator = NULL;
BreakpadRef BreakpadCreate(NSDictionary* parameters) {
try {
gMasterAllocator =
new ProtectedMemoryAllocator(sizeof(ProtectedMemoryAllocator) * 2);
gKeyValueAllocator =
new (gMasterAllocator->Allocate(sizeof(ProtectedMemoryAllocator)))
ProtectedMemoryAllocator(sizeof(LongStringDictionary));
int mutexResult = pthread_mutex_init(&gDictionaryMutex, NULL);
if (mutexResult == 0) {
int breakpad_pool_size = 4096;
gBreakpadAllocator =
new (gMasterAllocator->Allocate(sizeof(ProtectedMemoryAllocator)))
ProtectedMemoryAllocator(breakpad_pool_size);
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
Breakpad* breakpad = Breakpad::Create(parameters);
if (breakpad) {
gMasterAllocator->Protect();
gKeyValueAllocator->Protect();
gBreakpadAllocator->Protect();
[pool release];
return (BreakpadRef)breakpad;
}
[pool release];
}
} catch(...) {
fprintf(stderr, "BreakpadCreate() : error\n");
}
...
}
上述代码片段已经包含了对内存分配问题的解决,其 核心思路是:既然崩溃时无法分配内存,那么只要在崩溃前提前分配好崩溃处理程序所需的内存并将其保护起来避免被崩溃所破坏即可
。
ProtectedMemoryAllocator
这个类相当于一个内存池,它允许分配内存,但是分配内存无法被回收。此外,它还提供了一个Protect()
方法用于将内存池设置为只读,这样一来这块内存就不会在崩溃发生的时候被各种原因覆盖。
通过源码,我们可以一窥其实现的原理,首先是构造函数:
ProtectedMemoryAllocator::ProtectedMemoryAllocator(vm_size_t pool_size)
: pool_size_(pool_size),
next_alloc_offset_(0),
valid_(false) {
kern_return_t result = vm_allocate(mach_task_self(),
&base_address_,
pool_size,
TRUE
);
valid_ = (result == KERN_SUCCESS);
assert(valid_);
}
vm_allocate
是一个内核函数,用于申请虚拟内存,由于Breakpad需要直接申请一块较大的内存,用于整个模块的内存使用,因此它直接使用了该函数,而不是malloc。该类申请的内存大小是由参数pool_size
决定的,内存分配之后,base_address_
指向内存池的起始地址。
再看看Protect()
方法的实现:
kern_return_t ProtectedMemoryAllocator::Protect() {
kern_return_t result = vm_protect(mach_task_self(),
base_address_,
pool_size_,
FALSE,
VM_PROT_READ);
return result;
}
其同样调用了内核函数vm_protect
,将申请的虚拟内存设置为只读,这样就实现了内存的保护。
2.如何获取调用栈信息
Breakpad内部有一个MinidumpGenerator
类专门用于写入minidump,其中包括了我们关心的线程调用栈信息。由于涉及到minidump格式问题,我们不深入分析这个类,只是简单介绍下原理。
首先,我们需要理解线程调用栈的结构:
线程的调用栈分为若干栈帧(stack frame
),每个栈帧对应一个函数调用。上图包含了两个栈帧,DrawLine和DrawSquare。
栈帧主要由三部分组成:函数参数、返回地址、帧内的本地变量。上述DrawSquare函数调用DrawLine函数的时候,首先函数的参数入栈,然后把返回地址入栈,最后是函数内部本地变量。
这里要注意的是,有两个特殊的指针:Stack Pointer指向了调用栈的栈顶,Frame Pointer则指向了当前栈帧。
此外,我们还需要了解一下iOS系统中虚拟内存的相关知识:
我们知道,操作系统会对虚拟内存进行分页。在iOS系统中,为了更好的管理内存页,系统会将一组连续的内存页关联到一个VMObject上,也称为VM Region。我们可以通过XCode的Instruments工具,查看当前App的虚拟内存分配情况,其中就包含了VM Region的相关信息:
可以看到,VM Region被分为不同的Category,其中有一种Category叫做VM Stack,其包含的就是线程调用栈的信息。
为了获取VM Stack中的信息,Breakpad大致做了以下操作:
通过内核函数
task_threads
获取当前进程的所有线程通过内核函数
thread_get_state
获取目标线程的thread_state_t
,这个结构中包含了该线程调用栈的栈顶指针Stack Point_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29]; /* General purpose registers x0-x28 */
__uint64_t __fp; /* Frame pointer x29 */
__uint64_t __lr; /* Link register x30 */
__uint64_t __sp; /* Stack pointer x31 */
__uint64_t __pc; /* Program counter */
__uint32_t __cpsr; /* Current program status register */
__uint32_t __pad; /* Same size for 32-bit or 64-bit clients */
};
由Stack Pointer的地址作为起始地址,获取下一个VM Region,如果其Category为VM Stack,将此块内存的信息记录下来,写入minidump
iOS系统崩溃的捕获的更多相关文章
- iOS系统app崩溃日志手动符号化
iOS系统app崩溃日志手动符号化步骤: 1.在桌面建立一个crash文件夹,将symbolicatecrash工具..crash文件..dSYM文件放到该文件夹中 a.如何查询symbolicate ...
- iOS crash 崩溃问题的追踪方法
http://www.cnblogs.com/easonoutlook/archive/2012/12/27/2835884.html iOS crash 崩溃问题的追踪方法 在调试程序的时候,总是碰 ...
- 有关iOS系统中调用相机设备实现二维码扫描功能的注意点(3/3)
今天我们接着聊聊iOS系统实现二维码扫描的其他注意点. 大家还记得前面我们用到的输出数据的类对象吗?AVCaptureMetadataOutput,就是它!如果我们需要实现目前主流APP扫描二维码的功 ...
- 基于H5的移动端开发,window.location.href在IOS系统无法触发问题
最近负责公司的微信公众号开发项目,基于H5进行开发,某些页面window.location.href在Android机上能正常运行而IOS系统上无法运行,导致无法重定向到指定页面,查了好久终于找到方法 ...
- iOS 系统架构
https://developer.apple.com/library/ios/documentation/Miscellaneous/Conceptual/iPhoneOSTechOverview/ ...
- 超强教程:如何搭建一个 iOS 系统的视频直播 App?
现今,直播市场热火朝天,不少人喜欢在手机端安装各类直播 App,便于随时随地观看直播或者自己当主播.作为开发者来说,搭建一个稳定性强.延迟率低.可用性强的直播平台,需要考虑到部署视频源.搭建聊天室.优 ...
- iOS系统架构
1.iOS系统架构 iOS的系统架构分为四个层次 核心操作系统层 (Core OS) 它包括 内存管理 , 文件系统 , 电源管理以及一些其他的操作系统任务, 它可以直接和硬件设备进行交互 核心服务层 ...
- 在MacOS和iOS系统中使用OpenCV
在MacOS和iOS系统中使用OpenCV 前言 OpenCV 是一个开源的跨平台计算机视觉库,实现了图像处理和计算机视觉方面的很多通用算法. 最近试着在 MacOS 和 iOS 上使用 OpenCV ...
- 深入了解ios系统机制
1.什么叫ios? ios一般指ios(Apple公司的移动操作系统) . 苹果iOS是由苹果公司开发的移动操作系统.苹果公司最早于2007年1月9日的Macworld大会 ...
- iOS系统提供开发环境下命令行编译工具:xcodebuild
iOS系统提供开发环境下命令行编译工具:xcodebuild[3] xcodebuild 在介绍xcodebuild之前,需要先弄清楚一些在XCode环境下的一些概念[4]: Workspace:简单 ...
随机推荐
- centos下配置修改hosts文件以及生效命令详解
linux服务器hosts文件配置 hosts文件是Linux系统中一个负责IP地址与域名快速解析的文件,以ASCII格式保存在"/etc"目录下,文件名为"hosts& ...
- Git 如何删除本地分支和远程分支
查看已有的本地及远程分支:git branch -a 删除远程分支(当前删除的是origin/dev分支):git push origin --delete dev 删除后,再次查看分支情况: ...
- Redis稳定性之战:AOF日志支撑数据持久化
★ Redis24篇集合 1 介绍 AOF(Append Only File)持久化:以独立日志的方式存储了 Redis 服务器的顺序指令序列,并只记录对内存进行修改的指令. 当Redis服务发生雪崩 ...
- 【大语言模型基础】-详解Transformer原理
一.Transformer Transformer最开始用于机器翻译任务,其架构是seq2seq的编码器解码器架构.其核心是自注意力机制: 每个输入都可以看到全局信息,从而缓解RNN的长期依赖问题. ...
- Android Studio自带模拟器无法访问网络问题解决
测试APP的时候,发现Android Studio自带的模拟器访问不了百度等网站,之前一直用的好好的,觉得可能是版本的问题,也有可能是公司网络的问题(因为在家里的电脑的Android Studio的模 ...
- undefined reference to vtable for "xxx::xxx" in QT(已解决)
PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文发布于 2015-02-09 15:37:25 ...
- TP6框架--EasyAdmin学习笔记:数据表添加新参数,如何强制清除缓存
这是我写的学习EasyAdmin的第六章,这一章我给大家分享下如何在数据表中添加新参数,并强制清除缓存 这一章的主题是我在开发中碰到的一个问题,当我在网上疯狂查找解决方法依旧一无所获后,我又尝试了从底 ...
- 记录--你可能忽略的10种JavaScript快乐写法
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前言 代码的简洁.美感.可读性等等也许不影响程序的执行,但是却对人(开发者)的影响非常之大,甚至可以说是影响开发者幸福感的重要因素之一: ...
- 记录--【vue3】写hook三天,治好了我的组件封装强迫症。
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前言 我以前很喜欢封装组件,什么东西不喜欢别人的,总喜欢自己搞搞,这让人很有成就感,虽然是重复造轮子,但是能从无聊的crud业务中暂时解脱 ...
- ssm整合简单配置
最近由于系统重装,之前已经写好了的框架都被我删的一干二净,于是自己动手重新搭了个简单的ssm(spring springmvc mybatis) 运行环境 (java1.8,Tomcat8.5,mav ...