[转载]让我们来聊一聊Android插件化吧

现如今插件化的思想和应用在Android上越来越多了,各式各样的方案也是层出不穷,这篇文章旨在告诉大家插件化的核心思想是什么,又有什么样的实现方式。

前言

首先,这篇文章的题目为什么不沿用我之前xxxx!xxxxx这样的风格呢,因为我觉得这样风格太中二了。。

其次,我写这篇文章的原因是因为前些时候看到有大神写了一篇文章Android 插件化的 过去 现在 未来,里面的内容很不错,特别是有一些关于原理的东西,让我回想起当时看几个插件化框架的源码的时候产生的心得和体会,这里也是写出来给大家做一个分享吧。

插件化介绍

在开始真正讲解插件化之前,让我先告诉那些不了解插件化是什么的同学[什么是插件化]。

所谓插件化,就是让我们的应用不必再像原来一样把所有的内容都放在一个apk中,可以把一些功能和逻辑单独抽出来放在插件apk中,然后主apk做到[按需调用],这样的好处是一来可以减少主apk的体积,让应用更轻便,二来可以做到热插拔,更加动态化。

在后文中,我首先会对插件化的实现原理进行一个分析,接着我挑了其中两个比较有代表性的,[Small]和[DroidPlugin]来分析他们的实现有什么区别。

原理分析

在分析原理之前,让我们先想想做插件化会遇到的挑战。大家可以想一想,如果我要做一个插件apk,里面会包含什么?首先想到的肯定是activity和对应的资源,我们要做的事就是在主apk中调用插件apk中的activity,并且加载对应的资源。当然这只是其中的一个挑战,这里我不会带大家分析所有的原理,因为这样一天一夜都讲不完,所以我选取了其中最具代表性的一点:[主apk如何加载插件apk中的activity]。

首先,我们回想一下平时我们是怎么唤起一个activity的:

Intent intent = new Intent(ActivityA.this,ActivityB.class);
startActivity(intent);

我们调用的是activity的startActivity方法,而我们都知道我们的activity是继承自ContextThemeWrapper的,而ContextThemeWrapper只是一个包装类,真正的逻辑在ContextImpl中,让我们看看其中做了什么。

@Override
public void startActivity(Intent intent, Bundle options) {
    warnIfCallingFromSystemProcess();
    if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
        throw new AndroidRuntimeException(
                "Calling startActivity() from outside of an Activity "
                + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                + " Is this really what you want?");
    }
    mMainThread.getInstrumentation().execStartActivity(
        getOuterContext(), mMainThread.getApplicationThread(), null,
        (Activity)null, intent, -1, options);
}

其中调用了mMainThread.getInstrumentation()获取一个Instrumentation对象,这个对象大家可以看作是activity的管家,对activity的操作都会调用它去执行。让我们看看它的execStartActivity方法。

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                if (am.match(who, null, intent)) {
                    am.mHits++;
                    if (am.isBlocking()) {
                        return requestCode >= 0 ? am.getResult() : null;
                    }
                    break;
                }
            }
        }
    }
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess();
        int result = ActivityManagerNative.getDefault()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
    }
    return null;
}

这个方法最关键的一点就是调用了ActivityManagerNative.getDefault()去获取一个ActivityManagerNative对象并且调用了它的startActivity方法。

public abstract class ActivityManagerNative extends Binder implements IActivityManager

从它的定义就可以看出它是和aidl相关的,对于aidl这里我不细讲,大家可以自行查阅相关资料,概括来说就是Android中一种跨进程的通信方式。

那既然是跨进程的,通过ActivityManagerNative跨到了哪个进程呢?答案是ActivityManagerService,简称AMS,它是ActivityManagerNative的实现类。是Android framework层最最最重要的几个类之一,是运行在Android内核进程的。下面让我们看看AMS的startActivity方法。

@Override
public final int startActivity(IApplicationThread caller, String callingPackage,
        Intent intent, String resolvedType, IBinder resultTo,
        String resultWho, int requestCode, int startFlags,
        String profileFile, ParcelFileDescriptor profileFd, Bundle options) {
    return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
            resultWho, requestCode,
            startFlags, profileFile, profileFd, options, UserHandle.getCallingUserId());
}

可以看到调用了startActivityAsUser。而在startActivityAsUser方法中调用了ActivityStackSupervisor的startActivityMayWait方法。

final int startActivityMayWait(IApplicationThread caller, int callingUid,String callingPackage, Intent intent, String resolvedType, IBinder resultTo,String resultWho, int requestCode, int startFlags, String profileFile,
ParcelFileDescriptor profileFd, WaitResult outResult, Configuration config,Bundle options, int userId) {

     ..........

     int res = startActivityLocked(caller, intent, resolvedType,aInfo, resultTo, resultWho, requestCode, callingPid, callingUid,callingPackage, startFlags, options, componentSpecified, null);

    .........
  }
}

这个方法内容很多,前面主要是对一些权限的判断,这个我们等等再讲,而在判断完权限之后,调用了startActivityLocked方法。

在调用了startActivityLocked方法之后,是一系列和ActivityStack这个类的交互,这其中的过程我这里不分析了,从ActivityStack这个类的名字就可以看出它是和Activity栈相关的,交互的主要目的也就是处理activity栈的需求。最后会调用到realStartActivityLocked方法。

final boolean realStartActivityLocked(ActivityRecord r,ProcessRecord app, boolean andResume, boolean checkConfig)
        throws RemoteException {

    r.startFreezingScreenLocked(app, 0);
    if (false) Slog.d(TAG, "realStartActivity: setting app visibility true");
    mWindowManager.setAppVisibility(r.appToken, true);

    // schedule launch ticks to collect information about slow apps.
    r.startLaunchTickingLocked();

    // Have the window manager re-evaluate the orientation of
    // the screen based on the new activity order.  Note that
    // as a result of this, it can call back into the activity
    // manager with a new orientation.  We don't care about that,
    // because the activity is not currently running so we are
    // just restarting it anyway.
    if (checkConfig) {
        Configuration config = mWindowManager.updateOrientationFromAppTokens(
                mService.mConfiguration,
                r.mayFreezeScreenLocked(app) ? r.appToken : null);
        mService.updateConfigurationLocked(config, r, false, false);
    }

    r.app = app;
    app.waitingToKill = null;
    r.launchCount++;
    r.lastLaunchTime = SystemClock.uptimeMillis();

    if (localLOGV) Slog.v(TAG, "Launching: " + r);

    int idx = app.activities.indexOf(r);
    if (idx < 0) {
        app.activities.add(r);
    }
    mService.updateLruProcessLocked(app, true, true);

    final ActivityStack stack = r.task.stack;
    try {
        if (app.thread == null) {
            throw new RemoteException();
        }
        List<ResultInfo> results = null;
        List<Intent> newIntents = null;
        if (andResume) {
            results = r.results;
            newIntents = r.newIntents;
        }
        if (DEBUG_SWITCH) Slog.v(TAG, "Launching: " + r
                + " icicle=" + r.icicle
                + " with results=" + results + " newIntents=" + newIntents
                + " andResume=" + andResume);
        if (andResume) {
            EventLog.writeEvent(EventLogTags.AM_RESTART_ACTIVITY,
                    r.userId, System.identityHashCode(r),
                    r.task.taskId, r.shortComponentName);
        }
        if (r.isHomeActivity() && r.isNotResolverActivity()) {
            // Home process is the root process of the task.
            mService.mHomeProcess = r.task.mActivities.get(0).app;
        }
        mService.ensurePackageDexOpt(r.intent.getComponent().getPackageName());
        r.sleeping = false;
        r.forceNewConfig = false;
        mService.showAskCompatModeDialogLocked(r);
        r.compat = mService.compatibilityInfoForPackageLocked(r.info.applicationInfo);
        String profileFile = null;
        ParcelFileDescriptor profileFd = null;
        boolean profileAutoStop = false;
        if (mService.mProfileApp != null && mService.mProfileApp.equals(app.processName)) {
            if (mService.mProfileProc == null || mService.mProfileProc == app) {
                mService.mProfileProc = app;
                profileFile = mService.mProfileFile;
                profileFd = mService.mProfileFd;
                profileAutoStop = mService.mAutoStopProfiler;
            }
        }
        app.hasShownUi = true;
        app.pendingUiClean = true;
        if (profileFd != null) {
            try {
                profileFd = profileFd.dup();
            } catch (IOException e) {
                if (profileFd != null) {
                    try {
                        profileFd.close();
                    } catch (IOException o) {
                    }
                    profileFd = null;
                }
            }
        }
        app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_TOP);
        app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                System.identityHashCode(r), r.info,
                new Configuration(mService.mConfiguration), r.compat,
                app.repProcState, r.icicle, results, newIntents, !andResume,
                mService.isNextTransitionForward(), profileFile, profileFd,
                profileAutoStop);

        if ((app.info.flags&ApplicationInfo.FLAG_CANT_SAVE_STATE) != 0) {
            // This may be a heavy-weight process!  Note that the package
            // manager will ensure that only activity can run in the main
            // process of the .apk, which is the only thing that will be
            // considered heavy-weight.
            if (app.processName.equals(app.info.packageName)) {
                if (mService.mHeavyWeightProcess != null
                        && mService.mHeavyWeightProcess != app) {
                    Slog.w(TAG, "Starting new heavy weight process " + app
                            + " when already running "
                            + mService.mHeavyWeightProcess);
                }
                mService.mHeavyWeightProcess = app;
                Message msg = mService.mHandler.obtainMessage(
                        ActivityManagerService.POST_HEAVY_NOTIFICATION_MSG);
                msg.obj = r;
                mService.mHandler.sendMessage(msg);
            }
        }

    } catch (RemoteException e) {
        if (r.launchFailed) {
            // This is the second time we failed -- finish activity
            // and give up.
            Slog.e(TAG, "Second failure launching "
                  + r.intent.getComponent().flattenToShortString()
                  + ", giving up", e);
            mService.appDiedLocked(app, app.pid, app.thread);
            stack.requestFinishActivityLocked(r.appToken, Activity.RESULT_CANCELED, null,
                    "2nd-crash", false);
            return false;
        }

        // This is the first time we failed -- restart process and
        // retry.
        app.activities.remove(r);
        throw e;
    }

    r.launchFailed = false;
    if (stack.updateLRUListLocked(r)) {
        Slog.w(TAG, "Activity " + r
              + " being launched, but already in LRU list");
    }

    if (andResume) {
        // As part of the process of launching, ActivityThread also performs
        // a resume.
        stack.minimalResumeActivityLocked(r);
    } else {
        // This activity is not starting in the resumed state... which
        // should look like we asked it to pause+stop (but remain visible),
        // and it has done so and reported back the current icicle and
        // other state.
        if (DEBUG_STATES) Slog.v(TAG, "Moving to STOPPED: " + r
                + " (starting in stopped state)");
        r.state = ActivityState.STOPPED;
        r.stopped = true;
    }

    // Launch the new version setup screen if needed.  We do this -after-
    // launching the initial activity (that is, home), so that it can have
    // a chance to initialize itself while in the background, making the
    // switch back to it faster and look better.
    if (isFrontStack(stack)) {
        mService.startSetupActivityLocked();
    }

    return true;
}

这么长的方法,看的头都晕了,不过没关系,我们看重点的。

app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,System.identityHashCode(r), r.info,new Configuration(mService.mConfiguration), r.compat,
app.repProcState, r.icicle, results, newIntents, !andResume,
mService.isNextTransitionForward(), profileFile, profileFd,
profileAutoStop);

重点来了,调用了ApplicationThread的scheduleLaunchActivity方法。大家千万不要被这个类的名字所迷惑了,以为他是一个线程。其实它不仅是线程,还是一个Binder对象,也是和aidl相关的,实现了IApplicationThread接口,定义在ActivityThread类的内部。那我们为什么要用它呢?回想一下,刚刚在Instrumentation里调用了AMS的startActivity之后的所有操作,都是在系统进程中进行的,而现在我们要返回到我们自己的app进程,同样是跨进程,我们需要一个aidl框架去完成,所以这里才会有ApplicationThread。让我们看看具体的方法吧。

public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo,int procState, Bundle state, List<ResultInfo> pendingResults,List<Intent> pendingNewIntents, boolean notResumed, boolean isForward,String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) {

        updateProcessState(procState, false);

        ActivityClientRecord r = new ActivityClientRecord();

        r.token = token;
        r.ident = ident;
        r.intent = intent;
        r.activityInfo = info;
        r.compatInfo = compatInfo;
        r.state = state;

        r.pendingResults = pendingResults;
        r.pendingIntents = pendingNewIntents;

        r.startsNotResumed = notResumed;
        r.isForward = isForward;

        r.profileFile = profileName;
        r.profileFd = profileFd;
        r.autoStopProfiler = autoStopProfiler;

        updatePendingConfiguration(curConfig);

        queueOrSendMessage(H.LAUNCH_ACTIVITY, r);
    }

在最后调用了queueOrSendMessage(H.LAUNCH_ACTIVITY, r)这个方法。

而这里的H其实是一个handler,queueOrSendMessage方法的作用就是通过handler去send一个message。

public void handleMessage(Message msg) {    
    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
        switch (msg.what) {
            case LAUNCH_ACTIVITY: {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                ActivityClientRecord r = (ActivityClientRecord)msg.obj;

                r.packageInfo = getPackageInfoNoCheck(
                        r.activityInfo.applicationInfo, r.compatInfo);
                handleLaunchActivity(r, null);
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            } break;

     ..........
}

我们看H的handleMessage,如果message是我们刚刚发送的LAUNCH_ACTIVITY,则调用handleLaunchActivity方法。而在这个方法中,调用了performLaunchActivity去创建一个Activity。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");

    ActivityInfo aInfo = r.activityInfo;
    if (r.packageInfo == null) {
        r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                Context.CONTEXT_INCLUDE_CODE);
    }

    ComponentName component = r.intent.getComponent();
    if (component == null) {
        component = r.intent.resolveActivity(
            mInitialApplication.getPackageManager());
        r.intent.setComponent(component);
    }

    if (r.activityInfo.targetActivity != null) {
        component = new ComponentName(r.activityInfo.packageName,
                r.activityInfo.targetActivity);
    }

    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }

    try {
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);

        if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
        if (localLOGV) Slog.v(
                TAG, r + ": app=" + app
                + ", appName=" + app.getPackageName()
                + ", pkg=" + r.packageInfo.getPackageName()
                + ", comp=" + r.intent.getComponent().toShortString()
                + ", dir=" + r.packageInfo.getAppDir());

        if (activity != null) {
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config = new Configuration(mCompatConfiguration);
            if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                    + r.activityInfo.name + " with config " + config);
            activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config);

            if (customIntent != null) {
                activity.mIntent = customIntent;
            }
            r.lastNonConfigurationInstances = null;
            activity.mStartedActivity = false;
            int theme = r.activityInfo.getThemeResource();
            if (theme != 0) {
                activity.setTheme(theme);
            }

            activity.mCalled = false;
            mInstrumentation.callActivityOnCreate(activity, r.state);
            if (!activity.mCalled) {
                throw new SuperNotCalledException(
                    "Activity " + r.intent.getComponent().toShortString() +
                    " did not call through to super.onCreate()");
            }
            r.activity = activity;
            r.stopped = true;
            if (!r.activity.mFinished) {
                activity.performStart();
                r.stopped = false;
            }
            if (!r.activity.mFinished) {
                if (r.state != null) {
                    mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
                }
            }
            if (!r.activity.mFinished) {
                activity.mCalled = false;
                mInstrumentation.callActivityOnPostCreate(activity, r.state);
                if (!activity.mCalled) {
                    throw new SuperNotCalledException(
                        "Activity " + r.intent.getComponent().toShortString() +
                        " did not call through to super.onPostCreate()");
                }
            }
        }
        r.paused = true;

        mActivities.put(r.token, r);

    } catch (SuperNotCalledException e) {
        throw e;

    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to start activity " + component
                + ": " + e.toString(), e);
        }
    }

    return activity;
}

方法比较长,其中核心的内容是:

try {
    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
}

又调用了Instrumentation的newActivity去创建一个Activity。

至此,启动一个activity的过程就分析完了,让我们来总结一下。

(1) 我们app中是使用了Activity的startActivity方法,具体调用的是ContextImpl的同名函数。

(2) ContextImpl中会调用Instrumentation的execStartActivity方法。

(3) Instrumentation通过aidl进行跨进程通信,最终调用AMS的startActivity方法。

(4) 在系统进程中AMS中先判断权限,然后通过调用ActivityStackSupervisor和ActivityStack进行一系列的交互用来确定Activity栈的使用方式。

(5) 通过ApplicationThread进行跨进程通信,转回到app进程。

(6) 通过ActivityThread中的H(一个handler)传递消息,最终调用Instrumentation来创建一个Activity。

其实如果大家看过其他有关Android系统的调用,比如启动一个Service之类的,整个过程都是大同小异的,无非就是跨进程和AMS,WMS或者PMS进行通信,然后通过ApplicationThread回到app进程最后通过handler传递消息。

好了,说了这么多,这和我们的插件化有什么关系呢?大家想一想,如果我们要在主apk中启动一个插件apk的Activity,上面的哪一步会出问题?大家好好想一想,想一想,一想,想。。。。

没错!就是(4),第四步,权限验证会通不过,为啥呢?因为我们主apk的manifest中没有定义插件apk的Activity啊!

让我们回到代码,看看ActivityStackSupervisor的startActivityMayWait方法,关于权限验证的内容都在里面。

if (err == ActivityManager.START_SUCCESS && aInfo == null) {   
        // We couldn't find the specific class specified in the Intent.
        // Also the end of the line.
        err = ActivityManager.START_CLASS_NOT_FOUND;
}

if (err != ActivityManager.START_SUCCESS) {
            if (resultRecord != null) {
                resultStack.sendActivityResultLocked(-1,
                    resultRecord, resultWho, requestCode,
                    Activity.RESULT_CANCELED, null);
            }
            setDismissKeyguard(false);
            ActivityOptions.abort(options);
            return err;
}

在这个方法中有这么一段,而在Instrumentation中会去检查这个值。

public static void checkStartActivityResult(int res, Object intent) {
    if (res >= ActivityManager.START_SUCCESS) {
        return;
    }

    switch (res) {
        case ActivityManager.START_INTENT_NOT_RESOLVED:
        case ActivityManager.START_CLASS_NOT_FOUND:
            if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                throw new ActivityNotFoundException(
                        "Unable to find explicit activity class "
                        + ((Intent)intent).getComponent().toShortString()
                        + "; have you declared this activity in your AndroidManifest.xml?");
            throw new ActivityNotFoundException(
                    "No Activity found to handle " + intent);
        .....
}

如果找不到对应的Activity,直接抛出错误。

唔。。怎么办呢?找不到Activity,也许你会觉得直接在主apk的manifest中实现定义就好了,但是这是不可能的,因为你怎么知道插件apk中有什么Activity呢?如果以后要动态的修改插件apk中的Activity,难道你的主apk也要对应的一次次修改吗?

不要慌,办法都是人想出来的,让我们看看Samll和DroidPlugin的解决办法吧。

思考解决方案

首先我们要明确,要解决的核心问题是[如何能让没有在manifst中注册的Activity能启动起来]。由于权限验证机制是系统做的,我们肯定是没办法修改的,既然我们没办法修改,那是不是考虑去欺骗呢?也就是说可以在manifest中预先定义好几个Activity,俗称占坑,比如名字就叫ActivityA,ActivityB,在校验权限之前把我们插件apk中的Activity替换成定义好的Activity,这样就能顺利通过校验,而在之后真正生成Activity的地方再换回来,瞒天过海。

那怎么去欺骗呢?回归前面的代码,其实答案已经呼之欲出了——我们可以有两种选择,hook Instrumentation或者hook ActivityManagerNative。这也正好对应了Small和DroidPlugin的实现方案。

Small实现方式

首先,让我们看一下Small的实现方式,大家可以去它的GitHub上下载源码。

上文提到,Samll的实现方式是hook Instrumentation,让我们从代码上来看,我们看它的ApkBundleLauncher类,它内部有一个setUp方法。

@Override
public void setUp(Context context) {
    super.setUp(context);
    // Inject instrumentation
    if (sHostInstrumentation == null) {
        try {
            final Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            final Method method = activityThreadClass.getMethod("currentActivityThread");
            Object thread = method.invoke(null, (Object[]) null);
            Field field = activityThreadClass.getDeclaredField("mInstrumentation");
            field.setAccessible(true);
            sHostInstrumentation = (Instrumentation) field.get(thread);
            Instrumentation wrapper = new InstrumentationWrapper();
            field.set(thread, wrapper);

            if (context instanceof Activity) {
                field = Activity.class.getDeclaredField("mInstrumentation");
                field.setAccessible(true);
                field.set(context, wrapper);
            }
        } catch (Exception ignored) {
            ignored.printStackTrace();
            // Usually, cannot reach here
        }
    }
}

可以看到它通过反射获取了Instrumentation并且赋值成了自定义的InstrumentationWrapper。

而在自己定义的InstrumentationWrapper中,会去重写两个重要的方法,那就是execStartActivity和newActivity这两个方法。为什么会选择这两个方法呢?因为我们前面说过,在第一个方法中,系统会去校验权限,而第二个方法则是真正生成Activity实例的方法,我们通过重写两个方法,可以做到我们之前提到的[在execStartActivity的时候把Activity替换成自己的],而[在newActivity又换回成真正的Activity],从而做到[欺骗]的效果。

/** @Override V21+
 * Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, android.os.Bundle options) {
    wrapIntent(intent);
    return ReflectAccelerator.execStartActivityV21(sHostInstrumentation,
            who, contextThread, token, target, intent, requestCode, options);
}

/** @Override V20-
 * Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode) {
    wrapIntent(intent);
    return ReflectAccelerator.execStartActivityV20(sHostInstrumentation,
            who, contextThread, token, target, intent, requestCode);
}

可以看到Wrapper首先根据sdk版本重写了两个不同的方法,这里我们只关注一个就可以,先看wrapIntent方法。

private void wrapIntent(Intent intent) {
    ComponentName component = intent.getComponent();
    if (component == null) return; // ignore system intent

    String realClazz = intent.getComponent().getClassName();
    if (sLoadedActivities == null) return;

    ActivityInfo ai = sLoadedActivities.get(realClazz);
    if (ai == null) return;

    // Carry the real(plugin) class for incoming `newActivity' method.
    intent.addCategory(REDIRECT_FLAG + realClazz);
    String stubClazz = dequeueStubActivity(ai, realClazz);
    intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
}

这个方法的神奇之处在于它先获取了真正的Activity的信息并且保存起来,然后调用了dequeueStubActivity方法去生成一个[占坑]的Activity。

private String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) {
    if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
        // In standard mode, the stub activity is reusable.
        return STUB_ACTIVITY_PREFIX;
    }

    int availableId = -1;
    int stubId = -1;
    int countForMode = STUB_ACTIVITIES_COUNT;
    int countForAll = countForMode * 3; // 3=[singleTop, singleTask, singleInstance]
    if (mStubQueue == null) {
        // Lazy init
        mStubQueue = new String[countForAll];
    }
    int offset = (ai.launchMode - 1) * countForMode;
    for (int i = 0; i < countForMode; i++) {
        String usedActivityClazz = mStubQueue[i + offset];
        if (usedActivityClazz == null) {
            if (availableId == -1) availableId = i;
        } else if (usedActivityClazz.equals(realActivityClazz)) {
            stubId = i;
        }
    }
    if (stubId != -1) {
        availableId = stubId;
    } else if (availableId != -1) {
        mStubQueue[availableId + offset] = realActivityClazz;
    } else {
        // TODO:
        Log.e(TAG, "Launch mode " + ai.launchMode + " is full");
    }
    return STUB_ACTIVITY_PREFIX + ai.launchMode + availableId;
}

可以看到其中根据真正的Activity的launchMode等因素生成一个占坑Activity,而这些占坑Activity都是定义在manifest中的。

<application>
    <!-- Stub Activities -->
    <!-- 1 standard mode -->
    <activity android:name=".A" android:launchMode="standard"/>
    <!-- 4 singleTask mode -->
    <activity android:name=".A10" android:launchMode="singleTask"/>
    <activity android:name=".A11" android:launchMode="singleTask"/>
    <activity android:name=".A12" android:launchMode="singleTask"/>
    <activity android:name=".A13" android:launchMode="singleTask"/>
    <!-- 4 singleTop mode -->
    <activity android:name=".A20" android:launchMode="singleTop"/>
    <activity android:name=".A21" android:launchMode="singleTop"/>
    <activity android:name=".A22" android:launchMode="singleTop"/>
    <activity android:name=".A23" android:launchMode="singleTop"/>
    <!-- 4 singleInstance mode -->
    <activity android:name=".A30" android:launchMode="singleInstance"/>
    <activity android:name=".A31" android:launchMode="singleInstance"/>
    <activity android:name=".A32" android:launchMode="singleInstance"/>
    <activity android:name=".A33" android:launchMode="singleInstance"/>

    <!-- Web Activity -->
    <activity android:name=".webkit.WebActivity"
        android:screenOrientation="portrait"
        android:windowSoftInputMode="stateHidden|adjustPan"/>
    <!--<service android:name="net.wequick.small.service.UpgradeService"-->
        <!--android:exported="false"/>-->
</application>

通过这样的方式我们就顺利的完成了[瞒天过海]的第一步,在之后AMS的权限校验中就能顺利通过了。接着让我们来看newActivity方法。

@Override
/** Unwrap activity from STUB to REAL */
public Activity newActivity(ClassLoader cl, String className, Intent intent)
        throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    // Stub -> Real
    if (!className.startsWith(STUB_ACTIVITY_PREFIX)) {
        return super.newActivity(cl, className, intent);
    }
    className = unwrapIntent(intent, className);
    Activity activity = super.newActivity(cl, className, intent);
    return activity;
}

private String unwrapIntent(Intent intent, String className) {
            Set<String> categories = intent.getCategories();
            if (categories == null) return className;

            // Get plugin activity class name from categories
            Iterator<String> it = categories.iterator();
            String realClazz = null;
            while (it.hasNext()) {
                String category = it.next();
                if (category.charAt(0) == REDIRECT_FLAG) {
                    realClazz = category.substring(1);
                    break;
                }
            }
            if (realClazz == null) return className;
            return realClazz;
}

这里就更简单了,通过unwrapIntent获取真正的Activity并且调用父类的newActivity方法,也就是Instrumentation去生成一个Activity。

到这儿,Small的解决方案就基本讲完了,原理是很简单的,就是通过反射替换掉Instrumentation,然后在里面做文章。

DroidPlugin实现方式

DroidPlugin是另外一种插件化框架,它采取的方案是hook整个ActivityManagerNative。

首先让我们看它其中的IActivityManagerHook类。

public class IActivityManagerHook extends ProxyHook

它继承自ProxyHook。

public abstract class ProxyHook extends Hook implements InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        try {
            if (!isEnable()) {
                return method.invoke(mOldObj, args);
            }
            HookedMethodHandler hookedMethodHandler = mHookHandles.getHookedMethodHandler(method);
            if (hookedMethodHandler != null) {
                return hookedMethodHandler.doHookInner(mOldObj, method, args);
            }
            return method.invoke(mOldObj, args);
        }

        ...........
}

ProxyHook实现了InvocationHandler接口,也就是说它是用于动态代理的,在invoke方法中先通过mHookHandles去获取对应的hookedMethodHandler,这里的mHookHandles在我们对应的情况下是IActivityManagerHookHandle。

public class IActivityManagerHookHandle extends BaseHookHandle {
    
    @Override
    protected void init() {
        sHookedMethodHandlers.put("startActivity", new startActivity(mHostContext));
        sHookedMethodHandlers.put("startActivityAsUser", new startActivityAsUser(mHostContext));
        sHookedMethodHandlers.put("startActivityAsCaller", new startActivityAsCaller(mHostContext));
        sHookedMethodHandlers.put("startActivityAndWait", new startActivityAndWait(mHostContext));
        sHookedMethodHandlers.put("startActivityWithConfig", new startActivityWithConfig(mHostContext));
        sHookedMethodHandlers.put("startActivityIntentSender", new startActivityIntentSender(mHostContext));
        sHookedMethodHandlers.put("startVoiceActivity", new startVoiceActivity(mHostContext));
        sHookedMethodHandlers.put("startNextMatchingActivity", new startNextMatchingActivity(mHostContext));
        sHookedMethodHandlers.put("startActivityFromRecents", new startActivityFromRecents(mHostContext));
        sHookedMethodHandlers.put("finishActivity", new finishActivity(mHostContext));
        sHookedMethodHandlers.put("registerReceiver", new registerReceiver(mHostContext));
        sHookedMethodHandlers.put("broadcastIntent", new broadcastIntent(mHostContext));
        sHookedMethodHandlers.put("unbroadcastIntent", new unbroadcastIntent(mHostContext));
        sHookedMethodHandlers.put("getCallingPackage", new getCallingPackage(mHostContext));
        sHookedMethodHandlers.put("getCallingActivity", new getCallingActivity(mHostContext));
        sHookedMethodHandlers.put("getAppTasks", new getAppTasks(mHostContext));
        sHookedMethodHandlers.put("addAppTask", new addAppTask(mHostContext));
        sHookedMethodHandlers.put("getTasks", new getTasks(mHostContext));
        sHookedMethodHandlers.put("getServices", new getServices(mHostContext));
        sHookedMethodHandlers.put("getProcessesInErrorState", new getProcessesInErrorState(mHostContext));
        sHookedMethodHandlers.put("getContentProvider", new getContentProvider(mHostContext));
        sHookedMethodHandlers.put("getContentProviderExternal", new getContentProviderExternal(mHostContext));
        sHookedMethodHandlers.put("removeContentProviderExternal", new removeContentProviderExternal(mHostContext));
        sHookedMethodHandlers.put("publishContentProviders", new publishContentProviders(mHostContext));
        sHookedMethodHandlers.put("getRunningServiceControlPanel", new getRunningServiceControlPanel(mHostContext));
        sHookedMethodHandlers.put("startService", new startService(mHostContext));
        sHookedMethodHandlers.put("stopService", new stopService(mHostContext));
        sHookedMethodHandlers.put("stopServiceToken", new stopServiceToken(mHostContext));
        sHookedMethodHandlers.put("setServiceForeground", new setServiceForeground(mHostContext));
        sHookedMethodHandlers.put("bindService", new bindService(mHostContext));
        sHookedMethodHandlers.put("publishService", new publishService(mHostContext));
        sHookedMethodHandlers.put("unbindFinished", new unbindFinished(mHostContext));
        sHookedMethodHandlers.put("peekService", new peekService(mHostContext));
        sHookedMethodHandlers.put("bindBackupAgent", new bindBackupAgent(mHostContext));
        sHookedMethodHandlers.put("backupAgentCreated", new backupAgentCreated(mHostContext));
        sHookedMethodHandlers.put("unbindBackupAgent", new unbindBackupAgent(mHostContext));
        sHookedMethodHandlers.put("killApplicationProcess", new killApplicationProcess(mHostContext));
        sHookedMethodHandlers.put("startInstrumentation", new startInstrumentation(mHostContext));
        sHookedMethodHandlers.put("getActivityClassForToken", new getActivityClassForToken(mHostContext));
        sHookedMethodHandlers.put("getPackageForToken", new getPackageForToken(mHostContext));
        sHookedMethodHandlers.put("getIntentSender", new getIntentSender(mHostContext));
        sHookedMethodHandlers.put("clearApplicationUserData", new clearApplicationUserData(mHostContext));
        sHookedMethodHandlers.put("handleIncomingUser", new handleIncomingUser(mHostContext));
        sHookedMethodHandlers.put("grantUriPermission", new grantUriPermission(mHostContext));
        sHookedMethodHandlers.put("getPersistedUriPermissions", new getPersistedUriPermissions(mHostContext));
        sHookedMethodHandlers.put("killBackgroundProcesses", new killBackgroundProcesses(mHostContext));
        sHookedMethodHandlers.put("forceStopPackage", new forceStopPackage(mHostContext));
        sHookedMethodHandlers.put("getRunningAppProcesses", new getRunningAppProcesses(mHostContext));
        sHookedMethodHandlers.put("getRunningExternalApplications", new getRunningExternalApplications(mHostContext));
        sHookedMethodHandlers.put("getMyMemoryState", new getMyMemoryState(mHostContext));
        sHookedMethodHandlers.put("crashApplication", new crashApplication(mHostContext));
        sHookedMethodHandlers.put("grantUriPermissionFromOwner", new grantUriPermissionFromOwner(mHostContext));
        sHookedMethodHandlers.put("checkGrantUriPermission", new checkGrantUriPermission(mHostContext));
        sHookedMethodHandlers.put("startActivities", new startActivities(mHostContext));
        sHookedMethodHandlers.put("getPackageScreenCompatMode", new getPackageScreenCompatMode(mHostContext));
        sHookedMethodHandlers.put("setPackageScreenCompatMode", new setPackageScreenCompatMode(mHostContext));
        sHookedMethodHandlers.put("getPackageAskScreenCompat", new getPackageAskScreenCompat(mHostContext));
        sHookedMethodHandlers.put("setPackageAskScreenCompat", new setPackageAskScreenCompat(mHostContext));
        sHookedMethodHandlers.put("navigateUpTo", new navigateUpTo(mHostContext));
        sHookedMethodHandlers.put("serviceDoneExecuting", new serviceDoneExecuting(mHostContext));


    }
}

可以看到在init中生成了很多类,而我们在invoke方法中就会根据对应的方法名拿到对应的类。

回想一下,我们在这里对应的是什么方法?根据前面的文章,我们知道是startActivity方法,进而拿到的是startActivity类。

回到invoke方法,拿到HookedMethodHandler后,会执行它的doInnerHook方法。

public synchronized Object doHookInner(Object receiver, Method method, Object[] args) throws Throwable {
    long b = System.currentTimeMillis();
    try {
        mUseFakedResult = false;
        mFakedResult = null;
        boolean suc = beforeInvoke(receiver, method, args);
        Object invokeResult = null;
        if (!suc) {
            invokeResult = method.invoke(receiver, args);
        }
        afterInvoke(receiver, method, args, invokeResult);
        if (mUseFakedResult) {
            return mFakedResult;
        } else {
            return invokeResult;
        }
    } finally {
        long time = System.currentTimeMillis() - b;
        if (time > 5) {
            Log.i(TAG, "doHookInner method(%s.%s) cost %s ms", method.getDeclaringClass().getName(), method.getName(), time);
        }
    }
}

可以看到这个方法也是AOP的,在真正的调用method.invoke之前和之后会对应的调用beforeInvoke和afterInvoke。这两个方法是有待HookedMethodHandler的子类去实现的,这里是startActivity这个子类。

@Override
protected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {

    RunningActivities.beforeStartActivity();
    boolean bRet;
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
        bRet = doReplaceIntentForStartActivityAPILow(args);
    } else {
        bRet = doReplaceIntentForStartActivityAPIHigh(args);
    }
    if (!bRet) {
        setFakedResult(Activity.RESULT_CANCELED);
        return true;
    }

    return super.beforeInvoke(receiver, method, args);
}

根据sdk版本进入不同的方法,这里我们只看APIHigh。

protected boolean doReplaceIntentForStartActivityAPIHigh(Object[] args) throws RemoteException {
            int intentOfArgIndex = findFirstIntentIndexInArgs(args);
            if (args != null && args.length > 1 && intentOfArgIndex >= 0) {
                Intent intent = (Intent) args[intentOfArgIndex];
                //XXX String callingPackage = (String) args[1];
                if (!PluginPatchManager.getInstance().canStartPluginActivity(intent)) {
                    PluginPatchManager.getInstance().startPluginActivity(intent);
                    return false;
                }
                ActivityInfo activityInfo = resolveActivity(intent);
                if (activityInfo != null && isPackagePlugin(activityInfo.packageName)) {
                    ComponentName component = selectProxyActivity(intent);
                    if (component != null) {
                        Intent newIntent = new Intent();
                        try {
                            ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName());
                            setIntentClassLoader(newIntent, pluginClassLoader);
                        } catch (Exception e) {
                            Log.w(TAG, "Set Class Loader to new Intent fail", e);
                        }
                        newIntent.setComponent(component);
                        newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
                        newIntent.setFlags(intent.getFlags());


                        String callingPackage = (String) args[1];
                        if (TextUtils.equals(mHostContext.getPackageName(), callingPackage)) {
                        newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        args[intentOfArgIndex] = newIntent;
                        args[1] = mHostContext.getPackageName();
                    } else {
                        Log.w(TAG, "startActivity,replace selectProxyActivity fail");
                    }
                }
            }

            return true;
}

又是一个逻辑比较多的方法,但是很好理解,其实和Small做的事是差不多的,就是生成一个占坑的Intent,把真正的activity当作参数放在Intent中。

到这儿,就完成了第一步,我们要做的就是在对应的地方Hook掉这个AMS,至于怎么Hook大家自己去看源码,Hook的技巧不是重点,重点是Hook了什么。

那DroidPlugin是在哪里把Activity还原了呢?回想一下前文startActivity步骤6:

(6) 通过ActivityThread中的H(一个handler)传递消息,最终调用Instrumentation来创建一个Activity。

通过Handler去传递消息,让我们看看Handler的源码。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

在dispatchMessage中,有一个CallBack,如果它不为空,就使用它去处理而不走Handler的handleMessage方法。DroidPlugin正是利用了这一点,自己去生成了一个CallBack并且通过反射注入到了ActivityThread的H类中。

@Override
public boolean handleMessage(Message msg) {

     if (msg.what == LAUNCH_ACTIVITY) {
                return handleLaunchActivity(msg);
     }

     ...........
}

这是对应CallBack的handleMessage方法,如果message是LAUNCH_ACTIVITY,直接调用了handleLaunchActivity方法。

private boolean handleLaunchActivity(Message msg) {
    try {
        Object obj = msg.obj;
        Intent stubIntent = (Intent) FieldUtils.readField(obj, "intent");
        //ActivityInfo activityInfo = (ActivityInfo) FieldUtils.readField(obj, "activityInfo", true);
        stubIntent.setExtrasClassLoader(mHostContext.getClassLoader());
        Intent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
        // 这里多加一个isNotShortcutProxyActivity的判断,因为ShortcutProxyActivity的很特殊,启动它的时候,
        // 也会带上一个EXTRA_TARGET_INTENT的数据,就会导致这里误以为是启动插件Activity,所以这里要先做一个判断。
        // 之前ShortcutProxyActivity错误复用了key,但是为了兼容,所以这里就先这么判断吧。
        if (targetIntent != null && !isShortcutProxyActivity(stubIntent)) {
            IPackageManagerHook.fixContextPackageManager(mHostContext);
            ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager());
            ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);
            if (targetActivityInfo != null) {

                if (targetComponentName != null && targetComponentName.getClassName().startsWith(".")) {
                    targetIntent.setClassName(targetComponentName.getPackageName(), targetComponentName.getPackageName() + targetComponentName.getClassName());
                }

                ResolveInfo resolveInfo = mHostContext.getPackageManager().resolveActivity(stubIntent, 0);
                ActivityInfo stubActivityInfo = resolveInfo != null ? resolveInfo.activityInfo : null;
                if (stubActivityInfo != null) {
                    PluginManager.getInstance().reportMyProcessName(stubActivityInfo.processName, targetActivityInfo.processName, targetActivityInfo.packageName);
                }
                PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);
                ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(targetComponentName.getPackageName());
                setIntentClassLoader(targetIntent, pluginClassLoader);
                setIntentClassLoader(stubIntent, pluginClassLoader);

                boolean success = false;
                try {
                    targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);
                    if (stubActivityInfo != null) {
                        targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);
                    }
                    success = true;
                } catch (Exception e) {
                    Log.e(TAG, "putExtra 1 fail", e);
                }

                if (!success && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
                    try {
                        ClassLoader oldParent = fixedClassLoader(pluginClassLoader);
                        targetIntent.putExtras(targetIntent.getExtras());

                        targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);
                        if (stubActivityInfo != null) {
                            targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);
                        }
                        fixedClassLoader(oldParent);
                        success = true;
                    } catch (Exception e) {
                        Log.e(TAG, "putExtra 2 fail", e);
                    }
                }

                if (!success) {
                    Intent newTargetIntent = new Intent();
                    newTargetIntent.setComponent(targetIntent.getComponent());
                    newTargetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);
                    if (stubActivityInfo != null) {
                        newTargetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);
                    }
                    FieldUtils.writeDeclaredField(msg.obj, "intent", newTargetIntent);
                } else {
                    FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent);
                }
                FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);

                Log.i(TAG, "handleLaunchActivity OK");
            } else {
                Log.e(TAG, "handleLaunchActivity oldInfo==null");
            }
        } else {
            Log.e(TAG, "handleLaunchActivity targetIntent==null");
        }
    } catch (Exception e) {
        Log.e(TAG, "handleLaunchActivity FAIL", e);
    }

    if (mCallback != null) {
        return mCallback.handleMessage(msg);
    } else {
        return false;
    }
}

代码依旧很长,但是核心就是拿到真正的Activity并且创建。

至此,DroidPlugin的逻辑也就理完了,和Small不同,它是hook了整个AMS并且通过反射替换了H类的CallBack,做到了[瞒天过海]。

两种方式的比较

那么这两种方式孰优孰劣呢?

我认为,DroidPlugin的方式是更加[屌]的,大家可以看上面IActivityManagerHookHandle这个类,在init方法中put的那些类,和Activity相关的只有一小部分,更多的是service,broadcast等等的操作。这是Small使用[Hook Instrumentation]所做不到的,因为Instrumentation只是Activity的管家,如果涉及到service这样的,它就无能为力了。

但是从另外一个方面说,Service的动态注册这样的需求其实是不多的,Hook Instrumentation基本已能满足大部分的场景的,另外Hook AMS需要的知识储备是要多得多的,拿我来说吧,看DroidPlugin的源码比看Small的源码困难太多了。。

这些都是要大家自己斟酌的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容