iOS無痕埋點方案分享探究

寫在題前:
文章為本人原創(chuàng), 如果文章轉(zhuǎn)載,必須標明作者與出處,并將原文鏈接以及github地址附在文章首行, 否則將追究其法律責任。 請尊重作者勞動成果! github地址:https://github.com/SandyLoo/iOS-AnalysisProject。 方案暫時還不完善,您的star就是我的動力!

前言

當前互聯(lián)網(wǎng)行業(yè)的競爭已經(jīng)是非常激烈了, “功能驅(qū)動”的時代已經(jīng)過去了, 現(xiàn)在更加注重軟件的細節(jié), 以及用戶的體驗問題。 說到用戶體驗,就不得不提到用戶的操作行為。 在我們的軟件中,我們會到處進行埋點, 以便提取到我們想要的數(shù)據(jù),進而分析用戶的行為習慣。 通過這些數(shù)據(jù),我們也可以更好的分析出用戶的操作趨勢,從而在用戶體驗上把我們的app做的更好。

隨著公司業(yè)務(wù)的發(fā)展,數(shù)據(jù)的重要性日益體現(xiàn)出來。 數(shù)據(jù)埋點的全面性和準確性尤為重要。 只有拿到精準并詳細的數(shù)據(jù), 后面的分析才有意義。 然后隨著業(yè)務(wù)的不斷變化, 埋點的動態(tài)性也越來越重要。為了解決這些問題, 很多公司都提出自己的解決方案, 各中解決方案中,大體分為以下三種:

  1. 代碼埋點
    由開發(fā)人員在觸發(fā)事件的具體方法里,植入多行代碼把需要上傳的參數(shù)上報至服務(wù)端。

  2. 可視化埋點
    根據(jù)標識來識別每一個事件, 針對指定的事件進行取參埋點。而事件的標識與參數(shù)信息都寫在配置表中,通過動態(tài)下發(fā)配置表來實現(xiàn)埋點統(tǒng)計。

  3. 無埋點
    無埋點并不是不需要埋點,更準確的說應(yīng)該是“全埋”, 前端的任意一個事件都被綁定一個標識,所有的事件都別記錄下來。 通過定期上傳記錄文件,配合文件解析,解析出來我們想要的數(shù)據(jù), 并生成可視化報告供專業(yè)人員分析 , 因此實現(xiàn)“無埋點”統(tǒng)計。


由于考慮到“無埋點”的方案成本較高,并且后期解析也比較復(fù)雜,加上view_path的不確定性(具體可以參考: 網(wǎng)易HubbleData無埋點SDK在iOS端的設(shè)計與實現(xiàn))。所以本文重點分享一個 可視化埋點 的簡單實現(xiàn)方式。

可視化埋點

首先,可視化埋點并非完全拋棄了代碼埋點,而是在代碼埋點的上層封裝的一套邏輯來代替手工埋點,大體上架構(gòu)如下圖:


image

不過要實現(xiàn)可視化埋點也有很多問題需要解決,比如事件唯一標識的確定,業(yè)務(wù)參數(shù)的獲取,有邏輯判斷的埋點配置項信息等等。接下來我會重點圍繞唯一標識以及業(yè)務(wù)參數(shù)獲取這兩個問題給出自己的一個解決方案。

唯一標識問題

唯一標識的組成方式主要是又 target + action 來確定, 即任何一個事件都存在一個target與action。 在此引入AOP編程,AOP(Aspect-Oriented-Programming)即面向切面編程的思想,基于 Runtime 的 Method Swizzling能力,來 hook 相應(yīng)的方法,從而在hook方法中進行統(tǒng)一的埋點處理。例如所有的按鈕被點擊時,都會觸發(fā)UIApplication的sendAction方法,我們hook這個方法,即可攔截所有按鈕的點擊事件。

image

這里主要分為兩個部分 :

  • 事件的鎖定
    事件的鎖定主要是靠 “事件唯一標識符”來鎖定,而事件的唯一標識是由我們寫入配置表中的。

  • 埋點數(shù)據(jù)的上報。
    埋點數(shù)據(jù)的數(shù)據(jù)又分為兩種類型: 固定數(shù)據(jù)可變的業(yè)務(wù)數(shù)據(jù), 而固定數(shù)據(jù)我們可以直接寫到配置表中, 通過唯一標識來獲取。而對于業(yè)務(wù)數(shù)據(jù),我是這么理解的: 數(shù)據(jù)是有持有者的, 例如我們Controller的一個屬性值, 又或者數(shù)據(jù)再Model的某一個層級。 這么的話我們就可以通過KVC的的方式來遞歸獲取該屬性的值來取到業(yè)務(wù)數(shù)據(jù), 代碼后面會有介紹。

整體代碼示例

由于iOS中的事件場景是多樣的, 在此我以UIControl, UITablview(collectionView與tableView基本相同), UITapGesture, UIViewController的PV統(tǒng)計 為例,介紹一下具體思路。

  1. UIViewController PV統(tǒng)計
    頁面的統(tǒng)計較為簡單,利用Method Swizzing hook 系統(tǒng)的viewDidLoad, 直接通過頁面名稱即可鎖定頁面的展示代碼如下:
@implementation UIViewController (Analysis)

+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        SEL originalDidLoadSelector = @selector(viewDidLoad);
        SEL swizzingDidLoadSelector = @selector(user_viewDidLoad);
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector];
        
    });
}

-(void)user_viewDidLoad
{
    [self user_viewDidLoad];

   //從配置表中取參數(shù)的過程 1 固定參數(shù)  2 業(yè)務(wù)參數(shù)(此處參數(shù)被target持有)
    NSString * identifier = [NSString stringWithFormat:@"%@", [self class]];
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier];
    if (dic) {
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary * pagePara = dic[@"pagePara"];
        
        __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            
            id value = [CaptureTool captureVarforInstance:self withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
        
        NSLog(@"\n 事件唯一標識為:%@ \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", [self class], pageid, pagename, uploadDic);
    }
}
  1. UIControl 點擊統(tǒng)計。
    主要通過hook sendAction:to:forEvent: 來實現(xiàn), 其唯一標識符我們用 targetname/selector/tag來標記,具體代碼如下:
@implementation UIControl (Analysis)

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


-(void)user_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    [self user_sendAction:action to:target forEvent:event];
    
    NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [target class], NSStringFromSelector(action),self.tag];
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"ACTION"] objectForKey:identifier];
    if (dic) {
        
        NSString * eventid = dic[@"userDefined"][@"eventid"];
        NSString * targetname = dic[@"userDefined"][@"target"];
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary * pagePara = dic[@"pagePara"];
        __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            
            id value = [CaptureTool captureVarforInstance:target withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
        
        
        NSLog(@" \n  唯一標識符為 : %@, \n event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", identifier, eventid, targetname, pageid, pagename, uploadDic);
    }
}
  1. TableView (CollectionView) 的點擊統(tǒng)計。
    tablview的唯一標識, 我們使用 delegate.class/tableview.class/tableview.tag的組合來唯一鎖定。 主要是通過hook setDelegate 方法, 在設(shè)置代理的時候再去交互 didSelect 方法來實現(xiàn), 具體的原理是 具體代碼如下:
@implementation UITableView (Analysis)

+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        SEL originalAppearSelector = @selector(setDelegate:);
        SEL swizzingAppearSelector = @selector(user_setDelegate:);
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];
    });
}



-(void)user_setDelegate:(id<UITableViewDelegate>)delegate
{
    [self user_setDelegate:delegate];
    
    SEL sel = @selector(tableView:didSelectRowAtIndexPath:);

    SEL sel_ =  NSSelectorFromString([NSString stringWithFormat:@"%@/%@/%ld", NSStringFromClass([delegate class]), NSStringFromClass([self class]),self.tag]);
    
    
    //因為 tableView:didSelectRowAtIndexPath:方法是optional的,所以沒有實現(xiàn)的時候直接return
    if (![self isContainSel:sel inClass:[delegate class]]) {
        
        return;
    }
    
    
    BOOL addsuccess = class_addMethod([delegate class],
                                      sel_,
                                      method_getImplementation(class_getInstanceMethod([self class], @selector(user_tableView:didSelectRowAtIndexPath:))),
                                      nil);
    
    //如果添加成功了就直接交換實現(xiàn), 如果沒有添加成功,說明之前已經(jīng)添加過并交換過實現(xiàn)了
    if (addsuccess) {
        Method selMethod = class_getInstanceMethod([delegate class], sel);
        Method sel_Method = class_getInstanceMethod([delegate class], sel_);
        method_exchangeImplementations(selMethod, sel_Method);
    }
}


//判斷頁面是否實現(xiàn)了某個sel
- (BOOL)isContainSel:(SEL)sel inClass:(Class)class {
    unsigned int count;
    
    Method *methodList = class_copyMethodList(class,&count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *tempMethodString = [NSString stringWithUTF8String:sel_getName(method_getName(method))];
        if ([tempMethodString isEqualToString:NSStringFromSelector(sel)]) {
            return YES;
        }
    }
    return NO;
}


// 由于我們交換了方法, 所以在tableview的 didselected 被調(diào)用的時候, 實質(zhì)調(diào)用的是以下方法:
-(void)user_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    
    SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@/%@/%ld", NSStringFromClass([self class]),  NSStringFromClass([tableView class]), tableView.tag]);
    if ([self respondsToSelector:sel]) {
        IMP imp = [self methodForSelector:sel];
        void (*func)(id, SEL,id,id) = (void *)imp;
        func(self, sel,tableView,indexPath);
    }
    
    
    NSString * identifier = [NSString stringWithFormat:@"%@/%@/%ld", [self class],[tableView class], tableView.tag];
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"TABLEVIEW"] objectForKey:identifier];
    if (dic) {
        
        NSString * eventid = dic[@"userDefined"][@"eventid"];
        NSString * targetname = dic[@"userDefined"][@"target"];
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary * pagePara = dic[@"pagePara"];
        
        
        
        UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];
        __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            NSInteger containIn = [obj[@"containIn"] integerValue];
            id instance = containIn == 0 ? self : cell;
            id value = [CaptureTool captureVarforInstance:instance withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
        
        NSLog(@"\n event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic);
    }
    
}

@end

  1. gesture方式添加的的點擊統(tǒng)計。
    gesture的事件,是通過 hook initWithTarget:action:方法來實現(xiàn)的, 事件的唯一標識依然是target.class/actionname來鎖定的, 代碼如下:

@implementation UIGestureRecognizer (Analysis)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        [MethodSwizzingTool swizzingForClass:[self class] originalSel:@selector(initWithTarget:action:) swizzingSel:@selector(vi_initWithTarget:action:)];
    });
}

- (instancetype)vi_initWithTarget:(nullable id)target action:(nullable SEL)action
{
    UIGestureRecognizer *selfGestureRecognizer = [self vi_initWithTarget:target action:action];
    
    if (!target || !action) {
        return selfGestureRecognizer;
    }
    
    if ([target isKindOfClass:[UIScrollView class]]) {
        return selfGestureRecognizer;
    }
    
    Class class = [target class];
    
    
    SEL originalSEL = action;
    
    NSString * sel_name = [NSString stringWithFormat:@"%s/%@", class_getName([target class]),NSStringFromSelector(action)];
    SEL swizzledSEL =  NSSelectorFromString(sel_name);
    
    //給原對象添加一共名字為 “sel_name”的方法,并將方法的實現(xiàn)指向本類中的 responseUser_gesture:方法的實現(xiàn)
    BOOL isAddMethod = class_addMethod(class,
                                       swizzledSEL,
                                       method_getImplementation(class_getInstanceMethod([self class], @selector(responseUser_gesture:))),
                                       nil);
    
    if (isAddMethod) {
        [MethodSwizzingTool swizzingForClass:class originalSel:originalSEL swizzingSel:swizzledSEL];
    }
    
    //將gesture的對應(yīng)的sel存儲到 methodName屬性中,主要是方便 responseUser_gesture: 方法中取出來
    self.methodName = NSStringFromSelector(action);
    return selfGestureRecognizer;
}


-(void)responseUser_gesture:(UIGestureRecognizer *)gesture
{
    
    NSString * identifier = [NSString stringWithFormat:@"%s/%@", class_getName([self class]),gesture.methodName];
    
    //調(diào)用原方法
    SEL sel = NSSelectorFromString(identifier);
    if ([self respondsToSelector:sel]) {
        IMP imp = [self methodForSelector:sel];
        void (*func)(id, SEL,id) = (void *)imp;
        func(self, sel,gesture);
    }
    
    //處理業(yè)務(wù),上報埋點
    NSDictionary * dic = [[[DataContainer dataInstance].data objectForKey:@"GESTURE"] objectForKey:identifier];
    if (dic) {
        
        NSString * eventid = dic[@"userDefined"][@"eventid"];
        NSString * targetname = dic[@"userDefined"][@"target"];
        NSString * pageid = dic[@"userDefined"][@"pageid"];
        NSString * pagename = dic[@"userDefined"][@"pagename"];
        NSDictionary * pagePara = dic[@"pagePara"];
        
        __block NSMutableDictionary * uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];
        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            id value = [CaptureTool captureVarforInstance:self withPara:obj];
            if (value && key) {
                [uploadDic setObject:value forKey:key];
            }
        }];
        
        NSLog(@"\n event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic);
        
    }
}


配置表結(jié)構(gòu)

首先那, 配置表是一個json數(shù)據(jù)。 針對不同的場景 (UIControl , 頁面PV, Tabeview, Gesture)都做了區(qū)分, 用不同的key區(qū)別。 對于 "固定參數(shù)" , 我們之間寫到配置表中,而對于業(yè)務(wù)參數(shù), 我們之間寫清楚參數(shù)在業(yè)務(wù)內(nèi)的名字, 以及上傳時的 keyName, 參數(shù)的持有者。 通過Runtime + KVC來取值。 配置表可以是這個樣子:(僅供參考)

說明: json最外層有四個Key, 分別為 ACTION PAGEPV TABLEVIEW GESTURE, 分別對應(yīng) UIControl的點擊, 頁面PV, tableview cell點擊, Gesture 單擊事件的參數(shù)。 每個key對應(yīng)的value為json格式,Json中的keys, 即為唯一標識符。 標識符下的json有兩個key : userDefine指的 固定數(shù)據(jù), 即直接取值進行上報。 而pagePara為業(yè)務(wù)參數(shù)。 pagePara對應(yīng)的value也是一個json, json的keys, 即上報的keys, value內(nèi)的json包含三個參數(shù): propertyName 為屬性名字, containIn 參數(shù)只有0 ,1 兩種情況, 其實這個參數(shù)主要是為tabview cell的點擊取參做區(qū)別的,因為點擊cell的時候, 上報的參數(shù)可能是被target持有,又或者是被cell本身持有 。 當containIn = 0的時候, 取參數(shù)時就從target中取值,= 1的時候就從cell中取值。 propertyPath 是一般備選項, 因為有時候從instace內(nèi)遞歸取值的時候,可能會出現(xiàn)在不同的層級有相同的屬性名字, 此時 propertyPath就派上用處了。 例如有屬性 self.age 和 self.person.age , 其實如果需要self.person.age, 就把 propertyPath的值設(shè)為 person/age, 接著在取值的時候就會按照指定路徑進行取值。

{
    "ACTION": {
        "ViewController/jumpSecond": {
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "234",
                "pagename": "button點擊,跳轉(zhuǎn)至下一個頁面"
            },
            "pagePara": {
                "testKey9": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        }
    },
    
    "PAGEPV": {
        "ViewController": {
            "userDefined": {
                "pageid": "234",
                "pagename": "XXX 頁面展示了"
            },
            "pagePara": {
                "testKey10": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        }
    },
    "TABLEVIEW": {
        "ViewController/UITableView/0":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "234",
                "pagename": "tableview 被點擊"
            },
            "pagePara": {
                "user_grade": {
                    "propertyName": "grade",
                    "propertyPath":"",
                    "containIn": "1"
                }
            }
        }
    },
    
    "GESTURE": {
        "ViewController/controllerclicked:":{
            "userDefined": {
                "eventid": "201803074|93",
                "target": "",
                "pageid": "123",
                "pagename": "手勢響應(yīng)"
            },
            "pagePara": {
                "testKey1": {
                    "propertyName": "testPara",
                    "propertyPath":"",
                    "containIn": "0"
                }
            }
        }
    }
}

取參方法

@implementation CaptureTool

+(id)captureVarforInstance:(id)instance varName:(NSString *)varName
{
    id value = [instance valueForKey:varName];

    unsigned int count;
    objc_property_t *properties = class_copyPropertyList([instance class], &count);
    
    if (!value) {
        NSMutableArray * varNameArray = [NSMutableArray arrayWithCapacity:0];
        for (int i = 0; i < count; i++) {
            objc_property_t property = properties[i];
            NSString* propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)];
            NSArray* splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@"\""];
            if (splitPropertyAttributes.count < 2) {
                continue;
            }
            NSString * className = [splitPropertyAttributes objectAtIndex:1];
            Class cls = NSClassFromString(className);
            NSBundle *bundle2 = [NSBundle bundleForClass:cls];
            if (bundle2 == [NSBundle mainBundle]) {
//                NSLog(@"自定義的類----- %@", className);
                const char * name = property_getName(property);
                NSString * varname = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding];
                [varNameArray addObject:varname];
            } else {
//                NSLog(@"系統(tǒng)的類");
            }
        }
        
        for (NSString * name in varNameArray) {
            id newValue = [instance valueForKey:name];
            if (newValue) {
                value = [newValue valueForKey:varName];
                if (value) {
                    return value;
                }else{
                    value = [[self class] captureVarforInstance:newValue varName:varName];
                }
            }
        }
    }
    return value;
}


+(id)captureVarforInstance:(id)instance withPara:(NSDictionary *)para
{
    NSString * properyName = para[@"propertyName"];
    NSString * propertyPath = para[@"propertyPath"];
    if (propertyPath.length > 0) {
        NSArray * keysArray = [propertyPath componentsSeparatedByString:@"/"];
     
        return [[self class] captureVarforInstance:instance withKeys:keysArray];
    }
    return [[self class] captureVarforInstance:instance varName:properyName];
}

+(id)captureVarforInstance:(id)instance withKeys:(NSArray *)keyArray
{
    id result = [instance valueForKey:keyArray[0]];
    
    if (keyArray.count > 1 && result) {
        int i = 1;
        while (i < keyArray.count && result) {
            result = [result valueForKey:keyArray[i]];
            i++;
        }
    }
    return result;
}
@end


結(jié)尾

以上是自己的一些想法與實踐, 感覺目前的無痕埋點方案都還是不是很成熟, 不同的公司會有不同的方案, 但是可能大部分還是用的代碼埋點的方式。 代碼埋點的侵入性,維護性成本比較大, 尤其是當埋點特別多的時候, 有時候自己幾個月前寫的埋點代碼,突然需要改,自己都要找半天才能找到。 并且代碼埋點很致命的一個問題是無法動態(tài)更新, 即每次修改埋點,必須重新上線, 有時候上線后產(chǎn)品經(jīng)理突然跑過來問:為什么埋點數(shù)據(jù)不太正常那, 此時你突然發(fā)現(xiàn)有一句埋點代碼寫錯了, 這個時候你要么承認錯誤,承諾下次加上。要么趕快緊急上線解決。 通過以上方式,可以實現(xiàn)埋點的動態(tài)追加。 配置表可以通過服務(wù)端下載, 每次下載后就存在本地, 如果配置表有更新,只需要重新更新配置表就可以解決 。 方案中可能很多細節(jié)還需要完善,例如selector方法中存在業(yè)務(wù)邏輯判斷,即一個標識符無法唯一的鎖定一個埋點。 這種情況目前用配置表解決的成本較大, 并且業(yè)務(wù)是靈活的不好控制。 所以以上方案也只是涵蓋了大部分場景, 并非所有場景都適用,具體大家可以根據(jù)業(yè)務(wù)情況來決定使用范圍。

最后, 大家如果有什么建議,歡迎簡信給我。 我們一起來探討完善這個一個方案。

項目鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 最近跟同事花了點時間來思考可視化埋點,并沒有什么突破性的進展,不過市面上很多關(guān)于可視化埋點的技術(shù)文章都在講達...
    daixunry閱讀 8,239評論 1 38
  • 國家電網(wǎng)公司企業(yè)標準(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 12,298評論 6 13
  • 今天在微博上看到模特在紐約拍的寫真,這里就順便寫一寫這座城。我去過紐約不止一次,她給我的印象讓我很說不清楚。 都說...
    heartGeraldine閱讀 457評論 0 0
  • 各位看官,大家好??! 本寶寶在這個花季年齡開始關(guān)注護膚這一人生大事了,其實吧~恩,就是因為被初中生叫了聲阿姨。我當...
    冬天的六月閱讀 395評論 0 1
  • 凌晨過三十多分,身側(cè)丫頭的呼呼聲,窗外微微弱的光暈,四方的屋及未及七年的婚姻之恙,還有倒數(shù)二十四小時已不及,將滿三...
    丫頭的三小姐閱讀 197評論 0 0

友情鏈接更多精彩內(nèi)容