注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS运行unity导出工程权限问题

最近公司新开发了一款游戏,分别导出了安卓工程和xcode工程,在运行的过程中,出现了权限问题提示:在这边提示我,有两个.sh文件是被禁止的,没有运行的权限。其实这个问题很好解决,1、首先找到你xcode所在工程的文件夹,找到这两个.sh文件2、打开你的命令行控...
继续阅读 »

最近公司新开发了一款游戏,分别导出了安卓工程和xcode工程,在运行的过程中,出现了权限问题提示:


在这边提示我,有两个.sh文件是被禁止的,没有运行的权限。

其实这个问题很好解决,

1、首先找到你xcode所在工程的文件夹,找到这两个.sh文件


2、打开你的命令行控制器,输入chmod 777 MapFileParser.sh(其实这个·sh文件,直接拖入到命令行就行了)


3、运行完命令之后,再次编译工程即可

收起阅读 »

Jetpack之Navigation(2)

Jetpack之Navigation(1)2.原理 初始化过程 NavHostFragment生命周期方法 1.create—NavHostFragment的创建 在NavHostFragment.create方法 初始化Bundle,并且将graphRes...
继续阅读 »

Jetpack之Navigation(1)


2.原理


初始化过程 NavHostFragment生命周期方法


1.create—NavHostFragment的创建


在NavHostFragment.create方法



  1. 初始化Bundle,并且将graphResId,startDestinationArgs存储在Bundle中。

  2. new NavHostFragment()返回NavHostFragment实例。


    //NavHostFragment.java
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId,
@Nullable Bundle startDestinationArgs)
{
Bundle b = null;
if (graphResId != 0) {
b = new Bundle();
b.putInt(KEY_GRAPH_ID, graphResId);
}
if (startDestinationArgs != null) {
if (b == null) {
b = new Bundle();
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
}

final NavHostFragment result = new NavHostFragment();
if (b != null) {
result.setArguments(b);
}
return result;
}
复制代码

2.onInflate—XML文件的解析


主要是解析布局文件的两个属性:defaultNavHost和navGraph,并且初始化全局变量。


NavHostFragment.onInflate方法 当Fragment以XML的方式静态加载时,最先会调用onInflate的方法(调用时机:Fragment所关联的Activity在执行setContentView时)。


    //NavHostFragment.java
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState)
{
super.onInflate(context, attrs, savedInstanceState);

final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId; //navigation的图布局
}
navHost.recycle();

final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true; //是否监听物理返回键
}
a.recycle();
}
复制代码

3.onCreateNavController—创建Navigator


在实现导航的时候,我们需要根据navigation配置文件生成NavGraph类,然后在根据每个不同的actionid,找到对应的NavDestination就可以实现页面导航跳转了。


创建Navigator


Navigator类的作用是:能够实例化对应的NavDestination,并且能够实现导航功能,拥有自己的回退栈。


    @CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));

//创建Navigator并绑定到NavigatorProvider中。
//mNavigatorProvider是NavController中的全局变量,内部通过HashMap键值对的形式保存Navigator类。
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
复制代码


  1. 其中mNavigatorProvider是NavController中的全局变量,内部通过HashMap键值对的形式保存Navigator类

  2. createFragmentNavigator方法,构建了FragmentNavigator对象,其中抽象类Navigator还有个重要的实现类ActivityNavigator和NavGraphNavigator。这个两个类的对象在NavController的构造方法中被添加。


//NavController.java
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
复制代码

4.onCreate—导航初始化


无论是XML实现还是代码实现,都会执行Fragment的onCreate方法。NavController在这里被创建,并且NavHostFragment中有一个NavController对象。


   @CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();

//1.初始化NavController,NavController为导航的控制类,核心类
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
mNavController.setViewModelStore(getViewModelStore());
onCreateNavController(mNavController);

Bundle navState = null;
//2.开始恢复状态
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
}

if (navState != null) {
mNavController.restoreState(navState);
}

//3.设置导航图信息
if (mGraphId != 0) {
mNavController.setGraph(mGraphId);
} else {
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
super.onCreate(savedInstanceState);
}
复制代码

5.onCreateView


NavHostFragment的视图就只有一个FragmentContainerView 继承 FrameLayout


    //NavHostFragment.java
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());

//这行主要用于以代码方式添加fragment
containerView.setId(getContainerId());
return containerView;
}
复制代码

6.onViewCreated


    @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
//把mNavController记录在view的tag中
Navigation.setViewNavController(view, mNavController);

if (view.getParent() != null) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
//把mNavController记录在view的tag中
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}
复制代码

获取NavController


1.获取NavController


NavHostFragment.findNavController(fragment)
复制代码

    //NavHostFragment.java
@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
Fragment findFragment = fragment;
while (findFragment != null) {
if (findFragment instanceof NavHostFragment) {
return ((NavHostFragment) findFragment).getNavController();
}
Fragment primaryNavFragment = findFragment.getParentFragmentManager()
.getPrimaryNavigationFragment();
if (primaryNavFragment instanceof NavHostFragment) {
return ((NavHostFragment) primaryNavFragment).getNavController();
}
findFragment = findFragment.getParentFragment();
}

View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}

Dialog dialog = fragment instanceof DialogFragment
? ((DialogFragment) fragment).getDialog()
: null;
if (dialog != null && dialog.getWindow() != null) {
return Navigation.findNavController(dialog.getWindow().getDecorView());
}

throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}
复制代码

2.Navigation中findNavController


3.findViewNavController


通过view.tag查找NavController。内部调用了getViewNavController方法。


4.getViewNavController


getViewNavController方法 通过获取view的Tag,获取NavController对象,这里的tag ID和setViewNavController都是nav_controller_view_tag。


    //Navigation.java
@NonNull
public static NavController findNavController(@NonNull View view) {
//3.
NavController navController = findViewNavController(view);
if (navController == null) {
throw new IllegalStateException("View " + view + " does not have a NavController set");
}
return navController;
}


@Nullable
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
if (controller != null) {
return controller;
}
ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
return null;
}

@Nullable
private static NavController getViewNavController(@NonNull View view) {
//4.这里的tag ID和setViewNavController都是nav_controller_view_tag。
Object tag = view.getTag(R.id.nav_controller_view_tag);
NavController controller = null;
if (tag instanceof WeakReference) {
controller = ((WeakReference) tag).get();
} else if (tag instanceof NavController) {
controller = (NavController) tag;
}
return controller;
}
复制代码

导航navigate


navigate


在构建和获取到NavController对象以及NavGraph之后。下面是使用它来实现真正的导航了。下面从navigate开始分析。在navigate方法内部会查询到NavDestination,然后根据不同的Navigator实现页面导航。


public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
//2.根据id,获取对应的NavAction
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
//3.通过NavAction获取目的地id
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}

if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
combinedArgs.putAll(args);
}

if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}

if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used"
+ " in conjunction with a valid navOptions.popUpTo");
}
//4.利用目的地ID属性,通过findDestination方法,找到准备导航的目的地
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
if (navAction != null) {
throw new IllegalArgumentException("Navigation destination " + dest
+ " referenced from action "
+ NavDestination.getDisplayName(mContext, resId)
+ " cannot be found from the current destination " + currentNode);
} else {
throw new IllegalArgumentException("Navigation action/destination " + dest
+ " cannot be found from the current destination " + currentNode);
}
}
//5.开始导航
navigate(node, combinedArgs, navOptions, navigatorExtras);
}


private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
boolean launchSingleTop = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
...
}
复制代码

findDestination


如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。


  NavDestination findDestination(@IdRes int destinationId) {
if (mGraph == null) {
return null;
}
if (mGraph.getId() == destinationId) {
return mGraph;
}
//1.如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
NavGraph currentGraph = currentNode instanceof NavGraph
? (NavGraph) currentNode
: currentNode.getParent();
return currentGraph.findNode(destinationId);
}
复制代码

FragmentNavigator的实现


通过以上的分析,又来到了Navigator 的子类FragmentNavigator类。下面来看看FragmentNavigator.navigate的方法。
(1)调用instantiateFragment,通过反射机制构建Fragment实例

(2)处理进出场等动画逻辑

(3)最终调用FragmentManager来处理导航逻辑。


@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
//通过反射机制构建Fragment实例
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();

//处理动画逻辑
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}

//FragmentManager来处理导航逻辑
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);

final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;

boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
if (mBackStack.size() > 1) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
复制代码

ActivityNavigator


ActivityNavigator最终也是调用了startActivity方法,请自己阅读源码。


    @Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
....
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
ActivityOptionsCompat activityOptions = extras.getActivityOptions();
if (activityOptions != null) {
ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
} else {
mContext.startActivity(intent);
}
} else {
mContext.startActivity(intent);
}
...
}
复制代码

3.总结



  1. NavHostFragment 作为导航载体,在Activity的layout文件里被引用(或者在代码中动态),并且持有导航控制类NavController引用。

  2. NavController 将导航任务委托给Navigator类,Navigator类有两个重要的子类FragmentNavigator和ActivityNavigator子类。NavController类持有NavInflater类引用。

  3. NavInflater 负责解析Navgation文件,负责构建NavGraph导航图。

  4. NavDestination 存有各个目的地信息,在FragmentNavigator和ActivityNavigator内部分别对应一个Destination类,该类继承NavDestination。

  5. 在页面导航时,fragment的操作还是交由FragmentManager在操作,activity交由startActivity执行。


image-20210421174444170


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

收起阅读 »

Jetpack之Navigation(1)

1.基本使用引入库 def nav_version = "2.3.2" // Java language implementation implementation "androidx.navigation:navigation-fragment:$nav_...
继续阅读 »

1.基本使用

引入库



def nav_version = "2.3.2"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
复制代码

Activity布局


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

<!--
app:defaultNavHost="true"
拦截系统back键
-->

<androidx.fragment.app.FragmentContainerView
android:id="@+id/my_nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="9"
android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main"/>


</LinearLayout>
复制代码

在res/navigation目录下nav_graph_main.xml


<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph_main.xml"
app:startDestination="@id/page1Fragment">

<fragment
android:id="@+id/page1Fragment"
android:name="com.example.lsn4_navigationdemo.MainPage1Fragment"
android:label="fragment_page1"
tools:layout="@layout/fragment_main_page1">


<!--action:程序中使用id跳到destination对应的类-->
<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment" />

</fragment>

<fragment
android:id="@+id/page2Fragment"
android:name="com.example.lsn4_navigationdemo.MainPage2Fragment"
android:label="fragment_page2"
tools:layout="@layout/fragment_main_page2">

<action
android:id="@+id/action_page1"
app:destination="@id/page1Fragment" />

<action
android:id="@+id/action_page3"
app:destination="@id/page3Fragment" />

</fragment>

<!-- <navigation-->
<!-- android:id="@+id/nav_graph_page3"-->
<!-- app:startDestination="@id/page3Fragment">-->
<fragment
android:id="@+id/page3Fragment"
android:name="com.example.lsn4_navigationdemo.MainPage3Fragment"
android:label="fragment_page3"
tools:layout="@layout/fragment_main_page3"
>

<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment"/>

</fragment>


</navigation>
复制代码

Fragment中调用跳转


//方式一:直接跳入指定的fragment
Navigation.findNavController(view).navigate(R.id.page2Fragment);

//方式二:通过action
Navigation.findNavController(view).navigate(R.id.action_page2); //跳入page2
复制代码

Activity中调用跳转


//获取controller方式一
NavController controller=Navigation.findNavController(this,R.id.my_nav_host_fragment);

//获取controller方式二
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.my_nav_host_fragment);
NavController controller = navHostFragment.getNavController();

//跳转
controller.navigate(R.id.page2Fragment);
复制代码

Activity绑定navigation


除了在Activity的布局中指定navigation布局资源以外,还可以通过java代码进行设置


public class MainActivity extends AppCompatActivity {


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

//方式一
val finalHost = NavHostFragment.create(R.navigation.nav_graph_main)
supportFragmentManager.beginTransaction()
.replace(R.id.ll_fragment_navigation, finalHost)
.setPrimaryNavigationFragment(finalHost)
.commit();
}

//方式二
@Override
public boolean onSupportNavigateUp() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.my_nav_host_fragment);
return NavHostFragment.findNavController(fragment).navigateUp();
}
}
复制代码

底部导航


引入底部导航控件


implementation 'com.google.android.material:material:1.1.0'
复制代码

布局


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<!--
app:defaultNavHost="true"
拦截系统back键
-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/my_nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="9"
android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main"/>

<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="wrap_content"
app:itemTextColor="#ff0000"
app:menu="@menu/menu"/>

</LinearLayout>
复制代码

menu


<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/page1Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="1"/>
<item
android:id="@+id/page2Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="2"/>
<item
android:id="@+id/page3Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="3"/>

</menu>
复制代码

Activity绑定导航


public class MainActivity extends AppCompatActivity {

BottomNavigationView bottomNavigationView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.action_page1);
bottomNavigationView=findViewById(R.id.nav_view);

//获取controller方式一
//NavController controller=Navigation.findNavController(this,R.id.my_nav_host_fragment);

//获取controller方式二
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.my_nav_host_fragment);
NavController controller = navHostFragment.getNavController();

//绑定导航
NavigationUI.setupWithNavController(bottomNavigationView,controller);

}

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

收起阅读 »

drawable用Kotlin应该这样写

前言 通常我们在res/drawable下面自定义shape和selector来满足一些UI的设计,但是由于xml最终转换为drawable需要经过IO或反射创建,会有一些性能损耗,另外随着项目的增大和模块化等,很多通用的样式并不能快速复用,需要合理的项目资源...
继续阅读 »

前言


通常我们在res/drawable下面自定义shapeselector来满足一些UI的设计,但是由于xml最终转换为drawable需要经过IO或反射创建,会有一些性能损耗,另外随着项目的增大和模块化等,很多通用的样式并不能快速复用,需要合理的项目资源管理规范才能实施。那么通过代码直接创建这些drawable,可以在一定程度上降低这些副作用。本篇介绍用kotlin DSL简洁的语法特性来实现常见的drawable.


代码对应效果预览


shape_line
RECTANGLE
OVAL
LayerList
Selector

集成和使用



  1. 在项目级的build.gradle文件种添加仓库Jitpack:


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


  1. 添加依赖


dependencies {		
implementation 'com.github.forJrking:DrawableDsl:0.0.1’
}
复制代码


  1. 抛弃xml创建方式示例(其他参见demo)


// infix用法用于去掉括号更加简洁,详细后面说明
image src = shapeDrawable {
//指定shape样式
shape(ShapeBuilder.Shape.RECTANGLE)
//圆角,支持4个角单独设置
corner(20f)
//solid 颜色
solid("#ABE2E3")
//stroke 颜色,边框dp,虚线设置
stroke(android.R.color.white, 2f, 5f, 8f)
}
//按钮点击样式
btn.background = selectorDrawable {
//默认样式
normal = shapeDrawable {
corner(20f)
gradient(90, R.color.F97794, R.color.C623AA2)
}
//点击效果
pressed = shapeDrawable {
corner(20f)
solid("#84232323")
}
}
复制代码

实现思路


xml如何转换成drawable


xml变成drawable,通过android.graphics.drawable.DrawableInflater这个类来IO解析标签创建:


//标签创建
private Drawable inflateFromTag(@NonNull String name) {
switch (name) {
case "selector":
return new StateListDrawable();
case "level-list":
return new LevelListDrawable();
case "layer-list":
return new LayerDrawable();
....
case "color":
return new ColorDrawable();
case "shape":
return new GradientDrawable();
case "vector":
return new VectorDrawable();
...
}
}
//反射创建
private Drawable inflateFromClass(@NonNull String className) {
try {
Constructor<? extends Drawable> constructor;
synchronized (CONSTRUCTOR_MAP) {
constructor = CONSTRUCTOR_MAP.get(className);
if (constructor == null) {
final Class<? extends Drawable> clazz = mClassLoader.loadClass(className).asSubclass(Drawable.class);
constructor = clazz.getConstructor();
CONSTRUCTOR_MAP.put(className, constructor);
}
}
return constructor.newInstance();
} catch (NoSuchMethodException e) {
...
}
复制代码

代码实现


由于创建shape等需要设置各种属性来构建,比较符合build设计模式,那我们首先封装build模式的shapeBuilder,这样做虽然代码比起直接使用apply{}多但是可以让纯java项目用起来很舒服,其他实现请查看源码:


class ShapeBuilder : DrawableBuilder {
private var mRadius = 0f
private var mWidth = 0f
private var mHeight = 0f
...
private var mShape = GradientDrawable.RECTANGLE
private var mSolidColor = 0

/**分别设置四个角的圆角*/
fun corner(leftTop: Float,rightTop: Float,leftBottom: Float,rightBottom: Float): ShapeBuilder {
....
return this
}

fun solid(@ColorRes colorId: Int): ShapeBuilder {
mSolidColor = ContextCompat.getColor(context, colorId)
return this
}
// 省略其他参数设置方法 详细代码查看源码
override fun build(): Drawable {
val gradientDrawable = GradientDrawable()
gradientDrawable = GradientDrawable()
gradientDrawable.setColor(mSolidColor)
gradientDrawable.shape = mShape
....其他参数设置
return gradientDrawable
}
}
复制代码

把build模式转换为dsl


理论上所有的build模式都可以轻松转换为dsl写法:


inline fun shapeDrawable(builder: ShapeBuilder.() -> Unit): Drawable {
return ShapeBuilder().also(builder).build()
}
//使用方法
val drawable = shapeDrawable{
...
}
复制代码

备注:dsl用法参见juejin.cn/post/695318… 中dsl小节


infix函数来去括号


通过上面封装已经实现了dsl的写法,通常setBackground可以通过setter简化,但是我发现由于有些api设计还需要加括号,这样不太kotlin:


//容易阅读
iv1.background = shapeDrawable {
shape(ShapeBuilder.Shape.RECTANGLE)
solid("#ABE2E3")
}
//多了括号看起来不舒服
iv2.setImageDrawable(shapeDrawable {
solid("#84232323")
})
复制代码

怎么去掉括号呢?这里就要用到infix函数特点和规范:



  • Kotlin允许在不使用括号和点号的情况下调用函数

  • 必须只有一个参数

  • 必须是成员函数或扩展函数

  • 不支持可变参数和带默认值参数


/**为所有ImageView添加扩展infix函数 来去掉括号*/
infix fun ImageView.src(drawable: Drawable?) {
this.setImageDrawable(drawable)
}
//使用如下 舒服了
iv2 src shapeDrawable {
shape(ShapeBuilder.Shape.OVAL)
solid("#E3ABC2")
}
复制代码

当然了代码是用来阅读的。个人认为如果我们大量使用infix函数,阅读困难会大大增加,所以建议函数命名必须可以直击函数功能,而且函数功能简单且单一。


优缺点


优点:



  • 代码直接创建比起xml方式可以提升性能

  • dsl方式比起build模式和调用方法设置更加简洁符合kotlin风格

  • 通过合适的代码管理可以复用这些代码,比xml管理方便


缺点:



  • 没有as的预览功能,只有通过上机观测

  • api还没有覆盖所有drawable属性


后语


上面把的DrawableDsl基础用法介绍完了,欢迎大家使用,欢迎提Issues,记得给个star哦。Github链接:https://github.com/forJrking/DrawableDsl


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

Android各版本的行为变更

本文的主要内容来自官方,摘出了Android开发者应该关注的重点变更,目前已经更新到Android 11,新版本发布时会持续更新,如果对您有帮助请不吝点赞! 一、Android 5.0——21——Lollipop(棒棒糖) ART 限制获取AppTask ...
继续阅读 »

本文的主要内容来自官方,摘出了Android开发者应该关注的重点变更,目前已经更新到Android 11,新版本发布时会持续更新,如果对您有帮助请不吝点赞!



一、Android 5.0——21——Lollipop(棒棒糖)



  1. ART


  2. 限制获取AppTask


  3. WebView默认阻止第三方内容:setMixedContentMode()


  4. Material Design


  5. 提出JobScheduler



  6. 屏幕采集和屏幕共享


    Android 5.0 引入了新的 android.media.projection API,让您可以为应用添加屏幕采集和屏幕共享功能。例如,如果您想在视频会议应用中启用屏幕共享,便可使用此功能。

    新增的 createVirtualDisplay() 方法允许您的应用将主屏幕(默认显示)的内容采集到一个 Surface 对象中,然后您的应用便可将其发送至整个网络。该 API 只允许采集非安全屏幕内容,不允许采集系统音频。要开始采集屏幕,您的应用必须先使用通过 createScreenCaptureIntent() 方法获得的 Intent 启动屏幕采集对话框,请求用户授予权限。



二、Android 6.0——23——Marshmallow(棉花糖)



  1. 省电机制Doze引入

  2. 运行时权限

  3. 移除了对设备本地硬件标识符的编程访问权

  4. 指纹API


三、Android 7.0——24——Nougat(牛轧糖)



  1. FileProvider

  2. 低耗电模式进一步优化

  3. 多窗口支持

  4. 添加JIT,属于对AOT的一种补充机制


四、Android 8.0——26——Oreo(奥利奥)



  1. 当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。


  2. 应用无法使用其清单注册大部分隐式广播


  3. 应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException


  4. 后台应用时,降低了后台应用接收位置更新的频率


  5. 画中画模式


  6. 应用快捷菜单


  7. 音频自动闪避,失去焦点时自动调小音量



  8. 多显示器支持(移动Activity到另一个窗口会onConfigChange)


    在多窗口模式下,在任意时间点都只有用户最近一次互动的 Activity 处于活动状态。此 Activity 被视为最顶层的 Activity,而且是唯一处于 RESUMED 状态的 Activity。所有其他可见的 Activity 均处于 STARTED 而非 RESUMED 状态。不过,这些可见但并不处于 RESUMED 状态的 Activity 在系统中享有比不可见 Activity 更高的优先级。如果用户与其中一个可见的 Activity 互动,那么该 Activity 将进入 RESUMED 状态,而之前的最顶层 Activity 将进入 STARTED 状态。



五、Android 9.0——28——Pie



  1. 后台应用无法访问用户输入和一些传感器数据,如麦克风相机,加速器陀螺仪等,除非使用前台服务

  2. 通话记录权限从Phone组移动到CALL_LOG组,plus,一个组的权限有了一个,其他的的权限也就有了(申请会自动授权)

  3. 不允许使用非SDKAPI,也就是hide的一些SDK方法不许用反射和JNI调用

  4. 支持检测显示屏缺口,非功能区域DisplayCutout,比如检测挖孔屏

  5. 利用 Wi-Fi RTT 进行室内定位

  6. ImageDecoder类可取代BitmapFactory


六、Android 10——29——Q



  1. 屏幕边缘手势导航(各大厂商早就实现了)



  2. 为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储空间的分区访问权限(即分区存储)。此类应用只能访问外部存储空间上的应用专属目录,以及本应用所创建的特定类型的媒体文件。

    注意:如果您的应用在运行时请求与存储空间相关的权限,面向用户的对话框会表明您的应用正在请求对外部存储空间的广泛访问,即使已启用分区存储也是如此。


  • 后台应用不能启动一个新的Activity


  • 新增后台访问位置信息权限,无则不可以(ACCESS_BACKGROUND_LOCATION)


  • 应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号),普通应用基本是不可以的


  • 除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。



  • 七、Android 11——30——R



    1. 单次授权(权限只给这一次)


    2. 自动重置权限——几个月用户未与应用交互,将回收应用权限



    3. 支持瀑布屏API


      现有的 DisplayCutout.getSafeInset…() 方法现在会返回能够避开瀑布区域以及刘海的安全边衬区



    4. 新增前台服务类型属性,必须指明为camera和microphone才能访问麦克风,在后台运行时启动的前台服务无法访问麦克风和相机,除非具备后台访问权限


      <manifest>
      ...
      <service ... android:foregroundServiceType="location|camera" />
      </manifest>



    5. 软件包可见性——不是所有应用都对软件可见了


      1.影响queryIntentActivities()的返回结果
      2.影响启动其他应用的Service
      3.不影响启动Activity
      4.别的应用和你发生交互,那它对你是可见的
      5.系统的一些支持程序是可见的
      6.ContentProvider是可见的,如果你具备权限的话
      7.用<queries>生命想要可见的一些应用





    1. 应用退出原因


      Android 11 引入了 ActivityManager.getHistoricalProcessExitReasons() 方法,用于报告近期任何进程终止的原因。应用可以使用此方法收集崩溃诊断信息,例如进程终止是由于 ANR、内存问题还是其他原因所致。此外,您还可以使用新的 setProcessStateSummary() 方法存储自定义状态信息,以便日后进行分析。



    2. 资源加载器ResourcesLoader——可用做换肤和插件化


      Android 11 引入了一个新 API,允许应用动态扩展资源的搜索和加载方式。新的 API 类 ResourcesLoader 和 ResourcesProvider 主要负责提供新功能。两者协同作用,可以提供额外的资源,或修改现有资源的值。

      ResourcesLoader 对象是向应用的 Resources 实例提供 ResourcesProvider 对象的容器,而 ResourcesProvider 对象提供从 APK 和资源表加载资源数据的方法。

      此 API 的一个主要用例是自定义资源加载。您可以使用 loadFromDirectory() 创建一个 ResourcesProvider,用于重定向基于文件的资源的解析,从而让其搜索特定目录,而不是应用 APK。您可以通过 AssetManager API 类中的 open() 系列方法访问这些资源,就像访问 APK 中绑定的资源一样。


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

    收起阅读 »

    iOS内存管理

    将计算机上有限的物理内存分配给多个程序使用地址空间不隔离内存使用率低程序运行的地址不确定虚拟内存虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,每个进程只能访问自己的地址空间,这样就能有效的做到了进程的隔离。注:...
    继续阅读 »

    将计算机上有限的物理内存分配给多个程序使用

    • 地址空间不隔离

    • 内存使用率低

    • 程序运行的地址不确定

    虚拟内存
    虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,每个进程只能访问自己的地址空间,这样就能有效的做到了进程的隔离。
    注: 虚拟储存的实现需要依赖硬件的支持,对于不同的CPU来说不同,但是几乎所有的硬件都采用MMU(Memory Management Unit)的部件来进行页映射。


    分段
    把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。


    • 因为程序A和程序B被映射到了两块不同的物理空间区域,它们之间没有任何重叠,如果A程序访问虚拟空间的地址超出了0x00A00000这个范围,那么硬件就会判断这是一个非法访问,拒绝这个请求,所以做到了地址隔离。

    • 对于每个程序来说,无论它们被分配到物理地址的那一个区域,都是透明的,它们不关心物理地址的变化,只要按照从地址0x0000000到0x00A00000来编写程序,放置变量,所以程序不再需要重定位

    分页
    当一个程序运行时,在某个时间段内,它只是频繁的用到了一小部分数据,程序的很多数据其实在一个时间段内都不会被用到。人们很自然的想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。


    • 进程1(VP0,VP5)和进程2(VP0,VP3,VP4,VP5)的虚拟地址映射到物理地址(PP0,PP2,PP2,PP3,PP4),可以看到有些虚拟空间被映射到同一个物理页,这样就实现了内存共享。

    • 还有一部分位于磁盘中,如果进程需要用到这两个页时,操作系统就会接管进程,负责将虚拟地址从磁盘中读出来并装入内存,然后再与物理地址建立映射关系。

    • 保护也是页映射的目的之一,每个页可以设置权限属性,而只有操作系统有权限修改这些属性,那么操作系统就可以保护自己和保护进程了。

    PAE
    原先的32位地址只能访问最多4GB的物理内存,但是自从扩展至36位地址线以后,Intel修改了页映射方式,使得新的映射方式可以访问到更多的物理内存,Intel把这个地址扩展方式叫做PAE(Physical Address Extension)、

    AWE
    应用程序可以根据需求来选择申请和映射,比如一个应用程序0x10000000 ~0x20000000这一段256MB的虚拟地址空间用来做窗口,程序可以从高4GB的物理空间申请多个大小为256MB的物理空间,编号成A、B、C的等,然后根据需要将这个窗口映射到不同物理空间块,用到A时映射到A,用到B、C时再映射过去,叫做AWE(Address Windowing Extension),而Linux等UNIX类操作系统则采用mmap()系统调用来实现

    • 程序执行做需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将进程运行所需要的指令和数据全部装入内存中,这就是简单的静态装入。

    • 由于内存的昂贵稀有,所需的内存大余物理内存,所以我们将程序最常用的部分驻留在内存中,不太常用的数据存放在磁盘里,这就是动态装入。覆盖装入和页映射就是两种很典型的动态装载方法。

    覆盖装入(Overlay)


    • 这个树状结构从任何一个模块到树的根(main)模块都叫调用路径,当该模块被调用时,整个调用路径上的模块都必须在内存中,已确保执行完毕后能正确返回至模块。

    • 禁止跨树间调用。任意一个模块不允许跨过树状结构进行调用。

    • 由于跨模块间的调用需要经过覆盖管理器,已确保所有被调用到的模块都能够正确的驻留在内存,而且一旦模块没有在内存中,还需要从磁盘或者其他存储器读取响应的模块,所以覆盖装入的速度肯定比较慢,是典型的利用时间换取空间的方法

    页映射(Paging)


    • 将内存和所有磁盘中的数据和指令按照page为单位划分为若干个页,以后所有的装载和操作的单位都是页,硬件规定页的大小为4096字节,8192KB等,那么512MB的物理内存就拥有512 * 1024 * 1024 /4096 = 131072 页。

    • 假设32的机器有16KB的内存,每个页4096字节,则共有4个页(F0、F1、F2、F3),假设程序需有的指令和数据总和为32KB,那么程序被分为8个页(P0~P7)。很明显16KB的内存无法同时将32KB的程序装入,那么我们将按照动态装入的原理来进行整个装入过程。

    • 如果只有4个页,那么程序能一直执行下去,但问题很明显不是,如果超过4个页,装载管理器必须作出抉择,放弃目前正在使用的4个内存中的其中一个。至于放弃那个页有多种算法:比如F0,先进先出;比如很少访问的F2,最少使用法

    页错误(Page Fault)
    一些存储在磁盘中的数据,在CPU执行这个地址指令时,发现页面是一个空的页面,于是他就认为这是一个页错误,CPU将控制权交给操作系统,操作系统有专门处理例程来处理,操作系统将查询这个数据结构,然后找到空页面所在的VMA,计算相应的页面在可执行文件中的偏移,然后再物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射,然后再交给进程去执行。

    进程虚拟空间分布
    ELF文件被映射时,是以页长度为单位的,每个段在映射时的长度应该是系统页长度的整倍数,如果不是,多余的部分页将占领一个页,造成了内存空间的大量浪费。而在ELF文件中,段的权限直邮为数不多的几种组合:

    • 以代码段为代表的权限为可读可执行的段

    • 以数据段和BSS段为代表的权限为可读可写的段

    • 以只读数据段为代表的权限为只读的段

    那么对于相同的段,我们把他们合并在一起当成一个段来映射,ELF可执行文件引入一个概念叫做Segment,一个segment包含一个或多个section,这样很明显的减少了页面内部的碎片,节省了空间

    段地址对齐
    假设一个ELF执行文件,它有三个段需要装载,SEG0 、SEG1、SEG2,如图:


    可以看到这种对齐方式在文件段的内部会有很多内部碎片,浪费磁盘空间,可执行文件总长度只有12014字节,却占了5个页。为了解决这个问题,UNIX系统采用了让那些个个段接壤的部分共用一个物理页面,将该物理页面映射两次,系统将它们映射两份到虚拟地址空间,其他的都按照正常的页粒度进行映射。

    转自:https://www.jianshu.com/p/779154738361


    收起阅读 »

    iOS 中事件的响应链和传递链

    iOS事件链有两条:事件的响应链;Hit-Testing事件的传递链响应链:由离用户最近的view向系统传递。initial view –> super view –> ….. –> view controller –> window ...
    继续阅读 »

    iOS事件链有两条:事件的响应链;Hit-Testing事件的传递链

    • 响应链:由离用户最近的view向系统传递。initial view –> super view –> ….. –> view controller –> window –> Application –> AppDelegate

    • 传递链:由系统向离用户最近的view传递。UIKit –> active app's event queue –> window –> root view –> …… –> lowest view

    在iOS中只有继承UIResponder的对象才能够接收并处理事件,UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象,首先我们通过一张图来简单了解一下事件的传递以及响应


    1、传递链

    • 事件传递的两个核心方法

    - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
    - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
    • 第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件

    • 第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES

    • 其中UIView不接受事件处理的情况有

    1. alpha <0.01
    2. userInteractionEnabled = NO
    3. hidden = YES
    • 事件传递的流程图


    • 流程描述

         1、我们点击屏幕产生触摸事件,系统将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow

         2、在UIWindow中就会调用hitTest:withEvent:方法去返回一个最终响应的视图

         3、在hitTest:withEvent:方法中就会去调用pointInside: withEvent:去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图

         4、遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的hitTest:withEvent:方法,可以理解为是一个递归调用

         5、最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIWindow作为响应者

    2、相应链

    • 响应者链流程图


    • 响应者链的事件传递过程总结如下

         1、如果view的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图

         2、在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给UIWindow对象进行处理

         3、如果UIWindow对象也不处理,则将事件传递给UIApplication对象

         4、如果UIApplication也不能处理该事件,则将该事件丢弃

    3、实例场景

    • 在一个方形按钮中点击中间的圆形区域有效,而点击四角无效

    • 核心思想是在pointInside: withEvent:方法中修改对应的区域

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 如果控件不允许与用用户交互,那么返回nil
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
    return nil;
    }

    //判断当前视图是否在点击范围内
    if ([self pointInside:point withEvent:event]) {
    //遍历当前对象的子视图(倒序)
    __block UIView *hit = nil;
    [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    //坐标转换,把当前坐标系上的点转换成子控件坐标系上的点
    CGPoint convertPoint = [self convertPoint:point toView:obj];
    //调用子视图的hitTest方法,判断自己的子控件是不是最适合的View
    hit = [obj hitTest:convertPoint withEvent:event];
    //如果找到了就停止遍历
    if (hit) *stop = YES;
    }];

    //返回当前的视图对象
    return hit?hit:self;
    }else {
    return nil;
    }
    }

    // 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat x1 = point.x;
    CGFloat y1 = point.y;

    CGFloat x2 = self.frame.size.width / 2;
    CGFloat y2 = self.frame.size.height / 2;

    //判断是否在圆形区域内
    double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    if (dis <= self.frame.size.width / 2) {
    return YES;
    }
    else{
    return NO;
    }
    }

    转自:jianshu.com/p/cfcde82c67f6


    收起阅读 »

    iOS 中 如何从视频中提取音频

    .h文件/**提取视频中的音频@param videoPath 视频路径@param completionHandle 完成回调*/+(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void ...
    继续阅读 »

    .h文件
    /**

    提取视频中的音频

    @param videoPath 视频路径
    @param completionHandle 完成回调
    */

    +(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void (^)(NSURL *outputPath,BOOL isSucceed)) completionHandle;

    .m文件
    需要导入系统的#import <Photos/Photos.h>
    /**

    提取视频中的音频

    @param videoPath 视频路径
    @param completionHandle 完成回调
    */

    +(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void (^)(NSURL *outputPath,BOOL isSucceed)) completionHandle{

    AVAsset *videoAsset = [AVAsset assetWithURL:videoPath];
    //1创建一个AVMutableComposition
    AVMutableComposition *mixComposition = [[AVMutableComposition alloc]init];
    //2 创建一个轨道,类型为AVMediaTypeAudio
    AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

    //获取videoPath的音频插入轨道
    [audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:nil];

    //4创建输出路径
    NSURL *outputURL = [self exporterPath:@"mp4"];

    //5创建输出对象
    AVAssetExportSession *exporter = [[AVAssetExportSession alloc]initWithAsset:mixComposition presetName:AVAssetExportPresetAppleM4A];
    exporter.outputURL = outputURL ;
    exporter.outputFileType = AVFileTypeAppleM4A;
    exporter.shouldOptimizeForNetworkUse = YES;
    [exporter exportAsynchronouslyWithCompletionHandler:^{
    if (exporter.status == AVAssetExportSessionStatusCompleted) {
    NSURL *outputURL = exporter.outputURL;
    completionHandle(outputURL,YES);
    }else {
    NSLog(@"失败%@",exporter.error.description);
    completionHandle(outputURL,NO);
    }
    }];

    }
    // 输出路径
    + (NSURL *)exporterPath:(NSString *)filename{
    NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
    formatter.dateFormat = @"yyyyMMddHHmmss";
    NSString *str = [formatter stringFromDate:[NSDate date]];
    NSString *fileName = [NSString stringWithFormat:@"selfMusic%@.%@",str,filename];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docPath = [paths firstObject];
    //这个是录制视频时存储到本地的video
    NSString *path = [NSString stringWithFormat:@"%@/KSYShortVideoCache",docPath];
    //判断文件夹是否存在,不存在就创建
    //创建附件存储目录
    if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
    [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
    }
    NSString *outputFilePath = [path stringByAppendingPathComponent:fileName];
    return [NSURL fileURLWithPath:outputFilePath];
    }

    转自:https://www.jianshu.com/p/ffa126bb2736

    收起阅读 »

    静态拦截iOS对象方法调用的简易实现

    最近出现了几篇关于二进制重排启动优化的文章。所有方案中都需要事先统计所有的函数调用情况,并根据函数调用的频次来进行代码的重排。这些函数调用中,OC对象的方法调用最多。统计OC对象的方法调用可以在运行时通过第三方库比如fishhook来Hook所有objc_ms...
    继续阅读 »

    最近出现了几篇关于二进制重排启动优化的文章。所有方案中都需要事先统计所有的函数调用情况,并根据函数调用的频次来进行代码的重排。
    这些函数调用中,OC对象的方法调用最多。统计OC对象的方法调用可以在运行时通过第三方库比如fishhook来Hook所有objc_msgSend调用来实现,也可以在编译后链接前通过静态插桩的方式来实现Hook拦截。
    对于静态插桩的实现一般有如下两个方案:
    1、助于LLVM语法树分析来实现代码插桩。
    2、将源码编译为静态库,并通过修改静态库中.o目标文件的代码段来实现代码插桩。
    上述的两个方法实现起来比较复杂,要么就要了解LLVM,要么就要熟悉目标文件中间字节码以及符号表相关的底层知识。
    本文所介绍的是第三种静态Hook方案,也是依赖于静态库这个前提来实现对objc_msgSend函数进行Hook,从而实现在编译前链接后的OC对象方法调用插桩。
    这个方案实现的原理很简单。因为静态库其实只是一个编译阶段的中间产物,静态库目标文件中的所有引用的外部符号会保存到一张字符串表中,所有函数调用都只是记录了函数名称在字符串表的索引位置,在链接时会才会根据符号名称来替换为真实的函数调用指令。因此我们可以将所有静态库字符串表中的objc_msgSend统一替换为另外一个长度相同的字符串:hook_msgSend(名字任意只要长度一致并唯一)即可。然后在主工程源代码中实现一个名字为hook_msgSend的函数即可。这个函数必须要和objc_msgSend的函数签名保持一致,这样在链接时所有静态库中的objc_msgSend调用都会统一转化为hook_msgSend调用。

    下面的是具体的实现步骤:

    1.在主工程中编写hook_msgSend的实现。
    hook_msgSend的函数签名要和objc_msgSend保持一致,并且要在主工程代码中实现,而且必须要用汇编代码实现。具体实现的逻辑和目前很多文章中介绍的对objc_msgSend函数的Hook实现保持一致即可。
    很多对objc_msgSend进行Hook的实现其实是不完整的,因此如果想完全掌握函数调用ABI规则的话请参考:《深入iOS系统底层之函数调用》

    2. 将所有其他代码都统一编译为一个或多个静态库。
    将源代码按功能编译为一个或多个静态库,并且主工程链接这些静态库。这种程序代码的组织方式已经很成熟了,最常用的方法是我们可以借助代码依赖集成工具cocoapods来实现,这里就不再赘述了。

    3.在主工程的Build Phases 中添加Run Script脚本。
    我们需要保证这个脚本一定要运行在链接所有静态库之前执行。因此可以放到Compile Sources 下面。

    4.实现静态库符号替换的Run Script脚本。
    这是最为关键的一步,我们可以实现一个符号替换的程序,然后在Run Script脚本中 执行这个符号替换程序。符号替换程序的输入参数就是主工程中所链接的所有静态库的路径。至于这个符号替换程序如何编写则没有限制,你可以用ruby编写也可以用python也可以用C语言编写。 无论用何种方法实现,你都需要首先了解一下静态库.a的文件结构。你可以从:《深入iOS系统底层之静态库》一文中掌握到一个静态库文件的组成结构。了解了静态库文件的组成结构后,你的符号替换程序要做的事情就可以按如下步骤实现:

    一)、 打开静态库.a文件。
    二)、找.a文件中定义的字符串表部分。字符串表的描述如下:

    struct stringtab
    {
    int size; //字符串表的尺寸
    char strings[0]; //字符串表的内容,每个字符串以\0分隔。
    };

    字符串表中的strings的内容就是一个个以\0分隔的字符串,这些字符串的内容其实就是这个目标文件所引用的所有外部和内部的符号名称。
    三)、将字符串表中的objc_msgSend字符串替换为hook_msgSend字符串。
    四)、保存并关闭静态库.a文件。

    5.编译、链接并运行你的主工程程序。
    采用本文中所介绍的静态Hook方法的好处是我们不必Hook所有的OC方法调用,而是可以有选择的进行特定对象和类的方法调用拦截。因此这种技术不仅可以应用代码重排统计上,还可以应用在其他的监控和统计应用中。因为这种机制可以避免程序在运行时进行objc_msgSend替换而产生的函数调用风暴问题。另外的一个点就是这个方法不局限于对objc_msgSend进行Hook,还可以对任意的其他函数进行Hook处理。因此这种技术也可以应用在其他方面。

    转自:https://www.jianshu.com/p/843642c9df32

    收起阅读 »

    webpack手写loader

    手写loader   我们在在Webpack配置基础篇介绍过,loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则: 单一原则: 每个Loader只做一件事,...
    继续阅读 »

    手写loader


      我们在在Webpack配置基础篇介绍过,loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则:



    1. 单一原则: 每个Loader只做一件事,简单易用,便于维护;

    2. 链式调用: Webpack 会按顺序链式调用每个Loader;

    3. 统一原则: 遵循Webpack制定的设计规则和结构,输入与输出均为字符串,各个Loader完全独立,即插即用;

    4. 无状态原则:在转换不同模块时,不应该在loader中保留状态;


      因此我们就来尝试写一个less-loaderstyle-loader,将less文件处理后通过style标签的方式渲染到页面上去。


    同步loader


      loader默认导出一个函数,接受匹配到的文件资源字符串和SourceMap,我们可以修改文件内容字符串后再返回给下一个loader进行处理,因此最简单的一个loader如下:


    module.exports = function(source, map){
    return source
    }


    导出的loader函数不能使用箭头函数,很多loader内部的属性和方法都需要通过this进行调用,比如this.cacheable()来进行缓存、this.sourceMap判断是否需要生成sourceMap等。



      我们在项目中创建一个loader文件夹,用来存放我们自己写的loader,然后新建我们自己的style-loader:


    //loader/style-loader.js
    function loader(source, map) {
    let style = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style)
    `
    ;
    return style;
    }
    module.exports = loader;

      这里的source就可以看做是处理后的css文件字符串,我们把它通过style标签的形式插入到head中;同时我们也发现最后返回的是一个JS代码的字符串,webpack最后会将返回的字符串打包进模块中。


    异步loader


      上面的style-loader都是同步操作,我们在处理source时,有时候会进行异步操作,一种方法是通过async/await,阻塞操作执行;另一种方法可以通过loader本身提供的回调函数callback


    //loader/less-loader
    const less = require("less");
    function loader(source) {
    const callback = this.async();
    less.render(source, function (err, res) {
    let { css } = res;
    callback(null, css);
    });
    }
    module.exports = loader;

      callback的详细传参方法如下:


    callback({
    //当无法转换原内容时,给 Webpack 返回一个 Error
    error: Error | Null,
    //转换后的内容
    content: String | Buffer,
    //转换后的内容得出原内容的Source Map(可选)
    sourceMap?: SourceMap,
    //原内容生成 AST语法树(可选)
    abstractSyntaxTree?: AST
    })

      有些时候,除了将原内容转换返回之外,还需要返回原内容对应的Source Map,比如我们转换less和scss代码,以及babel-loader转换ES6代码,为了方便调试,需要将Source Map也一起随着内容返回。


    //loader/less-loader
    const less = require("less");
    function loader(source) {
    const callback = this.async();
    less.render(source,{sourceMap: {}}, function (err, res) {
    let { css, map } = res;
    callback(null, css, map);
    });
    }
    module.exports = loader;

      这样我们在下一个loader就能接收到less-loader返回的sourceMap了,但是需要注意的是:



    Source Map生成很耗时,通常在开发环境下才会生成Source Map,其它环境下不用生成。Webpack为loader提供了this.sourceMap这个属性来告诉loader当前构建环境用户是否需要生成Source Map。



    加载本地loader


      loader文件准备好了之后,我们需要将它们加载到webpack配置中去;在基础篇中,我们加载第三方的loader只需要安装后在loader属性中写loader名称即可,现在加载本地loader需要把loader的路径配置上。


    module.exports = {
    module: {
    rules: [{
    test: /\.less/,
    use: [
    {
    loader: './loader/style-loader.js',
    },
    {
    loader: path.resolve(__dirname, "loader", "less-loader"),
    },
    ],
    }]
    }
    }

      我们可以在loader中配置本地loader的相对路径或者绝对路径,但是这样写起来比较繁琐,我们可以利用webpack提供的resolveLoader属性,来告诉webpack应该去哪里解析本地loader。


    module.exports = {
    module: {
    rules: [{
    test: /\.less/,
    use: [
    {
    loader: 'style-loader',
    },
    {
    loader: 'less-loader',
    },
    ],
    }]
    },
    resolveLoader:{
    modules: [path.resolve(__dirname, 'loader'), 'node_modules']
    }
    }

      这样webpack会先去loader文件夹下找loader,没有找到才去node_modules;因此我们写的loader尽量不要和第三方loader重名,否则会导致第三方loader被覆盖加载。


    处理参数


      我们在配置loader时,经常会给loader传递参数进行配置,一般是通过options属性来传递的,也有像url-loader通过字符串来传参:


    {
    test: /\.(jpg|png|gif|bmp|jpeg)$/,
    use: 'url-loader?limt=1024&name=[hash:8].[ext]'
    }

      webpack也提供了query属性来获取传参;但是query属性很不稳定,如果像上面的通过字符串来传参,query就返回字符串格式,通过options方式就会返回对象格式,这样不利于我们处理。因此我们借助一个官方的包loader-utils帮助处理,它还提供了很多有用的工具。


    const { 
    getOptions,
    parseQuery,
    stringifyRequest,
    } = require("loader-utils");

    module.exports = function (source, map) {
    //获取options参数
    const options = getOptions(this);
    //解析字符串为对象
    parseQuery("?param1=foo")
    //将绝对路由转换成相对路径
    //以便能在require或者import中使用以避免绝对路径
    stringifyRequest(this, "test/lib/index.js")
    }

      常用的就是getOptions将处理后的参数返回出来,它内部的实现逻辑也非常的简单,也是根据query属性进行处理,如果是字符串的话调用parseQuery方法进行解析,源码如下:


    //loader-utils/lib/getOptions.js
    'use strict';
    const parseQuery = require('./parseQuery');
    function getOptions(loaderContext) {
    const query = loaderContext.query;
    if (typeof query === 'string' && query !== '') {
    return parseQuery(loaderContext.query);
    }
    if (!query || typeof query !== 'object') {
    return {};
    }
    return query;
    }
    module.exports = getOptions;

      获取到参数后,我们还需要对获取到的options参数进行完整性校验,避免有些参数漏传,如果一个个判断校验比较繁琐,这就用到另一个官方包schema-utils


    const { getOptions } = require("loader-utils");
    const { validate } = require("schema-utils");
    const schema = require("./schema.json");
    module.exports = function (source, map) {
    const options = getOptions(this);
    const configuration = { name: "Loader Name"};
    validate(schema, options, configuration);
    //省略其他代码
    }

      validate函数并没有返回值,打印返回值发现是`undefined,因为如果参数不通过的话直接会抛出ValidationError异常,直接进程中断;这里引入了一个schema.json,就是我们对options``中参数进行校验的一个json格式的对应表:


    {
    "type": "object",
    "properties": {
    "source": {
    "type": "boolean"
    },
    "name": {
    "type": "string"
    },
    },
    "additionalProperties": false
    }

      properties中的健名就是我们需要检验的options中的字段名称,additionalProperties代表了是否允许options中还有其他额外的属性。


    less-loader源码分析


      写完我们自己简单的less-loader,让我们来看一下官方的less-loader源码到底是怎么样的,这里贴上部分源码:


    import less from 'less';
    import { getOptions } from 'loader-utils';
    import { validate } from 'schema-utils';
    import schema from './options.json';
    async function lessLoader(source) {
    const options = getOptions(this);
    //校验参数
    validate(schema, options, {
    name: 'Less Loader',
    baseDataPath: 'options',
    });
    const callback = this.async();
    //对options进一步处理,生成less渲染的参数
    const lessOptions = getLessOptions(this, options);
    //是否使用sourceMap,默认取options中的参数
    const useSourceMap =
    typeof options.sourceMap === 'boolean'
    ? options.sourceMap : this.sourceMap;
    //如果使用sourceMap,就在渲染参数加入
    if (useSourceMap) {
    lessOptions.sourceMap = {
    outputSourceFiles: true,
    };
    }
    let data = source;
    let result;
    try {
    result = await less.render(data, lessOptions);
    } catch (error) {
    }
    const { css, imports } = result;
    //有sourceMap就进行处理
    let map =
    typeof result.map === 'string'
    ? JSON.parse(result.map) : result.map;

    callback(null, css, map);
    }
    export default lessLoader;

      可以看到官方的less-loader和我们写的简单的loader本质上都是调用less.render函数,对文件资源字符串进行处理,然后将处理好后的字符串和sourceMap通过callback返回。


    loader依赖


      在loader中,我们有时候也会使用到外部的资源文件,我们需要在loader对这些资源文件进行声明;这些声明信息主要用于使得缓存loader失效,以及在观察模式(watch mode)下重新编译。


      我们尝试写一个banner-loader,在每个js文件资源后面加上我们自定义的注释内容;如果传了filename,就从文件中获取预设好的banner内容,首先我们预设两个banner的txt:


    //loader/banner1.txt
    /* build from banner1 */

    //loader/banner2.txt
    /* build from banner2 */

      然后在我们的banner-loader中根据参数来进行判断:


    //loader/banner-loader
    const fs = require("fs");
    const path = require("path");
    const { getOptions } = require("loader-utils");

    module.exports = function (source) {
    const options = getOptions(this);
    if (options.filename) {
    let txt = "";
    if (options.filename == "banner1") {
    this.addDependency(path.resolve(__dirname, "./banner1.txt"));
    txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
    } else if (options.filename == "banner2") {
    this.addDependency(path.resolve(__dirname, "./banner1.txt"));
    txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
    }
    return source + txt;
    } else if (options.text) {
    return source + `/* ${options.text} */`;
    } else {
    return source;
    }
    };

      这里使用了this.addDependency的API将当前处理的文件添加到文件依赖中(并不是项目的package.json)。如果在观察模式下,依赖的text文件发生了变化,那么打包生成的文件内容也随之变化。



    如果不添加this.addDependency的话项目并不会报错,只是在观察模式下,如果依赖的文件发生了变化生成的bundle文件并不能及时更新。



    缓存加速


      在有些情况下,loader处理需要大量的计算非常耗性能(比如babel-loader),如果每次构建都重新执行相同的转换操作每次构建都会非常慢。


      因此webpack默认会将loader的处理结果标记为可缓存,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,它的输出结果必然是相同的;如果不想让webpack缓存该loader,可以禁用缓存:


    module.exports = function(source) {
    // 强制不缓存
    this.cacheable(false);
    return source;
    };

    手写loader所有代码均在webpackdemo19



    收起阅读 »

    深入webpack打包原理

    本文讨论的核心内容如下: webpack进行打包的基本原理 如何自己实现一个loader和plugin 注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11,node版本是v12.14.1,npm版本v6.13....
    继续阅读 »

    本文讨论的核心内容如下:



    1. webpack进行打包的基本原理

    2. 如何自己实现一个loaderplugin


    注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11node版本是v12.14.1npm版本v6.13.4(如果你喜欢yarn也是可以的),演示用的chrome浏览器版本81.0.4044.129(正式版本) (64 位)


    1. webpack打包基本原理


    webpack的一个核心功能就是把我们写的模块化的代码,打包之后,生成可以在浏览器中运行的代码,我们这里也是从简单开始,一步步探索webpack的打包原理


    1.1 一个简单的需求


    我们首先建立一个空的项目,使用npm init -y快速初始化一个package.json,然后安装webpack webpack-cli


    接下来,在根目录下创建src目录,src目录下创建index.jsadd.jsminus.js,根目录下创建index.html,其中index.html引入index.js,在index.js引入add.jsminus.js


    目录结构如下:



    文件内容如下:

    // add.js
    export default (a, b) => {
    return a + b
    }
    // minus.js
    export const minus = (a, b) => {
    return a - b
    }
    // index.js
    import add from './add.js'
    import { minus } from './minus.js'

    const sum = add(1, 2)
    const division = minus(2, 1)
    console.log('sum>>>>>', sum)
    console.log('division>>>>>', division)
    <!--index.html-->

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>demo</title>
    </head>
    <body>
    <script src="./src/index.js"></script>
    </body>
    </html>

    <<span class="hljs-attribute">span</span> class=<span class="hljs-string">"hljs-attribute"</span>>demo</span>

    这样直接在index.html引入index.js的代码,在浏览器中显然是不能运行的,你会看到这样的错误


    Uncaught SyntaxError: Cannot use import statement outside a module

    是的,我们不能在script引入的js文件里,使用es6模块化语法


    1.2 实现webpack打包核心功能


    我们首先在项目根目录下再建立一个bundle.js,这个文件用来对我们刚刚写的模块化js代码文件进行打包


    我们首先来看webpack官网对于其打包流程的描述:


    it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle)


    在正式开始之前,结合上面webpack官网说明进行分析,明确我们进行打包工作的基本流程如下:



    1. 首先,我们需要读到入口文件里的内容(也就是index.js的内容)

    2. 其次,分析入口文件,递归的去读取模块所依赖的文件内容,生成依赖图

    3. 最后,根据依赖图,生成浏览器能够运行的最终代码


    1. 处理单个模块(以入口为例)


    1.1 获取模块内容


    既然要读取文件内容,我们需要用到node.js的核心模块fs,我们首先来看读到的内容是什么:

    // bundle.js
    const fs = require('fs')
    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    console.log(body)
    }
    getModuleInfo('./src/index.js')


    1.2 分析模块内容


    我们安装@babel/parser,演示时安装的版本号为^7.9.6


    这个babel模块的作用,就是把我们js文件的代码内容,转换成js对象的形式,这种形式的js对象,称做抽象语法树(Abstract Syntax Tree, 以下简称AST)

    // bundle.js
    const fs = require('fs')
    const parser = require('@babel/parser')
    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
    // 表示我们要解析的是es6模块
    sourceType: 'module'
    })
    console.log(ast)
    console.log(ast.program.body)
    }
    getModuleInfo('./src/index.js')



    入口文件内容被放到一个数组中,总共有六个Node节点,我们可以看到,每个节点有一个type属性,其中前两个的type属性是ImportDeclaration,这对应了我们入口文件的两条import语句,并且,每一个type属性是ImportDeclaration的节点,其source.value属性是引入这个模块的相对路径,这样我们就得到了入口文件中对打包有用的重要信息了。


    接下来要对得到的ast做处理,返回一份结构化的数据,方便后续使用。


    1.3 对模块内容做处理


    ast.program.body部分数据的获取和处理,本质上就是对这个数组的遍历,在循环中做数据处理,这里同样引入一个babel的模块@babel/traverse来完成这项工作。


    安装@babel/traverse,演示时安装的版本号为^7.9.6

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default

    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
    sourceType: 'module'
    })
    const deps = {}
    traverse(ast, {
    ImportDeclaration({ node }) {
    const dirname = path.dirname(file);
    const absPath = './' + path.join(dirname, node.source.value)
    deps[node.source.value] = absPath
    }
    })
    console.log(deps)
    }
    getModuleInfo('./src/index.js')

    创建一个对象deps,用来收集模块自身引入的依赖,使用traverse遍历ast,我们只需要对ImportDeclaration的节点做处理,注意我们做的处理实际上就是把相对路径转化为绝对路径,这里我使用的是Mac系统,如果是windows系统,注意斜杠的区别


    获取依赖之后,我们需要对ast做语法转换,把es6的语法转化为es5的语法,使用babel核心模块@babel/core以及@babel/preset-env完成


    安装@babel/core @babel/preset-env,演示时安装的版本号均为^7.9.6

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const babel = require('@babel/core')

    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
    sourceType: 'module'
    })
    const deps = {}
    traverse(ast, {
    ImportDeclaration({ node }) {
    const dirname = path.dirname(file);
    const absPath = './' + path.join(dirname, node.source.value)
    deps[node.source.value] = absPath
    }
    })
    const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
    })
    const moduleInfo = { file, deps, code }
    console.log(moduleInfo)
    return moduleInfo
    }
    getModuleInfo('./src/index.js')


    2. 递归的获取所有模块的信息

    这个过程,也就是获取依赖图(dependency graph)的过程,这个过程就是从入口模块开始,对每个模块以及模块的依赖模块都调用getModuleInfo方法就行分析,最终返回一个包含所有模块信息的对象

    const parseModules = file => {
    // 定义依赖图
    const depsGraph = {}
    // 首先获取入口的信息
    const entry = getModuleInfo(file)
    const temp = [entry]
    for (let i = 0; i < temp.length; i++) {
    const item = temp[i]
    const deps = item.deps
    if (deps) {
    // 遍历模块的依赖,递归获取模块信息
    for (const key in deps) {
    if (deps.hasOwnProperty(key)) {
    temp.push(getModuleInfo(deps[key]))
    }
    }
    }
    }
    temp.forEach(moduleInfo => {
    depsGraph[moduleInfo.file] = {
    deps: moduleInfo.deps,
    code: moduleInfo.code
    }
    })
    console.log(depsGraph)
    return depsGraph
    }
    parseModules('./src/index.js')


    3. 生成最终代码


    在我们实现之前,观察上一节最终得到的依赖图,可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理


    我们首先调用之前说的parseModules方法,获得整个应用的依赖图对象:


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    }

    接下来我们应该把依赖图对象中的内容,转换成能够执行的代码,以字符串形式输出。
    我们把整个代码放在自执行函数中,参数是依赖图对象


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }

    接下来内容其实很简单,就是我们取得入口文件的code信息,去执行它就好了,使用eval函数执行,初步写出代码如下:


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    (function(code){
    eval(code)
    })(graph[file].code)
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }

    上面的写法是有问题的,我们需要对file做绝对路径转化,否则graph[file].code是获取不到的,定义adsRequire方法做相对路径转化为绝对路径


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    function absRequire(relPath){
    return require(graph[file].deps[relPath])
    }
    (function(require, exports, code){
    eval(code)
    })(absRequire, exports, graph[file].code)
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }

    接下来,我们只需要执行bundle方法,然后把生成的内容写入一个JavaScript文件即可


    const content = bundle('./src/index.js')
    // 写入到dist/bundle.js
    fs.mkdirSync('./dist')
    fs.writeFileSync('./dist/bundle.js', content)

    4. bundle.js的完整代码

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const babel = require('@babel/core')

    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    console.log(body)
    const ast = parser.parse(body, {
    sourceType: 'module'
    })
    // console.log(ast.program.body)
    const deps = {}
    traverse(ast, {
    ImportDeclaration({ node }) {
    const dirname = path.dirname(file);
    const absPath = './' + path.join(dirname, node.source.value)
    deps[node.source.value] = absPath
    }
    })
    const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
    })
    const moduleInfo = { file, deps, code }
    return moduleInfo
    }

    const parseModules = file => {
    // 定义依赖图
    const depsGraph = {}
    // 首先获取入口的信息
    const entry = getModuleInfo(file)
    const temp = [entry]
    for (let i = 0; i < temp.length; i++) {
    const item = temp[i]
    const deps = item.deps
    if (deps) {
    // 遍历模块的依赖,递归获取模块信息
    for (const key in deps) {
    if (deps.hasOwnProperty(key)) {
    temp.push(getModuleInfo(deps[key]))
    }
    }
    }
    }
    temp.forEach(moduleInfo => {
    depsGraph[moduleInfo.file] = {
    deps: moduleInfo.deps,
    code: moduleInfo.code
    }
    })
    // console.log(depsGraph)
    return depsGraph
    }


    // 生成最终可以在浏览器运行的代码
    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    function absRequire(relPath){
    return require(graph[file].deps[relPath])
    }
    (function(require, exports, code){
    eval(code)
    })(absRequire, exports, graph[file].code)
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }


    const build = file => {
    const content = bundle(file)
    // 写入到dist/bundle.js
    fs.mkdirSync('./dist')
    fs.writeFileSync('./dist/bundle.js', content)
    }

    build('./src/index.js')



    收起阅读 »

    iOS runtime之--动态修改字体大小

    那么既然runtime可以进行方法交换,我们只要自定义一个方法,替换系统的方法不就可以实现了吗?话不多说,我们开始动手UIFont设置font的类方法替换#import "UIFont+Category.h"#import "NSObject+Category...
    继续阅读 »

    介绍一下runtime的实际应用场景之一:怎样利用runtime的方法交换,在不修改原有代码的基础上动态的根据屏幕尺寸修改字体大小,包括xib和storyboard中拖的控件。

    我们知道,通常代码设置字体大小用的是UIFont的几个类方法 :

    systemFontOfSize

    fontWithName:size

    boldSystemFontOfSize

    italicSystemFontOfSize

    ...

    那么既然runtime可以进行方法交换,我们只要自定义一个方法,替换系统的方法不就可以实现了吗?话不多说,我们开始动手

    实现NSObject类方法交换

    创建NSObject分类,并增加一个可进行“Method交换”的方法。Method交换的本质,其实就是imp指针的交换。系统给我们提供了一个C语言的函数method_exchangeImplementations可以进行交换。流程如下:

    1.根据原方法和目标方法的selector,获取方法的method。如果是类方法用class_getClassMethod获取method,如是对象方法则用class_getInstanceMethod获取method

    2.获取到method后,调用method_exchangeImplementations函数进行两个method的imp指针的交换

    #import "NSObject+Category.h"
    #import

    @implementation NSObject (Category)

    /**
    @brief 方法替换
    @param originselector 替换的原方法
    @param swizzleSelector 替换后的方法
    @param isClassMethod 是否为类方法,YES为类方法,NO为对象方法
    */
    + (void)runtimeReplaceFunctionWithSelector:(SEL)originselector
    swizzleSelector:(SEL)swizzleSelector
    isClassMethod:(BOOL)isClassMethod
    {
    Method originMethod;
    Method swizzleMethod;
    if (isClassMethod == YES) {
    originMethod = class_getClassMethod([self class], originselector);
    swizzleMethod = class_getClassMethod([self class], swizzleSelector);
    }else{
    originMethod = class_getInstanceMethod([self class], originselector);
    swizzleMethod = class_getInstanceMethod([self class], swizzleSelector);
    }
    method_exchangeImplementations(originMethod, swizzleMethod);
    }
    @end

    UIFont设置font的类方法替换

    新建一个UIFont分类,在+(void)load方法中进行UIFont系统方法的替换
    #import "UIFont+Category.h"
    #import "NSObject+Category.h"

    @implementation UIFont (Category)

    //+(void)load方法会在main函数之前自动调用,不需要手动调用
    + (void)load
    {
    //交换systemFontOfSize: 方法
    [[self class] runtimeReplaceFunctionWithSelector:@selector(systemFontOfSize:) swizzleSelector:@selector(customSystemFontOfSize:) isClassMethod:YES];
    //交换fontWithName:size:方法
    [[self class] runtimeReplaceFunctionWithSelector:@selector(fontWithName:size:) swizzleSelector:@selector(customFontWithName:size:) isClassMethod:YES];
    }

    //自定义的交换方法
    + (UIFont *)customSystemFontOfSize:(CGFloat)fontSize
    {
    CGFloat size = [UIFont transSizeWithFontSize:fontSize];
    ///这里并不会引起递归,方法交换后,此时调用customSystemFontOfSize方法,其实是调用了原来的systemFontOfSize方法
    return [UIFont customSystemFontOfSize:size];
    }

    //自定义的交换方法
    + (UIFont *)customFontWithName:(NSString *)fontName size:(CGFloat)fontSize
    {
    CGFloat size = [UIFont transSizeWithFontSize:fontSize];
    return [UIFont customFontWithName:fontName size:size];
    }

    ///屏幕宽度大于320的,字体加10。(此处可根据不同的需求设置字体大小)
    + (CGFloat)transSizeWithFontSize:(CGFloat)fontSize
    {
    CGFloat size = fontSize;
    CGFloat width = [UIFont getWidth];
    if (width > 320) {
    size += 10;
    }
    return size;
    }

    ///获取竖屏状态下的屏幕宽度
    + (CGFloat)getWidth
    {
    for (UIScreen *windowsScenes in UIApplication.sharedApplication.connectedScenes) {
    UIWindowScene * scenes = (UIWindowScene *)windowsScenes;
    UIWindow *window = scenes.windows.firstObject;
    if (scenes.interfaceOrientation == UIInterfaceOrientationPortrait) {
    return window.frame.size.width;
    }
    return window.frame.size.height;
    }
    return 0;
    }

    @end

    至此就实现了,动态改变字体大小的目的,那xib和storyboard拖的控件怎么修改呢?我们接着看

    动态修改xib和storyboard控件的字体大小

    xib和sb拖拽的控件,都会调用 initWithCoder方法,那么我们可以自定义一个方法,替换掉initWithCoder,并在此方法中修改控件的字体不就可以了吗。我们先用UILabel举例,先创建一个UILabel的分类,然后在+(void)load方法中进行initWithCoder方法的交换

    #import "UILabel+Category.h"
    #import "NSObject+Category.h"

    @implementation UILabel (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    ///此时调用fontWithName:size:方法,实际上调用的是方法交换后的customFontWithName:size:
    self.font = [UIFont fontWithName:self.font.familyName size:self.font.pointSize];
    }
    return self;
    }

    @end

    此时我们就实现了,UILabel字体大小的动态修改,同理我们实现其它几个开发中常用的几个控件修改

    UIButton的分类

    #import "UIButton+Category.h"
    #import "NSObject+Category.h"

    @implementation UIButton (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    if (self.titleLabel != nil) {
    self.titleLabel.font = [UIFont fontWithName:self.titleLabel.font.familyName size:self.titleLabel.font.pointSize];
    }
    }
    return self;
    }

    @end

    UITextField的分类

    #import "UITextField+Category.h"
    #import "NSObject+Category.h"

    @implementation UITextField (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    self.font = [UIFont fontWithName:self.font.familyName size:self.font.pointSize];
    }
    return self;
    }

    @end

    UITextView的分类

    #import "UITextView+Category.h"
    #import "NSObject+Category.h"

    @implementation UITextView (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    self.font = [UIFont fontWithName:self.font.familyName size:self.font.pointSize];
    }
    return self;
    }

    @end

    到此,我们就完成了控件字体大小的动态修改,我们在storyboard中拖几个控件,然后用代码创建几个控件,分别看一下修改前和修改后的效果

                                                                           设置前


                                                                                        设置后

    注:Swift语言做法类似,但是Swift不允许重写+(void)load方法。所以如果是Swift,文中的+ (void)load需要改为自己定义的方法,并在AppDelegate的
    didFinishLaunchingWithOptions方法中进行调用。


    原贴链接:https://www.jianshu.com/p/c1f206433809
    收起阅读 »

    iOS runtime之--动态添加属性和方法

    一、runtime添加属性在Objective-C中,category分类默认只能添加方法,不能添加属性。根本原因在于声明了@property后,category并不会自动生成set和get方法。如果有需要在category中添加属性,可以利用runtime的...
    继续阅读 »

    一、runtime添加属性

    在Objective-C中,category分类默认只能添加方法,不能添加属性。根本原因在于声明了@property后,category并不会自动生成set和get方法。如果有需要在category中添加属性,可以利用runtime的特性实现。
    //新建一个NSObject的category类,并添加一个customString属性
    @interface NSObject (Category)
    @property(nonatomic,copy)NSString *customString;
    @end

    //在.m文件中实现set、get方法,此时添加属性代码便完成了,就是如此简单
    #import "NSObject+Category.h"
    #import <objc/message.h>
    - (void)setCustomString:(NSString *)customString {
    objc_setAssociatedObject(self, &customStringKey, customString, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }

    - (NSString *)customString {
    return objc_getAssociatedObject(self, &customStringKey);
    }

    //测试一下,如果打印出1111,就代表添加属性成a国
    - (void)viewDidLoad {
    [super viewDidLoad];
    ///动态添加属性
    NSObject *objct = [[NSObject alloc] init];
    objct.customString = @"1111";
    NSLog(@"%@",objct.customString);
    }

    动态添加属性,主要用到了两个runtime函数:
    1.添加属性


    此函数有四个参数。
    第一个参数指给哪个对象添加属性,第二个参数指属性的key指针,第三个参数指属性的名字,第四个参数指引用类型和原子性。
    其中着重讲一下第四个参数,此参数有五个值:
    OBJC_ASSOCIATION_ASSIGN 代表生成一个弱类型属性,相当于@property(atomic,assign)
    OBJC_ASSOCIATION_RETAIN_NONATOMIC相当于@property(nonatomic,strong)
    OBJC_ASSOCIATION_COPY_NONATOMIC,相当于@property(nonatomic,copy)
    OBJC_ASSOCIATION_RETAIN,相当于@property(atomic,strong)
    OBJC_ASSOCIATION_COPY,
    相当于@property(atomic,copy)
    上面代码生成的是string对象,所以我们一般用OBJC_ASSOCIATION_COPY_NONATOMIC

    2.获取属性值

    第二个函数是获取动态生成的属性,此函数有两个参数,第一个参数指的是哪个对象的属性,第二个参数为属性的key指针(每个动态添加的属性都需要有一个唯一的key)

    二、runtime动态添加方法

    为People类添加方法,如果运行后打印出“在唱歌”,说明添加方法成功
    ///例如我们有一个people类,people类中没有任何属性和方法,//我们为之添加一个名为sing的方法
    - (void)viewDidLoad {
    [super viewDidLoad];
    People *people = [[People alloc] init];
    //添加方法
    class_addMethod([People class], @selector(sing), class_getMethodImplementation([self class], @selector(peopleSing)), "v@:");
    //people调用刚添加的方法
    [people performSelector:@selector(sing)];
    }

    - (void)peopleSing
    {
    NSLog(@"在唱歌");
    }

    添加方法主要用到两个runtime函数

    1.添加方法函数


    此函数有四个参数
    第一个参数代表为哪个类添加方法
    第二个参数代表添加的方法名称
    第三个参数已经实现的方法的imp指针

    第四个参数"v@:":v:表示添加方法返回值为void @表示是id类型(也就是要添加的类) :表示添加的方法SEL 如下图


    2.获取方法imp指针


    此函数为获取imp指针,做为第一个函数的参数传入,这个函数有两个参数。
    第一个参数为方法实现所在的类。
    第二个参数为实现的方法的SEL

    原文地址:https://www.jianshu.com/p/795091958f8f
    收起阅读 »

    iOS- Dealloc流程解析 Dealloc 实现原理

    当对象的引用计数为0时, 系统会调用对象的dealloc方法释放- (void)dealloc { _objc_rootDealloc(self);}在内部void_objc_rootDealloc(id obj){ assert(obj); ...
    继续阅读 »

    当对象的引用计数为0时, 系统会调用对象的dealloc方法释放

    - (void)dealloc {
    _objc_rootDealloc(self);
    }

    在内部

    void
    _objc_rootDealloc(id obj)
    {
    assert(obj);
    obj->rootDealloc();
    }

    继续调用了rootDealloc方法

    显然调用顺序为:先调用当前类的dealloc,然后调用父类的dealloc,最后到了NSObject的dealloc.

    inline void
    objc_object::rootDealloc()
    {
    //判断对象是否采用了Tagged Pointer技术
    if (isTaggedPointer()) return; // fixme necessary?
    //判断是否能够进行快速释放
    //这里使用了isa指针里的属性来进行判断.
    if (fastpath(isa.nonpointer && //对象是否采用了优化的isa计数方式
    !isa.weakly_referenced && //对象没有被弱引用
    !isa.has_assoc && //对象没有关联对象
    !isa.has_cxx_dtor && //对象没有自定义的C++析构函数
    !isa.has_sidetable_rc //对象没有用到sideTable来做引用计数
    ))
    {
    //如果以上判断都符合条件,就会调用C函数 free 将对象释放
    assert(!sidetable_present());
    free(this);
    }
    else {
    //如果以上判断没有通过,做下一步处理
    object_dispose((id)this);
    }
    }

    内部做了一些判断, 如果满足这五个条件,直接调用free函数,进行内存释放.

    当一个最简单的类(没有任何成员变量,没有任何引用的类),这五个判断条件都是成立的,直接free.

    id 
    object_dispose(id obj)
    {
    if (!obj) return nil;

    objc_destructInstance(obj);
    free(obj);

    return nil;
    }

    调用objc_destructInstance函数来析构对象obj,再free(obj)释放内存.

    objc_destructInstance内部函数会销毁C++析构函数以及移除关联对象的操作.

    继续调用objc_object的clearDeallocating函数做下一步处理

    objc_object::clearDeallocating()
    {
    if (slowpath(!isa.nonpointer)) {
    // Slow path for raw pointer isa.
    // 如果要释放的对象没有采用了优化过的isa引用计数
    sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
    // Slow path for non-pointer isa with weak refs and/or side table data.
    // 如果要释放的对象采用了优化过的isa引用计数,并且有弱引用或者使用了sideTable的辅助引用计数
    clearDeallocating_slow();
    }
    assert(!sidetable_present());
    }

    根据是否采用了优化过的isa做引用计数分为两种:

    1. 要释放的对象没有采用优化过的isa引用计数:

    会调用sidetable_clearDeallocating() 函数做进一步处理

    void 
    objc_object::sidetable_clearDeallocating()
    {
    // 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    //在散列表SideTable中找到对应的引用计数表RefcountMap,拿到要释放的对象的引用计数
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
    //如果要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
    if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
    weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //从引用计数表中擦除该对象的引用计数
    table.refcnts.erase(it);
    }

    table.unlock();
    }
    1. 如果该对象采用了优化过的isa引用计数

    并且该对象有弱引用或者使用了sideTable的辅助引用计数,就会调用clearDeallocating_slow()函数做进一步处理.

    NEVER_INLINE void

    objc_object::clearDeallocating_slow()

    {
    assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

    // 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
    //要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
    weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //使用了sideTable的辅助引用计数,直接在SideTable中擦除该对象的引用计数
    if (isa.has_sidetable_rc) {
    table.refcnts.erase(this);
    }
    table.unlock();
    }

    以上两种情况都涉及weak_clear_no_lock函数, 它的作用就是将被弱引用对象的弱引用指针置为nil.

    void 

    weak_clear_no_lock(weak_table_t *weak_table, id referent_id)

    {
    //获取被弱引用对象的地址
    objc_object *referent = (objc_object *)referent_id;
    // 根据对象地址找到被弱引用对象referent在weak_table中对应的weak_entry_t
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
    /// XXX shouldn't happen, but does with mismatched CF/objc
    //printf("XXX no entry for clear deallocating %p\n", referent);
    return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;

    // 找出弱引用该对象的所有weak指针地址数组
    if (entry->out_of_line()) {
    referrers = entry->referrers;
    count = TABLE_SIZE(entry);
    }
    else {
    referrers = entry->inline_referrers;
    count = WEAK_INLINE_COUNT;
    }
    // 遍历取出每个weak指针的地址
    for (size_t i = 0; i < count; ++i) {
    objc_object **referrer = referrers[i];
    if (referrer) {
    // 如果weak指针确实弱引用了对象 referent,则将weak指针设置为nil
    if (*referrer == referent) {
    *referrer = nil;
    }
    // 如果所存储的weak指针没有弱引用对象 referent,这可能是由于runtime代码的逻辑错误引起的,报错
    else if (*referrer) {
    _objc_inform("__weak variable at %p holds %p instead of %p. "
    "This is probably incorrect use of "
    "objc_storeWeak() and objc_loadWeak(). "
    "Break on objc_weak_error to debug.\n",
    referrer, (void*)*referrer, (void*)referent);
    objc_weak_error();
    }
    }
    }
    weak_entry_remove(weak_table, entry);
    }

    这里也表明了为什么被weak修饰的对象在释放时, 所有弱引用该对象的指针都被设置为nil.
    dealloc整个方法释放流程如下图:


    看流程图发现,如果五个条件不满足.内存无法进行快速释放.在上面中,我看到博客里关于 objc_destructInstance 这个方法只是概述而过,所以我找了相关资料来了解一下.

    void *objc_destructInstance(id obj) 
    {
    if (obj) {
    Class isa_gen = _object_getClass(obj);
    class_t *isa = newcls(isa_gen);


    // Read all of the flags at once for performance.
    bool cxx = hasCxxStructors(isa);
    bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);


    // This order is important.
    if (cxx) object_cxxDestruct(obj);
    if (assoc) _object_remove_assocations(obj);

    if (!UseGC) objc_clear_deallocating(obj);
    }
    return obj;
    }

    总共干了三件事::

    1. 执行了object_cxxDestruct 函数
    2. 执行_object_remove_assocations,去除了关联对象.(这也是为什么category添加属性时,在释放时没有必要remove)
    3. 就是上面写的那个,清空引用计数表并清除弱引用表,将weak指针置为nil
      object_cxxDestruct是由编译器生成,这个方法原本是为了++对象析构,ARC借用了这个方法插入代码实现了自动内存释放的工作.

    这个释放.

    现象:

    1. 当类拥有实例变量时,这个方法会出现,且父类的实例变量不会导致子类拥有这个方法.
    2. 出现这个方法和变量是否被赋值,赋值成什么没有关系.

    所以, 我们可以认为这个方法就是用来释放该类中的属性的. weak修饰的属性应该不包含在内。



    摘自链接:https://www.jianshu.com/p/b25f50d852f2
    收起阅读 »

    iOS- weak 原理

    一、weak 基本用法weak 是弱引用,用 weak 来修饰、描述所引用对象的计数器并不会增加,而且 weak 会在引用对象被释放的时候自动置为 nil,这也就避免了野指针访问坏内存而引起奔溃的情况,另外 weak 也可以解决循环引用。拓展:为什么修饰代理使...
    继续阅读 »

    一、weak 基本用法

    weak 是弱引用,用 weak 来修饰、描述所引用对象的计数器并不会增加,而且 weak 会在引用对象被释放的时候自动置为 nil,这也就避免了野指针访问坏内存而引起奔溃的情况,另外 weak 也可以解决循环引用。

    拓展:为什么修饰代理使用 weak 而不是用 assign?
    assign 可用来修饰基本数据类型,也可修饰 OC 的对象,但如果用 assign 修饰对象类型指向的是一个强指针,当指向的这个指针释放之后,它仍指向这块内存,必须要手动给置为 nil,否则会产生野指针,如果还通过此指针操作那块内存,会导致 EXC_BAD_ACCESS 错误,调用了已经被释放的内存空间;而 weak 只能用来修饰 OC 对象,而且相比 assign 比较安全,如果指向的对象消失了,那么它会自动置为 nil,不会导致野指针

    二、weak 原理概括

    weak 表其实是一个哈希表,key 是所指对象的指针,value 是 weak 指针的地址数组。(value 是数组的原因是:因为一个对象可能被多个弱引用指针指向)

    Runtime 维护了一张 weak 表,用来存储某个对象的所有的 weak 指针。

    weak 原理实现过程三步骤

    初始化开始时,会调用 objc_initWeak 函数,初始化新的 weak 指针指向对象的地址。


    然后 objc_initWeak 函数里面会调用 objc_storeWeak() 函数,objc_storeWeak() 函数的作用是用来更新指针的指向,创建弱引用表。


    在最后会调用 clearDeallocating 函数。而clearDeallocating 函数首先根据对象的地址获取 weak 指针地址的数组,然后紧接着遍历这个数组,将其中的数组开始置为 nil,把这个 entry 从 weak 表中删除,最后一步清理对象的记录。

    初始化开始时,会调用 objc_initWeak 函数,初始化新的 weak 指针指向对象的地址。当我们初始化 weak 变量时,runtime 会调用 NSObject.mm 中的 objc_initWeak,而 objc_initWeak 函数里面的实现如下:
    id objc_initWeak(id *location, id newObj) {
    // 查看对象实例是否有效,无效对象直接导致指针释放
    if (!newObj) {
    *location = nil;
    return nil;
    }
    // 这里传递了三个 Bool 数值
    // 使用 template 进行常量参数传递是为了优化性能
    return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
    (location, (objc_object*)newObj);
    }

    通过上面代码可以看出,objc_initWeak()函数首先判断指针指向的类对象是否有效,无效,直接返回;否则通过 storeWeak() 被注册为一个指向 value 的 _weak 对象

    objc_initWeak 函数里面会调用 objc_storeWeak() 函数,objc_storeWeak() 函数的作用是用来更新指针的指向,创建弱引用表。

    在最后会调用 clearDeallocating 函数。而 clearDeallocating 函数首先根据对象的地址获取 weak 指针地址的数组,然后紧接着遍历这个数组,将其中的数组开始置为 nil,把这个 entry 从 weak 表中删除,最后一步清理对象的记录。

    问:当 weak 指向的对象被释放时,如何让 weak 指针置为 nil 的呢?
    答:
    调用 objc_release
    因为对象的引用计数为0,所以执行 dealloc
    在 dealloc 中,调用了 _objc_rootDealloc 函数
    在 _objc_rootDealloc 中,调用了 object_dispose 函数
    调用 objc_destructInstance
    最后调用 objc_clear_deallocating,详细过程如下:
    a. 从 weak 表中获取废弃对象的地址为键值的记录
    b. 将包含在记录中的所有附有 weak 修饰符变量的地址,赋值为 nil
    c. 将 weak 表中该记录删除
    d. 从引用计数表中删除废弃对象的地址为键值的记录



    摘自链接:https://www.jianshu.com/p/713f7f19d07b
    收起阅读 »

    iOS- Copy和Strong修饰

    情况一(@property (nonatomic,copy)NSString *str;)(@property (nonatomic,strong)NSString *str;)self. str = NSString(实例)@interface ViewCo...
    继续阅读 »

    情况一(@property (nonatomic,copy)NSString *str;)(@property (nonatomic,strong)NSString *str;)self. str = NSString(实例)

    @interface ViewController ()

    @property (nonatomic,copy)NSString *str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *base_str = @"我是";//实例化分配堆内存
    self.str = base_str;//copy对NSString只是指针拷贝(浅拷贝)

    NSLog(@"str--%p+++%@",self.str,self.str);//0x1006a4020+++我是
    NSLog(@"base_str--%p+++%@",base_str,base_str);//0x1006a4020+++我是

    NSLog(@"分割线---------------------------------------------");
    base_str = @"haha";//重新实例化重新分配堆内存(但是对原来的地址不影响)
    NSLog(@"str--%p+++%@",self.str,self.str);//0x1006a4020+++我是
    NSLog(@"base_str--%p+++%@",base_str,base_str);//0x1006a40a0+++haha

    }
    2021-03-22 16:22:42.509744+0800 IOS--多继承[36010:335669] str--0x1006a4020+++我是
    2021-03-22 16:22:42.509955+0800 IOS--多继承[36010:335669] base_str--0x1006a4020+++我是
    2021-03-22 16:22:42.510093+0800 IOS--多继承[36010:335669] 分割线---------------------------------------------
    2021-03-22 16:22:42.510221+0800 IOS--多继承[36010:335669] str--0x1006a4020+++我是
    2021-03-22 16:22:42.510330+0800 IOS--多继承[36010:335669] base_str--0x1006a40a0+++haha

    情况二(@property (nonatomic,copy)NSString *str;)self. str = NSMutableString(实例)

    @interface ViewController ()

    @property (nonatomic,copy)NSString *str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *m_str = [NSMutableString stringWithString:@"nihao"];
    self.str = m_str;//copy对NSMutableString生成了新的地址(深拷贝)

    NSLog(@"str--%p+++%@",self.str,self.str);//0xbe2d07ae3dfa791b+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000558870+++nihao

    NSLog(@"分割线---------------------------------------------");
    [m_str appendFormat:@"修改后"];
    NSLog(@"str--%p+++%@",self.str,self.str);//0xdb33f1772ec1e5d1+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000724db0+++nihao修改后

    }

    情况三(@property (nonatomic,strong)NSString *str;)self. str = NSMutableString(实例)

    @interface ViewController ()

    @property (nonatomic,strong)NSString *str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *m_str = [NSMutableString stringWithString:@"nihao"];
    self.str = m_str;//strong对NSMutableString没有生成了新的地址(浅拷贝)

    NSLog(@"str--%p+++%@",self.str,self.str);//0xbe2d07ae3dfa791b+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000558870+++nihao

    NSLog(@"分割线---------------------------------------------");
    [m_str appendFormat:@"修改后"];
    NSLog(@"str--%p+++%@",self.str,self.str);//0xdb33f1772ec1e5d1+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000724db0+++nihao修改后

    }
    2021-03-22 16:39:20.728281+0800 IOS--多继承[36287:351536] str--0x60000235e3d0+++nihao
    2021-03-22 16:39:20.728446+0800 IOS--多继承[36287:351536] m_str--0x60000235e3d0+++nihao
    2021-03-22 16:39:20.728574+0800 IOS--多继承[36287:351536] 分割线---------------------------------------------
    2021-03-22 16:39:20.728697+0800 IOS--多继承[36287:351536] str--0x60000235e3d0+++nihao修改后
    2021-03-22 16:39:20.728811+0800 IOS--多继承[36287:351536] m_str--0x60000235e3d0+++nihao修改后

    情况四(@property (nonatomic,strong)NSMutableString *m_str;)self.m_str = NSString

    @interface ViewController ()

    @property (nonatomic,strong)NSMutableString *m_str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *str = @"nihao";
    self.m_str = str;//strong对str只是引用计数+1(此时self.m_str还是不可变NSString)

    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    NSLog(@"分割线---------------------------------------------");
    str = @"修改后";
    [self.m_str appendFormat:@"修改"];//(编译能通过,运行时候Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendFormat:'*** First throw call stack:)
    //因为appendFormat是NSMutableString的方法
    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    }

    @interface ViewController ()

    @property (nonatomic,copy)NSMutableString *m_str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *str = [NSMutableString stringWithString:@"nihao"];
    self.m_str = str;//strong对str只是引用计数+1(此时self.m_str还是不可变NSString)

    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    NSLog(@"分割线---------------------------------------------");
    [self.m_str appendFormat:@"修改"];//(编译能通过,运行时候Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendFormat:'*** First throw call stack:)
    //因为appendFormat是NSMutableString的方法
    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    }


    当使用 strong 修饰属性的时候,属性的setter方法会直接强引用该对象,这样,当原object对象的值发生改变时,新对象的属性也改变;

    但是对于可变对象类型,如NSMutableString、NSMutableArray等则不可以使用copy修饰,因为Foundation框架提供的这些类都实现了NSCopying协议,使用copy方法返回的都是不可变对象,如果使用copy修饰符在对可变对象赋值时则会获取一个不可变对象,接下来如果对这个对象进行可变对象的操作则会产生异常,因为OC没有提供mutableCopy修饰符,对于可变对象使用strong修饰符即可。

    总结:

    针对不可变对象使用copy修饰,针对可变对象使用strong修饰。

    收起阅读 »

    Git 操作整理

    git 使用一般企业中使用代码管理工具Git开发时都是通过拉分支进行功能细致开发,所以掌握git的分支操作时必要的。使用git下载指定分支命令为: git clone-b分支名仓库地址。初始开发git操作流程:本地创建公钥:ssh-keygen-t ...
    继续阅读 »

    git 使用

    一般企业中使用代码管理工具Git开发时都是通过拉分支进行功能细致开发,所以掌握git的分支操作时必要的。
    使用git下载指定分支命令为: git clone-b分支名仓库地址
    初始开发git操作流程:

    本地创建公钥:ssh-keygen-t rsa-C"邮箱"并配置

    克隆最新主分支项目代码: git clone地址

    创建本地分支: git branch分支名

    查看本地分支:git branch

    查看远程分支: git branch-a

    切换分支: git checkout分支名(一般修改未提交则无法切换,大小写问题经常会有,可强制切换 git checkout分支名-f非必须慎用)

    将本地分支推送到远程分支:git push<远程仓库><本地分支>:<远程分支>

    必备知识点


    概念:
    1. Remote:远程主仓库

    2. Repository:本地仓库

    3. Index:Git追踪树,暂存区

    4. workspace:本地工作区(即你编辑器的代码)

    一般操作流程:《工作区》-> git status查看状态 -> git add.将所有修改加入暂存区-> git commit-m"提交描述"将代码提交到本地仓库-> git push将本地仓库代码更新到远程仓库。

    一、git remote

    为远程仓库指定别名,以便于管理远程主机,默认只有一个时为origin。

    1、查看主机名: git remote

    2、查看主机名即网址: git remote-v

    默认克隆远程仓库到本地时,远程主机为origin,如需指定别名可使用 git clone-o<别名><远程git地址>

    3、查看主机的详细信息: git remote show<主机名>

    4、添加远程主机: git remote add<主机名><网址>

    5、删除远程主机: git remote rm<主机名>

    6、修改远程主机的别名: git remote rename<原主机名><新主机名>

    二、git fetch

    将某个远程主机的更新,全部/分支 取回本地(此时之更新了Repository)它取回的代码对你本地的开发代码没有影响,如需彻底更新需合并或使用 git pull

    1. 远程主机的更新,全部取回本地: git fetch<远程主机名>

    2. 将远程仓库特定分支更新到本地: git fetch<远程主机名><分支名>

    如果需要将更新拉取但本地工作代码需要合并到本地某一分支: git merge<被合并的远程分支>,或者在此基础上创建出新分支并切换: git checkout-b<分支名><在此分支上创建>

    三、git pull

    拉取远程主机某分支的更新,再与本地的指定分支合并(相当与fetch加上了合并分支功能的操作)。

    1. 拉取远程某分支并与本地某一分支合并(没有则默认会创建): git pull<远程主机名><远程分支名>:<本地分支名>

    2. 如果远程分支是与当前所在分支合并,则冒号后面的部分可以省略: git pull<远程主机名><远程分支名>

    3. 如果当前分支与远程分支存在追踪关系,则可以省略远程分支名: git pull<远程主机名>

    4. 如果当前分支只有一个追踪分支,则远程主机名都可以省略: git pull

    四、git push

    将本地分支的更新,推送到远程主机,其命令格式与 git pull相似。

    1、将本地分支推送到远程分支: git push<远程主机名><本地分支名>:<远程分支名>

    2、如果省略远程分支名,则默认为将本地分支推送到与之关联的远程分支:(一般设置本地分支和与之关联的远程分支同名,防止混淆) git push<远程主机名><本地分支名>

    如果对应的远程分支不存在,则会被创建(m默认与本地分支同名)。

    3、如果省略本地分支名,则表示删除指定的远程分支,这等同于推送一个空的本地分支到对应远程分支: git push origin:<远程分支> 等同于 git push origin--delete<远程分支>

    4、如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略 git push origin

    5、如果当前分支只有一个追踪分支,那么主机名也可以省略: git push

    6、如果当前分支与多个主机存在追踪关系(使用场景相对来说较少),可以使用 -u 指定默认推送主机: git push-u origin<主机名>,设置时候需推送便可以直接使用 git push

    7、将本地的所有分支都推送到远程主机: git push--all origin

    8、如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做 git pull合并差异,然后再推送到远程主机。如果一定要推送,可以使用 --force选项(谨慎使用,除非你非常确认): git push--force origin

    注意:分支推送顺序的格式为<来源地>:<目的地>,所以 git pull格式:<远程分支>:<本地分支>, git push格式为:<本地分支>:<远程分支>

    五、分支操作

    1、创建本地分支: git branch test:(创建名为test的本地分支)。

    2、切换分支: git checkout test:(切换到test分支)。

    3、创建并切换分支 git branch-b test:(相当于以上两条命令的合并)。

    4、查看本地分支: git branch

    5、查看远程仓库所有分支: git branch-a

    6、删除本地分支: git branch-d test:(删除本地test分支)。

    7、分支合并: git merge master:(将master分支合并到当前分支)。

    8、本地分支重命名: git branch-m oldName newName

    9、远程分支重命名:

    1. 重命名远程分支对应的本地分支: git branch-m oldName newName;

    2. 删除远程分支: git push--deleteorigin oldName;

    3. 上传新命名的本地分支: git push origin newName;

    4. 把修改后的本地分支与远程分支关联: git branch--set-upstream-to origin/newName

    10、分支关联:

    查看当前的本地分支与远程分支的关联关系: git branch-vv


    把当前本地分支与远程origin的某分支进行关联处理(通过 --set-upstream-to 命令): git branch--set-upstream-to=origin/feature/clear-server-eslint-error_180713
    11、分支差异查看:

    查看本地当前分支与远程某一分支的差异: git diff origin/feature/reserve-3.4

    查看本地特定分支与远程分支的差异: git diff master origin/feature/reserve-3.4(查看本地master分支与远程feature/reserve-3.4分支的差异),如图:


    六、修改撤销

    1、 git checkout--<文件名>:丢弃工作区的修改,就是让这个文件回到最近一次 git commit或 git add时的状态。

    2、 git reset HEAD<文件名>:把暂存区的修改撤销掉(unstage),重新放回工作区。

    3、 git reset--hard commit_id:git版本回退,回退到特定的commit_id版本。

    流程: git log查看提交历史,以便确定要回退到哪个版本(commit 之后的即为ID)。


    4、 git reset--hard commit_id:回退到commit_id版本。

    5、 git reflog查看命令历史,以便确定要回到未来的哪个版本。更新远程代码到本地:

    • git fetch origin master(分支)

    • git pull// 将fetch下来的代码pull到本地

    • git diff master origin/master// 查看本地分支代码和远程仓库的差异

    6、拉取远程分支并创建本地分支:

    1. git checkout-b本地分支名origin/远程分支名:使用此方式会在本地新建分支,并自动切换到该本地分支;

    2. git fetch origin远程分支名:本地分支名:使用此方式会在本地新建分支,但是不会自动切换到该本地分支,需要手动checkout。

    七、配置

    1、 git config-l// 陈列出所有的git配置项。

    2、 git config core.ignorecasefalse //配置git不忽略大小写(默认忽略)参照(git 大小写)。


    原贴链接:https://www.jianshu.com/p/80252c51a70f

    收起阅读 »

    iOS------OpenGL 图形专有名词与坐标解析

    一.OpenGL简介OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操...
    继续阅读 »

    一.OpenGL简介

    OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D3D矢量图形的跨语言跨平台应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操作抽象为⼀个个的OpenGL指令,开发者可以在mac程序中使用OpenGl来实现图形渲染。图形API的目的就是实现图形的底层渲染,比如游戏场景/游戏人物的渲染,音视频解码后数据的渲染,地图上的渲染,动画绘制等。在iOS开发中,开发者唯一能够GPU的就是图形API。(GPU---图形处理器(英语:Graphics Processing Unit,缩写:GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑工作站、游戏机和一些移动设备(如平板电脑智能手机等)上做图像和图形相关运算工作的微处理器。)

    二.OpenGL专业名词解析

        1.OpenGL 上下⽂( context )

            OpenGL Context,中文解释就是OpenGL的上下文,因为OpenGL没有窗口的支持,我们在使用OpenGl的时候,一般是在main函数创建窗口: 

            //GLUT窗口大小、窗口标题

            glutInitWindowSize(800, 600);

            glutCreateWindow("Triangle");

            然后我们在创建的窗口里面绘制,个人理解上下文的意思就是指的是OpenGL的作用范围,当然OpenGL的Context不只是这个窗口,这个窗口我们可以理解为OpenGL的default framebuffer,所以Context还包含关于这个framebuffer的一些参数设置信息,具体内容可以查看OpenGL的Context的结构体,Context记录了OpenGL渲染需要的所有信息,它是一个大的结构体,它里面记录了当前绘制使用的颜色、是否有光照计算以及开启的光源等非常多我们使用OpenGL函数调用设置的状态和状态属性等等,你可以把它理解为是一个巨大的状态机,它里面保存OpenGl的指令,在图形渲染的时候,可以理解为这个状态机开始工作了,对某个属性或者开关发出指令。它的特点就是:有记忆功能,接收输入,根据输入的指令,修改当前的状态,并且可以输出内容,当停机的时候不再接收指令。

    2.渲染

            渲染就是把数据显示到屏幕上,在OpenGl中,渲染指的是将图形/图像数据转换为2D空间图像操作叫渲染

    3.顶点数组/顶点缓冲区

            在OpenGL中,基本图元有三种:点,线,三角形,复杂的图形由这三种图元组成,我们在画点/线/三角形的时候是不是应该先知道每个顶点的坐标,而这些坐标放在数组里,就叫顶点数组。顶点数组存在内存当中,但是为了提高性能,提前分配一块显存,将顶点数组预先存入到显存当中,这部分的显存就叫顶点缓冲区。

    4.着色器(shader)

            为什么要使用着色器?我们知道,OpenGL一般使用经典的固定渲染管线来渲染对象,OpenGL在实际调⽤绘制函数之前,还需指定⼀个由shader编译成的着⾊器程序。常⻅的着⾊器主要有顶点着⾊器(VertexShader),⽚段着⾊器(FragmentShader)/像素着⾊器(PixelShader),⼏何着⾊器(GeometryShader),曲⾯细分着⾊器(TessellationShader)。⽚段着⾊器和像素着⾊器只是在OpenGL和DX中的不同叫法⽽已。可惜的是,直到OpenGLES 3.0,依然只⽀持了顶点着⾊器和⽚段着⾊器这两个最基础的着⾊器。OpenGL在处理shader时,和其他编译器⼀样。通过编译、链接等步骤,⽣成了着⾊器程序(glProgram),着⾊器程序同时包含了顶点着⾊器和⽚段着⾊器的运算逻辑。在OpenGL进⾏绘制的时候,⾸先由顶点着⾊器对传⼊的顶点数据进⾏运算。再通过图元装配,将顶点转换为图元。然后进⾏光栅化,将图元这种⽮量图形,转换为栅格化数据。最后,将栅格化数据传⼊⽚段着⾊器中进⾏运算。⽚段着⾊器会对栅格化数据中的每⼀个像素进⾏运算,并决定像素的颜⾊(顶点着色器和片段/片元着色器会在下面讲解)

    5.管线

            OpenGL在渲染图形/图像的时候是按照特定的顺序来执行的,不能修改打破,管线的意思个人理解是读取顶点数据—>顶点着色器—>组装图元—>光栅化图元—>片元着色器—>写入帧缓冲区—>显示到屏幕上,类似这样的流水线,当图像/图形显示到屏幕上,这一条管线完成工作。下面是分步讲解:

           (1)读取顶点数据指的是将待绘制的图形的顶点数据传递给渲染管线中。

            (2)顶点着色器最终生成每个定点的最终位置,执行顶点的各种变换,它会针对每个顶点执行一次,确定了最终位置后,OpenGL就可以把这些顶点集合按照给定的参数类型组装成点,线或者三角形。


         (3)组装图元阶段包括两部分:图元的组装和图元处理,图元组装指的是顶点数据根据设置的绘制方式参数结合成完整的图元,例如点绘制方式中每个图元就只包含一个点,线段绘制方式中每个图源包含两个点;图元处理主要是剪裁以使得图元位于视景体内部的部分传递到下一个步骤,视景体外部的部分进行剪裁。视景体的概念与投影有关。

          (4)光栅化图元主要指的是将一个图元离散化成可显示的二维单元片段,这些小单元称为片元。一个片元对应了屏幕上的一个或多个像素,片元包括了位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。

          (5)片元着色器为每个片元生成最终的颜色,针对每个片元都会执行一次。一旦每个片元的颜色确定了,OpenGL就会把它们写入到帧缓冲区中。


    6.顶点着色器

             • ⼀般⽤来处理图形每个顶点变换(旋转/平移/投影等)                     

            • 顶点着⾊器是OpenGL中⽤于计算顶点属性的程序。顶点着⾊器是逐顶点运算的程序,也就是说每个顶点数据都会执⾏⼀次顶点着⾊器,当然这是并⾏的,并且顶点着⾊器运算过程中⽆法访问其他顶点的数据

            • ⼀般来说典型的需要计算的顶点属性主要包括顶点坐标变换、逐顶点光照运算等等。顶点坐标由⾃身坐标系转换到归⼀化坐标系的运算,就是在这⾥发⽣的。

    7.片元着色器(片段着色器)

            ⼀般⽤来处理图形中每个像素点颜⾊计算和填充⽚段着⾊器是OpenGL中⽤于计算⽚段(像素)颜⾊的程序。⽚段着⾊器是逐像素运算的程序,也就是说每个像素都会执⾏⼀次⽚段着⾊器,当然也是并⾏的

    8.光栅化Rasterization 

            • 是把顶点数据转换为⽚元的过程,具有将图转化为⼀个个栅格组成的图象的作⽤,特点是每个元素对应帧缓冲区中的⼀像素。

            • 光栅化就是把顶点数据转换为⽚元的过程。⽚元中的每⼀个元素对应于帧缓冲区中的⼀个像素。

            • 光栅化其实是⼀种将⼏何图元变为⼆维图像的过程。该过程包含了两部分的⼯作。第⼀部分⼯作:决定窗⼝坐标中的哪些整型栅格区域被基本图元占⽤;第⼆部分⼯作:分配⼀个颜⾊值和⼀个深度值到各个区域。光栅化过程产⽣的是⽚元

            • 把物体的数学描述以及与物体相关的颜⾊信息转换为屏幕上⽤于对应位置的像素及⽤于填充像素的颜⾊,这个过程称为光栅化,这是⼀个将模拟信号转化为离散信号的过程

    9.纹理

            纹理可以理解为图⽚. ⼤家在渲染图形时需要在其编码填充图⽚,为了使得场景更加逼真.⽽这⾥使⽤的图⽚,就是常说的纹理.但是在OpenGL,我们更加习惯叫纹理,⽽不是图⽚

    10.混合(Blending)

            在测试阶段之后,如果像素依然没有被剔除,那么像素的颜⾊将会和帧缓冲区中颜⾊附着上的颜⾊进⾏混合,混合的算法可以通过OpenGL的函数进⾏指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,⼀般可以通过像素着⾊器进⾏实现,当然性能会⽐原⽣的混合算法差⼀些,个人理解有点像iOS给RGB中红,绿,蓝设置不同的值得到不同的颜色,只是这里是操作片元着色器,来达到不同的显示。

    11.变换矩阵(Transformation)/投影矩阵Projection 

            在iOS核心动画中我们也会和矩阵打交到,变换矩阵顾名思义就是对图像/图形的放大/缩小/平移/选装等座处理。

            投影矩阵就是⽤于将3D坐标转换为⼆维屏幕坐标,实际线条也将在⼆维坐标下进⾏绘制    

    12.渲染上屏/交换缓冲区(SwapBuffer)     

        • 渲染缓冲区⼀般映射的是系统的资源⽐如窗⼝。如果将图像直接渲染到窗⼝对应的渲染缓冲区,则可以将图像显示到屏幕上。

        • 但是,值得注意的是,如果每个窗⼝只有⼀个缓冲区,那么在绘制过程中屏幕进⾏了刷新,窗⼝可能显示出不完整的图像

        • 为了解决这个问题,常规的OpenGL程序⾄少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在⼀个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。

        • 由于显示器的刷新⼀般是逐⾏进⾏的,因此为了防⽌交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换⼀般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进⾏交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步

        • 使⽤了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进⾏下⼀帧的渲染,使得帧率⽆法完全达到硬件允许的最⾼⽔平。为了解决这个问题,引⼊了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,⽽垂直同步发⽣时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利⽤硬件性能的⽬的

    13.坐标系

          OpenGl常见的坐标系有:

            1. Object or model coordinates(物体或模型坐标系)每一个实物都有自己的坐标系,在高中数学中,以自身建立的坐标系,自身坐标系由世界坐标系平移而来

            2. World coordinates(世界坐标系)个人理解为地球相对自己建立的坐标系,地球上所有生物都处于这个坐标系当中

            3. Eye (or Camera) coordinates(眼(或相机)坐标系)

            4. Normalized device coordinates(标准化的设备坐标系)

            5. Window (or screen) coordinates(.窗口(或屏幕)坐标系)个人理解为iOS下

            6.Clip coordinates(裁剪坐标系)主要作用是当图形/图像超出时,按照这个坐标系裁剪,裁剪好之后转换到screen坐标系

    14.正投影/透视投影

            正投影:类似于照镜子,1:1形成图形大小,这里不做重点讲解

            透视投影:在OpenGL中,如果想对模型进行操作,就要对这个模型的状态(当前的矩阵)乘上这个操作对应的一个矩阵.如果乘以变换矩阵(平移, 缩放, 旋转), 那相乘之后, 模型的位置被变换;如果乘以投影矩阵(将3D物体投影到2D平面), 相乘后, 模型的投影方式被设置;如果乘以纹理矩阵(), 模型的纹理方式被设置.而用来指定乘以什么类型的矩阵, 就是glMatriMode(GLenummode);glMatrixMode有3种模式: GL_PROJECTION 投影, GL_MODELVIEW 模型视图, GL_TEXTURE 纹理.所以,在操作投影矩阵以前,需要调用函数:glMatrixMode(GL_PROJECTION); //将当前矩阵指定为投影矩阵然后把矩阵设为单位矩阵




    作者:枫紫_6174
    链接:https://www.jianshu.com/p/03d3a5ab2db0






    收起阅读 »

    iOS Category---为什么只能加方法不能加属性

    一.面试题            相信大家在面试的时候经常会被问到Category的实现原理,以及Category为什么只能加方法不能加属性?个人理解这个问题本...
    继续阅读 »

    一.面试题

                相信大家在面试的时候经常会被问到Category的实现原理,以及Category为什么只能加方法不能加属性?个人理解这个问题本身问的就有问题,首先我们看分类的底层代码

                            struct category_t {

                                        const char *name;

                                        classref_t cls;

                                        struct method_list_t *instanceMethods; // 对象方法

                                        struct method_list_t *classMethods; // 类方法

                                        struct protocol_list_t *protocols; // 协议

                                        struct property_list_t *instanceProperties; // 属性

                                        // Fields below this point are not always present on disk.

                                        struct property_list_t *_classProperties;

                                        method_list_t*methodsForMeta(boolisMeta) {

                                    if(isMeta)returnclassMethods;

                                                elsereturninstanceMethods;

                                }

                            property_list_t*propertiesForMeta(boolisMeta,structheader_info *hi);

                        };

            通过上面的分类底层代码我们可以找到category_t 结构体,它里面包含了对象方法,类方法,协议,属性,既然分类的底层代码里面已经包含了属性,为什么我们面试的时候会被问到分类为什么不能添加属性?下面我们来揭开它的神秘面纱

            1.首先我们创建一个person类,再给penson类创建一个分类Person+TCText,在分类的.h文件




    这么写工程是不会报任何错误,给我们一种表面上其实是可以添加属性的,写上一个属性,系统会自动帮我们生成setter和getter方法,在分类里面写属性或者成员变量,系统只会帮我们做的一件事情就是它只会声明我们的setter和getter方法,不会帮我们实现,上面的这个属性等同于我们在分类里面写




    无论上面何种写法,我们在ViewController都能访问TCName这个属性


    但是,当我们在分类里面重写settet或者getter的时候,它就会出现:



    为什么什么会报错了?因为分类的属性,系统不会自动帮我们生成_TCName(相关属性赋值可以看苹果官网API,这里不做解释),在这里我们如果写:




    表面看上去不会报错,但是当外部访问TCName的时候,就会发现:



    程序一旦运行起来,就会因为递归,程序闪退。

    总结:不是说分类不能添加属性,是因为分类可以添加属性,但是由于系统不会自动帮分类的属性实现getter和setter方法,也不会帮其生成_TCName,无论你重写settet或者getter还是,你不能通过self.TCName去访问属性,重写了setter,这么访问就会发生递归,直接导致程序闪退。所以下次你该知道怎么回答面试官的问题了吧!!!



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/49d131c61348








    收起阅读 »

    iOS Metal语言规范深入

    一.函数修饰符Metal 有以下3种函数修饰符:        1)kernel , 表示该函数是⼀个数据并⾏计算着⾊函数. 它可以被分配在⼀维/⼆维/三维线程组中去执⾏      &nbs...
    继续阅读 »

    一.函数修饰符

    Metal 有以下3种函数修饰符:

            1)kernel , 表示该函数是⼀个数据并⾏计算着⾊函数. 它可以被分配在⼀维/⼆维/三维线程组中去执⾏

            2)vertex , 表示该函数是⼀个顶点着⾊函数 , 它将为顶点数据流中的每个顶点数据执⾏⼀次然后为每个顶 点⽣成数据输出到绘制管线

            3)fragment , 表示该函数是⼀个⽚元着⾊函数, 它将为⽚元数据流中的每个⽚元 和其关联执⾏⼀次然后 将每个⽚元⽣成的颜⾊数据输出到绘制管线中; 

    eg:

    //1.并行计算函数(kernel)

    kernelvoidCCTestKernelFunctionA(inta,intb)

    {

        /*

         注意:

         1. 使用kernel 修饰的函数返回值必须是void 类型

         2. 一个被函数修饰符修饰过的函数,不允许在调用其他的被函数修饰过的函数. 非法

         3. 被函数修饰符修饰过的函数,只允许在客户端对齐进行操作. 不允许被普通的函数调用.

         */

        //不可以的!

        //一个被函数修饰符修饰过的函数,不允许在调用其他的被函数修饰过的函数. 非法

        CCTestKernelFunctionB(1,2);//非法

        CCTestVertexFunctionB(1,2);//非法

        //可以! 你可以调用普通函数.而且在Metal 不仅仅只有这3种被修饰过的函数.普通函数也可以存在

        CCTest();

    }

    kernelvoidCCTestKernelFunctionB(inta,intb)

    {

    }

    //顶点函数

    vertexintCCTestVertexFunctionB(inta,intb){

    }

    //片元函数

    fragmentintCCTestVertexFunctionB(inta,intb){

    }

    //普通函数

    voidCCTest()

    {

    }

            说明:使⽤kernel 修饰的函数. 其返回值类型必须是void 类型; 只有图形着⾊函数才可以被 vertex 和 fragment 修饰. 对于图形着⾊函数, 返回值类型可以辨认出它是为 顶点做计算还是为每像素做计算. 图形着⾊函数的返回值可以为 void , 但是这也就意味着该函数不产⽣数 据输出到绘制管线; 这是⼀个⽆意义的动作; ⼀个被函数修饰符修饰的函数不能在调⽤其他也被函数修饰符修饰的函数; 这样会导致编译失败;

    二.⽤于变量或者参数的地址空间修饰符

            Metal 着⾊器语⾔使⽤ 地址空间修饰符 来表示⼀个函数变量或者参数变量 被分配于那⼀⽚内存区域. 所有的着⾊函数(vertex, fragment, kernel)的参数,如果是指针或是引⽤, 都必须带有地址空间修饰符号; 

            1) device: 设备地址空间

            Device Address Space(设备地址空间) ,在设备地址空间(Device) 指向设备内存池分配出来的缓存对象, 它是可读也是可写的; ⼀个缓存对象可 以被声明成⼀个标量,向量或是⽤户⾃定义结构体的指针或是引⽤. 

    eg:

            // 设备地址空间: device 用来修饰指针.引用

            //1.修饰指针变量

            device float4 *color;

            structCCStruct{

                floata[3];

                intb[2];

            };

            //2.修饰结构体类的指针变量

            device  CCStruct*my_CS;

            注意: 纹理对象总是在设备地址空间分配内存, device 地址空间修饰符不必出现在纹理类型定义中. ⼀个纹 理对象的内容⽆法直接访问. Metal 提供读写纹理的内建函数; 

            2)threadgroup: 线程组地址空间

            线程组地址空间⽤于为 并⾏计算着⾊函数分配内存变量. 这些变量被⼀个线程组的所有线程共享. 在线 程组地址空间分配的变量不能被⽤于图形绘制着⾊函数[顶点着⾊函数, ⽚元着⾊函数] 在并⾏计算着⾊函数中, 在线程组地址空间分配的变量为⼀个线程组使⽤, 声明周期和线程组相同; 

    eg:

            /*

             1. threadgroup 被并行计算计算分配内存变量, 这些变量被一个线程组的所有线程共享. 在线程组分配变量不能被用于图像绘制.

             2. thread 指向每个线程准备的地址空间. 在其他线程是不可见切不可用的

             */

            kernel void CCTestFouncitionF(threadgroup float *a)

            {

                //在线程组地址空间分配一个浮点类型变量x

                threadgroup float x;

                //在线程组地址空间分配一个10个浮点类型数的数组y;

                threadgroup float y[10];

            }

            constant float sampler[] = {1.0f,2.0f,3.0f,4.0f};

            kernel  void  CCTestFouncitionG(void)

            {

                //在线程空间分配空间给x,p

                float  x;

                thread  float  p = &x;

    }

           3) constant 常量地址空间

            常量地址空间指向的缓存对象也是从设备内存池分配存储, 但是它是只读的; 在程序域的变量必须定义在常量地址空间并且声明的时候初始化; ⽤来初始化的值必须是编译时的常 量. 在程序域的变量的⽣命周期和程序⼀样, 在程序中的并⾏计算着⾊函数或者图形绘制着⾊函数调⽤, 但 是constant 的值会保持不变; 

            注意: 常量地址空间的指针或是引⽤可以作为函数的参数. 向声明为常量的变量赋值会产⽣编译错误. 声明常量但是没有赋予初值也会产⽣编译错误; 

    eg:

            1 constant float samples[] = { 1.0f, 2.0f, 3.0f, 4.0f }; 

            2 //对⼀个常量地址空间的变量进⾏修改也会失败,因为它只读的 

            3 sampler[4] = {3,3,3,3}; //编译失败; 

            4 //定义为常量地址空间声明时不赋初值也会编译失败 

            5 constant float a; 

            4)thread 线程地址空间

            thread 地址空间指向每个线程准备的地址空间, 这个线程的地址空间定义的变量在其他线程不可⻅, 在 图形绘制着⾊函数或者并⾏计算着⾊函数中声明的变量thread 地址空间分配; 在图形绘制着色函数 或者 并行计算着色函数中声明的变量,在线程地址空间分配存储

    eg:

            kernel void CCTestFouncitionG(void){

            //在线程空间分配空间给x,p

            float x; 

            thread   float   p=&x;

            }

            对于图形着⾊器函数, 其指针或是引⽤类型的参数必须定义为 device 或是 constant 地址空间; 对于并⾏计算着⾊函数, 其指针或是引⽤类型的参数必须定义为 device 或是 threadgrounp 或是 constant 地址空间; 并不是所有的变量都需要修饰符,也可以定义普通变量(即无修饰符的变量)

    三.函数参数与变量

            图形绘制或者并⾏计算着⾊器函数的输⼊输出都是通过参数传递. 除了常量地址空间变量和程序域定义 的采样器以外.

            device buffer- 设备缓存, ⼀个指向设备地址空间的任意数据类型的指针或者引⽤; 

            constant buffer -常量缓存区, ⼀个指向常量地址空间的任意数据类型的指针或引⽤ 

            texture - 纹理对象; 

            sampler - 采样器对象; 

            threadGrounp - 在线程组中供各线程共享的缓存.

            注意: 被着⾊器函数的缓存(device 和 constant) 不能重名; 

            Attribute Qualifiers to Locate Buffers, Textures, and Samplers ⽤于寻址缓存,纹理, 采样器的属性修饰符;对于每个着⾊器函数来说, ⼀个修饰符是必须指定的. 他⽤来设定⼀个缓存,纹理, 采样器的位置; 

            device buffers/ constant buffer --> [[buffer (index)]] 

            texture -- [[texture (index)]] 

            sampler -- [[sampler (index)]] 

            threadgroup buffer -- [[threadgroup (index)]] 

            index是⼀个unsigned integer类型的值,它表示了⼀个缓存、纹理、采样器参数的位置(在函数参数索引 表中的位置)。 从语法上讲,属性修饰符的声明位置应该位于参数变量名之后 

            例⼦中展示了⼀个简单的并⾏计算着⾊函数 add_vectors,它把两个设备地址空间中的缓存inA和inB相 加,然后把结果写⼊到缓存out。属性修饰符 “(buffer(index))”为着⾊函数参数设定了缓存的位置。

    //属性修饰符

    /*

     1. device buffer(设备缓存)

     2. constant buffer(常量缓存)

     3. texture Object(纹理对象)

     4. sampler Object(采样器对象)

     5. 线程组 threadgroup


     属性修饰符目的:

     1. 参数表示资源如何定位? 可以理解为端口

     2. 在固定管线和可编程管线进行内建变量的传递

     3. 将数据沿着渲染管线从顶点函数传递片元函数.


     在代码中如何表现:

     1.已知条件:device buffer(设备缓存)/constant buffer(常量缓存)

     代码表现:[[buffer(index)]]

     解读:不变的buffer ,index 可以由开发者来指定.


     2.已知条件:texture Object(纹理对象)

     代码表现: [[texture(index)]]

     解读:不变的texture ,index 可以由开发者来指定.


     3.已知条件:sampler Object(采样器对象)

     代码表示: [[sampler(index)]]

     解读:不变的sampler ,index 可以由开发者来指定.


     4.已知条件:threadgroup Object(线程组对象)

     代码表示: [[threadgroup(index)]]

     解读:不变的threadgroup ,index 可以由开发者来指定.

     */

    //并行计算着色器函数add_vectros ,实现2个设备地址空间中的缓存A与缓存B相加.然后将结果写入到缓存out.

    //属性修饰符"(buffer(index))" 为着色函数参数设定了缓存的位置

    //并行计算着色器函数add_vectros ,实现2个设备地址空间中的缓存A与缓存B相加.然后将结果写入到缓存out.

    //属性修饰符"(buffer(index))" 为着色函数参数设定了缓存的位置

    kernelvoidadd_vectros(

                    const device float4*inA [[buffer(0)]],

                    const device float4*inB [[buffer(1)]],

                    device float4*out [[buffer(2)]]

                    uintid[[thread_position_in_grid]])

    {

        out[id] = inA[id] + inB[id];

    }

            注意:thread_position_in_grid : ⽤于表示当前节点在多线程⽹格中的位置; 

    四.内建变量属性修饰符 

            [[vertex_id]] 顶点id 标识符; 

            [[position]] 顶点信息(float4) /� 述了⽚元的窗⼝相对坐标(x, y, z, 1/w) 

            [[point_size]] 点的⼤⼩(float) 

            [[color(m)]] 颜⾊, m编译前得确定; 

            struct MyFragmentOutput { 

             // color attachment 0 

             float4 clr_f [[color(0)]]; // color attachment 1 

             int4 clr_i [[color(1)]]; // color attachment 2 

             uint4 clr_ui [[color(2)]]; }; 

             fragment MyFragmentOutput my_frag_shader( ... ) 

             { 

             MyFragmentOutput f; 

             .... 

             f.clr_f = ...; 

             ... 

             return f;

            }

            [[stage_in]] : ⽚元着⾊函数使⽤的单个⽚元输⼊数据是由顶点着⾊函数输出然后经过光栅化⽣成的.顶 点和⽚元着⾊函数都是只能有⼀个参数被声明为使⽤“stage_in”修饰符,对于⼀个使⽤ 了“stage_in”修饰符的⾃ 定义的结构体,其成员可以为⼀个整形或浮点标量,或是整形或浮点向量



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/a2baf448dd94
    收起阅读 »

    iOS Metal语言规范浅谈

    一.Metal简述            Metal着色器语言是用来编写3D图形渲染逻辑、并行Metal计算核心逻辑的一门编程语言,当你使用Metal...
    继续阅读 »

    一.Metal简述

                Metal着色器语言是用来编写3D图形渲染逻辑、并行Metal计算核心逻辑的一门编程语言,当你使用Metal框架来完成APP的实现时则需要使用Metal编程语言。

                Metal语言使用Clang 和LLVM进行编译处理,编译器对于在GPU上的代码执行效率有更好的控制

                Metal基于C++ 11.0语言设计的,在C++基础上多了一些扩展和限制,主要用来编写在GPU上执行的图像渲染逻辑代码以及通用并行计算逻辑代码

                Metal 像素坐标系统:Metal中纹理 或者 帧缓存区attachment的像素使用的坐标系统的原点是左上角

    1.1Metal 语⾔中不⽀持之处 

            Lambda 表达式;

            递归函数调⽤

            动态转换操作符

            类型识别

            对象创建new 和销毁delete 操作符;

            操作符 noexcept

            goto 跳转

            变量存储修饰符register 和 thread_local;

            虚函数修饰符;

            派⽣类

            异常处理

            C++ 标准库在Metal 语⾔中也不可使⽤;

    1.2Metal 语⾔中对于指针使⽤的限制

            Metal图形和并⾏计算函数⽤到的⼊参数; 如果是指针必须使⽤地址空间修饰符(device,threadgroup,constant)

            不⽀持函数指针;

            函数名不能出现main

    二.Metal的数据类型及语法

    2.1     Metal 数据类型--标量数据类型 

            bool 布尔类型, true/false

            char 有符号8位整数;

            unsigned char /uchar ⽆符号8-bit 整数;

            short 有符号16-bit整数;

            unsigned short / ushort ⽆符号32-bit 整数;

            half 16位bit 浮点数;

            float 32bit 浮点数;

            size_t 64 ⽆符号整数;

            void 该类型表示⼀个空的值集合

    说明:其中half 相当于OC中的float,float 相当于OC中的doublesize_t用来表示内存空间, 相当于 OC中 sizeof

    示例:boola=true;charb=5;intd=15;//用于表示内存空间size_t c=1;ptrdiff_t f=2;

    2.2Metal向量

            向量支持如下类型:- booln、charn、shortn、intn、ucharn、ushortn、uintn、halfn、floatn,其中 n 表示向量的维度,最多不超过4维向量示例:

            //直接赋值初始化

            bool2 A={1,2}

            ;//通过内建函数float4初始化

            float4 pos=float4(1.0,2.0,3.0,4.0);

            //通过下标从向量中获取某个值

            floatx=pos[0];floaty=pos[1];

            //通过for循环对一个向量进行运算

            float4 VB;

            for(inti=0;i<4;i++){

                VB[i]=pos[i]*2.0f;

            }

            说明:在OpenGL ES的GLSL语言中,例如2.0f,在着色器中书写时,是不能加f,写成2.0,而在Metal中则可以写成2.0f,其中f可以是大写,也可以是小写

    向量的访问规则:

    1.通过向量字母获取元素: 向量中的向量字母仅有2种,分别为xyzw、rgba

            int4 test=int4(0,1,2,3);

            inta=test.x; //获取的向量元素0

            intb=test.y; //获取的向量元素1

            intc=test.z; //获取的向量元素2

            intd=test.w;//获取的向量元素3

            inte=test.r; //获取的向量元素0

            intf=test.g;//获取的向量元素1

            intg=test.b; //获取的向量元素2

            inth=test.a; //获取的向量元素3

    2.多个分量同时访问

            float4 c;

            c.xyzw=float4(1.0f,2.0f,3.0f,4.0f);

            c.z=1.0f

            c.xy=float2(3.0f,4.0f);

            c.xyz=float3(3.0f,4.0f,5.0f);

            说明:赋值时分量不可重复,取值时分量可重复右边是取值 和 左边赋值都合法xyzw与rgba不能混合使用,GLSL中向量不能乱序访问,只是和Metal中的向量相似,并不是等价



    2.3矩阵

            矩阵支持如下类型- halfnxm、floatnxm,其中 nxm表示矩阵的行数和列数,最多4行4列,其中half、float相当于OC中的float、double- 普通的矩阵其本质就是一个数组

            float4x4 m;

            //将第二行的所有值都设置为2.0

            m[1]=float4(2.0f);

            //设置第一行/第一列为1.0f

            m[0][0]=1.0f;

            //设置第三行第四列的元素为3.0f

            m[2][3]=3.0f;

            float4 类型向量的构造方式

            1个float构成,表示一行都是这个值

            4个float构成

            2个float2构成

            1个float2+2个float构成(顺序可以任意组合)

            1个float2+1个float

            1个float4

    eg:

            //float4类型向量的所有可能构造方式//1个一维向量,表示一行都是xfloat4(floatx);

            ///4个一维向量 --> 4维向量float4(floatx,floaty,floatz,floatw);

            //2个二维向量 --> 4维向量float4(float2 a,float2 b);

            //1个二维向量+2个一维向量 --> 4维向量float4(float2 a,float b,float c);

            float4(floata,float2 b,floatc);float4(floata,floatb,float2 c);

            //1个三维向量+1个一维向量 --> 4维向量float4(float3 a,floatb);float4(floata,float3 b);

    //1个四维向量 --> 4维向量float4(float4 x);

    float3 类型向量的构造方式

            1个float构成,表示一行都是这个值

            3个float

            1个float+1个float2(顺序可以任意组合)

            1个float2

    eg:

            //float3类型向量的所有可能的构造的方式

            //1个一维向量float3(floatx);

            //3个一维向量float3(floatx,floaty,floatz);

            //1个一维向量 + 1个二维向量float3(floata,float2 b);

            /1个二维向量 + 1个一维向量float3(float2 a,floatb);

            //1个三维向量float3(float3 x);

            float2 类型向量的构造方式

     1个float构成,表示一行都是这个值

            2个float

            1个float2

    eg:

            //float2类型向量的所有可能的构造方式

            //1个一维向量float2(floatx);

            //2个一维向量float2(floatx,floaty);

            //1个二维向量float2(float2 x);

    三,Metal的其他类型


    1.纹理

    纹理类型

    纹理类型是一个句柄,指向一维/二维/三维纹理数据,而纹理数据对应一个纹理的某个level的mipmap的全部或者一部分

    纹理的访问权限

    在一个函数中描述纹理对象的类型

    access枚举值由Metal定义,定义了纹理的访问权利enum class access {sample, read, write};,有以下3种访问权利,当没写access时,默认的access 就是sample

    sample: 纹理对象可以被采样(即使用采样器去纹理中读取数据,相当于OpenGL ES的GLSL中sampler2D),采样一维这时使用 或者 不使用都可以从纹理中读取数据(即可读可写可采样)

    read:不使用采样器,一个图形渲染函数或者一个并行计算函数可以读取纹理对象(即仅可读)

    write:一个图形渲染函数 或者 一个并行计算可以向纹理对象写入数据(即可读可写)

    定义纹理类型

    描述一个纹理对象/类型,有以下三种方式,分别对应一维/二维/三维,

    其中T代表泛型,设定了从纹理中读取数据 或是 写入时的颜色类型,T可以是half、float、short、int等

    access表示纹理访问权限,当access没写时,默认是sample

    texture1d<T, access a = access::sample>

    texture2d<T, access a = access::sample>

    texture3d<T, access a = access::sample>

    eg:

    //类型 变量 修饰符

    /*

    类型

        - texture2d<float>,读取的数据类型是float,没写access,默认是sample

        - texture2d<float,access::read>,读取的数据类型是float,读取的方式是read

        - texture2d<float,access::write>,读取的数据类型是float,读取的方式是write

    变量名

        - imgA

        - imgB

        - imgC

    修饰符

        - [[texture(0)]] 对应纹理0

        - [[texture(1)]] 对应纹理1

        - [[texture(2)]] 对应纹理2

    */函数举例

    void foo (texture2d<float> imgA[[texture(0)]],

              texture2d<float,access::read> imgB[[texture(1)]],

              texture2d<float,access::write> imgC[[texture(2)]])

    {

        //...

    }

    2.采样器

            采样器类型决定了如何对一个纹理进行采样操作,在Metal框架中有一个对应着色器语言的采样器的对象MTLSamplerState,这个对象作为图形渲染着色器函数参数或是并行计算函数的参数传递,有以下几种状态:

    coord:从纹理中采样时,纹理坐标是否需要归一化

    enum class coord { normalized, pixel };

    filter:纹理采样过滤方式,放大/缩小过滤方式

    enum class filter { nearest, linear };

    min_filter:设置纹理采样的缩小过滤方式

    enum class min_filter { nearest, linear };

    mag_filter:设置纹理采样的放大过滤方式

    enum class mag_filter { nearest, linear };

    s_address、t_address、r_address:设置纹理s、t、r坐标(对应纹理坐标的x、y、z)的寻址方式

    s坐标:enum class s_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    t坐标:enum class t_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    r坐标:enum class r_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    address:设置所有纹理坐标的寻址方式

    enum class address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    mip_filter:设置纹理采样的mipMap过滤模式, 如果是none,那么只有一层纹理生效;

    enum class mip_filter { none, nearest, linear };





    作者:枫紫_6174
    链接:https://www.jianshu.com/p/17baccd48e77









    收起阅读 »

    iOS YUV与RGB&YUV算法

    数字媒体压缩        为缩小数字媒体文件的大小,我们需要对其使用压缩技术,一般来说我们所欣赏的媒体内容都进行过一定程度的压缩,无论在电视上的视频,蓝光碟片,网页上的视频流还是...
    继续阅读 »

    数字媒体压缩

            为缩小数字媒体文件的大小,我们需要对其使用压缩技术,一般来说我们所欣赏的媒体内容都进行过一定程度的压缩,无论在电视上的视频,蓝光碟片,网页上的视频流还是冲iTunes Store购买的资源,我们其实都是和这些内容的原始文件的压缩格式在打交到,对数字媒体进行压缩可以大幅度缩小文件的尺寸,但是通常会在资源的质量上有小幅可见的衰减,

    色彩的二次采样

            视频数据是使用称之为YCbCr颜色模式,它也常称为YUV,虽然YUV术语并不准确,但是读起来比较方便,大部分的软件开发者都更熟悉RGB颜色模式,即每个像素点都由红,绿,蓝三个颜色组合而成,YCbCr或者是YUV则使用色彩(颜色)通道UV替换了像素的亮度通道.




    从上图中我们可以看到图片的细节都保存在亮度的通道中,假设世界上没有阳光,我们的眼睛是看不到任何的东西,如果去除亮度,剩下的就是一副灰度图片,我们在看整合的色彩通道中关于图片的所有细节都丢失了,这是由于我们眼睛对亮度的敏感度要高于颜色,所以,在YUV中,我们可以通过大幅减少存储在每个像素点中的颜色信息,而不致于图片的质量严重受损,这个减少颜色数据的过程就称之为色彩的二次采样。

    我们平时所说的4:4:4和4:2:2以及4:2:0到底指的是什么,以及它的由来?

            色彩的二次采样一般发生在取样时,一些专业的相机以4:4:4的参数捕捉图像,但大部分情况下对于图片的拍摄使用4:2:2的方式进行的,面向消费者的摄像头装置,比如iPhone手机上的摄像头,通常是以4:2:0的方式进行拍摄,即使经过大量层级的二次抽样之后仍然可以捕捉到高质量的图片,iPhone手机上拍出来的高质量视频就是很好的例证,

    1.RGB的颜色编码

            RGB 三个字⺟分别代表了 红(Red)、绿(Green)、蓝(Blue),这三种颜⾊称为 三原⾊,将它们以不同的⽐例相加,可以产⽣多种多样的颜⾊。


     在图像显示中,⼀张1280 * 720 ⼤⼩的图⽚,就代表着它有1280 * 720 个像素点。其中每⼀个像素点的颜⾊显示都采⽤RGB 编码⽅法,将RGB 分别取不同的值,就会展示不同的颜⾊。

            RGB 图像中,每个像素点都有红、绿、蓝三个原⾊,其中每种原⾊都占⽤8 bit,也就是⼀个字节,那么⼀个像素点也就占⽤24 bit,也就是三个字节。

            ⼀张1280 * 720 ⼤⼩的图⽚,就占⽤1280 * 720 * 3 / 1024 / 1024 = 2.63 MB 存储空间

    2.YUV的颜色编码

            YUV 颜⾊编码采⽤的是 明亮度 和 ⾊度 来指定像素的颜⾊。其中,Y 表示明亮度(Luminance、Luma),⽽U 和V 表示⾊度(Chrominance、Chroma)。⽽⾊度⼜定义了颜⾊的两个⽅⾯:⾊调和饱和度



      和RGB 表示图像类似,每个像素点都包含Y、U、V 分量。但是它的Y 和UV 分量是可以分离的,如果没有UV 分量⼀样可以显示完整的图像,只不过是⿊⽩的。对于YUV 图像来说,并不是每个像素点都需要包含了Y、U、V 三个分量,根据不同的采样格式,可以每个Y 分量都对应⾃⼰的UV 分量,也可以⼏个Y 分量共⽤UV 分量

    传说中的4:4:4

            在4:4:4的模式下,色彩的全部信息被保全下来,如图:




    相邻的四个像素点ABCD,每个像素点有自己的YUV,在色彩的二次采样的过程中,分别保留自己的YUV,称之为4:4:4;

    传说中的4:2:2


    如图ABCD四个相邻的像素点,A(Y0,U0,V0),B(Y1,U1,V1),C(Y2,U2,V2),D(Y3,U3,V3),当二次采样的时候,A采样的时候保留(Y0,U0),B保留(Y1,V1),C保留(Y2,U2),D保留(Y3,V3);也就是说,每个像素点的Y(明亮度)保留其本身的值,而U和V的值是每间隔一个采样,而最终就变成:





     也就是说A借B的V1,B借A的U0,C借D的V3,D借C的U2,这就是传说中的4:2:2,⼀张1280 * 720 ⼤⼩的图⽚,在YUV 4:2:2 采样时的⼤⼩为:

    (1280 * 720 * 8 + 1280 * 720 * 0.5 * 8 * 2)/ 8 / 1024 / 1024 = 1.76 MB 。

            可以看到YUV 4:2:2 采样的图像⽐RGB 模型图像节省了三分之⼀的存储空间,在传输时占⽤的带宽也会随之减少

    传说中的4:2:0

            在上面说到的4:2:2中我们可以看到相邻的两个像素点的UV是左右互相借的,那可不可以上下左右借了,答案当然是可以的,






    YUV 4:2:0 采样,并不是指只采样U 分量⽽不采样V 分量。⽽是指,在每⼀⾏扫描时,只扫描⼀种⾊度分量(U 或者V),和Y 分量按照2 : 1 的⽅式采样。⽐如,第⼀⾏扫描时,YU 按照2 : 1 的⽅式采样,那么第⼆⾏扫描时,YV 分量按照2:1 的⽅式采样。对于每个⾊度分量来说,它的⽔平⽅向和竖直⽅向的采样和Y 分量相⽐都是2:1 。假设第⼀⾏扫描了U 分量,第⼆⾏扫描了V 分量,那么需要扫描两⾏才能够组成完整的UV 分量

            从映射出的像素点中可以看到,四个Y 分量是共⽤了⼀套UV 分量,⽽且是按照2*2 的⼩⽅格的形式分布的,相⽐YUV 4:2:2 采样中两个Y 分量共⽤⼀套UV 分量,这样更能够节省空间。⼀张1280 * 720 ⼤⼩的图⽚,在YUV 4:2:0 采样时的⼤⼩为:

    (1280 * 720 * 8 + 1280 * 720 * 0.25 * 8 * 2)/ 8 / 1024 / 1024 = 1.32 MB 相对于2.63M节省了一半的空间

    RGB — YUV 颜⾊编码转换

            对于图像显示器来说,它是通过RGB 模型来显示图像的,⽽在传输图像数据时⼜是使⽤YUV 模型,这是因为YUV 模型可以节省带宽。因此就需要采集图像时将RGB 模型转换到YUV 模型,显示时再将YUV 模型转换为RGB 模型。

            RGB 到YUV 的转换,就是将图像所有像素点的R、G、B 分量转换到Y、U、V 分量。

            Y = 0.299 * R + 0.587 * G + 0.114 * B 

            U = -0.147 * R - 0.289 * G + 0.436 * B 

            V = 0.615 * R - 0.515 * G - 0.100 * B

            R = Y + 1.14 * V 

            G = Y - 0.39 * U - 0.58 * V 

            B = Y + 2.03 * U



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/65cf8f073ee5


    收起阅读 »

    关于 webpack 的几个知识点

    随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,比如 sass 和 less 的代码浏览器是不支持的, 但如果摒弃了这些开发框架,那么开发的效率将大幅下降。在众多前端工程化工具中,webpack 脱颖而出成为了当今...
    继续阅读 »

    随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,比如 sass 和 less 的代码浏览器是不支持的, 但如果摒弃了这些开发框架,那么开发的效率将大幅下降。在众多前端工程化工具中,webpack 脱颖而出成为了当今最流行的前端构建工具。 然而大多数的使用者都只是单纯的会使用,而并不知道其深层的原理。希望通过以下的面试题总结可以帮助大家温故知新、查缺补漏,知其然而又知其所以然。

    1. webpack 与 grunt、gulp 的不同?

    三者都是前端构建工具,grunt 和 gulp 在早期比较流行,现在 webpack 相对来说比较主流,不过一些轻量化的任务还是会用 gulp 来处理,比如单独打包 CSS 文件等。

    grunt 和 gulp 是基于任务和流(Task、Stream)的。类似 jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个 web 的构建流程。

    webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。

    所以总结一下:

    • 从构建思路来说
      • gulp 和 grunt 需要开发者将整个前端构建过程拆分成多个 `Task`,并合理控制所有 `Task` 的调用关系。
        webpack 需要开发者找到入口,并需要清楚对于不同的资源应该使用什么 Loader 做何种解析和加工。
    • 对于知识背景来说
      • gulp 更像后端开发者的思路,需要对于整个流程了如指掌。 webpack 更倾向于前端开发者的思路。

    2. 你为什么最终选择使用 webpack?

    基于入口的打包工具除了 webpack 以外,主流的还有:rollup 和 parcel

    从应用场景上来看:

    • webpack 适用于大型复杂的前端站点构建
    • rollup 适用于基础库的打包,如 vue、react
    • parcel 适用于简单的实验性项目,他可以满足低门槛的快速看到效果

    由于 parcel 在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用 parcel

    3. 有哪些常见的 Loader?解决什么问题?

    • babel-loader:把 ES6 转换成 ES5
    • eslint-loader:通过 ESLint 检查 JavaScript 代码
    • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
    • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
    • image-loader:加载并且压缩图片文件
    • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
    • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
    • source-map-loader:加载额外的 Source Map 文件,以方便断点调试

    4. 有哪些常见的Plugin?解决什么问题?

    • define-plugin:定义环境变量
    • commons-chunk-plugin:提取公共代码
    • uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
    • HTMLWebpackPlugin:webpack 在自定生成 html 时需要用到它,能自动引入 js/css 文件
    • MiniCssExtractPlugin:将 css 代码抽成单独的文件,一般适用于发布环境,生产环境用 css-loader

    5. Loader 和 Plugin 的不同?

    不同的作用

    • Loader 直译为"加载器"。webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到 loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非JavaScript文件 的能力。
    • Plugin 直译为"插件"。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

    不同的用法

    • Loader 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载器 (loader) 和使用的参数(options
    • Plugin 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

    6. 如何利用 webpack 来优化前端性能?

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

    • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPluginParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩 css
    • 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用 webpack 对于output参数和各loader 的 publicPath 参数来修改资源路径
    • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
    • 提取公共代码。

    7. 如何提高 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://blog.csdn.net/Marker__/article/details/107619259
    收起阅读 »

    Xcode11,Transporter上传卡在——正在验证 APP - 正在通过App Store进行认证

    1.当卡死在 “Authenticating with the iTunes store”解决办法:关闭上传,并打开命令行,依次调用这三行代码: cd ~ mv .itmstransporter/ .old_itmstransporter/ "/Applica...
    继续阅读 »

    1.当卡死在 “Authenticating with the iTunes store”
    解决办法:
    关闭上传,并打开命令行,依次调用这三行代码:


    cd ~
    mv .itmstransporter/ .old_itmstransporter/
    "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter"
    `&lt;/pre&gt;

    先说结论,此方法有效,但是对于Xcode11来说Application Loader已经移除了,那么路径就要改变到Transporter下,所以需要修改最后一个命令。为什么有效呢,因为本质上iTMSTransporter是所有上传工具真正使用的可执行文件。所以Transporter下也会发现这个文件。

    &lt;span style="font-weight: bold; font-size: medium;"&gt;1.首先找到文件位置,反键显示包内容。&lt;/span&gt;
    ![](https://upload-images.jianshu.io/upload_images/5276080-dd51fa3a174b994a.png?imageMogr2/auto-orient/strip|imageView2/2/w/828/format/webp)

    &lt;span style="font-weight: bold; font-size: medium;"&gt;2.将iTMSTransporter的路径找到&lt;/span&gt;
    ![](https://upload-images.jianshu.io/upload_images/5276080-f664e303dd5b0547.png?imageMogr2/auto-orient/strip|imageView2/2/w/1033/format/webp)

    &lt;span style="font-weight: bold; font-size: medium;"&gt;3.执行以下命令&lt;/span&gt;
    &lt;pre&gt;`cd ~
    mv .itmstransporter/ .old_itmstransporter/
    "/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter"`&lt;/pre&gt;

    有两个点可能会出问题
    &lt;span style="color: rgb(77, 128, 191);"&gt;3.1 rename .itmstransporter/ to .old_itmstransporter/.itmstransporter/: Directory not empty&lt;/span&gt;
    如果第二句命令报以上错误,输入以下命令
    &lt;pre&gt;`mv .old_itmstransporter/ .itmstransporter/
    mv .itmstransporter/ .old_itmstransporter/

    3.2 no such file or directory: xxxxxxxx
    如果第三句命令报以上错误,是因为直接复制我的路径,但是你的应用路径跟我的不一致,自己将iTMSTransporter的路径找到并拼接好。
    接下来会出现[2020-01-15 18:08:13 CST] <main> INFO: Configuring logging…,然后就开始无尽的等待,如果长时间没有进展,建议切换4G网络开热点给电脑使用,说不定有奇效。
    最后指令执行完会出现[2020-01-15 18:10:07 CST] <main> DBG-X: Returning 0
    对于我来说,之后再去用Transporter上传,第一步正在通过App Store进行认证很快就过去了,然后在App Store验证时卡住了几分钟,接着出现了将数据发送到App Store时出错。

    然后我看到了稍后重新启动决定多等待以下,结果过了大概3分钟,就开始上传了。

    然后瞬间就上传成功了。至此我折腾了一个下午的上传IPA,终于结束了。

    重大更新
    如果一直命令一直卡着,也无法上传成功,可以试试下面的办法。

    上传卡住的原因:
    Transporter安装上第一次打开后,会在硬盘目录:/用户/你的电脑登录账号名/资源库/Caches/com.apple.amp.itmstransporter/目录下下载一些缓存文件,这些缓存文件没有下载完,或者下载失败没下载完时,使用Transporter去提交应用这个页面就会卡住或者这个页面很慢。

    那么一直更新不成功的话,可以下载这个文件夹直接覆盖自己的原有com.apple.amp.itmstransporter文件夹,如果原本没有也直接复制进去相当于创建了。

    步骤如下:

    https://download.csdn.net/download/Walter_White/12207626


    1.下载链接里的文件,把解压后的"com.apple.amp.itmstransporter"目录放到"/用户/你的电脑登录账号名/资源库/Caches/"目录下,覆盖你原有的"com.apple.amp.itmstransporter"目录。
    2.将新的"com.apple.amp.itmstransporter"目录下/obr/2.0.0/目录下的repository.xml文件中的所有"Simpsons"修改为你自己电脑的登录账号名,否则Transporter执行时会在错误的路径下找资源文件。
    3.再次尝试Transporter上传。
    4.如果时间App Store认证时间超过两分钟,建议手机开4g热点,电脑连接后再上传试试。

    转自:https://www.jianshu.com/p/c0d85c003b3e

    收起阅读 »

    【iOS】一个简单的人脸跟踪Demo

    1、sessionView - 相机画面的容器Viewself.detector - 脸部特征识别器- (void)viewDidLoad { [super viewDidLoad]; self.sessionView = [[UIView...
    继续阅读 »

    1、
    sessionView - 相机画面的容器View
    self.detector - 脸部特征识别器

    - (void)viewDidLoad {
    [super viewDidLoad];

    self.sessionView = [[UIView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.sessionView];

    self.faceView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"a"]];
    self.faceView.frame = CGRectZero;
    [self.view addSubview:self.faceView];

    self.leftEyeView = [[UIView alloc] init];
    self.leftEyeView.alpha = 0.4;
    self.leftEyeView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:self.leftEyeView];

    self.rightEyeView = [[UIView alloc] init];
    self.rightEyeView.alpha = 0.4;
    self.rightEyeView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:self.rightEyeView];

    self.mouthView = [[UIView alloc] init];
    self.mouthView.alpha = 0.4;
    self.mouthView.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.mouthView];

    self.context = [CIContext context];
    self.detector = [CIDetector detectorOfType:CIDetectorTypeFace context:self.context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
    }

    2、点击屏幕任意地方打开相机

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 避免重复打开,首先关闭原先的session
    [self.session stopRunning];
    self.session = [[AVCaptureSession alloc] init];

    // 移除原有的相机画面Layer
    [self.layer removeFromSuperlayer];

    NSError *error;

    // Device
    NSArray *devices = [AVCaptureDevice devices];
    NSLog(@"devices = %@", devices);
    AVCaptureDevice *defaultDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    // Input
    AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:defaultDevice error:&error];
    [self.session addInput:input];

    // Output
    AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
    [output setSampleBufferDelegate:(id)self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self.session addOutput:output];

    // 开始捕获相机画面
    [self.session startRunning];

    // 将相机画面添加到容器View中
    self.layer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
    self.layer.frame = self.view.bounds;
    [self.sessionView.layer addSublayer:self.layer];
    }

    3、脸部特征跟踪

    // AVCaptureAudioDataOutputSampleBufferDelegate
    - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    // printf("%s\n", __func__);
    // 1、获取当前帧图像
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CIImage *image = [[CIImage alloc] initWithCVImageBuffer:imageBuffer];

    CGFloat imageW = image.extent.size.width;
    CGFloat imageH = image.extent.size.height;

    2、对图像进行脸部特征识别
    CIFeature *feature = [[self.detector featuresInImage:image] lastObject];
    if (feature) {
    if (self.leftEyeView.frame.size.width == 0) {
    self.leftEyeView.frame = CGRectMake(0, 0, 20, 20);
    }
    if (self.rightEyeView.frame.size.width == 0) {
    self.rightEyeView.frame = CGRectMake(0, 0, 20, 20);
    }
    if (self.mouthView.frame.size.width == 0) {
    self.mouthView.frame = CGRectMake(0, 0, 20, 20);
    }
    NSLog(@"find");
    CIFaceFeature *face = (CIFaceFeature *)feature;
    dispatch_async(dispatch_get_main_queue(), ^{
    self.faceView.frame = CGRectMake(face.bounds.origin.y / imageW * self.sessionView.frame.size.height,
    face.bounds.origin.x / imageH * self.sessionView.frame.size.width,
    face.bounds.size.width / imageH * self.sessionView.frame.size.width,
    face.bounds.size.height / imageW * self.sessionView.frame.size.height);

    self.leftEyeView.center = CGPointMake(face.leftEyePosition.y / imageW * self.sessionView.frame.size.height,
    face.leftEyePosition.x / imageH * self.sessionView.frame.size.width);

    self.rightEyeView.center = CGPointMake(face.rightEyePosition.y / imageW * self.sessionView.frame.size.height,
    face.rightEyePosition.x / imageH * self.sessionView.frame.size.width);

    self.mouthView.center = CGPointMake(face.mouthPosition.y / imageW * self.sessionView.frame.size.height,
    face.mouthPosition.x / imageH * self.sessionView.frame.size.width);

    });
    }
    }

    大功告成
    手机记得横过来,home键在右边
    Demo地址:https://github.com/MagicBlind/Face-Detector

    转自:https://www.jianshu.com/p/db37d32e895e

    收起阅读 »

    iOS性能优化 — 三、安装包瘦身

    瘦身指导原则 总体指导原则为:压缩资源、删除无用/重复资源、删除无用代码、通过编译选项进行优化。 常规瘦身方案 压缩资源项目中资源包括图片、字符串、音视频等资源。由于项目中图片比较多,所以资源压缩一般会从图片入手。在把图片加入到项目中时候需要采用tinypng...
    继续阅读 »

    瘦身指导原则


    总体指导原则为:压缩资源、删除无用/重复资源、删除无用代码、通过编译选项进行优化。


    常规瘦身方案


    压缩资源
    项目中资源包括图片、字符串、音视频等资源。由于项目中图片比较多,所以资源压缩一般会从图片入手。在把图片加入到项目中时候需要采用tinypng或者ImageOptim对图片进行压缩;另外,可以通知设计,对切图进行压缩处理再上传;不需要内嵌到项目中的图片可以改为动态下载。


    • png,jpg,gif可以替换成webp


    • 动画图片可替换为lotties、APNG


    • 小图或表情图可替换为iconFont


    • 大图可替换为svg



    删除无用/重复资源
    删除无用的资源。项目中主要以删除图片为主:


    • 图片用2x和3x图就可以,不要用1x图。


    • 可以用LSUnusedResources搜索出未使用的图片然后删除之。注意:该软件搜索出来的图片有可能项目中还在用,删除之前需要在工程中先搜索下图片是否有使用再确认是否可以删除。



    删除无用代码
    删除无用类和库:可以用WBBladesForMac来分析,注意:通过字符串调用的类也会检测为无用类。

    非常规瘦身方案
    1、Strip :去除不必要的符号信息。
    -Strip Linked Product 和 Strip Swift Symbols 设置为 YES,Deployment Postprocessing 设置为 NO,发布代码的时候也需要勾选 Strip Swift Symbols。


    • Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在release下设为YES


    • Dead Code Stripping 设置为 YES


    • 对于动态库,可用strip -x [动态库路径] 去除不必要的符号信息



    2、Make Strings Read-Only设为YES。
    3、Link-Time Optimization(LTO)release下设为 Incremental。WWDC2016介绍编译时会移除没有被调用的方法和代码,优化程序运行效率。
    4、开启BitCode
    5、去除异常支持。不能使用@try @catch,包只缩小0.1M,效果不显著。
    Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,Other C Flags添加-fno-exceptions
    6、不生成debug symbols:不能生成dSYM,效果非常显著。
    Generate debug symbols选项 release 设置为NO

    脑图借鉴

    转自:https://www.jianshu.com/p/369c909c1067

    收起阅读 »

    关于webpack面试题总结

    最近在读《webpack深入浅出》,总结一下webpack关于面试常见的问题,分享出来,希望可以帮助更多小伙伴在找到心爱的工作和期待的薪水。一.常见的构建工具有哪些?他们各自优缺点?为什么选择了webpack?Grunt、Gulp、Fis3、Rollup、Np...
    继续阅读 »

    最近在读《webpack深入浅出》,总结一下webpack关于面试常见的问题,分享出来,希望可以帮助更多小伙伴在找到心爱的工作和期待的薪水。

    一.常见的构建工具有哪些?他们各自优缺点?为什么选择了webpack?

    Grunt、Gulp、Fis3、Rollup、Npm Script、webpack

    <1>Grunt的优点是:

    • 灵活,它只负责执行我们定义的任务;

    • 大量的可复用插件封装好了常见的构建任务。

    Grunt的缺点是:

    集成度不高,要写很多配置后才可以用,无法做到开箱即用。Grunt相当于进化版的NpmScript,它的诞生其实是为了弥补NpmScript的不足。

    <2>Gulp的优点是: 好用又不失灵活,既可以单独完成构建,也可以和其他工具搭配使用。

    其缺点: 和Grunt类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用。

    <3> Fis3的优点是:集成了各种Web开发所需的构建功能,配置简单、开箱即用。

    其缺点是 目前官方己经不再更新和维护,不支持最新版本的T、fode

    <4>Webpack的优点是:• 专注于处理模块化的项目,能做到开箱即用、一步到位:

    • 可通过Plugin扩展,完整好用又不失灵活;

    • 使用场景不局限于Web开发

    • 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展:

    • 良好的开发体验。

    Webpack的缺点是:只能用于采用模块化开发的项目。

    <5> Rollup是在Webpack流行后出现的替代品,讲述差别::
    • Rollup生态链还不完善,体验不如Webpack;

    • Rollup的功能不如Webpack完善,但其配置和使用更简单:

    • Rollup不支持CodeSpliting,但好处是在打包出来的代码中没有Webpack那段模块的加载、执行和缓存的代码。

    Roll up在用于打包JavaScript库时比Webpack更有优势,因为其打包出来的代码更小、

    深入浅出Webpack更快。

    缺点:但它的功能不够完善,在很多场景下都找不到现成的解决方案

    <6>Npm Script的优点 是内置,无须安装其他依赖。
    其缺点 是功能太简单,虽然提供了pre和post两个钩子,但不能方便地管理多个任务之间的依赖

    为啥选择webpack?
    大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,Webpack可以为这些新项目提供一站式的解决方案:
    • Webpack有良好的生态链和维护团队,能提供良好的开发体验并保证质量:

    • Webpack被全世界大量的Web开发者使用和验证,能找到各个层面所需的教程和经验分享。

    二.有哪些常见的Loader?你用过哪些Loader?

    1. 加载文件
    • raw-loader :将文本文件的内容加载到代码中

    • file-loader :将文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件

    • url-loader :和 file-loader 类似,但是能在文件很小的情况下以 base64 方式将文件的内容注入代码中

    • source-map-loader :加载额外的 SourceMap 文件,以方便断点调试

    • svg-inline-loader :将压缩后的SVG 内容注入代码中

    • node-loader :加载 Node.js 原生模块的 .node 文件

    • image-loader :加载并且压缩图片文件

    • json-loader:加载 JSON 文件

    • yaml-loader:加载 YAML 文件

    2. 编译模版
    • pug-loader :将 Pug 模版转换成 JavaScript 函数井返回。

    • handlebars-loader:将 Handlebars模版编译成函数并返回

    • s-loader :将 EJS 模版编译成函数井返回

    • haml-loader:将 HAML 代码转换成 HTML

    • markdown-loader 将 Markdown 文件转换成 HTML

    3.转换脚本语言
    • babel-loader :将 ES6 转换成 ES5

    • ts-loader :将 TypeScript 转换成 JavaScript,

    • awesome-typescript-loader: Type Script 转换成 JavaScript ,性能要比 ts-loader好

    • coffee-loader 将 CoffeeScript换成 JavaScript

    4.转换样式文件
    • css-loader :加载 css ,支持模块化、压缩、文件导入等特性。

    • style-loader :将 css 代码 注入JavaScript 中,通过 DOM 操作去加载 css

    • sass-loader :将 SCSS SASS 代码转换成 css

    • postcss-loader : 扩展 css 语法,使用css

    • less-loader : Less 代码转换成 css代码

    • stylus-loader :将 Stylu 代码转换成 css 码。

    5. 检查代码
    • eslint-loader :通过 ESLint 检查 JavaScript
    代码

    • tslint-loader :通过 TSLint peScript
    代码

    • mocha-loader :加载 Mocha 测试
    用例的代码

    • coverjs-loader : 计算测试的覆盖率。

    6.其他 Loader
    • vue-loader :加载 Vue. 单文件组件

    • i18n-loader:加载多语言版本,支持国际化

    • ignore-loader :忽略部分文件

    • ui-component-loader:按需加载
    UI 组件库,例如在使用 antdUI 组件库时,不会因为只用到了 Button 组件而打包进所有的组件

    3.有哪些常见的Plugin?你用过哪些Plugin?

    1.用于修改行为
    • define-plugin :定义环境变量

    • context-replacement-plugin : 修改 require 语句在寻找文件时的默认行为

    • ignore-plugin :用 于忽略部分文件

    2.用于优化
    • commons-chunk-plugin :提取公共代码。

    • extract-text-webpack-plugin :提取 JavaScript 中的 css 代码到单独的文件中

    • prepack-webpack-plugin :通过Facebook Prepack 优化输出的 JavaScript 代码的性能

    • uglifyjs-webpack-plugin :通过 UglifyES 压缩 S6 代码

    • webpack-parallel-uglify-plugin :多进程执行 glifyJS 代码压缩,提升构建的速度

    • imagemin-webpack-plugin : 压缩图片文件。

    • webpack-spritesmith :用插件制作碧图

    • ModuleConcatenationPlugin : 开启 WebpackScopeHoisting 功能

    • dll-plugin :借鉴 DDL 的思想大幅度提升构建速度

    • hot-module-replacem nt-plugin 开启模块热替换功能。

    3. 其他 Plugin
    • serviceworker-webpack-plugin :为网页应用增加离钱缓存功能

    • stylelint-webpack-plugin : stylelint集成到项目中,

    • i18n-webpack-plugin : 使网页支持国际化。

    • provide-plugin : 从环境中提供的全局变量中加载模块,而不用导入对应的文件。

    • web-webpack-plugin : 可方便地为单页应用输出 HTML ,比 html-webpack-plugin 好用

    4.那你再说一说Loader和Plugin的区别

    Loader :模块转换器,用于将模块的原内容按照需求转换成新内容。
    Plugin :扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑,来改变构建结
    果或做我们想要的事情。

    5.Webpack构建流程简单说一下

    初始化参数 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
    • 开始编译:用上 步得到的参数初始 Co er 对象,加载所有配置的插件,通
    过执行对象的 run 方法开始执行编译
    • 确定入口 根据配置中的 ntry 找出所有入口文件
    • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出
    模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
    • 完成模块编译 在经过第 步使用 Loader 翻译完所有模块后, 得到了每个模块被
    翻译后的最终内容及它们之间的依赖关系。
    • 输出资源:根据入口和模块之间的依赖关系,组装成 个个包含多个模块的 Chunk,
    再将每个 Chunk 转换成 个单独的文件加入输出列表中,这是可以修改输出内容
    的最后机会
    • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内
    容写入文件系统中。

    6.使用webpack开发时,你用过哪些可以提高效率的插件

    webpack-dashboard:可以更友好的展示相关打包信息。
    webpack-merge:提取公共配置,减少重复配置代码
    speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
    size-plugin:监控资源体积变化,尽早发现问题
    HotModuleReplacementPlugin:模块热替换

    7.模块打包原理知道吗?


    8.什么 是模块热更新?


    devServer.hot 配置是否启用 ,开启模块热替换功能后,将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览


    9.如何提高webpack的构建速度?


    10.文件监听原理呢?


    11.source map是什么?生产环境怎么用?


    12.如何对bundle体积进行监控和分析?


    13.文件指纹是什么?怎么用?


    14.在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?


    15.如何优化 Webpack 的构建速度?


    16.你刚才也提到了代码分割,那代码分割的本质是什么?有什么意义呢?


    17.是否写过Loader?简单描述一下编写loader的思路?


    18.是否写过Plugin?简单描述一下编写Plugin的思路?


    19.聊一聊Babel原理吧?


    20.什么是Tree-shaking?
    Tree Shaking 可以用来剔除 JavaScript 中用 不上的死代码。


    21.如何实现 按需加载?


    ``import(/* webpackChunkName : ” show " */ ’. / show ’>


    Webpack 内置了对 import *)语句的支持,当 Wepack 遇到了类似的语句时会这样


    处理:
    • 以./ show.j 为入口重新生成一个 Chunk;
    • 代码执行到 import 所在的语句时才去加载由 Chunk 对应生成的文件:
    • import 返回一个 Promise ,当文件加载成功时可以在 Promise then 方法中获取
    show.j 导出的内容。``


    22.如何配置单页应用?如何配置多页应用?


    23.如何利用webpack来优化前端性能?(提高性能和体验)


    24.npm打包时需要注意哪些?如何利用webpack来更好的构建


    25.什么是模块化,都有哪些?


    模块化是指一个复杂的系统分解为多个模块以方便编码。


    js模块化:


    mommon.js:核型思想,通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口。


    优点
    1.代码可复用于node环境并运行,例如同构应用
    2.通过npm发布的很多第三方模块都采用了mommonJS规范


    缺点:1.无法直接运行在浏览器环境下,必需通过工具转换成标准的es5


    AMD:异步方式去加载依赖的模块,主要用来解决针对浏览器环境的模块化问题,最具代表的实现是require.js


    优点
    1.可在不转换代码的情况下,直接在浏览器中运行
    2.可异步加载依赖
    3.可并行加载多个依赖
    4.代码可运行在浏览器和node环境下


    缺点 :1.js运行环境没有原生支持AMD,需要先导入实现了AMD的库后才能正常使用。


    es6模块化:

    import { readFile} from 'fs';
    import react from 'react';

    // 导出
    export function hello(){};
    export default{...}


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

    收起阅读 »

    NodeJs中的stream(流)- 基础篇

    一、什么是Stream(流) 流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。 流是可读的、可写的,或...
    继续阅读 »

    一、什么是Stream(流)



    流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。



    流是可读的、可写的,或是可读写的。


    二、NodeJs中的Stream的几种类型


    Node.js 中有四种基本的流类型:



    • Readable - 可读的流(fs.createReadStream())

    • Writable - 可写的流(fs.createWriteStream())

    • Duplex - 可读写的流(net.Socket)

    • Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate())


    NodeJs中关于流的操作被封装到了Stream模块中,这个模块也被多个核心模块所引用。

    const stream = require('stream');

    在 NodeJS 中对文件的处理多数使用流来完成



    • 普通文件

    • 设备文件(stdin、stdout)

    • 网络文件(http、net)


    注:在NodeJs中所有的Stream(流)都是EventEmitter的实例


    Example:


    1.将1.txt的文件内容读取为流数据

    const fs = require('fs');

    // 创建一个可读流(生产者)
    let rs = fs.createReadStream('./1.txt');

    通过fs模块提供的createReadStream()可以轻松创建一个可读的文件流。但我们并有直接使用Stream模块,因为fs模块内部已经引用了Stream模块并做了封装。所以说 流(stream)在 Node.js 中是处理流数据的抽象接口,提供了基础Api来构建实现流接口的对象。

    var rs = fs.createReadStream(path,[options]);

    1.path 读取文件的路径


    2.options



    • flags打开文件的操作, 默认为'r'

    • mode 权限位 0o666

    • encoding默认为null

    • start开始读取的索引位置

    • end结束读取的索引位置(包括结束位置)

    • highWaterMark读取缓存区默认的大小64kb


    Node.js 提供了多种流对象。 例如:



    • HTTP 请求 (request response)

    • process.stdout 就都是流的实例。


    2.创建可写流(消费者)处理可读流


    将1.txt的可读流 写入到2.txt文件中 这时我们需要一个可写流

    const fs = require('fs');
    // 创建一个可写流
    let ws = fs.createWriteStream('./2.txt');
    // 通过pipe让可读流流入到可写流 写入文件
    rs.pipe(ws);
    var ws = fs.createWriteStream(path,[options]);

    1.path 读取文件的路径


    2.options



    • flags打开文件的操作, 默认为'w'

    • mode 权限位 0o666

    • encoding默认为utf8

    • autoClose:true是否自动关闭文件

    • highWaterMark读取缓存区默认的大小16kb


    pipe 它是Readable流的方法,相当于一个"管道",数据必须从上游 pipe 到下游,也就是从一个 readable 流 pipe 到 writable 流。

    后续将深入将介绍pipe。




    如上图,我们把文件比作装水的桶,而水就是文件里的内容,我们用一根管子(pipe)连接两个桶使得水从一个桶流入另一个桶,这样就慢慢的实现了大文件的传输过程。

    三、为什么应该使用 Stream


    当有用户在线看视频,假定我们通过HTTP请求返回给用户视频内容

    const http = require('http');
    const fs = require('fs');

    http.createServer((req, res) => {
    fs.readFile(videoPath, (err, data) => {
    res.end(data);
    });
    }).listen(8080);

    但这样有两个明显的问题


    1.视频文件需要全部读取完,才能返回给用户,这样等待时间会很长

    2.视频文件一次全放入内存中,内存吃不消


    用流可以将视频文件一点一点读到内存中,再一点一点返回给用户,读一部分,写一部分。(利用了 HTTP 协议的 Transfer-Encoding: chunked 分段传输特性),用户体验得到优化,同时对内存的开销明显下降

    const http = require('http');
    const fs = require('fs');

    http.createServer((req, res) => {
    fs.createReadStream(videoPath).pipe(res);
    }).listen(8080);

    四、可读流(Readable Stream)



    可读流(Readable streams)是对提供数据的源头(source)的抽象。



    例如:



    • HTTP responses, on the client

    • HTTP requests, on the server

    • fs read streams

    • TCP sockets

    • process.stdin


    所有的 Readable 都实现了 stream.Readable 类定义的接口。


    可读流的两种模式(flowing 和 paused)


    1.在 flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给应用。


    2.在 paused 模式下,必须显式调用 stream.read()方法来从流中读取数据片段。


    所有初始工作模式为paused的Readable流,可以通过下面三种途径切换为flowing模式:



    • 监听'data'事件

    • 调用stream.resume()方法

    • 调用stream.pipe()方法将数据发送到Writable


    流动模式flowing


    流切换到流动模式 监听data事件

    const rs = fs.createReadStream('./1.txt');
    const ws = fs.createWriteStream('./2.txt');
    rs.on('data', chunk => {
    ws.write(chunk);
    });
    ws.on('end', () => {
    ws.end();
    });

    如果写入的速度跟不上读取的速度,有可能导致数据丢失。正常的情况应该是,写完一段,再读取下一段,如果没有写完的话,就让读取流先暂停,等写完再继续。

    var fs = require('fs');
    // 读取highWaterMark(3字节)数据,读完之后填充缓存区,然后触发data事件
    var rs = fs.createReadStream(sourcePath, {
    highWaterMark: 3
    });
    var ws = fs.createWriteStream(destPath, {
    highWaterMark: 3
    });

    rs.on('data', function(chunk) { // 当有数据流出时,写入数据
    if (ws.write(chunk) === false) { // 如果没有写完,暂停读取流
    rs.pause();
    }
    });

    ws.on('drain', function() { // 缓冲区清空触发drain事件 这时再继续读取
    rs.resume();
    });

    rs.on('end', function() { // 当没有数据时,关闭数据流
    ws.end();
    });

    或者使用更直接的pipe

    fs.createReadStream(sourcePath).pipe(fs.createWriteStream(destPath));

    暂停模式paused


    1.在流没有 pipe() 时,调用 pause() 方法可以将流暂停

    2.pipe() 时,需要移除所有 data 事件的监听,再调用 unpipe() 方法


    read(size)

    流在暂停模式下需要程序显式调用 read() 方法才能得到数据。read() 方法会从内部缓冲区中拉取并返回若干数据,当没有更多可用数据时,会返回null。read()不会触发'data'事件。


    使用 read() 方法读取数据时,如果传入了 size 参数,那么它会返回指定字节的数据;当指定的size字节不可用时,则返回null。如果没有指定size参数,那么会返回内部缓冲区中的所有数据。

    NodeJS 为我们提供了一个 readable 的事件,事件在可读流准备好数据的时候触发,也就是先监听这个事件,收到通知又数据了我们再去读取就好了:

    const fs = require('fs');
    rs = fs.createReadStream(sourcePath);

    // 当你监听 readable事件的时候,会进入暂停模式
    rs.on('readable', () => {
    console.log(rs._readableState.length);
    // read如果不加参数表示读取整个缓存区数据
    // 读取一个字段,如果可读流发现你要读的字节小于等于缓存字节大小,则直接返回
    let ch = rs.read(1);
    });

    暂停模式 缓存区的数据以链表的形式保存在BufferList中


    五、可写流(Writable Stream)



    可写流是对数据流向设备的抽象,用来消费上游流过来的数据,通过可写流程序可以把数据写入设备,常见的是本地磁盘文件或者 TCP、HTTP 等网络响应。



    Writable 的例子包括了:



    • HTTP requests, on the client

    • HTTP responses, on the server

    • fs write streams

    • zlib streams

    • crypto streams

    • TCP sockets

    • child process stdin

    • process.stdout, process.stderr


    所有 Writable 流都实现了 stream.Writable 类定义的接口。

    process.stdin.pipe(process.stdout);

    process.stdout 是一个可写流,程序把可读流 process.stdin 传过来的数据写入的标准输出设备。在了解了可读流的基础上理解可写流非常简单,流就是有方向的数据,其中可读流是数据源,可写流是目的地,中间的管道环节是双向流。


    可写流使用


    调用可写流实例的 write() 方法就可以把数据写入可写流

    const fs = require('fs');
    const rs = fs.createReadStream(sourcePath);
    const ws = fs.createWriteStream(destPath);

    rs.setEncoding('utf-8'); // 设置编码格式
    rs.on('data', chunk => {
    ws.write(chunk); // 写入数据
    });

    监听了可读流的 data 事件就会使可读流进入流动模式,我们在回调事件里调用了可写流的 write() 方法,这样数据就被写入了可写流抽象的设备destPath中。


    write() 方法有三个参数



    • chunk {String| Buffer},表示要写入的数据

    • encoding 当写入的数据是字符串的时候可以设置编码

    • callback 数据被写入之后的回调函数


    'drain'事件


    如果调用 stream.write(chunk) 方法返回 false,表示当前缓存区已满,流将在适当的时机(缓存区清空后)触发 'drain

    const fs = require('fs');
    const rs = fs.createReadStream(sourcePath);
    const ws = fs.createWriteStream(destPath);

    rs.setEncoding('utf-8'); // 设置编码格式
    rs.on('data', chunk => {
    let flag = ws.write(chunk); // 写入数据
    if (!flag) { // 如果缓存区已满暂停读取
    rs.pause();
    }
    });

    ws.on('drain', () => {
    rs.resume(); // 缓存区已清空 继续读取写入
    });

    六、总结


    stream(流)分为可读流(flowing mode 和 paused mode)、可写流、可读写流,Node.js 提供了多种流对象。 例如, HTTP 请求 和 process.stdout 就都是流的实例。stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。它们底层都调用了stream模块并进行封装。



    后续我们将继续对stream深入解析以及Readable Writable pipe的实现

    作者:Brolly
    链接:https://www.jianshu.com/p/1d36648fb87e
    来源:简书

    收起阅读 »

    线程切换哪家强?RxJava与Flow的操作符对比

    Flow作为Coroutine版的RxJava,同RxJava一样可以方便地进行线程切换。 本文针对两者在多线程场景中的使用区别进行一个简单对比。 1. RxJava 我们先来回顾一下RxJava中的线程切换 如上,RxJava使用subscriberOn...
    继续阅读 »

    Flow作为Coroutine版的RxJava,同RxJava一样可以方便地进行线程切换。
    本文针对两者在多线程场景中的使用区别进行一个简单对比。


    1. RxJava




    我们先来回顾一下RxJava中的线程切换
    在这里插入图片描述


    如上,RxJava使用subscriberOnobserveOn进行线程切换


    subscribeOn


    subscribeOn用来决定在哪个线程进行订阅,对于Cold流来说即决定了数据的发射线程。使用中有两点注意:



    1. 当调用链上只有一个subscribeOn时,可以出现在任意位置


    在这里插入图片描述
    在这里插入图片描述


    上面两种写法效果是一样的:都是在io线程订阅后发射数据



    1. 当调用链上有多个subscribeOn时,只有第一个生效:


    在这里插入图片描述


    上面第二个subscribeOn没有意义


    observeOn


    observeOn用来决定在哪个线程上响应



    1. observeOn决定调用链上下游操作符执行的线程


    在这里插入图片描述


    上面绿线部分的代码将会运行在主线程



    1. subscribeOn不同,调用链上允许存在多个observeOn且每个都有效


    在这里插入图片描述


    上面蓝色绿色部分因为observeOn的存在分别切换到了不同线程执行


    just


    RxJava的初学者经常会犯的一个错误是在Observable.just(...)里做耗时任务。 just并不是接受lambda,所以是立即执行的,不受subscribeOn的影响


    在这里插入图片描述


    如上,loadDataSync()不会在io执行,


    想要在io执行,需要使用Observable.deffer{}


    在这里插入图片描述


    flatMap


    结合上面介绍的RxJava的线程切换,看下面这段代码


    在这里插入图片描述


    如果我们希望loadData(id)并发执行,那么上面的写法是错误的。


    subscribe(io())意味着其上游的数据在单一线程中串行发射。因此虽然flatMap{}返回多个Observable, 都是都在单一线程中订阅,多个loadData始终运行在同一线程。


    代码经过一下修改后,可以达到并发执行的效果:


    在这里插入图片描述


    当订阅flatMap返回的Observable时,通过subscribeOn分别指定订阅线程。


    其他类似flatMap这种涉及多个Observable订阅的操作符(例如mergezip等),需要留意各自的subscribeOn的线程,以防不符合预期的行为出现。


    2. Flow




    接下来看一下 Flow的线程切换 。


    Flow是基于CoroutineContext进行线程切换,所以这部分内容需要你对Croutine事先有基本的了解。


    在这里插入图片描述
    flowOn类似于RxJava的subscribeOn,Flow中没有对应observeOn的操作符,因为collect是一个suspend函数,必须在CoroutineScope中执行,所以响应线程是由CoroutineContext决定的。例如你在main中执行collect,那么响应线程就是Dispatcher.Main


    flowOn


    flowOn类似于subscribeOn,因为它们都可以用来决定上游线程
    在这里插入图片描述
    上面代码中,flowOn前面代码将会在IO执行。


    subscribeOn不同的是,flowOn允许出现多次,每个都会影响其前面的操作
    在这里插入图片描述
    上面代码,根据颜色可以看出来flowOn影响的范围


    launchIn


    collect是suspend函数,所以后续代码因为协程挂起不会继续执行
    在这里插入图片描述
    所以上面代码可能会不符合预期,因为第一个collect不走完第二个走不到。


    正确的写法是为每个collect单独起一个协程
    在这里插入图片描述
    或者使用launchIn,写法更加优雅
    在这里插入图片描述
    launchIn不会挂起协程,所以与RxJava的subscribe更加接近。


    通过名字可以感觉出来launchIn只不过是之前例子中launch的一个链式调用的语法糖。


    flowOf


    flowOf类似于Observable.just(),需要注意flowOf内的内容是立即执行的,不受flowOn影响
    在这里插入图片描述


    希望calculate()运行在IO,可以使用flow{ }


    在这里插入图片描述


    flatMapMerge


    flatMapMerge类似RxJava的flatMap
    在这里插入图片描述
    如上,2个item各自flatMap成2个item,即一共发射了4条数据,日志输出如下:


    inner: pool-2-thread-2 @coroutine#4
    inner: pool-2-thread-3 @coroutine#5
    inner: pool-2-thread-3 @coroutine#5
    inner: pool-2-thread-2 @coroutine#4
    collect: pool-1-thread-2 @coroutine#2
    collect: pool-1-thread-2 @coroutine#2
    collect: pool-1-thread-2 @coroutine#2
    collect: pool-1-thread-2 @coroutine#2
    复制代码

    通过日志我们发现flowOn虽然写在flatMapMerge外面,inner的日志却可以打印在多个线程上(都来自pool2线程池),这与flatMap是不同的,同样场景下flatMap只能运行在线程池的固定线程上。


    如果将flowOn写在flatMapMerge内部


    在这里插入图片描述


    结果如下:


    inner: pool-2-thread-2 @coroutine#6
    inner: pool-2-thread-1 @coroutine#7
    inner: pool-2-thread-2 @coroutine#6
    inner: pool-2-thread-1 @coroutine#7
    collect: pool-1-thread-3 @coroutine#2
    collect: pool-1-thread-3 @coroutine#2
    collect: pool-1-thread-3 @coroutine#2
    collect: pool-1-thread-3 @coroutine#2
    复制代码

    inner仍然打印在多个线程,flowOn无论写在flatMapMerge内部还是外部,对flatMapMerge内的处理没有区别。


    但是flatMapMerge之外还是有区别的,看下面两段代码
    在这里插入图片描述
    在这里插入图片描述


    通过颜色可以知道flowOn影响的范围,向上追溯到flowOf为止


    3. Summary




    RxJava的Observable与Coroutine的Flow都支持线程切换,相关API的对比如下:































    线程池调度线程操作符数据源同步创建异步创建并发执行
    RxJavaSchedulers (io(), computation(), mainThread())subscribeOn, observeOnjustdeffer{}flatMap(inner subscribeOn)
    FlowDispatchers (IO, Default, Main)flowOnflowOfflow{}flatMapMerge(inner or outer flowOn)

    最后通过一个例子看一下如何将代码从RxJava迁移到Flow


    RxJava


    RxJava代码如下:


    在这里插入图片描述


    使用到的Schedulers定义如下:
    在这里插入图片描述


    代码执行结果:


    1: pool-1-thread-1
    1: pool-1-thread-1
    1: pool-1-thread-1
    2: pool-3-thread-1
    2: pool-3-thread-1
    2: pool-3-thread-1
    inner 1: pool-4-thread-1
    inner 1: pool-4-thread-2
    inner 1: pool-4-thread-1
    inner 1: pool-4-thread-1
    inner 1: pool-4-thread-2
    inner 1: pool-4-thread-2
    inner 1: pool-4-thread-3
    inner 2: pool-5-thread-1
    inner 2: pool-5-thread-2
    3: pool-5-thread-1
    inner 2: pool-5-thread-2
    inner 1: pool-4-thread-3
    inner 2: pool-5-thread-2
    inner 2: pool-5-thread-3
    3: pool-5-thread-1
    3: pool-5-thread-1
    3: pool-5-thread-1
    end: pool-6-thread-1
    end: pool-6-thread-1
    inner 1: pool-4-thread-3
    end: pool-6-thread-1
    3: pool-5-thread-1
    inner 2: pool-5-thread-1
    3: pool-5-thread-1
    inner 2: pool-5-thread-3
    inner 2: pool-5-thread-1
    end: pool-6-thread-1
    3: pool-5-thread-3
    3: pool-5-thread-3
    end: pool-6-thread-1
    inner 2: pool-5-thread-3
    3: pool-5-thread-3
    end: pool-6-thread-1
    end: pool-6-thread-1
    end: pool-6-thread-1
    end: pool-6-thread-1
    复制代码

    代码较长,通过颜色标记法帮我们理清线程关系


    在这里插入图片描述


    上色后一目了然了,需要特别注意的是由于flatMap中切换了数据源的同时切换了线程,所以打印 3的线程不是s2 而是 s4


    Flow


    首相创建对应的Dispatcher


    在这里插入图片描述


    然后将代码换成Flow的写法,主要遵循下列原则



    • RxJava通过observeOn切换后续代码的线程

    • Flow通过flowOn切换前置代码的线程


    在这里插入图片描述


    打印结果如下:


    1: pool-1-thread-1 @coroutine#6
    1: pool-1-thread-1 @coroutine#6
    1: pool-1-thread-1 @coroutine#6
    2: pool-2-thread-2 @coroutine#5
    2: pool-2-thread-2 @coroutine#5
    2: pool-2-thread-2 @coroutine#5
    inner 1: pool-3-thread-1 @coroutine#10
    inner 1: pool-3-thread-2 @coroutine#11
    inner 1: pool-3-thread-3 @coroutine#12
    inner 1: pool-3-thread-2 @coroutine#11
    inner 1: pool-3-thread-3 @coroutine#12
    inner 2: pool-4-thread-3 @coroutine#9
    inner 1: pool-3-thread-1 @coroutine#10
    inner 1: pool-3-thread-3 @coroutine#12
    inner 1: pool-3-thread-2 @coroutine#11
    inner 2: pool-4-thread-1 @coroutine#7
    inner 2: pool-4-thread-2 @coroutine#8
    inner 2: pool-4-thread-1 @coroutine#7
    inner 2: pool-4-thread-3 @coroutine#9
    inner 1: pool-3-thread-1 @coroutine#10
    3: pool-4-thread-1 @coroutine#3
    inner 2: pool-4-thread-3 @coroutine#9
    inner 2: pool-4-thread-2 @coroutine#8
    end: pool-5-thread-1 @coroutine#2
    3: pool-4-thread-1 @coroutine#3
    inner 2: pool-4-thread-2 @coroutine#8
    3: pool-4-thread-1 @coroutine#3
    end: pool-5-thread-1 @coroutine#2
    3: pool-4-thread-1 @coroutine#3
    end: pool-5-thread-1 @coroutine#2
    end: pool-5-thread-1 @coroutine#2
    3: pool-4-thread-1 @coroutine#3
    3: pool-4-thread-1 @coroutine#3
    end: pool-5-thread-1 @coroutine#2
    end: pool-5-thread-1 @coroutine#2
    3: pool-4-thread-1 @coroutine#3
    3: pool-4-thread-1 @coroutine#3
    end: pool-5-thread-1 @coroutine#2
    end: pool-5-thread-1 @coroutine#2
    inner 2: pool-4-thread-1 @coroutine#7
    3: pool-4-thread-1 @coroutine#3
    end: pool-5-thread-1 @coroutine#2
    复制代码

    从日志可以看到,123的时序性以及inner1inner2的并发性与RxJava的一致。


    4. FIN




    Flow在线程切换方面可以完全取代RxJava的能力,而且将subscribeOnobserveOn两个操作符合二为一成flowOn,学习成本更低。随着flow的操作符种类日趋完善,未来在Android/Kotlin开发中可以跟RxJava说再见了👋🏻


    image.png


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

    收起阅读 »

    用Jetpack Compose制作出可爱的天气动画

    1. 背景介绍 最近参加了Compose挑战赛的终极挑战,使用Compose完成了一个天气app。之前几轮挑战我也都有参与,每次都学到不少新东西,希望在这最后一轮挑战中,活用这段时间的积累做出更加成熟的作品。 项目挑战 因为没有美工协助,所以我考虑通过代码实...
    继续阅读 »

    1. 背景介绍




    最近参加了Compose挑战赛的终极挑战,使用Compose完成了一个天气app。之前几轮挑战我也都有参与,每次都学到不少新东西,希望在这最后一轮挑战中,活用这段时间的积累做出更加成熟的作品。


    项目挑战


    因为没有美工协助,所以我考虑通过代码实现app中的所有UI元素例如各种icon等,这样的UI在任何分辨率下都不会失真,而且可以更灵活地完成各种动画效果。


    为了降低实现成本,我将app中的UI元素定义成偏卡通的风格,更利于代码实现:
    在这里插入图片描述



    上面的动画没有使用giflottie等三方资源,所有效果都基于Compose代码绘制。



    MyApp:CuteWeather




    App界面比较简洁,采用单页面呈现(这也是挑战赛要求),可以查看近一周的天气信息和温度走势等。


    项目地址: github.com/vitaviva/co…


    在这里插入图片描述


    其中,卡通风格的天气动画算是这个app相对于同类应用的特色,本文将围绕这些天气动画介绍一下如何使用Compose绘制自定义图形、并基于这些图形实现动画。




    2. Compose自定义绘制




    像常规的Android开发一样,除了各种默认的Composable控件以外,Compose也提供了Canvas用来绘制自定义图形。


    Canvas相关的API在各个平台都大同小异,但在Compose上具有以下特点:



    • 用声明式的方式创建和使用Canvas

    • 通过DrawScope提供必要的state及各种APIs

    • API更简单易用


    声明式地创建和使用Canvas


    Compose中,Canvas作为Composable可以声明式地添加到其他Composable中,并通过Modifier进行配置


    Canvas(modifier = Modifier.fillMaxSize()){ // this: DrawScope 
    //内部进行自定义绘制
    }
    复制代码

    传统方式需要获取Canvas句柄命令式地进行绘制,而Canvas{...}通过状态驱动的方式执行block内的绘制逻辑,从而刷新UI。


    强大的DrawScope


    Canvas{...}通过DrawScope提供了一些当前绘制所需的state,例如经常使用到的size;DrawScope还提了各种常用的绘制API,例如drawLine


    Canvas(modifier = Modifier.fillMaxSize()){
    //通过size获取当前canvas的width和height
    val canvasWidth = size.width
    val canvasHeight = size.height

    //绘制直线
    drawLine(
    start = Offset(x=canvasWidth, y = 0f),
    end = Offset(x = 0f, y = canvasHeight),
    color = Color.Blue,
    strokeWidth = 5F //设置直线宽度
    )
    }
    复制代码

    上面代码绘制效果如下:


    在这里插入图片描述


    简单易用的API


    传统的Canvas API需要进行Paint的配置,而DrawScope的API则更简单、使用更友好。


    例如绘制一个圆,传统的API是这样:


    public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {
    //...
    }
    复制代码

    DrawScope提供的API:


    fun drawCircle(
    color: Color,
    radius: Float = size.minDimension / 2.0f,
    center: Offset = this.center,
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
    ) {...}
    复制代码

    虽然看起来参数变多了,但是其实已经通过size等设置了合适的默认值,同时省去了Paint的创建和配置,使用起来更方便。


    使用原生Canvas


    目前DrawScope提供的API还不及原生Canvas丰富(比如不支持drawText等),当不满足使用需求时,也可以直接使用原生Canvas对象进行绘制


       drawIntoCanvas { canvas ->
    //nativeCanvas是原生canvas对象,android平台即android.graphics.Canvas
    val nativeCanvas = canvas.nativeCanvas

    }
    复制代码

    上面对Compose中的Canvas做了简单介绍,下面结合app中的具体示例看一下实际使用效果


    首先,看一下雨水的绘制过程。




    3. 雨天效果




    雨天天气的关键是如何绘制不断下落的雨水


    在这里插入图片描述


    雨滴的绘制


    我们先绘制构成雨水的基本单元:雨滴


    在这里插入图片描述


    经拆解后,雨水效果可由三组雨滴构成,每一组雨滴分成上下两段,这样在运动时可以形成接连不断的效果。


    我们使用drawLine绘制每一段黑线,设置适当的stokeWidth,并通过cap设置端点的圆形效果:


    @Composable
    fun rainDrop() {

    Canvas(modifier) {

    val x: Float = size.width / 2 //x坐标: 1/2的位置

    drawLine(
    Color.Black,
    Offset(x, line1y1), //line1 的起点
    Offset(x, line1y2), //line1 的终点
    strokeWidth = width, //设置宽度
    cap = StrokeCap.Round//头部圆形
    )

    // line2同上
    drawLine(
    Color.Black,
    Offset(x, line2y1),
    Offset(x, line2y2),
    strokeWidth = width,
    cap = StrokeCap.Round
    )
    }
    }
    复制代码

    雨滴下落动画


    完成雨滴的基本图形绘制后,接下来为两线段增加位移动画,形成流动的效果。


    在这里插入图片描述


    以两线段中间空隙为动画的锚点,根据animationState变动其y轴位置,从canvas的顶端移动到低端(0 ~ size.hight),然后restart这个动画。


    然后以锚点为基准绘制上下两线段,就行成接连不断的动画效果了


    在这里插入图片描述


    代码如下:


    @Composable
    fun rainDrop() {
    //循环播放的动画 ( 0f ~ 1f)
    val animateTween by rememberInfiniteTransition().animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
    tween(durationMillis, easing = LinearEasing),
    RepeatMode.Restart //start动画
    )
    )

    Canvas(modifier) {

    // scope : 绘制区域
    val width = size.width
    val x: Float = size.width / 2

    // width/2是strokCap的宽度,scopeHeight处预留strokCap宽度,让雨滴移出时保持正圆,提高视觉效果
    val scopeHeight = size.height - width / 2

    // space : 两线段的间隙
    val space = size.height / 2.2f + width / 2 //间隙size
    val spacePos = scopeHeight * animateTween //锚点位置随animationState变化
    val sy1 = spacePos - space / 2
    val sy2 = spacePos + space / 2

    // line length
    val lineHeight = scopeHeight - space

    // line1
    val line1y1 = max(0f, sy1 - lineHeight)
    val line1y2 = max(line1y1, sy1)

    // line2
    val line2y1 = min(sy2, scopeHeight)
    val line2y2 = min(line2y1 + lineHeight, scopeHeight)

    // draw
    drawLine(
    Color.Black,
    Offset(x, line1y1),
    Offset(x, line1y2),
    strokeWidth = width,
    colorFilter = ColorFilter.tint(
    Color.Black
    ),
    cap = StrokeCap.Round
    )

    drawLine(
    Color.Black,
    Offset(x, line2y1),
    Offset(x, line2y2),
    strokeWidth = width,
    colorFilter = ColorFilter.tint(
    Color.Black
    ),
    cap = StrokeCap.Round
    )
    }
    }

    复制代码

    Compose自定义布局


    完成了单个雨滴的动画,接下来我们使用三个雨滴组成雨水的效果。


    首先可以使用Row+Space的方式进行组装,但是这种方式缺少灵活性,仅通过Modifier很难准确布局三雨滴的相对位,因此考虑借助Compose的自定义布局,以提高灵活性和准确性:


    Layout(
    modifier = modifier.rotate(30f), //雨滴旋转角度
    content = { // 定义子Composable
    Raindrop(modifier.fillMaxSize())
    Raindrop(modifier.fillMaxSize())
    Raindrop(modifier.fillMaxSize())
    }
    ) { measurables, constraints ->
    // List of measured children
    val placeables = measurables.mapIndexed { index, measurable ->
    // Measure each children
    val height = when (index) { //让三个雨滴的height不同,增加错落感
    0 -> constraints.maxHeight * 0.8f
    1 -> constraints.maxHeight * 0.9f
    2 -> constraints.maxHeight * 0.6f
    else -> 0f
    }
    measurable.measure(
    constraints.copy(
    minWidth = 0,
    minHeight = 0,
    maxWidth = constraints.maxWidth / 10, // raindrop width
    maxHeight = height.toInt(),
    )
    )
    }

    // Set the size of the layout as big as it can
    layout(constraints.maxWidth, constraints.maxHeight) {
    var xPosition = constraints.maxWidth / ((placeables.size + 1) * 2)

    // Place children in the parent layout
    placeables.forEachIndexed { index, placeable ->
    // Position item on the screen
    placeable.place(x = xPosition, y = 0)

    // Record the y co-ord placed up to
    xPosition += (constraints.maxWidth / ((placeables.size + 1) * 0.8f)).roundToInt()
    }
    }
    }
    复制代码

    Compose中可以使用Layout{...}对Composable进行自定义布局,content{...}中定义参与布局的子Composable。


    跟传统Android视图一样,自定义布局需要先后经历measurelayout两步。



    • measruemeasurables返回所有待测量的子Composable,constraints类似于MeasureSpec,封装父容器对子元素的布局约束。measurable.measure()中对子元素进行测量

    • layoutplaceables返回测量后的子元素,依次调用placeable.place()对雨滴进行布局,通过xPosition预留雨滴在x轴的间隔


    经过layout之后,通过 modifier.rotate(30f) 对Composable进行旋转,完成最终效果:


    在这里插入图片描述




    4. 雪天效果




    雪天效果的关键在于雪花的飘落。


    在这里插入图片描述


    雪花的绘制


    雪花的绘制非常简单,用一个圆圈代表一个雪花


    Canvas(modifier) {

    val radius = size / 2

    drawCircle( //白色填充
    color = Color.White,
    radius = radius,
    style = FILL
    )

    drawCircle(// 黑色边框
    color = Color.Black,
    radius = radius,
    style = Stroke(width = radius * 0.5f)
    )
    }
    复制代码

    雪花飘落动画


    雪花飘落的过程相对于雨滴坠落要复杂一些,由三个动画组成:



    1. 下落:改变y轴坐标:0f ~ 2.5f

    2. 左右飘移:改变x轴的offset:-1f ~ 1f

    3. 逐渐消失:改变alpha:1f ~ 0f


    借助InfiniteTransition同步控制多个动画,代码如下:


    @Composable
    private fun Snowdrop(
    modifier: Modifier = Modifier,
    durationMillis: Int = 1000 // 雪花飘落动画的druation
    ) {

    //循环播放的Transition
    val transition = rememberInfiniteTransition()

    //1. 下降动画:restart动画
    val animateY by transition.animateFloat(
    initialValue = 0f,
    targetValue = 2.5f,
    animationSpec = infiniteRepeatable(
    tween(durationMillis, easing = LinearEasing),
    RepeatMode.Restart
    )
    )

    //2. 左右飘移:reverse动画
    val animateX by transition.animateFloat(
    initialValue = -1f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
    tween(durationMillis / 3, easing = LinearEasing),
    RepeatMode.Reverse
    )
    )

    //3. alpha值:restart动画,以0f结束
    val animateAlpha by transition.animateFloat(
    initialValue = 1f,
    targetValue = 0f,
    animationSpec = infiniteRepeatable(
    tween(durationMillis, easing = FastOutSlowInEasing),
    )
    )

    Canvas(modifier) {

    val radius = size.width / 2

    // 圆心位置随AnimationState改变,实现雪花飘落的效果
    val _center = center.copy(
    x = center.x + center.x * animateX,
    y = center.y + center.y * animateY
    )

    drawCircle(
    color = Color.White.copy(alpha = animateAlpha),//alpha值的变化实现雪花消失效果
    center = _center,
    radius = radius,
    )

    drawCircle(
    color = Color.Black.copy(alpha = animateAlpha),
    center = _center,
    radius = radius,
    style = Stroke(width = radius * 0.5f)
    )
    }
    }
    复制代码

    animateYtargetValue设为2.5f是为了让雪花的运动轨迹更长,看起来更加真实


    雪花的自定义布局


    像雨滴一样,对雪花也使用Layout自定义布局


    @Composable
    fun Snow(
    modifier: Modifier = Modifier,
    animate: Boolean = false,
    ) {

    Layout(
    modifier = modifier,
    content = {
    //摆放三个雪花,分别设置不同duration,增加随机性
    Snowdrop( modifier.fillMaxSize(), 2200)
    Snowdrop( modifier.fillMaxSize(), 1600)
    Snowdrop( modifier.fillMaxSize(), 1800)
    }
    ) { measurables, constraints ->
    val placeables = measurables.mapIndexed { index, measurable ->
    val height = when (index) {
    // 雪花的height不同,也是为了增加随机性
    0 -> constraints.maxHeight * 0.6f
    1 -> constraints.maxHeight * 1.0f
    2 -> constraints.maxHeight * 0.7f
    else -> 0f
    }
    measurable.measure(
    constraints.copy(
    minWidth = 0,
    minHeight = 0,
    maxWidth = constraints.maxWidth / 5, // snowdrop width
    maxHeight = height.roundToInt(),
    )
    )
    }

    layout(constraints.maxWidth, constraints.maxHeight) {
    var xPosition = constraints.maxWidth / ((placeables.size + 1))

    placeables.forEachIndexed { index, placeable ->
    placeable.place(x = xPosition, y = -(constraints.maxHeight * 0.2).roundToInt())

    xPosition += (constraints.maxWidth / ((placeables.size + 1) * 0.9f)).roundToInt()
    }
    }
    }
    }
    复制代码

    最终效果如下:


    在这里插入图片描述




    5. 晴天效果




    通过一个旋转的太阳代表晴天效果


    在这里插入图片描述


    太阳的绘制


    太阳的图形由中心圆形和围绕圆环的等分线段组成。


    @Composable
    fun Sun(modifier: Modifier = Modifier) {

    Canvas(modifier) {

    val radius = size.width / 6
    val stroke = size.width / 20

    // draw circle
    drawCircle(
    color = Color.Black,
    radius = radius + stroke / 2,
    style = Stroke(width = stroke),
    )
    drawCircle(
    color = Color.White,
    radius = radius,
    style = Fill,
    )

    // draw line

    val lineLength = radius * 0.2f
    val lineOffset = radius * 1.8f
    (0..7).forEach { i ->

    val radians = Math.toRadians(i * 45.0)

    val offsetX = lineOffset * cos(radians).toFloat()
    val offsetY = lineOffset * sin(radians).toFloat()

    val x1 = size.width / 2 + offsetX
    val x2 = x1 + lineLength * cos(radians).toFloat()

    val y1 = size.height / 2 + offsetY
    val y2 = y1 + lineLength * sin(radians).toFloat()

    drawLine(
    color = Color.Black,
    start = Offset(x1, y1),
    end = Offset(x2, y2),
    strokeWidth = stroke,
    cap = StrokeCap.Round
    )
    }
    }
    }
    复制代码

    均分360度,每间隔45度画一条线段,cos计算x轴坐标,sin计算y轴坐标。


    太阳的旋转


    太阳的旋转动画很简单,通过Modifier.rotate不断转动Canvas即可。


    @Composable
    fun Sun(modifier: Modifier = Modifier) {

    //循环动画
    val animateTween by rememberInfiniteTransition().animateFloat(
    initialValue = 0f,
    targetValue = 360f,
    animationSpec = infiniteRepeatable(tween(5000), RepeatMode.Restart)
    )

    Canvas(modifier.rotate(animateTween)) {// 旋转动画

    val radius = size.width / 6
    val stroke = size.width / 20
    val centerOffset = Offset(size.width / 30, size.width / 30) //圆心偏移量

    // draw circle
    drawCircle(
    color = Color.Black,
    radius = radius + stroke / 2,
    style = Stroke(width = stroke),
    center = center + centerOffset //圆心偏移
    )

    //...略
    }
    }
    复制代码

    此外,DrawScope提供了rotate的API,也可以实现旋转效果。


    最后我们给太阳的圆心增加一个偏移量,让转动更加活泼:


    在这里插入图片描述




    6. 动画的组合、切换




    在实现了RainSnowSun等图形后,就可以使用这些图形组合成各种天气效果了。


    将图形组合成天气


    Compose的声明式语法非常有利于UI的组合:


    比如,多云转阵雨,我们摆放SunCloudRain等元素后,通过Modifier调整各自位置即可:


    @Composable
    fun CloudyRain(modifier: Modifier) {
    Box(modifier.size(200.dp)){
    Sun(Modifier.size(120.dp).offset(140.dp, 40.dp))
    Rain(Modifier.size(80.dp).offset(80.dp, 60.dp))
    Cloud(Modifier.align(Aligment.Center))
    }
    }
    复制代码

    让动画切换更加自然


    在这里插入图片描述


    当在多个天气动画之间进行切换时,我们希望能实现更自然的过渡。实现思路是将组成天气动画的各元素的Modifier配置变量化,然后通过Animation不断改变


    假设所有的天气都由CloudSunRain组成,无非就是offsetsizealpha值的不同:


    ComposeInfo


    data class IconInfo(
    val size: Float = 1f,
    val offset: Offset = Offset(0f, 0f),
    val alpha: Float = 1f,
    )
    复制代码

    //天气组合信息,即Sun、Cloud、Rain的位置信息
    data class ComposeInfo(
    val sun: IconInfo,
    val cloud: IconInfo,
    val rains: IconInfo,

    ) {
    operator fun times(float: Float): ComposeInfo =
    copy(
    sun = sun * float,
    cloud = cloud * float,
    rains = rains * float
    )

    operator fun minus(composeInfo: ComposeInfo): ComposeInfo =
    copy(
    sun = sun - composeInfo.sun,
    cloud = cloud - composeInfo.cloud,
    rains = rains - composeInfo.rains,
    )

    operator fun plus(composeInfo: ComposeInfo): ComposeInfo =
    copy(
    sun = sun + composeInfo.sun,
    cloud = cloud + composeInfo.cloud,
    rains = rains + composeInfo.rains,
    )
    }

    复制代码

    如上,ComposeInfo中持有各种元素的位置信息,运算符重载用于跟随Animation计算当前最新值。


    定义不同天气的ComposeInfo如下:


    //晴天
    val SunnyComposeInfo = ComposeInfo(
    sun = IconInfo(1f),
    cloud = IconInfo(0.8f, Offset(-0.1f, 0.1f), 0f),
    rains = IconInfo(0.4f, Offset(0.225f, 0.3f), 0f),
    )

    //多云
    val CloudyComposeInfo = ComposeInfo(
    sun = IconInfo(0.1f, Offset(0.75f, 0.2f), alpha = 0f),
    cloud = IconInfo(0.8f, Offset(0.1f, 0.1f)),
    rains = IconInfo(0.4f, Offset(0.225f, 0.3f), alpha = 0f),
    )

    //雨天
    val RainComposeInfo = ComposeInfo(
    sun = IconInfo(0.1f, Offset(0.75f, 0.2f), alpha = 0f),
    cloud = IconInfo(0.8f, Offset(0.1f, 0.1f)),
    rains = IconInfo(0.4f, Offset(0.225f, 0.3f), alpha = 1f),
    )

    复制代码

    ComposedIcon


    接着,定义ComposedIcon,消费ComposeInfo绘制天气组合的UI


    @Composable
    fun ComposedIcon(modifier: Modifier = Modifier, composeInfo: ComposeInfo) {

    //各元素的ComposeInfo
    val (sun, cloud, rains) = composeInfo

    Box(modifier) {

    //应用ComposeInfo到Modifier
    val _modifier = remember(Unit) {
    { icon: IconInfo ->
    Modifier
    .offset( icon.size * icon.offset.x, icon.size * icon.offset.y )
    .size(icon.size)
    .alpha(icon.alpha)
    }
    }

    Sun(_modifier(sun))
    Rains(_modifier(rains))
    AnimatableCloud(_modifier(cloud))
    }
    }
    复制代码

    ComposedWeather


    最后,定义ComposedWeather,通过动画更新当前的ComposedIcon


    @Composable
    fun ComposedWeather(modifier: Modifier, composedIcon: ComposedIcon) {

    val (cur, setCur) = remember { mutableStateOf(composedIcon) }
    var trigger by remember { mutableStateOf(0f) }

    DisposableEffect(composedIcon) {
    trigger = 1f
    onDispose { }
    }

    //创建动画(0f ~ 1f),用于更新ComposeInfo
    val animateFloat by animateFloatAsState(
    targetValue = trigger,
    animationSpec = tween(1000)
    ) {
    //当动画结束时,更新ComposeWeather到最新state
    setCur(composedIcon)
    trigger = 0f
    }

    //根据AnimationState计算当前ComposeInfo
    val composeInfo = remember(animateFloat) {
    cur.composedIcon + (weatherIcon.composedIcon - cur.composedIcon) * animateFloat
    }

    //使用最新的ComposeInfo显示Icon
    ComposedIcon(
    modifier,
    composeInfo
    )
    }
    复制代码

    到此,我们就实现了天气动画的自然过度了。



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

    为Fragment换装ViewPager2

    1. 开启ViewPager2之旅距离ViewPager2正式版的发布已经一年多了,目前ViewPager早已停止更新,官方鼓励使用ViewPager2替代。 ViewPager2底层基于RecyclerView实现,因此可以获得RecyclerView带来的...
    继续阅读 »

    为Fragment换装ViewPager2

    1. 开启ViewPager2之旅

    image.png

    距离ViewPager2正式版的发布已经一年多了,目前ViewPager早已停止更新,官方鼓励使用ViewPager2替代。 ViewPager2底层基于RecyclerView实现,因此可以获得RecyclerView带来的诸多收益:

    • 抛弃传统的PagerAdapter,统一了Adapter的API
    • 通过LinearLayoutManager可以实现类似抖音的纵向滑动
    • 支持DiffUitl,可以实现局部刷新
    • 支持RTL(right-to-left),对于一些有出海需求的APP非常有用
    • 支持ItemDecorator

    2. ViewPager2 + Fragment

    跟ViewPager一样,除了View以外,ViewPager2更多的是配合Fragment使用,这需要借助于FragmentStateAdapter

    image.png

    接下来,本文简单介绍一下FragmentStateAdapter的使用及实现原理:

    首先在gradle中引入ViewPager2:

     implementation 'androidx.viewpager2:viewpager2:1.1.0'
    复制代码

    然后在xml中布局:

    <androidx.viewpager2.widget.ViewPager2
    android:id="@+id/doppelgangerViewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    复制代码

    FragmentStateAdapter

    import androidx.appcompat.app.AppCompatActivity
    import androidx.fragment.app.Fragment
    import androidx.viewpager2.adapter.FragmentStateAdapter

    class DoppelgangerAdapter(activity: AppCompatActivity, val doppelgangerList: List<DoppelgangerItem>) :
    FragmentStateAdapter(activity) {

    override fun getItemCount(): Int {
    return doppelgangerList.size
    }

    override fun createFragment(position: Int): Fragment {
    return DoppelgangerFragment.getInstance(doppelgangerList[position])
    }
    }
    复制代码

    FragmentStateAdapter的API跟旧的Adapter很相似:

    • getItemCount:返回Item的数量
    • createFragment:用来根据position创建fragment
    • DoppelgangerFragment:创建的具体Fragment类型

    MainActivity

    在Activity中为ViewPager2设置Adapter:

    val doppelgangerAdapter = DoppelgangerAdapter(this, doppelgangerList) 
    doppelgangerViewPager.adapter = doppelgangerAdapter
    复制代码

    在这里插入图片描述


    3. 揭秘FragmentStateAdapter的实现

    因为ViewPager2继承自RecyclerView,因此可以推断出FragmentStateAdapter继承自RecyclerView.Adapter

    public abstract class FragmentStateAdapter extends 
    RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
    }
    复制代码

    虽说是继承关系,但两者的API却不一致,RecyclerView.Adapter关注的是ViewHolder的复用,而在FragmentStateAdapter中Framgent是不会复用的,即有多少个item就应该创建多少个Fragment,那么这其中是如何转换的呢?

    onCreateViewHolder

    通过FragmentStateAdapter声明中的泛型可以知道,ViewPager2之所以能够在RecyclerView的基础上对外屏蔽对ViewHolder的使用,其内部是借助FragmentViewHolder实现的。

    onCreateViewHolder中会创建一个FragmentViewHolder

    @NonNull
    @Override
    public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return FragmentViewHolder.create(parent);
    }
    复制代码

    FragmentViewHolder的主要作用是通过FrameLayout为Fragment提供用作容器的container:

    @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
    FrameLayout container = new FrameLayout(parent.getContext());
    container.setLayoutParams(
    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    ViewGroup.LayoutParams.MATCH_PARENT));
    container.setId(ViewCompat.generateViewId());
    container.setSaveEnabled(false);
    return new FragmentViewHolder(container);
    }
    复制代码

    onBindViewHolder

    @Override
    public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    ...
    ensureFragment(position);
    ...
    gcFragments();
    }
    复制代码

    ensureFragment(position),其内部会最终回调用createFragment创建当前Fragment

       private void ensureFragment(int position) {
    long itemId = getItemId(position);
    if (!mFragments.containsKey(itemId)) {
    // TODO(133419201): check if a Fragment provided here is a new Fragment
    Fragment newFragment = createFragment(position);
    newFragment.setInitialSavedState(mSavedStates.get(itemId));
    mFragments.put(itemId, newFragment);
    }
    }
    复制代码

    mFragments缓存创建的Fragment,供后面placeFramentInViewholder使用; gcFragments回收已经不再使用的的Fragment(对应的item已经删除),节省内存开销。

    placeFragmentInViewHolder

      @Override
    public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
    placeFragmentInViewHolder(holder);
    gcFragments();
    }
    复制代码

    onViewAttachToWindow的时候调用placeFragmentInViewHolder,将FragmentViewHolder的container与当前Fragment绑定

        void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
    Fragment fragment = mFragments.get(holder.getItemId());
    if (fragment == null) {
    throw new IllegalStateException("Design assumption violated.");
    }
    FrameLayout container = holder.getContainer();
    View view = fragment.getView();

    ...
    if (fragment.isAdded() && view.getParent() != null) {
    if (view.getParent() != container) {
    addViewToContainer(view, container);
    }
    return;
    }
    ...
    }
    复制代码

    void addViewToContainer(@NonNull View v, @NonNull FrameLayout container) {
    ...

    if (container.getChildCount() > 0) {
    container.removeAllViews();
    }

    if (v.getParent() != null) {
    ((ViewGroup) v.getParent()).removeView(v);
    }

    container.addView(v);
    }
    复制代码

    通过上面源码分析可以知道,虽然Fragment没有被复用,但是通过复用了ViewHolder的container实现了Framgent的交替显示


    4. 滑动监听

    监听页面滑动是一个常见需求,ViewPager2的API也发生了变化,使用OnPageChangeCallback

    image.png

    使用效果如下:

    var doppelgangerPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
    Toast.makeText(this@MainActivity, "Selected position: ${position}",
    Toast.LENGTH_SHORT).show()
    }
    }
    复制代码

    OnPageChangeCallback同样也有三个方法:

    • onPageScrolled: 当前页面开始滑动时
    • onPageSelected: 当页面被选中时
    • onPageScrollStateChanged: 当前页面滑动状态变动时

    在这里插入图片描述


    5. 纵向滑动

    设置纵向滑动很简单,一行代码搞定

    doppelgangerViewPager.orientation = ViewPager2.ORIENTATION_VERTICAL
    复制代码

    在这里插入图片描述

    源码也很简单

    /**
    * Sets the orientation of the ViewPager2.
    *
    * @param orientation {@link #ORIENTATION_HORIZONTAL} or {@link #ORIENTATION_VERTICAL}
    */

    public void setOrientation(@Orientation int orientation) {
    mLayoutManager.setOrientation(orientation);
    mAccessibilityProvider.onSetOrientation();
    }
    复制代码

    6. TabLayout


    配合TabLayout的使用也是一个常见需求,TabLayout需要引入material

    implementation 'com.google.android.material:material:1.2.0-alpha04'
    复制代码

    然后在xml中声明

    <com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorPrimary"
    app:tabMode="scrollable"
    app:tabTextColor="@android:color/white" />

    复制代码

    TabsLayoutMediator

    要关联TabLayout和ViewPager2需要借助TabLayoutMediator

    public TabLayoutMediator(
    @NonNull TabLayout tabLayout,
    @NonNull ViewPager2 viewPager,
    @NonNull TabConfigurationStrategy tabConfigurationStrategy)
    {
    this(tabLayout, viewPager, true, tabConfigurationStrategy);
    }
    复制代码

    其中,TabConfigurationStrategy定义如下:根据position配置当前tab

    /**
    * A callback interface that must be implemented to set the text and styling of newly created
    * tabs.
    */

    public interface TabConfigurationStrategy {
    /**
    * Called to configure the tab for the page at the specified position. Typically calls {@link
    * TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
    *
    * @param tab The Tab which should be configured to represent the title of the item at the given
    * position in the data set.
    * @param position The position of the item within the adapter's data set.
    */

    void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
    }
    复制代码

    在MainActivity中具体使用如下:

    TabLayoutMediator(tabLayout, doppelgangerViewPager) { tab, position ->
    //To get the first name of doppelganger celebrities
    tab.text = doppelgangerList[position].title
    }.attach()
    复制代码

    attach方法很关键,经过前面一系列配置后最终需要通过它关联两个组件。

    加入TabLayout后的最终效果如下:

    在这里插入图片描述


    7. DiffUtil 局部更新

    RecyclerView基于DiffUtil可以实现局部更新,如今,FragmentStateAdapter也可以对Fragment实现局部更新。

    首先定义DiffUtil.Callback

    class PagerDiffUtil(private val oldList: List<DoppelgangerItem>, private val newList: List<DoppelgangerItem>) : DiffUtil.Callback() {

    enum class PayloadKey {
    VALUE
    }

    override fun getOldListSize() = oldList.size

    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    return oldList[oldItemPosition].value == newList[newItemPosition].value
    }

    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
    return listOf(PayloadKey.VALUE)
    }
    }
    复制代码

    然后在Adapter中使用DiffUtil更新数据


    class DoppelgangerAdapter(private val activity: FragmentActivity) : FragmentStateAdapter(activity) {

    private val items: ArrayList<DoppelgangerItem> = arrayListOf()


    override fun createFragment(position: Int): Fragment {
    return DoppelgangerFragment.getInstance(doppelgangerList[position])
    }

    override fun getItemCount() = items.size

    override fun getItemId(position: Int): Long {
    return items[position].id.toLong()
    }

    override fun containsItem(itemId: Long): Boolean {
    return items.any { it.id.toLong() == itemId }
    }

    fun setItems(newItems: List<PagerItem>) {
    //不借助DiffUtil更新数据
    //items.clear()
    //items.addAll(newItems)
    //notifyDataSetChanged()

    //使用DiffUtil更新数据
    val callback = PagerDiffUtil(items, newItems)
    val diff = DiffUtil.calculateDiff(callback)
    items.clear()
    items.addAll(newItems)
    diff.dispatchUpdatesTo(this)
    }
    }
    复制代码

    8. 总结

    本文主要介绍了ViewPager2配合Fragment的使用方法以及FragmentStateAdapter的实现原理,顺带介绍了TabLayoutOnPageChangeCallbackDiffUtil等常见功能的用法。ViewPager2的使用非常简单,在性能以及使用体验等各方面都要优于传统的ViewPager,没尝试的小伙伴抓紧用起来吧~

    收起阅读 »

    一道面试题:ViewModel为什么横竖屏切换时不销毁?

    又到金三银四 往年面试中有关Jetpack的考察可以算是加分项,随着官方对Modern Android development (MAD) 的大力推广,今年基本上都是必选题了。 很多候选人对Jetpack各组件的功能及用法如数家珍,但一问及到原理往往卡壳。原理...
    继续阅读 »

    又到金三银四


    往年面试中有关Jetpack的考察可以算是加分项,随着官方对Modern Android development (MAD) 的大力推广,今年基本上都是必选题了。


    很多候选人对Jetpack各组件的功能及用法如数家珍,但一问及到原理往往卡壳。原理不清虽不影响API的使用,但也正因为如此,如果能对源码有一定了解,也许可以脱颖而出得到加分。


    本文分享一个入门级的源码分析,也是在面试中经常被问到的问题




    # ViewModel


    ViewModel是Android Jetpack中的重要组件,其优势是具有下图这样的生命周期、不会因为屏幕旋转等Activity配置变化而销毁,是实现MVVM架构中UI状态管理的重要基础。
    在这里插入图片描述


    class MyActivity : AppCompatActivity {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d(TAG, "onCreate")

    val activity: FragmentActivity = this
    val factory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory()

    // Activity由于横竖品切换销毁重建,此处的viewModel 仍然是重建前的实例
    val viewModel = ViewModelProvider(activity, factory).get(MyViewModel::class.java)
    // 如果直接new实例则会创建新的ViewModel实例
    // val viewModel = MyViewModel()

    Log.d(TAG, " - Activity :${this.hashCode()}")
    Log.d(TAG, " - ViewModel:${viewModel.hashCode()}")
    }
    }
    复制代码

    上面代码在横竖屏切换时的log如下:


    #Activity初次启动
    onCreate
    - Activity :132818886
    - ViewModel:249530701
    onStart
    onResume

    #屏幕旋转
    onPause
    onStop
    onRetainNonConfigurationInstance
    onDestroy
    onCreate
    - Activity :103312713 #Activity实例不同
    - ViewModel:249530701 #ViewModel实例相同
    onStart
    onResume
    复制代码

    下面代码是保证屏幕切换时ViewModel不销毁的关键,我们依次为入口看一下源码


    val viewModel = ViewModelProvider(activity, factory).get(MyViewModel::class.java)
    复制代码



    # ViewModelProvider


    ViewModelProvider源码很简单,分别持有一个ViewModelProvider.FactoryViewModelStore实例


    package androidx.lifecycle;

    public class ViewModelProvider {

    public interface Factory {
    @NonNull
    <T extends ViewModel> T create(@NonNull Class<T> modelClass);
    }

    private final Factory mFactory;
    private final ViewModelStore mViewModelStore;

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
    this(owner.getViewModelStore(), factory);
    }

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    mFactory = factory;
    this.mViewModelStore = store;
    }

    ...
    }
    复制代码

    get()返回ViewModel实例


    package androidx.lifecycle;

    public class ViewModelProvider {
    ...

    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();

    ...

    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }

    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
    //noinspection unchecked
    return (T) viewModel;
    } else {
    //noinspection StatementWithEmptyBody
    if (viewModel != null) {
    // TODO: log a warning.
    }
    }

    viewModel = mFactory.create(modelClass);
    mViewModelStore.put(key, viewModel);
    //noinspection unchecked
    return (T) viewModel;
    }

    ...
    }
    复制代码

    逻辑非常清晰:



    1. ViewModelProvider通过ViewModelStore获取ViewModel

    2. 若获取失败,则通过ViewModelProvider.Factory创建ViewModel




    # ViewModelStore


    package androidx.lifecycle;

    public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
    ViewModel oldViewModel = mMap.put(key, viewModel);
    if (oldViewModel != null) {
    oldViewModel.onCleared();
    }
    }

    final ViewModel get(String key) {
    return mMap.get(key);
    }

    public final void clear() {
    for (ViewModel vm : mMap.values()) {
    vm.onCleared();
    }
    mMap.clear();
    }
    }
    复制代码

    可见,ViewModelStore就是一个对Map的封装。


    val viewModel = ViewModelProvider(activity, factory).get(FooViewModel::class.java)
    复制代码

    上面代码ViewModelProvider()构造参数1中传入的FragmentActivity(基类是ComponentActivity)实际上是ViewModelStoreOwner的一个实现。


    package androidx.lifecycle;

    public interface ViewModelStoreOwner {
    @NonNull
    ViewModelStore getViewModelStore();
    }
    复制代码

    ViewModelProvider中的ViewModelStore正是来自ViewModelStoreOwner。


    public class ViewModelProvider {

    private final ViewModelStore mViewModelStore;

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
    this(owner.getViewModelStore(), factory);
    }

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    this.mViewModelStore = store;
    }
    复制代码

    Activity在onDestroy会尝试对ViewModelStore清空。如果是由于ConfigurationChanged带来的Destroy则不进行清空,避免横竖屏切换等造成ViewModel销毁。


    //ComponentActivity.java
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
    @NonNull Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    // Clear out the available context
    mContextAwareHelper.clearAvailableContext();
    // And clear the ViewModelStore
    if (!getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
    @NonNull Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    // Clear out the available context
    mContextAwareHelper.clearAvailableContext();
    // And clear the ViewModelStore
    if (!isChangingConfigurations()) {
    getViewModelStore().clear();
    }
    }
    }
    });()) {
    getViewModelStore().clear();
    }
    }
    }
    });
    复制代码



    # FragmentActivity#getViewModelStore()


    FragmentActivity实现了ViewModelStoreOwnergetViewModelStore方法


    package androidx.fragment.app;

    public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner ... {

    private ViewModelStore mViewModelStore;

    @NonNull
    @Override
    public ViewModelStore getViewModelStore() {
    ...

    if (mViewModelStore == null) {
    NonConfigurationInstances nc =
    (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) {
    // Restore the ViewModelStore from NonConfigurationInstances
    mViewModelStore = nc.viewModelStore;
    }
    if (mViewModelStore == null) {
    mViewModelStore = new ViewModelStore();
    }
    }
    return mViewModelStore;
    }

    static final class NonConfigurationInstances {
    Object custom;
    ViewModelStore viewModelStore;
    FragmentManagerNonConfig fragments;
    }

    ...
    }
    复制代码

    通过getLastNonConfigurationInstance() 获取 NonConfigurationInstances 实例,从而得到真正的viewModelStoregetLastNonConfigurationInstance()又是什么?


    # Activity#getLastNonConfigurationInstance()


    package android.app;

    public class Activity extends ContextThemeWrapper implements ... {

    /* package */ NonConfigurationInstances mLastNonConfigurationInstances;

    @Nullable
    public Object getLastNonConfigurationInstance() {
    return mLastNonConfigurationInstances != null
    ? mLastNonConfigurationInstances.activity : null;
    }
    复制代码


    Retrieve the non-configuration instance data that was previously returned by onRetainNonConfigurationInstance(). This will be available from the initial onCreate(Bundle) and onStart() calls to the new instance, allowing you to extract any useful dynamic state from the previous instance.



    通过官方文档我们知道,屏幕旋转前通过onRetainNonConfigurationInstance()返回的Activity实例,屏幕旋转后可以通过getLastNonConfigurationInstance()获取,因此屏幕旋转前后不销毁的关键就在onRetainNonConfigurationInstance




    # Activity#onRetainNonConfigurationInstance()


    #Activity初次启动
    onCreate
    - Activity :132818886
    - ViewModel:249530701
    onStart
    onResume

    #屏幕旋转
    onPause
    onStop
    onRetainNonConfigurationInstance
    onDestroy
    onCreate
    - Activity :103312713 #Activity实例不同
    - ViewModel:249530701 #ViewModel实例相同
    onStart
    onResume
    复制代码

    屏幕旋转时,onRetainNonConfigurationInstance()onStoponDestroy之间调用


    package android.app;

    public class Activity extends ContextThemeWrapper implements ... {

    public Object onRetainNonConfigurationInstance() {
    return null;
    }

    ...
    }
    复制代码

    onRetainNonConfigurationInstance在Activity中只有空实现,在FragmentActivity中被重写


    package androidx.fragment.app;

    public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner, ... {

    @Override
    public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();

    FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

    if (fragments == null && mViewModelStore == null && custom == null) {
    return null;
    }

    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = mViewModelStore;
    nci.fragments = fragments;
    return nci;
    }

    static final class NonConfigurationInstances {
    Object custom;
    ViewModelStore viewModelStore;
    FragmentManagerNonConfig fragments;
    }

    ...
    }
    复制代码

    FragmentActivity 通过 onRetainNonConfigurationInstance() 返回 了存放ViewModelStore的NonConfigurationInstances 实例。
    值得一提的是onRetainNonConfigurationInstance提供了一个hook时机:onRetainCustomNonConfigurationInstance,允许我们像ViewModel一样使得自定义对象不被销毁


    NonConfigurationInstances会在attach中由系统传递给新重建的Activity:


    final void attach(Context context, ActivityThread aThread,
    Instrumentation instr, IBinder token, int ident,
    Application application, Intent intent, ActivityInfo info,
    CharSequence title, Activity parent, String id,
    NonConfigurationInstances lastNonConfigurationInstances,
    Configuration config, String referrer, IVoiceInteractor voiceInteractor,
    Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken)

    复制代码

    然后在onCreate中,通过getLastNonConfigurationInstance()获取NonConfigurationInstances中的ViewModelStore


    package androidx.fragment.app;

    public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner ... {

    private ViewModelStore mViewModelStore;

    @SuppressWarnings("deprecation")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    mFragments.attachHost(null /*parent*/);

    super.onCreate(savedInstanceState);

    NonConfigurationInstances nc =
    (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null && nc.viewModelStore != null && mViewModelStore == null) {
    mViewModelStore = nc.viewModelStore;
    }
    ...
    }
    }
    复制代码



    # 总结


    Activity首次启动



    • FragmentActivity#onCreate()被调用

      • 此时 FragmentActivity 的 mViewModelStore 尚为 null



    • HogeActivity的onCreate() 被调用

      • ViewModelProvider 实例创建

      • FragmentActivity#getViewModelStore() 被调用,mViewModelStore被创建并赋值




    发生屏幕旋转



    • FragmentActivity#onRetainNonConfigurationInstance() 被调用

      • 持有mViewModelStore 的NonConfigurationInstances 实例被返回




    Activity重建



    • FragmentActivity#onCreate() 被调用

      • 从Activity#getLastNonConfigurationInstance() 获取 NonConfigurationInstances 实例

      • NonConfigurationInstances 中保存了屏幕旋转前的 FragmentActivity 的 mViewModelStore,将其赋值给重建后的FragmentActivity 的 mViewModelStore



    • HogeActivity#onCreate() 被调用

      • 通过ViewModelProvider#get() 获取 ViewModel 实例




    收起阅读 »

    iOS内存管理-深入解析自动释放池

    主要内容:AutoreleasePool简介AutoreleasePool底层原理Autorelease与NSThread、NSRunLoop的关系AutoreleasePool在主线程上的释放时机AutoreleasePool在子线程上的释放时机Autore...
    继续阅读 »

    主要内容:

  • AutoreleasePool简介
  • AutoreleasePool底层原理
  • Autorelease与NSThread、NSRunLoop的关系
  • AutoreleasePool在主线程上的释放时机
  • AutoreleasePool在子线程上的释放时机
  • AutoreleasePool需要手动添加的情况
  • 一、Autorelease简介

    iOS开发中的Autorelease机制是为了延时释放对象。自动释放的概念看上去很像ARC,但实际上这更类似于C语言中自动变量的特性。

    自动变量:在超出变量作用域后将被废弃;
    自动释放池:在超出释放池生命周期后,向其管理的对象实例的发送release消息。

    1.1 MRC下使用自动释放池
    在MRC环境中使用自动释放池需要用到NSAutoreleasePool对象,其生命周期就相当于C语言变量的作用域。对于所有调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:
    //MRC环境下的测试:
    //第一步:生成并持有释放池NSAutoreleasePool对象;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    //第二步:调用对象的autorelease实例方法;
    id obj = [[NSObject alloc] init];
    [obj autorelease];

    //第三步:废弃NSAutoreleasePool对象;
    [pool drain]; //向pool管理的所有对象发送消息,相当于[obj release]

    //obi已经释放,再次调用会崩溃(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
    NSLog(@"打印obj:%@", obj);

    理解NSAutoreleasePool对象的生命周期,如下图所示:


    1.2 ARC下使用自动释放池
    ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是@autoreleasepool块和__autoreleasing修饰符。比较两种环境下的代码差异如下图:

    如图所示,@autoreleasepool块替换了NSAutoreleasePoool类对象的生成、持有及废弃这一过程。而附有__autoreleasing修饰符的变量替代了autorelease方法,将对象注册到了Autoreleasepool;由于ARC的优化,__autorelease是可以被省略的,所以简化后的ARC代码如下:
    //ARC环境下的测试:
    @autoreleasepool {
    id obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }

    显式使用__autoreleasing修饰符的情况非常少见,这是因为ARC的很多情况下,即使是不显式的使用__autoreleasing,也能实现对象被注册到释放池中。主要包括以下几种情况:

  • 编译器会进行优化,检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回对象注册到Autoreleasepool;
  • 访问附有__weak修饰符的变量时,实际上必定要访问注册到Autoreleasepool的对象,即会自动加入Autoreleasepool;
  • id的指针或对象的指针(id*,NSError **),在没有显式地指定修饰符时候,会被默认附加上__autoreleasing修饰符,加入Autoreleasepool

  • 注意:如果编译器版本为LLVM.3.0以上,即使ARC无效@autoreleasepool块也能够使用;如下源码所示:

    //MRC环境下的测试:
    @autoreleasepool{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    }


    二、AutoRelease原理

    2.1 使用@autoreleasepool{}

    我们在main函数中写入自动释放池相关的测试代码如下:

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSLog(@"Hello, World!");
    }
    return 0;
    }

    为了探究释放池的底层实现,我们在终端使用clang -rewrite-objc + 文件名命令将上述OC代码转化为C++源码:

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
    __AtAutoreleasePool __autoreleasepool;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
    }//大括号对应释放池的作用域

    return 0;
    }

    在经过编译器clang命令转化后,我们看到的所谓的@autoreleasePool块,其实对应着__AtAutoreleasePool的结构体。

    2.2 分析结构体__AtAutoreleasePool的具体实现

    在源码中找到__AtAutoreleasePool结构体的实现代码,具体如下:

    extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
    extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

    struct __AtAutoreleasePool {
    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    void * atautoreleasepoolobj;
    };

    __AtAutoreleasePool结构体包含了:构造函数、析构函数和一个边界对象;
    构造函数内部调用:objc_autoreleasePoolPush()方法,返回边界对象atautoreleasepoolobj
    析构函数内部调用:objc_autoreleasePoolPop()方法,传入边界对象atautoreleasepoolobj

    分析main函数中__autoreleasepool结构体实例的生命周期是这样的:
    __autoreleasepool是一个自动变量,其构造函数是在程序执行到声明这个对象的位置时调用的,而其析构函数则是在程序执行到离开这个对象的作用域时调用。所以,我们可以将上面main函数的代码简化如下:

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
    }
    2.3 objc_autoreleasePoolPush与objc_autoreleasePoolPop

    进一步观察自动释放池构造函数与析构函数的实现,其实它们都只是对AutoreleasePoolPage对应静态方法pushpop的封装

    void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
    }

    void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
    }

    2.4 理解AutoreleasePoolPage
    AutoreleasePoolPage是一个C++中的类,打开Runtime的源码工程,在NSObject.mm文件中可以找到它的定义,摘取其中的关键代码如下:
    //大致在641行代码开始
    class AutoreleasePoolPage {
    # define EMPTY_POOL_PLACEHOLDER ((id*)1) //空池占位
    # define POOL_BOUNDARY nil //边界对象(即哨兵对象)
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
    static size_t const SIZE =
    #if PROTECT_AUTORELEASEPOOL
    PAGE_MAX_SIZE; // must be multiple of vm page size
    #else
    PAGE_MAX_SIZE; // size and alignment, power of 2
    #endif
    static size_t const COUNT = SIZE / sizeof(id);
    magic_t const magic; //校验AutoreleasePagePoolPage结构是否完整
    id *next; //指向新加入的autorelease对象的下一个位置,初始化时指向begin()
    pthread_t const thread; //当前所在线程,AutoreleasePool是和线程一一对应的
    AutoreleasePoolPage * const parent; //指向父节点page,第一个结点的parent值为nil
    AutoreleasePoolPage *child; //指向子节点page,最后一个结点的child值为nil
    uint32_t const depth; //链表深度,节点个数
    uint32_t hiwat; //数据容纳的一个上限
    //......
    };

    其实,每个自动释放池都是是由若干个AutoreleasePoolPage组成的双向链表结构,如下图所示:


    AutoreleasePoolPage中拥有parentchild指针,分别指向上一个和下一个page;当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的 Autorelease对象也会添加到新的page中;

    另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。

    2.5 理解哨兵对象/边界对象(POOL_BOUNDARY)的作用

    AutoreleasePoolPage的源码中,我们很容易找到边界对象(哨兵对象)的定义:

    #define POOL_BOUNDARY nil

    边界对象其实就是nil的别名,而它的作用事实上也就是为了起到一个标识的作用。

    每当自动释放池初始化调用objc_autoreleasePoolPush方法时,总会通过AutoreleasePoolPagepush方法,将POOL_BOUNDARY放到当前page的栈顶,并且返回这个边界对象;

    而在自动释放池释放调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。

    2.6 理解objc_autoreleasePoolPush方法
    经过前面的分析,objc_autoreleasePoolPush最终调用的是  AutoreleasePoolPagepush方法,该方法的具体实现如下:
    static inline void *push() {
    return autoreleaseFast(POOL_BOUNDARY);
    }

    static inline id *autoreleaseFast(id obj)
    {
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
    return page->add(obj);
    } else if (page) {
    return autoreleaseFullPage(obj, page);
    } else {
    1. return autoreleaseNoPage(obj);
    }
    }

    //压栈操作:将对象加入AutoreleaseNoPage并移动栈顶的指针
    id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
    }

    //当前hotPage已满时调用
    static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
    if (page->child) page = page->child;
    else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
    }

    //当前hotpage不存在时调用
    static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
    page->add(POOL_SENTINEL);
    }

    return page->add(obj);
    }

    观察上述代码,每次调用push其实就是创建一个新的AutoreleasePool,在对应的AutoreleasePoolPage中插入一个POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY 的内存地址。push方法内部调用的是autoreleaseFast方法,并传入边界对象(POOL_BOUNDARY)。hotPage可以理解为当前正在使用的AutoreleasePoolPage

    自动释放池最终都会通过page->add(obj)方法将边界对象添加到释放池中,而这一过程在autoreleaseFast方法中被分为三种情况:

  • 当前page存在且不满,调用page->add(obj)方法将对象添加至page的栈中,即next指向的位置
  • 当前page存在但是已满,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至page的栈中
  • 当前page不存在时,调用autoreleaseNoPage创建一个hotPage,再调用page->add(obj) 方法将对象添加至page的栈中

  • 2.7 objc_autoreleasePoolPop方法

    AutoreleasePool的释放调用的是objc_autoreleasePoolPop方法,此时需要传入边界对象作为参数。这个边界对象正是每次执行objc_autoreleasePoolPush方法返回的对象atautoreleasepoolobj

    同理,我们找到objc_autoreleasePoolPop最终调用的方法,即AutoreleasePoolPagepop方法,该方法的具体实现如下:

    static inline void pop(void *token)   //POOL_BOUNDARY的地址
    {
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token); //通过POOL_BOUNDARY找到对应的page
    stop = (id *)token;
    if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
    // This check is not valid with DebugPoolAllocation off
    // after an autorelease with a pool page but no pool in place.
    _objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
    token);
    }

    if (PrintPoolHiwat) printHiwat(); // 记录最高水位标记

    page->releaseUntil(stop); //向栈中的对象发送release消息,直到遇到第一个哨兵对象

    // memory: delete empty children
    // 删除空掉的节点
    if (DebugPoolAllocation && page->empty()) {
    // special case: delete everything during page-per-pool debugging
    AutoreleasePoolPage *parent = page->parent;
    page->kill();
    setHotPage(parent);
    } else if (DebugMissingPools && page->empty() && !page->parent) {
    // special case: delete everything for pop(top)
    // when debugging missing autorelease pools
    page->kill();
    setHotPage(nil);
    }
    else if (page->child) {
    // hysteresis: keep one empty child if page is more than half full
    if (page->lessThanHalfFull()) {
    page->child->kill();
    }
    else if (page->child->child) {
    page->child->child->kill();
    }
    }
    }

    上述代码中,首先根据传入的边界对象地址找到边界对象所处的page;然后选择当前page中最新加入的对象一直向前清理,可以向前跨越若干个page,直到边界所在的位置;清理的方式是向这些对象发送一次release消息,使其引用计数减一;

    另外,清空page对象还会遵循一些原则:

    1.如果当前的page中存放的对象少于一半,则子page全部删除;

    2.如果当前当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;

    2.8 autorelease方法

    上述是对自动释放池整个生命周期的分析,现在我们来理解延时释放对象autorelease方法的实现,首先查看该方法的调用栈:

    - [NSObject autorelease]
    └── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
    └── static id AutoreleasePoolPage::autorelease(id obj)
    └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
    ├── id *add(id obj)
    ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    │ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
    │ └── id *add(id obj)
    └── static id *autoreleaseNoPage(id obj)
    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
    └── id *add(id obj)

    如上所示,autorelease方法最终也会调用上面提到的 autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。关于autoreleaseFast的分析这里不再累述,我们主要来考虑一下两次调用的区别:

    autorelease函数和push函数一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,不过push函数入栈的是一个边界对象,而autorelease函数入栈的是一个具体的Autorelease的对象。

    三、AutoreleasePool与NSThread、NSRunLoop的关系

    由于AppKitUIKit框架的优化,我们很少需要显式的创建一个自动释放池块。这其中就涉及到AutoreleasePoolNSThreadNSRunLoop的关系。

    3.1 RunLoop和NSThread的关系
    RunLoop是用于控制线程生命周期并接收事件进行处理的机制,其实质是一个do-While循环。在苹果文档找到关于NSRunLoop的介绍如下:
    Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

    总结RunLoopNSThread(线程)之间的关系如下:
  • RunLoop与线程是一一对应关系,每个线程(包括主线程)都有一个对应的RunLoop对象;其对应关系保存在一个全局的Dictionary里;
  • 主线程的RunLoop默认由系统自动创建并启动;而其他线程在创建时并没有RunLoop,若该线程一直不主动获取,就一直不会有RunLoop
  • 苹果不提供直接创建RunLoop的方法;所谓其他线程Runloop的创建其实是发生在第一次获取的时候,系统判断当前线程没有RunLoop就会自动创建;
  • 当前线程结束时,其对应的Runloop也被销毁;
  • 3.2 RunLoop和AutoreleasePool的关系

    苹果文档中找到两者关系的介绍如下:

    The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

    如上所述,主线程的NSRunLoop在监测到事件响应开启每一次event loop之前,会自动创建一个autorelease pool,并且会在event loop结束的时候执行drain操作,释放其中的对象。

    3.3 Thread和AutoreleasePool的关系
    苹果文档中找到两者关系的介绍如下:

    Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

    如上所述, 包括主线程在内的所有线程都维护有它自己的自动释放池的堆栈结构。新的自动释放池被创建的时候,它们会被添加到栈的顶部,而当池子销毁的时候,会从栈移除。对于当前线程来说,Autoreleased对象会被放到栈顶的自动释放池中。当一个线程线程停止,它会自动释放掉与其关联的所有自动释放池。

    四、AutoreleasePool在主线程上的释放时机

    4.1 理解主线程上的自动释放过程
    分析主线程RunLoop管理自动释放池并释放对象的详细过程,我们在如下Demo中的主线程中设置断点,并执行lldb命令:po [NSRunLoop currentRunLoop],具体效果如下:

    我们看到主线程RunLoop中有两个与自动释放池相关的Observer,它们的 activities分别为0x10xa0这两个十六进制的数,转为二进制分别为110100000,对应CFRunLoopActivity的类型如下:
    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //0x1,启动Runloop循环
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5), //0xa0,即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7), //0xa0,退出RunLoop循环
    kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

    结合RunLoop监听的事件类型,分析主线程上自动释放池的使用过程如下:

  • App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler();
  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。order = -2147483647(即32位整数最小值)表示其优先级最高,可以保证创建释放池发生在其他所有回调之前;
  • 第二个Observer监视了两个事件BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop()来释放自动释放池。order = 2147483647(即32位整数的最大值)表示其优先级最低,保证其释放池子发生在其他所有回调之后;
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建AutoreleasePool了;
  • 最后,也可以结合图示理解主线程上自动释放对象的具体流程:


  • 程序启动到加载完成后,主线程对应的RunLoop会停下来等待用户交互
  • 用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。
  • RunLoop检测到事件后,就会创建自动释放池;
  • 所有的延迟释放对象都会被添加到这个池子中;
  • 在一次完整的运行循环结束之前,会向池中所有对象发送release消息,然后自动释放池被销毁;

  • 4.2 测试主线程上的对象自动释放过程
    下面的代码创建了一个Autorelease对象string,并且通过weakString进行弱引用(不增加引用计数,所以不会影响对象的生命周期),具体如下:
    @interface TestMemoryVC ()
    @property (nonatomic,weak)NSString *weakString;
    @end

    @implementation TestMemoryVC
    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"%@",@"WUYUBEICHEN"];
    self.weakString = string;
    }

    - (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@", self.weakString);
    }

    - (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@", self.weakString);
    }

    @end

    //打印结果:
    //viewWillAppear:WUYUBEICHEN
    //viewDidAppear:(null)
    代码分析:自动变量的string在离开viewDidLoad的作用域后,会依靠当前主线程上的RunLoop迭代自动释放。最终string对象在viewDidAppear方法执行前被释放(RunLoop完成此次迭代)。

    五、AutoreleasePool子线程上的释放时机

    子线程默认不开启RunLoop,那么其中的延时对象该如何释放呢?其实这依然要从ThreadAutoreleasePool的关系来考虑:
    Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.
    也就是说,每一个线程都会维护自己的 Autoreleasepool栈,所以子线程虽然默认没有开启RunLoop,但是依然存在AutoreleasePool,在子线程退出的时候会去释放autorelease对象。

    前面讲到过,ARC会根据一些情况进行优化,添加__autoreleasing修饰符,其实这就相当于对需要延时释放的对象调用了autorelease方法。从源码分析的角度来看,如果子线程中没有创建AutoreleasePool ,而一旦产生了Autorelease对象,就会调用autoreleaseNoPage方法自动创建hotpage,并将对象加入到其栈中。所以,一般情况下,子线程中即使我们不手动添加自动释放池,也不会产生内存泄漏。

    六、AutoreleasePool需要手动添加的情况

    尽管ARC已经做了诸多优化,但是有些情况我们必须手动创建AutoreleasePool,而其中的延时对象将在当前释放池的作用域结束时释放。苹果文档中说明了三种情况,我们可能会需要手动添加自动释放池:
    1. 编写的不是基于UI框架的程序,例如命令行工具;
    2. 通过循环方式创建大量临时对象;
    3. 使用非Cocoa程序创建的子线程;

    而在ARC环境下的实际开发中,我们最常遇到的也是第二种情况,以下面的代码为例:

    - (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }
    }
    上述代码中,obj因为离开作用域所以会被加入最近一次创建的自动释放池中,而这个释放池就是主线程上的RunLoop管理的;因为for循环在当前线程没有执行完毕,Runloop也就没有完成当前这一次的迭代,所以导致大量对象被延时释放。释放池中的对象将会在viewDidAppear方法执行前就被销毁。在此情况下,我们就有必要通过手动干预的方式及时释放不需要的对象,减少内存消耗;优化的代码如下:
    - (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
    @autoreleasepool{
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }
    }
    }


    摘自作者:梧雨北辰
    原贴链接:https://www.jianshu.com/p/7bd2f85f03dc

    收起阅读 »

    android 逆向工程-工具篇 jadx(九)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    Jadx Github

    下载地址:https://github.com/skylot/jadx


    使用 jadx

    双击 jadx-gui运行起来,直接打开  apk、dex、jar、zip、class、aar 文件。

    搜索功能

    点击 Navigation -> Text Search 或 Navigation -> Class Search 

    Class、Method、Field、Code四种类型搜索


    搜索引用的代码


    deobfuscation

    Tools -> deobfusation 方便我们识别和搜索,以免被混淆后的代码绕晕。

    一键导出 Gradle 

    主要是为了借助 AS 强大的 IDE 功能,例如方法跳转、引用搜索等等,阅读更方便。

    收起阅读 »

    android 逆向工程-技术篇 Android studio动态调试(八)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    一、下载插件smalidea

    地址: https://bitbucket.org/JesusFreke/smali/downloads    下载smalidea-0.03.zip


    二、反编译APK

    java -jar apktool.jar d -f F:\apktools\demo.apk -o F:\apktools\demo


    三、添加DUBUG属性

    在AndroidManifest.xml的application添加属性:android:debuggable="true"


    四、安装修改后的应用


    1、安装上面重新签名得到的apk应用

    2、创建目录Smali/src,并且把smali反编译出的文件放到该目录下


    五、调试启动应用

    adb shell am start -D -n app.mm.demo/.demoActivity
    adb shell  ps | grep demo 查看应用pid  24551
    然后进行端口转发:
    adb forward tcp:8700 jdwp:24551
    注意:如果不允许建立则输入netstat -ano查看进程
    kill了8700进程就好了


    六、用Android studio打开smali文件

    配置如下


    end

    Run->Debug,开始动态调试

    收起阅读 »

    android 逆向工程-工具篇 IDA pro入门(七)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    注:自行网上下载IDA pro

    我安装好的IDA 包含64和32两个版本,经测试 32位支持伪代码,可以F5对照C语言等进行更方便的分析。

    用IDA pro打开一个so文件


    展示如下(F5查看伪代码)


    • IDA View-A是反汇编窗口
    • HexView-A是十六进制格式显示的窗口
    • Imports是导入表(程序中调用到的外面的函数)
    • Functions是函数表(这个程序中的函数)
    • Structures是结构
    • Enums是枚举

    IDA View-A

    这里会有流程图(按回车进行切换),判断是执行绿色,判断否执行红色,蓝色为一个执行块。

    分析

    先展示些ARM汇编的基础

    寄存器

    • R0-R3:用于函数参数及返回值的传递
    • R4-R6, R8,R10-R11:没有特殊规定,就是普通的通用寄存器
    • R7:栈帧指针(Frame Pointer).指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址。
    • R9:操作系统保留
    • R12:又叫IP(intra-procedure scratch)
    • R13:又叫SP(stack pointer),是栈顶指针
    • R14:又叫LR(link register),存放函数的返回地址。
    • R15:又叫PC(program counter),指向当前指令地址。
    • CPSR:当前程序状态寄存器(Current Program State Register),在用户状态下存放像condition标志中断禁用等标志的。
    • VFP:(向量浮点运算)相关的寄存器

    基本的指令

    • add 加指令
    • sub 减指令
    • str 把寄存器内容存到栈上去
    • ldr 把栈上内容载入一寄存器中
    • .w是一个可选的指令宽度说明符。它不会影响为此指令的行为,它只是确保生成 32 位指令。
    • bl 执行函数调用,并把使lr指向调用者(caller)的下一条指令,即函数的返回地址
    • blx 同上,但是在ARM和thumb指令集间切换。
    • bx bx lr返回调用函数(caller)。
    • bne 数据跳转指令,标志寄存器中Z标志位不等于零时, 跳转到BNE后标签处。
    • CMP 比较命令
    • B 无条件跳转
    收起阅读 »

    android 逆向工程-开发篇 apk加固(六)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:

    注:参考 https://blog.csdn.net/jiangwei0910410003/article/details/48415225

    加密工具:https://github.com/dileber/DexShellTools/tree/master

    壳程序Apk:https://github.com/dileber/ReforceApk/tree/master

    加固原理

    一句话:通过修改壳apk中的dex文件,把需要加壳的apk通过二进制形式,来加密到壳apk中,运行时进行解密操作。

    加壳重点(其余的参考注释的文章):

    加壳时需要了解dex文件头部

    加壳后的dex文件需要替换壳的dex文件

    加壳后的apk需要对其重新签名:

    jarsigner -verbose -keystore 签名文件 -storepass 密码  -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA  签名后的文件 签名前的apk alias名称

    eg:
    jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei

    签名文件的密码:123456
    alais的密码:123456


    收起阅读 »

    android 逆向工程-工具篇 dex2jar jd-gui(五)

    Android逆向工程篇: android 逆向工程-工具篇 drozer(一) android 逆向工程-工具篇 apktool(二) android 逆向工程-语言篇 Smali(三) android 逆向工程-分析篇 漏洞与风险(四) android ...
    继续阅读 »

    Android逆向工程篇:



    获取classes.dex文件:


    使用压缩软件打开apk,把目录下的classes.dex文件解压出来


    下载dex2jar,并解压到目录下。


    在cmd中运行




    d2j-dex2jar.bat classes.dex


    于是在dex2jar目录下产生了一个classes.jar


    下载 jd-gui 官网地址


    直接下载win版打开 jar文件


    收起阅读 »

    iOS性能优化 — 四、内存泄露检测

    上篇文章为大家讲解了安装包瘦身,这篇文章继续为大家讲解下内存泄露检测。造成内存泄漏原因常见循环引用及解决方案怎么检测循环引用造成内存泄漏原因在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;调用CoreFound...
    继续阅读 »

    上篇文章为大家讲解了安装包瘦身,这篇文章继续为大家讲解下内存泄露检测。

    • 造成内存泄漏原因

    • 常见循环引用及解决方案

    • 怎么检测循环引用

    造成内存泄漏原因

    • 在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;

    • 调用CoreFoundation里面的C方法后创建对对象后不释放。比如调用CGImageCreate不调用CGImageRelease;

    • 循环引用。当对象A和对象B互相持有的时候,就会产生循环引用。常见产生循环引用的场景有在VC的cellForRowAtIndexPath方法中cell block引用self。

    常见循环引用及解决方案

    1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。

    cell.clickBlock = ^{
    self.name = @"akon";
    };

    cell.clickBlock = ^{
    _name = @"akon";
    };

    解决方案:把self改成weakSelf;

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.name = @"akon";
    };

    2)在cell的block中直接引用VC的成员变量造成循环引用。

    //假设 _age为VC的成员变量
    @interface TestVC(){

    int _age;

    }
    cell.clickBlock = ^{
    _age = 18;
    };

    解决方案有两种:

    • 用weak-strong dance

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    strongSelf->age = 18;
    };
    • 把成员变量改成属性

    //假设 _age为VC的成员变量
    @interface TestVC()

    @property(nonatomic, assign)int age;

    @end

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.age = 18;
    };

    3)delegate属性声明为strong,造成循环引用。

    @interface TestView : UIView

    @property(nonatomic, strong)id<TestViewDelegate> delegate;

    @end

    @interface TestVC()<TestViewDelegate>

    @property (nonatomic, strong)TestView* testView;

    @end

    testView.delegate = self; //造成循环引用

    解决方案:delegate声明为weak

    @interface TestView : UIView

    @property(nonatomic, weak)id<TestViewDelegate> delegate;

    @end

    4)在block里面调用super,造成循环引用。

    cell.clickBlock = ^{
    [super goback]; //造成循环应用
    };

    解决方案,封装goback调用

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    [weakSelf _callSuperBack];
    };

    - (void) _callSuperBack{
    [self goback];
    }

    5)block声明为strong
    解决方案:声明为copy
    6)NSTimer使用后不invalidate造成循环引用。
    解决方案:

    • NSTimer用完后invalidate;

    • NSTimer分类封装

    *   (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
    block:(void(^)(void))block
    repeats:(BOOL)repeats{

    return [self scheduledTimerWithTimeInterval:interval
    target:self
    selector:@selector(ak_blockInvoke:)
    userInfo:[block copy]
    repeats:repeats];
    }

    * (void)ak_blockInvoke:(NSTimer*)timer{

    void (^block)(void) = timer.userInfo;
    if (block) {
    block();
    }
    }

    怎么检测循环引用

    • 静态代码分析。 通过Xcode->Product->Anaylze分析结果来处理;

    • 动态分析。用MLeaksFinder或者Instrument进行检测。

    转自:https://www.jianshu.com/p/f06f14800cf7

    收起阅读 »

    Xcode12适配The linked library is missing one or more architectures required by this target问题

    问题升级到Xcode12后,运行Release模式后,会提示以下信息: The linked library 'xxxx.a/Framework' is missing one or more architectures required by this ta...
    继续阅读 »

    问题
    升级到Xcode12后,运行Release模式后,会提示以下信息:

    The linked library 'xxxx.a/Framework' is missing one or more architectures required by this target: armv7.

    又或者


    xxx/Pods/Target Support Files/Pods-xxx/Pods-xxx-frameworks.sh: line 128: ARCHS[@]: unbound variable
    Command PhaseScriptExecution failed with a nonzero exit code

    以上涉及架构问题

    解决方案

    在Target-Build Settings-Excluded Architectures中添加以下代码

    EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8 EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT))


    转自:https://www.jianshu.com/p/81741aed39f7


    收起阅读 »

    iOS 使用NSSetUncaughtExceptionHandler收集Crash

    在iOS程序崩溃时,一般我们是用Bugtags、Bugly、友盟等第三方收集崩溃,其实官方提供的NSUncaughtExceptionHandler来收集crash信息。实现方式如下:自定义一个UncaughtExceptionHandler类,在.h中: @...
    继续阅读 »

    在iOS程序崩溃时,一般我们是用Bugtags、Bugly、友盟等第三方收集崩溃,其实官方提供的NSUncaughtExceptionHandler来收集crash信息。实现方式如下:
    自定义一个UncaughtExceptionHandler类,在.h中:

    @interface CustomUncaughtExceptionHandler : NSObject
    + (void)setDefaultHandler;
    + (NSUncaughtExceptionHandler *)getHandler;
    @end

    复制代码
    在.m中实现:

    #import "CustomUncaughtExceptionHandler.h"

    // 沙盒的地址
    NSString * applicationDocumentsDirectory() {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    }


    // 崩溃时的回调函数
    void UncaughtExceptionHandler(NSException * exception) {
    NSArray * arr = [exception callStackSymbols];
    NSString * reason = [exception reason]; // // 崩溃的原因 可以有崩溃的原因(数组越界,字典nil,调用未知方法...) 崩溃的控制器以及方法
    NSString * name = [exception name];
    NSString * url = [NSString stringWithFormat:@"crash报告\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[arr componentsJoinedByString:@"\n"]];
    NSString * path = [applicationDocumentsDirectory() stringByAppendingPathComponent:@"crash.txt"];
    // 将一个txt文件写入沙盒
    [url writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    }

    @implementation CustomUncaughtExceptionHandler

    + (void)setDefaultHandler {
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
    }

    + (NSUncaughtExceptionHandler *)getHandler {
    return NSGetUncaughtExceptionHandler();
    }

    @end

    复制代码
    这样我们就实现好了一个自定义UncaughtExceptionHandler类,接下来只需要在合适的地方获取crash文件以及传到服务器上去即可,如下所示:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    //崩溃日志
    [CustomUncaughtExceptionHandler setDefaultHandler];
    //获取崩溃日志,然后发送
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *dataPath = [path stringByAppendingPathComponent:@"crash.txt"];
    NSData *data = [NSData dataWithContentsOfFile:dataPath];
    if (data != nil) {
    //发送崩溃日志
    NSLog(@"crash了:%@",data);
    }
    }

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


    收起阅读 »

    Bootstrap Table

    前端1.BootStrap Table1.1.1 HTML<div> <div class="panel-body table-responsive"> <table id="productTable" class="tab...
    继续阅读 »

    前端

    1.BootStrap Table

    1.1.1 HTML

    <div>
    <div class="panel-body table-responsive">
    <table id="productTable" class="table">
    </table>
    </div>
    </div>

    1.1.2 js初始化(开发常用方法)

    $('#productTable').bootstrapTable('refreshOptions',{pageNumber:1,pageSize:10});
    var tableObject= $.find("#productTable");
    $(tableObject).bootstrapTable({
    locale: 'zn-CN',
    pageSize: 10,
    pageNumber: 1,
    pageList: [10, 25, 50,100],
    clickToSelect: true,
    striped: true,
    ajax: function(ajaxParams) {
    json.NEXT_KEY = (ajaxParams.data.offset /ajaxParams.data.limit + 1) + "";
    json.PAGE_SIZE = ajaxParams.data.limit + "";
    //json.SORT_NAME = ajaxParams.data.sort;
    //json.SORT_ORDER = ajaxParams.data.order;
    YT.ajaxData({
    url:dataUrl,
    params: json,
    success: function (msg) {
    var resultData = {total: msg.TOTAL_NUM||0,rows: msg.LIST|| []};
    ajaxParams.success(resultData);
    }
    });
    },
    pagination: true,
    sidePagination: 'server',
    //sortName: '表格头排序字段',
    //sortOrder: 'desc',
    formatNoMatches: function() {
    return "暂无数据";
    },
    columns: [
    {
    checkbox: true,
    singleSelect : true,
    align: 'center'
    },
    {
    field: '',
    title: '操作',
    formatter: removeHtml,
    align: 'center'
    }]
    });
    // 自定义table列
    function removeHtml(value,row,index){
    var data = $("#productTable").bootstrapTable('getData');
    var params= data[index];
    return [
    '<a class="btn btn-xs btn-primary" >自定义一些方法</a>'
    ].join('')
    }
    // 常用方法
    1.获取当前table初始化数据
    var data = $("#productTable").bootstrapTable('getData');
    data-index:该属性是bootstrap table 下角标
    2.获取多选选中行的数据
    var data = $("#productTable").bootstrapTable('getSelections');
    3.清楚多选框全选
    $("#prodTable input[type='checkbox']:checked").prop("checked",false);
    4.获取每页显示的数量
    var pageSize = $('#prodTable').bootstrapTable('getOptions').pageSize;
    5.获取当前是第几页
    var pageNumber = $('#prodTable').bootstrapTable('getOptions').pageNumber;
    6.隐藏列、显示列(可用于初始化table之后的列的动添显示与隐藏,执行该时间之后数据会回滚到初始化table时的数据)
    $("#prodTable").bootstrapTable("hideColumn","GROUP_LEADER_PRICE")
    $("#prodTable").bootstrapTable("showColumn","GROUP_LEADER_PRICE")

    1.1.3 总计

    function statisticsTableInit() {
    var columns = [
    {
    field: 'column1',
    title: '表头1',
    align: 'center'
    },
    {
    field: 'column2',
    title: '表头2',
    align: 'center'
    },
    {
    field: 'column3',
    title: '表头3',
    align: 'center'
    }
    ];
    pageList.find("#prodTable").bootstrapTable({
    locale: 'zn-CN',
    columns: columns
    });
    }
    function statisticsAjax(json) {
    YT.ajaxData({
    url:YT.dataUrl,
    params: json,
    success: function (msg) {
    if(msg && msg.LIST){
    pageList.find("#prodTable").bootstrapTable('load',(msg.LIST));
    }
    }
    });
    }


    收起阅读 »

    JavaScript重构技巧 — 函数和类

    JavaScript 是一种易于学习的编程语言,编写运行并执行某些操作的程序很容易。然而,要编写一段干净的JavaScript 代码是很困难的。在本文中,我们将介绍一些与清理 JavaScript 函数和类有关的重构思想。不要直接对参数赋值在使用参数之前,我们...
    继续阅读 »

    JavaScript 是一种易于学习的编程语言,编写运行并执行某些操作的程序很容易。然而,要编写一段干净的JavaScript 代码是很困难的。

    在本文中,我们将介绍一些与清理 JavaScript 函数和类有关的重构思想。

    不要直接对参数赋值

    在使用参数之前,我们应该删除对参数的赋值,并将参数值赋给变量。

    例如,我们可能会写这样的代码:

    const discount = (subtotal) => {
    if (subtotal > 50) {
    subtotal *= 0.8;
    }
    }

    对比上面的代码,我们可以这样写:

    const discount = (subtotal) => {
    let _subtotal = subtotal;
    if (_subtotal > 50) {
    _subtotal *= 0.8;
    }
    }

    因为参数有可能是通过值或者引用传递的,如果是引用传递的,直接负值操作,有些结果会让感到困惑。

    本例是通过值传递的,但为了清晰起见,我们还是将参数赋值给变量了。

    用函数替换方法

    我们可以将一个方法变成自己的函数,以便所有类都可以访问它。

    例如,我们可能会写这样的代码:

    const hello = () => {
    console.log('hello');
    }
    class Foo {
    hello() {
    console.log('hello');
    }
    //...
    }
    class Bar {
    hello() {
    console.log('hello');
    }
    //...
    }

    我们可以将hello方法提取到函数中,如下所示:

    const hello = () => {
    console.log('hello');
    }
    class Foo {
    //...
    }
    class Bar {
    //...
    }

    由于hello方法不依赖于this,并且在两个类中都重复,因此我们应将其移至其自己的函数中以避免重复。

    替代算法

    相对流程式的写法,我们想用一个更清晰的算法来代替,例如,我们可能会写这样的代码:

    const doubleAll = (arr) => {
    const results = []
    for (const a of arr) {
    results.push(a * 2);
    }
    return results;
    }

    对比上面的代码,我们可以这样写:

    const doubleAll = (arr) => {
    return arr.map(a => a * 2);
    }

    通过数组方法替换循环,这样doubleAll函数就会更加简洁。

    如果有一种更简单的方法来解决我们的需求,那么我们就应该使用它。

    移动方法

    在两个类之间,我们可以把其中一个类的方法移动到另一个类中,例如,我们可能会写这样的代码:

    class Foo {
    method() {}
    }
    class Bar {
    }

    假如,我们在 Bar 类使用 method 的次数更多,那么应该把 method 方法移动到 Bar 类中, Foo 如果需要在直接调用 Bar 类的中方法即可。

    class Foo {
    }
    class Bar {
    method() {}
    }

    移动字段

    除了移动方法外,我们还可以移动字段。例如,我们可能会写这样的代码:

    class Foo {
    constructor(foo) {
    this.foo = foo;
    }
    }
    class Bar {
    }

    跟移动方法的原因类似,我们有时这么改代码:

    class Foo {
    }
    class Bar {
    constructor(foo) {
    this.foo = foo;
    }
    }

    我们可以将字段移至最需要的地方

    提取类

    如果我们的类很复杂并且有多个方法,那么我们可以将额外的方法移到新类中。

    例如,我们可能会写这样的代码:

    class Person {
    constructor(name, phoneNumber) {
    this.name = name;
    this.phoneNumber = phoneNumber;
    }
    addAreaCode(areaCode) {
    return `${areaCode}-${this.phoneNumber}`
    }
    }

    我们可以这样重构:

    class PhoneNumber {
    constructor(phoneNumber) {
    this.phoneNumber = phoneNumber;
    }
    addAreaCode(areaCode) {
    return `${areaCode}-${this.phoneNumber}`
    }
    }
    class Person {
    constructor(name, phoneNumber) {
    this.name = name;
    this.phoneNumber = new PhoneNumber(phoneNumber);
    }
    }

    上面我们将Person类不太相关的方法addAreaCode 移动了自己该处理的类中。

    通过这样做,两个类只做一件事,而不是让一个类做多件事。

    总结

    我们可以从复杂的类中提取代码,这些复杂的类可以将多种功能添加到自己的类中。

    此外,我们可以将方法和字段移动到最常用的地方。

    将值分配给参数值会造成混淆,因此我们应该在使用它们之前将其分配给变量。


    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    原文:https://levelup.gitconnected....

    收起阅读 »

    我是如何在 Vue 项目中做代码分割的

    通常为了开发效率,我们会使用 vue-cli 创建项目,这样创建的项目默认情况下编译是会对代码进行分割的。但是如果是自行配置的 webpack 环境的话,还是很有必要熟悉代码分割的相关知识的。为什么要做代码分割在配置 webpack 的过程...
    继续阅读 »

    通常为了开发效率,我们会使用 vue-cli 创建项目,这样创建的项目默认情况下编译是会对代码进行分割的。但是如果是自行配置的 webpack 环境的话,还是很有必要熟悉代码分割的相关知识的。

    为什么要做代码分割

    在配置 webpack 的过程中,很多时候我们的 webpack 入口只写了一个 entry: '${sourceDir}/index.js’,默认情况下只会生成一个 bundle 文件,包含了第三方库、公共代码及不同页面所用到的业务逻辑,这必然会造成该 bundle 文件体积过大,影响页面首次的加载速度,因此我们需要对代码进行分割,加快首次进入页面的速度。

    代码分割思路

    首先把第三方库、公共代码抽离出来,因为这些代码变动的频率小,可以打包成一个文件,这样每次上线文件都不发生变化,可以充分利用网络缓存加快文件下载速度,分割的细的话就是,第三方库为一个 js 文件, 公共代码为一个 js 文件。

    然后,按照路由(页面)进行代码分割,每个页面生成一个 js 文件,这样每次首次进入就只加载公共代码和本页面用的的 js 文件, 而不用加载其它页面无关的代码。

    最后,再进行精细分割的话,就是根据组件使用情况进行分割,来实现组件的懒加载,比如:页面中的不同 tab,可以根据 tab 的展示情况进行分割,把需要点击或者用户主动操作才能呈现的组件进行懒加载,这样就在页面级又进行了更细粒度的代码分割。

    代码分割实战

    第三方库及公共代码分割

    第一步我们进行第三方库的分割,比如 vue、vue-router、vuex、axios 等三方库,把它们放到 vender.js 中,然后 utils、common 文件等放在 common.js 中。这些通过 webpack 的 entry 及 splitChunk 配置即可实现。

    修改 entry 配置:

    {
    // ...
    entry: {
    // 把公共代码放到 common 里
    common: [`${sourceDir}/utils/index.js`],
    main: `${sourceDir}/index.js`,
    },
    // ...
    }

    splitChunk 配置:

    {
    optimization: {
    // splitChunks 配置
    splitChunks: {
    cacheGroups: {
    default: {
    name: 'vendor',
    // 把第三方库放到 vendor 里,包括 vue, vue-router, vuex 等
    // 因为他们都是从 node_modules 里加载的,这里直接正则匹配
    test: /[\\/]node_modules[\\/]/,
    chunks: 'initial',
    // 调整优先级,优先处理
    priority: 10,
    },
    common: {
    chunks: 'all',
    name: 'common',
    // 匹配 entry 里的 common 配置
    test: 'common',
    },
    },
    },
    // runtime 代码放在 runtime 文件中
    runtimeChunk: {
    name: 'runtime',
    },
    }
    }

    另外就是 output 配置了,[name] 表示让 chunk 名称作为文件名, [chunkhash:8] 表示加上 hash,上线后不走缓存加载最新的代码。

    {
    output: {
    path: path.join(__dirname, './dist'),
    filename: 'static/[name].[chunkhash:8].bundle.js',
    chunkFilename: 'static/[name].[chunkhash:8].bundle.js',
    },
    }

    做完第三方库及公共代码分割,打包后生成的文件如下:

    assets by path static/*.js 138 KiB
    asset static/vendor.4391b08b.bundle.js 133 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: default)
    asset static/main.0d6dab3a.bundle.js 3.9 KiB [emitted] [immutable] [minimized] (name: main)
    asset static/runtime.bdaa3432.bundle.js 1.1 KiB [emitted] [immutable] [minimized] (name: runtime)
    asset static/common.3f62940b.bundle.js 204 bytes [emitted] [immutable] [minimized] (name: common)
    asset index.html 537 bytes [emitted]
    asset static/main.acdc2841.bundle.css 127 bytes [emitted] [immutable] [minimized] (name: main)

    我们可以看到代码分割到了不同的文件中,vender.js 包含了所有的第三方库,main.js 包含了我们各个页面的业务逻辑,公共代码在 common 中,runtime 包含了运行时代码,这样代码就分散到了不同的文件中,各司其职,且有利于同时进行加载。

    但是 main.js 还是包含了多个页面的代码,如果只是进入首页的话,其它页面的代码就是多余的,接下来再进行优化。

    按路由分割

    这一个比较容易处理,只需改变下路由配置即可,以 () => import(path) 的方式加载页面组件:

    const routes = [
    {
    path: '/',
    // component: Home,
    component: () => import('./pages/Home'),
    },
    {
    path: '/todos',
    // component: Todos,
    component: () => import('./pages/Todos'),
    },
    {
    path: '/about',
    // component: About,
    component: () => import('./pages/About'),
    },
    {
    path: '/404',
    // component: NotFound,
    component: () => import('./pages/NotFound'),
    },
    {
    path: '*',
    redirect: '/404',
    },
    ];

    此时打包会看到多了很多文件,这是把不同页面的代码分割到了不同的 JS 文件中,只有访问对应的页面才会加载相关的代码。

    assets by path static/*.js 142 KiB
    asset static/vendor.4391b08b.bundle.js 133 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: default)
    asset static/runtime.07c35c52.bundle.js 3.99 KiB [emitted] [immutable] [minimized] (name: runtime)
    asset static/821.7ba5112d.bundle.js 1.89 KiB [emitted] [immutable] [minimized]
    asset static/main.1697fd27.bundle.js 1.68 KiB [emitted] [immutable] [minimized] (name: main)
    asset static/820.de28fd7b.bundle.js 562 bytes [emitted] [immutable] [minimized]
    asset static/646.a902d0eb.bundle.js 406 bytes [emitted] [immutable] [minimized]
    asset static/114.26876aa2.bundle.js 402 bytes [emitted] [immutable] [minimized]
    asset static/common.3f62940b.bundle.js 204 bytes [emitted] [immutable] [minimized] (name: common)
    assets by path static/*.css 127 bytes
    asset static/main.beb1183a.bundle.css 75 bytes [emitted] [immutable] [minimized] (name: main)
    asset static/821.cd9a22a5.bundle.css 52 bytes [emitted] [immutable] [minimized]
    asset index.html 537 bytes [emitted]

    当然,这个地方可能会有争议,争议的地方就是:「页面进入时就把所有页面的代码都下载下来,再进入其它页面不是更快吗?」。这就取决于项目情况了,看是着重于页面秒开,还是着重于页面切换体验。如果着重于秒开的话,配合 SSR 处理效果会更好。

    更细粒度的分割

    如果对于页面打开速度或性能有更高的要求,还可以做更细粒度的代码分割,比如页面中功能模块的懒加载。

    这里以一个点击按钮时加载相应的组件为例,进行代码演示:

    这里有一个 Load Lazy Demo 按钮,点击时才加载 LazyComponent 组件,LazyComponent 组件并没有什么特别之处,写法跟普通组件一样。

    <template>
    <button @click="loadLazyDemo">Load Lazy Demo</button>
    <template v-if="showLazyComponent">
    <lazy-component />
    </template>
    </template>

    这里通过一个 showLazyComponent 控制组件的显示,当点击按钮时,把 showLazyComponent 置为 true,然后就加载 LazyComponent 对应的代码了。其实关键还是通过 () => import(path) 的方式引入组件。

    <script>
    export default {
    data() {
    return {
    showLazyComponent: false,
    };
    },
    methods: {
    loadLazyDemo() {
    this.showLazyComponent = true;
    },
    },
    components: {
    'lazy-component': () => import('../components/LazyComponent'),
    },
    };
    </script>

    K,以上就是我在 Vue 项目中做的代码分割的相关内容。

    原文链接:https://segmentfault.com/a/1190000039859930

    收起阅读 »

    高质量代码的原则

    简单性原则What:追求简单自始至终都以最简单的逻辑编写代码,让编程初学者一眼就能看懂。在编程时我们要重视的是局部的完整性,而不是复杂的整体关联性。Why:Bug 喜欢出现在复杂的地方软件故障常集中在某一个区域,而这些区域都有一个共同的特点,那就是复杂。编写代...
    继续阅读 »

    简单性原则

    What:追求简单

    自始至终都以最简单的逻辑编写代码,让编程初学者一眼就能看懂。在编程时我们要重视的是局部的完整性,而不是复杂的整体关联性。

    Why:Bug 喜欢出现在复杂的地方

    软件故障常集中在某一个区域,而这些区域都有一个共同的特点,那就是复杂。编写代码时如果追求简单易懂,代码就很难出现问题。不过,简单易懂的代码往往给人一种不够专业的感觉。这也是经验老到的程序员喜欢写老练高深的代码的原因。所以我们要有足够的定力来抵挡这种诱惑。

    Do:编写自然的代码

    放下高超的技巧,坚持用简单的逻辑编写代码。既然故障集中在代码复杂的区域,那我们只要让代码简单到让故障无处可藏即可。不要盲目地让代码复杂化、臃肿化,要保证代码简洁。

    同构原则

    What:力求规范

    同等对待相同的东西,坚持不搞特殊。同等对待,举例来说就是同一个模块管理的数值全部采用同一单位、公有函数的参数个数统一等。

    Why:不同的东西会更显眼

    相同的东西用相同的形式表现能够使不同的东西更加突出。不同的东西往往容易产生 bug。遵循同构原则能让我们更容易嗅出代码的异样,从而找出问题所在。
    统一的代码颇具美感,而美的东西一般更容易让人接受,因此统一的代码有较高的可读性。

    Do:编写符合规范的代码

    可靠与简单是代码不可或缺的性质,在编写代码时,务必克制住自己的表现欲,以规范为先。

    对称原则

    What:讲究形式上的对称

    在思考一个处理时,也要想到与之成对的处理。比如有给标志位置 1 的处理,就要有给标志位置 0 的处理。

    Why:帮助读代码的人推测后面的代码

    具有对称性的代码能够帮助读代码的人推测后面的代码,提高其理解代码的速度。同时,对称性会给代码带来美感,这同样有助于他人理解代码。
    此外,设计代码时将对称性纳入考虑的范围能防止我们在思考问题时出现遗漏。如果说代码的条件分支是故障的温床,那么对称性就是思考的框架,能有效阻止条件遗漏。

    Do:编写有对称性的代码

    在出现“条件”的时候,我们要注意它的“反条件”。每个控制条件都存在与之成对的反条件(与指示条件相反的条件)。要注意条件与反条件的统一,保证控制条件具有统一性。
    我们还要考虑到例外情况并极力避免其发生。例外情况的特殊性会破坏对称性,成为故障的温床。特殊情况过多意味着需求没有得到整理。此时应重新审视需求,尽量从代码中剔除例外情况。
    命名也要讲究对称性。命名时建议使用 set/getstart/stopbegin/ end 和 push/pop 等成对的词语。

    层次原则

    What:讲究层次

    注意事物的主从关系、前后关系和本末关系等层次关系,整理事物的关联性。
    不同层次各司其职,同种处理不跨越多个层次,这一点非常重要。比如执行了获取资源的处理,那么释放资源的处理就要在相同的层次进行。又比如互斥控制的标志位置 1 和置 0 的处理要在同一层次进行。

    Why:层次结构有助于提高代码的可读性

    有明确层次结构的代码能帮助读代码的人抽象理解代码的整体结构。读代码的人可以根据自身需要阅读下一层次的代码,掌握更加详细的信息。
    这样可以提高代码的可读性,帮助程序员表达编码意图,降低 bug 发生的概率。

    Do:编写有抽象层次结构的代码

    在编写代码时设计各部分的抽象程度,构建层次结构。保证同一个层次中的所有代码抽象程度相同。另外,高层次的代码要通过外部视角描述低层次的代码。这样做能让调用低层次代码的高层次代码更加简单易懂。

    线性原则

    What:处理流程尽量走直线

    一个功能如果可以通过多个功能的线性结合来实现,那它的结构就会非常简单。
    反过来,用条件分支控制代码、毫无章法地增加状态数等行为会让代码变得难以理解。我们要避免做出这些行为,提高代码的可读性。

    Why:直线处理可提高代码的可读性

    复杂的处理流程是故障的温床。故障多出现在复杂的条件语句和循环语句中。另外,goto 等让流程出现跳跃的语句也是故障的多发地。
    如果能让处理由高层次流向低层次,一气呵成,代码的可读性就会大幅提高。与此同时,可维护性也将提高,添加功能等改良工作将变得更加容易。
    一般来说,自上而下的处理流程简单明快,易于理解。我们应避开复杂反复的处理流程。

    Do:尽量不在代码中使用条件分支

    尽量减少条件分支的数量,编写能让代码阅读者线性地看完整个处理流程的代码。
    为此,我们需要把一些特殊的处理拿到主处理之外。保证处理的统一性,注意处理的流程。记得时不时俯瞰代码整体,检查代码是否存在过于复杂的部分。
    另外,对于经过长期维护而变得过于复杂的部分,我们可以考虑对其进行重构。明确且可靠的设计不仅对我们自身有益,还可以给负责维护的人带来方便。

    清晰原则

    What:注意逻辑的清晰性

    逻辑具有清晰性就代表逻辑能清楚证明自身的正确性。也就是说,我们编写的代码要让人一眼就能判断出没有问题。任何不明确的部分都要附有说明。

    Why:消除不确定性

    代码免不了被人一遍又一遍地阅读,所以代码必须保持较高的可读性。编写代码时如果追求高可读性,我们就不会采用取巧的方式编写代码,编写出的代码会非常自然。代码是给人看的,也是由人来修改的,所以我们必须以人为对象来编写代码。消除代码的不确定性是对自己的作品负责,这么做也可以为后续负责维护的人提供方便。

    Do:编写逻辑清晰的代码

    我们应选用直观易懂的逻辑。会给读代码的人带来疑问的部分要么消除,要么加以注释。另外,我们应使用任何人都能立刻理解且不存在歧义的术语。要特别注意变量名等一定不能没有意义。

    安全原则

    What:注意安全性

    就是在编写代码时刻意将不可能的条件考虑进去。比如即便某个 if 语句一定成立,我们也要考虑 else 语句的情况;即便某个 case 语句一定成立,我们也要考虑 default 语句的情况;即便某个变量不可能为空,我们也要检查该变量是否为 null

    Why:防止故障发展成重大事故

    硬件提供的服务必须保证安全,软件也一样。硬件方面,比如取暖器,为防止倾倒起火,取暖器一般会配有倾倒自动断电装置。同样,设计软件时也需要考虑各种情况,保证软件在各种情况下都能安全地运行。这一做法在持续运营服务和防止数据损坏等方面有着积极的意义。

    Do:编写安全的代码

    选择相对安全的方法对具有不确定性的部分进行设计。列出所有可能的运行情况,确保软件在每种情况下都能安全运行。理解需求和功能,将各种情况正确分解到代码中,这样能有效提高软件安全运行的概率。
    为此,我们也要将不可能的条件视为考察对象,对其进行设计和编程。不过,为了统一标准,我们在编写代码前最好规定哪些条件需要写,哪些条件不需要写。


    原文链接:https://segmentfault.com/a/1190000039864589

    收起阅读 »

    TS实用工具类型

    Partial<Type>构造类型Type,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。例子interface Todo { title: string; description: string; } fu...
    继续阅读 »

    Partial<Type>

    构造类型Type,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。

    例子

    interface Todo {
    title: string;
    description: string;
    }

    function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    return { ...todo, ...fieldsToUpdate };
    }

    const todo1 = {
    title: 'organize desk',
    description: 'clear clutter',
    };

    const todo2 = updateTodo(todo1, {
    description: 'throw out trash',
    });

    Readonly<Type>

    构造类型Type,并将它所有的属性设置为readonly,也就是说构造出的类型的属性不能被再次赋值。

    例子

    interface Todo {
    title: string;
    }

    const todo: Readonly<Todo> = {
    title: 'Delete inactive users',
    };

    todo.title = 'Hello'; // Error: cannot reassign a readonly property

    这个工具可用来表示在运行时会失败的赋值表达式(比如,当尝试给冻结对象的属性再次赋值时)。

    Object.freeze

    function freeze<T>(obj: T): Readonly<T>;

    Record<Keys, Type>

    构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具可用来将某个类型的属性映射到另一个类型上。

    例子

    interface PageInfo {
    title: string;
    }

    type Page = 'home' | 'about' | 'contact';

    const x: Record<Page, PageInfo> = {
    about: { title: 'about' },
    contact: { title: 'contact' },
    home: { title: 'home' },
    };

    Pick<Type, Keys>

    从类型Type中挑选部分属性Keys来构造类型。

    例子

    interface Todo {
    title: string;
    description: string;
    completed: boolean;
    }

    type TodoPreview = Pick<Todo, 'title' | 'completed'>;

    const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
    };

    Omit<Type, Keys>

    从类型Type中获取所有属性,然后从中剔除Keys属性后构造一个类型。

    例子

    interface Todo {
    title: string;
    description: string;
    completed: boolean;
    }

    type TodoPreview = Omit<Todo, 'description'>;

    const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
    };

    Exclude<Type, ExcludedUnion>

    从类型Type中剔除所有可以赋值给ExcludedUnion的属性,然后构造一个类型。

    例子

    type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
    type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // "c"
    type T2 = Exclude<string | number | (() => void), Function>; // string | number

    Extract<Type, Union>

    从类型Type中提取所有可以赋值给Union的类型,然后构造一个类型。

    例子

    type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // "a"
    type T1 = Extract<string | number | (() => void), Function>; // () => void

    NonNullable<Type>

    从类型Type中剔除nullundefined,然后构造一个类型。

    例子

    type T0 = NonNullable<string | number | undefined>; // string | number
    type T1 = NonNullable<string[] | null | undefined>; // string[]

    Parameters<Type>

    由函数类型Type的参数类型来构建出一个元组类型。

    例子

    declare function f1(arg: { a: number; b: string }): void;

    type T0 = Parameters<() => string>;
    // []
    type T1 = Parameters<(s: string) => void>;
    // [s: string]
    type T2 = Parameters<<T>(arg: T) => T>;
    // [arg: unknown]
    type T3 = Parameters<typeof f1>;
    // [arg: { a: number; b: string; }]
    type T4 = Parameters<any>;
    // unknown[]
    type T5 = Parameters<never>;
    // never
    type T6 = Parameters<string>;
    // never
    // Type 'string' does not satisfy the constraint '(...args: any) => any'.
    type T7 = Parameters<Function>;
    // never
    // Type 'Function' does not satisfy the constraint '(...args: any) => any'.

    ConstructorParameters<Type>

    由构造函数类型来构建出一个元组类型或数组类型。
    由构造函数类型Type的参数类型来构建出一个元组类型。(若Type不是构造函数类型,则返回never)。

    例子

    type T0 = ConstructorParameters<ErrorConstructor>;
    // [message?: string | undefined]
    type T1 = ConstructorParameters<FunctionConstructor>;
    // string[]
    type T2 = ConstructorParameters<RegExpConstructor>;
    // [pattern: string | RegExp, flags?: string | undefined]
    type T3 = ConstructorParameters<any>;
    // unknown[]

    type T4 = ConstructorParameters<Function>;
    // never
    // Type 'Function' does not satisfy the constraint 'new (...args: any) => any'.

    ReturnType<Type>

    由函数类型Type的返回值类型构建一个新类型。

    例子

    type T0 = ReturnType<() => string>;  // string
    type T1 = ReturnType<(s: string) => void>; // void
    type T2 = ReturnType<(<T>() => T)>; // {}
    type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
    type T4 = ReturnType<typeof f1>; // { a: number, b: string }
    type T5 = ReturnType<any>; // any
    type T6 = ReturnType<never>; // any
    type T7 = ReturnType<string>; // Error
    type T8 = ReturnType<Function>; // Error

    InstanceType<Type>

    由构造函数类型Type的实例类型来构建一个新类型。

    例子

    class C {
    x = 0;
    y = 0;
    }

    type T0 = InstanceType<typeof C>; // C
    type T1 = InstanceType<any>; // any
    type T2 = InstanceType<never>; // any
    type T3 = InstanceType<string>; // Error
    type T4 = InstanceType<Function>; // Error

    Required<Type>

    构建一个类型,使类型Type的所有属性为required
    与此相反的是Partial

    例子

    interface Props {
    a?: number;
    b?: string;
    }

    const obj: Props = { a: 5 }; // OK

    const obj2: Required<Props> = { a: 5 }; // Error: property 'b' missing

    ThisParameterType<Type>

    从函数类型中提取 this 参数的类型。
    若函数类型不包含 this 参数,则返回 unknown 类型。

    例子

    function toHex(this: Number) {
    return this.toString(16);
    }

    function numberToString(n: ThisParameterType<typeof toHex>) {
    return toHex.apply(n);
    }

    OmitThisParameter<Type>

    Type类型中剔除 this 参数。
    若未声明 this 参数,则结果类型为 Type 。
    否则,由Type类型来构建一个不带this参数的类型。
    泛型会被忽略,并且只有最后的重载签名会被采用。

    例子

    function toHex(this: Number) {
    return this.toString(16);
    }

    const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

    console.log(fiveToHex());

    ThisType<Type>

    这个工具不会返回一个转换后的类型。
    它作为上下文的this类型的一个标记。
    注意,若想使用此类型,必须启用--noImplicitThis

    例子

    // Compile with --noImplicitThis

    type ObjectDescriptor<D, M> = {
    data?: D;
    methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
    };

    function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
    let data: object = desc.data || {};
    let methods: object = desc.methods || {};
    return { ...data, ...methods } as D & M;
    }

    let obj = makeObject({
    data: { x: 0, y: 0 },
    methods: {
    moveBy(dx: number, dy: number) {
    this.x += dx; // Strongly typed this
    this.y += dy; // Strongly typed this
    },
    },
    });

    obj.x = 10;
    obj.y = 20;
    obj.moveBy(5, 5);

    上面例子中,makeObject参数里的methods对象具有一个上下文类型ThisType<D & M>,因此methods对象的方法里this的类型为{ x: number, y: number } & { moveBy(dx: number, dy: number): number }

    lib.d.ts里,ThisType<T>标识接口是个简单的空接口声明。除了在被识别为对象字面量的上下文类型之外,这个接口与一般的空接口没有什么不同。

    原文链接:https://segmentfault.com/a/1190000039868550

    收起阅读 »

    怎么获取到环信老版本的SDK和Demo

    来到环信官网的下载页面:下载-即时通讯云-环信 找到想要下载的sdk,以iOS端为例,右键“SDK+Demo源码”,拷贝链接,然后修改链接里的版本号即可例如:https://download-sdk.oss-cn-beijing.aliyuncs.com/do...
    继续阅读 »

    来到环信官网的下载页面:下载-即时通讯云-环信

    找到想要下载的sdk,以iOS端为例,右键“SDK+Demo源码”,拷贝链接,然后修改链接里的版本号即可
    例如:https://download-sdk.oss-cn-beijing.aliyuncs.com/downloads/iOS_IM_SDK_V3.7.4.zip

    收起阅读 »

    (IM)iOS端离线推送收不到怎么办?

    离线推送收不到,按照下面步骤一步一步进行排查: 0、如果你的app之前可以收到离线推送,突然收不到了,那么先移步苹果开发者中心查看推送证书是否过期。如果过期了,需要重新制作证书,然后到环信管理后台(Console)将旧的删掉再上传新的。过期的一般会被封禁,需要...
    继续阅读 »

    离线推送收不到,按照下面步骤一步一步进行排查:


    0、如果你的app之前可以收到离线推送,突然收不到了,那么先移步苹果开发者中心查看推送证书是否过期。如果过期了,需要重新制作证书,然后到环信管理后台(Console)将旧的删掉再上传新的。过期的一般会被封禁,需要联系环信进行解封操作。


    1、首先已经按照环信的文档集成了离线推送:APNs离线推送


    2、如果是iOS13及以上的系统,那么需要将IM SDK更新到3.6.4或以上版本。
    如果更新后还不行那么退出登录、重启app、再登录试下。
    初始化sdk成功之后打印版本号:
    NSString *ver = [EMClient sharedClient].version;

    3、测试APNs推送的时候,接收方的APP需要是杀死状态,需要用户长连接断开才会发APNs推送;
    **所以直接上划杀死APP测试。**


    4、要确保导出p12时使用的Mac和创建CertificateSigningRequest.certSigningRequest文件的Mac是同一台;导出证书的时候要直接点击导出,不要点击秘钥的内容导出;确认 APP ID 是否带有推送功能;


    5、环信管理后台(Console)上传证书时填写的Bundle ID须与工程中的Bundle ID、推送证书的 APP ID 相同;选择的证书类型须与推送证书的环境一致;导出.p12文件需要设置密码,并在上传管理后台时传入;


    6、工程中初始化SDK那里填的证书名与环信管理后台上传的证书名称必须是相同的;


    7、测试环境测试,需要使用development环境的推送证书,Xcode直接真机运行;
    正式环境测试,需要使用production环境的推送证书,而且要打包,打包时选择Ad Hoc,导出IPA安装到手机上。

    8、APP杀死后可调用“获取单个用户”的rest接口,确认证书名称是否有绑定(正常情况下,登录成功后会绑定上推送证书,绑定后会显示推送证书名称);还需要确认绑定的证书名称和管理后台上传的证书名称是否一致。


    接口文档:获取单个用户
    获取用户信息
    如果没绑定上,那么退出登录、重启app、重新登录再试下。

    如果证书名称不一致,改正过来后重新登录试下。


    9、如果以上都确认无误,可以联系环信排查。需提供以下信息(请勿遗漏,以免反复询问耽误时间):
    appkey、devicetoken、bundle id、证书的.p12文件、证书名称、证书密码、收不到推送的环信id、测试的环境(development or production)、消息id、消息的内容和发送时间

    消息id要在消息发送成功后获取,如图:
    获取消息id
    收起阅读 »

    线上直播 | iOS Runtime 项目实际应用与面试对刚!

    直播主题:iOS  Runtime 项目实际应用与面试对刚!直播时间:4月28日 19:00 嘉宾介绍:Zuyu    环信生态开发者kol直播亮点:1. 如何使用runtime ...
    继续阅读 »

    直播主题:

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

    直播时间:

    4月28日 19:00 

    嘉宾介绍:

    Zuyu    环信生态开发者kol

    直播亮点:

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

    2. 如何使用runtime 进行hook

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


    欢迎大家进iOS开发交流群~~群里各个都是人才,说话还好听。

    直播过程中还会抽礼物~


    海报428.jpg



    收起阅读 »

    java设计模式:状态模式

    定义对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。优点结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。将状态转换显示化,减少对象间的相互...
    继续阅读 »

    定义

    对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

    优点

    1. 结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
    2. 将状态转换显示化,减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
    3. 状态类职责明确,有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。

    缺点

    1. 状态模式的使用必然会增加系统的类与对象的个数。
    2. 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
    3. 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。

    代码实现

    状态模式包含以下主要角色。

    1. 环境类:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
    2. 抽象状态:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
    3. 具体状态:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。
    状态模式中,行为是由状态来决定的,不同状态下有不同行为。

    举个例子把,比如电视,电视有2个状态,一个是开机,一个是关机,开机时可以切换频道,关机时切换频道不做任何响应。
    public interface TvState{
    public void nextChannerl();
    public void prevChannerl();
    public void turnUp();
    public void turnDown();
    }

    public class PowerOffState implements TvState{
    public void nextChannel(){}
    public void prevChannel(){}
    public void turnUp(){}
    public void turnDown(){}

    }


    public class PowerOnState implements TvState{
    public void nextChannel(){
    System.out.println("下一频道");
    }
    public void prevChannel(){
    System.out.println("上一频道");
    }
    public void turnUp(){
    System.out.println("调高音量");
    }
    public void turnDown(){
    System.out.println("调低音量");
    }

    }

    public interface PowerController{
    public void powerOn();
    public void powerOff();
    }

    public class TvController implements PowerController{
    TvState mTvState;
    public void setTvState(TvStete tvState){
    mTvState=tvState;
    }
    public void powerOn(){
    setTvState(new PowerOnState());
    System.out.println("开机啦");
    }
    public void powerOff(){
    setTvState(new PowerOffState());
    System.out.println("关机啦");
    }
    public void nextChannel(){
    mTvState.nextChannel();
    }
    public void prevChannel(){
    mTvState.prevChannel();
    }
    public void turnUp(){
    mTvState.turnUp();
    }
    public void turnDown(){
    mTvState.turnDown();
    }

    }


    public class Client{
    public static void main(String[] args){
    TvController tvController=new TvController();
    tvController.powerOn();
    tvController.nextChannel();
    tvController.turnUp();

    tvController.powerOff();
    //调高音量,此时不会生效
    tvController.turnUp();
    }


    }

    应用场景

    通常在以下情况下可以考虑使用状态模式。
    • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
    • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

    例子:

    应用在已登录状态,点击评论,会正常弹出评论框,而未登录状态下,则是要跳转到登录界面登录后,再正常评论。

    所以已登录和未登录状态下的评论行为是不同的,这个就可以用状态模式设计。

    收起阅读 »