0%

过年在家没啥事干,折腾了下黑苹果,安装上了 OSX 10.14.3,整个过程还算顺利,最终除了睡眠和声卡之外,其他貌似ok,目前用来开发(Android studio 以及 XCode)没啥问题。此次记录主要是为了在以后还有闲心折腾黑苹果的时候,能拿出来参考,不至于从头开始爬帖。

首先,以一个过来人的身份告诉你,搞黑苹果如果不上 www.tonymacx86.com 网站的话,等于白折腾,这上面有最简便的最新的折腾黑苹果的方法,在国内远景论坛、黑苹果社区等地方看到的清一色古老的安装方式,甚至还有变色龙安装法。不要害怕 English,对于搞黑苹果而言,只需要很简单的英语水平。我这次安装10.14.3参考的就是这个帖子,只要一步步来,基本上是没什么问题的。

这个帖子主要条理清晰地通过5步,即完成了黑苹果的安装:
Step 1: Download macOS Mojave(第一步:下载系统)
Step 2: Create a Bootable USB with UniBeast(第二步:通过 UniBeast 制作 U盘启动盘)
Step 3: Recommended BIOS Settings(第三步:设置 BIOS )
Step 4: Install macOS Mojave(第四步:安装系统)
Step 5: Post Installation with MultiBeast(第五步:二次安装)

在开始之前,首先申请 tonymacx86.com 的账号,然后 在从那里下载最新的 UniBeast 和 MultiBeast 工具,之后准备一个 32GB 的U盘吧,教程上面说 16G 以上就够了,但实际上是不够的。其中,UniBeast 的作用是 创建一个可以正常启动10.14.3 的 U盘启动盘。

第一步 下载系统

为了全新安装,首先可以从其他苹果系统里面下载系统,在苹果系统中:

打开 App Store -> 下载macos Mojave 更新,下载完成后,在 Applications(应用)里面能够找到它。

第二步 用 UniBeast 创建启动U盘

在第二步之前,首先备份好U盘的东西,因为制作启动盘会删除U盘中所有内容。

把U盘插到电脑上,打开:Application(应用)->Utilities(工具)-> Disk Utility(磁盘工具),从左边选中你的那个U盘,在Mojava或者更高版本中,可能你首先要设置 View(视图)->Show All Devices(显示所有设备),才能看到你的U盘。之后执行

1、 点击 Erase(抹掉)按钮
2、填写名称,目前先命名为 USB
3、在Format(格式)的地方选择 Mac os Extended(Journaled)
4、最后,点击 Erase(抹掉) 按钮正式抹掉

接下来,安装你下载的 UniBeast (这里貌似要求语言是English,否则还装不上,如果有这个要求,改下系统语言就行了),只要一直continue 下去就行,之后同意协议,最后选择安装的 Destination(目的磁盘)的时候,选择刚才抹掉的 USB盘 即可;在 select os Installation (选择安装系统)界面时,选择 Mojave 就行,之后在 Bootloader Options 界面选择 UEFI Boot Mode(根据需要有可能要选择 Legacy Boot Mode)。最后 Graphics Configuration 是可选的,可以根据你的显卡来对应设置。最后确认你的安装选项,输入密码,然后 Install。这时候大概要等待个10分钟,系统会写完。

系统完全写入U盘之后,把下载的 MultiBeast 软件拖到 U 盘,因为安装好之后需要它。

第三步 设置BIOS

这里只针对UEFI 方式的BIOS 了(其他方式请参考原文了)。进入到 BIOS 设置中,如果CPU支持 VT-d,将其设置为 disable,CFG-Lock、Secure Boot Mode 、IO Serial Port 也 disable,设置 XHCI 为 Enable,设置 OS 为 Other OS。

第四步 安装 macOS Mojave

将U盘插在电脑的USB2.0口上,这点特别要注意,不然第二次安装的时候会提示找不到resource,我就在这里折腾了两回才知道。开机,选择从你的 USB 盘启动(不同的电脑主板不一样,网上搜下即可)。
在clover界面,通过键盘上的左右箭头(键盘右下角的上下左右箭头)选择 Boot OS X Install from macOS Mojave

在安装界面,首先选择顶部菜单栏的 Utilities(工具),再打开 Disk Utility(磁盘工具),在左边选中你想要安装系统的分区,之后像之前抹掉U盘那样抹掉这个分区,名字命名为 Mojave(之后你可以改这个名字),之后关闭Disk Utility(磁盘工具),即可继续安装。

安装的时候会提示安装到哪个盘,选择刚才抹掉的 Mojave 盘,之后会继续安装,待安装完成后会自动重启。

第五步

在重启的时候要注意再次选择从U盘启动,之后在clover界面选择Mojave,之后就会自动安装,引导设置,完成。之后的事情就是安装驱动了,去论坛找各种kext即可,声卡可能就麻烦些,可能需要dsdt,这里就不细说。

问题集锦

提示已损坏,不能安装

如果安装过程中提示:

安装 macOS xxx”应用程序副本已损坏,不能用来安装macOS

则并不是镜像本身有问题,而是由于镜像的证书过期,并且苹果没有更新证书日期导致的。解决办法就是: 1、断开网络 2、在中断输入命令修改时间:

date 0201010116

之后,关闭终端再安装即可

休眠导致键盘不可用

如果是笔记本并且安装的是双系统(win + mac os),那么如果驱动对休眠的处理不好的话,在mac osx下发生休眠可能会导致系统重启,可能会重启进入windows系统,此时,你的键盘应该是不可用的。解决方案包括以下几个步骤:

  1. 启动windows ,在登录界面使用 “轻松使用-屏幕键盘” 的方式输入密码。
  2. 在windows 下使用 easyUefi 软件将 黑苹果的 efi 启动序列列为第一个,这样我们开机就会启动 clover。
  3. 重启系统,进入clover,此时键盘应该是不可以使用的,左右光标也是没作用的。如果能够看到你要启动的分区,则用鼠标点击直接启动,进入mac os 后,正常关闭,键盘就能正常使用了。
  4. 如果未能看到需要启动的分区,此时按字母 A 键,就会进入clover 的 “关于” 页面(这个我只在我电脑上试过,我在尝试按 A 或者 D 的时候发现的,对其他电脑的未做验证),此时,你的电脑键盘应该就可以使用了。这时候就可以退出“关于”页,之后进入黑苹果。
  5. 以上操作的主要目的是要正常关闭mac os ,这样键盘才能使用(在 mac os 或者 windows 下)。

从字面可以看出这应该是一种远程view,与远程service概念一样,RemoteViews表示的是一种view结构,这种view能够在其他进程显示。由于需要跨进程显示,RemoteViews结构提供了一组基础功能用于跨进程更新View。RemoteViews在Android中有2种应用场景:Notification以及桌面小部件

RemoteViews的应用

平时的开发过程中,Notifications主要通过NotificationManager的notify方法实现的,它除了默认效果外,还可以另外定义布局。使用RemoteViews实现通知栏时无法像Activity里面一样直接更新View,这是因为RemoteView界面运行在其他进程中,确切来说是系统的SystemServer进程。使用系统默认的样式弹出一个通知是很简单的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Notification notification = newNotification();
notification.icon = R.drawable.icon;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this,DemoActivity_1.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);

//使用普通样式展示一个通知
notification.setLatestEventInfo(this,"chapter_5","this is notification",pendingIntent);
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1,notification);

//使用RemoteViews的方式展示第一个通知
RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg,"chapter_5");
remoteViews.setImageViewResource(R.id.icon,R.drawable.icon1);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this,0,new Intent(this,DemoActivity_2.class),PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnclickPendingIntent(R.id.open_activity2,openActivity2PendingIntent);
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(2,notification);

以上代码展示只要提供当前应用的包名以及布局文件的id即可创建一个RemoteViews对象,而更新RemoteViews,由于无法直接访问里面的view,因而只能通过RemoteViews提供的一系列方法来更新,比如设置文本,需要采用 remoteViews.setTextViewText(R.id.msg,”chapter_5”) ,而更新图片则采用 remoteViews.setImageViewResource(R.id.icon,R.drawable.icon1),如果要给一个控件添加click事件,则要使用PendingIntent并且通过setOnclickPendingIntent。关于PendingIntent,它表示一种待定的Intent,这个Intent中所包含的意图必须由用户来出发。

RemoteViews在桌面小部件上的应用、PendingIntent概述、RemoteViews的内部机制等内容 待后续有集中的时间再添加

照例,如果自学不需要看我这个博客的话,资料如下:
官网关于Android Test的介绍的地址
Android官方关于测试的例子 ,需要的自取

单元测试、集成测试、黑盒测试、白盒测试等,只有单元测试是我们开发人员需要自己完成的,其余都是由测试人员完成的。单元测试本质上也是代码,是验证代码正确性的代码。

为什么要做单元测试

  • 便于后期重构。单元测试为后期测试提供了保障,在重构之后,只要单元测试还是全部通过,那么在很大程度上表示重构没有引入新的bug。
  • 优化设计。编写单元测试将使开发者从调用者的角度观察和思考,这样迫使开发者把程序设计成易于调用和低耦合的易测试的形式。
  • 避免代码出现回归。编写完成后,可以随时随地快速运行测试,而不是要求将代码部署到设备上,再手动执行覆盖各种路径。
  • 文档记录。单元测试是极好的“官方文档”,它展示函数或者类如何使用。

Android 测试类型(选自官网)

测试代码的位置取决于您要编写的测试的类型。 Android Studio 为以下两种测试类型提供了源代码目录(源集):

本地单元测试

位于 module-name/src/test/java/目录。

这些测试在计算机的本地 Java 虚拟机 (JVM) 上运行。 当您的测试没有 Android 框架依赖项或当您可以模拟 Android 框架依赖项时,可以利用这些测试来尽量缩短执行时间。

在运行时,这些测试的执行对象是去掉了所有 final 修饰符的修改版 android.jar。 这样一来,您就可以使用 Mockito 之类的常见模拟库。

Instrumented测试(仪器测试)

位于 module-name/src/androidTest/java/。

这些测试在硬件设备或模拟器上运行。 这些测试有权访问 Instrumentation API,让您可以获取某些信息(例如您要测试的应用的 Context), 并且允许您通过测试代码来控制受测应用。 可以在编写集成和功能 UI 测试来自动化用户交互时,或者在测试具有模拟对象无法满足的 Android 依赖项时使用这些测试。

由于仪器测试内置于 APK 中(与您的应用 APK 分离),因此它们必须拥有自己的 AndroidManifest.xml 文件。 不过,由于 Gradle 会自动在构建时生成该文件,因此它在您的项目源集中不可见。 您可以在必要时(例如需要为 minSdkVersion 指定其他值或注册测试专用的运行侦听器时)添加自己的清单文件。 构建应用时,Gradle 会将多个清单文件合并成一个清单。

Gradle 构建解读这些测试源集的方式与其解读项目应用源集的方式相同,您可以利用这一点根据构建变体创建测试。

以下示意图诠释了两种测试的代码结构(图中1表示的是仪器测试的代码,2表示的是单元测试的代码结构)

单元测试与仪器测试示意图

Junit4

在Android测试框架中,常用的有以下几个框架和工具类:JUnit4、AndroidJUnitRunner、Mockito、Espresso,其中主要的单元测试使用Junit4。Junit4是一套基于注解的单元测试框架,在Android studio中,编写在test目录下的测试类都是基于该框架实现,该目录下的代码直接运行在本地的JVM上,不需要Android真机或者模拟器支持。常用的注解如下(更多内容可以查看Junit4官网):

  • @BeforeClass 测试类里所有用例运行之前,运行一次这个方法。方法必须是public static void
  • @AfterClass 与BeforeClass对应
  • @Before 在每个用测试例运行之前都运行一次。
  • @After 与Before对应
  • @Test 指定该方法为测试方法,方法必须是public void
  • @RunWith 测试类名之前,用来确定这个类的测试运行器
    以下用一个简单的测试类来展示测试类的大概形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CaculatUtilTest{
private CaculatUtil mCaculatUtil;

@Before
public void setUp(){
mCaculatUtil = new CaculatUtil();
}

@Test
public void addTwoNumbers(){
assertEquals(3,mCaculatUtil.add(1,2));
//或者如果是静态方法,就类似于以下这种静态调用方法
assertEquals(3,Caculator.add(1,2));
}
}

Junit的断言和失败提示

Junit提供了多个以assert开头的函数,分别用来验证各类相等性质的问题,大致有如下几类:

  • assertEquals

    assertEquals的作用是判断两个值或者对象是否相等。接受2个参数,参数1为预期值,参数2为计算得到的值。

  • assertTrue 与 assertFalse

    assertTrue 与 assertFalse顾名思义就是分别验证真与假,只需要一个boolean类型的参数。例如 assertTrue(false)测试会失败, 而 assertTrue(true) 测试通过。

  • assertNull 与 assertNotNull

    和assertTrue、assertFalse类似,只不过是用来判断空或者非空。例如:assertNull(null) 会测试失败,因为值为null;而assertNull(“hell”)就能测试通过。

  • assertSame 与 assertNotSame

    assertSame用于判断两个对象是否是同一个对象,与assertEquals不同的是,assertSame强调的为同一个对象,而assertEquals只要两个对象相等即可(即调用equals函数时返回true)。

  • failNotEquals

    函数有3个参数,参数1位失败时提示信息,参数2为期望值,参数3是实际值。当两个对象不相等时抛出参数1的错误信息。

  • failSame与failNotSame

    failNotSame与failNotEquals类似,不是同一个对象时就抛出参数1的错误信息。

  • fail(String) 与 fail()

    fail(String)直接抛出当前测试用例参数1中的错误信息,而fail()给出默认的错误信息。

运行多个测试类——TestSuite

如果需要同时运行两个或多个Test类,JUnit提供了Suite注解,在对应的测试目录下创建一个空Test类:

  • @RunWith(Suite.class):配置Runner运行环境。
  • @Suite.SuiteClasses({A.class, B.class}):添加需要一起运行的测试类。
1
2
3
4
5
@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class, CalculatorWithParameterizedTest.class})
public class UnitTestSuite{

}

上述代码中,UnitTestSuite成了一个空类,测试类被添加到注解中了。
或者,如果不用注解,可以通过JUnit4TestAdapter包装测试类,并将JUnit4TestAdapter对象添加到TestSuit中,示例代码如下:

1
2
3
4
5
6
7
8
9
public class MathTestSuite{
public static Test suite(){
TestSuite suite = new TestSuite("com.book.jtm");
//添加测试用例
suite.addTest(new JUnit4TestAdapter(AdderTest.class));
suite.addTest(new JUnit4TestAdapter(DiverTest.class));
return suite;
}
}

上述代码有一个静态的suite函数,它返回一个Test对象,这个对象是TestSuite类型的。测试时,以Junit测试用例的形式运行这个MathTestSuite即可运行这两个测试类。

多个参数输入测试

当需要传入多个参数进行测试时,可以使用 @Parameters 来进行单个方法的多次不同参数的测试,对于测试类,使用该方法需要如下步骤:

  • 在测试类上添加@RunWith(Parameterized.class)注解
  • 添加测试类的构造函数
  • 添加获取参数集合的static方法,并在方法上添加@Parameters注解
  • 在需要测试的方法中直接使用成员变量,该变量由JUnit通过构造方法生成

直接上示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RunWith(Parameterized.class)//为测试类添加注解
public class CaculatUtilTest{
//两个传入的参数
private final int paramOne;
private final int paramTwo;
//期望值
private final int expectResult;

private CaculatUtil mCaculatUtil;

public CaculatUtilTest(int paramOne,int paramTwo,int expectResult){//添加构造函数
this.paramOne = paramOne;
this.paramTwo = paramTwo;
this.expectResult = expectResult;
}

//添加获取参数集合的static方法,并在方法上添加@Parameters注解
@Parameters
public static Collection<Object[]> initTestData(){
return Arrays.asList(new Object[][]{
{0,0,0},
{1,1,2},
{1,5,6}
});
}

@Before
public void setUp(){
mCaculatUtil = new CaculatUtil();
}

@Test
public void addTwoNumbers(){
//测试的方法中直接使用成员变量
assertEquals(expectResult,mCaculatUtil.add(paramOne,paramTwo));
}
}

AndroidJUnitRunner

当单元测试中涉及Android系统库的调用时,可以通过AndroidJUnitRunner方案完成测试,这样就能在测试类中使用Context、parcelable、Shareprefrence等类。使用方法是在androidTest目录下创建测试类(因为这涉及到Instrumented测试的内容),在该类上添加@RunWith(AndroidJUnit4.class)注解。如以下代码示范了如何在测试类中使用SharedPrefrences:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@RunWith(AndroidJUnit4.class)
public class SharedPreferencesHelperTest {

private static final String TEST_NAME = "Test name";

private static final String TEST_EMAIL = "test@email.com";

private static final Calendar TEST_DATE_OF_BIRTH = Calendar.getInstance();

private SharedPreferenceEntry mSharedPreferenceEntry;

private SharedPreferencesHelper mSharedPreferencesHelper;

private SharedPreferences mSharePreferences;

/** 上下文 */
private Context mContext;
……
@Before
public void setUp() throws Exception {
//获取application的context
mContext = InstrumentationRegistry.getTargetContext();
//实例化SharedPreferences
mSharePreferences = PreferenceManager.getDefaultSharedPreferences(mContext);

mSharedPreferenceEntry = new SharedPreferenceEntry(TEST_NAME, TEST_DATE_OF_BIRTH, TEST_EMAIL);
//实例化SharedPreferencesHelper,依赖注入SharePreferences
mSharedPreferencesHelper = new SharedPreferencesHelper(mSharePreferences);

//以下是在mock的相关操作,模拟commit失败
mMockSharePreferences = Mockito.mock(SharedPreferences.class);
mMockBrokenEditor = Mockito.mock(SharedPreferences.Editor.class);
when(mMockSharePreferences.edit()).thenReturn(mMockBrokenEditor);
when(mMockBrokenEditor.commit()).thenReturn(false);
mMockSharedPreferencesHelper = new SharedPreferencesHelper(mMockSharePreferences);
}

/**
* 测试保存数据是否成功
*/
@Test
public void sharedPreferencesHelper_SavePersonalInformation() throws Exception {
assertThat(mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry), is(true));
}
/**
* 测试保存数据,然后获取数据是否成功
*/
@Test
public void sharedPreferencesHelper_SaveAndReadPersonalInformation() throws Exception {
mSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry);
SharedPreferenceEntry sharedPreferenceEntry = mSharedPreferencesHelper.getPersonalInfo();
assertThat(isEquals(mSharedPreferenceEntry, sharedPreferenceEntry), is(true));
}
……
}

模拟所需要的模块

有时我们测试需要依赖于其他的功能模块,但是某些原因这个功能模块不能在测试时运用或未开发完,为了不阻塞测试,我们可以Mock对象来完成测试。还有一些场景,诸如对象很难被创建、真实对象运行缓慢、真实对象的错误很难出现等,也可以通过Mock对象来测试。

手动Mock对象

举个例子,开发一款记事本软件,登录成功后才能写/存笔记,小明小刘分别负责登录和写/存笔记功能,存笔记的时候时候需要用户信息User的实例,而用户信息在登录成功后才能获得。可行的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//保存数据的类
public class NoteDAO{
private NoteDAO noteDAO;

public void saveNote(User user,String note){
Log.d("NoteDAO","存储笔记");
}
}

//测试类
public class NoteTest{
@Before
public void setUp(){
noteDAO = new NoteDAO();
}

@Test
public void testSaveNote(){
MockLoginImpl loginImpl = new MockLoginImpl();
noteDAO.saveNote(loginImpl.login("dd","pwd"),"note_content");
}
}


//Mock类
public class MockLoginImpl {
public User login(String name,String pwd){
return new User(name,"1234556");
}
}

使用第三方工具Mockito

前面有例子已经涉及到Mockito的部分使用,可以在网上搜索相关使用,这里不再详细展开,如果需要,后面会专门介绍。

运行单元测试

  • 在Android studio中,对指定的测试类点击鼠标右键,选择对应的Run或者debug
  • 在Terminal输入gradle testDebugUnitTest或gradle testReleaseUnitTest指令来分别运行debug和release版本的unittesting,在执行的结果可以在xxx\project\app\build\reports\tests\testReleaseUnitTest中查看

声明:整篇文章有部分内容摘抄自博客:https://www.jianshu.com/p/925191464389

全局获取Context技巧

主要就是自定义 Application ,在 Application 中实现全局获取Context,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyApplication extends Application{
private static Context mContext;

@Override
public void onCreate(){
mContext = getApplicationContext();
}

public static Context getContext(){
return mContext;
}
}

使用Intent传递对象

Intent的putExtra()方法传递数据的时候,支持的数据类型是有限的,虽然常用的一些数据类型它都支持,但是当你想传递一些自定义对象的时候,就会无从下手。其实使用Intent来传递对象通常有两种实现方式:Serializable和Parcelable。

Serializable 方式

Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。至于序列化的方法也很简单,只需要实现Serializable这个接口就行,如以下的Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Person implements Serializable{
//TODO 这行在书中是没有的,实际上我们得加上,用于区分版本这个类的版本,不同的这个id不能反序列回来
//如果不写,系统会自动生成,但是如果改动了里面的属性(增加或者减少或者更改了属性),系统生成的这个值会改变
//(注意,如果没有实质改动(如只是改变属性的位置,或者在类中添加了空格)则值不会改变
private static final long serialVersionUID =1L;

private String name;
private int age;

public String getName(){
return name;
}

public void setName(String name){
this.name = name;
}

public int getAge(){
return age;
}

public void setAge(int age){
this.age = age;
}
}

如果要在Activity之间传递的话,只需要简单的几行代码即可:

1
2
3
4
5
6
Person person = new Person();
person.setName("Tom");
person.setAge("20");
Intent intent = new Intent(this,SecondActivity.class);
intent.putExtra("person_data",person);
startActivity(intent);

可以看到我们穿件了Person实例,之后直接将它传入putExtra()方法中了,只是因为Person实现了Serializable接口,所以才可以这么写。接下来要在SecondActivity中获取这个对象也很简单:

1
Person person = (Person)getIntent().getSerializableExtra("person_data");

这里调用了getSerializableExtra方法来获取通过参数传递过来的序列化对象,接着再向下转型得到Person对象,就成功实现了Intent传递对象。

Parcelable方式

除了Serializable之外,使用Parcelable也可以实现相同的效果,不过不同于将对象进行序列化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,这样也就实现了传递对象的功能了。下面修改下Person类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Person implements Parcelable{

private String name;
private int age;

。。。

@Override
public int describeContents(){
return 0;
}

@Override
public void writeToParcel(Parcel dest,int flags){
dest.writeString(name);//写出name
dest.writeInt(age);//写出age
}

public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>(){

@Override
public Person createFromParcel(Parcel source){
Person person = new Person();
person.name = source.readString();//读取name
person.age = source.readInt();
return person;
}

@Override
public Person[] newArray(int size){
return new Person[size];
}
};
}

Parcelable 的实现方式要稍微复杂些,首先继承 Parcelable 接口,这样就必须重写 describeContents() 和 writeToParcel() 两个方法。 describeContents 中直接返回0即可,writeToParcel中需要将Person类中的字段一一写出。除此之外,还得再Person类中提供一个 CREATOR 常量。接下来,我们仍然可以通过前面相同的代码来传递Person对象,只不过在SecondActivity中获取对象的时候需要稍加改动:

1
Person person = (Person)getIntent().getParcelableExtra("person_data");

一般来说,Serializable方式比较简单,但是这会把整个对象序列化,因此效率比Parcelable低一些,所以更加推荐Parcelable方式。

定制自己的日志工具

实用性不太强,公司项目有更强大的日志工具,因此 略

调试Android程序

已经掌握,略

创建定时任务

Android中的定时任务一般有两种实现方式:一是Java API中的Timer类,二是Android中的Alarm机制。但Timer类不太适用于哪些需要长期在后台运行的定时任务,因为为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android手机会在长时间不操作的情况下自动让CPU进入睡眠状态,这可能导致Timer中的定时任务无法正常运行。而Alarm则具有唤醒CPU功能,可以保证大多数情况下需要执行定时任务的时候CPU都能正常工作。注意,这里唤醒CPU和唤醒屏幕完全不是一个概念,千万不要混淆

Alarm机制

Alarm机制用法并不复杂,主要借助于AlarmManager实现,跟NotificationManager有点类似,获取实例的方法如下所示:

1
AlarmManager manager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);

接下来使用set()方法就可以设置一个定时任务了,比如想设定一个任务在10秒钟后执行:

1
2
long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);

第一个参数是整型参数,用于指定AlarmManager的工作类型,有4种值可选:ELAPSED_REALTIME(让定时任务的触发时间从系统开机开始算,但不会唤醒CPU)、ELAPSED_REALTIME_WAKEUP(表示定时任务从系统开机开始算起,但会唤醒CPU)、RTC(让定时任务触发时间从1970年1月1日0点开始算起,但不会唤醒CPU)、RTC_WAKEUP(让定时任务触发时间从1970年1月1日0点开始算起,但会唤醒CPU)。可以使用SystemClock.elapsedRealtime()获取到系统开机至今所经历的时间的毫秒数。Systemt.currentTimeMillis()可以获取到1970年1月1日0点至今所经历的毫秒数。第二个参数就是定时任务触发的时间,如果第一个参数是ELAPSED_REALTIME或者ELAPSED_REALTIME_WAKEUP,则传入开机至今时间加上延迟执行的时间;如果是RTC或者RTC_WAKEUP,则传入1970年1月1日0点至今的时间再加上延迟执行的时间。第三个参数不多说。

那么如果要实现一个长时间在后台定时运行的服务要如何做呢,其实只要建立一个普通服务,然后将触发定时任务的代码写到onStartCommand()方法中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class LongRuningService extends Service{

...

@Override
public int onstartCommand(Intent intent,int flags,int startId){

//TODO 之所以要在子线程里面执行逻辑操作,是因为逻辑操作需要耗时,放在主线程中可能会对定时任务准确性产生轻微影响。
new Thread(new Runnable(){
@Override
public void run(){
//执行具体的逻辑操作
}
}).start();

AlarmManager manager = (AlarmManager)getSystemService(ALARM_SERVICE);
int anHour = 60 * 60 * 1000;//一小时
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
Intent i = new Intent(this,LongRunningService(this,0,i,0));
//TODO 注意,从4.4开始,Alarm任务的触发将会变得不准确,有可能会延迟一段时间后任务才能得到执行,这不是bug,
//而是系统在耗电方面的优化,系统会自动检测目前有多少Alarm任务,然后将触发时间相近的几个任务放在一起执行,可以
//大幅度减少CPU被唤醒的次数,从而延长电池使用。如果要求Alarm任务执行时间必须准确无误,则使用 setExact()方法替代
//下面的set()方法。
manager.set(AlarmManager.ELAPSED_REALTIME_WEKEUP,triggerAtTime,pi);
return super.onStartCommand(intent,flags,startId);
}
}

这样,一旦启动了LongRuningService,就会设定一个定时任务,一个小时后会再次启动LongRuningService,形成一个永久循环。

Doze模式

在Android 6.0中,google加入了Doze模式,及大幅度延长电池使用寿命。主要表现为:如果设备未插电源,处于静止(7.0后删除这条件),并且屏幕关闭了一段时间之后,就会进入Doze模式,系统会对CPU、网络、Alarm等活动限制,当然,系统还会间歇性地退出Doze一小段时间,在这段时间,应用可以去完成他们的同步操作、Alarm任务等。如下图所示:

Doze模式示意图

可以看到,随着设备进入Doze模式的时间越长,间歇性退出Doze模式的时间间隔也会越来越长,因为如果设备长时间不使用的话,没必要频繁退出Doze。以下列出Doze模式下有具体哪些功能会收到限制:

  • 网络访问被禁止
  • 系统忽略唤醒CPU或者屏幕操作
  • 系统不再执行WIFI扫描
  • 系统不再执行同步服务
  • Alarm任务将会在下次退出Doze模式时候执行

不过,如果你真有非常特殊需求,要求Alarm任务在Doze模式也必须正常执行,可以调用AlarmManager的setAndAllowWhileIdle()或setExactAcnAllowWhileIdle()方法就能让定时任务即使在Doze模式下也能正常执行了,这两个方法之间的区别和set()、setExact()方法之间的区别一样。

*** 多窗口模式

在一个屏幕上,同时显示两个app界面。切换到多窗口模式,Activity会经历重新创建的过程。其实这是正常现象,进入多窗口模式后,Activity的大小发生了比较大的变化,此时默认会重新创建活动的。除此之外,像横竖屏也会重新创建活动。如果此时去操作另一个窗口,则当前窗口会执行onPause,而另一个窗口会执行onResume,这很好理解,因为两个窗口都是可见的,所以只会执行到onPause即可。因此,在考虑多窗口模式下,用户仍然可以看到处于暂停状态的应用,那么像视频播放器之类的应用应该在此时能够继续播放视频才对,因此最好不要在Activity的onPause()方法中去处理播放器的暂停逻辑,而应该在onStop()方法中处理,并且在onStart()方法中恢复视频播放。另外,针对进入多窗口时活动会被重新重新创建,如果想改变这一默认行为,可以在AndroidManifest.xml中进行配置:

1
2
3
<activity
android:name=".MainActivity"
android:configChanges="origintation|keyboardHidden|screenSize|screenLayout"

这样不管进入多窗口模式还是横竖屏切换,Activity都不会被重新创建,而是会将屏幕发生变化的事件通知到Activity的onConfigurationChanged()方法中,因此,如果有这方面的需求,只需要重写onConfigurationChanged()即可。

当然,如果想禁用多窗口模式,只需要在 AndroidManifest.xml 中的 或者 标签中加入如下属性即可

1
android:resizeableActivity=["true" | "false"]

需要注意的是,这个属性只有在项目的targetSdkVersion指定成24或者更高的时候才会有用。不过Android规定,如果targetSdkVersion小于24,并且Activity不允许横竖屏切换,那么应用也将不支持多窗口模式,如果不允许应用横竖屏切换,只需要在AndroidManifest.xml中添加如下配置:

1
android:screenOrientation=["portrait" | "landscape"]

服务非常适合执行那些不需要和用户交互还要求长期运行的任务。要注意的是,服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程,当应用程序被杀掉时,所有依赖该进程的服务也会停止。不要被服务的后台概念迷惑,实际上服务并不会自动开启子线程,所有的代码都是默认运行在主线程中。因此在使用服务时要注意主线程被阻塞的情况。

服务的基本用法

新建类MyService继承Service,要求重写其唯一一个抽象方法 onBind 。一般又要重写onCreate(服务创建时调用)、onStartCommand(每次服务启动的时候调用)以及onDestroy(服务销毁时调用)方法,它们是服务中最常用的3个方法。如果我们希望服务已启动就立刻执行某个动作,就可以将逻辑写在onStartCommand方法里。另外,还需要在AndroidManifest文件中做如下声明(四大组件都得声明):

1
2
3
4
5
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"
</service

其中export属性表示是否允许除了当前程序之外的其他程序访问这个服务,enable表示是否启用这个服务。

启动和停止服务

通过以下代码可以简单地实现启动和停止服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onClick(View v){
switch(v.getId()){
case R.id.start_service:
Intent startIntent = new Intent(this,MyService.class);
startService(startIntent);
break;
case R.id.stop_service:
Intent startIntent = new Intent(this,MyService.class);
stopService(startIntent);
break;
}
}

构建出Intent对象,使用startService即可启动MyService,会依次执行onCreate->onStartCommand;同理,使用stopService即可停止,onDestroy就会执行。如果在service里面,那么使用stopSelf即可停止自己。由于刚才点击start的时候,onCreate和onStartCommand都执行了,也许你会疑惑,这两个方法到底有什么区别呢?其实onCreate方法在服务第一次创建时调用,而onStartCommand则在每次启动服务时调用,由于是第一次创建,所以两个方法都执行了,如果多次点击start service按钮,那就只有onStartCommand方法执行了。

活动和服务通信

上一节虽然在活动中启动和停止了服务,但是启动服务之后,活动与服务基本上就没关系了,没法控制服务。如果希望控制服务执行,比如MyService提供下载功能,则希望可以决定什么时候开始下载,以及查看下载进度等,主要的思路是创建一个专门的Binder对象来对下载功能进行管理,然后通过ServiceConnection来实现通信。简易代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//service代码
public class MyService extends Service{
private DownloadBinder mBinder = new DownloadBinder();

class DownloadBinder extends Binder{
public void startDownload(){

}

public int getProgress(){
return 0;
}
}

@Override
public IBinder onBind(Intent intent){
return mBinder;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Activity代码
private ServiceConnection connection = new ServiceConnection(){
@Override
public void onServiceDisconnected(ComponentName name){//解除绑定时调用
}

@Override
public void onServiceConnected(ComponentName name,Ibinder service){//绑定时调用
downloadBinder = (MyService.DownloadBinder)service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
}
@Override
public void onClick(View v){
switch(v.getId()){
case R.id.bind_service:
Intent bindIntent = new Intent(this,MyService.class);
bindService(bindIntent,connection,BIND_AUTO_CREATE);//绑定服务
break;
case R.id.unbind_service:
unbindService(connection);//解绑服务
break;
}
}

服务生命周期

一旦在任何位置调用了Context的startService方法,相应的服务就会启动,如果服务还没创建过,onCrete先执行,在执行onStartCommand,如果已经创建了,则只会执行onStartCommand了,启动后该服务一直运行,直到调用stopService或者stopSelf为止。虽然多次启动服务onStartCommand会被多次执行,但是服务只会存在一个实例。
另外,还可以调用Context的bindService来获取一个服务的持久连接,这时会回调服务的onBind方法,类似地,如果之前没创建过该服务,就会先执行onCreate在执行onBind,之后,调用方可以获取到onBind方法里返回的IBinder对象实例,就能自由和服务进行通信。调用unbind方法,也会执行Myservice的onDestroy方法,即销毁服务。

服务的更多技巧

使用前台服务

由于服务运行在后台,因此其系统优先级比较低,当出现内存不足的情况时,容易被回收,这就考虑使用前台服务。前台服务和普通服务最大的区别在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉可以看到更详细的信息,非常类似于通知效果。其主要实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyService extends Service{
...

@Override
public void onCreate(){
super.onCreate();
Intent intent = new Intent(this,MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("title")
.***//其他配置省略了
.build();

//没有像普通Notification一样使用NotificationManager将通知展示出来,而是使用
//startForeground方法
**startForeground(1,notification);//与普通Notification和普通的Service区别的关键在这里**
}
}

使用IntentService

服务中的代码都是默认运行在主线程当中的,如果直接在服务里处理耗时操作,很容易出现ANR。当然,为了避免这种情况,你可以在onStartCommand方法中new 一个Thread来处理耗时逻辑,写法并不复杂,但是程序员容易忘记开启线程或者忘记调用stopSelf。为了简单地创建一个异步的、能够自动停止的服务,Android专门提供了IntentService,在继承Intentservice之后,你可以直接在它的 onHandleIntent方法中执行耗时操作(这是在新的线程里面,可以通过Thread.currentThread
().getId()方法查看线程ID,跟主线程不是同一个),在耗时操作执行完毕之后,这个IntentService会自动调用onDestroy停止。

服务的最佳实践——完整版下载示例

直接上代码吧,多说无益。用于执行异步任务的AsyncTask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//这里AsyncTask传入3个参数类型,第一个泛型指定为String,表示在执行AsyncTask时需要传入一个
//字符串给后台任务,第二个指定为Integer,表示使用整型数据作为进度显示单位,第三个泛型Intege则表示使用整数型
//数据来反馈执行结果
public class DownloadTask extends AsyncTask<String,Integer,Integer>{
public static final int TYPE_SUCCESS = 1;//成功
public static final int TYPE_FAILED = 2;//失败
public static final int TYPE_PAUSED = 3;//暂停
public static final int TYPE_CANCELED = 4;//取消

private DownloadListener downloadListener = null;
private boolean isPaused = false;
private boolean isCancled = false;
private int lastProgress;

public DownloadTask(DownloadListener listener){
downloadListener = listener;
}

@Override
protected Integer doInBackground(String... params) {//用于后台执行下载逻辑
InputStream is = null;
RandomAccessFile savedFile = null;
File file = null;
try{
long downloadedLength = 0;//记录已经下载的文件长度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
//SD卡的Download目录
String directory = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).getPath();
file = new File(directory + fileName);

//如果已经存在要下载的文件了,则读取已经下载的字节数,这样就可以在后面启动端点续传功能
if(file.exists()){
downloadedLength = file.length();
}
//获取文件总长
long contentLength = getContentLength(downloadUrl);
if(contentLength == 0){
return TYPE_FAILED;
}else if(contentLength == downloadedLength){
//已下载的字节和总字节相等,说明已经下载完成
return TYPE_SUCCESS;
}
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//断点下载,指定从哪个字节开始下载
.addHeader("RANGE","bytes=" + downloadedLength + "-")
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if(response != null){
//以下读取服务器响应的数据,并使用Java文件流方式,不断从网络读取数据,不断写到本地,知道
//文件全部下载完成为止
is = response.body().byteStream();
savedFile = new RandomAccessFile(file,"rw");
savedFile.seek(downloadedLength);//跳过已下载的字节
byte[] b = new byte[1024];
int total = 0;
int len;
while ((len = is.read(b)) != -1){
if(isCancled){
return TYPE_CANCELED;
}else if (isPaused){
return TYPE_PAUSED;
}else {
total += len;
savedFile.write(b,0,len);
//计算已下载的百分比
int progress = (int) ((total + downloadedLength) * 100/contentLength);
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if (is != null){
is.close();
}
if (savedFile != null){
savedFile.close();
}
if (isCancled && file != null){
file.delete();
}
}catch (Exception e){
e.printStackTrace();
}
}
return TYPE_FAILED;
}

@Override
protected void onProgressUpdate(Integer... values) {//用于在界面上更新当前的下载进度
int progress = values[0];
if (progress > lastProgress){
downloadListener.onProgress(progress);
lastProgress = progress;
Log.e("进度",progress+"");
}
}

@Override
protected void onPostExecute(Integer status) {//用于通知最终的下载结果
switch (status){
case TYPE_SUCCESS:
downloadListener.onSuccess();
break;
case TYPE_FAILED:
downloadListener.onFailed();
break;
case TYPE_PAUSED:
downloadListener.onPaused();
break;
case TYPE_CANCELED:
downloadListener.onCancled();
break;
default:

break;
}
}

public void pauseDownload(){
isPaused = true;
}

public void cancelDownload(){
isCancled = true;
}

//获取要下载的内容的大小
private long getContentLength(String downloadUrl) throws IOException{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.build();
Response response = client.newCall(request).execute();
if(response != null && response.isSuccessful()){
long contentLength = response.body().contentLength();
response.close();
return contentLength;
}
return 0;
}
}

用于执行后台任务的Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public class DownloadService extends Service{
private DownloadTask downloadTask;
private String downloadUrl;

private DownloadListener listener = new DownloadListener(){

@Override
public void onProgress(int progress) {
getNotificationManager().notify(1,getNotification("Downloading...",progress));
}

@Override
public void onSuccess() {
downloadTask = null;
//下载成功时将前台服务通知关闭,并创建一个下载成功的通知
//这句代码将服务创建成前台服务
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Success",-1));
Toast.makeText(DownloadService.this,"Download success",Toast.LENGTH_LONG).show();

}

@Override
public void onFailed() {
downloadTask = null;
//下载失败将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Failed",-1));
Toast.makeText(DownloadService.this,"Download failed",Toast.LENGTH_LONG).show();

}

@Override
public void onPaused() {
downloadTask = null;
Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_LONG).show();

}

@Override
public void onCancled() {
downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_LONG).show();
}
};

//为了让service可以和Activity通信,创了这个binder
class DownloadBinder extends Binder{
public void startDownload(String url){
if(downloadTask == null){
downloadUrl = url;
downloadTask = new DownloadTask(listener);
downloadTask.execute(downloadUrl);
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_LONG).show();
}
}

public void pauseDownload(){
if (downloadTask != null){
downloadTask.pauseDownload();
}
}

public void cancelDownload(){
if (downloadTask != null){
downloadTask.cancelDownload();
}else{
if (downloadUrl != null){
//取消下载时需要将文件删除,并将通知关闭
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).getPath();
File file = new File(directory + fileName);
if(file.exists()){//如果已经存在要下载的文件了,则读取已经下载的字节数,这样就可以在后面启动端点续传功能
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_LONG).show();
}
}
}
}

private DownloadBinder mBinder = new DownloadBinder();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}


private NotificationManager getNotificationManager(){
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}

private Notification getNotification(String title,int progress){
Intent intent = new Intent(this,MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);

NotificationCompat.Builder builder;

//TODO 注意这段代码,在8.0以后通知要求设置 NotificationChannel,否则会报错
//TODO 在书本中的代码没有这一段
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ){
NotificationChannel channel = new NotificationChannel("im_channel_id","System", NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if(manager != null){
**manager.createNotificationChannel(channel);
}
}

//书上是 builder = new NotificationCompat.Builder(this); ,但是这个方法现在已经废弃了
builder = new NotificationCompat.Builder(this,"im_channel_id");
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
if (progress > 0){
//当progress大于或者等于0才需显示下载进度
builder.setContentText(progress + "%");
builder.setProgress(100,progress,false);
}
return builder.build();
}
}

Activity中的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private DownloadService.DownloadBinder downloadBinder;
//为了能够控制service的行为
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownloadService.DownloadBinder) service;

}

@Override
public void onServiceDisconnected(ComponentName name) {

}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

findViewById(R.id.start).setOnClickListener(this);
findViewById(R.id.pause).setOnClickListener(this);
findViewById(R.id.cancel).setOnClickListener(this);

Intent intent = new Intent(this,DownloadService.class);
startService(intent);
bindService(intent,connection,BIND_AUTO_CREATE);

//6.0及以上要求动态申请权限,有权限才能使用这个下载功能
if(ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this, new String[]
{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);

}
}

@Override
public void onClick(View v) {
//首先判断downloadBinder是否有效
if (downloadBinder == null){
return;
}
switch (v.getId()){
case R.id.start:
String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
downloadBinder.startDownload(url);
break;
case R.id.pause:
downloadBinder.pauseDownload();
break;
case R.id.cancel:
downloadBinder.cancelDownload();
break;
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED){
Toast.makeText(MainActivity.this,"拒绝权限无法使用程序",Toast.LENGTH_SHORT).show();
finish();
}
break;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}

当然,还有要注意的是,要在AndroidManifest.xml中注册Service:

service android:name=”.DownloadService”

还有,在AndroidManifest.xml中需要标明网络权限和存储权限:

uses-permission android:name=”android.permission.INTERNET”
uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”

最后,针对本例子,例子中使用了okhttp,需要在buildgradle中添加依赖:

implementation ‘com.squareup.okhttp3:okhttp:3.9.0’

webview的用法

这节描述得比较简单,因此记住以下内容就行:

webview.setWebClient(new WebViewClient);

为webview设置webViewClient,其主要作用是当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前WebView中显示,而不是打开系统浏览器。

使用HTTP协议访问网络

介绍了HttpURLConnection 使用方法,简单上个图:

HttpURLConnection使用

OkHttp的简单使用,并不复杂,略。

解析XML格式数据

在网络上传输数据时最常用的格式有两种,XML和JSON,首先学习解析XML。

搭建简易服务器提供数据

学习解析xml和json之前,先搭建一个简易服务器提供解析的数据,按照以下步骤即可:

  1. 下载一个Apache服务器的安装包,官方下载地址http://httpd.apache.org/download.cgi

  2. 一路默认Next,域名随意填写如test.com,安装路径的话,可以选择安装在 C:\Apache 目录

  3. 为了验证安装是否成功,可以打开电脑的浏览器验证,输入 127.0.0.1 出现 It works 界面即可。

接下来进入到安装目录的htdocs目录下,按照上述安装过程应该是C:\Apache\htdocs目录,新建get_data.xml文件,编辑,并加入内容:

1
2
3
4
5
6
7
8
9
10
11
<apps>
<app>
<id>1</id>
<name>google map</name>
</app>

<app>
<id>1</id>
<name>google map</name>
</app>
</apps>

打开浏览器访问 http://127.0.0.1/get_data.xml就会显示上述内容,同理,如果在其中新建 get_data.json文件,并添加以下数据:

1
2
3
[{"id":"5","version":"5.5","name":"map"},
{"id":"6","version":"6.6","name":"boom"},
{"id":"7","version":"3.5","name":"clash"}]

打开浏览器访问 http://127.0.0.1/get_data.json 就会返回上述json。

Pull解析方式

解析XML格式的数据有多重方式,主要是Pull和SAX两种方式。通过自己搭建的服务器拉取XML数据之后通过Pull解析的示例如下:

pull解析方式代码

SAX解析

Pull解析方式虽然非常好用,它比XML解析方式要复杂一些,但是语义方面更清楚,通常情况下,我们都会新建一个雷继承自DefaultHandler,并重写父类的5个方法:

继承DefaultHandler

每当解析某个节点的时候,startElement方法就会得到调用,其中localName记录当前节点的名字。接下来的工作就非常简单了,修改MainActivity中的代码,如下所示:

SAX解析

解析JSON格式数据

JSON相对XML而言优势在于它体积更小,在网络上传输的时候可以更省流量,但缺点是语义性较差。

使用JSONObject

比较简单,直接上截图的例子:

JSONObject解析

使用GSON

解析单个的对象比较简单,比如解析:

{“name”:”Tom”,”age”:20}

那只需要定义Person类,有String类型的name字段以及int类型的age字段,则可以使用:

1
2
3
Gson gson = new Gson();
Person person = gson.fromJson(jsonData,Person.class);

解析数组稍微麻烦点:

1
List<Person> people = gson.fromJson(jsonData,new TypeToken<List<Person>>(){}.getType());

使用通知

我们可以在Activity中、BroadcastReceiver以及Service中创建通知,不论在哪里创建,整体步骤是相同的,下面通过示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//1、需要NotificationManager管理通知,通过调用Context的getSystemService方法获得
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

//2、创建一个延迟意图(PendingIntent),标明点击notification时的响应,这里可以启动Activity,Broadcast以及service等
//PendingIntent有点类似于Intent,不过前者倾向于在某个合适的时机去执行某个动作,而后者倾向于立即执行某个动作
Intent intent = new Intent(MainActivity.this,SecondActivity.class);
//根据启动的对象(Activity、Broadcast或service),可以使用getActivity()/getBroadcast()/getService()
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);


//3、通过Builder构造器创建Notification对象,几乎Android每个版本都会对通知这部分进行修改,因此我们需要使用
//support-v4包提供的NotificationCompat类来兼容性地实现,保证在各个版本上都能正常使用通知
Notification notification = new NotificationCompat.Builder(context)
.setContentTitle("title")
.setContentText("content")
.setSound(Uri.from(""))//控制通知的声音
//设置通知来的时候震动,数组中的值为时长,单位为毫秒,下标0表示手机静止时长,下标1为手机震动时长,下标2为手机静止
//时长,以此类推,这就实现了通知来时立刻震动1秒,静止1秒,再震动1秒
//注意震动需要权限 <uses-permission android:name="android.permission.VIBRATE">
.setVibrate(new long[]{0,1000,1000,1000})
.setWhen(System.currentMillis())//指定通知被创建的时间,下拉时这个时间会显示在通知上
.setSmallIcon(R.drawable.small_icon)//显示在顶部状态栏上的图标
.setLargeIcon(BitmapFactory.decodeResource(gerResources(),R.drawable.large_icon))//下拉时显示在通知左边
.setContentIntent(pi)//指明点击之后的意图
//通知自动消失,第二种取消方式是,将notification的id传入SecondActivity中,在进入到SecondActivity后,在SecondActivity
//的onCreate方法中重新获取manager,并且关闭这个通知:
//NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); manager.cancel(id);
.setLights(Color.GREEN,1000,1000)//设置灯光绿色和一闪一闪的效果
.setAutoCancel(true)
//设置style,一般通知只会显示很短的内容文字,但如果真的非常需要长文字,也是支持的,这样设置style,如果要显示一张大图片,
//以下换成NotificationCompat.BigTextStyle().bigPicture(bitmap)即可
.setStyle(new NotificationCompat.BigTextStyle().bigText("fdasfdsafdsafdafdasfsdafadsfdsfasdffasdfdsfdsfsda"))
//设置通知优先级,如果设置为最高的话,即要求用户立刻看,不会像普通通知只在状态栏显示一个图标,而是弹出一个横幅
//不论你当前在玩游戏还是看电影,这个横幅都会弹
.setPriority(NotificationCompat.PRIORITY_MAX)
.build();

//4、发出通知
manager.notify(1,notification);//第一个参数指定notification的id


调用摄像头和相册

平时使用QQ或者微信的时候经常要别人分享图片,这些图片可以使手机摄像头拍摄也可以从相册中选取,这种功能非常普遍。

摄像头拍照

直接上代码展示可能更加清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
takePhoto.setOnclickListener(new View.OnclickListener(){
@Override
public void onClick(View v){
//创建File对象,用于存储拍照后的图片
File outputImage = new File(getExternalCacheDir(),"output.jpg");
try{
if(outputImage.exists()){//存在了
outputImage.delete();
}
outputImage.createNewFile();
}catch(IOException e){

}

if(Build.VERSION.SDK_INT >= 24){
imageUri = FileProvider.getUriForFile(MainActivity.this,
"com.example.fileprovider",outputImage);

}else{
imageUri = Uri.fromFile(outputImage);
}

//启动相机
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);
startActivityForResult(intent,TAKE_PHOTO);
}
});


@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
switch(requestCode){
case TAKE_PHOTO:
if(resultCode == RESULT_OK){
try{
//将拍摄的照片显示出来
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver()
.openInputStream(imageUri));
ivPic.setImageBitmap(bitmap);
}catch (FileNotFoundException e){

}
}

break;
}
}

上面的代码中我们用了内容提供器,因此还需要在AndroidManifest.xml中声明这个提供器(有一点要注意的是,在4.4以前(4.4及以后不需要)访问SD卡得应用关联目录也是要声明权限的,为了兼容老版本的手机,需要声明 WRITE_EXTERNAL_STORAGE 权限):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<users-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:icon="@mipmap/ic_launcher"
...
android:theme="@style/AppTheme">

<privider
<!--这里,android:name属性的值是固定的,android:authorities属性的值必须要和刚才FileProvider.getUriForFile()-->
<!--方法中的第二个参数一致,另外,meta-data中用resource指定了Uri的共享路径-->
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>

</application>

当然,provider声明中使用了@xml/file_paths资源,这个资源我们还没创建,因此在res目录下可以创建这么个xml,内容如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8">
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--这里面external-path指定Uri共享的,name属性随便填,path属性表示共享的具体路径,这里设置空值就表示将整个SD卡进行共享-->
<!--当然,你可以仅仅共享我们存放output.jpg这张图片的路径-->
<external-path name="my_images" path="">
</paths>

以上整个代码首先创建了一个File对象,用于存放摄像头拍下的图片,我们将其命名为output.jpg,并将它存放在手机SD卡的应用关联缓存目录(指SD卡中专门用于存放当前应用缓存数据的位置,路径为/sdcard/Android/data//cache,调用getExternalCacheDir()方法就可以得到这个目录)下。为什么使用应用关联缓存目录来存放图片呢?因为从Android 6.0开始,读写SD卡被列为危险权限,如果将图片存放SD卡得任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步

接着会判断如果设备版本低于7.0,就调用Uri.fromFile()方法将File对象转换为Uri对象,这个Uri标识着图片的本地真实路径。否则就调用FileProvider的getUriForFile()方法获得Uri对象。之所以这样是因为从7.0开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出异常,而FileProvider则是一种特殊的内容提供器,可以选择性地将封装过的Uri共享给外部,提高应用安全性。

最后就是启动摄像头拍照并且回调获取图片了。

从相册中选择照片

直接选取一张现有图片比打开相机拍一张照片更加常用,一个优秀的应用应该将这两种方式都提供给用户。废话不多说直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
chooseFromAlbum.setOnclickListener(new View.OnclickListener(){
@Override
public void onClick(View v){
if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){

ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}else{
openAlbum();
}

}
});


private void openAlbum(){
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent,CHOOSE_PHOTO);//打开相册
}


@Override
public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults){
swithc(requestCode){
case 1:
if(grantResults.lenght > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
openAlbum();
}else{
Toast.makeText(this,"you denied the permission",Toast.LENGTH_SHORT).show();
}

break;
}
}


@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
switch(requestCode){
case CHOOSE_PHOTO:
if(resultCode == RESULT_OK){
if(Build.VERSION.SDK_INT >= 19){//4.4及以上
handleImageOnKitKat(data);
}else{//4.4以下
handleImageBeforeKitKat(data);
}

}

break;
}
}

//4.4及以上处理方式
@TargetApi(19)
private void handleImageOnKitKat(Intent data){
String imagePath = null;
Uri uri = data.getData();
//如果是document类型Uri,则通过document id处理
if(DocumentsContract.isDocumentUri(this,uri)){
String docId = DocumentsContract.getDocumentId(uri);
if("com.android.providers.media.documents".equals(uri.getAuthority()){
//解析出数字格式的id
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePaht(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,selection);
}else if("com.android.providers.downloads.documents".equals(uri.getAuthority())){
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),Long.valueOf(docId));
imagePath = getImagePath(contentUri,null);
}

}else if("content".equalsIgnoreCase(uri.getScheme())){//如果是content类型的uri,则使用普通方式处理
imagePath = getImagePath(uri,null);
}else if("file".eualsIgnoreCase(uri.getScheme())){//如果是file类型的uri,直接获取推按路径即可
imagePath = uri.getPath();
}

//根据路径显示图片
displayImage(imagePath);
}

//4.4以前处理方式
private void handleImageBeforeKitKat(Intent data){
Uri uri = data.getData();
//因为他的Uri没有封装过的,不需要任何解析直接去获取真实路径即可
String imagePath = getImagePath(uri,null);
displayImage(imagePath);
}


//通过Uri和selection来获取真实的图片路径
private String getImagePath(Uri uri,String selection){
String path = null;
Cursor cursor = getContentResolver().query(uri,null,selection,null,null);
if(cursor != null){
if(cursor.moveToFirst()){
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}

//根据路径显示图片
private void displayImage(String imagePath){
if(imagePath != null){
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
ivPicture.setImageBitmap(bitmap);
}else{
Toast.makeText(this,"failed to get image",Toast.LENGTH_SHORT).show();
}
}

因为照片是存在SD卡上的,所以我们首先进行权限处理,WRITE_EXTERNAL_STORAGE表示授予了对SD卡的读和写的能力。在onActivityResult回调中针对不同版本使用不同方式处理图片,因为从4.4开始,选取相册中的图片不再返回真实的Uri了,而是一个封装过的Uri,因此必须对这个Uri解析才行,在handleImageOnKitKat()方法中,如果返回的Uri是document类型的话,就取出document id进行处理,如果Uri的authority是media格式的话,document id还需要进行一次解析,要通过字符串分割的方式取出后半部分才能得到真正的数字id。

播放多媒体文件

播放音频和视频比较简单,没有兼容性等复杂问题,仅仅只需要记住:

  1. 申请 WRITE_EXTENAL_STORAGE 权限

  2. 使用 MediaPlayer 播放音频结束时,在 onDestroy方法中要进行 MediaPlayer.stop() 和 MediaPlayer.release() ,将资源释放掉;

  3. 使用 VideoView 播放视频结束时,在 onDestroy方法中要进行 VideoView.suspend() ,将资源释放掉;

其他内容略过。

可能你会有些疑惑,为什么要将我们程序中的数据共享给其他程序呢?当然,这是视情况而定的,比如账号密码之类的隐私数据显然是不能共享给其他程序的,不过一些可以让其他程序进行二次开发的基础性数据,我们还是可以选择共享。例如联系人程序、短信程序、多媒体库等,它们的数据库中保存了很多基础数据,如果不允许其他应用进行访问,则方便性就会大打折扣。

运行时权限

Android的权限机制在一开始就存在,但是在6.0以前保护隐私方面比较有限,因为像微信这种大家都离不开的软件,容易“店大欺客”,不同意它所有的权限只能不安装,这并不合理。

权限机制详解

开发者在AndroidManifest.xml中声明权限,一种情况是,用户如果在低于6.0的系统上安装该程序,会在安装时列出该应用所需要的权限,从而决定是否要安装这个程序,并且在用户安装成功之后,还能在设置中查看程序所申请的权限,但是对于那些离不开的程序(比如微信)来说,要么全部同意它申请的权限,要么不安装,这不太合理;如果在6.0及以上的系统中安装,则用户不必在安装时一次性授权所有申请的权限,而是在软件使用的过程中再对危险权限进行授权,就算拒绝了这个权限,仍然可以使用应用的其他功能,而不是以前那样直接无法安装。

Android 6.0 及以上将所有权限分为两类,普通权限和危险权限,普通权限是指不会直接威胁用户的安全和隐私的权限,这部分权限系统自动帮我们授权,避免用户不停地手动授权;危险权限则表示会触及用户隐私或者设备安全性的权限,如获取联系人、定位设备位置等,必须由程序员动态申请,由用户手动点击授权才可以,否则无法使用相应功能。目前为止,Android中的危险权限有9组共24个权限,如下列表所示(图片来自官网):

危险权限列表

这张表格无需记住,在使用的时候作为参照,如果权限在这张表中,则进行运行时处理就好。另外注意一下,表格中每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名,但是用户一旦同意授权了,那么该权限对应的权限组中所有的其他权限也会同时被授权

在程序运行时申请权限

以拨打电话的权限为例来说明权限的申请,点击一个按钮,就拨打指定的号码,在6.0以前可能是这样实现的:

  1. 在AndroidManifest.xml中申请权限:
    1
    <uses-permission android:name="android.permission.CALL_PHONE"/>
  2. 在代码中实现:
1
2
3
4
5
6
7
8
btnCall.setOnclickListener(new View.OnclickListener(){
@Override
public void onClick(View v){
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(Intent);
}
});

在6.0以下系统上能正常拨打电话,但是在6.0或者以上系统运行,会报错Permission Denial,可以看出是由于权限被禁止导致的,因此我们应该尝试使用以下方式来申请权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
btnCall.setOnclickListener(new View.OnclickListener(){
@Override
public void onClick(View v){

if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.CALL_PHONE) !=
PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermission(MainActivity.this,new
String[]{Manifest.permission.CALL_PHONE},1);
}else{
call();
}
}
});

private void call(){
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(Intent);
}

@Override
public void onRequestPermissionResult(int requestCode,String[] permissions,int[] grantResults){

switch(requestCode){
case 1:
if(grantResults.lenght > 0 && grantResults(0) == PackageManager.PERMISSION_GRANTED){
call();
}else{
Toast.makeText(this,"You denied the permission",Toast.LEGHTH_SHORT).show();
}

break;

}

}

上述第一步先判断用户是不是已经给我们授权了,使用的是ContextCompat.checkSelfPermission,如果已经授权,直接拨打电话,否则调用ActivityCompat.requestPermission方法向用户申请授权,这时候用户可以选择同意或者拒绝我们的申请,不论哪种结果,都会通过回调onRequestPermissionResult告知,在回调中根据不同的结果做不同的处理。记住,在动态声明权限后,AndroidManifest中还得添加 声明

访问其他程序中的数据

内容提供器的用法一般有两种,一是使用现有的内容提供器来读取和操作响应程序中的数据,另一种是创建自己的内容提供器给我们的数据提供外部访问接口。

ContentResolver的基本使用

如果想要访问内容提供器共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver方法获取到该类的实例。可以对内容进行CRUD操作,不同于SQLiteDatabase,ContentResolver增删改查不接收表名参数,而是使用Uri参数代替,该Uri主要由两部分组成:authority和path,前者用于对不同的应用程序做区分,一般采用程序包名形式,如某个程序的包名是com.example.app,那么对应的authority就可以命名为com.example.app.provider;path则是对同一应用程序中不同表做区分的,通常会添加到authority后面,所以内容Uri的形式一般如下所示(带协议声明):

content://com.example.app.provicer/table1
content://com.example.app.provicer/table2

正式查询的时候,将Uri作为参数传入,代码如下:

1
2
3
Uri uri = Uri.parse("content://com.example.app.provicer/table1");
Cuisor cursor = getContentResolver().query(uri,projection,selection,selectionArgs,sortOrder);

其中,query方法中各个参数对应的含义如下所示:

参数对应的含义

接下来便可以进行相应的增删改查操作,代码如下:

1
2
3
4
5
6
7
//查
if(cursor != null){
while(cursor.moveToNext()){
String colomn1 = cursor.getString(cursor.getColumnIndex("column1"));
int colomn2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
}
1
2
3
4
5
//增
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
1
2
3
4
//改,把column1的值清空
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ?",new String[]{"text","1"});
1
2
//删除
getContentResolver().delete(uri,"column2 = ?",new String[]{"1"});

其实整体就相当于sql语句,因此并不太难。

创建自己的内容提供器

因为基本上没有这样的需求,暂时略后续补上

Android系统主要提供了3种方式用于简单地实现数据持久化功能——文件存储、SharedPreference存储以及数据库存储。

文件存储

文件存储是Android中最基本的存储方式,它不对存储内容进行任何的格式化处理,因而比较适合用于存储一些简单的文本数据或者二进制数据

将数据存储到文件

Context类提供了一个openFileOutput()方法,可以用于将数据存储到指定文件,需要两个参数,第一个参数是文件名,纯粹的名称,不可以包含路径,因为所有的文件都是默认存储到/data/data//files/目录下;还有个参数是操作模式,主要有两种(其他2种在4.2被废弃了):

  • MODE_PRIVATE:默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原来文件中的内容。
  • MODE_APPEND:表示如果该文件已经存在,就往文件里面追加内容,不存在就创建新文件。

保存文件的一般如以下代码操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void save(){
String dataStr = "data to save";
FileOutputStream out = null;
BufferedWriter writer = null;
try{
//文件名是data
out = openFileOutput("data",Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(dataStr);
}catch(IOException e){
e.printStackTrace();
}finnaly{
try{
if(writer != null){
writer.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}

存储成功后,可以通过Android Device Monitor 进入File Explorer标签,在目录中/data/data//files/中就能找到 data 文件。同理,读取存到文件中的代码应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public String load(){
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try{
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while((line = reader.readLine()) != null){
content.append(line);
}

}catch(IOException e){
e.printStackTrace();
}finnaly{
try{
if(reader != null){
reader.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
return content.toString();
}

SharedPreference

SharedPreference是使用键值对的方式来存储数据的,保存一条数据的时候,需要给这条数据提供一个对应的键,读取数据时通过这个键把对应的值读取出来,SharedPreference文件都是存放在/data/data//shared_prefs目录下。要想存储数据,首先要获取到SharedPreference对象,Android主要提供了3中方式:

  • Context类中的getSharedPreference()方法:此方法接收两个参数,第一个用于指定文件名称,第二个用于指定操作模式,目前只有MODE_PRIVATE可选(其他的几种在4.2或者6.0版本被废弃了),并且是默认的操作模式,表示只有当前应用程序才可以对这个文件进行读写。

  • Activity中的getPreferences()方法:和Context类中的getSharedPreference()方法类似,只不过它只接受一个操作模式参数,因为使用这个方法时会自动将当前Activity的类名作为SharedPreference的文件名。

  • PreferenceManager类中的getDefaultSharedPreferences()方法:它接受一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreference文件。

获取到SharedPreference对象之后,就可以开始存储数据了,主要分为3步实现:

  1. 调用SharedPreference对象的edit()方法获取SharedPreference.Editor对象
  2. 向SharedPreference.Editor对象添加数据。
  3. 调用apply()方法提交,从而完成存储操作。

代码形式应该是这样的:

1
2
3
4
5
6
7
SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
editor.putString("name","Tom");
editor.apply();

//存储完成后,读取数据
SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
String name = pref.getString("name","");

SQLite数据库存储

文件存储和SharedPrefrences存储只适用于保存一些简单的数据和键值对,要存储大量复杂的关系型数据的时候,有点难以应付了。

创建数据库

Android为了让我们更方便地管理数据库,专门提供了一个SQLiteOpenHelper抽象类,要想使用的话,我们就需要创建一个自己的类去继承它,它有两个抽象方法,onCreate和onUpgrade用来创建和升级数据库,其它两个重要的实例方法:getReadableDatabase和getWritableDatabase,他们都可以创建或者打开一个现有的数据库(没有就创建),在数据库不可写入的时候(如磁盘满了),前者以只读的形式打开数据库,后者会出现异常。它有两个构造方法可重写,一般使用哪个参数较少的即可,总共4个参数,第一个context,第二个是数据库名,第三个是自定义的Cursor,一般传null,第四个表示当前的数据库版本号,用于对数据库进行升级操作。一般代码如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyDatabaseHelper extends SQLiteOpenHelper{
public static final CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement,"
+ "author text,"
+ "price real,"
+ "pages integer,"
+"name text)";

@Override
public void onCreate(SQLiteDatabase db){
db.exeSQL(CREATE_BOOK);
}
}

使用的时候应该是这样子的:

1
2
3
dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,1);
//就会创建成功了
dbHelper.getWritableDatabase();

上例创建了一个Book表,使用primary key 将id设置为主键,并用autoincrement关键字表示id是自增长的。可以使用

adb shell

命令,之后cd到/data/data//databases/目录下用ls列出该目录的文件,可以看到BookStore.db文件,以及BookStore.db-journal文件,后者是数据库的临时文件。SQLite没有其他数据库一样有很多繁杂的数据类型,它的数据类型很简单:integer表示整型,real表示浮点型,text表示文本,blob表示二进制类型

升级数据库

此时项目中有一张Book表用于存放输的各种详细数据了,但是如果再想添加一张Category表用于记录图书的分类,如果仅仅直接在MyDatabaseHelper的onCreate中写成:

1
2
3
4
5
@Override
public void onCreate(SQLiteDatabase db){
db.exeSQL(CREATE_BOOK);
db.exeSQL(CREATE_CATEGORY);
}

是行不通的,因为使用的时候先初始化helper:dbHelper = new MyDatabaseHelper(this,”BookStore.db”,null,1)再获取数据库:dbHelper.getWritableDatabase(),而由于此时已经存在数据库BookStore.db了,因此不会再执行helper的onCreate方法了。此时清除app数据可以做到创建Category表,但是这在实际应用中不合理,而我们可以用onUpgrade方法来解决,我们前面构造了MyDatabaseHelper,第4个参数是版本号,我们目前是1,所以只要传入的值大于当前版本号1,onUpgrade方法就可以执行,因此我们可以这样增加Category表:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyDatabaseHelper extends SQLiteOpenHelper{

...

@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
db.exeSQL("drop table if exists Book");
db.exeSQL("drop table if exists Category");
onCreate(db);
}

}

上述代码执行了两条drop语句,发现数据库已经存在Book表和Category表了就删除,然后调用onCreate方法重新创建,因此在onCreate中也得写成:

1
2
3
4
5
@Override
public void onCreate(SQLiteDatabase db){
db.exeSQL(CREATE_BOOK);
db.exeSQL(CREATE_CATEGORY);
}

在使用的时候也得升级版本号:

1
2
3
dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
//就会创建成功了
dbHelper.getWritableDatabase();

获取到数据库,接下来可以对其CRUD操作,其中C代表添加(Create),R代表查询(retrieve),U代表更新(Update),D代表删除(Delete)。Android开发者水平参差不齐,并非每一个都会SQL语言,Android提供了一系列的辅助性方法,是的在Android中即使不去编写SQL语句,也能轻松完成所有CRUD操作。getReadableDatabase与getWriteableDatabase方法不仅可以用来创建和升级数据库,他们还会返回一个SQLiteDatabase对象,借助这个对象就可以轻松CRUD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**添加数据**/
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name","thinking in java");
values.put("price",16.96);
values.put("pages",512);
//插入时指定表名为"Book"
db.insert("Book",null,values);

/**以下是更新**/
values.clear();
values.put("price",20);
//第三个参数对应SQL语句中的where部分,表示更新所有name等于?的行,而?是一个占位符,
//可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应内容
db.update("Book",values,"name=?",new String[]{"thinking in java"});

/**以下是删除**/
//表示删除pages的值大于500的数据
db.delete("Book","pages > ?",new String[]{"500"});

/**以下是查询**/
Cursor cusor = db.query("Book",null,null,null,null,null,null);
if(cursor.moveToFirst()){
do{
String name = cursor.getString(cursor.getColumnIndex("name"));
String pages = cursor.Double(cursor.getColumnIndex("price"));
}while(cursor.moveToNext());
}
cusor.close();

当然,可以直接使用SQL语句直接完成上述操作:

1
2
3
4
5
6
7
8
9
10
11
//添加
db.execSQL("insert into Book (name,pages,price) values(?,?,?)",new String[]{"thinking in java","512","20"});

//升级
db.execSQL("update Book set price = ? where name = ",new String[]{"20","thinking in java"});

//删除
db.execSQL("delete from Book where pages > ?",new String[]{"500"});

//查询
db.execSQL("select * from Book",null);

使用LitePal

广播机制介绍

Android中广播分为标准广播有序广播,标准广播是一种完全异步执行的广播,广播发出后,所有广播接收器机会会在同一时刻接收到广播,但同时意味着它也是无法被截断的。有序广播是一种同步执行的广播,同一时刻只有一个广播接收器能收到这条消息,当这个广播接收器的逻辑执行完毕之后才会继续传递,优先级高的广播接收器可以先收到广播,并且还可以阶段正在传递的广播,这样后面的广播接收器就收不到这条广播消息。

动态注册和静态注册广播

动态注册一般在Activity的onCreate方法中写上类似于:

1
2
3
4
IntentFilter filter = new IntentFilter();
filter.addAction("com.example.MyReceiver");
receiver = new MyReceiver();
registerReceiver(receiver,filter);

并且在onDestroy方法中注销广播:

1
> unregisterReceiver(receiver);

然后,完善一般是内部类的MyReceiver:

1
2
3
4
5
6
class MyReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context,Intent intent){
Toast.makeText(context,"receive the broadcast",Toast.LENGTH_SHORT).show();
}
}

最后得在适当的时候发送广播:

1
2
Intent intent = new Intent("com.example.MyReceiver");
sendBroadcast(intent);

当然,如果你是用广播在APP中实现强制退出登录(如QQ账号在另一台设备上登录了),那你只需要在当前Activity上弹一个窗提示已经被强制下线即可,因此有必要将广播在BaseActivity中注册,并且在BaseActivity的onPause方法(注意不是onDestroy方法了,因为我们只需要栈顶的Activity能够响应就行)中注销广播即可。如果是接收系统级广播,可能还得在AndroidManife.xml中声明相关权限。APP中实现强制退出登录时的广播接收器可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ForceOfflineReceiver extends BroadcastReceiver{
@Override
public void onReceive(final Context context,Intent intent){
AltertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("warning");
builder.setMessage("force offline");
builder.setCancelable(false);
builder.setPositiveButton("ok",new DialogInterface.OnclickListener(){
@Override
public void onclick(DialogInterface dialog,int which){
ActivityCollector.finishAll();//销毁所有活动
Intent intent = new Intent(context,LoginActivity.class);
context.startActivity(intent);

}
});


}
}

静态注册广播是在AndroidManife.xml中做如下的声明,其中MyReceiver类一般不是内部类,因为即使app未启动也能接收广播

1
2
3
4
5
6
7
8
9
10
11
12
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/appname">
...
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.MyReceiver"
</receiver>
</application>

如果要发送有序广播,只需要将以上发送广播的代码sendBroadcast(intent)替换成sendOrderedBroadcast(intent,null);即可。设置广播的优先级只需要设置intentFilter的priority属性即可(AndroidManifest文件中是intent-filter属性)。

使用本地广播

前面发送的广播属于系统全局广播,发出的广播可以被任何应用接收到,并且我们也可以接受来自其它任何应用发出的广播,这容易引起安全性问题,比如关键数据广播被其他应用截获,或者其他应用发送各种垃圾广播。本地广播的发送有些不同:

1
2
3
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
Intent intent = new Intent("com.example.MyReceiver");
manager.sendBroadcast(intent);

注册:

1
2
3
4
IntentFilter filter = new IntentFilter();
filter.addAction("com.example.MyReceiver");
localReceiver = new LocalReceiver();
manager.registerReceiver(localReceiver,filter);

同样注销广播:

1
manager.unregisterReceiver(localReceiver);

本地广播的几点优势:

  • 可以明确地知道正在发送的广播不会离开我们的程序,因此不必担心机密数据泄露。
  • 其他的程序无法将广播发送到我们程序内部,因此不用担心会有安全漏洞隐患。
  • 发送本地广播比发送系统全局广播更加高效。

另有一点需要说明:本地广播是无法通过静态注册方式来接收的,其实这也完全可以理解,因为静态注册主要就是为了让程序在未启动的情况下也能接收广播,而发送本地广播时,我们的程序肯定是已经启动了;此外,不要再onReceive方法中添加过多的逻辑或者进行任何耗时的操作,因为广播接收器中是不允许开启线程的,当onReceive方法运行了较长时间而没有结束时,程序就会报错。因此它更多的只是扮演一种打开程序其他组件的角色,如弹一条通知,或者启动一个服务等。