神策可視化埋點(diǎn)實(shí)現(xiàn)細(xì)節(jié)

埋點(diǎn)總體思路

普通界面中的打點(diǎn)抱环,不包過(guò)dialog等通過(guò)windowManager直接add的view油宜。

通過(guò)在Application中監(jiān)聽(tīng)acitivty的生命周期,在resumed方法中,遍歷Activity視圖中所有的view宪睹,給View設(shè)置AccessibilityDelegate,而當(dāng)View 產(chǎn)生了click蚕钦、long_click 等事件的時(shí)候亭病,會(huì)在響應(yīng)原有的Listener方法后發(fā)送消息給AccessibilityDelegate,然后在sendAccessibilityEvent方法下做打點(diǎn)操作嘶居。

客戶端如何搜集app界面數(shù)據(jù)?

源碼:ViewSnapshot

1. 控件樹(shù)的數(shù)據(jù)結(jié)構(gòu):json

rootView開(kāi)始遞歸解析罪帖,每個(gè)view對(duì)應(yīng)一個(gè)json字符串,格式如下:

{
    "hashCode": "view.hashCode()", 
    "id": "view.getId()", 
    "index": "getChildIndex(view.getParent(), view)", 
    "sa_id_name": "getResName(view)", 
    "top": "view.getTop()", // Top position of this view relative to its parent
    "left": "view.getLeft()", // Left position of this view relative to its parent
    "width": "view.getWidth()", // Return the width of the your view
    "height": "view.getHeight()", // Return the height of your view
    "scrollX": "view.getScrollX()", // 返回此視圖的向左滾動(dòng)位置
    "scrollY": "view.getScrollY()",// 返回此視圖的滾動(dòng)頂部位置邮屁。 
    "visibility": "view.getVisibility()", 
    "translationX": "view.getTranslationX()", // The horizontal location of this view relative to its left position. view的偏移量
    "translationY": "view.getTranslationY()", // The vertical location of this view relative to its top position
    "classes": [
        "view.getClass()", 
        "view.getSuperclass()", 
        "view.getSuperSuperclass()", 
        "...直到Object.class的下一級(jí)"
    ], 
    "importantForAccessibility": true,
    "clickable": false,
    "alpha": 1,
    "hidden": 0,
    "background": {
        "classes": [
            "android.graphics.drawable.ColorDrawable",
            "android.graphics.drawable.Drawable"
        ],
        "dimensions": {
            "left": 0,
            "right": 1200,
            "top": 0,
            "bottom": 1920
        },
        "color": -328966
    }
    "layoutRules": [], // RelativeLayout子view屬性 
    "subviews": [
        "child1.hashCode()", 
        "child2.hashCode()", 
        "..."
    ]
}

不同類型的View需要搜集的屬性有所不同整袁,神策采用mixpanel的方案,通過(guò)一個(gè)配置文件來(lái)定義收集哪些對(duì)象的哪些屬性信息:config示例

layoutRules的詳細(xì)解釋

View 的幾個(gè)重要屬性:


android:background
關(guān)聯(lián)方法: getBackground()佑吝、setBackground(ColorDrawable)坐昙、setBackgroundResource(int) 
屬性說(shuō)明: 視圖背景

android:alpha
關(guān)聯(lián)方法: getAlpha()、setAlpha(float) 
屬性說(shuō)明: 視圖透明度芋忿,值在0-1之間炸客。0為完全透明疾棵,1為完全不透明。

android:clickable
關(guān)聯(lián)方法: isClickable()痹仙、setClickable(boolean) 
屬性說(shuō)明: 視圖是否可點(diǎn)擊

android:importantForAccessibility
關(guān)聯(lián)方法: isImportantForAccessibility() 是尔、setImportantForAccessibility(int) 
屬性說(shuō)明: Describes whether or not this view is important for accessibility. If it is important, the view fires accessibility events and is reported to accessibility services that query the screen. Note: While not recommended, an accessibility service may decide to ignore this attribute and operate on all views in the view tree.

android:visibility
關(guān)聯(lián)方法: getVisibility()、setVisibility(int) 
屬性說(shuō)明: "view的可見(jiàn)性蝶溶。有3個(gè)取值: gone——不可見(jiàn)嗜历,同時(shí)不占用view的空間; invisible——不可見(jiàn)抖所,但占用view的空間梨州; visible——可見(jiàn)"

Android 中可以對(duì)點(diǎn)擊事件和文本編輯事件埋點(diǎn):

點(diǎn)擊事件

繼承于 android.view.View 的控件,且 .clickable() 屬性為 true 的控件田轧,點(diǎn)擊后觸發(fā)事件

文本編輯事件

繼承于 android.widget.EditText 的控件暴匠,編輯完成后觸發(fā)事件

用戶在管理界面中選擇控件進(jìn)行埋點(diǎn)時(shí),系統(tǒng)會(huì)自動(dòng)判定需要埋點(diǎn)的事件類型傻粘。

2. 截屏數(shù)據(jù)結(jié)構(gòu):json

一個(gè)liveActivitie對(duì)應(yīng)一個(gè)RootViewInfo實(shí)例:

private static class RootViewInfo {
    public RootViewInfo(String activityName, View rootView) {
        this.activityName = activityName;
        this.rootView = rootView;
        this.screenshot = null;
        this.scale = 1.0f;
    }

    public final String activityName;
    public final View rootView;
    public CachedBitmap screenshot;
    public float scale;
}

對(duì)RootViewInfo實(shí)例進(jìn)行處理后構(gòu)造json數(shù)據(jù):

{
    "activity": "info.activityName", 
    "scale": "info.scale", 
    "serialized_objects": {
        "rootObject": "info.rootView.hashCode()", 
        "objects": [
            "view json 1", 
            "view json 2", 
            "..."
        ]
    }, 
    "image_hash": "info.screenshot.getImageHash", 
    "screenshot": "info.screenshot.writeBitmapJSON"
}

截屏數(shù)據(jù)結(jié)構(gòu)示例

如何標(biāo)識(shí)唯一控件每窖?有些控件監(jiān)測(cè)不到該如何解決?

(1) 如何表示View的path

樹(shù)形結(jié)構(gòu)每一個(gè)view節(jié)點(diǎn)用PathElement表示弦悉,每個(gè)view節(jié)點(diǎn)的絕對(duì)路徑由List< Pathfinder.PathElement >表示

path示例:

"path": [
            {
                "prefix": null,
                "view_class": "com.android.internal.policy.PhoneWindow.DecorView",
                "index": "-1",
                "id": "-1",
                "sa_id_name": null
            },
            {
                "prefix": "shortest",
                "view_class": "com.android.internal.widget.ActionBarOverlayLayout",
                "index": "0",
                "id": "16909220",
                "sa_id_name": null
            },
            {
                "prefix": "shortest",
                "view_class": "android.widget.FrameLayout",
                "index": "0",
                "id": "16908290",
                "sa_id_name": "android: content"
            },
            {
                "prefix": "shortest",
                "view_class": "android.widget.LinearLayout",
                "index": "0",
                "id": "-1",
                "sa_id_name": null
            },
            {
                "prefix": "shortest",
                "view_class": "android.widget.Button",
                "index": "0",
                "id": "2131558506",
                "sa_id_name": "btn"
            }
        ]

(2) 反射R文件得到View的id

ResourceReader 獲取 android.R.id.class 文件以及 mResourcePackageName.R.class 中的內(nèi)部類 id 中的所有static int 變量窒典,類似:

public static final class id {
    ...
    public static final int btnAddAlarm=0x7f0d0055;
    public static final int btnPause=0x7f0d005c;
    public static final int btnReset=0x7f0d005e;
    public static final int btnResume=0x7f0d005d;
    ...
}

將 static int 變量的變量名和值組織成 Map< String, Integer > mIdNameToId 和 SparseArray< String > mIdToIdName ,以供后續(xù)使用稽莉。

(3) index的含義是什么瀑志?

index賦值規(guī)則:每個(gè)ViewGroup下的所有View先按照Class分類,再確認(rèn)是否有Resource Id污秆,如果存在劈猪,則index = 0,否則index = 該Class類型下的子view序號(hào)(從0開(kāi)始編號(hào))良拼。

對(duì)應(yīng)源碼:ViewSnapshot

private int getChildIndex(ViewParent parent, View child) {
    if (parent == null || !(parent instanceof ViewGroup)) {
        return -1;
    }

    ViewGroup _parent = (ViewGroup) parent;

    final String childIdName = getResName(child);

    String childClassName = mClassnameCache.get(child.getClass());
    int index = 0;
    for (int i = 0; i < _parent.getChildCount(); i++) {
        View brother = _parent.getChildAt(i);

        if (!Pathfinder.hasClassName(brother, childClassName)) {
            continue;
        }

        String brotherIdName = getResName(brother);

        if (null != childIdName && !childIdName.equals(brotherIdName)) {
            continue;
        }

        if (brother == child) {
            return index;
        }

        index++;
    }

    return -1;
}

算法思路:(index初始值0)

(1) 將當(dāng)前child與兄弟節(jié)點(diǎn)brother逐一比較战得;

(2) 若brother是child的子類或同類型,則進(jìn)行(3)庸推;否則常侦,回到(1);

(3) 若當(dāng)前child存在childIdName(即id)且與brother的brotherIdName不相同贬媒,則回到(1)刮吧;否則,進(jìn)行(4)掖蛤;

(4) 若當(dāng)前child == brother杀捻,則查找成功,否則,index++致讥,回到(1)

分析上述算法仅仆,可知index取值有如下規(guī)律:

(1) 當(dāng)child存在id時(shí),child與樹(shù)形結(jié)構(gòu)左側(cè)brother的匹配都會(huì)失敗垢袱,執(zhí)行continue墓拜,期間不會(huì)執(zhí)行到index++,直到成功匹配返回index = 0;

(2) 當(dāng)child的id不存在時(shí)请契,遍歷樹(shù)形結(jié)構(gòu)左側(cè)brother咳榜,若brother是child的子類或同類型,則index++爽锥,直到匹配成功涌韩。可以認(rèn)為index為左側(cè)brothers中child的子類或同類型的數(shù)目氯夷。因此臣樱,兄弟節(jié)點(diǎn)的排列順序也會(huì)影響index的取值。

注意:兄弟節(jié)點(diǎn)的排列順序也會(huì)影響index的取值

web配置頁(yè)面返回什么樣的配置信息腮考?

下發(fā)配置樣例

app界面數(shù)據(jù)如何傳輸?shù)絯eb配置頁(yè)面雇毫?

websocket

安卓端使用的WebSocketClient為Java-WebSocket

具體使用時(shí),實(shí)現(xiàn)WebSocketClient即可

/**
 * EditorClient should handle all communication to and from the socket. It should be fairly naive and
 * only know how to delegate messages to the ViewCrawlerHandler class.
 */
 
private class EditorClient extends WebSocketClient {

    public EditorClient(URI uri, int connectTimeout) throws InterruptedException {
        super(uri, new Draft_17(), null, connectTimeout);
    }

    @Override
    public void onOpen(ServerHandshake handshakedata) {
        if (SensorsDataAPI.ENABLE_LOG) {
            Log.d(LOGTAG, "Websocket connected: " + handshakedata.getHttpStatus() + " " + handshakedata
                    .getHttpStatusMessage());
        }

        mService.onWebSocketOpen();
    }

    @Override
    public void onMessage(String message) {
//      Log.d(LOGTAG, "Received message from editor:\n" + message);

        try {
            final JSONObject messageJson = new JSONObject(message);
            final String type = messageJson.getString("type");
            if (type.equals("device_info_request")) {
                mService.sendDeviceInfo(messageJson);
            } else if (type.equals("snapshot_request")) {
                mService.sendSnapshot(messageJson);
            } else if (type.equals("event_binding_request")) {
                mService.bindEvents(messageJson);
            } else if (type.equals("disconnect")) {
                mService.disconnect();
            }
        } catch (final JSONException e) {
            Log.e(LOGTAG, "Bad JSON received:" + message, e);
        }
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {
        if (SensorsDataAPI.ENABLE_LOG) {
            Log.d(LOGTAG, "WebSocket closed. Code: " + code + ", reason: " + reason + "\nURI: " + mURI);
        }
        mService.cleanup();
        mService.onWebSocketClose(code);
    }

    @Override
    public void onError(Exception ex) {
        if (ex != null && ex.getMessage() != null) {
            Log.e(LOGTAG, "Websocket Error: " + ex.getMessage());
        } else {
            Log.e(LOGTAG, "Unknown websocket error occurred");
        }
    }
}

web配置頁(yè)面如何解析app界面數(shù)據(jù)踩蔚?

自己總結(jié)的一個(gè)思路:

深度優(yōu)先遍歷控件樹(shù)棚放,輸出每個(gè)View的絕對(duì)位置和path、clickable馅闽,以及下列屬性:

{
    "prefix": null,
    "view_class": "com.android.internal.policy.PhoneWindow.DecorView",
    "index": "-1",
    "id": "-1",
    "sa_id_name": null
}

點(diǎn)擊事件發(fā)生時(shí)飘蚯,獲取點(diǎn)擊位置坐標(biāo),然后遍歷Activity界面中所有的View(控件也都是View)捞蛋,判斷哪個(gè)View區(qū)域包含點(diǎn)擊位置,從而判斷哪個(gè)View被點(diǎn)擊了柬姚。

為了縮小檢索范圍拟杉,可以只搜索clickable的View。

Application.ActivityLifecycleCallbacks的具體實(shí)現(xiàn)在哪里量承?

源碼:ViewCrawler$LifecycleCallbacks

在ViewCrawler的構(gòu)造函數(shù)中registerActivityLifecycleCallbacks:

public ViewCrawler(Context context, String resourcePackageName) {
        ...
        
        mLifecycleCallbacks = new LifecycleCallbacks();
        final Application app = (Application) context.getApplicationContext();
        app.registerActivityLifecycleCallbacks(mLifecycleCallbacks);

        ...
    }

實(shí)際實(shí)現(xiàn)見(jiàn)LifecycleCallbacks:

private class LifecycleCallbacks
            implements Application.ActivityLifecycleCallbacks {

    public LifecycleCallbacks() {
    }

    void enableConnector() {
        mEnableConnector = true;
        mEmulatorConnector.start();
    }

    void disableConnector() {
        mEnableConnector = false;
        mEmulatorConnector.stop();
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityStarted(Activity activity) {
    }

    @Override
    public void onActivityResumed(Activity activity) {
        if (mEnableConnector) {
            mEmulatorConnector.start();
        }

        mStartedActivities.add(activity);

        if (mStartedActivities.size() == 1) {// app從后臺(tái)恢復(fù)
            SensorsDataAPI.sharedInstance(mContext).appBecomeActive();
        }

        for (String className : mDisabledActivity) {// 在忽略監(jiān)測(cè)的Activities列表中檢索當(dāng)前activity
            if (className.equals(activity.getClass().getCanonicalName())) {
                return;
            }
        }
        mEditState.add(activity);
    }

    @Override
    public void onActivityPaused(Activity activity) {
        mStartedActivities.remove(activity);

        mEditState.remove(activity);
        if (mEditState.isEmpty()) {
            mEmulatorConnector.stop();
        }
    }

    @Override
    public void onActivityStopped(Activity activity) {
        if (mStartedActivities.size() == 0) {
            SensorsDataAPI.sharedInstance(mContext).appEnterBackground();
        }
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    }

    private final EmulatorConnector mEmulatorConnector = new EmulatorConnector();

    private boolean mEnableConnector = false;
}

ViewTreeObserver.OnGlobalLayoutListener的具體實(shí)現(xiàn)在哪里搬设?

源碼:EditState$EditBinding

【如何應(yīng)對(duì)界面動(dòng)態(tài)布局】
為了應(yīng)對(duì)頁(yè)面的動(dòng)態(tài)布局,我們需要在單一線程中實(shí)現(xiàn)事件監(jiān)測(cè)撕捍,通過(guò)循環(huán)操作拿穴,使每個(gè)事件都對(duì)當(dāng)前頁(yè)面的所有view進(jìn)行匹配。經(jīng)過(guò)實(shí)測(cè)忧风,也沒(méi)有發(fā)現(xiàn)對(duì)應(yīng)用交互有可感知的影響默色。

ViewTreeObserver.OnGlobalLayoutListener:當(dāng)在一個(gè)視圖樹(shù)中全局布局發(fā)生改變或者視圖樹(shù)中的某個(gè)視圖的可視狀態(tài)發(fā)生改變時(shí),所要調(diào)用的回調(diào)函數(shù)的接口類

private static class EditBinding implements ViewTreeObserver.OnGlobalLayoutListener, Runnable {
    public EditBinding(View viewRoot, ViewVisitor edit, Handler uiThreadHandler) {
        mEdit = edit;
        mViewRoot = new WeakReference<View>(viewRoot);
        mHandler = uiThreadHandler;
        mAlive = true;
        mDying = false;
    
        final ViewTreeObserver observer = viewRoot.getViewTreeObserver();
        if (observer.isAlive()) {
            observer.addOnGlobalLayoutListener(this);
        }
        run();
    }
    @Override
    public void onGlobalLayout() {
        run();
    }
    @Override
    public void run() {
        if (!mAlive) {
            return;
        }

        final View viewRoot = mViewRoot.get();
        if (null == viewRoot || mDying) {
            cleanUp();
            return;
        }

        // ELSE View is alive and we are alive
        mEdit.visit(viewRoot);

        mHandler.removeCallbacks(this);
        mHandler.postDelayed(this, 5000);
    }
    ...
}

Web配置頁(yè)面的配置是如何得到執(zhí)行的狮腿?即如何自動(dòng)埋點(diǎn)腿宰?

原理:通過(guò)在Application中監(jiān)聽(tīng)acitivty的生命周期呕诉,在resumed方法中,遍歷Activity視圖中所有的view吃度,給View設(shè)置AccessibilityDelegate甩挫,而當(dāng)View 產(chǎn)生了click、long_click 等事件的時(shí)候椿每,會(huì)在響應(yīng)原有的Listener方法后發(fā)送消息給AccessibilityDelegate伊者,然后在sendAccessibilityEvent方法下做打點(diǎn)操作。

ViewCrawler$LifecycleCallbacks.onActivityResumed(activity) -> 
mEditState.add(activity) -> 
EditState.applyEditsOnActivity(activity) -> 
EditState.applyChangesFromList(activity,rootView,List<ViewVisitor> changes)

核心語(yǔ)句final EditBinding binding = new EditBinding(rootView, visitor, mUiThreadHandler)

// Must be called on UI Thread
private void applyChangesFromList(final Activity activity, final View rootView,
                                  final List<ViewVisitor> changes) {
    synchronized (mCurrentEdits) {
        if (!mCurrentEdits.containsKey(activity)) {
            mCurrentEdits.put(activity, new HashSet<EditBinding>());
        }

        final int size = changes.size();
        for (int i = 0; i < size; i++) {
            final ViewVisitor visitor = changes.get(i);
            final EditBinding binding = new EditBinding(rootView, visitor, mUiThreadHandler);
            mCurrentEdits.get(activity).add(binding);
        }
    }
}

值得注意的是 EditBinding 是一個(gè) Runnable 對(duì)象

/* The binding between a bunch of edits and a view. Should be instantiated and live on the UI thread */
private static class EditBinding implements ViewTreeObserver.OnGlobalLayoutListener, Runnable {

    public EditBinding(View viewRoot, ViewVisitor edit, Handler uiThreadHandler) {
        mEdit = edit;
        mViewRoot = new WeakReference<View>(viewRoot);
        mHandler = uiThreadHandler;
        mAlive = true;
        mDying = false;

        final ViewTreeObserver observer = viewRoot.getViewTreeObserver();
        if (observer.isAlive()) {
            observer.addOnGlobalLayoutListener(this);
        }
        run();
    }

    @Override
    public void onGlobalLayout() {
        run();
    }

    @Override
    public void run() {
        if (!mAlive) {
            return;
        }

        final View viewRoot = mViewRoot.get();
        if (null == viewRoot || mDying) {
            cleanUp();
            return;
        }

        // ELSE View is alive and we are alive
        mEdit.visit(viewRoot);

        mHandler.removeCallbacks(this);
        mHandler.postDelayed(this, 5000);
    }
    ...
}

核心語(yǔ)句run()中的mEdit.visit(viewRoot);

public void visit(View rootView) {
    mPathfinder.findTargetsInRoot(rootView, mPath, this);
}

匹配到view后執(zhí)行 ViewVisitor.accumulate(viewfound)

@Override
public void accumulate(View found) {
    
    ...
    
    // We aren't already in the tracking call chain of the view
    final TrackingAccessibilityDelegate newDelegate =
            new TrackingAccessibilityDelegate(realDelegate);
    found.setAccessibilityDelegate(newDelegate);
    mWatching.put(found, newDelegate);
}

設(shè)置 View 的AccessibilityDelegateTrackingAccessibilityDelegate后间护,當(dāng) View 產(chǎn)生了click,long_click 等事件的時(shí)候亦渗,會(huì)在響應(yīng)原有的Listener方法后發(fā)送消息給AccessibilityDelegate,然后在AccessibilityDelegate.sendAccessibilityEvent()方法下做打點(diǎn)操作

/**
* 點(diǎn)擊事件監(jiān)聽(tīng)器
*/
public static class AddAccessibilityEventVisitor extends EventTriggeringVisitor {

    private class TrackingAccessibilityDelegate extends View.AccessibilityDelegate {
    
        public TrackingAccessibilityDelegate(View.AccessibilityDelegate realDelegate) {
            mRealDelegate = realDelegate;
        }
    
        ...

        @Override
        public void sendAccessibilityEvent(View host, int eventType) {
            if (eventType == mEventType) {
                fireEvent(host);// 埋點(diǎn)操作
            }
    
            if (null != mRealDelegate) {
                mRealDelegate.sendAccessibilityEvent(host, eventType);
            }
        }
    
        private View.AccessibilityDelegate mRealDelegate;
    }
    
    ...

    
}

fireEvent實(shí)際調(diào)用了DynamicEventTracker.OnEvent

    public void OnEvent(View v, EventInfo eventInfo, boolean debounce) {
        final long moment = System.currentTimeMillis();

        final JSONObject properties = new JSONObject();
        try {
            properties.put("$from_vtrack", String.valueOf(eventInfo.mTriggerId));
            properties.put("$binding_trigger_id", eventInfo.mTriggerId);
            properties.put("$binding_path", eventInfo.mPath);
            properties.put("$binding_depolyed", eventInfo.mIsDeployed);
        } catch (JSONException e) {
            Log.e(LOGTAG, "Can't format properties from view due to JSON issue", e);
        }

        // 對(duì)于Clicked事件兑牡,事件發(fā)生時(shí)即調(diào)用track記錄事件央碟;對(duì)于Edited事件,由于多次Edit時(shí)會(huì)觸發(fā)多次Edited均函,
        // 所以我們?cè)黾右粋€(gè)計(jì)時(shí)器亿虽,延遲發(fā)送Edited事件
        if (debounce) {
            final Signature eventSignature = new Signature(v, eventInfo);
            final UnsentEvent event = new UnsentEvent(eventInfo, properties, moment);

            // No scheduling mTask without holding a lock on mDebouncedEvents,
            // so that we don't have a rogue thread spinning away when no events
            // are coming in.
            synchronized (mDebouncedEvents) {
                final boolean needsRestart = mDebouncedEvents.isEmpty();
                mDebouncedEvents.put(eventSignature, event);
                if (needsRestart) {
                    mHandler.postDelayed(mTask, DEBOUNCE_TIME_MILLIS);
                }
            }
        } else {
            try {
                SensorsDataAPI.sharedInstance(mContext).track(eventInfo.mEventName, properties);
            } catch (InvalidDataException e) {
                Log.w("Unexpected exception", e);
            }
        }
    }

最終執(zhí)行track,與代碼打點(diǎn)殊途同歸

SensorsDataAPI.sharedInstance(mContext).track(eventInfo.mEventName, properties);

Activity生命周期調(diào)用有版本要求

要求API 14+ (Android 4.0+)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末苞也,一起剝皮案震驚了整個(gè)濱河市洛勉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌如迟,老刑警劉巖收毫,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異殷勘,居然都是意外死亡此再,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)玲销,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)输拇,“玉大人,你說(shuō)我怎么就攤上這事贤斜〔叻停” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵瘩绒,是天一觀的道長(zhǎng)猴抹。 經(jīng)常有香客問(wèn)我,道長(zhǎng)锁荔,這世上最難降的妖魔是什么蟀给? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上坤溃,老公的妹妹穿的比我還像新娘拍霜。我一直安慰自己,他們只是感情好薪介,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布祠饺。 她就那樣靜靜地躺著,像睡著了一般汁政。 火紅的嫁衣襯著肌膚如雪道偷。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天记劈,我揣著相機(jī)與錄音勺鸦,去河邊找鬼。 笑死目木,一個(gè)胖子當(dāng)著我的面吹牛换途,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刽射,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼军拟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了誓禁?” 一聲冷哼從身側(cè)響起懈息,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎摹恰,沒(méi)想到半個(gè)月后辫继,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡俗慈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年姑宽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闺阱。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炮车,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馏颂,到底是詐尸還是另有隱情示血,我是刑警寧澤棋傍,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布救拉,位于F島的核電站,受9級(jí)特大地震影響瘫拣,放射性物質(zhì)發(fā)生泄漏亿絮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望派昧。 院中可真熱鬧黔姜,春花似錦、人聲如沸蒂萎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)五慈。三九已至纳寂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泻拦,已是汗流浹背毙芜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留争拐,地道東北人腋粥。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像架曹,于是被迫代替她去往敵國(guó)和親隘冲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 版權(quán)歸屬于微信公眾號(hào)文章網(wǎng)易HubbleData之Android無(wú)埋點(diǎn)實(shí)踐文末有彩蛋哦? 1 背景 網(wǎng)易Hubbl...
    nailperry閱讀 8,229評(píng)論 3 31
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,504評(píng)論 25 707
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程音瓷,因...
    小菜c閱讀 6,358評(píng)論 0 17
  • 每個(gè)人都要學(xué)會(huì)成長(zhǎng)对嚼,吃飯也是要成長(zhǎng)。而我的成長(zhǎng)是第一次走到這社會(huì)绳慎!這要從我當(dāng)從學(xué)校出來(lái)說(shuō)起: 第一次來(lái)到另一個(gè)陌生...
    金風(fēng)雨露水閱讀 168評(píng)論 0 1
  • 周日天氣漸涼杏愤,早上穿了毛衣出去也沒(méi)覺(jué)得熱靡砌。天氣預(yù)報(bào)說(shuō)這幾天有雨,但看來(lái)也不準(zhǔn)珊楼。中午胡欣燉肉通殃,周末在家喝湯吃肉,太家...
    小王加油啊閱讀 206評(píng)論 0 0