iOS ------ Method Swizzling (动态方法交换)

news2024/12/26 11:04:30

一,Method Swizzling 简介

Method(方法)对应的是objc_method结构体;而objc_method结构体中包含了SEL method_name(方法名),IMP method_imp(方法实现)

// objc_method 结构体
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法类型
    IMP _Nonnull method_imp;                     // 方法实现
};

Method(方法),SEL(方法名),IMP(方法实现)三者的关系:

在运行中,class(类)维护了一个method list(方法列表)来确定消息的正确发送。OC中调用方法叫做发送消息,发送消息前会查找消息,查找过程就是通过SEL查找IMP的过程。method list (方法列表)存放的元素就是Method(方法)。而Method(方法)中映射了一对键值对:SEL(方法名)IMP(方法实现)

原理:
Method swizzling修改了method list(方法列表),使不同Method(方法)中的键值对发生了交换。比如交换前两个键值对分别为SEL A:IMP A,SEL B:IMP B,交换之后就变为了SEL A : IMP B、SEL B : IMP A。
在这里插入图片描述

二,MethodSwizzling简单代码实现

在当前类的+(void)load方法中增加Method Swizzling操作,交换(void)originalFunction(void)swizzledFunction的方法实现。

#import "ViewController.h"
#import <objc/runtime.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self SwizzlingMethod];
    [self originalFunction];
    [self swizzledFunction];
    
    
}
- (void)SwizzlingMethod {
    //当前类
    Class class = [self class];
    
    //方法名
    SEL originalSeletor = @selector(originalFunction);
    SEL swizzledSeletor = @selector(swizzledFunction);
    
    //方法结构体
    Method originalMethod = class_getInstanceMethod(class, originalSeletor);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSeletor);
    
    //调用交换两个方法的实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
//原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}
//替换方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}
@end

在这里插入图片描述

上面的代码简单的将两个方法进行了交换。但在实际应用中并不是那么简单,更多的是为当前类添加一个分类,然后在分类中进行MethodSwimming操作,并且要考虑的东西要更多,且更复杂。

三,MethodSwizzling的使用方案

一般是在该类的分类中添加MethodSwizzling交换方法

@implementation UIViewController (Swizzling)

// 交换 原方法 和 替换方法 的方法实现
+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 当前类
        Class class = [self class];
        
        // 原方法名 和 替换方法名
        SEL originalSelector = @selector(originalFunction);
        SEL swizzledSelector = @selector(swizzledFunction);
        
        // 原方法结构体 和 替换方法结构体
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        /* 如果当前类没有 原方法的 IMP,说明在从父类继承过来的方法实现,
         * 需要在当前类中添加一个 originalSelector 方法,
         * 但是用 替换方法 swizzledMethod 去实现它 
         */
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // 原方法的 IMP 添加成功后,修改 替换方法的 IMP 为 原始方法的 IMP
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            // 添加失败(说明已包含原方法的 IMP),调用交换两个方法的实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替换方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end

一些用到的方法

通过SEL获取方法Method

// 获取实例方法
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

// 获取类方法
OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);

IMP的getter/setter方法

// 获取一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m); 

// 设置一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)

替换方法

// 获取方法实现的编码类型
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m);

// 添加方法实现
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types);
                
// 替换方法的 IMP,如:A替换B(B指向A,A还是指向A)
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types);
                    
// 交换两个方法的 IMP,如:A交换B(B指向A,A指向B)
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

四,注意事项

1.保证方法交换只执行一次

为了保证方法交换的代码可以优先交换,一般会将其写在+load方法中,但是+load的方法也能被主动调用,如果多次调用就会被还原,如果调用[super load] 方法也会造成这样的结果;所以我们要保证方法只交换一次,可选择在单例模式下。

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self lz_methodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)];
    });
}

2.用子类方法替换父类方法

在子类中用子类的方法subFuntionA替换父类的方法function A。子类实例和父类实例分别调用function A,最终都实现的是subFuntionA。

如果我们在子类的方法subFuntionA1替换了父类中的方法functionA后想要继续调用functionA,同理应该这样写

- (void)subFunctionA {
    [self subFunctionA];
    NSLog(@"%s", __func__);
}

再用子类实例和父类实例分别调用function A。

父类调用时就会报错,子类调用就不会。

在上面的函数中调用subFuntionA,但父类本身方法列表中没subFuntionA,所以父类也就报了unrecognized selector 的错误。

出现上面找不到方法的原因是:子类用自己的实现直接替换了父类的方法。

如果我们能不能为子类添加一个和父类一样的方法,子类中进行替换就不会影响父类了。

+ (void)swizzingClassB:(Class)cls oldSEL:(SEL)oldSel toNewSel:(SEL)newSel {
    if (!cls) { return; }
    Method oldM = class_getInstanceMethod(cls, oldSel);
    Method newM = class_getInstanceMethod(cls, newSel);
    
    // 先尝试给 cls 添加方法(SEL: oldSel  IMP: newM),防止子类直接替换父类中的方法
    BOOL addSuccess = class_addMethod(cls, oldSel, method_getImplementation(newM), method_getTypeEncoding(oldM));
    
    if (addSuccess) { // 添加成功即:原本没有 oldSel,成功为子类添加了一个 oldSel - newM 的方法
        // 这里将原 newSel的imp替换为 oldM 的 IMP
        class_replaceMethod(cls, newSel, method_getImplementation(oldM), method_getTypeEncoding(oldM));
    }
    else {
        method_exchangeImplementations(oldM, newM);
    }
}
  • 使用class_addMethod为当前类添加functionA方法,关联subFuntionA方法的imp
  • 返回值为NO,说明子类已经实现了subFuntionA,则直接进行方法交换,不会影响父类
  • 返回值为YES,说明子类未实现了subFuntionA,添加成功后,使用class_replaceMethod将sub functionA替换为functionA的imp

再用子类实例和父类实例分别调用function A。
这时父类实例调用functionA没有受到子类方法交换的影响,实现的就是functionA。
而子类实例就会在实现subfunctionA中实现function A。

五,MethodSwizzling应用场景

1,为UITableView的异常加载占位图

对于UITableView的异常加载情况分为无数据或网络异常。
对于检测tableView是否为空,借助tableView的代理dataSource即可。核心代码是,依次获取table View所具有的组数和行数,通过isEmpty这个flag标示最后确定是否添加占位图。

- (void)checkEmpty {
    BOOL isEmpty = YES;//flag标示

    id  dataSource = self.dataSource;
    NSInteger sections = 1;//默认一组
    if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
        sections = [dataSource numberOfSectionsInTableView:self];//获取当前TableView组数
    }

    for (NSInteger i = 0; i < sections; i++) {
        NSInteger rows = [dataSource tableView:self numberOfRowsInSection:sections];//获取当前TableView各组行数
        if (rows) {
            isEmpty = NO;//若行数存在,不为空
        }
    }
    if (isEmpty) {//若为空,加载占位图
        if (!self.placeholderView) {//若未自定义,展示默认占位图
            [self makeDefaultPlaceholderView];
        }
        self.placeholderView.hidden = NO;
        [self addSubview:self.placeholderView];
    } else {//不为空,隐藏占位图
        self.placeholderView.hidden = YES;
    }
}

接下来实现如何添加占位图
如果可以让tableView在执行reloadData时自动检查其行数就可以了。也就是我们在原有的reload Data方法的基础上添加checkEmpty此方法。这里我们可以通过MethodSwizzling替换reload Data方法,给予它新的实现。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //方法交换,将reloadData实现交换为sure_reloadData
        [self methodSwizzlingWithOriginalSelector:@selector(reloadData) bySwizzledSelector:@selector(sure_reloadData)];
    });
}

- (void)sure_reloadData {
    [self checkEmpty];
    [self sure_reloadData];
}

这样就可以在实现reloadData的同时检查行数从而判断我是否加载占位图的功能。

具体实现demo
tableView的异常加载占位图

2,处理UIButton的重复点击

避免一个按钮被快速点击多次。同样利用Method Swizzling

  • 为 UIControl 或 UIButton 建立一个 Category。
  • 在分类中添加一个 NSTimeInterval xxx_acceptEventInterval; 的属性,设定重复点击间隔
  • 在分类中实现一个自定义的 xxx_sendAction:to:forEvent: 方法,在其中添加限定时间相应的方法。
  • 利用 Method Swizzling 将 sendAction:to:forEvent: 方法和 xxx_sendAction:to:forEvent: 进行方法交换。
#import "UIButton+TBCustom.h"
#import <objc/runtime.h>

@interface UIButton()

@property (nonatomic, assign) NSTimeInterval custom_acceptEventInterval; // 可以用这个给重复点击加间隔

@end

@implementation UIButton (TBCustom)

+ (void)load{
    Method systemMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    SEL sysSEL = @selector(sendAction:to:forEvent:);
    
    Method customMethod = class_getInstanceMethod(self, @selector(custom_sendAction:to:forEvent:));
    SEL customSEL = @selector(custom_sendAction:to:forEvent:);
    
    //添加方法 语法:BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 若添加成功则返回No
    // cls:被添加方法的类  name:被添加方法方法名  imp:被添加方法的实现函数  types:被添加方法的实现函数的返回值类型和参数类型的字符串
    BOOL didAddMethod = class_addMethod(self, sysSEL, method_getImplementation(customMethod), method_getTypeEncoding(customMethod));
    
    //如果系统中该方法已经存在了,则替换系统的方法  语法:IMP class_replaceMethod(Class cls, SEL name, IMP imp,const char *types)
    if (didAddMethod) {
        class_replaceMethod(self, customSEL, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
    }else{
        method_exchangeImplementations(systemMethod, customMethod);
        
    }
}

- (NSTimeInterval )custom_acceptEventInterval{
    return [objc_getAssociatedObject(self, "UIControl_acceptEventInterval") doubleValue];
}

- (void)setCustom_acceptEventInterval:(NSTimeInterval)custom_acceptEventInterval{
    objc_setAssociatedObject(self, "UIControl_acceptEventInterval", @(custom_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSTimeInterval )custom_acceptEventTime{
    return [objc_getAssociatedObject(self, "UIControl_acceptEventTime") doubleValue];
}

- (void)setCustom_acceptEventTime:(NSTimeInterval)custom_acceptEventTime{
    objc_setAssociatedObject(self, "UIControl_acceptEventTime", @(custom_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)custom_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    
    // 如果想要设置统一的间隔时间,可以在此处加上以下几句
    // 值得提醒一下:如果这里设置了统一的时间间隔,只会影响UIButton, 如果想统一设置,也想影响UISwitch,建议将UIButton分类,改成UIControl分类,实现方法是一样的
     if (self.custom_acceptEventInterval <= 0) {
         // 如果没有自定义时间间隔,则默认为.4秒
        self.custom_acceptEventInterval = .4;
     }
    
    // 是否小于设定的时间间隔
    BOOL needSendAction = (NSDate.date.timeIntervalSince1970 - self.custom_acceptEventTime >= self.custom_acceptEventInterval);
    
    // 更新上一次点击时间戳
    if (self.custom_acceptEventInterval > 0) {
        self.custom_acceptEventTime = NSDate.date.timeIntervalSince1970;
    }
    
    // 两次点击的时间间隔小于设定的时间间隔时,才执行响应事件
    if (needSendAction) {
        [self custom_sendAction:action to:target forEvent:event];
    }
}

addTarget:action:forControlEvents: 方法将 buttonTapped: 方法与按钮的点击事件关联起来。当用户点击按钮时,按钮会调用 sendAction:to:forEvent: 方法,并将 buttonTapped: 方法作为动作发送给指定的目标对象(在这里是 self,即当前对象)

3,处理数组越界的问题

Method Swizzling 可以用于解决数组越界导致的崩溃问题。通过交换 NSArray 或 NSMutableArray 的方法实现,我们可以在访问数组元素之前进行边界检查,以防止越界访问。

#import <objc/runtime.h>

@implementation NSArray (SafeAccess)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(objectAtIndex:);
        SEL swizzledSelector = @selector(safe_objectAtIndex:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (id)safe_objectAtIndex:(NSUInteger)index {
    if (index < self.count) {
        return [self safe_objectAtIndex:index];
    } else {
        NSLog(@"Array index out of bounds: %lu", (unsigned long)index);
        return nil;
    }
}

@end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1634980.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【GESP】2023年09月图形化一级 -- 小鸡躲球

小鸡躲球 1. 准备工作 (1)删除默认小猫角色。 (2)添加角色Chick和Ball。 (3)删除默认白色背景,添加背景Blue Sky。 2. 功能实现 (1)点击绿旗,小鸡角色站在地面上,初始位置为(X=-140,Y=-120),初始方向为90,旋转方式为任意旋转。在小鸡角色中设置舞台的初始…

办公楼智慧公厕自发电门锁感应器无需电池供电,环保节能!

在当今科技高速发展的时代&#xff0c;办公楼智慧公厕的自发电门锁感应器成以其独特的优势&#xff0c;为办公楼的卫生设施管理带来了全新的变革。这款感应器无需电池供电&#xff0c;真正做到了环保节能&#xff0c;同时在免电池、免维护、信号稳定、门锁升级、把手设计、应用…

编译工具各版本与操作系统版本号兼容性冷知识 : JetBrains IntelliJ IDEA 各个主要版本及其对应的操作系统版本号的兼容情况

编译工具各版本与操作系统版本号兼容性冷知识 &#x1f9e0;: JetBrains IntelliJ IDEA 各个主要版本及其对应的操作系统版本号的兼容情况 文章目录 编译工具各版本与操作系统版本号兼容性冷知识 &#x1f9e0;: JetBrains IntelliJ IDEA 各个主要版本及其对应的操作系统版本号…

深入OceanBase分布式数据库:MySQL 模式下的 SQL 基本操作

码到三十五 &#xff1a; 个人主页 OceanBase与MySQL模式下兼容性序 在当今的大数据时代&#xff0c;数据库技术的选择对于企业的信息化发展至关重要。OceanBase作为一种高性能、高可用的分布式关系数据库&#xff0c;在与MySQL模式的兼容性方面展现出了显著的优势&#xff0c…

qt5-入门-2D绘图-基础

参考&#xff1a; QPainter_w3cschool https://www.w3cschool.cn/learnroadqt/k7zd1j4l.html C GUI Programming with Qt 4, Second Edition 本地环境&#xff1a; win10专业版&#xff0c;64位&#xff0c;Qt 5.12 代码已经测试通过。其他例子日后更新。 目录 基础知识penb…

微信小程序个人中心、我的界面(示例四)

微信小程序个人中心、我的界面&#xff0c;九宫格简单布局&#xff08;示例四&#xff09; 微信小程序个人中心、我的界面&#xff0c;超简洁的九宫格界面布局&#xff0c;代码粘贴即用。更多微信小程序界面示例&#xff0c;请进入我的主页哦&#xff01; 1、js代码 Page({…

gateway全局token过滤器

添加gateway依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency>创建一个tokenFilter 实现全局过滤器GlobalFilter,并且实现fitler方法 Value("${…

PDF高效编辑器,支持修改PDF文档并转换格式从PDF文件转换成图片文件,轻松管理你的文档世界!

PDF文件已成为我们工作、学习和生活中不可或缺的一部分。然而&#xff0c;传统的PDF阅读器往往只能满足简单的查看需求&#xff0c;对于需要频繁编辑、修改或转换格式的用户来说&#xff0c;就显得力不从心。现在&#xff0c;我们为您带来一款全新的PDF高效编辑器&#xff0c;让…

Vue 组件单元测试深度探索:细致解析与实战范例大全

Vue.js作为一款广受欢迎的前端框架&#xff0c;以其声明式的数据绑定、组件化开发和灵活的生态系统赢得了广大开发者的心。然而&#xff0c;随着项目规模的增长&#xff0c;确保组件的稳定性和可靠性变得愈发关键。单元测试作为软件质量的守护神&#xff0c;为Vue组件的开发过程…

开源AI智能名片商城小程序:深度解读IMC(IP、MarTech、Content)视角

在数字化浪潮中&#xff0c;私域流量的运营已成为企业不可或缺的增长引擎。而开源AI智能名片商城小程序&#xff0c;则是以一种全新的视角——IMC&#xff08;IP、MarTech、Content&#xff09;&#xff0c;为企业打开私域流量运营的新篇章。今天&#xff0c;我们就来一起深入解…

mac安装软件遇到无法验证开发者的解决方法

现象 无法打开“”&#xff0c;因为无法验证开发者。这种情况说明正在安装的软件里面有的包被系统判定为不安全的。 解决方法 1、点击取消按钮 2、系统设置-隐私与安全性&#xff0c;点击打开按钮

python可视化图表

1.测试数据准备 2011年1月销售数据.txt 2011-01-01,4b34218c-9f37-4e66-b33e-327ecd5fb897,1689,湖南省 2011-01-01,5b6a6417-9a16-4243-9704-255719074bff,2353,河北省 2011-01-01,ae240260-68a9-4e59-b4c9-206be4c08a8d,2565,湖北省 2011-01-02,c833e851-880f-4e05-9de5-b5…

第6篇:创建Nios II工程之控制LED<一>

Q&#xff1a;还记得第1篇吗&#xff1f;设计简单的逻辑电路&#xff0c;控制DE2-115开发板上LED的亮与熄灭&#xff0c;一行Verilog HDL的assign赋值语句即可实现。本期开始创建Nios II工程&#xff0c;用C语言代码控制DE2-115开发板上的LED实现流水灯效果。 A&#xff1a;在…

力扣141.环形链表142.环形链表Ⅱ 附证明

题目链接&#xff1a; 141. 环形链表 - 力扣&#xff08;LeetCode&#xff09; 142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 141.环形链表 方法思路&#xff1a;快慢指针 代码: class Solution { public:bool hasCycle(ListNode *head) {if(!head){return fa…

基于uniapp vue3.0 uView 做一个点单页面(包括加入购物车动画和左右联动)

1、实现效果&#xff1a; 下拉有自定义组件&#xff08;商品卡片、进步器、侧边栏等&#xff09;源码 2、左右联动功能 使用scroll-view来做右边的菜单页&#xff0c;title的id动态绑定充当锚点 <scroll-view :scroll-into-view"toView" scroll-with-animation…

vue+element之解决upload组件上传文件失败后仍显示在列表上、自动上传、过滤、findIndex、splice、filter

MENU 前言错误案例(没有用)正确方法结束语 前言 el-upload上传失败后&#xff0c;文件仍显示在列表上。 这个pdf文件上传失败&#xff0c;仍显示在列表&#xff0c;给人错觉是上传成功&#xff0c;所以要把它去掉。 在element中&#xff0c;file-list和v-model:file-list是用于…

Codigger数据篇(中):数据可控性的灵活配置

在数据服务领域中&#xff0c;数据可控性无疑是至关重要的一环。数据可控性不仅关乎数据的安全性和隐私性&#xff0c;更直接影响到数据价值的实现。Codigger&#xff0c;在其数据可控性方面的灵活配置&#xff0c;为用户提供了更加便捷、高效的数据管理体验。 一、自主选择数…

拌合楼管理系统(十八)如何从一个winForm中的事件修改另外一个Form的控件的值

前言&#xff1a; 上篇讲述了如何手工调用海康的车牌识别摄像头进行拍照和识别车牌&#xff0c;我车牌识别的程序在应用的一个窗体&#xff0c;需要去更新另外一个窗体里面的lable中的内容为识别的车牌信息&#xff0c;同时还要写入到另外窗体的datagridview中。 一、实现效果 …

以23年电赛吉林赛区为例,如何避免冤假错案?再谈无人机赛题中不让使用机载计算机的问题!!!

有同学反馈吉林赛区在无人机赛题中使用树莓派后不被允许测评&#xff0c;由于缺少同省样本支撑&#xff0c;并且吉林省G题无人机获得国奖仅有一组&#xff0c;所以仅根据特定情况做回复&#xff0c;不代表任何组委会立场&#xff0c;因为按照该用户表述&#xff0c;自己没有任何…

【HTTP协议】了解http需要学习哪些内容

HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是超文本传输协议&#xff0c;互联网上应用最广泛的一种协议&#xff0c;它负责在客户端和服务器之间传输数据。本文将从HTTP协议的基本原理、请求-响应模型、常见特性以及应用场景等方面进行总结。 1. HTTP基本原理 …