[Innost]: http://blog.csdn.net/Innost/article/details/6083467

=============================================

MediaScanner分析

一 MediaScannerService

多媒体扫描是从MediaScannerService开始的。这是一个单独的package。位于

packages/providers/MediaProvider:含以下java文件

l         MediaProvider.java

l         MediaScannerReceiver.java

l         MediaScannerService.java

l         MediaThumbRequest.java

分析这个目录的Android.mk文件,发现它运行的进程名字就是android.process.media

application android:process=android.process.media

1.1    MediaScannerReceiver

这个类从BroadcastReceiver中派生,用来接收任务的。

MediaScannerReceiver extends BroadcastReceiver

在它重载的onRecieve函数内有以下几种走向:

if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {

// 收到”启动完毕“广播后,扫描内部存储

scan(context, MediaProvider.INTERNAL_VOLUME);

} else {

……….

if (action.equals(Intent.ACTION_MEDIA_MOUNTED) &&

externalStoragePath.equals(path)) {

/收到MOUNT信息后,扫描外部存储

scan(context, MediaProvider.EXTERNAL_VOLUME);

}

else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) &&

path != null && path.startsWith(externalStoragePath + "/")) {

//收到请求要求扫描某个文件,注意不会扫描内部存储上的文件

scanFile(context, path);

…………………………..

}

……下面是它调用的scan函数:

scan(Context context, String volume)

Bundle args = new Bundle();

args.putString("volume", volume);

//直接启动MediaScannerService了,

context.startService(

new Intent(context, MediaScannerService.class).putExtras(args));

总结:

MediaScannerReceiver是用来接收任务的,它收到广播后,会启动MediaService进行扫描工作。

下面看看MediaScannerService.

1.2    MediaScannerService

MSS标准的从Service中派生下来,

MediaScannerService extends Service implements Runnable

//注意:是一个Runnable…,可能有线程之类的东西存在

下面从Service的生命周期的角度来看看它的工作。

1. onCreate

public void onCreate()

PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);

mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

//获得电源锁,防止在扫描过程中休眠

//单独搞一个线程去跑扫描工作,防止ANR

Thread thr = new Thread(null, this, "MediaScannerService");

thr.start();

2. onStartCommand

@Override

public int onStartCommand(Intent intent, int flags, int startId)

{

//注意这个handler,是在另外一个线程中创建的,往这个handler里sendMessage

//都会在那个线程里边处理

//不明白的可以去查看handler和Looper机制

//这里就是同步机制,等待mServiceHandler在另外那个线程创建完毕

while (mServiceHandler == null) {

synchronized (this) {

try {

wait(100);

} catch (InterruptedException e) {

}

}

}

if (intent == null) {

Log.e(TAG, "Intent is null in onStartCommand: ",

new NullPointerException());

return Service.START_NOT_STICKY;

}

Message msg = mServiceHandler.obtainMessage();

msg.arg1 = startId;

msg.obj = intent.getExtras();

//把MediaScannerReceiver发出的消息传递到另外那个线程去处理。

mServiceHandler.sendMessage(msg);

………….

基本上MSR(MediaScannerReceiver)发出的请求都会传到onStartCommand中处理。如果有多个存储的话,也只能一个一个扫描了。

下面看看那个线程的主函数

3. run

public void run()

{

// reduce priority below other background threads to avoid interfering

// with other services at boot time.

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +

Process.THREAD_PRIORITY_LESS_FAVORABLE);

//不明白的去看看Looper和handler的实现

Looper.prepare();//把这个looper对象设置到线程本地存储

mServiceLooper = Looper.myLooper();

mServiceHandler = new ServiceHandler();//创建handler,默认会把这个looper

//的消息队列赋值给handler的消息队列,这样往handler中发送消息就是往这个线程的looper发

Looper.loop();//消息循环,内部会处理消息队列中的消息

//也就是handleMessage函数

}

上面handler中加入了一个扫描请求(假设是外部存储的),所以要分析handleMessage函数。

4. handleMessage

private final class ServiceHandler extends Handler

{

@Override

public void handleMessage(Message msg)

{

Bundle arguments = (Bundle) msg.obj;

String filePath = arguments.getString("filepath");

try {

……… 这里不讲了

} else {

String volume = arguments.getString("volume");

String[] directories = null;

if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {

//是扫描内部存储的请求?

// scan internal media storage

directories = new String[] {

Environment.getRootDirectory() + "/media",

};

}

else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {

//是扫描外部存储的请求?获取外部存储的路径

directories = new String[] {

Environment.getExternalStorageDirectory().getPath(),

};

}

if (directories != null) {

//真正的扫描开始了,上面只不过是把存储路径取出来罢了.

scan(directories, volume);

…..

//扫描完了,就把service停止了

stopSelf(msg.arg1);

}

};

5. scan函数

private void scan(String[] directories, String volumeName) {

mWakeLock.acquire();

//下面这三句话很深奥…

//从 getContentResolver获得一个ContentResover,然后直接插入

//根据AIDL,这个ContentResover的另一端是MediaProvider。只要去看看它的

//insert函数就可以了

//反正这里知道获得了一个扫描URI即可。

ContentValues values = new ContentValues();

values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);

Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

Uri uri = Uri.parse("file://" + directories[0]);

//发送广播,通知扫描开始了

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

try {

if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {

openDatabase(volumeName);

}

//创建真正的扫描器

MediaScanner scanner = createMediaScanner();

//交给扫描器去扫描文件夹  scanDirectories

scanner.scanDirectories(directories, volumeName);

} catch (Exception e) {

Log.e(TAG, "exception in MediaScanner.scan()", e);

}

//删除扫描路径

getContentResolver().delete(scanUri, null, null);

//通知扫描完毕

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));

mWakeLock.release();

}

说说上面那个深奥的地方,在MediaProvider中重载了insert函数,insert函数会调用insertInternal函数。

如下:

private Uri insertInternal(Uri uri, ContentValues initialValues) {

long rowId;

int match = URI_MATCHER.match(uri);

// handle MEDIA_SCANNER before calling getDatabaseForUri()

//刚才那个insert只会走下面这个分支,其实就是获得一个地址….

//太绕了!!!!!

if (match == MEDIA_SCANNER) {

mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);

return MediaStore.getMediaScannerUri();

}

……..

再看看它创建了什么样的Scanner,这就是MSS中的createMediaScanner

private MediaScanner createMediaScanner() {

//下面这个MediaScanner在framework/base/中,待会再分析

MediaScanner scanner = new MediaScanner(this);

//设置当前的区域,这个和字符编码有重大关系。

Locale locale = getResources().getConfiguration().locale;

if (locale != null) {

String language = locale.getLanguage();

String country = locale.getCountry();

String localeString = null;

if (language != null) {

if (country != null) {

//给扫描器设置当前国家和语言。

scanner.setLocale(language + "_" + country);

} else {

scanner.setLocale(language);

}

}

}

return scanner;

}

至此,MSS的任务完成了。接下来是MediaScanner的工作了。

6. 总结

MSS的工作流程如下:

l         1 单独启动一个带消息循环的工作线程。

l         2 主线程接收系统发来的任务,然后发送给工作线程去处理。

l         3 工作线程接收任务,创建一个MediaScanner去扫描。

l         4 MSS顺带广播一下扫描工作启动了,扫描工作完毕了。

二 MediaScanner

MediaScanner位置在

frameworks/base/media/下,包括jni和java文件。

先看看java实现。

这个类巨复杂,而且和MediaProvider交互频繁。在分析的时候要时刻回到MediaProvider去看看。

1.       初始化

public class MediaScanner

{

static {

//libmedia_jni.so的加载是在MediaScanner类中完成的

//这么重要的so为何放在如此不起眼的地方加载???

System.loadLibrary("media_jni");

native_init();

}

public MediaScanner(Context c) {

native_setup();//调用jni层的初始化,暂时不用看了,无非就是一些

//初始化工作,待会在再进去看看

……..

}

刚才MSS中是调用scanDirectories函数,我们看看这个。

2.       scanDirectories

public void scanDirectories(String[] directories, String volumeName) {

try {

long start = System.currentTimeMillis();

initialize(volumeName);//初始化

prescan(null);//扫描前的预处理

long prescan = System.currentTimeMillis();

for (int i = 0; i < directories.length; i++) {

//扫描文件夹,这里有一个很重要的参数 mClient

// processDirectory是一个native函数

processDirectory(directories[i], MediaFile.sFileExtensions, mClient);

}

long scan = System.currentTimeMillis();

postscan(directories);//扫描后处理

long end = System.currentTimeMillis();

…..打印时间,异常处理…没了…

下面简单讲讲initialize ,preScan和postScan都干嘛了。

private void initialize(String volumeName) {

//打开MediaProvider,获得它的一个实例

mMediaProvider = mContext.getContentResolver().acquireProvider("media");

//得到一些uri

mAudioUri = Audio.Media.getContentUri(volumeName);

mVideoUri = Video.Media.getContentUri(volumeName);

mImagesUri = Images.Media.getContentUri(volumeName);

mThumbsUri = Images.Thumbnails.getContentUri(volumeName);

//外部存储的话,可以支持播放列表之类的东西,搞了一些个缓存池之类的

//如mGenreCache等

if (!volumeName.equals("internal")) {

// we only support playlists on external media

mProcessPlaylists = true;

mGenreCache = new HashMap<String, Uri>();

preScan,这个函数很复杂:

大概就是创建一个FileCache,用来缓存扫描文件的一些信息,例如last_modified等。这个FileCache是从MediaProvider中已有信息构建出来的,也就是历史信息。后面根据扫描得到的新信息来对应更新历史信息。

postScan,这个函数做一些清除工作,例如以前有video生成了一些缩略图,现在video文件被干掉了,则对应的缩略图也要被干掉。

另外还有一个mClient,这个是从MediaScannerClient派生下来的一个东西,里边保存了一个文件的一些信息。后续再分析。

刚才说到,具体扫描工作是在processDirectory函数中完成的。这个是一个native函数。

在frameworks/base/media/jni/android_media_MediaScanner.cpp中。

三  MediaScanner JNI层分析

MediaScanner JNI层内容比较多,单独搞一节分析吧。

先看看android_media_MediaScanner这个文件。

1. native_init函数,jni对应的函数如下

static void

android_media_MediaScanner_native_init(JNIEnv *env)

{

jclass clazz;

clazz = env->FindClass("android/media/MediaScanner");

//得都JAVA类中mNativeContext这个成员id

fields.context = env->GetFieldID(clazz, "mNativeContext", "I");

//不熟悉JNI的自己去学习下吧

}

3.       native_setup函数,jni对应函数如下:

android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)

{

//创建MediaScanner对象

MediaScanner *mp = createMediaScanner();

//太变态了,自己不保存这个对象指针.

//却把它设置到java对象的mNativeContext去保存

env->SetIntField(thiz, fields.context, (int)mp);

}

//创建MediaScanner函数

static MediaScanner *createMediaScanner() {

#if BUILD_WITH_FULL_STAGEFRIGHT

..

//使用google自己的

return new StagefrightMediaScanner;

#endif

#ifndef NO_OPENCORE

//使用opencore提供的

….

return new PVMediaScanner();

#endif

4.       processDirectories函数,jni对应如下:

android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client)

{

MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

//每次都要回调到JAVA中去取这个Scanner!!

………

const char *pathStr = env->GetStringUTFChars(path, NULL);

const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);

…….

//又在C++这里搞一个client,然后把java的client放到C++Client中去保存

//而且还是栈上的临时变量..

MyMediaScannerClient myClient(env, client);

//scanner扫描文件夹,用得是C++的client

mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);

env->ReleaseStringUTFChars(path, pathStr);

env->ReleaseStringUTFChars(extensions, extensionsStr);

}

到这里似乎就没有了,那么扫描后的数据库是怎么更新的呢?为什么要传入一个client进去呢?看来必须得trace到scanner中去才知道了。

四 PVMediaScanner

这个在frameworks/av/media/libmedia/MediaScanner.cpp中。

1.       processDirectory

status_t MediaScanner::processDirectory(const char *path, const char* extensions,

MediaScannerClient& client, ExceptionCheck exceptionCheck, void* exceptionEnv)

{

InitializeForThread();

int error = 0;

status_t result = PVMFSuccess;

….

//调用client的设置区域函数

client.setLocale(mLocale);

//扫描文件夹,咋还没开始??

result = doProcessDirectory(pathBuffer, pathRemaining, extensions, client, exceptionCheck, exceptionEnv);

..

2.       doProcessDirectory

status_t MediaScanner::doProcessDirectory(char *path, int pathRemaining, const char* extensions,

MediaScannerClient& client, ExceptionCheck exceptionCheck, void* exceptionEnv)

{

…终于看到点希望了

//打开这个文件夹,枚举其中的内容。

//题外话,这个时候FileManager肯定删不掉这个文件夹!!

DIR* dir = opendir(path);

while ((entry = readdir(dir))) {

const char* name = entry->d_name;

//不处理.和..文件夹

if (isDirectory) {

……..

//不处理.开头的文件夹。如果是文件夹,递归调用doProcessDirectory

//深度优先啊!

int err = doProcessDirectory(path, pathRemaining - nameLength - 1, extensions, client, exceptionCheck, exceptionEnv);

if (err) {

LOGE("Error processing '%s' - skipping/n", path);

continue;

}

} else if (fileMatchesExtension(path, extensions)) {

//是一个可以处理的文件,交给client处理

//彻底疯掉了….这是干嘛呢???

client.scanFile(path, statbuf.st_mtime, statbuf.st_size);

这里要解释下,刚才createMediaScanner中,明明创建的是PVMediaScanner,为何这里看得是MediaScanner代码呢?

l         因为PVMediaScanner从MediaScanner中派生下来的,而且没有重载processDirectory函数

l         Eclaire没有PVMediaScanner这样的东西,估计是froyo又改了点啥吧。

FT,processDirctory无非是列举一个目录内容,然后又反回去调用client的scanFile处理了。为何搞这么复杂?只有回去看看C++的client干什么了。

3.       MediaScannerClient---JNI层

JNI中的这个类是这样的:

class MyMediaScannerClient : public MediaScannerClient

//这是它的scanFile实现

virtual bool scanFile(const char* path, long long lastModified, long long fileSize)

{

//再次崩溃了,C++的client调用了刚才传进去的java Client的

//scanFile函数…不过这次还传进去了该文件的路径,最后修改时间和文件大小。

mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);

…..想死,,,

}

没办法了,只能再去看看MediaScanner.java传进去的那个client了。

4.       MediaScannerClient----JAVA层

这个类在MediaScanner.java中实现。

private class MyMediaScannerClient implements MediaScannerClient:

public void scanFile(String path, long lastModified, long fileSize) {

//调用doScanFile..很烦..

doScanFile(path, null, lastModified, fileSize, false);

//下面是doScanFile

public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {

//预处理,看看之前创建的文件缓存中有没有这个文件

FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);

// rescan for metadata if file was modified since last scan

if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {

//如果事先有这个文件的信息,则需要修改一些信息,如长度,最后修改时间等

…..

//真正的扫描文件

processFile(path, mimeType, this);

//扫描完了,做最后处理

endFile(entry, ringtones, notifications, alarms, music, podcasts);

processFile又是jni层的。

对应android_media_MediaScanner_processFile函数

android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)

{

MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

//无语了,又搞一个  MyMediaScannerClient

MyMediaScannerClient myClient(env, client);

mp->processFile(pathStr, mimeTypeStr, myClient);

…}

第一次到PVMediaScanner中来了

status_t PVMediaScanner::processFile(const char *path, const char* mimeType, MediaScannerClient& client)

{

status_t result;

InitializeForThread();

//调用client的beginFile,估计是做一些啥预处理

client.setLocale(locale());

client.beginFile();

//LOGD("processFile %s mimeType: %s/n", path, mimeType);

const char* extension = strrchr(path, '.');

if (extension && strcasecmp(extension, ".mp3") == 0) {

result = parseMP3(path, client);//client又传进去了

…根据后缀名去扫描….

}

client.endFile();

到parseMP3去看看。这个在

static PVMFStatus parseMP3(const char *filename, MediaScannerClient& client)

{//这个函数太专业了,和编解码有关,我们重点关注client在这里干什么了

//原来,解析器从文件中解析出一个信息,就调用client的addStringTag

if (!client.addStringTag("duration", buffer))

….

addStringTag在JNI的client中处理。

这个MediaScannerClient是在opencore中的那个MediaScanner.cpp中实现的,而android_media_MediaScanner.cpp中的是MyMediaScannerClient,从MediaScannerClient派生下来的

bool MediaScannerClient::addStringTag(const char* name, const char* value)

{

if (mLocaleEncoding != kEncodingNone) {

//字符串编码之类的转换。不详述了

bool nonAscii = false;

const char* chp = value;

char ch;

while ((ch = *chp++)) {

if (ch & 0x80) {

nonAscii = true;

break;

}

}

//如果不是ASCII编码的话,内部先保存一下这些个tag信息

//待会扫描完后再集中做一次字符串编码转换

if (nonAscii) {

// save the strings for later so they can be used for native encoding detection

mNames->push_back(name);

mValues->push_back(value);

return true;

}

// else fall through

}

//调用子类的handleStringTag

return handleStringTag(name, value);

}

class MyMediaScannerClient : public MediaScannerClient{

//调用到子类的handleStringTag了

virtual bool handleStringTag(const char* name, const char* value)

{

//又传递到JAVA层的handleStringTag来处理

//麻木了..

mEnv->CallVoidMethod(mClient, mHandleStringTagMethodID, nameStr, valueStr);

}

JAVA层

MediaScannerService中的MyMediaScannerClient类

public void handleStringTag(String name, String value) {

//下层扫描的文件tag信息,全部处理后赋值给java层这个MyScannerClient了

例如MP3的title,专辑名等等。

….

int num = parseSubstring(value, 0, 0);

mTrack = (num * 1000) + (mTrack % 1000);

} else if (name.equalsIgnoreCase("duration")) {

mDuration = parseSubstring(value, 0, 0);

} else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {

mWriter = value.trim();

到这里了,还没写到数据库呢?啥时候更新数据库?看来是在client.endFile()中了。

但是这个endClient并没有调用到JAVA层去。那在哪里结束呢?

还记得JAVA中的doScanFile函数吗,对了,这个endFile就是在那里直接由JAVA调用的。

private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,

boolean alarms, boolean music, boolean podcasts)

throws RemoteException {

// update database

Uri tableUri;

boolean isAudio = MediaFile.isAudioFileType(mFileType);

boolean isVideo = MediaFile.isVideoFileType(mFileType);

boolean isImage = MediaFile.isImageFileType(mFileType);

….

//来了一个新文件,直接插入数据库

result = mMediaProvider.insert(tableUri, values);

//或者更新数据库

mMediaProvider.update(result, values, null, null);

这回真算是完了。

5.流程总结

l         MediaScanner(MS)调用scanDirectories中的processDirectory,进入到JNI层

l         JNI调用PVMediaScanner的processDirectory

l         PVMediaScanner的processDirectory为目录下的文件调用MyMediaScannerClient的scanFile

l         MyMediaScannerClient

Android MediaScanner 详尽分析的更多相关文章

  1. Android MediaScanner:(一)MediaScanner总体架构

    Android MediaScanner:(一)MediaScanner总体架构 分类: Android android.multimedia2012-05-19 18:29 5050人阅读 评论(5 ...

  2. 自定义View系列教程03--onLayout源码详尽分析

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  3. 自定义View系列教程02--onMeasure源码详尽分析

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  4. Android APP性能分析方法及工具

    近期读到<Speed up your app>一文.这是一篇关于Android APP性能分析.优化的文章.在这篇文章中,作者介绍他的APP分析优化规则.使用的工具和方法.我觉得值得大家借 ...

  5. Android之mtklog分析

    Android之mtklog分析 [海外场测反馈][xxx]动态测试时对比机xxxx拨打测试机xxxxx自动挂断电话 工作中遇到一个掉话的问题,需要分析log,log比较大,我也没法上传,就简答的讲讲 ...

  6. android用户界面详尽教程实例

    android用户界面详尽教程实例 1.android用户界面之AlarmManager教程实例汇总http://www.apkbus.com/android-48405-1-1.html2.andr ...

  7. Android源码分析-全面理解Context

    前言 Context在android中的作用不言而喻,当我们访问当前应用的资源,启动一个新的activity的时候都需要提供Context,而这个Context到底是什么呢,这个问题好像很好回答又好像 ...

  8. cocos2d-x for android:SimpleGame分析

    cocos2d-x for android:SimpleGame分析 作为cocos2d-x的标配DEMO,SimpleGame可算是给入门学cocos2d-x的俺们这些新手门学习的对象了,那么来分析 ...

  9. Android内存机制分析1——了解Android堆和栈

    //----------------------------------------------------------------------------------- Android内存机制分析1 ...

随机推荐

  1. JavaScript(10)——Ajax以及跨域处理

    Ajax以及跨域处理 哈哈哈,终于写到最后一章了.不过也还没有结束,说,不要为了学习而学习,恩.我是为了好好学习而学习呀.哈哈哈.正在尝试爱上代码,虽然有一丢丢的难,不过,我相信我会的! [Ajax] ...

  2. Tenured 区并发垃圾回收器CMS介绍

    当使用CMS收集器时,当开始进行收集时,old代的收集过程如下所示:1,首先jvm根据-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingO ...

  3. Chapter 1 First Sight——29

    I was anxious not to be late for class on my first day. 在我第一天上学的时候我非常焦虑我会上课迟到. One of my new acquain ...

  4. ASP.NET 读数据库绑定到 TreeView 递归方式

    <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs& ...

  5. 深入理解JAVA的多态性[转]

    昨天看到一个关于多态性的帖子,参考了回帖者的理解,加入了一些自己的看法,整理出来供大家参考,不一定完全正确,欢迎大家批评指正. (一)相关类 class A ...{         public S ...

  6. react中文API解读一(快速开始)

    记下自己的react学习之路 ,官方文档写的很详尽,学起来应该比较简单 官方文档地址:react.http://reactjs.cn/react/docs/getting-started.html 1 ...

  7. hdu_1348_Wall(凸包)

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=1348 题意:让你求n个点的凸包,凸包离点的距离为l 题解:就凸包周长+一个半径为l的圆周长 #incl ...

  8. NSNotificationCenter消息通信机制

    作用:NSNotificationCenter是专门供程序中不同类间的消息通信而设置的. 注册通知:即要在什么地方接受消息 [[NSNotificationCenter defaultCenter] ...

  9. Linux(centos5.0+)unison+inotify-tools触发式双向自动同步

    192.168.1.11是server1, 192.168.1.22是server2. [1]安装inotify-tools   各大linux发行版本都有inotify-tools软件包,建议通过y ...

  10. 登录验证的js;JS验证邮箱 验证密码

    var auth_email = 0, auth_pwd = 0, auth_captcha = 0, url_ajax = "/protected/user_ajax.php"; ...