Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

使用myeclipse编写Hibernate小栗子

$
0
0

Hibernate是java领域的一款开源的ORM框架技术

Hibernate对JDBC进行了非常轻量级的对象封装

准备前的工作

导入Hibernate必须的jar包 hibernate-core.zip

导入MySQL的JDBC驱动 mysql-connector-java-5.1.7-bin.jar

导入Junit4的jar包 Junit-4.10.jar


我们可以把上面的jar包 添加自定义用户类库,方便每次导入jar包。添加后最好就不要移动这些jar包的位置了哦  不然你就要重新修改用户类库了

基本步骤

  • 在MyEclipse Datebase Explorer 创建数据库连接
  • 创建hibernate的配置文件
  • 根据数据库表自动生成持久化类和对象关系映射文件
  • 使用junit通过Hibernate API编写访问数据库的代码

在MyEclipse Datebase Explorer 创建数据库连接

点击myeclipse的右上角图标open Perspective  选择MyEclipse Database Explorer

在DB Browser界面鼠标右键new 新建数据库驱动


我用的是mysql数据库,使用其它数据库请相应修改,把数据库帐号密码 驱动添加后点击finsh即可。

创建hibernate的配置文件

点击你创建的java项目 右键 MyEclipse->Add Hibernate Capabilities 


导入自定义类库的三个jar包 next


这里就是生成的hibernate配置文件位置和名称  点击next


选择我们刚刚在MyEclipse Database Explorer新建的数据库  点击next


create SessionFactory class是否创建SessionFactory类我取消勾选了  因为我在下面自己实例化了

点击finish即成功创建了hibernate的配置文件hibernate.cfg.xml

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<!-- Generated by MyEclipse Hibernate Tools.                   -->
<hibernate-configuration>

    <session-factory>
        <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/susu</property>
        <property name="connection.username">root</property>
        <property name="connection.password">123</property>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="myeclipse.connection.profile">MySQLDriver</property>
    
    </session-factory>

</hibernate-configuration>

根据数据库表自动生成持久化类和对象关系映射文件

点击myeclipse的右上角图标open Perspective  选择MyEclipse Database Explorer

在DB Browser界面打开我们创键的数据库连接

选中我们需要生成持久化类的表,右键选择Hibernate Reverse Engineering 

 

java src folder    持久化类生成的项目位置

java package     持久化类的包名

点击next


id generator  选择native  点击finish

在你选择的项目里即成功生成了持久化类和对象关系映射文件

持久化类

package com.susu.entity;

import java.util.Date;

/**
 * Students entity. @author MyEclipse Persistence Tools
 */

public class Students implements java.io.Serializable {

	// Fields

	private Integer id;
	private String name;
	private String gender;
	private Date date;
	private String address;

	// Constructors

	/** default constructor */
	public Students() {
	}

	/** full constructor */
	public Students(String name, String gender, Date date, String address) {
		this.name = name;
		this.gender = gender;
		this.date = date;
		this.address = address;
	}

	// Property accessors

	public Integer getId() {
		return this.id;
	}

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

	public String getName() {
		return this.name;
	}

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

	public String getGender() {
		return this.gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

	public Date getDate() {
		return this.date;
	}

	public void setDate(Date date) {
		this.date = date;
	}

	public String getAddress() {
		return this.address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

}

对象关系映射文件

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<!-- 
    Mapping file autogenerated by MyEclipse Persistence Tools
-->
<hibernate-mapping>
    <class name="com.susu.entity.Students" table="students" catalog="susu">
        <id name="id" type="java.lang.Integer">
            <column name="id" />
            <generator class="native" />
        </id>
        <property name="name" type="java.lang.String">
            <column name="name" length="20" />
        </property>
        <property name="gender" type="java.lang.String">
            <column name="gender" length="2" />
        </property>
        <property name="date" type="java.util.Date">
            <column name="date" length="0" />
        </property>
        <property name="address" type="java.lang.String">
            <column name="address" length="30" />
        </property>
    </class>
</hibernate-mapping>


这个时候会在hibernate.cfg.xml 新增一个映射  指向刚刚生成的关系映射文件

<mapping resource="com/susu/entity/Students.hbm.xml" />

通常我们还会加上

                <property name="show_sql">true</property>
		<property name="format_sql">true</property>
		<property name="hbm2ddl.auto">update</property>

在控制台输出sql 语句并对Sql语句进行排版

具体作用请看这篇文章点击打开链接

使用junit通过Hibernate API编写访问数据库的代码

import java.util.Date;
import java.util.Properties;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.susu.entity.Students;

public class StudentsTest {
	private SessionFactory sessionFactory;
	private Session session;
	private Transaction transaction;

	@Before
	public void init() {
		// 创建配置对象
		Configuration config = new Configuration().configure();
		// 创建服务注册对象
		ServiceRegistry serviceRegistry = new ServiceRegistryBuilder()
				.applySettings(config.getProperties()).buildServiceRegistry();
		// 创建会话工厂对象
		sessionFactory = config.buildSessionFactory(serviceRegistry);
		// 会话对象
		session = sessionFactory.openSession();
		// 开启事务
		transaction = session.beginTransaction();
	}

	@Test
	public void testSaveStudents() {
		Students stu=new Students("张三", "男", new Date(), "河南省信阳市");
		session.save(stu);
	}

	@After
	public void destroy() {
		transaction.commit();// 提交事务
		session.close();
		sessionFactory.close();
	}
}

运行结果:


打开mysql数据库会发现我们成功在Students表中插入了一条数据。










作者:su20145104009 发表于2016/11/21 21:24:12 原文链接
阅读:41 评论:0 查看评论

Unity3D开发小贴士(十三)Inspector中使用属性

$
0
0

我们知道Unity的组件类中,public的变量可以直接在Inspector中编辑,而其他访问级别的变量,可以为它们添加[SerializeField]特性来实现同样的效果。但是如果我们希望一个变量改变的时候调用一个属性(Property)的set访问器该怎样实现呢?


首先我们需要自定义一个特性(参考C#语法小知识(七)特性):

using UnityEngine;
using System.Collections;
using System;

[AttributeUsage(AttributeTargets.Field)]  
public class SetPropertyAttribute : PropertyAttribute {
	public string propertyName { get; private set;}
	public bool dirty { get; set;}
	public SetPropertyAttribute(string propertyName_)
	{
		propertyName = propertyName_;
	}
}

为这个特性添加了一个特性AttributeUsage,表示它的用途,只用在Field(字段/成员变量)上。

SetPropertyAttribute继承自PropertyAttribute,这是UnityEngine命名空间下的一个特性,用来在编辑器的PropertyDrawer中使用,也就是我们下面要实现的。

using UnityEditor;
using UnityEngine;
using System;
using System.Collections;
using System.Reflection;

[CustomPropertyDrawer(typeof(SetPropertyAttribute))]
public class AccessPropertyDrawer : PropertyDrawer {

	public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
	{
		// Rely on the default inspector GUI
		EditorGUI.BeginChangeCheck ();
		EditorGUI.PropertyField(position, property, label);

		// Update only when necessary
		SetPropertyAttribute setProperty = attribute as SetPropertyAttribute;
		if (EditorGUI.EndChangeCheck ()) {
			setProperty.dirty = true;

		} else if (setProperty.dirty) {
			object obj = property.serializedObject.targetObject;
			Type type = obj.GetType();
			PropertyInfo pi = type.GetProperty(setProperty.propertyName);
			if (pi == null)
			{
				Debug.LogError("Invalid property name: " + setProperty.propertyName + "\nCheck your [SetProperty] attribute");
			}
			else
			{
				pi.SetValue(obj, fieldInfo.GetValue(obj), null);
			}
			setProperty.dirty = false;
		}
	}
}
CustomPropertyDrawer用了指定这个类是SetPropertyAttribute的Drawer,所有添加了[SetProperty]特性的字段都会经过这个Drawer来绘制。

BeginChangeCheck和EndChangeCheck之间使用PropertyField来绘制字段,就可以检查字段的值是否发生了变化。

但是变化并不会立即体现出来,需要等下次绘制的时候再调用属性的set访问器。

最后我们测试一下:

using UnityEngine;
using System.Collections;

public class NewBehaviourScript : MonoBehaviour {

	[SerializeField]
	[SetProperty("Test")]
	protected int _test;

	public int Test
	{
		get { return _test;}
		set { _test = value;
			Debug.Log ("Test"+_test);}
	}

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}


(注:本文参考自https://github.com/LMNRY/SetProperty哈哈哈,其实就是抄的……

作者:ecidevilin 发表于2016/11/21 22:05:43 原文链接
阅读:41 评论:0 查看评论

React Native Android 源码框架浅析(主流程及 Java 与 JS 双边通信)

$
0
0

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

1 背景

有了前面《React Native Android 从学车到补胎和成功发车经历》《React Native Android Gradle 编译流程浅析》两篇文章的学习我们 React Native 已经能够基本接入处理一些事情了,那接下来的事情就是渐渐理解 RN 框架的一些东西,以便裁剪和对 RN 有个更深入的认识,所以本篇总结了我这段时间阅读源码的一些感触,主要总结了 React Native 启动流程、JS 调用 Java 流程、Java 调用 JS 流程。

涉及到源码分析了,所以有必要先交代下相关源码版本,以便引来不必要疑惑,如下:

"dependencies": {
  "react": "15.3.2",
  "react-native": "0.37.0"
}

首先通过前面的踩坑经历和编译流程浅析(编译流程已经暴露很多细节)我们能意识到 React Native 的大致框架流程应该是如下这样的:

这里写图片描述

也就是说其实需要我们编写代码是 Java 端(少)和 JS 端(多),其他的基本不变的,作为桥梁的核心是 C/C++ 来处理的,同时 JS(JSX)代码又是通过 Virtual DOM 来进行虚拟适应的,所以才有了 React Native 官方放话的 Learn once, do anywhere. 之说。下面我们就来解析下这个神奇的 React Native Android 框架吧。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

2 RN 启动流程框架浅析

还记得我们在《React Native Android 从学车到补胎和成功发车经历》中是怎么集成的 RN 吗?集成 RN 无非就是通过继承 ReactActivity 或者自己通过 ReactRootView 进行处理,但是实质都是触发了 ReactRootView 的 startReactApplication 方法,所以我们整个启动流程的核心入口就是这玩意;下面为了一致,我们直接从 ReactActivityDelegate 类的 onCreate 方法进行启动分析(分析源码本来就比较枯燥,坚持看下去收获是巨大的),如下:

public class ReactActivityDelegate {
  protected void onCreate(Bundle savedInstanceState) {
    //权限弹窗判断
    if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
      ......
    }
    //启动流程一定会执行的,mMainComponentName为我们设置的,与JS边保持一致
    if (mMainComponentName != null) {
      loadApp(mMainComponentName);
    }
    ......
  }

  protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    //创建一个ReactRootView,实质是一个FrameLayout
    mReactRootView = createRootView();
    //重磅启动流程核心方法!!!!!!
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    //把View设置进Activity
    getPlainActivity().setContentView(mReactRootView);
  }
}

可以看见,ReactActivityDelegate 只是一个抽出来的封装,上面的实质就是 new 了一个 ReactRootView(实质是 Android 的 FrameLayout),接着调用 ReactRootView 的 startReactApplication 方法,完事就像常规 Android 代码一样直接通过 Activity 的 setContentView 方法把 View 设置进去。所以可以看出来,RN 的神秘之处一定在于 ReactRootView 中,Activity 对于 RN 来说只是为了让 RN 依附符合 Android 的框架而已,所以说,说白了 RN 依旧是标准 Android,因此在我们集成开发中我们可以选择整个界面(包含多级跳转)都用 React Native 实现,或者一个 Android 现有界面中部分采用 React Native 实现,因为这货就是一个 View,爱咋咋地,具体如下所示:

这里写图片描述

既然明白了 RN 就是个 View,那就接着看看 ReactRootView 呗,如下:

/**
 React Native 的 Root View,负责监听标准 Android 的 View 相关各种东东及事件分发和子 View 渲染等。
 */
public class ReactRootView extends SizeMonitoringFrameLayout implements RootView {
  public void startReactApplication(ReactInstanceManager reactInstanceManager, String moduleName) {
    startReactApplication(reactInstanceManager, moduleName, null);
  }

  public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle launchOptions) {
    UiThreadUtil.assertOnUiThread();
    ......
    //标记判断,初始化会走进来的 
    if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
         mReactInstanceManager.createReactContextInBackground();
    }

    // We need to wait for the initial onMeasure, if this view has not yet been measured, we set which
    // will make this view startReactApplication itself to instance manager once onMeasure is called.
    if (mWasMeasured) {
      attachToReactInstanceManager();
    }
  }
}

可以看见,ReactRootView 果然是个牛逼的类,我也不多解释了,大段的英文注释已经交代很清楚用途和地位了,我们直接看上面代码的 startReactApplication 方法吧,可以看见他又调用了一个三个参数的同名方法,具体这三个参数来历如下(也是我们自己集成 RN 时手动 builder 模式创建的):

1. reactInstanceManager: 大内总管接口类,提供一个构造者模式的初始化 Builder,实现类是 XReactInstanceManagerImpl,这类也是我们在集成 RN 时 new ReactRootView 的之前自己创建的。
2. moduleName: 与 JS 代码约定的 String 类型识别 name,JS 端通过 AppRegistry.registerComponent 方法设置这个 name,Java 端重写基类的 getMainComponentName 方法设置这个 name,这样两边入口就对上了。
3. launchOptions: 这里默认是 null 的,如果自己不继承 ReactActivity 而自己实现的话可以通过这个参数在 startActivity 时传入一些参数到 JS 代码,用来依据参数初始化 JS 端代码。

这些参数都初始化传递好了以后,可以看见接着调用了 mReactInstanceManager 的 createReactContextInBackground 方法,mReactInstanceManager 就是上面说的第一个参数,实质是通过一个构造者模式创建的,实现类是 XReactInstanceManagerImpl,所以我们直接跳到 XReactInstanceManagerImpl 的 createReactContextInBackground 方法看看,如下:

  public void createReactContextInBackground() {
    ......
    recreateReactContextInBackgroundInner();
  }

  private void recreateReactContextInBackgroundInner() {
    UiThreadUtil.assertOnUiThread();

    if (mUseDeveloperSupport && mJSMainModuleName != null) {
        //如果是 dev 模式,BuildConfig.DEBUG=true就走这里,在线更新bundle,手机晃动出现调试菜单等等。
        //这个路线属于RN调试流程原理,后面再写文章分析,这里我们抓住主线分析
      ......
      return;
    }
    //非调试模式,即BuildConfig.DEBUG=false时执行
    recreateReactContextInBackgroundFromBundleLoader();
  }

  private void recreateReactContextInBackgroundFromBundleLoader() {
    //厉害了,word哥,在后台创建ReactContext,两个参数是重点。
    //mJSCConfig.getConfigMap()默认是一个WritableNativeMap,在前面通过构造模式构造时通过Builder类的set方法设置。
    //mJSBundleLoader是在前面通过构造模式构造时通过Builder类的多个setXXX方法均可设置的,
    //最终在Builder中build方法进行判断处理,你可以自定义Loader,或者按照build方法规则即可,
    //默认是JSBundleLoader.createAssetLoader静态方法返回的JSBundleLoader抽象类的实现类。
    //自定义热更新时setJSBundleFile方法参数就是巧妙的利用这里是走JSBundleLoader.createAssetLoader还是JSBundleLoader.createFileLoader!!!!!!
    recreateReactContextInBackground(
        new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()),
        mBundleLoader);
  }

  private void recreateReactContextInBackground(
      JavaScriptExecutor.Factory jsExecutorFactory,
      JSBundleLoader jsBundleLoader) {
    UiThreadUtil.assertOnUiThread();
    //封装一把,把两个参数封装成ReactContextInitParams对象
    ReactContextInitParams initParams =
        new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
    if (mReactContextInitAsyncTask == null) {
        //初始化进来一定会走啦,这货不就是创建一个AsyncTask,然后执行,同时传递封装的参数initParams给task。
      // No background task to create react context is currently running, create and execute one.
      mReactContextInitAsyncTask = new ReactContextInitAsyncTask();
      mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, initParams);
    } else {
      // Background task is currently running, queue up most recent init params to recreate context
      // once task completes.
      mPendingReactContextInitParams = initParams;
    }
  }

通过上面注释可以看见,我们《React Native Android 从学车到补胎和成功发车经历 #3-5 RN 集成后热更新核心思路》的热更新面纱也是从这个地方开始揭晓的,ReactInstanceManager 的 setJSBundleFile 如下:

    /**
     * Path to the JS bundle file to be loaded from the file system.
     *
     * Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle"}
     */
    public Builder setJSBundleFile(String jsBundleFile) {
      if (jsBundleFile.startsWith("assets://")) {
        mJSBundleAssetUrl = jsBundleFile;
        mJSBundleLoader = null;
        return this;
      }
      return setJSBundleLoader(JSBundleLoader.createFileLoader(jsBundleFile));
    }

我们先记住这个提示,后面再边分析主加载流程边插入介绍热更新的原理,所以我们还是把思路先回到 XReactInstanceManagerImpl 内部类的 ReactContextInitAsyncTask 上,如下:

  private final class ReactContextInitAsyncTask extends
      AsyncTask<ReactContextInitParams, Void, Result<ReactApplicationContext>> {
    ......
    @Override
    protected Result<ReactApplicationContext> doInBackground(ReactContextInitParams... params) {
      ......
      try {
        //异步执行的重量级核心方法createReactContext,创建ReactContext
        JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();
        return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader()));
      } catch (Exception e) {
        // Pass exception to onPostExecute() so it can be handled on the main thread
        return Result.of(e);
      }
    }

    @Override
    protected void onPostExecute(Result<ReactApplicationContext> result) {
      try {
        //回到主线程执行的重量级核心方法setupReactContext,设置ReactContext相关
        setupReactContext(result.get());
      } catch (Exception e) {
        mDevSupportManager.handleException(e);
      } finally {
        mReactContextInitAsyncTask = null;
      }
        ......
    }
    ......
  }

可以看见,这就是典型的 AsyncTask 用法,我们先关注 doInBackground 方法,onPostExecute 方法等会回头再看;doInBackground 中首先把上面封装的 ReactContextInitParams 对象里 JavaScriptExecutor.Factory 工厂对象拿到,接着调用了工厂类的 create 方法创建 JavaScriptExecutor 抽象类的实现类 JSCJavaScriptExecutor 对象(因为上面分析 recreateReactContextInBackground 方法时第一个参数传入的是 new JSCJavaScriptExecutor.Factory(mJSCConfig.getConfigMap()))。接着往下执行了 createReactContext 方法,两个参数分别是前面封装的 ReactContextInitParams 对象中的 JSCJavaScriptExecutor 实例和 JSBundleLoader.createAssetLoader 静态方法创建的匿名内部类 JSBundleLoader 对象(热更新的话可能是另一个 Loader,参见前面分析);createReactContext 方法如下(有点长,但是句句核心啊):

  private ReactApplicationContext createReactContext(
      JavaScriptExecutor jsExecutor,
      JSBundleLoader jsBundleLoader) {
    ......
    //这货默认不就是上面刚刚分析的"assets://" + bundleAssetName么
    mSourceUrl = jsBundleLoader.getSourceUrl();
    List<ModuleSpec> moduleSpecs = new ArrayList<>();
    Map<Class, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
    //!!!Js层模块注册表,通过它把所有的JavaScriptModule注册到CatalystInstance。我们自定义的继承JavaScriptModule接口的Java端也是通过他来管理。
    JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();
    //ContextWrapper封装类,其实就是getApplicationContext的封装,用在ReactContext中
    final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
    //如果是开发模式下ReactApplicationContext中有崩溃就捕获后交给mDevSupportManager处理(出错时弹个红框啥玩意的都是这货捕获的功劳)
    if (mUseDeveloperSupport) {
        //mDevSupportManager实例对象来源于XReactInstanceManagerImpl构造方法中一个工厂方法,实质由useDeveloperSupport决定DevSupportManager是哪个实例。
        //非开发模式情况下mDevSupportManager为DisabledDevSupportManager实例,开发模式下为DevSupportManagerImpl实例。
     reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
    }
    ......
    try {
        //创建CoreModulesPackage(ReactPackage),RN framework的核心Module Package,主要通过createNativeModules、createJSModules和createViewManagers等方法创建本地模块,JS模块及视图组件等。
        //CoreModulesPackage封装了通信、调试等核心类。
      CoreModulesPackage coreModulesPackage =
        new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider);
        //当我们设置mLazyNativeModulesEnabled=true(默认false)后启动可以得到延迟加载,感觉没啥卵用,没整明白有何用意。
        //拼装来自coreModulesPackage的各种module了,JS的直接add进了jsModulesBuilder映射表、Native的直接保存在了moduleSpecs、reactModuleInfoMap中。
      processPackage(
        coreModulesPackage,
        reactContext,
        moduleSpecs,
        reactModuleInfoMap,
        jsModulesBuilder);
    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
    }
    //加载我们自定义的ReactPackage,譬如自己封装的和MainReactPackage等,mPackages就来源于我们自己定义的;整个过程同上CoreModulesPackage,进行各种拼装module。
    // TODO(6818138): Solve use-case of native/js modules overriding
    for (ReactPackage reactPackage : mPackages) {
      Systrace.beginSection(
          TRACE_TAG_REACT_JAVA_BRIDGE,
          "createAndProcessCustomReactPackage");
      try {
        processPackage(
          reactPackage,
          reactContext,
          moduleSpecs,
          reactModuleInfoMap,
          jsModulesBuilder);
      } finally {
        Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      }
    }
    ......
    //!!!Java层模块注册表,通过它把所有的NativeModule注册到CatalystInstance。我们自定义的继承NativeModule接口的Java端也是通过他来管理。
    NativeModuleRegistry nativeModuleRegistry;
    try {
        //new一个NativeModuleRegistry,其管理了NativeModule和OnBatchCompleteListener列表(JS调用Java结束时的回掉管理)。
       nativeModuleRegistry = new NativeModuleRegistry(moduleSpecs, reactModuleInfoMap);
    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END);
    }
    //依据外面是否设置mNativeModuleCallExceptionHandler异常捕获实现来决定exceptionHandler是使用外面的还是DevSupportManager。
    NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null
        ? mNativeModuleCallExceptionHandler
        : mDevSupportManager;
    //!!!重点创建CatalystInstance的CatalystInstanceImpl实现实例
    CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder()
        .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
        .setJSExecutor(jsExecutor)
        .setRegistry(nativeModuleRegistry)
        .setJSModuleRegistry(jsModulesBuilder.build())
        .setJSBundleLoader(jsBundleLoader)
        .setNativeModuleCallExceptionHandler(exceptionHandler);

    ......
    final CatalystInstance catalystInstance;
    try {
      catalystInstance = catalystInstanceBuilder.build();
    } finally {
      Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      ReactMarker.logMarker(CREATE_CATALYST_INSTANCE_END);
    }

    if (mBridgeIdleDebugListener != null) {
      catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
    }
    //关联reactContext与catalystInstance
    reactContext.initializeWithInstance(catalystInstance);
    //通过catalystInstance加载js bundle文件
    catalystInstance.runJSBundle();

    return reactContext;
  }

可以发现,上面这段代码做的事情真特么多,不过总的来说 createReactContext() 方法做的都是一些取数据组表放表的过程,核心就是通过 ReactPackage 实现类的 createNativeModules()、createJSModules() 等方法把所有 NativeModule 包装后放入 NativeModuleRegistry 及 JavaScriptModule 包装后放入 JavaScriptModuleRegistry,然后把这两张映射表交给 CatalystInstanceImpl,同时包装创建 ReactContext 对象,然后通过 CatalystInstanceImpl 的 runJSBundle() 方法把 JS bundle 文件的 JS 代码加载进来等待 Task 结束以后调用 JS 入口进行渲染 RN。既然这样就去看看 CatalystInstanceImpl 的 build 方法中调用的 CatalystInstanceImpl 构造方法到底干了哪些鸟事,如下:

public class CatalystInstanceImpl implements CatalystInstance {
    ......
    // C++ parts
  private final HybridData mHybridData;
  private native static HybridData initHybrid();

  private CatalystInstanceImpl(
      final ReactQueueConfigurationSpec ReactQueueConfigurationSpec,
      final JavaScriptExecutor jsExecutor,
      final NativeModuleRegistry registry,
      final JavaScriptModuleRegistry jsModuleRegistry,
      final JSBundleLoader jsBundleLoader,
      NativeModuleCallExceptionHandler nativeModuleCallExceptionHandler) {
    FLog.d(ReactConstants.TAG, "Initializing React Xplat Bridge.");
    //native C++方法,用来初始化JNI相关状态然后返回mHybridData。
    mHybridData = initHybrid();
    //创建ReactNative的三个线程nativeModulesThread和jsThread、uiThread,都是通过Handler来管理的。
    mReactQueueConfiguration = ReactQueueConfigurationImpl.create(
        ReactQueueConfigurationSpec,
        new NativeExceptionHandler());
    mBridgeIdleListeners = new CopyOnWriteArrayList<>();
    mJavaRegistry = registry;
    mJSModuleRegistry = jsModuleRegistry;
    mJSBundleLoader = jsBundleLoader;
    mNativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
    mTraceListener = new JSProfilerTraceListener(this);
    //native C++方法,用来初始化Bridge。
    initializeBridge(
      new BridgeCallback(this),
      jsExecutor,
      mReactQueueConfiguration.getJSQueueThread(),
      mReactQueueConfiguration.getNativeModulesQueueThread(),
      mJavaRegistry.getModuleRegistryHolder(this));
    mMainExecutorToken = getMainExecutorToken();
  }

  private native void initializeBridge(ReactCallback callback,
                                       JavaScriptExecutor jsExecutor,
                                       MessageQueueThread jsQueue,
                                       MessageQueueThread moduleQueue,
                                       ModuleRegistryHolder registryHolder);

    ......
}

刚刚分析 createReactContext() 方法的总结没错,CatalystInstanceImpl 这货就是个封装总管,负责了 Java 层代码到 JNI 封装初始化的任务和 Java 与 JS 调用的 Java 端控制中心。所以我们先看看调用 native initializeBridge 方法时传入的 5 个参数吧,分别如下:
1. callback参数: CatalystInstanceImpl 的内部静态实现类 BridgeCallback,负责相关接口回调回传。
2. jsExecutor参数: 前面分析的 XReactInstanceManagerImpl 中赋值为 JSCJavaScriptExecutor 实例,JSCJavaScriptExecutor 中也有自己的 native initHybrid 的 C++ 方法被初始化时调用,具体在 OnLoad.cpp 的 JSCJavaScriptExecutorHolder 类中。
3. jsQueue参数: 来自于 mReactQueueConfiguration.getJSQueueThread(),mReactQueueConfiguration就是 CatalystInstanceImpl 中创建的 ReactQueueConfigurationImpl.create(
ReactQueueConfigurationSpec,
new NativeExceptionHandler()); 第一个参数来自于 XReactInstanceManagerImpl 中 CatalystInstanceImpl 的建造者,实质为包装相关线程名字、类型等,然后通过 ReactQueueConfigurationImpl 的 create 创建对应线程的 Handler,这里就是名字为 js 的后台线程 Handler,第二个参数为异常捕获回调实现。
4. moduleQueue参数: 来自于 mReactQueueConfiguration.getNativeModulesQueueThread(),mReactQueueConfiguration就是 CatalystInstanceImpl 中创建的 ReactQueueConfigurationImpl.create(
ReactQueueConfigurationSpec,
new NativeExceptionHandler()); 第一个参数来自于 XReactInstanceManagerImpl 中 CatalystInstanceImpl 的建造者,实质为包装相关线程名字、类型等,然后通过 ReactQueueConfigurationImpl 的 create 创建对应线程的 Handler,这里就是名字为 native_modules 的后台线程 Handler,第二个参数为异常捕获回调实现。
5. registryHolder参数: mJavaRegistry 对象来自于 XReactInstanceManagerImpl 中 CatalystInstanceImpl 的建造者,通过 mJavaRegistry.getModuleRegistryHolder(this) 传递一个 Java 层的 ModuleRegistryHolder 实例到同名的 C++ 中,具体在 mJavaRegistry.getModuleRegistryHolder(this) 的返回值处为 return new ModuleRegistryHolder(catalystInstanceImpl, javaModules, cxxModules); 而 ModuleRegistryHolder 的构造方法中调用了 C++ 的 initHybrid(catalystInstanceImpl, javaModules, cxxModules); 方法。

CatalystInstanceImpl 这货会玩,自己在 Java 层直接把持住了 JavaScriptModuleRegistry 映射表,把 NativeModuleRegistry 映射表、BridgeCallback 回调、JSCJavaScriptExecutor、js 队列 MessageQueueThread、native 队列 MessageQueueThread 都通过 JNI 嫁接到了 C++ 中。那我们现在先把目光转移到 CatalystInstanceImpl.cpp 的 initializeBridge 方法上(关于 JNI 的 OnLoad 中初始化注册模块等等就不介绍了),如下:

void CatalystInstanceImpl::initializeBridge(
    jni::alias_ref<ReactCallback::javaobject> callback,
    // This executor is actually a factory holder.
    JavaScriptExecutorHolder* jseh,
    jni::alias_ref<JavaMessageQueueThread::javaobject> jsQueue,
    jni::alias_ref<JavaMessageQueueThread::javaobject> moduleQueue,
    ModuleRegistryHolder* mrh) {
    ......
  // Java CatalystInstanceImpl -> C++ CatalystInstanceImpl -> Bridge -> Bridge::Callback
  // --weak--> ReactCallback -> Java CatalystInstanceImpl
    ......
    //instance_为ReactCommon目录下 Instance.h 中类的实例;JNI封装规则就不介绍了,之前写过文章的。
    //第一个参数为JInstanceCallback实现类,父类在cxxreact/Instance.h中。
    //第二个参数为JavaScriptExecutorHolder,实质对应java中JavaScriptExecutor,也就是上面分析java的initializeBridge方法第二个参数JSCJavaScriptExecutor。
    //第三第四个参数都是java线程透传到C++,纯C++的JMessageQueueThread。
    //第五个参数为C++的ModuleRegistryHolder的getModuleRegistry()方法。
  instance_->initializeBridge(folly::make_unique<JInstanceCallback>(callback),
                              jseh->getExecutorFactory(),
                              folly::make_unique<JMessageQueueThread>(jsQueue),
                              folly::make_unique<JMessageQueueThread>(moduleQueue),
                              mrh->getModuleRegistry());
}

到此 CatalystInstance 的实例 CatalystInstanceImpl 对象也就初始化 OK 了,同时通过 initializeBridge 建立了 Bridge 连接。关于这个 Bridge 在RN 中是通过 libjsc.so 中 JSObjectRef.h 的 JSObjectSetProperty(m_context, globalObject, jsPropertyName, valueToInject, 0, NULL); 来关联的,这样就可以在 Native 设置 JS 执行,反之同理。

由于这一小节我们只讨论 RN 的加载启动流程,所以 initializeBridge 的具体实现我们下面分析互相通信交互时再仔细分析,故我们先把思路还是回到 XReactInstanceManagerImpl 中 createReactContext 方法的 reactContext.initializeWithInstance(catalystInstance); 一行,可以看见,这行代码意思就是将刚刚初始化的 catalystInstance 传递给全局唯一的 reactContext 对象,同时在 reactContext 中通过 catalystInstance 拿到 JS、Native、UI 几个 Thread 的引用,方便快速访问使用这些对象。接着调用了 catalystInstance.runJSBundle(); 方法,这个方法实现如下:

  @Override
  public void runJSBundle() {
    ......
    mJSBundleHasLoaded = true;
    //mJSBundleLoader就是前面分析的依据不同设置决定是JSBundleLoader的createAssetLoader还是createFileLoader等静态方法的匿名实现类。
    // incrementPendingJSCalls();
    mJSBundleLoader.loadScript(CatalystInstanceImpl.this);
    ......
  }

通过注释我们假设 Loader 是默认的,也即 JSBundleLoader 类的如下方法:

  public static JSBundleLoader createAssetLoader(
      final Context context,
      final String assetUrl) {
    return new JSBundleLoader() {
      @Override
      public void loadScript(CatalystInstanceImpl instance) {
        instance.loadScriptFromAssets(context.getAssets(), assetUrl);
      }

      @Override
      public String getSourceUrl() {
        return assetUrl;
      }
    };
  }

可以看见,它实质又调用了 CatalystInstanceImpl 的 loadScriptFromAssets 方法,我们继续跟踪 CatalystInstanceImpl 的这个方法吧,如下:

native void loadScriptFromAssets(AssetManager assetManager, String assetURL);

loadScriptFromAssets 既然是一个 native 方法,我们去 CatalystInstanceImpl.cpp 看下这个方法的实现,如下:

void CatalystInstanceImpl::loadScriptFromAssets(jobject assetManager,
                                                const std::string& assetURL) {
  const int kAssetsLength = 9;  // strlen("assets://");
  //获取source路径名,不计前缀,这里默认就是index.android.bundle
  auto sourceURL = assetURL.substr(kAssetsLength);
    //assetManager是Java传递的AssetManager。
    //extractAssetManager是JSLoader.cpp中通过系统动态链接库android/asset_manager_jni.h的AAssetManager_fromJava方法来获取AAssetManager对象的。
  auto manager = react::extractAssetManager(assetManager);
    //通过JSLoader对象的loadScriptFromAssets方法读文件,得到大字符串script(即index.android.bundle文件的JS内容)。
  auto script = react::loadScriptFromAssets(manager, sourceURL);
    //判断是不是Unbundle,这里不是Unbundle,因为打包命令我们用了react.gradle的默认bundle,没用unbundle命令(感兴趣的自己分析这条路线)。
  if (JniJSModulesUnbundle::isUnbundle(manager, sourceURL)) {
    instance_->loadUnbundle(
      folly::make_unique<JniJSModulesUnbundle>(manager, sourceURL),
      std::move(script),
      sourceURL);
    return;
  } else {
    //bundle命令打包的,所以走这里。
    //instance_为ReactCommon目录下 Instance.h 中类的实例,前面分析过了。
    instance_->loadScriptFromString(std::move(script), sourceURL);
  }
}

看来还没到头,这货又走到了 ReactCommon 目录下 Instance 实例的 loadScriptFromString 方法去了(由此可以看出来前面 ReactNativeAndroid 目录下的 jni 代码都是 Android 平台特有的封装,直到 ReactCommon 才是通用的),如下:

//string为index.android.bundle内容。
//sourceURL在这里默认为index.android.bundle。
void Instance::loadScriptFromString(std::unique_ptr<const JSBigString> string,
                                    std::string sourceURL) {
    //callback_就是initializeBridge传进来的,实质实现是CatalystInstanceImpl的BridgeCallback。
    //说白了就是回传一个状态,要开始搞loadScriptFromString了
  callback_->incrementPendingJSCalls();
  SystraceSection s("reactbridge_xplat_loadScriptFromString",
                    "sourceURL", sourceURL);
    //厉害了,Word哥,年度大戏啊!
    //nativeToJsBridge_也是Instance::initializeBridge方法里初始化的,实现在Common的NativeToJsBridge类里。
  nativeToJsBridge_->loadApplication(nullptr, std::move(string), std::move(sourceURL));
}

妈的,没完没了了,继续跟吧,到 Common 的 NativeToJsBridge.cpp 看看 loadApplication 方法吧,如下:

//unbundle传入的是个空指针。
//startupScript为bundle文件内容。
//startupScript为bundle文件名。
void NativeToJsBridge::loadApplication(
    std::unique_ptr<JSModulesUnbundle> unbundle,
    std::unique_ptr<const JSBigString> startupScript,
    std::string startupScriptSourceURL) {
    //runOnExecutorQueue实质就是获取一个MessageQueueThread,然后在其线程中执行一个task。
  runOnExecutorQueue(
      m_mainExecutorToken,
      [unbundleWrap=folly::makeMoveWrapper(std::move(unbundle)),
       startupScript=folly::makeMoveWrapper(std::move(startupScript)),
       startupScriptSourceURL=std::move(startupScriptSourceURL)]
        (JSExecutor* executor) mutable {

    auto unbundle = unbundleWrap.move();
    if (unbundle) {
      executor->setJSModulesUnbundle(std::move(unbundle));
    }
    //因为我们是bundle命令打包的,所以走这里继续执行!!!
    executor->loadApplicationScript(std::move(*startupScript),
                                    std::move(startupScriptSourceURL));
  });
}

靠靠靠,还不到头,又特么绕到 JSExecutor 的 loadApplicationScript 方法里面去了,继续跟吧(这个 executor 是 runOnExecutorQueue 方法中回传的一个 map 中取的,实质是 OnLoad 中 JSCJavaScriptExecutorHolder 对应,也即 java 中 JSCJavaScriptExecutor,所以 JSExecutor 实例为 JSCExecutor.cpp 中实现),如下:

//script为bundle文件内容,sourceURL为bundle文件名
void JSCExecutor::loadApplicationScript(std::unique_ptr<const JSBigString> script, std::string sourceURL) throw(JSException) {
  SystraceSection s("JSCExecutor::loadApplicationScript",
                    "sourceURL", sourceURL);
    ......
    //把bundle文件和文件名等内容转换成js可以识别的String
  String jsScript = jsStringFromBigString(*script);
  String jsSourceURL(sourceURL.c_str());
    //使用webkit JSC去真正解释执行Javascript了!
  evaluateScript(m_context, jsScript, jsSourceURL);
    //绑定桥,核心是通过getGlobalObject将JS与C++通过webkit JSC bind
  bindBridge();
  flush();
    ......
}

去他大爷的,没完没了了,继续看看 bindBridge() 方法和 flush() 方法,如下:

void JSCExecutor::bindBridge() throw(JSException) {
  ......
  auto global = Object::getGlobalObject(m_context);
  auto batchedBridgeValue = global.getProperty("__fbBatchedBridge");
  ......

  auto batchedBridge = batchedBridgeValue.asObject();
  m_callFunctionReturnFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnFlushedQueue").asObject();
  m_invokeCallbackAndReturnFlushedQueueJS = batchedBridge.getProperty("invokeCallbackAndReturnFlushedQueue").asObject();
  //通过webkit JSC获取MessageQueue.js的flushedQueue
  m_flushedQueueJS = batchedBridge.getProperty("flushedQueue").asObject();
  m_callFunctionReturnResultAndFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnResultAndFlushedQueue").asObject();
}

void JSCExecutor::flush() {
  SystraceSection s("JSCExecutor::flush");
  //m_flushedQueueJS->callAsFunction({})即调用MessageQueue.js的flushedQueue方法。
  //即把JS端相关通信交互数据通过flushedQueue返回传给callNativeModules。
  callNativeModules(m_flushedQueueJS->callAsFunction({}));
}

void JSCExecutor::callNativeModules(Value&& value) {
  SystraceSection s("JSCExecutor::callNativeModules");
  try {
    //把JS端相关通信数据转为JSON格式字符串数据
    auto calls = value.toJSONString();
    //m_delegate实质为Executor.h中ExecutorDelegate类的实现类JsToNativeBridge对象。
    //故callNativeModules为JsToNativeBridge.cpp中实现的方法,把calls json字符串pase成格式结构。
    m_delegate->callNativeModules(*this, folly::parseJson(calls), true);
  } catch (...) {
    ......
  }
}

卧槽!又绕回到了 JsToNativeBridge.cpp 的 callNativeModules 方法,那就看下吧,如下:

    //executor即为前面的JSCExecutor。
    //calls为被解析OK的JS端JSON通信参数结构。
    //isEndOfBatch通知是否一个批次处理OK了,这里传递了true进来,说明JS文件Loader OK了。
  void callNativeModules(
      JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) override {
      //拿到token
    ExecutorToken token = m_nativeToJs->getTokenForExecutor(executor);
    //扔到nativeQueue的线程队列去等待执行
    m_nativeQueue->runOnQueue([this, token, calls=std::move(calls), isEndOfBatch] () mutable {
      // An exception anywhere in here stops processing of the batch.  This
      // was the behavior of the Android bridge, and since exception handling
      // terminates the whole bridge, there's not much point in continuing.
      for (auto& call : react::parseMethodCalls(std::move(calls))) {
        //调用Native registry表中的java NativeMethod方法。
        m_registry->callNativeMethod(
          token, call.moduleId, call.methodId, std::move(call.arguments), call.callId);
      }
      //一些类似数据库事务操作的机制,用来告诉OK了
      if (isEndOfBatch) {
        m_callback->onBatchComplete();
        m_callback->decrementPendingJSCalls();
      }
    });
  }

终于尼玛明朗了,上面这段调用不就是前面分析的那个回调么,说白了就是 CatalystInstanceImpl.java 中 CatalystInstanceImpl 构造方法中调用 C++ 的 initializeBridge 方法时传入的第一个参数 BridgeCallback 么,也就是说 JS bundle 文件被加载完成以后 JS 端调用 Java 端时会触发 Callback 的 onBatchComplete 方法,这货最终又会触发 OnBatchCompleteListener 接口的 onBatchComplete 方法,这不就把 JS Bundle 文件加载完成以后回调 Java 通知 OK 了么,原来主要的流程是这么回事。为了接下来不迷糊,赶紧先来一把小梳理总结,用图说话,如下:

这里写图片描述

上面这幅图已经囊括了我们上面那些枯燥的启动流程的部分流程分析了,好了,从上图可以知道我们前面贴出来的 AsyncTask 的 onPostExecute 方法还没分析,所以我们把目光再回到 XReactInstanceManagerImpl 的那个 ReactContextInitAsyncTask 中,doInBackground 方法执行完成后返回了 Result 包装的 reactContext,所以我们看下 onPostExecute 方法中调用的核心方法 setupReactContext,如下:

  private void setupReactContext(ReactApplicationContext reactContext) {
    ......
    CatalystInstance catalystInstance =
        Assertions.assertNotNull(reactContext.getCatalystInstance());
    //执行Native Java Module 的 initialize
    catalystInstance.initialize();
    //重置DevSupportManager实现类的reactContext相关
    mDevSupportManager.onNewReactContextCreated(reactContext);
    //内存状态回调设置
    mMemoryPressureRouter.addMemoryPressureListener(catalystInstance);
    //置位生命周期
    moveReactContextToCurrentLifecycleState();
    //核心方法!!!!
    for (ReactRootView rootView : mAttachedRootViews) {
      attachMeasuredRootViewToInstance(rootView, catalystInstance);
    }
    ......
  }

到此我们再追一下 mAttachedRootViews 这个列表赋值的地方吧,依旧是这个类的 attachMeasuredRootView(ReactRootView rootView) 方法,然而这个方法唯一被调用的地方在 ReactRootView 的 attachToReactInstanceManager() 中,再次发现 attachToReactInstanceManager 又是在 ReactRootView 已经 measure 的情况下才会触发,所以也就是说 mAttachedRootViews 中保存的都是 ReactRootView。那我们继续回到 XReactInstanceManagerImpl 中 setupReactContext 方法的 attachMeasuredRootViewToInstance(rootView, catalystInstance); 里看看,如下:

  private void attachMeasuredRootViewToInstance(
      ReactRootView rootView,
      CatalystInstance catalystInstance) {
    ......
    //彻底reset ReactRootView中的UI
    // Reset view content as it's going to be populated by the application content from JS
    rootView.removeAllViews();
    rootView.setId(View.NO_ID);
    //通过UIManagerModule设置根布局为ReactRootView
    UIManagerModule uiManagerModule = catalystInstance.getNativeModule(UIManagerModule.class);
    int rootTag = uiManagerModule.addMeasuredRootView(rootView);
    //设置相关tag
    rootView.setRootViewTag(rootTag);
    //把Java端启动传递的launchOptions包装成JS用的类型
    @Nullable Bundle launchOptions = rootView.getLaunchOptions();
    WritableMap initialProps = Arguments.makeNativeMap(launchOptions);
    //获取我们startReactApplication设置的JS端入口name,继承ReactActivity的话值为getMainComponentName()设置的
    String jsAppModuleName = rootView.getJSModuleName();
    //包装相关参数,rootTag告知JS端Native端的ReactRootView是哪个
    WritableNativeMap appParams = new WritableNativeMap();
    appParams.putDouble("rootTag", rootTag);
    appParams.putMap("initialProps", initialProps);
    //核心大招!!!!!React Native真正的启动流程入口是被Java端在这里拉起来的!!!!!!!!
    catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams);
    ......
  }

坚持一下,分析源码就是个苦逼的过程,坚持下来就好了,马上看到希望了;我们知道 AppRegistry.class 是 JS 端暴露给 Java 端的接口方法,所以 catalystInstance.getJSModule(AppRegistry.class) 实质就桥接到 JS 端代码去了,那就去看看 AppRegistry.js 的代码吧,如下:

//JS端对应代码,注意这个变量上面的英文已经交代很详细啦
var AppRegistry = {
    ......
    //我们JS端自己在index.android.js文件中调用的入口就是:
    //AppRegistry.registerComponent('TestRN', () => TestRN);
  registerComponent: function(appKey: string, getComponentFunc: ComponentProvider): string {
    runnables[appKey] = {
      run: (appParameters) =>
        renderApplication(getComponentFunc(), appParameters.initialProps, appParameters.rootTag)
    };
    return appKey;
  },
    ......
    //上面java端 AppRegistry 调用的 JS 端就是这个方法,索引到我们设置的appkey=TestRN字符串的JS入口
  runApplication: function(appKey: string, appParameters: any): void {
    ......
    runnables[appKey].run(appParameters);
  },
    ......
};

真他妈不容易啊,总算到头了,原来 React Native 是这么被启动起来的。现在回过头来看发现其实主启动流程也就那么回事,还以为很神秘嘻嘻的,现在总算被揭开了。总结一下吧,如下图所示即为整个 React Native 加载主流成的主要情况:

这里写图片描述

到这里 React Native 的启动流程就分析完了,不过,我猜你看到这里的时候一定会骂我,因为我知道上面的主流程中你会有很多疑惑,这也是我写这篇阅读 RN 源码总结最纠结的地方,因为想尽可能的将主加载流程和通信方式分开来分析,以便做到模块化理解,但是后来发现关联性又很强,揉一起分析更乱套,所以就有了这么一篇很长的文章,前面就当是主流程综述概要分析,细节在下面通信方式分析时会继续提及浅析,所以建议带着上面的疑惑继续向下看完这篇文章再回到 Part 2 RN 启动流程框架浅析 这一部分来看一遍,这样你的疑惑就全部揭开了。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

3 RN Java 调用 JS 端框架浅析

这是一个悲伤的故事,看源码没有伴,RN 接入也在自己一个人搞,所以搞起来总是挺慢,好在一直在坚持,源码也断断续续在工作之余看了一个多星期,这篇文章也占用了我一个美好的周末时光,有种说不出来的感觉,唉,不扯了,我们现在来看看 RN 中 Java 是如何调用 JS 代码的。

首先,通过上面加载流程或者以前我们自定义 Java & JS 交互模块的经历我们知道 JS 端代码模块对应的 Java 端都是继承 JavaScriptModule 来实现的(可以看上面 reactPackage.createJSModules() 方法,返回的是 JS 端给 Java 端约定好的 JS 模块 Java 实现);要说 Java 端如何调用 JS 端代码就得有个例子,我们就拿上面启动流程中最后 CatalystInstanceImpl.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams); 拉起 JS 端 index.android.js 的 JS 组件入口来分析,这就是一个典型的 Java 端调用 JS 端代码的例子,首先我们可以知道 AppRegistry.java 是继承 JavaScriptModule 的,如下:

public interface AppRegistry extends JavaScriptModule {
  void runApplication(String appKey, WritableMap appParameters);
  void unmountApplicationComponentAtRootTag(int rootNodeTag);
  void startHeadlessTask(int taskId, String taskKey, WritableMap data);
}

然后 AppRegistry.java 是在 CoreModulesPackage 的 createJSModules() 方法中被添加入列表的,CoreModulesPackage 又是在主启动流程的 processPackage() 方法中被包装后加入 JavaScriptModuleRegistry 映射表的,JavaScriptModuleRegistry 映射表又被 Java 层的 CatalystInstanceImpl 接管。所以 Java 调用 JS 方法都是通过 CatalystInstanceImpl.getJSModule(class).methodXXX() 来执行的(我们自己模块调用的话是通过 ReactContext.getJSModule(),因为 ReactContext 在主启动流程中持有了 CatalystInstanceImpl 实例,所以 CatalystInstanceImpl 是不直接对外的),那我们就沿着这条线去观摩一把,如下 CatalystInstanceImpl.java 的 getJSModule 方法:

  @Override
  public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
      //mMainExecutorToken来自于native C++代码
    return getJSModule(mMainExecutorToken, jsInterface);
  }

  @Override
  public <T extends JavaScriptModule> T getJSModule(ExecutorToken executorToken, Class<T> jsInterface) {
      //mJSModuleRegistry就是启动流程中processPackage()方法加进去交给CatalystInstanceImpl托管的JS代码映射表
    return Assertions.assertNotNull(mJSModuleRegistry)
        .getJavaScriptModule(this, executorToken, jsInterface);
  }

接着去 JavaScriptModuleRegistry 映射表中看看 getJavaScriptModule() 方法,如下:

  public synchronized <T extends JavaScriptModule> T getJavaScriptModule(
    CatalystInstance instance,
    ExecutorToken executorToken,
    Class<T> moduleInterface) {
    //module加载的缓存,加载过一次且缓存存在就直接从缓存取
    ......

    //获取JavaScriptModule模块的方式,以AppRegistry模块获取为例,略叼,动态代理生成获取JS Module
    JavaScriptModuleRegistration registration =
        Assertions.assertNotNull(
            mModuleRegistrations.get(moduleInterface),
            "JS module " + moduleInterface.getSimpleName() + " hasn't been registered!");
    JavaScriptModule interfaceProxy = (JavaScriptModule) Proxy.newProxyInstance(
        moduleInterface.getClassLoader(),
        new Class[]{moduleInterface},
        new JavaScriptModuleInvocationHandler(executorToken, instance, registration));
    instancesForContext.put(moduleInterface, interfaceProxy);
    return (T) interfaceProxy;
  }

从上面这段代码我们可以看见,getJSModule 获取 JsModule 的实质是通过 Java 的动态代理来实现的,同时 JavaScriptModuleRegistration 对 JavaScriptModule 的包装是为了检查实现 JavaScriptModule 接口的类不能存在重载,因为与 JS 端对应,JS 不支持。那我们不妨把视线转移到 JavaScriptModuleInvocationHandler 的 invoke 方法,可以发现实质是调用了 mCatalystInstance.callFunction(executorToken, mModuleRegistration.getName(), method.getName(), jsArgs); 语句,继续跟了一下发现调用了 CatalystInstanceImpl.java 的 native callJSFunction() 方法把相关参数传递到了 C++ 层,额,前面我们知道 CatalystInstanceImpl.cpp 只是 JNI 对于 Android 层适配的特有封装,实质对应了 Common 里 Instance.cpp,而这里的 native callJSFunction() 实质是通过 Instance::callJSFunction() 调用了 NativeToJsBridge::callFunction() 方法,进而放在了 JSCExecutor 的线程队列中触发了 JSCExecutor::callFunction() 方法,我们重点关注下 JSCExecutor::callFunction() 方法,如下:

void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) {
  ......
  auto result = [&] {
    try {
        //m_callFunctionReturnFlushedQueueJS来自于JSCExecutor::bindBridge()方法中初始化,JSCExecutor::bindBridge()是在前面分析启动流程时被调用的,实质是负责通过 Webkit JSC 拿到 JS 端代码的相关对象和方法引用,譬如拿到 JS 端 BatchedBridge.js 的 __fbBatchedBridge 属性与 MessageQueue.js 的 callFunctionReturnFlushedQueue 方法引用。此处实质为调用 MessageQueue.js 的 callFunctionReturnFlushedQueue 方法。
      return m_callFunctionReturnFlushedQueueJS->callAsFunction({
        Value(m_context, String::createExpectingAscii(moduleId)),
        Value(m_context, String::createExpectingAscii(methodId)),
        Value::fromDynamic(m_context, std::move(arguments))
      });
    } catch (...) {
      std::throw_with_nested(
        std::runtime_error("Error calling function: " + moduleId + ":" + methodId));
    }
  }();
    //调用 native 模块,暂时忽略,下一小节解释,这里重点关注 Java 调用 JS 通信
  callNativeModules(std::move(result));
}

既然都说了 m_callFunctionReturnFlushedQueueJS 是 JSCExecutor::bindBridge() 方法中初始化的,实质依赖 Webkit JSC 架起了 JS 代码与 C++ 的桥梁,那我们就去 JS 端看看 MessageQueue.js 的 callFunctionReturnFlushedQueue() 方法,如下:

  callFunctionReturnFlushedQueue(module: string, method: string, args: Array<any>) {
    guard(() => {
      this.__callFunction(module, method, args);
      this.__callImmediates();
    });

    return this.flushedQueue();
  }

继续跟下 this.__callFunction(module, method, args),如下:

  __callFunction(module: string, method: string, args: Array<any>) {
    ......
    //_callableModules属性是通过registerCallableModule()方法添加的,而我们以AppRegistry.js为例,可以看见AppRegistry.js中有调用BatchedBridge.registerCallableModule('AppRegistry', AppRegistry);把自己注册进去,BatchedBridge.js是与MessageQueue.js绑死的。
    //说白了,这里就是在 JS 端的 Modules 中查映射表找到 AppRegistry.js
    const moduleMethods = this._callableModules[module];
    ......
    //拿到Java端调用的对应JS端指定Module的方法,譬如AppRegistry.js的runApplication()方法
    const result = moduleMethods[method].apply(moduleMethods, args);
    Systrace.endEvent();
    return result;
  }

握草,React Native Java 层调用 JS 层通信原来原来就这么回事;实质就是 Java 与 JS 端都准备好一个 Module 映射表,然后当 Java 端调用 JS 代码时 Java 端通过查表动态代理创建一个与 JS 对应的 Module 对象,当调用这个 Module 的方法时 Java 端通过动态代理的 invoke 方法触发 C++ 层,层层调用后通过 JSCExecutor 执行 JS 端队列中的映射查表找到 JS 端方法进行调用;同时会发现在 JSCExecutor 中每次 Java 调用 JS 之后会进行 Java 端的一个回调(从 JS 层的 MessageQueue.js 中获得累积的 JS Call)。为了不迷糊,我们对这一阶段总结如下图:

这里写图片描述

不多说了,一切都在上面图里,所以这时候如果你再回头去想第一部分 RN 启动流程源码浅析的疑惑点就完全明白了,也就串起来了。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

4 RN JS 调用 Java 端框架浅析

上面已经总结了 React Native 的启动流程与 Java 端代码调用 JS 端代码的通信流程,下面我们再来分析 JS 端代码调用 Java 端代码的通信流程,这样整个 React Native 核心框架的三大问题都得到浅析了,方便日后裁剪 React Native。既然要分析 JS 调用 Java 端通信框架了,我们一样有必要先找到一个例子来做突破口,那我们就以 RN 官方的《Native Modules 》为例来说明,因为这是我们经常要封装且最熟悉的东西,可以看见文档中举了一个封装 ToastAndroid 的例子,那就拿它下手吧。我们在 JS 端使用 Android 端封装的 Java 模块时是如下这样用的:

//通过NativeModules拿到ToastAndroid
import { NativeModules } from 'react-native';
module.exports = NativeModules.ToastAndroid;

//使用的地方在JS中相关逻辑处调用,官方文档标准
import ToastAndroid from './ToastAndroid';
ToastAndroid.show('Awesome', ToastAndroid.SHORT);

可以看见,JS 调用 Java 的第一步就是通过 JS 端的 NativeModules 拿到 相关的 Java 映射 Module,那我们就去 JS 的 NativeModules 里看看吧,如下:

//暂时只关注NativeModules.js中精彩的
......
//JSC全局唯一global添加一个属性,赋值为NativeModules.js中方法引用
global.__fbGenNativeModule = genModule;

let NativeModules : {[moduleName: string]: Object} = {};
//依据是否定义global.nativeModuleProxy属性来决定怎么获取。
//nativeModuleProxy属性实质是在主加载流程中JSCExecutor::JSCExecutor()构造时通过installGlobalProxy(m_context, "nativeModuleProxy", exceptionWrapMethod<&JSCExecutor::getNativeModule>());创建的,所以当JS调用NativeModules时实质为JSCExecutor::getNativeModule()方法
if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
} else {
    //只关注精彩的主线,DEV等模式的咱们不管
  ......
}
module.exports = NativeModules;

我们看下 JSCExecutor::getNativeModule() 方法,如下:

JSValueRef JSCExecutor::getNativeModule(JSObjectRef object, JSStringRef propertyName) {
  ......
    //m_nativeModules来源于JsToNativeBridge的getModuleRegistry()方法,实质被转换为了JSCNativeModules.cpp
  return m_nativeModules.getModule(m_context, propertyName);
}

继续跟踪上面 JSCNativeModules.cpp 的 getModule(m_context, propertyName) 可以发现其核心就是调用 JSCNativeModules::createModule(const std::string& name, JSContextRef context) 方法,而这个方法里实质是通过 JSC 获取全局设置的 JS 属性,然后通过 JNI 查找 Java 端映射表再触发 JS 端相关方法:

folly::Optional<Object> JSCNativeModules::createModule(const std::string& name, JSContextRef context) {
      ......
      //JSC获取NativeModules.js中的global.__fbGenNativeModule = genModule;属性
    m_genNativeModuleJS = global.getProperty("__fbGenNativeModule").asObject();
    m_genNativeModuleJS->makeProtected();
    ......
  }
    //调用folly::Optional<ModuleConfig> ModuleRegistry::getConfig(const std::string& name)获取Native的配置表
  auto result = m_moduleRegistry->getConfig(name);
  if (!result.hasValue()) {
    return nullptr;
  }
    //JS端调用m_genNativeModuleJS对应方法
  Value moduleInfo = m_genNativeModuleJS->callAsFunction({
    Value::fromDynamic(context, result->config),
    JSValueMakeNumber(context, result->index)
  });
  ......
  return moduleInfo.asObject().getProperty("module").asObject();
}

上面主要分两步,通过 C++ 获取 Java 层映射表、通过 JSC 调用 JS 端方法,我们先看下 ModuleRegistry::getConfig(const std::string& name) 源码,如下:

folly::Optional<ModuleConfig> ModuleRegistry::getConfig(const std::string& name) {
  //临界值判断
  ......
  //modules_列表实质来源自上面加载流程中分析的CatalystInstanceImpl::initializeBridge();
  //实质就是Java端在CatalystInstanceImpl中传递到C++的ModuleRegistryHolder::getModuleRegistry()方法;
  //module实质就是ModuleRegistryHolder.cpp构造中把Java端传递过来的Module包装成CxxNativeModule、JavaNativeModule,这两实质是NativeModule C++子类
  NativeModule* module = modules_[it->second].get();

  // string name, object constants, array methodNames (methodId is index), [array promiseMethodIds], [array syncMethodIds]
  //准备创建一个动态config对象
  folly::dynamic config = folly::dynamic::array(name);

  {
    //module->getConstants()实际通过反射调用了Java层JavaModuleWrapper的getConstants方法
    config.push_back(module->getConstants());
  }

  {
    //module->getMethods()实际反射调用了Java层JavaModuleWrapper的getMethods方法,也就是BaseJavaModule.java的getMethods方法,这里会通过Java的findMethods()方法过滤出继承BaseJavaModule实现类中有@ReactMethod注解的方法!!!!!!!!(符合官方文档)
    std::vector<MethodDescriptor> methods = module->getMethods();

    folly::dynamic methodNames = folly::dynamic::array;
    folly::dynamic promiseMethodIds = folly::dynamic::array;
    folly::dynamic syncMethodIds = folly::dynamic::array;

    for (auto& descriptor : methods) {
      // TODO: #10487027 compare tags instead of doing string comparison?
      methodNames.push_back(std::move(descriptor.name));
      if (descriptor.type == "promise") {
        promiseMethodIds.push_back(methodNames.size() - 1);
      } else if (descriptor.type == "sync") {
        syncMethodIds.push_back(methodNames.size() - 1);
      }
    }

    if (!methodNames.empty()) {
      config.push_back(std::move(methodNames));
      if (!promiseMethodIds.empty() || !syncMethodIds.empty()) {
        config.push_back(std::move(promiseMethodIds));
        if (!syncMethodIds.empty()) {
          config.push_back(std::move(syncMethodIds));
        }
      }
    }
  }

  ......
  return ModuleConfig({it->second, config});
}

真相渐渐浮出水面了,我们继续回到前面 JSCNativeModules::createModule(const std::string& name, JSContextRef context) 中看看最后调用的 NativeModules.js 中的 global.__fbGenNativeModule = genModule 方法,如下:

function genModule(config: ?ModuleConfig, moduleID: number): ?{name: string, module?: Object} {
  ......
  //通过JSC拿到C++中从Java端获取的Java的Module映射表包装配置类
  const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
  ......
  const module = {};
  //遍历构建module的属性方法
  methods && methods.forEach((methodName, methodID) => {
    const isPromise = promiseMethods && arrayContains(promiseMethods, methodID);
    const isSync = syncMethods && arrayContains(syncMethods, methodID);
    invariant(!isPromise || !isSync, 'Cannot have a method that is both async and a sync hook');
    const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
    //生成Module的函数方法
    module[methodName] = genMethod(moduleID, methodID, methodType);
  });
  Object.assign(module, constants);
  ......
  //返回一个
  return { name: moduleName, module };
}

到此 JS 调用 Java 的准备工作已经就绪了(即 JS 是如何拿到 Native 映射表与方法的),JSC会准备好一个 JS 使用的 NativeModule 对象。

有了 JS 端的 Module 映射对象,访问就变得明朗了许多;还记得刚刚 NativeModules.js 中 genMethod() 方法生成的 JS Module 的方法属性吗?当我们 JS 真正调用 ToastAndroid.show(‘Awesome’, ToastAndroid.SHORT); 时实质就是调用 genMethod() 设置的方法;那我们就仔细看看这个 genMethod() 方法,可以发现它是依据调用的 JS 方法是不是 promise、sync 等走不同逻辑,但是主线核心都是调用了 BatchedBridge.enqueueNativeCall() 方法,那我们看看 MessageQueue.js 的 enqueueNativeCall 方法,如下:

  enqueueNativeCall(moduleID: number, methodID: number, params: Array<any>, onFail: ?Function, onSucc: ?Function) {
    ......
    this._callID++;
    //_queue是个队列,用于存放调用的模块、方法、参数信息
    //把JS准备调用Java的模块名、方法名、调用参数放到数组里存起来
    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);
    ......
    this._queue[PARAMS].push(params);

    const now = new Date().getTime();
    //如果5ms内有多个方法调用就先待在队列里防止过高频率,否则调用C++的nativeFlushQueueImmediate方法
    if (global.nativeFlushQueueImmediate &&
        now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) {
      global.nativeFlushQueueImmediate(this._queue);
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
    }
    ......
  }

这时候 JS 端调用 Java 通信的 JS 端调用流程就结束了,主要就是通过 JSC 桥接获取 Java 端 Module 的映射表转换为 JS NativeModule 的属性和相关方法,当 JS 端通过 NativeModule.XXX.method(); 使用时实质就是把 method 方法扔进了 JS 的队列,然后在队列中分两种情况,一种是方法调用超过 5ms 时直接触发 nativeFlushQueueImmediate 方法,另一种是当 Java 调用 JS 时也会把之前队列里存的方法调用通过 JSCExecutor::flush() 处理(这回就明白上面分析启动流程为毛 Java 拉起 JS 后又来了一个反回调了吧)。

那我们先看看 JS 直接触发 nativeFlushQueueImmediate 的流程吧,由于 JS 端调用了 global.nativeFlushQueueImmediate 方法,所以实质是通过 JSC 调用了 C++ 的 JSCExecutor::nativeFlushQueueImmediate(size_t argumentCount, const JSValueRef arguments[]) 方法,因为在启动流程中 C++ 初始化 JSCExecutor 对象时通过 initOnJSVMThread() 调用 JSCExecutor::initOnJSVMThread() 进而调用 installGlobalFunction() 方法通过 JSC 把它已经关联给了 JS 。回过头发现 JSCExecutor::nativeFlushQueueImmediate 实质调用了 JSCExecutor::flushQueueImmediate(Value&& queue) 方法,进而调用了 JsToNativeBridge 的 callNativeModules(JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) 方法,只是 isEndOfBatch=false 而已。

接着我们再看下 Java 调用 JS 时把之前队列里存的方法通过 JSCExecutor::flush() 调用的情况,会发现其实质也是调用了 callNativeModules(m_flushedQueueJS->callAsFunction({})) 方法,m_flushedQueueJS->callAsFunction({})就是调用 MessageQueue.js 中的 flushedQueue() 方法,得到 JS 队列中睡觉的方法,然后传给了 callNativeModules 方法,接着发现实质也是调用了 JsToNativeBridge 的 callNativeModules(JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) 方法,只是 isEndOfBatch=true 而已。

这下就有意思了,两个分支实质原理是一致的,那我们直接看下 JsToNativeBridge 的 callNativeModules 方法:

  void callNativeModules(
      JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) override {
    ExecutorToken token = m_nativeToJs->getTokenForExecutor(executor);
    //在native队列中执行
    m_nativeQueue->runOnQueue([this, token, calls=std::move(calls), isEndOfBatch] () mutable {
      //遍历来自js队列的调用方法列表
      for (auto& call : react::parseMethodCalls(std::move(calls))) {
          //m_registry是C++的ModuleRegistry,在前面启动流程分析CatalystInstanceImpl.initializeBridge()时候传递了一个Java的ModuleRegistryHolder到C++去保存
        m_registry->callNativeMethod(
          token, call.moduleId, call.methodId, std::move(call.arguments), call.callId);
      }
      if (isEndOfBatch) {
          //类似数据库事务操作标记回调Java状态
        m_callback->onBatchComplete();
        m_callback->decrementPendingJSCalls();
      }
    });
  }

没啥说的,拿出 JS 队列里存在的 JS 调用 Java 的所有方法通过 ModuleRegistry::callNativeMethod 方法遍历调用,那就去看看这个方法,如下:

void ModuleRegistry::callNativeMethod(ExecutorToken token, unsigned int moduleId, unsigned int methodId,
                                      folly::dynamic&& params, int callId) {
  ......
  //modules_是创建ModuleRegistryHolder时根据Java层ModuleRegistryHolder创建的C++ NativeModule。
  //moduleId为模块在列表中的索引值。
  modules_[moduleId]->invoke(token, methodId, std::move(params));
}

那就继续去看看 C++ 的 ModuleRegistryHolder 构造方法中包装 Java Module 的 C++ 的 NativeModule 的子类 JavaNativeModule 的 invoke 方法吧,如下:

class JavaNativeModule : public NativeModule {
 public:
  ......
  void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) override {
    //wrapper_参数为ModuleRegistryHolder.cpp构造方法中由Java传入的Java Module被C++包装的JavaModuleWrapper对象(ModuleRegistryHolder.h中定义,映射Java的JavaModuleWrapper.java)
    //通过反射调用JavaModuleWrapper的invoke方法,同时把methodId和参数传过去。
    static auto invokeMethod =
      wrapper_->getClass()->getMethod<void(JExecutorToken::javaobject, jint, ReadableNativeArray::javaobject)>("invoke");
    invokeMethod(wrapper_, JExecutorToken::extractJavaPartFromToken(token).get(), static_cast<jint>(reactMethodId),
                 ReadableNativeArray::newObjectCxxArgs(std::move(params)).get());
  }
  ......
};

去他奶奶的,这不就是调用 Java 的对应方法么,直接到 JNI 映射的 Java 类中看这个方法,如下:

class JavaModuleWrapper {
    ......
  @DoNotStrip
  public void invoke(ExecutorToken token, int methodId, ReadableNativeArray parameters) {
    if (mMethods == null || methodId >= mMethods.size()) {
      return;
    }
    //mMethods为所有继承BaseJavaModule类的BaseJavaModule.JavaMethod对象
    mMethods.get(methodId).invoke(mCatalystInstance, token, parameters);
  }
}

喔噢!明朗了!就这样 JS 就调用了 Java 模块的方法。简单通过框架流程图来总结回顾下这个流程吧,如下:
这里写图片描述

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

5 总结

此刻只想呵呵,真特么不容易,Java、C++、JS 跳来跳去的看,总算基本搞明白这三大核心主体知识点了,有了上面三个仔细的分析下面对上面三个来个综合总结,按照我个人阅读完 React Native 这一部份源码后的核心总结如下:

  • Java层 ReactContext(ReactApplicationContext): React Native 封装后的 Android Context,通过其访问设置 RN 包装起来的核心类实现等;
  • Java层 ReactInstanceManager(ReactInstanceManagerImpl): RN对 Android 层暴露的大内总管,负责掌管 CatalystInstanceImpl 实例、ReactRootView、Activity 生命周期等;
  • Java/C++层 CatalystInstance(CatalystInstanceImpl): RN Java、C++、JS通信总舵主,统管 JS、Java 核心 Module 映射表、回调等等,三端入口与桥梁;
  • C++层 NativeToJsBridge: Java 调用 JS 的桥梁,用来调用 JS Module、回调 Java(通过JsToNativeBridge)等;
  • C++层 JsToNativeBridge: JS 调用 Java 的桥梁,用来调用 Java Module等;
  • C++层 JSCExecutor: 掌管 Webkit 的 JavaScriptCore,JS 与 C++ 的转换桥接都在这里中转处理;
  • JS层 MessageQueue: 队列栈,用来处理 JS 的调用队列、调用 Java 或者 JS Module 的方法、处理回调、管理 JS Module 等;
  • 多层 JavaScriptModule/BaseJavaModule(NativeModule): 双端字典映射表中的模块,负责 Java/JS 到彼此的映射调用格式申明,由 CatalystInstance 统管;

尼玛,看了好久的源码,搞明白以后其实发现主流程和互相调用就上面几幅图那么回事。。。。这篇文章差点难产了,断断续续工作之余抽空才搞定,总的来说还是要坚持;通过上面的分析和之前《React Native Android 从学车到补胎和成功发车经历》《React Native Android Gradle 编译流程浅析》两篇文章的配合,我们对于 React Native 已经渐渐不觉得陌生了,本篇已经循序渐进揭开了 React Native 比较核心的启动主流程脉络,后面抽空会对 React Native 进行更加全面的解刨分析,力求今年把 React Native 这个技能点翻页封存吧,加油。

这里写图片描述

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

作者:yanbober 发表于2016/11/21 22:06:02 原文链接
阅读:20 评论:0 查看评论

微信公众号支付开发

$
0
0

前段时间公司要求开发微信公众号支付功能,在和XX大学合作的过程中需要帮助推广其精品课程,故开发了对其课程进行打赏的功能。此公众号为服务号。开发过程中遇到了多多少少的问题,虽然不是大问题但是总是浪费时间去排查,所以写了此文作为梳理。 --2016.10

一、微信支付

1.微信支付简介微信支付:

是集成在微信客户端的支付功能,用户可以通过手机完成快速的支付流程。微信支付以绑定银行卡的快捷支付为基础,向用户提供安全、快捷、高效的支付服务。

2.条件:

申请微信公众号支付有2个条件:

一个是已微信认证的服务号

一个是已微信认证的政府、媒体订阅号

3.微信支付模式

刷卡支付: 刷卡支付是用户展示微信钱包内的“刷卡条码/二维码”给商户系统扫描后直接完成支付的模式。主要应用线下面对面收银的场景。

扫码支付: 扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。

公众号支付: 公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

APP支付: APP支付又称移动端支付,是商户通过在移动端应用APP中集成开放SDK调起微信支付模块完成支付的模式。

二、开发流程

1.开发思路


1.1 配置

图1:申请支付功能


图2: 填写授权回调域名(安全起见,创造一个安全的支付环境)


图3: 填写授权目录


图4: 设置密钥(也是为了支付安全)


1.2 授权(此处针对课程进行打赏,并不要求用户关注公众号,因此使用静默授权)


2. 静默授权

关于网页授权的两种scope的区别说明
1)、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
2)、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
https ://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

3.统一下单

商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易回话标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付。
字段在return_code 和result_code都为SUCCESS的时候有返回prepay_id,微信生成的预支付回话标识,用于后续接口调用中使用,该值有效期为2小时。
微信统一下单接口链接:https://api.mch.weixin.qq.com/pay/unifiedorder

4.调起支付API

在微信浏览器里面打开H5网页中执行JS调起支付。接口输入输出数据格式为JSON。
注意:WeixinJSBridge内置对象在其他浏览器中无效。
列表中参数名区分大小,大小写错误签名验证会失败。


三、遇到的问题

1.未添加测试者进白名单:需要在公众号里配置


2.body中文编码方式:uft-8


3. 支付完成回调(同步和异步)

/**
 * 提交打赏信息
 */
function subRewardPrice(){
	//获取打赏金额
	var price = $("#rewardPrice").val();
	//获取课程ID
	var courseId = $("#courseId").val();
	if(null==price||price==""||parseFloat(price)<1){
	     $.msgbox({msg:"金额不得小于1元!",icon:"error",time:2000});
	     return;
	}
	// 支付方式: 1支付宝,2微信
	var statement = $("#statement").val();
	var openid = $("#openid").val();
	$.ajax({
    	 type:'post',
    	 url:'shippingBuyCourseMobile/rewardCourseMobile.htm',
    	 data:{
  			 courseId:courseId,
  			 payAmount:price,
  			 statement:statement,
  			 openid:openid
    	 },
    	 dataType:'json',
    	 success:function(data){
    		 if(statement == "1"){
 	    	 	// 处理支付宝支付下单成功
	    	 	$(".warp").after(data);
		           var queryParam = '';
		           Array.prototype.slice.call(document.querySelectorAll('input[type=hidden]')).forEach(function (ele) {
		        	   queryParam += ele.name + '=' + encodeURIComponent(ele.value) + '&';
		           });
		           var gotoUrl = document.querySelector('#alipay_form').getAttribute('action') + queryParam;
		           _AP.pay(gotoUrl);
    		 }else if(statement == "2"){
	    	 	// 处理微信支付下单成功
	    		 var returnCode = data.returnCode;
	    		 var resultCode = data.resultCode;
	    		 // 在return_code 和result_code都为SUCCESS的时候才返回prepay_id
	    		 if(returnCode == "SUCCESS" && resultCode == "SUCCESS"){
		    		if (typeof WeixinJSBridge == "undefined"){
		    		   if(document.addEventListener){
		    		       document.addEventListener('WeixinJSBridgeReady', onBridgeReady(data), false);
		    		   }else if (document.attachEvent){
		    		       document.attachEvent('WeixinJSBridgeReady', onBridgeReady(data)); 
		    		       document.attachEvent('onWeixinJSBridgeReady', onBridgeReady(data));
		    		   }
		    		}else{
		    		   onBridgeReady(data);
		    		}
	    		 }
    		 }
    	}
    });
}

//放重复提交变量
var submitFlag = 0;
//打赏 微信支付
function onBridgeReady(data) {
	if(!submitFlag){
		submitFlag++;
	}else {
		return;
	}
	var orderNo = data.orderNo;
	var courseId = $("#courseId").val();
	WeixinJSBridge.invoke('getBrandWCPayRequest', {
		"appId" : data.appId, // 公众号名称,由商户传入     
		"timeStamp" : data.timeStamp, // 时间戳,自1970年以来的秒数     
		"nonceStr" : data.nonceStr, // 随机串     
		"package" : "prepay_id=" + data.prepayId, // 随机串     
		"signType" : "MD5", // 微信签名方式 
		"paySign" : data.paySign
	// 微信签名 
	}, function(res) {
		submitFlag = 0;
		// 把对象转字符串
		// 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。 
		if (res.err_msg == "get_brand_wcpay_request:ok") {
			$.ajax({
				type : 'post',
				url : 'shippingBuyCourseMobile/updateOrderStatus.htm',
				data : {
					orderNo : orderNo
				},
				dataType : 'text',
				success : function(data) {
					window.location.href = "shippingCourseMobileSearch/shippingCourseMobileDetailIndex.htm?courseId="+courseId;
				}
			});
		} else {
			alert("打赏失败!");
		}
	});
}


作者:Yasha009 发表于2016/11/24 16:38:02 原文链接
阅读:66 评论:1 查看评论

IOS-CoreData的使用详解

$
0
0

前言:很多小的App只需要一个ManagedContext在主线程就可以了,但是有时候对于CoreData的操作要耗时很久的,比如App开启的时候要载入大量数据,如果都放在主线程,毫无疑问会阻塞UI造成用户体验很差。通常的方式是,主线程一个ManagedContext处理UI相关的,后台一个线程的ManagedContext负责耗时操作的,操作完成后通知主线程。使用CoreData的并行主要有两种方式

Notification child/parent context
何时会使用到后台-简单来说就是要耗费大量时间,如果在主线程上会影响用户体验的时候。
例如:导入大量数据 执行大量计算

CoreData与线程安全

CoreData不是线程安全的,对于ManagedObject以及ManagedObjectContext的访问都只能在对应的线程上进行,而不能跨线程。
有几条自己总结的规则:
对于多个线程,每个线程使用自己独立的ManagedContext 对于线程间需要传递ManagedObject的,传递ManagedObject ID,通过objectWithID或者existingObjectWithID来获取 对于持久化存储协调器(NSPersistentStoreCoordinator)来说,可以多个线程共享一个NSPersistentStoreCoordinator

CoreData几个对象的介绍如下:

具体实现之前先来认识如下几个对象

(1)NSManagedObjectModel(被管理的对象模型)

相当于实体,不过它包含 了实体间的关系

(2)NSManagedObjectContext(被管理的对象上下文)

操作实际内容

作用:插入数据 查询 更新 删除

(3)NSPersistentStoreCoordinator(持久化存储助理)

相当于数据库的连接器

(4)NSFetchRequest(获取数据的请求)

相当于查询语句

(5)NSPredicate(相当于查询条件)

(6)NSEntityDescription(实体结构)

为了方便实现,本文整理一个数据管理类来测试CoreData:CoreDataManager

CoreDataManager.h

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface CoreDataManager : NSObject<NSCopying>

@property(strong,nonatomic,readonly)NSManagedObjectModel* managedObjectModel;//管理数据模型

@property(strong,nonatomic,readonly)NSManagedObjectContext* managedObjectContext;//管理数据内容

@property(strong,nonatomic,readonly)NSPersistentStoreCoordinator* persistentStoreCoordinator;//持久化数据助理

//创建数据库管理者单例
+(instancetype)shareManager;

//插入数据
-(void)insertData:(NSString*)tempName;

//删除数据
-(void)deleteData;

//删除数据
-(void)deleteData:(NSString*)tempName;

//查询数据
-(void)queryData;

//根据条件查询
-(void)queryData:(NSString*)tempName;

//更新数据
-(void)updateData:(NSString*)tempName;

@end

CoreDataManager.m

#import "CoreDataManager.h"
#import "Car.h"

static CoreDataManager *shareManager=nil;

@implementation CoreDataManager

@synthesize managedObjectContext =_managedObjectContext;

@synthesize managedObjectModel = _managedObjectModel;

@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;

//实例化对象
-(instancetype)init
{
    self=[super init];
    if (self) {

    }
    return self;
}

//创建数据库管理者单例
+(instancetype)shareManager
{
    //这里用到了双重锁定检查
    if(shareManager==nil){
        @synchronized(self){
            if(shareManager==nil){
                shareManager =[[[self class]alloc]init];
            }
        }
    }
    return shareManager;
}

-(id)copyWithZone:(NSZone *)zone
{

    return shareManager;
}

+(id)allocWithZone:(struct _NSZone *)zone
{
    if(shareManager==nil){
        shareManager =[super allocWithZone:zone];
    }
    return shareManager;
}

//托管对象
-(NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel!=nil) {
        return _managedObjectModel;
    }

    NSURL* modelURL=[[NSBundle mainBundle] URLForResource:@"myCoreData" withExtension:@"momd"];
    _managedObjectModel=[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

//托管对象上下文
-(NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext!=nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator* coordinator=[self persistentStoreCoordinator];
    if (coordinator!=nil) {
        _managedObjectContext=[[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];//NSMainQueueConcurrencyType NSPrivateQueueConcurrencyType

        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}

//持久化存储协调器
-(NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator!=nil) {
        return _persistentStoreCoordinator;
    }
    NSString* docs=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject];
    NSURL* storeURL=[NSURL fileURLWithPath:[docs stringByAppendingPathComponent:@"myCoreData.sqlite"]];
    NSLog(@"path is %@",storeURL);
    NSError* error=nil;
    _persistentStoreCoordinator=[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Error: %@,%@",error,[error userInfo]);
    }
    return _persistentStoreCoordinator;
}

//插入数据
-(void)insertData:(NSString*)tempName
{
    //读取类
    Car *car=[NSEntityDescription insertNewObjectForEntityForName:@"Car" inManagedObjectContext:self.managedObjectContext];
    car.name=tempName;
    //保存
    NSError *error;
    [self.managedObjectContext save:&error];
}

//删除数据
-(void)deleteData
{
    //创建读取类
    NSEntityDescription *entity =[NSEntityDescription entityForName:@"Car" inManagedObjectContext:self.managedObjectContext];

    //创建连接
    NSFetchRequest* request=[[NSFetchRequest alloc] init];
    [request setEntity:entity];

    //启动查询
    NSError *error;
    NSArray *deleteArr=[self.managedObjectContext executeFetchRequest:request error:&error];
    if(deleteArr.count){
        for (Car *car in deleteArr) {
            [self.managedObjectContext deleteObject:car];
        }
        NSError *error;
        [self.managedObjectContext save:&error];
    }else{
        NSLog(@"未查询到可以删除的数据");
    }

}

//删除数据
-(void)deleteData:(NSString*)tempName;
{
    //创建读取类
    NSEntityDescription *entity =[NSEntityDescription entityForName:@"Car" inManagedObjectContext:self.managedObjectContext];

    //创建连接
    NSFetchRequest* request=[[NSFetchRequest alloc] init];
    [request setEntity:entity];

    //创建检索条件
    NSPredicate *predicate =[NSPredicate predicateWithFormat:@"name=%@",tempName];
    [request setPredicate:predicate];

    //启动查询
    NSError *error;
    NSArray *deleteArr=[self.managedObjectContext executeFetchRequest:request error:&error];
    if(deleteArr.count){
        for (Car *car in deleteArr) {
            [self.managedObjectContext deleteObject:car];
        }
         NSError *error;
        [self.managedObjectContext save:&error];
    }else{
        NSLog(@"未查询到可以删除的数据");
    }
}


//查询数据
-(void)queryData
{
    //创建读取类
    NSEntityDescription *entity =[NSEntityDescription entityForName:@"Car" inManagedObjectContext:self.managedObjectContext];

    //创建连接
    NSFetchRequest* request=[[NSFetchRequest alloc] init];
    [request setEntity:entity];

    //启动查询
    NSError *error;
    NSArray *carArr=[self.managedObjectContext executeFetchRequest:request error:&error];
    for(Car *car in carArr){
        NSLog(@"car---->%@",car.name);
    }

}

-(void)queryData:(NSString*)tempName
{
    //创建读取类
    NSEntityDescription *entity =[NSEntityDescription entityForName:@"Car" inManagedObjectContext:self.managedObjectContext];

    //创建连接
    NSFetchRequest* request=[[NSFetchRequest alloc] init];
    [request setEntity:entity];

    //创建检索条件
    NSPredicate *predicate =[NSPredicate predicateWithFormat:@"name=%@",tempName];
    [request setPredicate:predicate];

    //启动查询
    NSError *error;
    NSArray *carArr=[self.managedObjectContext executeFetchRequest:request error:&error];
    for(Car *car in carArr){
        NSLog(@"car---->%@",car.name);
    }

}

//更新数据
-(void)updateData:(NSString*)tempName
{
    //创建读取类
    NSEntityDescription *entity =[NSEntityDescription entityForName:@"Car" inManagedObjectContext:self.managedObjectContext];

    //创建连接
    NSFetchRequest* request=[[NSFetchRequest alloc] init];
    [request setEntity:entity];

    //创建检索条件
    NSPredicate *predicate =[NSPredicate predicateWithFormat:@"name=%@",tempName];
    [request setPredicate:predicate];

    //启动查询
    NSError *error;
    NSArray *deleteArr=[self.managedObjectContext executeFetchRequest:request error:&error];
    if(deleteArr.count){
        for (Car *car in deleteArr) {
            car.name=@"test";
        }
        NSError *error;
        [self.managedObjectContext save:&error];
    }else{
        NSLog(@"未查询到可以删除的数据");
    }

}

@end

测试一下效率:测试数据10000条

NSMutableArray *testArray =[[NSMutableArray alloc]init];
           int testMaxCount =10000;
           for(int i=0;i<testMaxCount;i++){
               NSString *string = [[NSString alloc] initWithFormat:@"%d",i];
              [testArray addObject:string];
           }

            //测试一下效率  第1种
           CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
           for(NSString *tempName in testArray){
                [[CoreDataManager shareManager]insertData:tempName];
           }
           CFAbsoluteTime end=CFAbsoluteTimeGetCurrent();
           NSLog(@"coreData数据插入 time cost: %0.3f", end - start);

           //测试一下效率  第2种
             start = CFAbsoluteTimeGetCurrent();
            [[CoreDataManager shareManager]insertDatas:testArray];
             end=CFAbsoluteTimeGetCurrent();
            NSLog(@"coreData数据插入 time cost: %0.3f", end - start);

insertData函数:

//插入数据
-(void)insertData:(NSString*)tempName
{
    //读取类
    Car *car=[NSEntityDescription insertNewObjectForEntityForName:@"Car" inManagedObjectContext:self.managedObjectContext];
    car.name=tempName;
    //保存
    NSError *error;
    [self.managedObjectContext save:&error];
}

insertDatas函数:

//插入数据
-(void)insertDatas:(NSArray*)tempNames
{
    for(NSString *name in tempNames){
        Car *car=[NSEntityDescription insertNewObjectForEntityForName:@"Car" inManagedObjectContext:self.managedObjectContext];
        car.name=name;
    }
    NSError *error;
    [self.managedObjectContext save:&error];

}

**运行结果:
第一种:8.408
第二种:0.162
但是有个超级大的问题,
第二种方式虽然效率高,但是插入数据乱序。
第一种正常但是效率超低,同样近似的数据量sqlite效率比这个高不知多少倍。
Coredata批量操作支持的不太好。
另外:CoreData 的数据模型升级兼容性比较差,如果模型不对,会导致程序连起都起不来。虽然提供了模型升级代码,但是在客户端的管理模型版本管理也会相对复杂。**

并行的解决方案之Notification

简单来说,就是不同的线程使用不同的context进行操作,当一个线程的context发生变化后,利用notification来通知另一个线程Context,另一个线程调用mergeChangesFromContextDidSaveNotification来合并变化。

Notification的种类

NSManagedObjectContextObjectsDidChangeNotification 当Context中的变量改变时候触发。

NSManagedObjectContextDidSaveNotification 在一个context调用save完成以后触发。注意,这些managed object只能在当前线程使用,如果在另一个线程响应通知,要调用mergeChangesFromContextDidSaveNotification来合并变化。

NSManagedObjectContextWillSaveNotification。将要save。
一种比较好的iOS模式就是使用一个NSPersistentStoreCoordinator,以及两个独立的Contexts,一个context负责主线程与UI协作,一个context在后台负责耗时的处理。

为什么说这样做的效率更高?

这样做两个context共享一个持久化存储缓存,而且这么做互斥锁只需要在sqlite级别即可。设置当主线程只读的时候,都不需要锁。

并行的解决方案之child/parent context

ChildContext和ParentContext是相互独立的。只有当ChildContext中调用Save了以后,才会把这段时间来Context的变化提交到ParentContext中,ChildContext并不会直接提交到NSPersistentStoreCoordinator中, parentContext就相当于它的NSPersistentStoreCoordinator。

这其中有几点要注意

通常主线程context使用NSMainQueueConcurrencyType,其他线程childContext使用NSPrivateQueueConcurrencyType. child和parent的特点是要用Block进行操作,performBlock,或者performBlockAndWait,保证线程安全。
这两个函数的区别是performBlock不会阻塞运行的线程,相当于异步操作,performBlockAndWait会阻塞运行线程,相当于同步操作。
举例
和上述类似,这次不需要监听变化,因为变化会自动提交到mainContext。

CoreData线程安全问题

NSManagedObjectContext不是线程安全的,只能在创建NSManagedObjectContext的那个线程里访问它。一个数据库有多个UIManagedDocument和context,它们可以在不同的线程里创建,只要能管理好它们之间的关系就没问题。

线程安全的意思是,程序可能会崩溃,如果多路访问同一个NSManagedObjectContext,或在非创建实例的线程里访问实例,app就会崩溃。对此要怎么做呢?NSManagedObjectContext有个方法叫performBlock可以解决这个问题:

[context performBlock:^{   //or performBlockAndWait:
    // do stuff with context
}];

它会自动确保block里的东西都在正确的context线程里执行,但这不一定就意味着使用了多线程。事实上,如果在主线程下创建的context,那么这个block会回到主线程来,而不是在其他线程里运行,这个performBlock只是确保block运行在正确的线程里。

NSManagedObjectContext,包括所有使用SQL的Core Data,都有一个parentContext,这就像是另一个NSManagedObjectContext,在真正写入数据库之前要写入到这里。可以获取到parentContext,可以让parentContext调用performBlock来做一些事,这总是在另一个线程里进行。parentContext和创建的NSManagedObjectContext不在一个线程里运行,可以通过performBlock在那个线程里执行你要做的事。记住,如果改变了parentContext,必须保存,然后重新获取child context。如果你想在非主线程载入很多内容,那么就全部放入数据库,然后在主线程去获取,这个效率非常快。

作者:Maxdong24 发表于2016/11/24 16:38:39 原文链接
阅读:65 评论:0 查看评论

Android混淆打包

$
0
0

关于Android 混淆的分享,哪里不对及时纠正
在项目的build.gradle文件中正式环境的配置中

release {   
  // 不显示Log   会在 BuildConfig 这个类中生成一个“LOG_DEBUG”的静态变量
  buildConfigField "boolean", "LOG_DEBUG", "false"    
  minifyEnabled true    
  //Zipalign优化    
  zipAlignEnabled true
  // 移除无用的resource文件    
  shrinkResources true    
  //加载默认混淆配置文件 progudard-android.txt在sdk目录里面,不用管,proguard.cfg是我们自己配的混淆文  件    
  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'    
  //签名    
  signingConfig signingConfigs.relealse
}


//签名    直接配置签名文件
signingConfigs {        
  debug {//            
  storeFile file(".../key.keystore")        
  }       
   relealse {            
  storeFile file(".../key.keystore")            
  storePassword "123456"            
  keyAlias "keyAlias "            
  keyPassword "123456"        
  }    
}

这是截图
Paste_Image.png

混淆的文件配置 proguard-rules.pro

# 代码混淆压缩比,在0~7之间,默认为5,一般不做修改
-optimizationpasses 5


# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames


# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses


# 这句话能够使我们的项目混淆后产生映射文件# 包含有类名->混淆后类名的映射关系
-verbose


# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers


# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify


# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses


# 避免混淆泛型-keepattributes Signature
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable


# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*


# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆
# 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService


# 保留support下的所有类及其内部类
-keep class android.support.** {*;}


# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**


# 保留R下面的资源
-keep class **.R$* {*;}


# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {    
    native <methods>;
}


# 保留在Activity中的方法参数是view的方法,# 这样以来我们在layout中写的onClick就不会被影响
-keepclassmembers class * extends android.app.Activity{    
    public void *(android.view.View);
}


# 保留枚举类不被混淆
-keepclassmembers enum * {    
    public static **[] values();    
    public static ** valueOf(java.lang.String);
}


# 保留我们自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View{    
    *** get*();    
    void set*(***);    
    public <init>(android.content.Context);    
    public <init>(android.content.Context, android.util.AttributeSet);    
    public <init>(android.content.Context, android.util.AttributeSet, int);
}


# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {    
    public static final android.os.Parcelable$Creator *;
}


# 保留Serializable序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {    
    static final long serialVersionUID;    
    private static final java.io.ObjectStreamField[] serialPersistentFields;    
    !static !transient <fields>;    
    !private <fields>;    
    !private <methods>;    
    private void writeObject(java.io.ObjectOutputStream);    
    private void readObject(java.io.ObjectInputStream);    
    java.lang.Object writeReplace();    
    java.lang.Object readResolve();
}


# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {    
    void *(**On*Event);    
    void *(**On*Listener);
}


# webView处理,项目中没有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {    
    public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {    
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);    
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {    
    public void *(android.webkit.webView, jav.lang.String);
}


# 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用
# 记得proguard-android.txt中一定不要加-dontoptimize才起作用
# 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制
#-assumenosideeffects class android.util.Log {
#    public static int v(...);
#    public static int i(...);
#    public static int w(...);
#    public static int d(...);
#    public static int e(...);
}
#以上都是基本不变的
#实体类
......


############################################### 
# 三方包
############################################### 

# Butterknife
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {    @butterknife.* <fields>;}
-keepclasseswithmembernames class * {    @butterknife.* <methods>;}


# OkHttp3-dontwarn okhttp3.logging.**
-keep class okhttp3.internal.**{*;}
-dontwarn okio.**


# Okio
-dontwarn com.squareup.**
-dontwarn okio.**
-keep public class org.codehaus.* { *; }
-keep public class java.nio.* { *; }


# Retrofit
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Exceptions# RxJava
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { 
    long producerIndex; 
    long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { 
    rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef { 
    rx.internal.util.atomic.LinkedQueueNode consumerNode;
}

常用的自定义混淆规则

# 不混淆某个类
-keep public class name.huihui.example.Test { *; }
# 不混淆某个包所有的类
-keep class name.huihui.test.** { *; }
# 不混淆某个类的子类
-keep public class * extends name.huihui.example.Test { *; }
# 不混淆所有类名中包含了“model”的类及其成员
-keep public class **.*model*.** {*;}
# 不混淆某个接口的实现
-keep class * implements name.huihui.example.TestInterface { *; }
# 不混淆某个类的构造方法
-keepclassmembers class name.huihui.example.Test { 
  public <init>(); 
}
# 不混淆某个类的特定的方法
-keepclassmembers class name.huihui.example.Test { 
  public void test(java.lang.String); 
}
# 不混淆某个类的内部类
-keep class name.huihui.example.Test$* {
      *;
}

一些三方混淆网上都能找到

以下是混淆的语法 借鉴的 一叶飘舟

-include {filename} 从给定的文件中读取配置参数
-basedirectory {directoryname} 指定基础目录为以后相对的档案名称
-injars {class_path} 指定要处理的应用程序jar,war,ear和目录
-outjars {class_path} 指定处理完后要输出的jar,war,ear和目录的名称
-libraryjars {classpath} 指定要处理的应用程序jar,war,ear和目录所需要的程序库文件
-dontskipnonpubliclibraryclasses 指定不去忽略非公共的库类。
-dontskipnonpubliclibraryclassmembers 指定不去忽略包可见的库类的成员。

保留选项
-keep {Modifier} {class_specification} 保护指定的类文件和类的成员
-keepclassmembers {modifier} {class_specification} 保护指定类的成员,如果此类受到保护他们会保护的更好
-keepclasseswithmembers {class_specification} 保护指定的类和类的成员,但条件是所有指定的类和类成员是要存在。
-keepnames {class_specification} 保护指定的类和类的成员的名称(如果他们不会压缩步骤中删除)
-keepclassmembernames {class_specification} 保护指定的类的成员的名称(如果他们不会压缩步骤中删除)
-keepclasseswithmembernames {class_specification} 保护指定的类和类的成员的名称,如果所有指定的类成员出席(在压缩步骤之后)
-printseeds {filename} 列出类和类的成员
-keep选项的清单,标准输出到给定的文件

压缩
-dontshrink 不压缩输入的类文件 -printusage {filename}
-dontwarn 如果有警告也不终止
-whyareyoukeeping {class_specification}

优化
-dontoptimize 不优化输入的类文件
-assumenosideeffects {class_specification} 优化时假设指定的方法,没有任何副作用
-allowaccessmodification 优化时允许访问并修改有修饰符的类和类的成员

混淆
-dontobfuscate 不混淆输入的类文件
-printmapping {filename}
-applymapping {filename} 重用映射增加混淆
-obfuscationdictionary {filename} 使用给定文件中的关键字作为要混淆方法的名称
-overloadaggressively 混淆时应用侵入式重载
-useuniqueclassmembernames 确定统一的混淆类的成员名称来增加混淆
-flattenpackagehierarchy {package_name} 重新包装所有重命名的包并放在给定的单一包中
-repackageclass {package_name} 重新包装所有重命名的类文件中放在给定的单一包中 -dontusemixedcaseclassnames 混淆时不会产生形形色色的类名
-keepattributes {attribute_name,…} 保护给定的可选属性,例如LineNumberTable, LocalVariableTable, SourceFile, Deprecated, Synthetic, Signature, and

InnerClasses.
-renamesourcefileattribute {string} 设置源文件中给定的字符串常量

作者:u012659709 发表于2016/11/24 16:44:53 原文链接
阅读:66 评论:0 查看评论

Android四大组件——Service后台服务、前台服务、IntentService、跨进程服务、无障碍服务、系统服务

$
0
0

Service后台服务、前台服务、IntentService、跨进程服务、无障碍服务、系统服务


本篇文章包括以下内容:

前言

作为四大组件之一的Service类,是面试和笔试的必备关卡,我把我所学到的东西总结了一遍,相信你看了之后你会对Service娓娓道来,在以后遇到Service的问题胸有成竹,废话不多说,开车啦

Service简介

Service是Android中实现程序后台运行的解决方案,它非常适用于去执行那些不需要和用户交互而且还要求长期运行的任务。Service默认并不会运行在子线程中,它也不运行在一个独立的进程中,它同样执行在UI线程中,因此,不要在Service中执行耗时的操作,除非你在Service中创建了子线程来完成耗时操作

Service的运行不依赖于任何用户界面,即使程序被切换到后台或者用户打开另一个应用程序,Service仍然能够保持正常运行,这也正是Service的使用场景。当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行

后台服务

后台服务可交互性主要是体现在不同的启动服务方式,startService()和bindService()。bindService()可以返回一个代理对象,可调用Service中的方法和获取返回结果等操作,而startService()不行

不可交互的后台服务

不可交互的后台服务即是普通的Service,Service的生命周期很简单,分别为onCreate、onStartCommand、onDestroy这三个。当我们startService()的时候,首次创建Service会回调onCreate()方法,然后回调onStartCommand()方法,再次startService()的时候,就只会执行一次onStartCommand()。服务一旦开启后,我们就需要通过stopService()方法或者stopSelf()方法,就能把服务关闭,这时就会回调onDestroy()

一、创建服务类

创建一个服务非常简单,只要继承Service,并实现onBind()方法

public class BackGroupService extends Service {

    /**
     * 綁定服务时调用
     *
     * @param intent
     * @return
     */
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e("Service", "onBind");
        return null;
    }

    /**
     * 服务创建时调用
     */
    @Override
    public void onCreate() {
        Log.e("Service", "onCreate");
        super.onCreate();
    }

    /**
     * 执行startService时调用
     *
     * @param intent
     * @param flags
     * @param startId
     * @return
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e("Service", "onStartCommand");
        //这里执行耗时操作
        new Thread() {
            @Override
            public void run() {
                while (true){
                    try {
                        Log.e("Service", "doSomething");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 服务被销毁时调用
     */
    @Override
    public void onDestroy() {
        Log.e("Service", "onDestroy");
        super.onDestroy();
    }
}

二、配置服务

Service也是四大组件之一,所以必须在manifests中配置

<service android:name=".Service.BackGroupService"/>

三、启动服务和停止服务

我们通过两个按钮分别演示启动服务和停止服务,通过startService()开启服务,通过stopService()停止服务

public class MainActivity extends AppCompatActivity {

    Button bt_open, bt_close;

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

        bt_open = (Button) findViewById(R.id.open);
        bt_close = (Button) findViewById(R.id.close);

        final Intent intent = new Intent(this, BackGroupService.class);

        bt_open.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //启动服务
                startService(intent);
            }
        });

        bt_close.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //停止服务
                stopService(intent);
            }
        });
    }
}

当你开启服务后,还有一种方法可以关闭服务,在设置中,通过应用->找到自己应用->停止

四、运行代码

运行程序后,我们点击开始服务,然后一段时间后关闭服务。我们以Log信息来验证普通Service的生命周期:onCreate->onStartCommand->onDestroy

11-24 00:19:51.483 16407-16407/com.handsome.boke2 E/Service: onCreate
11-24 00:19:51.483 16407-16407/com.handsome.boke2 E/Service: onStartCommand
11-24 00:19:51.485 16407-16613/com.handsome.boke2 E/Service: doSomething
11-24 00:19:53.490 16407-16613/com.handsome.boke2 E/Service: doSomething
11-24 00:19:55.491 16407-16613/com.handsome.boke2 E/Service: doSomething
11-24 00:19:57.491 16407-16613/com.handsome.boke2 E/Service: doSomething
11-24 00:19:58.056 16407-16407/com.handsome.boke2 E/Service: onDestroy
11-24 00:19:59.492 16407-16613/com.handsome.boke2 E/Service: doSomething
11-24 00:20:01.494 16407-16613/com.handsome.boke2 E/Service: doSomething
11-24 00:20:03.495 16407-16613/com.handsome.boke2 E/Service: doSomething

其中你会发现我们的子线程进行的耗时操作是一直存在的,而我们Service已经被关闭了,关闭该子线程的方法需要直接通过Home键关闭该应用程序

可交互的后台服务

可交互的后台服务是指前台页面可以调用后台服务的方法,可交互的后台服务实现步骤是和不可交互的后台服务实现步骤是一样的,区别在于启动的方式和获得Service的代理对象

一、创建服务类

和普通Service不同在于这里返回一个代理对象,返回给前台进行获取,即前台可以获取该代理对象执行后台服务的方法

public class BackGroupService extends Service {

    /**
     * 綁定服务时调用
     *
     * @param intent
     * @return
     */
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e("Service", "onBind");
        //返回代理对象
        return new MyBinder();
    }

    /**
     * 代理类
     */
    class MyBinder extends Binder {
        public void showToast() {
            Log.e("Service", "showToast");
        }

        public void showList() {
            Log.e("Service", "showList");
        }
    }

    /**
     * 解除绑定服务时调用
     * @param intent
     * @return
     */
    @Override
    public boolean onUnbind(Intent intent) {
        Log.e("Service", "onUnbind");
        return super.onUnbind(intent);
    }

    /**
     * 服务创建时调用
     */
    @Override
    public void onCreate() {
        Log.e("Service", "onCreate");
        super.onCreate();
    }

    /**
     * 服务被销毁时调用
     */
    @Override
    public void onDestroy() {
        Log.e("Service", "onDestroy");
        super.onDestroy();
    }
}

二、配置服务

<service android:name=".Service.BackGroupService"/>

三、绑定服务和解除绑定服务

我们通过两个按钮分别演示绑定服务和解除绑定服务,通过bindService()开启服务,通过unbindService()停止服务

public class MainActivity extends AppCompatActivity {

    Button bt_open, bt_close;

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

        bt_open = (Button) findViewById(R.id.open);
        bt_close = (Button) findViewById(R.id.close);

        final Intent intent = new Intent(this, BackGroupService.class);

        bt_open.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                bindService(intent, conn, BIND_AUTO_CREATE);
            }
        });

        bt_close.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                unbindService(conn);
            }
        });


    }

    ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //拿到后台服务代理对象
            BackGroupService.MyBinder myBinder = (BackGroupService.MyBinder) service;
            //调用后台服务的方法
            myBinder.showToast();
            myBinder.showList();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
}

这里和startService的区别在于多了一个ServiceConnection对象,该对象是用户绑定后台服务后,可获取后台服务代理对象的回调,我们可以通过该回调,拿到后台服务的代理对象,并调用后台服务定义的方法,也就实现了后台服务和前台的交互

四、运行代码

运行程序后,我们点击绑定服务,然后一段时间后解除绑定服务。我们以Log信息来验证Service的生命周期:onCreate->onBind->onUnBind->onDestroy,其中也可以看到我们调用后台服务的方法showToast和showList

11-24 00:55:32.775 15408-15408/com.handsome.boke2 E/Service: onCreate
11-24 00:55:32.775 15408-15408/com.handsome.boke2 E/Service: onBind
11-24 00:55:32.796 15408-15408/com.handsome.boke2 E/Service: showToast
11-24 00:55:32.796 15408-15408/com.handsome.boke2 E/Service: showList
11-24 00:55:34.290 15408-15408/com.handsome.boke2 E/Service: onUnbind
11-24 00:55:34.290 15408-15408/com.handsome.boke2 E/Service: onDestroy

混合性交互的后台服务

或许你会迷惑,startService和bindService之间有什么关系?其实简单的说两者之间是没有关联的,类似于你亲妈生了个双胞胎一样,只有纯粹的血缘关系。那么问题来了,这两个启动方式是否可以同时使用呢,答案是可以的

将上面两种启动方式结合起来就是混合性交互的后台服务了,即可以单独运行后台服务,也可以运行后台服务中提供的方法,其完整的生命周期是:onCreate->onStartCommand->onBind->onUnBind->onDestroy

前台服务

由于后台服务优先级相对比较低,当系统出现内存不足的情况下,它就有可能会被回收掉,所以前台服务就是来弥补这个缺点的,它可以一直保持运行状态而不被系统回收。例如:墨迹天气在状态栏中的天气预报

一、创建服务类

前台服务创建很简单,其实就在Service的基础上创建一个Notification,然后使用Service的startForeground()方法即可启动为前台服务

public class ForegroundService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        showNotification();
    }

    /**
     * 启动前台通知
     */
    private void showNotification() {
        //创建通知详细信息
        Notification.Builder mBuilder = new Notification.Builder(this)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("2016年11月24日")
                .setContentText("今天天气阴天,8到14度");
        //创建点击跳转Intent
        Intent intent = new Intent(this, MainActivity.class);
        //创建任务栈Builder
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
        stackBuilder.addParentStack(MainActivity.class);
        stackBuilder.addNextIntent(intent);
        PendingIntent pendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT);
        //设置跳转Intent到通知中
        mBuilder.setContentIntent(pendingIntent);
        //获取通知服务
        NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        //构建通知
        Notification notification = mBuilder.build();
        //显示通知
        nm.notify(0, notification);
        //启动为前台服务
        startForeground(0, notification);
    }
}

二、配置服务

<service android:name=".Service.ForegroundService"/>

三、启动前台服务

startService(new Intent(this, ForegroundService.class));

四、运行代码

我们可以看到状态栏确实增加了我们这条通知

当我们将该程序退出并杀掉的时候,通过设置->应用->选择正在运行中的应用,我们可以发现,我们的程序退出杀掉了,而服务还在进行着

IntentService

IntentService是专门用来解决Service中不能执行耗时操作这一问题的,创建一个IntentService也很简单,只要继承IntentService并覆写onHandlerIntent函数,在该函数中就可以执行耗时操作了

public class TheIntentService extends IntentService {

    public TheIntentService(String name) {
        super(name);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //在这里执行耗时操作
    }
}

AIDL跨进程服务

关于AIDL跨进程服务的使用和原理分析,可以见我另一篇博客:Android基础——初学者必知的AIDL在应用层上的Binder机制

AccessibilityService无障碍服务

关于AccessibilityService无障碍服务的使用和实例,可以见我另一篇博客:Android进阶——学习AccessibilityService实现微信抢红包插件

系统服务

系统服务提供了很多便捷服务,可以查询Wifi、网络状态、查询电量、查询音量、查询包名、查询Application信息等等等相关多的服务,具体大家可以自信查询文档,这里举例几个常见的服务

1. 判断Wifi是否开启

WifiManager wm = (WifiManager) getSystemService(WIFI_SERVICE);
boolean enabled = wm.isWifiEnabled();

需要权限

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

2. 获取系统最大音量

AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
int max = am.getStreamMaxVolume(AudioManager.STREAM_SYSTEM);

3. 获取当前音量

AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
int current = am.getStreamMaxVolume(AudioManager.STREAM_RING);

4. 判断网络是否有连接

ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
boolean isAvailable = info.isAvailable();

需要权限

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

部分源码下载

源码下载

作者:qq_30379689 发表于2016/11/24 16:46:12 原文链接
阅读:290 评论:0 查看评论

【腾讯TMQ】敏捷测试-快速俘虏产品&开发

$
0
0

快速互联网的状态下,测试的价值体现在哪里?俗话说,长江后浪推前浪,前浪拍死沙滩上。我们在新人面前标签应该不仅限于工龄属性上的增长,在经验累积上也是有加分项的。那么问题来了,能体现我们经验值的有什么呢?

今年比较喜闻乐见的词并且能体现测试的价值体现——测试分析。术业有专攻,每个行业都有行业的专长,个人认为“快准猛”是可以拿来衡量每个行业的价值所在,无论是传统行业,还是互联网行业。比如,医生可以很快的定位出病人的病痛;测试人员可以很快找到bug所在。而测试分析目的是为了通过分析,可以更快的找到bug。

怎么快速提升测试分析呢?我们测试分析的对象是产品的需求,是开发写的代码。那既然是读需求,读代码,如何用简单易用的办法快速提升自己准确的读需求,读代码能力?

这里插入一个段子,女朋友(产品)让男朋友(开发)做个需求:你回来的路上,去超市买点橙子回来;如果看到西瓜,就买个西瓜。男朋友在路上看到西瓜,就只买了西瓜回来。这可以说是程序员思维的if else,但是也是没好好读好需求,注孤生的节奏啊。今天手把手把产品跟开发拿下,无论说什么都能完美的理解。

一、读需求

文绉绉的需求,怎么快速的理解需求,并且转换成我们想要的内容呢?产品的少女心特别难俘虏跟读懂,从获取需求,定义需求的边界,获取业务用例是一个系列的过程,教你两招读心术:

1.多想–想破坏,扣字眼

破坏:读需求的时候,存在一万个怎么办,需要任何怎么办都需要有路径进行解决,这种办法可以比较快的确定需求的边界,这种可以选择典型的探索性摸索。要是产品MM要买西瓜的时候,如果没有西瓜呢,问一下要不买个苹果?还是不买了?

扣字眼:NLP语言是一种语言模型,讲一句话需求划分为很多种类型的词汇,通过对词汇的深刻理解,进行理解需求,也是目前做需求分析的很热门的一种方式。产品MM说要买一个苹果?纳尼,我要问问是6块钱一斤的苹果,还是6888一斤的那个苹果。

这两个都在TMQ里面是热门文章,有兴趣的人请详细阅读。

探索性日常采用的方法总结

NLP典型案例解析

2.多画—建立业务模型分析

产品MM哪天跟我说了好几个路径,又买苹果,又买橙子,又买西瓜的,我头都听大了,还要从中华广场买的苹果,从维多利亚买的橙子,在华景路买的西瓜,想想都心塞,最后她自己都不知道自己要什么。这个时候从需求分析的时候,动手建立业务模型。业务模型的建立,有易于我们对需求的理解,而且建立一个平等的可以互相理解的沟通平台。白纸黑字很多时候也会存在歧义,所以我们要对一个信息进行二次确认的时候,建议使用UML建模语言。

一份优秀的需求的特性是:完整的,正确的,精准的,可行的,必要的,无歧义的。从优秀的需求上出发,在做需求分析的过程中,我们就可以更好的理解需求的含义。

二、读代码

这是一门偷窥开发GG每日做什么的最佳手段,但是一般人肯定不想身边有个测试妹子碎碎问。所以要采用下面的几个读心术的工具,来知道我们的开发怎么实现某些功能。学习代码其实是上一门编程课程,但是我们的老师是谁呢?应该把我们的开发当做我们的老师,多听,多看他们是怎么实现需求。

1.多听—手段1 CR

CR全称叫做codereview,当我们完全没有基础的情况下,多上CR,认真听讲,认真做笔记,甚至CR后自己写一遍伪代码来学习功能模块。
CR面向的对象是全部人员,主讲人是开发,讲述的是新功能的实现逻辑,每个API是怎么封装的,每个跳转是怎么实现的,这个UI是怎么布局的。大家会有谈论环节,多人讨论主要讨论点:

  这个可能会出现异常?这样子字符串转换会出现crash。
  这里函数会有性能问题?为什么要前台进程执行?
  这里会不会存在安全性问题?
  这个API可能会有性能问题,提取MD5的时间比较长
  这里的try catch 的范围不对,这里锁的范围不准

通过CR我们可以学习到需求是怎么实现的原理,还可以学习关于性能,安全的问题。在听讲的过程,不断思考自己的测试路径是否会覆盖到这些异常路径,测试过程是否存在优化。

比如这类型的CR结果,我们要学会是否在测试用例中能够覆盖,后续怎么避免该类型的问题,用例的设计以及选取方便是否可以更精简的路线

2.多听—手段2 根因分析

这里的根因分析面向的对象也是开发。每个版本末,开发对这个版本里面出现的bug的根因进行总结,并且给FT讲解。根因分析是CR的完美闭环,在项目开发过程中,发现bug的时间延后,版本质量风险更大,为此我们也在前期做了冒烟测试;做了bug总结分析,是否可以把bug提前发现,根因分析这类型的bug在CR是否可以被提前发现,是否有类似问题可以总结归纳成方法。比如上次通过UI适配的问题,总结了当前主要采用UI还原的集中方式主要有:

根因分析讲解的是bug为什么会出现,这个bug的实现逻辑是怎么样子的,怎么解决这个bug,是沟通问题?UI问题?需求问题?等

根因分析是测试分析的二次解读,是否自己的测试分析能够覆盖到这个路径;是否下次遇到类似问题,我们可以怎么减少时间去测试且不漏测。

3.多看—手段1 看更新代码

自已看开发每日更新代码

通过关注开发的提交的代码,大知识难消化,那么就切成多个小部分的查看。通过观察开发每日提交的代码,查看这个代码的修改点是什么,是否在自己的覆盖范围内,完善自己的测试分析。

在CR的累计基础上,小部分的代码消化会比较快,而且测试路径也比较准,测试分析的时候关注影响的测试范围点是什么。

每日code 关注

4.多看—手段2 看svndiff

如果你不懂某一行代码的意思,那么你就把这个代码给注释掉去运行,查看是否会有什么明显的变化。“对比“是一种很好的学习办法,svn diff 就是测试分析的利器,今天突然需求增加;今天突然砍掉这个功能;今天只修改了这个bug,你测试一下?what……

这里主要介绍两个svn diff 的利器,CR客户端&svnlog

CR客户端,是腾讯自研的一个客户端,

从上图可以看出,客户端里面可以根据一个svn的基版本跟右版本进行对比,并且输出对比文件,双击文件可以查看到diff 的内容。

Svnlog,是svn自带的工具,可以查看开发提交的日志,通过多选svn的记录,右键,copy to clipboard, 再拷贝到记事本里面,可以查看连续的几个开发的日志,并且修改的文件

5.多想—如果是你,你怎么做

大胆猜测如果你的角色是开发,你会怎么做实现这个功能,怎么抽象表结构,类方法,属性,页面等,从模型设计到接口设计。测试分析要做那么多么?如果是小需求的话,其实在脑海里能够转换就行了;如果是比较大的需求,我们可能需要多涂鸦,从整体上查看是否有实现漏洞,或者需要多关注哪一些环节的测试要点。

6.多写多动手

不会写程序的产品不是好测试,摆脱开发做根因分析

孰能生巧,这绝对不是说假的。一个不懂开发的人,写了10年的代码,也是可以写出一些代码来。实践是唯一快速上手的事情,从写一个helloword 开始,到写一个Log日志,到尝试协助开发定位问题,在这个过程也会受益良多,并且比自己看半天代码的收益会很多,并且在这个过程,你对每个API的使用会更加的熟练,评估内容更加准确。

摆脱开发做根因分析,以往我们可能常常问下开发,为什么这样子,为什么会出现那样子,如果学会自己定位,第一次可能定位到package的问题,第二次定位到class的问题,再定位到method,细化到哪一行,哪一个调用,哪一个根因。逐渐的提升自己的代码能力,对测试分析评估是起了很大作用。

三、总结

上述的动作,词汇可能很简单,但是做起来其实是有难度的,学习最好的过程就是放下身段把自己当做新人进行学习,多听少讲。

测试分析感觉听起来像是一个开发,其实不然,测试分析没有必要跟着开发一样实现代码,但是至少能看懂开发的代码,知道开发解决的是什么问题,会不会影响以前的逻辑,会不会造成其他的bug。这个也是开发跟测试岗位的术业与专攻,我们关注的是从代码里面发觉更准确的测试路径,提前把bug更早的发现。

巧妙的使用好上述手段,其实我们已经完美的俘虏了产品跟开发的内心,更好的合作的前提就是互相了解,互相读懂,才能更好的做下一步的操作,并且通过简单的操作提升测试自身的能力。

原文链接:http://tmq.qq.com/2016/11/agile_testing_development/

关注我们的微信公众号查看完整内容哦~~~~

想知道更多测试相关干货
请关注我们的微信公众号:腾讯移动品质中心TMQ
二维码:
这里写图片描述

版权声明:腾讯TMQ拥有内容的全部版权,任何人或单位对本贴内容进行复制、转载时请申明原创腾讯tmq,否则将追究法律责任。

作者:TMQ1225 发表于2016/11/24 16:48:14 原文链接
阅读:69 评论:0 查看评论

时间戳转换

$
0
0
package com.whzg.zbjy.utils;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

public class DateUtils{

    public static String getTodayDateTime() {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",
                Locale.getDefault());
        return format.format(new Date());
    }

    /**
     * 掉此方法输入所要转换的时间输入例如("2014年06月14日16时09分00秒")返回时间戳 
     *
     * @param time
     * @return
     */
    public String data(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒",
                Locale.CHINA);
        Date date;
        String times = null;
        try {
            date = sdr.parse(time);
            long l = date.getTime();
            String stf = String.valueOf(l);
            times = stf.substring(0, 10);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return times;
    }

    public static String getTodayDateTimes() {
        SimpleDateFormat format = new SimpleDateFormat("MM月dd日",
                Locale.getDefault());
        return format.format(new Date());
    }

    /**
     * 获取当前时间 
     *
     * @return
     */
    public static String getCurrentTime_Today() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        return sdf.format(new java.util.Date());
    }

    /**
     * 调此方法输入所要转换的时间输入例如("2014-06-14-16-09-00")返回时间戳 
     *
     * @param time
     * @return
     */
    public static String dataOne(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss",
                Locale.CHINA);
        Date date;
        String times = null;
        try {
            date = sdr.parse(time);
            long l = date.getTime();
            String stf = String.valueOf(l);
            times = stf.substring(0, 10);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return times;
    }

    public static String getTimestamp(String time, String type) {
        SimpleDateFormat sdr = new SimpleDateFormat(type, Locale.CHINA);
        Date date;
        String times = null;
        try {
            date = sdr.parse(time);
            long l = date.getTime();
            String stf = String.valueOf(l);
            times = stf.substring(0, 10);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return times;
    }

    /**
     * 调用此方法输入所要转换的时间戳输入例如(1402733340)输出("2014年06月14日16时09分00秒") 
     *
     * @param time
     * @return
     */
    public static String times(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    /**
     * 调用此方法输入所要转换的时间戳输入例如(1402733340)输出("2014-06-14  16:09:00") 
     *
     * @param time
     * @return
     */
    public static String timedate(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    /**
     * 调用此方法输入所要转换的时间戳输入例如(1402733340)输出("2014年06月14日16:09") 
     *
     * @param time
     * @return
     */
    public static String timet(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy年MM月dd日  HH:mm");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    /**
     * 调用此方法输入所要转换的时间戳输入例如(1402733340)输出("06月14日16:09")
     *
     * @param time
     * @return
     */
    public static String timet1(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("MM月dd日  HH:mm");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    /**
     * @param time斜杠分开
     * @return
     */
    public static String timeslash(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy/MM/dd,HH:mm");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    /**
     * @param time斜杠分开
     * @return
     */
    public static String timeslashData(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy/MM/dd");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
//      int i = Integer.parseInt(time);  
        String times = sdr.format(new Date(lcc * 1000L));
        return times;

    }

    /**
     * @param time斜杠分开
     * @return
     */
    public static String timeMinute(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("HH:mm");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    public static String tim(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyyMMdd HH:mm");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;
    }

    public static String time(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;
    }

    // 调用此方法输入所要转换的时间戳例如(1402733340)输出("2014年06月14日16时09分00秒")  
    public static String times(long timeStamp) {
        SimpleDateFormat sdr = new SimpleDateFormat("MM月dd日  #  HH:mm");
        return sdr.format(new Date(timeStamp)).replaceAll("#",
                getWeek(timeStamp));

    }

    private static String getWeek(long timeStamp) {
        int mydate = 0;
        String week = null;
        Calendar cd = Calendar.getInstance();
        cd.setTime(new Date(timeStamp));
        mydate = cd.get(Calendar.DAY_OF_WEEK);
        // 获取指定日期转换成星期几  
        if (mydate == 1) {
            week = "周日";
        } else if (mydate == 2) {
            week = "周一";
        } else if (mydate == 3) {
            week = "周二";
        } else if (mydate == 4) {
            week = "周三";
        } else if (mydate == 5) {
            week = "周四";
        } else if (mydate == 6) {
            week = "周五";
        } else if (mydate == 7) {
            week = "周六";
        }
        return week;
    }

    // 并用分割符把时间分成时间数组  
    /**
     * 调用此方法输入所要转换的时间戳输入例如(1402733340)输出("2014-06-14-16-09-00") 
     *
     * @param time
     * @return
     */
    public String timesOne(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    public static String timesTwo(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        return times;

    }

    /**
     * 并用分割符把时间分成时间数组 
     *
     * @param time
     * @return
     */
    public static String[] timestamp(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
        @SuppressWarnings("unused")
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        String[] fenge = times.split("[年月日时分秒]");
        return fenge;
    }

    /**
     * 根据传递的类型格式化时间 
     *
     * @param str
     * @param type
     *            例如:yy-MM-dd 
     * @return
     */
    public static String getDateTimeByMillisecond(String str, String type) {

        Date date = new Date(Long.valueOf(str));

        SimpleDateFormat format = new SimpleDateFormat(type);

        String time = format.format(date);

        return time;
    }

    /**
     * 分割符把时间分成时间数组 
     *
     * @param time
     * @return
     */
    public String[] division(String time) {

        String[] fenge = time.split("[年月日时分秒]");

        return fenge;

    }

    /**
     * 输入时间戳变星期 
     *
     * @param time
     * @return
     */
    public static String changeweek(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        Date date = null;
        int mydate = 0;
        String week = null;
        try {
            date = sdr.parse(times);
            Calendar cd = Calendar.getInstance();
            cd.setTime(date);
            mydate = cd.get(Calendar.DAY_OF_WEEK);
            // 获取指定日期转换成星期几  
        } catch (Exception e) {
            // TODO Auto-generated catch block  
            e.printStackTrace();
        }
        if (mydate == 1) {
            week = "星期日";
        } else if (mydate == 2) {
            week = "星期一";
        } else if (mydate == 3) {
            week = "星期二";
        } else if (mydate == 4) {
            week = "星期三";
        } else if (mydate == 5) {
            week = "星期四";
        } else if (mydate == 6) {
            week = "星期五";
        } else if (mydate == 7) {
            week = "星期六";
        }
        return week;

    }

    /**
     * 获取日期和星期 例如:2014-11-13 11:00 星期一 
     *
     * @param time
     * @param type
     * @return
     */
    public static String getDateAndWeek(String time, String type) {
        return getDateTimeByMillisecond(time + "000", type) + "  "
                + changeweekOne(time);
    }

    /**
     * 输入时间戳变星期 
     *
     * @param time
     * @return
     */
    public static String changeweekOne(String time) {
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        long lcc = Long.valueOf(time);
        int i = Integer.parseInt(time);
        String times = sdr.format(new Date(i * 1000L));
        Date date = null;
        int mydate = 0;
        String week = null;
        try {
            date = sdr.parse(times);
            Calendar cd = Calendar.getInstance();
            cd.setTime(date);
            mydate = cd.get(Calendar.DAY_OF_WEEK);
            // 获取指定日期转换成星期几  
        } catch (Exception e) {
            // TODO Auto-generated catch block  
            e.printStackTrace();
        }
        if (mydate == 1) {
            week = "星期日";
        } else if (mydate == 2) {
            week = "星期一";
        } else if (mydate == 3) {
            week = "星期二";
        } else if (mydate == 4) {
            week = "星期三";
        } else if (mydate == 5) {
            week = "星期四";
        } else if (mydate == 6) {
            week = "星期五";
        } else if (mydate == 7) {
            week = "星期六";
        }
        return week;

    }

    /**
     * 获取当前时间 
     *
     * @return
     */
    public static String getCurrentTime() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        return sdf.format(new java.util.Date());
    }

    /**
     * 输入日期如(2014年06月14日16时09分00秒)返回(星期数) 
     *
     * @param time
     * @return
     */
    public String week(String time) {
        Date date = null;
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
        int mydate = 0;
        String week = null;
        try {
            date = sdr.parse(time);
            Calendar cd = Calendar.getInstance();
            cd.setTime(date);
            mydate = cd.get(Calendar.DAY_OF_WEEK);
            // 获取指定日期转换成星期几  
        } catch (Exception e) {
            // TODO Auto-generated catch block  
            e.printStackTrace();
        }
        if (mydate == 1) {
            week = "星期日";
        } else if (mydate == 2) {
            week = "星期一";
        } else if (mydate == 3) {
            week = "星期二";
        } else if (mydate == 4) {
            week = "星期三";
        } else if (mydate == 5) {
            week = "星期四";
        } else if (mydate == 6) {
            week = "星期五";
        } else if (mydate == 7) {
            week = "星期六";
        }
        return week;
    }

    /**
     * 输入日期如(2014-06-14-16-09-00)返回(星期数) 
     *
     * @param time
     * @return
     */
    public String weekOne(String time) {
        Date date = null;
        SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        int mydate = 0;
        String week = null;
        try {
            date = sdr.parse(time);
            Calendar cd = Calendar.getInstance();
            cd.setTime(date);
            mydate = cd.get(Calendar.DAY_OF_WEEK);
            // 获取指定日期转换成星期几  
        } catch (Exception e) {
            // TODO Auto-generated catch block  
            e.printStackTrace();
        }
        if (mydate == 1) {
            week = "星期日";
        } else if (mydate == 2) {
            week = "星期一";
        } else if (mydate == 3) {
            week = "星期二";
        } else if (mydate == 4) {
            week = "星期三";
        } else if (mydate == 5) {
            week = "星期四";
        } else if (mydate == 6) {
            week = "星期五";
        } else if (mydate == 7) {
            week = "星期六";
        }
        return week;
    }
}  


作者:duoluo9 发表于2016/11/24 16:53:32 原文链接
阅读:76 评论:0 查看评论

Android 4.4系统原生截图解析

$
0
0

这里写图片描述
本文是在MTK4.4的系统源码基础上进行分析的,如果和你的系统代码有出入,请以自己的系统代码为主。但基本的流程应该还是一样的。下面是分析4.4系统的截图实现。即power键+volume-键实现的截图。系统是在PhoneWindowManager类中实现监听按键事件,并响应相应的按键事件去实现截图。
./base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
首先,在PhoneWindowManager.java中看下截图的实现的。

Android 实现截图的方法

// Assume this is called from the Handler thread.
private void takeScreenshot() {
    synchronized (mScreenshotLock) {
        if (mScreenshotConnection != null) {
            return;
        }
        //初始化要绑定的服务,从这里可以看出要绑定的服务是SystemUI里的TakeScreenshotService
        ComponentName cn = new ComponentName("com.android.systemui",
                "com.android.systemui.screenshot.TakeScreenshotService");
        Intent intent = new Intent();
        intent.setComponent(cn);
        ServiceConnection conn = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                synchronized (mScreenshotLock) {
                    if (mScreenshotConnection != this) {
                        return;
                    }
                    Messenger messenger = new Messenger(service);
                    Message msg = Message.obtain(null, 1);
                    final ServiceConnection myConn = this;
                    Handler h = new Handler(mHandler.getLooper()) {
                        @Override
                        public void handleMessage(Message msg) {
                            synchronized (mScreenshotLock) {
                                if (mScreenshotConnection == myConn) {
                                    mContext.unbindService(mScreenshotConnection);
                                    mScreenshotConnection = null;
                                    mHandler.removeCallbacks(mScreenshotTimeout);
                                }
                            }
                        }
                    };
                    msg.replyTo = new Messenger(h);
                    msg.arg1 = msg.arg2 = 0;
                    //判断状态栏是否显示,主要用于截图后的退出动画
                    if (mStatusBar != null && mStatusBar.isVisibleLw())
                        msg.arg1 = 1;
                    //判断导航栏是否显示,主要用于截图后的退出动画
                    if (mNavigationBar != null && mNavigationBar.isVisibleLw())
                        msg.arg2 = 1;
                    try {
                        messenger.send(msg);
                    } catch (RemoteException e) {
                    }
                }
            }
            @Override
            public void onServiceDisconnected(ComponentName name) {}
        };
        //绑定Service
        if (mContext.bindServiceAsUser(
                intent, conn, Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
            mScreenshotConnection = conn;
            //设置超时机制,若超时就解除绑定
            mHandler.postDelayed(mScreenshotTimeout, 10000);
        }
    }
}

final Runnable mScreenshotTimeout = new Runnable() {
    @Override public void run() {
        synchronized (mScreenshotLock) {
            if (mScreenshotConnection != null) {
                mContext.unbindService(mScreenshotConnection);
                mScreenshotConnection = null;
            }
        }
    }
};

然后,再来看takeScreenshot是在哪里被调用的。

//takeScreenshot()放在Runnable里面
private final Runnable mScreenshotRunnable = new Runnable() {
    @Override
    public void run() {
        takeScreenshot();
    }
};

系统把takeScreenshot方法放在了mScreenshotRunnable里面了,通过源码知道mScreenshotRunnable在两个地方被调用了,一是当KEYCODE_SYSRQ键按下的时候开始截图,二是当KEYCODE_POWER和KEYCODE_VOLUME_DOWN同时按下时开始截图,接下来我们看下mScreenshotRunnable实际被被调用的代码实现。

方法一:直接按下KEYCODE_SYSRQ实现截图


/** {@inheritDoc} */
@Override
public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
......

    else if (keyCode == KeyEvent.KEYCODE_SYSRQ) {
        if (down && repeatCount == 0) {
            //监听按键KEYCODE_SYSRQ按下,实现系统截图
            mHandler.post(mScreenshotRunnable);
        }
        return -1;
    }
......
}

方法二:KEYCODE_POWER和KEYCODE_VOLUME_DOWN同时按下实现截图

private static final long SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS = 150;

//interceptScreenshotChord通过mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay())实现截图
private void interceptScreenshotChord() {
    //mScreenshotChordEnabled系统允许截屏,
    //mVolumeDownKeyTriggeredyin音量-键按下
    //mPowerKeyTriggered电源键按下
    //!mVolumeUpKeyTriggered音量+键没有按下
    if (mScreenshotChordEnabled
            && mVolumeDownKeyTriggered && mPowerKeyTriggered && !mVolumeUpKeyTriggered) {
        final long now = SystemClock.uptimeMillis();
        //这个判断就是保证音量-键和电源键同时按下的效果,即他们间隔时间不能超过150毫秒
        if (now <= mVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
                && now <= mPowerKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
            mVolumeDownKeyConsumedByScreenshotChord = true;
            cancelPendingPowerKeyAction();
            //延时调用mScreenshotRunnable
            mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
        }
    }
}

/** {@inheritDoc} */
//监听按键消息
@Override
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags, boolean isScreenOn) {
......
    //音量-键
    if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
        if (down) {
            if (isScreenOn && !mVolumeDownKeyTriggered
                    && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                mVolumeDownKeyTriggered = true;
                mVolumeDownKeyTime = event.getDownTime();
                mVolumeDownKeyConsumedByScreenshotChord = false;
                cancelPendingPowerKeyAction();
                //实现截图的方法
                interceptScreenshotChord();
            }
        } else {
            mVolumeDownKeyTriggered = false;
            cancelPendingScreenshotChordAction();
        }
    }

......
    //power键
    case KeyEvent.KEYCODE_POWER: {
        if(SystemProperties.get("sys.factorytest","close").equals("open")){//add by zonglibo 
                                Log.d(TAG, "factorytest is open!");
                                result &= ACTION_PASS_TO_USER;
                        } else{
        result &= ~ACTION_PASS_TO_USER;
        if (down) {
            mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn, event.getDownTime(),
                    isImmersiveMode(mLastSystemUiFlags));
            if (isScreenOn && !mPowerKeyTriggered
                    && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                mPowerKeyTriggered = true;
                mPowerKeyTime = event.getDownTime();
                //实现截图的方法
                interceptScreenshotChord();
            }
......
}

通过上面的分析,可以知道,我们按下KEYCODE_SYSRQ或者同时按下KEYCODE_POWER和KEYCODE_VOLUME_DOWN都可以实现截图,那么这样我们就可以使用adb模拟按键的方式,来测试KEYCODE_SYSRQ按键的截图效果,看是否是我们分析的那样。
KEYCODE_SYSRQ对应的数值是120,所以我们可以用下面命令实现模拟点击KEYCODE_SYSRQ的效果。

模拟按键KEYCODE_SYSRQ点击截图

adb shell input keyevent 120
或者
adb shell input keyevent KEYCODE_SYSRQ

这两种方式都是可以模拟KEYCODE_SYSRQ按键点击效果的,测试证明,当我执行上面的命令后,是可以实现截图的。感兴趣的朋友可以动手实践一下。

SystemUI中真正实现截图

下面我们来分析takeScreenshot是怎么去实现截图的,通过它的源码我们知道,它其实是调用了TakeScreenshotService这个服务,我们来看下这个服务的代码。
./base/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java

public class TakeScreenshotService extends Service {
    private static final String TAG = "TakeScreenshotService";

    private static GlobalScreenshot mScreenshot;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    final Messenger callback = msg.replyTo;
                    if (mScreenshot == null) {
                        mScreenshot = new GlobalScreenshot(TakeScreenshotService.this);
                    }
                    mScreenshot.takeScreenshot(new Runnable() {
                        @Override public void run() {
                            Message reply = Message.obtain(null, 1);
                            try {
                                callback.send(reply);
                            } catch (RemoteException e) {
                            }
                        }
                    }, msg.arg1 > 0, msg.arg2 > 0);
            }
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return new Messenger(mHandler).getBinder();
    }
}

从上可以看出TakeScreenshotService又调用了GlobalScreenshot类中的takeScreenshot方法去是实现截图,那我们就看takeScreenshot的实现代码。
./base/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java

/**
 * Takes a screenshot of the current display and shows an animation.
 */
void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
    // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
    // only in the natural orientation of the device :!)
    mDisplay.getRealMetrics(mDisplayMetrics);
    float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
    boolean isPlugIn =
        com.mediatek.systemui.statusbar.util.SIMHelper.isSmartBookPluggedIn(mContext);
    if (isPlugIn) {
        dims[0] = mDisplayMetrics.heightPixels;
        dims[1] = mDisplayMetrics.widthPixels;
    }
    float degrees = getDegreesForRotation(mDisplay.getRotation());
    Xlog.d("takeScreenshot", "dims = " + dims[0] + "," + dims[1] + " of " + degrees);
    boolean requiresRotation = (degrees > 0);
    if (requiresRotation) {
        // Get the dimensions of the device in its native orientation
        mDisplayMatrix.reset();
        mDisplayMatrix.preRotate(-degrees);
        mDisplayMatrix.mapPoints(dims);
        dims[0] = Math.abs(dims[0]);
        dims[1] = Math.abs(dims[1]);
        Xlog.d("takeScreenshot", "reqRotate, dims = " + dims[0] + "," + dims[1]);
    }

    // Take the screenshot
    //真正实现截图的地方,并返回截图后的bitmap,SurfaceControl.screenshot它到最后调用了C++的方法去实现截图,这里就不在往下分析了,感兴趣朋友可以自行去了解。
    if (isPlugIn) {
        mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1], SurfaceControl.BUILT_IN_DISPLAY_ID_HDMI);
        degrees = 270f - degrees;
    } else {
        mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
    }
    //截图失败就发送错误的通知消息
    if (mScreenBitmap == null) {
        Xlog.d("takeScreenshot", "mScreenBitmap == null, " + dims[0] + "," + dims[1]);
        notifyScreenshotError(mContext, mNotificationManager);
        finisher.run();
        return;
    }

    if (requiresRotation) {
        // Rotate the screenshot to the current orientation
        Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
                mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(ss);
        c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
        c.rotate(degrees);
        c.translate(-dims[0] / 2, -dims[1] / 2);
        c.drawBitmap(mScreenBitmap, 0, 0, null);
        c.setBitmap(null);
        // Recycle the previous bitmap
        mScreenBitmap.recycle();
        mScreenBitmap = ss;
    }

    // Optimizations
    mScreenBitmap.setHasAlpha(false);
    mScreenBitmap.prepareToDraw();

    // Start the post-screenshot animation 执行截图后的动画,这里有保存截图的方法
    startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
            statusBarVisible, navBarVisible);
}


/**
 * Starts the animation after taking the screenshot
 */
private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
        boolean navBarVisible) {
    // Add the view for the animation
    mScreenshotView.setImageBitmap(mScreenBitmap);
    mScreenshotLayout.requestFocus();

    // Setup the animation with the screenshot just taken
    if (mScreenshotAnimation != null) {
        mScreenshotAnimation.end();
        mScreenshotAnimation.removeAllListeners();
    }

    mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
    //这里是属性动画,感兴趣的可以看我之前写动画系列的文章
    ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
    ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
            statusBarVisible, navBarVisible);
    mScreenshotAnimation = new AnimatorSet();
    mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
    mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            // Save the screenshot once we have a bit of time now
            //动画结束后将截屏保存为图片
            saveScreenshotInWorkerThread(finisher);
            mWindowManager.removeView(mScreenshotLayout);

            // Clear any references to the bitmap
            mScreenBitmap = null;
            mScreenshotView.setImageBitmap(null);
        }
    });
    mScreenshotLayout.post(new Runnable() {
        @Override
        public void run() {
            /// M: [ALPS01233166] Check if this view is currently attached to a window.
            if (!mScreenshotView.isAttachedToWindow()) return;

            // Play the shutter sound to notify that we've taken a screenshot
            //播放截屏的声音
            mCameraSound.play(MediaActionSound.SHUTTER_CLICK);

            mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            mScreenshotView.buildLayer();
            //开始执行动画
            mScreenshotAnimation.start();
        }
    });
}

/**
 * Creates a new worker thread and saves the screenshot to the media store.
 */
private void saveScreenshotInWorkerThread(Runnable finisher) {
    SaveImageInBackgroundData data = new SaveImageInBackgroundData();
    data.context = mContext;
    data.image = mScreenBitmap;
    data.iconSize = mNotificationIconSize;
    data.finisher = finisher;
    if (mSaveInBgTask != null) {
        mSaveInBgTask.cancel(false);
    }
    //开启一个AsyncTask,执行图片保存的操作
    mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager,
            SCREENSHOT_NOTIFICATION_ID).execute(data);
}

SaveImageInBackgroundTask里面执行就是将截屏后的图片信息保存的Media数据库里,并在通知栏显示截屏结果,同时将图片保存在本地,它的代码就单独贴出来,内容比较多。

/**
 * An AsyncTask that saves an image to the media store in the background.
 */
class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Void,
        SaveImageInBackgroundData> {
    private static final String TAG = "SaveImageInBackgroundTask";

    private static final String SCREENSHOTS_DIR_NAME = "Screenshots";
    private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
    private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";

    private final int mNotificationId;
    private final NotificationManager mNotificationManager;
    private final Notification.Builder mNotificationBuilder;
    private final File mScreenshotDir;
    private final String mImageFileName;
    private final String mImageFilePath;
    private final long mImageTime;
    private final BigPictureStyle mNotificationStyle;
    private final int mImageWidth;
    private final int mImageHeight;

    // WORKAROUND: We want the same notification across screenshots that we update so that we don't
    // spam a user's notification drawer.  However, we only show the ticker for the saving state
    // and if the ticker text is the same as the previous notification, then it will not show. So
    // for now, we just add and remove a space from the ticker text to trigger the animation when
    // necessary.
    private static boolean mTickerAddSpace;

    SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data,
            NotificationManager nManager, int nId) {
        Resources r = context.getResources();

        // Prepare all the output metadata
        mImageTime = System.currentTimeMillis();
        String imageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(mImageTime));
        //保存的文件名
        mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);

        String screenshotSavePath = Settings.System.getStringForUser(context.getContentResolver(), "screenshot_save_path",UserHandle.USER_CURRENT);

        StorageManager storageManager = StorageManager.from(context);
        if(screenshotSavePath == null || (screenshotSavePath != null && !Environment.MEDIA_MOUNTED.equals(storageManager.getVolumeState(screenshotSavePath))))        {         
            screenshotSavePath = StorageManagerEx.getDefaultPath();        

        }
        File Dir = new File(screenshotSavePath);
        //图片要保存的路径
        mScreenshotDir = new File(Dir, SCREENSHOTS_DIR_NAME);
        /*mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES), SCREENSHOTS_DIR_NAME);*/
        //图片要保存的绝对路径
        mImageFilePath = new File(mScreenshotDir, mImageFileName).getAbsolutePath();

        // Create the large notification icon
        mImageWidth = data.image.getWidth();
        mImageHeight = data.image.getHeight();
        int iconSize = data.iconSize;

        final int shortSide = mImageWidth < mImageHeight ? mImageWidth : mImageHeight;
        Bitmap preview = Bitmap.createBitmap(shortSide, shortSide, data.image.getConfig());
        Canvas c = new Canvas(preview);
        Paint paint = new Paint();
        ColorMatrix desat = new ColorMatrix();
        desat.setSaturation(0.25f);
        paint.setColorFilter(new ColorMatrixColorFilter(desat));
        Matrix matrix = new Matrix();
        matrix.postTranslate((shortSide - mImageWidth) / 2,
                            (shortSide - mImageHeight) / 2);
        c.drawBitmap(data.image, matrix, paint);
        c.drawColor(0x40FFFFFF);
        c.setBitmap(null);

        Bitmap croppedIcon = Bitmap.createScaledBitmap(preview, iconSize, iconSize, true);

        // Show the intermediate notification
        mTickerAddSpace = !mTickerAddSpace;
        mNotificationId = nId;
        mNotificationManager = nManager;
        mNotificationBuilder = new Notification.Builder(context)
            .setTicker(r.getString(R.string.screenshot_saving_ticker)
                    + (mTickerAddSpace ? " " : ""))
            .setContentTitle(r.getString(R.string.screenshot_saving_title))
            .setContentText(r.getString(R.string.screenshot_saving_text))
            .setSmallIcon(R.drawable.stat_notify_image)
            .setWhen(System.currentTimeMillis());

        mNotificationStyle = new Notification.BigPictureStyle()
            .bigPicture(preview);
        mNotificationBuilder.setStyle(mNotificationStyle);

        Notification n = mNotificationBuilder.build();
        n.flags |= Notification.FLAG_NO_CLEAR;
        mNotificationManager.notify(nId, n);

        // On the tablet, the large icon makes the notification appear as if it is clickable (and
        // on small devices, the large icon is not shown) so defer showing the large icon until
        // we compose the final post-save notification below.
        //通知栏显示截屏的缩略图
        mNotificationBuilder.setLargeIcon(croppedIcon);
        // But we still don't set it for the expanded view, allowing the smallIcon to show here.
        mNotificationStyle.bigLargeIcon(null);
    }

    @Override
    protected SaveImageInBackgroundData doInBackground(SaveImageInBackgroundData... params) {
        if (params.length != 1) return null;
        if (isCancelled()) {
            params[0].clearImage();
            params[0].clearContext();
            return null;
        }

        // By default, AsyncTask sets the worker thread to have background thread priority, so bump
        // it back up so that we save a little quicker.
        Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);

        Context context = params[0].context;
        Bitmap image = params[0].image;
        Resources r = context.getResources();

        try {
            // Create screenshot directory if it doesn't exist
            mScreenshotDir.mkdirs();

            // media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds
            // for DATE_TAKEN
            long dateSeconds = mImageTime / 1000;

            // Save the screenshot to the MediaStore
            //将截屏后要保存的图片的信息保存到MediaStore数据库
            ContentValues values = new ContentValues();
            ContentResolver resolver = context.getContentResolver();
            values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath);
            values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName);
            values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName);
            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime);
            values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds);
            values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds);
            values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png");
            values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth);
            values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight);
            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

            String subjectDate = new SimpleDateFormat("hh:mma, MMM dd, yyyy")
                .format(new Date(mImageTime));
            String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
            Intent sharingIntent = new Intent(Intent.ACTION_SEND);
            sharingIntent.setType("image/png");
            sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
            sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);

            Intent chooserIntent = Intent.createChooser(sharingIntent, null);
            chooserIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK
                    | Intent.FLAG_ACTIVITY_NEW_TASK);

            mNotificationBuilder.addAction(R.drawable.ic_menu_share,
                     r.getString(com.android.internal.R.string.share),
                     PendingIntent.getActivity(context, 0, chooserIntent,
                             PendingIntent.FLAG_CANCEL_CURRENT));

            //将截屏后的bitmap保存为png图片
            FileOutputStream out = new FileOutputStream(mImageFilePath);
            boolean bCompressOK = image.compress(Bitmap.CompressFormat.PNG, 100, out);
            out.flush();
            out.close();
            /// M: [ALPS00800619] Handle Compress Fail Case.
            if (!bCompressOK) {
                resolver.delete(uri, null, null);
                params[0].result = 1;
                return params[0];
            }

            // update file size in the database
            values.clear();

            /// M: FOR ALPS00266037 & ALPS00289039 pic taken by phone shown wrong on cumputer. @{
            InputStream inputStream = resolver.openInputStream(uri);
            int size = inputStream.available();
            inputStream.close();
            values.put(MediaStore.Images.ImageColumns.SIZE, size);

            // values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length());
            uri = uri.buildUpon().appendQueryParameter("notifyMtp", "1").build();
            resolver.update(uri, values, null, null);
            /// M: FOR ALPS00266037 & ALPS00289039. @}

            params[0].imageUri = uri;
            params[0].image = null;
            params[0].result = 0;
        } catch (Exception e) {
            // IOException/UnsupportedOperationException may be thrown if external storage is not
            // mounted
            params[0].clearImage();
            params[0].result = 1;
        }

        // Recycle the bitmap data
        if (image != null) {
            image.recycle();
        }

        return params[0];
    }

    @Override
    protected void onPostExecute(SaveImageInBackgroundData params) {
        if (isCancelled()) {
            params.finisher.run();
            params.clearImage();
            params.clearContext();
            return;
        }

        //通知栏显示,截屏后的执行结果
        if (params.result > 0) {
            // Show a message that we've failed to save the image to disk
            GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager);
        } else {
            // Show the final notification to indicate screenshot saved
            Resources r = params.context.getResources();

            // Create the intent to show the screenshot in gallery
            Intent launchIntent = new Intent(Intent.ACTION_VIEW);
            launchIntent.setDataAndType(params.imageUri, "image/png");
            launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

            mNotificationBuilder
                .setContentTitle(r.getString(R.string.screenshot_saved_title))
                .setContentText(r.getString(R.string.screenshot_saved_text))
                .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0))
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true);

            Notification n = mNotificationBuilder.build();
            n.flags &= ~Notification.FLAG_NO_CLEAR;
            mNotificationManager.notify(mNotificationId, n);
        }
        //整个截屏事件结束
        params.finisher.run();
        params.clearContext();
    }
}

整个系统的截屏流程,代码分析差不多讲完了,我们来整理一下他们之间的调用关系。
这里写图片描述
android系统基本的截图流程就是这样了,不排除个别厂商会使用自定义的一套截图方法。所以基本上,要做系统截图功能,只要知道上面关系图中的那些类和方法就可以做一定的个性化定制了。

作者:QQxiaoqiang1573 发表于2016/11/24 17:12:57 原文链接
阅读:46 评论:0 查看评论

android开发之日常

$
0
0

写在前面


主要由于最近在重学c/c++以及摸索视觉这一块基础知识,其中产生了很多问题,特别是在看了一个log4z的源码后瞬间产生了18个问题然后记录下来问了做c++的朋友。这18个问题在别人看来肯定觉得很菜,都是很基本的东西,但它们难在我对c++开发者每天的工作内容的不了解,所以才会有看了面试宝典找个公司进去就知道了这样的回答。所以反过来,我想那些转战android的朋友肯定最初也会有类似的问题。比如说我其中几个问题:

  • 宏定义中,有很多代码IDE无法识别,所以如果当中的代码有错误,怎么识别呢?
  • 宏定义穿插在代码的任意地方,是不是不规范?
  • 类的构造器重载不放在一起是不是不规范?
  • 一个cpp文件里面一两千行代码是不是常态?
  • wchar_t一般的使用场景是什么?
  • ##_VA_ARGS_,”##”这样的命名方式的场景是什么?

如果有初学者问的是android开发相关的内容呢或者说网上的代码和工作中的代码有多少区别呢?所以我依照过去写过的一些项目代码,利用开源的api做了一个应用,里面的不少东西都可以展现android开发者在项目里面每天都在苦逼的干一些什么事情,以及哪些招聘上面没有写的知识需要重点掌握。
与此同时在尝试一些新东西,也算是学习的一种方式,因为先前看到知乎客户端用fragment打造整个app的文章,的确快很多,而恰巧fragment的坑着实又很多,因此看能不能用view来代替。当然,这种方式只是一种尝试而且还在研究当中,并非是android开发每天的工作内容。再加上前端时间看奇异博士,壮哉我大漫威啊~~果断找到它的开源api,做一个尽量好看的东西,没有ui的情况下,已经尽力。
最后,个人认为,这篇文章对其他领域希望学习android的朋友益处不少。


MarvelComic

这里写图片描述

这基本上就是最近每天都面对的项目结构,因为有很多零碎知识网上都有,所以我只能把重点放在结构和流程上。包里面分为data,util,views,以及permission,activity和application单独拿出来放到平级。一般情况下,只需要把application单独拿出来,原因在于,application是我们在开发过程中参与业务相对较少或者最少,而起到一个总管职责的类,可以把它看成是程序的入口,进入你应用的大门。而activity的放置情况有很多种,因为它与业务联系最密切,而多人协调开发又要根据模块来进行划分,所以一般它会单独存放。

当然几乎每个项目的分包都会不一样,但相同的地方肯定都会存在,为何首先要说包结构?因为它很重要,对于android开发来说,头脑里一定要有比较清晰的认识,一说到android项目结构,立马要浮现application,网络,util,特殊view,activity,fragment等等对应的常用包名称以及它们可能会存在的位置。有什么样的作用呢?最重要的就是能够让人迅速的熟悉整个项目结构,公司项目如此,开源项目尤为如此,阅读源码就更是如此了。比如说U2020,如果想要找它的接口地址的用法,不要犹豫直接点击data,或者你希望找到某个界面的动画肯定就是ui。而你又发现多了一个U2020Module.java文件,熟悉点的人一看就知道,肯定是用了dagger2框架,相应的逻辑处理多半在module里面。而这又有一个前提条件,那就是对application,activity以及其他类之间的关系的熟悉。

在这个项目里其结构如下:

这里写图片描述

一般情况项目中是没有Pool这个节点的,但隐隐约约从很多项目里也能看出它的存在。很多人都熟悉,在添加地图sdk或者统计sdk等等很多类似的东西的时候,它们往往要求我们在application的onCreate里面加上这样一句代码XXSDK.initialize(this),为什么不加在activity里或是其他什么地方?原因自然不少,但从意图上来说,主要目的就是为了让自己的功能从一开始就完全独立,这也是设计一个库的基本原则,作为一个独立的个体而存在就在于进了application的门,就得各顾各的事,尽量不要互相影响。比如地图初始化启动服务等等跟其他代码没有任何关系,所以只需要把最大的生命周期交给我,我自己玩自己的就行,数据库是否初始化不关我的事,ui是否开始渲染也不关我的事。这样做的好处非常多,比如它与业务完全分开。而这也体现了application的特殊存在。

为了让代码的组织形式更清晰,好懂好理解,扩展性更强。对象池Pool就作为一个组织者放在application里面,有任何相关的业务只需要在Pool里面找就是了。另外,基于dagger1,它有一个叫ObjectGraph的类,是专门用于在application创建时负责一口气创建所需的实例,并在需要的时候拿出来即可。Pool就是其类似的存在,只不过我们不用构建对象图,也不用一一对应,因为散养在这里更加适合,每个view都有极大的可能性会下载图片,或者作数据库操作。这也是我对dagger在这一点上所持的一些怀疑,限制是需要相应代码作处理的,而且也是与业务有关联的,而为了实质性的实现反转,为何不仅仅在思想上进行反转呢?当然dagger2作为dagger1的编译时版本,它更是在原有基础上加强了更多的限制。这点上,我并没有对对象之间的关系进行任何限制,而该对pool用get的时候还得用get,但观念上已经向控制反转靠拢了。

其实这里面的内容说多并不多,说少也可不少,主要分为这样几个部分:

  • 网络Retrofit
  • 数据库 Database
  • View

网络

对于网络,先说下题外的,一般招聘要求网络编程,这个词可是个超级大词。但如果单纯要求你会使用sdk自带的UrlConnection/HttpClient却是不行的,尽管很多人会发现进入公司后,基本操作的就是它根本就没有什么可说的。问题在于,当你遇到其他问题的时候会束手无策,多数人会对只会用开源库的人嗤之以鼻的原因也不是不可理喻。
比如,Retrofit很多人都知道这个库,其普及率也是相当高。只要配置配置就可以完美运行,这是jack他们写这个库的初衷,这一点是肯定值得推崇的。但如果因为这个就不去理解它的原理,很难应付各种各样的需求,如在其基础上进行数据加密,怎么实现?这个在其github上可找不到具体的东西,又如一套自定义错误码管理又如何实现?当遇到类似的问题时,大概除了jack这样的工作狂,其他人基本不会回答你提的issue。

要了解这一块内容,一定要深入理解这几个方面的内容:

URL和URI
长连接和短连接
HTTP和HTTPS
Restful
注解
代理等各种比较常见的设计模式
线程池

这只是其中几个比较重要的部分,这些部分仅仅是帮助理解Retrofit的,或许这样的列表太多人提,看起来根本没有动力,如同大一的时候学数据结构一样提不起劲。那么说说它们的实际作用和目的,为什么要了解它们:

URL和URI,理解这两者的区别,你会明白为何Retrofit会出现。而且在以后可能会用到的本地数据库也会经常用到。好像看上去相当重量级,是的,就是如此重要,很多思想就是这样一步一步得以发扬光大。
长连接和短连接,理解这两者的区别,主要是为了帮助理解Http。后面你会遇到推送,聊天等等需求,也会遇到。
HTTP和HTTPS,Retrofit是个网络库,不理解http是不行的,而且相关的知识也能够用来解答为什么选择okhttp。不光如此,一定要去看一次请求和返回的具体数据,看看都是些什么样子的数据。而https需要加密解密的知识,理解它会让你对协议的安全有一定了解,比如实实在在的项目需求,token如何设计,sharedpreference里数据的保护如何设计等等。
Restful,有了前面几个知识,要了解这个不难,Retrofit之所以如此普及,并非它有什么黑科技,最主要的原因在于规范,规范,规范。重要的事情说三遍。在网络交互中,规范的重要性不言而喻。
注解,设计模式,线程池,这几个方面的基本知识可以让你了解Retrofit是如何实现的,当然这三个方面是很大的知识块,循序渐进。

花了很多时间在这个上面,因为网上的教程基本不会提及这些内容学来有什么具体的实际作用。有目标才会有动力。

回到MarvelComic上来,

这里写图片描述

尝试着画一个流程图,但很难将其尽量画得简单好懂,当然这也归功于里面的确有些很绕的地方。个人觉得可以参考

遗憾的是它是从源码分析的角度来画的,而且博主,也没有尽量以设计者的角度来进行描述(这方面当然只能是去YouTube上找jack关于retrofit2的演讲视频),不过已经是相当详尽了。看完可以以自己的理解仔细读读源码,并且站在设计者的角度思考比如设计者如何将Retrofit和Okhttp两个框架进行结合的(个人觉得这其实是比较难的地方,哪个地方稍微设计得不好就会有很大的影响),以及它是如何支持Rxjava或其他方式的,因为CallAdapter可以很好的让我们理解为何会有retrofit的从1到2的变化。不建议初学者仔细看,但请求流程要了解。
看完这位博主的描述,需要得到一些基本认识,比如一次请求,从发起到请求前操作,进行请求,返回后操作,数据返回给开发者。了解了这些就可以理解helper包里面这些类的作用,以及他们会在何时被调用和其存在的意义。由于这两个框架写得非常规范,因此仔细理解源码中的命名肯定会有很大的帮助。

/**
   * return {@link Retrofit} instance
   */
  private static Retrofit createRetrofit(OkHttpClient client, Gson gson) {
    return new Retrofit.Builder().baseUrl(EndPoint.endPoint())
        .client(client)
        .addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create())
        .addConverterFactory(BaseConverterFactory.create(gson))
        .build();
  }

  /**
   * return {@link OkHttpClient} instance
   */
  private static OkHttpClient createOkHttpClient(MarvelApplication app,
      HashMap<String, String> tagsRef) {
    HttpLoggingInterceptor interceptor =
        new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
          @Override public void log(String message) {
            if (BuildConfig.DEBUG) Timber.tag("OkHttp").d(message);
          }
        });
    interceptor.setLevel(
        BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE);
    return new OkHttpClient.Builder().addInterceptor(interceptor)
        .addNetworkInterceptor(new CacheInterceptor(app, tagsRef))
        .cache(new Cache(new File(app.getCacheDir(), "HttpResponseCache"), MAX_DISK_CACHE_SIZE))
        .retryOnConnectionFailure(true)
        .readTimeout(20, TimeUnit.SECONDS)
        .connectTimeout(20, TimeUnit.SECONDS)
        .build();
  }

上面两个在Pool中,分别初始化获取Retrofit和OkHttpClient实例的代码,算是比较标准化的代码,可以想象成Retrofit将okhttp包裹进自身,由于两者都是builder方式,因此可以点进去看每个方法的作用。他们的作用总结下来,主要是用来统一处理请求当中的不同需求,这一点和以前一样,不然每个请求都添加头信息就惨了。OkHttpClient的初始化设置插入了好几个其他对象,主要用来解决:

1 请求日志(这是正常项目所必须且能在debug和release环境切换)
2 拦截器用于处理请求前和请求后的操作(比如Marvel的api强制要求要统一添加apikey,而如果返回与服务端约定好的错误code则需要自己在这个地方进行处理),关于intercept和networkIntercept的区别可以参看这里
3 连接失败重试
4 超时时间(正常15秒,此处由于Marvel网站请求很卡,所以改成20s),请注意服务端也有超时时间,客户端也有,如果两者不一致的处理就需要自己写逻辑了。
5 CacheOkHttp是严格按照web标准进行缓存处理的。由于图片框架用的Picasso,因此设置图片的缓存也在这里,都是通过okhttp的缓存策略进行处理的。

对于helper包里那几个具体的类,我就不讲他们的具体实现而着重于它们的用途以及理解。
首先,上面说过,retrofit这个框架重点在于对网络请求的规范和统一。它将一次请求分为了如下几个步骤:

1 请求前转换
helper中,实际上没有在请求前对请求数据进行特殊转换,何时需要统一处理呢?比如文件上传,或者以对象流的形式作为参数传递时,而这也需要设置适当的注解。

--- 拦截整体代码
    @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    HttpUrl original = request.url();
    String tag = getETag(original);

    Request.Builder requestBuilder = request.newBuilder();

    if (!isActiveNetwork(application)) {
      //dead network force cache
      requestBuilder = requestBuilder.cacheControl(CacheControl.FORCE_CACHE).url(original);
    }
    request = requestBuilder.url(original.newBuilder()
        .addQueryParameter(EndPoint.key(), EndPoint.publicKeyValue())
        .addQueryParameter(EndPoint.hash(),
            Utils.hash(EndPoint.publicKeyValue(), EndPoint.privateKeyValue()))
        .addQueryParameter(EndPoint.timeStamp(), Utils.getUnixTimeStamp())
        .build()).addHeader("If-None-Match", tag).addHeader("ETag", tag).build();

    Response response = chain.proceed(request);

    if (isActiveNetwork(application)) {
      // read from cache for 1 minute
      response = response.newBuilder()
          .removeHeader("Pragma")
          .header("Cache-Control", "public, max-age=" + MAX_AGE)
          .build();
    } else {
      // tolerate 4-weeks stale
      response.newBuilder()
          .removeHeader("Pragma")
          .header("Cache-Control", "public, only-if-cached, max-stale=" + MAX_STALE)
          .build();
    }
    setETag(original, response.header("ETag"));
    return response;
  }
--- end (以"Response response = chain.proceed(request);"这句代码为分界线分为拦截前,请求和拦截后)

2 请求前拦截
拦截这一块内容现在都转到了okhttp里面去设置拦截器,方法不变,网上有不少代码讲解这一块内容就不赘述。大概就是对url进行重新处理,无论是重造还是添加参数都可以,请求头添加键值对也可以,对于每次请求都会作一样的这样的处理。而网上没有详细说明的地方在于Cache,okhttp的缓存机制是严格按照规范走的,也就是说影响它的因素包括服务端的返回头,客户端的请求头,如果服务端不允许okhttp是不会让你缓存的(不讨论trick方式),另外它的实现是以url为保存基准对这次请求的返回数据进行保存的,这和浏览器是一样的。Marvel由于服务端强行要求将apikey和时间戳的hash值放在url后面传递,导致了url每一次都会发生变化,而这将导致本地无法实现etag缓存。因此这里简单作了处理,其生命周期为application的周期。
所以规范很重要啊。

3 请求
Response response = chain.proceed(request);
仔细理解chain.proceed这两个单词,你会感叹这代码真的写得很舒服。

4 请求后拦截
如果debug你会发现,这里的执行已经是在另外的线程上面了,因此可以明显感受到等待服务器返回数据。请求后没有进行特殊处理,只是简单根据网络判断要不要从缓存拉数据重新构建response,当然实际的操作是okhttp拿到我们返回的response,根据我们设置的头信息自行选择缓存策略。

5 请求后转换
接下来就是请求后转换,由于用的gson,因此需要自己定义gson对数据的转换过程。要提的是错误处理,这里是套用的retrofit1源码里面的错误处理代码,对错误进行大致区分。在实际项目当中,由于有的后端并不会坚持规范的返回格式,在存在自定义错误code时,其处理转换是重点。

需要注意的是,尽管okhttp是以builder模式构建不应当会存在设置参数的顺序问题,但由于其内是以List作为容器保存我们的拦截器,因此如果有多个拦截器的话,业务需要时应当注意顺序是否会出现逻辑错误。
关于eTag,它只是web缓存的很小的一块,这里有很详尽的web缓存的解释,这个在实际操作中是很重要的知识点。

数据库

移动端的数据库的使用是家常便饭,对于使用过SqlServer,mysql等等的人来说它的使用应该没有什么问题,注意一些细节就可以了。
这里写图片描述
一般情况下,用到数据库的地方多数是缓存,也就是对请求数据以某种方式进行缓存,而且一般也没有需要对数据库实现共享的需求,否则contentprovider就要出场了,对,它是四大组件之一,这里就是在实际项目中可能会用到这个组件的场景之一。要理解它的使用,网上有很多介绍,需要的基础知识就是restful,为什么?用一用就知道了。Marvel对于数据库的操作只是curd,需要了解的知识点有这样三个,注解,sqlbrite框架,页面缓存机制。

注解

对于注解这一块知识,其实比较暧昧,一般来说了解和熟悉其实都是可以的。但自从butterknife,dagger2之后,大家都开始关注起编译时注解了,因为它避免了运行时的开销,这对手机这样的环境来说是大利好。网上对这一块内容的介绍有很多,但多数都只是参照butterknife,没有实际的运用场景,的确这样的场景比较难找。这里之所以用到它最主要的原因是,数据库表的建立。
在使用gson之后,我们请求的数据都会被映射到一个对象中去,而这个对象需要我们自己创建,并且对它的字段成员进行命名,这是手动coding第一次,当需要建表的时候,我们需要根据gson映射的对象里的成员再一次手动写成对应的数据库表的列名组成sql语句。尤其是使用contentprovider时需要填充一个参数(什么参数忘了),就需要第三次手动写一遍。如果字段有很多的话,就是件很苦逼的差事了。

这里写图片描述

建立两个module的库,一个是注解,一个编译器,最后依赖到主module上。至于具体的创建方式,我强烈建议有兴趣的朋友看这个,这位大神挺强大,YouTube上也有他关于这一块的视频讲解。关于调试

@TableInfo(ComicModel.class) public class ComicModel {

  /**
   * id : 42882
   * digitalId : 26110
   * title : Lorna the Jungle Girl (1954) #6
   * modified : 2015-10-15T11:13:52-0400
   * format : Comic
   * pageCount : 32
   * urls :
   * [
   * {"type":"detail","url":"http://marvel.com/comics/issue/42882/lorna_the_jungle_girl_1954_6?
   * utm_campaign=apiRef&utm_source=37425d749b0301ff752ffcb8f996acc5"},
   * {"type":"reader","url":"http://marvel.com/digitalcomics/view.htm?
   * iid=26110&utm_campaign=apiRef&utm_source=37425d749b0301ff752ffcb8f996acc5"}]
   * prices : [{"type":"printPrice","price":0}
   * ]
   * thumbnail : {"path":"http://i.annihil.us/u/prod/marvel/i/mg/9/40/50b4fc783d30f","extension":"jpg"}
   * images : [{"path":"http://i.annihil.us/u/prod/marvel/i/mg/9/40/50b4fc783d30f","extension":"jpg"}]
   */

  public int id;
  public int digitalId;
  public String title;
  public String modified;
  public String format;
  public int pageCount;
  public Thumbnail thumbnail;
  public List<Link> urls;
  public List<Images> images;
}

这个过程很清晰,首先根据服务端返回的数据,创建成一个类并利用gson把json数据映射成一个对象,我把请求返回的数据模型写到了注释里面。创建好这个类后,在需要存入数据库的Class前面添加一个@TableInfo这个注解:

@Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface TableInfo {
  Class<?> value();
}

该注解会获取到该类的一切信息并将这些信息在编译的时候,拿到我们自己写的编译器库进行编译,编译的结果如下:

public final class ComicModel_TABLE {
  public static final String ID_COL = "ID";

  public static final String DIGITALID_COL = "DIGITALID";

  public static final String TITLE_COL = "TITLE";

  public static final String MODIFIED_COL = "MODIFIED";

  public static final String FORMAT_COL = "FORMAT";

  public static final String PAGECOUNT_COL = "PAGECOUNT";

  public static final String THUMBNAIL_COL = "THUMBNAIL";

  public static final String URLS_COL = "URLS";

  public static final String IMAGES_COL = "IMAGES";

  public static final String TABLE_CREATE = "CREATE TABLE COMICMODEL_TABLE (_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,ID TEXT,DIGITALID TEXT,TITLE TEXT,MODIFIED TEXT,FORMAT TEXT,PAGECOUNT TEXT,THUMBNAIL TEXT,URLS TEXT,IMAGES TEXT,UNIQUE (_id) ON CONFLICT REPLACE)";

  public static final String TABLE_NAME = "COMICMODEL_TABLE";

  public static final String TABLE_DROP = "DROP TABLE IF EXISTS COMICMODEL_TABLE";

  public static final String TABLE_BASE_QUERY = "SELECT * FROM COMICMODEL_TABLE";

  public static final String TABLE_DELETE = "DELETE FROM COMICMODEL_TABLE";
}

上面就是我们编译出来的,包含表名,列名,增删改查等等信息的字符串,拿到它们,在sqliteHelper中就可以这样写了:

public final class DatabaseHelper extends SQLiteOpenHelper {

  public static final String DATABASE_NAME = "marvel.db";
  /**
   * warning : every release should check version, if <bold>database<bold/> has change, version
   * code should change
   */
  public static final int DATABASE_VERSION = 3;

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

  @Override public void onCreate(SQLiteDatabase db) {
    db.execSQL(ComicModel_TABLE.TABLE_CREATE);
    db.execSQL(CharacterModel_TABLE.TABLE_CREATE);
    db.execSQL(ComicDetailModel_TABLE.TABLE_CREATE);
  }

  @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    if (oldVersion != DATABASE_VERSION) {
      db.execSQL(ComicModel_TABLE.TABLE_DROP);
      db.execSQL(CharacterModel_TABLE.TABLE_DROP);
      db.execSQL(ComicDetailModel_TABLE.TABLE_DROP);
      onCreate(db);
    }
  }
}

当然这仅仅是简化了一个很小的步骤,其他该写的操作数据库的代码该写的还是要写:

/**
 * Created by lawson on 16/7/25.
 *
 * dealer which deal with detail about something like crud operation between customer and provider
 */
public abstract class SqliteDealer<T> {

  /**
   * function of dealing with cursor
   */
  public Func1<SqlBrite.Query, List<T>> MAP = new Func1<SqlBrite.Query, List<T>>() {
    @Override public List<T> call(SqlBrite.Query query) {
      Cursor cursor = query.run();
      try {
        List<T> values = null;
        if (cursor != null) {
          values = new ArrayList<>(cursor.getCount());
          while (cursor.moveToNext()) {
            values.add(fromCursor(cursor));
          }
        }
        return values;
      } finally {
        if (cursor != null) {
          cursor.close();
        }
      }
    }
  };

  /**
   * get data from cursor {@link Cursor}
   */
  public abstract T fromCursor(Cursor cursor);

  /**
   * save data to {@link ContentValues}
   */
  public abstract ContentValues toContentValues(T model);
}

MAP负责从sqlbrite拿数据,如何拿需要自己实现fromCursor方法,toContentValues方法是保存获取的网络数据存到sqlbrite中去。关于sqlbrite,它是以rxjava封装了对数据库的许多操作相当于包了一层,我们不用直接与sqlite交互,对rxjava有兴趣的朋友可以读读其内部的源码,写得很有意思,特别是数据改动的监听(或者Trigger),有了它的存在,我们不用自己去监听数据发生的变化,而可以直接将rx流导入列表的adapter中去。

页面缓存机制

对于这点,其实是很简单的,但往往被很多人所忽略。

后面会说到view的数据填充时会发现,其实我们设计缓存的初衷除了断网时能看到数据,还有就是在数据获取到之前先展示缓存数据。为什么能做到这一点?因为加载本地数据比加载网络数据要快,正因为存在这个时间差,所以才会有页面缓存这样的东西的存在。否则存在网络比加载本地数据更快的情况,那么缓存这个流程该怎么走呢?可能有些许不同的地方,但通用的流程骨架是一样的:同时加载本地和加载网络,由于本地数据加载更快,因此页面很快就能看到数据展现,当网络数据回来时,先将这部分数据进行缓存,然后再走一遍加载本地数据的流程,这样网络,本地和页面上的数据就可以得到统一
所以任何移动端的缓存,都要考虑考虑这个过程,才能很好的设计,否则会存在同步等难以解决的问题。


可能不少人对于RxJava并不熟悉,MarvelComic里基本是由rxjava贯穿,如果放在两年前,是有必要说一说的,但现在网上关于这一块内容实在不少,可看的也不少,而且比较关键的是,这一块东西着实并不容易靠讲解就能领会,所以多实践吧。

关于view部分,时间关系下次再说。最后需要说的是,篇幅较长,对初学者而言,可以多debug这个项目的流程,除了view部分其余的都是很通用的,很多在职开发者每天都做和考虑的事情;对老司机而言,我想更多的应该关注源码部分,的的确确有不少有趣的地方,比如动态代理的缓存,Call,okio等等,然后实实在在的利用这些知识去解决或优化自己项目中的一些代码,学以致用,比如命名规范,抽象,多人协同开发时模块的划分,代码的限制(比如继承一个空接口等等),或者上面说到的编译时注解的运用等等。

最后,源码地址

作者:Pizza_Lawson 发表于2016/11/24 17:34:45 原文链接
阅读:48 评论:0 查看评论

加载底部自定义Dialog

$
0
0
  • 创建自定义Dialog布局的xml文件,位置vendor/mdiatek/proprietary/packages/apps/Gallery2Zuk/res/layout/delete_photos_dialog_layout.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#fafafa"
        android:gravity="center"
        android:orientation="vertical" >
        
        <TextView 
            android:id="@+id/delete_photos_dialog_message"
            android:layout_width="match_parent"
            android:layout_height="52dip" 
            android:gravity="center"
            android:textColor="#999999"
            android:textSize="14sp"
            android:layout_marginTop="11dp"
            android:layout_marginBottom="5dp"
            android:background="#fafafa"/>
        
        <View 
            android:layout_width="match_parent"
            android:layout_height="0.66700006dip"
            android:background="#dfdfdf"/>
        
        <TextView 
            android:id="@+id/dialog_delete_bt"
            android:layout_width="match_parent"
            android:layout_height="52dip"
            android:gravity="center"
            android:text="@string/delete"
            android:textColor="#333333"
            android:textSize="14sp"
            android:background="@drawable/dialog_button_bg"/>
        
        <View 
            android:layout_width="match_parent"
            android:layout_height="4dp"
            android:layout_gravity="center"
            android:background="#dddddd"/>
        
        <TextView 
            android:id="@+id/dialog_cancel_bt"
            android:layout_width="match_parent"
            android:layout_height="52dip"
            android:gravity="center"
            android:text="@string/cancel"
            android:textColor="#333333"
            android:enabled="true"
            android:textSize="14sp"
            android:background="@drawable/dialog_button_bg"/>
        
    
    </LinearLayout>
    

  • 然后设置该layout布局的style
    <style name="delete_photos_dialog_style">
        <item name="android:windowNoTitle">true</item>
        <item anme="android:backgroundDimEnabled">true</item>
    </style>

  • backgroundDimEnabled属性是是否允许对话框的背景变暗,为ture则背景变暗
  • 建立一个类用来加载该布局,并且调整布局在屏幕上的位置,类的位置vendor/mdiatek/proprietary/packages/apps/Gallery2Zuk/src/com/android/gallery3d/wind/base/view/DeletePhotosDialog.java
    import android.view.Gravity;
    import android.view.View;
    import android.view.LayoutInflater;
    import android.view.Window;
    import android.view.WindowManager;
    import android.content.Context;
    import android.app.Dialog;
    import android.widget.LinearLayout;
    import android.widget.TextView;
    
    import java.util.ArrayList;
    
    import android.net.Uri;
    import android.os.Bundle;
    
    import com.android.gallery3d.R;
    import com.android.gallery3d.wind.menu.MenuExecutor;
    public class DeletePhotosDialog implements View.OnClickListner{
    	
    	private Context mContext;
    	private Dialog mDialog;
    	private TextView dialogMessage;
    	private TextView dialogDeleteBT;
    	private TextView dialogCancelBT;
    	
    	private ArrayList<Uri> uris;
    	private FragmentCallback callBack;//FragmentCallback应该是MenuExecutor中的一个接口
    	private int id;
    	
    	public DeletePhotosDialog(Context context,ArrayList<Uri> uris,FragmentCallback callBack,int id){
    		mContext=context;
    		this.uris=uris;
    		this.callBack=callBack;
    		this.id=id;
    	}
    	
    	public Dialog createDeletePhotosDialog(String message){
    		mDialog = new Dialog(mContext,R.style.delete_photos_dialog_style);//根据style创建一个dialog
    		LinearLayout root=(LinearLayout)LayoutInflator.from(mContext).inflate(
    				R.layout.delete_photos_dialog_layout,null);
    		dialogMessage=(TextView)root.findViewById(R.id.delete_photos_dialog_message);
    		dialogMessage.setText(message);
    		dialogDeleteBT=(TextView)root.findViewById(R.id.dialog_cancel_bt);
    		
    		dialogDeleteBT.setOnClickListener(this);
    		
    		dialogCancelBT=(TextView)root.findViewById(R.id.dialog_cancel_bt);
    		dialogCancelBT.setOnClickListener(this);
    		mDialog.setContentView(root);//给Dialog对象设置布局
    		Window dialogWindow=mDialog.getWindow();//定义一个Window对象,将mDialog通过getWindow得到的Window赋值给该对象
    		dialogWindow.setGravity(Gravity.BOTTOM);//设置Window的对齐方式为底部对齐
    		WindowManager.LayoutParams lp=dialogWindow.getAttributes();//获取对话框当前的参数值
    		lp.x=0;//新位置X坐标
    		lp.y=130;//新位置Y坐标
    		lp.width=(int)mContext.getResources().getDisplayMetrics().widthPixels;//宽度
    		root.measure(0, 0);//对root布局进行测量
    		lp.height=root.getMeasuredHeight();//得到布局的高度
    		lp.alpha=9f;//透明度
    		dialogWindow.setAttributes(lp);
    		return mDialog;
    		
    	}
    	
    	@Override
    	public void onClick(View v){
    		
    		switch(v.getId()){
    		case R.id.dialog_delete_bt:
    			Bundle bundle = new Bundle();
    			bundle.putParcelableArrayList("URI_LIST", uris);
    			callBack.startMemuAction(id,bundle);
    			if(mDialog!=null){
    				mDialog.dismiss();//点击删除按钮以后,dialog去掉
    			}
    			break;
    		case R.id.dialog_cancel_bt:
    			if(mDialog!=null){
    				mDialog.dismiss();//点击删除按钮以后,dialog去掉
    			}
    			break;
    		default:
    			break;
    		}
    	}
    }
    

  • 参考链接http://blog.csdn.net/fancylovejava/article/details/21617553
  • 找到点击弹出该对话框的按钮,位置在vendor/mdiatek/proprietary/packages/apps/Gallery2Zuk/src/com/android/gallery3d/wind/moment/MomentFragment.java
    public void bottomButtonClick(){
    ......
    ImageView deletePhotos=(ImageView)getActivity().findViewById(R.id.btn_delete);
    deletePhotos.setOnClickListner(new View.OnClickListener(){
    @Override
    public void onClick(View v){
        String confirmMsg="";
        String type=getSelectedItemType();//得到选中项的数据类型
        if(type.equalsIgnoreCase("photo")){
        confirmMsg=getResources().getQuantityString(R.plurals.delete_selection_photo,getSelectedCount(),getSelectedCount());//getSelectedCount()方法获得被选中项的数量
    }else if (type.equalsIgnoreCase("video")){
        confirmMsg=getResources().getQuantityString(R.plurals.delete_selection_video,getSelectedCount(),getSelectedCount());
    }else if(type.equalsIgnoreCase("burst")){
        confirmMsg=getResources().getQuantityString(R.plurals.delete_selection_burst,getSelectedCount(),mConshotCount,getSelectedCount());
    }else{
        confirmMsg=getResources().getQuantityString(R.plurals.delete_selection_item,getSelectedCount(),getSelectedCount());
    }
    mMenuExecutor.onDelete(R.id.btn_delete,confirmMsg,getSelectedUris());//getSelectedUris()方法获取所有被选项的uri
    }
    });
    }

  • 在string.xml中添加如下内容
    <plurals name="delete_selection_photo">
        <item quantity="one">Delete this photo?</item>
        <item quantity="other">Delete %1$d photo(s)?</item>
    </plurals>
    <plurals name="delete_selection_video">
        <item quantity="one">Delete this video?This will also remove \n this video from other albums.</item>
        <iten quantity="other">Delete %1$d video(s)?This will also remove the \n video(s) from other albums.</item>
    </plurals>
    <plurals name="delete_selection_burst">
        <item quantity="one">Delete %1$d photos in this burst sequence?</item>
        <iten quantity="other">Delete %1$d photos in %2$d burst sequences?</item>
    </plurals>
    <plurals name="delete_selection_item">
        <item quantity="one">Delete this item?This will also remove \n this item from other albums.</item>
        <iten quantity="other">Delete %1$d item(s)?This will also remove the \n item(s) from other albums.</item>
    </plurals>

  • 参考链接http://www.cnblogs.com/meiyitian/articles/2221742.html
  • 我们看一实现删除操作的方法onDelete,它在vendor/mdiatek/proprietary/packages/apps/Gallery2Zuk/src/com/android/gallery3d/wind/menu/MenuExecutor.java中,内容如下
    public void onDelete(int id,String confirmMsg,ArrayList<Uri>uriList){
        mUriList=uriLIst;
        if(confirmMsg!=null){
            DeletePhotosDialog deletePhotosDialog=new DeletePhotosDialog(mContext,mUriList,mFragmentCallback,id);//这里调用了我们前面定义的用来加载删除dialog的类DeletePhotosDialog 
            deletePhotosDialog.createDeletePhotosDialog(confirmMsg).show();//这里创建删除dialog并显示
            }else{
                Bundle bundle=new Bundle();
                bundle.putParcelableArrayList("URI_LIST",mUriList);
                mFragmentCallback.startMenuAction(id,bundle);
            }
        }

  • 这里继续补充接口回调的知识
  • 在DeletePhotosDialog.java中点击删除后的操作
    callBack.startMenuAction(id,bundle);

    callBack是接口FragmentCallback的对象,这里是通过DeletePhotosDialog的构造方法传入的
    public DeletePhotosDialog(Context context,ArrayList<Uri> uris,FragmentCallback callBack,int id)

    于是我们去找实际调用DeletePhotosDialog类并传入参数的位置,就是上面提到的onDelete方法,通过该方法体,我们发现mFragmentCallback来自于类MenuExecutor中的构造方法
    public MenuExecutor(Context context,final FragmentCallback fragmentCallback)

    于是我们要去找实际调用MenuExecutor类并传入参数的位置,于是我们发现在类MomentFragment的initViews方法中实际调用了该类
    mMenuExecutor=new MenuExecutor(getActivity(),this);

    这里重点来了,说明MenuExecutor这个类实现了接口FragmentCallback,于是找到实现该类需要重写的方法startMenuAction中的内容,如下
    final MenuFragment menuFragment=MenuFragment.newInstance(actionId,uris);    menuFragment.setProgressCallback(this);

    这里又将类本身传入作为参数,于是我们发现该类同时实现了接口MenuFragment.ProgressCallBack,我们在去看看实现这个接口,都需要重写哪些方法,接口的内容如下
    public interface ProgressCallBack{	
    void onProgressStart(int actionId,int max);	
    void onProgressUpdate(int index);	
    void onProgressComplete(String msg);
    }

    于是我们回到MomentFragment类中,看看该类是如何重写这些方法的,我们发现在重写的方法onProgressStart中,调用了方法showDialog(deletingCount);,于是我们找到该类中的showDialog方法,这个方法就是真正生成ProgressDialog的位置,内容如下
  • private void showDialog(int max){
        int dialogTitle;
        if(actionId==R.id.btn_delete){
            dialogTitle=R.id.btn_delete;
        }else{
             dialogTitle=R.string.ok;
        }
        if(progressDialog !=null && progressDialog.isShowing()){
            progressDialog.cancel();
        }
        progressDialog=CustomProgressDialog.createProgressDialog(getActivity(),R.style.CustomProgressDialog,dialogTitle,max);
    progressDialog.show();
    }

作者:u012966861 发表于2016/11/24 17:49:00 原文链接
阅读:68 评论:0 查看评论

Android图表库MPAndroidChart(十一)——多层级的堆叠条形图

$
0
0

Android图表库MPAndroidChart(十一)——多层级的堆叠条形图


事实上这个也是条形图的一种扩展,我们看下效果就知道了

这里写图片描述

是吧,他一般满足的需求就是同类数据比较了,不过目前我还真没看过哪个app有这样的图表,但是并不代表我们不能实现呀对吧,我们来看下基本实现

一.基本实现

看下我们的layout是怎么定义的

    <com.github.mikephil.charting.charts.BarChart
        android:id="@+id/mBarChart"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

然后直接初始化轴线

        //堆叠条形图
        mBarChart = (BarChart) findViewById(R.id.mBarChart);
        mBarChart.setOnChartValueSelectedListener(this);
        mBarChart.getDescription().setEnabled(false);
        mBarChart.setMaxVisibleValueCount(40);
        // 扩展现在只能分别在x轴和y轴
        mBarChart.setPinchZoom(false);
        mBarChart.setDrawGridBackground(false);
        mBarChart.setDrawBarShadow(false);
        mBarChart.setDrawValueAboveBar(false);
        mBarChart.setHighlightFullBarEnabled(false);

        // 改变y标签的位置
        YAxis leftAxis = mBarChart.getAxisLeft();
        leftAxis.setValueFormatter(new MyAxisValueFormatter());
        leftAxis.setAxisMinimum(0f);
        mBarChart.getAxisRight().setEnabled(false);

        XAxis xLabels = mBarChart.getXAxis();
        xLabels.setPosition(XAxis.XAxisPosition.TOP);

        Legend l = mBarChart.getLegend();
        l.setVerticalAlignment(Legend.LegendVerticalAlignment.BOTTOM);
        l.setHorizontalAlignment(Legend.LegendHorizontalAlignment.RIGHT);
        l.setOrientation(Legend.LegendOrientation.HORIZONTAL);
        l.setDrawInside(false);
        l.setFormSize(8f);
        l.setFormToTextSpace(4f);
        l.setXEntrySpace(6f);

        setData();

这里尾巴上跟着个setData的方法就是设置数据的方法了

    //初始化
    private void setData() {
        ArrayList<BarEntry> yVals1 = new ArrayList<BarEntry>();

        for (int i = 0; i < 30 + 1; i++) {
            float mult = (50 + 1);
            float val1 = (float) (Math.random() * mult) + mult / 3;
            float val2 = (float) (Math.random() * mult) + mult / 3;
            float val3 = (float) (Math.random() * mult) + mult / 3;
            yVals1.add(new BarEntry(i, new float[]{val1, val2, val3}));
        }

        BarDataSet set1;

        if (mBarChart.getData() != null &&
                mBarChart.getData().getDataSetCount() > 0) {
            set1 = (BarDataSet) mBarChart.getData().getDataSetByIndex(0);
            set1.setValues(yVals1);
            mBarChart.getData().notifyDataChanged();
            mBarChart.notifyDataSetChanged();
        } else {
            set1 = new BarDataSet(yVals1, "三年级一班期末考试");
            set1.setColors(getColors());
            set1.setStackLabels(new String[]{"及格", "优秀", "不及格"});

            ArrayList<IBarDataSet> dataSets = new ArrayList<IBarDataSet>();
            dataSets.add(set1);

            BarData data = new BarData(dataSets);
            data.setValueFormatter(new MyValueFormatter());
            data.setValueTextColor(Color.WHITE);

            mBarChart.setData(data);
        }
        mBarChart.setFitBars(true);
        mBarChart.invalidate();
    }

这里我模拟了下颜色,所有我把颜色都调成了随机的

   private int[] getColors() {
        int stacksize = 3;
        //有尽可能多的颜色每项堆栈值
        int[] colors = new int[stacksize];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = ColorTemplate.MATERIAL_COLORS[i];
        }
        return colors;
    }

这样运行出来的效果 就和上面的截图一模一样了呢,我们继续来看下他的其他功能

二.显示顶点值

这里写图片描述

三.x轴动画

这里写图片描述

四.y轴动画

这里写图片描述

五.xy轴动画

这里写图片描述

六.显示边框

这里写图片描述

了解了大概,我就可以把整个的代码送上了,因为实际的代码并不多

activity_stackedbar.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.github.mikephil.charting.charts.BarChart
        android:id="@+id/mBarChart"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">


        <Button
            android:id="@+id/btn_show_values"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="顶点显示值"/>

        <Button
            android:id="@+id/btn_anim_x"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="X轴动画"/>

        <Button
            android:id="@+id/btn_anim_y"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Y轴动画"/>

        <Button
            android:id="@+id/btn_anim_xy"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="XY轴动画"/>


    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">


        <Button
            android:id="@+id/btn_save_pic"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="保存到相册"/>

        <Button
            android:id="@+id/btn_auto_mix_max"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自动最大最小值"/>

        <Button
            android:id="@+id/btn_actionToggleHighlight"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="高亮显示"/>

        <Button
            android:id="@+id/btn_show_border"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="显示边框"/>

    </LinearLayout>

</LinearLayout>

StackedBarActivity

public class StackedBarActivity extends BaseActivity implements OnChartValueSelectedListener, View.OnClickListener {

    private BarChart mBarChart;

    //显示顶点值
    private Button btn_show_values;
    //x轴动画
    private Button btn_anim_x;
    //y轴动画
    private Button btn_anim_y;
    //xy轴动画
    private Button btn_anim_xy;
    //保存到sd卡
    private Button btn_save_pic;
    //切换自动最大最小值
    private Button btn_auto_mix_max;
    //高亮显示
    private Button btn_actionToggleHighlight;
    //显示边框
    private Button btn_show_border;


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

        initView();
    }

    //初始化
    private void initView() {

        //基本控件
        btn_show_values = (Button) findViewById(R.id.btn_show_values);
        btn_show_values.setOnClickListener(this);
        btn_anim_x = (Button) findViewById(R.id.btn_anim_x);
        btn_anim_x.setOnClickListener(this);
        btn_anim_y = (Button) findViewById(R.id.btn_anim_y);
        btn_anim_y.setOnClickListener(this);
        btn_anim_xy = (Button) findViewById(R.id.btn_anim_xy);
        btn_anim_xy.setOnClickListener(this);
        btn_save_pic = (Button) findViewById(R.id.btn_save_pic);
        btn_save_pic.setOnClickListener(this);
        btn_auto_mix_max = (Button) findViewById(R.id.btn_auto_mix_max);
        btn_auto_mix_max.setOnClickListener(this);
        btn_actionToggleHighlight = (Button) findViewById(R.id.btn_actionToggleHighlight);
        btn_actionToggleHighlight.setOnClickListener(this);
        btn_show_border = (Button) findViewById(R.id.btn_show_border);
        btn_show_border.setOnClickListener(this);

        //堆叠条形图
        mBarChart = (BarChart) findViewById(R.id.mBarChart);
        mBarChart.setOnChartValueSelectedListener(this);
        mBarChart.getDescription().setEnabled(false);
        mBarChart.setMaxVisibleValueCount(40);
        // 扩展现在只能分别在x轴和y轴
        mBarChart.setPinchZoom(false);
        mBarChart.setDrawGridBackground(false);
        mBarChart.setDrawBarShadow(false);
        mBarChart.setDrawValueAboveBar(false);
        mBarChart.setHighlightFullBarEnabled(false);

        // 改变y标签的位置
        YAxis leftAxis = mBarChart.getAxisLeft();
        leftAxis.setValueFormatter(new MyAxisValueFormatter());
        leftAxis.setAxisMinimum(0f);
        mBarChart.getAxisRight().setEnabled(false);

        XAxis xLabels = mBarChart.getXAxis();
        xLabels.setPosition(XAxis.XAxisPosition.TOP);

        Legend l = mBarChart.getLegend();
        l.setVerticalAlignment(Legend.LegendVerticalAlignment.BOTTOM);
        l.setHorizontalAlignment(Legend.LegendHorizontalAlignment.RIGHT);
        l.setOrientation(Legend.LegendOrientation.HORIZONTAL);
        l.setDrawInside(false);
        l.setFormSize(8f);
        l.setFormToTextSpace(4f);
        l.setXEntrySpace(6f);

        setData();
    }

    //初始化
    private void setData() {
        ArrayList<BarEntry> yVals1 = new ArrayList<BarEntry>();

        for (int i = 0; i < 30 + 1; i++) {
            float mult = (50 + 1);
            float val1 = (float) (Math.random() * mult) + mult / 3;
            float val2 = (float) (Math.random() * mult) + mult / 3;
            float val3 = (float) (Math.random() * mult) + mult / 3;
            yVals1.add(new BarEntry(i, new float[]{val1, val2, val3}));
        }

        BarDataSet set1;

        if (mBarChart.getData() != null &&
                mBarChart.getData().getDataSetCount() > 0) {
            set1 = (BarDataSet) mBarChart.getData().getDataSetByIndex(0);
            set1.setValues(yVals1);
            mBarChart.getData().notifyDataChanged();
            mBarChart.notifyDataSetChanged();
        } else {
            set1 = new BarDataSet(yVals1, "三年级一班期末考试");
            set1.setColors(getColors());
            set1.setStackLabels(new String[]{"及格", "优秀", "不及格"});

            ArrayList<IBarDataSet> dataSets = new ArrayList<IBarDataSet>();
            dataSets.add(set1);

            BarData data = new BarData(dataSets);
            data.setValueFormatter(new MyValueFormatter());
            data.setValueTextColor(Color.WHITE);

            mBarChart.setData(data);
        }
        mBarChart.setFitBars(true);
        mBarChart.invalidate();
    }

    private int[] getColors() {
        int stacksize = 3;
        //有尽可能多的颜色每项堆栈值
        int[] colors = new int[stacksize];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = ColorTemplate.MATERIAL_COLORS[i];
        }
        return colors;
    }

    @Override
    public void onValueSelected(Entry e, Highlight h) {

    }

    @Override
    public void onNothingSelected() {

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            //显示顶点值
            case R.id.btn_show_values:
                for (IDataSet set : mBarChart.getData().getDataSets())
                    set.setDrawValues(!set.isDrawValuesEnabled());

                mBarChart.invalidate();
                break;
            //x轴动画
            case R.id.btn_anim_x:
                mBarChart.animateX(3000);
                break;
            //y轴动画
            case R.id.btn_anim_y:
                mBarChart.animateY(3000);
                break;
            //xy轴动画
            case R.id.btn_anim_xy:
                mBarChart.animateXY(3000, 3000);
                break;
            //保存到sd卡
            case R.id.btn_save_pic:
                if (mBarChart.saveToGallery("title" + System.currentTimeMillis(), 50)) {
                    Toast.makeText(getApplicationContext(), "保存成功",
                            Toast.LENGTH_SHORT).show();
                } else
                    Toast.makeText(getApplicationContext(), "保存失败",
                            Toast.LENGTH_SHORT).show();
                break;
            //切换自动最大最小值
            case R.id.btn_auto_mix_max:
                mBarChart.setAutoScaleMinMaxEnabled(!mBarChart.isAutoScaleMinMaxEnabled());
                mBarChart.notifyDataSetChanged();
                break;
            //高亮显示
            case R.id.btn_actionToggleHighlight:
                if (mBarChart.getData() != null) {
                    mBarChart.getData().setHighlightEnabled(
                            !mBarChart.getData().isHighlightEnabled());
                    mBarChart.invalidate();
                }
                break;
            //显示边框
            case R.id.btn_show_border:
                for (IBarDataSet set : mBarChart.getData().getDataSets())
                    ((BarDataSet) set).setBarBorderWidth(set.getBarBorderWidth() == 1.f ? 0.f : 1.f);
                mBarChart.invalidate();
                break;
        }
    }
}

到这里,堆叠条形图算是完成了,是不是不难,如果觉得难以理解,请看之前的系列,这样你就熟悉了

有兴趣的加群:555974449

Sample:http://download.csdn.net/detail/qq_26787115/9689868

作者:qq_26787115 发表于2016/11/24 17:49:40 原文链接
阅读:321 评论:0 查看评论

OkHttp使用教程

$
0
0
这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!

Android系统提供了两种HTTP通信类,HttpURLConnection和HttpClient。
关于HttpURLConnection和HttpClient的选择>>官方博客
尽管Google在大部分安卓版本中推荐使用HttpURLConnection,但是这个类相比HttpClient实在是太难用,太弱爆了。
OkHttp是一个相对成熟的解决方案,据说Android4.4的源码中可以看到HttpURLConnection已经替换成OkHttp实现了。所以我们更有理由相信OkHttp的强大。

OkHttp 处理了很多网络疑难杂症:会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP。OkHttp还处理了代理服务器问题和SSL握手失败问题。

使用 OkHttp 无需重写您程序中的网络代码。OkHttp实现了几乎和java.net.HttpURLConnection一样的API。如果你用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块。


注:在国内使用OkHttp会因为这个问题导致部分酷派手机用户无法联网,所以对于大众app来说,需要等待这个bug修复后再使用。或者尝试使用OkHttp的老版本。
截止到目前,OkHttp一直没有修复,并把修复计划延迟到了OkHttp2.3中。不是所有设备都能重现,仅少量设备会出现这个问题。(如果问题这么明显,OkHttp早就修复了)

入门

官方资料

官方介绍
github源码

使用范围

OkHttp支持Android 2.3及其以上版本。
对于Java, JDK1.7以上。

jar包准备

官方介绍页面有链接位置。这里把下载链接也写在下面。
OkHttp
Okio

基本使用

HTTP GET

  1. OkHttpClient client = new OkHttpClient();
  2.  
  3. String run(String url) throws IOException {
  4.     Request request = new Request.Builder().url(url).build();
  5.     Response response = client.newCall(request).execute();    if (response.isSuccessful()) {        return response.body().string();
  6.     } else {        throw new IOException("Unexpected code " + response);
  7.     }
  8. }

Request是OkHttp中访问的请求,Builder是辅助类。Response即OkHttp中的响应。

Response类:

  1. public boolean isSuccessful()
  2. Returns true if the code is in [200..300),
  3.  which means the request was successfully received, understood, and accepted.

response.body()返回ResponseBody类

可以方便的获取string

  1. public final String string() throws IOException
  2. Returns the response as a string decoded with the charset of the Content-Type header. If that header is either absent or lacks a charset,
  3.  this will attempt to decode the response body as UTF-8.Throws:
  4. IOException

当然也能获取到流的形式:

  1. public final InputStream byteStream()

HTTP POST

POST提交Json数据

  1. public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
  2. OkHttpClient client = new OkHttpClient();
  3. String post(String url, String json) throws IOException {
  4.      RequestBody body = RequestBody.create(JSON, json);
  5.       Request request = new Request.Builder()
  6.       .url(url)
  7.       .post(body)
  8.       .build();
  9.       Response response = client.newCall(request).execute();
  10.     f (response.isSuccessful()) {
  11.         return response.body().string();
  12.     } else {
  13.         throw new IOException("Unexpected code " + response);
  14.     }
  15. }

使用Request的post方法来提交请求体RequestBody

POST提交键值对

很多时候我们会需要通过POST方式把键值对数据传送到服务器。 OkHttp提供了很方便的方式来做这件事情。

  1. OkHttpClient client = new OkHttpClient();
  2. String post(String url, String json) throws IOException {
  3.  
  4.      RequestBody formBody = new FormEncodingBuilder()
  5.     .add("platform", "android")
  6.     .add("name", "bug")
  7.     .add("subject", "XXXXXXXXXXXXXXX")
  8.     .build();
  9.  
  10.       Request request = new Request.Builder()
  11.       .url(url)
  12.       .post(body)
  13.       .build();
  14.  
  15.       Response response = client.newCall(request).execute();
  16.     if (response.isSuccessful()) {
  17.         return response.body().string();
  18.     } else {
  19.         throw new IOException("Unexpected code " + response);
  20.     }
  21. }

总结

通过上面的例子我们可以发现,OkHttp在很多时候使用都是很方便的,而且很多代码也有重复,因此特地整理了下面的工具类。
注意:

  • OkHttp官方文档并不建议我们创建多个OkHttpClient,因此全局使用一个。 如果有需要,可以使用clone方法,再进行自定义。这点在后面的高级教程里会提到。

  • enqueue为OkHttp提供的异步方法,入门教程中并没有提到,后面的高级教程里会有解释。

  1. import java.io.IOException;
  2. import java.util.List;
  3. import java.util.concurrent.TimeUnit;
  4. import org.apache.http.client.utils.URLEncodedUtils;
  5. import org.apache.http.message.BasicNameValuePair;
  6. import cn.wiz.sdk.constant.WizConstant;
  7. import com.squareup.okhttp.Callback;
  8. import com.squareup.okhttp.OkHttpClient;
  9. import com.squareup.okhttp.Request;
  10. import com.squareup.okhttp.Response; 
  11.  
  12. public class OkHttpUtil {
  13.     private static final OkHttpClient mOkHttpClient = new OkHttpClient();
  14.     static{
  15.         mOkHttpClient.setConnectTimeout(30, TimeUnit.SECONDS);
  16.     }
  17.     /**
  18.      * 该不会开启异步线程。
  19.      * @param request
  20.      * @return
  21.      * @throws IOException
  22.      */
  23.     public static Response execute(Request request) throws IOException{
  24.         return mOkHttpClient.newCall(request).execute();
  25.     }
  26.     /**
  27.      * 开启异步线程访问网络
  28.      * @param request
  29.      * @param responseCallback
  30.      */
  31.     public static void enqueue(Request request, Callback responseCallback){
  32.         mOkHttpClient.newCall(request).enqueue(responseCallback);
  33.     }
  34.     /**
  35.      * 开启异步线程访问网络, 且不在意返回结果(实现空callback)
  36.      * @param request
  37.      */
  38.     public static void enqueue(Request request){
  39.         mOkHttpClient.newCall(request).enqueue(new Callback() {
  40.             
  41.             @Override
  42.             public void onResponse(Response arg0) throws IOException {
  43.                 
  44.             }
  45.             
  46.             @Override
  47.             public void onFailure(Request arg0, IOException arg1) {
  48.                 
  49.             }
  50.         });
  51.     }
  52.     public static String getStringFromServer(String url) throws IOException{
  53.         Request request = new Request.Builder().url(url).build();
  54.         Response response = execute(request);
  55.         if (response.isSuccessful()) {
  56.             String responseUrl = response.body().string();
  57.             return responseUrl;
  58.         } else {
  59.             throw new IOException("Unexpected code " + response);
  60.         }
  61.     }
  62.     private static final String CHARSET_NAME = "UTF-8";
  63.     /**
  64.      * 这里使用了HttpClinet的API。只是为了方便
  65.      * @param params
  66.      * @return
  67.      */
  68.     public static String formatParams(List<BasicNameValuePair> params){
  69.         return URLEncodedUtils.format(params, CHARSET_NAME);
  70.     }
  71.     /**
  72.      * 为HttpGet 的 url 方便的添加多个name value 参数。
  73.      * @param url
  74.      * @param params
  75.      * @return
  76.      */
  77.     public static String attachHttpGetParams(String url, List<BasicNameValuePair> params){
  78.         return url + "?" + formatParams(params);
  79.     }
  80.     /**
  81.      * 为HttpGet 的 url 方便的添加1个name value 参数。
  82.      * @param url
  83.      * @param name
  84.      * @param value
  85.      * @return
  86.      */
  87.     public static String attachHttpGetParam(String url, String name, String value){
  88.         return url + "?" + name + "=" + value;
  89.     }
  90. }

高级

高级属性其实用的不多,这里主要是对OkHttp github官方教程进行了翻译。

同步get

下载一个文件,打印他的响应头,以string形式打印响应体。
响应体的 string() 方法对于小文档来说十分方便、高效。但是如果响应体太大(超过1MB),应避免适应 string()方法 ,因为他会将把整个文档加载到内存中。
对于超过1MB的响应body,应使用流的方式来处理body。

  1. private final OkHttpClient client = new OkHttpClient();
  2.  
  3. public void run() throws Exception {
  4.     Request request = new Request.Builder()
  5.         .url("http://publicobject.com/helloworld.txt")
  6.         .build();
  7.  
  8.     Response response = client.newCall(request).execute();
  9.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  10.  
  11.     Headers responseHeaders = response.headers();
  12.     for (int i = 0; i < responseHeaders.size(); i++) {
  13.       System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
  14.     }
  15.  
  16.     System.out.println(response.body().string());
  17. }

异步get

在一个工作线程中下载文件,当响应可读时回调Callback接口。读取响应时会阻塞当前线程。OkHttp现阶段不提供异步api来接收响应体。

  1. private final OkHttpClient client = new OkHttpClient();
  2.  
  3. public void run() throws Exception {
  4.     Request request = new Request.Builder()
  5.         .url("http://publicobject.com/helloworld.txt")
  6.         .build();
  7.  
  8.     client.newCall(request).enqueue(new Callback() {
  9.       @Override public void onFailure(Request request, Throwable throwable) {
  10.         throwable.printStackTrace();
  11.       }
  12.  
  13.       @Override public void onResponse(Response response) throws IOException {
  14.         if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  15.  
  16.         Headers responseHeaders = response.headers();
  17.         for (int i = 0; i < responseHeaders.size(); i++) {
  18.           System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
  19.         }
  20.  
  21.         System.out.println(response.body().string());
  22.       }
  23.     });
  24. }

提取响应头

典型的HTTP头 像是一个 Map<String, String> :每个字段都有一个或没有值。但是一些头允许多个值,像Guava的Multimap。例如:HTTP响应里面提供的Vary响应头,就是多值的。OkHttp的api试图让这些情况都适用。
当写请求头的时候,使用header(name, value)可以设置唯一的name、value。如果已经有值,旧的将被移除,然后添加新的。使用addHeader(name, value)可以添加多值(添加,不移除已有的)。
当读取响应头时,使用header(name)返回最后出现的name、value。通常情况这也是唯一的name、value。如果没有值,那么header(name)将返回null。如果想读取字段对应的所有值,使用headers(name)会返回一个list。
为了获取所有的Header,Headers类支持按index访问。

  1. private final OkHttpClient client = new OkHttpClient();
  2.  
  3. public void run() throws Exception {
  4.     Request request = new Request.Builder()
  5.         .url("https://api.github.com/repos/square/okhttp/issues")
  6.         .header("User-Agent", "OkHttp Headers.java")
  7.         .addHeader("Accept", "application/json; q=0.5")
  8.         .addHeader("Accept", "application/vnd.github.v3+json")
  9.         .build();
  10.  
  11.     Response response = client.newCall(request).execute();
  12.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  13.  
  14.     System.out.println("Server: " + response.header("Server"));
  15.     System.out.println("Date: " + response.header("Date"));
  16.     System.out.println("Vary: " + response.headers("Vary"));
  17. }

Post方式提交String

使用HTTP POST提交请求到服务。这个例子提交了一个markdown文档到web服务,以HTML方式渲染markdown。因为整个请求体都在内存中,因此避免使用此api提交大文档(大于1MB)。

  1. public static final MediaType MEDIA_TYPE_MARKDOWN
  2.   = MediaType.parse("text/x-markdown; charset=utf-8");
  3.  
  4. private final OkHttpClient client = new OkHttpClient();
  5.  
  6. public void run() throws Exception {
  7.     String postBody = ""
  8.         + "Releases\n"
  9.         + "--------\n"
  10.         + "\n"
  11.         + " * _1.0_ May 6, 2013\n"
  12.         + " * _1.1_ June 15, 2013\n"
  13.         + " * _1.2_ August 11, 2013\n";
  14.  
  15.     Request request = new Request.Builder()
  16.         .url("https://api.github.com/markdown/raw")
  17.         .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
  18.         .build();
  19.  
  20.     Response response = client.newCall(request).execute();
  21.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  22.  
  23.     System.out.println(response.body().string());
  24. }

Post方式提交流

以流的方式POST提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用OutputStream,你可以使用BufferedSink.outputStream()来获取。

  1. public static final MediaType MEDIA_TYPE_MARKDOWN
  2.       = MediaType.parse("text/x-markdown; charset=utf-8");
  3.  
  4. private final OkHttpClient client = new OkHttpClient();
  5.  
  6. public void run() throws Exception {
  7.     RequestBody requestBody = new RequestBody() {
  8.       @Override public MediaType contentType() {
  9.         return MEDIA_TYPE_MARKDOWN;
  10.       }
  11.  
  12.       @Override public void writeTo(BufferedSink sink) throws IOException {
  13.         sink.writeUtf8("Numbers\n");
  14.         sink.writeUtf8("-------\n");
  15.         for (int i = 2; i <= 997; i++) {
  16.           sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
  17.         }
  18.       }
  19.  
  20.       private String factor(int n) {
  21.         for (int i = 2; i < n; i++) {
  22.           int x = n / i;
  23.           if (* i == n) return factor(x) + " × " + i;
  24.         }
  25.         return Integer.toString(n);
  26.       }
  27.     };
  28.  
  29.     Request request = new Request.Builder()
  30.         .url("https://api.github.com/markdown/raw")
  31.         .post(requestBody)
  32.         .build();
  33.  
  34.     Response response = client.newCall(request).execute();
  35.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  36.  
  37.     System.out.println(response.body().string());
  38. }

Post方式提交文件

以文件作为请求体是十分简单的。

  1. public static final MediaType MEDIA_TYPE_MARKDOWN
  2.   = MediaType.parse("text/x-markdown; charset=utf-8");
  3.  
  4. private final OkHttpClient client = new OkHttpClient();
  5.  
  6. public void run() throws Exception {
  7.     File file = new File("README.md");
  8.  
  9.     Request request = new Request.Builder()
  10.         .url("https://api.github.com/markdown/raw")
  11.         .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
  12.         .build();
  13.  
  14.     Response response = client.newCall(request).execute();
  15.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  16.  
  17.     System.out.println(response.body().string());
  18. }

Post方式提交表单

使用FormEncodingBuilder来构建和HTML<form>标签相同效果的请求体。键值对将使用一种HTML兼容形式的URL编码来进行编码。

  1. private final OkHttpClient client = new OkHttpClient();
  2.  
  3. public void run() throws Exception {
  4.     RequestBody formBody = new FormEncodingBuilder()
  5.         .add("search", "Jurassic Park")
  6.         .build();
  7.     Request request = new Request.Builder()
  8.         .url("https://en.wikipedia.org/w/index.php")
  9.         .post(formBody)
  10.         .build();
  11.  
  12.     Response response = client.newCall(request).execute();
  13.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  14.  
  15.     System.out.println(response.body().string());
  16. }

Post方式提交分块请求

MultipartBuilder可以构建复杂的请求体,与HTML文件上传形式兼容。多块请求体中每块请求都是一个请求体,可以定义自己的请求头。这些请求头可以用来描述这块请求,例如他的Content-Disposition。如果Content-LengthContent-Type可用的话,他们会被自动添加到请求头中。

  1. private static final String IMGUR_CLIENT_ID = "...";
  2. private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
  3.  
  4. private final OkHttpClient client = new OkHttpClient();
  5.  
  6. public void run() throws Exception {
  7.     // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
  8.     RequestBody requestBody = new MultipartBuilder()
  9.         .type(MultipartBuilder.FORM)
  10.         .addPart(
  11.             Headers.of("Content-Disposition", "form-data; name=\"title\""),
  12.             RequestBody.create(null, "Square Logo"))
  13.         .addPart(
  14.             Headers.of("Content-Disposition", "form-data; name=\"image\""),
  15.             RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
  16.         .build();
  17.  
  18.     Request request = new Request.Builder()
  19.         .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
  20.         .url("https://api.imgur.com/3/image")
  21.         .post(requestBody)
  22.         .build();
  23.  
  24.     Response response = client.newCall(request).execute();
  25.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  26.  
  27.     System.out.println(response.body().string());
  28. }

使用Gson来解析JSON响应

Gson是一个在JSON和Java对象之间转换非常方便的api。这里我们用Gson来解析Github API的JSON响应。
注意:ResponseBody.charStream()使用响应头Content-Type指定的字符集来解析响应体。默认是UTF-8。

  1. private final OkHttpClient client = new OkHttpClient();
  2. private final Gson gson = new Gson();
  3.  
  4. public void run() throws Exception {
  5.     Request request = new Request.Builder()
  6.         .url("https://api.github.com/gists/c2a7c39532239ff261be")
  7.         .build();
  8.     Response response = client.newCall(request).execute();
  9.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  10.  
  11.     Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
  12.     for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
  13.       System.out.println(entry.getKey());
  14.       System.out.println(entry.getValue().content);
  15.     }
  16. }
  17.  
  18. static class Gist {
  19.     Map<String, GistFile> files;
  20. }
  21.  
  22. static class GistFile {
  23.     String content;
  24. }

响应缓存

为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。
一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttp(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。
响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600 ,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600

  1. private final OkHttpClient client;
  2.  
  3. public CacheResponse(File cacheDirectory) throws Exception {
  4.     int cacheSize = 10 * 1024 * 1024; // 10 MiB
  5.     Cache cache = new Cache(cacheDirectory, cacheSize);
  6.  
  7.     client = new OkHttpClient();
  8.     client.setCache(cache);
  9. }
  10.  
  11. public void run() throws Exception {
  12.     Request request = new Request.Builder()
  13.         .url("http://publicobject.com/helloworld.txt")
  14.         .build();
  15.  
  16.     Response response1 = client.newCall(request).execute();
  17.     if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
  18.  
  19.     String response1Body = response1.body().string();
  20.     System.out.println("Response 1 response:          " + response1);
  21.     System.out.println("Response 1 cache response:    " + response1.cacheResponse());
  22.     System.out.println("Response 1 network response:  " + response1.networkResponse());
  23.  
  24.     Response response2 = client.newCall(request).execute();
  25.     if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
  26.  
  27.     String response2Body = response2.body().string();
  28.     System.out.println("Response 2 response:          " + response2);
  29.     System.out.println("Response 2 cache response:    " + response2.cacheResponse());
  30.     System.out.println("Response 2 network response:  " + response2.networkResponse());
  31.  
  32.     System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
  33. }

扩展

在这一节还提到了下面一句:
There are cache headers to force a cached response, force a network response, or force the network response to be validated with a conditional GET.

我不是很懂cache,平时用到的也不多,所以把Google在Android Developers一段相关的解析放到这里吧。

Force a Network Response

In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip the cache, and fetch data directly from the server. To force a full refresh, add the no-cache directive:

  1. connection.addRequestProperty("Cache-Control", "no-cache");

If it is only necessary to force a cached response to be validated by the server, use the more efficient max-age=0 instead:

  1. connection.addRequestProperty("Cache-Control", "max-age=0");

Force a Cache Response

Sometimes you'll want to show resources if they are available immediately, but not otherwise. This can be used so your application can show something while waiting for the latest data to be downloaded. To restrict a request to locally-cached resources, add the only-if-cached directive:

  1. try {
  2.      connection.addRequestProperty("Cache-Control", "only-if-cached");
  3.      InputStream cached = connection.getInputStream();
  4.      // the resource was cached! show it
  5.   catch (FileNotFoundException e) {
  6.      // the resource was not cached
  7.  }
  8. }

This technique works even better in situations where a stale response is better than no response. To permit stale cached responses, use the max-stale directive with the maximum staleness in seconds:

  1. int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks staleconnection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);

以上信息来自:HttpResponseCache - Android SDK | Android Developers

取消一个Call

使用Call.cancel()可以立即停止掉一个正在执行的call。如果一个线程正在写请求或者读响应,将会引发IOException。当call没有必要的时候,使用这个api可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的call都可以取消。
你可以通过tags来同时取消多个请求。当你构建一请求时,使用RequestBuilder.tag(tag)来分配一个标签。之后你就可以用OkHttpClient.cancel(tag)来取消所有带有这个tag的call。

  1. private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  2. private final OkHttpClient client = new OkHttpClient();
  3.  
  4. public void run() throws Exception {
  5.     Request request = new Request.Builder()
  6.         .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
  7.         .build();
  8.  
  9.     final long startNanos = System.nanoTime();
  10.     final Call call = client.newCall(request);
  11.  
  12.     // Schedule a job to cancel the call in 1 second.
  13.     executor.schedule(new Runnable() {
  14.       @Override public void run() {
  15.         System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
  16.         call.cancel();
  17.         System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
  18.       }
  19.     }, 1, TimeUnit.SECONDS);
  20.  
  21.     try {
  22.       System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
  23.       Response response = call.execute();
  24.       System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
  25.           (System.nanoTime() - startNanos) / 1e9f, response);
  26.     } catch (IOException e) {
  27.       System.out.printf("%.2f Call failed as expected: %s%n",
  28.           (System.nanoTime() - startNanos) / 1e9f, e);
  29.     }
  30. }

超时

没有响应时使用超时结束call。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。OkHttp支持连接,读取和写入超时。

  1. private final OkHttpClient client;
  2.  
  3. public ConfigureTimeouts() throws Exception {
  4.     client = new OkHttpClient();
  5.     client.setConnectTimeout(10, TimeUnit.SECONDS);
  6.     client.setWriteTimeout(10, TimeUnit.SECONDS);
  7.     client.setReadTimeout(30, TimeUnit.SECONDS);
  8. }
  9.  
  10. public void run() throws Exception {
  11.     Request request = new Request.Builder()
  12.         .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
  13.         .build();
  14.  
  15.     Response response = client.newCall(request).execute();
  16.     System.out.println("Response completed: " + response);
  17. }

每个call的配置

使用OkHttpClient,所有的HTTP Client配置包括代理设置、超时设置、缓存设置。当你需要为单个call改变配置的时候,clone 一个OkHttpClient。这个api将会返回一个浅拷贝(shallow copy),你可以用来单独自定义。下面的例子中,我们让一个请求是500ms的超时、另一个是3000ms的超时。

  1. private final OkHttpClient client = new OkHttpClient();
  2.  
  3. public void run() throws Exception {
  4.     Request request = new Request.Builder()
  5.         .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
  6.         .build();
  7.  
  8.     try {
  9.       Response response = client.clone() // Clone to make a customized OkHttp for this request.
  10.           .setReadTimeout(500, TimeUnit.MILLISECONDS)
  11.           .newCall(request)
  12.           .execute();
  13.       System.out.println("Response 1 succeeded: " + response);
  14.     } catch (IOException e) {
  15.       System.out.println("Response 1 failed: " + e);
  16.     }
  17.  
  18.     try {
  19.       Response response = client.clone() // Clone to make a customized OkHttp for this request.
  20.           .setReadTimeout(3000, TimeUnit.MILLISECONDS)
  21.           .newCall(request)
  22.           .execute();
  23.       System.out.println("Response 2 succeeded: " + response);
  24.     } catch (IOException e) {
  25.       System.out.println("Response 2 failed: " + e);
  26.     }
  27. }

处理验证

这部分和HTTP AUTH有关。
相关资料:HTTP AUTH 那些事 - 王绍全的博客 - 博客频道 - CSDN.NET

OkHttp会自动重试未验证的请求。当响应是401 Not Authorized时,Authenticator会被要求提供证书。Authenticator的实现中需要建立一个新的包含证书的请求。如果没有证书可用,返回null来跳过尝试。

  1. public List<Challenge> challenges()
  2. Returns the authorization challenges appropriate for this response's code. 
  3. If the response code is 401 unauthorized, 
  4. this returns the "WWW-Authenticate" challenges.
  5. If the response code is 407 proxy unauthorized, this returns the "Proxy-Authenticate" challenges.
  6. Otherwise this returns an empty list of challenges.

当需要实现一个Basic challenge, 使用Credentials.basic(username, password)来编码请求头。

  1. private final OkHttpClient client = new OkHttpClient();
  2.  
  3. public void run() throws Exception {
  4.     client.setAuthenticator(new Authenticator() {
  5.       @Override public Request authenticate(Proxy proxy, Response response) {
  6.         System.out.println("Authenticating for response: " + response);
  7.         System.out.println("Challenges: " + response.challenges());
  8.         String credential = Credentials.basic("jesse", "password1");
  9.         return response.request().newBuilder()
  10.             .header("Authorization", credential)
  11.             .build();
  12.       }
  13.  
  14.       @Override public Request authenticateProxy(Proxy proxy, Response response) {
  15.         return null; // Null indicates no attempt to authenticate.
  16.       }
  17.     });
  18.  
  19.     Request request = new Request.Builder()
  20.         .url("http://publicobject.com/secrets/hellosecret.txt")
  21.         .build();
  22.  
  23.     Response response = client.newCall(request).execute();
  24.     if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  25.  
  26.     System.out.println(response.body().string());
  27. }
作者:qq_35114086 发表于2016/11/24 17:56:36 原文链接
阅读:55 评论:0 查看评论

Unity3D自学笔记——星际2技能系统分析(二)Ability(Move, Stop, Attack)

$
0
0

星际2技能系统分析(二)Ability(Move, Stop, Attack)

人族机枪兵,有移动,停止,攻击及兴奋剂四个技能,对应类图如下
这里写图片描述

CAbil

技能的虚基类,所有技能类都派生于它

属性

这里写图片描述
Name: 技能名称
SetId:唯一标识
Alignment:技能瞄准对象
TechPlayer:科技玩家
TechAliasArray:科技别名
SharedFlags:共享标签

XML定义

这里写图片描述

CAbilMove

移动技能基类

这里写图片描述
CmdButtonArray:技能按钮的值,关联CButton对象
FleeRange:当遭遇敌人攻击但又无法还击时,单位将逃跑的距离
FleeTime:单位逃离单次攻击时将会花费的最大时间
FllowAcquireRange:跟随搜索范围
FllowRangeSlop:跟随距离
MinPatrolDistance:最小巡逻距离

XML定义

这里写图片描述

实体类

移动实体类,根节点为继承的基类名, ID为实体类名

这里写图片描述

CAbilStop

停止技能基类

属性

这里写图片描述

XML定义

这里写图片描述

实体类

这里写图片描述

CAbilAttack

攻击技能基类

属性

这里写图片描述
MinAttackSpeedMultiplier:使用该技能后攻击速度提高的最小倍数。
MaxAttackSpeedMultiplier:使用该技能后攻击速度提高的最大倍数。
CmdButtonArray:技能的特定命令相关的数据
AcquireFilters:定义拥有该技能的单位可以自己获取的目标类型
AcquirePriority:当一个以上的同单位技能尝试自动获取目标时,返回最高的“获取有限级”的技能会最先有机会潜在目标。不会获取目标的技能应将之设为0。该值被设为0的技能不会获取目标。
SmartFilters:这些类型对象能被攻击

XML定义

这里写图片描述

实体类

这里写图片描述

执行逻辑

用CAbilMove举例
这里写图片描述
上图中重名文件是因为包含在不同的XML文件中,可以理解为存在与不同的类库里

每个对象除了基本的属性外,还可以挂载其他的模组,如按钮,单位,验证器等,基类挂载了按钮
这里写图片描述
实体类挂载了单位,验证器,移动器,移动器包含了飞行和地面两个
这里写图片描述

可以推算出其逻辑为
1.用户点击Move按钮
2.Move按钮找到当前单位的CMoveAbil
3.Move实体类找到对应的CMover
4.CMover根据Move实体类及单位的属性执行单位移动

类似于MVC模式
View(CButton)
Model(CAbil)
Controller(CMover)

作者:alistair_chow 发表于2016/11/24 18:10:08 原文链接
阅读:46 评论:0 查看评论

安卓系统常用广播汇总

$
0
0

安卓系统中有很多的广播和接收事件,了解这些事件对开发应用功能的思路会有很大的帮助。

android.provider.Telephony.SMS_RECEIVED
接收到短信时的广播
Intent.ACTION_AIRPLANE_MODE_CHANGED;
//关闭或打开飞行模式时的广播
 
Intent.ACTION_BATTERY_CHANGED;
//充电状态,或者电池的电量发生变化
//电池的充电状态、电荷级别改变,不能通过组建声明接收这个广播,只有通过Context.registerReceiver()注册
 
Intent.ACTION_BATTERY_LOW;
//表示电池电量低
 
Intent.ACTION_BATTERY_OKAY;
//表示电池电量充足,即从电池电量低变化到饱满时会发出广播
 
Intent.ACTION_BOOT_COMPLETED;
//在系统启动完成后,这个动作被广播一次(只有一次)。
 
Intent.ACTION_CAMERA_BUTTON;
//按下照相时的拍照按键(硬件按键)时发出的广播
 
Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
//当屏幕超时进行锁屏时,当用户按下电源按钮,长按或短按(不管有没跳出话框),进行锁屏时,android系统都会广播此Action消息
 
Intent.ACTION_CONFIGURATION_CHANGED;
//设备当前设置被改变时发出的广播(包括的改变:界面语言,设备方向,等,请参考Configuration.java)
 
Intent.ACTION_DATE_CHANGED;
//设备日期发生改变时会发出此广播
 
Intent.ACTION_DEVICE_STORAGE_LOW;
//设备内存不足时发出的广播,此广播只能由系统使用,其它APP不可用?
 
Intent.ACTION_DEVICE_STORAGE_OK;
//设备内存从不足到充足时发出的广播,此广播只能由系统使用,其它APP不可用?
 
Intent.ACTION_DOCK_EVENT;
//
//发出此广播的地方frameworks\base\services\java\com\android\server\DockObserver.java
 
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE;
////移动APP完成之后,发出的广播(移动是指:APP2SD)
 
Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE;
//正在移动APP时,发出的广播(移动是指:APP2SD)
 
Intent.ACTION_GTALK_SERVICE_CONNECTED;
//Gtalk已建立连接时发出的广播
 
Intent.ACTION_GTALK_SERVICE_DISCONNECTED;
//Gtalk已断开连接时发出的广播
 
Intent.ACTION_HEADSET_PLUG;
//在耳机口上插入耳机时发出的广播
 
Intent.ACTION_INPUT_METHOD_CHANGED;
//改变输入法时发出的广播
 
Intent.ACTION_LOCALE_CHANGED;
//设备当前区域设置已更改时发出的广播
 
Intent.ACTION_MANAGE_PACKAGE_STORAGE;
//
 
Intent.ACTION_MEDIA_BAD_REMOVAL;
//未正确移除SD卡(正确移除SD卡的方法:设置--SD卡和设备内存--卸载SD卡),但已把SD卡取出来时发出的广播
//广播:扩展介质(扩展卡)已经从 SD 卡插槽拔出,但是挂载点 (mount point) 还没解除 (unmount)
 
Intent.ACTION_MEDIA_BUTTON;
//按下"Media Button" 按键时发出的广播,假如有"Media Button" 按键的话(硬件按键)
 
Intent.ACTION_MEDIA_CHECKING;
//插入外部储存装置,比如SD卡时,系统会检验SD卡,此时发出的广播?
 
Intent.ACTION_MEDIA_EJECT;
//已拔掉外部大容量储存设备发出的广播(比如SD卡,或移动硬盘),不管有没有正确卸载都会发出此广播?
//广播:用户想要移除扩展介质(拔掉扩展卡)。
 
Intent.ACTION_MEDIA_MOUNTED;
//插入SD卡并且已正确安装(识别)时发出的广播
//广播:扩展介质被插入,而且已经被挂载。
 
Intent.ACTION_MEDIA_NOFS;
//
 
Intent.ACTION_MEDIA_REMOVED;
//外部储存设备已被移除,不管有没正确卸载,都会发出此广播?
// 广播:扩展介质被移除。
 
Intent.ACTION_MEDIA_SCANNER_FINISHED;
//广播:已经扫描完介质的一个目录
 
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE;
//
 
Intent.ACTION_MEDIA_SCANNER_STARTED;
//广播:开始扫描介质的一个目录
 
Intent.ACTION_MEDIA_SHARED;
// 广播:扩展介质的挂载被解除 (unmount),因为它已经作为 USB 大容量存储被共享。
 
Intent.ACTION_MEDIA_UNMOUNTABLE;
//
 
Intent.ACTION_MEDIA_UNMOUNTED
// 广播:扩展介质存在,但是还没有被挂载 (mount)。
 
Intent.ACTION_NEW_OUTGOING_CALL;
//
 
Intent.ACTION_PACKAGE_ADDED;
//成功的安装APK之后
//广播:设备上新安装了一个应用程序包。
//一个新应用包已经安装在设备上,数据包括包名(最新安装的包程序不能接收到这个广播)
 
Intent.ACTION_PACKAGE_CHANGED;
//一个已存在的应用程序包已经改变,包括包名
 
Intent.ACTION_PACKAGE_DATA_CLEARED;
//清除一个应用程序的数据时发出的广播(在设置--应用管理--选中某个应用,之后点清除数据时?)
//用户已经清除一个包的数据,包括包名(清除包程序不能接收到这个广播)
 
 
Intent.ACTION_PACKAGE_INSTALL;
//触发一个下载并且完成安装时发出的广播,比如在电子市场里下载应用?
//
 
Intent.ACTION_PACKAGE_REMOVED;
//成功的删除某个APK之后发出的广播
//一个已存在的应用程序包已经从设备上移除,包括包名(正在被安装的包程序不能接收到这个广播)
 
 
Intent.ACTION_PACKAGE_REPLACED;
//替换一个现有的安装包时发出的广播(不管现在安装的APP比之前的新还是旧,都会发出此广播?)
 
Intent.ACTION_PACKAGE_RESTARTED;
//用户重新开始一个包,包的所有进程将被杀死,所有与其联系的运行时间状态应该被移除,包括包名(重新开始包程序不能接收到这个广播)
 
 
Intent.ACTION_POWER_CONNECTED;
//插上外部电源时发出的广播
 
Intent.ACTION_POWER_DISCONNECTED;
//已断开外部电源连接时发出的广播
 
Intent.ACTION_PROVIDER_CHANGED;
//
 
Intent.ACTION_REBOOT;
//重启设备时的广播
 
Intent.ACTION_SCREEN_OFF;
//屏幕被关闭之后的广播
 
Intent.ACTION_SCREEN_ON;
//屏幕被打开之后的广播
 
Intent.ACTION_SHUTDOWN;
//关闭系统时发出的广播
 
Intent.ACTION_TIMEZONE_CHANGED;
//时区发生改变时发出的广播
 
Intent.ACTION_TIME_CHANGED;
//时间被设置时发出的广播
 
Intent.ACTION_TIME_TICK;
//广播:当前时间已经变化(正常的时间流逝)。
//当前时间改变,每分钟都发送,不能通过组件声明来接收,只有通过Context.registerReceiver()方法来注册
 
Intent.ACTION_UID_REMOVED;
//一个用户ID已经从系统中移除发出的广播
//
 
Intent.ACTION_UMS_CONNECTED;
//设备已进入USB大容量储存状态时发出的广播?
 
Intent.ACTION_UMS_DISCONNECTED;
//设备已从USB大容量储存状态转为正常状态时发出的广播?
 
Intent.ACTION_USER_PRESENT;
//
 
Intent.ACTION_WALLPAPER_CHANGED;
//设备墙纸已改变时发出的广播
android系统部分广播 
String ADD_SHORTCUT_ACTION 动作:在系统中添加一个快捷方式。. "android.intent.action.ADD_SHORTCUT"
String ALL_APPS_ACTION 动作:列举所有可用的应用。
输入:无。 "android.intent.action.ALL_APPS"
String ALTERNATIVE_CATEGORY 类别:说明 activity 是用户正在浏览的数据的一个可选操作。 "android.intent.category.ALTERNATIVE"
String ANSWER_ACTION 动作:处理拨入的电话。 "android.intent.action.ANSWER"
String BATTERY_CHANGED_ACTION 广播:充电状态,或者电池的电量发生变化。 "android.intent.action.BATTERY_CHANGED"
String BOOT_COMPLETED_ACTION 广播:在系统启动后,这个动作被广播一次(只有一次)。 "android.intent.action.BOOT_COMPLETED"
String BROWSABLE_CATEGORY 类别:能够被浏览器安全使用的 activities 必须支持这个类别。 "android.intent.category.BROWSABLE"
String BUG_REPORT_ACTION 动作:显示 activity 报告错误。 "android.intent.action.BUG_REPORT"
String CALL_ACTION 动作:拨打电话,被呼叫的联系人在数据中指定。 "android.intent.action.CALL"
String CALL_FORWARDING_STATE_CHANGED_ACTION 广播:语音电话的呼叫转移状态已经改变。 "android.intent.action.CFF"
String CLEAR_CREDENTIALS_ACTION 动作:清除登陆凭证 (credential)。 "android.intent.action.CLEAR_CREDENTIALS"
String CONFIGURATION_CHANGED_ACTION 广播:设备的配置信息已经改变,参见 Resources.Configuration. "android.intent.action.CONFIGURATION_CHANGED"
Creator CREATOR 无 无
String DATA_ACTIVITY_STATE_CHANGED_ACTION 广播:电话的数据活动(data activity)状态(即收发数据的状态)已经改变。 "android.intent.action.DATA_ACTIVITY"
String DATA_CONNECTION_STATE_CHANGED_ACTION 广播:电话的数据连接状态已经改变。 "android.intent.action.DATA_STATE"
String DATE_CHANGED_ACTION 广播:日期被改变。 "android.intent.action.DATE_CHANGED"
String DEFAULT_ACTION 动作:和 VIEW_ACTION 相同,是在数据上执行的标准动作。 "android.intent.action.VIEW"
String DEFAULT_CATEGORY 类别:如果 activity 是对数据执行确省动作(点击, center press)的一个选项,需要设置这个类别。 "android.intent.category.DEFAULT"
String DELETE_ACTION 动作:从容器中删除给定的数据。 "android.intent.action.DELETE"
String DEVELOPMENT_PREFERENCE_CATEGORY 类别:说明 activity 是一个设置面板 (development preference panel). "android.intent.category.DEVELOPMENT_PREFERENCE"
String DIAL_ACTION 动作:拨打数据中指定的电话号码。 "android.intent.action.DIAL"
String EDIT_ACTION 动作:为制定的数据显示可编辑界面。 "android.intent.action.EDIT"
String EMBED_CATEGORY 类别:能够在上级(父)activity 中运行。 "android.intent.category.EMBED"
String EMERGENCY_DIAL_ACTION 动作:拨打紧急电话号码。 "android.intent.action.EMERGENCY_DIAL"
int FORWARD_RESULT_LAUNCH 启动标记:如果这个标记被设置,而且被一个已经存在的 activity 用来启动新的 activity,已有 activity 的回复目标 (reply target) 会被转移给新的 activity。 16 0x00000010
String FOTA_CANCEL_ACTION 广播:取消所有被挂起的 (pending) 更新下载。 "android.server.checkin.FOTA_CANCEL"
String FOTA_INSTALL_ACTION 广播:更新已经被确认,马上就要开始安装。 "android.server.checkin.FOTA_INSTALL"
String FOTA_READY_ACTION 广播:更新已经被下载,可以开始安装。 "android.server.checkin.FOTA_READY"
String FOTA_RESTART_ACTION 广播:恢复已经停止的更新下载。 "android.server.checkin.FOTA_RESTART"
String FOTA_UPDATE_ACTION 广播:通过 OTA 下载并安装操作系统更新。 "android.server.checkin.FOTA_UPDATE"
String FRAMEWORK_INSTRUMENTATION_TEST_CATEGORY 类别:To be used as code under test for framework instrumentation tests. "android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"
String GADGET_CATEGORY 类别:这个 activity 可以被嵌入宿主 activity (activity that is hosting gadgets)。 "android.intent.category.GADGET"
String GET_CONTENT_ACTION 动作:让用户选择数据并返回。 "android.intent.action.GET_CONTENT"
String HOME_CATEGORY 类别:主屏幕 (activity),设备启动后显示的第一个 activity。 "android.intent.category.HOME"
String INSERT_ACTION 动作:在容器中插入一个空项 (item)。 "android.intent.action.INSERT"
String INTENT_EXTRA 附加数据:和 PICK_ACTIVITY_ACTION 一起使用时,说明用户选择的用来显示的 activity;和 ADD_SHORTCUT_ACTION 一起使用的时候,描述要添加的快捷方式。 "android.intent.extra.INTENT"
String LABEL_EXTRA 附加数据:大写字母开头的字符标签,和 ADD_SHORTCUT_ACTION 一起使用。 "android.intent.extra.LABEL"
String LAUNCHER_CATEGORY 类别:Activity 应该被显示在顶级的 launcher 中。 "android.intent.category.LAUNCHER"
String LOGIN_ACTION 动作:获取登录凭证。 "android.intent.action.LOGIN"
String MAIN_ACTION 动作:作为主入口点启动,不需要数据。 "android.intent.action.MAIN"
String MEDIABUTTON_ACTION 广播:用户按下了“Media Button”。 "android.intent.action.MEDIABUTTON"
String MEDIA_BAD_REMOVAL_ACTION 广播:扩展介质(扩展卡)已经从 SD 卡插槽拔出,但是挂载点 (mount point) 还没解除 (unmount)。 "android.intent.action.MEDIA_BAD_REMOVAL"
String MEDIA_EJECT_ACTION 广播:用户想要移除扩展介质(拔掉扩展卡)。 "android.intent.action.MEDIA_EJECT"
String MEDIA_MOUNTED_ACTION 广播:扩展介质被插入,而且已经被挂载。 "android.intent.action.MEDIA_MOUNTED"
String MEDIA_REMOVED_ACTION 广播:扩展介质被移除。 "android.intent.action.MEDIA_REMOVED"
String MEDIA_SCANNER_FINISHED_ACTION 广播:已经扫描完介质的一个目录。 "android.intent.action.MEDIA_SCANNER_FINISHED"
String MEDIA_SCANNER_STARTED_ACTION 广播:开始扫描介质的一个目录。 "android.intent.action.MEDIA_SCANNER_STARTED"
String MEDIA_SHARED_ACTION 广播:扩展介质的挂载被解除 (unmount),因为它已经作为 USB 大容量存储被共享。 "android.intent.action.MEDIA_SHARED"
String MEDIA_UNMOUNTED_ACTION 广播:扩展介质存在,但是还没有被挂载 (mount)。 "android.intent.action.MEDIA_UNMOUNTED"
String MESSAGE_WAITING_STATE_CHANGED_ACTION 广播:电话的消息等待(语音邮件)状态已经改变。 "android.intent.action.MWI"
int MULTIPLE_TASK_LAUNCH 启动标记:和 NEW_TASK_LAUNCH 联合使用,禁止将已有的任务改变为前景任务 (foreground)。 8 0x00000008
String NETWORK_TICKLE_RECEIVED_ACTION 广播:设备收到了新的网络 "tickle" 通知。 "android.intent.action.NETWORK_TICKLE_RECEIVED"那些党棍就是强盗
int NEW_TASK_LAUNCH 启动标记:设置以后,activity 将成为历史堆栈中的第一个新任务(栈顶)。 4 0x00000004
int NO_HISTORY_LAUNCH 启动标记:设置以后,新的 activity 不会被保存在历史堆栈中。 1 0x00000001
String PACKAGE_ADDED_ACTION 广播:设备上新安装了一个应用程序包。 "android.intent.action.PACKAGE_ADDED"
String PACKAGE_REMOVED_ACTION 广播:设备上删除了一个应用程序包。 "android.intent.action.PACKAGE_REMOVED"
String PHONE_STATE_CHANGED_ACTION 广播:电话状态已经改变。 "android.intent.action.PHONE_STATE"
String PICK_ACTION 动作:从数据中选择一个项目 (item),将被选中的项目返回。 "android.intent.action.PICK"
String PICK_ACTIVITY_ACTION 动作:选择一个 activity,返回被选择的 activity 的类(名)。 "android.intent.action.PICK_ACTIVITY"
String PREFERENCE_CATEGORY 类别:activity是一个设置面板 (preference panel)。 "android.intent.category.PREFERENCE"
String PROVIDER_CHANGED_ACTION 广播:更新将要(真正)被安装。 "android.intent.action.PROVIDER_CHANGED"
String PROVISIONING_CHECK_ACTION 广播:要求 polling of provisioning service 下载最新的设置。 "android.intent.action.PROVISIONING_CHECK"
String RUN_ACTION 动作:运行数据(指定的应用),无论它(应用)是什么。 "android.intent.action.RUN"
String SAMPLE_CODE_CATEGORY 类别:To be used as an sample code example (not part of the normal user experience). "android.intent.category.SAMPLE_CODE"
String SCREEN_OFF_ACTION 广播:屏幕被关闭。 "android.intent.action.SCREEN_OFF"
String SCREEN_ON_ACTION 广播:屏幕已经被打开。 "android.intent.action.SCREEN_ON"
String SELECTED_ALTERNATIVE_CATEGORY 类别:对于被用户选中的数据,activity 是它的一个可选操作。 "android.intent.category.SELECTED_ALTERNATIVE"
String SENDTO_ACTION 动作:向 data 指定的接收者发送一个消息。 "android.intent.action.SENDTO"
String SERVICE_STATE_CHANGED_ACTION 广播:电话服务的状态已经改变。 "android.intent.action.SERVICE_STATE"
String SETTINGS_ACTION 动作:显示系统设置。输入:无。 "android.intent.action.SETTINGS"
String SIGNAL_STRENGTH_CHANGED_ACTION 广播:电话的信号强度已经改变。 "android.intent.action.SIG_STR"
int SINGLE_TOP_LAUNCH 启动标记:设置以后,如果 activity 已经启动,而且位于历史堆栈的顶端,将不再启动(不重新启动) activity。 2 0x00000002
String STATISTICS_REPORT_ACTION 广播:要求 receivers 报告自己的统计信息。 "android.intent.action.STATISTICS_REPORT"
String STATISTICS_STATE_CHANGED_ACTION 广播:统计信息服务的状态已经改变。 "android.intent.action.STATISTICS_STATE_CHANGED"
String SYNC_ACTION 动作:执行数据同步。 "android.intent.action.SYNC"
String TAB_CATEGORY 类别:这个 activity 应该在 TabActivity 中作为一个 tab 使用。 "android.intent.category.TAB"
String TEMPLATE_EXTRA 附加数据:新记录的初始化模板。 "android.intent.extra.TEMPLATE"
String TEST_CATEGORY 类别:作为测试目的使用,不是正常的用户体验的一部分。 "android.intent.category.TEST"
String TIMEZONE_CHANGED_ACTION 广播:时区已经改变。 "android.intent.action.TIMEZONE_CHANGED"
String TIME_CHANGED_ACTION 广播:时间已经改变(重新设置)。 "android.intent.action.TIME_SET"
String TIME_TICK_ACTION 广播:当前时间已经变化(正常的时间流逝)。 "android.intent.action.TIME_TICK"
String UMS_CONNECTED_ACTION 广播:设备进入 USB 大容量存储模式。 "android.intent.action.UMS_CONNECTED"
String UMS_DISCONNECTED_ACTION 广播:设备从 USB 大容量存储模式退出。 "android.intent.action.UMS_DISCONNECTED"
String UNIT_TEST_CATEGORY 类别:应该被用作单元测试(通过 test harness 运行)。 "android.intent.category.UNIT_TEST"
String VIEW_ACTION 动作:向用户显示数据。 "android.intent.action.VIEW"
String WALLPAPER_CATEGORY 类别:这个 activity 能过为设备设置墙纸。 "android.intent.category.WALLPAPER"
String WALLPAPER_CHANGED_ACTION 广播:系统的墙纸已经改变。 "android.intent.action.WALLPAPER_CHANGED"
String WALLPAPER_SETTINGS_ACTION 动作:显示选择墙纸的设置界面。输入:无。 "android.intent.action.WALLPAPER_SETTINGS"
String WEB_SEARCH_ACTION 动作:执行 web 搜索。 "android.intent.action.WEB_SEARCH"
String XMPP_CONNECTED_ACTION 广播:XMPP 连接已经被建立。 "android.intent.action.XMPP_CONNECTED"
String XMPP_DISCONNECTED_ACTION 广播:XMPP 连接已经被断开。 "android.intent.action.XMP"

作者:HongEnIT 发表于2016/11/24 18:27:04 原文链接
阅读:43 评论:0 查看评论

React Native 之 Natigator与NatigatorIOS使用

$
0
0

前言

  • 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML快速入门(一) 学习

  • 本人接触 React Native 时间并不是特别长,所以对其中的内容和性质了解可能会有所偏差,在学习中如果有错会及时修改内容,也欢迎万能的朋友们批评指出,谢谢

  • 文章第一版出自简书,如果出现图片或页面显示问题,烦请转至 简书 查看 也希望喜欢的朋友可以点赞,谢谢


  • 开发中,几乎所有的APP中或多或少都会涉及到多个界面间的切换,在React Native中有两个组件负责实现这样的效果 —— Navigator 和 NavigatorIOS

  • Navigator可以在iOS和Android同时使用,而NavigatorIOS则是包装了UIKit库的导航功能,使用户可以使用左划功能来返回到上一界面


  • 官方文档中是这样解释的:使用导航器可以让你在应用的不同场景(页面)间进行切换。导航器通过路由对象来分辨不同的场景。利用renderScene方法,导航栏可以根据指定的路由来渲染场景

  • 可以通过configureScene属性获取指定路由对象的配置信息,从而改变场景的动画或者手势。查看Navigator.SceneConfigs来获取默认的动画和更多的场景配置选项

  • configureScene:可选的函数,用来配置场景动画和手势。会带有两个参数调用(一个是当前的路由,一个是当前的路由栈)然后它会返回一个场景配置对象

    • Navigator.SceneConfigs.PushFromRight(默认)


        (route, routeStack) => Navigator.SceneConfigs.FloatFromRight
    

    效果:
    PushFromRight.gif

    • Navigator.SceneConfigs.FloatFromLeft


        (route, routeStack) => Navigator.SceneConfigs.FloatFromLeft
    

    效果:
    FloatFromLeft.gif

    • Navigator.SceneConfigs.FloatFromBottom


        (route, routeStack) => Navigator.SceneConfigs.FloatFromBottom
    

    效果:
    FloatFromBottom.gif

    • Navigator.SceneConfigs.FloatFromBottomAndroid


        (route, routeStack) => Navigator.SceneConfigs.FloatFromBottomAndroid
    

    效果:
    FloatFromBottomAndroid.gif

    • Navigator.SceneConfigs.FadeAndroid


        (route, routeStack) => Navigator.SceneConfigs.FadeAndroid
    

    效果:
    FadeAndroid.gif

    • Navigator.SceneConfigs.HorizontalSwipeJump


        (route, routeStack) => Navigator.SceneConfigs.HorizontalSwipeJump
    

    效果:
    HorizontalSwipeJump.gif

    • Navigator.SceneConfigs.HorizontalSwipeJumpFromRight


        (route, routeStack) => Navigator.SceneConfigs.HorizontalSwipeJumpFromRight
    

    效果:
    HorizontalSwipeJumpFromRight.gif

    • Navigator.SceneConfigs.VerticalUpSwipeJump


        (route, routeStack) => Navigator.SceneConfigs.VerticalUpSwipeJump
    

    效果:
    VerticalUpSwipeJump.gif

    • Navigator.SceneConfigs.VerticalDownSwipeJump


        (route, routeStack) => Navigator.SceneConfigs.VerticalDownSwipeJump
    

    效果:
    VerticalDownSwipeJump.gif

  • initialRoute:定义启动时加载的路由。路由是导航栏用来识别渲染场景的一个对象。 initialRoute 必须是 initialRouteStack(路由栈) 中的一个路由。initialRoute 默认为 initialRouteStack 中最后一项

  • initialRouteStack:提供一个路由集合用来初始化。如果没有设置初始路由的话则必须设置该属性。如果没有提供该属性,它将被默认设置成一个只含有 initialRoute 的数组

  • naviagtionBar:可选参数,提供一个在场景切换的时候保持的导航栏

  • navigator:可选参数,提供从父导航器获得的导航器对象

  • onDidFocus:每当导航切换完成或初始化之后,调用此回调,参数为新场景的路由

  • onWillFocus:会在导航切换之前调用,参数为目标路由器

  • renderScene:必要参数,用来渲染指定路由的场景。调用的参数是路由和导航器

        (route, navigator) => <MySceneComponent title={route.title} navigator={navigator} />
    
  • sceneStyle:将会应用在每个场景的容器上的样式


  • 如果你得到一个 navigator对象 的引用,则可以调用许多方法来进行导航
    • getCurrentRoutes():获取当前栈里的路由,也就是push进来,没有pop掉的那些
    • jumpBack():跳回之前的路由,当前前提是保留现在的,还可以再跳回来,会给你保留原样
    • jumpForward():上一个方法不是盗用之前的路由,用这个就可以跳回来了
    • push(route):跳转到新场景,并且将场景入栈,你可以稍后跳转过去
    • pop():跳转回去并且卸载掉当前场景
    • replace(route):用一个新的路由替换掉当前场景
    • replaceAtIndex(route, index):替换掉指定序列的路由场景
    • replacePrevious(route):替换掉之前的场景
    • resetTO(route):跳转到新场景,并且重置整个路由栈
    • immediatelyResetRouteStack(routeStack):用新的路由数组来重置路由栈
    • popToRoute(route):pop到路由指定的场景,在整个路由栈中,处于指定场景之后的场景将会被卸载
    • popToTop():pop到栈中的第一个场景,卸载掉所有的其它场景

  • 这边我们先来完成一个最基本的导航控制器,然后慢慢深入,做一个完整的导航控制器
  • 首先,我们先创建2个组件(home、Temp)并初始化组件,以供使用

    • home组件代码


        import React, { Component } from 'react';
        import {
            AppRegistry,
            StyleSheet,
            Text,
            View
        } from 'react-native';
    
        var Home = React.createClass( {
            render() {
                return (
                    <View style={styles.container}>
                        <Text>点击跳转</Text>
                    </View>
                );
            }
        });
    
        var styles = StyleSheet.create({
            container: {
                backgroundColor:'yellow',
                flex:1,
                justifyContent:'center',
                alignItems:'center'
            },
        });
    
        module.exports = Home;
    
    • temp组件代码


        import React, { Component } from 'react';
        import {
            AppRegistry,
            StyleSheet,
            Text,
            View
        } from 'react-native';
    
        var Temp = React.createClass( {
            render() {
                return (
                    <View style={styles.container}>
                        <Text>点击返回</Text>
                    </View>
                );
            }
        });
    
        var styles = StyleSheet.create({
            container: {
                backgroundColor:'yellow',
                flex:1,
                justifyContent:'center',
                alignItems:'center'
            },
        });
    
        module.exports = Temp;
    
  • 实例化 Navigator 需要2个必要的属性 —— initialRoute 和 renderSence,它们的作用分别是告诉导航器需要渲染的场景、根据路由描述渲染出来

        <Navigator
            style={{flex: 1}}       // 布局
            initialRoute={{
                name:'Home',    // 名称
                component:Home  // 要跳转的板块
            }}
    
            renderScene={(route, navigator) => {    // 将板块生成具体的组件
                    let Component = route.component;    // 获取路由内的板块
                    return <Component {...route.params} navigator={navigator} />    // 根据板块生成具体组件
            }}
        />
    
  • 实际上这样我们的导航器就已经创建完毕了,但是从界面上我们看不到导航栏,但是已经具备导航功能,我们分别进到 hometemp 文件中修改代码,看看是否真的已经实现导航功能

    • home代码


        // 引入外部文件
        var Temp = require('./Temp');
    
        var Home = React.createClass( {
            render() {
                return (
                    <View style={styles.container}>
                        <TouchableOpacity
                            onPress={() => {this.props.navigator.push({
                                component:Temp
                            })}}
                        >
                            <Text>点击跳转</Text>
                        </TouchableOpacity>
                    </View>
                );
            }
        });
    
    • temp代码


        var Temp = React.createClass( {
            render() {
                return (
                    <View style={styles.container}>
                        <TouchableOpacity
                            onPress={() => {this.props.navigator.pop()}}
                        >
                            <Text>临时页面</Text>
                        </TouchableOpacity>
                    </View>
                );
            }
        });
    

    效果:
    navigator基本效果.gif

  • 在这里,我们还可以为导航器设置转场动画,以满足我们开发需求,官方已经提供了几种常用的转场动画,这边我们就取其中一种作示例,其它的效果可以参考上面的案例

        <Navigator
            initialRoute={{
                name:'Home',    // 名称
                component:Home  // 要跳转的板块
            }}
    
            configureScene={(route) => {    // 跳转动画
                return Navigator.SceneConfigs.FloatFromBottom;
            }}
    
            style={{flex: 1}}
            renderScene={(route, navigator) => {    // 将板块生成具体的组件
                    let Component = route.component;    // 获取路由内的板块
                    return <Component {...route.params} navigator={navigator} />    // 根据板块生成具体组件
            }}
        />
    

    效果:
    navigator转场动画.gif

  • 为了让导航栏更加人性化,我们可以自己定制导航栏的样式,定制方式和我们自定义界面一样,只不过将按钮的响应改为导航栏对应的功能即可,这边就以最基本的导航栏样式为例

    • 视图部分


        var Home = React.createClass( {
            render() {
                return (
                    <View style={styles.container}>
                        {/* 实例化导航栏 */}
                        {this.setupNavBar()}
                    </View>
                );
            },
    
            setupNavBar(){
                return(
                    <View style={styles.navBarStyle}>
                        {/* 左边按钮 */}
                        <TouchableOpacity
                            onPress={() => {this.props.navigator.push({
                                component:Temp
                            })}}
                        >
                            <Text style={styles.leftButtonTitleStyle}>按钮</Text>
                        </TouchableOpacity>
    
                        {/* 中间标题 */}
                        <Text style={styles.navBarTitleStyle}>标题</Text>
    
                        {/* 右边按钮 */}
                        <TouchableOpacity>
                            <Text style={styles.rightButtonTitleStyle}>按钮</Text>
                        </TouchableOpacity>
                    </View>
                )
            }
        });
    
    • 样式部分


        var styles = StyleSheet.create({
            container: {
                backgroundColor:'yellow',
                flex:1
            },
    
            navBarStyle: {
                // 尺寸
                width:width,
                // 当前系统为iOS时,导航栏高度为64
                height:Platform.OS === 'ios' ? 64 : 44,
                // 背景颜色
                backgroundColor:'rgba(255, 255, 255, 0.9)',
                // 底部分隔线
                borderBottomWidth:0.5,
                borderBottomColor:'gray',
                // 主轴方向
                flexDirection:'row',
                // 对齐方式
                alignItems:'center',
                justifyContent:'space-between',
                // 当前系统为iOS时,下次移动15
                paddingTop:Platform.OS === 'ios' ? 15 : 0
            },
    
            leftButtonTitleStyle: {
                // 字体大小
                fontSize:15,
                // 字体颜色
                color:'blue',
                // 内边距
                paddingLeft:8
            },
    
            navBarTitleStyle: {
                // 字体大小
                fontSize:17,
                // 字体颜色
                color:'black'
            },
    
            rightButtonTitleStyle: {
                // 字体大小
                fontSize:15,
                // 字体颜色
                color:'blue',
                // 内边距
                paddingRight:8
            }
        });
    

    iOS运行效果:
    iOS运行效果.gif

Android运行效果:
android运行效果.gif


  • barTintColor:导航条的背景颜色

        barTintColor='red'    // 导航栏背景颜色
    

    效果:
    导航栏背景色

  • initialRoute( {component: function, title: string, passProps: object, backButtonIcon: Image.propTypes.source, backButtonTitle: string, leftButtonIcon: Image.propTypes.source, leftButtonTitle: string, onLeftButtonPress: function, rightButtonIcon: Image.propTypes.source, rightButtonTitle: string, onRightButtonPress: function, wrapperStyle: [object Object]} ): 使用“路由”对象来包含要渲染的子视图、它们的属性、已经导航条配置。“push”和任何其他的导航函数的参数都是这样的路由对象(下面实例模块会详细讲解)

  • itemWrapperStyle:导航器中的组件的默认属性。一个常见的用途是设置所有页面的背景颜色

  • navigationBarHidden:布尔值,决定导航栏是否隐藏

        navigationBarHidden={true}      // 隐藏导航栏
    

    效果:
    navigationBarHidden.gif

  • shadowHidden:布尔值,决定是否要隐藏1像素的阴影

        shadowHidden={true}     // 隐藏导航栏下面的阴影
    

    效果:
    隐藏阴影

  • tintColor:导航栏上按钮的颜色

        tintColor='orange'  // 按钮的颜色
    

    效果:
    导航栏按钮颜色

  • titleTextColor:导航器标题的文字颜色

        titleTextColor='green'  // 导航栏标题的文字颜色
    

    效果:
    导航栏标题颜色

  • translucent:布尔值,决定导航条是否半透明(注:当不半透明时页面会向下移动导航栏等高的距离,以防止内容被遮盖)

        translucent={false}     // 决定导航栏是否半透明(注:当不半透明时页面会向下移动导航栏等高的距离,以防止内容被遮盖)
    

    效果:
    导航栏不半透明效果

  • interactivePopGestureEnabled:决定是否启用滑动返回手势。不指定此属性时,手势会根据 navigationBar 的显隐情况决定是否启用(显示时启用手势,隐藏时禁用手势),指定此属性后,手势与 navigationBar 的显隐情况无关

        interactivePopGestureEnabled={false}    // 决定是否启用滑动返回手势
    

    效果:
    interactivePopGestureEnabled关闭.gif


  • push(route):导航器跳转到一个新的路由

  • pop():回到上一页

  • popN():回到N页之前。当 N=1 的时候,效果和 pop() 一样

  • replace(route):替换当前页的路由,并立即加载新路由的视图

  • replacePrevious(route):替换上一页的路由/视图

  • replacePreviousAndPop(route):替换上一页的路由/视图并且立即切换回上一页

  • resetTO(route):替换最顶级的路由并且回到它

  • replaceAtIndex:替换指定路由

  • popToRoute(route):一直回到某个指定的路由

  • popToTop():回到最顶层的路由


  • 先来看看怎么使用 NavigatorIOS,我们需要给他指定一个路由,这样它才能知道显示哪个页面
  • Navigator 一样 NavigatorIOS 需要有个根视图来完成初始化,所以我们需要先创建一个组件来描述这个界面,并将这个组件通过路由的形式告诉 NavigatorIOS,这样就可以将这个界面展示出来

    • 首先,创建一个 Home 组件,用来作为 NavigatorIOS 的根视图

      • 视图部分


          var Home = React.createClass( {
              render() {
                  return (
                      <View style={styles.container}>
                          <Text>点击跳转页面</Text>
                      </View>
                  );
              }
          }); 
      
      • 样式部分


          var styles = StyleSheet.create({
              container: {
                  // 背景颜色
                  backgroundColor:'yellow',
                  flex:1,
                  // 对齐方式
                  justifyContent:'center',
                  alignItems:'center'
              },
          });
      
    • 接着我们在 index.ios.js 内获得 Home 文件


        // 引用外部文件
        var Home = require('./home');
    
    • 然后我们实例化一个 NavigatorIOS 并设置路由


        var navigatorDemo = React.createClass( {
            render() {
                return (
                    <NavigatorIOS
                        initialRoute={{
                            component: Home,    // 要跳转的页面
                            title:'首页'    // 跳转页面导航栏标题
                        }}
                        style={{flex:1}}  // 此项不设置,创建的导航控制器只能看见导航条而看不到界面
                    />
                );
            }
        });
    

    效果:
    基本效果

  • 这样我们就完成了基本的导航控制器了,那么怎么进行跳转呢?其实也很简单,官方提供的方法内有多种供我们选择(具体参考上面的方法一栏)

    • 这边我们就来实现最简单的跳转和返回,我们使用 TouchableOpacityHome 中的 标签 拥有接收事件的能力,并且当点击的时候通过调用 props 来获取 navigator,并传递给他一个路由,使其知道跳转到哪个页面


        var Home = React.createClass( {
            render() {
                return (
                    <View style={styles.container}>
                        <TouchableOpacity
                            onPress={() => {this.props.navigator.push({
                                component:Temp,     // 需要跳转的页面
                                title:'跳转的界面'       // 跳转页面导航栏标题
                            })}}
                        >
                            <Text>点击跳转页面</Text>
                        </TouchableOpacity> 
                    </View>
                );
            }
        });
    

    效果:
    NavigatorIOS效果.gif

  • 这边顺便来看下导航栏左右两边的按钮怎么设置,并且响应点击事件

        initialRoute={{
           component: Home,    // 要跳转的页面
           title:'首页',   // 跳转页面导航栏标题
           leftButtonTitle:'左边',   // 实例化左边按钮
           onLeftButtonPress:() => {alert('左边')},  // 左边按钮点击事件
           rightButtonTitle:'右边',  // 实例化右边按钮
           onRightButtonPress:() => {alert('右边')}  // 右边按钮点击事件
            }}
    

    效果:
    NavigatorIOS左右按钮.gif

  • 当然图片设置的方式也是一样的,只需要调用 leftButtonIcon 和 ‘rightButtonIcon` 即可(和TabBarIOS一样,只支持本地图片)

补充


  • props:组件中的props是一种父级向子级传递数据的方式
    • this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点
    • 它里面包含了所有的属性,所以上面我们在别的文件中可以通过 this.props.navigator 的方式获取 navigator
  • state:state 是React中组件的一个对象.React把用户界面当做是状态机,想象它有不同的状态然后渲染这些状态,可以轻松让用户界面与数据保持一致。React中,更新组件的 state,会导致重新渲染用户界面(不要操作DOM).简单来说,就是用户界面会随着 state 变化而变化
    • 原理:常用的通知React数据变化的方法是调用 setState(data,callback).这个方法会合并data到 this.state,并重新渲染组件.渲染完成后,调用可选的。callback 回调.大部分情况不需要提供 callback,因为React会负责吧界面更新到最新状态
    • 哪些组件应该有 state
      • 大部分组件的工作应该是从 props 里取数据并渲染出来.但是,有时需要对用户输入,服务器请求或者时间变化等作出响应,这时才需要 state
      • 组件应该尽可能的无状态化,这样能隔离state,把它放到最合理的地方(Redux做的就是这个事情?),也能减少冗余并易于解释程序运作过程
      • 常用的模式就是创建多个只负责渲染数据的无状态(stateless)组件,在他们的上层创建一个有状态(stateful)组件并把它的状态通过props
      • 传给子级.有状态的组件封装了所有的用户交互逻辑,而这些无状态组件只负责声明式地渲染数据
    • 哪些应该作为 state
      • state 应该包括那些可能被组件的事件处理器改变并触发用户界面更新的数据.这中数据一般很小且能被JSON序列化.当创建一个状态化的组件的时候,应该保持数据的精简,然后存入 this.state。 在render()中在根据 state 来计算需要的其他数据.因为如果在state里添加冗余数据或计算所得数据,经常需要手动保持数据同步
    • 哪些不应该作为 state
      • this.state 应该仅包括能表示用户界面状态所需要的最少数据.因此,不应该包括:
        • 计算所得数据:
          • React组件:在render()里使用props和state来创建它
          • 基于props的重复数据:尽可能保持用props来做作为唯一的数据来源.把props保存到state中的有效的场景是需要知道它以前的值得时候,因为未来的props可能会变化
作者:yeshaojian 发表于2016/11/24 18:36:46 原文链接
阅读:32 评论:0 查看评论

Android Handler分析

$
0
0

Android Handler分析

标签(空格分隔): handler

最近面试总是被问到handler相关的东西,那就静下心来,仔细分析一下handler的源码,看一下内部到底是个什么鬼,也好出去装逼不是。

  • 首先看一下handler的构造方法,都有些什么鬼,我把源码的注释去掉了,想看的自己去源码里看
   private static final boolean FIND_POTENTIAL_LEAKS = false;

 /**
     * Default constructor associates this handler with the {@link Looper} for the
     * current thread.
     *
     * If this thread does not have a looper, this handler won't be able to receive messages
     * so an exception is thrown.
     */
    public Handler() {
        this(null, false);
    }


    public Handler(Callback callback) {
        this(callback, false);
    }


    public Handler(Looper looper) {
        this(looper, null, false);
    }

    public Handler(Looper looper, Callback callback) {
        this(looper, callback, false);
    }

    public Handler(boolean async) {
        this(null, async);
    }

/**
* @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for
each {@link Message} that is sent to it or {@link Runnable} that is posted to it.也就是说async=true的时候,handler sent或者post的message,均是异步的
*
*/
    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
//返回全路径的完整的类型,类似class.getName()用法                    
klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }


    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

主要看一下37行–55行:
我们看到FIND_POTENTIAL_LEAKS 默认是false,当FIND_POTENTIAL_LEAKS=true的时候,貌似并没有什么卵用,直接略过。继续向下看,
47行:

mLooper = Looper.myLooper()
 if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }

如果Looper=null,直接就抛出异常,这也就解释了为什么我们在子线程创建Handler之前,要调用一下Looper.prepare(),否则会报错。一个线程只能有一个Looper
60行:可以看到MessageQueue是Looper来维护的
再看10-17行:就知道我们在activity或者fragment中,实现handler有两种方式1,实现Callback接口,来处理Message.2,重写内部handleMessage(Message msg)来处理Message。

再来看下一段代码:

    public final Message obtainMessage()
    {
        return Message.obtain(this);
    }

    public final Message obtainMessage(int what)
    {
        return Message.obtain(this, what);
    }

    public final Message obtainMessage(int what, Object obj)
    {
        return Message.obtain(this, what, obj);
    }

    public final Message obtainMessage(int what, int arg1, int arg2)
    {
        return Message.obtain(this, what, arg1, arg2);
    }

    public final Message obtainMessage(int what, int arg1, int arg2, Object obj)
    {
        return Message.obtain(this, what, arg1, arg2, obj);
    }

    Message源码:

        public static Message obtain(Handler h, int what, 
            int arg1, int arg2, Object obj) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.arg1 = arg1;
        m.arg2 = arg2;
        m.obj = obj;

        return m;
    }

        /**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     消息队列中消息的复用
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

由以上源码可以看出:android api已经给我们提供了封装Message的方法

//实现Message的复用,
Message msg = handler.obtain();
Message msg = Message.obtain()

所以不要再直接

//不要这样使用
Message msg = new Message();

继续向下看源码:


    public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }


    public final boolean postAtTime(Runnable r, long uptimeMillis)
    {
        return sendMessageAtTime(getPostMessage(r), uptimeMillis);
    }


    public final boolean postAtTime(Runnable r, Object token, long uptimeMillis)
    {
        return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
    }


    public final boolean postDelayed(Runnable r, long delayMillis)
    {
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }


    public final boolean postAtFrontOfQueue(Runnable r)
    {
        return sendMessageAtFrontOfQueue(getPostMessage(r));
    }


除了最后一个方法外,其余的方法应该都很眼熟吧,使用也没什么难的
最后一个方法26-29行:
向实现了Runnable接口的对象发送一个消息。使得Runnable对象r能够在消息队列的下一个迭代中继续执行。
这个方法只能在非常特殊的情况下才有用—它很容易饿死在消息队列中,导致排序问题或者其他难以预料的负面影响,说实话,我也没用过

再来看一下sendMessage相关的几个方法:

  public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }


    public final boolean sendEmptyMessage(int what)
    {
        return sendEmptyMessageDelayed(what, 0);
    }


    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageDelayed(msg, delayMillis);
    }



    public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageAtTime(msg, uptimeMillis);
    }

    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }


    public final boolean sendMessageAtFrontOfQueue(Message msg) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, 0);
    }

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

从上往下看,到最后全部都交给了MessageQueue去处理了,queue.enqueueMessage(msg, uptimeMillis)这个方法应该就是入队了,看下源码

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

2-7行:是否已经在处理中和是否有处理它的Handler,
10-16行:检查是否在中止,如果是,那么message直接回收
22行-end:就是入栈操作了
在这其中作了一些减少同步操作的优化,即使当前消息队列已经处于 Blocked 状态,且队首是一个消息屏障(这里是通过 p.target ==null来判断队首是否是消息屏障),并且要插入的消息是所有异步消息中最早要处理的才会 needwake激活消息队列去获取下一个消息。

继续往下看:


 public final void removeCallbacks(Runnable r)
    {
        mQueue.removeMessages(this, r, null);
    }


    public final void removeCallbacks(Runnable r, Object token)
    {
        mQueue.removeMessages(this, r, token);
    }

    public final void removeMessages(int what) {
        mQueue.removeMessages(this, what, null);
    }


    public final void removeMessages(int what, Object object) {
        mQueue.removeMessages(this, what, object);
    }


    public final void removeCallbacksAndMessages(Object token) {
        mQueue.removeCallbacksAndMessages(this, token);
    }

这里Remove操作也是全部交给了MessageQueue去处理了.

再看一下Looper有什么鬼:


    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    final MessageQueue mQueue;
    final Thread mThread;

从代码看出,Looper主要就是维护了这三个东西,ThreadLocal,MessageQueue,Thread;

  public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

        private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

也就是说我们要想获得Looper必须先调用prepare(),这也解释了为毛在子线程需要首先调用Looper.prepare(),然后再调用Loop.myLooper()才能获得Looper;而且也能知道一个线程只能有一个Looper,依据就在这里。

至于为毛UI线程不需要调用Looper.prepare(),看下面的代码就知道了


    /**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
 public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

    /**
     * Returns the application's main looper, which lives in the main thread of the application.
     */
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

这下就清楚了,Android 系统已经为我们准备好了MainLooper,不需要我们再主动去调用Looper.prepare(),

剩下只有Looper的核心loop:

 /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // 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);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }

因此要想Looper开始工作还要调用Loop.loop(),进入消息循环
18行—从消息队列中取出消息
31行—handler分发消息dispatchMessage(msg);
48行–消息回收

那我们在回过头来看一下Handler.dispatchMessage(msg),看看其中是怎么处理的

 public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

        private static void handleCallback(Message message) {
        message.callback.run();
    }

       /**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(Message msg) {
    }

    public interface Callback {
        public boolean handleMessage(Message msg);
    }

先看1-12行:如果msg.callback!=null,直接执行handleCallback(msg)方法,然后跟踪过去,你发现直接调用了run(),这也说明了Handler.post(Runnable)其实是运行在Handler绑定的线程中的,并不是子线程
再往下:你会发现这里有两种handleMessage的方案,一种实现实现Handler.Callback 调用callback.handleMessage(),另一种直接handleMessage();

下面轮到Message了,看一下Message内部是个什么样子,看看有没有我们想象的那么神秘:

    public int what;
    public int arg1; 
    public int arg2;
    public Object obj;

    /**
     * Optional Messenger where replies to this message can be sent.  The
     * semantics of exactly how this is used are up to the sender and
     * receiver.
     */
    public Messenger replyTo;

    public int sendingUid = -1;
    static final int FLAG_IN_USE = 1 << 0;

   static final int FLAG_ASYNCHRONOUS = 1 << 1;

   static final int FLAGS_TO_CLEAR_ON_COPY_FROM = FLAG_IN_USE;

    int flags;

   long when;

    Bundle data;

    Handler target;

   Runnable callback;

    Message next;

    private static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;

    private static final int MAX_POOL_SIZE = 50;

    private static boolean gCheckRecycle = true;


    /** @hide */
    public static void updateCheckRecycle(int targetSdkVersion) {
        if (targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
            gCheckRecycle = false;
        }
    }

    public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }


    /*package*/ boolean isInUse() {
        return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
    }

    /*package*/ void markInUse() {
        flags |= FLAG_IN_USE;
    }

看数据结构,Message就是一个链式结构的设计,里面只有一个核心的recycleUnchecked()方法,实现消息的重置和复用

说了这么多,有点晕乎乎的感觉,那就用图形化来说明一下:

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

再来个例子,主线程向子线程发送消息,

  public class MyThread extends Thread implements Handler.Callback{

    public Handler mHandler;
    public Handler secondHandler;

    @Override
    public void run(){
        Looper.prepare();
        mHandler = new Handler(this);
        secondHandler = new Handler(){
            @Override
            public void  handleMessage(Message msg) {
                System.out.println("msg----"+msg.toString());

            }
        };
        Looper.loop();
    }

    @Override
    public boolean handleMessage(Message msg) {
        System.out.println("msg----"+msg.toString());
        //记得要关闭 Looper.myLooper().quit();
        return false;
    }

}


public class MainActivity extends Activity {

    MyThread thread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        thread = new MyThread();
        thread.start();

    }

    @Override
    protected void onResume(){
        super.onResume();
        thread.mHandler.sendEmptyMessage(3);
        thread.secondHandler.sendEmptyMessage(55);
    }
}

Handler相关内容完美结束,尼玛再也不怕出去谁问handler机制了,如果有错误的地方,希望大大们提醒我一下。

作者:baidu_17508977 发表于2016/11/25 18:05:47 原文链接
阅读:57 评论:0 查看评论

多种方式实现Android定时任务,哪一款是你的FEEL?

$
0
0

前言

项目中总是会因为各种需求添加各种定时任务,所以就打算小结一下Android中如何实现定时任务,下面的解决方案的案例大部分都已在实际项目中实践,特此列出供需要的朋友参考,如果有什么使用不当或者存在什么问题,欢迎留言指出!直接上干货!

解决方案

普通线程sleep的方式实现定时任务

创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果,这是最常见的,可以快速简单地实现。但是这是java中的实现方式,不建议使用

    public class ThreadTask {  
    public static void main(String[] args) {  
        final long timeInterval = 1000;  
        Runnable runnable = new Runnable() {  
            public void run() {  
                while (true) {  
                    System.out.println("execute task");  
                    try {  
                        Thread.sleep(timeInterval);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            }  
        };  
        Thread thread = new Thread(runnable);  
        thread.start();  
    }  
} 

Timer实现定时任务

和普通线程+sleep(long)+Handler的方式比,优势在于

  • 可以控制TimerTask的启动和取消
  • 第一次执行任务时可以指定delay的时间。

在实现时,Timer类调度任务,TimerTask则是通过在run()方法里实现具体任务(然后通过Handler与线程协同工作,接收线程的消息来更新主UI线程的内容)。

  • Timer实例可以调度多任务,它是线程安全的。当Timer的构造器被调用时,它创建了一个线程,这个线程可以用来调度任务。
   /**
    * start Timer
    */
    protected synchronized void startPlayerTimer() {
        stopPlayerTimer();
        if (playTimer == null) {
            playTimer = new PlayerTimer();
            Timer m_musictask = new Timer();
            m_musictask.schedule(playTimer, 5000, 5000);
        }
    }

    /**
     * stop Timer
     */
    protected synchronized void stopPlayerTimer() {
        try {
            if (playTimer != null) {
                playTimer.cancel();
                playTimer = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class PlayerTimer extends TimerTask {

        public PlayerTimer() {
        }

        public void run() {
            //execute task
        }
    }

然而Timer是存在一些缺陷的

  • Timer在执行定时任务时只会创建一个线程,所以如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会发生一些缺陷
  • 如果Timer调度的某个TimerTask抛出异常,Timer会停止所有任务的运行
  • Timer执行周期任务时依赖系统时间,修改系统时间容易导致任务被挂起(如果当前时间小于执行时间)

注意:

  • Android中的Timer和java中的Timer还是有区别的,但是大体调用方式差不多
  • Android中需要根据页面的生命周期和显隐来控制Timer的启动和取消
    java中Timer:

    这里写图片描述

    Android中的Timer:

    这里写图片描述

ScheduledExecutorService实现定时任务

ScheduledExecutorService是从JDK1.5做为并发工具类被引进的,存在于java.util.concurrent,这是最理想的定时任务实现方式。
相比于上面两个方法,它有以下好处:

  • 相比于Timer的单线程,它是通过线程池的方式来执行任务的,所以可以支持多个任务并发执行 ,而且弥补了上面所说的Timer的缺陷
  • 可以很灵活的去设定第一次执行任务delay时间
  • 提供了良好的约定,以便设定执行的时间间隔

简例:

public class ScheduledExecutorServiceTask 
{  
    public static void main(String[] args) 
    {  
        final TimerTask task = new TimerTask()  
        {  
            @Override  
            public void run()  
            {  
               //execute task  
            }  
        };   
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);  
        pool.scheduleAtFixedRate(task, 0 , 1000, TimeUnit.MILLISECONDS);  

    }  
}

实际案例背景:

  • 应用中多个页面涉及比赛信息展示(包括比赛状态,比赛结果),因此需要实时更新这些数据

思路分析:

  • 多页面多接口刷新
    • 就是每个需要刷新的页面基于自身需求基于特定接口定时刷新
    • 每个页面要维护一个定时器,然后基于页面的生命周期和显隐进行定时器的开启和关闭(保证资源合理释放)
    • 而且这里的刷新涉及到是刷新局部数据还是整体数据,刷新整体数据效率会比较低,显得非常笨重
  • 多页面单接口刷新
    • 接口给出一组需要实时进行刷新的比赛数据信息,客户端基于id进行统一过滤匹配
    • 通过单例封装统一定时刷新回调接口(注意内存泄露的问题,页面销毁时关闭ScheduledExecutorService )
    • 需要刷新的item统一调用,入口唯一,方便维护管理,扩展性好
    • 局部刷新,效率高
public class PollingStateMachine implements INetCallback {

    private static volatile PollingStateMachine instance = null;
    private ScheduledExecutorService pool;
    public static final int TYPE_MATCH = 1;

    private Map matchMap = new HashMap<>();
    private List<WeakReference<View>> list = new ArrayList<>();
    private Handler handler;

    // private constructor suppresses
    private PollingStateMachine() {
        defineHandler();
        pool = Executors.newSingleThreadScheduledExecutor();
        pool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                doTasks();
            }
        }, 0, 10, TimeUnit.SECONDS);
    }

    private void doTasks() {
        ThreadPoolUtils.execute(new PollRunnable(this));
    }

    public static PollingStateMachine getInstance() {
        // if already inited, no need to get lock everytime
        if (instance == null) {
            synchronized (PollingStateMachine.class) {
                if (instance == null) {
                    instance = new PollingStateMachine();
                }
            }
        }
        return instance;
    }
    public <VIEW extends View> void subscibeMatch(VIEW view, OnViewRefreshStatus onViewRefreshStatus) {
        subscibe(TYPE_MATCH,view,onViewRefreshStatus);
    }

    private <VIEW extends View> void subscibe(int type, VIEW view, OnViewRefreshStatus onViewRefreshStatus) {
        view.setTag(onViewRefreshStatus);
        if (type == TYPE_MATCH) {
            onViewRefreshStatus.update(view, matchMap);
        } 
        for (WeakReference<View> viewSoftReference : list) {
            View textView = viewSoftReference.get();
            if (textView == view) {
                return;
            }
        }
        WeakReference<View> viewSoftReference = new WeakReference<View>(view);
        list.add(viewSoftReference);
    }

    public void updateView(final int type) {
        Iterator<WeakReference<View>> iterator = list.iterator();
        while (iterator.hasNext()) {
            WeakReference<View> next = iterator.next();
            final View view = next.get();
            if (view == null) {
                iterator.remove();
                continue;
            }
            Object tag = view.getTag();
            if (tag == null || !(tag instanceof OnViewRefreshStatus)) {
                continue;
            }
            final OnViewRefreshStatus onViewRefreshStatus = (OnViewRefreshStatus) tag;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (type == TYPE_MATCH) {
                        onViewRefreshStatus.update(view, matchMap);
                    } 
                }
            });

        }
    }

    public void clear() {
        pool.shutdown();
        instance = null;
    }

    private Handler defineHandler() {
        if (handler == null && Looper.myLooper() == Looper.getMainLooper()) {
            handler = new Handler();
        }
        return handler;
    }


    @Override
    public void onNetCallback(int type, Map msg) {
        if (type == TYPE_MATCH) {
            matchMap=msg;
        } 
        updateView(type);
    }
}

需要刷新的item调用

PollingStateMachine.getInstance().subscibeMatch(tvScore, new OnViewRefreshStatus<ScoreItem, TextView>(matchViewItem.getMatchID()) {
            @Override
            public void OnViewRefreshStatus(TextView view, ScoreItem scoreItem) {
                //刷新处理
                }
        });

网络数据回调接口

public interface INetCallback {

    void onNetCallback(int type,Map msg);

}

刷新回调接口:

public abstract class OnViewRefreshStatus<VALUE, VIEW extends View> {
    private static final String TAG = OnViewRefreshStatus.class.getSimpleName();
    private long key;

    public OnViewRefreshStatus(long key) {
        this.key = key;
    }

    public long getKey() {
        return key;
    }

    public void update(final VIEW view, Map<Long, VALUE> map) {
        final VALUE value = map.get(key);

        if (value == null) {
            return;
        }
        OnViewRefreshStatus(view, value);

    }


    public abstract void OnViewRefreshStatus(VIEW view, VALUE value);

}

Handler实现定时任务

通过Handler延迟发送消息的形式实现定时任务。

  • 这里通过一个定时发送socket心跳包的案例来介绍如何通过Handler完成定时任务

案例背景

  • 由于移动设备的网络的复杂性,经常会出现网络断开,如果没有心跳包的检测, 客户端只会在需要发送数据的时候才知道自己已经断线,会延误,甚至丢失服务器发送过来的数据。 所以需要提供心跳检测

下面的代码只是用来说明大体实现流程

  • WebSocket初始化成功后,就准备发送心跳包
  • 每隔30s发送一次心跳
  • 创建的时候初始化Handler,销毁的时候移除Handler消息队列
private static final long HEART_BEAT_RATE = 30 * 1000;//目前心跳检测频率为30s
private Handler mHandler;
private Runnable heartBeatRunnable = new Runnable() {
        @Override
        public void run() {
            // excute task
            mHandler.postDelayed(this, HEART_BEAT_RATE);
        }
    };
public void onCreate() {
     //初始化Handler
    }

//初始化成功后,就准备发送心跳包
public void onConnected() {
        mHandler.removeCallbacks(heartBeatRunnable);
        mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);
    }

public void onDestroy() {
        mHandler.removeCallbacks(heartBeatRunnable)
    }

注意:这里开启的runnable会在这个handler所依附线程中运行,如果handler是在UI线程中创建的,那么postDelayed的runnable自然也依附在主线程中。

AlarmManager实现精确定时操作

我们使用Timer或者handler的时候会发现,delay时间并没有那么准。如果我们需要一个严格准时的定时操作,那么就要用到AlarmManager,AlarmManager对象配合Intent使用,可以定时的开启一个Activity,发送一个BroadCast,或者开启一个Service.

案例背景

  • 在比赛开始前半个小时本地定时推送关注比赛的提醒信息

AndroidManifest.xml中声明一个全局广播接收器

  <receiver
            android:name=".receiver.AlarmReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="${applicationId}.BROADCAST_ALARM" />
            </intent-filter>
        </receiver>

接收到action为IntentConst.Action.matchRemind的广播就展示比赛
提醒的Notification

public class AlarmReceiver extends BroadcastReceiver {
    private static final String TAG = "AlarmReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null) {
            return;
        }
        String action = intent.getAction();
        if (TextUtils.equals(action, IntentConst.Action.matchRemind)) {
            //通知比赛开始
            long matchID = intent.getLongExtra(Net.Param.ID, 0);
            showNotificationRemindMe(context, matchID);
        }

    }
}

AlarmUtil提供设定闹铃和取消闹铃的两个方法

public class AlarmUtil {
    private static final String TAG = "AlarmUtil";

    public static void controlAlarm(Context context, long startTime,long matchId, Intent nextIntent) {
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, (int) matchId, nextIntent,
                PendingIntent.FLAG_ONE_SHOT);
        alarmManager.set(AlarmManager.RTC_WAKEUP, startTime, pendingIntent);
    }

    public static void cancelAlarm(Context context, String action,long matchId) {
        Intent intent = new Intent(action);
        PendingIntent sender = PendingIntent.getBroadcast(
                context, (int) matchId, intent, 0);

        // And cancel the alarm.
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(sender);
    }


}

关注比赛的时候,会设置intent的action为IntentConst.Action.matchRemind,会根据比赛的开始时间提前半小时设定闹铃时间,取消关注比赛同时取消闹铃

   if (isSetAlarm) {
            long start_tm = matchInfo.getStart_tm();
            long warmTime = start_tm - 30 * 60;

            Intent intent = new Intent(IntentConst.Action.matchRemind);
            intent.putExtra(Net.Param.ID, matchInfo.getId());
            AlarmUtil.controlAlarm(context, warmTime * 1000,matchInfo.getId(), intent);

            Gson gson = new Gson();
            String json = gson.toJson(matchInfo);
            SportDao.getInstance(context).insertMatchFollow(eventID, matchInfo.getId(), json, matchInfo.getType());
        } else {
            AlarmUtil.cancelAlarm(context, IntentConst.Action.matchRemind,matchInfo.getId());
            SportDao.getInstance(context).deleteMatchFollowByID(matchInfo.getId());
        }
作者:s003603u 发表于2016/11/25 18:08:18 原文链接
阅读:45 评论:0 查看评论

【Android安全】自带加密光环的SharedPreference

$
0
0

项目地址:https://github.com/afinal/SecuritySharedPreference

前言

安全问题长久以来就是Android系统的一大弊病,很多人也因此舍弃Android选择了苹果,作为一个Android Developer,我们需要对用户的隐私负责,需要对用户的数据安全倾尽全力,想到这里,我就热血沸腾,仿佛自己化身正义的天使(我编不下去了。。。)。

概述

现在,我们回归正题,SharedPreference是我们比较常用的保存数据到本地的方式,我们习惯在SharedPreference中保存用户信息,用户偏好设置,以及记住用户名密码等等。但是,SharedPreference存在一些安全隐患,我们都知道,SharedPreference是以“键值对”的形式把数据保存在data/data/packageName/shared_prefs文件夹中的xml文件中。
在正常的情况下,我们没办法访问data/data目录,我们也没办法拿到xml中的文件。但是,我们root手机之后,通过命令行可以获得读写data/data目录的权限,我们SharedPreference中保存的数据也就很容易泄漏,造成不可挽回的损失。那么我们今天就从SharedPreference开刀,为APP的安全尽一份绵薄之力。

代码

SecuritySharedPreference.java

我们实现了SharedPreference接口和SharedPreference.Editor接口,重写了保存数据和取出数据的方法,我们在保存数据和取出数据的时候加了加解密层,这样可以保证我们在操作自定义的SharedPreference时候像调用原生的一样简单。

package com.domain.securitysharedpreference;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 自动加密SharedPreference
 * Created by Max on 2016/11/23.
 */

public class SecuritySharedPreference implements SharedPreferences {

    private SharedPreferences mSharedPreferences;
    private static final String TAG = SecuritySharedPreference.class.getName();
    private Context mContext;

    /**
     * constructor
     * @param context should be ApplicationContext not activity
     * @param name file name
     * @param mode context mode
     */
    public SecuritySharedPreference(Context context, String name, int mode){
        mContext = context;
        if (TextUtils.isEmpty(name)){
            mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        } else {
            mSharedPreferences =  context.getSharedPreferences(name, mode);
        }

    }

    @Override
    public Map<String, String> getAll() {
        final Map<String, ?> encryptMap = mSharedPreferences.getAll();
        final Map<String, String> decryptMap = new HashMap<>();
        for (Map.Entry<String, ?> entry : encryptMap.entrySet()){
            Object cipherText = entry.getValue();
            if (cipherText != null){
                decryptMap.put(entry.getKey(), entry.getValue().toString());
            }
        }
        return decryptMap;
    }

    /**
     * encrypt function
     * @return cipherText base64
     */
    private String encryptPreference(String plainText){
        return EncryptUtil.getInstance(mContext).encrypt(plainText);
    }

    /**
     * decrypt function
     * @return plainText
     */
    private String decryptPreference(String cipherText){
        return EncryptUtil.getInstance(mContext).decrypt(cipherText);
    }

    @Nullable
    @Override
    public String getString(String key, String defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        return encryptValue == null ? defValue : decryptPreference(encryptValue);
    }

    @Nullable
    @Override
    public Set<String> getStringSet(String key, Set<String> defValues) {
        final Set<String> encryptSet = mSharedPreferences.getStringSet(encryptPreference(key), null);
        if (encryptSet == null){
            return defValues;
        }
        final Set<String> decryptSet = new HashSet<>();
        for (String encryptValue : encryptSet){
            decryptSet.add(decryptPreference(encryptValue));
        }
        return decryptSet;
    }

    @Override
    public int getInt(String key, int defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Integer.parseInt(decryptPreference(encryptValue));
    }

    @Override
    public long getLong(String key, long defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Long.parseLong(decryptPreference(encryptValue));
    }

    @Override
    public float getFloat(String key, float defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Float.parseFloat(decryptPreference(encryptValue));
    }

    @Override
    public boolean getBoolean(String key, boolean defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Boolean.parseBoolean(decryptPreference(encryptValue));
    }

    @Override
    public boolean contains(String key) {
        return mSharedPreferences.contains(encryptPreference(key));
    }

    @Override
    public SecurityEditor edit() {
        return new SecurityEditor();
    }

    @Override
    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mSharedPreferences.registerOnSharedPreferenceChangeListener(listener);
    }

    @Override
    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
    }

    /**
     * 处理加密过渡
     */
    public void handleTransition(){
        Map<String, ?> oldMap = mSharedPreferences.getAll();
        Map<String, String> newMap = new HashMap<>();
        for (Map.Entry<String, ?> entry : oldMap.entrySet()){
            Log.i(TAG, "key:"+entry.getKey()+", value:"+ entry.getValue());
            newMap.put(encryptPreference(entry.getKey()), encryptPreference(entry.getValue().toString()));
        }
        Editor editor = mSharedPreferences.edit();
        editor.clear().commit();
        for (Map.Entry<String, String> entry : newMap.entrySet()){
            editor.putString(entry.getKey(), entry.getValue());
        }
        editor.commit();
    }

    /**
     * 自动加密Editor
     */
    final class SecurityEditor implements Editor {

        private Editor mEditor;

        /**
         * constructor
         */
        private SecurityEditor(){
            mEditor = mSharedPreferences.edit();
        }

        @Override
        public Editor putString(String key, String value) {
            mEditor.putString(encryptPreference(key), encryptPreference(value));
            return this;
        }

        @Override
        public Editor putStringSet(String key, Set<String> values) {
            final Set<String> encryptSet = new HashSet<>();
            for (String value : values){
                encryptSet.add(encryptPreference(value));
            }
            mEditor.putStringSet(encryptPreference(key), encryptSet);
            return this;
        }

        @Override
        public Editor putInt(String key, int value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Integer.toString(value)));
            return this;
        }

        @Override
        public Editor putLong(String key, long value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Long.toString(value)));
            return this;
        }

        @Override
        public Editor putFloat(String key, float value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Float.toString(value)));
            return this;
        }

        @Override
        public Editor putBoolean(String key, boolean value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Boolean.toString(value)));
            return this;
        }

        @Override
        public Editor remove(String key) {
            mEditor.remove(encryptPreference(key));
            return this;
        }

        /**
         * Mark in the editor to remove all values from the preferences.
         * @return this
         */
        @Override
        public Editor clear() {
            mEditor.clear();
            return this;
        }

        /**
         * 提交数据到本地
         * @return Boolean 判断是否提交成功
         */
        @Override
        public boolean commit() {

            return mEditor.commit();
        }

        /**
         * Unlike commit(), which writes its preferences out to persistent storage synchronously,
         * apply() commits its changes to the in-memory SharedPreferences immediately but starts
         * an asynchronous commit to disk and you won't be notified of any failures.
         */
        @Override
        @TargetApi(Build.VERSION_CODES.GINGERBREAD)
        public void apply() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
                mEditor.apply();
            } else {
                commit();
            }
        }
    }
}

EncryptUtil.java

现在我们实现自己的加解密工具类,加解密工具类大家可以根据自己的实际情况进行定制。我采用的是AES128的加密方式,首先获取当前设备的序列号,然后拼接一个随机字符串,生成hash值,作为AES加密的key,详细代码如下:

package com.domain.securitysharedpreference;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/**
 * AES加密解密工具
 * @author Max
 * 2016年11月25日15:25:17
 */
public class EncryptUtil {

    private String key;
    private static EncryptUtil instance;
    private static final String TAG = EncryptUtil.class.getSimpleName();


    private EncryptUtil(Context context){
        String serialNo = getDeviceSerialNumber(context);
        //加密随机字符串生成AES key
        key = SHA(serialNo + "#$ERDTS$D%F^Gojikbh").substring(0, 16);
        Log.e(TAG, key);
    }

    /**
     * 单例模式
     * @param context context
     * @return
     */
    public static EncryptUtil getInstance(Context context){
        if (instance == null){
            synchronized (EncryptUtil.class){
                if (instance == null){
                    instance = new EncryptUtil(context);
                }
            }
        }

        return instance;
    }

    /**
     * Gets the hardware serial number of this device.
     *
     * @return serial number or Settings.Secure.ANDROID_ID if not available.
     */
    @SuppressLint("HardwareIds")
    private String getDeviceSerialNumber(Context context) {
        // We're using the Reflection API because Build.SERIAL is only available
        // since API Level 9 (Gingerbread, Android 2.3).
        try {
            String deviceSerial = (String) Build.class.getField("SERIAL").get(null);
            if (TextUtils.isEmpty(deviceSerial)) {
                return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
            } else {
                return deviceSerial;
            }
        } catch (Exception ignored) {
            // Fall back  to Android_ID
            return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        }
    }


    /**
     * SHA加密
     * @param strText 明文
     * @return
     */
    private String SHA(final String strText){
        // 返回值
        String strResult = null;
        // 是否是有效字符串
        if (strText != null && strText.length() > 0){
            try{
                // SHA 加密开始
                MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
                // 传入要加密的字符串
                messageDigest.update(strText.getBytes());
                byte byteBuffer[] = messageDigest.digest();
                StringBuffer strHexString = new StringBuffer();
                for (int i = 0; i < byteBuffer.length; i++){
                    String hex = Integer.toHexString(0xff & byteBuffer[i]);
                    if (hex.length() == 1){
                        strHexString.append('0');
                    }
                    strHexString.append(hex);
                }
                strResult = strHexString.toString();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }

        return strResult;
    }


    /**
     * AES128加密
     * @param plainText 明文
     * @return
     */
    public String encrypt(String plainText) {
        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, keyspec);
            byte[] encrypted = cipher.doFinal(plainText.getBytes());
            return Base64.encodeToString(encrypted, Base64.NO_WRAP);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * AES128解密
     * @param cipherText 密文
     * @return
     */
    public String decrypt(String cipherText) {
        try {
            byte[] encrypted1 = Base64.decode(cipherText, Base64.NO_WRAP);
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            cipher.init(Cipher.DECRYPT_MODE, keyspec);
            byte[] original = cipher.doFinal(encrypted1);
            String originalString = new String(original);
            return originalString;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

这样我们基本上实现了SharedPreference的加解密存储,APP的数据安全进一步得到了保证,现在和大家说一下使用方式:首先我们看一下普通的SharedPreference:

/**
 * 以常规的SharedPreference保存数据
 */
private void saveInCommonPreference(){
    SharedPreferences sharedPreferences = getSharedPreferences("common_prefs", Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putString("username", mEmailView.getText().toString());
    editor.putString("password", mPasswordView.getText().toString());
    editor.apply();
}

我们看一下本地保存的效果

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="username">1136138123@qq.com</string>
    <string name="password">147258369</string>
</map>

其次,我们来看一下SecuritySharedPreference的使用方式:

/**
 * 以加密的SharedPreference保存数据
 */
private void saveInSecurityPreference(){
    SecuritySharedPreference securitySharedPreference = new SecuritySharedPreference(getApplicationContext(), "security_prefs", Context.MODE_PRIVATE);
    SecuritySharedPreference.SecurityEditor securityEditor = securitySharedPreference.edit();
    securityEditor.putString("username", mEmailView.getText().toString());
    securityEditor.putString("password", mPasswordView.getText().toString());
    securityEditor.apply();
}

我们看一下本地保存的效果有什么区别

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="BZFXj2GNc39n80SizhqRug==">Rnfpxffj9rNl29dsoQxlUzpSaR9m5K6myIYtqQOiIRU=</string>
    <string name="qF87qMi9YiXtVcIzaHOXrA==">HoHo+CFJrXK3CPMUpcTTow==</string>
</map>

效果非常棒!我们通过简简单单的两个类,实现了SharedPreference的加密。虽然只是个非常简单的小功能,但是给数据安全提供了护盾,O(∩_∩)O哈哈~
有的同学可能会说,我的项目中已经使用了SharedPreference,如何迁移到SecuritySharedPreference呢?如何进行不加密数据到加密数据的过渡呢?这一点其实我已经替大家做好了,我们在下一次升级应用的时候,在第一次使用SharedPreference时,调用handleTransition()方法进行数据加密的过渡。

作者:voidmain_123 发表于2016/11/25 18:16:14 原文链接
阅读:132 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>