Activity的启动流程是一个资深Android工程师必须掌握的内容,也是高职级面试中的高频面试知识点,无论是从事应用层开发,还是Framework开发,其重要性都无需我多言。而要真正理解它,就不可避免地要深入到源码了,本文将从Android8.1系统源码入手,来抽丝剥茧。由于Activity的启动流程涉及到的细节非常多而且复杂,为了便于读理解,本文将摒弃众多的细节,而着重于关键流程的梳理。

尽管简化了很多细节,但流程还是不少,为了便于读者阅读和理解,笔者会先给出重要的结论和UML序列图,读者正确的阅读方式也是先知道梗概,再结合UML序列图来看源码,同时最好能自己再IDE上打开源码,顺着笔者的思路去阅读,否者会看得晕头转向。

1、启动Activity的若干场景

Activity的启动有多种途径,比较常见的有:

(1)点击Launcher中的快捷图标,这种方式进入的是根Activity;

(2)从其它应用跳转到某个应用的activity,这种场景下启动的可以是根Activity,也可以是其它Activity,如:从某些应用拨打电话、开启相机、打开浏览器等;

(3)同一个应用种从某个组件中启动Activity。

而启动某个Activity的时候,也可能有两种情形:

(1)目标Activity所在应用程序进程不存在,也就是此时该应用还没有启动的情形;

(2)目标Activity所在应用程序进程存在,也就是该应用之前启动过。

上面这些场景,Activity的启动流程肯定是存在一定差异的,但核心流程基本一致,都是在基本流程基础上或增或减部分流程。从Launcher中点击快捷图标启动一个根Activity的场景,就比较有代表性,本文将以此情形来介绍Activity的启动流程。

2、根Activity启动流程概貌

这里,我先给出结论,读者们先宏观看看这其中大概有哪几步。先上图:

从Launcher中点击快捷图标到启动根Activity过程中,主要涉及到4个进程的交互:Launcher所在应用进程、ActivityManagerService(后文简称AMS)所在的SystemServe系统进程、Zygote系统进程、目标根Activity所在的应用程序进程(这里请读者注意一下不同颜色所表示的不同进程,后文会与此保持一致)。

(1)Launcher进程请求AMS创建根Activity。我们知道,在系统启动过程中,会启动SystemServer进程, AMS、PackageManagerService(后文简称PMS)也是在这个环节中启动的,所以AMS是运行在SystemServer进程当中的。应用的根Activity会在AndroidManifest.xml文件中注册,PMS解析出这些信息,并在Launcher中对这些包名、Activity路径及名称等信息进行封装,当点击快捷图标时,Launcher会调用startActivity方法去启动该图标所对应的根Activity。然后在Luancher进程中通过层层调用,直到通过Binder方式实现IPC,流程就进入到AMS中,也就是SystemServer进程中。

(2)AMS请求创建根Activity所在的进程。AMS收到Launcher进程启动根Activity的请求后,会先判断根Activity所在的进程是否已经创建过了,如果没有创建过,则会向Zygote进程请求创建该进程,我们目前讨论的情形就是根Activity所在进程没有创建过的情况。我们知道,Zygote进程在启动的时候,会作为服务端创建一个名为“zygote”的Socket,用于监听AMS发起的创建新应用进程请求,所以此时流程进入到Zygote进程中。

(3)Zygote进程fork出目标进程。Zygote收到AMS的请求后,会以fork的方式创建这个新的应用进程,此过程中会实例化一个ActivityThread对象,也就是一般所说的主线程,运行其入口main方法。

(4)AMS调度应用进程创建和启动根Activity。根Activity所在的应用程序进程被创建后,AMS在SystemServer进程中也经过层层调用,最终又通过Binder方式实现IPC,将启动Activity的任务交给应用程序进程中的ApplicationThread本地代理,此后,流程进入到根Activity所在的应用程序进程中。这部分流程中,SystemServer中所做的工作主要是根Actifity创建和启动前的一些准备工作,比如当前用户权限等。

(5)在应用进程中完成根Activity的创建和启动。在这里将创建根Activity实例、Applicaiton实例,调用各个生命周期方法,并将DecorView(布局文件中的View会添加到DecorView中)添加到Window中显示出来。

上文中涉及到系统启动流程相关知识,比如Zygote、SystemServer、AMS、PMS 的启动以及Zygote的功能,读者不清楚的画,最好先阅读这篇文章:【系统之音】Android系统启动篇

3、从Launcher到AMS

先上UML序列图

前面说过,点击Luancher中的快捷图标的时候,会通过startActivity启动其对应的Activity,Launcher进程中的这部分流程源码如下:

Launcher.java的源码路径:/packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

 //==========Launcher.java========
private void startAppShortcutOrInfoActivity(View v) {
......
boolean success = startActivitySafely(v, intent, item);
......
} public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
......
startActivity(intent, optsBundle);
......
} //===========Activity.java========
/**
* Launch a new activity.
* ......
*/
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
......
startActivityForResult(intent, -1);
}
} public void startActivityForResult(...) {
if (mParent == null) { //表示当前根Activity还没有创建
......
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
......
}
} //===========Instrumentation.java==============
public ActivityResult execStartActivity(...){
......
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options); //代码①
......
}

通过上述代码后,流程就从Launcher进程进入到AMS所在的SystemServer进程了, 这部分流程比较简单,这里就不做过多解释了。这里重点看一下代码①处(第43行)的ActivityManager.getService():

 //=========ActivityManager.java=========
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
}; //=================Singleton.java=========
/**
* Singleton helper class for lazily initialization.
* ......
*/
public abstract class Singleton<T> {
private T mInstance; protected abstract T create(); public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}

ActivityManager.getService()这句代码实际上就是通过单例模式获取AMS在Launcher进程中的远程代理,类似这样的代码实现在系统源码种还是比较常见的。

4、从AMS到ApplicationThread(ActivityThread)

先上UML序列图:

这一部分的启动流程调用非常繁琐,可谓是“峰回路转”,笔者在跟进其调用流程时差点昏厥了,大有“山重水复疑无路”的困惑,直到看到下面代码:

 final boolean realStartActivityLocked(...){
......
app.thread.scheduleLaunchActivity(...); //代码②
......
}

此时又深感“柳暗花明又一村”了。ApplicationThread是ActivityThread中的一个内部类,也是应用程序进程中的一个服务(Stub),通过Binder方式对外提供服务:

 //=======ActivityThread.java======
private class ApplicationThread extends IApplicationThread.Stub {
......
public final void scheduleLaunchActivity(...){
......
}
......
}

这里我们需要重点理解下图中的模型:

每个应用程序都运行在一个独立的进程中(当然也可以声明为多个进程,这里不做讨论),不同进程之间内存等资源是不能直接共享的,只能通过Binder方式来和外界交互。这就好比系统像个大海,应用程序进程就像一座座孤岛,而Binder就是孤岛之间的桥梁或者船只。上图中模拟了应用程序进程与SystemServer进程的交互方式,应用程序进程持有了SystemServer进程中AMS/WMS等系统服务的远程代理Proxy,通过这个Proxy来调用SystemServer进程中的系统服务;SystemServer进程中也持有了应用程序进程中的ApplicationThread的远程代理Proxy,通过这个Proxy来调用应用程序进程中的方法。

代码②处的app.thread就是ApplicationThread在SystemServer端的远程代理(Proxy),正式通过这个远程代理调用根Activity所在应用程序进程中的相关方法的从这里开始,流程就进入到目标应用程序进程了。

由于该部分又繁琐,又没有涉及到直接创建和启动Activity的代码,所以这里就不贴代码了。对于这一部分的流程,个人建议读者没有必要太纠结细节,知道其大概做了些什么就够了。

5、在应用程序进程中创建和启动根Activity

先看看ActivityThread类主要结构

ActivityThread一般被称作主线程(当然它不是真正的线程),它包含两个很重要的内部类,ApplicationThread(前面已经介绍过了)和H。这个H是Handler的子类,拥有主线程的looper,所以其Callback的回调函数handleMessage运行在主现在当中,所以这个H类的作用其实就是将线程切换到主线程。

我们结合上图来看看如下源码:

 //=======ActivityThread.java======
private class ApplicationThread extends IApplicationThread.Stub {
......
public final void scheduleLaunchActivity(...){
ActivityClientRecord r = new ActivityClientRecord();
......
r.intent = intent;
......
sendMessage(H.LAUNCH_ACTIVITY, r);
}
......
} private void sendMessage(int what, Object obj) {
sendMessage(what, obj, 0, 0, false);
} private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
......
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
......
mH.sendMessage(msg);
} final H mH = new H();
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
......
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//代码③
......
}
}
......
}

这样就明确了,此时的流程,又由ApplicationThread经过Handler进入到了主线程(ActivityThread)中了。

从代码③处开始,流程就由主线程ActivityThread来处理了,还是先上UML序列图:

参照该图和如下源码,来看看应用程序进程是如何创建和启动Activity的:

 //=======ActivityThread.java=======
private void handleLaunchActivity(...){
......
WindowManagerGlobal.initialize();//代码④
......
Activity a = performLaunchActivity(r, customIntent);//代码⑤
if (a != null) {
handleResumeActivity(...);//代码⑫
}
......
}

代码④,其作用是通过单例模式获取一个WMS在应用程序进程中的远程代理Proxy,我们知道,后面Activity中setContentView加载的layout文件,就需要通过WMS添加到Window中来显示。该方法代码比较简单:

 //========WindowManagerGlobal.java======
......
private static IWindowManager sWindowManagerService;
......
public static void initialize() {
getWindowManagerService();
}
......
public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
......
}
return sWindowManagerService;
}
}

进入到代码⑤处:

 //===========ActivityThread.java=======
private Activity performLaunchActivity(...){
......
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);//代码⑥
......
} catch (Exception e) {
......
}
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);//代码⑦
......
activity.attach(...);
......
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); //代码⑩-1
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);//代码⑩-2
}
......
if (!r.activity.mFinished) {
activity.performStart();//代码⑪
......
}
......
} catch (Exception e) {
......
}
......
}

代码⑥处,以ClassLoader的方式创建Activity实例:

 //================Instrumentation.java=====
/**
* Perform instantiation of the process's {@link Activity} object.
* ......
* @return The newly instantiated Activity object.
*/
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}

从代码⑦处深入,该代码的作用在于处理Application相关的业务:

 //=========LoadedApk.java=========
public Application makeApplication(...){
if (mApplication != null) {
return mApplication;
}
......
Application app = null;
......
try {
java.lang.ClassLoader cl = getClassLoader();
......
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext); //代码⑧
......
} catch (Exception e) {
......
}
mActivityThread.mAllApplications.add(app);
mApplication = app;
if (instrumentation != null) {
try {
instrumentation.callApplicationOnCreate(app);//代码⑨
} catch (Exception e) {
......
}
}
......
}

代码⑧处,也是以ClassLoader方式创建Application实例:

 //================Instrumentaion.java=========
/**
* Perform instantiation of the process's {@link Application} object.
* ......
* @return The newly instantiated Application object.
*/
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return newApplication(cl.loadClass(className), context);
} /**
* Perform instantiation of the process's {@link Application} object.
* ......
* @return The newly instantiated Application object.
*/
static public Application newApplication(Class<?> clazz, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = (Application)clazz.newInstance();
app.attach(context);
return app;
}

代码⑨处,调用Application的onCreate方法,在自定义的Application中,重写的onCreate方法开始执行:

 //=============LoadedApk.java========
/**
* Perform calling of the application's {@link Application#onCreate} method.
* ......
* @param app The application being created.
*/
public void callApplicationOnCreate(Application app) {
app.onCreate();
} //=========Application.java========
@CallSuper
public void onCreate() {
}

代码⑩(代码⑩-1或代码⑩-2)中,执行Activity的onCreate方法,根Activity中的onCreate方法执行:

 //==============Instrumentation.java========
/**
* Perform calling of an activity's {@link Activity#onCreate} method.
* ......
*/
public void callActivityOnCreate(Activity activity, Bundle icicle) {
prePerformCreate(activity);
activity.performCreate(icicle);
postPerformCreate(activity);
}
/**
* Perform calling of an activity's {@link Activity#onCreate} method.
* ......
*/
public void callActivityOnCreate(Activity activity, Bundle icicle,
PersistableBundle persistentState) {
prePerformCreate(activity);
activity.performCreate(icicle, persistentState);
postPerformCreate(activity);
} //==========Activity.java========
final void performCreate(Bundle icicle) {
performCreate(icicle, null);
}
final void performCreate(Bundle icicle, PersistableBundle p
......
if (persistentState != null) {
onCreate(icicle, persistentState);
} else {
onCreate(icicle);
}
......
} public void onCreate(@Nullable Bundle savedInstanceState,
@Nullable PersistableBundle persistentState) {
onCreate(savedInstanceState);
} @MainThread
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
......
}
代码⑪开始执行Activity的onStart方法,根Activity的onStart方法开始执行:
 //======Activity.java======
final void performStart() {
......
mInstrumentation.callActivityOnStart(this);
......
} //=========Instrumentation.java=========
/**
* Perform calling of an activity's {@link Activity#onStart} method.
* ......
*/
public void callActivityOnStart(Activity activity) {
activity.onStart();
} //======Activity.java======
@CallSuper
protected void onStart() {
......
}
代码⑫的handleResumeActivity方法用于处理resume相关的业务:
 //========ActivityThread.java======
final void handleResumeActivity(...){
......
r = performResumeActivity(token, clearHide, reason);//代码⑬
......
//如下过程将DecorView添加到窗口中 代码段⑯
r.window = r.activity.getWindow(); //PhoneWindow实例
View decor = r.window.getDecorView(); //DecorView实例
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
......
ViewRootImpl impl = decor.getViewRootImpl();
......
wm.addView(decor, l);
......
}

深入代码⑬中:

 //======ActivityThread=======
public final ActivityClientRecord performResumeActivity(...){
......
r.activity.performResume();
......
} //======Activity.java=====
final void performResume() {
performRestart();//代码⑭
......
mInstrumentation.callActivityOnResume(this);//代码⑮
......
}

代码⑭用于处理reStart相关的业务,当前场景是新创建根Activity,所以不会走这个流程;如果是从其它界面回退到这个activity,就会走调用onRestart和onStart的流程:

 //===========Activity.java========
final void performRestart() {
......
if (mStopped) {
mStopped = false;
......
mInstrumentation.callActivityOnRestart(this);
......
performStart();
}
} //============Instrumentation.java===========
/**
* Perform calling of an activity's {@link Activity#onRestart} method.
* ......
* @param activity The activity being restarted.
*/
public void callActivityOnRestart(Activity activity) {
activity.onRestart();
} //===========Activity.java===========
@CallSuper
protected void onRestart() {
mCalled = true;
} final void performStart() {
......
mInstrumentation.callActivityOnStart(this);
......
} //============Instrumentation.java==========
/**
* Perform calling of an activity's {@link Activity#onStart} method.
* ......
* @param activity The activity being started.
*/
public void callActivityOnStart(Activity activity) {
activity.onStart();
}
//==============Activity.java============
@CallSuper
protected void onStart() {
......
}

代码⑮处调用acitvity的onResume方法,这样一来根Activity的onResume回调方法就执行了:

 //========Instrumentation.java=========
/**
* Perform calling of an activity's {@link Activity#onResume} method.
* ......
* @param activity The activity being resumed.
*/
public void callActivityOnResume(Activity activity) {
......
activity.onResume();
......
} @CallSuper
protected void onResume() {
......
}

代码段⑯(对应第7~16行)的作用在于将DecorView添加到Window,并完成界面的绘制流程。我们知道,根Activity在onCreate生命周期回调方法中会通过setContentView方法加载layout布局文件,将其加入到DecorView中,绘制部分详情可以阅读【【朝花夕拾】Android自定义View篇之(一)View绘制流程】。

这样,应用程序进程就完成了根Activity的创建和启动,界面也完成了显示。从上面的UML图和源码分析,可以发现这部分笔者是按照Activity的生命周期为主线来介绍的,实际上读者完全可以结合Activity的生命周期来理解和记忆这部分的主要流程。下图再次总结了这部分的主要流程:

想必读者应该对第4~7步的生命周期顺序非常熟悉了。需要注意的是,第2和第3步可能会和我们平时的认知有些出入,实际上Activity的实例比Application的实例要更早创建。第1和第8步是关于图形界面显示的,也需要重点关心。

6、其它场景启动流程

到目前为止,从Laucher中点击一个快捷图标来启动根Activity的整个流程就介绍完毕了,这个场景搞清楚了,其它场景就不在话下了。比如,从Launcher中启动一个应用程序进程已经启动的应用的根Activity,就在上述流程基础上少了Zytote创建应用程序进程这一步,如下图:

比如,应用程序内部启动另外一个新Activity时,就只需要考虑应用程序进程和SystemSever两个进程,如下图:

还有从其它应用中启动指定某另外应用中的Activity的场景和从Launcher启动的流程类似;再次打开一个已经启动的Activity,就无需走create流程,而是走onRestart-onStart-onResume生命周期流程,等等,这里就不一一列举了。

史上最全且最简洁易懂的Activity启动流程解析的更多相关文章

  1. 开源框架】Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发

    [原][开源框架]Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发,欢迎各位... 时间 2015-01-05 10:08:18 我是程序猿,我为自己代言 原文  http: ...

  2. GitHub上史上最全的Android开源项目分类汇总 (转)

    GitHub上史上最全的Android开源项目分类汇总 标签: github android 开源 | 发表时间:2014-11-23 23:00 | 作者:u013149325 分享到: 出处:ht ...

  3. 吐血总结|史上最全的MySQL学习资料!!

    在日常工作与学习中,无论是开发.运维.还是测试,对于数据库的学习是不可避免的,同时也是日常工作的必备技术之一.在互联网公司,开源产品线比较多,互联网企业所用的数据库占比较重的还是MySQL. 在刚刚出 ...

  4. 移动端IM开发者必读(二):史上最全移动弱网络优化方法总结

    1.前言 本文接上篇<移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”>,关于移动网络的主要特性,在上篇中已进行过详细地阐述,本文将针对上篇中提到的特性,结合我们的实践经 ...

  5. 你想找的Python资料这里全都有!没有你找不到!史上最全资料合集

    你想找的Python资料这里全都有!没有你找不到!史上最全资料合集 2017年11月15日 13:48:53 技术小百科 阅读数:1931   GitHub 上有一个 Awesome - XXX 系列 ...

  6. 史上最全面的SignalR系列教程-4、SignalR 自托管全解(使用Self-Host)-附各终端详细实例

    1.概述 通过前面几篇文章 史上最全面的SignalR系列教程-1.认识SignalR 史上最全面的SignalR系列教程-2.SignalR 实现推送功能-永久连接类实现方式 史上最全面的Signa ...

  7. 史上最全 69 道 Spring 面试题和答案

    史上最全 69 道 Spring 面试题和答案 目录Spring 概述依赖注入Spring beansSpring注解Spring数据访问Spring面向切面编程(AOP)Spring MVC Spr ...

  8. 史上最全USB HID开发资料

    史上最全USB HID开发资料 史上最全USB HID开发资料,悉心整理一个月,亲自测试. 涉及STM32 C51 8051F例子都有源码,VC上位机例子以及源码,USB协议,HID协议,USB抓包工 ...

  9. nacos 实战(史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

随机推荐

  1. ElementUI 级联选择框 设置最后一级可选及相关问题解决

    在使用 elementUI 的 el-cascader 级联选择框进行省市联动效果时,有这么一个需求:该级联选择框一共有三级结构分别为国家-省份-城市,国家和省份为必选项,城市为可选项.具体实现如下: ...

  2. 将数组内的元素循环左移P个位置

    问题可以转化为将数组内前 n 个元素进行逆置,再将后(n-p)个元素逆置,最后将整个数组逆置 void Reverse(int A[],int pos1,int pos2){ // 将A[pos1]与 ...

  3. Python灰帽子:黑客与逆向工程师的Python编程之道|百度网盘免费下载|新手黑客入门

    百度网盘免费下载:Python灰帽子:黑客与逆向工程师的Python编程之道 提取码:tgpg 目录  · · · · · · 第1章 搭建开发环境 11.1 操作系统要求 11.2 获取和安装Pyt ...

  4. Django---博客项目实战

    1.urls from django.conf.urls import url from django.contrib import admin from blog import views urlp ...

  5. 看完这篇,再也不怕被问到 AsyncTask 的原理了

    本文很多资料基于Google Developer官方对AsyncTask的最新介绍. AsyncTask 是什么 AsyncTask is designed to be a helper class ...

  6. 路径总和(leetcode 113)

    题目描述如下所示: 给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径.(https://leetcode-cn.com/problems/path-sum-ii/) ...

  7. Caused by: java.sql.SQLSyntaxErrorException: Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'c.id'

    打开mysql客户端,输入 select @@global.sql_mode 再执行 set @@global.sql_mode ='STRICT_TRANS_TABLES,NO_ZERO_IN_DA ...

  8. Seaborn基础1

    import seaborn as sns import numpy as np import matplotlib.pyplot as plt # # 折线图 def sinplot(flip = ...

  9. Python 访问字符串中的值

    Python 访问字符串中的值 Python 不支持单字符类型,单字符在 Python 中也是作为一个字符串使用.高佣联盟 www.cgewang.com Python 访问子字符串,可以使用方括号来 ...

  10. PHP empty() 函数

    empty() 函数用于检查一个变量是否为空.高佣联盟 www.cgewang.com empty() 判断一个变量是否被认为是空的.当一个变量并不存在,或者它的值等同于 FALSE,那么它会被认为不 ...