Android 隐式启动 Activity 可能存在的坑

3,787 阅读10分钟
原文链接: blog.csdn.net

  转载本专栏文章,请注明出处,尊重原创 。文章博客地址:道龙的博客

   本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布  

  本篇文章,对隐式启动Activity再做分析。

  有些人可能会说了,隐式启动活动不是很简单吗?这有什么不理解的?话先别说的这么早,对于隐式启动,还是具有很大的坑要爬的,当然,您如果是一个资深开发者就另当别论了。

  本篇文章,我们从最简单的开始,一步步引入,相信这样的方式,读起来也会轻松一些。

  我们平时启动一个活动,会通过两种方式。1、显示启动;2、隐式启动。

  (一)首先,我们来看两个很简单的小案例(实现打电话)。

我们再在布局文件提供一个TextView用于提示输入电话号码,在EditText里面输入号码,点击按钮的同时,获取到用户输入的号码,并且启动打电话功能。

主活动代码很简单:

MainActivity:

public class MainActivity extends Activity {

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

        //给按钮设置点击侦听
        //1.拿到按钮对象
        Button bt = (Button) findViewById(R.id.bt_call);//Button类是View的子类,向下转型要强转。
        //2.设置侦听
        bt.setOnClickListener(new MyListener());
    }

    class MyListener implements View.OnClickListener {

        //按钮被点击时,此方法调用
        @Override
        public void onClick(View v) {
            //获取用户输入的号码
            EditText et = (EditText) findViewById(R.id.et_phone);
            String phone = et.getText().toString();

            //我们需要告诉系统,我们的动作:我要打电话
            //创建意图对象
            Intent intent = new Intent();
            //把打电话的动作ACTION_CALL封装至意图对象当中
            intent.setAction(Intent.ACTION_CALL);
            //设置打给谁
            intent.setData(Uri.parse("tel:" + phone));//这个tel:必须要加上,表示我要打电话。否则不会有打电话功能,由于在打电话清单文件里设置了这个协议
            //把动作告诉系统,启动系统打电话功能。
            startActivity(intent);
        }

    }

}
运行后如下:



这个简直太简单了,估计代码都能烂肚子里了。

嗯,的确很简单,那就紧跟脚步,我们继续看一个简单的自定义启动活动的代码。

(二)自定义Activity,并隐式方式启动

我们都知道,如果要想自定义隐式启动别的activity,需要给该Activity添加“意图过滤器”<intent-filter>。

通过在<activity>标签下配置<intent-filter>的内容,可以指定当前活动能够响应的 action
和 category,打开 AndroidManifest.xml,添加如下代码:

<activity android:name=".NextActivity">
    <intent-filter>
        <action android:name="com.itydl"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

在<action>标签中我们指明了当前活动可以响应 com.itydl 这个 action,我们可以随便写里面的内容,它的加入表示给我们的Activity添加一个动作,只有带动作的Activity才能被隐式启动。而<category>标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的 Intent 中还可能带有的 category。意图中设置的action必须跟"com.itydl"是完全匹配的,只有<action>和<category>中的内容同时能够匹配上 Intent中指定的 action和 category时,这个活动才能响应该 Intent。我们也道,android.intent.category.DEFAULT 是一种默认的 category,在调用startActivity()方法的时候会自动将这category添加到 Intent中(查看所有系统源码,也都带上了这个category,我们也无需多去关心category它可能存在的坑了)。

对于Action的原理是:当StartActivity()运行的时候,该Activity会去系统所有清单文件中找对应的Action("")里面能匹配的Activity,找有没有对应的action与我们所写入的能匹配的,如果有(这里是NextActivity),这样就启动了NextActivity。

既然,NextActivity有了动作,那么我们的MainActivity再添加一个按钮,使用隐式方式启动它。如下:

mNext = (Button) findViewById(R.id.bt_next);
mNext.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent();
        intent.setAction("com.itydl");
        startActivity(intent);
    }
});

此时运行程序,发现能够正常启动。我们发现第一个例子启动打电话功能时候,还添加了一个setData()。那么,我们自定义的也可以同样添加data。修改清单文件NextActivity配置代码如下:

<intent-filter>
    <action android:name="com.itydl"/>
    <data android:scheme="ydl"/>
    <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
这个时候,我们再运行程序,发现程序崩溃。这是因为我们隐式启动代码中,并没有完全跟清单文件中<intent-filter>下面的内容匹配。要想匹配成功,我们也要去代码中设置data。再回到小案例一中,我们也就知道了为什么启动打电话功能要设置setData了,还不是因为电话清单文件源码中有这个data标签吗。

在这里浅显介绍一下系统添加data下面的标签都有哪些:

<data>标签中主要可以配置以下内容。
1.  Android:scheme
用于指定数据的协议部分。
2.  android:host
用于指定数据的主机名部分。
3.  android:port
用于指定数据的端口部分,一般紧随在主机名之后。
4.  android:path
用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
5.  android:mimeType
用于指定可以处理的数据类型,允许使用通配符的方式进行指定。只有<data>标签中指定的内容和 Intent 中携带的 Data 完全一致时,当前活动才能够响应该 Intent。不过一般在<data>标签中都不会指定过多的内容,常见的是mimeTypescheme

给我们的隐式启动按钮加入如下代码:

intent.setData(Uri.parse("ydl:qwe"));
运行程序不再报错。

上边的内容,对于一个初学者来说是必须掌握的内容。那么再来看看一些细节问题,这些细节问题不知道,可能还真会让你不知道如何去隐式启动别的Activity!

(三)深入分析隐式启动的细节

首先先从自定义隐式启动开始讨论。上边代码Uri.parse()参数内容"ydl:qwe"我们看到qwe好像很别扭,其实setData的英文名称就告诉我们设置数据的了。基于上面对系统<data>里面配置介绍,我们清单文件中是android:scheme,这是一个协议,因而我们设置数据必须要以scheme后边内容开头。这里是"ydl"作为了协议。而后面"qwe"内容可以随便写。例如我改成如下代码:

intent.setData(Uri.parse("ydl:234"));
仍然可以启动下一个活动。

那么我们再回到最初的小案例,有如下代码:

intent.setData(Uri.parse("tel:" + phone));

这里不就是一个协议吗?在清单文件中对应的Activity肯定有这个协议,我们就去上层源码看一看,找到如下代码:

<activity android:name="OutgoingCallBroadcaster"
                android:permission="android.permission.CALL_PHONE"
                android:theme="@android:style/Theme.NoDisplay"
                android:configChanges="orientation|keyboardHidden">
            <!-- CALL action intent filters, for the various ways
                 of initiating an outgoing call. -->
            <intent-filter>
                <action android:name="android.intent.action.CALL" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="tel" />
            </intent-filter>
            <intent-filter android:icon="@drawable/ic_launcher_sip_call">
                <action android:name="android.intent.action.CALL" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="sip" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.CALL" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="voicemail" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.CALL" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="vnd.android.cursor.item/phone" />
                <data android:mimeType="vnd.android.cursor.item/phone_v2" />
                <data android:mimeType="vnd.android.cursor.item/person" />
            </intent-filter>
        </activity>
我们看到android:permission就是我们打电话要指定的权限。而往下看第一个intent-filter

action android:name="android.intent.action.CALL" />不就是针对我们代码中要设置的Action(intent.setAction(Intent.ACTION_CALL);)吗?

<data android:scheme="tel"不就是我们在代码中要指定的协议吗(intent.setData(Uri.parse("tel:110"));)?只不过我们所以带过去一些数据phone,用户输入的号码吗?

到了这里,相信这个细节大家都能掌握了。那么接着往下继续爬坑~

我们发现,打电话OutgoingCallBroadcaster的清单文件中有太多的intent-filter,看花了眼,我们到底怎么知道匹配哪个intent-filter才能启动打电话这个活动呢?其实,原理很简单,在清单文件中的intent-filter代表我们可以有好几种启动方式好几种写法去启动这个打电话这个活动。我们使用哪一种方式都无所谓的,都可以。那么我们模仿这种情况,来对自定义启动活动也这么添加几个intent-filter,看完下面的内容,这些坑也就爬完了。

(四)模仿源码为自己的活动配置不同的intent-filter

首先我们在清单文件中继续添加intent-filter

<intent-filter>
    <action android:name="com.itydl2"/>
    <data android:scheme="ydl2"/>
    <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
此时两个intent-filter,我们为了验证刚才的说法,把启动活动的代码修改一下:

Intent intent = new Intent();
intent.setData(Uri.parse("ydl2:234"));
intent.setAction("com.itydl2");
startActivity(intent);
我们发现都改成了ydl2,这样启动时匹配的是上边这个intent-filter。能正常启动活动。

注意,启动互动代码必须与某一个intent-filter相匹配才能正常启动活动,否则会报错。这是为什么,相信我们很简单就能理解原因了。

继续跟进:

我们在清单文件中除了可以配置多个intent-filter,还可以配置多个data,以及多个action吗?(很多源码中也是这样的)。我们再把清单文件中代码作如下修改来试试:

<intent-filter>
    <action android:name="com.itydl2"/>
    <action android:name="com.itydl3"/>
    <action android:name="com.itydl4"/>
    <action android:name="com.itydl5"/>
    <data android:scheme="ydl2"/>
    <data android:scheme="ydl3"/>
    <data android:scheme="ydl4"/>
    <data android:scheme="ydl5"/>
    <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
这里添加了多个data多个action,其实,在我们在启动活动的代码位置,只需要配置任意一条data以及任意一条action就可以成功启动这个activity了,不需要全部匹配。活动代码即使这么写,也是没有任何问题:

Intent intent = new Intent();
intent.setAction("com.itydl2");
intent.setData(Uri.parse("ydl4:234"));
startActivity(intent);
启动活动成功。

最后,还有一点点就完结了。即常见的android:mimeType这个属性

我们知道了,data是用来传递数据的,听过setData把数据放进里面,会把数据传递给目标Activity,在目标activity可以通过getIntent()(获取启动这个activity的意图对象)方式来获取里面的数据。而android:mimeType是用来定义你setData用来传递什么数据类型的。例如,一般我们传递文本数据,我们就可以这么写:

<intent-filter>
    <action android:name="com.itydl2"/>
    
    <data android:mimeType="text/username"/>
    <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
我们首先测试只有android:mimeType的时候。

Intent intent = new Intent();
intent.setAction("com.itydl2");
intent.setType("text/username");
startActivity(intent);
这个时候,代码和清单文件也是完全匹配的。我们运行程序,也是没有任何问题。

当android:mimeType和android:scheme同时存在的时候,我们需要注意

如果代码先设置setData();后setType()后者会把前者清理掉,反之亦然,这里就跟你女神问你“你妈和我掉水里你先救谁”原理是一样的。我们需要通过setDataAndType()方法,把schememimeType同时设置进去才能匹配对应的清单文件中的intent-filter。例如我清单文件中代码是:

<intent-filter>
    <action android:name="com.itydl2"/>
    <data android:scheme="ydl2"/>
    <data android:mimeType="text/username"/>
    <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
那么我们启动的时候,需要通过
intent.setDataAndType(Uri.parse("ydl2:qwe"),"text/username");
这样就能完好匹配清单文件内容,可以正常启动actiivity了。


终于写完了,相信看完此篇文章你对隐式启动活动的原理更加清晰了。以后启动任何互动,只需要看看那个活动的清单文件内容,就能轻松通过隐式启动的方式去打开它了!


最后,祝大家圣诞快乐!



喜欢我的朋友可以关注我的博客专栏。

也可以打开微信搜索公众号  Android程序员开发指南  或者手机扫描下方二维码 在公众号阅读更多Android文章。

微信公众号图片: