注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android-Jetpack-Hilt 组件 包爽攻略

Hilt 是啥? Hilt 就是依赖Dagger2 而来的 一个 专属android 端的 依赖注入框架。Dagger2 是啥? Dagger是以前 square 做的 依赖注入框架,但是大量使用了反射,谷歌觉得这东西不错,拿来改了一下,使用编译期注解 大幅度...
继续阅读 »

Hilt 是啥?


Hilt 就是依赖Dagger2 而来的 一个 专属android 端的 依赖注入框架。Dagger2 是啥?
Dagger是以前 square 做的 依赖注入框架,但是大量使用了反射,谷歌觉得这东西不错,拿来改了一下,使用编译期注解 大幅度提高性能以后 的东西就叫Dagger2了, 国外的app 多数都用了Dagger2, 但是这个框架在国内用的人很少。


依赖注入是啥?


说简单一点,如果你构造一个对象所需要的值 是别人给你的,那就叫依赖注入,如果是你自己new出来的,那就不叫依赖注入


class A1 {
public A1(String name) {
this.name = name;
}

private String name;

}

class A2 {
public A2() {
this.name = "wuyue";
}

private String name;

}
复制代码

例如上面的, A1 这个类 构造函数的时候 name的值 是外面传过来的,那这个A1对象的构建过程 就是依赖注入,因为你A1对象 是依赖 外部传递过来的值


再看A2 A2的构造函数 是直接 自己 new出来 赋值的。那自然就不叫依赖注入了。


所以依赖注入 对于大部分人来说 其实每天都在用。


既然每天都在用 那用这些依赖注入的框架有啥用?


这是个好问题, 依赖注入的技术既然每天都在用,为啥我们还要用 这些什么Dagger2 Hilt 之类的依赖注入框架呢? 其实原因就是 你用了这些所谓的依赖注入框架 可以让你少写很多代码,且变的很容易维护


你在构造一个对象的时候 如果是手动new 出来的,那么如果日后这个对象的构造方法发生了改变,那么你所有new
的地方 都要挨个修改,这岂不是很麻烦? 如果有依赖注入框架帮你处理 那你其实只要改一个地方就可以了。


第一个简单的例子


在这个例子中,我们熟悉一下Hilt的基本用法。


首先在root project 中的 dependencies 加入依赖


 classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
复制代码

然后在你的app工程中


apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
复制代码

自定义Application 注意要加注解了。


@HiltAndroidApp
class MyApplication:Application() {

}
复制代码

首先定义一个class


data class Person constructor(val name: String, val age: Int) {
@Inject
constructor() : this("vivo", 18)
}
复制代码

注意 这个class 中 使用了 Inject注解 其实就是告诉 Hilt 如何来提供这个对象


然后写我们的activity 页面


class MainActivity : AppCompatActivity() {

@Inject
lateinit var person: Person

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.v("wuyue", "person:$person")
}
}
复制代码

注意 要加入 @AndroidEntryPoint 这个注解,同样的你 声明person对象的时候也一样 要使用Inject注解。


这就是一个最简单的Hilt的例子


好处是显而易见的, 比方说 以后Person的使用 可以不用那么写了,直接Inject 就可以 我压根不用关心
这个Person对象是怎么被构造出来的,以后构造函数发生了改变 调用的地方 也不用修改代码。


当然了,这里有人会说, 你这我虽然明白了优点,但是实际android编程中 没人这么用呀,


有没有更好的例子呢?


获取 Retrofit/Okhttp 对象


通常来说,我们一个项目里面,总会有网络请求,这些网络请求 都会有一些 基础的Retrofit或者是Okhttp的对象, 我们很多时候都会写成单例 然后去get他们出来, 有没有更简便的写法?
有的


//retrofit的 service
interface BaiduApiService{

}

@Module
@InstallIn(ActivityComponent::class)
object BaiduApiModule{

@Provides
fun provideBaiduService():BaiduApiService{
return Retrofit.Builder().baseUrl("https://baidu.com").build().create(BaiduApiService::class.java)
}
}
复制代码

然后:


@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var baiduApiService: BaiduApiService

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

}
}

复制代码

即可。仔细体会体会 这种依赖注入框架的 写法 是不是比你之前 单例的写法要简洁方便了很多?


这里解释一下几个注解的含义


@Module 多数场景下 用来提供构造 无法使用@Inject的依赖,啥意思?


第一个例子中 Person 这个class 是我们自己写的吧,构造函数 前面 我们可以加入Inject 注解


但是例如像Retrofit这样的第三方库 ,我们拿不到他们的代码呀, 又想用 Hilt,怎么办呢


自然就是这个Module了,另外用module 的 时候,一般还要配合使用InstallIn
注解,后面跟的参数值 是用来指定module的范围的


可以看下有多少个范围


image.png


最后 就是 @Provides 这个注解, 这个很简单


一般也是用来 和@Module 一起配合的。 你哪个函数 提供了依赖注入 你就在这个函数上加入这个注解就可以了。


多对象 细节不同 怎么处理


举个例子 一个项目里面 可以有多个okhttp的client对吧,有的接口 我们要做一个拦截器 比如说打印一些埋点,
有些接口 我们要做一个拦截器 来判断下登录态是否失效,不一样的场景,我们需要 new不同的okhttp client
那有没有更简便的写法,答案是有的!


@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DataReportsOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class CheckTokenOkHttpClient
复制代码

我们先用Qualifier 限定符 来标记一下


@Module
@InstallIn(ActivityComponent::class)
object OkHttpModule {
@DataReportsOkHttpClient
@Provides
fun provideDataReportInterceptorOkHttpClient(
dataReportInterceptor: DataReportInterceptor
): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(dataReportInterceptor).build()
}

@CheckTokenOkHttpClient
@Provides
fun provideCheckTokenInterceptorOkHttpClient(
checkTokenInterceptor: CheckTokenInterceptor
): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(checkTokenInterceptor).build()
}
}
复制代码

然后这里 provides的方法 注意了 要加上我们前面的我们先用Qualifier 标记, @DataReportsOkHttpClient


但是到这里还没完,这里一定注意一个原则:


使用Hilt的依赖注入组件 他自己的依赖 也必须是Hilt提供的,啥意思?


你看这里 我们2个provide 方法都需要一个参数 这个参数是干嘛的?就是函数参数 是一个okhttp的interceptor
对吧 ,


但是因为我们这里是依赖注入的模块,所以你使用的参数也必须是依赖注入提供的,


所以这里你如果拦截器 这么写:



class DataReportInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}

}
复制代码

那是编译不过的,因为Hilt组件 不知道你这个对象 应该如何去哪里构造,所以这里你必须也指定 这个拦截器的构造 是Hilt 注入的。


所以你只要这么改就可以了:


class DataReportInterceptor  @Inject constructor() : Interceptor {
init {
}

override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}

}

class CheckTokenInterceptor @Inject constructor() : Interceptor {

init {
}

override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}

}


复制代码

这样Hilt 就知道要去哪里 取这个依赖了。(这个地方官方文档竟然没有提到,导致很多人照着官方文档写demo 一直报错)


一些小技巧


android中 构造很多对象 都需要Context,Hilt 默认为我们实现了这种Context,不需要我们再费尽心思 构造Context了(实际上你也很难构造处理 因为Context 是无法 new出来的)


class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
复制代码

对应的当然还有Application的Context


此外 我们还可以限定 这些依赖注入对象的 作用域


image.png


大家有兴趣可以去官网查看一下。很简单,就不演示了。


其实就是 你对象的作用与 如果是Activity 那么 fragment和view 肯定可以获取到 并且共享他的状态


能理解Activity》Fragment》View 那就很容易理解了。


到底为啥要用Hilt呀


我们学了前面的基础例子以后 一定要把这个问题想明白,否则这个框架你是无法真正理解的,理解他以后 才能用得好。


Hilt要解决的问题就是:


在android开发中,我们太多的场景是干啥?是在Activity里面 构造对象,而这些对象我们是怎么构建出来的?


大部分人都是New出来的对吧,但是这些New出来的对象 所属的Class 一旦发生了构造函数的变更,


我们还得去找出所有 引用这个Class 的 地方 11 去修改 调用方法。 这个就很不方便了。


回顾下前面我们的例子,使用Hilt的话 可以极大避免这种场景。


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

收起阅读 »

iOS多张图片合成一张

在我们的开发过程中,有时候会遇到不同的需求,比如将不同的图片合成一张图片下边是实现代码:#import "RootViewController.h"@interface RootViewController ()@end@implementation Root...
继续阅读 »

在我们的开发过程中,有时候会遇到不同的需求,比如将不同的图片合成一张图片


下边是实现代码:

#import "RootViewController.h"

@interface RootViewController ()

@end

@implementation RootViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.

NSArray *imgArray = [[NSArray alloc] initWithObjects:
[UIImage imageNamed:@"1.jpg"],
[UIImage imageNamed:@"2.jpg"],
[UIImage imageNamed:@"3.jpg"],
[UIImage imageNamed:@"4.jpg"],
[UIImage imageNamed:@"5.jpg"],
nil];

NSArray *imgPointArray = [[NSArray alloc] initWithObjects:
@"10", @"10",
@"10", @"25",
@"30", @"15",
@"30", @"50",
@"20", @"80",
nil];


BOOL suc = [self mergedImageOnMainImage:[UIImage imageNamed:@"1.jpg"] WithImageArray:imgArray AndImagePointArray:imgPointArray];

if (suc == YES) {
NSLog(@"Images Successfully Mearged & Saved to Album");
}
else {
NSLog(@"Images not Mearged & not Saved to Album");
}

}
#pragma -mark -functions
//多张图片合成一张
- (BOOL) mergedImageOnMainImage:(UIImage *)mainImg WithImageArray:(NSArray *)imgArray AndImagePointArray:(NSArray *)imgPointArray
{

UIGraphicsBeginImageContext(mainImg.size);

[mainImg drawInRect:CGRectMake(0, 0, mainImg.size.width, mainImg.size.height)];
int i = 0;
for (UIImage *img in imgArray) {
[img drawInRect:CGRectMake([[imgPointArray objectAtIndex:i] floatValue],
[[imgPointArray objectAtIndex:i+1] floatValue],
img.size.width,
img.size.height)];

i+=2;
}

CGImageRef NewMergeImg = CGImageCreateWithImageInRect(UIGraphicsGetImageFromCurrentImageContext().CGImage,
CGRectMake(0, 0, mainImg.size.width, mainImg.size.height));

UIGraphicsEndImageContext();
if (NewMergeImg == nil) {
return NO;
}
else {
UIImageWriteToSavedPhotosAlbum([UIImage imageWithCGImage:NewMergeImg], self, nil, nil);
return YES;
}
}



- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

@end

转自:https://www.cnblogs.com/gchlcc/p/6774420.html

收起阅读 »

Kotlin实战---使用Room封装本地数据层

没有Kotlin基础的小伙伴先进这里→ Koltin基础文章 Kotlin网络模型的实现→ Kotlin网络模型的实现 kotlin实战---MVP模式实现登录,实现Base层封装→ kotlin实战---MVP模式实现登录,实现Base层封装 1、为什么使用...
继续阅读 »

没有Kotlin基础的小伙伴先进这里→
Koltin基础文章


Kotlin网络模型的实现→
Kotlin网络模型的实现


kotlin实战---MVP模式实现登录,实现Base层封装→
kotlin实战---MVP模式实现登录,实现Base层封装


1、为什么使用Room


Room 是一个 SQLite 对象映射库。它可用来避免样板代码,还可以轻松地将 SQLite 表数据转换为 Java 对象。Room 提供 SQLite 语句的编译时检查,并且可以返回 RxJava、Flowable 和 LiveData 可观察对象,使用ROOM可以让你更简单,更流畅的操作数据库,使用简单通过注解的方式就能对数据库进行增删改查,Google工程师帮你封装了访问SqlLite的代码,使你的代码性能更高


2、数据库的封装


先来一个图,理清思路再看代码


在这里插入图片描述


2.1、LocalRoomRequestManager


接口层实现,类似于网络模块里的API,将操作SqlLite的接口写到这里边


/***
* 数据库获取标准接口,数据库读取
* 只为 LocalRoomRequestManager 服务
* DB 数据
*/
interface IDatabaseRequest {
fun insertStudents(vararg students: Student)

fun updateStudents(vararg students: Student)

fun deleteStudents(vararg students: Student)

fun deleteAllStudent()

fun queryAllStudent() : List<Student> ?

// TODO 可扩展 ...
}

/**
* 为了扩展,这样写(在仓库里面的)
* 本地获取标准接口(在仓库里面) 也就是本地的数据读取(包括本地xml数据,等)
* 只为 LocalRoomRequestManager 服务
*
* xml 数据 本地数据
*/
interface ILocalRequest {
}
复制代码

LocalRoomRequestManager类的实现,初始化的通过dataBase层获取dao,然后通过dao层进行增删改查


class LocalRoomRequestManager :ILocalRequest,IDatabaseRequest{
var studentDao:StudentDao?=null
//相当于Java代码的构造代码块
init{
val studentDatabase=StudentDatabase.getDataBase()
studentDao=studentDatabase?.getStudentDao()
}
companion object{
var INSTANCE: LocalRoomRequestManager? = null

fun getInstance() : LocalRoomRequestManager {
if (INSTANCE == null) {
synchronized(LocalRoomRequestManager::class) {
if (INSTANCE == null) {
INSTANCE = LocalRoomRequestManager()
}
}
}
return INSTANCE!!
}
}
override fun updateStudents(vararg students: Student) {
studentDao?.updateStudents(*students)
}

override fun deleteStudents(vararg students: Student) {
studentDao?.deleteStudent(*students)
}

override fun deleteAllStudent() {
studentDao?.deleteAllStudent()
}

override fun queryAllStudent(): List<Student>? {
return studentDao?.queryAllStudents()
}

override fun insertStudents(vararg students: Student) {
studentDao?.insertStudents(*students)
}

}
复制代码

2.2、Room操作


真正用来操作数据库的代码


初始化数据库


@Database(entities = [Student::class],version = 1)
abstract class StudentDatabase: RoomDatabase() {
abstract fun getStudentDao():StudentDao

companion object{
private var INSTANCE:StudentDatabase?=null
//Application 调用
fun getDatabase(context: Context):StudentDatabase?{
if(INSTANCE==null){
INSTANCE=Room.databaseBuilder(context,StudentDatabase::class.java,"student_database.db")
.allowMainThreadQueries()//允许在主线程查询
.build()
}
return INSTANCE
}
//使用者调用
fun getDataBase():StudentDatabase?= INSTANCE
}

}
复制代码

在Application里去初始化database


class MyApplication : Application() {

override fun onCreate() {
super.onCreate()

// 初始化
StudentDatabase.getDatabase(this)
}

}
复制代码

Room.databaseBuilde 就是实例化的DataBase的实现类
实现类里的代码:
在这里插入图片描述
这些都是框架生成的代码,省去了我们许多的样板代码
Dao层和Entity实现


@Dao
interface StudentDao {
/***
* 可变参数,插入数据
*/
@Insert
fun insertStudents(vararg students:Student)
//更新数据
@Update
fun updateStudents(vararg students:Student)

//根据条件删除
@Delete
fun deleteStudent(vararg students:Student)
//删除全部
@Query("delete from student")
fun deleteAllStudent()
//查询全部
@Query("SELECT * FROM student ORDER BY ID DESC")
fun queryAllStudents():List<Student>

}

@Entity
class Student(){
@PrimaryKey(autoGenerate = true)//设置为主键,自动增长
var id:Int=0
@ColumnInfo(name="name")//别名 数据库中的名字如果不设置,默认是属性名称
lateinit var name:String
@ColumnInfo(name ="phoneNumber")
lateinit var phoneNumber:String
//次构造
constructor(name:String,phoneNumber:String): this(){
this.name=name
this.phoneNumber=phoneNumber
}
}
复制代码

框架生成的代码,大家可以自己去看一下,里面自动添加了事务,也加了锁,非常的nice
写完这些再去用MVP把LocalRoomRequestManager和Model层连起来,MVP上一篇贴的很详细了,这次的就不贴了
Kotlin版的适配器写法


class CollectAdapter :RecyclerView.Adapter<CollectAdapter.MyViewHolder>() {
// 接收 数据库的数据
var allStudents: List<Student> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
val itemView: View = layoutInflater.inflate(R.layout.item_collect_list, parent, false)
return MyViewHolder(itemView)
}

override fun getItemCount(): Int =allStudents.size

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val student: Student = allStudents[position]
holder.tvID.text = "${position + 1}"
holder.tvName.text = student.name
holder.tvPhoneNumber.text = "${student.phoneNumber}"
}

inner class MyViewHolder(itemView: View):RecyclerView.ViewHolder(itemView){
var tvID: TextView = itemView.findViewById(R.id.tv_id)
var tvName: TextView = itemView.findViewById(R.id.tv_name)
var tvPhoneNumber: TextView = itemView.findViewById(R.id.tv_phoneNumber)
}

}
复制代码

最终的效果


在这里插入图片描述


3、总结


需要注意的点在可变参数的传递过程中,不能将参数直接丢给方法得加一个*
LocalRoomRequestManager.getInstance().insertStudents(*students)
体验用Kotlin开发项目的感觉,感觉比Java好用很多,还是很nice的,作为官方直推的语言还是挺值得学习的,


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

你的Android库是否还在Application中初始化?

通常来说,当我们引入一个第三方库,第一件要做的事情是在Application中的onCreate传入context初始化这个库 😞。但是为什么像一些库如Firebase🔥,初始化的时候并不需要在Application中初始化呢?今天我们就来探索一下这个问题 🧐...
继续阅读 »

通常来说,当我们引入一个第三方库,第一件要做的事情是在Application中的onCreate传入context初始化这个库 😞。但是为什么像一些库如Firebase🔥,初始化的时候并不需要在Application中初始化呢?今天我们就来探索一下这个问题 🧐


Android库的初始化


举个栗子,我们需要在app中国呢使用ARouter,在使用前需要初始化传入context,因此如果没有application时我们要创建一个:


class MainApplication : Application() {
override fun onCreate(){
super.onCreate()
ARouter.init(mApplication);
}
}
复制代码

然后要在清单文件 AndroidManifest.xml 中声明才会执行 :


<application
android:name=".MainApplication"
...
复制代码

更多库怎么办


现在想象我们使用了ARouter,友盟统计,Realm,ToastUtils等库时,我们的application可能会是如下形式:


class MainApplication : Application() {
override fun onCreate(){
super.onCreate()
ARouter.init(this)
UMConfigure.init(this,...)
Realm.init(this)
ToastUtils.init(this)
}
}
复制代码

在项目中, 仅仅为了初始化一些库,我就必须得新建Application并且在onCreate中调用它。(译者:也许你认为这也没什么,但是如果你自己创建了多个库需要context时,你每次得预留一个init方法暴露给调用者,使用时又得在application初始化。)



Useless


无需“初始化”的库


如果你的项目加入了Firebase 🔥, 你会发现它并没有要求初始化, 你只要使用它 :


这个数据库访问没有需要context的传入,通过离线访问存储本地。可以猜测它有一个机制获取上下文application context,自动完成初始化。


ContentProvider & Manifest-Merger


developer.android.com/studio/buil…



你的Apk文件只包含一个清单文件AndroidManifest.xml,但是你的Android Studio项目可能会有多个源集(main source set),构建变体(build variants),导入的库(imported libraries)构成。因此在编译构建app时,gradle插件会将多个manifest文件合并到一个清单文件中去。



我们可以看下合并后的清单文件(目录如下):



app/build/intermediates/merged_manifests/MY_APP/processMY_APPDebugManifest/merged/AndroidManifest.xml



我们可以发现一个关于Firebase库的provider被引入到清单文件中:


使用Android Studio点击打开FirebaseInitProvider, 我们知道了这个provider通过this.getContext()来访问上下文。

内容提供者ContentsProviders会直接在Application创建后完成初始化,因此通过它来完成library的初始化不失为一个好办法。


自动初始化我们的库


如果我们自定义了ToastUtils库需要初始化,我们自己提供一个Provider :


class ToastInitProvider : ContentProvider() {

override fun onCreate(): Boolean {
ToastUtils.init(context)
return true
}
...
}
复制代码

然后这个库中的AndroidManifest.xml中加入它


<provider
android:name=".ToastInitProvider"
android:authorities="xxx.xx.ToastInitProvider" />
复制代码

然后当我们使用这个ToastUtils库时,无需添加额外的代码在项目中初始化它😎,直接使用它即可:


ToastUtils.show("this is toast")
复制代码
Stetho.getInstance().configure(…)
复制代码

删除Application


如果一些库没有使用InitProviders,我们可以创建它:


class ARouterInitProvider : ContentProvider() {

override fun onCreate(): Boolean {
ARouter.init(this)
return true
}
...
}
class RealmInitProvider : ContentProvider() {

override fun onCreate(): Boolean {
Realm.init(this)
return true
}
...
}
复制代码

然后加入到清单文件AndroidManifest中 :


<provider
android:name=".ARouterInitProvider"
android:authorities="${applicationId}.ARouterInitProvider" />
<provider
android:name=".RealmInitProvider"
android:authorities="${applicationId}.RealmInitProvider" />
复制代码

现在我们可以 移除 这个 MainApplication


Happy Dance


项目地址


https://github.com/florent37/ApplicationProvider

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

引入Jetpack架构后,你的App会发生哪些变化?

前言 上篇文章我给大家分享了我对Android架构的理解,从思想层面去讲述架构的演进过程。很多小伙伴读完后拍手叫好,表示还想听我讲一下对Jetpack 架构的看法,本着帮人帮到底的精神,今天我将再次动笔 尽量从本质上讲清楚Jetpack 架构存在的意义,以及解...
继续阅读 »

前言


上篇文章我给大家分享了我对Android架构的理解,从思想层面去讲述架构的演进过程。很多小伙伴读完后拍手叫好,表示还想听我讲一下对Jetpack 架构的看法,本着帮人帮到底的精神,今天我将再次动笔 尽量从本质上讲清楚Jetpack 架构存在的意义,以及解决的问题。


同时我也有一个基于Jetpack MVVM的完整开源项目,已经按照上篇文章提出的思想做了重构,目前托管在Github,希望也能为你提供一些帮助。github地址


知识储备:需要对Lifcycle、LiveData、ViewModel、DataBinding有基本了解


目录



  • 1. 有了Lifecycle,再也不用担心生命周期同步问题

    • 1.1 为什么要做生命周期绑定?

    • 1.2 Lifecycle解决了哪些问题?



  • 2. LiveData并不是只运用观察者模式

    • 2.1 观察者模式的优点有哪些?

    • 2.2 LiveData基于观察者模式又做了哪些扩展?

    • 2.3 LiveData + Lifecycle 实现 1 + 1 > 2



  • 3. ViewModel与LiveData真乃天作之合

    • 3.1 如何优雅的实现Fragment之间通讯?

    • 3.2 由ViewModel担任 VM/Presenter 的好处有哪些?



  • 4. 解除你对DataBinding的误解

    • 4.1 使用DataBinding的好处有哪些?

    • 4.2 为什么很多人说DataBinding很难调试?



  • 5. Jetpack和MVVM有什么关系?

    • 5.1 什么是MVVM

    • 5.2 Jetpack只是让MVVM更简单、更安全




1. 有了Lifecycle,再也不用担心生命周期同步问题


1.1 为什么要做生命周期绑定?


关于Activity/Fragment其最重要的概念就是生命周期管理,我们开发者需要在不同生命周期回调中做不同事情。比如onCreate做一些初始化操作,onResume做一些恢复操作等等等等,以上这些操作都比较单一直接去写也没有多大问题。


但有一些组件需要强依赖于Activity/Fragment生命周期,常规写法一旦疏忽便会引发安全问题,比如下面这个案例:


现有一个视频播放界面,我们需要做到当跳到另一个界面就暂停播放,返回后再继续播放,退出后重置播放,常规思路:


#class PlayerActivity
onCreate(){
player.init()
}
onResume(){
player.resume()
}
onPause(){
player.pause()
}
onDestroy(){
player.release()
}
复制代码

读过我上篇文章的小伙伴可能一眼就能看出来这违背了控制反转,人不是机器很容易写错或者忘写,特别是player.release()如果忘写便会引发内存泄漏
此时我们可以基于控制反转思想(将player生命周期控制权交给不会出错的框架)进行改造:
第一步:


interface ObserverLifecycle{
onCreate()
...
onDestroy()
}
复制代码

首先定义一个观察者接口,包含Activity/Fragment主要生命周期方法


第二步:


class BaseActivity{
val observers = mutableList<ObserverLifecycle>()
onCreate(){
observers.forEach{
observer.onCreate()
}
}
...
onDestroy(){
observers.forEach{
observer.onDestroy()
}
}
}
复制代码

BaseActivity中观察生命周期并逐一通知到observers的观察者


第三步:


class VideoPlayer : ObserverLifecycle{
onCreate(){
init()
}
...
onDestroy(){
release()
}
}
class PlayerActivity : BaseActivity{
observers.add(videoPlayer)
}
复制代码

播放器实现ObserverLifecycle接口,并在每个时机调用相应方法。PlayerActivity只需将videoPlayer注册到observers即可实现生命周期同步。


其实不光videoPlayer,任何需要依赖Activity生命周期的组件 只需实现ObserverLifecycle接口最后注册到Activityobservers即可实现生命周期自动化管理,进而可以规避误操作带来的风险


1.2 Lifecycle解决了哪些问题?


既然生命周期的同步如此重要,Google肯定不会视而不见,虽然自定义ObserverLifecycle可以解决这种问题,但并不是每个人都能想到。所以Google就制定了一个标准化的生命周期管理工具Lifecycle,让开发者碰到生命周期问题自然而然的想到Lifecycle,就如同想在Android手机上新建一个界面就会想到Activity一样。


同时ActivityFragment内部均内置了Lifecycle,使用非常简单,以1.1 案例通过Lifecycle改造后如下:


class VideoPlayer : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate(){
init()
}
..
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(){
release()
}
}
class PlayerActivity : BaseActivity{
lifecycle.addObserver(videoPlayer)
}
复制代码

两步操作即可,不用我们自己向观察者(videoPlayer)做生命周期分发处理。


2. LiveData并不是只运用观察者模式


2.1 观察者模式的优点有哪些?


观察者是一种常见并且非常实用的一种行为型模式,具有扩展性强、耦合性低的特性。


本文1.1 中 生命周期同步设计就是一个标准的观察者模式,ObserverLifecycle可作为观察者,PlayerActivity作为被观察者,当被观察者(PlayerActivity)生命周期发生改变时会主动通知到观察者(VideoPlayer)


同时观察者在不改变代码结构的情况随意扩展,比如PlayerActivity属于一个MVP架构,此时可以将Presenter实现ObserverLifecycle作为观察者 随后 注册到被观察者(PlayerActivity)中,
这样Presenter也可以监测到Activity生命周期,并且代码结构没有任何改变,符合开闭原则(对扩展开发 修改关闭)


2.2 LiveData基于观察者模式又做了哪些扩展?


LiveData符合标准的观察者模式,所以它具备扩展性强、耦合性低的特性,同样它还是一个存储数据的容器,当容器数据改变时会触发观察者,即数据驱动。


数据驱动是前端开发领域非常重要的一个概念,说数据驱动之前我们先思考一个问题,为什么要改变数据?
答案显而易见,无非是想让数据使用者感知到而已,而LiveData可以优雅的实现这一流程,将 改变、通知 两步操作合并为一步 即省事也提高了安全性.


根据LiveData的特性决定它非常适合去做数据驱动UI,下面举个例子简单描述下:


# 需求:改变textView内容以及对应的数据,用LiveData实现方式如下
val liveData = MutableLiveData<String>()
liveData?.observe(this, Observer { value->
textView.text = value
})
//这一步会改变liveData值并且会触发textView重新渲染
liveData.value = "android"
复制代码

看起来平平无奇甚至理所当然,但它确实解决了我们前端开发的痛点,在此之前数据和UI都需要我们开发者单独修改,当面对十几个View时很难做到不漏不忘。
引入liveData后改变数据会自动触发UI渲染,将两步操作合并为一步,大大降低出错的概率
关于数据驱动UI上篇文章我已经做了详细描述,感兴趣的可以翻回去查看。


2.3 LiveData + Lifecycle 实现 1 + 1 > 2


LiveDataLifecycle的加持下可以实现只在可见状态接收通知,说的通俗一点Activity执行了onStop()后内部的LiveData就无法收到通知,这样设计有什么好处?
举个例子:
ActivityAActivityB共享同一个LiveData,伪代码如下


class ActivityA{
liveData?.observe(this, Observer { value->
textView.text = value
})
}
class ActivityB{
liveData?.observe(this, Observer { value->
textView.text = value
})
}
复制代码

ActivityA启动ActivityB后多次改变liveData值,等回到ActivityA时 你肯定不希望Observer收到多次通知而引发textView多次重绘。


引入Lifecycle后这个问题便可迎刃而解,liveData绑定Lifecycle(例子中的this)后,当回到ActivityA时只会取liveData最新的值然后做通知,从而避免多余的操作引发的性能问题


3. ViewModel与LiveData真乃天作之合


3.1 Jetpack ViewModel 并不等价于 MVVM ViewModel


经常有小伙伴将Jetpack ViewModelMVVM ViewModel,其实这二者根本没有在同一个层次,MVVM ViewModelMVVM架构中的一个角色,看不见摸不着只是一种思想。
Jetpack ViewModel是一个实实在在的框架用于做状态托管,有对应的作用域可跟随Activity/Fragment生命周期,但这种特性恰好可以充当MVVM ViewModel的角色,分隔数据层和视图层并做数据托管。


所以结论是Jetpack ViewModel可以充当MVVM ViewModel 但二者并不等价


3.2 如何优雅的实现Fragment之间通讯?


ViewModel官方定义是一个带作用域的状态托管框架,为了将其状态托管发挥到极致,Google甚至单独为ViewModel开了个后门,Activity横竖屏切换时不会销毁对应的ViewModel,为的就是横竖屏能共用同一个ViewModel,从而保证数据的一致性。


既然是状态托管框架那ViewModel的第一要务 就要时时刻刻保证最新状态分发到视图层,这让我不禁想到了LiveData,数据的承载以及分发交给Livedata,而ViewModel专注于托管LiveData保证不丢失,二者搭配简直是天作之合。


有了ViewModelLiveDataFragment之间可以更优雅的通讯。比如我的开源项目中的音乐播放器(属于单Activity多Fragment架构下),播放页和首页悬浮都包含音乐基本自信,如下图所示:


image.png
想要使两个Fragment中播放信息实时同步,最优雅的方式是将播放状态托管在Activity作用域下ViewModelLiveData中,然后各自做状态监听,这样只有要有一方改变就能立即通知到另一方,简单又安全,具体细节可至我的开源项目中查看。


3.3 由ViewModel担任 VM/Presenter 的好处有哪些?


传统MVVMMVP遇到最多的的问题无非就是多线程下的内存泄露,ViewModel可以完全规避这个问题,内部的viewModelScope是一个协程的扩展函数,viewModelScope生命周期跟随ViewModel对应的Lifecycle(Activity/Fragment),当页面销毁时会一并结束viewModelScope协程作用域,所以将耗时操作直接放在viewModelScope即刻


另外在界面销毁时会调用ViewModelonClear方法,可以在该方法做一些释放资源的操作,进一步降低内存泄露的风险


4. 解除你对DataBinding的误解


4.1 使用DataBinding的作用有哪些?


DataBinding最大的优点跟唯一的作用就是数据 UI双向绑定UI和数据修改任何一方另外一方都会自动同步,这样的好处其实跟LiveData的类似,都是做数据跟UI同步操作,用来保证数据和UI一致性。其实写到这可以发现,不管是LiveDataDataBinding还是DiffUtil都是用来解决数据和UI一致性问题,可见Google对这方面有多么重视,所以我们一定要紧跟官方步伐


小知识点:



DataBinding包中的ObservebleFile作用跟LiveData基本一致,但ObservebleFile有一个去重的效果,



4.2 为什么很多人说DataBinding很难调试?


经常听一些小伙伴提DataBinding不好用,原因是要在xml中写业务逻辑不好调试,对于这个观点我是持否定态度的。并不是我同意xml中写业务逻辑这一观点,我觉得碰到问题就得去解决问题,如果解决问题的路上有障碍就尽量扫清障碍,而不是一味的逃避。


{vm.isShow ? View.VISIBLE : View.GONE}之类的业务逻辑不写在xml放在哪好呢?关于这个问题我在上篇文章Data Mapper章节中描述的很清楚,拿到后端数据转换成本地模型(此过程会编写所有数据相关逻辑),本地模型与设计图一一对应,不但可以将视图与后段隔离,而且可以解决xml中编写业务逻辑的问题。


5. Jetpack和MVVM有什么关系?


5.1 什么是MVVM


MVVM其实是前端领域一个专注于界面开发的架构模式,总共分为ViewViewModelRepository三个模块 (需严格按照单一设计原则划分)




  • View(视图层): 专门做视图渲染以及UI逻辑的处理

  • Repository(远程): 代表远程仓库,从Repository取需要的数据

  • ViewModel: Repository取出的数据需暂存到ViewModel,同时将数据映射到视图层



分层固然重要,但MVVM最核心点是通过ViewModel做数据驱动UI以及双向绑定的操作用来解决数据/UI的一致性问题。MVVM就这么些东西,千万不要把它理解的特别复杂


其实我上篇文章也简单说过,好的架构不应该局限到某一种模式(MVC/MVP/MVVM)上,需要根据自己项目的实际情况不断添砖加瓦。如果你们的后端比较善变我建议引入Data Mapper的概念~如果你经常和同事开发同一个界面,可以试图将每一条业务逻辑封装到use case中,这样大概率可以解决Git冲突的问题..等等等等,总之只要能实实在在 提高 开发效率以及项目稳定性的架构就是好架构.


5.2 Jetpack只是让MVVM更简单、更安全


Jetpack是Android官方为确立标准化开发而提供的一套框架,Lifecycle可以让开发者不用过多考虑 生命周期引发的一系列问题 ~ 有了DataBinding的支持让数据UI双向绑定成为了可能 ~ LiveData的存在解除ViewModelActivity双向依赖的问题....


归根到底Jetpack就是一套开发框架,MVVM在这套框架的加持之下变得更加简单、安全。


Tips:作者公司项目引入Jetpack后,项目稳定性有着肉眼可见的提升。


综上所述



  • Lifecycle 解决了生命周期 同步问题

  • LiveData 实现了真正的状态驱动

  • ViewModel 可以让 Fragment 通讯变得更优雅

  • DataBinding 让双向绑定成为了可能

  • Jetpack 只是让 MVVM 更简单、更安全

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

Android系统开发-选择并启动默认Launcher

如果在Android设备上又安装了一个Launcher应用,当我们返回主页的时候,Android就会弹出一个弹窗,要用户 选择要启动的Launcher应用,如下图所示: 这个是普通Android设备的正常流程,现在我们的需求是不再显示这个提示窗,在设置中增加...
继续阅读 »

如果在Android设备上又安装了一个Launcher应用,当我们返回主页的时候,Android就会弹出一个弹窗,要用户 选择要启动的Launcher应用,如下图所示:



这个是普通Android设备的正常流程,现在我们的需求是不再显示这个提示窗,在设置中增加一个选择默认启动Launcher的页面,默认选择Launcher3。


Settings



在设置中增加一个这样的页面,显示所有声明了"android.intent.category.HOME"的应用


 private fun getAllLauncherApps(): MutableList<AppInfo> {
val list = ArrayList<AppInfo>()
val launchIntent = Intent(Intent.ACTION_MAIN, null)
.addCategory(Intent.CATEGORY_HOME)
val intents = packageManager.queryIntentActivities(launchIntent, 0)

//遍历
for (ri in intents) {
//得到包名
val packageName = ri.activityInfo.applicationInfo.packageName
if (packageName == "com.android.settings") { //不显示原生设置
continue
}
//得到图标
val icon = ri.loadIcon(packageManager)
//得到应用名称
val appName = ri.loadLabel(packageManager).toString()

//封装应用信息对象
val appInfo = AppInfo(icon, appName, packageName)
//添加到list
list.add(appInfo)
}
return list
}
复制代码

使用PackageManager提供的queryIntentActivities方法就可以获取所有Launcher应用,原生设置中也有Activity声明了HOME属性,在这里就把它屏蔽掉。


默认选择Launcher3应用为默认启动


private val DEFAULT_LAUNCHER = "my_default_launcher"
defaultLauncher = Settings.Global.getString(contentResolver, DEFAULT_LAUNCHER)
if (defaultLauncher.isNullOrEmpty()) {
defaultLauncher = "com.android.launcher3"
Settings.Global.putString(contentResolver, DEFAULT_LAUNCHER, defaultLauncher)
}
复制代码

当选择另一个应用,就把选择应用的包名设置到 Settings.Global中。


这样应用选择页面完成,也设置了一个全局的参数提供给系统。


启动


最开始提到了Launcher选择弹窗,我们就考虑在这里做点事,把弹窗的逻辑给跳过,就可以实现默认启动。


弹窗源码位于frameworks/base/core/java/com/android/internal/app/ResolverActivity.java


在这里就不具体分析源码了,就看关键部分


public boolean configureContentView(List<Intent> payloadIntents, Intent[] initialIntents,
List<ResolveInfo> rList, boolean alwaysUseOption) {
// The last argument of createAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
// turn this off when running under voice interaction, since it results in
// a more complicated UI that the current voice interaction flow is not able
// to handle.
mAdapter = createAdapter(this, payloadIntents, initialIntents, rList,
mLaunchedFromUid, alwaysUseOption && !isVoiceInteraction());

final int layoutId;
if (mAdapter.hasFilteredItem()) {
layoutId = R.layout.resolver_list_with_default;
alwaysUseOption = false;
} else {
layoutId = getLayoutResource();
}
mAlwaysUseOption = alwaysUseOption;

int count = mAdapter.getUnfilteredCount();
if (count == 1 && mAdapter.getOtherProfile() == null) {
// Only one target, so we're a candidate to auto-launch!
final TargetInfo target = mAdapter.targetInfoForPosition(0, false);
if (shouldAutoLaunchSingleChoice(target)) {
safelyStartActivity(target);
mPackageMonitor.unregister();
mRegistered = false;
finish();
return true;
}
}
if (count > 0) {
// add by liuwei,if set my_default_launcher,start default
String defaultlauncher = Settings.Global.getString(this.getContentResolver(), "my_default_launcher");

final TargetInfo defaultTarget = mAdapter.targetInfoForDefault(defaultlauncher);
if(defaultTarget != null){
safelyStartActivity(defaultTarget);
mPackageMonitor.unregister();
mRegistered = false;
finish();
return true;
}
//end
setContentView(layoutId);
mAdapterView = (AbsListView) findViewById(R.id.resolver_list);
onPrepareAdapterView(mAdapterView, mAdapter, alwaysUseOption);
} else {
setContentView(R.layout.resolver_list);

final TextView empty = (TextView) findViewById(R.id.empty);
empty.setVisibility(View.VISIBLE);

mAdapterView = (AbsListView) findViewById(R.id.resolver_list);
mAdapterView.setVisibility(View.GONE);
}
return false;
}
复制代码

在configureContentView中判断launcher应用个数,如果为1,则直接启动,finish当前页面。下面判断count>0,我们就在这里面增加自己的逻辑,获取配置的Settings.Global参数,再去Adapter中判断是否有应用包名和参数匹配,如果有就safelyStartActivity(),关闭弹窗。如果没有匹配包名,就走正常流程,弹窗提示用户。


mAdapter.targetInfoForDefault函数是在 public class ResolveListAdapter extends BaseAdapter中增加函数


 public TargetInfo targetInfoForDefault(String myDefault){
if(myDefault == null){
return null;
}

TargetInfo info = null;
for(int i=0;i<mDisplayList.size();i++){
String disPackageName = mDisplayList.get(i).getResolveInfo().activityInfo.applicationInfo.packageName;
if(myDefault.equals(disPackageName) ){
info = mDisplayList.get(i);
break;
}
}
return info;
}

复制代码

OK,功能实现完成,自测也没有问题。


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

在 Kotlin 序列化中使用 DataStore

我们之前已经 数据类 非常适合与 DataStore 结合使用,这是因为它们能够与 Kotlin 序列化无缝协作。DataStore 会依赖数据类自动生成的 equals 和 hashCode。数据类也会生成便于调试和更新数据的 toString 和 copy...
继续阅读 »

我们之前已经 分享Proto DataStore 和 Preferences DataStore 的使用方法。这两个 DataStore 版本都会在后台使用 Protos 对数据进行序列化。您也可以使用 Kotlin 序列化,结合使用 DataStore 与自定义数据类。这有助于减少样板代码,且无需学习或依赖于 Protobuf 库,同时仍可以为数据提供架构。


您需要完成以下几项操作:



  • 定义数据类

  • 确保您的数据类不可变

  • 使用 Kotlin 序列化实现 DataStore 序列化器

  • 开始使用


定义数据类


Kotlin 数据类 非常适合与 DataStore 结合使用,这是因为它们能够与 Kotlin 序列化无缝协作。DataStore 会依赖数据类自动生成的 equalshashCode。数据类也会生成便于调试和更新数据的 toStringcopy 函数。


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class UserPreferences(
val showCompleted: Boolean,
val sortOrder: SortOrder
)
复制代码

确保您的数据类不可变


确保您的数据类不可变是非常重要的,这是因为 DataStore 无法兼容可变类型。结合使用可变类型与 DataStore 会导致难以捕获的错误和竞争条件。数据类并非一定不可变。


Vars 是可变的,所以您应使用 vals 代替:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- var num: Int
+ val num: Int
)
- myObj.num = 5 // Fails to compile when num is val
+ val newObj = myObj.copy(num = 5)
复制代码

数组是可变的,所以您不应将其公开。


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- var num: IntArray
)
- myObj.num = 5 // This would mutate your object
复制代码

即使将只读列表用作数据类的一部分,该数据类也仍为可变的。您应考虑改用 不可变/持久化集合:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- val nums: List<Int>
+ val nums: PersistentList<Int>
)

- val myInts = mutableListOf(1, 2, 3, 4)
- val myObj = MyData(myInts)
- myInts.add(5) // Fails to compile with PersistentList, but mutates with List
+ val newData = myObj.copy(
+ nums = myObj.nums.mutate { it += 5 } // Mutate returns a new PersistentList
+ )
复制代码

将可变类型用作数据类的一部分会令数据类变为可变状态。您不应采取上述做法,反而要确保所有内容都是不可变类型。


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- val mutableType: MutableType
)

- val myType = MutableType()
- val myObj = MyData(myType)
- myType.mutate()
复制代码

实现 DataStore 序列化器


Kotlin 序列化支持包括 JSON 和协议缓冲区在内的 多种格式。我将在此处使用 JSON,因为它十分常见、易于使用且会以明文形式进行存储,便于调试。Protobuf 也是一个不错的选择,因为它规模更小、速度更快且兼容 protobuf-lite


要使用 Kotlin 序列化读取数据类并将其写入 JSON,您需要使用 @Serializable 注释数据类并使用 Json.decodeFromString<YourType>(string)Json.encodeToString(data)。以下是带有 UserPreferences 的示例:


 /* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

@Serializable
data class UserPreferences(
val showCompleted: Boolean = false,
val sortOrder: SortOrder = SortOrder.None
)

object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue = UserPreferences()

override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return Json.decodeFromString(
UserPreferences.serializer(), input.readBytes().decodeToString())
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read UserPrefs", serialization)
}
}

override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
output.write(Json.encodeToString(UserPreferences.serializer(), t).encodeToByteArray())
}
}
复制代码

⚠️ 将 Parcelables 与 DataStore 一起使用并不安全,因为不同 Android 版本之间的数据格式可能会有所变化。


使用序列化器


在您构建时,将您创建的序列化器传递到 DataStore:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

val Context.dataStore by dataStore("my_file.json", serializer = UserPreferencesSerializer)

复制代码

其读取数据看起来与使用 protos 进行读取一样:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

suspend fun getShowCompleted(): Boolean {
context.dataStore.data.first().showCompleted
}
复制代码

您可以使用生成的 .copy() 函数更新数据:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

suspend fun setShowCompleted(newShowCompleted: Boolean) {
// This will leave the sortOrder value untouched:
context.dataStore.updateData { it.copy(newShowCompleted = showCompleted) }
}
复制代码

总结


结合使用 DataStore 与 Kotlin 序列化和数据类可减少样板文件并有助于简化代码,但您必须多加小心,避免因为可变性而引发错误。您只需定义数据类和实现序列化器即可。快来动手尝试一下吧!


如要详细了解 DataStore,您可以查看我们的 文档 并获得一些使用 Proto DataStorePreferences DataStore Codelab 的实践经验。


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

ART虚拟机 | 锁

本文基于Android 11(R) Java中对临界区的锁定通常用synchronize代码块完成,因此标题中的“锁”实际上是对synchronize关键字的剖析。Synchronize代码块使用时必须传入一个对象,这个对象可以是this对象,可以是类对象(e...
继续阅读 »

本文基于Android 11(R)


Java中对临界区的锁定通常用synchronize代码块完成,因此标题中的“锁”实际上是对synchronize关键字的剖析。Synchronize代码块使用时必须传入一个对象,这个对象可以是this对象,可以是类对象(e.g. Foo.class),也可以是任何其他对象。因此我们可以说,锁的状态和对象关联。亦或者,每个对象天生都是一把锁。


Synchronize生成的字节码会对应两条指令,分别是monitor-entermonitor-exit。下面我们针对monitor_enter,分别从解释执行和机器码执行两个方向去寻找这个指令的最终实现。


解释执行


[art/runtime/interpreter/interpreter_switch_impl-inl.h]


HANDLER_ATTRIBUTES bool MONITOR_ENTER() {
...
ObjPtr<mirror::Object> obj = GetVRegReference(A());
if (UNLIKELY(obj == nullptr)) {
...
} else {
DoMonitorEnter<do_assignability_check>(self, &shadow_frame, obj); <===调用
...
}
}
复制代码

[art/runtime/interpreter/interpreter_common.h]


static inline void DoMonitorEnter(Thread* self, ShadowFrame* frame, ObjPtr<mirror::Object> ref)
NO_THREAD_SAFETY_ANALYSIS
REQUIRES(!Roles::uninterruptible_) {
...
StackHandleScope<1> hs(self);
Handle<mirror::Object> h_ref(hs.NewHandle(ref)); <===调用
h_ref->MonitorEnter(self);
...
}
复制代码

[art/runtime/mirror/object-inl.h]


inline ObjPtr<mirror::Object> Object::MonitorEnter(Thread* self) {
return Monitor::MonitorEnter(self, this, /*trylock=*/false); <===调用
}
复制代码

解释执行会使用switch-case方式分别解析每一条指令,由上述代码可知,monitor-enter指令最终会调用Monitor::MonitorEnter静态函数。


机器码执行


[art/runtime/arch/arm64/quick_entrypoints_arm64.S]


ENTRY art_quick_lock_object_no_inline
// This is also the slow path for art_quick_lock_object.
SETUP_SAVE_REFS_ONLY_FRAME // save callee saves in case we block
mov x1, xSELF // pass Thread::Current
bl artLockObjectFromCode // (Object* obj, Thread*) <===调用
...
END art_quick_lock_object_no_inline
复制代码

[art/runtime/entrypoints/quick/quick_lock_entrypoints.cc]


extern "C" int artLockObjectFromCode(mirror::Object* obj, Thread* self){
...
if (UNLIKELY(obj == nullptr)) {
...
} else {
ObjPtr<mirror::Object> object = obj->MonitorEnter(self); // May block <===调用
...
}
}
复制代码

[art/runtime/mirror/object-inl.h]


inline ObjPtr<mirror::Object> Object::MonitorEnter(Thread* self) {
return Monitor::MonitorEnter(self, this, /*trylock=*/false); <===调用
}
复制代码

殊途同归,机器码执行时最终也会调用Monitor::MonitorEnter


锁的两种形态


虚拟机中将锁实现为两种形态,一种称为Thin Lock,另一种称为Fat Lock。


Thin Lock用于竞争较弱的场景。在竞争发生时,采用自旋(spin)和让渡CPU(yield)的方式等待锁,而不是进行系统调用和上下文切换。当持有锁的线程很快完成操作时,短暂的自旋会比上下文切换开销更小。


可是如果自旋一段时间发现还无法获取到锁时,Thin Lock就会膨胀为Fat Lock,一方面增加数据结构存储与锁相关的具体信息,另一方面通过系统调用挂起线程。


总结一下,Fat Lock功能健全,但开销较大。而Thin Lock开销虽小,但无法用于长时间等待的情况。所以实际的做法是先使用Thin Lock,当功能无法满足时再膨胀为Fat Lock。


文章开头提到,每个对象天生都是一把锁。那么这个锁的信息到底存在对象的什么位置呢?


答案是存在art::mirror::Object的对象头中(详见ART虚拟机 | Java对象和类的内存结构)。对象头中有一个4字节长的字段monitor_,其中便存储了锁相关的信息。


monitor字段.png


4字节共32bits,高位的两个bits用于标记状态。不同的状态,存储的信息含义也不同。两个bits共4种状态,分别为ThinOrUnlock(Thin/Unlock共用一个状态),Fat,Hash和ForwardingAddress。ThinOrUnlock和Fat表示锁的状态,Hash是为对象生成HashMap中所用的哈希值,ForwardingAddress是GC时使用的状态。


上图中的m表示mark bit state,r表示read barrier state,都是配合GC使用的标志,在讨论锁的时候可以不关心。


当我们对一个空闲对象进行monitor-enter操作时,锁的状态由Unlock切换到Thin。代码如下。


switch (lock_word.GetState()) {
case LockWord::kUnlocked: {
// No ordering required for preceding lockword read, since we retest.
LockWord thin_locked(LockWord::FromThinLockId(thread_id, 0, lock_word.GCState()));
if (h_obj->CasLockWord(lock_word, thin_locked, CASMode::kWeak, std::memory_order_acquire)) {
...
return h_obj.Get(); // Success!
}
continue; // Go again.
}
复制代码

LockWord对象的大小就是4字节,所以可以将它等同于art::mirror::Objectmonitor_字段,只不过它内部实现了很多方法可以灵活操作4字节中的信息。锁状态切换时,将当前线程的thread id(thread id并非tid,对每个进程而言它都从1开始)存入monitor_字段,与GC相关的mr标志保持不变。


当对象被线程锁定后,假设我们在同线程内对该它再次进行monitor-enter操作,那么就会发生Thin Lock的重入。如果在不同线程对该对象进行monitor-enter操作,那么就会发生Thin Lock的竞争。代码和流程图如下。


case LockWord::kThinLocked: {
uint32_t owner_thread_id = lock_word.ThinLockOwner();
if (owner_thread_id == thread_id) {
uint32_t new_count = lock_word.ThinLockCount() + 1;
if (LIKELY(new_count <= LockWord::kThinLockMaxCount)) {
LockWord thin_locked(LockWord::FromThinLockId(thread_id,
new_count,
lock_word.GCState()));
if (h_obj->CasLockWord(lock_word,
thin_locked,
CASMode::kWeak,
std::memory_order_relaxed)) {
AtraceMonitorLock(self, h_obj.Get(), /* is_wait= */ false);
return h_obj.Get(); // Success!
}
continue; // Go again.
} else {
// We'd overflow the recursion count, so inflate the monitor.
InflateThinLocked(self, h_obj, lock_word, 0);
}
} else {
// Contention.
contention_count++;
Runtime* runtime = Runtime::Current();
if (contention_count <= runtime->GetMaxSpinsBeforeThinLockInflation()) {
sched_yield();
} else {
contention_count = 0;
// No ordering required for initial lockword read. Install rereads it anyway.
InflateThinLocked(self, h_obj, lock_word, 0);
}
}
continue; // Start from the beginning.
}
复制代码

ThinLock.png


在ThinLock膨胀为FatLock前,需要执行50次sched_yieldsched_yield会将当前线程放到CPU调度队列的末尾,这样既不用挂起线程,也不用一直占着CPU。不过android master分支已经将这个流程再度优化了,在50次sched_yield之前,再执行100次自旋操作。和sched_yield相比,自旋不会释放CPU。由于单次sched_yield耗时也有微秒,对于锁持有时间极短的情况,用自旋更省时间。


接下来介绍锁的膨胀过程。


void Monitor::InflateThinLocked(Thread* self, Handle<mirror::Object> obj, LockWord lock_word,
uint32_t hash_code) {
DCHECK_EQ(lock_word.GetState(), LockWord::kThinLocked);
uint32_t owner_thread_id = lock_word.ThinLockOwner();
if (owner_thread_id == self->GetThreadId()) {
// We own the monitor, we can easily inflate it.
Inflate(self, self, obj.Get(), hash_code);
} else {
ThreadList* thread_list = Runtime::Current()->GetThreadList();
// Suspend the owner, inflate. First change to blocked and give up mutator_lock_.
self->SetMonitorEnterObject(obj.Get());
bool timed_out;
Thread* owner;
{
ScopedThreadSuspension sts(self, kWaitingForLockInflation);
owner = thread_list->SuspendThreadByThreadId(owner_thread_id,
SuspendReason::kInternal,
&timed_out);
}
if (owner != nullptr) {
// We succeeded in suspending the thread, check the lock's status didn't change.
lock_word = obj->GetLockWord(true);
if (lock_word.GetState() == LockWord::kThinLocked &&
lock_word.ThinLockOwner() == owner_thread_id) {
// Go ahead and inflate the lock.
Inflate(self, owner, obj.Get(), hash_code);
}
bool resumed = thread_list->Resume(owner, SuspendReason::kInternal);
DCHECK(resumed);
}
self->SetMonitorEnterObject(nullptr);
}
}
复制代码

void Monitor::Inflate(Thread* self, Thread* owner, ObjPtr<mirror::Object> obj, int32_t hash_code) {
DCHECK(self != nullptr);
DCHECK(obj != nullptr);
// Allocate and acquire a new monitor.
Monitor* m = MonitorPool::CreateMonitor(self, owner, obj, hash_code);
DCHECK(m != nullptr);
if (m->Install(self)) {
if (owner != nullptr) {
VLOG(monitor) << "monitor: thread" << owner->GetThreadId()
<< " created monitor " << m << " for object " << obj;
} else {
VLOG(monitor) << "monitor: Inflate with hashcode " << hash_code
<< " created monitor " << m << " for object " << obj;
}
Runtime::Current()->GetMonitorList()->Add(m);
CHECK_EQ(obj->GetLockWord(true).GetState(), LockWord::kFatLocked);
} else {
MonitorPool::ReleaseMonitor(self, m);
}
}
复制代码

膨胀(Inflate)的具体操作比较简单,简言之就是创建一个Monitor对象,存储更多的信息,然后将Monitor Id放入原先的monitor_字段中。


关键的地方在于膨胀的充分条件。如果Thin Lock本来就由本线程持有,那么膨胀不需要经过任何人同意,可以直接进行。但如果该Thin Lock由其他线程持有,那么膨胀之前必须先暂停(这里的暂停并不是指将线程从CPU上调度出去,而是不允许它进入Java世界改变锁状态)持有线程,防止膨胀过程中对锁信息的更新存在竞争。膨胀之后,持有线程恢复运行,此时它看到的Lock已经变成了Fat Lock。


当锁膨胀为Fat Lock后,由于持有锁的动作并未完成,所以该线程会再次尝试。只不过这次走的是Fat Lock分支,执行如下代码。


case LockWord::kFatLocked: {
// We should have done an acquire read of the lockword initially, to ensure
// visibility of the monitor data structure. Use an explicit fence instead.
std::atomic_thread_fence(std::memory_order_acquire);
Monitor* mon = lock_word.FatLockMonitor();
if (trylock) {
return mon->TryLock(self) ? h_obj.Get() : nullptr;
} else {
mon->Lock(self);
DCHECK(mon->monitor_lock_.IsExclusiveHeld(self));
return h_obj.Get(); // Success!
}
}
复制代码

{
ScopedThreadSuspension tsc(self, kBlocked); // Change to blocked and give up mutator_lock_.

// Acquire monitor_lock_ without mutator_lock_, expecting to block this time.
// We already tried spinning above. The shutdown procedure currently assumes we stop
// touching monitors shortly after we suspend, so don't spin again here.
monitor_lock_.ExclusiveLock(self);
}
复制代码

上述代码的ScopedThreadSuspension对象用于完成线程状态的切换,之所以叫scoped,是因为它是通过构造和析构函数完成状态切换和恢复的。在作用域内的局部变量会随着作用域的结束而自动析构,因此花括号结束,线程状态也就由Blocked切换回Runnable了。


最终调用monitor_lock_(Mutex对象)的ExclusiveLock方法。


void Mutex::ExclusiveLock(Thread* self) {
if (!recursive_ || !IsExclusiveHeld(self)) {
#if ART_USE_FUTEXES
bool done = false;
do {
int32_t cur_state = state_and_contenders_.load(std::memory_order_relaxed);
if (LIKELY((cur_state & kHeldMask) == 0) /* lock not held */) {
done = state_and_contenders_.CompareAndSetWeakAcquire(cur_state, cur_state | kHeldMask);
} else {
...
if (!WaitBrieflyFor(&state_and_contenders_, self,
[](int32_t v) { return (v & kHeldMask) == 0; })) {
// Increment contender count. We can't create enough threads for this to overflow.
increment_contenders();
// Make cur_state again reflect the expected value of state_and_contenders.
cur_state += kContenderIncrement;
if (UNLIKELY(should_respond_to_empty_checkpoint_request_)) {
self->CheckEmptyCheckpointFromMutex();
}
do {
if (futex(state_and_contenders_.Address(), FUTEX_WAIT_PRIVATE, cur_state,
nullptr, nullptr, 0) != 0) {
...
cur_state = state_and_contenders_.load(std::memory_order_relaxed);
} while ((cur_state & kHeldMask) != 0);
decrement_contenders();
}
}
} while (!done);
...
exclusive_owner_.store(SafeGetTid(self), std::memory_order_relaxed);
RegisterAsLocked(self);
}
recursion_count_++;
...
}
复制代码

Mutex::ExclusiveLock最终通过futex系统调用陷入内核态,在内核态中将当前线程从CPU中调度出去,实现挂起。值得注意的是,FatLock中依然有spin和yield的操作(WaitBrieflyFor函数),这是因为Thin Lock一旦膨胀为Fat Lock就很难deflate回去,而后续对Fat Lock的使用依然会碰到短时持有锁的情况,这也意味先前的优化此处依然可用。


上面这一块代码算是锁的核心实现,被调用的次数也非常多,因此任何一点微小的优化都很重要。我之前写过一篇文章调试经验 | C++ memory order和一个相关的稳定性问题详细分析了一个由memory order使用错误导致的线程卡死的问题,其中还介绍了C++的memory order,它也正是Java volatile关键字的(ART)底层实现。


此外我还给谷歌提过ExclusiveLock的bug,这个bug既会消耗battery,也会在某些情况下导致系统整体卡死。下面是谷歌的具体回复,感兴趣的可以查看修复


Hans Reply.png


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

iOS 实现类似通讯录按拼音排序 - PinYin4Objc

最近项目中需要实现类似通讯录那样按拼音进行排序以及索引列表的显示的功能,这里使用了 PinYin4Objc 这个库来实现此功能。PinYinObjc是一个效率很高的汉字转拼音类库,智齿简体和繁体中文,有如下特点:1、效率高,使用数据缓存,第一次初始化以后,拼音...
继续阅读 »

最近项目中需要实现类似通讯录那样按拼音进行排序以及索引列表的显示的功能,这里使用了 PinYin4Objc 这个库来实现此功能。

PinYinObjc是一个效率很高的汉字转拼音类库,智齿简体和繁体中文,有如下特点:
1、效率高,使用数据缓存,第一次初始化以后,拼音数据存入文件缓存和内存缓存,后面转换效率大大提高;
2、支持自定义格式化,拼音大小写等等;
3、拼音数据完整,支持中文简体和繁体,与网络上流行的相关项目比,数据很全,几乎没有出现转换错误的问题.

在项目中使用可以cocoapods来管理:pod 'PinYin4Objc', '~> 1.1.1'
也可以直接去github上下载源码:PinYinObjc

项目需求:
获取一个销售人员的列表,并且把自己放到第一个,用#标示,如图:


代码实现过程:

1、获取销售人员列表数据(这里是通过网络请求获取):

///查询列表数据
- (void)fetchSallersList {
[_listAPI startWithCompletionWithSuccess:^(id responseDataDict) {
[self.tableView.mj_header endRefreshing];
///解析数据
NSMutableArray *array = [SCSalesModel mj_objectArrayWithKeyValuesArray:responseDataDict];
self.resultList = [array mutableCopy];
///处理数据
[self conversionResultData];
[self changeResultList];
///刷新UI
[self reloadUI];
} failure:^(NSError *error) {
[self.tableView.mj_header endRefreshing];
[SCAlertHelper handleError:error];
}];
}

2、将每个销售人员的名字转成拼音并转成大写字母:

HanyuPinyinOutputFormat *outputFormat=[[HanyuPinyinOutputFormat alloc] init];
[outputFormat setToneType:ToneTypeWithoutTone];
[outputFormat setVCharType:VCharTypeWithV];
[outputFormat setCaseType:CaseTypeUppercase];
[self.resultList enumerateObjectsUsingBlock:^(SCSalesModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *pinyin = [[PinyinHelper toHanyuPinyinStringWithNSString:obj.salesName withHanyuPinyinOutputFormat:outputFormat withNSString:@""] uppercaseString];
SCLog(@"名字转拼音大写:%@", pinyin);
obj.pinyinName = pinyin;
}];

3、按照拼音字段pinyinName进行排序:

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"pinyinName" ascending:YES];
NSArray *array = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[self.resultList sortUsingDescriptors:array];
4、定义一个全局变量 dataDictionary 来组织数据结构

key: 将汉字转完拼音后的第一个字母, 也就是上图section中的 A、B、C...

value: 是一个成员数组,存放每个section下的成员列表
_dataDictionary = [[NSMutableDictionary alloc] init];
//存放每个 section 下的成员数组
NSMutableArray *currentArray = nil;
//用于获取拼音中第一个字母
NSRange aRange = NSMakeRange(0, 1);
NSString *firstLetter = nil;
//遍历成员列表组织数据结构
for (SCSalesModel *seller in self.resultList) {
//如果是本人,则暂时不放如 dataDictionary 中
if ([seller.salesId isEqualToString:[SCUserModel currentLoggedInUser].userId]) {
_owerSaller = seller;
continue;
}
//获取拼音中第一个字母,如果已经存在则直接将该成员加入到当前的成员数组中,如果不存在,创建成员数据,添加一个 key-value 结构到 dataDictionary 中
firstLetter = [seller.pinyinName substringWithRange:aRange];
if ([_dataDictionary objectForKey:firstLetter] == nil) {
currentArray = [NSMutableArray array];
[_dataDictionary setObject:currentArray forKey:firstLetter];
}
[currentArray addObject:seller];
}

5、再定义一个全局变量 allKeys 用于显示索引列表中索引:

_allKeys = [[NSMutableArray alloc] initWithArray:[[_dataDictionary allKeys] sortedArrayUsingFunction:sortObjectsByKey context:NULL]];
//然后将本人加入到排好序 allKeys 的最前面
if (_owerSaller) {
[_allKeys insertObject:@"#" atIndex:0];
[_dataDictionary setObject:[NSArray arrayWithObjects:_owerSaller, nil] forKey:@"#"];
}

//其中sortObjectsByKey是排序方法
NSInteger sortObjectsByKey(id user1, id user2, void *context) {
NSString *u1,*u2;
//类型转换
u1 = (NSString*)user1;
u2 = (NSString*)user2;
return [u1 localizedCompare:u2];
}

6、最后就是通过 allKeys 和 dataDictionary 进行配置一下 tableview 的各个代理就 OK 了。


借鉴:http://www.cnblogs.com/jerryfeng/p/4288244.html


菜鸟笔记!希望对你有帮助!

转自:https://www.jianshu.com/p/96d141698700


收起阅读 »

iOS 基于AFNetWorking的联想搜索的实现

需求描述:输入框搜索功能,输入小米,键盘输入按照x-i-a-o-m-i的顺序,而请求是根据输入框内容的变化进行请求,输入框每变化一次就要进行一次请求,直到输入停止,请求的结果列表展示。关键点:频繁的网络请求,又不能影响下次请求的进行,这就要求当新的请求开始前,...
继续阅读 »

需求描述:
输入框搜索功能,输入小米,键盘输入按照x-i-a-o-m-i的顺序,而请求是根据输入框内容的变化进行请求,输入框每变化一次就要进行一次请求,直到输入停止,请求的结果列表展示。
关键点:频繁的网络请求,又不能影响下次请求的进行,这就要求当新的请求开始前,1.展示上次请求的结果;2.就是请求还未返回那就直接取消请求直接进行下次请求.
直接上代码,在封装的网络请求工具里,AFHTTPSessionManager在工具类初始化的时候创建,当前任务@property (nonatomic, strong)NSURLSessionDataTask *currentTask;

[_currentTask cancel];为取消当前任务
[_currentManager.operationQueue cancelAllOperations];取消所有任务

- (void)frequentlyPOST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
hudOnView:(UIView *)onView
{
if (_currentTask) {
[_currentTask cancel];
[_currentManager.operationQueue cancelAllOperations];
[ProgressHUDUtil hideHUD:onView];
}
[ProgressHUDUtil showLoadingWithView:onView];

_currentTask = [_currentManager POST:Append2Str(API_Base, URLString) parameters:parameters progress:^(NSProgress * _Nonnull uploadProgress) {

} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *result =[[ NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
[ProgressHUDUtil hideHUD:onView];
success(task,result);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
failure(task,error);
}];

}


转自:https://www.jianshu.com/p/777cdfb5e681

收起阅读 »

简易版 React-Router实现

上一篇简单的介绍了react-router 的使用方法和基本的API,对于react-router几个重要的API做了源码解读。这篇就实现一个简易版的 react-router设计思路由上图可知,核心内容就是如何监听到URL的改变?图中说到三种方式,其实也就两...
继续阅读 »

上一篇简单的介绍了react-router 的使用方法和基本的API,对于react-router几个重要的API做了源码解读。这篇就实现一个简易版的 react-router

设计思路


由上图可知,核心内容就是如何监听到URL的改变?图中说到三种方式,其实也就两种pushstate 和 浏览器的前进和回退。刷新页面还是处于当前的URL,不涉及URL的改变。上一篇文章中也讲到 前端路由的原理有两点

  1. URL改变 页面不刷新。
  2. 监听到URL的改变。

所以在设计 react-router 的时候需要考虑 pushstate 和 浏览器的前进和回退这两种方式的URL改变。

Router

功能:负责监听页面对象发生了改变,并开始重新渲染页面 **

  1. 先定义一个上下文,方便把history数据传入所有的子组件
const RouteContext = React.createContext({})
  1. 定义 Router 组件,主要内容监听URL变化
const globalHistory = window.history // history 使用window 全局的history
class Router extends React.Component {
constructor(props) {
super(props)
this.state = { // 把location 设置为state 每次URL的改变,能够更新页面
location: window.location
}
// 第一种跳转方式:浏览器的前进后退,触发popstate 事件
window.addEventListener("popstate", () => {
this.setState({
location: window.location
})
})
}
// 第二种跳转方式:pushstate
// 向子组件提供push 方法更新路由,跳转页面
push = (route) => {
globalHistory.pushState({}, "", route)
this.setState({
location: window.location
})
}
// 定义上下文,把通用内容传入子组件
render() {
const { children } = this.props
const { location } = this.state
return (
<RouteContext.Provider value={{
history: globalHistory,
location,
push: this.push,
}}>
{
React.cloneElement(children, {
history: globalHistory,
location,
push: this.push,
})
}
</RouteContext.Provider>
)
}
}

export default Router

Route

功能:页面开始渲染后,根据具体的页面location信息展示具体路由地址对应的内容 **

import React, { useContext } from 'react'
const Route = (props) => {
// 在上下文中获取到相关信息
const context = useContext(RouteContext)
// 计算 location 匹配到的 path
const computedPath = (path, exact) => {
...TODO
// 这里内容和源码一样,其核心使用了path-to-regexp 库,能够计算出URL中的参数
}
// eslint-disable-next-line no-unused-vars
const { render, children, component, path, exact = false, ...rest } = props
const match = computedPath(path, exact)
const params = { ...context, match, location: context.location }
// 渲染 也就是源码中的三目运算。把相关的属性传入子组件
if (match) {
if (children) {
if (typeof children === 'function') {
return children(params)
}
return React.cloneElement(children, params)
} else if (component) {
return component(params)
} else if (render) {
return render(params)
}
}
return null
}

export default Route

这样一个简单的React-Router 就实现了,能够实现页面的跳转。

完整代码:https://github.com/LiuSandy/web

原文链接:https://zhuanlan.zhihu.com/p/366482879


收起阅读 »

React setState数据更新机制

为什么使用setState在React 的开发过程中,难免会与组件的state打交道。使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法。为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个de...
继续阅读 »

为什么使用setState

在React 的开发过程中,难免会与组件的state打交道。使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法。为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个demo。

class Index extends React.Component {
this.state = {
count: 0
}
onClick = () => {
this.setState({
count: 10
})
}
render() {
return (
<div>
<span>{this.state.count}</span>
<button onClick={this.onClick}>click</button>
</div>
)
}
}

根据上面代码可以看到,点击按钮后把state 中 count 的值修改为 10。并更新页面的显示。所以state的改变有两个作用:对应的值改变 和 页面更新。要想做到这两点在react 中 非 setState 不可。 假如说我们把 onClick 的方法内容修改为 this.state.count = 10 并在方法内打印出 this.state 的值,可以看到state的值已经改变。但是页面并没有更新到最新的值。 ☆总结一下:

  1. state 值的改变,目的是页面的更新,希望React 使用最新的 state来渲染页面。但是直接赋值的方式并不能让React监听到state的变化。
  2. 必须通过setState 方法来告诉React state的数据已经变化。

☆扩展一下:

在vue中,采用的就是直接赋值的方式来更新data 数据,并且Vue也能够使用最新的data数据渲染页面。这是为什么呢? 在vue2中采用的是 Object.defineProperty() 方式监听数据的get 和 set 方法,做到数据变化的监听 在vue3中采用的是ES6 的 proxy 方式监听数据的变化

setState 的用法

想必所有人都会知道setState 的用法,在这里还是想记录一下: setState方法有两个参数:第一个参数可以是对象直接修改属性值,也可以是函数能够拿到上一次的state值。第二个参数是一个可选的回调函数,可以获取最新的state值 回调函数会在组件更新完成之后执行,等价于在 componentDidUpdate 生命周期内执行。

  1. 第一个参数是对象时:如同上文的demo一样,直接修改state的属性值
this.setState({
key:newState
})
  1. 第一个参数是函数时:在函数内可以获取上一次state 的属性值。
// prevState 是上一次的 state,props 是此次更新被应用时的 props
this.setState((prevState, props) => {
return {
key: prevState.key
}
})

他们两者的区别主要体现在setState的异步更新上面!!!

异步更新还是同步更新

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式 将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

先修改一下上面的代码,如果在onClick 方法中连续调用三次setState,根据上文可知 setState是一个异步的方式,每次调用只是将更改加入队列,同步调用的时候只会执行最后一次更新,所以结果是1而不是3。

onClick = () => {
const { count } = this.state
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
}

可以把上面代码理解为 Object.assign() 方法,

Object.assign(
state,
{ count: state.count + 1 },
{ count: state.count + 1 },
{ count: state.count + 1 }
)

如果第一个参数传入一个函数,连续调用三次,是不是和传入对象方式的结果是一样的呢?

onClick = () => {
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
}

结果和传入对象的方式大相径庭,使用函数的方式就能够实现自增为3的效果。这又是为什么呢? 在函数内能够拿到最新的state 和 props值。由上文可知 setState 的更新是分批次的,使用函数的方式确保了当前state 是建立在上一个state 之上的,所以实现了自增3的效果。

☆总结一下: 为什么setState 方法是异步的呢?

  1. 可以显著的提升性能,react16 引入了 Fiber 架构,Fiber 中对任务进行了划分和优先级的分类,优先处理优先级比较高的任务。页面的响应就是一个优先级比较高任务,所以如果setState是同步,那么更新一次就要更新一次页面,就会阻塞到页面的响应。最好的办法就是获得到多个更新,之后进行批量的更新。只更新一次页面。
  2. 如果同步更新state,但是还没有执行render 函数,那么state 和 props 就不能够保持同步。

是不是所有的setState 都是异步的形式呢?答案是 否!!!在React 中也会存在setState 同步的场景

onClick = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
}, 0)
}

上面的代码会打印出0,2。这又是为什么呢?其实React 中的 setState 并不是严格意义上的异步函数。他是通过队列的延迟执行实现的。使用 isBatchingUpdates 判断当前的setState 是加入到更新队列还是更新页面。当 isBatchingUpdates=ture 是加入更新队列,否则执行更新。

知道了React 是使用 isBatchingUpdates 来判断是否加入更新队列。那么为什么在 setTimeout 事件中 isBatchingUpdates 值为 false ? 原因就是在React中,对HTML的原生事件做了一次封装叫做合成事件。所以在React自己的生命周期和合成事件中,可以控制 isBatchingUdates 的值,可以根据值来判断是否更新页面。而在宿主环境提供的原生事件中(即非合成事件),无法将 isBatchingUpdates 的值置为 false,所以就会立即执行更新。

☆所以setState 并不是有同步的场景,而是在特殊的场景下不受React 的控制 **

总结

setState 并不是单纯的同步函数或者异步函数,他的同步和异步的表现差异体现在调用的场景不同。在React 的生命周期和合成事件中他表现为异步函数。而在DOM的原生事件等非合成事件中表现为同步函数。

本节通过分析setState 的更新机制了解到setState 同步和异步的两种场景,下一节深入剖析下调用setState都做了什么?结合源码了解下为什么会出现两种场景?

原文:https://zhuanlan.zhihu.com/p/366781311

收起阅读 »

配置 ESLint 自动格式化自闭合标签(Self closing tag)

对于没有子元素或不需要子元素的 HTML 标签,通常写成其自闭合的形式会显得简洁些,- <SomeComponent></SomeComponent> + <SomeComponent/> 通过配置 ESLint 可在格式化...
继续阅读 »

对于没有子元素或不需要子元素的 HTML 标签,通常写成其自闭合的形式会显得简洁些,

- <SomeComponent></SomeComponent>
+ <SomeComponent/>

通过配置 ESLint 可在格式化的时候将标签自动变成自闭合形式。

create-react-app

如果是使用 create-react-app 创建的项目,直接在 package.json 的 eslint 配置部分加上如下配置即可:

"eslintConfig": {
"extends": "react-app",
+ "rules": {
+ "react/self-closing-comp": [
+ "error"
+ ]
}

安装依赖

安装 ESLint 相关依赖:

$ yarn add eslint eslint-plugin-react

如果是 TypeScript 项目,还需要安装如下插件:

$ yarn add @typescript-eslint/eslint-plugin  @typescript-eslint/parser

配置 ESLint

通过 yarn eslint --init 向导来完成创建,

或手动创建 .eslintrc.json 填入如下配置:

{
"extends": ["eslint:recommended", "plugin:react/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint"],
"rules": {
"react/self-closing-comp": ["error"]
}
}

安装 ESLint for Vscode

当然了,还需要安装 VSCode 插件 dbaeumer.vscode-eslint

然后配置 VSCode 在保存时自动进行修正动作:

"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},

使用

完成上述配置后,如果发现保存时,格式并未生效,或者只 JavaScript 文件生效,需要补上如下的 VSCode 配置:

"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
]

也可查看 VSCode 的状态栏,看是否有报错可确定是什么原因导致 ESLint 工作不正常,比如 mac BigSur 中细化了权限,需要点击警告图标然后点击允许。




原文:https://zhuanlan.zhihu.com/p/368639332

收起阅读 »

iOS 功能丰富的 Category 类型工具库

YYCategories安装CocoaPods在 Podfile 中添加  pod 'YYCategories'。执行 pod install 或 pod update。导入 <YYCategories/...
继续阅读 »

YYCategories

安装

CocoaPods

  1. 在 Podfile 中添加  pod 'YYCategories'
  2. 执行 pod install 或 pod update
  3. 导入 <YYCategories/YYCategories.h>。

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYCategories"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入 <YYCategories/YYCategories.h>。

手动安装

  1. 下载 YYCategories 文件夹内的所有内容。
  2. 将 YYCategories 内的源文件添加(拖放)到你的工程。
  3. 为 NSObject+YYAddForARC.m 和 NSThread+YYAdd.m 添加编译参数 -fno-objc-arc
  4. 链接以下 frameworks:
    • UIKit
    • CoreGraphics
    • QuartzCore
    • Accelerate
    • ImageIO
    • CoreText
    • CoreFoundation
    • libz
  5. 导入 YYCategories.h

注意

我希望调用 API 时,有着和调用系统自带 API 一样的体验,所以我并没有为 Category 方法添加前缀。我已经用工具扫描过这个项目中的 API,确保没有对系统 API 产生影响。我知道没有前缀的 Category 可能会带来麻烦(比如可能和其他某些类库产生冲突),所以如果你只需要其中少量代码,那最好将那段代码取出来,而不是导入整个库。


常见问题及demo下载:https://github.com/ibireme/YYCategories

源码下载:YYCategories.zip




收起阅读 »

iOS 异步绘制与显示的工具类

YYAsyncLayeriOS 异步绘制与显示的工具类。简单用法@interface YYLabel : UIView @property NSString *text; @property UIFont *font; @end @implementatio...
继续阅读 »

YYAsyncLayer

iOS 异步绘制与显示的工具类。

简单用法

@interface YYLabel : UIView
@property NSString *text;
@property UIFont *font;
@end

@implementation YYLabel

- (void)setText:(NSString *)text {
_text = text.copy;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)setFont:(UIFont *)font {
_font = font;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)layoutSubviews {
[super layoutSubviews];
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)contentsNeedUpdated {
// do update
[self.layer setNeedsDisplay];
}

#pragma mark - YYAsyncLayer

+ (Class)layerClass {
return YYAsyncLayer.class;
}

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {

// capture current state to display task
NSString *text = _text;
UIFont *font = _font;

YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {
//...
};

task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
if (isCancelled()) return;
NSArray *lines = CreateCTLines(text, font, size.width);
if (isCancelled()) return;

for (int i = 0; i < lines.count; i++) {
CTLineRef line = line[i];
CGContextSetTextPosition(context, 0, i * font.pointSize * 1.5);
CTLineDraw(line, context);
if (isCancelled()) return;
}
};

task.didDisplay = ^(CALayer *layer, BOOL finished) {
if (finished) {
// finished
} else {
// cancelled
}
};

return task;
}
@end

安装

CocoaPods

  1. 在 Podfile 中添加 pod 'YYAsyncLayer'
  2. 执行 pod install 或 pod update
  3. 导入

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYAsyncLayer"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入

手动安装

  1. 下载 YYAsyncLayer 文件夹内的所有内容。
  2. 将 YYAsyncLayer 内的源文件添加(拖放)到你的工程。
  3. 导入 YYAsyncLayer.h


系统要求

该项目最低支持 iOS 6.0 和 Xcode 8.0






收起阅读 »

iOS 全局并发队列管理工具

YYDispatchQueuePooliOS 全局并发队列管理工具。当某个 block 所在线程被锁住时,concurrent queue 会创建大量线程以至于占用了过多资源而影响到主线程。这里可以用一个全局的 serial queue pool 来尽量控制全...
继续阅读 »

YYDispatchQueuePool

iOS 全局并发队列管理工具。

当某个 block 所在线程被锁住时,concurrent queue 会创建大量线程以至于占用了过多资源而影响到主线程。这里可以用一个全局的 serial queue pool 来尽量控制全局线程数。

用法

// 从全局的 queue pool 中获取一个 queue
dispatch_queue_t queue = YYDispatchQueueGetForQOS(NSQualityOfServiceUtility);

// 创建一个新的 serial queue pool
YYDispatchQueuePool *pool = [[YYDispatchQueuePool alloc] initWithName:@"file.read" queueCount:5 qos:NSQualityOfServiceBackground];
dispatch_queue_t queue = [pool queue];

安装

CocoaPods

  1. 在 Podfile 中添加 pod 'YYDispatchQueuePool'
  2. 执行 pod install 或 pod update
  3. 导入 <YYDispatchQueuePool/YYDispatchQueuePool.h>。

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYDispatchQueuePool"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入 <YYDispatchQueuePool/YYDispatchQueuePool.h>。

手动安装

  1. 下载 YYDispatchQueuePool 文件夹内的所有内容。
  2. 将 YYDispatchQueuePool 内的源文件添加(拖放)到你的工程。
  3. 导入 YYDispatchQueuePool.h

系统要求

该项目最低支持 iOS 6.0 和 Xcode 8.0


常见问题及demo下载:https://github.com/ibireme/YYDispatchQueuePool

源码下载:YYDispatchQueuePool.zip




收起阅读 »

iOS 键盘管理工具

YYKeyboardManageriOS 键盘监听管理工具类。'兼容性该项目能很好的兼容 iPhone / iPad / iPod,兼容 iOS 6~11, 并且能很好的处理屏幕旋转。用法// 获取键盘管理器 YYKeyboardManager *manag...
继续阅读 »

YYKeyboardManager

iOS 键盘监听管理工具类。'

兼容性

该项目能很好的兼容 iPhone / iPad / iPod,兼容 iOS 6~11, 并且能很好的处理屏幕旋转。

用法

// 获取键盘管理器
YYKeyboardManager *manager = [YYKeyboardManager defaultManager];

// 获取键盘的 view 和 window
UIView *view = manager.keyboardView;
UIWindow *window = manager.keyboardWindow;

// 获取键盘当前状态
BOOL visible = manager.keyboardVisible;
CGRect frame = manager.keyboardFrame;
frame = [manager convertRect:frame toView:self.view];

// 监听键盘动画
[manager addObserver:self];
- (void)keyboardChangedWithTransition:(YYKeyboardTransition)transition {
CGRect fromFrame = [manager convertRect:transition.fromFrame toView:self.view];
CGRect toFrame = [manager convertRect:transition.toFrame toView:self.view];
BOOL fromVisible = transition.fromVisible;
BOOL toVisible = transition.toVisible;
NSTimeInterval animationDuration = transition.animationDuration;
UIViewAnimationCurve curve = transition.animationCurve;
}

安装

CocoaPods

  1. 在 Podfile 中添加 pod 'YYKeyboardManager'
  2. 执行 pod install 或 pod update
  3. 导入 <YYKeyboardManager/YYKeyboardManager.h>。

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYKeyboardManager"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入 <YYKeyboardManager/YYKeyboardManager.h>。

手动安装

  1. 下载 YYKeyboardManager 文件夹内的所有内容。
  2. 将 YYKeyboardManager 内的源文件添加(拖放)到你的工程。
  3. 导入 YYKeyboardManager.h

系统要求

该项目最低支持 iOS 6.0 和 Xcode 8.0










收起阅读 »

浅谈前端权限设计方案

前端权限架构的设计一直都是备受关注的技术点.通过给项目引入了权限控制方案,可以满足我们灵活的调整用户访问相关页面的许可. 比如哪些页面向游客开放,哪些页面必须要登录后才能访问,哪些页面只能被某些角色访问(比如超级管理员).有些页面即使用户登录了但受到角色的限制...
继续阅读 »

前端权限架构的设计一直都是备受关注的技术点.通过给项目引入了权限控制方案,可以满足我们灵活的调整用户访问相关页面的许可.


比如哪些页面向游客开放,哪些页面必须要登录后才能访问,哪些页面只能被某些角色访问(比如超级管理员).有些页面即使用户登录了但受到角色的限制,他也只被允许看到页面的部分内容.


出于实际工作的需要,很多项目(尤其类后台管理系统)需要引入权限控制.倘若权限整体的架构设计的不好或者没有设计,会导致项目中各种权限代码混入业务代码造成结构混乱,其次想给新模块引入权限控制或者功能扩展都十分棘手.


虽然前端在权限层面能做一些事情,但很遗憾真正对权限进行把关的是后端.例如一个软件系统,前端在不写一行权限代码的情况下,当用户进入某个他无权访问的页面时,后端是可以判断他越权访问并拒绝返回数据的.由此可见前端即使不做什么整个系统也是可以正常运行的,但这样应用的体验很不好.另外一个很重要的原因就是前端做的权限校验都是可以被本地数据造假越权通过.


前端如果能判断某用户越权访问页面时,就不要让他进入那张页面后再弹出无权访问的信息提示,因为这样体验很差.最优方案是直接关闭那些页面的入口,只让他看到他能访问的页面.即使他通过输入路径恶意访问,导航最后只会将它带到默认页面或404页面.


前端做的权限控制大抵是先接受后台发送的权限数据,然后将数据注入到应用中.整个应用于是开始对页面的展现内容以及导航逻辑进行控制,从而达到权限控制的目的.前端做的权限控制虽然能提供一层防护,但根本目的还是为了优化体验.


本文接下来将从下面三个层面,从易到难步步推进,讲述目前前端主流的权限控制方案的实现.(下面代码将会以vue3vue-router 4演示)



  • 登录权限控制

  • 页面权限控制

  • 内容权限控制
登录权限控制

登录权限控制要做的事情,是实现哪些页面能被游客访问,哪些页面只有登录后才能被访问.在一些没有引入角色的软件系统中,通过是否登录来评定页面能否被访问在实际工作中非常常见.

实现这个功能也非常简单,首先按照惯例定义一份路由.

export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/list", // 列表页
name:"List",
component: List,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
}
]

假定存在三个页面:登录页、列表页和个人中心页.登录页和列表页所有人都可以访问,但个人中心页面需要登录后才能看到,给该路由添加一个meta对象,并将need_login置为true;


另外对于那些需要登录后才能看到的页面,用户如果没有登录就访问,就将页面跳转到登录页.等到他填写完用户名和密码点击登录后直接跳转到原来他想访问的页面.


在代码层面,通过router.beforeEach可以轻松实现上述目标,每次页面跳转时都会调用router.beforeEach包裹的函数,代码如下.


to是要即将访问的路由信息,从其中拿到need_login的值可以判断是否需要登录.再从vuex中拿到用户的登录信息.


如果用户没有登录并且要访问的页面又需要登录时就使用next跳转到登录页面,并将需要访问的页面路由名称通过redirect_page传递过去,在登录页面就可以拿到redirect_page等登录成功后直接跳转.

//vue-router4 创建路由实例
const router = createRouter({
history: createWebHashHistory(),
routes,
});

router.beforeEach((to, from, next) => {
const { need_login = false } = to.meta;
const { user_info } = store.state; //从vuex中获取用户的登录信息
if (need_login && !user_info) {
// 如果页面需要登录但用户没有登录跳到登录页面
const next_page = to.name; // 配置路由时,每一条路由都要给name赋值
next({
name: 'Login',
params: {
redirect_page: next_page,
...from.params, //如果跳转需要携带参数就把参数也传递过去
},
});
} else {
//不需要登录直接放行
next();
}
});

页面权限控制


页面权限控制要探讨的问题是如何给不同角色赋予不同的页面访问权限,接下来先了解一下角色的概念.


在一些权限设置比较简单的系统里,使用上面第一种方法就足够了,但如果系统引入了角色,那么就要在上面基础上,再进一步改造增强权限控制的能力.


角色的出现是为了更加个性化配置权限列表.比如当前系统设置三个角色:普通会员,管理员以及超级管理员.普通会员能够浏览软件系统的所有内容,但是它不能编辑和删除内容.管理员拥有普通会员的所有能力,另外它还能删除和编辑内容.超级管理员拥有软件系统所有权限,他单独拥有赋予某个账号为管理员或移除其身份的能力.


一旦软件系统引入了角色的概念,那么每个账户在注册之后就会被赋予相应的角色,从而拥有相应的权限.我们前端要做的事情就是依据不同角色给与它相应页面访问和操作的权限.这里要注意,前端依据的客体是角色,不是某个账户,因为账户是依托于角色的.


普通会员,管理员以及超级管理员这样角色的安排还是一种非常简单的划分方式,在实际项目中,角色的划份要更加细致的多.比如一些常见的后台业务系统,软件系统会按照公司的各个部门来建立角色,诸如市场部,销售部,研发部之类.公司的每个成员就会被划分到相应角色中,从而只具备该角色所拥有的权限.


公司另外一些高层领导他们的账户则会被划分到普通管理员或高级管理员中,那么他们相较于其他角色也会拥有更多的权限.


上面介绍那么多角色的概念其实是为了从全栈的维度去理解权限的设计,但真正落地到前端项目中是不需要去处理角色逻辑的,那部分功能主要由后端完成.


现在假定后端不处理角色完全交给前端来做会出现什么问题.首先前端新建一个配置文件,假定当前系统设定三种角色:普通会员,管理员以及超级管理员以及每个角色能访问的页面列表(伪代码如下).

export const permission_list = {
member:["List","Detail"], //普通会员
admin:["List","Detail","Manage"], // 管理员
super_admin:["List","Detail","Manage","Admin"] // 超级管理员
}

数组里每个值对应着前端路由配置的name值.普通会员能访问列表页详情页,管理员能额外访问内容管理页面,超级管理员能额外访问人员管理页面.


整个运作流程简述如下.当用户登录成功之后,通过接口返回值得知用户数据和所属角色.拿到角色值后就去配置文件里取出该角色能访问的页面列表数组,随后将这部分权限数据加载到应用中从而达到权限控制的目的.


从上面流程看,角色放在前端配置也是可以的.但假如项目已经上线,产品经理要求项目急需增加一个新角色合作伙伴,并把原来已经存在的用户张三移动到合作伙伴角色下面.那这样的变动会导致前端需要修改代码文件,在原来的配置文件上再新建角色来满足这一需求.


由此可见由前端来配置角色列表是非常不灵活且容易出错的,那么最优方案是交给后端去配置.用户一旦登录后,后端接口直接返回该账号拥有的权限列表就行了,至于该账户属于什么角色以及角色拥有的页面权限全部丢给后端去处理.


用户登录成功后,后端接口数据返回如下.

{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}

前端现在不需要理会张三属于什么角色,只需要按照张三的权限列表给他相应的访问权限就行了,其他都交给后端处理.

通过接口的返回值permission_list可知,张三能访问列表页详情页以及内容管理页.我们先回到路由配置页面,看看如何配置.

//静态路由
export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
},
{
path:"/", // 首页
name:"Home",
component: Home,
}
]

//动态路由
export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
},
{
path:"/detail", // 详情页
name:"Detail",
component: Detail
},
{
path:"/manage", // 内容管理页
name:"Manage",
component: Manage
},
{
path:"/admin", // 人员管理页
name:"Admin",
component: Admin
}
]

现在将所有路由分成两部分,静态路由routes和动态路由dynamic_routes.静态路由里面的页面是所有角色都能访问的,它里面主要区分登录访问和非登录访问,处理的逻辑与上面介绍的登录权限控制一致.


动态路由dynamic_routes里面存放的是与角色定制化相关的的页面.现在继续看下面张三的接口数据,该如何给他设置权限.

{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}

用户登录成功后,一般会将上述接口信息存到vuexlocalStorage里面.假如此时刷新浏览器,我们就要动态添加路由信息.

import store from "@/store";

export const routes = [...]; //静态路由

export const dynamic_routes = [...]; //动态路由

const router = createRouter({ //创建路由对象
history: createWebHashHistory(),
routes,
});

//动态添加路由
if(store.state.user != null){ //从vuex中拿到用户信息
//用户已经登录
const { permission_list } = store.state.user; // 从用户信息中获取权限列表
const allow_routes = dynamic_routes.filter((route)=>{ //过滤允许访问的路由
return permission_list.includes(route.name);
})
allow_routes.forEach((route)=>{ // 将允许访问的路由动态添加到路由栈中
router.addRoute(route);
})
}

export default router;

核心代码在动态添加路由里面,主要利用了vue-router 4提供的APIrouter.addRoute,它能够给已经创建的路由实例继续添加路由信息.


我们先从vuex里面拿到当前用户的权限列表,然后遍历动态路由数组dynamic_routes,从里面过滤出允许访问的路由,最后将这些路由动态添加到路由实例里.


这样就实现了用户只能按照他对应的权限列表里的规则访问到相应的页面,至于那些他无权访问的页面,路由实例根本没有添加相应的路由信息,因此即使用户在浏览器强行输入路径越权访问也是访问不到的.


由于vue-router 4废除了之前的router.addRoutes,换成了router.addRoute.每一次只能一个个添加路由信息,所以要将allow_routes遍历循环添加.


动态添加路由这部分代码最好单独封装起来,因为用户第一次使用还没登录时,store.state.user是为空的,上面动态添加路由的逻辑会被跳过.那么在用户登录成功获取到权限列表的信息后,需要再把上面动态添加路由的逻辑执行一遍.


添加嵌套子路由


假如静态路由的形式如下,现在想把列表页添加到Tabs嵌套路由的children里面.

const routes = [
{
path: '/', //标签容器
name: 'Tabs',
component: Tabs,
children: [{
path: '', //首页
name: 'Home',
component: Home,
}]
}
]

export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
}
]

官方router.addRoute给出了相应的配置去满足这样的需求(代码如下).router.addRoute接受两个参数,第一个参数对应父路由的name属性,第二个参数是要添加的子路由信息.

router.addRoute("Tabs", {
path: "/list",
name: "List",
component: List,
});

切换用户信息是非常常见的功能,但是应用在切换成不同账号后可能会引发一些问题.例如用户先使用超级管理员登录,由于超级管理员能访问所有页面,因此所有页面路由信息都会被添加到路由实例里.


此时该用户退出账号,使用一个普通会员的账号登录.在不刷新浏览器的情况下,路由实例里面仍然存放了所有页面的路由信息,即使当前账号只是一个普通会员,如果他越权访问相关页面,路由还是会跳转的,这样的结果并不是我们想要的.


解决方案有两个.第一是用户每次切换账户后刷新浏览器重新加载,刷新后的路由实例是重新配置的所以可以避免这个问题,但是刷新页面会带来不好的体验.


第二个方案是当用户选择登出后,清除掉路由实例里面处存放的路由栈信息(代码如下).

const router = useRouter(); // 获取路由实例
const logOut = () => { //登出函数
//将整个路由栈清空
const old_routes = router.getRoutes();//获取所有路由信息
old_routes.forEach((item) => {
const name = item.name;//获取路由名词
router.removeRoute(name); //移除路由
});
//生成新的路由栈
routes.forEach((route) => {
router.addRoute(route);
});
router.push({ name: "Login" }); //跳转到登录页面
};

移除单个路由主要利用了官方提供的API,即router.removeRoute.


路由栈清空后什么页面都不能访问了,甚至登录页面都访问不了.所以需要再把静态的路由列表routes引入进来,使用router.addRoute再添加进入.这样就能让路由栈恢复到最初的状态.


内容权限控制


页面权限控制它能做到让不同角色访问不同的页面,但对于一些颗粒度更小的项目,比如希望不同的角色都能进入页面,但要求看到的页面内容不一样,这就需要对内容进行权限控制了.


假设某个后台业务系统的界面如下图所示.表格里面存放的是列表数据,当点击发布需求时跳转到新增页面.当勾选列表中的某一条数据后,点击修改按钮显示修改该条数据的弹出框.同理点击删除按钮显示删除该条数据的弹出框


假设项目需求该系统存在三个角色:职员、领导和高层领导.职员不具备修改删除以及发布需求的功能,他只能查看列表.当职员进入该页面时,页面上只显示列表内容,其他三个按钮移除.


领导角色保留列表发布需求按钮.高级领导角色保留页面上所有内容.


我们拿到图片后要先要对页面内容整体分析一遍,按照增删查改四个维度对页面内容进行归类.使用简称CURD来标识(CURD分别代表创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete)).


上图中列表内容属于查询操作,因此设定为R.凡是具备R权限的用户就显示该列表内容.


发布需求属于新增操作,设定凡是具备C权限的用户就显示该按钮.


同理修改按钮对应着U权限,删除按钮对应着D权限.


由此可以推断出职员角色在该页面的权限编码为R,它只能查看列表内容无法操作.


领导角色对应的权限编码为CR.高级领导对应的权限编码为CURD.


现在用户登录完成后,假设后端接口返回的数据如下(将这条数据存到vuex):

{
user_id:1,
user_name:"张三",
permission_list:{
"List":"CR", //权限编码
"Detail":"CURD" //权限编码
}
}

张三除了静态路由设置的页面外,他只能额外访问List列表页以及Detail详情页.其中列表页他只具备创建和新增权限,详情页他具备增删查改所有权限.那么当张三访问上图中的页面时,页面中应该只显示列表发布需求按钮.


我们现在要做的就是设计一个方案尽可能让页面内容方便被权限编码控制.首先创建一个全局的自定义指令permission,代码如下:

import router from './router';
import store from './store';

const app = createApp(App); //创建vue的根实例

app.directive('permission', {
mounted(el, binding, vnode) {
const permission = binding.value; // 获取权限值
const page_name = router.currentRoute.value.name; // 获取当前路由名称
const have_permissions = store.state.permission_list[page_name] || ''; // 当前用户拥有的权限
if (!have_permissions.includes(permission)) {
el.parentElement.removeChild(el); //不拥有该权限移除dom元素
}
},
});

当元素挂载完毕后,通过binding.value获取该元素要求的权限编码.然后拿到当前路由名称,通过路由名称可以在vuex中获取到该用户在该页面所拥有的权限编码.如果该用户不具备访问该元素的权限,就把元素dom移除.


对应到上面的案例,在页面里按照如下方式使用v-permission指令.

<template>
<div>
<button v-permission="'U'">修改</button> <button v-permission="'D'">删除</button>
</div>
<p>
<button v-permission="'C'">发布需求</button>
</p>

<!--列表页-->
<div v-permission="'R'">
...
</div>
</template>

将上面模板代码和自定义指令结合理解一下就很容易明白整个内容权限控制的逻辑.首先前端开发页面时要将页面分析一遍,把每一块内容按照权限编码分类.比如修改按钮就属于U,删除按钮属于D.并用v-permission将分析结果填写上去.


当页面加载后,页面上定义的所有v-permission指令就会运行起来.在自定义指令内部,它会从vuex中取出该用户所拥有的权限编码,再与该元素所设定的编码结合起来判端是否拥有显示权限,权限不具备就移除元素.


虽然分析过程有点复杂,但是以后每个新页面想接入权限控制非常方便.只需要将新页面的各个dom元素添加一个v-permission和权限编码就完成了,剩下的工作都交给自定义指令内部去做.


延伸



如果项目中删除操作并不是单独放置在一个按钮,而是与列表捆绑在一起放在表格的最后一列,如下图所示.

这样的界面样式在实际工作中非常常见,但似乎上面的v-permission就并不能友好的支持这样的样式.自定义指令在这种情况下虽然不能用,但我们仍然可以采用相同的思路去优化我们现有的代码结构.


例如模板代码如下.整个列表被封装成了一个组件List,那么在List内部就可以写很多的逻辑控制。


比如List组件内也可以通过vuex拿到该用户在当前页面的权限编码,如果发现具备D权限就显示列表中最后删除那一列,否则就不显示.至于整个列表的显示隐藏仍然可以使用v-permission来控制.

<template>
<div>
<button v-permission="'C'">添加资源</button>
</div>

<!--列表页-->
<List v-permission="'R'">
...
</List>
</template>

动态导航

下图中的动态导航也是实际工作中非常常见的需求,比如销售部所有成员只能看到销售模块下的两个页面,同理采购部成员只能看到采购模块下的页面.

下面侧边栏导航组件需要根据不同权限显示不同的页面结构,以满足不同角色群体的要求.

我们要把这种需要个性化设置的组件与上面使用v-permission控制的模式区分开.上面那些页面之所以能使用v-permission来控制,主要原因是因为产品经理在设计整个软件系统的页面时是按照增删查改的规则进行的.因此我们就能抽象出其中存在的共性与规律,再借助自定义指令来简化权限系统的开发.


但是侧边栏组件一般全局只有一个,没有什么特别的规律而言,那就只需要在组件内部使用v-if依据权限值动态渲染就行了.


比如后台接口如下:

{
user_id:1,
user_name:"张三",
permission_list:{
"SALE":true, //显示销售大类
"S_NEED":"CR", //权限编码
"S_RESOURCE":"CURD", //权限编码
}
}

张三拥有访问需求资源页面,但注意SALE并没有与哪个页面对应上,它仅仅只是表示是否显示销售这个一级导航.

接下来在侧面栏组件通过vuex拿到权限数据,再动态渲染页面就可以了.

<template>
<div v-if="permission_list['HOME']">系统首页</div>
<div v-if="permission_list['SALE']">
<p>销售</p>
<div v-if="permission_list['S_NEED']">需求</div>
<div v-if="permission_list['S_RESOURCE']">资源</div>
</div>
<div v-if="permission_list['PURCHASE']">
<p>采购</p>
<div v-if="permission_list['P_NEED']">需求</div>
<div v-if="permission_list['P_RESOURCE']">资源</div>
</div>
</template>

尾言

前端提供的权限控制为应用加固了一层保险,但同时也要警惕前端设定的校验都是可以通过技术手段破解的.权限问题关乎到软件系统所有数据的安危,重要性不言而喻.

为了确保系统平稳运行,前后端都应该做好自己的权限防护.

 原文链接:https://juejin.cn/post/6949453195987025927



收起阅读 »

iOS 一个比较完美的 Growing TextView

iOS 一个比较完美的 Growing TextView文章缘由现在都 2019 年了,App 中使用自动增高的输入框已经很常见了,即时通讯的 Chat 界面、社交类 App 的评论功能都可以看到自增高输入框。但写出一个自增高输入框容易,写好难。现在市面上一些...
继续阅读 »

iOS 一个比较完美的 Growing TextView

文章缘由
现在都 2019 年了,App 中使用自动增高的输入框已经很常见了,即时通讯的 Chat 界面、社交类 App 的评论功能都可以看到自增高输入框。但写出一个自增高输入框容易,写好难。现在市面上一些主流 App 的输入框依然会有一些瑕疵,例如:文字挡住一部分、粘贴大量文字时出现偏移,具体问题下面详细分析。
现在 iOS 开发已经过了搭建 UI 和普通业务功能的初级阶段,App 要想赢得用户的青睐,除了 App 的功能、UI 设计,交互体验的细节处理至关重要。一般用户只要使用输入框能正常输入即可,90% 的用户不会察觉输入框的一些细节,但作为开发人员应该知道这些细节(bug)并做出处理。

主流 App 的输入框之痛

粘贴文本出现文字偏移
这个问题严格来说算 bug,毕竟粘贴还是一个很常见的操作。


挡住部分文字
一个输入框要么显示 N 行文字、要么显示 N + 1行,如果显示 N + 0.5 行就有点不可思议了。这个问题对 App 功能没有任何影响,但这么多 App 竟然都有这个问题而且非常普遍,是我始料未及的。测试了多个 App 后,只有QQ的输入框做的最好,粘贴、遮挡文字等问题根本不存在。






比较完美的输入框
写出一个自增高的输入框还是比较容易的,大致过程就是给 textView 添加左、右、下/上、高度四个约束,然后监听文字变化的通知,进而修改输入框的高度。如果想写好,就要花时间打磨了。我接下来主要说一下大家可能遇到的一些细节问题。

1、粘贴文本,文字偏移

我的做法是继承 UITextView 重写 setBounds 方法,重新调整contentOffset

- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
// NSLog(@”bounds:%@”, NSStringFromCGRect(bounds));
if (self.contentSize.height <= self.bounds.size.height + 1){
self.contentOffset = CGPointZero; // Fix wrong contentOfset
} else if (!self.tracking) {
CGPoint offset = self.contentOffset;
if (offset.y > self.contentSize.height - bounds.size.height) {
offset.y = self.contentSize.height - bounds.size.height;
if (!self.decelerating && !self.tracking && !self.dragging) {
self.contentOffset = offset;
}
// Fix wrong contentOfset when paster huge text
}
}
}

2、文字离输入框顶部间隙时大时小
正常情况下滚动输入框的文字,文字可以滚动到控件顶部。而 QQ 的输入框,不管怎么滑动文字,文字和输入框顶部都有一段固定间隔。


先了解输入框的一个属性textContainerInset,这个值默认是 (8, 0, 8, 0),就是说默认情况下文字和输入框顶部有 8pt 的偏移,所以当文字输入较多的时候文字向上滚动,那么文字和控件顶部间隙会减小到 0。
实现QQ输入框的效果,我能想到的方案是把textContainerInset全设置为 0(或者top/bottom为0),这样文字就紧挨着输入框,文字和输入框顶部的固定间距就是 0 了。接着要给UITextView添加一个 containerView ,containerView 竖向比 UITextView 高出一部分,从而实现 顶部/底部 的固定间距。

3、挡住部分文字
这个问题是因为 单行文字高度 最大行数 != 输入框的最大高度,输入框的最大高度可不是随便设置的,先确定输入框的font和最大行数,font.lineHeight 行数就是输入框的最大高度。这样就不会出现文字挡住一部分的问题了

GrowTextView

接下来就是我自己写的自增高输入框了,目前没发现什么问题,至少没有上面涉及的问题。




收起阅读 »

【直播回放】主题:iOS Runtime 项目实际应用与面试对刚!

视频回放: 直播主题:iOS  Runtime 项目实际应用与面试对刚!嘉宾介绍:Zuyu   环信生态开发者kol分享大纲:1. 如何使用runtime 动态创建类2. 如何使用runti...
继续阅读 »

视频回放:

直播主题:

iOS  Runtime 项目实际应用与面试对刚!

嘉宾介绍:

Zuyu   环信生态开发者kol

分享大纲:

1. 如何使用runtime 动态创建类

2. 如何使用runtime 进行hook

3. Method Swizzling 误区详解 ,让你面试和开发如虎添翼

直播交流群:

添加环信冬冬微信,备注:428runtime



收起阅读 »

超过 js 的 number 类型最大值(9007 1992 5474 0992)的解决办法

bug经过:点击修改无法展示信息(修改时调用queryOne,以id(long)为值,页面传过去的id=1480042498255640-00 ,在数据库中该id=148004249825564012,即错误的id)根本原因:js的number类型有个最大值(...
继续阅读 »

bug经过:点击修改无法展示信息(修改时调用queryOne,以id(long)为值,页面传过去的id=1480042498255640-00 ,在数据库中该id=148004249825564012,即错误的id)

根本原因:

js的number类型有个最大值(安全值)。即2的53次方,为9007199254740992。如果超过这个值,那么js会出现不精确的问题。这个值为16位。

解决方法:

1.后端发字符串类型。

将后端发过来的long类型转为string类型再向前端传。如果向前端传的是DAO集合,则每个DAO都需要转类型,太过于繁琐。想想就算了。

2.在userDao中加入一个字段

如果项目已经成型并且修改数据库会造成不可预料的问题那么可以在User对象中再增加一个String类型id映射字段,如下
    private Long userId;
    private String userIdStr;
    public String getUserIdStr() {
        return this.userId+"";
    }
    public void setUserIdStr(String userIdStr) {
        this.userIdStr = userIdStr;

    }

这个方法是比较靠谱的,确实可以正常的显示数据,查询单个数据id的值都是正确的。但修改用户时无法获取前端传过来的userDao中的userIdStr的值,因为上面的getUserIdStr()不能获取userIdStr的值(如果id没有值)。

3.控制用户新建数据时id的长度。兜兜转转觉得这个最方便。

温馨提示:以后设计表字段时尽量用varchar类型。

原文链接:https://blog.csdn.net/sunmerZeal/article/details/80844843


收起阅读 »

JavaScript 对象

为什么要有对象?如果有一组相关的数据,松散的存储不利于使用,存入数组中受下标限制又必须有固定的顺序,而对象可以自定义名称存储一系列无序列表的相关数据什么是对象?现实生活中的对象:万物皆可对象,对象是一个具体的事物,一个具体的事物就会有行为和特征举例:一部车,一...
继续阅读 »

为什么要有对象?

  • 如果有一组相关的数据,松散的存储不利于使用,存入数组中受下标限制又必须有固定的顺序,而对象可以自定义名称存储一系列无序列表的相关数据

什么是对象?

现实生活中的对象:万物皆可对象,对象是一个具体的事物,一个具体的事物就会有行为和特征

举例:一部车,一个手机

车是一类事物。,门口停的那辆车才是对象

特征:红色、四个轮子

行为:驾驶、刹车

  • JavaScript 中的对象:
  1. JavaScript 中的对象其实就是生活中对象的一个抽象
  2. JavaScript 的对象是无序属性的集合
  • 其属性可以包含基本值、对象或函数。对象就是一组没有顺序的值。我们可以把 JavaScript 中的对象想象成键值对,其中值可以是数据和函数
  • 对象的行为和特征:
  1. 特征---在对象中用属性表示
  2. 行为---在对象中用方法表示

对象字面量(用字面量创建对象)

  • 创建一个对象最简单的方法是使用对象字面量赋值给变量。类似数组
  • 对象字面量语法:{}
  • 内部可以存放多条数据,数据与数据之间用逗号分隔,最后一个后面不要加逗号
  • 每条数据都是有属性名和属性值组成,键值对写法: k : v
  • k:属性名
  • v:属性值,可以实任意类型的数据,比如简单类型数据、函数、对象
var obj = {
k:v,
k:v,
k:v,
};

区分属性和方法

  • 属性:对象的描述性特征,一般是名词,相当于定义在对象内部的变量
  • 方法:对象的行为和功能,一般是动词,定义在对象中的函数

调用对象内部属性和方法的语法

  • 用对象的变量名打点调用某个属性名,得到属性值
  • 在对象内部用 this 打点调用属性名。this 替代对象
  • 用对象的变量名后面加 [] 调用,[] 内部是字符串格式的属性名
  • 调用方法时,需要在方法名后加 () 执行
/* 
现实生活中:万物皆对象 对象是一个具体事物 看得见摸得着的实物

对象是一组无序的相关属性和方法的集合 所有事物的是对象

对象是由属性和方法组成的
属性:事物的特征 在对象中用属性来表示(常用名词)
方法:事物的行为 在对象中用方法来表示(常用动词)

对象的字面量:就是花括号{} 里面包含了表达这个具体实物(对象)的属性和方法
*/
//创建一个空的对象
var obj = {
uname:'张三',
age:'男',
sayHi: function () {
console.log('Hi!');
console.log(this.uname + "向你说您好");
}
}
// 1.我们在创建对象时我们采用键值对的形式 键 属性名 : 属性 属性值
// 2.多个属性或者方法中间用逗号隔开
// 3.方法冒号后面跟的是一个匿名函数

// 使用对象
// 1)调用对象的属性 我们采取 对象名.属性名
console.log(obj.uname);
// 2)调用对象也可以 对象名['属性名']
console.log(obj['age']);
// 3)调用对象的方法 对象.方法名
obj.sayHi();


更改对象内部属性和方法的语法

  • 更改属性的属性值方法:先调用属性,再等号赋值
obj.age = 19;
  • 增加新的属性和属性值:使用点语法或者 [] 方法直接定义新属性,等号赋值
obj.height = 180;
  • 删除一条属性:使用一个 delete 关键字,空格后面加属性调用
delete obj.sex;

new Object() 创建对象

  • object() 构造函数,是一种特殊的函数。主要用来再创建对象时初始化对象,即为对象成员变量赋值初始值,总与 new 运算符一起使用在创建对象的语句中
  1. 构造函数用于创建一类对象,首字母要大写
  2. 构造函数要和 new 一起使用才有意义
// 利用new object 创建对象
var obj = new Object();//创建了一个空对象
obj.name = '张三';
obj.age = 18;
obj.sex = '男';
obj.sayHi = function() {
console.log('Hi~');
}
//1.我们是利用等号赋值的方法给对象 属性和方法 赋值
//2.每个 属性和方法 用分号结束

// 调用
console.log(obj.name);
console.log(obj['sex']);
obj.sayHi();


new 在执行时会做四件事情

  • new 会在内存中创建一个新的空对象
  • new 会让 this 指向这个新的对象
  • 执行构造函数 目的 :给这个新对象加属性和方法
  • new 会返回这个新的对象

工厂 函数创建对象

  • 如果要创建多个类似的对象,可以将 new Object() 过程封装到一个函数中,将来调用函数就能创建一个对象,相当于一个生产对象的函数工厂,用来简化代码
// 我们为什么需要使用函数
// 就是因我们前面两种创建对象的方式一次只能创建一次对象
var ldh = {
uname: '刘德华',
age: 55,
sing = function() {
console.log('冰雨');
}
}
var zxy = {
uname: '张学友',
age: 58,
sing = function() {
console.log('李香兰');
}
}
// 因为我们一次创建一个对象 里面有很多的属性和方法是大量相同的 我们只能复制
// 因此我们可以利用函数的方法 重复这些相同的代码
// 又因为这个函数不一样 里面封装的不是普通代码 而是对象
// 函数 可以把我们对象里面一些相同的属性和方法抽象出来封装到函数里面

用 工厂方法 函数创建对象

function createStar(uname, age, sex) {
//创建一个空对象
var Star = new Object();
//添加属性和方法,属性可以接受参数的值
Star.name = uname;
Star.age = age;
Star.sex = sex;

Star.sing = function(sang) {
console.log(sang);
}
//将对象做为函数的返回值
return Star;
}

var p1 = createStar("张三",18,"男");

自定义构造函数

  • 比工厂方法更加简单
  • 自定义一个创建具体对象的构造函数,函数内部不需要 new 一个构造函数的过程,直接使用 this 代替对象进行属性和方法的书写,也不需要 return 一个返回值
  • 使用时,利用 new 关键字调用自定义的构造函数即可
  • 注意:构造函数的函数名首字母需要大写,区别于其他普通函数名
// 利用构造函数创建对象
// 我们需要创建四大天王的对象 相同的属性: 名字 年龄 性别 相同的方法 : 唱歌
// 构造函数的语法格式
/*
function 构造函数名() {
this.属性 = 值;
this.方法 = fucntion() {}
}
// 调用构造函数
new 构造函数名();
*/

function Star(uname, age, sex) {
this.name = uname;
this.age = age;
this.sex = sex;

this.sing = function(sang) {
console.log(sang);
}
}
var ldh = new Star('刘德华', 18, '男');
console.log(typeof ldh);//object
console.log(ldh.name);
console.log(ldh.age);
console.log(ldh.sex);
ldh.sing('冰雨');
// 1.构造函数首字母必须大写
// 2.构造函数不需要return就能返回结果
// 3.调用函数返回的是一个对象
var zxy = new Star('张学友', 29, '男')
console.log(zxy);
// 4.我们调用构造函数必须使用new

对象遍历

  • for in 循环也是循环的一种,专门用来遍历对象,内部会定义一个 k 变量,k 变量在每次循环时会从第一个开始接收属性名,一直接收到最后一个属性名,执行完后会跳出循环。
  • 简单的循环遍历:输出每一项的属性名和属性值
//循环遍历输出每一项
for(var k in obj){
console.log(k + "项的属性值" + obj[k]);
}

案例:

//遍历对象
var obj = {
uname: '王二狗',
age: 18,
sex: '男'
}
console.log(obj.uname);
console.log(obj.age);
console.log(obj.sex);
//但是一个一个输出很累

// 因此我们引出 for...in...语句 --用于对数组或者对象的属性进行循环操作

/*
基本格式:
for (变量 in 对象) {

}

*/
for (k in obj) {
console.log(k); //k变量输出 得到的是属性名
console.log(obj[k]); //obj[k] 输出对象各属性的属性值 切记不要用obj.k 那样就变成输出 k 属性名的属性值了 ---!!!:k是变量不加''
}

简单类型和复杂类型的区别

  • 我们已经学过简单类型数据和一些复杂类型的数据,现在来看一下他们之间的区别有哪些
  • 基本类型又叫做值类型,复杂类型又叫做引用类型
  • 值类型:简单数据类型,基本数据类型,在存储时,变量中存储的是值本身,因此叫做值类型
  • 引用类型:复杂数据类型,在存储时,变量中存储的仅仅是地址(引用),因此叫做引用数据类型

堆和栈

  • JavaScript 中没有堆和栈的概念,此处我们用堆和栈来讲解,目的是方便理解和方便以后的学习
  • 堆栈空间分配区别
  1. 栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值相等
  2. 堆(操作系统):存储复杂类型(对象),一般由程序员分配释放,若程序员不释放,有垃圾回收机制回收

简单数据类型(基本类型)在内存中的存储

变量中如果存储的是简单类型的数据,那么变量中存储的是值本身,如果将变量赋值给另一个变量,是将内部的值赋值一份给了另一个变量,两个变量之间没有联系,一个变化,另一个不会同时变化

var a = 5;
var b = a; //将 a 内部存储的数据 5 复制了一份
a = 10;
console.log(a);
console.log(b);
// 因此 a 和 b 发生改变,都不会互相影响


复杂数据类型(引用类型)在内存中的存储

如果将复杂数据赋值给一个变量,复杂类型的数据会在内存中创建一个原型,而变量中存储的是指向对象的一个地址,如果将变量赋值给另一个变量,相当于将地址复制一份给了新的变量,两个变量的地址相同,指向的是同一个原型,不论通过哪个地址更改了原型,都是在原型上发生的更改,两个变量下次访问时,都会发生变化

// 复杂数据类型
var p1 = {
name: "zs",
age: 18,
sex: "male"
}
var p = p1; //p1 将内部存储的指向对象原型的地址复制给了 p
// 两个变量之间是一个联动的关系,一个变化,会引起另一个变化
p.name = "ls";
console.log(p);
console.log(p1);

// 数组和函数存储在变量中时,也是存储的地址
var arr = [1,2,3,4];
var arr2 =arr;
arr[4] = 5;
console.log(arr);
console.log(arr2);

内置对象

  • JavaScript 包含:ECMA DOM BOM
  • ECMAscript 包含:变量、数据、运算符、条件分支语句、循环语句、函数、数组、对象···
  • JavaScript 的对象包含三种:自定义对象 内置对象 浏览器对象
  • ECMAscript 的对象:自定义对象 内置对象
  • 使用一个内置对象,只需要知道对象中有哪些成员,有什么功能,直接使用
  • 需要参考一些说明手册 W3C / MDN

MDN

Mozilla 开发者网络(MDN) 提供有关开放网络技术(Open Web)的信息,包括 HTML、CSS 和 万维网 HTML5 应用的API

如何学习一个方法?

  1. 方法的功能
  2. 参数的意义和类型
  3. 返回值意义和类型
  4. demo 进行测试

Math 对象

  • Math 对象它具有数学常数和函数的属性和方法,我们可以直接进行使用
  • 根据数学相关的运算来找 Math 中的成员(求绝对值,取整)

演示:

Math.PI圆周率
Math.random()生成随机数
Math.floor()/Math.ceil()向下取整/向上取整
Math.round()取整,四舍五入
Math.abs()绝对值
Math.max()/Math.min()求最大和最小值
Math.sin()/Math.cos()正弦/余弦
Math.power()/Math.sqrt()求指数次幂/求平方根

Math.random()

如何求一个区间内的随机值

Math.random()*(max_num - min_num) + min_num

Math.max()/Math.min()

// Math数学对象 不是一个构造函数 所以我们不需要用new来调用 而是直接使用里面的属性和方法即可
console.log(Math.PI); //一个属性值 圆周率
console.log(Math.max(99, 199, 299)); //299
console.log(Math.max(-10, -20, -30)); //-10
console.log(Math.max(-10, -20, '加个字符串')); //NaN
console.log(Math.max()); //-Infinity
console.log(Math.min(99, 199, 299)); //99
console.log(Math.min()); //Infinity

创建数组对象的第二种方式

字面量方式

new Array() 构造函数方法

// 字面量方法
// var arr = [1,2,3];

// 数组也是对象,可以通过构造函数生存
//空数组
var arr = new Array();
//添加数据,可以传参数
var arr2 = new Array(1,2,3);
var arr3 = new Array("zs","ls","ww");
console.log(arr);
console.log(arr2);
console.log(arr3);

// 检测数组的数据类型
console.log(typeof(arr));//object
console.log(typeof(arr2));//object
console.log(typeof(arr3));//object

由于 object 数据类型的范围较大,所以我们需要一个更精确的检测数据类型的方法

  • instanceof 检测某个实例是否时某个对象类型
var arr = [1,2,3];
var arr1 = new Array(1,2,3)
var a = {};
// 检测某个实例对象是否属于某个对象类型
console.log(arr instanceof Array);//true
console.log(arr1 instanceof Array);//true
console.log(a instanceof Array);//true

function fun () {
console.log(1);
}
console.log(fun instanceof Function);//true

数组对象的属性和方法

toString()

  • toString() 把数组转换成字符串,逗号分隔每一项
// 字面量方法
var arr = [1,2,3,4];

// toString() 方法:可以转字符串
console.log(arr.toString());//1,2,3,4

数组常用方法

首尾数据操作:

  • push() 在数组末尾添加一个或多个元素,并返回数组操作后的长度
// 字面量方法
var arr = [1,2,3,4];

// 首尾操作方法
// 尾推,参数是随意的,有一个或者多个
console.log(arr.push(5,6,7,8)); //8(数组长度)
console.log(arr);//[1,2,3,4,5,6,7,8]
console.log(arr.push([5,6,7,8])); //5(数组长度)
console.log(arr);//[1,2,3,4,Array(4)]
  • pop() 删除数组最后一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];

//尾删,删除最后一项数据
// 不需要传参
console.log(arr.pop());//4(被删除的那一项数据)
console.log(arr);//[1,2,3]
  • shift() 删除数组第一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];

// 首删,删除第一项数据,不需要传参
console.log(arr.shift());//1
console.log(arr);//[2,3,4]
  • unshift() 在数组开头添加一个或多个元素,并返回数组的长度
// 字面量方法
var arr = [1,2,3,4];

// 首添,参数与 push 方法类似
console.log(arr.unshift(-1,0));//6
console.log(arr);//[-1,0,1,2,3,4]


案例:将数组的第一项移动到最后一项

// 字面量方法
var arr = [1,2,3,4];

// 将数组的第一项移动到最后一项
// 删除第一项
// 将删除的项到最后一项
arr.push(arr.shift());
console.log(arr);//[2,3,4,1]
arr.push(arr.shift());
console.log(arr);//[3,4,1,2]
arr.push(arr.shift());
console.log(arr);//[4,1,2,3]
arr.push(arr.shift());
console.log(arr);//[1,2,3,4]


数组常用方法

合并和拆分:

concat()

  • 将两个数组合并成一个新的数组,原数组不受影响。参数位置可以是一个数组字面量、数组变量、零散的值
// 字面量方法
var arr = [1,2,3,4];
// 合并方法
// 参数:数组 数组的变量 零散的值
// 返回值:一个新的拼接后的数组
var arr1 = arr.concat([5,6,7]);
console.log(arr);//[1,2,3,4]
console.log(arr1);//[1,2,3,4,5,6,7]

slice(start,end)

  • 从当前数组中截取一个新的数组,不影响原来的数组,返回一个新的数组,包含从 start 到end (不包括该元素)的元素
  • 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取


删除,插入,替换:

splice(index,howmany,element1,element2,...)

用于插入、删除或替换数组的元素

index:删除元素的开始位置

howmany:删除元素的个数,可以是0

element1,element2:要替换的新数据

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取


位置方法:

indexOf() 查找数据在数组中最先出现的下标

lastndexOf() 查找数据在数组中最后一次出现的下标

注意:如果没有找到返回-1

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,4,5];

// 查找某个元素在数组中从前往后第一次 出现位置的下标
console.log(arr.indexOf(4));//3 (数字4的下标)
// 查找某个元素在数组中从前往后最后一次出现位置的下标
console.log(arr.lastIndexOf(4));//10
console.log(arr.lastIndexOf(11));//-1 (代表数组中不存在11这个数据)


排序方法:

倒序:reverse() 将数组完全颠倒,第一项变成最后一项,最后一项变成第一项

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 数组倒序
console.log(arr.reverse());//[10,9,8,7,6,5,4,3,2,1]

从大到小排序:sort() 默认根据字符编码顺序,从大到小排序

如果想要根据数值大小进行排序,必须添加sort的比较函数参数

该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数具有两个参数 a 和 b,根据 a 和 b 的关系作为判断条件,返回值根据条件分为三个分支,整数、负数、0:

返回值是负数-1:a 排在 b 前面

返回值是整数1:a 排在 b 后面

返回值是0:a 和 b 的顺序保持不变

人为控制的是判断条件

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,20,30];
// 排序,默认按照字符编码顺序来排序
arr.sort();
console.log(arr);//[1,10,2,20,3,30,4,5,6,7,8,9] (如果不添加函数)

// 添加一个比较函数
arr.sort(function(a,b) {
if (a > b) {
return -1;//表示 a 要排在 b 前面
} else if (a < b) {
return 1;//表示 a 要排在 b后面
} else {
return 0;;//表示 a 和 b 保持原样,不换位置
}
});
console.log(arr);//[30,20,10,9,8,7,6,5,4,3,2,1] (添加函数之后)
// 想要从小到大排序只要将函数 大于小于 号,反向即可


转字符串方法:将数组的所有元素连接到一个字符串中

join() 通过参数作为连字符将数组中的每一项用连字符连成一个完整的字符串

var arr = [1,2,3,4,5,6,7,8,9,10,20,30];

// 转字符串方法
var str = arr.join();
console.log(str);//1,2,3,4,5,6,7,8,9,10,20,30
var str = arr.join("*");
console.log(str);//1*2*3*4*5*6*7*8*9*10*20*30
var str = arr.join("");
console.log(str);//123456789102030


清空数组方法总结

var arr = [1,2,3,4,5,6,7,8,9,10,20,30];

// 方式1 推荐
arr = [];

// 方式2
arr.length = 0;

// 方式 3
arr.splice(0,arr.length);

基本包装类型

为了方便操作简单数据类型,JavaScript 还提供了特殊的简单类型对象:String 基本类型时没有方法的。

当调用 str.substring() 等方法的时候,先把 str 包装成 String 类型的临时对象,再调用 substring 方法,最后销毁临时对象

// 基本数据类型:没有属性值和方法
// 对象数据类型:有属性和方法
// 但是:字符串是可以调用一些属性和方法
var str = "这是一个字符串";
var str2 = str.slice(3,5);
console.log(str2);//个字

// 基本包装类型,基本类型的数据在进行一些特殊操作时,会暂时被包装成一个对象,结束后再被销毁
// 字符串也有一种根据构造函数创建方法
var str3 = new String("abcdef");
console.log(str);//这是一个字符串
console.log(str3);//Sring{"abcdef"}

// 模拟计算机的工作
var str4 = new String(str);
// 字符串临时被计算机包装成字符串对象
var str2 = str4.slice(3,5);
str4 = null;


字符串的特点

字符串是不可变的

// 定义一个字符串   
var a = "abc";
a = "cde";
// 字符串是不可变的,当 a 被重新赋值时,原来的值 "abc" 依旧在电脑内存中
// 在 JavaScript 解释器 固定时间释放内存的时候可能会被处理掉

由于字符串的不可变,在大量拼接字符串的时候会有效率问题

由于每次拼接一个字符串就会开辟一个空间去存储字符串

// 大量拼接字符串也效率问题
var sum = "";
for(var i = 1; i <= 10000000; i++) {
sum += i;
}
console.log(sum);

测试一下我们发现,浏览器转了一会才显示出来

因此在我们以后,不要大量用字符串拼接的方法,以后我们会有更好的方法替代


字符串属性

长度属性:str.length

字符串长度指的是一个字符串中所有的字符总数


字符串方法

indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置

  • 找到指定的字符串在原字符串中第一次出现的位置的下标。如果子字符串在原字符串中没有,返回值是 -1

concat() 方法用于连接两个或多个字符串

  • 参数比较灵活,可以是字符串、或者字符串变量、多个字符串
  • 生成的是一个新的字符串,原字符串不发生变化

split() 方法用于把一个字符串分割成字符串数组(和数组中的 join() 方法是对应的)

  • 参数部分是割符,利用分割符将字符串分割成多个部分,多个部分作为数组的每一项组成数组
  • 如果分割符是空字符串,相当于将每个字符拆分成数组中的每一项
// 定义一个字符串
var str = "这是一个字符串,abc, $%#";

// 长度属性
console.log(str.length);//18

// charAt() 返回指定的下标位置的字符
console.log(str.charAt(6));//串 (字符串对象是一种伪数组,所以需要从 0 开始数)

// indexOf() 返回子串在原始字符串中第一次出现位置的下标
console.log(str.indexOf("字"));//4
console.log(str.indexOf("字符串"));//4
console.log(str.indexOf("字 符串"));//-1

// concat() 字符串拼接
var str2 = str.concat("哈哈哈","普通");
console.log(str);//这是一个字符串,abc, $%#
console.log(str2);//这是一个字符串,abc, $%#哈哈哈普通

// split() 分割字符串成一个数组
var arr = str.split("")//一个一个字符分割
console.log(arr);
var arr = str.split(",")//按逗号进行分割
console.log(arr);

// 字符串内容倒置
var arr = str.split("")//一个一个字符分割
arr.reverse();
strn = arr.join("");
console.log(strn);
// 用连续打点方式化简
var arr = str.split("").reverse().join("")
console.log(arr);


toLowerCase() 把字符串转换为小写

toUpperCase() 把字符串转换为大写

  • 将所有的英文字符转为大写或者小写
  • 生成的是新的字符串,原字符串不发生变化
// 大小写转换
var str1 = str.toUpperCase();
console.log(str);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%#
var str2 = str1.toLowerCase();
console.log(str2);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%# --字符串本身不会发生改变


截取字符串的三种方法

slice() 方法可以提取字符串的某个部分,并以新的字符串返回被提取的部分

  • 语气:slice(start,end)
  • 从开始位置截取到结束位置(不包括结束位置)的字符串
  • 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾

substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符

  • 语法:substr(start,howmany)
  • 从开始位置截取到指定长度的字符串
  • start 参数区分正负。正值表示下标位置,负值表示从后往前数第几个位置
  • howmany 参数必须为正数,也可以不写,不写表示从 start 截取到最后

substring() 方法用于提取字符串中介于两个指定下标之间的字符

  • 语法:substring(start,end)
  • 参数只能为正数
  • 两个参数都是指代下标,两个数字大小不限制,执行方法之前会比较一下两个参数的大小,会用小的数当做开始位置,大的当作结束位置,从开始位置截取到结束位置但是不包含结束位置
  • 如果不写第二个参数,从开始截取到字符串结尾
// 截取字符串:三种
// slice(start,end) 从开始位置截取到结束位置,但是不包含结束位置
var str1 = str.slice(3,7);
console.log(str1);//个字符串
var str1 = str.slice(-7);
console.log(str1);//, $%#

// substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符
var str2 = str.substr(6);
console.log(str2);//串,abc, $%#
var str2 = str.substr(6,3);
console.log(str2);//串,a

// substring() 参数必须为整数 小的数当做开始位置,大的当作结束位置
var str3 = str.substring(3,7);
console.log(str3);//个字符串

注意:如果参数取小数会自动省略小数部分

原文链接:https://zhuanlan.zhihu.com/p/366886609

收起阅读 »

JavaScript 函数

为什么要有函数?如果要在多个地方求某个数的约数个数,应该怎么做函数的概念函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行...
继续阅读 »

为什么要有函数?

  • 如果要在多个地方求某个数的约数个数,应该怎么做


函数的概念

  • 函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行。
  • 函数的作用就是封装一段代码,将来可以重复使用

函数声明

  • 函数声明又叫函数定义,函数必须先定义然后才能使用
  • 如果没有定义函数直接使用,会出现一个引用错误
  • 函数声明语法:
function 函数名 (参数) {
封装的结构体;
}

特点:函数声明的时候,函数体并不会执行,只有当函数被调用的时候才会执行

函数调用

  • 调用方法:函数名();
  • 函数调用也叫作函数执行,调用时会将函数内部封装的所有的结构体的代码立即执行
  • 函数内部语句执行的位置,与函数定义的位置无关,与函数调用位置有关
  • 函数可以一次调用,多次执行

函数的参数1

  • 我们希望函数执行结果不是一成不变的,可以根据自定义的内容发生一些变化
  • 函数预留了一个接口,专门用于让用户自定义内容,使函数发生一些执行效果变化
  • 接口:就是函数的参数,函数参数的本质就是变量,可以接收任意类型的数据,导致函数执行结果根据参数不同,结果也不同
  • 一个函数可以设置 0 个或者多个参数,参数之间用逗号分隔

案例:累加求和函数

        // 函数:封装了一段可以重复调用执行的代码块,通过代码块可以实现大量代码的重复使用

// 1、声明一个累加求和函数

// num1~num2之间所有数之和
function getSum(num1,num2) {
var sum = 0;
for (var i = num1; i <= num2; i++) {
sum += i;
}
console.log(sum);
}

// 2、调用函数
getSum(1,100);
getSum(11,1100);
getSum(321,1212);

函数的参数2

  • 函数的参数根据书写位置不同,名称也不相同
  • 形式参数:定义的 () 内部的参数,叫做形式参数,本质是变量,可以接收实际参数传递过来的数据。简称形参
  • 实际参数:调用的 () 内部的参数,叫做实际参数,本质就是传递的各种类型的数据,传递给每个形参,简称实参
  • 函数执行过程,伴随传参的过程

函数的参数优点

  • 不论使用自己封装的函数,还是其他人封装的函数,只需要知道传递什么参数,执行什么功能,没必要知道内部的结构是什么
  • 一般自己封装的函数或者其他人封装的函数需要有一个 API 接口说明,告诉用户参数需要传递什么类型的数据,实现什么功能

函数的返回值

  • 函数能够通过参数接收数据,也能够将函数执行结果返回一个值
  • 利用函数内部的一个 return 的关键字设置函数的返回值
  • 作用 1 :函数内部如果结构体执行到一个 return 的关键字,会立即停止后面代码的执行
  • 作用 2 : 可以在 return 关键字后面添加空格,空格后面任意定义一个数据字面量或者表达式,函数在执行完自身功能之后,整体会被 return 矮化成一个表达式,表达式必须求出一个值继续可以参加程序,表达式的值就是 return 后面的数据

案例:求和函数

var num1 = Number(prompt("请输入第一个数:"));
var num2 = Number(prompt("请输入第二个数:"));
function sum(a,b) {
return a + b;
}
console.log(sum(num1,num2));

函数的返回值应用

  • 函数如果有返回值,执行结果可以当成普通函数参与程序
  • 函数如果有返回值,可以作为一个普通数据赋值给一个变量,甚至赋值给其他函数的实际参数
  • 注意:如果函数没有设置 return 语句,那么函数有默认的返回值 undefined ; 如果函数使用 return 语句,但是 return 后面没有任何值,那么函数的返回值也是 undefined
// 1、return 终止函数
function getSum(num1, num2) {
return num1 + num2;
console.log('return除了返回值还起到终止函数的作用,所以在return后面的代码均不执行!');
}
console.log(getSum(10, 20));

// 2、return 只能返回一个值
function fn(num1,num2) {
return num1, num2; //返回的结果是最后一个值
}
console.log(fn(10, 20));

// 3、 我们求任意两个数 加减乘除 的结果
function getResult(num1, num2) {
return ['求和:' + (num1 + num2), '求差:' + (num1 - num2), '求积:' + (num1 * num2), '求商:' + (num1 / num2)];
}
re = getResult(10, 20);
console.log(re);
// 想要输出多个值可以利用数组
// 4、我们的函数如果有return 则返回的是 return后面的值 如果函数没有 return 则返回undefined

函数表达式

  • 函数表达式是函数定义的另外一种方式
  • 定义方法:就是将函数的定义、匿名函数赋值给一个变量
  • 函数定义赋值给一个变量,相当于将函数整体矮化成了表达式
  • 匿名函数:函数没有函数名
  • 调用函数表达式,方法是给变量名加 () 执行,不能使用函数名加 () 执行
// 函数的两种声明方式
// 1、利用函数关键字自定义函数(命名函数)
function fn() {

}
fn();
// 2、函数表达式(匿名函数)
// var 变量名 = function() {};
var fun = function(aru) {
console.log('我是函数表达式');
console.log(aru);
}
fun('我是默默!');
// (1)fun是变量名 不是函数名
// (2)函数表达式声明方式跟声明变量差不多,只不过变量里面存的是值 而 函数表达式里面存的是函数

函数数据类型

  • 函数是一种独特的数据类型 function -- 是 object 数据类型的一种,函数数据类型
  • 由于函数是一种数据类型,可以参与其他程序
  • 例如,可以把函数作为另外一个函数的参数,在另一个函数中调用
  • 或者,可以把函数作为返回值从函数内部返回
// 函数是一种数据类型,可以当成其他函数的参数
setInterval(function() {
console.log(1);
},1000)
//每隔 1s 输出一个 1

arguments 对象

  • JavaScript 中,arguments 对象是比较特别的一个对象,实际上是当前函数的一个内置属性。也就是说所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有实参。arguments 是一个伪数组,因此及可以进行遍历
  • 函数的实参个数和形参个数可以不一致,所有的实参都会存储在函数内部的 arguments 类数组对象中
/*
当我们不确定有多少个参数传递的时候 可以用arguments来获取 在JS中 arguments其实是当前函数的
一个内置对象 所有函数都内置了一个arguments对象 arguments对象中存储了传递的所有实参
*/
// arguments的使用
function fn() {
console.log(arguments); //里面存储了所有的实参
}
fn(1, 2, 3);

/*
arguments展示形式是一个伪数组,因此可以进行遍历,伪数组有如下特点:
具有length属性
按照索引方式存储数据
不具有数组的 push pop 等方法
*/

案例:利用 arguments 求一组数最大值

function getMax() {
var max = arguments[0];
var arry = arguments;
for (var i = 0; i < arry.length; i++) {
if (arry[i] > max) {
max = arry[i];
}
}
return max;
}

console.log(getMax(1, 2, 5, 11, 3));
console.log(getMax(1, 2, 5, 11, 3, 100, 111));
console.log(getMax(1, 2, 5, 11, 3, 1212, 22, 222, 2333));


函数递归

  • 函数内部可以通过函数名调用函数自身的方式,就是函数递归现象
  • 递归的次数太多容易出现错误:超出计算机的计算最大能力
  • 更多时候,使用递归去解决一些数学的现象
  • 例如可以输出斐波那契数列的某一项的值
// 函数,如果 传入的参数1,返回1,如果传入的是 1 以上的数字,让他返回参数 + 函数调用上一项
function fun (a) {
if (a === 1) {
return 1;
} else {
return a + fun(a - 1);
}
}
// 调用函数
console.log(fun(1));
console.log(fun(2));
console.log(fun(3));
console.log(fun(100));
// 这样我们就用递归做出了 n 以内数累加求和的函数

案例:输出斐波那契数列任意项

// 斐波那契数列(每一项等于前两项之和 1,1,2,3,5,8,13,21,34,55 ···)
// 参数:正整数
// 返回值:对应的整数位置的斐波那契数列的值
function fibo(a) {
if (a === 1 || a === 2) {
return 1;
} else {
return fibo(a - 1) + fibo(a - 2);
}
}
console.log(fibo(1));
console.log(fibo(2));
console.log(fibo(3));
console.log(fibo(4));


作用域

  • 作用域:变量可以起作用的范围
  • 如果变量定义在一个函数内部,只能在函数内部被访问到,在函数外部不能使用这个变量,函数就是变量定义的作用域
  • 任何一对花括号 {} 中的结构体都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域
  • 在 es6 之前没有块级作用域的概念,只有函数作用域,现阶段可以认为 JavaScript 没有块级作用域
// js现阶段没有块级作用域 js作用域:局部作用域 全局作用域 现阶段我们js没有块级作用域
// js在ES6的时候新增块级作用域的概念
// 块级作用域就是{}中的区域

if (3 > 2) {
var num1 = 10;
}
console.log(num1);//10

/*
说明js没有块级作用域,外部可以调用{}内声明的变量
*/

全局变量和局部变量

  • 局部:变量:定义在函数内部的变量,只能在函数作用域被访问到,在外面没有定义的
  • 全局变量:从广义上来说,也是一种局部变量,定义在全局的变量,作用域范围是全局,
  • 在整个 js 程序任意位置都能被访问到
  • 局部变量退出作用域之后会被销毁,全局变量关闭页面或浏览器才会销毁

函数参数也是局部变量

  • 函数的参数本质是一个变量,也有自己的作用域,函数的参数也是属于函数自己内部的局部变量,只能在函数内部被使用,在函数外面没有定义

函数的作用域

  • 函数也有自己的作用域,定义在哪个作用域内部,只能在这个作用域范围内被访问,出了作用域不能被访问
  • 函数定义在另一个函数内部,如果外部函数没有执行时,相当于内部代码没写

作用域链

  • 只有函数可以制造作用域结构,那么只要是代码,就至少有一个作用域,即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用领域
  • 将这样的所有的作用域列出来,可以有一个结构:函数内指向函数外的链式结构。就称作作用域链

遮蔽小于效应

  • 程序在遇到一个变量时,使用时作用域查找顺序,不同层次的函数内都有可能定义相同名字的变量,一个变量在使用时,会优先从自己所在层作用域查找变量,如果当前层没有变量定义会按照顺序从本层往外依次查找,直到第一个变量定义。整个过程中会发生内层变量的效果,叫做“遮蔽效应”
/* 
1、只要是代码就至少有一个作用域
2、写在函数内部的局部作用域
3、如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
4、根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,
就被称作作用域链
*/

// 作用域链 : 内部函数访问外部函数的变量 采取的是链式查找的方式来决定取哪个值 这种结构我们称作用域链
// 就近原则
var num = 10;

function fn() {//外部函数
var num = 20;

function fun() {//内部函数
console.log(num);//20
}

fun();

}
fn();

不写 var 关键字的影响

  • 在函数内部想要定义新的变量,如果不使用关键字 var ,相当于定义的全局变量。如果全局变量也有相同的标识符,会被函数内部的变量影响,局部变量污染全局变量
  • 注意:每次定义变量时都必须写 var 关键字,否则就会定义在全局,可能污染全局
function fn() {
a = 2;
}
console.log(a);//2


预解析

  • JavaScript 代码的执行是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器执行 JavaScript 代码的时候,分为两个过程:预解析过程和代码执行过程
  • 预解析过程:
  1. 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
  2. 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
  3. 先提升 var ,再提升 function
  • Javascript 的执行过程:在预解析之后,根据新的代码顺序,从上往下按照既定规律执行 js 代码

变量声明提升

  • 在与解析过程中,所有定义的变量,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的声明变量过程
  • 提升过程中,只提升声明过程,不提升变量赋值,相当于变量定义未赋值,变量内存储 undefined 值
  • 因此,在 js 中会出现一种现象,在前面调用后定义的变量,不会报错,只会使用 undefined值

函数声明提升

  • 在与解析过程中,所有定义的函数,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的函数声明过程
  • 在预解析之后的代码执行过程中,函数定义过程已经在最开始就会执行,一旦函数定义成功,后续就可以直接调用函数
  • 因此,在 js 中会出现一种特殊现象,在前面调用后定义的函数,不会报错,而且能正常执行函数内部的代码(如果使用 var 声明的函数,在定义函数之前调用函数,会直接报错
/*
1、
console.log(num);报错
*/

// 2、
console.log(num);//undefined
var num = 10;

// 3、
fn();//11

function fn() {
console.log(11);
}
// 4、
/*
fun();//报错
var fun = function() {
console.log(22);
}
*/

/*
1、我们js引擎运行js 分为两步: 预解析 代码执行
(1) 预解析 js引擎会把js 里面所有的 var 和 function 提升到当前作用域的最前面
(2) 代码执行 按照代码书写的顺序从上往下执行

2、预解析分为 变量预解析(变量提升) 函数与解析(函数提升)
(1) 变量提升 就是把所有的变量声明提升到当前的作用域最前面 不提升赋值操作
(2) 函数提升 把所有的函数声明提升到当前作用域的最前边 不调用函数
*/

提升顺序

  • 预解析过程中,先提升 var 变量声明,在提升 function 函数声明
  • 假设出现变量名和函数名相同,那么后提升的函数名标识符会覆盖先提升的变量名,那么在后续代码种出现调用标识符时,内部是函数的定义过程,而不是 undefined
  • 如果调用标识符的过程在源代码函数和变量定义的后面,相当于函数名覆盖了一次变量名,结果在执行到变量赋值时,又被新值覆盖了函数的值,那么在后面再次调用标识符,用的就是变量存的新值
  • 建议:不要书写相同的标识符给变量名或函数名,避免出现覆盖

函数声明提升的应用

  • 函数声明提升可以用于调整代码的顺序,将大段的定义过程放到代码最后,但是不影响代码执行效果

IIFE 自调用函数

  • IIFE:immediately-invoked function expression,叫做即时调用的函数表达式,也叫做自调用函数表达式,表示函数在自定义时就立即调用
  • 函数调用方式:函数名或函数表达式的变量名后面加 () 运算符
  • 函数名定义的形式不能实现立即执行自调用,函数使用函数表达式形式可以实现立即执行,原因是因为函数表达式定义过程中,将函数矮化成表达式,后面加 () 运算符就可以立即执行
  • 启发:如果想实现 IIFE ,可以想办法将函数矮化成表达式
// 关键字定义的方式,不能立即执行
// function fun() {
// console.log(1);
// }();

// 函数表达式,可以立即调用
var foo = function () {
console.log(2);
}();
  • 函数矮化成表达式,就可以实现自调用
  • 函数矮化成表达式的方法,可以让函数参与一些运算,也就是说给函数前面加一些运算符。

数学运算符:+ - ()

逻辑运算符:!非运算

  • IIFE 结构可以封住函数的作用域,在结构外面是不能调用函数的
  • IIFE 最常用的时 () 运算符,而且函数可以不写函数名,使用匿名函数
// 通过前面添加操作符可以将我们的函数矮化成表达式
+ function fun() {
console.log(1);
}();
- function fun() {
console.log(1);
}();
(function fun() {
console.log(1);
})();
!function fun() {
console.log(1);
}();



收起阅读 »

JavaScript 数组

为什么学习数组?之前学习的数据类型,只能存储一个值(比如:Number/String)。如果我们想存储班级中所有学生的成绩,此时该如何存储?数组的概念所谓数组(Array),就是将多个元素(通常是同一类型)按一定顺序排列放到一个集合中,那么这个集合我们就称之为...
继续阅读 »
为什么学习数组?
  • 之前学习的数据类型,只能存储一个值(比如:Number/String)。如果我们想存储班级中所有学生的成绩,此时该如何存储?

数组的概念

  • 所谓数组(Array),就是将多个元素(通常是同一类型)按一定顺序排列放到一个集合中,那么这个集合我们就称之为数组

数组的定义

  • 数组是一组有序的数组集合。数组内部可以存放多个数据,不限制数据类型,并且数组的长度可以动态的调整。
  • 创建数组最简单的方式就是数组字面量方式
  • 数组的字面量:[]
  • 一般将数组字面量赋值给一个变量,方便后期对数组进行操作
  • 如果存放多个数据,每个数据之间用逗号分隔,最后一个后面不需要加逗号
var arr = [];//创建一个空的数组
var arr1 = [1, 2, '数组', true, undefined, true];

获取数组元素

  • 数组可以通过一个 index (索引值、下标)去获取对应的某一项数据,进行下一步操作
  • index:从 0 开始,按照整数排序往后顺序排序,例如 0,1,2,3······
  • 可以通过 index 获取某项值之后,使用或者更改数组项的值
  • 调用数据:利用数组变量名后面直接加 [index] 方式
var arr = ['red', 'orange', 'blue',];//索引号按顺序0 1 2...
console.log(arr[0]); //red
console.log(arr[1]); //orange
console.log(arr[2]); //blue
console.log(arr[3]); //undefined
// 从代码中我们可以发现,从数组中取出每一个元素时,代码是重复的,不一样的是代码的索引值在增加
// 因此我们有更简便的方法一次调用数组中的多个元素
  • 注意:如果索引值超过了数组最大项,相当于这一项没有赋值,内部存储的就是 undefined
  • 更改数据:arr[index] 调用这一项数据,后面等号赋值更改数据
var arr = [1, 2, '数组', true];
console.log(arr[5]);//undefined
arr[2] = 'haha';
console.log(arr[2]);//'haha'


数组的长度

  • 数组有一个 length 的属性,记录的是数组的数据的总长度
  • 使用方法:变量.length
console.log(arr.length);
  • 数组的长度与数组最后一项的下标存在关系,最后一项的下标等于数组的 length-1
  • 获取最后一项数据时,可以这样书写:
console.log(arr[arr.length-1]);
  • 数组的长度不是固定不变的,可以发生更改

更改数组长度:

  • 增加数组长度:直接给数组 length 属性赋一个大于原来长度的值。赋值方式使用等号赋值
  • 或者,可以给一个大于最大下标的项直接赋值,可以强制拉长数组
  • 缩短数组长度:强制给 length 属性赋值,后面数组被会直接删除,删除时不可逆的

更改数组长度:

var arr = [1, 3, 5, 7];
arr.length = 10;
console.log(arr);

拉长数组长度:

var arr = [1, 3, 5, 7];
arr[14] = 6;
console.log(arr);
console.log(arr.length);//15

缩短数组长度:

var arr = [1, 3, 5, 7];
arr.length = 3
console.log(arr);//[1,3,4]
console.log(arr.length);//3


数组的遍历

  • 遍历:遍及所有,对数组的每一个元素都访问一次就叫遍历。利用 for 循环,将数组中的每一项单独拿出来,进行一些操作
  • 根据下标在 0 到 arr.length-1 之间,进行 for 循环遍历
//遍历数组就是把数组的元素从头到尾访问一遍
var arry = ['red','blue','green']
for(var i = 0; i < 3; i++){
console.log(arry[i]);
}
//1.因为索引号从0开始,所以计数器i必须从0开始
//2.输出时计数器i当索引号使用

// 通用写法
var arry = ['red','blue','green']
for(var i = 0; i < arry.length; i++){ //也可以写成: i <= arry.length - 1
console.log(arry[i]);
}


数组应用案例

  • 求一组数中的所有数的和以及平均值
var arry = [2, 6, 7, 9, 11];
var sum = 0;
var average = 0;
for (var i = 0; i < arry.length; i++) {
sum += arry[i];
}
average = sum / arry.length;
console.log(sum,average);//同时输出多个变量用逗号隔开

原文:https://zhuanlan.zhihu.com/p/365784347

收起阅读 »

JavaScript 常见的三种数组排序方式

一、冒泡排序冒泡排序 的英文名是 Bubble Sort ,它是一种比较简单直观的排序算法简单来说它会重复走访过要排序的数列,一次比较两个数,如果他们的顺序错误就会将他们交换过来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成这个算法...
继续阅读 »

一、冒泡排序

冒泡排序 的英文名是 Bubble Sort ,它是一种比较简单直观的排序算法

简单来说它会重复走访过要排序的数列,一次比较两个数,如果他们的顺序错误就会将他们交换过来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成

这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端


算法思路(以按递增顺序排列为例):

1、我们需要做一个内层循环来比较每对相邻元素的大小,如果前面大于后面,就让他们交换位置,我们要让小的数在前面,大的数在后面

2、当内层循环结束时,在数组最后一位的元素,就一定是这个数组中最大的元素了,这时候除了最后一个元素不用再动以外(所以内层循环每循环一次就可以少循环一次)我们还要再来确定这个数组中第二大的元素,第三大的元素,以此类推,因此我们还需要一层外层循环。如果这个数组有 n 个元素我们就要确定 n - 1 个元素的位置,所以外层循环需要循环的次数就是 n - 1 次

3、只需要内外两层循环嵌套,就可以把数组排序好啦,虽然实现方式可能有很多种,这只是我个人的想法,代码如下,排序功能已封装成函数,请放心食用:

var myArr = [89,34,76,15,98,25,67];

function bubbleSort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
for (var j = 0; j < arr.length - i; j++) {
if(arr[j] > arr[j + 1]) {
// 交换两个数的位置
var temp = 0;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}

return arr;
}

console.log(bubbleSort(myArr));

二、选择排序

选择排序 英文叫法是 Selection sort,这也是一种简单直观的排序方法

这种排序首先会在未排序的数组中找到最小或者最大的元素,存放在排序数组的起始位置

然后再从未排序的数列中去找到这个数组中第二大或这第二小的数放在已排序的数之后,以此类推,不断重复直到所有元素排列完毕


算法思路(以按递增顺序排列为例):

1、我们需要内层循环找出未排序数列中的最小值(找最小值可以用之前谁比最小值小谁就替换最小值的思路),循环后找到未排序数列中的最小元素时记录最小的那个元素在数组中的索引值,用索引获得最小值的位置后把它放在数组的第一位,此处注意,如果直接放在第一位会替换第一位数组中原来的元素,我们需要交换最小值的位置,和第一个元素的位置(利用两个变量交换数值的方法)

2、每经过一次内层循环,我们就能确定一个未排序数组中最小值的位置,在确定倒数第二个数的位置时,最后一个数的位置也自然而然地被确定了,因此数组中有 n 个元素我们就需要进行 n - 1 次内层循环,我们就用用外层循环来保持内层循环的重复进行

var myArr = [89,34,76,15,98,25,67];

function selectSort(arr) {
for(var i = 0; i < arr.length - 1; i++) {
//i < arr.length - 1 因为排完倒数第二个,倒数第一个数自然在它正确的位置了
var index = i;
for(var j = i + 1; j < arr.length; j++) {
// 寻找最小值
if(arr[index] > arr[j]){
// 保存最小值索引
index = j;
}
}

// 将未排序中的最小数字,放到未排序中的最左侧
if(index != i) {
var temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}
}
return arr;
}

console.log(selectSort(myArr));


三、插入排序

插入排序 英文称为 Insertion sort ,插入排序也被叫做直接插入排序

它的基本思想是将一个未排序的元素插入到已经排序好的数组中,从而使得已排序的数组增加一个元素,通过插入不断完善已排序数组的过程,就是排序整个数组的过程。


算法思路(以按递增顺序排列为例):

1、因为数组中第一个元素前面没有元素可以进行比较,所以我们从第二个元素开始比较,用 current 变量来进行存储当前要和别人比较的元素,用 preIndex变量 来方便我们去找当前准备插入元素之前的元素

2、内层循环就是按顺序比较插入元素和之前元素的大小,来确定插入元素的位置, preIndex 每比较一次就自减1 ,让准备插入元素和它之前的所有已排序元素都比较一遍,每当待插入元素比前一个数小了,前面的元素就往右挪一个位置,直到前一个数小于待插入数,跳出判断,待插入元素放在前一次判断挪动元素留出的空位上,由于我们提前用 current 保存了要插入的元素,所以要插入的元素不会因为前面的元素覆盖而丢失。

3、每循环一次内层循环,我们就可以确定一个元素的插入位置,但由于我们内层循环是从第二个元素开始的(也就是索引为 1 的元素),因此如果有 n 个元素,我们就需要 n - 1 次内层循环,内存循环我们用外层循环来实现,外层循环就这么被定义完成了

var myArr = [89,34,76,15,98,25,67];

function insertionSort(arr) {
for (var i = 1; i <= arr.length - 1; i++) {
var preIndex = i - 1;
current = arr[i];
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}

console.log(insertionSort(myArr));


总结:

选择排序(一种不稳定的排序方法)

优点:移动数据的次数已知(n-1次);

缺点:比较次数多。


冒泡排序

优点:稳定;

缺点:慢,每次只能移动相邻两个数据。


插入排序

优点:稳定,快;

缺点:比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是当数据总量庞大的时候,但用链表可以解决这个问题。

原文:https://zhuanlan.zhihu.com/p/368208410

收起阅读 »

js 取小数点后几位方法

一 取后两位 为例: 四舍五入 1.toFixed() Number的toFixed()方法可把 Number 四舍五入为指定小数位数的数字。 const test = 1.12 / 3 // 0.37333333333333335 console.lo...
继续阅读 »

一 取后两位 为例:


四舍五入


1.toFixed()

Number的toFixed()方法可把 Number 四舍五入为指定小数位数的数字。



const test = 1.12 / 3 // 0.37333333333333335
console.log(test.toFixed(2)) // 0.37
复制代码

注意:
.兼容问题



/**
* firefox/chrome ie某些版本中,对于小数最后一位为 5 时进位不正确(不进位)。
* 修复方式即判断最后一位为 5 的,改成 6, 再调用 toFixed
*/
function(number, precision) {
const str = number + '';
const len = str.length;
let last = str[len - 1] // 或者 str.substr(len - 1, len);
if(last == '5') {
let = '6';
str = str.substr(0, len - 1) + last;
return (str - 0).toFixed(precision)
} else {
return number.toFixed(precision)
}
}

或者为:
function toFixed(number, precision) {
const tempCount = Math.pow(10, precision);
let target = number * tempCount + 0.5;
target = parseInt(des, 10) / tempCount;
return target + '';
}

复制代码

.精确问题



/**
* toFixed 有时候会碰到如下精度缺失问题
* 可以使用下面例子的方法解决
* 或者 (test * 100).toFixed(2) + '%';
*/
const test = 1.12 / 3 // 0.37333333333333335
console.log(test.toFixed(4)) // 0.3733
console.log((test).toFixed(4) * 100 + '%') // 37.330000000000005%

复制代码


  1. Math.round()



/**
* 利用Math.round
* 保留两位小数
*/
function toDecimal(num) {
let tar = parseFloat(num);
if (isNaN(tar)) { return };
tar = Math.round(num * 100) / 100;
}


/**
* 利用Math.round 强制保留两位小数 10 则为 10.00
* 保留两位小数
*/

function toDecimal(num) {
let tar = parseFloat(num);
if (isNaN(tar)) {return};
tar = Math.round(num * 100) / 100;

let tarStr = tar.toString();
let decIndexOf = tarStr.indexOf('.');
if(decIndexOf < 0) {
tarStr += '.';
decInexOf = tarStr.length;
}
while (tarStr.length <= decIndexOf + 2) {
tarStr += '0';
}
return tarStr;
}

复制代码

不四舍五入


1.先把小数取整 在计算



const test = 1.12 / 3 // 0.37333333333333335
Math.floor(test * 100) / 100 // floor 是向下取整 0.37
复制代码

2.使用正则表达式



const test = 1.12 / 3 // 0.37333333333333335
let target = test + '' // test.toString()
target = target.match(/^\d+(?:\.\d{0, 2}?/)
//输出结果为 0.37。但整数如 10 必须写为10.0000
// 如果是负数,先转换为正数再计算,最后转回负数


作者:maomaoweiw
链接:https://juejin.cn/post/6844903638020816903

收起阅读 »

JavaScript的小技巧

类型转换数组转字符串var arr = [1,2,3,4,5]; var str = arr+''; //1,2,3,4,5 字符串转数字var str = '777'; var num = str * 1; //777 var str = '777'; v...
继续阅读 »

类型转换

数组转字符串

var arr = [1,2,3,4,5];
var str = arr+''; //1,2,3,4,5

字符串转数字

var str = '777';
var num = str * 1; //777

var str = '777';
var num = str - 0; //777

字符串转数字

var str = '666';
var num = str * 1; // 666

向下取整

var num = ~~4.2144235; //  4

var num = 293.9457352 >> 0; // 293

boolean 转换

var bool = !!null; //  false
var bool = !!'null'; // true

var bool = !!undefined; // false
var bool = !!'undefined'; // true

var bool = !!0; // false
var bool = !!'0'; // true

var bool=!!''; // true
var bool=!![]; // true
var bool=!!{}; // true

var bool=!!new Boolean('false'); // true
var bool=!!new Boolean('true'); // true

判断对象下面是否有此属性

直接判断

var obj = {a:789};
if(obj.a){ //obj.b ==>789
console.log('运行了') //可以运行
}

if(obj.b){ //obj.b ==>undefined
console.log('运行了') //没有运行
}

var obj2 = {a:false};
if(obj2.a){ //obj.b ==>false
console.log('运行了') //没有运行
}
// 不严谨,如果值为0,undefined,false,null... 也会判断为false

in 操作符

var obj = {a:789};
if('a' in obj){ // 'a' in obj ==>true
console.log('运行了') //可以运行
}

if('b' in obj){// 'a' in obj ==>false
console.log('运行了') //没有运行
}

利用hasOwnProperty

var obj = {a:789};
if(obj.hasOwnProperty('a')){ //==>true
console.log('运行了') //可以运行
}

if(obj.hasOwnProperty('b')){ //==>false
console.log('运行了') //没有运行
}

还有好多好多,得慢慢写

原文:https://zhuanlan.zhihu.com/p/368353172

收起阅读 »

线程池基本参数解析

一、线程池构造方法参数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, l...
继续阅读 »

一、线程池构造方法参数


public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码

corePoolSize: 核心线程池数量


maximumPoolSize:最大线程池数量(包含核心线程池数量)


keepAliveTime: 线程执行完后的存活时间和 TimeUnit 联合使用


TimeUnit:线程执行完后的存活时间和 keepAliveTime 联合使用


BlockingQueue:任务队列,当新的任务到来,核心线程数已满,会加入任务队列


ThreadFactory: 线程工厂,生产线程


RejectedExecutionHandler:新的任务到来,如果已超过最大线程数 且任务队列已满,则会对该任务进行拒绝策略




二、keepAliveTime


作用在非核心线程,如果也需要作用在核心线程上,那需要调用


public void allowCoreThreadTimeOut(boolean value)


三、阻塞队列


1.ArrayBlockingQueue

存储方式:数组 final Object[] items;


构造方法两个参数:int capcity 数组大小,boolean fair是否是公平锁


ReentrantLock:公平锁和非公平锁 主要区别在获取锁的机制上
公平锁:获取时会检查队列中其他任务是否要获取锁,如果其他任务要获取锁,先让其他任务获取
非公平锁:获取时不管队列中是否有任务要获取锁,直接尝试获取
复制代码

2.LinkedBlockingDeque

存储方式:双向链表


构造方法参数:无参默认 int 最大容量,也可以传入容量值


3.PriorityBlockingQueue

存储方式:数组 private transient Object[] queue


构造函数:参数1初始容量 默认11,参数2 :比较器


4.SychronizeQueue

没有存储容量,必须找到执行线程,找不到就执行拒绝策略


5.DelayedWorkQueue

存储方式:数组 ,默认大小 16


private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
复制代码

四、线程工厂


1.DefaultThreadFactory: 生成线程 组,线程编号,线程名字 线程优先级(默认是 5)

2.PrivilegedThreadFactory 继承 DefaultThreadFactory

五、拒绝策略


1.CallerRunsPolicy

public static class CallerRunsPolicy implements RejectedExecutionHandler {

public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
复制代码

直接在调用者线程进行执行,前提是 线程池未关闭


2.AbortPolicy

public AbortPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
复制代码

抛出异常


3.DiscardPolicy

public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
复制代码

直接什么也不做 丢弃任务


4.DiscardOldestPolicy

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
复制代码

从任务队列中删除最旧的,然后重新执行该任务,这里是个隐式循环,因为excute 可能会重新触发拒绝策略




六、ThreadPoolExecutor


1.FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
复制代码

核心线程数和最大线程数相等机只有核心线程;任务队列大小无限制;DefaultThreadFactory(也可以传入定制);拒绝策略是AbortPolicy


2.CacheThreadPool

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
复制代码

核心线程数是0 ,最大线程数是 MAX_VALUE,任务队列无容量,每来一个任务都会新开线程执行任务,执行完后存活一分钟 即可释放线程;DefaultThreadFactory(也可以传入定制);拒绝策略是AbortPolicy


3.SingleThreadExecutor

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
复制代码

核心线程和最大线程数量都是1,任务队列大小无限制,DefaultThreadFactory(也可以传入定制);拒绝策略是AbortPolicy


4.WorkStealingPool(java1.8)

Java8新增的创建线程池的方法,如果不主动设置它的并发数,那么这个方法就会以当前机器的CPU处理器个数为线程个数,这个线程池会并行处理任务,不能够保证任务执行的顺序


七、ThreadScheduledExecutor


1.SingleThreadScheduledExecutor

核心线程数是1,最大线程数无限制,非核心线程最大存活时间 是0 秒,执行完立即结束


2.ScheduledThreadPoolExecutor

核心线程数可传入,最大线程数无限制,非核心线程最大存活时间 是0 秒,执行完立即结束


八、基本执行与选择


cpu密集型任务,设置为CPU核心数+1; IO密集型任务,设置为CPU核心数*2;


CPU密集型任务指的是需要cpu进行大量计算的任务,提高CPU的利用率。核心线程数不宜设置过大,太多的线程会互相抢占cpu资源导致不断切换线程,反而浪费了cpu。最理想的情况是每个CPU都在进行计算,没有浪费,但很有可能其中的一个线程会突然挂起等待IO,此时额外的一个等待线程就可以马上进行工作,而不必等待挂起结束。


IO密集型任务指的是任务需要频繁进行IO操作,这些操作会导致线程长时间处于挂起状态,那么需要更多的线程来进行工作,不会让cpu都处于挂起状态,浪费资源。一般设置为cpu核心数的两倍即可


1.在线程数没有达到核心线程数时,每个新任务都会创建一个新的线程来执行任务。

2.当线程数达到核心线程数时,每个新任务会被放入到等待队列中等待被执行。

3.当等待队列已经满了之后,如果线程数没有到达总的线程数上限,那么会创建一个非核心线程来执行任务。

4.当线程数已经到达总的线程数限制时,新的任务会被拒绝策略者处理

九、三方使用的选择


1.Okhttp

核心线程数是 0 ,最大线程数是 Integer.MAX_VALUE,线程执行完后允许存活最大时间 60S,队列采用的是 SynchronousQueue,及无容量的队列,这里采用无容量的队列是因为 Dispatcher 自己有实现队列


//Dispatcher.java
//最大同时异步请求个数
private int maxRequests = 64;
//单个host的同时最大请求数
private int maxRequestsPerHost = 5;

//准备执行的异步队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

//正在执行的异步队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

//正在执行的同步队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
//线程池
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}

//加入准备队列 并判断能不能执行
void enqueue(AsyncCall call) {
synchronized (this) {
//加入准备队列
readyAsyncCalls.add(call);
}
//执行
promoteAndExecute();
}

private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));

List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
//将可以执行的异步请求集合筛选出来 如果已经超过同时最大请求个数则直接跳出循环,否则如果超过最大host同时请求个数 继续下次循环
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
//如果正在之子那个的
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.

i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
//执行筛选出来的可执行任务
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}

return isRunning;
}

复制代码

2.EventBus

核心线程数是0 ,最大线程数是 MAX_VALUE,任务队列无容量,每来一个任务都会新开线程执行任务,执行完后存活一分钟 即可释放线程;拒绝策略是AbortPolicy


public class EventBusBuilder {
private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
}

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

线程池系列分析-线程池的拒绝策略

前言 线程池系列的第二篇文章。拒绝策略的说明。技术人嘛。还是要经常归纳总结的 什么是拒绝策略 首先要明白,为什么线程池要有一个拒绝策略。也就是他出现的背景是什么。 了解过线程池的小伙伴应该都知道。线程池的构造参数中就有一个拒绝策略 public ThreadP...
继续阅读 »

前言


线程池系列的第二篇文章。拒绝策略的说明。技术人嘛。还是要经常归纳总结的


什么是拒绝策略


首先要明白,为什么线程池要有一个拒绝策略。也就是他出现的背景是什么。
了解过线程池的小伙伴应该都知道。线程池的构造参数中就有一个拒绝策略


public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
//拒绝策略的接口
RejectedExecutionHandler handler) {
复制代码

拒绝。意味着不满足某些条件,线程池也是一样。当线程数超过了maximumPoolSize的时候。就会拒绝添加任务。起到了保护线程池的作用


有哪些拒绝策略


RejectedExecutionHandler本身也是一个接口。如下


public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
复制代码

线程池本身提供了4中不同的拒绝策略




图片来源Java线程池实现原理及其在美团业务中的实践


AbortPolicy



可以看到。非常简单粗暴,直接抛出一个异常。


DiscardPolicy



嘿,啥也不管。啥也不问。任务拒绝就拒接了。也不给个提示啥的。就相当于直接把任务丢弃


DiscardOldestPolicy



如果线程池还在运行。那么就将阻塞队列中最前面的任务给取消,在执行当前任务
这么说有点玄乎。笔者写了一个简单的测试代码。能够更加描述清楚。笔者仿造DiscardOldestPolicy写了一个一摸一样的拒绝策略。然后加上打印。观察



完整的测试代码点击查看

public class Main {

public static class CustomDiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public CustomDiscardOldestPolicy() {
}

/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
System.out.println("被拒绝的任务 " + r.toString());
Runnable runnable = e.getQueue().poll();
if (runnable != null)
System.out.println("队列中拿到的任务 " + runnable.toString());
else
System.out.println("队列中拿到的任务 null");
e.execute(r);
}
}
}

public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1,
2,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2),
new CustomDiscardOldestPolicy()
);

for (int i = 0; i < 5; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("任务执行---开始" + Thread.currentThread().getName() + " 任务 " + this.toString());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println("添加任务 " + runnable.toString());
pool.execute(runnable);
}
}
}

复制代码


在上面的测试代码中。笔者将最大线程数控制在2个。核心线程数控制在1个。并且选择了一个有长度的队列ArrayBlockingQueue。设置其长度为2。



  • 当任务1添加进入以后。因为核心线程数是1.所以直接创建新的线程执行任务。

  • 当任务2添加进入以后。因为超过了核心线程数1.所以被添加到队列当中。此时的队列中有一个任务2

  • 当任务3添加进入以后。同理。被添加到队列当中。此时队列当中有两个任务了。

  • 当任务4添加进入以后。这个时候队列因为已经满了。所以判断是否超过了最大线程数。超过了就直接拒绝策略。


CallerRunsPolicy



默认的拒绝策略


在线程池的构造方法中可以看到 有一个defaultHandler拒绝策略


public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
复制代码

defaultHandler给我们创建了一个AbortPolicy。这也是线程池默认的策略。就是直接抛出异常


private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();

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

【Java】ArrayList实现原理浅析

为什么要写这篇文章,还真不是我好奇,就是因为团队技术分享了,我也要搞一个分享的内容,我满脸写着期待(绝望)和开心(难过)。 一.ArrayList的底层数据结构 ArrayList底层的数据结构是数组,它是一个Object元素类型的数组,所有操作操作底层都是基...
继续阅读 »

为什么要写这篇文章,还真不是我好奇,就是因为团队技术分享了,我也要搞一个分享的内容,我满脸写着期待(绝望)和开心(难过)。


一.ArrayList的底层数据结构


ArrayList底层的数据结构是数组,它是一个Object元素类型的数组,所有操作操作底层都是基于数组的。(我甚至一度在想,我需不需要解释数组是个什么东西,春困使我放弃这个操作)


二.ArrayList的扩容机制


这个算是比较有讲头的一个东西了,我整个的技术分享就是用这个来保饭碗的。


2.1三种构造函数分析


要讲扩容机制,就要先说ArrayList的三种构造函数:


transient Object[] elementData;
复制代码

注:elementData是ArrayList的底层数据结构,是一个对象数组,存放实际元素,用transient标记,代表序列化时不会被序列化;


2.1.1 空参构造函数


给elementData设置一个空对象数组


private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
复制代码

2.1.2 指定数组容量大小参数的构造函数


指定容量>0,直接new一个指定大小的对象数组;
=0,指定一个空对象数组
<0,抛异常


private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
复制代码

2.1.3 集合参数的构造函数


a.将集合转化成数组
b.判断数组的长度,length!=0;true,判断数组类型是否为Object类型数组?->否,拷贝elementData的数据,拷贝为Object数组,赋值给elementData
false:设置elementData为空对象数组


private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
复制代码

2.1.4 什么是数组的深拷贝和浅拷贝?



深拷贝:不单单是引用拷贝,还开辟一块新的内存空间



浅拷贝: 引用拷贝


Q:这里的Arrays.copyOf深拷贝还是浅拷贝
A:浅拷贝,只复制了对象的引用,即内存地址,并没有为每个元素新创建对象。
原因就不过多解释了,具体去查看这篇博客吧。blog.csdn.net/abysscarry/…


2.2.扩容机制发生的时间


add()的时候调用


1.add(E e) ;添加具体某个元素
代码操作释义:检测是否需要扩容操作;将集合中实际元素个数+1;


public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
复制代码

2.add(int index, E element) ;根据下标去添加某个元素


public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

ensureCapacityInternal(size + 1); // Increments modCount!!
//原数组;源数组要复制的起始位置;目标数组;目标数组复制的起始位置;要copy的数组的长度
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
复制代码

这里的System.arraycopy()是深拷贝还是浅拷贝?



答:只有数组为一维数组,并且元素为基本类型或String类型时,才是深复制,其它都属于浅复制;
System.arraycopy()常用作数组的扩容,如ArrayList底层数组的扩容



2.3插入数据时,ArrayList和LinkedList的区别


2.3.1 ArrayList如何插入数据?


曾几何时,你麻木的记住,ArrayList.add(index,e)效率<LinkedList的add,时至今日,打工人顿悟了。来,让我们一层一层的剥开他的衣服...呸,代码!
System.arraycopy(elementData, index, elementData, index + 1,size - index);你看看这句代码都干了些什么事儿。



为了在指定的下标插入一个数据,我们要把目标index的位置到size-index的数据全都进行复制,紧紧是为了给某个add的数据让位。



倘若数组长度是100,然后add(0,element),也就意味,为了给他让位,我们需要拷贝移动1-99的数据位置。


2.3.2 LinkedList是如何插入数据?


public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
复制代码

分析:linkBefore(element,node(index)),传入的参数是:1.目标元素;2.目标元素的当前节点对象。


LinkedList插入数据过程:



  1. 获取当前的目标节点的pre=pred;

  2. new一个新的结节点newNode,3个参数代表着:a.当前操作节点的pre=pred;b.元素e=e;c.下一个节点next=succ;

  3. 把new的新节点,设置给当前操作节点的pre。(将操作节点的pre设置为新插入的元素节点)

  4. 判断当前插入的元素位置是否为LinkedList的头节点;若不是的话,则将当前操作元素节点的next指为new的要插入进来的那个节点。(将操作节点的next指向新插入的元素的节点)


在某个位置插入某个元素的大概流程如图所示:
在这里插入图片描述
也就是说,我们通过LinkedList向某个位置插入一个数据,我们只需要改变两个数据节点的pre和next指向就完成了。


ArrayList和LinkedList的插入效率比较


ArrayList的add是尾部效率ArrayList>LinkedList
ArrayList的add是头部效率ArrayList<LinkedList



原因:ArrayList的内存空间连续,且不需要复制数组。LinkedList需要创建一个新的节点,前后引用进行重新排列。



我不知道你们有没有恍然大悟,但是我恍然大悟了。


接下来就是揭开扩容的真面目了,扩容和构造器有什么关系?


2.4 触发扩容以及扩容大小


上面说扩容机制是在add(e)时触发的,来看看add(e)的源码:


add(E e)


public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
复制代码

ensureCapacityInternal(int minCapacity) :确保插入元素容器最小的值;
minCapacity:当前数组的长度+1


构造函数为空参数构造函数,则给minCapacity设置默认的值为minCapacity=DEFAULT_CAPACITY=10;
若不为空参数构造函数,则minCapacity=数组已存在数据size+1


private static final int DEFAULT_CAPACITY = 10;
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
复制代码

ensureExplicitCapacity(int minCapacity) :判断是否需要扩容
elementData.length是现有数据的长度。


private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
复制代码

grow(int minCapacity) :实现扩容



  1. 获取旧容量

  2. 现将原元素数组的长度增大1.5倍,随后和newCapacity比较。

  3. 新容量小于参数指定容量,修改新容量:newCapacity(新增容量)<minnewCapacity:newCapacity=minCapacity

  4. 新容量大于最大容量:newCapacity>minnewCapacity:newCapacity:将就数组拷贝到扩容后的新数组中。


private void grow(int minCapacity) {
int oldCapacity = elementData.length; //旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //新容量为旧容量的1.5倍
if (newCapacity - minCapacity < 0) //新容量小于参数指定容量,修改新容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) //新容量大于最大容量
newCapacity = hugeCapacity(minCapacity); //指定新容量
//拷贝扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
复制代码

第一次调用添加元素的add和addAll,size=0,则minCapacity=1
如果ArrayList给了特定的初始值,则需要根据数组实际长度和数组容量差来判断是否调用扩容;如果没有指定初始容量,第一次调用add则一定需要调用grow()


三.ArrayList的线程安全性


在多线程下,ArrayList不能保证原子性(即同一时刻只能有一个线程来对它进行操作)。


举个栗子:线程A对ArrayList进行++处理,期待100;线程B对ArrayList进行--处理,期待98。多线程进行时,可能本应该为100,因为又有--操作,可能++后,结果仍然为99.


多线程环境下,ArrayList线程是不安全的。


保证线程安全性的方法



1.使用synchronized关键字;



2.用Collection类中的静态方法synchronizedList(),对Arraylist进行调用


四.ArrayList常用方法介绍


arrayList.get(position):根据数组下标进行取值,set同理


arrayList.add(postion):判断是否扩容,根据数组下标进行赋值


arrayList.remove(index)步骤
1.在目标元素的位置设置赋值的起始位置;
2.将目标位置开始到数组最后一位的数组进行复制;
3.然后覆盖拷贝到要移除的位置上,将要移除的位置进行覆盖;
4.再将最后一个位置的数据进行null设置,等待回收。


remove(index)源码:


public E remove(int index) {
//第一步先判断是否有越界,如果越界直接IndexOutOfBoundsException
rangeCheck(index);
modCount++;
//把该元素从数组中提出
E oldValue = elementData(index);
//需要复制的长度
int numMoved = size - index - 1;
if (numMoved > 0)
//原数组,从哪开始复制,目标数组,复制起始位置,长度。过程如下图:
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//赋值null等待回收
elementData[--size] = null;
return oldValue;
}
复制代码

五.ArrayList的去重处理


1.循环list中所有的元素然后删除


    public static ArrayList removeDuplicate_1(ArrayList list){
for(int i =0;i<list.size()-1;i++){
for(int j=list.size()-1;j>i;j--){
if(list.get(i).equals(list.get(j)))
list.remove(j);
}
}

return list;
}
复制代码

2.利用hashSet剔除重复元素,无序


public static ArrayList removeDuplicate_2(ArrayList list){
HashSet set = new HashSet(list);
//使用LinkedHashSet可以保证输入的顺序
//LinkedHashSet<String> set2 = new LinkedHashSet<String>(list);
list.clear();
list.addAll(set);
return list;
}
复制代码

3.利用list的contains方法去重


public static ArrayList removeDuplicate_3(ArrayList list){
ArrayList tempList = new ArrayList(list.size());
for(int i=0;i<list.size();i++){
if(!tempList.contains(list.get(i)))
tempList.add(list.get(i));
}
return tempList;
}
复制代码

contains是根据什么原理来进行比较的呢?
可以看出contains其实也是用equals来比较的,而equals是比较的地址


public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
复制代码

六.什么时候选择ArrayList?


6.1 ArrayList的for和Iterator遍历效率差别


ArrayList实现了RandomAccess接口,这个接口是个标志接口,可以随机访问,使得ArrayList的for循环遍历的效率高于Iterator遍历;而LinkedList则是Iterator遍历效率更高。


for()和Iterator的抉择?

for循环遍历,基于计数器的:
顺序存储:读取性能比较高。适用于遍历顺序存储集合。
链式存储:时间复杂度太大,不适用于遍历链式存储的集合。


迭代器遍历,Iterator:
顺序存储:如果不是太在意时间,推荐选择此方式,毕竟代码更加简洁,也防止了Off-By-One的问题。
链式存储:平均时间复杂度降为O(n),推荐此遍历方式。


6.1.2 for和Iterator()的remove()不同之处


对ArrayList遍历:


1.迭代器进行遍历的时候不可以对迭代的对象,所以不能在使用Iterator遍历的同时list一处这个元素,但是可以使用iterator的remove();


2.而for循环是不可以在for循环的时候调用list的remove()方法,会报错。


6.3 集合里各位实现类的优缺点比较


对于数据的操作,一般是增删改查,排序,数据重复,是否可存空,线程安全性来看的。
将根据以上操作对相应的集合进行优缺点的比较整理。如下:


List:有序,元素可重复

实现类:
线程安全:Vector,
线程不安全:ArrayList,LinkedList
插入和删除效率高:LinkedList
查询速度高:ArrayList


Set:元素不能重复

查询速度:LinkedHashSet=HashSet
查询,查找速度:HashSet>TreeSet
查询,删除,增加元素的效率都很高


Map

线程安全:HashTable,key和value都不能为空
线程不安全:HashMap,key和value都能为空
迭代访问速度快:LinkedHashMap,迭代遍历时,取出的键值对的顺序是其插入顺序;遍历速度<HashMap


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

java开发:异常你了解多少

一、异常体系1、error/exception异常是 Throwable 这个父类实现的,下面有两大子类,Error与ExceptionError表示错误,exception表示异常Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代...
继续阅读 »

一、异常体系

1、error/exception

异常是 Throwable 这个父类实现的,下面有两大子类,Error与Exception

Error表示错误,exception表示异常

Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,

Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。

2、unckecked exception/checked exception

非检查异常(unckecked exception):

Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。

检查异常(checked exception):

除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。

二、异常使用

1、运行java异常处理机制

  1. try…catch语句
  2. finaly 任何情况下都会执行(健壮性)
  3. throws 方法声明处抛出多个异常,用逗号隔开【public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN】
  4. throw 抛出异常

2、异常处理原理

java虚拟机用方法调用栈(method invocation stack)来跟踪每个线程中一系列的方法调用过程。该堆栈保存了每个调用方法的本地信息(比如方法的局部变量)。每个线程都有一个独立的方法调用栈。对于Java应用程序的主线程,堆栈底部是程序的入口方法main()。当一个新方法被调用时,Java虚拟机把描述该方法的栈结构置入栈顶,位于栈顶的方法为正在执行的方法。

当一个方法正常执行完毕,Java虚拟机会从调用栈中弹出该方法的栈结构,然后继续处理前一个方法。如果在执行方法的过程中抛出异常,则Java虚拟机必须找到能捕获该异常的catch代码块。它首先查看当前方法是否存在这样的catch代码块,如果存在,那么就执行该catch代码块;否则,Java虚拟机会从调用栈中弹出该方法的栈结构,继续到前一个方法中查找合适的catch代码块。在回溯过程中,如果Java虚拟机在某个方法中找到了处理该异常的代码块,则该方法的栈结构将成为栈顶元素,程序流程将转到该方法的异常处理代码部分继续执行。当Java虚拟机追溯到调用栈的底部的方法时,如果仍然没有找到处理该异常的代码块,按以下步骤处理。

(1)调用异常对象的printStackTrace()方法,打印来自方法调用栈的异常信息。

(2)如果该线程不是主线程,那么终止这个线程,其他线程继续正常运行。如果该线程是主线程(即方法调用栈的底部为main()方法),那么整个应用程序被终止。

总结:方法进栈,只要没运行完就一直进栈,运行完出栈,一旦出现问题,找catch,当前代码块没找到就出栈,找到执行catch,未找到判断是否是主线程,不是则杀死当前线程,其他安全,是则退出。

3、异常流程的运行过程

finaly不执行的情况:

try{
System.out.println("try");
System.exit(0);
}catch (Exception e){
}finally {
System.out.println("finally");
}

System.exit(0);关闭虚拟机,不会执行finally

catch块中有catch

public static void bar2()
{
try {
System.out.println("try");
int a = 5 / 0;
} catch (Exception e){
System.out.println("catch");
throw new NullPointerException();
}finally {
System.out.println("finally");
}

public static void main(String[] args){
try {
bar2();
}catch (Exception e){
System.out.println("out catch");
}
}

执行结果
try
catch
finally
out catch

finally代码块会在return之前执行:

public static void main(String[] args){        
int i = bar3();
System.out.println(""+i);
}

try{
return 1;
} catch (Exception e){
System.out.println("catch");
} finally{
System.out.println("finally");
}
return 0;

输出:
catch
finally
1

但是无法在finaly中改变返回值
public static int bar3(){
int a = 4;
try{
return a;
} catch (Exception e){
System.out.println("catch");
} finally{
a++;
System.out.println("finally");
}
return a;
}

输出:
finally
4

finally会在return前执行,但也无法改变return变量的值。

finally中的return 会覆盖 try 或者catch中的返回值。

 int m = foo();
System.out.println(""+m);
int n = bar();
System.out.println(""+n);

}


public static int foo() {
try{
int a = 5 / 0;
} catch (Exception e){
System.out.println("catch");
return 1;
} finally{
System.out.println("finally");
return 2;
}

}

public static int bar()
{
try {
System.out.println("try");
return 1;
}finally {
System.out.println("finally");
return 2;
}
}

输出:
catch
finally
2
try
finally
2

finally中有return 会导致catch中的异常丢失:

 public static int bar2()
{
try {
System.out.println("try");
int a = 5 / 0;
} catch (Exception e){
System.out.println("catch");
throw new NullPointerException();
}finally {
System.out.println("finally");
return 1;

}
}

结果:finally中绝对不要使用return语句

收起阅读 »

Android修炼系列(九),你的签名校验形同虚设..

声明:本文所述均为技术讨论,切勿用于违法行为。 我们知道签名是Android软件的一种有效身份标识,因为签名所使用的秘钥文件是我们所独有的,而当我们app被重新打包后,app的签名信息势必会被篡改,所有我们就可以根据软件运行时签名与发布时签名的相同与否来决定...
继续阅读 »

声明:本文所述均为技术讨论,切勿用于违法行为。



我们知道签名是Android软件的一种有效身份标识,因为签名所使用的秘钥文件是我们所独有的,而当我们app被重新打包后,app的签名信息势必会被篡改,所有我们就可以根据软件运行时签名与发布时签名的相同与否来决定是否需要将app中止运行。常用的Java层签名校验方法见下:


签名校验


Android SDK中提供了检测软件签名的方法,我们可以使用签名对象的 hashCode() 方法来获取一个Hash值,在代码中比较它的值即可,下面是获取当前运行时的签名信息代码:


    public static int getSignature(Context context) {
PackageManager pm = context.getPackageManager();
PackageInfo pi;
StringBuilder sb = new StringBuilder();
// 获取签名信息
try {
pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = pi.signatures;
for (Signature signature : signatures) {
sb.append(signature.toCharsString());
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return sb.toString().hashCode();
}
复制代码

接下来我们需要跟我们发布时的签名信息比较,在这里已经把Hash值MD5加密了:


    int signature = getSignature(getApplicationContext());
if(!MD5Util.getMD5(String.valueOf(signature)).equals("发布时签名值")){
// 可能被重编译了,需要退出
android.os.Process.killProcess(android.os.Process.myPid());
}
复制代码

classes.dex的crc32校验


通常重编译 apk 就是重编译 classes 文件,而代码重新编译后,生成的 classes.dex 文件的Hash值就会改变,所以我们可以检查程序安装后 classes.dex 文件的Hash值来判断软件是否被重新打包过。至于Hash算法MD5和CRC都可以,在这里就直接使用CRC算法获取当前运行的app的crc32值了:


    public static long getApkCRC(Context context) {
ZipFile zf;
try {
zf = new ZipFile(context.getPackageCodePath());
// 获取apk安装后的路径
ZipEntry ze = zf.getEntry("classes.dex");
return ze.getCrc();
}catch (Exception e){
return 0;
}
}
复制代码

有了当前的crc32值了,那么我们只需要将其与我们app发布时的crc32原始值做比较了,这是我们的java逻辑,R.string.classes_txt 的值我们我们可以先随意赋予一个(不影响),随后AndroidStudio开始正式打包:


    String srcStr = MD5Util.getMD5(String.valueOf(CommentUtils.getApkCRC(getApplicationContext())));
if(!srcStr.equals(getString(R.string.classes_txt))){
// 可能被重编译了,需要退出
android.os.Process.killProcess(android.os.Process.myPid());
}
复制代码

当打包成功后,我们获取apk的classes.dex的crc32值,随后将该crc32值赋予R.string.classes_txt,最后通过AndroidStudio再重新打包即可(因为更改资源文件并不会改变classe.dex的crc32值,改变代码才会)。获取classes.dex的crc32值的方法,可使用 Windows CRC32命令工具,使用方法如下:


在这里插入图片描述


Java层面的校验方法都是脆弱的,因为破解者可以直接更改我们的判断逻辑以达到绕开校验的目的,所以我们只能通过增加其破解工作量,来达到一点点防破解的夙愿。


建议将crc32值或签名的hash值进行MD5加密,在代码中使用加密后的值进行比较,防止反编译后的全局搜索;建议将签名校验与 classes.dex 校验结合起来使用,先进行签名校验,校验成功后将正确签名hash值作为参数去后台请求 classes.dex 的crc32值,再与当前运行crc32值进行比较;建议进行多处校验,每处使用变形判断语句,并与其他判断条件组合使用,以增加破解时的工作量。


反编译与二次打包


在讲述如何绕过Java代码签名校验的内容前,我先简单介绍下如何使用apktool来反编译apk,并进行二次打包。首先需要下载工具,这里使用的是:apktool.jar和apktool.bat


使用apktool获取apk资源文件和smali文件


将我们下载的apktool.jar文件和apktool.bat文件放在一起,并将待编译apk文件拷贝过来,如下目录:


QQ20210404-161827@2x.png


随后在相应文件目录下,执行命令:apktool d test.apk,执行完毕,我们会发现apktool所在目录下生成了一个与apk同名的文件夹,即apk反编译出来的资源文件和smali文件,smali文件是dex文件反编译的结果,但不同于dex2jar的反编译过程:


QQ20210404-161610@2x.png


使用apktool对apk文件进行二次打包


在上述的反编译操作完成后,我们就能够发现smali文件夹内的.smali文件,其由smali语言编写,即Davlik的寄存器语言,smali有自己的语法并且可以修改,修改后可以被二次打包为apk,需要注意的是,apk经过二次打包后并不能直接安装,必须要经过签名后才能安装。


QQ20210404-161947@2x.png


现在我们要将编译出来的test文件,重新打包成apk文件,刚才我就说了,smali是有自己的语法并且可以修改的,所以我们完全可以按照我们的要求,更改smali文件之后再进行打包,不过在这里我就仅仅简单的演示下打包操作了。


QQ20210404-160313@2x.png


首先我们打开cmd命令,输入命令:apktool b test,执行命令完毕后,会在test文件夹中生成dist文件夹,该文件夹下就保存着我们二次打包后生成的apk文件,但是这个apk文件由于没有进行过签名,所以是不能够安装和运行的,签名的方法咱们接着往下看:


QQ20210404-160555@2x.png


使用Auto-sign对二次打包后的apk文件进行签名


首先我们需要下载Auto-sign工具,并放在apktool所在目录下(推荐):


QQ20210404-160833@2x.png


随后将我们待签名的apk文件复制到Auto-sign目录之下,并更改名称为update.zip :


QQ20210404-160940@2x.png


至于为何要更改为update.zip文件,我们可以看下Sign.bat文件则一目了然:


这里写图片描述


最后我们双击Sign.bat文件,将同目录下生成的update_signed.zip文件更改为test.apk文件即可,这个test.apk文件就是我们最终所需要的签名后的二次打包文件,在这个例子中,如果用户app没有做签名校验,那么重新打包后的apk与原始apk功能完全一样:


QQ20210404-161350@2x.png


通过上面的操作,我们能够发现,如果我们不进行签名校验,那么不法者仅仅只凭借apktool和Auto-sign工具就可以轻松破解我们的app并重新打包成新的apk。


绕过Java代码签名校验


在这里我就仅以一个简单demo为例,首先我们将待编译apk通过apktool生成我们所需要的smali文件,这些文件会根据程序包的层次结构生成相应的目录,程序中所有的类都会在相应的目录下生成独立的smali文件:


QQ20210404-162421@2x.png


然后我们通过 dex2jar 和 jd-gui 得到反编译出的java代码(往往都是已混淆的),通过查看Java代码我们可以快速搜索出需要的Android API方法,再通过API方法的位置来定位到相应smali文件的大概位置:


在这里插入图片描述


一般java层面的签名校验都离不开signatures来获取签名信息,所以我们可以在 jd-gui 中全局搜索signatures关键字,找到获取签名的方法,当然如果app在校验失败前有着特殊的Toast提示或者Log信息那就更方便了:


在这里插入图片描述


随后打开我们查找到的signatures代码,一般情况下app都会进行多处校验:


signatures获取签名信息


随后我们顺藤摸瓜,找到f()方法被调用的地方,在这里就只拿 jd-gui 来试试水了,我们可以先通过AndroidManifest.xml文件找到Application和主Activity,一般在这里都会进行一些校验和身份状态的判断,改好一处之后,通过运行app,再根据app退出或者卡住的位置来定位下一处校验代码的位置,直到运行成功。


在这里插入图片描述


通过上面的语句我们可以知道,这只是一个简单的equals()比较,之后我们打开相应的smali文件,搜索"F010AF8CFE611E1CC74845F80266",定位签名校验的反编译代码位置:


invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;

move-result-object v0

invoke-static {v0}, Lcom/lcmhy/c/a/d;->a(Ljava/lang/String;)Ljava/lang/String;

move-result-object v0

const-string v1, "F010AF8CFE611E1CC74845F80266"

invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

move-result v0

if-nez v0, :cond_1
复制代码

在这里我们只需要将判断语句if-nezv0, :cond_1更改为if-eqz v0, :cond_1翻转逻辑即可,随后我们通过apkTool重新打包并签名,ok,运行成功。


hook绕过系统签名


上面的方式虽然正统,但在实际的操作过程中,如果目标代码很多,校验逻辑又分散在各处,就很难全部修改绕过了。所以目前都是尝试从最根本的接口入手,通过hook系统的接口来达到绕过签名校验的目的。通过上文我们知道获取系统签名的API如下:


    public static int getSignature(Context context) {
PackageManager pm = context.getPackageManager();
...
// 获取签名信息
pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
...
}
复制代码

可以看到,获取签名最核心的方法:getPackageInfo,我们来查看下getPackageManager源码:


QQ20210404-164724@2x.png


可以看到这里会接着调用ActivityThread的静态方法,我们进入ActivityThread源码看下:


QQ20210404-165110@2x.png


竟然有个静态变量sPackageManager ,而且是个接口。到这里你想到了什么?第一想法当然是动态代理啊,不了解的可以去翻下我的上篇文章:Android修炼系列(一),写一篇易懂的动态代理讲解,思路就是通过反射将 sPackageManager 对象替换成我们的代理对象,并在代理对象中对于 getPackageInfo 方法进行重定义即可。


QQ20210404-165522@2x.png


代码也不难,直接通过反射拿到sPackageManager,并注入到我们的代理对象内,hook代码如下:


    public void hookGetPackageInfo(Application app) throws Exception {
Class clzActivityThread = Class.forName("android.app.ActivityThread");
Method methodGetPackageManager = clzActivityThread.getDeclaredMethod("getPackageManager");
methodGetPackageManager.setAccessible(true);
Object sPackageManager = methodGetPackageManager.invoke(null);
// 动态代理
Class clzIPackageManager = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(clzActivityThread.getClassLoader()
, new Class[]{clzIPackageManager}
, new PackageManagerProxy(app, sPackageManager));
// 替换原sPackageManager
Field filedIPackageManager = clzActivityThread.getDeclaredField("sPackageManager");
filedIPackageManager.setAccessible(true);
filedIPackageManager.set(null, proxy);
}
复制代码

这是我们代理对象代码,如果有不清楚的,可以翻一翻原来的文章:


    @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("getPackageInfo")) {
String packageName = "";
if (null != args && args.length > 0 && args[0] instanceof String) {
packageName = (String) args[0];
}
final PackageInfo packageInfo;
if (mApplication.getPackageName().equals(packageName)
&& null != (packageInfo = (PackageInfo) method.invoke(mRealPackageManager, args))) {
final byte[] b = null; // 原APK签名
Signature signature = new Signature(b);
if (null == packageInfo.signatures) {
packageInfo.signatures = new Signature[1];
}
packageInfo.signatures[0] = signature;
return packageInfo;
}
}
return method.invoke(mRealPackageManager, args);
}
复制代码


好了,本文到这里,关于签名校验的介绍就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



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

收起阅读 »

Android修炼系列(八),你真的会写注释吗?

本节主要介绍下我们常用的 javadoc tag ,虽然内容比较简单,但若正确使用,真的能使我们的代码高大上不少。不仅如此,只要我们按照Javadoc 注释规则,在编码完成后,Javadoc 也能够帮我们从源代码中生成相应的 Html 格式的 API 开发文档...
继续阅读 »

本节主要介绍下我们常用的 javadoc tag ,虽然内容比较简单,但若正确使用,真的能使我们的代码高大上不少。不仅如此,只要我们按照Javadoc 注释规则,在编码完成后,Javadoc 也能够帮我们从源代码中生成相应的 Html 格式的 API 开发文档。可以点击Oracle规范,我将常用的javadoc tag 根据自己的习惯进行了整理,见下:


在这里插入图片描述


tags


在给公共类或公共方法添加注释的时候,第一句话应该是一个简短的摘要。注意左侧不要紧挨 * 号,要有一个空格。如果注释有多个段落,使用< p>段落标记来分隔段落。我们还可使用< tt>标签来让特定的内容呈现出等宽的文本效果。见下:


    /**
* 第一句话是这个方法的<tt>简短</tt>摘要。
* 如果这个描述太长,记得换行。
*
* <p>如果多个段落可以这样
* 当回车的时候与标签首部对齐即可
*/
public void test(){}
复制代码

如果注释描述里需要包含一个列表,一组选项等,我们可以使用< li>标签来标识,注意标签后不需要空格,见下:


    /**
* 第一句话是这个方法的简短摘要。
* 如果这个描述太长,记得换行。
*
* <p>如果多个段落可以这样
*
* <ul>
* <li>这是列表1
* <li>这是列表2...
* 同样回车后与标签对齐即可
* </ul>
*/
public void test(){}
复制代码

@param 是用来描述方法的输入参数。注意在方法描述和tag 之间需要插入空白注释行。不需要每个参数param的描述都对齐,但要保证同个param的多行描述对齐。param 的描述不需要在句尾加标点。


    /**
* 第一句话是这个方法的简短摘要。
* 如果这个描述太长,记得换行。
*
* @param builderTest 添加参数的描述,如果描述很长,
* 需要回车,这里需要对齐
* @param isTest 添加参数描述,不需要刻意与其他param
* 参数对齐
*/
public void test(String builderTest, boolean isTest){}
复制代码

@return 是用来描述方法的返回值。要写在@param tag之后,与其他tag 之间不需要换行。@throws 是对方法可能会抛出的异常来进行说明的,通常格式为:异常类名+异常在方法中出现的原因。见下:


    /**
* 第一句话是这个方法的简短摘要。
*
* @param capacity 添加参数描述,不需要刻意与其他param
* 参数对齐
* @return 描述返回值的含义,可以多行,不需要句号结尾
* @throws IllegalArgumentException 如果初始容量为负
* <ul>
* <li>这是抛出异常的条件1(非必须),注意<li>格式
* </ul>
* @throws 注意如果方法还存在其他异常,可并列多个
*/
public int test(int capacity){
if (capacity < 0)
throw new IllegalArgumentException("Illegal initial capacity");
return capacity;
}
复制代码

@deprecated 用于指出一些旧特性已由改进的新特性所取代,建议用户不要再使用旧特性。常与@link 配合,当然@link的使用位置没有任何限制,当我们的描述需要涉及到其他类或方法时,我们就可以使用@link啦,javadoc会帮我们生成超链接:


    /**
* 第一句话是这个方法的简短摘要。
* 如果这个描述太长,记得换行。
*
* @deprecated 从2.0版本起不推荐使用,替换为{@link #Test2()}
* @param isTest 添加参数描述,不需要刻意与其他param
* 参数对齐
*/
public void test(boolean isTest){}
复制代码

@link 常见形式见下:
在这里插入图片描述


@code 用来标记一小段等宽字体,也可以用来标记某个类或方法,但不会生成超链接。常与@link配合,首次通过@link生成超链接,之后通过@code 呈现等宽字体。


    /**
* 第一句话是这个方法的简短摘要。
* 我们可以关联{@link Test}类,随后通过{@code Test}类怎样怎样
* 也可以标记一个方法{@code request()}
*
* @param isTest 添加参数描述,不需要刻意与其他param
* 参数对齐
*/
public void test(boolean isTest){}
复制代码

@see 用来引用其它类的文档,相当于超链接,javadoc会在其生成的HTML文件中,将@see标签链到其他的文档上:


    /**
* 第一句话是这个方法的简短摘要。
*
* @param capacity 添加参数描述,不需要刻意与其他param
* 参数对齐
* @return 描述返回值的含义,可以多行,不需要句号结尾
* @throws IllegalArgumentException 如果初始容量为负
* @see com.te.Test2
* @see #test(int)
*/
public int test(int capacity){
if (capacity < 0)
throw new IllegalArgumentException("Illegal initial capacity");
return capacity;
}
复制代码

@see形式与@link类似,见下:
在这里插入图片描述
@since 用来指定方法或类最早使用的版本。在标记类时,常与@version和@author配合,一个用来指定当前版本和版本的说明信息,一个用来指定编写类的作者和联系信息等。我们也可以通过< pre>来添加一段代码示例。见下:


    /**
* 第一句话是这个类的简短摘要。
* <pre>
* Test<Test2> t = new Test<>();
* </pre>
*
* <p>同样可以多个段落。
*
* @param <T> 注意当类使用泛型时,我们需要使用params说明。这时格式需要插入空白行
*
* @author mjzuo 123@qq.com
* @see com.te.Test2
* @version 2.1
* @since 2.0
*/
public class Test<T extends Test2> {
/**
* 第一句话是这个方法的简短摘要。
*
* @params capacity 参数的描述
* @return 返回值的描述
* @since 2.1
*/
public int test2(int capacity) {
return capacity;
}
}
复制代码

@inheritDoc 用来从当前这个类的最直接的基类中继承相关文档到当前的文档注释中。如下的test() 方法,会直接继承该类的直接父类的test()方法注释。注意与其他tag 不需要插入空行:


    /**
* {@inheritDoc}
* @since 2.0
*/
public void test(boolean isTest){}
复制代码

@docRoot 它总是指向文档的根目录,表示从任何生成的页面到生成的文档根目录的相对路径。例如我们可以在每个生成的文档页面都加上版权链接,假设我们的版权页面copyright.html 在根目录下:


    /**
* <a href="{@docRoot}/copyright.html">Copyright</a>
*/
public class Test {}
复制代码

@hide 当我们使用google提供的Doclava时,可以使用 @hide 来屏蔽我们不想暴露在javaDoc文档中的方法。


    /**
* {@hide}
*/
public class Test {}
复制代码


好了,本文到这里,关于常用的javaDoc tag的介绍就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。


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

Android修炼系列(七),方法调用,背后的秘密

在前篇已经讲解了类是如何被加载的? 和 对象是如何被分配和回收的?,本节主要看下,方法又是如何被调用和执行的? 栈帧 栈帧是虚拟机 栈内存 中的元素,是支持虚拟机进行方法调用和方法执行的数据结构。其内存储了方法的局部变量表、操作数栈、动态连接、方法返回地址和一...
继续阅读 »

在前篇已经讲解了类是如何被加载的?对象是如何被分配和回收的?,本节主要看下,方法又是如何被调用和执行的?


栈帧


栈帧是虚拟机 栈内存 中的元素,是支持虚拟机进行方法调用和方法执行的数据结构。其内存储了方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。每个方法的调用开始到执行完毕,都对应了栈帧在栈里的入栈到出栈的过程。


一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对于当前栈帧进行操作。


下文主要从局部变量表、动态连接、操作数栈三个方面进行介绍。


局部变量表


局部变量表是一个存储变量值的空间,存放着我们熟悉的方法参数和方法内的局部变量。它的大小,在程序编译时就被确定下来了,并被写入到了方法表的Code属性之中。前文我们知道,Class文件就是一组以8位字节为单位的2进制流,各项数据是严格按照特定顺序紧凑的排列在了Class文件之中。而方法表即在如下位置:


image.png


Code属性出现在方法表的属性集合之中,但也不是所有的方法表都存在这个属性,如接口和抽象类的方法就不存在Code属性。其中Code属性内的max_locals就定义了方法所需要的分配局部变量表的最大容量。而局部变量表又以变量槽Slot为最小单位,存放着我们如下的数据类型的数据:


image.png


其中reference类型表示对一个对象实例的引用,还记得对象的成员变量的引用吗?只不过一个在栈内存中,一个在栈帧的局部变量表的code属性中。


注意局部变量表中第0位索引的Slot默认用于传递方法所属对象实例的引用,接下来开始按照方法参数的顺序来给参数分配Slot(从1开始),最后再根据方法体内定义的变量顺序和作用域来分配其余的Slot。


image.png


动态连接


前面讲过,Class文件的常量池主要存放两大类常量:字面常量和符号引用。其中符号引用包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。


我们方法调用中的目标方法在Class文件里就是一个常量池中的符号引用,字节码中的方法调用指令就以常量池中的指向方法的符号引用作为参数。举个栗子:


    10:    invokevirtual #22 //Method ...A.hello:()V
复制代码

invokevirtual就是调用指令,参数是常量池中第22项的常量,注释显示了这个常量是A.hello()的符号引用。


java代码在javac编译的时候,并不会有“连接”的步骤,而是在虚拟机加载Class文件的时候进行“动态连接”。也就是说Class文件不会保存各个方法字段的内存入口地址(直接引用),所以虚拟机是无法直接使用的。这就要求在虚拟机运行时,从虚拟机获得符号引用,再在类创建时或运行时解析为直接引用。


在类加载的解析阶段,就会有一部分符号引用被直接被转化为直接引用,这类转换称为静态解析,这类方法都符合“编译期可知,运行期不可变”,符合这个条件的有静态方法、私有方法、实例构造器、父类方法、final方法,也就是我们常称的非虚方法。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。


每个栈帧都包含有一个指向运行时常量池的该栈帧所属方法的符号引用,持有这个引用就是为了支持方法调用过程中的动态连接。


操作数栈


操作数栈也叫操作栈,是一个后入先出的栈结构。操作数栈的的最大深度也在编译的时候就被写入到了Code属性之中的max_stacks数据项中。操作数栈的每个数据元素可以是任意的java数据类型,包括long和double,其中32位的数据类型占栈容量为1,64位数据类型占栈容量为2。


方法的调用并不等同于方法的执行,方法调用阶段的唯一任务,就是确定要调用的是哪一个方法,还不涉及方法内部的具体运行过程。而在方法的执行过程中,会通过各种字节指令往操作栈中写入和提取内容,也就是出栈/入栈操作。这些编译器编译的字节码指令都被存放在了方法属性集合中的Code属性里面了。


这些指令操作包括将局部变量表的Slot数据推入栈顶,也可将栈内的数据出栈并存入Slot中,也可通过指令将数据出栈操作再入栈等等。以下面方法为栗子:


    public int a() {
int a = 10;
int b = 20;
return (a + b) * 100;
}
复制代码

通过javap -c 查看其字节码如下:


   public int a();
Code:
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: bipush 100
11: imul
12: ireturn
复制代码

在方法刚刚执行的时候,操作栈是空的。


image.png


:0 首先执行偏移地址为0的指令,bipush指令的作用是将单字节的整数型常量值 10 推入操作数栈顶:


image.png


:2 执行偏移地址为2的指令,istore_1的指令是将操作栈顶的整数型值出栈并存放在第一个局部变量Slot中。后续的2条指令是一样的,将 b:10 存放在局部变量Slot中。


image.png


:6 执行偏移地址6的指令,iload_1的作用是将局部变量表第1个Slot中的整形值复制到操作栈顶:


image.png


:7 同理,执行偏移地址7的指令,iload_1的作用是将局部变量表第2个Slot中的整形值复制到操作栈顶:


image.png


:8 执行偏移地址8的指令,iadd指令的作用将操作数栈中头两个栈元素出栈,做整形加法,然后把结果重新入栈。即在iadd执行完毕后,元素10、20出栈,相加结果30会重新入栈:


image.png


:9 执行偏移地址为9的指令,bipush指令的作用是将单字节的整数型常量值 100 推入操作数栈顶:


image.png


:11 执行偏移地址为11的指令,imul指令是将操作栈顶两个元素出栈,并做乘法运算,然后将结果重新入栈,与iadd操作一样:


image.png


:12 执行偏移地址为12的指令,ireturn指令,它将结束方法执行并将操作栈的整型值返回此方法的调用者。到此为止,此方法执行结束。



好了,本文到这里,关于方法是如何被JVM调用和执行的介绍就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



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


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

收起阅读 »

Android修炼系列(六),时间与空间复杂度的概念

本来是想将时间复杂度和空间复杂度的内容,放到后面的算法系列,但后想想,其实复杂度的审视应该是贯彻于整个开发过程之中的,应该是属于更大概念的“代码规范”的一部分,而不应局限在某个算法上。当然本文仅是以能用能理解为主,并不会深入到推倒公式的那种程度。分析当一个问题...
继续阅读 »

本来是想将时间复杂度和空间复杂度的内容,放到后面的算法系列,但后想想,其实复杂度的审视应该是贯彻于整个开发过程之中的,应该是属于更大概念的“代码规范”的一部分,而不应局限在某个算法上。当然本文仅是以能用能理解为主,并不会深入到推倒公式的那种程度。

分析

当一个问题的算法被确定以后,那么接下来最重要的当然是评估该算法所用时间和占用内存资源的问题了,如果其运行时间超出了我们所能接受的底线,或者资源的占用多到当前设备不能满足的程度,那么对于我们来说,这个算法就是无用的,即使它能够正确的运行。

相比于执行完程序再事后统计其所用时间和占用空间的方法,理论层面的复杂度分析更有优势,主要表现在两点:

1、算法运行所在的设备,配置不同、运行环境的不同,都会给算法本身运行的实际时间和空间的计算带来偏差;

2、测试数据规模的大小,数据本身的特殊性与否,也会使实际的运行结果不具有普适性,不容易正确的反应算法的性能的一个真实情况。

那怎么从理论层面来分析复杂度呢?

大O标记法

关于 大O 标记法的相关描述,我就直接引用「数据结构与算法分析」的内容了:

一般来说,估计算法资源消耗所需的分析是一个理论问题,因此需要一套正式的系统架构,我们先从某些数学定义开始。

如果存在正常数 c 和 n_0 使得当N ≥ n_0 时,T(N) ≤ c f(N),则记为T(N) = O( f(N) )。

定义的目的是建立一种相对的级别。给定两个函数,通常存在一些点,在这些点上一个函数的值小于另一个函数的值,因此,一般地宣称,比如说f(N) < g(N) ,是没有什么意义的。于是,我们比较它们的相对增长率。当将相对增长率应用到算法分析时,我们将会明白为什么它是重要的度量。

虽然对于较小的 N 值,1000N 要比 N^2 大,但 N^2 以更快的速度增长,因此 N^2 最终将是更大的函数。在这种情况下,N = 1000 是转折点。定义是说,最后总会存在某个点 n_0 ,从它以后 c · f(N) 总是至少与 T(N) 一样大,从而若忽略常数因子,则 f(N) 至少与 T(N)一样大。

在我们的例子中,T(N) = 1000N,f(N) = N^2n_0 = 1000 而 c=1。我们也可以让 n_0 = 10 而 c = 100。因此,可以说 1000N = O(N^2)。这种记法称之为 大O标记法。人们常常不说“...级的”,而是说“大O...”。

同理还有下面的几个定义:

函数表达式含义
T(N) = O( f(N) )是说T(N) 的增长率小于或等于 f(N) 的增长率(符号读音'大O')
T(N) = Ω( g(N) )是说T(N) 的增长率大于或等于 g(N) 的增长率(符号读音'omega')
T(N) = Θ( h(N) )是说T(N) 的增长率等于 h(N) 的增长率(符号读音'theta')
T(N) = o( p(N) )是说T(N) 的增长率小于 p(N) 的增长率(符号读音'小o')

还有一点需要知道的是,当 T(N) = O( f(N) ) 时,我们是在保证函数 T(N) 是在以不快于 f(N)的速度增长,因此 f(N) 是T(N)的一个上界。这意味着 f(N) = Ω( T(N) ),于是我们说T(N)是f(N)的一个下界。”

时间复杂度分析

下面我们来看一段非常简单的代码

1    private static int getNum(int n) {
2 int currentNum = 0;
3 for(int i = 0; i < n; i++) {
4 currentNum += i*i;
5 }
6 return currentNum;
7

在分析时,我们可以忽略调用方法、变量的声明和返回值的开销,所以我们只需要分析第2、3、4行的时间开销:

第2行占用1个时间单元;第4行的1次执行实际占用3个时间单元(1次乘法、1次加法、一次赋值),但是这么精确的计算是没有意义的,对于我们分析大O的结果也是无关紧要的,而且随着程序的复杂度提高这种方式也会变得越来越不可操作,(推导过程就省略了,直接上结论了,本节主要是用法层面).

所以我们也记第4行的1次执行时间开销为1个时间单元,则 n 次执行开销为 n 个时间单元;同理第3行执行 n 次的时间开销也为 n 个时间单元,所以执行总开销为 (2n + 1) 个时间单元。所以f(N) = 2n+1,根据上文T(N) = c · f(N)到T(N) = O(2n + 1)的大O表示过程知道,我们可以抛弃一些前导的常数和抛弃低阶项,所以T(N) = O(N)

知道了分析方法,下面我们再来看看其他复杂度的代码

1    private static void getNum(int n) {
2 int currentNum = 0;
3 for(int i = 0; i < n; i++) {
4 for(int j = 0; j < n; j++) {
5 currentNum++;
6 }
7 }
8

通过上面代码我们可知:第2行1个单元时间,第3行 n 个单元时间,第4行 n^2 个单元时间,第5行 n^2 个单元时间,所以总时间开销f(N) = 2·n^2 + n + 1,所以复杂度T(N) = O(N^2),当然O(N^3^)都是同理的。

1    private static void getNum(int n) {
2 int currentNum = 0;
3 for(int k = 0; k < n; k++) {
4 currentNum++;
5 }
6 for(int i = 0; i < n; i++) {
7 for(int j = 0; j < n; j++) {
8 currentNum++;
9 }
}

通过上面代码我们可知:第2行1个单元时间,第3行 n 个单元时间,第4行 n 个单元时间,第6行 n 个单元时间,第7行 n^2 个单元时间,第8行 n^2 个单元时间,所以总时间开销f(N) = 2·n^2+3·n + 1,所以复杂度T(N) = O(N^2)

1        if(condition) {
2 S1
3 } else {
4 S2
5 }

这是一段伪代码,在这里主要是分析 if 语句的复杂度,在一个 if 语句中,它的运行时间从不超过判断condition的运行时间加上 S1 和 S2 中运行时间长者的总的运行时间。

1    private static void getNum(int n) {
2 int currentNum = 0;
3 currentNum++;
4 if(currentNum > 0) {
5 currentNum--;
6 }
7

通过上面的代码我们可知,第2行1个时间单元,第3行1个时间单元,第4行1个时间单元,第5行1个时间单元,所以总开销4个时间单元,所以复杂度T(N) = O(1),注意这里不是O(4)哦。

1    private static void getNum(int n, int m) {
2 int currentNum = 0;
3 for(int i = 0; i < n; i++) {
4 currentNum++;
5 }
6 for(int j = 0; j < m; j++) {
7 currentNum++;
8 }
9

通过上面的代码我们可知,第2行是1个单元时间,第3行是 n 个单元时间,第4行是 n 个单元时间,第6行是 m 个单元时间,第7行是 m 个时间单元,所以总的时间开销f(N) = 2·n +2·m + 1,所以复杂度T(N) = O(n+m),同理,O(m·n)的复杂度也是同样分析。

1    private static void getNum(int n) {
2 int currentNum = 1;
3 while (currentNum <= n) {
4 currentNum *= 2;
5 }
6

通过上面的代码我们可知,第2行需要1个单元时间;第3行每次执行需要1个单元时间,那么现在需要执行多少次呢?通过分析我们知道当 2^次=n时 while 循环结束,所以次数 = log_2n,所以第3行总需要 log_2n 个单元时间;第4行同理也需要 log_2n 个单元时间,所以总时间开销f(N) = 2·log_2n + 1,所以复杂度T(N) = O(logn),注意的是这里不但省略了常数,系数,还省略了底哦。

1    private static void getNum(int n) {
2 int currentNum = 1;
3 for(int i = 0; i < n; i++, currentNum = 1) {
4 while (currentNum <= n) {
5 currentNum *= 2;
6 }
7 }
8

通过上面的代码我们可知,第2行1个单元时间,第3行 n 个单元时间,第4行根据上文我们需要n·log_2n个单元时间,第5行也需要n·log_2n个单元时间,所以总时间花销f(N) = 2·n·log_2n + n + 1,所以复杂度T(N) = O(n·logn)

空间复杂度分析

上面我们简单介绍了几种常见的时间复杂度,空间的复杂度比时间复杂度要简单许多,下面就来分析一下空间的复杂度:

空间复杂度考量的是算法所需要的存储空间问题,一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元,若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。

1    private static void getNum(int n) {
2 int i = 0;
3 for(; i<n; i++){
4 i*=2;
5 }
6

通过上面的代码我们知道,第2行我们只需要1个空间单元;第3行、第4行不需要额外的辅助空间单元,所以空间复杂度S(N) = O(1),注意不是只有1个空间单元才是O(1)哦,如果空间单元是常量阶的复杂度都是O(1)哦。

1    private static void getNum(int n) {
2 int i = 0;
3 int[] array = new int[n];
4 for(; i<array.length; i++){
5 i*=2;
6 }
7

根据上面的代码我们可知,第2行需要1个空间单元;第3行需要 n 个空间单元;第4行、第5行不需要额外的空间单元,所以总消耗f(n) = n + 1,所以空间复杂度S(N) = O(n),其他情况的分析与时间复杂度分析方法一样,在这里就不详细介绍了。

好了,本文到这里就结束了,关于时间复杂度和空间复杂度的介绍应该够平时所需了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。

参考 1、数据结构与算法分析:机械工业出版社


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

收起阅读 »

在Swift中使用泛型

Swift 5.0 都发布这么久了,而泛型作为该语言的重要特性,还是非常有必要了解一下的。在 Swift 泛型的运用几乎随处可见,在系统层面,Swift 标准库是通过泛型代码构建出来的,Swift 的数组和字典类型都是泛型集。在主流的轮子里,也是有大量的泛型使...
继续阅读 »

Swift 5.0 都发布这么久了,而泛型作为该语言的重要特性,还是非常有必要了解一下的。
在 Swift 泛型的运用几乎随处可见,在系统层面,Swift 标准库是通过泛型代码构建出来的,Swift 的数组和字典类型都是泛型集。在主流的轮子里,也是有大量的泛型使用。使用泛型可以提审代码的复用性。
下面就通过实例看看我们的代码怎么写:

1、函数中使用泛型
举个简单的例子哦,比如现在有个简单的需求,就是写一个方法,这个方法需要把传过来的整数参数打印出来。

简单需求卡卡的代码就出来啦:

/// 打印一个整形数字
func myPrintInt(arg:Int){
print(arg)
}

看着没啥毛病哦,产品姥爷又说我现在这个方法要支持字符串。

简单再给添加一个方法好啦:

func myPrintString(arg:String){
print(arg)
}

产品:现在要支持Float、Array、Dictionary ......

这也好办啊:

// 打印任何东西
func myPrintAny(arg:Any){
print(any1)
}

很好,现在我要你传进去两个参数,而且参数类型要一致,你要怎么写。

下面的写法可以不?参数靠大家自觉。

//参数类型千万要一样啊。。。。。
func myPrintAny(any1:Any, any2:Any){
print(any1)
print(any2)
}

写成这样的话就可以那赔偿走人啦。

这时候就应该使用泛型了。

// 打印任何东西
func myPrint<T>(any1:T, any2:T){
print(any1)
print(any2)
}

方法的使用:

myPrint(any1: 1, any2: 1)
myPrint(any1: "1", any2: "1")
myPrint(any1: ["1","2"], any2: ["3","4"])

这里就可以看出泛型的优势所在了,大大提升了代码的可复用性。而且同时也提升了代码的安全性。

泛型和Any的区别
从表面上看,这好像和泛型极其相似。Any 类型和泛型两者都能用于定义接受两个不同类型参数的函数。然而,理解两者之间的区别至关重要:泛型可以用于定义灵活的函数,类型检查仍然由编译器负责;而 Any 类型则可以避开 Swift 的类型系统 (所以应该尽可能避免使用)。

2、类中泛型
实现一个栈,栈里边的元素可以是任何类型,但是所有元素又必须是同一种类型,使用泛型实现的代码就是这样的。

//类作用域
class YJKStack<T>: NSObject {
//栈空间
private var list:[T] = []

//进栈
public func push(item:T){
list.append(item)
}

//出栈
public func pop() -> T{
return list.removeLast()
}
}

当你扩展一个泛型类的时候,原始类型定义中声明的类型参数列表在扩展里是可以使用的,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

简单说就是类中的泛型,在其扩展中也是可以进行使用的。

extension YJKStack{
/// 获取栈顶元素
public func getLast() -> T?{
return list.last
}
}

3、泛型类型约束
在实际运用中,我们的参数虽然可以不是特定的类,但是通常需要这个参数要实现某个协议或者是某个类的子类。这时候就要给泛型添加约束了,代码就是下面这一堆喽

//class YJKProtocolStack<T: A&B>  须实现多个协议的话,用 & 符号链接就好啦。
class YJKProtocolStack<T: A>: NSObject {
//栈空间
private var list:[T] = []

//进栈
public func push(item:T){
list.append(item)
}

//出栈
public func pop() -> T{
return list.removeLast()
}
}

protocol A {}

protocol B {}

看了上面的代码,可能有的小伙伴就迷茫啦,既然有YJKProtocolStack<T: A&B>, 为啥没有 YJKProtocolStack<T: A|B>呢,其实想想就可以明白,如果用 | 的话,T 表示的就不是一个指定的类型啦,这样和泛型的定义是不一致的。

4、关联类
在类及函数里都知道泛型怎么玩了,那么在协议里怎么用啦,是不是和类是一样的呢,写个代码看一下:

//Protocols do not allow generic parameters; use associated types instead
//一敲出来,编译器就提示你错误啦,并且告诉你怎么写了。
protocol C<T> {

}

//正确的写法就是下面这样的哦
protocol C {
// Swift 中使用 associatedtype 关键字来设置关联类型实例
// 具体类型由实现类来决定
associatedtype ItemType

func itemAtIndex(index:Int) -> ItemType

func myPrint(item:ItemType)

// 局部作用域的泛型和类的写法是一样的。
func test<T>(a:T)
}

//协议的泛型约束
protocol D {
associatedtype ItemType:A
}

再来看看实现类怎么玩:

//遵循了 C 协议的类
class CClassOne<T>:C{

//要指定 C 协议中, ItemType 的具体类型
typealias ItemType = T

public var list:[ItemType] = []

//协议方法的实现
func itemAtIndex(index:Int) -> ItemType{
return list[index]
}

func myPrint(item:ItemType){

}

func test<T>(a: T) {

}
}

//实现2
class CClassTwo:C{

typealias ItemType = Int

public var list:[ItemType] = []
func itemAtIndex(index:Int) -> ItemType{
return list[index]
}

func myPrint(item:ItemType){

}

func test<T>(a: T) {

}
}

通过上面两个例子可以看出,只要在实现类中 指定 ItemType 的类型就好啦。这个类型 还可以是个泛型,也可以是具体的数据类型。

还有一点要讲的就是结构体中使用泛型和类是完全一样的处理哦。

5、Where 语句
Where 其实也是做类型约束的,你可以写一个where语句,紧跟在在类型参数列表后面,where语句后跟一个或者多个针对关联类型的约束,以及(或)一个或多个类型和关联类型间的等价(equality)关系。

看看下面几个代码就行啦,不多说了

func test4<T:A>(arg1:T){}
func test5<T>(arg1:T) where T:A{}
// 上面两个方法的作用是一模一样的

//这个方法 arg1 和 arg2 只需是实现 C 协议的对象就好啦
func test6<T1:C, T2:C>(arg1:T1, arg2:T2){}

//这个方法 arg1 和 arg2 需要实现 C 协议, 并且 T1 与 T2 的泛型类型要一致
func test7<T1:C, T2:C>(arg1:T1, arg2:T2) where T1.ItemType == T2.ItemType{}

//这个方法 arg1 和 arg2 需要实现 C 协议, && T1 与 T2 的泛型类型要一致 && T1 的泛型 遵循A协议
func test8<T1:C, T2:C>(arg1:T1, arg2:T2) where T1.ItemType == T2.ItemType, T1.ItemType:A{}

本文写到这里就没结束啦,简单介绍啦泛型在 Swift 中的使用,大家想要深入理解泛型,想要融会贯通、运用自如,还需要大家找时间多看看大神写的代码。

转自:https://www.jianshu.com/p/a01f212e628c

收起阅读 »

关于 iOS 中各种锁的整理

名词解释原子:同一时间只允许一个线程访问临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显...
继续阅读 »

名词解释
原子:
同一时间只允许一个线程访问

临界区:
指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

自旋锁:
是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

互斥锁(Mutex):
是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。
当线程来到临界区,获取不到锁,就会去睡眠主动让出时间片,让出时间片会导致操作系统切换到另一个线程,这种上下文的切换也耗时

读写锁:
是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。
读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。

信号量(semaphore):
是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

条件锁:
就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。
当资源被分配到了,条件锁打开,进程继续运行。

递归锁:
递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。

互斥锁 :

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。

1. NSLock
是 Foundation 框架中以对象形式暴露给开发者的一种锁(Foundation框架同时提供了NSConditionLock,NSRecursiveLock,NSCondition)
NSLock 内部封装了 pthread_mutex 属性为 PTHREAD_MUTEX_ERRORCHECK 它会损失一定的性能来换错误提示。
NSLock 比 pthread_mutex 要慢,因为他还要经过方法调用,但是有缓存多次调用影响不大

NSLock定义如下:

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end

lock 和 tryLock 方法都会请求加锁, 唯一不同的是 trylock 在没有获得锁的时候可以继续做一些任务和处理,lockBeforeDate方法也比较简单,就是在limit时间点之前获得锁,没有拿到返回NO。

2. pthread_mutex :
pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 表示互斥锁。
互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换,性能不及信号量。

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(&(_ticketMutex), &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);

/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1 // NSLock 使用
#define PTHREAD_MUTEX_RECURSIVE 2 // 递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL

备注:我们可以不初始化属性,在传属性的时候直接传NULL,表示使用默认属性 PTHREAD_MUTEX_NORMAL。pthread_mutex_init(mutex, NULL);

3. @synchronized :
@synchronized要一个参数,这个参数相当于信号量

// 用在防止多线程访问属性上比较多
- (void)setTestInt:(NSInteger)testInt {
@synchronized (self) {
_testInt = testInt;
}
}

自旋锁 :
实现原理 : 保护临界区只有一个线程可以访问
伪代码 :

do {  
Acquire Lock // 获取锁
Critical section // 临界区
Release Lock // 释放锁
Reminder section // 不需要锁保护的代码
}

实现思路很简单,理论上定义一个全局变量,用来表示锁的状态即可

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
do {
while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
lock = true; // 挂上锁,这样别的线程就无法获得锁
Critical section // 临界区
lock = false; // 相当于释放锁,这样别的线程可以进入临界区
Reminder section // 不需要锁保护的代码
}

有一个问题就是一开始有多个线程执行 while 循环, 他们都不会在这里卡住,而是继续执行,这样就无法保证锁的可靠性了,解决思路很简单,就是确保申请锁的过程是原子的。

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
do {
while(test_and_set(&lock); // test_and_set 是一个原子操作
Critical section // 临界区
lock = false; // 相当于释放锁,这样别的线程可以进入临界区
Reminder section // 不需要锁保护的代码
}
如过临界区执行时间过长,使用自旋锁是不合适的。忙等的线程白白占用 CPU 资源。

1. OSSpinLock :

编译器会报警告,大家已经不使用了,在某些场景下已经不安全了,主要是发生在低优先级的线程拿到锁时,高优先级线程进入忙等状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁,这被称为优先级反转


新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。

高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。

这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。


2. os_unfair_lock:

os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用,解决了优先级反转问题


两种自旋锁的使用
// 需要导入的头文件
#import
#import
#import
// 自旋锁 实现
- (void)OSSpinLock {
if (@available(iOS 10.0, *)) { // iOS 10以后解决了优先级反转问题

os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
NSLog(@"线程1 准备上锁");
os_unfair_lock_lock(unfairLock);
sleep(4);
NSLog(@"线程1执行");
os_unfair_lock_unlock(unfairLock);
NSLog(@"线程1 解锁成功");
} else { // 会造成优先级反转,不建议使用
__block OSSpinLock oslock = OS_SPINLOCK_INIT;

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSLog(@"线程2 befor lock");
OSSpinLockLock(&oslock);
NSLog(@"线程2执行");
sleep(3);
OSSpinLockUnlock(&oslock);
NSLog(@"线程2 unlock");
});

//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"线程1 befor lock");
OSSpinLockLock(&oslock);
NSLog(@"线程1 sleep");
sleep(3);
NSLog(@"线程1执行");
OSSpinLockUnlock(&oslock);
NSLog(@"线程1 unlock");
});

// 可以看出不同的队列优先级,执行的顺序不同,优先级越高,越早被执行
}
}

读写锁:


上文有说到,读写锁又称共享-互斥锁


1. pthread_rwlock:

pthread_rwlock经常用于文件等数据的读写操作,需要导入头文件#import

iOS中的读写安全方案需要注意一下场景



  • 同一时间,只能有1个线程进行写的操作

  • 同一时间,允许有多个线程进行读的操作

  • 同一时间,不允许既有写的操作,又有读的操作
//初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);

//读加锁
pthread_rwlock_rdlock(&_lock);
//读尝试加锁
pthread_rwlock_trywrlock(&_lock)

//写加锁
pthread_rwlock_wrlock(&_lock);
//写尝试加锁
pthread_rwlock_trywrlock(&_lock)

//解锁
pthread_rwlock_unlock(&_lock);
//销毁
pthread_rwlock_destroy(&_lock);

用法:实现多读单写

#import 
@interface pthread_rwlockDemo ()
@property (assign, nonatomic) pthread_rwlock_t lock;
@end

@implementation pthread_rwlockDemo

- (instancetype)init
{
self = [super init];
if (self) {
// 初始化锁
pthread_rwlock_init(&_lock, NULL);
}
return self;
}

- (void)otherTest{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
[self read];
});
dispatch_async(queue, ^{
[self write];
});
}
}
- (void)read {
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)write
{
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)dealloc
{
pthread_rwlock_destroy(&_lock);
}
@end

递归锁:
递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。

1. pthread_mutex(recursive):
pthread_mutex_t锁是默认是非递归的。可以通过设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t锁设置为递归锁。

pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);

2. NSRecursiveLock:
NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致

#import "RecursiveLockDemo.h"
@interface RecursiveLockDemo()
@property (nonatomic,strong) NSRecursiveLock *ticketLock;
@end
@implementation RecursiveLockDemo
//卖票
- (void)sellingTickets{
[self.ticketLock lock];
[super sellingTickets];
[self.ticketLock unlock];
}
@end

条件锁:
1. NSCondition:
定义:

@interface NSCondition : NSObject  {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

遵循NSLocking协议,使用的时候同样是lock,unlock加解锁,wait是傻等,waitUntilDate:方法是等一会,都会阻塞掉线程,signal是唤起一个在等待的线程,broadcast是广播全部唤起。

NSCondition *lock = [[NSCondition alloc] init];
//Son 线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
while (No Money) {
[lock wait];
}
NSLog(@"The money has been used up.");
[lock unlock];
});

//Father线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"Work hard to make money.");
[lock signal];
[lock unlock];
});

2.NSConditionLock:
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
定义:

@interface NSConditionLock : NSObject  {
@private
void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition; //
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

用法 :

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end
@implementation NSConditionLockDemo
- (instancetype)init
{
if (self = [super init]) {
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}
return self;
}

- (void)otherTest
{
[[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
}

- (void)__one
{
[self.conditionLock lock];
NSLog(@"__one");
sleep(1);
[self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
[self.conditionLock unlockWithCondition:3];
}
@end

里面有三个常用的方法

* 1、- (instancetype)initWithCondition:(NSInteger)condition;  //初始化Condition,并且设置状态值
* 2、- (void)lockWhenCondition:(NSInteger)condition; //当状态值为condition的时候加锁
* 3、- (void)unlockWithCondition:(NSInteger)condition; //当状态值为condition的时候解锁

信号量 dispatch_semaphore:
在加锁的过程中,如过线程 1 已经获取了锁,并在执行任务过程中,那么其他线程会被阻塞,直到线程 1 任务结束后完成释放锁。

实现原理 :
信号量的 wait 最终调用到这里

int sem_wait (sem_t *sem) {  
int *futex = (int *) sem;
if (atomic_decrement_if_positive (futex) > 0)
return 0;
int err = lll_futex_wait (futex, 0);
return -1;
)

首先把信号值减一,并判断是否大于 0,如过大于 0 说明不用等待,立即返回。
否则线程进入睡眠主动让出时间片,让出时间片会导致操作系统切换到另一个线程,这种上下文的切换也耗时,大概 10 微妙,而且还要切回来,如过等待时间很短,那么等待耗时还没有切换耗时长,很不划算。

自旋锁和信号量的实现简单,所以加锁和解锁的效率高

总结
其实本文写的都是一些再基础不过的内容,在平时阅读一些开源项目的时候经常会遇到一些保持线程同步的方式,因为场景不同可能选型不同,这篇就做一下简单的记录吧~我相信读完这篇你应该能根据不同场景选择合适的锁了吧、能够道出自旋锁和互斥锁的区别了吧。

性能排序:
1、os_unfair_lock
2、OSSpinLock
3、dispatch_semaphore
4、pthread_mutex
5、dispatch_queue(DISPATCH_QUEUE_SERIAL)
6、NSLock
7、NSCondition
8、pthread_mutex(recursive)
9、NSRecursiveLock
10、NSConditionLock
11、@synchronized

转自:https://www.jianshu.com/p/eaab05cf0e1c

收起阅读 »

常用开发加密方法

前言相信大家在开发中都遇到过,有些隐秘信息需要做加密传输的场景.A:你就把 XXX 做一下base64加密传过来就行这些问题相信大家都遇到过,那么在实际开发中我们应该如何选择加密方法呢?加密这里我就直接抛出来几个加密规则AES 对称加密,双方只有同一个秘钥ke...
继续阅读 »

前言
相信大家在开发中都遇到过,有些隐秘信息需要做加密传输的场景.
A:你就把 XXX 做一下base64加密传过来就行

这些问题相信大家都遇到过,那么在实际开发中我们应该如何选择加密方法呢?


加密
这里我就直接抛出来几个加密规则

  • AES 对称加密,双方只有同一个秘钥key

  • RSA 非对称加密,生成一对公私钥.

首先要明确一点, 即使做了加密也不能保证我们的信息就是绝对安全的,只是尽可能的提升破解难度,加密算法的实现都是公开的,所以秘钥如何安全的存储是我们要重点考虑的问题.

关于这两种加密算法大家可以网上查一下原理,这里我不介绍原理,只介绍给大家特定场景下如何选择最优的加密规则,以及一些小Tips.

AES
对称加密,很好理解,生成唯一秘钥key,双方本别可以用key做加密/解密.是比较常用的加密首段,AES只是一种加密规则,具体的加密还有很多种,目前主流使用的是AES/GCM.

RSA
非对称加密,生成一对秘钥,public key/private key,
加解密使用时: public key加密, private key解密.
签名验证时 : private key签名 , public key 验签

这里说一下实际案例:

某某公司,2B的后台支付接口,突然有一天一个商家反馈为什么我账户里钱都没有了,通过日志一查发现都是正常操作刷走了.而某公司并没有办法证明自己的系统是没问题的.理论上这个接口的key下发给商户,但是某某公司也是有这个key的,所以到底是谁泄漏了key又是谁刷走了账户里的钱,谁也无法证明.

这里我们要想一个问题,我们要怎么做才能防止出现此类问题后,商户过来说不是我刷的钱,寻求赔偿的时候, 拿出证据打发他们?

这个问题就可以利用RSA来解决,在接入公司生成APP key 要求接入方自己生成一对RSA秘钥,然后讲 public key上传给我们, private key由接入方自己保存, 而我们只需要验证订单中的签名是否是由private key签名的,而非其他阿猫阿狗签名的订单. 如果出现了上诉问题,那么说明接入方的private key泄漏与我们无关,这样我们就能防止接入方抵赖.

完整性校验.防串改

很多情况下我们需要对数据的完整性做校验, 比如对方发过来一个文件, 我们怎么知道这个问题件就是源文件, 而非被别人恶意拦截串改后的问题?

早些年大家下载程序的时候应该会看到,当前文件的md5值是XXXXX,这个就是为了防止文件被修改的存在的.早期我们都是用md5/sha1来做完整性校验,后来由sha1升级出现了sha256.大家可能不知道应该如何选择.

下面是一个经典故事
Google之前公开过两个不同的PDF,而它们拥有相同的sha1值


两个不同的文件拥有相同sha1值,这意味着我们本地使用的程序sha1是源文件非串改后的,但实际上可能早已偷梁换柱.这是很可怕的.
所以推荐大家在用完整性校验时要使用sha256,会更安全些.

转自:https://www.jianshu.com/p/fa85cbe1027b

收起阅读 »

iOS 13:更多系统APP和组件采用Swift编写

苹果在 2014 年 WWDC 发布了全新 Swift 编程语言,Swift 是苹果平台未来的编程语言。自那以后,很多第三方开发者开始使用 Swift 编写程序,不过苹果 iOS 和 macOS 系统,以及各种系统应用还是采用 Objective-C 编写。这...
继续阅读 »

苹果在 2014 年 WWDC 发布了全新 Swift 编程语言,Swift 是苹果平台未来的编程语言。自那以后,很多第三方开发者开始使用 Swift 编写程序,不过苹果 iOS 和 macOS 系统,以及各种系统应用还是采用 Objective-C 编写。

这种情况存在很多原因,首先,苹果目前大量的 Objective-C 代码工作的很完美,没有必要为了重写而重写,没有问题就不要创造新的问题。其次,直到 Swift 5.0,ABI 才稳定,Swift 5.1,模块稳定,对于在系统级别大规模部署很重要。

自 iOS 9 之后,开发者 Alexandre Colucci 一直在统计苹果系统中 Swift 的使用情况。最新的数据显示,在 iOS 13 中,一共有 141 个使用 Swift 编写的二进制可执行文件,是 iOS 12 的两倍多,iOS 12 中有 66 个。


iOS 13 中,Sidecar 副屏、查找和提醒事项等新功能、新应用都采用 Swift 编写,其他使用 Swift 的 app 包括健康、Books 电子书以及一些系统服务,负责 AirPods 和 HomePod 配对的服务,以及查找 App 的离线查找功能等。

转自:https://www.jianshu.com/p/1227b27fcb2c

收起阅读 »

CoreSimulator与Xcode两个文件夹造成Mac中多了100G的“其他”空间

tips 没有购买cleanMyMac的同学,不要担心,我既然写了文章,肯定是不为了让同学们花钱购买软件的。CoreSimulator与Xcode两个文件夹造成Mac中多了100G的“其他”空间;请原谅我表述的不太明白,还是上图吧:1.清理之前mac电脑只剩下...
继续阅读 »

tips 没有购买cleanMyMac的同学,不要担心,我既然写了文章,肯定是不为了让同学们花钱购买软件的。
CoreSimulator与Xcode两个文件夹造成Mac中多了100G的“其他”空间;
请原谅我表述的不太明白,还是上图吧:

1.清理之前mac电脑只剩下了23.4GB的存储空间可用,“其他”这一项目占了200多GB


2.清理了Xcode文件夹中的部分文件


3.清理了CoreSimulator文件夹中的部分文件


使用Clean My Mac 版本4.0.4 中的卸载功能,查看(下边的是我清理之后,清理之前,Xcode占用129GB,现在是37.7GB)


我们通过下图可以得知,Xcode.app本身才7.8个G,可是下边的两个文件夹(清理之前占了120G左右)清理之后还占了30GB左右。


没有购买cleanMyMac的同学,不要担心,我既然写了文章,肯定是不为了让同学们花钱购买软件的。

1.请在电脑上 点击 “前往文件夹”功能,(快捷键:com + shift + g)
Xcode 文件目录:(不要全部删除整个文件,我们去选择删除DeviceSupport里边老旧的版本就好了)

~/Library/Developer/Xcode/iOS\ DeviceSupport

CoreSimulator 文件目录:(不要全部删除整个文件,我们去选择删除Devices里的一些文件就好了)

~/Library/Developer/CoreSimulator/Devices
当然也可以在终端输入  
open ~/Library/Developer/Xcode/iOS\ DeviceSupport

还有

open ~/Library/Developer/CoreSimulator/Devices

也是一样的;

我把12.4以下的都删除了。


想了解这些文件是什么的,可以参照这篇文章 iOS开发-Xcode清理系统内存占用过多的方法



↓ ↓ ↓ ↓ ↓2020年01月08日添加 ↓ ↓ ↓ ↓

感谢评论区的建议。按照建议,亲测后感觉留言里提到ncdu方便快捷,有可取之处。如果有喜欢了解的小伙伴,可以参考这篇文章一个查看MAC硬盘占用的小工具ncdu



转自:https://www.jianshu.com/p/48d8e6870a7c

收起阅读 »

iOS websocket接入

接触WebSocket最近公司的项目中有一个功能 需要服务器主动推数据到APP。考虑到普通的HTTP 通信方式只能由客户端主动拉取,服务器不能主动推给客户端 。然后就想出的2种解决方案。1.和后台沟通了一下 他们那里使用的是WebSocket ,所以就使用We...
继续阅读 »

接触WebSocket

最近公司的项目中有一个功能 需要服务器主动推数据到APP。
考虑到普通的HTTP 通信方式只能由客户端主动拉取,服务器不能主动推给客户端 。然后就想出的2种解决方案。

1.和后台沟通了一下 他们那里使用的是WebSocket ,所以就使用WebSocket让我们app端和服务器建立长连接。这样就可以事实接受他发过来的消息
2.使用推送,也可以实现接收后台发过来的一些消息

最后还是选择了WebSocket,找到了facebook的 SocketRocket 框架。下面是接入过程中的一些记录

WebSocket

WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:

WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;

WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。

具体在这儿 WebSocket 是什么原理为什么可以实现持久连接?

用法
我使用的是pod管理库 所以在podfile中加入
pod 'SocketRocket'

在使用命令行工具cd到当前工程 安装
pod install

如果是copy的工程中的 SocketRocket库的github地址:SocketRocket

导入库到工程中以后首先封装一个SocketRocketUtility单例

SocketRocketUtility.m文件中的写法如下:

#import "SocketRocketUtility.h"
#import <SocketRocket.h>

NSString * const kNeedPayOrderNote = @"kNeedPayOrderNote";//发送的通知名称

@interface SocketRocketUtility()<SRWebSocketDelegate>
{
int _index;
NSTimer * heartBeat;
NSTimeInterval reConnectTime;
}

@property (nonatomic,strong) SRWebSocket *socket;

@end

@implementation SocketRocketUtility

+ (SocketRocketUtility *)instance {
static SocketRocketUtility *Instance = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
Instance = [[SocketRocketUtility alloc] init];
});
return Instance;
}

//开启连接
-(void)SRWebSocketOpenWithURLString:(NSString *)urlString {
if (self.socket) {
return;
}

if (!urlString) {
return;
}

//SRWebSocketUrlString 就是websocket的地址 写入自己后台的地址
self.socket = [[SRWebSocket alloc] initWithURLRequest:
[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]];

self.socket.delegate = self; //SRWebSocketDelegate 协议

[self.socket open]; //开始连接
}

//关闭连接
- (void)SRWebSocketClose {
if (self.socket){
[self.socket close];
self.socket = nil;
//断开连接时销毁心跳
[self destoryHeartBeat];
}
}

#pragma mark - socket delegate
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
NSLog(@"连接成功,可以与服务器交流了,同时需要开启心跳");
//每次正常连接的时候清零重连时间
reConnectTime = 0;
//开启心跳 心跳是发送pong的消息 我这里根据后台的要求发送data给后台
[self initHeartBeat];
[[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketDidOpenNote object:nil];
}

- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
NSLog(@"连接失败,这里可以实现掉线自动重连,要注意以下几点");
NSLog(@"1.判断当前网络环境,如果断网了就不要连了,等待网络到来,在发起重连");
NSLog(@"2.判断调用层是否需要连接,例如用户都没在聊天界面,连接上去浪费流量");
NSLog(@"3.连接次数限制,如果连接失败了,重试10次左右就可以了,不然就死循环了。)";
_socket = nil;
//连接失败就重连
[self reConnect];
}

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
NSLog(@"被关闭连接,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);
//断开连接 同时销毁心跳
[self SRWebSocketClose];
}

/*
该函数是接收服务器发送的pong消息,其中最后一个是接受pong消息的,
在这里就要提一下心跳包,一般情况下建立长连接都会建立一个心跳包,
用于每隔一段时间通知一次服务端,客户端还是在线,这个心跳包其实就是一个ping消息,
我的理解就是建立一个定时器,每隔十秒或者十五秒向服务端发送一个ping消息,这个消息可是是空的
*/
-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload{

NSString *reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
NSLog(@"reply===%@",reply);
}

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
//收到服务器发过来的数据 这里的数据可以和后台约定一个格式 我约定的就是一个字符串 收到以后发送通知到外层 根据类型 实现不同的操作
NSLog(@"%@",message);

[[NSNotificationCenter defaultCenter] postNotificationName:kNeedPayOrderNote object:message];
}

#pragma mark - methods
//重连机制
- (void)reConnect
{
[self SRWebSocketClose];
//超过一分钟就不再重连 所以只会重连5次 2^5 = 64
if (reConnectTime > 64) {
return;
}

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.socket = nil;
[self SRWebSocketOpen];
NSLog(@"重连");
});

//重连时间2的指数级增长
if (reConnectTime == 0) {
reConnectTime = 2;
}else{
reConnectTime *= 2;
}
}

//初始化心跳
- (void)initHeartBeat
{
dispatch_main_async_safe(^{
[self destoryHeartBeat];
__weak typeof(self) weakSelf = self;
//心跳设置为3分钟,NAT超时一般为5分钟
heartBeat = [NSTimer scheduledTimerWithTimeInterval:3*60 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"heart");
//和服务端约定好发送什么作为心跳标识,尽可能的减小心跳包大小
[weakSelf sendData:@"heart"];
}];
[[NSRunLoop currentRunLoop]addTimer:heartBeat forMode:NSRunLoopCommonModes];
})
}

//取消心跳
- (void)destoryHeartBeat
{
dispatch_main_async_safe(^{
if (heartBeat) {
[heartBeat invalidate];
heartBeat = nil;
}
})
}

//pingPong机制
- (void)ping{
[self.socket sendPing:nil];
}

#define WeakSelf(ws) __weak __typeof(&*self)weakSelf = self
- (void)sendData:(id)data {

WeakSelf(ws);
dispatch_queue_t queue = dispatch_queue_create("zy", NULL);

dispatch_async(queue, ^{
if (weakSelf.socket != nil) {
// 只有 SR_OPEN 开启状态才能调 send 方法,不然要崩
if (weakSelf.socket.readyState == SR_OPEN) {
[weakSelf.socket send:data]; // 发送数据

} else if (weakSelf.socket.readyState == SR_CONNECTING) {
NSLog(@"正在连接中,重连后其他方法会去自动同步数据");
// 每隔2秒检测一次 socket.readyState 状态,检测 10 次左右
// 只要有一次状态是 SR_OPEN 的就调用 [ws.socket send:data] 发送数据
// 如果 10 次都还是没连上的,那这个发送请求就丢失了,这种情况是服务器的问题了,小概率的
[self reConnect];

} else if (weakSelf.socket.readyState == SR_CLOSING || weakSelf.socket.readyState == SR_CLOSED) {
// websocket 断开了,调用 reConnect 方法重连
[self reConnect];
}
} else {
NSLog(@"没网络,发送失败,一旦断网 socket 会被我设置 nil 的");
}
});
}

-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

然后在需要开启socket的地方调用
[[SocketRocketUtility instance] SRWebSocketOpenWithURLString:@"写入自己后台的地址"];
在需要断开连接的时候调用
[[SocketRocketUtility instance] SRWebSocketClose];

使用这个框架最后一个很重要的 需要注意的一点

这个框架给我们封装的webscoket在调用它的sendPing senddata方法之前,一定要判断当前scoket是否连接,如果不是连接状态,程序则会crash。

结语
这里简单的实现了连接和收发数据 后续看项目需求在加上后续的改进 希望能够帮助第一次写的iOSer 。 希望有更好的方法的童鞋可以有进一步的交流 : )

4月10日 更新:

/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

上面发送心跳包的方法是iOS10才可以用的 其他版本会崩溃 要适配版本 要选择 这个方法

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

8月10日 更新demo地址
demo地址
可以下载下来看看哦 :)


demo中的后台地址未设置 所以很多同学直接运行就报错了 设置一个自己后台的地址就ok了 :)

转自:https://www.jianshu.com/p/821b777555d3

收起阅读 »

iOS 用symbolicatecrash符号化崩溃日志中系统库方法堆栈

说明现在已经有很多第三方平台支持解析crash日志中的系统方法了,比如bugly。但是万一遇到情况特殊或者公司要求,还是走上传崩溃日志到自己的服务器,然后自己去定期解析的话,就需要用到symbolicatecrash这个工具了。指令操作均在终端中进行。另外,每...
继续阅读 »

说明
现在已经有很多第三方平台支持解析crash日志中的系统方法了,比如bugly。但是万一遇到情况特殊或者公司要求,还是走上传崩溃日志到自己的服务器,然后自己去定期解析的话,就需要用到symbolicatecrash这个工具了。

指令操作均在终端中进行。
另外,每次打包上架提交审核的时候,把对应的.xcarchive与ipa文件一同拷贝一份,按照版本号保存下来是个好习惯。

1.前期准备工作
前期准备工作只需要在第一次尝试解析的时候进行,如果可以成功执行最终的命令行解析日志就不需要重复执行。

  • 确定Xcode路径,执行如下指令

xcode-select --print-path

目的:确保Xcode路径存在。如果路径中有空格的存在,请把空格去掉。比如如果Xcode 的名字是“Xcode 9.2”请修改成“Xcode9.2”或者“Xcode”。否则后面你会遇到很多稀奇古怪的错误。
修改方法:应用程序→Xcode→重命名

  • 添加Xcode路径
    如果Xcode路径已经存在,或者不需要修改,请跳过这一步。注意如果改过Xcode应用的名字也需要进行这一步操作
    执行如下指令

sudo xcode-select -s 路径

路径部分直接把Xcode应用内Developer文件夹拖拽进去会自动生成。
Developer文件夹:应用程序→Xcode→右键,显示包内容→Contents文件夹→Developer

  • 确定Xcode command line tools是否安装
    执行如下指令

xcode-select --install

如果输出以下内容说明已经安装,否则根据提示安装即可。

xcode-select: error: command line tools are already installed, use "Software Update" to install updates

2.解析准备工作
解析所需文件

解析崩溃日志需要三个文件

①.崩溃日志文件(通常为.crash如果服务器上面是.txt也没关系,直接下下来把尾缀改成.crash就行)
②.产生崩溃日志的app包对应的.dSYM符号表(注意符号表和包一定要匹配。否则,堆栈方法会错乱)
③.崩溃分析工具symbolicatecrash(Xcode自带)

.dSYM符号表的获取:Xcode→window→organizer 选择Archives→选择想要解析崩溃日志的App包→右键,show in finder→右键(.xcarchive),显示包内容→dSYMs→xxx.app.dSYM
如果自己这里没有app打包文件就只有跟打包的同事要。

symbolicatecrash的获取:应用程序(Applications)→Xcode→右键,显示包内容→Contents→SharedFrameworks→DVTFoundation.framework→Versions→A→Resources→symbolicatecrash

tips:如果到了DVTFoundation.framework这里打不开下一步了,选择如下浏览方式即可。


3.解析日志

<1>将上述三个文件放在一个文件夹内
文件夹名称可以任意起,路径随意但最好不要出现中文。


<2>在终端中进入该文件夹内
直接拖拽文件夹到路径部分会自动生成

cd 路径

<3>解析日志

./symbolicatecrash ./*.crash ./*.app.dSYM>symbol.crash

这个方法一次只能解析一个日志文件,然后输出一个解析过后的symbol.crash日志文件(会覆盖之前存在的symbol.crash),这个输出的日志文件就是我们可以直接阅读的日志文件。symbol部分可以任意修改成其他名字。

如果要解析多个日志文件,需要逐一将文件夹内的日志文件替换。或者将所有需要解析的日志文件全部放在文件夹内,但是每次指定需要解析的.crash文件。

如果出现下面类似的错误,报错无法执行

Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line xx(数字).

执行指令

export DEVELOPER_DIR=Xcode Developer文件夹路径

像上面一样把Developer文件夹拖拽到等号后面路径部分就行,然后再执行解析指令就不会报错了。

<4>查看解析结果


<5>给Xcode添加对应固件的符号文件
①.下载对应固件符号文件
这个需要结合崩溃日志的信息来,比如这里日志中提到崩溃发生的固件是8.3(12F70)我们就要去找这个固件的符号文件,找的时候还要注意是否区分了CPU架构。下载地址放在后面

②.下载完成后添加进Xcode
打开Finder:点击菜单前往→前往文件夹→输入
~/Library/Developer/Xcode/iOS DeviceSupport→前往

将下载好的符号文件放入定位到的路径里面。


③.再次解析日志文件


<6>固件符号文件下载地址
首先感谢iOS Crash分析必备:符号化系统库方法作者的无私分享。该文章的作者收集了几乎所有固件的符号文件并分享了出来,为了尊重原作者这里就不放下载地址了。大家可以在他的文章当中找到下载地址,以及目前收集了哪些固件符号文件。

转自:https://www.jianshu.com/p/21532aef2811

收起阅读 »

关于WKWebView的post请求丢失body问题的解决方案

WKWebView的优点这里不做过多介绍,主要说一下最近解决WKWebView的post请求丢失body问题的解决方案。WKWebView 通过loadrequest方法加载Post请求会丢失请求体(body)中的内容,进而导致服务器拿不到body中的内容的问...
继续阅读 »

WKWebView的优点这里不做过多介绍,主要说一下最近解决WKWebView的post请求丢失body问题的解决方案。
WKWebView 通过loadrequest方法加载Post请求会丢失请求体(body)中的内容,进而导致服务器拿不到body中的内容的问题的发生。这个问题的产生主要是因为WKWebView的网络请求的进程与APP不是同一个进程,所以网络请求的过程是这样的:
由APP所在的进程发起request,然后通过IPC通信(进程间通信)将请求的相关信息(请求头、请求行、请求体等)传递给webkit网络线进程接收包装,进行数据的HTTP请求,最终再进行IPC的通信回传给APP所在的进程的。这里如果发起的request请求是post请求的话,由于要进行IPC数据传递,传递的请求体body中根据系统调度,将其舍弃,最终在WKWebView网络进程接受的时候请求体body中的内容变成了空,导致此种情况下的服务器获取不到请求体,导致问题的产生。
为了能够获取POST方法请求之后的body内容,这两天整理了一些解决方案,大致分为三种:

  1. 将网络请求交由Js发起,绕开系统WKWebView的网络的进程请求达到正常请求的目的

  2. 改变POST请求的方法为GET方法(有风险,不一定服务器会接受GET方法)

  3. 将Post请求的请求body内容放入请求的Header中,并通过URLProtocol拦截自定义协议,在拦截中通过NSConnection进行重新请求(重新包装请求body),然后通过回调Client客户端来传递数据内容

三种方法中,我采用了第三种方案,这里说一下第三种方案的实现方式,大致分为三步:

  1. 注册拦截的自定义的scheme

  2. 重写loadRequest()方法,根据request的method方法是否为POST进行URL的拦截替换

  3. 在URLProtocol中进行request的重新包装(获取请求的body内容),使用NSURLConnection进行HTTP请求并将数据回传

这里说明一下为什么要自己去注册自定义的scheme,而不是直接拦截https/http。主要原因是:如果注册了https/http的拦截,那么所有的http(s)请求都会交由系统进程处理,那么此时系统进程会通过IPC的形式传递给实现URLProctol协议的类去处理,在通过IPC传递的过程中丢失body体(上面有讲到),所以在拦截的时候是拿不到POST方法的请求体body的。然而并不是所有的http请求都会走loadrequest()方法(比如js中的ajax请求),所以导致一些POST请求没有被包装(将请求体body内容放到请求头header)就被拦截了,进而丢失请求体body内容,问题一样会产生。所以为了避免这样的问题,我们需要自己去定一个scheme协议,保证不过度拦截并且能够处理我们需要处理的POST请求内容。

以下是具体的实现方式:

  • 注册拦截的自定义的scheme

[NSURLProtocol registerClass:NSClassFromString(@“GCURLProtocol")];
[NSURLProtocol wk_registerScheme:@"gc"];
[NSURLProtocol wk_registerScheme:WkCustomHttp];
[NSURLProtocol wk_registerScheme:WkCustomHttps];
  • 重写loadRequest()方法,根据request的method方法是否为POST进行URL的拦截替换

//包装请求头内容
- (WKNavigation *)loadRequest:(NSURLRequest *)request{
NSLog(@"发起请求:%@ method:%@",request.URL.absoluteString,request.HTTPMethod);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
NSMutableDictionary *requestHeaders = [request.allHTTPHeaderFields mutableCopy];
//判断是否是POST请求,POST请求需要包装request中的body内容到请求头中(会有丢失body问题的产生)
//,包装完成之后重定向到拦截的协议中自己包装处理请求数据内容,拦截协议是GCURLProtocol,请自行搜索
if ([mutableRequest.HTTPMethod isEqualToString:@"POST"] && ([mutableRequest.URL.scheme isEqualToString:@"http"] || [mutableRequest.URL.scheme isEqualToString:@"https"])) {
NSString *absoluteStr = mutableRequest.URL.absoluteString;
if ([[absoluteStr substringWithRange:NSMakeRange(absoluteStr.length-1, 1)] isEqualToString:@"/"]) {
absoluteStr = [absoluteStr stringByReplacingCharactersInRange:NSMakeRange(absoluteStr.length-1, 1) withString:@""];
}

if ([mutableRequest.URL.scheme isEqualToString:@"https"]) {
absoluteStr = [absoluteStr stringByReplacingOccurrencesOfString:@"https" withString:WkCustomHttps];
}else{
absoluteStr = [absoluteStr stringByReplacingOccurrencesOfString:@"http" withString:WkCustomHttp];
}

mutableRequest.URL = [NSURL URLWithString:absoluteStr];
NSString *bodyDataStr = [[NSString alloc]initWithData:mutableRequest.HTTPBody encoding:NSUTF8StringEncoding];
[requestHeaders addEntriesFromDictionary:@{@"httpbody":bodyDataStr}];
mutableRequest.allHTTPHeaderFields = requestHeaders;

NSLog(@"当前请求为POST请求Header:%@",mutableRequest.allHTTPHeaderFields);

}
return [super loadRequest:mutableRequest];
}
  • 在URLProtocol中进行request的重新包装(获取请求的body内容),使用NSURLConnection进行HTTP请求并将数据回传(以下是主要代码)

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {

NSString *scheme = request.URL.scheme;

if ([scheme isEqualToString:InterceptionSchemeKey]){

if ([self propertyForKey:HaveDealRequest inRequest:request]) {
NSLog(@"已经处理,放行");
return NO;
}
return YES;
}

if ([scheme isEqualToString:WkCustomHttp]){

if ([self propertyForKey:HaveDealWkHttpPostBody inRequest:request]) {
NSLog(@"已经处理,放行");
return NO;
}
return YES;
}

if ([scheme isEqualToString:WkCustomHttps]){

if ([self propertyForKey:HaveDealWkHttpsPostBody inRequest:request]) {
NSLog(@"已经处理,放行");
return NO;
}
return YES;
}

return NO;

}
- (void)startLoading {

//截获 gc 链接的所有请求,替换成本地资源或者线上资源
if ([self.request.URL.scheme isEqualToString:InterceptionSchemeKey]) {
[self htmlCacheRequstLoad];
}

else if ([self.request.URL.scheme isEqualToString:WkCustomHttp] || [self.request.URL.scheme isEqualToString:WkCustomHttps]){
[self postBodyAddLoad];
}
else{
NSMutableURLRequest *newRequest = [self cloneRequest:self.request];
NSString *urlString = newRequest.URL.absoluteString;
[self addHttpPostBody:newRequest];
[NSURLProtocol setProperty:@YES forKey:GCProtocolKey inRequest:newRequest];
[self sendRequest:newRequest];
}


}

- (void)addHttpPostBody:(NSMutableURLRequest *)redirectRequest{

//判断当前的请求是否是Post请求
if ([self.request.HTTPMethod isEqualToString:@"POST"]) {
NSLog(@"post请求");
NSMutableDictionary *headerDict = [redirectRequest.allHTTPHeaderFields mutableCopy];
NSString *body = headerDict[@"httpbody"]?:@"";
if (body.length) {
redirectRequest.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
NSLog(@"body:%@",body);
}
}
}
- (void)postBodyAddLoad{

NSMutableURLRequest *cloneRequest = [self cloneRequest:self.request];
if ([cloneRequest.URL.scheme isEqualToString:WkCustomHttps]) {
cloneRequest.URL = [NSURL URLWithString:[cloneRequest.URL.absoluteString stringByReplacingOccurrencesOfString:WkCustomHttps withString:@"https"]];
[NSURLProtocol setProperty:@YES forKey:HaveDealWkHttpsPostBody inRequest:cloneRequest];
}else if ([cloneRequest.URL.scheme isEqualToString:WkCustomHttp]){

cloneRequest.URL = [NSURL URLWithString:[cloneRequest.URL.absoluteString stringByReplacingOccurrencesOfString:WkCustomHttp withString:@"http"]];
[NSURLProtocol setProperty:@YES forKey:HaveDealWkHttpPostBody inRequest:cloneRequest];
}
//添加body内容
[self addHttpPostBody:cloneRequest];
NSLog(@"请求body添加完成:%@",[[NSString alloc]initWithData:cloneRequest.HTTPBody encoding:NSUTF8StringEncoding]);
[self sendRequest:cloneRequest];

}
//复制Request对象
- (NSMutableURLRequest *)cloneRequest:(NSURLRequest *)request
{
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:request.URL cachePolicy:request.cachePolicy timeoutInterval:request.timeoutInterval];

newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields;
[newRequest setValue:@"image/webp,image/*;q=0.8" forHTTPHeaderField:@"Accept"];

if (request.HTTPMethod) {
newRequest.HTTPMethod = request.HTTPMethod;
}

if (request.HTTPBodyStream) {
newRequest.HTTPBodyStream = request.HTTPBodyStream;
}

if (request.HTTPBody) {
newRequest.HTTPBody = request.HTTPBody;
}

newRequest.HTTPShouldUsePipelining = request.HTTPShouldUsePipelining;
newRequest.mainDocumentURL = request.mainDocumentURL;
newRequest.networkServiceType = request.networkServiceType;

return newRequest;
}

#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
/**
* 收到服务器响应
*/
NSURLResponse *returnResponse = response;
[self.client URLProtocol:self didReceiveResponse:returnResponse cacheStoragePolicy:NSURLCacheStorageAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
/**
* 接收数据
*/
if (!self.recData) {
self.recData = [NSMutableData new];
}
if (data) {
[self.recData appendData:data];
}
}
- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response
{
/**
* 重定向
*/
if (response) {
[self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self.client URLProtocolDidFinishLoading:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
/**
* 加载失败
*/
[self.client URLProtocol:self didFailWithError:error];
}

转自:https://www.jianshu.com/p/4dfc80ca7db2

收起阅读 »

iOS - 同一个workspace下创建多个项目编程

在iOS开发中,相关联的多个项目可能会放在同一个workspace下进行开发,那习惯了一个项目在一个工作空间下的同学该怎么快速开撸呢?只需要三步而已!第一步,先用Xcode在目标目录下创建一个workspace文件。见图说话。第二步,用Xcode打开works...
继续阅读 »

在iOS开发中,相关联的多个项目可能会放在同一个workspace下进行开发,那习惯了一个项目在一个工作空间下的同学该怎么快速开撸呢?

只需要三步而已!


第一步,先用Xcode在目标目录下创建一个workspace文件。见图说话。


第二步,用Xcode打开workspace文件,然后在该workspace下创建多个Project文件。


在创建工程的过程中有个主意点:将新建Project添加的目标和组 都是workspace。如图:


第三步,多个工程间文件互相引用问题:多个工程间的文件引用方法:在工程A的Setting选项下的Header Search Paths 下添加“$(SRCROOT)/../B”,这个工程A中即可引用工程B的文件,不过导入文件的方式是:#import <Person.m>


如上设置,多个工程间的类就可以共享使用了。



收起阅读 »

uniapp实现$router

作为 Vue 重度用户,在使用 uni-app 过程中不可避免的把 Vue 开发习惯带了过去。无论是项目目录结构,还是命名风格,甚至我还封装了一些库,如 https://zhuanlan.zhihu.com/p/141451626 提到的 ...
继续阅读 »

作为 Vue 重度用户,在使用 uni-app 过程中不可避免的把 Vue 开发习惯带了过去。无论是项目目录结构,还是命名风格,甚至我还封装了一些库,如 https://zhuanlan.zhihu.com/p/141451626 提到的 _request 等。

众所周知,用 Vue 开发项目,其实就是用的 Vue 全家桶。即 Vue + Vuex + VueRouter 。在代码里的体现就是:

this + this.$store + this.$router/$route

然而由于 uni-app 为了保证跨端同时简化语法,用的是微信小程序那套 API。其中就包括路由系统。因为在 uni-app 中,没有 $router/$route。只有 uni[‘路由方法’]。讲真的,这样做确实很容易上手,但同时也是有许多问题:

  1. 路由传参数只支持字符串,对象参数需要手动JSON序列化
  2. 传参有长度限制
  3. 传参不支持特殊符号如 url
  4. 不支持路由拦截和监听

因此,需要一个工具来将现有的路由使用方式变为 vue-router 的语法,并且完美解决以上几个问题。

vue-router 的语法这里不再赘述。简单的来说就是将路由的用法由:

uni.navigateTo({
url: `../login/login?data=${JSON.stringify({ from: 'index', time:Date.now() })}`
})

变成:

this.$router.push('/login', {
data: {
from: 'index',
time: Date.now()
}
})

同时传参通过一个 $route 对象。因此我们的需求就是事现一个 $router 和 $route 对象。并给定相应方法。比如调用:

push('/login')

其实就是执行了:

uni.navigateTo({ url:`../login/login ` })

实现起来非常简单:

push 方法接收到 '/login' 将其拼接为 `../login/login` 后调用 uni.navigateTo 就可以。

然而这样并不严谨。此时的 push 方法只能在页面内使用。而不能在 pages 文件夹以外的地方使用,因为这里用的是相对路径。只要改成 `pages/login/login` 就好。

$route 的实现就是在路由发生变化时,动态改变一个公共对象 route 的内部值。

而通过全局 mixin onShow 方法,可以实现对路由变化动态监听。

通过 require.context 预引入路由列表实现更好的错误提示。

最后通过一个页面堆栈数据列表实现 route 实时更新。

最后的代码:

import Vue from 'vue'

export const route = { // 当前路由对象所在的 path 等信息。默认为首页
fullPath: '/pages/index/index',
path: '/index',
type: 'push',
query: {}
}

let onchange = () => {} // 路由变化监听函数
const _$UNI_ACTIVED_PAGE_ROUTES = [] // 页面数据缓存
let _$UNI_ROUTER_PUSH_POP_FUN = () => {} // pushPop resolve 函数
const _c = obj => JSON.parse(JSON.stringify(obj)) // 简易克隆方法
const modulesFiles = require.context('@/pages', true, /\.vue$/) // pages 文件夹下所有的 .vue 文件

Vue.mixin({
onShow() {
const pages = getCurrentPages().map(e => `/${e.route}`).reverse() // 获取页面栈
if (pages[0]) { // 当页面栈不为空时执行
let old = _c(route)
const back = pages[0] != route.fullPath
const now = _$UNI_ACTIVED_PAGE_ROUTES.find(e => e.fullPath == pages[0]) // 如果路由没有被缓存就缓存
now ? Object.assign(route, now) : _$UNI_ACTIVED_PAGE_ROUTES.push(_c(route)) // 已缓存就用已缓存的更新 route 对象
_$UNI_ACTIVED_PAGE_ROUTES.splice(pages.length, _$UNI_ACTIVED_PAGE_ROUTES.length) // 最后清除无效缓存
if (back) { // 当当前路由与 route 对象不符时,表示路由发生返回
onchange(route, old)
}
}
}
})

const router = new Proxy({
route: route, // 当前路由对象所在的 path 等信息,
afterEach: to => {}, // 全局后置守卫
beforeEach: (to, next) => next(), // 全局前置守卫
routes: modulesFiles.keys().map(e => e = e.replace(/^\./, '/pages')), // 路由表
_getFullPath(route) { // 根据传进来的路由名称获取完整的路由名称
return new Promise((resolve, reject) => {
const fullPath = this.routes.find(e => RegExp(route + '.vue').test(e))
fullPath ? resolve(fullPath.replace(/\.vue$/, '')) : reject(`路由 ${ route + '.vue' } 不存在于 pages 目录中`)
})
},
_formatData(query) { // 序列化路由传参
let queryString = '?'
Object.keys(query).forEach(e => {
if (typeof query[e] === 'object') {
queryString += `${e}=${JSON.stringify(query[e])}&`
} else {
queryString += `${e}=${query[e]}&`
}
})
return queryString.length === 1 ? '' : queryString.replace(/&$/, '')
},
_beforeEach(path, fullPath, query, type) { // 处理全局前置守卫
return new Promise(resolve => {
this.beforeEach({ path, fullPath, query, type }, resolve)
})
},
_next(next) { // 处理全局前置守卫 next 函数传经来的方法
return new Promise((resolve, reject) => {
if (typeof next === 'function') { // 当 next 为函数时, 表示重定向路由,
reject('在全局前置守卫 next 中重定向路由')
Promise.resolve().then(() => next(this)) // 此处一个微任务的延迟是为了先触发重定向的reject
} else if (next === false) { // 当 next 为 false 时, 表示取消路由
reject('在全局前置守卫 next 中取消路由')
} else {
resolve()
}
})
},
_routeTo(UNIAPI, type, path, query, notBeforeEach, notAfterEach) {
return new Promise((resolve, reject) => {
this._getFullPath(path).then((fullPath) => { // 检查路由是否存在于 pages 中
const routeTo = url => { // 执行路由
const temp = _c(route) // 将 route 缓存起来
Object.assign(route, { path, fullPath, query, type }) // 在路由开始执行前就将 query 放入 route, 防止少数情况出项的 onLoad 执行时,query 还没有合并
UNIAPI({ url }).then(([err]) => {
if (err) { // 路由未在 pages.json 中注册
Object.assign(route, temp) // 如果路由跳转失败,就将 route 恢复
reject(err)
return
} else { // 跳转成功, 将路由信息赋值给 route
resolve(route) // 将更新后的路由对象 resolve 出去
onchange({ path, fullPath, query, type }, temp)
!notAfterEach && this.afterEach(route) // 如果没有禁止全局后置守卫拦截时, 执行全局后置守卫拦截
}
})
}
if (notBeforeEach) { // notBeforeEach 当不需要被全局前置守卫拦截时
routeTo(`${fullPath}${this._formatData(query)}`)
} else {
this._beforeEach(path, fullPath, query, type).then((next) => { // 执行全局前置守卫,并将参数传入
this._next(next).then(() => { // 在全局前置守卫 next 没传参
routeTo(`${fullPath}${this._formatData(query)}`)
}).catch(e => reject(e)) // 在全局前置守卫 next 中取消或重定向路由
})
}
}).catch(e => reject(e)) // 路由不存在于 pages 中, reject
})
},
pop(data) {
if (typeof data === 'object') {
_$UNI_ROUTER_PUSH_POP_FUN(data)
}
uni.navigateBack({ delta: typeof data === 'number' ? data : 1 })
},
// path 路由名 // query 路由传参 // isBeforeEach 是否要被全局前置守卫拦截 // isAfterEach 是否要被全局后置守卫拦截
push(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.navigateTo, 'push', path, query, notBeforeEach, notAfterEach)
},
pushPop(path, query = {}, notBeforeEach, notAfterEach) {
return new Promise(resolve => {
_$UNI_ROUTER_PUSH_POP_FUN(null)
_$UNI_ROUTER_PUSH_POP_FUN = resolve
this._routeTo(uni.navigateTo, 'pushPop', path, query, notBeforeEach, notAfterEach)
})
},
replace(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.redirectTo, 'replace', path, query, notBeforeEach, notAfterEach)
},
switchTab(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.switchTab, 'switchTab', path, query, notBeforeEach, notAfterEach)
},
reLaunch(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.reLaunch, 'reLaunch', path, query, notBeforeEach, notAfterEach)
}
}, {
set(target, key, value) {
if (key == 'onchange') {
onchange = value
}
return Reflect.set(target, key, value)
}
})

Object.setPrototypeOf(route, router) // 让 route 继承 router
export default router


收起阅读 »

uniapp与flutter,跨平台解决方案你该如何选择

为了做毕设,用了下uniapp与flutter,说真的,这是两款十分优秀的产品,几乎做到了各自领域性能和跨平台的极致。那么这两款产品到底有什么不同,在选型的时候应该如何取舍,这是我写这篇文章的目的。uniapp与flutter都是为了解决跨平台问题的框架uni...
继续阅读 »

为了做毕设,用了下uniapp与flutter,说真的,这是两款十分优秀的产品,几乎做到了各自领域性能和跨平台的极致。那么这两款产品到底有什么不同,在选型的时候应该如何取舍,这是我写这篇文章的目的。

uniapp与flutter都是为了解决跨平台问题的框架

uniapp是从h5 app到小程序一步步发展过来的,也就是走的html的路线。

html从最早的网页套壳一步步发展至今,为了解决早期套壳的体验问题,我们尝试用js代码调用原生接口,与原生进行交互,出现了一系列如React Native,Cordova,Weex,Framework7,MUI之类的框架,这些框架的出现进一步丰富h5应用的功能。但是这些技术要求很高的优化技巧,要走很多坑,在ios的体验尚可,但是Android上由于更新维护问题,js引擎差别很大,早期Android的js引擎极差,这些框架使用体验都不好,当然也有硬件方面的原因。而且Android上webview存在性能瓶颈,复杂应用不做预加载的情况下使用体验真的不好。后来为了使体验达到h5所能做的极致,小程序出现了,为了性能,屏蔽了dom,规定了独特的规范,按照这些规范去写,编译时框架提前给你优化好,事实证明这样做确实可以提高h5应用的使用体验。

uniapp延续了小程序的思路,和vue结合,屏蔽dom,提前优化,确实很好,也做到了跨平台,这是一款极为优秀的跨各种小程序的解决方案,与它自家的h5+结合也是一个还算不错的h5+ app的前端框架。但是uniapp的定位中有一个极大的问题,就是小程序与h5 app之间的距离太大了,强跨的体验真是极差,得不偿失。举个栗子,3d渲染,多人视频,nfc写卡,这种小程序完全做不到,当然uniapp也可以调h5+ runtime,但是一个复杂的移动端应用可能会加各种各样的东西,你完全预料不到可能出现什么需求,并且这些需求越来越多的情况下,小程序端与移动端分开维护是必然的结果,强行结合只能是结构混乱,难以维护。那么如果分开维护,uniapp与前面提到的那些框架并没有明显优势。

那么接着说flutter,flutter与h5技术栈的思路完全不同,JSCore,V8再怎样优秀,也始终解决不了JavaScript本身语法缺陷和运行在浏览器的事实。

===========================

这里我之前写flutter用dart做了一个渲染引擎,有人言辞激烈的抨击了我的错误,后来我仔细看了一下资料。


官网上是这样说的

Flutter is built with C, C++, Dart, and Skia (a 2D rendering engine).See this architecture diagram for abetter picture of the main components.

确实,dart只是用来组织各种控件的一个工具,这个图形渲染是用了这个叫Skia的图形库

Skia is an open source 2D graphics library which provides common APIs that work across a variety of hardware and software platforms.

这个Skia,Google旗下,开源2D图形库,提供了多种软硬件平台的通用API。

确实是我的错,没调查清楚,但是这个方式还是令我觉得,很难受。

=================================

也就是说flutter和cocos,unity3d一样,完全可以用来写游戏,突破60fps,而且自己渲染,大大减少了与原生的通信次数,并且使用 Platform Channels 来跟系统通信大大丰富了一些偏门功能的应用,去组件库看了下tcp直连mqtt都支持了,刚好毕设会用到,开心。

所以如果你需要跨平台,技术选型时遇到问题

1.看需求

如果你的应用需求足够简单,像小程序之类的完全可以做到,选uniapp。因为说真的,像点单这种功能,谁没事愿意专门装个肯打鸡,coco之类在手机上,反正我去点单的时候,能用小程序我就不会再装app了,如果有人愿意装app,稍微改改顺便出个app版看着比较好看。

如果你的需求复杂,必然要分开维护,还是和之前一样,uniapp是一个极好的跨各种小程序的解决方案,一次编译,微信小程序,支付宝小程序,百度小程序,多端运行。那app端你可以再选择h5或者flutter。

如果你需要适配横屏,建议用flutter,横屏的交互加上material design的加持,这样和桌面端就没有太大区别了,目前flutter已经可以编译运行在Windows和linux上,虽然目前还很不完善,但是Google的野心和背书能力让我觉得flutter的野心不止于此。未来能附赠一套桌面端,意外之喜。


2.学习成本

flutter的学习成本主要在Dart,而uniapp主要在vue。说真的,我之前做Android和JavaWeb的,Java转Dart真的没有压力,有人说flutter嵌套太多,安卓xml布局嵌套不多吗,公司现在维护的ERP系统jQuery写的跟使一样,各种+ " append。

而我一个传统Java使用者刚开始遇到vue真的难受了好一阵子,这个this的真是vue里令我最难受的,使一样。推荐周围同学学uniapp,学过C++,Java的普遍反映也是vue看不懂。你们再也不是像jQuery一样好单纯好不做作的前端了。

总之前端的uniapp学习成本低,学过后端Java,C++的,flutter上手成本低。


3.社区

刚开始Google要出Fuchsia OS的时候我还嗤之以鼻,真当程序狗们都会乖乖听你话吗,那win phone坟头草都老高了。没想到啊,你们早在苹果骂安卓垃圾的时候就想着今天了吧。

Google在安卓界的背书能力感觉跟Spring在JavaWeb界的背书能力不逞多让,只要Android和Fuchsia不死,Flutter应该不会有太大问题,而且Flutter的社区是真的真的真的很活跃啊,github上问题的解决速度和出视频的速度真是令我叹为观止。

相比之下DCloud出MUI到现在不愠不火就让我不禁对uniapp有些担心,虽然微信,支付宝在后面背书,希望一群国内一线大厂们能给力点吧。而且我在uniapp提的问题一个多月了,无人问津

【报Bug】使用小程序组件,当参数为函数时,传不过去 - DCloud问答

希望你们珍惜你们的银牌赞助者。而且出视频的速度一言难尽,看B站居然没有,讲道理一个好的教学视频真的很重要,干啃API在学习时真是费力不讨好的事情,你学习的思路和文档的思路是不一样的。不过uniapp的QQ群倒是很火,不管怎样,一个国产的优秀产品,希望你们能有一个好的未来。

原文:https://zhuanlan.zhihu.com/p/55466963

收起阅读 »

uni-app 的使用体验总结

[实践] uni-app 的使用总结最近使用 uni-app 的感受。使用体验没用之前以为真和 Vue 一样,用了之后才知道。有点类似 Vue 和 小程序结合的感觉。写类似小程序的标签,有着小程序和 Vue 的生命周期钩子。对比 uni-app 文档和微信小程...
继续阅读 »

[实践] uni-app 的使用总结


最近使用 uni-app 的感受。

使用体验

没用之前以为真和 Vue 一样,用了之后才知道。有点类似 Vue 和 小程序结合的感觉。写类似小程序的标签,有着小程序和 Vue 的生命周期钩子。对比 uni-app 文档和微信小程序的文档,不差多少,只是将 wx => uni,熟悉 Vue 和 小程序可以直接上手。

如果看过其他小程序的文档,可以发现,文档主要的三大章节就体现在框架组件API 。

uni-app 需要注意看注意事项,文档给出了和 Vue 使用的区别。例如动态的 Class 与 Style 绑定,在 H5 能用,APP 和小程序的体现就不一样。

配置项跟着文档来,开发环境也是现成的,下载 HBuilderX 导入项目就能运行,日常开发习惯了 VSCode,所以 HBuilderX 的主要作用就是用来打包 APK 和起各个端的服务,coding 的话当然还是用 VSCode。

路由

uni-app 的路由全部配置在 pages.json 文件里,就会导致多人开发的时候,路由无法拆分,如果处理的不好,就会发生冲突。

导航

导航栏需要注意的一个问题就是不同端的展示形式会不同,所以要处理兼容问题,导航栏可以自定义,用原生,框架,插件但是兼容性都不同,多端需求一定要在不同设备跑一下看效果。

例如在小程序和 APP 中,原生导航栏取消不了,就不能用自定义的导航栏,要在 pages.json 中配置原生导航栏。

兼容方法就是用 uni-app 提供的条件编译,处理各端不同的差异,我们支付的业务逻辑也是通过条件编译,区分不同端调用不同的支付方式。

生命周期

分为 应用的生命周期页面的生命周期组件的生命周期。写过小程序和 Vue 的很好理解,大致上和 Vue 的还是差不多的,页面生命周期针对当前的页面,应用生命周期针对小程序、APP。这些过程可能都要踩一下!

网络请求和环境配置

官方的 uni.request 虽然封装好了基本的请求,但是没有拦截,我们开始也是自己在这基础上加了层壳,简单的封装发送请求。当然也可以选择第三方库的使用,如 flyio、axios。

我们是前端自己封装了 HTTP 请求,并且统一接口的请求方式,所有的接口放到 api.js 文件中进行统一管理。这样大家在页面请求接口的时候风格才统一,包括约定好请求拦截和响应拦截,具体拦截的参数和后台约定好。

资源优化

  • 暂时接触不到 Webpack 之类的资源打包优化,但是文档中有提到资源预取、预加载、treeShaking 只需要在配置文件中设置即可,或者在开发工具勾上。小程序也是勾选自动压缩混淆。
  • 删除没用到文件和图片资源,因为打包的时候是会算进去的,比如 static 目录下的资源文件都会被打包,而且图片资源太大也不好。
  • uni-app 运行时的框架主库 chunk-vendors.js 文件是经过处理的,部署做 gzip

Web-View 组件

在 uni-app 中使用 Web-View,可以使用本地的资源和网络的资源,不同平台也是有差异的,小程序不支持本地 HTML,且小程序端 Web-View 组件一定有原生导航栏。

需要注意的是网页向应用 postMessage 的时候需要引入 uni.web-view.js,不然是没办法通信拿不到数据。

TODO: 这个坑后面再详细总结下!

全局状态

最开始是直接使用类似小程序的 globalData 来管理我们的全局状态,但是后面发现需求一多,加了各种东西之后,需要取这个状态的时候就很痛苦,做为程序猿嘛,都想偷懒吖,每次都得引入一下 getApp().globalData.data 这样很繁琐可不行,就替换成了 Vuex,需要取这个变量的时候,直接 this.vuex_xxxx 就能拿到这个值。

有段时间重写了 HTTP 请求部分和全局状态管理部分。

小程序中要在每一个页面中添加使用共有的数据,可以有三种方式解决。

Vue.prototype

它的作用是可以挂载到 Vue 的所有实例上,供所有的页面使用。

// main.js
Vue.prototype.$globalVar = "Hello";

然后在 pages/index/index 中使用:

<template>
<view>{{ useGlobalVar }}</view>
</tempalte>
<script>
export default {
data (){
return {
useGlobalVar: $globalVar
}
}
}
</script>

globalData

<!-- App.vue -->
<script>
export default {
globalData:{
data:1
}
onShow() {

getApp().globalData.data; // 使用

getApp().globalData.data = 1; // 更新

};
</script>

Vuex

Vuex 是 Vue 专用的状态管理模式。能够集中管理其数据,并且可观测其数据变化,以及流动。


之前看到一个通俗化比喻:用交通工具来比喻项目中这几种描述全局变量的方式。

下面列举这些方式通俗的理解状态:

Vue 插件 vue-bus 可以来管理一部分全局变量(叫应用状态吧),学习后发现,bus(中文意思:公交车)这名字取得挺形象的。

先罗列一下这些方式,不过这种分类并不严谨。

1、VueBus:公交车 2、Vuex:飞机 3、全局 import

  • a.new Vue():专车;
  • b.Vue.use:快车;
  • c.Vue.prototype:顺风车。

4、globalData:地铁

首先 VueBus,像公交车一样灵活便捷,随时都可以乘坐;表现在代码里,很轻便,召之即来,缺点就是不好维护,没有一个专门的文件去管理这些变量。想象平时等公交车的心情,知道它回来,但不知道它什么时候来,给人一种很不安的感觉。

而 Vuex,它像飞机,很庄重,塔台要协调飞机运作畅顺,飞机随时向地面报告自己的位置,适合用在大型项目。表现代码中,就是集中式管理所有状态,并且以可预测的方式发生变化。也对应着飞机绝对不能失联的特点。

第三种方式是全局 import,分三种类型,分别是:new Vue()Vue.use()Vue.prototype。可以用网约车来比喻,三种类型分别对应:专车、快车、顺风车。都足够灵活,表现在代码里:一处导入,处处可用。

再分别说明:

new Vue() 就像滴滴的礼橙专车,官方运营,安全可靠。表现在代码里,就是只有 Vue 官方维护的库才能使用这种方式。

Vue.use() 就像快车,必须符合滴滴的规范,才能成为专职司机。表现在代码中,就是导入的插件(或者库)必须符合 Vue 的写法(即封装了 Vue 插件写法)。

Vue.prototype 像顺风车,要求没上面两个那么严,符合一般 js 写法就行,就像顺风车的准入门槛稍稍低一点。

当然,uni-app 的项目里还有可以用 globalData 定义全局变量,非要比喻,可以用地铁,首先比 vue-bus 更好管理维护,想象地铁是不是比公交更可靠;其次比 Vuex 更简单,因为 globalData 真的就是简单的定义一些变量。

globalData 是微信小程序发明的,Vue 项目好像没有对应的概念,但是在 uni-app 中一样可用。

上面说到,这种分类方式不严谨,主要体现在原理上,并不是简单的并列关系或包含关系。

插件市场

uni-app 的主要特色也源自于它的插件市场十分丰富。

用得比较好的组件:

uView:我们用了这个库的骨架屏。这个库还是有很多技巧可以学到的。

https://www.uviewui.com/js/intro.html

ColorUI-UniApp:是个样式库,不是组件库。

https://ext.dcloud.net.cn/plugin?id=239

答题模版:左右滑答题模版,单选题、多选项,判断题,填空题,问答题。基于 ColorUI 做的。

https://ext.dcloud.net.cn/plugin?id=451

uCharts 高性能跨全端图表:

https://ext.dcloud.net.cn/plugin?id=271

最后:各端的差异性,很多东西,H5 挺好的,上真机就挂了,真机好着的,换小程序就飘了,不同小程序之间也有差异,重点是仔细阅读文档。

云打包限制,云打包(打 APK) 的每天做了限制,超出次数需要购买。

虽然可能一些原生可以实现的功能 uni-app 实现不了,不过整体开发下来还行,很多的坑还是因为多端不兼容,除了写起来麻烦一点,基本上都还是有可以解决的策略。比之前用 Weex 写 APP 开发体验好一点,比 React Native 的编译鸡肋一点(这点体验不是很好),至于 Flutter 还没有试过,有机会的话会试一下。

原文:https://zhuanlan.zhihu.com/p/153500294

收起阅读 »

使用uniapp开发项目来的几点心得体会

先说一下提前须要会的技术要想快速入手uniapp的话,你最好提前学会vue、微信小程序开发,因为它几乎就是这两个东西的结合体,不然,你就只有慢慢研究吧。为什么要选择uniapp???开发多个平台的时候,对,就是开发多端,其中包括安卓、IOS、H5/公众号、微信...
继续阅读 »

先说一下提前须要会的技术

要想快速入手uniapp的话,你最好提前学会vue、微信小程序开发,因为它几乎就是这两个东西的结合体,不然,你就只有慢慢研究吧。

为什么要选择uniapp???

开发多个平台的时候,对,就是开发多端,其中包括安卓、IOS、H5/公众号、微信小程序、百度小程序...等其它小程序时,如果每个平台开发,人力开发成本高,后期维护也难,原生开发周期也长,那Unipp就是你的优先选择,官方是这样介绍的~哈~ 先来说一下uniapp的优点

uniapp优点

优点一,多端支持

当然是多端开发啦,uni-app是一套可以适用多端的开源框架,一套代码可以同时生成ios,Android,H5,微信小程序,支付宝小程序,百度小程序等。

优点二,更新迭代快

用了它的Hbx你就知道,经常会右下角会弹出让你更新,没错,看到它经常更新,这么努力的在先进与优化,还是选良心的了。

优点三,扩张强

你可以把轻松的把uniapp编译到你想要的端,也可以把其它端的转换成uniapp,例如微信小程序,h5等;如果开发app的时候,前端表现不够,你还可以原生嵌套开发。

优点四,开发成本、门槛低

不管你是公司也好,个人也好,如果你想开发多终端兼容的移动端,那uniapp就很适合你,不然以个人的能力要开发多端,哈哈... 洗洗睡觉吧。

优点五,组件丰富

社区还是比较成熟,生态好,组件丰富,支持npm方式安装第三方包,兼容mpvue,DCloud有大量的组件供你使用,当然付费的也不贵,你还可以发布你开发的,赚两个鸡腿钱还是可以的。


开发上的优点暂且不说,大体上的有这么一些,接下来说一下开发过程中的缺点

uniapp缺点

缺点一:爬坑

每个程序前期肯定都会有很多的坑,这里点明一下:腾讯,敢问谁没在微信开发上坑哭过,现在不也爬起来了,2年前有人提的bug,你现在去看,他依然在那,不离不弃呀。uniapp坑也有,一般的都有人解决了,没解决的,你就要慢慢的去琢磨了,官方bug的话,提交反馈,等官方修复。

缺点二:某些组件不成熟

我说的是某些官方组件,像什么地图组件,直播组件等,你要在上面开发一些特别功能的话,那真的是比较费神的。

缺点二:nvue有点蛋疼

某些组件或某些功能,官方明确说,建议用nvue开发,那么问题来了,nvue有很多的局限,特别是css,很多都不支持,什么文字只能是text,只支持class样式,很多的,要看文档来。


暂时从使用上的总结就这么一些,如果你有不同的见解,留言交流交流~~

原文:https://zhuanlan.zhihu.com/p/336773995

收起阅读 »

iOS- 安装CocoaPods详细过程

一、简介什么是CocoaPodsCocoaPods是OS X和iOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods”的依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。CocoaPods的好...
继续阅读 »

一、简介

  • 什么是CocoaPods

CocoaPods是OS X和iOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods”的依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。

  • CocoaPods的好处

1、在引入第三方库时它可以自动为我们完成各种各样的配置,包括配置编译阶段、连接器选项、甚至是ARC环境下的-fno-objc-arc配置等。

2、使用CocoaPods可以很方便地查找新的第三方库,这些类库是比较“标准的”,而不是网上随便找到的,这样可以让我们找到真正好用的类库。

二、Cocoapods安装步骤

注意:在终端输入命令时,取$后面部分输入

1、升级Ruby环境

终端输入:$ gem update --system

此时会出现


这是因为你没有权限去升级Ruby

这时应该输入:$ sudo gem update --system


接下来输入密码,注意:输入密码的时候没有任何反应,光标也不会移动,你尽管输入就是了,输完了直接回车。
等一会如果出现

恭喜你,升级Ruby成功了。

2、更换Ruby镜像

首先移除现有的Ruby镜像

终端输入:$ gem sources --remove https://gems.ruby-china.org/

然后添加国内最新镜像源(淘宝的Ruby镜像已经不更新了)

终端输入:$ gem sources -a https://gems.ruby-china.com/

执行完毕之后输入gem sources -l来查看当前镜像

终端输入:$ gem sources -l

如果结果是

*** CURRENT SOURCES ***
https://gems.ruby-china.com/
说明添加成功,否则继续执行$ gem source -a https://gems.ruby-china.com/来添加

3、安装CocoaPods

接下来开始安装
终端输入:$ sudo gem install cocoapods


说明没有权限,需要输入

终端输入:$ sudo gem install -n /usr/local/bin cocoapods

安装成功如下:


到这之后再执行pod setup(PS:这个过程是漫长的,要有耐心)

终端输入:$ pod setup

然后你会看到出现了Setting up CocoaPods master repo,卡住不动了,说明Cocoapods在将它的信息下载到 ~/.cocoapods里。
你可以command+n新建一个终端窗口,执行cd ~/.cocoapods/进入到该文件夹下,然后执行du -sh *来查看文件大小,每隔几分钟查看一次,这个目录最终大小是900多M(我的是930M)
当出现Setup completed的时候说明已经完成了。

4、CocoaPods的使用

1、首先我们来搜索一下三方库
终端输入:$ pod search AFNetworking

这时有可能出现


这是因为之前pod search的时候生成了缓存文件search_index.json
执行rm ~/Library/Caches/CocoaPods/search_index.json来删除该文件
然后再次输入pod search AFNetworking进行搜索
这时会提示Creating search index for spec repo 'master'..
等待一会将会出现搜索结果如下:


出现这个了就说明搜索成功了,看一下上图中的这一句:
pod 'AFNetworking', '~> 3.1.0'
这句话一会我们要用到,这是CocoaPods添加三方库的关键字段
然后退出这个界面(这一步只是验证一下cocoapods有没有安装成功,能不能搜索到你想要的三方库),直接按"q"就退出去了。

2、在工程中创建一个Podfile文件

要想在你的工程中创建Podfile文件,必须先要进到该工程目录下

终端输入:$ cd /Users/liyang/Desktop/CocoaPodsTest
//这是我电脑上的路径,你输入你自己项目的路径或直接拖拽也行

进来之后就创建

终端输入:$ touch Podfile

然后你在你的工程目录下可以看到多了一个Podfile文件

3、编辑你想导入的第三方库的名称及版本

使用vim编辑Podfile文件

终端输入:$ vim Podfile

进入如下界面:


进来之后紧接着按键盘上的英文'i'
下面的"Podsfile" 0L, 0C将变成-- INSERT --
然后就可以编辑文字了,输入以下文字

platform :ios, '7.0'
target 'MyApp' do
pod 'AFNetworking', '~> 3.1.0'
end
解释一下
platform :ios, '7.0'代表当前AFNetworking支持的iOS最低版本是iOS 7.0,
'MyApp'就是你自己的工程名字,
pod 'AFNetworking', '~> 3.1.0'代表要下载的AFNetworking版本是3.1.0及以上版本,还可以去掉后面的'~> 3.1.0',直接写pod 'AFNetworking',这样代表下载的AFNetworking是最新版。
编辑完之后成如下样子:

此时该退出去了,怎么退出去呢?跟着我做,先按左上角的esc键,再按:键,再输入wq,点击回车,就保存并退出去了。

这时候,你会发现你的项目目录中名字为Podfile的文件的内容就是你刚刚输入的内容。

4、把该库下载到Xcode中

终端输入:$ pod install

这就开始下载了,需要一段时间,出现如下界面就说明安装好了


这个时候关闭所有的Xcode窗口,再次打开工程目录会看到多了一个后缀名为.xcworkspace文件。


以后打开工程就双击这个文件打开了,而不再是打开.xcodeproj文件。
进入工程后引入头文件不再是#import "AFNetworking.h",而是#import <AFNetworking.h>




原贴链接:https://www.jianshu.com/p/9e4e36ba8574
收起阅读 »

uni-app 悬浮框动效

<view class="menu" :class="{active:menuFlag}"> <image src="../../static/svg/1.svg" class="menuTrigger" @tap="clickMenu"&...
继续阅读 »


<view class="menu" :class="{active:menuFlag}">
<image src="../../static/svg/1.svg" class="menuTrigger" @tap="clickMenu"></image>
<image src="../../static/svg/2.svg" class="menuItem menuItem1"></image>
<image src="../../static/svg/3.svg" class="menuItem menuItem2"></image>
<image src="../../static/svg/4.svg" class="menuItem menuItem3"></image>
</view>
.menu{
position: fixed;
width: 110rpx;
height: 110rpx;
bottom: 120rpx;
right: 44rpx;
border-radius: 50%;
}
.menuTrigger{
position: absolute;
top: 0;
left: 0;
width: 70rpx;
height: 70rpx;
background-color: green;
border-radius: 50%;
padding: 20rpx;
cursor: pointer;
transition: .35s ease;
}
.menuItem{
position: absolute;
width: 50rpx;
height: 50rpx;
top: 10rpx;
left: 10rpx;
padding: 20rpx;
border-radius: 50%;
background-color: white;
border: none;
box-shadow: 0 0 5rpx 1rpx rgba(0,0,0,.05);
z-index: -1000;
opacity: 0;
}
.menuItem1{
transition: .35s ease;
}
.menuItem2{
transition: .35s ease .1s;
}
.menuItem3{
transition: .35s ease .2s;
}
.menu.active .menuTrigger{
transform: rotateZ(225deg);
background-color: pink;
}
.menu.active .menuItem1{
top: -106rpx;
left: -120rpx;
opacity: 1;
}
.menu.active .menuItem2{
top: 10rpx;
left: -164rpx;
opacity: 1;
}
.menu.active .menuItem3{
top: 126rpx;
left: -120rpx;
opacity: 1;
}
data() {
return {
mask: false,
menuFlag: false,
}
},

clickMenu(){
this.menuFlag = !this.menuFlag;
},


原文链接:https://zhuanlan.zhihu.com/p/364244176

收起阅读 »