Activity:是Android与用户进行交互的接口,一个跟用户交互的组件,它可以是布满整块屏幕,也可以悬浮与其它界面之上,它提供了一个界面供用户点击,滑动等操作,这就是Activity的意义
Android是通过栈的方式来管理Activity的,一个Activity是什么状态决定了它在栈中处于什么位置,你能看到的页面即处于前端的Activity,它总是位于栈顶,当它销毁后,处于第二层的Activity会被推到栈顶;如果有新的Activity进入到栈顶,那原Activity就会被压入到栈的第二层。一个Activity在栈中位置的变化反应了它不同状态的转换
onCreate:顾名思义,这是创建的意思,也就是创建一个activity,这是第一步,并且一个activity在一个完整的生命周期里只会回调一次onCreate。在这里主要进行一些初始化的操作,比如setcontentview加载布局,进行控件的初始化,绑定按钮事件,此时activity还在后台,不可见。切记不能在这里进行复杂耗时操作,要不然进去改activity可能会黑屏一些时间,然后才显示出来。
onStart:见名知意,这表示启动,这是回调的第二个方法,此时activity已经可见但未获取焦点,用户还不能与其交互,在一个完整的生命周期里最少会回调一次onStart。
onResume:当回调到这个方法,activity获得焦点并与我们眼对眼见面了,可以开始各种交互了。此时activity处于栈顶,在一个完整的生命周期里最少会回调一次onResume。
onPause:此时activity还可见但失去焦点,无法进行交互,在这里也不要做耗时操作,因为回调这个方法完才会跳转到其它activity,so。。。,而且android中指定如果onPause在500ms内没有执行完毕的话就会强制关闭Activity。
onStop:表示停止,此时activity已经不可见了,但是activity还在内存中,没有被回收,但是内存如果紧张,有可能被回收
onDestroy:到这activity就从内存销毁了,在这可以进行一些资源的释放
onRestart():表明activity正在重新启动,是不可见状态回到可见状态时候被调用
没有在AndroidManifest.xml里给Activity配置android:configChanges属性时(如果配置了但是没有配置screenSize属性也是下面一样情况)
竖屏界面A:onCreate -> onStart -> onResume
横屏时竖屏界面A:onPause -> onSaveInstanceState-> onStop -> onDestroy,再执行横屏界面B onCreate -> onStart -> onRestoreInstanceState - > onResume
再竖屏时横屏界面B:onPause -> onSaveInstanceState-> onStop -> onDestroy 再执行竖屏界面A onCreate -> onStart -> onRestoreInstanceState -> onResume。
设置Activity的android:configChanges=”orientation|keyboardHidden|screenSize”时
竖屏界面A:onCreate -> onStart -> onResume
横屏时回调onConfigurationChanged方法,竖屏时回调onConfigurationChanged方法,不会再执行生命周期函数
如果设置了android:screenOrientation="portrait"属性,portrait表示竖屏,landscape表示横屏,那屏幕就不会旋转了,也就不会有生命周期变化了,也 不会回调onConfigurationChanged方法。
这里其实我用的是5.0版本的手机试的,可能低版本会有不一样,当orientation和screenSize只配一个的话,情况是跟第一种一样的,只有两个都配置,才是第 二种情况,keyboardHidden是为了让输入键盘隐藏,最好这三个属性都配置,以适应大多数版本,无奈,版本更新差别太大了。以后有不同机型测试后再来补 充
前台 / 可见 / 服务 / 后台 / 空
前台进程 Active Process:一般是前台正在交互的activity、与前台activity绑定的service、正在执行onReceive事件处理的函数的BroadCast Receiver,这几种情况的进程就是可见进程,这些是android通过回收资源尽力保护的进程
可见进程 Visible Process:比如一个activity处于可见但并不是处于前台或者不响应用户事件,处于暂停(OnPause)状态;还有一种就是被这种Activity绑定的Service,这种就是可见进程;这些情况一般发生在当一个activity被部分遮盖的时候(被一个非全屏或者透明的Activity)。可见进程只在极端的情况下,才会被杀死来保护前台进程的运行。
服务进程 Service Process:包含已经启动的service,service以动态的方式持续运行但没有可见的界面。因为Service不直接和用户交互,它们拥有比Visible Process较低的优先级
后台进程 Background Process:进程中的Activity不可见或进程中没有任何启动的service,这些进程都可以是后台进程;比如按home键,activity的前台进程就变成了后台进程;在系统中,拥有大量的后台进程,并且Android会按照后看见先杀掉的原则来杀掉后台进程以获取系统资源给前台进程
空进程 Empty Process:为了改善整个系统的性能,android经常在内存中保留那些已经走完生命周期的应用程序。android维护这些缓存来改善应用程序重新启动的时间,这些进程在资源需要的时候会被杀掉
任务是一个activity的集合,它用栈的方式来管理activity;这个栈被称为返回栈(back stack),栈里的activity顺序是按打开顺序放置。
当用户在home界面点击应用图标时候,这个应用的任务就会被转移到前台,如果这个应用的任务是空的,说明最近这个应用没有被启动过,系统就会去创建一个新的任务,将该应用的主activity放入到返回栈中。
当一个activity启动了一个新activity的时候,新的activity会被放置到返回栈的栈顶并获取焦点;前一个activity仍然保留在任务栈,但处于停止状态。
当用户按下返回键的时候,处于栈顶的activity会被移除掉,前一个activity就会重新回到栈顶的位置。我们只能向栈顶添加activity或者将栈顶的activity移除掉。这说明返回栈是一个典型的后进先出的数据结构。
如果一直按返回键,返回栈中的activity会一个一个的被移除,最终返回到主屏幕,这时候返回栈中activity全被清空,对应的任务也就不存在了。
当打开一个应用,对应的任务处于前台;这时候点击home键回到主屏幕,任务就被转移到后台;当任务处于后台状态的时候,返回栈中的activity都进入停止状态,但在返回栈中的顺序不会变,每个activity的信息和数据都在;当处于内存不足的情况下有可能会被销毁回收
如果你的应用程序有多个入口可以启动同一个activity,默认情况下每次启动都会创建一个该activity的新的实例,并不是将下面的activity移动到栈顶;带来的一个问题就是同一个activity在一个返回栈里存在多个实例;势必会引起一些问题。
Android系统管理任务和返回栈的方式就是把所有的activity都放入到一个相同的任务当中,通过一个后进先出的栈来管理;开发者可以通过修改activity启动模式来修改这一行为。
获取栈顶应用
public class AppUtils {
private static String PACKAGE_NAME_UNKNOWN = "unknown";
/**
* 获取栈顶应用包名
* 要做版本兼容
* 在Android5.0以前,系统允许我们通过ActivityManager的getRunningTasks()函数,直接获取当前运行的App
* 从Android5.0开始,系统为了安全起见,开始重重设限,要求我们通过用户手动授权的方式,获得USAGE_STATS_SERVICE的访问权限,才能读到数据
* 但是,这种方式有两个问题:
* 要求用户手动授权,不能自动运行
* 在碎片化系统中表现不一,有的系统无法给出权限,例如某些小米手机就无法真正获取权限
* @param context
* @return
*/
public static String getTopActivityPackageName(@NonNull Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
ActivityManager am = ((ActivityManager) context.getSystemService(ACTIVITY_SERVICE));
List taskInfo = am.getRunningTasks(5);
PACKAGE_NAME_UNKNOWN = taskInfo.get(0).topActivity.getPackageName();
return PACKAGE_NAME_UNKNOWN;
}
final UsageStatsManager usageStatsManager = (UsageStatsManager)context.getSystemService(Context.USAGE_STATS_SERVICE);
if(usageStatsManager == null) {
return PACKAGE_NAME_UNKNOWN;
}
String topActivityPackageName = PACKAGE_NAME_UNKNOWN;
long time = System.currentTimeMillis();
// 查询最后十秒钟使用应用统计数据
List usageStatsList = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000*10, time);
// 以最后使用时间为标准进行排序
if(usageStatsList != null) {
SortedMap sortedMap = new TreeMap();
for (UsageStats usageStats : usageStatsList) {
sortedMap.put(usageStats.getLastTimeUsed(),usageStats);
}
if(sortedMap.size() != 0) {
topActivityPackageName = sortedMap.get(sortedMap.lastKey()).getPackageName();
Log.e(TAG,"Top activity package name = " + topActivityPackageName);
}
}
return topActivityPackageName;
}
/**
* 获取栈顶应用前需要检查权限
* @param context
*/
public static void checkUsageStateAccessPermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if(!checkAppUsagePermission(context)) {
requestAppUsagePermission(context);
}
}
}
public static boolean checkAppUsagePermission(Context context) {
UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
if(usageStatsManager == null) {
return false;
}
long currentTime = System.currentTimeMillis();
// try to get app usage state in last 1 min
List stats = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, currentTime - 60 * 1000, currentTime);
if (stats.size() == 0) {
return false;
}
return true;
}
/**
* 跳转到设置页面让用户同意
* 同时要配置权限
*
* @param context
*/
public static void requestAppUsagePermission(Context context) {
Intent intent = new Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(TAG,"Start usage access settings activity fail!");
}
}
}
在Activity启动中涉及到Intent这个API,Intent的中文意思是“意图,意向”,在Android中提供了Intent机制来协助应用间的交互与通讯,Intent负责对应用中一次操作的动作、动作涉及数据、附加数据进行描述,Android则根据此Intent的描述,负责找到对应的组件,将 Intent传递给调用的组件,并完成组件的调用。Intent不仅可用于应用程序之间,也可用于应用程序内部的Activity/Service之间的交互。因此,可以将Intent理解为不同组件之间通信的“媒介”,专门提供组件互相调用的相关信息
Intent有七大属性:
component(组件):目的组件
action(动作):用来表现意图的行动
category(类别):用来表现动作的类别
data(数据):表示与动作要操纵的数据
type(数据类型):对于data范例的描写
extras(扩展信息):扩展信息
Flags(标志位):期望这个意图的运行模式
这应该用的是最多的一个启动方式了,先在AndroidManifest.xml里配置activity,然后进行如下操作:
Intent intent = new Intent(MainActivity.this,CloneActivity.class);
startActivity(intent);
它与显示启动最大区别就是不需要在Intent的实例化中传参, 但是需要在AndroidManifest.xml给该activity配置intentfilter属性,启动代码如下
Intent intent = new Intent();
intent.setAction("com.mangoer.activityreview");
startActivity(intent);
隐式启动的过滤过程如图
一般显示启动用于同一APP内,隐式启动用于启动APP外部活动,如果有多个组件被匹配成功,就会以对话框列表的方式让用户进行选择。每个Intent中只能指定一个action,但却能指定多个category;类别越多,动作越具体,意图越明确 在Intent添加类别可以添加多个类别,那就要求被匹配的组件必须同时满足这多个类别,才 能匹配成功。操作Activity的时候,如果没有类别,须加上默认类别
比如打开百度:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
Uri data=Uri.parse("http://www.baidu.com");
intent.setData(data);
startActivity(intent);
有时候我们跳转到别的activity后,希望能够携带一些数据返回到原activity,那就需要用到startActivityForResult,具体见Github
有时候可能需要携带数据跳转到其它activity,具体用法见Github
启动模式允许你去定义如何将一个activity的实例和当前任务栈进行关联,有两种方式来定义启动模式
如果一个被启动的activity在manifest里定义了启动模式,然后在使用Intent启动的时候也设置了flag定义启动模式,那么Intent中的定义模式将会覆盖manifest中的定义
通过设置launchMode属性来定义,有四种可选参数:
使用方法就是在使用startActivity的时候构建Intent,对Intent加入一个flag来改变Activity与任务的关联模式
affinity可以用于指定一个Activity更加愿意依附于哪一个任务,在默认情况下,同一个应用程序中的所有Activity都具有相同的affinity,所以,这些Activity都更加倾向于运行在相同的任务当中。当然了,你也可以去改变每个Activity的affinity值,通过元素的taskAffinity属性就可以实现了。
taskAffinity属性接收一个字符串参数,你可以指定成任意的值(经我测试字符串中至少要包含一个.),但必须不能和应用程序的包名相同,因为系统会使用包名来作为默认的affinity值。
通常可以与allowTaskReparenting属性配合使用,设置为true时,Activity就拥有了一个转移所在任务的能力。具体点来说,就是一个Activity现在是处于某个任务当中的,但是它与另外一个任务具有相同的affinity值,那么当另外这个任务切换到前台的时候,该Activity就可以转移到现在的这个任务当中。
如果用户将任务切换到后台之后过了很长一段时间,系统会将这个任务中除了最底层的那个Activity之外的其它所有Activity全部清除掉。当用户重新回到这个任务的时候,最底层的那个Activity将得到恢复。这个是系统默认的行为,因为既然过了这么长的一段时间,用户很有可能早就忘记了当时正在做什么,那么重新回到这个任务的时候,基本上应该是要去做点新的事情了。
当然,既然说是默认的行为,那就说明我们肯定是有办法来改变的,在元素中设置以下几种属性就可以改变系统这一默认行为:
scheme 是一种页面之间跳转的协议,不仅可以用于app之间进行跳转,还可以用于 H5 页面跳转到app页面,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面,主要用于支持以下几种场景
scheme 协议定义和 http 协议类似,都是标准的 URI 结构
[scheme:][//host:port][path][?query][#fragment]
我们从一个例子来看
mango://appwork:8080/man?id=8897&name=angel
匹配规则:
注意:如果将data标签加在了APP的启动Activity里,那么该App的桌面图标将会隐藏;否则需要另外添加一个intent-filter,将其添加到里面;如果希望在浏览器的网页上或者H5里通过scheme协议唤起应用,那么需要添加一个标签
data的完整配置如下:
在使用scheme协议跳转到具体页面时需要先检查下目的页面是否存在
public boolean valldCheck(Uri uri){
PackageManager packageManager = getPackageManager();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
List activities = packageManager.queryIntentActivities(intent, 0);
return activities.isEmpty();
}
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("mango://appwork/man?id=8897&name=angel"));
startActivity(intent);
写一个html,放在assets目录下
Title
打开应用
然后通过webview加载这个文件
webview.loadUrl("file:///android_asset/index.html");
这时候点击后就可以跳转到指定的Activity了
我们也可以通过WebViewClient的shouldOverrideUrlLoading方法去解析H5页面中的链接,然后进行相应的跳转,也方便我们判断要跳转的应用是否安装了
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.e(TAG,"shouldOverrideUrlLoading url="+url);
// 根据协议的参数,判断是否是所需要的url
// 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
//假定传入进来的 url = "mango://appwork/man?id=8897&name=angel"(同时也是约定好的需要拦截的)
Uri uri = Uri.parse(url);
// 如果url的协议 = 预先约定的 mango 协议
// 就解析往下解析参数
if ( uri.getScheme().equals("mango")) {
// 如果 authority = 预先约定协议里的 appwork,即代表都符合约定的协议
// 所以拦截url,下面JS开始跳转到native指定页面
if (uri.getAuthority().equals("appwork")) {
Intent intent = new Intent(Intent.ACTION_VIEW,uri);
startActivity(intent);
}
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
);
}
Uri data = getIntent().getData();
if (data != null) {
Log.e(TAG,"data="+data.toString());
String scheme = data.getScheme();
String authority = data.getAuthority();
String host = data.getHost();
int port = data.getPort();
String path = data.getPath();
Set queryParameterNames = data.getQueryParameterNames();
for (String key : queryParameterNames) {
Log.e(TAG,"key="+key);
String value = data.getQueryParameter(key);
Log.e(TAG,"value="+value);
}
}
当别人使用scheme协议调用我们的activity的时候,如果使用FLAG_ACTIVITY_NEW_TASK启动activity,由上面使用Intent Flags 定义启动模式可知,这时候系统可能为这个activity新建一个任务,导致与被调用的应用的其它activity不处于同一个任务,这时候需要在manifest里给activity设置taskAffinity值,通常是应用包名,强制处于同一个任务中。
URI中的参数如果包含特殊字符,需要先进行url编码
当应用A启动应用B的activity时候,可能应用B所在的任务还是处于后台,这样需要在应用B被启动的activity里做处理,当收到启动请求后强制把当前任务移到前台
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
activityManager.moveTaskToFront(getTaskId(), ActivityManager.MOVE_TASK_WITH_HOME);
更多Activity知识总结请详见我的Github