Android 开发中遇到的 bug(8)

前言

记录开发中遇到的 bug,不再让自己重复地被同样的 bug 折磨。

正文

1. 从 Java 转向 Kotlin,使用 Glide,但是 GlideApp 却无法生成

时间:2019年7月29日21:53:28
解决办法:
把 dependencies 中的

dependencies {
    implementation 'com.github.bumptech.glide:glide:4.9.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
}

替换为

apply plugin: 'kotlin-kapt'
dependencies {
    implementation 'com.github.bumptech.glide:glide:4.9.0'
    kapt 'com.github.bumptech.glide:compiler:4.9.0'
}

可以参考这个文档:https://kotlinlang.org/docs/reference/kapt.html。

2. 误用 Kotlin 中的 sortedWith() 方法排序,集合没有变化

时间:2019年8月4日14:17:06
问题描述:
看下边的小例子:

data class Man(val name: String, val age: Int, val type: Int)

fun main(args: Array<String>) {
    val list = mutableListOf<Man>()
    list.add(Man("wzc", 31,2))
    list.add(Man("wzj", 32,1))
    list.add(Man("wcx", 3,1))
    list.add(Man("wcg", 7,1))
    println("before sort")
    for (man in list) {
        println(man)
    }
    list.sortedWith(Comparator {lh, rh ->
        if (lh.type.compareTo(rh.type) == 0) {
            lh.age.compareTo(rh.age)
        } else {
            lh.type.compareTo(rh.type)
        }
    })
    println("after sort")
    for (man in list) {
        println(man)
    }
}

/*
打印结果:
before sort
Man(name=wzc, age=31, type=2)
Man(name=wzj, age=32, type=1)
Man(name=wcx, age=3, type=1)
Man(name=wcg, age=7, type=1)
after sort
Man(name=wzc, age=31, type=2)
Man(name=wzj, age=32, type=1)
Man(name=wcx, age=3, type=1)
Man(name=wcg, age=7, type=1)
 */

可以看到排序前后,打出的内容没有丝毫变化。
解决方法:
看一下 sortedWith 的代码:

/**
 * Returns a list of all elements sorted according to the specified [comparator].
 * 
 * The sort is _stable_. It means that equal elements preserve their order relative to each other after sorting.
 */
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
    if (this is Collection) {
       if (size <= 1) return this.toList()
       @Suppress("UNCHECKED_CAST")
       return (toTypedArray<Any?>() as Array<T>).apply { sortWith(comparator) }.asList()
    }
    return toMutableList().apply { sortWith(comparator) }
}

可以排序后的结果是在返回值里面。
修改代码:

data class Man(val name: String, val age: Int, val type: Int)

fun main(args: Array<String>) {
    val list = mutableListOf<Man>()
    list.add(Man("wzc", 31,2))
    list.add(Man("wzj", 32,1))
    list.add(Man("wcx", 3,1))
    list.add(Man("wcg", 7,1))
    println("before sort")
    for (man in list) {
        println(man)
    }
//    list.sortedWith(Comparator {lh, rh ->
//        if (lh.type.compareTo(rh.type) == 0) {
//            lh.age.compareTo(rh.age)
//        } else {
//            lh.type.compareTo(rh.type)
//        }
//    })
//    println("after sort")
//    for (man in list) {
//        println(man)
//    }
    val sortedWith = list.sortedWith(Comparator { lh, rh ->
        if (lh.type.compareTo(rh.type) == 0) {
            lh.age.compareTo(rh.age)
        } else {
            lh.type.compareTo(rh.type)
        }
    })
    list.clear()
    list.addAll(sortedWith)
    println("after sort")
    for (man in list) {
        println(man)
    }
}

/*
打印结果:
before sort
Man(name=wzc, age=31, type=2)
Man(name=wzj, age=32, type=1)
Man(name=wcx, age=3, type=1)
Man(name=wcg, age=7, type=1)
after sort
Man(name=wcx, age=3, type=1)
Man(name=wcg, age=7, type=1)
Man(name=wzj, age=32, type=1)
Man(name=wzc, age=31, type=2)
 */

可以看到,正常排序了。可以看到还有个 sortWith 方法:

expect fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit

二者的区别是:sortedWith() 方法可以通过 Iterable 对象调用,排序结果在返回值里;而 sortWith() 方法只能通过 MutableList 来调用,排序结果不在返回值里,而是直接在调用对象里了。sortedWith() 方法内部最终还是调用 sortWith() 方法来排序的。

3. java.lang.RuntimeException: Using WebView from more than one process at once with the same data directory is not supported. https://crbug.com/558377

时间:2019年8月9日15:35:31
问题描述:这个错误都是在 9.0 机子上出现的。
问题分析:查看 android 9.0 的行为变更文档:https://developer.android.google.cn/about/versions/pie/android-9.0-changes-28 。 在框架安全性变更部分可以看到如下内容:

如果您的应用必须在多个进程中使用 WebView 的实例,则必须先利用 WebView.setDataDirectorySuffix() 函数为每个进程指定唯一的数据目录后缀,然后再在该进程中使用 WebView 的给定实例。 该函数会将每个进程的网络数据放入其在应用数据目录内自己的目录中。
注:即使您使用 setDataDirectorySuffix(),系统也不会跨应用的进程界限共享 Cookie 以及其他网络数据。 如果应用中的多个进程需要访问同一网络数据,您需要自行在这些进程之间复制数据。 例如,您可以调用 getCookie() 和 setCookie(),在不同进程之间手动传输 Cookie 数据。

查看自己的应用,确实在多进程中使用了 WebView。
解决办法:在自定义的 Application 中为新的进程指定唯一的数据目录后缀

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        val processName = getCurProcessName()
        if (TextUtils.equals(processName, packageName)) {
             webviewSetPath(processName);
        } 
    }

    /**
     * 获取当前进程名称
     */
    private fun getCurProcessName(): String? {
        val pid = android.os.Process.myPid()

        val mActivityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        for (appProcess in mActivityManager
                .runningAppProcesses) {
            if (appProcess.pid == pid) {
                return appProcess.processName
            }
        }
        return null
    }

    fun webviewSetPath(processName: String?) {
        if (TextUtils.isEmpty(processName)) {
            return
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            WebView.setDataDirectorySuffix(processName);
        }
    }
}

4. net::ERR_CLEARTEXT_NOT_PERMITTED

时间:2019年8月10日19:08:40
问题描述:在 Android 9.0 手机上使用 WebView 访问页面出现

问题分析:
查看文档:

默认情况下启用网络传输层安全协议 (TLS)
如果您的应用以 Android 9 或更高版本为目标平台,则默认情况下 isCleartextTrafficPermitted() 函数返回 false。 如果您的应用需要为特定域名启用明文,您必须在应用的网络安全性配置中针对这些域名将 cleartextTrafficPermitted 显式设置为 true。

需要进行网络安全性配置。打开对应的文档:https://developer.android.google.cn/training/articles/security-config.html 。
配置如下:
在 res/xml 下,新建 network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="system"/>
            <certificates src="user"/>
        </trust-anchors>
    </base-config>
</network-security-config>

在清单文件中,application 节点下增加:

 <application
... 
 android:networkSecurityConfig="@xml/network_security_config"
... >

5. 使用 Room 报错:错误: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: slice)

时间:2019年8月10日19:38:12
问题描述:

@Dao
interface VideoDetailInfoDao {
    @Query("select status from videodetailinfo where video_task_id =:taskId and video_type = ${VideoInfo.TYPE_SLICE}")
    fun getSliceStatusList(taskId: Int): List<Byte>?
}

定位到这个查询语句出错,对应的 .java 是这样的:

@org.jetbrains.annotations.Nullable()
    @androidx.room.Query(value = "select status from videodetailinfo where video_task_id =:taskId and video_type = slice")
    public abstract java.util.List<java.lang.Byte> getSliceStatusList(int taskId);

解决办法:
查询 sql 资料:SQL 使用单引号来环绕文本值(大部分数据库系统也接受双引号)。如果是数值,请不要使用引号。
我应该是没有加单引号导致的。加上单引号重新运行正常:

@Dao
interface VideoDetailInfoDao {
    @Query("select status from videodetailinfo where video_task_id =:taskId and video_type = '${VideoInfo.TYPE_SLICE}'")
    fun getSliceStatusList(taskId: Int): List<Byte>?
}

6. Caused by: android.os.TransactionTooLargeException: data parcel size 1098756 bytes

时间:2019年8月12日18:57:30
问题描述:

08-12 18:57:30.513 9595-9595/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.omnipotent.free.videodownloader.pro, PID: 9595
    java.lang.RuntimeException: Failure from system
        at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1413)
        at android.app.ContextImpl.startService(ContextImpl.java:1379)
        at android.content.ContextWrapper.startService(ContextWrapper.java:581)
        at com.omnipotent.free.videodownloader.pro.ui.download.DownloadIntentService$Companion.start(DownloadIntentService.kt:36)
        at com.omnipotent.free.videodownloader.pro.ui.download.select.DownloadSelectDialog.startDownloadService(DownloadSelectDialog.kt:124)
        at com.omnipotent.free.videodownloader.pro.ui.download.select.DownloadSelectPresenter.clickDownload(DownloadSelectPresenter.kt:61)
        at com.omnipotent.free.videodownloader.pro.ui.download.select.DownloadSelectDialog$setupListeners$2.onClick(DownloadSelectDialog.kt:78)
        at android.view.View.performClick(View.java:5264)
        at android.view.View$PerformClick.run(View.java:21297)
        at android.os.Handler.handleCallback(Handler.java:743)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:150)
        at android.app.ActivityThread.main(ActivityThread.java:5621)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684)
     Caused by: android.os.TransactionTooLargeException: data parcel size 1098756 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(Binder.java:505)
        at android.app.ActivityManagerProxy.startService(ActivityManagerNative.java:3682)
        at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1397)
        at android.app.ContextImpl.startService(ContextImpl.java:1379) 
        at android.content.ContextWrapper.startService(ContextWrapper.java:581) 
        at com.omnipotent.free.videodownloader.pro.ui.download.DownloadIntentService$Companion.start(DownloadIntentService.kt:36) 
        at com.omnipotent.free.videodownloader.pro.ui.download.select.DownloadSelectDialog.startDownloadService(DownloadSelectDialog.kt:124) 
        at com.omnipotent.free.videodownloader.pro.ui.download.select.DownloadSelectPresenter.clickDownload(DownloadSelectPresenter.kt:61) 
        at com.omnipotent.free.videodownloader.pro.ui.download.select.DownloadSelectDialog$setupListeners$2.onClick(DownloadSelectDialog.kt:78) 
        at android.view.View.performClick(View.java:5264) 
        at android.view.View$PerformClick.run(View.java:21297) 
        at android.os.Handler.handleCallback(Handler.java:743) 
        at android.os.Handler.dispatchMessage(Handler.java:95) 
        at android.os.Looper.loop(Looper.java:150) 
        at android.app.ActivityThread.main(ActivityThread.java:5621) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684) 

问题分析:
我这边的使用场景是把一个集合 Parcelable 化,通过 Intent 传递给一个 Service。
查看 TransactionTooLargeException 类的文档:

The Binder transaction failed because it was too large.
During a remote procedure call, the arguments and the return value of the call are transferred as Parcel objects stored in the Binder transaction buffer. If the arguments or the return value are too large to fit in the transaction buffer, then the call will fail and TransactionTooLargeException will be thrown.
The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all transactions in progress for the process. Consequently this exception can be thrown when there are many transactions in progress even when most of the individual transactions are of moderate size.
There are two possible outcomes when a remote procedure call throws TransactionTooLargeException. Either the client was unable to send its request to the service (most likely if the arguments were too large to fit in the transaction buffer), or the service was unable to send its response back to the client (most likely if the return value was too large to fit in the transaction buffer). It is not possible to tell which of these outcomes actually occurred. The client should assume that a partial failure occurred.
The key to avoiding TransactionTooLargeException is to keep all transactions relatively small. Try to minimize the amount of memory needed to create a Parcel for the arguments and the return value of the remote procedure call. Avoid transferring huge arrays of strings or large bitmaps. If possible, try to break up big requests into smaller pieces.
If you are implementing a service, it may help to impose size or complexity contraints on the queries that clients can perform. For example, if the result set could become large, then don’t allow the client to request more than a few records at a time. Alternately, instead of returning all of the available data all at once, return the essential information first and make the client ask for additional information later as needed.

参考:https://mp.weixin.qq.com/s/v57mDRnSEZ6hl7-bc3ugwA 这篇文章讲得很好。
解决方案:我最后是先把数据存在单例里面,到 Service 里再去单例里面把数据取出来解决的。取出来之后再把数据清空,避免被单例持有。

7. \app\src\main\res\layout\dk_zxing_activity_scanner.xml:18: AAPT: error: ‘16sp’ is incompatible with attribute dkLabelTextSize (attr) float [weak].

时间:2019年8月29日19:26:28
问题描述:项目中的扫描条码功能直接集成了滴滴开源的哆啦A梦,同时也集成了哆啦A梦。测试反馈扫描页面的提示文字太小了,就去添加了对应的属性:

<com.view.ViewfinderView
app:dkLabelTextSize="16sp"/>

再去看获取的地方,

labelTextSize = array.getFloat(R.styleable.ViewfinderView_dkLabelTextSize, 36f);

就改成了

labelTextSize = array.getDimensionPixelSize(R.styleable.ViewfinderView_dkLabelTextSize, 36);

同时更改了 attrs.xml

    <declare-styleable name="ViewfinderView">
        <attr name="dkLabelTextSize" format="float" />
    </declare-styleable>

    <declare-styleable name="ViewfinderView">
        <attr name="dkLabelTextSize" format="dimension" />
    </declare-styleable>

运行后,就报出了上面的错误:AAPT: error: ‘16sp’ is incompatible with attribute dkLabelTextSize (attr) float [weak]
问题分析:仔细看一下,这句报错的意思,16sp 和 dkLabelTextSize 这个属性不兼容,然后这个属性是 float 的。float 是哪里来的?明明我已经改掉了。但是哆啦A梦里面确实有一个的。

接着,就把自己项目里的 ViewfinderView 更名为 ViewfinderView1,重新编译,却报出了新的错误:

  G:\AndroidWorkspaces\VideoDownload_Pro\app\build\intermediates\incremental\mergeDebuggerDebugResources\merged.dir\values\values.xml:7367: AAPT: error: duplicate value for resource 'attr/dkLabelTextSize' with config ''.
      
G:\AndroidWorkspaces\VideoDownload_Pro\app\build\intermediates\incremental\mergeDebuggerDebugResources\merged.dir\values\values.xml:7367: AAPT: error: resource previously defined here.

这里面是说,重复定义之前使用过的属性。打开提到的位置,确实可以看到重复定义:

   <declare-styleable name="ViewfinderView"><attr format="color" name="dkCornerColor"/><attr format="color" name="dkLaserColor"/><attr format="color" name="dkFrameColor"/><attr format="color" name="dkMaskColor"/><attr format="color" name="dkResultPointColor"/><attr format="color" name="dkResultColor"/><attr format="color" name="dkLabelTextColor"/><attr format="string" name="dkLabelText"/><attr format="float" name="dkLabelTextSize"/></declare-styleable>
    <declare-styleable name="ViewfinderView1">
        <attr format="color" name="dkCornerColor"/>
        <attr format="color" name="dkLaserColor"/>
        <attr format="color" name="dkFrameColor"/>
        <attr format="color" name="dkMaskColor"/>
        <attr format="color" name="dkResultPointColor"/>
        <attr format="color" name="dkResultColor"/>
        <attr format="color" name="dkLabelTextColor"/>
        <attr format="string" name="dkLabelText"/>
        <attr format="dimension" name="dkLabelTextSize"/>

下面就把属性名,也一并改掉:

    <declare-styleable name="ViewfinderView1">
        <attr name="omniCornerColor" format="color" />
        <attr name="omniLaserColor" format="color" />
        <attr name="omniFrameColor" format="color" />
        <attr name="omniMaskColor" format="color" />
        <attr name="omniResultPointColor" format="color" />
        <attr name="omniResultColor" format="color" />
        <attr name="omniLabelTextColor" format="color" />
        <attr name="omniLabelText" format="string" />
        <attr name="omniLabelTextSize" format="dimension" />
    </declare-styleable>

同步更新使用到的地方,编译通过了。
从这里得出一点:尽量不要使用相同的属性名字。

8. 使用 ktlint 来检查代码,检查出 Wildcard import

时间:2019年8月30日20:23:34
问题描述:看到 android-sunflower 里集成了 ktlint,这边项目里也集成了一下。可是检测出了 星号导入 的问题。这边把星号导入改成直接导入名字那种,编译器又自动变成了星号导入。
解决办法:查看 https://stackoverflow.com/questions/3348816/intellij-never-use-wildcard-imports 。我的 as 是 windows 版的。打开 Settings -> Editor -> Kotlin,Imports 选项,下面都勾选第一项,默认是最后一项。反正就是把里面可以星号导入的都去掉。可以看下面的截图:
在这里插入图片描述

9. 使用 GreenDao,自增长主键(@Id(autoincrement = true))却报错

时间:2019年8月30日20:53:30
问题描述:这是同事问我的问题,就是一个对象使用自增长主键,来插入数据表。

@Id(autoincrement = true)
private long   id;

但却报出:

android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: WEATHER._id (code 1555)
in dao.insert(weather)

刚开始,在捋他的代码流程。花了不少时间。最后还是定位下是自增主键的问题。
解决办法:搜索 “greendao 自增主键报错”,“greendao autoincrement error”,迅速找到了与我们的错误一样的案例。并且在 greenDao 的 github 上,也找到了对应的 issue:https://github.com/greenrobot/greenDAO/issues/441
就是把 long 改成 Long,

  @Id(autoincrement = true)
  private Long id;

总结一下,是解决问题思路的问题,应该先去官网上查询 issue 的。走了不少弯路。真是的。

10. Kotlin 中出现 java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter savedInstanceState

时间:2019年9月2日20:01:46
问题描述:把一个原来是 .java 的类转成 .kt 的报出这个错误:

class EmptyDownDialog(context: Context) : Dialog(context, R.style.dialog) {

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.pop_no_download)
    }
}

问题分析:再仔细看一下日志提供的信息:参数 savedInstanceState 被指定为 non-null,是 null 的。
看一下 onCreate 方法的参数 savedInstanceState 确实是指定为 non-null 的,它实际上是可以为 null 的。这样把一个 null 赋值给 non-null 的变量,就报出了这个错误。
解决办法:在 Bundle 后面加上 ?,表明 savedInstanceState 是可空的。解决了这个问题。

class EmptyDownDialog(context: Context) : Dialog(context, R.style.dialog) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.pop_no_download)
    }
}

最后

代码出错了,关键是要仔细查看日志。能够仔细地查看日志,就离解决问题很近了。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: Android底层开发指的是在Android操作系统对底层系统组件进行开发和调试的过程。底层开发需要对操作系统的内部机制、底层接口和系统调用有深入的了解。 在Android底层开发,需要掌握Linux内核、C/C++编程语言以及JNI(Java Native Interface)等技术。开发者需要了解Linux内核的机制和原理,以便能够理解和分析Android系统在底层运行时的行为和逻辑。 在底层开发,还需要通过C/C++编程语言来编写底层库和驱动程序,与硬件进行交互和通信。这些底层库和驱动程序负责实现Android系统各个组件的底层功能,如图形显示、音频处理、网络通信等。 另外,JNI技术用于在Java层和底层库之间进行交互。通过JNI,可以调用底层库的函数和方法,实现Java层与底层的数据传递和功能调用。 底层开发经常会涉及到调试和性能优化的工作。开发者需要使用调试工具来分析和追踪底层代码的执行过程,以及解决底层开发遇到的问题和bug。性能优化方面,可以通过调整底层代码和参数来提高系统的性能和响应能力。 总之,Android底层开发是一项需要对操作系统原理和底层技术有深入了解的工作。通过学习和实践,开发者可以掌握底层开发技术,为Android系统的功能和性能做出贡献。 ### 回答2: Android底层开发是指在Android操作系统直接与硬件进行交互的开发工作。在Android底层开发开发者需要熟悉Linux内核、硬件驱动程序以及底层库等关键技术。 Android操作系统基于Linux内核,因此熟悉Linux内核是进行Android底层开发的基础。开发者需要了解Linux内核的基本原理和结构,以便理解Android系统底层的运行机制。 硬件驱动程序是Android底层开发的重要组成部分,它们负责将硬件设备与Android系统进行连接和通信。开发者需要掌握硬件驱动的编写和调试技巧,以确保硬件设备在Android系统能够正常工作。 底层库是Android底层开发的另一个关键技术。Android提供了一系列的底层库,用于实现底层功能,比如图形处理、网络通信、多媒体播放等。开发者需要熟悉这些库的使用方法和原理,以便在底层开发进行功能的实现和调试。 Android底层开发通常涉及到一些高级的编程语言,比如C/C++。开发者需要熟练掌握这些编程语言,以便能够编写出高效和稳定的底层代码。 总之,Android底层开发是一项复杂而庞大的工作,需要开发者具备扎实的技术基础和深厚的理论知识。只有掌握了底层开发所需的关键技术,开发者才能够在Android系统开发出高效、稳定和功能丰富的应用程序。 ### 回答3: Android底层开发是指在Android操作系统上进行系统级别的功能开发Android底层开发主要包括四个方面的内容:内核开发、HAL(硬件抽象层)开发、驱动程序开发和底层库开发。 首先,内核开发是指对Android系统内核进行修改和优化,以满足特定需求,并提供更好的性能和稳定性。内核开发需要对操作系统的核心组件进行深入研究和理解,包括进程管理、内存管理、文件系统等。 其次,HAL开发通过编写硬件抽象层的代码,将硬件和操作系统进行连接。这样做的目的是为了让Android系统能够支持不同品牌和型号的硬件设备,如传感器、摄像头、显示屏等。HAL开发需要理解硬件设备的工作原理和规范,并编写对应的接口和驱动程序。 驱动程序开发是为了让Android系统能够正确地识别和使用硬件设备。驱动程序是一种特殊的软件,用于与硬件设备进行通信和控制。驱动程序开发需要具备底层编程语言的知识,如C或C++,以及硬件设备的技术规范和接口协议。 最后,底层库开发是指编写和优化Android系统底层的库文件,以提供一些基础的功能和服务,如网络通信、图形显示、音频处理等。底层库开发需要对操作系统的功能和架构有深入了解,并具备编程技巧和算法优化的能力。 总之,Android底层开发需要深入理解操作系统和硬件设备的工作原理,具备底层编程语言的知识和技能,以及良好的系统分析和优化能力。通过进行Android底层开发开发者能够深入了解Android系统的运行原理,提升系统性能,并实现定制化和优化的功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值