ClassLoader--02基于Android5.0的openDexFileNative

相关源码地址:
http://aospxref.com/android-5.0.1_r1/xref/art/runtime/native/dalvik_system_DexFile.cc
http://aospxref.com/android-5.0.1_r1/xref/art/runtime/class_linker.cc
http://aospxref.com/android-5.0.1_r1/xref/art/runtime/dex_file.cc

分析涉及到几个主要的方法:

1、dalvik_system_DexFile.DexFile_openDexFileNative
2、class_linker.OpenDexFilesFromOat
3、class_linker.FindOpenedOatDexFile
4、class_linker.LoadMultiDexFilesFromOatFile
5、class_linker.CreateOatFileForDexLocation
6、oat_file.OpenDexFile
7、dex_file.Open
8、dex_file.OpenZip
9、dex_file.OpenFromZip

通过对Java层的源码分析, 其实并没有得到很有用的信息, 针对DexClassLoader与PathClassLoader的区别还是比较模糊的, 所以这篇打算从native层进行分析, 尝试搞清楚DexClassLoader与PathClassLoader区别的本质.

一、java层

1.1 PathClassLoader与DexClassLoader初始化
// 1. 注意PathClassLoader与DexClassLoader不同点仅在于初始化时传给BaseDexClassLoader时是否支持optimizedDirectory;
// 2. 正如DexClassLoader的类注释描述, DexClassLoader支持加载外部jar、apk中的dex中的类, 这个特殊点正是因为optimizedDirectory;
public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
    super(dexPath, null, libraryPath, parent);
}
// 一个类加载器,用于加载包含{@code classes.dex}条目的{@code .jar}和{@code .apk}文件中的类。
// 这可用于执行未作为应用程序的一部分安装的代码。
// 此类加载器需要一个应用程序专用的可写目录来缓存优化的类。使用{@code Context.getCodeCacheDir()}来创建
// 这样的目录:{@ code File dexOutputDir = context.getCodeCacheDir();}
// 不要在外部存储上缓存优化的类, 外部存储不提供保护应用程序免受代码注入攻击所必需的访问控制。
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
}
1.2 DexPathList初始化
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
    this.definingContext = definingContext;
    // splitDexPath不分析, 就是常规切割, 继续分析makeDexElements;
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
    this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
1.3 DexPathList.makeDexElements
private static final String DEX_SUFFIX = ".dex";
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) {
    ArrayList<Element> elements = new ArrayList<Element>();
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();
        if (file.isDirectory()) {
            // We support directories for looking up resources.
            // This is only useful for running libcore tests.
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()){
            // 这里一定要注意哈, 当前的file可能是.dex、 .jar、 .apk三种类型;
            if (name.endsWith(DEX_SUFFIX)) {
                // 当前文件是.dex文件
                // Raw dex file (not inside a zip/jar).
                dex = loadDexFile(file, optimizedDirectory);
            } else {// 当前文件是.jar或者是.apk文件
                zip = file;
                dex = loadDexFile(file, optimizedDirectory);
            }
        } 
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}
1.4 DexPathList.loadDexFile
private static DexFile loadDexFile(File file, File optimizedDirectory) {
    if (optimizedDirectory == null) {
        // 如果optimizedDirectory为null, 则直接返回DexFile();
        return new DexFile(file);
    } else {
        // optimizedPath = new File(optimizedDirectory, file.getName()).getPath();
        // 注意optimizedPath是指优化后的dex文件存放的路径;
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

private static String optimizedPathFor(File path, File optimizedDirectory) {
    // 我们不想使用“.odex”,因为构建系统将其用于与仅资源jar文件配对的文件。如果VM可以假设匹配的jar中没有classes.dex,
    // 则不需要打开jar来检查更新的依赖项,从而在启动时提供轻微的性能提升。这里使用“.dex”匹配对/ data / dalvik-cache
    // 中文件的使用。
    String fileName = path.getName();
    File result = new File(optimizedDirectory, fileName);
    return result.getPath();
}
1.5 DexFile.loadDex
// loadDex最终也是调用了DexFile();
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) {
    // 我们可能想要缓存先前打开的DexFile对象。缓存将与close()同步.
    // 当应用程序决定多次打开它时,这将有助于我们避免多次映射相同的DEX.
    return new DexFile(sourcePathName, outputPathName, flags);
}
1.6 DexFile初始化
// 触发native层的加载;
private DexFile(String sourceName, String outputName, int flags) throws IOException {
    mCookie = openDexFile(sourceName, outputName, flags);
    mFileName = sourceName;
    guard.open("close");
}

private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
    // Use absolute paths to enable the use of relative paths when testing on host.
    return openDexFileNative(new File(sourceName).getAbsolutePath(),
                            (outputName == null) ? null : new File(outputName).getAbsolutePath(),
                            flags);
}
// sourceName: 对应原始jar/apk中的.dex;
// outputName: dex文件优化后存放的地方;
private static native long openDexFileNative(String sourceName, String outputName, int flags);

二、native层dex文件加载

2.1 dalvik_system_DexFile.DexFile_openDexFileNative
// javaSourceName: 源.jar/.apk中的.dex
// javaOutputName: 对应Java层的optimizedDirectory;
static jlong DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
    ScopedUtfChars sourceName(env, javaSourceName);

    NullableScopedUtfChars outputName(env, javaOutputName);
    ClassLinker* linker = Runtime::Current()->GetClassLinker();
    // 创建DexFile的集合, 用于存放获取的DexFile对象;
    std::unique_ptr<std::vector<const DexFile*>> dex_files(new std::vector<const DexFile*>());
    std::vector<std::string> error_msgs;
    // 从oat中加载优化后的.dex文件;
    bool success = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs, dex_files.get());
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(dex_files.release()));
}
2.2 class_linker.OpenDexFilesFromOat
// dex_location: 对应sourceName, 可能是.dex、 .jar、 .apk其中的一种;
// oat_location: 对应outputName, 对应Java层的optimizedDirectory;
bool ClassLinker::OpenDexFilesFromOat(const char* dex_location, const char* oat_location,
                                      std::vector<std::string>* error_msgs,
                                      std::vector<const DexFile*>* dex_files) {
    // 在进行dex优化之前, 需要做三件事:
    // (1) 内存缓存: 首先检查我们是否已经打开了对应的oat文件, 这里可以理解为内存缓存, 在内存中是否有这样的一份缓存;
    // (2) 磁盘缓存: 如果没有打开过对应的oat文件, 再次判断是否已经做过dex优化, 并且在磁盘中缓存了对应的oat文件,
    // (3) 如果磁盘中存在,              
    // 1) Check whether we have an open oat file.
    // This requires a dex checksum, use the "primary" one.
    uint32_t dex_location_checksum;
    uint32_t* dex_location_checksum_pointer = &dex_location_checksum;
    bool have_checksum = true;
    std::string checksum_error_msg;
    if (!DexFile::GetChecksum(dex_location, dex_location_checksum_pointer, &checksum_error_msg)) {
        // This happens for pre-opted files since the corresponding dex files are no longer on disk.
        dex_location_checksum_pointer = nullptr;
        have_checksum = false;
    }
    bool needs_registering = false;  
    // (1) 首先校验目标oat文件是否存在, 也就是说与javaSourceName和javaOutputName对应的目标oat文件
    //     是否已经被加载过, 如果被加载 过, 则直接从内存缓存中获取目OatFile对象.
    const OatFile::OatDexFile* oat_dex_file = FindOpenedOatDexFile(oat_location, dex_location,
                                                                   dex_location_checksum_pointer);
    // 尝试通过OatDexFile获取目标OatFile并为OatFile* open_oat_file赋值, 接下来以open_oat_file为
    // 线索进行分析.
    std::unique_ptr<const OatFile> open_oat_file(
        oat_dex_file != nullptr ? oat_dex_file->GetOatFile() : nullptr);  
    // 这里开始第二步, 如果没有打开过对应的oat文件, 又分两种情况, 一种是磁盘中有, 也就是进行过dex优化,
    // 另一种是磁盘中没有, 也就是没有进行过dex优化.
    if (open_oat_file.get() == nullptr) {
        if (oat_location != nullptr) {// 这里是第一种情况, 进行过dex优化操作.
            //... 
        } else {// 这里是第二种情况, 未进行过dex优化操作.
            //...
        }   
        // 标志位, 如果没有缓存过目标OatFile, 此时将标志位置为true, 后续创建OatFile对象之后
        // 根据该标志位确认是否需要进行缓存.
        needs_registering = true;
    }  
    // (3) 尝试从磁盘中找到目标OatFile对象.
    bool success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
                                                dex_location_checksum_pointer,
                                                false, error_msgs, dex_files);
    if (success) {
        const OatFile* oat_file = open_oat_file.release();  // Avoid deleting it.
        if (needs_registering) {
            // 将目标oat_file进行注册, 以便于下次使用
            RegisterOatFile(oat_file);
        }
        return oat_file->IsExecutable();
    } else {
        ...
    }
    // (4) 如果没有对应的oat文件或者不匹配, 则需要进行重新生成和加载.
    std::string cache_location;
    // oat_location对应java层的optimizedDirectory.
    if (oat_location == nullptr) {
        // Use the dalvik cache.
        const std::string dalvik_cache(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)));
        cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());
        // 如果oat_location = null, 我们使用android系统默认路径: /data/dalvik-cache为oat默认路径;
        oat_location = cache_location.c_str();
    }
    bool has_flock = true;

    if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {
        // 创建oat文件
        open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),
                                                        oat_location, error_msgs));
    }
    // 再次尝试从OatFile中获取目标DexFile
    success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
                                           dex_location_checksum_pointer,
                                           true, error_msgs, dex_files);
    if (success) {
        // 如果获取成功, 缓存该OatFile
        RegisterOatFile(open_oat_file.release());
        return true;
    } else {
        return false;
    }
}
2.3 class_linker.FindOpenedOatDexFile
// 从oat_files_查找是否存在对应的OatFile, 查找过程知道, OatFile缓存了oat_location;
// oat_location: 对应java层的optimizedDirectory
const OatFile::OatDexFile* ClassLinker::FindOpenedOatDexFile(const char* oat_location,
                                                             const char* dex_location,
                                                             const uint32_t* dex_location_checksum) {
    ReaderMutexLock mu(Thread::Current(), dex_lock_);
    for (const OatFile* oat_file : oat_files_) {
        DCHECK(oat_file != nullptr);
        if (oat_location != nullptr) {
            if (oat_file->GetLocation() != oat_location) {
                continue;
            }
        }
        // 如果oat_files_不存在与oat_location对应的OatFile, 是不会执行到这里的;
        // 如果存在与oat_location对应的OatFile, 根据OatFile获取oat_dex_file;
        const OatFile::OatDexFile* oat_dex_file = oat_file->GetOatDexFile(dex_location,
                                                                          dex_location_checksum,
                                                                          false);
        if (oat_dex_file != nullptr) {
            return oat_dex_file;
        }
    }
    return nullptr;
}

如果存在已经打开的oat文件, 这里会遍历所有打开的oat文件, 查找目标oat_file, 如果存在, 则返回该OatDexFile对象.
这里还有很关键的一个点: 就是我们在加载Class时是从优化后的Dex中加载, 源码这里的顺序是先判断是否存在目标OatDexFile, 而OatDexFile与oat_location也就是java层的optimizedDirectory是一一对应的关系, 再结合模块<2.2L59~L65>对oat_location的赋值, 大致明白DexClassLoader与PathClassLoader的本质区别, DexClassLoader不支持传入optimizedDirectory, 所以被优化后的dex文件都存放在固定目录下, 如果使用DexClassLoader加载也只能加载固定目录下的dex文件, 而PathClassLoader支持optimizedDirectory, 被优化后的dex文件存放在optimizedDirectory路径下, 所以支持加载外部dex文件.

2.4 class_linker.LoadMultiDexFilesFromOatFile
static bool LoadMultiDexFilesFromOatFile(const OatFile* oat_file,
                                         const char* dex_location,
                                         const uint32_t* dex_location_checksum,
                                         bool generated,
                                         std::vector<std::string>* error_msgs,
                                         std::vector<const DexFile*>* dex_files) {
    size_t old_size = dex_files->size();  // To rollback on error.
    bool success = true;
    for (size_t i = 0; success; ++i) {
        // 获取目标OatDexFile对象.
        const OatFile::OatDexFile* oat_dex_file = oat_file->GetOatDexFile(next_name, nullptr, false);
        if (oat_dex_file == nullptr) {
            break;  // Not found, done.
        }
        // Checksum test. Test must succeed when generated.
        success = !generated;
        if (next_location_checksum_pointer != nullptr) {
            success = next_location_checksum == oat_dex_file->GetDexFileLocationChecksum();
        }
        if (success) {
            // 获取目标DexFile对象
            const DexFile* dex_file = oat_dex_file->OpenDexFile(&error_msg);
            if (dex_file == nullptr) {
                success = false;
                error_msgs->push_back(error_msg);
            } else {
                // 如果目标DexFile存在, 则将该DexFile进行压栈操作, 在dex_files中缓存起来.
                dex_files->push_back(dex_file);
            }
        }
    }
    if (dex_files->size() == old_size) {
        success = false;  // We did not even find classes.dex
    }
    return success;
}

如果获取到目标DexFile, 将标志位置为true, 同时将DexFile进行压栈操作, 下次直接从栈获取即可, 如果没有获取到目标DexFile, 标志位置为false.

2.5 class_linker.CreateOatFileForDexLocation
const OatFile* ClassLinker::CreateOatFileForDexLocation(const char* dex_location,
                                                        int fd, const char* oat_location,
                                                        std::vector<std::string>* error_msgs) {
    // Generate the output oat file for the dex file
    VLOG(class_linker) << "Generating oat file " << oat_location << " for " << dex_location;
    std::string error_msg;
    // 创建.dex对应的oat文件, 如何创建的先不分析<TODO看完老罗的文章之后再做分析>
    if (!GenerateOatFile(dex_location, fd, oat_location, &error_msg)) {
        CHECK(!error_msg.empty());
        error_msgs->push_back(error_msg);
        return nullptr;
    }
    // 创建完成之后再打开该文件并创建OatFile实例;
    std::unique_ptr<OatFile> oat_file(OatFile::Open(oat_location, oat_location, nullptr,
                                            !Runtime::Current()->IsCompiler(),
                                            &error_msg));
    if (oat_file.get() == nullptr) {
        std::string compound_msg = StringPrintf("\nFailed to open generated oat file '%s': %s",
                                                oat_location, error_msg.c_str());
        error_msgs->push_back(compound_msg);
        return nullptr;
    }
    return oat_file.release();
}
2.6 oat_file.OpenDexFile
const DexFile* OatFile::OatDexFile::OpenDexFile(std::string* error_msg) const {
    return DexFile::Open(dex_file_pointer_, FileSize(), dex_file_location_,
                         dex_file_location_checksum_, error_msg);
}
2.7 dex_file.Open
bool DexFile::Open(const char* filename, const char* location, std::string* error_msg,
                   std::vector<const DexFile*>* dex_files) {
    uint32_t magic;
    ScopedFd fd(OpenAndReadMagic(filename, &magic, error_msg));
    // 如果filename是.zip格式, 即java层对应的.jar或者.apk;
    if (IsZipMagic(magic)) {
        // 从压缩包中获取.dex文件, 结合下文对OpenZip的分析可知, 将zip中的.dex文件放入dex_files中;
        return DexFile::OpenZip(fd.release(), location, error_msg, dex_files);
    }
    // 如果filename是.dex格式;
    if (IsDexMagic(magic)) {
        // 则直接加载dex文件;
        std::unique_ptr<const DexFile> dex_file(DexFile::OpenFile(fd.release(), location, true, error_msg));
        if (dex_file.get() != nullptr) {
            dex_files->push_back(dex_file.release());
            return true;
        } else {
            return false;
        }
    }
    *error_msg = StringPrintf("Expected valid zip or dex file: '%s'", filename);
    return false;
}
2.8 dex_file.OpenZip
bool DexFile::OpenZip(int fd, const std::string& location, std::string* error_msg,
                      std::vector<const  DexFile*>* dex_files) {
    std::unique_ptr<ZipArchive> zip_archive(ZipArchive::OpenFromFd(fd, location.c_str(), error_msg));
    return DexFile::OpenFromZip(*zip_archive, location, error_msg, dex_files);
}

2.9 dex_file.OpenFromZip
bool DexFile::OpenFromZip(const ZipArchive& zip_archive, const std::string& location,
                          std::string* error_msg, std::vector<const DexFile*>* dex_files) {
    ZipOpenErrorCode error_code;
    std::unique_ptr<const DexFile> dex_file(Open(zip_archive, kClassesDex, location, error_msg, &error_code));
    if (dex_file.get() == nullptr) {
        return false;
    } else {
        // Had at least classes.dex.
        dex_files->push_back(dex_file.release());
        // Now try some more.
        size_t i = 2;
        // We could try to avoid std::string allocations by working on a char array directly. As we
        // do not expect a lot of iterations, this seems too involved and brittle.
        while (i < 100) {
            std::string name = StringPrintf("classes%zu.dex", i);
            std::string fake_location = location + kMultiDexSeparator + name;
            std::unique_ptr<const DexFile> next_dex_file(Open(zip_archive, name.c_str(), fake_location,
                                                         error_msg, &error_code));
            if (next_dex_file.get() == nullptr) {
                if (error_code != ZipOpenErrorCode::kEntryNotFound) {
                    LOG(WARNING) << error_msg;
                }
                break;
            } else {
                // 遍历zip文件, 将.dex文件放入到dex_files集合中;
                dex_files->push_back(next_dex_file.release());
            }
            i++;
        }
        return true;
    }
}

如果filename(对应java层的dexPath)对应的文件是一个.zip/.jar文件, 此时会对该包里面所有的dex文件进行dex优化操作, 这个知识点在对Multidex进行分析时会涉及到(为什么5.0以后不需要使用Multidex).

2.10 class_linker.RegisterOatFile
const OatFile* ClassLinker::RegisterOatFile(const OatFile* oat_file) {
    WriterMutexLock mu(Thread::Current(), dex_lock_);
    if (kIsDebugBuild) {
        for (size_t i = 0; i < oat_files_.size(); ++i) {
            CHECK_NE(oat_file, oat_files_[i]) << oat_file->GetLocation();
        }
    }
    VLOG(class_linker) << "Registering " << oat_file->GetLocation();
        // 缓存OatFile对象;
    oat_files_.push_back(oat_file);
    return oat_file;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 141,558评论 1 298
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,739评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 93,327评论 0 211
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,752评论 0 174
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,452评论 1 252
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,617评论 1 171
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,286评论 2 267
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,083评论 0 165
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,839评论 6 227
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,413评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,186评论 2 213
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,506评论 1 223
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,171评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,049评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,417评论 3 202
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,588评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,942评论 0 163
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,392评论 2 228
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,499评论 2 229

推荐阅读更多精彩内容