android 使用相机拍照以及FileProvider源码浅析

调用系统相机拍照有两种方式:


ONE:  使用Intent传递Bitmap

    button.setOnClickListener(new View.OnClickListener() {
       public void onClick(View v) {
           Intent intent=new Intent("android.media.action.IMAGE_CAPTURE");
           startActivityForResult(intent,1);
       		}

               为一个button 设置点击事件,隐式intent的action为"android.media.action.IMAGE_CAPTURE"

     同样应该为其设置回调方法。。

   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode,resultCode,data);
        if(resultCode==RESULT_OK){
            if(requestCode==1){
                Bundle  bundler=data.getExtras();
                Bitmap bitmap=(Bitmap) bundler.get("data");    
                iamgeview.setImageBitmap(bitmap);   //自定义一个ImageView接受Bitmap
            }
        }
   }

             相机的Activity会将Bitmap对象序列化后存放Bundler里 。大概代码: 

               Bundle bundle=new Bundle();
               bundle.putParcelable("data", object);  //object即是序列化后的Bitmap对象
               intent.putExtras(bundle);

这里不需要权限,因为我们只是通过intent打开相机的activity而打开相机的activity并没有权限。      这种方法得到的Bitmap只是缩略图,画质非常的差,android做了相关处理防止内存泄露。  所以一般我们用的是第二种。


扫描二维码关注公众号,回复: 2533279 查看本文章


TWO:   使用Intent传递URI:

二者不同的是前者直接在两个Activity之间传递Bitmap,而后者是发送一个URI给相机的Acitivity后让其把Bitmap数据直接保存到URI对应的文件里, 

               然后我们的app再直接访问这个文件,这样就不存在内存泄露了。 

      button.setOnClickListener(new View.OnClickListener() {
         @Override
          public void onClick(View v) {
            File file=new File(getExternalCacheDir().getPath(),"Camera");
                if (file.exists()){
                    file.delete();
                }
            try {
               file.createNewFile();
            } catch (IOException e) {
               e.printStackTrace();
            }
            Uri image_uri=Uri.fromFile(file);
            Intent intent=new Intent("android.media.action.IMAGE_CAPTURE");
            intent.putExtra(MediaStore.EXTRA_OUTPUT,image_uri);
            startActivityForResult(intent,1);
    }
});		
            getExternalCacheDir()   //   得到系统给app分配的外置存储空间文件夹:0/Android/data/包名/cache
	
   我们在这里创建了一个名为Camera的文件。然后通过Uri.fromFile(file)得到文件的uri,然后将其存放在一个"key"为MediaStore.EXTRA_OUTPUT
  这个必须是固定格式不然相机Activity得不到正确的数据。    这个key的本身的值为"output"。继续修改回调方法。
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode,resultCode,data);
            if(resultCode==RESULT_OK){
            if(requestCode==1){
                File file=new File(getExternalCacheDir(),"Camera");
                    if(file.exists()){
                        try {
                            imageview.setImageBitmap(BitmapFactory.decodeStream(new FileInputStream(file)));
                        } catch (FileNotFoundException e) {
                        }
                    }
             }
        }
   }
  		将这个file文件读取后放到imageview中,然后就ok了。  
  为什么这里任然要执行onActivityResult 应为我们发起的intent目标Activity本身就是一个最终会执行setResult(),然后finish()。
 不同上述的是 前者无intent.putExtra(MediaStore.EXTRA_OUTPUT,image_uri);所以猜想源码肯定是会读取这个值,如果读不到则把Bitmap数据包装成intent
发回来,能读到则把Bitmap数据存放到uri对应的File文件,而intent的返回值为null。
 
 
  注意 intent传输uri方法在sdk版本低于24也就是android7.0时使用,而对于7.0及以上我们则需要用到内容提供器(FileProvider),
	详细阅读这里:http://blog.csdn.net/djy1992/article/details/72533310   
	  为什么出现这种问题,其实就是Uri.fromFile(file)方法不让用了,我们不能从File转换Uri了,所以需要FileProvider。
从继承关系看	
       public class FileProvider extends ContentProvider 
FileProvider是继承ContentProvider所以也需要在Mainfest中定义
  <provider
    android:name="android.support.v4.content.FileProvider"    
    android:authorities="com.android.shuai"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file"></meta-data>
</provider>

    name:为定值"android.support.v4.content.FileProvider"。相当自定义的ContentProvider,FileProvider为重写的类。

     authorities:自定义值, 
     android:exported="false": 不可更改否则报错   
     android:grantUriPermissions="true">  :不可更改否则报错
     <meta-data>标签内的name:为定值, 

 resource:新建XML目录新建XML文件 

   <?xml version="1.0" encoding="utf-8"?>
   <paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="LinLangTianXia" path="" />
   </paths>
    <external-path> : 首地址,通俗讲就是地址从这里开始。 
    path="" :  首地址后的详细地址:
	臂如标签就是上述的<external-path> ,path=""; 那么我们的内容提供器可以提供的转换File地址为:
"/Storage/"。只要要转换的File文件被这个目录包括就可以。
如果path="A" 那就是"/Storage/A/"。
   同理<external-path>标签还有很多;像<files-path><cache-path>这种,如果要转换的File文件不在上述内就会报错。
  name则是上述地址的映射,比如上述"/Storage/A/" name="LinLangTianXia" 最后为/LinLangTianXia/, 也就是说将我们的真实地址隐藏了。
 
 
	举个例子    :7.0以下得到的Uri为:file:///storage/emulated/0/Android/data/listen.myapplication/cache/Camera: 
		     7.0以上得到的Uri为: content://com.android.shuai/LinLangTianXia/Android/data/listen.myapplication/cache/Camera:
     				格式为:content://authorities/name/+未映射的地址。 authorities告诉程序是那个内容提供者, name映射真实地址。 
当然这个转换过程不用我们来操作看代码:
    if(Build.VERSION.SDK_INT>=24){
       image_uri=FileProvider.getUriForFile(Main.this,"com.android.shuai",file); 
      } 
	调用一下FileProvider.getUriForFile(Context,authorities,file)就ok了, 这个方法会自动帮我们封装Uri,这个file就是上述我们说的file
必须在其内部,否则报错。     其他不变同7.0以下下,   通俗讲FileProvider只是帮我们修改了Uri使其更安全而已。	  当然阅读到这会有个疑问它们具体的过程到底是怎样的,我们看源码:
   public static Uri getUriForFile(Context context, String authority, File file) {
       final PathStrategy strategy = getPathStrategy(context, authority);
       return strategy.getUriForFile(file);    //看到strategy对象可以帮我们转换File为Uri
    }         
	进入PathStrategy发现是个接口实现它的是类SimplePathStrategy,继续进:


static class SimplePathStrategy implements PathStrategy {
    private final String mAuthority;
    private final HashMap<String, File> mRoots = new HashMap<String, File>();

    public SimplePathStrategy(String authority) {
        mAuthority = authority;    //传入的authority。

    }
    @Override
    public Uri getUriForFile(File file) {
        String path;
        try {
            path = file.getCanonicalPath();   //相当file.getPath();
        } catch (IOException e) {
            throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
        }

        // Find the most-specific root path
        Map.Entry<String, File> mostSpecific = null;
        for (Map.Entry<String, File> root : mRoots.entrySet()) {
            final String rootPath = root.getValue().getPath();
            if (path.startsWith(rootPath) && (mostSpecific == null
                    || rootPath.length() > mostSpecific.getValue().getPath().length())) {
                mostSpecific = root;
            }
        }

        if (mostSpecific == null) {
            throw new IllegalArgumentException(
                    "Failed to find configured root that contains " + path);
        }

        // Start at first char of path under root
        final String rootPath = mostSpecific.getValue().getPath();
        if (rootPath.endsWith("/")) {
            path = path.substring(rootPath.length());
        } else {
            path = path.substring(rootPath.length() + 1);
        }

        // Encode the tag and path separately
        path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
        return new Uri.Builder().scheme("content")
                .authority(mAuthority).encodedPath(path).build();
    }
}                                         //截取部分
       
    看这行: return new Uri.Builder().scheme("content")
     	           .authority(mAuthority).encodedPath(path).build();  是不是就是我们上述的结果。
	看到相信你肯定会有疑惑,既然Uri被这种方式修改那在接收端会不会存在另一种方式给它修改回去,没错,接着看剩下源码:
 
 
class SimplePathStrategy {
private final String mAuthority ;
private final HashMap<String, File> mRoots = new HashMap<String, File>();
public SimplePathStrategy(String authority) {
mAuthority = authority;
}
/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
public void addRoot(String name, File root) {
if (TextUtils. isEmpty (name)) {
throw new IllegalArgumentException( "Name must not be empty" );
}
try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots .put(name, root);
}
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf( '/' , 1 );
final String tag = Uri. decode (path.substring( 1 , splitIndex));
path = Uri. decode (path.substring(splitIndex + 1 ));
final File root = mRoots .get(tag);
if (root == null ) {
throw new IllegalArgumentException( "Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException( "Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException( "Resolved path jumped beyond configured root" );
}
return file}
}
看到这些代码是不是有点头疼,别急,慢慢来: 笔者将其提取然后直接调用getFileForUri报错提示root为空,所以上述的addRoot要先调用,
那这个addroot方法又在那里调用呢?
public SimplePathStrategy parsePathStrategy(Context context, String authority)
        throws IOException, XmlPullParserException {
    final SimplePathStrategy strat = new SimplePathStrategy(authority);    //这里创建了SimplePathStrategy对象

 
    final ProviderInfo info = context.getPackageManager()
            .resolveContentProvider(authority, PackageManager.GET_META_DATA);
    final XmlResourceParser in = info.loadXmlMetaData(
            context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
    if (in == null) {
        throw new IllegalArgumentException(
                "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
    }

    int type;
    while ((type = in.next()) != END_DOCUMENT) {
        if (type == START_TAG) {
            final String tag = in.getName();

            final String name = in.getAttributeValue(null, ATTR_NAME);
            String path = in.getAttributeValue(null, ATTR_PATH);

            File target = null;
            if (TAG_ROOT_PATH.equals(tag)) {
                target = DEVICE_ROOT;
            } else if (TAG_FILES_PATH.equals(tag)) {
                target = context.getFilesDir();
            } else if (TAG_CACHE_PATH.equals(tag)) {
                target = context.getCacheDir();
            } else if (TAG_EXTERNAL.equals(tag)) {
                target = Environment.getExternalStorageDirectory();
            } else if (TAG_EXTERNAL_FILES.equals(tag)) {
                File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
                if (externalFilesDirs.length > 0) {
                    target = externalFilesDirs[0];
                }
            } else if (TAG_EXTERNAL_CACHE.equals(tag)) {
                File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
                if (externalCacheDirs.length > 0) {
                    target = externalCacheDirs[0];
                }
            }

            if (target != null) {
                strat.addRoot(name, buildPath(target, path));       //这里调用了addRoot方法();
            }
        }
    }
    return strat;
}
        ok  我们的逻辑也算梳理顺了,  在相机源码中肯定先调用了parsePathStrategy()方法得到了SimplePathStrategy
对象,然后直接调用了它的getFileForUri()方法于是就将Uri给改了回来。     笔者熬夜到两点了,  水平有限源码分析的很浅
,但是看在笔者努力的份上  给点支持,谢谢!!!
 
  
        		
	
	  

		
 
 
 
 
	
	             

     
  

     

     
	

  

猜你喜欢

转载自blog.csdn.net/qq_36043263/article/details/78783080