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

来源:互联网 发布:php跟java的区别 商城 编辑:程序博客网 时间:2024/06/07 17:29

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


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做了相关处理防止内存泄露。  所以一般我们用的是第二种。



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给改了回来。     笔者熬夜到两点了,  水平有限源码分析的很浅
,但是看在笔者努力的份上  给点支持,谢谢!!!
        
  
             

     
  

     

     

  

原创粉丝点击