注意JNI可能出现的异常

未检测异常

  本机能调用的许多 JNI 方法都会引起与执行线程相关的异常。当 Java 代码执行时,这些异常会造成执行流程发生变化,这样便会自动调用异常处理代码。当某个本机方法调用某个 JNI 方法时会出现异常,但检测异常并采用适当措施的工作将由本机来完成。一个常见的 JNI 缺陷是调用 JNI 方法而未在调用完成后测试异常。这会造成代码有大量漏洞以及程序崩溃。

  举例来说,考虑调用 GetFieldID() 的代码,如果无法找到所请求的字段,则会出现 NoSuchFieldError。如果本机代码继续运行而未检测异常,并使用它认为应该返回的字段 ID,则会造成程序崩溃。举例来说,如果 Java 类经过修改,导致 charField 字段不再存在,则清单 10 中的代码可能会造成程序崩溃 — 而不是抛出一个 NoSuchFieldError:

  清单 10. 未能检测异常


   jclass objectClass;
  jfieldID fieldID;
  jchar result = 0;
  objectClass = (*env)->GetObjectClass(env, obj);
  fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
  result = (*env)->GetCharField(env, obj, fieldID);

  添加异常检测代码要比在事后尝试调试崩溃简单很多。经常,您只需要检测是否出现了某个异常,如果是则立即返回 Java 代码以便抛出异常。然后,使用常规的 Java 异常处理流程处理它或者显示它。举例来说,清单 11 将检测异常:

  清单 11. 检测异常


   jclass objectClass;
  jfieldID fieldID;
  jchar result = 0;
  objectClass = (*env)->GetObjectClass(env, obj);
  fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
  if((*env)->ExceptionOccurred(env)) {
  return;
  }
  result = (*env)->GetCharField(env, obj, fieldID);

  不检测和清除异常会导致出现意外行为。您可以确定以下代码的问题吗?


     fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
  if (fieldID == NULL){
  fieldID = (*env)->GetFieldID(env, objectClass,"charField", "D");
  }
  return (*env)->GetIntField(env, obj, fieldID);

  问题在于,尽管代码处理了初始 GetFieldID() 未返回字段 ID 的情况,但它并未清除 此调用将设置的异常。因此,本机返回的结果会造成立即抛出一个异常。

  未检测返回值

  许多 JNI 方法都通过返回值来指示调用成功与否。与未检测异常相似,这也存在一个缺陷,即代码未检测返回值却假定调用成功而继续运行。对于大多数 JNI 方法来说,它们都设置了返回值和异常状态,这样应用程序更可以通过检测异常状态或返回值来判断方法运行正常与否。

  您可以确定以下代码的问题吗?


   clazz = (*env)->FindClass(env, "com/ibm/j9//HelloWorld");
  method = (*env)->GetStaticMethodID(env, clazz, "main",
  "([Ljava/lang/String;)V");
  (*env)->CallStaticVoidMethod(env, clazz, method, NULL);

  问题在于,如果未发现 HelloWorld 类,或者如果 main() 不存在,则本机将造成程序崩溃。

  未正确使用数组方法

  GetXXXArrayElements() 和 ReleaseXXXArrayElements() 方法允许您请求任何元素。同样,GetPrimitiveArrayCritical()、ReleasePrimitiveArrayCritical()、GetStringCritical() 和 ReleaseStringCritical() 允许您请求数组元素或字符串字节,以最大限度降低直接指向数组或字符串的可能性。这些方法的使用存在两个常见的缺陷。其一,忘记在 ReleaseXXX() 方法调用中提供更改。即便使用 Critical 版本,也无法保证您能获得对数组或字符串的直接引用。一些 JVM 始终返回一个副本,并且在这些 JVM 中,如果您在 ReleaseXXX() 调用中指定了 JNI_ABORT,或者忘记调用了 ReleaseXXX(),则对数组的更改不会被复制回去。

  举例来说,考虑以下代码:


   void modifyArrayWithoutRelease(JNIEnv* env, jobject obj, jarray arr1) {
  jboolean isCopy;
  jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);
  if ((*env)->ExceptionCheck(env)) return;
  buffer[0] = 1;
  }

  在提供直接指向数组的指针的 JVM 上,该数组将被更新;但是,在返回副本的 JVM 上则不是如此。这会造成您的代码在一些 JVM 上能够正常运行,而在其他 JVM 上却会出错。您应该始终始终包括一个释放(release)调用,如清单 12 所示:

  清单 12. 包括一个释放调用


   void modifyArrayWithRelease(JNIEnv* env, jobject obj, jarray arr1) {
  jboolean isCopy;
  jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);
  if ((*env)->ExceptionCheck(env)) return;
  buffer[0] = 1;
  (*env)->ReleaseByteArrayElements(env, arr1, buffer, JNI_COMMIT);
  if ((*env)->ExceptionCheck(env)) return;
  }

  第二个缺陷是不注重规范对在 GetXXXCritical() 和 ReleaseXXXCritical() 之间执行的代码施加的限制。本机可能不会在这些方法之间发起任何调用,并且可能不会由于任何原因而阻塞。未重视这些限制会造成应用程序或 JVM 中出现间断性死锁。

  举例来说,以下代码看上去可能没有问题:


     void workOnPrimitiveArray(JNIEnv* env, jobject obj, jarray arr1) {
  jboolean isCopy;
  jbyte* buffer = (*env)->GetPrimitiveArrayCritical(env, arr1, &isCopy);
  if ((*env)->ExceptionCheck(env)) return;
  processBufferHelper(buffer);
  (*env)->ReleasePrimitiveArrayCritical(env, arr1, buffer, 0);
  if ((*env)->ExceptionCheck(env)) return;
  }

  但是,我们需要验证在调用 processBufferHelper() 时可以运行的所有代码都没有违反任何限制。这些限制适用于在 Get 和 Release 调用之间执行的所有代码,无论它是不是本机的一部分。

  未正确使用全局引用

  本机可以创建一些全局引用,以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用,或者完全失去对它们的跟踪。考虑一个本机创建了全局引用,但是未删除它或将它存储在某处:


 lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {
  jobject gref = (*env)->NewGlobalRef(env, keepObj);
  }

  创建全局引用时,JVM 会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它 — 因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活动状态。在某些情况下,这会显著加剧内存泄漏。

  避免常见缺陷

  假设您编写了一些新 JNI 代码,或者继承了别处的某些 JVI 代码,如何才能确保避免了常见缺陷,或者在继承代码中发现它们?表 1 提供了一些确定这些常见缺陷的技巧:

  表 1. 确定 JNI 编程缺陷的清单

  未缓存触发数组副本错误界限过多回访使用大量本地引用使用错误的 JNIEnv未检测异常未检测返回值未正确使用数组未正确使用全局引用

  规范验证     X X  X

  方法跟踪X X X X   X  X X

  转储         X

  -verbose:jni     X

  代码审查X X X X X X X X X X

  您可以在开发周期的早期确定许多常见缺陷,方法如下:

  根据规范验证新代码

  分析方法跟踪

  使用 -verbose:jni 选项

  生成转储

  执行代码审查

  根据 JNI 规范验证新代码

  维持规范的限制列表并审查本机与列表的遵从性是一个很好的实践,这可以通过手动或自动代码分析来完成。确保遵从性的工作可能会比调试由于违背限制而出现的细小和间断性故障轻松很多。下面提供了一个专门针对新开发代码(或对您来说是新的)的规范顺从性检查列表:

  验证 JNIEnv 仅与与之相关的线程使用。

  确认未在 GetXXXCritical() 的 ReleaseXXXCritical() 部分调用 JNI 方法。

  对于进入关键部分的方法,验证该方法未在释放前返回。

  验证在所有可能引起异常的 JNI 调用之前都检测了异常。

  确保所有 Get/Release 调用在各 JNI 方法中都是相匹配的。

  IBM 的 JVM 实现包括开启自动 JNI 检测的选项,其代价是较慢的执行速度。与出色的代码单元测试相结合,这是一种极为强大的工具。您可以运行应用程序或单元测试来执行遵从性检查,或者确定所遇到的 bug 是否是由本机引起的。除了执行上述规范遵从性检查之外,它还能确保:

  传递给 JNI 方法的参数属于正确的类型。

  JNI 代码未读取超过数组结束部分之外的内容。

  传递给 JNI 方法的指针都是有效的。

  JNI 检测报告的所有结论并不一定都是代码中的错误。它们还包括一些针对代码的建议,您应该仔细阅读它们以确保代码功能正常。

  您可以通过以下命令行启用 JNI 检测选项:

 
   Usage: -Xcheck:jni:[option[,option[,...]]]
  all      check application and system classes
  verbose    trace certain JNI functions and activities
  trace     trace all JNI functions
  nobounds    do not perform bounds checking on strings and arrays
  nonfatal    do not exit when errors are detected
  nowarn     do not display warnings
  noadvice    do not display advice
  novalist    do not check for va_list reuse
  valist     check for va_list reuse
  pedantic    perform more thorough, but slower checks
  help      print this screen

  使用 IBM JVM 的 -Xcheck:jni 选项作为标准开发流程的一部分可以帮助您更加轻松地找出代码错误。特别是,它可以帮助您确定在错误线程中使用 JNIEnv 以及未正确使用关键区域的缺陷的根源。

  最新的 Sun JVM 提供了一个类似的 -Xcheck:jni 选项。它的工作原理不同于 IBM 版本,并且提供了不同的信息,但是它们的作用是相同的。它会在发现未符合规范的代码时发出警告,并且可以帮助您确定常见的 JNI 缺陷。

  分析方法跟踪

  生成对已调用本机方法以及这些本机方法发起的 JNI 回调的跟踪,这对确定大量常见缺陷的根源是非常有用的。可确定的问题包括:

  大量 GetFieldID() 和 GetMethodID() 调用 — 特别是,如果这些调用针对相同的字段和方法 — 表示字段和方法未被缓存。

  GetTypeArrayElements() 调用实例(而非 GetTypeArrayRegion())有时表示存在不必要的复制。

  在 Java 代码与本机代码之前来回快速切换(由时间戳指示)有时表示 Java 代码与本机代码之间的界限有误,从而造成性能较差。

  每个本机函数调用后面都紧接着大量 GetFieldID() 调用,这种模式表示并未传递所需的参数,而是强制本机回访完成工作所需的数据。

  调用可能抛出异常的 JNI 方法之后缺少对 ExceptionOccurred() 或 ExceptionCheck() 的调用表示本机未正确检测异常。

  GetXXX() 和 ReleaseXXX() 方法调用的数量不匹配表示缺少释放操作。

  在 GetXXXCritical() 和 ReleaseXXXCritical() 调用之间调用 JNI 方法表示未遵循规范施加的限制。

  如果调用 GetXXXCritical() 和 ReleaseXXXCritical() 之间相隔的时间较长,则表示未遵循 “不要阻塞调用” 规范所施加的限制。

  NewGlobalRef() 和 DeleteGlobalRef() 调用之间出现严重失衡表示释放不再需要的引用时出现故障。

  一些 JVM 实现提供了一种可用于生存方法跟踪的机制。您还可以通过各种外部工具来生成跟踪,比如探查器和代码覆盖工具。

  IBM JVM 实现提供了许多用于生成跟踪信息的方法。第一种方法是使用 -Xcheck:jni:trace 选项。这将生成对已调用的本机方法以及它们发起的 JNI 回调的跟踪。清单 13 显示某个跟踪的摘录(为便于阅读,隔开了某些行):

  清单 13. IBM JVM 实现所生成的方法跟踪


   Call JNI: java/lang/System.getPropertyList()[Ljava/lang/String; {
  00177E00  Arguments: void
  00177E00  FindClass("java/lang/String")
  00177E00  FindClass("com/ibm/oti/util/Util")
  00177E00  Call JNI: com/ibm/oti/vm/VM.useNativesImpl()Z {
  00177E00   Arguments: void
  00177E00   Return: (jboolean)false
  00177E00  }
  00177E00  Call JNI: java/security/AccessController.initializeInternal()V {
  00177E00   Arguments: void
  00177E00   FindClass("java/security/AccessController")
  00177E00   GetStaticMethodID(java/security/AccessController, "doPrivileged",
  "(Ljava/security/PrivilegedAction;)Ljava/lang/Object;")
  00177E00   GetStaticMethodID(java/security/AccessController, "doPrivileged",
  "(Ljava/security/PrivilegedExceptionAction;)Ljava/lang/Object;")
  00177E00   GetStaticMethodID(java/security/AccessController, "doPrivileged",
  "(Ljava/security/PrivilegedAction;Ljava/security/AccessControlContext;)
  Ljava/lang/Object;")
  00177E00   GetStaticMethodID(java/security/AccessController, "doPrivileged",
  "(Ljava/security/PrivilegedExceptionAction;
  Ljava/security/AccessControlContext;)Ljava/lang/Object;")
  00177E00   Return: void
  00177E00  }
  00177E00  GetStaticMethodID(com/ibm/oti/util/Util, "toString",
  "([BII)Ljava/lang/String;")
  00177E00  NewByteArray((jsize)256)
  00177E00  NewObjectArray((jsize)118, java/lang/String, (jobject)NULL)
  00177E00  SetByteArrayRegion([B@0018F7D0, (jsize)0, (jsize)30, (void*)7FF2E1D4)
  00177E00  CallStaticObjectMethod/CallStaticObjectMethodV(com/ibm/oti/util/Util,
  toString([BII)Ljava/lang/String;, (va_list)0007D758) {
  00177E00   Arguments: (jobject)0x0018F7D0, (jint)0, (jint)30
  00177E00   Return: (jobject)0x0018F7C8
  00177E00  }
  00177E00  ExceptionCheck()

  清单 13 中的跟踪摘录显示了已调用的本机方法(比如 AccessController.initializeInternal()V)以及本机方法发起的 JNI 回调。

  使用 -verbose:jni 选项

  Sun 和 IBM JVM 还提供了一个 -verbose:jni 选项。对于 IBM JVM 而言,开启此选项将提供关于当前 JNI 回调的信息。清单 14 显示了一个示例:

  清单 14. 使用 IBM JVM 的 -verbose:jni 列出 JNI 回调

  


<JNI GetStringCritical: buffer=0x100BD010> 
<JNI ReleaseStringCritical: buffer=100BD010> 
<JNI GetStringChars: buffer=0x03019C88> 
<JNI ReleaseStringChars: buffer=03019C88> 
<JNI FindClass: java/lang/String> 
<JNI FindClass: java/io/WinNTFileSystem> 
<JNI GetMethodID: java/io/WinNTFileSystem.<init> ()V> 
<JNI GetStaticMethodID: com/ibm/j9/offload/tests/HelloWorld.main ([Ljava/lang/String;)V> 
<JNI GetMethodID: java/lang/reflect/Method.getModifiers ()I> 
<JNI FindClass: java/lang/String> 
<JNI GETMETHODID: JAVA io WinNTFileSystem.

  对于 Sun JVM 而言,开启 -verbose:jni 选项不会提供关于当前调用的信息,但它会提供关于所使用的本机方法的额外信息。清单 15 显示了一个示例:

  清单 15. 使用 Sun JVM 的 -verbose:jni


   [Dynamic-linking native method java.util.zip.ZipFile.getMethod ... JNI]
  [Dynamic-linking native method java.util.zip.Inflater.initIDs ... JNI]
  [Dynamic-linking native method java.util.zip.Inflater.init ... JNI]
  [Dynamic-linking native method java.util.zip.Inflater.inflateBytes ... JNI]
  [Dynamic-linking native method java.util.zip.ZipFile.read ... JNI]
  [Dynamic-linking native method java.lang.Package.getSystemPackage0 ... JNI]
  [Dynamic-linking native method java.util.zip.Inflater.reset ... JNI]

  开启此选项还会让 JVM 针对使用过多本地引用而未通知 JVM 的情况发起警告。举例来说,IBM JVM 生成了这样一个消息:


   JVMJNCK065W JNI warning in FindClass: Automatically grew local reference frame capacity
  from 16 to 48. 17 references are in use.
  Use EnsureLocalCapacity or PushLocalFrame to explicitly grow the frame.

  虽然 -verbose:jni 和 -Xcheck:jni:trace 选项可帮助您方便地获取所需的信息,但手动审查此信息是一项艰巨的任务。一个不错的提议是,创建一些脚本或实用工具来处理由 JVM 生成的跟踪文件,并查看 警告。

  生成转储

  运行中的 Java 进程生成的转储包含大量关于 JVM 状态的信息。对于许多 JVM 来说,它们包括关于全局引用的信息。举例来说,最新的 Sun JVM 在转储信息中包括这样一行:


  JNI global references: 73

  通过生成前后转储,您可以确定是否创建了任何未正常释放的全局引用。

  您可以在 Unix® 环境中通过对 java 进程发起 kill -3 或 kill -QUIT 来请求转储。在 Windows® 上,使用 Ctrl+Break 组合键。

  对于 IBM JVM,使用以下步骤获取关于全局引用的信息:

  将 -Xdump:system:events=user 添加到命令行。这样,当您在 UNIX 系统上调用 kill -3 或者在 Windows 上按下 Ctrl+Break 时,JVM 便会生成转储。

  程序在运行中时会生成后续转储。

  运行 jextract -nozip core.XXX output.XML,这将会将转储信息提取到可读格式的 output.xml 中。

  查找 output.xml 中的 JNIGlobalReference 条目,它提供关于当前全局引用的信息,如清单 16 所示:

  清单 16. output.xml 中的 JNIGlobalReference 条目

  

<rootobject type="Thread" id="0x10089990" reachability="strong" /> 
<rootobject type="Thread" id="0x10089fd0" reachability="strong" /> 
<rootobject type="JNIGlobalReference" id="0x100100c0" reachability="strong" /> 
<rootobject type="JNIGlobalReference" id="0x10011250" reachability="strong" /> 
<rootobject type="JNIGlobalReference" id="0x10011840" reachability="strong" /> 
<rootobject type="JNIGlobalReference" id="0x10011880" reachability="strong" /> 
<rootobject type="JNIGlobalReference" id="0x10010af8" reachability="strong" /> 
<rootobject type="JNIGlobalReference" id="0x10010360" reachability="strong" /> 
<rootobject type="JNIGlobalReference" id="0x10081f48" reachability="strong" /> 
<rootobject type="StringTable" id="0x10010be0" reachability="weak" /> 
<rootobject type="StringTable" id="0x10010c70" reachability="weak" /> 
<rootobject type="StringTable" id="0x10010d00" reachability="weak" /> 
<rootobject type="StringTable" id="0x10011018" reachability="weak" /> 

  通过查看后续 Java 转储中报告的数值,您可以确定全局引用是否出现的泄漏。

  参见 参考资料 获取关于使用转储文件以及 IBM JVM 的 jextract 的更多信息。

  执行代码审查

  代码审查经常可用于确定常见缺陷,并且可以在各种级别上完成。继承新代码时,快速扫描可以发现各种问题,从而避免稍后花费更多时间进行调试。在某些情况下,审查是确定缺陷实例(比如未检查返回值)的唯一方法。举例来说,此代码的问题可能可以通过代码审查轻松确定,但却很难通过调试来发现:


   int calledALot(JNIEnv* env, jobject obj, jobject allValues){
  jclass cls = (*env)->GetObjectClass(env,allValues);
  jfieldID a = (*env)->GetFieldID(env, cls, "a", "I");
  jfieldID b = (*env)->GetFieldID(env, cls, "b", "I");
  jfieldID c = (*env)->GetFieldID(env, cls, "c", "I");
  jfieldID d = (*env)->GetFieldID(env, cls, "d", "I");
  jfieldID e = (*env)->GetFieldID(env, cls, "e", "I");
  jfieldID f = (*env)->GetFieldID(env, cls, "f", "I");
  }
  jclass getObjectClassHelper(jobject object){
  /* use globally cached JNIEnv */
  return cls = (*globalEnvStatic)->GetObjectClass(globalEnvStatic,allValues);
  }

  代码审查可能会发现第一个方法未正确缓存字段 ID,尽管重复使用了相同的 ID,并且第二个方法所使用的 JNIEnv 并不在应该在的线程上。

  结束语

  现在,您已经了解了 10 大 JNI 编程缺陷,以及一些用于在已有或新代码中确定它们的良好实践。坚持应用这些实践有助于提高 JNI 代码的正确率,并且您的应用程序可以实现所需的性能水平。

  有效集成已有代码资源的能力对于面向对象架构(SOA)和基于云的计算这两种技术的成功至关重要。JNI 是一项非常重要的技术,用于将非 Java 旧有代码和组件集成到基于 Java 的平台中,充当 SOA 或基于云的系统的基本元素。正确使用 JNI 可以加速将这些组件转变为服务的过程,并允许您从现有投资中获得最大优势。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本书是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。本书从三个方面来组织内容。第一,介绍Android开发者不容易掌握的一些知识点;第二,结合Android源代码和应用层开发过程,融会贯通,介绍一些比较深入的知识点;第三,介绍一些核心技术和Android的性能优化思想。 第1章 Activity的生命周期和启动模式 1 1.1 Activity的生命周期全面分析 1 1.1.1 典型情况下的生命周期分析 2 1.1.2 异常情况下的生命周期分析 8 1.2 Activity的启动模式 16 1.2.1 Activity的LaunchMode 16 1.2.2 Activity的Flags 27 1.3 IntentFilter的匹配规则 28 第2章 IPC机制 35 2.1 Android IPC简介 35 2.2 Android中的多进程模式 36 2.2.1 开启多进程模式 36 2.2.2 多进程模式的运行机制 39 2.3 IPC基础概念介绍 42 2.3.1 Serializable接口 42 2.3.2 Parcelable接口 45 2.3.3 Binder 47 2.4 Android中的IPC方式 61 2.4.1 使用Bundle 61 2.4.2 使用文件共享 62 2.4.3 使用Messenger 65 2.4.4 使用AIDL 71 2.4.5 使用ContentProvider 91 2.4.6 使用Socket 103 2.5 Binder连接池 112 2.6 选用合适的IPC方式 121 第3章 View的事件体系 122 3.1 View基础知识 122 3.1.1 什么是View 123 3.1.2 View的位置参数 123 3.1.3 MotionEvent和TouchSlop 125 3.1.4 VelocityTracker、GestureDetector和Scroller 126 3.2 View的滑动 129 3.2.1 使用scrollTo/scrollBy 129 3.2.2 使用动画 131 3.2.3 改变布局参数 133 3.2.4 各种滑动方式的对比 133 3.3 弹性滑动 135 3.3.1 使用Scroller 136 3.3.2 通过动画 138 3.3.3 使用延时策略 139 3.4 View的事件分发机制 140 3.4.1 点击事件的传递规则 140 3.4.2 事件分发的源码解析 144 3.5 View的滑动冲突 154 3.5.1 常见的滑动冲突场景 155 3.5.2 滑动冲突的处理规则 156 3.5.3 滑动冲突的解决方式 157 第4章 View的工作原理 174 4.1 初识ViewRoot和DecorView 174 4.2 理解MeasureSpec 177 4.2.1 MeasureSpec 177 4.2.2 MeasureSpec和LayoutParams的对应关系 178 4.3 View的工作流程 183 4.3.1 measure过程 183 4.3.2 layout过程 193 4.3.3 draw过程 197 4.4 自定义View 199 4.4.1 自定义View的分类 200 4.4.2 自定义View须知 201 4.4.3 自定义View示例 202 4.4.4 自定义View的思想 217 第5章 理解RemoteViews 218 5.1 RemoteViews的应用 218 5.1.1 RemoteViews在通知栏上的应用 219 5.1.2 RemoteViews在桌面小部件上的应用 221 5.1.3 PendingIntent概述 228 5.2 RemoteViews的内部机制 230 5.3 RemoteViews的意义 239 第6章 Android的Drawable 243 6.1 Drawable简介 243 6.2 Drawable的分类 244 6.2.1 BitmapDrawable 244 6.2.2 ShapeDrawable 247 6.2.3 LayerDrawable 251 6.2.4 StateListDrawable 253 6.2.5 LevelListDrawable 255 6.2.6 TransitionDrawable 256 6.2.7 Ins

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值