注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

移动架构 (六) 轻量级进程间通信框架设计

移动架构 (一) 架构第一步,学会画各种 UML 图移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架移动架构 (三) AMS 源码分析移动架构 (四) EventBus 3.1.1 源码分析及实现自己的轻...
继续阅读 »

移动架构 (一) 架构第一步,学会画各种 UML 图

移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架

动架构 (三) AMS 源码分析

移动架构 (四) EventBus 3.1.1 源码分析及实现自己的轻量级 EventBus 框架,根据 TAG 发送接收事件。

移动架构 (五) 仅仅对 Java Bean 的操作,就能完成对数据持久化

概述


现在多进程传递数据使用越来越广泛了,在 Android 中进程间通信提供了 文件AIDLBinderMessengerContentProviderSocketMemoryFile 等,实际开发中使用最多的应该是 AIDL ,但是 AIDL 需要编写 aidl 文件,如果使用 AIDL 仅仅是为了传递数据, 那么 YKProBus 是你不错的选择。


YKProBus


怎么使用?



1. root/build.gradle 中添加框架 maven


	allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
复制代码

2. app/build.gradle 中添加框架 依赖


	dependencies {
implementation 'com.github.yangkun19921001:YKProBus:1.0.1'
}
复制代码

3. 发送进程绑定接收进程服务


EventManager.getInstance().bindApplication(Context context,String proName);
复制代码

4. 发送消息


 EventManager.getInstance().sendMessage(int messageTag,Bundle bundle);
复制代码

5. 接收进程中需要在清单文件注册服务


<service       
android:name="com.devyk.component_eventbus.proevent.service.MessengerService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.devyk.component_eventbus.service"></action>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
复制代码

6. 接收消息


6.1 在需要接收消息的类中实现 IMessageHandler 并实例化一个 Handler 用于接收发送进程发来的消息


public class MainActivity extends Activity implements IMessageHandler{
...


/**
* 接收其它进程发送过来的消息
*
* @return
*/
@Override
public Handler getHandler() {
return new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0x001:
...
break;
}
}
};
}
...
}
复制代码

6.2 注册当前类需要接收消息


 EventManager.getInstance().registerMessager(int messageTag, Object obj);
复制代码

框架设计大概流程


Messenger-YKProBus-.png


Messenger 源码分析


Messenger 内部其实也是依赖 aidl 实现的进程间通信。


服务端


    @Override
public IBinder onBind(Intent intent) {
return mServiceMessenger.getServiceMessenger().getBinder();
}
复制代码

getBinder() 跟进去


    public IBinder getBinder() {
return mTarget.asBinder();
}
复制代码

mTarget 从何而来,从源码找找


    public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
复制代码

这里是我们实例化 服务端 Messenger 传入的 Handler target


    /**
* 初始化服务端 Messenger
*/
public MessengerManager() {
if (null == mServiceMessenger)
mServiceMessenger = new Messenger(mMessengerServiceHandler);
}
复制代码

那么我们在点击 getIMessenger() 在看看内部实现


    final IMessenger getIMessenger() {
synchronized (mQueue) {
if (mMessenger != null) {
return mMessenger;
}
mMessenger = new MessengerImpl();
return mMessenger;
}
}

复制代码

继续点击 MessengerImple


    private final class MessengerImpl extends IMessenger.Stub {
public void send(Message msg) {
msg.sendingUid = Binder.getCallingUid();
Handler.this.sendMessage(msg);
}
}
复制代码

这个是一个内部实现的类,可以看到继承的是 IMessenger.Stub 然后实现 send (Message msg) 函数,然后通过 mTarget.sendMessage(msg) 发送消息,最后在我们传入进去的 mMessengerServiceHandler 的 handleMessage (Message) 接收发来的消息。


既然这里内部帮我们写了 aidl 文件 ,并且也继承了 IMessenger.Stub 我们今天就要看到 aidl 才死心 , 好吧我们来找找 IMessenger aidl 文件。


IMessenger-aidl-.jpg


可以看到是在 framework/base/core/java/android/os 路径中,我们点击在来看下文件中怎么写的


IMessenger-aidl-2.jpg


内部就一个 send 函数,看到这,大家应该都明白了,Messenger 其实也没什么大不了,就是系统内部帮我们写了 aidl 并且也实现了 aidl ,最后又帮我们做了一个 Handler 线程间通信,所以服务端收到了客服端发来的消息。


客服端


客服端需要在 bindServicer onServiceConnected 回调中拿到 servicer, 平时我们自己写 应该是这么拿到 Ibinder 对象吧


IMessenger mServiceMessenger = IMessenger.Stub.asInterface(service);
复制代码

但是我们实际客服端是这样拿到服务端的 Messenger


    /**
* 服务端消息是否连接成功
*/
private class EventServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
isBindApplication = true;
// 得到服务信使对象
mServiceMessenger = new Messenger(service);

//将本地信使告诉服务端
registerMessenger();

String proName = ProcessUtils.getProName(mApplicationContext);
Log.d(TAG, " EventServiceConnection " + proName);

}

@Override
public void onServiceDisconnected(ComponentName name) {
isBindApplication = false;
}
}
复制代码
// 得到服务信使对象
mServiceMessenger = new Messenger(service);
复制代码

跟进去


public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
复制代码

这不就是我们刚刚说的自己实现的那种写法吧,到这里我们都懂了吧,我们平时写的 aidl android 中已经帮我们写了,想当于在 aidl 中封装下就变成了现在的 Messenger , 而我们又在 Messenger 上封装了下,想当于 三次封装了,为了使用更简单。封装才是王道!


总结


我们自己的 YKProBus 为了进程间通信使用更简单方便,其实相当于在 AIDL 中的三次封装。想要了解的可以去看下我具体的封装或者 Messenger 源码。


感谢大家抽空阅览文章,谢谢!


作者:DevYK
链接:https://juejin.cn/post/6844903910587498510
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动架构 (五) 仅仅对 Java Bean 的操作,就能完成对数据持久化

移动架构 (一) 架构第一步,学会画各种 UML 图移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架移动架构 (三) AMS 源码分析移动架构 (四) EventBus 3.1.1 源码分析及实现自己的轻...
继续阅读 »

移动架构 (一) 架构第一步,学会画各种 UML 图

移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架

移动架构 (三) AMS 源码分析

移动架构 (四) EventBus 3.1.1 源码分析及实现自己的轻量级 EventBus 框架,根据 TAG 发送接收事件。

前言


GitHub 上面开源了多个 Android 数据库,比如 GreenDao , LitePal , ORMLite 等,开源的数据库一般都是使用非常简单,不用开发者写 SQL,创建 table 等一些繁琐的操作,都是基于对对象的一些操作。那么我们自己可以设计一款不用自己来写 SQL 的轻量级数据库吗?当然可以,下面我们就来开干。


使用




  1. 给一个 Object 对象定义协定好的注解 table ,id


    @YKTable("tb_police")
    public class Police {

    /**
    * 人员 id
    */
    @YKField("_id")
    private String id;

    /**
    * 人员姓名
    */
    private String name;

    public Police(String id, String name) {
    this.id = id;
    this.name = name;
    }

    public Police() {
    }

    public String getId() {
    return id;
    }

    public void setId(String id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    @Override
    public String toString() {
    return "Police{" +
    "id='" + id + '\'' +
    ", name='" + name + '\'' +
    '}';
    }
    }
    复制代码


  2. 插入数据


    BaseDaoFactory.getOurInstance().getBaseDao(Police.class).insert(new Police("01", "DevYK"));
    复制代码


  3. 删除数据


    BaseDaoFactory.getOurInstance().getBaseDao(Police.class).delete(new Police("01", "DevYK"));
    复制代码


  4. 更新数据


    BaseDaoFactory.getOurInstance().getBaseDao(Police.class).updata(new Police("02", "BaseDevYK"), new Police("01", "DevYK"));
    复制代码


  5. 查询数据


    BaseDaoImp<Police> mBaseDao BaseDaoFactory.getOurInstance().getBaseDao(Police.class);
    Police police = new Police();
    police.setId("02");
    List<Police> policeLists = mBaseDao.query(police);
    复制代码


到这里,增删改查咱们都已经操作完了,使用是不是非常的简单,不用使用者写一行 SQL 语句,基本上一行代码解决,下面我们来看看源码实现,它到底做了些什么?


自动创建 table


流程


12da459c2a7d5e1ad0aa1dbc197a7af1.png


创建表核心代码


    /**
* 初始化
*
* @param sqLiteDatabase
* @param entityClass
*/
public void init(SQLiteDatabase sqLiteDatabase, Class<T> entityClass) {
this.mSQLiteDatabase = sqLiteDatabase;
this.mEntityClass = entityClass;

if (!isInit) {
//自动建表,取得表名
if (entityClass != null && (entityClass.getAnnotation(YKTable.class) == null)) {
//通过反射得到类名
this.mTableName = entityClass.getSimpleName();
} else {
if (TextUtils.isEmpty(entityClass.getAnnotation(YKTable.class).value())) {
//如果有注解但是注解为空的话,就取当前 类名
this.mTableName = entityClass.getSimpleName();
} else {
//取得注解上面的表名
this.mTableName = entityClass.getAnnotation(YKTable.class).value();
}

}

//执行创建表的操作, 使用 getCreateTabeSql () 生成 sql 语句
String autoCreateTabSql = getCreateTableSql();
Log.i(TAG, "tagSQL-->" + autoCreateTabSql);
//执行创建表的 SQL
this.mSQLiteDatabase.execSQL(autoCreateTabSql);
mCacheMap = new HashMap<>();
initCacheMap();
isInit = true;

}
}
复制代码

insert 插入数据


流程


SQLite-insert.png


插入核心代码


    /**
* 插入数据
* @param entity
* @return
*/
@Override
public long insert(T entity) {
//1. 准备好 ContentValues 中需要的数据
Map<String, String> map = getValues(entity);
if (map == null || map.size() == 0) return 0;
//2. 把数据转移到 ContentValues 中
ContentValues values = getContentValues(map);
//将数据插入表中
return mSQLiteDatabase.insert(mTableName, null, values);
}
复制代码

  1. 首先接收外部传入的数据对象。

  2. 对数据对象进行解析,拿到数据库表中对应字段的值,拿到之后将字段 key,对应的值 values 存入 map。

  3. 将 map 解析为 ContentValues。

  4. 进行数据库插入 mSQLiteDatabase.insert。


delete 删除数据


流程图


SQLite-delete.png


核心代码流程




  1. 拿到对应字段的值


      /**
    * key(字段) - values(成员变量) ---》getValues 后 ---》key (成员变量的名字) ---values 成员变量的值 id 1,name alan , password 123
    *
    * @param entity
    * @return
    */
    private Map<String, String> getValues(T entity) {
    HashMap<String, String> map = new HashMap<>();
    //返回所有的成员变量
    Iterator<Field> iterator = mCacheMap.values().iterator();
    while (iterator.hasNext()) {
    Field field = iterator.next();
    field.setAccessible(true);

    try {
    Object object = field.get(entity);
    if (object == null) {
    continue;
    }
    String values = object.toString();

    String key = "";

    if (field.getAnnotation(YKField.class) != null) {
    key = field.getAnnotation(YKField.class).value();
    } else {
    key = field.getName();

    }

    if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(values)) {
    map.put(key, values);
    }
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    }

    }
    return map;
    }

    复制代码


  2. 将拿到要删除的 key,values 字段对应的值,自动生成 SQL


        public Condition(Map<String,String> whereCasue) {
    //whereArgs 里面的内容存入的 list
    ArrayList list = new ArrayList();
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("1=1");

    //取得所有成员变量的名字
    Set<String> keys = whereCasue.keySet();
    Iterator<String> iterator = keys.iterator();
    while (iterator.hasNext()) {
    String key = iterator.next();
    String value = whereCasue.get(key);
    if (value != null){
    stringBuffer.append(" and " + key + "=?");
    list.add(value);
    }
    }

    this.whereClause = stringBuffer.toString();
    this.whereArgs = (String[]) list.toArray(new String[list.size()]);
    }

    复制代码


  3. 根据生成的 SQL 条件删除数据


        int delete = mSQLiteDatabase.delete(mTableName, condition.getWhereClause(), condition.getWhereArgs());


    复制代码


query 查询数据


流程图


SQLite-query.png


核心代码流程




  1. 拿到查询的条件的对象,转为 key,values 的 map 对象


    Map<String, String> values = getValues(where);

    private Map<String, String> getValues(T entity) {
    HashMap<String, String> map = new HashMap<>();
    //返回所有的成员变量
    Iterator<Field> iterator = mCacheMap.values().iterator();
    while (iterator.hasNext()) {
    //拿到成员变量
    Field field = iterator.next();
    //设置可操作的权限
    field.setAccessible(true);

    try {
    //拿到成员变量对应的值 values
    Object object = field.get(entity);
    if (object == null) {
    continue;
    }
    String values = object.toString();

    String key = "";
    //如果成员变量上声明了注解,直接拿到该值 key,反之反射拿
    if (field.getAnnotation(YKField.class) != null) {
    key = field.getAnnotation(YKField.class).value();
    } else {
    key = field.getName();

    }
    //将类中拿到的 key values 存入 map 中
    if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(values)) {
    map.put(key, values);
    }
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    }

    }
    return map;
    }

    复制代码


  2. 将 map 转为自动生成 SQL 对象


        public Condition(Map<String,String> whereCasue) {
    //whereArgs 里面的内容存入的 list
    ArrayList list = new ArrayList();
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("1=1");

    //取得所有成员变量的名字
    Set<String> keys = whereCasue.keySet();
    Iterator<String> iterator = keys.iterator();
    while (iterator.hasNext()) {
    String key = iterator.next();
    String value = whereCasue.get(key);
    if (value != null){
    //查询的条件 sql
    stringBuffer.append(" and " + key + "=?");
    //查询的条件对应的 值
    list.add(value);
    }
    }

    this.whereClause = stringBuffer.toString();
    this.whereArgs = (String[]) list.toArray(new String[list.size()]);
    }

    复制代码


  3. 查询数据,并遍历 Cursor 取出数据


    Cursor cursor = mSQLiteDatabase.query(mTableName,null,condition.getWhereClause(), condition.getWhereArgs(), null, limitString,  orderBy);

    List<T> result = getResult(cursor, where);


    复制代码


update 更新数据


流程图


SQLite-update.png


核心代码流程




  1. 拿到需要更新的值,转为 map


    Map<String, String> values = getValues(entity);
    复制代码


  2. 将需要更新的 map 转为 ContentValues


    ContentValues contentValues = getContentValues(values);
    复制代码


  3. 将条件转为 map


    //条件
    Map<String, String> whereMp = getValues(where);
    复制代码


  4. 将条件 map 转为 sql


        public Condition(Map<String,String> whereCasue) {
    //whereArgs 里面的内容存入的 list
    ArrayList list = new ArrayList();
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("1=1");

    //取得所有成员变量的名字
    Set<String> keys = whereCasue.keySet();
    Iterator<String> iterator = keys.iterator();
    while (iterator.hasNext()) {
    String key = iterator.next();
    String value = whereCasue.get(key);
    if (value != null){
    //查询的条件 sql
    stringBuffer.append(" and " + key + "=?");
    //查询的条件对应的 值
    list.add(value);
    }
    }

    this.whereClause = stringBuffer.toString();
    this.whereArgs = (String[]) list.toArray(new String[list.size()]);
    }
    复制代码


总结


通过上面的步骤,我们不编写 SQL ,完全是依赖数据对象来做操作,对 SQL 不太熟悉的很友好。对于那些开源框架为什么可以自动建表,不用填写 SQL ,仅仅通过数据 Bean 就能操作数据库的好奇,在没有了解它们内部实现的原理下,看看自己能不能实现,结果也没那么难嘛。但是实际项目还是建议在了解内部实现原理的情况下使用开源框架。在这里推荐下 郭霖大神的 LitePal 框架非常稳定,使用非常简单,易上手。


如果对上面源码感兴趣的可以去我的代码仓库YKDB详细了解。


作者:DevYK
链接:https://juejin.cn/post/6844903907613753351
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

移动架构 (四) EventBus 3.1.1 源码分析及实现自己的轻量级 EventBus 框架,根据 TAG 发送接收事件。

移动架构 (一) 架构第一步,学会画各种 UML 图移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架移动架构 (三) AMS 源码分析EventBus 我相信大家不会很默认,应该也都在项目中使用过,虽然 ...
继续阅读 »

移动架构 (一) 架构第一步,学会画各种 UML 图
移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架

移动架构 (三) AMS 源码分析

EventBus 我相信大家不会很默认,应该也都在项目中使用过,虽然 EventBus 在项目中不便于管理,发射出去的消息不便于跟踪或者阅读,但是不可否认它是一个优秀的开源项目,还是值得我们大家学习的,我个人感觉唯一不足的就是 EventBus 功能上不能根据 TAG 来进行发射和接收消息,只能通过注解 + 消息类型进行找消息。那么我们自己可以实现这个功能吗?不可否认,当然可以! 想要实现这个功能,我们先大概简要的了解下 EventBus 使用及源码是怎么实现的。


EventBus 简单使用


EventBus 可以代替 Android 传统的 Intent, Handler, Broadcast 或接口函数, 在 Fragment, Activity, Service 线程间进行数据传递。


添加 EventBus 到项目中


implementation 'org.greenrobot:eventbus:3.1.1'
复制代码

register


EventBus.getDefault().register(this);
复制代码

注解实现接收的 Event


    @Subscribe(threadMode = ThreadMode.MAIN)
public void receive(String event){
Log.d(TAG,"接收到 EventBus post message:" + event);
}
复制代码

发射数据


EventBus.getDefault().post("发射一个测试消息");
复制代码

注销注册的事件


EventBus.getDefault().unregister(this);
复制代码

这里就简单介绍下 EventBus 使用,想详细了解的可以看 EventBus GitHub


EventBus 3.1.1 源码分析


上面小节咱们学习了 EventBus 的简单使用,那么我们就根据上面使用到的来进行源码分析。


register


流程图:


EventBus-register.png


register 代码:


		/**
*
*EventBus register
*/
public void register(Object subscriber) {
//1. 拿到当前注册 class
Class<?> subscriberClass = subscriber.getClass();
//2. 查找当前 class 类中所有订阅者的方法
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
//3.
subscribe(subscriber, subscriberMethod);
}
}
}
复制代码

subscribe(x,x)代码:


    //必须加锁调用
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
//拿到订阅者参数类型
Class<?> eventType = subscriberMethod.eventType;
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
//根据参数类型,拿到当前所有订阅者
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
//4. 如果没有拿到,则存进去缓存中
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
//如果已经存在相同类型的注册事件,就抛出异常
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}

......
}
复制代码


  1. 拿到当前传入进来的 this 对象。




  2. 查找当前 this 类中所有订阅的函数。




  3. subscriptionsByEventType 完成数据初始化。


    subscriptionsByEventType = new HashMap();
    复制代码

    // 参考上图注释 4 ,根据参数类型存储订阅者和订阅方法




post


流程图:


EventBus-post.png


代码


 /** 发送事件*/
public void post(Object event) {
//在当前线程中取出变量数据
PostingThreadState postingState = currentPostingThreadState.get();
//将需要发送的数据添加进当前线程的队列中
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);

if (!postingState.isPosting) {
postingState.isMainThread = isMainThread();
postingState.isPosting = true;
if (postingState.canceled) {
throw new EventBusException("Internal error. Abort state was not reset");
}
try {
while (!eventQueue.isEmpty()) {
//开启取出数据
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}
复制代码
    private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class<?> eventClass = event.getClass();
boolean subscriptionFound = false;
if (eventInheritance) {
//查找所有的事件类型
List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
int countTypes = eventTypes.size();
for (int h = 0; h < countTypes; h++) {
Class<?> clazz = eventTypes.get(h);
subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
}
} else {
//发送事件
subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
}
if (!subscriptionFound) {
if (logNoSubscriberMessages) {
logger.log(Level.FINE, "No subscribers registered for event " + eventClass);
}
if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
eventClass != SubscriberExceptionEvent.class) {
post(new NoSubscriberEvent(this, event));
}
}
}
复制代码
    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
CopyOnWriteArrayList<Subscription> subscriptions;
synchronized (this) {
//这里的容器就是 register 的时候添加进行去的,现在拿出来
subscriptions = subscriptionsByEventType.get(eventClass);
}
if (subscriptions != null && !subscriptions.isEmpty()) {
for (Subscription subscription : subscriptions) {
postingState.event = event;
postingState.subscription = subscription;
boolean aborted = false;
try {
//发送到订阅者哪里去
postToSubscription(subscription, event, postingState.isMainThread);
aborted = postingState.canceled;
} finally {
postingState.event = null;
postingState.subscription = null;
postingState.canceled = false;
}
if (aborted) {
break;
}
}
return true;
}
return false;
}
复制代码
    private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
//根据订阅者 threadMode 来进行发送
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
//通过反射发送
invokeSubscriber(subscription, event);
break;
case MAIN:
//如果是主线程直接在当前线程发送
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
//开启一个子线程发送
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}
复制代码

步骤



  1. post

  2. 获取事件类型

  3. 根据类型,获取订阅者和订阅方法

  4. 根据订阅者的 threadMode 来判断线程

  5. 通过反射直接调用订阅者的订阅方法来完成本次通信息


Subscribe


这里是订阅者的意思,通过自定义注解实现


@Documented
@Retention(RetentionPolicy.RUNTIME) //注解会在class字节码文件中存在,在运行时可以通过反射获取到
@Target({ElementType.METHOD}) //只能在方法上声明
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.POSTING; //默认在 post 线程

boolean sticky() default false; //默认不是粘性事件

int priority() default 0; //线程优先级默认
}
复制代码

threadMode


package org.greenrobot.eventbus;

public enum ThreadMode {
POSTING, // post 线程
MAIN,//指定主线程
MAIN_ORDERED,
BACKGROUND,//后台进行
ASYNC;//异步

private ThreadMode() {
}
}
复制代码

unregister


流程:


EventBus-unRegister.png


EventBus 源码总结


到这里我们就简单的分析了 注册 - > 订阅 - > 发送 - >接收事件 简单的流程就是这样了,如果想更深入的话,建议下载源码来看 EventBus GitHub 架构方面还是要注重基础知识,比如 注解 + 反射 + 设计模式 (设计模式的话建议去看 《Android 源码设计模式》一书 ),现在开源项目几乎离不开这几项技术。只有我们掌握了基础 + 实现原理。我们模仿着也能写出来同样的项目。


下面我们就简单模仿下 EventBus 原理,实现自己的 EventBus 框架。


YEventBus 根据 TAG 实现接收消息架构实现


说明一点,这里我们还是根据 EventBus 的核心原理实现,并不会有 EventBus 那么多功能,我们只是学习 EventBus 原理的同时,能根据它的原理,自己写一套轻量级的 EventBus 架构。


最后一共差不多 300 行代码实现根据 TAG 发送/接收事件,下面是效果图:



以下我就直接贴代码了 每一步代码都有详细的注释,相信应该不难理解。


使用方式




  • 添加依赖


      allprojects {
    repositories {
    ...
    maven { url 'https://jitpack.io' }
    }
    }

    dependencies {
    implementation 'com.github.yangkun19921001:YEventBus:Tag'
    }
    复制代码


  • 注册事件


     //开始注册事件。模仿 EventBus
    YEventBus.getDefault().register(this);
    复制代码


  • 订阅消息


        /**
    * 这里是自定义的注册,最后通过反射来获取当前类里面的订阅者
    *
    * @param meg
    */
    @YSubscribe(threadMode = YThreadMode.MAIN, tag = Constants.TAG_1)
    public void onEvent(String meg) {
    Toast.makeText(getApplicationContext(), "收到:" + meg, Toast.LENGTH_SHORT).show();
    }
    复制代码


  • 发送消息


    YEventBus.getDefault().post(Constants.TAG_1, "发送 TAG 为 1 的消息");
    复制代码


register


 /**
* 注册方法
*/
public void register(Object subscriber) {
//拿到当前注册的所有的订阅者
List<YSubscribleMethod> ySubscribleMethods = mChacheSubscribleMethod.get(subscriber);
//如果订阅者已经注册了 就不需要再注册了
if (ySubscribleMethods == null) {
//开始反射找到当前类的订阅者
ySubscribleMethods = getSubscribleMethods(subscriber);
//注册了就存在缓存中,避免多次注册
mChacheSubscribleMethod.put(subscriber, ySubscribleMethods);
}
}

/**
* 拿到当前注册的所有订阅者
*
* @param subscriber
* @return
*/
private List<YSubscribleMethod> getSubscribleMethods(Object subscriber) {
//拿到注册的 class
Class<?> subClass = subscriber.getClass();
//定义一个容器,用来装订阅者
List<YSubscribleMethod> ySubscribleMethodList = new ArrayList<>();
//开始循环找到
while (subClass != null) {
//1. 开始进行筛选,如果是系统的就不需要进行下去
String subClassName = subClass.getName();
if (subClassName.startsWith(Constants.JAVA) ||
subClassName.startsWith(Constants.JAVA_X) ||
subClassName.startsWith(Constants.ANDROID) ||
subClassName.startsWith(Constants.ANDROID_X)
) {
break;
}
//2. 遍历拿到当前 class
Method[] declaredMethods = subClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
//3. 检测当前方法中是否有 我们的 订阅者 注解也就是 YSubscribe
YSubscribe annotation = declaredMethod.getAnnotation(YSubscribe.class);
//如果没有直接跳出查找
if (annotation == null)
continue;

// check 这个方法的参数是否有多个
Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
if (parameterTypes.length > 1) {
throw new RuntimeException("YEventBus 只能接收一个参数");
}

//4. 符合要求,最后添加到容器中
//4.1 拿到需要在哪个线程中接收事件
YThreadMode yThreadMode = annotation.threadMode();
//只能在当前 tag 相同下才能接收事件
String tag = annotation.tag();
YSubscribleMethod subscribleMethod = new YSubscribleMethod(tag, declaredMethod, yThreadMode, parameterTypes[0]);
ySubscribleMethodList.add(subscribleMethod);

}
//去父类找订阅者
subClass = subClass.getSuperclass();
}
return ySubscribleMethodList;
}
复制代码

post (这里不是粘性事件)


    /**
* post 方法
*/
public void post(String tag, Object object) {
//拿到当前所有订阅者持有的类
Set<Object> subscriberClass = mChacheSubscribleMethod.keySet();
//拿到迭代器,
Iterator<Object> iterator = subscriberClass.iterator();
//进行循环遍历
while (iterator.hasNext()) {
//拿到注册 class
Object subscribleClas = iterator.next();
//获取类中所有添加订阅者的注解
List<YSubscribleMethod> ySubscribleMethodList = mChacheSubscribleMethod.get(subscribleClas);
for (YSubscribleMethod subscribleMethod : ySubscribleMethodList) {
//判断这个方法是否接收事件
if (!TextUtils.isEmpty(tag) && subscribleMethod.getTag().equals(tag) //注解上面的 tag 是否跟发送者的 tag 相同,相同就接收
&& subscribleMethod.getEventType().isAssignableFrom(object.getClass() //判断类型
)
) {
//根据注解上面的线程类型来进行切换接收消息
postMessage(subscribleClas, subscribleMethod, object);
}

}

}

}

private void postMessage(final Object subscribleClas, final YSubscribleMethod subscribleMethod, final Object message) {

//根据需要的线程来进行切换
switch (subscribleMethod.getThreadMode()) {
case MAIN:
//如果接收的是主线程,那么直接进行反射,执行订阅者的方法
if (isMainThread()) {
postInvoke(subscribleClas, subscribleMethod, message);
} else {//如果接收消息在主线程,发送线程在子线程那么进行线程切换
mHandler.post(new Runnable() {
@Override
public void run() {
postInvoke(subscribleClas, subscribleMethod, message);
}
});
}
break;
case ASYNC://需要在子线程中接收
if (isMainThread())
//如果当前 post 是在主线程中,那么切换为子线程
ThreadUtils.executeByCached(new ThreadUtils.Task<Boolean>() {
@Nullable
@Override
public Boolean doInBackground() throws Throwable {
postInvoke(subscribleClas, subscribleMethod, message);
return true;
}

@Override
public void onSuccess(@Nullable Boolean result) {
Log.i(TAG, "执行成功");
}

@Override
public void onCancel() {

}

@Override
public void onFail(Throwable t) {

}
});
else
postInvoke(subscribleClas, subscribleMethod, message);
break;
case POSTING:
case BACKGROUND:
case MAIN_ORDERED:
postInvoke(subscribleClas, subscribleMethod, message);
break;
default:
break;
}
}

/**
* 反射调用订阅者
*
* @param subscribleClas
* @param subscribleMethod
* @param message
*/
private void postInvoke(Object subscribleClas, YSubscribleMethod subscribleMethod, Object message) {
Log.i(TAG, "post message: " + "TAG:" + subscribleMethod.getTag() + " 消息体:" + message);
Method method = subscribleMethod.getMethod();
//执行
try {
method.invoke(subscribleClas, message);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
复制代码

Subscribe 订阅者


/**
* <pre>
* author : devyk on 2019-07-27 18:05
* blog : https://juejin.im/user/3368559355637566/posts
* github : https://github.com/yangkun19921001
* mailbox : yang1001yk@gmail.com
* desc : This is YSubscribe
* </pre>
*/

@Target(ElementType.METHOD) //target 描述此注解在哪里使用
@Retention(RetentionPolicy.RUNTIME) //retention 描述此注解保留的时长 这里是在运行时
public @interface YSubscribe {
YThreadMode threadMode() default YThreadMode.POSTING; //默认是在 post 线程接收数据

String tag() default "";//根据消息来接收事件
}

复制代码

TreadMode 线程模式


/**
* <pre>
* author : devyk on 2019-07-27 18:14
* blog : https://juejin.im/user/3368559355637566/posts
* github : https://github.com/yangkun19921001
* mailbox : yang1001yk@gmail.com
* desc : This is YThreadMode
* </pre>
*/
public enum YThreadMode {
/**
* Subscriber will be called directly in the same thread, which is posting the event. This is the default. Event delivery
* implies the least overhead because it avoids thread switching completely. Thus this is the recommended mode for
* simple tasks that are known to complete in a very short time without requiring the main thread. Event handlers
* using this mode must return quickly to avoid blocking the posting thread, which may be the main thread.
*/
POSTING,

/**
* On Android, subscriber will be called in Android's main thread (UI thread). If the posting thread is
* the main thread, subscriber methods will be called directly, blocking the posting thread. Otherwise the event
* is queued for delivery (non-blocking). Subscribers using this mode must return quickly to avoid blocking the main thread.
* If not on Android, behaves the same as {@link #POSTING}.
*/
MAIN,

/**
* On Android, subscriber will be called in Android's main thread (UI thread). Different from {@link #MAIN},
* the event will always be queued for delivery. This ensures that the post call is non-blocking.
*/
MAIN_ORDERED,

/**
* On Android, subscriber will be called in a background thread. If posting thread is not the main thread, subscriber methods
* will be called directly in the posting thread. If the posting thread is the main thread, EventBus uses a single
* background thread, that will deliver all its events sequentially. Subscribers using this mode should try to
* return quickly to avoid blocking the background thread. If not on Android, always uses a background thread.
*/
BACKGROUND,

/**
* Subscriber will be called in a separate thread. This is always independent from the posting thread and the
* main thread. Posting events never wait for subscriber methods using this mode. Subscriber methods should
* use this mode if their execution might take some time, e.g. for network access. Avoid triggering a large number
* of long running asynchronous subscriber methods at the same time to limit the number of concurrent threads. EventBus
* uses a thread pool to efficiently reuse threads from completed asynchronous subscriber notifications.
*/
ASYNC

}
复制代码

unRegister取消注册


    /**
* 取消注册订阅者
*/
public void unRegister(Object subscriber) {
Log.i(TAG, "unRegister start:当前注册个数" + mChacheSubscribleMethod.size());
Class<?> subClas = subscriber.getClass();
List<YSubscribleMethod> ySubscribleMethodList = mChacheSubscribleMethod.get(subClas);
if (ySubscribleMethodList != null)
mChacheSubscribleMethod.remove(subscriber);

Log.i(TAG, "unRegister success:当前注册个数" + mChacheSubscribleMethod.size());
}

复制代码

框架怎么实现根据 TAG 接收消息


这个其实很简单,拿到 post 发送的 tag,跟订阅者的 tag 比较下就行了。其实只要了解原理也没有那么难得。


//判断这个方法是否接收事件
if (!TextUtils.isEmpty(tag) && subscribleMethod.getTag().equals(tag) //注解上面的 tag 是否跟发送者的 tag 相同,相同就接收
&& subscribleMethod.getEventType().isAssignableFrom(object.getClass() //判断类型
)
复制代码

总结


最后我们根据开源项目 EventBus 实现了自己 代码传送阵 YEventBus 框架,可以根据 TAG 发送/接收消息。只要了解开源框架原理,根据自己需求改动原有框架或者实现自己的框架都不是太难,加油!


作者:DevYK
链接:https://juejin.cn/post/6844903901129342989
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

移动架构 (三) AMS 源码分析

移动架构 (一) 架构第一步,学会画各种 UML 图移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架AMS 介绍 ActivityManagerService 简称 AMS , 是 Android 内核中...
继续阅读 »

移动架构 (一) 架构第一步,学会画各种 UML 图

移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架

AMS 介绍


ActivityManagerService 简称 AMS , 是 Android 内核中核心功能之一,由 com.android.server.SystemService.java 启动。


AMS 启动流程


以下流程因为涉及的源代码太多了 , 我这里以 UML 流程图跟代码截图以示


Android 系统启动


Android-.png


应用进程启动



ServiceManager 启动


BinderServiceManager.png


AMS 注册


AMS.png


AMS 启动详解


AMS-.png


代码流程:




  1. AMS 具体是在 SystemService 进中启动的,主要是在 com/android/server/SystemServer.java main() 函数中进行。


        /**
    * 这里的 main 函数 主要是 zygote 通过反射调用
    */

    public static void main(String[] args) {
    new SystemServer().run();
    }
    复制代码


  2. 从上图可以得知 main 方法主要是执行的 com/android/server/SystemServer.java run() 函数,我们具体来看下 run() 到底干了什么?


        private void run() {
    try {
    ....
    //加载了动态库libandroid_servers.so
    System.loadLibrary("android_servers");
    //创建SystemServiceManager,它会对系统的服务进行创建、启动和生命周期管理
    mSystemServiceManager = new SystemServiceManager(mSystemContext);
    mSystemServiceManager.setRuntimeRestarted(mRuntimeRestart);
    LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);
    } finally {

    }

    // 启动各种服务
    try {
    traceBeginAndSlog("StartServices");
    //启动了ActivityManagerService、PowerManagerService、PackageManagerService 等服务
    startBootstrapServices();
    // 启动了BatteryService、UsageStatsService和WebViewUpdateService 等服务
    startCoreServices();
    // 启动了CameraService、AlarmManagerService、VrManagerService等服务
    startOtherServices();
    SystemServerInitThreadPool.shutdown();

    ....

    } catch (Throwable ex) {
    throw ex;
    } finally {
    traceEnd();
    }
    // Loop forever.
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    复制代码

    run () 函数核心任务就是初始化一些事务和核心服务启动,这里核心服务初略有 80 多个,系统把核心服务大概分为了 3 类,分别是引导服务、核心服务和其他服务,因为这小节主要研究 AMS 启动,所以我们只关注 startBootstrapServices(); 内容就行了。




  3. startBoostrapService() 这个函数的主要作用,根据官方的意思就是启动需要获得的小型的关键服务。 这些服务具有复杂的相互依赖性,这就是我们在这里将它们全部初始化的原因。 除非您的服务也与这些依赖关系缠绕在一起,否则应该在其他一个函数中初始化它。


    public void startBootstrapServices(){
    ...

    //启动 AMS 服务
    mActivityManagerService = mSystemServiceManager.startService(
    ActivityManagerService.Lifecycle.class).getService();
    //设置管理器
    mActivityManagerService.setSystemServiceManager(mSystemServiceManager);
    //设置安装器
    mActivityManagerService.setInstaller(installer);

    ...

    }
    复制代码

    这里主要调用 SystemServiceManager 的 startService 方法,方法的参数是 ActivityManagerService.Lifecycle.class com/android/server/SystemServiceManager.java 我们再来看看 SSM 的 startService 方法主要干嘛了?




  4. SSM startService(Class serviceClass)


        public  T startService(Class serviceClass) {
    try {
    ...
    final T service;
    try {
    Constructor constructor = serviceClass.getConstructor(Context.class);
    //实例化对象
    service = constructor.newInstance(mContext);
    } catch (InvocationTargetException ex) {
    throw new RuntimeException("Failed to create service " + name
    + ": service constructor threw an exception", ex);
    }

    startService(service);
    return service;
    } finally {
    Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
    }
    }
    复制代码

    跟踪代码可以得知 startService 方法传入的参数是 Lifecycle.class,Lifecycle 继承自 SystemService 。首先,通过反射来创建 Lifecycle 实例,得到传进来的 Lifecycle 的构造器 constructor ,接着又调用 constructor 的newInstance 方法来创建 Lifecycle 类型的 service 对象。接着将刚创建的 service 添加到 ArrayList 类型的mServices 对象中来完成注册。最后在调用 service 的 onStart 方法来启动 service ,并返回该 service 。Lifecycle 是 AMS 的内部类。




  5. Lifecycle 走向


        public static final class Lifecycle extends SystemService {
    private final ActivityManagerService mService;

    public Lifecycle(Context context) {
    super(context);
    mService = new ActivityManagerService(context);
    }

    @Override
    public void onStart() {
    mService.start();
    }

    public ActivityManagerService getService() {
    return mService;
    }
    }
    复制代码

    主要执行 Lifecyle onStart()


        public void startService(@NonNull final SystemService service) {
    // 把 AMS 服务添加到系统服务中
    mServices.add(service);
    // Start it.
    long time = System.currentTimeMillis();
    try {
    //执行 Lifecyle 重 onStart 函数方法
    service.onStart();
    } catch (RuntimeException ex) {
    throw new RuntimeException("Failed to start service " + service.getClass().getName()
    + ": onStart threw an exception", ex);
    }
    warnIfTooLong(System.currentTimeMillis() - time, service, "onStart");
    }
    复制代码

    上面第四步调用 constructor 的 newInstance 方法已经实例化了 Lifecycle 并创建 new


    ActivityManagerService(context); 对象,接着又在第四步 startService(service) 调用了 service.start();实际上是调用了 Lifecyle onStart 函数方法。




  6. ActivityManagerService getService()


            mActivityManagerService = mSystemServiceManager.startService(
    ActivityManagerService.Lifecycle.class).getService();
    复制代码

    从上面代码得知实际上是调用了 AMS 中内部类 Lifecycle 中的 getService 函数返回了 AMS 实例,那么 AMS 实例就相当于创建了。




  7. 最后启动 AMS 进程是在 Zygote 中执行的,可以参考应用进程中的流程图。


作者:DevYK
链接:https://juejin.cn/post/6844903894166798349
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动架构 (二) Android 中 Handler 架构分析,并实现自己简易版本 Handler 框架

移动架构 (一) 架构第一步,学会画各种 UML 图Android 中消息机制Android 的消息机制主要指 Handler 的运行机制,先来看下 Handler 的一张运行架构图来对 Handler 有个大概的了解。Handler 消息机制图:Handle...
继续阅读 »

移动架构 (一) 架构第一步,学会画各种 UML 图


Android 中消息机制

Android 的消息机制主要指 Handler 的运行机制,先来看下 Handler 的一张运行架构图来对 Handler 有个大概的了解。

Handler 消息机制图:

Handler-.png

Handler 类图:

Handler.png

以上图的解释:

  1. 以 Handler 的 sendMessage () 函数为例,当发送一个 message 后,会将此消息加入消息队列 MessageQueue 中。
  2. Looper 负责去遍历消息队列并且将队列中的消息分发非对应的 Handler 进行处理。
  3. 在 Handler 的 handlerMessage 方法中处理该消息,这就完成了一个消息的发送和处理过程。

这里从图中可以看到 Android 中 Handler 消息机制最重要的四个对象分别为 Handler 、Message 、MessageQueue 、Looper。

ThreadLocal 的工作原理

ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据, 数据存储以后,只有再指定线程中可以获取到存储的数据,对于其它线程来说则是无法获取到存储的对象。下面就是我们验证 ThreadLocal 存取是否是按照刚刚那样所说。

  • 子线程中存,子线程中取

     	// 代码测试       
    new Thread("thread-1"){
    @Override
    public void run() {
    ThreadLocal<String> mThread_A = new ThreadLocal();
    mThread_A.set("thread-1");
    System.out.println("mThread_A :"+mThread_A.get());

    }
    }.start();

    //打印结果
    mThread_A :thread-1
    复制代码
  • 主线程中存,子线程取

    	//主线程中存,子线程取    
    final ThreadLocal<String> mThread_B = new ThreadLocal();
    mThread_B.set("thread_B");
    new Thread(){
    @Override
    public void run() {
    System.out.println("mThread_B :"+mThread_B.get());
    }
    }.start();

    //打印结果
    mThread_B :null
    复制代码
  • 主线程存,主线程取

    	//主线程存,主线程取
    ThreadLocal<String> mThread_C = new ThreadLocal();
    mThread_C.set("thread_C");
    System.out.println("mThread_C :"+mThread_C.get());

    //打印结果
    mThread_C :thread_C
    复制代码

结果是不是跟上面我们所说的答案一样,那么为什么会是这样勒?现在我们带着问题去看下 ThreadLocal 源码到底做了什么?

ThreadLocal-.jpg

从上图可以 ThreadLocal 主要函数组成部分,这里我们用到了 set , get 那么就从 set , get 入手吧。

ThreadLocal set(T):

ThreadLocal-set.jpg

(图 1)

ThreadLocal-getMap.jpg

(图 2)

ThreadLocal-createMap.jpg

(图 3)

ThreadLocal-ThreadLocalMap-createMap-set.jpg

(图 四)

从 (图一) 得知 set 函数里面获取了当前线程,这里我们主要看下 getMap(currentThread) 主要干什么了?

从 (图二) 中我们得知 getMap 主要是从当前线程拿到 ThreadLocalMap 这个实例对象,如果当前线程的 ThreadLocalMap 为 NULL ,那么就 createMap ,这里的 ThreadLocalMap 可以暂时理解为一个集合对象就行了,它 (图四) 底层是一个数组实现的添加数据。

ThreadLocal T get():

ThreadLocal-get.jpg

这里的 get() 函数其实已经能够说明为什么在不同线程存储的数据拿不到了。因为存储是在当前线程存储的,取数据也是在当前所在的线程取得,所以不可能拿到的。带着问题我们找到了答案。是不是有点小激动呀?(^▽^)

Android 消息机制源码分析

这里我们就直接看源码,一下是我看源码的流程。

  1. 创建全局唯一的 Looper 对象和全局唯一 MessageQueue 消息对象。

    Handler--Looper-MessageQueue.png

  2. Activity 中创建 Handler。

    Handler-Activity-create.png

  3. Handler sendMessage 发送一个消息的走向。

    Handler-message-.png

  4. Handler 消息处理。

    Handler-06c719af736b41fb.png

消息阻塞和延时

阻塞和延时

Looper 的阻塞主要是靠 MessageQueue 来实现的,在 MessageQueue -> next() nativePollOnce(ptr, nextPollTimeoutMillis) 进行阻塞 , 在 MessageQueue -> enqueueMessage() -> nativeWake(mPtr) 进行唤醒。主要依赖 native 层的 looper epoll 进制进行的。

f3da65a44123337f1b5b586a02aad8eb.png

阻塞和延时,主要是 next() 的 nativePollOnce(ptr , nextPollTimeoutMillis) 调用 native 方法来操作管道,由 nextPollTimeoutMillis 决定是否需要阻塞 , nextPollTimeoutMilis 为 0 的时候表示不阻塞 , 为 -1 的时候表示一直阻塞直到被唤醒,其它时间表示延时。

唤醒

主要是指 enqueueMessage () @MessageQueue 进行唤醒。

Handler-.jpg

阻塞 -> 唤醒 消息切换

Handler-f6fe406dad09b444.jpg

总结

简单的理解阻塞和唤醒就是在主线程的 MessageQueue 没有消息时,便阻塞在 Loop 的 queue.next() 中的 nativePollOnce() 方法里面,此时主线程会释放 CPU 资源进入休眠状态,直到下一个消息到达或者有消息的时候才触发,通过往 pipe 管道写端写入数据来唤醒主线程工作。

这里采用的 epoll 机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪 (读或写就绪) , 则立刻通知相应程序进行读或者写操作,本质同步 I/O , 即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量的 CPU 资源。

延时入队

Handler-c4d53e3afdc11095.jpg

主要指 enqueueMessage() 消息入队列(Message 单链表),上图代码对 message 对象池重新排序,遵循规则 ( when 从小到大) 。

此处 for 死循环退出情况分为两种

  1. p == null 表示对象池中已经运行到了最后一个,无需要再循环。
  2. 碰到下一个消息 when 小于前一个,立马退出循环 (不管对象池中所有 message 是否遍历完) 进行重新排序。

好了,到了这里 Handler 源码分析算是告一段落了,下面我们来看下面试中容易被问起的问题。

常见问题分析

为什么不能在子线程中更新 UI ,根本原因是什么?

checkThread.jpg

mThread 是主线程,这里会检查当前线程是否是主线程,那么为什么没有在 onCreate 里面没有进行这个检查呢?这个问题原因出现在 Activity 的生命周期中 , 在 onCreate 方法中, UI 处于创建过程,对用户来说界面还不可见,直到 onStart 方法后界面可见了,再到 onResume 方法后页面可以交互,从某种程度来讲, 在 onCreate 方法中不能算是更新 UI,只能说是配置 UI,或者是设置 UI 属性。 这个时候不会调用到 ViewRootImpl.checkThread () , 因为 ViewRootImpl 没有创建。 而在 onResume 方法后, ViewRootImpl 才被创建。 这个时候去交户界面才算是更新 UI。

setContentView 知识建立了 View 树,并没有进行渲染工作 (其实真正的渲染工作实在 onResume 之后)。也正是建立了 View 树,因此我们可以通过 findViewById() 来获取到 View 对象,但是由于并没有进行渲染视图的工作,也就是没有执行 ViewRootImpl.performTransversal。同样 View 中也不会执行 onMeasure (), 如果在 onResume() 方法里直接获取 View.getHeight() / View.getWidth () 得到的结果总是 0。

为什么主线程用 Looper 死循环不会引发 ANR 异常?

简单来说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写入数据来唤醒主线程工作。这里采用的是 epoll 机制,是一种 IO 多路复用机制。

为什么 Handler 构造方法里面的 Looper 不是直接 new ?

如果在 Handler 构造方法里面直接 new Looper(), 可能是无法保证 Looper 唯一,只有用 Looper.prepare() 才能保证唯一性,具体可以看 prepare 方法。

MessageQueue 为什么要放在 Looper 私有构造方法初始化?

因为一个线程只绑定一个 Looper ,所以在 Looper 构造方法里面初始化就可以保证 mQueue 也是唯一的 Thread 对应一个 Looper 对应一个 mQueue。

Handler . post 的逻辑在哪个线程执行的?是由 Looper 所在线程还是 Handler 所在线程决定的?

由 Looper 所在线程决定的。逻辑是在 Looper.loop() 方法中,从 MessageQueue 中拿出 message ,并且执行其逻辑,这里在 Looper 中执行的,因此有 Looper 所在线程决定。

MessageQueue.next() 会因为发现了延迟消息,而进行阻塞。那么为什么后面加入的非延迟消息没有被阻塞呢?

可以参考 消息阻塞和延时 -> 唤醒

Handler 的 dispatchMessage () 分发消息的处理流程?

handlerMessage-.jpg

  1. 属于 Runnable 接口。

  2. 通过下面代码形式调用。

        private static Handler mHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
    return true;
    }
    });
    复制代码
  3. 如果第一步,第二部都不满足直接走下面 handlerMessage 参考下面代码实现方式

        private static Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
    super.handleMessage(msg);
    }
    };
    复制代码

也可以通过 debug 方式来具体看 dispatchMessage 执行状态。

实现自己的 Handler 简单架构

主要实现测试代码

Handler-34a4a4e9e149d8c4.jpg

代码传送阵


作者:DevYK
链接:https://juejin.cn/post/6844903892828815367
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

移动架构 (一) 架构第一步,学会画各种 UML 图

注意: 文章中 UML 图开始用是 Windows PowerDesigner 工具,后来换电脑了用的 StarUML。 UML 定义 UML 是统一建模语言, 是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开...
继续阅读 »

注意: 文章中 UML 图开始用是 Windows PowerDesigner 工具,后来换电脑了用的 StarUML。


UML


定义


UML 是统一建模语言, 是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法。


作用



  1. 帮组开发团队以一种可视化的方式理解系统的功能需求。

  2. 有利于开发团队队员之间在各个开发环节间确立沟通的标准,便于系统文档的制定和项目的管理。因为 UML 的简单、直观和标准性,在一个团队中用 UML 来交流比用文字说明的文档要好的多。

  3. UML 为非专业编程人士理解软件的功能和构造,提供了一种直白、简单、通俗的方法。

  4. 使用 UML 可以方便的理解各种框架的设计方式。


面向对象模型


用例图 (User Case Diagram)


概述



  • 用例图主要模拟系统中的动态行为,并且描述了用户、需求、以及系统功能单元之间的关系。

  • 用例图由参与者 (用户) ,用例 (功能) 和它们之间的关系组成。


目的



  1. 用来收集系统的要求。

  2. 用于获取系统的外观图。

  3. 识别外部和内部影响因素。

  4. 显示要求之间的相互作用是参与者。


构成元素















































组成元素 说明 符号表示
参与者 (Actor) 表示与你自己的程序或者系统进行正在交互的动作。用一个小人表示
用例 (User Case) 表示在一个系统或者程序中某个功能的描述。用一个椭圆代表
关联关系 (Association) 表示参与者与用例之间的关系。用一个箭头表示
包含关系 (Include) 表示一个大的功能分解成多个小模块的动作。用一个带包含文字的虚线箭头表示
扩展关系 (Extend) 表示用例功能的延伸,相当于是为用例提供附加功能。用一个带扩展文字的虚线箭头表示
依赖 (dependency) 表示一个用例依赖于另一个用例(相当于程序里面的一个类引用另一个类的关系)。用一个带依赖文字的虚线箭头表示
泛化 (Generalization) 相当于程序里面的继承关系。用一个箭头表示

用例图例子


需求: 以一个登录的例子来画一个用例图



  1. 包含 登录/注册/

  2. 登录/注册 支持手机号码、第三方 QQ/weichat/GitHub 登录注册


效果图:



提供的登录用例基本上已经包含了刚刚所学的组成元素部分。


结构图


类图 (Class Diagram)


概念

类图 (Class Diagram) 是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其它类的关系等。


类图不显示暂时性的信息,类图是面向对象建模的主要组成部分。它即用于应用程序的系统分类的一般概念建模,也用于详细建模,将模型转换成编程代码。


构成元素









































构成元素 说明 表示符号
泛化 (Generalization) 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征和行为。用一个带三角箭头的实线,箭头指向父类表示
实现 (Realization) 是一种类与接口的关系, 表示类是接口所有特征和行为的实现。用一个带三角箭头的虚线,箭头指向接口表示.
关联 (Association) 1. 是一种拥有的关系, 它使一个类知道另一个类的属性和方法. 2.关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头。用一个带普通箭头的实心线,指向被拥有者
依赖 (Dependency) 是一种使用的关系, 即一个类的实现需要另一个类的协助, 所以要尽量不使用双向的互相依赖.用一个带箭头的虚线,指向被使用者
聚合 (Aggregation) 聚合是一种特殊的关联 (Association) 形式,表示两个对象之间的所属 (has-a) 关系。所有者对象称为聚合对象,它的类称为聚合类;从属对象称为被聚合对象,它的类称为被聚合类。例如,一个公司有很多员工就是公司类 Company 和员工类Employee 之间的一种聚合关系。被聚合对象和聚合对象有着各自的生命周期,即如果公司倒闭并不影响员工的存在。用一个带空心菱形的实心线,菱形指向整体
VXn1BV.png
组合 (Composition) 是整体与部分的关系, 但部分不能离开整体而单独存在. 如公司和部门是整体和部分的关系, 没有公司就不存在部门。用一个带实心菱形的实线,菱形指向整体表示。

类图例子

需求: 基于 google 官方 MVP 架构 绘制一个基本的 MVP 类图架构


ZIm7Fg.jpg


组合结构图 (Composite Structure Diagram)


概念

用来显示组合结构或部分系统的内部构造,包括类、接口、包、组件、端口和连接器等元素。比类图更抽象的表示,一般来说先画组合结构图,再画类图。


构成元素





























































构成元素 说明 表示符号
类 (Class) 表示对某件事物的描述
class.jpg
接口 (Interface) 表示用于对 Class 的说明
25a5bf4ec2a49f23bbc971fb55242484.jpg
端口 (port) 表示部件和外部环境的交互点
958803fcc1ecf9dd710a7fa4d3d7f284.jpg
部件 (part) 表示被描述事物所拥有的内部成分
388d69ae3fb52b2777f1efa2051e2d03.jpg
泛化 (Generalication) 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征和行为。用一个带三角箭头的实线,箭头指向父类表示
实现 (Realization) 是一种类与接口的关系, 表示类是接口所有特征和行为的实现。用一个带三角箭头的虚线,箭头指向接口表示.
关联 (Association) 1. 是一种拥有的关系, 它使一个类知道另一个类的属性和方法. 2.关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头。用一个带普通箭头的实心线,指向被拥有者
依赖 (Dependency) 是一种使用的关系, 即一个类的实现需要另一个类的协助, 所以要尽量不使用双向的互相依赖.用一个带箭头的虚线,指向被使用者
聚合 (Aggregation) 聚合是一种特殊的关联 (Association) 形式,表示两个对象之间的所属 (has-a) 关系。所有者对象称为聚合对象,它的类称为聚合类;从属对象称为被聚合对象,它的类称为被聚合类。例如,一个公司有很多员工就是公司类 Company 和员工类Employee 之间的一种聚合关系。被聚合对象和聚合对象有着各自的生命周期,即如果公司倒闭并不影响员工的存在。用一个带空心菱形的实心线,菱形指向整体
VXn1BV.png
组合 (Composition) 是整体与部分的关系, 但部分不能离开整体而单独存在. 如公司和部门是整体和部分的关系, 没有公司就不存在部门。用一个带实心菱形的实线,菱形指向整体表示。

注意事项

侧重类的整体特性,就用类图;侧重类的内部结构,就使用组合结构图。


组合结构图例子

Composite-Structures1-.md.png


对象图 (Object Diagram)


概念

显示某时刻对象和对象之间的关系


构成元素


























构成元素 说明 表示符号
对象 (Object) 代表某个事物
class.jpg
实例链接 (Instance Link) 链是类之间关系的实例
-3c9ce1846469aa82.jpg
依赖 (Dependency) 想当于 A 对象使用 B 对象里面的属性

对象图例子


包图 (Package Diagram)


概念

包与包的之间的关系


构成元素


























构成元素 说明 表示符号
包 (Package) 当对一个比较复杂的软件系统进行建模时,会有大量的类、接口、组件、节点和图需要处理;如果放在同一个地方的话,信息量非常的大,显得很乱,不方便查询,所以就对这些信息进行分组,将语义或者功能相同的放在同一个包中,这样就便于理解和处理整个模型
PackageDiagram1.png
泛化 (Generalization) 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征和行为。用一个带三角箭头的实线,箭头指向父类表示
依赖 (Dependency) 是一种使用的关系, 即一个类的实现需要另一个类的协助, 所以要尽量不使用双向的互相依赖.用一个带箭头的虚线,指向被使用者

包图例子

PackageDiagram2.md.png


动态图


时序图 (Sequence Diagram)


概念

时序图(Sequence Diagram) , 又名序列图、循序图、顺序图,是一种UML交互图。


它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。


它可以表示用例的行为顺序,当执行一个用例行为时,其中的每条消息对应一个类操作或状态机中引起转换的触发事件。


构成元素































构成元素 说明 表示符号
参与者 (Actor) 表示与你自己的程序或者系统进行正在交互的动作。用一个小人表示
对象 (Object) 代表某个事物
class.jpg
控制焦点 (Activation) 控制焦点是顺序图中表示时间段的符号,在这个时间段内对象将执行相应的操作。用小矩形表示
1563183352712.jpg
消息 (Message) 消息一般分为同步消息(Synchronous Message),异步消息(Asynchronous Message)和返回消息(Return Message)
1563183471285.jpg

时序图例子

需求:这里为了简单就用一个登陆的时序图为参考


SequenceDiagram_.png


通讯图 (Communication Diagram)


概念

顺序图强调先后顺序,通信图则是强调相互之间的关系。顺序图和通信图基本同构,但是很少使用通信图,因为顺序图更简洁,更直观。


构成元素































构成元素 说明 表示符号
参与者 (Actor) 表示与你自己的程序或者系统进行正在交互的动作。用一个小人表示
对象 (Object) 代表某个事物
class.jpg
实例链接 (Instance Link) 链是类之间关系的实例
-3c9ce1846469aa82.jpg
消息 (Message) 消息一般分为同步消息(Synchronous Message),异步消息(Asynchronous Message)和返回消息(Return Message)
1563183471285.jpg

通讯图例子


活动图 (Activity Diagram)


概念

活动图是 UML 用于对系统的动态行为建模的另一种常用工具,它描述活动的顺序,展现从一个活动到另一个活动的控制流。活动图在本质上是一种流程图。活动图着重表现从一个活动到另一个活动的控制流,是内部处理驱动的流程。


构成元素









































构成元素 说明 表示符号
活动 (Activity) 活动状态用于表达状态机中的非原子的运行
1563204107193.jpg
对象节点 (Object Node) 某件事物的具体代表
1563204175149.jpg
判断 (Decision) 对某个事件进行判断
1563204318945.jpg
同步 (synchronization) 指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程;
1563204447903.jpg
开始 (final) 表示成实心黑色圆点
1563204607110.jpg
结束 (Flow Final) 分为活动终止节点(activity final nodes)和流程终止节点(flow final nodes)。而流程终止节点表示是子流程的结束。
1563204701765.jpg

活动图例子

需求: 点开直播 -> 观看直播的动作


cdcf1ece24ce8cf2829939376955d829.jpg


状态图 (Statechart Diagram)


概念

描述了某个对象的状态和感兴趣的事件以及对象响应该事件的行为。转换 (transition) 用标记有事件的箭头表示。状态(state)用圆角矩形表示。通常的做法会包含一个初始状态,当实例创建时,自动从初始状态转换到另外一个状态。


状态图显示了对象的生命周期:即对象经历的事件、对象的转换和对象在这些事件之间的状态。当然,状态图不必要描述所有的事件。


构成元素




































构成元素 说明 表示符号
开始 (final) 表示成实心黑色圆点
1563204607110.jpg
结束 (Flow Final) 分为活动终止节点(activity final nodes)和流程终止节点(flow final nodes)。而流程终止节点表示是子流程的结束。
1563204701765.jpg
状态 (state) 某一时刻变化的记录
1563205220353.jpg
过渡 (Transition) 相当于 A 点走向 B 点的过渡
1563205289524.jpg
同步 (synchronization) 共同执行一个指令
1563204447903.jpg

状态图例子

需求: 这里直接借鉴 Activity 官方状态图


Activity-.jpg


交错纵横图 (Interaction overview Diagram)


概念

用来表示多张图之间的关联


构成元素




































构成元素 说明 表示符号
开始 (final) 表示成实心黑色圆点
1563204607110.jpg
结束 (Flow Final) 分为活动终止节点(activity final nodes)和流程终止节点(flow final nodes)。而流程终止节点表示是子流程的结束。
1563204701765.jpg
同步 (synchronization) 共同执行一个指令
1563204447903.jpg
判断 (Decision) 对某个事件进行判断
1563204318945.jpg
流 (Flow) 事件流的走向 可以参考,开始跟结束

交错纵横图例子

82ea391d3a841e9097d737c315be3879.png


交互图


组件图 (Component Diagram)


概念

组件图(component diagram)是用来反映代码的物理结构。从组件图中,您可以了解各软件组件(如源代码文件或动态链接库)之间的编译器和运行时依赖关系。使用组件图可以将系统划分为内聚组件并显示代码自身的结构


构成元素





















构成元素 说明 表示符号
组件 (Component) 组件用一个左侧带有突出两个小矩形的矩形来表示
35397ded31ae2f0576de21395a532b6c.jpg
接口 (Interface) 接口由一组操作组成,它指定了一个契约,这个契约必须由实现和使用这个接口的构件的所遵循
1aa3560034ea28d1a9f621bb59d3cc5f.jpg

组件图例子

810389027b2bc0e8a2bd6432147372a8.png


部署图 (Deployment Diagram)


概念

部署图可以用于描述规范级别的架构,也可以描述实例级别的架构。这与类图和对象图有点类似,做系统集成很方便。


构成元素


























构成元素 说明 表示符号
节点 (node) 结点是存在与运行时的代表计算机资源的物理元素,可以是硬件也可以是运行其上的软件系统
node.jpg
节点实例 (Node Instance) 与结点的区别在于名称有下划线
466029a462fe609fa554ddfe910c6050.jpg
物件(Artifact) 物件是软件开发过程中的产物,包括过程模型(比如用例图、设计图等等)、源代码、可执行程序、设计文档、测试报告、需求原型、用户手册等等。
5adb2c458e0218a5094b76d8b0564101.jpg

部署图例子

617cd1f8a76331806f29f144ee9b5912.png


经典例子


微信支付时序图



总结


只要掌握常用的几种图 (用例图、类图、时序图、活动图) ,就已经迈向架构第一步了,加油!


作者:DevYK
链接:https://juejin.cn/post/6844903891067207693
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

美团外卖Android Crash治理之路

Crash率是衡量一个App好坏的重要指标之一,如果你忽略了它的存在,它就会愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失。本文讲述美团外卖Android客户端团队在将App的Crash率从千分之三做到万分之二过程中所做的大量实践工作,抛砖引玉...
继续阅读 »

Crash率是衡量一个App好坏的重要指标之一,如果你忽略了它的存在,它就会愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失。本文讲述美团外卖Android客户端团队在将App的Crash率从千分之三做到万分之二过程中所做的大量实践工作,抛砖引玉,希望能够为其他团队提供一些经验和启发。


面临的挑战和成果


面对用户使用频率高,外卖业务增长快,Android碎片化严重这些问题,美团外卖Android App如何持续的降低Crash率,是一项极具挑战的事情。通过团队的全力全策,美团外卖Android App的平均Crash率从千分之三降到了万分之二,最优值万一左右(Crash率统计方式:Crash次数/DAU)。


美团外卖自2013年创建以来,业务就以指数级的速度发展。美团外卖承载的业务,从单一的餐饮业务,发展到餐饮、超市、生鲜、果蔬、药品、鲜花、蛋糕、跑腿等十多个大品类业务。目前美团外卖日完成订单量已突破2000万,成为美团点评最重要的业务之一。美团外卖客户端所承载的业务模块越来越多,产品复杂度越来越高,团队开发人员日益增加,这些都给App降低Crash率带来了巨大的挑战。


Crash的治理实践


对于Crash的治理,我们尽量遵守以下三点原则:



  • 由点到面。一个Crash发生了,我们不能只针对这个Crash的去解决,而要去考虑这一类Crash怎么去解决和预防。只有这样才能使得这一类Crash真正被解决。

  • 异常不能随便吃掉。随意的使用try-catch,只会增加业务的分支和隐蔽真正的问题,要了解Crash的本质原因,根据本质原因去解决。catch的分支,更要根据业务场景去兜底,保证后续的流程正常。

  • 预防胜于治理。当Crash发生的时候,损失已经造成了,我们再怎么治理也只是减少损失。尽可能的提前预防Crash的发生,可以将Crash消灭在萌芽阶段。


常规的Crash治理


常规Crash发生的原因主要是由于开发人员编写代码不小心导致的。解决这类Crash需要由点到面,根据Crash引发的原因和业务本身,统一集中解决。常见的Crash类型包括:空节点、角标越界、类型转换异常、实体对象没有序列化、数字转换异常、Activity或Service找不到等。这类Crash是App中最为常见的Crash,也是最容易反复出现的。在获取Crash堆栈信息后,解决这类Crash一般比较简单,更多考虑的应该是如何避免。下面介绍两个我们治理的量比较大的Crash。


NullPointerException


NullPointerException是我们遇到最频繁的,造成这种Crash一般有两种情况:



  • 对象本身没有进行初始化就进行操作。

  • 对象已经初始化过,但是被回收或者手动置为null,然后对其进行操作。


针对第一种情况导致的原因有很多,可能是开发人员的失误、API返回数据解析异常、进程被杀死后静态变量没初始化导致,我们可以做的有:



  • 对可能为空的对象做判空处理。

  • 养成使用@NonNull和@Nullable注解的习惯。

  • 尽量不使用静态变量,万不得已使用SharedPreferences来存储。

  • 考虑使用Kotlin语言。


针对第二种情况大部分是由于Activity/Fragment销毁或被移除后,在Message、Runnable、网络等回调中执行了一些代码导致的,我们可以做的有:



  • Message、Runnable回调时,判断Activity/Fragment是否销毁或被移除;加try-catch保护;Activity/Fragment销毁时移除所有已发送的Runnable。

  • 封装LifecycleMessage/Runnable基础组件,并自定义Lint检查,提示使用封装好的基础组件。

  • 在BaseActivity、BaseFragment的onDestory()里把当前Activity所发的所有请求取消掉。


IndexOutOfBoundsException


这类Crash常见于对ListView的操作和多线程下对容器的操作。


针对ListView中造成的IndexOutOfBoundsException,经常是因为外部也持有了Adapter里数据的引用(如在Adapter的构造函数里直接赋值),这时如果外部引用对数据更改了,但没有及时调用notifyDataSetChanged(),则有可能造成Crash,对此我们封装了一个BaseAdapter,数据统一由Adapter自己维护通知, 同时也极大的避免了The content of the adapter has changed but ListView did not receive a notification,这两类Crash目前得到了统一的解决。


另外,很多容器是线程不安全的,所以如果在多线程下对其操作就容易引发IndexOutOfBoundsException。常用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap,同时也要注意有一些类的内部实现也是用的线程不安全的容器,如Bundle里用的就是ArrayMap。


系统级Crash治理


众所周知,Android的机型众多,碎片化严重,各个硬件厂商可能会定制自己的ROM,更改系统方法,导致特定机型的崩溃。发现这类Crash,主要靠云测平台配合自动化测试,以及线上监控,这种情况下的Crash堆栈信息很难直接定位问题。下面是常见的解决思路:



  1. 尝试找到造成Crash的可疑代码,看是否有特异的API或者调用方式不当导致的,尝试修改代码逻辑来进行规避。

  2. 通过Hook来解决,Hook分为Java Hook和Native Hook。Java Hook主要靠反射或者动态代理来更改相应API的行为,需要尝试找到可以Hook的点,一般Hook的点多为静态变量,同时需要注意Android不同版本的API,类名、方法名和成员变量名都可能不一样,所以要做好兼容工作;Native Hook原理上是用更改后方法把旧方法在内存地址上进行替换,需要考虑到Dalvik和ART的差异;相对来说Native Hook的兼容性更差一点,所以用Native Hook的时候需要配合降级策略。

  3. 如果通过前两种方式都无法解决的话,我们只能尝试反编译ROM,寻找解决的办法。


我们举一个定制系统ROM导致Crash的例子,根据Crash平台统计数据发现该Crash只发生在vivo V3Max这类机型上,Crash堆栈如下:


java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:304)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
at java.util.concurrent.FutureTask.run(FutureTask.java:242)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
at android.widget.AbsListView$UpdateBottomFlagTask.isSuperFloatViewServiceRunning(AbsListView.java:7689)
at android.widget.AbsListView$UpdateBottomFlagTask.doInBackground(AbsListView.java:7665)
at android.os.AsyncTask$2.call(AsyncTask.java:292)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
... 4 more
复制代码

我们发现原生系统上对应系统版本的AbsListView里并没有UpdateBottomFlagTask类,因此可以断定是vivo该版本定制的ROM修改了系统的实现。我们在定位这个Crash的可疑点无果后决定通过Hook的方式解决,通过源码发现AsyncTask$SerialExecutor是静态变量,是一个很好的Hook的点,通过反射添加try-catch解决。因为修改的是final对象所以需要先反射修改accessFlags,需要注意ART和Dalvik下对应的Class不同,代码如下:


  public static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field artField = Field.class.getDeclaredField("artField");
artField.setAccessible(true);
Object artFieldValue = artField.get(field);
Field accessFlagsFiled = artFieldValue.getClass().getDeclaredField("accessFlags");
accessFlagsFiled.setAccessible(true);
accessFlagsFiled.setInt(artFieldValue, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}

复制代码
private void initVivoV3MaxCrashHander() {
if (!isVivoV3()) {
return;
}
try {
setFinalStatic(AsyncTask.class.getDeclaredField("SERIAL_EXECUTOR"), new SafeSerialExecutor());
Field defaultfield = AsyncTask.class.getDeclaredField("sDefaultExecutor");
defaultfield.setAccessible(true);
defaultfield.set(null, AsyncTask.SERIAL_EXECUTOR);
} catch (Exception e) {
L.e(e);
}
}
复制代码

美团外卖App用上述方法解决了对应的Crash,但是美团App里的外卖频道因为平台的限制无法通过这种方式,于是我们尝试反编译ROM。
Android ROM编译时会将framework、app、bin等目录打入system.img中,system.img是Android系统中用来存放系统文件的镜像 (image),文件格式一般为yaffs2或ext。但Android 5.0开始支持dm-verity后,system.img不再提供,而是提供了三个文件system.new.dat,system.patch.dat,system.transfer.list,因此我们首先需要通过上述的三个文件得到system.img。但我们将vivo ROM解压后发现厂商将system.new.dat进行了分片,如下图所示:



经过对system.transfer.list中的信息和system.new.dat 1 2 3 ... 文件大小对比研究,发现一些共同点,system.transfer.list中的每一个block数*4KB 与对应的分片文件的大小大致相同,故大胆猜测,vivo ROM对system.patch.dat分片也只是单纯的按block先后顺序进行了分片处理。所以我们只需要在转化img前将这些分片文件合成一个system.patch.dat文件就可以了。最后根据system.img的文件系统格式进行解包,拿到framework目录,其中有framework.jar和boot.oat等文件,因为Android4.4之后引入了ART虚拟机,会预先把system/framework中的一些jar包转换为oat格式,所以我们还需要将对应的oat文件通过ota2dex将其解包获得dex文件,之后通过dex2jarjd-gui查看源码。


OOM


OOM是OutOfMemoryError的简称,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草。
导致OOM的原因大部分如下:



  • 内存泄漏,大量无用对象没有被及时回收导致后续申请内存失败。

  • 大内存对象过多,最常见的大对象就是Bitmap,几个大图同时加载很容易触发OOM。


内存泄漏
内存泄漏指系统未能及时释放已经不再使用的内存对象,一般是由错误的程序代码逻辑引起的。在Android平台上,最常见也是最严重的内存泄漏就是Activity对象泄漏。Activity承载了App的整个界面功能,Activity的泄漏同时也意味着它持有的大量资源对象都无法被回收,极其容易造成OOM。
常见的可能会造成Activity泄漏的原因有:



  • 匿名内部类实现Handler处理消息,可能导致隐式持有的Activity对象无法回收。

  • Activity和Context对象被混淆和滥用,在许多只需要Application Context而不需要使用Activity对象的地方使用了Activity对象,比如注册各类Receiver、计算屏幕密度等等。

  • View对象处理不当,使用Activity的LayoutInflater创建的View自身持有的Context对象其实就是Activity,这点经常被忽略,在自己实现View重用等场景下也会导致Activity泄漏。


对于Activity泄漏,目前已经有了一个非常好用的检测工具:LeakCanary,它可以自动检测到所有Activity的泄漏情况,并且在发生泄漏时给出十分友好的界面提示,同时为了防止开发人员的疏漏,我们也会将其上报到服务器,统一检查解决。另外我们可以在debug下使用StrictMode来检查Activity的泄露、Closeable对象没有被关闭等问题。


大对象
在Android平台上,我们分析任一应用的内存信息,几乎都可以得出同样的结论:占用内存最多的对象大都是Bitmap对象。随着手机屏幕尺寸越来越大,屏幕分辨率也越来越高,1080p和更高的2k屏已经占了大半份额,为了达到更好的视觉效果,我们往往需要使用大量高清图片,同时也为OOM埋下了祸根。
对于图片内存优化,我们有几个常用的思路:



  • 尽量使用成熟的图片库,比如Glide,图片库会提供很多通用方面的保障,减少不必要的人为失误。

  • 根据实际需要,也就是View尺寸来加载图片,可以在分辨率较低的机型上尽可能少地占用内存。除了常用的BitmapFactory.Options#inSampleSize和Glide提供的BitmapRequestBuilder#override之外,我们的图片CDN服务器也支持图片的实时缩放,可以在服务端进行图片缩放处理,从而减轻客户端的内存压力。
    分析App内存的详细情况是解决问题的第一步,我们需要对App运行时到底占用了多少内存、哪些类型的对象有多少个有大致了解,并根据实际情况做出预测,这样才能在分析时做到有的放矢。Android Studio也提供了非常好用的Memory Profiler堆转储分配跟踪器功能可以帮我们迅速定位问题。


AOP增强辅助


AOP是面向切面编程的简称,在Android的Gradle插件1.5.0中新增了Transform API之后,编译时修改字节码来实现AOP也因为有了官方支持而变得非常方便。
在一些特定情况下,可以通过AOP的方式自动处理未捕获的异常:



  • 抛异常的方法非常明确,调用方式比较固定。

  • 异常处理方式比较统一。

  • 和业务逻辑无关,即自动处理异常后不会影响正常的业务逻辑。典型的例子有读取Intent Extras参数、读取SharedPreferences、解析颜色字符串值和显示隐藏Window等等。


这类问题的解决原理大致相同,我们以Intent Extras为例详细介绍一下。读取Intent Extras的问题在于我们非常常用的方法 Intent#getStringExtra 在代码逻辑出错或者恶意攻击的情况下可能会抛出ClassNotFoundException异常,而我们平时在写代码时又不太可能给所有调用都加上try-catch语句,于是一个更安全的Intent工具类应运而生,理论上只要所有人都使用这个工具类来访问Intent Extras参数就可以防止此类型的Crash。但是面对庞大的旧代码仓库和诸多的业务部门,修改现有代码需要极大成本,还有更多的外部依赖SDK基本不可能使用我们自己的工具类,此时就需要AOP大展身手了。
我们专门制作了一个Gradle插件,只需要配置一下参数就可以将某个特定方法的调用替换成另一个方法:


WaimaiBytecodeManipulator {
replacements(
"android/content/Intent.getIntExtra(Ljava/lang/String;I)I=com/waimai/IntentUtil.getInt(Landroid/content/Intent;Ljava/lang/String;I)I",
"android/content/Intent.getStringExtra(Ljava/lang/String;)Ljava/lang/String;=com/waimai/IntentUtil.getString(Landroid/content/Intent;Ljava/lang/String;)Ljava/lang/String;",
"android/content/Intent.getBooleanExtra(Ljava/lang/String;Z)Z=com/waimai/IntentUtil.getBoolean(Landroid/content/Intent;Ljava/lang/String;Z)Z",
...)
}
}
复制代码

上面的配置就可以将App代码(包括第三方库)里所有的Intent.getXXXExtra调用替换成IntentUtil类中的安全版实现。当然,并不是所有的异常都只需要catch住就万事大吉,如果真的有逻辑错误肯定需要在开发和测试阶段及时暴露出来,所以在IntentUtil中会对App的运行环境做判断,Debug下会将异常直接抛出,开发同学可以根据Crash堆栈分析问题,Release环境下则在捕获到异常时返回对应的默认值然后将异常上报到服务器。


依赖库的问题


Android App经常会依赖很多AAR, 每个AAR可能有多个版本,打包时Gradle会根据规则确定使用的最终版本号(默认选择最高版本或者强制指定的版本),而其他版本的AAR将被丢弃。如果互相依赖的AAR中有不兼容的版本,存在的问题在打包时是不能发现的,只有在相关代码执行时才会出现,会造成NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError等异常。如图所示,order和store两个业务库都依赖了platform.aar,一个是1.0版本,一个是2.0版本,默认最终打进APK的只有platform 2.0版本,这时如果order库里用到的platform库里的某个类或者方法在2.0版本中被删除了,运行时就可能发生异常,虽然SDK在升级时会尽量做到向下兼容,但很多时候尤其是第三方SDK是没法得到保证的,在美团外卖Android App v6.0版本时因为这个原因导致热修复功能丧失,因此为了提前发现问题,我们接入了依赖检查插件Defensor。




Defensor在编译时通过DexTask获取到所有的输入文件(也就是被编译过的class文件),然后检查每个文件里引用的类、字段、方法等是否存在。



除此之外我们写了一个Gradle插件SVD(strict version dependencies)来对那些重要的SDK的版本进行统一管理。插件会在编译时检查Gradle最终使用的SDK版本是否和配置中的一致,如果不一致插件会终止编译并报错,并同时会打印出发生冲突的SDK的所有依赖关系。


Crash的预防实践


单纯的靠约定或规范去减少Crash的发生是不现实的。约定和规范受限于组织架构和具体执行的个人,很容易被忽略,只有靠工程架构和工具才能保证Crash的预防长久的执行下去。


工程架构对Crash率的影响


在治理Crash的实践中,我们往往忽略了工程架构对Crash率的影响。Crash的发生大部分原因是源于程序员的不合理的代码,而程序员工作中最直接的接触的就是工程架构。对于一个边界模糊,层级混乱的架构,程序员是更加容易写出引起Crash的代码。在这样的架构里面,即使程序员意识到导致某种写法存在问题,想要去改善这样不合理的代码,也是非常困难的。相反,一个层级清晰,边界明确的架构,是能够大大减少Crash发生的概率,治理和预防Crash也是相对更容易。这里我们可以举几个我们实践过的例子阐述。


业务模块的划分
原来我们的Crash基本上都是由个别同学关注解决的,团队里的每个同学都会提交可能引起Crash的代码,如果负责Crash的同学因为某些事情,暂时没有关注App的Crash率,那么造成Crash的同学也不会知道他的代码引起了Crash。


对于这个问题,我们的做法是App的业务模块化。业务模块化后,每个业务都有都有唯一包名和对应的负责人。当某个模块发生了Crash,可以根据包名提交问题给这个模块的负责人,让他第一时间进行处理。业务模块化本身也是工程架构优先需要考虑的事情之一。


页面跳转路由统一处理页面跳转
对外卖App而言,使用过程中最多的就是页面间的跳转,而页面间跳转经常会造成ActivityNotFoundException,例如我们配了一个scheme,但对方的scheme路径已经发生了变化;又例如,我们调用手机上相册的功能,而相册应用已被用户自己禁用或移除了。解决这一类Crash,其实也很简单,只需要在startActivity增加ActivityNotFoundException异常捕获即可。但一个App里,启动Activity的地方,几乎是随处可见,无法预测哪一处会造成ActivityNotFoundException。
我们的做法是将页面的跳转,都通过我们封装的scheme路由去分发。这样的好处是,通过scheme路由,在工程架构上所有业务都是解耦,模块间不需要相互依赖就可以实现页面的跳转和基本类型参数的传递;同时,由于所有的页面跳转都会走scheme路由,我们只需要在scheme路由里一处加上ActivityNotFoundException异常捕获即可解决这种类型的Crash。路由设计示意图如下:



网络层统一处理API脏数据
客户端的很大一部分的Crash是因为API返回的脏数据。比如当API返回空值、空数组或返回不是约定类型的数据,App收到这些数据,就极有可能发生空指针、数组越界和类型转换错误等Crash。而且这样的脏数据,特别容易引起线上大面积的崩溃。
最早我们的工程的网络层用法是:页面监听网络成功和失败的回调,网络成功后,将JSON数据传递给页面,页面解析Model,初始化View,如图所示。这样的问题就是,网络虽然请求成功了,但是JSON解析Model这个过程可能存在问题,例如没有返回数据或者返回了类型不对的数据,而这个脏数据导致问题会出现在UI层,直接反应给用户。



根据上图,我们可以看到由于网络层只承担了请求网络的职责,没有承担数据解析的职责,数据解析的职责交给了页面去处理。这样使得我们一旦发现脏数据导致的Crash,就只能在网络请求的回调里面增加各种判断去兼容脏数据。我们有几百个页面,补漏完全补不过来。通过几个版本的重构,我们重新划分了网络层的职责,如图所示:



从图上可以看出,重构后的网络层负责请求网络和数据解析,如果存在脏数据的话,在网络层就会发现问题,不会影响到UI层,返回给UI层的都是校验成功的数据。这样改造后,我们发现这类的Crash率有了极大的改善。


大图监控


上面讲到大对象是导致OOM的主要原因之一,而Bitmap是App里最常见的大对象类型,因此对占用内存过大的Bitmap对象的监控就很有必要了。
我们用AOP方式Hook了三种常见图片库的加载图片回调方法,同时监控图片库加载图片时的两个维度:



  1. 加载图片使用的URL。外卖App中除静态资源外,所有图片都要求发布到专用的图片CDN服务器上,加载图片时使用正则表达式匹配URL,除了限定CDN域名之外还要求所有图片加载时都要添加对应的动态缩放参数。

  2. 最终加载出的图片结果(也就是Bitmap对象)。我们知道Bitmap对象所占内存和其分辨率大小成正比,而一般情况下在ImageView上设置超过自身尺寸的图片是没有意义的,所以我们要求显示在ImageView中的Bitmap分辨率不允许超过View自身的尺寸(为了降低误报率也可以设定一个报警阈值)。


开发过程中,在App里检测到不合规的图片时会立即高亮出错的ImageView所在的位置并弹出对话框提示ImageView所在的Activity、XPath和加载图片使用的URL等信息,如下图,辅助开发同学定位并解决问题。在Release环境下可以将报警信息上报到服务器,实时观察数据,有问题及时处理。


Lint检查


我们发现线上的很多Crash其实可以在开发过程中通过Lint检查来避免。Lint是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题,提醒开发人员及早修正,提高代码质量。


但是Android原生提供的Lint规则(如是否使用了高版本API)远远不够,缺少一些我们认为有必要的检测,也不能检查代码规范。因此我们开始开发自定义Lint,目前我们通过自定义Lint规则已经实现了Crash预防、Bug预防、提升性能/安全和代码规范检查这些功能。如检查实现了Serializable接口的类,其成员变量(包括从父类继承的)所声明的类型都要实现Serializable接口,可以有效的避免NotSerializableException;强制使用封装好的工具类如ColorUtil、WindowUtil等可以有效的避免因为参数不正确产生的IllegalArgumentException和因为Activity已经finish导致的BadTokenException。


Lint检查可以在多个阶段执行,包括在本地手动检查、编码实时检查、编译时检查、commit时检查,以及在CI系统中提Pull Request时检查、打包时检查等,如下图所示。更详细的内容可参考《美团外卖Android Lint代码检查实践》



资源重复检查


在之前的文章《美团外卖Android平台化架构演进实践》中讲述了我们的平台化演进过程,在这个过程中大家很大的一部分工作是下沉,但是下沉不完全就会导致一些类和资源的重复,类因为有包名的限制不会出现问题。但是一些资源文件如layout、drawable等如果同名则下层会被上层覆盖,这时layout里view的id发生了变化就可能导致空指针的问题。为了避免这种问题,我们写了一个Gradle插件通过hook MergeResource这个Task,拿到所有library和主库的资源文件,如果检查到重复则会中断编译过程,输出重复的资源名及对应的library name,同时避免有些资源因为样式等原因确实需要覆盖,因此我们设置了白名单。同时在这个过程中我们也拿到了所有的的图片资源,可以顺手做图片大小的本地监控,如下图所示:


Crash的监控&止损的实践


监控


在经过前面提到的各种检查和测试之后,应用便开始发布了。我们建立了如下图的监控流程,来保证异常发生时能够及时得到反馈并处理。首先是灰度监控,灰度阶段是增量Crash最容易暴露的阶段,如果这个阶段没有很好的把握住,会使得增量变存量,从而导致Crash率上升。如果条件允许的话,可以在灰度期间制定一些灰度策略去提高这个阶段Crash的暴露。例如分渠道灰度、分城市灰度、分业务场景灰度、新装用户的灰度等等,尽量覆盖所有的分支。灰度结束之后便开始全量,在全量的过程中我们还需要一些日常Crash监控和Crash率的异常报警来防止突发情况的发生,例如因为后台上线或者运营配置错误导致的线上Crash。除此之外还需要一些其他的监控,例如,之前提到的大图监控,来避免因为大图导致的OOM。具体的输出形式主要有邮件通知、IM通知、报表。



止损


尽管我们在前面做了那么多,但是Crash还是无法避免的,例如,在灰度阶段因为量级不够,有些Crash没有被暴露出来;又或者某些功能客户端比后台更早上线,而这些功能在灰度阶段没有被覆盖到;这些情况下,如果出现问题就需要考虑如何止损了。


问题发生时首先需要评估重要性,如果问题不是很严重而且修复成本较高可以考虑在下个版本再修复,相反如果问题比较严重,对用户体验或下单有影响时就必须要修复。修复时首先考虑业务降级,主要看该部分异常的业务是否有兜底或者A/B策略,这样是最稳妥也是最有效的方式。如果业务不能降级就需要考虑热修复了,目前美团外卖Android App接入的热修复框架是自研的Robust,可以修复90%以上的场景,热修成功率也达到了99%以上。如果问题发生在热修复无法覆盖的场景,就只能强制用户升级。强制升级因为覆盖周期长,同时影响用户的体验,只在万不得已的情况下才会使用。


展望


Crash的自我修复


我们在做新技术选型时除了要考虑是否能满足业务需求、是否比现有技术更优秀和团队学习成本等因素之外,兼容性和稳定性也非常重要。但面对国内非富多彩的Android系统环境,在体量百万级以上的的App中几乎不可能实现毫无瑕疵的技术方案和组件,所以一般情况下如果某个技术实现方案可以达到0.01‰以下的崩溃率,而其他方案也没有更好的表现,我们就认为它是可以接受的。但是哪怕仅仅十万分之一的崩溃率,也代表还有用户受到影响,而我们认为Crash对用户来说是最糟糕的体验,尤其是涉及到交易的场景,所以我们必须本着每一单都很重要的原则,尽最大努力保证用户顺利执行流程。


实际情况中有一些技术方案在兼容性和稳定性上做了一定妥协的场景,往往是因为考虑到性能或扩展性等方面的优势。这种情况下我们其实可以再多做一些,进一步提高App的可用性。就像很多操作系统都有“兼容模式”或者“安全模式”,很多自动化机械机器都配套有手动操作模式一样,App里也可以实现备用的降级方案,然后设置特定条件的触发策略,从而达到自动修复Crash的目的。


举例来讲,Android 3.0中引入了硬件加速机制,虽然可以提高绘制帧率并且降低CPU占用率,但是在某些机型上还是会有绘制错乱甚至Crash的情况,这时我们就可以在App中记录硬件加速相关的Crash问题或者使用检测代码主动检测硬件加速功能是否正常工作,然后主动选择是否开启硬件加速,这样既可以让绝大部分用户享受硬件加速带来的优势,也可以保障硬件加速功能不完善的机型不受影响。
还有一些类似的可以做自动降级的场景,比如:



  • 部分使用JNI实现的模块,在SO加载失败或者运行时发生异常则可以降级为Java版实现。

  • RenderScript实现的图片模糊效果,也可以在失败后降级为普通的Java版高斯模糊算法。

  • 在使用Retrofit网络库时发现OkHttp3或者HttpURLConnection网络通道失败率高,可以主动切换到另一种通道。


这类问题都需要根据具体情况具体分析,如果可以找到准确的判定条件和稳定的修复方案,就可以让App稳定性再上一个台阶。


特定Crash类型日志自动回捞


外卖业务发展迅速,即使我们在开发时使用各种工具、措施来避免Crash的发生,但Crash还是不可避免。线上某些怪异的Crash发生后,我们除了分析Crash堆栈信息之外,还可以使用离线日志回捞、下发动态日志等工具来还原Crash发生时的场景,帮助开发同学定位问题,但是这两种方式都有它们各自的问题。离线日志顾名思义,它的内容都是预先记录好的,有时候可能会漏掉一些关键信息,因为在代码中加日志一般只是在业务关键点,在大量的普通方法中不可能都加上日志。动态日志(Holmes)存在的问题是每次下发只能针对已知UUID的一个用户的一台设备,对于大量线上Crash的情况这种操作并不合适,因为我们并不能知道哪个发生Crash的用户还会再次复现这次操作,下发配置充满了不确定性。


我们可以改造Holmes使其支持批量甚至全量下发动态日志,记录的日志等到发生特定类型的Crash时才上报,这样一来可以减少日志服务器压力,同时也可以极大提高定位问题的效率,因为我们可以确定上报日志的设备最后都真正发生了该类型Crash,再来分析日志就可以做到事半功倍。


总结


业务的快速发展,往往不可能给团队充足的时间去治理Crash,而Crash又是App最重要的指标之一。团队需要由一个个Crash个例,去探究每一个Crash发生的最本质原因,找到最合理解决这类Crash的方案,建立解决这一类Crash的长效机制,而不能饮鸩止渴。只有这样,随着版本的不断迭代,我们才能在Crash治理之路上离目标越来越近。


作者:美团技术团队
链接:https://juejin.cn/post/6844903620920492046
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin集成Arouter

使用场景 为了软件间的解耦操作,确保模块之间Activity的相互跳转不受影响。故引用阿里巴巴的Arouter。但官网上还没有针对Kotlin的集成说明,故在此记录下来 如何使用 gradle配置 目录配置,常量类配置 在Application中进行Ar...
继续阅读 »

使用场景



为了软件间的解耦操作,确保模块之间Activity的相互跳转不受影响。故引用阿里巴巴的Arouter。但官网上还没有针对Kotlin的集成说明,故在此记录下来



如何使用



  1. gradle配置

  2. 目录配置,常量类配置

  3. Application中进行Arouter初始化

  4. Activity的配置


1. gradle配置


注意需要在两个地方进行配置


1.根目录下的build.gradle中配置,在dependencies中增加arouter-register引用


 dependencies {
classpath "com.alibaba:arouter-register:1.0.2"
}
复制代码

2.在模块所在的build.gradle中添加引用及编译配置


plugins {
// 1.增加kotlin-kapt引用
id 'kotlin-kapt'
}



android {

// 2.增加Arouter编译配置,注意顺序。此处应该在android{}中
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}

dependencies {

// 3. 添加gradle引用
implementation 'com.alibaba:arouter-api:1.5.1'
kapt "com.alibaba:arouter-compiler:1.5.1"

}
复制代码

注意:此处的与官网教程不一样。官网的配置是针对java的,所以我没有使用
javaCompileOptionsannotationProcessor'com.alibaba:arouter-compiler:1.5.1'这两个配置对kotlin不生效。


2. 目录配置,常量类配置


新建一个ui包用于存放需要跳转的Activity,随后新建一个ConstantObject文件。添加Activity的常量资源


常量类Constants


object Constants {
object Activitys{
const val RECYCLELIST_ACTIVITY = "/ui/RecycleListActivity"
}
}
复制代码

目录结构


目录结构


3. 在Application中进行Arouter初始化


class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG){
ARouter.openLog()
ARouter.openDebug()
}
ARouter.init(this)

}

override fun onTerminate() {
super.onTerminate()
ARouter.getInstance().destroy()
}
}
复制代码

**注意:**此处有两个小坑。



  1. 重写的APP类需要在Manifest中进行添加,否则不会执行。(只需要在application节点中添加name并指向这个类即可)

  2. 注意BuildConfig这个类是引用谁的,因为Arouter本身也有BuildConfig此处需要引用AnroidBuildConfig。博主引用错了后,一直无法跳转。而且也一直没有报错,坑了很久


4. Activity的配置


以上工作做完后,就可以在需要跳转的Activity进行配置了。


跳转到的Activity,增加@Route注解


@Route(path = Constants.Activitys.RECYCLELIST_ACTIVITY)
class RecycleListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycle_list)
}
}
复制代码

需要进行跳转的Activity,调用Arouter单例进行跳转


 mBtnList.setOnClickListener {
ARouter.getInstance().build(Constants.Activitys.RECYCLELIST_ACTIVITY).navigation()
}

作者:约束№证
链接:https://juejin.cn/post/6955465825809760286
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Object类和Any详解

Any Any类是kotlin类结构的跟,每个kotlin都继承或间接继承于Any类 /** * The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superc...
继续阅读 »

Any


Any类是kotlin类结构的跟,每个kotlin都继承或间接继承于Any类


/**
* The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
*/
public open class Any {
// kotlin的函数可以没有函数体,其不是Abstract方法,所以子类不必重写。
public open operator fun equals(other: Any?): Boolean
public open fun hashCode(): Int
public open fun toString(): String
}
复制代码

里面有三个open的方法equals、hashCode和toString,其中equals和hashCode如果需要修改就必须同时修改。


Object


同样java中Object也是class结构的根,每个类继承或者间接继承于Object


package java.lang;

public class Object {
public Object() {
}

private static native void registerNatives();

public final native Class<?> getClass();

public native int hashCode();

public boolean equals(Object var1) {
return this == var1;
}

protected native Object clone() throws CloneNotSupportedException;

public String toString() {
return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
}

public final native void notify();

public final native void notifyAll();

public final native void wait(long var1) throws InterruptedException;

public final void wait(long var1, int var3) throws InterruptedException {
if (var1 < 0L) {
throw new IllegalArgumentException("timeout value is negative");
} else if (var3 >= 0 && var3 <= 999999) {
if (var3 > 0) {
++var1;
}

this.wait(var1);
} else {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
}

public final void wait() throws InterruptedException {
this.wait(0L);
}

protected void finalize() throws Throwable {
}

static {
registerNatives();
}
}
复制代码

相比于Kotlin,java中的class方法丰富的多,十二个。其中7个本地方法包含一个静态本地方法,5个可以被子类覆盖的方法


private static native void registerNatives();

static {
registerNatives();
}
复制代码

静态本地方法在类加载时执行。该方法的作用是通过类加载器加载一些本地方法到JVM中。Object类在被加载时,会加载一些methods中的本地方法到JVM中如下:


static JNINativeMethod methods[] = {
{“hashCode”, “()I”, (void *)&JVM_IHashCode},
{“wait”, “(J)V”, (void *)&JVM_MonitorWait},
{“notify”, “()V”, (void *)&JVM_MonitorNotify},
{“notifyAll”, “()V”, (void *)&JVM_MonitorNotifyAll},
{“clone”, “()Ljava/lang/Object;”, (void *)&JVM_Clone},
};
复制代码

@Contract(pure = true) public final native Class<?> getClass();

返回该对象的类的Class对象。Class对象可以用于反射等场景。


public native int hashCode();

返回对象的哈希值,主要用于HashMap的hash tables。


哈希需要注意的几点:




  • 相等的对象必须要有相同的哈希码




  • 不相等的对象一定有着不同的哈希码——错!




  • 有同一个哈希值的对象一定相等——错!




  • 重写equals时必须重写hashCode




equals

 public boolean equals(Object var1) {
return this == var1;
}
复制代码

判断引用是否指向同一个地址,就是判断两个引用指向的对象是否是同一个对象, String重写了该方法,判断字符串是否相等。


protected native Object clone() throws CloneNotSupportedException;

该方法用于拷贝。要调用该方法需要类实现Cloneable接口,否则抛出CloneNotSupportedException。


浅拷贝,重写clone方法,调用super.clone():


public class TestOne implements Cloneable {
@NonNull
@Override
protected TestOne clone() {
TestOne obj = null;
try {
obj = (TestOne) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
复制代码

深拷贝


public class TestTwo implements Cloneable {
public TestOne var;
@NonNull
@Override
protected TestTwo clone() {
TestTwo obj = null;
try {
obj = (TestTwo) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
obj.var = obj.var.clone();
return obj;
}
}
复制代码

public String toString()

返回类名和对象的哈希值的十六进制字符串,推荐子类重写该方法。


notify()、notifyAll()、wait()方法

从1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。


public synchronize void method(){
method body
}
等同于
public void method(){
this.intrinsicLock.lock();
try{
method body
}
finally{ this.intrinsicLock.unlock();}
}
复制代码

从这个示例我们即可看出上面几个方法的作用。


public final native void notify();

随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法或同步块中调用。如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常。


public final native void notifyAll();

解除那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常。


public final void wait() throws InterruptedException

使线程进入等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常。


public final void wait(long millis, int nanos) throws InterruptedException

public final native void wait(long millis) throws InterruptedException;

参数:millis 毫秒数 nanos 纳秒数 < 1000 000

使线程进入等待状态直到它被通知或者经过指定的时间。这些方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者该方法抛出一个IllegalMonitorStateException异常。


protected void finalize() throws Throwable

当一个堆空间中的对象没有被栈空间变量指向的时候,这个对象会等待被java回收。


GC特点:



  • 当对象不再被程序所使用的时候,垃圾回收器将会将其回收

  • 垃圾回收是在后台运行的,我们无法命令垃圾回收器马上回收资源,但是我们可以告诉他可以尽快回收资源(System.gc()和Runtime.getRuntime().gc())

  • 垃圾回收器在回收某个对象的时候,首先会调用该对象的finalize()方法

  • GC主要针对堆内存

  • 单例模式的缺点


Any和Object相同点


Kotlin中的Any只存在于编译期,运行期就不存在了。


val any = Any()
println("any:$any ")
println("anyClass:${any.javaClass} ")

val obj = any as Object
synchronized(obj){
obj.wait()
}
println("obj:$obj ")
println("obj:${any.`class`} ")

I/System.out: any:java.lang.Object@d12ebc1
I/System.out: anyClass:class java.lang.Object
I/System.out: obj:java.lang.Object@d12ebc1
I/System.out: obj:class java.lang.Object
复制代码

从上面的示例可以看出在runtime,Any变成了Object,在kotlin中也可以将Any强转为Object。


从Kolitn的官方文档 kotlinlang.org/docs/java-i… 可以看到Object对应的就是Any


Kotlin专门处理一些Java类型。这些类型不是按原样从Java加载的,而是映射到相应的Kotlin类型。映射只在编译时起作用,运行时表示保持不变。Java的原语类型映射到相应的Kotlin类型(保持平台类型)


从上面的示例可以看出obj可以any混用,any强转后不仅可以使用notify()等方法,还可以使用Any的扩展方法,如使用 obj.apply { }


作者:zbt
链接:https://juejin.cn/post/6955413784143855629
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(五),写一篇超全面的annotation讲解(2)

自定义编译期注解(CLASS) 为什么要最后说编译期注解呢,因为相对前面的自定义注解来说,编译期注解有些难度,涉及到的东西比较多,但却是平时用到的最多的注解,因为编译期注解不存在反射,所以对性能没有影响。 本来也想用绑定 View 的例子讲解,但是现在这样的 ...
继续阅读 »

自定义编译期注解(CLASS)


为什么要最后说编译期注解呢,因为相对前面的自定义注解来说,编译期注解有些难度,涉及到的东西比较多,但却是平时用到的最多的注解,因为编译期注解不存在反射,所以对性能没有影响。


本来也想用绑定 View 的例子讲解,但是现在这样的 demo 网上各种泛滥,而且还有各路大牛写的,所以我就没必要班门弄斧了。在这里以跳转界面为例:


    Intent intent = new Intent (this, NextActivity.class);
startActivity (intent);
复制代码

本着方便就是改进的原则,让我们定义一个编译期注解,来自动生成上述的代码,想想每次需要的时候只需要一个注解就能跳转到想要跳转的界面是不是很刺激。


1.首先新建一个 android 项目,在创建两个 java module(File -> New -> new Module ->java Module),因为有的类在android项目中不支持,建完后项目结构如下:


这里写图片描述


其中 annotation 中盛放自定义的注解,annotationprocessor 中创建注解处理器并做相关处理,最后的 app 则为我们的项目。


注意:MyFirstProcessor类为上文讲解 AbstractProcessor 所建的类,可以删去,跟本项目没有关系。


2.处理各自的依赖


annotation


processor


app


3.编写自定义注解,这是一个应用到字段之上的注解,被注解的字段为传递的参数


/**
* 这是一个自定义的跳转传值所用到的注解。
* value 表示要跳转到哪个界面activity的元素,传入那个界面的名字。
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface IntentField {
String value () default " ";
}
复制代码

4.自定义注解处理器,获取被注解元素的类型,进行相应的操作。


@AutoService(javax.annotation.processing.Processor.class)
public class MyProcessot extends AbstractProcessor{

private Map<Element, List<VariableElement>> items = new HashMap<>();
private List<Generator> generators = new LinkedList<>();

// 做一些初始化工作
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
Utils.init();
generators.add(new ActivityEnterGenerator());
generators.add(new ActivityInitFieldGenerator());
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

// 获取所有注册IntentField注解的元素
for (Element elem : roundEnvironment.getElementsAnnotatedWith(IntentField.class)) {
// 主要获取ElementType 是不是null,即class,interface,enum或者注解类型
if (elem.getEnclosingElement() == null) {
// 直接结束处理器
return true;
}

// 如果items的key不存在,则添加一个key
if (items.get(elem.getEnclosingElement()) == null) {
items.put(elem.getEnclosingElement(), new LinkedList<VariableElement>());
}

// 我们这里的IntentField是应用在一般成员变量上的注解
if (elem.getKind() == ElementKind.FIELD) {
items.get(elem.getEnclosingElement()).add((VariableElement)elem);
}
}

List<VariableElement> variableElements;
for (Map.Entry<Element, List<VariableElement>> entry : items.entrySet()) {
variableElements = entry.getValue();
if (variableElements == null || variableElements.isEmpty()) {
return true;
}
// 去通过自动javapoet生成代码
for (Generator generator : generators) {
generator.genetate(entry.getKey(), variableElements, processingEnv);
generator.genetate(entry.getKey(), variableElements, processingEnv);
}
}
return false;
}

// 指定当前注解器使用的Java版本
@Override public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

// 指出注解处理器 处理哪种注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>(2);
annotations.add(IntentField.class.getCanonicalName());
return annotations;
}
}
复制代码

5.这是一个工具类方法,提供了本 demo 中所用到的一些方法,其实实际里面的方法都很常见,只不过做了一个封装而已.


public class Utils {

private static Set<String> supportTypes = new HashSet<>();

/** 当getIntent的时候,每种类型写的方式都不一样,所以把每种方式都添加到了Set容器中。*/
static void init() {
supportTypes.add(int.class.getSimpleName());
supportTypes.add(int[].class.getSimpleName());
supportTypes.add(short.class.getSimpleName());
supportTypes.add(short[].class.getSimpleName());
supportTypes.add(String.class.getSimpleName());
supportTypes.add(String[].class.getSimpleName());
supportTypes.add(boolean.class.getSimpleName());
supportTypes.add(boolean[].class.getSimpleName());
supportTypes.add(long.class.getSimpleName());
supportTypes.add(long[].class.getSimpleName());
supportTypes.add(char.class.getSimpleName());
supportTypes.add(char[].class.getSimpleName());
supportTypes.add(byte.class.getSimpleName());
supportTypes.add(byte[].class.getSimpleName());
supportTypes.add("Bundle");
}

/** 获取元素所在的包名。*/
public static String getPackageName(Element element) {
String clazzSimpleName = element.getSimpleName().toString();
String clazzName = element.toString();
return clazzName.substring(0, clazzName.length() - clazzSimpleName.length() - 1);
}


/** 判断是否是String类型或者数组或者bundle,因为这三种类型getIntent()不需要默认值。*/
public static boolean isElementNoDefaultValue(String typeName) {
return (String.class.getName().equals(typeName) || typeName.contains("[]") || typeName.contains("Bundle"));
}

/**
* 获得注解要传递参数的类型。
* @param typeName 注解获取到的参数类型
*/
public static String getIntentTypeName(String typeName) {
for (String name : supportTypes) {
if (name.equals(getSimpleName(typeName))) {
return name.replaceFirst(String.valueOf(name.charAt(0)), String.valueOf(name.charAt(0)).toUpperCase())
.replace("[]", "Array");
}
}
return "";
}

/**
* 获取类的的名字的字符串。
* @param typeName 可以是包名字符串,也可以是类名字符串
*/
static String getSimpleName(String typeName) {
if (typeName.contains(".")) {
return typeName.substring(typeName.lastIndexOf(".") + 1, typeName.length());
}else {
return typeName;
}
}


/** 自动生成代码。*/
public static void writeToFile(String className, String packageName, MethodSpec methodSpec, ProcessingEnvironment processingEnv, ArrayList<FieldSpec> listField) {
TypeSpec genedClass;
if(listField == null) {
genedClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(methodSpec).build();
}else{
genedClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(methodSpec)
.addFields(listField).build();
}
JavaFile javaFile = JavaFile.builder(packageName, genedClass)
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}

}

复制代码

6.自定义一个接口,把需要自动生成的每个java文件的方法都独立出去。


public interface Generator {
void genetate(Element typeElement
, List<VariableElement> variableElements
, ProcessingEnvironment processingEnv);

}
复制代码

7.编写自动生成文件的格式,生成后的类格式如下:


跳转类格式


上图为本例中的MainActivity$Enter类,如果你想生成一个类,那么这个类的格式和作用肯定已经在你的脑海中有了定型,如果你自己都不知道想要生成啥,那还玩啥。


/**
* 这是一个要自动生成跳转功能的.java文件类
* 主要思路:1.使用javapoet生成一个空方法
* 2.为方法加上实参
* 3.方法的里面的代码拼接
* 主要需要:获取字段的类型和名字,获取将要跳转的类的名字
*/
public class ActivityEnterGenerator implements Generator{

private static final String SUFFIX = "$Enter";

private static final String METHOD_NAME = "intentTo";

@Override
public void genetate(Element typeElement, List<VariableElement> variableElements, ProcessingEnvironment processingEnv) {
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.returns(void.class);
// 设置生成的METHOD_NAME方法第一个参数
methodBuilder.addParameter(Object.class, "context");
methodBuilder.addStatement("android.content.Intent intent = new android.content.Intent()");

// 获取将要跳转的类的名字
String name = "";

// VariableElement 主要代表一般字段元素,是Element的一种
for (VariableElement element : variableElements) {
// Element 只是一种语言元素,本身并不包含信息,所以我们这里获取TypeMirror
TypeMirror typeMirror = element.asType();
// 获取注解在身上的字段的类型
TypeName type = TypeName.get(typeMirror);
// 获取注解在身上字段的名字
String fileName = element.getSimpleName().toString();
// 设置生成的METHOD_NAME方法第二个参数
methodBuilder.addParameter(type, fileName);
methodBuilder.addStatement("intent.putExtra(\"" + fileName + "\"," +fileName + ")");
// 获取注解上的元素
IntentField toClassName = element.getAnnotation(IntentField.class);
String name1 = toClassName.value();
if(null != name && "".equals(name)){
name = name1;
}
// 理论上每个界面上的注解value一样,都是要跳转到的那个类名字,否则提示错误
else if(name1 != null && !name1.equals(name)){
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "同一个界面不能跳转到多个活动,即value必须一致");
}
}
methodBuilder.addStatement("intent.setClass((android.content.Context)context, " + name +".class)");
methodBuilder.addStatement("((android.content.Context)context).startActivity(intent)");

/**
* 自动生成.java文件
* 第一个参数:要生成的类的名字
* 第二个参数:生成类所在的包的名字
* 第三个参数:javapoet 中提供的与自动生成代码的相关的类
* 第四个参数:能够为注解器提供Elements,Types和Filer
*/
Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX, Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv,null);
}

}
复制代码

当我们定义了跳转的类,那么接下来肯定就是在另一个界面获取传递过来的数据了,参考格式如下,这是本demo中自动生成的MainActivity$Init 类。


获取参数格式


/**
* 要生成一个.Java文件,在这个Java文件里生成一个获取上个界面传递过来数据的方法
* 主要思路:1.使用Javapoet生成一个空的的方法
* 2.为方法添加需要的形参
* 3.拼接方法内部的代码
* 主要需要:获取传递过来字段的类型
*/
public class ActivityInitFieldGenerator implements Generator {

private static final String SUFFIX = "$Init";

private static final String METHOD_NAME = "initFields";

@Override
public void genetate(Element typeElement, List<VariableElement> variableElements, ProcessingEnvironment processingEnv) {

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PROTECTED)
.returns(Object.class);

final ArrayList<FieldSpec> listField = new ArrayList<>();

if (null != variableElements && variableElements.size() != 0) {
VariableElement element = variableElements.get(0);
// 当前接收数据的字段的名字
IntentField currentClassName = element.getAnnotation(IntentField.class);
String name = currentClassName.value();

methodBuilder.addParameter(Object.class, "currentActivity");
methodBuilder.addStatement(name + " activity = (" + name + ")currentActivity");
methodBuilder.addStatement("android.content.Intent intent = activity.getIntent()");
}

for (VariableElement element : variableElements) {

// 获取接收字段的类型
TypeName currentTypeName = TypeName.get(element.asType());
String currentTypeNameStr = currentTypeName.toString();
String intentTypeName = Utils.getIntentTypeName(currentTypeNameStr);

// 字段的名字,即key值
Name filedName = element.getSimpleName();

// 创建成员变量
FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(element.asType()),filedName+"")
.addModifiers(Modifier.PUBLIC)
.build();
listField.add(fieldSpec);

// 因为String类型的获取 和 其他基本类型的获取在是否需要默认值问题上不一样,所以需要判断是哪种
if (Utils.isElementNoDefaultValue(currentTypeNameStr)) {
methodBuilder.addStatement("this."+filedName+"= intent.get" + intentTypeName + "Extra(\"" + filedName + "\")");
} else {
String defaultValue = "default" + element.getSimpleName();
if (intentTypeName == null) {
// 当字段类型为null时,需要打印错误信息
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "the type:" + element.asType().toString() + " is not support");
} else {
if ("".equals(intentTypeName)) {
methodBuilder.addStatement("this." + filedName + "= (" + TypeName.get(element.asType()) + ")intent.getSerializableExtra(\"" + filedName + "\")");
} else {
methodBuilder.addParameter(TypeName.get(element.asType()), defaultValue);
methodBuilder.addStatement("this."+ filedName +"= intent.get"
+ intentTypeName + "Extra(\"" + filedName + "\", " + defaultValue + ")");
}
}
}
}
methodBuilder.addStatement("return this");
Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX, Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv, listField);
}
}
复制代码

8、在Activity中使用刚才的自定义注解。


public class MainActivity extends AppCompatActivity {

@IntentField("NextActivity")
int count = 10;
@IntentField("NextActivity")
String str = "编译器注解";
@IntentField("NextActivity")
StuBean bean = new StuBean(1,"No1");

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
addOnclickListener();
}

public void addOnclickListener() {
findViewById(R.id.tvnext).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 从哪个界面进行跳转,则以哪个界面打头,enter 结尾
// 例如 MainActivity$Enter
new MainActivity$Enter()
.intentTo(MainActivity.this, count, str, bean);
}
});
}
}
复制代码

9.这是实体bean


public class StuBean implements Serializable{
public StuBean(int id , String name) {
this.id = id;
this.name = name;
}
//学号
public int id;
//姓名
public String name;
}
复制代码

10、在NextActivity接收并打印数据:


public class NextActivity extends AppCompatActivity {

private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);
textView = (TextView) findViewById(R.id.tv);

// 想获取从哪个界面传递过来的数据,就已哪个类打头,init结尾
// 例如 MainActivity$Init
MainActivity$Init formIntent = (MainActivity$Init)new MainActivity$Init().initFields(this,0);
textView.setText(formIntent.count + "---" + formIntent.str + "---" +formIntent.bean.name);

// 打印上个界面传递过来的数据
Log.i("Tag",formIntent.count + "---" + formIntent.str + "---" + formIntent.bean.name);
}
}
复制代码

11.运行结果:


这里写图片描述


总结


好了,看到这里,你应该对注解有所了解了,但是看的再懂也不如自己动手练一下。如果你仔细研究了,你会发现一个非常奇怪的事情,当我们设置 RetentionPolicy.CLASS 级别的时候,仍能通过反射获取注解信息,当我们设置 RetentionPolicy.SOURCE 级别的时候,仍能走通编译期注解,是不是非常迷惑。


之后只能又找了一些资料(非权威),看到了一个比较受认同的解释:这个属性主要给IDE 或者编译器开发者准备的,一般应用级别上不太会用到。



好了,本文到这里就结束了,关于注解的讲解应该非常全面了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考 1、B.E,Java编程思想:机械工业出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6936609673416015908
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(五),写一篇超全面的annotation讲解(1)

不学注解,也许是因为平时根本不需要没事自定义个这玩意玩,可随着Android形势越来越内卷,不学点东西是真不行了。而通过本文的学习,可以让你对于注解有个全面的认识,你会发现,小小的注解,大有可为,编不下去了.. 注解不同于注释,注释的作用是为了方便自己或者别...
继续阅读 »

不学注解,也许是因为平时根本不需要没事自定义个这玩意玩,可随着Android形势越来越内卷,不学点东西是真不行了。而通过本文的学习,可以让你对于注解有个全面的认识,你会发现,小小的注解,大有可为,编不下去了..



注解不同于注释,注释的作用是为了方便自己或者别人的阅读,能够利用 javadoc 提取源文件里的注释来生成人们所期望的文档,对于代码本身的运行是没有任何影响的。


而注解的功能就要强大很多,不但能够生成描述符文件,而且有助于减轻编写“样板”代码的负担,使代码干净易读。通过使用扩展的注解(annotation)API 我们能够在 编译期运行期 对代码进行操控。



注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后的某个时刻非常方便的使用这些数据。 —Jeremy Meyer



本文主要对于下面几个方面进行讲解,篇幅很长,建议收藏查看:



Java 最初内置的三种标准注解


注解是 java SE5中的重要的语言变化之一,你可能对注解的原理不太理解,但你每天的开发中可能无时无刻不在跟注解打交道,最常见的就是 @Override 注解,所以注解并没有那么神秘,也没有那么冷僻,不要害怕使用注解(虽然使用的注解大部分情况都是根据需要自定义的注解),用的多了自然就熟了。为什么说最初的三种标准注解呢,因为在后续的 java 版本中又陆陆续续的增加了一些注解,不过原理都是一样的。























java SE5内置的标准注解含义
@Override表示当前的方法定义将覆盖超类中的方法,如果方法拼写错误或者方法签名不匹配,编译器便会提出错误提示
@Deprecated表示当前方法已经被弃用,如果开发者使用了注解为它的元素,编译器便会发出警告信息
@SuppressWarnings可以关闭不当的编译器警告信息

Java 提供的四种元注解和一般注解


所谓元注解(meta-annotation)也是一种注解,只不过这种注解负责注解其他的注解。所以再说元注解之前我们来看一下普通的注解:



public @interface LogClassMessage {}



这是一个最普通的注解,注解的定义看起来很像一个接口,在 interface 前加上 @ 符号。事实上在语言级别上,注解也和 java 中的接口、类、枚举是同一个级别的,都会被编译成 class 文件。而前面提到的元注解存在的目的就是为了修饰这些普通注解,但是要明确一点,元注解只是给普通注解提供了作用,并不是必须存在的。



























java 提供的元注解作用
@Target定义你的注解应用到什么地方(详见下文解释)
@Retention定义该注解在哪个级别可用(详见下文解释)
@Documented将此注解包含在 javadoc 中
@Inherited允许子类继承超类中的注解

〔1〕@Target使用的时候添加一个 ElementType 参数,表示当前注解可以应用到什么地方,即可以指定一种,也可以同时指定多种,使用方法如下:


    // 表示当前的注解只能应用到类、接口(包括注解)、enum上面
@Target(ElementType.TYPE)
public @interface LogClassMessage {}
复制代码

    // 表示当前的注解只能应用到方法和成员变量上面
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {}
复制代码

下面来看一下 ElementType 的全部参数含义:







































ElementType 参数说明
ElementType.CONSTRUCTOR构造器的声明
ElementType.FIELD域的声明(包括enum的实例)
ElementType.LOCATION_VARLABLE局部变量的声明
ElementType.METHOD方法的声明
ElementType.PACKAGE包的声明
ElementType.PARAMETER参数的声明
ElementType.TYPE类、接口(包括注解类型)、enum声明

〔2〕@Retention用来注解在哪一个级别可用,需要添加一个 RetentionPolicy 参数,用来表示在源代码中(SOURCE),在类文件中(CLASS)或者运行时(RUNTIME):


    // 表示当前注解运行时可用
@Retention(RetentionPolicy.RUNTIME)
public @interface LogClassMessage {}
复制代码

下面来看一下 RetentionPolicy 的全部参数含义:























RetentionPolicy 参数说明
RetentionPolicy.SOURCE注解将被编译器丢弃,只能存于源代码中
RetentionPolicy.CLASS注解在class文件中可用,能够存于编译之后的字节码之中,但会被VM丢弃
RetentionPolicy.RUNTIMEVM在运行期也会保留注解,因此运行期注解可以通过反射获取注解的相关信息

在注解中,一般都会包含一些元素表示某些值,并且可以为这些元素设置默认值,没有元素的注解也称为标记注解(marker annotation)


    @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {
public int id () default -1;
public String message() default "";
}
复制代码

注:虽然上面的 id 和 message 定义和接口的方法定义很类似,但是在注解中将 id 和 message 称为:int 元素 id , String 元素 message。而且注解元素的类型是有限制的,并不是任何类型都可以,主要包括:基本数据类型(理论上是没有基本类型的包装类型的,但是由于自动封装箱,所以也不会报错)、String 类型、enum 类型、Class 类型、Annotation 类型、以及以上类型的数组,(没有等字,说明目前注解的元素类型只支持上面列出的这几种),否则编译器便会提示错误。



invalid type 'void ' for annotation member // 例如注解类型为void的错误信息



对于默认值限制 ,Bruce Eckel 在其书中是这样描述的:编译器对元素的默认值有些过分挑剔,首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供注解的值。其次,对于非基本类型的元素,无论在源代码声明中,或者在注解接口中定义默认值时,都不能以 null 作为其值。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有元素都存在,并且都具有相应的值。为了绕开这个约束,我们只能自己定义一些特殊的值,例如空字符串或者负数,以此表示某个元素的不存在,这算得上是一个习惯用法。


参考系统的标准注解


怎么说呢,接触一种知识的途径有很多,可能每一种的结果都是大同小异的,都能让你学到东西,但是实现的方式、实现过程中的规范、方法和思路却并不一定是最佳的。


上文讲到的是注解的基本语法,那么系统是怎么用的呢?首先让我们来看一下使用频率最高的 @Override :


    @Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}
复制代码

〔1〕首先系统定义一个没有元素的标记注解 Override ,随后使用元注解 @Target 指明 Override 注解只能应用于方法之上(你可以细想想,是不是在我们实际使用这个注解的时候,只能是重写方法,没有见过重写类或者字段的吧),使用注解 @Retention 表示当前注解只能存在源代码中,并不会出现在编译之后的 class 文件之中。


    @Override
protected void onResume() {
super.onResume();
}
复制代码

〔2〕如在 Activity 中我们可以重写 onResume() 方法,添加注解 @override 之后编译器便会去检查父类中是否存在相同方法,如果不存在便会报错。


〔3〕也许到这里你会感到很疑惑,注解到底是怎么工作的,怎么系统这样定义一个注解 Override 它就能工作了?黑魔法吗,擦擦,完成看不到实现过程嘛(泪流满面),经过查阅了一些资料(非权威)了解到,其实处理过程都编写在了编译器里面,也就是说编译器已经给我们写好了处理方法,当编译器进行检查的时候就会调用相应的处理方法。


注解处理器


介绍之前,先引用 Jeremy Meyer 的一段话:如果没有用来读取注解的工具,那么注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5 扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具 apt帮助程序员解析带有注解的 java 源代码。


根据上面描述我们可以知道,注解处理器并不是一个特定格式,并不是只有继承了 AbstractProcessor 这个抽象类才叫注解处理器,凡是根据相关API 来读取注解的类或者方法都可以称为注解处理器。


反射机制下的处理器


最简单的注解处理器莫过于,直接使用反射机制的 getDeclaredMethods 方法获取类上所有方法(字段原理是一样的),再通过调用 getAnnotation 获取每个方法上的特定注解,有了注解便可以获取注解之上的元素值,方法如下:


    public void getAnnoUtil(Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
LogClassMessage logClassMessage = m.getAnnotation(LogClassMessage .class);
if(null != logClassMessage) {
int id = logClassMessage.id();
String method = logClassMessage.message();
}
}
}
复制代码

由于反射对性能会有一定的损耗,所以上述类型的注解处理器并不占主流,现在使用最多的还是 AbstractProcessor 自定义注解处理器,因为后者并不需要通过反射实现,效率和直接调用普通方法没有区别,这也是为什么编译期注解比运行时注解更受欢迎。


但是并不是说为了性能运行期注解就不能用了,只能说不能滥用,要在性能方面给予考虑。目前主要的用到运行期注解的框架差不多都有缓存机制,只有在第一次使用时通过反射机制,当再次使用时直接从缓存中取出。


好了,说着说着就跑题,还是来聊一下这个 AbstractProcessor 类吧,到底有何魅力让这么多人为她沉迷,方法如下:



public class MyFirstProcessor extends AbstractProcessor {

/**
* 做一些初始化工作,注释处理工具框架调用了这个方法,给我们传递一个 ProcessingEnvironment 类型的实参。
*
* <p>如果在同一个对象多次调用此方法,则抛出IllegalStateException异常。
*
* @param processingEnvironment 这个参数里面包含了很多工具方法
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {

// 返回用来在元素上进行操作的某些工具方法的实现
Elements es = processingEnvironment.getElementUtils();
// 返回用来创建新源、类或辅助文件的Filer
Filer filer = processingEnvironment.getFiler();
// 返回用来在类型上进行操作的某些实用工具方法的实现
Types types = processingEnvironment.getTypeUtils();

// 这是提供给开发者日志工具,我们可以用来报告错误和警告以及提示信息
// 注意 message 使用后并不会结束过程,Kind 参数表示日志级别
Messager messager = processingEnvironment.getMessager();
messager.printMessage(Diagnostic.Kind.ERROR, "例如当默认值为空则提示一个错误");
// 返回任何生成的源和类文件应该符合的源版本
SourceVersion version = processingEnvironment.getSourceVersion();

super.init(processingEnvironment);
}

/**
* @return 如果返回true 不要求后续Processor处理它们,反之,则继续执行处理。
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

/**
* TypeElement 这表示一个类或者接口元素集合常用方法不多,TypeMirror getSuperclass()返回直接超类。
*
* <p>详细介绍下 RoundEnvironment 这个类,常用方法:
* boolean errorRaised() 如果在以前的处理round中发生错误,则返回true
* Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)
* 这里的 a 即你自定义的注解class类,返回使用给定注解类型注解的元素的集合
* Set<? extends Element> getElementsAnnotatedWith(TypeElement a)
*
* <p>Element 的用法:
* TypeMirror asType() 返回此元素定义的类型 如int
* ElementKind getKind() 返回元素的类型 如 e.getkind() = ElementKind.FIELD 字段
* boolean equals(Object obj) 如果参数表示与此元素相同的元素,则返回true
* Name getSimpleName() 返回此元素的简单名称
* List<? extends Elements> getEncloseElements 返回元素直接封装的元素
* Element getEnclosingElements 返回此元素的最里层元素,如果这个元素是个字段等,则返回为类
*/

return false;
}

/**
* 指出注解处理器 处理哪种注解
* 在 jdk1.7 中,我们可以使用注解 {@SupportedAnnotationTypes()} 代替
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}

/**
* 指定当前注解器使用的Jdk版本。
* 在 jdk1.7 中,我们可以使用注解{@SupportedSourceVersion()}代替
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}

复制代码

自定义运行期注解(RUNTIME)


我们在开发中经常会需要计算一个方法所要执行的时间,以此来直观的比较哪个实现方式最优,常用方法是开始结束时间相减



System.currentTimeMillis()



但是当方法多的时候,是不是减来减去都要减的怀疑人生啦,哈哈,那么下面我就来写一个运行时注解来打印方法执行的时间。


1.首先我们先定义一个注解,并给注解添加我们需要的元注解:


/**
* 这是一个自定义的计算方法执行时间的注解。
* 只能作用于方法之上,属于运行时注解,能被VM处理,可以通过反射得到注解信息。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CalculateMethodRunningTime {

// 要计算时间的方法的名字
String methodName() default "no method to set";
}
复制代码

2.利用反射方法在程序运行时,获取被添加注解的类的信息:


public class AnnotationUtils {

// 使用反射通过类名获取类的相关信息。
public static void getClassInfo(String className) {
try {
Class c = Class.forName(className);
// 获取所有公共的方法
Method[] methods = c.getMethods();
for (Method m : methods) {
Class<CalculateMethodRunningTime> ctClass = CalculateMethodRunningTime.class;
if (m.isAnnotationPresent(ctClass)) {
CalculateMethodRunningTime anno = m.getAnnotation(ctClass);
// 当前方法包含查询时间的注解时
if (anno != null) {
final long beginTime = System.currentTimeMillis();
m.invoke(c.newInstance(), null);
final long time = System.currentTimeMillis() - beginTime;
Log.i("Tag", anno.methodName() + "方法执行所需要时间:" + time + "ms");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码

3.在 activity 中使用注解,注意咱们的注解是作用于方法之上的:


public class ActivityAnnotattion extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anno);
AnnotationUtils.getClassInfo("com.annotation.zmj.annotationtest.ActivityAnnotattion");
}

@CalculateMethodRunningTime(methodName = "method1")
public void method1() {
long i = 100000000L;
while (i > 0) { i--; }
}

}
复制代码

4.运行结果:


这里写图片描述



作者:矛盾的阿呆i
链接:https://juejin.cn/post/6936609673416015908
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(四),谈起泛型,大家都很佛性

当我们new了一个对象,会发生什么呢?来段代码: public class Tested { public static int T = 10; public int c = 1; } 复制代码 类初始化 在编译期,编译器会将 Tested.j...
继续阅读 »

当我们new了一个对象,会发生什么呢?来段代码:


public class Tested {
public static int T = 10;
public int c = 1;
}
复制代码

类初始化


在编译期,编译器会将 Tested.java类转换成 Tested.class 字节码文件。当虚拟机接收到new 字节码指令时,如果此时类还未被初始化,则虚拟机会先进行类的初始化过程。



在类加载完成后。虚拟机会为new Tested() 的Tested对象,在java堆中分配内存。而对象所需要的内存大小在类加载完成后就被确定了。


指针碰撞


如果 java 中的内存是规整的,即使用过的放在一边,空闲的在另一边,中间放着指针作为分界点的指示器。那所分配的内存就仅仅是将指针像空闲空间挪动一段与对象大小相等的距离。这种方式内称为指针碰撞。


空闲列表


如果 java 中的内存是不工整的,使用过的和空闲的内存相互交错,那么虚拟机就必须维护一个列表记录哪些内存是可用的。在分配的时候从列表中找到一块足够大的空间给对象示例,并更新表的记录。这种分配方式称为空闲列表。


对象初始化


当我们的对象内存被分配完毕后,虚拟机就会对对象进行初始化操作。



此时Tested 对象在我们眼里就算出生了,在虚拟机眼里就是真正可用的了。可对象的生命并不是无穷的,它也会经历自己的死亡。


可达性分析


在主流实现中,我们通过可达性分析来判断一个对象是否存活。实现思路是:通过一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始像下搜索,搜索所走的路径被称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。见图:


在这里插入图片描述


即使 Obj5 与 Obj4 由于与 GC Roots 没有引用链相连,所以我们称 GC Roots 到对象 Obj4 和 Obj5 不可达。所以 Obj4 和 Obj5 就是可回收的。


既然Obj4 和 Obj5 是可回收的,那么是否一定会被回收呢?不一定。此时虚拟机会进行第一次的标记过程。因为 java 内能够重写 finalize() 方法(在这里只是分析特例,不推荐在任何情况下使用此方法),当对象重写了此方法,并且 finalize() 方法还未被虚拟机调用,那么虚拟机就会将此对象放入一个专门的F-Queue队列,由一个单独的 Finalizer 线程去执行它,如果队列中对象的 finalize() 方法在虚拟机第二次标记之前执行,并在此次执行过程中又将自己与GC Roots 引用链相连,那么虚拟机在进行第二次标记时,就会将该对象从 F-Queue队列移除,否则就宣告该对象死亡。注意:finalize() 方法只会被执行一次,所以一个对象一生只有一次机会进入F-Queue队列,有机会逃脱本此死亡。


如果对象已经宣告死亡了,那么虚拟机怎么来回收它吗?


标记-清除算法


这是最基础的收集算法,主要分为标记和清除两个阶段。首先标记出所以需要回收的对象,在标记完成后统一回收所有被标记的对象。可以参考上面的空闲列表。其有两点不足:


a. 效率问题,标记和清除两个过程效率都不高。


b. 空间问题,因为堆中的内存不是规整的,已使用的和空闲的内存相互交错,这也就导致了每次GC回收后,产生大量的内存碎片,而当再次分配一个大对象时,如果无法找到足够的连续内存,又会再此触发GC回收。


复制算法


复制算法是将堆内存分成大小相等的两块,每次只使用其中一块,这样内存就是规整的了,参考指针碰撞。每当一块内存使用完了,就将该块内存中存活的对象复制到另一边,随后将该块内存一次清理掉。


现在的虚拟机都采用这种方式来回收新生代,只是并不是按照1:1的比例来划分内存,而是将内存分为一块较大的 Eden 空间,和两块较小的 Survivor 空间(HotSpot虚拟机默认Eden:Survivor = 8 :1)。每次只使用 Eden 和 其中一块 Survivor 空间,当回收时,将 Eden 空间和当前正使用的 Survivor 空间内存活的对象复制到另一块空闲的 Survivor空间,随后清空 Eden 和 刚才用过的 Survivor 内存。


注意:由于我们无法保证每次 存活的对象所占内存一直都不大于 Survivor 内存值,所以就会有溢出风险。所以在 分代收集算法 中,虚拟机会将内存先划分为一块新生代内存和一块为老年代内存。而在新生代内存中,会采用这种8:1:1的内存分配方式,如果溢出了,就将该情况下的存活对象全部放在老年代内存里,说白了就是一种兜底策略。这里要注意的是,不是溢出的那部分,而是全部的存活对象。


标记-整理算法


标记-整理算法中的标记过程,与标记-清除算法中的标记过程一样,不同的是,当标记完成并清理回收完对象后,会将当前不连续的碎片内存就行整理,即存活的对象都移到一端,来保证接下来要分配的内存的规整性。我们的 分代收集算法 中的老年代内存块,就是采用的该算法(当然也可以是标记-清除算法,不同虚拟机的策略不同)。所以就不再对分代收集算法就行赘述了。



好了,本文到这里,关于“对象”的生命周期的讲解就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考
1、周志明,深入理解JAVA虚拟机:机械工业出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6935481800365981727
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

移动应用遗留系统重构(6)- 测试篇

前言 上一篇移动应用遗留系统重构(5)- 重构方法篇我们分享了进行依赖解除的重构流程。主要为4个操作步骤,识别内聚包、解除依赖、移动、验收。同时最后也提出了一个问题,重构时如何保证功能的正确性,不会修改出新问题? 其实这个问题容易但又不简单。容易的是把修改得功...
继续阅读 »

前言


上一篇移动应用遗留系统重构(5)- 重构方法篇我们分享了进行依赖解除的重构流程。主要为4个操作步骤,识别内聚包、解除依赖、移动、验收。同时最后也提出了一个问题,重构时如何保证功能的正确性,不会修改出新问题?


其实这个问题容易但又不简单容易的是把修改得功能仔细测一篇保证所有功能正常就可以了。不简单的是如何全面、高效、可重复的执行这个过程。我们很容易联想到的方案就是自动化测试。但最大的问题是,对大部分遗留系统来说都是没有任何自动化测试。而且大量的坏味道代码,可测试性低,我们也很难补充充分的自动化测试。那么我们有什么折中的策略吗?


测试策略


我们先来看看Google Android开发者官网上对于测试的介绍,将不同的类型的测试分为三类测试(即小型、中型和大型测试)。




图片来源developer.android.com




  • 小型测试是指单元测试,用于验证应用的行为,一次验证一个类。

  • 中型测试是指集成测试,用于验证模块内堆栈级别之间的互动或相关模块之间的互动。

  • 大型测试是指端到端测试,用于验证跨越了应用的多个模块的用户操作流程。


前面提到对于遗留单体系统来说通常没有任何自动化测试,并且通常内部结构耦合严重,所以实施中小型的成本非常高。显然对于遗留系统,测试金字塔模型适用度较低。 所以对于遗留系统,可能比较适合的策略模型如下:



对于遗留单体系统,一个可行的思路是先补充中大型的测试,作为基本的冒烟测试,重构优化内部结构后再及时补充中小型测试。


CloudDisk示例


对于我们这个浓缩版的CloudDisk,界面上也比较简单。主要是有一个主界面,主界面上主要为文件、动态、用户。(后续的MV*重构篇会持续补充页面交互及逻辑)



我们可以设计一组UI的测试验证基本的功能。主要的几个测试点如下:



  1. 主界面能正常运行并显示3个Fragment

  2. 3个Fragment能正常显示

  3. 点击登录按钮,能够跳转到登录页面


测试设计的用例如下:


@RunWith(AndroidJUnit4.class)
@LargeTest
public class SmokeTesting
{

@Test
public void should_show_fragment_list_when_activity_launch() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_user)).perform(click());
//then
List fragments = activity.getSupportFragmentManager().getFragments();
assertThat(fragments.size() == 3);
assertThat(fragments.get(0) instanceof FileFragment);
assertThat(fragments.get(1) instanceof DynamicFragment);
assertThat(fragments.get(2) instanceof UserCenterFragment);
});
}

@Test
public void show_show_file_ui_when_click_tab_file() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_file)).perform(click());
//then
onView(withText("Hello file fragment")).check(matches(isDisplayed()));
});
}

@Test
public void show_show_dynamic_ui_when_click_tab_dynamic() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_dynamic)).perform(click());
//then
onView(withText("Hello dynamic fragment")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
});
}

@Test
public void show_show_user_center_ui_when_click_tab_dynamic() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_user)).perform(click());
//then
onView(withText("Hello user center fragment")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
});
}

@Test
public void show_show_login_ui_when_click_login_button() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
Intents.init();
//when
onView(withId(R.id.fab)).perform(click());
//then
intended(IntentMatchers.hasComponent("com.cloud.disk.platform.login.LoginActivity"));
Intents.release();
});
}
}
复制代码

详细代码见Github提交


我们可以将用例运行在Robolectric上,提高反馈的速度,执行命令如下:


./gradlew testDebug --tests SmokeTesting
复制代码

测试执行结果如下:




当然实际的项目里情况更复杂,数据可能来自网络服务、数据库等等。我们还需要进行Mock。后续的MV*重构篇会持续补充常见坏味道示例代码及更多的自动化测试用例。



更多测试框架及设计可以参考Google官方
在 Android 平台上测试应用


总结


这一篇我们介绍了常用的测试分类及遗留系统的测试策略,对于遗留单体系统,一个可行的思路是先补充中大型的测试,作为基本的冒烟测试,重构优化内部结构后再及时补充中小型测试。同时也给CloudDisk补充了一组基础的大型测试作为冒烟测试,作为后续重构的基本守护测试。


下一篇移动应用遗留系统重构(7)- 解耦重构演示篇(一) 我们将基于方法篇的流程开始对CloudDisk进行重构的改造,具体的解耦操作会以视频的方式展示。


参考资料


developer.android.com


CloudDisk示例代码


CloudDisk


系列链接


移动应用遗留系统重构(1)- 开篇


移动应用遗留系统重构(2)-架构篇


移动应用遗留系统重构(3)-示例篇


移动应用遗留系统重构(4)-分析篇


移动应用遗留系统重构(5)- 重构方法篇


大纲



作者:JunBin
链接:https://juejin.cn/post/6954635678982340622
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(5)- 重构方法篇

前言 上一篇 移动应用遗留系统重构(4)-分析篇  我们根据CloudDisk未来的架构,借助ArchUnit进行架构测试守护以及Intellij的Dependendencies分析出了按照未来的架构设计需要解决的异常依赖。 这一篇开始我们将分享进...
继续阅读 »

前言


上一篇

移动应用遗留系统重构(4)-分析篇  我们根据CloudDisk未来的架构,借助ArchUnit进行架构测试守护以及Intellij的Dependendencies分析出了按照未来的架构设计需要解决的异常依赖。


这一篇开始我们将分享进行依赖解除的重构流程、方法以及常用的工具使用。


重构流程


1.识别一个内聚的包
2.解除该包的异常依赖
3.移动该包对应的代码及资源到新的模块
4.包解耦验收

1.识别内聚的包


对于移动应用通常我们可以通过产品的业务划分进行领域的识别划分。例如CloudDisk这个产品的相对还是比较清晰,业务上主要分为文件、动态及个人中心。


对于部分遗留系统来说,旧代码可能散落在不同的包下,或者原先的代码组织方式是以功能划分,而非业务划分。就像CloudDisk的代码一样,第一步我们得先把相关的业务代码组织到同一包下,这个阶段我们可以先不管是否存在异常依赖,因为只有先组织到一个内聚的包下才方便我们进行依赖分析及代码重构。


2.解除异常依赖


这里我们将介绍几种通用的依赖解除手法。包含下沉、接口提取、路由跳转。



后续的演示篇会通过视频进行具体的操作演示
























依赖解除手法使用场景
下沉原本类功能属于Library或者Platform的,直接下沉。例如LogUtil 或 DateUtil等
接口提取适用于Bundle间有数据或者行为依赖。例如某个BundleA中的classA需要触发BundleB的某个业务行为
路由跳转适用于UI页面间跳转。例如某个BundleA中的Activity1,需要跳转到BundleB的Activity2

重构手法:



  1. 类下沉




  • 具体类移动到适当的 Lib 模块中




  • 在调用模块增加对该 Lib 的依赖





  1. 接口提取



  • 在适当的公用模块中创建空的接口

  • 将调用具体页面类的跳转代码块所在的包中建立新的实现类实现该接口

  • (自动)将调用代码块通过 Extract method 提取成新方法



如已经是独立方法跳过此步




  • (自动)在原调用逻辑所属的类中增加实现类成员变量作为delegate



需要预留 Inject 接口,建议采用 Constructor Inject,静态成员提供setter




  • (自动)将新方法调用转移到delegate



如果是静态方法先通过 Change Method Signature 将 delegate 作为参数传给该方法




  • (自动)将新方法 Pull up 到接口

  • (自动)将实现类移动到壳程序中

  • 在壳程序中实现实现类的Inject



  1. 路由跳转



  • 在跳转类定义对应的映射Path

  • 在调用处使用对应的path进行跳转


3.移动代码及资源


当包的异常依赖全部解耦完后,就可以直接进行移动了。这里我们分享2中常用的代码移动方式。



  1. Move


这种方式大家应该比较常用,选择一个File或者Directory,按下F6选择希望移动后的目录则可。



但是这种方式会存在一个问题,就是被移动的类如果依赖了其他的类或者资源,移动后会出现依赖异常。


适用场景:移动的File或Directory没有其他的依赖



  1. Modularize


Modularize能够分析出移动的File存在的相关依赖,并一起关联移动,很好解决Move的痛点,非常适用于跨Module的移动。



选择移动的Module后点击Preview。



这里注意,有一些划线的文件,那是因为这个文件同时被多处引用,如果跟随一起移动,那么其他的地方会报错。所以我们需要将划线的文件先移动至公用的合适位置。待Preview没有任何的文件划线时,就可以进行移动。


4.包解耦验收



  • 所有模块编译通过

  • 所有新增模块符合模块依赖规则

  • 通过架构守护测试


总结


这一篇我们分享了进行依赖解除的重构流程,主要为4个操作步骤,识别内聚包、解除依赖、移动、验收。同时也介绍了Intellij中非常好用的Modularize功能。接下来我们就可以开始动手进行代码重构,但此时我们又面临着另外一个问题,也是很多同学在做重构时经常担心的一个问题。重构时如何保证功能的正确性,不会修改出新问题。


下一篇移动应用遗留系统重构(6)- 测试篇,我们将分享对于单体移动应用遗留系统,如何制定测试策略及有效补充自动化测试,更好为重构保驾护航。


系列链接


移动应用遗留系统重构(1)- 开篇

移动应用遗留系统重构(2)-架构篇

移动应用遗留系统重构(3)-示例篇

移动应用遗留系统重构(4)-分析篇



大纲



作者:JunBin
链接:https://juejin.cn/post/6952298178095874055
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(4)-分析篇

前言 上一篇移动应用遗留系统重构(3)-示例篇我们介绍了CloudDisk的业务及代码现状。分享了“理想”(未来的架构设计)与“现实”(目前的代码现状),接下来在我们开始动手进行重构时,我们首先得知道往理想的设计架构演化,中间存在多少问题。一方面作为开始重构的...
继续阅读 »

前言


上一篇移动应用遗留系统重构(3)-示例篇我们介绍了CloudDisk的业务及代码现状。分享了“理想”(未来的架构设计)与“现实”(目前的代码现状),接下来在我们开始动手进行重构时,我们首先得知道往理想的设计架构演化,中间存在多少问题。一方面作为开始重构的输入,另外一方面我们有数据指标,也能更好评估工作量及衡量进度。


接下来我们将根据架构篇团队采用的架构设计,结合目前的代码,总结分析工具及方法。


架构设计


我们先回忆一下架构篇里团队采用的架构设计。




  1. 代码复用



  • 公共能力复用,有一层专门统一管理应用公用的基础能力,如图片、网络、存储能力、安全等

  • 公用业务能力复用,有一层专门统一管理应用的业务通用组件,如分享、推送、登录等



  1. 低耦合



  • 业务模块间通过API方式依赖,不依赖具体的模块实现

  • 依赖方向清晰,上层模块依赖下层模块



  1. 并行研发



  • 业务模块支持独立编译调试

  • 业务模块独立发布


结合该4层架构、已有的代码,以及业务的后续演化,团队设计的新架构如下



分析工具


ArchUnit


有了架构设计后,我们就能识别代码的边界,这里我们可以通过Archunit进行边界约束描述。我们可以得到2条通用的守护规则。



  1. 垂直方向,下层模块不能反向依赖上层

  2. 横向方向,组件之间不能存在相互的依赖


转化为ArchUnit的测试用例如下:


  @ArchTest
public static final ArchRule architecture_layer_should_has_right_dependency =layeredArchitecture()
.layer("Library").definedBy("..cloud.disk.library..")
.layer("PlatForm").definedBy("..cloud.disk.platform..")
.layer("FileBundle").definedBy("..cloud.disk.bundle.file..")
.layer("DynamicBundle").definedBy("..cloud.disk.bundle.dynamic..")
.layer("UserBundle").definedBy("..cloud.disk.bundle.user..")
.layer("AllBundle").definedBy("..cloud.disk.bundle..")
.layer("App").definedBy("..cloud.disk.app..")
.whereLayer("App").mayOnlyBeAccessedByLayers()
.whereLayer("FileBundle").mayOnlyBeAccessedByLayers("App")
.whereLayer("DynamicBundle").mayOnlyBeAccessedByLayers("App")
.whereLayer("UserBundle").mayOnlyBeAccessedByLayers("App")
.whereLayer("PlatForm").mayOnlyBeAccessedByLayers("App","AllBundle")
.whereLayer("Library").mayOnlyBeAccessedByLayers("App","AllBundle","PlatForm");
复制代码

当然这个用例的执行是失败的,因为我们基本的包结构还没有调整。但有了架构守护测试用例,我们就可以逐步把代码移动到对应的Package中,直到守护用例运行通过为止。


接下来我们先运用IDE工具进行基础的包结构调整,调整后的结构如下



调整后运行ArchUnit测试运行结果如下


这些异常的提示就是我们需要处理的异常依赖。但是ArchUnit的这个提示比较不不友好,接下来我们介绍另外一种分析异常依赖的方式,使用Intellij Dependencies 。


Intellij Dependencies


我们选择对应的Package,选择Analyze菜单,点击Dependencies,可以找出该Package所依赖的相关类。


我们选择dynamic Package进行分析后,发现根据现有的架构约束,存在横向的Bundle依赖需要进行解除依赖。



我是在实际重构过程中,我们可以频繁借助该功能验证耦合解除情况,并且同时通过ArchUnit测试做好守护。


详细代码见Cloud Disk


总结


这一篇我们分享了如何借助工具进行异常依赖的分析。当我们有了未来的架构设计后,可以借助ArchUnit进行架构测试守护,通过Intellij的Dependendencies 我们可以方便以Package或者Class为单位进行依赖分析。


当我们已经分析出需要处理的异常依赖,接下来我们就可以逐步进行重构。下一篇,我们将给大家分享实践总结的一些重构套路,移动应用遗留系统重构(5)- 重构方法篇。


系列链接


大纲




作者:JunBin
链接:https://juejin.cn/post/6950077521790500894
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(3)-示例篇

前言 上一篇移动应用遗留系统重构(2)-架构篇我们介绍了业内的优秀架构实践以及CloudDisk团队根据业务情况设计的分层架构。 这一篇我们将介绍一个浓缩版的示例,示例中我们设计了一些常见的异常依赖,后续的重构篇我们也将基于这个示例进行操作演示。为了简化代码及...
继续阅读 »

前言


上一篇移动应用遗留系统重构(2)-架构篇我们介绍了业内的优秀架构实践以及CloudDisk团队根据业务情况设计的分层架构。


这一篇我们将介绍一个浓缩版的示例,示例中我们设计了一些常见的异常依赖,后续的重构篇我们也将基于这个示例进行操作演示。为了简化代码及对业务上下文的理解,示例中的部分实现都是空实现,重点体现异常的耦合依赖。


仓库地址:CloudDisk


CloudDisk示例


项目概述


CloudDisk是一个类似于Google Drive的云存储应用。该应用主要拥有3大核心业务模块。



  1. 文件模块:用于管理用户云端文件系统。用户能够上传、下载、浏览文件。

  2. 动态模块:类似微信朋友圈,用于可以在动态上分享信息及文件

  3. 个人中心模块:用于管理用户个人信息


问题说明


该项目已经维护超过10年以上,目前有用开发人员100+。代码在一个大单体模块中,约30w行左右,编译时间5分钟以上。团队目前主要面临几个问题。



  1. 开发效率低,编译时间长,经常出现代码合并冲突

  2. 代码质量差,经常修改出新问题

  3. 市场响应慢,需要对齐各个模块进行整包发布


代码分析


代码在一个Module中,且在一个Git仓中管理。采用"MVC"结构,按功能进行划分Package。


包结构如下:



主要包说明:



































包名功能说明
adapterViewPager RecycleView等适配器类
callback接口回调
controller主要的业务逻辑
model数据模型
uiActivity、Fragment相关界面
util公用工具类

主要类说明:







































类名功能说明
MainActivity应用主界面,用于加载显示各个模块的Fragment
CallBack网络接口操作回调
DynamicController动态模块主要业务逻辑,包含发布及获取列表
FileController文件模块主要业务逻辑,主要包含上传、下载、获取文件列表
UserController用户模块主要业务逻辑,主要包含登录,获取用户信息
HttpUtils网络请求,用于发送get及post请求
LogUtils主要用于进行日志记录

详细源码见CloudDisk



为了简化业务上下文理解,代码都是空实现,只体现模块的异常依赖,后续的MV*重构篇会持续补充常见坏味道示例代码。



总结


CloudDisk在业务最初发展的时候,采用了单一Module及简单“MVC”架构很好的支持了业务的发展,但随着业务的演化及人员膨胀,这样的模式已经很难高效的支持业务及团队的发展。


前面我们已经分享了“理想”(未来的架构设计)与“现实”(目前的代码现状),接下来在我们开始动手进行重构时,我们首先得知道往理想的设计架构演化,中间存在多少问题。一方面作为开始重构的输入,另外一方面我们有数据指标,也能评估工作量及衡量进度。


下一篇,我们将给大家分享移动应用遗留系统重构(4)-分析篇。介绍常用的分析工具及框架,并对CloudDisk团队目前的代码进行分析。


系列链接


大纲



作者:JunBin
链接:https://juejin.cn/post/6947855094272491556
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(2)-架构篇

前言 上一篇移动应用遗留系统重构(1)- 开篇我们分享了移动应用遗留系统常见的问题。那么好的实践或者架构设计是怎样的呢? 这一篇我们将整理业内优秀的移动应用架构设计,包含微信、淘宝、支付宝以及美团外卖。其中的部分产品也经历过遗留系统的重构改造,具有非常好的参考...
继续阅读 »

前言


上一篇移动应用遗留系统重构(1)- 开篇我们分享了移动应用遗留系统常见的问题。那么好的实践或者架构设计是怎样的呢?


这一篇我们将整理业内优秀的移动应用架构设计,包含微信、淘宝、支付宝以及美团外卖。其中的部分产品也经历过遗留系统的重构改造,具有非常好的参考意义。


优秀实践


微信


从微信对外分享的架构演进文章中可知,微信应用其实也是经历了从大单体到模块化的演进。




图片来源 微信Android模块化架构重构实践



我们看下介绍中后续改造后的架构设计。




图片来源 微信Android模块化架构重构实践



设计中提到重构主要3个目标



  • 改变通信方式 (API化)

  • 重新设计模块 (中心化业务代码回归各自业务)

  • 约束代码边界 (pins工程结构,更细粒度管控边界)


我们可以发现重构后架构比原来的单体应用的一些变化。



  1. 业务模块独立编译调试,耦合度低

  2. 代码复用高,有统一公共的组件库及kernel

  3. 模块职责、代码边界清晰,强约束


更多信息可阅读原文,微信Android模块化架构重构实践


手淘


从手机淘宝客户端架构探索实践的分享中介绍到手机淘宝从1.0用单工程编写开始,东西非常简陋;到2.0为索引许多三方库的庞大的单工程;再到3.0打破了单工程开发模式实现业务复用。




图片来源 手机淘宝客户端架构探索实践



淘宝架构主要分为四层,最上层是组件Bundle(业务组件),往下是容器(核心层),中间件Bundle(功能封装),基础库Bundle(底层库)。


文章提到架构演化的一些优点及变化很值得深思。



  1. 业务复用,减少人力

  2. 基础复用,做深做精

  3. 敏捷开发,快速试错


支付宝


在支付宝mPass实践讨论分析一文中,提到支付宝客户端的总体架构图如下。




图片来源 开篇 | 模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨



分享文章中介绍到5层架构设计如下:




  • 最底层是支付宝框架的容器层,包括类加载资源加载和安全模块;




  • 第二层是我们抽离出来的组件层,包括网络库,日志库,缓存库,多媒体库,日志等等,简单说这些是一些通用的能力层;




  • 第三层是我们定制的框架层,这是关键部分,是我们得以实现上千人,上千多个工程共同开发一个 App 的基础。




  • 第四层是基于框架封装出来的业务服务层;




  • 第五层便是具体的业务模块,其中每一个模块都是一个或多个具体的工程;




文章中介绍到关于工程之间的依赖关系的处理比较特别。


在支付宝的架构里,编译参与的部分是和运行期参与的部分是分离的:编译期使用 bundle 的接口包,运行期使用 bundle 包本身。bundle 的接口包是 bundle 包的一部分,即刚才说的 bundle 的代码部分。bundle 的资源包同时打进接口包,在编译期提供给另一个 bundle 引用。


更多信息可阅读原文,开篇 | 模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨


美团


最后看另外一个跨平台技术架构相关的分享,在外卖客户端容器化架构的演进分享中提到了美团外包的整体架构如下。




图片来源 外卖客户端容器化架构的演进



特别的一点是是采用了容器化架构,根据业务场景及PV,支持多种容器技术。在文末的总结提到,容器化架构相对于传统的移动端架构而言,充分地利用了现在的跨端技术,将动态化的能力最大化的赋予业务。通过动态化,带来业务迭代周期缩短、编译的加速、开发效率的提升等好处。同时,也解决了面临着的多端复用、平台能力、平台支撑、单页面多业务团队、业务动态诉求强等业务问题。但对线上的可用性、容器的可用性、支撑业务的线上发布上提出了更加严格的要求。


更多信息可阅读原文,外卖客户端容器化架构的演进


总结


架构是为了解决业务的问题,没有银弹。 但通过这些业内的优秀实践分享,我们可以发现一些优秀的设计范式。



  1. 代码复用



  • 公共能力复用,有专门统一管理应用公用的基础能力,如图片、网络、存储能力、安全等

  • 公用业务能力复用,有专门统一管理应用的业务通用组件,如分享、推送、登录等



  1. 低耦合,高内聚



  • 业务模块间通过API方式依赖,不依赖具体的模块实现

  • 依赖方向清晰,上层模块依赖下层模块



  1. 并行研发



  • 业务模块支持独立编译调试

  • 业务模块独立发布


结合这些特点及CloudDisk团队的业务,团队采用的架构设计如下。



下一篇,移动应用遗留系统重构(3)- 示例篇,我们将继续介绍CloudDisk的业务及团队问题,分析现有的代码。


参考


微信Android模块化架构重构实践


手机淘宝客户端架构探索实践



参考来自阿里云开发者社区,但链接已失效



开篇 | 模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨


外卖客户端容器化架构的演进


系列链接


大纲





作者:JunBin
链接:https://juejin.cn/post/6945313969556946980
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(1)- 开篇

前言 2008年9月22日,谷歌正式对外发布第一款Android手机。苹果公司最早于2007年1月9日的MacWorld大会上公布IOS系统。移动应用领域的发展已经超过10年。在App Annie 最新的移动市场报告中分享2020应用下载量已经达到2180亿次...
继续阅读 »

前言


2008年9月22日,谷歌正式对外发布第一款Android手机。苹果公司最早于2007年1月9日的MacWorld大会上公布IOS系统。移动应用领域的发展已经超过10年。在App Annie 最新的移动市场报告中分享2020应用下载量已经达到2180亿次,同比增加7%。根据Statista的统计,2020年度Google Play的应用数量为3148932个。


在移动互联网的高速发展及竞争中,更快及更高质量的交付用户,显然尤为重要。但很多产品随着移动互联网的发展,已经迭代超过10年。在这个过程中人员流动、技术债务累计、技术生态更新,使得产生了大量的遗留系统。就像一辆低排量的破旧汽车,再大的马路,技术再好的驾驶员,达到车辆本身的系统瓶颈,速度就很难再提升起来。


遗留系统



在以往的项目中,遇到了大量的这种遗留系统。这些系统具有以下一些特点。



  • 大泥球架构,代码量上百万行,开发人员超过100+

  • 内部耦合高,代码修改维护牵一发动全身,质量低

  • 编译集成调试慢,没有任何自动化测试,开发效率低

  • 技术栈陈旧,祖传代码,无人敢动


在这样的背景下,个别少的团队选择重写,当然没有良好的过程管理及方法,好多重写完又成了新的遗留系统。也有的团队选择重构,但目前相关的方法及教程比较少。这里推荐一下《重构(第2版)》,书中有基本的重构手法。另外一本《修改代码的艺术》,书中有很多基于遗留代码开发的示例。但对于开发人员来说,缺少比较贴近移动应用项目实战的重构指导及系统方法。很多团队依旧没有解决遗留系统根本的原因,仅靠不断的堆人,恶性循环。


CloudDisk 演示示例


CloudDisk是一个类似于Google Drive的云存储应用。该应用主要拥有3大核心业务模块,文件、动态及个人中心。


该项目已经维护超过10年以上,目前有用开发人员100+。目前代码在一个大模块中,约30w行左右,编译时间10分钟以上。团队目前主要还面临几个问题。



  1. 开发效率低



编译时间长,经常出现代码合并冲突。遗留大量技术债务,团队疲于交付需求




  1. 代码质量差



经常修改出新问题,版本提测问题多,没有任何自动化测试




  1. 版本发布周期长



往往需要1个月以上,市场反馈响应慢



我们希望通过一个更贴近实际工程项目的浓缩版遗留系统示例,持续解决团队在产品不断迭代中遇到的问题。从架构设计与分析、安全重构、基础生态设施、流水线、编译构建等方面,一步一步介绍如何进行持续演化。我们将通过文章及视频演示的方式进行分享,希望通过这个系列文章,大家可以更系统的掌握移动应用项目中实战的重构技巧及落地方法。


大纲



作者:JunBin
链接:https://juejin.cn/post/6943470229905211422
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

这15个Android开源库,只有经常逛Github的才知道!

哈喽,大家好,我是西哥! 又到了大家最喜欢了的环节--开源库推荐,前面为大家推荐了我收藏的一些非常酷的开源库,受到大家一致好评,还没看过的,请移步至: 【Android珍藏】推荐10个炫酷的开源库 【开源推荐】进阶实战,从一款音乐播放器开始 【2020年Git...
继续阅读 »

哈喽,大家好,我是西哥!


又到了大家最喜欢了的环节--开源库推荐,前面为大家推荐了我收藏的一些非常酷的开源库,受到大家一致好评,还没看过的,请移步至:


【Android珍藏】推荐10个炫酷的开源库


【开源推荐】进阶实战,从一款音乐播放器开始


【2020年GitHub 上那些优秀Android开源库,这里是Top10!】


本期又为大家带来了哪些有趣的库呢?本期为大家精选了15个有趣又有用的开源,排名不分先后,一起来看看吧!


1. Coil



Coil是Android上的一个全新的图片加载框架,它的全名叫做 coroutine image loader,即协程图片加载库。与传统的图片加载库Glide,Picasso或Fresco等相比。该具有轻量(只有大约1500个方法)、快、易于使用、更现代的API等优势。


它支持GIF和SVG,并且可以执行四个默认转换:模糊圆形裁剪灰度圆角


示例如下:


imageView.load(“https://www.example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}
复制代码

并且是全用Kotlin编写,如果你是纯Kotlin项目的话,那么这个库应该是你的首选。


Github地址:github.com/coil-kt/coi…


2. MultiSearchView



该库具有一个非常酷的Search View 动画!


使用非常简单,并且可以自定义,你可以在在styles.xml下添加自定义样式。


示例代码:


<com.iammert.library.ui.multisearchviewlib.MultiSearchView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
复制代码

multiSearchView.setSearchViewListener(object : MultiSearchView.MultiSearchViewListener{
override fun onItemSelected(index: Int, s: CharSequence) {
}

override fun onTextChanged(index: Int, s: CharSequence) {
}

override fun onSearchComplete(index: Int, s: CharSequence) {
}

override fun onSearchItemRemoved(index: Int) {
}

})
复制代码

自定义样式:


  <!-- Search Text Style. -->
<style name="SearchTextStyle">
<!-- Custom values write to here for SearchEditText. -->
<item name="android:focusable">true</item>
<item name="android:focusableInTouchMode">true</item>
<item name="android:enabled">true</item>
<item name="android:hint">Search</item>
<item name="android:imeOptions">actionSearch</item>
<item name="android:textSize">18sp</item>
<item name="android:maxLength">15</item>
<item name="android:inputType">textCapSentences</item>
<item name="android:textColorHint">#80999999</item>
<item name="android:textColor">#000</item>
</style>
复制代码

然后,您应该将样式设置为MultiSearchView下的app:searchTextStyle


Github地址:github.com/iammert/Mul…


3. CalendarView



CalendarView是一个高度可定制化的日历组件库,用recycleView实现。


它有如下特性:



  • 单一或范围选择

  • 周历或者月历模式

  • 边界日期

  • 自定义日历视图

  • 水平或者垂直滚动模式

  • 完全可定制的视图


该库的文档也非常全面,并包含许多示例。此外,还有一个示例应用程序展示了库的所有功能。


它是用纯Kotlin编写的,并在MIT许可下发布。如果您需要在应用程序中使用日历视图,这是一个不错的选择。



注意:该库通过Java 8+ API使用了java.time类,以便向后兼容,因为这些类是在Java 8中添加的。



因此,需要在app的build.gradle 中添加如下配置:


android {
defaultConfig {
// Required ONLY when setting minSdkVersion to 20 or lower
multiDexEnabled true
}

compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:<latest-version>'
}
复制代码

Github: github.com/kizitonwose…


4. Bubble Navigation















FloatingTopBarActivityTopBarActivity














BottomBarActivityEqualBottomBarActivity

Bubble Navigation是一个轻巧的库,可通过大量自定义选项轻松制作精美的导航栏。


它有很多非常的特性:




  • 针对不同用例的两种类型的NavigationViews




    • BubbleNavigationConstraintView(支持spreadspreadinside, 和 packed莫斯)




    • BubbleNavigationLinearView(允许平均分配,使用权重或packed模式)






  • 高度可定制化




  • 您可以添加小红点,它具有BubbleToggleView来创建新的UI组件,而不仅仅是导航




示例:


<com.gauravk.bubblenavigation.BubbleNavigationConstraintView
android:id="@+id/top_navigation_constraint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="380dp"
android:background="@color/white"
android:elevation="4dp"
android:padding="12dp"
app:bnc_mode="spread">

<com.gauravk.bubblenavigation.BubbleToggleView
android:id="@+id/c_item_rest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:bt_active="true"
app:bt_colorActive="@color/search_active"
app:bt_colorInactive="@color/search_inactive"
app:bt_icon="@drawable/ic_restaurant"
app:bt_shape="@drawable/transition_background_drawable_restaurant"
app:bt_title="@string/restaurant"
app:bt_padding="@dimen/internal_padding"
app:bt_titlePadding="@dimen/title_padding" />

<!-- Add more child items here - max upto 5 -->

</com.gauravk.bubblenavigation.BubbleNavigationConstraintView>
复制代码

Github文档很完善,有很多示例,更多用法和属性可去Github了解。


Github:github.com/gauravk95/b…


5. FabFilter



这是一个有趣的项目,它不是一个直接可用的库,而是一个示例应用程序,展示了使用不使用 MotionLayout两种方式来实现的高级UI动画。


详细的实现细节可以看看Medium上的系列文章:


“Complex UI/Animations on Android”


“Complex UI/Animations on Android — featuring MotionLayout”


Github:github.com/nikhilpanju…


6.android-showcase



android-showcase是一个非常优秀的开源项目,它是一个展示应用程序,展示了如何使用Kotlin和最新的Jetpack 技术栈来开发一个APP。


该项目为您带来了一系列最佳实践,工具和解决方案:



  • 100% Kotlin

  • 现代架构 (feature modules, clean architecture, Model-View-ViewModel, Model-View-Intent)

  • Android Jetpack组件

  • 单Activity模式,使用Navigation导航


看完这个项目,在模块化,Clean体系结构,测试、设置CI / CD工具,等方面,你将会受到启发。感谢作者的开源。


Github:github.com/igorwojda/a…


7. Croppy



Croppy是一个Android图片裁剪库。


它有很多强大的特性:



  • 双指缩放

  • 裁剪任意大小

  • 按照长宽比例裁剪

  • 显示裁剪后的Bitmap

  • 自动居中裁剪

  • 全面的动画使用体验


更多使用细节请看Github。


Github: github.com/lyrebirdstu…


8. RubberPicker



一个炫酷的、有趣的SeekBar动画库。


RubberPicker库包含RubberSeekBarRubberRangePicker,其灵感来自CubertoiOS橡胶范围选择器


使用示例:


布局文件中配置


<com.jem.rubberpicker.RubberSeekBar
...
app:minValue="20"
app:maxValue="80"
app:elasticBehavior="cubic"
app:dampingRatio="0.3"
app:stiffness="300"
app:stretchRange="24dp"
app:defaultThumbRadius="16dp"
app:normalTrackWidth="4dp"
app:highlightTrackWidth="8dp"
app:normalTrackColor="#AAAAAA"
app:highlightTrackColor="#BA1F33"
app:defaultThumbInsideColor="#FFF"
app:highlightDefaultThumbOnTouchColor="#CD5D67"/>

<!-- Similar attributes can be applied for RubberRangePicker as well-->
<com.jem.rubberpicker.RubberRangePicker
...
app:minValue="0"
app:maxValue="100"
app:elasticBehavior="linear"
app:dampingRatio="0.4"
app:stiffness="400"
app:stretchRange="36dp"
app:defaultThumbRadius="16dp"
app:normalTrackWidth="4dp"
app:highlightTrackWidth="8dp"
app:normalTrackColor="#AAAAAA"
app:highlightTrackColor="#BA1F33"
app:defaultThumbInsideColor="#CFCD5D67"
app:highlightDefaultThumbOnTouchColor="#CD5D67"/>
复制代码

或者,在代码中动态配置:


val rubberSeekBar = RubberSeekBar(this)
rubberSeekBar.setMin(20)
rubberSeekBar.setMax(80)
rubberSeekBar.setElasticBehavior(ElasticBehavior.CUBIC)
rubberSeekBar.setDampingRatio(0.4F)
rubberSeekBar.setStiffness(1000F)
rubberSeekBar.setStretchRange(50f)
rubberSeekBar.setThumbRadius(32f)
rubberSeekBar.setNormalTrackWidth(2f)
rubberSeekBar.setHighlightTrackWidth(4f)
rubberSeekBar.setNormalTrackColor(Color.GRAY)
rubberSeekBar.setHighlightTrackColor(Color.BLUE)
rubberSeekBar.setHighlightThumbOnTouchColor(Color.CYAN)
rubberSeekBar.setDefaultThumbInsideColor(Color.WHITE)

val currentValue = rubberSeekBar.getCurrentValue()
rubberSeekBar.setCurrentValue(currentValue + 10)
rubberSeekBar.setOnRubberSeekBarChangeListener(object : RubberSeekBar.OnRubberSeekBarChangeListener {
override fun onProgressChanged(seekBar: RubberSeekBar, value: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(seekBar: RubberSeekBar) {}
override fun onStopTrackingTouch(seekBar: RubberSeekBar) {}
})


//Similarly for RubberRangePicker
val rubberRangePicker = RubberRangePicker(this)
rubberRangePicker.setMin(20)
...
rubberRangePicker.setHighlightThumbOnTouchColor(Color.CYAN)

val startThumbValue = rubberRangePicker.getCurrentStartValue()
rubberRangePicker.setCurrentStartValue(startThumbValue + 10)
val endThumbValue = rubberRangePicker.getCurrentEndValue()
rubberRangePicker.setCurrentEndValue(endThumbValue + 10)
rubberRangePicker.setOnRubberRangePickerChangeListener(object: RubberRangePicker.OnRubberRangePickerChangeListener{
override fun onProgressChanged(rangePicker: RubberRangePicker, startValue: Int, endValue: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
override fun onStopTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
})
复制代码

更多、更详细的使用请看Github。


Github:github.com/Chrisvin/Ru…


9. Switcher



一个炫酷的Switcher 切换动画库,真是的太可爱了,我前面也写过文章专门介绍过:


炫酷!从未见过如此Q弹的Switcher


它的灵感来自于 Dribble上Oleg Frolov 的设计。


Github: github.com/bitvale/Swi…


10. StfalconImageViewer



StfalconImageViewer是一个图片查看库,
该库简单且可定制。它包含一个全屏图像查看器具有共享的图像过渡支持捏合缩放功能以及滑动手势来关闭手势。


Github文档说明了如何使用每个功能。同样值得注意的是:该库与所有最受欢迎的图像处理库(例如Picasso,Glide等)兼容。


所有可配置项如下:


StfalconImageViewer.Builder<String>(this, images, ::loadImage)
.withStartPosition(startPosition)
.withBackgroundColor(color)
//.withBackgroundColorResource(R.color.color)
.withOverlayView(view)
.withImagesMargin(R.dimen.margin)
//.withImageMarginPixels(margin)
.withContainerPadding(R.dimen.padding)
//.withContainerPadding(R.dimen.paddingStart, R.dimen.paddingTop, R.dimen.paddingEnd, R.dimen.paddingBottom)
//.withContainerPaddingPixels(padding)
//.withContainerPaddingPixels(paddingStart, paddingTop, paddingEnd, paddingBottom)
.withHiddenStatusBar(shouldHideStatusBar)
.allowZooming(isZoomingAllowed)
.allowSwipeToDismiss(isSwipeToDismissAllowed)
.withTransitionFrom(targeImageView)
.withImageChangeListener(::onImageChanged)
.withDismissListener(::onViewerDismissed)
.withDismissListener(::onViewerDismissed)
复制代码

更详细的使用请看Github。


Github: github.com/stfalcon-st…


11. Broccoli



Broccoli是一个show View Loading 库,也就是我常说的骨架屏,在内容加载的时候,显示一个占位符。


该库带有很平滑的动画效果,你可以配合RecyclerView一起使用,等待加载内容时,再也不枯燥了。


示例:


Broccoli broccoli = new Broccoli();

//add the default style placeholder
broccoli.addPlaceholders('activity', 'view_id', 'view_id');

or
//add the default style placeholder
broccoli.addPlaceholders('view1', 'view2', 'view3');

or

//add the custom style placeholder
broccoli.addPlaceholder(new PlaceholderParameter.Builder()
.setView('view')
.setAnimation('scaleAnimation');
.setDrawable(DrawableUtils.createRectangleDrawable(placeHolderColor, 0))
.build());

or
//add the custom style placeholder with gradient animation
broccoli.addPlaceholder(new PlaceholderParameter.Builder()
.setView('view')
.setDrawable(new BroccoliGradientDrawable(Color.parseColor("#DDDDDD"),
Color.parseColor("#CCCCCC"), 0, 1000, new LinearInterpolator())
.build());
broccoli.show();
复制代码

更多使用请看Github。


Github: github.com/samlss/Broc…


12. Orbit MVI



这是一个用于Kotlin和Android的Model-View-Intent (MVI)框架。它的灵感来自Jake Wharton,RxFeedback和Mosby的“Managing State with RxJava”。


根据ReadMe所说:



Orbit在您的redux实现周围提供了尽可能小的结构,以使其易于使用,但您仍可以使用RxJava的强大功能。



redux系统可能如下所示:


data class State(val total: Int = 0)

data class AddAction(val number: Int)

sealed class SideEffect {
data class Toast(val text: String) : SideEffect()
}

class CalculatorViewModel : OrbitViewModel<State, SideEffect> (State(), {

perform("addition")
.on<AddAction>()
.sideEffect { post(SideEffect.Toast("Adding ${event.number}")) }
.reduce {
currentState.copy(currentState.total + event.number)
}

...
})
复制代码

activity / fragment 中:


// Example of injection using koin, your DI system might differ
private val viewModel by viewModel<CalculatorViewModel>()

override fun onCreate() {
...
addButton.setOnClickListener { viewModel.sendAction(AddAction) }
}

override fun onStart() {
viewModel.connect(this, ::handleState, ::handleSideEffect)
}

private fun handleState(state: State) {
...
}

private fun handleSideEffect(sideEffect: SideEffect) {
when (sideEffect) {
is SideEffect.Toast -> toast(sideEffect.text)
}
}
复制代码

详细使用请看Github。


Github: github.com/babylonheal…


13. IndicatorScrollView















IndicatorScrollViewIndicatorScrollView

该库为NestedScrollView添加了逻辑,使它可以在滚动时,更改对指示器进行动态响应。


README文件包含开始项目所需的所有信息,例如如何使用IndicatorScrollViewIndicatorViewIndicatorItem。目前,它的版本为1.0.2,是根据Apache 2.0许可发布的。它支持API 16及更高版本。


文档示例很详细,更多使用相关请看Github。


Github: github.com/skydoves/In…


14. Cyanea



Cyanea 是一个Android 主题引擎库。


它允许那你动态更换应用主题,它内置了很多主题如:



  • Theme.Cyanea.Dark

  • Theme.Cyanea.Dark.LightActionBar

  • Theme.Cyanea.Dark.NoActionBar

  • Theme.Cyanea.Light

  • Theme.Cyanea.Light.DarkActionBar

  • Theme.Cyanea.Light.NoActionBar


更多详细信息请看Github。


Github: github.com/jaredrummle…


15. Android MotionLayout Carousel



这是一个示例项目,它展示了如何使用MotionLayout来实现一个炫酷的轮播图。


文档几乎没有任何说明,但是如果你最近也在探索MotionLayout,这将是一个很好示例。


Github: github.com/faob-dev/Mo…


总结


以上就是本期的开源项目推荐,如果你也有好玩的、有趣的、强大的开源项目,也可以推荐给西哥,欢迎评论区留言讨论。

作者:依然范特稀西

链接:https://juejin.cn/post/6856637313964441614
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

7个你应该知道的Gradle实用技巧

前言 Gradle在android开发中应用地十分广泛,但相信有很多同学并不很了解gradle 本文主要介绍了使用gradle的一些实用技巧,帮助读者增进对这个熟悉的陌生人的了解 主要包括以下内容 1.Gradle依赖树查询 2.使用循环优化Gradle依赖...
继续阅读 »

前言


Gradleandroid开发中应用地十分广泛,但相信有很多同学并不很了解gradle

本文主要介绍了使用gradle的一些实用技巧,帮助读者增进对这个熟悉的陌生人的了解

主要包括以下内容



  • 1.Gradle依赖树查询

  • 2.使用循环优化Gradle依赖管理

  • 3.支持代码提示的Gradle依赖管理

  • 4.Gradle模块化

  • 5.Library模块Gradle代码复用

  • 6.资源文件分包

  • 7.AAR依赖与源码依赖快速切换


1.Gradle依赖树查询


有时我们在分析依赖冲突时,需要查看依赖树,我们常用的查看依赖树的命令为


gradlew app:dependencies
复制代码

不过这种命令行方式查看依赖树出来的信息太多,看的有些费劲

所以官方又推出了Scan工具来帮助我们更加方便地查看依赖树

在项目根目录位置下运行gradle build --scan即可,然后会生成 HTML 格式的分析文件的分析文件


分析文件会直接上传到Scan官网,命令行最后会给出远程地址

第一次跑会让你在 Scan 官网注册一下,邮件确认后就能看了

scan 工具是按照依赖变体挨个分类的,debugCompileClassPath 就是 dedug 打包中的依赖包了


如上,使用这种方式分析依赖树更加方便简洁


2.使用循环优化Gradle依赖管理


如下所示,我们常常使用ext来管理依赖


    dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation rootProject.ext.dependencies["appcompat-v7"]
implementation rootProject.ext.dependencies["cardview-v7"]
implementation rootProject.ext.dependencies["design"]
implementation rootProject.ext.dependencies["constraint-layout"]
annotationProcessor rootProject.ext.dependencies["glide_compiler"]
...
}
复制代码

这样虽然实现了依赖的统一管理,但是随着项目越来越大,依赖也会越来越多,常常会有几十甚至上百行,导致build.gradle越来越长


有没有一种好的方式不在 build.gradle 中写这么多的依赖配置?

有,就是使用循环遍历依赖。

示例如下,首先添加config.gradle


ext{
dependencies = [
// base
"appcompat-v7" : "com.android.support:appcompat-v7:${version["supportLibraryVersion"]}",
...
]

annotationProcessor = [
"glide_compiler" : "com.github.bumptech.glide:compiler:${version["glideVersion"]}",
...
]

apiFileDependencies = [
"launchstarter" :"libs/launchstarter-release-1.0.0.aar"
]

debugImplementationDependencies = [
"MethodTraceMan" : "com.github.zhengcx:MethodTraceMan:1.0.7"
]

...

implementationExcludes = [
"com.android.support.test.espresso:espresso-idling-resource:3.0.2" : [
'com.android.support' : 'support-annotations'
]
]

...
}
复制代码

然后在build.gradle中配置如下:


apply from config.gradle
...

def implementationDependencies = project.ext.dependencies
def processors = project.ext.annotationProcesso
def implementationExcludes = project.ext.implementationExcludes
dependencies{
// 处理所有的 xxximplementation 依赖
implementationDependencies.each { k, v -> implementation v }
// 处理 annotationProcessor 依赖
processors.each { k, v -> annotationProcessor v }
// 处理所有包含 exclude 的依赖
implementationExcludes.each { entry ->
implementation(entry.key) {
entry.value.each { childEntry ->
exclude(group: childEntry)
}
}
}
...

}
复制代码

这样做的优点在于

1.后续添加依赖不需要改动build.gradle,直接在config.gradle中添加即可

2.精简了build.gradle的长度


3.支持代码提示的Gradle依赖管理


上面介绍了通过config.gradle管理依赖的方法

在我们添加Gradle依赖时,还有一些痛点

1.不支持代码提示

2.不支持单击跳转

3.多模块开发时,不同模块相同的依赖需要复制粘贴


使用buildSrc+kotlin可以解决这个问题

效果如下:



由于buildSrc是对全局的所有module的配置,所以可以在所有module中直接使用


这里就不多介绍了,详细开发及引入buildSrc的过程可见:

[译]Kotlin + buildSrc:更好的管理Gadle依赖


buildSrc vs includeBuild


上面介绍的方法使用的是buildSrc,使用起来比较方便

不过它的缺点在于构建速度上会慢一些,使用includeBuild可以实现同样的效果

两者实现的最终效果是差不多的

详细实现可见:【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本?巧用 includeBuild


4.Gradle模块化


我们在开发中,引入一些插件时,有时需要在build.gradle中引入一些配置,比如greendao,推送,tinker

这些其实是可以封装在相应gradle文件中,然后通过apply from引入

举个例子,例如在我们使用greendao数据库时,需要在build.gradle中指定版本


这种时候应该新建一个greendao-config.gradle


apply plugin: 'org.greenrobot.greendao'

//greenDao指定版本和路劲等
greendao {
//数据库的schema版本,也可以理解为数据库版本号
schemaVersion 1
//设置DaoMaster、DaoSession、Dao包名,也就是要放置这些类的包的全路径。
daoPackage 'com.example.ausu.big_progect.dao'
//设置DaoMaster、DaoSession、Dao目录
targetGenDir 'src/main/java'
}
复制代码

然后再在build.gradle中引入


apply from 'greendao-config.gradle'
复制代码

这样做主要有2个优点

1.单一职责原则,将greendao的相关配置封装在一个文件里,不与其他文件混淆

2.精简了build.gradle的代码,同时后续修改数据库相关时不需要修改build.gradle的代码


5.Library模块Gradle代码复用


随着我们项目的越来越大,Library Module也越建越多,每个Module都有自己的build.gradle

但其实每个build.gradle的内容都差不多,我们能不能将重复的部分封装起来复用?


我们可以做一个 basic 抽取,同样将共有参数/信息提取到 basic.gradle 中,每个 module apply,这样就是减少了不少代码量


apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
// 指定用于编译项目的 API 级别
compileSdkVersion Versions.compileSDK
// 指定在生成项目时要使用的 SDK 工具的版本,Android Studio 3.0 后不需要手动配置。
buildToolsVersion Versions.buildTools

// 指定 Android 插件适用于所有构建版本的版本属性的默认值
defaultConfig {
minSdkVersion Versions.minSDK
targetSdkVersion Versions.targetSDK
versionCode 1
versionName "1.0"
}

// 配置 Java 编译(编码格式、编译级别、生成字节码版本)
compileOptions {
encoding = 'utf-8'
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

lintOptions {
// lint 异常后继续执行
abortOnError false
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
}
复制代码

然后在相应的模块的build.gradle中引入即可


apply from:"../basic.gradle"

dependencies {
api Deps.constraintLayout
api Deps.retrofit
}
复制代码

这样是不是简洁很多?读者可根据项目实际情况判断是否适合抽取basic.gradle使用


6.资源文件分包


随着项目越来越大,项目中的资源文件也越来越大,比如layoutdrawable文件夹下的文件数量常常可达几百甚至上千个

我们能不能像代码一样,对资源文件进行分包呢?


答案是可以的,主要是利用gradlesourceSets属性

我们可以将资源文件像代码一样按业务分包,具体操作如下


1.新建res_xxx目录


main 目录下新建 res_core, res_feed(根据业务模块命名)等目录,在res_core中新建res目录中相同的文件夹如:layoutdrawable-xxhdpivalues等。


2.在gradle中配置res_xx目录


android {
//...
sourceSets {
main {
res.srcDirs(
'src/main/res',
'src/main/res_core',
'src/main/res_feed',
)
}
}
}
复制代码

以上就完成了资源文件分包,这样做主要有几点好处

1.按业务分包查找方便,结构清晰

2.strings.xmlkey-value型文件多人修改可以减少冲突

3.当删除模块或做组件化改造时资源文件删除或迁移方便,不必像以前一样一个个去找


7.AAR依赖与源码依赖快速切换


当我们的项目中Module越来越多,为了加快编译速度,常常把Module发布成AAR,然后在项目中直接依赖AAR

但是我们有时候又需要修改AAR,就需要依赖于源码

所以我们需要一个可以快速地切换依赖AAR与依赖源码的方式


我们下面举个例子,以retrofit为例

假如我们要修改retrofit的源码,修改步骤如下:

1.首先下载retrofit,可以放到和项目同级的目录,并修改目录名为retrofit-source,以便区分

2.在settings.gradle文件中添加需要修改的aar库的源码project


include ':retrofit-source'
project(':retrofit-source').projectDir = new File("../retrofit-source")
复制代码

3.替换aar为源码

build.gradle(android) 脚本中添加替换策略


allprojects {
repositories {
...
}

configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute module( "com.squareup.retrofit2:retrofit") with project(':retofit-source')
}
}
}
}
复制代码

如上几步,就可以比较方便地实现aar依赖与源码依赖间的互换了

这样做的主要优点在于

1.不需要修改原有的依赖配置,而是通过全局的配置,利用本地的源码替换掉aar,侵入性低

2.如果有多个Module依赖于同一个aar,不需要重复修改,只需在根目录build.gradle中修改一处


总结


本文主要介绍了几个实用的Gradle技巧,如果觉得有所帮助,可以帮忙点赞

如果发现本文还有什么不足,欢迎在评论区指出~


作者:RicardoMJiang
链接:https://juejin.cn/post/6947675376835362846
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一文读懂 View.Post 的原理及缺陷

很多开发者都了解这么一个知识点:在 Activity 的 onCreate 方法里我们无法直接获取到 View 的宽高信息,但通过 View.post(Runnable)这种方式就可以,那背后的具体原因你是否有了解过呢? 读者可以尝试以下操作。可以发现,除了通...
继续阅读 »

很多开发者都了解这么一个知识点:在 Activity 的 onCreate 方法里我们无法直接获取到 View 的宽高信息,但通过 View.post(Runnable)这种方式就可以,那背后的具体原因你是否有了解过呢?


读者可以尝试以下操作。可以发现,除了通过 View.post(Runnable)这种方式可以获得 View 的真实宽高外,其它方式取得的值都是 0


/**
* 作者:leavesC
* 时间:2020/03/14 11:05
* 描述:
* GitHub:https://github.com/leavesC
*/
class MainActivity : AppCompatActivity() {

private val view by lazy {
findViewById<View>(R.id.view)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getWidthHeight("onCreate")
view.post {
getWidthHeight("view.Post")
}
Handler().post {
getWidthHeight("handler")
}
}

override fun onResume() {
super.onResume()
getWidthHeight("onResume")
}

private fun getWidthHeight(tag: String) {
Log.e(tag, "width: " + view.width)
Log.e(tag, "height: " + view.height)
}

}
复制代码

github.leavesc.view E/onCreate: width: 0
github.leavesc.view E/onCreate: height: 0
github.leavesc.view E/onResume: width: 0
github.leavesc.view E/onResume: height: 0
github.leavesc.view E/handler: width: 0
github.leavesc.view E/handler: height: 0
github.leavesc.view E/view.Post: width: 263
github.leavesc.view E/view.Post: height: 263
复制代码

从这就可以引申出几个疑问:



  • View.post(Runnable) 为什么可以得到 View 的真实宽高

  • Handler.post(Runnable)View.post(Runnable)有什么区别

  • onCreateonResume 函数中为什么无法直接得到 View 的真实宽高

  • View.post(Runnable) 中的 Runnable 是由谁来执行的,可以保证一定会被执行吗


后边就来一一解答这几个疑问,本文基于 Android API 30 进行分析


一、View.post(Runnable)


看下 View.post(Runnable) 的方法签名,可以看出 Runnable 的处理逻辑分为两种:



  • 如果 mAttachInfo 不为 null,则将 Runnable 交由mAttachInfo内部的 Handler 进行处理

  • 如果 mAttachInfo 为 null,则将 Runnable 交由 HandlerActionQueue 进行处理


    public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
复制代码

1、AttachInfo


先来看View.post(Runnable)的第一种处理逻辑


AttachInfo 是 View 内部的一个静态类,其内部持有一个 Handler 对象,从注释可知它是由 ViewRootImpl 提供的


final static class AttachInfo {

/**
* A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
* handler can be used to pump events in the UI events queue.
*/
@UnsupportedAppUsage
final Handler mHandler;

AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
···
mHandler = handler;
···
}

···
}
复制代码

查找 mAttachInfo 的赋值时机可以追踪到 View 的 dispatchAttachedToWindow 方法,该方法被调用就意味着 View 已经 Attach 到 Window 上了


	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
···
}
复制代码

再查找dispatchAttachedToWindow 方法的调用时机,可以跟踪到 ViewRootImpl 类。ViewRootImpl 内就包含一个 Handler 对象 mHandler,并在构造函数中以 mHandler 作为构造参数之一来初始化 mAttachInfo。ViewRootImpl 的performTraversals()方法就会调用 DecorView 的 dispatchAttachedToWindow 方法并传入 mAttachInfo,从而层层调用整个视图树中所有 View 的 dispatchAttachedToWindow 方法,使得所有 childView 都能获取到 mAttachInfo 对象


	final ViewRootHandler mHandler = new ViewRootHandler();

public ViewRootImpl(Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
···
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
···
}

private void performTraversals() {
···
if (mFirst) {
···
host.dispatchAttachedToWindow(mAttachInfo, 0);
···
}
···
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
···
}
复制代码

此外,performTraversals()方法也负责启动整个视图树的 Measure、Layout、Draw 流程,只有当 performLayout 被调用后 View 才能确定自己的宽高信息。而 performTraversals()本身也是交由 ViewRootHandler 来调用的,即整个视图树的绘制任务也是先插入到 MessageQueue 中,后续再由主线程取出任务进行执行。由于插入到 MessageQueue 中的消息是交由主线程来顺序执行的,所以 attachInfo.mHandler.post(action)就保证了 action 一定是在 performTraversals 执行完毕后才会被调用,因此我们就可以在 Runnable 中获取到 View 的真实宽高了


2、HandlerActionQueue


再来看View.post(Runnable)的第二种处理逻辑


HandlerActionQueue 可以看做是一个专门用于存储 Runnable 的任务队列,mActions 就存储了所有要执行的 Runnable 和相应的延时时间。两个post方法就用于将要执行的 Runnable 对象保存到 mActions中,executeActions就负责将mActions中的所有任务提交给 Handler 执行


public class HandlerActionQueue {

private HandlerAction[] mActions;
private int mCount;

public void post(Runnable action) {
postDelayed(action, 0);
}

public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}

public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}

mActions = null;
mCount = 0;
}
}

private static class HandlerAction {
final Runnable action;
final long delay;

public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}

public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}

···

}
复制代码

所以说,getRunQueue().post(action)只是将我们提交的 Runnable 对象保存到了 mActions 中,还需要外部主动调用 executeActions方法来执行任务


而这个主动执行任务的操作也是由 View 的 dispatchAttachedToWindow来完成的,从而使得 mActions 中的所有任务都会被插入到 mHandler 的 MessageQueue 中,等到主线程执行完 performTraversals() 方法后就会来执行 mActions,所以此时我们依然可以获取到 View 的真实宽高


	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
···
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
···
}
复制代码

二、Handler.post(Runnable)


Handler.post(Runnable)View.post(Runnable)有什么区别呢?


从上面的源码分析就可以知道,View.post(Runnable)之所以可以获取到 View 的真实宽高,主要就是因为确保了获取 View 宽高的操作一定是在 View 绘制完毕之后才被执行,而 Handler.post(Runnable)之所以不行,就是其无法保证这一点


虽然这两种post(Runnable)的操作都是往同个 MessageQueue 插入任务,且最终都是交由主线程来执行。但绘制视图树的任务是在onResume被回调后才被提交的,所以我们在onCreate中用 Handler 提交的任务就会早于绘制视图树的任务被执行,因此也就无法获取到 View 的真实宽高了


三、onCreate & onResume


onCreateonResume 函数中为什么无法也直接得到 View 的真实宽高呢?


从结果反推原因,这说明当 onCreateonResume被回调时 ViewRootImpl 的 performTraversals()方法还未执行,那么performTraversals()方法的具体执行时机是什么时候呢?


这可以从 ActivityThread -> WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl 这条调用链上找到答案


首先,ActivityThread 的 handleResumeActivity 方法就负责来回调 Activity 的 onResume 方法,且如果当前 Activity 是第一次启动,则会向 ViewManager(wm)添加 DecorView


	@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
···
//Activity 的 onResume 方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
···
if (r.window == null && !a.mFinished && willBeVisible) {
···
ViewManager wm = a.getWindowManager();
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//重点
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
···
}
复制代码

此处的 ViewManager 的具体实现类即 WindowManagerImpl,WindowManagerImpl 会将操作转交给 WindowManagerGlobal


    @UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
复制代码

WindowManagerGlobal 就会完成 ViewRootImpl 的初始化并且调用其 setView 方法,该方法内部就会再去调用 performTraversals 方法启动视图树的绘制流程


public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
···
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
···
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
复制代码

所以说, performTraversals 方法的调用时机是在 onResume 方法之后,所以我们在 onCreateonResume 函数中都无法获取到 View 的实际宽高。当然,当 Activity 在单次生命周期过程中第二次调用onResume 方法时自然就可以获取到 View 的宽高属性


四、View.post(Runnable) 的兼容性


从以上分析可以得出一个结论:由于 View.post(Runnable)最终都是往和主线程关联的 MessageQueue 中插入任务且最终由主线程来顺序执行,所以即使我们是在子线程中调用View.post(Runnable),最终也可以得到 View 正确的宽高值


但该结论也只在 API 24 及之后的版本上才成立,View.post(Runnable) 方法也存在着一个版本兼容性问题,在 API 23 及之前的版本上有着不同的实现方式


	//Android API 24 及之后的版本
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

//Android API 23 及之前的版本
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
复制代码

在 Android API 23 及之前的版本上,当 attachInfo 为 null 时,会将 Runnable 保存到 ViewRootImpl 内部的一个静态成员变量 sRunQueues 中。而 sRunQueues 内部是通过 ThreadLocal 来保存 RunQueue 的,这意味着不同线程获取到的 RunQueue 是不同对象,这也意味着如果我们在子线程中调用View.post(Runnable) 方法的话,该 Runnable 永远不会被执行,因为主线程根本无法获取到子线程的 RunQueue


    static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();

static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}
复制代码

此外,由于sRunQueues 是静态成员变量,主线程会一直对应同一个 RunQueue 对象,如果我们是在主线程中调用View.post(Runnable)方法的话,那么该 Runnable 就会被添加到和主线程关联的 RunQueue 中,后续主线程就会取出该 Runnable 来执行


即使该 View 是我们直接 new 出来的对象(就像以下的示例),以上结论依然生效,当系统需要绘制其它视图的时候就会顺便取出该任务,一般很快就会执行到。当然,由于此时 View 并没有 attachedToWindow,所以获取到的宽高值肯定也是 0


        val view = View(Context)
view.post {
getWidthHeight("view.Post")
}
复制代码

View.post(Runnable)方法的兼容性问题做下总结:



  • 当 API < 24 时,如果是在主线程进行调用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 均会被执行。但只有在 View 被 AttachedToWindow 的情况下才可以获取到 View 的真实宽高

  • 当 API < 24 时,如果是在子线程进行调用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 都将永远不会被执行

  • 当 API >= 24 时,不管是在主线程还是子线程进行调用,只要 View 被 AttachedToWindow 后,提交的 Runnable 都会被执行,且都可以获取到 View 的真实宽高值。如果没有被 AttachedToWindow 的话,Runnable 也将永远不会被执行


五、总结


Activity 的 onResume 方法在第一次被调用后,绘制视图树的 Runnable 才会被 Post 到和主线程关联的 MessageQueue 中,虽然该 Runnable 和回调 Activity 的 onResume 方法的操作都是在主线程中执行的,但是该 Runnable 只有等到主线程后续将其从 MessageQueue 取出来后才会被执行,所以这两者其实是构成了异步行为,因此我们在onCreateonResume 这两个方法里才无法直接获取到 View 的宽高大小


当 View 还未绘制完成时,通过 View.post(Runnable)提交的 Runnable 会等到 View.dispatchAttachedToWindow方法被调用后才会被保存到 MessageQueue 中,这样也依然保证了该 Runnable 一定是会在 View 绘制完成后才会被执行,所以此时我们才能获取到 View 的宽高大小


除了View.post(Runnable)外,我们还可以通过 OnGlobalLayoutListener 来获取 View 的宽高属性,onGlobalLayout 方法会在视图树发生变化的时候被调用,在该方法中我们就可以来获取 View 的宽高大小


        view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
val width = view.width
}
})
复制代码

按照我自己的想法,系统提供View.post(Runnable)这个方法的目的不仅仅是为了用来获取 View 的宽高等属性这么简单,有可能是为了提供一种优化手段,使得我们可以在整个视图树均绘制完毕后才去执行一些不紧急又必须执行的操作,使得整个视图树可以尽快地呈现出来,以此优化用户体验


作者:业志陈
链接:https://juejin.cn/post/6939763855216082974
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一文读懂 View & Window 机制(二)

一文读懂 View & Window 机制(一)六、DecorView DecorView 是 FrameLayout 的子类,其 onResourcesLoaded 方法在拿到 PhoneWindow 传递过来的 layoutResource 后,就...
继续阅读 »

一文读懂 View & Window 机制(一)

六、DecorView


DecorView 是 FrameLayout 的子类,其 onResourcesLoaded 方法在拿到 PhoneWindow 传递过来的 layoutResource 后,就会生成对应的 View 并添加为自己的 childView,就像普通的 ViewGroup 通过 addView 方法来添加 childView 一样,该 childView 就对应 mContentRoot,我们可以在 Activity 中通过(window.decorView as ViewGroup).getChildAt(0)来获取到 mContentRoot


所以 DecorView 可以看做是 Activity 中整个视图树的根布局


public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

@UnsupportedAppUsage
private PhoneWindow mWindow;

ViewGroup mContentRoot;

DecorView(Context context, int featureId, PhoneWindow window,
WindowManager.LayoutParams params) {
···
}

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}

}
复制代码

七、ActivityThread


完成以上步骤后,此时其实还只是完成了 Activity 整个视图树的加载工作,虽然 Activity 的 attach方法已经创建了 Window 对象,但还需要将 DecorView 提交给 WindowManager 后才能正式将视图树展示到屏幕上


DecorView 具体的提交时机还需要看 ActivityThread 的 handleResumeActivity 方法,该方法用于回调 Activity 的 onResume 方法,里面就会回调到 Activity 的makeVisible 方法,从方法名就可以猜出来makeVisible就用于令 Activity 变为可见状态


	@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
···
r.activity.makeVisible();
···
}
复制代码

makeVisible 方法会判断当前 Activity 是否已经将 DecorView 提交给 WindowManager 了,如果还没的话就进行提交,最后将 DecorView 的可见状态设为 VISIBLE,至此才建立起 Activity 和 WindowManager 之间的关联关系,Activity 也才正式变为可见状态


    void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
复制代码

八、做下总结


对以上流程做下总结



  1. 每个 Activity 内部都包含一个 Window 对象,该对象的具体实现类是 PhoneWindow。Activity 的 setContentViewfindViewById 等操作都会交由 Window 来实现,Window 是 Activity 和整个 View 系统交互的入口

  2. PhoneWindow 根据 theme 和 features 得知 Activity 的基本视图属性,由此来选择合适的根布局文件 layoutResource,每种 layoutResource虽然在布局结构上略有不同,但是均会包含一个 ID 名为content的 FrameLayout,ContentParent 即该 FrameLayout。我们可以通过 Window.ID_ANDROID_CONTENT来拿到该 ID,也可以在 Activity 中通过 findViewById<View>(Window.ID_ANDROID_CONTENT) 来获取到ContentParent

  3. PhoneWindow 并不直接管理视图树,而是交由 DecorView 去管理。DecorView 会根据layoutResource来生成对应的 rootView 并将开发者指定的 ContentView 添加为ContentParent的 childView,所以可以将 DecorView 看做是视图树的根布局。正因为如此,Activity 的 findViewById 操作实际上会先交由 Window,Window 再交由 DecorView 去完成,因为 DecorView 才是实际持有 ContentView 的容器类

  4. PhoneWindow 是 Window 这个抽象类的的唯一实现类,Activity 和 Dialog 内部其实都是使用 PhoneWindow 来加载视图树,因此 PhoneWindow 成为了上层类和视图树系统之间的交互入口,从而也将 Activity 和 Dialog 的共同视图逻辑给抽象出来了,减轻了上层类的负担,这也是 Window 机制存在的好处之一

  5. Activity 的视图树是在makeVisible 方法里提交给 WindowManager 的,之后 WindowManagerImpl 会通过 ViewRootImpl 来完成整个视图树的绘制流程,至此 Activity 才对用户可见

  6. View 通过 Canvas 绘制自身,定义了具体的 UI 效果。View 和 ViewGroup 共同组成一个具体的视图树,视图树的根布局则是 DecorView,DecorView 的存在使得视图树有了一个统一的容器,有利于统一系统的主题样式并对所有 childView 进行统一管理。Activity 通过 Window 和视图树进行交互,将具体的视图树处理逻辑抽取给 PhoneWindow 实现,减轻了自身负担。PhoneWindow 拿到 DecorView 后,又通过 ViewRootImpl 来对 DecorView 进行管理,由其来完成整个视图树的 Measure、Layout、Draw 流程。当整个视图树绘制完成后,就将 DecorView 提交给 WindowManager,从而将 Activity 显示到屏幕上


九、一个 Demo


这里我也提供一个自定义 Window 的 Demo,实现了基本的拖拽移动和点击事件,代码点击这里:AndroidOpenSourceDemo



十、一文系列


最近比较倾向于只用一篇文章来写一个知识点,也懒得总是想文章标题,就一直沿用一开始用的一文读懂XXX,写着写着也攒了蛮多篇文章了,之前也已经写了几篇关于 View 系统的文章,希望对你有所帮助 😇😇


作者:业志陈
链接:https://juejin.cn/post/6942303848996274213
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一文读懂 View & Window 机制(一)

Android 系统中,Window 在代码层次上是一个抽象类,在概念上表示的是一个窗口。Android 中所有的视图都是通过 Window 来呈现的,例如 Activity、Dialog 和 Toast 等,它们实际上都是挂载在 Window 上的。大部分情...
继续阅读 »

Android 系统中,Window 在代码层次上是一个抽象类,在概念上表示的是一个窗口。Android 中所有的视图都是通过 Window 来呈现的,例如 Activity、Dialog 和 Toast 等,它们实际上都是挂载在 Window 上的。大部分情况下应用层开发者很少需要来和 Window 打交道,Activity 已经隐藏了 Window 的具体实现逻辑了,但我觉得来了解 Window 机制的一个比较大的好处是可以加深我们对 View 绘制流程以及事件分发机制的了解,这两个操作就涉及到我们的日常开发了,实现自定义 View 和解决 View 的滑动冲突时都需要我们掌握这方面的知识点,而这两个操作和 Window 机制有很大的关联。视图树只有被挂载到 Window 后才会触发视图树的绘制流程,之后视图树才有机会接收到用户的触摸事件。所以说,视图树被挂载到了 Window 上是 Activity 和 Dialog 等视图能够展示到屏幕上且和用户做交互的前置条件


本文就以 Activity 为例子,展开讲解 Activity 是如何挂载到 Window 上的,基于 Android API 30 进行分析,希望对你有所帮助 😇😇


一、Window


Window 存在的意义是什么呢?


大部分情况下,用户都是在和应用的 Activity 做交互,应用在 Activity 上接收用户的输入并在 Activity 上向用户做出交互反馈。例如,在 Activity 中显示了一个 Button,当用户点击后就会触发 OnClickListener,这个过程中用户就是在和 Activity 中的视图树做交互,此时还没有什么问题。可是,当需要在 Activity 上弹出 Dialog 时,系统需要确保 Dialog 是会覆盖在 Activity 之上的,有触摸事件时也需要确保 Dialog 是先于 Activity 接收到的;当启动一个新的 Activity 时又需要覆盖住上一个 Activity 显示的 Dialog;在弹出 Toast 时,又需要确保 Toast 是覆盖在 Dialog 之上的


这种种要求就涉及到了一个层次管理问题,系统需要对当前屏幕上显示的多个视图树进行统一管理,这样才能来决定不同视图树的显示层次以及在接收触摸事件时的优先级。系统就通过 Window 这个概念来实现上述目的


想要在屏幕上显示一个 Window 并不算多复杂,代码大致如下所示


	private val windowManager by lazy {
context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}

private val floatBallView by lazy {
FloatBallView(context)
}

private val floatBallWindowParams: WindowManager.LayoutParams by lazy {
WindowManager.LayoutParams().apply {
width = FloatBallView.VIEW_WIDTH
height = FloatBallView.VIEW_HEIGHT
gravity = Gravity.START or Gravity.CENTER_VERTICAL
flags =
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
} else {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
}
}

fun showFloatBall() {
windowManager.addView(floatBallView, floatBallWindowParams)
}
复制代码

显示一个 Window 最基本的操作流程有:



  1. 声明希望显示的 View,即本例子中的 floatBallView,其承载了我们希望用户看到的视图界面

  2. 声明 View 的位置参数和交互逻辑,即本例子中的 floatBallWindowParams,其规定了 floatBallView 在屏幕上的位置,以及和用户之间的交互逻辑

  3. 通过 WindowManager 来添加 floatBallView,从而将 floatBallView 挂载到 Window 上,WindowManager 是外界访问 Window 的入口


当中,WindowManager.LayoutParams 的 flags 属性就用于控制 Window 的显示特性和交互逻辑,常见的有以下几个:




  1. WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE。表示当前 Window 不需要获取焦点,也不需要接收各种按键输入事件,按键事件会直接传递给下层具有焦点的 Window




  2. WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL。表示当前 Window 区域的单击事件希望自己处理,其它区域的事件则传递给其它 Window




  3. WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED。表示当前 Window 希望显示在锁屏界面




此外,WindowManager.LayoutParams 的 type 属性就用于表示 Window 的类型。Window 有三种类型:应用 Window、子 Window、系统 Window。应用类Window 对应 Activity。子 Window 具有依赖关系,不能单独存在,需要附属在特定的父 Window 之中,比如 Dialog 就是一个子 Window。系统 Window 是需要声明权限才能创建的 Window,比如 Toast 和 statusBar 都是系统 Window


从这也可以看出,系统 Window 是处于最顶层的,所以说 type 属性也用于控制 Window 的显示层级,显示层级高的 Window 就会覆盖在显示层级低的 Window 之上。应用 Window 的层级范围是 1~99,子 Window 的层级范围是 1000~1999,系统 Window 的层级范围是 2000~2999。如果想要让我们创建的 Window 位于其它 Window 之上,那么就需要使用比较大的层级值了,但想要显示自定义的系统级 Window 的话就必须向系统动态申请权限


WindowManager.LayoutParams 内就声明了这些层级值,我们可以择需选取。例如,系统状态栏本身也是一个 Window,其 type 值就是 TYPE_STATUS_BAR


    public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {

public int type;

//应用 Window 的开始值
public static final int FIRST_APPLICATION_WINDOW = 1;
//应用 Window 的结束值
public static final int LAST_APPLICATION_WINDOW = 99;

//子 Window 的开始值
public static final int FIRST_SUB_WINDOW = 1000;
//子 Window 的结束值
public static final int LAST_SUB_WINDOW = 1999;

//系统 Window 的开始值
public static final int FIRST_SYSTEM_WINDOW = 2000;
//系统状态栏
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
//系统 Window 的结束值
public static final int LAST_SYSTEM_WINDOW = 2999;

}
复制代码

二、WindowManager


每个 Window 都会关联一个 View,想要显示 Window 也离不开 WindowManager,WindowManager 就提供了对 View 进行操作的能力。WindowManager 本身是一个接口,其又继承了另一个接口 ViewManager,WindowManager 最基本的三种操作行为就由 ViewManager 来定义,即添加 View、更新 View、移除 View


public interface ViewManager {
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
复制代码

WindowManager 的实现类是 WindowManagerImpl,其三种基本的操作行为都交由了 WindowManagerGlobal 去实现,这里使用到了桥接模式


public final class WindowManagerImpl implements WindowManager {

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}

}
复制代码

这里主要看下 WindowManagerGlobal 是如何实现 addView 方法的即可


首先,WindowManagerGlobal 会对入参参数进行校验,并对 LayoutParams 做下参数调整。例如,如果当前要显示的是子 Window 的话,那么就需要使其 LayoutParams 遵循父 Window 的要求才行


	public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
···
}
复制代码

之后就会为当前的视图树(即 view)构建一个关联的 ViewRootImpl 对象,通过 ViewRootImpl 来绘制视图树并完成 Window 的添加过程。ViewRootImpl 的 setView方法会触发启动整个视图树的绘制流程,即完成视图树的 Measure、Layout、Draw 流程,具体流程可以看我的另一篇文章:一文读懂 View 的 Measure、Layout、Draw 流程


	public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
···

ViewRootImpl root;
View panelParentView = null;

···

root = new ViewRootImpl (view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

// do this last because it fires off messages to start doing things
try {
//启动和 view 关联的整个视图树的绘制流程
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
复制代码

ViewRootImpl 内部最终会通过 WindowSession 来完成 Window 的添加过程,mWindowSession 是一个Binder对象,真正的实现类是 Session,也就是说,Window 的添加过程涉及到了 IPC 调用。后面就比较复杂了,能力有限就不继续看下去了


        mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
adjustLayoutParamsForCompatibility(mWindowAttributes);
res = mWindowSession.addToDisplayAsUser(
mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls
);
setFrame(mTmpFrame);
复制代码

需要注意的是,这里所讲的视图树代表的是很多种不同的视图形式。我们知道,在启动一个 Activity 或者显示一个 Dialog 的时候,都需要为它们指定一个布局文件,布局文件会通过 LayoutInflater 加载映射为一个具体的 View 对象,即最终 Activity 和 Dialog 都会被映射为一个 View 类型的视图树,它们都会通过 WindowManager 的 addView 方法来显示到屏幕上,WindowManager 对于 Activity 和 Dialog 来说具有统一的操作行为入口


三、Activity & Window


这里就以 Activity 为例子来展开讲解 Window 相关的知识点,所以也需要先对 Activity 的组成结构做个大致的介绍。Activity 和 Window 之间的关系可以用以下图片来表示





  1. 每个 Activity 均包含一个 Window 对象,即 Activity 和 Window 是一对一的关系




  2. Window 是一个抽象类,其唯一的实现类是 PhoneWindow




  3. PhoneWindow 内部包含一个 DecorView,DecorView 是 FrameLayout 的子类,其内部包含一个 LinearLayout,LinearLayout 中又包含两个自上而下的 childView,即 ActionBar 和 ContentParent。我们平时在 Activity 中调用的 setContentView 方法实际上就是在向 ContentParent 执行 addView 操作




Window 这个抽象类里定义了多个和 UI 操作相关的方法,我们平时在 Activity 中调用的setContentViewfindViewById方法都会被转交由 Window 来实现,Window 是 Activity 和视图树系统交互的入口。例如,其 getDecorView() 方法就用于获取内嵌的 DecorView,findViewById() 方法就会将具体逻辑转交由 DecorView 来实现,因为 DecorView 才是真正包含 contentView 的容器类


public abstract class Window {

public Window(Context context) {
mContext = context;
mFeatures = mLocalFeatures = getDefaultFeatures(context);
}

public abstract void setContentView(@LayoutRes int layoutResID);

@Nullable
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}

public abstract void setTitle(CharSequence title);

public abstract @NonNull View getDecorView();

···

}
复制代码

四、Activity # setContentView


每个 Activity 内部都包含一个 Window 对象 mWindow,在 attach 方法中完成初始化,这说明 Activity 和 Window 是一对一的关系。mWindow 对象对应的是 PhoneWindow 类,这也是 Window 的唯一实现类


public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {

@UnsupportedAppUsage
private Window mWindow;

@UnsupportedAppUsage
private WindowManager mWindowManager;

@UnsupportedAppUsage
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

//初始化 mWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
···
}

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

}
复制代码

Activity 的attach 方法又是在 ActivityThread 的 performLaunchActivity 方法中被调用的,在通过反射生成 Activity 实例后就会调用attach 方法,且可以看到该方法的调用时机是早于 Activity 的 onCreate 方法的。所以说,在生成 Activity 实例后不久其 Window 对象就已经被初始化了,而且早于各个生命周期回调函数


	private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
···
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}

···

activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);

···

if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
return activity;
}
复制代码

此外,从 Activity 的setContentView 的方法签名来看,具体逻辑都交由了 Window 的同名方法来实现,传入的 layoutResID 就是我们希望在屏幕上呈现的布局,那么 PhoneWindow 自然就需要去加载该布局文件生成对应的 View。而为了能够有一个对 View 进行统一管理的入口,View 应该要包含在一个指定的 ViewGroup 中才行,该 ViewGroup 指的就是 DecorView


下面就再来看下 PhoneWindow 是如何处理这一个流程的


五、PhoneWindow # setContentView


PhoneWindow 的 setContentView 方法的逻辑可以总结为:



  1. PhoneWindow 内部包含一个 DecorView 对象 mDecor。DecorView 是 FrameLayout 的子类,其内部包含两个我们经常会接触到的 childView:actionBar 和 contentParent,actionBar 即 Activity 的标题栏,contentParent 即 Activity 的视图内容容器

  2. 如果 mContentParent 为 null 的话则调用 installDecor() 方法来初始化 DecorView,从而同时初始化 mContentParent;不为 null 的话则移除 mContentParent 的所有 childView,为 layoutResID 腾出位置(不考虑转场动画,实际上最终的操作都一样)

  3. 通过LayoutInflater.inflate生成 layoutResID 对应的 View,并将其添加到 mContentParent 中,从而将我们的目标视图挂载到一个统一的容器中(不考虑转场动画,实际上最终的操作都一样)

  4. 当 ContentView 添加完毕后会回调 Callback.onContentChanged 方法,我们可以通过重写 Activity 的该方法从而得到布局内容改变的通知


所以说,Activity 的 setContentView 方法实际上就是在向 DecorView 的 mContentParent 执行 addView 操作,所以该方法才叫setContentView而非setView


public class PhoneWindow extends Window implements MenuBuilder.Callback {

private DecorView mDecor;

ViewGroup mContentParent;

@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将 layoutResID 对应的 View 添加到 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
}

mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回调通知 contentView 发生变化了
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);

// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeFrameworkOptionalFitsSystemWindows();

final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);

if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
···
} else {
···
}
···
}
}

}
复制代码

mContentParent 通过 generateLayout 方法来完成初始化,该方法主要完成的操作有两个:



  1. 读取我们为 Activity 设置的 theme 属性,以此配置基础的 UI 风格。例如,如果我们设置了 <item name="windowNoTitle">true</item>的话,那么就会执行 requestFeature(FEATURE_NO_TITLE) 来隐藏标题栏

  2. 根据 features 来选择合适的布局文件,得到 layoutResource。之所以会有多种布局文件,是因为不同的 Activity 会有不同的显示要求,有的要求显示 title,有的要求显示 leftIcon,而有的可能全都不需要,为了避免控件冗余就需要来选择合适的布局文件。而虽然每种布局文件结构上略有不同,但均会包含一个 ID 名为content的 FrameLayout,mContentParent 就对应该 FrameLayout。DecorView 会拿到 layoutResource 并生成对应的 View 对象(对应 DecorView 中的 mContentRoot),并将其添加为mContentParent的 childView


	protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.

TypedArray a = getWindowStyle();

···

//第一步
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}

···

// Inflate the window decor.

//第二步
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
···
//交由 DecorView 去生成 layoutResource 对应的 View
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

//正常来说每种 layoutResource 都会包含一个 ID 为 ID_ANDROID_CONTENT 的 ViewGroup
//如果找不到的话就直接抛出异常
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}

···
return contentParent;
}
复制代码


作者:业志陈
链接:https://juejin.cn/post/6942303848996274213
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(三),一个对象在JVM的生死存亡

当我们new了一个对象,会发生什么呢?来段代码: public class Tested { public static int T = 10; public int c = 1; } 复制代码 类初始化 在编译期,编译器会将 Tested.j...
继续阅读 »

当我们new了一个对象,会发生什么呢?来段代码:


public class Tested {
public static int T = 10;
public int c = 1;
}
复制代码

类初始化


在编译期,编译器会将 Tested.java类转换成 Tested.class 字节码文件。当虚拟机接收到new 字节码指令时,如果此时类还未被初始化,则虚拟机会先进行类的初始化过程。



在类加载完成后。虚拟机会为new Tested() 的Tested对象,在java堆中分配内存。而对象所需要的内存大小在类加载完成后就被确定了。


指针碰撞


如果 java 中的内存是规整的,即使用过的放在一边,空闲的在另一边,中间放着指针作为分界点的指示器。那所分配的内存就仅仅是将指针像空闲空间挪动一段与对象大小相等的距离。这种方式内称为指针碰撞。


空闲列表


如果 java 中的内存是不工整的,使用过的和空闲的内存相互交错,那么虚拟机就必须维护一个列表记录哪些内存是可用的。在分配的时候从列表中找到一块足够大的空间给对象示例,并更新表的记录。这种分配方式称为空闲列表。


对象初始化


当我们的对象内存被分配完毕后,虚拟机就会对对象进行初始化操作。



此时Tested 对象在我们眼里就算出生了,在虚拟机眼里就是真正可用的了。可对象的生命并不是无穷的,它也会经历自己的死亡。


可达性分析


在主流实现中,我们通过可达性分析来判断一个对象是否存活。实现思路是:通过一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始像下搜索,搜索所走的路径被称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。见图:


在这里插入图片描述


即使 Obj5 与 Obj4 由于与 GC Roots 没有引用链相连,所以我们称 GC Roots 到对象 Obj4 和 Obj5 不可达。所以 Obj4 和 Obj5 就是可回收的。


既然Obj4 和 Obj5 是可回收的,那么是否一定会被回收呢?不一定。此时虚拟机会进行第一次的标记过程。因为 java 内能够重写 finalize() 方法(在这里只是分析特例,不推荐在任何情况下使用此方法),当对象重写了此方法,并且 finalize() 方法还未被虚拟机调用,那么虚拟机就会将此对象放入一个专门的F-Queue队列,由一个单独的 Finalizer 线程去执行它,如果队列中对象的 finalize() 方法在虚拟机第二次标记之前执行,并在此次执行过程中又将自己与GC Roots 引用链相连,那么虚拟机在进行第二次标记时,就会将该对象从 F-Queue队列移除,否则就宣告该对象死亡。注意:finalize() 方法只会被执行一次,所以一个对象一生只有一次机会进入F-Queue队列,有机会逃脱本此死亡。


如果对象已经宣告死亡了,那么虚拟机怎么来回收它吗?


标记-清除算法


这是最基础的收集算法,主要分为标记和清除两个阶段。首先标记出所以需要回收的对象,在标记完成后统一回收所有被标记的对象。可以参考上面的空闲列表。其有两点不足:


a. 效率问题,标记和清除两个过程效率都不高。


b. 空间问题,因为堆中的内存不是规整的,已使用的和空闲的内存相互交错,这也就导致了每次GC回收后,产生大量的内存碎片,而当再次分配一个大对象时,如果无法找到足够的连续内存,又会再此触发GC回收。


复制算法


复制算法是将堆内存分成大小相等的两块,每次只使用其中一块,这样内存就是规整的了,参考指针碰撞。每当一块内存使用完了,就将该块内存中存活的对象复制到另一边,随后将该块内存一次清理掉。


现在的虚拟机都采用这种方式来回收新生代,只是并不是按照1:1的比例来划分内存,而是将内存分为一块较大的 Eden 空间,和两块较小的 Survivor 空间(HotSpot虚拟机默认Eden:Survivor = 8 :1)。每次只使用 Eden 和 其中一块 Survivor 空间,当回收时,将 Eden 空间和当前正使用的 Survivor 空间内存活的对象复制到另一块空闲的 Survivor空间,随后清空 Eden 和 刚才用过的 Survivor 内存。


注意:由于我们无法保证每次 存活的对象所占内存一直都不大于 Survivor 内存值,所以就会有溢出风险。所以在 分代收集算法 中,虚拟机会将内存先划分为一块新生代内存和一块为老年代内存。而在新生代内存中,会采用这种8:1:1的内存分配方式,如果溢出了,就将该情况下的存活对象全部放在老年代内存里,说白了就是一种兜底策略。这里要注意的是,不是溢出的那部分,而是全部的存活对象。


标记-整理算法


标记-整理算法中的标记过程,与标记-清除算法中的标记过程一样,不同的是,当标记完成并清理回收完对象后,会将当前不连续的碎片内存就行整理,即存活的对象都移到一端,来保证接下来要分配的内存的规整性。我们的 分代收集算法 中的老年代内存块,就是采用的该算法(当然也可以是标记-清除算法,不同虚拟机的策略不同)。所以就不再对分代收集算法就行赘述了。



好了,本文到这里,关于“对象”的生命周期的讲解就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考
1、周志明,深入理解JAVA虚拟机:机械工业出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6935481800365981727
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(二),Class类加载过程与类加载器

在说类加载器和双亲委派模型之前,我们先来梳理下Class类文件的加载过程,JAVA虚拟机为了保证 实现语言的无关性,是将虚拟机只与“Class 文件”字节码 这种特定形式的二进制文件格式 相关联,而不是与实现语言绑定。类加载过程Class类...
继续阅读 »


在说类加载器和双亲委派模型之前,我们先来梳理下Class类文件的加载过程,JAVA虚拟机为了保证 实现语言的无关性,是将虚拟机只与“Class 文件”字节码 这种特定形式的二进制文件格式 相关联,而不是与实现语言绑定。

类加载过程

Class类从被加载到虚拟机内存开始,到卸载出内存为止,其生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中加载过程见下:

在这里插入图片描述

加载阶段

加载阶段做了什么?过程见下图。其中类的全限定名是Class文件(JAVA由编译器自动生成)内的代表常量池内的16进制值所代表的特定符号引用。因为Class文件格式有其自己的一套规范,如第1-4字节代表魔数,第5-6字节代表次版本,第7-8字节代表主版本号等等。

说白了就是,虚拟机不关心我们的这种“特定二进制流”从哪里来的,从本地加载也好,从网上下载的也罢,都没关系。虚拟机要做的就是将该二进制流写在自己的内存中并生成相应的Class对象(并不是在堆中)。在这个阶段,我们能够通过我们自定义类加载器来控制二进制流的获取方式。

验证阶段

验证阶段,正因为加载阶段虚拟机不介意二进制的来源,所以就可能存在着影响虚拟机正常运行的安全隐患。所以虚拟机对于该二进制流的校验工作非常重要。校验方式包括但不限于:

准备阶段

准备阶段在此阶段将正式为类变量分配内存并设置变量的初始化值。注意的是,类变量是指 static 的静态变量,是分配在方法区之中的,而不像对象变量,分配在堆中。还有一点需要注意,final 常量在此阶段就已经被赋值了。如下:

    public static int SIZE = 10; // 初始化值 == 0
public static final int SIZE = 10; // 初始化值 == 10
复制代码

解析阶段

解析阶段是将常量池内的符号引用替换为直接引用的过程。符号引用就是上文说的Class文件格式标准所规定的特定字面量,而直接引用就是我们说的指针,内存引用等概念

初始化阶段

到了初始化阶段,就开始真正执行我们的字节码程序了。也可以理解成:类初始化阶段就是虚拟机内部执行类构造 < clinit >() 方法的过程。注意,这个类构造方法可不是虚拟机内部生成的,而是我们的编译器自动生成的,是编译器自动收集类中的所有类变量的 赋值动作 和静态语句块(static{}块)中的语句合并产生的,具体分析见下。

注意,这里说的是类变量赋值动作,即static 并且具有赋值操作,如果无赋值操作,那么在准备阶段进行的方法区初始化就算完成了。为何还要加上static{} 呢?我们可以把static{} 理解成:是由多个静态初始化动作组织成的一个特殊的“静态子句”,与其他的静态初始化动作一样。这也是为何 static {} 只会执行一遍并在对象构造方法之前执行的原因。如下代码:

public class Tested {
public static int T;
// public static int V; // 无赋值,不在类构造中再次初始化
public int c = 1; // 不会在类构造中

static {
T = 10;
}
}
复制代码

还有一点,编辑器收集类变量的顺序,也就是虚拟机在此初始化阶段的执行顺序,这个顺序就是变量在类中语句定义的先后顺序,如上面的:语句 2 : T 在 6 : T 之前,这是两个独立的语句。类构造< clinit >的其他特点如下:

编译期的< clinit >

我们将流程回溯到编译期阶段,以刚刚的Tested 类代码为例。通过 javap -c /Tested.class (注意:/../Tested 绝对路径),获取Class文件:

public class com.tencent.lo.Tested {
public static int T;

public int c;

public com.tencent.lo.Tested();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field c:I
9: return

static {};
Code:
0: bipush 10
2: putstatic #3 // Field T:I
5: return
}
复制代码

在Class 文件中我们能很明显的看到 invokespecial 对应的对象构造 "< init >" : () V ,那为什么没有看到< clinit > 类构造方法呢?其实上面的 static {} 就是。我们来看下OpenJDK源码 Constants接口,此接口定义了在编译器中所用到的常量,这是一个自动生成的类。

public interface Constants extends RuntimeConstants {
public static final boolean tracing = true;

Identifier idClassInit = Identifier.lookup("<clinit>");
Identifier idInit = Identifier.lookup("<init>");
}
复制代码

MemberDefinition类 中,判断是否为类构造器字符:

    public final boolean isInitializer() {
return getName().equals(idClassInit); // 类构造
}
public final boolean isConstructor() {
return getName().equals(idInit); // 对象构造
}
复制代码

而在MemberDefinition 的 toString() 方法中,我们能够看到,当类构造时,会输出特定字符,而不会像对象构造那样输出规范的字符串。

    public String toString() {
Identifier name = getClassDefinition().getName();
if (isInitializer()) { // 类构造
return isStatic() ? "static {}" : "instance {}";
} else if (isConstructor()) { // 对象构造
StringBuffer buf = new StringBuffer();
buf.append(name);
buf.append('(');
Type argTypes[] = getType().getArgumentTypes();
for (int i = 0 ; i < argTypes.length ; i++) {
if (i > 0) {
buf.append(',');
}
buf.append(argTypes[i].toString());
}
buf.append(')');
return buf.toString();
} else if (isInnerClass()) {
return getInnerClass().toString();
}
return type.typeString(getName().toString());
}
复制代码

类加载器

“虚拟机将类加载阶段中的“通过一个全限定名来获取描述此类的二进制字节流”这个动作放到了外部来实现,以便开发者可以自己决定如何获取所需的类文件,而实现这个动作的代码模块就被称为类加载器。对于任意一个类来说,只有在类加载器相同的情况下比较两者是否相同才有意义,否则即使是同个文件,在不同加载器下,在虚拟机看来其仍然是不同的,是两个独立的类。我们可以将类加载器分为三类”:

双亲委派

而所谓的双亲委派模型就是:“如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把加载的操作委托给父类加载器去完成,每一层次加载器都是如此,因此所有的加载请求都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围没有找到所需的类,因为上面所说的启动类加载器和扩展类加载器,只能加载特定目录之下的,或被-x参数所指定的类库),子类才会尝试自己加载”。注意这里说的父类只是形容层次结构,其并不是直接继承关系,而是通过组合方式来复用父类的加载器的。

在这里插入图片描述

“双亲委派的好处就是,使加载器也具备了优先级的层次结构。例如,java.lang.Object存放在< JAVA_HOME>\lib 下的rt.jar包内,无论哪个类加载器要加载这个类,最终都会委派给最顶层的启动类加载器,所以保证了Object类在各类加载器环境中都是同一个类。相反,如果没有双亲委派模型,如果用户编写了一个java.lang.Object类,并放在程序的ClassPath下,那么系统将会出现多个不同的Object类”。

为何?因为每个加载器各自为政,不会委托给父构造器,如上面所说,只要加载器不同,即使类Class文件相同,其也是独立的。

试想如果自己在项目中编写了一个java.lang.Object 类(当然不能放入rt.jar类库中替换掉同名Object文件,这样做没有意义,如果虚拟机加载校验能通过的话,只是相当于改了源码嘛),我们通过自定义的构造器来加载这个类可以吗?理论上来说,虽然这两个类都是java.lang.Object,但由于构造器不同,对于虚拟机来说这是不同的Class文件,当然可以。但是实际上呢?来段代码见下:

    public void loadPathName(String classPath) throws ClassNotFoundException {
new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
InputStream is = getClass().getResourceAsStream(name);
if (is == null)
return super.loadClass(name);
byte[] b;
try {
b = new byte[is.available()];
is.read(b);
} catch (Exception e) {
return super.loadClass(name);
}
return defineClass(name, b, 0, b.length);
}
}.loadClass(classPath);
}
复制代码

实际的执行逻辑是 defineClass 方法。可以发现,自定义加载器是无法加载以 java. 开头的系统类的。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError {

protectionDomain = preDefineClass(name, protectionDomain);
... // 略

return c;
}

private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// 在这里能看到系统类,自定义的加载器是不能加载的
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
... // 略

return pd;
}
复制代码

如果你用AS直接查看,你会发现,defineClass 内部是没有具体实现的,源码见下。可这并不代表android 的 defineClass 方法实现与 java 不同,因为都是引用的 java.lang 包下的ClassLoader 类,逻辑肯定都是一样的。之所以看到的源码不一样,这是由于SDK和JAVA源码包的区别导致的。SDK内的源码是谷歌提供给我们方便开发查看的,并不完全等同于源码。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
throw new UnsupportedOperationException("can't load this type of class file");
}
复制代码

好了,本文到这里就结束了,关于类加载过程的讲解也应该够用了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。

参考 1、周志明,深入理解JAVA虚拟机:机械工业出版社

收起阅读 »

Android修炼系列(一),写一篇易懂的动态代理讲解

在说动态代理之前,先来简单看下代理模式。代理是最基本的设计模式之一。它能够插入一个用来替代“实际”对象的“代理”对象,来提供额外的或不同的操作。这些操作通常涉及与“实际”对象的通信,因此“代理”对象通常充当着中间人的角色。 代理模式 代理对象为“实际”对象提供...
继续阅读 »

在说动态代理之前,先来简单看下代理模式。代理是最基本的设计模式之一。它能够插入一个用来替代“实际”对象的“代理”对象,来提供额外的或不同的操作。这些操作通常涉及与“实际”对象的通信,因此“代理”对象通常充当着中间人的角色。


代理模式


代理对象为“实际”对象提供一个替身或占位符以控制对这个“实际”对象的访问。被代理的对象可以是远程的对象,创建开销大的对象或需要安全控制的对象。来看下类图:


代理模式


再来看下类图对应代码,这是IObject接口,真实对象RealObj和代理对象ObjProxy都实现此接口:


/**
* 为实际对象Tested和代理对象TestedProxy提供对外接口
*/
public interface IObject {
void request();
}
复制代码

RealObj是实际处理request() 逻辑的对象,但是出于设计的考量,需要对RealObj内部的方法调用进行控制访问


public class RealObject implements IObject {

@Override
public void request() {
// 模拟一些操作
}
}
复制代码

ObjProxy是RealObj的代理类,其同样实现了IObject接口,所以具有相同的对外方法。客户端与RealObj的所有交互,都必须通过ObjProxy。


public class ObjProxy implements IObject {
IObject realT;

public ObjProxy(IObject t) {
realT = t;
}

@Override
public void request() {
if (isAllow()) realT.request();
}

private boolean isAllow() {
return true;
}
}
复制代码

番外


代理模式和装饰者模式不管是在类图,还是在代码实现上,几乎是一样的,但我们为何还要进行划分呢?其实学设计模式,不能拘泥于格式,不能死记形式,重要的是要理解模式背后的意图,意图只有一个,但实现的形式却可能多种多样。这也就是为何那么多变体依然属于xx设计模式的原因。


代理模式的意图是替代真正的对象以实现访问控制,而装饰者模式的意图是为对象加入额外的行为。


动态代理


Java的动态代理可以动态的创建代理并动态的处理所代理方法的调用,在动态代理上所做的所以调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的策略。类图见下:


动态代理


还以上面的代码为例,这是对外的接口IObject:


public interface IObject {
void request();
}
复制代码

这是 InvocationHandler 的实现类,类图中 Proxy 的方法调用都会被系统传入此类,即 invoke 方法,而 ObjProxyHandler 又持有着 RealObject 实例,所以 ObjProxyHandler 是“真正”对 RealObject 对象进行访问控制的代理类。


public class ObjProxyHandler implements InvocationHandler {
IObject realT;

public ObjProxyHandler(IObject t) {
realT = t;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// request方法时,进行校验
if (method.getName().equals("request") && !isAllow())
return null;
return method.invoke(realT, args);
}

private boolean isAllow() {
return false;
}
}
复制代码

RealObj是实际处理request() 逻辑的对象。


public class RealObject implements IObject {
@Override
public void request() {
// 模拟一些操作
}
}
复制代码

动态代理的使用方法如下:我们通过 Proxy.newProxyInstance 静态方法来创建代理,其参数如下,一个类加载器、一个代理实现的接口列表、一个 InvocationHandler 的接口实现。


    public void startTest() {
IObject proxy = (IObject) Proxy.newProxyInstance(
IObject.class.getClassLoader(),
new Class[]{IObject.class},
new ObjProxyHandler(new RealObject()));
proxy.request(); // ObjProxyHandler的invoke方法会被调用
}
复制代码

Proxy源码


来看下Proxy 源码,当我们 newProxyInstance(...) 时,首先系统会进行判空处理,之后获取我们实际的 Proxy 代理类 Class 对象,再通过一个参数的构造方法生成我们的代理对象 p(p : 返回值),这里能看出来 p 是持有我们的对象 h 的。注意 cons.setAccessible(true) 表示,即使是 cl 是私有构造,也可以获得对象。源码见下:


public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();

/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
...
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
cons.setAccessible(true);
// END Android-removed: Excluded AccessController.doPrivileged call.
}
return cons.newInstance(new Object[]{h});
...
}
复制代码

其中 getProxyClass0(...) 是用来检查并获取实际代理对象的。首先会有一个65535的接口限制检测,随后从代理缓存proxyClassCache 中获取代理类,如果给定的接口不存在,则通过 ProxyClassFactory 新建。见下:


    private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
复制代码

存放代理 Proxy.class 的缓存 proxyClassCache,是一个静态常量,所以在我们类加载时,其就已经被初始化完毕了。见下:


private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
复制代码

Proxy 提供的 getInvocationHandler(Object proxy)方法和 invoke(...) 方法很重要。分别为获取当前代理关联的调用处理器对象 InvocationHandler,并将当前Proxy方法调用 调度给 InvocationHandler。是不是与上面的代理思维很像,至于这两个方法何时被调用的,推测是写在了本地方法内,当我们调用proxy.request 方法时(系统创建Proxy时,会自动 implements 用户传递的接口,可以为多个),系统就会调用Proxy invoke 方法,随后proxy 将方法调用传递给 InvocationHandler。


public static InvocationHandler getInvocationHandler(Object proxy)
throws IllegalArgumentException
{
/*
* Verify that the object is actually a proxy instance.
*/
if (!isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy instance");
}
final Proxy p = (Proxy) proxy;
final InvocationHandler ih = p.h;

return ih;
}

// Android-added: Helper method invoke(Proxy, Method, Object[]) for ART native code.
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = proxy.h;
return h.invoke(proxy, method, args);
}
复制代码

ProxyClassFactory


重点是ProxyClassFactory 类,这里的逻辑不少,所以我将ProxyClassFactory 单独抽出来了。能看到,首先其会检测当前interface 是否已被当前类加载器所加载。


        Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
复制代码

之后会进行判断是否为接口。这也是我们说的第二个参数为何不能传基类或抽象类的原因。


        if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
复制代码

之后判断当前 interface 是否已经存在于缓存cache内了。


        if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
复制代码

检测非 public 修饰符的 interface 是否在是同一个包名,如果不是则抛出异常


    for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
...

复制代码

检验通过后,会 getMethods(...) 获取接口内的全部方法。


随后会对methords进行一个排序。具体的代码我就不贴了,排序规则是:如果方法相等(返回值和方法签名一样)或同是一个接口内方法,则当前顺序不变,如果两个方法所在的接口存在继承关系,则父类在前,子类在后。


之后 validateReturnTypes(...) 判断 methords 是否存在方法签名相同并且返回值类型也相同的methord,如果有则抛出异常。


之后通过 deduplicateAndGetExceptions(...) 方法,将 methords 方法内的相同方法的父类方法剔除掉,并将 methord 保存在数组中。


转成一维数组和二维数组,Method[] methodsArray,Class< ? >[][] exceptionsArray,随后给当前代理类命名:包名 + “$Proxy” + num


最后调用系统提供的 native 方法 generateProxy(...) 。这是真正的代理类创建方法。感兴趣的可以查看下java_lang_reflect_Proxy.cc源码
class_linker.cc源码


        List<Method> methods = getMethods(interfaces);
Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE);
validateReturnTypes(methods);
List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods);

Method[] methodsArray = methods.toArray(new Method[methods.size()]);
Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

return generateProxy(proxyName, interfaces, loader, methodsArray,
exceptionsArray);
复制代码


好了,本文到这里就结束了,关于动态代理的讲解也应该够用了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考
1、Head First 设计模式:中国电力出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6935029399125262349
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

研究Android音视频-2-MediaCodec使用:YUV码流编码位MP4的示例

本文解决的问题 查看编解码器 录制YUV文件 将YUV文件编码为MP4视频格式 官方的示意图 数据流 input:给解码器输入需要解码或需要编码的数据流 output:解码器输出解码好或编码好的数据给客户端 MediaCodec内部采用异步的方式处理数...
继续阅读 »

本文解决的问题



  1. 查看编解码器

  2. 录制YUV文件

  3. 将YUV文件编码为MP4视频格式


官方的示意图


数据流


input:给解码器输入需要解码或需要编码的数据流


output:解码器输出解码好或编码好的数据给客户端



MediaCodec内部采用异步的方式处理数据,将处理好的数据写入缓冲区,客户端从缓冲区取数据使用,使用后必须手动释放缓冲区,否则无法继续处理数据



状态



  • Stopped

    • Error

    • Uninitialized:新建MediaCodec后,会进入该状态

    • Configured:调用configured方法后,进入该状态



  • Executing

    • Flushed:调用start方法后,进入该状态

    • Running:调用dequeueInputBuffer方法后,进入该状态

    • End of Stream



  • Released


功能描述


打印设备支持的编解码选项


val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)//创建查看设备可以使用的编解码器
val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS)//创建查看所有编解码器

fun printCodecInfo() {
val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)//创建查看设备可以使用的编解码器
val codecInfos = mediaCodecList.codecInfos
codecInfos.forEach { codecInfo ->
if (codecInfo.isEncoder)
println(
"encoder name: ${codecInfo.name} \n" +
" canonicalName: ${codecInfo.canonicalName} \n" +
" isAlias: ${codecInfo.isAlias} \n" +
" isSoftwareOnly: ${codecInfo.isSoftwareOnly} \n" +
" supportedTypes: ${
codecInfo.supportedTypes.map {
println("encoder: $it")
}
} \n" +
" isVendor: ${codecInfo.isVendor} \n" +
" isHardwareAccelerated: ${codecInfo.isHardwareAccelerated}" +
""
)
}

codecInfos.forEach { codecInfo ->
if (!codecInfo.isEncoder)
println(
"decoder name: ${codecInfo.name} \n" +
" canonicalName: ${codecInfo.canonicalName} \n" +
" isAlias: ${codecInfo.isAlias} \n" +
" isSoftwareOnly: ${codecInfo.isSoftwareOnly} \n" +
" supportedTypes: ${
codecInfo.supportedTypes.map {
println("decoder: $it")
}
} \n" +
" isVendor: ${codecInfo.isVendor} \n" +
" isHardwareAccelerated: ${codecInfo.isHardwareAccelerated}" +
""
)
}
}

/*
打印示例
"video/avc":H.264硬件编码器
encoder name: OMX.qcom.video.encoder.avc
canonicalName: OMX.qcom.video.encoder.avc
isAlias: false
isSoftwareOnly: false
supportedTypes: [kotlin.Unit]
isVendor: true
isHardwareAccelerated: true
"video/hevc":H.265软解编码器
encoder name: c2.android.hevc.encoder
canonicalName: c2.android.hevc.encoder
isAlias: false
isSoftwareOnly: true
supportedTypes: [kotlin.Unit]
isVendor: false
isHardwareAccelerated: false
*/

/*查找指定的编解码器*/
val codec = findCodec("video/avc", false, true)//查找H.264硬解码器
fun findCodec(
mimeType: String,
isEncoder: Boolean,
isHard: Boolean = true
): MediaCodecInfo? {
val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecInfos = mediaCodecList.codecInfos
return codecInfos.find {
it.isEncoder == isEncoder && !it.isSoftwareOnly == isHard && hasThisCodec(
it,
mimeType
)
}
}
private fun hasThisCodec(codecInfo: MediaCodecInfo, mimeType: String): Boolean {
return codecInfo.supportedTypes.find { it.equals(mimeType) } != null
}
复制代码

录制yuv文件


打开Camera,通过回调流保存原始YUV数据。简单存几秒即可,1080p下存了225帧,存储占用1080*1920*225*3/2=667.4M字节


示例工程: 地址


示例代码:


 val fos = FileOutputStream("$filesDir/test.yuv")
cameraView = findViewById(R.id.cameraview)
cameraView.cameraParams.facing = Camera.CameraInfo.CAMERA_FACING_BACK
cameraView.cameraParams.isFilp = false
cameraView.cameraParams.isScaleWidth = true
cameraView.cameraParams.previewSize.previewWidth = 1920
cameraView.cameraParams.previewSize.previewHeight = 1080
cameraView.addPreviewFrameCallback(object : PreviewFrameCallback {
override fun analyseData(data: ByteArray): Any {
fos.write(data)
return 0
}

override fun analyseDataEnd(t: Any) {}
})
addLifecycleObserver(cameraView)
复制代码

YUV视频流编码为h.264码流并通过MediaMuxer保存为mp4文件


编码流程:



  1. 查询编码队列是否空闲

  2. 将需要编码的数据复制到编码队列

  3. 查询编码完成队列是否有完成的数据

  4. 将已编码完成的数据复制到cpu内存


单线程示例代码:


//TODO YUV视频流编码为H.264/H.265码流并通过MediaMuxer保存为mp4文件
fun convertYuv2Mp4(context: Context) {
val yuvPath = "${context.filesDir}/test.yuv"
val saveMp4Path = "${context.filesDir}/test.mp4"
File(saveMp4Path).deleteOnExit()

val mime = "video/avc" //若设备支持H.265也可以使用'video/hevc'编码器
val format = MediaFormat.createVideoFormat(mime, 1920, 1080)
format.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
)
//width*height*frameRate*[0.1-0.2]码率控制清晰度
format.setInteger(MediaFormat.KEY_BIT_RATE, 1920 * 1080 * 3)
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
//每秒出一个关键帧,设置0为每帧都是关键帧
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
format.setInteger(
MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR//遵守用户设置的码率
)

//定义并启动编码器
val videoEncoder = MediaCodec.createEncoderByType(mime)
videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
videoEncoder.start()

// 当前编码帧信息
val bufferInfo = MediaCodec.BufferInfo()

//定义混合器:输出并保存h.264码流为mp4
val mediaMuxer =
MediaMuxer(
"${context.filesDir}/test.mp4",
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
);
var muxerTrackIndex = -1
val byteArray = ByteArray(1920 * 1080 * 3 / 2)
var read = 0
var inputEnd = false//数据读取完毕,并且全部都加载至编码器
var pushEnd = false //数据读取完毕,并且成功发出eof信号
val presentTimeUs = System.nanoTime() / 1000


//从文件中读取yuv码流,模拟输入流
FileInputStream("${context.filesDir}/test.yuv").use { fis ->
loop1@ while (true) {
//step1 将需要编码的数据逐帧送往编码器
if (!inputEnd) {
//step1.1 查询编码器队列是否空闲
val inputQueueIndex = videoEncoder.dequeueInputBuffer(30);
if (inputQueueIndex > 0) {
read = fis.read(byteArray)
if (read == byteArray.size) {
//默认从Camera中保存的YUV NV21,编码后颜色成反,手动转为NV12后,颜色正常
val convertCost = measureTimeMillis {
val start = 1920 * 1080
val end = 1920 * 1080 / 4 - 1
for (i in 0..end) {
val temp = byteArray[2 * i + start]
byteArray[2 * i + start] = byteArray[2 * i + start + 1]
byteArray[2 * i + start + 1] = temp
}
}
//step1.2 将数据送往编码器,presentationTimeUs为送往编码器的跟起始值的时间差,单位为微妙
val inputBuffer =
videoEncoder.getInputBuffer(inputQueueIndex)
inputBuffer?.clear()
inputBuffer?.put(byteArray)
videoEncoder.queueInputBuffer(
inputQueueIndex,
0,
byteArray.size,
System.nanoTime() / 1000 - presentTimeUs,
0
)
} else {
inputEnd = true//文件读取结束标记
}
}
}

//step2 将结束标记传给编码器
if (inputEnd && !pushEnd) {
val inputQueueIndex = videoEncoder.dequeueInputBuffer(30);
if (inputQueueIndex > 0) {
val pts: Long = System.nanoTime() / 1000 - presentTimeUs
videoEncoder.queueInputBuffer(
inputQueueIndex,
0,
byteArray.size,
pts,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
pushEnd = true
println("数据输入完成,成功发出eof信号")
}
}

//step3 从编码器中取数据,不及时取出,缓冲队列被占用,编码器将阻塞不进行编码工作
val outputQueueIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 30)
when (outputQueueIndex) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
//step3.1 标记新的解码数据到来,在此添加视频轨道到混合器
muxerTrackIndex = mediaMuxer.addTrack(videoEncoder.outputFormat)
mediaMuxer.start()
}
MediaCodec.INFO_TRY_AGAIN_LATER -> {
}
else -> {
when (bufferInfo.flags) {
MediaCodec.BUFFER_FLAG_CODEC_CONFIG -> {
// SPS or PPS, which should be passed by MediaFormat.
}
MediaCodec.BUFFER_FLAG_END_OF_STREAM -> {
bufferInfo.set(0, 0, 0, bufferInfo.flags)
videoEncoder.releaseOutputBuffer(outputQueueIndex, false)
println("数据解码并获取完成,成功发出eof信号")
break@loop1
}
else -> {
mediaMuxer.writeSampleData(
muxerTrackIndex,
videoEncoder.getOutputBuffer(outputQueueIndex)!!,
bufferInfo
)
}
}
videoEncoder.releaseOutputBuffer(outputQueueIndex, false)
}
}
}

//释放应该释放的具柄
mediaMuxer.release()
videoEncoder.stop()
videoEncoder.release()
}
}
复制代码


作者:君子陌路
链接:https://juejin.cn/post/6955080139885838372
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

AdapterViewFlipper 图片/文字 轮播动画控件

1. 问题/坑点 1.1 item宽高不生效问题 需要注意的是,AdapterViewFlipper 在布局时,宽高一定要用 match_parent 或者 具体dp值。 如果宽、高中使用了 wrap_content 时,会导致 AdapterViewFlip...
继续阅读 »

1. 问题/坑点


1.1 item宽高不生效问题


需要注意的是,AdapterViewFlipper 在布局时,宽高一定要用 match_parent 或者 具体dp值


如果宽、高中使用了 wrap_content 时,会导致 AdapterViewFlipper 容器的宽高,最终变成第一个item的宽高。即使后续item的宽高超过第一个item,也不会生效,内容显示只会被限定在第一个的宽高范围内


原理也很好理解,后续item没有绘制出来时, wrap_content 计算出来的结果,就是第一个item的宽高。当后续 item 显示的时候,没有地方去重新更新父容器 AdapterViewFlipper 的宽高。


2. 常用方法




  1. AdapterViewAnimator支持的XML属性如下:



    • android:animateFirstView:设置显示组件的第一个View时是否使用动画。

    • android:inAnimation:设置组件显示时使用的动画。

    • android:loopViews:设置循环到最后一个组件时是否自动跳转到第一个组件。

    • android:outAnimation:设置组件隐藏时使用的动画。




  2. 轮播控制:



    • startFlippingstopFlipping : 开始、停止播放

    • showPreviousshowNext:上一个、下一个




  3. 轮播状态与参数



    • isFlipping:是否轮播中

    • flipInterval: 动画间隔




  4. 设置入场、出场动画:setInAnimationsetOutAnimation




3. 文字/图片 轮播 Demo



/**
* 图片/文字轮播
* 坑点:text_flipper height 如果设置wrap_content 导致item宽度只会以第一个item的为准
*/
class FlipperAnimActivity : AppCompatActivity(), View.OnClickListener {

private var textFlipper: AdapterViewFlipper? = null
private var imgFlipper: AdapterViewFlipper? = null
private var preBtn: Button? = null
private var nextBtn: Button? = null
private var autoBtn: Button? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_flipper_anim)
initTextFlipper()
initImgFlipper()
}

// 文字轮播
private fun initTextFlipper() {
textFlipper = findViewById(R.id.text_flipper)
val list = listOf("文字轮播测试0", "文字轮播测试02...")
textFlipper?.adapter = TextFlipperAdapter(this, list)
textFlipper?.setInAnimation(this, R.animator.text_flipper_in_from_bottom)
textFlipper?.setOutAnimation(this, R.animator.text_flipper_out_to_top)
// textFlipper?.flipInterval
// textFlipper?.startFlipping()
}

// 图片轮播
private fun initImgFlipper() {
imgFlipper = findViewById(R.id.img_flipper)
val list = listOf("http://www.nicesoso.com/test/file/img/test.jpg", "http://www.nicesoso.com/test/file/img/test_h_1.jpg",
"http://www.nicesoso.com/test/file/img/test_h_2.jpg")
imgFlipper?.adapter = ImgFlipperAdapter(this, list)
imgFlipper?.setInAnimation(this, R.animator.img_flipper_in)
preBtn = findViewById(R.id.prev_btn)
nextBtn = findViewById(R.id.next_btn) as Button
autoBtn = findViewById(R.id.auto_btn) as Button

preBtn?.setOnClickListener(this)
nextBtn?.setOnClickListener(this)
autoBtn?.setOnClickListener(this)
}

override fun onClick(v: View?) {
when (v?.id) {
R.id.prev_btn -> {
imgFlipper?.showPrevious()
imgFlipper?.stopFlipping()
}
R.id.next_btn -> {
imgFlipper?.showNext()
imgFlipper?.stopFlipping()
}
R.id.auto_btn -> {
imgFlipper?.startFlipping()
}
}
}

override fun onDestroy() {
super.onDestroy()
textFlipper?.takeIf { it.isFlipping }?.stopFlipping()
imgFlipper?.takeIf { it.isFlipping }?.stopFlipping()
}
}
复制代码

3.1 文字轮播:TextFlipperAdapter


class TextFlipperAdapter(private val context: Context, private val datas: List<String>) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.item_flipper_text, parent, false)
val textView = view?.findViewById<TextView?>(R.id.text)
textView?.text = datas.get(position)
return view
}

override fun getItem(position: Int): Any {
return datas.get(position)
}

override fun getItemId(position: Int): Long {
return position.toLong()
}

override fun getCount(): Int {
return datas.size
}
}
复制代码

3.2 图片轮播:ImgFlipperAdapter


class ImgFlipperAdapter(private val context: Context, private val datas: List<String>) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = convertView ?: ImageView(context)
(view as? ImageView)?.scaleType = ImageView.ScaleType.FIT_XY
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
(view as? ImageView)?.let { Glide.with(context).load(datas.get(position)).into(it) }

return view
}

override fun getItem(position: Int): Any {
return datas.get(position)
}

override fun getItemId(position: Int): Long {
return position.toLong()
}

override fun getCount(): Int {
return datas.size
}
}


复制代码

3.3 布局:activity_flipper_anim.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@android:color/holo_red_light"
android:orientation="vertical">

<!--宽高要必须设置填充满,否则wrap_content时,大小变成第一个item的大小-->
<AdapterViewFlipper
android:id="@+id/text_flipper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:autoStart="true"
android:flipInterval="2000" />
</RelativeLayout>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">

<AdapterViewFlipper
android:id="@+id/img_flipper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:flipInterval="5000" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:gravity="center"
android:text="图片轮播测试(5s)"
android:textSize="24sp" />

<Button
android:id="@+id/prev_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:text="上一个" />

<Button
android:id="@+id/next_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:text="下一个" />

<Button
android:id="@+id/auto_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:text="自动播放" />

</RelativeLayout>
</LinearLayout>
复制代码

3.4 动画


文字轮播,入场动画:res/animator/text_flipper_in_from_bottom.xml


<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="y"
android:valueFrom="100"
android:valueTo="0"
android:valueType="floatType" />
复制代码

文字轮播,出场动画:res/animator/text_flipper_out_to_top.xml


<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="y"
android:valueFrom="0"
android:valueTo="-100"
android:valueType="floatType" />
复制代码

图片轮播,入场动画:res/animator/img_flipper_in.xml


<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="x"
android:valueFrom="500"
android:valueTo="0"
android:valueType="floatType" />
复制代码

参考



  1. AdapterViewFlipper轻松完成图片轮播

作者:小明code
链接:https://juejin.cn/post/6954741671531642910
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Jcenter 停止服务,我哭了 —— 说一说我们的迁移方案

在今年的 2 月 3 日,Jcenter 运营官方发布一则通告称:包括 GoCenter、Bintray、JCenter 在内的多项软件包管理和分发服务都将停止运营。 UPDATE: To better support the community in thi...
继续阅读 »

在今年的 2 月 3 日,Jcenter 运营官方发布一则通告称:包括 GoCenter、Bintray、JCenter 在内的多项软件包管理和分发服务都将停止运营。


UPDATE: To better support the community in this migration, JFrog has extended the JCenter new package versions submission deadline through March 31st 2021.


To clarify, the JCenter repository will keep serving packages for 12 months until February 1st 2022. Only the JCenter REST API and UI will be sunsetted on May 1st 2021.


原文链接如下:jfrog.com/blog/into-t…


对于 JFrog 停止运营 Jcenter 的原因我们不予置评。但是 Jcenter 仓库作为 Android 开发者使用最多的仓库,突然停止运营对于很多开发者和开源企业都带来很大的影响。同样我们神策 Android 系的 SDK 都是开源发布在 Jcenter 仓库上,所以我们内部也开始启动迁移方案的调研,在这个过程中,我们主要调研了三种备选方案:JitPack 仓库、公有 Maven 仓库和私有 Maven 仓库,最终我们采用发布到公有 Maven 仓库作为主选,发布到 JitPack 仓库作为备份应急,过渡阶段也会继续发布到 Jcenter。本篇文章主要介绍公有 Maven 和 JitPack 仓库的发布流程。


1. JitPack 仓库


JitPack 是一个基于 GitHub 开源仓库的发布仓库,它完美地与 GitHub 仓库兼容,使用 Jcenter 发布仓库时,我们需要在脚本中标注对应的 GroupId、Artifac 等信息,但是使用 JitPack 可以省去 Jcenter 发布过程中诸多繁琐的配置。下面介绍基于 JitPack 仓库发布的步骤。


1.1. 发布 GitHub Release


当我们的代码从本地分支推送到 GitHub 远程分支后,我们就可以发布一个版本的 Release。 在仓库的 Release 主页面,点击“Draft a new release”按钮发布一个新版本。这里我们以我们开源的一款 SDK 为例。



分别填写 Tag version、Release title、Describe this release 字段,我们以本次提交的版本是 1.2.3 为例:



  • Tag version:v1.2.3

  • Release title: Release 1.2.3


最后点击下方的“Publish release”绿色按钮。


1.2. Jitpack 发布


进入Jitpack 官方,使用 GitHub 账号进行登录。




  • 点击右上角「Sign In」使用 GitHub 账号登录

  • 在「Git repo url」中输入需要发布的仓库地址


这里依照神策分析埋点 SDK 为例:



配置项说明:



  • Releases:已发布的 Release 版本;

  • Builds:构建记录;

  • Branches:当前仓库的分支;

  • Commits:提交记录;


在 Releases 选项卡页面,点击「Get it」按钮即可进行对应版本的发布。发布成功后 Log 选项卡会呈现绿色,提示「Status:ok」,反之则是红色,提示错误异常信息。


1.3. 版本引用


第一步:在项目根目录的 build.gradle 文件中添加:


allprojects {


repositories {


...


maven { url '[https://jitpack.io](https://jitpack.io/)' }


}


}


第二步:在项目 Module 下的 build.gradle 文件中添加:


dependencies {


implementation 'com.github.sensorsdata:sa-sdk-android:5.1.3'


}


至此我们就完成了版本远程仓库的发布和引用。


1.4. 版本删除


在 JitPack 仓库包中,如果一个版本出现 Bug 需要删除,则需要现在 GitHub 的 Release 中删除该版本即可。


2. 公有 Maven 仓库


或许当初是由于发布到 Maven 仓库流程过于繁琐,Android 才会选择 Jcenter 作为默认的仓库地址。但是随着 Jcenter 的停止运营,仍然需要迁移到 Maven 仓库。


对于发布到 Maven 仓库,Sonatype 官方也提供了基于 Gradle 的脚本配置的指导文档,同时 Android 官方也提供了基于 Maven Publish 插件发布的指导帮助文档。但是由于 Maven 仓库要求对于发布的资源必须进行签名,但是 Android 官方提供的配置文档缺失签名部分,所以会导致发布到 Maven 之后,并不能真正的发布成功。Sonatype 提供的基于的指导文档是基于 maven 插件的 uploadArchives 任务,而 maven 插件在 Gradle 7.0 中已经被废弃并且被删除。所以我们最终采用基于 maven-publish 插件的 publishing 任务来发布到 Maven 仓库。


下面详细介绍一下发布到共有 Maven 仓库的操作步骤。


2.1. 注册 Sonatype 账号和申请项目


2.1.1. 注册 Sonatype 账号


注册地址:issues.sonatype.org/secure/Sign…



2.1.2. 申请创建新项目


在注册完成账号之后,创建 Issue 申请项目。


申请地址:issues.sonatype.org/secure/Crea…



完成申请之后,会与官方的支持人员与你确认沟通,以便确认你是该域名的持有者。需要注意的是,现在针对 GitHub 用户的域名是以:io.github. 开头,不允许自定义。如果有独立的域名可以使用域名作为Group Id。


2.2. GPG 签名


对于发布到 Maven 的资源,需要有签名校验,比较普遍的签名有 GPG 和 PGP,这里使用 GPG 签名作为示例。


2.2.1. GPG 安装


GPG 可以使用安装命令行,也可以通过客户端工具安装。这里我使用 brew 命令进行安装。


➜ ~ brew install gpg


安装完成后,通过 gpg --version 查看是否安装成功。


➜ ~ gpg --version


gpg (GnuPG``/MacGPG2``) 2.2.10


libgcrypt 1.8.3


Copyright (C) 2018 Free Software Foundation, Inc.


License GPLv3+: GNU GPL version 3 or later <https:``//gnu``.org``/licenses/gpl``.html>


This is free software: you are free to change and redistribute it.


There is NO WARRANTY, to the extent permitted by law.


Home: /Users/dengshiwei/``.gnupg


支持的算法:


公钥:RSA, ELG, DSA, ECDH, ECDSA, EDDSA


对称加密:IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256,


TWOFISH, CAMELLIA128, CAMELLIA192, CAMELLIA256


散列:SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224


压缩:不压缩, ZIP, ZLIB, BZIP2


2.2.2. 生成密钥


指令:gpg --gen-key



输入:gpg --gen-key 后,会提示我们输入真实姓名、电子邮箱地址用于标识我们这个用户,最后确认信息时会让我们填入标识该用户的密码信息(很重要,一定要记住)。


2.2.3. 查看密钥


指令:gpg --list-keys


➜ ~ gpg --list-keys


/Users/dengshiwei/``.gnupg``/pubring``.kbx


------------------------------------


pub dsa2048 2010-08-19 [SC] [已过期:2020-06-15]


85E38F69046B44C1EC9FB07B76D78F0500D026C4


uid [已过期] GPGTools Team <team@gpgtools.org>


pub rsa2048 2021-03-01 [SC] [有效至:2023-03-01]


C19CC95F56C5D3EF8ADA2222133C922134E5CD035


uid [ 绝对 ] dengshiwei <dengshiwei@sensorsdata.cn>


sub rsa2048 2021-03-01 [E] [有效至:2023-03-01]


这里可以看到我的公钥 ID = C19CC95F56C5D3EF8ADA2222133C922134E5CD035,仅仅有公钥是不够的,我们需要发布公钥到密钥服务器才能进行识别。


2.2.4. 发布公钥到服务器


指令:gpg --keyserver hkp://pool.sks-keyservers.net --send-keys 公钥 ID


➜ ~ gpg --keyserver hkp:``//pool``.sks-keyservers.net --send-keys C19CC95F56C5D3EF8ADA299933C922134E5CD035


gpg: 将密钥‘33C922134E5CD035’上传到 hkp:``//pool``.sks-keyservers.net


2.2.5. 查看密钥是否发布成功


指令:gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 公钥 ID


➜ ~ gpg --keyserver hkp:``//pool``.sks-keyservers.net --recv-keys C19CC95F56C5D3EF8ADA299933C922134E5CD035


gpg: 密钥 33C922134E5CD035:“dengshiwei <dengshiwei@sensorsdata.cn>”未改变


gpg: 合计被处理的数量:1


gpg: 未改变:1


这样就标志着发布成功了。


2.2.6. 密钥导出


gpg 中提供了导出公钥和私钥的签名方法。



  • gpg -a -o public-file.key --export KeyId : 导出公钥到 public-file.key 文件中;

  • gpg -a -o private-file.key --export-secret-keys KeyId : 导出私钥到 private-file.key 文件中。


其中:



  • -a 为 --armor 的简写,表示密钥以 ASCII 的形式输出,默认以二进制的形式输出;

  • -o 为 --output 的简写,指定写入的文件。


2.3. 发布脚本


2.3.1. 引入发布和签名插件


在需要发布的工程项目的 build.gradle 文件中引入 maven-public 和 signing 插件。


build.gradle


apply plugin: 'signing'


apply plugin: 'maven-publish'


2.3.2. 配置发布脚本


maven-public 插件通过 publishing 任务来发布资源。这里的配置主要包含:javadoc 文件、pop 文件、javadocJar 文件等。


task sourceJar(``type``: Jar) {


from android.sourceSets.main.java.srcDirs


classifier = 'sources'


}


task javadoc(``type``: Javadoc) {


dependsOn 'assembleRelease'


source = android.sourceSets.main.java.srcDirs +


'build/generated/not_namespaced_r_class_sources/release/generateReleaseRFile/out/com/sensorsdata/analytics/android/sdk/R.java'


classpath += project.files(android.getBootClasspath().``join``(File.pathSeparator))


}


javadoc {


options {


encoding "UTF-8"


charSet 'UTF-8'


}


}


task javadocJar(``type``: Jar, dependsOn: javadoc) {


classifier = 'javadoc'


from javadoc.destinationDir


}


publishing {


publications {


mavenAndroid(MavenPublication) {


groupId = 'com.sensorsdata.analytics.android'


artifactId = 'SensorsAnalyticsSDK'


version = version


artifact sourceJar


artifact javadocJar


artifact(``'build/outputs/aar/SensorsAnalyticsSDK.aar'``)


// 配置 pop 文件格式


pom {


packaging 'aar'


name = 'The official Android SDK for Sensors Analytics.'


description = 'The official Android SDK for Sensors Analytics.'


url = 'The official Android SDK for Sensors Analytics.'


licenses {


license {


name=``'The Apache Software License, Version 2.0'


url=``'[http://www.apache.org/licenses/LICENSE-2.0.txt'](http://www.apache.org/licenses/LICENSE-2.0.txt')


}


}


developers {


developer {


id = 'dengshiwei'


name = 'dengshiwei'


email = 'dengshiwei@sensorsdata.com'


}


}


scm {


connection = '[https://github.com/sensorsdata/sa-sdk-android'](https://github.com/sensorsdata/sa-sdk-android')


developerConnection = '[https://github.com/sensorsdata/sa-sdk-android.git'](https://github.com/sensorsdata/sa-sdk-android.git')


url = '[https://github.com/sensorsdata/sa-sdk-android'](https://github.com/sensorsdata/sa-sdk-android')


}


}


}


}


// 配置远程仓库


repositories {


// maven 对应的用户名和密码自定义存在 local``.properties 文件中


Properties properties = new Properties()


properties.load(project.rootProject.``file``(``'local.properties'``).newDataInputStream())


def mavenUser = properties.getProperty(``"maven.user"``)


def mavenPassword = properties.getProperty(``"maven.password"``)


maven {


// 发布地址,新申请的项目发布地址为:https:``//s01``.oss.sonatype.org``/service/local/staging/deploy/maven2/


url = '[https://oss.sonatype.org/service/local/staging/deploy/maven2/'](https://oss.sonatype.org/service/local/staging/deploy/maven2/')


credentials {


username mavenUser


password mavenPassword


}


}


}


}


// 对 mavenAndroid 发布内容进行签名


signing {


sign publishing.publications.mavenAndroid


}


发布脚本配置中有以下需要注意:



  • groupId:需要配置自己申请的 groupId;

  • artifactId:需要修改为自己项目的 artifactId;

  • pom 中的文件描述需要修改为自己项目的描述;

  • repositories 部分配置了远程仓库对应的用户名和密码,发布地址需要根据是否是新项目进行修改,旧项目域名是 oss.sonatype.org,新项目域名是:s01.oss.sonatype.org

  • signing 签名部分需要配置对应的 gpg 密钥和账户信息


签名密钥配置


signing 签名要求在 gradle.properties 配置文件中配置密钥信息。


signing.keyId=YourKeyId


signing.password=YourPublicKeyPassword


signing.secretKeyRingFile=PathToYourKeyRingFile




  • signing.keyId:指刚才我们发布的公钥 ID,这里有一个坑就是这个 ID 的长度只能是 8 位,所以我们可以直接截取公钥 ID 的后 8 位即可;




  • signing.password:发布公钥对应的密码;




  • signing.secretKeyRingFile:指 gpg key 导出的私钥对应的问题,是绝对路径。可以通过指令 gpg --keyring secring.gpg --export-secret-keys > ~/.gnupg/secring.gpg 进行导出。




至此,我们完成发布相关的所有配置。


2.4. 版本发布


当我们配置好 publishing 后,我们可以通过指令:./gradlew publish 来执行发布任务。还可以通过 Android Studio 中的 Gradle 任务看板来执行 publish 任务。



2.5. 版本引用


第一步:在项目根目录的 build.gradle 文件中添加:


allprojects {


repositories {


...


mavenCentral()


}


}


第二步:在项目 Module 下的 build.gradle 文件中添加:


dependencies {


implementation 'com.github.sensorsdata:sa-sdk-android:5.1.3'


}


至此我们就完成了版本远程仓库的发布和引用。


3. Jcenter 同步到 Maven


对于新发布的版本,我们可以选择直接发布到 Maven,但是对于已经发布到 Jcenter 的仓库,我们需要借助 Jcenter 提供的 Maven Central 功能进行同步。


3.1. Jcenter 配置 GPG 密钥


在 Jcenter 首页点击个人头像后点击「Edit Profile」进入个人信息配置页面,然后选中「GPG Signing」选项,配置导出的公钥和私钥。



配置完成后点击「Update」按钮即可。


3.2. Jcenter 配置 Sonatype 账号


在个人信息配置页面点击「Accounts」选项,配置注册的 Sonatype 账号。



3.3. 同步仓库到 Maven


进入仓库主页,点击「Maven Central」选项卡切换到对应的页面,点击「Sync」同步至 Maven。



至此完成整个 Jecenter 到 Maven 仓库的同步。


4. 总结


本次 Jcenter 的停止维护对于 Android 开发者来说会造成很大的影响,同时在迁移配置脚本的过程中也遇到很多问题,这里就不一一列举了,遇到问题首先要做的就是要反复检查核对自己的配置。希望 Android 官方能尽快出一个快速高效的迁移方案。


作者:Ant_way
链接:https://juejin.cn/post/6954924581677170724
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

SparseArray解析

作者:btcw链接:https://juejin.cn/post/6954972237443104782来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SparseArray


Sparse[spɑːrs]


文档介绍


/**
* <code>SparseArray</code> maps integers to Objects and, unlike a normal array of Objects,
* its indices can contain gaps. <code>SparseArray</code> is intended to be more memory-efficient
* than a
* <a href="/reference/java/util/HashMap"><code>HashMap</code></a>, because it avoids
* auto-boxing keys and its data structure doesn't rely on an extra entry object
* for each mapping.
*
* <p>Note that this container keeps its mappings in an array data structure,
* using a binary search to find keys. The implementation is not intended to be appropriate for
* data structures
* that may contain large numbers of items. It is generally slower than a
* <code>HashMap</code> because lookups require a binary search,
* and adds and removes require inserting
* and deleting entries in the array. For containers holding up to hundreds of items,
* the performance difference is less than 50%.
*
* <p>To help with performance, the container includes an optimization when removing
* keys: instead of compacting its array immediately, it leaves the removed entry marked
* as deleted. The entry can then be re-used for the same key or compacted later in
* a single garbage collection of all removed entries. This garbage collection
* must be performed whenever the array needs to be grown, or when the map size or
* entry values are retrieved.
*
* <p>It is possible to iterate over the items in this container using
* {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
* <code>keyAt(int)</code> with ascending values of the index returns the
* keys in ascending order. In the case of <code>valueAt(int)</code>, the
* values corresponding to the keys are returned in ascending order.
*/
复制代码

SparseArray是谷歌提供的k-v键值对存储类,key固定为int,value为泛型(内部为Object)。虽然


内部主要方法


ContainerHelpers


工具类,二分查找法查找int或者long值,找到返回index,没有找到返回取反后的值(为负数)。


PS:


>>: 	带符号右移
>>>: 无符号右移
最高位为1表示负数,负数则数值位取反

01111111111111111111111111111111 int maxVal 补:01111111111111111111111111111111
10000000000000000000000000000000 int minVal 补:11111111111111111111111111111111
因为在计算机系统中,数值一律用补码来表示和存储。0的补码属于正数范围,所以int值的范围高区间(正数区间)减1:
-2^31~2^31-1

复制代码

// This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;

while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];

if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present 取反为负
}
复制代码

put


public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;

if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//可能碰到需要重新排列,如果重排则重新计算索引位置。
if (mGarbage && mSize >= mKeys.length) {
gc();

// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}

mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
复制代码

先用二分查找法查找查找值的index,index大于0则集合中存在,小于0则不存在(见ContainerHelpers)。找到则更新;没找到,则获取需要插入的index位置(返回的负数为插入位置取反),key数组和value数组分别在对应index插入元素。也就是说这里面的元素是通过key的大小进行排序的。其中插入元素的方法是在GrowingArrayUtils.insert内部中调用System.arraycopy,内部真实数组的size在这里进行改变


delete


删除某个键值对,这里的删除并不是真实删除,而是把它的value标记为DELETED,mGarbage标记为true。然后在put、size、keyAt、valueAt、setValueAt、indexForKey、indexOfValue、indexOfValue、indexOfValueByValue、append等方法中触发成员gc方法


/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
复制代码

gc


遍历数组,把未被标记为DELETE的元素放到数组前面,并刷新size大小。(这里的size并不是内存两个数组的size大小,而是有效位数的大小)


private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);

int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;

for (int i = 0; i < n; i++) {
Object val = values[i];

if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}

o++;
}
}

mGarbage = false;
mSize = o;

// Log.e("SparseArray", "gc end with " + mSize);
}
复制代码

get


通过key获取元素。根据调用方法获取不到返回默认或者null。


/**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
public E get(int key) {
return get(key, null);
}

/**
* Gets the Object mapped from the specified key, or the specified Object
* if no such mapping has been made.
*/
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
复制代码

append


对比put,在append的元素大于最大的一个的时候,直接追加在最后,而不是先二分查找再插入。不大于最后一个的时候就调用put。


/**
* Puts a key/value pair into the array, optimizing for the case where
* the key is greater than all existing keys in the array.
*/
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}

if (mGarbage && mSize >= mKeys.length) {
gc();
}

mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
复制代码

和HashMap、ArrayMap对比,SparseArray的优缺点:


SparseArray的限制在于键必须是int类型,值必须是Object类型。这样可以避免key自动装箱产生过多的Object。但是这样的话,如果key值相同,那么数据就会被直接覆盖。


SparseArray不能保证保留它们的插入顺序,在迭代的时候应该注意。SparseArray中没有Iterator,SparseArray只实现了Cloneable接口,而没有继承Collection、List或者Map接口。


查找数据的时候使用的是二分法,明显比通过hashcode慢,所以数据越大,查找速度慢的劣势越明显,所以SparseArray适于数据一千以内的场景中。


优点:



  • 避免了基本数据类型的装箱操作

  • 不需要额外的结构体,单个元素的存储成本更低

  • 数据量小的情况下,随机访问的效率更高


缺点:



  • 插入操作需要复制数组,增删效率降低

  • 数据量巨大时,复制数组成本巨大,gc()成本也巨大

  • 数据量巨大时,查询效率也会明显下降


————————————————


参考资料:


优缺点总结:blog.csdn.net/b1480521874…


作者:btcw
链接:https://juejin.cn/post/6954972237443104782
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

iOS -- malloc分析(2)

2.10 segregated_band_grow分析boolean_t segregated_band_grow(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, u...
继续阅读 »

2.10 segregated_band_grow分析


boolean_t
segregated_band_grow(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
用来计算slot_current_base_addr 的联合体
nano_blk_addr_t u; // the compiler holds this in a register
uintptr_t p, s;
size_t watermark, hiwater;

if (0 == pMeta->slot_current_base_addr) { // First encounter?
//利用nano_blk_addr_t 来计算slot_current_base_addr。
u.fields.nano_signature = NANOZONE_SIGNATURE;
u.fields.nano_mag_index = mag_index;
u.fields.nano_band = 0;
u.fields.nano_slot = (slot_bytes >> SHIFT_NANO_QUANTUM) - 1;
u.fields.nano_offset = 0;

//根据设置的属性计算 slot_current_base_addr
p = u.addr;
pMeta->slot_bytes = (unsigned int)slot_bytes;
pMeta->slot_objects = SLOT_IN_BAND_SIZE / slot_bytes;
} else {
p = pMeta->slot_current_base_addr + BAND_SIZE; // Growing, so stride ahead by BAND_SIZE

u.addr = (uint64_t)p;
if (0 == u.fields.nano_band) { // Did the band index wrap?
return FALSE;
}

assert(slot_bytes == pMeta->slot_bytes);
}
pMeta->slot_current_base_addr = p;
//BAND_SIZE = 1 << 21 = 2097152 = 256kb
mach_vm_address_t vm_addr = p & ~((uintptr_t)(BAND_SIZE - 1)); // Address of the (2MB) band covering this (128KB) slot
if (nanozone->band_max_mapped_baseaddr[mag_index] < vm_addr) {
//如果最大能存储的地址 仍然小于目标地址,则小开辟新的band
#if !NANO_PREALLOCATE_BAND_VM
// Obtain the next band to cover this slot
//// mac 和模拟器 或重新使用
// Obtain the next band to cover this slot
//重新申请新的 band,调用mach_vm_map 从pmap 转换。
kern_return_t kr = mach_vm_map(mach_task_self(), &vm_addr, BAND_SIZE, 0, VM_MAKE_TAG(VM_MEMORY_MALLOC_NANO),
MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_DEFAULT, VM_PROT_ALL, VM_INHERIT_DEFAULT);

void *q = (void *)vm_addr;
if (kr || q != (void *)(p & ~((uintptr_t)(BAND_SIZE - 1)))) { // Must get exactly what we asked for
if (!kr) {
mach_vm_deallocate(mach_task_self(), vm_addr, BAND_SIZE);
}
return FALSE;
}
#endif
nanozone->band_max_mapped_baseaddr[mag_index] = vm_addr;
}

// Randomize the starting allocation from this slot (introduces 11 to 14 bits of entropy)
if (0 == pMeta->slot_objects_mapped) { // First encounter?
pMeta->slot_objects_skipped = (malloc_entropy[1] % (SLOT_IN_BAND_SIZE / slot_bytes));
pMeta->slot_bump_addr = p + (pMeta->slot_objects_skipped * slot_bytes);
} else {
pMeta->slot_bump_addr = p;
}

pMeta->slot_limit_addr = p + (SLOT_IN_BAND_SIZE / slot_bytes) * slot_bytes;
pMeta->slot_objects_mapped += (SLOT_IN_BAND_SIZE / slot_bytes);

u.fields.nano_signature = NANOZONE_SIGNATURE;
u.fields.nano_mag_index = mag_index;
u.fields.nano_band = 0;
u.fields.nano_slot = 0;
u.fields.nano_offset = 0;
s = u.addr; // Base for this core.

// Set the high water mark for this CPU's entire magazine, if this resupply raised it.
watermark = nanozone->core_mapped_size[mag_index];
hiwater = MAX(watermark, p - s + SLOT_IN_BAND_SIZE);
nanozone->core_mapped_size[mag_index] = hiwater;

return TRUE;
}
  • nano_blk_addr_t u 用来计算 slot_current_base_addr 的联合体

  • 利用 nano_blk_addr_t 来计算 slot_current_base_addr

  • 根据设置的属性计算 slot_current_base_addr

  • 如果最大能存储的地址 仍然小于目标地址,则小开辟新的band

  • mac 和模拟器 或重新使用

  • 重新申请新的 band,调用 mach_vm_map 从 pmap 转换。

当进入 segregated_band_grow 时,如果当前的 band 不够用,则使用 mach_vm_map 经由 pmap 重新映射物理内存到虚拟内存。

关于通过 nano_blk_addr_t 的联合体结构如下,其每个成员所占的 bit位数 已经写出。


struct nano_blk_addr_s {
uint64_t
nano_offset:NANO_OFFSET_BITS, //17 locates the block
nano_slot:NANO_SLOT_BITS, //4 bucket of homogenous quanta-multiple blocks
nano_band:NANO_BAND_BITS, //17
nano_mag_index:NANO_MAG_BITS, //6 the core that allocated this block
nano_signature:NANOZONE_SIGNATURE_BITS; // the address range devoted to us.
};

#endif
// clang-format on

typedef union {
uint64_t addr;
struct nano_blk_addr_s fields;
} nano_blk_addr_t;

下面通过 LLDB 分析



在 free 的阶段,也是使用如上的方式获取 对应的 slot,mag_index

下面来梳理下 nana_zone 分配过程:

  • 确定当前 cpu 对应的 mag 和通过 size参数 计算出来的 slot ,去对应 chained_block_s 的链表中取已经被释放过的内存区块缓存,如果取到检查指针地址是否有问题,没有问题就直接返回;
  • 初次进行 nano malloc时,nano zon并没有缓存,会直接在 nano zone范围的地址空间上直接分配连续地址内存;
  • 如当前 Band 中当前 Slot 耗尽则向系统申请新的 Band(每个 Band固定大小 2M,容纳了16个128k的槽),连续地址分配内存的基地址、limit地址以及当前分配到的地址由 meta data 结构维护起来,而这些 meta data 则以 MagSlot 为维度(Mag个数是处理器个数,Slot是16个)的二维数组形式,放在 nanozone_t 的 meta_data字段中。
    流程如下


2.11 scalable zone(helper_zone) 分析

在 szone 上分配的内存包括 tiny、small和large 三大类,其中 tiny 和 small 的分配、释放过程大致相同,larg类型有自己的方式管理。同样会通过create_scalable_zone来构造zone。 这里不在复述create_scalable_zone`,直接看内存的分配策略

2.12 szone_malloc_should_clear 分析


MALLOC_NOINLINE void *
szone_malloc_should_clear(szone_t *szone, size_t size, boolean_t cleared_requested)
{
void *ptr;
msize_t msize;
//64位 <= 1008B 32位<= 496B
if (size <= SMALL_THRESHOLD) {
// tiny size: <=1008 bytes (64-bit), <=496 bytes (32-bit)
// think tiny
msize = TINY_MSIZE_FOR_BYTES(size + TINY_QUANTUM - 1);
if (!msize) {
msize = 1;
}
ptr = tiny_malloc_should_clear(&szone->tiny_rack, msize, cleared_requested);
} else if (size <= szone->large_threshold) {
//64位 <= 128KB 32位 <= 128KB
// small size: <=15k (iOS), <=64k (large iOS), <=128k (macOS)
// think small
msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
if (!msize) {
msize = 1;
}
ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
} else {
// large: all other allocations
size_t num_kernel_pages = round_page_quanta(size) >> vm_page_quanta_shift;
if (num_kernel_pages == 0) { /* Overflowed */
ptr = 0;
} else {
ptr = large_malloc(szone, num_kernel_pages, 0, cleared_requested);
}
}
#if DEBUG_MALLOC
if (LOG(szone, ptr)) {
malloc_report(ASL_LEVEL_INFO, "szone_malloc returned %p\n", ptr);
}
#endif
/*
* If requested, scribble on allocated memory.
*/

if ((szone->debug_flags & MALLOC_DO_SCRIBBLE) && ptr && !cleared_requested && size) {
memset(ptr, SCRIBBLE_BYTE, szone_size(szone, ptr));
}
return ptr;
}

这里以看出在 szone 上分配的内存包括 tinysmall 和large 三大类,我们以 tiny为例 开始下面的分析

2.12 tiny_malloc_should_clear 分析

void *
tiny_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
void *ptr;
mag_index_t mag_index = tiny_mag_get_thread_index() % rack->num_magazines;
//获取magazine. magazines 是一个由64个magazine_t组成的数组
magazine_t *tiny_mag_ptr = &(rack->magazines[mag_index]);

MALLOC_TRACE(TRACE_tiny_malloc, (uintptr_t)rack, TINY_BYTES_FOR_MSIZE(msize), (uintptr_t)tiny_mag_ptr, cleared_requested);

#if DEBUG_MALLOC
if (DEPOT_MAGAZINE_INDEX == mag_index) {
malloc_zone_error(rack->debug_flags, true, "malloc called for magazine index -1\n");
return (NULL);
}

if (!msize) {
malloc_zone_error(rack->debug_flags, true, "invariant broken (!msize) in allocation (region)\n");
return (NULL);
}
#endif

SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);

#if CONFIG_TINY_CACHE
ptr = tiny_mag_ptr->mag_last_free;
//如果开启了tiny 的缓存。
if (tiny_mag_ptr->mag_last_free_msize == msize) {
// we have a winner
//优先查看上次最后释放的区块是否和此次请求的大小刚好相等(都是对齐之后的slot大小),如果是则直接返回。
tiny_mag_ptr->mag_last_free = NULL;
tiny_mag_ptr->mag_last_free_msize = 0;
tiny_mag_ptr->mag_last_free_rgn = NULL;
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
}
#if DEBUG_MALLOC
if (LOG(szone, ptr)) {
malloc_report(ASL_LEVEL_INFO, "in tiny_malloc_should_clear(), tiny cache ptr=%p, msize=%d\n", ptr, msize);
}
#endif
return ptr;
}
#endif /* CONFIG_TINY_CACHE */

while (1) {
//先从freelist 查找
ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
if (ptr) {
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
}
return ptr;
}
//从一个后备magazine中取出一个可用region,完整地拿过来放到当前magazine,再走一遍上面的步骤。
if (tiny_get_region_from_depot(rack, tiny_mag_ptr, mag_index, msize)) {
//再次尝试从freelist 中获取
ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
if (ptr) {
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
}
return ptr;
}
}

// The magazine is exhausted. A new region (heap) must be allocated to satisfy this call to malloc().
// The allocation, an mmap() system call, will be performed outside the magazine spin locks by the first
// thread that suffers the exhaustion. That thread sets "alloc_underway" and enters a critical section.
// Threads arriving here later are excluded from the critical section, yield the CPU, and then retry the
// allocation. After some time the magazine is resupplied, the original thread leaves with its allocation,
// and retry-ing threads succeed in the code just above.
if (!tiny_mag_ptr->alloc_underway) {
//如果没有正在申请新的的 regin 操作,则进行申请操作
void *fresh_region;

// time to create a new region (do this outside the magazine lock)
//设置当前正在申请新的 堆
tiny_mag_ptr->alloc_underway = TRUE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
//申请新的堆 1m
fresh_region = mvm_allocate_pages_securely(TINY_REGION_SIZE, TINY_BLOCKS_ALIGN, VM_MEMORY_MALLOC_TINY, rack->debug_flags);
SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);

// DTrace USDT Probe
MAGMALLOC_ALLOCREGION(TINY_SZONE_FROM_RACK(rack), (int)mag_index, fresh_region, TINY_REGION_SIZE);

if (!fresh_region) { // out of memory!
tiny_mag_ptr->alloc_underway = FALSE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
return NULL;
}
//从最近的一个 region 或者新申请的 region中malloc
ptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region);

// we don't clear because this freshly allocated space is pristine
tiny_mag_ptr->alloc_underway = FALSE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
return ptr;
} else {
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
yield();
SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);
}
}
/* NOTREACHED */
}

  • 获取 magazine.

  • magazines 是一个由 64个magazine_t 组成的数组

  • 如果开启了 tiny 的缓存

  • 优先查看上次最后释放的区块是否和此次请求的大小刚好相等(都是对齐之后的 slot大小),如果是则直接返回。

  • ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize); 先从 freelist 查找

  • 从一个后备 magazine 中取出一个可用 region,完整地拿过来放到当前 magazine,再走一遍上面的步骤。

  • void *fresh_region; 如果没有正在申请新的的 regin 操作,则进行申请操作

  • tiny_mag_ptr->alloc_underway = TRUE; 设置当前正在申请新的 堆

  • fresh_region = mvm_allocate_pages_securely(TINY_REGION_SIZE, TINY_BLOCKS_ALIGN, VM_MEMORY_MALLOC_TINY, rack->debug_flags); 申请新的堆 --- 1M

  • ptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region); 从最近的一个 region 或者新申请的 region 中 malloc

每次调用 free 函数,会直接把要释放的内存优先放到mag_last_free 指针上,在下次 alloc 时,也会优先检查mag_last_free 是否存在大小相等的内存,如果存在就直接返回。

2.14 tiny_malloc_from_free_list & tiny_get_region_from_depot 分析

  • tiny_malloc_from_free_list函数的作用是从 free_list 中不断进行各种策略尝试。

  • 从上面的流程可以看出,在查找已经释放的内存缓存,会采用2步缓存查找(策略1,2),及两步备用内存的开辟(策略3,4)。

  • 当 free_list 流程仍然找不到可以使用内存,就会使用tiny_get_region_from_depot

每一个类型的 rack 指向的 magazines ,都会在下标为-1 , magazine_t 当做备用:depot,该方法的作用是从备用的 depot查找出是否有满足条件的 region 如果存在,更新 depot 和 region 的关联关系,然后在关联当前的magazine_t 和 region。之后在再次重复 free_list 过程

2.15 mvm_allocate_pages_securely 的分析

  • 走到这一步,就需要申请新的 heap 了,这里需要理解虚拟内存和物理内存的映射关系。

  • 你其实只要记住两点:vm_map 代表就是一个进程运行时候涉及的虚拟内存,pmap 代表的就是和具体硬件架构相关的物理内存。

  • 重新申请的核心函数为 mach_vm_map ,其概念如图



2.16  tiny_malloc_from_region_no_lock 的分析

重新申请了新的内存 (region) 之后,挂载到当前的 magazine下并分配内存。

这个方法的主要作用是把新申请的内存地址,转换为region,并进行相关的关联。及更新对应的 magazine。整个 scalable_zone 的结构体关系,及流程如下



2.17 nano_zone 总结

malloc 库会检查指针地址,如果没有问题,则以链表的形式将这些区块按大小存储起来。这些链表的头部放在 meta_data数组 中对应的 [mag][slot]元素中。

其实从缓存获取空余内存和释放内存时都会对指向这篇内存区域的指针进行检查,如果有类似地址不对齐、未释放/多次释放、所属地址与预期的 mag、slot 不匹配等情况都会以报错结束。

2.18 scalable_zone 分析

  • 首先检查指针指向地址是否有问题。
    如果 last free指针 上没有挂载内存区块,则放到 last free上。

  • 如果有 last free ,置换内存,并把 last free 原有内存区块挂载到 free list上(在挂载的 free list前,会先根据 region 位图检查前后区块是否能合并成更大区块,如果能会合并成一个)。

  • 合并后所在的 region 如果空闲字节超过一定条件,则将把此 region 放到后备的 magazine 中(-1)。

  • 如果整个 region 都是空的,则直接还给系统内核。


三、流程总结





四、拓展补充

  • malloc_zone_t 提供了一个模板类,或者理解为malloc_zone_t 提供一类接口(高度抽象了alloc一个对象所需要的特征),freecalloc等。

  • 由所有拓展的结构体来实现真正的目标函数。

  • 同上对于上层 Objc,提供了抽象接口(依赖倒置),这样就降低了调用者 (Objc) 与实现模块间的耦合。



作者:Cooci
链接:https://www.jianshu.com/p/ff4e55c9c332






收起阅读 »

iOS —— malloc分析(1)

一、malloc_zone_t 分析这个家伙是一个非常重要的家伙,我们先来看看 malloc_zone_t 的结构typedef struct _malloc_zone_t { void *reserved1; /* RESER...
继续阅读 »

一、malloc_zone_t 分析

这个家伙是一个非常重要的家伙,我们先来看看 malloc_zone_t 的结构

typedef struct _malloc_zone_t {
void *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */
void *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */
size_t (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
void *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
void *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
void (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
void *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone);
const char *zone_name;

unsigned (* MALLOC_ZONE_FN_PTR(batch_malloc))(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested);

struct malloc_introspection_t * MALLOC_INTROSPECT_TBL_PTR(introspect);
unsigned version;

void *(* MALLOC_ZONE_FN_PTR(memalign))(struct _malloc_zone_t *zone, size_t alignment, size_t size);

void (* MALLOC_ZONE_FN_PTR(free_definite_size))(struct _malloc_zone_t *zone, void *ptr, size_t size);

size_t (* MALLOC_ZONE_FN_PTR(pressure_relief))(struct _malloc_zone_t *zone, size_t goal);

boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);

} malloc_zone_t;


malloc_zone_t 是一个非常基础结构,里面包含一堆函数指针,用来存储一堆相关的处理函数的具体实现的地址,例如mallocfreerealloc等函数的具体实现。后续会基于malloc_zone_t进行扩展。

二、calloc 的流程

2.1  calloc -> malloc_zone_calloc 的流程


void * calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
  • 这个 default_zone 其实是一个“假的”zone,同时它也是malloc_zone_t类型。它存在的目的就是要引导程序进入一个创建真正的 zone 的流程。
  • 下面来看一下 default_zone 的引导流程。

  • 2.2 default_zone 引导

    void * malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
    {
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
    internal_check();
    }

    ptr = zone->calloc(zone, num_items, size);

    if (malloc_logger) {
    malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
    (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    return ptr;
    }
  • ptr = zone->calloc(zone, num_items, size)
  • 此时传进来的 zone 的类型是 上面 calloc 传入的 defaultzone,所以 zone->calloc的调用实现要看defaultzone 的定义。

  • 2.3 defaultzone 的定义

    static virtual_default_zone_t virtual_default_zone
    __attribute__((section("__DATA,__v_zone")))
    __attribute__((aligned(PAGE_MAX_SIZE))) = {
    NULL,
    NULL,
    default_zone_size,
    default_zone_malloc,
    default_zone_calloc,
    default_zone_valloc,
    default_zone_free,
    default_zone_realloc,
    default_zone_destroy,
    DEFAULT_MALLOC_ZONE_STRING,
    default_zone_batch_malloc,
    default_zone_batch_free,
    &default_zone_introspect,
    10,
    default_zone_memalign,
    default_zone_free_definite_size,
    default_zone_pressure_relief,
    default_zone_malloc_claimed_address,
    };
    • 从上面的结构可以看出 defaultzone->calloc 实际的函数实现为 default_zone_calloc
    static void *
    default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
    {
    zone = runtime_default_zone();

    return zone->calloc(zone, num_items, size);
    }
    • 引导创建真正的 zone
    • 使用真正的 zone 进行 calloc

    2.4 zone分析

    在创建正在的 zone时,其实系统是有对应的一套创建策略的。在跟踪 runtime_default_zone 方法后,最终会进入如下调用



    static void
    _malloc_initialize(void *context __unused)
    {
    ...... - 省略多余代码
    //创建helper_zone,
    malloc_zone_t *helper_zone = create_scalable_zone(0, malloc_debug_flags);
    //创建 nano zone
    if (_malloc_engaged_nano == NANO_V2) {
    zone = nanov2_create_zone(helper_zone, malloc_debug_flags);
    } else if (_malloc_engaged_nano == NANO_V1) {
    zone = nano_create_zone(helper_zone, malloc_debug_flags);
    }
    //如果上面的if else if 成立,这进入 nonazone
    if (zone) {
    malloc_zone_register_while_locked(zone);
    malloc_zone_register_while_locked(helper_zone);

    // Must call malloc_set_zone_name() *after* helper and nano are hooked together.
    malloc_set_zone_name(zone, DEFAULT_MALLOC_ZONE_STRING);
    malloc_set_zone_name(helper_zone, MALLOC_HELPER_ZONE_STRING);
    } else {
    //使用helper_zone分配内存
    zone = helper_zone;
    malloc_zone_register_while_locked(zone);
    malloc_set_zone_name(zone, DEFAULT_MALLOC_ZONE_STRING);
    }
    //缓存default_zone
    initial_default_zone = zone;
    .....
    }
    • 创建 helper_zone
    • 创建 nano zone
    • 如果上面的 if else if 成立,这进入 nonazone
    • 使用 helper_zone 分配内存
    • 缓存 default_zone

    在这里 会存在两种 zone

      1. nanozone_t
      1. scalable_zone

    2.5 nanozone_t 分析


    typedef struct nano_meta_s {
    OSQueueHead slot_LIFO MALLOC_NANO_CACHE_ALIGN;
    unsigned int slot_madvised_log_page_count;
    volatile uintptr_t slot_current_base_addr;
    volatile uintptr_t slot_limit_addr;
    volatile size_t slot_objects_mapped;
    volatile size_t slot_objects_skipped;
    bitarray_t slot_madvised_pages;
    // position on cache line distinct from that of slot_LIFO
    volatile uintptr_t slot_bump_addr MALLOC_NANO_CACHE_ALIGN;
    volatile boolean_t slot_exhausted;
    unsigned int slot_bytes;
    unsigned int slot_objects;
    } *nano_meta_admin_t;

    // vm_allocate()'d, so page-aligned to begin with.
    typedef struct nanozone_s {
    // first page will be given read-only protection
    malloc_zone_t basic_zone;
    uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)];

    // remainder of structure is R/W (contains no function pointers)
    // page-aligned
    // max: NANO_MAG_SIZE cores x NANO_SLOT_SIZE slots for nano blocks {16 .. 256}
    //以Mag、Slot为维度,维护申请的band内存部分 slot 的范围为 1~16
    struct nano_meta_s meta_data[NANO_MAG_SIZE][NANO_SLOT_SIZE];//
    _malloc_lock_s band_resupply_lock[NANO_MAG_SIZE];
    uintptr_t band_max_mapped_baseaddr[NANO_MAG_SIZE];
    size_t core_mapped_size[NANO_MAG_SIZE];
    unsigned debug_flags;
    uintptr_t cookie;
    malloc_zone_t *helper_zone;
    } nanozone_t;
    • nanozone_t 同样是 malloc_zone_t 类型。在nano_create_zone 函数内部会完成对 calloc等函数的重新赋值。

    2.6  nano_create_zone 分析

    malloc_zone_t *
    nano_create_zone(malloc_zone_t *helper_zone, unsigned debug_flags)
    {
    nanozone_t *nanozone;
    int i, j;
    //构造nano zone
    /* Note: It is important that nano_create_zone resets _malloc_engaged_nano
    * if it is unable to enable the nanozone (and chooses not to abort). As
    * several functions rely on _malloc_engaged_nano to determine if they
    * should manipulate the nanozone, and these should not run if we failed
    * to create the zone.
    */

    // MALLOC_ASSERT(_malloc_engaged_nano == NANO_V1);

    /* get memory for the zone. */
    nanozone = nano_common_allocate_based_pages(NANOZONE_PAGED_SIZE, 0, 0, VM_MEMORY_MALLOC, 0);
    if (!nanozone) {
    _malloc_engaged_nano = NANO_NONE;
    return NULL;
    }
    //构造对zone 的一些函数进行重新赋值
    /* set up the basic_zone portion of the nanozone structure */
    nanozone->basic_zone.version = 10;
    nanozone->basic_zone.size = (void *)nano_size;
    nanozone->basic_zone.malloc = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_malloc_scribble : (void *)nano_malloc;
    nanozone->basic_zone.calloc = (void *)nano_calloc;
    nanozone->basic_zone.valloc = (void *)nano_valloc;
    nanozone->basic_zone.free = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_free_scribble : (void *)nano_free;
    nanozone->basic_zone.realloc = (void *)nano_realloc;
    nanozone->basic_zone.destroy = (void *)nano_destroy;
    nanozone->basic_zone.batch_malloc = (void *)nano_batch_malloc;
    nanozone->basic_zone.batch_free = (void *)nano_batch_free;
    nanozone->basic_zone.introspect = (struct malloc_introspection_t *)&nano_introspect;
    nanozone->basic_zone.memalign = (void *)nano_memalign;
    nanozone->basic_zone.free_definite_size = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_free_definite_size_scribble
    : (void *)nano_free_definite_size;

    nanozone->basic_zone.pressure_relief = (void *)nano_pressure_relief;
    nanozone->basic_zone.claimed_address = (void *)nano_claimed_address;

    nanozone->basic_zone.reserved1 = 0; /* Set to zero once and for all as required by CFAllocator. */
    nanozone->basic_zone.reserved2 = 0; /* Set to zero once and for all as required by CFAllocator. */

    mprotect(nanozone, sizeof(nanozone->basic_zone), PROT_READ); /* Prevent overwriting the function pointers in basic_zone. */

    /* Nano zone does not support MALLOC_ADD_GUARD_PAGES. */
    if (debug_flags & MALLOC_ADD_GUARD_PAGES) {
    malloc_report(ASL_LEVEL_INFO, "nano zone does not support guard pages\n");
    debug_flags &= ~MALLOC_ADD_GUARD_PAGES;
    }

    /* set up the remainder of the nanozone structure */
    nanozone->debug_flags = debug_flags;

    if (phys_ncpus > sizeof(nanozone->core_mapped_size) /
    sizeof(nanozone->core_mapped_size[0])) {
    MALLOC_REPORT_FATAL_ERROR(phys_ncpus,
    "nanozone abandoned because NCPUS > max magazines.\n");
    }

    /* Initialize slot queue heads and resupply locks. */
    OSQueueHead q0 = OS_ATOMIC_QUEUE_INIT;
    for (i = 0; i < nano_common_max_magazines; ++i) {
    _malloc_lock_init(&nanozone->band_resupply_lock[i]);

    for (j = 0; j < NANO_SLOT_SIZE; ++j) {
    nanozone->meta_data[i][j].slot_LIFO = q0;
    }
    }

    /* Initialize the security token. */
    nanozone->cookie = (uintptr_t)malloc_entropy[0] & 0x0000ffffffff0000ULL; // scramble central 32bits with this cookie

    nanozone->helper_zone = helper_zone;

    return (malloc_zone_t *)nanozone;
    }
    • 构造 nano zone
    • 构造对 zone 的一些函数进行重新赋值
    • Nano zone 不支持 MALLOC_ADD_GUARD_PAGES
    • 建立其余的 nanozone 结构
    • 初始化插槽队列头并重新供应锁
    • 初始化安全令牌。

    2.7 nano_calloc 分析

    过程参考 defaultzone 。回到上面 default_zone_calloc 函数内。下一步就是使用 nanozone_t 调用 calloc

    下面是 nano_calloc 的实现

    static void *
    nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
    {
    size_t total_bytes;

    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
    return NULL;
    }
    // 如果要开辟的空间小于 NANO_MAX_SIZE 则进行nanozone_t的malloc。
    if (total_bytes <= NANO_MAX_SIZE) {
    void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
    if (p) {
    return p;
    } else {
    /* FALLTHROUGH to helper zone */
    }
    }
    //否则就进行helper_zone的流程
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
    }
    • 如果要开辟的空间小于 NANO_MAX_SIZE 则进行
    • 否则就进行 helper_zone 的流程

    2.8 _nano_malloc_check_clear分析

    这里我们也可以看出使用 nanozone_t 的限制为不超过256B。继续看 _nano_malloc_check_clear



    static void *
    _nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
    {
    MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

    void *ptr;
    size_t slot_key;
    // 获取16字节对齐之后的大小,slot_key非常关键,为slot_bytes/16的值,也是数组的二维下下标
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    //根据_os_cpu_number经过运算获取 mag_index(meta_data的一维索引)
    mag_index_t mag_index = nano_mag_index(nanozone);
    //确定当前cpu对应的mag和通过size参数计算出来的slot,去对应metadata的链表中取已经被释放过的内存区块缓存
    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
    //检测是否存在已经释放过,可以直接拿来用的内存,已经被释放的内存会缓存在 chained_block_s 链表
    //每一次free。同样会根据 index 和slot 的值回去 pMeta,然后把slot_LIFO的指针指向释放的内存。
    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
    if (ptr) {

    ...省略无关代码

    //如果缓存的内存存在,这进行指针地址检查等异常检测,最后返回
    //第一次调用malloc时,不会执行这一块代码。
    } else {
    //没有释放过的内存,所以调用函数 获取内存
    ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
    }

    if (cleared_requested && ptr) {
    memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
    }
    return ptr;
    }
    • 获取16字节对齐之后的大小, slot_key 非常关键,为slot_bytes/16 的值,也是数组的二维下下标

    • 根据 _os_cpu_number 经过运算获取 mag_index ( meta_data 的一维索引)

    • 确定当前 cpu 对应的 mag 和通过 size 参数计算出来的 slot,去对应 metadata 的链表中取已经被释放过的内存区块缓存

    • 检测是否存在已经释放过,可以直接拿来用的内存,已经被释放的内存会缓存在chained_block_s链表

    • 每一次 free。同样会根据  index 和 slot 的值回去 pMeta,然后把 slot_LIFO 的指针指向释放的内存。

    • 如果缓存的内存存在,这进行指针地址检查等异常检测,最后返回

    • 没有释放过的内存,所以调用函数 获取内存

    该方法主要是通过 cpu 与 slot 确定 index,从chained_block_s 链表中找出是否存在已经释放过的缓存。如果存在则进行指针检查之后返回,否则进入查询 meta data 或者开辟 band

    2.9 segregated_next_block 分析


    static MALLOC_INLINE void *
    segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
    {
    while (1) {
    //当前这块pMeta可用内存的结束地址
    uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
    //原子的为pMeta->slot_bump_addr添加slot_bytes的长度,偏移到下一个地址
    uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
    //减去添加的偏移量,获取当前可以获取的地址
    b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.

    if (b < theLimit) { // Did we stay within the bound of the present slot allocation?
    //如果地址还在范围之内,则返回地址
    return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
    } else {
    //已经用尽了
    if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
    pMeta->slot_bump_addr = theLimit;
    return 0; // We're toast
    } else {
    // One thread will grow the heap, others will see its been grown and retry allocation
    _malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
    // re-check state now that we've taken the lock
    //多线程的缘故,重新检查是否用尽
    if (pMeta->slot_exhausted) {
    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
    return 0; // Toast
    } else if (b < pMeta->slot_limit_addr) {
    //如果小于最大限制地址,当重新申请一个新的band后,重新尝试while
    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
    continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
    } else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
    //申请新的band成功,重新尝试while
    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
    continue; // ... the slot has been successfully grown by us. Now try again.
    } else {
    pMeta->slot_exhausted = TRUE;
    pMeta->slot_bump_addr = theLimit;
    _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
    return 0;
    }
    }
    }
    }
    }
    • 当前这块 pMeta 可用内存的结束地址

    • 原子的为 pMeta->slot_bump_addr 添加 slot_bytes 的长度,偏移到下一个地址

    • b -= slot_bytes 减去添加的偏移量,获取当前可以获取的地址

    • 如果地址还在范围之内,则返回地址 return (void *)b

    • pMeta->slot_exhausted 多线程的缘故,重新检查是否用尽

    • _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]); 如果小于最大限制地址,当重新申请一个新的 band 后,重新尝试 while

    • _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);申请新的 band成功,重新尝试 while

    如果是第一次调用 segregated_next_block 函数,band 不存在,缓存也不会存在,所以会调用segregated_band_grow。来开辟新的 band




    收起阅读 »

    OpenGL绘制正方形并让其移动

    一.main函数里面的基本设置 在我们集成OpenGl之后,我们开始绘制正方形,首先我们倒入我们需要的东西:并且定义;main函数里面:     //设置当前工作目录,针对MAC OS X    /*...
    继续阅读 »

    一.main函数里面的基本设置

     在我们集成OpenGl之后,我们开始绘制正方形,首先我们倒入我们需要的东西:




    并且定义;



    main函数里面:

         //设置当前工作目录,针对MAC OS X

        /*

         `GLTools`函数`glSetWorkingDrectory`用来设置当前工作目录。实际上在Windows中是不必要的,因为工作目录默认就是与程序可执行执行程序相同的目录。但是在Mac OS X中,这个程序将当前工作文件夹改为应用程序捆绑包中的`/Resource`文件夹。`GLUT`的优先设定自动进行了这个中设置,但是这样中方法更加安全。

         */

        gltSetWorkingDirectory(argv[0]);

        //初始化GLUT库,这个函数只是传说命令参数并且初始化glut库

        glutInit(&argc, argv);

        /*

         初始化双缓冲窗口,其中标志GLUT_DOUBLE、GLUT_RGBA、GLUT_DEPTH、GLUT_STENCIL分别指

         双缓冲窗口、RGBA颜色模式、深度测试、模板缓冲区

      --GLUT_DOUBLE`:双缓存窗口,是指绘图命令实际上是离屏缓存区执行的,然后迅速转换成窗口视图,这种方式,经常用来生成动画效果;

         --GLUT_DEPTH`:标志将一个深度缓存区分配为显示的一部分,因此我们能够执行深度测试;

         --GLUT_STENCIL`:确保我们也会有一个可用的模板缓存区。

         深度、模板测试后面会细致讲到

         */

        glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);


        //GLUT窗口大小、窗口标题

        //glutInitWindowSize(600, 800);

        glutInitWindowSize(500, 500);

        glutCreateWindow("Triangle");


        /*

         GLUT 内部运行一个本地消息循环,拦截适当的消息。然后调用我们不同时间注册的回调函数。我们一共注册2个回调函数:

         1)为窗口改变大小而设置的一个回调函数

         2)包含OpenGL 渲染的回调函数

         */

        //注册重塑函数

        glutReshapeFunc(changeSize);

        //注册显示函数

        glutDisplayFunc(RenderScene);

        //注册特殊函数

       glutSpecialFunc(SpecialKeys);

    /*

         初始化一个GLEW库,确保OpenGL API对程序完全可用。

         在试图做任何渲染之前,要检查确定驱动程序的初始化过程中没有任何问题

         */

        GLenumstatus =glewInit();

        if(GLEW_OK!= status) {

            printf("GLEW Error:%s\n",glewGetErrorString(status));

            return1;

        }

        //设置我们的渲染环境

        setupRC();

        glutMainLoop();

    特别说明:

                        (1) //注册重塑函数glutReshapeFunc(changeSize)为初始化窗口大小,上一篇文章我们提到OpenGl没有窗口的支持,我们要创建一个窗口,这个函数会在窗口大小改变时,接收新的宽度&高度的时候重新调用



      (2)//注册显示函数glutDisplayFunc(RenderScene)为屏幕显示图形的时候调用,清除一个或者一组特定的缓存区,设置一组浮点数来表示红色(GLfloatvRed[] = {1.0,0.0,0.0,1.0f};其中表示为(r,g,b,alpha),也可以理解为OC当中的colorWithRed:<#(CGFloat)#> green:<#(CGFloat)#> blue:<#(CGFloat)#> alpha:<#(CGFloat)#>),传递到存储着色器,提交着色器,因为在屏幕渲染的时候是双缓冲区,我们还要交换缓冲区进行渲染glutSwapBuffers()


    (3)注册特殊函数 glutSpecialFunc(SpecialKeys);在iphone上,我们事件的交互是通过用户点击的,在mac上,我们只通过键盘实现用户操作,这里注册的键位以达到交互效果,


      其中GLfloatstepSize =0.025f;表示移动的快慢,可以修改,超过1.0f,已经超过屏幕界限,在OpenGL中,坐标范围是(-1,1)

                            GLfloatblockX =vVerts[0]和GLfloatblockY =vVerts[10];表示记录正方形四个顶点的某一顶点的x和y初始位置,你可以选择任何一个顶点,在移动过程中,其他顶点都是相对于这个顶点在移动并且做超出窗口的判断,类似于贪吃蛇只能在该窗口下吃,不能超出界限,在我们更新好四个顶点的位置之后,提交复制顶点数组triangleBatch.CopyVertexData3f(vVerts);重新绘制glutPostRedisplay();,这个时候就会调用 void RenderScene(void),然后在这里大家肯定很好奇为什么数组下标是01,34,67,9 10以及为什么是blockSize * 2,下面我们继续讲解

                        我们在定义顶点数组坐标:



    对应的数组下标是0,1,2,3,4,5,6,7,8,9,10,11,对应的是{x,y,z}表示一个顶点,我们移动的时候是在只是x,y轴上的坐标发生变化,而z轴上的值不会变化,所以我们的数组下标2,5,8,11不会变化,所以你看到的变换数组下标是01,34,67,9 10


    在OpenGL中,我们定义的四个顶位置如上图,以逆时针绘制,当然你也可以改为顺时针绘制,我们在设置边长的时候blockSize是相对原点坐标(0,0,0)设置的

       特别说明一下,我这里选择的相对点是A点的X和D点的Y,(你也可以选择A点的X,Y进行平移相对点的设置)

     当往上移动的时候:

                  if(key ==GLUT_KEY_UP) {,

                    blockY += stepSize;

        }


    变化的是Y轴上的值,因为我们是以D点Y坐标为相对移动的点,所以D点的Y新坐标为vVerts[10] = blockY;D点的x不变vVerts[9] = blockX;,A点的X坐标不变vVerts[0] = blockX;那此时A点的Y坐标是不是D点的新Y坐标的值减去blockSize*2(宽度*2),就是vVerts[1] = blockY -blockSize*2;是不是一下子明朗了。。。。。。。。BC点的XY坐标以此类推就会明白为什么是blockSize*2








    收起阅读 »

    iOS Block浅谈

    一.Block的本质 block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。 Block底层结构图查看Block源码: struct __block_impl { void*isa; int...
    继续阅读 »

    一.Block的本质


        block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。

    Block底层结构图
    查看Block源码:

    struct __block_impl {


    void*isa;

    int Flags;

    int Reserved;

    void *FuncPtr;

    };


    struct __main_block_impl_0 {


    struct __block_impl impl;


    struct__main_block_desc_0* Desc;


    // 构造函数(类似于OC的init方法),返回结构体对象


    main_block_impl_0(void*fp,structmain_block_desc_0 *desc,intflags=0) {


    impl.isa = &_NSConcreteStackBlock;

    impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;

    }


    };


    // 封装了block执行逻辑的函数


    static void main_block_func_0(struct main_block_impl_0 *__cself) {


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);

    }

    static struct __main_block_desc_0 {


    size_treserved;


    size_tBlock_size;


    } main_block_desc_0_DATA = { 0, sizeof(struct main_block_impl_0)};


    int main(intargc,constchar* argv[]) {


    /* @autoreleasepool */{__AtAutoreleasePool__autoreleasepool;

    // 定义block变量

    void(*block)(void) = &__main_block_impl_0(

    __main_block_func_0,

    &__main_block_desc_0_DATA

    );

    // 执行block内部的代码

    block->FuncPtr(block);

    }

    return0;

    }


    说明:FuncPtr:指向调用函数的地址,__main_block_desc_0 :block描述信息,Block_size:block的大小


    二.Block变量的捕获


    2.1局部变量的捕获


        对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

    int age=10;


    void(^Block)(void)=^{


    NSLog(@”age:%d”,age);


    };


    age=20;


    Block();


    2.2__block 修饰的外部变量


        对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值

    __block int age=10;


    myBlock block=^{


    NSLog(@”age = %d”,age);


    };


    age=18;


    block();


    输出:18;


    auto int age=10;


    static int num=25;


    void(^Block)(void)=^{


    NSLog(@”age:%d,num:%d”,age,num);


    };


    age=20;


    num=11;


    Block();


        输出结果为:age:10,num:11,auto变量block访问方式是值传递,也就是当block定义的时候,值已经传到block里面了,static变量block访问方式是指针传递,auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可,block不需要对全局变量捕获,都是直接采用取值的,局部变量的捕获是因为考虑作用域的问题,需要跨函数访问,就需要捕获,当出了作用域,局部变量已经被销毁,这时候如果block访问,就会出问题。

    2.2.block变量捕获机制


    block变量捕获机制
    block里访问self,self是当调用block函数的参数,参数是局部变量,self指向调用者,所以它也会捕获self,block里访问成员,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量。

    3.3Block的类型


        block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

    NSGlobalBlock ( _NSConcreteGlobalBlock )全局block即数据区


    NSStackBlock ( _NSConcreteStackBlock )堆区block


    NSMallocBlock ( _NSConcreteMallocBlock )栈区block


        说明:堆区,程序员自己控制,程序员自己管理,栈区,系统自动控制,一般我们使用最多的是堆区Block,判断类型的根据是没有访问auto变量的block是__NSGlobalBlock __ ,放在数据段访问了auto变量的block是__NSStackBlock __;[__NSStackBlock __ copy]操作就变成了__NSMallocBlock __,__NSGlobalBlock __ 调用copy操作后,什么也不做__NSStackBlock __ 调用copy操作后,复制效果是:从栈复制到堆;副本存储位置是堆__NSMallocBlock __ 调用copy操作后,复制效果是:引用计数增加;副本存储位置是堆,在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况是:

    1.block作为函数返回值时

    2.将block赋值给__strong指针时

    3.block作为Cocoa API中方法名含有usingBlock的方法参数时

    4.block作为GCD API的方法参数时

    三.对象类型的auto变量


    typedefvoid(^XBTBlock)(void);


    XBTBlock block;


    {


    Person*p=[[Person alloc]init];


    p.age=10;


    block=^{


    NSLog(@”======= %d”,p.age);


    };}


    Person.m


    -(void)dealloc{


    NSLog(@”Person - dealloc”);


    }


        说明:block为堆block,block里面有一个Person指针,Person指针指向Person对象。只要block还在,Person就还在。block强引用了Person对象。在MRC下,就会打印,因为堆空间的block会对Person对象retain操作,拥有一次Person对象。无论MRC还是ARC,栈空间上的block,不会持有对象;堆空间的block,会持有对象。

    特别说明:block内部访问了对象类型的auto变量时,是否会强引用?


    栈block


    a) 如果block是在栈上,将不会对auto变量产生强引用


    b) 栈上的block随时会被销毁,也没必要去强引用其他对象


    堆block


    1.如果block被拷贝到堆上:


    a) 会调用block内部的copy函数


    b) copy函数内部会调用_Block_object_assign函数


    c) _Block_object_assign函数会根据auto变量的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用


    2.如果block从堆上移除


    a) 会调用block内部的dispose函数


    b) dispose函数内部会调用_Block_object_dispose函数


    c) _Block_object_dispose函数会自动释放引用的auto变量(release)


    正确答案:


    如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象


    如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用


    3.2gcd的block中引用 Person对象什么时候销毁?


    eg:-(void)touchesBegan:(NSSet )toucheswithEvent:(UIEvent)event{


    Person*person = [[Personalloc]init];

    person.age=10;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    NSLog(@"age:%d",person.age);

    });

    NSLog(@"touchesBegan");

    }


    输出:touchesBegan


            age:10

    Person-dealloc

    说明:gcd的block默认会做copy操作,即dispatch_after的block是堆block,block会对Person强引用,block销毁时候Person才会被释放,如果上诉Person用__weak。即添加代码为__weak Person*weakPerson=person;,在Block中变成NSLog(@"age:%p",weakPerson);,它就不输出age,使用__weak修饰过后的对象,堆block会采用弱引用,无法延时Person的寿命,所以在touchesBegan函数结束后,Person就会被释放,gcd就无法捕捉到Person,gcd内部只要有强引用Person,Person就会等待执行完再销毁!如果gcd内部先强引用后弱引用,Person会等待强引用执行完毕后释放,只要强引用执行完,就不会等待后执行的弱引用,会直接释放的

    eg:-(void)touchesBegan:(NSSet )toucheswithEvent:(UIEvent)event{


    Person*person = [[Personalloc]init];

    person.age=10;

    __weakPerson*weakPerson = person;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),

    dispatch_get_main_queue(), ^{

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    NSLog(@"2-----age:%p",weakPerson);

    });

    NSLog(@"1-----age:%p",person);

    });

    NSLog(@"touchesBegan");

    }


    四.Block的修饰符


        block在修改NSMutableArray,不需要加__block,auto修饰变量,block无法修改,因为block使用的时候是内部创建了变量来保存外部的变量的值,block只有修改内部自己变量的权限,无法修改外部变量的权限。

    static修饰变量,block可以修改,因为block把外部static修饰变量的指针存入,block直接修改指针指向变量值,即可修改外部变量值。全局变量值,全局变量无论哪里都可以修改,当然block内部也可以修改。

    eg:block int age = 10,系统做了哪些—-》编译器会将block变量包装成一个对象


    __block 修饰符作用:


        __block可以用于解决block内部无法修改auto变量值的问题

    __block不能修饰全局变量、静态变量(static)

    编译器会将__block变量包装成一个对象

    __block修改变量:age->__forwarding->age

    __Block_byref_age_0结构体内部地址和外部变量age是同一地址

    __block的内存管理---->当block在栈上时,并不会对__block变量产生强引用

    block的属性修饰词为什么是copy?


        block一旦没有进行copy操作,就不会在堆上

    block在堆上,程序员就可以对block做内存管理等操作,可以控制block的生命周期,会调用block内部的copy函数

    copy函数内部会调用_Block_object_assign函数

    _Block_object_assign函数会对__block变量形成强引用(retain)

    对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用,当block从堆中移除时,会调用block内部的dispose函数dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的__block变量(release),当block在栈上时,对它们都不会产生强引用,当block拷贝到堆上时,都会通过copy函数来处理它们,对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用

    block的forwarding指针说明:


        栈上__block的__forwarding指向本身

    栈上__block复制到堆上后,栈上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身

    五. block循环引用


        1.ARC下如何解决block循环引用的问题?

    三种方式:__weak、__unsafe_unretained、__block

    1)第一种方式:__weak

    Person*person=[[Person alloc]init];

    // __weak Person *weakPerson = person;

    __weaktypeof(person)weakPerson=person;

    person.block=^{

    NSLog(@"age is %d",weakPerson.age);

    };

    2)第二种方式:__unsafe_unretained

    __unsafe_unretained Person*person=[[Person alloc]init];

    person.block=^{

    NSLog(@"age is %d",weakPerson.age);

    };

    3)第三种方式:__block

    __block Person*person=[[Person alloc]init];

    person.block=^{

    NSLog(@"age is %d",person.age);

    person=nil;

    };

    person.block();

    三种方法比较:weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil,unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,__block:必须把引用对象置位nil,并且要调用该block


    作者:枫紫_6174
    链接:https://www.jianshu.com/p/4bde3936b154


    收起阅读 »

    OutOfMemoryError 可以被 try catch 吗?

    目录 OutOfMemoryError 可以被 try catch 吗? 捕获 OutOfMemoryError 有什么意义? JVM 中哪一块内存不会发生 OOM ? OutOfMemoryError 可以被 try catch 吗? 群里小伙伴碰到的一...
    继续阅读 »

    目录



    • OutOfMemoryError 可以被 try catch 吗?

    • 捕获 OutOfMemoryError 有什么意义?

    • JVM 中哪一块内存不会发生 OOM ?


    OutOfMemoryError 可以被 try catch 吗?


    群里小伙伴碰到的一道比较经典的面试题,但我相信很多第一次碰到这个问题的同学应该无法立刻给出答案,最好的办法肯定还是动手测一测。


    注意看下面的 Gif,每点击一次 Allocate 20MB ,都会给数组容量增加 20*1024*1024,当然应该并不是 20 MB。如下面代码所示:


    binding.allocate.setOnClickListener {
    try {
    bytes = ByteArray(bytes.size + 1024 * 1024 * 20)
    refreshMemory()
    } catch (e: OutOfMemoryError) {
    binding.oomError.text = "Catch OOM : \n ${e.message}"
    }
    }
    复制代码


    当点击第 7 次时,发生了 OutOfMemoryError ,并且 catch 代码块执行了。



    Catch OOM : Failed to allocate a 146801680 byte allocation with 25165824 free bytes and 133MB until OOM, target footprint 153948888, growth limit 268435456



    所以,OutOfMemoryError 是可以 try catch 的。


    顺道画了一个思维导图回顾一下 Java 的异常体系。



    上面的图片没有罗列出所有的异常类型,但也基本概括了 Java 异常的继承体系。所有的异常类都继承自 ThrowableThrowable 有两个直接子类 ErrorException


    Exception 一般指可以/应该捕获和处理的异常。它的两个直接子类 IOExceptionRuntimeException 及其子类都是我们在代码中经常遇到的一些错误。RuntimeException 是在程序运行中可能发生的异常,我们可以不捕获它,但可能带来 Crash 的代价,但是过多的捕获异常又不利于暴露和调试异常情况。在开发过程中,我们更多的应该及时暴露问题。除了 RuntimeException 以外,其他异常可以统称为 非运行时异常 或者 受检异常,这些异常必须被捕获,否则编译期就会报错。


    Error 一般指非正常状态的,比较严重的,不应该被捕获的系统错误。


    再回头看看 OutOfMemoryError 的父类们,



    OutOfMemoryError <- VirtualMachineError <- Error



    OutOfMemoryError 是一个 Error ,Error 不应该被捕获。那么,捕获 OutOfMemoryError 有什么意义呢?


    捕获 OutOfMemoryError 有什么意义?


    一般情况下并没有什么太大意义,相信你在开发中也几乎没有写过 catch OOM 的代码。


    如果你把捕获 OOM 当做处理 OOM 的一种手段,无疑是不合适的。你无法保证你 catch 的代码就是导致 OOM 的原因,可能它只是压死骆驼的最后一根稻草,甚至你也无法保证你的 catch 代码块中不会再次触发 OOM 。


    我也从来没有写过捕获 OOM 的代码,但无意中在 Android 源码中发现了这样的操作。在 View.javabuildDrawingCacheImpl() 方法中有这么一段代码:


    try {
    bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
    width, height, quality);
    bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
    if (autoScale) {
    mDrawingCache = bitmap;
    } else {
    mUnscaledDrawingCache = bitmap;
    }
    if (opaque && use32BitCache) bitmap.setHasAlpha(false);
    } catch (OutOfMemoryError e) {
    // If there is not enough memory to create the bitmap cache, just
    // ignore the issue as bitmap caches are not required to draw the
    // view hierarchy
    if (autoScale) {
    mDrawingCache = null;
    } else {
    mUnscaledDrawingCache = null;
    }
    mCachingFailed = true;
    ......
    复制代码

    buildDrawingCacheImpl() 方法的大致作用是为当前 View 生成一个 Bitmap 缓存。在构建 Bitmap 对象的时候,如果捕捉到了 OOM ,就放弃生成 Bitmap 缓存,因为在 View 的绘制过程中 Bitmap Cache 并不是必须存在的。所以在这里没有必要抛出 OOM ,而是自己捕获就可以了。


    在你自己明确知道可能发生 OOM 的情况下设置一个兜底策略,这可能是捕获 OOM 的唯一意义了。如果你有其他奇淫技巧,欢迎在评论区补充。


    JVM 中哪一块内存不会发生 OOM ?


    最后补充一道我曾经遇到过的面试题,JVM 中哪一块内存不会发生 OOM ?


    当时面试的时候一下没反应过来,回来之后翻了翻 《深入理解Java虚拟机》 。但凡是 JVM 的相关问题,基本上都可以在这本书上找到答案。以下内容均总结摘抄自这本书,也可以查看我的相关读书笔记:第2章:Java内存区域与内存移溢出异常


    Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,如下图所示:



    Java 虚拟机栈 。每个方法被执行的时候,Java 虚拟机栈都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。


    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。 如果 Java 虚拟机栈支持动态扩展,当栈扩展时无法申请到足够的内存会排抛出 OutOfMemoryError 异常。


    本地方法栈。为虚拟机使用到的 Native 方法服务。《Java 虚拟机规范》对本地方法栈中方法使用的语言、使用方式和数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它。Hotspot 将本地方法栈和虚拟机栈合二为一。


    本地方法栈也会在栈深度溢出和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 。


    Java 堆。所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里 “几乎” 所有的对象实例都在这里分配内存。在 《Java 虚拟机规范》中对 Java 堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。


    Java 堆以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。


    Java 堆既可以被实现成固定大小,也可以是扩展的。如果在 Java 堆中没有内存完成实例分配,并且堆无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 。


    方法区。方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。


    虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做“非堆”,目的是与 Java 堆分开来。


    Hotspot 设计之初选择把垃圾收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,使得 HotSpot 的 GC 能够像管理 Java 堆一样管理这部分内存,但导致 Java 应用更容易遇到内存溢出的问题。在 JDK 8 中,彻底废弃了永久代的概念。


    如果方法区无法满足新的内存分配的需求时,将抛出 OutOfMemoryError 。


    运行时常量池。方法区的一部分。Class 文件的常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后方法方法去的运行时常量池。


    运行时常量池具有动态性,运行期间也可以将新的常量放入池中,如 String.intern() 。


    常量池受到方法区的限制,当无法再申请到内存时,会抛出 OutOfMemoryError 。


    唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域是 程序计数器。程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。


    最后


    这是专栏第五篇文章了,写作素材大多来自于身边的小伙伴。我也维护了一份 面试题文档,但考虑到共享文档比较容易造成混乱,后面也可能通过其他方式进行分享。


    作者:秉心说TM
    链接:https://juejin.cn/post/6874916707543187463
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    那你讲一下LeakCanary的原理

    今天来写一波内存泄漏工具LeakCanary的分析,也整理一下之前的笔记,废话不多说,源码整起来。 我用的1.5.1版本。 LeakCanary核心源码解析 看源码还是那句话,先找入口,顺着入口看主流程。 LeakCanary监听Activity的onDes...
    继续阅读 »

    今天来写一波内存泄漏工具LeakCanary的分析,也整理一下之前的笔记,废话不多说,源码整起来。


    我用的1.5.1版本。


    LeakCanary核心源码解析


    看源码还是那句话,先找入口,顺着入口看主流程



    LeakCanary监听Activity的onDestory方法,然后介入,所以从这里开始



    发现入口在RefWatcher.watch方法里,这个RefWatcher是核心类,跟进去watch方法。



    可以看出这里开始搞事情了,先解释一下这些变量的意义



    这样就可以解释,上图watch方法里面的操作了


    使用randomUUID生成一个随机数key,并加到了一个set中,这个key跟当前监控的activity是一一对应的,这个很关键,watchedReference就是当前关闭的activity对象,然后包装成KeyedWeakReference对象,KeyedWeakReference继承了WeakReference。



    后续就是就操作这个KeyedWeakReference这个弱引用。


    到了这里就不得不提一下,强软弱虚这四个东西,如果还有不懂的小伙伴,还是要去看一下的,这里只提一下弱引用,简单来说就是。


    弱引用持有的对象被回收了,那么弱引用就会放到与之关联的引用队列中。


    ???


    讲人话!那举个栗子


    就是上面watch方法的watchedReference(就是那个被监测的activity)被KeyedWeakReference弱引用持有了,当activity被回收了,RefWatcher#queue里面就会有这个KeyedWeakReference。


    反之如果一顿操作之后queue都没有这个KeyedWeakReference,说明activity没有被回收,那么就判定为内存泄漏。


    这也是LeakCanary的核心思想。



    好,接着watch方法往下走,ensureGoneAsync方法跟进去到一个ensureGone的核心方法里



    红框这里是核心中的核心。看起来很短,但浓缩才是精华。



    removeWeaklyReachableReferences方法里就是遍历把引用队列queue里的对象,在retainsKeys这个set里面移除掉。


    讲了半天,还记得开头这个retainsKeys放的什么吗?是那个UUID生成的与每个activity一一对应的key,可以理解为activity对应的一个值。当队列有一个弱引用了,说明有一个对应activity被回收了。


    gone方法里判断retainsKeys集合里还有没有这个activity对应的值,没有说明正常回收了。


    ensureGone的整个流程基本就清楚了,理一下ensureGone的流程。


    首先根据队列的对象,移除对应set里对应的key值,gone判断是否移除成功,成功返回DONE,没有泄露结束流程,gone判断还存在,原谅ta再给一次机会,调用runGc触发回收,再次移除key值,gone判断还存在,不好意思,没有机会了,使用heapDumper.dumpHeap出调用链,showNotification展示到通知栏,最后展示到DisplayLeakActivity页面上。


    最后还是弱引用基础知识的应用,所以说为什么大厂喜欢问基础知识,其实很多东西都是构建在基础知识之上。


    关于gc的补充点


    这里补充一点东西,也是之前被某厂问到了,当时没答出来的,主要是之前没看那么细,后面又翻了一下源码。



    当时问的是上面gc的时候,是怎么gc的?第二次removeWeaklyReachableReferences是什么时候触发的?


    当时想的是gc不都是System.gc嘛,还能有什么骚操作?回来打开一下源码,咦,发现还真有。。


    那从GCTrigger入手了。



    发现runGc里面不是直接调用System.gc的,用了一个Runtime.getRuntime().gc(),这是啥?



    然后看见上面一堆注释还贴了源码url,觉得事情很重要,静下心来用我多年修炼来的四级的英语阅读能力扫了一遍。


    简单来说就是,作者从AOSP那里Ctrl+c来了一段代码,因为System.gc()不能保证每次gc而Runtime.gc()会相对更可靠。


    然后调用了enqueuReferences方法,里面就直接sleep了100ms,简单粗暴。。作者对于此的解释是没有很好能够获取到对象真正加入到队列的时机,所以直接等100ms让回收,100ms后执行runFinalization,然后就可以去第二次remove。


    好家伙一个类就解答了两个问题,核心流程写完了,这次先写到这里,第一次用掘金,不知道掘金文章支不支持后续编辑,后续再补充一波


    谢谢,朋友们


    作者:海鸟跟鱼
    链接:https://juejin.cn/post/6927165351692238861
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    【android每日一问】怎么检测UI卡顿?(线上及线下)

    什么是UI卡顿? 在Android系统中,我们知道UI线程负责我们所有视图的布局,渲染工作,UI在更新期间,如果UI线程的执行时间超过16ms,则会产生丢帧的现象,而大量的丢帧就会造成卡顿,影响用户体验。 UI卡顿产生的原因? 在UI线程中做了大量的耗时操作...
    继续阅读 »

    什么是UI卡顿?


    在Android系统中,我们知道UI线程负责我们所有视图的布局,渲染工作,UI在更新期间,如果UI线程的执行时间超过16ms,则会产生丢帧的现象,而大量的丢帧就会造成卡顿,影响用户体验。


    UI卡顿产生的原因?



    • 在UI线程中做了大量的耗时操作,导致了UI刷新工作的阻塞。

    • 系统CPU资源紧张,APP所能分配的时间片减少。

    • Ardroid虚拟机频繁的执行GC操作,导致占用了大量的系统资源,同时也会导致UI线程的短暂停顿,从而产生卡顿。

    • 代码编写不当,产生了过度绘制,导致CPU执行时间变长,早场卡顿。


    从上可知,大部分的卡顿原因都产生于代码编写不当导致,而这类问题都可以通过各种优化方案进行优化,所以我们需要做的就是尽可能准确的找到卡顿的原因,定位到准确的代码模块,最好是能定位到哪个方法导致卡顿,这样我们APP的性能就能得到很大的提升。


    UI卡顿方案



    • 开发阶段


    在开发阶段我们可以借助开发工具为我们提供的各种便利来有效的识别卡顿,如下:


    System Trace


    具体使用可以看blog.csdn.net/u011578734/…
    写的文章。


    Android CPU Profiler



    • Android Studio CPU 性能剖析器可实时检查应用的 CPU 使用率和线程活动。你还可以检查方法跟踪记录、函数跟踪记录和系统跟踪记录中的详细信息。

    • 使用CPU profiler可以查看主线程中,每个方法的耗时情况,以及每个方法的调用栈,可以很方便的分析卡顿产生的原因,以及定位到具体的代码方法。


    具体使用方法可以参考
    blog.csdn.net/u011578734/…


    线上UI卡顿检测方案


    线上检测方案比较流行的是BlockCanary和WatchDog,下面我们就看看它们是怎么做到检测UI卡顿的并反馈给开发人员。


    BlockCanary



    • BlockCanary能检测到主线程的卡顿, 并将结果记录下来, 以友好的方式展示,很类似于LeakCanary的展示。


    BlockCanary的使用很简单,只要在Application中进行设置一下就可以如下:


    BlockCanary.install(this, new AppBlockCanaryContext()).start();
    复制代码


    • AppBlockCanaryContext继承自BlockCanaryContext是对BlockCanary中各个参数进行配置的类


    可配置参数如下:


    //卡顿阀值
    int getConfigBlockThreshold();
    boolean isNeedDisplay();
    String getQualifier();
    String getUid();
    String getNetworkType();
    Context getContext();
    String getLogPath();
    boolean zipLogFile(File[] src, File dest);
    //可将卡顿日志上传到自己的服务
    void uploadLogFile(File zippedFile);
    String getStackFoldPrefix();
    int getConfigDumpIntervalMillis();
    复制代码


    • 在某个消息执行时间超过设定的标准时会弹出通知进行提醒,或者上传。


    原理


    熟悉Android的Handler机制的同学一定知道,Handler中重要的组成部分,looper,并且应用的主线程只有一个Looper存在,不管有多少handler,最后都会回到这里。
    我们注意到Looper.loop()中有这么一段代码:


    public static void loop() {
    ...

    for (;;) {
    ...

    // This must be in a local variable, in case a UI event sets the logger
    Printer logging = me.mLogging;
    if (logging != null) {
    logging.println(">>>>> Dispatching to " + msg.target + " " +
    msg.callback + ": " + msg.what);
    }

    msg.target.dispatchMessage(msg);

    if (logging != null) {
    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }

    ...
    }
    }
    复制代码

    注意到两个很关键的地方是logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);这两行代码,它调用的时机正好在dispatchMessage(msg)的前后,而主线程卡也就是在dispatchMessage(msg)卡住了。


    BlockCanary的流程图


    (图片来自网络)


    blockcanary_flow.png


    BlockCanary就是通过替换系统的Printer来增加了一些我们想要的堆栈信息,从而满足我们的需求。


    替换原有的Printer是通过以下方法:


    Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
    复制代码

    并在mainLooperPrinter中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。如下所示:


    @Override
    public void println(String x) {
    if (!mStartedPrinting) {
    mStartTimeMillis = System.currentTimeMillis();
    mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
    mStartedPrinting = true;
    startDump();
    } else {
    final long endTime = System.currentTimeMillis();
    mStartedPrinting = false;
    if (isBlock(endTime)) {
    notifyBlockEvent(endTime);
    }
    stopDump();
    }
    }

    private boolean isBlock(long endTime) {
    return endTime - mStartTimeMillis > mBlockThresholdMillis;
    }
    复制代码


    • BlockCanary dump的信息包括如下:


    基本信息:安装包标示、机型、api等级、uid、CPU内核数、进程名、内存、版本号等
    耗时信息:实际耗时、主线程时钟耗时、卡顿开始时间和结束时间
    CPU信息:时间段内CPU是否忙,时间段内的系统CPU/应用CPU占比,I/O占CPU使用率
    堆栈信息:发生卡慢前的最近堆栈,可以用来帮助定位卡慢发生的地方和重现路径
    复制代码


    • 获取系统状态信息是通过如下代码实现:


    threadStackSampler = new ThreadStackSampler(Looper.getMainLooper().getThread(),
    sBlockCanaryContext.getConfigDumpIntervalMillis());
    cpuSampler = new CpuSampler(sBlockCanaryContext.getConfigDumpIntervalMillis());
    复制代码

    下面看一下ThreadStackSampler是怎么工作的?


    protected void doSample() {
    // Log.d("BlockCanary", "sample thread stack: [" + mThreadStackEntries.size() + ", " + mMaxEntryCount + "]");
    StringBuilder stringBuilder = new StringBuilder();

    // Fetch thread stack info
    for (StackTraceElement stackTraceElement : mThread.getStackTrace()) {
    stringBuilder.append(stackTraceElement.toString())
    .append(Block.SEPARATOR);
    }
    // Eliminate obsolete entry
    synchronized (mThreadStackEntries) {
    if (mThreadStackEntries.size() == mMaxEntryCount && mMaxEntryCount > 0) {
    mThreadStackEntries.remove(mThreadStackEntries.keySet().iterator().next());
    }
    mThreadStackEntries.put(System.currentTimeMillis(), stringBuilder.toString());
    }
    }
    复制代码

    直接去拿主线程的栈信息, 每半秒去拿一次, 记录下来, 如果发生卡顿就显之显示出来
    拿CPU的信息较麻烦, 从/proc/stat下面拿实时的CPU状态, 再从/proc/" + mPid + "/stat中读取进程时间, 再计算各CPU时间占比和CPU的工作状态.


    基于系统WatchDog原理来实现



    • 启动一个卡顿检测线程,该线程定期的向UI线程发送一条延迟消息,执行一个标志位加1的操作,如果规定时间内,标志位没有变化,则表示产生了卡顿。如果发生了变化,则代表没有长时间卡顿,我们重新执行延迟消息即可。


    public class WatchDog {
    private final static String TAG = "budaye";
    //一个标志
    private static final int TICK_INIT_VALUE = 0;
    private volatile int mTick = TICK_INIT_VALUE;
    //任务执行间隔
    public final int DELAY_TIME = 4000;
    //UI线程Handler对象
    private Handler mHandler = new Handler(Looper.getMainLooper());
    //性能监控线程
    private HandlerThread mWatchDogThread = new HandlerThread("WatchDogThread");
    //性能监控线程Handler对象
    private Handler mWatchDogHandler;

    //定期执行的任务
    private Runnable mDogRunnable = new Runnable() {
    @Override
    public void run() {
    if (null == mHandler) {
    Log.e(TAG, "handler is null");
    return;
    }
    mHandler.post(new Runnable() {
    @Override
    public void run() {//UI线程中执行
    mTick++;
    }
    });
    try {
    //线程休眠时间为检测任务的时间间隔
    Thread.sleep(DELAY_TIME);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    //当mTick没有自增时,表示产生了卡顿,这时打印UI线程的堆栈
    if (TICK_INIT_VALUE == mTick) {
    StringBuilder sb = new StringBuilder();
    //打印堆栈信息
    StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
    for (StackTraceElement s : stackTrace) {
    sb.append(s.toString() + "\n");
    }
    Log.d(TAG, sb.toString());
    } else {
    mTick = TICK_INIT_VALUE;
    }
    mWatchDogHandler.postDelayed(mDogRunnable, DELAY_TIME);
    }
    };

    /**
    * 卡顿监控工作start方法
    */
    public void startWork(){
    mWatchDogThread.start();
    mWatchDogHandler = new Handler(mWatchDogThread.getLooper());
    mWatchDogHandler.postDelayed(mDogRunnable, DELAY_TIME);
    }
    }

    复制代码


    • 调用startWork即可开启卡顿检测。

    作者:不做android
    链接:https://juejin.cn/post/6953923470635827236
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Android逆向之https

    介绍HTTP协议发展史、状态码、方法,整理了几乎所有常见的头部,讲述TLS的单向认证流程,Android中HTTPS抓包方法、防抓包策略以及绕过防抓包策略思路。 HTTP协议 超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协...
    继续阅读 »

    介绍HTTP协议发展史、状态码、方法,整理了几乎所有常见的头部,讲述TLS的单向认证流程,Android中HTTPS抓包方法、防抓包策略以及绕过防抓包策略思路。



    HTTP协议


    超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用层最为广泛的一种网络协议。


    发展史















































    协议发展说明
    HTTP/0.91991年定稿最早的HTTP协议,没有作为正式标准只有GET命令;没有请求头、请求体、返回头;服务器只能读取HTML文件以ASCII字符流返回给客户端;默认80端口
    HTTP/1.01996年发布,正式作为标准引入了POST、HEAD等命令、请求头、响应头、状态码;提供了缓存、多字符集支持、multi-part、authorization、内容编码等;默认不是持久连接
    HTTP/1.11997年发布,2015年前使用最广默认持久连接不必声明keep-alive;引入pipelining管道机制,同一TCP连接里同时发送多个请求,但服务器需按照请求顺序串行返回响应;请求头新增Host,使同一台物理服务器可以同时部署多个web服务
    HTTPS互联网巨头大力推行在传统HTTP协议的TCP与HTTP之间加入一层SSL/TLS;通过混合加密、摘要算法、数字证书来保证安全性;使用443端口
    SPDY2009年由Google公开,不是标准已逐渐被HTTP/2取代基于TLS,在HTTPS的SSL层与HTTP层之间增加一层SPDY层;支持多路复用,可在同一TCP连接并发处理多个HTTP请求;可以赋予请求的优先级顺序;支持请求头和响应头压缩;支持服务器向客户端主动推送、提示
    HTTP/22015年发布,逐步覆盖市场基于SPDY的标准化协议,可在TCP上使用不是必须在TLS上;HTTPS连接时使用了NPN的规范版ALPN;消息头的压缩算法采用新算法HPACK,而SPDY采用DEFLATE;依然没有解决TCP对头阻塞问题
    QUIC/HTTP32012由Google提出,2015年提交给IETF,下一代互联网标准传输协议不再是基于TCP而是通过UDP;使用 stream 进一步扩展了 HTTP/2 的多路复用;引入 Connection ID,使得 HTTP/3 支持连接迁移以及 NAT 的重绑定;含有一个包括验证、加密、数据及负载的 built-in 的TLS安全机制;将拥塞控制移出了内核,通过用户空间来实现;头部压缩更换成了兼容 HPACK的QPACK压缩方案

    状态码


    状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,共分为5种类型:































    分类说明
    1**表示请求已被接受,需要继续处理的临时响应
    2**成功,操作被成功接收并处理
    3**重定向,需要进一步的操作以完成请求
    4**客户端错误,请求包含语法错误或无法完成请求
    5**服务器错误,服务器在处理请求的过程中发生了错误

    方法


    方法的名称区分大小写,并且通常是一个简短的单词,由英文大写字母组成。


    接收请求时,服务器尝试确定请求的方法,如果失败,则返回带有代码 501 和短语的响应消息 Not Implemented






























































    方法说明1.0、1.1中支持的协议版本
    GET获取资源1.0、1.1
    POST传输实体主体1.0、1.1
    PUT传输文件1.0、1.1
    HEAD获得报文首部1.0、1.1
    DELETE删除资源1.0、1.1
    OPTIONS删除资源1.0、1.1
    TRACE追踪路径1.1
    CONNECT将服务器作为代理,让服务器代替用户去访问1.1
    LINK建立和资源之间的联系1.0
    UNLINK断开连接关系1.0

    首部


    http首部主要分为五大部分:



    1. 通用首部:各种类型的报文(请求、响应报文)都可以使用,提供有关报文最基本的信息。

    2. 请求首部:专用于请求报文的首部,用于给服务器提供相关信息,告诉服务器客户端的期望和能力。

    3. 响应首部:专用于响应报文的首部,用于告诉客户端是谁在响应以及响应者的能力。

    4. 实体首部:用于描述http报文的负荷(主体),提供了有关实体及其内容的相关信息。

    5. 扩展首部:非标准首部,由应用开发者定义的首部。


    (以下整理的首部并非完全按照这五类划分,部分扩展首部也按照功能划入了请求首部、响应首部等部分)


    通用首部

























































    字段名说明示例
    Cache-Control控制缓存的行为Cache-Control: no-cache
    Connection逐跳首部、连接的管理(HTTP/1.1默认持久连接)Connection: close
    Date创建报文的日期时间Date: Tue, 15 Nov 2010 08:12:31 GMT
    PragmaHTTP/1.1之前版本的历史遗留字段,用来包含实现特定的指令Pragma: no-cache
    Trailer说明传输中分块编码的编码信息Trailer: Max-Forwards
    Transfer-Encoding逐跳首部,指定传输报文主体时使用的编码方式Transfer-Encoding: chunked
    Upgrade升级为其他协议Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11
    Via代理服务器的相关信息Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)
    Warning错误通知Warning: 199 Miscellaneous warning

    请求首部





























































































































































    字段说明示例
    Accept客户端能够接收的内容类型Accept: text/plain, text/html
    Accept-Charset客户端可以接受的字符编码集Accept-Charset: iso-8859-5
    Accept-Encoding端到端首部,告知服务器客户端能够处理的编码方式和相对优先级Accept-Encoding: compress, gzip
    Accept-Language客户端可接受的自然语言Accept-Language: en,zh
    AuthorizationWeb认证信息Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
    DNT指示该用户的跟踪的偏好:0用户更喜欢在目标站点上进行跟踪;1用户不希望在目标站点上跟踪DNT: 1
    Expect客户端要求的特殊服务器行为。若服务器不能理解或者满足,则须返回417状态,或者如果请求有其他问题,返回4xx状态Expect: 100-continue
    Forwarded代理服务器的客户端的信息,此标头标准版本是X-Forwarded-For,X-Forwarded-Host与X-Forwarded-ProtoForwarded: for=192.0.2.60; proto=http; by=203.0.113.43
    From用户的电子邮箱地址From: user@email.com
    Host指定请求的服务器的域名和端口号Host: www.zcmhi.com
    If-Match当客户端If-Match的值若与服务端的ETag一致,才会执行请求,否则会拒绝412If-Match: W/"67ab43", "54ed21", "7892dd"
    If-Modified-Since若If-Modifed-Since字段值早于资源的更新时间,则希望服务端能处理该请求f-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT
    If-None-Match如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变If-None-Match: “737060cd8c284d8af7ad3082f209582d”
    If-Range告知服务器若指定的If-Range字段值和请求资源的ETag值一致时,则作为范围请求处理,否则返回全部资源If-Range: “737060cd8c284d8af7ad3082f209582d”
    If-Unmodified-Since比较资源的更新时间,与If-Modified-Since相反If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT
    Max-Forwards该字段以十进制整数形式指定可经过的服务器最大数目。服务器在往下一个服务器转发请求之前,会将Max-Forwards的值减1后重新赋值,当服务器接收到Max-Forwards值为0的请求时,则不再进行转发,而是直接返回响应Max-Forwards: 10
    Proxy-Authorization代理服务器要求客户端的认证信息Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
    Public-Key-Pins将特定的加密公钥与特定的Web务器相关联,以降低伪造证书对MITM攻击的风险Public-Key-Pins: pin-sha256=""; pin-sha256=""; max-age=5184000; includeSubDomains; report-uri=""
    Public-Key-Pins-Report-Only将针对违规的报告发送到头中report-uri指定的报告,但是,Public-Key-Pins如果违反了钉住规则,仍然允许浏览器连接到服务器Public-Key-Pins-Report-Only: pin-sha256="; pin-sha256=""; includeSubDomains; report-uri=""
    Range实体的节点范围请求Range: bytes=5001-10000
    Referer指定该请求是从哪个页面跳转页来的,常被用于分析用户来源等信息Referer: www.example.com/index.html
    Referrer-Policy用于过滤Referrer报头的策略Referrer-Policy: origin-when-cross-origin
    CookieHTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器Cookie: $Version=1; Skin=new;
    TE逐跳首部,告知服务器客户端能够处理的编码方式和相对优先级TE: gzip, deflate; q=0.5
    Upgrade-Insecure-Requests向服务器发送一个信号,表示客户对加密和认证响应的偏好,Upgrade-Insecure-Requests: 1
    User-AgentHTTP 客户端程序的信息User-Agent: Mozilla/5.0 (Linux; X11)
    X-Forwarded-For来表示 HTTP 请求端真实 IPX-Forwarded-For: IP0, IP1, IP2
    X-Forwarded-Host可用于确定最初使用哪个主机X-Forwarded-Host: id42.example-cdn.com
    X-Forwarded-Proto确定客户端和负载平衡器之间使用的协议确定客户端和负载平衡器之间使用的协议

    响应首部






































































































    字段说明示例
    Accept-Ranges是否接受字节范围请求Accept-Ranges: bytes
    Age从原始服务器到代理缓存形成的估算时间(以秒计,非负)Age: 12
    ETag资源的匹配信息ETag: “737060cd8c284d8af7ad3082f209582d”
    Expires响应过期的日期和时间Expires: Thu, 01 Dec 2010 16:00:00 GMT
    Location配合 3xx : Redirection 的响应,提供重定向的 URILocation: www.example.com
    Proxy-Authenticate代理服务器对客户端的认证方式Proxy-Authenticate: Basic
    Retry-After如果实体暂时不可取,通知客户端在指定时间之后再次尝试Retry-After: 120
    Set-CookieHttp CookieSet-Cookie: status-enable; expires=Tue, 05 Jul 2018 02:01:22 GMT; path=/; domain=.example.com;
    Serverweb服务器信息Server: Apache/1.3.27 (Unix) (Red-Hat/Linux)
    SourceMap响应报头链接生成的代码到一个源映射,使浏览器来重构原始源并在调试器呈现重构原始SourceMap: /path/to/file.js.map
    Strict-Transport-Security通常缩写为HSTS,告诉客户端它应该只使用HTTPS,而不是使用HTTP进行通信Strict-Transport-Security: max-age=31536000; includeSubDomains
    Tk显示了对相应请求的跟踪情况Tk: ! (under construction) Tk: ? (dynamic) Tk: G (gateway or multiple parties)
    Vary告知下游的代理服务器,应当如何对以后的请求协议头进行匹配,以决定是否可使用已缓存的响应内容而不是重新从原服务器请求新的内容Vary: Accept-Encoding,User-Agent
    WWW-Authenticate表明客户端请求实体应该使用的授权方案WWW-Authenticate: Basic
    X-Content-Type-Options如果服务器发送响应头 "X-Content-Type-Options: nosniff",则script和styleSheet元素会拒绝包含错误的 MIME 类型的响应。这是一种安全功能,有助于防止基于 MIME 类型混淆的攻击X-Content-Type-Options: nosniff
    X-DNS-Prefetch-Control控制浏览器的DNS预读取功能X-DNS-Prefetch-Control: on
    X-Frame-Options给浏览器指示允许一个页面可否在 , 或者 中展现的标记,网站可以使用此功能,来确保自己网站的内容没有被嵌套到别人的网站中去,也从而避免了点击劫持的攻击X-Frame-Options: ALLOW-FROM example.com/
    X-XSS-Protection是 IE,Chrome 和 Safari 的一个特性,当检测到跨站脚本攻击 (XSS)时,浏览器将停止加载页面X-XSS-Protection: 1; mode=block

    实体首部



































































    字段说明示例
    Allow服务器支持的HTTP请求方法Allow: GET, HEAD
    Content-Disposition指示回复的内容是以内联的形式还是以附件的形式下载并保存到本地;也可在multipart/form-data 类型的应答消息体中,用来给出其对应字段的相关信息Content-Disposition: attachment; filename="filename.jpg"
    Content-Encoding告知客户端服务器对实体的主体选用的内容编码方式Content-Encoding: gzip
    Content-Language实体主体使用的自然语言Content-Language: zh-CN
    Content-Length实体部分大小Content-Length: 15000
    Content-Location返回报文主体返回资源对应的URIContent-Location: httpo://www.example.com/index.html
    Content-MD5检查报文主体在传输过程中是否保持完整,对报文主体执行 MD5 算法获得218位二进制数,再通过 Base64 编码后将结果写入Content-MD5: ZTEwYWRjMzk0OWJhNTlhYmJlNTZlMDU3ZjIwZjg4M2U=
    Content-Range针对范围请求,表示当前发送部分及整个实体大小Content-Range: bytes 5001-10000/10000
    Content-type实体主体内对象的媒体类型Content-Type: text/html; charset=utf-8
    Expires将资源失效日期告知客户端Expires: Wed, 04 Jul 2012 08:26:05 GMT
    Last-Modified资源最终修改时间Last-Modified: wed, 25 May 2018 09:11:40 GMT

    跨域资源共享首部


    跨域资源共享标准新增了一组HTTP头部字段,属于扩展首部,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,对那些可能对服务器产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨域请求。服务器允许后才发起实际的HTTP请求。预检请求的返回中,服务器也可以通知客户端是否需要携带身份凭证。




















































    字段说明示例
    Access-Control-Allow-Credentials指示的请求的响应是否可以暴露于该页面,当true值返回时它可以被暴露,凭证是 Cookie ,授权标头或 TLS 客户端证书Access-Control-Allow-Credentials: true
    Access-Control-Allow-Headers用于预检请求中,列出了将会在正式请求的Access-Control-Request-Headers字段中出现的首部信息Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Allow-Methods在对预检请求的应答中明确了客户端所要访问的资源允许使用的方法或方法列表Access-Control-Allow-Methods: POST, GET, OPTIONS
    Access-Control-Allow-Origin指定了该响应的资源是否被允许与给定的origin共享Access-Control-Allow-Origin: developer.mozilla.org
    Access-Control-Expose-Headers出了哪些首部可以作为响应的一部分暴露给外部Access-Control-Expose-Headers: Content-Length, X-Kuma-Revision
    Access-Control-Max-Age表示预检请求的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息)可以被缓存多久Access-Control-Max-Age: 600
    Access-Control-Request-Headers出现于预检请求中,用于通知服务器在真正的请求中会采用哪些请求头Access-Control-Request-Headers: X-PINGOTHER, Content-Type
    Access-Control-Request-Method出现于预检请求中,用于通知服务器在真正的请求中会采用哪种HTTP方法Access-Control-Request-Method: POST

    安全性的不足



    1. 通信使用明文,内容可能会被窃听(可窃听)。

    2. 无法证明报文的完整性,内容有可能已遭篡改(可篡改)。

    3. 不验证通信方的身份,因此有可能遭遇伪装(可冒充)。


    HTTPS协议


    可以理解为HTTP+SSL/TLS, 即 HTTP 下加入 SSL/TLS 层,用于安全的 HTTP 数据传输,简单来说HTTP + 加密 + 认证 + 完整性保护 = HTTPS,用来解决HTTP协议安全性的不足。


    HTTPS


    SSL/TLS历史


    HTTPS相比HTTP多出了SSL/TLS 层,SSL 协议原本由网景公司开发,后来被 IETF 标准化,正式名称叫做 TLS,TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3,TLS1.1和TLS1.0不支持HTTP2,目前应用最广泛的应该还是 TLS 1.2,SSL协议发展历史如下:















































    协议发布时间状态
    SSL 1.0未公布未公布
    SSL 2.01995年已于2011年弃用 RFC6176
    SSL 3.01996年已于2015年弃用 RFC7568
    TLS 1.01999年RFC2246
    TLS 1.12006年RFC4346
    TLS 1.22008年RFC5246,目前最广泛应用
    TLS 1.32018年RFC8446

    TLS1.2单向认证流程


    以目前使用最广泛的TLS1.2说明下认证流程即握手流程,认证分为单向和双向两种模式,单向认证客户端验证服务端证书合法即可访问,一般Web应用都是采用SSL单向认证的;双向认证需要客户端和服务器都需要持有证书,两者证书验证均合法才可以继续访问。


    使用wireshark抓包TLS1.2,单向认证流程如下:
    wireshark抓包TLS1.2




    1. 客户端发送Client Hello。将一个Unix时间戳、TLS版本、支持的所有加密套件、支持的签名算法、生成的随机数Random_C等发送给服务器。




    2. 服务器发送Server Hello。将服务器Unix时间戳、生成的随机数Random_S、协商的加密算法套件等发送给客户端。




    3. 服务器发送Certificate、Server Key Exchange、Server Hello Done。Certificate是数字证书,Server Key Exchange为公钥参数(有时也可不需要),Server Hello Done表明服务器已经将所有预计的握手消息发送完毕。




    4. 客户端发送Client Key Exchange、Change Cipher Spec、Encrypted Handshake Message。客户端首先需要校验证书,证书向上按照证书链逐级校验,每一级证书校验过程是通过拿到证书签发者(Issuer)的证书中的公钥(证书 = 使用者公钥 + 主体信息如公司名称等 + CA对信息的确认签名 + 指纹)对本级证书(Subject)的签名进行数学验证,并校验证书是否被吊销,是否在有效期,是否与域名匹配等,验证成功即证书有效,整个一级一级验证上去,形成信任链,如果校验不通过则中断连接,浏览器弹出警告,校验正确后解析得到服务器公钥,并发送Client Key Exchange、Change Cipher Spec、Encrypted Handshake Message。Client Key Exchange:生成一个随机数 Pre-master,并用证书公钥加密,通过Fuc(random_C, random_S, Pre-Master)生成一个协商密钥;Change Cipher Spec:通知服务器协商完成,以后就使用上面生成的协商密钥进行对称加密;Encrypted Handshake Message:结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥与算法进行加密。




    5. 服务器发送Change Cipher Spec、Encrypted Handshake Message。服务器使用私钥解密得到 Pre-master数值,基于之前交换的两个明文随机数 random_C 和 random_S,同样通过Fuc(random_C, random_S, Pre-Master)得到协商密钥,计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性,验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;encrypted_handshake_message:服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥与算法加密并发送到客户端。




    6. 客户端计算所有接收信息的hash值,并采用协商密钥解密encrypted_handshake_message,验证服务器发送的数据和密钥,验证通过则握手完成。




    7. 开始使用协商密钥与算法进行加密通信。(Encrypted Alert是由客户端或服务器发送,意味着加密通信因为某些原因需要中断,警告对方不要再发送敏感的数据)。




    Android抓包HTTPS


    HTTP/HTTPS抓包工具有不少,常见的电脑端抓包工具有fiddlerCharlesBurp SuitewhistleAnyProxy,Android端抓包APP也有HttpCanary,wireshark也可以抓包但是不可以解密HTTPS内容。


    使用Charles抓包安卓HTTP很简单,将手机WIFI设置代理为Charles,步骤如下:



    1. 为方便设置代理,使手机与电脑处于同一局域网(连接同一个WIFI,或者电脑连到同一个路由器的LAN端口)。

    2. 电脑使用ipconfig查看局域网ip,并打开Charles。

    3. 手机连接的WIFI--高级设置--代理服务器,代理服务器填写电脑的局域网ip,Charles的代理端口默认8888。

    4. 电脑端Charles允许连接即可。

    5. Charles--Proxy--SSL Proxying Setting,Enable SSL Proxying打勾,add添加抓包的Host和Port,一般Port都是443,Host和Port都填*则抓包所有。


    打开APP就能在Charles上看到抓包的HTTP,但是HTTPS都会显示为Unknown,因为还没有安装Charles的证书。手机设置代理以后,浏览器访问chls.pro/ssl下载Charles证书并安装。Android 7.0以下直接安装即可,但是Android 7.0及以上默认不信任用户自己安装的证书,而只信任系统预设的证书,解决方法有:




    • 手动制作Charles证书,按照Android系统预设的格式,并推到/system/etc/security/cacerts目录下,从而让系统把Charles证书当作系统证书




    • 如果使用Magisk实现的root,Magisk安装MagiskTrustUserCerts模块,模块原理同上,可以把自定义的用户证书当作系统证书




    • 反编译apk,资源文件中添加network_security_config.xml,修改AndroidManifest.xml(修改APP的网络安全配置,信任用户证书)


      AndroidManifest.xml修改:




      ...


      复制代码

      network_security_config.xml内容:











      复制代码



    • frida或者xposed hook实现证书信任


      在JSSE中证书信任管理器类实现了X509TrustManager接口,我们可以自己实现一个X509TrustManager,通过hook修改掉网络请求库的X509TrustManager配置。


      比如自定义X509TrustManager实现信任所有服务端的证书(无论是否过期、是否经过认证):


      public class TrustAllManager implements X509TrustManager {
      @Override
      public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType)
      throws java.security.cert.CertificateException {
      }

      @Override
      public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType)
      throws java.security.cert.CertificateException {
      }

      @Override
      public java.security.cert.X509Certificate[] getAcceptedIssuers() {
      return new java.security.cert.X509Certificate[0];
      }
      }
      复制代码

      okhttp的X509TrustManager设置为:


      OkHttpClient.Builder builder = new OkHttpClient.Builder();
      builder.sslSocketFactory(createAllSSLSocketFactory(), new TrustAllManager());
      复制代码

      我们可以通过hook OkHttpClient.Builder类的sslSocketFactory方法,实现修改X509TrustManager配置,从而实现用户证书的信任。




    HTTPS抓包原理


    代理原理


    Charles、fiddler、HttpCanary抓包都是基于代理实现的,对HTTPS的抓包原理其实也都差不多,类似于中间人攻击,原理如下(图片来源于谈移动端抓包方式和原理及如何防犯中间人攻击):


    Charles抓包原理



    • TLS握手时拦截服务器证书,得到服务器公钥,并将Charles自己的证书发送给客户端,客户端原本校验Charles证书不通过,但我们可以手动使客户端信任Charles证书。

    • Charles拦截请求得到Random_S和Random_C等未加密信息,其他加密部分比如Pre-Master由于是使用Charles的证书公钥加密的,Charles可以使用自己的私钥解密得到内容,再使用服务器证书公钥重新加密后发送给服务器,从而与服务器完成TLS认证,并获取到密钥。

    • 每次发送HTTPS请求报文经过Charles,Charles再使用得到的对称密钥进行解密。


    Android防抓包策略及绕过思路


    Android上HTTPS抓包成本并不算高,使系统信任第三方证书就能够实现抓包,Android 7.0 (API 24)及以上虽默认不再信任用户CA,提高了安全性但抓包成本也不算高,还可添加其他防抓包策略进一步提高安全性。


    设置无代理模式


    由于Charles、fiddler这些抓包工具是基于代理实现的(wireshark不是基于代理,而是网卡抓包),所以可以将APP所用的HTTP客户端设置为无代理,设置之后HTTP客户端不会连接到代理服务器,这样的话Charles就无法直接抓包了,比如OkHttp配置无代理:


    OkHttpClient.Builder()
    .proxy(Proxy.NO_PROXY)
    .build()
    复制代码

    绕过方案:



    1. 手动修改DNS。由上面的代理原理图可知,若请求不走代理,就要通过DNS解析获取ip地址再发送请求,我们可以直接修改Android的DNS配置,将请求域名解析到Charles代理服务器上,从而实现抓包。

    2. VPN流量转发。使用drony之类的APP先将手机请求导到VPN,再对VPN的网络进行Charles的代理。

    3. 使用frida或者xposed hookOkHttpClient.Builderproxy方法,使无代理配置不起作用。


    增强本地证书校验


    APP本地做证书校验时不仅仅校验公钥,并且设置更为严格的校验模式,直接安装的Charles证书将因为格式问题不能验证通过,可以通过实现X509TrustManager 接口实现。这种方式其实适用于Android 7.0以下,Android 7.0以上通过MagiskTrustUserCerts等方式安装的证书由于已经被当作系统内置证书,这种方式应该不再起作用。


    SSL Pinning证书锁定


    应用中只信任固定证书或是公钥,将可信 CA 限制在一个很小的 CA 集范围内,应用的服务器将使用这个集合,这样可以防止因泄露系统中其他 100 多个 CA 中的某个 CA 而破坏应用安全通道。


    通常有两种锁定方式证书固定公钥固定。证书固定:将证书的某些字节码硬编码在用程序中,证书校验时检查证书中是否存在相同的字节码;公钥固定:网站会提供已授权公钥的哈希列表,指示客户端在后续通讯中只接受列表上的公钥。


    OkHttp配置实现证书锁定,对特定的host做证书公钥验证,公钥经过Sha1算法hash一下,然后Base64加密一次,然后在结果前面加上字符串"sha256/"或者"sha1/":


    CertificatePinner certificatePinner = new CertificatePinner.Builder()
    .add(example.com, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build();

    OkHttpClient client = new OkHttpClient();
    client.setCertificatePinner(certificatePinner);
    复制代码

    Android 7.0及以上实现证书锁定:





    example.com

    7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=

    fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=



    复制代码

    绕过方案:



    1. 可以尝试Xposed+JustTrustMe模块或者JustTrustMePlus模块或者TrustMeAlready模块

    2. Frida+DroidSSLUnpinning脚本

    3. hook,需要反编译分析源码


    TLS双向认证


    TLS认证有单向认证和双向认证模式,双向认证除了客户端去验证服务器端的证书外,服务器也同时需要验证客户端的证书,如果没有通过验证,则会拒绝连接,如果通过验证,服务器获得用户的公钥。


    绕过方案:双向认证需要客户端证书,所以APP内是要有证书的并且有操作证书的地方,hook这部分代码获取证书及密钥,再将证书格式转换一下,导入到Burp Suite或者Charles这些抓包软件中,实现抓包。


    此外,APP内肯定有传输内容SSL解密的实现,针对这部分代码进行hook,也可直接拿到数据,这也是更为通用的获取请求内容的办法,实现方式可以参考Frida的脚本frida_ssl_logger


    Android网络安全方面配置可以查看developer android,此外还可通过HTTPS请求的报文密、重要请求走Socket通信,APP加固防止hook等手段提高安全性。


    作者:flyleft
    链接:https://juejin.cn/post/6954470356551729165
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    zygote启动流程

    对zygote的理解 在Android系统中,zygote是一个native进程,是所有应用进程的父进程。而zygote则是Linux系统用户空间的第一个进程——init进程,通过fork的方式创建并启动的。 作用 zygote进程在启动时,会创建一个Dalv...
    继续阅读 »

    对zygote的理解


    在Android系统中,zygote是一个native进程,是所有应用进程的父进程。而zygote则是Linux系统用户空间的第一个进程——init进程,通过fork的方式创建并启动的。


    作用


    zygote进程在启动时,会创建一个Dalvik虚拟机实例,每次孵化新的应用进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面,从而使得每个应用程序进程都有一个独立的Dalvik虚拟机实例。


    zygote进程的主要作用有两个:



    • 启动SystemServer。

    • 孵化应用进程。


    启动流程


    启动入口

    Zygote进程在init进程中,通过解析init.zygote.rc配置文件,以service(服务)的方式启动并创建的。


    以init.zygote32.rc为例来看下:


    脚本讲解

    //    system\core\rootdir\init.zygote32.rc
    service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    priority -20
    user root
    group root readproc reserved_disk
    socket zygote stream 660 root system
    socket usap_pool_primary stream 660 root system
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    onrestart restart wificond
    writepid /dev/cpuset/foreground/tasks
    复制代码


    这段脚本要求 init 进程创建一个名为 zygote 的进程,该进程要执行的程序是“/system/bin/app_process”。并且为 zygote 进程创建一个 socket 资源 (用于进程间通信,ActivityManagerService 就是通过该 socket 请求 zygote 进程 fork 一个应用程序进程)。


    后面的**--zygote**是参数,表示启动的是zygote进程。在app_process的main函数中会依据该参数决定执行ZygoteInit还是Java类。


    启动过程

    zygote要执行的程序便是system/bin/app_process,它的源代码在frameworks/base/cmds/app_process/app_main.cpp


    App_main::main

    int main(int argc, char* const argv[])
    {
    ...
    while (i < argc) {
    const char* arg = argv[i++];
    if (strcmp(arg, "--zygote") == 0) {//是否有--zygote参数。这个是启动zygote进程的时候的参数
    zygote = true;
    //进程名称,设置为zygote
    niceName = ZYGOTE_NICE_NAME;
    } else if (strcmp(arg, "--start-system-server") == 0) {//是否有--start-system-server
    startSystemServer = true;
    ....
    if (zygote) {
    //最最重要方法。。。如果是zygote进程,则启动ZygoteInit。
    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
    runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
    fprintf(stderr, "Error: no class name or --zygote supplied.\n");
    app_usage();
    LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
    }
    }
    复制代码

    AndroidRuntime::start

    void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
    {
    ...
    JNIEnv* env;
    //重点方法 创建VM虚拟机,参数是指针,可以用于获取返回的值,可以使用env来和Java层来做交互
    if (startVm(&mJavaVM, &env, zygote) != 0) {
    return;
    }
    onVmCreated(env);
    //重点方法 给虚拟机注册一些JNI函数,(系统so库、用户自定义so库 、加载函数等。)
    if (startReg(env) < 0) {
    ALOGE("Unable to register all android natives\n");
    return;
    }

    //找到类的main方法,并调用。如果是zygote的话,这里就会启动ZygoteInit类的main方法
    jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
    "([Ljava/lang/String;)V");
    if (startMeth == NULL) {
    ALOGE("JavaVM unable to find main() in '%s'\n", className);
    /* keep going */
    } else {
    //调用main方法。这里通过JNI调用Java方法之后,Zygote(Native层)就进入了Java的世界,从而开启了Android中Java的世界。
    env->CallStaticVoidMethod(startClass, startMeth, strArray);
    }
    复制代码

    App_main.main
    AndroidRuntime.start
    startVm//创建虚拟机
    startReg//注册JNI函数
    ZygoteInit.main//这里就进入到了Java层了
    registerZygoteSocket//建立IPC的通讯机制
    preload//预加载类和资源
    startSystemServer//启动system_server
    runSelectLoop//等待进程创建的请求
    复制代码


    对应的源码地址:
    /frameworks/base/cmds/app_process/App_main.cpp (内含AppRuntime类)
    /frameworks/base/core/jni/AndroidRuntime.cpp
    /frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    /frameworks/base/core/java/com/android/internal/os/Zygote.java
    /frameworks/base/core/java/android/net/LocalServerSocket.java



    Zygote进程的启动过程中,除了会创建一个Dalvik虚拟机实例之外,还会将Java运行时库加载到进程中,以及注册一些Android核心类的JNI方法到创建的Dalvik虚拟机实例中。


    zygote进程初始化时启动虚拟,并加载一些系统资源。这样zygote fork出子进程之后,子进程也会继承能正常工作的虚拟机和各种系统资源,剩下的只需要装载APK文件的字节码就可以运行程序,。


    Java应用程序不能以本地进程的形态运行,必须在一个独立的虚拟机中运行。如果每次都重新启动虚拟机,肯定就会拖慢应用程序的启动速度。


    注意:APK应用程序进程被zygote进程孵化出来以后,不仅会获得Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库。


    作者:kailaisi
    链接:https://juejin.cn/post/6944949447503642632
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Android 开发也要懂得数据结构 - SparseArray源码

    在之前分析 HashMap 就知道当容量达到 75% 时就需要扩容,那也就意味着 25% 的内存空间啥也不放,浪费掉了,为了解决这个问题,就有了 SparseArray。 本文章使用的是 JDK1.8 ,不同版本源码有差异。 可先食用 Android 开发也要...
    继续阅读 »
    • 在之前分析 HashMap 就知道当容量达到 75% 时就需要扩容,那也就意味着 25% 的内存空间啥也不放,浪费掉了,为了解决这个问题,就有了 SparseArray

    • 本文章使用的是 JDK1.8 ,不同版本源码有差异。

    • 可先食用 Android 开发也要懂得数据结构 - HashMap源码


    1.SparseArray特点



    • SparseArray的结构是 双数组 ,就是key和value都是数组,下标一一对应。

    • SparseArray虽然是 key-valye 结构,但是key只能是 int 类型,用于代替 HashMap<Integer,Object>,这也是缺点,还有 LongSparseArray

    • HashMap 处理 int 类型的key,是需要 装箱成Integer类型 ,消耗一些资源,而 SparseArray 就不需要装箱操作,更快一些。

    • HashMap 保存数据是以 Entry对象保存,还要计算hashCode,在内存方面是要大于 SparseArray。

    • SparseArray 的查找速度比较快,利用的是二分法查找,二分查找的要求就是key是有序排列的。

    • 二分查找虽然挺快的,数据量大的时候跟HashMap比就没有什么优势了,千级以下使用。


    2.SparseArray常用的方法


    2.1 基本参数



    • DELETED 是删除的位置放的东西,SparseArray 删除相当于是打上标记,就不需要移动数组,减少数组移动的耗时。

    • mGarbage 标志是如果有删除,就为true,用于后面的 gc() 方法的标记。

    • 可以看到 SparseArray 的 key 和 value 都是数组。

    • 还有一个长度mSize,与List,Map一样样,这个是实际数据的长度,不是容量的大小。


        private static final Object DELETED = new Object();
    private boolean mGarbage = false;

    @UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
    private int[] mKeys;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
    private Object[] mValues;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
    private int mSize;
    复制代码

    2.2 构造方法



    • 构造方法两种,可以选填容量大小或者不填,不填容量,容量默认就为10。

    • 如果填写的容量为0,那就会创建一个非常轻量的数组。


        /**
    * Creates a new SparseArray containing no mappings.
    */
    public SparseArray() {
    this(10);
    }

    /**
    * Creates a new SparseArray containing no mappings that will not
    * require any additional memory allocation to store the specified
    * number of mappings. If you supply an initial capacity of 0, the
    * sparse array will be initialized with a light-weight representation
    * not requiring any additional array allocations.
    */
    public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
    mKeys = EmptyArray.INT;
    mValues = EmptyArray.OBJECT;
    } else {
    mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
    mKeys = new int[mValues.length];
    }
    //还没有放入数据,所以mSize为0。
    mSize = 0;
    }
    复制代码

    2.3 二分查找 ContainerHelpers.binarySearch(mKeys, mSize, key)



    • 显示利用二分法,找出要放入元素的 key 对应的下标,如果找不到就返回二分范围小值的取反。


        //比较简单的二分法查找
    // This is Arrays.binarySearch(), but doesn't do any argument validation.
    static int binarySearch(int[] array, int size, int value) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
    final int mid = (lo + hi) >>> 1;
    final int midVal = array[mid];
    //如果中间这个数小于目标值
    if (midVal < value) {
    //检索的范围最小就为中间+1
    lo = mid + 1;
    } else if (midVal > value) {
    //如果大于,范围的最大就为中间-1
    hi = mid - 1;
    } else {
    //找到就返回下标位置
    return mid; // value found
    }
    }
    //找不到就返回lo取反
    return ~lo; // value not present
    }
    复制代码

    2.4 放入元素 put(int key, E value)



    • 如果 key 位置已存在,直接覆盖。

    • 如果找不到 key 对应下标,且在范围内,有删除过闲置的位置,就把当前数据放在这个位置。

    • 如果这都不满足条件,就调用 gc() 方法整理一遍数据,把标记过删除的位置,干掉,再插入数据。

    • 插入元素如果容量不够,就扩容,如果原始容量小于4的,扩容成8,否则就以2倍的大小扩容。

    • 数组的插入需要移动后面位置的元素。


        /**
    * Adds a mapping from the specified key to the specified value,
    * replacing the previous mapping from the specified key if there
    * was one.
    */
    public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    mValues[i] = value;
    } else {
    //取反
    i = ~i;

    //如果i小于长度,且i位置没东西,就放在i位置。
    if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
    }

    //如果有垃圾(删除过东西),且数据容量长度大于等于key数组时
    if (mGarbage && mSize >= mKeys.length) {
    //整理一下数据
    gc();

    //整理后再次查找索引位置
    // Search again because indices may have changed.
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
    }

    //插入数据
    mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
    mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
    mSize++;
    }
    }

    //GrowingArrayUtils.insert
    /**
    * Primitive int version of {@link #insert(Object[], int, int, Object)}.
    */
    public static int[] insert(int[] array, int currentSize, int index, int element) {
    assert currentSize <= array.length;

    if (currentSize + 1 <= array.length) {
    System.arraycopy(array, index, array, index + 1, currentSize - index);
    array[index] = element;
    return array;
    }

    int[] newArray = new int[growSize(currentSize)];
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
    }

    //GrowingArrayUtils.growSize
    //扩容,如果容量小于4的,扩容成8,否则就以2倍的大小扩容。
    /**
    * Given the current size of an array, returns an ideal size to which the array should grow.
    * This is typically double the given size, but should not be relied upon to do so in the
    * future.
    */
    public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
    }
    复制代码

    2.5 整理数据 gc()



    • 把非 DELETED 位置的数据,一个个往前移动。

    • mGarbage 设置为 false


        private void gc() {
    // Log.e("SparseArray", "gc start with " + mSize);

    int n = mSize;
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
    Object val = values[i];

    if (val != DELETED) {
    if (i != o) {
    keys[o] = keys[i];
    values[o] = val;
    values[i] = null;
    }

    o++;
    }
    }

    mGarbage = false;
    mSize = o;

    // Log.e("SparseArray", "gc end with " + mSize);
    }
    复制代码

    2.6 删除元素 remove(int key)或者delete(int key)



    • 我们知道真正数组的删除,是要以移动后面的元素的,每次会造成大量的操作,所以改为标记清除,先打上标记,可以在放入元素时重新利用上空闲的位置,或者后面gc时再一次性清除掉。


        /**
    * Alias for {@link #delete(int)}.
    */
    public void remove(int key) {
    delete(key);
    }

    /**
    * Removes the mapping from the specified key, if there was any.
    */
    public void delete(int key) {
    //用二分法,找到要删除数据对应的下标
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    mValues[i] = DELETED;
    mGarbage = true;
    }
    }
    }
    复制代码

    2.7 查找元素 get(int key)



    • 利用二分查找找出 key 对应的下标,然后返回同样下标位置的值。

    • 两个参数的方法,也可设置找不到放回的默认值,如果找不到就返回默认值,否则是null。


        /**
    * Gets the Object mapped from the specified key, or <code>null</code>
    * if no such mapping has been made.
    */
    public E get(int key) {
    return get(key, null);
    }

    /**
    * Gets the Object mapped from the specified key, or the specified Object
    * if no such mapping has been made.
    */
    @SuppressWarnings("unchecked")
    public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i < 0 || mValues[i] == DELETED) {
    return valueIfKeyNotFound;
    } else {
    return (E) mValues[i];
    }
    }
    复制代码

    2.8 查找key对应的下标 indexOfKey(int key)



    • 如果有标记垃圾,先整理一遍再放回 key 对应的下标。


        /**
    * Returns the index for which {@link #keyAt} would return the
    * specified key, or a negative number if the specified
    * key is not mapped.
    */
    public int indexOfKey(int key) {
    if (mGarbage) {
    gc();
    }

    return ContainerHelpers.binarySearch(mKeys, mSize, key);
    }
    复制代码

    2.9 查找value对应的下标 indexOfValue(E value)



    • 查找之前依然会整理一次数据,不同位置都可能保存着这个value,所以遍历后,返回第一个,如果找不到就返回-1。


        /**
    * Returns an index for which {@link #valueAt} would return the
    * specified value, or a negative number if no keys map to the
    * specified value.
    * <p>Beware that this is a linear search, unlike lookups by key,
    * and that multiple keys can map to the same value and this will
    * find only one of them.
    * <p>Note also that unlike most collections' {@code indexOf} methods,
    * this method compares values using {@code ==} rather than {@code equals}.
    */
    public int indexOfValue(E value) {
    if (mGarbage) {
    gc();
    }

    for (int i = 0; i < mSize; i++) {
    if (mValues[i] == value) {
    return i;
    }
    }

    return -1;
    }
    复制代码

    2.10 长度size()



    • 返回数据的长度。


        /**
    * Returns the number of key-value mappings that this SparseArray
    * currently stores.
    */
    public int size() {
    if (mGarbage) {
    gc();
    }

    return mSize;
    }

    作者:包籽
    链接:https://juejin.cn/post/6922733430719381511
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    View.post()为什么能准确拿到View的宽高?

    老生常谈之------View.post() 起因:之前一群里的哥们问 Handler.post() 为什么会在 Activity 的 onResume() 之后执行,我找了一遍之后并没有找到原因,后来从这个问题我想起其他的问题 view.post() 为什...
    继续阅读 »

    老生常谈之------View.post()


    起因:之前一群里的哥们问 Handler.post() 为什么会在 ActivityonResume() 之后执行,我找了一遍之后并没有找到原因,后来从这个问题我想起其他的问题 view.post() 为什么在 view.post() 之后为什么可以准确的获取到 view的宽高。


    疑问🤔️:View.post()为什么会准确的获取到View的宽高?


    public boolean post(Runnable action) {
    //注释1:判断attachInfo如果不为空 直接调用attachInfo内部Handler.post()方法
    //这样就有一个问题attachInfo在哪里赋值?这个问题先存疑。
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
    return attachInfo.mHandler.post(action);
    }
    //注释2:此时attachInfo是空的
    getRunQueue().post(action);
    return true;
    }
    复制代码

    存疑1:attachInfo在哪里赋值?


    我们点进去看一下 getRunQueue().post(action); 是何方神圣。


    private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
    mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
    }
    复制代码

    上面代码很简单,那么接下来就需要看看 HandlerActionQueue() 是什么玩意了。


    public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;

    //注释1
    public void post(Runnable action) {
    postDelayed(action, 0);
    }
    //注释2
    public void postDelayed(Runnable action, long delayMillis) {
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
    if (mActions == null) {
    mActions = new HandlerAction[4];
    }
    mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
    mCount++;
    }
    }

    public void removeCallbacks(Runnable action) {}

    //假装不知道这个是执行方法
    public void executeActions(Handler handler) {}

    public int size() {}

    public Runnable getRunnable(int index) {}

    public long getDelay(int index) {}

    private static class HandlerAction {}
    }

    复制代码

    先看注释1 可以看到这个post()方法 其实就是getRunQueue().post(action); 它内部调用了注释2方法 也就是postDelayed()
    注释2简单看了里面的代码 发现大概逻辑只是对 我们postrunnable进行缓存起来,那么我们从哪里真正执行这个runnable呢?
    我发现 我们一路跟进来直到postDelayed()方法时并没有看到对应的执行方法那么我们就放大招了


    影·奥义!时光回溯。


    我们看看getRunQueue()时 返回的mRunQueue在哪里调用的就好了。


    	//不会上传图片...大小不懂得控制 所以用代码块 
    private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
    mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
    }
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    //忽略的一些代码
    if (mRunQueue != null) {
    mRunQueue.executeActions(info.mHandler);
    mRunQueue = null;
    }
    }
    复制代码

    通过检查对mRunQueue的引用 可以看到view代码内只有这两个方法有所使用,那么第一个方法我们以及看过了 所以我们重点看下第二个方法。


    从上面第二个方法 可以得知mAttachInfodispatchAttachedToWindow(AttachInfo info, int visibility)内的info赋值 那么 info又在哪里生成?先继续看下去。


    通过对dispatchAttachedToWindow() 的调用关系可以发现以下方法会调用


    private void performTraversals() {
    // cache mView since it is used so much below...
    final View host = mView;
    /.../
    //注释1
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    /.../
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量
    performLayout(lp, mWidth, mHeight);//执行布局
    performDraw();//执行绘制

    }
    复制代码

    可以看到 host内会调用这个方法,并且将mAttachInfo作为参数传入,而这个host是一个DecorView


    为什么是DecorView? 我们可以反推回去验证
    通过源码我们得知 hostViewRootImpl 中的mView的成员变量
    mView检查赋值的地方 可以看到以下代码:


     public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
    if (mView == null) {
    mView = view;//对mView 赋值
    /.../
    }
    }
    }
    复制代码

    那什么时候能调用到setView()呢?
    可以看到setView() 并不是静态方法,所以要调用并需要引用实例才可以。


    那么我们可以看看构造方法


    public ViewRootImpl(Context context, Display display) {
    mContext = context;
    /.../
    //注释1
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
    }
    复制代码

    可以看到注释1处的代码我们能够得知 mAttachInfo是在ViewRootImpl构造器中创建出来的。


    我们再对构造器查看调用 可以发现再 WindowManagerGlobaladdView()方法 会创建ViewRootImpl


         public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
    /.../
    root = new ViewRootImpl(view.getContext(), display);
    }
    复制代码

    WindowManagerGlobaladdView()方法 又被WindowManagerImpladdView()调用


       @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
    复制代码

    再对这个方法进行调用检查可以看到


    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
    String reason) {
    if (r.window == null && !a.mFinished && willBeVisible) {
    r.window = r.activity.getWindow();
    View decor = r.window.getDecorView(); //注释1
    decor.setVisibility(View.INVISIBLE);
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    a.mDecor = decor;
    l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
    l.softInputMode |= forwardBit;
    if (r.mPreserveWindow) {
    a.mWindowAdded = true;
    r.mPreserveWindow = false;
    ViewRootImpl impl = decor.getViewRootImpl();
    if (impl != null) {
    impl.notifyChildRebuilt();
    }
    }
    if (a.mVisibleFromClient) {
    if (!a.mWindowAdded) {
    a.mWindowAdded = true;
    wm.addView(decor, l); //注释2
    } else {
    a.onWindowAttributesChanged(l);
    }
    }
    } else if (!willBeVisible) {
    if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
    r.hideForNow = true;
    }
    }
    复制代码

    可以在注释1处:会返回一个DecorView 并且在注释2处添加进入。
    至此 我们可以得知ViewRootImpl处的mView就是 DecorView


    知识点:
    并且我们可以从上面代码看出在onResumeview 才会被添加在window内并且执行view的测量布局绘制 这就是为什么在onCreate()时获取到view宽高会是0的原因,因为那时view都没有添加进window呢!!!


    时间再次穿梭 回到ViewRootImplperformTraversals()


    既然已知mView就是DecorView 那么这个DecorView是一个继承于FrameLayoutViewGroup
    我们在DecorViewFrameLayout内没有找到对dispatchAttachedToWindow()方法的处理,就自然而然的来到了ViewGroup处。


     @Override
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
    final View child = children[i];
    //遍历调用 这样最终会回到View的dispatchAttachedToWindow()
    child.dispatchAttachedToWindow(info,
    combineVisibility(visibility, child.getVisibility()));
    }
    }
    复制代码

    这时候我们又回到了ViewdispatchAttachedToWindow()方法内的mRunQueue.executeActions(info.mHandler);并点击去看源码


      public void executeActions(Handler handler) {
    synchronized (this) {
    final HandlerAction[] actions = mActions;//注释1
    for (int i = 0, count = mCount; i < count; i++) {
    final HandlerAction handlerAction = actions[i];
    handler.postDelayed(handlerAction.action, handlerAction.delay);//注释2
    }
    mActions = null;
    mCount = 0;
    }
    }
    复制代码

    注释1处 我们可以看到mActions其实是我们用view.post方法时 传入的runnable的存储数组。
    注释2处 就会将runnable 交给handler.post()方法并且添加进这个Handler所持有的Looper内部的MessageQueue中。


    至此我们可以大概的进行一次总结了。


    总结:


    1:View内部的mAttachInfo 会在ViewdispatchAttachedToWindow()方法内赋值
    dispatchDetachedFromWindow()赋值为null,并且mAttachInfo的根本是在 ViewRootImpl的构造器内创建的,所以我们就可以知道当viewattchInfo不为空时 这个 view是已经被添加进窗口内的,如果为null就说明view没有在window内。


    2: 我们能通过view.post()正确的获取View的宽高主要得益于Android内的生命周期是被Handler所驱动的,所以当ViewRootImplActivityonResume()生命周期内被创建时,其实主线程的Handler 是在执行处理一个Message的流程中,虽然我们从上面ViewRootImpl内的performTraversals()源码中看到 view缓存的runnable会在performMeasure(), performLayout(),performDraw()这些方法前先被post出去并且添加到MessageQueue链表中,但是这些runnable是属于下一个Message的,而performMeasure(), performLayout(),performDraw()这三个方法是属于本次Message的逻辑,只有本次消息处理完成Handler内部的Looper才会进行下一次消息的处理,最终保证了 View.post()能够正确的拿到View的宽高。



    作者:FeanCheng
    链接:https://juejin.cn/post/6916024212696236040
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。  

    收起阅读 »

    ARouter原理与缺陷解析

    前言 本文主要包括以下内容 1.为什么需要ARouter及ARouter的基本原理 2.什么是APT及ARoutr注解是如何生效的? 3.ARouter有什么缺陷? 4.什么是字节码插桩,及如何利用字节码插桩优化ARouter? 为什么需要ARouter 我们...
    继续阅读 »

    前言


    本文主要包括以下内容

    1.为什么需要ARouterARouter的基本原理

    2.什么是APTARoutr注解是如何生效的?

    3.ARouter有什么缺陷?

    4.什么是字节码插桩,及如何利用字节码插桩优化ARouter?


    为什么需要ARouter


    我们知道,传统的Activity之间通信,通过startActivity(intent),而在组件化的项目中,上层的module没有依赖关系(即便两个module有依赖关系,也只能是单向的依赖)

    那么如何实现在没有依赖的情况下进行界面跳转呢?

    ARoutr帮我们实现了这点


    使用ARouter的原因就是为了解耦,即没有依赖时可以彼此跳转


    什么是APT


    APTAnnotation Processing Tool的简称,即注解处理工具。

    它是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。

    我们常用的ButterKnife,其原理就是通过注解处理器在编译期扫描代码中加入的@BindView@OnClick等注解进行扫描处理,然后生成XXX_ViewBinding类,实现了view的绑定。


    ARouter中使用的注解处理器就是javapoet

    (1)JavaPoet是square推出的开源java代码生成框架

    (2)简洁易懂的API,上手快

    (3)让繁杂、重复的Java文件,自动化生成,提高工作效率,简化流程

    (4) 相比原始APT方法,JavaPoet是OOP的


    ARoutr的注解是如何生效的?


    我们在使用ARouter时都会在Activity上添加注解


    @Route(path = "/kotlin/test")
    class KotlinTestActivity : Activity() {
    ...
    }

    @Route(path = "/kotlin/java")
    public class TestNormalActivity extends AppCompatActivity {
    ...
    }
    复制代码

    这些注解在编译时会被arouter-compiler处理,使用JavaPoet在编译期生成类文件

    生成的文件如下所示:


    public class ARouter$$Group$$kotlin implements IRouteGroup {
    @Override
    public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/kotlin/java", RouteMeta.build(RouteType.ACTIVITY, TestNormalActivity.class, "/kotlin/java", "kotlin", null, -1, -2147483648));
    atlas.put("/kotlin/test", RouteMeta.build(RouteType.ACTIVITY, KotlinTestActivity.class, "/kotlin/test", "kotlin", new java.util.HashMap<String, Integer>(){{put("name", 8); put("age", 3); }}, -1, -2147483648));
    }
    }

    public class ARouter$$Root$$modulekotlin implements IRouteRoot {
    @Override
    public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("kotlin", ARouter$$Group$$kotlin.class);
    }
    }
    复制代码

    如上所示,将注解的key与类的路径通过一个Map关联起来了

    只要我们拿到这个Map,即可在运行时通过注解的key拿到类的路径,实现在不依赖的情况下跳转


    如何拿到这个Map呢?


    ARouter缺陷


    ARouter的缺陷就在于拿到这个Map的过程

    我们在使用ARouter时都需要初始化,ARouter所做的即是在初始化时利用反射扫描指定包名下面的所有className,然后再添加map

    源码如下


    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    //load by plugin first
    loadRouterMap();
    if (registerByPlugin) {
    logger.info(TAG, "Load router map by arouter-auto-register plugin.");
    } else {
    Set<String> routerMap;

    // It will rebuild router map every times when debuggable.
    if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
    logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
    // These class was generated by arouter-compiler.
    //反射扫描对应包
    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
    if (!routerMap.isEmpty()) {
    //
    context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
    }

    PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
    } else {
    logger.info(TAG, "Load router map from cache.");
    routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
    }
    ....
    }
    }
    复制代码

    如上所示:

    1.初次打开时会利用ClassUtils.getFileNameByPackageName来扫描对应包下的所有className

    2.在初次扫描后会存储在SharedPreferences中,这样后续就不需要再扫描了,这也是一个优化

    3.以上两个过程都是耗时操作,即是ARouter初次打开时可能会造成慢的原因

    4.那有没有办法优化这个过程,让第一次打开也不需要扫描呢?


    利用字节码插桩优化ARouter首次启动耗时


    我们再看看上面的代码


    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    //load by plugin first
    loadRouterMap();
    if (registerByPlugin) {
    logger.info(TAG, "Load router map by arouter-auto-register plugin.");
    } else {
    ....
    }
    }

    private static void loadRouterMap() {
    //registerByPlugin一直被置为false
    registerByPlugin = false;
    }
    复制代码

    在初始化时,会在扫描之前,判断registerByPlugin,如果我们需要的map已经被插件注册了,那也就不需要进行下面的耗时操作了

    但是我们可以看到在loadRouterMap中,registerByPlugin一直被设为false

    registerByPlugin是不是一直没有生效?

    这里面其实用到了字节码插桩来在loadRouterMap方法中插入代码


    什么是编译插桩?


    顾名思义,所谓编译插桩就是在代码编译期间修改已有的代码或者生成新代码。实际上,我们项目中经常用到的 Dagger、ButterKnife 甚至是 Kotlin 语言,它们都用到了编译插桩的技术。

    理解编译插桩之前,需要先回顾一下Android项目中.java文件的编译过程:

    image.png

    从上图可以看出,我们可以在 1、2 两处对代码进行改造。

    1.在.java文件编译成.class文件时,APTAndroidAnnotation 等就是在此处触发代码生成。

    2.在.class文件进一步优化成.dex文件时,也就是直接操作字节码文件,这就是字码码插桩


    ARouter注解生成用了第一种方法,而启动优化则用了第二种方法



    ASM是一个十分强大的字节码处理框架,基本上可以实现任何对字节码的操作,也就是自由度和开发的掌控度很高.

    但是其相对来说比AspectJ上手难度要高,需要对Java字节码有一定了解.

    不过ASM为我们提供了访问者模式来访问字节码文件,这种模式下可以比较简单的做一些字节码操作,实现一些功能。

    同时ASM可以精确的只注入我们想要注入的代码,不会额外生成一些包装代码,所以性能上影响比较微小。


    关于ASM使用的具体细节可以参见:深入探索编译插桩技术(四、ASM 探秘)


    字节码插桩对ARouter具体做了什么优化?


    //源码代码,插桩前
    private static void loadRouterMap() {
    //registerByPlugin一直被置为false
    registerByPlugin = false;
    }
    //插桩后反编译代码
    private static void loadRouterMap() {
    registerByPlugin = false;
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
    }
    复制代码

    1.插桩前源码与插桩后反编译代码如上所示

    2.插桩后代码即在编译期在loadRouterMap中插入了register代码

    3.通过这种方式即可避免在运行时通过反射扫描className,优化了启动速度


    插件使用


    使用Gradle插件实现路由表的自动加载


    apply plugin: 'com.alibaba.arouter'

    buildscript {
    repositories {
    jcenter()
    }

    dependencies {
    classpath "com.alibaba:arouter-register:?"
    }
    }
    复制代码

    1.可选使用,通过ARouter提供的注册插件进行路由表的自动加载

    2.默认通过扫描dex的方式进行加载,通过gradle插件进行自动注册可以缩短初始化时间,同时解决应用加固导致无法直接访问dex文件,初始化失败的问题

    3.需要注意的是,该插件必须搭配api 1.3.0以上版本使用!

    4.ARouter插件基于AutoRegister进行开发,关于其原理的更多介绍可见:AutoRegister:一种更高效的组件自动注册方案


    总结


    本文主要讲述了

    1.使用ARouter的根本原因是为在互相不依赖的情况下进行页面跳转以实现解藕

    2.什么是APTARoutr注解生成的代码解析

    3.ARouter的缺陷在于首次初始化时会通过反射扫描dex,同时将结果存储在SP中,会拖慢首次启动速度

    4.ARouter提供了插件实现在编译期实现路由表的自动加载,从而避免启动耗时,其原理是字节码插桩


    作者:RicardoMJiang
    链接:https://juejin.cn/post/6945610863730491422
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    ConstraintLayout 约束布局

    约束布局 约束布局概念 ConstraintLayout 可让您使用扁平视图层次结构(无嵌套视图组)创建复杂的大型布局。它与 RelativeLayout相似,其中所有的视图均根据同级视图与父布局之间的关系进行布局,但其灵活性要高于 RelativeLayou...
    继续阅读 »

    约束布局


    约束布局概念


    ConstraintLayout 可让您使用扁平视图层次结构(无嵌套视图组)创建复杂的大型布局。它与 RelativeLayout相似,其中所有的视图均根据同级视图与父布局之间的关系进行布局,但其灵活性要高于 RelativeLayout,并且更易于与 Android Studio 的布局编辑器配合使用。


    要在 ConstraintLayout 中定义某个视图的位置,您必须为该视图添加至少一个水平约束条件和一个垂直约束条件。每个约束条件均表示与其他视图、父布局或隐形引导线之间连接或对齐方式。每个约束条件均定义了视图在竖轴或者横轴上的位置;因此每个视图在每个轴上都必须至少有一个约束条件,但通常情况下会需要更多约束条件。


    当您将视图拖放到布局编辑器中时,即使没有任何约束条件,它也会停留在您放置的位置。不过,这只是为了便于修改;当您在设备上运行布局时,如果视图没有任何约束条件,则会在位置 [0,0](左上角)处进行绘制。


    从 Android Studio 2.3 起,官方的模板默认使用 ConstraintLayout


    ConstraintLayout 添加到项目中


    如需在项目中使用 ConstraintLayout,请按以下步骤操作:




    1. 确保您的 maven.google.com 代码库已在模块级 build.gradle 文件中声明


       repositories {
      google()
      }
      复制代码

      2将该库作为依赖项添加到同一个文件中,如以下示例所示。请注意,最新版本可能与示例中显示的不同:


      dependencies {
      implementation "androidx.constraintlayout:constraintlayout:2.0.4"
      }
      复制代码



    转换布局


    如需将现有布局转换为约束布局,请按以下步骤操作:




    1. Android Studio 中打开您的布局,然后点击编辑器窗口底部的 Design 标签页。




    2. ComponentTree 窗口中,右键点击该布局,然后点击 Convert layout to ConstraintLayout




    img


    Constraintlayout基本使用


    相对定位,基线



    • layout_constraintLeft_toLeftOf

    • layout_constraintLeft_toRightOf

    • layout_constraintRight_toLeftOf

    • layout_constraintRight_toRightOf

    • layout_constraintTop_toTopOf

    • layout_constraintTop_toBottomOf

    • layout_constraintBottom_toTopOf

    • layout_constraintBottom_toBottomOf

    • layout_constraintBaseline_toBaselineOf //基础线

    • layout_constraintStart_toEndOf

    • layout_constraintStart_toStartOf

    • layout_constraintEnd_toStartOf

    • layout_constraintEnd_toEndOf


    img


    居中




    • app:layout_constraintBottom_toBottomOf=parent




    • app:layout_constraintLeft_toLeftOf=parent




    • app:layout_constraintRight_toRightOf=parent




    • app:layout_constraintTop_toTopOf=parent


      img




    边距



    • android:layout_marginStart

    • android:layout_marginEnd

    • android:layout_marginLeft

    • android:layout_marginTop

    • android:layout_marginRight

    • android:layout_marginBottom


    img


    隐藏边距



    • layout_goneMarginStart

    • layout_goneMarginEnd

    • layout_goneMarginLeft

    • layout_goneMarginTop

    • layout_goneMarginRight

    • layout_goneMarginBottom


    偏移量(Bias)



    • layout_constraintHorizontal_bias

    • layout_constraintVertical_bias


    img


    img


    角度定位



    • layout_constraintCircle : 设置一个控件id

    • layout_constraintCircleRadius : 设置半径

    • layout_constraintCircleAngle :控件的角度 (in degrees, from 0 to 360)


    imgimg


    宽高约束Ratio


    Raito可以根据控件一个边尺寸比重生成另一个边的尺寸。Ration必须设置一个控件宽高尺寸为odp(MATCH_CONSTRAINT)


     //根据宽的边生成高的边按比重1:1
    <Button android:layout_width="wrap_content"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="1:1" />
    复制代码

     //高比16:9
    <Button android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="H,16:9"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
    复制代码


    app:layout_constraintVertical_chainStyle="",app:layout_constraintHorizontal_chainStyle=""



    • CHAIN_SPREAD :有点像LinearLayout里面元素设置比重1:1:1 平均分布。

    • CHAIN_SPREAD_INSIDE:链的两边元素靠边,里面元素平均分配距离。

    • CHAIN_PACKED:链的元素挨到一起。


    img


    img


    辅助工具


    优化器Optimizer


    app:layout_optimizationLevel对使用约束布局公开的优化配置项



    • none : 不启动优化

    • standard : 仅优化直接约束和屏障约束(默认)

    • direct : 优化直接约束

    • barrier : 优化屏障约束

    • chain : 优化链约束 (experimental)

    • dimensions :优化尺寸测量(experimental), 减少测量匹配约束布局的节点


    障碍Barrier


    app:barrierDirection=""


    image-20210422184856292.png


    有点像弱化(轻量级)的基础线通过设置指向的方向在此方向位置最远处生成一个虚拟线做一个阻挡作用的线。


    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="start"
    app:constraint_referenced_ids="button9,button10,button11" />

    <Button
    android:id="@+id/button9"
    android:layout_width="65dp"
    android:layout_height="50dp"
    android:text="Button"
    tools:layout_editor_absoluteX="108dp"
    tools:layout_editor_absoluteY="341dp" />

    <Button
    android:id="@+id/button10"
    android:layout_width="203dp"
    android:layout_height="49dp"
    android:text="Button"
    tools:layout_editor_absoluteX="84dp"
    tools:layout_editor_absoluteY="242dp" />

    <Button
    android:id="@+id/button11"
    android:layout_width="146dp"
    android:layout_height="49dp"
    android:text="Button"
    tools:layout_editor_absoluteX="71dp"
    tools:layout_editor_absoluteY="437dp" />


    </androidx.constraintlayout.widget.ConstraintLayout>
    复制代码

    image-20210422172658792.png


    Group


    Group可以同意控制引用的控件集合的visible状态。


    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <androidx.constraintlayout.widget.Group
    android:id="@+id/group"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:constraint_referenced_ids="button9,button10,button11" />

    <Button
    android:id="@+id/button9"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    tools:layout_editor_absoluteX="46dp"
    tools:layout_editor_absoluteY="241dp" />

    <Button
    android:id="@+id/button10"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    tools:layout_editor_absoluteX="171dp"
    tools:layout_editor_absoluteY="241dp" />

    <Button
    android:id="@+id/button11"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    tools:layout_editor_absoluteX="296dp"
    tools:layout_editor_absoluteY="237dp" />


    </androidx.constraintlayout.widget.ConstraintLayout>
    复制代码

    image-20210422160952772.png


    指示线Guideline


    //已parent start作为边开始计算


    app:layout_constraintGuide_begin=""


    //已parent end作为边开始计算


    app:layout_constraintGuide_end=""


    //百分比的位置


    app:layout_constraintGuide_percent=""


    <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintGuide_begin="196dp" />

    <androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:layout_constraintGuide_begin="336dp" />
    复制代码

    image-20210422151939691.png


    占位符Placeholder


    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.Placeholder
    android:id="@+id/placeholder"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:content="@+id/textview"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    <ImageView
    android:id="@+id/textview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_launcher_background"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:onClick="toSetGoneWithPlaceHolder"
    />


    </androidx.constraintlayout.widget.ConstraintLayout>
    复制代码

    image-20210422160434685.png


    placehoder提供了一个虚拟(空的)控件,但是它可以成一个布局已经存在的控件


    使用setContent()方法在placehoder上设置一个其他控件的id,placehoder会成为设置控件id的内容,如果显示内容的控件存在屏幕内它会从隐藏


    通过PlaceHolder的参数来显示内容.


    设置约束布局


    可以通过代码的方式设置约束布局的属性


    //创建一个Constraint数据集合
    ConstraintSet c = new ConstraintSet();
    //Copy布局的配置
    c.clone(mContext, R.layout.mine_info_view);
    //新增或者替换行为参数
    c.setVerticalChainStyle(userNameView.getId(),chain);
    //执行
    c.applyTo(this.<ConstraintLayout>findViewById(R.id.constraintlayout));

    作者:剑yyds
    链接:https://juejin.cn/post/6953937448875327495
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Jetpack Compose TriStateCheckbox,Checkbox,Switch用法详解

    这篇文章我们会通过分析TriStateCheckbox,Checkbox,Switch 他们的代码,并且讲解他们每个属性的含义以及用法。 一:TriStateCheckbox 我们来看下TriStateCheckbox的代码 @Composable fun T...
    继续阅读 »

    这篇文章我们会通过分析TriStateCheckbox,Checkbox,Switch 他们的代码,并且讲解他们每个属性的含义以及用法。


    一:TriStateCheckbox


    我们来看下TriStateCheckbox的代码


    @Composable
    fun TriStateCheckbox(
    state: ToggleableState,
    onClick: (() -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: CheckboxColors = CheckboxDefaults.colors()
    ){
    ...
    }
    复制代码


    • state 按钮的状态,有三种ToggleableState.On 中间打钩,ToggleableState.Off不选中,ToggleableState.Indeterminate 中间是横杠的。

    • onClick 点击回调

    • modifier 修饰符,我们在以前文章讲过Modifier用法详解

    • enabled 是否可用

    • interactionSource 处理状态的属性,比如按下的时候什么效果,正常时候什么效果,获取焦点时候什么效果等。类似之前再布局文件里写Selector。interactionSource.collectIsPressedAsState() 判断是否按下状态interactionSource.collectIsFocusedAsState() 判断是否获取焦点的状态interactionSource.collectIsDraggedAsState() 判断是否拖动

    • colors 设置颜色值CheckboxDefaults.colors(checkedColor,uncheckedColor,disabledColor,checkmarkColor,disabledIndeterminateColor) checkedColor表示选中时候的背景填充的颜色,uncheckedColor没有选中时候的背景颜色,disabledColor不可用时候的背景色,checkmarkColor这个指的是框里面的打钩,横杠图标的颜色。disabledIndeterminateColor当不可用时,且状态ToggleableState.Indeterminate的时候的颜色。


    比如我们举例:当是打钩选中的时候,再点击我们变成横杠,当是横杠的时候点击变成不选中,当是不选中的时候,点击变成选中。并且当属于按下状态的时候,我们的背景色改成红色,否则选中背景色是绿色。代码如下


    @Preview()
    @Composable
    fun triStateCheckboxTest(){
    val context = LocalContext.current
    val interactionSource = remember {
    MutableInteractionSource()
    }
    val pressState = interactionSource.collectIsPressedAsState()
    val borderColor = if (pressState.value) Color.Green else Color.Black
    var isCheck = remember {
    mutableStateOf(false)
    }

    var toggleState = remember {
    mutableStateOf(ToggleableState(false))
    }

    Column(modifier = Modifier.padding(10.dp,10.dp)) {
    TriStateCheckbox(
    state = toggleState.value,
    onClick = {
    toggleState.value = when(toggleState.value){
    ToggleableState.On->{
    ToggleableState.Indeterminate
    }
    ToggleableState.Off->ToggleableState.On
    else-> ToggleableState.Off
    }
    },
    modifier = Modifier.size(50.dp),
    enabled = true,
    interactionSource = interactionSource,
    colors = CheckboxDefaults.colors(
    checkedColor= if(pressState.value) Color.Red else Color.Green,
    uncheckedColor = Color.Gray,
    disabledColor = Color.Gray,
    checkmarkColor = Color.White,
    disabledIndeterminateColor = Color.Yellow
    )
    )
    }
    }
    复制代码

    二:Checkbox


    Checkbox其实内部就是new了个TriStateCheckbox。只是CheckBox没有ToggleableState.Indeterminate的情况,只有选中和不选中。代码如下:


    @Composable
    fun Checkbox(
    checked: Boolean,
    onCheckedChange: ((Boolean) -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: CheckboxColors = CheckboxDefaults.colors()
    ) {
    TriStateCheckbox(
    state = ToggleableState(checked),
    onClick = if (onCheckedChange != null) { { onCheckedChange(!checked) } } else null,
    interactionSource = interactionSource,
    enabled = enabled,
    colors = colors,
    modifier = modifier
    )
    }
    复制代码


    • checked 是否选中

    • onCheckedChange 选中改变的监听

    • modifier 修饰符

    • enabled 是否可用

    • interactionSource 跟上面一样

    • colors 跟上面一样


    举例:


    @Preview()
    @Composable
    fun checkBoxTest(){
    val context = LocalContext.current
    val interactionSource = remember {
    MutableInteractionSource()
    }
    val pressState = interactionSource.collectIsPressedAsState()
    val borderColor = if (pressState.value) Color.Green else Color.Black
    var isCheck = remember {
    mutableStateOf(false)
    }

    Column(modifier = Modifier.padding(10.dp,10.dp)) {
    Checkbox(
    checked = isCheck.value,
    onCheckedChange = {
    isCheck.value = it
    },
    modifier = Modifier.size(50.dp),
    enabled = true,
    interactionSource = interactionSource,
    colors = CheckboxDefaults.colors(
    checkedColor= Color.Red,
    uncheckedColor = Color.Gray,
    disabledColor = Color.Gray,
    checkmarkColor = Color.White,
    disabledIndeterminateColor = Color.Yellow
    )
    )
    }
    }
    复制代码

    三 Switch


    Switch 开关控件,代码如下:


    @Composable
    @OptIn(ExperimentalMaterialApi::class)
    fun Switch(
    checked: Boolean,
    onCheckedChange: ((Boolean) -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: SwitchColors = SwitchDefaults.colors()
    ) {
    ...
    }
    复制代码


    • checked 是否选中

    • onCheckedChange 选中改变的监听

    • modifier 修饰符

    • enabled 是否可用

    • interactionSource 跟上面一样

    • colors 设置各种颜色 通过SwitchDefaults.colors设置,SwitchDefaults.colors的代码如下
       @Composable
      fun colors(
      checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
      checkedTrackColor: Color = checkedThumbColor,
      checkedTrackAlpha: Float = 0.54f,
      uncheckedThumbColor: Color = MaterialTheme.colors.surface,
      uncheckedTrackColor: Color = MaterialTheme.colors.onSurface,
      uncheckedTrackAlpha: Float = 0.38f,
      disabledCheckedThumbColor: Color = checkedThumbColor
      .copy(alpha = ContentAlpha.disabled)
      .compositeOver(MaterialTheme.colors.surface),
      disabledCheckedTrackColor: Color = checkedTrackColor
      .copy(alpha = ContentAlpha.disabled)
      .compositeOver(MaterialTheme.colors.surface),
      disabledUncheckedThumbColor: Color = uncheckedThumbColor
      .copy(alpha = ContentAlpha.disabled)
      .compositeOver(MaterialTheme.colors.surface),
      disabledUncheckedTrackColor: Color = uncheckedTrackColor
      .copy(alpha = ContentAlpha.disabled)
      .compositeOver(MaterialTheme.colors.surface)
      ): SwitchColors = DefaultSwitchColors(
      checkedThumbColor = checkedThumbColor,
      checkedTrackColor = checkedTrackColor.copy(alpha = checkedTrackAlpha),
      uncheckedThumbColor = uncheckedThumbColor,
      uncheckedTrackColor = uncheckedTrackColor.copy(alpha = uncheckedTrackAlpha),
      disabledCheckedThumbColor = disabledCheckedThumbColor,
      disabledCheckedTrackColor = disabledCheckedTrackColor.copy(alpha = checkedTrackAlpha),
      disabledUncheckedThumbColor = disabledUncheckedThumbColor,
      disabledUncheckedTrackColor = disabledUncheckedTrackColor.copy(alpha = uncheckedTrackAlpha)
      )
      复制代码


      • checkedThumbColor 当没有设置checkedTrackColor的时候。表示选中的背景颜色(圆形部分,包括横的部分),当有设置checkedTrackColor则该属性只作用再圆形的部分

      • checkedTrackColor 这个是表示开关开起来的时候横线部分的背景

      • checkedTrackAlpha 这个是表示开关开起来的时候横线部分的背景的透明度

      • uncheckedThumbColor 当没有设置uncheckedTrackColor的时候。表示没有选中的背景颜色(圆形部分,包括横的部分),当有设置uncheckedTrackColor则该属性只作用再圆形的部分

      • uncheckedTrackColor 这个是表示开关关闭的时候横线部分的背景

      • uncheckedTrackAlpha 这个是表示开关关闭的时候横线部分的背景的透明度

      • disabledCheckedThumbColor 表示不可用的时候的并且选中时候背景颜色 有设置disabledCheckedTrackColor则只作用于圆形部分,没有设置disabledCheckedTrackColor则作用于圆形跟横线部分

      • disabledCheckedTrackColor 表示不可用的时候并且选中时候的横线部分的背景颜色

      • disabledUncheckedThumbColor 表示不可用的时候的并且开关关闭的时候背景颜色 有设置disabledUncheckedTrackColor则只作用于圆形部分,没有设置disabledUncheckedTrackColor则作用于圆形跟横线部分

      • disabledUncheckedTrackColor 表示不可用的时候的并且开关关闭的时候的横线部分的背景颜色




    @Preview()
    @Composable
    fun switchTest(){
    val context = LocalContext.current
    val interactionSource = remember {
    MutableInteractionSource()
    }
    val pressState = interactionSource.collectIsPressedAsState()
    val checkedThumbColor = if (pressState.value) Color.Green else Color.Red
    var isCheck = remember {
    mutableStateOf(false)
    }

    Column(modifier = Modifier.padding(10.dp,10.dp)) {
    Switch(
    checked = isCheck.value,
    onCheckedChange = {
    isCheck.value = it
    },
    // modifier = Modifier.size(50.dp),
    enabled = true,
    interactionSource = interactionSource,
    colors = SwitchDefaults.colors(checkedThumbColor= checkedThumbColor,checkedTrackColor=Color.Yellow,checkedTrackAlpha = 0.1f)
    )
    }
    }

    作者:Bug小明
    链接:https://juejin.cn/post/6954206207481479205
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Kotlin - 内部类

    内部类 内部类就是定义在类内部的类,Kotlin 中的内部类大致分为 2 种: 静态内部类 非静态内部类 静态内部类 在某个类中像普通类一样声明即可,可以认为静态内部类与外部类没有关系,只是定义在了外部类"体内"而已,在使用静态内部类时需要"带上"外部类:...
    继续阅读 »

    内部类


    内部类就是定义在类内部的类,Kotlin 中的内部类大致分为 2 种:



    • 静态内部类

    • 非静态内部类


    静态内部类


    在某个类中像普通类一样声明即可,可以认为静态内部类与外部类没有关系,只是定义在了外部类"体内"而已,在使用静态内部类时需要"带上"外部类:


    class Outer {
    val a: Int = 0

    class Inner {
    val a: Int = 5
    }
    }

    fun main() {
    val outer = Outer()
    println(outer.a)
    val inner = Outer.Inner()
    println(inner.a)
    }
    复制代码


    注意,在 Java 中,这种写法就是在定义 非静态内部类,而 静态内部类 需要在声明时使用 static 修饰。



    非静态内部类


    非静态内部类与静态内部类有的区别有:



    • 非静态内部类 会持有外部类的引用,而 静态内部类 不会(可以认为两者没有关系)。

    • 非静态内部类 使用时需要基于外部类对象(Outer().Inner()),而 静态内部类 则是基于外部类(Outer.Inner())。


    因为 非静态内部类 会持有外部类的引用,所以内部类可以直接使用外部类成员;当非静态内部类与外部类存在同名成员时,可以使用 @标记 来解决歧义:


    class Outer {
    val b: Int = 3
    val a: Int = 0

    inner class Inner {
    val a: Int = 5
    fun test() {
    println("Outer b = $b") // 3,因为持有外部类的引用,所以直接使用外部类成员
    println("Outer a = ${this@Outer.a}") // 0,使用 @Outer 指定this是外部类对象
    println("Inner a = ${this@Inner.a}") // 5,使用 @Inner 指定this是内部类对象
    println("Inner a = ${this.a}") // 5,不使用 @标记 默认this就是内部类对象
    }
    }
    }

    fun main() {
    val inner = Outer().Inner()
    inner.test()
    }
    复制代码

    为了区分 Kotlin 与 Java 中静态内部类和非静态内部类声明上的不同,以下分别使用 Kotlin 和 Java 各自编写内部类代码:


    // Kotlin - 非静态内部类
    class Outer {
    inner class Inner {...}
    }

    // Kotlin - 静态内部类
    class Outer {
    class Inner {...}
    }
    复制代码

    // Java - 非静态内部类
    public class Outer {
    public class Inner {...}
    }

    // Java - 静态内部类
    public class Outer {
    public static class Inner {...}
    }
    复制代码

    匿名内部类


    匿名内部类就是没有定义名字的内部类,一般格式为 object : 接口或(和)类,实际开发中,方法的回调接口(即 callback)一般不会专门声明一个类再创建对象来使用,而是直接使用匿名内部类:


    val textArea = TextArea()
    textArea.addTextListener(object : TextListener {
    override fun textValueChanged(e: TextEvent?) {...}
    })
    复制代码

    Kotlin 的匿名内部类很强大,在使用时,可以有多个接口或父类,如:


    val textArea = TextArea()
    textArea.addTextListener(object : TextField(), TextListener {
    override fun textValueChanged(e: TextEvent?) {...}
    })
    复制代码

    这个匿名内部类既是 TextField 的子类,也是 TextListener 的实现类,不过可能实际应用场景会比较少,了解即可。



    Java 中的匿名内部类就没这么强大了,只能是 单个 接口(或父类) 的 实现类(子类)。


    作者:GitLqr
    链接:https://juejin.cn/post/6930977250296725517
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Kotlin - 区间与数组

    区间(Range) 区间是一个数学上的概念,表示范围。 区间的声明 Kotlin 中可以使用 .. 或 until 来声明区间: val range: IntRange = 0..1024 // 闭区间[0,1024],包括1024 val rangeExcl...
    继续阅读 »

    区间(Range)


    区间是一个数学上的概念,表示范围。


    区间的声明


    Kotlin 中可以使用 ..until 来声明区间:


    val range: IntRange = 0..1024 // 闭区间[0,1024],包括1024
    val rangeExclusive: IntRange = 0 until 1024 // 半开区间[0,1024),不包括1024
    val emptyRange: IntRange = 0..-1 // 空区间[]
    复制代码

    其实这里的 .. 操作符对应的是 Int 类中的一个 rangeTo() 方法:


    /** Creates a range from this value to the specified [other] value. */
    public operator fun rangeTo(other: Byte): IntRange
    /** Creates a range from this value to the specified [other] value. */
    public operator fun rangeTo(other: Short): IntRange
    /** Creates a range from this value to the specified [other] value. */
    public operator fun rangeTo(other: Int): IntRange
    /** Creates a range from this value to the specified [other] value. */
    public operator fun rangeTo(other: Long): LongRange
    复制代码

    区间常用操作


    判断某个元素是否在区间内:


    println(range.contains(50)) // true
    println(500 in range) // true
    复制代码


    这里的 in 关键字对应的就是 IntRange 类中的 contains() 方法,因此上面的两行代码实质上是一样的。



    判断区间是否为空:


    println(rangeExclusive.isEmpty()) // false
    println(emptyRange.isEmpty()) // true
    复制代码

    对区间进行遍历:


    // 输出:0, 1, 2, 3 ..... 1020, 1021, 1022, 1023,
    for (i in rangeExclusive) {
    print("$i, ")
    }
    复制代码


    这里的 infor 配合使用,就可以实现区间的遍历效果。



    区间的类型


    所有的区间都是 ClosedRange 的子类,IntRange 最常用。通过源码不难发现,除了 IntRangeClosedRange 的子类还有 LongRangeCharRange 等等。



    以 CharRange 为例,我们还可以写出 26 个大小写字母的区间:


    // a b c d e f g h i j k l m n o p q r s t u v w x y z
    val lowerRange: CharRange = 'a'..'z'
    // A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
    val upperRange: CharRange = 'A'..'Z'
    复制代码

    数组(Array)


    数组(Array)跟数(Number)没有关系,它指的是一系列对象。


    创建数组


    创建数组一般有 2 种方式:



    1. 使用 Array 类创建数组

    2. 使用库函数 arrayOfXXX() 创建数组


    使用 Array 类创建数组


    先看看 Array 的构造函数:


    public class Array<T> {
    /**
    * Creates a new array with the specified [size], where each element is calculated by calling the specified
    * [init] function. The [init] function returns an array element given its index.
    */
    public inline constructor(size: Int, init: (Int) -> T)

    ...
    }
    复制代码

    使用 Array 创建数组,需要指定元素类型(一般情况下可以省略),有 2 个必传参数,分别是数组长度 size,和元素初始化函数 init。


    val array = Array<String>(5) { index -> "No.$index" }
    println(array.size) // 5
    for (str in array) { // No.0 No.1 No.2 No.3 No.4
    print("$str ")
    }
    复制代码


    当函数参数是最后一个形参时,可以把它写到括号外,这是 Kotlin 中的 lambda 写法,当然,你也可以不用 lambda 写法,就写在括号内:val array = Array<String>(5, { index -> "No.$index" }),关于 lambda 的相关知识在这里暂不细说。



    使用库函数 arrayOfXXX() 创建数组


    直接使用 Array 创建数组会稍稍有点麻烦,要指定个数,又要传入初始化函数, 而实际开发中,我们希望有更方便的写法来提高工作效率,Kotlin 就为此就提供了一系列创建数组的库函数 arrayOfXXX()


    val arrayOfString: Array<String> = arrayOf("我", "是", "LQR")
    val arrayOfHuman: Array<Human> = arrayOf(Boy("温和", "英俊", "浑厚"), Girl("温柔", "甜美", "动人"))
    val arrayOfInt: IntArray = intArrayOf(1, 3, 5, 7)
    val arrayOfChar: CharArray = charArrayOf('H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd')
    复制代码

    要注意,用于存放 String 类型或自定义类型的对象数组 的创建使用的是 arrayOf(),而基本数据类型数组的创建则有专门的库函数,如:intArrayOf()charArrayOf() 等。intArrayOf()charArrayOf() 等库函数是 Kotlin 为了避免基本数据装箱折箱的开销而专门创造出来的,比如:intArrayOf(1, 3, 5, 7) 创建出来的数组是 IntArray 类型,对应到 Java 中是 int[] ,而 arrayOf(1, 2, 3, 4) 创建出来的数组是 Array<Int> 类型,对应 Java 中是 Integer[]


    基本数据类型数组


    为了避免不必要的装箱和拆箱,基本数据类型的数组是定制的:



































    JavaKotlin
    int[]IntArray
    short[]ShortArray
    long[]LongArray
    float[]FloatArray
    double[]DoubleArray
    char[]CharArray


    注意:IntArrayArray<Int> 是完全不同的类型,无法直接相互转换!

    原话:Kotlin 也有无装箱开销的专门的类来表示原生类型数组: ByteArray、 ShortArray、IntArray 等等。这些类与 Array 并没有继承关系,但是它们有同样的方法属性集。



    了解更多 Kotlin 中数组的相关知识,请访问:www.kotlincn.net/docs/refere…


    数组常用操作


    可以使用 .size 获取数组长度,使用 for-in 遍历数组:


    println(arrayOfInt.size) // 4
    for (int in arrayOfInt) { // 1 3 5 7
    print("$int ")
    }
    复制代码

    Array 定义了 get 与 set 函数(按照运算符重载约定这会转变为 []),因此我们可以通过 [] 来获取或修改数组中的元素:


    println(arrayOfHuman[1]) // 我是性格温柔,长相甜美,声音动人的人
    arrayOfHuman[1] = Boy("温和1", "英俊1", "浑厚1")
    println(arrayOfHuman[1]) // 我是性格温和1,长相英俊1,声音浑厚1的人
    复制代码


    注意:自定义类型对象使用 println() 默认输出的是对象地址信息,如:com.charylin.kotlinlearn.Boy@7440e464 ,需要重写类的 toString() 方法来修改输出日志内容。



    CharArray 提供了 joinToString() 方法,用于将字符数组拼接成字符串,默认以 , 作为拼接符:


    println(arrayOfChar.joinToString()) // H, e, l, l, o, W, o, r, l, d
    println(arrayOfChar.joinToString("")) // HelloWorld
    复制代码

    可以使用 slice() 方法对数组进行切片:


    println(arrayOfInt.slice(1..2)) // [3, 5]

    作者:GitLqr
    链接:https://juejin.cn/post/6922825808671145991
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    【疯狂Android之Kotlin】关于Kotlin的高阶函数

    高阶函数介绍 概念 相信许多同学都已经知道,所谓的高阶函数就是就是方法的参数 或 返回值 是函数类型的 函数 2. 通过例子说明 List 集合的 forEach( )循环 , 该方法就是接收一个高阶函数类型变量作为参数 , 有点类似于C/C++中的函数指...
    继续阅读 »

    高阶函数介绍



    1. 概念


    相信许多同学都已经知道,所谓的高阶函数就是就是方法的参数 或 返回值 是函数类型的 函数
    2. 通过例子说明
    List 集合的 forEach( )循环 , 该方法就是接收一个高阶函数类型变量作为参数 , 有点类似于C/C++中的函数指针(指向函数的指针)


    函数作为函数参数



    1. 这里先介绍下sumBy()这个高阶函数,通过这个我们来看下如何将函数用作参数,源码如下:


    // sumBy函数的源码
    public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
    var sum: Int = 0
    for (element in this) {
    sum += selector(element)
    }
    return sum
    }
    复制代码


    1. 说明



    • 大家这里可以不必纠结inline,和sumBy函数前面的CharSequence.。因为这是Koltin中的内联函数与扩展功能。

    • 该函数返回一个Int类型的值。并且接受了一个selector()函数作为该函数的参数。其中,selector()函数接受一个Char类型的参数,并且返回一个Int类型的值。

    • 定义一个sum变量,并且循环这个字符串,循环一次调用一次selector()函数并加上sum。用作累加。其中this关键字代表字符串本身。



    1. 该函数的作用:把字符串中的每一个字符转换为Int的值,用于累加,最后返回累加的值



    • 举个例子


    val testStr = "abc"
    val sum = testStr.sumBy { it.toInt() }
    println(sum)
    复制代码


    • 输出结果


    294  // 因为字符a对应的值为97,b对应98,c对应99,故而该值即为 97 + 98 + 99 = 294
    复制代码

    函数作为函数返回值



    1. 同样这里也是使用一个lock()函数来进行讲解,先看看源码:


    fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
    return body()
    }
    finally {
    lock.unlock()
    }
    }
    复制代码


    1. 说明



    • 这其中用到了kotlin中泛型的知识点,这里暂且不考虑。同学我会在后续的文章进行介绍。

    • 从源码可以看出,该函数接受一个Lock类型的变量作为参数1,并且接受一个无参且返回类型为T的函数作为参数2.

    • 该函数的返回值为一个函数,我们可以看这一句代码return body()可以看出。



    1. 使用



    • 下面的代码都是伪代码,我就是按照官网的例子直接拿过来用的


    fun toBeSynchronized() = sharedResource.operation()
    val result = lock(lock, ::toBeSynchronized)
    复制代码

    其中,::toBeSynchronized即为对函数toBeSynchronized()的引用,其中关于双冒号::的使用在这里不做讨论与讲解。



    • 上面的写法也可以写作:


    val result = lock(lock, {sharedResource.operation()} )
    复制代码

    函数作为函数类型变量


    这里同学我使用匿名函数来简单讲解一下



    1. 函数变量需求


    在上面的forEach()函数中, 需要传入一个 (String) -> Unit 函数类型的变量, 该函数类型的函数 参数是 String 类型 , 返回值是Unit空类型 ;



    1. 普通的函数声明 : 下面定义的函数 , 参数类型是 String , 返回值是 Unit 空类型 , 这个函数是 (String) -> Unit 类型的 , 但是 study 不能当做参数传入到 forEach 方法中;list.forEach(study),是错误的调用,编译不通过 ;


    fun study(student : String) : Unit{
    println(student + " 在学习")
    }
    复制代码


    1. 函数类型变量 : 可以使用匿名函数 , 赋值给一个变量 , 然后将这个变量当做参数传递给 forEach 当做参数 ;



    • 指定变量 : 为 (String) -> Unit 类型函数指定一个引用变量 var study2 ;

    • 匿名函数 : 顾名思义,就是没有函数名称 , 省略调上面普通函数的名称,赋值给变量 ; 具体用法如下 :


    var study2 = fun (student : String) : Unit{
    println(student + " 在学习")
    }
    复制代码

    高阶函数的使用与示例




    • 在上面的这些例子中,我们出现了str.sumBy{ it.toInt },这样的写法这里主要讲高阶函数中对Lambda语法的简写。




    • 从上面的例子我们的写法应该是这样的:




    str.sumBy( { it.toInt } )
    复制代码


    1. 但是根据Kotlin中的约定,即当函数中只有一个函数作为参数,并且您使用了lambda表达式作为相应的参数,则可以省略函数的小括号()。


    故而我们可以写成:


    str.sumBy{ it.toInt }
    复制代码


    1. 还有一个约定,即当函数的最后一个参数是一个函数,并且你传递一个lambda表达式作为相应的参数,则可以在圆括号之外指定它。故而上面例2中的代码,所以我们可写成:


    val result = lock(lock){
    sharedResource.operation()
    }
    复制代码

    Kotlin常用标准高阶函数介绍


    介绍几个Kotlin中常用的标准高阶函数。如果用好下面的几个函数,能减少很多的代码量,并增加代码的可读性。下面的几个高阶函数的源码几乎上都出自Standard.kt文件


    TODO函数


    其实严格来说,该函数不是一个高阶函数,只是一个抛出异常以及测试错误的一个普通函数



    1. 看下它的源码如下:


    @kotlin.internal.InlineOnly
    public inline fun TODO(): Nothing = throw NotImplementedError()

    @kotlin.internal.InlineOnly
    public inline fun TODO(reason: String): Nothing =
    throw NotImplementedError("An operation is not implemented: $reason")
    复制代码


    • 作用:显示抛出NotImplementedError错误

    • NotImplementedError错误类继承至Java中的Error



    1. 举个例子:


    fun main(args: Array<String>) {
    TODO("测试TODO函数,是否显示抛出错误")
    }
    复制代码


    • 如果调用TODO()时,不传参数的,则会输出An operation is not implemented.


    with()函数


    对于with函数,其实简单来说就是可以让用户省略点号之前的对象引用,针对with对象,在Standard.kt中语法如下


    public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
    }
    复制代码

    看出with是一个全局函数,并没有作为任何类的扩展方法,仔细查看block会发现它又是一个带接收者的字面函数,这是一种临时的扩展方法,只在调用过程中有效,调用结束之后就不再生效,所以block就成了receiver临时的扩展函数,临时扩展函数的内部调换用上下文就是receiver对象。


    举个栗子


    class MyLogger {
    var tag: String = "TAG"

    fun e(msg: String) {
    println("$tag $i")
    }

    fun tag(tagStr: String) {
    tag = tagStr
    }
    }

    fun main(args: Array<String>) {
    val logger = MyLogger()
    with(logger) {
    tag("Kotlin")
    e("It is a good language")
    }
    }
    复制代码

    apply()函数


    关于apply()函数用于lambda表达式里切换上下文,可以查看


    public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
    }
    复制代码


    • 可以看到block这个函数,它不是一个普通函数,block其实就是带接收者的字面函数,这样传入的lambda表达式就临时扩展了T类,调用lambda表达式时的上下文就是调用方法的T类对象。


    /**
    * @author : Jacky
    * @date: : 2021/2/24
    * 数据库链接
    **/
    class DbConfig {
    var url: String = ""
    var username: String = ""
    var password: String = ""

    override fun toString(): String {
    return "url = $url, username = $username, password = $password"
    }
    }

    class DbConnection {
    fun config(conf: DbConfig) {
    println(conf)
    }
    }

    fun main(args: Array<String>) {
    val conn = DbConnection()

    //上下表达式
    conn.config(DbConfig().apply {
    url = "mysql://127.0.0.1:3306/hello"
    username = "root"
    password = "123456"
    })
    }
    复制代码

    这里使用apply函数,不但初始化了所有属性的值还可以把对象返回来用来配置数据库连接对象


    also()函数



    1. 关于T.also函数来说,它和T.apply很相似。我们先看看其源码的实现:


    public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
    }
    复制代码


    • 上面的源码在结合T.apply函数的源码我们可以看出: T.also函数中的参数block函数传入了自身对象

    • 这个函数的作用是用用block函数调用自身对象,最后在返回自身对象



    1. 下面举个例子,用实例来说明一下和apply的区别


    "kotlin".also {
    println("结果:${it.plus("-java")}")
    }.also {
    println("结果:${it.plus("-php")}")
    }

    "kotlin".apply {
    println("结果:${this.plus("-java")}")
    }.apply {
    println("结果:${this.plus("-php")}")
    }
    复制代码


    • 输出结果如下


    结果:kotlin-java
    结果:kotlin-php

    结果:kotlin-java
    结果:kotlin-php
    复制代码


    • 可以看出,他们的区别在于:



    • T.also中只能使用it调用自身,而T.apply中只能使用this调用自身。

    • 因为在源码中T.also是执行block(this)后在返回自身。而T.apply是执行block()后在返回自身。

    • 这就是为什么在一些函数中可以使用it,而一些函数中只能使用this的关键所在


    let()函数



    1. 首先查看Standard.kt源文件let函数的源代码


    public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
    }
    复制代码

    接收了调用者作为参数并且返回任意的类型的lambda表达式,最后以自己为参数调用lambda表达式


    val arr = intArrayOf(1, 2, 4, 6)
    arr.let {
    var sum = 0
    // 这个it就是arr对象
    for (i in it) {
    sum += i
    }
    println(sum)
    }
    复制代码

    对于系统标准高阶函数的总结



    • 一般我们使用最多就是also,let,apply这三个函数,一般在实际项目开发过程中都不会进行连贯着使用

    • 关于它们之间的区别,或者在什么情况下需要使用那个高阶函数,同学可以参考以下两篇文章



    自定义高阶函数



    1. 多说不易,我们可以看下一个例子:


    // 源代码
    fun test(a : Int , b : Int) : Int{
    return a + b
    }

    fun sum(num1 : Int , num2 : Int) : Int{
    return num1 + num2
    }

    // 调用
    test(10,sum(3,5)) // 结果为:18

    // lambda
    fun test(a : Int , b : (num1 : Int , num2 : Int) -> Int) : Int{
    return a + b.invoke(3,5)
    }

    // 调用
    test(10,{ num1: Int, num2: Int -> num1 + num2 }) // 结果为:18
    复制代码

    以上我们可以看到直接写死了值,这在开发中是非常不合理的,上面的例子在阐述Lambda的语法,另举一个例子



    1. 传入两个参数,并传入一个函数实现不同的逻辑


    private fun resultByOpt(num1 : Int , num2 : Int , result : (Int ,Int) -> Int) : Int{
    return result(num1,num2)
    }

    private fun testDemo() {
    val result1 = resultByOpt(1,2){
    num1, num2 -> num1 + num2
    }

    val result2 = resultByOpt(3,4){
    num1, num2 -> num1 - num2
    }

    val result3 = resultByOpt(5,6){
    num1, num2 -> num1 * num2
    }

    val result4 = resultByOpt(6,3){
    num1, num2 -> num1 / num2
    }

    println("result1 = $result1")
    println("result2 = $result2")
    println("result3 = $result3")
    println("result4 = $result4")
    }
    复制代码


    • 输出结果为


    result1 = 3
    result2 = -1
    result3 = 30
    result4 = 2
    复制代码


    • 根据传入不同的Lambda表达式,实现了两个数的(+、- 、 * 、/)。

    • 当然了,在实际的项目开发中,自己去定义高阶函数的实现是很少了,因为用系统给我们提供的高阶函数已经够用了。

    • 不过,当我们掌握了Lambda语法以及怎么去定义高阶函数的用法后。在实际开发中有了这种需求的时候也难不倒我们了。fighting


    总结



    • 既然我们选择了Kotlin这门编程语言。那其高阶函数时必须要掌握的一个知识点,因为,在系统的源码中,实现了大量的高阶函数操作。

    • 除了上面讲解到的标准高阶函数外,对于字符串(String)以及集合等,都用高阶函数去编写了他们的一些常用操作。比如,元素的过滤、排序、获取元素、分组等等

    • 对于上面讲述到的标准高阶函数,同学我要多实践,因为它们真的能在实际的项目开发中减少大量的代码编写量。

    作者:RainyJiang
    链接:https://juejin.cn/post/6933066312000389134
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Android 开发经验谈:多线程你了解多少?

     i= i+1;如上代码很简单,在单线程中i就等于i+1,执行不会出问题。但是在多线程中就会有问题。在说多线程之前我从别人的博客里摘了一段文字:大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入...
    继续阅读 »
     

    i= i+1;

    如上代码很简单,在单线程中i就等于i+1,执行不会出问题。

    但是在多线程中就会有问题。

    在说多线程之前我从别人的博客里摘了一段文字:

    大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

      也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

    当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

    也就是说有两个线程来执行这段代码,在两个线程中都有缓存,计算时都会把i的值写入到自己的缓存中,计算后再进行同步,这就导致计算结果与预期不符。

    为了解决每个线程中都用自己的缓存,于是就采用了关键字volatile

    volatile关键字就会强制变量都从主存中获取

    但是呢...由于volatile加减并非线程安全,volatile并不适用于计算。

    当然也有其专用的使用范围。

    volatile(java5):可以保证多线程下的可见性;

    读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程重新拷贝一份,这样就保证子线程的会跟主线程的一致。

    写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。

    所以在Android 单例模式中,我大多都采用volatile关键字修饰

    当然如何让i=i+1呢?

    咱们可以采用如下几个synchronized, AtomicInteger,lock

    AtomicInteger:

    一个提供原子操作的Integer的类。 一种线程安全的加减操作接口, 相比 synchroized、lock 高效.

    例子:

    private final AtomicInteger mThreadNumber = new AtomicInteger(1);
    mThreadNumber.getAndIncrement()

    这便可以保证线程安全。

    synchronized:

    synchronized是java内置关键字,对象锁,在jvm层面的锁,只能把一块代码锁着,并不能获取到锁的状态。

    悲观锁机制,线程获取的是独占锁,当一个线程进入时后,其他线程被阻塞等待。

    synchronized修饰方法时要注意以下几点:
    1. synchronized关键字不能继承。
    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这

    2. 在定义接口方法时不能使用synchronized关键字。

    3. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

    误区:

    非静态:无论你是对方法标注synchronized还是对类标注synchronized,都是对对象的锁。

    例:

    public class Test {

    int i = 0;
    public synchronized void methon1(){
    i++;
    System.out.println("methon1 "+i);
    }

    public synchronized void methon2(){
    i++;
    System.out.println("methon2 "+i);
    }

    }

    public static void main(String[] args){
    Test test = new Test();
    new Thread(new Runnable() {
    @Override
    public void run() {
    int i =0;
    do {
    test.methon1();
    i++;
    }while (i<100);
    }
    }).start();
    new Thread(new Runnable() {
    @Override
    public void run() {
    int i =0;
    do {
    test.methon2();
    i++;
    }while (i<100);
    }
    }).start();
    }
    }

    输出:
    methon2 1
    methon1 2
    methon2 3
    methon1 4
    methon2 5
    methon1 6
    methon2 7
    methon1 8
    methon2 9
    methon1 10
    methon2 11
    methon1 12
    methon2 13

    所以你要是想对某个方法单独进行方法锁,就必须锁另一个的对象,此时你用静态方法可能比较好呢。

    例:


    Object object = new Object();

    public void methon2(){
    synchronized (object){
    i++;
    System.out.println("methon2 "+i);
    }
    }

    lock:

    需要指定起始位置与终止位置 

    lock 与unlock,锁与释放锁

    一般为了健壮性在finally中调用unlock

    相比synchronized 性能低效,操作比较重量级

    乐观锁机制:假设没有锁,如果有冲突失败,则重试。

    Lock可以知道线程有没有成功获取到锁。

      1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

      2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。


    sleep/wait区别

    sleep()方法,我们首先要知道该方法是属于Thread类中的native方法。而wait()方法,则是属于Object类中的。

    wait方法需要锁来控制

    sleep 方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

    在调用sleep()方法的过程中,线程不会释放对象锁。

    而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

    wait/notify机制

    方法名称描述
    notify()随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程”
    notifyAll()使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现
    wait()使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
    wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
    wait(long,int)对于超时时间更细力度的控制,可以达到纳秒
    收起阅读 »

    Android添加联系人到通讯录

    本周项目中遇到了需要添加联系人或者添加到已有联系人的需求,联系人中需要保存的字段有很多,之前不太熟悉,在这里总结一下。 字段 联系人名字 名字不知道为什么,值设置了之后传过去没有,于是自己通过Intent最后又单独传了一次 // 联系人名字 ContentVa...
    继续阅读 »

    本周项目中遇到了需要添加联系人或者添加到已有联系人的需求,联系人中需要保存的字段有很多,之前不太熟悉,在这里总结一下。


    字段


    联系人名字

    名字不知道为什么,值设置了之后传过去没有,于是自己通过Intent最后又单独传了一次


    // 联系人名字
    ContentValues row1 = new ContentValues();String name = lastName + middleName + firstName;row1.put(ContactsContract.Data.MIMETYPE,
    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);row1.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
    name);row1.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
    firstName);row1.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
    lastName);row1.put(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME,
    middleName);
    复制代码

    联系人昵称

    // 联系人昵称
    ContentValues row2 = new ContentValues();
    row2.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
    row2.put(ContactsContract.CommonDataKinds.Nickname.NAME, nickName);
    复制代码

    联系人头像

    这里需要将图片的byte数组传进去


    ContentValues row3 = new ContentValues();
    //添加头像
    row3.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
    Bitmap bitmap = BitmapFactory.decodeFile(photoFilePath);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    row3.put(ContactsContract.CommonDataKinds.Photo.PHOTO, baos.toByteArray());
    复制代码

    联系人备注

    // 联系人备注
    ContentValues row4 = new ContentValues();
    row4.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE);
    row4.put(ContactsContract.CommonDataKinds.Note.NOTE, remark);
    复制代码

    联系人号码

    号码有很多种类型,电话,手机,传真,公司,家庭,等


    ContentValues row5 = new ContentValues();
    // 联系人的电话号码
    addPhoneNumber(row5, values, mobilePhoneNumber,
    ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);ContentValues row6 = new ContentValues();
    // 联系人的公司电话
    addPhoneNumber(row6, values, hostNumber,
    ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN);ContentValues row7 = new ContentValues();
    // 联系人的工作号码
    addPhoneNumber(row7, values, workPhoneNumber,
    ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE);ContentValues row8 = new ContentValues();
    // 联系人的工作传真
    addPhoneNumber(row8, values, workFaxNumber,
    ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK);ContentValues row9 = new ContentValues();
    // 联系人的住宅号码
    addPhoneNumber(row9, values, homePhoneNumber,
    ContactsContract.CommonDataKinds.Phone.TYPE_HOME);ContentValues row10 = new ContentValues();
    // 联系人的住宅传真
    addPhoneNumber(row10, values, homeFaxNumber,
    ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME);

    //封装的添加方法
    private void addPhoneNumber(
    ContentValues row, ArrayList<ContentValues> values, String phoneNumber, int type) {
    row.put(ContactsContract.Data.MIMETYPE,
    ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
    row.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
    row.put(ContactsContract.CommonDataKinds.Phone.TYPE, type);
    values.add(row);
    }
    复制代码

    联系人公司和职位

    // 联系人公司和职位
    ContentValues row11 = new ContentValues();
    row11.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
    row11.put(ContactsContract.CommonDataKinds.Organization.COMPANY, organization);
    row11.put(ContactsContract.CommonDataKinds.Organization.TITLE, title);
    复制代码

    网站

    // 联系人网站
    ContentValues row12 = new ContentValues();
    row12.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
    row12.put(ContactsContract.CommonDataKinds.Website.URL, url);
    复制代码

    联系人邮箱

    // 插入Email数据
    ContentValues row13 = new ContentValues();
    row13.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
    row13.put(ContactsContract.CommonDataKinds.Email.DATA, email);
    row13.put(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_WORK);
    复制代码

    联系人地址

    地址分为家庭,工作和其他。有个问题,这里分段传入之后添加完成无法显示,只能自己将国家省市街道拼接,传入到了地址字段,这样就可以显示出来,但是邮政编码是无法显示的


    //其他地址
    ContentValues row14 = new ContentValues();
    addAddress(row14, values, addressCountry, addressState, addressCity, addressStreet, addressPostalCode, ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER);
    //家庭地址
    ContentValues row15 = new ContentValues();
    addAddress(row15, values, homeAddressCountry, homeAddressState, homeAddressCity, homeAddressStreet, homeAddressPostalCode, ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME);
    //工作地址
    ContentValues row16 = new ContentValues();
    addAddress(row16, values, workAddressCountry, workAddressState, workAddressCity, workAddressStreet, workAddressPostalCode, ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK);

    //添加地址方法
    private void addAddress(ContentValues row, ArrayList<ContentValues> values, String country, String region, String city, String street, String addressPostalCode, int type) {
    row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
    row.put(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, country + region + city + street);
    row.put(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, country);
    row.put(ContactsContract.CommonDataKinds.StructuredPostal.REGION, region);
    row.put(ContactsContract.CommonDataKinds.StructuredPostal.CITY, city);
    row.put(ContactsContract.CommonDataKinds.StructuredPostal.STREET, street);
    row.put(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, addressPostalCode);
    row.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, type);
    values.add(row);
    }
    复制代码

    添加方式


    添加方式分为三种,一种是静默添加,直接存入数据库中,另外两种是跳转,直接新增或者添加到现有联系人中


    1.静默添加

    以添加名字为例,直接插入到数据库中


    // 向RawContacts.CONTENT_URI空值插入,
    // 先获取Android系统返回的rawContactId
    // 后面要基于此id插入值
    Uri rawContactUri = mActivity.getContentResolver().insert(ContactsContract.RawContacts.CONTENT_URI, values);
    long rawContactId = ContentUris.parseId(rawContactUri);
    values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
    // 内容类型
    values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
    // 联系人名字
    values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, firstName);
    values.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, lastName);
    values.put(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, middleName);
    // 向联系人URI添加联系人名字
    mActivity.getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
    复制代码

    2.跳转添加

    将上边的所有row添加到数组中,一起传递


    List<ContentValues> values = new ArrayList<>();
    //添加需要设置的数据
    ...
    Intent intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);
    intent.putExtra(ContactsContract.Intents.Insert.NAME, name);
    intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, values);
    mActivity.startActivity(intent);
    复制代码

    2.添加到现有联系人

    将上边的所有row添加到数组中,一起传递,跳转后需要选择联系人


    List<ContentValues> values = new ArrayList<>();
    //添加需要设置的数据
    ...
    Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
    intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
    intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, values);
    startActivity(intent);
    复制代码

    权限

    权限不能忘了,不然会闪退报错的,分别是联系人的读写权限


        <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
    复制代码

    总结到此为止,还算比较详细,有补充欢迎评论。


    作者:lebronzhen
    链接:https://juejin.cn/post/6953963082339057672
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Jetpack之Navigation(2)

    Jetpack之Navigation(1)2.原理 初始化过程 NavHostFragment生命周期方法 1.create—NavHostFragment的创建 在NavHostFragment.create方法 初始化Bundle,并且将graphRes...
    继续阅读 »

    Jetpack之Navigation(1)


    2.原理


    初始化过程 NavHostFragment生命周期方法


    1.create—NavHostFragment的创建


    在NavHostFragment.create方法



    1. 初始化Bundle,并且将graphResId,startDestinationArgs存储在Bundle中。

    2. new NavHostFragment()返回NavHostFragment实例。


        //NavHostFragment.java
    @NonNull
    public static NavHostFragment create(@NavigationRes int graphResId,
    @Nullable Bundle startDestinationArgs)
    {
    Bundle b = null;
    if (graphResId != 0) {
    b = new Bundle();
    b.putInt(KEY_GRAPH_ID, graphResId);
    }
    if (startDestinationArgs != null) {
    if (b == null) {
    b = new Bundle();
    }
    b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
    }

    final NavHostFragment result = new NavHostFragment();
    if (b != null) {
    result.setArguments(b);
    }
    return result;
    }
    复制代码

    2.onInflate—XML文件的解析


    主要是解析布局文件的两个属性:defaultNavHost和navGraph,并且初始化全局变量。


    NavHostFragment.onInflate方法 当Fragment以XML的方式静态加载时,最先会调用onInflate的方法(调用时机:Fragment所关联的Activity在执行setContentView时)。


        //NavHostFragment.java
    @CallSuper
    @Override
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
    @Nullable Bundle savedInstanceState)
    {
    super.onInflate(context, attrs, savedInstanceState);

    final TypedArray navHost = context.obtainStyledAttributes(attrs,
    androidx.navigation.R.styleable.NavHost);
    final int graphId = navHost.getResourceId(
    androidx.navigation.R.styleable.NavHost_navGraph, 0);
    if (graphId != 0) {
    mGraphId = graphId; //navigation的图布局
    }
    navHost.recycle();

    final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
    final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
    if (defaultHost) {
    mDefaultNavHost = true; //是否监听物理返回键
    }
    a.recycle();
    }
    复制代码

    3.onCreateNavController—创建Navigator


    在实现导航的时候,我们需要根据navigation配置文件生成NavGraph类,然后在根据每个不同的actionid,找到对应的NavDestination就可以实现页面导航跳转了。


    创建Navigator


    Navigator类的作用是:能够实例化对应的NavDestination,并且能够实现导航功能,拥有自己的回退栈。


        @CallSuper
    protected void onCreateNavController(@NonNull NavController navController) {
    navController.getNavigatorProvider().addNavigator(
    new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));

    //创建Navigator并绑定到NavigatorProvider中。
    //mNavigatorProvider是NavController中的全局变量,内部通过HashMap键值对的形式保存Navigator类。
    navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
    }
    复制代码


    1. 其中mNavigatorProvider是NavController中的全局变量,内部通过HashMap键值对的形式保存Navigator类

    2. createFragmentNavigator方法,构建了FragmentNavigator对象,其中抽象类Navigator还有个重要的实现类ActivityNavigator和NavGraphNavigator。这个两个类的对象在NavController的构造方法中被添加。


    //NavController.java
    public NavController(@NonNull Context context) {
    mContext = context;
    while (context instanceof ContextWrapper) {
    if (context instanceof Activity) {
    mActivity = (Activity) context;
    break;
    }
    context = ((ContextWrapper) context).getBaseContext();
    }
    mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
    mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    }
    复制代码

    4.onCreate—导航初始化


    无论是XML实现还是代码实现,都会执行Fragment的onCreate方法。NavController在这里被创建,并且NavHostFragment中有一个NavController对象。


       @CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
    final Context context = requireContext();

    //1.初始化NavController,NavController为导航的控制类,核心类
    mNavController = new NavHostController(context);
    mNavController.setLifecycleOwner(this);
    mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

    mNavController.enableOnBackPressed(
    mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
    mIsPrimaryBeforeOnCreate = null;
    mNavController.setViewModelStore(getViewModelStore());
    onCreateNavController(mNavController);

    Bundle navState = null;
    //2.开始恢复状态
    if (savedInstanceState != null) {
    navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
    if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
    mDefaultNavHost = true;
    getParentFragmentManager().beginTransaction()
    .setPrimaryNavigationFragment(this)
    .commit();
    }
    mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
    }

    if (navState != null) {
    mNavController.restoreState(navState);
    }

    //3.设置导航图信息
    if (mGraphId != 0) {
    mNavController.setGraph(mGraphId);
    } else {
    final Bundle args = getArguments();
    final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
    final Bundle startDestinationArgs = args != null
    ? args.getBundle(KEY_START_DESTINATION_ARGS)
    : null;
    if (graphId != 0) {
    mNavController.setGraph(graphId, startDestinationArgs);
    }
    }
    super.onCreate(savedInstanceState);
    }
    复制代码

    5.onCreateView


    NavHostFragment的视图就只有一个FragmentContainerView 继承 FrameLayout


        //NavHostFragment.java
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
    @Nullable Bundle savedInstanceState) {
    FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());

    //这行主要用于以代码方式添加fragment
    containerView.setId(getContainerId());
    return containerView;
    }
    复制代码

    6.onViewCreated


        @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    if (!(view instanceof ViewGroup)) {
    throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
    }
    //把mNavController记录在view的tag中
    Navigation.setViewNavController(view, mNavController);

    if (view.getParent() != null) {
    mViewParent = (View) view.getParent();
    if (mViewParent.getId() == getId()) {
    //把mNavController记录在view的tag中
    Navigation.setViewNavController(mViewParent, mNavController);
    }
    }
    }
    复制代码

    获取NavController


    1.获取NavController


    NavHostFragment.findNavController(fragment)
    复制代码

        //NavHostFragment.java
    @NonNull
    public static NavController findNavController(@NonNull Fragment fragment) {
    Fragment findFragment = fragment;
    while (findFragment != null) {
    if (findFragment instanceof NavHostFragment) {
    return ((NavHostFragment) findFragment).getNavController();
    }
    Fragment primaryNavFragment = findFragment.getParentFragmentManager()
    .getPrimaryNavigationFragment();
    if (primaryNavFragment instanceof NavHostFragment) {
    return ((NavHostFragment) primaryNavFragment).getNavController();
    }
    findFragment = findFragment.getParentFragment();
    }

    View view = fragment.getView();
    if (view != null) {
    return Navigation.findNavController(view);
    }

    Dialog dialog = fragment instanceof DialogFragment
    ? ((DialogFragment) fragment).getDialog()
    : null;
    if (dialog != null && dialog.getWindow() != null) {
    return Navigation.findNavController(dialog.getWindow().getDecorView());
    }

    throw new IllegalStateException("Fragment " + fragment
    + " does not have a NavController set");
    }
    复制代码

    2.Navigation中findNavController


    3.findViewNavController


    通过view.tag查找NavController。内部调用了getViewNavController方法。


    4.getViewNavController


    getViewNavController方法 通过获取view的Tag,获取NavController对象,这里的tag ID和setViewNavController都是nav_controller_view_tag。


        //Navigation.java
    @NonNull
    public static NavController findNavController(@NonNull View view) {
    //3.
    NavController navController = findViewNavController(view);
    if (navController == null) {
    throw new IllegalStateException("View " + view + " does not have a NavController set");
    }
    return navController;
    }


    @Nullable
    private static NavController findViewNavController(@NonNull View view) {
    while (view != null) {
    NavController controller = getViewNavController(view);
    if (controller != null) {
    return controller;
    }
    ViewParent parent = view.getParent();
    view = parent instanceof View ? (View) parent : null;
    }
    return null;
    }

    @Nullable
    private static NavController getViewNavController(@NonNull View view) {
    //4.这里的tag ID和setViewNavController都是nav_controller_view_tag。
    Object tag = view.getTag(R.id.nav_controller_view_tag);
    NavController controller = null;
    if (tag instanceof WeakReference) {
    controller = ((WeakReference) tag).get();
    } else if (tag instanceof NavController) {
    controller = (NavController) tag;
    }
    return controller;
    }
    复制代码

    导航navigate


    navigate


    在构建和获取到NavController对象以及NavGraph之后。下面是使用它来实现真正的导航了。下面从navigate开始分析。在navigate方法内部会查询到NavDestination,然后根据不同的Navigator实现页面导航。


    public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
    @Nullable Navigator.Extras navigatorExtras) {
    NavDestination currentNode = mBackStack.isEmpty()
    ? mGraph
    : mBackStack.getLast().getDestination();
    if (currentNode == null) {
    throw new IllegalStateException("no current navigation node");
    }
    @IdRes int destId = resId;
    //2.根据id,获取对应的NavAction
    final NavAction navAction = currentNode.getAction(resId);
    Bundle combinedArgs = null;
    if (navAction != null) {
    if (navOptions == null) {
    navOptions = navAction.getNavOptions();
    }
    //3.通过NavAction获取目的地id
    destId = navAction.getDestinationId();
    Bundle navActionArgs = navAction.getDefaultArguments();
    if (navActionArgs != null) {
    combinedArgs = new Bundle();
    combinedArgs.putAll(navActionArgs);
    }
    }

    if (args != null) {
    if (combinedArgs == null) {
    combinedArgs = new Bundle();
    }
    combinedArgs.putAll(args);
    }

    if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
    popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
    return;
    }

    if (destId == 0) {
    throw new IllegalArgumentException("Destination id == 0 can only be used"
    + " in conjunction with a valid navOptions.popUpTo");
    }
    //4.利用目的地ID属性,通过findDestination方法,找到准备导航的目的地
    NavDestination node = findDestination(destId);
    if (node == null) {
    final String dest = NavDestination.getDisplayName(mContext, destId);
    if (navAction != null) {
    throw new IllegalArgumentException("Navigation destination " + dest
    + " referenced from action "
    + NavDestination.getDisplayName(mContext, resId)
    + " cannot be found from the current destination " + currentNode);
    } else {
    throw new IllegalArgumentException("Navigation action/destination " + dest
    + " cannot be found from the current destination " + currentNode);
    }
    }
    //5.开始导航
    navigate(node, combinedArgs, navOptions, navigatorExtras);
    }


    private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
    @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    boolean popped = false;
    boolean launchSingleTop = false;
    if (navOptions != null) {
    if (navOptions.getPopUpTo() != -1) {
    popped = popBackStackInternal(navOptions.getPopUpTo(),
    navOptions.isPopUpToInclusive());
    }
    }
    Navigator navigator = mNavigatorProvider.getNavigator(
    node.getNavigatorName());
    Bundle finalArgs = node.addInDefaultArgs(args);
    NavDestination newDest = navigator.navigate(node, finalArgs,
    navOptions, navigatorExtras);
    ...
    }
    复制代码

    findDestination


    如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。


      NavDestination findDestination(@IdRes int destinationId) {
    if (mGraph == null) {
    return null;
    }
    if (mGraph.getId() == destinationId) {
    return mGraph;
    }
    //1.如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
    NavDestination currentNode = mBackStack.isEmpty()
    ? mGraph
    : mBackStack.getLast().getDestination();
    NavGraph currentGraph = currentNode instanceof NavGraph
    ? (NavGraph) currentNode
    : currentNode.getParent();
    return currentGraph.findNode(destinationId);
    }
    复制代码

    FragmentNavigator的实现


    通过以上的分析,又来到了Navigator 的子类FragmentNavigator类。下面来看看FragmentNavigator.navigate的方法。
    (1)调用instantiateFragment,通过反射机制构建Fragment实例

    (2)处理进出场等动画逻辑

    (3)最终调用FragmentManager来处理导航逻辑。


    @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
    @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    if (mFragmentManager.isStateSaved()) {
    Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
    + " saved its state");
    return null;
    }
    String className = destination.getClassName();
    if (className.charAt(0) == '.') {
    className = mContext.getPackageName() + className;
    }
    //通过反射机制构建Fragment实例
    final Fragment frag = instantiateFragment(mContext, mFragmentManager,
    className, args);
    frag.setArguments(args);
    final FragmentTransaction ft = mFragmentManager.beginTransaction();

    //处理动画逻辑
    int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
    int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
    int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
    int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
    if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
    enterAnim = enterAnim != -1 ? enterAnim : 0;
    exitAnim = exitAnim != -1 ? exitAnim : 0;
    popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
    popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
    ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
    }

    //FragmentManager来处理导航逻辑
    ft.replace(mContainerId, frag);
    ft.setPrimaryNavigationFragment(frag);

    final @IdRes int destId = destination.getId();
    final boolean initialNavigation = mBackStack.isEmpty();
    final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
    && navOptions.shouldLaunchSingleTop()
    && mBackStack.peekLast() == destId;

    boolean isAdded;
    if (initialNavigation) {
    isAdded = true;
    } else if (isSingleTopReplacement) {
    if (mBackStack.size() > 1) {
    mFragmentManager.popBackStack(
    generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
    FragmentManager.POP_BACK_STACK_INCLUSIVE);
    ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
    }
    isAdded = false;
    } else {
    ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
    isAdded = true;
    }
    if (navigatorExtras instanceof Extras) {
    Extras extras = (Extras) navigatorExtras;
    for (Map.Entry sharedElement : extras.getSharedElements().entrySet()) {
    ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
    }
    }
    ft.setReorderingAllowed(true);
    ft.commit();
    if (isAdded) {
    mBackStack.add(destId);
    return destination;
    } else {
    return null;
    }
    }
    复制代码

    ActivityNavigator


    ActivityNavigator最终也是调用了startActivity方法,请自己阅读源码。


        @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
    @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    ....
    if (navigatorExtras instanceof Extras) {
    Extras extras = (Extras) navigatorExtras;
    ActivityOptionsCompat activityOptions = extras.getActivityOptions();
    if (activityOptions != null) {
    ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
    } else {
    mContext.startActivity(intent);
    }
    } else {
    mContext.startActivity(intent);
    }
    ...
    }
    复制代码

    3.总结



    1. NavHostFragment 作为导航载体,在Activity的layout文件里被引用(或者在代码中动态),并且持有导航控制类NavController引用。

    2. NavController 将导航任务委托给Navigator类,Navigator类有两个重要的子类FragmentNavigator和ActivityNavigator子类。NavController类持有NavInflater类引用。

    3. NavInflater 负责解析Navgation文件,负责构建NavGraph导航图。

    4. NavDestination 存有各个目的地信息,在FragmentNavigator和ActivityNavigator内部分别对应一个Destination类,该类继承NavDestination。

    5. 在页面导航时,fragment的操作还是交由FragmentManager在操作,activity交由startActivity执行。


    image-20210421174444170


    作者:贾里
    链接:https://juejin.cn/post/6953624951194664968
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »