安卓-Kotlin-高级教程-六-

256 阅读26分钟

安卓 Kotlin 高级教程(六)

原文:Pro Android with Kotlin

协议:CC BY-NC-SA 4.0

十四、测试

关于信息技术中的测试已经说了很多。在过去的几十年中,测试获得了广泛的关注,这有三个原因。

  • 测试是开发者和用户之间的接口。

  • 测试在某种程度上是可以被设计的。

  • 测试有助于增加利润。

开发人员往往对他们的软件有偏见。这么说并没有冒犯的意思。很自然,如果你在某个主题上花了很多时间,你可能会失去预测新用户脑子里在想什么的能力。因此,强烈建议定期走出你的开发人员角色,问自己这样一个问题,“假设我对应用一无所知,如果我进入这个 GUI 工作流,它是否有意义,它是否容易遵循,是否很难犯不可恢复的错误?”测试对此有所帮助。它迫使开发人员扮演最终用户的角色,并问这个问题。

发展远不是一门工业工程科学。这是好消息也是坏消息。如果它有一个强大的工程路径,那么遵循一致同意的开发模式会更容易,其他开发人员也会更容易理解你在做什么。另一方面,不那么精确的可工程化也为更多的创造力打开了领域,并允许开发成为一门艺术。如今的测试倾向于优先考虑可工程性。这源于这样一个事实,你可以精确地说出软件应该做什么,完全不知道一行代码。因此,部分测试并不关心事情是如何在编码层面上完成的,从而消除了开发需求如何得到满足的过多可能性。这对于低级别的单元测试来说是不正确的,但是即使对于那些单元测试,你也可以看到软件工件契约和测试方法的强烈重叠。因此,测试可操作性的等级比单纯的开发要高一些。然而,因为测试只是开发过程的一个方面,作为一名开发人员,仍然有可能拥有一份有趣的工作,同时生活在两个世界中。你可以在开发代码时成为艺术家,在编写测试时成为工程师。

在开发链的另一端,根据您的意图,您可能希望让最终用户为您的应用花费一些钱。测试显然有助于避免因为你没有预料到的错误而产生的挫折感,让公众更容易购买你的应用。

关于 Android 的测试已经说了很多,你可以在 Android 官方文档中找到很好的信息和入门或高级视频。本章的其余部分应该被看作是关于测试问题的建议和经验知识的集合。我不打算介绍测试,涵盖它的每一个方面,但是我希望我可以给你一个你自己更深入研究的起点。

单元测试

单元测试针对类级别,测试应用的底层功能。我所说的“功能性”是指单元测试通常检查方法调用的输入和输出之间的确定性关系,可能但不一定以确定、直接的方式包括类实例的状态变量。

标准单元测试

在 Android 环境中,标准单元测试的运行不依赖于设备硬件或任何 Android 框架类,因此可以在开发机器上执行。

它们通常对库有用,而不是 GUI 相关的功能,这就是为什么这种单元测试的适用性在某种程度上对大多数 Android 应用是有限的。

然而,如果您的应用的类包含方法调用,并且您可以在给定各种输入集的情况下预测调用结果,那么使用标准单元测试是有意义的。将单元测试添加到您的应用中很容易。事实上,如果你使用 Android Studio 开始一个新项目,单元测试已经为你设置好了,你甚至会得到一个样本测试类,如图 14-1 所示。

img/463716_1_En_14_Fig1_HTML.jpg

图 14-1

初始单元测试设置

因此,您可以立即开始使用该测试类作为例子来编写单元测试;只需在源代码的test部分添加更多的测试类。

注意

虽然在技术上没有必要,但是一个常见的惯例是对测试类使用与被测试类相同的名称,加上Test。所以,com.example.myapp.TheClass的测试类应该叫做com.example.myapp.TheClassTest

要在 Android Studio 中运行单元测试,右键单击test部分并选择 Run Tests in 或 Debug Tests in。

使用存根 Android 框架的单元测试

默认情况下,用于执行单元测试的 Gradle 插件包含一个 Android 框架的存根版本,每当调用 Android 类时都会抛出异常。

您可以通过将以下内容添加到应用的build.gradle文件来更改此行为:

android {
  ...
  testOptions {
    unitTests.returnDefaultValues = true
  }
}

任何对 Android 类的方法的调用都不做任何事情,并根据需要返回null

模拟 Android 框架的单元测试

如果您需要从单元测试内部访问 Android 类,并期望它们做真实的事情,使用社区支持的 Robolectric 框架作为单元测试实现是一个有效的选择。使用 Robolectric,您可以模拟点击按钮、读写文本以及许多其他与 GUI 相关的活动。尽管如此,所有这些都在您的开发机器上运行,这大大加快了测试速度。

要允许您的项目使用 Robolectric,请将以下内容添加到您的应用的build.gradle文件中:

android {
  testOptions {
      unitTests {
          includeAndroidResources = true
      }
  }
}

dependencies {
  ...
  //testImplementation 'junit:junit:4.12'
  testImplementation "org.robolectric:robolectric:3.8"
}

例如,一个测试类模拟点击一个Button,然后检查点击动作是否更新了一个TextView,如下所示:

package com.example.robolectric

import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowApplication
import android.content.Intent
import android.widget.Button
import android.widget.TextView
import org.junit.Test
import org.robolectric.Robolectric
import org.junit.Assert.*

@RunWith(RobolectricTestRunner::class)
class MainActivityTest {
  @Test
  fun clickingGo_shouldWriteToTextView() {
      val activity = Robolectric.setupActivity(
            MainActivity::class.java!!)
      activity.findViewById<Button>(R.id.go).
            performClick()
      assertEquals("Clicked",
            activity.findViewById<TextView>(
            R.id.tv).text)
  }
}

通过右键单击test部分并选择 Run Tests in 或 Debug Tests in,您可以像任何普通的单元测试一样开始那个测试。

更多测试选项和详情,请参见 Robolectric 主页 www.robolectric.org

模拟单元测试

模仿意味着您让测试挂钩到 Android OS 函数的调用,并通过模仿它们的功能来模拟它们的执行。

如果你想在单元测试中包含模仿,Android 开发者文档建议你使用 Mockito 测试库。我建议更进一步,使用 PowerMock,它位于 Mockito 之上,但增加了更多功能,比如模仿静态或最终类。

要启用 PowerMock,请将以下内容添加到您的应用的build.gradle文件中(删除powermock:后的换行符):

android {
  ...
  testOptions {
      unitTests.returnDefaultValues = true
  }
}

dependencies {
  ...
  testImplementation ('org.powermock:
        powermock-mockito-release-full:1.6.1') {
      exclude module: 'hamcrest-core'
      exclude module: 'objenesis'
  }
  testImplementation 'org.reflections:reflections:0.9.11'
 }
}

不要删除或注释掉dependencies部分中的testImplementation 'junit:junit:4.12'行,因为仍然需要它。unitTests.returnDefaultValues = true条目负责单元测试的存根 Android 实现,以防万一,不抛出异常。reflections包用于扫描包来搜索测试类。

作为一个重要的例子,我给出了一个向数据库写入条目的活动。我们将模拟实际的数据库实现,但仍然希望确保创建必要的表并执行insert语句。该活动如下所示:

class MainActivity : AppCompatActivity() {

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

  fun save(v: View) {
      saveInDb(et.text.toString())
  }

  fun count(v: View) {
      val db = openOrCreateDatabase("MyDb",
            MODE_PRIVATE, null)
      with(db) {
          val resultSet = rawQuery(
                "Select * from MyItems", null)
          val cnt = resultSet.count
          Toast.makeText(this@MainActivity,
                "Count: ${cnt}", Toast.LENGTH_LONG).
                show()
      }
      db.close()
}

private fun saveInDb(item:String) {
    val tm = System.currentTimeMillis() / 1000
    val db = openOrCreateDatabase("MyDb",
          MODE_PRIVATE, null)
    with(db) {
        execSQL("CREATE TABLE IF NOT EXISTS " +
              "MyItems(Item VARCHAR,timestamp INT);")
        execSQL("INSERT INTO MyItems VALUES(?,?);",
              arrayOf(item, tm))
    }
    db.close()
  }
}

相应的布局文件如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android=
        "http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context="com.example.powermock.MainActivity"
  android:orientation="vertical">

  <EditText
      android:id="@+id/et"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text=""/>
  <Button
      android:id="@+id/btnSave"
      android:text="Save"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:onClick="save"/>
  <Button
      android:id="@+id/btnCount"
      android:text="Count"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:onClick="count"/>
</LinearLayout>

它包含一个 ID 为etEditText视图元素和两个调用活动的方法save()count()的按钮。

对于测试本身,在源代码的test部分创建一个类MainActivityTest。内容如下:

import android.database.sqlite.SQLiteDatabase
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatcher
import org.powermock.core.classloader.annotations.
      PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.mockito.BDDMockito.*
import org.mockito.Matchers
import org.powermock.reflect.Whitebox

@RunWith(PowerMockRunner::class)
@PrepareForTest(MainActivity::class)
class MainActivityTest {

@Test
fun table_created() {
    val activity = MainActivity()
    val activitySpy = spy(activity)
    val db = mock(SQLiteDatabase::class.java)

    // given
    given(activitySpy.openOrCreateDatabase(
          anyString(), anyInt(), any())).willReturn(db)

    // when
    Whitebox.invokeMethod<Unit>(
          activitySpy,"saveInDb","hello")

    // then
    verify(db).execSQL(Matchers.argThat(
          object : ArgumentMatcher<String>() {
        override
        fun matches(arg:Any):Boolean {
            return arg.toString().matches(
            Regex("(?i)create table.*\\bMyItems\\b.*"))
        }
    }))

}

    @Test
    fun item_inserted() {
        val activity = MainActivity()
        val activitySpy = spy(activity)
        val db = mock(SQLiteDatabase::class.java)

        // given
        given(activitySpy.openOrCreateDatabase(
              anyString(), anyInt(), any())).willReturn(db)

        // when
        Whitebox.invokeMethod<Unit>(
              activitySpy,"saveInDb","hello")

        // then
        verify(db).execSQL(Matchers.argThat(
              object : ArgumentMatcher<String>() {
            override
            fun matches(arg:Any):Boolean {
                return arg.toString().matches(
                Regex("(?i)insert into MyItems\\b.*"))
            }
        }), Matchers.argThat(
              object : ArgumentMatcher<Array<Any>>() {
            override
            fun matches(arg:Any):Boolean {
                val arr = arg as Array<Any>
                return arr[0] == "hello" &&
                      arr[1] is Number
            }
        }))
    }
}

@RunWith(PowerMockRunner::class)将确保 PowerMock 被用作单元测试运行程序,并且@PrepareForTest(MainActivity::class)准备了MainActivity类,所以即使它被标记为final(这是 Kotlin 默认的做法),它也可以被模拟。

第一个函数table_created()应该确保在必要时创建表。它的作用如下:

  • 我们实例化MainActivity,这是可能的,因为实例化不调用 Android 框架类。

  • 我们将MainActivity实例包装成一个间谍。这允许我们挂钩方法调用来模拟实际的实现。

  • 我们创建了一个对SQLiteDatabase的模拟,这样我们就可以在不实际使用真实数据库的情况下挂钩数据库操作。

  • 接下来的//given//when//then部分遵循 BDD 开发风格。

  • //given部分中,我们模拟出活动的openOrCreateDatabase()调用,并让它返回我们的模拟数据库。

  • //when内部,我们称活动类的private方法为saveInDb()。在测试开发中调用私有方法是不被允许的,但是在这里我们没有其他的机会,因为我们不能使用save()方法并让它在没有更复杂工作的情况下访问EditText视图。由于所有的模拟准备,这个调用到达了真实的 activity 类,但是将使用模拟的数据库而不是真实的数据库。

  • //then部分,我们可以检查saveInDb()的调用是否调用了适当的数据库操作来创建必要的表。为此,我们使用了一个ArgMatcher,它允许我们检查适当的方法调用参数。

测试函数item_inserted()做了几乎相同的事情,但是检查是否有合适的insert语句被发送到数据库。

在 Android Studio 中使用 PowerMock 作为 Kotlin 的单元测试运行程序有一个缺点:通常你可以使用的上下文菜单来运行所有的单元测试,但是由于某些原因,这对于 PowerMock 和 Kotlin 来说并不适用。作为一种变通方法,我将一个单独的测试类作为一个套件,调用它在包中可以找到的所有测试类。这就是我们在 Gradle 构建文件中添加testImplementation 'org.reflections:reflections:0.9.11'的原因。

@RunWith(TestAll.TestAllRunner::class)
class TestAll {
  class TestAllRunner(klass: Class<*>?,
        runners0: List<Runner>) :
        ParentRunner<Runner>(klass) {
      private val runners: List<Runner>

      constructor(clazz: Class<*>) : t
            his(clazz, listOf<Runner>()) {
      }

      init {
          val classLoadersList = arrayOf(
                  ClasspathHelper.contextClassLoader(),
                  ClasspathHelper.staticClassLoader())

          val reflections = Reflections(
                ConfigurationBuilder()
                  .setScanners(SubTypesScanner(false),
                        TypeAnnotationsScanner())
                  .setUrls(ClasspathHelper.
                        forClassLoader(
                        *classLoadersList))
                  .filterInputsBy(FilterBuilder().
                        include(FilterBuilder.
                        prefix(
                        javaClass.`package`.name))))

          runners = reflections.getTypesAnnotatedWith(
                RunWith::class.java).filter {
              clazz ->
              clazz.getAnnotation(RunWith::class.java).
                    value.toString().
                    contains(".PowerMockRunner")
          }.map { PowerMockRunner(it) }
      }

      override fun getChildren(): List<Runner> = runners

      override fun describeChild(child: Runner):
            Description = child.description

      override fun runChild(runner: Runner,
            notifier: RunNotifier) {
          runner.run(notifier)
      }
    }
}

这个类提供了自己的测试运行器实现,它使用init ...块中的reflections库来扫描包中的测试类。您现在可以在这个TestAll类上运行测试,它将依次运行它在包中找到的所有测试类。

集成测试

集成测试介于在开发机器上进行细粒度测试的单元测试和在真实或虚拟设备上运行的成熟的用户界面测试之间。集成测试也在设备上运行,但它们并不测试整个应用,而是在一个隔离的执行环境中测试选定的组件。

集成测试发生在源代码的androidTest部分。你还需要在应用的build.gradle文件中添加几个包,如下所示(去掉androidTestImplementation后的换行符):

dependencies {
  ...
  androidTestImplementation
      'com.android.support:support-annotations:27.1.1'
  androidTestImplementation
      'com.android.support.test:runner:1.0.2'
  androidTestImplementation
      'com.android.support.test:rules:1.0.2'
}

测试服务

要测试具有绑定的服务,请编写如下代码:

@RunWith(AndroidJUnit4::class)
class ServiceTest {

  // A @Rule wraps around the test invocation - here we
  // use the 'ServiceTestRule' which makes sure the
  // service gets started and stopped correctly.
  @Rule @JvmField
  val mServiceRule = ServiceTestRule()

  @Test
  fun testWithBoundService() {
      val serviceIntent = Intent(
          InstrumentationRegistry.getTargetContext(),
          MyService::class.java
      ).apply {
          // If needed, data can be passed to the
          // service via the Intent.
          putExtra("IN_VAL", 42L)
      }

      // Bind the service and grab a reference to the
      // binder.
      val binder: IBinder = mServiceRule.
            bindService(serviceIntent)

      // Get the reference to the service
      val service: MyService =
            (binder as MyService.MyBinder).getService()

      // Verify that the service is working correctly.
      assertThat(service.add(11,27), `is`(38))
  }
}

这用一个add(Int, Int)服务方法测试了一个名为MyService的简单服务。

class MyService : Service() {
  class MyBinder(val servc:MyService) : Binder() {
      fun getService():MyService {
          return servc
      }
  }
  private val binder: IBinder = MyBinder(this)

  override fun onBind(intent: Intent): IBinder = binder

  fun add(a:Int, b:Int) = a + b
}

要运行集成测试,右键单击源代码的androidTest部分,并选择 Run Tests in。这将创建并上传一个 APK 文件,通过InstrumentationRegistry.getTargetContext()创建一个集成测试上下文,然后在设备上运行测试。

测试意向服务

除了官方文档声明,基于IntentService类的服务也可以进行集成测试。您不能使用@Rule ServiceTestRule来处理服务生命周期,因为意向服务有自己的何时开始和停止的想法。但是你可以自己处理生命周期。作为一个例子,我给出了一个简单意向服务的测试,该服务工作 10 秒钟,并通过一个ResultReceiver不断发回数据。

服务本身如下所示:

class MyIntentService() :
    IntentService("MyIntentService") {
  class MyResultReceiver(val cb: (Double) -> Unit) :
        ResultReceiver(null) {
      companion object {
          val RESULT_CODE = 42
          val INTENT_KEY = "my.result.receiver"
          val DATA_KEY = "data.key"
      }
      override
      fun onReceiveResult(resultCode: Int,
            resultData: Bundle?) {
          super.onReceiveResult(resultCode, resultData)
          val d = resultData?.get(DATA_KEY) as Double
          cb(d)
      }
  }
  var status = 0.0
  override fun onHandleIntent(intent: Intent) {
      val myReceiver = intent.
              getParcelableExtra<ResultReceiver>(
                      MyResultReceiver.INTENT_KEY)
      for (i in 0..100) {
          Thread.sleep(100)
          val bndl = Bundle().apply {
              putDouble(MyResultReceiver.DATA_KEY,
                    i * 0.01)
          }
          myReceiver.send(MyResultReceiver.RESULT_CODE, bndl)
      }
  }
}

这里是测试类,也在源代码的androidTest部分:

@RunWith(AndroidJUnit4::class)
class MyIntentServiceTest {

  @Test
  fun testIntentService() {
      var serviceVal = 0.0

      val ctx = InstrumentationRegistry.
            getTargetContext()
      val serviceIntent = Intent(ctx,
            MyIntentService::class.java
      ).apply {
          `package`= ctx.packageName
          putExtra(
              MyIntentService.MyResultReceiver.
                    INTENT_KEY,
              MyIntentService.MyResultReceiver( { d->
                      serviceVal = d
              }))
      }
      ctx.startService(serviceIntent)

      val tm0 = System.currentTimeMillis() / 1000
      var ok = false
      while(System.currentTimeMillis() / 1000 - tm0
            < 20) {

      if(serviceVal == 1.0) {
          ok = true
          break
      }
      Thread.sleep(1000)
    }

    assertThat(ok, `is`(true))
  }
}

这个测试调用服务,监听一会儿它的结果,当它检测到服务按预期完成了它的工作时,让测试通过。

测试内容供应器

为了测试内容供应器,Android 提供了一个名为ProviderTestCase2的特殊类,它启动一个隔离的临时环境,因此测试不会干扰用户的数据。例如,一个测试用例如下所示:

@RunWith(AndroidJUnit4::class)
class MyContentProviderTest :
    ProviderTestCase2<MyContentProvider>(
    MyContentProvider::class.java,
    "com.example.database.provider.MyContentProvider") {

  @Before public override   // "public" necessary!
  fun setUp() {
      context = InstrumentationRegistry.
            getTargetContext()
      super.setUp()

      val mockRslv: ContentResolver = mockContentResolver
      mockRslv.delete(MyContentProvider.CONTENT_URI,
            "1=1", arrayOf())
  }

  @Test
  fun test_inserted() {
      val mockCtx: Context = mockContext
      val mockRslv: ContentResolver = mockContentResolver

      // add an entry
      val cv = ContentValues()
      cv.put(MyContentProvider.COLUMN_PRODUCTNAME,
            "Milk")
      cv.put(MyContentProvider.COLUMN_QUANTITY,
            27)
      val newItem = mockRslv.insert(
            MyContentProvider.CONTENT_URI, cv)

      // query all
      val cursor = mockRslv.query(
            MyContentProvider.CONTENT_URI,
            null, null, null)
      assertThat(cursor.count, `is`(1))

      cursor.moveToFirst()
      val ind = cursor.getColumnIndex(
            MyContentProvider.COLUMN_PRODUCTNAME)
      assertThat(cursor.getString(ind), `is`("Milk"))
  }
}

内容供应器对使用的列名、权限和 URI 有偏见。对于测试用例来说,重要的是使用模拟的内容解析器与内容提供者对话。

注意

注意setUp()中前两行的顺序。这和你在 2018 年 5 月起的安卓开发者文档里能读到的不一样。这里的医生是错的。

测试广播接收器

对于测试广播接收机,Android 测试框架并不特别关注。被测广播接收机实际做什么也很重要。假设它会产生某种副作用,例如向数据库中写入一些东西,那么您可以使用我们之前为内容提供者使用的相同测试上下文来模拟该数据库操作。

例如,如果您从androidTest源部分中查看下面的测试用例:

import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
import org.hamcrest.Matchers.*
import android.content.Intent

@RunWith(AndroidJUnit4::class)
class BroadcastTest {
  @Test
  fun testBroadcastReceiver() {
      val context = InstrumentationRegistry.
            getTargetContext()

      val intent = Intent(context,
            MyReceiver::class.java)
      intent.putExtra("data", "Hello World!")
      context.sendBroadcast(intent)

      // Give the receiver some time to do its work
      Thread.sleep(5000)

      // Check the DB for the entry added
      // by the broadcast receiver
      val db = MyDBHandler(context)
      val p = db.findProduct("Milk")
      assertThat(p, isA(Product::class.java))
      assertThat(p!!.productName, `is`("Milk"))
  }
}

你可以看到我们使用了由InstrumentationRegistry.getTargetContext()提供的上下文。这将确保广播接收机和稍后的测试所使用的数据库为其数据使用临时空间。

您可以像启动任何其他集成测试一样启动此测试,方法是右键单击它或它所在的包,然后选择 Run 或 Run Tests in。

用户界面测试

进行用户界面测试,你可以研究用户故事,看看你的应用作为一个整体是否像预期的那样运行。这是两个框架:

  • 表达

    使用 Espresso 编写针对您的应用的测试,忽略任何 interapp 活动。使用 Espresso,您可以做一些事情,例如当某个View ( ButtonTextViewEditText等)出现时,做一些事情(输入文本,执行点击),然后您可以检查是否出现一些后置条件。

  • UI 自动机

    使用 UI Automator 编写跨多个应用的测试。使用 UI Automator,您可以检查布局以找出活动的 UI 结构,模拟活动的动作,并检查 UI 元素。

有关如何使用它们的详细信息,请参考在线文档。例如,在您最喜欢的搜索引擎中输入 android 自动化 ui 测试来查找资源。

十五、故障排除

在前一章,我们讨论了测试你的应用的方法。如果测试失败,日志通常会告诉你到底发生了什么,如果这还不够,你可以扩展应用的日志来查看哪里出错了。

但是,即使有最好的测试概念,你的应用仍然有可能不像预期的那样运行。首先,从功能角度来看,它有时可能做不了正确的事情。其次,从非功能的角度来看,它可能表现不佳,这意味着随着时间的推移,它会耗尽内存资源,或者在速度方面表现不佳。

在这一章中,我们将讨论修复你的应用可能暴露的问题的技术。我们将讨论日志记录、调试和监控,以及 Android Studio 和 SDK 中的工具在这些主题方面对我们的帮助。

记录

登录 Android 很容易;您只需导入android.util.Log,并在代码中编写类似Log.e("LOG", "Message")的语句来发布日志消息。Android Studio 随后会帮助您收集、过滤和分析日志记录。

虽然使用这种日志功能进行开发非常方便,但是在发布应用时,就会出现问题。您不希望影响应用的性能,文档建议删除所有日志记录,这基本上等于否定了您在日志记录中所做的所有工作。如果您的用户后来报告了问题,您可以添加日志记录语句进行故障排除,在修复完成后再删除它们,以此类推。

为了纠正这个过程,我建议从一开始就在日志记录周围添加一个简单的包装器。

class Log {
  companion object {
      fun v(tag: String, msg: String) {
          randroid.util.Log.v(tag, msg)
      }

      fun v(tag: String, msg: String, tr: Throwable) {
          android.util.Log.v(tag, msg, tr)
      }

      fun d(tag: String, msg: String) {
          android.util.Log.d(tag, msg)
      }

      fun d(tag: String, msg: String, tr: Throwable) {
          android.util.Log.d(tag, msg, tr)
      }

      fun i(tag: String, msg: String) {
          android.util.Log.i(tag, msg)
      }

      fun i(tag: String, msg: String, tr: Throwable) {
          android.util.Log.i(tag, msg, tr)
      }

      fun w(tag: String, msg: String) {
          android.util.Log.w(tag, msg)
      }

      fun w(tag: String, msg: String, tr: Throwable) {
          android.util.Log.w(tag, msg, tr)
      }

      fun w(tag: String, tr: Throwable) {
          android.util.Log.w(tag, tr)
      }

      fun e(tag: String, msg: String) {
          android.util.Log.e(tag, msg)
      }

      fun e(tag: String, msg: String, tr: Throwable) {
          android.util.Log.e(tag, msg, tr)
      }
  }
}

然后,您可以使用与 Android 标准相同的简单日志记录符号,但是以后您可以自由地更改日志记录实现,而不需要修改其余的代码。例如,您可以添加一个简单的开关,如下所示:

class Log {
  companion object {
      val ENABLED = true

      fun v(tag: String, msg: String) {
          if(!ENABLED) return
          // <- add this to all the other statements
          android.util.Log.v(tag, msg)
      }
      ...
  }
}

或者,您可以只为虚拟设备启用日志记录。不幸的是,没有简单可靠的方法来确定你的应用是否运行在虚拟设备上。博客中介绍的所有解决方案都有其优点和缺点,并且可能会因新的 Android 版本而发生变化。相反,您可以做的是将构建变量传输到您的应用。为此,在您的应用的build.gradle文件中添加以下内容:

buildTypes {
    release {
        ...
        buildConfigField "boolean", "LOG", "false"
    }
    debug {
        ...
        buildConfigField "boolean", "LOG", "true"
    }
}

然后,在日志实现中,您只需替换以下内容:

val ENABLED = BuildConfig.LOG

它为调试 apk 打开日志记录,否则关闭日志记录。

使用完全不同的日志实现也是可能的。例如,要将日志切换到 Log4j,请在应用的build.gradle文件的dependencies部分添加以下内容(删除implementation后的换行符):

implementation
  'de.mindpipe.android:android-logging-log4j:1.0.3'
implementation
  'log4j:log4j:1.2.17'

要实际配置日志记录,请在您的自定义Log类中添加以下内容:

companion object {
  ...
  private val mLogConfigrator = LogConfigurator().apply {
      fileName = Environment.
          getExternalStorageDirectory().toString() +
          "/" + "log4j.log"
      maxFileSize = (1024 * 1024).toLong()
      filePattern = "%d - [%c] - %p : %m%n"
      maxBackupSize = 10
      isUseLogCatAppender = true
      configure()
  }

  private var ENABLED = true // or, see above
  // private var ENABLED = BuildConfig.LOG

  fun v(tag: String, msg: String) {
      if(!ENABLED) return
      Logger.getLogger(tag).trace(msg)
      // <- add similar lines to all the other
      // statements
  }
  ...
}

这个例子将日志写到由Environment.getExternalStorageDirectory()返回的目录中,这个目录在设备上通常映射到/sdcard。你也可以在其他地方这样做。如果您使用这里显示的外部存储器,不要忘记检查并可能获得适当的写权限!更准确地说,您需要在您的AndroidManifest.xml文件中包含以下内容:

<uses-permission android:name=
      "android.permission.WRITE_EXTERNAL_STORAGE"/>

一旦你的应用开始记录到设备内部的文件,你就可以通过使用文件浏览器从 Android Studio 内部轻松访问日志文件。通过查看➤工具 Windows ➤设备文件资源管理器启动它。然后双击打开日志文件,如图 15-1 所示。

img/463716_1_En_15_Fig1_HTML.jpg

图 15-1

访问设备上的日志文件

提高性能的最后一个方法是使用 lambdas 进行日志记录活动。为此,在您的自定义记录器中使用如下记录方法:

fun v(tag: String, msg: ()->String) {
      if(!ENABLED) return
      Logger.getLogger(tag).trace(msg.invoke())
}
... similar for the other statements

然后,在代码中发出如下日志消息:

Log.v("LOG",
      {-> "Number of items added = " + calculate()})

这种方法的优点是,如果未启用日志记录,则不会计算日志记录消息,从而为应用的生产版本增加了一些性能提升。

排除故障

从 Android Studio 内部调试就不多说了;它只是像预期的那样工作。

您在代码中设置断点,一旦程序流到达断点,您就可以单步执行程序的其余部分,并观察程序做什么以及变量如何更改它们的值。

性能监控

Android Studio 有一个非常强大的性能监视器,可以让您分析到方法级别的性能问题。要使用它,您必须首先找到一种方法,在循环中运行易受性能问题影响的那部分代码。您可以尝试使用测试来实现这一点,但是在代码中临时添加人工循环也是可行的。

然后,随着循环的运行,在 Android Studio 中打开视图➤工具 Windows ➤ Android Profiler。分析器首先抱怨高级分析没有启用,如图 15-2 所示。

img/463716_1_En_15_Fig2_HTML.jpg

图 15-2

高级分析警报

通过单击蓝色的运行配置链接来启用它。确保选中该框,如图 15-3 所示,然后点击确定。

img/463716_1_En_15_Fig3_HTML.jpg

图 15-3

高级分析设置

轮廓监视器随即出现,如图 15-4 所示。除了 CPU 分析,它还包含内存使用分析和网络监视器。

img/463716_1_En_15_Fig4_HTML.jpg

图 15-4

剖面仪通道

在那里,单击 CPU 通道将视图缩小到您在图 15-5 中看到的性能监视器图。

img/463716_1_En_15_Fig5_HTML.jpg

图 15-5

CPU 分析部分

滚动下方窗格中的线程,然后可以尝试查找可疑的线程。对于我在这里运行的示例,您可以看到 Thread-4 做了相当多的工作。将其重命名为 PiCalcThread(app 计算 pi)然后点击会显示一条信息,提示还没有捕获到数据,如图 15-6 所示。

img/463716_1_En_15_Fig6_HTML.jpg

图 15-6

CPU 分析线程

在窗格的顶部,可以看到捕捉控件,如图 15-7 所示。

img/463716_1_En_15_Fig7_HTML.jpg

图 15-7

CPU 分析捕获控件

对于我们即将开始的捕获,您可以从以下选项中进行选择:

  • Sampled (Java) :选择此项以定期捕获应用的调用堆栈。这是侵入性最小的捕捉方式,你通常会选择这个。

  • Instrumented (Java): 选择此项为应用中的每个方法调用的收集数据。这本身将引入高性能影响,并将收集大量数据。如果样本变量不能提供足够的信息,请选择此项。

  • Sampled (Native) :这个只能在 Android 8 (API 等级 26)开始的设备上使用。它将对本地调用进行采样。这深入到了 Android 的内部,你通常只会用这个进行深度分析。

一旦你选择了你的捕捉模式,点击红色的球开始捕捉。让捕获运行一段时间,然后结束它并开始分析收集的数据。Android Studio 为每个线程提供了不同的收集数据视图,并且各有千秋。一张火焰图见图 15-8 ,一张俯视图见图 15-9 。

img/463716_1_En_15_Fig9_HTML.jpg

图 15-9

自上而下的图表

img/463716_1_En_15_Fig8_HTML.jpg

图 15-8

火焰图

对于这个例子,浏览图表可以看到,在BigDecimal.divide()中消耗了相当多的 CPU 能力。为了提高这个例子的性能,您可以尽量避免过于频繁地调用这个方法,或者您可以尝试找到一个替代方法。

作为分析的额外辅助,您可以打开过滤器。点击控制器面板右侧的过滤符号,如图 15-10 所示。Android Studio 随后会高亮显示图表中匹配的条目,如图 15-11 所示。

img/463716_1_En_15_Fig11_HTML.jpg

图 15-11

分析过滤器已打开

img/463716_1_En_15_Fig10_HTML.jpg

图 15-10

分析过滤器

有关性能监控的更多信息和细节,请参见 Android Studio 的文档。

内存使用监控

除了分析应用的性能,如前一章所示,Android Studio 的分析器还可以帮助您找到内存泄漏或与内存管理不善相关的问题。同样,将有问题的代码部分放入一个循环中并启动它。通过查看➤工具 Windows ➤ Android Profiler 打开 profiler。选择内存通道,分析器会立即显示内存使用图,如图 15-12 所示。

img/463716_1_En_15_Fig12_HTML.jpg

图 15-12

内存监视器

运行一段时间后,您可以看到内存使用率上升。这是因为在示例应用中,我添加了一个人为的内存泄漏。参见图 15-13 。

img/463716_1_En_15_Fig13_HTML.jpg

图 15-13

更长时间的内存分析

要开始分析,请使用鼠标选择适当的区域。然后视图立即切换到使用统计视图,如图 15-14 所示。

img/463716_1_En_15_Fig14_HTML.jpg

图 15-14

内存分析,已选择

要找到漏洞,请从“按类排列”模式切换到“按调用堆栈排列”通过单击、双击和/或按 Enter 键进入树。最终结果可能如图 15-15 所示。

img/463716_1_En_15_Fig15_HTML.jpg

图 15-15

内存分析,详细信息

几乎有 40,000 个分配的橙色线属于run() (com.example.perfmonitor.A$go$1,这正是我放置内存泄漏的点。

class A {
  fun go(l:MutableList<String>) {
      Thread {
          while (true) {
              l.add("" + System.currentTimeMillis())
              Thread.sleep(1)
          }
      }.start()
  }
}

如果这还不足以解决内存问题,您可以获取一个堆转储。为此,单击 profiler 窗口标题中的堆转储符号,如图 15-16 所示。

img/463716_1_En_15_Fig16_HTML.jpg

图 15-16

进行堆转储

然后,您可以使用前面介绍的相同技术,或者将堆导出为 HPROF 文件,并使用其他工具来分析转储。要执行这样的导出,单击堆转储视图左上角的导出图标,如图 15-17 所示。

img/463716_1_En_15_Fig17_HTML.jpg

图 15-17

保存堆转储

注意

这样的堆转储允许您确定对象引用关系——这超出了 Android Studio 的内存分析。这使您能够最大限度地了解内存结构,但是需要一些时间来熟悉堆转储分析工具并找到正确的答案。

十六、分发应用

如果你已经完成了你的应用,你需要找到一种方法来分发它。实现这一目的的主要途径是 Google Play 商店,但如果你能说服你的用户允许从“其他来源”安装应用,也可以使用其他发布渠道我在这里没有给出发行渠道的列表,也没有给出使用 Google Play 商店的详细说明。有太多的选择取决于你的目标市场。此外,这本书并不打算成为应用营销的一般介绍。

你自己的应用商店

现在,设备允许用户从 Google Play 商店之外的来源安装应用,APK 文件可以从任何服务器上呈现,包括你自己的公司服务器。请注意,根据使用的 Android 版本,流程会有所不同。

  • 直到 Android 7 (API 级别 25),在“安全”部分有一个系统范围的设置,允许从 Google Play 以外的其他来源安装应用。

  • 从 Android 8 (API 级别 26)开始,从其他来源安装应用的权限是基于每个应用进行处理的,例如浏览器中的设置。

无论您选择哪种分销渠道,您都必须首先通过构建➤生成签名的 APK 来生成签名的 APK。然后,将其复制到服务器,并确保该文件被分配了 MIME 类型application/vnd.android.package-archive

注意

虽然 Android Studio 会自动将应用的调试版本上传到虚拟设备或通过 USB 连接的设备,但对于虚拟设备,您也可以测试签名的 APK 安装程序。如果您在本地开发机器上运行服务器,在虚拟设备内部使用 IP 10.0.2.2 连接到开发机器。最好先卸载开发构建过程中安装的版本。

然后,您可以使用设备的浏览器下载并安装 APK 文件。

谷歌 Play 商店

尽管这不是对如何使用 Google Play 商店的介绍,但这里有几个关于发行技术方面的附加要点:

  • 如前所述,你必须在你的应用发布到 Google Play 之前给它签名。

  • 在线文档建议在分发应用之前,删除应用内部的所有日志记录语句。作为破坏性较小的替代方案,遵循第十五章的说明,创建一个自定义记录器。

  • 如果你的应用使用数据库,当数据库模式改变时提供更新机制。见SQLiteOpenHelper类。如果你忘记了这一点,从一个版本到另一个版本更新应用和升级数据库会变得非常麻烦。

  • 可以为不同的设备分发不同的 apk。这一功能在本书中被忽略了,因为如今在现代设备中,一个应用的大小不再起重要作用,你通常可以将所有内容放入一个 APK 文件中。如果您仍然想提供多个 apk,请查阅在线文档。使用您最喜欢的搜索引擎搜索 android multiple apk 或类似产品。

  • 如果你在一个真实的设备上测试你的应用,如果你在你的设备上使用一个不同于你用来分发应用的谷歌账户,事情会变得简单一些。否则谷歌不会让你使用 Play store 安装你的应用。尽早这样做,因为以后更改设备的 Google 帐户可能会很复杂。

  • 本地化所有显示给用户的文本!在本书中,出于简洁的原因,没有使用本地化,但是您绝对应该为您的应用使用本地化。Android Studio 附带的 LINT checker 有助于发现本地化缺陷,使用模拟器附带的自定义区域设置切换器也可以让您进行检查。

  • 虽然只为智能手机外形(屏幕尺寸、分辨率)开发有点诱人,但你应该检查你的设计是否有其他外形。各种模拟器可以帮助你做到这一点。你至少应该在平板电脑上测试你的应用。

十七、即时应用

即时应用允许设备用户使用应用,而无需实际安装它们。在 Google Play 商店上,你有时会看到的“尝试”按钮会启动这样一个即时应用。

开发即时应用

要开发即时应用,在创建 Android 应用时可以使用一个开关,如图 17-1 所示。

img/463716_1_En_17_Fig1_HTML.jpg

图 17-1

添加即时应用功能

比方说你给项目取名instantapp;该向导创建了四个模块,如下所示:

  • 基地

    包含普通可安装应用变体和即时应用的基础。与许多博客所建议的相反,你不需要在这里放任何重要的东西。Android Studio 在这个模块中只创建了两个文件:一个是包含标记符baseFeature truebuild.gradle文件,另一个是添加组件和代码时不需要调整的非常基本的AndroidManifest.xml文件。由于构建文件,基本模块依赖于可安装应用和即时应用变体。

注意:对于干净的设计纯粹主义者来说,可安装应用和即时应用都依次依赖于基础模块,这听起来像是一种循环依赖。但是,这种依赖性不能理解为 Java 包依赖性!

  • app

    包含可安装应用的构建说明。这不包含代码,因为可安装应用和即时应用共享相同的代码基础,这将进入feature模块。

  • 即时应用

    包含即时应用的构建说明。这也不包含代码。

  • 功能

    可安装应用和即时应用的共享代码在这里。

截至 2018 年 5 月,向导的输出与 Google Play 商店的预期不匹配。为了避免以后推出即时应用时出现问题,请更改feature模块的AndroidManifest.xml文件,并为http方案添加另一个<data>元素,如下所示:

<data
    android:host="myfirstinstantapp.your server.com"
    android:pathPattern="/instapp"
    android:scheme="https"/>
<data
    android:scheme="http"/>

此外,意图过滤器必须添加属性android:autoVerify = "true"。Play store 会检查它,如果它丢失了,就会投诉。

其余的开发与正常的 Android 应用开发没有实质性的不同。只是运行即时 app 和你所知道的不一样。我们将在接下来的章节中讨论这一点。

在模拟器上测试即时应用

即时应用可以在模拟器上测试。为此,确保选择的运行配置显示instantapp,如图 17-2 所示。

img/463716_1_En_17_Fig2_HTML.jpg

图 17-2

运行配置

此外,如果您从按下灰色小三角时弹出的菜单中打开编辑配置,您应该会看到选择了 URL 启动方法,如图 17-3 所示。

img/463716_1_En_17_Fig3_HTML.jpg

图 17-3

发射方法

对于在仿真设备上运行,输入的 URL 是否存在并不重要,但它必须与来自模块featureAndroidManifest.xml的意图过滤器内的主机规范相匹配。否则,配置屏幕会报错。

警告

对于开发来说,添加一个android:port属性会导致问题。根据你的具体情况,当你想推出你的应用时,你可能需要一个,但是在开发期间不要使用它,或者把它注释掉!

构建部署工件

在推出即时应用之前,你必须为可安装应用和即时应用创建签名的 apk。

警告

两个变体和基本模块都需要有相同的版本信息,如它们的build.gradle文件所示。

为了创建部署工件,对appinstantapp进行两次构建➤生成签名的 APK。

可安装应用的部署工件通常是一个.apk文件,而即时应用是一个 zip 文件。

准备深层链接

深层链接是显示在网页或应用中的 URL,并链接到即时应用的功能。每当用户点击或点击一个 URL 时,借助于推出的即时应用的意图过滤器,相应的功能会立即下载并启动,而无需安装。

对于生产应用,连接到即时应用的 URL 必须存在,并且域的根必须有一个名为.well-known/assetlinks.json的文件。顺便说一句,谷歌验证你所指的域名存在,是你的。这个文件的结构在在线文档中有解释,但是 Android Studio 也有一个向导:进入工具➤应用链接助手。

如果手动生成文件assetlinks.json,则需要输入证书指纹。除非您已经拥有它,否则您可以通过以下方式获得它:

keytool -list -v -keystore my-release-key.keystore

这种文件的一个示例如下,其中指纹被裁剪:

[{
  "relation":
      ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example",
    "sha256_cert_fingerprints":
        ["14:6D:E9:83:C5:73:06...50"]
  }
}]

推出即时应用

要使用 Google Play 控制台推出即时应用,您必须创建一个新应用,然后推出可安装应用和即时应用。

在此过程中,Play console 将执行各种检查,以查看您的应用中的所有配置是否正确,它还将检查您的服务器是否如前一部分所述设置正确。

十八、命令行界面

在这一章中,我们总结了可以用来构建、管理和维护在 Android Studio 之外运行的任务的命令行工具。

注意

  • 这些工具在某种程度上是“半”官方的;您可能会在 SDK 文件夹中稍微不同的位置找到它们,并且在线文档不一定是最新的。

  • 这里显示的工具是 Linux 发行版的一部分。对于其他操作系统,提供了相应的版本,它们的用法将是相似的。

  • 该清单并不详尽;如果您安装了更少或更多的 SDK 包,它可能会与您的安装有所不同。

SDK 工具

表 18-1 描述了该文件夹中提供的与平台无关的工具:

表 18-1

SDK 工具

|

命令

|

描述

| | --- | --- | | apkanalyzer | 使用这个来分析你能找到的 APK 文件,例如,在PROJECT-DIR/PROJECT/release文件夹中(PROJECT经常读为app)。调用不带参数的命令,如下所示,将显示用法信息:./apkanalyzer | | archquery | 这是一个查询操作系统架构的简单工具。./archquery例如,这会输出x86_64。 | | avdmanager | 用这个来管理虚拟设备(AVD = Android 虚拟设备)。您可以创建、移动和删除设备,还可以列出设备、目标和 avd。调用不带参数的命令,如下所示,将显示用法信息:./avdmanager您可以在∼/.android/avd中找到该命令处理的虚拟设备的数据文件。用于创建设备的系统映像在SDK_INST/system-images中。 | | jobb | 使用它来管理不透明二进制 Blob (OBB)文件。这些是 APK 扩展文件,存放在外部存储器上,例如 SD 卡,只能从你的应用内部访问。调用不带参数的命令,如下所示,将显示用法信息:./job | | lint | 这是用于代码检查的 LINT 工具。调用不带参数的命令,如下所示,会显示用法信息。./lint | | monkeyrunner | 这是一个强大的测试工具,通过在你的电脑上使用 Python 脚本来控制 Android 应用。调用以下命令会显示使用信息:./monkeyrunner不带参数启动它会启动一个 Jython shell。你可以在第十四章中找到更多关于monkeyrunner的细节。 | | screenshot2 | 使用它从设备或模拟器中截取屏幕截图。调用不带参数的命令,如下所示,将显示用法信息:./screenshot2 | | sdkmanager | 这个工具帮助你管理 Android SDK 的包。您可以安装、卸载或更新 SDK 软件包,并且可以使用它来列出已安装和可用的软件包。援引./sdkmanager ––help显示详细的使用信息。例如,要列出已安装和可用的包,包括构建工具、平台、文档、源代码、系统映像和更多 SDK 组件,请调用:./sdkmanager ––list要安装新组件,该工具需要下载它们。存在几个标志;查看–help的输出,了解如何指定代理或禁用 HTTPS。 | | uiautomatorviewer | 这将打开 UI Automator GUI。./uiautomatorviewer更多信息见第十四章。 |

SDK_INST/tools/bin

这些工具侧重于虚拟设备、SDK 本身以及各种测试和工件管理任务的管理。

在父目录中,如下所示:

SDK_INST/tools

您会发现更多的工具。对它们的总结见表 18-2 。

表 18-2

更多 SDK 工具

|

命令

|

描述

| | --- | --- | | android | 已弃用。不带参数地调用它来查看概要。 | | emulator | 模拟器管理工具。我们在第一章谈到了模拟器。引起./emulator –help获取此命令的用法信息。 | | emulator-check | 用于主机系统的诊断工具。查看的输出./emulator-check –h为了一个概要。 | | mksdcard | 创建用作模拟器映像的 FAT32 映像。调用它时不带用法信息参数,如下所示:./mksdcard | | monitor | 启动图形设备监视器。这与从 Android Studio 的工具➤ Android ➤ Android 设备监视器中调用的是同一个设备监视器。注意,如果您在 Android Studio 的一个实例正在运行时运行这个命令,您可能会得到一个错误消息。 | | proguard | Proguard 程序驻留在这个目录中。使用 Proguard,您可以通过忽略文件、类、字段和方法来缩小 APK 文件。在在线文档中找到“压缩您的代码和资源”,了解 Proguard 是如何工作的。 |

SDK 构建工具

表 18-3 列出了该文件夹中提供的构建工具:

表 18-3

SDK 工具

|

命令

|

描述

| | --- | --- | | aapt | 这是 Android 素材打包工具。调用不带参数的命令,如下所示,将显示用法信息:./aapt该工具能够列出 APK 文件的内容,并从中提取信息。此外,它能够打包素材,添加和删除 APK 文件中的元素。该工具还负责创建 R 类,它将资源映射到代码内部可用的资源 id(Android Studio 会自动为您完成这项工作)。 | | aapt2 | 这是前面描述的aapt工具的后继者。调用不带参数的命令,如下所示,会显示一些基本的用法信息:./aapt2使用CMD调用compilelinkdumpdiffoptimizeversion中的任意一个./aapt2 CMD -h,会给出更详细的信息。在aapt命令的帮助下进行交叉检查提供了额外的帮助。 | | aarch64-linux-android-ld | Android 对象文件的特殊链接器,针对 64 位 ARM 架构的设备。调用该命令会显示详细的使用信息,如下所示:./aarch64-linux-android-ld ––help通常,如果你使用 Android Studio,你不必直接调用这个工具,因为它会为你处理编译和链接。 | | aidl | AIDL 是 Android 接口定义语言,处理不同应用的绑定服务类之间的低级进程间通信。aidl工具可用于将*.aidl接口定义文件编译成定义接口的 Java 语言接口文件。调用不带参数的命令会显示用法信息。./aidl | | apksigner | 管理 APK 文件签名。APK 文件需要签名才能发布。Android Studio 可以帮助你完成这个过程(参见构建➤生成签名的 APK ),但是你也可以使用这个工具。有关用法信息,请按如下方式调用它:./apksigner –h | | arm-linux-androideabi-ld | Android 目标文件的特殊链接器,目标是 32 位 ARM 架构的设备和 ABI 编译器生成的目标文件。调用该命令会显示详细的使用信息。./arm-linux-androideabi-ld ––help如果你使用 Android Studio,通常你不需要直接调用这个工具,因为它会为你处理编译和链接。 | | bcc_compat | Android 用于renderscript的一个 BCC 编译器。按如下方式调用命令会显示用法信息:./bcc_compat ––help | | dexdump | 一个工具,用于调查 APK 文件中包含类的 DEX 文件。如下所示,不带参数调用会显示用法信息:./dexdump | | dx | 一个管理 DEX 文件的工具。例如,您可以创建 DEX 文件或转储其内容。调用以下命令获取用法信息:./dx ––help | | i686-linux-android-ld | Android 对象文件的链接器,面向 x86 架构的设备。如此处所示,调用命令会显示详细的使用信息:./i686-linux-android-ld ––help通常,如果你使用 Android Studio,你不必直接调用这个工具,因为它会为你处理编译和链接。 | | llvm-rs-cc | renderscript 源代码编译器(脱机模式)。引起./llvm-rs-cc ––help查看一些使用信息。 | | mainDexClasses | 这是为了遗留应用希望允许命令dx上的–multi-dex,并使用com.android.multidex.installer库加载多个文件。mainDexClasses脚本将在–main-dex-list中提供给dx的文件内容。 | | mipsel-linux-android-ld | 一个 Android 对象文件的链接器,目标是具有 MIPS 架构的设备。调用命令./mipsel-linux-android-ld ––help显示详细的使用信息。通常,如果你使用 Android Studio,你不必直接调用这个工具,因为它会为你处理编译和链接。 | | split-select | 在给定目标设备配置的情况下,允许生成用于选择拆分 APK 的逻辑。调用命令./split-select ––help显示一些使用信息。 | | x86_64-linux-android-ld | Android 对象文件的链接器,面向具有 x86 64 位架构的设备。调用命令./x86_64-linux-android-ld --help显示详细的使用信息。通常,如果你使用 Android Studio,你不必直接调用这个工具,因为它会为你处理编译和链接。 | | zipalign | ZIP 对齐实用程序。开发人员不一定习惯的事实是,操作系统可能依赖于以某种方式对齐的归档文件的元素,例如,条目总是从 32 位边界开始。该工具可用于相应地修改 ZIP 文件。援引./zipalign –h显示使用信息。 |

SDK_INST/build-tools/[VERSION]

其中包括连接器、编译器、APK 文件工具和一个 Android 界面定义语言(AIDL)管理工具。

SDK 平台工具

表 18-4 描述了该文件夹中提供的平台相关工具:

表 18-4

SDK 平台工具

|

命令

|

描述

| | --- | --- | | adb | Android 调试桥。关于adb命令的描述,见表后的文字。 | | dmtracedump | 从跟踪转储创建图形化的调用堆栈图。跟踪文件必须是用android.os.Debug类获取的。不带参数地调用它,如下所示,以获取有关该命令的信息:./dmstracedump | | e2fsdroid | 挂载一个映像文件。目前已损坏。 | | etc1tool | 使用这个在 PNG 和 ETC1 图像格式之间转换。引起./etc1tool ––help查看使用信息。 | | fastboot | 这是您可以用来修改设备固件的快速启动程序。引起./fastboot ––help获取使用信息。 | | hprof-conv | 使用它将从 Android OS 工具获得的 HPROF 堆文件转换成标准的 HPROF 格式。不带参数地调用它,如下所示,以获取用法信息:./hprof-conv | | make_f2fs | 用来在某个设备上创建一个 F2FS 文件系统。如下所示,不带参数调用它以获取用法信息:./make_f2fs | | mke2fs | 生成 Linux 第二个扩展文件系统。不带参数调用它,如下所示,以查看选项:./mke2fs | | sload_f2fs | 用于将文件加载到 F2FS 设备中。不带参数调用它,如下所示,以查看选项:./sload_f2fs | | sqlite3 | 启动 SQLite 管理工具。如下所示调用它以获取用法信息:./sqlite3 –help | | systrace/ systrace.py | 研究 Android 系统的图形 Systrace 实用程序。工具adb所在的路径必须是PATH环境变量的一部分,并且必须安装 Python。然后你就可以跑了python systrace/systrace.py –h获取命令概要。 |

SDK_INST/platform-tools

adb命令调用的 Android Debug Bridge (ADB)是一个多功能工具,可以将您的开发 PC 连接到正在运行的仿真器和通过 USB 或 Wi-Fi 连接的设备。它由开发 PC 上的客户端和透明服务器进程以及设备上运行的守护程序组成。您可以使用adb进行以下操作:

  • 查询可访问设备

  • 安装和卸载应用(APK 文件)

  • 将文件复制到设备或从设备复制文件

  • 执行备份和恢复

  • 连接到应用的日志输出

  • 在设备上启用 root 访问权限

  • 在设备上启动 shell(例如,查看和研究应用的文件)

  • 开始和停止活动和服务

  • 发布广播

  • 启动和停止分析会话

  • 转储堆

  • 访问设备上的软件包管理器

  • 截图录视频

  • 重启设备。

有关更多详细信息,请在在线文档中找到“Android 调试桥”页面。通过以下方式调用它,以显示该命令提供的帮助:

./adb

例如,使用此列表列出连接的设备:

./adb devices

要打开设备上的 shell,请使用下面的命令,参数DEVICE_NAME是设备列表中第一列的条目之一:

./adb -s DEVICE_NAME shell

如果只有一个设备,在前面的命令中,您可以省略-s标志和设备名称。

注意

您必须在真实设备上启用调试,ADB 才能成功连接到这些设备。