注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

浅谈Android插件化

一、认识插件化 1.1 插件化起源 插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。 想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才...
继续阅读 »

一、认识插件化


1.1 插件化起源


插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。


想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已。


常见的应用安装目录有:



  • /system/app:系统应用

  • /system/priv-app:系统应用

  • /data/app:用户应用


那可能大家会想问,既然安装这个过程如此简单,Android 是怎么运行应用中的代码的呢,我们先看 Apk 的构成,一个常见的 Apk 会包含如下几个部分:



  • classes.dexJava 代码字节码

  • res:资源文件

  • libso 文件

  • assets:静态资产文件

  • AndroidManifest.xml:清单文件


其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已。


那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?


1.2 插件化优点


插件化让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益:



  • 减少安装Apk的体积、按需下载模块

  • 动态更新插件

  • 宿主和插件分开编译,提升开发效率

  • 解决方法数超过65535的问题


想象一下,你的应用拥有 Native 应用一般极高的性能,又能获取诸如 Web 应用一样的收益。


嗯,理想很美好不是嘛?


1.3 与组件化的区别



  • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。

  • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。


二、插件化的技术难点


? 想让插件的Apk真正运行起来,首先要先能找到插件Apk的存放位置,然后我们要能解析加载Apk里面的代码。


? 但是光能执行Java代码是没有意义的,在Android系统中有四大组件是需要在系统中注册的,具体来说是在 Android 系统的 ActivityManagerService (AMS)PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMSPMS,如何欺骗系统,让他承认一个未安装的 Apk 中的组件,如何让宿主动态加载执行插件Apk中 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等是插件化最大的难点。


? 另外,应用资源引用(特指 R 中引用的资源,如 layoutvalues 等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。


总结一下,其实做到插件化的要点就这几个:



  • 如何加载并执行插件 Apk 中的代码(ClassLoader Injection

  • 让系统能调用插件 Apk 中的组件(Runtime Container

  • 正确识别插件 Apk 中的资源(Resource Injection


当然还有其他一些小问题,但可能不是所有场景下都会遇到,我们后面再单独说。


三、ClassLoader Injection


ClassLoader 是插件化中必须要掌握的,因为我们知道Android 应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk,系统不会处理其中的类,所以需要使用 ClassLoader 加载 Apk,然后反射里面的代码。


3.1 java 中的 ClassLoader



  • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等


  • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包


  • AppClassLoader 负责加载 classpath 里的 jar 包和目录



3.2 android 中的 ClassLoader


在Android系统中ClassLoader是用来加载dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件



  • PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

    public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
    ClassLoader parent)
    {
    super(dexPath, null, libraryPath, parent);
    }
    }

  • DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

    public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
    String libraryPath, ClassLoader parent)
    {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
    }


我们在插件化中一般使用的是 DexClassLoader。


3.3 双亲委派机制


每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。


    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 先从父类加载器中进行加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 没有找到,再自己加载
c = findClass(name);
}
}
return c;
}
复制代码

3.4 如何加载插件中的类


要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要哪些参数。


public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
// ...
}
}
复制代码

构造函数需要四个参数: dexPath 是需要加载的 dex / apk / jar 文件路径 optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置 librarySearchPath 是 native 依赖的位置 parent 就是父类加载器,默认会先从 parent 加载对应的类


创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:


    // 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin() {
var inputStream = assets.open("plugin.apk")
File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
}

private fun init() {
extractPlugin()
pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用来加载插件类
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
}


3.5 执行插件类的方法


通过反射来执行类的方法


val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test",null).invoke(loadClass)

我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。


四、Runtime Container


我们之前说到 Activity 插件化最大的难点是如何欺骗系统,让他承认一个未安装的 Apk 中的组件。 因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。 如果直接把插件的 Activity 注册到宿主 Manifest 里就失去了插件化的动态特性,因为每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。


4.1 为什么没有注册的 Activity 不能和系统交互


这里的不能直接交互的含义有两个



  1. 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:

    android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?



这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:


public class Instrumentation {
public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}

switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
...
}
}
}



  1. Activity 的生命周期无法被调用,其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。


4.2 运行时容器技术


由于Android中的组件(Activity,Service,BroadcastReceiver和ContentProvider)是由系统创建的,并且由系统管理生命周期。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。插件化如何支持组件生命周期的管理。 大致分为两种方式:



  • 运行时容器技术(ProxyActivity代理)

  • 预埋StubActivity,hook系统启动Activity的过程


我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。


它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:



  • pluginName

  • pluginApkPath

  • pluginActivityName


等,其实最重要的就是 pluginApkPathpluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:



  • 转发所有来自系统的生命周期回调至插件 Activity

  • 接受 Activity 方法的系统调用,并转发回系统


我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity


public class ContainerActivity extends Activity {
private PluginActivity pluginActivity;

@Override
protected void onCreate(Bundle savedInstanceState) {
String pluginActivityName = getIntent().getString("pluginActivityName", "");
pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
if (pluginActivity == null) {
super.onCreate(savedInstanceState);
return;
}

pluginActivity.onCreate();
}

@Override
protected void onResume() {
if (pluginActivity == null) {
super.onResume();
return;
}
pluginActivity.onResume();
}

@Override
protected void onPause() {
if (pluginActivity == null) {
super.onPause();
return;
}
pluginActivity.onPause();
}

// ...
}

public class PluginActivity {
private ContainerActivity containerActivity;

public PluginActivity(ContainerActivity containerActivity) {
this.containerActivity = containerActivity;
}

@Override
public <T extends View> T findViewById(int id) {
return containerActivity.findViewById(id);
}
// ...
}

// 插件 `Apk` 中真正写的组件
public class TestActivity extends PluginActivity {
// ......
}

是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。


4.3 字节码替换


该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。


class TestActivity extends Activity {}
->
class TestActivity extends PluginActivity {}

有没有什么办法能让插件组件的编写与原来没有任何差别呢?


Shadow 的做法是字节码替换插件,这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。


实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:


class TestActivity extends Activity {}

然后完成编译后,最后的字节码中,显示的却是:


class TestActivity extends PluginActivity {}

到这里基本的框架就差不多结束了。


五、Resource Injection


最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id


资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:



  • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 ApkPackageInfo

  • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例


我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 ApkPackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:


PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
pluginApkPath,
PackageManager.GET_ACTIVITIES
| PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

Resources injectResources = null;
try {
injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
// ...
}

拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:


public class PluginResources extends Resources {
private Resources hostResources;
private Resources injectResources;

public PluginResources(Resources hostResources, Resources injectResources) {
super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
this.hostResources = hostResources;
this.injectResources = injectResources;
}

@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return injectResources.getString(id, formatArgs);
} catch (NotFoundException e) {
return hostResources.getString(id, formatArgs);
}
}

// ...
}

然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:


public class ContainerActivity extends Activity {
private Resources pluginResources;

@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
// ...
}

@Override
public Resources getResources() {
if (pluginActivity == null) {
return super.getResources();
}
return pluginResources;
}
}

这样就完成了资源的注入。



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

二阶贝塞尔仿微信扔炸弹动画

前言 新出来的微信炸屎动画很多人都玩过了,所以先仿照一个微信扔炸弹的动画,在后续有时间会做一个完整的,效果如下: 具体实现 其中最麻烦的就是绘制抛物线了,爆炸的效果只是播放了一个动画,另外微信貌似都是通过代码绘制的,可能不是动画,奈何没有人家那技术,...
继续阅读 »

前言


新出来的微信炸屎动画很多人都玩过了,所以先仿照一个微信扔炸弹的动画,在后续有时间会做一个完整的,效果如下:



具体实现


其中最麻烦的就是绘制抛物线了,爆炸的效果只是播放了一个动画,另外微信貌似都是通过代码绘制的,可能不是动画,奈何没有人家那技术,只能找一张动画来凑合。


二阶贝塞尔曲线


抛物线在这里是通过二阶贝塞尔曲线来完成,所以先来了解下什么是二阶贝塞尔曲线,从下图中可以发现,二阶贝塞尔曲线有三个关键点,我们可以称作起点坐标、终点坐标,还有控制点。


录屏_选择区域_20210615170032.gif


起点和终点坐标好理解,控制点可以理解成开始下降的转折点,而古老的数学大神早就提供好了公式,我们只需要向这个公式提供这几个参数即可得到x、y,当然还有个参数是时间,有了时间控制,我们可以在指定秒内把他平滑的绘制完成。


公式如下:


x = (1 - t)^2 * 0 + 2 t (1 - t) * 1 + t^2 * 1 = 2 t (1 - t) + t^2
y= (1 - t)^2 * 1 + 2 t (1 - t) * 1 + t^2 * 0 = (1 - t)^2 + 2 t (1 - t)

自定义二阶贝塞尔曲线计算器


提到动画,首先可能会想到ObjectAnimator类,没错,抛物线也是通过ObjectAnimator来完成的,只不过我们需要自定义一个TypeEvaluator,用来提供二阶贝塞尔曲线的x和y。


TypeEvaluator只有一个方法,定义如下:


public abstract T evaluate (float fraction, 
T startValue,
T endValue)



fraction表示开始值和结束值之间的比例,startValue、endValue分别是开始值和结束值,这个比例也可以当作是时间,可能官方一点叫比例,他会自动计算,值的范围是0-1,比如取值0.5的时候就是动画完成了一半,1的时候动画完成。


所以套入二阶贝塞尔曲线公式得到如下代码:


class PointFTypeEvaluator(var control: PointF) : TypeEvaluator<PointF> {
override fun evaluate(fraction: Float, startValue: PointF, endValue: PointF): PointF {
return getPointF(startValue, endValue, control, fraction)
}

private fun getPointF(start: PointF, end: PointF, control: PointF, t: Float): PointF {
val pointF = PointF()
pointF.x = (1 - t) * (1 - t) * start.x + 2 * t * (1 - t) * control.x + t * t * end.x
pointF.y = (1 - t) * (1 - t) * start.y + 2 * t * (1 - t) * control.y + t * t * end.y
return pointF
}

}

播放动画


然后使用ObjectAnimator进行播放。


 val animator = ObjectAnimator.ofObject(activityMainBinding.boom, "mPointF",
PointFTypeEvaluator(controlP), startP, endP)

注意的是这个View需要有point方法,参数是PointF,方法内主要完成x和y的设置。


 public void setPoint(PointF pointF) {
setX(pointF.x);
setY(pointF.y);
}

当然微信炸弹落地的位置是随机的,我们也加个随机。


class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding;

private fun getRandom(max: Int, min: Int): Int {
val random = java.util.Random()
return random.nextInt(max - min + 1) + min
}

private fun getRandomPointF():PointF{
val outMetrics = DisplayMetrics()
val offset = 100
windowManager.defaultDisplay.getMetrics(outMetrics)
val width = outMetrics.widthPixels
val height = outMetrics.heightPixels
return PointF(getRandom(width / 2 + offset, width / 2 - offset).toFloat(), getRandom(height / 2 + offset, height / 2 - offset).toFloat())
}


override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);



binding.button.setOnClickListener {
binding!!.boom.visibility = View.VISIBLE
val startP = PointF()
val endP = PointF()
val controlP = PointF()
val randomPointF = getRandomPointF()
startP.x = 916f
startP.y = 1353f
endP.x = randomPointF.x
endP.y = randomPointF.y
controlP.x = randomPointF.x + getRandom(200, 50)
controlP.y = randomPointF.y - getRandom(200, 50)
val animator = ObjectAnimator.ofObject(binding.boom, "point",
PointFTypeEvaluator(controlP), startP, endP)

animator.start()
}
}

}

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">


<data>

</data>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">



<com.airbnb.lottie.LottieAnimationView
android:visibility="gone"
android:id="@+id/lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="boom.json">
</com.airbnb.lottie.LottieAnimationView>

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始">
</Button>

<com.example.kotlindemo.widget.MyImageView
android:id="@+id/boom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_boom"
android:visibility="gone">
</com.example.kotlindemo.widget.MyImageView>
</RelativeLayout>
</layout>

效果如下:


录屏_选择区域_20210615174149.gif


爆炸效果


爆炸效果是使用的动画,用的lottie框架,这里提供爆炸文件的下载地址。


https://lottiefiles.com/download/public/9990-explosion

有了结束的坐标点,只需要吧LottieAnimationView移动到对应位置进行播放即可,播放后隐藏,完整代码如下:


package com.example.kotlindemo

import android.animation.Animator
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.PointF
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.example.kotlindemo.databinding.ActivityMainBinding
import com.example.kotlindemo.widget.PointFTypeEvaluator
import meow.bottomnavigation.MeowBottomNavigation
import kotlin.random.Random


class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding;

private fun getRandom(max: Int, min: Int): Int {
val random = java.util.Random()
return random.nextInt(max - min + 1) + min
}

private fun getRandomPointF():PointF{
val outMetrics = DisplayMetrics()
val offset = 100
windowManager.defaultDisplay.getMetrics(outMetrics)
val width = outMetrics.widthPixels
val height = outMetrics.heightPixels
return PointF(getRandom(width / 2 + offset, width / 2 - offset).toFloat(), getRandom(height / 2 + offset, height / 2 - offset).toFloat())
}


override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);


binding!!.button.setOnClickListener {
binding!!.boom.visibility = View.VISIBLE
val startP = PointF()
val endP = PointF()
val controlP = PointF()
val randomPointF = getRandomPointF()
startP.x = 916f
startP.y = 1353f
endP.x = randomPointF.x
endP.y = randomPointF.y
controlP.x = randomPointF.x + getRandom(200, 50)
controlP.y = randomPointF.y - getRandom(200, 50)
val animator = ObjectAnimator.ofObject(binding.boom, "point",
PointFTypeEvaluator(controlP), startP, endP)
animator.duration = 600
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
val measuredHeight = binding.lottie.measuredHeight
val measuredWidth = binding.lottie.measuredWidth
binding.lottie.x = randomPointF.x - measuredWidth / 2
binding.lottie.y = randomPointF.y - measuredHeight / 2
binding.lottie.visibility = View.VISIBLE
binding.boom.visibility = View.GONE
binding.lottie.playAnimation()
binding.lottie.addAnimatorListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
binding.lottie.visibility = View.GONE
}

override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
}

override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
animator.start()
}

}

}




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

Android App唤醒丶保活详解 , 以及代码展示

安卓进程进程保活分为: 黑色保活,白色保活,灰色保活 黑色保活: 可以说黑色保活,可以通过网络切换,拍照,拍视频,开机,利用系统产生的广播唤醒app,接入三方的sdk也会唤醒一些app,如支付宝,微信..........这样的话,这样的话,不敢想象系统存...
继续阅读 »

安卓进程进程保活分为:


黑色保活,白色保活,灰色保活


黑色保活:


可以说黑色保活,可以通过网络切换,拍照,拍视频,开机,利用系统产生的广播唤醒app,接入三方的sdk也会唤醒一些app,如支付宝,微信..........这样的话,这样的话,不敢想象系统存活会给系统带来多大的负担,所以我们的安卓手机也变得卡了,google官方可能也认识了这么一点,所以取消了


ACTION_NEW_PICTURE(拍照),ACTION_NEW_VIDEO(拍视频),CONNECTIVITY_ACTION(网络切换)


app也会随着做一点改变,(不过sdk的使用还是会通过一个app启动相关的一些app , 黑色保活我个人认为不推荐使用,毕竟为了我们广大安卓用户。)


白色保活:


白色保活手段非常简单,就是调用系统api启动一个前台的Service进程,这样会在系统的通知栏生成一个Notification,用来让用户知道有这样一个app在运行着,哪怕当前的app退到了后台。




不过用户看到这个图标的时候,都会把它清空的。。。。



灰色保活:


可以说,灰色保活是用的最多,当用户不知不觉中这个app程序已经在后台运行了。


它是利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。这样做带来的好处就是,用户无法察觉到你运行着一个前台进程(因为看不到Notification),但你的进程优先级又是高于普通后台进程的。API < 18,启动前台Service时直接传入new Notification();API >= 18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理;


安卓app唤醒:


其实app唤醒的介绍很好说,app唤醒就是当打开一个app的时候,另一个app里有对应刚打开那个app的属性标志,根据你想要的唤醒方式,执行不同的代码操作,这样就可以唤醒另一个没打开的app了。(代码在最下面)


下面我展示一下这几种状态下的代码:


这个是xml布局,主要是为了展示我所介绍的几种保活方式:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">

<Button
android:id="@+id/mBtn_white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="白色保活" />

<Button
android:id="@+id/mBtn_gray"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="灰色保活" />

<Button
android:id="@+id/mBtn_black"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="黑色保活(发广播)" />

<Button
android:id="@+id/mBtn_background_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="普通后台 Service 进程" />

</LinearLayout>

下面是主要实现类:


WakeReceiver


import android.app.Notification;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

public class WakeReceiver extends BroadcastReceiver {
private final static String TAG = WakeReceiver.class.getSimpleName();
private final static int WAKE_SERVICE_ID = -1111;
/**
* 灰色保活手段唤醒广播的action
*/
public final static String GRAY_WAKE_ACTION = "com.wake.gray";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GRAY_WAKE_ACTION.equals(action)) {
Log.i(TAG, "wake !! wake !! ");

Intent wakeIntent = new Intent(context, WakeNotifyService.class);
context.startService(wakeIntent);
}
}
/**
* 用于其他进程来唤醒UI进程用的Service
*/
public static class WakeNotifyService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "WakeNotifyService->onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "WakeNotifyService->onStartCommand");
if (Build.VERSION.SDK_INT < 18) {
startForeground(WAKE_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标
} else {
Intent innerIntent = new Intent(this, WakeGrayInnerService.class);
startService(innerIntent);
startForeground(WAKE_SERVICE_ID, new Notification());
}
return START_STICKY;
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "WakeNotifyService->onDestroy");
super.onDestroy();
}
}

/**
* 给 API >= 18 的平台上用的灰色保活手段
*/
public static class WakeGrayInnerService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "InnerService -> onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "InnerService -> onStartCommand");
startForeground(WAKE_SERVICE_ID, new Notification());
//stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "InnerService -> onDestroy");
super.onDestroy();
}
}
}

BackGroundService


import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

/**
* 普通的后台Service进程
*
* @author clock
* @since 2016-04-12
*/
public class BackgroundService extends Service {

private final static String TAG = BackgroundService.class.getSimpleName();

@Override
public void onCreate() {
Log.i(TAG, "onCreate");
super.onCreate();
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
super.onDestroy();
}
}

GrayService


import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import com.example.renzheng.receiver.WakeReceiver;

/**
* 灰色保活手法创建的Service进程
*
* @author Clock
* @since 2016-04-12
*/
public class GrayService extends Service {

private final static String TAG = GrayService.class.getSimpleName();
/**
* 定时唤醒的时间间隔,5分钟
*/
private final static int ALARM_INTERVAL = 5 * 60 * 1000;
private final static int WAKE_REQUEST_CODE = 6666;

private final static int GRAY_SERVICE_ID = -1001;

@Override
public void onCreate() {
Log.i(TAG, "GrayService->onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "GrayService->onStartCommand");
if (Build.VERSION.SDK_INT < 18) {
startForeground(GRAY_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标
} else {
Intent innerIntent = new Intent(this, GrayInnerService.class);
startService(innerIntent);
startForeground(GRAY_SERVICE_ID, new Notification());
}

//发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(WakeReceiver.GRAY_WAKE_ACTION);
PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_INTERVAL, operation);

return START_STICKY;
}

@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "GrayService->onDestroy");
super.onDestroy();
}

/**
* 给 API >= 18 的平台上用的灰色保活手段
*/
public static class GrayInnerService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "InnerService -> onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "InnerService -> onStartCommand");
startForeground(GRAY_SERVICE_ID, new Notification());
//stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "InnerService -> onDestroy");
super.onDestroy();
}
}
}

WhileService


import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
import com.example.renzheng.MainActivity;
import com.example.renzheng.R;
/**
* 正常的系统前台进程,会在系统通知栏显示一个Notification通知图标
*
* @author clock
* @since 2016-04-12
*/
public class WhiteService extends Service {

private final static String TAG = WhiteService.class.getSimpleName();
private final static int FOREGROUND_ID = 1000;

@Override
public void onCreate() {
Log.i(TAG, "WhiteService->onCreate");
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "WhiteService->onStartCommand");
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("Foreground");
builder.setContentText("I am a foreground service");
builder.setContentInfo("Content Info");
builder.setWhen(System.currentTimeMillis());
Intent activityIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
Notification notification = builder.build();
startForeground(FOREGROUND_ID, notification);
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "WhiteService->onDestroy");
super.onDestroy();
}
}

MainActivity


import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import com.example.renzheng.service.BackgroundService;
import com.example.renzheng.service.GrayService;
import com.example.renzheng.service.WhiteService;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private final static String TAG = MainActivity.class.getSimpleName();
/**
* 黑色唤醒广播的action
*/
private final static String BLACK_WAKE_ACTION = "com.wake.black";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.mBtn_white).setOnClickListener(this);
findViewById(R.id.mBtn_gray).setOnClickListener(this);
findViewById(R.id.mBtn_black).setOnClickListener(this);
findViewById(R.id.mBtn_background_service).setOnClickListener(this);
}

@Override
public void onClick(View v) {
int viewId = v.getId();
if (viewId == R.id.mBtn_white) { //系统正常的前台Service,白色保活手段
Intent whiteIntent = new Intent(getApplicationContext(), WhiteService.class);
startService(whiteIntent);

} else if (viewId == R.id.mBtn_gray) {//利用系统漏洞,灰色保活手段(API < 18 和 API >= 18 两种情况)
Intent grayIntent = new Intent(getApplicationContext(), GrayService.class);
startService(grayIntent);

} else if (viewId == R.id.mBtn_black) { //拉帮结派,黑色保活手段,利用广播唤醒队友
Intent blackIntent = new Intent();
blackIntent.setAction(BLACK_WAKE_ACTION);
sendBroadcast(blackIntent);

} else if (viewId == R.id.mBtn_background_service) {//普通的后台进程
Intent bgIntent = new Intent(getApplicationContext(), BackgroundService.class);
startService(bgIntent);
}
}
}

代码注册权限:


 


<receiver
android:name=".receiver.WakeReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.wake.gray" />
</intent-filter>
</receiver>

<service
android:name=".service.WhiteService"
android:enabled="true"
android:exported="false"
android:process=":white" />
<service
android:name=".service.GrayService"
android:enabled="true"
android:exported="false"
android:process=":gray" />
<service
android:name=".service.GrayService$GrayInnerService"
android:enabled="true"
android:exported="false"
android:process=":gray" />
<service
android:name=".service.BackgroundService"
android:enabled="true"
android:exported="false"
android:process=":bg" />
<service
android:name=".receiver.WakeReceiver$WakeNotifyService"
android:enabled="true"
android:exported="false" />

<service
android:name=".receiver.WakeReceiver$WakeGrayInnerService"
android:enabled="true"
android:exported="false" />

 


下面是app唤醒代码:


有2个APP,分别为A和B,当A活着的时候,试着开启B的后台服务,将原本杀死的B的后台服务程序活起来。反之也一样。


1.先看B的代码:


创建一个服务B,给服务添加一个process属性,设置action。


 

<service
android:name=".B"
android:process=":test">
<intent-filter>
<action android:name="yangyang" />
</intent-filter>
</service>

B的代码,在onStartCommand方法中弹出toast:


public class B extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {

Toast.makeText(this, "B 已经唤醒", Toast.LENGTH_SHORT).show();
return START_STICKY;
}
}

2.看A的代码,在MainActivity中点击开启B应用的B服务的代码:


public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

Button btn = (Button) findViewById(R.id.btn);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendService();
}
});
}

private void sendService() {
boolean find = false;

ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
Intent serviceIntent = new Intent();

for (ActivityManager.RunningServiceInfo runningServiceInfo : mActivityManager.getRunningServices(100)) {
if (runningServiceInfo.process.contains(":test")) {//判断service是否在运行
Log.e("zhang", "process:" + runningServiceInfo.process);
find = true;
}
}
//判断服务是否起来,如果服务没起来,就唤醒
if (!find) {
serviceIntent.setPackage("com.example.b);
serviceIntent.setAction("yangyang");
startService(serviceIntent);
Toast.makeText(this, "开始唤醒 B", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(this, "B 不用唤醒", Toast.LENGTH_SHORT).show();
}
}
}

这里只是写了A启动B服务的代码,反之也是一样的。被启动应用的Servcie在AndroidMainfest.xml中注册时注意,添加process属性,和设置action匹配规则。


————————————————
版权声明:本文为CSDN博主「看美丽风晴」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/nazicsdn/article/details/79752617

收起阅读 »

有“声”聚一堂|RTE 2021 编程挑战赛圆满收官啦!

6 月 12 日,由声网Agora 与环信联合主办的“RTE 2021 编程挑战赛”圆满落幕。从 200+ 支参赛队伍中冲出重围的 46 支决赛队伍用精彩的答辩为历时 2 个多月的大赛划下了圆满的句号。今年的“RTE 2021 创新编程挑战赛”共分为 2 个赛...
继续阅读 »

6 月 12 日,由声网Agora 与环信联合主办的“RTE 2021 编程挑战赛”圆满落幕。从 200+ 支参赛队伍中冲出重围的 46 支决赛队伍用精彩的答辩为历时 2 个多月的大赛划下了圆满的句号。

今年的“RTE 2021 创新编程挑战赛”共分为 2 个赛道:应用创新赛道延续了「使用声网Agora SDK 开发应用」的赛题;技术创新赛道开发者可以「利用声网云市场插件接口,开发自研插件与功能演示 Demo」。


尽管此次的赛制与赛题对参赛队伍提出了更高的要求,但同时也为大家提供了独有的技术创新空间。相较去年而言,两个赛道的报名队伍及提交作品几乎都是去年的两倍。

本次大赛的决赛和颁奖都是通过 Agora Video Call App 在线上进行的,全程通过 B 站进行了直播。 最终,决赛共诞生了应用创新赛道的一、二、三等奖团队各一名,“环信专项奖”一名,以及“优秀奖” 六名;技术创新赛道“技术创新专项奖”一名,“优秀奖”一名。



应用创新赛道

一等奖:Agora Home AI

随着智能设备性能提升和网络的快速发展,以音视频为基础的智能硬件也正在蓬勃发展中。跨品牌、跨产品的设备管理也成为萦绕在用户日常使用中绕不开的一个话题。

「Agora Home AI」 系统以智能家居为主题,使用云信令 SDK 实现了IoT 设备远程控制。同时,通过声网Agora RTC SDK 实现人与机器的 1V1 视频,将机器人端采集到的视频发送至 PC 控制中心,进行 AI 智能检测,触发事件响应。


系统采用开源了 Yolo V3 算法进行各种视频数据的处理,支持 C#、C++ 调用;Unity 3D、VS 系列开发。目前已支持 Yolo 基础 80 种物体识别、安全帽识别、冰球识别文件等。采用声网提供的云信令 SDK 进行远程设备控制,构建群组房间进行消息实时通信,支持通过自定义协议进行智能硬件的控制。

「Agora Home AI」可以帮助用户实现可穿戴设备、智能家具设备、视频监控设备接入何控制。包括智能灯光、智能门窗、智能门锁、智能安防、智能手环监测、智能家电控制等配套产品,让用户实现多种品牌的智能设备在统一的交互平台内互联互通、统一管理、智能联动。为给用户创造更舒适、更安全、更节能的家居生活环境。


二等奖:Agora FIow

获得第二名的作品「Agora Flow」是一个基于声网+环信 SDK 搭建的音视频 Low Code Web 共享编辑器。

作品的灵感来源于在使用声网Agora SDK 的过程中,创作者一直在思考关于音视频服务除了以 SDK 的形式来提供服务和为开发者赋能外,还有没有别的形式呢?Low Code 就是这样一个可能的解决方案。将音视频相关功能进行模块化集成,提供一个图形化界面,让开发者可以用做 PPT 的形式来完成想要实现的功能。
作品通过声网的音视频传输及云信令 SDK 产品,提供了基于 Web 的集成了 RTC Chat SDK 的模版工程,通过 CodeGen 来生成配置项。实现了在线流程图编辑器 Low Code 项目的自动生成。作品中的一切的操作几乎都可以通过拖拽来完成。


有开发者开玩笑说,这次的大赛作品很多都是开发了一个 App,而「Agora Flow」则是做了一个帮助开发者能更好开发 App 的项目。



三等奖:都市探险家

「都市探险家」项目是一款利用地图 LBS + 云信令 SDK + 实时音视频构建的社交产品。这款产品为想要寻找共同爱好的新朋友并一起在都市进行旅游、探索的小伙伴而设计。


产品的使用十分简单便捷,用户注册登录后,通过 LBS 地图会自动更新用户所在位置,只要点击“发起任务”并选择“探险”人数,用户就可以与小伙伴进行一次全新的都市探险啦。
对于 RTE 场景而言,产品中实现了多人语聊房场景。并且,通过云信令 SDK 的使用结合了实际的业务场景,对于当下的语聊房场景进行了拓展。 产品未来也会接入视频聊天的功能,让没有办法即时出行的小伙伴也能共同参与到城市的探险当中。



环信专项奖:忘忧馆

「忘忧馆」是一个很有温度的作品,希望可以帮助现代生活中的人们通过彼此倾诉忘掉烦恼、解除忧愁,传播正能量。


这是一款陌生人社交 App,包含信息流。结合了几种最常见的社交产品形态,包括文字聊天,通话等等。让一些不方便与亲人和朋友诉说的烦恼,可以在和陌生人交流时找到共鸣与安慰。


优秀奖:Vchat

「Vchat」利用人脸骨骼识别和云信令 SDK 实现了虚拟 3D 角色的实时通话。使用 tensorflow.js 的 WebGL 引擎作为后端,使用现有开源的人脸识别模型通过摄像头识别人脸的位置以及五官的状态。再通过 Three.js 和 Vrm.js 将人脸数据实时更新到虚拟的 3D 模型上。


在视频部分,通过实时消息 RTM SDK 将人脸骨骼数据实时传输到频道中让其他用户订阅还原人脸。而语音部分则是通过 RTC SDK 将声音进行实时传输并让用户进行订阅。可实现同步换脸、变声聊天等功能。
除了上述的「Vchat」以外,还有「灵动课堂答题组件」、「Agora X-Runtime」、「Weln」、「欢信(bla-bla.app)」、「智能AR毛笔临摹教学系统/CopyTeachWorks」作品获得了此次大赛“应用创新赛道”的优秀奖。关于这些优秀的作品可能没有办法在这里跟大家一一呈现,感兴趣的小伙伴可以前往参赛作品的 Github 仓库进行查看:https://github.com/AgoraIO-Community/RTE-2021-Innovation-Challenge/tree/master/Application-Challenge


技术创新赛道

技术创新专项奖:人脸识别

「技术创新专项奖」是为“技术创新赛道”专门设置的一个奖项。获奖作品是一个在 iOS 平台上使用使用 AgoraEngineKit2 开发接入一个基于 C++ 语言封装的「人脸识别」插件。

作品通过 TYSMExtensionManger 类与对外交互,对内则处理插件实现的相关逻辑。将 IExtensionProvider、IVideoFilter 和自己的开发的 Processer 都放在同一个地方。用 framework 方式对外公开两个文件,既方便开发者查阅,同时也可作为作为参数传递,增强代码可阅读性。 


该插件可以支持人脸检测、追踪、以及多脸的追踪识别,对脸部轮廓、眼睛、眉毛、鼻子、嘴巴等识别到的区域以 3D 点状作出反馈。



优秀奖:Water Mask

「Water Mask」项目是“技术赛道”中的参赛作品,通过在声网 SDK 的视频采集或者播放环节,在 YUV 域上或者编码后添加图片或文字类型的隐性水印。

隐性水印(盲水印)添加后,用户不能直接看到视频中的水印信息。在保护视频发布者版权的同时,也保障了用户的视频观看体验。未来,「Water Mask」还希望在音频处理上,可以扩展声纹水印,在视频版权追溯、认证防伪等场景为行业带来更多、更好的体验。
以上就是本届 「RTE 2021 编程挑战赛」的部分获奖作品及团队情况。关于本次挑战赛的更多作品情况将开源在 Github,感兴趣的小伙伴可前往进行查看:

https://github.com/AgoraIO-Community/RTE-2021-Innovation-Challenge



收起阅读 »

ios中应用Lottie解决动画问题

Lottie的简单介绍:使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份J...
继续阅读 »

Lottie的简单介绍:

使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份JSON文件, 解析动画结构和参数信息并渲染。

Lottie的优点:

1、设计即所见: 设计师用AE设计好动画后直接导出Json文件,Lottie 解析Json文件后调Core Animation的API绘制渲染。还原度更好,开发成本更低。
2、跨平台: 支持iOS、Android、React Native。
3、性能:Lottie对于从AE导出的Json文件,用Core Animation做矢量动画, 性能较佳。Lottie 对解析后的数据模型有内存缓存。但是对多图片帧动画,性能比较差。
支持动画属性多:比起脸书的Keyframes,Lottie支持了更多AE动画属性,比如Mask, Trim Paths,Stroke (shape layer)等。
4、包大小,相比动辄上百K的帧动画,Json文件包大小很小。有图片资源的情况下,同一张图片也可以被多个图层复用,而且运行时内存中只有一个UIImage对象(iOS)。

Lottie在iOS中的使用

1、pod 'lottie-ios' 使用cocoaPods来加载Lottie。
2、在使用的界面添加头文件#import <Lottie/Lottie.h>
3、简单的使用介绍(要想深入学习,还需要自己点击进入源代码中去深究每一个方法和属性,在此就不一一列举了)

LOTAnimationView * animation = [LOTAnimationView animationNamed:@"HappyBirthday"];
animation.loopAnimation = YES; //是否是循环播放
animation.frame = self.view.bounds;
[self.view addSubview:animation];
animation.backgroundColor = [UIColor whiteColor];
[animation playWithCompletion:^(BOOL animationFinished) {
//播放完成,循环播放则不进入此方法
}];
//可以以动画为北京来添加子控件
UILabel * newV = [[UILabel alloc]initWithFrame:CGRectMake(100,100,200,100)];
newV.backgroundColor = [UIColor clearColor];
newV.textColor = [UIColor blackColor];
newV.text = @"Lottie的使用教程";
[animation addSubview:newV];

另外的创建方法

/// Load animation by name from the default bundle, Images are also loaded from the bundle
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));

/// Loads animation by name from specified bundle, Images are also loaded from the bundle
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle NS_SWIFT_NAME(init(name:bundle:));

/// Creates an animation from the deserialized JSON Dictionary
+ (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));

/// Loads an animation from a specific file path. WARNING Do not use a web URL for file path.
+ (nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath NS_SWIFT_NAME(init(filePath:));

/// Creates an animation from the deserialized JSON Dictionary, images are loaded from the specified bundle
+ (nonnull instancetype)animationFromJSON:(nullable NSDictionary *)animationJSON inBundle:(nullable NSBundle *)bundle NS_SWIFT_NAME(init(json:bundle:));

/// Creates an animation from the LOTComposition, images are loaded from the specified bundle
- (nonnull instancetype)initWithModel:(nullable LOTComposition *)model inBundle:(nullable NSBundle *)bundle;

/// Loads animation asynchrounously from the specified URL
- (nonnull instancetype)initWithContentsOfURL:(nonnull NSURL *)url;

LOTAnimationView的属性

/// Flag is YES when the animation is playing
@property (nonatomic, readonly) BOOL isAnimationPlaying;

/// Tells the animation to loop indefinitely.
@property (nonatomic, assign) BOOL loopAnimation;

/// The animation will play forward and then backwards if loopAnimation is also YES
@property (nonatomic, assign) BOOL autoReverseAnimation;

/// Sets a progress from 0 - 1 of the animation. If the animation is playing it will stop and the compeltion block will be called.
/// The current progress of the animation in absolute time.
/// e.g. a value of 0.75 always represents the same point in the animation, regardless of positive
/// or negative speed.
@property (nonatomic, assign) CGFloat animationProgress;

/// Sets the speed of the animation. Accepts a negative value for reversing animation.
@property (nonatomic, assign) CGFloat animationSpeed;

/// Read only of the duration in seconds of the animation at speed of 1
@property (nonatomic, readonly) CGFloat animationDuration;

/// Enables or disables caching of the backing animation model. Defaults to YES
@property (nonatomic, assign) BOOL cacheEnable;

/// Sets a completion block to call when the animation has completed
@property (nonatomic, copy, nullable) LOTAnimationCompletionBlock completionBlock;

/// Set the amimation data
@property (nonatomic, strong, nullable) LOTComposition *sceneModel;

4、简单应用的场景:(1)App的动画引导页。(2)一些特定的动画界面。(3)来作为Tabbar来使用。
5、这里来介绍下作为Tabbar的使用gitHub上原作者
6、Lottie动画资源网站
7、后续有新的学习会更新的。

链接:https://www.jianshu.com/p/7af085a6a20a

收起阅读 »

iOS - Block 准备面试必须了解的东西

一.Block的本质        block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。查看Block源码:struct __block_impl {    void*isa;    int Fla...
继续阅读 »

一.Block的本质

        block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。


查看Block源码:

struct __block_impl {

    void*isa;

    int Flags;

    int Reserved;

    void *FuncPtr;

};

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct__main_block_desc_0* Desc;

  // 构造函数(类似于OC的init方法),返回结构体对象

  __main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,intflags=0) {

    impl.isa = &_NSConcreteStackBlock;

    impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;

  }

};

// 封装了block执行逻辑的函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);

        }

static struct __main_block_desc_0 {

  size_treserved;

  size_tBlock_size;

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(intargc,constchar* argv[]) {

    /* @autoreleasepool */{__AtAutoreleasePool__autoreleasepool;

        // 定义block变量

        void(*block)(void) = &__main_block_impl_0(

                                                   __main_block_func_0,

                                                   &__main_block_desc_0_DATA

                                                   );

        // 执行block内部的代码

        block->FuncPtr(block);

    }

    return0;

}

说明:FuncPtr:指向调用函数的地址,__main_block_desc_0 :block描述信息,Block_size:block的大小

二.Block变量的捕获

2.1局部变量的捕获

        对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

int age=10;

void(^Block)(void)=^{

NSLog(@"age:%d",age);

};

age=20;

Block();

2.2__block 修饰的外部变量

        对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值

__block int age=10;

myBlock block=^{

NSLog(@"age = %d",age);

};

age=18;

block();

输出:18;

auto int age=10;

static int num=25;

void(^Block)(void)=^{

NSLog(@"age:%d,num:%d",age,num);

};

age=20;

num=11;

Block();

        输出结果为:age:10,num:11,auto变量block访问方式是值传递,也就是当block定义的时候,值已经传到block里面了,static变量block访问方式是指针传递,auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可,block不需要对全局变量捕获,都是直接采用取值的,局部变量的捕获是因为考虑作用域的问题,需要跨函数访问,就需要捕获,当出了作用域,局部变量已经被销毁,这时候如果block访问,就会出问题。

2.2.block变量捕获机制




 block里访问self,self是当调用block函数的参数,参数是局部变量,self指向调用者,所以它也会捕获self,block里访问成员,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量。

3.3Block的类型

        block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

__NSGlobalBlock __ ( _NSConcreteGlobalBlock )全局block即数据区

__NSStackBlock __ ( _NSConcreteStackBlock )堆区block

__NSMallocBlock __ ( _NSConcreteMallocBlock )栈区block

        说明:堆区,程序员自己控制,程序员自己管理,栈区,系统自动控制,一般我们使用最多的是堆区Block,判断类型的根据是没有访问auto变量的block是__NSGlobalBlock __ ,放在数据段访问了auto变量的block是__NSStackBlock __;[__NSStackBlock __ copy]操作就变成了__NSMallocBlock __,__NSGlobalBlock __ 调用copy操作后,什么也不做__NSStackBlock __ 调用copy操作后,复制效果是:从栈复制到堆;副本存储位置是堆__NSMallocBlock __ 调用copy操作后,复制效果是:引用计数增加;副本存储位置是堆,在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况是:

                1.block作为函数返回值时

                2.将block赋值给__strong指针时

                3.block作为Cocoa API中方法名含有usingBlock的方法参数时

                4.block作为GCD API的方法参数时

三.对象类型的auto变量

typedefvoid(^XBTBlock)(void);

XBTBlock block;

{

Person*p=[[Person alloc]init];

p.age=10;

block=^{

NSLog(@"======= %d",p.age);

};}

Person.m

-(void)dealloc{

NSLog(@"Person - dealloc");

}

        说明:block为堆block,block里面有一个Person指针,Person指针指向Person对象。只要block还在,Person就还在。block强引用了Person对象。在MRC下,就会打印,因为堆空间的block会对Person对象retain操作,拥有一次Person对象。无论MRC还是ARC,栈空间上的block,不会持有对象;堆空间的block,会持有对象。

特别说明:block内部访问了对象类型的auto变量时,是否会强引用?

栈block

a) 如果block是在栈上,将不会对auto变量产生强引用

b) 栈上的block随时会被销毁,也没必要去强引用其他对象

堆block

1.如果block被拷贝到堆上:

a) 会调用block内部的copy函数

b) copy函数内部会调用_Block_object_assign函数

c) _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

2.如果block从堆上移除

a) 会调用block内部的dispose函数

b) dispose函数内部会调用_Block_object_dispose函数

c) _Block_object_dispose函数会自动释放引用的auto变量(release)

正确答案:

如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象

如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

3.2gcd的block中引用 Person对象什么时候销毁?

eg:-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{

    Person*person = [[Personalloc]init];

    person.age=10;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"age:%d",person.age);

    });

    NSLog(@"touchesBegan");

}

输出:touchesBegan

            age:10

            Person-dealloc

        说明:gcd的block默认会做copy操作,即dispatch_after的block是堆block,block会对Person强引用,block销毁时候Person才会被释放,如果上诉Person用__weak。即添加代码为__weak Person*weakPerson=person;,在Block中变成NSLog(@"age:%p",weakPerson);,它就不输出age,使用__weak修饰过后的对象,堆block会采用弱引用,无法延时Person的寿命,所以在touchesBegan函数结束后,Person就会被释放,gcd就无法捕捉到Person,gcd内部只要有强引用Person,Person就会等待执行完再销毁!如果gcd内部先强引用后弱引用,Person会等待强引用执行完毕后释放,只要强引用执行完,就不会等待后执行的弱引用,会直接释放的

eg:-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{

    Person*person = [[Personalloc]init];

    person.age=10;

    __weakPerson*weakPerson = person;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),

                   dispatch_get_main_queue(), ^{

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"2-----age:%p",weakPerson);

        });

        NSLog(@"1-----age:%p",person);

    });

    NSLog(@"touchesBegan");

}

四.Block的修饰符

        block在修改NSMutableArray,不需要加__block,auto修饰变量,block无法修改,因为block使用的时候是内部创建了变量来保存外部的变量的值,block只有修改内部自己变量的权限,无法修改外部变量的权限。

        static修饰变量,block可以修改,因为block把外部static修饰变量的指针存入,block直接修改指针指向变量值,即可修改外部变量值。全局变量值,全局变量无论哪里都可以修改,当然block内部也可以修改。

eg:__block int age = 10,系统做了哪些---》编译器会将__block变量包装成一个对象

__block 修饰符作用:

        __block可以用于解决block内部无法修改auto变量值的问题

        __block不能修饰全局变量、静态变量(static)

        编译器会将__block变量包装成一个对象

        __block修改变量:age->__forwarding->age        

        __Block_byref_age_0结构体内部地址和外部变量age是同一地址

        __block的内存管理---->当block在栈上时,并不会对__block变量产生强引用

block的属性修饰词为什么是copy?

        block一旦没有进行copy操作,就不会在堆上

        block在堆上,程序员就可以对block做内存管理等操作,可以控制block的生命周期,会调用block内部的copy函数

        copy函数内部会调用_Block_object_assign函数

        _Block_object_assign函数会对__block变量形成强引用(retain)

        对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用,当block从堆中移除时,会调用block内部的dispose函数dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的__block变量(release),当block在栈上时,对它们都不会产生强引用,当block拷贝到堆上时,都会通过copy函数来处理它们,对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用

__block的__forwarding指针说明:

        栈上__block的__forwarding指向本身

        栈上__block复制到堆上后,栈上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身

五. block循环引用

        1.ARC下如何解决block循环引用的问题?

        三种方式:__weak、__unsafe_unretained、__block

        1)第一种方式:__weak

        Person*person=[[Person alloc]init];

        // __weak Person *weakPerson = person;

        __weaktypeof(person)weakPerson=person;

        person.block=^{

            NSLog(@"age is %d",weakPerson.age);

        };

        2)第二种方式:__unsafe_unretained

        __unsafe_unretained Person*person=[[Person alloc]init];

        person.block=^{

            NSLog(@"age is %d",weakPerson.age);

        };

        3)第三种方式:__block

        __block Person*person=[[Person alloc]init];

        person.block=^{

            NSLog(@"age is %d",person.age);

            person=nil;

        };

        person.block();

三种方法比较:__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil,__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,__block:必须把引用对象置位nil,并且要调用该block









作者:枫紫
链接:https://www.jianshu.com/p/4bde3936b154






收起阅读 »

iOS - Metal的认识

一.Metal 简介        在 WWDC 2014 上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲...
继续阅读 »

一.Metal 简介

        在 WWDC 2014 上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲染性能,并支持大家熟悉的游戏引擎及公司。

        Metal 是一种低层次的渲染应用程序编程接口,提供了软件所需的最低层,保证软件可以运行在不同的图形芯片上。Metal 提升了 A7 与 A8 处理器效能,让其性能完全发挥。

        Metal,充分利用GPU的运算能力,在现阶段,AVFoundation ⼈脸识别/.... 等大量需要显示计算的时候,苹果采用了硬件加速器驱动GPU工作,在音视频方面,⾳频编码/解码 / 视频编码/解码 ->压缩任务 ->都与硬件加速器分不开,苹果提供的Metal,能发挥GPU/CPu的最大性能,并且管理我们的资源。

二.Metal的渲染流程

        Metal的渲染流程借鉴了OpenGLES的流程,它通过控制顶点着色器/片元着色器(Metal里面叫顶点函数/片元函数),交给帧缓冲区,最后显示到屏幕上





值得注意的是,在OpenGlES中,图元装配有9中,在Metal中,图元装配只有五种,他们分别是:

                 MTLPrimitiveTypePoint = 0, 点

                 MTLPrimitiveTypeLine = 1, 线段

                 MTLPrimitiveTypeLineStrip = 2, 线环

                 MTLPrimitiveTypeTriangle = 3,  三角形

                 MTLPrimitiveTypeTriangleStrip = 4, 三角型扇

三.Metal的初级准备工作

3.1Metal的注意事项

        在讲Metal的初级使用之前,我们先来看看苹果爸爸给我们的建议,首先,苹果建议我们Separate Your Rendering Loop,即分离我们渲染,Metal给我们提供了一个View,叫MTKView,它继承自UiView,它主要的渲染是通过MTKViewDelegate协议回调实现,两个重要的协议方法是:

        1)当MTKView视图发生大小改变时调用

        /*!

         @method mtkView:drawableSizeWillChange:

         @abstract Called whenever the drawableSize of the view will change

         @discussion Delegate can recompute view and projection matricies or regenerate any buffers to be compatible with the new view size or resolution

         @paramviewMTKView which called this method

         @paramsizeNew drawable size in pixels

         */

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size;

        2)每当视图需要渲染时调用

        /*!

         @method drawInMTKView:

         @abstract Called on the delegate when it is asked to render into the view

         @discussion Called on the delegate when it is asked to render into the view

         */

        - (void)drawInMTKView:(nonnullMTKView*)view;

    3.2  Metal是如何驱动GPU工作的?



相关对应代码:在ViewController中,我们把当前的View变成MTKView,当然你也可以用self.view添加一个子视图View,CCRenderer是自定义的一个类,主要是分离MTview的渲染,

         _view.device = MTLCreateSystemDefaultDevice();一个MTLDevice 对象就代表这着一个GPU,通常我们可以调用方法MTLCreateSystemDefaultDevice()来获取代表默认的GPU单个对象.

        在CCRenderer中的初始化方法中- (id)initWithMetalKitView:(MTKView *)mtkView我们拿到device,创建newCommandQueue队列:

                _commandQueue = [_device newCommandQueue];

        所有应用程序需要与GPU交互的第一个对象是一个对象->MTLCommandQueue. 你使用MTLCommandQueue 去创建对象,并且加入MTLCommandBuffer 对象中.确保它们能够按照正确顺序发送到GPU.对于每一帧,一个新的MTLCommandBuffer 对象创建并且填满了由GPU执行的命令.

        在CCRenderer中,我们实现了MTKView的协议代理方法,在- (void)drawInMTKView:(nonnullMTKView*)view中,我们通过创建好的队列再创建命令缓冲区并且加入到MTCommandBuffer对象中去:

                id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

        值得注意的是,在创建好命令缓冲区后,Metal提出了一个概念叫渲染描述符:(个人理解这个渲染描述符是给每个命令打上一个标记,GPU在工作的时候通过这个渲染描述符取出相应的命令,如果说的不对,请大神指点)从视图绘制中,获得渲染描述符:

                MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

通过渲染描述符renderPassDescriptor创建MTLRenderCommandEncoder                

                id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];

        最后 [renderEncoderendEncoding];

当编码器结束之后,命令缓存区就会接受到2个命令.

         1) present

         2) commit

         因为GPU是不会直接绘制到屏幕上,因此你不给出去指令.是不会有任何内容渲染到屏幕上.

        [commandBuffer presentDrawable:view.currentDrawable];

        [commandBuffercommit];

        至此,Metal的准备工作已经完成

四.用Metal渲染一个简单的三角形

在做好上面的准备的准备工作后:


//初始化MTKView

- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView

{

    self= [superinit];

    if(self)

    {

        NSError*error =NULL;


        //1.获取GPU 设备

        _device= mtkView.device;

        //2.在项目中加载所有的(.metal)着色器文件

        // 从bundle中获取.metal文件

        id defaultLibrary = [_devicenewDefaultLibrary];

        //从库中加载顶点函数

        id vertexFunction = [defaultLibrarynewFunctionWithName:@"vertexShader"];

        //从库中加载片元函数

        id fragmentFunction = [defaultLibrarynewFunctionWithName:@"fragmentShader"];

        //3.配置用于创建管道状态的管道

        MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];

        //管道名称

        pipelineStateDescriptor.label=@"Simple Pipeline";

        //可编程函数,用于处理渲染过程中的各个顶点

        pipelineStateDescriptor.vertexFunction= vertexFunction;

        //可编程函数,用于处理渲染过程中各个片段/片元

        pipelineStateDescriptor.fragmentFunction= fragmentFunction;

        //一组存储颜色数据的组件

        pipelineStateDescriptor.colorAttachments[0].pixelFormat= mtkView.colorPixelFormat;


        //4.同步创建并返回渲染管线状态对象

        _pipelineState= [_devicenewRenderPipelineStateWithDescriptor:pipelineStateDescriptorerror:&error];

        //判断是否返回了管线状态对象

        if (!_pipelineState)

        {


            //如果我们没有正确设置管道描述符,则管道状态创建可能失败

            NSLog(@"Failed to created pipeline state, error %@", error);

            returnnil;

        }

        //5.创建命令队列

        _commandQueue = [_device newCommandQueue];

    }

    return self;

}

//每当视图需要渲染帧时调用

- (void)drawInMTKView:(nonnullMTKView*)view

{

    //1. 顶点数据/颜色数据

    staticconstCCVertextriangleVertices[] =

    {

        //顶点,    RGBA 颜色值

        { {  0.5, -0.25,0.0,1.0}, {1,0,0,1} },

        { { -0.5, -0.25,0.0,1.0}, {0,1,0,1} },

        { { -0.0f,0.25,0.0,1.0}, {0,0,1,1} },

    };

    //2.为当前渲染的每个渲染传递创建一个新的命令缓冲区

    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

    //指定缓存区名称

    commandBuffer.label=@"MyCommand";


    //3.

    // MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。

    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

    //判断渲染目标是否为空

    if(renderPassDescriptor !=nil)

    {

        //4.创建渲染命令编码器,这样我们才可以渲染到something

        id renderEncoder =[commandBufferrenderCommandEncoderWithDescriptor:renderPassDescriptor];

        //渲染器名称

        renderEncoder.label=@"MyRenderEncoder";

        //5.设置我们绘制的可绘制区域

        /*

        typedef struct {

            double originX, originY, width, height, znear, zfar;

        } MTLViewport;

         */

        //视口指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域

        //为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。

        MTLViewportviewPort = {

            0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0

        };

        [renderEncodersetViewport:viewPort];

        //[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];


        //6.设置当前渲染管道状态对象

        [renderEncodersetRenderPipelineState:_pipelineState];



        //7.从应用程序OC 代码 中发送数据给Metal 顶点着色器 函数

        //顶点数据+颜色数据

        //  1) 指向要传递给着色器的内存的指针

        //  2) 我们想要传递的数据的内存大小

        //  3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。

        [renderEncodersetVertexBytes:triangleVertices

                               length:sizeof(triangleVertices)

                              atIndex:CCVertexInputIndexVertices];

        //viewPortSize 数据

        //1) 发送到顶点着色函数中,视图大小

        //2) 视图大小内存空间大小

        //3) 对应的索引

        [renderEncodersetVertexBytes:&_viewportSize

                               length:sizeof(_viewportSize)

                              atIndex:CCVertexInputIndexViewportSize];



        //8.画出三角形的3个顶点

        // @method drawPrimitives:vertexStart:vertexCount:

        //@brief 在不使用索引列表的情况下,绘制图元

        //@param 绘制图形组装的基元类型

        //@param 从哪个位置数据开始绘制,一般为0

        //@param 每个图元的顶点个数,绘制的图型顶点数量

        /*

         MTLPrimitiveTypePoint = 0, 点

         MTLPrimitiveTypeLine = 1, 线段

         MTLPrimitiveTypeLineStrip = 2, 线环

         MTLPrimitiveTypeTriangle = 3,  三角形

         MTLPrimitiveTypeTriangleStrip = 4, 三角型扇

         */


        [renderEncoderdrawPrimitives:MTLPrimitiveTypeTriangle

                          vertexStart:0

                          vertexCount:3];

        //9.表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离

        [renderEncoderendEncoding];

        //10.一旦框架缓冲区完成,使用当前可绘制的进度表

        [commandBufferpresentDrawable:view.currentDrawable];

    }

    //11.最后,在这里完成渲染并将命令缓冲区推送到GPU

    [commandBuffercommit];

}



 

Metal文件:(语法下篇介绍)

#include 

//使用命名空间 Metal

using namespace metal;

// 导入Metal shader 代码和执行Metal API命令的C代码之间共享的头

#import "CCShaderTypes.h"

// 顶点着色器输出和片段着色器输入

//结构体

typedef struct

{

    //处理空间的顶点信息

    float4clipSpacePosition [[position]];

    //颜色

    float4color;

} RasterizerData;

//顶点着色函数

vertex RasterizerData

vertexShader(uintvertexID [[vertex_id]],

             constantCCVertex*vertices [[buffer(CCVertexInputIndexVertices)]],

             constantvector_uint2*viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])

{

    /*

     处理顶点数据:

        1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.

        2) 将顶点颜色值传递给返回值

     */


    //定义out

    RasterizerDataout; 

//    //初始化输出剪辑空间位置

//    out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);

//

//    // 索引到我们的数组位置以获得当前顶点

//    // 我们的位置是在像素维度中指定的.

//    float2 pixelSpacePosition = vertices[vertexID].position.xy;

//

//    //将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型

//    vector_float2 viewportSize = vector_float2(*viewportSizePointer);

//

//    //每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.

//    //计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.

//    out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);

    out.clipSpacePosition= vertices[vertexID].position;

    //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.

    out.color= vertices[vertexID].color;

    //完成! 将结构体传递到管道中下一个阶段:

    returnout;

}

//当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.

// 片元函数

//[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.

//一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.

//被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.

fragmentfloat4fragmentShader(RasterizerDatain [[stage_in]])

{

    //返回输入的片元颜色

    returnin.color;

}

用于OC和Metal桥接的文件:

/*

 介绍:

 头文件包含了 Metal shaders 与C/OBJC 源之间共享的类型和枚举常数

*/

#ifndef CCShaderTypes_h

#define CCShaderTypes_h

// 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用

typedef enum CCVertexInputIndex

{

    //顶点

    CCVertexInputIndexVertices    =0,

    //视图大小

    CCVertexInputIndexViewportSize =1,

} CCVertexInputIndex;

//结构体: 顶点/颜色值

typedef struct

{

    // 像素空间的位置

    // 像素中心点(100,100)

    vector_float4 position;

    // RGBA颜色

    vector_float4 color;

} CCVertex;

#endif


作者:枫紫
链接:https://www.jianshu.com/p/a6f3c90d6ba5





收起阅读 »

iOS KVO底层原理&&KVO的isa指向

一.简单复习一下KVO的使用定义一个类,继承自NSObject,并添加一个name的属性#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface TCPerson ...
继续阅读 »

一.简单复习一下KVO的使用

  • 定义一个类,继承自NSObject,并添加一个name的属性
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END

  • 在ViewController我们简单的使用一下KVO
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end


当点击屏幕的时候,控制台输出:

2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到<TCPerson: 0x600003444d10>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

二.深入剖析KVO的底层

  • 在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.name = @"cang lao shi";
    }我们知道self.person1.name的本质是[self.person1 setName:@"cang lao shi"];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}
在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:

- (void)setName:(NSString *)name{
_name = name;
}

  • 在ViewController我们新建一个person2,代码变成了:
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];

self.person2.name = @"ttttttttt";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

  • 注意:当我们点击屏幕的时候输出的结果是:

2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到<TCPerson: 0x600002ce8230>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

  • 既然我们改变name的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?



三.KVO的isa指向

  • 上篇文章中我分析了实例对象,类对象,元类对象的isa,既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:
    打开lldb

(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
  • 从上面的打印我们看到 self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson
  • NSKVONotifying_TCPerson是runtime动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面代码不能在xcode中运行):

  • #import "NSKVONotifying_TCPerson.h"

    @implementation NSKVONotifying_TCPerson
    //NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
    - (void)setName:(NSString *)name{
    _NSSetIntVaueAndNotify();
    }
    //改变过程
    void _NSSetIntVaueAndNotify(){
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
    }
    //通知观察者
    - (void)didChangeValueForKey:(NSString *key){
    [observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context];
    }
    @end
    未添加观察self.person2实例对象的isa指向流程图:



    添加观察self.person1实例对象的isa指向流程图:




    所以KVO其本质是动态生成一个NSKVONotifying_TCPerson
    类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/0b6083b91b04
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。







    收起阅读 »

    View系列:事件分发(二)

    滑动冲突常见场景:内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)内外层滑动方向一致(如:RecyclerView嵌套)一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截父View事件发送方,父...
    继续阅读 »

    滑动冲突

    常见场景:

    1. 内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)
    2. 内外层滑动方向一致(如:RecyclerView嵌套)

    image-20210602150942026

    一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截

    父View

    事件发送方,父View拦截。

    父View根据自己的需求,选择在何时给onInterceptTouchEvent返回true,使事件直接分发给自己处理(前提:子View未设置requestDisallowInteceptTouchEvent(true),否则根本就不会经过onInterceptTouchEvent方法)。

    • DOWN不要拦截,否则根据事件分发逻辑,事件直接给父View自己处理了
    • UP不要拦截,否则子View无法出发click事件,无法移除longClick消息
    • 在MOVE中根据逻辑需求判断是否拦截
        public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
    intercepted = false;
    break;
    }
    case MotionEvent.ACTION_UP: {
    intercepted = false;
    break;
    }
    case MotionEvent.ACTION_MOVE: {
    if (满足父容器的拦截要求) {
    intercepted = true;
    } else {
    intercepted = false;
    }
    break;
    }
    }
    return intercepted;
    }

    子View

    事件接收方,内部拦截

    事件已经传递到子View,子View只有选择是否消费该事件,或者向父View申请拦截事件。

    注意:申请拦截事件,不代表就以后就收不到事件了。request只是会清除FLAG_DISALLOW_INTERCEPT标记,导致父View检查onInterceptTouchEvent方法,仅此而已(恢复到默认状态)。主要看父View.onInterceptTouchEvent中的返回值。

        public boolean dispatchTouchEvent(MotionEvent event) {//或 onTouchEvent
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
    parent.requestDisallowInterceptTouchEvent(true);//不许拦截
    break;
    }
    case MotionEvent.ACTION_MOVE: {
    int deltaX = x - mLastX;
    int deltaY = y - mLastY;
    if (父容器需要此类点击事件) {
    parent.requestDisallowInterceptTouchEvent(false);//申请拦截
    }
    break;
    }
    case MotionEvent.ACTION_UP: {
    break;
    }
    }
    return super.dispatchTouchEvent(event);
    }

    :cry:多点触控

    安卓自定义View进阶-多点触控详解

    自由地对图片进行缩放和移动

    多点触控相关的事件:

    事件简介
    ACTION_DOWN第一个 手指 初次接触到屏幕 时触发。
    ACTION_MOVE手指 在屏幕上滑动 时触发,会多次触发(单个或多个手指)。
    ACTION_UP最后一个 手指 离开屏幕时触发。
    ACTION_POINTER_DOWN有非主要的手指按下(即按下之前已经有手指在屏幕上)。
    ACTION_POINTER_UP有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。
    以下事件类型不推荐使用---以下事件在2.0开始,在 2.2 版本以上被废弃---
    ACTION_POINTER_1_DOWN第 2 个手指按下,已废弃,不推荐使用。
    ACTION_POINTER_2_DOWN第 3 个手指按下,已废弃,不推荐使用。
    ACTION_POINTER_3_DOWN第 4 个手指按下,已废弃,不推荐使用。
    ACTION_POINTER_1_UP第 2 个手指抬起,已废弃,不推荐使用。
    ACTION_POINTER_2_UP第 3 个手指抬起,已废弃,不推荐使用。
    ACTION_POINTER_3_UP第 4 个手指抬起,已废弃,不推荐使用。

    多点触控相关的方法:

    方法简介
    getActionMasked()与 getAction() 类似,多点触控需要使用这个方法获取事件类型
    getActionIndex()获取该事件是哪个指针(手指)产生的。
    getPointerCount()获取在屏幕上手指的个数。
    getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
    findPointerIndex(int pointerId)通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。
    getX(int pointerIndex)获取某一个指针(手指)的X坐标
    getY(int pointerIndex)获取某一个指针(手指)的Y坐标

    index和pointId

    在 2.2 版本以上,我们可以通过getActionIndex() 轻松获取到事件的索引(Index),Index 变化有以下几个特点:

    1、从 0 开始,自动增长。 2、之前落下的手指抬起,后面手指的 Index 会随之减小。 (0、1、2 --> 第2个手指抬起 --> 第三个手指变为1 --> 0、1) 3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。 4、对 move 事件无效。 **getActionIndex()**获取到的始终是数值 0

    相同点不同点
    1. 从 0 开始,自动增长。
    2. 落下手指时优先填补空缺(填补之前抬起手指的编号)。
    Index 会变化,pointId 始终不变。

    pointerIndex 与 pointerId

    pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。

    类型简介
    pointerIndex用于获取具体事件,可能会随着其他手指的抬起和落下而变化
    pointerId用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变

    这两个数值使用以下两个方法相互转换:

    方法简介
    getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
    findPointerIndex(int pointerId)通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。

    自定义View示例

    img
    /**
    * Created by Varmin
    * on 2017/7/5 16:16.
    * 文件描述:left,content,right三个tag,在布局中给每个部分设置该tag。用于该ViewGroup内部给子View排序。
    * 功能:默认全部关闭左右滑动。分别设置打开
    */
    public class SlideView extends ViewGroup implements View.OnClickListener, View.OnLongClickListener {
    private static final String TAG = "SlideView";
    public final String LEFT = "left";
    public final String CONTENT = "content";
    public final String RIGHT = "right";
    private Scroller mScroller;
    /**
    * scroller滑动时间。默认250ms
    */
    public static final int DEFAULT_TIMEOUT = 250;
    public static final int SLOW_TIMEOUT = 500;
    /**
    * 左右View的宽度
    */
    private int leftWidth;
    private int rightWidth;
    private GestureDetector mGesture;
    private ViewConfiguration mViewConfig;

    public SlideView(Context context) {
    super(context);
    init(context);
    }

    public SlideView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
    }

    public SlideView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
    }

    private void init(Context context) {
    mScroller = new Scroller(context);
    //都是自己处理的,这里没有用到该手势方法
    //缺点:误差有些大。这种精确滑动的,最好自己判断
    mGesture = new GestureDetector(context, new SlideGestureDetector());
    mViewConfig = ViewConfiguration.get(context);
    //默认false
    setClickable(true);
    }

    /**
    * 所有的子View都映射完xml,该方法最早能获取到childCount
    * 在onMeasuer/onLayout中获取,注册监听的话,会多次被调用
    * 在构造方法中,不能获取到childCount。
    */
    @Override
    protected void onFinishInflate() {
    super.onFinishInflate();
    initListener();
    }

    private void initListener() {
    for (int i = 0; i < getChildCount(); i++) {
    View childView = getChildAt(i);
    childView.setClickable(true);
    childView.setOnClickListener(this);
    if (CONTENT.equals(childView.getTag())) {
    childView.setOnLongClickListener(this);
    }
    }

    }
    @Override
    public void onClick(View v) {
    String tag = (String) v.getTag();
    switch (tag) {
    case LEFT:
    Toast.makeText(getContext(), "Left", Toast.LENGTH_SHORT).show();
    break;
    case CONTENT:
    Toast.makeText(getContext(), "Content", Toast.LENGTH_SHORT).show();
    closeAll(SLOW_TIMEOUT);
    break;
    case RIGHT:
    Toast.makeText(getContext(), "Right", Toast.LENGTH_SHORT).show();
    break;
    }
    }

    @Override
    public boolean onLongClick(View v) {
    Toast.makeText(getContext(), "Content_LongClick", Toast.LENGTH_SHORT).show();
    return true;
    }

    /**
    * 每个View的大小都是由父容器给自己传递mode来确定。
    * 每个View的位置都是由父容器给自己设定好自己在容器中的左上右下来确定位置。
    * 所以,继承至ViewGroup的容器,要在自己内部实现对子View大小和位置的确定。
    */

    /**
    * 子View不会自己测量自己的,所以在这里测量各个子View大小
    * 另外,处理自己是wrap的情况,给自己一个确定的值。
    */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    //测量子View
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    //测量自己
    //默认是给该ViewGroup设置固定宽高,假设不纯在wrap情况,onlayout中也不考虑此情况
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
    View childView = getChildAt(i);
    int childWidth = childView.getMeasuredWidth();
    int childHeight = childView.getMeasuredHeight();
    String tag = (String) childView.getTag();
    switch (tag) {
    case LEFT:
    leftWidth = childWidth;
    childView.layout(-childWidth, 0, 0, childHeight);
    break;
    case CONTENT:
    childView.layout(0, 0, childWidth, childHeight);
    break;
    case RIGHT:
    rightWidth = childWidth;
    childView.layout(getMeasuredWidth(), 0,
    getMeasuredWidth() + childWidth, childHeight);
    break;
    }
    }

    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean handled = super.onInterceptTouchEvent(ev);
    if (handled) {
    return true;
    }
    switch (ev.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
    mInitX = (int) ev.getX();
    mInitY = (int) ev.getY();
    break;
    case MotionEvent.ACTION_MOVE:
    int offsetX = (int) (ev.getX() - mInitX);
    int offsetY = (int) (ev.getY() - mInitY);
    /**
    * 判断可以横向滑动了
    * 1,拦截自己的子View接收事件
    * 2,申请父ViewGroup不要看拦截事件。
    */
    if ((Math.abs(offsetX) - Math.abs(offsetY)) > mViewConfig.getScaledTouchSlop()) {
    requestDisallowInterceptTouchEvent(true);
    return true;
    }
    break;
    case MotionEvent.ACTION_UP:
    //重置回ViewGroup默认的拦截状态
    requestDisallowInterceptTouchEvent(false);
    break;
    }
    return handled;
    }

    private int mInitX;
    private int mOffsetX;
    private int mInitY;
    private int mOffsetY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    boolean handled = false;
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
    break;
    case MotionEvent.ACTION_MOVE:
    mOffsetX = (int) (event.getX() - mInitX);
    mOffsetY = (int) (event.getY() - mInitY);
    if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > 0) {//横向触发条件
    //预估,偏移offsetX后的大小
    int mScrollX = getScrollX() + (-mOffsetX);
    if (mScrollX <= 0) {//向右滑动,显示leftView:110
    //上面的是预估,如果预估大于目标:你不能return放弃了,要调整mOffsetX的值使其刚好等于目标
    if (Math.abs(mScrollX) > leftWidth) {
    mOffsetX = leftWidth - Math.abs(getScrollX());
    //return true;
    }
    }else {//向左滑动,显示rightView:135
    if (mScrollX > rightWidth) {
    mOffsetX = getScrollX() - rightWidth;
    //return true;
    }
    }
    this.scrollBy(-mOffsetX,0);
    mInitX = (int) event.getX();
    mInitY = (int) event.getY();
    return true;
    }

    break;
    case MotionEvent.ACTION_UP:
    int upScrollX = getScrollX();
    if (upScrollX > 0) {//向左滑动,显示rightView
    if (upScrollX >= (rightWidth/2)) {
    mOffsetX = upScrollX - rightWidth;
    }else {
    mOffsetX = upScrollX;
    }
    }else {//向右,显示leftView
    if (Math.abs(upScrollX) >= (leftWidth/2)) {
    mOffsetX = leftWidth - Math.abs(upScrollX);
    }else {
    mOffsetX = upScrollX;
    }
    }
    // this.scrollBy(-mOffsetX,0);//太快
    // startScroll(-mOffsetX, 0, 1000);//直接放进去,不行?
    /**
    * 注意startX。dx表示的是距离,不是目标位置
    */
    mScroller.startScroll(getScrollX(), getScrollY(), -mOffsetX, 0,SLOW_TIMEOUT);
    invalidate();

    break;
    }

    if (!handled) {
    handled = super.onTouchEvent(event);
    }
    return handled;
    }


    @Override
    public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
    scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    invalidate();
    }
    }


    /**
    * 虽然传入的dx、dy并不是scrollTo实际要到的点,dx,dy只是一小段距离。
    * 但是computeScroll()我们scrollTo的是:现在位置+dx的距离 = 目标位置
    *
    * @param dx //TODO *距离!距离!并不是说要到达的目标。*
    * @param dy
    * @param duration 默认的滑动时间是250,复位的时候如果感觉太快可以自己设置事件.
    *
    */
    private void startScroll(int dx, int dy, int duration) {
    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
    //mScroller.extendDuration(duration); 在250ms基础上增加。构造函数传入的话,就是duration的时间。
    invalidate();
    }


    /**
    * 是否打开,ListView中复用关闭
    * @return
    */
    public boolean isOpened(){
    return getScrollX() != 0;
    }
    public void closeAll(int duration){
    mScroller.startScroll(getScrollX(), getScrollY(), (-getScrollX()), 0, duration);
    invalidate();
    }
    }

    Tips

    scrollTo/By

    通过三种方式可以实现View的滑动:

    1. 通过View本身提供的scrollTo/scrollBy方法;

    2. 通过动画使Veiw平移。

    3. 通过改变View的LayoutParams属性值。

    **setScrollX/Y、scrollTo: **移动到x,y的位置

    **scrollBy: **移动x,y像素的距离

        public void setScrollX(int value) {
    scrollTo(value, mScrollY);
    }

    public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
    }

    public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
    int oldX = mScrollX;
    int oldY = mScrollY;
    mScrollX = x;
    mScrollY = y;
    invalidateParentCaches();
    onScrollChanged(mScrollX, mScrollY, oldX, oldY);
    }
    }

    **注意:**假如scrollTo(30,10),按照View右下正,左上负的概念,因该是向右滑动30,向下滑动10。


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

    收起阅读 »

    View系列:事件分发(一)

    基础相关View坐标系MotionEvent当用户触摸屏幕的时候,产生Touch事件,事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象事件类型具体动作MotionEvent.ACTION_DOWN按下View(所有事件的开始)Moti...
    继续阅读 »

    基础相关

    View坐标系

    View坐标系

    MotionEvent

    当用户触摸屏幕的时候,产生Touch事件,事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象

    image-20210531100221285

    image-20210531100250617

    事件类型具体动作
    MotionEvent.ACTION_DOWN按下View(所有事件的开始)
    MotionEvent.ACTION_MOVE滑动View
    MotionEvent.ACTION_UP抬起View(与DOWN对应)
    MotionEvent.ACTION_CANCEL结束事件
    MotionEvent.ACTION_OUTSIDE事件发生在视图范围外

    辅助类

    辅助类-dev

    View触摸相关工具类全解

    ViewConfiguration

    获取 Android 系统常用的距离、速度、时间等常量

    VelocityTracker

    跟踪触摸事件的速度。此设置对于手势标准中包含速度的手势(例如滑动)非常有用。

    GestureDetector

    手势检测,该类支持的一些手势包括 onDown()、onLongPress()、onFling() 等。可以将 GestureDetector 与onTouchEvent() 方法结合使用。

    OverScroller

    回弹工具类,不同的回弹效果可以自定义不同的动画插值器

    TouchDelegate

    扩展子视图的可轻触区域

    img

    view1.post(new Runnable() {
    @Override
    public void run() {
    Rect bounds = new Rect();
    // 获取View2占据的矩形区域在其父View(也就是View1)中的相对坐标
    view2.getHitRect(bounds);
    // 计算扩展后的矩形区域Bounds相对于View1的坐标
    bounds.left -= 100;
    bounds.top -= 50;
    bounds.right += 100;
    bounds.bottom += 50;
    TouchDelegate touchDelegate = new TouchDelegate(bounds, view2);
    // 为View1设置TouchDelegate
    view1.setTouchDelegate(touchDelegate);
    }
    });

    事件处理

    image-20210531100411928

    • 每一个DOWN / MOVE / UP / CANCLE都是一个事件,并不是连起来才是一个事件
    • 事件的消费,是看返回true/false,而不是看有没有处理操作
    • Activity、ViewGroup、View
      • 都有分发、消费事件的能力
      • 只有ViewGroup有拦截事件的能力

    事件分发

    window中的View是树形结构,可能会重叠在一起,当我们点击的区域有多个View都可以响应的时候,事件分发机制决定了这个点击事件应该给谁处理。

    分发机制类似洋葱模型、责任链模式、冒泡...

    分发:Activity -> PhoneWindow -> DecorView -> ViewGroup ->  @1 -> ... -> View
    消费:Activity <- PhoneWindow <- DecorView <- ViewGroup <- @1 <- ... <- View
    • 如果事件被消费,就意味着事件信息传递终止 如果在@1处消费事件,就不在往下传递了,直接返回
    • 如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃事

    image

    View

    优先级:

    1. OnTouchListener.onTouch
    2. onTouchEven

    注意:OnTouchListener.onTouch返回false,并不代表该View不消费事件了,得看dispatchTouchEvent返回的结果

    public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    // 被遮盖,不响应事件
    if (onFilterTouchEventForSecurity(event)) {
    ...
    //setOnTouchListener设置的监听,优先级高
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
    && (mViewFlags & ENABLED_MASK) == ENABLED
    && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
    }

    // 系统已实现好的,优先级低。
    if (!result && onTouchEvent(event)) {
    result = true;
    }
    }
    ...
    return result;
    }

    onTouchEvent:

    • View即使设置了setEnable(false),只要是可点击状态就会消费事件,只是不做出回应
    • 只要进入CLICKABLE判断,就返回true消费时间
    事件处理
    DOWN发送LongClick延迟消息,过期触发
    MOVE移除LongClick消息
    CANCLE移除LongClick消息
    UP移除LongClick消息
    触发Click事件
    <!--只关注事件的分发,不关注其它状态的变化-->
    public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int action = event.getAction();

    //View被禁用的话,如果是可以点击的,一样返回true,表示消费了事件。只是不作出回应。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
    return (((viewFlags & CLICKABLE) == CLICKABLE
    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    // 委托:扩大点击事件、委托其它处理
    if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
    return true;
    }
    }

    /**
    * 只要进入该if,就返回true,消费事件
    */

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
    case MotionEvent.ACTION_DOWN:
    if (isInScrollingContainer) {
    } else {
    //长按事件,发送延时消息到队列
    checkForLongClick(0, x, y);
    }
    break;
    case MotionEvent.ACTION_MOVE:
    if (!pointInView(x, y, mTouchSlop)) {
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    //移除长按事件的消息。
    removeLongPressCallback();
    setPressed(false);
    }
    }
    break;
    case MotionEvent.ACTION_UP:
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    // 移除长按事件的消息
    removeLongPressCallback();

    //点击事件: 可知onclick事件是在UP的时候触发
    if (!focusTaken) {
    if (!post(mPerformClick)) {
    performClick();
    }
    }
    }
    }
    break;
    case MotionEvent.ACTION_CANCEL:
    //移除长按事件
    removeLongPressCallback();
    mHasPerformedLongPress = false;
    break;
    }
    return true;
    }

    return false;
    }

    ViewGroup

    1. DOWN事件:
      • 清除之前状态,mFirstTouchTarget = null
      • 进入逻辑1、2寻找接收事件的子View
        • mFirstTouchTarget = null,进入逻辑3
        • mFirstTouchTarget != null, 进入逻辑4
    2. MOVE/UP事件:
      • mFirstTouchTarget = null,注释1处不满足逻辑1判断条件,进入逻辑3
      • mFirstTouchTarget != null,不满足逻辑2判断条件,进入逻辑4
    3. CANCLE事件:
      • mFirstTouchTarget = null,注释2处不满足逻辑1判断条件,进入逻辑3
      • mFirstTouchTarget != null,注释2处不满足逻辑1判断条件,进入逻辑4

    总结,

    • DOWN事件就是用来清理状态、寻找新接收事件子View的

    • DOWN事件的后续事件:

      • 未找到子View接收情况下,直接自己处理
      • 找到子View接收的情况下,直接给子View
        @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    ....
    // 如果该View被遮蔽,并且在被遮蔽时不响应点击事件,则不分发该触摸事件,即返回false。
    if (onFilterTouchEventForSecurity(ev)) {
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    /**
    * step1:DOWN事件的时候,表示最初开始事件,清除之前的状态。
    */

    if (actionMasked == MotionEvent.ACTION_DOWN) {
    // 关键:每次DOWN的时候,清除前一个手势的mFirstTouchTarget = null
    cancelAndClearTouchTargets(ev);
    // 清除状态
    resetTouchState();
    }


    /**
    * step2:拦截判断
    */

    final boolean intercepted;
    // ACTION_DOWN(初始状态)或 有子View处理事件:判断是否拦截
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    //默认没有该标记位,返回false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
    //默认返回false,并不是每次都会调用
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action);
    } else {//requestDisallowInterceptTouchEvent(true)
    intercepted = false;
    }
    } else {
    //[注释1],没有子View接收事件,拦截
    intercepted = true;
    }


    /**
    * step3:找能接收事件的子View,并赋值给mFirstTouchTarget
    */

    final boolean canceled = resetCancelNextUpFlag(this)
    || actionMasked == MotionEvent.ACTION_CANCEL; //[注释2]
    // *****每次都会初始化这两个变量****
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    //如果在这一层不满足判断条件,直接就到[逻辑3,4]了。
    //[逻辑1]


    /*
    step4:到这,已经跳出了上面的大嵌套判断!--上面的大嵌套就是用来找接收事件的子View的。
    一旦确定找到了或者没有接收者,后面的事件:
    1. 检查intercepte状态。
    2. 进入下面的逻辑,后面的事件直接确定分发给谁
    */

    // 没有找到接收事件的View,以后的move/up也通过这一步给ViewGroup
    [逻辑3] if (mFirstTouchTarget == null) {
    //没有接收事件的子View,调用自己的dispatchTouchEvent
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
    TouchTarget.ALL_POINTER_IDS);
    [逻辑4] } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    final TouchTarget next = target.next;
    // 在DOWN找到接受事件的子View时,赋值alreadyDispatchedToNewTouchTarget = true
    // 此时已经消费了事件,所以直接返回true
    // 后面的其它事件中,alreadyDispatchedToNewTouchTarget被重置,不在满足该条件
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted;
    // 子View消费事件
    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    }
    }
    }
    }
    return handled;
    }

    Activity

    Touch事件先是传递到Activity,接着由Activity传递到最外层布局,然后一层层遍历循环到View

        public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    // 交互 空实现
    onUserInteraction();
    }
    // DecorView实际是ViewGroup的dispatchTouchEvent方法
    if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
    }
    // down点击到外部区域,消费事件,finish
    return onTouchEvent(ev);
    }

    onUserInteraction()

    这是一个空实现,用的也比较少,不深究: 此方法是activity的方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。

    onTouchEvent(event)

        public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
    finish();
    return true;
    }
    return false;
    }

    mWindow即使PhoneWindow,该方法是@hide,并且在Window类中定义。

        /** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
    && isOutOfBounds(context, event) && peekDecorView() != null) {
    return true;
    }
    return false;
    }
    • mCloseOnTouchOutside是一个boolean变量,它是由Window的android:windowCloseOnTouchOutside属性值决定。
    • isOutOfBounds(context, event)是判断该event的坐标是否在context(对于本文来说就是当前的Activity)之外。是的话,返回true;否则,返回false。
    • peekDecorView()则是返回PhoneWindow的mDecor。

    总的来说:如果设置了android:windowCloseOnTouchOutside为true,并且是DOWN事件点击了Activity外部区域(比如Activity是一个Dialog),返回true,消费事件,并且finish。

    ACTION_CANCEL

    子View在接收事件过程中,被中断,父View会传给子View一个CANCEL事件

     [逻辑4]      } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted; //注释1

    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    //...
    }
    }
    }


    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
    //发送CANCEL事件给子View
    handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
    }
    //...
    }

    ACTION_OUTSIDE

    设置了FLAG_WATCH_OUTSIDE_TOUCH,事件发生在当前视图的范围之外

    例如,点击音量键之外的区域取消音量键显示:

    //frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java        
    // 给音量键Window设置FLAG_WATCH_OUTSIDE_TOUCH
    mDialog = new CustomDialog(mContext);
    mWindow = mDialog.getWindow();
    mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
    | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH //设置Window Flag
    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
    ......

    // 重写onTouchEvent并处理ACTION_OUTSIDE事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (mShowing) {
    if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
    dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
    return true;
    }
    }
    return false;
    }

    事件拦截

    一文解决Android View滑动冲突

    只有ViewGroup有事件拦截的能力,View可根据情况申请父View进行拦截

    image-20210531100411928

    View

    View没有拦截事件的能力,只能根据不同需求调用mParent.requestDisallInterceptTouchEvent(true/false) 申请父View是否进行拦截。

    注意:如果在子View接收事件的过程中被父View拦截,父View会给子View一个CANCEL事件,注意处理相关逻辑。

    ViewGroup

    onInterceptTouchEvent
    • 设置了FLAG_DISALLOW_INTERCEPT标记时,不会调用
    • 其它时候都会调用
        /**
    * ViewGroup事件分发时的拦截检查机制
    */

    //默认没有该标记位,返回false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//注释1
    if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
    intercepted = onInterceptTouchEvent(ev);//默认返回false
    } else {
    intercepted = false;//requestDisallowInterceptTouchEvent(true)
    }


    /**
    * 默认返回false
    */

    public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
    && ev.getAction() == MotionEvent.ACTION_DOWN
    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
    return true;
    }
    return false;
    }

    /*
    * disallowIntercept = true时,不允许拦截,注释1为true
    * disallowIntercept = false时,允许拦截,注释1为false
    */

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    // We're already in this state, assume our ancestors are too
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
    return;
    }

    if (disallowIntercept) {
    mGroupFlags |= FLAG_DISALLOW_INTERCEPT;// 添加标记,使得注释1为true
    } else {
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;// 清除标记,使得注释1为false
    }

    if (mParent != null) {
    mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
    }
    requestDisallowInterceptTouchEvent
    • true,不允许拦截,注释1为true,不会调用onInterceptTouchEvent
    • false,允许拦截,注释1为false(默认),调用onInterceptTouchEvent

    注意:调用requestDisallowInterceptTouchEvent(false)申请拦截,并不会真的就被父View拦截了。它只是一个标记,使得父View会检查onInterceptTouchEvent这个方法(默认也会调用)。 它只会影响 mGroupFlags & FLAG_DISALLOW_INTERCEPT值,真正决定要不要被拦截是看 onInterceptTouchEvent的返回值。如果为true:

    在注释1处cancelChild = true,会导致给子类发送CANCEL事件,然后修改mFirstTouchTarget,不再给子View传递事件。

    [逻辑4]      } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted; //注释1
    // 子View消费事件
    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    }
    }
    }

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
    handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
    }
    //...
    }

    Activity

    Activity没有onInterceptTouchEvent方法,也没有mParent,不具备主动或被动拦截能力

    收起阅读 »

    View系列:动画

    View Animation(视图动画)最大的特点是:并没有改变目标实际的属性(宽高/位置等)。例如:移动后,点击原来的位置出发点击事件;移动后再旋转,还是回到原来的位置旋转。Tween Animation(补间动画)锚点可以是数值、百分数、百分数p三种样式,...
    继续阅读 »

    View Animation(视图动画)

    最大的特点是:并没有改变目标实际的属性(宽高/位置等)。例如:移动后,点击原来的位置出发点击事件;移动后再旋转,还是回到原来的位置旋转。

    Tween Animation(补间动画)

    锚点

    可以是数值、百分数、百分数p三种样式,比如50、50%、50%p。[不是只有pivotx/y才可以用这3中样式,其它变换的属性也可以]

    • 当为数值时,表示在当前View的左上角,即原点处加上50px,做为起始缩放点;
    • 如果是50%,表示在当前控件的左上角加上自己宽度的50%做为起始点;
    • 如果是50%p,那么就是表示在当前的左上角加上父控件宽度的50%做为起始点x轴坐标(是在目标的左上角原点加上相对于父控件宽度的距离,不是锚点在父控件的那个位置)。

    fromX/toX等等类型的数据也可以用上面的3中数据 类型,只不过有的不适合。比如scale用%p就没意义了。养成好习惯,只在锚点的属性上随便用这3中类型,from/to属性分清类型用相应的数值(浮点倍数/角度...)。

    从Animation继承的属性
    android:duration 动画持续时间,以毫秒为单位 
    android:fillAfter 如果设置为true,控件动画结束时,将保持动画最后时的状态
    android:fillBefore 如果设置为true,控件动画结束时,还原到开始动画前的状态
    android:fillEnabled 与android:fillBefore 效果相同,都是在动画结束时,将控件还原到初始化状态
    android:repeatCount 重复次数
    android:repeatMode 重复类型,有reverse和restart两个值,reverse表示倒序回放,restart表示重新放一遍,必须与repeatCount一起使用才能看到效果。因为这里的意义是重复的类型,即回放时的动作。
    android:interpolator 设定插值器,其实就是指定的动作效果,比如弹跳效果等,不在这小节中讲解,后面会单独列出一单讲解。
    scale
    <?xml version="1.0" encoding="utf-8"?>
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:duration="700"
    android:fromXScale="50%" //也可以用上面的3中类型
    android:fromYScale="50%"
    android:toXScale="200%"
    android:toYScale="200%"
    android:pivotX="0.5"
    android:pivotY="0.5"
    android:repeatCount = "2"
    android:repeatMode = "reverse"
    android:fillAfter = "true"
    />

    alpha
    <?xml version="1.0" encoding="utf-8"?>
    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:fromAlpha="0.1"
    android:toAlpha="1"
    android:duration="1500"
    android:repeatMode = "reverse"
    android:repeatCount = "2"
    android:fillAfter = "true"
    >

    </alpha>
    rotate
    <?xml version="1.0" encoding="utf-8"?>
    <rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:fromDegrees="0"
    android:toDegrees="270"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="700"
    android:repeatMode = "reverse"
    android:repeatCount = "3"
    android:fillAfter = "true"
    >

    </rotate>
    translate
    <?xml version="1.0" encoding="utf-8"?>
    <translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:duration="700"
    android:fillAfter="true"
    android:fromXDelta="50"
    android:fromYDelta="50%p"
    android:repeatCount="3"
    android:repeatMode="reverse"
    android:toXDelta="70%p"
    android:toYDelta="80%p">

    </translate>
    AnimationSet animSet = new AnimationSet(false);
    Animation scaleAnim = AnimationUtils.loadAnimation(this, R.anim.scale_anim); //资源文件
    Animation rotateAnim = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
    AlphaAnimation alphaAnim = new AlphaAnimation(0.2f, 1.0f); //代码生成
    //valueType 3中类型的数据(px, 自身%, 父类%p),这里已自身为参照物。
    TranslateAnimation traslateAnim = new TranslateAnimation(
    Animation.RELATIVE_TO_SELF, 0.2f,
    Animation.RELATIVE_TO_SELF, 3.0f,
    Animation.RELATIVE_TO_SELF, 0f,
    Animation.RELATIVE_TO_SELF, 1.0f);
    ivTarget.startAnimation(animSet);
    自定义Animation
    private class MoveAnimation extends Animation {
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
    super.applyTransformation(interpolatedTime, t);
    mInterpolatedTime = interpolatedTime;
    invalidate();
    }
    }

    Frame Animation(逐帧动画)

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    //false 一直重复执行,true执行一次。
    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_1"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_2"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_3"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_4"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_4"/>

    </animation-list>
    • 需要注意的是,动画的启动需要在view和window建立连接后才可以绘制,比如上面代码是在用户触摸后启动。如果我们需要打开界面就启动动画的话,则可以在Activity的onWindowFocusChanged()方法中启动。

    Property Animation(属性动画)

    属性动画是指通过改变View属性来实现动画效果,包括:ValueAnimator、ObjectAnimator、TimeAnimator

    ValueAnimator

    该类主要针对数值进行改变,不对View进行操作

    ValueAnimator animator = ValueAnimator.ofInt(0,400);  
    animator.setDuration(1000);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    //拿到监听结果,自己处理。
    int curValue = (int)animation.getAnimatedValue();
    tvTextView.layout(curValue,curValue,curValue+tv.getWidth(),curValue+tv.getHeight());
    }
    });
    animator.setInterpolator(new LinearInterpolator());
    animator.start();

    监听:

    /**
    * 监听器一:监听动画变化时的实时值
    * 添加方法为:public void addUpdateListener(AnimatorUpdateListener listener)
    */

    public static interface AnimatorUpdateListener {
    void onAnimationUpdate(ValueAnimator animation);
    }
    /**
    * 监听器二:监听动画变化时四个状态
    * 添加方法为: public void addListener(AnimatorListener listener)
    */

    public static interface AnimatorListener {
    void onAnimationStart(Animator animation);
    void onAnimationEnd(Animator animation);
    void onAnimationCancel(Animator animation);
    void onAnimationRepeat(Animator animation);
    }


    /**
    * 移除AnimatorUpdateListener
    */

    void removeUpdateListener(AnimatorUpdateListener listener);
    void removeAllUpdateListeners();
    /**
    * 移除AnimatorListener
    */

    void removeListener(AnimatorListener listener);
    void removeAllListeners();

    ObjectAnimator

    ValueAnimator只能对数值进行计算,不能直接操作View,需要我们在监听器中自己去操作控件。这样就有点麻烦了,于是Google在ValueAmimator的基础上又派生出了ObjerctAnimator类,让动画直接与控件关联起来。

     	ObjectAnimator rotateObject = ObjectAnimator.ofFloat(tvPropertyTarget, 
    "Rotation",
    0, 20, -20, 40, -40, 0);
    rotateObject.setDuration(2000);
    rotateObject.start();
    setter/getter 属性名

    在View中已经实现了一些属性的setter/getter方法,在构造动画时可以直接对控件使用。

    • 要使用一个属性,必须在控件中有对应的setter/getter方法,属性setter/getter方法的命名必须以驼峰方式
    • ObjectAnimator在使用该属性的时候,会把setter/getter和属性第一个字母大写转换后的字段拼接成方法名,通过反射的方式调用该方法传值。 所以,上文中"Rotation/rotation"可以首字母可以大小写都行
    //1、透明度:alpha  
    public void setAlpha(float alpha)

    //2、旋转度数:rotation、rotationX、rotationY
    public void setRotation(float rotation) //围绕Z轴旋转
    public void setRotationX(float rotationX)
    public void setRotationY(float rotationY)

    //3、平移:translationX、translationY
    public void setTranslationX(float translationX)
    public void setTranslationY(float translationY)

    //缩放:scaleX、scaleY
    public void setScaleX(float scaleX)
    public void setScaleY(float scaleY)

    image-20210603130556023

    自定义属性做动画
    public class PointView extends View {
    private float mRadius = 0;
    public PointView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
    }

    public void setRadius(float radius){
    this.mRadius = radius;
    invalidate();
    }

    public float getRadius(){
    return mRadius;
    }
    }

    //radius属性首字母大小写无所谓,最后都是要转成大些的。
    ObjectAnimator pointAnim = ObjectAnimator.ofFloat(pointPropertyAnim,
    "Radius",
    10, 40, 40, 80, 60, 100, 80, 120,60);
    pointAnim.start();

    什么时候需要用到get方法呢? 前面构造动画时传入的取值范围都是多个参数,Animator知道是从哪个值变化到哪个值。当只传入一个参数的时候,Animator怎么知道哪里是起点?这时通过get方法找到初始值。 如果没有找到get方法,会用该参数类型的默认初始值复制。如:ofInt方法传入一个值,找不到get方法时,默认给的初始值是Int类型的初始值0.

    原理

    image-20210603131108900ObjectAnimator的方便之处在于:

    ValueAnimator只负责把数值给监听器,ObjectAnimator只负责调用set方法。至于实现,都是靠我们自己或者set中的方法。

    插值器

    设置动画运行过程中的进度比例,类似匀速变化、加速变化、回弹等

    • 参数input:是一个float类型,它取值范围是0到1,表示当前动画的进度,取0时表示动画刚开始,取1时表示动画结束,取0.5时表示动画中间的位置,其它类推。
    • 返回值:表示当前实际想要显示的进度。取值可以超过1也可以小于0,超过1表示已经超过目标值,小于0表示小于开始位置。(给估值器使用
    • 插值器默认每10ms刷新一次
    public class PointInterpolator implements Interpolator {
    /**
    * input 是实际动画执行的时间比例 0~1
    * newInput 你想让动画已经执行的比例 0~1。
    * 注意:都是比例,而不是实际的值。
    *
    * setDuration(1000)情况下:前200ms走了3/4的路程比例,后800ms走了1/4的路程比例。
    */

    @Override
    public float getInterpolation(float input) {
    if (input <= 0.2) {//后1/4的时间,输出3/4的比例
    float newInput = input*4;
    return newInput;
    }else {//后3/4的时间,输出1/4的比例
    float newInput = (float) (input - 0.2)/4 + 0.8f;
    return newInput;
    }
    }
    }

    使用方式和默认插值器

    在xml和代码中使用插值器,省略代码中使用方式

    <?xml version="1.0" encoding="utf-8"?>
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
    // 通过资源ID设置插值器
    android:interpolator="@android:anim/overshoot_interpolator"
    android:duration="3000"
    android:fromXScale="0.0"
    android:fromYScale="0.0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toXScale="2"
    android:toYScale="2" />

    内置插值器动画展示

    Android动画之Interpolator

    Android动画插值器

    作用资源ID对应的Java类
    动画加速进行@android:anim/accelerte_interpolatorAcceleraterplator
    快速完成动画,超出再回到到结束样式@android:anim/overshoot_interpolatorOvershootInterpolator
    先加速再减速@android:anim/accelerate_decelerate_interpolatorAccelerateDecelerateInterpolator
    先退后再加速前进@android:anim/anticipate_interpolatorAnticipateInterpolator
    先退后再加速前进,超出终点后再回终点@android:anim/anticipate_overshoot_interpolatorAnticipateOvershootInterpolator
    最后阶段弹球效果@android:anim/bounce_interpolatorBounceInterpolator
    周期运动@android:anim/cycle_interpolatorCycleInterpolator
    减速@android:anim/decelerate_interpolatorDecelerateInterpolator
    匀速@android:anim/linear_interpolatorLinearInterpolator

    估值器

    设置 属性值 从初始值过渡到结束值 的变化具体数值

    • 参数fraction: 表示当前动画的进度(插值器返回值
    • 返回值:表示当前对应类型的取值,也就是UpdateListener接口方法中传入的值
    public class PointEvaluator implements TypeEvaluator<Point> {
    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
    int radius = (int) (startValue.getRadius() +
    fraction*(endValue.getRadius() - startValue.getRadius()));
    return new Point(radius);
    }
    }

    自定义插值器、估值器、属性的使用:

    public void doAnimation(){
    //ObjectAnimator animator = ObjectAnimator.ofInt(mView, "Radius", 20, 80);
    ValueAnimator animatior = new ValueAnimator();
    animatior.setObjectValues(new Point(20), new Point(80));
    animatior.setInterpolator(new PointInterpolator());
    animatior.setEvaluator(new PointEvaluator());

    animatior.setDuration(2000);
    animatior.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    mPoint = (Point) animation.getAnimatedValue();
    invalidate();
    }
    });
    animatior.start();
    }

    PropertyValuesHolder

    它其中保存了动画过程中所需要操作的属性和对应的值

    通过ObjectAnimator.ofFloat(Object target, String propertyName, float… values)构造的动画,ofFloat()的内部实现其实就是将传进来的参数封装成PropertyValuesHolder实例来保存动画状态,后期的各种操作也是以PropertyValuesHolder为主。

    //将需要操作的多个属性和值封装起来,一起放到ObjectAnimator中,相当于set操作。
    PropertyValuesHolder rotateHolder = PropertyValuesHolder.ofFloat("Rotation", 0, 360, 0);
    PropertyValuesHolder scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", 1, 2, 1,2,1);
    PropertyValuesHolder scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", 1, 2, 1,2,1);
    ObjectAnimator objectAnim = ObjectAnimator.ofPropertyValuesHolder(ivHolderTarget,
    rotateHolder,
    scaleXHolder,
    scaleYHolder);
    objectAnim.setDuration(2000);
    objectAnim.setInterpolator(new LinearInterpolator());
    objectAnim.start();

    KeyFrame(主要帧)

    如果想要更精确的控制动画,想要控制整个动画过程的某个点或某个时段达到的值,可以通过自定义插值器或估值器来实现,但是那样又有些费事,并且不容易计算这段时间内值的变化。 这时可以用Keyframe来实现,即设置好某个时间点和值,系统会自动计算该点和上个点之间,值的变化。

    /***
    * 实现左右摇晃,每边最后有震动的效果。
    * 摇晃角度100度:0.2f/0.2~0.4/0.4~0.5,分别设置不同的角度和加速器。
    * 每个比例点达到哪个角度,这在估值器中也能做到,但是需要自己算每个时间段内值的变化过程。
    * KeyFrame可以设置好 比例-值 以后,系统根据默认或设置的加速器改变:上个点和该点内的值如何变换。
    * 这样可以更精确的控制动画过程,同时也不用自己费劲去计算值因该如何变换。
    */

    Keyframe kfRotation1 = Keyframe.ofFloat(0, 0); //第一帧,如果没有该帧,会直接跳到第二帧开始动画。
    //第二帧 0.2f时达到60度,线性加速应该作用于从0~0.2f的这段时间,而不是作用在0.2~0.4f这段。因为已经定好60度是要的结果了,那么实现就应该在前面这段。
    Keyframe kfRotation2 = Keyframe.ofFloat(0.2f, 60);
    kfRotation2.setInterpolator(new LinearInterpolator());
    Keyframe kfRotation3 = Keyframe.ofFloat(0.4f, 100);
    kfRotation3.setInterpolator(new BounceInterpolator());
    Keyframe kfRotation4 = Keyframe.ofFloat(0.5f, 0);
    kfRotation4.setInterpolator(new LinearInterpolator()); //最少有2帧
    Keyframe kfRotation5 = Keyframe.ofFloat(0.7f, -60);
    kfRotation5.setInterpolator(new LinearInterpolator());
    Keyframe kfRotation6 = Keyframe.ofFloat(0.9f, -100);
    kfRotation6.setInterpolator(new BounceInterpolator());
    Keyframe kfRotation7 = Keyframe.ofFloat(1f, 0);//最后一帧,如果没有该帧,会以最后一个KeyFrame做结尾
    kfRotation7.setInterpolator(new LinearInterpolator());

    Keyframe kfScaleX1 = Keyframe.ofFloat(0, 1);
    Keyframe kfScaleX2 = Keyframe.ofFloat(0.01f,2.8f);
    Keyframe kfScaleX3 = Keyframe.ofFloat(0.8f,2.0f);
    Keyframe kfScaleX4 = Keyframe.ofFloat(1f,1.0f);

    Keyframe kfScaleY1 = Keyframe.ofFloat(0, 1);
    Keyframe kfScaleY2 = Keyframe.ofFloat(0.01f,2.8f);
    Keyframe kfScaleY4 = Keyframe.ofFloat(0.8f,2.0f);
    Keyframe kfScaleY5 = Keyframe.ofFloat(1f,1.0f);

    PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofKeyframe("rotation", kfRotation1, kfRotation2, kfRotation3,kfRotation4, kfRotation5, kfRotation6, kfRotation7);
    PropertyValuesHolder scaleXHolder = PropertyValuesHolder.ofKeyframe("scaleX", kfScaleX1, kfScaleX2, kfScaleX3, kfScaleX4);
    PropertyValuesHolder scaleYHolder = PropertyValuesHolder.ofKeyframe("scaleY", kfScaleY1, kfScaleY2, kfScaleY4, kfScaleY5);


    ObjectAnimator objectAnim = ObjectAnimator.ofPropertyValuesHolder(ivHolderTarget,
    rotationHolder,
    scaleXHolder,
    scaleYHolder);
    objectAnim.setDuration(1500);

    AnimatorSet

    AnimatorSet针对ValueAnimator和ObjectAnimator都是适用的,但一般而言,我们不会用到ValueAnimator的组合动画。

    playTogether/playSequentially

    无论是playTogether还是playSequentially方法,它们只是,仅仅是激活了动画什么时候开始,并不参与动画的具体操作。 例如:如果是playTogether,它只负责这个动画什么时候一起激活,至于anim1/anim2/anim3...哪个马上开始,哪个有延迟,哪个会无限重复,set都不管,只负责一起激活。 如果是playSequentially,它只负责什么时候开始激活第一个(因为有可能set设置延迟),并在第一个动画结束的时候,激活第二个,以此类推。

    ObjectAnimator anim1 = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);

    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    anima2.setStartDelay(2000);
    anima2.setRepeatCount(ValueAnimator.INFINITE);

    ObjectAnimator anim3 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim3.setStartDelay(2000);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.playTogether(anim1, anim2, anim3);//playSequentially(按次序播放)
    animatorSet.setDuration(2000);
    animatorSet.setStartDelay(2000);
    animatorSet.start();
    play(x).with(x)
    • play(anim1).with(anim2):2000ms后set开始激活动画,anim1启动,再过2000ms后anim2启动。
    • play(anim2).with(anim1):2000ms后set开始激活动画,再过2000ms后启动anim2,并且启动anim1.
    set监听

    addListener监听的是AnimatorSet的start/end/cacle/repeat。不会监听anim1/anim2的动画状态的。

    联合动画XML实现
    单独设置和Set中设置
    • 以set为准:
    //设置单次动画时长
    public AnimatorSet setDuration(long duration);
    //设置加速器
    public void setInterpolator(TimeInterpolator interpolator)
    //设置ObjectAnimator动画目标控件
    public void setTarget(Object target)
    ObjectAnimator anim1 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    anim1.setDuration(500000000);

    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim2.setDuration(3000);//每次3000,而不是3次3000ms
    anim2.setRepeatCount(3);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.play(tv2TranslateY).with(tv1TranslateY);
    animatorSet.setDuration(2000);//以Set为准
    animatorSet.start();

    setDuration()是指单个动画的时间,并不是指总共做完这个动画过程的时间。比如:anim2中设置了3000ms,重复3次。是指每次3000ms,不是3次3000ms。
    另外animatorSet设置了时间以后,anim1/anim2虽然也设置了,但是这时以set为准。即,anim1/anim2的单个动画时间为2000ms。只不过anim2是每次2000ms,重复3次,共6000ms。

    • 不以set为准:setStartDelay
    ObjectAnimator anim1 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim2.setStartDelay(2000);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.addListener(new Animator.AnimatorListener(){...});
    animatorSet.play(anim1).with(anim2);
    animatorSet.setStartDelay(3000);//指的是Set的激活延迟,而不是动画延迟
    animatorSet.setDuration(2000);
    animatorSet.start();

    setStartDelay不会覆盖单个动画的该方法,只会延长set的激活时间。所以,上面代码中动画的启动过程是:3000ms后set开始激活动画,anim1启动,再过2000ms后anim2启动。

    ViewPropertyAnimator

    属性动画已不再是针对于View而进行设计的了,而是一种对数值不断操作的过程,我们将属性动画对数值的操作过程设置到指定对象的属性上来,从而形成一种动画的效果。 虽然属性动画给我们提供了ValueAnimator类和ObjectAnimator类,在正常情况下,基本都能满足我们对动画操作的需求,但ValueAnimator类和ObjectAnimator类本身并不是针对View对象的而设计的,而我们在大多数情况下主要都还是对View进行动画操作的。

    因此Google官方在Android 3.1系统中补充了ViewPropertyAnimator类,这个类便是专门为View动画而设计的。

    • 专门针对View对象动画而操作的类
    • 更简洁的链式调用设置多个属性动画,这些动画可以同时进行
    • 拥有更好的性能,多个属性动画是一次同时变化,只执行一次UI刷新(也就是只调用一次invalidate,而n个ObjectAnimator就会进行n次属性变化,就有n次invalidate)
    • 每个属性提供两种类型方法设置。scaleX()/scaleXBy()
    • 该类只能通过View的animate()获取其实例对象的引用
    • 自动调用start
    btn.animate()
    .alpha(0.5f)
    .rotation(360)
    .scaleX(1.5f).scaleY(1.5f)
    .translationX(50).translationY(50)
    .setDuration(5000);

    image-20210604100429893

    layoutAnimation

    布局动画,api1,该属性只对创建ViewGroup时,对其子View有动画。已经创建过了该ViewGroup的话,再向其添加子View不会有动画。

    • onCreat创建加载布局时:
    //anim -> rotate_anim.xml
    <?xml version="1.0" encoding="utf-8"?>


    // layoutAnimation标签
    <?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="1"
    android:animationOrder="normal"
    android:animation="@anim/rotate_anim">

    </layoutAnimation>

    //定义在LinearLayout上,在该界面生成时,Button显示动画。但是,后面在LinearLayout中添加Button时,不再有动画。
    <LinearLayout
    android:id="@+id/ll_tips_target_animation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layoutAnimation="@anim/layout_animation"
    android:tag="在xml中设置的layoutAnimation"
    android:orientation="vertical">

    <Button
    style="@style/base_button"
    android:text="ViewGroup初始化时,子View有动画"/>

    </LinearLayout>
    • 代码中动态设置layoutAnimation,添加View
            //代码生成ViewGroup
    LinearLayout linear = new LinearLayout(this);

    Animation animation = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
    LayoutAnimationController controller = new LayoutAnimationController(animation);
    controller.setDelay(1);
    //动画模式,正常/倒叙/随机
    controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
    //设置layoutAnimation
    linear.setLayoutAnimation(controller);
    linear.setLayoutAnimationListener(new Animation.AnimationListener() {

    });

    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
    LinearLayout.LayoutParams.MATCH_PARENT,
    LinearLayout.LayoutParams.WRAP_CONTENT);
    linear.setLayoutParams(params);
    //给该ViewGroup添加子View,子View会有动画。
    addVeiw(linear,null);
    llTargetAnim.addView(linear, 0);

    使用场景:

    该属性只有ViewGroup创建的时候才能有效果,所以不适合动态添加子View的操作显示动画。一般做界面显示的时候的入场动画,比如打开一个界面,多个固定不变的item有动画的显示出来。(进入设置界面,信息展示界面)。

    android:animateLayoutChanges属性:

    Api11后,添加/移除子View时所带的默认动画,在Xml中设置。不能自定义动画,只能使用默认的。所以,使用范围较小。

    <LinearLayout
    android:animateLayoutChanges="true"
    />

    image


    收起阅读 »

    「Java 路线」| 关于泛型能问的都在这里了(含Kotlin)

    前言 泛型(Generic Type) 无论在哪一门语言里,都是最难语法的存在,细节之繁杂、理解之困难,令人切齿; 在这个系列里,我将总结Java & Kotlin中泛型的知识点,带你从 语法 & 原理 全面理解泛型。追求简单易懂又...
    继续阅读 »

    前言



    • 泛型(Generic Type) 无论在哪一门语言里,都是最难语法的存在,细节之繁杂、理解之困难,令人切齿;

    • 在这个系列里,我将总结Java & Kotlin中泛型的知识点,带你从 语法 & 原理 全面理解泛型。追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!

    • 首先,尝试回答这些面试中容易出现的问题,相信看完这篇文章,这些题目都难不倒你:


    1、下列代码中,编译出错的是:
    public class MyClass<T> {
    private T t0; // 0
    private static T t1; // 1
    private T func0(T t) { return t; } // 2
    private static T func1(T t) { return t; } // 3
    private static <T> T func2(T t) { return t; } // 4
    }
    2、泛型的存在是用来解决什么问题?
    3、请说明泛型的原理,什么是泛型擦除机制,具体是怎样实现的?



    目录





    1. 泛型基础




    • 问:什么是泛型,有什么作用?


    答:在定义类、接口和方法时,可以附带类型参数,使其变成泛型类、泛型接口和泛型方法。与非泛型代码相比,使用泛型有三大优点:更健壮(在编译时进行更强的类型检查)、更简洁(消除强转,编译后自动会增加强转)、更通用(代码可适用于多种类型)



    • 问:什么是类型擦除机制?


    答:泛型本质上是 Javac 编译器的一颗 语法糖,这是因为:泛型是 JDK1.5 中引进的新特性,为了 向下兼容,Java 虚拟机和 Class 文件并没有提供泛型的支持,而是让编译器擦除 Code 属性中所有的泛型信息,需要注意的是,泛型信息会保留在类常量池的属性中。



    • 问:类型擦除的具体步骤?


    答:类型擦除发生在编译时,具体分为以下 3 个步骤:



    • 1:擦除所有类型参数信息,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object

    • 2:(必要时)插入类型转换,以保持类型安全

    • 3:(必要时)生成桥接方法以在子类中保留多态性


    举个例子:


    源码:
    public class Parent<T> {
    public void func(T t){
    }
    }

    public class Child<T extends Number> extends Parent<T> {
    public T get() {
    return null;
    }
    public void func(T t){
    }
    }

    void test(){
    Child<Integer> child = new Child<>();
    Integer i = child.get();
    }
    ---------------------------------------------------------
    字节码:
    public class Parent {
    public void func(Object t){
    }
    }

    public class Child extends Parent {
    public Number get() {
    return null;
    }
    public void func(Number t) {
    }

    桥方法 - synthetic
    public void func(Object t){
    func((Number)t);
    }
    }

    void test() {
    Child<Integer> child = new Child();
    // 插入强制类型转换
    Integer i = (Integer) child.get();
    }

    步骤1:Parent 中的类型参数 T 被擦除为 Object,而 Child 中的类型参数 T 被擦除为 Number;


    步骤2:child.get(); 插入了强制类型转换


    步骤3:在 Child 中生成桥方法,桥方法是编译器生成的,所以会带有 synthetic 标志位。为什么子类中需要增加桥方法呢,可以先思考这个问题:假如没有桥方法,会怎么样?你可以看看下列代码调用的是子类还是父类方法:


    Parent<Integer> child = new Child<>();
    Parent<Integer> parent = new Parent<>();

    child.func(1); // Parent#func(Object);
    parent.func(1); // Parent#func(Object);

    这两句代码都会调用到 Parent#func(),如果你看过之前我写过的一篇文章,相信难不到你:《Java | 深入理解方法调用的本质(含重载与重写区别)》。在这里我简单分析下:



    1、方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)


    2、这两句代码调用的方法符号引用为:


    child.func(new Object()) => com/xurui/Child.func(Object)


    parent.func(new Object()) => com/xurui/Parent.func(Object)


    3、这两句方法调用的字节码指令为 invokevirtual


    4、类加载解析阶段解析类的继承关系,生成类的虚方法表


    5、调用阶段(动态分派):Child 没有重写 func(Object),所以 Child 的虚方法表中存储的是Parent#func(Object);Parent 的虚方法表中存储的是Parent#func(Object);



    可以看到,即使使用对象的实际类型为 Child ,这里调用的依旧是父类的方法。这样就 失去了多态性。 因此,才需要在泛型子类中添加桥方法。



    • 问:为什么擦除后,反编译还是看到类型参数 T ?


    反编译Parent.class,可以看到 T ,不是已经擦除了吗?

    public class Parent<T> {
    public Parent() {
    }

    public void func(T t) {
    }
    }

    答:泛型中所谓的类型擦除,其实只是擦除Code 属性中的泛型信息,在类常量池属性(Signature 属性、LocalVariableTypeTable 属性)中其实还保留着泛型信息,这也是在运行时可以反射获取泛型信息的根本依据,我在第 4 节说。



    • 问:泛型的限制 & 类型擦除会带来什么影响?


    由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。为了避免程序的运行结果与程序员语义不一致的情况,泛型在使用上存在一些限制。好处是类型擦除不会为每种参数化类型创建新的类,因此泛型不会增大内存消耗。


    泛型的限制




    2. Kotlin的实化类型参数


    前面我们提到,由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。例如下面的代码是不合法的,因为T并不是一个真正的类型,而仅仅是一个符号:


    在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:

    Java:
    <T> List<T> filter(List list) {
    List<T> result = new ArrayList<>();
    for (Object e : list) {
    if (e instanceof T) { // compiler error
    result.add(e);
    }
    }
    return result;
    }
    ---------------------------------------------------
    Kotlin:
    fun <T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
    if (e is T) { // cannot check for instance of erased type: T
    result.add(e)
    }
    }
    return result
    }

    Kotlin中,有一种方法可以突破这种限制,即:带实化类型参数的内联函数


    Kotlin:
    inline fun <reified T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
    if (e is T) {
    result.add(e)
    }
    }
    return result
    }

    关键在于inlinereified,这两者的语义是:



    • inline(内联函数): Kotlin编译器将内联函数的字节码插入到每一次调用方法的地方

    • reified(实化类型参数): 在插入的字节码中,使用类型实参的确切类型代替类型实参


    规则很好理解,对吧。很明显,当发生方法内联时,方法体字节码就变成了:


    调用:
    val list = listOf("", 1, false)
    val strList = filter<String>(list)
    ---------------------------------------------------
    内联后:
    val result = ArrayList<String>()
    for (e in list) {
    if (e is String) {
    result.add(e)
    }
    }

    需要注意的是,内联函数整个方法体字节码会被插入到调用位置,因此控制内联函数体的大小。如果函数体过大,应该将不依赖于T的代码抽取到单独的非内联函数中。



    注意,无法从 Java 代码里调用带实化类型参数的内联函数



    实化类型参数的另一个妙用是代替 Class 对象引用,例如:


    fun Context.startActivity(clazz: Class<*>) {
    Intent(this, clazz).apply {
    startActivity(this)
    }
    }

    inline fun <reified T> Context.startActivity() {
    Intent(this, T::class.java).apply {
    startActivity(this)
    }
    }

    调用方:
    context.startActivity(MainActivity::class.java)
    context.startActivity<MainActivity>() // 第二种方式会简化一些



    3. 变型:协变 & 逆变 & 不变


    变型(Variant)描述的是相同原始类型的不同参数化类型之间的关系。说起来有点绕,其实就是说:IntegerNumber的子类型,问你List<Integer>是不是List<Number>的子类型?


    变型的种类具体分为三种:协变型 & 逆变型 & 不变型



    • 协变型(covariant): 子类型关系被保留

    • 逆变型(contravariant): 子类型关系被翻转

    • 不变型(invariant): 子类型关系被消除


    在 Java 中,类型参数默认是不变型的,例如:


    List<Number> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // compiler error

    相比之下,数组是支持协变型的:


    Number[] nums;
    Integer[] ints = new Integer[10];
    nums = ints; // OK 协变,子类型关系被保留

    那么,当我们需要将List<Integer>类型的对象,赋值给List<Number>类型的引用时,应该怎么做呢?这个时候我们需要限定通配符



    • <? extends> 上界通配符


    要想类型参数支持协变,需要使用上界通配符,例如:


    List<? extends Number> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK

    但是这会引入一个编译时限制:不能调用参数包含类型参数 E 的方法,也不能设置类型参数的字段,简单来说,就是只能访问不能修改(非严格):


    // ArrayList.java
    public boolean add(E e) {
    ...
    }

    l1.add(1); // compiler error


    • <? super> 下界通配符


    要想类型参数支持逆变,需要使用下界通配符,例如:


    List<? super Integer> l1;
    List<Number> l2 = new ArrayList<>();
    l1 = l2; // OK

    同样,这也会引入一个编译时限制,但是与协变相反:不能调用返回值为类型参数的方法,也不能访问类型参数的字段,简单来说,就是只能修改不能访问(非严格):


    // ArrayList.java
    public E get(int index) {
    ...
    }

    Integer i = l1.get(0); // compiler error


    • <?> 无界通配符

    其实很简单,很多资料其实都解释得过于复杂了。 < ?> 其实就是 的缩写。例如:
    List<?> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK

    理解了这点,这个问题就很好回答了:



    • 问:List 与 List<?>有什么区别?


    答:List 是原生类型,可以添加或访问元素,不具备编译期安全性,而 List 其实是 List的缩写,是协变型的(可引出协变型的特点与限制);从语义上,List 表明使用者清楚变量是类型安全的,而不是因为疏忽而使用了原生类型 List。



    泛型代码的设计,应遵循PECS原则(Producer extends Consumer super):



    • 如果只需要获取元素,使用 <? extends T>

    • 如果只需要存储,使用<? super T>


    举例:


    // Collections.java public static void copy(List<? super T> dest, List<? extends T> src) { }



    在 Kotlin 中,变型写法会有些不同,但是语义是完全一样的:


    协变:
    val l0: MutableList<*> 相当于MutableList<out Any?>
    val l1: MutableList<out Number>
    val l2 = ArrayList<Int>()
    l0 = l2 // OK
    l1 = l2 // OK
    ---------------------------------------------------
    逆变:
    val l1: MutableList<in Int>
    val l2 = ArrayList<Number>()
    l1 = l2 // OK

    另外,Kotlin 的in & out不仅仅可以用在类型实参上,还可以用在泛型类型声明的类型参数上。其实这是一种简便写法,表示类设计者知道类型参数在整个类上只能协变或逆变,避免在每个使用的地方增加,例如 Kotlin 的List被设计为不可修改的协变型:


    public interface List<out E> : Collection<E> {
    ...
    }


    注意:在 Java 中,只支持使用点变型,不支持 Kotlin 类似的声明点变型



    小结一下:





    4. 使用反射获取泛型信息


    前面提到了,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。


    获取泛型类型实参:需要利用Type体系


    4.1 获取泛型类 & 泛型接口声明


    TypeVariable ParameterizedType GenericArrayType WildcardType


    Gson TypeToken


    Editting....




    5. 总结



    • 应试建议

      • 1、第 1 节非常非常重点,着重记忆:泛型的本质和设计缘由、泛型擦除的三个步骤、限制和优点,已经总结得很精华了,希望能帮到你;

      • 2、着重理解变型(Variant)的概念,以及各种限定符的含义;

      • 3、Kotlin 相关的部分,作为知识积累和思路扩展为主,非应试重点。







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

    收起阅读 »

    「Java 路线」| 反射机制(含 Kotlin)

    前言 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注! 目录 1. 类型系统...
    继续阅读 »

    前言



    • 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。

    • 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!




    目录



    1. 类型系统的基本概念


    首先,梳理一一下类型系统的基础概念:



    • 问:什么是强 / 弱类型语言?


    答:强 / 弱类型语言的区分,关键在于变量是否 (倾向于) 类型兼容。例如,Java 是强类型语言,变量有固定的类型,以下代码在 Java 中是非法的:


    public class MyRunnable {
    public abstract void run();
    }

    // 编译错误:Incompatible types
    java.lang.Runnable runnable = new MyRunnable() {
    @Override
    public void run() {

    }
    }
    runnable.run(); // X

    相对地,JavaScript 是弱类型语言,一个变量没有固定的类型,允许接收不同类型的值:


    function MyRunnable(){
    this.run = function(){
    }
    }
    function Runnable(){
    this.run = function(){
    }
    }
    var ss = new MyRunnable();
    ss.run(); // 只要对象有相同方法签名的方法即可
    ss = new Runnable();
    ss.run();

    更具体地描述,Java的强类型特性体现为:变量仅允许接收相同类型或子类型的值。 嗯(黑人问号脸)?和你的理解一致吗?请看下面代码,哪一行是有问题的:


    注意,请读者假设 1 ~ 4 号代码是单独运行的

    long numL = 1L;
    int numI = 0;
    numL = numI; // 1
    numI = (int)numL; // 2

    Integer integer = new Integer(0);
    Object obj = new Object();
    integer = (Integer) obj; // 3 ClassCastException
    obj = integer; // 4

    在这里,第 3 句代码会发生运行时异常,结论:



    • 1:调用字节码指令 i2l,将 int 值转换为 long 值。(此时,numL 变量接收的是相同类型的值,命题正确)


    • 2:调用字节码指令 l2i,将 long 值转换为 int 值。(此时,numI 变量接收的是相同类型的值,命题正确)


    • 3:调用字节码指令 checkcast,发现 obj 变量的值不是 Integer 类型,抛出 ClassCastException。(此时,Integer 变量不允许接收 Object 对象,命题正确)


    • 4:integer 变量的值是 obj 变量的子类型,可以接收。(此时,Object 变量允许接收 Integer 对象,命题正确)



    用一张图概括一下:






    • 问:什么是静态 / 动态类型语言?


    答:静态 / 动态类型语言的区分,关键在于类型检查是否 (倾向于) 编译时执行。例如, Java & C/C++ 是静态类型语言,而 JavaScript 是动态类型语言。需要注意的是,这个定义并不是绝对的,例如 Java 也存在运行时类型检查的方式,例如上面提到的 checkcast 指令本质上是在运行时检查变量的类型与对象的类型是否相同。 那么 Java 是如何在运行时获得类型信息的呢?这就是我们下一节要讨论的问题。




    2. 反射的基本概念



    • 问:什么是反射?为什么要使用反射?


    答:反射(Reflection)是一种在运行时 动态访问类型信息 的机制。Java 是静态强类型语言,它倾向于在编译时进行类型检查,因此当我们访问一个类时,它必须是编译期已知的,而使用反射机制可以解除这种限制,赋予 Java 语言动态类型的特性。例如:


    void func(Object obj) {
    try {
    Method method = obj.getClass().getMethod("run",null);
    method.invoke(obj,null);
    }
    ... 省略 catch
    }
    func(runnable); 调用 Runnale#run()
    func(myRunnable); 调用 MyRunnale#run()


    • 问:Java 运行时类型信息是如何表示的?


    所有的类在第一次使用时动态加载到内存中,并构造一个 Class 对象,其中包含了与类有关的所有信息,Class 对象是运行时访问类型信息的入口。需要注意的是,每个类 / 内部类 / 接口都拥有各自的 Class 对象。



    • 问:获取 Class 对象有几种方式,有什么区别?


    答:获取 Class 对象是反射的起始步骤,具体来说,分为以下三种方式:



    • 问:为什么反射性能差,怎么优化?


    答:主要有以下原因:


    性能差原因优化方法
    产生大量中间变量缓存元数据对象
    增加了检查可见性操作调用Method#setAccessible(true),减少不必要的检查
    Inflation 机制会生成字节码,而这段字节码没有经过优化/
    缺少编译器优化,普通调用有一系列优化手段,例如方法内联,而反射调用无法应用此优化/
    增加了装箱拆箱操作,反射调用需要构建包装类/


    3. 反射调用的 Inflation 机制


    反射调用是反射的一个较为常用的场景,这里我们来分析下反射调用的源码。反射调用需要使用Method#invoke(...),源码如下:


    Method.java


    public Object invoke(Object obj, Object... args) {
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
    ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
    }

    NativeMethodAccessorImpl.java


    class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
    this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) {
    1. 检查调用次数是否超过阈值
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
    2. ASM 生成新类
    MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
    3. 设置为代理
    this.parent.setDelegate(var3);
    }
    4. 调用 native 方法
    return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
    this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
    }

    ReflectionFactory.java


    public class ReflectionFactory {

    private static int inflationThreshold = 15;

    static int inflationThreshold() {
    return inflationThreshold;
    }
    }

    可以看到,反射调用最终会委派给 NativeMethodAccessorImpl ,要点如下:



    • 当反射调用执行次数较少时,直接通过 native 方法调用;

    • 当反射调用执行次数较多时,则通过 ASM 字节码生成技术生成新的类,以后的反射调用委派给新生成的类来处理。



    提示: 为什么不一开始就生成新类呢?因为生成字节码的时间成本高于执行一次 native 方法的时间成本,所以在反射调用执行次数较少时,就直接调用 native 方法了。





    4. 反射的应用场景


    4.1 类型判断



    4.2 创建对象



    • 1、使用 Class.newInstance(),适用于类拥有无参构造方法


    Class classType = Class.forName("java.lang.String");
    String str= (String) classType.newInstance();


    • 2、Constructor.newInstance(),适用于使用带参数的构造方法


    Class classType = Class.forName("java.lang.String");
    Constructor constructor = classType.getConstructor(new Class[]{String.class});
    constructor.setAccessible(true);
    String employee3 = (String) constructor.newInstance(new Object[]{"123"});

    4.3 创建数组


    创建数组需要元素的 Class 对象作为 ComponentType:



    • 1、创建一维数组


    Class classType = Class.forName("java.lang.String");
    String[] array = (String[]) Array.newInstance(classType, 5); 长度为5
    Array.set(array, 3, "abc"); 设置元素
    String string = (String) Array.get(array,3); 读取元素


    • 2、创建多维数组


    Class[] dimens = {3, 3};
    Class[][] array = (Class[][]) Array.newInstance(int.class, dimens);

    4.3 访问字段、方法


    Editting...


    4.4 获取泛型信息


    我们知道,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!


    4.5 获取运行时注解信息


    注解是一种添加到声明上的元数据,而RUNTIME注解在类加载后会保存在 Class 对象,可以反射获取。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!






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

    Java | JDK 动态代理的原理其实很简单

    前言 代理模式(Proxy Pattern)也称委托模式(Delegate Pattern),是一种结构型设计模式,也是一项基础设计技巧; 其中,动态代理有很多有意思的应用场景,比如 AOP、日志框架、全局性异常处理、事务处理等。这篇文章,我们主要...
    继续阅读 »

    前言



    • 代理模式(Proxy Pattern)也称委托模式(Delegate Pattern),是一种结构型设计模式,也是一项基础设计技巧;

    • 其中,动态代理有很多有意思的应用场景,比如 AOP、日志框架、全局性异常处理、事务处理等。这篇文章,我们主要讨论最基本的 JDK 动态代理。




    目录





    前置知识


    这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~





    1. 概述



    • 什么是代理 (模式)? 代理模式 (Proxy Pattern) 也称委托模式 (Deletage Pattern),属于结构型设计模式,也是一项基本的设计技巧。通常,代理模式用于处理两种问题:

      • 1、控制对基础对象的访问

      • 2、在访问基础对象时增加额外功能



    这是两种非常朴素的场景,正因如此,我们常常会觉得其它设计模式中存在代理模式的影子。UML 类图和时序图如下:





    • 代理的基本分类: 静态代理 + 动态代理,分类的标准是 “代理关系是否在编译期确定;


    • 动态代理的实现方式: JDK、CGLIB、Javassist、ASM





    2. 静态代理


    2.1 静态代理的定义


    静态代理是指代理关系在编译期确定的代理模式。使用静态代理时,通常的做法是为每个业务类抽象一个接口,对应地创建一个代理类。举个例子,需要给网络请求增加日志打印:


    1、定义基础接口
    public interface HttpApi {
    String get(String url);
    }

    2、网络请求的真正实现
    public class RealModule implements HttpApi {
    @Override
    public String get(String url) {
    return "result";
    }
    }

    3、代理类
    public class Proxy implements HttpApi {
    private HttpApi target;

    Proxy(HttpApi target) {
    this.target = target;
    }

    @Override
    public String get(String url) {
    // 扩展的功能
    Log.i("http-statistic", url);
    // 访问基础对象
    return target.get(url);
    }
    }

    2.2 静态代理的缺点



    • 1、重复性: 需要代理的业务或方法越多,重复的模板代码越多;

    • 2、脆弱性: 一旦改动基础接口,代理类也需要同步修改(因为代理类也实现了基础接口)。




    3. 动态代理


    3.1 动态代理的定义


    动态代理是指代理关系在运行时确定的代理模式。需要注意,JDK 动态代理并不等价于动态代理,前者只是动态代理的实现之一,其它实现方案还有:CGLIB 动态代理、Javassist 动态代理和 ASM 动态代理等。因为代理类在编译前不存在,代理关系到运行时才能确定,因此称为动态代理。


    3.2 JDK 动态代理示例


    我们今天主要讨论JDK 动态代理(Dymanic Proxy API),它是 JDK1.3 中引入的特性,核心 API 是 Proxy 类和 InvocationHandler 接口。它的原理是利用反射机制在运行时生成代理类的字节码。


    我们继续用打印日志的例子,使用动态代理时:


    public class ProxyFactory {
    public static HttpApi getProxy(HttpApi target) {
    return (HttpApi) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new LogHandler(target));
    }

    private static class LogHandler implements InvocationHandler {
    private HttpApi target;

    LogHandler(HttpApi target) {
    this.target = target;
    }
    // method底层的方法无参数时,args为空或者长度为0
    @Override
    public Object invoke(Object proxy, Method method, @Nullable Object[] args)
    throws Throwable
    {
    // 扩展的功能
    Log.i("http-statistic", (String) args[0]);
    // 访问基础对象
    return method.invoke(target, args);
    }
    }
    }

    如果需要兼容多个业务接口,可以使用泛型:


    public class ProxyFactory {
    @SuppressWarnings("unchecked")
    public static T getProxy(T target) {
    return (T) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new LogHandler(target));
    }

    private static class LogHandler implements InvocationHandler {
    // 同上
    }
    }

    客户端调用:


    HttpAPi proxy = ProxyFactory.getProxy(target);
    OtherHttpApi proxy = ProxyFactory.getProxy(otherTarget);

    通过泛型参数传递不同的类型,客户端可以按需实例化不同类型的代理对象。基础接口的所有方法都统一到 InvocationHandler#invoke() 处理。静态代理的两个缺点都得到解决:



    • 1、重复性:即使有多个基础业务需要代理,也不需要编写过多重复的模板代码;

    • 2、脆弱性:当基础接口变更时,同步改动代理并不是必须的。


    3.3 静态代理 & 动态代理对比



    • 共同点:两种代理模式实现都在不改动基础对象的前提下,对基础对象进行访问控制和扩展,符合开闭原则。

    • 不同点:静态代理存在重复性和脆弱性的缺点;而动态代理(搭配泛型参数)可以实现了一个代理同时处理 N 种基础接口,一定程度上规避了静态代理的缺点。从原理上讲,静态代理的代理类 Class 文件在编译期生成,而动态代理的代理类 Class 文件在运行时生成,代理类在 coding 阶段并不存在,代理关系直到运行时才确定。




    4. JDK 动态代理源码分析


    这一节,我们来分析 JDK 动态代理的源码,核心类是 Proxy,主要分析 Proxy 如何生成代理类,以及如何将方法调用统一分发到 InvocationHandler 接口。


    4.1 API 概述


    Proxy 类主要包括以下 API:



























    Proxy 描述
    getProxyClass(ClassLoader, Class...) : Class 获取实现目标接口的代理类 Class 对象
    newProxyInstance(ClassLoader,Class[],InvocationHandler) : Object 获取实现目标接口的代理对象
    isProxyClass(Class) : boolean 判断一个 Class 对象是否属于代理类
    getInvocationHandler(Object) : InvocationHandler 获取代理对象内部的 InvocationHandler

    4.2 核心源码


    Proxy.java


    1、获取代理类 Class 对象
    public static Class getProxyClass(ClassLoader loader,Class... interfaces){
    final Class[] intfs = interfaces.clone();
    ...
    1.1 获得代理类 Class 对象
    return getProxyClass0(loader, intfs);
    }

    2、实例化代理类对象
    public static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h){
    ...
    final Class[] intfs = interfaces.clone();
    2.1 获得代理类 Class对象
    Class cl = getProxyClass0(loader, intfs);
    ...
    2.2 获得代理类构造器 (接收一个 InvocationHandler 参数)
    // private static final Class[] constructorParams = { InvocationHandler.class };
    final Constructor cons = cl.getConstructor(constructorParams);
    final InvocationHandler ih = h;
    ...
    2.3 反射创建实例
    return newInstance(cons, ih);
    }

    可以看到,实例化代理对象也需要先通过 getProxyClass0(...) 获取代理类 Class 对象,而 newProxyInstance(...) 随后会获取参数为 InvocationHandler 的构造函数实例化一个代理类对象。


    我们先看下代理类 Class 对象是如何获取的:


    Proxy.java


    -> 1.12.1 获得代理类 Class对象
    private static Class getProxyClass0(ClassLoader loader,Class... interfaces) {
    ...
    从缓存中获取代理类,如果缓存未命中,则通过ProxyClassFactory生成代理类
    return proxyClassCache.get(loader, interfaces);
    }

    private static final class ProxyClassFactory implements BiFunction[], Class>{

    3.1 代理类命名前缀
    private static final String proxyClassNamePrefix = "$Proxy";

    3.2 代理类命名后缀,从 0 递增(原子 Long)
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class apply(ClassLoader loader, Class[] interfaces)
    {
    Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
    3.3 参数校验
    for (Class intf : interfaces) {
    // 验证参数 interfaces 和 ClassLoder 中加载的是同一个类
    // 验证参数 interfaces 是接口类型
    // 验证参数 interfaces 中没有重复项
    // 否则抛出 IllegalArgumentException
    }
    // 验证所有non-public接口来自同一个包

    3.4(一般地)代理类包名
    // public static final String PROXY_PACKAGE = "com.sun.proxy";
    String proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";

    3.5 代理类的全限定名
    long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    3.6 生成字节码数据
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);

    3.7 从字节码生成 Class 对象
    return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
    }
    }

    -> 3.6 生成字节码数据
    public static byte[] generateProxyClass(final String var0, Class[] var1) {
    ProxyGenerator var2 = new ProxyGenerator(var0, var1);
    ...
    final byte[] var3 = var2.generateClassFile();
    return var3;
    }

    ProxyGenerator.java


    private byte[] generateClassFile() {
    3.6.1 只代理Object的hashCode、equals和toString
    this.addProxyMethod(hashCodeMethod, Object.class);
    this.addProxyMethod(equalsMethod, Object.class);
    this.addProxyMethod(toStringMethod, Object.class);

    3.6.2 代理接口的每个方法
    ...
    for(var1 = 0; var1 < this.interfaces.length; ++var1) {
    ...
    }

    3.6.3 添加带有 InvocationHandler 参数的构造器
    this.methods.add(this.generateConstructor());
    var7 = this.proxyMethods.values().iterator();
    while(var7.hasNext()) {
    ...
    3.6.4 在每个代理的方法中调用InvocationHandler#invoke()
    }

    3.6.5 输出字节流
    ByteArrayOutputStream var9 = new ByteArrayOutputStream();
    DataOutputStream var10 = new DataOutputStream(var9);
    ...
    return var9.toByteArray();
    }

    以上代码已经非常简化了,主要关注核心流程:JDK 动态代理生成的代理类命名为 com.sun.proxy$Proxy[从0开始的数字](例如:com.sun.proxy$Proxy0),这个类继承自 java.lang.reflect.Proxy。其内部还有一个参数为 InvocationHandler 的构造器,对于代理接口的方法调用都会分发到 InvocationHandler#invoke()。


    UML 类图如下,需要注意图中红色箭头,表示代理类和 HttpApi 接口的代理关系在运行时才确定:




    提示: Android 系统中生成字节码和从字节码生成 Class 对象的步骤都是 native 方法:



    • private static native Class generateProxy(…)

    • 对应的native方法:dalvik/vm/native/java_lang_reflect_Proxy.cpp



    4.3 查看代理类源码


    可以看到,ProxyGenerator#generateProxyClass() 其实是一个静态 public 方法,所以我们直接调用,并将代理类 Class 的字节流写入磁盘文件,使用 IntelliJ IDEA 的反编译功能查看源代码。


    输出字节码:


    byte[] classFile = ProxyGenerator.generateProxyClass("$proxy0",new Class[]{HttpApi.class});
    // 直接写入项目路径下,方便使用IntelliJ IDEA的反编译功能
    String path = "/Users/pengxurui/IdeaProjects/untitled/src/proxy/HttpApi.class";
    try(FileOutputStream fos = new FileOutputStream(path)){
    fos.write(classFile);
    fos.flush();
    System.out.println("success");
    } catch (Exception e){
    e.printStackTrace();
    System.out.println("fail");
    }

    反编译结果:


    public final class $proxy0 extends Proxy implements HttpApi {
    //反射的元数据Method存储起来,避免重复创建
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $proxy0(InvocationHandler var1) throws {
    super(var1);
    }

    /**
    * Object#hashCode()
    * Object#equals(Object)
    * Object#toString()
    */


    // 实现了HttpApi接口
    public final String get() throws {
    try {
    //转发到Invocation#invoke()
    return (String)super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
    throw var2;
    } catch (Throwable var3) {
    throw new UndeclaredThrowableException(var3);
    }
    }

    static {
    try {
    //Object#hashCode()
    //Object#equals(Object)
    //Object#toString()
    m3 = Class.forName("HttpApi").getMethod("get");
    } catch (NoSuchMethodException var2) {
    throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
    throw new NoClassDefFoundError(var3.getMessage());
    }
    }
    }

    4.4 常见误区



    • 基础对象必须实现基础接口,否则不能使用动态代理


    这个想法可能来自于一些没有实现任何接口的类,因此就没有办法得到接口的Class对象作为Proxy#newProxyInstance() 的参数,这确实会带来一些麻烦,举个例子:


    package com.domain;
    public interface HttpApi {
    String get();
    }

    // 另一个包的non-public接口
    package com.domain.inner;
    /**non-public**/interface OtherHttpApi{
    String get();
    }

    package com.domain.inner;
    // OtherHttpApiImpl类没有实现HttpApi接口或者没有实现任何接口
    public class OtherHttpApiImpl /**extends OtherHttpApi**/{
    public String get() {
    return "result";
    }
    }

    // Client:
    HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // TODO:扩展的新功能
    // IllegalArgumentException: object is not an instance of declaring class
    return method.invoke(impl,args);
    }
    });
    api.get();

    在这个例子里,OtherHttpApiImpl 类因为历史原因没有实现 HttpApi 接口,虽然方法签名与 HttpApi 接口的方法签名完全相同,但是遗憾,无法完成代理。也有补救的办法,找到 HttpApi 接口中签名相同的 Method,使用这个 Method 来转发调用。例如:


    HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // TODO:扩展的新功能
    if (method.getDeclaringClass() != impl.getClass()) {
    // 找到相同签名的方法
    Method realMethod = impl.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
    return realMethod.invoke(impl, args);
    }else{
    return method.invoke(impl,args);
    }
    }
    });



    5. 总结


    今天,我们讨论了静态代理和动态代理两种代理模式,静态代理在设计模式中随处可见,但存在重复性和脆弱性的缺点,动态代理的代理关系在运行时确定,可以实现一个代理处理 N 种基础接口,一定程度上规避了静态代理的缺点。在我们熟悉的一个网络请求框架中,就充分利用了动态代理的特性,你知道是在说哪个框架吗?




    参考资料







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

    这一次,彻底搞懂SparseArray实现原理

    最近在整理SparseArray这一知识点的时候,发现网上大多数SparseArray原理分析的文章都存在很多问题(可以说很多作者并没有读懂SparseArray的源码),也正因此,才有了这篇文章。我们知道,SparseArray与ArrayMap是Andro...
    继续阅读 »

    最近在整理SparseArray这一知识点的时候,发现网上大多数SparseArray原理分析的文章都存在很多问题(可以说很多作者并没有读懂SparseArray的源码),也正因此,才有了这篇文章。我们知道,SparseArray与ArrayMap是Android中高效存储K-V的数据结构,也是是Android面试中的常客,弄懂它们的实现原理是很有必要的,本篇文章就以SparseArray的源码为例进行深入分析。


    一、SparseArray的类结构


    SparseArray可以翻译为稀疏数组,从字面上可以理解为松散不连续的数组。虽然叫做Array,但它却是存储K-V的一种数据结构。其中Key只能是int类型,而Value是Object类型。我们来看下它的类结构:


    public class SparseArray<E> implements Cloneable {
    // 用来标记此处的值已被删除
    private static final Object DELETED = new Object();
    // 用来标记是否有元素被移除
    private boolean mGarbage = false;
    // 用来存储key的集合
    private int[] mKeys;
    // 用来存储value的集合
    private Object[] mValues;
    // 存入的元素个数
    private int mSize;

    // 默认初始容量为10
    public SparseArray() {
    this(10);
    }

    public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
    mKeys = EmptyArray.INT;
    mValues = EmptyArray.OBJECT;
    } else {
    mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
    mKeys = new int[mValues.length];
    }
    mSize = 0;
    }

    // ...省略其他代码

    }

    可以看到SparseArray仅仅实现了Cloneable接口并没有实现Map接口,并且SparseArray内部维护了一个int数组和一个Object数组。在无参构造方法中调用了有参构造,并将其初始容量设置为了10。


    二、SparseArray的remove()方法


    是不是觉得很奇怪?作为一个容器类,不先讲put方法怎么先将remove呢?这是因为remove方法的一些操作会影响到put的操作。只有先了解了remove才能更容易理解put方法。我们来看remove的代码:



    // SparseArray
    public void remove(int key) {
    delete(key);
    }

    public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    mValues[i] = DELETED;
    mGarbage = true;
    }
    }
    }

    可以看到remove方法直接调用了delete方法。而在delete方法中会先通过二分查找(二分查找代码后边分析)找到key所在的位置,然后将这一位置的value值置为DELETE,注意,这里还将mGarbage设置为了true来标记集合中存在删除元素的情况。想象一下,在删除多个元素后这个集合中是不是就可能会出现不连续的情况?大概这也是SparseArray名字的由来吧。


    三、SparseArray的put()方法


    作为一个存储K-V类型的数据结构,put方法是key和value的入口。也是SparseArray中最重要的一个方法。先来看下put方法的代码:


    // SparseArray
    public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) { // 意味着之前mKeys中已经有对应的key存在了,第i个位置对应的就是key。
    mValues[i] = value; // 直接更新value
    } else { // 返回负数说明未在mKeys中查找到key

    // 取反得到待插入key的位置
    i = ~i;

    // 如果插入位置小于size,并且这个位置的value刚好是被删除掉的,那么直接将key和value分别插入mKeys和mValues的第i个位置
    if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
    }
    // mGarbage为true说明有元素被移除了,此时mKeys已经满了,但是mKeys内部有被标记为DELETE的元素
    if (mGarbage && mSize >= mKeys.length) {
    // 调用gc方法移动mKeys和mValues中的元素,这个方法可以后边分析
    gc();

    // 由于gc方法移动了数组,因此插入位置可能有变化,所以需要重新计算插入位置
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
    }
    // GrowingArrayUtils的insert方法将会将插入位置之后的所有数据向后移动一位,然后将key和value分别插入到mKeys和mValue对应的第i个位置,如果数组空间不足还会开启扩容,后边分析这个insert方法
    mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
    mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
    mSize++;
    }
    }

    虽然这个方法只有寥寥数行,但是想要完全理解却并非易事,即使写了很详细的注释也不容易读懂。我们不妨来详细分析一下。第一行代码通过二分查找得到了一个index。看下二分查找的代码:


    // ContainerHelpers
    static int binarySearch(int[] array, int size, int value) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
    final int mid = (lo + hi) >>> 1;
    final int midVal = array[mid];

    if (midVal < value) {
    lo = mid + 1;
    } else if (midVal > value) {
    hi = mid - 1;
    } else {
    return mid; // value found
    }
    }
    return ~lo; // value not present
    }

    关于二分查找相信大家都是比较熟悉的,这一算法用于在一组有序数组中查找某一元素所在位置的。如果数组中存在这一元素,则将这个元素对应的位置返回。如果不存在那么此时的lo就是这个元素的最佳存储位置。上述代码中将lo取反作为了返回值。因为lo一定是大于等于0的数,因此取反后的返回值必定小于等于0.明白了这一点,再来看put方法中的这个if...else是不是很容易理解了?


    // SparseArray
    public void put(int key, E value) {

    if (i >= 0) {
    mValues[i] = value; // 直接更新value
    } else {
    i = ~i;
    // ... 省略其它代码
    }
    }

    如果i>=0,意味着当前的这个key已经存在于mKeys中了,那么此时put只需要将最新的value更新到mValues中即可。而如果i<=0就意味着mKeys中之前没有对应的key。因此就需要将key和value分别插入到mKeys和mValues中。而插入的最佳位置就是对i取反。


    得到插入位置之后,如果这个位置是被标记为删除的元素,那么久可以直接将其覆盖掉了,因此有以下代码:


    public void put(int key, E value) {
    // ...
    if (i >= 0) {
    // ...
    } else {
    // 如果i对应的位置是被删除掉的,可以直接将其覆盖
    if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
    }
    // ...
    }

    }

    如果上边条件不满足,那么继续往下看:


    public void put(int key, E value) {
    // ...
    if (i >= 0) {
    // ...
    } else {
    // mGarbage为true说明有元素被移除了,此时mKeys已经满了,但是mKeys内部有被标记为DELETE的元素
    if (mGarbage && mSize >= mKeys.length) {
    // 调用gc方法移动mKeys和mValues中的元素,这个方法可以后边分析
    gc();

    // 由于gc方法移动了数组,因此插入位置可能有变化,所以需要重新计算插入位置
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
    }
    // ...
    }

    }

    上边我们已经知道,在remove元素的时候mGarbage会被置为true,这段代码意味着有被移除的元素,被移除的位置并不是要插入的位置,并且如果mKeys已经满了,那么就调用gc方法来移动元素填充被移除的位置。由于mKeys中元素位置发生了变化,因此key插入的位置也可能改变,因此需要再次调用二分法来查找key的插入位置。


    以上代码最终会确定key被插入的位置,接下来调用GrowingArrayUtils的insert方法来进行key的插入操作:


    // SparseArray
    public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    // ...
    } else {
    // ...

    // GrowingArrayUtils的insert方法将会将插入位置之后的所有数据向后移动一位,然后将key和value分别插入到mKeys和mValue对应的第i个位置,如果数组空间不足还会开启扩容,后边分析这个insert方法
    mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
    mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
    mSize++;
    }
    }

    GrowingArrayUtils的insert方法代码如下:


    // GrowingArrayUtils
    public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    assert currentSize <= array.length;
    // 如果插入后数组size小于数组长度,能进行插入操作
    if (currentSize + 1 <= array.length) {
    // 将index之后的所有元素向后移动一位
    System.arraycopy(array, index, array, index + 1, currentSize - index);
    // 将key插入到index的位置
    array[index] = element;
    return array;
    }

    // 来到这里说明数组已满,需需要进行扩容操作。newArray即为扩容后的数组
    T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
    growSize(currentSize));
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
    }

    // 返回扩容后的size
    public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
    }

    insert方法的代码比较容易理解,如果数组容量足够,那么就将index之后的元素向后移动一位,然后将key插入index的位置。如果数组容量不足,那么则需要进行扩容,然后再进行插入操作。


    四、SparseArray的gc()方法


    这个方法其实很容易理解,我们知道Java虚拟机在内存不足时会进行GC操作,标记清除法在回收垃圾对象后为了避免内存碎片化,会将存活的对象向内存的一端移动。而SparseArray中的这个gc方法其实就是借鉴了垃圾收集整理碎片空间的思想。


    关于mGarbage这个参数上边已经有提到过了,这个变量会在删除元素的时候被置为true。如下:


    // SparseArray中所有移除元素的方法中都将mGarbage置为true

    public E removeReturnOld(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    final E old = (E) mValues[i];
    mValues[i] = DELETED;
    mGarbage = true;
    return old;
    }
    }
    return null;
    }

    public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    mValues[i] = DELETED;
    mGarbage = true;
    }
    }
    }


    public void removeAt(int index) {
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
    throw new ArrayIndexOutOfBoundsException(index);
    }
    if (mValues[index] != DELETED) {
    mValues[index] = DELETED;
    mGarbage = true;
    }
    }



    而SparseArray中所有插入和查找元素的方法中都会判断如果mGarbage为true,并且mSize >= mKeys.length时调用gc,以append方法为例,代码如下:


    public void append(int key, E value) {

    if (mGarbage && mSize >= mKeys.length) {
    gc();
    }

    // ... 省略无关代码
    }

    源码中调用gc方法的地方多达8处,都是与添加和查找元素相关的方法。例如put()、keyAt()、setValueAt()等方法中。gc的实现其实比较简单,就是将删除位置后的所有数据向前移动一下,代码如下:


    private void gc() {
    // Log.e("SparseArray", "gc start with " + mSize);

    int n = mSize;
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
    Object val = values[i];

    if (val != DELETED) {
    if (i != o) {
    keys[o] = keys[i];
    values[o] = val;
    values[i] = null;
    }

    o++;
    }
    }

    mGarbage = false;
    mSize = o;

    // Log.e("SparseArray", "gc end with " + mSize);
    }

    五、SparseArray的get()方法


    这个方法就比较简单了,因为put的时候是维持了一个有序数组,因此通过二分查找可以直接确定key在数组中的位置。


    public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i < 0 || mValues[i] == DELETED) {
    return valueIfKeyNotFound;
    } else {
    return (E) mValues[i];
    }
    }

    六、总结


    可见SparseArray是一个使用起来很简单的数据结构,但是它的原理理解起来似乎却没那么容易。这也是网上大部分文章对应SparseArray的解析都是含糊不清的原因。相信通过本篇文章的学习一定对SparseArray的实现有了新的认识!


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

    iOS 音视频编解码----H264-I(关键)帧,B/P(参考)帧

    内容元素1.图像(image)2.音频(Audio)3.元素信息(Meta-data)编码格式1.Video:H2642.Audio:AAC3.容器封装:MP4/MOV/FLV/RM/RMVB/AVIH264当我们需要对发送的视频文件进行编码时,只要是H264...
    继续阅读 »




  • 内容元素

    1.图像(image)


    2.音频(Audio)


    3.元素信息(Meta-data)


  • 编码格式

    1.Video:H264


    2.Audio:AAC


    3.容器封装:MP4/MOV/FLV/RM/RMVB/AVI


  • H264

    当我们需要对发送的视频文件进行编码时,只要是H264文件,AVFoundation都提供视频编解码器支持,这个标准被广泛应用于消费者视频摄像头捕捉到的资源并成为网页流媒体视频最主要的格式。H264规范是MPEG4定义的一部分,H264遵循早期的MPEG-1/MPEG-2标准,但是在以更低比特率得到 更高图片质量方面有了进步。


  • 编码的本质






    • 为什么要编码?

    举个例子: (具体大小我没数过,只做讲解参考)
    你的老公,Helen,将于明天晚上6点零5份在重庆的江北机场接你
    ----------------------23 * 2 + 10 = 56个字符--------------------------


    你的老公将于明天晚上6点零5分在江北机场接你
    ----------------------20 * 2 + 2 = 42个字符----------------------------


    Helen将于明天晚上6点在机场接你
    ----------------------10 * 2 + 2 = 26个字符----------------------------


    相信大家看了这个例子之后,心里应该大概明白编码的本质:只要不接收方不会产生误解,就可以产生数据的承载量,编码视频的本质也是如此,编码的本质就是减少数据的冗余


    • 在引入I(关键)帧,B/P(参考)帧之前,我们先来了解一下人眼和视频摄像头的对帧的识别

      我们人眼看实物的时候,一般的一秒钟只要是连续的16帧以上,我们就会认为事物是动的,对于摄像头来说,一秒钟所采集的图片远远高于了16帧,可以达到几十帧,对于一些高质量的摄像头,一秒钟可以达到60帧,对于一般的事物来说,你一秒钟能改变多少个做动作?所以,当摄像头在一秒钟内采集视频的时候,前后两帧的图片数据里有大量的相同数据,对于这些数据我们该怎么处理了?当然前后两帧也有不同





  • I帧(I-frames,也叫关键帧)


    • 我们知道在视频的传输过程中,它是分片段传输的,这个片段的第一帧,也就是第一张图片,就是I帧,也就是传说中的关键帧
    • I帧:也就是关键帧,帧内压缩(也就是压缩独立视频帧,被称为帧内压缩),帧内压缩通过消除包含在每个独立视频帧内的色彩以及结构中的冗余信息来进行压缩,因此可在不降低图片适量的情况下尽可能的缩小尺寸,这类似于JEPG压缩的原理,帧内压缩也可以称为有损压缩算法,但通常用于对原因图片的一部分进行处理,以生成极高质量的照片,通过这一过程的创建的帧称为I-frames;将第一帧完整的保存下来.如果没有这个关键帧后面解码数据,是完成不了的.所以I帧特别关键.

  • P帧(P-frames,又称为预测帧)

    • P帧:向前参考帧(在I帧(关键帧)后,P帧参考关键帧,保存下一帧和前一帧的不同数据).压缩时只参考前一个帧.属于帧间压缩技术.
    • 帧间压缩技术:很多帧被组合在一起作为一组图片(简称GOP),对于GOP所存在的时间维度的冗余可以被消除,如果想象视频文件中的典型场景,就会有一些特定的运动元素的概念,比如行驶中的汽车或者街道上行走的路人,场景的背景环信通道是固定的,或者在一定的时间内,有些元素的改变很小或者不变,这些数据就称为时间上的冗余,这些数据就可以通过帧间压缩的方式进行消除,也就是帧间压缩,视频的第一帧会被作为关键帧完整保存下来.而后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面所有的帧只存储于前一帧的差异.这样就能将数据大大的减少.从而达到一个高压缩率的效果.这就是P帧,保存前后两帧不通的数据

  • B帧(B-frames,又称为双向帧)

    • B帧,又叫双向帧,它是基于使用前后两帧进行编码后得到的帧,几乎不需要存储空间,但是解压过程会消耗较长的时间,因为它依赖周围其他的帧
    • B帧的特点:B帧使得视频的压缩率更高.存储的数据量更小.如果B帧的数量越多,你的压缩率就越高.这是B帧的优点,但是B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.如果网络状态很好的话,解码会比较快,如果网络不好时解码会稍微慢一些.丢包时还需要重传.对实时互动的直播,一般不会使用B帧.如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧.如果我们在实时互动的直播,我们需要提高时效性,这时就不能使用B帧了.

  • GOP一组帧的理解

    • 如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组.

    • 什么叫一组帧?
      就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧.我们称为GOP






    • 视频花屏/卡顿原因

      • 我们平常在观看视频的时候,出现视频的花屏或者卡顿,第一反应就是我们的网络出现了问题,其实我们的网络没有问题,是我们在解码的时候I帧,B/P帧出现了丢失
      • 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误.
      • 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像.
      • 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因.

    所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.



    作者:枫紫
    链接:https://www.jianshu.com/p/94d2a8bbc3ac





    收起阅读 »

    iOS 音视频编解码基本概念

    内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC容器封装: • MP4/MOV/FLV/RM/RMVB/AVI.视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面...
    继续阅读 »



    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 容器封装: • MP4/MOV/FLV/RM/RMVB/AVI

    • .视频相关基础概念

      • 1.视频文件格式

        相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.jpg等,我们常见的视频文件后缀有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。这些后缀名通常在操作系统上用相应的应用程序打开,比如.doc用word打开。对于视频来说,为什么会有这么多的文件格式了,那是因为通过了不同的方式来实现了视频这件事---------视频的封装格式。
    • 2.视频的封装格式

      视频封装格式,通常我们把它称作为视频格式,它相当于一种容器,比如可乐的瓶子,矿泉水瓶等等。它里面包含了视频的相关信息(视频信息,音频信息,解码方式等等),一种封装格式直接反应了视频的文件格式,封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.




    • 相关视频封装格式的优缺点:

      • 1.AVI 格式:这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。
      • 2.WMV 格式:可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。
      • 3.MPEG 格式:为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。
      • 4.Matroska 格式:是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。
      • 5.Real Video 格式:用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。
      • 6.QuickTime File Format 格式:是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。
      • 7.Flash Video 格式: Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。
    • 视频的编码格式

    • 视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:

      • 视频的质量、
      • 用来表示视频所需要的数据量(通常称之为码率)、
      • 编码算法和解码算法的复杂度
      • 针对数据丢失和错误的鲁棒性(Robustness)
      • 编辑的方便性
      • 随机访问
      • 编码算法设计的完美性
      • 端到端的延时以及其它一些因素
    • 常见的编码方式:

    • H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

      • H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
      • H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
      • H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
      • H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
    • 当前不建议用H.265是因为太过于消耗CPU,而且目前H.264已经满足了大多的视频需求,虽然H.265是H.264的升级版,期待后续硬件跟上

    • MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

      • MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
      • MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
      • MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
        其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。
    • 可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

    • 音频编码方式

      • 视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

      • AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。

      • MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。

      • WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

    直播/小视频中的编码格式

    • 视频编码格式

      • H264编码的优势:
        低码率
        高质量的图像
        容错能力强
        网络适应性强
    • 总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
      举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
      音频编码格式:

    • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

    • LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)

    • HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)

    • 优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

    关于H264

    • H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

    • 图像

      • H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

    当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

    当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

    「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好




    • 片(Slice),每一帧图像可以分为多个片

    网络提取层单元(NALU, Network Abstraction Layer Unit),
    NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

    宏块(Macroblock),分片是由宏块组成。




    作者:枫紫
    链接:https://www.jianshu.com/p/9602f3c9b82b


    收起阅读 »

    iOS 特效 - iCarousel

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其...
    继续阅读 »

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其他“CoverFlow”库不同,iCarousel 可以处理任何类型的视图,而不仅仅是图像,因此它非常适合在您的应用程序中以流畅且令人印象深刻的方式呈现分页数据。它还使得以最少的代码更改在不同的轮播效果之间切换变得非常容易。

    支持的操作系统和 SDK 版本

    • 支持的构建目标 - iOS 10.0 / Mac OS 10.12(Xcode 8.0,Apple LLVM 编译器 8.0)
    • 最早支持的部署目标 - iOS 5.0 / Mac OS 10.7
    • 最早的兼容部署目标 - iOS 4.3 / Mac OS 10.6

    注意:“支持”表示该库已经过此版本的测试。“兼容”意味着库应该在这个操作系统版本上工作(即它不依赖于任何不可用的 SDK 功能)但不再进行兼容性测试,可能需要调整或错误修复才能正确运行。

    ARC兼容性

    从 1.8 版开始,iCarousel 需要 ARC。如果您希望在非 ARC 项目中使用 iCarousel,只需将 -fobjc-arc 编译器标志添加到 iCarousel.m 类。为此,请转到目标设置中的 Build Phases 选项卡,打开 Compile Sources 组,双击列表中的 iCarousel.m 并在弹出窗口中键入 -fobjc-arc。

    如果您希望将整个项目转换为 ARC,请在 iCarousel.m 中注释掉 #error 行,然后在 Xcode 中运行 Edit > Refactor > Convert to Objective-C ARC... 工具并确保您希望转换的所有文件使用 ARC 进行(包括 iCarousel.m)检查。

    线程安全

    iCarousel 派生自 UIView 并且 - 与所有 UIKit 组件一样 - 它只能从主线程访问。您可能希望使用线程来加载或更新轮播内容或项目,但始终确保一旦您的内容加载完毕,您就可以在更新轮播前切换回主线程。

    安装

    要在应用程序中使用 iCarousel 类,只需将 iCarousel 类文件(不需要演示文件和资产)拖到您的项目中并添加 QuartzCore 框架。您也可以使用 Cocoapods 以正常方式安装它。


    轮播类型

    iCarousel 支持以下内置显示类型:

    • iCarouselTypeLinear
    • iCarouselTypeRotary
    • iCarouselTypeInvertedRotary
    • iCarouselTypeCylinder
    • iCarouselTypeInvertedCylinder
    • iCarouselTypeWheel
    • iCarouselTypeInvertedWheel
    • iCarouselTypeCoverFlow
    • iCarouselTypeCoverFlow2
    • iCarouselTypeTimeMachine
    • iCarouselTypeInvertedTimeMachine

    您还可以使用iCarouselTypeCustomcarousel:itemTransformForOffset:baseTransform:委托方法实现自己的定制轮播样式

    注意:iCarouselTypeCoverFlowiCarouselTypeCoverFlow2类型之间的区别非常微妙,但是 for 的逻辑要iCarouselTypeCoverFlow2复杂得多。如果您轻弹转盘,它们基本上是相同的,但是如果您用手指缓慢拖动转盘,则差异应该很明显。iCarouselTypeCoverFlow2旨在尽可能接近地模拟标准 Apple CoverFlow 效果,并且将来可能会为了该目标而进行微妙的更改。

    显示类型可视化示例

    线性

    线性

    旋转式

    旋转式


    倒转

    倒转


    圆筒

    圆筒

    倒置气缸

    倒置气缸

    Cover Flow功能

    Cover Flow功能



    特性

    iCarousel 具有以下属性(注意:对于 Mac OS,在使用属性时将 NSView 替换为 UIView):

    @property (nonatomic, weak) IBOutlet id dataSource;

    一个支持 iCarouselDataSource 协议并可以提供视图来填充轮播的对象。

    @property (nonatomic, weak) IBOutlet id delegate;

    一个支持 iCarouselDelegate 协议并且可以响应轮播事件和布局请求的对象。

    @property (nonatomic, assign) iCarouselType type;

    用于切换轮播显示类型(详见上文)。

    @property (nonatomic, assign) CGFloat perspective;

    用于调整各种 3D 轮播视图的透视缩短效果。应为负值,小于 0 且大于 -0.01。超出此范围的值将产生非常奇怪的结果。默认值为 -1/500 或 -0.005;

    @property (nonatomic, assign) CGSize contentOffset;

    此属性用于调整轮播项目视图相对于轮播中心的偏移。它默认为 CGSizeZero,这意味着轮播项目居中。更改此值会移动轮播项目而不改变其视角,即消失点随轮播项目移动,因此如果您将轮播项目向下移动,则不会看起来好像您在俯视轮播。

    @property (nonatomic, assign) CGSize viewpointOffset;

    此属性用于调整相对于轮播项目的用户视角。它与调整 contentOffset 有相反的效果,即如果您向上移动视点,则轮播似乎向下移动。与 contentOffset 不同,移动视点也会改变相对于旋转木马项目的透视消失点,因此如果您向上移动视点,它会看起来好像您在俯视旋转木马。

    @property (nonatomic, assign) CGFloat decelerationRate;

    旋转木马在轻弹时减速的速率。较高的值意味着较慢的减速。默认值为 0.95。值应在 0.0(释放时旋转木马立即停止)到 1.0(旋转木马无限期地继续而不减速,除非它到达终点)的范围内。

    @property (nonatomic, assign) BOOL bounces;

    设置旋转木马是应该弹过终点并返回,还是停止不动。请注意,这对设计为包装的轮播类型或 carouselShouldWrap 委托方法返回 YES 的类型没有影响。

    @property (nonatomic, assign) CGFloat bounceDistance;

    未包裹的传送带越过末端时反弹的最大距离。这是以 itemWidth 的倍数来衡量的,因此值 1.0 表示轮播将反弹整个项目宽度,值 0.5 表示项目宽度的一半,依此类推。默认值为 1.0;

    @property (nonatomic, assign, getter = isScrollEnabled) BOOL scrollEnabled;

    启用和禁用用户滚动轮播。如果此属性设置为 NO,则仍然可以通过编程方式滚动轮播。

    @property (nonatomic, readonly, getter = isWrapEnabled) BOOL wrapEnabled;

    如果启用包装,则返回 YES,否则返回 NO。此属性是只读的。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 返回一个值iCarouselOptionWrap

    @property (nonatomic, assign, getter = isPagingEnabled) BOOL pagingEnabled;

    启用和禁用分页。启用分页后,轮播将在用户滚动时在每个项目视图处停止,这与 UIScrollView 的 pagingEnabled 属性非常相似。

    @property (nonatomic, readonly) NSInteger numberOfItems;

    轮播中的项目数(只读)。要设置它,请实现numberOfItemsInCarousel:dataSource 方法。请注意,并非所有这些项目视图都会在给定的时间点加载或可见 - 轮播在滚动时按需加载项目视图。

    @property (nonatomic, readonly) NSInteger numberOfPlaceholders;

    要在轮播中显示的占位符视图的数量(只读)。要设置它,请实现numberOfPlaceholdersInCarousel:dataSource 方法。

    @property (nonatomic, readonly) NSInteger numberOfVisibleItems;

    屏幕上同时显示的轮播项目视图的最大数量(只读)。此属性对于性能优化很重要,并且会根据轮播类型和视图框架自动计算。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 iCarouselOptionVisibleItems 返回一个值。

    @property (nonatomic, strong, readonly) NSArray *indexesForVisibleItems;

    一个数组,包含当前加载和在轮播中可见的所有项目视图的索引,包括占位符视图。该数组包含 NSNumber 对象,其整数值与视图的索引匹配。项目视图的索引从零开始并匹配传递给 dataSource 以加载视图的索引,但是任何可见占位符视图的索引要么是负数(小于零)要么大于或等于numberOfItems此数组中占位符视图的索引等同于与 dataSource 一起使用的占位符视图索引。

    @property (nonatomic, strong, readonly) NSArray *visibleItemViews;

    当前显示在轮播中的所有项目视图的数组(只读)。这包括任何可见的占位符视图。此数组中的视图索引与项目索引不匹配,但是这些视图的顺序与 visibleItemIndexes 数组属性的顺序匹配,即您可以通过从visibleItemIndexes 数组(或者,您可以只使用该indexOfItemView:方法,这要容易得多)。

    @property (nonatomic, strong, readonly) UIView *contentView;

    包含轮播项目视图的视图。如果您想将它们与轮播项目散布,您可以向此视图添加子视图。如果您希望视图出现在所有轮播项目的前面或后面,您应该将其直接添加到 iCarousel 视图本身。请注意,当应用程序运行时, contentView 中的视图顺序会经常发生且未记录的更改。添加到 contentView 的任何视图都应将其 userInteractionEnabled 属性设置为 NO 以防止与 iCarousel 的触摸事件处理发生冲突。

    @property (nonatomic, assign) CGFloat scrollOffset;

    这是轮播的当前滚动偏移量,是 itemWidth 的倍数。这个值,四舍五入到最接近的整数,是 currentItemIndex 值。您可以使用此值在轮播移动时定位其他屏幕元素。如果您希望以编程方式将轮播滚动到特定偏移量,也可以设置该值。如果您希望禁用内置手势处理并提供您自己的实现,这可能很有用。

    @property (nonatomic, readonly) CGFloat offsetMultiplier;

    这是用户用手指拖动轮播时使用的偏移乘数。它不影响编程滚动或减速速度。对于大多数轮播类型,这默认为 1.0,但对于 CoverFlow 风格的轮播默认为 2.0,以补偿它们的项目间隔更近的事实,因此必须进一步拖动以移动相同的距离。您不能直接设置此属性,但可以通过实现carouselOffsetMultiplier:委托方法来覆盖默认值

    @property (nonatomic, assign) NSInteger currentItemIndex;

    轮播中当前居中项目的索引。设置此属性等效于scrollToItemAtIndex:animated:将动画参数设置为 NO进行调用

    @property (nonatomic, strong, readonly) UIView *currentItemView;

    轮播中当前居中的项目视图。此视图的索引匹配currentItemIndex

    @property (nonatomic, readonly) CGFloat itemWidth;

    轮播中项目的显示宽度(只读)。这是从使用carousel:viewForItemAtIndex:reusingView:dataSource 方法传递给轮播的第一个视图自动派生的您还可以使用carouselItemWidth:委托方法覆盖此值,这将更改为轮播项目分配的空间(但不会调整项目视图的大小或缩放)。

    @property (nonatomic, assign) BOOL centerItemWhenSelected;

    当设置为 YES 时,点击轮播中除与 currentItemIndex 匹配的项目之外的任何项目都会使其平滑地动画到中心。点击当前选定的项目将不起作用。默认为是。

    @property (nonatomic, assign) CGFloat scrollSpeed;

    这是用户用手指轻弹轮播时的滚动速度倍增器。默认为 1.0。

    @property (nonatomic, readonly) CGFloat toggle;

    此属性用于iCarouselTypeCoverFlow2轮播变换。它是公开的,以便您可以使用carousel:itemTransformForOffset:baseTransform:委托方法实现自己的 CoverFlow2 样式变体

    @property (nonatomic, assign) BOOL stopAtItemBoundary;

    默认情况下,轮播将在轻弹时停在确切的项目边界处。如果将此属性设置为 NO,它将自然停止,然后 - 如果 scrollToItemBoundary 设置为 YES - 向后或向前滚动到最近的边界。

    @property (nonatomic, assign) BOOL scrollToItemBoundary;

    默认情况下,当轮播停止移动时,它会自动滚动到最近的项目边界。如果将此属性设置为 NO,则轮播在停止后将不会滚动并停留在它所在的位置,即使它在当前索引上没有完全对齐。例外情况是,如果 wrapping 被禁用并bounces设置为 YES,那么无论此设置如何,如果轮播结束后停止,轮播将自动滚动回第一个或最后一个项目索引。

    @property (nonatomic, assign, getter = isVertical) BOOL vertical;

    此属性切换轮播是在屏幕上水平显示还是垂直显示。所有内置的轮播类型都适用于两个方向。切换到垂直会更改轮播的布局以及屏幕上滑动检测的方向。请注意,自定义轮播变换不受此属性影响,但滑动手势方向仍会受到影响。

    @property (nonatomic, readonly, getter = isDragging) BOOL dragging;

    如果用户已开始滚动轮播但尚未释放它,则返回 YES。

    @property (nonatomic, readonly, getter = isDecelerating) BOOL decelerating;

    如果用户不再拖动轮播,但它仍在移动,则返回 YES。

    @property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;

    如果当前正在以编程方式滚动轮播,则返回 YES。

    @property (nonatomic, assign) BOOL ignorePerpendicularSwipes;

    如果是,则轮播将忽略与轮播方向垂直的滑动手势。所以对于水平轮播,垂直滑动不会被拦截。这意味着您可以在旋转木马项目视图中拥有一个垂直滚动的 scrollView,它仍然可以正常工作。默认为是。

    @property (nonatomic, assign) BOOL clipsToBounds;

    这实际上不是 iCarousel 的属性,而是继承自 UIView。它包含在此处是因为它是一个经常被遗漏的功能。将此设置为 YES 以防止轮播项目视图溢出其边界。您可以通过勾选“剪辑子视图”选项在界面生成器中设置此属性。默认为否。

    @property (nonatomic, assign) CGFloat autoscroll;

    此属性可用于设置轮播以恒定速度滚动。值为 1.0 将以每秒一项的速度向前滚动轮播。自动滚动值可以为正也可以为负,默认为 0.0(固定)。如果用户与轮播交互,自动滚动将停止,并在他们停止时恢复。

    方法

    iCarousel 类具有以下方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (void)scrollToItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将使轮播在指定的项目上居中,无论是立即还是平滑的动画。对于包裹式轮播,轮播将自动确定要滚动的最短(直接或环绕)距离。如果您需要控制滚动方向,或者想要滚动一圈以上,请改用 scrollByNumberOfItems 方法。

    - (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)scrollDuration;

    此方法允许您控制轮播滚动到指定索引所需的时间。

    - (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration;

    此方法允许您将轮播滚动固定距离,以轮播项目宽度为单位。可以为 itemCount 指定正值或负值,具体取决于您希望滚动的方向。iCarousel 优雅地处理边界问题,因此如果您指定的距离大于轮播中项目的数量,滚动将在到达轮播结束时被限制(如果环绕被禁用)或无缝环绕。

    - (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollToItemAtIndex:,但允许您滚动到小数偏移量。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则调用此方法后,轮播将自动滚动到最近的项目索引。反正。

    - (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollByNumberOfItems:,但允许您滚动项目的小数部分。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则无论如何调用此方法后,轮播都会自动滚动到最近的项目索引。

    - (void)reloadData;

    这将从数据源重新加载所有轮播视图并刷新轮播显示。

    - (UIView *)itemViewAtIndex:(NSInteger)index;

    返回具有指定索引的可见项视图。请注意,索引与轮播中的位置有关,而不是在visibleItemViews数组中的位置,这可能会有所不同。传递负索引或大于或等于的索引numberOfItems以检索占位符视图。该方法仅适用于可见的项目视图,如果指定索引处的视图尚未加载,或者索引超出范围,则返回 nil。

    - (NSInteger)indexOfItemView:(UIView *)view;

    轮播中给定项目视图的索引。适用于项目视图和占位符视图,但是占位符视图索引与数据源使用的索引不匹配,并且可能为负数(indexesForVisibleItems有关更多详细信息,请参阅上面的属性)。此方法仅适用于可见的项目视图,并且将为当前未加载的视图返回 NSNotFound。要获取所有当前加载的视图的列表,请使用该visibleItemViews属性。

    - (NSInteger)indexOfItemViewOrSubview:(UIView *)view

    此方法为您提供传递的视图或包含作为参数传递的视图的视图的项目索引。它的工作方式是从传递的视图开始沿着视图层次结构向上移动,直到找到一个项目视图并在轮播中返回其索引。如果未找到当前加载的项目视图,则返回 NSNotFound。此方法对于处理嵌入在项目视图中的控件上的事件非常有用。这允许您将所有项目控件绑定到视图控制器上的单个操作方法,然后确定触发操作的控件与哪个项目相关。您可以在Controls Demo示例项目中看到此技术的示例。

    - (CGFloat)offsetForItemAtIndex:(NSInteger)index;

    itemWidth以中心位置的倍数返回指定项索引的偏移量这与用于计算视图变换和 alpha 的值相同,可用于根据它们在轮播中的位置自定义项目视图。每当carouselDidScroll:调用委托方法时,每个视图的这个值都会发生变化

    - (UIView *)itemViewAtPoint:(CGPoint)point;

    返回轮播边界内指定点的最前面的项目视图。用于实现您自己的点击检测。

    - (void)removeItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将从轮播中删除一个项目。其余项目将滑过以填补空白。请注意,调用此方法时数据源不会自动更新,因此后续调用 reloadData 将恢复已删除的项目。

    - (void)insertItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这会将一个项目插入到轮播中。新的item会从dataSource中请求,所以在调用这个方法之前要确保新的item已经添加到数据源data中,否则会在carousel中得到重复的item,或者其他怪事。

    - (void)reloadItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    此方法将重新加载指定的项目视图。将从数据源请求新项目。如果动画参数为 YES,它将从旧项目视图交叉淡入淡出到新项目视图,否则将立即交换。

    协议

    iCarousel 通过提供两个协议接口 iCarouselDataSource 和 iCarouselDelegate 来遵循 Apple 的数据驱动视图约定。iCarouselDataSource 协议具有以下必需的方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel;

    返回轮播中的项目(视图)数。

    - (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示在轮播中指定索引处的视图。reusingView参数的工作方式类似于 UIPickerView,其中先前显示在轮播中的视图被传递回要回收的方法。如果这个参数不是 nil,你可以设置它的属性并返回它,而不是创建一个新的视图实例,这会稍微提高性能。与 UITableView 不同,没有用于区分不同轮播视图类型的重用标识符,因此如果您的轮播包含多个不同的视图类型,那么您应该忽略此参数并在每次调用该方法时返回一个新视图。您应该确保每次carousel:viewForItemAtIndex:reusingView: 方法被调用时,它要么返回 reusingView 要么返回一个全新的视图实例,而不是维护自己的可回收视图池,因为为不同的轮播项目索引返回同一视图的多个副本可能会导致轮播显示问题。

    iCarouselDataSource 协议有以下可选方法:

    - (NSUInteger)numberOfPlaceholdersInCarousel:(iCarousel *)carousel;

    返回要在轮播中显示的占位符视图的数量。当轮播中的项目数量太少而无法填充轮播宽度,并且您希望在空白空间中显示某些内容时,将使用占位符视图。它们与轮播一起移动并且行为与任何其他轮播项目一样,但它们不计入 numberOfItems 值,并且不能设置为当前选定的项目。启用换行时,占位符会隐藏。占位符出现在轮播项目的两侧。对于 n 个占位符视图,前 n/2 个项目将出现在项目视图的左侧,接下来的 n/2 个项目将出现在右侧。您可以有奇数个占位符,在这种情况下,轮播将是不对称的。

    - (UIView *)carousel:(iCarousel *)carousel placeholderViewAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示为占位符视图的视图。工作方式与carousel:viewForItemAtIndex:reusingView:占位符 reusingViews 与用于常规轮播的 reusingViews 存储在单独的池中,因此如果您的占位符视图与项目视图不同,这不是问题。

    iCarouselDelegate 协议具有以下可选方法:

    - (void)carouselWillBeginScrollingAnimation:(iCarousel *)carousel;

    每当轮播开始动画滚动时,都会调用此方法。这可以在用户完成滚动轮播后以编程方式或自动触发,因为轮播会重新对齐自身。

    - (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel;

    当轮播结束动画滚动时调用此方法。

    - (void)carouselDidScroll:(iCarousel *)carousel;

    每当滚动轮播时都会调用此方法。无论轮播是通过编程还是通过用户交互滚动,它都会被调用。

    - (void)carouselCurrentItemIndexDidChange:(iCarousel *)carousel;

    每当轮播滚动到足以改变 currentItemIndex 属性时,就会调用此方法。无论项目索引是以编程方式更新还是通过用户交互更新,都会调用它。

    - (void)carouselWillBeginDragging:(iCarousel *)carousel;

    当用户开始拖动轮播时调用此方法。如果用户点击/点击轮播,或者轮播以编程方式滚动,它不会触发。

    - (void)carouselDidEndDragging:(iCarousel *)carousel willDecelerate:(BOOL)decelerate;

    当用户停止拖动轮播时调用此方法。willDecelerate 参数指示转盘是否行得足够快以至于它在停止之前需要减速(即当前索引不一定是它将停止的索引),或者它是否会在它所在的位置停止。请注意,即使 willDecelerate 为 NO,轮播仍会自动滚动,直到它与当前索引完全对齐。如果您需要知道它何时完全停止移动,请使用 carouselDidEndScrollingAnimation 委托方法。

    - (void)carouselWillBeginDecelerating:(iCarousel *)carousel;

    当轮播开始减速时调用此方法。它通常会在 carouselDidEndDragging:willDecelerate: 方法之后立即调用,假设 willDecelerate 为 YES。

    - (void)carouselDidEndDecelerating:(iCarousel *)carousel;

    当轮播完成减速时调用此方法,您可以假设此时的 currentItemIndex 是最终停止值。与以前的版本不同,在大多数情况下,轮播现在将准确地停在最终索引位置。唯一的例外是启用了弹跳的非包裹式转盘,如果最终停止位置超出转盘的末端,则转盘将自动滚动,直到它与结束索引完全对齐。为了向后兼容,轮播将始终scrollToItemAtIndex:animated:在完成减速后调用如果您需要确定轮播何时完全停止移动,请使用carouselDidEndScrollingAnimation委托方法。

    - (CGFloat)carouselItemWidth:(iCarousel *)carousel;

    返回轮播中每个项目的宽度 - 即每个项目视图的间距。如果未实现该方法,则默认为carousel:viewForItemAtIndex:reusingView:dataSource 方法返回的第一个项目视图的宽度如果从返回的视图carousel:viewForItemAtIndex:reusingView:不正确(例如,如果视图大小不同,或者在其背景图像中包含影响其大小的投影或外部发光),则此方法应仅用于裁剪或填充项目视图- 如果您只是想要将视图隔开一点,那么最好使用该iCarouselOptionSpacing值。

    - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform;

    此方法可用于为每个轮播视图提供自定义转换。offset 参数是视图与旋转木马中间的距离。当前居中的项目视图的偏移量为 0.0,右侧的偏移值为 1.0,左侧的偏移值为 -1.0,依此类推。要实现线性轮播样式,您只需将偏移值乘以项目宽度并将其用作变换的 x 值。仅当轮播类型为 iCarouselTypeCustom 时才会调用此方法。

    - (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value;

    该方法用于自定义标准轮播类型的参数。通过实施此方法,您可以调整选项,例如圆形转盘中显示的项目数量,或coverflow 转盘中的倾斜量,以及转盘是否应环绕以及是否应在末端淡出等. 对于任何您不想调整的选项,只需返回默认值即可。这些选项的含义在下面的iCarouselOption 值下列出检查选项演示以获取使用此方法的高级示例。

    - (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。如果用户点击当前选定视图中的控件(即作为 UIControl 子类的任何视图),则不会触发此方法。

    - (BOOL)carousel:(iCarousel *)carousel shouldSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。方法的目的是让您有机会忽略轮播上的点击。如果你从方法中返回 YES,或者没有实现它,tap 将正常处理并carousel:didSelectItemAtIndex:调用方法。如果您返回 NO,轮播将忽略点击并继续向上传播视图层次结构。这是防止轮播拦截打算由另一个视图处理的点击事件的好方法。


    检测项目视图上的点击

    在 iOS 上的 iCarousel 中检测点击视图有两种基本方法。第一种方法是简单地使用carousel:didSelectItemAtIndex:委托方法,每次点击项目时都会触发方法。如果您只对点击当前居中的项目感兴趣,您可以将该currentItemIndex属性与此方法的 index 参数进行比较

    或者,如果您想要更多控制,您可以提供 UIButton 或 UIControl 作为项目视图并自己处理触摸交互。有关如何完成此操作的示例,请参阅按钮演示示例项目(不适用于 Mac OS;见下文)。

    您还可以在您的项目视图中嵌套 UIControls,这些将按预期接收触摸(请参阅Controls Demo示例项目以获取示例)。

    如果您希望检测其他类型的交互,例如滑动、双击或长按,最简单的方法是将 UIGestureRecognizer 附加到您的项目视图或其子视图,然后再将其传递给轮播。

    请注意,除了当前选定的项目视图之外,任何项目视图上的点击和手势都将被忽略,除非您将该centerItemWhenSelected属性设置为 NO。

    在 Mac OS 上,目前没有简单的方法可以在 iCarousel 项目视图中嵌入控件。您不能只在项目视图中或在项目视图中提供 NSButton,因为应用于项目视图的转换意味着命中检测无法正常工作。我正在研究可能的解决方案(如果您知道解决此问题的好方法,请与我们联系,或在 github 上 fork 项目)。

    demo及常见问题:https://github.com/nicklockwood/iCarousel

    源码下载:iCarousel-master.zip


    收起阅读 »

    iOS 应用分享平台fir使用遇到的一些坑

    前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分...
    继续阅读 »

    前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分享出来给大家,希望能对你有所帮助。

    首先要确定你们使用平台的需求,我这里有蒲公英(fir同类型网站)对于应用分享需求的介绍


    如果只是小范围的几个人来安装,使用Ad-hoc方式,去一个个添加UDID就好了,好处是使用你自己的免费证书也可以申请。
    如果是想做线下推广,没办法及时获取添加目标UDID的话,最好还是要使用In-house方式,不过装机数量苹果好像还是有一定限制,这个具体政策不太清楚。

    我的目的是给测试团队装机,所以选择Ad-hoc方式做。

    简化下来一共需要三大步
    1 . 在你的Apple Developer 页面的Devices中添加目标的(于我就是“测试团队”)苹果手机UDID。(关于UDID的获取看这里 http://fir.im/udid 这个网址使用苹果手机的Safari浏览器访问)


    在这里点击“+”输入用户的UDID(name是你自己定的,建议起个和此UDID手机拥有者相关的名字,后面会用到),点击下方的注册,会跳转确认注册页面


    确认账号无误后可以点击下方的确定,目标UDID就乖乖加入到你的Devices列表中了😊。

    注意:这里就会有一个坑,我导入的第一个UDID出现这种情况


    你会发现这个缺少了Model:这一项,目前我没有发现是因为什么(隐约赶脚是因为录入这个UDID时,网络或者苹果官网之类的问题😊)。这种账号是无法添加进描述文件的,添加进去也无法识别和使用。

    还有一种情况是你添加了目标UDID,在Devices列表中找不到,再次注册该UDID又会提示它不是有效的,多次尝试无果也只好作罢。

    2 . 在Distribution中添加一个用于测试的描述文件,并在此步骤中添加目标手机到描述文件中。


    在此点击“+”,添加一个新的描述文件。


    选择你需要的方式,我的是Ad-hoc


    然后是选择自己项目


    选择开发者(或团队)


    选择你要添加的目标UDID(此时使用的是你创建Device时的名字)


    给你的描述文件命名(项目中添加Provisioning Profile时使用这个名字)

    creat之后点击下载,描述文件就会下载到电脑。

    这里倒是没有什么坑,就是苹果官网如果访问起来困难,部分页面会不显示你已有的一些资料,会提示要你新建一个项目。如果你确定自己有项目的话,刷新一下就好了。

    3 . 将描述文件添加到Xcode,然后在项目中选择相应的打包选项,生成.ipa文件。然后大功告成,将其上传到fir平台后点击“预览”会自动生成一个带有二维码的网址。(需要使用iphone自带的safari浏览器访问该链接)

    现在可以关掉万恶的苹果官网,来到桌面上,建议先彻底关闭Xcode,然后双击一下你下载下来的描述文件,Xcode会自动打开,此时描述文件就已经添加好了。


    在 Xcode 中点击project图标,在info这个tab下找到configuration设置,里面默认的是debug和release。点击+,选择Duplicate the “Release configuration”,给生成的新东西起个名字,推荐使用ad hoc distribution


    点击targets图标,在build settings这个tab下,找到code signing部分。将Code Signing Identity中的ad hoc distribution证书设置为刚刚导入到 Xcode 中对应测试应用的证书。注意不要改动Debug和Release中的证书。
    在下方的Provisioning Profile中选择你下载下来的描述文件。
    保证target中info这个tab下的bundle indentifier里面有预设值,其必须和provision portal输入匹配。这个很重要,否则将来会出错。


    在Xcode左上角run按钮右侧有一个下拉菜单,选择device或者simulator,点击菜单下方的edit schema。保证Archive中Build Configuration中的值是ad hoc distribution


    配置工作到此结束。点击Product中的Archive,程序开始编译,编译完成后弹出设置框,点选"Export" 然后选"Save for Ad Hoc Develoyment"

    按操作提示就会生成一个.ipa文件。此.ipa可以被安装到之前设置的测试应用设备中。

    然后创建一个fir账号,在其上发布就好了。

    本文借鉴于http://blog.csdn.net/yuanbohx/article/details/9213879
    该博客6楼指出其在文章中的错误,实测6楼所说是正确的。

    链接:https://www.jianshu.com/p/cbda1e434add

    收起阅读 »

    超强的游戏模拟器, 做游戏开发必备 - OpenEmu

    OpenEmuOpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 S...
    继续阅读 »

    OpenEmu

    alt text


    OpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 Sparkle,它用于自动更新。OpenEmu 使用模块化架构,允许使用游戏引擎插件,允许 OpenEmu 支持大量不同的仿真引擎和后端,同时保留熟悉的 macOS 原生前端。

    目前 OpenEmu 可以加载以下游戏引擎作为插件:



    最低要求

    macOS 10.14


    demo及常见问题:https://github.com/OpenEmu/OpenEmu

    源码下载:OpenEmu-master.zip





    收起阅读 »

    Apple 的xcodebuild的扩展!

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。特征xctool是替代品,xcodebuild test它增加了一些额外的功能:更快的并行测试运行。xctool可以选择并行运行所有测...
    继续阅读 »

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。

    特征

    xctool是替代品,xcodebuild test它增加了一些额外的功能:

    • 更快的并行测试运行。

      xctool可以选择并行运行所有测试包,从而显着加快测试运行速度。在 Facebook,通过并行运行,我们看到了 2 倍和 3 倍的加速。

      使用-parallelize带有run-teststest选项来启用。有关详细信息,请参阅并行化测试运行

    • 测试结果的结构化输出。

      xctool将所有测试结果捕获为结构化 JSON 对象。如果您正在构建一个持续集成系统,这意味着您不再需要正则表达式解析xcodebuild输出。

      尝试使用Reporters之一自定义输出或使用该-reporter json-stream选项获取完整的事件流

    • 人性化的 ANSI 颜色输出。

      xcodebuild非常冗长,为每个源文件打印完整的编译命令和输出。默认情况下,xctool仅在出现问题时才详细说明,从而更容易确定问题所在。

    • 用Objective-C编写。

      xctool是用 Objective-C 编写的。Mac OS X 和 iOS 开发人员可以轻松提交新功能并修复他们可能遇到的任何错误,而无需学习新语言。我们非常欢迎拉取请求!

    注意:不推荐使用 xctool 构建项目,并且不会更新以支持 Xcode 的未来版本。我们建议移动到 xcodebuild(使用xcpretty)来满足简单的需求,或者使用xcbuild来满足更多的需求。xctool 将继续支持测试(见上文)。

    要求

    • Xcode 7 或更高版本
    • 您需要安装 Xcode 的命令行工具。从 Xcode,通过Xcode → Preferences → Downloads安装

    安装

    brew install xctool

    xctool 的命令和选项主要是 xcodebuild 的超集。在大多数情况下,您只需将xcodebuildxctool交换,事情就会按预期运行,但输出更具吸引力。

    您始终可以通过以下方式获得帮助和完整的选项列表:

    path/to/xctool.sh -help


    在运行测试之前,您需要构建它们。您可以使用xcodebuild、 xcbuildBuck来做到这一点。

    例如:

    xcodebuild \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-for-testing


    如果您使用 Xcode 7 进行构建,您可以继续使用 xctool 使用 build-tests 构建测试,或者仅使用测试操作来运行测试。

    例如:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-tests

    并行化测试运行

    xctool可以选择并行运行单元测试,从而更好地利用其他空闲的 CPU 内核。在 Facebook,通过并行化我们的测试运行,我们已经看到了 2 倍和 3 倍的收益。

    要允许测试包同时运行,请使用以下-parallelize 选项:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    run-tests -parallelize


    常见问题及demo下载:https://github.com/facebookarchive/xctool

    源码下载:xctool-master.zip



    收起阅读 »

    DKNightVersion 的实现 --- 如何为 iOS 应用添加夜间模式

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。在正式介绍目前版本的实现之前,我会先简单介绍...
    继续阅读 »

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。

    其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。

    在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。

    我们会以对 backgroundColor 为例说明整个框架的工作原理。

    方法调剂的版本

    如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。

    其核心思路就是使用方法调剂修改 backgroundColor 的存取方法。

    使用 nightBackgroundColor

    在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在分类中添加 nightBackgroundColor 属性,并且使用方法调剂改变 backgroundColor 的 setter 方法。

    在当前主题为 DKThemeVersionNormal 时,将颜色保存至 normalBackgroundColor 中,然后再调用原 backgroundColor 的 setter 方法,更新视图的颜色。

    DKNightVersionManager

    这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。

    整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要工作就是负责改变应用的主题、并在主题改变时通知其它视图更新颜色:

    - (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {
    if ([object respondsToSelector:@selector(changeColor)]) {
    [object changeColor];
    }
    if ([object respondsToSelector:@selector(subviews)]) {
    if (![object subviews]) {
    // Basic case, do nothing.
    return;
    } else {
    for (id subview in [object subviews]) {
    // recursive darken all the subviews of current view.
    [self changeColor:subview];
    if ([subview respondsToSelector:@selector(changeColor)]) {
    [subview changeColor];
    }
    }
    }
    }
    }

    如果主题更新,那么就会递归地调用 changeColor 方法,刷新全部的视图颜色,而这个方法的实现比较简单:

    - (void)changeColor {
    if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
    self.backgroundColor = self.normalBackgroundColor;
    } else {
    self.backgroundColor = self.nightBackgroundColor;
    }
    }

    上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:

    1、在高速滚动的 scrollView 上面来回切换夜间模式,会出现颜色错乱的问题
    2、由于对 backgroundColor 属性进行不合适的方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
    3、无法适配第三方 UI 控件

    使用色表的版本

    为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 nightBackgroundColor 的使用,并且重新设计底层的实现,转而使用更为稳定、安全的方法实现夜间模式,先看一下效果图:

    <em>新的实现不仅能够支持夜间模式,而且能够支持多主题。</em>

    DKColorPicker

    与上一个版本实现上的不同,在 2.0 中删除了全部的 nightBackgroundColor,使用一个名为 dk_backgroundColorPicker 的属性取代它。

    @property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;

    这个属性其实就是一个 block,它接收参数 DKThemeVersion *themeVersion,但是会返回一个 UIColor *:

    在第一次传入 picker 或者每次主题改变时,都会将当前主题 DKThemeVersion 传入 picker 并执行,然后,将得到的 UIColor 赋值给对应的属性 backgroundColor 更新视图颜色。

    typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);

    比如下面使用 DKColorPickerWithRGB 创建一个临时的 DKColorPicker:

    1、在 DKThemeVersionNormal 时返回 0xffffff
    2、在 DKThemeVersionNight 时返回 0x343434
    3、在自定义的主题下返回 0xfafafa (这里的顺序与色表中主题的顺序有关)

    cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);

    同时,每一个对象还持有一个 pickers 数组,来存储自己的全部 DKColorPicker:

    @interface NSObject ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    在第一次使用这个属性时,当前对象注册为 DKNightVersionThemeChangingNotificaiton 通知的观察者。

    在每次收到通知时,都会调用 night_update 方法,将当前主题传入 DKColorPicker,并再次执行,并将结果传入对应的属性 [self performSelector:sel withObject:result]。

    - (void)night_updateColor {
    [self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull selector, DKColorPicker _Nonnull picker, BOOL * _Nonnull stop) {
    SEL sel = NSSelectorFromString(selector);
    id result = picker(self.dk_manager.themeVersion);
    [UIView animateWithDuration:DKNightVersionAnimationDuration
    animations:^{
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:sel withObject:result];
    #pragma clang diagnostic pop
    }];
    }];
    }

    也就是说,在每次改变主题的时候,都会发出通知。

    DKColorTable

    虽然我们在上面临时创建了一些 DKColorPicker。不过在 DKNightVersion 中,我更推荐使用色表,来减少相同的 DKColorPicker 的创建,并且能够更好地管理整个应用中的颜色:

    NORMAL   NIGHT    RED
    #ffffff #343434 #fafafa BG
    #aaaaaa #313131 #aaaaaa SEP
    #0000ff #ffffff #fa0000 TINT
    #000000 #ffffff #000000 TEXT
    #ffffff #444444 #ffffff BAR

    上面就是默认色表文件 DKColorTable.txt 中的内容,其中,第一行表示主题,NORMAL 主题必须存在,而且必须为第一列,而最右面的 BG、SEP 就是对应 DKColorPicker 的 key。

    self.tableView.dk_backgroundColorPicker =  DKColorPickerWithKey(BG);

    在使用时,上面的代码就相当于返回了一个在 NORMAL 时返回 #ffffff、NIGHT 时返回 #343434 以及 RED 时返回 #fafafa 的 DKColorPicker。

    pickerify

    虽然说,我们使用色表以及 DKColorPicker 解决了,但是,到目前为止我们还没有解决第三方框架的问题。

    比如我们使用了某个第三方框架,或者自己添加了某个 color 属性,比如说:

    @interface DKView ()

    @property (nonatomic, strong) UIColor *weirdColor;

    @end

    weirdColor 并没有对应的 DKColorPicker,但是,我们可以通过 pickerify 在想要使用 dk_weirdColorPicker 的地方生成这个对应的 picker:

    @pickerify(DKView, weirdColor);

    然后,我们就可以使用 dk_weirdColorPicker 属性了:

    view.dk_weirdColorPicker = DKColorPickerWithKey(BG);

    pickerify 其实是一个宏:

    #define pickerify(KLASS, PROPERTY) interface \
    KLASS (Night) \
    @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \
    @end \
    @interface \
    KLASS () \
    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers; \
    @end \
    @implementation \
    KLASS (Night) \
    - (DKColorPicker)dk_ ## PROPERTY ## Picker { \
    return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \
    } \
    - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \
    objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \
    [self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\
    [self.pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \
    } \
    @end

    这个宏根据传入的类和属性名,为我们生成了对应 picker 的存取方法,它也可以说是一种元编程的手段。

    这里生成的 setter 方法不是标准意义上的驼峰命名法 dk_setweirdColorPicker:,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。

    嵌入式 Ruby
    由于框架中很多的代码,都是重复的,所以在这里使用了嵌入式 Ruby 模板来生成对应的文件 color.m.irb:

    //
    // <%= klass.name %>+Night.m
    // <%= klass.name %>+Night
    //
    // Copyright (c) 2015 Draveness. All rights reserved.
    //
    // These files are generated by ruby script, if you want to modify code
    // in this file, you are supposed to update the ruby code, run it and
    // test it. And finally open a pull request.

    #import "<%= klass.name %>+Night.h"
    #import "DKNightVersionManager.h"
    #import <objc/runtime.h>

    @interface <%= klass.name %> ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    @implementation <%= klass.name %> (Night)

    <% klass.properties.each do |property| %><%= """
    - (DKColorPicker)dk_#{property.name}Picker {
    return objc_getAssociatedObject(self, @selector(dk_#{property.name}Picker));
    }

    - (void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker {
    objc_setAssociatedObject(self, @selector(dk_#{property.name}Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
    self.#{property.name} = picker(self.dk_manager.themeVersion);
    [self.pickers setValue:[picker copy] forKey:@\"#{property.setter}\"];
    }
    """ %><% end %>

    @end

    这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 generator 文件夹,其中包含了代码生成器的全部代码。

    小结

    如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 README 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,AFNetworking、 BlocksKit 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。

    Git仓库地址

    转自:https://draveness.me/night/

    收起阅读 »

    iOS-单元测试汇总

    前言:对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。我看...
    继续阅读 »

    前言:
    对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。

    我看了几篇单元测试的文章,其中写到单元测试多数用于:

    1.调试接口是否正常使用。比如要测试一个网络接口,通常每次都要重新启动,经过繁复的操作之后,才能测试到网络接口。要是用单元测试,就可以直接测试那个方法,相对方便很多。

    2.比如由于修改较多,想测试分享功能是否正常,(而不是重新启动程序,进入到分享界面,点击分享,填写分享内容。),在单元测试通过了,直接用到相应的地方。

    3.自动发布、自动测试(特别在一些大的项目,以防止程序被误改或引起新的问题)。
    4.用户注册/登陆等

    了解一下单元测试:
    单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

    通常来说,程序员每修改一次代码就会修改某个单元,那我们就可以对这个单元做修改的验证(单元测试),在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书(产品需求)要求的工作目标,而且没有程序错误。

    每个理想的测试案例独立于其它case,测试时需隔离模块。单元测试通常由软件开发人员编写,用于确保所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是手动的,或是构建自动化的一部分。

    单元测试允许程序员在未来重构代码,且确保模块依然工作正确。这个过程是为所有方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。

    可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。在连续的单元测试环境,通过其固有的持续维护工作,单元测试可以延续用于准确反映当任何变更发生时可执行程序和代码的表现。借助于上述开发实践和单元测试的覆盖,可以总是维持准确性。

    了解一下单元测试目的:
    保证代码的质量 (帮助你编写高质量代码、减少bu)
    代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,尤其包含了许多单元分支的情况下,单元测试可以保证代码的行为和结果与我们的预期和需求一致。在测试某段代码的行为是否和你的期望一致时,你需要确认,在任何情况下,这段代码是否都和你的期望一致,譬如参数可能为空,可能的异步操作等。

    有一部分bug的原因是开发人员在编写工作代码的时候没有考虑到某些case或者边际条件。造成这种问题的原因很多,其中很重要的一个原因是我们对工作代码所要完成的功能思考不足,而编写单元测试,特别是先写单元测试再写工作代码就可以帮助开发人员思考编写的代码到底要实现哪些功能。例如实现一个简单的用户注册功能的业务类方法,用单元测试再写工作代码的方式来工作的话开发人员就会先考虑各种场景相关,例如正常注册、用户名重复、没有满足必要的填写内容......等等,之后就会编写相关的测试用例。编写单元测试代码的过程就是促使开发人员思考工作代码实现内容和逻辑的过程,之后实现工作代码的时候,开发人员思路会更清晰,实现代码的质量也会有相应的提升。

    保证代码的可维护性 (提升代码的反馈速度,减少重复工作,保证你最后的代码修改不会破坏之前代码的功能)
    保证原有单元测试正确的情况下,无论如何修改单元内部代码,测试的结果应该是正确的,且修改后不会影响到其他的模块。

    开发人员实现某个功能或者修补了某个bug,如果有相应的单元测试支持的话,开发人员可以马上通过运行单元测试来验证之前完成的代码是否正确,而不需要反复通过编译运行simulator、等待应用启动、通过输入数据等繁琐的步骤来验证所完成的功能。用单元测试代码来验证代码和通过发布应用以人工的方式来验证代码这两者的效率差很多,所以单元测试其实还能节约人力成本。

    项目越做越大,代码越来越多,特别涉及到一些公用接口之类的代码或是底层的基础库,谁也不敢保证这次修改的代码不会破坏之前的功能,所以与此相关的需求会被搁置或推迟,由于不敢改进代码,代码也变得越来越难以维护,质量也越来越差。而单元测试就是解决这种问题的很好方法(不敢说最好的)。由于代码的历史功能都有相应的单元测试保证,修改了某些代码以后,通过运行相关的单元测试就可以验证出新调整的功能是否有影响到之前的功能。当然要实现到这种程度需要很大的付出,不但要能够达到比较高的测试覆盖率,而且单元测试代码的编写质量也要有保证。

    保证代码的可扩展性
    为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行,说明代码的依赖性很高。

    了解一下单元测试的本质:
    是一种验证行为
    单元测试在开发前期检验了代码逻辑的正确性,开发后期,无论是修改代码内部抑或重构,测试的结果为这一切提供了可量化的保障。

    是一种设计行为
    为了可进行单元测试,尤其是先写单元测试(TDD),我们将从调用者思考,从接口上思考,我们必须把程序单元设计成接口功能划分清晰的,易于测试的,且与外部模块耦合性尽可能小。

    是一种快速回归的方式
    在原代码基础上开发及修改功能时,单元测试是一种快捷,可靠的回归。

    除了那些大拿们编写的代码,我相信很多易于维护、设计良好的代码都是通过不断的重构才得到的。虽然说单元测试本身不能直接改进生产代码的质量,但它为生产代码提供了“安全网”,让开发人员可以勇敢地改进代码,从而让代码的clean和beautiful不再是梦想。

    是程序优良的文档
    从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。

    由于给代码写很多单元测试,相当于给代码加上了规格说明书,开发人员通过读单元测试代码也能够帮助开发人员理解现有代码。很有Open Source的项目(如,AFNetworking, FMDB,喵神的VVDoucment等)都有相当量的单元测试代码,通过读这些测试代码会有助于理解生产源代码。

    两种测试思想
      测试驱动开发(Test-driven development,TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。

    行为驱动开发(Behavior-driven development,BDD)是一种敏捷软件开发的技术,BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了 测试驱动开发方法(TDD)。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。

    在iOS单元测试框架中,kiwi是BDD的代表。

    介绍
    OCUnit(即用XCTest进行测试)其实就是苹果自带的测试框架。

    GHUnit是一个可视化的测试框架。
    有了它,你可以点击APP来决定测试哪个方法,并且可以点击查看测试结果等。

    OCMock就是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法A,方法A里面调用到了方法B,而且方法B是有参数传入,但又不是方法A所提供。这时候,你可以使用OCMock来模拟方法B返回的值。(在不影响测试的情况下,就可以这样去模拟。)除了这些,在没有网络的情况下,也可以通过OCMock模拟返回的数据。

    UITests就是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UITests就可以帮助解决这个问题了。

    案例 1

    简单的单元测试
    1-1 创建一个新的项目


    1-2点开测试文件,进入到这个类

    setUp       :每个测试方法调用前执行
    tearDown :每个测试方法调用后执行
    testExample :是测试方法,和我们新建的没有差别。
    测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法
    测试方法的执行顺序: 字典序排序。
    快捷键:Command + U进行单元测试,这个快捷键是全部测试。


    1-3在testExample方法中输入如下:

    NSLog(@"自定义测试testExample");
    int a= 3;
    XCTAssertTrue(a == 0,"a 不能等于 0");


    备注:红色的叉子:代表测试未通过。绿色叉子:代表测试通过。

    案例 2

    iOS-Main - 单元测试 &基本体验

    案例 3
    进行网络请求的测试
    使用CocoaPods安装AFNetworking和STAlertView(CocoaPods安装和使用教程 )
    Pofile:

    platform :ios, '7.0'
    target 'UnitTestDemoTests' do
    pod 'AFNetworking', '~> 2.5.0'
    pod 'STAlertView', '~> 1.0.0'
    end
    target 'UnitTestDemoTestsTests' do
    pod 'AFNetworking', '~> 2.5.0'
    pod 'STAlertView', '~> 1.0.0'
    end

    iOS9的http安全问题:现在进行异步请求的网络测试,由于测试方法主线程执行完就会结束,所以需要设置一下,否则没法查看异步返回结果。
    也可以在方法结束前设置等待,调回回来的时候再让它继续执行。(另一种异步函数的单元测试)定义宏如下:

    //waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。
    #define WAIT do {\
    [self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\
    [self waitForExpectationsWithTimeout:30 handler:nil];\
    } while (0);

    #define NOTIFY \
    [[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil];

    增加测试方法:

    -(void)testRequest{
    // 1.获得请求管理者
    AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
    mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil];

    // 2.发送GET请求
    [mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"responseObject:%@",responseObject);
    XCTAssertNotNil(responseObject, @"返回出错");
    NOTIFY //继续执行
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"error:%@",error);
    XCTAssertNil(error, @"请求出错");
    NOTIFY //继续执行
    }];
    WAIT //暂停
    }

    有时候我们想测试一下整个流程是否可以跑通,比如获取验证码、登录、上传头像,查询个人资料。其实只要输入验证码就可以完成整个测试。这时候就需要用到输入框了,以便程序继续执行。使用了一个第三方的弹出输入框STAlertView,前面已经设置。
    STAlertView的使用方法:

    - (void)testAlertView
    {

    self.stAlertView = [[STAlertView alloc]initWithTitle:@"验证码" message:nil textFieldHint:@"请输入手机验证码" textFieldValue:nil cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancelButtonBlock:^{
    //点击取消返回后执行
    [self testAlertViewCancel];
    NOTIFY //继续执行
    } otherButtonBlock:^(NSString *b) {
    //点击确定后执行
    [self alertViewComfirm:b];
    NOTIFY //继续执行
    }];

    [self.stAlertView show];
    WAIT //设置等待时间
    }

    案例 4
    测试的执行顺序


    通过上述测试得出结论:
    可以看到无论我们怎样调换test方法的书写顺序,其测试顺序都是不变的。
    目前初步的结论:测试方法执行的顺序与方法名中test后面的字符大小有关,小者优先,例如testA,testB1,testB2三个方法相继执行。
    案例 5
    Xcode集成了对单元测试的支持,XCode4.x集成的是OCUnit,到了XCode5.x时代就升级为了XCTest,XCode7.x时代XCtest还可以进行UI测试。下面我们简单介绍下XCTest的使用。

    在xcode新建项目中,默认会建一个单元测试的target,并建立一个继承于XCTestCase的测试用例类


     本例实现了一个个税计算方法,在测试用例中测试输入后输出是否符合结果。
    创建一个名为ASRevenueBL的 .h .m文件,如下面所示:


    ASRevenueBL.h

    #import <Foundation/Foundation.h>
    @interface ASRevenueBL : NSObject
    - (double)calculate:(double)revenue;
    @end

    ASRevenueBL.m

    import "ASRevenueBL.h"

    #define baseNum 3500.0 // 起征点

    @implementation ASRevenueBL

    /*
    * method:传入收入计算税值
    * revenue:收入
    */
    - (double)calculate:(double)revenue
    {
    double tax = 0.0; // 税
    // 应纳税所得额 = 工资收入金额 - 各项社会保险费 - 起征点(3500元)
    // 应纳税额 = 应纳税所得额 x 税率 - 速算扣除数
    double dbTaxRevenue = revenue - baseNum;
    if(dbTaxRevenue <= 1500){
    tax = dbTaxRevenue * 0.03;
    } else if (dbTaxRevenue > 1500 && dbTaxRevenue <= 4500){
    tax = dbTaxRevenue *0.1 -105;
    } else if (dbTaxRevenue > 4500 && dbTaxRevenue <= 9000){
    tax = dbTaxRevenue * 0.2 - 555;
    }else if (dbTaxRevenue > 9000 && dbTaxRevenue <= 35000) {
    tax = dbTaxRevenue * 0.25 - 1005;
    } else if (dbTaxRevenue > 35000 && dbTaxRevenue <= 55000) {
    tax = dbTaxRevenue * 0.3 - 2755;
    } else if (dbTaxRevenue > 55000 && dbTaxRevenue <= 80000) {
    tax = dbTaxRevenue * 0.35 - 5505;
    } else if (dbTaxRevenue > 80000) {
    tax = dbTaxRevenue * 0.45 - 13505;
    }
    return tax;
    }

    导入测试方法所在的类的头文件,并创建一个类,在测试方法调用前,初始化类对象,测试完毕后,将对象置nil,其方法测试如下方测试代码:

    #import <XCTest/XCTest.h>
    #import "ASRevenueBL.h"

    @interface UnitTestsTwoTests : XCTestCase
    @property (nonatomic, strong) ASRevenueBL *revenueBL;
    @end

    @implementation UnitTestsTwoTests

    - (void)setUp {
    [super setUp];

    self.revenueBL = [[ASRevenueBL alloc] init];
    }

    - (void)tearDown {
    self.revenueBL = nil;
    [super tearDown];
    }

    - (void)testLevel1
    {
    double revenue = 5000;
    double tax = [self.revenueBL calculate:revenue];
    XCTAssertEqual(tax, 45.0,@"测试案例1失败");
    XCTAssertTrue(tax == 45.0);
    }

    - (void)testLevel2 {
    XCTestExpectation *exp = [self expectationWithDescription:@"超时"];
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{
    double revenue = 1500;
    double tax = [self.revenueBL calculate:revenue];
    sleep(1);
    NSLog(@"%f",tax);
    XCTAssertEqual(tax, 45, @"用例2测试失败");
    [exp fulfill]; // exp结束
    }];

    [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
    if (error) {
    NSLog(@"Timeout Error: %@", error);
    }
    }];
    }


    - (void)testExample {

    }

    - (void)testPerformanceExample {

    [self measureBlock:^{
    for (int a = 0; a<10; a+=a) {
    NSLog(@"%zd", a);
    }
    }];

    }
    @end


    testLevel1通过revenueBL计算出来的tax与预期相同,测试通过;testLevel2通过revenueBL计算出来的tax与预期不同,测试不通过,反映出了程序一些逻辑漏洞;testPerformanceExample中的平均执行时间比基准值低,测试通过。

    案例 6 命令行测试
    在命令行中也可以启动测试,便于持续集成。

    Assuner$ cd Desktop/
    Desktop Assuner$ cd ASUnitTestFirstDemo/
    ASUnitTestFirstDemo Assuner$ xcodebuild test -project ASUnitTestFirstDemo.xcodeproj -scheme ASUnitTestFirstDemo -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
    // 可以有多个destination

    结果

    Test Suite 'All tests' started at 2017-09-11 11:12:16.348
    Test Suite 'ASUnitTestFirstDemoTests.xctest' started at 2017-09-11 11:12:16.349
    Test Suite 'ASUnitTestFirstDemoTests' started at 2017-09-11 11:12:16.349
    Test Case '-[ASUnitTestFirstDemoTests testLevel1]' started.
    Test Case '-[ASUnitTestFirstDemoTests testLevel1]' passed (0.001 seconds).
    Test Case '-[ASUnitTestFirstDemoTests testLevel2]' started.
    /Users/liyongguang-eleme-iOS-Development/Desktop/ASUnitTestFirstDemo/ASUnitTestFirstDemoTests/ASUnitTestFirstDemoTests.m:46: error: -[ASUnitTestFirstDemoTests testLevel2] : ((tax) equal to (45.0)) failed: ("-60") is not equal to ("45") - 用例2测试失败
    Test Case '-[ASUnitTestFirstDemoTests testLevel2]' failed (1.007 seconds).
    Test Suite 'ASUnitTestFirstDemoTests' failed at 2017-09-11 11:12:17.358.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.009) seconds
    Test Suite 'ASUnitTestFirstDemoTests.xctest' failed at 2017-09-11 11:12:17.359.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.010) seconds
    Test Suite 'All tests' failed at 2017-09-11 11:12:17.360.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.012) seconds
    Failing tests:
    -[ASUnitTestFirstDemoTests testLevel2]
    ** TEST FAILED **

    如果是workspace

    xcodebuild -workspace ASKiwiTest.xcworkspace -scheme ASKiwiTest-Example -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' test

    每个test方法都会跑一遍,并给出结果描述。

    案例 7 代码的执行时间测试-(性能测试)
    性能测试主要使用 measureBlock 方法 ,用于测试一组方法的执行时间,通过设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。


    假如直接执行方法,因为block中没有内容,所以方法的执行时间为0.0s,如果我们把baseline设成0.05,偏差10%,是可以通过的测试的。但是如果设置如果我们把baseline为1,偏差10%,那测试会失败,因为不满足条件。
    如上图所示,这个方法是用来测试block内代码的执行时间的,我们可以通过打印很清楚的看到它其实执行了10次,用处也很宽广,比如想测试shenfenzheng的识别时间,请求的时间,转模型的速度等等都可以通过它来测试,这里只是举个简单的例子.

    我们可以看下打印发现他确实是执行了十次.


    再来看看左边的执行代码相关信息,这里由于打印"1"执行的太快无法看出效果,所以我将测试内容换成了使用for循环打印1-9999,看看他们的执行时间.


    可以很清楚的看到,10次的平均时间是1.382秒,第一次时间是1.85秒,并且可以看到第一次执行时间超过了平均时间33%,这里的测试结果都是和机器性能有关系的.

    案例 8 登陆模块测试


    案例 9 加法测试

    - (void)testExample {
    //设置变量和设置预期值
    NSUInteger a = 10;NSUInteger b = 15;
    NSUInteger expected = 24;
    //执行方法得到实际值
    NSUInteger actual = [self add:a b:b];
    //断言判定实际值和预期是否符合
    XCTAssertEqual(expected, actual,@"add方法错误!");
    }

    -(NSUInteger)add:(NSUInteger)a b:(NSUInteger)b{
    return a+b;
    }

    从这也能看出一个测试用例比较规范的写法,1:定义变量和预期,2:执行方法得到实际值,3:断言

    案例 10 代码来自于AFNetworking,用于测试backgroundImageForState方法

    - (void)testThatBackgroundImageChanges {
    XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);
    NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIButton * _Nonnull button, NSDictionary<NSString *,id> * _Nullable bindings) {
    return [button backgroundImageForState:UIControlStateNormal] != nil;
    }];

    [self expectationForPredicate:predicate
    evaluatedWithObject:self.button
    handler:nil];
    [self waitForExpectationsWithTimeout:20 handler:nil];
    }

    利用谓词计算,button是否正确的获得了backgroundImage,如果正确20秒内正确获得则通过测试,否则失败。

    expectationForNotification 方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。

    - (void)testAsynExample1 {
    [self expectationForNotification:(@"监听通知的名称xxx") object:nil handler:nil];
    [[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx" object:nil];

    //设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:3 handler:nil];
    }

    这个例子也可以用expectationWithDescription实现,只是多些很多代码而已,但是这个可以帮助你更好的理解 expectationForNotification 方法和 expectationWithDescription的区别。同理,expectationForPredicate方法也可以使用expectationWithDescription实现。

    func testAsynExample1() {
    let expectation = expectationWithDescription("监听通知的名称xxx")
    let sub = NSNotificationCenter.defaultCenter().addObserverForName("监听通知的名称xxx", object: nil, queue: nil) { (not) -> Void in
    expectation.fulfill()
    }

    NSNotificationCenter.defaultCenter().postNotificationName("监听通知的名称xxx", object: nil)
    waitForExpectationsWithTimeout(1, handler: nil)
    NSNotificationCenter.defaultCenter().removeObserver(sub)
    }

    XCTest常见的断言

    XCTFail(format...)  生成一个失败的测试
    XCTAssertNil(a1,format...)为空判断, a1为空时通过,反之不通过;
    XCTAssertNotNil(a1,format...) 不为空判断,a1不为空时通过,反之不通过;
    XCTAssert(expression,format...) 当expression求值为true时通过;
    XCTAssertTrue(expression,format...) 当expression求值为true时通过;
    XCTAssertFalse(expression,format...) 当expression求值为False时通过;
    XCTAssertEqualObjects(a1, a2,format...) 判断相等 [a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
    XCTAssertNotEqualObjects(a1, a2,format...) 判断不等,[a1 isEqual:a2]值为False时通过;
    XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
    XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
    XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
    XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
    XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态)
    XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
    XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
    XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
    XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

    特别注意下XCTAssertEqualObjects和XCTAssertEqual。 XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES

    备注:

    1.关于私有方法的测试,只能通过扩展来实现

    2.关于case的方法名字,一定要以test开头并注意驼峰命名法,且不能加入参数。

    3.单元测试类继承自XCTestCase,他有一些重要的方法,其中最重要的有3个,setUp ,tearDown,measureBlock.

    4.md + 5切换到测试选项卡后会看到很多小箭头,点击可以单独或整体测试.

    5.cmd + U运行整个单元测试

    6.使用pod的项目中,在XC测试框架中测试内容包括第三方包时,需要手动去设置Header Search Paths才能找到头文件 ,还需要设置test target的PODS_ROOT。

    7.xcode7要使用真机做跑测试时,证书必须配对,否则会报错exc_breakpoint错误

    链接:https://www.jianshu.com/p/4001e06b150e

    收起阅读 »

    OpenGLES/(GLKit/CoreAnimation正方体的渲染+旋转)

    一.Hello--OpenGLES                 OpenGL可用于渲染...
    继续阅读 »

    一.Hello--OpenGLES 

                    OpenGL可用于渲染2D和3D图像,是一个多用途的开源图形库。OpenGL设计用来将函数命令转换成图形命令,发送到GPU中。GPU正是被设计用来处理图形命令的,所以OpenGL的绘制非常高效。

                    OpenGLES是OpenGL的简化版本,抛弃了冗余的文件及命令,使之专用于嵌入式设备。OpenGLES使得移动APP能充分利用GPU的强大运算能力。iOS设备上的GPU能执行更精确的2D和3D绘制,以及更加复杂的针对每个像素的图形脚本(shader)计算。⽀持的平台: iOS, Andriod , BlackBerry ,bada ,Linux ,Windows。

    1.1准备工程

                    iOS新建工程,@interface ViewController : UIViewController改成-->@interface ViewController : GLKViewController,.h文件导入#import,.m导入#import#import,最后在Main.storyboard中将view改成GLVIew






    GLView

    1.2EAGLContext(OpenGL 上下文)

                    EAGLContext对象管理着OpenGLES的渲染context,即所有绘制的状态,命令及资源信息,并控制GPU去执行渲染运算。 绘制如textures及renderbuffers的过程,是由一个与context绑定的EAGLSharegroup对象来管理的。当初始化一个EAGLContext对象的时候,可选择新建一个sharegroup,或者使用已有的,这一点我们往往采用系统默认即可。在绘制到context之前,我们要先绑定一个完整的framebuffer对象到context中。






    Hello-OpenGLES

                    1)初始化上写文:context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];(参数知识选择版本)

                    2)设置当前上下文:[EAGLContext setCurrentContext:context];

                    3)GLView绑定上下文:GLKView *view =(GLKView *) self.view;  view.context=context;

                    注意:在使用GLview中,我们必须实现它的协议:GLKViewDelegate--->- (void)glkView:(GLKView*)viewdrawInRect:(CGRect)rect,GLKView对象使其OpenGL ES上下文成为当前上下文,并将其framebuffer绑定为OpenGL ES呈现命令的目标。然后,委托方法应该绘制视图的内容。我们给GLview设置颜色,看一下效果:glClearColor(1, 0, 0, 1.0);






    Hello--OpenGlES

    二.显示图片







    加载图片

    2.1设置顶点坐标/纹理坐标

                     在OpenGl中我们显示一张图片,首先我们设置顶点数组,绑定纹理,在OpenGLES中,我们一样这么设置:

                                    GLfloatvertexData[] = {

                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            0.5,0.5,  0.0f,    1.0f,1.0f,//右上

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上


                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上

                                                                            -0.5, -0.5,0.0f,  0.0f,0.0f,//左下

                                                                        };

                      在OpenGL中我们提到了图形绘制是点,线,三角形,正方形由两个三角形组成,就是六个顶点,而我们知道,纹理的坐标范围是(0,1),其原点是在左下角,所以坐标(0,0)是原点,右上角(1,1);

    2.2开辟顶点缓存区并把数据存到缓中区

                    (1).创建顶点缓存区标识符ID

                            GLuint  bufferID;

                            glGenBuffers(1, &bufferID);(分配纹理)

                    (2).绑定顶点缓存区.(明确作用)

                            glBindBuffer(GL_ARRAY_BUFFER, bufferID);

                    (3).将顶点数组的数据copy到顶点缓存区中(GPU显存中)

                            glBufferData(GL_ARRAY_BUFFER,sizeof(vertexData), vertexData,GL_STATIC_DRAW);

                    (4).打开读取通道.

                            1)顶点坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribPosition);

                                glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 0);

                            2)纹理坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

                                glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 3);

    特别说明:

                        (1)在iOS中, 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的.意味着,顶点数据在着色器端(服务端)是不可用的. 即使你已经使用glBufferData方法,将顶点数据从内存拷贝到顶点缓存区中(GPU显存中).所以, 必须由glEnableVertexAttribArray 方法打开通道.指定访问属性.才能让顶点着色器能够访问到从CPU复制到GPU的数据.

                         注意: 数据在GPU端是否可见,即,着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。

                        (2)方法简介

                            glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)    功能: 上传顶点数据到显

    存的方法(设置合适的方式从buffer里面读取数据)

             参数列表:

                            index,指定要修改的顶点属性的索引值,例如 size, 每次读取数量。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a),纹理则是2个,type,指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。 normalized,指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)stride,指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0 ,ptr指定一个指针,指向数组中第一个顶点属性的第一个组件。初始值为0







    参数流程说明

    2.2获取纹理







    纹理

                    1)路径:NSString *filePath = [[NSBundle mainBundle]pathForResource:@"kunkun" ofType:@"jpg"];

                    2)参数:NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];

                                    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];

                    说明:纹理坐标原点是左下角,但是图片显示原点应该是左上角.我们要设置图片绘制从左上角开始绘制GLKTextureLoaderOriginBottomLeft;

                    3)cEffect:你可以把它理解成UIimageVIew,用于显示图片的控件,iOS提供GLKBaseEffect 完成着色器工作(顶点/片元)

                                cEffect = [[GLKBaseEffect alloc]init];

                                cEffect.texture2d0.enabled = GL_TRUE;

                                cEffect.texture2d0.name= textureInfo.name;

                    最后在GLVIew的delegate中:

                                                    1.清除颜色缓冲区

                                                    glClear(GL_COLOR_BUFFER_BIT);

                                                    2.准备绘制

                                                    [cEffect prepareToDraw];

                                                    3.开始绘制

                                                    glDrawArrays(GL_TRIANGLES, 0, 6);







    效果

    三.OpenGLES绘制立方体

            在OpenGLES绘制立方体,相当于绘制六个面,十二个三角形,60个数据(当然你在图元连接方式那里可以选择平面,GL_TRIANGLE_FAN,这样就会少设置一点数据,这里我选择GL_TRIANGLES)

    GLfloatvertexData[] = {

            //第一个面

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  -0.5f,    1.0f,1.0f,//右上

            -0.5,0.5, -0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5, -0.5f,  0.0f,0.0f,//左下

            0.5, -0.5,0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  0.5f,    1.0f,1.0f,//右上

            -0.5,0.5,0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5,0.5f,  0.0f,0.0f,//左下

    //

            //2

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5, -0.5,0.5f,    0.0f,1.0f,//右下

            -0.5, -0.5, -0.5f,    0.0f,0.0f,//右下

            0.5,0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            //3

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            0.5,0.5,0.5f,    0.0f,1.0f,//右下

            0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            -0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            -0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

        };







    效果


    四。CoreAnimation正方体的大体原理就是一个VIew上放六个imageVIew,并设置imageVIew旋转组成一个立方体,一共6个,最后添加定时器控制view的layer转动,达到效果,因为比较简单,这里不做展示



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/035061d80d5c
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    OpenGl纹理相关常用API

    一.原始图像数据1.像素包装:                    图像存储空间=图像的宽度*图像的高度*每个像素的字节数二.相关函数(加粗部分表示常用)2....
    继续阅读 »

    一.原始图像数据

    1.像素包装:

                        图像存储空间=图像的宽度*图像的高度*每个像素的字节数

    二.相关函数(加粗部分表示常用)

    2.1  改变像素存储方式----->void glPixelStorei(GLenum pname,GLint param);

            恢复像素存储⽅式----->void glPixelStoref(GLenum pname,GLfloat param);

    参数说明:

                    //参数1:GL_UNPACK_ALIGNMENT 指定OpenGL 如何从数据缓存区中解包图像数据

                    //参数2:表示参数GL_UNPACK_ALIGNMENT 设置的值

                   //GL_UNPACK_ALIGNMENT 指内存中每个像素⾏起点的排列请求

                    允许设置为1 (byte排列)

                                        2(排列为偶数byte的⾏)

                                        4(字word排列)

                                        8(⾏从双字节边界开始)

             举例: glPixelStorei(GL_UNPACK_ALIGNMENT,1);

    2.2  从颜⾊缓存区内容作为像素图直接读取

    void glReadPixels(GLint x,GLint y,GLSizei width,GLSizei height, GLenum format, GLenum type,const void * pixels);

    参数说明:

                    //参数1:x,矩形左下⻆的窗⼝坐标

                    //参数2:y,矩形左下⻆的窗⼝坐标

                    //参数3:width,矩形的宽,以像素为单位

                    //参数4:height,矩形的⾼,以像素为单位

                    //参数5:format,OpenGL 的像素格式,参考 表6-1

                    //参数6:type,解释参数pixels指向的数据,告诉OpenGL 使⽤缓存区中的什么数据类型来存储颜⾊分量,像素数据的数据类型,参考 表6-2

                    //参数7:pixels,指向图形数据的指针

                    glReadBuffer(mode);—> 指定读取的缓存

                    glWriteBuffer(mode);—> 指定写⼊的缓存

    2.3载⼊纹理

                    void glTexImage1D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLint border,GLenum format,GLenum type,void *data);

                    void glTexImage2D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLsizei height,GLint border,GLenum format,GLenum type,void * data);(这个是比较常用的)

                    void glTexImage3D(GLenum target,GLint level,GLint internalformat,GLSizei width,GLsizei height,GLsizei depth,GLint border,GLenum format,GLenum type,void *data);

    参数说明:

                        * target:`GL_TEXTURE_1D`、`GL_TEXTURE_2D`、`GL_TEXTURE_3D`。 

                        * Level:指定所加载的mip贴图层次。⼀般我们都把这个参数设置为0。

                        * internalformat:每个纹理单元中存储多少颜⾊成分。

                        * width、height、depth参数:指加载纹理的宽度、⾼度、深度。==注意!==这些值必须是2的整数次⽅。(这是因为OpenGL 旧版本上的遗留下的⼀个要求。当然现在已经可以⽀持不是2的整数次⽅。但是开发者们还是习惯使⽤以2的整数次⽅去设置这些参数。)

                        * border参数:允许为纹理贴图指定⼀个边界宽度。

                        * format、type、data参数:与我们在讲glDrawPixels 函数对于的参数相同

    2.4更新纹理

                    void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);

    参数说明:同载入纹理一样

    2.5插入替换纹理

                    void glCopyTexSubImage1D(GLenum target,GLint level,GLint xoffset,GLint x,GLint y,GLsizei width);

                    void glCopyTexSubImage2D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint x,GLint y,GLsizei width,GLsizei height);

                    void glCopyTexSubImage3D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint zOffset,GLint x,GLint y,GLsizei width,GLsizei height);

    参数说明:同载入纹理一样

    2.6使⽤颜⾊缓存区加载数据,形成新的纹理使⽤

                    void glCopyTexImage1D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLint border);

                    void glCopyTexImage2D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLsizei height,GLint border);

    特别说明:x,y 在颜⾊缓存区中指定了开始读取纹理数据的位置;缓存区⾥的数据,是源缓存区通过glReadBuffer设置的。注意:不存在glCopyTextImage3D ,因为我们⽆法从2D 颜⾊缓存区中获取体积 数据

    三.纹理对象

    3.1使⽤函数分配纹理对象&&指定纹理对象的数量 和 指针(指针指向⼀个⽆符号整形数组,由纹理对象标识符填充)。

                    void glGenTextures(GLsizei n,GLuint * textTures);

    3.2绑定纹理状态

                    void glBindTexture(GLenum target,GLunit texture);

    参数说明:

                    参数target:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数texture:需要绑定的纹理对象

    3.2删除纹理对象

                    void glDeleteTextures(GLsizei n,GLuint *textures);

                    参数说明:同分配纹理对象一样

    3.3测试纹理对象是否有效

                    GLboolean glIsTexture(GLuint texture);

                    说明:如果texture是⼀个已经分配空间的纹理对象,那么这个函数会返回GL_TRUE,否则会返回GL_FALSE。

    3.4设置纹理参数

                    glTexParameterf(GLenum target,GLenum pname,GLFloat param);

                    glTexParameteri(GLenum target,GLenum pname,GLint param);

                    glTexParameterfv(GLenum target,GLenum pname,GLFloat *param);

                    glTexParameteriv(GLenum target,GLenum pname,GLint *param);

    参数说明:

                    参数1:target,指定这些参数将要应⽤在那个纹理模式上,⽐如GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D。

                    参数2:pname,指定需要设置那个纹理参数

                    参数3:param,设定特定的纹理参数的值

    3.5过滤方式

            1)邻近过滤(GL_NEAREST)


      说明:当一像素点靠近A时,返回离这个点最近的像素值

            2)线性过滤(GL_LINEAR)













    说明:两种过滤效果本质上没有多大区别,肉眼很难区分的出来,只有当图片放大后,可惜清晰的看清楚两种过滤方式的差别,一般情况下,glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR) 纹理放⼤时,使⽤线性过滤

    3.6设置环绕⽅式

    当纹理坐标超出默认范围时,每个选项都有不同的输出效果



    设置环绕方式;

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_S,GL_CLAMP_TO_EDGE);

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_T,GL_CLAMP_TO_EDGE);

    参数说明:

                    参数1:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数2:GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,针对s,t,r坐标(s->x,t->y,r->z)

                    参数3:GL_REPEAT、GL_CLAMP、GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER

                    GL_REPEAT:OpenGL 在纹理坐标超过1.0的⽅向上对纹理进⾏重复;

                    GL_CLAMP:所需的纹理单元取⾃纹理边界或TEXTURE_BORDER_COLOR.

                    GL_CLAMP_TO_EDGE环绕模式强制对范围之外的纹理坐标沿着合法的纹理单元的最后⼀⾏或者最后⼀列来进⾏采样。

                    GL_CLAMP_TO_BORDER:在纹理坐标在0.0到1.0范围之外的只使⽤边界纹理单元。边界纹理单元是作为围绕基本图像的额外的⾏和列,并与基本纹理图像⼀起加载的。

    3.7OpenGL 像素格式

    常量说明

    GL_RGB                                                         描述红、绿、蓝顺序排列的颜⾊

    GL_RGBA                                                      按照红、绿、蓝、Alpha顺序排列的颜⾊

    GL_BGR                                                         按照蓝、绿、红顺序排列颜⾊

    GL_BGRA                                                       按照蓝、绿、红、Alpha顺序排列颜⾊

    GL_RED                                                         每个像素只包含了⼀个红⾊分量

    GL_GREEN                                                    每个像素只包含了⼀个绿⾊分量

    GL_BLUE                                                       每个像素只包含了⼀个蓝⾊分量

    GL_RG                                                           每个像素依次包含了一个红色和绿色的分量

    GL_RED_INTEGER                                        每个像素包含了一个整数形式的红⾊分量

    GL_GREEN_INTEGER                                   每个像素包含了一个整数形式的绿色分量

    GL_BLUE_INTEGER                                     每个像素包含了一个整数形式的蓝色分量

    GL_RG_INTEGER                                          每个像素依次包含了一个整数形式的红⾊、绿⾊分量

    GL_RGB_INTEGER                                       每个像素包含了一个整数形式的红⾊、蓝⾊、绿色分量

    GL_RGBA_INTEGER                                     每个像素包含了一个整数形式的红⾊、蓝⾊、绿⾊、Alpah分量

    GL_BGR_INTEGER                                        每个像素包含了一个整数形式的蓝⾊、绿⾊、红色分量

    GL_BGRA_INTEGER                                     每个像素包含了一个整数形式的蓝⾊、绿⾊、红色、Alpah分量

    GL_STENCIL_INDEX                                    每个像素只包含了一个模板值

    GL_DEPTH_COMPONENT                          每个像素只包含一个深度值

    GL_DEPTH_STENCIL                                 每个像素包含一个深度值和一个模板值

    3.8像素数据的数据类型

    GL_UNSIGNED_BYTE                        每种颜色分量都是一个8位无符号整数

    GL_BYTE                                            8位有符号整数

    GL_UNSIGNED_SHORT                    16位无符号整数

    GL_SHORT                                         16位有符号整数

    CL_UNSIGNED_INT                            32位无符号整数

    GL_INT                                               32位有符号整数

    GL_FLOAT                                        单精度浮点数

    GL_HALF_FLOAT                                半精度浮点数

    GL_UNSIGNED_BYTE_3_2_3            包装的RGB值

    GL_UNSIGNED_BYTE_2_3_3_REV    包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5         包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5_REV  包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4      包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4_REV   包装的RGB值

    GL_UNSIGNED_SHORT_5_5_5_1        包装的RGB值

    GL_UNSIGNED_SHORT_1_5_5_5_REV   包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8               包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8_REV      包装的RGB值

    GL_UNSIGNED_INT_10_10_10_2       包装的RGB值

    GL_UNSIGNED_INT_2_10_10_10_REV   包装的RGB值

    GL_UNSIGNED_INT_24_8                   包装的RGB值

    GL_UNSIGNED_INT_10F_11F_REV       包装的RGB值

    GL_FLOAT_24_UNSIGNED_INT_24_8_REV     包装的RGB值




    作者:枫紫
    链接:https://www.jianshu.com/p/bea1fd229b18


    收起阅读 »

    iOS---webView相关及原生和web的交互

    webView的基本应用,监听加载进度,返回上一页,异常处理web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView原生调用web:获取webView的标题等web原生互相调用:web获取a...
    继续阅读 »


  • webView的基本应用,监听加载进度,返回上一页,异常处理
  • web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView
  • 原生调用web:获取webView的标题等
  • web原生互相调用:web获取app当前的id、token等用户信息
  • 微信web里打开原生app
  • 一、webView的基本应用

    现在基本每个app都会或多或少用到web来实现快速迭代。正常都会将其封装在一个控制器里,以使其样式、功能统一
    (iOS8引入了WKWebView,使用独立的进程渲染web,解决了之前UIWebView内存泄漏和crash率高等被诟病已久的问题,所以现在基本都是用WKWebView了)


        //如果不考虑和原生的交互
    _webView = [[WKWebView alloc] initWithFrame:CGRectZero];
    [self.view addSubview:_webView];
    [_webView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(self.view);
    }];
    _webView.UIDelegate = self;
    _webView.navigationDelegate = self;
    [_webView loadRequest:[NSURLRequest requestWithURL:URL]];//这里的url是经过校检的

    如果要监听webview的加载进度

        //kvo监听
    [_webView addObserver:self forKeyPath:@"estimatedProgress" options:0 context:nil];

    //创建加载进度条UIProgressView
    {
    init progressView
    }

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]) {
    self.progressView.alpha = 1.0f;
    BOOL animated = _webView.estimatedProgress > self.progressView.progress;
    [self.progressView setProgress:_webView.estimatedProgress animated:animated];

    if (_webView.estimatedProgress >= 1.0f) {
    [UIView animateWithDuration:0.3f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^{
    self.progressView.alpha = 0.0f;
    } completion:^(BOOL finished) {}];
    }
    } else {
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    }
    返回上一页

            //kvo监听
    [_webView addObserver:self forKeyPath:@"canGoBack" options:0 context:nil];//监听是否有上一页

    //configBackButton里判断canGoBack,如果不可以返回就将按钮置灰或者隐藏
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqual: @"canGoBack"]) {
    [self configBackButton];
    }
    }
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    //按钮事件
    if ([weakSelf.webView canGoBack]) {
    [weakSelf.webView goBack];
    }
    当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用下面的回调函数,我们在这里执行[webView reload]解决白屏问题

    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {

    [webView reload];
    }

    二、web调用原生

    1.这三个代理方法是可以接收到web的调用比如 window.prompt("xxx")

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
    1. 在发送请求之前,决定是否跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    NSURL *url = navigationAction.request.URL;
    //可以在这里处理一些跳转,比如通过scheme来处理跳转到指定的原生页面(xxx://xxx),拦截跳转其他app,添加app白名单,拦截通用链接跳转等等

    //比如
    if ([@"mailto" isEqualToString:url.scheme] || [@"tel" isEqualToString:url.scheme]) {//系统scheme
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
    [[UIApplication sharedApplication] openURL:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
    } if ([@"xxxx" isEqualToString:url.scheme]) {
    // 如果该scheme是你定义好的scheme,可以根据后面的参数去处理跳转到app内的指定页面,或者其他操作
    decisionHandler(WKNavigationActionPolicyCancel);
    }else if ([scheme白名单 containsObject:url.scheme]) {//白名单
    // 打开scheme
    [[UIApplication sharedApplication] openURL:url];
    decisionHandler(WKNavigationActionPolicyCancel);
    } else {
    BOOL canOpenUniversalUrl = NO;
    for (NSString *str in universalLink白名单) {
    if ([url.absoluteString rangeOfString:str].location != NSNotFound) {
    canOpenUniversalUrl = YES;
    break;
    }
    }
    if (canOpenUniversalUrl) {
    // 打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow);
    } else {
    // Default 可以正常访问网页,但禁止打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow+2);
    }
    }
    }
    web只需
    window.location.href = "xxx"//这里的地址就是上方代理方法的url
    WKWebView可以使用WKScriptMessageHandler来实现JS调用原生方法
    首先初始化的时候,这里拿最常用的web调用关闭webView:xxx_close举例(也可以用上边的href的scheme方式实现,但不太合理)
        _webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:[self configWKConfiguration]];

    // config,js注入
    - (WKWebViewConfiguration *)configWKConfiguration {
    WKWebViewConfiguration* webViewConfig = [WKWebViewConfiguration new];
    WKUserContentController *userContentController = [WKUserContentController new];
    //这里如果用的不多,可以不用单独写一个js文件,直接用字符串就行了
    NSString *jsStr = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"js文件地址"] encoding:NSUTF8StringEncoding error:nil];
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
    [userContentController addUserScript:userScript];
    [userContentController addScriptMessageHandler:self name:"closeWebView"];

    webViewConfig.userContentController = userContentController;
    webViewConfig.preferences = [[WKPreferences alloc] init];
    webViewConfig.preferences.javaScriptEnabled = YES;
    return webViewConfig;
    }

    //app里的js文件里实现
    var xxx = {
    close: function() {
    window.webkit.messageHandlers.closeWebView.postMessage(null);
    },
    }

    //在这里能收到回调
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([message.name isEqualToString:@"closeWebView"]) {
    // 关闭页面
    [self.navigationController popViewControllerAnimated:YES];
    }
    }

    web中只需
          try {
    window.xxx.close();
    } catch (err) {}

    三、原生调用web,就是app调用web里的js方法

    1.比较常用的一种,获取webView的标题
    //也可以用正则去获取标题、图片之类的

        [webView evaluateJavaScript:@"document.title" completionHandler:^(id result, NSError * _Nullable error) {
    }];

    四. web原生互相调用

    比如一个场景,在web里获取app当前登录的账号id

    1. 首先像上边一样,通过js注入的方式web向app发送getUserId请求,app也要同步处理

    //web代码

          try {
    window.xxx.getUserId();//这里可以直接加返回值,但是app内的js没办法直接去获取原生用户信息这些变量,所以还是要通过原生的代理去实现
    } catch (err) {}
    1. 这时候app接收到这个请求,但还要将userId告诉web

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:"getUserId"]){
    NSDictionary *dict = xxx;//因为这个过程是异步的,所以这里最好多加一点信息,以便于web确认该结果是上边请求的返回
    NSString * jsMethod = [NSString stringWithFormat:@"window.getUserId(%@)",[dict yy_modelToJSONString]];
    [webView evaluateJavaScript:@"xxx" completionHandler:^(id result, NSError * _Nullable error) {
    }];
    }
    }

    1. web需要将getUserId方法挂载到window上,算是自定义回调,将上一步生成的用户信息dic当成参数传进来,然后去处理接收到的信息

    //web代码

        window["getUserId"] = function(value) {
    //在这里解析处理接收到的用户信息
    };

    五. web如何在微信里打开原生?

    普通的scheme跳转被微信给禁了,所以现在基本都是通过universalLink通用链接的方式,设置universalLink的方式网上有好多,另外通用链接可以设置多个,最好设置两个以上(因为这里有个隐藏的坑:web的域名不能和universalLink一样,否则无法跳转)
    web代码:


    window.location.href = '通用链接://具体落地页'//可以通过参数跳转到具体的页面
    作者:Theendisthebegi
    链接:https://www.jianshu.com/p/d66d694b762f










    收起阅读 »

    iOS开发宏定义整理

    宏定义今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能下面分开介绍各种宏:Macros.h这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK#...
    继续阅读 »

    宏定义

    今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能

    下面分开介绍各种宏:

    • Macros.h

    这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK
    #import "DimensMacros.h"
    #import "UtilsMacros.h"
    #import "PathMacros.h"
    #import "NotificationMacros.h"
    #import "APIStringMacros.h"

    • APIStringMacros_h(服务端API接口的宏)

    这里面主要放一些API相关的东西:比如你请求网络的接口hostname,port还有一些第三方的关键字段:极光推送的appkey....

    • DimensMacros.h (定义尺寸类的宏)

    这里面定义一些尺寸相关的宏:

    #pragma mark - 系统UI
    #define kNavigationBarHeight 44
    #define kStatusBarHeight 20
    #define kTopBarHeight 64
    #define kToolBarHeight 44
    #define kTabBarHeight 49
    #define kiPhone4_W 320
    #define kiPhone4_H 480
    #define kiPhone5_W 320
    #define kiPhone5_H 568
    #define kiPhone6_W 375
    #define kiPhone6_H 667
    #define kiPhone6P_W 414
    #define kiPhone6P_H 736
    /*** 当前屏幕宽度 */
    #define kScreenWidth [[UIScreen mainScreen] bounds].size.width
    /*** 当前屏幕高度 */
    #define kScreenHeight [[UIScreen mainScreen] bounds].size.height
    /*** 普通字体 */
    #define kFont(size) [UIFont systemFontOfSize:size]
    /*** 粗体 */
    #define kBoldFont(size) [UIFont boldSystemFontOfSize:size]
    #define kLineHeight (1 / [UIScreen mainScreen].scale)
    • NotificationMacros.h(通知Notification相关宏)

    这里面放一些关于通知定义的宏

    #define TNCancelFavoriteProductNotification     @"TNCancelFavoriteProductNotification"      //取消收藏时
    #define TNMarkFavoriteProductNotification @"TNMarkFavoriteProductNotification" //标记收藏时

    #define kNotficationDownloadProgressChanged @"kNotficationDownloadProgressChanged" //下载进度变化
    #define kNotificationPauseDownload @"kNotificationPauseDownload" //暂停下载
    #define kNotificationStartDownload @"kNotificationStartDownload" //开始下载

    #define kNotificationDownloadSuccess @"kNotificationDownloadSuccess" //下载成功
    #define kNotificationDownloadFailed @"kNotificationDownloadFailed" //下载失败
    #define kNotificationDownloadNewMagazine @"kNotificationDownloadNewMagazine"
    • UtilsMacros_h(工具类的宏)

    这里面存放一些方便开发的工具:颜色,打印,单利,版本...
    // 日志输出
    #ifdef DEBUG
    #define LMLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTIONLINE, ##VA_ARGS);
    #else
    #define LMLog(...)
    #endif

    #define WeakSelf(weakSelf)  __weak __typeof(&*self)weakSelf = self;

    #pragma mark - 颜色
    #define kWhiteColor [UIColor whiteColor]
    #define kBlackColor [UIColor blackColor]
    #define kDarkGrayColor [UIColor darkGrayColor]
    #define kLightGrayColor [UIColor lightGrayColor]
    #define kGrayColor [UIColor grayColor]
    #define kRedColor [UIColor redColor]
    #define kGreenColor [UIColor greenColor]
    #define kBlueColor [UIColor blueColor]
    #define kCyanColor [UIColor cyanColor]
    #define kYellowColor [UIColor yellowColor]
    #define kMagentaColor [UIColor magentaColor]
    #define kOrangeColor [UIColor orangeColor]
    #define kPurpleColor [UIColor purpleColor]
    #define kBrownColor [UIColor brownColor]
    #define kClearColor [UIColor clearColor]

    //16进制
    #define LMColorFromHex(s) [UIColor colorWithRed:(((s & 0xFF0000) >> 16))/255.0green:(((s &0xFF00) >>8))/255.0blue:((s &0xFF))/255.0alpha:1.0]
    //RGB
    #define kRGBAColor(r,g,b,a) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a]
    #define kRGBColor(r,g,b) kRGBAColor(r,g,b,1.0f)
    #define kSeperatorColor kRGBColor(234,237,240)
    #define kBgColor kRGBColor(243,245,247)

    #define krgbaColor(r,g,b,a) [UIColor colorWithRed:r green:g blue:b alpha:a]
    #define krgbColor(r,g,b) krgbColor(r,g,b,1.0f)

    #define kCommonHighLightRedColor krgbColor(1.00f,0.49f,0.65f)
    #define kCommonGrayTextColor krgbColor(0.63f,0.63f,0.63f)
    #define kCommonRedColor krgbColor(0.91f,0.33f,0.33f)
    #define kCommonBlackColor krgbColor(0.17f,0.23f,0.28f)
    #define kCommonTintColor krgbColor(0.42f,0.33f,0.27f)
    #define kCommonBgColor krgbColor(0.86f,0.85f,0.80f)
    #define kDetailTextColor krgbColor(0.56f,0.60f,0.62f)
    #define kLineBgColor krgbColor(0.86f,0.88f,0.89f)
    #define kTextColor krgbColor(0.32f,0.36f,0.40f)


    #define kVersion [NSString stringWithFormat:@"%@",[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]

    //System version utils

    #define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
    #define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
    #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

    //大于等于7.0的ios版本
    #define iOS7_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")

    //大于等于8.0的ios版本
    #define iOS8_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")

    //iOS6时,导航VC中view的起始高度
    #define YH_HEIGHT (iOS7_OR_LATER ? 64:0)

    //获取系统时间戳
    #define getCurentTime [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]]

    #define kWindow [UIApplication sharedApplication].keyWindow //主窗口
    #define kUserDefault [NSUserDefaults standardUserDefaults]

    #pragma mark - 字符串转化
    #define kEmptyStr @""
    #define kIntToStr(i) [NSString stringWithFormat: @"%d", i]
    #define kIntegerToStr(i) [NSString stringWithFormat: @"%ld", i]
    #define kValidStr(str) [NHUtils validString:str]

    #pragma mark - 单利
    #define SingletonH(methodName) + (instancetype)shared##methodName;
    // .m文件的实现
    #if __has_feature(objc_arc) // 是ARC
    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    #else // 不是ARC

    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    \
    - (oneway void)release \
    { \
    \
    } \
    \
    - (id)retain \
    { \
    return self; \
    } \
    \
    - (NSUInteger)retainCount \
    { \
    return 1; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    *PathMacros.h(沙河路径宏)

    这里面是一些沙河路径,还有一些plist路径
    //文件目录
    #define kPathTemp NSTemporaryDirectory()
    #define kPathDocument [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathCache [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathSearch [kPathDocument stringByAppendingPathComponent:@"Search.plist"]

    #define kPathMagazine               [kPathDocument stringByAppendingPathComponent:@"Magazine"]
    #define kPathDownloadedMgzs [kPathMagazine stringByAppendingPathComponent:@"DownloadedMgz.plist"]
    #define kPathDownloadURLs [kPathMagazine stringByAppendingPathComponent:@"DownloadURLs.plist"]
    #define kPathOperation [kPathMagazine stringByAppendingPathComponent:@"Operation.plist"]

    #define kPathSplashScreen [kPathCache stringByAppendingPathComponent:@"splashScreen"]

    这样导入宏,简单明了



    作者:Cooci
    链接:https://www.jianshu.com/p/db4f67e56214

    收起阅读 »

    iOS开发必备 - iOS 的锁

    这次主要想解决这些疑问:锁是什么?为什么要有锁?锁的分类问题为什么 OSSpinLock 不安全?解决自旋锁不安全问题有几种方式为什么换用其它的锁,可以解决 OSSpinLock 的问题?自旋锁和互斥锁的关系是平行对立的吗?信号量和互斥量的关系信号量和条件变量...
    继续阅读 »

    这次主要想解决这些疑问:

      1. 锁是什么?
      1. 为什么要有锁?
      1. 锁的分类问题
      1. 为什么 OSSpinLock 不安全?
      1. 解决自旋锁不安全问题有几种方式
      1. 为什么换用其它的锁,可以解决 OSSpinLock 的问题?
      1. 自旋锁和互斥锁的关系是平行对立的吗?
      1. 信号量和互斥量的关系
      1. 信号量和条件变量的区别


    锁是什么

    锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

    为什么要有锁?

    前面说到了,锁是用来保护线程安全的工具。

    可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。

    当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:

    程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。

    所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization)

    属性设置 atomic

    上面提到了原子性,我马上想到了属性关键字里, atomic 的作用。

    设置 atomic 之后,默认生成的 getter 和 setter 方法执行是原子的。

    但是它只保证了自身的读/写操作,却不能说是线程安全。

    如下情况:

    //thread A
    for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
    self.arr = @[@"1", @"2", @"3"];
    }else {
    self.arr = @[@"1"];
    }
    NSLog(@"Thread A: %@\n", self.arr);
    }

    //thread B
    if (self.arr.count >= 2) {
    NSString* str = [self.arr objectAtIndex:1];
    }

    就算在 thread B 中针对 arr 数组进行了大小判断,但是仍然可能在 objectAtIndex: 操作时被改变数组长度,导致出错。这种情况声明为 atomic 也没有用。

    而解决方式,就是进行加锁。

    需要注意的是,读/写的操作都需要加锁,不仅仅是对一段代码加锁。

    锁的分类

    锁的分类方式,可以根据锁的状态,锁的特性等进行不同的分类,很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。关于锁的分类,可以参考 Java中的锁分类 看一下。

    自旋锁和互斥锁的关系

    很多谈论锁的文章,都会提到互斥锁,自旋锁。很少有提到它们的关系,其实自旋锁,也是互斥锁的一种实现,而 spin lock和 mutex 两者都是为了解决某项资源的互斥使用,在任何时刻只能有一个保持者。

    区别在于 spin lock和 mutex 调度机制上有所不同。

    OSSpinLock

    OSSpinLock 是一种自旋锁。它的特点是在线程等待时会一直轮询,处于忙等状态。自旋锁由此得名。

    自旋锁看起来是比较耗费 cpu 的,然而在互斥临界区计算量较小的场景下,它的效率远高于其它的锁。

    因为它是一直处于 running 状态,减少了线程切换上下文的消耗。

    为什么 OSSpinLock 不再安全?

    关于 OSSpinLock 不再安全,原因就在于优先级反转问题。

    优先级反转(Priority Inversion)

    什么情况叫做优先级反转?

    wikipedia 上是这么定义的:

    优先级倒置,又称优先级反转、优先级逆转、优先级翻转,是一种不希望发生的任务调度状态。在该种状态下,一个高优先级任务间接被一个低优先级任务所抢先(preemtped),使得两个任务的相对优先级被倒置。 这往往出现在一个高优先级任务等待访问一个被低优先级任务正在使用的临界资源,从而阻塞了高优先级任务;同时,该低优先级任务被一个次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,该次高优先级任务获得执行权。

    再消化一下

    有:高优先级任务A / 次高优先级任务B / 低优先级任务C / 资源Z 。
    A 等待 C 执行后的 Z
    而 B 并不需要 Z,抢先获得时间片执行
    C 由于没有时间片,无法执行(优先级相对没有B高)。
    这种情况造成 A 在C 之后执行,C在B之后,间接的高优先级A在次高优先级任务B 之后执行, 使得优先级被倒置了。(假设: A 等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源。此时 C 无法与 A 争夺 CPU 时间,从而 C 无法执行,进而无法释放资源。造成的后果,就是 A 无法获得 Z 而继续推进。)

    而 OSSpinLock 忙等的机制,就可能造成高优先级一直 running ,占用 cpu 时间片。而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

    优先级反转的解决方案

    关于优先级反转一般有以下三种解决方案

    优先级继承

    优先级继承,故名思义,是将占有锁的线程优先级,继承等待该锁的线程高优先级,如果存在多个线程等待,就取其中之一最高的优先级继承。

    优先级天花板

    优先级天花板,则是直接设置优先级上限,给临界区一个最高优先级,进入临界区的进程都将获得这个高优先级。

    如果其他试图进入临界区的进程的优先级,都低于这个最高优先级,那么优先级反转就不会发生。

    禁止中断

    禁止中断的特点,在于任务只存在两种优先级:可被抢占的 / 禁止中断的 。

    前者为一般任务运行时的优先级,后者为进入临界区的优先级。

    通过禁止中断来保护临界区,没有其它第三种的优先级,也就不可能发生反转了。

    为什么使用其它的锁,可以解决优先级反转?

    我们看到很多本来使用 OSSpinLock 的知名项目,都改用了其它方式替代,比如 pthread_mutex 和 dispatch_semaphore 。

    那为什么其它的锁,就不会有优先级反转的问题呢?如果按照上面的想法,其它锁也可能出现优先级反转。

    原因在于,其它锁出现优先级反转后,高优先级的任务不会忙等。因为处于等待状态的高优先级任务,没有占用时间片,所以低优先级任务一般都能进行下去,从而释放掉锁。

    线程调度

    为了帮助理解,要提一下有关线程调度的概念。

    无论多核心还是单核,我们的线程运行总是 "并发" 的。

    当 cpu 数量大于等于线程数量,这个时候是真正并发,可以多个线程同时执行计算。

    当 cpu 数量小于线程数量,总有一个 cpu 会运行多个线程,这时候"并发"就是一种模拟出来的状态。操作系统通过不断的切换线程,每个线程执行一小段时间,让多个线程看起来就像在同时运行。这种行为就称为 "线程调度(Thread Schedule)"

    线程状态

    在线程调度中,线程至少拥有三种状态 : 运行(Running),就绪(Ready),等待(Waiting)

    处于 Running的线程拥有的执行时间,称为 时间片(Time Slice),时间片 用完时,进入Ready状态。如果在Running状态,时间片没有用完,就开始等待某一个事件(通常是 IO 或 同步 ),则进入Waiting状态。

    如果有线程从Running状态离开,调度系统就会选择一个Ready的线程进入 Running 状态。而Waiting的线程等待的事件完成后,就会进入Ready状态。

    dispatch_semaphore

    dispatch_semaphore 是 GCD 中同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。

    信号量机制

    信号量中,二元信号量,是一种最简单的锁。只有两种状态,占用和非占用。二元信号量适合唯一一个线程独占访问的资源。而多元信号量简称 信号量(Semaphore)。

    信号量和互斥量的区别

    信号量是允许并发访问的,也就是说,允许多个线程同时执行多个任务。信号量可以由一个线程获取,然后由不同的线程释放。

    互斥量只允许一个线程同时执行一个任务。也就是同一个程获取,同一个线程释放。

    之前我对,互斥量只由一个线程获取和释放,理解的比较狭义,以为这里的获取和释放,是系统强制要求的,用 NSLock 实验发现它可以在不同线程获取和释放,感觉很疑惑。

    实际上,的确能在不同线程获取/释放同一个互斥锁,但互斥锁本来就用于同一个线程中上锁和解锁。这里的意义更多在于代码使用的层面。

    关键在于,理解信号量可以允许 N 个信号量允许 N 个线程并发地执行任务。

    @synchonized

    @synchonized 是一个递归锁。

    递归锁

    递归锁也称为可重入锁。互斥锁可以分为非递归锁/递归锁两种,主要区别在于:同一个线程可以重复获取递归锁,不会死锁; 同一个线程重复获取非递归锁,则会产生死锁。

    因为是递归锁,我们可以写类似这样的代码:


    - (void)testLock{
    if(_count>0){
    @synchronized (obj) {
    _count = _count - 1;
    [self testLock];
    }
    }
    }

    而如果换成NSLock,它就会因为递归发生死锁了。

    实际使用问题

    如果obj 为 nil,或者 obj地址不同,锁会失效。

    所以我们要防止如下的情况:

    @synchronized (obj) {
    obj = newObj;
    }

    这里的 obj 被更改后,等到其它线程访问时,就和没加锁一样直接进去了。

    另外一种情况,就是 @synchonized(self). 不少代码都是直接将self传入@synchronized当中,而 self 很容易作为一个外部对象,被调用和修改。所以它和上面是一样的情况,需要避免使用。

    正确的做法是什么?obj 应当传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的,不被随便修改的。

    pthread_mutex

    pthread定义了一组跨平台的线程相关的 API,其中可以使用 pthread_mutex作为互斥锁。

    pthread_mutex 不是使用忙等,而是同信号量一样,会阻塞线程并进行等待,调用时进行线程上下文切换。

    pthread_mutex` 本身拥有设置协议的功能,通过设置它的协议,来解决优先级反转:

    pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol)

    其中协议类型包括以下几种:

    • PTHREAD_PRIO_NONE:线程的优先级和调度不会受到互斥锁拥有权的影响。
    • PTHREAD_PRIO_INHERIT:当高优先级的等待低优先级的线程锁定互斥量时,低优先级的线程以高优先级线程的优先级运行。这种方式将以继承的形式传递。当线程解锁互斥量时,线程的优先级自动被降到它原来的优先级。该协议就是支持优先级继承类型的互斥锁,它不是默认选项,需要在程序中进行设置。
    • PTHREAD_PRIO_PROTECT:当线程拥有一个或多个使用 PTHREAD_PRIO_PROTECT初始化的互斥锁时,此协议值会影响其他线程(如 thrd2)的优先级和调度。thrd2 以其较高的优先级或者以thrd2 拥有的所有互斥锁的最高优先级上限运行。基于被thrd2拥有的任一互斥锁阻塞的较高优先级线程对于 thrd2的调度没有任何影响。

    设置协议类型为 PTHREAD_PRIO_INHERIT ,运用优先级继承的方式,可以解决优先级反转的问题。

    而我们在 iOS 中使用的 NSLock,NSRecursiveLock等都是基于pthread_mutex 做实现的。

    NSLock

    NSLock属于 pthread_mutex的一层封装, 设置了属性为 PTHREAD_MUTEX_ERRORCHECK 。

    它会损失一定性能换来错误提示。并简化直接使用 pthread_mutex 的定义。

    NSCondition

    NSCondition是通过pthread中的条件变量(condition variable) pthread_cond_t来实现的。

    条件变量

    在线程间的同步中,有这样一种情况: 线程 A 需要等条件 C 成立,才能继续往下执行.现在这个条件不成立,线程 A 就阻塞等待. 而线程 B 在执行过程中,使条件 C 成立了,就唤醒线程 A 继续执行。

    对于上述情况,可以使用条件变量来操作。

    条件变量,类似信号量,提供线程阻塞与信号机制,可以用来阻塞某个线程,等待某个数据就绪后,随后唤醒线程。

    一个条件变量总是和一个互斥量搭配使用。

    NSCondition其实就是封装了一个互斥锁和条件变量,互斥锁的lock/unlock方法和后者的wait/signal统一封装在 NSCondition对象中,暴露给使用者。

    用条件变量控制线程同步,最为经典的例子就是 生产者-消费者问题。

    生产者-消费者问题

    生产者消费者问题,是一个著名的线程同步问题,该问题描述如下:

    有一个生产者在生产产品,这些产品将提供给若干个消费者去消费。要求让生产者和消费者能并发执行,在两者之间设置一个具有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经放入产品的缓冲区中再次投放产品。

    我们可以刚好可以使用 NSCondition解决生产者-消费者问题。具体的代码放置在文末的 Demo 里了。

    if(count==0){
    [condition wait];
    }

    上面这样是不能保证消费者是线程安全的。

    因为NSCondition可以给每个线程分别加锁,但加锁后不影响其他线程进入临界区。所以 NSCondition使用 wait并加锁后,并不能真正保证线程的安全。

    当一个signal操作发出时,如果有两个线程都在做 消费者 操作,那同时都会消耗掉资源,于是绕过了检查。

    例如我们的条件是,count == 0 执行等待。

    假设当前 count = 0,线程A 要判断到 count == 0,执行等待;

    线程B 执行了count = 1,并唤醒线程A 执行 count - 1,同时线程C 也判断到 count > 0 。因为处在不同的线程锁,同样判断执行了 count - 1。2 个线程都会执行count - 1,但是 count = 1,实际就出现count = -1的情况。

    所以为了保证消费者操作的正确,使用 while 循环中的判断,进行二次确认:


     while (count == 0) {
    [condition wait];
    }

    条件变量和信号量的区别

    每个信号量有一个与之关联的值,发出时+1,等待时-1,任何线程都可以发出一个信号,即使没有线程在等待该信号量的值。

    可是对于条件变量,例如 pthread_cond_signal发出信号后,没有任何线程阻塞在 pthread_cond_wait 上,那这个条件变量上的信号会直接丢失掉。

    NSConditionLock

    NSConditionLock称为条件锁,只有 condition 参数与初始化时候的 condition相等,lock才能正确进行加锁操作。

    这里分清两个概念:

    • unlockWithCondition:,它是先解锁,再修改 condition 参数的值。 并不是当 condition 符合某个件值去解锁。
    • lockWhenCondition:,它与 unlockWithCondition: 不一样,不会修改 condition 参数的值,而是符合 condition 的值再上锁。

    在这里可以利用 NSConditionLock实现任务之间的依赖.

    NSRecursiveLock

    NSRecursiveLock 和前面提到的 @synchonized一样,是一个递归锁。

    NSRecursiveLock 与 NSLock 的区别在于内部封装的pthread_mutex_t 对象的类型不同,NSRecursiveLock 的类型被设置为 PTHREAD_MUTEX_RECURSIVE

    NSDistributedLock

    这里顺带提一下 NSDistributedLock, 是 macOS 下的一种锁.

    苹果文档 对于NSDistributedLock 的描述是:

    A lock that multiple applications on multiple hosts can use to restrict access to some shared resource, such as a file

    意思是说,它是一个用在多个主机间的多应用的锁,可以限制访问一些共享资源,例如文件。

    按字面意思翻译,NSDistributedLock 应该就叫做 分布式锁。但是看概念和资料,在 解决NSDistributedLock进程互斥锁的死锁问题(一) 里面看到,NSDistributedLock 更类似于文件锁的概念。 有兴趣的可以看一看 Linux 2.6 中的文件锁

    其它保证线程安全的方式

    除了用锁之外,有其它方法保证线程安全吗?

    使用单线程访问

    首先,尽量避免多线程的设计。因为多线程访问会出现很多不可控制的情况。有些情况即使上锁,也无法保证百分之百的安全,例如自旋锁的问题。

    不对资源做修改

    而如果还是得用多线程,那么避免对资源做修改。

    如果都是访问共享资源,而不去修改共享资源,也可以保证线程安全。

    比如NSArry作为不可变类是线程安全的。然而它们的可变版本,比如 NSMutableArray 是线程不安全的。事实上,如果是在一个队列中串行地进行访问的话,在不同线程中使用它们也是没有问题的。

    总结

    如果实在要使用多线程,也没有必要过分追求效率,而更多的考虑线程安全问题,使用对应的锁。

    对于平时编写应用里的多线程代码,还是建议用 @synchronized,NSLock 等,可读性和安全性都好,多线程安全比多线程性能更重要。



    作者:Cooci
    链接:https://www.jianshu.com/p/c557308c0ec5




    收起阅读 »

    iOS开发堆栈你理解多少?

    浅谈堆栈理解Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。1、栈区(stack):...
    继续阅读 »

    浅谈堆栈理解
    Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;

    栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。

    1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。
    2、堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样,其类是与链表。

    操作系统iOS 中应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在几个个不同的内存区域 。


    栈区(stack):
    1、存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;
    2、程序猿不需要管理栈区变量的内存; 栈区地址从高到低分配;

    堆区(heap):
    1、堆区的内存分配使用的是alloc;
    2、需要程序猿管理内存;
    3、ARC的内存的管理,是编译器再编译的时候自动添加 retain、release、autorelease;
    4、堆区的地址是从低到高分配)

    全局区/静态区(static):
    包括两个部分:未初始化过 、初始化过; 也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域; eg:int a;未初始化的。int a = 10;已初始化的。

    常量区:常量字符串cString等就是放在这里;

    代码区:存放App代码;

    例子:

    int a = 10;  全局初始化区
    char *p; 全局未初始化区

    main{
    int b; 栈区
    char s[] = "abcdef" 栈
    char *p1; 栈
    char *p2 = "qwerty"; \\\\qwerty在常量区,p2在栈上。
    static int c =0; 全局(静态)初始化区
    leap1 = (char *)malloc(100);
    leap2 = (char *)malloc(200);
    分配得来得100和200字节的区域就在堆区。
    }

    “stack”
    局部变量、参数、返回值都存在这里,函数调用开始会参数入栈、局部变量入栈;调用结束依次出栈。

    正如名称所示,stack 是后进先出(LIFO )结构。当函数调用其他的函数时,stack frame会被创建;当其他函数退出后,这个frame会自动被破坏。

    “heap”

    动态内存区域,使用alloc或new申请的内存;为了访问你创建在heap 中的数据,你最少要求有一个保存在stack中的指针,因为你要通过stack中的指针访问heap 中的数据。

    你可以认为stack 中的一个指针仅仅是一个整型变量,保存了heap 中特定内存地址的数据。实际上,它有一点点复杂,但这是它的基本结构。

    简而言之,操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。

    在iOS 操作系统的stack 段和heap 段中,一般来说你都可以创建数据对象。

    stack 对象的优点主要有两点,一是创建速度快,二是管理简单,它有严格的生命周期。stack 对象的缺点是它不灵活。创建时长度是多大就一直是多 大,创建时是哪个函数创建的,它的owner 就一直是它。不像heap 对象那样有多个owner ,其实多个owner 等同于引用计数。只有 heap 对象才是采用“引用计数”方法管理它。

    堆空间和栈空间的大小是可变的,堆空间从下往上生长,栈空间从上往下生长。

    stack 对象的创建

    只要栈的剩余空间大于 stack 对象申请创建的空间,操作系统就会为程序提供这段内存空间,否则将报异常提示栈溢出。

    heap 对象的创建

    操作系统对于内存heap 段是采用链表进行管理的。操作系统有一个记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找第一个空间大于所申请的heap 节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。

    例如:

    NSString 的对象就是 stack 中的对象,NSMutableString 的对象就是heap 中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,可有多个owner, 适用于计数管理内存管理模式。

    两类对象的创建方法也不同,前者直接创建 NSString * str1=@"welcome"; ,而后者需要先分配再初始化 NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];。

    引用计数是放在堆内存中的一个整型,对象alloc开辟堆内存空间后,引用计数自动置1;

    NSString直接赋值是创建在_TEXT段中,_TEXT段是在编译时保存程序代码段的机器码,也就是说NSString会以字符串的形式保存起来,只要字符串名称相同,其地址就相同,就算在新建一个名字一样的NSString,还是原来那个;顺便讲一下_DATA段,他是保存全局变量和静态变量的值的)

    _TEXT段:整个程序的代码,以及所有的常量。这部分内存是是固定大小的,只读的。
    _DATA段:初始化为非零值的全局变量。
    BSS段:初始化为0或未初始化的全局变量和静态变量。
    更多细节我后面会讲一篇Mach-O内核方面的文章;

    静态和全局的区别

    static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;

    static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;

    static函数与普通函数有什么区别:static函数与普通函数作用域不同,只在定义该变量的源文件内有效;

    全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。

    补充:内存引用计数的实现

    GNUstep的实现是将引用计数保存在对象占用内存块头部的变量中

    好处是:

    少量的代码即可完成。

    能够统一管理引用计数内存块和对象引用计数内存块

    苹果的实现是保存在引用计数hash表中

    好处是:

    对象用内存块的分配无需考虑内存块的头部

    引用计数表各记录中存有内存块地址,可以从各个记录追溯到各对象的内存块,这点对调试非常重要

    weak对象释放是自动致nil实现:

    也是通过一个weakhash表实现的,将weak的对象地址注册到weakhash表中,如果该对象被destroy销毁,则在weak表中将该对象地址致nil,并清除记录

    链接:https://www.jianshu.com/p/1f075bdc2e29

    收起阅读 »

    浅谈Android插件化

    一、认识插件化1.1 插件化起源插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。想必大家都知道,在 Android ...
    继续阅读 »

    一、认识插件化

    1.1 插件化起源

    插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。

    想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已。

    常见的应用安装目录有:

    • /system/app:系统应用
    • /system/priv-app:系统应用
    • /data/app:用户应用

    那可能大家会想问,既然安装这个过程如此简单,Android 是怎么运行应用中的代码的呢,我们先看 Apk 的构成,一个常见的 Apk 会包含如下几个部分:

    • classes.dexJava 代码字节码
    • res:资源文件
    • libso 文件
    • assets:静态资产文件
    • AndroidManifest.xml:清单文件

    其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已。

    那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?

    1.2 插件化优点

    插件化让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益:

    • 减少安装Apk的体积、按需下载模块
    • 动态更新插件
    • 宿主和插件分开编译,提升开发效率
    • 解决方法数超过65535的问题

    想象一下,你的应用拥有 Native 应用一般极高的性能,又能获取诸如 Web 应用一样的收益。

    嗯,理想很美好不是嘛?

    1.3 与组件化的区别

    • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。
    • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。

    二、插件化的技术难点

    想让插件的Apk真正运行起来,首先要先能找到插件Apk的存放位置,然后我们要能解析加载Apk里面的代码。

    但是光能执行Java代码是没有意义的,在Android系统中有四大组件是需要在系统中注册的,具体来说是在 Android 系统的 ActivityManagerService (AMS) 和 PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMS 和 PMS,如何欺骗系统,让他承认一个未安装的 Apk 中的组件,如何让宿主动态加载执行插件Apk中 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等是插件化最大的难点。

    另外,应用资源引用(特指 R 中引用的资源,如 layoutvalues 等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。

    总结一下,其实做到插件化的要点就这几个:

    • 如何加载并执行插件 Apk 中的代码(ClassLoader Injection
    • 让系统能调用插件 Apk 中的组件(Runtime Container
    • 正确识别插件 Apk 中的资源(Resource Injection

    当然还有其他一些小问题,但可能不是所有场景下都会遇到,我们后面再单独说。

    三、ClassLoader Injection

    ClassLoader 是插件化中必须要掌握的,因为我们知道Android 应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk,系统不会处理其中的类,所以需要使用 ClassLoader 加载 Apk,然后反射里面的代码。

    3.1 java 中的 ClassLoader

    • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等

    • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包

    • AppClassLoader 负责加载 classpath 里的 jar 包和目录

    3.2 android 中的 ClassLoader

    在Android系统中ClassLoader是用来加载dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件

    • PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

      public class PathClassLoader extends BaseDexClassLoader {
      public PathClassLoader(String dexPath, ClassLoader parent) {
      super(dexPath, null, null, parent);
      }

      public PathClassLoader(String dexPath, String libraryPath,
      ClassLoader parent) {
      super(dexPath, null, libraryPath, parent);
      }
      }

    • DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

      public class DexClassLoader extends BaseDexClassLoader {
      public DexClassLoader(String dexPath, String optimizedDirectory,
      String libraryPath, ClassLoader parent) {
      super(dexPath, new File(optimizedDirectory), libraryPath, parent);
      }
      }

    我们在插件化中一般使用的是 DexClassLoader。

    3.3 双亲委派机制

    每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。

        protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
    try {
    if (parent != null) {
    // 先从父类加载器中进行加载
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }

    if (c == null) {
    // 没有找到,再自己加载
    c = findClass(name);
    }
    }
    return c;
    }

    3.4 如何加载插件中的类

    要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要哪些参数。

    public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    // ...
    }
    }

    构造函数需要四个参数: dexPath 是需要加载的 dex / apk / jar 文件路径 optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置 librarySearchPath 是 native 依赖的位置 parent 就是父类加载器,默认会先从 parent 加载对应的类

    创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:

        // 从 assets 中拿出插件 apk 放到内部存储空间
    private fun extractPlugin() {
    var inputStream = assets.open("plugin.apk")
    File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init() {
    extractPlugin()
    pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
    nativeLibDir = File(filesDir, "pluginlib").absolutePath
    dexOutPath = File(filesDir, "dexout").absolutePath
    // 生成 DexClassLoader 用来加载插件类
    pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
    }

    3.5 执行插件类的方法

    通过反射来执行类的方法

    val loadClass = pluginClassLoader.loadClass(activityName)
    loadClass.getMethod("test",null).invoke(loadClass)

    我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。

    四、Runtime Container

    我们之前说到 Activity 插件化最大的难点是如何欺骗系统,让他承认一个未安装的 Apk 中的组件。 因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。 如果直接把插件的 Activity 注册到宿主 Manifest 里就失去了插件化的动态特性,因为每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。

    4.1 为什么没有注册的 Activity 不能和系统交互

    这里的不能直接交互的含义有两个

    1. 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:

      android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?


    这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:

    public class Instrumentation {
    public static void checkStartActivityResult(int res, Object intent) {
    if (!ActivityManager.isStartResultFatalError(res)) {
    return;
    }

    switch (res) {
    case ActivityManager.START_INTENT_NOT_RESOLVED:
    case ActivityManager.START_CLASS_NOT_FOUND:
    if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
    throw new ActivityNotFoundException(
    "Unable to find explicit activity class "
    + ((Intent)intent).getComponent().toShortString()
    + "; have you declared this activity in your AndroidManifest.xml?");
    throw new ActivityNotFoundException(
    "No Activity found to handle " + intent);
    ...
    }
    }
    }


    1. Activity 的生命周期无法被调用,其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。

    4.2 运行时容器技术

    由于Android中的组件(Activity,Service,BroadcastReceiver和ContentProvider)是由系统创建的,并且由系统管理生命周期。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。插件化如何支持组件生命周期的管理。 大致分为两种方式:

    • 运行时容器技术(ProxyActivity代理)
    • 预埋StubActivity,hook系统启动Activity的过程

    我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。

    它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:

    • pluginName
    • pluginApkPath
    • pluginActivityName

    等,其实最重要的就是 pluginApkPath 和 pluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:

    • 转发所有来自系统的生命周期回调至插件 Activity
    • 接受 Activity 方法的系统调用,并转发回系统

    我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity

    public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    String pluginActivityName = getIntent().getString("pluginActivityName", "");
    pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
    if (pluginActivity == null) {
    super.onCreate(savedInstanceState);
    return;
    }

    pluginActivity.onCreate();
    }

    @Override
    protected void onResume() {
    if (pluginActivity == null) {
    super.onResume();
    return;
    }
    pluginActivity.onResume();
    }

    @Override
    protected void onPause() {
    if (pluginActivity == null) {
    super.onPause();
    return;
    }
    pluginActivity.onPause();
    }

    // ...
    }

    public class PluginActivity {

    private ContainerActivity containerActivity;

    public PluginActivity(ContainerActivity containerActivity) {
    this.containerActivity = containerActivity;
    }

    @Override
    public T findViewById(int id) {
    return containerActivity.findViewById(id);
    }
    // ...
    }

    // 插件 `Apk` 中真正写的组件
    public class TestActivity extends PluginActivity {
    // ......
    }

    是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。

    4.3 字节码替换

    该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。

    class TestActivity extends Activity {}
    ->
    class TestActivity extends PluginActivity {}

    有没有什么办法能让插件组件的编写与原来没有任何差别呢?

    Shadow 的做法是字节码替换插件,这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。

    实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:

    class TestActivity extends Activity {}
    然后完成编译后,最后的字节码中,显示的却是:
    class TestActivity extends PluginActivity {}

    到这里基本的框架就差不多结束了。

    五、Resource Injection

    最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id

    资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:

    • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的 PackageInfo
    • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例

    我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:

    PackageManager packageManager = getPackageManager();
    PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,
    PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
    );
    packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
    packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

    Resources injectResources = null;
    try {
    injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
    } catch (PackageManager.NameNotFoundException e) {
    // ...
    }

    拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:

    public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources, Resources injectResources) {
    super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
    this.hostResources = hostResources;
    this.injectResources = injectResources;
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
    try {
    return injectResources.getString(id, formatArgs);
    } catch (NotFoundException e) {
    return hostResources.getString(id, formatArgs);
    }
    }

    // ...
    }

    然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:

    public class ContainerActivity extends Activity {
    private Resources pluginResources;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    // ...
    pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
    // ...
    }

    @Override
    public Resources getResources() {
    if (pluginActivity == null) {
    return super.getResources();
    }
    return pluginResources;
    }
    }

    这样就完成了资源的注入。

    收起阅读 »

    APP路由框架与组件化简析

    前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。路由的概...
    继续阅读 »

    前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。

    路由的概念

    路由这个词本身应该是互联网协议中的一个词,维基百科对此的解释如下:

    路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

    个人理解,在前端开发中,路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme,并生成路由表,然后,能够根据外部输入字符串在路由表中匹配到对应的页面或者服务,进行跳转或者调用,并提供会获取返回值等,示意如下

    image.png

    所以一个基本路由框架要具备如下能力:

      1. APP路由的扫描及注册逻辑
      1. 路由跳转target页面能力
      1. 路由调用target服务能力

    APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。

    三方路由框架是否是APP强需求

    答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。

    Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。

    原生路由的限制:功能单一,扩展灵活性差,不易协同

    传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:


    import com.snail.activityforresultexample.test.SecondActivity;

    public class MainActivity extends AppCompatActivity {

    void jumpSecondActivityUseClassName(){

    Intent intent =new Intent(MainActivity.this, SecondActivity.class);
    startActivity(intent);
    }

    显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。

    第一步:manifest中配置activity的intent-filter,至少要配置一个action
















    第二步:调用

    void jumpSecondActivityUseFilter() {
    Intent intent = new Intent();
    intent.setAction("com.snail.activityforresultexample.SecondActivity");
    startActivity(intent);
    }

    如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:

    • 首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。
    • 其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。
    • 最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。

    可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的

    APP三方路由框架需具备的能力

    目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:

    • 路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑
    • scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离
    • 基础路由跳转能力 :页面跳转能力的支持
    • 服务类组件的支持 :如去某个服务组件获取一些配置等
    • [扩展]路由拦截逻辑:比如登陆,统一鉴权
    • 可定制的降级逻辑:找不到组件时的兜底

    可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,

    	@Route(path = "/test/activity2")
    public class Test2Activity extends AppCompatActivity {
    ...
    }

    build阶段会根据注解搜集路由scheme,生成路由表。第二步使用

            ARouter.getInstance()
    .build("/test/activity2")
    .navigation(this);

    如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。

    APP路由框架的实现

    路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询

    路由表的自动生成

    生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,

    image.png

    不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:

    image.png

    其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。

    JavaPoet如何搜集并生成路由表集合?

    以ARouter框架为例,先定义Router框架需要的注解如:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.CLASS)
    public @interface Route {

    /**
    * Path of route
    */

    String path();

    该注解用于标注需要路由的组件,用法如下:

    @Route(path = "/test/activity1", name = "测试用 Activity")
    public class Test1Activity extends BaseActivity {
    @Autowired
    int age = 10;

    之后利用APT扫描所有被注解的类,生成路由表,实现参考如下:

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
    if (CollectionUtils.isNotEmpty(annotations)) {

    Set routeElements = roundEnv.getElementsAnnotatedWith(Route.class);

    this.parseRoutes(routeElements);
    ...
    return false;
    }


    private void parseRoutes(Set routeElements) throws IOException {
    ...
    // Generate groups
    String groupFileName = NAME_OF_GROUP + groupName;
    JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
    TypeSpec.classBuilder(groupFileName)
    .addJavadoc(WARNING_TIPS)
    .addSuperinterface(ClassName.get(type_IRouteGroup))
    .addModifiers(PUBLIC)
    .addMethod(loadIntoMethodOfGroupBuilder.build())
    .build()
    ).build().writeTo(mFiler);

    产物如下:包含路由表,及局部注册入口。

    image.png

    自动注册:ASM搜集上述路由表并聚合插入Init代码区

    为了能够插入到Init代码区,首先需要预留一个位置,一般定义一个空函数,以待后续填充:

    	public class RouterInitializer {

    public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
    ...
    loadRouterTables();
    }
    //自动注册代码
    public static void loadRouterTables() {

    }
    }

    首先利用AOP工具,遍历上述APT中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,

    • 搜集目标,聚合路由表

        /**扫描jar*/
      fun scanJar(jarFile: File, dest: File?) {

      val file = JarFile(jarFile)
      var enumeration = file.entries()
      while (enumeration.hasMoreElements()) {
      val jarEntry = enumeration.nextElement()
      if (jarEntry.name.endsWith("XXRouterTable.class")) {
      val inputStream = file.getInputStream(jarEntry)
      val classReader = ClassReader(inputStream)
      if (Arrays.toString(classReader.interfaces)
      .contains("IHTRouterTBCollect")
      ) {
      tableList.add(
      Pair(
      classReader.className,
      dest?.absolutePath
      )
      )
      }
      inputStream.close()
      } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
      registerInitClass = dest
      }
      }
      file.close()
      }

    • 对目标Class注入路由表初始化代码

        fun asmInsertMethod(originFile: File?) {

      val optJar = File(originFile?.parent, originFile?.name + ".opt")
      if (optJar.exists())
      optJar.delete()
      val jarFile = JarFile(originFile)
      val enumeration = jarFile.entries()
      val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

      while (enumeration.hasMoreElements()) {
      val jarEntry = enumeration.nextElement()
      val entryName = jarEntry.getName()
      val zipEntry = ZipEntry(entryName)
      val inputStream = jarFile.getInputStream(jarEntry)
      //插桩class
      if (entryName.endsWith("RouterInitializer.class")) {
      //class文件处理
      jarOutputStream.putNextEntry(zipEntry)
      val classReader = ClassReader(IOUtils.toByteArray(inputStream))
      val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
      val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
      classReader.accept(cv, EXPAND_FRAMES)
      val code = classWriter.toByteArray()
      jarOutputStream.write(code)
      } else {
      jarOutputStream.putNextEntry(zipEntry)
      jarOutputStream.write(IOUtils.toByteArray(inputStream))
      }
      jarOutputStream.closeEntry()
      }
      //结束
      jarOutputStream.close()
      jarFile.close()
      if (originFile?.exists() == true) {
      Files.delete(originFile.toPath())
      }
      optJar.renameTo(originFile)
      }

    最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码:

     public static void loadRouterTables() {


    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
    ...
    }

    如此就完成了路由表的搜集与注册,大概的流程就是如此。当然对于支持服务、Fragment等略有不同,但大体类似。

    Router框架对服务类组件的支持

    通过路由的方式获取服务属于APP路由比较独特的能力,比如有个用户中心的组件,我们可以通过路由的方式去查询用户是否处于登陆状态,这种就不是狭义上的页面路由的概念,通过一串字符串如何查到对应的组件并调用其方法呢?这种的实现方式也有多种,每种实现方式都有自己的优劣。

    • 一种是可以将服务抽象成接口,沉到底层,上层实现通过路由方式映射对象
    • 一种是将实现方法直接通过路由方式映射

    先看第一种,这种事Arouter的实现方式,它的优点是所有对外暴露的服务都暴露接口类【沉到底层】,这对于外部的调用方,也就是服务使用方非常友好,示例如下:

    先定义抽象服务,并沉到底层

    image.png

    public interface HelloService extends IProvider {
    void sayHello(String name);
    }

    实现服务,并通过Router注解标记

    @Route(path = "/yourservicegroupname/hello")
    public class HelloServiceImpl implements HelloService {
    Context mContext;

    @Override
    public void sayHello(String name) {
    Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }

    使用:利用Router加scheme获取服务实例,并映射成抽象类,然后直接调用方法。

      ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

    这种实现方式对于使用方其实是很方便的,尤其是一个服务有多个可操作方法的时候,但是缺点是扩展性,如果想要扩展方法,就要改动底层库。

    再看第二种:将实现方法直接通过路由方式映射

    服务的调用都要落到方法上,参考页面路由,也可以支持方法路由,两者并列关系,所以组要增加一个方法路由表,实现原理与Page路由类似,跟上面的Arouter对比,不用定义抽象层,直接定义实现即可:

    定义Method的Router

    	public class HelloService {


    @MethodRouter(url = {"arouter://sayhello"})
    public void sayHello(String name) {
    Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }

    使用即可

     RouterCall.callMethod("arouter://sayhello?name=hello");

    上述的缺点就是对于外部调用有些复杂,尤其是处理参数的时候,需要严格按照协议来处理,优点是,没有抽象层,如果需要扩展服务方法,不需要改动底层。

    上述两种方式各有优劣,不过,如果从左服务组件的初衷出发,第一种比较好:对于调用方比较友好。另外对于CallBack的支持,Arouter的处理方式可能也会更方便一些,可以比较方便的交给服务方定义。如果是第二种,服务直接通过路由映射的方式,处理起来就比较麻烦,尤其是Callback中的参数,可能要统一封装成JSON并维护解析的协议,这样处理起来,可能不是很好。

    路由表的匹配

    路由表的匹配比较简单,就是在全局Map中根据String输入,匹配到目标组件,然后依赖反射等常用操作,定位到目标。

    组件化与路由的关系

    组件化是一种开发集成模式,更像一种开发规范,更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件,这些组件之间可能是不同的团队,处于不同的目的在各自维护,甚至是需要代码隔离,如果牵扯到组件间的调用与通信,就不可避免的借助路由,因为实现隔离的,只能采用通用字符串scheme进行通信,这就是路由的功能范畴。

    组件化需要路由支撑的根本原因:组件间代码实现的隔离

    总结

    • 路由不是一个APP的必备功能,但是大型跨团队的APP基本都需要
    • 路由框架的基本能力:路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能
    • 组件化与路由的关系:组件化的代码隔离导致路由框架成为必须
    收起阅读 »

    MVVMFrame for Android 是一个基于Google官方推出的JetPack(Lifecycle,LiveData,ViewModel,Room)构建的快速开发框架,从此构建一个MVVM模式的项目变得快捷简单。

    MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开...
    继续阅读 »

    MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开发框架。有了 MVVMFrame 的加持,从此构建一个 MVVM 模式的项目变得快捷简单。

    架构

    Image

    Android version

    引入

    由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

    1. 在Project的 build.gradle 里面添加远程仓库
    allprojects {
    repositories {
    //...
    mavenCentral()
    }
    }
    1. 在Module的 build.gradle 里面添加引入依赖项

    v2.x(使用 Hilt 简化 Dagger2 依赖注入用法)

    //AndroidX 版本
    implementation 'com.github.jenly1314:mvvmframe:2.1.0'

    以前发布至JCenter的版本

    v2.0.0(使用 Hilt 简化 Dagger2 依赖注入用法)

    //AndroidX 版本
    implementation 'com.king.frame:mvvmframe:2.0.0'

    v1.x 以前版本(使用 Dagger2)

    //AndroidX 版本
    implementation 'com.king.frame:mvvmframe:1.1.4'

    //Android Support版本
    implementation 'com.king.frame:mvvmframe:1.0.2'

    Dagger和 Room 的相关注解处理器

    你需要引入下面的列出的编译时的注解处理器,用于自动生成相关代码。其它对应版本具体详情可查看 Versions

    v2.x 版本($versions 相关可查看Versions

    你需要在项目根目录的 build.gradle 文件中配置 Hilt 的插件路径:

    buildscript {
    ...
    dependencies {
    ...
    classpath "com.google.dagger:hilt-android-gradle-plugin:$versions.daggerHint"
    }
    }

    接下来,在 app/build.gradle 文件中,引入 Hilt 的插件和相关依赖:

    ...
    apply plugin: 'dagger.hilt.android.plugin'

    dependencies{
    ...

    //AndroidX ------------------ MVVMFrame v2.x.x
    //lifecycle
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle"
    //room
    annotationProcessor "androidx.room:room-compiler:$versions.room"
    //hilt
    implementation "com.google.dagger:hilt-android:$versions.daggerHint"
    annotationProcessor "com.google.dagger:hilt-android-compiler:$versions.daggerHint"

    //从2.1.0以后已移除
    // implementation "androidx.hilt:hilt-lifecycle-viewmodel:$versions.hilt"
    // annotationProcessor "androidx.hilt:hilt-compiler:$versions.hilt"
    }

    v1.x 以前版本,建议 查看分支版本

    在 app/build.gradle 文件中引入 Dagger 和 Room 相关依赖:


    dependencies{
    ...

    //AndroidX ------------------ MVVMFrame v1.1.4
    //dagger
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.30.1'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.30.1'
    //room
    annotationProcessor 'androidx.room:room-compiler:2.2.5'
    }

    dependencies{
    ...

    // Android Support ------------------ MVVMFrame v1.0.2
    //dagger
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.19'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
    //room
    annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
    }

    如果你的项目使用的是 Kotlin,记得加上 kotlin-kapt 插件,并需使用 kapt 替代 annotationProcessor

    MVVMFrame引入的库(具体对应版本请查看 Versions

        //appcompat
    compileOnly deps.appcompat

    //retrofit
    api deps.retrofit.retrofit
    api deps.retrofit.gson
    api deps.retrofit.converter_gson

    //retrofit-helper
    api deps.jenly.retrofit_helper

    //lifecycle
    api deps.lifecycle.runtime
    api deps.lifecycle.extensions
    annotationProcessor deps.lifecycle.compiler

    //room
    api deps.room.runtime
    annotationProcessor deps.room.compiler

    //hilt
    compileOnly deps.dagger.hilt_android
    annotationProcessor deps.dagger.hilt_android_compiler

    compileOnly deps.hilt.hilt_viewmodel
    annotationProcessor deps.hilt.hilt_compiler

    //log
    api deps.timber

    示例

    集成步骤代码示例 (示例出自于app中)

    Step.1 启用DataBinding,在你项目中的build.gradle的android{}中添加配置:

    Android Studio 4.x 以后版本

    buildFeatures{
    dataBinding = true
    }

    Android Studio 4.x 以前版本

    dataBinding {
    enabled true
    }

    Step.2 使用JDK8编译(v1.1.2新增),在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    Step.3 自定义全局配置(继承MVVMFrame中的FrameConfigModule)(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

    /**
    * 自定义全局配置
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    public class AppConfigModule extends FrameConfigModule {
    @Override
    public void applyOptions(Context context, ConfigModule.Builder builder) {
    builder.baseUrl(Constants.BASE_URL)//TODO 配置Retrofit中的baseUrl
    .retrofitOptions(new RetrofitOptions() {
    @Override
    public void applyOptions(Retrofit.Builder builder) {
    //TODO 配置Retrofit
    //如想使用RxJava
    //builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    }
    })
    .okHttpClientOptions(new OkHttpClientOptions() {
    @Override
    public void applyOptions(OkHttpClient.Builder builder) {
    //TODO 配置OkHttpClient
    }
    })
    .gsonOptions(new GsonOptions() {
    @Override
    public void applyOptions(GsonBuilder builder) {
    //TODO 配置Gson
    }
    })
    .roomDatabaseOptions(new RoomDatabaseOptions<RoomDatabase>() {
    @Override
    public void applyOptions(RoomDatabase.Builder<RoomDatabase> builder) {
    //TODO 配置RoomDatabase
    }
    });
    }
    }

    Step.4 在你项目中的AndroidManifest.xml中通过配置meta-data来自定义全局配置(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

    <!-- MVVMFrame 全局配置 -->
    <meta-data android:name="com.king.mvvmframe.config.AppConfigModule"
    android:value="FrameConfigModule"/>

    Step.5 关于Application

    2.x版本 因为从2.x开始使用到了Hilt,所以你自定义的Application需加上 @HiltAndroidApp 注解,这是使用Hilt的一个必备前提。示例如下:

       @HiltAndroidApp
    public class YourApplication extends Application {

    }

    1.x版本 将你项目的 Application 继承MVVMFrame中的 BaseApplication

    /**
    * MVVMFrame 框架基于Google官方的Architecture Components dependencies 构建,在使用MVVMFrame时,需遵循一些规范:
    * 1.你的项目中的Application中需初始化MVVMFrame框架相关信息,有两种方式处理:
    * a.直接继承本类{@link BaseApplication}即可;
    * b.如你的项目中的Application本身继承了其它第三方的Application,因为Java是单继承原因,导致没法继承本类,可参照{@link BaseApplication}类,
    * 将{@link BaseApplication}中相关代码复制到你项目的Application中,在相应的生命周期中调用即可。
    *
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    public class App extends BaseApplication {

    @Override
    public void onCreate() {
    //TODO 如果默认配置已经能满足你的需求,你不需要自定义配置,可以通过下面注释掉的方式设置 BaseUrl,从而可以省略掉 step3 , setp4 两个步骤。
    // RetrofitHelper.getInstance().setBaseUrl(baseUrl);
    super.onCreate();
    //开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
    ApplicationComponent appComponent = DaggerApplicationComponent.builder()
    .appComponent(getAppComponent())
    .build();
    //注入
    appComponent.inject(this);

    }


    }

    其他

    关于v2.x

    因为v2.x版本 使用了 Hilt 的缘故,简化了之前 Dagger2 的用法,建议在新项目中使用。如果是从 v1.x 升级到 v2.x,集成步骤稍有变更,详情请查看 Step.5,并且可能还需要删除以前 @Component@Module等注解桥接层相关的逻辑代码,因为从v2.x开始,这些桥接逻辑无需自己编写,全部交由 Hilt 处理。

    关于使用 Hilt

    Hilt 是JetPack中新增的一个依赖注入库,其基于Dagger2研发(后面统称为Dagger),但它不同于Dagger。对于Android开发者来说,Hilt可以说专门为Android 打造。

    之前使用的Dagger for Android虽然也是针对于Android打造,也能通过 @ContributesAndroidInjector 来通过生成简化一部分样板代码,但是感觉还不够彻底。因为 Component 层相关的桥接还是要自己写。Hilt的诞生改善了这些问题。

    Hilt 大幅简化了Dagger 的用法,使得我们不用通过 @Component 注解去编写桥接层的逻辑,但是也因此限定了注入功能只能从几个 Android 固定的入口点开始,

    Hilt 一共支持 6 个入口点,分别是:

    Application

    Activity

    Fragment

    View

    Service

    BroadcastReceiver

    其中,只有 Application 这个入口点是使用 @HiltAndroidApp 注解来声明,示例如下

    Application 示例

       @HiltAndroidApp
    public class YourApplication extends Application {

    }

    其他的所有入口点,都是用 @AndroidEntryPoint 注解来声明,示例如下

    Activity 示例

       @AndroidEntryPoint
    public class YourActivity extends BaseActivity {

    }

    Fragment 示例

       @AndroidEntryPoint
    public class YourFragment extends BaseFragment {

    }

    Service 示例

       @AndroidEntryPoint
    public class YourService extends BaseService {

    }

    BroadcastReceiver 示例

       @AndroidEntryPoint
    public class YourBroadcastReceiver extends BaseBroadcastReceiver {

    }

    其它示例

    BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在类上添加 @HiltViewModel 并在构造函数上添加 @Inject 注解)

       @HiltViewModel
    public class YourViewModel extends BaseViewModel<YourModel> {
    @Inject
    public DataViewModel(@NonNull Application application, YourModel model) {
    super(application, model);
    }
    }

    BaseModel 示例 (如果您继承使用了BaseModel或其子类,你需要参照如下方式在构造函数上添加 @Inject 注解)

       public class YourModel extends BaseModel {
    @Inject
    public BaseModel(IDataRepository dataRepository){
    super(dataRepository);
    }
    }

    如果使用的是 v2.0.0 版本 (使用 androidx.hilt:hilt-lifecycle-viewmodel 的方式)

    BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在构造函数上添加 @ViewModelInject 注解)

       public class YourViewModel extends BaseViewModel<YourModel> {
    @ViewModelInject
    public DataViewModel(@NonNull Application application, YourModel model) {
    super(application, model);
    }
    }

    关于使用 Dagger

    之所以特意说 Dagger 是因为Dagger的学习曲线相对陡峭一点,没那么容易理解。

    1. 如果你对 Dagger 很了解,那么你将会更加轻松的去使用一些注入相关的骚操作。

    因为 MVVMFrame 中使用到了很多 Dagger 注入相关的一些操作。所以会涉及Dagger相关技术知识。

    但是并不意味着你一定要会使用 Dagger,才能使用MVVMFrameComponent

    如果你对 Dagger 并不熟悉,其实也是可以用的,因为使用 Dagger 全局注入主要都已经封装好了。你只需参照Demo 中的示例,照葫芦画瓢。 主要关注一些继承了BaseActivityBaseFragmentBaseViewModel等相关类即可。

    这里列一些主要的通用注入参照示例:(下面Dagger相关的示例仅适用于v1.x版本,因为v2.x已基于Hilt编写,简化了Dagger依赖注入桥接层相关逻辑)

    直接或间接继承了 BaseActivity 的配置示例:

    /**
    * Activity模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(subcomponents = BaseActivitySubcomponent.class)
    public abstract class ActivityModule {

    @ContributesAndroidInjector
    abstract MainActivity contributeMainActivity();

    }

    直接或间接继承了 BaseFragment 的配置示例:

    /**
    * Fragment模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(subcomponents = BaseFragmentSubcomponent.class)
    public abstract class FragmentModule {

    @ContributesAndroidInjector
    abstract MainFragment contributeMainFragment();

    }

    直接或间接继承了 BaseViewModel 的配置示例:

    /**
    * ViewModel模块统一管理:通过{@link Binds}和{@link ViewModelKey}绑定关联对应的ViewModel
    * ViewModelModule 例子
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module
    public abstract class ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(MainViewModel.class)
    abstract ViewModel bindMainViewModel(MainViewModel viewModel);
    }

    ApplicationModule 的配置示例

    /**
    * Application模块:为{@link ApplicationComponent}提供注入的各个模块
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(includes = {ViewModelFactoryModule.class,ViewModelModule.class,ActivityModule.class,FragmentModule.class})
    public class ApplicationModule {

    }

    ApplicationComponent 的配置示例

    /**
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @ApplicationScope
    @Component(dependencies = AppComponent.class,modules = {ApplicationModule.class})
    public interface ApplicationComponent {
    //指定你的 Application 继承类
    void inject(App app);
    }

    通过上面的通用配置注入你所需要的相关类之后,如果配置没什么问题,你只需 执行Make Project 一下,或通过 Make Project 快捷键 Ctrl + F9 ,就可以自动生产相关代码。 比如通过 ApplicationComponent 生成的 DaggerApplicationComponent 类。

    然后在你的 Application 集成类 App 中通过 DaggerApplicationComponent 构建 ApplicationComponent,然后注入即可。

        //开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
    ApplicationComponent appComponent = DaggerApplicationComponent.builder()
    .appComponent(getAppComponent())
    .build();
    //注入
    appComponent.inject(this);

    你也可以直接查看app中的源码示例

    关于设置 BaseUrl

    目前通过设置 BaseUrl 的入口主要有两种:

    1.一种是通过在 Manifest 中配置 meta-data 的来自定义 FrameConfigModule,在里面 通过 {@link ConfigModule.Builder#baseUrl(String)}来配置 BaseUrl。(一次设置,全局配置)

    2.一种就是通过RetrofitHelper {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来配置 BaseUrl。(可多次设置,动态全局配置,有前提条件)

    以上两种配置 BaseUrl 的方式都可以达到目的。但是你可以根据不同的场景选择不同的配置方式。

    主要场景与选择如下:

    一般场景:对于只使用单个不变的 BaseUrl的

    场景1:如果本库的默认已满足你的需求,无需额外自定义配置的。
         选择:建议你直接使用 {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来初始化 BaseUrl,切记在框架配置初始化 BaseUrl之前,建议在你自定义的 {@link Application#onCreate()}中初始化。
    场景2:如果本库的默认配置不满足你的需求,你需要自定义一些配置的。(比如需要使用 RxJava相关)
         选择:建议你在自定义配置中通过 {@link ConfigModule.Builder#baseUrl(String)} 来初始化 BaseUrl。

    二般场景:对于只使用单个 BaseUrl 但是,BaseUrl中途会变动的。

    场景3:和一般场景一样,也能分两种,所以选择也和一般场景也可以是一样的。
         选择:两种选择都行,但当 BaseUrl需要中途变动时,还需将 {@link RetrofitHelper#setDynamicDomain(boolean)} 设置为 {@code true} 才能支持动态改变 BaseUrl。

    特殊场景:对于支持多个 BaseUrl 且支持动态可变的。

       选择:这个场景的选择,主要涉及到另外的方法,请查看 {@link RetrofitHelper#putDomain(String, String)} 和 {@link RetrofitHelper#putDomain(String, HttpUrl)}相关详情

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档


    代码下载:MVVMFrame.zip

    收起阅读 »

    RetrofitHelper是一个支持配置多个BaseUrl,支持动态改变BaseUrl,动态配置超时时长的Retrofit帮助类

    RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。 支持配置多个BaseUrl 支持动态改变BaseUrl 支持动态配置超时时长 支持添加公...
    继续阅读 »


    RetrofitHelper

    RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。

    主要功能介绍

    •  支持配置多个BaseUrl
    •  支持动态改变BaseUrl
    •  支持动态配置超时时长
    •  支持添加公共请求头

    Gif 展示

    Image

    引入

    由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

    1. 在Project的 build.gradle 里面添加远程仓库
    allprojects {
    repositories {
    //...
    mavenCentral()
    }
    }
    1. 在Module的 build.gradle 里面添加引入依赖项
    //AndroidX 版本
    implementation 'com.github.jenly1314:retrofit-helper:1.0.1'

    RetrofitHelper引入的库(具体对应版本请查看 Versions

        compileOnly "androidx.appcompat:appcompat:$versions.appcompat"
    compileOnly "com.squareup.retrofit2:retrofit:$versions.retrofit"

    因为 RetrofitHelper 的依赖只在编译时有效,并未打入包中,所以您的项目中必须依赖上面列出相关库

    示例

    主要集成步骤代码示例

    Step.1 需使用JDK8编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    Step.2 通过RetrofitUrlManager初始化OkHttpClient,进行初始化配置

    //通过RetrofitHelper创建一个支持多个BaseUrl的 OkHttpClient
    //方式一
    val okHttpClient = RetrofitHelper.getInstance()
    .createClientBuilder()
    //...你自己的其他配置
    .build()
    //方式二
    val okHttpClient = RetrofitHelper.getInstance()
    .with(builder)
    //...你自己的其他配置
    .build()
    //完整示例
    val okHttpClient = RetrofitHelper.getInstance()
    .createClientBuilder()
    .addInterceptor(LogInterceptor())
    .build()
    val retrofit = Retrofit.Builder()
    .baseUrl(Constants.BASE_URL)
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create(Gson()))
    .build()

    Step.3 定义接口时,通过注解标记对应接口,支持动态改变 BaseUrl相关功能

     interface ApiService {

    /**
    * 接口示例,没添加任何标识,和常规使用一致
    * @return
    */
    @GET("api/user")
    fun getUser(): Call<User>


    /**
    * Retrofit默认返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
    * @return
    */
    @DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
    @Timeout(connectTimeout = 15,readTimeout = 15,writeTimeout = 15,timeUnit = TimeUnit.SECONDS) //超时标识,用于自定义超时时长
    @GET("api/user")
    fun getUser(): Call<User>

    /**
    * 动态改变 BaseUrl
    * @return
    */
    @BaseUrl(baseUrl) //baseUrl 标识,用于支持指定 BaseUrl
    @GET("api/user")
    fun getUser(): Call<User>


    //--------------------------------------

    /**
    * 使用RxJava返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
    * @return
    */
    @DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
    @Timeout(connectTimeout = 20,readTimeout = 10) //超时标识,用于自定义超时时长
    @GET("api/user")
    fun getUser(): Observable<User>

    }

    Step.4 添加多个 BaseUrl 支持

            //添加多个 BaseUrl 支持 ,domainName为域名别名标识,domainUrl为域名对应的 BaseUrl,与上面的接口定义表示一致即可生效
    RetrofitHelper.getInstance().putDomain(domainName,domainUrl)
            //添加多个 BaseUrl 支持 示例
    RetrofitHelper.getInstance().apply {
    //GitHub baseUrl
    putDomain(Constants.DOMAIN_GITHUB,Constants.GITHUB_BASE_URL)
    //Google baseUrl
    putDomain(Constants.DOMAIN_GOOGLE,Constants.GOOGLE_BASE_URL)
    }

    RetrofitHelper

    /**
    * Retrofit帮助类
    *


    * 主要功能介绍:
    * 1.支持管理多个 BaseUrl,且支持运行时动态改变
    * 2.支持接口自定义超时时长,满足每个接口动态定义超时时长
    * 3.支持添加公共请求头
    *


    *
    * RetrofitHelper中的核心方法
    *
    * {@link #createClientBuilder()} 创建 {@link OkHttpClient.Builder}初始化一些配置参数,用于支持多个 BaseUrl
    *
    * {@link #with(OkHttpClient.Builder)} 传入 {@link OkHttpClient.Builder} 配置一些参数,用于支持多个 BaseUrl
    *
    * {@link #setBaseUrl(String)} 和 {@link #setBaseUrl(HttpUrl)} 主要用于设置默认的 BaseUrl。
    *
    * {@link #putDomain(String, String)} 和 {@link #putDomain(String, HttpUrl)} 主要用于支持多个 BaseUrl,且支持 BaseUrl 动态改变。
    *
    * {@link #setDynamicDomain(boolean)} 设置是否支持 配置多个BaseUrl,且支持动态改变,一般会通过其他途径自动开启,此方法一般不会主动用到,只有在特殊场景下可能会有此需求,所以提供此方法主要用于提供更多种可能。
    *
    * {@link #setHttpUrlParser(HttpUrlParser)} 设置 HttpUrl解析器 , 当前默认采用的 {@link DomainParser} 实现类,你也可以自定义实现 {@link HttpUrlParser}
    *
    * {@link #setAddHeader(boolean)} 设置是否添加头,一般会通过{@link #addHeader(String, String)}相关方法自动开启,此方法一般不会主动用到,只有特殊场景下会有此需求,主要用于提供统一控制。
    *
    * {@link #addHeader(String, String)} 设置头,主要用于添加公共头消息。
    *
    * {@link #addHeaders(Map)} 设置头,主要用于设置公共头消息。
    *
    * 这里只是列出一些对外使用的核心方法,和相关的简单说明。如果想了解更多,可以查看对应的方法和详情。
    *
    *


    *
    * @author Jenly
    */
    public final class RetrofitHelper{
    //...
    }

    特别说明

            //通过setBaseUrl可以动态改变全局的 BaseUrl,优先级比putDomain(domainName,domainUrl)低,谨慎使用
    RetrofitHelper.getInstance().setBaseUrl(dynamicUrl)

    更多使用详情,请查看Demo中的源码使用示例或直接查看API帮助文档


    代码下载:RetrofitHelper.zip

    收起阅读 »

    LLDB调试利器及高级用法

    LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我...
    继续阅读 »

    LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我会把重点放在常用命令的使用方式和技巧上。

    一、 LLDB常用调试命令
    ❶ p、po及 image命令
    1、是打印对象,是打印对象的description,演示如下:


    2、p命令修改变量,演示如下:


    3、imagelookup -a用于寻找栈地址对应的代码位置,演示如下:


    3.1 从上图中我们可以看到当程序崩溃时并不能定位到指定的代码位置,使用image寻址命令可以定位到具体的崩溃位置在viewDidLoad方法中的第51行。


    3.2 这里说明为什么是程序的名称,因为LLDBDebug在编译后就是一个Macho的可执行文件,也可以理解为镜像文件,image并不是图像的意思,而是代表镜像。这里跟上我们自己的工程名,即用image定位寻址才是寻找我们自己的代码。
    ❷ bt及frame命令
    1、使用命令可以查看函数调用堆栈,然后用 命令即可查看对应函数详细,演示如下:


    1.1 上面函数执行的顺序如下:点击登录按钮--验证手机号--验证密码--开始登录。

    - (IBAction)login:(UIButton *)sender {

    [self validationPhone];
    }
    #pragma mark --验证手机号
    -(void)validationPhone{

    [self validationPwd];
    }
    #pragma mark --验证密码
    -(void)validationPwd{

    [self startLogin];
    }
    #pragma mark --开始登陆
    -(void)startLogin{

    NSLog(@"------开始登录...------");
    }

    1.2 从bt命令的打印信息中,我们可以很清楚看到函数调用顺序,如下图:


    1.3 接下来我们执行 frame select命令即可以查看函数相关信息,同时配合up和down命令追踪函数的调用和被调用关系,演示如下:


    1.4 同时可以使用frame variable很方便的查方法的调用者及方法名称,如下图:


    ❸ breakpoint命令
    1、b命令给函数下断点,演示如下图


    1.1 当我们的断点下成功后,控制台会打印如下信息:
    Breakpoint 1: where = LLDBDebug`-[ViewController login:] at ViewController.m:53, address = 0x00000001034fb0a0

    1.2 我们可以看到断点的位置在.m文件的53行,Breakpoint 1这里的1代表的是编号为1的组断点。
    使用 我们可以看到断点的数量,同时使用 后面跟上组号,即可删除,演示如下:


    3、\color{red}{breakpoint}的\color{red}{c},\color{red}{n},\color{red}{s}以及\color{red}{finish}命令,对应关系如下图:


    3.1 我们执行\color{red}{c},\color{red}{n},\color{red}{s}及\color{red}{finish}命令演示如下:


    ❹ breakpoint命令
    1.target stop-hook add -o "frame variable"每次进入断点都会自动打印详细的参数信息,演示如下:


    二、 LLDB高级用法
    ❶ 我们先来简单看下\color{red}{menthods}和\color{red}{pviews}命令的执行效果,演示如下图:


    1.1 \color{red}{menthods}命令可以打印当前对象的属性和方法,如下所示:

    (lldb) methods p1
    <Person: 0x60000003eac0>:
    in Person:
    Properties:
    @property (copy, nonatomic) NSString* name; (@synthesize name = _name;)
    @property (nonatomic) long age; (@synthesize age = _age;)
    Instance Methods:
    - (void) eat; (0x1098bf3e0)
    - (void) .cxx_destruct; (0x1098bf4f0)
    - (id) description; (0x1098bf410)
    - (id) name; (0x1098bf430)
    - (void) setName:(id)arg1; (0x1098bf460)
    - (void) setAge:(long)arg1; (0x1098bf4c0)
    - (long) age; (0x1098bf4a0)
    (NSObject ...)

    1.2 \color{red}{pviews}命令可以打印当前视图的层级结构,如下所示:

    (lldb) pviews
    <UIWindow: 0x7fd1719060a0; frame = (0 0; 414 736); gestureRecognizers = <NSArray: 0x60c000058660>; layer = <UIWindowLayer: 0x60c0000364c0>>
    | <UIView: 0x7fd16fc06d10; frame = (0 0; 414 736); alpha = 0.8; autoresize = W+H; layer = <CALayer: 0x60000003e7e0>>
    | | <UIButton: 0x7fd16fe0b520; frame = (54 316; 266 53); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x60400003b040>>
    | | | <UIButtonLabel: 0x7fd16fe023f0; frame = (117.667 17.6667; 30.6667 18); text = '登录'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60400008ac80>>
    | | | | <_UILabelContentLayer: 0x600000220260> (layer)
    | | <UILabel: 0x7fd16fc04a60; frame = (164 225; 80 47); text = 'Qinz'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000088fc0>>
    (lldb)

    1.3 如果你在原生的XCode中,是敲不出这些命令的,上面只是演示了两个常见的LLDB插件命令的用法,更加高级的用法下面会详细说明。不过在这之前,我们要安装两个插件,接下来先讲解环境的配置。
    ❷ LLDB插件配置:chisel及LLDB
    2.1 chisel是facebook开源的一款LLDB插件,里面封装了很多好用的命令,当然这些命令都是基于苹果提供的api。chisel下载
    2.2 这里建议使用包管理工具Homebrew来安装,然后配置脚本路径,演示如下:


    2.3 然后在lldb窗口执行命令,演示如下:


    2.4 看到输出"command script import /usr/local/opt/chisel/libexec/fblldb.py"即代表安装成功,这里还会看到一个"command script import /opt/LLDB/lldb_commands/dslldb.py
    "路径,这是我们接下来要安装的第二个插件

    Executing commands in '/Users/Qinz/.lldbinit'.
    command script import /usr/local/opt/chisel/libexec/fblldb.py
    command script import /opt/LLDB/lldb_commands/dslldb.py
    (lldb)

    2.5 这个插件的名称也叫LLDB,LLDB下载。我们先clone文件,我这里放置在opt文件夹下,你可以选择自己的文件目录放置,然后依次找到dslldb文件,在~/.initlldb文件中配置路径,演示如下:


    2.6 接下来依然在lldb窗口执行 command source ~/.lldbinit命令。到此LLDB插件的配置环境完成,接下来我们讲解这些插件的实用命令。
    ❸ lldb高级用法
    1. 搭配,让你快速找准控件,演示如下:


    1.1 taplog是点击控件,会打印控件的地址,大小及透明度等信息,我们拿到地址后执行flicker 0x7fd321e09710命令,此时控件会进行闪烁,这里动态图显示的闪烁效果明显。
    2. 和显示和隐藏控件,演示如下:


    \color{red}{vs}命令方便动态查看控件的层级关系,演示如下:


    3.1 当我们执行\color{red}{vs}命令后会进入动态调试阶段,会出现以下五个命令,每个命令我做了详细注释如下:

    (lldb) vs 0x7fe73550a090
    Use the following and (q) to quit.
    (w) move to superview //移动到父视图
    (s) move to first subview //移动到第一个子视图
    (a) move to previous sibling //移动上一个兄弟视图
    (d) move to next sibling //移动下一个兄弟视图
    (p) print the hierarchy //打印视图层级结构

    \color{red}{pactions}直接打印对象调用者及方法,演示如下:


    \color{red}{border}&\color{red}{unborder}给控件增加和去除边框,演示如下:


    5.1 这里的-c即是color,-w即设置边框的宽度。通过这个命令我们可以很方便的查看边框的边缘的问题,而不需要每次重启运行。
    6.\color{red}{pclass}打印对象的继承关系,演示如下图:


    \color{red}{presponder}命令打印响应链,演示如下图:


    \color{red}{caflush}这个命令会重新渲染,即可以重新绘制界面, 相当于执行了 [CATransaction flush] 方法,演示如下:


    \color{red}{search}搜索已经存在于栈中的控件及其子控件,演示如下:


    \color{red}{lookup}搜索,可执行正则表达式。演示如下:


    10.1 上面的搜索会搜索所用镜像模块,我们重点看与我们工程名字相同的模块,即可查看哪些地方调用了这些方法。
    11. \color{red}{pbundlepath}打印app路径及\color{red}{pdocspath}打印文档路径,演示如下:


    总结:上面详细讲解了LLDB常用命令及高级命令的用法,熟练掌握可大幅度提高Debug能力和开发效率。

    我是Qinz,希望我的文章对你有帮助。

    转自:https://www.jianshu.com/p/c91f843a64fc

    收起阅读 »

    UIViewController解耦---浅析Three20架构

    前言Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在...
    继续阅读 »

    前言
    Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在诟病Three20各个模块之间的耦合度太高,而且更多人在抱怨Three20极少的开发文档,我想这些大概也是Three20在苹果发布iOS6之后就停止了更新维护的原因吧。大神Joe Hewitt创建的在Github上的源码早已删除,目前只有少数人在GitHub上为自己的项目维护。而我也是有幸在某个项目中见识到了曾经耳闻,却未目睹的Three20框架,因此才有了这篇文章。

    架构
    最近大家都在讨论MVC、MVVM以及MVP三种在移动端开发中常用到的架构模式,究竟是哪种架构最强大,最适合移动开发者使用。这里笔者也阐述一下个人意见,有句方言叫“树挪死,人挪活”,个人认为,架构是死的,开发者是活的,我们不需要局限于哪一种架构的模式之下,看到大家都在用MVVM,于是花大成本将MVC架构模式的老项目重构成了MVVM架构,这种重构个人看来其实并没有意义。更多的架构话题就不想在这里讨论了,笔者推荐几篇大神们关于架构的见解。

    1、被误解的 MVC 和被神化的 MVVM
    这是一篇被早已被翻烂了的文章,起码我个人反复阅读了数次,由家喻户晓的唐巧大神编写。
    2、iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构
    最近在Cocoa China上发表的一篇译文,笔者之前看过俩次原文,讲的比较形象。
    3、MVC,MVP 和 MVVM 的图示
    大神阮一峰的博文,以图形展示的方式使得各层结构更加清晰明了。
    4、猿题库 iOS 客户端架构设计
    猿题库 iOS客户端开发者蓝晨钰的博文,以实际项目猿题库详解了架构设计

    UIViewController瘦身

    架构模式并不是限制思维,相反应该是发散思维,我们并不应该为了架构而架构,架构应该是服务于我们的代码逻辑,打造更具有扩展性和健壮的代码结构。就比如,大多数开发者都会遇到一个同样的问题,随着项目一天天的壮大,功能越来越多,需求越来越多,而我们的UIViewController也变得越来越臃肿。在上面推荐的博文中,笔者们都或多或少的阐述了如何打造更轻量级的UIViewController,大都列举了一些共性策略:

    1、将一个界面中的数据获取抽象成一个类,这里面细分一下,包括了网络请求和数据库缓存,我们可以针对这俩点再次封装成俩个类。
    2、将一个界面中的数据处理逻辑抽象成一个类,这里面包含了各种数据转换和算法逻辑,比如数据检索,数据遍历等。
    3、将一个界面中数据传递到UIView视图的过程抽象成一个模型类,这里面就包含了对应到UIView视图的每一个数据的传递,比如icon图标,title标题,comment评论内容等。
    4、将一个界面中所有展示的UIView视图的添加和渲染抽象成一个类,这里包含了添加控件,自定义动画等。这个对视图的封装仍然可以细分,每一个自定义控件都可以单独封装,因为这样可以完美的在其他的UIViewController达到复用的目的。

    而完成了上述抽象之后,就会发现我们需要在UIViewController中完成的工作仅仅是处理视图交互逻辑和数据传递逻辑,这样我们的UIViewController就比较容易维护了。

    Three20架构
    每一种框架的兴起和衰落都有其相应的时势和必然性。虽然Three20饱受诟病,早已跌落神坛,但是它的存在是有一定道理的。虽然它在模块之间的耦合度较高,但是个人认为它对UIViewController的抽象和封装也是一个非常好的借鉴。在这里以Three20中对TTTableViewController的解耦为例,先上图看一下TTTableViewController包含的模块:


    这里根据上面的结构图具体地解释一下解耦的设计方式。TTTableViewController的设计遵从了经典的MVC模式,TTModel负责数据的获取和处理逻辑,TTTableView负责视图展示,TTTableViewController负责TTModel与TTTableView之间的通信逻辑和界面的控件添加渲染。而TTTableViewController在顺应了MVC模式的前提下,也做了一些扩展,它将TTTableViewDatasource接收数据传递的逻辑抽象出来封装成了TTTableItem。而TTTableItem就是关联TTModel传递数据的过程,因而我们也可以把这一层称作是MVVM架构模式中的ViewModel

    根据上面的图示,我们可以看到获取数据的逻辑都在TTModel中,而且界面控件添加和动画渲染这些逻辑仍然都在TTTableViewController中,因此我根据大神们的一些建议,对项目中的Three20进行了一下强化,先上图看一下增加的结构:


    可以清晰地看到,我将TTModel中处理缓存数据的逻辑抽象出来,单独放在了TTCacheModel中,此外还将TTTableViewController中添加控件和渲染动画的逻辑抽象出来,放到了TTViewRender中,这样TTTableViewController就只关心界面交互以及TTModel和TTTableItem之间的数据传递逻辑。

    链接:https://www.jianshu.com/p/a748f6fd99dd

    收起阅读 »

    iOS RESideMenu 侧滑 第三方类库

    下载地址:https://github.com/romaonthego/RESideMenu效果如下:官方案例自己的实现效果具体代码下:AppDelegate.m文件中- (BOOL)application:(UIApplication *)applicati...
    继续阅读 »

    下载地址:https://github.com/romaonthego/RESideMenu
    效果如下:官方案例


    自己的实现效果


    具体代码下:

    AppDelegate.m文件中

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法

    DEMOLeftMenuViewController *leftMenuViewController = [[DEMOLeftMenuViewController alloc] init];

    RESideMenu *sideMenuViewController = [[RESideMenu alloc] initWithContentViewController:[[MainTabBarController alloc]init] leftMenuViewController:leftMenuViewController rightMenuViewController:[UINavigationController new]];
    sideMenuViewController.backgroundImage = [UIImage imageNamed:@"005.jpg"];
    sideMenuViewController.menuPreferredStatusBarStyle = 1; // UIStatusBarStyleLightContent
    sideMenuViewController.delegate = self;
    // sideMenuViewController.parallaxContentMaximumRelativeValue=100;
    // sideMenuViewController.bouncesHorizontally=YES;
    sideMenuViewController.contentViewShadowColor = [UIColor blackColor];
    sideMenuViewController.contentViewShadowOffset = CGSizeMake(0, 0);
    sideMenuViewController.contentViewShadowOpacity = 0.6;
    sideMenuViewController.contentViewShadowRadius = 12;
    // sideMenuViewController.contentViewShadowEnabled = YES;
    // sideMenuViewController.panFromEdge=NO;
    self.window.rootViewController = sideMenuViewController;

    左侧的控制器DEMOLeftMenuViewController.h和DEMOLeftMenuViewController.m

    #import <UIKit/UIKit.h>
    #import "RESideMenu.h"

    @interface DEMOLeftMenuViewController : UIViewController<UITableViewDataSource, UITableViewDelegate, RESideMenuDelegate>


    @end
    #import "DEMOLeftMenuViewController.h"
    #import "HomeViewController.h"
    #import "UIViewController+RESideMenu.h"
    #import "LoginViewController.h"
    #import "resigeViewController.h"

    @interface DEMOLeftMenuViewController ()
    @property (strong, readwrite, nonatomic) UITableView *tableView;

    @end

    @implementation DEMOLeftMenuViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    self.navigationController.title=@"登陆";
    self.tableView = ({
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, (self.view.frame.size.height - 54 * 5) / 2.0f, self.view.frame.size.width, 54 * 5) style:UITableViewStylePlain];
    tableView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth;
    tableView.delegate = self;
    tableView.dataSource = self;
    tableView.opaque = NO;
    tableView.backgroundColor = [UIColor clearColor];
    tableView.backgroundView = nil;
    tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    tableView.bounces = NO;
    tableView.scrollsToTop = NO;
    tableView;
    });
    [self.view addSubview:self.tableView];
    }

    #pragma mark -
    #pragma mark UITableView Delegate

    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    switch (indexPath.row) {
    case 0:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[LoginViewController alloc] init]] animated:YES completion:nil];
    break;
    case 1:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[resigeViewController alloc] init]] animated:YES completion:nil];
    break;
    default:
    break;
    }
    }

    #pragma mark -
    #pragma mark UITableView Datasource

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    return 54;
    }

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    return 1;
    }

    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)sectionIndex
    {
    return 5;
    }

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *cellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    cell.backgroundColor = [UIColor clearColor];
    cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:21];
    cell.textLabel.textColor = [UIColor whiteColor];
    cell.textLabel.highlightedTextColor = [UIColor lightGrayColor];
    cell.selectedBackgroundView = [[UIView alloc] init];
    }

    NSArray *titles = @[@"Home", @"Calendar", @"Profile", @"Settings", @"Log Out"];
    NSArray *images = @[@"IconHome", @"IconCalendar", @"IconProfile", @"IconSettings", @"IconEmpty"];
    cell.textLabel.text = titles[indexPath.row];
    cell.imageView.image = [UIImage imageNamed:images[indexPath.row]];

    return cell;
    }


    @end

    主页HomeViewController.h和HomeViewController.m实现侧滑的关键代码

    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"个人中心"
    style:UIBarButtonItemStylePlain
    target:self
    action:@selector(presentLeftMenuViewControl

    这个第三番可以实现很多效果

    总结

    优点:

    1.里面的文件较少,不需要使用cocoapods即可运行。

    2.里面自定义API也比较多,可以设置变小的抽屉效果或者不变小。

    3.里面有两个事例程序,一个是纯手码,一个是Storyboard得。可见作者也非常喜欢IB开发,此框架用IB开发应该可以完美兼容。

    4.可以使用手势拖来拖去。

    5.项目里各个文件不需要继承,导入头文件就行。

    缺点:

    1.左边显示的菜单可选项是固定的几个button,暂时想把左边换成tableView还不知道可不可行。

    2.不能实现状态栏右移。

    3.暂时没找到两边控制器的占比怎么自定义。

    转自:https://www.cnblogs.com/qianLL/p/5425738.html

    收起阅读 »

    PNChart:一个简单漂亮的iOS图表库

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。要求PNChart 适用...
    继续阅读 »

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。

    要求

    PNChart 适用于 iOS 7.0 或更高版本,与 ARC 项目兼容。如果需要支持 iOS 6 ,请使用 0.8.1 版本之前的 PNChart 。注意 0.8.2 版本仅支持 iOS 8.0+ ,0.8.3 及更新版本支持 iOS 7.0+ 。

    PNChart 依赖于下列框架,这些框架已经嵌入了 Xcode 开发工具:

    Foundation.framework

    UIKit.framework

    CoreGraphics.framework

    QuartzCore.framework

    你需要 LLVM 3.0 或更高版本来建立 PNChart 。

    安装

    通过CocoaPods安装(推荐):

    1、在你的 Podfile 文件中添加pod 'PNChart'。

    2、运行pod install进行安装。

    3、按需导入头文件#import "PNChart.h"。

    手动安装:

    拷贝PNChart文件夹到你的工程中。

    使用

    #import "PNChart.h"

    //For Line Chart

    PNLineChart*lineChart=[[PNLineChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [lineChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    // Line Chart No.1

    NSArray*data01Array=@[@60.1,@160.1,@126.4,@262.2,@186.2];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=lineChart.xLabels.count;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart No.2

    NSArray*data02Array=@[@20.1,@180.1,@26.4,@202.2,@126.2];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=lineChart.xLabels.count;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    lineChart.chartData=@[data01,data02];

    [lineChartstrokeChart];
    #import "PNChart.h"

    //For BarC hart

    PNBarChart*barChart=[[PNBarChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [barChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    [barChartsetYValues:@[@1,@10,@2,@6,@3]];

    [barChartstrokeChart];



    ``` Objective-C

    #import "PNChart.h"

    //For Circle Chart

    PNCircleChart*circleChart=[[PNCircleChartalloc]initWithFrame:CGRectMake(0,80.0,SCREEN_WIDTH,100.0)total:[NSNumbernumberWithInt:100]current:[NSNumbernumberWithInt:60]clockwise:NOshadow:NO];

    circleChart.backgroundColor=[UIColorclearColor];

    [circleChartsetStrokeColor:PNGreen];

    [circleChart strokeChart];



    ```Objective-C

    # import "PNChart.h"

    //For Pie Chart

    NSArray*items=@[[PNPieChartDataItemdataItemWithValue:10color:PNRed],

    [PNPieChartDataItemdataItemWithValue:20color:PNBluedescription:@"WWDC"],

    [PNPieChartDataItemdataItemWithValue:40color:PNGreendescription:@"GOOL I/O"],

    ];

    PNPieChart*pieChart=[[PNPieChartalloc]initWithFrame:CGRectMake(40.0,155.0,240.0,240.0)items:items];

    pieChart.descriptionTextColor=[UIColorwhiteColor];

    pieChart.descriptionTextFont=[UIFontfontWithName:@"Avenir-Medium"size:14.0];

    [pieChartstrokeChart];
    # import "PNChart.h"

    //For Scatter Chart

    PNScatterChart*scatterChart=[[PNScatterChartalloc]initWithFrame:CGRectMake(SCREEN_WIDTH/6.0-30,135,280,200)];

    [scatterChartsetAxisXWithMinimumValue:20andMaxValue:100toTicks:6];

    [scatterChartsetAxisYWithMinimumValue:30andMaxValue:50toTicks:5];

    NSArray*data01Array=[selfrandomSetOfObjects];

    PNScatterChartData*data01=[PNScatterChartDatanew];

    data01.strokeColor=PNGreen;

    data01.fillColor=PNFreshGreen;

    data01.size=2;

    data01.itemCount=[[data01ArrayobjectAtIndex:0]count];

    data01.inflexionPointStyle=PNScatterChartPointStyleCircle;

    __blockNSMutableArray*XAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:0]];

    __blockNSMutableArray*YAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:1]];

    data01.getData=^(NSUIntegerindex){

    CGFloatxValue=[[XAr1objectAtIndex:index]floatValue];

    CGFloatyValue=[[YAr1objectAtIndex:index]floatValue];

    return[PNScatterChartDataItemdataItemWithX:xValueAndWithY:yValue];

    };

    [scatterChartsetup];

    self.scatterChart.chartData=@[data01];

    /***

    this is for drawing line to compare

    CGPoint start = CGPointMake(20, 35);

    CGPoint end = CGPointMake(80, 45);

    [scatterChart drawLineFromPoint:start ToPoint:end WithLineWith:2 AndWithColor:PNBlack];

    ***/

    scatterChart.delegate=self;

    图例

    PNChart 允许在折线图和饼状图中添加图例,图例可以竖向堆叠布置或者横向并列布置。

    #import "PNChart.h"

    //For Line Chart

    //Add Line Titles for the Legend

    data01.dataTitle=@"Alpha";

    data02.dataTitle=@"Beta Beta Beta Beta";

    //Build the legend

    self.lineChart.legendStyle=PNLegendItemStyleSerial;

    self.lineChart.legendFontSize=12.0;

    UIView*legend=[self.lineChartgetLegendWithMaxWidth:320];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(100,400,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    //For Pie Chart

    //Build the legend

    self.pieChart.legendStyle=PNLegendItemStyleStacked;

    self.pieChart.legendFontSize=12.0;

    UIView*legend=[self.pieChartgetLegendWithMaxWidth:200];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(130,350,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    更新数据

    实时更新数据也非常简单。

    Objective-C

    if([self.titleisEqualToString:@"Line Chart"]){

    // Line Chart #1

    NSArray*data01Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=data01Array.count;

    data01.inflexionPointStyle=PNLineChartPointStyleTriangle;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart #2

    NSArray*data02Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=data02Array.count;

    data02.inflexionPointStyle=PNLineChartPointStyleSquare;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    [self.lineChartsetXLabels:@[@"DEC 1",@"DEC 2",@"DEC 3",@"DEC 4",@"DEC 5",@"DEC 6",@"DEC 7"]];

    [self.lineChartupdateChartData:@[data01,data02]];

    }

    elseif([self.titleisEqualToString:@"Bar Chart"])

    {

    [self.barChartsetXLabels:@[@"Jan 1",@"Jan 2",@"Jan 3",@"Jan 4",@"Jan 5",@"Jan 6",@"Jan 7"]];

    [self.barChartupdateChartData:@[@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30)]];

    }

    elseif([self.titleisEqualToString:@"Circle Chart"])

    {

    [self.circleChartupdateChartByCurrent:@(arc4random()0)];

    }

    代理回调

    Objective-C

    #import "PNChart.h"

    //For LineChart

    lineChart.delegate=self;

    动画

    默认绘制图表时使用动画,可以通过设置displayAnimation = NO来禁止动画。

    Objective-C


    #import "PNChart.h"

    //For LineChart

    lineChart.displayAnimation=NO;

    ```Objective-C



    //For DelegateMethod

    -(void)userClickedOnLineKeyPoint:(CGPoint)pointlineIndex:(NSInteger)lineIndexpointIndex:(NSInteger)pointIndex{

    NSLog(@"Click Key on line %f, %f line index is %d and point index is %d",point.x,point.y,(int)lineIndex,(int)pointIndex);

    }

    -(void)userClickedOnLinePoint:(CGPoint)pointlineIndex:(NSInteger)lineIndex{

    NSLog(@"Click on line %f, %f, line index is %d",point.x,point.y,(int)lineIndex);

    }

    开源协议

    PNChart 在MIT开源协议下可以使用,也就是说,只要在项目副本中包含了版权声明和许可声明,用户就可以使用 PNChart 做任何想做的事情,而 PNChart 也无需承担任何责任。可以通过查看 LICENSE 文件来获取更多相关信息。

    开源地址:https://github.com/kevinzhow/PNChart

    链接:https://www.jianshu.com/p/9c162d6f8f14

    收起阅读 »

    Android原生绘图进度条+简单自定义属性代码生成器

    先一下效果:一、简单自定义属性生成器1.玩安卓的应该都写过自定义控件的自定义属性:如下我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?2.通过attrs.xml自动生成相应代码秉承着能用代码解决的问题,绝对不动手。能够靠...
    继续阅读 »

    先一下效果:

    圆形进度条.gif

    横向进度条.gif

    一、简单自定义属性生成器

    1.玩安卓的应该都写过自定义控件的自定义属性:如下

    自定义控件.png

    我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?

    2.通过attrs.xml自动生成相应代码

    秉承着能用代码解决的问题,绝对不动手。能够靠智商解决的问题,绝对不靠体力的大无畏精神:

    写了一个小工具,将代码里的内容自动生成一下:基本上就是字符串的切割和拼装,工具附在文尾

    使用方法与注意点:

    1.拷贝到AndroidStudio的test里,将attrs.xml的文件路径设置一下,运行
    2.自定义必须符合命名规则,如z_pb_on_height,专属前缀如z_,单词间下划线连接即可
    3.它并不是什么高大上的东西,只是简单的字符串切割拼组,只适用简单的自定义属性[dimension|color|boolean|string](不过一般的自定义属性也够用了)

    自动生成.png

    在开篇之前:先看一下Android系统内自定义控件的书写风格,毕竟跟原生看齐没有什么坏处

    看一下LinearLayout的源码:

    1.构造方法使用最多参数的那个,其他用this(XXX)调用

     public LinearLayout(Context context) {
    this(context, null);
    }

    public LinearLayout(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
    }

    public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    ...
    }

    2.自定义属性的书写

    1).先将自定义属性的成员变量定义好

    2).如果自定义属性不是很多,一个一个a.getXXX,默认值直接写在后面就行了
    3).看了一下TextView的源码,自定义属性很多,它是先定义默认值的变量,再使用,而且用switch来对a.getXXX进行赋值

    final TypedArray a = context.obtainStyledAttributes(
    attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);

    int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
    if (index >= 0) {
    setOrientation(index);
    }

    index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
    if (index >= 0) {
    setGravity(index);
    }

    boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
    if (!baselineAligned) {
    setBaselineAligned(baselineAligned);
    }
    ......
    a.recycle();

    一、水平的进度条

    条形进度条分析.png

    1.自定义控件属性:values/attrs.xml

        






















    2.初始代码:将进行一些常规处理

    public class TolyProgressBar extends ProgressBar {

    private Paint mPaint;
    private int mPBWidth;
    private RectF mRectF;
    private Path mPath;
    private float[] mFloat8Left;//左边圆角数组
    private float[] mFloat8Right;//右边圆角数组

    private float mProgressX;//进度理论值
    private float mEndX;//进度条尾部
    private int mTextWidth;//文字宽度
    private boolean mLostRight;//是否不画右边
    private String mText;//文字

    private int mPbBgColor = 0xffC9C9C9;
    private int mPbOnColor = 0xff54F340;
    private int mPbOnHeight = dp(6);
    private int mPbBgHeight = dp(6);
    private int mPbTxtColor = 0xff525252;
    private int mPbTxtSize = sp(10);
    private int mPbTxtOffset = sp(10);
    private boolean mPbTxtGone= false;

    public TolyProgressBar(Context context) {
    this(context, null);
    }

    public TolyProgressBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TolyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyProgressBar);
    mPbOnHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_on_height, mPbOnHeight);
    mPbTxtOffset = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_offset, mPbTxtOffset);
    mPbOnColor = a.getColor(R.styleable.TolyProgressBar_z_pb_on_color, mPbOnColor);
    mPbTxtSize = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_size, mPbTxtSize);
    mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
    mPbBgColor = a.getColor(R.styleable.TolyProgressBar_z_pb_bg_color, mPbBgColor);
    mPbTxtGone = a.getBoolean(R.styleable.TolyProgressBar_z_pb_txt_gone, mPbTxtGone);
    a.recycle();

    init();
    }

    private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setTextSize(mPbTxtSize);
    mPaint.setColor(mPbOnColor);
    mPaint.setStrokeWidth(mPbOnHeight);

    mRectF = new RectF();
    mPath = new Path();


    mFloat8Left = new float[]{//仅左边两个圆角--为背景
    mPbOnHeight / 2, mPbOnHeight / 2,//左上圆角x,y
    0, 0,//右上圆角x,y
    0, 0,//右下圆角x,y
    mPbOnHeight / 2, mPbOnHeight / 2//左下圆角x,y
    };

    mFloat8Right = new float[]{
    0, 0,//左上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
    0, 0//左下圆角x,y
    };
    }

    }

    private int sp(int sp) {
    return (int) TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    private int dp(int dp) {
    return (int) TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }


    2.测量:

        @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = measureHeight(heightMeasureSpec);
    setMeasuredDimension(width, height);
    mPBWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//进度条实际宽度
    }

        /**
    * 测量高度
    *
    * @param heightMeasureSpec
    * @return
    */

    private int measureHeight(int heightMeasureSpec) {
    int result = 0;
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    int size = MeasureSpec.getSize(heightMeasureSpec);

    if (mode == MeasureSpec.EXACTLY) {
    //控件尺寸已经确定:如:
    // android:layout_height="40dp"或"match_parent"
    result = size;
    } else {
    int textHeight = (int) (mPaint.descent() - mPaint.ascent());
    result = getPaddingTop() + getPaddingBottom() + Math.max(
    Math.max(mPbBgHeight, mPbOnHeight), Math.abs(textHeight));

    if (mode == MeasureSpec.AT_MOST) {//最多不超过
    result = Math.min(result, size);
    }
    }
    return result;
    }
    复制代码

    3.绘制:

        @Override
    protected synchronized void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.translate(getPaddingLeft(), getHeight() / 2);

    parseBeforeDraw();//1.绘制前对数值进行计算以及控制的flag设置

    if (getProgress() == 100) {//进度达到100后文字消失
    whenOver();//2.
    }
    if (mEndX > 0) {//当进度条尾部>0绘制
    drawProgress(canvas);//3.
    }
    if (!mPbTxtGone) {//绘制文字
    mPaint.setColor(mPbTxtColor);
    int y = (int) (-(mPaint.descent() + mPaint.ascent()) / 2);
    canvas.drawText(mText, mProgressX, y, mPaint);
    } else {
    mTextWidth = 0 - mPbTxtOffset;
    }
    if (!mLostRight) {//绘制右侧
    drawRight(canvas);/4.
    }

    canvas.restore();
    }

    1).praseBeforeDraw()

    /**
    * 对数值进行计算以及控制的flag设置
    */

    private void parseBeforeDraw() {
    mLostRight = false;//lostRight控制是否绘制右侧
    float radio = getProgress() * 1.f / getMax();//当前百分比率
    mProgressX = radio * mPBWidth;//进度条当前长度
    mEndX = mProgressX - mPbTxtOffset / 2; //进度条当前长度-文字间隔的左半
    mText = getProgress() + "%";
    if (mProgressX + mTextWidth > mPBWidth) {
    mProgressX = mPBWidth - mTextWidth;
    mLostRight = true;
    }
    //文字宽度
    mTextWidth = (int) mPaint.measureText(mText);
    }

    2).whenOver()

    /**
    * 当结束是执行:
    */

    private void whenOver() {
    mPbTxtGone = true;
    mFloat8Left = new float[]{//只有进度达到100时让进度圆角是四个
    mPbBgHeight / 2, mPbBgHeight / 2,//左上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2//左下圆角x,y
    };
    }

    3).drawProgress()

    /**
    * 绘制左侧:(进度条)
    *
    * @param canvas
    */

    private void drawProgress(Canvas canvas) {
    mPath.reset();
    mRectF.set(0, mPbOnHeight / 2, mEndX, -mPbOnHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Left, Path.Direction.CW);//顺时针画
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPbOnColor);
    canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
    }

    4).drawRight()

    /**
    * 绘制左侧:(背景)
    *
    * @param canvas
    */

    private void drawRight(Canvas canvas) {
    float start = mProgressX + mPbTxtOffset / 2 + mTextWidth;
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeWidth(mPbBgHeight);
    mPath.reset();
    mRectF.set(start, mPbBgHeight / 2, mPBWidth, -mPbBgHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Right, Path.Direction.CW);//顺时针画
    canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
    }

    xml里使用:


    三、圆形进度条
    1.自定义属性





    2.代码实现:

    /**
    * 作者:张风捷特烈


    * 时间:2018/11/9 0009:11:49


    * 邮箱:1981462002@qq.com


    * 说明:圆形进度条
    */

    public class TolyRoundProgressBar extends TolyProgressBar {

    private int mPbRadius = dp(30);//进度条半径
    private int mMaxPaintWidth;

    public TolyRoundProgressBar(Context context) {
    this(context, null);
    }

    public TolyRoundProgressBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TolyRoundProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyRoundProgressBar);
    mPbRadius = (int) a.getDimension(R.styleable.TolyRoundProgressBar_z_pb_radius, mPbRadius);
    mPbOnHeight = (int) (mPbBgHeight * 1.8f);//让进度大一点
    a.recycle();

    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setDither(true);
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    mMaxPaintWidth = Math.max(mPbBgHeight, mPbOnHeight);
    int expect = mPbRadius * 2 + mMaxPaintWidth + getPaddingLeft() + getPaddingRight();
    int width = resolveSize(expect, widthMeasureSpec);
    int height = resolveSize(expect, heightMeasureSpec);
    int realWidth = Math.min(width, height);
    mPaint.setStrokeCap(Paint.Cap.ROUND);

    mPbRadius = (realWidth - getPaddingLeft() - getPaddingRight() - mMaxPaintWidth) / 2;
    setMeasuredDimension(realWidth, realWidth);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {

    String txt = getProgress() + "%";
    float txtWidth = mPaint.measureText(txt);
    float txtHeight = (mPaint.descent() + mPaint.ascent()) / 2;
    canvas.save();
    canvas.translate(getPaddingLeft() + mMaxPaintWidth / 2, getPaddingTop() + mMaxPaintWidth / 2);
    drawDot(canvas);
    mPaint.setStyle(Paint.Style.STROKE);
    //背景
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeWidth(mPbBgHeight);
    canvas.drawCircle(mPbRadius, mPbRadius, mPbRadius, mPaint);
    //进度条
    mPaint.setColor(mPbOnColor);
    mPaint.setStrokeWidth(mPbOnHeight);
    float sweepAngle = getProgress() * 1.0f / getMax() * 360;//完成角度
    canvas.drawArc(
    0, 0, mPbRadius * 2, mPbRadius * 2,
    -90, sweepAngle, false, mPaint);
    //文字
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPbTxtColor);
    canvas.drawText(txt, mPbRadius - txtWidth / 2, mPbRadius - txtHeight / 2, mPaint);
    canvas.restore();
    }

    /**
    * 绘制一圈点
    *
    * @param canvas
    */

    private void drawDot(Canvas canvas) {
    canvas.save();
    int num = 40;
    canvas.translate(mPbRadius, mPbRadius);
    for (int i = 0; i < num; i++) {
    canvas.save();
    int deg = 360 / num * i;
    canvas.rotate(deg);
    mPaint.setStrokeWidth(dp(3));
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    if (i * (360 / num) < getProgress() * 1.f / getMax() * 360) {
    mPaint.setColor(mPbOnColor);
    }
    canvas.drawLine(0, mPbRadius * 3 / 4, 0, mPbRadius * 4 / 5, mPaint);
    canvas.restore();
    }
    canvas.restore();
    }
    }




    附录:简单自定义属性生成器

    public class Attrs2Code {
    @Test
    public void main() {
    File file = new File("C:\\Users\\Administrator\\Desktop\\attrs.xml");
    initAttr("z_", file);
    }

    public static void initAttr(String preFix, File file) {
    HashMap format = format(preFix, file);
    String className = format.get("className");
    String result = format.get("result");
    StringBuilder sb = new StringBuilder();
    sb.append("TypedArray a = context.obtainStyledAttributes(attrs, R.styleable." + className + ");\r\n");
    format.forEach((s, s2) -> {
    String styleableName = className + "_" + preFix + s;
    if (s.contains("_")) {
    String[] partStrArray = s.split("_");
    s = "";
    for (String part : partStrArray) {
    String partStr = upAChar(part);
    s += partStr;
    }
    }
    if (s2.equals("dimension")) {
    // mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
    sb.append("m" + s + " = (int) a.getDimension(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("color")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getColor(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("boolean")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getBoolean(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("string")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getString(R.styleable." + styleableName + ");\r\n");
    }
    });
    sb.append("a.recycle();\r\n");
    System.out.println(result);
    System.out.println(sb.toString());
    }

    /**
    * 读取文件+解析
    *
    * @param preFix 前缀
    * @param file 文件路径
    */

    public static HashMap format(String preFix, File file) {
    HashMap container = new HashMap<>();
    if (!file.exists() && file.isDirectory()) {
    return null;
    }
    FileReader fr = null;
    try {
    fr = new FileReader(file);
    //字符数组循环读取
    char[] buf = new char[1024];
    int len = 0;
    StringBuilder sb = new StringBuilder();
    while ((len = fr.read(buf)) != -1) {
    sb.append(new String(buf, 0, len));
    }
    String className = sb.toString().split(""));
    container.put("className", className);
    String[] split = sb.toString().split("<");
    String part1 = "private";
    String type = "";//类型
    String name = "";
    String result = "";
    String def = "";//默认值

    StringBuilder sb2 = new StringBuilder();
    for (String s : split) {
    if (s.contains(preFix)) {
    result = s.split(preFix)[1];
    name = result.substring(0, result.indexOf("\""));
    type = result.split("format=\"")[1];
    type = type.substring(0, type.indexOf("\""));
    container.put(name, type);
    if (type.contains("color") || type.contains("dimension") || type.contains("integer")) {
    type = "int";
    def = "0";
    }
    if (result.contains("fraction")) {
    type = "float";
    def = "0.f";
    }
    if (result.contains("string")) {
    type = "String";
    def = "\"toly\"";
    }
    if (result.contains("boolean")) {
    type = "boolean";
    def = "false";

    }
    if (name.contains("_")) {
    String[] partStrArray = name.split("_");
    name = "";
    for (String part : partStrArray) {
    String partStr = upAChar(part);
    name += partStr;
    }
    sb2.append(part1 + " " + type + " m" + name + "= " + def + ";\r\n");
    }
    container.put("result", sb2.toString());
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    try {
    if (fr != null) {
    fr.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    return container;
    }

    /**
    * 将字符串仅首字母大写
    *
    * @param str 待处理字符串
    * @return 将字符串仅首字母大写
    */

    public static String upAChar(String str) {
    String a = str.substring(0, 1);
    String tail = str.substring(1);
    return a.toUpperCase() + tail;
    }
    }

    代码下载:bobing107-IPhoneWatch_progressbar-master.zip

    收起阅读 »

    一个Android强大的饼状图

    一、思路 1、空心图(一个大圆中心绘制一个小圆) 2、根据数据算出所占的角度 3、根据动画获取当前绘制的角度 4、根据当前角度获取Paint使用的颜色 5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要) 二、实现 1、空心图(一个大...
    继续阅读 »

    一、思路


      1、空心图(一个大圆中心绘制一个小圆)
    2、根据数据算出所占的角度
    3、根据动画获取当前绘制的角度
    4、根据当前角度获取Paint使用的颜色
    5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要)


    二、实现


    1、空心图(一个大圆中心绘制一个小圆)初始化数据


          paint = new Paint();
    paint.setAntiAlias(true);
    paint.setStyle(Paint.Style.FILL_AND_STROKE);

    screenW = DensityUtils.getScreenWidth(context);

    int width = DensityUtils.dip2px(context, 15);//圆环宽度
    int widthXY = DensityUtils.dip2px(context, 10);//微调距离

    int pieCenterX = screenW / 2;//饼状图中心X
    int pieCenterY = screenW / 3;//饼状图中心Y
    int pieRadius = screenW / 4;// 大圆半径

    //整个饼状图rect
    pieOval = new RectF();
    pieOval.left = pieCenterX - pieRadius;
    pieOval.top = pieCenterY - pieRadius + widthXY;
    pieOval.right = pieCenterX + pieRadius;
    pieOval.bottom = pieCenterY + pieRadius + widthXY;

    //里面的空白rect
    pieOvalIn = new RectF();
    pieOvalIn.left = pieOval.left + width;
    pieOvalIn.top = pieOval.top + width;
    pieOvalIn.right = pieOval.right - width;
    pieOvalIn.bottom = pieOval.bottom - width;

    //里面的空白画笔
    piePaintIn = new Paint();
    piePaintIn.setAntiAlias(true);
    piePaintIn.setStyle(Paint.Style.FILL);
    piePaintIn.setColor(Color.parseColor("#f4f4f4"));

    2、根据数据算出所占的角度


    使用递归保证cakeValues的值的总和必为100,然后根据值求出角度


       private void settleCakeValues(int i) {
    float sum = getSum(cakeValues, i);
    CakeValue value = cakeValues.get(i);
    if (sum <= 100f) {
    value.setItemValue(100f - sum);
    cakeValues.set(i, value);
    } else {
    value.setItemValue(0);
    settleCakeValues(i - 1);
    }
    }
    复制代码

    3、根据动画获取当前绘制的角度


    curAngle就是当前绘制的角度,drawArc()就是绘制的方法


    cakeValueAnimator.addUpdateListener(new AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    float mAngle = obj2Float(animation.getAnimatedValue("angle"));
    curAngle = mAngle;
    drawArc();
    }
    });

    4、根据当前角度获取Paint使用的颜色


    根据当前的角度,计算当前是第几个item,通过
    paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
    来设置paint的颜色
    复制代码

    private int getCurItem(float curAngle) {
    int res = 0;
    for (int i = 0; i < itemFrame.length; i++) {
    if (curAngle <= itemFrame[i] * ANGLE_NUM) {
    res = i;
    break;
    }
    }
    return res;
    }

    5、动态绘制即将绘制的 和 绘制已经绘制的部分


    最重要的一步,我的需求是4类,用不同的颜色




    绘制当前颜色的扇形,curStartAngle扇形的起始位置,curSweepAngle扇形的终止位置


      paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
    float curStartAngle = 0;
    float curSweepAngle = curAngle;
    if (curItem > 0) {
    curStartAngle = itemFrame[curItem - 1] * ANGLE_NUM;
    curSweepAngle = curAngle - (itemFrame[curItem - 1] * ANGLE_NUM);
    }
    canvas.drawArc(pieOval, curStartAngle, curSweepAngle, true, paint);

    绘制已经绘制的扇形。根据curItem判断绘制过得扇形


    for (int i = 0; i < curItem; i++) {
    paint.setColor(Color.parseColor(cakeValues.get(i).getColors()));
    if (i == 0) {
    canvas.drawArc(pieOval, startAngle,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
    continue;
    }
    canvas.drawArc(pieOval,itemFrame[i - 1] * ANGLE_NUM,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
    }


    绘制中心的圆


     canvas.drawArc(pieOvalIn, 0, 360, true, piePaintIn);

    6、特别注意


    isFirst判断是够是第一次绘制(绘制完成后,home键进入后台,再次进入,不需要动态绘制)


     @Override
    protected void onDraw(Canvas canvas) {
    if (isFirst && isDrawByAnim) {
    drawCakeByAnim();
    }
    isFirst = false;
    }
    复制代码
    isDrawByAnim判断是否需要动画绘制
    drawCake()为静态绘制饼状图


    public void surfaceCreated(SurfaceHolder holder) {
    if (!isFirst||!isDrawByAnim)
    drawCake();
    }

    更新


    增加立体效果,提取配置参数


    <declare-styleable name="CakeSurfaceView">
    <attr name="isDrawByAnim" format="boolean"/>//是否动画
    <attr name="isSolid" format="boolean"/>//是否立体
    <attr name="duration" format="integer|reference"/>//动画时间
    <attr name="defaultColor" format="string"/>//默认颜色

    <attr name="ringWidth" format="integer|reference"/>//圆环宽度
    <attr name="solidWidth" format="integer|reference"/>//立体宽度
    <attr name="fineTuningWidth" format="integer|reference"/>//微调宽度
    </declare-styleable>
    复制代码
    xml中使用
    复制代码

    <com.xp.xppiechart.view.CakeSurfaceView
    android:id="@+id/assets_pie_chart"
    android:background="#ffffff"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:defaultColor="#ff8712"
    app:ringWidth="20"
    app:solidWidth="5"
    app:duration="3000"
    app:isSolid="true"
    app:isDrawByAnim="true"/>
    复制代码


    以上就是简单的实现动态绘制饼状图,待完善,以后会更新。如有建议和意见,请及时沟通。


    代码下载:bobing107-CircularSectorProgressBar-master.zip

    收起阅读 »

    Android商品属性筛选与商品筛选!

    前言这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。筛选属性最终完成关于商品筛选是有两种方式(至少...
    继续阅读 »

    前言

    这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。

    筛选属性最终完成
    筛选属性最终完成

    关于商品筛选是有两种方式(至少我只见到两种):

    第一种: 将所有的商品的所有属性及详情返回给客户端,由客户端进行筛选。
    淘宝用的就是这种。
    第二种: 将所有的属性返回给客户端,客户选择完成属性后将属性发送给后台
    ,再由后台根据属性筛选出具体商品返回给客户端。
    京东就是这样搞的。。

    两种方式各有各的好处:

    第一种:体验性特别好,用户感觉不到延迟,立即选中立即就筛选出了详情。就是客户端比较费劲。。。

    第二种:客户端比较省时间,但是体验性太差了,你想想,在网络不是很通畅的时候,你选择一个商品还得等老半天。

    因为当时我没有参加到这个接口的设计,导致一直在变化。。我才不会告诉不是后台不给力,筛选不出来才一股脑的将所有锅甩给客户端。

    技术点

    1. 流式布局

       商品的属性并不是一样长的,所以需要自动适应内容的一个控件。
      推荐hongyang的博客。我就是照着那个搞的。
    2. RxJava

       不要问我,我不知道,我也是新手,我就是用它做出了效果,至于有没有
      用对,那我就不知道了。反正目的是达到了。
    3. Json解析???

    准备

    1. FlowLayout
    2. RxJava

    xml布局

    这个部分的布局不是很难,只是代码量较多,咱们就省略吧,直接看效果吧

    布局完成
    布局完成

    可以看到机身颜色、内存、版本下面都是空的,因为我们还没有将属性筛选出来。

    数据分析

    先看看整体的数据结构是怎么样的

    数据结构
    数据结构

    每一个商品都有一个父类,仅作标识,不参与计算,比如数据中的华为P9就是一个商品的类目,在这下面有着各种属性组成的商品子类,这才是真正的商品。

    而一个详细的商品是有三个基础属性所组成:

    1. 版本
    2. 内存
    3. 制式

    如上图中一个具体的商品的名称:"华为 P9全网通 3GB+32GB版 流光金 移动联通电信4G手机 双卡双待"

    商品属性据结构
    商品属性据结构

    所以,要获得一个具体的商品是非常的简单,只需要客户选中的三个属性与上图中所对应的属性完全相同,就能得到这个商品。其中最关键的还是将所有的商品属性筛选出来。

    筛选出所有属性及图片

    本文中使用的数据是直接从Assets目录中直接读取的。

    筛选出该商品的所有属性,怎么做呢?其实也是很简单的,直接for所有商品的所有属性,然后存储起来,去除重复的属性,那么最后剩下的就是该商品的属性了

     /**
    * 初始化商品信息
    *
  • 1. 提取所有的属性

  • *
  • 2. 提取所有颜色的照片

  • */

    private void initGoodsInfo() {
    //所有的颜色
    mColors = new ArrayList<>();
    //筛选过程中临时存放颜色
    mTempColors = new ArrayList<>();
    //所有的内存
    mMonerys = new ArrayList<>();
    //筛选过程中临时的内存
    mTempMonerys = new ArrayList<>();
    //所有的版本
    mVersions = new ArrayList<>();
    //筛选过程中的临时版本
    mTempVersions = new ArrayList<>();
    //获取到所有的商品
    shopLists = responseDto.getMsg().getChilds();
    callBack.refreshSuccess("¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax(), responseDto.getMsg().getParent().getName());
    callBack.parentName(responseDto.getMsg().getParent().getName());
    //遍历商品
    Observable.from(shopLists)
    //转换对象 获取所有商品的属性集合
    .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
    .subscribe(attrsEntity -> {
    //判断颜色
    if (mActivity.getString(R.string.shop_color).equals(attrsEntity.getAttrname()) && !mTempColors.contains(attrsEntity.getAttrvalue())) {
    mColors.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempColors.add(attrsEntity.getAttrvalue());
    }
    //判断制式
    if (mActivity.getString(R.string.shop_standard).equals(attrsEntity.getAttrname()) && !mTempVersions.contains(attrsEntity.getAttrvalue())) {
    mVersions.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempVersions.add(attrsEntity.getAttrvalue());
    }
    //判断内存
    if (mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()) && !mTempMonerys.contains(attrsEntity.getAttrvalue())) {
    mMonerys.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempMonerys.add(attrsEntity.getAttrvalue());
    }
    });

    // 提取出 每种颜色的照片
    tempImageColor = new ArrayList<>();
    mImages = new ArrayList<>();
    //遍历所有的商品列表
    Observable.from(shopLists)
    .subscribe(childsEntity -> {
    String color = childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue();
    if (!tempImageColor.contains(color)) {
    mImages.add(childsEntity.getShowimg());
    tempImageColor.add(color);
    }
    });
    // 提取出 每种颜色的照片

    //通知图片
    callBack.changeData(mImages, "¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax());
    callBack.complete(null);
    }

    初始化属性列表

    属性之间是有一些关系的,比如我这里是以颜色为初始第一项,那么我就得根据颜色筛选出这个颜色下的所有内存,然后根据内存筛选出所有的版本。同时,只要颜色、内存、版本三个都选择了,就得筛选出这个商品。

    {颜色>内存>版本}>具体商品

    颜色

    初始化颜色,设置选择监听,一旦用户选择了某个颜色,那么需要获取这个颜色下的所有内存,并且要开始尝试获取商品详情。

    1. 初始化颜色

       /**
      * 初始化颜色
      *
      * @hint
      */

      private void initShopColor() {
      for (TagInfo mColor : mColors) {
      //初始化所有的选项为未选择状态
      mColor.setSelect(false);
      }
      tvColor.setText("\"未选择颜色\"");
      mColors.get(colorPositon).setSelect(true);
      colorAdapter = new ProperyTagAdapter(mActivity, mColors);
      rlShopColor.setAdapter(colorAdapter);
      colorAdapter.notifyDataSetChanged();
      rlShopColor.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopColor.setOnTagSelectListener((parent, selectedList) -> {
      colorPositon = selectedList.get(0);
      strColor = mColors.get(colorPositon).getText();
      // L.e("选中颜色:" + strColor);
      tvColor.setText("\"" + strColor + "\"");
      //获取颜色照片
      initColorShop();
      //查询商品详情
      iterationShop();
      });
      }
    2. 获取颜色下所有的内存和该颜色的照片

       /**
      * 初始化相应的颜色的商品 获得 图片
      */

      private void initColorShop() {
      //初始化 选项数据
      Observable.from(mMonerys).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      L.e("开始筛选颜色下的内存----------------------------------------------------------------------------------");
      final List tempColorMemery = new ArrayList<>();
      //筛选内存
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()))
      .subscribe(attrsEntity -> {
      tempColorMemery.add(attrsEntity.getAttrvalue());
      // L.e("内存:"+attrsEntity.getAttrvalue());
      });

      Observable.from(mTempMonerys)
      .filter(s -> !tempColorMemery.contains(s))
      .subscribe(s -> {
      L.e("没有的内存:" + s);
      mMonerys.get(mTempMonerys.indexOf(s)).setChecked(false);
      });
      momeryAdapter.notifyDataSetChanged();
      L.e("筛选颜色下的内存完成----------------------------------------------------------------------------------");

      //获取颜色的照片
      ImageHelper.loadImageFromGlide(mActivity, mImages.get(tempImageColor.indexOf(strColor)), ivShopPhoto);
      }
    1. 根据选中的属性查询是否存在该商品

       /**
      * 迭代 选择商品属性
      */

      private void iterationShop() {
      // 选择的内存 选择的版本 选择的颜色
      if (strMemory == null || strVersion == null || strColor == null)
      return;
      //隐藏购买按钮 显示为缺货
      resetBuyButton(false);
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(1).getAttrvalue().equals(strVersion))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .subscribe(childsEntity -> {
      L.e(childsEntity.getShopprice());
      tvPrice.setText("¥" + childsEntity.getShopprice());
      // ImageHelper.loadImageFromGlide(mActivity, Constant.IMAGE_URL + childsEntity.getShowimg(), ivShopPhoto);
      L.e("已找到商品:" + childsEntity.getName() + " id:" + childsEntity.getPid());
      selectGoods = childsEntity;
      tvShopName.setText(childsEntity.getName());
      //显示购买按钮
      resetBuyButton(true);
      initShopStagesCount++;
      });
      }

    内存

    通过前面一步,已经获取了所有的内存。这一步只需要展示该所有内存,设置选择监听,选择了某个内存后就根据 选择颜色>选择内存 获取所有的版本。并在在其中也是要iterationShop()查询商品的,万一你是往回点的时候呢?

    1. 初始化版本

       /**
      * 初始化内存
      */

      private void initShopMomery() {
      for (TagInfo mMonery : mMonerys) {
      mMonery.setSelect(false);
      Log.e(" ", "initShopMomery: " + mMonery.getText());
      }
      tvMomey.setText("\"未选择内存\"");
      mMonerys.get(momeryPositon).setSelect(true);
      //-----------------------------创建适配器
      momeryAdapter = new ProperyTagAdapter(mActivity, mMonerys);
      rlShopMomery.setAdapter(momeryAdapter);
      rlShopMomery.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopMomery.setOnTagSelectListener((parent, selectedList) -> {
      momeryPositon = selectedList.get(0);
      strMemory = mMonerys.get(momeryPositon).getText();
      // L.e("选中内存:" + strMemory);
      iterationShop();
      tvMomey.setText("\"" + strMemory + "\"");
      iterationVersion();
      });
      }
    2. 根据已选择的颜色和内存获取到版本

       /**
      * 迭代 获取版本信息
      */

      private void iterationVersion() {
      if (strColor == null || strMemory == null) {
      return;
      }
      // L.e("开始迭代版本");
      Observable.from(mVersions).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      final List iterationTempVersion = new ArrayList<>();
      //1. 遍历出 这个颜色下的所有手机
      //2. 遍历出 这些手机的所有版本
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> attrsEntity.getAttrname().equals(mActivity.getString(R.string.shop_standard)))
      .subscribe(attrsEntity -> {
      iterationTempVersion.add(attrsEntity.getAttrvalue());
      });

      Observable.from(mTempVersions).filter(s -> !iterationTempVersion.contains(s)).subscribe(s -> {
      mVersions.get(mTempVersions.indexOf(s)).setChecked(false);
      });
      versionAdapter.notifyDataSetChanged();
      // L.e("迭代版本完成");
      }

    版本

    其实到了这一步,已经算是完成了,只需要设置监听,获取选中的版本,然后开始查询商品。

        /**
    * 初始化版本
    */

    private void initShopVersion() {
    for (TagInfo mVersion : mVersions) {
    mVersion.setSelect(false);
    }
    tvVersion.setText("\"未选择版本\"");
    mVersions.get(versionPositon).setSelect(true);
    //-----------------------------创建适配器
    versionAdapter = new ProperyTagAdapter(mActivity, mVersions);
    rlShopVersion.setAdapter(versionAdapter);
    rlShopVersion.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
    rlShopVersion.setOnTagSelectListener((parent, selectedList) -> {
    versionPositon = selectedList.get(0);
    strVersion = mVersions.get(versionPositon).getText();
    // L.e("选中版本:" + strVersion);
    iterationShop();
    tvVersion.setText("\"" + strVersion + "\"");
    });
    }

    完成

    最终效果图如下:

    筛选属性最终完成
    筛选属性最终完成

    不要在意后面的轮播图,那其实很简单的。

    代码下载:JiuYouYiShuSheng-Selector-master.zip

    收起阅读 »

    【开奖咯!】回帖晒晒端午节你们公司都发了什么?顺便抽个奖!~

    开奖咯!本次使用excel开奖,真实随机(参考链接https://www.excelhome.net/316.html)。部分用户回帖不符合活动要求,不参与本次开奖。参与回帖的10个随机幸运伙伴是:获得点赞最多的柳天明 5AuCf 4Lambert 3获得3...
    继续阅读 »
    开奖咯!本次使用excel开奖,真实随机(参考链接https://www.excelhome.net/316.html)。
    部分用户回帖不符合活动要求,不参与本次开奖。

    参与回帖的10个随机幸运伙伴是:


    获得点赞最多的

    柳天明 5
    AuCf 4
    Lambert 3

    获得3个最惨伙伴:

    yangjian、春春、孤狼☞小九

    请以上同学在6月17日 23:59前,将你的收件人,地址,电话,衣服图案(星空/字母)+尺码(L-3XL)信息发站内私信给@admin,超过领取截止时间未提交信息,视为放弃领取~

    感谢大家参与!下次见~

    =================================

    首先祝各位端午安康

    然而端午来临之际,各种群兴起了一些攀比之风

    有这样的



    还有这样的



    还有这样的



    然而我是这样的:




    不过节日没福利的同学们也没关系.环信精心为大家准备了端午福利 有福利的也可双喜临门!!!


    活动规则


    • 活动时间:即日起至 6 月 15 日 中午 12:00 截止
    • 参与方式 :在本篇帖子下留言关于端午福利或端午计划的回复(图文皆可,发图请单独开帖然后链接回到本帖下方)
    • 活动结束后,将从所有参与回帖的用户里随机抽取10人,赠送imgeek定制T恤。😉
    • 并且选出3个端午福利寒酸的盆友赠送夏日清凉挂脖风扇😆
    • 最多的前3名直接获得一件T恤!
    • 获奖名单将会在 6 月 15 日公布于本篇帖子下。
    T恤:



    收起阅读 »

    你还在用宏定义“iphoneX”判断安全区域(safe area)吗,教你正确使用Safe Area

    你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望...
    继续阅读 »

    你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。
    iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望被透明的状态栏或者导航栏遮挡的最高位置(status bar, navigation bar, toolbar, tab bar 等)。这个属性的值是一个 length 属性( topLayoutGuide.length)。 这个值可能由当前的 ViewController 或者 NavigationController 或者 TabbarController 决定。

    1、一个独立的ViewController,不包含于任何其他的ViewController。如果状态栏可见,topLayoutGuide表示状态栏的底部,否则表示这个ViewController的上边缘。

    2、包含于其他ViewController的ViewController不对这个属性起决定作用,而是由容器ViewController决定这个属性的含义:

    3、如果导航栏(Navigation Bar)可见,topLayoutGuide表示导航栏的底部。

    4、如果状态栏可见,topLayoutGuide表示状态栏的底部。

    5、如果都不可见,表示ViewController的上边缘。

    6、这部分还比较好理解,总之是屏幕上方任何遮挡内容的栏的最底部。

    iOS 11 开始弃用了这两个属性, 并且引入了 Safe Area 这个概念。苹果建议: 不要把 Control 放在 Safe Area 之外的地方

    // These objects may be used as layout items in the NSLayoutConstraint API
    @available(iOS, introduced: 7.0, deprecated: 11.0)
    open var topLayoutGuide: UILayoutSupport {get}
    @available(iOS, introduced: 7.0, deprecated: 11.0)
    open var bottomLayoutGuide: UILayoutSupport { get}

    今天, 来研究一下 iOS 11 中新引入的这个 API。

    UIView 中的 safe area
    iOS 11 中 UIViewController 的 topLayoutGuide 和 bottonLayoutGuide 两个属性被 UIView 中的 safe area 替代了。

    open var safeAreaInsets: UIEdgeInsets {get}
    @available(iOS 11.0, *)
    open func safeAreaInsetsDidChange()

    safeAreaInsets

    这个属性表示相对于屏幕四个边的间距, 而不仅仅是顶部还有底部。这么说好像没有什么感觉, 我们来看一看这个东西分别在 iPhone X 和 iPhone 8 中是什么样的吧!

    什么都没有做, 只是新建了一个工程然后在 Main.storyboard 中的 UIViewController 中拖了一个橙色的 View 并且设置约束为:


    在 ViewController.swift 的 viewDidLoad 中打印

    override func viewDidLoad() {
    super.viewDidLoad()
    print(view.safeAreaInsets)
    }
    // 无论是iPhone 8 还是 iPhone X 输出结果均为
    // UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)


    iPhone 8 VS iPhone X Safe Area (竖屏)


    iPhone 8 VS iPhone X Safe Area (横屏)

    这样对比可以看出, iPhone X 同时具有上下, 还有左右的 Safe Area。

    **再来看这个例子: ** 拖两个自定义的 View, 这个 View 上有一个 显示很多字的Label。然后设置这两个 View 的约束分别是:

    let view1 = MyView()
    let view2 = MyView()
    view.addSubview(view1)
    view.addSubview(view2)
    let screenW = UIScreen.main.bounds.size.width
    let screenH = UIScreen.main.bounds.size.height
    view1.frame = CGRect(x: 0, y: 0, width:screenW, height: 200)
    view2.frame = CGRect( x: 0, y: screenH - 200, width:screenW, height: 200)


    可以看出来, 子视图被顶部的刘海以及底部的 home 指示区挡住了。我们可以使用 frame 布局或者 auto layout 来优化这个地方:

    let insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? UIEdgeInsets.zero  
    view1.frame = CGRect(x: insets.left,y: insets.top,width:view.bounds.width - insets.left - insets.right,height: 200)
    view2.frame = CGRect(x: insets.left,y: screenH - insets.bottom - 200,width:view.bounds.width - insets.left - insets.right,height: 200)


    这样起来好多了, 还有另外一个更好的办法是直接在自定义的 View 中修改 Label 的布局:

    override func layoutSubviews() {
    super.layoutSubviews()
    if #available(iOS 11.0, *) {
    label.frame = safeAreaLayoutGuide.layoutFrame
    }
    }


    这样, 不仅仅是在 ViewController 中能够使用 safe area 了。

    UIViewController 中的 safe area

    在 iOS 11 中 UIViewController 有一个新的属性

    @available(iOS 11.0, *)
    open var additionalSafeAreaInsets: UIEdgeInsets

    当 view controller 的子视图覆盖了嵌入的子 view controller 的视图的时候。比如说, 当 UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 状态的时候, 就有 additionalSafeAreaInsets


    自定义的 View 上面的 label 布局兼容了 safe area。

    // UIView
    @available(iOS 11.0, *)
    open func safeAreaInsetsDidChange()
    //UIViewController
    @available(iOS 11.0, *)
    open func viewSafeAreaInsetsDidChange()

    这两个方法分别是 UIView 和 UIViewController 的 safe area insets 发生改变时调用的方法,如果需要做一些处理,可以重写这个方法。有点类似于 KVO 的意思。

    模拟 iPhone X 的 safe area


    额外的 safe area insets 也能用来测试你的 app 是否支持 iPhone X。在没有 iPhone X 也不方便使用模拟器的时候, 这个还是很有用的。

    //竖屏
    additionalSafeAreaInsets.top = 24.0
    additionalSafeAreaInsets.bottom = 34.0
    //竖屏, status bar 隐藏
    additionalSafeAreaInsets.top = 44.0
    additionalSafeAreaInsets.bottom = 34.0
    //横屏
    additionalSafeAreaInsets.left = 44.0
    additionalSafeAreaInsets.bottom = 21.0
    additionalSafeAreaInsets.right = 44.0

    UIScrollView 中的 safe area
    在 scroll view 上加一个 label。设置scroll 的约束为:

    scrollView.snp.makeConstraints { (make)  in
    make.edges.equalToSuperview()
    }


    iOS 7 中引入 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性在 iOS11 中被废弃掉了。取而代之的是 UIScrollView 的 contentInsetAdjustmentBehavior

    @available(iOS 11.0 , *)
    public enum UIScrollViewContentInsetAdjustmentBehavior : Int {
    case automatic //default value
    case scrollableAxes
    case never
    case always
    }
    @available(iOS 11.0 , *)
    open var contentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentBehavior

    Content Insets Adjustment Behavior

    never 不做调整。

    scrollableAxes content insets 只会针对 scrollview 滚动方向做调整。

    always content insets 会针对两个方向都做调整。

    automatic 这是默认值。当下面的条件满足时, 它跟 always 是一个意思

    1、能够水平滚动,不能垂直滚动

    2、scroll view 是 当前 view controller 的第一个视图

    3、这个controller 是被navigation controller 或者 tab bar controller 管理的

    4、automaticallyAdjustsScrollViewInsets 为 true

    在其他情况下 automoatc 跟 scrollableAxes 一样

    Adjusted Content Insets

    iOS 11 中 UIScrollView 新加了一个属性: adjustedContentInset

    @available(iOS 11.0, *)
    open var adjustedContentInset: UIEdgeInsets {get}

    adjustedContentInset 和 contentInset 之间有什么区别呢?

    在同时有 navigation 和 tab bar 的 view controller 中添加一个 scrollview 然后分别打印两个值:

    //iOS 10
    //contentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
    //iOS 11
    //contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    //adjustedContentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)

    然后再设置:

    `// 给 scroll view 的四个方向都加 10 的间距`
    `scrollView.contentInset = UIEdgeInsets(top: ``10``, left: ``10``, bottom: ``10``, right: ``10``)`

    打印:

    //iOS 10
    //contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
    //iOS 11
    //contentInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
    //adjustedContentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)

    由此可见,在 iOS 11 中 scroll view 实际的 content inset 可以通过 adjustedContentInset 获取。这就是说如果你要适配 iOS 10 的话。这一部分的逻辑是不一样的。

    系统还提供了两个方法来监听这个属性的改变

    //UIScrollView
    @available(iOS 11.0, *)
    open func adjustedContentInsetDidChange()
    //UIScrollViewDelegate
    @available(iOS 11.0, *)
    optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)

    UITableView 中的 safe area

    我们现在再来看一下 UITableView 中 safe area 的情况。我们先添加一个有自定义 header 以及自定义 cell 的 tableview。设置边框为 self.view 的边框。也就是

    tableView.snp.makeConstraints { (make) in
    make.edges.equalToSuperview()
    }
    或者
    tableView.frame = view.bounds


    自定义的 header 上面有一个 lable,自定义的 cell 上面也有一个 label。将屏幕横屏之后会发现,cell 以及 header 的布局均自动留出了 safe area 以外的距离。cell 还是那么大,只是 cell 的 contnt view 留出了相应的距离。这其实是 UITableView 中新引入的属性管理的:

    @available(iOS 11.0, *)
    open var insetsContentViewsToSafeArea: Bool

    insetsContentViewsToSafeArea 的默认值是 true, 将其设置成 no 之后:


    可以看出来 footer 和 cell 的 content view 的大小跟 cell 的大小相同了。这就是说:在 iOS 11 下, 并不需要改变 header/footer/cell 的布局, 系统会自动区适配 safe area

    需要注意的是, Xcode 9 中使用 IB 拖出来的 TableView 默认的边框是 safe area 的。所以实际运行起来 tableview 都是在 safe area 之内的。


    UICollectionView 中的 safe area

    我们在做一个相同的 collection view 来看一下 collection view 中是什么情况:


    这是一个使用了 UICollectionViewFlowLayout 的 collection view。 滑动方向是竖向的。cell 透明, cell 的 content view 是白色的。这些都跟上面 table view 一样。header(UICollectionReusableView) 没有 content view 的概念, 所以给其自身设置了红色的背景。

    从截图上可以看出来, collection view 并没有默认给 header cell footer 添加safe area 的间距。能够将布局调整到合适的情况的方法只有将 header/ footer / cell 的子视图跟其 safe area 关联起来。跟 IB 中拖 table view 一个道理。


    现在我们再试试把布局调整成更像 collection view 那样:


    截图上可以看出来横屏下, 左右两边的 cell 都被刘海挡住了。这种情况下, 我们可以通过修改 section insets 来适配 safe area 来解决这个问题。但是再 iOS 11 中, UICollectionViewFlowLayout 提供了一个新的属性 sectionInsetReference 来帮你做这件事情。

    @available(iOS 11.0, *)
    public enum UICollectionViewFlowLayoutSectionInsetReference : Int {
    case fromContentInset
    case fromSafeArea
    case fromLayoutMargins
    }
    /// The reference boundary that the section insets will be defined as relative to. Defaults to .fromContentInset.

    /// NOTE: Content inset will always be respected at a minimum. For example, if the sectionInsetReference equals `.fromSafeArea`, but the adjusted content inset is greater that the combination of the safe area and section insets, then section content will be aligned with the content inset instead.
    @available(iOS 11.0, *)
    open var sectionInsetReference: UICollectionViewFlowLayoutSectionInsetReference

    可以看出来,系统默认是使用 .fromContentInset 我们再分别修改, 看具体会是什么样子的。

    fromSafeArea

    这种情况下 section content insets 等于原来的大小加上 safe area insets 的大小。

    跟使用 .fromLayoutMargins 相似使用这个属性 colection view 的 layout margins 会被添加到 section content insets 上面。


    IB 中的 Safe Area

    前面的例子都说的是用代码布局要实现的部分。但是很多人都还是习惯用 Interface Builder 来写 UI 界面。苹果在 WWDC 2107 Session 412 中提到:Storyboards 中的 safe area 是向下兼容的 也就是说, 即使在 iOS10 及以下的 target 中,也可以使用 safe area 来做布局。唯一需要做的就是给每个 stroyboard 勾选 Use Safe Area Layout Guide。实际测试看,应该是 iOS9 以后都只需要这么做。

    知识点: 在使用 IB 设置约束之后, 注意看相对的是 superview 还是 topLayoutGuide/bottomLayoutGuide, 包括在 Xcode 9 中勾选了 Use Safe Area Layout Guide 之后,默认应该是相对于 safe area 了。

    总结

    1、在适配 iPhone X 的时候首先是要理解 safe area 是怎么回事。盲目的 if iPhoneX{} 只会给之后的工作代码更多的麻烦。

    2、如果只需要适配到 iOS9 之前的 storyboard 都只需要做一件事情。

    3、Xcode9 用 IB 可以看得出来, safe area 到处都是了。理解起来很简单。就是系统对每个 View 都添加了 safe area, 这个区域的大小,是否跟 view 的大小相同是系统来决定的。在这个 View 上的布局只需要相对于 safe area 就可以了。每个 View 的 safe area 都可以通过 iOS 11 新增的 API safeAreaInsets 或者 safeAreaLayoutGuide 获取。

    4、对与 UIViewController 来说新增了 **additionalSafeAreaInsets **这个属性, 用来管理有 tabbar 或者 navigation bar 的情况下额外的情况。

    5、对于 UIScrollView, UITableView, UICollectionView 这三个控件来说,系统以及做了大多数的事情。

    6、scrollView 只需要设置 contentInsetAdjustmentBehavior 就可以很容易的适配带 iPhoneX

    7、tableView 只需要在 cell header footer 等设置约束的时候相对于 safe area 来做

    8、对 collection view 来说修改 sectionInsetReference 为 .safeArea 就可以做大多数的事情了。

    总的来说, safe area 可以看作是系统在所有的 view 上加了一个虚拟的 view, 这个虚拟的 view 的大小等都是跟 view 的位置等有关的(当然是在 iPhoneX上才有值) 以后在写代码的时候,自定义的控件都尽量针对 safe area 这个虚拟的 view 进行布局。
    文中有些图片都是从这里来的, 很多内容也跟这篇文章差不多 可能需要梯子

    参考文章 可能需要梯子

    作者:CepheusSun
    链接:http://www.jianshu.com/p/63c0b6cc66fd

    转自:https://www.jianshu.com/p/5bebc28e0ede

    收起阅读 »

    深度优先搜索和广度优先搜索

    不撞南墙不回头-深度优先搜索基础部分对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321把问题形象化,假如有1,2,3三张扑克牌和编...
    继续阅读 »

    不撞南墙不回头-深度优先搜索

    基础部分

    对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。

    输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321

    把问题形象化,假如有1,2,3三张扑克牌和编号为1,2,3的三个箱子,把三张扑克牌分别放到三个箱子里有几种方法?

    我们用深度优先遍历搜索的思想来考虑这个问题。

    到1号箱子面前时,我们手里有1,2,3三种牌,我们把1放进去,然后走到2号箱子面签,手里有2,3两张牌, 然后我们把2放进去,再走到3号箱子前,手里之后3这张牌,所以把3放进去,然后再往前走到我们想象出来的一个4号箱子前,我们手里没牌了,所以,前面三个箱子中放牌的组合就是要输出的一种组合方式。(123)

    然后我们后退到3号箱子,把3这张拍取出来,因为这时我们手里只有一张牌,所以再往里放的话还是原来那种情况,所以我们还要再往后推,推到2号箱子前,把2从箱子中取出来,这时候我们手里有2,3两张牌,这时我们可以把3放进2号箱子,然后走到3号箱子中把2放进去,这又是一种要输出的组合方式.(132)

    就找这个思路继续下去再次回退的时候,我们就要退到1号箱,取出1,然后分别放2和3进去,然后产生其余的组合方式。

    有点啰嗦,但是基本是这么一个思路。

    我们来看一下实现的代码

    def sortNumber(self, n):
    flag = [False for i in range(n)]
    a = [0 for i in range(n)]
    l = []

    def dfs(step):
    if step == n:
    l.append(a[:])
    return
    for i in range(n):
    if flag[i] is False:
    flag[i] = True
    a[step] = i
    dfs(step + 1)
    flag[i] = False
    dfs(0)
    return l

    输出是

    [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]

    我们创建的a这个list相当于上面说到的箱子,flag这个list呢,来标识某一个数字是否已经被用过了。

    其实主要的思想就这dfs方法里面的这个for循环中,在依次的排序中,我们默认优先使用最小的那个数字,这个for循环其实就代表了一个位置上有机会放所有的这些数字,这个flag标识就避免了在一个位置重复使用数字的问题。

    如果if 成立,说明当前位置可以使用这个数字,所以把这个数字放到a这个数组中,然后flag相同为的标识改为True,也就是说明这个数已经被占用了,然后在调用方法本身,进行下一步。

    flag[i] = False这句代码是很重要的,在上面的dfs(也就是下一步)结束之后,返回到当前这个阶段,我们必须模拟收回这个数字,也就是把flag置位False,表示这个数字又可以用了。

    思路大概就是这样子的,这就是深度优先搜索的一个简单的场景。用debug跟一下,一步一步的来看代码就更清晰的了。

    迷宫问题

    上面我们已经简单的了解了深度优先搜索,下面我们通过一个迷宫的问题来进一步数字这个算法,然后同时引出我们的广度优先搜索。

    迷宫是由m行n列的单元格组成,每个单元格要不是空地,要不就是障碍物,我们的任务是找到一条从起点到终点的最短路径。

    我们抽象成模型来看一下


    start代表起点,end代表终点,x代表障碍物也就是不能通过的点。

    首先我们来分析一下,从start(0,0)这个点,甚至说是每一个点出发,都有四个方向可以走,上下左右,仅对于(0,0)这个点来说,只能往右和下走,因为往左和上就到了单元格外面了,我们可以称之为越界了。

    我们用深度优先的思想来考虑的话,我们可以从出发点开始,全部都先往一个方向走,然后走到遇到障碍物或者到了边界的情况下,在改变另一个方向,然后再走到底,这样一直走下去。

    拿到我们这个题目中,我们可以这样来思考,在走的时候,我们规定一个右下左上这样的顺序,也就是先往右走,走到不能往右走的时候在变换方向。比如我们从(0,0)走到(0,1)这个点,在(0,1)这个点也是先往右走,但是我们发现(0,2)是障碍物,所以我们就改变为往下走,走到(1,1),然后在(1,1)开始也是先向右走,这样一直走下去,直到找到我们的目标点。

    其中我们要注意一点,在右下左上这四个方向中有一个方向是我们来时候的方向,在当前这个点,四个方向没有走完之前我们不要后退到上一个点,所以我们也需要一个像前面排数字代码里面的flag数组来记录当前位置时候被占用。我们必须是四个方向都走完了才能往后退到上一个换方向。

    下面我贴一下代码

    def depthFirstSearch(self):
    m = 5
    n = 4

    # 5行 4 列
    flag = [[False for i in range(n)] for j in range(m)]
    # 存储不能同行的位置
    a = [[False for i in range(n)] for j in range(m)]
    a[0][2] = True
    a[2][2] = True
    a[3][1] = True
    a[4][3] = True

    global min_step
    min_step = 99999

    director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

    def dfs(x, y, step):

    # 什么情况下停止 (找到目标坐标)
    if x == 3 and y == 2:
    global min_step
    if step < min_step:
    min_step = step
    return

    # 右下左上
    for i in range(4):
    # 下一个点
    nextX = x + director_l[i][0]
    nextY = y + director_l[i][1]

    # 是否越界
    if nextX < 0 or nextX >= m or nextY < 0 or nextY >= n:
    continue

    # 不是障碍 and 改点还没有走过
    if a[x][y] is False and flag[x][y] is False:
    flag[x][y] = True
    dfs(nextX, nextY, step+1)
    flag[x][y] = False #回收

    dfs(0, 0, 0)
    return min_step

    首先flag这个算是二位数组吧,来记录我们位置是否占用了,然后a这个数组,是来记录整个单元格的,也就是标识那些障碍物的位置坐标。同样的,重点是这个dfs方法,他的参数x,y是指当前的坐标,step是步数。

    这个大家可以看到一个director_l的数组,他是来辅助我们根据当前左边和不同方向计算下一个位置的坐标的。

    dfs中我们已经注明了搜索停止的判断方式,也就是找到(3,2)这个点,然后下面的for循环,则代表四个不同的方向,每一个方向我们都会先求出他的位置,然后判断是否越界,如果没有越界在判断是否是障碍或者是否已经走过了,满足了所有的判断条件,我们在继续往下一个点,直到找到目标,比较路径的步数。

    这就是深度优先搜索了,当然,这个题目我们还有别的解法,这就到了我们说的广度优先搜索。

    层层递进-广度优先搜索

    我们先大体说一下广度优先搜索的思路,深度优先是先穷尽一个方向,而广度优先呢,则是基于一个位置,先拿到他所有能到达的位置,然后分别基于这些新位置,拿到他们能到达的所有位置,一次这样层层的递进,直到找到我们的终点。


    从(0,0)出发,可以到达(0,1)和(1,0),然后再从(0,1)出发到达(1,1),从(1,0)出发,到达(2,0)和(1,1),以此类推。

    所以我们我们维护一个队列来储存每一层遍历到达的点,当然了,不要重复储存同一个点。我们用一个指针head来标识当前的基准位置,也就是说最开始指向(0,0),当储存完毕所有(0,0)能抵达的位置时,我们就应该改变我们的基准位置了,这时候head++,就到了(0,1)这个位置,然后储存完他能到的所有位置,head++,就到了(1,0),然后继续。

    def breadthFirstSearch(self):

    class Node:
    def __init__(self):
    x = 0
    y = 0
    step = 0

    m, n = 5, 4
    # 记录
    flag = [[False for i in range(n)] for j in range(m)]

    # 储存地图信息
    a = [[False for i in range(n)] for j in range(m)]
    a[0][2] = True
    a[2][2] = True
    a[3][1] = True
    a[4][3] = True
    # 队列
    l = []
    startX, startY, step = 0, 0, 0
    head = 0
    index = 0

    node = Node()
    node.x = startX
    node.y = startY
    node.step = step
    index += 1
    l.append(node)
    flag[0][0] = True

    director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

    while head < index:

    last_node = l[head]
    # 处理四个方向
    for i in range(4):

    # 当前位置
    currentX = last_node.x + director_l[i][0]
    currentY = last_node.y + director_l[i][1]

    # 找到目标
    if currentX == 4 and currentY == 2:
    print('step = ' + str(last_node.step + 1))
    return

    #是否越界
    if currentX < 0 or currentY < 0 or currentX >= m or currentY >= n:
    continue

    if a[currentX][currentY] is False and flag[currentX][currentY] is False:


    #不是目标
    flag[currentX][currentY] = True

    node_new = Node()
    node_new.x = currentX
    node_new.y = currentY
    node_new.step = last_node.step+1
    l.append(node_new)
    index += 1



    head += 1

    首先我们定义了一个节点Node的类,来封装节点位置和当前的步数,flag,a,director_l这两个数组作用跟深度优先搜索相同,l是我们维护的队列,head指针指向当前基准的那个位置的,index指针指向队列尾。首先我们先把第一个Node(也就是起点)存进队列,广度优先搜索不需要递归,只要加一个循环就行。

    每次走到符合要求的位置,我们便把他封装成Node来存进对列中,每存一个index都要+1.

    head指针必须在一个节点四个方向都处理完了之后才可以+1,变换下一个基准节点。

    小结

    简单的介绍了深度优先搜索和广度优先搜索,深度优先有一种先穷尽一个方向然后结合使用回溯来找到解,广度呢,可能就是每做一次操作就涵盖了所有的可能结果,然后一步步往后推出去,找到最后的解。这算我个人的理解吧,不准确也不官方,思想也只能算是稍有体会,还得继续努力。

    题外话

    碍于自己的算法基础太差,最近一直在做算法题,我是先刷了一段时间的题目,发现吃力了,才开始看的书。感觉有点本末倒置。其实应该是先看看书,把算法的一些常用大类搞清楚了,形成一个知识框架,这样在遇到问题的时候可以知道往那些方向上面思考,可能会好一些吧。

    链接:https://www.jianshu.com/p/9a6a65078fc2

    收起阅读 »

    AndroidRoom库基础入门

    一、前言     Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充...
    继续阅读 »


    一、前言


        Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充分展示 SQLite 数据库的强大功能。Room 主要有以下几大优点:



    • 在编译时校验 SQL 语句;

    • 易用的注解减少重复和易错的模板代码;

    • 简化的数据库迁移路径。


        正是 Room 有以上的优点,所以建议使用 Room 访问数据库。


    二、Room 主要组件


        Room 主要组件有三个:



    • 数据库类(RoomDatabase):拥有数据库,并作为应用底层持久性数据的主要访问接入点。

    • 数据实体类(Entity):表示应用数据库中的表。

    • 数据访问对象(DAO):提供方法使得应用能够在数据库中查询、更新、插入以及删除数据。


        应用从数据库类获取一个与之相关联的数据访问对象(DAO)。应用可以通过这个数据访问对象(DAO)在数据库中检索数据,并以相关联的数据实体对象呈现结果;应用也可以使用对的数据实体类对象,更新数据库对应表中的行(或者插入新行)。应用对数据库的操作完全通过 Room 这个抽象层实现,无需直接操作 SQLite数据库。下图就是 Room 各个组件之间的关系图:


    Room组件关系图


    三、Room 基础入门


        大致了解了 Room 的工作原理之后,下面我们就来介绍一下 Room 的使用入门。


    3.1 引入 Room 库到项目


    引入 Room 库到项目,在项目程序模块下的 build.gradle 文件的 dependencies


    // Kotlin 开发环境,需要引入 kotlin-kapt 插件
    apply plugin: 'kotlin-kapt'

    // .........

    dependencies {
    // other dependecies

    def room_version = "2.3.0"
    implementation("androidx.room:room-runtime:$room_version")
    // 使用 Kotlin 注解处理工具(kapt,如果项目使用Kotlin语言开发,这个必须引入,并且需要引入 kotlin-kapt 插件
    kapt("androidx.room:room-compiler:$room_version")
    // To use Kotlin Symbolic Processing (KSP)
    // ksp("androidx.room:room-compiler:$room_version")

    // 可选 - 为 Room 添加 Kotlin 扩展和协程支持
    implementation("androidx.room:room-ktx:$room_version")

    // 可选 - 为 Room 添加 RxJava2 支持
    implementation "androidx.room:room-rxjava2:$room_version"

    // 可选 - 为 Room 添加 RxJava3 支持
    implementation "androidx.room:room-rxjava3:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")
    }


    注意事项:如果项目是用 kotlin 语言开发,一定要引入 kotlin 注解处理工具,并且在 build.gradle 中添加 kitlin-kapt插件(apply plugin: 'kotlin-kapt'),否则应用运行会抛出 xx.AppDatabase. AppDatabase_Impl does not exist 异常。



    3.2 Room 使用示例


        使用 Room 访问数据库,需要首先定义 Room 的三个组件,然后通过数据访问对象实例访问数据。


    3.2.1 定义数据实体类


        数据实体类对应数据库中的表,实体类的字段对应表中的列。定义 Room 数据实体类,使用 data class 关键字,并使用 @Entity 注解标注。更多关于数据实体类相关注解(包括属性相关注解),请参考: Android Room 数据实体类详解。如下代码所示:


    @Entity
    class User(@PrimaryKey val uid: Int, @ColumnInfo() val name: String, @ColumnInfo val age: Int)


    注意事项:默认情况下,Room 会根据实体类的类为表名(在数据库中表名其实不区分大小写),开发者也可以在 @Entity 注解通过 tableName 参数指定表名。



    3.3.2 定义数据访问对象(DAO)


        数据访问对象是访问数据库的桥梁,通过 DAO 访问数据,查询或者更新数据库中的数据(数据实体类是媒介)。数据访问对象(DAO)是一个接口,定义时添加 @Dao 注解标注,接口中的每一个成员方法表示一个操作,成员方法使用注解标示操作类型。更多关于数据访问对象(DAO)和数据操作类型注解,请参考:Android Room 数据访问对象详解。以下是简单的 DAO 示例代码:


    @Dao
    interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Query("SELECT * FROM user WHERE name LIKE :name")
    fun findByName(name: String): List<User>

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
    }


    注意事项:
    1. 数据访问对象是接口类型,成员方法是没有方法体的,成员方法必须使用注解标示操作类型;
    2. 数据库实体类成员方法中的 SQL 语句,在编译是会检查语法是否正确。



    3.3.3 定义数据库类


        数据库是存储数据的地方,使用 Room 定义数据库时,声明一个抽象类(abstract class),并用 @Database 注解标示,在 @Database 注解中使用 entities 参数指定数据库关联的数据实体类列表,使用 version 参数指定数据的版本。数据库类中包含获取数据访问实体类对象的抽象方法,更多关于数据库相关内容,请参考:Android Room 数据库详解,以下是简单的数据类定义。


    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
    }


    注意事项:
    1. 数据库类是一个抽象类,他的成员方法是抽象方法;
    2. 定义数据库类时必须指定关联的数据实体类列表,这样数据库类才知道需要创建那些表;
    3. 数据的版本号,如果数据库的表构造有变动时,需要升级版本号,这样数据库才会更新表结构(如修改表字段、新增表等,跟直接使用 SQLite 接口使用 SQLiteDatabase 类一样),但是数据库的升级并不是修改版本号那么简单,还需要处理数据库升级过程中需要修改的地方,更多详情请参考:Android Room 数据库升级



    3.3.4 创建数据库实例


        定义好数据实体类、数据访问对象(DAO)和数据类之后,便可以创建数据库实例。使用 Room.databaseBuilder().build() 创建一个数据库实体类,Room 会根据定义的数据实体类、数据库访问对象和数据库类,以及他们定义时指定的对应关系,自动创建数据库和对应的表关系。如以下示例代码所示:


    val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db").build()


    注意事项:
    1. 每一个 RoomDatabase 实例都是非常耗费资源的,如果你的应用是单个进程中运行,那么在实例 RoomDatabase 时请遵循单例设计模式,在单个进程中几乎不需要访问多个 RoomDatabase 实例。
    2. 如果你的应用在多个进程中运行(比如:远程服务(RemoteService)),在构建 RoomDatabase 的构建器中调用 Room.databaseBuilder().enableMultiInstanceInvalidation() 方法,这样一来,在每个进程中都有一个 RoomDatabase 实例,如果在某个进程中将共享的数据库文件失效,将会自动将这个失效自动同步给其他进程中的 RoomDatabase 实例。



    3.3.5 从数据库实例中获取数据访问对象(DAO)实例


        在定义数据库类时,将数据访问对象(DAO)类与之相关联,定义抽象方法返回对应的数据库访问对象(DAO)实例。在数据库实例化过程中,Room 会自动生成对应的数据访问对象(DAO),只需要调用定义数据库类时定义的抽象方法,即可获取对应的数据访问对象(DAO)实例。如下示例所示:


    val userDao = db.userDao()

    3.3.6 通过数据访问对象(DAO)实例操作数据库


        获取到数据访问对象(DAO)实例,就可以调用数据库访问对象(DAO)类中定义的方法操作数据库了。如下示例所示:


    Thread {
    // 插入数据
    userDao.insertAll(
    User(1, "Student1", 18),
    User(2, "Student2", 18),
    User(3, "Student3", 17),
    User(4, "Student4", 19)
    )

    // 查询数据
    val result = userDao.getAll()

    result.forEach {
    println("Student: id = ${it.uid}, name = ${it.name}, age = ${it.age}")
    }
    }.start()


    注意事项:
    1. 使用数据访问对象(DAO)实例操作数据库时,不能再 UI 主线程中调用 DAO 接口,否则会抛出异常(java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.



    四、编后语


        Room 是非常强大易用的,可以减少数据库操作过程中的出错,因为所有的 SQL 语句都在编译是进行检查,如果存在错误,将会在编译时就显示错误信息。不仅如此,Room 还非常优秀地处理了多进程很多线程访问数据库的问题。




    ————————————————
    版权声明:本文为CSDN博主「精装机械师」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/yingaizhu/article/details/117514630

    收起阅读 »

    Android数据库—SQLite

    Android数据库—SQLite 不适合存储大规模数据 用来存储每一个用户各自的信息 在线查看数据库方法 Android Studio查看SQLite数据库方法大全 从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻...
    继续阅读 »


    Android数据库—SQLite



    • 不适合存储大规模数据

    • 用来存储每一个用户各自的信息


    在线查看数据库方法


    Android Studio查看SQLite数据库方法大全



    从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻墙,比较麻烦。


    如今最新版的Android Studio可以直接在里面查看数据库,无需别的了。




    • stetho使用



      • build.gradle文件中引入依赖

        implementation 'com.facebook.stetho:stetho:1.5.1'


      • 在需要操作数据库的Activity中加入以下语句

      Stetho.initializeWithDefaults(this);


      • 谷歌调试



    继承SQLiteOpenHelper的类,加载驱动



    继承SQLiteOpenHelper类,实现三个方法。



    • 构造函数

    • 建表方法:onCreate方法

    • 更新表方法:onUpgrade方法




    • MySQLiteOpenHelper


    package com.hnucm.androiddatabase;

    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    import androidx.annotation.Nullable;

    //加载数据库驱动
    //建立连接
    public class MySQLiteOpenHelper extends SQLiteOpenHelper {
    //构造方法
    //name -> 数据库名字
    public MySQLiteOpenHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
    super(context, name, factory, version);
    }

    //建表
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
    //建表语句 自增长 主键
    sqLiteDatabase.execSQL("create table products(id integer primary key autoincrement,name varchar(20),singleprice double,restnum integer) ");
    }

    //更新表
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
    }

    在Activity中进行增删改查



    整个Activity都是用数据库,所以声明驱动和数据库为全局变量,方便使用。



    //加载驱动
    mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
    "product",null,1);
    //得到数据库
    sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();


    布局文件中设置四个按钮,进行增删改查操作。




    • 布局-------activity_main.xml


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


    <Button
    android:id="@+id/insert"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="增加一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/delete"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="删除一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/update"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="修改一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/select"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="查询一条商品信息"
    android:textSize="25sp"
    />


    </LinearLayout>


    • 总体逻辑代码-------MainActivity


    package com.hnucm.androiddatabase;

    import androidx.appcompat.app.AppCompatActivity;

    import android.content.ContentValues;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;

    public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    //声明增删改查四个按钮
    Button addBtn;
    Button delBtn;
    Button updateBtn;
    Button selectBtn;
    //声明驱动
    MySQLiteOpenHelper mySQLiteOpenHelper;
    //声明数据库
    SQLiteDatabase sqLiteDatabase;
    //数据对象
    ContentValues contentValues;
    //增删改查条件变量
    String id;
    String name;

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

    //加载驱动
    mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
    "product",null,1);
    //得到数据库
    sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();

    //初始化四个按钮
    addBtn = findViewById(R.id.insert);
    delBtn = findViewById(R.id.delete);
    updateBtn = findViewById(R.id.update);
    selectBtn = findViewById(R.id.select);

    //点击四个按钮
    addBtn.setOnClickListener(this);
    delBtn.setOnClickListener(this);
    updateBtn.setOnClickListener(this);
    selectBtn.setOnClickListener(this);
    }

    //四个按钮的点击事件
    @Override
    public void onClick(View view) {
    switch (view.getId()){
    //增加数据
    case R.id.insert:
    //创建数据,使用ContentValues -> HashMap
    contentValues = new ContentValues();
    //自增长 主键 增加无需加入id
    //contentValues.put("id",1);
    contentValues.put("name","辣条");
    contentValues.put("singleprice",3.50);
    contentValues.put("restnum",12);
    //将创建好的数据对象加入数据库中的哪一个表
    sqLiteDatabase.insert("products",null,contentValues);
    break;
    //删除数据
    case R.id.delete:
    //删除条件
    id = "1";
    name = "辣条";
    //在哪张表里,根据条件删除
    sqLiteDatabase.delete("products","id = ? and name = ?",
    new String[]{id,name});
    break;
    //修改数据
    case R.id.update:
    //修改条件
    id = "2";
    //将满足条件的数据修改
    contentValues = new ContentValues();
    contentValues.put("name","薯片");
    //在数据库中修改
    sqLiteDatabase.update("products",contentValues,"id=?",
    new String[]{id});
    break;
    //查询所有数据
    case R.id.select:
    //采用cursor游标查询
    Cursor cursor = sqLiteDatabase.query("products",null,null,
    null,null,null,null);
    //游标下一个存在,即没有到最后
    while(cursor.moveToNext()){
    //每一条数据取出每一列
    int id = cursor.getInt(cursor.getColumnIndex("id"));
    name = cursor.getString(cursor.getColumnIndex("name"));
    double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
    int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
    //打印数据
    Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
    + singleprice + ",restnum:" + restnum);
    }
    break;
    }
    }
    }

    增加数据


    //创建数据,使用ContentValues -> HashMap
    contentValues = new ContentValues();
    //自增长 主键 增加无需加入id
    //contentValues.put("id",1);
    contentValues.put("name","辣条");
    contentValues.put("singleprice",3.50);
    contentValues.put("restnum",12);
    //将创建好的数据对象加入数据库中的哪一个表
    sqLiteDatabase.insert("products",null,contentValues);

    删除数据


    //删除条件
    id = "1";
    name = "辣条";
    //在哪张表里,根据条件删除
    sqLiteDatabase.delete("products","id = ? and name = ?",
    new String[]{id,name});

    修改数据


    //修改条件
    id = "2";
    //将满足条件的数据修改
    contentValues = new ContentValues();
    contentValues.put("name","薯片");
    //在数据库中修改
    sqLiteDatabase.update("products",contentValues,"id=?",
    new String[]{id});

    查询数据


    //采用cursor游标查询
    //没有查询条件,所以查询表中所有信息
    Cursor cursor = sqLiteDatabase.query("products",null,null,
    null,null,null,null);
    //游标下一个存在,即没有到最后
    while(cursor.moveToNext()){
    //每一条数据取出每一列
    int id = cursor.getInt(cursor.getColumnIndex("id"));
    name = cursor.getString(cursor.getColumnIndex("name"));
    double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
    int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
    //打印数据
    Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
    + singleprice + ",restnum:" + restnum);
    }
    收起阅读 »

    总是听到有人说AndroidX,到底什么是AndroidX?

    Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。An...
    继续阅读 »

    Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。

    Android系统在刚刚面世的时候,可能连它的设计者也没有想到它会如此成功,因此也不可能在一开始的时候就将它的API考虑的非常周全。随着Android系统版本不断地迭代更新,每个版本中都会加入很多新的API进去,但是新增的API在老版系统中并不存在,因此这就出现了一个向下兼容的问题。

    举个例子,当Android系统发布到3.0版本的时候,突然意识到了平板电脑的重要性,因此为了让Android可以更好地兼容平板,Android团队在3.0系统(API 11)中加入了Fragment功能。但是Fragment的作用并不只局限于平板,以前的老系统中也想使用这个功能该怎么办?于是Android团队推出了一个鼎鼎大名的Android Support Library,用于提供向下兼容的功能。比如我们每个人都熟知的support-v4库,appcompat-v7库都是属于Android Support Library的,这两个库相信任何做过Android开发的人都使用过。

    但是可能很多人并没有考虑过support-v4库的名字到底是什么意思,这里跟大家解释一下。4在这里指的是Android API版本号,对应的系统版本是1.6。那么support-v4的意思就是这个库中提供的API会向下兼容到Android 1.6系统。它对应的包名如下:

    类似地,appcompat-v7指的是将库中提供的API向下兼容至API 7,也就是Android 2.1系统。它对应的包名如下:

    可以发现,Android Support Library中提供的库,它们的包名都是以android.support.*开头的。

    但是慢慢随着时间的推移,什么1.6、2.1系统早就已经被淘汰了,现在Android官方支持的最低系统版本已经是4.0.1,对应的API版本号是15。support-v4、appcompat-v7库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。

    那么很明显,Android团队也意识到这种命名已经非常不合适了,于是对这些API的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX本质上其实就是对Android Support Library进行的一次升级,升级内容主要在于以下两个方面。

    第一,包名。之前Android Support Library中的API,它们的包名都是在android.support.*下面的,而AndroidX库中所有API的包名都变成了在androidx.*下面。这是一个很大的变化,意味着以后凡是android.*包下面的API都是随着Android操作系统发布的,而androidx.*包下面的API都是随着扩展库发布的,这些API基本不会依赖于操作系统的具体版本。

    第二,命名规则。吸取了之前命名规则的弊端,AndroidX所有库的命名规则里都不会再包含具体操作系统API的版本号了。比如,像appcompat-v7库,在AndroidX中就变成了appcompat库。

    一个AndroidX完整的依赖库格式如下所示:

    implementation 'androidx.appcompat:appcompat:1.0.2'

    了解了AndroidX是什么之后,现在你应该放轻松了吧?它其实并不是什么全新的东西,而是对Android Support Library的一次升级。因此,AndroidX上手起来也没有任何困难的地方,比如之前你经常使用的RecyclerView、ViewPager等等库,在AndroidX中都会有一个对应的版本,只要改一下包名就可以完全无缝使用,用法方面基本上都没有任何的变化。

    但是有一点需要注意,AndroidX和Android Support Library中的库是非常不建议混合在一起使用的,因为它们可能会产生很多不兼容的问题。最好的做法是,要么全部使用AndroidX中的库,要么全部使用Android Support Library中的库。

    而现在Android团队官方的态度也很明确,未来都会为AndroidX为主,Android Support Library已经不再建议使用,并会慢慢停止维护。另外,从Android Studio 3.4.2开始,我发现新建的项目已经强制勾选使用AndroidX架构了。

    那么对于老项目的迁移应该怎么办呢?由于涉及到了包名的改动,如果从Android Support Library升级到AndroidX需要手动去改每一个文件的包名,那可真得要改死了。为此,Android Studio提供了一个一键迁移的功能,只需要对着你的项目名右击 → Refactor → Migrate to AndroidX,就会弹出如下图所示的窗口。

    这里点击Migrate,Android Studio就会自动检查你项目中所有使用Android Support Library的地方,并将它们全部改成AndroidX中对应的库。另外Android Studio还会将你原来的项目备份成一个zip文件,这样即使迁移之后的代码出现了问题你还可以随时还原回之前的代码。


    版权声明:本文为CSDN博主「guolin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/guolin_blog/article/details/97142065

    收起阅读 »

    ReactiveObjC看这里就够了

    1、什么是ReactiveObjCReactiveObjC是ReactiveCocoa系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传...
    继续阅读 »

    1、什么是ReactiveObjC

    ReactiveObjC是ReactiveCocoa
    系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传递。核心思路:创建信号->订阅信号(subscribeNext)->发送信号
    通过信号signals的传输,重新组合和响应,软件代码的编写逻辑思路将变得更清晰紧凑,有条理,而不再需要对变量的变化不断的观察更新。

    2、什么是函数响应式编程

    响应式编程是一种和事件流有关的编程模式,关注导致状态值改变的改变的行为事件,一系列事件组成了事件流,一系列事件是导致属性值发生变化的原因,非常类似于设计模式中的观察者模式。在网上流传一个非常经典的解释响应式编程的概念,在一般的程序开发中:a = b + c,赋值之后 b 或者 c 的值变化后,a 的值不会跟着变化,而响应式编程的目标就是,如果 b 或者 c 的数值发生变化,a 的数值会同时发生变化;

    3、ReactiveObjC的流程分析

    ReactiveObjC主要有三个关键类:
    1、RACSignal信号
    RACSignal 是各种信号的基类,其中RACDynamicSignal是用的最多的动态信号
    2、RACSubscriber订阅者
    RACSubscriber是实现了RACSubscriber协议的订阅者类,这个协议定义了4个必须实现的方法

    @protocol RACSubscriber <NSObject>
    @required
    - (void)sendNext:(nullable id)value; //常见
    - (void)sendError:(nullable NSError *)error; //常见
    - (void)sendCompleted; //常见
    - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
    @end

    RACSubscriber主要保存了三个block,跟三个常见的协议方法一一对应\

    @property (nonatomic, copy) void (^next)(id value);
    @property (nonatomic, copy) void (^error)(NSError *error);
    @property (nonatomic, copy) void (^completed)(void);

    3、RACDisposable清洁工
    RACDisposable主要是对资源的释放处理,其中使用RACDynamicSignal时,会创建一个RACCompoundDisposable管理清洁工对象。其内部定义了两个数组,一个是_inlineDisposables[2]固定长度2的A fast array,超出2个对象的长度由_disposables数组管理,_inlineDisposables数组速度快,两个数组都是线程安全的。

    4、ReactiveObjC导入工程的方式

    pod 'ReactiveObjC'

    5、ReactiveObjC的几种使用情况

    1、NSArray 数组遍历

    NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
    [array.rac_sequence.signal subscribeNext:^(id _Nullable x) {
    NSLog(@"数组内容:%@", x);
    }];

    2、NSArray快速替换数组中内容为99和单个替换数组内容,两个方法都不会改变原数组内容,操作完后都会生成一个新的数组,省去了创建可变数组然后遍历出来单个添加的步骤。

    NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
    /*
    NSArray * newArray = [[array.rac_sequence mapReplace:@"99"] array];
    NSLog(@"%@",newArray);
    */
    NSArray * newArray = [[array.rac_sequence map:^id _Nullable(id _Nullable value) {
    NSLog(@"原数组内容%@",value);
    return @"99";
    }] array];
    NSLog(@"%@",newArray);

    3、NSDictionary 字典遍历

    NSDictionary * dic = @{@"name":@"Tom",@"age":@"20"};
    [dic.rac_sequence.signal subscribeNext:^(id _Nullable x) {

    RACTupleUnpack(NSString *key, NSString * value) = x;//X为为一个元祖,RACTupleUnpack能够将key和value区分开
    NSLog(@"数组内容:%@--%@",key,value);
    }];

    4、UIButton 监听按钮的点击事件

    UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(100, 200, 100, 60);
    btn.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    //监听点击事件
    [[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
    }];

    5、UITextField 监听输入框的一些事件

    UITextField * textF = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
    textF.placeholder = @"请输入内容";
    textF.textColor = [UIColor blackColor];
    [self.view addSubview:textF];
    //实时监听输入框中文字的变化
    [[textF rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
    NSLog(@"输入框的内容--%@",x);
    }];
    //UITextField的UIControlEventEditingChanged事件,免去了KVO
    [[textF rac_signalForControlEvents:UIControlEventEditingChanged] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);
    }];
    //添加监听条件
    [[textF.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
    return [value isEqualToString:@"100"];//此处为判断条件,当输入的字符为100的时候执行下面方法
    }]subscribeNext:^(NSString * _Nullable x) {
    NSLog(@"输入框的内容为%@",x);
    }];

    6、KVO 代替KVO来监听按钮frame的改变

    UIButton * loginBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    loginBtn.frame = CGRectMake(100, 210, 100, 60);
    loginBtn.backgroundColor = [UIColor blueColor];
    [loginBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [loginBtn setTitle:@"666" forState:UIControlStateNormal];
    //[loginBtn setTitle:@"111" forState:UIControlStateDisabled];
    [self.view addSubview:loginBtn];
    //监听点击事件
    [[loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
    x.frame = CGRectMake(100, 210, 200, 300);
    }];
    //KVO监听按钮frame的改变
    [[loginBtn rac_valuesAndChangesForKeyPath:@"frame" options:(NSKeyValueObservingOptionNew) observer:self] subscribeNext:^(RACTwoTuple<id,NSDictionary *> * _Nullable x) {
    NSLog(@"frame改变了%@",x);
    }];

    //下面方法也能监听,但是在按钮创建的时候此方法也执行了,简单说就是在界面展示之前此方法就走了一遍,总感觉怪怪的。
    /*
    [RACObserve(loginBtn, frame) subscribeNext:^(id _Nullable x) {
    NSLog(@"frame改变了%@",x);
    }];

    7、NSNotification 监听通知事件

    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardDidShowNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
    NSLog(@"监听键盘弹出"); //不知道为啥此方法不止走一次,但是原本的通知监听方法只走一次,有知道的可以私信我,谢谢
    }];

    8、timer 代替timer定时循环执行方法

    [[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    //这里面的方法2秒一循环
    }];
    //如果关闭定时器,停止需要创建一个全局的disposable
    //@property (nonatomic, strong) RACDisposable * disposable;//创建
    /*
    self.disposable = [[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    NSLog(@"当前时间:%@", x); // x 是当前的系统时间
    //关闭计时器
    [self.disposable dispose];
    }];
    */

    6、开发中用到的小栗子

    1、发送短信验证码的按钮倒计时

    /*
    @property (nonatomic, strong) RACDisposable * disposable;
    @property (nonatomic, assign) NSInteger time;
    */
    //上面两句要提前定义
    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(100, 200, 200, 50)];
    btn.titleLabel.textAlignment = NSTextAlignmentCenter;
    btn.backgroundColor = [UIColor greenColor];
    [btn setTitle:@"发送验证码" forState:(UIControlStateNormal)];
    [self.view addSubview:btn];
    [[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    self.time = 10;
    btn.enabled = NO;
    [btn setTitle:[NSString stringWithFormat:@"请稍等%zd秒",self.time] forState:UIControlStateDisabled];
    self.disposable = [[RACSignal interval:1.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    //减去时间
    self.time --;
    //设置文本
    NSString *text = (self.time > 0) ? [NSString stringWithFormat:@"请稍等%zd秒",_time] : @"重新发送";
    if (self.time > 0) {
    btn.enabled = NO;
    [btn setTitle:text forState:UIControlStateDisabled];
    }else{
    btn.enabled = YES;
    [btn setTitle:text forState:UIControlStateNormal];
    //关掉信号
    [self.disposable dispose];
    }
    }];
    }];

    2、登录按钮的状态根据账号和密码输入框内容的长度来改变

    UITextField *userNameTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 70, 200, 50)];
    UITextField *passwordTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 130, 200, 50)];
    userNameTF.placeholder = @"请输入用户名";
    passwordTF.placeholder = @"请输入密码";
    [self.view addSubview:userNameTF];
    [self.view addSubview:passwordTF];
    UIButton *loginBtn = [[UIButton alloc]initWithFrame:CGRectMake(40, 180, 200, 50)];
    [loginBtn setTitle:@"马上登录" forState:UIControlStateNormal];
    [self.view addSubview:loginBtn];
    //根据textfield的内容来改变登录按钮的点击可否
    RAC(loginBtn, enabled) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
    return @(username.length >= 11 && password.length >= 6);
    }];
    //根据textfield的内容来改变登录按钮的背景色
    RAC(loginBtn, backgroundColor) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
    return (username.length >= 11 && password.length >= 6) ? [UIColor redColor] : [UIColor grayColor];
    }];

    结尾:
    本文参考:

    关于ReactiveObjC原理及流程简介https://www.jianshu.com/p/fecbe23d45c1

    响应式编程之ReactiveObjC常见用法https://www.jianshu.com/p/6af75a449d90

    【iOS 开发】ReactiveObjC(RAC)的使用汇总

    https://www.jianshu.com/p/0845b1a07bfa

    链接:https://www.jianshu.com/p/222c21007251

    收起阅读 »

    提升用户愉悦感的润滑剂-看SDWebImage本地缓存结构设计

    手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会...
    继续阅读 »

    手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;
    而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会永远将这个APP打入冷宫。想要优化界面的响应、节省流量,本地缓存对用户而言是透明的,却是必不可少的一环。
    设计本地缓存并不是开一个数组或本地数据库,把数据丢进去就能达到预期效果的,这是因为:

    1、内存读写快,但容量有限,图片容易丢失;
    2、磁盘容量大,图片“永久”保存,但读写较慢。

    这对计算机与生俱来的矛盾,导致缓存设计必须将两种存储方式组合使用,加上iOS系统平台特性,无形中增加了本地缓存系统的复杂度,本篇来看看 SDWebImage 是如何实现一个流畅的缓存系统的。

    SDWebImage 本地缓存的整体流程如下:


    缓存数据的格式

    在深入具体的读写流程之前,先了解一下存储数据的格式,这有助于我们理解后续的操作步骤:

    1、为了加快界面显示的需要,内存缓存的图片用 UIImage;
    2、磁盘缓存的是 NSData,是从网络下载到的原始数据。

    写入流程

    存入图片时,调用入口方法:

    - (void)storeImage:(nullable UIImage *)image
    imageData:(nullable NSData *)imageData
    forKey:(nullable NSString *)key
    toDisk:(BOOL)toDisk
    completion:(nullable SDWebImageNoParamsBlock)completionBlock

    先写入 SDMemoryCache :

    [self.memCache setObject:image forKey:key cost:cost];

    再写入磁盘,由 ioQueue 异步执行:

    - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

    读取流程

    读取图片时,调用入口方法为:

    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock

    首先从内存缓存中获取:

    UIImage *image = [self imageFromMemoryCacheForKey:key];

    如果内存中有,直接返回给外部调用者;当内存缓存获取失败时,从磁盘获取图片文件数据:

    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];

    解码为 UIImage:

    diskImage = [self diskImageForKey:key data:diskData options:options];

    并写回内存缓存,再返回给调用者。

    磁盘缓存

    磁盘缓存位于沙盒的 Caches 目录
    下:/Library/Caches/default/com.hackemist.SDWebImageCache.default/,
    保证了缓存图片在下次启动还存在,又不会被iTunes备份。
    文件名由cachedFileNameForKey生成,使用Key(即图片URL)的MD5值,顺便说明一下,图片的Key还有其他作用:

    1、作为获取缓存的索引
    2、防止重复写入

    写入过程很简单:

    - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

    利用 NSData 的文件写入方法:

    [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];

    内存缓存

    SDMemoryCache 是继承 NSCache 实现的,占用空间是用像素值来统计的(SDCacheCostForImage),因为 NSCache 的totalCostLimit 并不严格(关于 NSCache 的一些特性,请参考被忽视和误解的NSCache),用像素计算可以方
    便预估和加快运算。

    辅助内存缓存 weakCache

    你可能从看前面流程图时,就好奇这个辅助内存缓存的作用是什么,这是由于收到内存警告时,NSCache 里的图片可能已经被系统清除,但实际图片还是被界面上的 ImageView 保留着,因此在 weakCache 再保存一份,遇到这种情况时,只要简单地将 weakCache 中的值写回 NSCache 即可,这样提高了缓存命中率,也避免在界面保有图片时,缓存系统的误判,导致重复下载或从磁盘加载图片。
    weakCache 由 NSMapTable 实现,因为普通的NSDictionary无法分别对Key强引用,对值弱引用,即 weakCache 利用对 UIImage 的弱引用,可以判断是否被缓存以外的对象使用,是本地缓存加倍顺滑的关键喔。

    总结

    SDMemoryCache 的本地缓存很好地平衡了内存和磁盘的优缺点,最大限度利用了系统本身提供的 NSCache 和 NSData 的原生方法,巧妙地利用 weak 属性判断 UIImage 是否被引用问题,为我们开发提供了值得借鉴的思路。

    链接:https://www.jianshu.com/p/49ceb5f58590

    收起阅读 »

    几句代码轻松拥有扫码功能!

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。简单如斯,你不试试? Come on~ViewfinderVi...
    继续阅读 »

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。

    简单如斯,你不试试? Come on~

    ViewfinderView属性说明

    属性值类型默认值说明
    maskColorcolor#60000000扫描区外遮罩的颜色
    frameColorcolor#7F1FB3E2扫描区边框的颜色
    cornerColorcolor#FF1FB3E2扫描区边角的颜色
    laserColorcolor#FF1FB3E2扫描区激光线的颜色
    labelTextstring扫描提示文本信息
    labelTextColorcolor#FFC0C0C0提示文本字体颜色
    labelTextSizedimension14sp提示文本字体大小
    labelTextPaddingdimension24dp提示文本距离扫描区的间距
    labelTextWidthdimension提示文本的宽度,默认为View的宽度
    labelTextLocationenumbottom提示文本显示位置
    frameWidthdimension扫码框宽度
    frameHeightdimension扫码框高度
    laserStyleenumline扫描激光的样式
    gridColumninteger20网格扫描激光列数
    gridHeightinteger40dp网格扫描激光高度,为0dp时,表示动态铺满
    cornerRectWidthdimension4dp扫描区边角的宽
    cornerRectHeightdimension16dp扫描区边角的高
    scannerLineMoveDistancedimension2dp扫描线每次移动距离
    scannerLineHeightdimension5dp扫描线高度
    frameLineWidthdimension1dp边框线宽度
    scannerAnimationDelayinteger20扫描动画延迟间隔时间,单位:毫秒
    frameRatiofloat0.625f扫码框与屏幕占比
    framePaddingLeftdimension0扫码框左边的内间距
    framePaddingTopdimension0扫码框上边的内间距
    framePaddingRightdimension0扫码框右边的内间距
    framePaddingBottomdimension0扫码框下边的内间距
    frameGravityenumcenter扫码框对齐方式

    引入

    Gradle:

    最新版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:2.0.3'

    v1.x 旧版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9-androidx'

    //Android Support 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9'
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的JitPack来compile)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    版本说明

    v2.x 基于CameraX重构震撼发布

    v2.x 相对于 v1.x 的优势

    • v2.x基于CameraX,抽象整体流程,可扩展性更高。
    • v2.x基于CameraX通过预览裁剪的方式确保预览界面不变形,无需铺满屏幕,就能适配(v1.x通过遍历Camera支持预览的尺寸,找到与屏幕最接近的比例,减少变形的可能性(需铺满屏幕,才能适配))

    v2.x 特别说明

    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,那么动态权限申请相关都已经在CaptureActivity或CaptureFragment处理好了。
    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,如果有想要修改默认配置,可重写initCameraScan方法,修改CameraScan的配置即可,如果无需修改配置,直接在跳转原界面的onActivityResult 接收扫码结果即可(更多具体详情可参见app中的使用示例)。
    关于CameraX
    • CameraX暂时还是Beta版,可能会存在一定的稳定性,如果您有这个考量,可以继续使用 ZXingLite 以前的 v1.x 版本。相信不久之后CameraX就会发布稳定版。

    v1.x 说明

    【v1.1.9】 如果您正在使用 1.x 版本请点击下面的链接查看分支版本,当前 2.x 版本已经基于 Camerx 进行重构,不支持升级,请在新项目中使用。

    查看AndroidX版 1.x 分支 请戳此处

    查看Android Support版 1.x 分支 请戳此处

    查看 1.x API帮助文档

    使用 v1.x 版本的无需往下看了,下面的示例和相关说明都是针对于当前最新版本。

    示例

    布局示例

    可自定义布局(覆写getLayoutId方法),布局内至少要保证有PreviewView。

    PreviewView 用来预览,布局内至少要保证有PreviewView,如果是继承CaptureActivity或CaptureFragment,控件id可覆写getPreviewViewId方法自定义

    ViewfinderView 用来渲染扫码视图,给用户起到一个视觉效果,本身扫码识别本身没有关系,如果是继承CaptureActivity或CaptureFragment,控件id可复写getViewfinderViewId方法自定义,默认为previewView,返回0表示无需ViewfinderView

    ivFlashlight 用来内置手电筒,如果是继承CaptureActivity或CaptureFragment,控件id可复写getFlashlightId方法自定义,默认为ivFlashlight。返回0表示无需内置手电筒。您也可以自己去定义

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <com.king.zxing.ViewfinderView
    android:id="@+id/viewfinderView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <ImageView
    android:id="@+id/ivFlashlight"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:src="@drawable/zxl_flashlight_selector"
    android:layout_marginTop="@dimen/zxl_flashlight_margin_top" />
    </FrameLayout>

    或在你的布局中添加

        <include layout="@layout/zxl_capture"/>

    代码示例 (二维码/条形码)

        //跳转的默认扫码界面
    startActivityForResult(new Intent(context,CaptureActivity.class),requestCode);

    //生成二维码
    CodeUtils.createQRCode(content,600,logo);
    //生成条形码
    CodeUtils.createBarCode(content, BarcodeFormat.CODE_128,800,200);
    //解析条形码/二维码
    CodeUtils.parseCode(bitmapPath);
    //解析二维码
    CodeUtils.parseQRCode(bitmapPath);

    CameraScan配置示例

        //获取CameraScan,扫码相关的配置设置。CameraScan里面包含部分支持链式调用的方法,即调用返回是CameraScan本身的一些配置建议在startCamera之前调用。
    getCameraScan().setPlayBeep(true)//设置是否播放音效,默认为false
    .setVibrate(true)//设置是否震动,默认为false
    .setCameraConfig(new CameraConfig())//设置相机配置信息,CameraConfig可覆写options方法自定义配置
    .setNeedAutoZoom(false)//二维码太小时可自动缩放,默认为false
    .setNeedTouchZoom(true)//支持多指触摸捏合缩放,默认为true
    .setDarkLightLux(45f)//设置光线足够暗的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .setBrightLightLux(100f)//设置光线足够明亮的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .bindFlashlightView(ivFlashlight)//绑定手电筒,绑定后可根据光线传感器,动态显示或隐藏手电筒按钮
    .setOnScanResultCallback(this)//设置扫码结果回调,需要自己处理或者需要连扫时,可设置回调,自己去处理相关逻辑
    .setAnalyzer(new MultiFormatAnalyzer(new DecodeConfig()))//设置分析器,DecodeConfig可以配置一些解码时的配置信息,如果内置的不满足您的需求,你也可以自定义实现,
    .setAnalyzeImage(true)//设置是否分析图片,默认为true。如果设置为false,相当于关闭了扫码识别功能
    .startCamera();//启动预览


    //设置闪光灯(手电筒)是否开启,需在startCamera之后调用才有效
    getCameraScan().enableTorch(torch);

    CameraScan配置示例(只需识别二维码的配置示例)

            //初始化解码配置
    DecodeConfig decodeConfig = new DecodeConfig();
    decodeConfig.setHints(DecodeFormatManager.QR_CODE_HINTS)//如果只有识别二维码的需求,这样设置效率会更高,不设置默认为DecodeFormatManager.DEFAULT_HINTS
    .setFullAreaScan(false)//设置是否全区域识别,默认false
    .setAreaRectRatio(0.8f)//设置识别区域比例,默认0.8,设置的比例最终会在预览区域裁剪基于此比例的一个矩形进行扫码识别
    .setAreaRectVerticalOffset(0)//设置识别区域垂直方向偏移量,默认为0,为0表示居中,可以为负数
    .setAreaRectHorizontalOffset(0);//设置识别区域水平方向偏移量,默认为0,为0表示居中,可以为负数

    //在启动预览之前,设置分析器,只识别二维码
    getCameraScan()
    .setVibrate(true)//设置是否震动,默认为false
    .setAnalyzer(new MultiFormatAnalyzer(decodeConfig));//设置分析器,如果内置实现的一些分析器不满足您的需求,你也可以自定义去实现

    如果直接使用CaptureActivity需在您项目的AndroidManifest中添加如下配置

        <activity
    android:name="com.king.zxing.CaptureActivity"
    android:screenOrientation="portrait"
    android:theme="@style/CaptureTheme"/>

    快速实现扫码有以下几种方式:

    1、直接使用CaptureActivity或者CaptureFragment。(纯洁的扫码,无任何添加剂)

    2、通过继承CaptureActivity或者CaptureFragment并自定义布局。(适用于大多场景,并无需关心扫码相关逻辑,自定义布局时需覆写getLayoutId方法)

    3、在你项目的Activity或者Fragment中实例化一个CameraScan即可。(适用于想在扫码界面写交互逻辑,又因为项目架构或其它原因,无法直接或间接继承CaptureActivity或CaptureFragment时使用)

    4、继承CameraScan自己实现一个,可参照默认实现类DefaultCameraScan,其它步骤同方式3。(扩展高级用法,谨慎使用)

    其他

    需使用JDK8+编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    代码下载:ZXingLite.zip

    收起阅读 »