0x00

在上一篇文章apk安装和优化原理,在最后我们分析了DexClassLoader和PathClassLoader的构造函数的不同。

PathClassLoader最后调用的是new DexFile(pathFile),而DexClassLoader调用的是DexFile.loadDex(dexPathList[i], outputName, 0)。

0x01

new DexFile(pathFile)相应的代码位于libcore\dalvik\src\main\java\dalvik\system\DexFile.java。

    public DexFile(String fileName) throws IOException {
String wantDex = System.getProperty("android.vm.dexfile", "false");
if (!wantDex.equals("true"))
throw new UnsupportedOperationException("No dex in this VM"); mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
//System.out.println("DEX FILE cookie is " + mCookie);
}

DexFile.loadDex(dexPathList[i], outputName, 0)相应的代码也位于libcore\dalvik\src\main\java\dalvik\system\DexFile.java。

    static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException { /*
* TODO: we may want to cache previously-opened DexFile objects.
* The cache would be synchronized with close(). This would help
* us avoid mapping the same DEX more than once when an app
* decided to open it multiple times. In practice this may not
* be a real issue.
*/
return new DexFile(sourcePathName, outputPathName, flags);
}
    private DexFile(String sourceName, String outputName, int flags)
throws IOException { String wantDex = System.getProperty("android.vm.dexfile", "false");
if (!wantDex.equals("true"))
throw new UnsupportedOperationException("No dex in this VM"); mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
//System.out.println("DEX FILE cookie is " + mCookie);
}

我们能够看到事实上两者终于调用的都是openDexFile,仅仅只是DexClassLoader指定了生成优化后的apk路径,而PathClassLoader则不须要,由于在安装阶段已经生成了/data/dalvik-cache/xxx@classes.dex。

0x02

我们继续分析openDexFile,这种方法一个JNI方法。

    native private static int openDexFile(String sourceName, String outputName,
int flags) throws IOException;

代码位于libcore\dalvik\src\main\java\dalvik\system\DexFile.java。

相应的native方法位于dalvik\vm\native\dalvik_system_DexFile.c。

const DalvikNativeMethod dvm_dalvik_system_DexFile[] = {
{ "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I",
Dalvik_dalvik_system_DexFile_openDexFile },
{ "closeDexFile", "(I)V",
Dalvik_dalvik_system_DexFile_closeDexFile },
{ "defineClass", "(Ljava/lang/String;Ljava/lang/ClassLoader;ILjava/security/ProtectionDomain;)Ljava/lang/Class;",
Dalvik_dalvik_system_DexFile_defineClass },
{ "getClassNameList", "(I)[Ljava/lang/String;",
Dalvik_dalvik_system_DexFile_getClassNameList },
{ "isDexOptNeeded", "(Ljava/lang/String;)Z",
Dalvik_dalvik_system_DexFile_isDexOptNeeded },
{ NULL, NULL, NULL },
};

我们看到openDexFile相应的是Dalvik_dalvik_system_DexFile_openDexFile方法。

static void Dalvik_dalvik_system_DexFile_openDexFile(const u4* args,
JValue* pResult)
{
......
if (dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) {
LOGV("Opening DEX file '%s' (DEX)\n", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = true;
pDexOrJar->pRawDexFile = pRawDexFile;
} else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) {
LOGV("Opening DEX file '%s' (Jar)\n", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = false;
pDexOrJar->pJarFile = pJarFile;
} else {
LOGV("Unable to open DEX file '%s'\n", sourceName);
dvmThrowException("Ljava/io/IOException;", "unable to open DEX file");
} ...... RETURN_PTR(pDexOrJar);
}

代码位于dalvik\vm\native\dalvik_system_DexFile.c。

这里调用dvmJarFileOpen,代码也位于dalvik\vm\JarFile.c中。

int dvmJarFileOpen(const char* fileName, const char* odexOutputName,
JarFile** ppJarFile, bool isBootstrap)
{
ZipArchive archive;
DvmDex* pDvmDex = NULL;
char* cachedName = NULL;
bool archiveOpen = false;
bool locked = false;
int fd = -1;
int result = -1; /* Even if we're not going to look at the archive, we need to
* open it so we can stuff it into ppJarFile.
*/
if (dexZipOpenArchive(fileName, &archive) != 0)
goto bail;
archiveOpen = true; /* If we fork/exec into dexopt, don't let it inherit the archive's fd.
*/
dvmSetCloseOnExec(dexZipGetArchiveFd(&archive)); /* First, look for a ".odex" alongside the jar file. It will
* have the same name/path except for the extension.
*/
fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName);
if (fd >= 0) {
.......
} else {
ZipEntry entry; tryArchive:
/*
* Pre-created .odex absent or stale. Look inside the jar for a
* "classes.dex".
*/
entry = dexZipFindEntry(&archive, kDexInJarName);
if (entry != NULL) {
bool newFile = false; /*
* We've found the one we want. See if there's an up-to-date copy
* in the cache.
*
* On return, "fd" will be seeked just past the "opt" header.
*
* If a stale .odex file is present and classes.dex exists in
* the archive, this will *not* return an fd pointing to the
* .odex file; the fd will point into dalvik-cache like any
* other jar.
*/
if (odexOutputName == NULL) {
cachedName = dexOptGenerateCacheFileName(fileName,
kDexInJarName);
if (cachedName == NULL)
goto bail;
} else {
cachedName = strdup(odexOutputName);
}
LOGV("dvmDexCacheStatus: Checking cache for %s (%s)\n",
fileName, cachedName);
fd = dvmOpenCachedDexFile(fileName, cachedName,
dexGetZipEntryModTime(&archive, entry),
dexGetZipEntryCrc32(&archive, entry),
isBootstrap, &newFile, /*createIfMissing=*/true);
if (fd < 0) {
LOGI("Unable to open or create cache for %s (%s)\n",
fileName, cachedName);
goto bail;
}
locked = true; /*
* If fd points to a new file (because there was no cached version,
* or the cached version was stale), generate the optimized DEX.
* The file descriptor returned is still locked, and is positioned
* just past the optimization header.
*/
if (newFile) {
u8 startWhen, extractWhen, endWhen;
bool result;
off_t dexOffset; dexOffset = lseek(fd, 0, SEEK_CUR);
result = (dexOffset > 0); if (result) {
startWhen = dvmGetRelativeTimeUsec();
result = dexZipExtractEntryToFile(&archive, entry, fd) == 0;
extractWhen = dvmGetRelativeTimeUsec();
}
if (result) {
result = dvmOptimizeDexFile(fd, dexOffset,
dexGetZipEntryUncompLen(&archive, entry),
fileName,
dexGetZipEntryModTime(&archive, entry),
dexGetZipEntryCrc32(&archive, entry),
isBootstrap);
} if (!result) {
LOGE("Unable to extract+optimize DEX from '%s'\n",
fileName);
goto bail;
} endWhen = dvmGetRelativeTimeUsec();
LOGD("DEX prep '%s': unzip in %dms, rewrite %dms\n",
fileName,
(int) (extractWhen - startWhen) / 1000,
(int) (endWhen - extractWhen) / 1000);
}
} else {
......
}
} /*
* Map the cached version. This immediately rewinds the fd, so it
* doesn't have to be seeked anywhere in particular.
*/
if (dvmDexFileOpenFromFd(fd, &pDvmDex) != 0) {
LOGI("Unable to map %s in %s\n", kDexInJarName, fileName);
goto bail;
}
......
LOGV("Successfully opened '%s' in '%s'\n", kDexInJarName, fileName); *ppJarFile = (JarFile*) calloc(1, sizeof(JarFile));
(*ppJarFile)->archive = archive;
(*ppJarFile)->cacheFileName = cachedName;
(*ppJarFile)->pDvmDex = pDvmDex;
cachedName = NULL; // don't free it below
result = 0; bail:
......
return result;
}

我们主要关注dvmOpenCachedDexFile方法。首先调用这种方法,尝试依据cachedName所指的优化文件名称在cache中查找并读取优化文件,假设没有。则要就对Dex进行优化。我们前面说过对于PathClassLoader,在安装阶段已经生成了/data/dalvik-cache/xxx@classes.dex,所以dvmOpenCachedDexFile方法返回的newFile为false。而对于DexClassLoader已经指明了要生成优化后的dex的路径。这里dvmOpenCachedDexFile方法返回的newFile为true。

对于PathClassLoader。newFile为false。所以不须要再一次优化dex。而对于DexClassLoader,newFile为true。所以调用dvmOptimizeDexFile来优化dex。dvmOptimizeDexFile代码位于           dalvik\vm\analysis\DexPrepare.c。也就是说对于DexClassLoader也仅仅优化一次。第二次就不会再优化了。优化还是比較耗时的。

bool dvmOptimizeDexFile(int fd, off_t dexOffset, long dexLength,
const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)
{
......
pid = fork();
if (pid == 0) {
static const char* kDexOptBin = "/bin/dexopt";
......
androidRoot = getenv("ANDROID_ROOT");
if (androidRoot == NULL) {
LOGW("ANDROID_ROOT not set, defaulting to /system\n");
androidRoot = "/system";
}
execFile = malloc(strlen(androidRoot) + strlen(kDexOptBin) + 1);
strcpy(execFile, androidRoot);
strcat(execFile, kDexOptBin); .....
if (kUseValgrind)
execv(kValgrinder, argv);
else
execv(execFile, argv); LOGE("execv '%s'%s failed: %s\n", execFile,
kUseValgrind ? " [valgrind]" : "", strerror(errno));
exit(1);
} else {
......
}
}

也是fork出一个子线程去运行/system/bin/dexopt。也就是OptMain.c中的main方法,往后的运行流程请參考apk安装和优化原理一文。

简单的脱壳程序就在dvmDexFileOpenPartial下断点,參考使用IDA Pro进行简单的脱壳。DexClassLoader载入dex首先要进行优化。优化的过程中会调用dvmDexFileOpenPartial。而dvmDexFileOpenPartial的dexAddr是dex在内存的基地址。dexLength是dex在内存中长度,具体请參考apk安装和优化原理一文。这样就能够在内存中把dex dump出来。

我们回到dvmJarFileOpen,继续运行dvmDexFileOpenFromFd,代码位于dalvik\vm\DvmDex.c。

int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex)
{
DvmDex* pDvmDex;
DexFile* pDexFile;
MemMapping memMap;
int parseFlags = kDexParseDefault;
int result = -1; if (gDvm.verifyDexChecksum)
parseFlags |= kDexParseVerifyChecksum; if (lseek(fd, 0, SEEK_SET) < 0) {
LOGE("lseek rewind failed\n");
goto bail;
} if (sysMapFileInShmemWritableReadOnly(fd, &memMap) != 0) {
LOGE("Unable to map file\n");
goto bail;
} pDexFile = dexFileParse(memMap.addr, memMap.length, parseFlags);
if (pDexFile == NULL) {
LOGE("DEX parse failed\n");
sysReleaseShmem(&memMap);
goto bail;
} pDvmDex = allocateAuxStructures(pDexFile);
if (pDvmDex == NULL) {
dexFileFree(pDexFile);
sysReleaseShmem(&memMap);
goto bail;
} /* tuck this into the DexFile so it gets released later */
sysCopyMap(&pDvmDex->memMap, &memMap);
*ppDvmDex = pDvmDex;
result = 0; bail:
return result;
}

fd为优化后的dex。对于PathClassLoader,位于/data/dalvik-cache/xxx@classes.dex。

对于DexClassLoader。就是在new DexClassLoader(String dexPath, String dexOutputDir, String libPath, ClassLoader parent)指定的dexOutputDir路径中。

在dvmDexFileOpenFromFd方法中首先调用sysMapFileInShmemWritableReadOnly方法把优化后的dex载入进内存。


int sysMapFileInShmemWritableReadOnly(int fd, MemMapping* pMap)
{
#ifdef HAVE_POSIX_FILEMAP
off_t start;
size_t length;
void* memPtr; assert(pMap != NULL); if (getFileStartAndLength(fd, &start, &length) < 0)
return -1; memPtr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_FILE | MAP_PRIVATE,
fd, start);
if (memPtr == MAP_FAILED) {
LOGW("mmap(%d, R/W, FILE|PRIVATE, %d, %d) failed: %s\n", (int) length,
fd, (int) start, strerror(errno));
return -1;
}
if (mprotect(memPtr, length, PROT_READ) < 0) {
/* this fails with EACCESS on FAT filesystems, e.g. /sdcard */
int err = errno;
LOGV("mprotect(%p, %d, PROT_READ) failed: %s\n",
memPtr, length, strerror(err));
LOGD("mprotect(RO) failed (%d), file will remain read-write\n", err);
} pMap->baseAddr = pMap->addr = memPtr;
pMap->baseLength = pMap->length = length; return 0;
#else
return sysFakeMapFile(fd, pMap);
#endif
}

代码位于dalvik/libdex/SysUtil.c。
    MemMapping结构体pMap中baseAddr和addr都指向dex映射到内存的首地址。

返回到dvmDexFileOpenFromFd,继续运行dexFileParse。代码位于dalvik\libdex\DexFile.c。

DexFile* dexFileParse(const u1* data, size_t length, int flags)
{
DexFile* pDexFile = NULL;
const DexHeader* pHeader;
const u1* magic;
int result = -1; if (length < sizeof(DexHeader)) {
LOGE("too short to be a valid .dex\n");
goto bail; /* bad file format */
} pDexFile = (DexFile*) malloc(sizeof(DexFile));
if (pDexFile == NULL)
goto bail; /* alloc failure */
memset(pDexFile, 0, sizeof(DexFile)); /*
* Peel off the optimized header.
*/
if (memcmp(data, DEX_OPT_MAGIC, 4) == 0) {
magic = data;
if (memcmp(magic+4, DEX_OPT_MAGIC_VERS, 4) != 0) {
LOGE("bad opt version (0x%02x %02x %02x %02x)\n",
magic[4], magic[5], magic[6], magic[7]);
goto bail;
} pDexFile->pOptHeader = (const DexOptHeader*) data;
LOGV("Good opt header, DEX offset is %d, flags=0x%02x\n",
pDexFile->pOptHeader->dexOffset, pDexFile->pOptHeader->flags); /* parse the optimized dex file tables */
if (!dexParseOptData(data, length, pDexFile))
goto bail;
data += pDexFile->pOptHeader->dexOffset;
length -= pDexFile->pOptHeader->dexOffset;
......
} dexFileSetupBasicPointers(pDexFile, data);
pHeader = pDexFile->pHeader; ......
result = 0; bail:
if (result != 0 && pDexFile != NULL) {
dexFileFree(pDexFile);
pDexFile = NULL;
}
return pDexFile;
}

这种方法主要是生成DexFile结构,我们首先来看DexFile结构是什么样子的?

typedef struct DexFile {
/* directly-mapped "opt" header */
const DexOptHeader* pOptHeader; /* pointers to directly-mapped structs and arrays in base DEX */
const DexHeader* pHeader;
const DexStringId* pStringIds;
const DexTypeId* pTypeIds;
const DexFieldId* pFieldIds;
const DexMethodId* pMethodIds;
const DexProtoId* pProtoIds;
const DexClassDef* pClassDefs;
const DexLink* pLinkData; /*
* These are mapped out of the "auxillary" section, and may not be
* included in the file.
*/
const DexClassLookup* pClassLookup;
const void* pRegisterMapPool; // RegisterMapClassPool /* points to start of DEX file data */
const u1* baseAddr; /* track memory overhead for auxillary structures */
int overhead; /* additional app-specific data structures associated with the DEX */
//void* auxData;
} DexFile;

代码位于dalvik\libdex\DexFile.h中。

dexFileParse这种方法首先为pDexFile->pOptHeader赋值,将优化文件头部与DexFile数据结构下的pOptHeader变量进行关联。

然后调用dexParseOptData函数对优化数据进行处理,其作用也是将各个优化数据与DexFile数据结构中的相应成员变量进行关联。这里关联的是pClassLookup和pRegisterMapPool。

最后调用dexFileSetupBasicPointers将Dex文件里其它各部分数据与DexFile数据结构建立完整的映射关系。代码位于dalvik\libdex\DexFile.c。

void dexFileSetupBasicPointers(DexFile* pDexFile, const u1* data) {
DexHeader *pHeader = (DexHeader*) data; pDexFile->baseAddr = data;
pDexFile->pHeader = pHeader;
pDexFile->pStringIds = (const DexStringId*) (data + pHeader->stringIdsOff);
pDexFile->pTypeIds = (const DexTypeId*) (data + pHeader->typeIdsOff);
pDexFile->pFieldIds = (const DexFieldId*) (data + pHeader->fieldIdsOff);
pDexFile->pMethodIds = (const DexMethodId*) (data + pHeader->methodIdsOff);
pDexFile->pProtoIds = (const DexProtoId*) (data + pHeader->protoIdsOff);
pDexFile->pClassDefs = (const DexClassDef*) (data + pHeader->classDefsOff);
pDexFile->pLinkData = (const DexLink*) (data + pHeader->linkOff);
}

重点说一下baseAddr指向dex文件的头部。

运行完dexFileParse生成了DexFile结构。返回到dvmDexFileOpenFromFd,继续运行allocateAuxStructures方法来生成DvmDex结构pDvmDex。代码位于dalvik\vm\DvmDex.c。

static DvmDex* allocateAuxStructures(DexFile* pDexFile)
{
DvmDex* pDvmDex;
const DexHeader* pHeader;
u4 stringCount, classCount, methodCount, fieldCount; pDvmDex = (DvmDex*) calloc(1, sizeof(DvmDex));
if (pDvmDex == NULL)
return NULL; pDvmDex->pDexFile = pDexFile;
pDvmDex->pHeader = pDexFile->pHeader; pHeader = pDvmDex->pHeader; stringCount = pHeader->stringIdsSize;
classCount = pHeader->typeIdsSize;
methodCount = pHeader->methodIdsSize;
fieldCount = pHeader->fieldIdsSize; pDvmDex->pResStrings = (struct StringObject**)
calloc(stringCount, sizeof(struct StringObject*)); pDvmDex->pResClasses = (struct ClassObject**)
calloc(classCount, sizeof(struct ClassObject*)); pDvmDex->pResMethods = (struct Method**)
calloc(methodCount, sizeof(struct Method*)); pDvmDex->pResFields = (struct Field**)
calloc(fieldCount, sizeof(struct Field*)); LOGV("+++ DEX %p: allocateAux %d+%d+%d+%d * 4 = %d bytes\n",
pDvmDex, stringCount, classCount, methodCount, fieldCount,
(stringCount + classCount + methodCount + fieldCount) * 4); pDvmDex->pInterfaceCache = dvmAllocAtomicCache(DEX_INTERFACE_CACHE_SIZE); if (pDvmDex->pResStrings == NULL ||
pDvmDex->pResClasses == NULL ||
pDvmDex->pResMethods == NULL ||
pDvmDex->pResFields == NULL ||
pDvmDex->pInterfaceCache == NULL)
{
LOGE("Alloc failure in allocateAuxStructures\n");
free(pDvmDex->pResStrings);
free(pDvmDex->pResClasses);
free(pDvmDex->pResMethods);
free(pDvmDex->pResFields);
free(pDvmDex);
return NULL;
} return pDvmDex; }

然后返回到dvmDexFileOpenFromFd,运行sysCopyMap把DvmDex结构pDvmDex的memMap附上值。最后返回DvmDex结构pDvmDex。
    

运行完dvmDexFileOpenFromFd,返回到dvmJarFileOpen。生成了一个JarFile结构体ppJarFile,并赋值例如以下:

    *ppJarFile = (JarFile*) calloc(1, sizeof(JarFile));
(*ppJarFile)->archive = archive;
(*ppJarFile)->cacheFileName = cachedName;
(*ppJarFile)->pDvmDex = pDvmDex;

在返回到Dalvik_dalvik_system_DexFile_openDexFile中。生成一个DexOrJar结构体pDexOrJar。并赋值例如以下:

        pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
pDexOrJar->isDex = false;
pDexOrJar->pJarFile = pJarFile;

然后RETURN_PTR(pDexOrJar)返回给Java层的openDexFile,并赋值给mCookie。例如以下:

mCookie = openDexFile(fileName, null, 0);

下次通过mCookie。我们就能一步一步找到DexFile结构体(DexOrJar->JarFile->DvmDex->DexFile)。

0x03

最后附上一张DexFile的结构图:

图中有一处错误,baseAddr和pHeader事实上都指向DexHeader。也就是dex文件头在内存中的虚拟地址。

0x03

那么PatchClassLoader对象是什么时候生成的呢?

Android加壳原理分析一文中,在handleBindApplication方法里须要获取ClassLoader,这个ClassLoader对象就是PathClassLoader对象。以下来看看是怎么形成的?

private final void handleBindApplication(AppBindData data) {
......
try {
java.lang.ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Unable to instantiate instrumentation "
+ data.instrumentationName + ": " + e.toString(), e);
}
......
}

代码位于frameworks\base\core\java\android\app\ActivityThread.java。

instrContext.getClassLoader(),实现例如以下,代码位于frameworks\base\core\java\android\app\ContextImpl.java。

    @Override
public ClassLoader getClassLoader() {
return mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}

mPackageInfo.getClassLoader()。实现例如以下。代码位于frameworks\base\core\java\android\app\LoadedApk.java。

    public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
} if (mIncludeCode && !mPackageName.equals("android")) {
String zip = mAppDir; ......
mClassLoader =
ApplicationLoaders.getDefault().getClassLoader(
zip, mLibDir, mBaseClassLoader);
initializeJavaContextClassLoader();
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}

ApplicationLoaders.getDefault().getClassLoader( zip, mLibDir, mBaseClassLoader),实现例如以下,代码位于frameworks\base\core\java\android\app\ApplicationLoaders.java中。

    public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{
/*
* This is the parent we use if they pass "null" in. In theory
* this should be the "system" class loader; in practice we
* don't use that and can happily (and more efficiently) use the
* bootstrap class loader.
*/
ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent(); synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
} /*
* If we're one step up from the base class loader, find
* something in our cache. Otherwise, we create a whole
* new ClassLoader for the zip archive.
*/
if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
} PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent); mLoaders.put(zip, pathClassloader);
return pathClassloader;
} return new PathClassLoader(zip, parent);
}
}

最后的时候,我们看到了PathClassLoader对象的生成。

最新文章

  1. Unity4升级Unity5后Image Effects问题
  2. poj1611(并查集)
  3. insert into select 与select into from -- sql 批量插入
  4. IIS 7.5 + asp.net MVC4 设置路由处理URL请求
  5. HDU 2546(01背包)
  6. BestCoder Round #2 1001 (简单处理)
  7. leetcode@ [316] Remove Duplicate Letters (Stack &amp; Greedy)
  8. OWC控件的使用
  9. Bootstrap的响应式,当文字超过div长度,换行问题的处理!
  10. Spring Boot 属性配置和使用
  11. iOS项目架构 小谈
  12. 图片与base64的互转
  13. 【原】Java学习笔记020 - 面向对象
  14. 判断PDF文件是否相同(通过二进制流判断)
  15. Linux启动流程和脚本服务-6
  16. PythonStudy——运算符优先级 Operator precedence
  17. malloc函数 链表
  18. 5分钟简述Spring中的DI与AOP
  19. oracle数据库静态监听配置示例
  20. idea使用maven打包jar包

热门文章

  1. 【转】 [Unity3D]手机3D游戏开发:场景切换与数据存储(PlayerPrefs 类的介绍与使用)
  2. ZOJ 3781 Paint the Grid Reloaded(BFS+缩点思想)
  3. http content-type总结
  4. zookeeper与Kafka集群搭建及python代码测试
  5. exit() 与 return() 的区别
  6. 对计算属性中get和set的理解
  7. redis批量删除脚本
  8. BZOJ【1639】: [Usaco2007 Mar]Monthly Expense 月度开支
  9. 远程连接linux和linux的网络配置
  10. C 语言 和 python 调用 .so 文件