Android开发——关于32位与64位so的加载问题

Android加载so文件的机制

有时候会为了减少apk的大小,只设置支持 “armeabi-v7a” so库架构,然而在apk在安装的过程中,系统就会对apk进行解析根据里面so文件类型,确定这个apk安装是在32 还是 64位的虚拟机上,如果是32位虚拟机那么就不能使用64位so,如果是64位虚拟机也不能使用32位so。而64位设备可以提供32和64位两种虚拟机,根据apk选择开启哪一种,因此说64位设备兼容32的so库;加载so机制分下面四种情况:

1. 假设apk的lib目录放置了32和64位两种so,那么安装时根据当前设备的cpu架构从上到下筛选(X86 > arm64 > arm32),一旦发现lib里面有和设备匹配的so文件,那么直接选定这种架构为标准。比如当前设备是64位并且发现lib有一个64位的so,那么apk会拷贝lib下所有64位的so文件到data/data/packageName/lib64/目录下;

2. apk的lib目录只有32位的so,由于64位设备时兼容32位的库,所以安装时根据当前设备的cpu架构从上到下筛选(X86 > arm64 > arm32),能够正常的运行在32位设备和大部分64位设备上(目前没有遇到不正常运行的状态)。

3. apk的lib目录只有64位的so时,那这个apk只能运行在64位的设备上。

4. apk的lib不放任何的so文件,全部动态加载时,安装在32位设备就只能加载32位so,安装在64位的设备系统会默认将apk运行在64位虚拟机,不过可以通过指定当前加载。

TODO:

1. 如果apk的lib不放任何的so文件,并在build.grade中通过abiFilters设置过滤,是否能起到加载特定的so的目的

ndk {
    abiFilters 'armeabi-v7a'
}

Android so的动态加载

有时会为了减少apk的大小,会将so放到服务器上,比如我们的小游戏大厅中,在引擎框架下,会将每个小游戏编译成so,再启动相应的游戏时去下载这个游戏的so,然后再去加载so。

对于动态加载so方式的apk需要通过上传的方式进行处理,然后下载使用合适的so,下面来看下so的动态加载过程(8.0.0_r4):

/**
1625     * Loads the native library specified by the <code>libname</code>
1626     * argument.  The <code>libname</code> argument must not contain any platform
1627     * specific prefix, file extension or path. If a native library
1628     * called <code>libname</code> is statically linked with the VM, then the
1629     * JNI_OnLoad_<code>libname</code> function exported by the library is invoked.
1630     * See the JNI Specification for more details.
1631     *
1632     * Otherwise, the libname argument is loaded from a system library
1633     * location and mapped to a native library image in an implementation-
1634     * dependent manner.
1635     * <p>
1636     * The call <code>System.loadLibrary(name)</code> is effectively
1637     * equivalent to the call
1638     * <blockquote><pre>
1639     * Runtime.getRuntime().loadLibrary(name)
1640     * </pre></blockquote>
1641     *
1642     * @param      libname   the name of the library.
1643     * @exception  SecurityException  if a security manager exists and its
1644     *             <code>checkLink</code> method doesn't allow
1645     *             loading of the specified dynamic library
1646     * @exception  UnsatisfiedLinkError if either the libname argument
1647     *             contains a file path, the native library is not statically
1648     *             linked with the VM,  or the library cannot be mapped to a
1649     *             native library image by the host system.
1650     * @exception  NullPointerException if <code>libname</code> is
1651     *             <code>null</code>
1652     * @see        java.lang.Runtime#loadLibrary(java.lang.String)
1653     * @see        java.lang.SecurityManager#checkLink(java.lang.String)
1654     */
1655    @CallerSensitive
1656    public static void loadLibrary(String libname) {
1657        Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
1658    }

上面的源码很简单,直接看Runtime.loadLibrary0具体实现:

998    synchronized void loadLibrary0(ClassLoader loader, String libname) {
999        if (libname.indexOf((int)File.separatorChar) != -1) {
1000            throw new UnsatisfiedLinkError(
1001    "Directory separator should not appear in library name: " + libname);
1002        }
1003        String libraryName = libname;
1004        if (loader != null) {//ClassLoader非空时,利用ClassLoader的findLibrary()方法来获取library的path
1005            String filename = loader.findLibrary(libraryName);
1006            if (filename == null) {
1007                // It's not necessarily true that the ClassLoader used
1008                // System.mapLibraryName, but the default setup does, and it's
1009                // misleading to say we didn't find "libMyLibrary.so" when we
1010                // actually searched for "liblibMyLibrary.so.so".
1011                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
1012                                               System.mapLibraryName(libraryName) + "\"");
1013            }
1014            String error = doLoad(filename, loader);
1015            if (error != null) {
1016                throw new UnsatisfiedLinkError(error);
1017            }
1018            return;
1019        }
1020        //当loader为空时, 则从默认目录mLibPaths下来查找是否存在该动态库;
1021        String filename = System.mapLibraryName(libraryName);
1022        List<String> candidates = new ArrayList<String>();
1023        String lastError = null;
1024        for (String directory : getLibPaths()) {
1025            String candidate = directory + filename;
1026            candidates.add(candidate);
1027
1028            if (IoUtils.canOpenReadOnly(candidate)) {
1029                String error = doLoad(candidate, loader);
1030                if (error == null) {
1031                    return; // We successfully loaded the library. Job done.
1032                }
1033                lastError = error;
1034            }
1035        }
1036
1037        if (lastError != null) {
1038            throw new UnsatisfiedLinkError(lastError);
1039        }
1040        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
1041    }

1. loader为null 时在做什么?

a. mapLibraryName会调用System的native方法,System_mapLibraryName是其具体的实现,改方法是将xxx动态库的名字转换为libxxx.so(目前还不知道为啥要这么麻烦通过native来实现)。

 

147JNIEXPORT jstring JNICALL
148System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
149{
150    int len;
151    int prefix_len = (int) strlen(JNI_LIB_PREFIX);
152    int suffix_len = (int) strlen(JNI_LIB_SUFFIX);
153
154    jchar chars[256];
155    if (libname == NULL) {
156        JNU_ThrowNullPointerException(env, 0);
157        return NULL;
158    }
159    len = (*env)->GetStringLength(env, libname);
160    if (len > 240) {
161        JNU_ThrowIllegalArgumentException(env, "name too long");
162        return NULL;
163    }
164    cpchars(chars, JNI_LIB_PREFIX, prefix_len);
165    (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
166    len += prefix_len;
167    cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
168    len += suffix_len;
169
170    return (*env)->NewString(env, chars, len);
171}

b. getLibPaths的实现如下,只是为了获取加载库时搜索的路径列表:

 

1043    private volatile String[] mLibPaths = null;
1044
1045    private String[] getLibPaths() {
1046        if (mLibPaths == null) {
1047            synchronized(this) {
1048                if (mLibPaths == null) {
1049                    mLibPaths = initLibPaths();
1050                }
1051            }
1052        }
1053        return mLibPaths;
1054    }
1055
1056    private static String[] initLibPaths() {
1057        String javaLibraryPath = System.getProperty("java.library.path");
1058        if (javaLibraryPath == null) {
1059            return EmptyArray.STRING;
1060        }
1061        String[] paths = javaLibraryPath.split(":");
1062        // Add a '/' to the end of each directory so we don't have to do it every time.
1063        for (int i = 0; i < paths.length; ++i) {
1064            if (!paths[i].endsWith("/")) {
1065                paths[i] += "/";
1066            }
1067        }
1068        return paths;
1069    }
序号属性说明
1java.versionJava 运行时环境版本
2java.vendorJava 运行时环境供应商
3java.vendor.urlJava 供应商的 URL
4java.homeJava 安装目录
5java.vm.specification.versionJava 虚拟机规范版本
6java.vm.specification.vendorJava 虚拟机规范供应商
7java.vm.specification.nameJava 虚拟机规范名称
8java.vm.versionJava 虚拟机实现版本
9java.vm.vendorJava 虚拟机实现供应商
10java.vm.nameJava 虚拟机实现名称
11java.specification.versionJava 运行时环境规范版本
12java.specification.vendorJava 运行时环境规范供应商
13java.specification.nameJava 运行时环境规范名称
14java.class.versionJava 类格式版本号
15java.class.pathJava 类路径
16java.library.path加载库时搜索的路径列表
17java.io.tmpdir默认的临时文件路径
18java.compiler要使用的 JIT 编译器的名称
19java.ext.dirs一个或多个扩展目录的路径
20os.name操作系统的名称
21os.arch操作系统的架构
22os.version操作系统的版本
23file.separator文件分隔符(在 UNIX 系统中是“/”)
24path.separator路径分隔符(在 UNIX 系统中是“:”)
25line.separator行分隔符(在 UNIX 系统中是“/n”)
26user.name用户的账户名称
27user.home用户的主目录
28user.dir用户的当前工作目录

c. canOpenReadOnly方法来判定目标动态库是否存在

151    /**
152     * Do not use. This is for System.loadLibrary use only.
153     *
154     * Checks whether {@code path} can be opened read-only. Similar to File.exists, but doesn't
155     * require read permission on the parent, so it'll work in more cases, and allow you to
156     * remove read permission from more directories. Everyone else should just open(2) and then
157     * use the fd, but the loadLibrary API is broken by its need to ask ClassLoaders where to
158     * find a .so rather than just calling dlopen(3).
159     */
160    public static boolean canOpenReadOnly(String path) {
161        try {
162            // Use open(2) rather than stat(2) so we require fewer permissions. http://b/6485312.
163            FileDescriptor fd = Libcore.os.open(path, O_RDONLY, 0);
164            Libcore.os.close(fd);
165            return true;
166        } catch (ErrnoException errnoException) {
167            return false;
168        }
169    }

d. 如找到指定的目标动态库,调用doLoad来加载,在loader为null 时直接调用了nativeLoad

1070    private String doLoad(String name, ClassLoader loader) {
1071        // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
1072        // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.
1073
1074        // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
1075        // libraries with no dependencies just fine, but an app that has multiple libraries that
1076        // depend on each other needed to load them in most-dependent-first order.
1077
1078        // We added API to Android's dynamic linker so we can update the library path used for
1079        // the currently-running process. We pull the desired path out of the ClassLoader here
1080        // and pass it to nativeLoad so that it can call the private dynamic linker API.
1081
1082        // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
1083        // beginning because multiple apks can run in the same process and third party code can
1084        // use its own BaseDexClassLoader.
1085
1086        // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
1087        // dlopen(3) calls made from a .so's JNI_OnLoad to work too.
1088
1089        // So, find out what the native library search path is for the ClassLoader in question...
1090        String librarySearchPath = null;
1091        if (loader != null && loader instanceof BaseDexClassLoader) {
1092            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
1093            librarySearchPath = dexClassLoader.getLdLibraryPath();
1094        }
1095        // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
1096        // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
1097        // internal natives.
1098        synchronized (this) {
1099            return nativeLoad(name, loader, librarySearchPath);
1100        }
1101    }

e. nativeLoad是通过native来实现的,JVM_NativeLoad来完成具体的实现细节:

322JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
323                                 jstring javaFilename,
324                                 jobject javaLoader,
325                                 jstring javaLibrarySearchPath) {
326  ScopedUtfChars filename(env, javaFilename);
327  if (filename.c_str() == NULL) {
328    return NULL;
329  }
330
331  std::string error_msg;
332  {
333    art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
334    bool success = vm->LoadNativeLibrary(env,
335                                         filename.c_str(),
336                                         javaLoader,
337                                         javaLibrarySearchPath,
338                                         &error_msg);
339    if (success) {
340      return nullptr;
341    }
342  }
343
344  // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
345  env->ExceptionClear();
346  return env->NewStringUTF(error_msg.c_str());
347}

f. 真正加载so的实现来了,OpenNativeLibrary 来处理加载,加载完成之后创建动态库,并放到libraries_中,找到JNI_OnLoad符号所对应的方法, 并调用该方法:

766bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
767                                  const std::string& path,
768                                  jobject class_loader,
769                                  jstring library_path,
770                                  std::string* error_msg) {
771  error_msg->clear();
772
773  // See if we've already loaded this library.  If we have, and the class loader
774  // matches, return successfully without doing anything.
775  // TODO: for better results we should canonicalize the pathname (or even compare
776  // inodes). This implementation is fine if everybody is using System.loadLibrary.
777  SharedLibrary* library;
778  Thread* self = Thread::Current();
779  {
780    // TODO: move the locking (and more of this logic) into Libraries.
781    MutexLock mu(self, *Locks::jni_libraries_lock_);
782    library = libraries_->Get(path);
783  }
784  void* class_loader_allocator = nullptr;
785  {
786    ScopedObjectAccess soa(env);
787    // As the incoming class loader is reachable/alive during the call of this function,
788    // it's okay to decode it without worrying about unexpectedly marking it alive.
789    ObjPtr<mirror::ClassLoader> loader = soa.Decode<mirror::ClassLoader>(class_loader);
790
791    ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
792    if (class_linker->IsBootClassLoader(soa, loader.Ptr())) {
793      loader = nullptr;
794      class_loader = nullptr;
795    }
796
797    class_loader_allocator = class_linker->GetAllocatorForClassLoader(loader.Ptr());
798    CHECK(class_loader_allocator != nullptr);
799  }
800  if (library != nullptr) {
801    // Use the allocator pointers for class loader equality to avoid unnecessary weak root decode.
802    if (library->GetClassLoaderAllocator() != class_loader_allocator) {
803      // The library will be associated with class_loader. The JNI
804      // spec says we can't load the same library into more than one
805      // class loader.
806      StringAppendF(error_msg, "Shared library \"%s\" already opened by "
807          "ClassLoader %p; can't open in ClassLoader %p",
808          path.c_str(), library->GetClassLoader(), class_loader);
809      LOG(WARNING) << error_msg;
810      return false;
811    }
812    VLOG(jni) << "[Shared library \"" << path << "\" already loaded in "
813              << " ClassLoader " << class_loader << "]";
814    if (!library->CheckOnLoadResult()) {
815      StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
816          "to load \"%s\"", path.c_str());
817      return false;
818    }
819    return true;
820  }
821
822  // Open the shared library.  Because we're using a full path, the system
823  // doesn't have to search through LD_LIBRARY_PATH.  (It may do so to
824  // resolve this library's dependencies though.)
825
826  // Failures here are expected when java.library.path has several entries
827  // and we have to hunt for the lib.
828
829  // Below we dlopen but there is no paired dlclose, this would be necessary if we supported
830  // class unloading. Libraries will only be unloaded when the reference count (incremented by
831  // dlopen) becomes zero from dlclose.
832
833  Locks::mutator_lock_->AssertNotHeld(self);
834  const char* path_str = path.empty() ? nullptr : path.c_str();
835  bool needs_native_bridge = false;
836  void* handle = android::OpenNativeLibrary(env,
837                                            runtime_->GetTargetSdkVersion(),
838                                            path_str,
839                                            class_loader,
840                                            library_path,
841                                            &needs_native_bridge,
842                                            error_msg);
843
844  VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";
845
846  if (handle == nullptr) {
847    VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
848    return false;
849  }
850
851  if (env->ExceptionCheck() == JNI_TRUE) {
852    LOG(ERROR) << "Unexpected exception:";
853    env->ExceptionDescribe();
854    env->ExceptionClear();
855  }
856  // Create a new entry.
857  // TODO: move the locking (and more of this logic) into Libraries.
858  bool created_library = false;
859  {
860    // Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering.
861    std::unique_ptr<SharedLibrary> new_library(
862        new SharedLibrary(env,
863                          self,
864                          path,
865                          handle,
866                          needs_native_bridge,
867                          class_loader,
868                          class_loader_allocator));
869
870    MutexLock mu(self, *Locks::jni_libraries_lock_);
871    library = libraries_->Get(path);
872    if (library == nullptr) {  // We won race to get libraries_lock.
873      library = new_library.release();
874      libraries_->Put(path, library);
875      created_library = true;
876    }
877  }
878  if (!created_library) {
879    LOG(INFO) << "WOW: we lost a race to add shared library: "
880        << "\"" << path << "\" ClassLoader=" << class_loader;
881    return library->CheckOnLoadResult();
882  }
883  VLOG(jni) << "[Added shared library \"" << path << "\" for ClassLoader " << class_loader << "]";
884
885  bool was_successful = false;
886  void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
887  if (sym == nullptr) {
888    VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
889    was_successful = true;
890  } else {
891    // Call JNI_OnLoad.  We have to override the current class
892    // loader, which will always be "null" since the stuff at the
893    // top of the stack is around Runtime.loadLibrary().  (See
894    // the comments in the JNI FindClass function.)
895    ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
896    self->SetClassLoaderOverride(class_loader);
897
898    VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
899    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
900    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
901    int version = (*jni_on_load)(this, nullptr);
902
903    if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
904      // Make sure that sigchain owns SIGSEGV.
905      EnsureFrontOfChain(SIGSEGV);
906    }
907
908    self->SetClassLoaderOverride(old_class_loader.get());
909
910    if (version == JNI_ERR) {
911      StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
912    } else if (JavaVMExt::IsBadJniVersion(version)) {
913      StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
914                    path.c_str(), version);
915      // It's unwise to call dlclose() here, but we can mark it
916      // as bad and ensure that future load attempts will fail.
917      // We don't know how far JNI_OnLoad got, so there could
918      // be some partially-initialized stuff accessible through
919      // newly-registered native method calls.  We could try to
920      // unregister them, but that doesn't seem worthwhile.
921    } else {
922      was_successful = true;
923    }
924    VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
925              << " from JNI_OnLoad in \"" << path << "\"]";
926  }
927
928  library->SetResult(was_successful);
929  return was_successful;
930}
458void* OpenNativeLibrary(JNIEnv* env,
459                        int32_t target_sdk_version,
460                        const char* path,
461                        jobject class_loader,
462                        jstring library_path,
463                        bool* needs_native_bridge,
464                        std::string* error_msg) {
465#if defined(__ANDROID__)
466  UNUSED(target_sdk_version);
467  if (class_loader == nullptr) {
468    *needs_native_bridge = false;
469    return dlopen(path, RTLD_NOW);
470  }
471
472  std::lock_guard<std::mutex> guard(g_namespaces_mutex);
473  NativeLoaderNamespace ns;
474
475  if (!g_namespaces->FindNamespaceByClassLoader(env, class_loader, &ns)) {
476    // This is the case where the classloader was not created by ApplicationLoaders
477    // In this case we create an isolated not-shared namespace for it.
478    if (!g_namespaces->Create(env,
479                              target_sdk_version,
480                              class_loader,
481                              false,
482                              library_path,
483                              nullptr,
484                              &ns,
485                              error_msg)) {
486      return nullptr;
487    }
488  }
489
490  if (ns.is_android_namespace()) {
491    android_dlextinfo extinfo;
492    extinfo.flags = ANDROID_DLEXT_USE_NAMESPACE;
493    extinfo.library_namespace = ns.get_android_ns();
494
495    void* handle = android_dlopen_ext(path, RTLD_NOW, &extinfo);
496    if (handle == nullptr) {
497      *error_msg = dlerror();
498    }
499    *needs_native_bridge = false;
500    return handle;
501  } else {
502    void* handle = NativeBridgeLoadLibraryExt(path, RTLD_NOW, ns.get_native_bridge_ns());
503    if (handle == nullptr) {
504      *error_msg = NativeBridgeGetError();
505    }
506    *needs_native_bridge = true;
507    return handle;
508  }
509#else
510  UNUSED(env, target_sdk_version, class_loader, library_path);
511  *needs_native_bridge = false;
512  void* handle = dlopen(path, RTLD_NOW);
513  if (handle == nullptr) {
514    if (NativeBridgeIsSupported(path)) {
515      *needs_native_bridge = true;
516      handle = NativeBridgeLoadLibrary(path, RTLD_NOW);
517      if (handle == nullptr) {
518        *error_msg = NativeBridgeGetError();
519      }
520    } else {
521      *needs_native_bridge = false;
522      *error_msg = dlerror();
523    }
524  }
525  return handle;
526#endif
527}

2. loader不为null时做了什么?

a. findLibrary从所有的native目录中查找库文件,如果找到返回查找结果:

515    /**
516     * Finds the named native code library on any of the library
517     * directories pointed at by this instance. This will find the
518     * one in the earliest listed directory, ignoring any that are not
519     * readable regular files.
520     *
521     * @return the complete path to the library or {@code null} if no
522     * library was found
523     */
524    public String findLibrary(String libraryName) {
525        String fileName = System.mapLibraryName(libraryName);
526
527        for (NativeLibraryElement element : nativeLibraryPathElements) {
528            String path = element.findNativeLibrary(fileName);
529
530            if (path != null) {
531                return path;
532            }
533        }
534
535        return null;
536    }

b. findNativeLibrary查询当前nativeLibrary下面是否存在库文件,存在就返回:

781        public String findNativeLibrary(String name) {
782            maybeInit();
783
784            if (zipDir == null) {
785                String entryPath = new File(path, name).getPath();
786                if (IoUtils.canOpenReadOnly(entryPath)) {
787                    return entryPath;
788                }
789            } else if (urlHandler != null) {
790                // Having a urlHandler means the element has a zip file.
791                // In this case Android supports loading the library iff
792                // it is stored in the zip uncompressed.
793                String entryName = zipDir + '/' + name;
794                if (urlHandler.isEntryStored(entryName)) {
795                  return path.getPath() + zipSeparator + entryName;
796                }
797            }
798
799            return null;
800        }

c. 如果上述可以找到文件则执行doLoad加载:

1003        String libraryName = libname;
1004        if (loader != null) {
1005            String filename = loader.findLibrary(libraryName);
1006            if (filename == null) {
1007                // It's not necessarily true that the ClassLoader used
1008                // System.mapLibraryName, but the default setup does, and it's
1009                // misleading to say we didn't find "libMyLibrary.so" when we
1010                // actually searched for "liblibMyLibrary.so.so".
1011                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
1012                                               System.mapLibraryName(libraryName) + "\"");
1013            }
1014            String error = doLoad(filename, loader);
1015            if (error != null) {
1016                throw new UnsatisfiedLinkError(error);
1017            }
1018            return;
1019        }

 

以上参考:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android应用开发过程中,有时候我们需要实现全屏滚动的效果,或者在滚动页面中嵌入ListView等组件。下面就来介绍一下如何实现这些效果。 一、全屏滚动 实现全屏滚动需要用到Android系统提供的ScrollView组件。ScrollView可以包含多个子视图,并且可以在垂直方向上进行滚动。下面是一个简单的例子: ``` <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第一行"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第二行"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第三行"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第四行"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第五行"/> </LinearLayout> </ScrollView> ``` 上面的代码中,我们将ScrollView作为根布局,然后在ScrollView内部添加了一个垂直方向的LinearLayout,这个LinearLayout包含了多个TextView,每个TextView显示一行文本。运行这个应用,可以看到整个页面可以在垂直方向上滚动。 二、在滚动页面中嵌入ListView 有时候我们需要在滚动页面中嵌入ListView,这时候可以使用Android系统提供的NestedScrollView组件。NestedScrollView是ScrollView的子类,可以包含多个子视图,并且可以在垂直方向上进行滚动。和ScrollView不同的是,NestedScrollView可以嵌套其他可滚动的组件,例如ListView等。 下面是一个简单的例子: ``` <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第一行"/> <ListView android:layout_width="match_parent" android:layout_height="wrap_content" android:divider="@null" android:dividerHeight="0dp" android:scrollbars="none"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第三行"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第四行"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="这是第五行"/> </LinearLayout> </android.support.v4.widget.NestedScrollView> ``` 上面的代码中,我们将NestedScrollView作为根布局,然后在NestedScrollView内部添加了一个垂直方向的LinearLayout。这个LinearLayout包含了多个TextView和一个ListView。由于ListView也可以滚动,所以我们需要将它的滚动条隐藏掉,然后就可以在滚动页面中嵌入ListView了。 以上就是Android开发中如何实现全屏滚动和在滚动页面中嵌入ListView的方法。希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值