注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

CoordinatorLayout 嵌套Recycleview 卡顿问题

1.问题场景 伪代码: <CoordinatorLayout> <AppBarLayout> <RecycleView> </RecycleView> </AppBa...
继续阅读 »

1.问题场景


伪代码:
<CoordinatorLayout>
<AppBarLayout>
<RecycleView>
</RecycleView>
</AppBarLayout>
</ConstraintLayout>

一般这种做法是,底部view的相应滑动,滑动联动,但是同时会出现RecycleView ViewHoder复用失败,造成cpu 的消耗,item到达一定数量后会造成oom页面出现卡顿


2. 问题原理


RecycleView ViewHoder 复用问题第一时间我们应想到是; ViewGrop/onMeasureChild
测量问题,重写 onMeasureChild ,避免中间MeasureSpec.UNSPECIFIED模式 的赋值造成RecycleView的item复用,但是是失败的!


 @Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
child.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
}


原因是: parentHeightMeasureSpec 已经被设置 MeasureSpec.UNSPECIFIED 测量模式 看下源码CoordinatorLayout onMeasure 局部关键代码:


prepareChildren();

final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}

通过prepareChildren()结合LayoutParams


        R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}

我们可以得到 Behavior b 就是我们再布局内设置的 AppBarLayout. layout_behavior, 可以看到 Behavior/onMeasureChild 做了一层测量, ,我们继续看 Behavior/onMeasureChild 源码:


@Override
public boolean onMeasureChild(
@NonNull CoordinatorLayout parent,
@NonNull T child,
int parentWidthMeasureSpec,
int widthUsed,
int parentHeightMeasureSpec,
int heightUsed) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
// If the view is set to wrap on it's height, CoordinatorLayout by default will
// cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
// what we actually want, so we measure it ourselves with an unspecified spec to
// allow the child to be larger than it's parent
parent.onMeasureChild(
child,
parentWidthMeasureSpec,
widthUsed,
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
heightUsed);
return true;
}

// Let the parent handle it as normal
return super.onMeasureChild(
parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}

问题找到了问题关键 if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) ,造成了MeasureSpec.UNSPECIFIED 的使用, 而这个模式又会造成Recycleview.LayoutManager加载所有的item,导致复用失败; 看到这 AppBarLayout给固定值或者match_parent 不就解决问题了吗, 是能解决问题,但是这样 我们的layout ui就不符合我们绘制ui的布局了,也会造成页面空白显示问题,所以这样使用recycleview 嵌套是非法使用,矛盾使用!


解决问题



  • 同一使用RecycleView 使用,作为RecycleView item 的一部分,但是也会造成滑动冲突问题,然后通过 NestedScrollingParent3 外部拦截法,来解决内外层的滑动冲突,问题顺利解决


override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
if (e!!.action == MotionEvent.ACTION_DOWN) {
val childRecyclerView = findCurrentChildRecyclerView()

// 1. 是否禁止拦截
doNotInterceptTouchEvent = doNotInterceptTouch(e.rawY, childRecyclerView)

// 2. 停止Fling
this.stopFling()
childRecyclerView?.stopFling()
}

return if (doNotInterceptTouchEvent) {
false
} else {
super.onInterceptTouchEvent(e)
}
}


  • 根据业务场景,也可使用baserecyclerviewadapterhelper,一个优秀的Adapter 框架, 


addHeaderView来添加itemView,通过 notifyItemInserted(position) 添加ReceiveView 的item


@JvmOverloads
fun addHeaderView(view: View, index: Int = -1, orientation: Int = LinearLayout.VERTICAL): Int {
if (!this::mHeaderLayout.isInitialized) {
mHeaderLayout = LinearLayout(view.context)
mHeaderLayout.orientation = orientation
mHeaderLayout.layoutParams = if (orientation == LinearLayout.VERTICAL) {
RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
} else {
RecyclerView.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
}
}

val childCount = mHeaderLayout.childCount
var mIndex = index
if (index < 0 || index > childCount) {
mIndex = childCount
}
mHeaderLayout.addView(view, mIndex)
if (mHeaderLayout.childCount == 1) {
val position = headerViewPosition
if (position != -1) {
notifyItemInserted(position)
}
}
return mIndex
}
收起阅读 »

优雅地封装 Activity Result API,完美地替代 startActivityForResult()

前言 Activity Result API。这是官方用于替代 startActivityForResult() 和 onActivityResult() 的。虽然出了有大半年了,但是个人到现在没看到比较好用的封装。最初大多数人会用拓展函数进行封装,而在 a...
继续阅读 »

前言


Activity Result API。这是官方用于替代 startActivityForResult()onActivityResult() 的。虽然出了有大半年了,但是个人到现在没看到比较好用的封装。最初大多数人会用拓展函数进行封装,而在 activity-ktx:1.2.0-beta02 版本之后,调用注册方法的时机必须在 onStart() 之前,原来的拓展函数就不适用了,在这之后就没看到有人进行封装了。


个人对 Activity Result API 的封装思考了很久,已经尽量做到在 Kotlin 和 Java 都足够地好用,可以完美替代 startActivityForResult() 了。下面带着大家一起来封装 Activity Result API。


基础用法


首先要先了解基础的用法,在 ComponentActivity 或 Fragment 中调用 Activity Result API 提供的 registerForActivityResult() 方法注册结果回调(在 onStart() 之前调用)。该方法接收 ActivityResultContract 和 ActivityResultCallback 参数,返回可以启动另一个 activity 的 ActivityResultLauncher 对象。


ActivityResultContract 协议类定义生成结果所需的输入类型以及结果的输出类型,Activity Result API 已经提供了很多默认的协议类,方便大家实现请求权限、拍照等常见操作。


val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
 // Handle the returned Uri
}

只是注册回调并不会启动另一个 activity ,还要调用 ActivityResultLauncher#launch() 方法才会启动。传入协议类定义的输入参数,当用户完成后续 activity 的操作并返回时,将执行 ActivityResultCallback 中的 onActivityResult()回调方法。


getContent.launch("image/*")

完整的使用代码:


val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
  // Handle the returned Uri
}

override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  selectButton.setOnClickListener {
    getContent.launch("image/*")
  }
}

ActivityResultContracts 提供了许多默认的协议类:


协议类作用
RequestPermission()请求单个权限
RequestMultiplePermissions()请求多个权限
TakePicturePreview()拍照预览,返回 Bitmap
TakePicture()拍照,返回 Uri
TakeVideo()录像,返回 Uri
GetContent()获取单个内容文件
GetMultipleContents()获取多个内容文件
CreateDocument()创建文档
OpenDocument()打开单个文档
OpenMultipleDocuments()打开多个文档
OpenDocumentTree()打开文档目录
PickContact()选择联系人
StartActivityForResult()通用协议


我们还可以自定义协议类,继承 ActivityResultContract,定义输入和输出类。如果不需要任何输入,可使用 Void 或 Unit 作为输入类型。需要实现两个方法,用于创建与 startActivityForResult() 配合使用的 Intent 和解析输出的结果。


class PickRingtone : ActivityResultContract<Int, Uri?>() {
  override fun createIntent(context: Context, ringtoneType: Int) =
    Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
      putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
    }

  override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
    if (resultCode != Activity.RESULT_OK) {
      return null
    }
    return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
  }
}

自定义协议类实现后,就能调用注册方法和 launch() 方法进行使用。


val pickRingtone = registerForActivityResult(PickRingtone()) { uri: Uri? ->
  // Handle the returned Uri
}

pickRingtone.launch(ringtoneType)

不想自定义协议类的话,可以使用通用的协议 ActivityResultContracts.StartActivityForResult(),实现类似于之前 startActivityForResult() 的功能。


val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
  if (result.resultCode == Activity.RESULT_OK) {
      val intent = result.intent
      // Handle the Intent
  }
}

startForResult.launch(Intent(this, InputTextActivity::class.java))

封装思路


为什么要封装?


看完上面的用法,不知道大家会不会和我初次了解的时候一样,感觉比原来复杂很多。


主要是引入的新概念比较多,原来只需要了解 startActivityForResult()onActivityResult() 的用法,现在要了解一大堆类是做什么的,学习成本高了不少。


用法也有些奇怪,比如官方示例用注册方法得到一个叫 getContent 对象,这更像是函数的命名,还要用这个对象去调用 launch() 方法,代码阅读起来总感觉怪怪的。


而且有个地方个人觉得不是很好,callback 居然在 registerForActivityResult() 方法里传。个人觉得 callback 在 launch() 方法里传更符合习惯,逻辑也更加连贯,代码阅读性更好。最好改成下面的用法,启动后就接着处理结果的逻辑。


getContent.launch("image/*") { uri: Uri? ->
 // Handle the returned Uri
}

所以还是有必要对 Activity Result API 进行封装的。


怎么封装?


首先是修改 callback 传参的位置,实现思路也比较简单,重载 launch() 方法加一个 callback 参数,用个变量缓存起来。在回调的时候拿缓存的 callback 对象去执行。


private var callback: ActivityResultCallback? = null

fun launch(input: I?, callback: ActivityResultCallback<O>) {
 this.callback = callback
 launcher.launch(input)
}

由于需要缓存 callback 对象,还要写一个类来持有该缓存变量。


有一个不好处理的问题是 registerForActivityResult() 需要的 onStart() 之前调用。可以通过 lifecycle 在 onCreate() 的时候自动注册,但是个人思考了好久并没有想到更优的实现方式。就是获取 lifecycleOwner 观察声明周期自动注册,也是需要在 onStart() 之前调用,那为什么不直接执行注册方法呢?所以个人改变了思路,不纠结于自动注册,而是简化注册的代码。


前面说了需要再写一个类缓存 callback 对象,使用一个类的时候有个方法基本会用到,就是构造函数。我们可以在创建对象的时候进行注册。


注册方法需要 callback 和协议类对象两个参数,callback 是从 launch() 方法得到,而协议类对象就需要传了。这样用起来个人觉得还不够友好,综合考虑后决定用继承的方式把协议类对象给“隐藏”了。


最终得到以下的基类。


public class BaseActivityResultLauncher<I, O> {

 private final ActivityResultLauncher launcher;
 private ActivityResultCallback callback;

 public BaseActivityResultLauncher(ActivityResultCaller caller, ActivityResultContract contract) {
   launcher = caller.registerForActivityResult(contract, (result) -> {
     if (callback != null) {
       callback.onActivityResult(result);
       callback = null;
    }
  });
}

 public void launch(@SuppressLint("UnknownNullness") I input, @NonNull ActivityResultCallback callback) {
   this.callback = callback;
   launcher.launch(input);
}
}

改用了 Java 代码来实现,返回的结果可以判空也可以不判空,比如返回数组的时候一定不为空,只是数组大小为 0 。用 Kotlin 实现的话要写两个不同名的方法来应对这个情况,使用起来并不是很方便。


这是多增加一个封装的步骤来简化后续的使用,原本只是继承 ActivityResultContract 实现协议类,现在还需要再写一个启动器类继承 BaseActivityResultLauncher


比如用前面获取图片的示例,我们再封装一个 GetContentLauncher 类。


class GetContentLauncher(caller: ActivityResultCaller) :
BaseActivityResultLauncher(caller, GetContent())

只需这么简单的继承封装,后续使用就更加简洁易用了。


val getContentLauncher = GetContentLauncher(this)

override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  selectButton.setOnClickListener {
    getContentLauncher.launch("image/*") { uri: Uri? ->
  // Handle the returned Uri
}
  }
}

再封装一个 Launcher 类的好处是,能更方便地重载 launch() 方法,比如在类里增加一个方法在获取图片之前会先授权读取权限。如果改用 Kotlin 拓展函数来实现,在 Java 会更加难用。Launcher 类能对 Java 用法进行兼顾。


最后总结一下,对比原本 Activity Result API 的用法,改善了什么问题:



  • 简化冗长的注册代码,改成简单地创建一个对象;

  • 改善对象的命名,比如官方示例命名为 getContent 对象就很奇怪,这通常是函数的命名。优化后很自然地用类名来命名为 getContentLauncher,使用一个启动器对象调用 launch() 方法会更加合理;

  • 改变回调的位置,使其更加符合使用习惯,逻辑更加连贯,代码阅读性更好;

  • 输入参数和输出参数不会限制为一个对象,可以重载方法简化用法;

  • 能更方便地整合多个启动器的功能,比如获取读取权限后再跳转相册选择图片;


最终用法


由于 Activity Result API 已有很多的协议类,如果每一个协议都去封装一个启动器类会有点麻烦,所以个人已经写好一个库 ActivityResultLauncher 方便大家使用。还新增和完善了一些功能,有以下特点:



  • 完美替代 startActivityForResult()

  • 支持 Kotlin 和 Java 用法

  • 支持请求权限

  • 支持拍照

  • 支持录像

  • 支持选择图片或视频(已适配 Android 10)

  • 支持裁剪图片(已适配 Android11)

  • 支持打开蓝牙

  • 支持打开定位

  • 支持使用存储访问框架 SAF

  • 支持选择联系人


个人写了个 Demo 给大家来演示有什么功能,完整的代码在 Github 里。


demo-qr-code.png


screenshot


下面来介绍 Kotlin 的用法,Java 的用法可以查看 Wiki 文档


在根目录的 build.gradle 添加:


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

添加依赖:


dependencies {
   implementation 'com.github.DylanCaiCoding:ActivityResultLauncher:1.0.0'
}

用法也只有简单的两步:


第一步,在 ComponentActivityFragment 创建对应的对象,需要注意创建对象的时机要在 onStart() 之前。例如创建通用的启动器:


private val startActivityLauncher = StartActivityLauncher(this)

提供以下默认的启动器类:

启动器作用
StartActivityLauncher完美替代 startActivityForResult()
TakePicturePreviewLauncher调用系统相机拍照预览,只返回 Bitmap
TakePictureLauncher调用系统相机拍照
TakeVideoLauncher调用系统相机录像
PickContentLauncher, GetContentLauncher选择单个图片或视频,已适配 Android 10
GetMultipleContentsLauncher选择多个图片或视频,已适配 Android 10
CropPictureLauncher裁剪图片,已适配 Android 11
RequestPermissionLauncher请求单个权限
RequestMultiplePermissionsLauncher请求多个权限
AppDetailsSettingsLauncher打开系统设置的 App 详情页
EnableBluetoothLauncher打开蓝牙
EnableLocationLauncher打开定位
CreateDocumentLauncher创建文档
OpenDocumentLauncher打开单个文档
OpenMultipleDocumentsLauncher打开多个文档
OpenDocumentTreeLauncher访问目录内容
PickContactLauncher选择联系人
StartIntentSenderLauncher替代 startIntentSender()


第二步,调用启动器对象的 launch() 方法。


比如跳转一个输入文字的页面,点击保存按钮回调结果。我们替换掉原来 startActivityForResult() 的写法。


val intent = Intent(this, InputTextActivity::class.java)
intent.putExtra(KEY_NAME, "nickname")
startActivityLauncher.launch(intent) { activityResult ->
if (activityResult.resultCode == RESULT_OK) {
data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
}
}

为了方便使用,有些启动器会增加一些更易用的 launch() 方法。比如这个例子能改成下面更简洁的写法。


startActivityLauncher.launch(KEY_NAME to "nickname") { resultCode, data ->
if (resultCode == RESULT_OK) {
data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
}
}

由于输入文字页面可能有多个地方需要跳转复用,我们可以用前面的封装思路,自定义实现一个 InputTextLauncher 类,进一步简化调用的代码,只关心输入值和输出值,不用再处理跳转和解析过程。


inputTextLauncher.launch("nickname") { value ->
if (value != null) {
toast(value)
}
}

通常要对返回值进行判断,因为可能会有取消操作,要判断是不是被取消了。比如返回的 Boolean 要为 true,返回的 Uri 不为 null,返回的数组不为空数组等。


还有一些常用的功能,比如调用系统相机拍照和跳转系统相册选择图片,已适配 Android 10,可以直接得到 uri 来加载图片和用 file 进行上传等操作。


takePictureLauncher.launch { uri, file ->
if (uri != null && file != null) {
// 上传或取消等操作后建议把缓存文件删除,调用 file.delete()
}
}

pickContentLauncher.launchForImage(
onActivityResult = { uri, file ->
if (uri != null && file != null) {
// 上传或取消等操作后建议把缓存文件删除,调用 file.delete()
}
},
onPermissionDenied = {
// 拒绝了读取权限且不再询问,可引导用户到设置里授权该权限
},
onExplainRequestPermission = {
// 拒绝了一次读取权限,可弹框解释为什么要获取该权限
}
)

个人也新增了些功能,比如裁剪图片,通常上传头像要裁剪成 1:1 比例,已适配 Android 11。


cropPictureLauncher.launch(inputUri) { uri, file ->
if (uri != null && file != null) {
// 上传或取消等操作后建议把缓存文件删除,调用 file.delete()
}
}

还有开启蓝牙功能,能更容易地开启蓝牙和确保蓝牙功能是可用的(需要授权定位权限和确保定位已打开)。


enableBluetoothLauncher.launchAndEnableLocation(
"为保证蓝牙正常使用,请开启定位", // 已授权权限但未开启定位,会跳转对应设置页面,并吐司该字符串
onLocationEnabled= { enabled ->
if (enabled) {
// 已开启了蓝牙,并且授权了位置权限和打开了定位
}
},
onPermissionDenied = {
// 拒绝了位置权限且不再询问,可引导用户到设置里授权该权限
},
onExplainRequestPermission = {
// 拒绝了一次位置权限,可弹框解释为什么要获取该权限
}
)

更多的用法请查看 Wiki 文档


原本 Activity Result API 已经有很多默认的协议类,都封装了对应的启动器类。大家可能不会用到所有类,开了混淆会自动移除没使用到的类。


彩蛋


个人之前封装过一个 startActivityForResult() 拓展函数,可以直接在后面写回调逻辑。


startActivityForResult(intent, requestCode) { resultCode, data ->
// Handle result
}

下面是实现的代码,使用一个 Fragment 来分发 onActivityResult 的结果。代码量不多,逻辑应该比较清晰,感兴趣的可以了解一下,Activity Result API 的实现原理应该也是类似的。


inline fun FragmentActivity.startActivityForResult(
intent:
Intent,
requestCode:
Int,
noinline callback: (resultCode: Int, data: Intent?) -> Unit
)
=
DispatchResultFragment.getInstance(this).startActivityForResult(intent, requestCode, callback)

class DispatchResultFragment : Fragment() {
private val callbacks = SparseArray<(resultCode: Int, data: Intent?) -> Unit>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}

fun startActivityForResult(
intent:
Intent,
requestCode:
Int,
callback: (
resultCode: Int, data: Intent?) -> Unit
)
{
callbacks.put(requestCode, callback)
startActivityForResult(intent, requestCode)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val callback = callbacks.get(requestCode)
if (callback != null) {
callback.invoke(resultCode, data)
callbacks.remove(requestCode)
}
}

companion object {
private const val TAG = "dispatch_result"

fun getInstance(activity: FragmentActivity): DispatchResultFragment =
activity.run {
val fragmentManager = supportFragmentManager
var fragment = fragmentManager.findFragmentByTag(TAG) as DispatchResultFragment?
if (fragment == null) {
fragment = DispatchResultFragment()
fragmentManager.beginTransaction().add(fragment, TAG).commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
}
fragment
}
}
}

如果觉得 Activity Result API 比较复杂,也可以拷贝这个去用。不过 requestCode 处理得不够好,而且很多功能需要自己额外去实现,用起来可能没那么方便。



收起阅读 »

最优解前端面试题答法

1. JS事件冒泡和事件代理(委托) 1. 事件冒泡 会从当前触发的事件目标一级一级往上传递,依次触发,直到document为止。 <body> <div id="parentId"> 查看消息信息 <div id="chi...
继续阅读 »

1. JS事件冒泡和事件代理(委托)


1. 事件冒泡


会从当前触发的事件目标一级一级往上传递,依次触发,直到document为止。


<body>    <div id="parentId"> 查看消息信息 <div id="childId1"> 删除消息信息 </div>    </div></body><script>    let parent = document.getElementById('parentId');    let childId1 = document.getElementById('childId1');    parent.addEventListener('click', function () {        alert('查看消息信息');    }, false);    childId1.addEventListener('click', function () {        alert('删除消息信息');    }, false);     // 如出发消息列表里的删除按钮, 先执行了删除操作, 在向上冒泡执行‘ 查看消息信息’。        // 打印:删除消息信息 查看消息信息</script>

原生js取消事件冒泡


   try{
e.stopPropagation();//非IE浏览器
}
catch(e){
window.event.cancelBubble = true;//IE浏览器
}

vue.js取消事件冒泡


<div @click.stop="doSomething($event)">vue取消事件冒泡</div>

2. 事件代理(委托)


a. 为什么要用事件委托:


比如ul下有100个li,用for循环遍历所有的li,然后给它们添加事件,需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;


如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;


b. 事件委托的原理


事件委托:利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上,这样点击子元素时发现其本身没有相应事件就到父元素上寻找作出相应。这样做的优势有:


1、减少DOM操作,提高性能。


2、随时可以添加子元素,添加的子元素会自动有相应的处理事件。


<div id="box">
<input type="button" id="add" value="添加" />
<input type="button" id="remove" value="删除" />
<input type="button" id="move" value="移动" />
<input type="button" id="select" value="选择" />
</div>
方式一:需要4次dom操作
window.onload = function () {
var Add = document.getElementById("add");
var Remove = document.getElementById("remove");
var Move = document.getElementById("move");
var Select = document.getElementById("select");
Add.onclick = function () { alert('添加'); }; Remove.onclick = function () { alert('删除'); }; Move.onclick = function () { alert('移动'); }; Select.onclick = function () { alert('选择'); } }
方式二:委托它们父级代为执行事件
window.onload = function(){
var oBox = document.getElementById("box");
oBox.onclick = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLocaleLowerCase() == 'input'){
switch(target.id){
case 'add' :
alert('添加');
break;
case 'remove' :
alert('删除');
break;
case 'move' :
alert('移动');
break;
case 'select' :
alert('选择');
break;
}
}
}

}
用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的

3. 事件捕获


会从document开始触发,一级一级往下传递,依次触发,直到真正事件目标为止。


    <div> <button>            <p>点击捕获</p>        </button></div>    <script>        var oP = document.querySelector('p');        var oB = document.querySelector('button');        var oD = document.querySelector('div');        var oBody = document.querySelector('body');        oP.addEventListener('click', function () {            console.log('p标签被点击')        }, true);        oB.addEventListener('click', function () {            console.log("button被点击")        }, true);        oD.addEventListener('click', function () {            console.log('div被点击')        }, true);        oBody.addEventListener('click', function () {            console.log('body被点击')        }, true);    </script>    点击<p>点击捕获</p>,打印的顺序是:body=>div=>button=>p</body>

流程:先捕获,然后处理,然后再冒泡出去。


2. 原型链



1. 原型对象诞生原因和本质


为了解决无法共享公共属性的问题,所以要设计一个对象专门用来存储对象共享的属性,那么我们叫它「原型对象


原理:构造函数加一个属性叫做prototype,用来指向原型对象,我们把所有实例对象共享的属性和方法都放在这个构造函数的prototype属性指向的原型对象中,不需要共享的属性和方法放在构造函数中。实现构造函数生成的所有实例对象都能够共享属性。


构造函数:私有属性
原型对象:共有属性

2.  彼此之间的关系


构造函数中一属性prototype:指向原型对象,而原型对象一constructor属性,又指回了构造函数。

每个构造函数生成的实例对象都有一个proto属性,这个属性指向原型对象。


那原型对象的_proto_属性指向谁?-> null


3. 原型链是什么?


顾名思义,肯定是一条链,既然每个对象都有一个_proto_属性指向原型对象,那么原型对象也有_proto_指向原型对象的原型对象,直到指向上图中的null,这才到达原型链的顶端。


4. 原型链和继承使用场景


原型链主要用于继承,实现代码复用。因为js算不上是面向对象的语言,继承是基于原型实现而不是基于类实现的,


a. 判断函数的原型是否在对象的原型链上


对象 instanceof 函数(不推荐使用)


b. 创建一个新对象,该新对象的隐式原型指向指定的对象


Object.create(对象)


var obj = Object.create(Object.prototype);


obj.__proto__ === Object.prototype


c. new的实现


d. es6的class A extends B 


因为es6-没有类和继承的概念。js实现继承本质是把js中的对象构造函数在自己的脑中抽象成一个类,然后使用构造函数的protptype属性封装出一个类(另一个构造函数),使之完美继承前一构造函数的所有属性和方法。因为构造函数能new出一个具体的对象实例,这就在js中实现了现代化的面向对象和继承。


3. 闭包和垃圾回收机制


闭包的概念


  function f1(){    var n=999;    function f2(){      alert(n);    }    return f2;  }  var result=f1();  result(); // 999

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。


既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!


作用:一是前面提到的可以读取函数内部的变量,二是让这些变量的值始终保持在内存中,主要用来封装私有变量, 提供一些暴露的接口


垃圾回收


**垃圾回收机制:JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象

**


**注意点:**对于内存的管理,Javascript与C语言等底层语言JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理,


实现的原理:由于 f2 中引用了 相对于自己的全局变量 n ,所以 f2 会一直存在内存中,又因为 n 是 f1 中的局部变量,也就是说 f2 依赖 f1,所以说 f1 也会一直存在内存中,并不像普通函数那样,调用后变量便被垃圾回收了。


所以说,在setTimeout中的函数引用了外层 for循环的变量 i,导致 i 一直存在内存中,不被回收,所以等到JS队列执行 函数时,i 已经是 10了,所以最终打印 10个10。


五、使用闭包的注意点


1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。


for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
打印:5个6,原因js事件执行机制
办法一:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})(i);
}
打印:依次输出1到5
原因:因为实际参数跟定时器内部的i有强依赖。通过闭包,将i的变量驻留在内存中,当输出j时,
引用的是外部函数的变量值i,i的值是根据循环来的,执行setTimeout时已经确定了里面的的输出了。办法二:
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
打印:依次输出1到5因为for循环头部的let不仅将i绑定到for循环中,事实上它将其重新绑定到循环体的每一次迭代中,
确保上一次迭代结束的值重新被赋值。
setTimeout里面的function()属于一个新的域,
通过var定义的变量是无法传入到这个函数执行域中的,
通过使用let来声明块变量能作用于这个块,所以function就能使用i这个变量了;
这个匿名函数的参数作用域和for参数的作用域不一样,是利用了这一点来完成的。
这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。

4. js事件执行机制


事件循环的过程如下:



  1. JS引擎(唯一主线程)按顺序解析代码,遇到函数声明,直接跳过,遇到函数调用,入栈;

  2. 如果是同步函数调用,直接执行得到结果,同步函数弹出栈,继续下一个函数调用;

  3. 如果是异步函数调用,分发给Web API(多个辅助线程),异步函数弹出栈,继续下一个函数调用;

  4. Web API中,异步函数在相应辅助线程中处理完成后,即异步函数达到触发条件了(比如setTimeout设置的10s后),如果异步函数是宏任务,则入宏任务消息队列,如果是微任务,则入微任务消息队列;

  5. Event Loop不停地检查主线程的调用栈与回调队列,当调用栈空时,就把微任务消息队列中的第一个任务推入栈中执行,执行完成后,再取第二个微任务,直到微任务消息队列为空;然后

    去宏任务消息队列中取第一个宏任务推入栈中执行,当该宏任务执行完成后,在下一个宏任务执行前,再依次取出微任务消息队列中的所有微任务入栈执行。

  6. 上述过程不断循环,每当微任务队列清空,可作为本轮事件循环的结束。




链接:https://juejin.cn/post/6987429092542922783

收起阅读 »

项目中实用的前端性能优化

一、CDN 1. CDN的概念 CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性...
继续阅读 »

一、CDN


1. CDN的概念


CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。


典型的CDN系统由下面三个部分组成:



  • 分发服务系统: 最基本的工作单元就是Cache设备,cache(边缘cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数量、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • **运营管理系统:**运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。


2. CDN的作用


CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。


(1)在性能方面,引入CDN的作用在于:



  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快

  • 部分资源请求分配给了CDN,减少了服务器的负载


(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:



  • 针对DDoS:通过监控分析异常流量,限制其请求频率

  • 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信


除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。


3. CDN的原理


CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入 http://www.test.com 的解析过程如下:


(1) 检查浏览器缓存


(2)检查操作系统缓存,常见的如hosts文件


(3)检查路由器缓存


(4)如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询


(5)如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:



  • 根服务器返回顶级域名(TLD)服务器如.com.cn.org等的地址,该例子中会返回.com的地址

  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test的地址

  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标IP,本例子会返回http://www.test.com的地址

  • Local DNS Server会缓存结果,并返回给用户,缓存在系统中


CDN的工作原理:


(1)用户未使用CDN缓存资源的过程:



  1. 浏览器通过DNS对域名进行解析(就是上面的DNS解析过程),依次得到此域名对应的IP地址

  2. 浏览器根据得到的IP地址,向域名的服务主机发送数据请求

  3. 服务器向浏览器返回响应数据


(2)用户使用CDN缓存资源的过程:



  1. 对于点击的数据的URL,经过本地DNS系统的解析,发现该URL对应的是一个CDN专用的DNS服务器,DNS系统就会将域名解析权交给CNAME指向的CDN专用的DNS服务器。

  2. CND专用DNS服务器将CND的全局负载均衡设备IP地址返回给用户

  3. 用户向CDN的全局负载均衡设备发起数据请求

  4. CDN的全局负载均衡设备根据用户的IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求

  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的IP地址返回给全局负载均衡设备

  6. 全局负载均衡设备把服务器的IP地址返回给用户

  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。


如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。


image


CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。


4. CDN的使用场景



  • **使用第三方的CDN服务:**如果想要开源一些项目,可以使用第三方的CDN服务

  • **使用CDN进行静态资源的缓存:**将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。

  • **直播传送:**直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。


二、懒加载


1. 懒加载的概念


懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。


如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。


2. 懒加载的特点



  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。

  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。

  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。


3. 懒加载的实现原理


图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。


注意:data-xxx 中的xxx可以自定义,这里我们使用data-src来定义。


懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。


使用原生JavaScript实现懒加载:


知识点:


(1)window.innerHeight 是浏览器可视区的高度


(2)document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离


(3)imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)


(4)图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;


图示:


image


代码实现:


<div>
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
</div>
<script>
var imgs = document.querySelectorAll('img');
function lozyLoad(){
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
var winHeight= window.innerHeight;
for(var i=0;i < imgs.length;i++){
if(imgs[i].offsetTop < scrollTop + winHeight ){
imgs[i].src = imgs[i].getAttribute('data-src');
}
}
}
window.onscroll = lozyLoad;
</script>

4. 懒加载与预加载的区别


这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。



  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。

  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。


三、回流与重绘


1. 回流与重绘的概念及触发条件


(1)回流


当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流


下面这些操作会导致回流:



  • 页面的首次渲染

  • 浏览器的窗口大小发生变化

  • 元素的内容发生变化

  • 元素的尺寸或者位置发生变化

  • 元素的字体大小发生变化

  • 激活CSS伪类

  • 查询某些属性或者调用某些方法

  • 添加或者删除可见的DOM元素


在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:



  • 全局范围:从根节点开始,对整个渲染树进行重新布局

  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局


(2)重绘


当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘


下面这些操作会导致回流:



  • color、background 相关属性:background-color、background-image 等

  • outline 相关属性:outline-color、outline-width 、text-decoration

  • border-radius、visibility、box-shadow


注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。


2. 如何避免回流与重绘?


减少回流与重绘的措施:



  • 操作DOM时,尽量在低层级的DOM节点进行操作

  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局

  • 使用CSS的表达式

  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。

  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素

  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。

  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制


浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列


浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。


上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。


3. 如何优化动画?


对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。


4. documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?


MDN中对documentFragment的解释:



DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。



当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。


四、节流与防抖


1. 对节流与防抖的理解



  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。


防抖函数的应用场景:



  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次

  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce


节流函数的****适⽤场景:



  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动

  • 缩放场景:监控浏览器resize

  • 动画场景:避免短时间内多次触发动画引起性能问题


2. 实现节流函数和防抖函数


函数防抖的实现:


function debounce(fn, wait) {
var timer = null;

return function() {
var context = this,
args = [...arguments];

// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}

// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}

函数节流的实现:


// 时间戳版
function throttle(fn, delay) {
var preTime = Date.now();

return function() {
var context = this,
args = [...arguments],
nowTime = Date.now();

// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}

// 定时器版
function throttle (fun, wait){
let timeout = null
return function(){
let context = this
let args = [...arguments]
if(!timeout){
timeout = setTimeout(() => {
fun.apply(context, args)
timeout = null
}, wait)
}
}
}

五、图片优化


1. 如何对项目中的图片进行优化?



  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。

  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。

  3. 小图使用 base64 格式

  4. 将多个图标文件整合到一张图片中(雪碧图)

  5. 选择正确的图片格式:





    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好

    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替

    • 照片使用 JPEG




2. 常见的图片格式及使用场景


(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。


(2)GIF是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。


(3)JPEG是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。


(4)PNG-8是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。


(5)PNG-24是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。


(6)SVG是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。


(7)WebP是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。



  • 在无损压缩的情况下,相同质量的WebP图片,文件大小要比PNG小26%;

  • 在有损压缩的情况下,具有相同图片精度的WebP图片,文件大小要比JPEG小25%~34%;

  • WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。


六、Webpack优化


1. 如何提⾼webpack的打包速度**?**


(1)优化 Loader


对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。


首先我们优化 Loader 的文件搜索范围


module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}

对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。


当然这样做还不够,还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间


loader: 'babel-loader?cacheDirectory=true'

(2)HappyPack


受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。


HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了


module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]

(3)DllPlugin


DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:


// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}

然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中


// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}

(4)代码压缩


在 Webpack3 中,一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。


在 Webpack4 中,不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。


(5)其他


可以通过一些小的优化点来加快打包速度



  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面

  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径

  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助


2. 如何减少 Webpack 打包体积


(1)按需加载


在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。


按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。


(2)Scope Hoisting


Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。


比如希望打包两个文件:


// test.js
export const a = 1
// index.js
import { a } from './test.js'

对于这种情况,打包出来的代码会类似这样:


[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]

但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:


[
/* 0 */
function (module, exports, require) {
//...
}
]

这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了:


module.exports = {
optimization: {
concatenateModules: true
}
}

(3)Tree Shaking


Tree Shaking 可以实现删除项目中未被引用的代码,比如:


// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'

对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。


如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。


3. 如何⽤webpack来优化前端性能?


⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。



  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css

  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径

  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现

  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存

  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码


4. 如何提⾼webpack的构建速度?



  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码

  2. 通过 externals 配置来提取常⽤库

  3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。

  4. 使⽤ Happypack 实现多线程加速编译

  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度

  6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码

链接:https://juejin.cn/post/6987268877096845320

收起阅读 »

『前端BUG』—— 本地代理导致会话cookie中的数据丢失

vue
问题在本地用代理请求服务端接口,解决跨域问题后,发生了一件极其诡异的事情,明明登录成功了,但是请求每个接口都返回未登录的报错信息。原因该套系统是采用会话cookie进行登录用户的身份认证,故查看每个请求的Request Headers中的cookie的值,发现...
继续阅读 »

问题

在本地用代理请求服务端接口,解决跨域问题后,发生了一件极其诡异的事情,明明登录成功了,但是请求每个接口都返回未登录的报错信息。

原因

该套系统是采用会话cookie进行登录用户的身份认证,故查看每个请求的Request Headers中的cookie的值,发现原本如下图中的红框区域的SESSION不见了。

image.png

而明明登录接口的Response Headers中是存在set-cookie。

image.png

set-cookie会是把其值中的SESSION存储到浏览器的cookie中,存储成功后,每次请求服务端时,都会去浏览器中的cookie中读取SESSION,然后通过Request Headers中的cookie传递到服务端,完成身份认证。

另外set-cookie的值是服务端设置的,我们来认真观察一下set-cookie的值

SESSION=NjE1MTNmZWI1N2ExNDYyZGE4MWE0YmZjNjgwMmFmZGY=; Path=/api/operation/; HttpOnly; SameSite=Lax
复制代码

里面除SESSION,还有PathHttpOnlySameSite,而Path就是导致SESSION无法存储到客户端中的元凶,其中Path的值/api/operation/表示该cookie只有在用请求路径的前缀为/api/operation/才能使用。

回到代理配置中一看,

proxy: getProxy({
'/dev': {
target: 'https://xxx.xxx.com',
pathRewrite: { '^/dev': '/api' },
secure: false,
changeOrigin: true
}
}),
复制代码

代理后,请求服务端的地址为xxx.xxx.com/dev/operati… ,其路径为 dev/operation/xxx,自然与/api/operation/不匹配,导致该cookie无法使用,自然无法将SESSION保存到浏览器的cookie中。

解决

找到原因了,问题很好解决,只要更改一下代理配置。

proxy: getProxy({
'/api': {
target: 'https://xxx.xxx.com',
pathRewrite: { '^/api': '/api' },
secure: false,
changeOrigin: true
}
}),
复制代码

此外不要忘记更改 axios 的配置中的baseURL,将其改为/api/


链接:https://juejin.cn/post/6987408221669425189

收起阅读 »

「自我检验」输入URL发生了啥?希望你顺便懂这15个知识点

输入URL发生了啥? 1、浏览器的地址栏输入URL并按下回车。 2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。 3、DNS解析URL对应的IP。 4、根据IP建立TCP连接(三次握手)。 5、HTTP发起请求。 6、服务器处理请求,浏览器接收HT...
继续阅读 »

输入URL发生了啥?



  • 1、浏览器的地址栏输入URL并按下回车。

  • 2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。

  • 3、DNS解析URL对应的IP。

  • 4、根据IP建立TCP连接(三次握手)。

  • 5、HTTP发起请求。

  • 6、服务器处理请求,浏览器接收HTTP响应。

  • 7、渲染页面,构建DOM树。

  • 8、关闭TCP连接(四次挥手)。


永恒钻石


1. 浏览器应该具备什么功能?



  • 1、网络:浏览器通过网络模块来下载各式各样的资源,例如HTML文本,JavaScript代码,CSS样式表,图片,音视频文件等。网络部分尤为重要,因为它耗时长,而且需要安全访问互联网上的资源

  • 2、资源管理:从网络下载,或者本地获取到的资源需要有高效的机制来管理他们。例如如何避免重复下载,资源如何缓存等等

  • 3、网页浏览:这是浏览器的核心也是最基本的功能,最重要的功能。这个功能决定了如何将资源转变为可视化的结果

  • 4、多页面管理

  • 5、插件与管理

  • 6、账户和同步

  • 7、安全机制

  • 8、开发者工具


浏览器的主要功能总结起来就是一句话:将用户输入的url转变成可视化的图像


2. 浏览器的内核


在浏览器中有一个最重要的模块,它主要的作用是把一切请求回来的资源变成可视化的图像,这个模块就是浏览器内核,通常他也被称为渲染引擎


下面是浏览器内核的总结:



  • 1、IE:Trident

  • 2、Safari:WebKit。WebKit本身主要是由两个小引擎构成的,一个正是渲染引擎“WebCore”,另一个则是javascript解释引擎“JSCore”,它们均是从KDE的渲染引擎KHTML及javascript解释引擎KJS衍生而来。

  • 3、Chrome:Blink。在13年发布的Chrome 28.0.1469.0版本开始,Chrome放弃Chromium引擎转而使用最新的Blink引擎(基于WebKit2——苹果公司于2010年推出的新的WebKit引擎),Blink对比上一代的引擎精简了代码、改善了DOM框架,也提升了安全性。

  • 4、Opera:2013年2月宣布放弃Presto,采用Chromium引擎,又转为Blink引擎

  • 5、Firefox:Gecko


3. 进程和线程



  • 1、进程:程序的一次执行,它占有一片独有的内存空间,是操作系统执行的基本单元

  • 2、线程:是进程内的一个独立执行单元,是CPU调度的最小单元,程序运行基本单元

  • 3、一个进程中至少有一个运行的线程:主线程。它在进程启动后自动创建

  • 4、一个进程可以同时运行多个线程,我们常说程序是多线程运行的,比如你使用听歌软件,这个软件就是一个进程,而你在这个软件里听歌收藏歌点赞评论,这就是一个进程里的多个线程操作

  • 5、一个进程中的数据可以供其中的多个线程直接共享,但是进程与进程之间的数据时不能共享

  • 6、JS引擎是单线程运行


4. 浏览器渲染引擎的主要模块



  • 1、HTML解析器:解释HTML文档的解析器,主要作用是将HTML文本解释为DOM树

  • 2、CSS解析器:它的作用是为DOM中的各个元素对象计算出样式信息,为布局提供基础设施

  • 3、JavaScript引擎:JavaScript引擎能够解释JavaScript代码,并通过DOM接口和CSS接口来修改网页内容 和样式信息,从而改变渲染的结果

  • 4、布局(layout):在DOM创建之后,WebKit需要将其中的元素对象同样式信息结合起来,计算他们的大小位置等布局信息,形成一个能表达着所有信息的内部表示模型

  • 5、绘图模块(paint):使用图形库将布局计算后的各个网页的节点绘制成图像结果


5. 大致的渲染过程


第1题的第7点,渲染页面,构建DOM树,接下来说说大致的渲染过程



  • 1、浏览器会从上到下解析文档

  • 2、遇见HTML标记,调用HTML解析器解析为对应的token(一个token就是一个标签文本的序列化)并构建DOM树(就是一块内存,保存着tokens,建立他们之间的关系)

  • 3、遇见style/link标记调用相应解析器处理CSS标记,并构建出CSS样式树

  • 4、遇见script标记,调用JavaScript引擎处理script标记,绑定事件,修改DOM树/CSS树等

  • 5、将DOM树与CSS合并成一个渲染树

  • 6、根据渲染树来渲染,以计算每个节点的几何信息(这一过程需要依赖GPU)

  • 7、最终将各个节点绘制在屏幕上


02_浏览器渲染过程的副本.png


至尊星耀


6. CSS阻塞情况以及优化



  • 1、style标签中的样式:由HTML解析器进行解析,不会阻塞浏览器渲染(可能会产生“闪屏现象”),不会阻塞DOM解析

  • 2、link引入的CSS样式:由CSS解析器进行解析,阻塞浏览器渲染,会阻塞后面的js语句执行,不阻塞DOM的解析

  • 3、优化:使用CDN节点进行外部资源加速,对CSS进行压缩,优化CSS代码(不要使用太多层选择器)


注意:看下图,HTMLCSS是并行解析的,所以CSS不会阻塞HTML解析,但是,会阻塞整体页面的渲染(因为最后要渲染必须CSS和HTML一起解析完并合成一处)
02_浏览器渲染过程的副本.png


7. JS阻塞问题



  • 1、js会阻塞后续DOM的解析,原因是:浏览器不知道后续脚本的内容,如果先去解析了下面的DOM,而随后的js删除了后面所有的DOM,那么浏览器就做了无用功,浏览器无法预估脚本里面具体做了什么操作,例如像document.write这种操作,索性全部停住,等脚本执行完了,浏览器再继续向下解析DOM

  • 2、js会阻塞页面渲染,原因是:js中也可以给DOM设置样式,浏览器等该脚本执行完毕,渲染出一个最终结果,避免做无用功。

  • 3、js会阻塞后续js的执行,原因是维护依赖关系,例如:必须先引入jQuery再引入bootstrap


8. 资源加载阻塞


无论css阻塞,还是js阻塞,都不会阻塞浏览器加载外部资源(图片、视频、样式、脚本等)


原因:浏览器始终处于一种:“先把请求发出去”的工作模式,只要是涉及到网络请求的内容,无论是:图片、样式、脚本,都会先发送请求去获取资源,至于资源到本地之后什么时候用,由浏览器自己协调。这种做法效率很高。


9. 为什么CSS解析顺序从右到左


如果是从左到右的话:



  • 1、第一次从爷节点 -> 子节点 -> 孙节点1

  • 2、第一次从爷节点 -> 子节点 -> 孙节点2

  • 3、第一次从爷节点 -> 子节点 -> 孙节点3


如果三次都匹配不到的话,那至少也得走三次:爷节点 -> 子节点 -> 孙节点,这就做了很多无用功啊。


截屏2021-07-18 下午9.33.13.png


如果是从右到左的话:



  • 1、第一次从孙节点1,找不到,停止

  • 2、第一次从孙节点2,找不到,停止

  • 3、第一次从孙节点3,找不到,停止


这样的话,尽早发现找不到,尽早停止,可以少了很多无用功。


截屏2021-07-18 下午9.37.16.png


最强王者


10. 什么是重绘回流



  • 1、重绘:重绘是一个元素外观的改变所触发的浏览器行为,例如改变outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,所以并不一定伴随重排。

  • 2、回流:渲染对象在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排,或回流

  • 3、"重绘"不一定需要"重排",比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。

  • 4、"重排"大多数情况下会导致"重绘",比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。


11. 触发重绘的属性


* color * background * outline-color * border-style * background-image * outline * border-radius * background-position * outline-style * visibility * background-repeat * outline-width * text-decoration * background-size * box-shadow


12. 触发回流的属性


* width * top * text-align * height * bottom * overflow-y * padding * left * font-weight * margin * right * overflow * display * position * font-family * border-width * float * line-height * border * clear * vertival-align * min-height * white-space


13. 常见触发重绘回流的行为



  • 1、当你增加、删除、修改 DOM 结点时,会导致 Reflow , Repaint。

  • 2、当你移动 DOM 的位置

  • 3、当你修改 CSS 样式的时候。

  • 4、当你Resize窗口的时候(移动端没有这个问题,因为移动端的缩放没有影响布局视口)

  • 5、当你修改网页的默认字体时。

  • 6、获取DOM的height或者width时,例如clientWidth、clientHeight、clientTop、clientLeft、offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、scrollTop、scrollLeft、scrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、getBoundingClientRect()、scrollTo()


14. 针对重绘回流的优化方案



  • 1、元素位置移动变换时尽量使用CSS3的transform来代替top,left等操作

  • 2、不要使用table布局

  • 3、将多次改变样式属性的操作合并成一次操作

  • 4、利用文档素碎片(documentFragment),vue使用了该方式提升性能

  • 5、动画实现过程中,启用GPU硬件加速:transform:tranlateZ(0)

  • 6、为动画元素新建图层,提高动画元素的z-index

  • 7、编写动画时,尽量使用requestAnimationFrame


15. 浏览器缓存分类


image.png



  1. 强缓存

    1. 不会向服务器发送请求,直接从本地缓存中获取数据

    2. 请求资源的的状态码为: 200 ok(from memory cache)

    3. 优先级:cache-control > expires



  2. 协商缓存

    1. 向服务器发送请求,服务器会根据请求头的资源判断是否命中协商缓存

    2. 如果命中,则返回304状态码通知浏览器从缓存中读取资源

    3. 优先级:Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304
链接:https://juejin.cn/post/6986416221323264030

收起阅读 »

今天聊:大厂如何用一道编程题考察候选人水平

进入正题 面试环节对面试官的一些挑战 面试官和候选人的知识结构可能有差异 => 可能会错过优秀的人 遇到「面霸」,频繁面试刷题,但是实际能力一般 => 招到不合适的人 要在短短半个小时到一个小时内判断一个人,其实很难 相对靠谱的做法 笔试:"...
继续阅读 »

进入正题


面试环节对面试官的一些挑战



  • 面试官和候选人的知识结构可能有差异 => 可能会错过优秀的人

  • 遇到「面霸」,频繁面试刷题,但是实际能力一般 => 招到不合适的人

  • 要在短短半个小时到一个小时内判断一个人,其实很难


相对靠谱的做法



  • 笔试:"Talk is cheap, show me the code"


笔试常见的问题



  • 考通用算法,Google 能直接搜到,失去考察意义

  • 题目难度设计有问题。要么满分,要么零分,可能错过还不错的同学

  • 和实际工作内容脱节


我认为好的笔试题



  • 上手门槛低,所有人多多少少都能写一点,不至于开天窗

  • 考点多,通过一道题可以基本摸清候选人的代码综合素养

  • 给高端的人有足够的发挥空间。同样的结果,不同的实现方式可以看出候选人的技术深度


我常用的一道笔试题


很普通的一道题


// 假设本地机器无法做加减乘除运算,需要通过远程请求让服务端来实现。
// 以加法为例,现有远程API的模拟实现

const addRemote = async (a, b) => new Promise(resolve => {
setTimeout(() => resolve(a + b), 1000)
});

// 请实现本地的add方法,调用addRemote,能最优的实现输入数字的加法。
async function add(...inputs) {
// 你的实现
}

// 请用示例验证运行结果:
add(1, 2)
.then(result => {
console.log(result); // 3
});


add(3, 5, 2)
.then(result => {
console.log(result); // 10
})

答案一
最基本的答案,如果写不出来,那大概率是通过不了了


async function add(...args) {
let res = 0;
if (args.length <= 2) return res;

for (const item of args) {
res = await addRemote(res, item);
}
return res;
}

递归版本


async function add(...args) {
let res = 0;
if (args.length === 0) return res;
if (args.length === 1) return args[0];

const a = args.pop();
const b = args.pop();
args.push(await addRemote(a, b));
return add(...args);
}

常见的问题:



  • 没有判断入参个数

  • 仍然用了本地加法


答案二
有候选人的答案如下:


// Promise链式调用版本
async function add(...args) {
return args.reduce((promiseChain, item) => {
return promiseChain.then(res => {
return addRemote(res, item);
});
}, Promise.resolve(0));

}

从这个实现可以看出:



  • 对 Array.prototype.reduce 的掌握

  • 对于 Promise 链式调用的理解

  • 考察候选人对 async function 本质的理解


这个版本至少能到 70 分


答案三
之前的答案结果都是对的,但是我们把耗时打出来,可以看到耗时和参数个数成线性关系,因为所有计算都是串行的,显然不是最优的解



更好一点的答案:


function add(...args) {
if (args.length <= 1) return Promise.resolve(args[0])
const promiseList = []
for (let i = 0; i * 2 < args.length - 1; i++) {
const promise = addRemote(args[i * 2], args[i * 2 + 1])
promiseList.push(promise)
}

if (args.length % 2) {
const promise = Promise.resolve(args[args.length - 1])
promiseList.push(promise)
}

return Promise.all(promiseList).then(results => add(...results));
}


可以看到很明显的优化。


答案四
还能再优化吗?
有些同学会想到加本地缓存


const cache = {};

function addFn(a, b) {
const key1 = `${a}${b}`;
const key2 = `${b}${a}`;
const cacheVal = cache[key1] || cache[key2];

if (cacheVal) return Promise.resolve(cacheVal);

return addRemote(a, b, res => {
cache[key1] = res;
cache[key2] = res;
return res;
});
}

加了缓存以后,我们再第二次执行相同参数加法时,可以不用请求远端,直接变成毫秒级返回



还能再优化吗?交给大家去思考


其他考察点


有些时候会让候选人将代码提交到 Github 仓库,以工作中一个实际的模块标准来开发,可以考察:



  • git 操作,commit 规范

  • 工程化素养

  • 是否有单元测试

  • 覆盖率是否达标

  • 依赖的模块版本如何设置

  • 如何配置 ci/cd

  • 文档、注释

  • ...


更加开放的一种笔试形式



  • 给一道题目,让候选人建一个 Github 仓库来完成

  • 题目有一定难度,但是可以 Google,也可以用三方模块,和我们平时做项目差不多

  • 通常面向级别较高的候选人


实际题目


// 有一个 10G 文件,每一行是一个时间戳,
// 现在要在一台 2C4G 的机器上对它进行排序,输出排序以后的文件

// 案例输入
// 1570593273487
// 1570593273486
// 1570593273488
// …

// 输出
// 1570593273486
// 1570593273487
// 1570593273488
// …



先看一个答案,看看哪里有问题


async function sort(inputFile, outputFile) {
const input = fs.createReadStream(inputFile);
const rl = readline.createInterface({ input });
const arr = [];
for await (const line of rl) {
const item = Number(line);
arr.push(item);
}
arr.sort((a, b) => a - b);

fs.writeFileSync(outputFile, arr.join('\n'));
}

10GB 的文件无法一次性放进内存里处理,内存只有 4GB


再看一个神奇的答案,只有一行代码,而且从结果来说是正确的。但不是我们笔试想要的答案。


const cp = require('child_process');

function sort(inputFile, outputFile) {
cp.exec(`sort -n ${inputFile} > ${outputFile}`);
}

解题思路



  • 既然没办法一次性在内存中排序,那我们能否将 10GB 的文件拆分成若干个小文件

  • 小文件先分别排序,然后再合并成一个大的文件


再将问题拆解



  • 拆分大文件到小文件

  • 小文件在内存里排序

  • 合并所有小文件成一个整体排序过的大文件


本题最难的点在于如何合并所有小文件。代码如何实现?



  • 这里需要用到一种数据结构:堆

  • 堆:就是用数组实现的一个二叉树

  • 堆分为:最大堆和最小堆,下面是一个最小堆(父节点小于它的子节点)


image.png


堆有一些特性:



  • 对于一个父节点来说

    • 左节点位置:父节点位置 * 2 + 1

    • 右节点位置:父节点位置 * 2 + 2



  • 很容易查找最大值 / 最小值


我们尝试把下面数组构造成一个最小堆


image.png



  • 从最后一个非叶子节点开始往前处理

  • 10 比 5 大,所以交换它们的位置


image.png



  • 然后是节点 2,符合要求不需要处理

  • 最后到顶点 3,它比左子节点大,所以要交换


image.png


完整的实现参考:github.com/gxcsoccer/e…
image.png







链接:https://juejin.cn/post/6987529814324281380

收起阅读 »

【环信IM集成指南】Android 端常见问题整理

1、如何修改系统通知中的头像和用户名系统通知是在主module中自己写的,demo中是AgreeMsgDelegate,InviteMsgDelegate,OtherMsgDelegate中去修改头像和用户名。2. 如何修改会话列表中系统消息的头像和消息里的环...
继续阅读 »

1、如何修改系统通知中的头像和用户名

系统通知是在主module中自己写的,demo中是AgreeMsgDelegate,InviteMsgDelegate,OtherMsgDelegate中去修改头像和用户名。



2. 如何修改会话列表中系统消息的头像和消息里的环信ID?

在创建系统消息的时候,去修改设设置的username。
系统消息的创建一般在好友监听和群监听中。



3. app端使用token登录,怎么获取单个用户的token?

获取单个用户的token需要调rest接口去获取。
文档中的Request Body要修改一下使用:

{"grant_type": "password",

"username": "omg2",

"password": "123456"}

参考文档:http://docs-im.easemob.com/im/server/ready/user#获取管理员权限


4. 设置群组全员禁言后,怎么获取该群组是否是全员禁言状态?
调用获取群组详情的api拿到EMGroup对象,然后再调用isAllMemberMuted去获取是否全员禁言。

EMGroup group = EMClient.getInstance().groupManager().getGroupFromServer(groupId);

group.isAllMemberMuted();


5. 登录成功后获取不到群组信息,后台查看用户是在群内,怎么解决?
检查下是否是从本地获取的群组信息,当用户打开app时,需要先从服务器拉取群组信息(放子线程中)。




6. 何时调用本地群组信息,何时调用从服务器获取群组信息?
打开App,从服务器拉取(从服务器拉取之后,sdk会保存在本地,再调用本地获取能拿到信息),当群成员更新的时候,从服务器获取。


7. 如何设置群组永久禁言?
将禁言时间设置为-1即可。
EMClient.getInstance().groupManager().muteGroupMembers(groupId, muteMembers, -1);


8. 获取群成员数总是来回变化?

检查看下是否使用的是获取完整的群成员列表。
参考文档:https://docs-im.easemob.com/im/android/basics/group#获取完整的群成员列表


9. 消息能发送成功,但是接收不到别人发给我的消息。
检查下注册的环信id是否含有大写字母,如果含有大写字母的话,需要在登录的时候去转换成小写,发送消息等操作都是需要使用小写字母的。


10. 创建聊天室,显示you hava no permission to do this
创建聊天室,只能使用rest接口去创建。
参考文档:https://docs-im.easemob.com/im/server/basics/chatroom#创建聊天室

11. 设置的自动同意添加好友,为什么添加之后好友列表里查找不到?

回答:用户A添加 用户 B为好友,如果用户B是离线状态的话,用户A的好友列表是里不显示用户B的,用户 B上线之后,用户A的好友列表中会出现用户B。


12. 怎么去设置头像和昵称?
EaseIMkit设置头像和昵称:
1.头像和昵称是Easeimkit处理的,我们只需要在application中调用easeimkit的setUserProvider根据环信id,将本地存储的头像和昵称封装到EaseUser中返回给Easeimkit即可(具体可参考demo)
2.getUserInfo中的处理逻辑是:根据username先去本地获取,如果本地获取不到,再从服务器获取,并保存到本地或者做一个三级缓存,然后,再刷新会话列表。(注:getuserinfo是同步的)


13. 如何设置是听筒模式还是扬声器?
回答:easeimkit中isSpeakerOpened()回调中去设置。



14. 怎么发送红包消息?
1.发送自定义消息并携带扩展字段(扩展字段用来判断是显示已领取还是未领取),设置红包的自定义布局(已领取和未领取),默认未领取。
2.当用户点击领取红包之后(修改扩展字段的为已领取状态),将未领取隐藏显示已领取。并发送cmd消息,在接收到cmd消息后 ,修改扩展字段的为已领取状态,并刷新ui。
3.当用户杀掉app进来之后,根据扩展字段来决定显示已领取还是未领取。


15. 发送文件大小超过20M怎么办?
发送视频文件(在EMOption中设置不上传不下载),先去上传到你们的服务器上保存,拿到url放在消息里发送给对方(setRemoteUrl),对方收到之后解析消息里的url去下载视频文件。(注:这种方法会导致所有的附件都不下载不上传)



16. demo中使用的是百度地图,我想使用高德地图,怎么使用?
需要重写聊天页面地图的点击事件,跳转到高德就可以。



17. 在聊天页面点击视频,没有反应?
需要去自定义一个fragment去继承EaseChatFragment,重写EaseChatFragment中的selectVideoFromLocal。(具体实现可以参考demo中的ChatFragment)



18. 拉取漫游数据后,展示的时间是乱的,没有按时间排序?
1. 先打印下漫游数据和本地缓存中的数据是否一致?
2.检查下在EMOption中是否设置
option.setSortMessageByServerTime(false)



19.EMMessage怎么区分离线消息还是在线消息?
EMMessage中没有方法可以去判断是离线消息还是在线消息。
实现方法:
可以在接收消息的监听中,获取到消息的时间戳与本地时间戳做对比,超过一定的时间就算离线消息。 但是前提需要保证本地的时间戳是对的。



20.集成了环信SDK后的安卓App,只要授予了定位权限,一启动就会访问用户的位置信息,如果启动App不访问用户位置信息?
在EMOptions中设置setEnableStatistics为false。



21. gradle的形式引入easeimkit,怎么去监听发送消息成功或者失败?

在easeimkit中没有将发送成功的事件,回调到fragment中,如果用户需要在自定义view中用的话,继承EaseChatRow并重写onMessageSuccess()。



22. 如何实现一键已读功能?
可以调用将所有消息置为已读的api:

EMClient.getInstance().chatManager().markAllConversationsAsRead();
参考文档链接:http://docs-im.easemob.com/im/android/basics/message#未读消息数清零



23. 怎么收不到oppo推送?
1.检查下是否已经安装官网的文档去集成了oppo推送,在控制台搜索
EMPushHelper,是否有[EMPushHelper] uploadTokenInternal success输出,如果没有输出 ,请检查下oppo的集成。
2.在完成第一步的情况下,还是收不到离线推送,请检查下密钥上传的是否正确,console后台上传密钥是:master secret ,在 app中上传的密钥的是app secret。如果不正确请删除重新上传并提交工单提供appkey+证书id,让环信技术支持在后台解禁。


24.如何设置在线消息免打扰?
单个会话的免打扰模式您可以自己去实现,要自己去维护一个免打扰list集合,当监听到有消息时,去判断下是否是免打扰用户,如果是免打扰用户,就不去提醒。
群组免打扰:可以使用rest去设置。("notification_ignore_群组id": true)


25.搜索会话列表如何根据昵称搜索(或者根据其他某个字段去搜索)。
可以把会话显示的昵称(或某个字段)放在会话的扩展里,搜索的时候遍历会话扩展里的昵称。





26.群公告的长度有限制吗?

群公告不能超过512 字符。


27.如何设置群扩展字段

1.通过EMGroupOptions的extField设置的扩展字段。
2.从服务器获取群组信息,获取getExtension
EMGroup group = EMClient.getInstance().groupManager().getGroupFromServer(groupId);group.getExtension();


28.oppo推送报空指针
Process: com.example.is, PID: 24696
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.pm.PackageManager android.content.Context.getPackageManager()' on a null object reference
at com.heytap.mcssdk.d.a(Unknown Source:7)
at com.heytap.mcssdk.d.l(Unknown Source:6)
at com.heytap.mcssdk.d.n(Unknown Source:0)
at com.heytap.msp.push.HeytapPushManager.isSupportPush(Unknown Source:4)
at com.hyphenate.push.platform.oppo.a.b(Unknown Source:0)
at com.hyphenate.push.platform.a.a(Unknown Source:6)
at com.hyphenate.push.EMPushHelper.a(Unknown Source:145)
at com.hyphenate.push.EMPushHelper.register(Unknown Source:35)
at com.hyphenate.chat.EMClient$7.run(Unknown Source:204)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)

需要在application中对oppo进行初始化(如下图)。



29、android端本地在构造图片消息时 可以设置缩略图大小

( EMImageMessageBody.setThumbnailSize()) 也可以在console后台进行设置.

30、接收方接收到图片消息后 为什么remoteUrl和thumbnailUrl是一样的

服务端只存储原图,如果需要下载缩略图得话,在header中添加thumbnail: true”,当服务器看到过来的请求的 header 中包括这个的时候,就会返回缩略图,否则返回原始大图。


31、环信即时推送一次性可以给多少用户推送消息?

回答:一次可以推100个


32、漫游功能可以配置过滤cmd、已读回执等消息 

需要联系环信工作人员进行配置


33、如果是内网环境、物联网定向流量卡等 需要配置域名白名单或者ip白名单 允许访问环信接口 

(以上需要先对接商务验证身份后会提供对应的域名或者ip地址)


34、Android端如果有使用okhttp三方网络框架,没有做特殊处理可能会遇见SSL无法获取到信任证书问题 

可以参考这篇文章解决:https://www.imgeek.org/article/825359148


35、批量获取用户属性一次性最多获取100个id属性 超过的可以分段获取


36、开通敏感词后发现默认词库过滤了自己想要的词汇 

导致消息发送失败或者以*号展示 

这种情况下可以联系环信这边配置敏感词白名单进行过滤(需要提供appkey 和 需要添加白名单的词汇)


37、某些情况下客户端只能允许https协议 

这个有2种解决方式 

1、如果是已经上线的 可以直接找环信这边修改dnsconfig配置 全部换成https协议 

2、如果是开发中的 可以在sdk初始化的时候在EMoptions里面设置onlyhttps


38、关于Android第三方推送对接 

比如极光厂商对接、阿里云推送厂商对接、友盟厂商对接 这些目前来看都是可以实现的 只不过需要客户侧做一些特殊处理,对应推送厂商的sdk可以不用在重复
进行依赖,环信这边只需要在console后台上传证书信息并在端上初始化sdk的时候配置push信息、获取到第三方推送厂商返回的devicetoken并上传给环信进行绑定即可。



41、rest 发送消息可以设置ip白名单

也就是说可以配置自己服务器的ip,除了该服务器可以访问环信其它ip的请求全部过滤,防止重要信息泄露后有人故意往调用发送消息接口(这个在console后台安全配置可以设置)


42、目前android这边发现有些情况下会出现自己只登录了一个设备却老是被其它设备踢下线

查看登录记录后发现是来自同一个设备踢下线的事件,这种情况下就得分析一下是什么原因导致的自踢:
1、集成问题多进程多次初始化sdk会出现类似现象 

2、网络问题a账号登录服务端记录了状态之后,a账号突然没网服务端没法在心跳触发之前即时更新离线状态,这时候网络又恢复进行了重连又进行了登录,这时候服务端本身记录是登录状态,再次登录会把之前的踢下线,这种现象会造成自踢(解决方案 可以联系环信人员配置不自踢)


43、推送扩展字段结构 其中e为完全用户自定义扩展

而数据来源为em_apns_ext字段和em_apns_ext.extern两者有其优先级。

{
"payload":{
"ext":{
"em_apns_ext":{
"em_push_title":"您有一条新消息",
"em_push_content":"您有一条新消息",
"test1":"1",
"test2":"2",
"extern":{
"test3":"3",
"test4":"4"
}
}
}
}
}

自定义负载支持方式为,主动构建如下结构
{
"t":"toUsename",
"f":"fromUsername",
"m":"msg_id",
"g":"group_id",
"e":{}
}


1、当extern不存在时,e内容为em_apns_ext下push服务未使用字段。具体为移除em_push_title,em_push_content,em_push_name,em_push_channel_id,em_huawei_push_badge_class字段后剩余所有。如上则为

{
"e":{"test1":"1","test2":"2"}
}


2、当extern存在时,使用extern下字段。如上则为

{
"e":{"test1":"3","test2":"4"}
}



44.发送语音消息,想要吧amr修改成wav
回答:可以在录制的时候去修改EaseVoiceRecorder


45.用户隐私协议
回答:https://www.easemob.com/protocol
方案:需要将第三方的初始化写在application的public方法中,加一个判断,判断是 第一次安装时,不初始化,当用户点击了同意协议之后,再执行application中的public的初始化。


46.聊天室异常退出2分钟才算离开聊天室,这个能缩短时间么?比如几秒钟
回答:可以设置成0,断线立刻退出聊天室(可以提工单让环信工作人员配置)


47.音视频聊天设置声音外放
protected void openSpeakerOn() {
try {
if (!audioManager.isSpeakerphoneOn())
audioManager.setSpeakerphoneOn(true);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
} catch (Exception e) {
e.printStackTrace();
}
}


48.发送图片成功后,如何获取发送的原图?

回答:1.需要在发送图片的时候设置发送原图;

2.发送图片成功后,可以获取到远程的服务器路径,获取到路径自己下载





49. 如何添加自定义的表情(类似于demo中的兔斯基)
回答:
1.在聊天页面( ChatFragment )中添加:
//添加扩展表情
chatLayout.getChatInputMenu().getEmojiconMenu().addEmojiconGroup(EmojiconExampleGroupData.getData());
2. 在applicaiton中注册一下Delegate。。
EaseMessageTypeSetManager.getInstance()
.addMessageType(EaseExpressionAdapterDelegate.class) //自定义表情


50.出现下图报报错


解决办法:升级一个根目录build.gradle 里面的classpath 'com.huawei.agconnect:agcp:1.4.1.300'



51. 在聊天页面不显示地图的图片?
回答:百度地图没有缩略图的api,如果使用高德的话,应该是可以的


52. 自定义的布局没有已读未读?
回答:需要去发送一个已读ack,参考 EaseTextViewHolder #handleReceiveMessage去写 。

53. 环信管理后台查询到replaced是什么意思?
回答:replaced在后台msync的定义就是旧连接被新连接踢掉了


54. 使用了极光的厂商推送,在环信中怎么使用?
回答:产生冲突的原因:极光和环信使用的都是厂商推送
(一)用户既使用环信的离线推送,又使用极光的厂商推送的情况下:
解决思路:
1.小米,vivo,oppo,魅族,华为需要在极光中获取到token时调用环信的api上传给环信;
2.在application中先初始化极光,再初始化环信(要保证环信的初始化是在主进程中);
3.通过EMOptions设置各个厂商的证书appId和appKey。
4.在配置清单中修改下各个厂商的service
(二)不使用环信的离线推送,可以开通实时回调功能,将离线消息都配置到客户的服务器,客户使用极光推给用户。


55. 离线推送可以跳转到指定的页面吗 ?
回答:小米的在onNotificationMessageClicked里去解析MiPushMessage的content,拿到对应字段去自行跳转页面
vivo的在onNotificationMessageClicked里解析UPSNotificationMessage,拿到对应字段去自行跳转页面
OPPO的跟华为是一样的,在启动页的onCreate里去获取参数跳转
默认点击打开应用首页,可以在客户端首页获取到。在onCreate里去调用
Bundle bundle = getIntent().getExtras();
if(bundle != null){
String f = bundle.getString("f");
String t = bundle.getString("t");
}

字段对应的含义
f:from
t:to
m:msgid
g:groupid



56. 看文档fcm集成成功了,怎么没有收到推送?
回答:fcm是唤醒应用,环信服务器会将离线消息下发给客户端,接收到消息之后,自己做本地通知 。
接收消息的监听:在application中初始化环信成功之后,注册一个接收消息的监听,自己做一个判断是否运行在后台,如果运行在后台,本地通知,https://docs-im.easemob.com/im/android/basics/message#接收消息


57、 安卓和iOS音视频不通?
回答:看下安卓的sdk版本和ios的环信sdk版本,
3.7.5之前的版本使用的是环信的音视频,3.8.0之后的版本使用的是声网的音视频,需要确保各端在相同的版本下。


58. 在聊天页面点击大图崩溃?
回答:需要在配置清单中配置下EaseShowBigImageActivity。
android:screenOrientation="portrait"/>

59、
Android端账号被踢下线,消息接收不到。

账号在其他设备上登录,将当前设备踢下线的时候,当前设备需要在监听到被踢的时候调用退出的api,并传false。




60、Android 11报崩溃异常,报错如下:

java.lang.RuntimeException:Unable to start receiver com.hyphenate.chat.EMMonitorReceiver: java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.ant.health/com.hyphenate.chat.EMChatService }: app is in background uid UidRecord{ab5b452 u0a320 RCVR idle change:uncached procs:1 seq(0,0,0)}


A:去掉以下这几个方法,这几个方法对于安卓低版本保活的, 对于高版本 ,这个保活可以去掉。 高版本对保活有限制。



61、添加回调规则添加失败。
A:检查下回调规则名称是不是用的汉字,回调规则只能是数字、字母,不能用汉字。


62、对方离线了之后,发送的消息,上线后如何获取?
A:对方离线,消息会进入离线队列,如果没有集成第三方厂商离线推送,用户上线后,服务器下发给客户端。


63、调用SDK 方法报错: Cannot read property 'lookup' of undefined?
A:因为未登陆成功就调用了SDK 的api,需要在onOpened 链接成功回调执行后再去调用SDK 的api。


64、聊天室如何获取历史消息?
A:两种方式:1、环信服务器端主动推,需要联系商务开通服务,默认10条,数量可以调整。2、通过消息漫游接口自己去拉取历史消息,各端都有提供拉取漫游消息接口。


65、拉取消息漫游,conversationId是怎么获取的?
A:单聊的话,conversationId 就是对方用户的环信id。
群聊或聊天室的话,conversationId 就是groupid 或者chatroomid。


66、如何实现只有好友才可以发消息?
A:可以使用环信的发送前回调服务,消息先回调给配置的回调服务器,然后去判断收发双方是否是好友关系,如果是好友关系,那么下发消息,如果是非好友关系,则不下发消息,客户端ui可以根据不下发返回的code做提示。


67、调rest接口报401是什么原因?
A:调环信rest接口,需要管理员权限的token,确认下请求是否有token,且是在有效期,token的有效期以请求时服务器返回的时间为准。


68、调修改群信息报错如下
System.Net.WebException:“远程服务器返回错误: (400) 错误的请求。
A:检查下请求体,看下参数格式是否正确,比如"membersonly",,"allowinvites" 这两个参数的值为布尔值。


69、注册用户username是纯数字可以吗。

调restapi是可以的,serversdk的话,为了让用户使用更规范的名字,命名规则更严格一些,要求首位是字母。


70、自定义铃声只支持华为和小米
华为:
https://docs-im.e·asemob.com/im/other/integrationcases/appimnotifi#自定义推送铃声
小米:
直接调用rest接口去创建小米的通道,同时设置下铃声,https://dev.mi.com/console/doc/detail?pId=1163#_11
小米铃声 需要通道id 。。


自定义铃声
自定义播放铃声需要携带扩展字段 em_apns_ext 下面携带的就是标题和内容 em_android_push_ext 下 就是存放小米的通道id的 直接调用rest接口去创建小米的通道,同时设置下铃声


71、push推送后 点击通知栏后在哪里 设置跳转页面?
小米的在onNotificationMessageClicked里去解析MiPushMessage的content,拿到对应字段去自行跳转页面vivo的在onNotificationMessageClicked里解析UPSNotificationMessage,拿到对应字段去自行跳转页面OPPO的跟华为是一样的,在启动页的onCreate里去获取参数跳转
Bundle bundle = getIntent().getExtras();
if(bundle != null){
String f = bundle.getString(f);
String t = bundle.getString(t);}
字段对应的含义f:fromt:tom:msgidg:groupid.


72、在线push推送:
在线推送的话 3.8.7sdk已经封装在线push推送 如果想收到推送消息的话,需要自己做cmd消息 接收本地通知
3.8.5的话 需要 自己 去发一条 cmd消息 携带扩展字段, 端上接收到以后 做一个本地通知 。


73、 根据搜索框查找全局消息:
android 端调这个接口,不传from字段,实现全局搜索,EMClient.getInstance().chatManager().searchMsgFromDB
可以先拿到本地所有的会话列表 去搜索每一个会话的消息内容 , 然后存放到一个大集合,展示的话直接展示大集合里面的数据, 这个操作需要自己去实现的,


74、Dcoumentfile

自己加一个Documentfile的远程依赖,就可以了 百度连接 :https://developer.android.google.cn/jetpack/androidx/releases/documentfile?hl=zh-cn如果添加配置还是有问题的话,需要在gradle.properties中添加一下 android.enableJetifier=true


75、 头像昵称 用url获取:
只需要保证你返回得easeuser对象得数据是需要展示得ui得数据即可,不需要去管会话列表。。。可以打印在setUserProvider,看下返回得username数据,加入会话列表有十个,那么就会返回这个十个会话得username,然后,你需要根据返回得username去本地数据库查询对应得头像和昵称,如果有,就返回,如果没有就从网络请求并存数据,并去刷新会话列表,刷新会话列表得原因就是因为只要刷新就会再次执行setUserProvider,就又走一边这个逻辑,从数据库取
如果用环信的room数据库进行存储直接将demo中的db文件下所有类拉入自己的项目中



76、发送视频体积10mb:
相机是直接调用的系统的,跟随的是系统的大小,我拍摄15s视频大概18m左右。环信系统默认的是只能发送10M的视频文件,您需要在发送视频之前做下压缩 ,在调用环信发送视频方法之前去做判断,超过10m的话,压缩下再发送

 
77、用户A给B发送自定义消息,B可以收到,后台拿用户A的账号给用户B发送,B也可以收到消息,但是用户A的聊天页面不显示发送消息的内容
服务端:A send custom message B,B 看到自定义消息 C send cmd message A,携带自定义消息内容、A、B客户端:A receive cmd message ---> 解析message ---> 向B的会话插入以A身份发的自定义消息


78、 解决 UTF-8
解决问题的步骤:

步骤1、首先,在项目build.gradle文件中添加如下代码:
buildscript {
tasks.withType(JavaCompile) {//解决编码错误: 编码UTF-8的不可映射字
options.encoding = "UTF-8"}}


步骤2、如果导入AS后,文件代码注释出现乱码问题。将AS右下角 “file encoding”编码格式,先改为“GB2312”,弹出对话框,选择“reload”,此时注释乱码消失。接着再改为“UTF-8”,弹出对话框,选择“convert”,即可。


79、 用户在上线后报218
218 是指当前SDK已经登录了一个id,如果没有退出,然后再使用另外一个id 登录的话会提示这个错误
一般开启了自动登录的话,SDK 初始化后会自动登录上次的ID,可以排查一下这块的逻辑。
如果开启了自动登录,不需要再调用登录接口,如果没开启自动登录,可以在应用打开时调用登录api
这种情况其实再调用下logout 再调用login 其实也是可以的

80、 进入聊天页面后,拉取聊天页面之后,未读数+1
检查下调用conversation.getMessage(string, bool)时,传的是不是true。


81、 小米手机如何设置自定义铃声?

回答:1).需要先去小米厂商申请通道并设置添加上自定义铃声;

2) .安卓端在发送消息时,需要携带上在小米厂商申请的通道名称。


82、 华为手机如果设置自定义铃声?
在发送消息之前去设置扩展字段,
"em_android_push_ext":{
//指定自定义渠道
"em_push_channel_id":"Channel id",
"em_push_sound":"/raw/appsound"
}


注意事项:
(1)目前只支持华为EMUI 10以上的系统。
(2)华为EMUI 10以上自定义推送铃声,需要设置channel_id,通过em_push_channel_id进行设置。
需要注意的是,即使指定了渠道标识(channel id),消息最终能否展示在应用渠道上,受用户终端上该渠道是否创建以及渠道的开关策略控制。
a、如果本地已经创建该渠道,且已设置了对应的自定义铃声,收到推送消息时会播放自定义铃声。
b、如果本地没有创建指定的渠道,则华为会对消息进行智能分类,根据消息设置的级别及智能分类的结果,两者取低,根据级别下发到服务提醒,普通通知与营销通知三个中的一个通知渠道,如果该通知渠道之前没有创建且不是营销通知,则设置自定义铃声有效。
(3)对于华为EMUI 10以上系统,需要添加em_push_name和em_push_content参数,否则容易被华为通知智能分类分到营销通知渠道,从而不能播放自定义铃声。
(4)由于铃声是通知渠道的属性,因此铃声仅在渠道创建时有效,渠道创建后,即使设置自定义铃声也不会播放,而使用创建渠道时设置的铃声。



83、fcm离线push 通知栏是正常显示 但是 EMFCMMSGService 的onMessageReceived 不走?
设置下这个data数据,onMessageReceived没有执行的原因是没有data数据。




84、撤回消息提示,自己撤回的本地能加载出来,对方撤回的消息,杀了app再打开这条提示就没了。

在插入消息时,先设置to,再设置from




85、从服务器端获取会话列表,没有获取到rest发过来的消息?
联系下环信工作人员,或者提交工单,让工作人员配置下。



86、push推送消息成功了,但是在环信后台管理界面查不到?
把single改成list;




87、 怎么全局搜索?
调这个接口,不传from字段,实现全局搜索,

EMClient.getInstance().chatManager().searchMsgFromDB


88、 地图能正常显示,怎么显示缩略图?
百度地图没有缩略图的api,如果使用高德的话,是可以的。


89、EMMessage怎么区分离线消息还是在线消息?

EMMessage中没有方法可以去判断是离线消息还是在线消息。
实现方法:
可以在接收消息的监听中,获取到消息的时间戳与本地时间戳做对比,超过一定的时间就算离线消息。 但是前提需要保证本地的时间戳是对的。


90、在使用环信时,偶尔发送消息会出现500,是什么原因?
1.检查下用户是否登录?
2.检查下网络是否正常?


91、如何删除和某个用户的所有聊天记录?
可以直接调用api去删除EMClient.getInstance().chatManager().getConversation("环信id").clearAllMessages();


92、何时调本地取群组信息,何时调从服务器取群组信息?
回答:打开app,从服务器拉取(从服务器拉取之后,sdk会保存在本地,再调用本地获取能拿到信息),当群成员更新的时候,从服务器获取


93、如何设置群扩展字段?
1.通过EMGroupOptions的extField设置的扩展字段。
2.从服务器获取群组信息,获取getExtension
EMGroup group = EMClient.getInstance().groupManager().getGroupFromServer(groupId);group.getExtension();


94、Android 百度地图怎么替换为高德地图?
1.将easeimkit中关于百度地图的集成去掉,改成高德地图;
2.在chatfragment中重写位置的点击事件方法startMapLocation或者是直接在EaseChatFragment中直接修改点击事件startMapLocation跳转到高德地图;
3.在调用环信api去发送地理位置消息时,传入高德获取到的经纬度。


95、Rest发消息会话列表为什么获取不到?
默认rest 消息不写会话列表,如果需要的话,可以联系对接商务开通该项服务。



96、appkey获取会话列表服务是开通状态,但获取不到会话列表?
检查下用户id是不是大小写混写了,大小写混写会导致获取不到会话列表,如果自己应用有区分大小,登陆环信时建议全部转为小写。


97、拉黑与被拉黑发送消息返回什么错误码?
A 拉黑B,A可以给B正常发消息,B给A发消息会提示报错,3.6.3的返回604错误码,Android返回210错误码


98、不注册环信id可以收发消息吗?业务场景支持游客模式,不需要注册的。
使用环信必须要注册环信id,对于环信来说,收发消息的双方是环信id,环信并不关心该用户是客户业务系统里的哪个用户,游客也是客户业务的定义,环信本身不存在游客的说法,任何身份的用户对于环信来说就是一个环信id。


99、不登录可以发消息吗?业务场景不需要登录,只是授权。
发消息是必须要登录环信的,客户自己的应用可以实现为不用登录就可以发消息,但在进入自己应用的时候或者其他时机,比如点击发消息、咨询等icon时,底层需要去调用环信的登录方法,去登录环信,对于客户的用户来说是没有登录这个操作。


100、聊天室支持全员禁言吗?
A:支持,群组、聊天室全局禁言都是支持,调以下api去设置。
聊天室:
全员禁言:POST 'http://{url}/{org}/{app}/chatrooms/{chatroomId}/ban'
解除全员禁言:DETELE 'http://{url}/{org}/{app}/chatrooms/{chatroomId}/ban'
群组:
全员禁言:POST 'http://{url}/{org}/{app}/chatgroups/{chatgroupId}/ban'
解除全员禁言:DETELE 'http://{url}/{org}/{app}/ chatgroups/{ chatgroupId}/ban






收起阅读 »

ios--离屏渲染详解

目录:1.图像显示原理2.图像显示原理2.1 图像到屏幕的流程2.2 显示器显示的流程3.卡顿、掉帧3.1 垂直同步 Vsync + 双缓冲机制 Double Buffering2.3 掉帧和屏幕卡顿的本质4.离屏渲染4.1 什么事离屏渲染、离屏渲染的过程4....
继续阅读 »

目录:

  • 1.图像显示原理
  • 2.图像显示原理
    • 2.1 图像到屏幕的流程
    • 2.2 显示器显示的流程
  • 3.卡顿、掉帧
    • 3.1 垂直同步 Vsync + 双缓冲机制 Double Buffering
    • 2.3 掉帧和屏幕卡顿的本质
  • 4.离屏渲染
    • 4.1 什么事离屏渲染、离屏渲染的过程
    • 4.2 既然离屏渲染影响界面,为什么还要用
  • 5.触发离屏渲染
  • 6.如何优化
1.引言

先来聊聊为什么要了解离屏渲染?
看看现在app开发的大环境,14年的时候在深圳,基本上每个公司都要做一个app。不做一个app你都不一定能拉倒更多的投资。再看看现在,死了一大半,现在的用户也不想去下载太多的app。一般手机上只留一些常用的,基本全是大厂的app。然后ios这行问的也就越来越难。性能优化这个绝对会问,在网上也有许多性能优化的总结,但是你不能不知道为什么这么做能优化,要知道其为什么。那么,这时候你就需要知道界面是怎么渲染的,什么时候会掉帧,什么时候会卡顿,这些都使得我们非常有必要去了解离屏渲染。
离屏渲染过程

2.图像显示原理
2.1 图像到屏幕的流程

先来看一张图,我们结合这张图来说


首先要明白的一个东西是Render Server 进程,app本身其实并不负责渲染,渲染是有独立的进程负责的,它就是Render Server 。

当我们在代码里设置修改了UI界面的时候,其实它本质是通过Core Animation修改CALayer。在后续的核心动画总结中 我们会说到UIView和CALayer的关系,以及核心动画的设置等等,这个知识点有点多,需要单独详细的总结出来。所以最后按照图片中的流程显示。

  • 首先,有app处理事件(Handle Events),例如:用户点击了一个按钮,它会触发其他的视图的一个动画等
  • 其次,app通过CPU完成对显示内容的计算 例如:视图的创建,视图的布局 图片文本的绘制等。在完成了对显示内容的计算之后,app对图层进行打包,并在下一次Runloop时,将其发送至Render Server
  • 上面我们提到,Render Server负责渲染。Render Server通过执行Open GL、Core Graphics Metal相关程序。 调用GPU
  • GPU在物理层完成了对图像的渲染。

说到这我们就要停一下,我们来看下一个图




上面的流程图 细化了GPU到控制器的这一个过程。
GPU 拿到位图后执行顶点着色、图元装配、光栅化、片段着色等,最后将渲染的结果交到了Frame Buffer(帧缓存区当中)
然后视频控制器从帧缓存区中拿到要显示的对象,显示到屏幕上
图片中的黄色虚线暂时不用管,下面在说垂直同步信号的时候,就明白了。
这是从我们代码中设置UI,然后到屏幕的一个过程。

2.2 显示器显示的过程

现在从帧缓存中拿到了渲染的视图,又该怎么显示到显示器上面呢?

先来看一张图



从图中我们也能大致的明白显示的一个过程。

显示器的电子束从屏幕的左上方开始逐行显示,当第一行扫描完之后接着第二行 又是从左到右,就这样一直到屏幕的最下面扫描完成。我们都知道。手机它是有屏幕的刷新次数的。安卓的现在好多是120的,ios是60。1秒刷新60次,当我们扫描完成以后,屏幕刷新,然后视图就会显示出来。

3.UI卡顿 掉帧
3.1垂直同步 Vsync + 双缓冲机制 Double Buffering

首先我们了解了上面渲染的过程以后,需要考虑遇到一些特别的情况下,该怎么办?在我们代码里写了一个很复杂的UI视图,然后CPU计算布局、GPU渲染,最后放到缓存区。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,被放入帧缓冲器中 。
那么已扫描的部分就是上一帧的画面,而未扫描的部分就是新一帧的图像,这样是不是就造成了屏幕撕裂了。

但是,在我们平常开发的过程遇到过屏幕撕裂的问题吗?没有吧,这是为什么呢?
显然是苹果做了优化操作了。也就是垂直同步 Vsync + 双缓冲机制 Double Buffering。

垂直同步 Vsync
垂直同步 Vsync相当于给帧缓存加了锁,还记得上面说到的那个黄色虚线嘛,在我们扫描完一帧以后,就会发出一个垂直同步的信号,通知开始扫描下一帧的图像了。他就像一个位置秩序的,你得给我排队一个一个来,别插队。插队的后果就是屏幕撕裂。
双缓冲机制 Double Buffering
扫描显示排队进行了,这样在进行下一帧的位图传入的时候,也就意味着我要立刻拿到位图。不能等CPU+GPU计算渲染后再给位图,这样就影响性能。要怎么解决这个问题呢?肯定是 在你快要渲染之前你就要把这些都完成了。你就像排队打针一样,为了节省时间肯定事先都会挽起袖子,到医生那时,直接一针下去了事。扯远了 哈哈。想预先渲染好,就需要另外一个缓存来放下一帧的位图,在它需要扫描的时候,再把渲染好的位图给了帧缓存,帧缓存拿到以后 开始快乐的扫描 显示。
一个图解释




3.2 掉帧卡顿

垂直同步和双缓存机制完美的解决了屏幕撕裂的问题,但是又引出一个新的问题:掉帧。
掉帧是什么意思呢?从网上copy了一份图



其实很好理解,上面我们说了ios的屏幕刷新是60次,那么在一次刷新的过程中,我们CPU+GPU它没有把新渲染的位图放到帧缓存区,这时候是不是还是显示的原来的图像。当下刷新下一帧的时候,拿到了新的位图,这里是不是就丢失了一帧。

卡顿的根本原因:
CPU和GPU渲染流水线耗时过长 掉帧
我们平常写界面的时候,通过一些开源的库或者自己使用runloop写的库来检测界面卡顿的时候,屏幕刷新率在50以上就很可以了。一般人哪能体验到掉了10帧。你要刷新率是30,那卡顿想过就很明显了。

4 离屏渲染
4.1什么是离屏渲染 离屏渲染的过程

是指在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作.
过程:首先会创建一个当前屏幕缓冲区以外的新缓存区,屏幕渲染会有一个上下文环境,离屏渲染的过程就是切花上下文环境,现充当前屏幕切换到离屏,等结束以后又将上下文切换回来。所以需要更长的时间来处理。时间一长就可能造成掉帧。
并且 Offscreen Buffer离屏缓存 本身就需要额外的空间,大量的离屏渲染可能造成内存过大的压力。而且离屏缓存区并不是没有限制大小的,它是不能超过屏幕总像素的2.5倍。

4.2为什么要使用离屏渲染

1.一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
2.处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前(下一个VSync信号开始前)不能直接在屏幕中绘制,所以就需要屏幕外渲染。

5.触发离屏渲染
  1. 为图层设置遮罩(layer.mask)
  2. 图层的layer. masksToBounds/view.clipsToBounds属性设置为true
  3. 将图层layer. allowsGroupOpacity设置为yes和layer. opacity<1.0
  4. 为图层设置阴影(layer.shadow)
  5. 为图层设置shouldRasterize光栅化
    6 复杂形状设置圆角等
    7 渐变
    8 文本(任何种类,包括UILabel,CATextLayer,Core Text等)
    9 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
6 离屏渲染的优化
1 圆角优化

方法一

iv.layer.cornerRadius = 30;
iv.layer.masksToBounds = YES;
方法二
利用mask设置圆角,利用贝塞斯曲线和CAShapeLayer来完成

CAShapeLayer *mask1 = [[CAShapeLayer alloc] init];
mask1.opacity = 0.5;
mask1.path = [UIBezierPath bezierPathWithOvalInRect:iv.bounds].CGPath;
iv.layer.mask = mask1;
方法三
利用CoreGraphics画一个圆形上下文,然后把图片绘制上去

- (void)setCircleImage
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage * circleImage = [image imageWithCircle];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = circleImage;
});
});
}


#import "UIImage+Addtions.h"
@implementation UIImage (Addtions)
//返回一张圆形图片
- (instancetype)imageWithCircle
{
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
[path addClip];
[self drawAtPoint:CGPointZero];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
}
shadows(阴影)

设置阴影后,设置CALayer的shadowPath

view.layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;

mask(遮罩)

不使用mask
使用混合图层 使用混合图层,在layer上方叠加相应mask形状的半透明layer

sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];
allowsGroupOpacity(组不透明)

关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

edge antialiasing(抗锯齿)

不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便


view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;

如果视图内容是动态变化的,例如cell中的图片,这个时候使用光栅化会增加系统负荷。

作者:Harry__Li
链接:https://www.jianshu.com/p/3c3383bdeb71


收起阅读 »

iOS-分页控制器

使用:1、创建方法1.1 导入头文件#import "XLPageViewController.h"1.2 遵守协议@interface ViewController ()<XLPageViewControllerDelegate, XLPageView...
继续阅读 »




使用:

1、创建方法

1.1 导入头文件

#import "XLPageViewController.h"
1.2 遵守协议
@interface ViewController ()<XLPageViewControllerDelegate, XLPageViewControllerDataSrouce>
1.3 创建外观配置类

注:config负责所有的外观配置,defaultConfig方法设定了默认参数,使用时可按需配置。 →Config属性列表

  XLPageViewControllerConfig *config = [XLPageViewControllerConfig defaultConfig];

1.4 创建分页控制器

注:需要把pageViewController添加为当前视图控制器的子视图控制器,才能实现子视图控制器中的界面跳转。

  XLPageViewController *pageViewController = [[XLPageViewController alloc] initWithConfig:config];
pageViewController.view.frame = self.view.bounds;
pageViewController.delegate = self;
pageViewController.dataSource = self;
[self.view addSubview:pageViewController.view];
[self addChildViewController:pageViewController];
2、协议

2.1 XLPageViewControllerDelegate

//回调切换位置
- (void)pageViewController:(XLPageViewController *)pageViewController didSelectedAtIndex:(NSInteger)index;

2.2 XLPageViewControllerDataSrouce

@required

//根据index创建对应的视图控制器,每个试图控制器只会被创建一次。
- (UIViewController *)pageViewController:(XLPageViewController *)pageViewController viewControllerForIndex:(NSInteger)index;
//根据index返回对应的标题
- (NSString *)pageViewController:(XLPageViewController *)pageViewController titleForIndex:(NSInteger)index;
//返回分页数
- (NSInteger)pageViewControllerNumberOfPage;

@optional

//标题cell复用方法,自定义标题cell时用到
- (__kindof XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index;

3、自定义标题cell

3.1 创建一个XLPageTitleCell的子类

#import "XLPageTitleCell.h"

@interface CustomPageTitleCell : XLPageTitleCell

@end

3.2 注册cell、添加创建cell

//需要先注册cell
[self.pageViewController registerClass:CustomPageTitleCell.class forTitleViewCellWithReuseIdentifier:@"CustomPageTitleCell"];
//自定义标题cell创建方法
- (XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index {
CustomPageTitleCell *cell = [pageViewController dequeueReusableTitleViewCellWithIdentifier:@"CustomPageTitleCell" forIndex:index];
return cell;
}

3.3 复写cell父类方法

//通过此父类方法配置标题cell是否被选中样式
- (void)configCellOfSelected:(BOOL)selected {

}

//通过此父类方法配置标题cell动画;type:区分是当前选中cell/将要被选中的cell;progress:动画进度0~1
- (void)showAnimationOfProgress:(CGFloat)progress type:(XLPageTitleCellAnimationType)type {

}

4、特殊情况处理

4.1 和子view手势冲突问题

pageViewController的子视图中存在可滚动的子view,例如UISlider、UIScrollView等,如果子view和pageViewController发生滚动冲突时,可设置子view的xl_letMeScrollFirst属性为true。

  UISlider *slider = [[UISlider alloc] init];
slider.xl_letMeScrollFirst = true;
[childVC.view addSubview:slider];

4.2 全屏返回手势问题

pageViewController和全屏返回手势一起使用时,需要将其它手势的delegate的类名添加到respondOtherGestureDelegateClassList属性中。当滚动到第一个分页时,向右滑动会优先响应全屏返回。以FDFullscreenPopGesture为例:

self.pageViewController.respondOtherGestureDelegateClassList = @[@"_FDFullscreenPopGestureRecognizerDelegate"];

5、注意事项

使用时需注意标题不要重复标题是定位ViewController的唯一ID。


源码下载:XLPageViewController-master.zip 

常见问题及demo:https://github.com/mengxianliang/XLPageViewController





收起阅读 »

iOS - 呼吸动画库

先看效果

先看效果



需求和实现思路

具体要求

  • 内部头像呼吸放大缩小 无限循环
  • 每次放大同时需要背景还有一张图也放大 并且透明
  • 点击缩放整个背景视图


实现思路

首先 需要使用创建一个Layer 装第一个无限放大缩小的呼吸的图 背景也需要一个Layer 做 放大+透明度渐变的动画组并且也放置一张需要放大渐变的图片

最后点击触发. 添加一个一次性的缩放动画即可

呼吸动画layer和动画

呼吸layer

CALayer *layer = [CALayer layer];
layer.position = CGPointMake(kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.bounds = CGRectMake(0, 0, kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.backgroundColor = [UIColor clearColor].CGColor;
layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
layer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer addSublayer:layer];
复制代码

kHeartSizeHeight 和kHeartSizeWidth 是常量 demo中写好了100

加帧动画

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.f, @1.4f, @1.f];
animation.keyTimes = @[@0.f, @0.5f, @1.f];
animation.duration = 1; //1000ms
animation.repeatCount = FLT_MAX;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setValue:kBreathAnimationKey forKey:kBreathAnimationName];
[layer addAnimation:animation forKey:kBreathAnimationKey];
复制代码

差值器也可以自定义 例如:

[CAMediaTimingFunction functionWithControlPoints:0.33 :0 :0.67 :1]
复制代码

这里我做的持续时常1秒

放大渐变动画group

创建新layer

CALayer *breathLayer = [CALayer layer];
breathLayer.position = layer.position;
breathLayer.bounds = layer.bounds;
breathLayer.backgroundColor = [UIColor clearColor].CGColor;
breathLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
breathLayer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer insertSublayer:breathLayer below:layer];
//[self.heartView.layer addSublayer:breathLayer];
复制代码

这里用的是放在 呼吸layer后边 如果想放在呼吸layer前边 就把里面注释打开 然后注掉 inert那行代码

动画组 包含 放大 渐变


//缩放
CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.values = @[@1.f, @2.4f];
scaleAnimation.keyTimes = @[@0.f,@1.f];
scaleAnimation.duration = animation.duration;
scaleAnimation.repeatCount = FLT_MAX;
scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
//透明度
CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animation];
opacityAnimation.keyPath = @"opacity";
opacityAnimation.values = @[@1.f, @0.f];
opacityAnimation.duration = 0.4f;
opacityAnimation.keyTimes = @[@0.f, @1.f];
opacityAnimation.repeatCount = FLT_MAX;
opacityAnimation.duration = animation.duration;
opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];

//动画组
CAAnimationGroup *scaleOpacityGroup = [CAAnimationGroup animation];
scaleOpacityGroup.animations = @[scaleAnimation, opacityAnimation];
scaleOpacityGroup.removedOnCompletion = NO;
scaleOpacityGroup.fillMode = kCAFillModeForwards;
scaleOpacityGroup.duration = animation.duration;
scaleOpacityGroup.repeatCount = FLT_MAX;
[breathLayer addAnimation:scaleOpacityGroup forKey:kBreathScaleName];
复制代码

点击缩放动画

跟第一个一样 只不过 执行次数默认一次 执行完就可以了

- (void)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.0f, @0.8f, @1.f];
animation.keyTimes = @[@0.f,@0.5f, @1.f];
animation.duration = 0.35f;
animation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[self.heartView.layer addAnimation:animation forKey:@""];
}
复制代码

手势触发的时候 调用一下 

源码及demo地址:https://github.com/sunyazhou13/BreathAnimation



12个出现频率最高的iOS技术面试题及答案

这篇文章给大家总结了在iOS面试的时候可能会遇到的12个技术面试题,以及这些面试题但答案,这些答案只是给大家一些参考,大家可以再结合自己理解进行回答,有需要的朋友们下面来一起看看吧。前言随着移动互联网科技不断的发展和创新,如今无论是公司还是开发者或设计师个人而...
继续阅读 »

这篇文章给大家总结了在iOS面试的时候可能会遇到的12个技术面试题,以及这些面试题但答案,这些答案只是给大家一些参考,大家可以再结合自己理解进行回答,有需要的朋友们下面来一起看看吧。

前言

随着移动互联网科技不断的发展和创新,如今无论是公司还是开发者或设计师个人而言,面试都是一项耗时耗钱的项目,而面对iOS开发者及设计师在面试时可能会遇到的问题进行了筛选与汇总。下面我们一起来一下看看吧。

一、如何绘制UIView?

绘制一个UIView最灵活的方法就是由它自己完成绘制。实际上你不是绘制一个UIView,而是子类化一个UIView并赋予绘制自己的能力。当一个UIView需要执行绘制操作时,drawRect:方法就会被调用,覆盖此方法让你获得绘图操作的机会。当drawRect:方法被调用,当前图形的上下文也被设置为属于视图的图形上下文,你可以使用Core Graphic或者UIKit提供的方法将图形画在该上下文中。

二、什么是MVVM?主要目的是什么?优点有哪些?

MVVM即 Model-View-ViewModel

1.View主要用于界面呈现,与用户输入设备进行交互、

2.ViewModel是MVVM架构中最重要的部分,ViewModel中包含属性,方法,事件,属性验证等逻辑,负责ViewModel之间的通讯

3.Model就是我们常说的数据模型,用于数据的构造,数据的驱动,主要提供基础实体的属性。

MVVM主要目的是分离视图和模型

MVVM优点:低耦合,可重用性,独立开发,可测试

三、get请求与post请求的区别

1.get是向服务器发索取数据的一种请求,而post是向服务器提交数据的一种请求

2.get没有请求体,post有请求体

3.get请求的数据会暴露在地址栏中,而post请求不会,所以post请求的安全性比get请求号

4.get请求对url长度有限制,而post请求对url长度理论上是不会收限制的,但是实际上各个服务器会规定对post提交数据大小进行限制。

四、谈谈你对多线程开发的理解?ios中有几种实现多线程的方法?

好处:

1.使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片,视频的下载;

2.发挥多核处理器的优势,并发执行让系统运行的更快,更流畅,用户体验更好;

缺点:

1.大量的线程降低代码的可读性;

2.更多的线程需要更多的内存空间;

3当多个线程对同一个资源出现争夺的时候要注意线程安全的问题。

ios有3种多线程编程的技术:1.NSThread,2.NSOperationQueue,3.gcd;

五、XMPP工作原理;xmpp系统特点

原理:

1.所有从一个client到另一个clientjabber消息和数据都要通过xmpp server

2.client链接到server

3.server利用本地目录系统的证书对其认证

4.server查找,连接并进行相互认证

5.client间进行交互

特点:1)客户机/服务器通信模式;2)分布式网络;3)简单的客户端;4)XML的数据格式

六、地图的定位是怎么实现的?

1.导入了CoreLocation.framework

2.ios8以后,如果需要使用定位功能,就需要请求用户授权,在首次运行时会弹框提示

3.通过本机自带的gps获取位置信息(即经纬度)

七、苹果内购实现流程

程序通过bundle存储的plist文件得到产品标识符的列表。

程序向App Store发送请求,得到产品的信息。

App Store返回产品信息。

程序把返回的产品信息显示给用户(App的store界面)

用户选择某个产品

程序向App Store发送支付请求

App Store处理支付请求并返回交易完成信息。

App获取信息并提供内容给用户。

八、支付宝,微信等相关类型的sdk的集成

1.在支付宝开发平台创建应用并获取APPID

2.配置密钥

3.集成并配置SDK

4.调用接口(如交易查询接口,交易退款接口)

九、 gcd产生死锁的原因及解锁的方法

产生死锁的必要条件:1.互斥条件,2.请求与保持条件,3.不剥夺条件,4.循环等待条件。

解决办法:采用异步执行block。

十、生成二维码的步骤

1.使用CIFilter滤镜类生成二维码

2.对生成的二维码进行加工,使其更清晰

3.自定义二维码背景色、填充色

4.自定义定位角标

5.在二维码中心插入小图片


十一、在使用XMPP的时候有没有什么困难

发送附件(图片,语音,文档...)时比较麻烦

XMPP框架没有提供发送附件的功能,需要自己实现

实现方法,把文件上传到文件服务器,上传成功后获取文件保存路径,再把附件的路径发送给好友



十二、是否使用过环信,简单的说下环信的实现原理

环信是一个即时通讯的服务提供商

环信使用的是XMPP协议,它是再XMPP的基础上进行二次开发,对服务器Openfire和客户端进行功能模型的添加和客户端SDK的封装,环信的本质还是使用XMPP,基于Socket的网络通信

环信内部实现了数据缓存,会把聊天记录添加到数据库,把附件(如音频文件,图片文件)下载到本地,使程序员更多时间是花到用户体验体验上。



作者:iOS鑫
链接:https://www.jianshu.com/p/d95967869aed
收起阅读 »

最新iOS开发常见面试题-基础篇

1.iOS线程与进程的区别和联系?进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而...
继续阅读 »

1.iOS线程与进程的区别和联系?

进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。

程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

2.iOS 如何找到最合适的控件来处理事件?

自己是否能接收触摸事件?
触摸点是否在自己身上?
从后往前遍历子控件,重复前面的两个步骤
如果没有符合条件的子控件,那么就自己最适合处理

3.iOS static 关键字的作用?

(1)函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,

因此其值在下次调用时仍维持上次的值;

(2)在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;

(3)在模块内的 static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明

它的模块内;

(4)在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

(5)在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的static 成员变量。

4.iOS UIEvent对象的作用与常见属性?

每产生一个事件,就会产生一个UIEvent对象

UIEvent : 称为事件对象,记录事件产生的时刻和类型
常见属性 

  //事件类型
//@property(nonatomic,readonly) UIEventType type;
//@property(nonatomic,readonly) UIEventSubtype subtype;
//事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)

4.ViewController 的 loadView, viewDidLoad, viewDidUnload 分别是在什么时候调用的?在自定义ViewController的时候这几个函数里面应该做什么工作?

viewDidLoad在view 从nib文件初始化时调用,loadView在controller的view为nil时调用。此方法在编程实现view时调用,view 控制器默认会注册memory warning notification,当view controller的任何view 没有用的时候,viewDidUnload会被调用,在这里实现将retain 的view release,如果是retain的IBOutlet view 属性则不要在这里release,IBOutlet会负责release 。

5.object-c 的优缺点 ?

objc优点:

  1. Cateogies

  2. Posing

  3. 动态识别

  4. 指标计算

5)弹性讯息传递

  1. 不是一个过度复杂的 C 衍生语言

  2. Objective-C 与 C++ 可混合编程

缺点:

  1. 不支援命名空间

  2. 不支持运算符重载

3)不支持多重继承

4)使用动态运行时类型,所有的方法都是函数调用,所以很多编译时优化方法都用不到。(如内联函数等),性能低劣。

6.iOS引用与指针有什么区别?

1.引用必须被初始化,指针不必。
2.引用初始化以后不能被改变,指针可以改变所指的对象。
3.不存在指向空值的引用,但是存在指向空值的指针。

7.iOS堆和栈的区别 ?

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

申请大小:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

8.什么时候用delegate,什么时候用Notification?

delegate针对one-to-one关系,并且reciever可以返回值 给sender,notification 可以针对one-to-one/many/none,reciever无法返回值给sender.所以,delegate用于sender希望接受到 reciever的某个功能反馈值,notification用于通知多个object某个事件。

9.iOS UITouch对象的作用与常见属性?

当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象 一根手指对应一个UITouch对象

UITouch的作用:

保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
当手指离开屏幕时,系统会销毁相应的UITouch对象
UITouch的常见属性

//触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;

//触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;

//短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;

//记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;

//当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;

UITouch的常见方法

   //返回值表示触摸在view上的位置
//这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(UIView *)view;


10.object-c 的内存管理 ?

如果您通过分配和初始化(比如[[MyClass alloc] init])的方式来创建对象,您就拥有这个对象,需要负责该对象的释放。这个规则在使用NSObject的便利方法new 时也同样适用。

如果您拷贝一个对象,您也拥有拷贝得到的对象,需要负责该对象的释放。

如果您保持一个对象,您就部分拥有这个对象,需要在不再使用时释放该对象。

反过来,如果您从其它对象那里接收到一个对象,则您不拥有该对象,也不应该释放它(这个规则有少数的例外)

11.iOS单件实例是什么 ?

Foundation 和 Application Kit 框架中的一些类只允许创建单件对象,即这些类在当前进程中的唯一实例。举例来说,NSFileManager 和NSWorkspace 类在使用时都是基于进程进行单件对象的实例化。当向这些类请求实例的时候,它们会向您传递单一实例的一个引用,如果该实例还不存在,则首先进行实例的分配和初始化。单件对象充当控制中心的角色,负责指引或协调类的各种服务。如果类在概念上只有一个实例(比如NSWorkspace),就应该产生一个单件实例,而不是多个实例;如果将来某一天可能有多个实例,您可以使用单件实例机制,而不是工厂方法或函数。

12.iOS类工厂方法是什么 ?

类工厂方法的实现是为了向客户提供方便,它们将分配和初始化合在一个步骤中,返回被创建的对象,并进行自动释放处理。这些方法的形式是+ (type)className...(其中 className不包括任何前缀)。

工厂方法可能不仅仅为了方便使用。它们不但可以将分配和初始化合在一起,还可以为初始化过程提供对象的分配信息,类工厂方法的另一个目的是使类(比如NSWorkspace)提供单件实例。虽然init...方法可以确认一个类在每次程序运行过程只存在一个实例,但它需要首先分配一个“生的”实例,然后还必须释放该实例,工厂方法则可以避免为可能没有用的对象盲目分配内存。

13.一个指针可以是volatile 吗?解释为什么。

是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

14.iOS 类别的局限性 ?

有两方面局限性:

(1)无法向类中添加新的实例变量,类别没有位置容纳实例变量。

(2)名称冲突,即当类别中的方法与原始类方法名称冲突时,类别具有更高的优先级。类别方法将完全取代初始方法从而无法再使用初始方法。

无法添加实例变量的局限可以使用字典对象解决

15.什么是iOS键-值,键路径是什么 ?

模型的性质是通过一个简单的键(通常是个字符串)来指定的。视图和控制器通过键来查找相应的属性值。在一个给定的实体中,同一个属性的所有值具有相同的数据类型。键-值编码技术用于进行这样的查找—它是一种间接访问对象属性的机制。

键路径是一个由用点作分隔符的键组成的字符串,用于指定一个连接在一起的对象性质序列。第一个键的性质是由先前的性质决定的,接下来每个键的值也是相对于其前面的性质。键路径使您可以以独立于模型

实现的方式指定相关对象的性质。通过键路径,您可以指定对象图中的一个任意深度的路径,使其指向相关对象的特定属性。

16.iOS 类别的作用 ?

类别主要有3个作用:

(1)将类的实现分散到多个不同文件或多个不同框架中。

(2)创建对私有方法的前向引用。

(3)向对象添加非正式协议。

17.sprintf,strcpy,memcpy使用上有什么要注意的地方 ?

strcpy是一个字符串拷贝的函数,它的函数原型为strcpy(char dst, ct char *src);

将 src开始的一段字符串拷贝到dst开始的内存中去,结束的标志符号为'\0',由于拷贝的长度不是由我们自己控制的,所以这个字符串拷贝很容易出错。具备字符串拷贝功能的函数有memcpy,这是一个内存拷贝函数,它的函数原型为memcpy(char dst, c*t char src, unsigned int len);

将长度为len的一段内存,从src拷贝到dst中去,这个函数的长度可控。但是会有内存叠加的问题。

sprintf是格式化函数。将一段数据通过特定的格式,格式化到一个字符串缓冲区中去。sprintf格式化的函数的长度不可控,有可能格式化后的字符串会超出缓冲区的大小,造成溢出。

14答案是:

a) int a; // An integer

b) int *a; // A pointer to an integer

c) int **a; // A pointer to a pointer to an integer

d) int a[10]; // An array of 10 integers

e) int *a[10]; // An array of 10 pointers to integers

f) int (*a)[10]; // A pointer to an array of 10 integers

g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer

h) int (a[10])(int); // An array of 10 pointers to functi that take an integer argument and return an integer

18.readwrite,readonly,assign,retain,copy,nonatomic属性的作用

@property是一个属性访问声明,扩号内支持以下几个属性:

1,getter=getterName,setter=setterName,设置setter与getter的方法名

2,readwrite,readonly,设置可供访问级别

2,assign,setter方法直接赋值,不进行任何retain操作,为了解决原类型与环循引用问题

3,retain,setter方法对参数进行release旧值再retain新值,所有实现都是这个顺序(CC上有相关资料)

4,copy,setter方法进行Copy操作,与retain处理流程一样,先旧值release,再Copy出新的对象,retainCount为1。这是为了减少对上下文的依赖而引入的机制。copy是在你不希望a和b共享一块内存时会使用到。a和b各自有自己的内存。

5,nonatomic,非原子性访问,不加同步,多线程并发访问会提高性能。注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。锁被加到所属对象实例级(我是这么理解的...)。

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。

19"NSMutableString *"这个数据类型则是代表"NSMutableString"对象本身,这两者是有区别的。

NSString只是对象的指针而已。

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。

面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。


作者:iOS鑫
链接:https://www.jianshu.com/p/48a5b53c63e8

收起阅读 »

iOS面试备战-网络篇

计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及并用到的知识点,可想而知它的重要性。所以它也成为了iOS面试中经常被问及的问题。准备面试的话,网络相关的知识点一定不能错过。这里总结了一些我认为有用的和最近面试遇到的网络相关知识点。计算机...
继续阅读 »
计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及并用到的知识点,可想而知它的重要性。所以它也成为了iOS面试中经常被问及的问题。准备面试的话,网络相关的知识点一定不能错过。这里总结了一些我认为有用的和最近面试遇到的网络相关知识点。


计算机网络是如何分层的

网络有两种分层模型,一种是ISO(国际标准化组织)制定的OSI(Open System Interconnect)模型,它将网络分为七层。一种是TCP/IP的四层网络模型。OSI是一种学术上的国际标准,理想概念,TCP/IP是事实上的国际标准,被广泛应用于现实生活中。两者的关系可以看这个图:




注:也有说五层模型的,它跟四层模型的区别就是,在OSI模型中的数据链路层和物理层,前者将其作为两层,后者将其合并为一层称为网络接口层。一般作为面试题的话都是需要讲出OSI七层模型的。

各个分层的含义以及它们之间的关系用这张图表示:



Http协议

http协议特性

  • HTTP 协议构建于 TCP/IP 协议之上,是一个应用层协议,默认端口号是 80
  • 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
  • 无状态:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。
  • 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传。

请求方法

  • GET:请求获取Request-URI标识的资源,请求参数附加在url上,明文展示。

  • POST:在Request-URI所标识的资源后附加新的数据,常用于修改服务器资源或者提交资源到服务器。POST请求体是放到body中的,可以指定编码方式,更加安全。

  • HEAD:请求获取由Request-URI所标识的资源的响应消息报头。

  • PUT:请求服务器存储一个资源,并用Request-URI作为其标识。

  • DELETE:请求服务器删除Request-URI所标识的资源。

  • TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断。

  • OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求。

请求和响应报文

在Chrome查看其请求的Headers信息。

General

这里标记了请求的URL,请求方法为GET。状态码为304,代表文件未修改,可以直接使用缓存的文件。远程地址为185.199.111.153:443,此IP为Github 服务器地址,是因为我的博客是部署在GitHub上的。

除了304还有别的状态码,分别是:

  • 200 OK 客户端请求成功
  • 301 Moved Permanently 请求永久重定向
  • 302 Moved Temporarily 请求临时重定向
  • 304 Not Modified 文件未修改,可以直接使用缓存的文件。
  • 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
  • 401 Unauthorized 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因
  • 404 Not Found 请求的资源不存在,例如,输入了错误的URL
  • 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
  • 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。

Response Headers


content-encoding:用于指定压缩算法

content-length:资源的大小,以十进制字节数表示。

content-type:指示资源的媒体类型。图中所示内容类型为html的文本类型,文字编码方式为utf-8

last-modified:上次内容修改的日期,为6月8号

status:304 文件未修改状态码

注:其中content-type在响应头中代表,需要解析的格式。在请求头中代表上传到服务器的内容格式。

Request Headers




method:GET请求

:path:url路径

:scheme:https请求

accept:通知服务器可以返回的数据类型。

accept-encoding:编码算法,通常是压缩算法,可用于发送回的资源

accept-language:通知服务器预期发送回的语言类型。这是一个提示,并不一定由用户完全控制:服务器应该始终注意不要覆盖用户的显式选择(比如从下拉列表中选择语言)。

cookie:浏览器cookie

user-agent:用户代理,标记系统和浏览器内核

TCP三次握手和四次挥手的过程以及为什么要有三次和四次

在了解TCP握手之前我们先看下TCP的报文样式:

TCP三次握手

示意图如下:



三次握手是指建立一个TCP连接时,需要客户端和服务器总共发送3个数据包。

1、第一次握手(SYN=1, seq=x)

客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。

发送完毕后,客户端进入 SYN_SEND 状态。

2、第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1)

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

3、第三次握手(ACK=1, ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

问题一:为什么需要三次握手呢?

在谢希仁著的《计算机网络》里说,『为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误』。怎么理解呢,我们假设一种情况,有一个建立连接的第一次握手的报文段因为滞留到网络中过了较长时间才发送到服务端。这时服务器是要做ACK应答的,如果只有两次握手就代表连接建立,那服务器此时就要等待客户端发送建立连接之后的数据。而这只是一个因滞留而废弃的请求,是不是白白浪费了很多服务器资源。

从另一个角度看这个问题,TCP是全双工的通信模式,需要保证两端都已经建立可靠有效的连接。在三次握手过程中,我们可以确认的状态是:

第一次握手:服务器确认自己接收OK,服务端确认客户端发送OK。

第二次握手:客户端确认自己发送OK,客户端确认自己接收OK,客户端确认服务器发送OK,客户端确认服务器接收OK。

第三次握手:服务器确认自己发送OK,服务器确认客户端接收OK。

只有握手三次才能达到全双工的目的:确认自己和对方都能够接收和发送消息。

TCP四次挥手

示意图如下:


四次挥手表示要发送四个包,挥手的目的是断开连接。

1、第一次挥手(FIN=1, seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。

发送完毕后,客户端进入 FIN_WAIT_1 状态。

2、第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

3、第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

4、第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。

问题一:为什么挥手需要四次呢?为什么不能将ACK和FIN报文一起发送?

当服务器收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端『你发的FIN我收到了』。只有等到服务端所有的报文都发送完了,才能发FIN报文,所以要将ACK和FIN分开发送,这就导致需要四次挥手。

问题二:为什么TIMED_WAIT之后要等2MSL才进入CLOSED状态?

MSL是TCP报文的最大生命周期,因为TIME_WAIT持续在2MSL就可以保证在两个传输方向上的尚未接收到或者迟到的报文段已经消失,同时也是在理论上保证最后一个报文可靠到达。假设最后一个ACK丢失,那么服务器会再重发一个FIN,这是虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK。

HTTPS的流程

HTTPS = HTTP + TLS/SSL,它使用的端口默认为443,它的建立可以用下图表示:



1、客户端首次请求服务器,告诉服务器自己支持的协议版本,支持的加密算法及压缩算法,并生成一个随机数(client random)告知服务器。

2、服务器确认双方使用的加密方法,并返回给客户端证书以及一个服务器生成的随机数(server random)

3、客户端收到证书后,首先验证证书的有效性,然后生成一个新的随机数(premaster secret),并使用数字证书中的公钥,加密这个随机数,发送给服务器。

4、服务器接收到加密后的随机数后,使用私钥进行解密,获取这个随机数(premaster secret

5、服务器和客户端根据约定的加密方法,使用前面的三个随机数(client random, server random, premaster secret),生成『对话密钥』(session key),用来加密接下来的整个对话过程(对称加密)。

问题一:为什么握手过程需要三个随机数,而且安全性只取决于第三个随机数?

前两个随机数是明文传输,存在被拦截的风险,第三个随机数是通过证书公钥加密的,只有它是经过加密的,所以它保证了整个流程的安全性。前两个随机数的目的是为了保证最终对话密钥的『更加随机性』。

问题二:Charles如何实现HTTPS的拦截?

Charles要实现对https的拦截,需要在客户端安装Charles的证书并信任它,然后Charles扮演中间人,在客户端面前充当服务器,在服务器面前充当客户端。

问题三:为什么有些HTTPS请求(例如微信)抓包结果仍是加密的,如何实现的?


我在聊天过程中并没有抓到会话的请求,在小程序启动的时候到是抓到了一个加密内容。我手动触发该链接会下载一个加密文件,我猜测这种加密是内容层面的加密,它的解密是由客户端完成的,而不是在HTTPS建立过程完成的。

另外在研究这个问题的过程中,又发现了一些有趣的问题:

1、图中所示的三个https请求分别对应三个不同类型的图标,它们分别代表什么意思呢?

感谢iOS憨憨的回答。 第一个图标含义是HTTP/2.0,第二个图标含义是HTTP/1.1,第三个图标加锁是因为我用charles只抓取了443端口的请求,该请求端口为5228,所以不可访问。

2、第三个请求https://mtalk.google.com:5228图标和请求内容都加了锁,这个加锁是在https之上又加了一层锁吗?

这些问题暂时没有确切的答案,希望了解的小伙伴告知一下哈。

DNS解析流程

DNS(Domain name system)域名系统。DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户通过域名访问到对应的服务器(IP地址)。具体的解析流程是这样的:

1、浏览器中输入想要访问的网站域名,操作系统会检查本地hosts文件是否有这个网址的映射关系,如果有就调用这个IP地址映射,完成域名解析。没有的话就走第二步。

2、客户端回向本地DNS服务器发起查询,如果本地DNS服务器收到请求,并可以在本地配置区域资源中查到该域名,就将对应结果返回为给客户端。如果没有就走第三步。

3、根据本地DNS服务器的设置,采用递归或者迭代查询,直至解析完成。

其中递归查询和迭代查询可以用如下两图表示。

递归查询

如图所示,递归查询是由DNS服务器一级一级查询传递的。


迭代查询

如果所示,迭代查询是找到指定DNS服务器,由客户端发起查询。



DNS劫持

DNS劫持发生在DNS服务器上,当客户端请求解析域名时将其导向错误的服务器(IP)地址。

常见的解决办法是使用自己的解析服务器或者是将域名以IP地址的方式发出去以绕过DNS解析。

Cookie和Session的区别

HTTP 是无状态协议,说明它不能以状态来区分和管理请求和响应。也就是说,服务器单从网络连接上无从知道客户身份。

可是怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。

  • Cookie:Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,实际上Cookie是服务器在本地机器上存储的一小段文本,并随着每次请求发送到服务器。Cookie技术通过请求和响应报文中写入Cookie信息来控制客户端的状态。

  • Session:Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。当有用户请求创建一个session时,服务器会先检查这个客户端里是否已经包含了一个Session标识(session id),如果有就通过session id把session检索出来。如果没有就创建一个对应此Session的session id。这个session id会在本次响应中返回给客户端。

两者有以下区别:

1、存储位置:Cookie存放在客户端上,Session数据存放在服务器上。

2、Session 的运行依赖 session id,而 session id 是存在 Cookie 中的,也就是说,如果浏览器禁用了 Cookie ,同时 Session 也会失效

3、安全性:Cookie存在浏览器中,可能会被一些程序复制,篡改;而Session存在服务器相对安全很多。

4、性能:Session会在一定时间内保存在服务器上,当访问增多,会对服务器造成一定的压力。考虑到减轻服务器压力,应当使用Cookie

CDN是干什么用的

CDN(Content Delivery Network),根本作用是将网站的内容发布到最接近用户的网络『边缘』,以提高用户访问速度。概括的来说:CDN = 镜像(Mirror) + 缓存(Cache) + 整体负载均衡(GSLB)。

目前CDN都以缓存网站中的静态数据为主,如CSS、JS、图片和静态网页等数据。用户在从主站服务器请求到动态内容后再从CDN上下载这些静态数据,从而加速网页数据内容的下载速度,如淘宝有90%以上的数据都是由CDN来提供的。

CDN工作流程

一个用户访问某个静态文件(如CSS),这个静态文件的域名假如是http://www.baidu.com,而这个域名最终会被指向CDN全局中CDN负载均衡服务器,再由这个负载均衡服务器来最终分配是哪个地方的访问用户,返回给离这个访问用户最近的CDN节点。之后用户就直接去这个CDN节点访问这个静态文件了,如果这个节点中请求的文件不存在,就会再回到源站去获取这个文件,然后再返回给用户。


Socket的作用

socket位于应用层和传输层之间:


它的作用是为了应用层能够更方便的将数据经由传输层来传输。所以它的本质就是对TCP/IP的封装,然后应用程序直接调用socket API即可进行通信。上文中说的三次握手和四次挥手即是通过socket完成的。

我们可以从iOS中网络库分层找到BSD Sockets,它是位于CFNetwork之下。在CFNetwork中还有一个CFSocket,推测是对BSD Sockets的封装。


WebRTC是干什么用的

WebRTC

是一个可以用在视频聊天,音频聊天或P2P文件分享等Web App中的 API。借助WebRTC,你可以在基于开放标准的应用程序中添加实时通信功能。它支持在同级之间发送视频,语音和通用数据,从而使开发人员能够构建功能强大的语音和视频通信解决方案。该技术可在所有现代浏览器以及所有主要平台的本机客户端上使用。WebRTC项目是开源的并得到Apple,Google,Microsoft和Mozilla等的支持。

如果某一请求只在某一地特定时刻失败率较高,会有哪些原因

这个是某公司二面时的问题,是一个开放性问题,我总结了以下几点可能:

1、该时刻请求量过大

2、该地的网络节点较不稳定

3、用户行为习惯,比如该时刻为上班高峰期,或者某个群体的特定习惯



作者:iOS鑫
链接:https://www.jianshu.com/p/6b16f40d7354



收起阅读 »

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。

Lobster一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!一、功能介绍1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例; 2.组件中...
继续阅读 »


Lobster

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!

一、功能介绍

1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例;
2.组件中自己创建的Application生命周期方法伴随壳子工程Application生命周期调用而调用;
3.组件中自己创建的Application可以配置优先级,用于优先或延后执行。

二、应用场景

组件化框架中,各组件有时需要持有Application的实例,但很多做法是在公共库中创建BaseApplication,  
让壳子工程的Application去继承BaseApplication,进而组件去持有BaseApplication的实例达到使用的目的,
然而这样会加剧组件对公共库的过分依赖,项目较大时,就会造成一定的耦合,可能会出现改一处而动全身的场景。
因此,在组件化当中,各组件应该像一个应用一样维护一个自己的Application,使用时拿的是自己Application的实例,
与其他组件隔离,也与公共库隔离,降低耦合!

三、使用方式

1.需要在壳子工程和其他module中添加如下依赖:

android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [LOBSTER_MODULE_NAME: project.getName()]
}
}
}
}

dependencies {
implementation project(path: ':lobster-annotation')
annotationProcessor project(path: ':lobster-compiler')
...
}

2.在壳子工程和其他Module中的Application中添加注解: ShellApp注解作用于壳子工程(主工程)Application,一般来说只有一个,ModuleApp注解作用于组件Application,可以设置优先级,AppInstance注解作用于组件Application的实例。

// 壳子工程的Application
@ShellApp
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
}
}
// 其他Module的Application
@ModuleApp(priority = 1)
public class Module1App extends Application {
private static final String TAG = "Lobster";

@AppInstance
public static Application mApplication1;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module1App->onCreate");
}
}
// 其他Module的Application
@ModuleApp(priority = 2)
public class Module2App extends Application {
private static final String TAG = "Lobster";
@AppInstance
public static Application mApplication2;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module2App->onCreate");
Toast.makeText(mApplication2, "I come from Module2App", Toast.LENGTH_SHORT).show();
}
}

3.没有了,可以开始耍了!

代码下载:Lobster.zip

收起阅读 »

Android自定义搜索控件 KSearchView

KSearchView自定义搜索控件布局示例代码 <com.kacent.widget.view.KingSearchView android:id="@+id/search_view" android:layout_wi...
继续阅读 »

KSearchView

自定义搜索控件


布局示例代码

 <com.kacent.widget.view.KingSearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
<!-提示文本->
app:hint_text="输入搜索内容"
<!-icon的padding->
app:icon_padding_bottom="5dp"
<!-searchView输入框的padding->
app:search_padding_bottom="10dp"
app:search_padding_start="30dp"
app:search_padding_top="10dp"
<!-searchView背景设置->
app:search_view_background="@drawable/my_search_shape"
app:text_size="8sp" />

设置搜索监听器

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val searchView = findViewById<KingSearchView>(R.id.search_view)
searchView.setQueryListener(object : KingSearchView.OnQueryListener {
override fun onQuery(value: String) {
if (TextUtils.isEmpty(value)) {
Toast.makeText(this@MainActivity, "没有输入相关搜索内容", Toast.LENGTH_SHORT).show()
}
Log.e("搜索内容", value)
}
})
}
}


代码下载:KingSearchView-master.zip

收起阅读 »

Android基础到进阶UI爸爸级TextView介绍+实例

TextView是什么 向用户显示文本,并可选择允许他们编辑文本。TextView是一个完整的文本编辑器,但是基类为不允许编辑;其子类EditText允许文本编辑。 咱们先上一个图看看TextView的继承关系: 从上图可以看出TxtView继承了Vi...
继续阅读 »

TextView是什么


向用户显示文本,并可选择允许他们编辑文本。TextView是一个完整的文本编辑器,但是基类为不允许编辑;其子类EditText允许文本编辑。


咱们先上一个图看看TextView的继承关系:


从上图可以看出TxtView继承了View,它还是Button、EditText等多个组件类的父类。咱们看看这些子类是干嘛的。



  • Button:用户可以点击或单击以执行操作的用户界面元素。

  • CheckedTextView:TextView支持Checkable界面和显示的扩展。

  • Chronometer:实现简单计时器的类。

  • DigitalClock:API17已弃用可用TextClock替代。

  • EditText:用于输入和修改文本的用户界面元素。

  • TextClock:可以将当前日期和/或时间显示为格式化字符串。


看看他的儿子都这么牛掰,何况是爸爸,今天咱就看看这个爸爸级组件:TextView


使用TextView


1.在xml中创建并设置属性



咱们看上图说话。上图的文字显示多种多样,但是也仅包含TextView的部分功能,看看这多种多样的显示也是比较有意思的。


下面咱看看代码实践:


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

    <!--在Design中表示可从左侧控件展示处拖拽至布局文件上,创建简单一个TextView。-->
    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView" />

    <!--修改颜色、大小-->
    <!--设置颜色 @color/color_ff0000位置:app/values/colors-->
    <!--设置大小 @dimen/text_size_18位置:app/values/dimens-->
    <!--设置内容 @string/str_setting_color_size位置:app/values/strings-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/str_setting_color_size"
        android:layout_marginTop="@dimen/dimen_10"
        android:textColor="@color/color_ff0000"
        android:textSize="@dimen/text_size_20" />

    <!--添加图片和使用阴影-->
    <!--添加图片:drawableTop、drawableBottom、drawableLeft(drawableStart)、drawableRight(drawableEnd)-->
    <!--使用阴影:shadowColor(阴影颜色)、shadowDx(tv_2位置为基准,数字越大越往右)、
    shadowDy(tv_2位置为基准,数字越大越往下)、shadowRadius(数字越大越模糊)-->

    <!--图片 @mipmap/ic_launcher 位置:app/mipmap/任意一个目录能找到即可-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@mipmap/ic_launcher"
        android:layout_marginTop="@dimen/dimen_10"
        android:gravity="center_vertical"
        android:shadowColor="@color/color_FF773D"
        android:shadowDx="30"
        android:shadowDy="-20"
        android:shadowRadius="2"
        android:text="右侧添加图片和使用阴影"
        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_20" />

    <!--对电话和邮件增加链接-->
    <!--autoLink对文本内容自动添加E-mail地址、电话号码添加超级链接-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autoLink="email|phone"
        android:gravity="center_vertical"
        android:layout_marginTop="@dimen/dimen_10"
        android:text="可点击跳转邮件:SCC5201314@qq.com\n可点击跳转电话:0215201314"
        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_14" />

    <!--内容过多-->
    <!--maxLength最多显示几行,单行也可用android:singleline="true"-->
    <!--ellipsize,内容显示不下时,显示...(位置最前、中间、最后都可以),这里要加行数限制才行-->
    <!--lineSpacingMultiplier,行距-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:gravity="center_vertical"
        android:lineSpacingMultiplier="1.2"
        android:layout_marginTop="@dimen/dimen_10"
        android:maxLength="2"
        android:text="TxtView继承了View,它还是Button、EditText两个UI组件类的父类。它的作用是在用户界面上显示文本素。从功能上来看TextView就是个文本编辑器,只不过Android关闭的它的可编辑功能。如果需要一个可编辑的文本框,就要使用到它的子类Editext了,Editext允许用户编辑文本框中的内容。TextView和Editext它俩最大的区别就在于TextView不允许用户编辑文本内容,Editext允许用户编辑文本内容。
下面咱写几个实例来详细了解一下TextView的。"

        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_14" />

    <!--background设置背景色-->
    <!--padding内边距(边到可用范围的距离)-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/color_ff0000"
        android:layout_marginTop="@dimen/dimen_10"
        android:padding="10dp"
        android:text="背景色红色的文本"
        android:textColor="@color/white" />


    <!--带边框的文本-->
    <!--layout_margin外边距(TextView到其他控件的距离)-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimen_10"
        android:background="@drawable/bg_tv_frame_red"
        android:padding="10dp"
        android:text="带着红色边框的文本" />

    <!--带边框的文本背景色渐变-->
    <!--代码可实现文本的渐变-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimen_10"
        android:background="@drawable/bg_tv_frame_gradient"
        android:padding="10dp"
        android:textColor="@color/white"
        android:text="带着边框和背景色渐变的文本" />

    
</LinearLayout>

background设置边框的文件 android:background="@drawable/bg_tv_frame_red"


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!--radius四个圆角统一设置,也可以单独对某一个圆角设置。例:topLeftRadius-->
    <corners android:radius="2dp"/>
    <!--边框宽度width、颜色color-->
    <stroke android:width="4px" android:color="@color/color_ff0000" />
</shape>

带着边框和背景色渐变 android:background="@drawable/bg_tv_frame_gradient"


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!--radius四个圆角统一设置,也可以单独对某一个圆角设置。例:topLeftRadius-->
    <corners android:radius="8dp"/>
    <!--边框宽度width、颜色color-->
    <stroke android:width="1dp" android:color="@color/color_ff0000" />
    <!--渐变的颜色设置开始到结束-->
    <gradient
        android:startColor="@color/color_188FFF"
        android:centerColor="@color/color_FF773D"
        android:endColor="@color/color_ff0000"
        android:type="linear"
        />

</shape>

2.在xml中创建,在代码中设置属性




  • 布局文件


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_margin="@dimen/dimen_20"
   android:orientation="vertical">
   <TextView
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="下面是用代码实现效果"
       android:textSize="@dimen/text_size_18"
       android:layout_marginTop="@dimen/dimen_20"
       android:layout_marginBottom="@dimen/dimen_10"
       android:textColor="@color/black"
       android:textStyle="bold" />

   <TextView
       android:id="@+id/tv_flag"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textColor="@color/color_188FFF"
       android:layout_marginTop="@dimen/dimen_10"
       android:text="给文本加划线"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_gradient"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:textColor="@color/white"
       android:text="文字渐变是不是很神奇"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_bg"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:padding="10dp"
       android:text="设置背景色"
       android:textColor="@color/white"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_size"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:textColor="@color/color_ff0000"
       android:text="文字特别大小不一致" />

   <TextView
       android:id="@+id/tv_onclick"
       android:layout_width="match_parent"
       android:layout_marginTop="@dimen/dimen_10"
       android:layout_height="wrap_content"
       android:textSize="@dimen/dimen_20"
       android:text="可点击可长按" />
</LinearLayout>


  • 运行结果




  • 在代码中实现


        //下划线并加清晰
        tv_flag.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
        tv_flag.getPaint().setAntiAlias(true);//抗锯齿

        int[] colors = {0xff188fff0xffff773D0xffff0000};//颜色的数组
        LinearGradient mLinearGradient = new LinearGradient(000
                tv_gradient.getPaint().getTextSize(), colors, null, Shader.TileMode.CLAMP);
        tv_gradient.getPaint().setShader(mLinearGradient);
        tv_gradient.invalidate();

        int fillColor = Color.parseColor("#ff0000");//内部填充颜色
        GradientDrawable gd = new GradientDrawable();//创建drawable
        gd.setColor(fillColor);//设置背景色
        gd.setCornerRadius(10);//设置圆角
        tv_bg.setBackground(gd);//设置背景

        Spannable wordtoSpan = new SpannableString(tv_size.getText().toString());
        //setSpan:参数1,设置文字大小;参数2,开始的文字位置;参数3,结束改变文字位置不包含这个位置
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this18)), 02, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this24)), 25, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this10)), 5, tv_size.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv_size.setText(wordtoSpan);

        //TextView其实也是有点击事件的毕竟它的爸爸Veiew
        tv_onclick.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MLog.e("这里是点击事件");
                Toast.makeText(TextViewActivity.this,"这里是点击事件",Toast.LENGTH_SHORT).show();
            }
        });
        tv_onclick.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                MLog.e("这里长按事件");
                Toast.makeText(TextViewActivity.this,"这里长按事件",Toast.LENGTH_SHORT).show();
                //true表示事件已消费
                return true;
            }
        });


  • 运行结果分析



    • TextView的属性在xml中可以使用的大部分在代码中也是可以实现的,看个人喜好怎么去使用。

    • 因TextView继承View,所以可以使用View的方法。如View.OnClickListener()和View.OnLongClickListener()还有去慢慢探索吧。



3.在代码中创建并设置属性



  • 先看效果图:




  • 下面是实现所用的代码:


  //ll_act_tv布局文件根布局id
  LinearLayout ll_act_tv = findViewById(R.id.ll_act_tv);
  TextView textView = new TextView(this);//创建控件
  textView.setText("蠢代码写的哦");//设置控件内容
  textView.setTextColor(Color.RED);//设置控件颜色
  textView.setTextSize(DensityUtil.dip2px(this20));//设置控件字体大小
  ll_act_tv.addView(textView);

TextView今天就聊到这里,后面还有它的子类,比较子类也是比较厉害的不可能一文搞定。你学会了吗?嘿嘿嘿


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

Android-自定义拼图验证码

废话不多说,先上图: 从效果图开始"临摹" 分析 从上面的效果图中,我们可以很直观的看出一共包含三个元素:背景图、空缺部分、填充部分,需要注意的是: 1. 空缺部分缺失的图片刚好是填充部分 2. 我们把填充部分位置固定在左侧,而随机生成空缺部分在右侧...
继续阅读 »

废话不多说,先上图:


1.gif


从效果图开始"临摹"


分析


从上面的效果图中,我们可以很直观的看出一共包含三个元素:背景图、空缺部分、填充部分,需要注意的是:
1. 空缺部分缺失的图片刚好是填充部分
2. 我们把填充部分位置固定在左侧,而随机生成空缺部分在右侧,增加验证难度


思路



  1. 准备背景图片,通过canvas.drawBitmap() 方法画出背景图

  2. 计算View宽高,随机生成空缺部分的x坐标在(width/3, width)范围,固定填充部分的x左边在(0,width/3)范围内,保证填充部分和空缺部分在初始化时没有重叠。(不严谨,具体数值还要结合空缺部分/填充部分尺寸详细计算,仅提供思路)。

  3. 先随机生成空缺部分,然后根据空缺部分在原来Bitmap上的左边生成一样大小一样形状的图片,用于填充部分。

  4. 然后重写onTouchEvent方法,处理拖动时填充部分的位移,在MotionEvent.ACTION_UP条件下,计算填充部分和空缺部分在画布中的x坐标差值,判断当差值小于阙值 dx 时,则认为通过验证,否则调用 invalidate() 方法重新生成验证码。


主要代码分析


这里重写了onMeasure方法,根据我们准备的原图片尺寸设置View宽高,并且重新生成和View一样尺寸的背景图newBgBitmap,统一尺寸以便后面我们对左边的转化。(这里曾经有些地方参照画布尺寸计算,有些地方参照背景图bitmap尺寸计算,导致填充部分和空缺部分没有吻合)。


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int minimumWidth = getSuggestedMinimumWidth();
/*根据原背景图宽高比设置画布尺寸*/
width = measureSize(minimumWidth, widthMeasureSpec);
float scale = width / (float) bgBitmap.getWidth();
height = (int) (bgBitmap.getHeight() * scale);
setMeasuredDimension(width, height);

/*根据画布尺寸生成相同尺寸的背景图*/
newBgBitmap = clipBitmap(bgBitmap, width, height);
/*根据新的背景图生成填充部分*/
srcBitmap = createSmallBitmap(newBgBitmap);
}


设置画笔的混合模式,生成一张自定义形状的图片供填充部分使用


    public Bitmap createSmallBitmap(Bitmap var) {
Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
/*设置混合模式*/
paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

/*在指定范围随机生成空缺部分坐标,保证空缺部分出现在View右侧*/
int min = width / 3;
int max = width - shadowSize / 2 - padding;
Random random = new Random();
shadowLeft = random.nextInt(max) % (max - min + 1) + min;
Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
canvas1.drawBitmap(var, rect, rectF, paintSrc);
paintSrc.setXfermode(null);
return bitmap;
}

在onDraw()方法中依次画出背景图、空缺部分、填充部分,注意先后顺序(具体细节自行处理,例如阴影、凹凸感等等)


    @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, width, height);
/*画背景图*/
canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

bgPaint.setColor(Color.parseColor("#000000"));
/*画空缺部分周围阴影*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画空缺部分*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

bgPaint.setColor(Color.parseColor("#FFFFFF"));
/*画填充部分周围阴影*/
canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画填充部分*/
canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
}

草纸代码参考


随写随发布?


package com.example.qingfengwei.myapplication;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

import java.util.Random;


public class SlidingVerificationView extends View {

private Bitmap bgBitmap;
private Bitmap newBgBitmap;
private Bitmap srcBitmap;

private Paint paintShadow;
private Paint paintSrc;
private float curX;
private float lastX;

private int dx;
private int shadowSize = dp2px(60);
private int padding = dp2px(40);
private int shadowLeft;
private int srcLeft = padding;

private int width, height;

private Paint bgPaint;

private OnVerifyListener listener;

public SlidingVerificationView(Context context) {
this(context, null);
}

public SlidingVerificationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public SlidingVerificationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paintShadow = new Paint();
paintShadow.setAntiAlias(true);
paintShadow.setColor(Color.parseColor("#AA000000"));


paintSrc = new Paint();
paintSrc.setAntiAlias(true);
paintSrc.setFilterBitmap(true);
paintSrc.setStyle(Paint.Style.FILL_AND_STROKE);
paintSrc.setColor(Color.WHITE);

bgPaint = new Paint();
bgPaint.setMaskFilter(new BlurMaskFilter(5, BlurMaskFilter.Blur.OUTER));
bgPaint.setAntiAlias(true);
bgPaint.setStyle(Paint.Style.FILL);

bgBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.syzt);
}

public void setVerifyListener(OnVerifyListener listener) {
this.listener = listener;
}

public Bitmap clipBitmap(Bitmap bm, int newWidth, int newHeight) {
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
}


public Bitmap createSmallBitmap(Bitmap var) {
Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
/*设置混合模式*/
paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));


/*在指定范围随机生成空缺部分坐标,保证空缺部分出现在View右侧*/
int min = width / 3;
int max = width - shadowSize / 2 - padding;
Random random = new Random();
shadowLeft = random.nextInt(max) % (max - min + 1) + min;
Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
canvas1.drawBitmap(var, rect, rectF, paintSrc);
paintSrc.setXfermode(null);
return bitmap;
}


@Override
public boolean onTouchEvent(MotionEvent event) {
curX = event.getRawX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
dx = (int) (curX - lastX);
srcLeft = dx + padding;
invalidate();
break;
case MotionEvent.ACTION_UP:

boolean isSuccess = Math.abs(srcLeft - shadowLeft) < 8;

if (isSuccess) {
Toast.makeText(getContext(), "验证成功!", Toast.LENGTH_SHORT).show();
Log.d("w", "check success!");
} else {
Toast.makeText(getContext(), "验证失败!", Toast.LENGTH_SHORT).show();
Log.d("w", "check fail!");
srcBitmap = createSmallBitmap(newBgBitmap);
srcLeft = padding;
invalidate();
}

if (listener != null) {
listener.onResult(isSuccess);
}
break;
}

return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int minimumWidth = getSuggestedMinimumWidth();
/*根据原背景图宽高比设置画布尺寸*/
width = measureSize(minimumWidth, widthMeasureSpec);
float scale = width / (float) bgBitmap.getWidth();
height = (int) (bgBitmap.getHeight() * scale);
setMeasuredDimension(width, height);

/*根据画布尺寸生成相同尺寸的背景图*/
newBgBitmap = clipBitmap(bgBitmap, width, height);
/*根据新的背景图生成填充部分*/
srcBitmap = createSmallBitmap(newBgBitmap);

}

private int measureSize(int defaultSize, int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
int result = defaultSize;
switch (mode) {
case MeasureSpec.UNSPECIFIED:
result = defaultSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = size;
break;
}
return result;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, width, height);
/*画背景图*/
canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

bgPaint.setColor(Color.parseColor("#000000"));
/*画空缺部分周围阴影*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画空缺部分*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

bgPaint.setColor(Color.parseColor("#FFFFFF"));
/*画填充部分周围阴影*/
canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画填充部分*/
canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
}

public static int dp2px(float dp) {
float density = Resources.getSystem().getDisplayMetrics().density;
return (int) (density * dp + 0.5f);
}
}



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

手把手教你搭建AndroidJenkins环境及一键自动构建打包

前言: 之前看到后端的同事,在服务器上刷刷的敲命令觉得很酷,给我的感觉是Linux很难上手。自从公司给我配了台服务器后,操作了一下,感觉和想的不是那么一回事,所以还是得多动手,不要空想。 正文开始: 看到网上说Linux安装软件基本是用yum,所以这边也是...
继续阅读 »

前言: 之前看到后端的同事,在服务器上刷刷的敲命令觉得很酷,给我的感觉是Linux很难上手。自从公司给我配了台服务器后,操作了一下,感觉和想的不是那么一回事,所以还是得多动手,不要空想。


正文开始:


看到网上说Linux安装软件基本是用yum,所以这边也是使用yum,从0-1搭建Linux Jenkins,实现Android 一键自动化构建部署打包。


步骤一:安装JDK环境


1.查看jdk的版本:


# yum -y list java*


2.选择要安装的JDK版本(带devel是JDK)


# yum install -y java-1.8.0-openjdk-devel.x86_64


3.安装完后,查看是否安装成功(看到版本号表示安装成功,不需要像Windows配置java_home环境变量)


# java -version


image.png


步骤二:安装Jenkins


1.yum安装Jenkins


# yum install jenkins


如果没有Jenkins的repos,按照官网提示再操作:


# sudo wget -O /etc/yum.repos.d/jenkins.repo  https://pkg.jenkins.io/redhat-stable/jenkins.repo
# sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

2.修改Jenkins默认的配置(怕端口冲突,改成你想要名字和端口):


# cd /etc/sysconfig/
# vi jenkins

image.png


3.启动Jenkins


# service jenkins start


按照提示,到指定目录复制密码,下一步是选择安装默认的插件(推荐),注册一个账户,最后就会来到这个界面,表示Jenkins安装成功。


image.png


步骤三:安装gradle


1.安装gradle gradle.org/releases/


image.png 拿到安装包的下载链接: 创建一个安装的目录,我在新建了一个文件夹/opt/gradle/下 下载


# cd /opt
# mkdir gradle
# cd /opt/gradle
# wget https://downloads.gradle-dn.com/distributions/gradle-6.5-all.zip
# unzip /opt/gradle gradle-6.5-all.zip

步骤四:command tools 下载



  1. Command line tools only linux版本


developer.android.com/studio


# cd /opt
# mkdir android
# cd /opt/android
# wget https://dl.google.com/android/repository/commandlinetools-linux-7302050_latest.zip
# unzip /opt/android commandlinetools-linux-7302050_latest.zip

步骤五:配置gradle和Android SDK 环境变量


# cd /etc
# /etc/profile

image.png 在最后追加环境变量,保存


export ANDROID_HOME="/opt/android"
export GRADLE_HOME="/opt/gradle"
export PATH="$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$GRADLE_HOME/gradle-4.9/bin:$PATH"


刷新当前的shell环境


# source /etc/profile


配置gradle全局代理


def repoConfig = {
all { ArtifactRepository repo ->
if (repo instanceof MavenArtifactRepository) {
def url = repo.url.toString()
if (url.contains('repo1.maven.org/maven2') || url.contains('jcenter.bintray.com')) {
println "gradle 初始化: (${repo.name}: ${repo.url}) 移除"
remove repo
}
}
}
maven { url 'https://maven.aliyun.com/repository/central' }
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
mavenLocal()
mavenCentral()
}

allprojects {
buildscript {
repositories repoConfig
}

repositories repoConfig
}

这里因为项目里面配了一个google的maven地址,导致一直gradle构建超半个小时,排查了很久。


项目builid.gradle里面千万不要配google的maven地址
项目builid.gradle里面千万不要配google的maven地址
项目builid.gradle里面千万不要配google的maven地址
就是他,浪费了我几天时间排查,正常用
google()
jcenter()
就可以了

image.png


查看gradle是否配置成功
# gradle -version

image.png


步骤六:sdkmanager方式安装SDK


因为是sdkmanager访问的是google,所以配置了谷歌代理才可以访问,当时折腾了很久才找到的dl.google.com代理


# cd /etc/
# vi hosts

203.208.40.36 dl.google.com


image.png


# cd /opt/android/cmdline-tools


在里面创建一个latest,将文件夹里面文件全部放到latest里面。


# cd /opt/android/cmdline-tools/latest/bin


查看版本
# ./sdkmanager --list
安装想要的SDK版本
# ./sdkmanager --install "platforms;android-29"
# ./sdkmanager --install "build-tools;29.0.2"

步骤六:安装git


1.下载最新的git github.com/git/git/rel…


2.我通过xftp copy在/root目录下,解压


安装依赖库
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker

# cd /root/git-2.32.0
make prefix=/usr/local all
make prefix=/usr/local install

最后测试下git clone 你的git地址试试能不能拉代码

步骤七:最后一步,Jenkins上创建项目


1.在plugins.jenkins.io/ 下载插件


构建化插件:extended-choice-parameter


image.png


点击高级-上传插件-等待安装成功即可


image.png


2.新建任务


image.png


构建化参数添加参考


image.png


我这个教程,Jenkins里面是零配置,直接执行shell脚本,很方便


具体的脚本自己根据实际需求来编写,我这里就不举例了。


gradle clean
gradle assembleRelease--stacktrace -info

结语: 搭建Jenkins的服务,我深有体会,由于没有服务器,首先在自己电脑搭建了一套Windows的Jenkins,也是遇到了各种疑难问题,最后还是解决了。然后,在Linux上部署这套Jenkins环境,就变得很轻松。这次最大的收获就是熟悉了Linux的操作,实现了Android 一键自动打包上传到服务器,减少了打包,上传服务器繁琐的操作。


如果本文对你有帮助,请帮忙对个赞或者留言,你的鼓励是我分享的动力。




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

Android BaseDialog(开发必备)动画、加载进度、阴影

GitHubAPK使用方法将libray模块复制到项目中,或者直接在build.gradle中依赖:allprojects { repositories { maven { url 'https://jitpack.io' } } } ...
继续阅读 »

GitHub

APK

使用方法

将libray模块复制到项目中,或者直接在build.gradle中依赖:

allprojects {
repositories {

maven { url 'https://jitpack.io' }
}
}
dependencies {
compile 'com.github.AnJiaoDe:BaseDialog:V1.1.8'
}

1.Center


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:background="@drawable/white_shape"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="确定删除吗?"
android:textSize="16sp"
android:gravity="center"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/line"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp"

android:text="取消"
android:id="@+id/tv_cancel"
android:gravity="center"
android:textSize="16sp"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/line"/>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/tv_confirm"
android:layout_height="48dp"
android:text="确定"
android:gravity="center"

android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>

2.Left


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

3.Top


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

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

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

4.Right


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

5.Bottom


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

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

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

6.Progress


public class MainActivity extends BaseActivity {
private BaseDialog dialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_center).setOnClickListener(this);
findViewById(R.id.btn_left).setOnClickListener(this);
findViewById(R.id.btn_top).setOnClickListener(this);
findViewById(R.id.btn_right).setOnClickListener(this);
findViewById(R.id.btn_bottom).setOnClickListener(this);
findViewById(R.id.btn_progress).setOnClickListener(this);

}

@Override
public void onClick(View v) {

switch (v.getId()) {
case R.id.btn_center:
dialog = new BaseDialog(this);
dialog.contentView(R.layout.dialog_center)
.canceledOnTouchOutside(true).show();
dialog.findViewById(R.id.tv_confirm).setOnClickListener(this);
dialog.findViewById(R.id.tv_cancel).setOnClickListener(this);

break;
case R.id.btn_left:
BaseDialog dialog_left = new BaseDialog(this);

dialog_left.contentView(R.layout.dialog_left)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))
.dimAmount(0.5f)
.gravity(Gravity.LEFT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.LEFT)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_top:
BaseDialog dialog_top = new BaseDialog(this);

dialog_top.contentView(R.layout.dialog_photo)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
.dimAmount(0.5f)
.gravity(Gravity.TOP)
.offset(0, ScreenUtils.dpInt2px(this, 48))
.animType(BaseDialog.AnimInType.TOP)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_right:
BaseDialog dialog_right = new BaseDialog(this);

dialog_right.contentView(R.layout.dialog_right)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))

.gravity(Gravity.RIGHT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.RIGHT)
.offset(20, 0)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_bottom:
BaseDialog dialog_bottom = new BaseDialog(this);

dialog_bottom.contentView(R.layout.dialog_photo)
.gravity(Gravity.BOTTOM)
.animType(BaseDialog.AnimInType.BOTTOM)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_progress:

ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.color_iv(0xffffffff)
.color_bg_progress(0xffffffff)
.colors_progress(0xff2a5caa).show();
break;
case R.id.tv_confirm:
dialog.dismiss();
break;
case R.id.tv_cancel:
dialog.dismiss();
break;
}

}
}

源码:

BaseDialog

public class BaseDialog extends Dialog {

public BaseDialog(Context context) {
this(context, 0);

}


public BaseDialog(Context context, int themeResId) {
super(context, themeResId);

requestWindowFeature(Window.FEATURE_NO_TITLE);// 去除对话框的标题
GradientDrawable gradientDrawable = new GradientDrawable();
gradientDrawable.setColor(0x00000000);
getWindow().setBackgroundDrawable(gradientDrawable);//设置对话框边框背景,必须在代码中设置对话框背景,不然对话框背景是黑色的

dimAmount(0.2f);
}

public BaseDialog contentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
return this;
}


public BaseDialog contentView(@NonNull View view) {
getWindow().setContentView(view);
return this;
}

public BaseDialog contentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
return this;
}
public BaseDialog layoutParams(@Nullable ViewGroup.LayoutParams params) {
getWindow().setLayout(params.width, params.height);
return this;
}


/**
* 点击外面是否能dissmiss
*
* @param canceledOnTouchOutside
* @return
*/
public BaseDialog canceledOnTouchOutside(boolean canceledOnTouchOutside) {
setCanceledOnTouchOutside(canceledOnTouchOutside);
return this;
}

/**
* 位置
*
* @param gravity
* @return
*/
public BaseDialog gravity(int gravity) {

getWindow().setGravity(gravity);

return this;

}

/**
* 偏移
*
* @param x
* @param y
* @return
*/
public BaseDialog offset(int x, int y) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.x = x;
layoutParams.y = y;

return this;
}

/*
设置背景阴影,必须setContentView之后调用才生效
*/
public BaseDialog dimAmount(float dimAmount) {

WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.dimAmount = dimAmount;
return this;
}


/*
动画类型
*/
public BaseDialog animType(BaseDialog.AnimInType animInType) {


switch (animInType.getIntType()) {
case 0:
getWindow().setWindowAnimations(R.style.dialog_zoom);

break;
case 1:
getWindow().setWindowAnimations(R.style.dialog_anim_left);

break;
case 2:
getWindow().setWindowAnimations(R.style.dialog_anim_top);

break;
case 3:
getWindow().setWindowAnimations(R.style.dialog_anim_right);

break;
case 4:
getWindow().setWindowAnimations(R.style.dialog_anim_bottom);

break;
}
return this;
}


/*
动画类型
*/
public enum AnimInType {
CENTER(0),
LEFT(1),
TOP(2),
RIGHT(3),
BOTTOM(4);

AnimInType(int n) {
intType = n;
}

final int intType;

public int getIntType() {
return intType;
}
}
}

ProgressDialog

public class ProgressDialog extends BaseDialog {

private MaterialProgressDrawable progress;

private ValueAnimator valueAnimator;
private CircleImageView imageView;

public ProgressDialog(Context context) {
super(context);
setCanceledOnTouchOutside(false);

FrameLayout frameLayout = new FrameLayout(context);

imageView = new CircleImageView(context);

progress = new MaterialProgressDrawable(getContext(), imageView);


//设置圈圈的各种大小
progress.updateSizes(MaterialProgressDrawable.DEFAULT);

progress.showArrow(false);
imageView.setImageDrawable(progress);

frameLayout.addView(imageView);


valueAnimator = valueAnimator.ofFloat(0f, 1f);

valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float n = (float) animation.getAnimatedValue();
//圈圈的旋转角度
progress.setProgressRotation(n * 0.5f);
//圈圈周长,0f-1F
progress.setStartEndTrim(0f, n * 0.8f);
//箭头大小,0f-1F
progress.setArrowScale(n);
//透明度,0-255
progress.setAlpha((int) (255 * n));
}
});

getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
setContentView(frameLayout);

setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {

if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
hide();
return true;
}
return false;
}
});


}


public ProgressDialog duration(long duration) {
valueAnimator.setDuration(duration);

return this;
}


public ProgressDialog radius_iv(float radius_iv) {
imageView.radius(radius_iv);

return this;
}

public ProgressDialog color_iv(int color_iv) {
imageView.color(color_iv);

return this;
}

public ProgressDialog color_bg_progress(int color_bg_progress) {
progress.setBackgroundColor(color_bg_progress);

return this;
}

/**
* //圈圈颜色,可以是多种颜色
*
* @param colors_progress
* @return
*/
public ProgressDialog colors_progress(int... colors_progress) {
progress.setColorSchemeColors(colors_progress);

return this;
}

@Override
public void show() {
super.show();
if (progress == null) return;
progress.start();
if (valueAnimator == null) return;
valueAnimator.start();


}

@Override
public void hide() {
super.hide();
if (progress == null) return;
progress.stop();
if (valueAnimator == null) return;
valueAnimator.cancel();


}
}

参考:Android Dialog

GitHub

APK

收起阅读 »

一个Android文字展示动画框架:TextSurface

文字表面一个小动画框架,可以帮助您以漂亮的方式显示消息。用法创建TextSurface实例或将其添加到您的布局中。创建Text具有TextBuilder定义文本外观和位置的实例:Text textDaai = TextBuilder .create("Daa...
继续阅读 »

文字表面

一个小动画框架,可以帮助您以漂亮的方式显示消息。


用法

  1. 创建TextSurface实例或将其添加到您的布局中。
  2. 创建Text具有TextBuilder定义文本外观和位置的实例

Text textDaai = TextBuilder
.create("Daai")
.setSize(64)
.setAlpha(0)
.setColor(Color.WHITE)
.setPosition(Align.SURFACE_CENTER).build();

  1. 创建动画并将它们传递给TextSurface实例:

textSurface.play(
new Sequential(
Slide.showFrom(Side.TOP, textDaai, 500),
Delay.duration(500),
Alpha.hide(textDaai, 1500)
)
);


调整动画

  • 要按顺序播放动画,请使用 Sequential.java

  • 要同时播放动画,请使用 Parallel.java

  • 动画/效果可以这样组合:

    new Parallel(Alpha.show(textA, 500), ChangeColor.to(textA, 500, Color.RED))

    即文本的 alpha 和颜色将在 500 毫秒内同时更改

添加您自己的动画/效果

您可以扩展两个基本类来添加自定义动画:

Proguard 配置

该框架基于reflection广泛使用的标准 android 动画类为避免混淆,您需要排除框架的类:

-keep class su.levenetc.android.textsurface.** { *; }


下载

repositories {
maven { url "https://jitpack.io" }
}
//...
dependencies {
//...
compile 'com.github.elevenetc:textsurface:0.9.1'
}




github地址:https://github.com/elevenetc/TextSurface
下载地址:master.zip
收起阅读 »

基础篇 - 从构建层面看 import 和 require 的区别

前言 一切的一切,都是因为群里的一个问题 虽然说最近在做 webpack 相关的事情,但是也没有对着干问题做过相关的研究,网上很多文章包括 vue 都介绍了建议使用 import ,但是没有说为什么要使用 import,对于开发者来说,调用的方式是没有区别的...
继续阅读 »

前言


一切的一切,都是因为群里的一个问题


image.png


虽然说最近在做 webpack 相关的事情,但是也没有对着干问题做过相关的研究,网上很多文章包括 vue 都介绍了建议使用 import ,但是没有说为什么要使用 import,对于开发者来说,调用的方式是没有区别的,那么为什么 import 的包就要比 require 的包小呢


这里暂时就不说什么调用方式了,什么动态加载(require)、静态编译(import)的,这个网上都有,这篇文章就是分析一下为什么要用 import,而不用 require


正文


首先本地先基于 webpack 搭建一个环境只是为了测试,不需要搭建太复杂的内容


基础文件内容


// webpack.config.js
module.exports = {
mode: 'development',
entry: './src/index.js'
}

index.js 内添加两种调用方式


function test() {
const { b } = import('./importtest')
console.log(b())
}
test()

// or

function test() {
const { b } = require('./requiretest')
console.log(b())
}
test()

importtest.js 中也是简单输出一下


// importtest.js
export default {
b: function () {
return {
name: 'zhangsan'
}
}
}

requiretest.js 也是如此


// requiretest.js
module.exports = {
b: function() {
return {
name: 'lisi'
}
}
}

上述的方式分别执行 webpack 后,输出的内容分别如下


import 输出


image.png


在打包时一共输出了两个文件:main.jssrc_importtest_js.jsmain.js 里面输出的内容如下


image.png


main.js 里面就是 index.js 里面的内容,importtest 里面的内容,是通过一个索引的方式引用过来的,引用的地址就是 src_importtest_js.js


require 输出


image.png


require 打包时,直接输出了一个文件,就只有一个 main.jsmain.js 里面输出的内容如下


image.png


main.js 里面的内容是 index.jsrequiretest.js 里面的所有内容


综上所述,我们从数据角度来看 import 的包是要大于 require 的,但通过打包文件来看,由业务代码导致的文件大小其实 import 是要小于 require 的
复制代码

多引用情况下导致的打包变化


这个时候我们大概知道了 importrequire 打包的区别,接下来我们可以模拟一下一开始那位同学的问题,直接修改一下 webpack.config.js 的入口即可


module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
index1: './src/index1.js'
}
}
复制代码

这里直接保证 index.jsindex1.js 的内容一样即可,还是先测试一下 import 的打包


image.png


这里的内容和单入口时打包的 import 基本一致,里面出了本身的内容外,都是引用的 src_importtest_js 的地址,那么在看看 require 的包


image.png


这里内容和单入口打包的 require 基本一致,都是把 requiretest 的内容复制到了对应的文件内


虽然我们现在看的感觉多入口打包,还是 import 的文件要比 require 的文件大,但是核心问题在于测试案例的业务代码量比较少,所以看起来感觉 import 要比 require 大,当我们的业务代码量达到实际标准的时候,区别就看出来了


总结


import: 打包的内容是给到一个路径,通过该路径来访问对应的内容


require: 把当前访问资源的内容,打包到当前的文件内


到这里就可以解释为什么 vue 官方和网上的文章说推荐 import 而不推荐 require,因为每一个使用 require 的文件会把当前 require 的内容打包到当前文件内,所以导致了文件的过大,使用 import,抛出来的是一个索引,所以不会导致重复内容的打包,就不会出现包大的情况


当然这也不是绝对的,就好像上述案例那种少量的业务代码,使用 import 的代码量其实要比 require 大,所以不建议大家直接去确定某一种方式是最好的,某一种方式就是不行的,依场景选择方法


尾声


这篇文章就是一个简单的平时技术方面基础研究的简介,不是特别高深的东西,还希望对大家有所帮助,如果有覆盖面不够,或者场景不全面的情况,还希望大家提出,我在继续补充


这种类型的文章不是我擅长的方向,还是喜欢研究一些新的东西,欢迎大家指教:


链接:https://juejin.cn/post/6987219589612601357

收起阅读 »

小程序页面返回传值四种解决方案总结

使用场景 小程序从A页面跳转到B页面,在B页面选择一个值后返回到A页面,在A页面使用在B页面选中的值。例如:在购买订单页面跳转到地址列表,选择完地址以后回退到订单页面,订单页面的配送地址需要同步更新。 解决方案 常见的比容要容易解决的方案是使用小程序的全局存储...
继续阅读 »

使用场景


小程序从A页面跳转到B页面,在B页面选择一个值后返回到A页面,在A页面使用在B页面选中的值。例如:在购买订单页面跳转到地址列表,选择完地址以后回退到订单页面,订单页面的配送地址需要同步更新。


解决方案


常见的比容要容易解决的方案是使用小程序的全局存储globalData、本地缓存storage、获取小程序的页面栈,调用上一个Page的setData方法、以及利用wx.navigateTo的events属性监听被打开页面发送到当前页面的数据。下面给大家简单对比下四种方法的优缺点:


1、使用globalData实现


//page A
const app = getApp() //获取App.js实例
onShow() {//生命周期函数--监听页面显示
if (app.globalData.backData) {
this.setData({ //将B页面更新完的值渲染到页面上
backData: app.globalData.backData
},()=>{
delete app.globalData.backData //删除数据 避免onShow重复渲染
})
}
}
//page B
const app = getApp() //获取App.js实例
changeBackData(){
app.globalData.backData = '我被修改了'
wx.navigateBack()
}

2、使用本地缓存Storage实现


//page A
onShow: function () {
let backData = wx.getStorageSync('backData')
if(backData){
this.setData({
backData
},()=>{
wx.removeStorageSync('backData')
})
}
},
//page B
changeBackData(){
wx.setStorageSync('backData', '我被修改了')
wx.navigateBack()
},

3、使用小程序的Page页面栈实现


使小程序的页面栈,比其他两种方式会更方便一点而且渲染的会更快一些,不需要等回退到A页面上再把数据渲染出来,在B页面上的直接就会更新A页面上的值,回退到A页面的时候,值已经被更新了。globalData和Storage实现的原理都是在B页面上修改完值以后,回退到A页面,触发onShow生命周期函数,来更新页面渲染。


//page B
changeBackData(){
const pages = getCurrentPages();
const beforePage = pages[pages.length - 2]
beforePage.setData({ //会直接更新A页面的数据,A页面不需要其他操作
backData: "我被修改了"
})

4、使用wx.navigateTo API的events实现


wx.navigateTo的events的实现原理是利用设计模式的发布订阅模式实现的,有兴趣的同学可以自己动手实现一个简单的,也可以实现相同的效果。


//page A
goPageB() {
wx.navigateTo({
url: 'B',
events: {
getBackData: res => { //在events里面添加监听事件
this.setData({
backData: res.backData
})
},
},
})
},
//page B
changeBackData(){
const eventChannel = this.getOpenerEventChannel()
eventChannel.emit('getBackData', {
backData: '我被修改了'
});
wx.navigateBack()
}

总结


1和2两种方法在页面渲染效果上比后面两种稍微慢一点,3和4两种方法在B页面回退到A页面之前已经触发了更新,而1和2两种方法是等返回到A页面之后,在A页面才触发更新。并且1和2两种方式,要考虑到A页面更新完以后要删除globalData和Storage的数据,避免onShow方法里面重复触发setData更新页面,所以个人更推荐大家使用后面的3和4两种方式。


链接:https://juejin.cn/post/6986556857703727117

收起阅读 »

腾讯面试官:兄弟,你说你会Webpack,那说说他的原理?

原理图解 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser 2、然后使用@babel/traverse去找出入口文件所有依赖模块 3、然后使用@babel/core+@babel/preset-env将入...
继续阅读 »

image.png


原理图解



  • 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser

  • 2、然后使用@babel/traverse去找出入口文件所有依赖模块

  • 3、然后使用@babel/core+@babel/preset-env将入口文件的AST转为Code

  • 4、将2中找到的入口文件的依赖模块,进行遍历递归,重复执行1,2,3

  • 5。重写require函数,并与4中生成的递归关系图一起,输出到bundle


截屏2021-07-21 上午7.39.26.png


代码实现


webpack具体实现原理是很复杂的,这里只是简单实现一下,让大家粗略了解一下,webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!


image.png


目录


截屏2021-07-21 上午7.47.33.png


config.js


这个文件中模拟webpack的配置


const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}

入口文件


src/index.js是入口文件


// src/index
import { age } from './aa.js'
import { name } from './hh.js'

console.log(`${name}今年${age}岁了`)

// src/aa.js
export const age = 18

// src/hh.js
console.log('我来了')
export const name = '林三心'


1. 定义Compiler类


// index.js
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {}
// 重写 require函数,输出bundle
generate() {}
}

2. 解析入口文件,获取 AST


我们这里使用@babel/parser,这是babel7的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树


const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const ast = Parser.getAst(this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

3. 找出所有依赖模块


Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

4. AST 转换为 code


AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
const code = getCode(ast)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

5. 递归解析所有依赖项,生成依赖关系图


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

6. 重写 require 函数,输出 bundle


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
this.generate(dependencyGraph)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
// 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
generate(code) {
// 输出文件路径
const filePath = path.join(this.output.path, this.output.filename)
// 懵逼了吗? 没事,下一节我们捋一捋
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})`

// 把文件内容写入到文件系统
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}

new Compiler(options).run()

7. 看看main里的代码


实现了上面的代码,也就实现了把打包后的代码写到main.js文件里,咱们来看看那main.js文件里的代码吧:


(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('./src/index.js')
})({
"./src/index.js": {
"dependecies": {
"./aa.js": "./src\\aa.js",
"./hh.js": "./src\\hh.js"
},
"code": "\"use strict\";\n\nvar _aa = require(\"./aa.js\");\n\nvar _hh = require(\"./hh.js\");\n\nconsole.log(\"\".concat(_hh.name, \"\\u4ECA\\u5E74\").concat(_aa.age, \"\\u5C81\\u4E86\"));"
},
"./src\\aa.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.age = void 0;\nvar age = 18;\nexports.age = age;"
},
"./src\\hh.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nconsole.log('我来了');\nvar name = '林三心';\nexports.name = name;"
}
})

大家可以执行一下main.js的代码,输出结果是:


我来了
林三心今年18岁了

image.png


结语


webpack具体实现原理是很复杂的,这里只是简单实现一下,让大家粗略了解一下,webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!



链接:https://juejin.cn/post/6987180860852142093

收起阅读 »

Electron上手指南

前置 配置代理,解决网络问题: npm set electron_mirror https://npm.taobao.org/mirrors/electron/ # electron 二进制包镜像 npm set ELECTRON_MIRROR https:/...
继续阅读 »

前置


配置代理,解决网络问题:


npm set electron_mirror https://npm.taobao.org/mirrors/electron/ # electron 二进制包镜像
npm set ELECTRON_MIRROR https://cdn.npm.taobao.org/dist/electron/ # electron 二进制包镜像

安装:


npm install electron --save-dev

使用


和开发 Web 应用非常类似。


index.html


<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <title>Hello World!</title>
</head>
<body>
  <h1>Hello World!</h1>
  We are using Node.js <span id="node-version"></span>,
  Chromium <span id="chrome-version"></span>,
  and Electron <span id="electron-version"></span>.
</body>
</html>

main.js


const { app, BrowserWindow } = require('electron')

function createWindow() {
const win = new BrowserWindow({
  width: 800,
  height: 600
})

win.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()
})

甚至可以直接加载一个现成的线上应用:


const { app, BrowserWindow } = require('electron')

function createWindow() {
const win = new BrowserWindow({
  width: 800,
  height: 600
})

win.loadURL('https://www.baidu.com/')
}

app.whenReady().then(() => {
createWindow()
})

package.json


{
"name": "electron-demo",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
  "start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
  "electron": "^13.1.7"
}
}

执行:


npm start

打包构建


npm install --save-dev @electron-forge/cli
npx electron-forge import

npm run make

流程模型


Electron 与 Chrome 类似采用多进程架构。作为 Electron 应用开发者,可以控制着两种类型的进程:主进程和渲染器。


主进程


每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。


窗口管理


主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。
BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 可从主进程用 window 的 webContent 对象与网页内容进行交互。


const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)
复制代码

应用程序生命周期


主进程还能通过 Electron 的app 模块来控制应用程序的生命周期。 该模块提供了一整套的事件和方法,可以添加自定义的应用程序行为 ( 例如:以编程方式退出您的应用程序、修改程序坞或显示关于面板 ) 。


// 当 macOS 无窗口打开时退出应用
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

渲染器进程


每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。


预加载脚本


预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。


预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。


const { BrowserWindow } = require('electron')
//...
const win = new BrowserWindow({
preload: 'path/to/preload.js'
})
//...

由于预加载脚本与渲染器共享同一个全局 Window 接口,并且可以访问 Node.js API,因此它通过在 window 全局中暴露任意的网络内容来增强渲染器。



链接:https://juejin.cn/post/6987310547133792286

收起阅读 »

你真的了解package.json吗?来看看吧,这可能是最全的package解析

1. 概述 从我们接触前端开始,每个项目的根目录下一般都会有一个package.json文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。 当运行npm install命令的时候,会根据package.json文件中...
继续阅读 »

1. 概述


从我们接触前端开始,每个项目的根目录下一般都会有一个package.json文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。


当运行npm install命令的时候,会根据package.json文件中的配置自动下载所需的模块,也就是配置项目所需的运行和开发环境。


比如下面这个文件,只存在简单的项目名称和版本号。


{
"name" : "yindong",
"version" : "1.0.0",
}

package.json文件是一个JSON对象,这从他的后缀名.json就可以看出来,该对象的每一个成员就是当前项目的一项设置。比如name就是项目名称,version是版本号。


当然很多人其实并不关心package.json的配置,他们应用的更多的是dependencies或devDependencies配置。


下面是一个更完整的package.json文件,详细解释一下每个字段的真实含义。


{
"name": "yindong",
"version":"0.0.1",
"description": "antd-theme",
"keywords":["node.js","antd", "theme"],
"homepage": "https://zhiqianduan.com",
"bugs":{"url":"http://path/to/bug","email":"yindong@xxxx.com"},
"license": "ISC",
"author": "yindong",
"contributors":[{"name":"yindong","email":"yindong@xxxx.com"}],
"files": "",
"main": "./dist/default.js",
"bin": "",
"man": "",
"directories": "",
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"scripts": {
"start": "webpack serve --config webpack.config.dev.js --progress"
},
"config": { "port" : "8080" },
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"babel-plugin-import": "^1.13.3",
"glob": "^7.1.7",
"less": "^3.9.0",
"less-loader": "^9.0.0",
"style-loader": "^2.0.0",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"peerDependencies": {
"tea": "2.x"
},
"bundledDependencies": [
"renderized", "super-streams"
],
"engines": {"node": "0.10.x"},
"os" : [ "win32", "darwin", "linux" ],
"cpu" : [ "x64", "ia32" ],
"private": false,
"publishConfig": {}
}


2. name字段


package.json文件中最重要的就是nameversion字段,这两项是必填的。名称和版本一起构成一个标识符,该标识符被认为是完全唯一的。对包的更改应该与对版本的更改一起进行。


name必须小于等于214个字符,不能以._开头,不能有大写字母,因为名称最终成为URL的一部分因此不能包含任何非URL安全字符。
npm官方建议我们不要使用与核心节点模块相同的名称。不要在名称中加jsnode。如果需要可以使用engines来指定运行环境。


该名称会作为参数传递给require,因此它应该是简短的,但也需要具有合理的描述性。


3. version字段


version一般的格式是x.x.x, 并且需要遵循该规则。


package.json文件中最重要的就是nameversion字段,这两项是必填的。名称和版本一起构成一个标识符,该标识符被认为是完全唯一的。每次发布时version不能与已存在的一致。


4. description字段


description是一个字符串,用于编写描述信息。有助于人们在npm库中搜索的时候发现你的模块。


5. keywords字段


keywords是一个字符串组成的数组,有助于人们在npm库中搜索的时候发现你的模块。


6. homepage字段


homepage项目的主页地址。


7. bugs字段


bugs用于项目问题的反馈issue地址或者一个邮箱。


"bugs": { 
"url" : "https://github.com/owner/project/issues",
"email" : "project@hostname.com"
}

8. license字段


license是当前项目的协议,让用户知道他们有何权限来使用你的模块,以及使用该模块有哪些限制。


"license" : "BSD-3-Clause"

9. author字段 contributors字段


author是具体一个人,contributors表示一群人,他们都表示当前项目的共享者。同时每个人都是一个对象。具有name字段和可选的urlemail字段。


"author": {
"name" : "yindong",
"email" : "yindong@xx.com",
"url" : "https://zhiqianduan.com/"
}

也可以写成一个字符串


"author": "yindong yindong@xx.com (https://zhiqianduan.com/)"

10. files字段


files属性的值是一个数组,内容是模块下文件名或者文件夹名,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)


可以在模块根目录下创建一个.npmignore文件,写在这个文件里边的文件即便被写在files属性里边也会被排除在外,这个文件的写法与.gitignore类似。


11. main字段


main字段指定了加载的入口文件,require导入的时候就会加载这个文件。这个字段的默认值是模块根目录下面的index.js


12. bin字段


bin项用来指定每个内部命令对应的可执行文件的位置。如果你编写的是一个node工具的时候一定会用到bin字段。


当我们编写一个cli工具的时候,需要指定工具的运行命令,比如常用的webpack模块,他的运行命令就是webpack


"bin": {
"webpack": "bin/index.js",
}

当我们执行webpack命令的时候就会执行bin/index.js文件中的代码。


在模块以依赖的方式被安装,如果存在bin选项。在node_modules/.bin/生成对应的文件,
Npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。


所有node_modules/.bin/目录下的命令,都可以用npm run [命令]的格式运行。在命令行下,键入npm run,然后按tab键,就会显示所有可以使用的命令。


13. man字段


man用来指定当前模块的man文档的位置。


"man" :[ "./doc/calc.1" ]

14. directories字段


directories制定一些方法来描述模块的结构, 用于告诉用户每个目录在什么位置。


15. repository字段


指定一个代码存放地址,对想要为你的项目贡献代码的人有帮助


"repository" : {
"type" : "git",
"url" : "https://github.com/npm/npm.git"
}

16. scripts字段


scripts指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令。


"scripts": {
"start": "node ./start.js"
}

使用scripts字段可以快速的执行shell命令,可以理解为alias


scripts可以直接使用node_modules中安装的模块,这区别于直接运行需要使用npx命令。


"scripts": {
"build": "webpack"
}

// npm run build
// npx webpack

17. config字段


config字段用于添加命令行的环境变量。


{
"name" : "yindong",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}

然后,在server.js脚本就可以引用config字段的值。


console.log(process.env.npm_package_config_port); // 8080

用户可以通过npm config set来修改这个值。


npm config set yindong:port 8000

18. dependencies字段, devDependencies字段


dependencies字段指定了项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。


它们的值都是一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。


当安装依赖的时候使用--save参数表示将该模块写入dependencies属性,--save-dev表示将该模块写入devDependencies属性。


"devDependencies": {
"webpack": "^5.38.1",
}

对象的每一项通过一个键值对表示,前面是模块名称,后面是对应模块的版本号。版本号遵循“大版本.次要版本.小版本”的格式规定。



版本说明



固定版本: 比如5.38.1,安装时只安装指定版本。
波浪号: 比如~5.38.1, 表示安装5.38.x的最新版本(不低于5.38.1),但是不安装5.39.x,也就是说安装时不改变大版本号和次要版本号。
插入号: 比如ˆ5.38.1, ,表示安装5.x.x的最新版本(不低于5.38.1),但是不安装6.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
latest: 安装最新版本。




19. peerDependencies字段


当我们开发一个模块的时候,如果当前模块与所依赖的模块同时依赖一个第三方模块,并且依赖的是两个不兼容的版本时就会出现问题。


比如,你的项目依赖A模块和B模块的1.0版,而A模块本身又依赖B模块的2.0版。


大多数情况下,这不构成问题,B模块的两个版本可以并存,同时运行。但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。


最典型的场景就是插件,比如A模块是B模块的插件。用户安装的B模块是1.0版本,但是A插件只能和2.0版本的B模块一起使用。这时,用户要是将1.0版本的B的实例传给A,就会出现问题。因此,需要一种机制,在模板安装的时候提醒用户,如果A和B一起安装,那么B必须是2.0模块。


peerDependencies字段,就是用来供插件指定其所需要的主工具的版本。可以通过peerDependencies字段来限制,使用myless模块必须依赖less模块的3.9.x版本.


{
"name": "myless",
"peerDependencies": {
"less": "3.9.x"
}
}

注意,从npm 3.0版开始,peerDependencies不再会默认安装了。就是初始化的时候不会默认带出。


20. bundledDependencies字段


bundledDependencies指定发布的时候会被一起打包的模块.


21. optionalDependencies字段


如果一个依赖模块可以被使用, 同时你也希望在该模块找不到或无法获取时npm继续运行,你可以把这个模块依赖放到optionalDependencies配置中。这个配置的写法和dependencies的写法一样,不同的是这里边写的模块安装失败不会导致npm install失败。


22. engines字段


engines字段指明了该模块运行的平台,比如Node或者npm的某个版本或者浏览器。


{ "engines" : { "node" : ">=0.10.3 <0.12", "npm" : "~1.0.20" } }

23. os字段


可以指定你的模块只能在哪个操作系统上运行


"os" : [ "darwin", "linux", "win32" ]

24. cpu字段


限制模块只能在某种架构的cpu下运行


"cpu" : [ "x64", "ia32" ]

25. private字段


如果这个属性被设置为truenpm将拒绝发布它,这是为了防止一个私有模块被无意间发布出去。


"private": true

26. publishConfig字段


这个配置是会在模块发布时生效,用于设置发布用到的一些值的集合。如果你不想模块被默认标记为最新的,或者默认发布到公共仓库,可以在这里配置tag或仓库地址。


通常publishConfig会配合private来使用,如果你只想让模块被发布到一个特定的npm仓库,如一个内部的仓库。


"private": true,
"publishConfig": {
"tag": "1.0.0",
"registry": "https://registry.npmjs.org/",
"access": "public"
}

27. preferGlobal字段


preferGlobal的值是布尔值,表示当用户不将该模块安装为全局模块时(即不用–global参数),要不要显示警告,表示该模块的本意就是安装为全局模块。


"preferGlobal": false

28. browser字段


browser指定该模板供浏览器使用的版本。Browserify这样的浏览器打包工具,通过它就知道该打包那个文件。


"browser": {
"tipso": "./node_modules/tipso/src/tipso.js"
},


链接:https://juejin.cn/post/6987179395714646024


收起阅读 »

单独维护图片选择开源库ImagePicker,便于根据个人业务需要进行二次开发的要求

演示1.用法使用前,对于Android Studio的用户,可以选择添加: compile 'com.lzy.widget:imagepicker:0.6.1' //指定版本2.功能和参数含义温馨提示:目前库中的预览界面有个原图的复选框,暂时只做了UI,还没...
继续阅读 »

演示

imageimageimageimage

1.用法

使用前,对于Android Studio的用户,可以选择添加:

	compile 'com.lzy.widget:imagepicker:0.6.1'  //指定版本

2.功能和参数含义

温馨提示:目前库中的预览界面有个原图的复选框,暂时只做了UI,还没有做压缩的逻辑

配置参数参数含义
multiMode图片选着模式,单选/多选
selectLimit多选限制数量,默认为9
showCamera选择照片时是否显示拍照按钮
crop是否允许裁剪(单选有效)
style有裁剪时,裁剪框是矩形还是圆形
focusWidth矩形裁剪框宽度(圆形自动取宽高最小值)
focusHeight矩形裁剪框高度(圆形自动取宽高最小值)
outPutX裁剪后需要保存的图片宽度
outPutY裁剪后需要保存的图片高度
isSaveRectangle裁剪后的图片是按矩形区域保存还是裁剪框的形状,例如圆形裁剪的时候,该参数给true,那么保存的图片是矩形区域,如果该参数给fale,保存的图片是圆形区域
imageLoader需要使用的图片加载器,自需要实现ImageLoader接口即可

3.代码参考

更多使用,请下载demo参看源代码

  1. 首先你需要继承 com.lzy.imagepicker.loader.ImageLoader 这个接口,实现其中的方法,比如以下代码是使用 Picasso 三方加载库实现的
public class PicassoImageLoader implements ImageLoader {

@Override
public void displayImage(Activity activity, String path, ImageView imageView, int width, int height) {
Picasso.with(activity)//
                   .load(Uri.fromFile(new File(path)))//
.placeholder(R.mipmap.default_image)//
.error(R.mipmap.default_image)//
.resize(width, height)//
.centerInside()//
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)//
.into(imageView);
}

@Override
public void clearMemoryCache() {
//这里是清除缓存的方法,根据需要自己实现
}
}
  1. 然后配置图片选择器,一般在Application初始化配置一次就可以,这里就需要将上面的图片加载器设置进来,其余的配置根据需要设置
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_picker);

ImagePicker imagePicker = ImagePicker.getInstance();
imagePicker.setImageLoader(new PicassoImageLoader()); //设置图片加载器
imagePicker.setShowCamera(true); //显示拍照按钮
imagePicker.setCrop(true); //允许裁剪(单选才有效)
imagePicker.setSaveRectangle(true); //是否按矩形区域保存
imagePicker.setSelectLimit(9); //选中数量限制
imagePicker.setStyle(CropImageView.Style.RECTANGLE); //裁剪框的形状
imagePicker.setFocusWidth(800); //裁剪框的宽度。单位像素(圆形自动取宽高最小值)
imagePicker.setFocusHeight(800); //裁剪框的高度。单位像素(圆形自动取宽高最小值)
imagePicker.setOutPutX(1000);//保存文件的宽度。单位像素
imagePicker.setOutPutY(1000);//保存文件的高度。单位像素
}
  1. 以上配置完成后,在适当的方法中开启相册,例如点击按钮时
public void onClick(View v) {
Intent intent = new Intent(this, ImageGridActivity.class);
startActivityForResult(intent, IMAGE_PICKER);
}
}
  1. 如果你想直接调用相机
Intent intent = new Intent(this, ImageGridActivity.class);
intent.putExtra(ImageGridActivity.EXTRAS_TAKE_PICKERS,true); // 是否是直接打开相机
startActivityForResult(intent, REQUEST_CODE_SELECT);
  1. 重写onActivityResult方法,回调结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == ImagePicker.RESULT_CODE_ITEMS) {
if (data != null && requestCode == IMAGE_PICKER) {
ArrayList<ImageItem> images = (ArrayList<ImageItem>) data.getSerializableExtra(ImagePicker.EXTRA_RESULT_ITEMS);
MyAdapter adapter = new MyAdapter(images);
gridView.setAdapter(adapter);
} else {
Toast.makeText(this, "没有数据", Toast.LENGTH_SHORT).show();
}
}
}

代码下载:ImagePicker-master.zip

收起阅读 »

用Activity实现的锁屏程序,可有效的屏蔽Home键,Recent键,通知栏

功能目前市面上大部分锁屏应用都是用悬浮窗实现,而不用Activity。因为用Activity实现的锁屏应用,很大的问题就是Activity能被各种办法关闭或者绕过,所以本项目参考了一些前人的经验,也反编了一些现有锁屏应用的包,最后终于基本解决了所有能绕过Act...
继续阅读 »

功能

目前市面上大部分锁屏应用都是用悬浮窗实现,而不用Activity。因为用Activity实现的锁屏应用,很大的问题就是Activity能被各种办法关闭或者绕过,所以本项目参考了一些前人的经验,也反编了一些现有锁屏应用的包,最后终于基本解决了所有能绕过Activity锁屏的场景,让Activity实现的锁屏也能安安全全的挡在屏幕前。

  1. 亮屏自动启动锁屏Activity

  2. 锁屏界面屏蔽Home键,back键,recent键,防止将Activity退到后台

  3. 锁屏界面禁用通知栏下拉,防止点击通知跳到第三方应用,锁屏被绕过

  4. 最近列表中排除锁屏Activity,防止锁屏Activity在不正常的场景出现

设置说明

  1. 请先设置"我的锁屏"为默认的Launcher程序(桌面应用),才可以正常使用所有功能
  2. 第三方应用无权限禁用系统的锁屏,所以如果设置了密码锁,会出现双重锁屏情况,测试时请先禁用系统锁屏
  3. 来电和闹铃等场景会自动解除锁屏,但是来电和闹铃亮屏后,过程中按电源键关闭屏幕,再打开,锁屏界面会出现在来电或者闹铃界面之上,造成覆盖,需要另做特殊处理
收起阅读 »

Android仿ButterKnife,实现自己的BindView

仿ButterKnife,实现自己的BindViewButterKnife插件的出现让Android程序员从繁琐的findViewById重复代码中解放出来,尤其搭配各种自动生成代码的Android Studio插件,更是如虎添翼。 ButterKnife的实...
继续阅读 »


仿ButterKnife,实现自己的BindView

ButterKnife插件的出现让Android程序员从繁琐的findViewById重复代码中解放出来,尤其搭配各种自动生成代码的Android Studio插件,更是如虎添翼。 ButterKnife的实现原理,大家应该都有所耳闻,利用AbstractProcess,在编译时候为BindView注解的控件自动生成findViewById代码,ButterKnife#bind(Activity)方法,实质就是去调用自动生成的这些findViewById代码。 然而,当我需要去了解这些实现细节的时候,我决定去看看ButterKnife的源码。ButterKnife整个项目涵盖的注解有很多,看起来可能会消耗不少的时间,笔者基于这些天的摸索的该项目的思路,实现了自己的一个BindView注解的使用,来帮助大家了解。

GitHub链接

笔者实现的项目已经上传到Github,欢迎大家star。点击查看MyButterKnife

项目结构

Annotation module

我们需要处理的BindView注解,就声明在这个module里,简单不多说。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value() default -1;
}

Target为FIELD类型,表示这个注解用于类内属性的声明;Retention为CLASS,表示这个注解在项目编译时起作用,如果为RUNTIME则表示在运行时起作用,RUNTIME的注解都是结合反射使用的,所以执行效率上有所欠缺,应该尽量避免使用RUNTIME类注解。 BindView内的value为int类型,正是R.id对应的类型,方便我们直接对View声明其绑定的id:

@BindView(R.id.btn)
protected Button mBtn;

Compiler module

这个module是自动生成findViewById代码的重点,这里只有一个类,继承于AbstractProcessor。

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindProcess extends AbstractProcessor{
private Elements mElementsUtil;

/**
* key: eclosed elemnt
* value: inner views with BindView annotation
*/
private Map<TypeElement,Set<Element>> mElems;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementsUtil = processingEnv.getElementUtils();
mElems = new HashMap<>();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(BindView.class.getCanonicalName());
return types;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("Process start !");

initBindElems(roundEnv.getElementsAnnotatedWith(BindView.class));
generateJavaClass();

System.out.println("Process finish !");
return true;
}

private void generateJavaClass() {
for (TypeElement enclosedElem : mElems.keySet()) {
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(ClassName.get(enclosedElem.asType()),"activity")
.returns(TypeName.VOID);
for (Element bindElem : mElems.get(enclosedElem)) {
methodSpecBuilder.addStatement(String.format("activity.%s = (%s)activity.findViewById(%d)",bindElem.getSimpleName(),bindElem.asType(),bindElem.getAnnotation(BindView.class).value()));
}
TypeSpec typeSpec = TypeSpec.classBuilder("Bind"+enclosedElem.getSimpleName())
.superclass(TypeName.get(enclosedElem.asType()))
.addModifiers(Modifier.FINAL,Modifier.PUBLIC)
.addMethod(methodSpecBuilder.build())
.build();
JavaFile file = JavaFile.builder(getPackageName(enclosedElem),typeSpec).build();
try {
file.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
}

private void initBindElems(Set<? extends Element> bindElems) {
for (Element bindElem : bindElems) {
TypeElement enclosedElem = (TypeElement) bindElem.getEnclosingElement();
Set<Element> elems = mElems.get(enclosedElem);
if (elems == null){
elems= new HashSet<>();
mElems.put(enclosedElem,elems);
System.out.println("Add enclose elem "+enclosedElem.getSimpleName());
}
elems.add(bindElem);
System.out.println("Add bind elem "+bindElem.getSimpleName());
}
}

private String getPackageName(TypeElement type) {
return mElementsUtil.getPackageOf(type).getQualifiedName().toString();
}
}

类注解@AutoServic用于自动生成META-INF信息,对于AbstractProcessor的继承类,需要声明在META-INF里,才能在编译时生效。有了AutoService,可以自动把注解的类加入到META-INF里。使用AutoService需要引入如下包:

compile 'com.google.auto.service:auto-service:1.0-rc2'

然后编译时就会执行proces方法来生成代码,参数annotautions是一个集合,由于上面getSupportedAnnotationTypes返回的是@BindView注解,所以annotations参数里包含所有被@BindView注解的元素。把各元素按照所在类来分组,放入map中,然后generateJavaClass方法中用该map来生成代码,这里使用了javapoet包里的类,能很方便的生成各种java类,方法,修饰符等等。方法体类代码看似复杂,但稍微学一下javapoet包的使用,就可以很快熟练该方法的作用,以下是编译后生成出来的java类代码:

package top.lizhengxian.apt_sample;

public final class BindMainActivity extends MainActivity {
public static void bindView(MainActivity activity) {
activity.mBtn = (android.widget.Button)activity.findViewById(2131427422);
activity.mTextView = (android.widget.TextView)activity.findViewById(2131427423);
}
}

而被注解的原类如下:

public class MainActivity extends AppCompatActivity {

@BindView(R.id.btn)
protected Button mBtn;

@BindView(R.id.text)
protected TextView mTextView;

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

生成的java类位于如下位置: 

mybutterknife module

按理说,上面已经完成了整个findViewById的代码生成,在MainActivity的onCreat方法里,调用完setContentView后,就可以直接调用BindMainActivity.bindView(this)来完成各个View和id的绑定和实例化了。 但是我们观察ButterKnife中的实现,不管是哪个Activity类,都是调用ButterKnife.bindView(this)方法来注入的。而在本项目的代码中,不同的类,就会生成不同名字继承类,比如,如果另有一个HomeActivity类,那注入就要使用BindHomeActivity.bindView(this)来实现。 怎样实现ButterKnife那样统一方法来注入呢? 还是查看源码,可以发现,ButterKnife.bindView方法使用的还是反射来调用生成的类中的方法,也就是说,ButterKnife.bindView只是提供了统一入口。 对照于此,在mybutterknife module里,我们也可以用反射实现类似的方法路由,统一所有的注入方法入口:

public class MyButterKnife {
private static Map<Class,Method> classMethodMap = new HashMap<>();
public static void bindView(Activity target){
if (target != null){
Method method = classMethodMap.get(target.getClass());
try {
if (method == null) {
String bindClassName = target.getPackageName() + ".Bind" + target.getClass().getSimpleName();
Class bindClass = Class.forName(bindClassName);
method = bindClass.getMethod("bindView", target.getClass());
classMethodMap.put(target.getClass(), method);
}
method.invoke(null, target);
}catch (Exception e){
e.printStackTrace();
}
}
}
}

sample module

综上,轻轻松松实现了我们自己的BindView注解,使用方式如下:

public class MainActivity extends AppCompatActivity {

@BindView(R.id.btn)
protected Button mBtn;

@BindView(R.id.text)
protected TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bindView(this);
mBtn.setText("changed");
mTextView.setText("changed too");
}
}

运行代码,完美!


代码下载:MyButterKnife-master.zip

收起阅读 »

快速使用Windows版的MQTT 客户端实现消息收发

    在环信MQTT消息云产品上线之后,很多小伙伴对这个产品都跃跃欲试。从业务方面,比如想要应用在实时位置共享、实时数据传输以及IoT设备管理等业务中;从开发平台,涵盖了C、C#、Android、iOS以及Java等开发语言;综上所述,我...
继续阅读 »

    在环信MQTT消息云产品上线之后,很多小伙伴对这个产品都跃跃欲试。从业务方面,比如想要应用在实时位置共享、实时数据传输以及IoT设备管理等业务中;从开发平台,涵盖了C、C#、Android、iOS以及Java等开发语言;综上所述,我们的小Q真的是功能丰富,搭建简单,人见人爱。

   那为了让大家更快的体验我们小Q收发消息的特性,除了已提供的各端SDK外,我们今天也特地整理了Windows版的MQTT客户端收发消息流程,大家阅后赶快整起吧~

一、环境配置

1、根据电脑配置,下载eclipse安装包,点击下载

2、根据电脑配置,安装JDK,点击下载

3、本次电脑配置为Windows10,64位系统,

(1)eclipse选择为:org.eclipse.paho.ui.app-1.0.2-win32.win32.x86_64.zip;

(2)JDK选择为:jdk-8u291-windows-x64.exe。

上述安装包已上传至百度网盘,下载链接: https://pan.baidu.com/s/1m1q1HX6oTvrSPLFBwW9TBQ 密码: rhe3


二、操作流程

1、安装eclipse后,双击启动应用程序:


2、进入程序页面,点击【+】图标创建会话


3、输入连接信息,包括服务器地址、端口、clientID、用户名和密码

(获取方式见链接:https://docs-im.easemob.com/mqtt/qsquick



4、切换回【MQTT】页面,点击连接


5、添加订阅主题,选择订阅栏下的【+】图标,输入主题名称,点击【订阅】按钮,订阅成功后,可以在右侧的【历史记录】中查看;


6、发送消息,在发布窗口填写主题、QoS、以及消息体内容(消息负载支持json、XML和RAW格式),内容输入完成后,点击【发布】按钮;

7、由于发布主题与订阅主题相同,所以【历史记录】中存在已发布记录和已接收记录(可根据需要,创建不同的客户端实现消息收发)



收起阅读 »

Android 选择图片、上传图片之PictureSelector

效果图: 【注意】Demo已更新到最新版本,并稍作调整。(2019-07-05) 之前出过一篇 Android 选择图片、上传图片之ImagePicker,这个是okgo作者出的,就一般需求来讲是够了,但是没有压缩,需要自己去搞。 后来业务需求...
继续阅读 »


效果图:
这里写图片描述这里写图片描述这里写图片描述这里写图片描述


【注意】Demo已更新到最新版本,并稍作调整。(2019-07-05)





之前出过一篇 Android 选择图片、上传图片之ImagePicker,这个是okgo作者出的,就一般需求来讲是够了,但是没有压缩,需要自己去搞。
后来业务需求提升,页面要美,体验要好,便不是那么满足需求了,所幸在github上找到PictureSelector(然后当时没多久Matisse就开源了…可以看这里Android 选择图片、上传图片之Matisse),也不用自己再撸一个了,下面来介绍介绍PictureSelector



github



https://github.com/LuckSiege/PictureSelector


目前是一直在维护的,支持从相册或拍照选择图片或视频、音频,支持动态权限获取、裁剪(单图or多图裁剪)、压缩、主题自定义配置等功能、适配android 6.0+系统,而且你能遇到的问题,README文档都有解决方案。



功能特点


功能齐全,且兼容性好,作者也做了兼容测试



1.适配android6.0+系统
2.解决部分机型裁剪闪退问题
3.解决图片过大oom闪退问题
4.动态获取系统权限,避免闪退
5.支持相片or视频的单选和多选
6.支持裁剪比例设置,如常用的 1:1、3:4、3:2、16:9 默认为图片大小
7.支持视频预览
8.支持gif图片
9.支持.webp格式图片
10.支持一些常用场景设置:如:是否裁剪、是否预览图片、是否显示相机等
11.新增自定义主题设置
12.新增图片勾选样式设置
13.新增图片裁剪宽高设置
14.新增图片压缩处理
15.新增录视频最大时间设置
16.新增视频清晰度设置
17.新增QQ选择风格,带数字效果
18.新增自定义 文字颜色 背景色让风格和项目更搭配
19.新增多图裁剪功能
20.新增LuBan多图压缩
21.新增单独拍照功能
22.新增压缩大小设置
23.新增Luban压缩档次设置
24.新增圆形头像裁剪
25.新增音频功能查询



主题配置


这个就想怎么改就怎么改了


<!--默认样式 注意* 样式只可修改,不能删除任何一项 否则报错-->
<style name="picture.default.style" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<!--标题栏背景色-->
<item name="colorPrimary">@color/bar_grey</item>
<!--状态栏背景色-->
<item name="colorPrimaryDark">@color/bar_grey</item>
<!--是否改变图片列表界面状态栏字体颜色为黑色-->
<item name="picture.statusFontColor">false</item>
<!--返回键图标-->
<item name="picture.leftBack.icon">@drawable/picture_back</item>
<!--标题下拉箭头-->
<item name="picture.arrow_down.icon">@drawable/arrow_down</item>
<!--标题上拉箭头-->
<item name="picture.arrow_up.icon">@drawable/arrow_up</item>
<!--标题文字颜色-->
<item name="picture.title.textColor">@color/white</item>
<!--标题栏右边文字-->
<item name="picture.right.textColor">@color/white</item>
<!--图片列表勾选样式-->
<item name="picture.checked.style">@drawable/checkbox_selector</item>
<!--开启图片列表勾选数字模式-->
<item name="picture.style.checkNumMode">false</item>
<!--选择图片样式0/9-->
<item name="picture.style.numComplete">false</item>
<!--图片列表底部背景色-->
<item name="picture.bottom.bg">@color/color_fa</item>
<!--图片列表预览文字颜色-->
<item name="picture.preview.textColor">@color/tab_color_true</item>
<!--图片列表已完成文字颜色-->
<item name="picture.complete.textColor">@color/tab_color_true</item>
<!--图片已选数量圆点背景色-->
<item name="picture.num.style">@drawable/num_oval</item>
<!--预览界面标题文字颜色-->
<item name="picture.ac_preview.title.textColor">@color/white</item>
<!--预览界面已完成文字颜色-->
<item name="picture.ac_preview.complete.textColor">@color/tab_color_true</item>
<!--预览界面标题栏背景色-->
<item name="picture.ac_preview.title.bg">@color/bar_grey</item>
<!--预览界面底部背景色-->
<item name="picture.ac_preview.bottom.bg">@color/bar_grey_90</item>
<!--预览界面状态栏颜色-->
<item name="picture.status.color">@color/bar_grey_90</item>
<!--预览界面返回箭头-->
<item name="picture.preview.leftBack.icon">@drawable/picture_back</item>
<!--是否改变预览界面状态栏字体颜色为黑色-->
<item name="picture.preview.statusFontColor">false</item>
<!--裁剪页面标题背景色-->
<item name="picture.crop.toolbar.bg">@color/bar_grey</item>
<!--裁剪页面状态栏颜色-->
<item name="picture.crop.status.color">@color/bar_grey</item>
<!--裁剪页面标题文字颜色-->
<item name="picture.crop.title.color">@color/white</item>
<!--相册文件夹列表选中图标-->
<item name="picture.folder_checked_dot">@drawable/orange_oval</item>
</style>

功能配置


// 进入相册 以下是例子:用不到的api可以不写
PictureSelector.create(MainActivity.this)
.openGallery()//全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()、音频.ofAudio()
.theme()//主题样式(不设置为默认样式) 也可参考demo values/styles下 例如:R.style.picture.white.style
.maxSelectNum()// 最大图片选择数量 int
.minSelectNum()// 最小选择数量 int
.imageSpanCount(4)// 每行显示个数 int
.selectionMode()// 多选 or 单选 PictureConfig.MULTIPLE or PictureConfig.SINGLE
.previewImage()// 是否可预览图片 true or false
.previewVideo()// 是否可预览视频 true or false
.enablePreviewAudio() // 是否可播放音频 true or false
.isCamera()// 是否显示拍照按钮 true or false
.imageFormat(PictureMimeType.PNG)// 拍照保存图片格式后缀,默认jpeg
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true
.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
.setOutputCameraPath("/CustomPath")// 自定义拍照保存路径,可不填
.enableCrop()// 是否裁剪 true or false
.compress()// 是否压缩 true or false
.glideOverride()// int glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
.withAspectRatio()// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
.hideBottomControls()// 是否显示uCrop工具栏,默认不显示 true or false
.isGif()// 是否显示gif图片 true or false
.compressSavePath(getPath())//压缩图片保存地址
.freeStyleCropEnabled()// 裁剪框是否可拖拽 true or false
.circleDimmedLayer()// 是否圆形裁剪 true or false
.showCropFrame()// 是否显示裁剪矩形边框 圆形裁剪时建议设为false true or false
.showCropGrid()// 是否显示裁剪矩形网格 圆形裁剪时建议设为false true or false
.openClickSound()// 是否开启点击声音 true or false
.selectionMedia()// 是否传入已选图片 List<LocalMedia> list
.previewEggs()// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false
.cropCompressQuality()// 裁剪压缩质量 默认90 int
.minimumCompressSize(100)// 小于100kb的图片不压缩
.synOrAsy(true)//同步true或异步false 压缩 默认同步
.cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效 int
.rotateEnabled() // 裁剪是否可旋转图片 true or false
.scaleEnabled()// 裁剪是否可放大缩小图片 true or false
.videoQuality()// 视频录制质量 0 or 1 int
.videoMaxSecond(15)// 显示多少秒以内的视频or音频也可适用 int
.videoMinSecond(10)// 显示多少秒以内的视频or音频也可适用 int
.recordVideoSecond()//视频秒数录制 默认60s int
.isDragFrame(false)// 是否可拖动裁剪框(固定)
.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code

集成方式


compile引入


dependencies {
implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.2.3'
}

build.gradle加入


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

使用


使用非常简单,你想要的基本上都有



package com.yechaoa.pictureselectordemo;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;

import com.luck.picture.lib.PictureSelector;
import com.luck.picture.lib.config.PictureConfig;
import com.luck.picture.lib.config.PictureMimeType;
import com.luck.picture.lib.entity.LocalMedia;
import com.luck.picture.lib.permissions.Permission;
import com.luck.picture.lib.permissions.RxPermissions;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.functions.Consumer;

public class MainActivity extends AppCompatActivity {

private int maxSelectNum = 9;
private List<LocalMedia> selectList = new ArrayList<>();
private GridImageAdapter adapter;
private RecyclerView mRecyclerView;
private PopupWindow pop;

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

mRecyclerView = findViewById(R.id.recycler);

initWidget();
}

private void initWidget() {
FullyGridLayoutManager manager = new FullyGridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(manager);
adapter = new GridImageAdapter(this, onAddPicClickListener);
adapter.setList(selectList);
adapter.setSelectMax(maxSelectNum);
mRecyclerView.setAdapter(adapter);
adapter.setOnItemClickListener(new GridImageAdapter.OnItemClickListener() {
@Override
public void onItemClick(int position, View v) {
if (selectList.size() > 0) {
LocalMedia media = selectList.get(position);
String pictureType = media.getPictureType();
int mediaType = PictureMimeType.pictureToVideo(pictureType);
switch (mediaType) {
case 1:
// 预览图片 可自定长按保存路径
//PictureSelector.create(MainActivity.this).externalPicturePreview(position, "/custom_file", selectList);
PictureSelector.create(MainActivity.this).externalPicturePreview(position, selectList);
break;
case 2:
// 预览视频
PictureSelector.create(MainActivity.this).externalPictureVideo(media.getPath());
break;
case 3:
// 预览音频
PictureSelector.create(MainActivity.this).externalPictureAudio(media.getPath());
break;
}
}
}
});
}

private GridImageAdapter.onAddPicClickListener onAddPicClickListener = new GridImageAdapter.onAddPicClickListener() {

@SuppressLint("CheckResult")
@Override
public void onAddPicClick() {
//获取写的权限
RxPermissions rxPermission = new RxPermissions(MainActivity.this);
rxPermission.requestEach(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.subscribe(new Consumer<Permission>() {
@Override
public void accept(Permission permission) {
if (permission.granted) {// 用户已经同意该权限
//第一种方式,弹出选择和拍照的dialog
showPop();

//第二种方式,直接进入相册,但是 是有拍照得按钮的
// showAlbum();
} else {
Toast.makeText(MainActivity.this, "拒绝", Toast.LENGTH_SHORT).show();
}
}
});
}
};

private void showAlbum() {
//参数很多,根据需要添加
PictureSelector.create(MainActivity.this)
.openGallery(PictureMimeType.ofImage())// 全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()、音频.ofAudio()
.maxSelectNum(maxSelectNum)// 最大图片选择数量
.minSelectNum(1)// 最小选择数量
.imageSpanCount(4)// 每行显示个数
.selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选PictureConfig.MULTIPLE : PictureConfig.SINGLE
.previewImage(true)// 是否可预览图片
.isCamera(true)// 是否显示拍照按钮
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true
//.setOutputCameraPath("/CustomPath")// 自定义拍照保存路径
.enableCrop(true)// 是否裁剪
.compress(true)// 是否压缩
//.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
.glideOverride(160, 160)// glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
.withAspectRatio(1, 1)// 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
//.selectionMedia(selectList)// 是否传入已选图片
//.previewEggs(false)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中)
//.cropCompressQuality(90)// 裁剪压缩质量 默认100
//.compressMaxKB()//压缩最大值kb compressGrade()为Luban.CUSTOM_GEAR有效
//.compressWH() // 压缩宽高比 compressGrade()为Luban.CUSTOM_GEAR有效
//.cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效
.rotateEnabled(false) // 裁剪是否可旋转图片
//.scaleEnabled()// 裁剪是否可放大缩小图片
//.recordVideoSecond()//录制视频秒数 默认60s
.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code
}

private void showPop() {
View bottomView = View.inflate(MainActivity.this, R.layout.layout_bottom_dialog, null);
TextView mAlbum = bottomView.findViewById(R.id.tv_album);
TextView mCamera = bottomView.findViewById(R.id.tv_camera);
TextView mCancel = bottomView.findViewById(R.id.tv_cancel);

pop = new PopupWindow(bottomView, -1, -2);
pop.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
pop.setOutsideTouchable(true);
pop.setFocusable(true);
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 0.5f;
getWindow().setAttributes(lp);
pop.setOnDismissListener(new PopupWindow.OnDismissListener() {

@Override
public void onDismiss() {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 1f;
getWindow().setAttributes(lp);
}
});
pop.setAnimationStyle(R.style.main_menu_photo_anim);
pop.showAtLocation(getWindow().getDecorView(), Gravity.BOTTOM, 0, 0);

View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.tv_album:
//相册
PictureSelector.create(MainActivity.this)
.openGallery(PictureMimeType.ofImage())
.maxSelectNum(maxSelectNum)
.minSelectNum(1)
.imageSpanCount(4)
.selectionMode(PictureConfig.MULTIPLE)
.forResult(PictureConfig.CHOOSE_REQUEST);
break;
case R.id.tv_camera:
//拍照
PictureSelector.create(MainActivity.this)
.openCamera(PictureMimeType.ofImage())
.forResult(PictureConfig.CHOOSE_REQUEST);
break;
case R.id.tv_cancel:
//取消
//closePopupWindow();
break;
}
closePopupWindow();
}
};

mAlbum.setOnClickListener(clickListener);
mCamera.setOnClickListener(clickListener);
mCancel.setOnClickListener(clickListener);
}

public void closePopupWindow() {
if (pop != null && pop.isShowing()) {
pop.dismiss();
pop = null;
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
List<LocalMedia> images;
if (resultCode == RESULT_OK) {
if (requestCode == PictureConfig.CHOOSE_REQUEST) {// 图片选择结果回调

images = PictureSelector.obtainMultipleResult(data);
selectList.addAll(images);

//selectList = PictureSelector.obtainMultipleResult(data);

// 例如 LocalMedia 里面返回三种path
// 1.media.getPath(); 为原图path
// 2.media.getCutPath();为裁剪后path,需判断media.isCut();是否为true
// 3.media.getCompressPath();为压缩后path,需判断media.isCompressed();是否为true
// 如果裁剪并压缩了,以取压缩路径为准,因为是先裁剪后压缩的
adapter.setList(selectList);
adapter.notifyDataSetChanged();
}
}
}

}





Demo:https://github.com/yechaoa/PictureSelectorDemo



收起阅读 »

Android7.0拍照以及使用uCrop裁剪

一、引入 Android在7.0中修改了文件权限,所以从Android7.0开始要使用FileProvider来处理uri,从网上找了好多文章,解决了在7.0下拍照及相册选图的问题,但是参照网上的解决方案前切图片一直搞不定,最终使用了UCrop进行剪切...
继续阅读 »

一、引入



  1. Android在7.0中修改了文件权限,所以从Android7.0开始要使用FileProvider来处理uri,从网上找了好多文章,解决了在7.0下拍照及相册选图的问题,但是参照网上的解决方案前切图片一直搞不定,最终使用了UCrop进行剪切图片并返回文件地址,便于与服务器交互。

  2. 本文主要介绍在Android7.0上进行拍照,相册选图以及相应的图片剪切,当然也会向下兼容,同时我也在Android4.3的手机上进行了测试,在文章最后我会附上源码,会有我自认为详细的注释哈哈。








二、拍照及相册



  1. FileProvider

    想必FileProvider大家都很熟悉了,但是想了一下感觉还是写一下比较好。



    1. 在manifest中配置

       <application

      ... ...

      <provider
      android:name="android.support.v4.content.FileProvider"
      android:authorities="com.sdwfqin.sample.fileprovider"
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/file_paths_public"/>
      </provider>
      </application>

    2. 在 res 目录下新建文件夹 xml 然后创建资源文件 file_paths_public(名字随意,但是要和manifest中的名字匹配)

       <?xml version="1.0" encoding="utf-8"?>
      <paths>
      <!--照片-->
      <external-path
      name="my_images"
      path="Pictures"/>

      <!--下载-->
      <paths>
      <external-path
      name="download"
      path=""/>

      </paths>
      </paths>


  2. 调用相机拍照

     // 全局变量
    public static final int RESULT_CODE_1 = 201;
    // 7.0 以上的uri
    private Uri mProviderUri;
    // 7.0 以下的uri
    private Uri mUri;
    // 图片路径
    private String mFilepath = SDCardUtils.getSDCardPath() + "AndroidSamples";
    -----------
    /**
    * 拍照
    */

    private void camera() {
    File file = new File(mFilepath, System.currentTimeMillis() + ".jpg");
    if (!file.getParentFile().exists()) {
    file.getParentFile().mkdirs();
    }
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // Android7.0以上URI
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    //通过FileProvider创建一个content类型的Uri
    mProviderUri = FileProvider.getUriForFile(this, "com.sdwfqin.sample.fileprovider", file);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, mProviderUri);
    //添加这一句表示对目标应用临时授权该Uri所代表的文件
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    } else {
    mUri = Uri.fromFile(file);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);
    }
    try {
    startActivityForResult(intent, RESULT_CODE_1);
    } catch (ActivityNotFoundException anf) {
    ToastUtils.showShort("摄像头未准备好!");
    }
    }

  3. 相册选图

     // 全局变量
    public static final int RESULT_CODE_2 = 202;
    ----------
    private void selectImg() {
    Intent pickIntent = new Intent(Intent.ACTION_PICK,
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    pickIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
    startActivityForResult(pickIntent, RESULT_CODE_2);
    }

  4. onActivityResult

    需要注意的是拍照没有返回数据,用之前的uri就可以,从相册查找图片会返回uri

     case RESULT_CODE_1:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    // 调用裁剪方法
    cropRawPhoto(mProviderUri);
    } else {
    cropRawPhoto(mUri);
    }
    break;
    case RESULT_CODE_2:
    Log.i(TAG, "onActivityResult: " + data.getData());
    cropRawPhoto(data.getData());
    break;


三、图片剪裁(重点)



  1. 因为用原生的一直是各种报错,所以我这里用的是UCrop,大家可能都见过官方的展示图,界面可能在有些需求下显得过于复杂,但是真正使用起来感觉有很多都是可以修改的哈哈哈!推荐大家看一下官方的例子。项目地址:github.com/Yalantis/uC…


  2. 简单说一下引入方法但是并不能保证是最新的



    1. 依赖

       compile 'com.github.yalantis:ucrop:2.2.1'

    2. 在AndroidManifest中添加Activity

       <activity
      android:name="com.yalantis.ucrop.UCropActivity"
      android:screenOrientation="portrait"
      android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>


  3. 剪切图片

     public void cropRawPhoto(Uri uri) {

    // 修改配置参数(我这里只是列出了部分配置,并不是全部)
    UCrop.Options options = new UCrop.Options();
    // 修改标题栏颜色
    options.setToolbarColor(getResources().getColor(R.color.colorPrimary));
    // 修改状态栏颜色
    options.setStatusBarColor(getResources().getColor(R.color.colorPrimaryDark));
    // 隐藏底部工具
    options.setHideBottomControls(true);
    // 图片格式
    options.setCompressionFormat(Bitmap.CompressFormat.JPEG);
    // 设置图片压缩质量
    options.setCompressionQuality(100);
    // 是否让用户调整范围(默认false),如果开启,可能会造成剪切的图片的长宽比不是设定的
    // 如果不开启,用户不能拖动选框,只能缩放图片
    options.setFreeStyleCropEnabled(true);

    // 设置源uri及目标uri
    UCrop.of(uri, Uri.fromFile(new File(mFilepath, System.currentTimeMillis() + ".jpg")))
    // 长宽比
    .withAspectRatio(1, 1)
    // 图片大小
    .withMaxResultSize(200, 200)
    // 配置参数
    .withOptions(options)
    .start(this);
    }

  4. 剪切完图片的回掉

     if (resultCode == UCrop.RESULT_ERROR){
    mCameraTv.setText(UCrop.getError(data) + "");
    showMsg("图片剪裁失败");
    return;
    }
    if (resultCode == RESULT_OK) {
    switch (requestCode) {
    case UCrop.REQUEST_CROP:
    // 成功(返回的是文件地址)
    Log.i(TAG, "onActivityResult: " + UCrop.getOutput(data));
    mCameraTv.setText(UCrop.getOutput(data) + "");
    // 使用Glide显示图片
    Glide.with(this)
    .load(UCrop.getOutput(data))
    .crossFade()
    .into(mCameraImg);
    break;
    }
    }

  5. 完整的onActivityResult,包含拍照的回掉

     @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == UCrop.RESULT_ERROR){
    mCameraTv.setText(UCrop.getError(data) + "");
    showMsg("图片剪裁失败");
    return;
    }
    if (resultCode == RESULT_OK) {
    switch (requestCode) {
    case RESULT_CODE_1:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    cropRawPhoto(mProviderUri);
    } else {
    cropRawPhoto(mUri);
    }
    break;
    case RESULT_CODE_2:
    Log.i(TAG, "onActivityResult: " + data.getData());
    cropRawPhoto(data.getData());
    break;
    case UCrop.REQUEST_CROP:
    Log.i(TAG, "onActivityResult: " + UCrop.getOutput(data));
    mCameraTv.setText(UCrop.getOutput(data) + "");
    Glide.with(this)
    .load(UCrop.getOutput(data))
    .crossFade()
    .into(mCameraImg);
    break;
    }
    }
    }

    ```



四、源码


源码地址:github.com/sdwfqin/And…   

收起阅读 »

巨大图片显示 Subsampling Scale Image View

适用于 Android 的自定义图像视图,专为照片画廊而设计,无需OutOfMemoryErrors即可显示巨大的图像(例如地图和建筑计划)。包括捏缩放、平移、旋转和动画支持,并允许轻松扩展,因此您可以添加自己的覆盖和触摸事件检测。该视图可选地使用二次采样和图...
继续阅读 »

适用于 Android 的自定义图像视图,专为照片画廊而设计,无需OutOfMemoryErrors即可显示巨大的图像(例如地图和建筑计划)包括捏缩放、平移、旋转和动画支持,并允许轻松扩展,因此您可以添加自己的覆盖和触摸事件检测。

该视图可选地使用二次采样和图块来支持非常大的图像 - 加载低分辨率基础层,当您放大时,它会与可见区域的较小高分辨率图块重叠。这避免了在内存中保存过多数据。它非常适合显示大图像,同时允许您放大高分辨率细节。您可以禁用较小图像的平铺以及显示位图对象时。禁用平铺有一些优点和缺点,以便决定哪个最好,请参阅wiki

演示


特征

图像显示

  • 显示来自资产、资源、文件系统或位图的图像
  • 根据 EXIF 自动旋转文件系统(例如相机或图库)中的图像
  • 以 90° 为增量手动旋转图像
  • 显示源图像的一个区域
  • 在加载大图像时使用预览图像
  • 在运行时交换图像
  • 使用自定义位图解码器

启用平铺:

  • 显示巨大的图像,大于可以加载到内存中
  • 在放大时显示高分辨率细节
  • 测试高达 20,000x20,000 像素,但较大的图像速度较慢

手势检测

  • 一指平底锅
  • 两指捏合放大
  • 快速缩放(一指缩放)
  • 缩放时平移
  • 在平移和缩放之间无缝切换
  • 平移后抛出动量
  • 双击可放大和缩小
  • 禁用平移和/或缩放手势的选项

动画片

  • 为比例和中心设置动画的公共方法
  • 可定制的持续时间和缓动
  • 可选的不间断动画

可覆盖的事件检测

  • 支持OnClickListenerOnLongClickListener
  • 支持使用GestureDetector拦截事件OnTouchListener
  • 扩展以添加您自己的手势

轻松集成

  • 在 a 内使用ViewPager以创建照片库
  • 屏幕旋转后轻松恢复比例、中心和方向
  • 可以扩展以添加随图像移动和缩放的叠加图形
  • 处理视图调整大小和wrap_content布局

快速开始

1)将此库添加为应用程序的 build.gradle 文件中的依赖项。

dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
}

如果您的项目使用 AndroidX,请按如下方式更改工件名称:

dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
}

2)将视图添加到您的布局 XML。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>

3a)现在,在您的片段或活动中,设置图像资源、资产名称或文件路径。

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.resource(R.drawable.monkey));
// ... or ...
imageView.setImage(ImageSource.asset("map.png"))
// ... or ...
imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG"));

3b)或者,如果您Bitmap在内存中有一个对象,请将其加载到视图中。这不适合大图像,因为它绕过了子采样 - 您可能会得到一个OutOfMemoryError.

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.bitmap(bitmap));


github地址:https://github.com/davemorrissey/subsampling-scale-image-view

下载地址:master.zip

收起阅读 »

PhotoView 图片展示

PhotoView 旨在帮助生成一个易于使用的缩放 Android ImageView 实现。依赖将此添加到您的根build.gradle文件(不是您的模块build.gradle文件)中:allprojects { repositories { ...
继续阅读 »

PhotoView 旨在帮助生成一个易于使用的缩放 Android ImageView 实现。


依赖

将此添加到您的根build.gradle文件(不是您的模块build.gradle文件)中:

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

buildscript {
repositories {
maven { url "https://www.jitpack.io" }
}
}

然后,将库添加到您的模块中 build.gradle

dependencies {
implementation 'com.github.chrisbanes:PhotoView:latest.release.here'
}

特征

  • 开箱即用的缩放,使用多点触控和双击。
  • 滚动,平滑滚动。
  • 在滚动父级(例如 ViewPager)中使用时效果很好。
  • 允许在显示的矩阵更改时通知应用程序。当您需要根据当前缩放/滚动位置更新 UI 时很有用。
  • 允许在用户点击照片时通知应用程序。

用法

提供示例展示了如何以更高级的方式使用库,但为了完整起见,以下是让 PhotoView 工作所需的全部内容:

<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageResource(R.drawable.image);

就是这样!

视图组的问题

有一些 ViewGroups(使用 onInterceptTouchEvent 的那些)在放置 PhotoView 时抛出异常,最显着的是ViewPagerDrawerLayout这是一个尚未解决的框架问题。为了防止此异常(通常在缩小时发生),请查看HackyDrawerLayout,您可以看到解决方案是简单地捕获异常。任何使用 onInterceptTouchEvent 的 ViewGroup 也需要扩展并捕获异常。使用HackyDrawerLayout作为如何执行此操作的模板。基本实现是:

public class HackyProblematicViewGroup extends ProblematicViewGroup {

public HackyProblematicViewGroup(Context context) {
super(context);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
//uncomment if you really want to see these errors
//e.printStackTrace();
return false;
}
}
}

与 Fresco 一起使用

由于 Fresco 的复杂性,该库目前不支持 Fresco。这个项目作为一种替代解决方案。


github地址:https://github.com/Baseflow/PhotoView

下载地址:master.zip

收起阅读 »

Android agp 对 R 文件内联支持

agp
本文作者:郑超 背景 最近团队升级静态代码检测能力,依赖的相关编译检测能力需要用到较新的agp,而且目前云音乐agp版本用的是 3.5.0,对比现在 4.2.0 有较大差距,所以我们集中对 agp 进行了一次升级。在升级前通过官方文档,发现在 agp3...
继续阅读 »

本文作者:郑超



背景


最近团队升级静态代码检测能力,依赖的相关编译检测能力需要用到较新的agp,而且目前云音乐agp版本用的是 3.5.0,对比现在 4.2.0 有较大差距,所以我们集中对 agp 进行了一次升级。在升级前通过官方文档,发现在 agp3.6.0 和 4.1.0 版本分别对 R 文件的处理方式进行了相应的升级,具体升级如下。


agp 3.6.0 变更


Simplified R class generation


The Android Gradle plugin simplifies the compile classpath by generating only one R class for each library module in your project and sharing those R classes with other module dependencies. This optimization should result in faster builds, but it requires that you keep the following in mind:



  • Because the compiler shares R classes with upstream module dependencies, it’s important that each module in your project uses a unique package name.

  • The visibility of a library's R class to other project dependencies is determined by the configuration used to include the library as a dependency. For example, if Library A includes Library B as an 'api' dependency, Library A and other libraries that depend on Library A have access to Library B's R class. However, other libraries might not have access to Library B's R class If Library A uses the implementation dependency configuration. To learn more, read about dependency configurations.


从字面意思理解 agp3.6.0 简化了 R 的生成过程,每一个 module 直接生成 R.class (在 3.6.0 之前 R.class 生成的过程是为每个 module 先生成 R.java -> 再通过 javac 生成 R.class ,现在是省去了生成 R.java 和通过 javac 生成 R.class)


现在我们来验证一下这个结果,建一个工程,工程中会建立 android library module。分别用 agp3.5.0 和 agp3.6.0 编译,然后看构建产物。


agp 3.5.0 构建产物如下:


image


agp 3.6.0 构建产物如下:


image


从构建产物上来看也验证了这个结论,agp 3.5.0 到 3.6.0 通过减少 R 生成的中间过程,来提升 R 的生成效率(先生成 R.java 再通过 javac 生成 R.class 变为直接生成 R.class);


agp 4.1.0升级如下:


App size significantly reduced for apps using code shrinking


Starting with this release, fields from R classes are no longer kept by default, which may result in significant APK size savings for apps that enable code shrinking. This should not result in a behavior change unless you are accessing R classes by reflection, in which case it is necessary to add keep rules for those R classes.


从标题看 apk 包体积有显著减少(这个太有吸引力了),通过下面的描述,大致意思是不再保留 R 的 keep 规则,也就是 app 中不再包括 R 文件?(要不怎么减少包体积的)


在分析这个结果之前先介绍下 apk 中,R 文件冗余的问题;


R 文件冗余问题


android 从 ADT 14 开始为了解决多个 library 中 R 文件中 id 冲突,所以将 Library 中的 R 的改成 static 的非常量属性。


在 apk 打包的过程中,module 中的 R 文件采用对依赖库的R进行累计叠加的方式生成。如果我们的 app 架构如下:


image


编译打包时每个模块生成的 R 文件如下:



  1. R_lib1 = R_lib1;

  2. R_lib2 = R_lib2;

  3. R_lib3 = R_lib3;

  4. R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1本身的R)

  5. R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2本身的R)

  6. R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app本身R)


在最终打成 apk 时,除了 R_app(因为 app 中的 R 是常量,在 javac 阶段 R 引用就会被替换成常量,所以打 release 混淆时,app 中的 R 文件会被 shrink 掉),其余的 R 文件全部都会打进 apk 包中。这就是 apk 中 R 文件冗余的由来。而且如果项目依赖层次越多,上层的业务组件越多,将会导致 apk 中的 R 文件将急剧的膨胀。


R 文件内联(解决冗余问题)


系统导致的冗余问题,总不会难住聪明的程序员。在业内目前已经有一些R文件内联的解决方案。大致思路如下:



由于 R_app 是包括了所有依赖的的 R,所以可以自定义一个 transform 将所有 library module 中 R 引用都改成对 R_app 中的属性引用,然后删除所有依赖库中的 R 文件。这样在 app 中就只有一个顶层 R 文件。(这种做法不是非常彻底,在 apk 中仍然保留了一个顶层的 R,更彻底的可以将所有代码中对 R 的引用都替换成常量,并在 apk 中删除顶层的 R )



agp 4.1.0 R 文件内联


首先我们分别用 agp 4.1.0 和 agp 3.6.0 构建 apk 进行一个对比,从最终的产物来确认下是否做了 R 文件内联这件事。 测试工程做了一些便于分析的配置,配置如下:



  1. 开启 proguard


buildTypes {
release {
minifyEnabled true // 打开
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}


  1. 关闭混淆,仅保留压缩和优化(避免混淆打开,带来的识别问题)


// proguard-rules.pro中配置
-dontobfuscate

构建 release 包。 先看下 agp 3.6.0 生成的 apk:


image


从图中可以看到 bizlib module 中会有 R 文件,查看 SecondActivity 的 byte code ,会发现内部有对 R 文件的引用。


接着再来看 agp 4.1.0 生成的 apk:


image


可以看到,bizlib module 中已经没有 R 文件,并且查看 SecondActivity 的 byte code ,会发现内部的引用已经变成了一个常量。


由此可以确定,agp 4.1.0 是做了对 R 文件的内联,并且做的很彻底,不仅删除了冗余的 R 文件,并且还把所有对 R 文件的引用都改成了常量。


具体分析


现在我们来具体分析下 agp 4.1.0 是如何做到 R 内联的,首先我们大致分析下,要对 R 做内联,基本可以猜想到是在 class 到 dex 这个过程中做的。确定了大致阶段,那接下看能不能从构建产物来缩小相应的范围,最好能精确到具体的 task。(题外话:分析编译相关问题一般四板斧:1. 先从 app 的构建产物里面分析相应的结果;2.涉及到有依赖关系分析的可以将所有 task 的输入输出全部打印出来;3. 1、2满足不了时,会考虑去看相应的源码;4. 最后的大招就是调试编译过程;)


首先我们看下构建产物里面的 dex,如下图:


image


接下来在 app module 中增加所有 task 输入输出打印的 gradle 脚本来辅助分析,相关脚本如下:


gradle.taskGraph.afterTask { task ->
try {
println("---- task name:" + task.name)
println("-------- inputs:")
task.inputs.files.each { it ->
println(it.absolutePath)
}
println("-------- outputs:")
task.outputs.files.each { it ->
println(it.absolutePath)
}
} catch (Exception e) {

}
}

minifyReleaseWithR8 相应的输入输出如下:


image


从图中可以看出,输入有整个 app 的 R 文件的集合(R.jar),所以基本明确 R 的内联就是在 minifyReleaseWithR8 task 中处理的。


接下来我们就具体分析下这个 task。 具体的逻辑在 R8Task.kt 里面.


创建 minifyReleaseWithR8 task 代码如下:


class CreationAction(
creationConfig: BaseCreationConfig,
isTestApplication: Boolean = false
) : ProguardConfigurableTask.CreationAction<R8Task, BaseCreationConfig>(creationConfig, isTestApplication) {
override val type = R8Task::class.java
// 创建 minifyReleaseWithR8 task
override val name = computeTaskName("minify", "WithR8")
.....
}

task 执行过程如下(由于代码过多,下面仅贴出部分关键节点):


    // 1. 第一步,task 具体执行
override fun doTaskAction() {
......
// 执行 shrink 操作
shrink(
bootClasspath = bootClasspath.toList(),
minSdkVersion = minSdkVersion.get(),
......
)
}

// 2. 第二步,调用 shrink 方法,主要做一些输入参数和配置项目的准备
companion object {
fun shrink(
bootClasspath: List<File>,
......
)
{
......
// 调用 r8Tool.kt 中的顶层方法,runR8
runR8(
filterMissingFiles(classes, logger),
output.toPath(),
......
)
}
// 3. 第三步,调用 R8 工具类,执行混淆、优化、脱糖、class to dex 等一系列操作
fun runR8(
inputClasses: Collection<Path>,
......
)
{
......
ClassFileProviderFactory(libraries).use { libraryClasses ->
ClassFileProviderFactory(classpath).use { classpathClasses ->
r8CommandBuilder.addLibraryResourceProvider(libraryClasses.orderedProvider)
r8CommandBuilder.addClasspathResourceProvider(classpathClasses.orderedProvider)
// 调用 R8 工具类中的run方法
R8.run(r8CommandBuilder.build())
}
}
}

至此可以知道实际上 agp 4.1.0 中是通过 R8 来做到 R 文件的内联的。那 R8 是如果做到的呢?这里简要描述下,不再做具体代码的分析:



R8 从能力上是包括了 Proguard 和 D8(java脱糖、dx、multidex),也就是从 class 到 dex 的过程,并在这个过程中做了脱糖、Proguard 及 multidex 等事情。在 R8 对代码做 shrink 和 optimize 时会将代码中对常量的引用替换成常量值。这样代码中将不会有对 R 文件的引用,这样在 shrink 时就会将 R 文件删除。



当然要达到这个效果 agp 在 4.1.0 版本里面对默认的 keep 规则也要做一些调整,4.1.0 里面删除了默认对 R 的 keep 规则,相应的规则如下:



-keepclassmembers class **.R$* {
public static <fields>;
}


总结



  1. 从 agp 对 R 文件的处理历史来看,android 编译团队一直在对R文件的生成过程不断做优化,并在 agp 4.1.0 版本中彻底解决了 R 文件冗余的问题。


  2. 编译相关问题分析思路:



    1. 先从 app 的构建产物里面分析相应的结果;

    2. 涉及到有依赖关系分析的可以将所有 task 的输入输出全部打印出来;

    3. 1、2满足不了时,会考虑去看相应的源码;

    4. 最后的大招就是调试编译过程;


  3. 从云音乐 app 这次 agp 升级的效果来看,app 的体积降低了接近 7M,编译速度也有很大的提升,特别是 release 速度快了 10 分钟+(task 合并),整体收益还是比较可观的。



文章中使用的测试工程


参考资料



  1. Shrink, obfuscate, and optimize your app

  2. r8

  3. Android Gradle plugin release notes



本文发布自 网易云音乐大前端团队,文章未经授权禁止任何形式的转载。我们常年招收前端、iOS、Android,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!


收起阅读 »

教你使用whistle工具捉小程序包

介绍 我们说起捉包工具,可能大家比较熟悉的Fiddler工具,它是通过断点修改请求响应的方式,平时使用测试捉包也是很方便的,今天主角介绍另一个捉包工具whistle,这个工具比较轻,无需安装客户端只需通过终端node即可跑起捉取数据 whistle用的是类似...
继续阅读 »

介绍



  • 我们说起捉包工具,可能大家比较熟悉的Fiddler工具,它是通过断点修改请求响应的方式,平时使用测试捉包也是很方便的,今天主角介绍另一个捉包工具whistle,这个工具比较轻,无需安装客户端只需通过终端node即可跑起捉取数据

  • whistle用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能,更多内容介绍请查看官方文档


安装



  1. 安装node 文档地址


$ node -v  // 查看node版本号
v12.0.0 //(建议12版本以上,不然手机捉包会有点问题)


  1. 安装whistle


npm install -g whistle
或者直接指定镜像安装:
npm install whistle -g --registry=https://registry.npm.taobao.org


  1. 使用whistle

    • 启动whistle: (以下指令,window系统不需要$符号)


    $ w2 start


    • 重启whsitle:


    $ w2 restart


    • 停止whistle:3


    $ w2 stop


    • 调试模式启动whistle(主要用于查看whistle的异常及插件开发):


    $ w2 run

    w2 start启动完即可查看本地ip,把ip拷贝到浏览器即可


image.png
在浏览器显示效果
image.png
4. 配置代理 更多配置请查看官方文档

抓取 Https 请求需要配置



  • 电脑上安装根证书(现在安装证书也没那么麻烦,下载完直接点安装一步下一步就行)


   下载根证书:Whistle 监控界面 -> HTTPS -> Download RootCA

   下载完根证书后点击rootCA.crt文件,弹出根证书安装对话框。

   Windows 安装方法:

image.png



  • 移动端需要在设置中配置当前Wi-Fi的代理,以 harmonyOS 为例:


image.png



  • 手机上安装根证书


   iOS:

   Safari 地址栏输入 rootca.pro,按提示安装证书。  

   iOS 10.3 之后需要手动信任自定义根证书,设置路径:设置 --> 通用 --> 关于本机 --> 证书信任设置


   Android:

   用浏览器扫描 whistle 监控界面 HTTPS 的二维码下载安装,或者浏览器地址栏 rootca.pro 按提示安装。

   ca 证书安装完后可以在 Android 手机的“设置” -》“安全和隐私” -》“受信任的凭证” 里查看手机上有没有安装成功。

   部分浏览器不会自动识别 ca 证书,可以通过 Android Chrome 来完成安装。



  • 电脑选择配置勾选捉取https:


image.png



  • 最后捉取得效果图:


image.png


总结



  • 其实使用并不难,按上面安装步骤来即可,这个捉包方法适用于捉取小程序体验版或测试版,不支持小程序正式版本,如果打开小程序正式版本,整个小程序请求接口都会异常无法请求;如果你的体验版小程序无法捉取,请尝试打开调试工具;(本文仅限学习,方便测试使用,还有更多好玩的东西,请移步到官方文档

链接:https://juejin.cn/post/6986888917622456351

收起阅读 »

面试了十几个高级前端,竟然连(扁平数据结构转Tree)都写不出来

前言 招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。 我们看下题目:打平的数据内容如下: let arr = [ {id: 1, name: '部门1...
继续阅读 »

前言


招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。


我们看下题目:打平的数据内容如下:


let arr = [
{id: 1, name: '部门1', pid: 0},
{id: 2, name: '部门2', pid: 1},
{id: 3, name: '部门3', pid: 1},
{id: 4, name: '部门4', pid: 3},
{id: 5, name: '部门5', pid: 4},
]

输出结果


[
{
"id": 1,
"name": "部门1",
"pid": 0,
"children": [
{
"id": 2,
"name": "部门2",
"pid": 1,
"children": []
},
{
"id": 3,
"name": "部门3",
"pid": 1,
"children": [
// 结果 ,,,
]
}
]
}
]

我们的要求很简单,可以先不用考虑性能问题。实现功能即可,回头分析了面试的情况,结果使我大吃一惊。


10%的人没思路,没碰到过这种结构


60%的人说用过递归,有思路,给他个笔记本,但就是写不出来


20%的人在引导下,磕磕绊绊能写出来


剩下10%的人能写出来,但性能不是最佳


感觉不是在招聘季节遇到一个合适的人真的很难。


接下来,我们用几种方法来实现这个小算法


什么是好算法,什么是坏算法


判断一个算法的好坏,一般从执行时间占用空间来看,执行时间越短,占用的内存空间越小,那么它就是好的算法。对应的,我们常常用时间复杂度代表执行时间,空间复杂度代表占用的内存空间。


时间复杂度



时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。



随着n的不断增大,时间复杂度不断增大,算法花费时间越多。 常见的时间复杂度有



  • 常数阶O(1)

  • 对数阶O(log2 n)

  • 线性阶O(n)

  • 线性对数阶O(n log2 n)

  • 平方阶O(n^2)

  • 立方阶O(n^3)

  • k次方阶O(n^K)

  • 指数阶O(2^n)


计算方法



  1. 选取相对增长最高的项

  2. 最高项系数是都化为1

  3. 若是常数的话用O(1)表示


举个例子:如f(n)=3*n^4+3n+300 则 O(n)=n^4


通常我们计算时间复杂度都是计算最坏情况。计算时间复杂度的要注意的几个点



  • 如果算法的执行时间不随n增加增长,假如算法中有上千条语句,执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。 举例如下:代码执行100次,是一个常数,复杂度也是O(1)


    let x = 1;
while (x <100) {
x++;
}


  • 多个循环语句时候,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的方法决定的。举例如下:在下面for循环当中,外层循环每执行一次内层循环要执行n次,执行次数是根据n所决定的,时间复杂度是O(n^2)


  for (i = 0; i < n; i++){
for (j = 0; j < n; j++) {
// ...code
}
}


  • 循环不仅与n有关,还与执行循环判断条件有关。举例如下:在代码中,如果arr[i]不等于1的话,时间复杂度是O(n)。如果arr[i]等于1的话,循环不执行,时间复杂度是O(0)


    for(var i = 0; i<n && arr[i] !=1; i++) {
// ...code
}

空间复杂度



空间复杂度是对一个算法在运行过程中临时占用存储空间的大小。



计算方法:



  1. 忽略常数,用O(1)表示

  2. 递归算法的空间复杂度=(递归深度n)*(每次递归所要的辅助空间)


计算空间复杂度的简单几点



  • 仅仅只复制单个变量,空间复杂度为O(1)。举例如下:空间复杂度为O(n) = O(1)。


   let a = 1;
let b = 2;
let c = 3;
console.log('输出a,b,c', a, b, c);


  • 递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1) = O(n)。


    function fun(n) {
let k = 10;
if (n == k) {
return n;
} else {
return fun(++n)
}
}

不考虑性能实现,递归遍历查找


主要思路是提供一个递getChildren的方法,该方法递归去查找子集。
就这样,不用考虑性能,无脑去查,大多数人只知道递归,就是写不出来。。。


/**
* 递归查找,获取children
*/
const getChildren = (data, result, pid) => {
for (const item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []};
result.push(newItem);
getChildren(data, newItem.children, item.id);
}
}
}

/**
* 转换方法
*/
const arrayToTree = (data, pid) => {
const result = [];
getChildren(data, result, pid)
return result;
}

从上面的代码我们分析,该实现的时间复杂度为O(2^n)


不用递归,也能搞定


主要思路是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //

// 先转成map存储
for (const item of items) {
itemMap[item.id] = {...item, children: []}
}

for (const item of items) {
const id = item.id;
const pid = item.pid;
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n),需要一个Map把数据存储起来,空间复杂度O(n)


最优性能


主要思路也是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储。不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //
for (const item of items) {
const id = item.id;
const pid = item.pid;

if (!itemMap[id]) {
itemMap[id] = {
children: [],
}
}

itemMap[id] = {
...item,
children: itemMap[id]['children']
}

const treeItem = itemMap[id];

if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为O(n),需要一个Map把数据存储起来,空间复杂度O(n)


链接:https://juejin.cn/post/6983904373508145189

收起阅读 »

前端是不是又要回去操作真实dom年代?

写在开头 近期我有写两篇文章,一篇是:petite-vue源码解析和掘金编辑器的源码解析,发现里面用到了Svelte这个框架 加上最近React17,vite大家也在逐步的用在生产环境中,我于是有了今天的思考 看前端的技术演进 原生Javascript ...
继续阅读 »

写在开头



  • 近期我有写两篇文章,一篇是:petite-vue源码解析和掘金编辑器的源码解析,发现里面用到了Svelte这个框架

  • 加上最近React17,vite大家也在逐步的用在生产环境中,我于是有了今天的思考


看前端的技术演进



  • 原生Javascript - Jquery为代表的时代,例如,引入Jquery只要


<script src="cdn/jquery.min,js"></script>


  • 接着便又有了gulp webpack等构建工具出现,React和Vue也在这个时候开始火了起来,随即而来的是一大堆工程化的辅助工具,例如babel,还有提供整套服务的create-react-app等脚手架

  • 这也带来了问题,当然这个是npm的问题,每次启动项目前,都要安装大量的依赖,即便出现了yarn pnpm`等优化的依赖管理工具,但是这个问题根源不应该使用工具解决,而是问题本质是依赖本地化,代码和依赖需要工具帮助才能运行在浏览器中



总结就是:现有的开发模式,让项目太重,例如我要使用某个脚手架,我只想写一个helloworld演示下,结果它让我装500mb的依赖,不同的脚手架产物,配置不同,产物也不同



理想的开发模式




  • 1.不需要辅助的工具配置,我不需要webpack这类帮我打包的工具,模块化浏览器本身就支持,而且是一个规范。例如vite号称不打包,用的是浏览器本身支持的esm模块化,但是它没有解决依赖的问题,因为依赖问题本身是依赖的问题,而不是工具的问题




  • 2.不需要安装依赖,一切都可以import from remote,我觉得webpack5Module Federation设计,就考虑到了这一点,下面是官方的解释:




    • 多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。




    • 这通常被称作微前端,但并不仅限于此。







但是这可能并不是最佳实践,目前是有import from http,例如



import lodash from 'https://unpackage/lodash/es'


  • 这里又会有人问,那你不都是要发请求吗,都是要每次启动的时候去远程拉取,还不如在本地呢。import from http我想只是解决了一个点的问题,就是不用手动安装依赖到本地磁盘

  • 前段时间我写过,在浏览器中本地运行Node.js




这个技术叫WebContainers技术,感兴趣的可以去翻翻我公众号之前的文章




  • 等等,别急。这些仅仅开了个头,新的技术往往要探索才能实现价值最大化,我想此处应该可以彻底颠覆现有的开发模式,而且应该就在3-5年内。


将几个新的前端技术理念融合?



  • vite的不打包理念:直接使用浏览器支持的esm模块化

  • WebContainers技术:让浏览器直接运行node.js

  • import from remote,从一个个远程地址直接引入可以使用的依赖

  • 现在很火的webIDE:类似remix编辑器,直接全部可以在云端搞定

  • 浏览器的优化,天然有缓存支持


会发生什么变化?



  • 我们所有的一切开始,都直接启动一个浏览器即可

  • 浏览器中的webIDE,可以直接引入远程依赖,浏览器可以运行Node.js,使用的都是esm模块化,不需要打包工具,项目启动的时间和热更新时间都非常短,构建也是直接可以在浏览器中构建



这些看似解决了我们之前提出的大部分问题,回到今天的主题





回到主题



  • 前端会不会回到操作原生dom的时代?

  • 我觉得,有这个趋势,例如petite-vue,还有Svelte



因为之前写过petite-vue源码解析了,我们今天就讲讲Svelte



Svelte



Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 在浏览器中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。




  • 与使用虚拟(virtual)DOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM






  • 上面是官方的介绍,我们看看知乎这篇文章https://zhuanlan.zhihu.com/p/97825481,感觉他写得很好,这里照搬一些过来吧直接




  • React和Vue都是基于runtime的框架。所谓基于runtime的框架就是框架本身的代码也会被打包到最终的bundle.js并被发送到用户浏览器。




  • 当用户在你的页面进行各种操作改变组件的状态时,框架的runtime会根据新的组件状态(state)计算(diff)出哪些DOM节点需要被更新





可是,这些被打包进去的框架,实在太大了。



(今天还在跟同事说,前年写的登录站点,纯原生手工打造,性能无敌)



  • 100kb对于一个弱网环境来说,很要命,我们看看svelte减少了多少体积:



科普



  • 虚拟dom并没有加快用户操作浏览器响应的速度,只是说,方便用于数据驱动视图,更便于管理而已,并且在一定程度上,更慢。真正最快的永远是:


currentDom.innerHtml = '前端巅峰';


所以Svelte并不是说多好,而是它的这种理念,可能未来会越来越成为主流



React17的改变



  • 大家应该都知道,现有的浏览器都是无法直接解译JSX的,所以大多数React用户都需要使用Babel或者TypeScript之类的编译器来将JSX转换为浏览器能够理解的JavaScript语言。许多预配置的工具箱(如:Create React App 或者Next.js)内部也有JSX的转换。

  • React 17.0,尽管React团队想对JSX的转换进行改进,但React团队不想打破现有的配置。这就是为什么React团队与Babel合作,为想要升级的开发者提供了一个全新的JSX转换的重写版本。

  • 通过全新的转换,你可以单独使用JSX而无需引入React.



我猜想,或许React团队有意将jsx语法推动到成为es标准语法中去,剥离开来希望会大大提升。



重点



  • 说了这么多,大家可能没理解到重点,那就是:大家都在想着减轻自身的负重,把丢下来的东西标准化,交给浏览器处理,这也是在为未来的只需要打开一个浏览器,就可以完成所有的事情做铺垫

  • 而我,相信这一天应该不远了,据我所知已经有不少顶尖的团队在研发这种产品



链接:https://juejin.cn/post/6986613468975595556

收起阅读 »

面试官:你知道git xx 和git xx的区别吗?看完这篇Git指南后直呼:内行!

Git
前言 作为一名工程师,既然想要加入一个团队,并肩作战地协同开发项目,就必不可少要学会Git基本操作。面试过程中,面试官不止是考察1-3年的工程师,高级岗位也同样会考察团队协作的能力。相信小伙伴们经常会在面试中被问到以下问题吧,可以帮助你测试一下你的Git基础牢...
继续阅读 »

前言


作为一名工程师,既然想要加入一个团队,并肩作战地协同开发项目,就必不可少要学会Git基本操作。面试过程中,面试官不止是考察1-3年的工程师,高级岗位也同样会考察团队协作的能力。相信小伙伴们经常会在面试中被问到以下问题吧,可以帮助你测试一下你的Git基础牢不牢固。



  • 代码开发到一半,需要紧急切换分支修复线上问题,该怎么办?

  • 代码合并有几种模式?分别有什么优缺点?

  • git fetchgit pull有什么区别,有合并操作吗?

  • git mergegit rebase有什么区别,它们的应用场景有哪些?

  • git resetgit revert有什么区别,该如何选择,回滚后的<commit-id>还能找到吗?


如果你心中已有答案,那么可以选择跳过这篇文章啦,愉快地继续摸鱼~


如果你对这些概念还有些模糊,或者没有实际操作过,那么就需要好好阅读本篇文章啦!


接下来马上进入正文啦,本文分四个部分介绍,大家可以自由选择阅读。



  • Git的区域示例图,帮助大家理解Git的结构。

  • Git的基本使用场景,介绍一些常用git命令。

  • Git的进阶使用场景,介绍一些高频出现的面试题目以及应用场景。

  • 最后介绍Git的下载地址、基本配置和工具推荐。


Git的区域


画了一个简单的示意图,供大家参考


yuque_diagram.jpg



  • 远程仓库(Remote):在远程用于存放代码的服务器,远程仓库的内容能够被分布其他地方的本地仓库修改。

  • 本地仓库(Repository):在自己电脑上的仓库,平时我们用git commit 提交到暂存区,就会存入本地仓库。

  • 暂存区(Index):执行 git add 后,工作区的文件就会被移入暂存区,表示哪些文件准备被提交,当完成某个功能后需要提交代码,可以通过 git add 先提交到暂存区。

  • 工作区(Workspace):工作区,开发过程中,平时打代码的地方,看到是当前最新的修改内容。


Git的基本使用场景


以下命令远程主机名默认为origin,如果有其他远程主机,将origin替换为其他即可。


git fetch


# 获取远程仓库特定分支的更新
git fetch origin <分支名>

# 获取远程仓库所有分支的更新
git fetch --all

git pull


# 从远程仓库拉取代码,并合并到本地,相当于 git fetch && git merge 
git pull origin <远程分支名>:<本地分支名>

# 拉取后,使用rebase的模式进行合并
git pull --rebase origin <远程分支名>:<本地分支名>

注意



  • 直接git pull 不加任何选项,等价于git fetch + git merge FETCH_HEAD,执行效果就是会拉取所有分支信息回来,但是只合并当前分支的更改。其他分支的变更没有执行合并。

  • 使用git pull --rebase 可以减少冲突的提交点,比如我本地已经提交,但是远程其他同事也有新的代码提交记录,此时拉取远端其他同事的代码,如果是merge的形式,就会有一个merge的commit记录。如果用rebase,就不会产生该合并记录,是将我们的提交点挪到其他同事的提交点之后。


git branch


# 基于当前分支,新建一个本地分支,但不切换
git branch <branch-name>

# 查看本地分支
git branch

# 查看远程分支
git branch -r

# 查看本地和远程分支
git branch -a

# 删除本地分支
git branch -D <branch-name>

# 基于旧分支创建一个新分支
git branch <new-branch-name> <old-branch-name>

# 基于某提交点创建一个新分支
git branch <new-branch-name> <commit-id>

# 重新命名分支
git branch -m <old-branch-name> <new-branch-name>

git checkout


# 切换到某个分支上
git checkout <branch-name>

# 基于当前分支,创建一个分支并切换到新分支上
git checkout -b <branch-name>

git add


# 添把当前工作区修改的文件添加到暂存区,多个文件可以用空格隔开
git add xxx

# 添加当前工作区修改的所有文件到暂存区
git add .

git commit


# 提交暂存区中的所有文件,并写下提交的概要信息
git commit -m "message"

# 相等于 git add . && git commit -m
git commit -am

# 对最近一次的提交的信息进行修改,此操作会修改commit的hash值
git commit --amend

git push


# 推送提交到远程仓库
git push

# 强行推送到远程仓库
git push -f

git tag


# 查看所有已打上的标签
git tag

# 新增一个标签打在当前提交点上,并写上标签信息
git tag -a <version> -m 'message'

# 为指定提交点打上标签
git tag -a <version> <commit-id>

# 删除指定标签
git tag -d <version>

Git的进阶使用场景



HEAD表示最新提交 ;HEAD^表示上一次; HEAD~n表示第n次(从0开始,表示最近一次)



正常协作



  • git pull 拉取远程仓库的最新代码

  • 工作区修改代码,完成功能开发

  • git add . 添加修改的文件到暂存区

  • git commit -m 'message' 提交到本地仓库

  • git push将本地仓库的修改推送到远程仓库


代码合并


git merge


自动创建一个新的合并提交点merge-commit,且包含两个分支记录。如果合并的时候遇到冲突,仅需要修改解决冲突后,重新commit。



  • 场景:如dev要合并进主分支master,保留详细的合并信息

  • 优点:展示真实的commit情况

  • 缺点:分支杂乱


git checkout master
git merge dev

rf1o2b6eduboqwkigg3w.gif


git merge 的几种模式



  • git merge --ff (默认--ff,fast-farward)

    • 结果:被merge的分支和当前分支在图形上并为一条线,被merge的提交点commit合并到当前分支,没有新的提交点merge

    • 缺点:代码合并不冲突时,默认快速合并,主分支按时间顺序混入其他分支的零碎commit点。而且删除分支,会丢失分支信息。



  • git merge --no-ff(不快速合并、推荐)

    • 结果:被merge的分支和当前分支不在一条线上,被merge的提交点commit还在原来的分支上,并在当前分支产生一个新提交点merge

    • 优点:代码合并产生冲突就会走这个模式,利于回滚整个大版本(主分支自己的commit点)



  • git merge --squash(把多次分支commit历史压缩为一次)

    • 结果:把多次分支commit历史压缩为一次




image.png


git rebase



  • 不产生merge commit,变换起始点位置,“整理”成一条直线,且能使用命令合并多次commit。

  • 如在develop上git rebase master 就会拉取到master上的最新代码合并进来,也就是将分支的起始时间指向master上最新的commit上。自动保留的最新近的修改,不会遇到合并冲突。而且可交互操作(执行合并删除commit),可通过交互式变基来合并分支之前的commit历史git rebase -i HEAD~3

  • 场景:主要发生在个人分支上,如 git rebase master整理自己的dev变成一条线。频繁进行了git commit提交,可用交互操作drop删除一些提交,squash提交融合前一个提交中。

  • 优点:简洁的提交历史

  • 缺点:发生错误难定位,解决冲突比较繁琐,要一个一个解决。


git checkout dev
git rebase master

dwyukhq8yj2xliq4i50e.gifmsofpv7k6rcmpaaefscm.gif


git merge和git rebase的区别



  • merge会保留两个分支的commit信息,而且是交叉着的,即使是ff模式,两个分支的commit信息会混合在一起(按真实提交时间排序),多用于自己dev合并进master。

  • rebase意思是变基,改变分支的起始位置,在dev上git rebase master,将dev的多次commit一起拉到要master最新提交的后面(时间最新),变成一条线,多用于整理自己的dev提交历史,然后把master最新代码合进来。

  • 使用rebase还是merge更多的是管理风格的问题,有个较好实践:

    • 就是dev在merge进主分支(如master)之前,最好将自己的dev分支给rebase到最新的主分支(如master)上,然后用pull request创建普通merge请求。

    • 用rebase整理成重写commit历史,所有修改拉到master的最新修改前面,保证dev运行在当前最新的主branch的代码。避免了git历史提交里无意义的交织。



  • 假设场景:从 dev 拉出分支 feature-a。

    • 那么当 dev 要合并 feature-a 的内容时,使用 git merge feature-a

    • 反过来当 feature-a 要更新 dev 的内容时,使用 git rebase dev



  • git merge和git rebase 两者对比图

    • git merge图示 image.png

    • git rebase图示 image.png




取消合并


# 取消merge合并
git merge --abort
# 取消rebase合并
git rebase --abort

代码回退


代码回退的几种方式



  • git checkout

  • git reset

    • --hard:硬重置,影响【工作区、暂存区、本地仓库】

    • --mixed:默认,影响【暂存区、本地仓库】,被重置的修改内容还留在工作区

    • --soft:软重置,影响 【本地仓库】,被重置的修改内容还留在工作区和暂存区



  • git revert


# 撤回工作区该文件的修改,多个文件用空格隔开
git checkout -- <file-name>
# 撤回工作区所有改动
git checkout .

# 撤回已经commit到暂存区的文件
git reset <file-name>
# 撤回已经commit到暂存区的所有文件
git reset .
# 丢弃已commit的其他版本,hard参数表示同时重置工作区的修改
git reset --hard <commit-id>
# 回到上一个commit的版本,hard参数表示同时重置工作区的修改
git reset --hard HEAD^

# 撤销0ffaacc这次提交
git revert 0ffaacc
# 撤销最近一次提交
git revert HEAD
# 撤销最近2次提交,注意:数字从0开始
git revert HEAD~1

# 回退后要执行强制推送远程分支
git push -f

git reset和git revert的区别



  • reset是根据来移动HEAD指针,在该次提交点后面的提交记录会丢失。


hlh0kowt3hov1xhcku38.gif



  • revert会产生新的提交,来抵消选中的该次提交的修改内容,可以理解为“反做”,不会丢失中间的提交记录。


3kkd2ahn41zixs12xgpf.gif



  • 使用建议

    • 公共分支回退使用git revert,避免丢掉其他同事的提交。

    • 自己分支回退可使用git reset,也可以使用git revert,按需使用。




挑拣代码


git cherry-pick



  • “挑拣”提交,单独抽取某个分支的一个提交点,将这个提交点的所有修改内容,搬运到你的当前分支。

  • 如果我们只想将其他分支的某个提交点合并进来,不想用git merge将所有提交点合并进来,就需要使用这个git cherry-pick


git cherry-pick <commit-id>

2dkjx4yeaal10xyvj29v.gif


暂存代码


git stash



  • 当我们想要切换去其他分支修复bug,此时当前的功能代码还没修改完整,不想commit,就需要暂存当前修改的文件,然后切换到hotfix分支修复bug,修复完成再切换回来,将暂存的修改提取出来,继续功能开发。

  • 还有另一种场景就是,同事在远程分支上推送了代码,此时拉下来有冲突,可以将我们自己的修改stash暂存起来,然后先拉最新的提交代码,再pop出来,这样可以避免一个冲突的提交点。


# 将本地改动的暂存起来
git stash
# 将未跟踪的文件暂存(另一种方式是先将新增的文件添加到暂存区,使其被git跟踪,就可以直接git stash)
git stash -u
# 添加本次暂存的备注,方便查找。
git stash save "message"
# 应用暂存的更改
git stash apply
# 删除暂存
git stash drop
# 应用暂存的更改,然后删除该暂存,等价于git stash apply + git stash drop
git stash pop
# 删除所有缓存
git stash clear

打印日志



  1. git log


可以显示所有提交过的版本信息,如果感觉太繁琐,可以加上参数  --pretty=oneline,只会显示版本号和提交时的备注信息。



  1. git reflog


git reflog 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作),例如执行 git reset --hard HEAD~1,退回到上一个版本,用git log是看不出来被删除的,用git reflog则可以看到被删除的,我们就可以买后悔药,恢复到被删除的那个版本。


Git的下载、配置、工具推荐



  • Git下载地址


  • 两种拉取代码的方式

    • https:每次都要手动输入用户名和密码

    • ssh :自动使用本地私钥+远程的公钥验证是否为一对秘钥



  • 配置ssh

    • ssh-keygen -t rsa -C "邮箱地址"

    • cd ~/.ssh切换到home下面的ssh目录、cat id_rsa.pub命令查看公钥的内容,然后复制

    • github的settings -> SSH and GPG keys-> 复制刚才的内容贴入 -> Add SSH key

    • 全局配置一下Git用户名和邮箱

      • git config --global user.name "xxx"

      • git config --global user.email "xxx@xx.com"

      • image.png





  • Git 相关工具推荐

    • 图形化工具 SourceTree :可视化执行git命令,解放双手

    • VSCode插件 GitLens:可以在每行代码查看对应git的提交信息,而且提供每个提交点的差异对比




结尾


阅读到这里,是不是感觉对Git相关概念更加清晰了呢,那么恭喜你,再也不怕因为误操作,丢失同事辛辛苦苦写的代码了,而且将在日常工作的协同中游刃有余。



  • 💖建议收藏文章,工作中有需要的时候翻出来看一看~

  • 📃创作不易,如果我的文章对你有帮助,辛苦大佬们点个赞👍🏻,支持我一下~

  • 📌如果有错漏,欢迎大佬们指正~

  • 👏欢迎转载分享,请注明出处,谢谢~

链接:https://juejin.cn/post/6986868722136776718

收起阅读 »

为了让她10分钟入门canvas,我熬夜写了3个小项目和这篇文章

1. canvas实现时钟转动 实现以下效果,分为几步: 1、找到canvas的中心,画出表心,以及表框 2、获取当前时间,并根据时间画出时针,分针,秒针,还有刻度 3、使用定时器,每过一秒获取新的时间,并重新绘图,达到时钟转动的效果 1.1 表心,表框...
继续阅读 »

image.png


1. canvas实现时钟转动


实现以下效果,分为几步:



  • 1、找到canvas的中心,画出表心,以及表框

  • 2、获取当前时间,并根据时间画出时针,分针,秒针,还有刻度

  • 3、使用定时器,每过一秒获取新的时间,并重新绘图,达到时钟转动的效果


截屏2021-07-19 下午8.52.15.png


1.1 表心,表框


画表心,表框有两个知识点:



  • 1、找到canvas的中心位置

  • 2、绘制圆形


//html

<canvas id="canvas" width="600" height="600"></canvas>

// js

// 设置中心点,此时300,300变成了坐标的0,0
ctx.translate(300, 300)
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
// 执行画线段的操作stroke
ctx.stroke()

让我们来看看效果,发现了,好像不对啊,我们是想画两个独立的圆线,怎么画出来的两个圆连到一起了


截屏2021-07-19 下午9.10.07.png
原因是:上面代码画连个圆时,是连着画的,所以画完大圆后,线还没斩断,就接着画小圆,那肯定会大圆小圆连一起,解决办法就是:beginPath,closePath


ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0

// 画大圆
+ ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
+ ctx.closePath()

// 画小圆
+ ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
+ ctx.closePath()

1.2 时针,分针,秒针


画这三个指针,有两个知识点:



  • 1、根据当前时,分,秒计算角度

  • 2、在计算好的角度上去画出时针,分针,秒针


如何根据算好的角度去画线呢,比如算出当前是3点,那么时针就应该以12点为起始点,顺时针旋转2 * Math.PI / 12 * 3 = 90°,分针和秒针也是同样的道理,只不过跟时针不同的是比例问题而已,因为时在表上有12份,而分针和秒针都是60份


截屏2021-07-19 下午10.07.19.png


这时候又有一个新问题,还是以上面的例子为例,我算出了90°,那我们怎么画出时针呢?我们可以使用moveTo和lineTo去画线段。至于90°,我们只需要将x轴顺时针旋转90°,然后再画出这条线段,那就得到了指定角度的指针了。但是上面说了,是要以12点为起始点,我们的默认x轴确是水平的,所以我们时分秒针算出角度后,每次都要减去90°。可能这有点绕,我们通过下面的图演示一下,还是以上面3点的例子:


截屏2021-07-19 下午10.30.23.png


截屏2021-07-19 下午10.31.02.png
这样就得出了3点指针的画线角度了。


又又又有新问题了,比如现在我画完了时针,然后我想画分针,x轴已经在我画时针的时候偏转了,这时候肯定要让x轴恢复到原来的模样,我们才能继续画分针,否则画出来的分针是不准的。这时候save和restore就派上用场了,save是把ctx当前的状态打包压入栈中,restore是取出栈顶的状态并赋值给ctxsave可多次,但是restore取状态的次数必须等于save次数


截屏2021-07-19 下午10.42.06.png


懂得了上面所说,剩下画刻度了,起始刻度的道理跟时分秒针道理一样,只不过刻度是死的,不需要计算,只需要规则画出60个小刻度,和12个大刻度就行


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
// 把状态保存起来
+ ctx.save()

// 画大圆
ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
ctx.closePath()

// 画小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()

----- 新加代码 ------

// 获取当前 时,分,秒
let time = new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 时针
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo设置画线起点
ctx.moveTo(-10, 0)
// lineTo设置画线经过点
ctx.lineTo(40, 0)
// 设置线宽
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
// 恢复成上一次save的状态
ctx.restore()
// 恢复完再保存一次
ctx.save()

// 分针
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.strokeStyle = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

//秒针
ctx.rotate(2 * Math.PI / 60 * sec - - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
ctx.stroke()
ctx.closePath()
}

ctx.restore()

截屏2021-07-19 下午10.53.53.png


最后一步就是更新视图,使时钟转动起来,第一想到的肯定是定时器setInterval,但是注意一个问题:每次更新视图的时候都要把上一次的画布清除,再开始画新的视图,不然就会出现千手观音的景象


截屏2021-07-19 下午10.57.05.png


附上最终代码:


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

setInterval(() => {
ctx.save()
ctx.clearRect(0, 0, 600, 600)
ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
ctx.save()

// 画大圆
ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
ctx.closePath()

// 画小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()

// 获取当前 时,分,秒
let time = new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 时针
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo设置画线起点
ctx.moveTo(-10, 0)
// lineTo设置画线经过点
ctx.lineTo(40, 0)
// 设置线宽
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 分针
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.strokeStyle = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

//秒针
ctx.rotate(2 * Math.PI / 60 * sec - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}

ctx.restore()
ctx.restore()
}, 1000)

效果 very good啊:


clock的副本.gif


2. canvas实现刮刮卡


小时候很多人都买过充值卡把,懂的都懂啊哈,用指甲刮开这层灰皮,就能看底下的答案了。
截屏2021-07-19 下午11.02.09.png


思路是这样的:



  • 1、底下答案是一个div,顶部灰皮是一个canvascanvas一开始盖住div

  • 2、鼠标事件,点击时并移动时,鼠标经过的路径都画圆形开路,并且设置globalCompositeOperationdestination-out,使鼠标经过的路径都变成透明,一透明,自然就显示出下方的答案信息。


关于fill这个方法,其实是对标stroke的,fill是把图形填充,stroke只是画出边框线


// html
<canvas id="canvas" width="400" height="100"></canvas>
<div class="text">恭喜您获得100w</div>
<style>
* {
margin: 0;
padding: 0;
}
.text {
position: absolute;
left: 130px;
top: 35px;
z-index: -1;
}
</style>


// js
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// 填充的颜色
ctx.fillStyle = 'darkgray'
// 填充矩形 fillRect(起始X,起始Y,终点X,终点Y)
ctx.fillRect(0, 0, 400, 100)
ctx.fillStyle = '#fff'
// 绘制填充文字
ctx.fillText('刮刮卡', 180, 50)

let isDraw = false
canvas.onmousedown = function () {
isDraw = true
}
canvas.onmousemove = function (e) {
if (!isDraw) return
// 计算鼠标在canvas里的位置
const x = e.pageX - canvas.offsetLeft
const y = e.pageY - canvas.offsetTop
// 设置globalCompositeOperation
ctx.globalCompositeOperation = 'destination-out'
// 画圆
ctx.arc(x, y, 10, 0, 2 * Math.PI)
// 填充圆形
ctx.fill()
}
canvas.onmouseup = function () {
isDraw = false
}

效果如下:


guaguaka.gif


3. canvas实现画板和保存


框架:使用vue + elementUI


其实很简单,难点有以下几点:



  • 1、鼠标拖拽画正方形和圆形

  • 2、画完一个保存画布,下次再画的时候叠加

  • 3、保存图片


第一点,只需要计算出鼠标点击的点坐标,以及鼠标的当前坐标,就可以计算了,矩形长宽计算:x - beginX, y - beginY,圆形则要利用勾股定理:Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))


第二点,则要利用canvas的getImageDataputImageData方法


第三点,思路是将canvas生成图片链接,并赋值给具有下载功能的a标签,并主动点击a标签进行图片下载


看看效果吧:


截屏2021-07-19 下午11.16.24.png


截屏2021-07-19 下午11.17.41.png


具体代码我就不过多讲解了,说难也不难,只要前面两个项目理解了,这个项目很容易就懂了:


<template>
<div>
<div style="margin-bottom: 10px; display: flex; align-items: center">
<el-button @click="changeType('huabi')" type="primary">画笔</el-button>
<el-button @click="changeType('rect')" type="success">正方形</el-button>
<el-button
@click="changeType('arc')"
type="warning"
style="margin-right: 10px"
>圆形</el-button
>
<div>颜色:</div>
<el-color-picker v-model="color"></el-color-picker>
<el-button @click="clear">清空</el-button>
<el-button @click="saveImg">保存</el-button>
</div>
<canvas
id="canvas"
width="800"
height="400"
@mousedown="canvasDown"
@mousemove="canvasMove"
@mouseout="canvasUp"
@mouseup="canvasUp"
>
</canvas>
</div>
</template>

<script>
export default {
data() {
return {
type: "huabi",
isDraw: false,
canvasDom: null,
ctx: null,
beginX: 0,
beginY: 0,
color: "#000",
imageData: null,
};
},
mounted() {
this.canvasDom = document.getElementById("canvas");
this.ctx = this.canvasDom.getContext("2d");
},
methods: {
changeType(type) {
this.type = type;
},
canvasDown(e) {
this.isDraw = true;
const canvas = this.canvasDom;
this.beginX = e.pageX - canvas.offsetLeft;
this.beginY = e.pageY - canvas.offsetTop;
},
canvasMove(e) {
if (!this.isDraw) return;
const canvas = this.canvasDom;
const ctx = this.ctx;
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
this[`${this.type}Fn`](ctx, x, y);
},
canvasUp() {
this.imageData = this.ctx.getImageData(0, 0, 800, 400);
this.isDraw = false;
},
huabiFn(ctx, x, y) {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
},
rectFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.rect(beginX, beginY, x - beginX, y - beginY);
ctx.stroke();
ctx.closePath();
},
arcFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
this.isDraw && ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.arc(
beginX,
beginY,
Math.round(
Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
),
0,
2 * Math.PI
);
ctx.stroke();
ctx.closePath();
},
saveImg() {
const url = this.canvasDom.toDataURL();
const a = document.createElement("a");
a.download = "sunshine";
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
clear() {
this.imageData = null
this.ctx.clearRect(0, 0, 800, 400)
}
},
};
</script>

<style lang="scss" scoped>
#canvas {
border: 1px solid black;
}
</style>

结语


链接:https://juejin.cn/post/6986785259966857247

收起阅读 »

一个酷炫的 android 粒子动画库

一、灵感做这个粒子动画库的灵感来自于 MIUI 卸载应用时的动画:这个爆炸的粒子效果看起来很酷炫,而且粒子颜色是从 icon 中拿到的。最开始我简单实现了类似爆炸的效果,后来想到可以直接扩展一下,写一个通用的粒子动画库。二、使用项目地址:github.com/...
继续阅读 »


一、灵感

做这个粒子动画库的灵感来自于 MIUI 卸载应用时的动画:

这个爆炸的粒子效果看起来很酷炫,而且粒子颜色是从 icon 中拿到的。

最开始我简单实现了类似爆炸的效果,后来想到可以直接扩展一下,写一个通用的粒子动画库。

二、使用

项目地址:github.com/ultimateHan…

Particle 是一个使用 kotlin 编写的粒子动画库,可以用几行代码轻松搞定一个粒子动画。同时也支持高度自定义的粒子动画轨迹,可以打造出非常炫酷的自定义动画。这个项目发布了 0.1 版本在 JitPack 上,按如下操作引入:

在根目录的 build.gradle 中的 allprojects 中添加(注意不是 buildScript):

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

然后在你的项目中引入依赖即可。

implementation 'com.github.ultimateHandsomeBoy666:Particle:0.1'

在引入了 Particle 之后,只需要下面几行简单的代码,就可以实现上面的粒子爆炸效果:

Particles.with(context, container) // container 是粒子动画的宿主父 ViewGroup
.colorFromView(button)// 从 button 中采样颜色
.particleNum(200)// 一共 200 个粒子
.anchor(button)// 把 button 作为动画的锚点
.shape(Shape.CIRCLE)// 粒子形状是圆形
.radius(2, 6)// 粒子随机半径 2~6
.anim(ParticleAnimation.EXPLOSION)// 使用爆炸动画
.start()

三、粒子形状

粒子的形状支持圆形、三角形、矩形、五角星以及矢量图形及位图,并且支持多种图形粒子混合

下面详细说明。

Shape.CIRCLE 和 Shape.HOLLOWCIRCLE

  • 圆形和空心圆

  • 使用 radius 定义圆的大小。空心圆使用 strokeWidth 定义粗细。

Shape.TRIANGLE 和 Shape.HOLLOWTRIANGLE

  • 实心三角形和空心三角形

  • 使用 width 和 height 定义三角形的大小。空心三角形使用 strokeWidth 定义粗细。

Shape.RECTANGLE 和 Shape.HOLLOWRECTANGLE

  • 实心矩形和空心矩形。

  • 使用 width 和 height 定义矩形的大小。空心矩形使用 strokeWidth 定义粗细。

Shape.PENTACLE 和 Shape.HOLLOWPENTACLE

  • 实心五角星和空心五角星

  • 使用 radius 定义五角星外接圆的大小。空心五角星使用 strokeWidth 定义粗细。

Shape.BITMAP

  • 支持位图。

  • 支持矢量图,只需要把矢量图 xml 的资源 id 传入即可。

  • 图片粒子不受 color 设置的影响。

除了上述单种图形以外,还支持多种图形的混合粒子,如下:

四、粒子动画

动画控制

粒子的动画使用 ValueAnimator 来控制,可以自行定义 animator 来控制动画的行为,包括动画时长、Interpolater、重复、开始结束的监听等等。

粒子特效

目前仅支持粒子在运动过程中的旋转,如下。后续会增加更多效果

粒子轨迹

粒子轨迹的控制使用 IPathGenerator 接口的派生类来完成。库中自带四种轨迹动画,分别是:

  • ParticleAnimation.EXPLOSION 爆炸💥效果
  • ParticleAnimation.RISE 粒子上升
  • ParticleAnimation.FALL 粒子下降
  • ParticleAnimation.FIREWORK 烟花🎇效果

如果想要自定义粒子运动轨迹的话,可以继承 IPathGenerator 接口,复写生成粒子坐标的方法:

private fun createPathGenerator(): IPathGenerator {
// LinearPathGenerator 库中自带
return object : LinearPathGenerator() {
val cos = Random.nextDouble(-1.0, 1.0)
val sin = Random.nextDouble(-1.0, 1.0)

override fun getCurrentCoord(progress: Float, duration: Long): Pair<Int, Int> {
// 在这里写你想要的粒子轨迹
val originalX = distance * progress
val originalY = 100 * sin(originalX / 50)
val x = originalX * cos - originalY * sin
val y = originalX * sin + originalY * cos
return Pair((0.01 * x * originalY).toInt(), (0.008 * y * originalX).toInt())
}
}
}

然后把这个返回 IPathGenerator 的方法通过高阶函数的形式传入即可:

particleManager!!.colorFromView(button)
.particleNum(300)
.anchor(it)
.shape(Shape.CIRCLE, Shape.BITMAP)
.radius(8, 12)
.strokeWidth(10f)
.size(20, 20)
.rotation(Rotation(600))
.bitmap(R.drawable.ic_thumbs_up)
.anim(ParticleAnimation.with({
// 控制动画的animator
createAnimator()
}, {
// 粒子运动的轨迹
createPathGenerator()
})).start()

上述代码中的 ParticleAnimation.with 方法接受两个高阶函数分别生成动画控制和粒子轨迹。

fun with(animator: () -> ValueAnimator = DEFAULT_ANIMATOR_LAMBDA,
generator: () -> IPathGenerator)
: ParticleAnimation {
return ParticleAnimation(generator, animator)
}

终于,经过上面的折腾,可以得到下面的酷炫动画:

当然,只要你想要,可以构造出无限多的粒子动画轨迹,不过这可能要求一点数学功底🐶。

在 github.com/ultimateHan… 目录下有一份我之前试验的比较酷炫的轨迹公式合集,可以参考。

五、注意事项

  • 粒子动画比较消耗内存和 CPU,所以粒子数目太多,比如超过 1000 的话,可能会有卡顿。
  • 默认在动画结束的时候,粒子是不会消失的。如果要让粒子在动画结束时消失,可以自定义 ValueAnimator 监听动画结束,在结束时调用 ParticleManager.hide() 方法来隐藏粒子。
  • 如果需要反复触发粒子动画,比如按一次按钮触发一次,可以使用一个全局的 particleManager 变量来启动和取消粒子动画,可以避免内存消耗和内存抖动。比如:
particleManager = Particles.with(this, container)
button.setOnClickListener {

particleManager!!.colorFromView(button)
.particleNum(300)
.anchor(it)
.shape(Shape.CIRCLE, Shape.BITMAP)
.radius(8, 12)
.rotation(Rotation(600))
.anim(ParticleAnimation.EXPLOSION)

particleManager!!.start()
}

代码下载:ChipsLayoutManager-master.zip

收起阅读 »

Android 可扩展视图设计

前言问题飞书团队在去年对Chat页面进行了布局优化,在优化的时候发现了一个现象:很多布局(特别是RootView)往往会被附加非常多的功能(输入法监控、渲染耗时统计 、侧边栏滑出抽屉等),而且这些功能在很多场景下都会被用到。当时面临一个问题:如何优雅地扩展一个...
继续阅读 »

前言

问题

飞书团队在去年对Chat页面进行了布局优化,在优化的时候发现了一个现象:很多布局(特别是RootView)往往会被附加非常多的功能(输入法监控、渲染耗时统计 、侧边栏滑出抽屉等),而且这些功能在很多场景下都会被用到。

当时面临一个问题:如何优雅地扩展一个View的功能?

常用方案

对于View的功能扩展,一般有三条路可走:

  1. 一个自定义View的无限膨胀
  2. 多层自定义View
  3. 多重继承自定义View

但是,这三个方案都有问题:

  1. 一个自定义View,会完全没有可复用性,可维护性差
  2. 多层自定义View,会有过度绘制问题(增加了视图层级)
  3. 多重继承自定义View,会有耦合性问题,因为如果有N个功能自由组合,使用继承的方式来实现,最终自定义View的个数会是:C(N,1)+C(N,2)+...+C(N,N)

一个想法

我们知道,在软件设计中有一对非常重要的概念:is-a 和 has-a  简单理解,is-a表示继承关系,has-a是组合关系,而has-a要比is-a拥有更好的可扩展性。

那么在扩展视图功能的时候,是不是也可以用has-a(组合)代替常用的is-a(继承)?

答案是可以的,而且我们可以使用委托模式来实现它,委托模式天然适合这个工作:设计的出发点就是为has-a替代is-a提供解决方案的, 而Kotlin在语言层面对委托模式提供了非常优雅的支持,在这种场景下可以使用它的by接口委托 

探索

概念定义

  • Widget: 系统View / ViewGroup、自定义View / ViewGroup。
  • WidgetPlus: 委托者。继承自Widget,并可通过register()的方式has some items。
  • DelegateItem: 被委托者。接受来自WidgetPlus的委托,负责业务逻辑的具体实现。
  • IDelegate: 被委托者接口。

不支持在 Docs 外粘贴 block

流程设计

无法复制加载中的内容

角色转换

在被委托接口IDelagate的“润滑”下,Widget、WidgetPlus和Item相互之间是可以做到无缝转换的

  • Widget -> WidgetPlus

    • 简单描述:一个视图可以改造为功能可扩展的视图(可双向
    • 转换方法:实现IDelegate接口、支持item注册
  • Widget -> DelegateItem

    • 简单描述:自定义视图可以被改造为一个功能项,供其它可扩展视图动态配置(可双向
    • 转换方法:自定义Widget移除对Widget的继承,实现IDelegate接口
  • WidgetPlus -> DelegateItem

    • 简单描述:一个可扩展视图(本身带有一部分功能),可被改造为功能项(可双向
    • 转换方法:移除对Widget的继承,保留IDelegate接口的实现

无法复制加载中的内容

通信和调用

  • 可扩展视图和扩展项应该支持双向通信:

    • WidgetPlus -> DelegateItem

      • 这个比较简单,WidgetPlus会用组合的方式持有Item,在收到业务或系统的请求时,委托Item去执行具体的实现逻辑。
    • DelegateItem -> WidgetPlus

      • 在Item初始化的时候,需要传入WidgetPlus的相关信息(widgetPlus、context、attrs、defStyleAttr、defStyleRes)
  • WidgetPlus跟Items拥有相同的API,需要设置调用原则:

    • 所有公共方法,一律使用WidgetPlus对象来触发(无论是在外部代码还是Item内部)
    • Item私有方法,使用Item对象来触发

竞争机制

一个WidgetPlus同时持有多个Item的时候,如果这些Item被委托实现了相同的方法,那么就会出现Item的内部竞争问题。这里,可以根据方法类别来分别处理:

  1. 无返回值方法

    1. 比如onMeasure(),按照Item注册列表顺序执行
  2. 有返回值方法

  • 比如onTouchEvent():Boolean,这里出现了功能冲突,因为不可能同时返回多个值,只能取第一个返回值作为WidgetPlus的返回值。
  • 对于这种情形,可以打印日志以便Develop时就被发现,解决方法有两种:
  1. 合而为一,即把两个Item合并,在一个Item中处理冲突;
  2. 分而治之,即把其中一个Item转换为WidgetPlus,创建两级视图。

关键点

1:1

  • 一个WidgetPlus可以无限扩展Item功能项,但是对一种Item功能项只能持有一个对象。
  • 但是,由于外部调用具有不可控性,所以register()的入参应该是Item的Class对象,在WidgetPlus内部反射调用Item的构造来生成对象。

Center

WidgetPlus中还是有一部分代码量的,为了减少Widget的转换成本、增加后续的可维护性,可以在WidgetPlus和Item直接再加一层DelegateCenter,由它来统一管理。

无法复制加载中的内容

Super

  • 问题:在重写Widget的系统方法时,是需要执行superMethod的,而Item在进行业务实现时,无法直接触发到这个superMethod的。
  • 有两个解决方案:
  1. 把Widget的method拆分为methodBefore()、methodAfter()、isHasSuper(),分别委托Item实现
  2. 把superMethod作为委托参数,这里可以使用Kotlin的方法类型参数

很显然,第二种方案要更好。

示意代码

 /**

* Widget

*/

package android.widget;

public class LinearLayout extends ViewGroup {

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

}



/**

* WidgetPlus

*/

class LinearLayoutPlus() : LinearLayout(), IDelegate by DelegateCenter() {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

onDelegateMeasure(widthMeasureSpec, heightMeasureSpec) { _, _ ->

super.onMeasure(widthMeasureSpec, heightMeasureSpec)}

}

}



/**

* Center

*/

class DelegateCenter() : IDelegate {



private val itemList = mutableListOf<IItem>()



fun register(item: Class<IDelegate>) {

plusList.add(item.newInstance())

}



fun unRegister(item: Class<IDelegate>) {

plusList.remove(item)

}



override fun onDelegateMeasure(

widthMeasureSpec: Int,

heightMeasureSpec: Int,

superMethod: (Int, Int) -> Unit) {

for (item in itemList) {

item.onDelegateMeasure(widthMeasureSpec, heightMeasureSpec,superMethod)

}

}

}



/**

* delegate interface

*/

interface IDelegate : IItem {



fun register(item: Class<IDelegate>)



fun unRegister(item: Class<IDelegate>)

}



/**

* Item interface

*/

interface IItem{

fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int,

superMethod: (Int, Int) -> Unit)

}



/**

* Item1

*/

class Item1() : IItem() {

override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: I nt, superMethod: (Int, Int) -> Unit) {}

}



/**

* Item2

*/

class Item2() : IItem() {

override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int, superMethod: (Int, Int) -> Unit) {}

}



/**

* main

*/

fun main() {

val plus = LinearLayoutPlus(context, attrs)

plus.register(Item1::class.java)

plus.register(Item2::class.java)

}
复制代码

背景知识

类与类之间的关系

  • 类与类之间有六种关系:
关系描述耦合度语义代码层面
继承继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己新功能的能力☆☆☆☆☆☆is-a在Java中继承关系通过关键字extends明确标识
实现实现指的是一个类实现接口(可以是多个)的功能☆☆☆☆☆is-a在Java中实现关系通过关键字implements明确标识
组合它体现整体与部分间的关系,而且具有不可分割性,生命周期是一致的☆☆☆☆contains-a类B作为类A的成员变量,只能从语义上来区别聚合和关联
聚合它体现整体与部分间的关系,它们是可分离的,各有自己的生命周期☆☆☆has-a类B作为类A的成员变量,只能从语义上来区别组合和关联
关联这种使用关系具有长期性,而且双方的关系一般是平等的☆☆has-a类B作为类A的成员变量,只能从语义上来区别组合和聚合
依赖这种使用关系具有临时性,非常的脆弱use-a类B作为入参,在类A的某个方法中被使用
  • 继承和实现体现的一种纵向关系,一般是明确无异议的。而组合、聚合、关联和依赖体现的是横向关系,它们之间就比较难区分了,这几种关系都是语义级别的,从代码层面并不能完全区分。

委托模式

  • 定义:有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。
  • 能力: 是一种基础模式,状态模式、策略模式、访问者模式等在本质上就是在特殊场合采用了委托模式,委托模式使得我们可以用组合、聚合、关联来替代继承。
  • 委托模式不能等价于代理模式: 虽然它们都是把业务需要实现的逻辑交给一个目标实现类来完成,但是使用代理模式的目的在于提供一种代理以控制对这个对象的访问,但是委托模式的出发点是将某个对象的请求拜托给另一个对象。
  • 委托模式是可以自由切换被委托者,委托者甚至可以自实现业务逻辑,例如Java ClassLoader的双亲委派模型中,在委托父加载器加载失败的情况下,可以切换为自己去加载。

收起阅读 »

深入解析dio(一) Socket 编程实现本地多端群聊

深入解析dio(一) Socket 编程实现本地多端群聊引言无论你是否用过, wendux 大佬开源的 dio 项目,应该是目前 Flutter 中最 🔥 的网络请求库,在 github 上接近 1W 的 star。但其...
继续阅读 »

深入解析dio(一) Socket 编程实现本地多端群聊

引言

无论你是否用过, wendux 大佬开源的 dio 项目,应该是目前 Flutter 中最 🔥 的网络请求库,在 github 上接近 1W 的 star。

但其实 Dart 中已经有 dart:io 库为我们提供了网络服务,为何 Dio 又如此受到开发者青睐?背后有哪些优秀的设计值得我们学习?

这个系列预计会花 6 期左右从计算机网络原理,到 Dart 中的网络编程,最后再到 Dio 的架构设计,通过原理分析 + 练习的方式,带大家由浅入深的掌握 Dart 中的网络编程与 Dio 库的设计。

本期,我们会通过编写一个简单的本地群聊服务一起学习计算机网络基础知识与 Dart 中的 Socket 编程


Socket 是什么

想要了解 Socket 是什么,需要先复习一下网络基础。

无论微信聊天,观看视频或者打开网页,当我们通过网络进行一次数据传输时。数据根据网络协议进行传输, 在 TCP/IP 协议中,经历如下的流转:

image.png

TCP/IP 定义了四层结构,每一层都是为了完成一种功能,为了完成这些功能,需要遵循一些规则,这些规则就是协议,每一层都定义了一些协议。

  • 应用层

应用层决定了向用户提供应用服务时通信的活动。TCP/IP 协议族内预存了各类通用的应用服务。比如,FTP(FileTransfer Protocol,文件传输协议)和 DNS(Domain Name System,域名系统)服务就是其中两类。HTTP 协议也处于该层。

  • 传输层

传输层对上层应用层,提供处于网络连接中的两台计算机之间端到端的数据传输。在传输层有两个性质不同的协议:TCP(Transmission ControlProtocol,传输控制协议)和UDP(User Data Protocol,用户数据报协议)。

  • 网络层(又名网络互连层)

网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所起的作用就是在众多的选项内选择一条传输路线。

  • 网络访问层(又名链路层)

用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。

今天的主角 Socket 是应用层 与 TCP/IP 协议族通信的中间软件抽象层,表现为一个封装了 TCP / IP协议族 的编程接口(API)

image.png

为什么我们一开始要了解 Socket 编程,因为比起直接使用封装好的网络接口,Socket 能让我们更接近接近网络的本质,同时不用关心底层链路的细节。


如何使用 Dart 中的 Socket

dart:io 库中提供了两个类,第一个是 Socket,我们可以用它作为客户端与服务器建立连接。 第二个是 ServerSocket,我们将使用它创建一个服务器,并与客户端进行连接。

1、Socket 客户端

本系列代码均上传,可直接运行:io_practice/socket_study

Socket 类中有一个静态方法 connect(host, int port) 。第一个参数 host 可以是一个域名或者 IP 的 String,也可以是 InternetAddress 对象。

connect 返回一个 Future<Socket> 对象,当 socket 与 host 完成连接时 Future 对象回调。

// socket_pratice1.dart
void main() {
Socket.connect("www.baidu.com", 80).then((socket) {
print('Connected to: '
'${socket.remoteAddress.address}:${socket.remotePort}');
socket.destroy();
});
}
复制代码

这个 case 中,我们通过 80 端口(为 HTTP 协议开放)与 http://www.baidu.com 连接。连接到服务器之后,打印出连接的 IP 地址和端口,最后通过 socket.destroy() 关闭连接。在命令行中 执行 dart socket_pratice1.dart 可以看到如下输出:

➜  socket_study dart socket_pratice1.dart 
socket_pratice2.dart: Warning: Interpreting this as package URI, 'package:io_pratice/socket_study/socket_pratice2.dart'.
Connected to: 220.181.38.149:80
复制代码

通过简单的函数调用,Dart 为我们完成了 http://www.baidu.com 的 IP 查找与 TCP 建立连接,我们只需要等待即可。 在连接建立之后,我们可以和服务端进行数据交互,为此我们需要做两件事。

1、发起请求 2、响应接受数据

对应 Socket 中提供的两个方法 Socket.write(String data) 和 Socket.listen(void onData(data)) 。

// socket_pratice2.dart
void main() {
String indexRequest = 'GET / HTTP/1.1\nConnection: close\n\n';

//与百度通过 80 端口连接
Socket.connect("www.baidu.com", 80).then((socket) {
print('Connected to: '
'${socket.remoteAddress.address}:${socket.remotePort}');

//监听 socket 的数据返回
socket.listen((data) {
print(new String.fromCharCodes(data).trim());
}, onDone: () {
print("Done");
socket.destroy();
});

//发送数据
socket.write(indexRequest);
});
}
复制代码

运行这段代码可以看到 HTTP/1.1 请求头,以及页面数据。这是学习 web 协议很好的一个工具,我们还可以看到设 cookie 等值。(一般不用这种方式连接 HTTP 服务器,Dart 中提供了 HttpClient 类,提供更多能力)

➜  socket_study dart socket_pratice2.dart 
socket_pratice2.dart: Warning: Interpreting this as package URI, 'package:io_pratice/socket_study/socket_pratice2.dart'.
Connected to: 220.181.38.150:80
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-Length: 14615
Content-Type: text/html
...
...
(headers and HTML code)
...
</script></body></html>
Done
复制代码

2、ServerSocket

使用 Socket 可以很容易的与服务器连接,同样我们可以使用 ServerSocket 对象创建一个可以处理客户端请求的服务器。 首先我们需要绑定到一个特定的端口并进行监听,使用 ServerSocket.bind(address,int port) 方法即可。这个方法会返回 Future<ServerSocket> 对象,在绑定成功后返回 ServerSocket 对象。之后 ServerSocket.listen(void onData(Socket event)) 方法注册回调,便可以得到客户端连接的 Socket 对象。注意,端口号需要大于 1024 (保留范围)。

// serversocket_pratice1.dart
void main() {
ServerSocket.bind(InternetAddress.anyIPv4, 4567)
.then((ServerSocket server) {
server.listen(handleClient);
});
}

void handleClient(Socket client) {
print('Connection from '
'${client.remoteAddress.address}:${client.remotePort}');
client.write("Hello from simple server!\n");
client.close();
}
复制代码

与客户端不同的是,在 ServerSocket.listen 中我们监听的不是二进制数据,而是客户端连接。 当客户端发起连接时,我们可以得到一个表示客户端连接的 Socket 对象。作为参数调用 handleClient(Socket client) 函数。通过这个 Socket 对象,我们可以获取到客户端的 IP 端口等信息,并且可以与其通信。运行这个程序后,我们需要一个客户端连接服务器。可以将上一个案例中 conect 的地址改为 127.0.0.0.1,端口改为 4567,或者使用 telnet 作为客户端发起。

运行服务端程序:

➜  socket_study dart serversocket_pratice1.dart 
serversocket_pratice1.dart: Warning: Interpreting this as package URI, 'package:io_pratice/socket_study/serversocket_pratice1.dart'.
Connection from 127.0.0.1:54555 // 客户端连接之后打印其 ip 与端口
复制代码

客户端使用 telnet 请求:

➜  io_pratice telnet localhost 4567
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello from simple server! // 来自服务端的消息
Connection closed by foreign host.
复制代码

即使客户端关闭连接,服务器程序仍然不会退出,继续等待下一个连接,Dart 已经为我们处理好了一切。

实战:本地群聊服务

1、聊天服务器

有了上面的实践,我们可以尝试编写一个简单的群聊服务。当某个客户端发送消息时,其他所有连接的客户端都可以收到这条消息,并且能优雅的处理错误和断开连接。

image.png

如图,我们的三个客户端与服务器保持连接,当其中一个发送消息时,由服务端将消息分发给其他连接者。 所以我们创建一个集合来存储每一个客户端连接对象

List<ChatClient> clients = [];
复制代码

每一个 ChatClient 表示一个连接,我们通过对 Socket 进行简单的封装,提供基本的消息监听,退出与异常处理:

class ChatClient {
Socket _socket;
String _address;
int _port;

ChatClient(Socket s){
_socket = s;
_address = _socket.remoteAddress.address;
_port = _socket.remotePort;

_socket.listen(messageHandler,
onError: errorHandler,
onDone: finishedHandler);
}

void messageHandler(List data){
String message = new String.fromCharCodes(data).trim();
// 接收到客户端的套接字之后进行消息分发
distributeMessage(this, '${_address}:${_port} Message: $message');
}

void errorHandler(error){
print('${_address}:${_port} Error: $error');
// 从保存过的 Client 中移除
removeClient(this);
_socket.close();
}

void finishedHandler() {
print('${_address}:${_port} Disconnected');
removeClient(this);
_socket.close();
}

void write(String message){
_socket.write(message);
}
}
复制代码

当服务端接受到某个客户端发送的消息时,需要转发给聊天室的其他客户端。

image.png

我们通过 messageHandler 中的 distributeMessage 进行消息分发:

...
void distributeMessage(ChatClient client, String message){
for (ChatClient c in clients) {
if (c != client){
c.write(message + "\n");
}
}
}
...
复制代码

最后我们只需要监听每一个客户端的连接,将其添加至 clients 集合中即可:

// chatroom.dart

ServerSocket server;

void main() {
ServerSocket.bind(InternetAddress.ANY_IP_V4, 4567)
.then((ServerSocket socket) {
server = socket;
server.listen((client) {
handleConnection(client);
});
});
}

void handleConnection(Socket client){
print('Connection from '
'${client.remoteAddress.address}:${client.remotePort}');

clients.add(new ChatClient(client));

client.write("Welcome to dart-chat! "
"There are ${clients.length - 1} other clients\n");
}
复制代码

直接运行程序

➜ dart chatroom.dart
复制代码

使用 telnet 测试服务器连接:

➜  socket_study telnet localhost 4567 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Welcome to dart-chat! There are 0 other clients
复制代码

2、聊天客户端

聊天客户端会简单很多,他只需要连接到服务器并接受消息;以及读取用户的输入信息并将其发送至客户端的方法。

前面我们已经实践过如何从服务器接收数据,所以我们只需实现发送消息即可。

通过 dart:io 中的 stdin 能帮助我们轻松的读取键盘输入:

// chatclient.dart

Socket socket;

void main() {
Socket.connect("localhost", 4567)
.then((Socket sock) {
socket = sock;
socket.listen(dataHandler,
onError: errorHandler,
onDone: doneHandler,
cancelOnError: false);
})
.catchError((AsyncError e) {
print("Unable to connect: $e");
exit(1);
});

// 监听键盘输入,将数据发送至服务端
stdin.listen((data) =>
socket.write(
new String.fromCharCodes(data).trim() + '\n'));
}

void dataHandler(data){
print(new String.fromCharCodes(data).trim());
}

void errorHandler(error, StackTrace trace){
print(error);
}

void doneHandler(){
socket.destroy();
exit(0);
}
复制代码

之后运行服务器,并通过多个命令行运行多个客户端程序。你可以在某个客户端中输入消息,之后在其他客户端接收到消息。

image.png

如果你有多个设备,也可以通过 Socket.connect(host, int port) 与服务器进行连接,当然这需要你提供每个设备的 IP 地址,这该如何做到?下一期我会通过 UDP 与组播协议进一步完善群聊服务。

收起阅读 »

【开源项目】集成环信IM开发的一款社交app---共享影院

项目介绍该项目旨在嵌入当今已经较为成熟的视频播放行业,让用户可以创建一个观影房,与远端的其他用户进行视频通讯并且同时观看同一视频。做到相隔万里,依然可以零距离互动,感受视频所带来的乐趣。项目结构share-cinema: 共享影院前端源码video-backe...
继续阅读 »

项目介绍

该项目旨在嵌入当今已经较为成熟的视频播放行业,让用户可以创建一个观影房,与远端的其他用户进行视频通讯并且同时观看同一视频。做到相隔万里,依然可以零距离互动,感受视频所带来的乐趣。

项目结构

  • share-cinema: 共享影院前端源码
  • video-backend: 共享影院后端源码
  • 演示视频.mp4: 共享影院功能演示视频


相关技术

  • Agora Video SDK :实现高清、稳定、流畅的及时通讯
  • 环信IM SDK :实现安全、简单的文字聊天
  • Socket.io :实现聊天室中同步播放、暂停以及拖动进度条
  • Celery + Flower : 实现后台的用户画像及模型更新
  • 图计算 :生成用户推荐模型


作品背景

《共享影院》项目的设计灵感来自当下非常流行的一种视频形式:Reaction Video(反应视频)。反应视频,顾名思义,就是记录下人们对事情做出反应的视频。在表现形式上,画面由两个部分组成,包括观看的视频资源,以及观看者本人的反应。这有点像观看体育比赛时电视台邀请的实况解说。
2013年,美剧《权利的游戏》第三季热播,大量的油管网友录制自己或朋友看剧时的激烈反应,引发了全球观众的集体共鸣,反应视频由此走入大众视野。目前,这类反应视频已经在多个视频平台成为了一类发展成熟且庞大的分支。Youtube上最火的Reaction类频道,目前已累积90亿次播放量,收获了1970万订阅。
Reaction Video之所以会如此成功,有以下两部分的因素:

  • 认同感:对于观众来说,他们希望在看视频的时候可以找到与自己有相同关注点的人,也期待着他人在看视频时会不会产生与自己相近的反应。
  • 分享感:对于反应视频的制作者来说,他们希望与大家分享自己在看视频时的喜怒哀乐。

这两种心理因素同样适应于如今非常流行的弹幕文化,但是其中仍然存在一些缺点。对于反应视频的制作者,或者发送弹幕的人来说,他们发表了自己的观点,收获了分享的满足感,却很难得到及时的反馈;对于观众而言,他们看到了弹幕或视频制作者的反应,从中找到了认同感,却难以与其分享自己的感受。
同时我们注意到,从以前“看完视频写评论”到“看视频时发弹幕”、“直播互动聊天”等新型视频形式,随着现在人们生活节奏的加快,人们越来越需要在相同的时间内获得更多的信息。
因此,我们提出《共享影院》这个项目,从“视频+音视频通讯+文字聊天”的形式上,将认同感与分享感合二为一,为看视频的用户提供及时地、双向地、新颖地视频娱乐体验。

创新性

  • 2020年,网易云音乐推出了《一起听音乐》,可以与好友一起同步听歌曲。
  • 2020年,BiliBili推出了《一起看》功能,提供了同步观影,语音消息等功能。
  • 2021年,抖音推出了《一起看》功能,提供和好友一起刷短视频的功能。
  • 我们的《共享影院》不但提供了与好友一起看视频的功能、实现了聊天室内的同步观影,而且与这些产品不同的是,首次结合了视频通讯,将Reaction Video的理念加入进来。不仅可以让已经观看过视频的人,将感兴趣的视频推荐给好友,再次观看进而与好友更加深入的交流;还可以在赛事直播以及新品发布时,与好友第一时间面对面见证历史时刻。此外,我们还引入了“陌生人匹配”,在孤独的时候为用户推荐最符合用户画像的“熟悉的陌生人”。
一起看时进行音视频通讯
一起看时进行文字聊天
观看精彩影视作品
观看赛事直播与发布会
陌生人匹配


潜在商业价值

反应类视频所带来的市场需求

  • 给用户带来认同感与分享感
  • 为用户提供一个私密的共享空间
  • 彼此之间的互动体验

文化推广,促进营销

该项目为用户之间提供了更多的讨论机会,这种“一起观看”的形式并不只停留在用户的首次观影,许多用户会为了与他人分享而将同一作品进行多次观看。同时,用户邀请和分享给好友视频资源,这对视频本身的内容营销有着颇多利好。可以快速带动视频内容的宣传,同时增加点播数,创造更多收益。

会员制度

部分电视剧、电影、综艺,需要开通会员才能观看。共同观影要求所有用户都满足权限才能进行观看。因此,在视频得到推广的同时,视频平台也将从中获益。

快速接入 《共享影院》的核心内容简单,可变性强

可以快速与已经成熟的视频平台对接,迅速投入商业化使用。视频资源不但可以是影视剧集,还可以是实况球赛、网络直播、新品发布会等各式各样的视频类型,上升空间大。
趣味性

借助音视频通讯来吸引用户观看视频。使用Agora Video SDK可以快速提升音视频通讯技术,提供美颜、变声、AR Face等多种玩法,增加视频生活的趣味性,提高用户黏性。


运行说明

前端

  • 《共享影院》项目前端由vue.cli 4.x搭建,启动前请按以下步骤执行
  • 安装依赖

npm install

  • 本地启动

npm run serve

运行后通过 https://localhost:8020/ 进行访问

后端

  • 《共享影院》项目后端由python3+ Flask框架搭建,运行前请按以下步骤操作
  • 请在requirements.txt所在目录下执行
  • 请在环信IM管理控制台手动创建一个名为superadmin的管理员用户,用于在后台创建聊天室
  • 请在环信IM管理控制台手动将用户注册方式修改为开放注册,用于实现用户注册

pip install -r requirements.txt

  • 添加声网RTC所需的相关配置


在app.py同一路径下创建config.py文件

文件中添加agora_token相关信息

# 声网SDK配置

appid = ""

appsecret = ""#

环信IM配置

url = "http://a1.easemob.com/"

orgname = ""

appname = ""

clientid = ""

clientsecret = ""

  • 启动后端


python app.py


操作指南

  • 前后端均正常运行之后,使用https://localhost:8020进入主页
  • 用户A选择主页中的任一视频,将自动创建观影房,观影房将有一个独立的房间号
  • 用户B从主页左上角的输入框输入用户A的房间号,将进入用户A的房间
  • 此时双方在观影房内的播放、暂停、拖动进度条均保持同步
  • 观影房中右侧的三个按钮分别是:禁止麦克风、退出房间、禁止摄像头

注意事项

  • 部分后台数据与模型储存在服务器上,为方便用户浏览项目中的全部功能,我们提供了本地运行版本,部分数据已经进行了模拟。
  • 视频资源因空间较大,我们只上传了部分视频资源,方便演示播放功能。
  • 项目中的图片以及视频资源均来源于网络。


收起阅读 »

Android资源管理及资源的编译和打包过程分析

前言在工作和学习中,我们除了要写一些业务代码,还要对项目的编译和打包流程有一定的认识,才能在遇到相关问题的时候能有所头绪。在这个过程中,我们往往会忽略掉资源文件是如何被添加进去的,Android的资源管理框架是一个很庞大和复杂的框架,资源编译打包的过程也很复杂...
继续阅读 »

前言

在工作和学习中,我们除了要写一些业务代码,还要对项目的编译和打包流程有一定的认识,才能在遇到相关问题的时候能有所头绪。在这个过程中,我们往往会忽略掉资源文件是如何被添加进去的,Android的资源管理框架是一个很庞大和复杂的框架,资源编译打包的过程也很复杂和繁琐,本文就来浅谈一下Android的资源文件是如何编译和打包的吧,除了当做一个自我总结,也希望能对看到本文的你有所帮助和启发。当然了文章比较长,希望你能耐心的看完。

编译打包流程

Android一个包中,除了代码以外,还有很多的资源文件,这些资源文件在apk打包的过程中,通过AAPT工具,打包到apk中。我们首先看一下apk的打包流程图,

image.png

概述一下这张图,打包主要有一下几个步骤:

  • 打包资源文件:通过aapt工具将res目录下的文件打包生成R.java文件和resources.arsc资源文件,比如AndroidManifest.xml和xml布局文件等。
  • 处理aidl files:如果有aidl接口,通过aidl工具打包成java接口类
  • java Compiler:javac编译,将R.java,源码文件,aidl.java编译为class文件
  • dex:源码.class,第三方jar包等class文件通过dx工具生成dex文件
  • apkbuilder:apkbuilder将所有的资源编译过的和不需要编译的,dex文件,arsc资源文件打包成一个完整的apk文件
  • jarsigner:以上生成的是一个没有签名的apk文件,这里通过jarsigner工具对该apk进行签名,从而得到一个带签名的apk文件
  • zipalign:对齐,将apk包中所有的资源文件距离文件起始偏移为4的整数倍,这样运行时可以减少内存的开销

资源分类

asset目录

存放原始资源文件,系统在编译时不会编译该目录下的资源文件,所以不能通过id的方式访问,如果要访问这些文件,需要指定文件名来访问。可以通过AssetManager访问原始文件,它允许你以简单的字节流的形式打开和读取和应用程序绑定在一起的原始资源文件。以下是一个从assets中读取本地的json文件的实例:

        StringBuilder sb = new StringBuilder();
AssetManager assets = getAssets();
try {
InputStream open = assets.open(“xxx.json”);
//使用一个转换流转换为字符流进行读取
InputStreamReader inputStreamReader = new InputStreamReader(open);
//缓冲字符流
BufferedReader reader = new BufferedReader(inputStreamReader);
String readLine;
while((readLine = reader.readLine())!=null){
sb.append(readLine);
}
String s = sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
复制代码

来看看一般项目中asset目录下会放些什么东东吧

image.png

res目录

存放可编译的资源文件(raw除外),编译时,系统会自动在R.java文件中生成资源文件的id,访问这种资源可以通过R.xxx.id即可。

目录资源类型
animator/用于定义属性动画的xml
anim/用于定义补间动画的xml(属性动画也可以在这里定义)
color/用于颜色状态列表的xml
drawable/位图文件(.9.png、.png、.jpg、.gif)
mipmap/适用于不同启动器图标密度的可绘制对象文件
layout/用于定义用户界面布局的 XML 文件
menu/用于定义应用菜单(如选项菜单、上下文菜单或子菜单)的 XML 文件
values/包含字符串、整型数和颜色等简单值的 XML 文件
XML/可在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(如可搜索配置)都必须保存在此处。
font/带有扩展名的字体文件(如 .ttf、.otf 或 .ttc),或包含 元素的 XML 文件
raw/需以原始形式保存的任意文件

编译资源文件的结果

好处

对资源进行编译有以下两点好处

  • 空间占用小:二进制xml文件占用的空间更小,因为所有的xml文件的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串池中。有了这个字符串池,原来使用字符串的地方就可以使用一个整数索引代替,从而可以减少文件的大小
  • 解析速度快:二进制的xml文件解析的速度更快,xml文件中不在包含字符串值,所以就省去了解析字符串的时间,从而提高了速度。

编译完成之后,除了assets资源之外,会给其他所有的资源生成一个id,根据这些id,打包工具会生成一个资源索引表resources.arsc以及R.java文件。资源索引表会记录所有资源的信息,根据资源id和设备信息,快速的匹配最合适的资源,R文件则记录各个资源的id常量。

生成资源索引表

首先来看一张图,这是resources.arsc的结构图 20160623160331859.png

整个resources.arsc是由一系列的chunk组成的,每一个chunk都有一个头,用来描述chunk的元数据。

  • header:每个chunk的头部用来描述该chunk的元信息,包括当前chunk的类型,头大小,块大小等
  • Global String Pool:全局字符串池,将所有字符串放到这个池子中,大家都复用这个池子中的数据,什么样的字符串会放到这个池子中呢?所有资源的文件的路径名,以及资源文件中所定义的资源的值,所以这个池子也可以叫做资源项的值字符串资源池,包含了所有在资源包里定义的资源项的值字符串,比如下面代码中ABC就存放在这里
  • package数据块:
    • package header:记录包的元数据,包名、大小、类型等
    • 资源类型字符串池:存储所有类型相关的字符串,如:attr、drawable、layout、anim等
    • 资源项名称字符串池:存储应用所有资源文件中资源项名称相关的字符串,比如下边的app_name就存放在这里。
    • Type Spec:类型规范数据块,用来描述资源项的配置差异性,通过这个差异性描述,我们就可以知道每一个资源项的配置状况。Android设备众多,为了使得应用程序支持不同的大小、密度、语言,Android将资源组织为18个维度,每一个资源类都对应一组配置列表,配置这个资源类的不同维度,最后再使用一套匹配算法来为应用程序在资源目录中选择最合适的资源。
    • config list:上边说到,每个type spec是一个类型的描述,每个类型会有多个维度,config list就是由多个ResTable_type结构来描述的,每一个ResTable_type描述的就是一个维度。
 <resources>    
    <string name="app_name">ABC</string>    
</resources>
复制代码

生成R文件和资源id

image.png

首先看一下R文件的结构图,每一种资源文件都对应一个静态内部类,对照前面所说的res文件目录结构,其中每个静态内部类中的一个静态常量分别定义一条资源标识符

image.png

或者这样:

    public static final class layout {
        public static final int main=0x7f030000;
    }
复制代码

public static final int main=0x7f030000;就表示layout目录下的main.xml文件。id中最高字节代表package的id,次高字节代表type的id,最后的字节代表当前类型中出现的序号。

  • package id:相当于一个命名空间,限定资源的来源,Android系统当前定义了两个资源命令空间,其中系统资源命令空间是0x01,另外一个应用程序资源命令空间为0x7f,所有位于 0x01到0x7f 之间的packageid都是合法的。
  • type id:指资源的类型id,如anim、color、layout、raw...等,每一种资源都对应一个type id
  • entry id:指每一个资源在其所属资源类型中出现的次序,不同资源类型的entry id是有可能相同的,但是由于他们的type id不同,所以一样可以进行区分。

资源文件只能以小写字母和下划线作为首字母,随后的名字中只能出现a-z或者0-9或者_.这些字符,否则会报错。

当我们在相应的res的资源目录中添加资源文件时,便会在相应的R文件中的静态内部类中自动生成一条静态的常量,对添加的文件进行索引。

在布局文件中当我们需要为组件添加id属性时,可以使用@+id/idname,+表示在R文件的名为id的内部类中添加一条记录。如果这个id不存在,则会首先生成它。

资源文件打包流程

说完了资源文件的一些基本信息以后,相信你对apk包内的资源文件有了一个更加明确的认识了吧,接下来我们就来讲一讲资源文件是如何打包到apk中的,这个过程非常复杂,需要好好的理解和记忆。

Android资源打包工具在编译应用程序资源之前,会创建资源表ResourceTable,当应用程序资源编译完之后,这个资源表就包含了资源的所有信息,然后就可以根据这个资源表来生成资源索引文件resources.arsc了。

解析AndroidManifest.xml

获取要编译资源的应用程序的包名、minSdkVersion等,有了包名就可以创建资源表了,也就是ResourceTable。

添加被引用的资源包

通常在编译一个apk包的时候,至少会涉及到两个资源包,一个是被引用的系统资源包,里面有很多系统级的资源,比如我们熟知的四大布局 LinearLayout、FrameLayout等以及一些属性layout_width、layout_height、layout_oritation等,另一个就是当前正在编译的应用程序的资源包。

收集资源文件

在编译应用程序资源之前,aapt会创建AaptAssets对象,用来收集当前需要编译的资源文件,这些资源文件被保存在AaptAssets类的成员变量mRes中。

将收集到的资源增加到资源表ResourceTable

之前将资源添加到了AaptAssets中,这一步将资源添加到ResourceTable中,我们最后要根据这个资源表来生成resources.arsc资源索引表,回头看看arsc文件的结构图,它也有一个resourceTable。

这一步收集到资源表的资源是不包括values的,因为values资源需要经过编译后,才能添加到资源表中

编译values资源

values资源描述的是一些比较简单的轻量级资源,如strings/colors/dimen等,这些资源是在编译的过程中进行收集的

给bag资源分配id

values资源下,除了string之外,还有其他的一些特殊资源,这些资源给自己定义一些专用的值,比如LinearLayout的orientation属性,它的取值范围为 vertical 和 horizontal,这就相当于定义了vertical和horizontal两个bag。

在编译其他非values资源之前,我们需要给之前收集到的bag资源分配资源id,因为它可能会被其它非values类资源所引用。

编译xml文件

之前的六步为编译xml文件做好了准备,收集到了xml所需要用到的所有资源,现在可以开始编译xml文件了,比如layout、anims、animators等。编译xml文件又可以分为四个步骤

解析xml文件

这一步会将xml文件转化为一系列树形结构的XMLNode,每一个XMLNode都表示一个xml元素,解析完成之后,就可以得到一个根节点XMLNode,然后就可以根据这个根节点来完成下边的操作

赋予属性名称id

这一步为每个xml元素的属性名称都赋予资源id,比如一个控件TextView,它有layout_width和layout_height两个属性,这里就要给这些属性名称赋予一个资源id。对系统资源包来说,这些属性名称都是它定义好的一些列bag资源,在编译的时候,就已经分配好了资源id了。

对于每一个xml文件都是从根节点开始给属性名称赋予资源id,然后再递归的给每一个子节点属性名称赋予资源id,一直到每一个节点的属性名称都有了资源id为止。

解析属性值

这一步是上一步的进一步深化,上一步为每个属性赋值id,这一步对属性对应的值进行解析,比如对于刚才的TextView,就会对其width和height的值进行解析,可能是match_parent也可能是warp_content.

压平xml文件

将xml文件进行扁平化处理,将其变为二进制格式,有如下几个步骤

  1. 收集有资源id的属性名称字符串,并将它们放在一个数组里。这些收集到的属性名称字符串保存在字符串资源池中,与收集到的资源id数组是一一对应的。
  2. 收集xml文件中其他所有的字符串,也就是没有资源id的字符串
  3. 写入xml文件头,最终编译出来的xml二进制文件是一系列的chunk组成的,每一个chunk都有一个头部,来描述元信息。
  4. 写入资源池字符串,将第一步和第二步收集到的内容写入Global String pool中,也就是之前所说的arsc文件结构里的全局字符串资源池中
  5. 写入资源id,将所有的资源id收集起来,生成package时要用到,对应arsc文件的结构的package。
  6. 压平xml文件,就是将各个xml元素中的字符串都替换掉,这些字符串或者被替换为到字符串资源池的一个索引,或者被替换为一个具有类型的其他值

给资源生成资源符号

这里生成资源符号为之后生成R文件做准备,之前的操作将所有收集到的资源文件都按照类型保存在资源表中,也就是ResourceTable对象。aapt在这里只需要遍历每一个package里面的type,然后取出每一个entry的名称,在根据其在相应的type中出现的次序,就可以计算出相应的资源id了,然后就能得到其资源符号。资源符号=名称+资源id

根据资源id生成资源索引表

在这里我们将生成resources.arsc,对其生成的步骤再次进行拆解

  1. 按照package收集类型字符串,如drawable、string、layout、id等,当前被编译的应用程序有几个package,就对应几组类型字符串,每一组类型字符串保存在其所属的package中。
  2. 收集资源型名称字符串,还是以package为单位,比如在string.xml中,<resources>    <string name="app_name">ABC</string>  </resources>就可以收集其中的属性app_name
  3. 收集资源项值字符串,还是上面的string.xml就可以收集到ABC
  4. 生成package数据块,就是按照之前说的resources.arsc文件格式中package的格式进行一步步的解析和收集
  5. 写入资源索引表头部,也就是ResTable_header
  6. 写入资源项的值字符串资源池,上面的第3步,将所有的值字符串收集起来了,这里直接写入就好了
  7. 写入package数据块,将第4步收集到的package数据块写入到资源索引表中。

经过以上几步,资源项索引表resources.arsc就生成好了。

编译AndroidManifest.xml文件

经过以上的几个步骤,应用程序的所有资源就编译完成了,这里就将应用程序的配置文件AndroidManifest.xml也编译为二进制文件。

生成R文件

到这里,我们已经知道了所有的资源以及其对应的id,然后就可以愉快的写入到R文件了,根据不同的type写到不同的静态内部类中,就像之前所描述的R文件的格式那样。

打包到APK

所有的资源文件都编译以及生成完之后,就可以将其打包到apk中了

  • assets目录
  • res目录,除了values之外,因为values目录下的资源文件经过编译以后,已经直接写入到资源索引表中去了
  • 资源索引表resources.arsc
  • 除了资源文件之外的其他文件(dex、AndroidManifest.xml、签名信息等)

结语

终于捋完了,整个资源文件的编译打包过程真的是很复杂又很繁琐的一个过程,在阅读的过程中要时刻对照着那几张机构图才能更好地对这些文件有更清晰的认识。资源文件在Android的学习和工作中是非常重要的,很多时候这些知识会被忽略掉,但是如果有时间好好捋一捋这些知识对于自身是一个很大的提升。

画个流程图

最后再用一张流程图来回顾一个整个流程

image.png


收起阅读 »

Android高手笔记 - 网络优化

一文带你了解android中对注入框架的检测。(以下的检测来源于对某APP进行逆向分析得出的情况)1.检测栈信息2.检测包名信息public static boolean xp1(Context context) {         boolean scan...
继续阅读 »

一文带你了解android中对注入框架的检测。

(以下的检测来源于对某APP进行逆向分析得出的情况)

1.检测栈信息

image.png

2.检测包名信息

public static boolean xp1(Context context) {

        boolean scanPackage = scanPackage(context, new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5pbnN0YWxsZXI=", 2)));

        MLog.b("attack", "Installed xposed:" + scanPackage);

        return scanPackage;

}

解密
ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5pbnN0YWxsZXI= = de.robv.android.xposed.installer
 

 

 public static boolean xp2(Context context) {

        StackTraceElement[] stackTrace;

        context.getFilesDir();

        try {

            throw new Exception("凸一_一凸");

        } catch (Exception e) {

            MLog.a("attack", e.getMessage());

            boolean z = false;

            for (StackTraceElement stackTraceElement : e.getStackTrace()) {

                if (stackTraceElement.getClassName().equals(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2))) && stackTraceElement.getMethodName().equals(new String(Base64.decode("bWFpbg==", 2)))) {

                    z = true;

                }

                if (stackTraceElement.getClassName().equals(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2))) && stackTraceElement.getMethodName().equals(new String(Base64.decode("aGFuZGxlSG9va2VkTWV0aG9k", 2)))) {

                    z = true;

                }

            }

            MLog.b("attack", "Exception hit:" + z);

            return z;

        }

    }

 

解密:

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=de.robv.android.xposed.XposedBridge


aGFuZGxlSG9va2VkTWV0aG9k = handleHookedMethod

bWFpbg==main
 

 ```

```C++


  public static String xp3(Context context) {

        String str;

        context.getFilesDir();

        try {

            Field declaredField = DexAOPEntry.java_lang_ClassLoader_loadClass_proxy(ClassLoader.getSystemClassLoader(), new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRIZWxwZXJz", 2))).getDeclaredField(new String(Base64.decode("ZmllbGRDYWNoZQ==", 2)));

            declaredField.setAccessible(true);

            Map map = (Map) declaredField.get(null);

            ArrayList arrayList = new ArrayList();

            arrayList.addAll(map.keySet());

            str = new JSONArray(arrayList).toString();

        } catch (Exception e) {

            str = null;

        }

        MLog.b("attack", "FieldInHook msg:" + str);

        return str;

    }


解密:

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRIZWxwZXJz =de.robv.android.xposed.XposedHelpers

ZmllbGRDYWNoZQ== fieldCache


 public static String xp4(Context context) {

        String str;

        context.getFilesDir();

        PackHookPlugin packHookPlugin = new PackHookPlugin(1);

        try {

            Field declaredField = DexAOPEntry.java_lang_ClassLoader_loadClass_proxy(ClassLoader.getSystemClassLoader(), new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2))).getDeclaredField(new String(Base64.decode("c0hvb2tlZE1ldGhvZENhbGxiYWNrcw==", 2)));

            declaredField.setAccessible(true);

            Map map = (Map) declaredField.get(null);

            Class java_lang_ClassLoader_loadClass_proxy = DexAOPEntry.java_lang_ClassLoader_loadClass_proxy(ClassLoader.getSystemClassLoader(), new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2UkQ29weU9uV3JpdGVTb3J0ZWRTZXQ=", 2)));

            Method declaredMethod = java_lang_ClassLoader_loadClass_proxy.getDeclaredMethod(new String(Base64.decode("Z2V0U25hcHNob3Q=", 2)), new Class[0]);

            for (Entry entry : map.entrySet()) {

                Member member = (Member) entry.getKey();

                Object value = entry.getValue();

                String a = ScanMethod.a(member.toString());

                if (!"".equals(a) && java_lang_ClassLoader_loadClass_proxy.isInstance(value)) {

                    for (Object obj : (Object[]) declaredMethod.invoke(value, new Object[0])) {

                        String[] split = obj.getClass().getClassLoader().toString().split("\"");

                        if (split.length > 1) {

                            packHookPlugin.a(StringTool.a(split, 1), a);

                        }

                    }

                }

            }

            JSONArray a2 = packHookPlugin.a();

            JSONArray methodToNative = methodToNative();

            if (a2 != null) {

                if (methodToNative != null) {

                    for (int i = 0; i < methodToNative.length(); i++) {

                        a2.put(methodToNative.getJSONObject(i));

                    }

                }

                str = a2.toString();

            } else {

                if (methodToNative != null) {

                    str = methodToNative.toString();

                }

                str = null;

            }

        } catch (Exception e) {

        }

        MLog.b("attack", "MethodInHook msg:" + str);

        return str;

}

解密:

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=de.robv.android.xposed.XposedBridge

 

c0hvb2tlZE1ldGhvZENhbGxiYWNrcw== sHookedMethodCallbacks

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2UkQ29weU9uV3JpdGVTb3J0ZWRTZXQ= de.robv.android.xposed.XposedBridge$CopyOnWriteSortedSet

Z2V0U25hcHNob3Q=getSnapshot

 ```

```C++


 public static boolean xp5(Context context) {

        try {

            Throwable th = new Throwable();

            th.setStackTrace(new StackTraceElement[]{new StackTraceElement(new String(Base64.decode("U2NhbkF0dGFjaw==", 2)), "", "", 0), new StackTraceElement(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2)), "", "", 0)});

            StackTraceElement[] stackTrace = th.getStackTrace();

            if (stackTrace.length != 2 || !stackTrace[1].getClassName().equals(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2)))) {

                return true;

            }

            return false;

        } catch (Exception e) {

            return false;

        }

    }

解密:

U2NhbkF0dGFjaw== ScanAttack

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U= de.robv.android.xposed.XposedBridge


    public static boolean xp6(Context context) {

        try {

            StringWriter stringWriter = new StringWriter();

            new Throwable().printStackTrace(new PrintWriter(stringWriter));

            if (stringWriter.toString().contains(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZA==", 2)))) {

                return true;

            }

            return false;

        } catch (Exception e) {

            return false;

        }

    }

解密:ZGUucm9idi5hbmRyb2lkLnhwb3NlZA==de.robv.android.xposed

收起阅读 »

Android基础到进阶UI祖宗级 View介绍+实用

View的继承关系在Android系统中,任何可视化控件都需要从android.view.View类继承。而任何从android.view.View继承的类都可以称为视图(View)。Android的绝大部分UI组件都放在android.widget包及其子包...
继续阅读 »

View的继承关系

在Android系统中,任何可视化控件都需要从android.view.View类继承。而任何从android.view.View继承的类都可以称为视图(View)。Android的绝大部分UI组件都放在android.widget包及其子包,下图就是android.widget包中所有View及其子类的继承关系:

从上图看,有很多布局类等为什么没有在上图看到,在这里要说明一下这里仅是android.widget包的,还有其他视图的虽然也继承View但是他们不属于android.widget包例如下面两个组件:

RecyclerView继承ViewGroup,但是属于androidx.recyclerview.widget包的。

ConstraintLayout继承ViewGroup,但是属于androidx.constraintlayout.widget包的;

其他还有很多其他包、或自定义控件,这里就不做过多描述了。

Android中的视图类可分为3种:布局(Layout)类视图容器(View Container)类视图类(例TextView),这3种类都是android.view.View的子类。ViewGroup是一个容器类,该类也是View的重要子类,所有的布局类和视图容器类都是ViewGroup的子类,而视图类直接继承自View类。 下图描述了View、ViewGroup、视图容器类及视图类的继承关系。

从上图所示的继承关系可以看出:

  • Button、TextView、EditText都是视图类,TextView是Button和EditText的父类,TextView直接继承自View类。
  • GridView和ListView是ViewGroup的子类,但并不是直接子类,GridView、ListView继承自AbsListView继承自AdapterView继承自ViewGroup,从而形成了视图容器类的层次结构。
  • 布局视图虽然也属于容器视图,但由于布局视图具有排版功能,所以将这类视图置为布局类

对于一个Android应用的图形用户界面来说,ViewGroup作为容器来装其他组件,而ViewGroup里除了可以包含普通View组件之外,还可以再次包含ViewGroup组件。

创建View对象

使用XML布局定义View,再用代码控制View

XML布局文件是Android系统中定义视图的常用方法,所有的XML布局文件必须保存在res/layout目录中。XML布局文件的命名及定义需要注意如下几点:

  • XML布局文件的扩展名必须是xml。
  • 由于aapt会根据每一个XML布局文件名在R类的内嵌类中生成一个int类型的变量,这个变量名就是XML布局文件名,因此,XML布局文件名(不包含扩展名)必须符合Java变量名的命名规则,例如,XML布局文件名不能以数字开头。
  • 每一个XML布局文件的根节点可以是任意的视图标签,如< LinearLayout >,< TextView >。
  • XML布局文件的根节点必须包含android命名空间,而且命名空间的值必须是android="schemas.android.com/apk/res/and…
  • 为XML布局文件中的标签指定ID时需要使用这样的格式:@+id/tv_xml,其实@+id就是在R.java文件里新增一个id名称,在同一个xml文件中确保ID唯一。
  • 由于每一个视图ID都会在R.id类中生成与之相对应的变量,因此,视图ID的值也要符合Java变量的命名规则,这一点与XML布局文件名的命名规则相同。

举例

1.创建activity_view.xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color_666666"
    android:orientation="vertical">
    <TextView
        android:id="@+id/tv_xml"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dimen_20"
        android:background="@color/color_188FFF"
        android:padding="@dimen/dimen_10"
        android:text="XML设置TextView"
        android:textColor="@color/white"
        android:textSize="@dimen/text_size_18" />
    <Button
        android:id="@+id/btn_xml"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dimen_20"
        android:background="@color/color_188FFF"
        android:padding="@dimen/dimen_10"
        android:text="按钮"
        android:textColor="@color/white"
        android:textSize="@dimen/text_size_18" />
</LinearLayout>

2.加载布局文件、关联控件

如果要使用上面的XML布局文件(activity_view.xml),通常需要在onCreate方法中使用setContentView方法指定XML布局文件的资源lD,并获取在activity_view.xml文件中定义的某个View,代码如下:

public class ViewActivity extends AppCompatActivity{
    private Button btnXml;
    private TextView tvXml;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //加载布局文件
        setContentView(R.layout.activity_view);
        //如果想获得在activity_view.xml文件中定义的某个View
        //关联控件:R.id.tv_xml是tvXml的ID,确保这个ID在R.layout.activity_view中
        tvXml = findViewById(R.id.tv_xml);
        //关联控件:R.id.btn_xml是btnXml的ID,确保这个ID在R.layout.activity_view中
        btnXml = findViewById(R.id.btn_xml);
    }
}

3.在获得XML布局文件中的视图对象时需要注意下面几点:

  • 先使用setContentView方法装载XML布局文件,再使用findViewByld方法,否则findViewByld方法会由于未找到控件而产生空指针异常,导致应用崩溃。

  • 虽然所有的XML布局文件中的视图ID都在R.id类中生成了相应的变量,但使用findViewByld方法只能获得已经装载的XML布局文件中的视图对象。

    • 例,activity_view.xml中TextView的对应R.id.tv_xml;
    • 其他XML文件中有TextView的R.id.tv_shuaiciid,tv_shuaici不在activity_view.xml中如果使用 tvXml = findViewById(R.id.tv_shuaici);
    • 结果应用崩溃。原因:在activity_view.xml中找不到ID为tv_shuaici的视图对象。

4.用代码控制视图

虽然使用XML布局文件可以非常方便地对控件进行布局,但若想控制这些控件的行为,仍然需要编写Java代码。在上面介绍了使用findViewByld方法获得指定的视图对象,当获得视图对象后,就可以使用代码来控制这些视图对象了。例如,下面的代码获得了一个TextView对象,并修改了TextView的文本。

TextView tvXml = findViewById(R.id.tv_xml);
//直接使用字符串来修改TextView的文本
tvXml.setText("帅次");
//使用字符串资源(res/values/strings.xml)
//其中R.string.str_tv_shuaici是字符串资源ID,系统会使用这个ID对应的字符串设置TextView的文本。
tvXml.setText(R.string.str_tv_shuaici);

选择其中一样即可,如果同时设置,最后一次设置为最终结果。

使用代码的方式来创建View对象

在更高级的Android应用中,往往需要动态添加视图。要实现这个功能,最重要的是获得当前的视图容器对象,这个容器对象所对应的类需要继承ViewGroup类。 将其他的视图添加到当前的容器视图中需要如下几步:

  • 第1步,获得当前的容器视图对象;
  • 第2步,获得或创建待添加的视图对象;
  • 第3步,将相应的视图对象添加到容器视图中。

实例

1.获得当前的容器视图对象

//1、获取activity_view.xml中LinearLayout对象
 //2、也可以给LinearLayout添加@+id/,然后通过findViewById关联控件也能获取LinearLayout对象
LinearLayout linearLayout =
        (LinearLayout)getLayoutInflater().inflate(R.layout.activity_view,null);
//加载布局文件
setContentView(linearLayout);

2.获得或创建待添加的视图对象

EditText editText = new EditText(this);
editText.setHint("请输入内容");

3.将相应的视图对象添加到容器视图中

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //1、获取activity_view.xml中LinearLayout对象
        //2、也可以给LinearLayout添加@+id/,然后通过findViewById关联控件也能获取LinearLayout对象
        LinearLayout linearLayout =
                (LinearLayout)getLayoutInflater().inflate(R.layout.activity_view,null);
        //加载布局文件
        setContentView(linearLayout);
        EditText editText = new EditText(this);
        editText.setHint("请输入内容");
        linearLayout.addView(editText);
    }

效果图如下:

总结

  • 实际上不管使用那种方式,他们创建Android用户界面行为的本质是完全一样的。大部分时候,设置UI组件的XML属性还有对应的方法。
  • 对于View类而言,它是所有UI组件的基类,因此它包含的XML属性和方法是所有组件都可以使用的。

自定义View

为什么要自定义View

Android系统提供了一系列的原生控件,但这些原生控件并不能够满足我们的需求时,我们就需要自定义View了。

自定义View的基本方法

自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。

  • 测量:onMeasure()决定View的大小;
  • 布局:onLayout()决定View在ViewGroup中的位置;
  • 绘制:onDraw()决定绘制这个View。

需要用到的两个对象

  • Canvas(画布),可在画布上面绘制东西,绘制的内容取决于所调用的方法。如drawCircle方法,用来绘制圆形,需要我们传入圆心的x和y坐标,以及圆的半径。
  • Paint(画笔),用来告诉画布,如何绘制那些要被绘制的对象。

这两个方法暂时了解就行,如果拓展开,这不够写,后面可能会针对这两个对象单独拉一个章节出来。

自绘控件View实例

1、直接继承View类

自绘View控件时,最主要工作就是绘制出丰富的内容,这一过程是在重写的onDraw方法中实现的。由于是View,它没有子控件了,所以重写onLayout没有意义。onMeasure的方法可以根据自己的需要来决定是否需要重写,很多情况下,不重写该方法并不影响正常的绘制。

/**
 * 创建人:scc
 * 功能描述:自定义View
 */

public class CustomView extends View {
    private Paint paint;
    //从代码创建视图时使用的简单构造函数。
    public CustomView(Context context) {
        super(context);
    }
    //从XML使用视图时调用的构造函数。
    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    //View的绘制工作
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //实例化画笔对象
        paint = new Paint();
        //给画笔设置颜色
        paint.setColor(Color.RED);
        //设置画笔属性
        //paint.setStyle(Paint.Style.FILL);//画笔属性是实心圆
        paint.setStyle(Paint.Style.STROKE);//画笔属性是空心圆
        paint.setStrokeWidth(10);//设置画笔粗细
        //cx:圆心的x坐标;cy:圆心的y坐标;参数三:圆的半径;参数四:定义好的画笔
        canvas.drawCircle(getWidth() / 4, getHeight() / 4150, paint);
    }
}

2、在布局 XML 文件中使用自定义View

<com.scc.demo.view.CustomView
        android:id="@+id/view_circle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

3、实现效果:

性能优化

在自定义View时需要注意,避免犯以下的性能错误:

  • 在非必要时,对View进行重绘。
  • 绘制一些不被用户所看到的的像素,也就是过度绘制。(被覆盖的地方)
  • 在绘制期间做了一些非必要的操作,导致内存资源的消耗。

可进一步了解和优化:

  • View.invalite()是最最广泛的使用操作,因为在任何时候都是刷新和更新视图最快的方式。

在自定义View时要小心避免调用非必要的方法,因为这样会导致重复强行绘制整个视图层级,消耗宝贵的帧绘制周期。检查清楚View.invalite()和View.requestLayout()方法调用时间位置,因为这会影响整个UI,导致GPU和它的帧速率变慢。

  • 避免过渡重绘。为了避免过渡重绘,我们可以利用Canvas方法,只绘制控件中所需要的部分。整个一般在重叠部分或控件时特别有用。相应的方法是Canvas.clipRect()(指定要被绘制的区域);
  • 在实现View.onDraw()方法中,不应该在方法内及调用的方法中进行任何的对象分配。在该方法中进行对象分配,对象会被创建和初始化。而当View.onDraw()方法执行完毕时。垃圾回收器会释放内存。如果View带动画,那么View在一秒内会被重绘60次。所以要避免在View.onDraw()方法中分配内存。

永远不要在View.onDraw()方法中及调用的方法中进行内存分配,避免带来负担。垃圾回收器多次释放内存,会导致卡顿。最好的方式就是在View被首次创建出来时,实例化这些对象。

到这里View基本差不多了,还有其他属性、方法、事件等,在后面的TexView、Button、Layout等中慢慢了解。

收起阅读 »

iOS开发常见面试题(底层篇)

1.iOS 类(class)和结构体(struct)有什么区别?Swift 中,类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型的区别。举个简单的例子,代码如下clas...
继续阅读 »

1.iOS 类(class)和结构体(struct)有什么区别?

Swift 中,类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型的区别。

举个简单的例子,代码如下

class Temperature {
var value: Float = 37.0
}

class Person {
var temp: Temperature?

func sick() {
temp?.value = 41.0
}
}

let A = Person()
let B = Person()
let temp = Temperature()

A.temp = temp
B.temp = temp

A.sick() 上面这段代码,由于 Temperature 是 class ,为引用类型,故 A 的 temp 和 B 的 temp指向同一个对象。A 的 temp修改了,B 的 temp 也随之修改。这样 A 和 B 的 temp 的值都被改成了41.0。如果将 Temperature 改为 struct,为值类型,则 A 的 temp 修改不影响 B 的 temp。

内存中,引用类型诸如类是在堆(heap)上,而值类型诸如结构体实在栈(stack)上进行存储和操作。相比于栈上的操作,堆上的操作更加复杂耗时,所以苹果官方推荐使用结构体,这样可以提高 App 运行的效率。

class有这几个功能struct没有的:

class可以继承,这样子类可以使用父类的特性和方法 类型转换可以在runtime的时候检查和解释一个实例的类型 可以用deinit来释放资源 一个类可以被多次引用 struct也有这样几个优势:

结构较小,适用于复制操作,相比于一个class的实例被多次引用更加安全。 无须担心内存memory leak或者多线程冲突问题


2.iOS自动释放池是什么,如何工作 ?

当您向一个对象发送一个autorelease消息时,Cocoa就会将该对象的一个引用放入到最新的自动释放池。它仍然是个正当的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放。

1.object-c 是通过一种"referring counting"(引用计数)的方式来管理内存的, 对象在开始分配内存(alloc)的时候引用计数为一,以后每当碰到有copy,retain的时候引用计数都会加一, 每当碰到release和autorelease的时候引用计数就会减一,如果此对象的计数变为了0, 就会被系统销毁.

2.NSAutoreleasePool 就是用来做引用计数的管理工作的,这个东西一般不用你管的.

3.autorelease和release没什么区别,只是引用计数减一的时机不同而已,autorelease会在对象的使用真正结束的时候才做引用计数减一.

3.iOS你在项目中用过 runtime 吗?举个例子

Objective-C 语言是一门动态语言,编译器不需要关心接受消息的对象是何种类型,接收消息的对象问题也要在运行时处理。

pragramming 层面的 runtime 主要体现在以下几个方面:

1.关联对象 Associated Objects
2.消息发送 Messaging
3.消息转发 Message Forwarding
4.方法调配 Method Swizzling
5.“类对象” NSProxy Foundation | Apple Developer Documentation
6.KVC、KVO About Key-Value Coding

4.KVC /KVO的底层原理和使用场景

1 KVC(KeyValueCoding)
1.1 KVC 常用的方法

(1)赋值类方法
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

(2)取值类方法
// 能取得私有成员变量的值
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

1.2 KVC 底层实现原理

当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作:
1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值
2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(_key).如果有,直接给该成员属性进行赋值
3.如果没有成员属性_key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值
4.如果都没有,就会调用 valueforUndefinedKey 和setValue:forUndefinedKey:方法
1.3 KVC 的使用场景
1.3.1 赋值
(1) KVC 简单属性赋值

Person *p = [[Person alloc] init];
// p.name = @"jack";
// p.money = 22.2;
使用setValue: forKey:方法能够给属性赋值,等价于直接给属性赋值
[p setValue:@"rose" forKey:@"name"];
[p setValue:@"22.2" forKey:@"money"];

(2) KVC复杂属性赋值

//给Person添加 Dog属性
Person *p = [[Person alloc] init];
p.dog = [[Dog alloc] init];
// p.dog.name = @"阿黄";

1)setValue: forKeyPath: 方法的使用
//修改p.dog 的name 属性
[p.dog setValue:@"wangcai" forKeyPath:@"name"];
[p setValue:@"阿花" forKeyPath:@"dog.name"];

2)setValue: forKey: 错误用法
[p setValue:@"阿花" forKey:@"dog.name"];
NSLog(@"%@", p.dog.name);

3)直接修改私有成员变量
[p setValue:@"旺财" forKeyPath:@"_name"];

(3) 添加私有成员变量

Person 类中添加私有成员变量_age
[p setValue:@"22" forKeyPath:@"_age"];

1.3.2 字典转模型

(1)简单的字典转模型
+(instancetype)videoWithDict:(NSDictionary *)dict
{
JLVideo *videItem = [[JLVideo alloc] init];
//以前
// videItem.name = dict[@"name"];
// videItem.money = [dict[@"money"] doubleValue] ;

//KVC,使用setValuesForKeysWithDictionary:方法,该方法默认根据字典中每个键值对,调用setValue:forKey方法
// 缺点:字典中的键值对必须与模型中的键值对完全对应,否则程序会崩溃
[videItem setValuesForKeysWithDictionary:dict];
return videItem;
}

(2)复杂的字典转模型
注意:复杂字典转模型不能直接通过KVC 赋值,KVC只能在简单字典中使用,比如:
NSDictionary *dict = @{
@"name" : @"jack",
@"money": @"22.2",
@"dog" : @{
@"name" : @"wangcai",
@"money": @"11.1",

}

};
JLPerson *p = [[JLPerson alloc]init]; // p是一个模型对象
[p setValuesForKeysWithDictionary:dict];
内部转换原理:
// [p setValue:@"jack" forKey:@"name"];
// [p setValue:@"22.2" forKey:@"money"];
// [p setValue:@{
// @"name" : @"wangcai",
// @"money": @"11.1",
//
// } forKey:@"dog"]; //给 dog赋值一个字典肯定是不对的

(3)KVC解析复杂字典的正确步骤
NSDictionary *dict = @{
@"name" : @"jack",
@"money": @"22.2",
@"dog" : @{
@"name" : @"wangcai",
@"price": @"11.1",
},
//人有好多书
@"books" : @[
@{
@"name" : @"5分钟突破iOS开发",
@"price" : @"19.8"
},
@{
@"name" : @"3分钟突破iOS开发",
@"price" : @"24.8"
},
@{
@"name" : @"1分钟突破iOS开发",
@"price" : @"29.8"
}
]
};

XMGPerson *p = [[XMGPerson alloc] init];
p.dog = [[XMGDog alloc] init];
[p.dog setValuesForKeysWithDictionary:dict[@"dog"]];

//保存模型的可变数组
NSMutableArray *arrayM = [NSMutableArray array];

for (NSDictionary *dict in dict[@"books"]) {
//创建模型
Book *book = [[Book alloc] init];
//KVC
[book setValuesForKeysWithDictionary:dict];
//将模型保存
[arrayM addObject:book];
}

p.books = arrayM;

备注:
(1)当字典中的键值对很复杂,不适合用KVC;
(2)服务器返还的数据,你可能不会全用上,如果在模型一个一个写属性非常麻烦,所以不建议使用KVC字典转模型

1.3.3 取值
(1) 模型转字典

 Person *p = [[Person alloc]init];
p.name = @"jack";
p.money = 11.1;
//KVC取值
NSLog(@"%@ %@", [p valueForKey:@"name"], [p valueForKey:@"money"]);

//模型转字典, 根据数组中的键获取到值,然后放到字典中
NSDictionary *dict = [p dictionaryWithValuesForKeys:@[@"name", @"money"]];
NSLog(@"%@", dict);

(2) 访问数组中元素的属性值

Book *book1 = [[Book alloc] init];
book1.name = @"5分钟突破iOS开发";
book1.price = 10.7;

Book *book2 = [[Book alloc] init];
book2.name = @"4分钟突破iOS开发";
book2.price = 109.7;

Book *book3 = [[Book alloc] init];
book3.name = @"1分钟突破iOS开发";
book3.price = 1580.7;

// 如果valueForKeyPath:方法的调用者是数组,那么就是去访问数组元素的属性值
// 取得books数组中所有Book对象的name属性值,放在一个新的数组中返回
NSArray *books = @[book1, book2, book3];
NSArray *names = [books valueForKeyPath:@"name"];
NSLog(@"%@", names);

//访问属性数组中元素的属性值
Person *p = [[Person alloc]init];
p.books = @[book1, book2, book3];
NSArray *names = [p valueForKeyPath:@"books.name"];
NSLog(@"%@", names);

2 KVO (Key Value Observing)
2.1 KVO 的底层实现原理

(1)KVO 是基于 runtime 机制实现的
(2)当一个对象(假设是person对象,对应的类为 JLperson)的属性值age发生改变时,系统会自动生成一个继承自JLperson的类NSKVONotifying_JLPerson,在这个类的 setAge 方法里面调用
[super setAge:age];
[self willChangeValueForKey:@"age"];
[self didChangeValueForKey:@"age"];
三个方法,而后面两个方法内部会主动调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context方法,在该方法中可以拿到属性改变前后的值.

2.2 KVO的作用
作用:能够监听某个对象属性值的改变

// 利用KVO监听p对象name 属性值的改变
Person *p = [[XMGPerson alloc] init];
p.name = @"jack";

/* 对象p添加一个观察者(监听器)
Observer:观察者(监听器)
KeyPath:属性名(需要监听哪个属性)
*/

[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];

/**
* 利用KVO 监听到对象属性值改变后,就会调用这个方法
*
* @param keyPath 哪一个属性被改了
* @param object 哪一个对象的属性被改了
* @param change 改成什么样了
*/

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
// NSKeyValueChangeNewKey == @"new"
NSString *new = change[NSKeyValueChangeNewKey];
// NSKeyValueChangeOldKey == @"old"
NSString *old = change[NSKeyValueChangeOldKey];

NSLog(@"%@-%@",new,old);
}

5.iOS中持久化方式有哪些?

属性列表文件 -- NSUserDefaults 的存储,实际是本地生成一个 plist 文件,将所需属性存储在 plist 文件中

对象归档 -- 本地创建文件并写入数据,文件类型不限

SQLite 数据库 -- 本地创建数据库文件,进行数据处理

CoreData -- 同数据库处理思想相同,但实现方式不同

6.什么是KVC和KVO?

KVC(Key-Value-Coding)内部的实现:一个对象在调用setValue的时候
(1)首先根据方法名找到运行方法的时候所需要的环境参数。
(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。
(3)再直接查找得来的具体的方法实现。KVO(Key-Value- Observing):当观察者为一个对象的属性进行了注册,被观察对象的isa指针被修改的时候,isa指针就会指向一个中间类,而不是真实的类。所以 isa指针其实不需要指向实例对象真实的类。所以我们的程序最好不要依赖于isa指针。在调用类的方法的时候,最好要明确对象实例的类名

7.iOS中属性修饰符的作用?

ios5之前是MRC,内存需要程序员进行管理,ios5之后是ARC,除非特殊情况,比如C框架或者循环引用,其他时候是不需要程序员手动管理内存的。 ios中当我们定义属性@property的时候就需要属性修饰符,下面我们就看一下不同属性修饰符的作用。有错误和不足的地方还请大家谅解并批评指正。

主要的属性修饰符有下面几种:

  • copy
  • assign
  • retain
  • strong
  • weak
  • readwrite/readonly (读写策略、访问权限)
  • nonatomic/atomic (安全策略)

如果以MRC和ARC进行区分修饰符使用情况,可以按照如下方式进行分组:

 1. MRC: assign/ retain/ copy/  readwritereadonly/ nonatomic、atomic  等。
2. ARC: assign/ strong/ weak/ copy/ readwritereadonly/ nonatomic、atomic 等。

属性修饰符对retainCount计数的影响。

  1. alloc为对象分配内存,retainCount 为1 。
  2. retain MRC下 retainCount + 1。
  3. copy 一个对象变成新的对象,retainCount为 1, 原有的对象计数不变。
  4. release 对象的引用计数 -1。
  5. autorelease 对象的引用计数 retainCount - 1,如果为0,等到最近一个pool结束时释放。

不管MRC还是ARC,其实都是看reference count是否为0,如果为0那么该对象就被释放,不同的地方是MRC需要程序员自己主动去添加retain 和 release,而ARC apple已经给大家做好,自动的在合适的地方插入retain 和 release类似的内存管理代码,具体原理如下,图片摘自官方文档。



MRC 和 ARC原理

下面就详述上所列的几种属性修饰符的使用场景,应用举例和注意事项。

8.iOS atomatic nonatomic区别和理解

第一种

atomic和nonatomic区别用来决定编译器生成的getter和setter是否为原子操作。atomic提供多线程安全,是描述该变量是否支持多线程的同步访问,如果选择了atomic 那么就是说,系统会自动的创建lock锁,锁定变量。nonatomic禁止多线程,变量保护,提高性能。

atomic:默认是有该属性的,这个属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题。

nonatomic:如果该对象无需考虑多线程的情况,请加入这个属性,这样会让编译器少生成一些互斥加锁代码,可以提高效率。

atomic的意思就是setter/getter这个函数,是一个原语操作。如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,可以保证数据的完整性。nonatomic不保证setter/getter的原语行,所以你可能会取到不完整的东西。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。

比如setter函数里面改变两个成员变量,如果你用nonatomic的话,getter可能会取到只更改了其中一个变量时候的状态,这样取到的东西会有问题,就是不完整的。当然如果不需要多线程支持的话,用nonatomic就够了,因为不涉及到线程锁的操作,所以它执行率相对快些。

下面是载录的网上一段加了atomic的例子:




{lock}
if (property != newValue) {
[property release];
property = [newValue retain];
}
{unlock}

可以看出来,用atomic会在多线程的设值取值时加锁,中间的执行层是处于被保护的一种状态,atomic是oc使用的一种线程保护技术,基本上来讲,就是防止在写入未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

第二种

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。

atomic
设置成员变量的@property属性时,默认为atomic,提供多线程安全。

在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。加了atomic,setter函数会变成下面这样:


                    {lock}
if (property != newValue) {
[property release];
property = [newValue retain];
}
{unlock}

nonatomic
3禁止多线程,变量保护,提高性能。

3atomic是Objc使用的一种线程保护技术,基本上来讲,是防止在写未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

3指出访问器不是原子操作,而默认地,访问器是原子操作。这也就是说,在多线程环境下,解析的访问器提供一个对属性的安全访问,从获取器得到的返回值或者通过设置器设置的值可以一次完成,即便是别的线程也正在对其进行访问。如果你不指定 nonatomic ,在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值。

9.iOS UIViewController的完整生命周期

UIViewController的完整生命周期

-[ViewControllerinitWithNibName:bundle:];

-[ViewControllerinit];

-[ViewControllerloadView];

-[ViewControllerviewDidLoad];

-[ViewControllerviewWillDisappear:];

-[ViewControllerviewWillAppear:];

-[ViewControllerviewDidAppear:];

-[ViewControllerviewDidDisappear:];

1、 alloc 创建对象,分配空间

2、init(initWithNibName) 初始化对象,初始化数据

3、loadView 从nib载入视图 ,通常这一步不需要去干涉。除非你没有使用xib文件创建视图

4、viewDidLoad 载入完成,可以进行自定义数据以及动态创建其他控件

5、viewWillAppear 视图将出现在屏幕之前,马上这个视图就会被展现在屏幕上了

6、viewDidAppear 视图已在屏幕上渲染完成

当一个视图被移除屏幕并且销毁的时候的执行顺序,这个顺序差不多和上面的相反

1、viewWillDisappear 视图将被从屏幕上移除之前执行

2、viewDidDisappear 视图已经被从屏幕上移除,用户看不到这个视图了

3、dealloc 视图被销毁,此处需要对你在init和viewDidLoad中创建的对象进行释放

ViewController 的 loadView,、viewDidLoad,、viewDidUnload 分别是在什么时候调用的?

viewDidLoad在view从nib文件初始化时调用,loadView在controller的view为nil时调用。

此方法在编程实现view时调用,view控制器默认会注册memory warning notification,当view controller的任何view没有用的时候,viewDidUnload会被调用,在这里实现将retain的view release,如果是retain的IBOutlet view属性则不要在这里release,IBOutlet会负责release。

10.ios7 层协议,tcp四层协议及如何对应的?


11.iOS应用导航模式有哪些?

平铺模式,一般由scrollView和pageControl组合而成的展示方式。手机自带的天气比较典型。

标签模式,tabBar的展示方式,这个比较常见。

树状模式,tableView的多态展示方式,常见的9宫格、系统自带的邮箱等展现方式。

12.一个参数既可以是const还可以是volatile吗?解释为什么。

• 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

13.iOS 响应者链的事件传递过程?

如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图

在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

如果window对象也不处理,则其将事件或消息传递给UIApplication对象

如果UIApplication也不能处理该事件或消息,则将其丢弃

14.iOS 请说明并比较以下关键词:weak,block

weak与weak基本相同。前者用于修饰变量(variable),后者用于修饰属性(property)。weak 主要用于防止block中的循环引用。 block也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。block用于修饰某些block内部将要修改的外部变量。 weak和block的使用场景几乎与block息息相关。而所谓block,就是Objective-C对于闭包的实现。闭包就是没有名字的函数,或者理解为指向函数的指针。

15.iOS UIView的Touch事件注意点

如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件(掌握)
UIView不接收触摸事件的三种情况:
不接收用户交互 : userInteractionEnabled = NO
隐藏 : hidden = YES
透明 : alpha = 0.0 ~ 0.01
UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的

16.iOS 说明并比较关键词:strong, weak, assign, copy等等

strong表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。

weak表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。

assign主要用于修饰基本数据类型,如NSInteger和CGFloat,这些数值主要存在于栈上。

weak 一般用来修饰对象,assign一般用来修饰基本数据类型。原因是assign修饰的对象被释放后,指针的地址依然存在,造成野指针,在堆上容易造成崩溃。而栈上的内存系统会自动处理,不会造成野指针。

copy与strong类似。不同之处是strong的复制是多个指针指向同一个地址,而copy的复制每次会在内存中拷贝一份对象,指针指向不同地址。copy一般用在修饰有可变对应类型的不可变对象上,如NSString, NSArray, NSDictionary。

Objective-C 中,基本数据类型的默认关键字是atomic, readwrite, assign;普通属性的默认关键字是atomic, readwrite, strong。

1、属性readwrite,readonly,assign,retain,copy,nonatomic 各自什么作用,他们在那种情况下用?

readwrite:默认的属性,可读可写,生成settergetter方法。

readonly:只读,只生成getter方法,也就是说不能修改变量。

assign:用于声明基本数据类型(intfloat)仅设置变量,是赋值属性。

retain:持有属性,setter方法将传入的参数先保留,再赋值,传入的参数 引用计数retaincount 会加1

在堆上开辟一块空间,用指针a指向,然后将指针a赋值(assign)给指针b,等于是a和b同时指向这块堆空间,当a不使用这块堆空间的时候,是否要释放这块堆空间?答案是肯定要的,但是这件堆空间被释放后,b就成了野指针。

如何避免这样的问题? 这就引出了引用计数器,当a指针这块堆空间的时候,引用计数器+1,当b也指向的时候,引用计数器变成了2,当a不再指向这块堆空间时,release-1,引用计数器为1,当b也不指向这块堆空间时,release-1,引用计数器为0,调用dealloc函数,空间被释放

总结:当数据类型为int,float原生类型时,可以使用assign。如果是上面那种情况(对象)就是用retain。

copy:是赋值特性,setter方法将传入对象赋值一份;需要完全一份新的变量时,直接从堆区拿。

当属性是 NSString、NSArray、NSDictionary时,既可以用strong 修饰,也可以用copy修饰。当用strong修饰的NSString 指向一个NSMutableString时,如果在不知情的情况下这个NSMutableString的别的引用修改了值,就会出现:一个不可变的字符串却被改变了的情况, 使用copy就不会出现这种情况。


nonatomic:非原子性,可以多线程访问,效率高。
atomic:原子性,属性安全级别的表示,同一时刻只有一个线程访问,具有资源的独占性,但是效率很低。
strong:强引用,引用计数+ 1,ARC下,一个对象如果没有强引用,系统就会释放这个对象。
weak:弱引用,不会使引用计数+1.当一个指向对象的强引用都被释放时,这块空间依旧会被释放掉。

使用场景:在ARC下,如果使用XIB 或者SB 来创建控件,就使用 weak。纯代码创建控件时,用strong修饰,如果想用weak 修饰,就需要先创建控件,然后赋值给用weak修饰的对象。

查找了一些资料,发现主要原因是,controller需要拥有它自己的view(这个view是所以子控件的父view),因此viewcontroller对view就必须是强引用(strong reference),得用strong修饰view。对于lable,它的父view是view,view需要拥有label,但是controller是不需要拥有label的。如果用strong修饰,在view销毁的情况下,label还仍然占有内存,因为controller还对它强引用;如果用wak修饰,在view销毁的时label的内存也同时被销毁,避免了僵尸指针出现。

用引用计数回答就是:因为Controller并不直接“拥有”控件,控件由它的父view“拥有”。使用weak关键字可以不增加控件引用计数,确保控件与父view有相同的生命周期。控件在被addSubview后,相当于控件引用计数+1;父view销毁后,所有的子view引用计数-1,则可以确保父view销毁时子view立即销毁。weak的控件在removeFromSuperview后也会立即销毁,而strong的控件不会,因为Controller还保有控件强引用。

总结归纳为:当控件的父view销毁时,如果你还想继续拥有这个控件,就用srtong;如果想保证控件和父view拥有相同的生命周期,就用weak。当然在大多数情况下用两个都是可以的。

使用weak的时候需要特别注意的是:先将控件添加到superview上之后再赋值给self,避免控件被过早释放。

17.iOS里什么是响应链,它是怎么工作的?

第一反应就是,响应链就是响应链啊,由一串UIResponder对象链接,收到响应事件时由上往下传递,直到能响应事件为止。

但其中却大有文章...

1.由一串UIResponder对象链接 ?

我们知道UIResponder类里有个属性:

@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

如果我们对响应链原理不清楚的话,会很容易的认为,这条链是由 nextResponder 指针连接起来的,在寻找响应者的时候是顺着这个指针找下去直到找到响应者为止的,但这是错误的认为。 举个例子: 现在我们有这样一个场景:

AppDelegate上的Window上有一个UIViewController *ViewController, 然后在ViewController.view 上按顺序添加viewA和viewB,viewB稍微覆盖viewA一部分用来测试, 给viewA,viewB 分别添加点击手势tapA 和 tapB,然后把viewB.userInteractionEnabled = NO,让viewB不能响应点击。

然后我们点击重复的那块区域,会发现viewA响应了tap手势,执行了tapA的事件。 我们知道viewB设置了viewB.userInteractionEnabled = NO,不响应tap手势是正常的,但怎么会透过viewB,viewA响应了手势?

我们知道nextResponder指针指向的规则:

  • UIView
  • 如果 view 是一个 view controller 的 root view,nextResponder 是这个 view controller.
  • 如果 view 不是 view controller 的 root view,nextResponder 则是这个 view 的 superview
  • UIViewController
  • 如果 view controller 的 view 是 window 的 root view, view controller 的 nextResponder 是这个 window
  • 如果 view controller 是被其他 view controller presented调起来的,那么 view controller 的 nextResponder 就是发起调起的那个 view controller
  • UIWindow
  • window 的 nextResponder 是 UIApplication 对象.
  • UIApplication
  • UIApplication 对象的 nextResponder 是 app delegate, 但是 app delegate 必须是 UIResponder 对象,并且不能使 view ,view controller 或 UIApplication 对象他本身.

那么上述情况下,viewB所在的响应者链应该是: viewB -> ViewController.view -> ViewController -> Window -> Application 这种情况下怎么也轮不到viewA去响应啊。

所以,当有事件需要响应时,nextResponder 并不是链接响应链的那根绳子,响应链的工作方式另有别的方式

2. 那么响应链是如何工作,正确找到应该响应该事件的响应者的?

UIKit使用基于视图的hit-testing来确定touch事件发生的位置。具体解释就是,UIKit将touch的位置和视图层级中的view的边界进行了比较,UIView的方法 hitTest:withEvent: 在视图层级中进行,寻找包含指定touch的最深子视图。这个视图成为touch事件的第一个响应者。

说白了就是,当有touch事件来的时候,会从最下面的视图开始执行 hitTest:withEvent: ,如果符合成为响应者的条件,就会继续遍历它的 subviews 继续执行 hitTest:withEvent: ,直到找到最合适的view成为响应者。

这里要注意几个点:

  • 符合响应者的条件包括
  • touch事件的位置在响应者区域内
  • 响应者 hidden 属性不为 YES
  • 响应者 透明度 不是 0
  • 响应者 userInteractionEnabled 不为 NO
  • 遍历 subview 时,是从上往下顺序遍历的,即 view.subviews 的 lastObject 到 firstObject 的顺序,找到合适的响应者view,即停止遍历.

所以再回看上面的例子,当我们点击中间的重复区域时,流程其实是这样:

  • AppDelegate 的 window 收到事件,并开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
  • window 上只有 viewcontroller.view ,所以viewcontroller.view 开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
  • viewcontroller.view 有两个子view, viewA 和 viewB ,但是viewB 在 viewA 上边,所以先 viewB 执行 hitTest:withEvent: ,结果发现viewB 不符合要求,因为viewB 的 userInteractionEnabled 为 NO.
  • 接下来 viewA 执行 hitTest:withEvent: ,发现符合条件,并且viewA 也没有子view可去遍历,于是返回viewA.
  • viewA成了最终事件的响应者.

这样就完美解释了,最开始例子的响应状况.

那么如果 viewB 的 userInteractionEnabled 属性为YES的话,是怎么样的呢?

如果 viewB 的 userInteractionEnabled 属性为YES,上面流程的第三部就会发现viewB是符合要求的,而直接返回viewB作为最终响应者,中断子view的遍历,viewA都不会被遍历到了.

这就是响应链相关的点,如果有什么不对的请留言提示,然后有什么别的需要补充的我会及时补充~

18.什么是iOS的动态绑定 ?

—在运行时确定要调用的方法

动态绑定将调用方法的确定也推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,您的代码每次执行都可以得到不同的结果。运行时因子负责确定消息的接收者和被调用的方法。运行时的消息分发机制为动态绑定提供支持。当您向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的isa指针定位对象的类,并以此为起点确定被调用的方法,方法和消息是动态绑定的。而且,您不必在Objective-C 代码中做任何工作,就可以自动获取动态绑定的好处。您在每次发送消息时,

特别是当消息的接收者是动态类型已经确定的对象时,动态绑定就会例行而透明地发生。

19.iOS单元测试框架有哪些?

OCUnit 是 OC 官方测试框架, 现在被 XCTest 所取代。 XCTest 是与 Foundation 框架平行的测试框架。 GHUnit 是第三方的测试框架。github地址OCMock都是第三方的测试框架。

20.iOS ARC全解?

考查点

我记得在刚接触iOS的时候对这个ARC和MRC就讨论颇深,认为ARC是对程序员的一种福利,让我们节省了大量的代码,那么ARC是什么呢?

ARC 是苹果在 WWDC 2011 提出来的技术,因此很多新入行的同学可能对此技术细节并不熟悉。但是,虽然 ARC 极大地简化了我们的内存管理工作,但是引用计数这种内存管理方案如果不被理解,那么就无法处理好那些棘手的循环引用问题。所以,这道面试题其实是考查同学对于 iOS 程序内存管理的理解深度。 答案

自动的引用计数(Automatic Reference Count 简称 ARC),是苹果在 WWDC 2011 年大会上提出的用于内存管理的技术。

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt) 等语言也提供了基于引用计数的内存管理方式。

引用计数这种内存管理方式虽然简单,但是手工写大量的操作引用计数的代码不但繁琐,而且容易被遗漏。于是苹果在 2011 年引入了 ARC。ARC 顾名思义,是自动帮我们填写引用计数代码的一项功能。

ARC 的想法来源于苹果在早期设计 Xcode 的 Analyzer 的时候,发现编译器在编译时可以帮助大家发现很多内存管理中的问题。后来苹果就想,能不能干脆编译器在编译的时候,把内存管理的代码都自动补上,带着这种想法,苹果修改了一些内存管理代码的书写方式(例如引入了 @autoreleasepool 关键字)后,在 Xcode 中实现了这个想法。

ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:

编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。

相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1 的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

但是也有人认为,ARC 也附带有运行期的一些机制来使 ARC 能够更好的工作,他们主要是指 weak 关键字。weak 变量能够在引用计数为 0 时被自动设置成 nil,显然是有运行时逻辑在工作的。我通常并没有把这个算在 ARC 的概念当中,当然,这更多是一个概念或定义上的分歧,因为除开 weak 逻辑之外,ARC 核心的代码都是在编译期填充的。

21.iOS内存的使用和优化的注意事项

重用问题:

如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews

设置正确的reuseIdentifier,充分重用;

尽量把views设置为不透明:

当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;

不要使用太复杂的XIB/Storyboard:

载入时就会将XIB/storyboard需要的所有资源,

包括图片全部载入内存,即使未来很久才会使用。

那些相比纯代码写的延迟加载,性能及内存就差了很多;

选择正确的数据结构:

学会选择对业务场景最合适的数组结构是写出高效代码的基础。

比如,数组: 有序的一组值。

使用索引来查询很快,使用值查询很慢,插入/删除很慢。

字典: 存储键值对,用键来查找比较快。

集合: 无序的一组值,用值来查找很快,插入/删除很快。

gzip/zip压缩:

当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。

延迟加载:

对于不应该使用的数据,使用延迟加载方式。

对于不需要马上显示的视图,使用延迟加载方式。

比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。

数据缓存:

对于cell的行高要缓存起来,使得reload数据时,效率也极高。

而对于那些网络数据,不需要每次都请求的,应该缓存起来,

可以写入数据库,也可以通过plist文件存储。

处理内存警告:

一般在基类统一处理内存警告,将相关不用资源立即释放掉

重用大开销对象:

一些objects的初始化很慢,

比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。

通常是作为属性存储起来,防止反复创建。

避免反复处理数据:

许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。

在服务器端和客户端使用相同的数据结构很重要;

使用Autorelease Pool:

在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;

22.什么是iOS的目标-动作机制 ?

目标是动作消息的接收者。一个控件,或者更为常见的是它的单元,以插座变量(参见"插座变量"部分)

的形式保有其动作消息的目标。

动作是控件发送给目标的消息,或者从目标的角度看,它是目标为了响应动作而实现的方法。

程序需要某些机制来进行事件和指令的翻译。这个机制就是目标-动作机制。

23.iOS 事件传递的完整过程?

先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。

调用最合适控件的touches….方法

如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者
接着就会调用上一个响应者的touches….方法

如何判断上一个响应者:
如果当前这个view是控制器的view,那么控制器就是上一个响应者
如果当前这个view不是控制器的view,那么父控件就是上一个响应者

24.什么是iOS的响应者链?

  • 响应者链条:是由多个响应者对象连接起来的链条
  • 作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
  • 响应者对象:能处理事件的对象



25.iOS UIView的Touch事件有哪几种触摸事件?

处理事件的方法

UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件

  //一根或者多根手指开始触摸view
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
//一根或者多根手指在view上移动
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
//一根或者多根手指离开view
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

26.iOS开发:Objective-C中通知与协议的区别?

what is difference between NSNotification and protocol? (通知和协议的不同之处?)

我想大家都知道这个东西怎么用,但是更深层次的思考可能就比较少了吧,众所周知就是代理是一对一的,但是通知是可以多对多的.但是为什么是这个样子,有没有更深的思考过这个问题?

今天看了下网上的几个视频教程,KVO、KVC、谓词、通知,算是开发中的高级点的东西了。通知和协议都是类似于回调一样,于是就在思考通知和协议到底有什么不同,或者说什么时候该用通知,什么时候该用协议。

下面是网上摘抄的一段解释:

协议有控制链(has-a)的关系,通知没有。首先我一开始也不太明白,什么叫控制链(专业术语了~)。但是简单分析下通知和代理的行为模式,我们大致可以有自己的理解简单来说,通知的话,它可以一对多,一条消息可以发送给多个消息接受者。代理按我们的理解,到不是直接说不能一对多,比如我们知道的明星经济代理人,很多时候一个经济人负责好几个明星的事务。只是对于不同明星间,代理的事物对象都是不一样的,一一对应,不可能说明天要处理A明星要一个发布会,代理人发出处理发布会的消息后,别称B的发布会了。但是通知就不一样,他只关心发出通知,而不关心多少接收到感兴趣要处理。因此控制链(has-a从英语单词大致可以看出,单一拥有和可控制的对应关系。

1.通知:

通知需要有一个通知中心:NSNotificationCenter,自定义通知的话需要给一个名字,然后监听。

优点:通知的发送者和接受者都不需要知道对方。可以指定接收通知的具体方法。通知名可以是任何字符串。

缺点:较键值观察(KVO)需要多点代码,在删掉前必须移除监听者。

2.协议

通过setDelegate来设置代理对象,最典型的例子是常用的TableView.

优点:支持它的类有详尽和具体信息。

缺点:该类必须支持委托。某一时间只能有一个委托连接到某一对象。

相信看到这些东西,认真思考一下,就可以知道在那种情况下使用通知,在那种情况下使用代理了吧.

27.写一个NSString类的实现

 + (id)initWithCString:(c*****t char *)nullTerminatedCString  encoding:(NSStringEncoding)encoding;** 

+ (id) stringWithCString: (c*****t char*)nullTerminatedCString

encoding: (NSStringEncoding)encoding

{

NSString *obj;

obj = [self allocWithZone: NSDefaultMallocZone()];

obj = [obj initWithCString: nullTerminatedCString encoding: encoding];

return AUTORELEASE(obj);

}

28.iOS 事件的产生和传递流程

发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理 touchesBegan… touchesMoved… touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递(不实现touches方法,系统会自动向上一个响应者传递),将事件交给上一个响应者进行处理
如果一个事件既想自己处理也想交给上一个响应者处理,那么自己实现touches方法,并且调用super的touches方法,[super touches、、、];

29.关键字volatile有什么含意?并给出三个不同的例子?

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到

这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

• 并行设备的硬件寄存器(如:状态寄存器)

• 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

• 多线程应用中被几个任务共享的变量

30.iOS hitTest方法&pointInside方法

hitTest方法
当事件传递给控件的时候,就会调用控件的这个方法,去寻找最合适的view
point:当前的触摸点,point这个点的坐标系就是方法调用者

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
pointInside方法
作用:判断当前这个点在不在方法调用者(控件)上

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent:的实现原理

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{

// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;

// 2. 判断点在不在当前控件
if ([self pointInside:point withEvent:event] == NO) return nil;

// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;

for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];

// 把当前控件上的坐标系转换成子控件上的坐标系
CGPoint childP = [self convertPoint:point toView:childView];

UIView *fitView = [childView hitTest:childP withEvent:event];


if (fitView) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}


作者:iOS鑫
链接:https://www.jianshu.com/p/2cc5d8b4e8d3








收起阅读 »

iOS面试题快来来来(内存方向)

1.形成tableView卡顿的缘由有哪些?1.最经常使用的就是cell的重用, 注册重用标识符若是不重用cell时,每当一个cell显示到屏幕上时,就会从新建立一个新的cellhtml若是有不少数据的时候,就会堆积不少cell。ios若是重用cell,为ce...
继续阅读 »

1.形成tableView卡顿的缘由有哪些?

  • 1.最经常使用的就是cell的重用, 注册重用标识符

    若是不重用cell时,每当一个cell显示到屏幕上时,就会从新建立一个新的cellhtml

    若是有不少数据的时候,就会堆积不少cell。ios

    若是重用cell,为cell建立一个ID,每当须要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,若是没有再从新建立cellc++

  • 2.避免cell的从新布局

    cell的布局填充等操做 比较耗时,通常建立时就布局好面试

    如能够将cell单独放到一个自定义类,初始化时就布局好swift

  • 3.提早计算并缓存cell的属性及内容

    当咱们建立cell的数据源方法时,编译器并非先建立cell 再定cell的高度xcode

    而是先根据内容一次肯定每个cell的高度,高度肯定后,再建立要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提早估算高度告诉编译器,编译器知道高度后,紧接着就会建立cell,这时再调用高度的具体计算方法,这样能够方式浪费时间去计算显示之外的cell缓存

  • 4.减小cell中控件的数量

    尽可能使cell得布局大体相同,不一样风格的cell可使用不用的重用标识符,初始化时添加控件,网络

    不适用的能够先隐藏数据结构

  • 5.不要使用ClearColor,无背景色,透明度也不要设置为0

    渲染耗时比较长多线程

  • 6.使用局部更新

    若是只是更新某组的话,使用reloadSection进行局部更

  • 7.加载网络数据,下载图片,使用异步加载,并缓存

  • 8.少使用addView 给cell动态添加view

  • 9.按需加载cell,cell滚动很快时,只加载范围内的cell

  • 10.不要实现无用的代理方法,tableView只遵照两个协议

  • 11.缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这二者同时存在才会出现“窜动”的bug。因此个人建议是:只要是固定行高就写预估行高来减小行高调用次数提高性能。若是是动态行高就不要写预估方法了,用一个行高的缓存字典来减小代码的调用次数便可

  • 12.不要作多余的绘制工做。在实现drawRect:的时候,它的rect参数就是须要绘制的区域,这个区域以外的不须要进行绘制。例如上例中,就能够用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否须要绘制image和text,而后再调用绘制方法。

  • 13.预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,而后再绘制到屏幕;

  • 14.使用正确的数据结构来存储数据。

2.如何提高 tableview 的流畅度?

  • 本质上是下降 CPU、GPU 的工做,从这两个大的方面去提高性能。

    CPU:对象的建立和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制

    GPU:纹理的渲染

  • 卡顿优化在 CPU 层面

    尽可能用轻量级的对象,好比用不到事件处理的地方,能够考虑使用 CALayer 取代 UIView

    不要频繁地调用 UIView 的相关属性,好比 frame、bounds、transform 等属性,尽可能减小没必要要的修改

    尽可能提早计算好布局,在有须要时一次性调整对应的属性,不要屡次修改属性

    Autolayout 会比直接设置 frame 消耗更多的 CPU 资源

    图片的 size 最好恰好跟 UIImageView 的 size 保持一致

    控制一下线程的最大并发数量

    尽可能把耗时的操做放到子线程

    文本处理(尺寸计算、绘制)

    图片处理(解码、绘制)

  • 卡顿优化在 GPU层面

    尽可能避免短期内大量图片的显示,尽量将多张图片合成一张进行显示

    GPU能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用 CPU 资源进行处理,因此纹理尽可能不要超过这个尺寸

    尽可能减小视图数量和层次

    减小透明的视图(alpha<1),不透明的就设置 opaque 为 YES

    尽可能避免出现离屏渲染

  • iOS 保持界面流畅的技巧

    1.预排版,提早计算

    在接收到服务端返回的数据后,尽可能将 CoreText 排版的结果、单个控件的高度、cell 总体的高度提早计算好,将其存储在模型的属性中。须要使用时,直接从模型中往外取,避免了计算的过程。

    尽可能少用 UILabel,可使用 CALayer 。避免使用 AutoLayout 的自动布局技术,采起纯代码的方式

    2.预渲染,提早绘制

    例如圆形的图标能够提早在,在接收到网络返回数据时,在后台线程进行处理,直接存储在模型数据里,回到主线程后直接调用就能够了

    避免使用 CALayer 的 Border、corner、shadow、mask 等技术,这些都会触发离屏渲染。

    3.异步绘制

    4.全局并发线程

    5.高效的图片异步加载

3.APP启动时间应从哪些方面优化?

App启动时间能够经过xcode提供的工具来度量,在Xcode的Product->Scheme-->Edit Scheme->Run->Auguments中,将环境变量DYLD_PRINT_STATISTICS设为YES,优化需如下方面入手

  • dylib loading time

    核心思想是减小dylibs的引用

    合并现有的dylibs(最好是6个之内)

    使用静态库

  • rebase/binding time

    核心思想是减小DATA块内的指针

    减小Object C元数据量,减小Objc类数量,减小实例变量和函数(与面向对象设计思想冲突)

    减小c++虚函数

    多使用Swift结构体(推荐使用swift)

  • ObjC setup time

    核心思想同上,这部份内容基本上在上一阶段优化事后就不会太过耗时

    initializer time

  • 使用initialize替代load方法

    减小使用c/c++的attribute((constructor));推荐使用dispatch_once() pthread_once() std:once()等方法

    推荐使用swift

    不要在初始化中调用dlopen()方法,由于加载过程是单线程,无锁,若是调用dlopen则会变成多线程,会开启锁的消耗,同时有可能死锁

    不要在初始化中建立线程

4.如何下降APP包的大小

下降包大小须要从两方面着手

  • 可执行文件

    编译器优化:Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES,去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO, Other C Flags 添加 -fno-exceptions 利用 AppCode 检测未使用的代码:菜单栏 -> Code -> Inspect Code

    编写LLVM插件检测出重复代码、未被调用的代码

  • 资源(图片、音频、视频 等)

    优化的方式能够对资源进行无损的压缩

    去除没有用到的资源

5.如何检测离屏渲染与优化

  • 检测,经过勾选Xcode的Debug->View Debugging-->Rendering->Run->Color Offscreen-Rendered Yellow项。
  • 优化,如阴影,在绘制时添加阴影的路径

6.怎么检测图层混合

一、模拟器debug中color blended layers红色区域表示图层发生了混合

二、Instrument-选中Core Animation-勾选Color Blended Layers

避免图层混合:

  • 确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明
  • 如无特殊须要,不要设置低于1的alpha值
  • 确保UIImage没有alpha通道

UILabel图层混合解决方法:

iOS8之后设置背景色为非透明色而且设置label.layer.masksToBounds=YES让label只会渲染她的实际size区域,就能解决UILabel的图层混合问题

iOS8 以前只要设置背景色为非透明的就行

为何设置了背景色可是在iOS8上仍然出现了图层混合呢?

UILabel在iOS8先后的变化,在iOS8之前,UILabel使用的是CALayer做为底图层,而在iOS8开始,UILabel的底图层变成了_UILabelLayer,绘制文本也有所改变。在背景色的四周多了一圈透明的边,而这一圈透明的边明显超出了图层的矩形区域,设置图层的masksToBounds为YES时,图层将会沿着Bounds进行裁剪 图层混合问题解决了

7.平常如何检查内存泄露?

  • 目前我知道的方式有如下几种

    Memory Leaks

    Alloctions

    Analyse

    Debug Memory Graph

    MLeaksFinder

  • 泄露的内存主要有如下两种:

    Laek Memory 这种是忘记 Release 操做所泄露的内存。

    Abandon Memory 这种是循环引用,没法释放掉的内存。



作者:iOS鑫
链接:https://www.jianshu.com/p/f9da4407c04b

收起阅读 »

UIScrollView属性及其代理方法

一、UIScrollView是什么?1、UIScrollView是滚动的view,UIView本身不能滚动,子类UIScrollview拓展了滚动方面的功能。2、UIScrollView是所有滚动视图的基类。以后的UITableView,UITextView等...
继续阅读 »

一、UIScrollView是什么?

1、UIScrollView是滚动的view,UIView本身不能滚动,子类UIScrollview拓展了滚动方面的功能。
2、UIScrollView是所有滚动视图的基类。以后的UITableView,UITextView等视图都是继承于该类。
使用场景:显示不下(单张大图);内容太多(图文混排);滚动头条(图片);相册等

二、UIScrollView使用

1、UIScrollview主要专长于两个方面:

      a、滚动:contentSize大于frame.size的时候,能够滚动。
b、 缩放:自带缩放,可以指定缩放倍数。
2、UIScrollView滚动相关属性contentSize

 //定义内容区域大小,决定是否能够滑动
contentOffset //视图左上角距离坐标原点的偏移量
scrollsToTop //滑动到顶部(点状态条的时候)
pagingEnabled //是否整屏翻动
bounces //边界是否回弹
scrollEnabled //是否能够滚动
showsHorizontalScrollIndicator //控制是否显示水平方向的滚动条
showVerticalScrollIndicator //控制是否显示垂直方向的滚动条
alwaysBounceVertical //控制垂直方向遇到边框是否反弹
alwaysBounceHorizontal //控制水平方向遇到边框是否反弹

3、UIScrollView缩放相关属性

minimumZoomScale  //  缩小的最小比例
maximumZoomScale //放大的最大比例
zoomScale //设置变化比例
zooming //判断是否正在进行缩放反弹
bouncesZoom //控制缩放的时候是否会反弹
要实现缩放,还需要实现delegate,指定缩放的视图是谁。

4.UIScrollView滚动实例应用
- (void)scrollView{
// 创建滚动视图,但我们现实的屏幕超过一屏时,就需要滚动视图
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
scrollView.backgroundColor = [UIColor yellowColor];
scrollView.tag = 1000;
// 设置滚动区域
scrollView.contentSize = CGSizeMake(4 * CGRectGetWidth(self.view.frame), self.view.frame.size.height);
[self.view addSubview:scrollView];
// 添加子视图
for (int i = 0; i < 4; i ++) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) * i, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))];
label.text = [NSString stringWithFormat:@"这是%d个视图",i];
label.font = [UIFont systemFontOfSize:30];
[scrollView addSubview:label];
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg",i]]]; // (有四张片分别取名0.jpg,1.jpg,2.jpg.3.jpg)
[imageView setFrame:self.view.frame];
[label addSubview:imageView];

// label.backgroundColor = [UIColor colorWithRed:arc4random()%256/255.0 green:
// arc4random()%256/255.0 blue:arc4random()%256/255.0 alpha:1];

}
// 设置分页效果 (默认值为NO)
scrollView.pagingEnabled = YES;
// 设置滚动条是否显示(默认值是YES)
scrollView.showsHorizontalScrollIndicator = YES;
// 设置边界是否有反弹效果(默认值是YES)
scrollView.bounces = YES;
// 设置滚动条的样式
scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
/*
indicatorStyle(枚举值)
UIScrollViewIndicatorStyleDefault, //白色
UIScrollViewIndicatorStyleBlack, // 黑色
*/


// 设置scrollView的代理
scrollView.delegate = self; // (记得导入协议代理 <UIScrollViewAccessibilityDelegate>)
}

5、UIScrollView滚动代理方法
// 滚动就会触发
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{ NSLog(@"只有scrollview是跟滚动状态就会调用此方法");
}
//开始拖拽时触发
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
NSLog(@"开始拖拽");

}
// 结束拖拽时触发
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
NSLog(@"结束拖拽");
}
// 开始减速时触发
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
NSLog(@"开始减速");

}
// 结束减速时触发(停止)
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
NSLog(@"结束减速(停止)");
}

6、UIScrollView缩放实例应用
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
// 初始化一个scrollView
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
scrollView.backgroundColor = [UIColor greenColor];
scrollView.delegate = self;


// 设置缩放比率
// 设置可缩小道德最小比例
scrollView.minimumZoomScale = 0.5;
// 设置可放大的最大比例
scrollView.maximumZoomScale = 2.0;
[self.view addSubview:scrollView];

// 使得要添加的图片宽高成比例
UIImage *myImage = [UIImage imageNamed:@"7.jpg"];
// 得到原始宽高
float imageWidth = myImage.size.width;
float imageHeight = myImage.size.height;
// 这里我们规定imageView的宽为200,根据此宽度得到等比例的高度
float imageViewWidth = 200;
float imageViewHeight = 200 *imageHeight/imageWidth;
// 初始化一个UIimageview
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imageViewWidth, imageViewHeight)];
// 为imageView设置图片
imageView.image = myImage;
// 让imageView居中
imageView.center = self.view.center;
imageView.tag = 1000;
[scrollView addSubview:imageView];

}

7、UIScrollView缩放有关的代理

#pragma mark -- 滚动视图与缩放有关的代理方法
//指定scrollview的某一个子视图为可缩放视图,前提条件是次视图已经添加到scrollview上面
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView{
UIView *imageView = (UIView*)[scrollView viewWithTag:1000];
return imageView;
}

// 开始缩放的代理方法 第二个参数view:这个参数使我们将要缩放的视图(这里就是imageView)
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view{
NSLog(@"%@",view);

}

// 正在缩放的代理方法 只要在缩放就执行该方法,所以此方法会在缩放过程中多次调用
- (void)scrollViewDidZoom:(UIScrollView *)scrollView{
// 在缩放过程中为了使得该视图一直在屏幕中间,所以我们需要在他缩放的过程中一直调整他的center
// 得到scrollview的子视图
UIImageView *imageView = (UIImageView *)[scrollView viewWithTag:1000];
// 打印imageView的frame,分析为什么他的位置会改变
// NSLog(@"frame -- %@",NSStringFromCGRect(imageView.frame));

// 设置imageview的center,是他的位置一直在屏幕中央
imageView.center = scrollView.center;
// 打印contentSize 分析为什么缩放之后会滑动
NSLog(@"contentSize %@",NSStringFromCGSize(scrollView.contentSize));
}


// 缩放结束所执行的代理方法
/**
* @ view 当前正在缩放的视图
* @ scale 当前正在缩放视图的缩放比例
*/

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale{
// 缩放完成之后恢复原大小,这里运用到2D仿射变换函数中与捏合有关的函数
view.transform =CGAffineTransformMakeScale(1, 1);


}



作者:小猪也浪漫
链接:https://www.jianshu.com/p/62918c39b95e

收起阅读 »