博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 四大组件之“ BroadcastReceiver ”
阅读量:5057 次
发布时间:2019-06-12

本文共 17038 字,大约阅读时间需要 56 分钟。

前言

Android四大组件重要性已经不言而喻了,今天谈谈的是Android中的广播机制。在我们上学的时候,每个班级的教室里都会装有一个喇叭,这些喇叭都是接入到学校的广播室的,一旦有什么重要的通知,就会播放一条广播来告知全校的师生。类似的工作机制其实在计算机领域也有很广泛的应用,如果你了解网络通信原理应该会知道,在一个 IP 网络范围中最大的 IP 地址是被保留作为广播地址来使用的。比如某个网络的 IP 范围是 192.168.0.XXX,子网掩码是 255.255.255.0,那么这个网络的广播地址就是 192.168.0.255。 广播数据包会被发送到同一网络上的所有端口,这样在该网络中的每台主机都将会收到这条广播。为了方便于进行系统级别的消息通知,Android 也引入了一套类似的广播消息机制。

目录

  • 广播机制介绍
  • BroadcastReceiver用法
  • 发送自定义广播
  • 本地广播介绍
  • 广播的注册过程
  • 广播的发送和接受过程
  • 总结

广播机制介绍

为什么说 Android中的广播机制更加灵活呢?这是因为 Android中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会接收到自己所关心的广播内容,这些 播可能是来自于系统的,也可能是来自于其他应用程序的。Android提供了一套完整的 API, 允许应用程序自由地发送和接收广播。接收广播的方法则需要引入一个新的概念,广播接收器(Broadcast Receiver),它就是用来接收来自系统和应用中的广播。

在Android广播中,主要分为两种类型:标准广播和有序广播。

标准广播(Normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的 广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可 言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。标准广播的工作流程如图所示。

标准广播

有序广播(Ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。

有序广播

BroadcastReceiver用法

BroadcastReceiver主要包括两方面的内容,一个是广播的注册过程,另一个是广播的发送和接收过程。那么该如何创建一个广播接收器呢?其实只需要新建一个类,让它继承自BroadcastReceiver, 并重写父类的 onReceive()方法就行了。这样当有广播到来时,onReceive()方法就会得到执行, 具体的逻辑就可以在这个方法中处理。 广播的使用方法有两个:静态方法和动态方法。

动态方法

public class MainActivity extends Activity {      private IntentFilter intentFilter;    private NetworkChangeReceiver networkChangeReceiver;      @Override    protected void onCreate(Bundle savedInstanceState) {       super.onCreate(savedInstanceState);       setContentView(R.layout.activity_main);       intentFilter = new IntentFilter();    intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");      networkChangeReceiver = new NetworkChangeReceiver();           registerReceiver(networkChangeReceiver, intentFilter);  }       @Override   protected void onDestroy() {       super.onDestroy();      unregisterReceiver(networkChangeReceiver);    }      private class NetworkChangeReceiver extends BroadcastReceiver {      @Override       public void onReceive(Context context, Intent intent) {         Toast.makeText(context, "网络变化", Toast.LENGTH_SHORT).show();     }   }  }

可以看到,我们在 MainActivity 中定义了一个内部类 NetworkChangeReceiver,这个类 是继承自 BroadcastReceiver的,并重写了父类的 onReceive()方法。这样每当网络状态发生变 化时,onReceive()方法就会得到执行,这里只是简单地使用 Toast提示了一段文本信息。

然后观察 onCreate()方法,首先我们创建了一个 IntentFilter 的实例,并给它添加了一个 值为 android.net.conn.CONNECTIVITY_CHANGE 的 action,为什么要添加这个值呢?因为 当网络状态发生变化时,系统发出的正是一条值为 android.net.conn.CONNECTIVITY_ CHANGE 的广播,也就是说我们的广播接收器想要监听什么广播,就在这里添加相应的 action就行了。接下来创建了一个 NetworkChangeReceiver的实例,然后调用 registerReceiver() 方法进行注册,将 NetworkChangeReceiver 的实例和 IntentFilter 的实例都传了进去,这样 NetworkChangeReceiver就会收到所有值为android.net.conn.CONNECTIVITY_CHANGE的广 播,也就实现了监听网络变化的功能。

最后要记得,动态注册的广播接收器一定都要取消注册才行,这里我们是在 onDestroy() 方法中通过调用 unregisterReceiver()方法来实现的。

静态方法

动态注册的广播接收器可以自由地控制注册与注销,在灵活性方面有很大的优势,但是 它也存在着一个缺点,即必须要在程序启动之后才能接收到广播,因为注册的逻辑是写在 onCreate()方法中的。那么有没有什么办法可以让程序在未启动的情况下就能接收到广播 呢?这就需要使用静态注册的方式了。

这里我们准备让程序接收一条开机广播,当收到这条广播时就可以在 onReceive()方法里 执行相应的逻辑,从而实现开机启动的功能。新建一个 BootCompleteReceiver 继承自 BroadcastReceiver,代码如下所示

public class BootCompleteReceiver extends BroadcastReceiver {    @Override      public void onReceive(Context context, Intent intent) {         Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();      }  }

可以看到,这里不再使用内部类的方式来定义广播接收器,因为稍后我们需要在 AndroidManifest.xml中将这个广播接收器的类名注册进去。在 onReceive()方法中,还是简单 地使用 Toast弹出一段提示信息。

然后修改 AndroidManifest.xml文件,代码如下所示:

……

标签内出现了一个新的标签,所有静态注册的广播接收器 都是在这里进行注册的。它的用法其实和标签非常相似,首先通过 android:name 来指定具体注册哪一个广播接收器,然后在标签里加入想要接收的广播就行了, 由于Android系统启动完成后会发出一条值为android.intent.action.BOOT_COMPLETED的广 播,因此我们在这里添加了相应的 action。

另外,监听系统开机广播也是需要声明权限的,可以看到,我们使用 标签又加入了一条 android.permission.RECEIVE_BOOT_COMPLETED权限

发送自定义广播

现在你已经学会了通过广播接收器来接收系统广播,接下来我们就要学习一下如何在应用程序中发送自定义的广播。前面已经介绍过了,广播主要分为两种类型,标准广播和有序 广播。

在API文档中关于BroadcastReceiver的概述:

  • 广播接收器是一个专注于接收广播通知信息,并做出对应处理的组件。很多广播是源自于系统代码的──比如,通知时区改变、电池电量低、拍摄了一张照片或者用户改变了语言选项。应用程序也可以进行广播──比如说,通知其它应用程序一些数据下载完成并处于可用状态。
  • 应用程序可以拥有任意数量的广播接收器以对所有它感兴趣的通知信息予以响应。所有的接收器均继承自BroadcastReceiver基类。
  • 广播接收器没有用户界面。然而,它们可以启动一个activity来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

那么广播事件的流程如何呢,如下:

  • 注册广播事件:注册方式有两种,一种是静态注册,就是在AndroidManifest.xml文件中定义,注册的广播接收器必须要继承BroadcastReceiver;另一种是动态注册,是在程序中使用Context.registerReceiver注册,注册的广播接收器相当于一个匿名类。两种方式都需要IntentFIlter。

  • 发送广播事件:通过Context.sendBroadcast来发送,由Intent来传递注册时用到的Action。

  • 接收广播事件:当发送的广播被接收器监听到后,会调用它的onReceive()方法,并将包含消息的Intent对象传给它。onReceive中代码的执行时间不要超过10s,否则Android会弹出超时dialog。

具体做法:

在发送广播之前,我们还是需要先定义一个广播接收器来准备接收此广播才行,不然发 出去也是白发。因此新建一个 MyBroadcastReceiver继承自 BroadcastReceiver

public class MyBroadcastReceiver extends BroadcastReceiver {        @Override      public void onReceive(Context context, Intent intent) {         Toast.makeText(context, "接收到广播消息", Toast.LENGTH_SHORT).show();    }  }

这里当 MyBroadcastReceiver收到自定义的广播时,就会弹出提示语。然后在 AndroidManifest.xml中对这个广播接收器进行注册:

……

可以看到,这里让 MyBroadcastReceiver 接收一条值为 com.example.broadcasttest. MY_BROADCAST的广播,因此待会儿在发送广播的时候,我们就需要发出这样的一条广播。

public class MainActivity extends Activity {      @Override      protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);        Button button = (Button) findViewById(R.id.button);        button.setOnClickListener(new OnClickListener() {           @Override            public void onClick(View v) {            Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");                   sendBroadcast(intent);           }      });       } }

可以看到,我们在按钮的点击事件里面加入了发送自定义广播的逻辑。首先构建出了一 个 Intent对象,并把要发送的广播的值传入,然后调用了 Context的 sendBroadcast()方法将广 播发送出去,这样所有监听 com.example.broadcasttest.MY_BROADCAST 这条广播的广播接 收器就会收到消息。此时发出去的广播就是一条标准广播。

本地广播介绍

前面我们发送和接收的广播全部都是属于系统全局广播,即发出的广播可以被其他任何的任何应用程序接收到,并且我们也可以接收来自于其他任何应用程序的广播。这样就很容易会引起安全性的问题,比如说我们发送的一些携带关键性数据的广播有可能被其他的应用 程序截获,或者其他的程序不停地向我们的广播接收器里发送各种垃圾广播。

为了能够简单地解决广播的安全性问题,Android 引入了一套本地广播机制,使用这个机制发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播,这样所有的安全性问题就都不存在了。 本地广播的用法并不复杂,主要就是使用了一个 LocalBroadcastManager 来对广播进行管理,并提供了发送广播和注册广播接收器的方法。下面我们就通过具体的实例来尝试一下它的用法,修改 MainActivity中的代码,如下所示:

public class MainActivity extends Activity {       private IntentFilter intentFilter;      private LocalReceiver localReceiver;      private LocalBroadcastManager localBroadcastManager;      @Override      protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);       setContentView(R.layout.activity_main);       localBroadcastManager = LocalBroadcastManager.getInstance(this);       // 获取实例        Button button = (Button) findViewById(R.id.button);      button.setOnClickListener(new OnClickListener() {          @Override          public void onClick(View v) {             Intent intent = new Intent("com.example.broadcasttest. LOCAL_BROADCAST");                   localBroadcastManager.sendBroadcast(intent);// 发送本地广播         }      });       intentFilter = new IntentFilter();         intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");       localReceiver = new LocalReceiver();         // 注册本地广播监听器      localBroadcastManager.registerReceiver(localReceiver, intentFilter);     }        @Override     protected void onDestroy() {       super.onDestroy();        localBroadcastManager.unregisterReceiver(localReceiver);     }        private class LocalReceiver extends BroadcastReceiver {          @Override         public void onReceive(Context context, Intent intent) {             Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();         }      }  }

有没有感觉这些代码很熟悉?没错,其实这基本上就和我们前面所学的动态注册广播接 收器以及发送广播的代码是一样。只不过现在首先是通过 LocalBroadcastManager的 getInstance() 方法得到了它的一个实例,然后在注册广播接收器的时候调用的是 LocalBroadcastManager 的 registerReceiver()方法,在发送广播的时候调用的是 LocalBroadcastManager的 sendBroadcast() 方法,仅此而已。

另外还有一点需要说明,本地广播是无法通过静态注册的方式来接收的。其实这也完全 可以理解,因为静态注册主要就是为了让程序在未启动的情况下也能收到广播,而发送本地 广播时,我们的程序肯定是已经启动了,因此也完全不需要使用静态注册的功能。

总结下使用本地广播的几点优势吧。

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

广播的注册过程

我们现在知道了广播的注册有静态注册和动态注册,其中静态注册的广播在应用安装时由系统自动完成注册的。具体来说是由PMS(PackageManagerService)来完成整个注册过程的,除了广播以为,其他三大组件也都是应用安装时由PMS解析并注册的,这里分析下广播的动态注册过程,动态注册过程是从ContextWrapper的registerReceiver方法开始的,和Activity以及Service一样。ContextWrapper并没有做实际的工作,基本将注册过程直接交给ContextImpl来完成。

@Override  public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {      return registerReceiver(receiver, filter, null, null);  }

ContextImpl的registerReceiver方法调用了自己的registerReceiverInternal方法,具体实现如下:

private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,    IntentFilter filter, String broadcastPermission,Handler scheduler, Context context) {      IIntentReceiver rd = null;      if (receiver != null) {        if (mPackageInfo != null && context != null) {          if (scheduler == null) {            scheduler = mMainThread.getHandler();          }          rd = mPackageInfo.getReceiverDispatcher(            receiver, context, scheduler,            mMainThread.getInstrumentation(), true);        } else {          if (scheduler == null) {            scheduler = mMainThread.getHandler();          }          rd = new LoadedApk.ReceiverDispatcher(            receiver, context, scheduler, null, true).getIIntentReceiver();        }      }      try {        return ActivityManagerNative.getDefault().registerReceiver(          mMainThread.getApplicationThread(), mBasePackageName,          rd, filter, broadcastPermission, userId);      } catch (RemoteException e) {        return null;      }}

系统首先从mPackageInfo获取IIntentReceiver对象,然后再采用跨进程的方式向AMS发送广播注册的请求。之所以用IIntentReceiver而不是直接采用BroadcastReceiver,这是因为上述注册过程是一个进程间通信的过程,而BroadcastReceiver作为一个Android组件是不能直接跨进程传递的,所以需要通过IIntentReceiver来中转一下,毫无疑问,IIntentReceiver必须是一个Binder接口,它的具体实现是LoadedApk.ReceiverDispatcher,ReceiverDispatcher的内部同时保存了BroadcastReceiver和InnerReceiver,这样当接收到广播时ReceiverDispatcher可以很方便地调用BroadcastReceiver的onReceiver方法。

这里的ActivityManagerNative.getDefault()实际上就是一个AMS。具体代码如下:

public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,Context context, Handler handler,Instrumentation instrumentation, boolean registered) {   synchronized (mReceivers) {     LoadedApk.ReceiverDispatcher rd = null;     ArrayMap
map = null; if (registered) { map = mReceivers.get(context); if (map != null) { rd = map.get(r); } } if (rd == null) { rd = new ReceiverDispatcher(r, context, handler,instrumentation, registered); if (registered) { if (map == null) { map = new ArrayMap
(); mReceivers.put(context, map); } map.put(r, rd); } } else { rd.validate(context, handler); } rd.mForgotten = false; return rd.getIIntentReceiver(); }

由于注册的广播真正的实现过程是在AMS中,最终会把远程的InnerReceiver对象以及IntentFilter对象存储起来,这样整个广播的注册过程就完成了。

广播的发送和接受过程

当通过send方法来发送广播时,AMS会查找出匹配的广播接收者并将广播发送给他们处理。广播的发送有几种类型:普通广播,有序广播和粘性广播。这里分析下普通广播的实现。

广播的发送和接收。其本质是一个过程的两个阶段。广播的发送仍然开始于ContextWrapper的sendBroadcast方法,之所以不是Context,那是因为Context的sendBroadcast是一个抽象方法。和广播的注册过程一样ContextWrapper的sendBroadcast方法仍然什么都不做,只是把事情交给ContextImpl去处理,ContextImpl的sendBroadcast方法源码如下:

public void sendBroadcast(Intent intent) {  warnIfCallingFromSystemProcess();  String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());  try {    intent.prepareToLeaveProcess();    ActivityManagerNative.getDefault().broadcastIntent(      mMainThread.getApplicationThread(), intent, resolvedType, null,      Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, false, false,      getUserId());  } catch (RemoteException e) {  }}

它直接向AMS发起了一个异步请求用于发送广播。那么AMS的broadcastIntent方法的源码如下:

public final int broadcastIntent(IApplicationThread caller,            Intent intent, String resolvedType, IIntentReceiver resultTo,            int resultCode, String resultData, Bundle map,            String requiredPermission, boolean serialized, boolean sticky, int userId) {        synchronized(this) {            intent = verifyBroadcastLocked(intent);                        final ProcessRecord callerApp = getRecordForAppLocked(caller);            final int callingPid = Binder.getCallingPid();            final int callingUid = Binder.getCallingUid();            final long origId = Binder.clearCallingIdentity();            int res = broadcastIntentLocked(callerApp,                    callerApp != null ? callerApp.info.packageName : null,                    intent, resolvedType, resultTo,                    resultCode, resultData, map, requiredPermission, serialized, sticky,                    callingPid, callingUid, userId);            Binder.restoreCallingIdentity(origId);            return res;        }    }

从代码上看,broadcastIntent调用了broadcastIntentLocked方法,但在AMS的broadcastIntentLocked方法里有这么一句:

// By default broadcasts do not go to stopped apps.    intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);

这表示在Android5.0下,默认情况下广播不会发送给已经停止的应用。FLAG_EXCLUDE_STOPPED_PACKAGES的含义是表示 不包含已经停止的应用,这个时候广播不会发送给已经停止的应用。

在broadcastIntentLocked的内部,会根据intent-filter查找出匹配的广播接收者并经过一系列的条件过滤,最终会将满足条件的广播接收者添加到BroadcastQueue中,接着BroadcastQueue将会广播发送给相应的广播接收者。

if ((receivers != null && receivers.size() > 0)                || resultTo != null) {            BroadcastQueue queue = broadcastQueueForIntent(intent);            BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp,                    callerPackage, callingPid, callingUid, requiredPermission,                    receivers, resultTo, resultCode, resultData, map, ordered,                    sticky, false);            if (DEBUG_BROADCAST) Slog.v(                    TAG, "Enqueueing ordered broadcast " + r                    + ": prev had " + queue.mOrderedBroadcasts.size());            if (DEBUG_BROADCAST) {                int seq = r.intent.getIntExtra("seq", -1);                Slog.i(TAG, "Enqueueing broadcast " + r.intent.getAction() + " seq=" + seq);            }            boolean replaced = replacePending && queue.replaceOrderedBroadcastLocked(r);             if (!replaced) {                queue.enqueueOrderedBroadcastLocked(r);                queue.scheduleBroadcastsLocked();            }        }

下面看下BroadcastQueue中广播的发送过程的实现。如下所示:

public void scheduleBroadcastsLocked() {            if (DEBUG_BROADCAST) Slog.v(TAG, "Schedule broadcasts ["                    + mQueueName + "]: current="                    + mBroadcastsScheduled);            if (mBroadcastsScheduled) {                return;            }            mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));            mBroadcastsScheduled = true;        }

BroadcastQueue的scheduleBroadcastsLocked方法并没有立即发送广播,而是发送了一个BROADCAST_INTENT_MSG类型的消息,BroadcastQueue收到消息后会调用processNextBroadcast方法,BroadcastQueue的processNextBroadcast方法对普通广播的处理方式如下:

// First, deliver any non-serialized broadcasts right away.                while (mParallelBroadcasts.size() > 0) {                    r = mParallelBroadcasts.remove(0);                    r.dispatchTime = SystemClock.uptimeMillis();                    r.dispatchClockTime = System.currentTimeMillis();                    final int N = r.receivers.size();                    if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG, "Processing parallel broadcast ["                            + mQueueName + "] " + r);                    for (int i=0; i

可以看到,无序广播存储在mParallelBroadcasts中,系统会遍历mParallelBroadcasts并将其中的广播发送给它们所有接收者,具体的发送过程是通过deliverToRegisteredReceiverLocked方法来实现的。

最终呢,会调用ApplicationThread的scheduleRegisteredReceiver的实现比较简单,它通过InnerReceiver来实现广播的接收。然后InnerReceiver的performReceive方法会调用LoadedApk.ReceiverDispatcher的PerformReceive方法。最终会回调到receiver.onReceive()这个方法。

很显然,这个时候BroadcastReceiver的onReceive方法被执行了,也就是说应用收到广播了,同时,onReceive方法是在广播接收者的主线程中被调用,所以不能做耗时操作,因为是在ApplicationThread的主线程上执行的。

总结

总结一下,Android中应用程序发送广播的过程:

  • 通过sendBroadcast把一个广播通过Binder发送给ActivityManagerService,ActivityManagerService根据这个广播的Action类型找到相应的广播接收器,然后把这个广播放进自己的消息队列中,就完成第一阶段对这个广播的异步分发。
  • ActivityManagerService在消息循环中处理这个广播,并通过Binder机制把这个广播分发给注册的ReceiverDispatcher,ReceiverDispatcher把这个广播放进MainActivity所在线程的消息队列中,就完成第二阶段对这个广播的异步分发。
  • ReceiverDispatcher的内部类Args在MainActivity所在的线程消息循环中处理这个广播,最终是将这个广播分发给所注册的BroadcastReceiver实例的onReceive函数进行处理:

作为Android中四大组件之一的广播,可以应用很多场景的,比如用户异地登陆强制下线,应用开机启动服务,网络状态变化通知等等,掌握好其中的定义,使用方法,背后的注册流程,发送和接收消息流程机制,对于我们在开发时是很有帮助的。

参考信息:

1,http://blog.csdn.net/zuolongsnail/article/details/6450156

2,《第一行代码》

阅读扩展

源于对掌握的Android开发基础点进行整理,罗列下已经总结的文章,从中可以看到技术积累的过程。

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,

转载于:https://www.cnblogs.com/cr330326/p/5787971.html

你可能感兴趣的文章
利用堆实现堆排序&优先队列
查看>>
Mono源码学习笔记:Console类(四)
查看>>
Android学习路线(十二)Activity生命周期——启动一个Activity
查看>>
《Genesis-3D开源游戏引擎完整实例教程-跑酷游戏篇03:暂停游戏》
查看>>
CPU,寄存器,一缓二缓.... RAM ROM 外部存储器等简介
查看>>
windows下编译FreeSwitch
查看>>
git .gitignore 文件不起作用
查看>>
Alan Turing的纪录片观后感
查看>>
c#自定义控件中的事件处理
查看>>
App.config自定义节点读取
查看>>
unity3d根据手机串号和二维码做正版验证
查看>>
二十六、Android WebView缓存
查看>>
django Models 常用的字段和参数
查看>>
linux -- 嵌入式linux下wifi无线网卡驱动
查看>>
SVN使用教程总结
查看>>
SQL中varchar和nvarchar有什么区别?
查看>>
(转)跟我一起写MAKEFILE
查看>>
Linux内存段的分析
查看>>
网卡启动问题
查看>>
Ruby元编程:单元测试框架如何找到测试用例
查看>>