精华内容
下载资源
问答
  • ContentProvider

    2021-03-19 14:31:06
    文章目录一、作用二、具体使用2.1、统一资源标识符(URI) 一、作用 进程间进行数据交互 & 共享,即跨进程通信 ...(外界进程通过URI找到对应的ContentProvider & 其中的数据,在进行数据操作) 具体使用 ...

    一、作用

    进程间进行数据交互 & 共享,即跨进程通信
    在这里插入图片描述

    二、具体使用

    2.1、统一资源标识符(URI)

    • 定义:Uniform Resource Identifier,即统一资源标识符
    • 作用:唯一标识ContentProvide & 其中数据。(外界进程通过URI找到对应的ContentProvider & 其中的数据,在进行数据操作)
    • 具体使用
      在这里插入图片描述
     	private static final String SCHEMA = "content://";//主题名称
        private static final String AUTHORITY = "com.test.provider";//授权信息
        private static Uri uri_provider = Uri.parse(SCHEMA + AUTHORITY);//设置Uri
    

    2.2、MIME数据类型

    • 作用:指定某个扩展名的文件用某种应用程序来打开如指定“.html”文件采用“text”应用程序打开、指定“.pdf”文件采用“flash”应用程序打开
    • 具体作用:
      根据ContextProvider根据URI返回MIME类型
    ContentProvider.getType(uri);
    

    MIME类型组成每种MIME类型由2部分组成(MIME= 类型 + 子类型)

    text/html(类型/子类型)
    

    MIME类型有2种形式:
    形式1: 单条记录 vnd.android.cursor.item/自定义
    形式2: 多条记录 vnd.android.cursor.dir/自定义

    注意:
    1.vnd:标识父类型和子类型具有非标准的、特定形式、
    2. 父类型已固定好(即不能更改),只能区别是单挑还是多条记录
    3. 子类型可自定义

    2.3、ContentProvider类

    1. 组织数据方式
    • ContentProvider 主要以表格的形式组织数据(同时也支持文件数据,只是表格形式用得比较多)
    • 每个表格中包含多张表格,每张表格包含行&列,分别对应记录 & 字段(同数据库一样)
    1. 主要方法
    • 进程间共享数据的本质是:添加、删除、查询、修改数据
      核心方法:
    public Uri insert(Uri uri, ContentValues values)
    // 外部进程向 ContentProvider 中添加数据
    public int delete(Uri uri, String selection, String[] selectio
    nArgs)
    // 外部进程 删除 ContentProvider 中的数据
    public int update(Uri uri, ContentValues values, String select
    ion, String[] selectionArgs)
    // 外部进程更新 ContentProvider 中的数据
    public Cursor query(Uri uri, String[] projection, String selec
    tion, String[] selectionArgs, String sortOrder)
    // 外部应用 获取 ContentProvider 中的数据
    // 注:
    // 1. 上述4个方法由外部进程回调,并运行在ContentProvider进程的Binder
    线程池中(不是主线程)
    // 2. 存在多线程并发访问,需要实现线程同步
    // a. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要
    ,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无
    法进行线程同步
    // b. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步
    <-- 2个其他方法 -->
    public boolean onCreate()
    // ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvi
    der时 由系统进行调用
    // 注:运行在ContentProvider进程的主线程,故不能做耗时操作
    public String getType(Uri uri)
    // 得到数据类型,即返回当前 Url 所代表数据的MIME类型
    
    
    • Android 为常见的数据(如通讯录、日程表等)提供了内置了默认的ContentProvider

    2.4、ContentResolver类

    • 作用:统一管理不同 ContentProvider 间的操作
      1. 通过 URI 即可操作 不同的 ContentProvider 中的数据
      2. 外部进程通过 ContentResolver 类,从而与ContentProvider类进行交互
    • 具体使用
    // 外部进程向 ContentProvider 中添加数据
    public Uri insert(Uri uri, ContentValues values)
    // 外部进程 删除 ContentProvider 中的数据
    public int delete(Uri uri, String selection, String[] selectionA
    rgs)
    // 外部进程更新 ContentProvider 中的数据
    public int update(Uri uri, ContentValues values, String selectio
    n, String[] selectionArgs)
    // 外部应用 获取 ContentProvider 中的数据
    public Cursor query(Uri uri, String[] projection, String selecti
    on, String[] selectionArgs, String sortOrder)
    
    public class DBOpenHelper extends SQLiteOpenHelper {
        private static final String DBName = "DbProvider.db";
        private static final int DB_VERSION = 1;
        private static Context context;
        private static DBOpenHelper instance;
        public static final Object CONTACTS_TABLE_LOCK = new Object();
    
        public DBOpenHelper(@Nullable Context context) {
            super(context, DBName, null, DB_VERSION);
        }
    
        public static DBOpenHelper getInstance(Context context) {
            if (null == instance) {
                synchronized (DBOpenHelper.class) {
                    if (null == instance) {
                        instance = new DBOpenHelper(context);
                    }
                }
            }
    
            return instance;
        }
    
        @Override
        public void onCreate(SQLiteDatabase db) {
            createTables(db);
        }
    
        /**
         * 创建表
         *
         * @param db
         */
        private void createTables(SQLiteDatabase db) {
            db.execSQL(SQL_CONTENTS_TABLE.SQL_CREATE_USER_TABLE);
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            if (oldVersion != newVersion) {
                createTables(db);
            }
        }
    
        /**
         * 用户数据存储类
         */
        public static final class DbUser {
            public static final String TABLE_NAME = "user";
            public static final String _ID = "_id";
            public static final String PROVIDER_NAME = "name";
            public static final String PROVIDER_PWD = "password";
            public static final String PROVIDER_CREATE_DATE = "create_date";
    
            //ContentValues 与 HashMap 是存储机制,
            // ContentValues 只能存储基本数据类型,
            // HashMap 可以存储对象也可以存储基本类型
            public static ContentValues parseContentValues(Users users) {
                ContentValues values = new ContentValues();
                values.put(PROVIDER_NAME, users.getName());
                values.put(PROVIDER_PWD, users.getPassword());
                values.put(PROVIDER_CREATE_DATE, users.getCreateDate());
                return values;
            }
        }
    
        /**
         * 数据库创建表格接口
         */
        static interface SQL_CONTENTS_TABLE {
            public static final String SQL_CREATE_USER_TABLE = "CREATE TABLE "
                    + DbUser.TABLE_NAME + " ("
                    + DbUser.PROVIDER_NAME + " TEXT,"
                    + DbUser.PROVIDER_PWD + " TEXT,"
                    + DbUser.PROVIDER_CREATE_DATE + " INTEGER"
                    + ");";
        }
    
        /***
         * 插入用户集合
         * @param users
         */
        public void insertUsers(List<Users> users) {
            Uri contacts = MyContentProvider.ContactsUri.URI_CONTACTS;
            ArrayList<ContentProviderOperation> ops = new ArrayList<>();
            for (Users user : users) {
                ContentValues values = DbUser.parseContentValues(user);
                ContentProviderOperation build = ContentProviderOperation.newInsert(contacts).withValues(values).build();
                ops.add(build);
            }
            synchronized (CONTACTS_TABLE_LOCK) {
                doResolverApplyBatch(contacts, ops);
            }
        }
    
        private void doResolverApplyBatch(Uri contacts, ArrayList<ContentProviderOperation> ops) {
            try {
                context.getContentResolver().applyBatch(contacts.getAuthority(), ops);
            } catch (OperationApplicationException e) {
                e.printStackTrace();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 插入用户信息
         *
         * @param users
         */
        public void insertUser(Users users) {
            ContentValues values = DbUser.parseContentValues(users);
            context.getContentResolver().insert(MyContentProvider.ContactsUri.URI_CONTACTS, values);
        }
    
        /***
         * 通过用户名称删除用户
         * @param name
         */
        public void deleteUserByName(String name) {
            context.getContentResolver().delete(MyContentProvider.ContactsUri.URI_CONTACTS,
                    DbUser.PROVIDER_NAME + "=?", new String[]{name});
        }
    
        /**
         * 通过用户名次及密码删除用户
         * @param name
         * @param pwd
         */
        public void deleteUserByNameAndPwd(String name, String pwd) {
            context.getContentResolver().delete(MyContentProvider.ContactsUri.URI_CONTACTS,
                    DbUser.PROVIDER_NAME + "=? And " + DbUser.PROVIDER_PWD + "=?", new String[]{name, pwd});
        }
    
    
        /**
         * 查询所有用户
         *
         * @param group
         * @param name
         * @return
         */
        public ArrayList<Users> queryUserAll(String group, String name) {
            ArrayList<Users> beans = new ArrayList<>();
            Cursor cursor = null;
            cursor = context.getContentResolver().query(MyContentProvider.ContactsUri.URI_CONTACTS,
                    null, null, null, null);
            if (null != cursor && cursor.moveToFirst()) {
                do {
                    Users users = new Users();
                    users.setCreateDate(cursor.getLong(cursor.getColumnIndex(DbUser.PROVIDER_CREATE_DATE)));
                    users.setName(cursor.getString(cursor.getColumnIndex(DbUser.PROVIDER_NAME)));
                    users.setPassword(cursor.getString(cursor.getColumnIndex(DbUser.PROVIDER_PWD)));
                    beans.add(users);
                } while (cursor.moveToNext());
                cursor.close();
            }
            return beans;
        }
    
    }
    
    

    MyContentProvider.java 类

    public class MyContentProvider extends ContentProvider {
    
        private static final String SCHEMA = "content://";//主题名称
        private static final String AUTHORITY = "com.test.provider";//授权信息
        private static Uri uri_provider = Uri.parse(SCHEMA + AUTHORITY);//设置Uri
        public static final String PARAM_GROUP_BY = "groupBy";
        public static final String PARAM_LIMIT = "limit";
        private static final int MATCH_CODE = 200;///匹配成功后的匹配码
        private DBOpenHelper openHelper = null;
        private static UriMatcher matcher = null;
    
        static interface ContactsUri {
            static final String PATH = "User";
            public static final Uri URI_CONTACTS = Uri.withAppendedPath(uri_provider, PATH);
    
        }
    
        static {
            //匹配不成功返回NO_MATCH(-1)
            matcher = new UriMatcher(UriMatcher.NO_MATCH);
            matcher.addURI(AUTHORITY, ContactsUri.PATH, MATCH_CODE);//添加我们需要的Uri
        }
    
        @Override
        public boolean onCreate() {
            openHelper = DBOpenHelper.getInstance(getContext());
            return true;
        }
    
        @Nullable
        @Override
        public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
            SQLiteDatabase db = openHelper.getReadableDatabase();
            int match = matcher.match(uri);
            SQLiteQueryBuilder query = new SQLiteQueryBuilder();
            String limit = uri.getQueryParameter(PARAM_LIMIT);
            String groupBy = uri.getQueryParameter(PARAM_GROUP_BY);
            switch (match) {
                case MATCH_CODE:
                    query.setTables(DBOpenHelper.DbUser.TABLE_NAME);
                    break;
                default:
                    throw new UnsupportedOperationException("Unknown URL " + uri.toString());
            }
            Cursor cursor = query.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
            return cursor;
        }
    
        @Nullable
        @Override
        public String getType(@NonNull Uri uri) {
            return null;
        }
    
        @Nullable
        @Override
        public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
            int match = matcher.match(uri);
            SQLiteDatabase dbWritable = openHelper.getWritableDatabase();
            long id = -1;
            switch (match) {
                case MATCH_CODE:
                    id = dbWritable.insert(DBOpenHelper.DbUser.TABLE_NAME, null, values);
                    break;
                default:
                    throw new UnsupportedOperationException("Unknown insert URI " + uri);
            }
            if (id >= 0) {
                getContext().getContentResolver().notifyChange(uri, null);
                return ContentUris.withAppendedId(uri, id);
            }
            return null;
        }
    
        @Override
        public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
            int match = matcher.match(uri);
            SQLiteDatabase db = openHelper.getWritableDatabase();
            int id = 0;
            switch (match) {
                case MATCH_CODE:
                    id = db.delete(DBOpenHelper.DbUser.TABLE_NAME, selection, selectionArgs);
                    break;
                default:
                    throw new UnsupportedOperationException("Unknown delete URI " + uri);
    
            }
            if (id > 0) {
                getContext().getContentResolver().notifyChange(uri, null);
            }
            return id;
        }
    
        @Override
        public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
            int match = matcher.match(uri);
            SQLiteDatabase db = openHelper.getWritableDatabase();
            int updateId = 0;
            switch (match) {
                case MATCH_CODE:
                    updateId = db.update(DBOpenHelper.DbUser.TABLE_NAME, values, selection, selectionArgs);
                    break;
                default:
                    throw new UnsupportedOperationException("Unknown update URI " + uri);
    
            }
            if (updateId > 0) {
                getContext().getContentResolver().notifyChange(uri, null);
            }
            return updateId;
        }
    }
    
    

    Android 提供了3个用于辅助 ContentProvide 的工具类:1.ContentUris;2.UriMatcher;3.ContentObserver

    2.5、ContentUris 类

    • 作用:操作URI
    • 具体使用核心方法有两个:“withAppendedId()” & “parseId()”
     // withAppendedId()作用:向URI追加一个id
    Uri uri = Uri.parse("content://cn.scu.myprovider/user")
    Uri resultUri = ContentUris.withAppendedId(uri, 7);
    // 最终生成后的Uri为:content://cn.scu.myprovider/user/7
    // parseId()作用:从URL中获取ID
    Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")
    long personid = ContentUris.parseId(uri);
    //获取的结果为:7
    

    2.6、UriMatcher 类

    • 作用:1. “ContentProvider” 中注册 URI;2.根据 URI 匹配 ContentProvider 中对应的数据表。
    • 具体使用:
    // 步骤1:初始化UriMatcher对象
    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
    //常量UriMatcher.NO_MATCH = 不匹配任何路径的返回码
    // 即初始化时不匹配任何东西
    // 步骤2:在ContentProvider 中注册URI(addURI())
    int URI_CODE_a = 1int URI_CODE_b = 2;
    matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a);
    matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b);
    // 若URI资源路径 = content://cn.scu.myprovider/user1 ,则返回注册码U
    RI_CODE_a
    // 若URI资源路径 = content://cn.scu.myprovider/user2 ,则返回注册码U
    RI_CODE_b
    // 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(
    match())
    @Override
    public String getType (Uri uri){
    Uri uri = Uri.parse(" content://cn.scu.myprovider/user1");
    switch (matcher.match(uri)) {
    // 根据URI匹配的返回码是URI_CODE_a
    // 即matcher.match(uri) == URI_CODE_a
    case URI_CODE_a:
    return tableNameUser1;
    // 如果根据URI匹配的返回码是URI_CODE_a,则返回ContentProvider
    中的名为tableNameUser1的表
    case URI_CODE_b:
    return tableNameUser2;
    // 如果根据URI匹配的返回码是URI_CODE_b,则返回ContentProvider
    中的名为tableNameUser2的表
    }
    }
    

    2.7、ContentObserver类

    • 定义:内容观察者
    • 作用:观察Uri 引起ContentProvider 中的数据变化 & 通知外界(即访问该数据访问者)当“ContentProvider” 中的数据发生变化(增、删、改)时,就会触发该 “ContentObserver”类
    
        private Handler handler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                Log.i(TAG, "handleMessage");
            }
        };
    
        private ContentObserver contentObserver = new ContentObserver(handler) {
            @Override
            public boolean deliverSelfNotifications() {
                Log.i(TAG, "deliverSelfNotifications");
                return super.deliverSelfNotifications();
            }
    
            @Override
            public void onChange(boolean selfChange) {
                super.onChange(selfChange);
                ArrayList<Users> users = mInstance.queryUserAll();
                for (Users user : users) {
                    Log.i(TAG, user.toString() + "\n");
    
                }
            }
        };
    
       @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_life_cycle);
            mInstance = DBOpenHelper.getInstance(this);
            getContentResolver().registerContentObserver(MyContentProvider.ContactsUri.URI_CONTACTS,
                    true,
                    contentObserver);
            ((Button) findViewById(R.id.btn_next)).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    ArrayList<Users> uList = mInstance.queryUserAll();
                    if (uList.size() > 0) {
                        Users users = new Users();
                        users.setName("张三" + uList.size());
                        users.setPassword("1234");
                        users.setCreateDate(System.currentTimeMillis());
                        mInstance.insertUser(users);
                    } else {
                        Users users = new Users();
                        users.setName("张三");
                        users.setPassword("1234");
                        users.setCreateDate(System.currentTimeMillis());
                        mInstance.insertUser(users);
                    }
    
                }
            });
            ((Button) findViewById(R.id.btn_back)).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    ArrayList<Users> uList = mInstance.queryUserAll();
                    if (uList.size() > 0) {
                        mInstance.deleteUserByName("张三" + (uList.size() - 1));
                    } else {
                        Log.i(TAG, "没有了");
                    }
                }
            });
        }
    

    打印数据

    LifeCycleActivity: Users{id=0, name='张三', password='1234', createDate=1616396903943}
    Users{id=0, name='张三1', password='1234', createDate=1616397031345}
    Users{id=0, name='张三2', password='1234', createDate=1616397187407}
    

    2.8、进程间进行数据共享

    在这里插入图片描述myApp 应用进程

    • permission 声明权限
    • exported 允许其他进程访问
        <permission
            android:name="com.test.PROVIDER"
            android:protectionLevel="normal" />
      <provider
                android:name=".MyContentProvider"
                android:permission="com.test.PROVIDER"
                android:exported="true"
                android:authorities="com.test.provider" />
    

    Other 应用进程
    声明 __myApp 应用进程 __权限

        <uses-permission android:name="com.test.PROVIDER" />
    
      private fun select(): ArrayList<Users> {
            val beans = ArrayList<Users>()
            var uri: Uri = Uri.parse("content://com.test.provider/User")
            var cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
            if (null != cursor && cursor.moveToFirst()) {
                do {
                    var u = Users();
                    u.createDate = cursor.getLong(cursor.getColumnIndex("create_date"))
                    u.name = cursor.getString(cursor.getColumnIndex("name"))
                    u.password = cursor.getString(cursor.getColumnIndex("password"))
                    beans.add(u)
                } while (cursor.moveToNext())
            }
            return beans;
        }
    

    打印

    I/TAG: [Users
    {id=0, name='张三', password='1234', createDate=1616396903943}, 
    Users
    {id=0, name='张三1', password='1234', createDate=1616397031345},
     Users
     {id=0, name='张三2', password='1234', createDate=1616397187407}]
    
    

    三、优点

    3.1、安全

    ContentProvider 为应用间的数据交互提供了一个安全的环境:允许把自己的应
    用数据根据需求开放给 其他应用 进行 增、删、改、查,而不用担心因为直接开放
    数据库权限而带来的安全问题

    3.2、访问简单 & 高效

    对比与其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同:

    • 采用文件方式 对外共享数据,需要进行文件操作读写数据;
    • 采用 Sharedpreferences 共享数据,需要使用 sharepreferences API读写数据

    这使得访问数据变得复杂 & 难度大

    • 而采用 ContentProvider 方式,其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单 & 高效。(如一开始数据存储方式采用 SQLite 数据库,后来把数据库换成 MongoDB,也不会对上层数据 ContentProvider 使用代码产生影响)

    四、总结

    ContentProvider
    定义: 内容提供者,是Android四大组件之一
    作用: 进程间进行数据交互 & 共享、即跨进程通信
    原理: Binder机制
    优点: 1.安全;2.访问简单 & 高效(统一了数据访问)
    总结:
    1.将存放在Android 的数据,以安全的方式进行封装;
    2. 最终提供统一的存储 & 获取数据的接口供其他进程调用,从而实现跨进程的数据共享

    展开全文
  • ContentProvider流程详解

    2021-12-05 21:36:49
    ContentProvider是一种内容共享型组件,它通过Binder向其他组件乃至其他应用提供数据。当ContentProvider所在的进程启动时,ContentProvider会同事启动并被发布到AMS中。需要注意的是,这个时候ContentProvider的...

    ContentProvider是一种内容共享型组件,它通过Binder向其他组件乃至其他应用提供数据。当ContentProvider所在的进程启动时,ContentProvider会同事启动并被发布到AMS中。需要注意的是,这个时候ContentProvider的onCreate要先于Application的onCreate而执行,这是四大组件中一个少有的现象。

    当一个应用启动时,入口方法为ActivityThread的main方法,main方法是一个静态方法,在main方法中会创建ActivityThread的实例并创建主线程的消息队列,然后再ActivityThread的attach方法中会远程调用AMS的attachApplication方法并将ApplicationThread对象提供给AMS。ApplicationThread是一个Binder对象,它的Binder接口是IApplicationThread,主要用于ActivityThread和AMS之间的通信。在AMS的attachApplication方法中,会调用ApplicationThread的bindApplication方法,bindApplicaiton的逻辑会经过ActivityThread中的Handler切换到ActivityThread中去执行,具体的方法是handleBindApplication,在这个方法中ActivityThread会创建Applicaiton对象并加载ContentProvider。需要注意的是ActivityThread会先加载ContentProvider,然后再调用Applicaiton的onCreate方法。
    ContentProvider启动后,外界就可以通过它所提供的增删改查四个接口来操作数据源,即insert、delete、update、query。这四个方法都是通过Binder来调用的,外界无法直接访问ContentProvider,它只能通过AMS根据Uri来获取对应的ContentProvider的Binder接口IContentProvider,然后在通过IContentProvider来访问ContentProvider中的数据源。
    一般来说ContentProvider都应该是单实例的。具体是由android:multiprocess属性来决定的,当android:multiprocess为false时,ContentProvider为单实例,这也是默认值,当multiprocess为true时,为多实例,这个时候在每个调用者的进程中都存在一个ContentProvider对象。由于在实际开发中,并未发现多实例的ContentProvider的具体使用场景,因此我们可以简单认为ContentProvider都是单实例的。

    启动流程

    访问ContentProvider需要通过ContentResolver,ContentResolver是一个抽象类,通过Context的getContentResolver方法获取的实际上是ApplicationContentResolver对象。ApplicationContentResolver类继承了ContentResolver并实现了ContentResolver中的抽象方法。当ContentProvider所在的进程未启动时,第一次访问它时就会触发ContentProvider的创建。通过ContentProvider的四个方法中的任何一个都可以触发ContentProvider的启动过程。这里选择query方法。

    ContentResolver.java

    @Override
        public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
                @Nullable String[] projection, @Nullable Bundle queryArgs,
                @Nullable CancellationSignal cancellationSignal) {
            Objects.requireNonNull(uri, "uri");
    
            try {
                if (mWrapped != null) {
                    return mWrapped.query(uri, projection, queryArgs, cancellationSignal);
                }
            } catch (RemoteException e) {
                return null;
            }
    
            IContentProvider unstableProvider = acquireUnstableProvider(uri);
            if (unstableProvider == null) {
                return null;
            }
            IContentProvider stableProvider = null;
            Cursor qCursor = null;
            try {
                long startTime = SystemClock.uptimeMillis();
    
                ICancellationSignal remoteCancellationSignal = null;
                if (cancellationSignal != null) {
                    cancellationSignal.throwIfCanceled();
                    remoteCancellationSignal = unstableProvider.createCancellationSignal();
                    cancellationSignal.setRemote(remoteCancellationSignal);
                }
                try {
                    qCursor = unstableProvider.query(mPackageName, mAttributionTag, uri, projection,
                            queryArgs, remoteCancellationSignal);
                } catch (DeadObjectException e) {
                    // The remote process has died...  but we only hold an unstable
                    // reference though, so we might recover!!!  Let's try!!!!
                    // This is exciting!!1!!1!!!!1
                    unstableProviderDied(unstableProvider);
                    stableProvider = acquireProvider(uri);
                    if (stableProvider == null) {
                        return null;
                    }
                    qCursor = stableProvider.query(mPackageName, mAttributionTag, uri, projection,
                            queryArgs, remoteCancellationSignal);
                }
                if (qCursor == null) {
                    return null;
                }
    
                // Force query execution.  Might fail and throw a runtime exception here.
                qCursor.getCount();
                long durationMillis = SystemClock.uptimeMillis() - startTime;
                maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);
    
                // Wrap the cursor object into CursorWrapperInner object.
                final IContentProvider provider = (stableProvider != null) ? stableProvider
                        : acquireProvider(uri);
                final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
                stableProvider = null;
                qCursor = null;
                return wrapper;
            } catch (RemoteException e) {
                // Arbitrary and not worth documenting, as Activity
                // Manager will kill this process shortly anyway.
                return null;
            } finally {
                if (qCursor != null) {
                    qCursor.close();
                }
                if (cancellationSignal != null) {
                    cancellationSignal.setRemote(null);
                }
                if (unstableProvider != null) {
                    releaseUnstableProvider(unstableProvider);
                }
                if (stableProvider != null) {
                    releaseProvider(stableProvider);
                }
            }
        }
    
    public final IContentProvider acquireUnstableProvider(Uri uri) {
        if (!SCHEME_CONTENT.equals(uri.getScheme())) {
            return null;
        }
        String auth = uri.getAuthority();
        if (auth != null) {
            return acquireUnstableProvider(mContext, uri.getAuthority());
        }
        return null;
    }
    

    首先会通过acquireUnstableProvider——>acquireProvider方法获取ContentProvider。该方法是个抽象方法,具体实现在ApplicationContentResolver类中,是ContextImpl的一个静态内部类。

    	private static final class ApplicationContentResolver extends ContentResolver {
    		...
            @Override
            @UnsupportedAppUsage
            protected IContentProvider acquireProvider(Context context, String auth) {
                return mMainThread.acquireProvider(context,
                        ContentProvider.getAuthorityWithoutUserId(auth),
                        resolveUserIdFromAuthority(auth), true);
            }
    		...
    

    ApplicationContentResolver的acquireProvider方法并没有处理任何逻辑,它直接调用了ActivityThread的acquireProvider方法。

    ActivityThread.java

      public final IContentProvider acquireProvider(
                Context c, String auth, int userId, boolean stable) {
            //从缓存中获取ContentProvider实例对象
            final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
            if (provider != null) {
                return provider;
            }
    
            // There is a possible race here.  Another thread may try to acquire
            // the same provider at the same time.  When this happens, we want to ensure
            // that the first one wins.
            // Note that we cannot hold the lock while acquiring and installing the
            // provider since it might take a long time to run and it could also potentially
            // be re-entrant in the case where the provider is in the same process.
            ContentProviderHolder holder = null;
            try {
            	//当缓存中没有ContentProvider实例时吗,需要通过AMS来创建一个ContentProvider实例
                synchronized (getGetProviderLock(auth, userId)) {
                    holder = ActivityManager.getService().getContentProvider(
                            getApplicationThread(), c.getOpPackageName(), auth, userId, stable);
                }
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
            if (holder == null) {
                if (UserManager.get(c).isUserUnlocked(userId)) {
                    Slog.e(TAG, "Failed to find provider info for " + auth);
                } else {
                    Slog.w(TAG, "Failed to find provider info for " + auth + " (user not unlocked)");
                }
                return null;
            }
    
            // Install provider will increment the reference count for us, and break
            // any ties in the race.
            holder = installProvider(c, holder, holder.info,
                    true /*noisy*/, holder.noReleaseNeeded, stable);
            return holder.provider;
        }
    

    上面的代码先会从缓存中查找是否已经存在目标ContentProvider,如果存在就直接返回。ActivityThread通过mProviderMap来存储已经启动的ContentProvider对象。如果ContentProvider没有启动,那么就发送一个进程间请求给AMS让其启动目标ContentProvider,最后再通过installProvider方法来修改引用计数。
    ContentProvider被启动时会伴随着进程的启动,在AMS中,首先会启动ContentProvider所在的进程,然后在启动ContentProvider。启动进程是由AMS的startProcessLocked方法来完成的,其内部主要是通过Process的start方法来向Zygote申请fork一个新进程的启动,新进程启动后会反射调用ActivityThread的main方法。

    public static void main(String[] args) {
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    
            // Install selective syscall interception
            AndroidOs.install();
    
            // CloseGuard defaults to true and can be quite spammy.  We
            // disable it here, but selectively enable it later (via
            // StrictMode) on debug builds, but using DropBox, not logs.
            CloseGuard.setEnabled(false);
    
            Environment.initForCurrentUser();
    
            // Make sure TrustedCertificateStore looks in the right place for CA certificates
            final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
            TrustedCertificateStore.setDefaultUserDirectory(configDir);
    
            // Call per-process mainline module initialization.
            initializeMainlineModules();
    
            Process.setArgV0("<pre-initialized>");
    
            Looper.prepareMainLooper();
    
            // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
            // It will be in the format "seq=114"
            long startSeq = 0;
            if (args != null) {
                for (int i = args.length - 1; i >= 0; --i) {
                    if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                        startSeq = Long.parseLong(
                                args[i].substring(PROC_START_SEQ_IDENT.length()));
                    }
                }
            }
            ActivityThread thread = new ActivityThread();
            thread.attach(false, startSeq);
    
            if (sMainThreadHandler == null) {
                sMainThreadHandler = thread.getHandler();
            }
    
            if (false) {
                Looper.myLooper().setMessageLogging(new
                        LogPrinter(Log.DEBUG, "ActivityThread"));
            }
    
            // End of event ActivityThreadMain.
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            Looper.loop();
    
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    

    main方法是个静态方法,首先会创建ActivityThread实例并调用attch来进行一系列的初始化,接着就开始进行消息循环了。ActivityThread的attach方法会将ApplicationThread对象通过AMS的attachApplication方法跨进程传递给AMS,最终AMS会完成ContentProvider的创建。

    ActivityThread.java

    @UnsupportedAppUsage
    private void attach(boolean system, long startSeq) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        if (!system) {
            android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
                                                    UserHandle.myUserId());
            RuntimeInit.setApplicationObject(mAppThread.asBinder());
            final IActivityManager mgr = ActivityManager.getService();
            try {
            	//在AMS中注册ApplicationThread
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
            // Watch for getting close to heap limit.
            BinderInternal.addGcWatcher(new Runnable() {
                @Override public void run() {
                    if (!mSomeActivitiesChanged) {
                        return;
                    }
                    Runtime runtime = Runtime.getRuntime();
                    long dalvikMax = runtime.maxMemory();
                    long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
                    if (dalvikUsed > ((3*dalvikMax)/4)) {
                        if (DEBUG_MEMORY_TRIM) Slog.d(TAG, "Dalvik max=" + (dalvikMax/1024)
                                + " total=" + (runtime.totalMemory()/1024)
                                + " used=" + (dalvikUsed/1024));
                        mSomeActivitiesChanged = false;
                        try {
                            ActivityTaskManager.getService().releaseSomeActivities(mAppThread);
                        } catch (RemoteException e) {
                            throw e.rethrowFromSystemServer();
                        }
                    }
                }
            });
        } else {
            // Don't set application object here -- if the system crashes,
            // we can't display an alert, we just want to die die die.
            android.ddm.DdmHandleAppName.setAppName("system_process",
                    UserHandle.myUserId());
            try {
                mInstrumentation = new Instrumentation();
                mInstrumentation.basicInit(this);
                ContextImpl context = ContextImpl.createAppContext(
                        this, getSystemContext().mPackageInfo);
                mInitialApplication = context.mPackageInfo.makeApplication(true, null);
                mInitialApplication.onCreate();
            } catch (Exception e) {
                throw new RuntimeException(
                        "Unable to instantiate Application():" + e.toString(), e);
            }
        }
    
        ViewRootImpl.ConfigChangedCallback configChangedCallback
                = (Configuration globalConfig) -> {
            synchronized (mResourcesManager) {
                // TODO (b/135719017): Temporary log for debugging IME service.
                if (Build.IS_DEBUGGABLE && mHasImeComponent) {
                    Log.d(TAG, "ViewRootImpl.ConfigChangedCallback for IME, "
                            + "config=" + globalConfig);
                }
    
                // We need to apply this change to the resources immediately, because upon returning
                // the view hierarchy will be informed about it.
                if (mResourcesManager.applyConfigurationToResourcesLocked(globalConfig,
                        null /* compat */)) {
                    updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
                            mResourcesManager.getConfiguration().getLocales());
    
                    // This actually changed the resources! Tell everyone about it.
                    if (mPendingConfiguration == null
                            || mPendingConfiguration.isOtherSeqNewer(globalConfig)) {
                        mPendingConfiguration = globalConfig;
                        sendMessage(H.CONFIGURATION_CHANGED, globalConfig);
                    }
                }
            }
        };
        ViewRootImpl.addConfigCallback(configChangedCallback);
    }
    

    AMS的attachApplication方法调用了attachApplicationLocked方法,attachApplicationLocked中又调用了ApplicationThread的bindApplication。

    thread.bindApplication(processName, appInfo, providerList,
                            instr2.mClass,
                            profilerInfo, instr2.mArguments,
                            instr2.mWatcher,
                            instr2.mUiAutomationConnection, testMode,
                            mBinderTransactionTrackingEnabled, enableTrackAllocation,
                            isRestrictedBackupMode || !normalMode, app.isPersistent(),
                            new Configuration(app.getWindowProcessController().getConfiguration()),
                            app.compat, getCommonServicesLocked(app.isolated),
                            mCoreSettingsObserver.getCoreSettingsLocked(),
                            buildSerial, autofillOptions, contentCaptureOptions,
                            app.mDisabledCompatChanges);
    
    @Override
    public final void bindApplication(String processName, ApplicationInfo appInfo,
            ProviderInfoList providerList, ComponentName instrumentationName,
            ProfilerInfo profilerInfo, Bundle instrumentationArgs,
            IInstrumentationWatcher instrumentationWatcher,
            IUiAutomationConnection instrumentationUiConnection, int debugMode,
            boolean enableBinderTracking, boolean trackAllocation,
            boolean isRestrictedBackupMode, boolean persistent, Configuration config,
            CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
            String buildSerial, AutofillOptions autofillOptions,
            ContentCaptureOptions contentCaptureOptions, long[] disabledCompatChanges) {
        if (services != null) {
            if (false) {
                // Test code to make sure the app could see the passed-in services.
                for (Object oname : services.keySet()) {
                    if (services.get(oname) == null) {
                        continue; // AM just passed in a null service.
                    }
                    String name = (String) oname;
    
                    // See b/79378449 about the following exemption.
                    switch (name) {
                        case "package":
                        case Context.WINDOW_SERVICE:
                            continue;
                    }
    
                    if (ServiceManager.getService(name) == null) {
                        Log.wtf(TAG, "Service " + name + " should be accessible by this app");
                    }
                }
            }
    
            // Setup the service cache in the ServiceManager
            ServiceManager.initServiceCache(services);
        }
    
        setCoreSettings(coreSettings);
    
        AppBindData data = new AppBindData();
        data.processName = processName;
        data.appInfo = appInfo;
        data.providers = providerList.getList();
        data.instrumentationName = instrumentationName;
        data.instrumentationArgs = instrumentationArgs;
        data.instrumentationWatcher = instrumentationWatcher;
        data.instrumentationUiAutomationConnection = instrumentationUiConnection;
        data.debugMode = debugMode;
        data.enableBinderTracking = enableBinderTracking;
        data.trackAllocation = trackAllocation;
        data.restrictedBackupMode = isRestrictedBackupMode;
        data.persistent = persistent;
        data.config = config;
        data.compatInfo = compatInfo;
        data.initProfilerInfo = profilerInfo;
        data.buildSerial = buildSerial;
        data.autofillOptions = autofillOptions;
        data.contentCaptureOptions = contentCaptureOptions;
        data.disabledCompatChanges = disabledCompatChanges;
        sendMessage(H.BIND_APPLICATION, data);
    }
    

    ActivityThread的bindApplicaiton会发送一个BIND_APPLICATION类型的消息给handler H,它收到消息后会调用ActivityThread的handleBindActivity方法,完成了Application的创建以及ContentProvider的创建。

    1. 创建ContextImpl和Instrumentation
    2. 创建Application对象
    3. 启动当前进程的ContentProvider并调用其onCreate方法
    4. 调用Applicaiton的onCreate方法
    展开全文
  • ContentProvider导致App闪退问题分析 一、背景 今日有部分线上用户反馈打开App后会偶现闪退,但奇怪的是我们在捞取相关设备的日志查看时却没有发现任何的异常栈信息,最后还是依靠系统的BugReport文件才发现了一些...

    一、背景

          近日有部分线上用户反馈打开App后会偶现闪退,但奇怪的是我们在捞取相关设备的App日志时却没有发现任何的异常栈信息,这给我们定位问题带来了不小的难度。没有明确的异常栈信息,那就只能找规律了。从大量的日志中我们发现进程挂掉的前面一小段时间里,都出现了与操作某一个ContentProvider组件相关的日志信息。通过推测+尝试,最终成功在本地复现了闪退现象,并抓取到了关键的系统日志。日志如下:

    2021-03-30 16:28:35.661 1091-1459/? I/ActivityManager: Killing 20972:com.test.demo1/u0a71 (adj 100): depends on provider com.test.demo2/.provider.SharedProvider in dying proc com.test.demo2 (adj 0)
    2021-03-30 16:28:35.668 1091-1459/? I/ActivityManager: Killing 22561:com.test.demo2/u0a1222 (adj 0): timeout publishing content providers
    2021-03-30 16:28:35.674 1091-1459/? D/ActivityManager: proc ProcessRecord{2522dce 22561:com.test.demo2/u0a1222} already removed. so we skip next process.
    2021-03-30 16:28:35.676 1091-3793/? E/ActivityManager: Timeout waiting for provider com.test.demo2/11222 for provider com.test.demo2.SharedProviderAuthority providerRunning=false caller=com.test.demo1/10071
    

          com.test.demo1在进程启动时会去查询com.test.demo2实现的一个ContentProvider组件,该组件名为SharedProvider,对应的authority是com.test.demo2.SharedProviderAuthority。com.test.demo1就是出现闪退问题的App。根据日志我们可以总结出以下几个信息:

    1. 系统将要杀死com.test.demo1进程,因为它依赖了将死进程com.test.demo2的ContentProvider
    2. com.test.demo2由于注册ContentProvider超时正在被杀死

          也就是说com.test.demo2注册ContentProvider超时除了导致自身被杀以外,同时还导致了调用方com.test.demo1被杀。

          这个就有点超出我们以往的认知了,一般来说调用方进程和被调用方进程相互之间都是独立的,被调用方进程出现崩溃等问题不应该会影响到调用方的逻辑。com.test.demo1原先预想的实现逻辑也是如此,优先去查com.test.demo2的SharedProvider中的数据,如果取到了就展示该数据,如果没取到就展示默认的数据。不管demo2的进程是否存活,是否发生崩溃等,我们都不希望它影响到调用方demo1的进程。

    二、结合源码分析原因

    2.1 初步定义关键代码位置

          分析这种问题时,我们可以先通过关键日志定位到导致问题发生的关键代码,再从关键代码处往上层层剖析,这样往往能达到事半功倍的效果。很明显,"depends on provider"是日志中最为关键的一个词,我们直接在安卓Framework的源码中搜一下,就会找到如下的关键代码。这段代码位于ActivityManagerService.java中

    private final boolean removeDyingProviderLocked(ProcessRecord proc,
            ContentProviderRecord cpr, boolean always) {
        ...
        for (int i = cpr.connections.size() - 1; i >= 0; i--) {
            ...
            ProcessRecord capp = conn.client;
            conn.dead = true;
            // 关键就在于conn.stableCount > 0 这个条件
            if (conn.stableCount > 0) {
            	// 由于三方应用的进程基本都不是常驻进程,因此都会满足以下这个if条件,从而走到kill逻辑中
                if (!capp.isPersistent() && capp.thread != null
                        && capp.pid != 0
                        && capp.pid != MY_PID) {
                    capp.kill("depends on provider "
                            + cpr.name.flattenToShortString()
                            + " in dying proc " + (proc != null ? proc.processName : "??")
                            + " (adj " + (proc != null ? proc.setAdj : "??") + ")",
                            ApplicationExitInfo.REASON_DEPENDENCY_DIED,
                            ApplicationExitInfo.SUBREASON_UNKNOWN,
                            true);
                }
                ...
        }
        ...
    }
    

          从方法名来看,removeDyingProviderLocked应该是AMS用来移除将死进程的Provider信息的。并且在移除这些Provider信息的时候会根据一些条件来判断是否要杀死调用方。接下去我们可以分两个方向来分析,一个是removeDyingProviderLocked(ProcessRecord proc, ContentProviderRecord cpr, boolean always)这个方法何时会被调用,另一个则是conn.stableCount在满足怎样的条件时会大于0。

    2.1 removeDyingProviderLocked()方法的调用逻辑

          探究removeDyingProviderLocked(ProcessRecord proc, ContentProviderRecord cpr, boolean always)方法调用逻辑其实就是就是在探究ContentProvider的注册和查询流程,注册方以及调用方是如何和system_server做交互的。这部分内容比较多,不熟悉ContentProvider原理的同学可以看这篇文章: 理解ContentProvider原理

          借用这篇文章中的一张概括图继续往下分析

    在这里插入图片描述

          导致demo1闪退的关键就在system_server到provider process的交互过程中。

          AMS首先会调用getContentProviderImp()方法尝试获取target provider。如果ContentProvider还未被注册(即所在进程还未启动),则会调用startProcessLocked()方法来启动server process,对应开头的例子就是指com.test.demo2进程

        private ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
                String name, IBinder token, int callingUid, String callingPackage, String callingTag,
                boolean stable, int userId) {
                ...
                    // If the provider is not already being launched, then get it
                    // started.
                    if (i >= N) {
                        final long origId = Binder.clearCallingIdentity();
    
                        try {
                            ...
                            if (proc != null && proc.thread != null && !proc.killed) {
                                if (DEBUG_PROVIDER) Slog.d(TAG_PROVIDER,
                                        "Installing in existing process " + proc);
                                if (!proc.pubProviders.containsKey(cpi.name)) {
                                    checkTime(startTime, "getContentProviderImpl: scheduling install");
                                    proc.pubProviders.put(cpi.name, cpr);
                                    try {
                                        proc.thread.scheduleInstallProvider(cpi);
                                    } catch (RemoteException e) {
                                    }
                                }
                            } else {
                                checkTime(startTime, "getContentProviderImpl: before start process");
                                proc = startProcessLocked(cpi.processName,
                                        cpr.appInfo, false, 0,
                                        new HostingRecord("content provider",
                                            new ComponentName(cpi.applicationInfo.packageName,
                                                    cpi.name)),
                                        ZYGOTE_POLICY_FLAG_EMPTY, false, false, false);
                                checkTime(startTime, "getContentProviderImpl: after start process");
                                if (proc == null) {
                                    Slog.w(TAG, "Unable to launch app "
                                            + cpi.applicationInfo.packageName + "/"
                                            + cpi.applicationInfo.uid + " for provider "
                                            + name + ": process is bad");
                                    return null;
                                }
                            }
                            cpr.launchingApp = proc;
                            mLaunchingProviders.add(cpr);
                        } finally {
                            Binder.restoreCallingIdentity(origId);
                        }
                    }
                ...
            }
    

          而server(com.test.demo2)进程在启动时会调用attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq)方法,关键代码如下:

        static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG = 57;
        
        private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
                int pid, int callingUid, long startSeq) {
            // ...
            if (providers != null && checkAppInLaunchingProvidersLocked(app)) {
                Message msg = mHandler.obtainMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG);
                msg.obj = app;
                mHandler.sendMessageDelayed(msg,
                        ContentResolver.CONTENT_PROVIDER_PUBLISH_TIMEOUT_MILLIS);
            }
            // ...
        }
    

          server(com.test.demo2)进程会判断当前AndroidManifest.xml文件中是否存在需要注册的ContentProvider,如果存在就给Handler发送一个延时消息。这个消息的处理逻辑如下:

                case CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG: {
                    ProcessRecord app = (ProcessRecord)msg.obj;
                    synchronized (ActivityManagerService.this) {
                        processContentProviderPublishTimedOutLocked(app);
                    }
                } break;
    
        private final void processContentProviderPublishTimedOutLocked(ProcessRecord app) {
            cleanupAppInLaunchingProvidersLocked(app, true);
            mProcessList.removeProcessLocked(app, false, true,
                    ApplicationExitInfo.REASON_INITIALIZATION_FAILURE,
                    ApplicationExitInfo.SUBREASON_UNKNOWN,
                    "timeout publishing content providers");
        }
    
        final boolean cleanUpApplicationRecordLocked(ProcessRecord app,
                boolean restarting, boolean allowRestart, int index, boolean replacingPid) {
            ...
            // Remove published content providers.
            for (int i = app.pubProviders.size() - 1; i >= 0; i--) {
                ContentProviderRecord cpr = app.pubProviders.valueAt(i);
                if (cpr.proc != app) {
                    // If the hosting process record isn't really us, bail out
                    continue;
                }
                final boolean alwaysRemove = app.bad || !allowRestart;
                final boolean inLaunching = removeDyingProviderLocked(app, cpr, alwaysRemove);
                ...
            }
            ...
        }
    

    AMS$MainHandler.handleMessage()

    ​ —> AMS.processContentProviderPublishTimedOutLocked()

    ​ —> AMS.cleanUpApplicationRecordLocked()

    ​ —> AMS.removeDyingProviderLocked()

          经过层层调用最终调用到了AMS.removeDyingProviderLocked()方法。

          我们在全局范围内搜索CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG的时候会发现,还有removeMessage的方法。总共有两个调用地方

    • 重启server进程
        final boolean cleanUpApplicationRecordLocked(ProcessRecord app,
                boolean restarting, boolean allowRestart, int index, boolean replacingPid) {
            ...
            if (restart && allowRestart && !app.isolated) {
                // We have components that still need to be running in the
                // process, so re-launch it.
                if (index < 0) {
                    ProcessList.remove(app.pid);
                }
    
                // Remove provider publish timeout because we will start a new timeout when the
                // restarted process is attaching (if the process contains launching providers).
                mHandler.removeMessages(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG, app);
    
                mProcessList.addProcessNameLocked(app);
                app.pendingStart = false;
                mProcessList.startProcessLocked(app,
                        new HostingRecord("restart", app.processName),
                        ZYGOTE_POLICY_FLAG_EMPTY);
                return true;
            }
            ...
        }
    
    • ContentProvider注册成功
        public final void publishContentProviders(IApplicationThread caller,
                List<ContentProviderHolder> providers) {
                ...
                        if (wasInLaunchingProviders) {
                            mHandler.removeMessages(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG, r);
                        }
                ...
        }
    

          看到这里,removeDyingProviderLocked(ProcessRecord proc, ContentProviderRecord cpr, boolean always)的调用过程就已经很清晰了。system_server在启动进程时如果目标进程有需要注册的ContentProvider,就会发送一个10s的超时信息;如果目标进程的ContentProvider在十秒内加载完成,system_server就会移除这个超时信息;如果没有注册完成,system_server就会处理这个信息,最终就会调用到removeDyingProviderLocked()方法。

          但是,调用到removeDyingProviderLocked()这个方法并不一定就会导致调用方进程被杀,还要满足conn.stableCount > 0的条件,因此接下去我们继续看下conn.stableCount的相关赋值逻辑。

    4.2 conn.stableCount的赋值逻辑

          conn.stableCount的赋值涉及到ContentProvider中的引用计数逻辑,详细分析可见: ContentProvider引用计数。关键就在于下面这张表

    在这里插入图片描述

          再看下com.test.demo1中调用ContentProvider的逻辑

    在这里插入图片描述

          我们会看到com.test.demo1通过ContentResolver的call()方法来操作com.test.demo2的SharedProvider,ContentResolver.call()方法的实现如下:

        public final @Nullable Bundle call(@NonNull String authority, @NonNull String method,
                @Nullable String arg, @Nullable Bundle extras) {
            Preconditions.checkNotNull(authority, "authority");
            Preconditions.checkNotNull(method, "method");
    
            try {
                if (mWrapped != null) return mWrapped.call(authority, method, arg, extras);
            } catch (RemoteException e) {
                return null;
            }
            // 关键地方:stableCount+1
            IContentProvider provider = acquireProvider(authority);
            if (provider == null) {
                // provider为null,抛出异常
                throw new IllegalArgumentException("Unknown authority " + authority);
            }
            try {
                final Bundle res = provider.call(mPackageName, authority, method, arg, extras);
                Bundle.setDefusable(res, true);
                return res;
            } catch (RemoteException e) {
                // Arbitrary and not worth documenting, as Activity
                // Manager will kill this process shortly anyway.
                return null;
            } finally {
                releaseProvider(provider);
            }
        }
    

          显然client端调用call()方法后如果server端的ContentProvider注册失败,stableCount就会加一但是没有减一,此时如果服务端超过十秒没注册完相应的Provider组件,那么就会导致client端被system_server杀死。

          但如果我们把call()方法换成常用的query()方法,就会发现并不会出现这个问题。这是为什么呢?我们再看下query方法的实现:

        public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
                @Nullable String[] projection, @Nullable Bundle queryArgs,
                @Nullable CancellationSignal cancellationSignal) {
            Preconditions.checkNotNull(uri, "uri");
    
            try {
                if (mWrapped != null) {
                    return mWrapped.query(uri, projection, queryArgs, cancellationSignal);
                }
            } catch (RemoteException e) {
                return null;
            }
    
            IContentProvider unstableProvider = acquireUnstableProvider(uri);
            if (unstableProvider == null) {
                return null;
            }
            IContentProvider stableProvider = null;
            Cursor qCursor = null;
            try {
                long startTime = SystemClock.uptimeMillis();
    
                ICancellationSignal remoteCancellationSignal = null;
                if (cancellationSignal != null) {
                    cancellationSignal.throwIfCanceled();
                    remoteCancellationSignal = unstableProvider.createCancellationSignal();
                    cancellationSignal.setRemote(remoteCancellationSignal);
                }
                try {
                    qCursor = unstableProvider.query(mPackageName, uri, projection,
                            queryArgs, remoteCancellationSignal);
                } catch (DeadObjectException e) {
                    // The remote process has died...  but we only hold an unstable
                    // reference though, so we might recover!!!  Let's try!!!!
                    // This is exciting!!1!!1!!!!1
                    unstableProviderDied(unstableProvider);
                    stableProvider = acquireProvider(uri);
                    if (stableProvider == null) {
                        return null;
                    }
                    qCursor = stableProvider.query(
                            mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
                }
                if (qCursor == null) {
                    return null;
                }
    
                // Force query execution.  Might fail and throw a runtime exception here.
                qCursor.getCount();
                long durationMillis = SystemClock.uptimeMillis() - startTime;
                maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);
    
                // Wrap the cursor object into CursorWrapperInner object.
                final IContentProvider provider = (stableProvider != null) ? stableProvider
                        : acquireProvider(uri);
                final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
                stableProvider = null;
                qCursor = null;
                return wrapper;
            } catch (RemoteException e) {
                // Arbitrary and not worth documenting, as Activity
                // Manager will kill this process shortly anyway.
                return null;
            } finally {
                if (qCursor != null) {
                    qCursor.close();
                }
                if (cancellationSignal != null) {
                    cancellationSignal.setRemote(null);
                }
                if (unstableProvider != null) {
                    releaseUnstableProvider(unstableProvider);
                }
                if (stableProvider != null) {
                    releaseProvider(stableProvider);
                }
            }
        }
    

          从代码中我们很明显就能看出原因所在,query方法调的是acquireUnstableProvider(),stableCount的值并不会增加,所以即使服务端超过10s没有注册完成Provider,也不会导致客户端被杀。

          至此,我们终于找到导致线上用户App闪退的原因了。小结一下就是,demo1进程通过ContentResolve的call()方法来查询demo2的ContentProvider时,由于demo2进程启动较慢,超过十秒还没有注册好相应的ContentProvider,导致AMS在杀死demo2进程的同时,也连带着杀死了demo1进程。

    三、总结

    3.1 会导致闪退的ContentResolver方法

          根据ContentResolve中各个方法的实现逻辑,我大致列出了以下几个有可能导致调用方进程闪退的方法。包括:acquireProvider()、getStreamTypes()、canonicalize()、uncanonicalize()、refresh()、insert()、bulkInsert()、delete()、update()、call()、acquireContentProviderClient()(有些不是public类型的方法我也列出来了)。

    3.2 解决方案

          问题找到了,如何解决呢?方案一:不使用ContentResolve的call()方法,直接用query()。这种方案简单粗暴,在当前的业务场景下确实也能满足需求。但是总有治标不治本的感觉,如果以后必须要用call()方法怎么办呢?况且不仅仅是call()方法会导致这个问题,如3.1中所列的,update()等方法也存在这个问题。

          我们再仔细回想下这个问题发生的关键点在哪,一个是demo1进程调用了call()方法来启动demo2进程,另一个是demo2进程启动太慢。我们能改变的只有第一点,至于第二点demo2进程的启动速度则不是我们可以把握的,即使是demo2进程本身也很难把握,进程启动速度是和当时设备的状态强相关的。

          既然用call()方法来启动demo2进程可能会导致闪退,我们能不能先用query()方法来启动demo2进程,之后判断拿到的返回结果,如果返回的Cursor对象不为null再调用call()方法。如此一来既不会有闪退的风险,也能够调用任意的方法了。思路大概就是这个思路,只是调完query()方法再调call()方法总有种脱裤子放屁的样子。其实还有一个更优雅的方法,就是acquireContentProviderClient()方法。这个方法返回的是一个ContentProviderClient对象,通过判断这个对象是不是空,我们再决定是否继续调用call()方法。

    3.3 其他坑

          趁着这次线上bug仔细梳理了下ContentProvider的相关逻辑,同时排查了下App中个业务方对ContentProvider的使用逻辑,避免后续又出现类似问题。在排查的过程中发现了各种五花八门的写法。有连返回的Cursor是不是null都不判断就直接往下操作的,还有不带try…catch保护的,再有就是直接调用3.1所列的可能导致调用方闪退的方法的。平时没出事的原因是ContentProvider使用得较少,而server 进程启动慢于十秒出现的概率也比较低,如果不是大规模地去实现这个Provider的话,还是不容易发现问题的。

          此外,上面说的都是调用方Client端的坑。除调用方外,被调用方Server端中需要注意的一个坑就是,ContentProvider的onCreate()方法会先于Application的onCreate()被调用,而App的基础组件一般都是在Application的onCreate()方法中才初始化的,因此千万不要在ContentProvider的onCreate()中调用基础组件,query()等其他的方法里面最好也不要调。并且如果崩溃是发生在ContentProvider的onCreate()方法中,热修复都修复不了(热修复组件都还没来得及初始化呢!!!)

    3.4一种比较安全的写法

          想要防止出现由于ContentProvider导致的异常闪退等问题,就需要规范地使用ContentProvider,考虑到种种可能出现的异常情况。从3.3的分析中,我们可以知道,操作ContentProvider的代码逻辑中需要至少需要加上非空判断 + try…catch保护,而且如果调用的是stable相关的方法,则要先用通过acquireContentProviderClient()方法来尝试拉起ContentProvider所在的进程,代码如下:

    private void queryProvider() {
        try {
            ContentResolver contentResolver = getContentResolver();
            String targetProviderAuthority = "com.test.demom2.SharedProviderAuthority";
            ContentProviderClient targetProviderClient = contentResolver.acquireUnstableContentProviderClient(targetProviderAuthority);
            if (targetProviderClient == null) {
                Log.e(TAG, "targetProviderClient is null, return");
                return;
            }
            Bundle bundle = contentResolver.call(targetProviderAuthority, "xxx", null, null);
            if (bundle == null) {
                Log.e(TAG, "bundle is null, return");
            }
            // 具体的业务逻辑
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
        }
    }
    

          最后,还有一个小问题说明下,为何在我们的进程日志里面看不到任何的异常栈信息?原因其实很简单,因为我们的进程根本就没有发生异常!我们的进程被杀仅仅只是因为我们调用的ContentProvider组件加载超时了。

    展开全文
  • 1.内容提供者ContentProvider ContentProvider是android四大组件之一,它为不同的应用之间实现数据共享,提供统一的接口,也就是说ContentProvider可以实现进程间的数据共享,实现跨进程通信。 ContentProvider主要...

    1.内容提供者ContentProvider
    ContentProvider是android四大组件之一,它为不同的应用之间实现数据共享,提供统一的接口,也就是说ContentProvider可以实现进程间的数据共享,实现跨进程通信。
    ContentProvider主要功能是为不同的app之间数据共享提供统一的接口,而且ContentProvider是以类似数据库中表的方式将数据暴露,也就是说ContentProvider就像一个“数据库”,那么外界获取其提供的数据,也就应该与从数据库中获取数据的操作基本一样,只不过是采用URI来表示外界需要访问的“数据库”。
    也就是说,如果我们想让其他的应用使用我们自己程序内的数据,就可以使用ContentProvider定义一个对外开放的接口,从而使得其他的应用可以使用我们自己应用中的文件、数据库内存储的信息。比如,在Android系统中,很多数据如:联系人信息、短信信息、图片库、音频库等,这些信息在开发中还是经常用到的,这些信息谷歌工程师已经帮我们封装好了,我们可以使用谷歌给我们的Uri去直接访问这些数据。

    2.ContentProvider的优势
    虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。
    ContentProvider为应用间的数据交互提供了一个安全的环境。
    ContentProvider提供了对底层数据存储方式的抽象,比如下图,底层使用了Sqlit数据库,在用ContentProvider进行封装后,把sqlit换成其他数据库也不会影响其功能。
    在这里插入图片描述
    3.ContentProvider的用法
    首先我们需要知道三个类:
    ContentProvider(内容提供者)
    ContentResolver(内容解析者)
    ContentObserver(内容观察者)

    假如我们现在有个应用A提供了数据 ,应用B要操作应用A的数据,那么应用A需要使用ContentProvider去共享自己数据;应用B使用ContentResolver去操作应用A的数据,通过ContentObserver去监听应用A的数据变化,当应用A的数据发生改变时,通知ContentObserver去告诉应用B数据变化了及时更新。
    这就是通信的大致流程,在了解更加详细的流程之前,我们还需要知道几个概念。

    4.ContentProvider中的URI
    Uri — 通用资源标志符(Universal Resource Identifier),Uri代表要操作的数据,Android中可用的每种资源,比如图像、视频片段等都可以用Uri来表示。
    ①URI格式
    ContentProvider中的URI是有固定格式的,例如:
    在这里插入图片描述
    Authority:授权信息,用来区别不同的ContentProvider。
    path:表名,用来区分ContentProvider中不同的数据表。
    id:id号,用来区分表中的不同数据。
    ②UriMatch
    UriMatch主要为了匹配URI,比如应用A提供了数据,但是并不是说所有的应用都可以操作这些数据,只有提供了满足应用A的URI,才能访问A的数据,而UriMatch就是做这个匹配工作的,他有如下方法:
    (1)public UriMatcher(int code) :它的作用就是创建一个UriMatch对象。
    用法如下:
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    //匹配不成功返回NO_MATCH(-1)
    (2)public void addURI(String authority,String path, int code):它的作用是在ContentProvider添加一个用于匹配的Uri,当匹配成功时返回第三个参数code。
    参数说明:
    authority:主机名(用于唯一标示一个ContentProvider,这个需要和清单文件中的authorities属性相同);
    path:路径路径(可以用来表示我们要操作的数据,路径的构建应根据业务而定);
    code:返回值(用于匹配uri的时候,作为匹配成功的返回值)
    (3)public int match(Uri uri) :这里的Uri就是传过来的要进行验证匹配的Uri。
    Uri可以是精确的字符串,Uri中带有*表示可匹配任意text,#表示只能匹配数字。
    假如add的uri是: content://com.example.test/student/#,那么content://com.example.test/student/10 可以匹配成功,这里的10可以是任意的数字。

    5.ContentProvider
    要使用ContentProvider,需要自定义自己的内容提供者,继承自ContentProvider,并实现下列方法:
    ①public boolean onCreate()在创建ContentProvider时调用。
    ②public Cursor query (Uri, String[], String, String[], String) 用于查询指定Uri的ContentProvider,返回一个Cursor。
    ③public Uri insert (Uri, ContentValues) 用于添加数据到指定Uri的ContentProvider中(外部应用向ContentProvider中添加数据)。
    ④public int update (Uri, ContentValues, String, String[]) 用于更新指定Uri的ContentProvider中的数据。
    ⑤public int delete (Uri, String, String[]) 用于从指定Uri的ContentProvider中删除数据。
    ⑥public String getType (Uri) 用于返回指定的Uri中的数据的MIME类型。

    注意:数据访问的方法(如:insert和update可能被多个线程同时调用,此时必须是线程安全的。其他方法(如:onCreate)只能被应用的主线程调用,它应当避免冗长的操作。

    6.ContentResolver
    我们知道ContentProvider共享数据是通过定义一个对外开放的统一的接口来实现的。 然而,其他应用程序并不直接调用这些方法,而是使用一个 ContentResolver对象,通过调用它的方法作为替代。ContentResolver可以与任意内容提供者进行会话,通过与其合作来对所有相关交互通讯进行管理。 当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver类来完成。
    要获取ContentResolver对象,可以使用Context提供的getContentResolver()方法:ContentResolver cr = getContentResolver();
    在上面我们提到ContentProvider可以向其他应用程序提供数据,与之对应的ContentResolver则负责获取ContentProvider提供的数据,修改、添加、删除、更新数据等。

    ContentResolver 类也提供了与ContentProvider类相对应的四个方法:
    (1)public Uri insert(Uri uri, ContentValues values) 该方法用于往ContentProvider添加数据。
    (2)public int delete(Uri uri, String selection, String[] selectionArgs) 该方法用于从ContentProvider删除数据。
    (3)public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 该方法用于更新ContentProvider中的数据。
    (4)public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 该方法用于从ContentProvider中获取数据。
    这些方法的第一个参数为Uri,代表要操作的是哪个ContentProvider和对其中的什么数据进行操作,假设给定的是 Uri.parse(“content://com.qstingda.provider.personprovider/contact/15”),那么将会对主机名为com.qstingda.provider.personprovider的ContentProvider进行操作,path为contact/15的数据。

    ContentResolver(内容解析者)请求被自动转发到合适的内容提供者实例 ,所以子类不需要担心跨进程调用的细节。

    7.ContentObserver用法
    ContentObserver内容观察者,用于观察指定的Uri引起的数据库的变化,然后通知主线程,根据需求做我们想要做的处理。比方说当观察到ContentProvider的数据变化时会自动调用谷歌工程师给我们提供好的方法,可以在此方法中通知主线程数据改变等等。
    要实现这样的功能,首先要做的就是注册这个观察者,这里的注册是在需要监测ContentProvider的应用中进行注册,并不是在ContentProvider中。在ContentProvider中要做的就是当数据变化时进行通知,这里通知的方法谷歌已经帮我们写好,直接调用就行了。

    注册内容观察者:
    public final void registerContentObserver (Uri uri, boolean notifyForDescendents, ContentObserver observer)
    这样就注册了一个观察者实例,当指定的Uri发生改变时,这个实例会回调实例对象做相应处理。
    其中参数的意义:
    uri:需要观察的Uri
    notifyForDescendents:如果为true,表示以这个Uri为开头的所有Uri都会被匹配到;如果为false,表示精确匹配,即只会匹配这个给定的Uri。

    举个例子,假如有这么几个Uri:
    ①content://com.example.studentProvider/student
    ②content://com.example.studentProvider/student/#
    ③content://com.example.studentProvider/student/10
    ④content://com.example.studentProvider/student/teacher
    假如观察的Uri为content://com.example.studentProvider/student,当notifyForDescendents为true时,则以这个Uri开头的Uri的数据变化时都会被捕捉到,在这里也就是①②③④的Uri的数据的变化都能被捕捉到;当notifyForDescendents为false时,则只有①中Uri变化时才能被捕捉到。

    看到registerContentObserver 这个方法,立马应该能够想到ContentResolver中的另一个方法
    public final voidunregisterContentObserver(ContentObserver observer),它的作用就是取消对注册的那个Uri的观察,这里传进去的参数就是在registerContentObserver中传递进去的ContentObserver对象。

    那么问题来了,怎么去写一个ContentObserver呢?其实它的实现很简单,直接创建一个类继承ContentObserver,需要注意的是,必须要实现它的构造方法:
    public ContentObserver(Handlerhandler)
    这里传进去的是一个Handler对象,这个Handler对象的作用一般要依赖于ContentObserver的另一个方法,即:
    public void onChange(boolean selfChange)
    这个方法的作用就是当指定的Uri的数据发生变化时会回调的方法,此时可以借助构造方法中的Handler对象将这个变化的消息发送给主线程,当主线程接收到这个消息之后就可以按照我们的需求来完成相应的操作。
    下面通过一个例子更深入的了解一下ContentProvider的用法。

    8.举例
    我们自定义一个ContentProvider,然后在其它应用中来访问自定义的ContentProvider的数据。
    这个案例的运行效果如下:
    在这里插入图片描述
    这里的插入数据,是在一个项目中向另一个项目中的ContentProvider中插入一条数据,其他的操作也是,接下来就来看看怎么实现上述的效果。

    自定义的PeopleContentProvider的代码如下:
    public class PeopleContentProvider extends ContentProvider {
    //这里的AUTHORITY就是我们在AndroidManifest.xml中配置的authorities,这里的authorities可以随便写
    private static final String AUTHORITY = “com.example.studentProvider”;
    //匹配成功后的匹配码
    private static final int MATCH_ALL_CODE = 100;
    private static final int MATCH_ONE_CODE = 101;
    private static UriMatcher uriMatcher;
    private SQLiteDatabase db;
    private DBOpenHelper openHelper;
    private Cursor cursor = null;
    //数据改变后指定通知的Uri
    private static final Uri NOTIFY_URI = Uri.parse(“content://” + AUTHORITY + “/student”);

    //在静态代码块中添加要匹配的 Uri
    static {
    //匹配不成功返回NO_MATCH(-1)
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(AUTHORITY, “student”, MATCH_ALL_CODE);// 匹配记录集合
    uriMatcher.addURI(AUTHORITY, “student/#”, MATCH_ONE_CODE);// 匹配单条记录
    }

    @Override
    public boolean onCreate() {
    openHelper = new DBOpenHelper(getContext());
    db = openHelper.getWritableDatabase();
    return false;
    }

    //删除
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
    switch (uriMatcher.match(uri)) {
    //uriMatcher通过语句uriMatcher.addURI(AUTHORITY, “student”, MATCH_ALL_CODE);加入的Uri为content://com.example.studentProvider/student,如果传进来的uri跟这个uri匹配成功,则uri matched.match(uri)会返回MATCH_ALL_CODE,我们可以在这里对这个ContentProvider中的数据库进行删除等操作。这里如果匹配成功,我们将删除所有的数据
    case MATCH_ALL_CODE:
    int count=db.delete(“personData”, null, null);
    if(count>0){
    notifyDataChanged();
    return count;
    }
    break;
    case MATCH_ONE_CODE:
    // 这里可以做删除单条数据的操作。
    break;
    default:
    throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
    }
    return 0;
    }

    @Override
    public String getType(Uri uri) {
    return null;
    }

    //插入
    @Override
    public Uri insert(Uri uri, ContentValues values) {
    //如果传入的uri为content://com.example.studentProvider/student则能匹配成功,在这里进行插入操作
    int match=uriMatcher.match(uri);
    if(match!=MATCH_ALL_CODE){
    throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
    }

    long rawId = db.insert(“personData”, null, values);
    Uri insertUri = ContentUris.withAppendedId(uri, rawId);
    if(rawId>0){
    notifyDataChanged();
    return insertUri;
    }
    return null;
    }

    //查询
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {
    switch (uriMatcher.match(uri)) {
    //如果传入的uri为content://com.example.studentProvider/student则能匹配成功,就根据条件查询数据并将查询出的cursor返回
    case MATCH_ALL_CODE:
    cursor = db.query(“personData”, null, null, null, null, null, null);
    break;
    case MATCH_ONE_CODE:
    // 根据条件查询一条数据…
    break;
    default:
    throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
    }
    return cursor;
    }

    //更新
    @Override
    public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) {
    switch (uriMatcher.match(uri)) {
    case MATCH_ONE_CODE:
    long age = ContentUris.parseId(uri);
    selection = “age = ?”;
    selectionArgs = new String[] { String.valueOf(age) };
    int count = db.update(“personData”, values, selection,selectionArgs);
    if(count>0){
    notifyDataChanged();
    }
    break;
    case MATCH_ALL_CODE:
    // 如果有需求的话,可以对整个表进行操作
    break;
    default:
    throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
    }
    return 0;
    }

    //通知指定URI数据已改变
    private void notifyDataChanged() {
    getContext().getContentResolver().notifyChange(NOTIFY_URI, null);
    }
    }
    在我们的PeopleContentProvider中,我们用到了DBHelper类,代码如下:
    public class DBHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = “provider.db”;
    private static final int DATABASE_VERSION = 1;

    public DBHelper(Context context) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    String sql = “CREATE TABLE IF NOT EXISTS personData” +
    “(_id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR, age INTEGER, info TEXT)”;
    db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL(“DROP TABLE IF EXISTS personData”);
    onCreate(db);
    }
    }

    至此,ContentProvider类就定义好了。注意这个案例牵扯到两个项目,一个是包含我们自定义的ContentProvider,另一个项目是访问这个包含ContentProvider项目中的数据。
    写好PeopleContentProvider之后,千万不要忘了在清单文件中注册:
    < provider
    android:name=“com.example.contentproviderpractice.PeopleContentProvider”
    android:authorities=“com.example.student”
    android:exported=“true” >
    < /provider>
    这里的authorities是唯一标识内容提供者的,为内容提供者指定一个唯一的标识,这样其他应用才可以唯一获取此provider,exported的值为[flase|true],当为true时: 当前提供者可以被其它应用使用,此时任何应用都可以使用Provider通过URI 来获得它,也可以通过相应的权限来使用Provider;当为false时: 当前提供者不能被其它应用使用,默认为true。
    注册好之后运行到手机上,此时其它的应用就可以通过ContentResolver来访问这个PeopleContentProvider了。

    然后我们再新建另一个项目B,在MainActivity的代码如下:
    public class MainActivity extends Activity implements OnClickListener {
    private ContentResolver contentResolver;
    private ListView lvShowInfo;
    private MyAdapter adapter;
    private Button btnInit;
    private Button btnInsert;
    private Button btnDelete;
    private Button btnUpdate;
    private Button btnQuery;
    private Cursor cursor;

    private static final String AUTHORITY = “com.example.studentProvider”;
    private static final Uri STUDENT_ALL_URI = Uri.parse(“content://” + AUTHORITY + “/student”);

    private Handler handler=new Handler(){
    public void handleMessage(android.os.Message msg) {
    //当ContentObsever收到数据改变的消息后,通过handler做一些操作,比方更新listview等,根据业务需求来定…
    cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
    adapter.changeCursor(cursor);
    };
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    lvShowInfo=(ListView) findViewById(R.id.lv_show_info);
    initData();
    }

    private void initData() {
    btnInit=(Button) findViewById(R.id.btn_init);
    btnInsert=findViewById(R.id.btn_insert);
    btnDelete=findViewById(R.id.btn_delete);
    btnUpdate=findViewById(R.id.btn_update);
    btnQuery=findViewById(R.id.btn_query);
    btnInit.setOnClickListener(this);
    btnInsert.setOnClickListener(this);
    btnDelete.setOnClickListener(this);
    btnUpdate.setOnClickListener(this);
    btnQuery.setOnClickListener(this);
    //注册内容观察者
    contentResolver = getContentResolver();
    contentResolver.registerContentObserver( STUDENT_ALL_URI,true,new PersonOberserver(handler));
    //adapter
    adapter=new MyAdapter(MainActivity.this,cursor);
    lvShowInfo.setAdapter(adapter);
    }

    @Override
    public void onClick(View v) {
    switch (v.getId()) {
    //初始化
    case R.id.btn_init:
    ArrayList< Student> students = new ArrayList< Student>();
    Student student1 = new Student(“苍老师”,25,“一个会教学的好老师”);
    Student student2 = new Student(“柳岩”,26,“大方”);
    Student student3 = new Student(“杨幂”,27,“漂亮”);
    Student student4 = new Student(“张馨予”,28,“不知道怎么评价”);
    Student student5 = new Student(“范冰冰”,29,"。。。");

    students.add(student1);
    students.add(student2);
    students.add(student3);
    students.add(student4);
    students.add(student5);

    for (Student Student : students) {
    ContentValues values = new ContentValues();
    values.put(“name”, Student.getName());
    values.put(“age”, Student.getAge());
    values.put(“introduce”, Student.getIntroduce());
    contentResolver.insert(STUDENT_ALL_URI, values);
    }
    break;
    //增
    case R.id.btn_insert:
    Student student = new Student(“小明”, 26, “帅气男人”);
    //实例化一个ContentValues对象
    ContentValues contentValues = new ContentValues(); contentValues.put(“name”,student.getName()); contentValues.put(“age”,student.getAge()); contentValues.put(“introduce”,student.getIntroduce());
    //这里的uri和ContentValues对象经过一系列处理之后会传到ContentProvider中的insert方法中,在我们自定义的ContentProvider中进行匹配操作 contentResolver.insert(STUDENT_ALL_URI,insertContentValues);
    break;
    //删
    case R.id.btn_delete:
    //删除所有条目
    contentResolver.delete(STUDENT_ALL_URI, null, null);
    //删除_id为1的记录
    Uri delUri = ContentUris.withAppendedId(STUDENT_ALL_URI,1);
    contentResolver.delete(delUri, null, null);
    break;
    //改
    case R.id.btn_update:
    ContentValues contentValues = new ContentValues();
    contentValues.put(“introduce”,“性感”);
    //更新数据,将age=26的条目的introduce更新为"性感"(原来age=26的introduce为"大方"),生成的Uri为:content://com.example.studentProvider/student/26
    Uri updateUri = ContentUris.withAppendedId(STUDENT_ALL_URI,26); contentResolver.update(updateUri,contentValues, null, null);
    break;
    //查
    case R.id.btn_query:
    Cursor cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
    //此处用到了CursorAdapter
    adapter=new MyAdapter(this,cursor);
    lvShowInfo.setAdapter(adapter);
    cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
    adapter.changeCursor(cursor);
    break;
    }
    }
    }
    可以看出,如果我们想操作ContentProvider,必须要知道该内容提供者的Uri,在正确得到Uri之后,就可以通过ContentResolver对象来操作ContentProvider中的数据了。假如你需要插入数据只需要调用contentResolver.insert(uri, contentValues);把正确的uri和ContentValues键值对传过去就行了,执行这句话系统就会根据我们提供的uri找到对应的ContentProvider,因为我们的uri中包含了authority(主机等各种信息),得到对应的ContentProvider后将调用ContentResolver的与之对应的增删改查方法,并将参数通过ContentResolver的增删改查方法传递到ContentProvider中,从而调用ContentProvider对应的方法。

    项目B中,PersonObserver的代码如下:
    public class PersonOberserver extends ContentObserver {
    private Handler handler;

    public PersonOberserver(Handler handler) {
    super(handler);
    this.handler=handler;
    }

    //ContentResolver观察到数据变化后,自动回调该方法
    @Override
    public void onChange(boolean selfChange) {
    super.onChange(selfChange);
    //向handler发送消息,在handler中进行数据更新后的操作
    Message msg = new Message();
    handler.sendMessage(msg);
    }
    我们在构造方法中接收了Handler,然后当监听到指定的Uri的数据变化时就会通过Handler消息机制发送一条消息,然后的操作就由我们自行完成了。

    最后,我们来理一理整个操作的运行流程:首先有两个项目,一个是有ContentProvider的项目A,在这个ContentProvider中初始化了一个数据库,我们的目的就是在另一个项目B中来操作项目A中ContentProvider中的数据,例如插入一条数据,查询等。对于怎么在另一个项目中操作ContentProvider中的数据,是通过ContentResolver(内容解析者)对象来操作的,假如我们要进行insert操作,那么需要调用ContentResolver的insert(uri, ContentValues);将Uri和ContentValues对象经过一系列操作传递到ContentProvider的中,然后在ContentProvider会对这个Uri进行匹配,如果匹配成功则按照我们的需求去执行相应的操作,如:插入数据、查询数据等。
    整个流程可以简单理解为:app B中想要操作appA中的数据,因此app A提供一个ContentProvider,在app B中通过ContentResolver的增删改查方法,实际上调用了app A中ContentProvider的增删改查方法,增删改查操作后数据发生了变化,自动回调app B中ContentObserver类中的onChange方法,从而在app B中进行数据变化后的操作。(这一切交互的基础是同一个uri,app A中ContentProvider提供该uri,app B中通过ContentResolver调用增删改查方法是都要传入这个uri,同时注册ContentObserver时也要传入这个uri)

    下面我们通过一张图再来说一下这个过程:
    在这里插入图片描述
    从图中可以看出在OtherApplication中注册了ContentObserver之后,当Application1中的数据库发生了变化时,只需要在ContentProvider中调用ContentResolver的notifyChange(Uri, ContentObserver observer),由于在OtherApplication中注册了ContentObserver(注册时用的Uri和ContentProvider中发生变化的Uri一样),因此在ContentObserver中会收到这个变化信息,它就可以将这个消息通过Handler发送给OtherApplication。

    展开全文
  • 查看多媒体ContentProvider前面我们学习了ContentProvider来保存和检索数据,Android为常用的数据类型(如:音视频、图片和联系方式等)提供了大量的ContentProvider,它们被定义在android.provider包下。那么我们如何...
  • 一、基本概念:1.ContentProvider为存储和获取数据提供了统一的接口;2.使用ContentProvider可以在不同的应用程序之间共享数据;3.Android为常见的一些数据提供了ContentProvider(包括音频、视频、图片和通讯录等等 ...
  • Android 11 ContentProvider启动流程分析 这一篇主要介绍一下ContentProvider的启动流程,并通过query方法为代表看一下整个操作方法的执行流程。ContentProvider的主要作用是实现数据共享的,它一般是配合...
  • ContentProvider是Android的四大组件之一,以标准化的方式在Android 应用间共享数据。ContentProvider封装的数据存储以及增删改查等,并且必须实现一个对外统一的接口(Uri)。
  • Service 管理 官方对Service的描述是: Service是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。此外,组件可通过绑定...
  • 实验六、contentprovider实验+SQLite数据库的实现 【实验名称】实验六、contentprovider实验+SQLite数据库的实现 【实验目的】 1、掌握如何在Android 开发中使用 SQLite 数据库 2、熟悉设计数据库表结构的方法与步骤...
  • ContentProvider原理分析

    2021-06-07 18:11:21
    前景知识:对于ContentProvider理解必须对IBinder有所了解(一个提供跨进程信息交换的一个工具),会ContentProvider的简单使用。本文是基于你已经具备以上知识点阅读1.名词解释为了方便讲解定义几个名词Provider:...
  • 此时推荐一个小技巧:借助 ContentProvider 实现完全"无侵"的初始化, 让 SDK 更加易用。 ContentProvider 初始化原理 ContentProvider 的 onCreate 的调用在 Application 的 attachBaseContext 和 onCreate 之间, ...
  • ContentProvider 虽然是Android四大组件之一,但是对比其他组件,出场频率的确是低了一些。不过它在一些场合上还是非常好用的,比如跨进程传输数据,接下来介绍下它的基本使用。 一、创建ContentProvider 自定义一个...
  • ContentProvider 是 Android 的四大组件之一,用于保存和检索数据,是 Android 系统中不同应用程序之间共享数据的接口。在 Android 系统中,应用程序之间是相互独立的,分别运行在自己的进程中,相互之间没有数据...
  • import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database...
  • 我很好奇是否有人可以解释ContentProvider的生命周期。 我特别感兴趣的是,什么时候以及在什么情况下提供商被销毁。 文档似乎没有涵盖这一点。本文的ContentProvider部分讨论了难以获得的文档,但它包含了一个指向...
  • ContentProvider Timeout 4.1attachApplicationLocked private boolean attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) { // Find the ...
  • ContentProvider结合Room保存数据库ContentProvider结合Room保存数据库1. ContentProvider使用理解执行顺序重写Provider的增删改查方法使用Provider的增删改查方法2. Room单独使用@Database不使用@Entity如何创建表...
  • 006-Android-ContentProvider习题

    千次阅读 2021-04-22 13:16:41
    1、在ContentProvider中ContentUris的作用是干什么( B)。 A、用于获取Uri路径后面的ID部分 B、增删改查的方法都在这个类中 C、用于添加URI的类 D、根本就用不到这个类,没关系 2、利用内容解析者查询短信数据时uri...
  • 一、使用ContentProvider(内容提供者)共享数据ContentProvider在android中的作用是对外共享数据, 也就是说你可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对你应用中...
  • 自定义ContentProvider实现步骤
  • I'm especially interested in if, when and under what circumstances a ... A process containing a ContentProvider can be terminated, whether due to low memory conditions, old age, user action, etc.
  • ContentProvider 实战

    2021-09-18 14:47:20
    近期项目中需要从一个应用中拷贝一份文件(该文件无法直接拿到),并且文件放置的目录是data下,外部应用无法直接访问,所以考虑使用ContentProvider来实现。 ContentProvider,Android四大组件;ContentProvider ...
  • 五、ContentProvider相关 ContentProvider内容提供者: 负责数据存取,常用于APP进数据共享,跨进程数据存取等…比如读取相册,读取联系人,都是ContentProvider来实现的 **1.**我们想在自己的应用中访问别的应用...
  • ContentProvider管理联系人的实例:package com.android.xiong.getsystemcontentprovidertest;import java.util.ArrayList;import android.app.Activity;import android.app.AlertDialog;import android.content....
  • 【实验名称】实验六、contentprovider实验+SQLite数据库的实现 【实验目的】 1、掌握如何在Android 开发中使用 SQLite 数据库 2、熟悉设计数据库表结构的方法与步骤 3、理解contentprovider的使用方法及流程,理解...
  • 刚开始学习ContentProvider时是学的系统的ContentProvider,没碰到什么问题,但是在学习自定义ContentProvider的实现时,唉,碰到好几个问题,花了我好多时间。 碰到的问题: 1.java.lang....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 50,267
精华内容 20,106
关键字:

contentprovider

友情链接: cs5536_isa.rar