術語表

最后更新于:2022-04-01 01:47:50

### [Activity](#) An activity represents a single screen with a user interface. - [5.1.1. 使得网络服务可发现](#) - [5.1.2. 使用WiFi建立P2P连接](#) - [5.2.1. 连接到网络](#) - [5.2.2. 管理网络](#) - [5.2.3. 解析XML数据](#) - [5.6.3. 创建Sync Adpater](#) - [5.7.2. 建立请求队列](#) - [5.7.3. 创建标准的网络请求](#) - [5.7.1. 发送简单的网络请求](#) - [0. 序言](#) - [4.3.3. 展示卡片翻转动画](#) - [4.3.1. View间渐变](#) - [4.3.5. 布局变更动画](#) - [4.3.2. 使用ViewPager实现屏幕侧滑](#) - [13.2.2. 处理查询的结果](#) - [13.2. 使用CursorLoader在后台加载数据](#) - [13.2.1. 使用CursorLoader执行查询任务](#) - [13.1.1. 创建IntentService](#) - [13.1.3. 报告后台任务执行状态](#) - [13.1.2. 发送工作任务到IntentService](#) - [13.3.1. 保持设备的唤醒](#) - [1.2.2. 添加Action按钮](#) - [1.2. 添加ActionBar](#) - [1.2.4. ActionBar的覆盖层叠](#) - [1.2.1. 建立ActionBar](#) - [1.2.3. 自定义ActionBar的风格](#) - [1.4. 管理Activity的生命周期](#) - [1.4.2. 暂停与恢复Activity](#) - [1.4.4. 重新创建Activity](#) - [1.4.1. 启动与销毁Activity](#) - [1.4.3. 停止与重启Activity](#) - [1.6.1. 保存到Preference](#) - [1.1.3. 建立简单的用户界面](#) - [1.1.1. 创建Android项目](#) - [1.1.4. 启动其他的Activity](#) - [1.5.3. Fragments之间的交互](#) - [1.5.1. 创建一个Fragment](#) - [1.5.2. 建立灵活动态的UI](#) - [1.5. 使用Fragment建立动态的UI](#) - [1. Android入门基础:从这里开始](#) - [1.7.3. Intent过滤](#) - [1.7. 与其他应用的交互](#) - [1.7.2. 接收Activity返回的结果](#) - [1.7.1. Intent的发送](#) - [1.3.3. 适配不同的系统版本](#) - [16. Android测试程序](#) - [6.1.3. 使用Intents修改联系人信息](#) - [6.1.2. 获取联系人详情](#) - [6.1.1. 获取联系人列表](#) - [2.3.2. 接收其他设备的文件](#) - [2.3.1. 发送文件给其他设备](#) - [2.2.3. 请求分享一个文件](#) - [2.2.2. 分享文件](#) - [2.1.2. 接收从其他App返回的数据](#) - [2.1.1. 给其他App发送简单的数据](#) - [4.1.3. 缓存Bitmap](#) - [4.1.5. 在UI上显示Bitmap](#) - [4.2.1. 建立OpenGL ES的环境](#) - [12.3.1. 处理控制器输入动作](#) - [12.1.1. 检测常用的手势](#) - [12.1.2. 跟踪手势移动](#) - [12.1.4. 处理多触摸手势](#) - [12.1.6. 管理ViewGroup中的触摸事件](#) - [12.2.4. 处理按键动作](#) - [12.2.2. 处理输入法可见性](#) - [6.2.3. 显示位置地址](#) - [6.2.4. 创建和监视地理围栏](#) - [6.2.1. 获取最后可知位置](#) - [6.2.2. 获取位置更新](#) - [11.6.7. 维护兼容性](#) - [11.6.6. 自定义动画](#) - [11.6.1. 开始使用Material Design](#) - [11.6. 创建使用Material Design的应用](#) - [11.6.3. 创建Lists与Cards](#) - [11.6.4. 定义Shadows与Clipping视图](#) - [11.6.2. 使用Material的主题](#) - [3.1.1. 控制音量与音频播放](#) - [3.2.1. 简单的拍照](#) - [3.2.2. 简单的录像](#) - [3.3.3. 打印自定义文档](#) - [3.3.2. 打印HTML文档](#) - [14.3.2. 使用include标签重用layouts](#) - [14.1. 管理应用的内存](#) - [14.6. 避免出现程序无响应ANR](#) - [15.3. 为防止SSL漏洞而更新Security](#) - [15.1. Security Tips](#) - [16.1.2. 创建与执行测试用例](#) - [16.1.5. 创建功能测试](#) - [16.1.3. 测试UI组件](#) - [16.1.4. 创建单元测试](#) - [16.1. 测试你的Activity](#) - [16.1.1. 建立测试环境](#) - [8.3.3. 使用TV应用进行搜索](#) - [8.3.2. 使得TV App能够被搜索](#) - [8.2.1. 创建目录浏览器](#) - [8.2.3. 创建详情页](#) - [8.2.4. 显示正在播放卡片](#) - [8.6. TV Apps Checklist](#) - [8.1.2. 处理TV硬件部分](#) - [8.1.3. 创建TV的布局文件](#) - [8.1.1. 创建TV应用的第一步](#) - [11.4.2. 开发辅助服务](#) - [11.3.1. 抽象新的APIs](#) - [11.3.2. 代理至新的APIs](#) - [11.3.3. 使用旧的APIs实现新API的效果](#) - [11.3.4. 使用版本敏感的组件](#) - [11.2.4. 优化自定义View](#) - [11.1.3. 实现可适应的UI](#) - [11.5.2. 隐藏系统Bar](#) - [11.5.4. 全屏沉浸式应用](#) - [11.5.3. 隐藏导航Bar](#) - [10.5.1. 为App内容开启深度链接](#) - [10.5. 使得你的App内容可被Google搜索](#) - [10.1.2. 为多种大小的屏幕进行规划](#) - [10.2.3. 提供向上的导航](#) - [10.2.5. 实现向下的导航](#) - [10.2. 实现高效的导航](#) - [10.2.1. 使用Tabs创建Swipe视图](#) - [10.2.2. 创建抽屉导航](#) - [10.2.4. 提供向后的导航](#) - [10.3.1. 建立Notification](#) - [10.3.4. 使用BigView风格](#) - [10.3. 通知提示用户](#) - [10.3.2. 当启动Activity时保留导航](#) - [10.3.5. 显示Notification进度](#) - [10.4. 增加搜索功能](#) - [10.4.2. 保存并搜索数据](#) - [10.4.1. 建立搜索界面](#) - [7.2.1. 创建并运行可穿戴应用](#) - [7.2. 创建可穿戴的应用](#) - [7.2.2. 创建自定义的布局](#) - [7.2.3. 添加语音功能](#) - [7.4.2. 同步数据单元](#) - [7.4.5. 处理数据层的事件](#) - [7.4. 发送并同步数据](#) - [7.4.4. 发送与接收消息](#) - [7.1.2. 在Notifcation中接收语音输入](#) - [7.3.4. 创建2D-Picker](#) - [7.3.2. 创建Cards](#) - [7.3.5. 创建确认界面](#) - [7.3.6. 退出全屏的Activity](#) - [7.3.1. 定义Layouts](#) - [7.3.3. 创建Lists](#)
';

创建功能测试

最后更新于:2022-04-01 01:47:47

> 编写:[huanglizhuo](https://github.com/huanglizhuo) - 原文:[http://developer.android.com/training/](http://developer.android.com/training/activity-testing/activity-functional-testing.html)[activity](# "An activity represents a single screen with a user interface.")-testing/[activity](# "An activity represents a single screen with a user interface.")-functional-testing.html 功能测试包括验证单个应用中的各个组件是否与使用者期望的那样(与其它组件)协同工作。比如,我们可以创建一个功能测试验证在用户执行UI交互时[Activity](# "An activity represents a single screen with a user interface.")是否正确启动目标[Activity](# "An activity represents a single screen with a user interface.")。 要为[Activity](# "An activity represents a single screen with a user interface.")创建功能测,我们的测试类应该对[ActivityInstrumentationTestCase2](http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html)进行扩展。与[ActivityUnitTestCase](http://developer.android.com/reference/android/test/ActivityUnitTestCase.html)不同,[ActivityInstrumentationTestCase2](http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html)中的测试可以与Android系统通信,发送键盘输入及点击事件到UI中。 要了解一个完整的测试例子可以参考示例应用中的`SenderActivityTest.java`。 ### 添加测试方法验证函数的行为 我们的函数测试目标应该包括: - 验证UI控制是否正确启动了目标[Activity](# "An activity represents a single screen with a user interface.")。 - 验证目标[Activity](# "An activity represents a single screen with a user interface.")的表现是否按照发送[Activity](# "An activity represents a single screen with a user interface.")提供的数据呈现。 我们可以这样实现测试方法: ~~~ @MediumTest public void testSendMessageToReceiverActivity() { final Button sendToReceiverButton = (Button) mSenderActivity.findViewById(R.id.send_message_button); final EditText senderMessageEditText = (EditText) mSenderActivity.findViewById(R.id.message_input_edit_text); // Set up an ActivityMonitor ... // Send string input value ... // Validate that ReceiverActivity is started ... // Validate that ReceiverActivity has the correct data ... // Remove the ActivityMonitor ... } ~~~ 测试会等待匹配的[Activity](# "An activity represents a single screen with a user interface.")启动,如果超时则会返回null。如果ReceiverActivity启动了,那么先前配置的[ActivityMoniter](http://developer.android.com/reference/android/app/Instrumentation.ActivityMonitor.html)就会收到一次碰撞(Hit)。我们可以使用断言方法验证ReceiverActivity是否的确启动了,以及[ActivityMoniter](http://developer.android.com/reference/android/app/Instrumentation.ActivityMonitor.html)记录的碰撞次数是否按照预想地那样增加。 ### 设立一个ActivityMonitor 为了在应用中监视单个[Activity](# "An activity represents a single screen with a user interface.")我们可以注册一个[ActivityMoniter](http://developer.android.com/reference/android/app/Instrumentation.ActivityMonitor.html)。每当一个符合要求的[Activity](# "An activity represents a single screen with a user interface.")启动时,系统会通知[ActivityMoniter](http://developer.android.com/reference/android/app/Instrumentation.ActivityMonitor.html),进而更新碰撞数目。 通常来说要使用[ActivityMoniter](http://developer.android.com/reference/android/app/Instrumentation.ActivityMonitor.html),我们可以这样: 1. 使用[getInstrumentation()](http://developer.android.com/reference/android/test/InstrumentationTestCase.html#getInstrumentation())方法为测试用例实现[Instrumentation](http://developer.android.com/reference/android/app/Instrumentation.html)。 1. 使用[Instrumentation](http://developer.android.com/reference/android/app/Instrumentation.html)的一种addMonitor()方法为当前instrumentation添加一个[Instrumentation.ActivityMonitor](http://developer.android.com/reference/android/app/Instrumentation.ActivityMonitor.html)实例。匹配规则可以通过[IntentFilter](http://developer.android.com/reference/android/content/IntentFilter.html)或者类名字符串。 1. 等待开启一个[Activity](# "An activity represents a single screen with a user interface.")。 1. 验证监视器撞击次数的增加。 1. 移除监视器。 下面是一个例子: ~~~ // Set up an ActivityMonitor ActivityMonitor receiverActivityMonitor = getInstrumentation().addMonitor(ReceiverActivity.class.getName(), null, false); // Validate that ReceiverActivity is started TouchUtils.clickView(this, sendToReceiverButton); ReceiverActivity receiverActivity = (ReceiverActivity) receiverActivityMonitor.waitForActivityWithTimeout(TIMEOUT_IN_MS); assertNotNull("ReceiverActivity is null", receiverActivity); assertEquals("Monitor for ReceiverActivity has not been called", 1, receiverActivityMonitor.getHits()); assertEquals("Activity is of wrong type", ReceiverActivity.class, receiverActivity.getClass()); // Remove the ActivityMonitor getInstrumentation().removeMonitor(receiverActivityMonitor); ~~~ ### 使用Instrumentation发送一个键盘输入 如果[Activity](# "An activity represents a single screen with a user interface.")有一个[EditText](http://developer.android.com/reference/android/widget/EditText.html),我们可以测试用户是否可以给[EditText](http://developer.android.com/reference/android/widget/EditText.html)对象输入数值。 通常在[ActivityInstrumentationTestCase2](http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html)中给[EditText](http://developer.android.com/reference/android/widget/EditText.html)对象发送串字符,我们可以这样做: 1. 使用[runOnMainSync()](http://developer.android.com/reference/android/app/Instrumentation.html#runOnMainSync(java.lang.Runnable))方法在一个循环中同步地调用[requestFocus()](http://developer.android.com/reference/android/view/View.html#requestFocus())。这样,我们的UI线程就会在获得焦点前一直被阻塞。 1. 调用[waitForIdleSync()](http://developer.android.com/reference/android/app/Instrumentation.html#waitForIdleSync())方法等待主线程空闲(也就是说,没有更多事件需要处理)。 1. 调用[sendStringSync()](http://developer.android.com/reference/android/app/Instrumentation.html#sendStringSync(java.lang.String))方法给[EditText](http://developer.android.com/reference/android/widget/EditText.html)对象发送一个我们输入的字符串。 比如: ~~~ // Send string input value getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { senderMessageEditText.requestFocus(); } }); getInstrumentation().waitForIdleSync(); getInstrumentation().sendStringSync("Hello Android!"); getInstrumentation().waitForIdleSync(); ~~~ 本节例子[AndroidTestingFun.zip](http://developer.android.com/shareables/training/AndroidTestingFun.zip)
';

创建单元测试

最后更新于:2022-04-01 01:47:45

> 编写:[huanglizhuo](https://github.com/huanglizhuo) - 原文:[http://developer.android.com/training/](http://developer.android.com/training/activity-testing/activity-unit-testing.html)[activity](# "An activity represents a single screen with a user interface.")-testing/[activity](# "An activity represents a single screen with a user interface.")-unit-testing.html [Activity](# "An activity represents a single screen with a user interface.")单元测试可以快速且独立地(和系统其它部分分离)验证一个[Activity](# "An activity represents a single screen with a user interface.")的状态以及其与其它组件交互的正确性。一个单元测试通常用来测试代码中最小单位的代码块(可以是一个方法,类,或者组件),而且也不依赖于系统或网络资源。比如说,你可以写一个单元测试去检查[Activity](# "An activity represents a single screen with a user interface.")是否正确地布局或者是否可以正确地触发一个Intent对象。 单元测试一般不适合测试与系统有复杂交互的UI。我们应该使用如同[测试UI组件](#)所描述的`ActivityInstrumentationTestCase2`来对这类UI交互进行测试。 这节内容将会讲解如何编写一个单元测试来验证一个[Intent](http://developer.android.com/reference/android/content/Intent.html)是否正确地触发了另一个[Activity](# "An activity represents a single screen with a user interface.")。由于测试是与环境独立的,所以[Intent](http://developer.android.com/reference/android/content/Intent.html)实际上并没有发送给Android系统,但我们可以检查Intent对象的载荷数据是否正确。读者可以参考一下示例代码中的`LaunchActivityTest.java`,将它作为一个例子,了解完备的测试用例是怎么样的。 > **注意**: 如果要针对系统或者外部依赖进行测试,我们可以使用Mocking Framework的Mock类,并把它集成到我们的你的单元测试中。要了解更多关于Android提供的Mocking Framework内容请参考[Mock Object Classes](http://developer.android.com/tools/testing/testing_android.html#MockObjectClasses)。 ### 编写一个Android单元测试例子 ActiviUnitTestCase类提供对于单个[Activity](# "An activity represents a single screen with a user interface.")进行分离测试的支持。要创建单元测试,我们的测试类应该继承自`ActiviUnitTestCase`。继承`ActiviUnitTestCase`的[Activity](# "An activity represents a single screen with a user interface.")不会被Android自动启动。要单独启动[Activity](# "An activity represents a single screen with a user interface."),我们需要显式的调用startActivity()方法,并传递一个[Intent](http://developer.android.com/reference/android/content/Intent.html)来启动我们的目标[Activity](# "An activity represents a single screen with a user interface.")。 例如: ~~~ public class LaunchActivityTest extends ActivityUnitTestCase<LaunchActivity> { ... @Override protected void setUp() throws Exception { super.setUp(); mLaunchIntent = new Intent(getInstrumentation() .getTargetContext(), LaunchActivity.class); startActivity(mLaunchIntent, null, null); final Button launchNextButton = (Button) getActivity() .findViewById(R.id.launch_next_activity_button); } } ~~~ ### 验证另一个[Activity](# "An activity represents a single screen with a user interface.")的启动 我们的单元测试目标可能包括: - 验证当Button被按下时,启动的LaunchActivity是否正确。 - 验证启动的Intent是否包含有效的数据。 为了验证一个触发[Intent](http://developer.android.com/reference/android/content/Intent.html)的Button的事件,我们可以使用[getStartedActivityIntent()](http://developer.android.com/reference/android/test/ActivityUnitTestCase.html#getStartedActivityIntent())方法。通过使用断言方法,我们可以验证返回的[Intent](http://developer.android.com/reference/android/content/Intent.html)是否为空,以及是否包含了预期的数据来启动下一个[Activity](# "An activity represents a single screen with a user interface.")。如果两个断言值都是真,那么我们就成功地验证了[Activity](# "An activity represents a single screen with a user interface.")发送的Intent是正确的了。 我们可以这样实现测试方法: ~~~ @MediumTest public void testNextActivityWasLaunchedWithIntent() { startActivity(mLaunchIntent, null, null); final Button launchNextButton = (Button) getActivity() .findViewById(R.id.launch_next_activity_button); launchNextButton.performClick(); final Intent launchIntent = getStartedActivityIntent(); assertNotNull("Intent was null", launchIntent); assertTrue(isFinishCalled()); final String payload = launchIntent.getStringExtra(NextActivity.EXTRAS_PAYLOAD_KEY); assertEquals("Payload is empty", LaunchActivity.STRING_PAYLOAD, payload); } ~~~ 因为LaunchActivity是独立运行的,所以不可以使用[TouchUtils](http://developer.android.com/reference/android/test/TouchUtils.html)库来操作UI。如果要直接进行[Button](http://developer.android.com/reference/android/widget/Button.html)点击,我们可以调用[perfoemClick()](http://developer.android.com/reference/android/view/View.html#performClick())方法。 本节示例代码[AndroidTestingFun.zip](http://developer.android.com/shareables/training/AndroidTestingFun.zip)
';

测试UI组件

最后更新于:2022-04-01 01:47:43

> 编写:[huanglizhuo](https://github.com/huanglizhuo) - 原文:[http://developer.android.com/training/](http://developer.android.com/training/activity-testing/activity-ui-testing.html)[activity](# "An activity represents a single screen with a user interface.")-testing/[activity](# "An activity represents a single screen with a user interface.")-ui-testing.html 通常情况下,[Activity](# "An activity represents a single screen with a user interface."),包括用户界面组件(如按钮,复选框,可编辑的文本域,和选框)允许用户与Android应用程序交互。本节介绍如何对一个简单的带有按钮的界面交互测试。我们可以使用相同的步骤来测试其他更复杂的UI组件。 > **注意**: 这一节的测试方法叫做白盒测试,因为我们拥有要测试应用程序的源码。Android Instrumentation框架适用于创建应用程序中UI部件的白盒测试。用户界面测试的另一种类型是黑盒测试,即无法得知应用程序源代码的类型。这种类型的测试可以用来测试应用程序如何与其他应用程序,或与系统进行交互。黑盒测试不包括在本节中。了解更多关于如何在你的Android应用程序进行黑盒测试,请阅读[UI Testing guide](http://developer.android.com/tools/testing/testing_ui.html)。 要参看完整的测试案例,可以查看本节示例代码中的`ClickFunActivityTest.java`文件。 ### 使用 Instrumentation 建立UI测试 当测试拥有UI的[Activity](# "An activity represents a single screen with a user interface.")时,被测试的[Activity](# "An activity represents a single screen with a user interface.")在UI线程中运行。然而,测试程序会在程序自己的进程中,单独的一个线程内运行。这意味着,我们的测试程序可以获得UI线程的对象,但是如果它尝试改变UI线程对象的值,会得到`WrongThreadException`错误。 为了安全地将`Intent`注入到`Activity`,或是在UI线程中执行测试方法,我们可以让测试类继承于[ActivityInstrumentationTestCase2](http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html)。要学习如何在UI线程运行测试方法,请看[在UI线程测试](http://developer.android.com/tools/testing/activity_testing.html#RunOnUIThread)。 ### 建立测试数据集(Fixture) 当为UI测试建立测试数据集时,我们应该在[setUp()](http://developer.android.com/reference/junit/framework/TestCase.html#setUp())方法中指定[touch mode](http://developer.android.com/guide/topics/ui/ui-events.html#TouchMode)。把touch mode设置为真可以防止在执行编写的测试方法时,人为的UI操作获取到控件的焦点(比如,一个按钮会触发它的点击监听器)。确保在调用[getActivity()](http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html#getActivity())方法前调用了[setActivityInitialTouchMode](http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html#setActivityInitialTouchMode(boolean))。 比如: ~~~ public class ClickFunActivityTest extends ActivityInstrumentationTestCase2 { ... @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); mClickFunActivity = getActivity(); mClickMeButton = (Button) mClickFunActivity .findViewById(R.id.launch_next_activity_button); mInfoTextView = (TextView) mClickFunActivity.findViewById(R.id.info_text_view); } } ~~~ ### 添加测试方法确认UI响应表现 UI测试目标应包括: _. 检验[Activity](# "An activity represents a single screen with a user interface.")启动时[Button](http://developer.android.com/reference/android/widget/Button.html)在正确布局位置显示。_. 检验[TextView](http://developer.android.com/reference/android/widget/TextView.html)初始化时是隐藏的。*. 检验[TextView](http://developer.android.com/reference/android/widget/TextView.html)在[Button](http://developer.android.com/reference/android/widget/Button.html)点击时显示预期的字符串 接下来的部分会演示怎样实现上述验证方法 ### 验证Button布局参数 我们应该像如下添加的测试方法那样。验证[Activity](# "An activity represents a single screen with a user interface.")中的按钮是否正确显示: ~~~ @MediumTest public void testClickMeButton_layout() { final View decorView = mClickFunActivity.getWindow().getDecorView(); ViewAsserts.assertOnScreen(decorView, mClickMeButton); final ViewGroup.LayoutParams layoutParams = mClickMeButton.getLayoutParams(); assertNotNull(layoutParams); assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT); assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT); } ~~~ 在调用[assertOnScreen()](http://developer.android.com/reference/android/test/ViewAsserts.html#assertOnScreen(android.view.View, android.view.View))方法时,传递根视图以及期望呈现在屏幕上的视图作为参数。如果想呈现的视图没有在根视图中,该方法会抛出一个[AssertionFailedError](http://developer.android.com/reference/junit/framework/AssertionFailedError.html)异常,否则测试通过。 我们也可以通过获取一个[ViewGroup.LayoutParams](http://developer.android.com/reference/android/view/ViewGroup.LayoutParams.html)对象的引用验证[Button](http://developer.android.com/reference/android/widget/Button.html)布局是否正确,然后调用`assert`方法验证[Button](http://developer.android.com/reference/android/widget/Button.html)对象的宽高属性值是否与预期值一致。 `@MediumTest`注解指定测试是如何归类的(和它的执行时间相关)。要了解更多有关测试的注解,见本节示例。 ### 验证TextView的布局参数 可以像这样添加一个测试方法来验证[TextView](http://developer.android.com/reference/android/widget/TextView.html)最初是隐藏在[Activity](# "An activity represents a single screen with a user interface.")中的: ~~~ @MediumTest public void testInfoTextView_layout() { final View decorView = mClickFunActivity.getWindow().getDecorView(); ViewAsserts.assertOnScreen(decorView, mInfoTextView); assertTrue(View.GONE == mInfoTextView.getVisibility()); } ~~~ 我们可以调用`getDecorView()`方法得到一个[Activity](# "An activity represents a single screen with a user interface.")中修饰试图(Decor View)的引用。要修饰的View在布局层次视图中是最上层的ViewGroup([FrameLayout](http://developer.android.com/reference/android/widget/FrameLayout.html)) ### 验证按钮的行为 可以使用如下测试方法来验证当按下按钮时[TextView](http://developer.android.com/reference/android/widget/TextView.html)变得可见: ~~~ @MediumTest public void testClickMeButton_clickButtonAndExpectInfoText() { String expectedInfoText = mClickFunActivity.getString(R.string.info_text); TouchUtils.clickView(this, mClickMeButton); assertTrue(View.VISIBLE == mInfoTextView.getVisibility()); assertEquals(expectedInfoText, mInfoTextView.getText()); } ~~~ 在测试中调用[clickView()](http://developer.android.com/reference/android/test/TouchUtils.html#clickView(android.test.InstrumentationTestCase, android.view.View))可以让我们用编程方式点击一个按钮。我们必须传递正在运行的测试用例的一个引用和要操作按钮的引用。 > **注意**:[TouchUtils](http://developer.android.com/reference/android/test/TouchUtils.html)辅助类提供与应用程序交互的方法可以方便进行模拟触摸操作。我们可以使用这些方法来模拟点击,轻敲,或应用程序屏幕拖动View。 > **警告**[TouchUtils](http://developer.android.com/reference/android/test/TouchUtils.html)方法的目的是将事件安全地从测试线程发送到UI线程。我们不可以直接在UI线程或任何标注@UIThread的测试方法中使用[TouchUtils](http://developer.android.com/reference/android/test/TouchUtils.html)这样做可能会增加错误线程异常。 ### 应用测试注解 [@SmallTest](http://developer.android.com/reference/android/test/suitebuilder/annotation/SmallTest.html) ~~~ 标志该测试方法是小型测试的一部分。 ~~~ [@MediumTest](http://developer.android.com/reference/android/test/suitebuilder/annotation/MediumTest.html) ~~~ 标志该测试方法是中等测试的一部分。 ~~~ [@LargeTest](http://developer.android.com/reference/android/test/suitebuilder/annotation/LargeTest.html) ~~~ 标志该测试方法是大型测试的一部分。 ~~~ 通常情况下,如果测试方法只需要几毫秒的时间,那么它应该被标记为[@SmallTest](http://developer.android.com/reference/android/test/suitebuilder/annotation/SmallTest.html),长时间运行的测试(100毫秒或更多)通常被标记为[@MediumTest](http://developer.android.com/reference/android/test/suitebuilder/annotation/MediumTest.html)或[@LargeTest](http://developer.android.com/reference/android/test/suitebuilder/annotation/LargeTest.html),这主要取决于测试访问资源在网络上或在本地系统。 可以参看[Android Tools Protip](https://plus.google.com/+AndroidDevelopers/posts/TPy1EeSaSg8),它可以更好地指导我们使用测试注释 我们可以创建其它的测试注释来控制测试的组织和运行。要了解更多关于其他注释的信息,见[Annotation](http://developer.android.com/reference/java/lang/annotation/Annotation.html)类参考。 本节示例代码[AndroidTestingFun.zip](http://developer.android.com/shareables/training/AndroidTestingFun.zip)
';

创建与执行测试用例

最后更新于:2022-04-01 01:47:40

> 编写:[huanglizhuo](https://github.com/huanglizhuo) - 原文:[http://developer.android.com/training/](http://developer.android.com/training/activity-testing/activity-basic-testing.html)[activity](# "An activity represents a single screen with a user interface.")-testing/[activity](# "An activity represents a single screen with a user interface.")-basic-testing.html 为了验证应用的布局设计和功能是否符合预期,为应用的每个[Activity](# "An activity represents a single screen with a user interface.")建立测试非常重要。对于每一个测试,我们需要在测试用例中创建一个个独立的部分,包括测试数据,前提条件和[Activity](# "An activity represents a single screen with a user interface.")的测试方法。之后我们就可以运行测试并得到测试报告。如果有任何测试没有通过,这表明在我们代码中可能有潜在的缺陷。 > **注意**: 在测试驱动开发(TDD)方法中, 不推荐先编写大部分或整个应用,并在开发完成后再运行测试。而是应该先编写测试,然后及时编写正确的代码,以通过测试。通过更新测试案例来反映新的功能需求,并以此反复。 ### 创建一个测试用例 [Activity](# "An activity represents a single screen with a user interface.")测试都是通过结构化的方式编写的。请务必把测试代码放在一个单独的包内,从而与被测试的代码分开。 按照惯例,测试包的名称应该遵循与应用包名相同的命名方式,在应用包名后接“.tests”。在创建的测试包中,为我们的测试用例添加Java类。按照惯例,测试用例名称也应遵循要测试的Java或Android的类相同的名称,并增加后缀“Test”。 要在Eclipse中创建一个新的测试用例可遵循如下步骤: a. 在Package Explorer中,右键点击待测试工程的src/文件夹,**New > Package**。 b. 设置文件夹名称`<你的包名称>.tests`(比如, `com.example.android.testingfun.tests`)并点击**Finish**。 c. 右键点击创建的测试包,并选择**New > Calss**。 d. 设置文件名称`<你的Activity名称>Test`(比如, `MyFirstTestActivityTest`),然后点击**Finish**。 ### 建立测试数据集(Fixture) 测试数据集包含运行测试前必须生成的一些对象。要建立测试数据集,可以在我们的测试中覆写[setUp()](http://developer.android.com/reference/junit/framework/TestCase.html#setUp())和[tearDown()](http://developer.android.com/reference/junit/framework/TestCase.html#tearDown())方法。测试会在运行任何其它测试方法之前自动执行[setUp()](http://developer.android.com/reference/junit/framework/TestCase.html#setUp())方法。我们可以用这些方法使得被测试代码与测试初始化和清理是分开的。 在你的Eclipse中建立测试数据集: 1 . 在 Package Explorer中双击测试打开之前编写的测试用例,然后修改测试用例使它继承[ActivityTestCase](http://developer.android.com/reference/android/test/ActivityTestCase.html)的子类。比如: ~~~ public class MyFirstTestActivityTest extends ActivityInstrumentationTestCase2<MyFirstTestActivity> { ~~~ 2 . 下一步,给测试用例添加构造函数和setUp()方法,并为我们想测试的[Activity](# "An activity represents a single screen with a user interface.")添加变量声明。比如: ~~~ public class MyFirstTestActivityTest extends ActivityInstrumentationTestCase2<MyFirstTestActivity> { private MyFirstTestActivity mFirstTestActivity; private TextView mFirstTestText; public MyFirstTestActivityTest() { super(MyFirstTestActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); mFirstTestActivity = getActivity(); mFirstTestText = (TextView) mFirstTestActivity .findViewById(R.id.my_first_test_text_view); } } ~~~ 构造函数是由测试用的Runner调用,用于初始化测试类的,而[setUp()](http://developer.android.com/reference/junit/framework/TestCase.html#setUp())方法是由测试Runner在其他测试方法开始前运行的。 通常在`setUp()`方法中,我们应该: - 为`setUp()` 调用父类构造函数,这是JUnit要求的。 - 初始化测试数据集的状态,具体而言: - 定义保存测试数据及状态的实例变量 - 创建并保存正在测试的[Activity](# "An activity represents a single screen with a user interface.")的引用实例。 - 获得想要测试的[Activity](# "An activity represents a single screen with a user interface.")中任何UI组件的引用。 我们可以使用[getActivity()](http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html#getActivity())方法得到正在测试的[Activity](# "An activity represents a single screen with a user interface.")的引用。 ### 增加一个测试前提 我们最好在执行测试之前,检查测试数据集的设置是否正确,以及我们想要测试的对象是否已经正确地初始化。这样,测试就不会因为有测试数据集的设置错误而失败。按照惯例,验证测试数据集的方法被称为`testPreconditions()`。 例如,我们可能想添加一个像这样的`testPreconditons()`方法: ~~~ public void testPreconditions() { assertNotNull(“mFirstTestActivity is null”, mFirstTestActivity); assertNotNull(“mFirstTestText is null”, mFirstTestText); } ~~~ Assertion(断言,译者注)方法源自于Junit[Assert](http://developer.android.com/reference/junit/framework/Assert.html)类。通常,我们可以使用断言来验证某一特定的条件是否是真的。 - 如果条件为假,断言方法抛出一个AssertionFailedError异常,通常会由测试Runner报告。我们可以在断言失败时给断言方法添加一个字符串作为第一个参数从而给出一些上下文详细信息。 - 如果条件为真,测试通过。 在这两种情况下,Runner都会继续运行其它测试用例的测试方法。 ### 添加一个测试方法来验证[Activity](# "An activity represents a single screen with a user interface.") 下一步,添加一个或多个测试方法来验证[Activity](# "An activity represents a single screen with a user interface.")布局和功能。 例如,如果我们的[Activity](# "An activity represents a single screen with a user interface.")含有一个[TextView](http://developer.android.com/reference/android/widget/TextView.html),可以添加如下方法来检查它是否有正确的标签文本: ~~~ public void testMyFirstTestTextView_labelText() { final String expected = mFirstTestActivity.getString(R.string.my_first_test); final String actual = mFirstTestText.getText().toString(); assertEquals(expected, actual); } ~~~ 该 `testMyFirstTestTextView_labelText()` 方法只是简单的检查Layout中[TextView](http://developer.android.com/reference/android/widget/TextView.html)的默认文本是否和`strings.xml`资源中定义的文本一样。 > **注意**:当命名测试方法时,我们可以使用下划线将被测试的内容与测试用例区分开。这种风格使得我们可以更容易分清哪些是测试用例。 做这种类型的字符串比较时,推荐从资源文件中读取预期字符串,而不是在代码中硬性编写字符串做比较。这可以防止当资源文件中的字符串定义被修改时,会影响到测试的效果。 为了进行比较,预期的和实际的字符串都要做为[assertEquals()](http://developer.android.com/reference/junit/framework/Assert.html#assertEquals(java.lang.String, java.lang.String))方法的参数。如果值是不一样的,断言将抛出一个[AssertionFailedError](http://developer.android.com/reference/junit/framework/AssertionFailedError.html)异常。 如果添加了一个`testPreconditions()`方法,我们可以把测试方法放在testPreconditions之后。 要参看一个完整的测试案例,可以参考本节示例中的MyFirstTestActivityTest.java。 ### 构建和运行测试 我们可以在Eclipse中的包浏览器(Package Explorer)中运行我们的测试。 利用如下步骤构建和运行测试: 1. 连接一个Android设备,在设备或模拟器中,打开设置菜单,选择开发者选项并确保启用USB调试。 1. 在包浏览器(Package Explorer)中,右键单击测试类,并选择**Run As > Android Junit Test**。 1. 在Android设备选择对话框,选择刚才连接的设备,然后单击“确定”。 1. 在JUnit视图,验证测试是否通过,有无错误或失败。 本节示例代码[AndroidTestingFun.zip](http://developer.android.com/shareables/training/AndroidTestingFun.zip)
';

建立测试环境

最后更新于:2022-04-01 01:47:38

> 编写:[huanglizhuo](https://github.com/huanglizhuo) - 原文:[http://developer.android.com/training/](http://developer.android.com/training/activity-testing/preparing-activity-testing.html)[activity](# "An activity represents a single screen with a user interface.")-testing/preparing-[activity](# "An activity represents a single screen with a user interface.")-testing.html 在开始编写并运行我们的测试之前,我们应该建立测试开发环境。本小节将会讲解如何建立Eclipse IDE来构建和运行我们的测试,以及怎样用Gradle构建工具在命令行下构建和运行我们的测试。 > 注意: 本小节基于的是Eclipse及ADT插件。然而,你在自己测试开发时可以自由选用IDE或命令行。 ### 用Eclipse建立测试 安装了Android Developer Tools (ADT) 插件的Eclipse将为我们创建,构建,以及运行Android程序提供一个基于图形界面的集成开发环境。Eclipse可以自动为我们的Android应用项目创建一个对应的测试项目。 开始在Eclipse中创建测试环境: 1. 如果还没安装Eclipse [ADT](http://developer.android.com/sdk/installing/bundle.html)插件,请先下载安装。 1. 导入或创建我们想要测试的Android应用项目。 1. 生成一个对应于应用程序项目测试的测试项目。为导入项目生成一个测试项目: a.在项目浏览器里,右击我们的应用项目,然后选择**Android Tools > New Test Project** b.在新建Android测试项目面板,为我们的测试项目设置合适的参数,然后点击**Finish** 现在应该可以在Eclipse环境中创建,构建和运行测试项目了。想要继续学习如何在Eclipse中进行这些任务,可以阅读[创建与执行测试用例](#) ### 用命令行建立测试 如果正在使用Gradle version 1.6或者更高的版本作为构建工具,可以用Gradle Wrapper创建。构建和运行Android应用测试。确保在`gradle.build`文件中,`defaultConfig`部分中的[minSdkVersion](http://developer.android.com/guide/topics/manifest/uses-sdk-element.html)属性是8或更高。可以参考包含在下载包中的示例文件gradle.build ### 用Gradle Wrapper运行测试: 1. 连接Android真机或开启Android模拟器。 1. 在项目目录运行如下命令: > ./gradlew build connectedCheck 进一步学习Gradle关于Android测试的内容,参看[Gradle Plugin User Guide](http://www.gradle.org/docs/current/userguide/userguide_single.html)。 进一步学习使用Gradle及其它命令行工具,参看[Testing from Other IDEs.](http://developer.android.com/tools/testing/testing_otheride.html)。 本节示例代码[AndroidTestingFun.zip](http://developer.android.com/shareables/training/AndroidTestingFun.zip)
';

测试你的Activity

最后更新于:2022-04-01 01:47:36

> 编写:[huanglizhuo](https://github.com/huanglizhuo) - 原文:[http://developer.android.com/training/](http://developer.android.com/training/activity-testing/index.html)[activity](# "An activity represents a single screen with a user interface.")-testing/index.html 我们应该把编写和运行测试作为Android应用开发周期的一部分。完备的测试可以帮助我们在开发过程中尽早发现漏洞,并让我们对自己的代码更有信心。 测试用例定义了一系列对象和方法从而独立进行多个测试。测试用例可以编写成测试组并按计划的运行,由测试框架组织成一个可以重复运行的测试Runner(运行器,译者注)。 这节内容将会讲解如何基于最流行的JUnit框架来自定义测试框架。我们可以编写测试用例来测试我们应用程序的特定行为,并在不同的Android设备上检测一致性。测试用例还可以用来描述应用组件的预期行为,并作为内部代码文档。 ### 课程 - [**建立测试环境**](#) 学习如何创建测试项目 - [**创建与执行测试用例**](#) 学习如何写测试用例来检验[Activity](# "An activity represents a single screen with a user interface.")中的特性,并使用Android框架提供的Instrumentation运行用例。 - [**测试UI组件**](#) 学习如何编写UI测试用例 - [**创建单元测试**](#) 学习如何隔离开[Activity](# "An activity represents a single screen with a user interface.")执行单元测试 - [**创建功能测试**](#) 学习如何执行功能测试来检验各[Activity](# "An activity represents a single screen with a user interface.")之间的交互
';

Android测试程序

最后更新于:2022-04-01 01:47:33

These classes and articles provide information about how to test your Android application. #### [Testing Your ](#)[Activity](# "An activity represents a single screen with a user interface.") How to test Activities in your Android applications.
';

使用设备管理条例增强安全性

最后更新于:2022-04-01 01:47:31

> 编写:[craftsmanBai](https://github.com/craftsmanBai) - [http://z1ng.net](http://z1ng.net) - 原文: [http://developer.android.com/training/enterprise/device-management-policy.html](http://developer.android.com/training/enterprise/device-management-policy.html) Android 2.2(API Level 8)之后,Android平台通过设备管理API提供系统级的设备管理能力。 在这一小节中,你将学到如何通过使用设备管理策略创建安全敏感的应用程序。比如某应用可被配置为:在给用户显示受保护的内容之前,确保已设置一个足够强度的锁屏密码。 ### 定义并声明你的策略 首先,你需要定义多种在功能层面提供支持的策略。这些策略可以包括屏幕锁密码强度、密码过期时间以及加密等等方面。 你须在res/xml/device_admin.xml中声明选择的策略集,它将被应用强制实行。在Android manifest也需要引用声明的策略集。 每个声明的策略对应[DevicePolicyManager](http://developer.android.com/reference/android/app/admin/DevicePolicyManager.html)中一些相关设备的策略方法(例如定义最小密码长度或最少大写字母字符数)。如果一个应用尝试调用XML中没有对应策略的方法,程序在会运行时抛出一个[SecurityException](http://developer.android.com/reference/java/lang/SecurityException.html)异常。 如果应用程序试图管理其他策略,那么强制锁force-lock之类的其他权限就会发挥作用。正如你将看到的,作为设备管理权限激活过程的一部分,声明策略的列表会在系统屏幕上显示给用户。如下代码片段在res/xml/device_admin.xml中声明了密码限制策略: ~~~ <device-admin xmlns:android="http://schemas.android.com/apk/res/android"> <uses-policies> <limit-password /> </uses-policies> </device-admin> ~~~ 在Android manifest引用XML策略声明: ~~~ <receiver android:name=".Policy$PolicyAdmin" android:permission="android.permission.BIND_DEVICE_ADMIN"> <meta-data android:name="android.app.device_admin" android:resource="@xml/device_admin" /> <intent-filter> <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> </intent-filter> </receiver> ~~~ ### 创建一个设备管理接受端 创建一个设备管理广播接收端(broadcast receiver),可以接收到与你声明的策略有关的事件通知。也可以对应用程序有选择地重写回调函数。 在同样的应用程序(Device Admin)中,当设备管理(device administrator)权限被用户设为禁用时,已配置好的策略就会从共享偏好设置(shared preference)中擦除。 你应该考虑实现与你的应用业务逻辑相关的策略。例如,你的应用可以采取一些措施来降低安全风险,如:删除设备上的敏感数据,禁用远程同步,对管理员的通知提醒等等。 为了让广播接收端能够正常工作,请务必在Android manifest中注册下面代码片段所示内容。 ~~~ <receiver android:name=".Policy$PolicyAdmin" android:permission="android.permission.BIND_DEVICE_ADMIN"> <meta-data android:name="android.app.device_admin" android:resource="@xml/device_admin" /> <intent-filter> <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> </intent-filter> </receiver> ~~~ ### 激活设备管理器 在执行任何策略之前,用户需要手动将程序激活为具有设备管理权限,下面的程序片段显示了如何触发设置框以便让用户为你的程序激活权限。 通过指定[EXTRA_ADD_EXPLANATION](http://developer.android.com/reference/android/app/admin/DevicePolicyManager.html#EXTRA_ADD_EXPLANATION)给出明确的说明信息,以告知用户为应用程序激活设备管理权限的好处。 ~~~ if (!mPolicy.isAdminActive()) { Intent activateDeviceAdminIntent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN); activateDeviceAdminIntent.putExtra( DevicePolicyManager.EXTRA_DEVICE_ADMIN, mPolicy.getPolicyAdmin()); // It is good practice to include the optional explanation text to // explain to user why the application is requesting to be a device // administrator. The system will display this message on the activation // screen. activateDeviceAdminIntent.putExtra( DevicePolicyManager.EXTRA_ADD_EXPLANATION, getResources().getString(R.string.device_admin_activation_message)); startActivityForResult(activateDeviceAdminIntent, REQ_ACTIVATE_DEVICE_ADMIN); } ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-07-28_55b7247b6347a.png) 如果用户选择"Activate",程序就会获取设备管理员权限并可以开始配置和执行策略。当然,程序也需要做好处理用户选择放弃激活的准备,比如用户点击了“取消”按钮,返回键或者HOME键的情况。因此,如果有必要的话,策略设置中的_[onResume()](http://developer.android.com/reference/android/app/Activity.html#onResume())_方法需要加入重新评估的逻辑判断代码,以便将设备管理激活选项展示给用户。 ### 实施设备策略控制 在设备管理权限成功激活后,程序就会根据请求的策略来配置设备策略管理器。要牢记,新策略会被添加到每个版本的Android中。所以你需要在程序中做好平台版本的检测,以便新策略能被老版本平台很好的支持。例如,“密码中含有的最少大写字符数”这个安全策略只有在高于API Level 11(Honeycomb)的平台才被支持,以下代码则演示了如何在运行时检查版本: ~~~ DevicePolicyManager mDPM = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); ComponentName mPolicyAdmin = new ComponentName(context, PolicyAdmin.class); ... mDPM.setPasswordQuality(mPolicyAdmin, PASSWORD_QUALITY_VALUES[mPasswordQuality]); mDPM.setPasswordMinimumLength(mPolicyAdmin, mPasswordLength); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { mDPM.setPasswordMinimumUpperCase(mPolicyAdmin, mPasswordMinUpperCase); } ~~~ 这样程序就可以执行策略了。当程序无法访问正确的锁屏密码的时候,通过设备策略管理器(Device Policy Manager)API可以判断当前密码是否适用于请求的策略。如果当前锁屏密码满足策略,设备管理API不会采取纠正措施。明确地启动设置程序中的系统密码更改界面是应用程序的责任。例如: ~~~ if (!mDPM.isActivePasswordSufficient()) { ... // Triggers password change screen in Settings. Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); startActivity(intent); } ~~~ 一般来说,用户可以从可用的锁屏机制中任选一个,例如“无”、“图案”、“PIN码”(数字)或密码(字母数字)。当一个密码策略配置好后,那些比已定义密码策略弱的密码会被禁用。比如,如果配置了密码级别为“Numeric”,那么用户只可以选择PIN码(数字)或者密码(字母数字)。 一旦设备通过设置适当的锁屏密码处于被保护的状态,应用程序便允许访问受保护的内容。 ~~~ if (!mDPM.isAdminActive(..)) { // Activates device administrator. ... } else if (!mDPM.isActivePasswordSufficient()) { // Launches password set-up screen in Settings. ... } else { // Grants access to secure content. ... startActivity(new Intent(context, SecureActivity.class)); } ~~~
';

为防止SSL漏洞而更新Security

最后更新于:2022-04-01 01:47:28

> 编写:[craftsmanBai](https://github.com/craftsmanBai) - [http://z1ng.net](http://z1ng.net) - 原文: [http://developer.android.com/training/articles/security-gms-provider.html](http://developer.android.com/training/articles/security-gms-provider.html) 安卓依靠security provider保障网络通信安全。然而有时默认的security provider存在安全漏洞。为了防止这些漏洞被利用,Google Play services 提供了一个自动更新设备的security provider的方法来对抗已知的漏洞。通过调用Google Play services方法,可以确保你的应用运行在可以抵抗已知漏洞的设备上。 举个例子,OpenSSL的漏洞(CVE-2014-0224)会导致中间人攻击,在通信双方不知情的情况下解密流量。Google Play services 5.0提供了一个补丁,但是必须确保应用安装了这个补丁。通过调用Google Play services方法,可以确保你的应用运行在可抵抗攻击的安全设备上。 **注意**:更新设备的security provider不是更新[android.net.SSLCertificateSocketFactory](http://developer.android.com/reference/android/net/SSLCertificateSocketFactory.html).比起使用这个类,我们更鼓励应用开发者使用融入密码学的高级方法。大多数应用可以使用类似[HttpsURLConnection](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html),[HttpClient](http://developer.android.com/reference/org/apache/http/client/HttpClient.html),[AndroidHttpClient](http://developer.android.com/reference/android/net/http/AndroidHttpClient.html)这样的API,而不必去设置[TrustManager](http://developer.android.com/reference/javax/net/ssl/TrustManager.html)或者创建一个[SSLCertificateSocketFactory](http://developer.android.com/reference/android/net/SSLCertificateSocketFactory.html)。 ### 使用ProviderInstaller给Security Provider打补丁 使用providerinstaller类来更新设备的security provider。你可以通过调用该类的方法[installIfNeeded()]()(或者[ installifneededasync]())来验证security provider是否为最新的(必要的话更新它)。 当你调用[installifneeded]()时,[providerinstaller]()会做以下事情: - 如果设备的Provider成功更新(或已经是最新的),该方法返回正常。 - 如果设备的Google Play services 库已经过时了,这个方法抛出[googleplayservicesrepairableexception]()异常表明无法更新Provider。应用程序可以捕获这个异常并向用户弹出合适的对话框提示更新Google Play services。 - 如果产生了不可恢复的错误,该方法抛出[googleplayservicesnotavailableexception]()表示它无法更新[Provider]()。应用程序可以捕获异常并选择合适的行动,如显示标准问题解决流程图。 [installifneededasync]()方法类似,但它不抛出异常,而是通过相应的回调方法,以提示成功或失败。 如果[installifneeded]()需要安装一个新的[Provider](),可能耗费30-50毫秒(较新的设备)到350毫秒(旧设备)。如果security provider已经是最新的,该方法需要的时间量可以忽略不计。为了避免影响用户体验: - 线程加载后立即在后台网络线程中调用[installifneeded](),而不是等待线程尝试使用网络。(多次调用该方法没有害处,如果安全提供程序不需要更新它会立即返回。) - 如果用户体验会受线程阻塞的影响——比如从UI线程中调用,那么使用[installifneededasync()]()调用该方法的异步版本。(当然,如果你要这样做,在尝试任何安全通信之前必须等待操作完成。[providerinstaller]()调用监听者的[onproviderinstalled()]()方法发出成功信号。 **警告**:如果[providerinstaller]()无法安装更新Provider,您的设备security provider会容易受到已知漏洞的攻击。你的程序等同于所有HTTP通信未被加密。一旦[Provider]()更新,所有安全API(包括SSL API)的调用会经过它(但这并不适用于[android.net.sslcertificatesocketfactory](),面对[cve-2014-0224]()这种漏洞仍然是脆弱的)。 ### 同步修补 修补security provider最简单的方法就是调用同步方法[installIfNeeded()](http://developer.android.com/reference/com/google/android/gms/security/ProviderInstaller.html##installIfNeeded(android.content.Context).如果用户体验不会被线程阻塞影响的话,这种方法很合适。 举个例子,这里有一个sync adapter会更新security provider。由于它运行在后台,因此在等待security provider更新的时候线程阻塞是可以的。sync adapter调用installifneeded()更新security provider。如果返回正常,sync adapter可以确保security provider是最新的。如果返回异常,sync adapter可以采取适当的行动(如提示用户更新Google Play services)。 ~~~ /** * Sample sync adapter using {@link ProviderInstaller}. */ public class SyncAdapter extends AbstractThreadedSyncAdapter { ... // This is called each time a sync is attempted; this is okay, since the // overhead is negligible if the security provider is up-to-date. @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { try { ProviderInstaller.installIfNeeded(getContext()); } catch (GooglePlayServicesRepairableException e) { // Indicates that Google Play services is out of date, disabled, etc. // Prompt the user to install/update/enable Google Play services. GooglePlayServicesUtil.showErrorNotification( e.getConnectionStatusCode(), getContext()); // Notify the SyncManager that a soft error occurred. syncResult.stats.numIOExceptions++; return; } catch (GooglePlayServicesNotAvailableException e) { // Indicates a non-recoverable error; the ProviderInstaller is not able // to install an up-to-date Provider. // Notify the SyncManager that a hard error occurred. syncResult.stats.numAuthExceptions++; return; } // If this is reached, you know that the provider was already up-to-date, // or was successfully updated. } } ~~~ ### 异步修补 更新security provider可能耗费350毫秒(旧设备)。如果在一个会直接影响用户体验的线程中更新,如UI线程,那么你不会希望进行同步更新,因为这可能导致应用程序或设备冻结直到操作完成。因此你应该使用异步方法[installifneededasync()](http://developer.android.com/reference/com/google/android/gms/security/ProviderInstaller.html#installIfNeededAsync(android.content.Context, com.google.android.gms.security.ProviderInstaller.ProviderInstallListener)。方法通过调用回调函数来反馈其成功或失败。例如,下面是一些关于更新security provider在UI线程中的活动的代码。调用installifneededasync()来更新security provider,并指定自己为监听器接收成功或失败的通知。如果security provider是最新的或更新成功,会调用[onproviderinstalled()](http://developer.android.com/reference/com/google/android/gms/security/ProviderInstaller.ProviderInstallListener.html#onProviderInstalled()方法,并且知道通信是安全的。如果security provider无法更新,会调用[onproviderinstallfailed()](http://developer.android.com/reference/com/google/android/gms/security/ProviderInstaller.ProviderInstallListener.html#onProviderInstallFailed(int, android.content.Intent)方法,并采取适当的行动(如提示用户更新Google Play services) ~~~ /** * Sample activity using {@link ProviderInstaller}. */ public class MainActivity extends Activity implements ProviderInstaller.ProviderInstallListener { private static final int ERROR_DIALOG_REQUEST_CODE = 1; private boolean mRetryProviderInstall; //Update the security provider when the activity is created. @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ProviderInstaller.installIfNeededAsync(this, this); } /** * This method is only called if the provider is successfully updated * (or is already up-to-date). */ @Override protected void onProviderInstalled() { // Provider is up-to-date, app can make secure network calls. } /** * This method is called if updating fails; the error code indicates * whether the error is recoverable. */ @Override protected void onProviderInstallFailed(int errorCode, Intent recoveryIntent) { if (GooglePlayServicesUtil.isUserRecoverableError(errorCode)) { // Recoverable error. Show a dialog prompting the user to // install/update/enable Google Play services. GooglePlayServicesUtil.showErrorDialogFragment( errorCode, this, ERROR_DIALOG_REQUEST_CODE, new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { // The user chose not to take the recovery action onProviderInstallerNotAvailable(); } }); } else { // Google Play services is not available. onProviderInstallerNotAvailable(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == ERROR_DIALOG_REQUEST_CODE) { // Adding a fragment via GooglePlayServicesUtil.showErrorDialogFragment // before the instance state is restored throws an error. So instead, // set a flag here, which will cause the fragment to delay until // onPostResume. mRetryProviderInstall = true; } } /** * On resume, check to see if we flagged that we need to reinstall the * provider. */ @Override protected void onPostResume() { super.onPostResult(); if (mRetryProviderInstall) { // We can now safely retry installation. ProviderInstall.installIfNeededAsync(this, this); } mRetryProviderInstall = false; } private void onProviderInstallerNotAvailable() { // This is reached if the provider cannot be updated for some reason. // App should consider all HTTP communication to be vulnerable, and take // appropriate action. } } ~~~
';

使用HTTPS与SSL

最后更新于:2022-04-01 01:47:26

> 编写:[craftsmanBai](https://github.com/craftsmanBai) - [http://z1ng.net](http://z1ng.net) - 原文: [http://developer.android.com/training/articles/security-ssl.html](http://developer.android.com/training/articles/security-ssl.html) SSL,传输层安全([TSL](http://en.wikipedia.org/wiki/Transport_Layer_Security)),是一个常见的用来加密客户端和服务器通信的模块。但是应用程序错误地使用SSL可能会导致应用程序的数据在网络中被恶意攻击者拦截。为了帮助你确保这种情况不在你的应用中发生,这篇文章主要说明使用网络安全协议常见的陷阱和使用[Public-Key Infrastructure(PKI)](http://en.wikipedia.org/wiki/Public-key_infrastructure)时一些值得关注的问题。 ### 概念 一个典型的SSL使用场景是,服务器配置中包含了一个证书,有匹配的公钥和私钥。作为SSL客户端和服务端握手的一部分,服务端通过使用[public-key cryptography(公钥加密算法)](http://en.wikipedia.org/wiki/Public-key_cryptography)进行证书签名来证明它有私钥。 然而,任何人都可以生成他们自己的证书和私钥,因此一次简单的握手不能证明服务端具有匹配证书公钥的私钥。一种解决这个问题的方法是让客户端拥有一套或者更多可信赖的证书。如果服务端提供的证书不在其中,那么它将不能得到客户端的信任。 这种简单的方法有一些缺陷。服务端应该根据时间升级到强壮的密钥(key rotation),更新证书中的公钥。不幸的是,现在客户端应用需要根据服务端配置的变化来进行更新。如果服务端不在应用程序开发者的控制下,问题将变得更加麻烦,比如它是一个第三方网络服务。如果程序需要和任意的服务器进行对话,例如web浏览器或者email应用,这种方法也会带来问题。 为了解决这个问题,服务端通常配置了知名的的发行者证书(称为[Certificate Authorities(CAs)](http://en.wikipedia.org/wiki/Certificate_authority).提供的平台通常包含了一系列知名可信赖的CAs。Android4.2(Jelly Bean)包含了超过100CAs并在每个发行版中更新。和服务端相似的是,一个CA拥有一个证书和一个私钥。当为一个服务端发布颁发证书的时候,CA用它的私钥为服务端签名。客户端可以通过服务端拥有被已知平台CA签名的证书来确认服务端。 然而,使用CAs又带来了其他的问题。因为CA为许多服务端证书签名,你仍然需要其他的方法来确保你对话的是你想要的服务器。为了解决这个问题,使用CA签名的的证书通过特殊的名字如 gmail.com 或者带有通配符的域名如 *.google.com 来确认服务端。下面这个例子会使这些概念具体化一些。[openssl](http://www.openssl.org/docs/apps/openssl.html)工具的客户端命令关注Wikipedia服务端证书信息。端口为443,因为默认为HTTPS。这条命令将open s_client的输出发送给openssl x509,根据[X.509 standard](http://en.wikipedia.org/wiki/X.509)格式化证书中的内容。特别的是,这条命令需要subject,包含服务端名字和issuer来确认CA。 ~~~ $ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA ~~~ 可以看到RapidSSL CA颁发给匹配*.wikipedia.org的服务端证书。 ### 一个HTTP的例子 假设你有一个知名CA颁发证书的web服务器,你可以使用下面的代码发送一个安全请求: ~~~ URL url = new URL("https://wikipedia.org"); URLConnection urlConnection = url.openConnection(); InputStream in = urlConnection.getInputStream(); copyInputStreamToOutputStream(in, System.out); ~~~ 是的,它就是这么简单。如果你想要修改HTTP的请求,你可以把它扔到 [HttpURLConnection](http://developer.android.com/reference/java/net/HttpURLConnection.html).Android关于[HttpURLConnetcion](http://developer.android.com/reference/java/net/HttpURLConnection.html)文档中还有更贴切的例子关于怎样去处理请求、响应头、posting的内容、cookies管理、使用代理、抓responses等等。但是就这些确认证书和域名的细节而言,Android框架已经通过API来为你考虑这些细节。下面是其他需要关注的问题。 ### 服务器普通问题的验证 假设从[getInputStream()](http://developer.android.com/reference/java/net/URLConnection.html#getInputStream()接受内容,会抛出一个异常: ~~~ javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374) at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209) at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478) at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433) at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290) at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240) at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282) at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177) at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271) ~~~ 这种情况发生的原因包括: 1.[颁布证书给服务器的CA不是知名的。](http://developer.android.com/training/articles/security-ssl.html#UnknownCa) 2.[服务器证书不是CA签名的而是自己签名的。](http://developer.android.com/training/articles/security-ssl.html#SelfSigned) 3.[服务器配置缺失了中间CA](http://developer.android.com/training/articles/security-ssl.html#MissingCa) 下面将会分别讨论当你和服务器安全连接时如何去解决这些问题。 ### 无法识别证书机构 在这种情况中,[SSLHandshakeException](http://developer.android.com/reference/javax/net/ssl/SSLHandshakeException.html)异常产生的原因是你有一个不被系统信任的CA。可能是你的证书来源于新CA而不被安卓信任,也可能是你的应用运行版本较老没有CA。更多的时候,一个CA不知名是因为它不是公开的CA,而是政府,公司,教育机构等组织私有的。 幸运的是,你可以教会[HttpsURLConnection](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html)学会信任特殊的CA。过程可能会让人感到有一些费解,下面这个例子是从[InputStream](http://developer.android.com/reference/java/io/InputStream.html)中获得特殊的CA,使用它去创建一个密钥库,用来创建和初始化[TrustManager](http://developer.android.com/reference/javax/net/ssl/TrustManager.html)。[TrustManager](http://developer.android.com/reference/javax/net/ssl/TrustManager.html)是系统用来验证服务器证书的,这些证书通过使用[TrustManager](http://developer.android.com/reference/javax/net/ssl/TrustManager.html)信任的CA和密钥库中的密钥创建。给定一个新的TrustManager,下面这个例子初始化了一个新的[SSLContext](http://developer.android.com/reference/javax/net/ssl/SSLContext.html),提供了一个[SSLSocketFactory](http://developer.android.com/reference/javax/net/ssl/SSLSocketFactory.html),你可以覆盖来自[HttpsURLConnection](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html)的默认[SSLSocketFactory](http://developer.android.com/reference/javax/net/ssl/SSLSocketFactory.html)。这样连接时会使用你的CA来进行证书验证。 下面是一个华盛顿的大学的组织性的CA的使用例子 ~~~ // Load CAs from an InputStream // (could be from a resource or ByteArrayInputStream or ...) CertificateFactory cf = CertificateFactory.getInstance("X.509"); // From https://www.washington.edu/itconnect/security/ca/load-der.crt InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt")); Certificate ca; try { ca = cf.generateCertificate(caInput); System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN()); } finally { caInput.close(); } // Create a KeyStore containing our trusted CAs String keyStoreType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(keyStoreType); keyStore.load(null, null); keyStore.setCertificateEntry("ca", ca); // Create a TrustManager that trusts the CAs in our KeyStore String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); tmf.init(keyStore); // Create an SSLContext that uses our TrustManager SSLContext context = SSLContext.getInstance("TLS"); context.init(null, tmf.getTrustManagers(), null); // Tell the URLConnection to use a SocketFactory from our SSLContext URL url = new URL("https://certs.cac.washington.edu/CAtest/"); HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); urlConnection.setSSLSocketFactory(context.getSocketFactory()); InputStream in = urlConnection.getInputStream(); copyInputStreamToOutputStream(in, System.out); ~~~ 使用一个常用的了解你CA的TrustManager,系统可以确认你的服务器证书来自于一个可信任的发行者。 注意:许多网站提供了简陋的二选一方案是否安装TrustManager。如果你这样做还不如不加密通讯过程,因为任何人都可以在公共wifi热点下,使用伪装成你的服务器的代理发送你的用户流量,进行DNS欺骗,来攻击你的用户。然后攻击者便可记录用户密码和其他个人资料。这种方式奏效是因为攻击者可以生成一个证书,并且缺少可以验证该证书是否来自受信任的来源的TrustManager。你的应用可以同任何人会话。所以不这样做,暂时的也不行。如果你能始终让你的应用信任服务器证书的发行者,那么你可以这么做。 ### 自签名服务器证书 第二种[SSLHandshakeException](http://developer.android.com/reference/javax/net/ssl/SSLHandshakeException.html)取决于自签名证书,意味着服务器就是它自己的CA。这同未知证书权威机构类似,因此你同样可以用前面部分中提到的方法。 你可以创建你自己的TrustManager,这一次直接信任服务器证书。有之前提到的将你的应用直接捆绑证书的所有缺点,但是可以安全的执行。然而你应该小心确保你的自签名证书拥有合适的强密钥。到2012年,一个2048位65537指数位一年到期的RSA签名是合理的。当轮换密钥时,你应该查看权威机构(比如[NIST](http://www.nist.gov/))的建议([recommendation](http://csrc.nist.gov/groups/ST/key_mgmt/index.html))来了解哪种密钥是合适的。 ### 缺少中间证书颁发机构 第三种SSLHandshakeException情况的产生于缺少中间CA。大多数公开的CA不直接给服务器签名。相反,他们使用它们主要的机构(简称根认证机构)证书来给中间认证机构签名,他们这样做,因此根认证机构可以离线存储减少危险。然而,操作系统典型的比如安卓只信任直接地根认证机构,在服务器证书(由中间证书颁发机构签名)和证书验证者(只知道根认证机构)之间留下了一个缺口。为了解决这个问题,服务器并不在SSL握手的过程中只向客户端发送它的证书,而是一系列的从服务器到必经的任何中间机构到达根认证机构的证书。 下面是一个 mail.google.com证书链,以openssls_client命令显示: ~~~ $ openssl s_client -connect mail.google.com:443 --- Certificate chain 0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mail.google.com i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA 1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority --- ~~~ 这里显示了一台服务器发送了一个Thawte SGC CA为mail.google.com颁发的证书,Thawte SGC CA是一个中间证书颁发机构,Thawte SGC CA的证书由被安卓信任的Verisign CA颁发。然而,配置一台服务器不包括中间证书机构是不常见的。例如,一台服务器导致安卓浏览器的错误和应用的异常: ~~~ $ openssl s_client -connect egov.uscis.gov:443 --- Certificate chain 0 s:/C=US/ST=District Of Columbia/L=Washington/O=U.S. Department of Homeland Security/OU=United States Citizenship and Immigration Services/OU=Terms of use at www.verisign.com/rpa (c)05/CN=egov.uscis.gov i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)10/CN=VeriSign Class 3 International Server CA - G3 --- ~~~ 更有趣的是,用大所属桌面浏览器访问这台服务器不会导致类似于完全未知CA的或者自签名的服务器证书导致的错误。这是因为大多数桌面浏览器缓存随着时间的推移信任中间证书机构。一旦浏览器访问并且从一个网站了解到的一个中间证书机构,下一次它将不需要中间证书机构包含证书链。 一些站点故意这样做目的是让二级服务器用来提供资源服务。比如,他们可能会让他们的主HTML页面用一台拥有全部证书链的服务器来提供,但是像图片,CSS,或者JavaScript等这样的资源用不包含CA的服务器来提供,以此节省带宽。不幸的是,有时这些服务器可能会提供一个在应用中调用的web服务。这里有两种解决这些问题的方法: - 配置服务器使它包含服务器链中的中间证书颁发机构 - 或者,像对待不知名的CA一样对待中间CA,并且创建一个TrustManager来直接信任它,就像在前两节中做的那样。 ### 验证主机名常见问题 就像在文章开头提到的那样,有两个关键的部分来确认SSL的连接。第一个是确认证书来源于信任的源,这也是前一个部分关注的焦点。这一部分关注第二部分:确保你当前对话的服务器有正确的证书。当情况不是这样时,你可能会看到这样的典型错误: ~~~ java.io.IOException: Hostname 'example.com' was not verified at libcore.net.http.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223) at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446) at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290) at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240) at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282) at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177) at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271) ~~~ 服务器配置错误可能会导致这种情况发生。服务器配置了一个证书,这个证书没有匹配的你想连接的服务器的subject或者命名空间中二选一的subject。一个证书被许多不同的服务器使用是可能的。比如,使用 [openssl](http://www.openssl.org/docs/apps/openssl.html) s_client -connect google.com:443 |openssl x509 -text 查看google证书,你可以看到一个subject支持 _google.con _.youtube.com, *.android.com或者其他的。这种错误只会发生在你在连接的服务器名称没有被证书列为可接受。 不幸的是另外一种原因也会导致这种情况发生:[虚拟化服务](http://en.wikipedia.org/wiki/Virtual_hosting)。当用HTTP同时拥有一个以上主机名的服务器共享时,web服务器可以从 HTTP/1.1请求中找到客户端需要的目标主机名。不行的是,使用HTTPS会使情况变得复杂,因为服务器必须知道在发现HTTP请求前返回哪一个证书。为了解决这个问题,新版本的SSL,特别是TLSV.1.0和之后的版本,支持[服务器名指示(SNI)](http://en.wikipedia.org/wiki/Server_Name_Indication),允许SSL客户端为服务端指定目标主机名,从而返回正确的证书。幸运的是,从安卓2.3开始,[HttpsURLConnection](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html)支持SNI。不幸的是,Apache HTTP客户端不这样,这也是我们不鼓励用它的原因之一。如果你需要支持安卓2.2或者更老的版本或者Apache HTTP客户端,一个解决方法是建立一个可选的虚拟化服务并且使用特别的端口,这样服务端就能够清楚该返回哪一个证书。 采用不使用你的虚拟服务的主机名[HostnameVerifier](http://developer.android.com/reference/javax/net/ssl/HostnameVerifier.html)而不是服务器默认的来替换,是很重要的选择。 注意:替换[HostnameVerifier](http://developer.android.com/reference/javax/net/ssl/HostnameVerifier.html)可能会非常危险,如果另外一个虚拟服务不在你的控制下,中间人攻击可能会直接使流量到达另外一台服务器而超出你的预想。如果你仍然确定你想覆盖主机名验证,这里有一个为单[URLConnection](http://developer.android.com/reference/java/net/URLConnection.html)替换验证过程的例子 ~~~ // Create an HostnameVerifier that hardwires the expected hostname. // Note that is different than the URL's hostname: // example.com versus example.org HostnameVerifier hostnameVerifier = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier(); return hv.verify("example.com", session); } }; // Tell the URLConnection to use our HostnameVerifier URL url = new URL("https://example.org/"); HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); urlConnection.setHostnameVerifier(hostnameVerifier); InputStream in = urlConnection.getInputStream(); copyInputStreamToOutputStream(in, System.out); ~~~ 但是请记住,如果你发现你在替换主机名验证,特别是虚拟服务,另外一个虚拟主机不在你的控制的情况是非常危险的,你应该找到一个避免这种问题产生的托管管理。 ### 关于直接使用SSL Socket的警告 到目前为止,这些例子聚焦于使用HttpsURLConnection上。有时一些应用需要让SSL和HTTP分开。举个例子,一个email应用可能会使用SSL的变种,SMTP,POP3,IMAP等。在那些例子中,应用程序会想使用[SSLSocket](http://developer.android.com/reference/javax/net/ssl/SSLSocket.html)直接连接,与HttpsURLConnection做的方法相似。这种技术到目前为止处理了证书验证问题,也应用于SSLSocket中。事实上,当使用常规的TrustManager时,传递给HttpsURLConnection的是SSLSocketFactory。如果你需要一个带常规的SSLSocket的TrustManager,采取下面的步骤使用SSLSocketFactory来创建你的SSLSocket。注意:SSLSocket不具有主机名验证功能。它取决于它自己的主机名验证,通过传入预期的主机名调用[getDefaultHostNameVerifier()](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html#getDefaultHostnameVerifier())。进一步需要注意的是,当发生错误时,[HostnameVerifier.verify()](http://developer.android.com/reference/javax/net/ssl/HostnameVerifier.html#verify(java.lang.String, javax.net.ssl.SSLSession)不知道抛出异常,而是返回一个布尔值,你需要进一步明确的检查。下面是一个演示的方法。这个例子演示了当它连接gmail.com 443端口并且没有SNI支持的时候,你将会收到一个mail.google.com的证书。你需要确保证书的确是mail.google.com的。 ~~~ // Open SSLSocket directly to gmail.com SocketFactory sf = SSLSocketFactory.getDefault(); SSLSocket socket = (SSLSocket) sf.createSocket("gmail.com", 443); HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier(); SSLSession s = socket.getSession(); // Verify that the certicate hostname is for mail.google.com // This is due to lack of SNI support in the current SSLSocket. if (!hv.verify("mail.google.com", s)) { throw new SSLHandshakeException("Expected mail.google.com, " "found " + s.getPeerPrincipal()); } // At this point SSLSocket performed certificate verificaiton and // we have performed hostname verification, so it is safe to proceed. // ... use socket ... socket.close(); ~~~ ### 黑名单 SSL 主要依靠CA来确认证书来自正确无误服务器和域名的所有者。少数情况下,CA被欺骗,或者在[Comodo](http://en.wikipedia.org/wiki/Comodo_Group#Breach_of_security)和[DigiNotar](http://en.wikipedia.org/wiki/DigiNotar)的例子中,一个主机名的证书被颁发给了除了服务器和域名的拥有者之外的人,导致被破坏。 为了减少这着危险,安卓可以将一些黑名单或者整个CA列入黑名单。尽管名单是以前是嵌入操作系统的,从安卓4.2开始,这个名单在以后的方案中可以远程更新。 ### 阻塞 一个应用可以通过阻塞技术保护它自己免于受虚假证书的欺骗。这是简单运用使用未知CA的例子,限制应用信任的CA仅来自被应用使用的服务器。阻止了来自系统中另外一百多个CA的欺骗而导致的应用安全通道的破坏。 ### 客户端验证 这篇文章聚焦在SSL的使用者同服务器的安全对话上。SSL也支持服务端通过验证客户端的证书来确认客户端的身份。这种技术也与TrustManager的特性相似。可以参考在[HttpsURLConnection](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html)文档中关于创建一个常规的[KeyManager](http://developer.android.com/reference/javax/net/ssl/KeyManager.html)的讨论。 ### nogotofail:网络流量安全测试工具 对于已知的TLS/SSL漏洞和错误,nogotofail提供了一个简单的方法来确认你的应用程序是安全的。它是一个自动化的、强大的、用于测试网络的安全问题可扩展性的工具,任何设备的网络流量都可以通过它。nogotofail主要应用于三种场景: - 发现错误和漏洞。 - 验证修补程序和等待回归。 - 了解应用程序和设备产生的交通。 nogotofail 可以工作在Android,iOS,Linux,Windows,Chrome OS,OSX环境下,事实上任何需要连接到Internet的设备都可以。Android和Linux环境下有简单易用获取通知的客户端配置设置,以及本身可以作为靶机,部署为一个路由器,VPN服务器,或代理。你可以在nogotofail开源项目访问该工具。
';

Security Tips

最后更新于:2022-04-01 01:47:24

# 安全要点 > 编写:[craftsmanBai](https://github.com/craftsmanBai) - [http://z1ng.net](http://z1ng.net) - 原文:[http://developer.android.com/training/articles/security-tips.html](http://developer.android.com/training/articles/security-tips.html) Android的安全性体现在系统显著地减少了应用程序安全问题带来的影响。你可以在默认的系统设置和文件权限设置的环境下建立应用,避免了一堆头疼的安全问题。 帮助你建立应用的部分核心安全特性如下: - Android应用程序沙盒,将你的应用数据和代码同其他程序隔开。 - 具有鲁棒性实现了常见安全功能的应用框架,例如密码学应用,权限控制,安全IPC - 使用ASLR, NX,ProPolice,safe_iop,OpenBSD dlmalloc,OpenBSD calloc,Linux mmap_min_addr等技术,减少了常见内存管理错误。 - 加密文件系统可以保护丢失或被盗走的设备数据。 - 用户权限控制限制访问系统详细情况和用户数据。 - 应用程序权限以单个应用为基础控制其数据。 尽管如此,熟悉Android安全特性仍然很重要。遵守这些习惯将其作为优秀的代码风格,能够减少无意间给用户带来的安全问题。 ### 数据存储 对于一个Android的应用程序来说,最为常见的安全问题是存放在设备上的数据能否被其他应用获取。在设备上存放数据基本方式有三种: ### 使用内存储器 默认情况下,你在[内存储器](http://developer.android.com/guide/topics/data/data-storage.html#filesInternal)中创建的文件只有你的应用可以访问。这种机制被Android加强了并且对于大多数应用程序都是有效的。你应该避免在IPC文件中使用[MODE_WORLD_WRITEABLE](http://developer.android.com/reference/android/content/Context.html#MODE_WORLD_WRITEABLE)或者[MODE_WORLD_READABLE](http://developer.android.com/reference/android/content/Context.html#MODE_WORLD_READABLE)模式,因为它们不为特殊程序提供限制数据访问的功能,它们也不对数据格式进行任何控制。如果你想与其他应用的进程共享数据,可以使用[content provider](http://developer.android.com/guide/topics/providers/content-providers.html),它给其他应用提供了可读写权限以及逐项动态获取权限。 如果想对敏感数据进行特别保护,你可以使用应用程序无法直接获取的密钥来加密本地文件。例如,密钥可以存放在[密钥库](http://developer.android.com/reference/java/security/KeyStore.html)而非设备上,使用用户密码进行保护。尽管这种方式无法在具有root权限时监视用户输入的密码的情况下保护数据,但是它可以为未进行[文件系统加密](http://source.android.com/tech/encryption/index.html)已丢失的设备提供保护。 ### 使用外部存储器 创建于[外部存储](http://developer.android.com/guide/topics/data/data-storage.html#filesExternal)的文件,比如SD卡,是全局可读写的。由于外部存储器可被用户移除并且能够被任何应用修改,因此不应使用外部存储存储应用的敏感信息。当处理来自外部存储器的数据时,应用程序应该[执行输入验证](http://developer.android.com/training/articles/security-tips.html#InputValidation)(参看输入验证章节)我们强烈建议应用在动态加载之前不要把可执行文件或class文件存储到外部存储器中。如果一个应用从外部存储器检索可执行文件,那么在动态加载之前它们应该进行签名与加密验证。 ### 使用Content Providers [ContentProviders](http://developer.android.com/guide/topics/providers/content-providers.html)提供一个结构存储机制,可以限制你自己的应用或者导出给其他应用程序允许访问。如果你不打算为其他应用提供访问你的[ContentProvider](http://developer.android.com/reference/android/content/ContentProvider.html)功能,那么在manifest中标记他们为[android:exported=false](http://developer.android.com/guide/topics/manifest/provider-element.html#exported)即可。要建立一个给其他应用使用而导出的[ContentProvider](http://developer.android.com/reference/android/content/ContentProvider.html),你可以为读写操作指定一个单一的[permission](http://developer.android.com/guide/topics/manifest/provider-element.html#prmsn),或者在manifest中为读写操作指定确切的许可。我们强烈建议你限制权限给手头要求完成的任务。记住,通常显示新功能稍后加入许可比把许可撤走并打断已经存在的用户更容易。 如果你使用Content Provider仅在自己的应用中共享数据,使用签名级别[android:protectionLevel](http://developer.android.com/guide/topics/manifest/permission-element.html#plevel)的许可是更可取的。签名许可不需要用户确认,当应用使用同样的密钥获取数据时,这提供了更好的用户体验,也更好地控制了Content Provider数据的访问。Content Providers也可以通过声明[android:grantUriPermissions](http://developer.android.com/guide/topics/manifest/provider-element.html#gprmsn)并在触发组件的Intent对象中使用[FLAG_GRANT_READ_URI_PERMISSION](http://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_READ_URI_PERMISSION)和[FLAG_GRANT_WRITE_URI_PERMISSION](http://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_WRITE_URI_PERMISSION)标志提供更细致的访问。这些许可的作用域可以通过[grant-uri-permission](http://developer.android.com/guide/topics/manifest/grant-uri-permission-element.html)进一步限制。当访问一个ContentProvider时,使用参数化的查询方法,比如[query()](http://developer.android.com/reference/android/content/ContentProvider.html#query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String), [update()](http://developer.android.com/reference/android/content/ContentProvider.html#update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]),和[delete()](http://developer.android.com/reference/android/content/ContentProvider.html#delete(android.net.Uri, java.lang.String, java.lang.String[])来避免来自不信任源潜在的SQL注入。注意,如果提交方法之前的selection是通过连接用户数据建立的,使用参数化的方法是不够的。不要对“写”权限有一个错误的观念。考虑“写”权限允许sql语句,使得部分数据使用创造性的WHERE语句并且解析结果变为可能。例如:入侵者可能在通话记录中通过修改一条记录来检测某个特定存在的电话号码,只要那个电话号码已经存在。如果content provider数据有可预见的结构,提供“写”权限也许等同于同时提供了“读写”权限。 ### 使用许可 因为安卓沙盒将应用程序隔开,程序必须显式地共享资源和数据。它们通过声明他们需要的权限来获取额外的功能,而基本的沙盒不提供这些功能,比如相机访问设备。 ### 请求许可 我们建议最小化应用请求的许可数量,不具有访问敏感资料的权限可以减少无意中滥用这些权限的风险,可以增加用户接受度,并且减少应用可被攻击者攻击利用。 如果你的应用可以设计成不需要任何许可,那最好不过。例如:与其请求访问设备信息来建立一个标识,不如建立一个[GUID](http://developer.android.com/reference/java/util/UUID.html)(这个例子在[Handling User Data](http://developer.android.com/training/articles/security-tips.html#UserData)中有说明)。 除了请求许可之外,你的应用可以使用[permissions](http://developer.android.com/guide/topics/manifest/permission-element.html)来保护可能会暴露给其他应用的安全敏感的IPC:比如[ContentProvider](http://developer.android.com/reference/android/content/ContentProvider.html)。通常来说,我们建议使用访问控制而不是用户权限确认许可,因为许可会使用户感到困惑。例如,考虑在权限设置上为应用间的IPC通信使用单一开发者提供的[签名保护级别](http://developer.android.com/guide/topics/manifest/permission-element.html#plevel) 不要泄漏受许可保护的数据。只有当应用通过IPC暴露数据才会发生这种情况,因为它具有特殊权限,却不要求任何客户端的IPC接口有那样的权限。更多细节带来的潜在影响以及这种问题发生的频率在USENIX: [http://www.cs.be rkeley.edu/~afelt/felt_usenixsec2011.pdf](http://www.cs.berkeley.edu/~afelt/felt_usenixsec2011.pdf)研究论文中都有说明。 ### 创建许可 通常,你应该力求建立拥有尽量少许可的应用,直至满足你的安全需要。建立一个新的许可对于大多数应用相对少见,因为[系统定义的许可](http://developer.android.com/reference/android/Manifest.permission.html)覆盖很多情况。在适当的地方使用已经存在的许可执行访问检查。 如果必须建立一个新的许可,考虑能否使用[signature protection level](http://developer.android.com/guide/topics/manifest/permission-element.html#plevel)来完成你的任务。签名许可对用户是透明的并且只允许相同开发者签名的应用访问,与应用执行许可检查一样。如果你建立一个[dagerous protction level](http://developer.android.com/guide/topics/manifest/permission-element.html#plevel),那么用户需要决定是否安装这个应用。这会使其他开发者困惑,也使用户困惑。 如果你要建立一个危险的许可,则会有多种复杂情况需考虑: - 对于用户将要做出的安全决定,许可需要用字符串对其进行简短的表述。 - 许可字符串必须保证语言的国际化。 - 用户可能对一个许可感到困惑或者知晓风险而选择不安装应用 - 当许可的创造未安装的时候,应用可能要求许可。 上面每一个因素都为应用开发者带来了重要的非技术挑战,同时也使用户感到困惑,这也是我们不建议使用危险许可的原因。 ### 使用网络 网络交易具有很高的安全风险,因为它涉及到传送私人的数据。人们对移动设备的隐私关注日益加深,特别是当设备进行网络交易时,因此应用采取最佳方式保护用户数据安全极为重要。 ### 使用IP网络 android下的网络与Linux环境下的差别并不大。主要考虑的是确保对敏感数据采用了适当的协议,比如使用[HTTPS进行网络传输](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html)。我们在任何支持HTTPS的服务器上更愿意使用HTTPS而不是HTTP,因为移动设备频繁连接不安全的网络,比如公共WiFi热点。 认证加密socket级别的通信可通过使用[SSLSocket](http://developer.android.com/reference/javax/net/ssl/SSLSocket.html)类轻松实现。由于Android设备使用WiFi连接不安全网络的频率,对于所有应用来说,使用安全网络是极力鼓励支持的。 我们发现部分应用使用[localhost](http://en.wikipedia.org/wiki/Localhost)端口处理敏感的IPC。不鼓励这种方法是因为这些接口可被设备上的其他应用访问。相反,你应该在可认证的地方使用android IPC机制,例如[Service](http://developer.android.com/reference/android/app/Service.html)(比使用回环还糟的是绑定INADDR_ANY,因为你的应用可能收到来自任何地方来的请求,我们也已经见识过了)。 一个有必要重复的常见议题是,确保不信任从HTTP或者其他不安全协议下载的数据。这包括在[WebView](http://developer.android.com/reference/android/webkit/WebView.html)中的输入验证和对于http的任何响应。 ### 使用电话网络 SMS协议是Android开发者使用最频繁的电话协议,主要为用户与用户之间的通信设计,但对于想要传送数据的应用来说并不合适。由于SMS的限制性,我们强烈建议使用[Google Cloud Messaging](http://developer.android.com/google/gcm/index.html)(GCM)和IP网络从web服务器发送数据消息给用户设备应用。 很多开发者没有意识到SMS在网络上或者设备上是不加密的,也没有牢固验证。特别是任何SMS接收者应该预料到恶意用户也许已经给你的应用发送了SMS:不要指望未验证的SMS数据执行敏感操作。你也应该注意到SMS在网络上也许会遭到冒名顶替并且/或者拦截,对于Android设备本身,SMS消息是通过广播intent传递的,所以他们也许会被其他拥有[READ_SMS](http://developer.android.com/reference/android/Manifest.permission.html#READ_SMS)许可的应用截获。 ### 输入验证 无论应用运行在什么平台上,功能不完善的输入验证是最常见的影响应用安全问题之一。Android有平台级别的对策,用于减少应用的公开输入验证问题,你应该在可能的地方使用这些功能。同样需要注意的是,选择类型安全的语言能减少输入验证问题。 如果你使用native代码,那么任何从文件读取的,通过网络接收的,或者通过IPC接收的数据都有可能引发安全问题。最常见的问题是[buffer overflows](http://en.wikipedia.org/wiki/Buffer_overflow),[use after free](http://en.wikipedia.org/wiki/Double_free#Use_after_free),和[off-by-one](http://en.wikipedia.org/wiki/Off-by-one_error)。Android提供安全机制比如ASLR和DEP以减少这些漏洞的可利用性,但是没有解决基本的问题。小心处理指针和管理缓存可以预防这些问题。 动态、基于字符串的语言,比如JavaScript和SQL,都常受到由转义字符和[脚本注入](http://en.wikipedia.org/wiki/Code_injection)带来的输入验证问题。 如果你使用提交到SQL Database或者Content Provider的数据,SQL注入也许是个问题。最好的防御是使用参数化的查询,就像ContentProviders中讨论的那样。限制权限为只读或者只写可以减少SQL注入的潜在危害。 如果你不能使用上面提到的安全功能,我们强烈建议使用结构严谨的数据格式并且验证符合期望的格式。黑名单策略与替换危险字符是有效的,但这些技术在实践中是易错的并且当错误可能发生的时候应该尽量避免。 ### 处理用户数据 通常来说,处理用户数据安全最好的方法是最小化获取敏感数据用户个人数据的API使用。如果你对数据进行访问并且可以避免存储或传输,那就不要存储和传输数据。最后,思考是否有一种应用逻辑可能被实现为使用hash或者不可逆形式的数据。例如,你的应用也许使用一个email地址的hash作为主键,避免传输或存储email地址,这减少无意间泄漏数据的机会,并且也能减少攻击者尝试利用你的应用的机会。 如果你的应用访问私人数据,比如密码或者用户名,记住司法也许要求你提供一个使用和存储这些数据的隐私策略的解释。所以遵守最小化访问用户数据最佳的安全实践也许只是简单的服从。 你也应该考虑你应用是否会疏忽暴露个人信息给其他方,比如广告第三方组件或者你应用使用的第三方服务。如果你不知道为什么一个组件或者服务请求个人信息,那么就不要提供给它。通常来说,通过减少应用访问个人信息,会减少这个区域潜在的问题。 如果必须访问敏感数据,评估这个信息是否必须要传到服务器,或者是否可以被客户端操作。考虑客户端上使用敏感数据运行的任何代码,避免传输用户数据确保不会无意间通过过渡自由的IPC、world writable文件、或网络socket暴露用户数据给其他设备上的应用。这里有一个泄漏权限保护数据的特别例子,在[Requesting Permissions](http://developer.android.com/training/articles/security-tips.html#RequestingPermissions)章节中讨论。 如果需要GUID,建立一个大的、唯一的数字并保存它。不要使用电话标识,比如与个人信息相关的电话号码或者IMEI。这个话题在[Android Developer Blog](http://android-developers.blogspot.com/2011/03/identifying-app-installations.html)中有更详细的讨论。 应用开发者应谨慎的把log写到机器上。在Android中,log是共享资源,一个带有[READ_LOGS](http://developer.android.com/reference/android/Manifest.permission.html#READ_LOGS)许可的应用可以访问。即使电话log数据是临时的并且在重启之后会擦除,不恰当地记录用户信息会无意间泄漏用户数据给其他应用。 ### 使用WebView 因为[WebView](http://developer.android.com/reference/android/webkit/WebView.html)能包含HTML和JavaScript浏览网络内容,不恰当的使用会引入常见的web安全问题,比如[跨站脚本攻击](http://en.wikipedia.org/wiki/Cross_site_scripting)(JavaScript注入)。Android采取一些机制通过限制WebView的能力到应用请求功能最小化来减少这些潜在的问题。 如果你的应用没有在WebView内直接使用JavaScript,不要调用[setJavaScriptEnabled()](http://developer.android.com/reference/android/webkit/WebSettings.html#setJavaScriptEnabled(boolean)。某些样本代码使用这种方法,可能会导致在产品应用中改变用途:所以如果不需要的话移除它。默认情况下WebView不执行JavaScript,所以跨站脚本攻击不会产生。 使用[addJavaScriptInterface()](http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)要特别的小心,因为它允许JavaScript执行通常保留给Android应用的操作。只把[addJavaScriptInterface()](http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)暴露给可靠的输入源。如果不受信任的输入是被允许的,不受信任的JavaScript也许会执行Android方法。总得来说,我们建议只把[addJavaScriptInterface()](http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)暴露给你应用内包含的JavaScript。 如果你的应用通过WebView访问敏感数据,你也许想要使用[clearCache()](http://developer.android.com/reference/android/webkit/WebView.html#clearCache(boolean)方法来删除任何存储到本地的文件。服务端的header,比如no-cache,能用于指示应用不应该缓存特定的内容。 ### 处理证书 通常来说,我们建议请求用户证书频率最小化--使得钓鱼攻击更明显,并且降低其成功的可能。取而代之使用授权令牌然后刷新它。 可能的情况下,用户名和密码不应该存储到设备上,而使用用户提供的用户名和密码执行初始认证,然后使用一个短暂的、特定服务的授权令牌。可以被多个应用访问的service应该使用[AccountManager](http://developer.android.com/reference/android/accounts/AccountManager.htmls)访问。如果可能的话,使用AccountManager类来执行基于云的服务并且不把密码存储到设备上。 使用AccountManager获取[Account](http://developer.android.com/reference/android/accounts/Account.html)之后,进入任何证书前检查[CREATOR](http://developer.android.com/reference/android/accounts/Account.html#CREATOR),这样你就不会因为疏忽而把证书传递给错误的应用。 如果证书只是用于你创建的应用,那么你能使用[checkSignature()](http://developer.android.com/reference/android/content/pm/PackageManager.html#checkSignatures(int, int)验证访问AccountManager的应用。或者,如果一个应用要使用证书,你可以使用[KeyStore](http://developer.android.com/reference/java/security/KeyStore.html)来储存。 ### 使用密码学 除了采取数据隔离之外,支持完整的文件系统加密,并且提供安全交流通道。Android提供大量加密算法来保护数据。 通常来说,尝试使用最高级别的已存在framework的实现来支持,如果你需要安全的从一个已知的位置取回一个文件,一个简单的HTTPS URI也许就足够了,并且这部分不要求任何加密知识。如果你需要一个安全隧道,考虑使用[HttpsURLConnection](http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html)或者[SSLSocket](http://developer.android.com/reference/javax/net/ssl/SSLSocket.html)要比使用你自己的协议好。 如果你发现你的确需要实现你自己的协议,我们强烈建议你不要自己实现加密算法。使用已经存在的加密算法,比如[Cipher](http://developer.android.com/reference/javax/crypto/Cipher.html)类中提供的AES或者RSA。 使用一个安全的随机数生成器([SecureRandom](http://developer.android.com/reference/java/security/SecureRandom.html))来初始化加密的key([KeyGenerator](http://developer.android.com/reference/javax/crypto/KeyGenerator.html))。使用一个不安全随机数生成器生成的key严重削弱算法的优点,而且可能遭到离线攻击。 如果你需要存储一个key来重复使用,使用类似于[KeyStore](http://developer.android.com/reference/java/security/KeyStore.html)的机制,提供长期储存和检索加密key的功能。 ### 使用进程间通信 一些Android应用试图使用传统的Linux技术实现IPC,比如网络socket和共享文件。我们强烈鼓励使用Android系统IPC功能,比如[Intent](http://developer.android.com/reference/android/content/Intent.html),[Binder](http://developer.android.com/reference/android/os/Binder.html),[Messenger](http://developer.android.com/reference/android/os/Messenger.html)和[BroadcastReceiver](http://developer.android.com/reference/android/content/BroadcastReceiver.html)。Android IPC机制允许你为每一个IPC机制验证连接到你的IPC和设置安全策略的应用的身份。 很多安全元素通过IPC机制共享。Broadcast Receiver, Activitie,和Service都在应用的manifest中声明。如果你的IPC机制不打算给其他应用使用,设置`android:exported`属性为false。这对于同一个UID内包含多个进程的应用,或者在开发后期决定不想通过IPC暴露功能并且不想重写代码的时候非常有用。 如果你的IPC打算让别的应用访问,你可以通过使用Permission标记设置一个安全策略。如果IPC是使用同一个密钥签名的独立的应用间的,使用[signature](http://developer.android.com/guide/topics/manifest/permission-element.html#plevel)更好一些。 ### 使用意图 Intent是Android中异步IPC机制的首选。根据你应用的需求,你也许使用[sendBroadcast()](http://developer.android.com/reference/android/content/Context.html#sendBroadcast(android.content.Intent),[sendOrderedBroadcast()](http://developer.android.com/reference/android/content/Context.html#sendOrderedBroadcast(android.content.Intent, java.lang.String)或者直接的intent来指定一个应用组件。 注意,有序广播可以被接收者“消费”,所以他们也许不会被发送到所有的应用中。如果你要发送一个intent给指定的receiver,这个intent必须被直接的发送给这个receiver。 intent的发送者能在发送的时候验证接受者是否有一个许可指定了一个non-Null Permission。只有有那个许可的应用才会收到这个intent。如果广播intent内的数据是敏感的,你应该考虑使用许可来保证恶意应用没有恰当的许可无法注册接收那些消息。这种情况下,可以考虑直接执行这个receiver而不是发起一个广播。 注意:intent过滤器不能作为安全特性--组件可被intent显式调用,可能会没有符合intent过滤器的数据。你应该在intent接收器内执行输入验证,确认对于调用接收者,服务、或活动来说格式正确合理。 ### 使用服务 [Service](http://developer.android.com/reference/android/app/Service.html)经常被用于为其他应用提供服务。每个service类必须在它的manifest文件进行相应的声明。 默认情况下,Service不能被导出和被其他应用执行。如果你加入了任何intent过滤器到服务蛇呢光明中,那么它默认为可以被导出。最好明确声明[android:exported](http://developer.android.com/guide/topics/manifest/service-element.html#exported)元素来确定它按照你设想的运行。可以使用[android:permission](http://developer.android.com/guide/topics/manifest/service-element.html#prmsn)保护Service。这样做,其他应用在他们自己的manifest文件中将需要声明相应的[](http://developer.android.com/guide/topics/manifest/uses-permission-element.html)元素来启动、停止或者绑定到这个service上。 一个Service可以使用许可保护单独的IPC调用,在执行调用前通过调用[checkCallingPermission()](http://developer.android.com/reference/android/content/Context.html#checkCallingPermission(java.lang.String)来实现。我们建议使用manifest中声明的许可,因为那些是不容易监管的。 ### 使用binder和messenger接口 在Android中,[Binders](http://developer.android.com/reference/android/os/Binder.html)和[Messenger](http://developer.android.com/reference/android/os/Messenger.html)是RPC-style IPC的首选机制。必要的话,他们提供一个定义明确的接口,促进彼此的端点认证。 我们强烈鼓励在一定程度上设计不要求指定许可检查的接口。Binder和[Messenger](http://developer.android.com/reference/android/os/Messenger.html)不在应用的manifest中声明,因此你不能直接在Binder上应用声明的许可。它们在应用的manifest中继承许可声明,[Service](http://developer.android.com/reference/android/app/Service.html)或者[Activity](# "An activity represents a single screen with a user interface.")内实现了许可。如果你打算创建一个接口,在一个指定binder接口上要求认证和/或者访问控制,这些控制必须在Binder和[Messenger](http://developer.android.com/reference/android/os/Messenger.html)的接口中明确添加代码。 如果提供一个需要访问控制的接口,使用[checkCallingPermission()](http://developer.android.com/reference/android/content/Context.html#checkCallingPermission(java.lang.String)来验证调用者是否拥有必要的许可。由于你的应用的id已经被传递到别的接口,因此代表调用者访问一个Service之前这尤其重要。如果调用一个Service提供的接口,如果你没有对给定的Service访问许可,[bindService()](http://developer.android.com/reference/android/content/Context.html#bindService(android.content.Intent, android.content.ServiceConnection, int)请求也许会失败。如果调用你自己的应用提供的本地接口,使用[clearCallingIdentity()](http://developer.android.com/reference/android/os/Binder.html#clearCallingIdentity()来进行内部安全检查是有用的。 更多关于用服务运行IPC的信息,参见[Bound Services](http://developer.android.com/guide/components/bound-services.html) ### 利用广播接收机 [Broadcast receivers](http://developer.android.com/reference/android/content/BroadcastReceiver.html)是用来处理通过[intent](http://developer.android.com/reference/android/content/Intent.html)发起的异步请求。 默认情况下,receiver是导出的,并且可以被任何其他应用执行。如果你的[BroadcastReceiver](http://developer.android.com/reference/android/content/BroadcastReceiver.html)打算让其他应用使用,你也许想在应用的manifest文件中使用[](http://developer.android.com/guide/topics/manifest/receiver-element.html)元素对receiver使用安全许可。这将阻止没有恰当许可的应用发送intent给这个[BroadcastReceiver](http://developer.android.com/reference/android/content/BroadcastReceiver.html)。 ### 动态加载代码 我们极为不鼓励从应用文件外加载代码。由于代码注入或者代码篡改这样做显著增加了应用暴露的可能,同事也增加了版本管理和应用测试的复杂性。最终可能造成无法验证应用的行为,因此在某些环境下应该被限制。 如果你的应用确实动态加载了代码,最重要的事情是记住运行动态加载的代码与应用具有相同的安全许可。用户决定安装你的应用是基于你的id,他们期望你提供任何运行在应用内部的代码,包括动态加载的代码。 动态加载代码主要的风险在于代码来源于可确认的源头。如果这个模块是之间直接包含在你的应用中,那么它们不能被其他应用修改,不论代码是本地库或者是使用[DexClassLoader](http://developer.android.com/reference/dalvik/system/DexClassLoader.html)加载的类这都是事实。我们见过很多应用实例尝试从不安全的地方加载代码,比如从网络上通过非加密的协议或者从world writable位置(比如外部存储)下载数据。这些地方会允许网络上其他人在传输过程中修改其内容,或者允许用户设备上的其他应用修改其内容。 ### 在虚拟机器安全性 Dalvik是安卓的运行时虚拟机(VM).Dalvik是特别为安卓建立的,但许多其他虚拟机相关的安全代码的也适用于安卓。一般来说,你不应该关心与自己有关的虚拟机的安全问题。你的应用程序在一个安全的沙盒环境下运行,所以系统上的其他进程无法访问你的代码或私人数据。 如果你想更深入了解虚拟机的安全问题,我们建议您熟悉一些现有文献的主题。推荐两个比较流行的资源: - [http://www.securingjava.com/toc.html](http://www.securingjava.com/toc.html) - [https://www.owasp.org/index.php/Java_Security_Resources](https://www.owasp.org/index.php/Java_Security_Resources) 这个文档集中于安卓与其他VM环境不同地方。对于有在其他环境下有VM编程经验开发者来说,这里有两个普遍的问题可能对于编写Android应用来说有些不同: - 一些虚拟机,比如JVM或者.net,担任一个安全的边界作用,代码与底层操作系统隔离。在Android上,Dalvik VM不是一个安全边界:应用沙箱是在系统级别实现的,所以Dalvik可以在同一个应用与native代码相互操作,没有任何安全约束。 - 已知的手机上的存储限制,对来发者来说,想要建立模块化应用和使用动态类加载是很常见的。要这么做的时候需要考虑两个资源:一是在哪里恢复你的应用逻辑,二是在哪里存储它们。不要从未验证的资源使用动态类加载器,比如不安全的网络资源或者外部存储,因为那些代码可能被修改为包含恶意行为。 ### 本地代码的安全 一般来说,对于大多数应用开发,我们鼓励开发者使用Android SDK而不是使用[Android NDK]([http://developer.android.com/tools/sdk/ndk/index.html](http://developer.android.com/tools/sdk/ndk/index.html)) 的native代码。编译native代码的应用更为复杂,移植性差,更容易包含常见的内存崩溃错误,比如缓冲区溢出。 Android使用Linux内核编译并且与Linux开发相似,如果你打算使用native代码,安全策略尤其有用。这篇文档讨论的最佳策略实在太少了,但最受欢迎的资源之一“Secure Programming for Linux and Unix HOWTO”,在这里可以找到[http://www.dwheeler.com/secure-programsAndroid](http://www.dwheeler.com/secure-programs)。 与大多数Linux环境的一个重要区别是应用沙箱。在Android中,所有的应用运行在应用沙箱中,包括用native代码编写的应用。在最基本的级别中,与Linux相似,对于开发者来说最好的方式是知道每个应用被分配一个权限非常有限的唯一UID。这里讨论的比[Android Security Overview](http://source.android.com/tech/security/index.html)中更细节化,你应该熟悉应用许可,即使你使用的是native代码。
';

Android安全与隐私

最后更新于:2022-04-01 01:47:21

> 编写:[craftsmanBai](https://github.com/craftsmanBai) - [http://z1ng.net](http://z1ng.net) - 原文:[http://developer.android.com/training/best-security.html](http://developer.android.com/training/best-security.html) 下面的课程教你如何确保应用程序数据的安全。 ### [安全要点](#) 怎样执在执行多个任务的同时确保应用程序数据和用户数据的安全。 ### [HTTPS和SSL的安全](#) 如何确保应用程序在进行网络传输时是安全的。 ### [更新你的Security Provider对抗SSL漏洞攻击](#) 如何使用和更新Google Play services security provider来对抗SSL漏洞攻击。 ### [企业级开发](#) 如何为企业级应用程序实施设备管理策略。
';

优化多核处理器(SMP)下的Android程序

最后更新于:2022-04-01 01:47:19

> 编写:[kesenhoo](https://github.com/kesenhoo) - 原文:[http://developer.android.com/training/articles/smp.html](http://developer.android.com/training/articles/smp.html) 从Android 3.0开始,系统针对多核CPU架构的机器做了优化支持。这份文档介绍了针对多核系统应该如何编写C,C++以及Java程序。这里只是作为Android应用开发者的入门教程,并不会深入讨论这个话题,并且我们会把讨论范围集中在ARM架构的CPU上。 如果你并没有时间学习整篇文章,你可以跳过前面的理论部分,直接查看实践部分。但是我们并不建议这样做。 ### 0)简要介绍 **SMP** 的全称是“**Symmetric Multi-Processor**”。 它表示的是一种双核或者多核CPU的设计架构。在几年前,所有的Android设备都还是单核的。 大多数的Android设备已经有了多个CPU,但是通常来说,其中一个CPU负责执行程序,其他的CPU则处理设备硬件的相关事务(例如,音频)。这些CPU可能有着不同的架构,运行在上面的程序无法在内存中彼此进行沟通交互。 目前大多数售卖的Android设备都是SMP架构的,这使得软件开发者处理问题更加复杂。对于多线程的程序,如果多个线程执行在不同的内核上,这会使得程序更加容易发生**race conditions**。 更糟糕的是,基于ARM架构的SMP比起x86架构来说,更加复杂,更难进行处理。那些在x86上测试通过的程序可能会在ARM上崩溃。 下面我们会介绍为何会这样以及如何做才能够使得你的代码行为正常。 ### 1)理论篇 这里会快速并且简要的介绍这个复杂的主题。其中一些部分并不完整,但是并没有出现错误或者误导。 查看文章末尾的[**进一步阅读**]()可以了解这个主题的更多知识。 ### 1.1)内存一致性模型(Memory consistency models) 内存一致性模型(Memory consistency models)通常也被叫做“memory models”,描述了硬件架构如何确保内存访问的一致性。例如,如果你对地址A进行了一个赋值,然后对地址B也进行了赋值,那么内存一致性模型就需要确保每一个CPU都需要知道刚才的操作赋值与操作顺序。 这个模型通常被程序员称为:**顺序一致性(sequential consistency)**, 请从文章末尾的**进一步阅读**查看**Adve & Gharachorloo**这篇文章。 - 所有的内存操作每次只能执行一个。 - 所有的操作,在单核CPU上,都是顺序执行的。 如果你关注一段代码在内存中的读写操作,在sequentially-consistent的CPU架构上,是按照期待的顺序执行的。It’s possible that the CPU is actually reordering instructions and delaying reads and writes, but there is no way for code running on the device to tell that the CPU is doing anything other than execute instructions in a straightforward manner. (We’re ignoring memory-mapped device driver I/O for the moment.) To illustrate these points it’s useful to consider small snippets of code, commonly referred to as litmus tests. These are assumed to execute in program order, that is, the order in which the instructions appear here is the order in which the CPU will execute them. We don’t want to consider instruction reordering performed by compilers just yet. Here’s a simple example, with code running on two threads: Thread 1 Thread 2A = 3B = 5 reg0 = Breg1 = A | Thread 1 | Thread 2 | |-----|-----| | A = 3 B = 5 | reg0 = B reg1 = A | In this and all future litmus examples, memory locations are represented by capital letters (A, B, C) and CPU registers start with “reg”. All memory is initially zero. Instructions are executed from top to bottom. Here, thread 1 stores the value 3 at location A, and then the value 5 at location B. Thread 2 loads the value from location B into reg0, and then loads the value from location A into reg1. (Note that we’re writing in one order and reading in another.) Thread 1 and thread 2 are assumed to execute on different CPU cores. You should always make this assumption when thinking about multi-threaded code. Sequential consistency guarantees that, after both threads have finished executing, the registers will be in one of the following states: | Registers | States | |-----|-----| | reg0=5, reg1=3 | possible (thread 1 ran first) | | reg0=0, reg1=0 | possible (thread 2 ran first) | | reg0=0, reg1=3 | possible (concurrent execution) | | reg0=5, reg1=0 | never | To get into a situation where we see B=5 before we see the store to A, either the reads or the writes would have to happen out of order. On a sequentially-consistent machine, that can’t happen. Most uni-processors, including x86 and ARM, are sequentially consistent. Most SMP systems, including x86 and ARM, are not. #### 1.1.1)Processor consistency #### 1.1.2)CPU cache behavior #### 1.1.3)Observability #### 1.1.4)ARM’s weak ordering ### 1.2)Data memory barriers #### 1.2.1)Store/store and load/load #### 1.2.2)Load/store and store/load #### 1.2.3)Barrier instructions #### 1.2.4)Address dependencies and causal consistency #### 1.2.5)Memory barrier summary ### 1.3)Atomic operations #### 1.3.1)Atomic essentials #### 1.3.2)Atomic + barrier pairing #### 1.3.3)Acquire and release ### 2)实践篇 调试内存一致性(memory consistency)的问题非常困难。如果内存栅栏(memory barrier)导致一些代码读取到陈旧的数据,你将无法通过调试器检查内存dumps文件来找出原因。By the time you can issue a debugger query, the CPU cores will have all observed the full set of accesses, and the contents of memory and the CPU registers will appear to be in an “impossible” state. ### 2.1)What not to do in C #### 2.1.1)C/C++ and “volatile” #### 2.1.2)Examples ### 2.2)在Java中不应该做的事 我们没有讨论过Java语言的一些相关特性,因此我们首先来简要的看下那些特性。 #### 2.2.1)Java中的"synchronized"与"volatile"关键字 **“synchronized”**关键字提供了Java一种内置的锁机制。每一个对象都有一个相对应的“monitor”,这个监听器可以提供互斥的访问。 “synchronized”代码段的实现机制与自旋锁(spin lock)有着相同的基础结构: 他们都是从获取到CAS开始,以释放CAS结束。这意味着编译器(compilers)与代码优化器(code optimizers)可以轻松的迁移代码到“synchronized”代码段中。一个实践结果是:你**不能**判定synchronized代码段是执行在这段代码下面一部分的前面,还是这段代码上面一部分的后面。更进一步,如果一个方法有两个synchronized代码段并且锁住的是同一个对象,那么在这两个操作的中间代码都无法被其他的线程所检测到,编译器可能会执行“锁粗化lock coarsening”并且把这两者绑定到同一个代码块上。 另外一个相关的关键字是**“volatile”**。在Java 1.4以及之前的文档中是这样定义的:volatile声明和对应的C语言中的一样可不靠。从Java 1.5开始,提供了更有力的保障,甚至和synchronization一样具备强同步的机制。 volatile的访问效果可以用下面这个例子来说明。如果线程1给volatile字段做了赋值操作,线程2紧接着读取那个字段的值,那么线程2是被确保能够查看到之前线程1的任何写操作。更通常的情况是,**任何**线程对那个字段的写操作对于线程2来说都是可见的。实际上,写volatile就像是释放件监听器,读volatile就像是获取监听器。 非volatile的访问有可能因为照顾volatile的访问而需要做顺序的调整。例如编译器可能会往上移动一个非volatile加载操作,但是不会往下移动。Volatile之间的访问不会因为彼此而做出顺序的调整。虚拟机会注意处理如何的内存栅栏(memory barriers)。 当加载与保存大多数的基础数据类型,他们都是原子的atomic, 对于long以及double类型的数据则不具备原子型,除非他们被声明为volatile。即使是在单核处理器上,并发多线程更新非volatile字段值也还是不确定的。 #### 2.2.2)Examples 下面是一个错误实现的单步计数器(monotonic counter)的示例: ([Java theory and practice: Managing volatility](#)). ~~~ class Counter { private int mValue; public int get() { return mValue; } public void incr() { mValue++; } } ~~~ 假设get()与incr()方法是被多线程调用的。然后我们想确保当get()方法被调用时,每一个线程都能够看到当前的数量。最引人注目的问题是mValue++实际上包含了下面三个操作。 1. reg = mValue 1. reg = reg + 1 1. mValue = reg 如果两个线程同时在执行`incr()`方法,其中的一个更新操作会丢失。为了确保正确的执行`++`的操作,我们需要把`incr()`方法声明为“synchronized”。这样修改之后,这段代码才能够在单核多线程的环境中正确的执行。 然而,在SMP的系统下还是会执行失败。不同的线程通过`get()`方法获取到得值可能是不一样的。因为我们是使用通常的加载方式来读取这个值的。我们可以通过声明`get()`方法为synchronized的方式来修正这个错误。通过这些修改,这样的代码才是正确的了。 不幸的是,我们有介绍过有可能发生的锁竞争(lock contention),这有可能会伤害到程序的性能。除了声明`get()`方法为synchronized之外,我们可以声明`mValue`为**“volatile”**. (请注意`incr()`必须使用synchronize) 现在我们知道volatile的mValue的写操作对于后续的读操作都是可见的。`incr()`将会稍稍有点变慢,但是`get()`方法将会变得更加快速。因此读操作多于写操作时,这会是一个比较好的方案。(请参考AtomicInteger.) 下面是另外一个示例,和之前的C示例有点类似: ~~~ class MyGoodies { public int x, y; } class MyClass { static MyGoodies sGoodies; void initGoodies() { // runs in thread 1 MyGoodies goods = new MyGoodies(); goods.x = 5; goods.y = 10; sGoodies = goods; } void useGoodies() { // runs in thread 2 if (sGoodies != null) { int i = sGoodies.x; // could be 5 or 0 .... } } } ~~~ 这段代码同样存在着问题,`sGoodies = goods`的赋值操作有可能在`goods`成员变量赋值之前被察觉到。如果你使用`volatile`声明`sGoodies`变量,你可以认为load操作为`atomic_acquire_load()`,并且把store操作认为是`atomic_release_store()`。 (请注意仅仅是`sGoodies`的引用本身为`volatile`,访问它的内部字段并不是这样的。赋值语句`z = sGoodies.x`会执行一个volatile load MyClass.sGoodies的操作,其后会伴随一个non-volatile的load操作::`sGoodies.x`。如果你设置了一个本地引用`MyGoodies localGoods = sGoodies, z = localGoods.x`,这将不会执行任何volatile loads.) 另外一个在Java程序中更加常用的范式就是臭名昭著的**“double-checked locking”**: ~~~ class MyClass { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(); } } } return helper; } } ~~~ 上面的写法是为了获得一个MyClass的单例。我们只需要创建一次这个实例,通过`getHelper()`这个方法。为了避免两个线程会同时创建这个实例。我们需要对创建的操作加synchronize机制。然而,我们不想要为了每次执行这段代码的时候都为“synchronized”付出额外的代价,因此我们仅仅在helper对象为空的时候加锁。 在单核系统上,这是不能正常工作的。JIT编译器会破坏这件事情。请查看[4)Appendix](#)的“‘Double Checked Locking is Broken’ Declaration”获取更多的信息, 或者是Josh Bloch’s Effective Java书中的Item 71 (“Use lazy initialization judiciously”)。 在SMP系统上执行这段代码,引入了一个额外的方式会导致失败。把上面那段代码换成C的语言实现如下: ~~~ if (helper == null) { // acquire monitor using spinlock while (atomic_acquire_cas(&this.lock, 0, 1) != success) ; if (helper == null) { newHelper = malloc(sizeof(Helper)); newHelper->x = 5; newHelper->y = 10; helper = newHelper; } atomic_release_store(&this.lock, 0); } ~~~ 此时问题就更加明显了: `helper`的store操作发生在memory barrier之前,这意味着其他的线程能够在store x/y之前观察到非空的值。 你应该尝试确保store helper执行在`atomic_release_store()`方法之后。通过重新排序代码进行加锁,但是这是无效的,因为往上移动的代码,编译器可以把它移动回原来的位置:在`atomic_release_store()`前面。(_这里没有读懂,下次再回读_) 有2个方法可以解决这个问题: - 删除外层的检查。这确保了我们不会在synchronized代码段之外做任何的检查。 - 声明helper为volatile。仅仅这样一个小小的修改,在前面示例中的代码就能够在Java 1.5及其以后的版本中正常工作。 下面的示例演示了使用volatile的2各重要问题: ~~~ class MyClass { int data1, data2; volatile int vol1, vol2; void setValues() { // runs in thread 1 data1 = 1; vol1 = 2; data2 = 3; } void useValues1() { // runs in thread 2 if (vol1 == 2) { int l1 = data1; // okay int l2 = data2; // wrong } } void useValues2() { // runs in thread 2 int dummy = vol2; int l1 = data1; // wrong int l2 = data2; // wrong } ~~~ 请注意`useValues1()`,如果thread 2还没有察觉到`vol1`的更新操作,那么它也无法知道`data1`或者`data2`被设置的操作。一旦它观察到了`vol1`的更新操作,那么它也能够知道data1的更新操作。然而,对于`data2`则无法做任何猜测,因为store操作是在volatile store之后发生的。 `useValues2()`使用了第2个volatile字段:vol2,这会强制VM生成一个memory barrier。这通常不会发生。为了建立一个恰当的“happens-before”关系,2个线程都需要使用同一个volatile字段。在thread 1中你需要知道vol2是在data1/data2之后被设置的。(The fact that this doesn’t work is probably obvious from looking at the code; the caution here is against trying to cleverly “cause” a memory barrier instead of creating an ordered series of accesses.) ### 2.3)What to do #### 2.3.1)General advice 在C/C++中,使用`pthread`操作,例如mutexes与semaphores。他们会使用合适的memory barriers,在所有的Android平台上提供正确有效的行为。请确保正确这些技术,例如在没有获得对应的mutex的情况下赋值操作需要很谨慎。 避免直接使用atomic方法。如果locking与unlocking之间没有竞争,locking与unlocking一个pthread mutex 分别需要一个单独的atomic操作。如果你需要一个lock-free的设计,你必须在开始写代码之前了解整篇文档的要点。(或者是寻找一个已经为SMP ARM设计好的库文件)。 Be extremely circumspect with "volatile” in C/C++. It often indicates a concurrency problem waiting to happen. In Java, the best answer is usually to use an appropriate utility class from the java.util.concurrent package. The code is well written and well tested on SMP. Perhaps the safest thing you can do is make your class immutable. Objects from classes like String and Integer hold data that cannot be changed once the class is created, avoiding all synchronization issues. The book Effective Java, 2nd Ed. has specific instructions in “Item 15: Minimize Mutability”. Note in particular the importance of declaring fields “final" (Bloch). If neither of these options is viable, the Java “synchronized” statement should be used to guard any field that can be accessed by more than one thread. If mutexes won’t work for your situation, you should declare shared fields “volatile”, but you must take great care to understand the interactions between threads. The volatile declaration won’t save you from common concurrent programming mistakes, but it will help you avoid the mysterious failures associated with optimizing compilers and SMP mishaps. The Java Memory Model guarantees that assignments to final fields are visible to all threads once the constructor has finished — this is what ensures proper synchronization of fields in immutable classes. This guarantee does not hold if a partially-constructed object is allowed to become visible to other threads. It is necessary to follow safe construction practices.(Safe Construction Techniques in Java). #### 2.3.2)Synchronization primitive guarantees The pthread library and VM make a couple of useful guarantees: all accesses previously performed by a thread that creates a new thread are observable by that new thread as soon as it starts, and all accesses performed by a thread that is exiting are observable when a join() on that thread returns. This means you don’t need any additional synchronization when preparing data for a new thread or examining the results of a joined thread. Whether or not these guarantees apply to interactions with pooled threads depends on the thread pool implementation. In C/C++, the pthread library guarantees that any accesses made by a thread before it unlocks a mutex will be observable by another thread after it locks that same mutex. It also guarantees that any accesses made before calling signal() or broadcast() on a condition variable will be observable by the woken thread. Java language threads and monitors make similar guarantees for the comparable operations. #### 2.3.3)Upcoming changes to C/C++ The C and C++ language standards are evolving to include a sophisticated collection of atomic operations. A full matrix of calls for common data types is defined, with selectable memory barrier semantics (choose from relaxed, consume, acquire, release, acq_rel, seq_cst). See the Further Reading section for pointers to the specifications. ### 3)Closing Notes While this document does more than merely scratch the surface, it doesn’t manage more than a shallow gouge. This is a very broad and deep topic. Some areas for further exploration: - Learn the definitions of **happens-before**, **synchronizes-with**, and other essential concepts from the Java Memory Model. (It’s hard to understand what “volatile” really means without getting into this.) - Explore what compilers are and aren’t allowed to do when reordering code. (The JSR-133 spec has some great examples of legal transformations that lead to unexpected results.) - Find out how to write immutable classes in Java and C++. (There’s more to it than just “don’t change anything after construction”.) - Internalize the recommendations in the Concurrency section of **Effective Java, 2nd Edition**. (For example, you should avoid calling methods that are meant to be overridden while inside a synchronized block.) - Understand what sorts of barriers you can use on x86 and ARM. (And other CPUs for that matter, for example Itanium’s acquire/release instruction modifiers.) - Read through the **java.util.concurrent** and **java.util.concurrent.atomic** APIs to see what's available. - Consider using concurrency annotations like `@ThreadSafe` and `@GuardedBy` (from net.jcip.annotations). The Further Reading section in the appendix has links to documents and web sites that will better illuminate these topics. ### 4)Appendix ### 4.1)SMP failure example ### 4.2)Implementing synchronization stores ### 4.3)Further reading
';

JNI使用指南

最后更新于:2022-04-01 01:47:17

> 编写:[pedant](https://github.com/pedant) - 原文:[http://developer.android.com/training/articles/perf-jni.html](http://developer.android.com/training/articles/perf-jni.html) JNI全称Java Native Interface。它为托管代码(使用Java编程语言编写)与本地代码(使用C/C++编写)提供了一种交互方式。它是与厂商无关的(vendor-neutral),支持从动态共享库中加载代码,虽然这样会稍显麻烦,但有时这是相当有效的。 如果你对JNI还不是太熟悉,可以先通读[Java Native Interface Specification](http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)这篇文章来对JNI如何工作以及哪些特性可用有个大致的印象。这种接口的一些方面不能立即一读就显而易见,所以你会发现接下来的几个章节很有用处。 # JavaVM 及 JNIEnv JNI定义了两种关键数据结构,“JavaVM”和“JNIEnv”。它们本质上都是指向函数表指针的指针(在C++版本中,它们被定义为类,该类包含一个指向函数表的指针,以及一系列可以通过这个函数表间接地访问对应的JNI函数的成员函数)。JavaVM提供“调用接口(invocation interface)”函数, 允许你创建和销毁一个JavaVM。理论上你可以在一个进程中拥有多个JavaVM对象,但安卓只允许一个。 JNIEnv提供了大部分JNI功能。你定义的所有本地函数都会接收JNIEnv作为第一个参数。 JNIEnv是用作线程局部存储。因此,**你不能在线程间共享一个JNIEnv变量**。如果在一段代码中没有其它办法获得它的JNIEnv,你可以共享JavaVM对象,使用GetEnv来取得该线程下的JNIEnv(如果该线程有一个JavaVM的话;见下面的AttachCurrentThread)。 JNIEnv和JavaVM的在C声明是不同于在C++的声明。头文件“jni.h”根据它是以C还是以C++模式包含来提供不同的类型定义(typedefs)。因此,不建议把JNIEnv参数放到可能被两种语言引入的头文件中(换一句话说:如果你的头文件需要#ifdef __cplusplus,你可能不得不在任何涉及到JNIEnv的内容处都要做些额外的工作)。 # 线程 所有的线程都是Linux线程,由内核统一调度。它们通常从托管代码中启动(使用Thread.start),但它们也能够在其他任何地方创建,然后连接(attach)到JavaVM。例如,一个用pthread_create启动的线程能够使用JNI AttachCurrentThread 或 AttachCurrentThreadAsDaemon函数连接到JavaVM。在一个线程成功连接(attach)之前,它没有JNIEnv,**不能够调用JNI函数**。 连接一个本地环境创建的线程会触发构造一个java.lang.Thread对象,然后其被添加到主线程群组(main ThreadGroup),以让调试器可以探测到。对一个已经连接的线程使用AttachCurrentThread不做任何操作(no-op)。 安卓不能中止正在执行本地代码的线程。如果正在进行垃圾回收,或者调试器已发出了中止请求,安卓会在下一次调用JNI函数的时候中止线程。 连接过的(attached)线程在它们退出之前**必须通过JNI调用DetachCurrentThread**。如果你觉得直接这样编写不太优雅,在安卓2.0(Eclair)及以上, 你可以使用pthread_key_create来定义一个析构函数,它将会在线程退出时被调用,你可以在那儿调用DetachCurrentThread (使用生成的key与pthread_setspecific将JNIEnv存储到线程局部空间内;这样JNIEnv能够作为参数传入到析构函数当中去)。 # jclass, jmethodID, jfieldID 如果你想在本地代码中访问一个对象的字段(field),你可以像下面这样做: - 对于类,使用FindClass获得类对象的引用 - 对于字段,使用GetFieldId获得字段ID - 使用对应的方法(例如GetIntField)获取字段下面的值 类似地,要调用一个方法,你首先得获得一个类对象的引用,然后是方法ID(method ID)。这些ID通常是指向运行时内部数据结构。查找到它们需要些字符串比较,但一旦你实际去执行它们获得字段或者做方法调用是非常快的。 如果性能是你看重的,那么一旦查找出这些值之后在你的本地代码中缓存这些结果是非常有用的。因为每个进程当中的JavaVM是存在限制的,存储这些数据到本地静态数据结构中是非常合理的。 类引用(class reference),字段ID(field ID)以及方法ID(method ID)在类被卸载前都是有效的。如果与一个类加载器(ClassLoader)相关的所有类都能够被垃圾回收,但是这种情况在安卓上是罕见甚至不可能出现,只有这时类才被卸载。注意虽然jclass是一个类引用,但是**必须要调用NewGlobalRef保护起来**(见下个章节)。 当一个类被加载时如果你想缓存些ID,而后当这个类被卸载后再次载入时能够自动地更新这些缓存ID,正确做法是在对应的类中添加一段像下面的代码来初始化这些ID: ~~~ /* * 我们在一个类初始化时调用本地方法来缓存一些字段的偏移信息 * 这个本地方法查找并缓存你感兴趣的class/field/method ID * 失败时抛出异常 */ private static native void nativeInit(); static { nativeInit(); } ~~~ 在你的C/C++代码中创建一个nativeClassInit方法以完成ID查找的工作。当这个类被初始化时这段代码将会执行一次。当这个类被卸载后而后再次载入时,这段代码将会再次执行。 # 局部和全局引用 每个传入本地方法的参数,以及大部分JNI函数返回的每个对象都是“局部引用”。这意味着它只在当前线程的当前方法执行期间有效。**即使这个对象本身在本地方法返回之后仍然存在,这个引用也是无效的**。 这同样适用于所有jobject的子类,包括jclass,jstring,以及jarray(当JNI扩展检查是打开的时候,运行时会警告你对大部分对象引用的误用)。 如果你想持有一个引用更长的时间,你就必须使用一个全局(“global”)引用了。NewGlobalRef函数以一个局部引用作为参数并且返回一个全局引用。全局引用能够保证在你调用DeleteGlobalRef前都是有效的。 这种模式通常被用在缓存一个从FindClass返回的jclass对象的时候,例如: ~~~ jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass)); ~~~ 所有的JNI方法都接收局部引用和全局引用作为参数。相同对象的引用却可能具有不同的值。例如,用相同对象连续地调用NewGlobalRef得到返回值可能是不同的。**为了检查两个引用是否指向的是同一个对象,你必须使用IsSameObject函数**。绝不要在本地代码中用==符号来比较两个引用。 得出的结论就是你**绝不要在本地代码中假定对象的引用是常量或者是唯一的**。代表一个对象的32位值从方法的一次调用到下一次调用可能有不同的值。在连续的调用过程中两个不同的对象却可能拥有相同的32位值。不要使用jobject的值作为key. 开发者需要“不过度分配”局部引用。在实际操作中这意味着如果你正在创建大量的局部引用,或许是通过对象数组,你应该使用DeleteLocalRef手动地释放它们,而不是寄希望JNI来为你做这些。实现上只预留了16个局部引用的空间,所以如果你需要更多,要么你删掉以前的,要么使用EnsureLocalCapacity/PushLocalFrame来预留更多。 注意jfieldID和jmethodID是映射类型(opaque types),不是对象引用,不应该被传入到NewGlobalRef。原始数据指针,像GetStringUTFChars和GetByteArrayElements的返回值,也都不是对象(它们能够在线程间传递,并且在调用对应的Release函数之前都是有效的)。 还有一种不常见的情况值得一提,如果你使用AttachCurrentThread连接(attach)了本地进程,正在运行的代码在线程分离(detach)之前决不会自动释放局部引用。你创建的任何局部引用必须手动删除。通常,任何在循环中创建局部引用的本地代码可能都需要做一些手动删除。 # UTF-8、UTF-16 字符串 Java编程语言使用UTF-16格式。为了便利,JNI也提供了支持[变形UTF-8(Modified UTF-8)](http://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8)的方法。这种变形编码对于C代码是非常有用的,因为它将\u0000编码成0xc0 0x80,而不是0x00。最惬意的事情是你能在具有C风格的以\0结束的字符串上计数,同时兼容标准的libc字符串函数。不好的一面是你不能传入随意的UTF-8数据到JNI函数而还指望它正常工作。 如果可能的话,直接操作UTF-16字符串通常更快些。安卓当前在调用GetStringChars时不需要拷贝,而GetStringUTFChars需要一次分配并且转换为UTF-8格式。注意**UTF-16字符串不是以零终止字符串**,\u0000是被允许的,所以你需要像对jchar指针一样地处理字符串的长度。 **不要忘记Release你Get的字符串**。这些字符串函数返回jchar_或者jbyte_,都是指向基本数据类型的C格式的指针而不是局部引用。它们在Release调用之前都保证有效,这意味着当本地方法返回时它们并不主动释放。 **传入NewStringUTF函数的数据必须是变形UTF-8格式**。一种常见的错误情况是,从文件或者网络流中读取出的字符数据,没有过滤直接使用NewStringUTF处理。除非你确定数据是7位的ASCII格式,否则你需要剔除超出7位ASCII编码范围(high-ASCII)的字符或者将它们转换为对应的变形UTF-8格式。如果你没那样做,UTF-16的转换结果可能不会是你想要的结果。JNI扩展检查将会扫描字符串,然后警告你那些无效的数据,但是它们将不会发现所有潜在的风险。 # 原生类型数组 JNI提供了一系列函数来访问数组对象中的内容。对象数组的访问只能一次一条,但如果原生类型数组以C方式声明,则能够直接进行读写。 为了让接口更有效率而不受VM实现的制约,GetArrayElements系列调用允许运行时返回一个指向实际元素的指针,或者是分配些内存然后拷贝一份。不论哪种方式,返回的原始指针在相应的Release调用之前都保证有效(这意味着,如果数据没被拷贝,实际的数组对象将会受到牵制,不能重新成为整理堆空间的一部分)。**你必须释放(Release)每个你通过Get得到的数组**。同时,如果Get调用失败,你必须确保你的代码在之后不会去尝试调用Release来释放一个空指针(NULL pointer)。 你可以用一个非空指针作为isCopy参数的值来决定数据是否会被拷贝。这相当有用。 Release类的函数接收一个mode参数,这个参数的值可选的有下面三种。而运行时具体执行的操作取决于它返回的指针是指向真实数据还是拷贝出来的那份。 - 0 - 真实的:实际数组对象不受到牵制 - 拷贝的:数据将会复制回去,备份空间将会被释放。 - JNI_COMMIT - 真实的:不做任何操作 - 拷贝的:数据将会复制回去,备份空间将**不会被释放**。 - JNI_ABORT - 真实的:实际数组对象不受到牵制.之前的写入**不会**被取消。 - 拷贝的:备份空间将会被释放;里面所有的变更都会丢失。 检查isCopy标识的一个原因是对一个数组做出变更后确认你是否需要传入JNI_COMMIT来调用Release函数。如果你交替地执行变更和读取数组内容的代码,你也许可以跳过无操作(no-op)的JNI_COMMIT。检查这个标识的另一个可能的原因是使用JNI_ABORT可以更高效。例如,你也许想得到一个数组,适当地修改它,传入部分到其他函数中,然后丢掉这些修改。如果你知道JNI是为你做了一份新的拷贝,就没有必要再创建另一份“可编辑的(editable)”的拷贝了。如果JNI传给你的是原始数组,这时你就需要创建一份你自己的拷贝了。 另一个常见的错误(在示例代码中出现过)是认为当isCopy是false时你就可以不调用Release。实际上是没有这种情况的。如果没有分配备份空间,那么初始的内存空间会受到牵制,位置不能被垃圾回收器移动。 另外注意JNI_COMMIT标识**没有**释放数组,你最终需要使用一个不同的标识再次调用Release。 # 区间数组 当你想做的只是拷出或者拷进数据时,可以选择调用像GetArrayElements和GetStringChars这类非常有用的函数。想想下面: ~~~ jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); } ~~~ 这里获取到了数组,从当中拷贝出开头的len个字节元素,然后释放这个数组。根据代码的实现,Get函数将会牵制或者拷贝数组的内容。上面的代码拷贝了数据(为了可能的第二次),然后调用Release;这当中JNI_ABORT确保不存在第三份拷贝了。 另一种更简单的实现方式: ~~~ env->GetByteArrayRegion(array, 0, len, buffer); ~~~ 这种方式有几个优点: - 只需要调用一个JNI函数而是不是两个,减少了开销。 - 不需要指针或者额外的拷贝数据。 - 减少了开发人员犯错的风险-在某些失败之后忘记调用Release不存在风险。 类似地,你能使用SetArrayRegion函数拷贝数据到数组,使用GetStringRegion或者GetStringUTFRegion从String中拷贝字符。 # 异常 **当异常发生时你一定不能调用大部分的JNI函数**。你的代码收到异常(通过函数的返回值,ExceptionCheck,或者ExceptionOccurred),然后返回,或者清除异常,处理掉。 当异常发生时你被允许调用的JNI函数有: - DeleteGlobalRef - DeleteLocalRef - DeleteWeakGlobalRef - ExceptionCheck - ExceptionClear - ExceptionDescribe - ExceptionOccurred - MonitorExit - PopLocalFrame - PushLocalFrame - ReleaseArrayElements - ReleasePrimitiveArrayCritical - ReleaseStringChars - ReleaseStringCritical - ReleaseStringUTFChars 许多JNI调用能够抛出异常,但通常提供一种简单的方式来检查失败。例如,如果NewString返回一个非空值,你不需要检查异常。然而,如果你调用一个方法(使用一个像CalllObjectMethod的函数),你必须一直检查异常,因为当一个异常抛出时它的返回值将不会是有效的。 注意中断代码抛出的异常不会展开本地调用堆栈信息,Android也还不支持C++异常。JNI Throw和ThrowNew指令仅仅是在当前线程中放入一个异常指针。从本地代码返回到托管代码时,异常将会被注意到,得到适当的处理。 本地代码能够通过调用ExceptionCheck或者ExceptionOccurred捕获到异常,然后使用ExceptionClear清除掉。通常,抛弃异常而不处理会导致些问题。 没有内建的函数来处理Throwable对象自身,因此如果你想得到异常字符串,你需要找出Throwable Class,然后查找到getMessage "()Ljava/lang/String;"的方法ID,调用它,如果结果非空,使用GetStringUTFChars,得到的结果你可以传到printf(3) 或者其它相同功能的函数输出。 # 扩展检查 JNI的错误检查很少。错误发生时通常会导致崩溃。Android也提供了一种模式,叫做CheckJNI,这当中JavaVM和JNIEnv函数表指针被换成了函数表,它在调用标准实现之前执行了一系列扩展检查的。 额外的检查包括: - 数组:试图分配一个长度为负的数组。 - 坏指针:传入一个不完整jarray/jclass/jobject/jstring对象到JNI函数,或者调用JNI函数时使用空指针传入到一个不能为空的参数中去。 - 类名:传入了除“java/lang/String”之外的类名到JNI函数。 - 关键调用:在一个“关键的(critical)”get和它对应的release之间做出JNI调用。 - 直接的ByteBuffers:传入不正确的参数到NewDirectByteBuffer。 - 异常:当一个异常发生时调用了JNI函数。 - JNIEnv_s:在错误的线程中使用一个JNIEnv_。 - jfieldIDs:使用一个空jfieldID,或者使用jfieldID设置了一个错误类型的值到字段(比如说,试图将一个StringBuilder赋给String类型的域),或者使用一个静态字段下的jfieldID设置到一个实例的字段(instance field)反之亦然,或者使用的一个类的jfieldID却来自另一个类的实例。 - jmethodIDs:当调用Call*Method函数时时使用了类型错误的jmethodID:不正确的返回值,静态/非静态的不匹配,this的类型错误(对于非静态调用)或者错误的类(对于静态类调用)。 - 引用:在类型错误的引用上使用了DeleteGlobalRef/DeleteLocalRef。 - 释放模式:调用release使用一个不正确的释放模式(其它非 0,JNI_ABORT,JNI_COMMIT的值)。 - 类型安全:从你的本地代码中返回了一个不兼容的类型(比如说,从一个声明返回String的方法却返回了StringBuilder)。 - UTF-8:传入一个无效的变形UTF-8字节序列到JNI调用。 (方法和域的可访问性仍然没有检查:访问限制对于本地代码并不适用。) 有几种方法去启用CheckJNI。 如果你正在使用模拟器,CheckJNI默认是打开的。 如果你有一台root过的设备,你可以使用下面的命令序列来重启运行时(runtime),启用CheckJNI。 ~~~ adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start ~~~ 随便哪一种,当运行时(runtime)启动时你将会在你的日志输出中见到如下的字符: ~~~ D AndroidRuntime: CheckJNI is ON ~~~ 如果你有一台常规的设备,你可以使用下面的命令: ~~~ adb shell setprop debug.checkjni 1 ~~~ 这将不会影响已经在运行的app,但是从那以后启动的任何app都将打开CheckJNI(改变属性为其它值或者只是重启都将会再次关闭CheckJNI)。这种情况下,你将会在下一次app启动时,在日志输出中看到如下字符: ~~~ D Late-enabling CheckJNI ~~~ # 本地库 你可以使用标准的System.loadLibrary方法来从共享库中加载本地代码。在你的本地代码中较好的做法是: - 在一个静态类初始化时调用System.loadLibrary(见之前的一个例子中,当中就使用了nativeClassInit)。参数是“未加修饰(undecorated)”的库名称,因此要加载“libfubar.so”,你需要传入“fubar”。 - 提供一个本地函数:**jint JNI_OnLoad(JavaVM_ vm, void_ reserved)** - 在JNI_OnLoad中,注册所有你的本地方法。你应该声明方法为“静态的(static)”因此名称不会占据设备上符号表的空间。 JNI_OnLoad函数在C++中的写法如下: ~~~ jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return -1; } // 使用env->FindClass得到jclass // 使用env->RegisterNatives注册本地方法 return JNI_VERSION_1_6; } ~~~ 你也可以使用共享库的全路径来调用System.load。对于Android app,你也许会发现从context对象中得到应用私有数据存储的全路径是非常有用的。 上面是推荐的方式,但不是仅有的实现方式。显式注册不是必须的,提供一个JNI_OnLoad函数也不是必须的。你可以使用基于特殊命名的“发现(discovery)”模式来注册本地方法(更多细节见:[JNI spec](http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/design.html#wp615)),虽然这并不可取。因为如果一个方法的签名错误,在这个方法实际第一次被调用之前你是不会知道的。 关于JNI_OnLoad另一点注意的是:任何你在JNI_OnLoad中对FindClass的调用都发生在用作加载共享库的类加载器的上下文(context)中。一般FindClass使用与“调用栈”顶部方法相关的加载器,如果当中没有加载器(因为线程刚刚连接)则使用“系统(system)”类加载器。这就使得JNI_OnLoad成为一个查寻及缓存类引用很便利的地方。 # 64位机问题 Android当前设计为运行在32位的平台上。理论上它也能够构建为64位的系统,但那不是现在的目标。当与本地代码交互时,在大多数情况下这不是你需要担心的,但是如果你打算存储指针变量到对象的整型字段(integer field)这样的本地结构中,这就变得非常重要了。为了支持使用64位指针的架构,**你需要使用long类型而不是int类型的字段来存储你的本地指针**。 # 不支持的特性/向后兼容性 除了下面的例外,支持所有的JNI 1.6特性: - DefineClass没有实现。Android不使用Java字节码或者class文件,因此传入二进制class数据将不会有效。 对Android以前老版本的向后兼容性,你需要注意: - 本地函数的动态查找在Android 2.0(Eclair)之前,在搜索方法名称时,字符“$”不会转换为对应的“_00024”。要使它正常工作需要使用显式注册方式或者将本地方法的声明移出内部类。 - 分离线程在Android 2.0(Eclair)之前,使用pthread_key_create析构函数来避免“退出前线程必须分离”检查是不可行的(运行时(runtime)也使用了一个pthread key析构函数,因此这是一场看谁先被调用的竞赛)。 - 全局弱引用在Android 2.0(Eclair)之前,全局弱引用没有被实现。如果试图使用它们,老版本将完全不兼容。你可以使用Android平台版本号常量来测试系统的支持性。在Android 4.0 (Ice Cream Sandwich)之前,全局弱引用只能传给NewLocalRef, NewGlobalRef, 以及DeleteWeakGlobalRef(强烈建议开发者在使用全局弱引用之前都为它们创建强引用hard reference,所以这不应该在所有限制当中)。从Android 4.0 (Ice Cream Sandwich)起,全局弱引用能够像其它任何JNI引用一样使用了。 - 局部引用在Android 4.0 (Ice Cream Sandwich)之前,局部引用实际上是直接指针。Ice Cream Sandwich为了更好地支持垃圾回收添加了间接指针,但这并不意味着很多JNI bug在老版本上不存在。更多细节见[JNI Local Reference Changes in ICS](http://android-developers.blogspot.com/2011/11/jni-local-reference-changes-in-ics.html)。 - 使用GetObjectRefType获得引用类型在Android 4.0 (Ice Cream Sandwich)之前,使用直接指针(见上面)的后果就是正确地实现GetObjectRefType是不可能的。我们可以使用依次检测全局弱引用表,参数,局部表,全局表的方式来代替。第一次匹配到你的直接指针时,就表明你的引用类型是当前正在检测的类型。这意味着,例如,如果你在一个全局jclass上使用GetObjectRefType,而这个全局jclass碰巧与作为静态本地方法的隐式参数传入的jclass一样的,你得到的结果是JNILocalRefType而不是JNIGlobalRefType。 # FAQ: 为什么出现了UnsatisfiedLinkError? 当使用本地代码开发时经常会见到像下面的错误: ~~~ java.lang.UnsatisfiedLinkError: Library foo not found ~~~ 有时候这表示和它提示的一样---未找到库。但有些时候库确实存在但不能被dlopen(3)找开,更多的失败信息可以参见异常详细说明。 你遇到“library not found”异常的常见原因可能有这些: - 库文件不存在或者不能被app访问到。使用adb shell ls -l 检查它的存在性和权限。 - 库文件不是用NDK构建的。这就导致设备上并不存在它所依赖的函数或者库。 另一种UnsatisfiedLinkError错误像下面这样: ~~~ java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10) ~~~ 在日志中,你会发现: ~~~ W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V ~~~ 这意味着运行时尝试匹配一个方法但是没有成功,这种情况常见的原因有: - 库文件没有得到加载。检查日志输出中关于库文件加载的信息。 - 由于名称或者签名错误,方法不能匹配成功。这通常是由于: - 对于方法的懒查寻,使用 extern "C"和对应的可见性(JNIEXPORT)来声明C++函数没有成功。注意Ice Cream Sandwich之前的版本,JNIEXPORT宏是不正确的,因此对新版本的GCC使用旧的jni.h头文件将不会有效。你可以使用arm-eabi-nm查看它们出现在库文件里的符号。如果它们看上去比较凌乱(像_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass这样而不是Java_Foo_myfunc),或者符号类型是小写的“t”而不是一个大写的“T”,这时你就需要调整声明了。 - 对于显式注册,在进行方法签名时可能犯了些小错误。确保你传入到注册函数的签名能够完全匹配上日志文件里提示的。记住“B”是byte,“Z”是boolean。在签名中类名组件是以“L”开头的,以“;”结束的,使用“/”来分隔包名/类名,使用“$”符来分隔内部类名称(比如说,Ljava/util/Map$Entry;)。 使用javah来自动生成JNI头文件也许能帮助你避免这些问题。 # FAQ: 为什么FindClass不能找到我的类? 确保类名字符串有正确的格式。JNI类名称以包名开始,然后使用左斜杠来分隔,比如java/lang/String。如果你正在查找一个数组类,你需要以对应数目的综括号开头,使用“L”和“;”将类名两头包起来,所以一个一维字符串数组应该写成[Ljava/lang/String;。 如果类名称看上去正确,你可能运行时遇到了类加载器的问题。FindClass想在与你代码相关的类加载器中开始查找指定的类。检查调用堆栈,可能看起像: ~~~ Foo.myfunc(Native Method) Foo.main(Foo.java:10) dalvik.system.NativeStart.main(Native Method) ~~~ 最顶层的方法是Foo.myfunc。FindClass找到与类Foo相关的ClassLoader对象然后使用它。 这通常正是你所想的。如果你创建了自己的线程那么就会遇到麻烦(也许是调用了pthread_create然后使用AttachCurrentThread进行了连接)。现在跟踪堆栈可能像下面这样: ~~~ dalvik.system.NativeStart.run(Native Method) ~~~ 最顶层的方法是NativeStart.run,它不是你应用内的方法。如果你从这个线程中调用FindClass,JavaVM将会启动“系统(system)”的而不是与你应用相关的加载器,因此试图查找应用内定义的类都将会失败。 下面有几种方法可以解决这个问题: - 在JNI_OnLoad中使用FindClass查寻一次,然后为后面的使用缓存这些类引用。任何在JNI_OnLoad当中执行的FindClass调用都使用与执行System.loadLibrary的函数相关的类加载器(这个特例,让库的初始化更加的方便了)。如果你的app代码正在加载库文件,FindClass将会使用正确的类加载器。 - 传入类实例到一个需要它的函数,你的本地方法声明必须带有一个Class参数,然后传入Foo.class。 - 在合适的地方缓存一个ClassLoader对象的引用,然后直接发起loadClass调用。这需要额外些工作。 # FAQ: 使用本地代码怎样共享原始数据? 也许你会遇到这样一种情况,想从你的托管代码或者本地代码访问一大块原始数据的缓冲区。常见例子包括对bitmap或者声音文件的处理。这里有两种基本实现方式。 你可以将数据存储到byte[]。这允许你从托管代码中快速地访问。然而,在本地代码端不能保证你不去拷贝一份就直接能够访问数据。在某些实现中,GetByteArrayElements和GetPrimitiveArrayCritical将会返回指向在维护堆中的原始数据的真实指针,但是在另外一些实现中将在本地堆空间分配一块缓冲区然后拷贝数据过去。 还有一种选择是将数据存储在一块直接字节缓冲区(direct byte buffer),可以使用java.nio.ByteBuffer.allocateDirect或者NewDirectByteBuffer JNI函数创建buffer。不像常规的byte缓冲区,它的存储空间将不会分配在程序维护的堆空间上,总是可以从本地代码直接访问(使用GetDirectBufferAddress得到地址)。依赖于直接字节缓冲区访问的实现方式,从托管代码访问原始数据将会非常慢。 选择使用哪种方式取决于两个方面: 1.大部分的数据访问是在Java代码还是C/C++代码中发生? 2.如果数据最终被传到系统API,那它必须是怎样的形式(例如,如果数据最终被传到一个使用byte[]作为参数的函数,在直接的ByteBuffer中处理或许是不明智的)? 如果通过上面两种情况仍然不能明确区分的,就使用直接字节缓冲区(direct byte buffer)形式。它们的支持是直接构建到JNI中的,在未来的版本中性能可能会得到提升。
';

避免出现程序无响应ANR

最后更新于:2022-04-01 01:47:14

> 编写:[kesenhoo](https://github.com/kesenhoo) - 原文:[http://developer.android.com/training/articles/perf-anr.html](http://developer.android.com/training/articles/perf-anr.html) 可能你写的代码在性能测试上表现良好,但是你的应用仍然有时候会反应迟缓(sluggish),停顿(hang)或者长时间卡死(frezze),或者是应用处理输入的数据花费时间过长。对于你的应用来说最槽糕的事情是出现"程序无响应(Application Not Responding)" (ANR)的警示框。 在Android中,系统通过显示ANR警示框来保护程序的长时间无响应。对话框如下: ![anr](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-07-28_55b7247b30839.png) 此时,你的应用已经经历过一段时间的无法响应了,因此系统提供用户可以退出应用的选择。为你的程序提供良好的响应性是至关重要的,这样才能够避免系统为用户显示ANR的警示框。 这节课描述了Android系统是如何判断一个应用不可响应的。这节课还会提供程序编写的指导原则,确保你的程序保持响应性。 ### 是什么导致了ANR?(What Triggers ANR?) 通常来说,系统会在程序无法响应用户的输入事件时显示ANR。例如,如果一个程序在UI线程执行I/O操作(通常是网络请求或者是文件读写),这样系统就无法处理用户的输入事件。或者是应用在UI线程花费了太多的时间用来建立一个复杂的在内存中的数据结构,又或者是在一个游戏程序的UI线程中执行了一个复杂耗时的计算移动的操作。确保那些计算操作高效是很重要的,不过即使是最高效的代码也是需要花时间执行的。 **对于你的应用中任何可能长时间执行的操作,你都不应该执行在UI线程**。你可以创建一个工作线程,把那些操作都执行在工作线程中。这确保了UI线程(这个线程会负责处理UI事件) 能够顺利执行,也预防了系统因代码僵死而崩溃。因为UI线程是和类级别相关联的,你可以把相应性作为一个类级别(class-level)的问题(相比来说,代码性能则属于方法级别(method-level)的问题) 在Android中,程序的响应性是由[Activity](# "An activity represents a single screen with a user interface.") Manager与Window Manager系统服务来负责监控的。当系统监测到下面的条件之一时会显示ANR的对话框: - 对输入事件(例如硬件点击或者屏幕触摸事件),5秒内都无响应。 - BroadReceiver不能够在10秒内结束接收到任务。 ### 如何避免ANRs(How to Avoid ANRs) Android程序通常是执行在默认的UI线程(也就是main线程)中的。这意味着在UI线程中执行的任何长时间的操作都可能触发ANR,因为程序没有给自己处理输入事件或者broadcast事件的机会。 因此,任何执行在UI线程的方法都应该尽可能的简短快速。特别是,在[activity](# "An activity represents a single screen with a user interface.")的生命周期的关键方法`onCreate()`与`onResume()`方法中应该尽可能的做比较少的事情。类似网络或者DB操作等可能长时间执行的操作,或者是类似调整bitmap大小等需要长时间计算的操作,都应该执行在工作线程中。(在DB操作中,可以通过异步的网络请求)。 为了执行一个长时间的耗时操作而创建一个工作线程最方便高效的方式是使用`AsyncTask`。只需要继承AsyncTask并实现`doInBackground()`方法来执行任务即可。为了把任务执行的进度呈现给用户,你可以执行`publishProgress()`方法,这个方法会触发`onProgressUpdate()`的回调方法。在`onProgressUpdate()`的回调方法中(它执行在UI线程),你可以执行通知用户进度的操作,例如: ~~~ private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { // Do the long-running work in here protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i < count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float) count) * 100)); // Escape early if cancel() is called if (isCancelled()) break; } return totalSize; } // This is called each time you call publishProgress() protected void onProgressUpdate(Integer... progress) { setProgressPercent(progress[0]); } // This is called when doInBackground() is finished protected void onPostExecute(Long result) { showNotification("Downloaded " + result + " bytes"); } } ~~~ 为了能够执行这个工作线程,只需要创建一个实例并执行`execute()`: ~~~ new DownloadFilesTask().execute(url1, url2, url3); ~~~ 相比起AsycnTask来说,创建自己的线程或者HandlerThread稍微复杂一点。如果你想这样做,**你应该通过`Process.setThreadPriority()`并传递`THREAD_PRIORITY_BACKGROUND`来设置线程的优先级为"background"。**如果你不通过这个方式来给线程设置一个低的优先级,那么这个线程仍然会使得你的应用显得卡顿,因为这个线程默认与UI线程有着同样的优先级。 如果你实现了Thread或者HandlerThread,请确保你的UI线程不会因为等待工作线程的某个任务而去执行Thread.wait()或者Thread.sleep()。UI线程不应该去等待工作线程完成某个任务,你的UI线程应该提供一个Handler给其他工作线程,这样工作线程能够通过这个Handler在任务结束的时候通知UI线程。使用这样的方式来设计你的应用程序可以使得你的程序UI线程保持响应性,以此来避免ANR。 BroadcastReceiver有特定执行时间的限制说明了broadcast receivers应该做的是:简短快速的任务,避免执行费时的操作,例如保存数据或者注册一个Notification。正如在UI线程中执行的方法一样,程序应该避免在broadcast receiver中执行费时的长任务。但不是采用通过工作线程来执行复杂的任务的方式,你的程序应该启动一个IntentService来响应intent broadcast的长时间任务。 > **Tip:** 你可以使用StrictMode来帮助寻找因为不小心加入到UI线程的潜在的长时间执行的操作,例如网络或者DB相关的任务。 ### 增加响应性(Reinforce Responsiveness) 通常来说,100ms - 200ms是用户能够察觉到卡顿的上限。这样的话,下面有一些避免ANR的技巧: - 如果你的程序需要响应正在后台加载的任务,在你的UI中可以显示ProgressBar来显示进度。 - 对游戏程序,在工作线程执行计算的任务。 - 如果你的程序在启动阶段有一个耗时的初始化操作,可以考虑显示一个闪屏,要么尽快的显示主界面,然后马上显示一个加载的对话框,异步加载数据。无论哪种情况,你都应该显示一个进度信息,以免用户感觉程序有卡顿的情况。 - 使用性能测试工具,例如Systrace与Traceview来判断程序中影响响应性的瓶颈。
';

与UI线程通信

最后更新于:2022-04-01 01:47:12

> 编写:[AllenZheng1991](https://github.com/AllenZheng1991) - 原文:[http://developer.android.com/training/multiple-threads/communicate-ui.html](http://developer.android.com/training/multiple-threads/communicate-ui.html) 在前面的课程中你学习了如何在一个被[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)管理的线程中开启一个任务。最后这一节课将会向你展示如何从执行的任务中发送数据给运行在UI线程中的对象。这个功能允许你的任务可以做后台工作,然后把得到的结果数据转移给UI元素使用,例如位图数据。 任何一个APP都有自己特定的一个线程用来运行UI对象,比如[View](http://developer.android.com/reference/android/view/View.html)对象,这个线程我们称之为UI线程。只有运行在UI线程中的对象能访问运行在其它线程中的对象。因为你的任务执行的线程来自一个线程池而不是执行在UI线程,所以他们不能访问UI对象。为了把数据从一个后台线程转移到UI线程,需要使用一个运行在UI线程里的[Handler](http://developer.android.com/reference/android/os/Handler.html)。 ### 在UI线程中定义一个Handler [Handler](http://developer.android.com/reference/android/os/Handler.html)属于Android系统的线程管理框架的一部分。一个[Handler](http://developer.android.com/reference/android/os/Handler.html)对象用于接收消息和执行处理消息的代码。一般情况下,如果你为一个新线程创建了一个[Handler](http://developer.android.com/reference/android/os/Handler.html),你还需要创建一个[Handler](http://developer.android.com/reference/android/os/Handler.html),让它与一个已经存在的线程关联,用于这两个线程之间的通信。如果你把一个[Handler](http://developer.android.com/reference/android/os/Handler.html)关联到UI线程,处理消息的代码就会在UI线程中执行。 你可以在一个用于创建你的线程池的类的构造方法中实例化一个[Handler](http://developer.android.com/reference/android/os/Handler.html)对象,并把它定义为全局变量,然后通过使用[Handler (Looper) ](http://developer.android.com/reference/android/os/Handler.html#Handler)这一构造方法实例化它,用于关联到UI线程。[Handler(Looper)](http://developer.android.com/reference/android/os/Handler.html#Handler(android.os.Looper))这一构造方法需要传入了一个[Looper](http://developer.android.com/reference/android/os/Looper.html)对象,它是Android系统的线程管理框架中的另一部分。当你在一个特定的[Looper](http://developer.android.com/reference/android/os/Looper.html)实例的基础上去实例化一个[Handler](http://developer.android.com/reference/android/os/Handler.html)时,这个[Handler](http://developer.android.com/reference/android/os/Handler.html)与[Looper](http://developer.android.com/reference/android/os/Looper.html)运行在同一个线程里。例如: ~~~ private PhotoManager() { ... // Defines a Handler object that's attached to the UI thread mHandler = new Handler(Looper.getMainLooper()) { ... ~~~ 在这个[Handler](http://developer.android.com/reference/android/os/Handler.html)里需要重写[handleMessage()](http://developer.android.com/reference/android/os/Handler.html#handleMessage(android.os.Message))方法。当这个[Handler](http://developer.android.com/reference/android/os/Handler.html)接收到由另外一个线程管理的[Handler](http://developer.android.com/reference/android/os/Handler.html)发送过来的新消息时,Android系统会自动调用这个方法,而所有线程对应的[Handler](http://developer.android.com/reference/android/os/Handler.html)都会收到相同信息。例如: ~~~ /* * handleMessage() defines the operations to perform when * the Handler receives a new Message to process. */ @Override public void handleMessage(Message inputMessage) { // Gets the image task from the incoming Message object. PhotoTask photoTask = (PhotoTask) inputMessage.obj; ... } ... } } ~~~ 下一部分将向你展示如何用[Handler](http://developer.android.com/reference/android/os/Handler.html)转移数据。 ### 把数据从一个任务中转移到UI线程 为了从一个运行在后台线程的任务对象中转移数据到UI线程中的一个对象,首先需要存储任务对象中的数据和UI对象的引用;接下来传递任务对象和状态码给实例化[Handler](http://developer.android.com/reference/android/os/Handler.html)的那个对象。在这个对象里,发送一个包含任务对象和状态的[Message](http://developer.android.com/reference/android/os/Message.html)给[Handler](http://developer.android.com/reference/android/os/Handler.html)也运行在UI线程中,所以它可以把数据转移到UI线程。 ### 在任务对象中存储数据 比如这里有一个[Runnable](http://developer.android.com/reference/java/lang/Runnable.html),它运行在一个编码了一个[Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html)且存储这个[Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html)到父类_PhotoTask_对象里的后台线程。这个[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)同样也存储了状态码_DECODE_STATE_COMPLETED_。 ~~~ // A class that decodes photo files into Bitmaps class PhotoDecodeRunnable implements Runnable { ... PhotoDecodeRunnable(PhotoTask downloadTask) { mPhotoTask = downloadTask; } ... // Gets the downloaded byte array byte[] imageBuffer = mPhotoTask.getByteBuffer(); ... // Runs the code for this task public void run() { ... // Tries to decode the image buffer returnBitmap = BitmapFactory.decodeByteArray( imageBuffer, 0, imageBuffer.length, bitmapOptions ); ... // Sets the ImageView Bitmap mPhotoTask.setImage(returnBitmap); // Reports a status of "completed" mPhotoTask.handleDecodeState(DECODE_STATE_COMPLETED); ... } ... } ... ~~~ _PhotoTask_类还包含一个用于给[ImageView](http://developer.android.com/reference/android/widget/ImageView.html)显示[Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html)的handler。虽然[Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html)和[ImageView](http://developer.android.com/reference/android/widget/ImageView.html)ImageView的引用在同一个对象中,但你不能把这个[Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html)分配给[ImageView](http://developer.android.com/reference/android/widget/ImageView.html)去显示,因为它们并没有运行在UI线程中。 这时,下一步应该发送这个状态给`PhotoTask`对象。 ### 发送状态取决于对象层次 _PhotoTask_是下一个层次更高的对象,它包含将要展示数据的编码数据和[View](http://developer.android.com/reference/android/view/View.html)对象的引用。它会收到一个来自_PhotoDecodeRunnable_的状态码,并把这个状态码单独传递到一个包含线程池和[Handler](http://developer.android.com/reference/android/os/Handler.html)实例的对象: ~~~ public class PhotoTask { ... // Gets a handle to the object that creates the thread pools sPhotoManager = PhotoManager.getInstance(); ... public void handleDecodeState(int state) { int outState; // Converts the decode state to the overall state. switch(state) { case PhotoDecodeRunnable.DECODE_STATE_COMPLETED: outState = PhotoManager.TASK_COMPLETE; break; ... } ... // Calls the generalized state method handleState(outState); } ... // Passes the state to PhotoManager void handleState(int state) { /* * Passes a handle to this task and the * current state to the class that created * the thread pools */ sPhotoManager.handleState(this, state); } ... } ~~~ ### 转移数据到UI 从_PhotoTask_对象那里,_PhotoManager_对象收到了一个状态码和一个_PhotoTask_对象的handler。因为状态码是_TASK_COMPLETE_,所以创建一个[Message](http://developer.android.com/reference/android/os/Message.html)应该包含状态和任务对象,然后把它发送给[Handler](http://developer.android.com/reference/android/os/Handler.html): ~~~ public class PhotoManager { ... // Handle status messages from tasks public void handleState(PhotoTask photoTask, int state) { switch (state) { ... // The task finished downloading and decoding the image case TASK_COMPLETE: /* * Creates a message for the Handler * with the state and the task object */ Message completeMessage = mHandler.obtainMessage(state, photoTask); completeMessage.sendToTarget(); break; ... } ... } ~~~ 最终,[Handler.handleMessage()](http://developer.android.com/reference/android/os/Handler.html#handleMessage(android.os.Message))会检查每个传入进来的[Message](http://developer.android.com/reference/android/os/Message.html),如果状态码是_TASK_COMPLETE_,这时任务就完成了,而传入的[Message](http://developer.android.com/reference/android/os/Message.html)里的_PhotoTask_对象里同时包含一个[Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html)和一个[ImageView](http://developer.android.com/reference/android/widget/ImageView.html)。因为[Handler.handleMessage()](http://developer.android.com/reference/android/os/Handler.html#handleMessage(android.os.Message))运行在UI线程里,所以它能安全地转移[Bitmap](http://developer.android.com/reference/android/graphics/Bitmap.html)数据给[ImageView](http://developer.android.com/reference/android/widget/ImageView.html): ~~~ private PhotoManager() { ... mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message inputMessage) { // Gets the task from the incoming Message object. PhotoTask photoTask = (PhotoTask) inputMessage.obj; // Gets the ImageView for this task PhotoView localView = photoTask.getPhotoView(); ... switch (inputMessage.what) { ... // The decoding is done case TASK_COMPLETE: /* * Moves the Bitmap from the task * to the View */ localView.setImageBitmap(photoTask.getImage()); break; ... default: /* * Pass along other messages from the UI */ super.handleMessage(inputMessage); } ... } ... } ... } ... } ~~~
';

启动与停止线程池中的线程

最后更新于:2022-04-01 01:47:10

> 编写:[AllenZheng1991](https://github.com/AllenZheng1991) - 原文:[http://developer.android.com/training/multiple-threads/run-code.html](http://developer.android.com/training/multiple-threads/run-code.html) 在前面的课程中向你展示了如何去定义一个可以管理线程池且能在他们中执行任务代码的类。在这一课中我们将向你展示如何在线程池中执行任务代码。为了达到这个目的,你需要把任务添加到线程池的工作队列中去,当一个线程变成可运行状态时,ThreadPoolExecutor从工作队列中取出一个任务,然后在该线程中执行。 这节课同时也向你展示了如何去停止一个正在执行的任务,这个任务可能在刚开始执行时是你想要的,但后来发现它所做的工作并不是你所需要的。你可以取消线程正在执行的任务,而不是浪费处理器的运行时间。例如你正在从网络上下载图片且对下载的图片进行了缓存,当检测到正在下载的图片在缓存中已经存在时,你可能希望停止这个下载任务。当然,这取决于你编写APP的方式,因为可能压在你启动下载任务之前无法获知是否需要启动这个任务。 ### 启动线程池中的线程执行任务 为了在一个特定的线程池的线程里开启一个任务,可以通过调用ThreadPoolExecutor.execute(),它需要提供一个Runnable类型的参数,这个调用会把该任务添加到这个线程池中的工作队列。当一个空闲的线程进入可执行状态时,线程管理者从工作队列中取出等待时间最长的那个任务,并且在线程中执行它。 ~~~ public class PhotoManager { public void handleState(PhotoTask photoTask, int state) { switch (state) { // The task finished downloading the image case DOWNLOAD_COMPLETE: // Decodes the image mDecodeThreadPool.execute( photoTask.getPhotoDecodeRunnable()); ... } ... } ... } ~~~ 当ThreadPoolExecutor在一个线程中开启一个Runnable后,它会自动调用Runnable的run()方法。 ### 中断正在执行的代码 为了停止执行一个任务,你必须中断执行这个任务的线程。在准备做这件事之前,当你创建一个任务时,你需要存储处理该任务的线程。例如: ~~~ class PhotoDecodeRunnable implements Runnable { // Defines the code to run for this task public void run() { /* * Stores the current Thread in the * object that contains PhotoDecodeRunnable */ mPhotoTask.setImageDecodeThread(Thread.currentThread()); ... } ... } ~~~ 想要中断一个线程,你可以调用[Thread.interrupt()](http://developer.android.com/reference/java/lang/Thread.html#interrupt())。需要注意的是这些线程对象都被系统控制,系统可以在你的APP进程之外修改他们。因为这个原因,在你要中断一个线程时,你需要把这段代码放在一个同步代码块中对这个线程的访问加锁来解决这个问题。例如: ~~~ public class PhotoManager { public static void cancelAll() { /* * Creates an array of Runnables that's the same size as the * thread pool work queue */ Runnable[] runnableArray = new Runnable[mDecodeWorkQueue.size()]; // Populates the array with the Runnables in the queue mDecodeWorkQueue.toArray(runnableArray); // Stores the array length in order to iterate over the array int len = runnableArray.length; /* * Iterates over the array of Runnables and interrupts each one's Thread. */ synchronized (sInstance) { // Iterates over the array of tasks for (int runnableIndex = 0; runnableIndex < len; runnableIndex++) { // Gets the current thread Thread thread = runnableArray[taskArrayIndex].mThread; // if the Thread exists, post an interrupt to it if (null != thread) { thread.interrupt(); } } } } ... } ~~~ 在大多数情况下,通过调用Thread.interrupt()能立即中断这个线程,然而他只能停止那些处于等待状态的线程,却不能中断那些占据CPU或者耗时的连接网络的任务。为了避免拖慢系统速度或造成系统死锁,在尝试执行耗时操作之前,你应该测试当前是否存在处于挂起状态的中断请求: ~~~ /* * Before continuing, checks to see that the Thread hasn't * been interrupted */ if (Thread.interrupted()) { return; } ... // Decodes a byte array into a Bitmap (CPU-intensive) BitmapFactory.decodeByteArray( imageBuffer, 0, imageBuffer.length, bitmapOptions); ... ~~~
';

为多线程创建线程池

最后更新于:2022-04-01 01:47:07

> 编写:[AllenZheng1991](https://github.com/AllenZheng1991) - 原文:[http://developer.android.com/training/multiple-threads/create-threadpool.html](http://developer.android.com/training/multiple-threads/create-threadpool.html) 在前面的课程中展示了如何在单独的一个线程中执行一个任务。如果你的线程只想执行一次,那么上一课的内容已经能满足你的需要了。 如果你想在一个数据集中重复执行一个任务,而且你只需要一个执行运行一次。这时,使用一个[IntentService](http://developer.android.com/reference/android/app/IntentService.html)将能满足你的需求。为了在资源可用的的时候自动执行任务,或者允许不同的任务同时执行(或前后两者),你需要提供一个管理线程的集合。为了做这个管理线程的集合,使用一个[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)实例,当一个线程在它的线程池中变得不受约束时,它会运行队列中的一个任务。为了能执行这个任务,你所需要做的就是把它加入到这个队列。 一个线程池能运行多个并行的任务实例,因此你要能保证你的代码是线程安全的,从而你需要给会被多个线程访问的变量附上同步代码块(synchronized block)。当一个线程在对一个变量进行写操作时,通过这个方法将能阻止另一个线程对该变量进行读取操作。典型的,这种情况会发生在静态变量上,但同样它也能突然发生在任意一个只实例化一次。为了学到更多的相关知识,你可以阅读[进程与线程](http://developer.android.com/guide/components/processes-and-threads.html)这一API指南。 ### 定义线程池类 在自己的类中实例化[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)类。在这个类里需要做以下事: **1. 为线程池使用静态变量** 为了有一个单一控制点用来限制CPU或涉及网络资源的[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)类型,你可能需要有一个能管理所有线程的线程池,且每个线程都会是单个实例。比如,你可以把这个作为一部分添加到你的全局变量的声明中去: ~~~ public class PhotoManager { ... static { ... // Creates a single static instance of PhotoManager sInstance = new PhotoManager(); } ... ~~~ **2. 使用私有构造方法** 让构造方法私有从而保证这是一个单例,这意味着你不需要在同步代码块(synchronized block)中额外访问这个类: ~~~ public class PhotoManager { ... /** * Constructs the work queues and thread pools used to download * and decode images. Because the constructor is marked private, * it's unavailable to other classes, even in the same package. */ private PhotoManager() { ... } ~~~ **3.通过调用线程池类里的方法开启你的任务** 在线程池类中定义一个能添加任务到线程池队列的方法。例如: ~~~ public class PhotoManager { ... // Called by the PhotoView to get a photo static public PhotoTask startDownload( PhotoView imageView, boolean cacheFlag) { ... // Adds a download task to the thread pool for execution sInstance. mDownloadThreadPool. execute(downloadTask.getHTTPDownloadRunnable()); ... } ~~~ **4. 在构造方法中实例化一个[Handler](http://developer.android.com/reference/android/os/Handler.html),且将它附加到你APP的UI线程。** 一个[Handler](http://developer.android.com/reference/android/os/Handler.html)允许你的APP安全地调用UI对象(例如 [View](http://developer.android.com/reference/android/view/View.html)对象)的方法。大多数UI对象只能从UI线程安全的代码中被修改。这个方法将会在[与UI线程进行通信(Communicate with the UI Thread)](#)这一课中进行详细的描述。例如: ~~~ private PhotoManager() { ... // Defines a Handler object that's attached to the UI thread mHandler = new Handler(Looper.getMainLooper()) { /* * handleMessage() defines the operations to perform when * the Handler receives a new Message to process. */ @Override public void handleMessage(Message inputMessage) { ... } ... } } ~~~ ### 确定线程池的参数 一旦有了整体的类结构,你可以开始定义线程池了。为了初始化一个[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)对象,你需要提供以下数值: **1. 线程池的初始化大小和最大的大小** 这个是指最初分配给线程池的线程数量,以及线程池中允许的最大线程数量。在线程池中拥有的线程数量主要取决于你的设备的CPU内核数。 这个数字可以从系统环境中获得: ~~~ public class PhotoManager { ... /* * Gets the number of available cores * (not always the same as the maximum number of cores) */ private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); } ~~~ 这个数字可能并不反映设备的物理核心数量,因为一些设备根据系统负载关闭了一个或多个CPU内核,对于这样的设备,`availableProcessors()`方法返回的是处于活动状态的内核数量,可能少于设备的实际内核总数。 **2.线程保持活动状态的持续时间和时间单位** 这个是指线程被关闭前保持空闲状态的持续时间。这个持续时间通过时间单位值进行解译,是[TimeUnit()](http://developer.android.com/reference/java/util/concurrent/TimeUnit.html)中定义的常量之一。 **3.一个任务队列** 这个传入的队列由[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)获取的[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)对象组成。为了执行一个线程中的代码,一个线程池管理者从先进先出的队列中取出一个[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)对象且把它附加到一个线程。当你创建线程池时需要提供一个队列对象,这个队列对象类必须实现[BlockingQueue](http://developer.android.com/reference/java/util/concurrent/BlockingQueue.html)接口。为了满足你的APP的需求,你可以选择一个Android SDK中已经存在的队列实现类。为了学习更多相关的知识,你可以看一下[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)类的概述。下面是一个使用[LinkedBlockingQueue](http://developer.android.com/reference/java/util/concurrent/LinkedBlockingQueue.html)实现的例子: ~~~ public class PhotoManager { ... private PhotoManager() { ... // A queue of Runnables private final BlockingQueue<Runnable> mDecodeWorkQueue; ... // Instantiates the queue of Runnables as a LinkedBlockingQueue mDecodeWorkQueue = new LinkedBlockingQueue<Runnable>(); ... } ... } ~~~ ### 创建一个线程池 为了创建一个线程池,可以通过调用[ThreadPoolExecutor()](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html#ThreadPoolExecutor(int, int, long, java.util.concurrent.TimeUnit, java.util.concurrent.BlockingQueue<java.lang.Runnable>))构造方法初始化一个线程池管理者对象,这样就能创建和管理一组可约束的线程了。如果线程池的初始化大小和最大大小相同,[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)在实例化的时候就会创建所有的线程对象。例如: ~~~ private PhotoManager() { ... // Sets the amount of time an idle thread waits before terminating private static final int KEEP_ALIVE_TIME = 1; // Sets the Time Unit to seconds private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; // Creates a thread pool manager mDecodeThreadPool = new ThreadPoolExecutor( NUMBER_OF_CORES, // Initial pool size NUMBER_OF_CORES, // Max pool size KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDecodeWorkQueue); } ~~~
';

在一个线程中执行一段特定的代码

最后更新于:2022-04-01 01:47:05

> 编写:[AllenZheng1991](https://github.com/AllenZheng1991) - 原文:[http://developer.android.com/training/multiple-threads/define-runnable.html](http://developer.android.com/training/multiple-threads/define-runnable.html) 这一课向你展示了如何通过实现 [Runnable](http://developer.android.com/reference/java/lang/Runnable.html)接口得到一个能在重写的[`Runnable.run()`](http://developer.android.com/reference/java/lang/Runnable.html)方法中执行一段代码的单独的线程。另外你可以传递一个[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)对象到另一个对象,然后这个对象可以把它附加到一个线程,并执行它。一个或多个执行特定操作的[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)对象有时也被称为一个任务。 [Thread](http://developer.android.com/reference/java/lang/Runnable.html)和[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)只是两个基本的线程类,通过他们能发挥的作用有限,但是他们是强大的Android线程类的基础类,例如Android中的[HandlerThread](http://developer.android.com/reference/android/os/HandlerThread.html), [AsyncTask](http://developer.android.com/reference/android/os/AsyncTask.html)和[IntentService](http://developer.android.com/reference/android/app/IntentService.html)都是以它们为基础。[Thread](http://developer.android.com/reference/java/lang/Runnable.html)和[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)同时也是[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)类的基础。[ThreadPoolExecutor](http://developer.android.com/reference/java/util/concurrent/ThreadPoolExecutor.html)类能自动管理线程和任务队列,甚至可以并行执行多个线程。 ### 定义一个实现Runnable接口的类 直接了当的方法是通过实现[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)接口去定义一个线程类。例如: ~~~ public class PhotoDecodeRunnable implements Runnable { ... @Override public void run() { /* * 把你想要在线程中执行的代码写在这里 */ ... } ... } ~~~ ### 实现run()方法 在一个类里,[`Runnable.run()`](http://developer.android.com/reference/java/lang/Runnable.html)包含执行了的代码。通常在[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)中执行任何操作都是可以的,但需要记住的是,因为[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)不会在UI线程中运行,所以它不能直接更新UI对象,例如[View](http://developer.android.com/reference/android/view/View.html)对象。为了与UI对象进行通信,你必须使用另一项技术,在[与UI线程进行通信](#)这一课中我们会对其进行描述。 在[Runnable.run()](http://developer.android.com/reference/java/lang/Runnable.html#run())方法的开始的地方通过调用参数为[THREAD_PRIORITY_BACKGROUND](http://developer.android.com/reference/android/os/Process.html#THREAD_PRIORITY_BACKGROUND")的[Process.setThreadPriority()](http://developer.android.com/reference/android/os/Process.html#setThreadPriority(int))方法来设置线程使用的是后台运行优先级。这个方法减少了通过[Runnable](http://developer.android.com/reference/java/lang/Runnable.html)创建的线程和和UI线程之间的资源竞争。 **你还应该通过在Runnable自身中调用[Thread.currentThread()](http://developer.android.com/reference/java/lang/Thread.html#currentThread())来存储一个引用到Runnable对象的线程。** 下面这段代码展示了如何创建run()方法: ~~~ class PhotoDecodeRunnable implements Runnable { ... /* * 定义要在这个任务中执行的代码 */ @Override public void run() { // 把当前的线程变成后台执行的线程 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); ... /* * 在PhotoTask实例中存储当前线程,以至于这个实例能中断这个线程 */ mPhotoTask.setImageDecodeThread(Thread.currentThread()); ... } ... } ~~~
';