Runtime在项目中的使用场景

使用vue-cli(vue脚手架)快速搭建项目 1、《亿级流量Java高并发与网络编程实战》2、《分布式一致性算法开发实战 媒体宣传文案》3、《Java多线程与大数据处理实战》4、《深入浅出TCPIP和VPN》 目录一、什么是VueCLI1、如果你只是简单写几个Vue的Demo程序,那么你不需要VueCLI脚手架。2、如果你在开发大型项目,那么你需要,并且必然需要使用VueCLI。3、CLI是什么意思?4、脚手架长什么样?5、脚手架依赖于node.js和webpack二、安装Vue CLI脚手架三、runtime-compiler和runtime-onl 阅读详情

    由于最近申请的辞职,所以不得不做好下一家面试的准备。 在iOS面试过程中Runtime, Runloop基本是必问的两个问题。 Runtime的概念性问题就不多说了, 本篇文章我会讲述一下Runtime在iOS开发中的实际使用场景。

    首先,归纳下Runtime的几个使用场景。

  1. 做用户埋点统计
  2. 处理异常崩溃(NSDictionary, NSMutableDictionary,  NSArray, NSMutableArray 的处理)
  3. 按钮最小点击区设置
  4. 按钮重复点击设置
  5. 手势的重复点击处理
  6. UIButton点击事件带多参数
  7. MJRefresh封装
  8. 服务端控制页面跳转
  9. 字典转模型

一 用户埋点

        在做app运营的时候, 我们经常会需要接入一些第三方做统计, 例如友盟统计,google统计等。 例如外面需要统计某个页面用户停留的时长, 统计某个页面的展示次数。 通常我们的做法是 : 需要统计A页面停留时长的时候,我们再A页面出现(appear)的时候记录一个时间戳,页面消失(dispear)的时候用当前时间戳与之前的时间戳求出时间间隔,然后上报到分析平台。 如果统计页面展示次数, 就在每次页面出现时调用统计方法。  这样做的坏处是 代码侵入性太强,维护性与易读性都不太好。 假设以后要改需求, 就要进入到代码所在处进行修改。 又或者别人接手你的代码, 根本不知道已经做了哪些埋点, 需求改来改去,时间久了, 项目中全都是垃圾代码。

        此时,为了优化统计, 我们使用 Hook (钩子)的思想, 例如Runtime的 Method sweezing(方法交换)去拦截系统方法来实现共计。

        首先,我们写一个集成NSObject的工具类,实现方法交换

#import "HookTool.h"
#import <objc/runtime.h>



@implementation HookTool

+(void)swizzingForClass:(Class)cls originalSel:(SEL)originalSelector swizzingSel:(SEL)swizzingSelector
{
    Class class = cls;
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method  swizzingMethod = class_getInstanceMethod(class, swizzingSelector);
    
    BOOL addMethod = class_addMethod(class,
                                     originalSelector,
                                     method_getImplementation(swizzingMethod),
                                     method_getTypeEncoding(swizzingMethod));
    
    if (addMethod) {
        class_replaceMethod(class,
                            swizzingSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    }else{
        
        method_exchangeImplementations(originalMethod, swizzingMethod);
    }
}
@end

       接着,我们写一个UIViewController的分类, 在Load方法中把系统方法替换掉:

#import "UIViewController+actionAnalysis.h"
#import "HookTool.h"
#import "NSDate+Convenience.h"


@implementation UIViewController (actionAnalysis)

+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalAppearSelector = @selector(viewWillAppear:);
        SEL swizzingAppearSelector = @selector(user_viewWillAppear:);
        [HookTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
        
        SEL originalDisappearSelector = @selector(viewWillDisappear:);
        SEL swizzingDisappearSelector = @selector(user_viewWillDisappear:);
        [HookTool swizzingForClass:[self class] originalSel:originalDisappearSelector swizzingSel:swizzingDisappearSelector];
    });
}



-(void)user_viewWillAppear:(BOOL)animated
{
    //页面出现

    [self user_viewWillAppear:animated];
}


-(void)user_viewWillDisappear:(BOOL)animated
{
    //页面消失
    
    [self user_viewWillDisappear:animated];
}

@end

          此时还有个问题, 首先你可能并不想对每个页面进行统计, 但是又不想每次添加一个统计就加一个if判断。 这个时候我们就在Xcode中加入一张plist表, plist表里面记录我们所需统计的信息

      

此时,我们只需要在hook的方法中去实现统计逻辑

-(void)user_viewWillAppear:(BOOL)animated
{
    NSDictionary * pageenter = [[HookTool getConfig] objectForKey:@"page_enter_anysis"];
    if ([pageenter.allKeys containsObject:NSStringFromClass([self class])]) {
        NSLog(@"%@ 页面展示", NSStringFromClass([self class]));
    }
    
    NSDictionary * pagetime = [[HookTool getConfig] objectForKey:@"page_time_anysis"];
    if ([pagetime.allKeys containsObject:NSStringFromClass([self class])]) {
        //此处用Userdefault存储只是因为方便书写, 实际用可以用一个单例去存储中间值
        [[NSUserDefaults standardUserDefaults] setDouble:[[NSDate date] timeIntervalSince1970] * 1000 forKey:@"appeartime"];
    }

    [self user_viewWillAppear:animated];
}


-(void)user_viewWillDisappear:(BOOL)animated
{
    //页面停留时间统计
    NSDictionary * pagetime = [[HookTool getConfig] objectForKey:@"page_time_anysis"];
    if ([pagetime.allKeys containsObject:NSStringFromClass([self class])]) {
        double leaveTime = NSDate.currenMillisecondTimestamp - [[NSUserDefaults standardUserDefaults] doubleForKey:@"appeartime"];
        NSLog(@"%@ 页面的停留时间为 %lf ms", [self class], leaveTime);
    }
    
    [self user_viewWillDisappear:animated];
}
        这样的话,以后做页面时长或者页面展示的统计,就只需要维护这个plist表就行了,不需要具体改动代码。 

        点击事件统计:

        与VC的统计类似, 也是利用catagory + hook的思想来实现,  我们可以添加一个UIControl的分类。但是具体需要hook UIControl的哪个方法那 ? 点击进入UIControl的api, 我们很容易发现需要Hook的方法

- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;

         接着我们在UIControl的分类中实现方法的交互

@implementation UIControl (actionAnalysis)

+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzingSelector = @selector(user_sendAction:to:forEvent:);
        [HookTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
    });
}


-(void)user_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"\n***hook success.\n[1]action:%@\n[2]target:%@ \n[3]event:%ld", NSStringFromSelector(action), target, (long)event);
    [self user_sendAction:action to:target forEvent:event];
}

      同样的, 我们只需要在plist中添加click的统计所需的参数就可以了

利用Runtime做用户埋点的就说这么多, 文章只提供思路, 具体plist的结构,或者代码细节根据情况自己做实现就行了。另外, 由于需求变动的原因,造成代码与配置表不匹配(例如可能会出现某个method名字被改变   )从而造成埋点统计失败,   建议写一个单元测试对Plist进行测试,思路: 在单元测试中我们首先读取plist配置文件,遍历所有的页面。在一个页面内遍历所有的ControlEventIDs,对每个响应函数名进行respondsToSelector:判断。 这样可以有效减少埋点失效问题。


二   处理异常崩溃(NSDictionary, NSMutableDictionary,  NSArray, NSMutableArray 的处理)

        

          在开发过程中, 有时候会出现set object for key的时候 object为Nil或者Key为Nil, 又或者初始化array, dic的时候由于数据个数与指定的长度不一致造成崩溃。 此时利用runtime对异常情况进行捕捉,提前return或者抛弃多余的长度。


Dic:

#import "NSDictionary+Safe.h"
#import <objc/runtime.h>

@implementation NSDictionary (Safe)

+ (void)load {
    Method originalMethod = class_getClassMethod(self, @selector(dictionaryWithObjects:forKeys:count:));
    Method swizzledMethod = class_getClassMethod(self, @selector(na_dictionaryWithObjects:forKeys:count:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

+ (instancetype)na_dictionaryWithObjects:(const id [])objects forKeys:(const id <NSCopying> [])keys count:(NSUInteger)cnt {
    id nObjects[cnt];
    id nKeys[cnt];
    int i=0, j=0;
    for (; i<cnt && j<cnt; i++) {
        if (objects[i] && keys[i]) {
            nObjects[j] = objects[i];
            nKeys[j] = keys[i];
            j++;
        }
    }
    
    return [self na_dictionaryWithObjects:nObjects forKeys:nKeys count:j];
}

@end

@implementation NSMutableDictionary (Safe)

+ (void)load {
    Class dictCls = NSClassFromString(@"__NSDictionaryM");
    Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
    Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    if (!anObject || !aKey)
        return;
    [self na_setObject:anObject forKey:aKey];
}

@end


array:

#import "NSArray+Safe.h"
#import <objc/runtime.h>

@implementation NSArray (Safe)

+ (void)load {
    Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
    Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
    id nObjects[cnt];
    int i=0, j=0;
    for (; i<cnt && j<cnt; i++) {
        if (objects[i]) {
            nObjects[j] = objects[i];
            j++;
        }
    }
    
    return [self na_arrayWithObjects:nObjects count:j];
}
@end

@implementation NSMutableArray (Safe)

+ (void)load {
    Class arrayCls = NSClassFromString(@"__NSArrayM");
    
    Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
    Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
    method_exchangeImplementations(originalMethod1, swizzledMethod1);
    
    Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
    Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
    method_exchangeImplementations(originalMethod2, swizzledMethod2);
}

- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_insertObject:anObject atIndex:index];
}

- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
    if (!anObject)
        return;
    [self na_setObject:anObject atIndex:index];
}

@end


三  按钮最小点击区设置

        “按钮太不好点中了,点击好几次才点击到”,  测试经常会有这样的抱怨, 但是此时按钮图片本身设计就很小。  此时,例如Runtime进行点击区放大, 是个挺好的解决版本(当然也要注意不需要扩大的场景: 例如去年开发一个类似猫眼电影的app, 用户选座位的View里面是一个个小的控件,此时点击区域就不能放大,不然会误点,或者点击区遮盖。)

static const void *topNameKey = @"topNameKey";
static const void *rightNameKey = @"rightNameKey";
static const void *bottomNameKey = @"bottomNameKey";
static const void *leftNameKey = @"leftNameKey";


- (void)setEnlargeEdgeWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left{
    
    objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (CGRect)enlargedRect
{
    NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);
    NSNumber *rightEdge = objc_getAssociatedObject(self, &rightNameKey);
    NSNumber *bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
    NSNumber *leftEdge = objc_getAssociatedObject(self, &leftNameKey);
    if (topEdge && rightEdge && bottomEdge && leftEdge) {
        return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
                          self.bounds.origin.y - topEdge.floatValue,
                          self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    }
    else
    {
        return self.bounds;
    }
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds)) {
        return [super hitTest:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? self : nil;
}


四  按钮的重复点击

      这个就不多说了,详细大部分程序员都遇到过, 直接上代码

+ (void)load{
    Method originalMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
    Method swizzledMethod = class_getInstanceMethod([self class], @selector(User_SendAction:to:forEvent:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

#pragma mark -- 时间间隔 --
static const void *ButtonDurationTime = @"ButtonDurationTime";
- (NSTimeInterval)durationTime{
    NSNumber *number = objc_getAssociatedObject(self, &ButtonDurationTime);
    return number.doubleValue;
}
- (void)setDurationTime:(NSTimeInterval)durationTime{
    NSNumber *number = [NSNumber numberWithDouble:durationTime];
    objc_setAssociatedObject(self, &ButtonDurationTime, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
   
}

- (void)User_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
  
    self.userInteractionEnabled = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.durationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.userInteractionEnabled = YES;
    });
    
    [self User_SendAction:action to:target forEvent:event];
}

五  手势的重复点击处理

      手势重复点击有个误区: 不能通过拦截 addTarget:(id)target action:(SEL)action 这个方法来实现,因为这个方法是是添加方法,即使我们交换了,在执行的时候并没有什么变化的。正确的做法是添加一个timeInterval,然后在代理里面根据timeInterval设置UITapGestureRecognizer的enable属性

#import "UITapGestureRecognizer+LOOExtension.h"
#import <objc/runtime.h>

@interface UITapGestureRecognizer ()
///时间间隔
@property (nonatomic,assign) NSTimeInterval duration;

@end

static const void *UITapGestureRecognizerduration = @"GestureRecognizerduration";

@implementation UITapGestureRecognizer (LOOExtension)

#pragma mark - Getter Setter

- (NSTimeInterval)duration{
    NSNumber *number = objc_getAssociatedObject(self, &UITapGestureRecognizerduration);
    return number.doubleValue;
}

- (void)setDuration:(NSTimeInterval)duration{
    NSNumber *number = [NSNumber numberWithDouble:duration];
    objc_setAssociatedObject(self, &UITapGestureRecognizerduration, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
}



/**
 添加点击事件
 
 @param target taeget
 @param action action
 @param duration 时间间隔
 */
- (instancetype)initWithTarget:(id)target action:(SEL)action withDuration:(NSTimeInterval)duration{
    
    self = [super init];
    if (self) {
        self.duration = duration;
        self.delegate = self;
        [self addTarget:target action:action];
    }
    return self;
    
}


- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    self.enabled = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.enabled = YES;
    });
    
    return YES;
}

@end


六  UIButton点击带多参数

UIButton *btn = // create the button  
objc_setAssociatedObject(btn, "firstObject", someObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);   //实际上就是KVC  
objc_setAssociatedObject(btn, "secondObject", otherObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  
  
[btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];  
  
- (void)click:(UIButton *)sender  
{  
    id first = objc_getAssociatedObject(btn, "firstObject");        //取参  
    id second = objc_setAssociatedObject(btn, "secondObject");  
    // etc.  
}  

这么使用runtime感觉有点鸡肋,至少在自己的iOS生涯中,没有必须需要这么做的时候。 其实写个子类,添加个Parameter属性岂不是更简单。

七  MJRefresh的封装

大部分程序员应该都用过MJRefresh这个工具,大部分用法都每次出现tabview初始化后, 都初始化出来一个 mj_header, mj_footer, 并且设置 header与footer后, 把mj_header与mj_footer复制给tableview.mj_header, tableview.mj_footer.  每次去重复创建Header, Footer, 这个是不能容忍的。  我们知道tableview和collectionView都是继承自scrollView,那么我们可以在 scrollView的分类里面添加一些方法,那么我们在以后使用的时候,就不需要一遍一遍的重复写无用代码了,只需要调用scrollView分类方法就可以了。

#import "UIScrollView+JHRefresh.h"
#import <MJRefresh.h>
@implementation UIScrollView (JHRefresh)
/**
 添加刷新事件
 
 @param headerBlock 头部刷新
 @param footerBlock 底部刷新
 */
- (void)setRefreshWithHeaderBlock:(void(^)(void))headerBlock
                      footerBlock:(void(^)(void))footerBlock{
    if (headerBlock) {
        
        MJRefreshNormalHeader *header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
            if (headerBlock) {
                headerBlock();
            }
        }];
        header.stateLabel.font = [UIFont systemFontOfSize:13];
        header.lastUpdatedTimeLabel.font = [UIFont systemFontOfSize:13];
       
        self.mj_header = header;
    }
    
    if (footerBlock) {
        MJRefreshBackNormalFooter *footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
            footerBlock();
        }];
        footer.stateLabel.font = [UIFont systemFontOfSize:13];
        [footer setTitle:@"暂无更多数据" forState:MJRefreshStateNoMoreData];
        [footer setTitle:@"" forState:MJRefreshStateIdle];
        self.mj_footer.ignoredScrollViewContentInsetBottom = 44;
        self.mj_footer = footer;
    }
}



/**
 开启头部刷新
 */
- (void)headerBeginRefreshing{
    [self.mj_header beginRefreshing];
}


/**
 没有更多数据
 */
- (void)footerNoMoreData{
    [self.mj_footer setState:MJRefreshStateNoMoreData];
}

/**
 结束刷新
 */
- (void)endRefresh{
    
    if (self.mj_header) {
        [self.mj_header endRefreshing];
    }
    if (self.mj_footer) {
        [self.mj_footer endRefreshing];
    }
}


八  服务端控制页面跳转

      项目开发中,我们可能会有这样的需求: 根据服务端推送过来的数据规则,跳转到对应的控制器。 之前我们的做法是这样的: 前端与服务端定义好规则, 例如服务端推送  Push/Live/WatchLive/12, Push: push方式跳转 , Live指的直播模块, WatchLive指的看直播的功能, 12指的房间号, 也就是跳转到12号主播间。  但是这么做坏处就是,必须提前与服务端约定好协议, 每次运营如果加一个新的跳转, 移动端需要改代码,重新上线。扩展性很低。

    其实利用Runtime完全可以写成通用的方式来实现跳转。例如外面与服务端定义好推送规则后,服务端推送过来的数据如下:

// 这个规则肯定事先跟服务端沟通好,跳转对应的界面需要对应的参数
NSDictionary *userInfo = @{
                           @"class": @"LiveViewController",     //VC的名字
                           @"property": @{
                                        @"ID": @"123",          //参数名字为 ID , value为 123
                                        @"type": @"12"          //type为附加信息, 根据实际情况定义
                                   }
                           };

接着我们利用Runtime进行跳转

// 类名
    NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];
    const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
    
    // 从一个字串返回一个类
    Class newClass = objc_getClass(className);
    if (!newClass)
    {
        return;   //推送的class不存在
    }
    // 创建对象
    id instance = [[newClass alloc] init];
    
    // 对该对象赋值属性
    NSDictionary * propertys = params[@"property"];
    [propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        // 检测这个对象是否存在该属性
        if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
            // 利用kvc赋值
            [instance setValue:obj forKey:key];
        }
    }];
    
    // 获取导航控制器
    UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
    UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
    // 跳转到对应的控制器
    [pushClassStance pushViewController:instance animated:YES];

检测属性是否存在

- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
{
    unsigned int outCount, i;
    
    // 获取对象里的属性列表
    objc_property_t * properties = class_copyPropertyList([instance
                                                           class], &outCount);
    
    for (i = 0; i < outCount; i++) {
        objc_property_t property =properties[i];
        //  属性名转成字符串
        NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        // 判断该属性是否存在
        if ([propertyName isEqualToString:verifyPropertyName]) {
            free(properties);
            return YES;
        }
    }
    free(properties);
    
    return NO;
}

九  字典转模型

 获取属性的列表的方法是字典转模型的比较核心的方法。常见的字典转模型的三方有 MJExtension,  YYModel, JsonModel等, 翻看其源码, 都会发现  Ivar *class_copyIvarList(Class cls, unsigned int *outCount)的使用

MJExtension核心代码摘录
YYModel核心代码摘录


JsonModel json字典转model 摘录



        基本上主流的json 转model 都少不了,使用运行时动态获取属性的属性名的方法,来进行字典转模型替换,字典转模型效率最高的(耗时最短的)的是KVC,其他的字典转模型是在KVC 的key 和Value 做处理,动态的获取json 中的key 和value ,当然转换的过程中,第三方框架需要做一些判空啊,镶嵌的逻辑处理, 再进行KVC 转模型.这句代码 [xx  setValue:value forKey:key];无论JsonModle,YYKIt,MJextension 都少不了[xx  setValue:value forKey:key];这句代码的,不信可以去搜,这是字典转模型的核心方法,


参考文章:

RunTime使用案例

iOS Runtime基础学习

Runtime 10种用法

iOS 万能跳转界面方法

iOS动态性(二)可复用而且高度解耦的用户统计埋点实现


Runtime的实际使用场景 iOS 作者China-meng,源码Runtime-demo,介绍: Runtime的实际使用场景   1.动态给分类添加属性   2.方法的交换swizzling   3.字典转模型  4.获取所有的私有属性和方法   5.对私有属性修改   6.归档解档  7.动态添加方法 立即下载

相关推荐

3dm游戏插件库

3dm游戏插件可以解决游戏插件丢失的问题

方法:删除Provisioning Profiles(描述文件)

问题有些时候, Mac 上的关于证书,描述文件等之类的东西经过长期积累, 无效的过期很多, 都不能用了, 一直在你的 Mac上存在着. 有时候更是在你更新过Provisioning Profiles文件后, 竟然无法运行. 然后你就可以考虑要去删除这些陈旧的东西了.

DCSnail-蜗牛 1万+

Imatest-Star模块(西门子星图)

星图分析指南:基于ISO12233标准的MTF测试方法,通过放射状射线和正弦灰度变化评估相机SFR性能。

weixin_64706696的博客 1571

XCode5中清理老的provisioning profiles

xcode5 如果切换开发者账号,老的provisioning 还会在生成ipa时选择provisioning看到(很讨厌吧),需要手动删除 xcode5 provisioning profile path: ~/Library/MobileDevice/Provisioning Profiles 打开并日期排序,删除老的 provisioning profile 文件即可

火星的你 750

FLUENT UDF编译及蒸发冷凝相关问题

蒸发冷凝与相间传质

weixin_48029137的博客 1万+

SARScape实战:集成GACOS数据优化InSAR大气校正全流程

本文详细介绍了如何在SARScape软件中集成GACOS数据,以优化InSAR大气校正的全流程。通过实战步骤,从GACOS数据申请、下载、导入,到在单景干涉图和SBAS-InSAR流程中应用,有效剥离大气相位延迟,显著提升形变监测结果的精度与可靠性,实现从“雾里看花”到“拨云见日”的转变。

weixin_28419039的博客 401

如何清理旧的Provisioning Profiles文件

参考:点击打开链接iOS开发时经常遇到描述文件里不包含签名。Provisioning profile "iOS Team Provisioning Profile:xxx" doesn't include signing certificate1. 资源库----&gt;MobileDevice----&gt;Provisioning Profiles,将里面的齿轮文件全部删除,然后到Xcode下...

u010596262的博客 2422

机器学习(ML)在材料领域应用

机器学习作为一种兼顾开发效率以及开发成本的方法,已经逐渐应用于材料发现、结构分析、性质预测、反向设计等多个领域,并且在材料学研究中展现出惊人的潜力。传统的发现新材料的方法,如经验试错法和基于密度泛函理论(DFT)的方法,往往需要较长的研发周期,成本高、效率低,已经不能很好的适应如今需求量激增的材料学领域 ...

W5632DE的博客 3718

usmt可以删除吗_win10 usmt.ppkg可以删除么

工具/原料硬件:电脑操作系统:Windows10方法/步骤1.在Windows10系统中,右键菜单里有一个【使用Windows Defender扫描】的项目。2.Win10删除/恢复使用Windows Defender扫描的方法3.步骤:4.在打开的注册表编辑器窗口,依次展开:HKEY_CLASSES_ROOT*shellexContextMenuHandlersEPP,右键点击EPP,在右键菜单...

weixin_39820588的博客 9031

SAP-ABAP:SAP ABAP 深度解析:BAPI 从入门到精通 —— 原理、实战与自定义开发全指南

SAP ABAP BAPI 深度解析摘要 本文全面介绍了 SAP ABAP 中的 BAPI(业务应用程序编程接口)技术,涵盖以下核心内容: BAPI 定义与特点:BAPI 是 SAP 为业务对象提供的标准化编程接口,具有面向对象、封装业务逻辑、标准化错误处理和版本兼容性等特点。 BAPI 与 RFC 关系:所有 BAPI 都是 RFC,但需额外满足业务对象注册、标准化参数和错误处理等要求。 BAPI 核心作用:包括业务逻辑封装、系统集成、标准化错误处理、版本兼容和审计合规。 实战演示:通过创建销售订单的完整

baidu_35680696的博客 1034

【系统分析师】2025年下半年真题:案例分析-答案及详解(回忆版)

Redis相比Memcached在电商高并发场景下具有显著优势:支持持久化(RDB/AOF)、丰富数据结构(如有序集合实现排行榜)、原生支持集群和哨兵模式(高可用性)。布隆过滤器通过位数组和哈希函数预先存储所有合法订单ID,当查询请求到达时快速拦截无效ID(如伪造ID),避免其穿透缓存直接冲击数据库,从而预防缓存击穿问题。

数据知道的博客 5988

删除Xcode里面的Provisioning Profiles文件

删除路径:~/Library/MobileDevice/Provisioning Profiles

u011405160的专栏 786

基于若依的ruoyi-nbcio-plus支持VForm3表单字段数据保存到数据库的一种方法——全网首创(一)

基于若依的ruoyi-nbcio-plus支持VForm3表单字段数据保存到数据库的一种方法——全网首创(一)

宁波阿成的博客 660

Yocto项目 - VIRTUAL-RUNTIME,它有什么用?

VIRTUAL-RUNTIME 是 Yocto 中一个重要的虚拟包标识,用于定义和选择运行时需要的虚拟包。它通常会在 RDEPENDS 或镜像定义中使用,用于进一步拆分构建时的高级方案和完整性。

Interview_TC的博客 1199

基于Simulink的转向控制系统仿真建模示例

本文完成了基于 Simulink 的 EPS 转向控制系统仿真,实现了:✅掌握 EPS 助力控制基本原理✅构建车速敏感的可变助力策略✅在 Simulink 中验证低速轻便、高速沉稳的手感✅达成“安全、舒适、高效”的现代转向系统。

xiaoheshang_123的博客 87

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

vueCli中Runtime + Compiler和Runtime-only的选择问题

首先,分别选择Runtime + Compiler和Runtime-only后发现它们的区别只在main.js这个文件中 (左边为Runtime + Compiler的mian.js文件,右边为Runtime-only的mian.js文件) 我们发现它们的区别只在vue实例的声明方式上 在此之前,我们还得先知道一个知识,那就是vue程序的运行过程 首先vue的template(即组件)会解析成ast(抽象语法树)再编译为render函数再变成virtual-dom(虚拟语法树)最后被解析成为了UI(真实do

qq_45963949的博客 327

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

使用vue-cli创建项目注意事项,以及Runtime Only和Runtime+Compiler区别

在我们使用vue-cli创建项目的时候,会提示你安装的版本 可以看到有两种版本: Runtime Only 版本 和 Runtime+Compiler 版本。 1.Runtime Only 我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。 在将 .vue 文件编译成 JavaS.

qq_36742720的博客 915

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

探究iOS底层原理|Runtime的相关应用

Runtime 相关应用解析

多分享一点点,共同进步一点点 399

ArcGIS Runtime Toolkit for Android 常见问题解决方案

ArcGIS Runtime Toolkit for Android 常见问题解决方案 一、项目基础介绍 ArcGIS Runtime Toolkit for Android 是一个开源项目,由 Esri 公司维护。该项目提供了与 ArcGIS Runtime SDK for Android 配合使用的视图组件,这些组件可以帮助开发者更快地构建地图和场景应用。主要编程语言为 Java 和 Kotl...

gitblog_00870的博客 843

Galacean Runtime 项目常见问题解决方案

Galacean Runtime 是一个基于 TypeScript 的高性能实时交互式引擎,支持 2D 和 3D 图形渲染、动画、物理效果等。它旨在为开发者提供易于使用且轻量级的设计,通过组件系统来构建项目。该项目主要用于网页和移动端,支持 HTML5 和支付宝小程序。主要编程语言为 TypeScript。 ## 2. 新手常见问题及解决步骤 ### 问题一:如何安装 Galacean Run...

gitblog_00619的博客 1122

mysql scope runtime_maven scope runtime

https://blog.csdn.net/ningbohezhijunbl/article/details/25818069There are 6 scopes available:compileThis is the default scope, used if none is specified. Compile dependencies are available in all class...

weixin_28742653的博客 111

onnxruntime-web-demo:展示 ONNX Runtime Web 强大能力的互动演示

onnxruntime-web-demo:展示 ONNX Runtime Web 强大能力的互动演示 项目介绍 onnxruntime-web-demo 是一个互动演示门户,通过 VueJS 展示 ONNX Runtime Web 的实际应用案例。目前,该演示支持四种不同的示例,帮助用户快速体验 ONNX Runtime Web 的强大功能。用户可以通过访问 ONNX Runtime Web 演示...

gitblog_00405的博客 1387

Java Runtime Compiler 使用教程

Java Runtime Compiler(JRC)是一个开源项目,旨在提供一个在运行时动态编译和加载Java代码的工具。该项目由OpenHFT团队开发,支持在运行时编译Java代码并将其加载到当前的ClassLoader中。JRC特别适用于需要在运行时生成和执行代码的场景,例如动态代理、代码热更新等。 ## 2、项目快速启动 ### 2.1 环境准备 确保你已经安装了Java开发环境(JD

gitblog_00464的博客 636

Unity Runtime Inspector 安装与使用指南

Unity Runtime Inspector 安装与使用指南 项目概述 Unity Runtime Inspector 是一款专为Unity 3D设计的运行时检查器和层次结构工具,支持在游戏运行期间进行调试和实时编辑。它兼容Unity支持的各种平台,包括移动设备,并且提供了高度优化的界面来减少垃圾回收(GC)产生的影响。 项目目录结构及介绍 主要目录简介 Assets: 这个目录包含了项目的...

gitblog_00093的博客 729

ONNX Runtime Ruby 教程

ONNX Runtime Ruby 教程 项目介绍 ONNX Runtime Ruby 是一个高效执行机器学习模型的Ruby绑定库,它使Ruby开发者能够轻松地在他们的应用程序中集成并运行ONNX格式的模型。ONNX Runtime由微软开发,并支持多种深度学习框架的模型转换,如TensorFlow和PyTorch。此Ruby版本由Andrew Kane维护,旨在提供无缝的体验,让Ruby社区能...

gitblog_00553的博客 991

【亲测免费】 微软ONNX Runtime与Unreal Engine 5的实时风格迁移项目

微软ONNX Runtime与Unreal Engine 5的实时风格迁移项目 项目基础介绍与编程语言 本项目,由微软维护,名为OnnxRuntime-UnrealEngine,是一个在Unreal Engine 5中利用ONNX Runtime实现神经风格迁移的示例。主要编程语言包括C++和少量的C#,旨在展示如何将机器学习模型集成进游戏项目,特别是在实时渲染环境中。通过结合先进的游戏引擎和强大...

gitblog_00320的博客 819
上一篇: Xcode登录apple id 提示 This action could not be completed
下一篇: iOS 中的HTTP(面试前可以记忆一下)
SandyLoo
博客等级 码龄11年 14粉丝 33原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值