MediaScanner分析

1 MediaScannerService
  多媒体扫描是从MediaScannerService开始的。这是一个单独的package。位于packages/providers/MediaProvider:含以下java文件
java代码:

  1. MediaProvider.java
  2. MediaScannerReceiver.java
  3. MediaScannerService.java
  4. MediaThumbRequest.java
    分析这个目录的Android.mk文件,发现它运行的进程名字就是android.process.media。
    application android:process=android.process.media
    1.1 MediaScannerReceiver
    这个类从BroadcastReceiver中派生,用来接收任务的。
    MediaScannerReceiver extends BroadcastReceiver
    在它重载的onRecieve函数内有以下几种走向:
    java代码:
  5. if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
  6.          // Scan both internal storage
    
  7.         收到”启动完毕“广播后,扫描内部存储
    
  8.          scan(context, MediaProvider.INTERNAL_VOLUME);
    
  9.      } 
    
  10. } else {
  11. if (Intent.ACTION_MEDIA_MOUNTED.equals(action))
  12. {
  13. /收到MOUNT信息后,扫描外部存储
  14. scan(context, MediaProvider.EXTERNAL_VOLUME);
  15. }
  16. else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) )
  17. {
  18. //收到请求要求扫描某个文件,注意不会扫描内部存储上的文件
  19. scanFile(context, path);
  20. }
    下面是它调用的scan函数:
    java代码:
  21. scan(Context context, String volume)
  22. Bundle args = new Bundle();
  23. args.putString(“volume”, volume);
  24. //直接启动MediaScannerService了,
  25. context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    总结:
      MediaScannerReceiver是用来接收任务的,它收到广播后,会启动MediaService进行扫描工作。插拔SD卡的时候会启动媒体扫描。
      下面看看MediaScannerService.
    1.2 MediaScannerService
    MSS标准的从Service中派生下来,
      MediaScannerService extends Service implements Runnable
      //注意:是一个Runnable…,可能有线程之类的东西存在
      下面从Service的生命周期的角度来看看它的工作。
    1.2.1 onCreate
    java代码:
  26. public void onCreate()
  27. PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
  28. mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
  29. //获得电源锁,防止在扫描过程中休眠
  30. //单独搞一个线程去跑扫描工作,防止ANR
  31. Thread thr = new Thread(null, this, “MediaScannerService”);
  32. thr.start();
    1.2.2 onStartCommand
    java代码:
  33. @Override
  34. public int onStartCommand(Intent intent, int flags, int startId){
  35. //注意这个handler,是在另外一个线程中创建的,往这个handler里sendMessage都会在那个线程里边处理
  36. //不明白的可以去查看handler和Looper机制
  37. //这里就是同步机制,等待mServiceHandler在另外那个线程创建完毕
  38. while (mServiceHandler == null) {
  39. synchronized (this) {
  40. try {
  41. wait(100);
  42. } catch (InterruptedException e) {
  43. }
  44. }
  45. }
  46. if (intent == null) {
  47. Log.e(TAG, "Intent is null in onStartCommand: ",new NullPointerException());
  48. return Service.START_NOT_STICKY;
  49. }
  50. Message msg = mServiceHandler.obtainMessage();
  51. msg.arg1 = startId;
  52. msg.obj = intent.getExtras();
  53. //把MediaScannerReceiver发出的消息传递到另外那个线程去处理。
  54. mServiceHandler.sendMessage(msg);
    1.3 run
    java代码:
  55. public void run(){
  56. // reduce priority below other background threads to avoid interfering
  57. // with other services at boot time.
  58. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +Process.THREAD_PRIORITY_LESS_FAVORABLE);
  59. //不明白的去看看Looper和handler的实现
  60. Looper.prepare();
  61. //把这个looper对象设置到线程本地存储
  62. mServiceLooper = Looper.myLooper();
  63. mServiceHandler = new ServiceHandler();
  64. //创建handler,默认会把这个looper
  65. //的消息队列赋值给handler的消息队列,这样往handler中发送消息就是往这个线程的looper发
  66. Looper.loop();
  67. //消息循环,内部会处理消息队列中的消息
  68. //也就是handleMessage函数
  69. }
    上面handler中加入了一个扫描请求(假设是外部存储的),所以要分析handleMessage函数。
    1.4 handleMessage
    java代码:
  70. private final class ServiceHandler extends Handler{
  71. @Override
  72. public void handleMessage(Message msg){
  73. Bundle arguments = (Bundle) msg.obj;
  74. String filePath = arguments.getString(“filepath”);
  75. try {
  76. 这里不讲了
  77. } else {
  78. String volume = arguments.getString(“volume”);
  79. String[] directories = null;
  80. if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
  81. //是扫描内部存储的请求?
  82. // scan internal media storage
  83. directories = new String[] {
  84. Environment.getRootDirectory() + “/media”,
  85. };
  86. }
  87. else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
  88. //是扫描外部存储的请求?获取外部存储的路径
  89. directories = new String[] {
  90. Environment.getExternalStorageDirectory().getPath(),
  91. };
  92. }
  93. if (directories != null) {
  94. //真正的扫描开始了,上面只不过是把存储路径取出来罢了.
  95. scan(directories, volume);
  96. //扫描完了,就把service停止了
  97. stopSelf(msg.arg1);
  98. }
  99. };
    1.5 scan函数
    java代码:
  100. private void scan(String[] directories, String volumeName) {
  101. mWakeLock.acquire();
  102. //下面这三句话很深奥…
  103. //从 getContentResolver获得一个ContentResover,然后直接插入
  104. //根据AIDL,这个ContentResover的另一端是MediaProvider。只要去看看它的insert函数就可以了
  105. //反正这里知道获得了一个扫描URI即可。
  106. ContentValues values = new ContentValues();
  107. values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
  108. Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
  109. Uri uri = Uri.parse(“file://” + directories[0]);
  110. //发送广播,通知扫描开始了
  111. sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
  112. try {
  113. if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
  114. openDatabase(volumeName);
  115. }
  116. //创建真正的扫描器
  117. MediaScanner scanner = createMediaScanner();
  118. //交给扫描器去扫描文件夹 scanDirectories
  119. scanner.scanDirectories(directories, volumeName);
  120. } catch (Exception e) {
  121. Log.e(TAG, “exception in MediaScanner.scan()”, e);
  122. }
  123. //删除扫描路径
  124. getContentResolver().delete(scanUri, null, null);
  125. //通知扫描完毕
  126. sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
  127. mWakeLock.release();
  128. }
    说说上面那个深奥的地方,在MediaProvider中重载了insert函数,insert函数会调用insertInternal函数。
      如下:
    java代码:
  129. private Uri insertInternal(Uri uri, ContentValues initialValues) {
  130. long rowId;
  131. int match = URI_MATCHER.match(uri);
  132. // handle MEDIA_SCANNER before calling getDatabaseForUri()
  133. //刚才那个insert只会走下面这个分支,其实就是获得一个地址….
  134. //太绕了
  135. if (match == MEDIA_SCANNER) {
  136. mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
  137. return MediaStore.getMediaScannerUri();
  138. }
    再看看它创建了什么样的Scanner,这就是MSS中的createMediaScanner

java代码:

  1. private MediaScanner createMediaScanner() {
  2. //下面这个MediaScanner在framework/base/中,待会再分析
  3. MediaScanner scanner = new MediaScanner(this);
  4. //设置当前的区域,这个和字符编码有重大关系。
  5. Locale locale = getResources().getConfiguration().locale;
  6. if (locale != null) {
  7. String language = locale.getLanguage();
  8. String country = locale.getCountry();
  9. String localeString = null;
  10. if (language != null) {
  11. if (country != null) {
  12. //给扫描器设置当前国家和语言。
  13. scanner.setLocale(language + “_” + country);
  14. } else {
  15. scanner.setLocale(language);
  16. }
  17. }
  18. }
  19. return scanner;
  20. }
    至此,MSS的任务完成了。接下来是MediaScanner的工作了。
    1.6 总结
    MSS的工作流程如下:
      1 单独启动一个带消息循环的工作线程。
      2 主线程接收系统发来的任务,然后发送给工作线程去处理。
      3 工作线程接收任务,创建一个MediaScanner去扫描。
      4 MSS顺带广播一下扫描工作启动了,扫描工作完毕了。
    2 MediaScanner
      MediaScanner位置在Framework/base/media下,包括jni和java文件。
      先看看java实现。
      这个类巨复杂,而且和MediaProvider交互频繁。在分析的时候要时刻回到MediaProvider去看看。
    2.1 初始化
    java代码:
  21. public class MediaScanner{
  22. static {
  23. //libmedia_jni.so的加载是在MediaScanner类中完成的
  24. System.loadLibrary(“media_jni”);
  25. native_init();
  26. }
  27. public MediaScanner(Context c) {
  28. native_setup();//调用jni层的初始化,暂时不用看了,无非就是一些
  29. //初始化工作,待会在再进去看看
  30. }
    刚才MSS中是调用scanDirectories函数,我们看看这个。
    2.2 scanDirectories
    java代码:
  31. public void scanDirectories(String[] directories, String volumeName) {
  32. try {
  33. long start = System.currentTimeMillis();
  34. initialize(volumeName);//初始化
  35. prescan(null);//扫描前的预处理
  36. long prescan = System.currentTimeMillis();
  37. for (int i = 0; i < directories.length; i++) {
  38. //扫描文件夹,这里有一个很重要的参数 mClient
  39. // processDirectory是一个native函数
  40. processDirectory(directories[i], MediaFile.sFileExtensions, mClient);
  41. }
  42. long scan = System.currentTimeMillis();
  43. postscan(directories);//扫描后处理
  44. long end = System.currentTimeMillis();
  45. 打印时间,异常处理没了
  46. 下面简单讲讲initialize ,preScan和postScan都干嘛了。
  47. private void initialize(String volumeName) {
  48. //打开MediaProvider,获得它的一个实例
  49. mMediaProvider = mContext.getContentResolver().acquireProvider(“media”);
  50. //得到一些uri
  51. mAudioUri = Audio.Media.getContentUri(volumeName);
  52. mVideoUri = Video.Media.getContentUri(volumeName);
  53. mImagesUri = Images.Media.getContentUri(volumeName);
  54. mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
  55. //外部存储的话,可以支持播放列表之类的东西,搞了一些个缓存池之类的
  56. //如mGenreCache等
  57. if (!volumeName.equals(“internal”)) {
  58. // we only support playlists on external media
  59. mProcessPlaylists = true;
  60. mGenreCache = new HashMap();
    preScan,这个函数很复杂:
      大概就是创建一个FileCache,用来缓存扫描文件的一些信息,例如last_modified等。这个FileCache是从MediaProvider中已有信息构建出来的,也就是历史信息。后面根据扫描得到的新信息来对应更新历史信息。
      postScan,这个函数做一些清除工作,例如以前有video生成了一些缩略图,现在video文件被干掉了,则对应的缩略图也要被干掉。
      另外还有一个mClient,这个是从MediaScannerClient派生下来的一个东西,里边保存了一个文件的一些信息。后续再分析。
    在frameworks/base/media/jni/android_media_MediaScanner.cpp中。
      刚才说到,具体扫描工作是在processDirectory函数中完成的。这个是一个native函数。
    3 MediaScanner JNI层分析
      MediaScanner JNI层内容比较多,单独搞一节分析吧。先看看android_media_MediaScanner这个文件。
    3.1 native_init函数
    java代码:
  61. static void
  62. android_media_MediaScanner_native_init(JNIEnv *env){
  63. jclass clazz;
  64. clazz = env->FindClass(“android/media/MediaScanner”);
  65. //得都JAVA类中mNativeContext这个成员id
  66. fields.context = env->GetFieldID(clazz, “mNativeContext”, “I”);
  67. }
    3.2 native_setup函数
    java代码:
  68. android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz){
  69. //创建MediaScanner对象
  70. MediaScanner *mp = createMediaScanner();
  71. //太变态了,自己不保存这个对象指针.
  72. //却把它设置到java对象的mNativeContext去保存
  73. env->SetIntField(thiz, fields.context, (int)mp);
  74. }
  75. //创建MediaScanner函数
  76. static MediaScanner *createMediaScanner() {
  77. MediaScanner *mp = new StagefrightMediaScanner;
  78. env->SetIntField(thiz, fields.context, (int)mp);
  79. MediaScanner *mp1 = new HelixMediaScanner;
  80. //android 4.4 upgrade can not use helixplayer #ifndef NO_OPENCORE
  81. env->SetIntField(thiz, fields.context1, (int)mp1);
    3.3 processDirectories函数
    c++代码:
  82. android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client){
  83. MediaScanner *mp = getNativeScanner_l(env, thiz); //每次都要回调到JAVA中去取这个Scanner!!
  84. const char *pathStr = env->GetStringUTFChars(path, NULL);
  85. //又在C++这里搞一个client,然后把java的client放到C++Client中去保存
  86. //而且还是栈上的临时变量…
  87. MyMediaScannerClient myClient(env, client);
  88. //scanner扫描文件夹,用得是C++的client
  89. MediaScanResult result = mp->processDirectory(pathStr, myClient);
  90. //然后这里调用StagefrightMediaScanner父类MediaScanner的processDirectory。
  91. }
    1.processDirectory
    frameworks\av\media\libmedia\MediaScanner.cpp
    C++代码:
  92. MediaScanResult MediaScanner::processDirectory(
  93.      const char *path, MediaScannerClient &client) {
    
  94.  int pathLength = strlen(path);
    
  95.  if (pathLength >= PATH_MAX) {
    
  96.      return MEDIA_SCAN_RESULT_SKIPPED;
    
  97.  }
    
  98.  char* pathBuffer = (char *)malloc(PATH_MAX + 1);
    
  99.  if (!pathBuffer) {
    
  100.      return MEDIA_SCAN_RESULT_ERROR;
    
  101. }
    
  102. int pathRemaining = PATH_MAX - pathLength;
    
  103. strcpy(pathBuffer, path);
    
  104. if (pathLength > 0 && pathBuffer[pathLength - 1] != '/') {
    
  105.     pathBuffer[pathLength] = '/';
    
  106.     pathBuffer[pathLength + 1] = 0;
    
  107.     --pathRemaining;
    
  108. }
    
  109. client.setLocale(locale());
    
  110. MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
    
  111. free(pathBuffer);
    
  112. return result;
    
  113. }
  114. doProcessDirectory
    C++代码:
  115. status_t MediaScanner::doProcessDirectory(char path, int pathRemaining, const char extensions,
  116. MediaScannerClient& client, ExceptionCheck exceptionCheck, void* exceptionEnv)
  117. {
  118. //终于看到点希望了
  119. //打开这个文件夹,枚举其中的内容。
  120. //题外话,这个时候FileManager肯定删不掉这个文件夹!!
  121. DIR* dir = opendir(path);
  122. while ((entry = readdir(dir))) {
  123. const char* name = entry->d_name;
  124. //不处理.和…文件夹
  125. if (isDirectory) {
  126. //不处理.开头的文件夹。如果是文件夹,递归调用doProcessDirectory
  127. //深度优先啊!
  128. int err = doProcessDirectory(path, pathRemaining - nameLength - 1, extensions, client, exceptionCheck, exceptionEnv);
  129. if (err) {
  130. LOGE("Error processing ‘%s’ - skipping ", path);
  131. continue;
  132. }
  133. } else if (fileMatchesExtension(path, extensions)) {
  134. //是一个可以处理的文件,交给client处理
  135. client.scanFile(path, statbuf.st_mtime, statbuf.st_size);
  136. //最终调到了MediaScanner.java中MyMediaScannerClient的scanFile实现。
    这里要解释下,刚才createMediaScanner中,明明创建的是StagefrightMediaScanner,为何这里看得是MediaScanner代码呢?
    因为StagefrightMediaScanner从MediaScanner中派生下来的,而且没有重载processDirectory函数。
    processDirctory无非是列举一个目录内容,然后又反回去调用client的scanFile处理了。为何搞这么复杂?只有回去看看C++的client干什么了。
    到这里似乎就没有了,那么扫描后的数据库是怎么更新的呢?为什么要传入一个client进去呢?看来必须得trace到scanner中去才知道了
    3.4 MediaScannerClient—JNI层
      JNI中的这个类是这样的:
    java代码:
  137. class MyMediaScannerClient : public MediaScannerClient
  138. mScanFileMethodID = env->GetMethodID(
  139.                                  mediaScannerClientInterface,
    
  140.                                  "scanFile",
    
  141.                                  "(Ljava/lang/String;JJZZ)V");
    
  142. mHandleStringTagMethodID = env->GetMethodID(
  143.                                  mediaScannerClientInterface,
    
  144.                                 "handleStringTag",                                    "(Ljava/lang/String;Ljava/lang/String;)V");
    
  145. mSetMimeTypeMethodID = env->GetMethodID(
  146.                                 mediaScannerClientInterface,
    
  147.                                 "setMimeType",
    
  148.                                 "(Ljava/lang/String;)V");
    
  149. //这三个接口最终是调用的MediaScanner.java中MyMediaScannerClient的实现。
  150. //这是它的scanFile实现
  151. virtual bool scanFile(const char* path, long long lastModified, long long fileSize){
  152. //再次崩溃了,C++的client调用了刚才传进去的java Client的
  153. //scanFile函数…不过这次还传进去了该文件的路径,最后修改时间和文件大小。
  154. mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
  155. }
    没办法了,只能再去看看MediaScanner.java传进去的那个client了。
    3.5 MediaScannerClient----JAVA层
    这个类在MediaScanner.java中实现。
    java代码:
  156. private class MyMediaScannerClient implements MediaScannerClient:
  157. public void scanFile(String path, long lastModified, long fileSize) {
  158. //调用doScanFile…很烦…
  159. doScanFile(path, null, lastModified, fileSize, false);
  160. //下面是doScanFile
  161. public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {
  162. //预处理,看看之前创建的文件缓存中有没有这个文件
  163. FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);
  164. // rescan for metadata if file was modified since last scan
  165. if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
  166. //如果事先有这个文件的信息,则需要修改一些信息,如长度,最后修改时间等
  167. //真正的扫描文件
  168. //processFile又是jni层的。
  169. processFile(path, mimeType, this);
  170. //扫描完了,做最后处理,保存数据到媒体数据库
  171. endFile(entry, ringtones, notifications, alarms, music, podcasts);
  172. //对应android_media_MediaScanner_processFile函数
  173. android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client){
  174. MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
  175. //无语了,又搞一个 MyMediaScannerClient
  176. MyMediaScannerClient myClient(env, client);
  177. mp->processFile(pathStr, mimeTypeStr, myClient);
  178. }
    关于endFile这里涉及到数据的处理策略,如:
    1.如果没有解析到Artist,则AlbumArtist作为其值
    2.如果没有解析到title(歌曲名),则以文件名(从data字段获取)作为歌曲名,这类文件修改文件名歌曲名也会跟着刷新。有时候在文件管理中改变文件名,music列表中歌曲名没有更新。这是因为绝大部分的音乐文件都能解析到title这个字段。
    3.如果没有解析到album字段,则会以文件所在的文件夹名作为这个字段的值。这里就是为什么在同一路径下的音乐文件如果文件信息中没有album信息,其专辑封面可能一样的原因。因为album_id是根据album和albumartist或是文件所在路径来决定的。album_id是封面信息的唯一标识符,而封面数据的生成是从音频文件解析到的。但如果音乐文件不含封面信息,android会根据一定的策略来生成封面。在MediaProvider.java中的getCompressedAlbumArt()可以看到相关的处理。
    4.还有其他处理可以看endFile()及其最后的MediaProvider.java中的insertFile()的实现代码。
    4 StagefrightMediaScanner
    在frameworks\av\media\libstagefright中
      1. processFile
    C++代码:
  179. MediaScanResult StagefrightMediaScanner::processFile(
  180.      const char *path, const char *mimeType,
    
  181.      MediaScannerClient &client) {
    
  182.  ALOGV("processFile '%s'.", path);
    
  183.  client.setLocale(locale());//涉及到字符编码,
    
  184.  client.beginFile();//MyMediaScannerClient再调用MediaScannerClient.cpp的实现
    
  185. MediaScanResult result = processFileInternal(path, mimeType, client);
  186.  client.endFile();//MyMediaScannerClient再调用MediaScannerClient.cpp的实现
    
  187.  return result;
    
  188. }
  189. processFileInternal
    C++代码:
  190. MediaScanResult StagefrightMediaScanner::processFileInternal(
  191.      const char *path, const char *mimeType,
    
  192.      MediaScannerClient &client) {
    
  193.  const char *extension = strrchr(path, '.');
    
  194.  if (!extension) {
    
  195.      return MEDIA_SCAN_RESULT_SKIPPED;
    
  196.  }
    
  197. if (!FileHasAcceptableExtension(extension)) {
    
  198.     return MEDIA_SCAN_RESULT_SKIPPED;//看文件后缀名是否符合要求,原始的只支持几中格式,新增的需要在table中增加,否则不会解析文件获取不到文件的时长等信息。Size和type在MediaScanner.java的begin file中就赋值了。Title在endFile中会判断如果在解析文件的时候没有获取到会重新获取。也就是问题单中DTS2014041707410中qcp的音频文件时长获取不到的原因。
    
  199. }
    
  200. 。。。。
  201. for (size_t i = 0; i < kNumEntries; ++i) {
  202.     const char *value;
    
  203. if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
  204.         status = client.addStringTag(kKeyMap[i].tag, value);
    
  205. //保存解析到的数据,addStringTag最终会调到MediaScanner.java的handleStringTag接口保存数据。
  206.         if (status != OK) {
    
  207.             return MEDIA_SCAN_RESULT_ERROR;
    
  208.         }
    
  209.     }
    
  210. }
    

因为mtp拷贝中文歌曲的时候老出现乱码,关于字符编码这里说两句:
StagefrightMediaScanner::processFile()是对扫描到的文件中的多媒体文件里的字串数据进行转换处理的过程:

  1. client.setLocale(locale());
    这个函数会根据设置的语言来设置mLocaleEncoding的值,mLocaleEncoding默认值是kEncodingNone,如果语言用户设置了英文,那么中文字串就会因为这个函数导致mLocaleEncoding的值没有变成kEncodingGBK,进而产生乱码。2.从for循环中的client.addStringTag(kKeyMap.tag, value); 可以看到,只有mLocaleEncoding != kEncodingNone才会去对文件中读到的字串进行判断,然后如果不是ascii码才会压进client.beginFile();中创建的名值对(名:如"Aritist",值:如"李宗盛")的栈,否则直接调用handleStringTag()扔给Client,结束处理。
    3.client.endFile();首先对栈里的所有的“值”字串的值进行区间判断,得到这些字串的可能编码,并取“与”,就是不断缩小可能编码范围:
    encoding &= possibleEncodings(mValues->getEntry(i));
    然后判断如果得到的可能编码范围中有mLocaleEncoding的,就以mLocaleEncoding为栈中所有“值”字串的编码进行转换:
    if (encoding & mLocaleEncoding)
    convertValues(mLocaleEncoding);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值