内容简介:此文已由作者尹彬彬授权网易云社区发布。欢迎访问网易云社区,了解更多网易技术产品运营经验。
此文已由作者尹彬彬授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
0X0 前言
在Android系统中,当我们安装apk文件的时候,lib目录下的so文件会被解压到app的原生库目录,一般来说是放到/data/data/<package-name>/lib目录下,而根据系统和CPU架构的不同,其拷贝策略也是不一样的,在我们测试过程中发现不正确地配置了so文件,比如某些app使用第三方的so时,只配置了其中某一种CPU架构的so,可能会造成app在某些机型上的适配问题。所以这篇文章主要介绍一下在不同版本的Android系统中,安装apk时,PackageManagerService选择解压so库的策略,并给出一些so文件配置的建议。
0x1 Android4.0以前
当apk被安装时,执行路径虽然有差别,但最终要调用到的一个核心函数是copyApk,负责拷贝apk中的资源。
参考2.3.6的android源码,它的copyApk其内部函数一段选取原生库so逻辑:
public static int listPackageNativeBinariesLI(ZipFile zipFile, List> nativeFiles) throws ZipException, IOException { String cpuAbi = Build.CPU_ABI; int result = listPackageSharedLibsForAbiLI(zipFile, cpuAbi, nativeFiles); /* * Some architectures are capable of supporting several CPU ABIs * for example, 'armeabi-v7a' also supports 'armeabi' native code * this is indicated by the definition of the ro.product.cpu.abi2 * system property. * * only scan the package twice in case of ABI mismatch */ if (result == PACKAGE_INSTALL_NATIVE_ABI_MISMATCH) { final String cpuAbi2 = SystemProperties.get("ro.product.cpu.abi2", null); if (cpuAbi2 != null) { result = listPackageSharedLibsForAbiLI(zipFile, cpuAbi2, nativeFiles); } if (result == PACKAGE_INSTALL_NATIVE_ABI_MISMATCH) { Slog.w(TAG, "Native ABI mismatch from package file"); return PackageManager.INSTALL_FAILED_INVALID_APK; } if (result == PACKAGE_INSTALL_NATIVE_FOUND_LIBRARIES) { cpuAbi = cpuAbi2; } } /* * Debuggable packages may have gdbserver embedded, so add it to * the list to the list of items to be extracted (as lib/gdbserver) * into the application's native library directory later. */ if (result == PACKAGE_INSTALL_NATIVE_FOUND_LIBRARIES) { listPackageGdbServerLI(zipFile, cpuAbi, nativeFiles); } return PackageManager.INSTALL_SUCCEEDED; }复制代码
这段代码中的Build.CPU_ABI和“ro.product.cpu.abi2”分别为手机支持的主abi和次abi属性字符串,abi为手机支持的指令集所代表的字符串,比如armeabi-v7a、armeabi、x86、mips等,而主abi和次abi分别表示手机支持的第一指令集和第二指令集。代码首先调用listPackageSharedLibsForAbiLI来遍历主abi目录。当主abi目录不存在时,才会接着调用listPackageSharedLibsForAbiLI遍历次abi目录。
/* * Find all files of the form lib//lib.so in the .apk * and add them to a list to be installed later. * * NOTE: this method may throw an IOException if the library cannot * be copied to its final destination, e.g. if there isn't enough * room left on the data partition, or a ZipException if the package * file is malformed. */ private static int listPackageSharedLibsForAbiLI(ZipFile zipFile, String cpuAbi, List> libEntries) throws IOException, ZipException { final int cpuAbiLen = cpuAbi.length(); boolean hasNativeLibraries = false; boolean installedNativeLibraries = false; if (DEBUG_NATIVE) { Slog.d(TAG, "Checking " + zipFile.getName() + " for shared libraries of CPU ABI type " + cpuAbi); } Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); // skip directories if (entry.isDirectory()) { continue; } String entryName = entry.getName(); /* * Check that the entry looks like lib//lib.so * here, but don't check the ABI just yet. * * - must be sufficiently long * - must end with LIB_SUFFIX, i.e. ".so" * - must start with APK_LIB, i.e. "lib/" */ if (entryName.length() < MIN_ENTRY_LENGTH || !entryName.endsWith(LIB_SUFFIX) || !entryName.startsWith(APK_LIB)) { continue; } // file name must start with LIB_PREFIX, i.e. "lib" int lastSlash = entryName.lastIndexOf('/'); if (lastSlash < 0 || !entryName.regionMatches(lastSlash + 1, LIB_PREFIX, 0, LIB_PREFIX_LENGTH)) { continue; } hasNativeLibraries = true; // check the cpuAbi now, between lib/ and /lib.so if (lastSlash != APK_LIB_LENGTH + cpuAbiLen || !entryName.regionMatches(APK_LIB_LENGTH, cpuAbi, 0, cpuAbiLen)) continue; /* * Extract the library file name, ensure it doesn't contain * weird characters. we're guaranteed here that it doesn't contain * a directory separator though. */ String libFileName = entryName.substring(lastSlash+1); if (!FileUtils.isFilenameSafe(new File(libFileName))) { continue; } installedNativeLibraries = true; if (DEBUG_NATIVE) { Log.d(TAG, "Caching shared lib " + entry.getName()); } libEntries.add(Pair.create(entry, libFileName)); } if (!hasNativeLibraries) return PACKAGE_INSTALL_NATIVE_NO_LIBRARIES; if (!installedNativeLibraries) return PACKAGE_INSTALL_NATIVE_ABI_MISMATCH; return PACKAGE_INSTALL_NATIVE_FOUND_LIBRARIES; }复制代码
listPackageSharedLibsForAbiLI中判断当前遍历的apk中文件的entry名是否符合so命名的规范且包含相应abi字符串名。如果符合则规则则将so的entry名加入list,如果遍历失败或者规则不匹配则返回相应错误码。
拷贝so策略:
遍历apk中文件,当apk中lib目录下主abi子目录中有so文件存在时,则全部拷贝主abi子目录下的so;只有当主abi子目录下没有so文件的时候即PACKAGE_INSTALL_NATIVE_ABI_MISMATCH的情况,才会拷贝次ABI子目录下的so文件。
策略问题:
当so放置不当时,安装apk时会导致拷贝不全。比如apk的lib目录下存在armeabi/libx.so,armeabi/liby.so,armeabi-v7a/libx.so这3个so文件,那么在主ABI为armeabi-v7a且系统版本小于4.0的手机上,apk安装后,按照拷贝策略,只会拷贝主abi目录下的文件即armeabi-v7a/libx.so,当加载liby.so时就会报找不到so的异常。另外如果主abi目录不存在,这个策略会遍历2次apk,效率偏低。
0x2 Android 4.0-Android 4.0.3
参考4.0.3的android源码,同理,找到处理so拷贝的核心逻辑(native层):
static install_status_titerateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) { ScopedUtfChars filePath(env, javaFilePath); ScopedUtfChars cpuAbi(env, javaCpuAbi); ScopedUtfChars cpuAbi2(env, javaCpuAbi2); ZipFileRO zipFile; if (zipFile.open(filePath.c_str()) != NO_ERROR) { LOGI("Couldn't open APK %s\n", filePath.c_str()); return INSTALL_FAILED_INVALID_APK; } const int N = zipFile.getNumEntries(); char fileName[PATH_MAX]; for (int i = 0; i < N; i++) { const ZipEntryRO entry = zipFile.findEntryByIndex(i); if (entry == NULL) { continue; } // Make sure this entry has a filename. if (zipFile.getEntryFileName(entry, fileName, sizeof(fileName))) { continue; } // Make sure we're in the lib directory of the ZIP. if (strncmp(fileName, APK_LIB, APK_LIB_LEN)) { continue; } // Make sure the filename is at least to the minimum library name size. const size_t fileNameLen = strlen(fileName); static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN; if (fileNameLen < minLength) { continue; } const char* lastSlash = strrchr(fileName, '/'); LOG_ASSERT(lastSlash != NULL, "last slash was null somehow for %s\n", fileName); // Check to make sure the CPU ABI of this file is one we support. const char* cpuAbiOffset = fileName + APK_LIB_LEN; const size_t cpuAbiRegionSize = lastSlash - cpuAbiOffset; LOGV("Comparing ABIs %s and %s versus %s\n", cpuAbi.c_str(), cpuAbi2.c_str(), cpuAbiOffset); if (cpuAbi.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi.c_str(), cpuAbiRegionSize)) { LOGV("Using ABI %s\n", cpuAbi.c_str()); } else if (cpuAbi2.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi2.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi2.c_str(), cpuAbiRegionSize)) { LOGV("Using ABI %s\n", cpuAbi2.c_str()); } else { LOGV("abi didn't match anything: %s (end at %zd)\n", cpuAbiOffset, cpuAbiRegionSize); continue; } // If this is a .so file, check to see if we need to copy it. if ((!strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX, LIB_SUFFIX_LEN) && !strncmp(lastSlash, LIB_PREFIX, LIB_PREFIX_LEN) && isFilenameSafe(lastSlash + 1)) || !strncmp(lastSlash + 1, GDBSERVER, GDBSERVER_LEN)) { install_status_t ret = callFunc(env, callArg, &zipFile, entry, lastSlash + 1); if (ret != INSTALL_SUCCEEDED) { LOGV("Failure for entry %s", lastSlash + 1); return ret; } } } return INSTALL_SUCCEEDED; }复制代码
拷贝so策略:
遍历apk中所有文件,如果符合so文件的规则,且为主ABI目录或者次ABI目录下的so,就解压拷贝到相应目录。
策略问题:
存在同名so覆盖,比如一个app的armeabi和armeabi-v7a目录下都包含同名的so,那么就会发生覆盖现象,覆盖的先后顺序根据so文件对应ZipFileR0中的hash值而定,考虑这样一个例子,假设一个apk同时有armeabi/libx.so和armeabi-v7a/libx.so,安装到主ABI为armeabi-v7a的手机上,拷贝so时根据遍历顺序,存在一种可能即armeab-v7a/libx.so优先遍历并被拷贝,随后armeabi/libx.so被遍历拷贝,覆盖了前者。本来应该加载armeabi-v7a目录下的so,结果按照这个策略拷贝了armeabi目录下的so。
apk中文件entry的散列计算函数如下:
/* * Simple string hash function for non-null-terminated strings. *//*static*/ unsigned int ZipFileRO::computeHash(const char* str, int len) { unsigned int hash = 0; while (len--) hash = hash * 31 + *str++; return hash; }/* * Add a new entry to the hash table. */void ZipFileRO::addToHash(const char* str, int strLen, unsigned int hash) { int ent = hash & (mHashTableSize-1); /* * We over-allocate the table, so we're guaranteed to find an empty slot. */ while (mHashTable[ent].name != NULL) ent = (ent + 1) & (mHashTableSize-1); mHashTable[ent].name = str; mHashTable[ent].nameLen = strLen; }复制代码
0x3 Android 4.0.4以后
以4.1.2系统为例,遍历选择so逻辑如下:
static install_status_titerateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) { ScopedUtfChars filePath(env, javaFilePath); ScopedUtfChars cpuAbi(env, javaCpuAbi); ScopedUtfChars cpuAbi2(env, javaCpuAbi2); ZipFileRO zipFile; if (zipFile.open(filePath.c_str()) != NO_ERROR) { ALOGI("Couldn't open APK %s\n", filePath.c_str()); return INSTALL_FAILED_INVALID_APK; } const int N = zipFile.getNumEntries(); char fileName[PATH_MAX]; bool hasPrimaryAbi = false; for (int i = 0; i < N; i++) { const ZipEntryRO entry = zipFile.findEntryByIndex(i); if (entry == NULL) { continue; } // Make sure this entry has a filename. if (zipFile.getEntryFileName(entry, fileName, sizeof(fileName))) { continue; } // Make sure we're in the lib directory of the ZIP. if (strncmp(fileName, APK_LIB, APK_LIB_LEN)) { continue; } // Make sure the filename is at least to the minimum library name size. const size_t fileNameLen = strlen(fileName); static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN; if (fileNameLen < minLength) { continue; } const char* lastSlash = strrchr(fileName, '/'); ALOG_ASSERT(lastSlash != NULL, "last slash was null somehow for %s\n", fileName); // Check to make sure the CPU ABI of this file is one we support. const char* cpuAbiOffset = fileName + APK_LIB_LEN; const size_t cpuAbiRegionSize = lastSlash - cpuAbiOffset; ALOGV("Comparing ABIs %s and %s versus %s\n", cpuAbi.c_str(), cpuAbi2.c_str(), cpuAbiOffset); if (cpuAbi.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi.c_str(), cpuAbiRegionSize)) { ALOGV("Using primary ABI %s\n", cpuAbi.c_str()); hasPrimaryAbi = true; } else if (cpuAbi2.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi2.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi2.c_str(), cpuAbiRegionSize)) { /* * If this library matches both the primary and secondary ABIs, * only use the primary ABI. */ if (hasPrimaryAbi) { ALOGV("Already saw primary ABI, skipping secondary ABI %s\n", cpuAbi2.c_str()); continue; } else { ALOGV("Using secondary ABI %s\n", cpuAbi2.c_str()); } } else { ALOGV("abi didn't match anything: %s (end at %zd)\n", cpuAbiOffset, cpuAbiRegionSize); continue; } // If this is a .so file, check to see if we need to copy it. if ((!strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX, LIB_SUFFIX_LEN) && !strncmp(lastSlash, LIB_PREFIX, LIB_PREFIX_LEN) && isFilenameSafe(lastSlash + 1)) || !strncmp(lastSlash + 1, GDBSERVER, GDBSERVER_LEN)) { install_status_t ret = callFunc(env, callArg, &zipFile, entry, lastSlash + 1); if (ret != INSTALL_SUCCEEDED) { ALOGV("Failure for entry %s", lastSlash + 1); return ret; } } } return INSTALL_SUCCEEDED; }复制代码
拷贝so策略:
遍历apk中文件,当遍历到有主Abi目录的so时,拷贝并设置标记hasPrimaryAbi为真,以后遍历则只拷贝主Abi目录下的so,当标记为假的时候,如果遍历的so的entry名包含次abi字符串,则拷贝该so。
策略问题:
经过实际测试,so放置不当时,安装apk时存在so拷贝不全的情况。这个策略想解决的问题是在4.0~4.0.3系统中的so随意覆盖的问题,即如果有主abi目录的so则拷贝,如果主abi目录不存在这个so则拷贝次abi目录的so,但代码逻辑是根据ZipFileR0的遍历顺序来决定是否拷贝so,假设存在这样的apk,lib目录下存在armeabi/libx.so,armeabi/liby.so,armeabi-v7a/libx.so这三个so文件,且hash的顺序为armeabi-v7a/libx.so在armeabi/liby.so之前,则apk安装的时候liby.so根本不会被拷贝,因为按照拷贝策略,armeabi-v7a/libx.so会优先遍历到,由于它是主abi目录的so文件,所以标记被设置了,当遍历到armeabi/liby.so时,由于标记被设置为真,liby.so的拷贝就被忽略了,从而在加载liby.so的时候会报异常。
0x4 64位系统支持
Android在5.0之后支持64位ABI,以5.1.0系统为例:
public static int copyNativeBinariesWithOverride(Handle handle, File libraryRoot, String abiOverride) { try { if (handle.multiArch) { // Warn if we've set an abiOverride for multi-lib packages.. // By definition, we need to copy both 32 and 64 bit libraries for // such packages. if (abiOverride != null && !CLEAR_ABI_OVERRIDE.equals(abiOverride)) { Slog.w(TAG, "Ignoring abiOverride for multi arch application."); } int copyRet = PackageManager.NO_NATIVE_LIBRARIES; if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot, Build.SUPPORTED_32_BIT_ABIS, true /* use isa specific subdirs */); if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES && copyRet != PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS) { Slog.w(TAG, "Failure copying 32 bit native libraries; copyRet=" +copyRet); return copyRet; } } if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot, Build.SUPPORTED_64_BIT_ABIS, true /* use isa specific subdirs */); if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES && copyRet != PackageManager.INSTALL_FAILED_NO_MATCHING_ABIS) { Slog.w(TAG, "Failure copying 64 bit native libraries; copyRet=" +copyRet); return copyRet; } } } else { String cpuAbiOverride = null; if (CLEAR_ABI_OVERRIDE.equals(abiOverride)) { cpuAbiOverride = null; } else if (abiOverride != null) { cpuAbiOverride = abiOverride; } String[] abiList = (cpuAbiOverride != null) ? new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS; if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null && hasRenderscriptBitcode(handle)) { abiList = Build.SUPPORTED_32_BIT_ABIS; } int copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot, abiList, true /* use isa specific subdirs */); if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES) { Slog.w(TAG, "Failure copying native libraries [errorCode=" + copyRet + "]"); return copyRet; } } return PackageManager.INSTALL_SUCCEEDED; } catch (IOException e) { Slog.e(TAG, "Copying native libraries failed", e); return PackageManager.INSTALL_FAILED_INTERNAL_ERROR; } }复制代码
copyNativeBinariesWithOverride分别处理32位和64位so的拷贝,内部函数copyNativeBinariesForSupportedAbi首先会根据abilist去找对应的abi。
public static int copyNativeBinariesForSupportedAbi(Handle handle, File libraryRoot, String[] abiList, boolean useIsaSubdir) throws IOException { createNativeLibrarySubdir(libraryRoot); /* * If this is an internal application or our nativeLibraryPath points to * the app-lib directory, unpack the libraries if necessary. */ int abi = findSupportedAbi(handle, abiList); if (abi >= 0) { /* * If we have a matching instruction set, construct a subdir under the native * library root that corresponds to this instruction set. */ final String instructionSet = VMRuntime.getInstructionSet(abiList[abi]); final File subDir; if (useIsaSubdir) { final File isaSubdir = new File(libraryRoot, instructionSet); createNativeLibrarySubdir(isaSubdir); subDir = isaSubdir; } else { subDir = libraryRoot; } int copyRet = copyNativeBinaries(handle, subDir, abiList[abi]); if (copyRet != PackageManager.INSTALL_SUCCEEDED) { return copyRet; } } return abi; }复制代码
findSupportedAbi内部实现是native函数,首先遍历apk,如果so的全路径中包含abilist中的abi字符串,则记录该abi字符串的索引,最终返回所有记录索引中最靠前的,即排在abilist中最前面的索引。
static int findSupportedAbi(JNIEnv *env, jlong apkHandle, jobjectArray supportedAbisArray) { const int numAbis = env->GetArrayLength(supportedAbisArray); VectorsupportedAbis; for (int i = 0; i < numAbis; ++i) { supportedAbis.add(new ScopedUtfChars(env, (jstring) env->GetObjectArrayElement(supportedAbisArray, i))); } ZipFileRO* zipFile = reinterpret_cast(apkHandle); if (zipFile == NULL) { return INSTALL_FAILED_INVALID_APK; } UniquePtr it(NativeLibrariesIterator::create(zipFile)); if (it.get() == NULL) { return INSTALL_FAILED_INVALID_APK; } ZipEntryRO entry = NULL; char fileName[PATH_MAX]; int status = NO_NATIVE_LIBRARIES; while ((entry = it->next()) != NULL) { // We're currently in the lib/ directory of the APK, so it does have some native // code. We should return INSTALL_FAILED_NO_MATCHING_ABIS if none of the // libraries match. if (status == NO_NATIVE_LIBRARIES) { status = INSTALL_FAILED_NO_MATCHING_ABIS; } const char* fileName = it->currentEntry(); const char* lastSlash = it->lastSlash(); // Check to see if this CPU ABI matches what we are looking for. const char* abiOffset = fileName + APK_LIB_LEN; const size_t abiSize = lastSlash - abiOffset; for (int i = 0; i < numAbis; i++) { const ScopedUtfChars* abi = supportedAbis[i]; if (abi->size() == abiSize && !strncmp(abiOffset, abi->c_str(), abiSize)) { // The entry that comes in first (i.e. with a lower index) has the higher priority. if (((i < status) && (status >= 0)) || (status < 0) ) { status = i; } } } } for (int i = 0; i < numAbis; ++i) { delete supportedAbis[i]; } return status; }复制代码
举例说明,在某64位测试手机上的abi属性显示如下,它有2个abilist,分别对应该手机支持的32位和64位abi的字符串组。
当处理32位so拷贝时,findSupportedAbi索引返回之后,若返回为0,则拷贝armeabi-v7a目录下的so,如果为1,则拷贝armeabi目录下so。
拷贝so策略:
分别处理32位和64位abi目录的so拷贝,abi由遍历apk结果的所有so中符合abilist列表的最靠前的序号决定,然后拷贝该abi目录下的so文件。
策略问题:
策略假定每个abi目录下的so都放置完全的,这是和2.3.6一样的处理逻辑,存在遗漏拷贝so的可能。
0x5 建议
针对android系统的这些拷贝策略的问题,我们给出了一些配置so的建议:
1)针对armeabi和armeabi-v7a两种ABI
方法1:由于armeabi-v7a指令集兼容armeabi指令集,所以如果损失一些应用的性能是可以接受的,同时不希望保留库的两份拷贝,可以移除armeabi-v7a目录和其下的库文件,只保留armeabi目录;比如apk使用第三方的so只有armeabi这一种abi时,可以考虑去掉apk中lib目录下armeabi-v7a目录。
方法2:在armeabi和armeabi-v7a目录下各放入一份so;
2)针对x86
目前市面上的x86机型,为了兼容arm指令,基本都内置了libhoudini模块,即二进制转码支持,该模块负责把ARM指令转换为X86指令,所以如果是出于apk包大小的考虑,并且可以接受一些性能损失,可以选择删掉x86库目录,x86下配置的armeabi目录的so库一样可以正常加载使用;
3)针对64位ABI
如果app开发者打算支持64位,那么64位的so要放全,否则可以选择不单独编译64位的so,全部使用32位的so,64位机型默认支持32位so的加载。比如apk使用第三方的so只有32位abi的so,可以考虑去掉apk中lib目录下的64位abi子目录,保证apk安装后正常使用。
0x6 备注
其实本文是因为在Android的so加载上遇到很多坑,相信很多朋友都遇到过UnsatisfiedLinkError这个错误,反应在用户的机型上也是千差万别,但是有没有想过,可能不是apk逻辑的问题,而是Android系统在安装APK的时候,由于PackageManager的问题,并没有拷贝相应的SO呢?可以参考下面第4个链接,作者给出了解决方案,就是当出现UnsatisfiedLinkError错误时,手动拷贝so来解决的。
参考文章:
android源码:https://android.googlesource.com/platform/frameworks/base
apk安装过程及原理说明:http://blog.csdn.net/hdhd588/article/details/6739281
UnsatisfiedLinkError的错误及解决方案:
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 快速上手JS-SDK自动化测试
【推荐】 2017年内容安全十大事件盘点
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 负载均衡策略之轮询策略
- 负载均衡策略之两次随机选择策略
- 深度策略梯度算法是真正的策略梯度算法吗?
- 产品策略模型方法论:谈谈我对产品策略模型的6点理解
- 重学 Java 设计模式:实战策略模式「模拟多种营销类型优惠券,折扣金额计算策略场景」
- redis 数据淘汰策略
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design systems
Not all design systems are equally effective. Some can generate coherent user experiences, others produce confusing patchwork designs. Some inspire teams to contribute to them, others are neglected. S......一起来看看 《Design systems》 这本书的介绍吧!