Android Native庫的加載及動態(tài)鏈接的過程
Native庫的裝載過程
我們從一個簡單的NDK Demo開始分析。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
final TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
tv.setText(stringFromJNI());
}
});
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
// Used to load the 'native-lib' library on application startup.
// 動態(tài)庫的裝載及鏈接
static {
System.loadLibrary("native-lib");
}
}
Android 鏈接器Linker之前的工作
下面從 System.loadLibrary() 開始分析。
public static void loadLibrary(String libname) {
這里VMStack.getCallingClassLoader()返回應(yīng)用的類加載器
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
下面看 loadLibrary0()
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
// findLibrary()返回庫的全路徑名
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
// 裝載動態(tài)庫
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
......
}
參數(shù) loader 為Android的應(yīng)用類加載器,它是 PathClassLoader 類型的對象,繼承自 BaseDexClassLoader 對象,下面看 BaseDexClassLoader 的 findLibrary() 方法。
public String findLibrary(String name) {
// 調(diào)用DexPathList的findLibrary方法
return pathList.findLibrary(name);
}
下面看 DexPathList 的 findLibrary() 方法
public String findLibrary(String libraryName) {
// 產(chǎn)生平臺相關(guān)的庫名稱這里返回libxxx.so
String fileName = System.mapLibraryName(libraryName);
for (Element element : nativeLibraryPathElements) {
// 查找動態(tài)庫返回庫的全路徑名
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
回到 loadLibrary0() ,有了動態(tài)庫的全路徑名就可以裝載庫了,下面看 doLoad() 。
private String doLoad(String name, ClassLoader loader) {
......
// 獲取應(yīng)用類加載器的Native庫搜索路徑
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.getLdLibraryPath();
}
// nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
// of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}
nativeLoad() 最終調(diào)用 LoadNativeLibrary() ,下面直接分析 LoadNativeLibrary() 。
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
jstring library_path,
std::string* error_msg) {
......
SharedLibrary* library;
Thread* self = Thread::Current();
{
// TODO: move the locking (and more of this logic) into Libraries.
// 檢查動態(tài)庫是否已經(jīng)裝載過,如果已經(jīng)裝載過(類加載器也匹配)直接返回。
MutexLock mu(self, *Locks::jni_libraries_lock_);
library = libraries_->Get(path);
}
......
// 沒有裝載過,裝載鏈接動態(tài)庫
// 參數(shù)patch_str傳遞的是動態(tài)庫的全路徑,之所以還要傳遞搜索路徑,是因為可能包含它的依賴庫
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path);
......
// 查找動態(tài)庫中的"JNI_OnLoad"函數(shù)
sym = library->FindSymbol("JNI_OnLoad", nullptr);
if (sym == nullptr) {
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = true;
} else {
// Call JNI_OnLoad. We have to override the current class
// loader, which will always be "null" since the stuff at the
// top of the stack is around Runtime.loadLibrary(). (See
// the comments in the JNI FindClass function.)
ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader);
VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
// 調(diào)用庫的JNI_OnLoad函數(shù)注冊JNI, 本文暫不討論
int version = (*jni_on_load)(this, nullptr);
......
}
......
}
對于JNI注冊,這里暫不討論,下面看 OpenNativeLibrary() 的實現(xiàn)。
void* OpenNativeLibrary(JNIEnv* env,
int32_t target_sdk_version,
const char* path,
jobject class_loader,
jstring library_path) {
#if defined(__ANDROID__)
UNUSED(target_sdk_version);
if (class_loader == nullptr) {
return dlopen(path, RTLD_NOW);
}
std::lock_guard<std::mutex> guard(g_namespaces_mutex);
// 找到類加載器映射的命名空間(Android應(yīng)用類加載器創(chuàng)建時創(chuàng)建)
// 關(guān)于命名空間的動態(tài)鏈接請參考http://jackwish.net/namespace-based-dynamic-linking-chn.html
android_namespace_t* ns = g_namespaces->FindNamespaceByClassLoader(env, class_loader);
.......
android_dlextinfo extinfo;
// 在一個不同的命名空間中裝載
extinfo.flags = ANDROID_DLEXT_USE_NAMESPACE;
extinfo.library_namespace = ns;
// RILD_NOW表示重定位在dlopen返回前完成,不會延遲到第一次執(zhí)行(RTLD_LAZY)
return android_dlopen_ext(path, RTLD_NOW, &extinfo);
......
}
下面看 android_dlopen_ext() 的實現(xiàn)
void* android_dlopen_ext(const char* filename, int flags, const android_dlextinfo* extinfo) {
// __builtin_return_address是編譯器的內(nèi)建函數(shù),__builtin_return_address(0)表示當前函數(shù)的返回地址
void* caller_addr = __builtin_return_address(0);
return dlopen_ext(filename, flags, extinfo, caller_addr);
}
接下來就Android鏈接器linker的工作了。
Android 鏈接器Linker的裝載工作
下面從 do_dlopen() 開始分析。
void* do_dlopen(const char* name, int flags, const android_dlextinfo* extinfo,
void* caller_addr) {
// caller_addr在libnativeloader.so中
// 查找地址所在的動態(tài)庫(采用遍歷查找,可以優(yōu)化查找)
soinfo* const caller = find_containing_library(caller_addr);
// ns為調(diào)用庫所在命名空間
android_namespace_t* ns = get_caller_namespace(caller);
......
if (extinfo != nullptr) {
......
// extinfo->flags為ANDROID_DLEXT_USE_NAMESPACE
if ((extinfo->flags & ANDROID_DLEXT_USE_NAMESPACE) != 0) {
if (extinfo->library_namespace == nullptr) {
DL_ERR("ANDROID_DLEXT_USE_NAMESPACE is set but extinfo->library_namespace is null");
return nullptr;
}
// 命名空間使用應(yīng)用自身類加載器-命名空間
ns = extinfo->library_namespace;
}
}
......
// 在命名空間ns中裝載庫
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
......
}
find_library() 當參數(shù)translated_name不為空時,直接調(diào)用 find_libraries() ,這是裝載鏈接的關(guān)鍵函數(shù),下面看它的實現(xiàn)。
static bool find_libraries(android_namespace_t* ns,
soinfo* start_with,
const char* const library_names[],
size_t library_names_count, soinfo* soinfos[],
std::vector<soinfo*>* ld_preloads,
size_t ld_preloads_count, int rtld_flags,
const android_dlextinfo* extinfo,
bool add_as_children) {
// ns為應(yīng)用類加載器-命名空間
// 這里start_with為libnativeloader.so的soinfo
// library_names為需要裝載的動態(tài)庫,不包含依賴庫
// library_names_count需要裝載的動態(tài)庫的數(shù)量,這里為1
// ld_preloads為nullptr
// add_as_children為false
......
// 為需要裝載的動態(tài)庫創(chuàng)建LoadTask添加到load_tasks
// LoadTask用于管理動態(tài)庫的裝載
for (size_t i = 0; i < library_names_count; ++i) {
const char* name = library_names[i];
load_tasks.push_back(LoadTask::create(name, start_with, &readers_map));
}
// Construct global_group.
// 收集命名空間ns中設(shè)置了DF_1_GLOBAL(RTLD_GLOBAL:共享庫中的符號可被后續(xù)裝載的庫重定位)標志的動態(tài)庫
soinfo_list_t global_group = make_global_group(ns);
......
// Step 1: expand the list of load_tasks to include
// all DT_NEEDED libraries (do not load them just yet)
// load_tasks以廣度優(yōu)先遍歷的順序存儲動態(tài)庫依賴樹
// 例如依賴樹: 1
// / \
// 2 3
// / \
// 4 5
// load_tasks: 1->2->3->4->5
for (size_t i = 0; i<load_tasks.size(); ++i) {
LoadTask* task = load_tasks[i];
soinfo* needed_by = task->get_needed_by();
bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
task->set_extinfo(is_dt_needed ? nullptr : extinfo);
task->set_dt_needed(is_dt_needed);
// 收集動態(tài)庫的信息以及它的依賴庫
if(!find_library_internal(ns, task, &zip_archive_cache, &load_tasks, rtld_flags)) {
return false;
}
soinfo* si = task->get_soinfo();
if (is_dt_needed) {
// si添加到needed_by的依賴中
needed_by->add_child(si);
}
if (si->is_linked()) {
// 已經(jīng)鏈接過的庫增加引用計數(shù)
si->increment_ref_count();
}
......
if (soinfos_count < library_names_count) {
soinfos[soinfos_count++] = si;
}
}
// Step 2: Load libraries in random order (see b/24047022)
LoadTaskList load_list;
// 需要裝載的庫放到load_list中
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
auto pred = [&](const LoadTask* t) {
return t->get_soinfo() == si;
};
if (!si->is_linked() &&
std::find_if(load_list.begin(), load_list.end(), pred) == load_list.end() ) {
load_list.push_back(task);
}
}
// 隨機化load_list中庫的順序
shuffle(&load_list);
for (auto&& task : load_list) {
// 裝載動態(tài)庫
if (!task->load()) {
return false;
}
}
// Step 3: pre-link all DT_NEEDED libraries in breadth first order.
// 預(yù)鏈接load_tasks中沒有鏈接過的庫,見下文
for (auto&& task : load_tasks) {
soinfo* si = task->get_soinfo();
if (!si->is_linked() && !si->prelink_image()) {
return false;
}
}
// Step 4: Add LD_PRELOADed libraries to the global group for
// future runs. There is no need to explicitly add them to
// the global group for this run because they are going to
// appear in the local group in the correct order.
if (ld_preloads != nullptr) {
for (auto&& si : *ld_preloads) {
si->set_dt_flags_1(si->get_dt_flags_1() | DF_1_GLOBAL);
}
}
// Step 5: link libraries.
// 鏈接過程,見下文
soinfo_list_t local_group;
// 廣度優(yōu)先遍歷添加動態(tài)庫依賴圖到local_group中
walk_dependencies_tree(
(start_with != nullptr && add_as_children) ? &start_with : soinfos,
(start_with != nullptr && add_as_children) ? 1 : soinfos_count,
[&] (soinfo* si) {
local_group.push_back(si);
return true;
});
// We need to increment ref_count in case
// the root of the local group was not linked.
bool was_local_group_root_linked = local_group.front()->is_linked();
bool linked = local_group.visit([&](soinfo* si) {
if (!si->is_linked()) {
if (!si->link_image(global_group, local_group, extinfo)) {
return false;
}
}
return true;
});
if (linked) {
// 設(shè)置鏈接標志
local_group.for_each([](soinfo* si) {
if (!si->is_linked()) {
si->set_linked();
}
});
failure_guard.disable();
}
......
}
find_libraries() 中動態(tài)庫的裝載可以分為兩部分
- 收集動態(tài)庫的信息及其依賴庫
- 裝載動態(tài)庫及依賴庫
- 收集動態(tài)庫的信息及依賴庫
下面從 find_library_internal() 開始分析。
static bool find_library_internal(android_namespace_t* ns,
LoadTask* task,
ZipArchiveCache* zip_archive_cache,
LoadTaskList* load_tasks,
int rtld_flags) {
soinfo* candidate;
// 在應(yīng)用類加載器-命名空間中查找動態(tài)庫是否已經(jīng)裝載過
if (find_loaded_library_by_soname(ns, task->get_name(), &candidate)) {
task->set_soinfo(candidate);
return true;
}
// 在默認命名空間中查找動態(tài)庫是否已經(jīng)裝載過
if (ns != &g_default_namespace) {
// check public namespace
candidate = g_public_namespace.find_if([&](soinfo* si) {
return strcmp(task->get_name(), si->get_soname()) == 0;
});
......
}
......
// 裝載庫
if (load_library(ns, task, zip_archive_cache, load_tasks, rtld_flags)) {
return true;
} else {
// In case we were unable to load the library but there
// is a candidate loaded under the same soname but different
// sdk level - return it anyways.
if (candidate != nullptr) {
task->set_soinfo(candidate);
return true;
}
}
return false;
}
下面分析 load_library()
static bool load_library(android_namespace_t* ns,
LoadTask* task,
ZipArchiveCache* zip_archive_cache,
LoadTaskList* load_tasks,
int rtld_flags) {
......
// 打開庫文件返回文件描述符
int fd = open_library(ns, zip_archive_cache, name, needed_by, &file_offset, &realpath);
if (fd == -1) {
DL_ERR("library \"%s\" not found", name);
return false;
}
task->set_fd(fd, true);
task->set_file_offset(file_offset);
// 裝載庫
return load_library(ns, task, load_tasks, rtld_flags, realpath);
}
下面看另一個 load_library() 的實現(xiàn)
static bool load_library(android_namespace_t* ns,
LoadTask* task,
LoadTaskList* load_tasks,
int rtld_flags,
const std::string& realpath) {
off64_t file_offset = task->get_file_offset();
const char* name = task->get_name();
const android_dlextinfo* extinfo = task->get_extinfo();
......
// 為動態(tài)庫創(chuàng)建soinfo,用于記錄動態(tài)鏈接信息等
soinfo* si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
if (si == nullptr) {
return false;
}
task->set_soinfo(si);
// Read the ELF header and some of the segments.
// 讀取ELF文件頭以及一些段信息
if (!task->read(realpath.c_str(), file_stat.st_size)) {
soinfo_free(si);
task->set_soinfo(nullptr);
return false;
}
......
// 查找依賴庫,創(chuàng)建LoadTask添加到load_tasks
for_each_dt_needed(task->get_elf_reader(), [&](const char* name) {
load_tasks->push_back(LoadTask::create(name, si, task->get_readers_map()));
});
return true;
}
下面分析ELF文件頭以及段信息的讀取過程,也就是LoadTask的 read() ,它直接調(diào)用ElfReader的 Read() 方法。
bool ElfReader::Read(const char* name, int fd, off64_t file_offset, off64_t file_size) {
CHECK(!did_read_);
CHECK(!did_load_);
name_ = name;
fd_ = fd;
file_offset_ = file_offset;
file_size_ = file_size;
if (ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeaders() &&
ReadSectionHeaders() &&
ReadDynamicSection()) {
did_read_ = true;
}
return did_read_;
}
ReadElfHeader() : 讀取ELF文件頭信息
VerifyElfHeader() : 校驗ELF(文件類型等)
ReadProgramHeaders() : 根據(jù)ELF文件頭信息獲取程序頭表
ReadSectionHeaders() : 根據(jù)ELF文件頭信息獲取段頭表
ReadDynamicSection() : 獲取Dynamic Section的信息
裝載動態(tài)庫
動態(tài)庫的裝載在LoadTask的 load() 中實現(xiàn)。
bool load() {
ElfReader& elf_reader = get_elf_reader();
// 映射動態(tài)庫的可加載Segment到進程的虛擬地址空間中
if (!elf_reader.Load(extinfo_)) {
return false;
}
// 保存裝載信息
// 動態(tài)庫裝載的起始地址
si_->base = elf_reader.load_start();
// 可裝載的Segment大小之和
si_->size = elf_reader.load_size();
si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
// 動態(tài)庫裝載的期望起始地址,通常si_->load_bias = si_->base
si_->load_bias = elf_reader.load_bias();
// 動態(tài)庫程序頭表項數(shù)
si_->phnum = elf_reader.phdr_count();
// 動態(tài)庫程序頭表的地址
si_->phdr = elf_reader.loaded_phdr();
return true;
}
在實際的地址計算中,使用si_->load_bias,不使用si_->base。
下面看ElfReader的 Load() 方法
bool ElfReader::Load(const android_dlextinfo* extinfo) {
CHECK(did_read_);
CHECK(!did_load_);
if (ReserveAddressSpace(extinfo) &&
LoadSegments() &&
FindPhdr()) {
did_load_ = true;
}
return did_load_;
}
ReserveAddressSpace(): 保留虛擬地址空間為動態(tài)庫(裝載地址隨機化)
LoadSegments() : 裝載ELF文件中可裝載的Segments
FindPhdr() : 確保程序頭表包含在一個可加載的Segment中
動態(tài)庫的裝載已經(jīng)完成,下面看鏈接過程。
Native庫動態(tài)鏈接的過程
預(yù)鏈接
下面看 prelink_image()
bool soinfo::prelink_image() {
/* Extract dynamic section */
ElfW(Word) dynamic_flags = 0;
// 根據(jù)程序頭表的地址計算dynamic section的地址
phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags);
......
uint32_t needed_count = 0;
// 解析dynamic section獲取動態(tài)鏈接信息
for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
DEBUG("d = %p, d[0](tag) = %p d[1](val) = %p",
d, reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
switch (d->d_tag) {
......
case DT_STRTAB:
// 動態(tài)字符串表的地址
strtab_ = reinterpret_cast<const char*>(load_bias + d->d_un.d_ptr);
break;
case DT_STRSZ:
strtab_size_ = d->d_un.d_val;
break;
case DT_SYMTAB:
// 動態(tài)符號表的地址
symtab_ = reinterpret_cast<ElfW(Sym)*>(load_bias + d->d_un.d_ptr);
break;
......
case DT_JMPREL:
// 需重定位的函數(shù)表(.rela.plt)的地址
#if defined(USE_RELA)
plt_rela_ = reinterpret_cast<ElfW(Rela)*>(load_bias + d->d_un.d_ptr);
#else
plt_rel_ = reinterpret_cast<ElfW(Rel)*>(load_bias + d->d_un.d_ptr);
#endif
break;
......
case DT_RELA:
// 需重定位的數(shù)據(jù)表(.rela.dyn)的地址
rela_ = reinterpret_cast<ElfW(Rela)*>(load_bias + d->d_un.d_ptr);
break;
......
case DT_NEEDED:
// 依賴的動態(tài)庫
++needed_count;
break;
}
}
......
// Sanity checks.
// 檢查動態(tài)鏈接信息
if (relocating_linker && needed_count != 0) {
DL_ERR("linker cannot have DT_NEEDED dependencies on other libraries");
return false;
}
if (nbucket_ == 0 && gnu_nbucket_ == 0) {
DL_ERR("empty/missing DT_HASH/DT_GNU_HASH in \"%s\" "
"(new hash type from the future?)", get_realpath());
return false;
}
if (strtab_ == 0) {
DL_ERR("empty/missing DT_STRTAB in \"%s\"", get_realpath());
return false;
}
if (symtab_ == 0) {
DL_ERR("empty/missing DT_SYMTAB in \"%s\"", get_realpath());
return false;
}
......
}
鏈接
鏈接主要完成符號重定位工作,下面從 link_image() 開始分析
bool soinfo::link_image(const soinfo_list_t& global_group, const soinfo_list_t& local_group,
const android_dlextinfo* extinfo) {
......
#if defined(USE_RELA)
// rela_為重定位數(shù)據(jù)表的地址
if (rela_ != nullptr) {
DEBUG("[ relocating %s ]", get_realpath());
// 數(shù)據(jù)引用重定位
if (!relocate(version_tracker,
plain_reloc_iterator(rela_, rela_count_), global_group, local_group)) {
return false;
}
}
// plt_rela_為重定位函數(shù)表的地址
if (plt_rela_ != nullptr) {
DEBUG("[ relocating %s plt ]", get_realpath());
// 函數(shù)引用重定位
if (!relocate(version_tracker,
plain_reloc_iterator(plt_rela_, plt_rela_count_), global_group, local_group)) {
return false;
}
}
#else
......
}
下面以函數(shù)引用重定位為例分析 relocate() 方法
template<typename ElfRelIteratorT>
bool soinfo::relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
const soinfo_list_t& global_group, const soinfo_list_t& local_group) {
for (size_t idx = 0; rel_iterator.has_next(); ++idx) {
const auto rel = rel_iterator.next();
if (rel == nullptr) {
return false;
}
// rel->r_info的低32位
ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
// rel->r_info的高32位
ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);
// 重定位地址的存儲位置
ElfW(Addr) reloc = static_cast<ElfW(Addr)>(rel->r_offset + load_bias);
ElfW(Addr) sym_addr = 0;
const char* sym_name = nullptr;
ElfW(Addr) addend = get_addend(rel, reloc);
......
if (sym != 0) {
// sym為動態(tài)符號表項的索引
// symtab_[sym].st_name為符號在動態(tài)字符串表的索引
// sysm_name為需重定位的符號名
sym_name = get_string(symtab_[sym].st_name);
const version_info* vi = nullptr;
if (!lookup_version_info(version_tracker, sym, sym_name, &vi)) {
return false;
}
// 查找符號返回符號表項的地址
if (!soinfo_do_lookup(this, sym_name, vi, &lsi, global_group, local_group, &s)) {
return false;
}
if (s == nullptr) {
......
} else {
......
// 根據(jù)符號表項計算符號地址
sym_addr = lsi->resolve_symbol_address(s);
......
}
......
}
switch (type) {
// ELF64中R_GENERIC_JUMP_SLOT = R_AARCH64_JUMP_SLOT
case R_GENERIC_JUMP_SLOT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO JMP_SLOT %16p <- %16p %s\n",
reinterpret_cast<void*>(reloc),
reinterpret_cast<void*>(sym_addr + addend), sym_name);
// 符號地址更新到reloc(GOT表)中
*reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
break;
......
}
}
return true;
}
參考
Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification
總結(jié)
以上所述是小編給大家介紹的Android Native庫的加載及動態(tài)鏈接的過程,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
Android使用View Animation實現(xiàn)動畫加載界面
這篇文章主要為大家詳細介紹了Android使用View Animation實現(xiàn)動畫加載界面的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-04-04
淺談Android app開發(fā)中Fragment的Transaction操作
這篇文章主要介紹了Android app開發(fā)中Fragment的Transaction操作,包括Transaction和Fragment的生命周期的聯(lián)系等內(nèi)容,需要的朋友可以參考下2016-02-02
Android使用相機實現(xiàn)拍照存儲及展示功能詳解
這篇文章主要介紹了Android使用相機實現(xiàn)拍照存儲及展示功能,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-01-01
Android 從底部彈出Dialog(橫向滿屏)的實例代碼
在android開發(fā)中經(jīng)常會遇到底部彈出框的功能,今天小編抽時間給大家整理一個底部彈出橫向滿屏的dialog,需要的朋友參考下2016-11-11
Android使用ContentProvider初始化SDK庫方案小結(jié)
這篇文章主要介紹了Android使用ContentProvider初始化SDK庫方案總結(jié),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04
Android使用ViewFlipper和GestrueDetector共同實現(xiàn)滑屏效果實例
這篇文章主要介紹了Android使用ViewFlipper和GestrueDetector共同實現(xiàn)滑屏效果,結(jié)合完整實例形式分析了ViewFlipper和GestrueDetector控件實現(xiàn)滑屏功能的布局與相關(guān)操作技巧,需要的朋友可以參考下2017-02-02

