依赖注入框架Autofac(IOC)的简单使用 ioc依赖注入方式
1,灵活的组件实例化:Autofac支持自动装配,给定的组件类型Autofac自动选择使用构造函数注入或者属性注入,Autofac还可以基于lambda表达式创建实例,这使得容器非常灵活,很容易和其他的组件集成。
2,资源管理的可视性:基于依赖注入容器构建的应用程序的动态性,意味着什么时候应该处理那些资源有点困难。Autofac通过容器来跟踪组件的资源管理。对于不需要清理的对象,例如Console.Out,我们调用ExternallyOwned()方法告诉容器不用清理。细粒度的组件生命周期管理:应用程序中通常可以存在一个应用程序范围的容器实例,在应用程序中还存在大量的一个请求的范围的对象,例如一个HTTP请求,一个IIS工作者线程或者用户的会话结束时结束。通过嵌套的容器实例和对象的作用域使得资源的可视化。
3,Autofac的设计上非常务实,这方面更多是为我们这些容器的使用者考虑:
●组件侵入性为零:组件不需要去引用Autofac。
●灵活的模块化系统:通过模块化组织你的程序,应用程序不用纠缠于复杂的XML配置系统或者是配置参数。
●自动装配:可以是用lambda表达式注册你的组件,autofac会根据需要选择构造函数或者属性注入
●XML配置文件的支持:XML配置文件过度使用时很丑陋,但是在发布的时候通常非常有用
Autofac的简单使用,并加入了Repository模式.
定义两个简单实体类:public class Persion{ public string Name { get; set; } public int Age { get; set; }}public class Custom{ public string CustomName { get; set; } public int CustomID { get; set; }}
定义泛型数据库访问接口:public interface Idal<T> where T:class{ void Insert(T entity); void Update(T entity); void Delete(T entity);}
泛型数据库访问接口的泛型实现:public class Dal<T>:Idal<T> where T : class{ #region Idal<T> Members public void Insert(T entity) { HttpContext.Current.Response.Write("您添加了一个:" +entity.GetType().FullName); } public void Update(T entity) { HttpContext.Current.Response.Write("您更新一个:" +entity.GetType().FullName); } public void Delete(T entity) { HttpContext.Current.Response.Write("您删除了一个:" +entity.GetType().FullName); } #endregion}
使用Repository模式实现访问。
Repository的泛型接口:public interface IRepository<T> where T:class{ void Insert(T entity); void Update(T entity); void Delete(T entity);}
Repository泛型接口的泛型实现:public class Repository<T>:IRepository<T> where T:class{ private Idal<T> _dal; public Repository(Idal<T> dal) { _dal = dal; } #region IRepository<T> Members public void Insert(T entity) { _dal.Insert(entity); } public void Update(T entity) { _dal.Update(entity); } public void Delete(T entity) { _dal.Delete(entity); } #endregion}
IDependency的依赖接口,不需要任何方法体,所有的业务对象都实现该接口public interface IDependency{}
实现IDependency接口的CustomBll类,通过Repository模式存储数据。public class CustomBll:IDependency{ private readonly IRepository<Custom> _repository; public CustomBll(IRepository<Custom> repository) { _repository = repository; } public void Insert(Custom c) { _repository.Insert(c); } public void Update(Custom c) { _repository.Update(c); } public void Delete(Custom c) { _repository.Delete(c); }}
实现IDependency接口的PersionBll类,通过Repository模式存储数据。public class PersionBll:IDependency{ private readonly IRepository<Persion> _repository; public PersionBll(IRepository<Persion> repository) { _repository = repository; } public void Insert(Persion p) { _repository.Insert(p); } public void Update(Persion p) { _repository.Update(p); } public void Delete(Persion p) { _repository.Delete(p); }}
下面编写组件实例化测试
var builder = new ContainerBuilder();builder.RegisterGeneric(typeof(Dal<>)).As(typeof(Idal<>)) .InstancePerDependency();builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)) .InstancePerDependency();builder.Register(c=>new PersionBll((IRepository<Persion>) c.Resolve(typeof(IRepository<Persion>))));builder.Register(c => new CustomBll((IRepository<Custom>) c.Resolve(typeof(IRepository<Custom>))));//var container = builder.Build()教程里都是使用这行代码,//我本地测试需要加入ContainerBuildOptions枚举选项。using (var container = builder.Build(ContainerBuildOptions.None)){ // var repository= container.Resolve(typeof(IRepository<Persion>),new TypedParameter()); // IRepository<Persion> _repository = repository as Repository<Persion>; // var m = new PersionBll(_repository); Persion p = new Persion(); p.Name = "小人"; p.Age = 27; var m = container.Resolve<PersionBll>(); m.Insert(p); Custom c = new Custom(); c.CustomName = "小小"; c.CustomID = 10; var cc = container.Resolve<CustomBll>(); cc.Update(c);}
这里通过ContainerBuilder方法RegisterGeneric对泛型类进行注册(当然也可以通过ContainerBuilder方法RegisterType对不是泛型的类进行注册),当注册的类型在相应得到的容器中可以Resolve你的类实例。
builder.RegisterGeneric(typeof(Dal<>)).As(typeof(Idal<>)).InstancePerDependency();通过AS可以让类中通过构造函数依赖注入类型相应的接口。(当然也可以使用builder.RegisterType<类>().As<接口>();来注册不是泛型的类 )
Build()方法生成一个对应的Container实例,这样,就可以通过Resolve解析到注册的类型实例。
注:如果要获得某个泛型的实例,需要将泛型T代表的类传进去。如上c.Resolve(typeof(IRepository<Persion>))返回的是Object,需要转换为响应的接口。
当然可以使用autofac的新特性RegisterAssemblyTypes,从一个程序集的注册类型设置根据用户指定的规则,例子如下:
var builder = new ContainerBuilder();builder.RegisterGeneric(typeof(Dal<>)).As(typeof(Idal<>)).InstancePerDependency();builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)).InstancePerDependency();//上面的那些类如果在单独的工程里,如生成的程序集为AutofacUnitTest,就可以使用//Assembly.Load("AutofacUnitTest")获得响应的程序集。如果所有的文件在一个控制台程序里,//可以通过Assembly.GetExecutingAssembly(); 直接获得相应的程序集。Assembly dataAccess = Assembly.Load("AutofacUnitTest");builder.RegisterAssemblyTypes(dataAccess) .Where(t => typeof(IDependency).IsAssignableFrom(t) && t.Name.EndsWith("Bll"));//RegisterAssemblyTypes方法将实现IDependency接口并已Bll结尾的类都注册了,语法非常的简单。
Ioc(Inverse of control)已经是叫嚷了很久的技术了,一直没有机会细看,最近因为看源代码的关系,研究了一点,拿出来分享一下。
当前网络上有很多Ioc的框架,比如说微软的企业库就使用Ioc技术重写了,还有Prism模式也用到了Ioc。我看的函数库是Autofac,但是理念跟其他的函数库大同小异,实际上,为了方便程序员在不同的Ioc框架上移植程序,各个框架的编写者开会定义了一个大家都支持的接口集:Common Service Locator。
什么是Ioc
Ioc简言之,就是将类似下面创建对象的代码—我们称之为情况1:
varchecker=newMemoChecker(memos,newPrintingNotifier(Console.Out));
转换成下面这样—称之为情况2:
varchecker=container.Resolve<MemoCheck>();
而container.Resolve<MemoCheck>这一行代码在创建MemoCheck这个类型的实例时,又可以通过下面的代码创建MemoCheck构造函数所需要的两个参数:
newMemoChecker(container.Resolve<IQueryable<Memo>>(),
container.Resolve<IMemoDueNotifier>())
情况2相对情况1的好处在于,在情况1 的代码里,程序员需要显式指定构建MemoChecker实例所要求的参数类型的实例。也就是说,MemoChecker在构造一个实例时,你需要显式传入第二个参数的具体实例(PrintingNotifier)。这样就导致一个问题,如果在后期程序发布以后,需要更换MemoCheck的第二个参数,那就只有修改程序代码一条路可走了。
针对于情况1的这个问题,那肯定有人会说,那就把MemoChecker构造函数的第二个参数定义成一个接口,然后在创建MemoChecker实例的时候,读一个配置文件,找到实现这个接口的具体类型,通过反射等机制创建对象传给MemoChecker的构造函数。这样就可以通过修改配置文件的方式,通过添加实现接口的插件,动态地修改程序的行为—这正是情况2所要做的,也就是Ioc和依赖注入(Dependence Injection)要解决的一个通用问题。
关于Ioc和依赖注入,网上已经有很多文章讲解这个概念了,有兴趣的朋友可以看看这篇文章,里面介绍的很详细:
http://martinfowler.com/articles/injection.html
使用Autofac实现依赖注入
我先以CodeProject的一个示例代码为例,讲解一下用Autofac实现依赖注入的基本步骤,下面是代码:
1usingSystem;
2usingSystem.Collections.Generic;
3usingSystem.Linq;
4usingSystem.IO;
5usingAutofac;
6
7namespaceRemember
8{
9interfaceIMemoDueNotifier
10{
11voidMemoIsDue(Memomemo);
12}
13
14classMemo
15{
16publicstringTitle{get;set;}
17publicDateTimeDueAt{get;set;}
18}
19
20classMemoChecker
21{
22readonlyIList<Memo>_memos;
23readonlyIMemoDueNotifier_notifier;
24
25publicMemoChecker(IList<Memo>memos,IMemoDueNotifiernotifier)
26{
27_memos=memos;
28_notifier=notifier;
29}
30
31publicvoidCheckNow()
32{
33varoverdueMemos=_memos.Where(memo=>memo.DueAt<DateTime.Now);
34
35foreach(varmemoinoverdueMemos)
36_notifier.MemoIsDue(memo);
37}
38}
39
40classPrintingNotifier:IMemoDueNotifier
41{
42readonlyTextWriter_writer;
43
44publicPrintingNotifier(TextWriterwriter)
45{
46_writer=writer;
47}
48
49publicvoidMemoIsDue(Memomemo)
50{
51_writer.WriteLine("Memo'{0}'isdue!",memo.Title);
52}
53}
54
55classProgram
56{
57staticvoidMain()
58{
59varmemos=newList<Memo>{
60newMemo{Title="ReleaseAutofac1.1",
61DueAt=newDateTime(2007,03,12)},
62newMemo{Title="UpdateCodeProjectArticle",
63DueAt=DateTime.Now},
64newMemo{Title="ReleaseAutofac3",
65DueAt=newDateTime(2011,07,01)}
66};
67
68varbuilder=newContainerBuilder();
69builder.Register(c=>newMemoChecker(
70c.Resolve<IList<Memo>>(),c.Resolve<IMemoDueNotifier>()));
71builder.RegisterType<PrintingNotifier>().As<IMemoDueNotifier>();
72builder.RegisterInstance(memos).As<IList<Memo>>();
73
74builder.RegisterInstance(Console.Out)
75.As<TextWriter>()
76.ExternallyOwned();
77
78using(varcontainer=builder.Build())
79{
80container.Resolve<MemoChecker>().CheckNow();
81}
82
83Console.WriteLine("Done!Pressanykey.");
84Console.ReadKey();
85}
86}
87}
88
这个程序的作用是检查所有的记事项,提醒用户这些过期的记事项。这个程序里最主要的类是MemoChecker,MemoChecker需要两个对象才能构建一个实例—Memo和IMemoDueNotifier。而这两个类型的对象,是由autofac自行解析的,autofac知道如何找到一个接口是由哪个对象实现的—这个过程叫做Resolve。而接口和实现接口对象的对映关系是由程序员在配置文件app.config,或者自己在程序的入口处(例如Main函数)注册好的—这个过程叫Register。因为实现接口的某些对象,有可能它的构造函数也会接受其他接口,而实现这些接口的对象也需要解析。因此,Autofac将所有的接口,和实现接口的对象都放到一个容器里,这个容器自己解析实现接口的对象之间的依赖关系—也就是ContainerBuilder。ContainerBuilder在Build的过程中,通过多次调用Resolve解决容器内部的对象依赖关系。当依赖关系都解析完毕以后,以后要创建对象,不需要再用类似下面的代码显式创建了:
varbuilder=newMemoChecker();
创建对象的工作,全部都交给Container解决,Container自己在内部找到构造对象时,Container创建调用构造函数要用到的参数的对象,解决对象之间的依赖关系,然后你只要用类似下面的代码就可以获取到你要的对象:
varbuilder=container.Resolve<MemoChecker>();
使用Autofac基于配置文件实现依赖注入
前面讲到的依赖注入,还是基于代码的,很多时候,使用Ioc和依赖注入技术,主要是为了支持插件技术。比如说,其他插件只要实现了定义的接口,那么,终端用户理论上可以只通过将实现插件的assembly拷贝到程序文件夹,并修改配置文件的形式来无缝集成新的插件。
那我们来看Autofac自带的例子—Calculator。这个程序有三个Assembly组成,Calculator是那个支持插件的程序;Calculator.Api包括了接口的定义,这样,Calculator和它的插件通过引用这个Assembly,就可以实现相互交互了;而Calculator.Operations就是最后实现接口的一些插件。
我们来看一看代码:
Calculator.Api定义了一个接口—这个接口将会被Calculator(支持插件的程序)和Calculator.Operations(插件)所使用:
1usingSystem;
2
3namespaceCalculator.Api
4{
5publicinterfaceIOperation
6{
7stringOperator
8{
9get;
10}
11
12doubleApply(doublelhs,doublerhs);
13}
14}
15
而在Calculator这个Assembly里,定义了一个Calculator这个类,枚举所有实现了IOperation的插件—这个枚举过程由Autofac自动完成:
1usingSystem;
2usingSystem.Collections.Generic;
3usingSystem.Linq;
4usingSystem.Text;
5usingCalculator.Api;
6
7namespaceCalculator
8{
9classCalculator
10{
11IDictionary<string,IOperation>_operations=newDictionary<string,IOperation>();
12
13publicCalculator(IEnumerable<IOperation>operations)
14{
15if(operations==null)
16thrownewArgumentNullException("operations");
17
18foreach(IOperationopinoperations)
19_operations.Add(op.Operator,op);
20}
21
22publicIEnumerable<string>AvailableOperators
23{
24get
25{
26return_operations.Keys;
27}
28}
29
30publicdoubleApplyOperator(stringop,doublelhs,doublerhs)
31{
32if(op==null)
33thrownewArgumentNullException("op");
34
35IOperationoperation;
36if(!_operations.TryGetValue(op,outoperation))
37thrownewArgumentException("Unsupportedoperation.");
38
39returnoperation.Apply(lhs,rhs);
40}
41}
42}
43
请注意Calculator的构造函数,这个构造函数接受一个IEnumerable<IOperation>类型的参数,这个参数是autofac通过读取配置文件自动构建好一个实例,下面就是app.config文件里的具体设置:
1<?xmlversion="1.0"?>
2<configuration>
3<configSections>
4<sectionname="calculator"type="Autofac.Configuration.SectionHandler,Autofac.Configuration"/>
5</configSections>
6
7<calculatordefaultAssembly="Calculator.Api">
8<components>
9<componenttype="Calculator.Operations.Add,Calculator.Operations"member-of="operations"/>
10<componenttype="Calculator.Operations.Multiply,Calculator.Operations"member-of="operations"/>
11
12<componenttype="Calculator.Operations.Divide,Calculator.Operations"member-of="operations">
13<parameters>
14<parametername="places"value="4"/>
15</parameters>
16</component>
17
18</components>
19</calculator>
20
21</configuration>
22
在程序(Calculator)启动的时候,调用Autofac API里面的ContainerBuilder.RegisterModule来告诉Autofac读取配置文件里的接口与实现接口对象的映射关系。
1namespaceCalculator
2{
3
4staticclassProgram
5{
6[STAThread]
7staticvoidMain()
8{
9try
10{
11varbuilder=newContainerBuilder();
12
13...
14
15builder.RegisterModule(newConfigurationSettingsReader("calculator"));
16
17...
18}
19catch(Exceptionex)
20{
21DisplayException(ex);
22}
23}
24}
25}
26
上节说了一下基本的理论知识,例子可能不太好,不过无所谓了,目的是要让大家明白啥是依赖倒置和依赖注入,目的就达到了,简单一句话,这2玩意都是用来解耦合的。
不过依赖倒置这个词哥哥真不敢苟同,哥哥来个颠覆的说法,我说这是依赖正置。
因为本来就应该是上层依赖上层嘛,低层也应该依赖上层,但是由于程序语言的原因,导致代码和实际完全不符合,搞得抽象经常依赖具体,具体更是依赖具体。
体现在代码中就是接口中关联类型,类型中也关联类型。完全反了。所以我们呢要让他正常起来,让接口只依赖接口,类也只依赖接口,这个实际比较相符合,所以哥哥我叫他依赖正置。
说了依赖正置,接下来再说说控制反转IOC,我们要说的autofac就是是IOC的一个框架。
啥是控制反转吧?你想啊,我们一般写代码是这样:举个去死的栗子
public interface IPerson{ void GoToHell(IPlace hell);}public class CodeFarmer : IPerson{ public void GoToHell(IPlace hell) { hell.Accept(this); }}public interface IPlace{ void Accept(IPerson person);}public class Hell : IPlace{ public void Accept(IPerson person) { // do sth.. }}public class God{ public void Do() { IPerson you = new CodeFarmer(); you.GoToHell(new Hell()); }}
看,咱们是没有违反基本的DIP原则吧,基本都是依据接口编程的。
但是还是老话,在客户端god哪里还是有问题,他要你去死,还必须要知道hell的创建逻辑,god表示你这东西简直不能用,我只是要你去死而已呀,我还要给你指明通向hell的道路?
god表示很忙!把hell的初始化放在person 中如何?可以,不是有句话说,人一出生,就是坐上了通往死亡的列车,谁说的?好像是我自己!!
public interface IPerson{ IPlace Hell { get; set; } void GoToHell();}public class CodeFarmer : IPerson{ public IPlace Hell { get; set; } public CodeFarmer() { Hell = new Hell(); } public void GoToHell() { Hell.Accept(this); }}public interface IPlace{ void Accept(IPerson person);}public class Hell : IPlace{ public void Accept(IPerson person) { // do sth.. }}public class God{ public void Do() { IPerson you = new CodeFarmer(); you.GoToHell(); }}
这样,你自己往hell走就行了,god表示我是老板,我只发指令给你,具体的路线你自己去搞!
但是CodeFarmer就不忙?最后要死了都要自己找路去死?简直不能干,这个行业!那怎么办?这个new Hell()的部分放在那里好呢?
其实这个就是我们程序里面常见的问题,依赖的具体对象创建到哪里注入比较好呢?好像哪里都不符合逻辑!
那既然这样,那创世者说我发个公告牌如何?我就告诉你,这个地方有个公告板,里面告诉你了地狱怎么走,你要去的话,你自己去看看就得了,没必要自己摸路。
代码如下:
public interface IPerson{ void GoToHell(IPlace hell);}public class CodeFarmer : IPerson{ public void GoToHell(IPlace hell) { hell.Accept(this); }}public interface IPlace{ void Accept(IPerson person);}public class Hell : IPlace{ public void Accept(IPerson person) { // do sth.. }}public class God{ public void Do() { IPerson you = InfoBoard.PlaceInfo["person"]; you.GoToHell(InfoBoard.PlaceInfo["hell"]); }}public class InfoBoard{ public static Dictionary<string, IPlace> PlaceInfo = new Dictionary<string, IPlace>(); static InfoBoard() { PlaceInfo.Add("hell", new Hell());PlaceInfo.Add("person", new CodeFarmer()); }}
这样,不管是在god中,还是在codefamer中,直接可以使用InfoBoard.PlaceInfo["hell"]来去到通往地狱的路,有指明灯多好啊。
比较官方的语言:应用控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用,传递给它。也可以说,依赖被注入到对象中。所以,控制反转是,关于一个对象如何获取他所依赖的对象的引用,这个责任的反转。
怎么样,真jb难懂是吧?
我就给你翻译成人类能懂的话,你不是要面向接口编程吗?你接口,你TM最后总要有new的时候吧?你一旦new了,一个对象和另一个对象实际上的强依赖是不是产生了?既然这样,那我们都不new,由一个在程序看来虚无缥缈的地方保存有所有抽象类和接口对象的具体实现。从哪里取就行了,这样可好?那我和你永远只关心接口就行了
就像上面的栗子中,不管是god中还是codefarmer还是hell中,是不是没有任何创建彼此的代码?是不是这个耦合彻底解除了(或者说是转移了到InfoBoard中了,便于集中管理,前文有提到)?
我告诉你,这个就是一个最简单的ioc容器。控制反转,我还是那句话,我自己给他定义一个名词,控制转移比较好!谈不上什么正反,这个!!
那有的同学问,你TM这用你昨天讲的反射不是可以达到一样的效果?你说你玩这个,你玩他有什么用啊?我想说的是,就是可以达到一样的效果!!但是,我只多说一个词,性能!其他的我不多讲。如果你的系统没那么多jjyy的性能的事情,完全用反射就行了
那IOC容器就这样就行了?肯定不行啊,举个栗子:
我们用ado访问数据库
public class InfoBoard{ public static Dictionary<string, object> PlaceInfo = new Dictionary<string, object>(); static InfoBoard() { PlaceInfo.Add("conn", new SqlConnection("connect string")); PlaceInfo.Add("comm", new SqlCommand("SELECT *****************", InfoBoard.PlaceInfo["conn"] as SqlConnection)); }}
看如果我执行一次comm后实际上SqlConnection如果释放了,这个comm是不是没用了?
如果要保证comm有效这个SqlConnection是不是不能释放,这2种方式都不是我们想要的,所以,你必须要有个对象过期策略,和非托管资源的释放问题。
这个SqlConnection根被就不能写在静态构造中,实际的ioc框架还会遇到很多问题,这就是一个autofac代码茫茫多的原因
西门子哪个广告语,我一直拿着用,精于心,简于形,为了一点点的性能问题,背后的工作是巨大的,还好有现成的框架帮我们做了很多事情。
下面说下Autofac
下载,引用,或者通过NUGET方式获取框架,这些不多说。正常人都能搞定
using Autofac;public interface IPerson{ IPlace Hell { get; set; } string Name { get; set; } void GoToHell();}public class CodeFarmer : IPerson{ public string Name { get; set; } public IPlace Hell { get; set; } public CodeFarmer(IPlace Hell) { this.Hell = Hell; } public void GoToHell() { Hell.Accept(this); }}public interface IPlace{ void Accept(IPerson person);}public class Hell : IPlace{ public void Accept(IPerson person) { Console.WriteLine(person.Name+" is gonna die"); Console.ReadKey(); }}class Program{ private static IContainer Container { get; set; } static void InitApplication() { var builder = new ContainerBuilder(); builder.RegisterType<Hell>().As<IPlace>(); builder.RegisterType<CodeFarmer>().As<IPerson>(); Container = builder.Build(); } static void Main(string[] args) { InitApplication(); var aSadMan = Container.Resolve<IPerson>(); aSadMan.Name = "hard worker"; //var goodPlace = Container.Resolve<IPlace>(); aSadMan.GoToHell(); }}
主要看Program
private static IContainer Container { get; set; }
有个Container容器有没有?
builder.RegisterType<Hell>().As<IPlace>();
builder.RegisterType<CodeFarmer>().As<IPerson>();
往容器里面加东西有没有?
var aSadMan = Container.Resolve<IPerson>();
从容器里面取东西有没有?
那有的人就问了,你CodeFarmer里面的IPlace对应的hell是怎么实例化的?CodeFarmer的构造函数都没看到调用啊?
这里就是所谓的构造函数注入,你只需要知道,这样写,然后解析他的上层类IPerson,这个hell是自动实例化的,够造函数自动被调用了,里面的参数自动被解析成hell,因为你前面有往container中registertype过,这样就行了,是不是很强大呢?
public IPlace Hell { get; set; } public CodeFarmer(IPlace Hell) { this.Hell = Hell; }
如果Hell的构造里面还要注入其他的依赖,这个解析可以一直嵌套下去,无论有多少层,只要你从最上面的入口做了类似
var aSadMan = Container.Resolve<IPerson>();
的操作!
行了,先说到这,下回再扯吧,欢迎拍砖,往死里拍,上文有个错误,把依赖倒置说成了DI应该是dip,汗!因为,我写这些,例子都是我自己随便想的,基本上时间也仓促,难免有不完全对的地方,但是核心我都说明白了的,大家有问题可以提出来探讨,就是不要直接说有问题,但是不说明问题再那里,这样就没意思了!基本我都不会检查第2遍。人都说第一想到的东西都是最真实和正确的,有没有?
更多阅读
避孕套的正确使用方法图解 如何使用避孕套图解
避孕套的正确使用方法(图解)——简介正确的使用避孕套才不会导致避孕失败避孕套的正确使用方法(图解)——方法/步骤避孕套的正确使用方法(图解) 1、选择适合自己的避孕套,不能过大或过小。打开包装向避孕套内吹气,如果漏气就说明这个套套破
梦幻诛仙活动任务赏金猎人的简单流程 梦幻诛仙赏金任务
赏金猎人,该活动真的很简单,经验还可以吧就是消耗时间。梦幻诛仙活动任务赏金猎人的简单流程——步骤/方法梦幻诛仙活动任务赏金猎人的简单流程 1、活动领取的NPC:念裳 坐标在进入混沌时空附近梦幻诛仙活动任务赏金猎人的简单流程 2
3dmax中光域网简单使用 3dmax光域网不亮
3dmax中光域网简单使用——简介光域网分布(WebDistribution) 方式通过指定光域网文件来描述灯光亮度的分布状况。光域网是一种关于光源亮度分布的三维表现形式,存储于IES文件当中。这种文件通常可以从灯光的制造厂商那里获得,格式主要
睡眠面膜:蜂胶睡美睡眠面膜的正确使用方法
睡眠面膜:蜂胶睡美睡眠面膜的正确使用方法——简介?为了保持美白肌肤,爱美MM们一天到晚都在做着护肤工作,夜间也会使用睡眠面膜来塑造自己的美;蜂胶睡美睡眠面膜进入人们的视线后为爱美人士夜间护理带来了很大的方便,不少朋友也因为肌肤
无线路由器的正确使用方法 眼霜的正确使用方法
无线路由器的正确使用方法——简介现代是互联网高速发展的时代,随着互联网的普及,很多人的家里都装上了台式电脑,然而,电脑需要联网才能正常使用,这其中就提到了电脑要用路由器来共享上网,这对新手来说,是一个难题,但这对小编来说,是非常简单