我们可以利用DexClassLoader来实现动态加载dex文件,而很多资料也只是对于DexClassLoader的使用进行了介绍,没有深入讲解dex的动态加载机制,我们就借助于Android4.4的源码来探索。先从一个简单的动态加载dex文件开始 具体实现细节可以参考这篇文章AndroidDex数据动态加载技术
Android4.4的源码在百度网盘分享: Android 4.4源码下载
先是我们要封装到text.jar文件中的很简单的调用函数,只是简单的产生Toast:
/* * 对外接口 */ public interface Iinterface { public void call(); public String getData(); }
public class IClass implements Iinterface{ private Context context; public IClass(Context context){ super(); this.context = context; } //@Override public void call() { // TODO Auto-generated method stub Toast.makeText(context, "call method", 0).show(); } //@Override public String getData() { // TODO Auto-generated method stub return "Hello ,I am from IClass"; } }
在MainActivity中只是解压test.jar文件,然后通过DexClassLoader类来加载dex文件,最后通过反射调用相关方法:
public class FileUtile { //MainActivity "testdex.jar", "testdex.jar" public static void CopyAssertJarToFile(Context context, String filename, String des) { try { //返回 File ,获取外部存储目录即 SDCard //path "/mnt/sdcard/testdex.jar" //File.separator Windows \ linux / File file = new File(Environment.getExternalStorageDirectory().getPath() + File.separator + des); if (file.exists()) { return; } //取得资源文件的输入流 InputStream inputStream = context.getAssets().open(filename); file.createNewFile(); //创建"/mnt/sdcard/testdex.jar" 文件 FileOutputStream fileOutputStream = new FileOutputStream(file); byte buffer[] = new byte[1024]; int len = 0; while ((len = inputStream.read(buffer)) != 0) { fileOutputStream.write(buffer, 0, len); } inputStream.close(); fileOutputStream.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FileUtile.CopyAssertJarToFile(this, "testdex.jar", "testdex.jar"); /*拷贝*/ File file = new File(Environment.getExternalStorageDirectory() .toString() + File.separator + "testdex.jar"); final File optimizedDexOutputPath = getDir("temp", Context.MODE_PRIVATE); /* * Parameters dexPath 需要装载的APK或者Jar文件的路径。包含多个路径用File.pathSeparator间隔开,在Android上默认是 ":" optimizedDirectory 优化后的dex文件存放目录,不能为null libraryPath 目标类中使用的C/C++库的列表,每个目录用File.pathSeparator间隔开; 可以为 null parent 该类装载器的父装载器,一般用当前执行类的装载器 */ DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), optimizedDexOutputPath.getAbsolutePath(), null, getClassLoader()); try { Class<?> iclass = classLoader.loadClass("com.demo.dex.IClass"); Constructor<?> istructor = iclass.getConstructor(Context.class); //利用反射原理去调用 Method method = iclass.getMethod("call", null); String data = (String) method.invoke(istructor.newInstance(this), null); //System.out.println(data); Log.d("CCDebug",data); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
我们从DexClassLoaderl类开始分析:
在\libcore\dalvik\src\main\java\dalvik\system\ DexClassLoader.java文件下
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
非常简单的DexClassLoader的构造函数,只是调用了父类BaseDexClassLoader的构造函数,在同一目录下的BaseDexClassLoader.java的源码:
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }
同样的,也是很简单的调用父类ClassLoader的构造函数,然后生成一个DexPathList对象,在同一目录下的DexPathList.java文件中:
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { //省略参数校验以及异常处理的代码 this.definingContext = definingContext; …… this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); …… this.nativeLibraryDirectories = splitLibraryPath(libraryPath); }
我们继续阅读DexPathList.java文件中makeDexElements 的关键代码:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) { // …… for (File file : files) { File zip = null; DexFile dex = null; String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { //.dex文件 // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ex) { System.logE("Unable to load dex file: " + file, ex); } } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) { //.apk .jar .zip文件 zip = file; try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException suppressed) { suppressedExceptions.add(suppressed); } } else 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 { System.logW("Unknown file type for: " + file); } } //…… return elements.toArray(new Element[elements.size()]); }
DexPathList.java文件中:
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException { if (optimizedDirectory == null) { return new DexFile(file); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0); } } //生成odex的目录 private static String optimizedPathFor(File path, File optimizedDirectory) { String fileName = path.getName(); if (!fileName.endsWith(DEX_SUFFIX)) { int lastDot = fileName.lastIndexOf("."); if (lastDot < 0) { fileName += DEX_SUFFIX; } else { StringBuilder sb = new StringBuilder(lastDot + 4); sb.append(fileName, 0, lastDot); sb.append(DEX_SUFFIX); fileName = sb.toString(); } } File result = new File(optimizedDirectory, fileName); return result.getPath(); }
optimizedPathFor主要是对文件的后缀进行修正,如果没有后缀名,就在末尾加上.dex,如果文件结尾不是.dex,就将后缀替换为.dex,然后创建我们的.dex文件,然后返回我们创建的.dex文件的路径,继续执行DexFile.loadDex() 函数:
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException { return new DexFile(sourcePathName, outputPathName, flags); }
这里直接返回了一个DexFile对象,下面来看看这个类的构造函数:
//sourceName 就是我们要加载的自己的.jar文件路径 // outputName 在optimizedPathFor() 函数中修正的加载.dex的路径 private DexFile(String sourceName, String outputName, int flags) throws IOException { if (outputName != null) { try { String parent = new File(outputName).getParent(); /* ??????*/ if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) { throw new IllegalArgumentException("Optimized data directory " + parent + " is not owned by the current user. Shared storage cannot protect" + " your application from code injection attacks."); } } catch (ErrnoException ignored) { // assume we‘ll fail with a more contextual error later } } //我们的重点就是在openDexFile()函数上 mCookie = openDexFile(sourceName, outputName, flags); mFileName = sourceName; guard.open("close"); //System.out.println("DEX FILE cookie is " + mCookie); }
openDexFile函数的返回值是一个整型,保存在mCookie中,文件名保存在mFileName中
private static int openDexFile(String sourceName, String outputName, int flags) throws IOException { return openDexFileNative(new File(sourceName).getCanonicalPath(), (outputName == null) ? null : new File(outputName).getCanonicalPath(), flags); }
在openDexFile()中只是调用了openDexFileNative () 继续跟入在\ dalvik\v m\nat ive\dalvik _sys tem_DexFile.cpp文件中的openDexFileNative() 函数,接下重点就在这个函数:
static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult) { //args[0]: sourceName java层传入的 //args[1]: outputName StringObject* sourceNameObj = (StringObject*) args[0]; StringObject* outputNameObj = (StringObject*) args[1]; DexOrJar* pDexOrJar = NULL; JarFile* pJarFile; RawDexFile* pRawDexFile; //DexOrJar* JarFile* RawDexFile* 目录 char* sourceName; char* outputName; //…… sourceName = dvmCreateCstrFromString(sourceNameObj); if (outputNameObj != NULL) outputName = dvmCreateCstrFromString(outputNameObj); else outputName = NULL; /*判断要加载的dex是否为系统中的dex文件 * gDvm ??? */ if (dvmClassPathContains(gDvm.bootClassPath, sourceName)) { ALOGW("Refusing to reopen boot DEX ‘%s‘", sourceName); dvmThrowIOException( "Re-opening BOOTCLASSPATH DEX files is not allowed"); free(sourceName); free(outputName); RETURN_VOID(); } /* * Try to open it directly as a DEX if the name ends with ".dex". * If that fails (or isn‘t tried in the first place), try it as a * Zip with a "classes.dex" inside. */ //判断sourcename扩展名是否是.dex if (hasDexExtension(sourceName) && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) { ALOGV("Opening DEX file ‘%s‘ (DEX)", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = true; pDexOrJar->pRawDexFile = pRawDexFile; pDexOrJar->pDexMemory = NULL; //.jar文件 } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) { ALOGV("Opening DEX file ‘%s‘ (Jar)", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = false; pDexOrJar->pJarFile = pJarFile; pDexOrJar->pDexMemory = NULL; } else { //都不满足,抛出异常 ALOGV("Unable to open DEX file ‘%s‘", sourceName); dvmThrowIOException("unable to open DEX file"); } if (pDexOrJar != NULL) { pDexOrJar->fileName = sourceName; //把pDexOr这个结构体中的内容加到gDvm中的userDexFile结构的hash表中,便于Dalvik以后的查找 addToDexFileTable(pDexOrJar); } else { free(sourceName); } free(outputName); RETURN_PTR(pDexOrJar); }
接下来再看对.dex文件的处理函数dvmRawDexFileOpen 在dalvik\vm\RawDexFile.cpp文件中:
/* See documentation comment in header. */ int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName, RawDexFile** ppRawDexFile, bool isBootstrap) { DvmDex* pDvmDex = NULL; char* cachedName = NULL; int result = -1; int dexFd = -1; int optFd = -1; u4 modTime = 0; u4 adler32 = 0; size_t fileSize = 0; bool newFile = false; bool locked = false; dexFd = open(fileName, O_RDONLY); //打开dex文件 if (dexFd < 0) goto bail; /* If we fork/exec into dexopt, don‘t let it inherit the open fd. */ dvmSetCloseOnExec(dexFd);//dexfd不继承 //校验dex文件的标志,将第8字节开始的4个字节赋值给adler32。 if (verifyMagicAndGetAdler32(dexFd, &adler32) < 0) { ALOGE("Error with header for %s", fileName); goto bail; } //得到dex文件的大小和修改时间,保存在modTime和filesize中 if (getModTimeAndSize(dexFd, &modTime, &fileSize) < 0) { ALOGE("Error with stat for %s", fileName); goto bail; } //odexOutputName就是odex文件名,如果odexOutputName为空,则自动生成一个。 if (odexOutputName == NULL) { cachedName = dexOptGenerateCacheFileName(fileName, NULL); if (cachedName == NULL) goto bail; } else { cachedName = strdup(odexOutputName); } //主要是验证缓存文件名的正确性,之后将dexOptHeader结构写入fd中 optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime, adler32, isBootstrap, &newFile, /*createIfMissing=*/true); locked = true; if (newFile) { u8 startWhen, copyWhen, endWhen; bool result; off_t dexOffset; dexOffset = lseek(optFd, 0, SEEK_CUR); //文件指针的位置 result = (dexOffset > 0); if (result) { startWhen = dvmGetRelativeTimeUsec(); //将dex文件中的内容拷贝到当前odex文件,也就是dexOffset开始 result = copyFileToFile(optFd, dexFd, fileSize) == 0; copyWhen = dvmGetRelativeTimeUsec(); } if (result) { //优化odex文件 result = dvmOptimizeDexFile(optFd, dexOffset, fileSize, fileName, modTime, adler32, isBootstrap); } } /* * Map the cached version. This immediately rewinds the fd, so it * doesn‘t have to be seeked anywhere in particular. */ //将odex文件映射到内存空间(mmap),并用mprotect将属性置为只读属性,并将映射的dex结构放在pDvmDex数据结构中,具体代码在下面。 if (dvmDexFileOpenFromFd(optFd, &pDvmDex) != 0) { ALOGI("Unable to map cached %s", fileName); goto bail; } …… }
//Dalvik/vm/RewDexFile.cpp static int verifyMagicAndGetAdler32(int fd, u4 *adler32) { u1 headerStart[12]; ssize_t amt = read(fd, headerStart, sizeof(headerStart)); if (amt < 0) { ALOGE("Unable to read header: %s", strerror(errno)); return -1; } if (amt != sizeof(headerStart)) { ALOGE("Unable to read full header (only got %d bytes)", (int) amt); return -1; } if (!dexHasValidMagic((DexHeader*) (void*) headerStart)) { return -1; } *adler32 = (u4) headerStart[8] | (((u4) headerStart[9]) << 8) | (((u4) headerStart[10]) << 16) | (((u4) headerStart[11]) << 24); return 0; }
//dalvik\vm\DvmDex.cpp /* * Given an open optimized DEX file, map it into read-only shared memory and * parse the contents. * * Returns nonzero on error. */ 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) { ALOGE("lseek rewind failed"); goto bail; } //mmap映射fd文件,就是我们之前的odex文件 if (sysMapFileInShmemWritableReadOnly(fd, &memMap) != 0) { ALOGE("Unable to map file"); goto bail; } pDexFile = dexFileParse((u1*)memMap.addr, memMap.length, parseFlags); if (pDexFile == NULL) { ALOGE("DEX parse failed"); 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 */ //将映射odex文件的内存拷贝到DvmDex的结构中 sysCopyMap(&pDvmDex->memMap, &memMap); pDvmDex->isMappedReadOnly = true; *ppDvmDex = pDvmDex; result = 0; bail: return result; } /*dalvik\libdex\SysUtil.cpp */ int sysMapFileInShmemWritableReadOnly(int fd, MemMapping* pMap) { 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 (mprotect(memPtr, length, PROT_READ) < 0) { //……. } pMap->baseAddr = pMap->addr = memPtr; pMap->baseLength = pMap->length = length; return 0; //…… }
下面在分析文件后缀不是.dex的情况:
/*如果不是.dex文件*/ 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; //打开.jar文件并映射,内存结构放在ZipArchive中,之后将具体分析的代码 if (dexZipOpenArchive(fileName, &archive) != 0) goto bail; archiveOpen = true; dvmSetCloseOnExec(dexZipGetArchiveFd(&archive)); //不继承 // openAlternateSuffix函数将fileName的后缀名改为”.odex”,例如 //”Hello.jar”--?”Hello.odex”,然后调用open()”打开”Hello.odex文件 //如果成功返回”Hello.odex”的文件描述符 fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName); if (fd >= 0) { ALOGV("Using alternate file (odex) for %s ...", fileName); //…检验optHeader if (!dvmCheckOptHeaderAndDependencies(fd, false, 0, 0, true, true)) { //…… goto tryArchive; } } else { ZipEntry entry; tryArchive: /* * Pre-created .odex absent or stale. Look inside the jar for a * "classes.dex". */ // static const char* kDexInJarName = "classes.dex"; /* 在dexZipFindEntry函数中,对kDexInJarName也就是”class.dex”进行hash运算,找到”class.dex”在archive结构中的表项 */ entry = dexZipFindEntry(&archive, kDexInJarName); if (entry != NULL) { bool newFile = false; //如果odex缓存路径为空,则自动生成一个路径 if (odexOutputName == NULL) { cachedName = dexOptGenerateCacheFileName(fileName, kDexInJarName); if (cachedName == NULL) goto bail; } else { cachedName = strdup(odexOutputName); } //创建cachedName对应的文件 (.odex) fd = dvmOpenCachedDexFile(fileName, cachedName, dexGetZipEntryModTime(&archive, entry), dexGetZipEntryCrc32(&archive, entry), //…… locked = true; //…… if (newFile) { //成功创建.odex文件 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) { //优化dex文件-?.odex result = dvmOptimizeDexFile(fd, dexOffset, dexGetZipEntryUncompLen(&archive, entry), fileName, dexGetZipEntryModTime(&archive, entry), dexGetZipEntryCrc32(&archive, entry), isBootstrap); } //已经得到了.odex文件,下面的流程就和.dex文件一样了。 //映射.odex文件, if (dvmDexFileOpenFromFd(fd, &pDvmDex) != 0) //………… return result; }
//\dalvik\libdex\SysUtil.cpp int dexZipOpenArchive(const char* fileName, ZipArchive* pArchive) { int fd, err; ……. memset(pArchive, 0, sizeof(ZipArchive)); //打开文件 fd = open(fileName, O_RDONLY | O_BINARY, 0); …… return dexZipPrepArchive(fd, fileName, pArchive); } int dexZipPrepArchive(int fd, const char* debugFileName, ZipArchive* pArchive) { int result = -1; memset(pArchive, 0, sizeof(*pArchive)); pArchive->mFd = fd; //Zip的文件描述符 if (mapCentralDirectory(fd, debugFileName, pArchive) != 0) goto bail; if (parseZipArchive(pArchive) != 0) { goto bail; } /* success */ result = 0; bail: if (result != 0) dexZipCloseArchive(pArchive); //失败释放pArchive结构 return result; } static int mapCentralDirectory(int fd, const char* debugFileName, ZipArchive* pArchive) { /* * Get and test file length. */ //检验文件长度的有效性 off64_t fileLength = lseek64(fd, 0, SEEK_END); if (fileLength < kEOCDLen) { return -1; } size_t readAmount = kMaxEOCDSearch; if (fileLength < off_t(readAmount)) readAmount = fileLength; u1* scanBuf = (u1*) malloc(readAmount); if (scanBuf == NULL) { return -1; } int result = mapCentralDirectory0(fd, debugFileName, pArchive, fileLength, readAmount, scanBuf); free(scanBuf); return result; } tatic int mapCentralDirectory0(int fd, const char* debugFileName, ZipArchive* pArchive, off64_t fileLength, size_t readAmount, u1* scanBuf) { /* * Make sure this is a Zip archive. */ //校验文件是否合法的Zip文件 //…… //偏移16的地方 //偏移12 if (sysMapFileSegmentInShmem(fd, centralDirOffset, centralDirSize, &pArchive->mDirectoryMap) != 0) { ALOGW("Zip: cd map failed"); return -1; } pArchive->mNumEntries = numEntries; pArchive->mDirectoryOffset = centralDirOffset; return 0; } int sysMapFileSegmentInShmem(int fd, off_t start, size_t length, MemMapping* pMap) { size_t actualLength; off_t actualStart; int adjust; void* memPtr; assert(pMap != NULL); /* adjust to be page-aligned */ adjust = start % SYSTEM_PAGE_SIZE; actualStart = start - adjust; actualLength = length + adjust; //映射 memPtr = mmap(NULL, actualLength, PROT_READ, MAP_FILE | MAP_SHARED, fd, actualStart); // ……. pMap->baseAddr = memPtr; pMap->baseLength = actualLength; pMap->addr = (char*)memPtr + adjust; pMap->length = length; return 0; }
ZipArchive的结构体如下:
struct ZipArchive { /* open Zip archive */ int mFd; //打开的zip文件 /* mapped central directory area */ off_t mDirectoryOffset; MemMapping mDirectoryMap; //映射内存的结构 /* number of entries in the Zip archive */ int mNumEntries; // int mHashTableSize; //名字hash表的大小 ZipHashEntry* mHashTable; //hash表的表项, }; struct ZipHashEntry { const char* name; unsigned short nameLen; };
我们可以简要总结下整个的加载流程,首先是对文件名的修正,后缀名置为”.dex”作为输出文件,然后生个一个DexPathList对象函数直接返回一个DexPathList对象,
在DexPathList的构造函数中调用makeDexElements()函数,在makeDexElement()函数中调用loadDexFile()开始对.dex或者是.jar .zip .apk文件进行处理,
跟入loadDexFile()函数中,会发现里面做的工作很简单,调用optimizedPathFor()函数对optimizedDiretcory路径进行修正。
之后才真正通过DexFile.loadDex()开始加载文件中的数据,其中的加载也只是返回一个DexFile对象。
在DexFile类的构造函数中,重点便放在了其调用的openDexFile()函数,在openDexFile()中调用了openDexFileNative()真正进入native层,
在openDexFileNative()的真正实现中,对于后缀名为.dex的文件或者其他文件(.jar .apk .zip)分开进行处理:
.dex文件调用dvmRawDexFileOpen();
其他文件调用dvmJarFileOpen()。
在dvmRawDexFileOpen()函数中,检验dex文件的标志,检验odex文件的缓存名称,之后将dex文件拷贝到odex文件中,并对odex进行优化
调用dvmDexFileOpenFromFd()对优化后的odex文件进行映射,通过mprotect置为"只读"属性并将映射的内存结构保存在DvmDex*结构中。
dvmJarFileOpen()先对文件进行映射,结构保存在ZipArchive中,然后再尝试以文件名作为dex文件名来“打开”文件,
如果失败,则调用dexZipFindEntry在ZipArchive的名称hash表中找名为"class.dex"的文件,然后创建odex文件,下面就和
dvmRawDexFileOpen()一样了,就是对dex文件进行优化和映射。
也只是分析了一个大概流程,还有很多有待之后进行深入。而这里对于阅读Android源码,有了新的体会,首先是工具上,我之前一直是用Source InSight 但是对于一些函数的实现,找起来却是不太方便,因为必须要将函数实现的文件导入到工程中,而用VS来阅读源码,利用Ctrl+Shift+F的功能,在Android源码目录下搜索更为方便,然后可以在Source InSight中进行导入,阅读。其次不得不说阅读源码真的是一个比较痛苦的过程,但真的学习下来,收获还是很大的。